From bd62b5077a9a7e693f2c07ddae881d95e84b0e0f Mon Sep 17 00:00:00 2001 From: Ivo Nascimento Date: Tue, 29 Jul 2014 21:16:48 -0300 Subject: [PATCH 001/456] FIX Continue Header line breaker closereactphp/react#308 --- src/Response.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Response.php b/src/Response.php index 13b22211..36e375fc 100644 --- a/src/Response.php +++ b/src/Response.php @@ -43,7 +43,7 @@ public function writeContinue() throw new \Exception('Response head has already been written.'); } - $this->conn->write("HTTP/1.1 100 Continue\r\n"); + $this->conn->write("HTTP/1.1 100 Continue\r\n\r\n"); } public function writeHead($status = 200, array $headers = array()) From 2e39caf42d7eb9e54983df144c92e0cb013e58f8 Mon Sep 17 00:00:00 2001 From: Ivo Nascimento Date: Tue, 29 Jul 2014 21:31:02 -0300 Subject: [PATCH 002/456] update Response test --- tests/ResponseTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ResponseTest.php b/tests/ResponseTest.php index 2ca0a991..7692fb83 100644 --- a/tests/ResponseTest.php +++ b/tests/ResponseTest.php @@ -93,7 +93,7 @@ public function writeContinueShouldSendContinueLineBeforeRealHeaders() $conn ->expects($this->at(3)) ->method('write') - ->with("HTTP/1.1 100 Continue\r\n"); + ->with("HTTP/1.1 100 Continue\r\n\r\n"); $conn ->expects($this->at(4)) ->method('write') From a2bad36f1438f2420ce1ddc5c7fdccea14f0bd61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ste=CC=81phane=20Goetz?= Date: Wed, 8 Oct 2014 22:45:25 +0200 Subject: [PATCH 003/456] Rename RequestHeaderParser to RequestParser Future change will be made and the class will become a body parser as well --- ...RequestHeaderParser.php => RequestParser.php} | 2 +- src/Server.php | 2 +- ...eaderParserTest.php => RequestParserTest.php} | 16 ++++++++-------- 3 files changed, 10 insertions(+), 10 deletions(-) rename src/{RequestHeaderParser.php => RequestParser.php} (96%) rename tests/{RequestHeaderParserTest.php => RequestParserTest.php} (91%) diff --git a/src/RequestHeaderParser.php b/src/RequestParser.php similarity index 96% rename from src/RequestHeaderParser.php rename to src/RequestParser.php index 39bc127a..15b06c29 100644 --- a/src/RequestHeaderParser.php +++ b/src/RequestParser.php @@ -9,7 +9,7 @@ * @event headers * @event error */ -class RequestHeaderParser extends EventEmitter +class RequestParser extends EventEmitter { private $buffer = ''; private $maxSize = 4096; diff --git a/src/Server.php b/src/Server.php index 4bf6cc28..d58d2894 100644 --- a/src/Server.php +++ b/src/Server.php @@ -20,7 +20,7 @@ public function __construct(SocketServerInterface $io) // TODO: chunked transfer encoding (also for outgoing data) // TODO: multipart parsing - $parser = new RequestHeaderParser(); + $parser = new RequestParser(); $parser->on('headers', function (Request $request, $bodyBuffer) use ($conn, $parser) { // attach remote ip to the request as metadata $request->remoteAddress = $conn->getRemoteAddress(); diff --git a/tests/RequestHeaderParserTest.php b/tests/RequestParserTest.php similarity index 91% rename from tests/RequestHeaderParserTest.php rename to tests/RequestParserTest.php index dd3a0cbe..814bbe4d 100644 --- a/tests/RequestHeaderParserTest.php +++ b/tests/RequestParserTest.php @@ -2,13 +2,13 @@ namespace React\Tests\Http; -use React\Http\RequestHeaderParser; +use React\Http\RequestParser; -class RequestHeaderParserTest extends TestCase +class RequestParserTest extends TestCase { public function testSplitShouldHappenOnDoubleCrlf() { - $parser = new RequestHeaderParser(); + $parser = new RequestParser(); $parser->on('headers', $this->expectCallableNever()); $parser->feed("GET / HTTP/1.1\r\n"); @@ -23,7 +23,7 @@ public function testSplitShouldHappenOnDoubleCrlf() public function testFeedInOneGo() { - $parser = new RequestHeaderParser(); + $parser = new RequestParser(); $parser->on('headers', $this->expectCallableOnce()); $data = $this->createGetRequest(); @@ -35,7 +35,7 @@ public function testHeadersEventShouldReturnRequestAndBodyBuffer() $request = null; $bodyBuffer = null; - $parser = new RequestHeaderParser(); + $parser = new RequestParser(); $parser->on('headers', function ($parsedRequest, $parsedBodyBuffer) use (&$request, &$bodyBuffer) { $request = $parsedRequest; $bodyBuffer = $parsedBodyBuffer; @@ -59,7 +59,7 @@ public function testHeadersEventShouldReturnBinaryBodyBuffer() { $bodyBuffer = null; - $parser = new RequestHeaderParser(); + $parser = new RequestParser(); $parser->on('headers', function ($parsedRequest, $parsedBodyBuffer) use (&$bodyBuffer) { $bodyBuffer = $parsedBodyBuffer; }); @@ -75,7 +75,7 @@ public function testHeadersEventShouldParsePathAndQueryString() { $request = null; - $parser = new RequestHeaderParser(); + $parser = new RequestParser(); $parser->on('headers', function ($parsedRequest, $parsedBodyBuffer) use (&$request) { $request = $parsedRequest; }); @@ -100,7 +100,7 @@ public function testHeaderOverflowShouldEmitError() { $error = null; - $parser = new RequestHeaderParser(); + $parser = new RequestParser(); $parser->on('headers', $this->expectCallableNever()); $parser->on('error', function ($message) use (&$error) { $error = $message; From 244543041d3c1e39b76cd948aa231e2d41b3b252 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ste=CC=81phane=20Goetz?= Date: Wed, 8 Oct 2014 22:59:47 +0200 Subject: [PATCH 004/456] Added POST and Multipart handling - fixed the header overflow - added the body to the Request - parse POST form the body content or as multipart/form-data - handle file uploads - use Content-Length to know when the content arrived --- src/MultipartParser.php | 193 ++++++++++++++++++++++++++++++++++ src/Request.php | 52 ++++++++- src/RequestParser.php | 109 ++++++++++++++++--- src/Server.php | 3 +- tests/MultipartParserTest.php | 99 +++++++++++++++++ tests/RequestParserTest.php | 191 +++++++++++++++++++++++++++++++-- tests/ServerTest.php | 2 +- 7 files changed, 620 insertions(+), 29 deletions(-) create mode 100644 src/MultipartParser.php create mode 100644 tests/MultipartParserTest.php diff --git a/src/MultipartParser.php b/src/MultipartParser.php new file mode 100644 index 00000000..681bf231 --- /dev/null +++ b/src/MultipartParser.php @@ -0,0 +1,193 @@ +input = $input; + $this->boundary = $boundary; + } + + /** + * @return array + */ + public function getPost() + { + return $this->post; + } + + /** + * @return array + */ + public function getFiles() + { + return $this->files; + } + + /** + * Do the actual parsing + */ + public function parse() + { + $blocks = $this->split($this->boundary); + + foreach ($blocks as $value) { + if (empty($value)) { + continue; + } + + $this->parseBlock($value); + } + } + + /** + * @param $boundary string + * @returns Array + */ + protected function split($boundary) + { + $boundary = preg_quote($boundary); + $result = preg_split("/\\-+$boundary/", $this->input); + array_pop($result); + return $result; + } + + /** + * Decide if we handle a file, post value or octet stream + * + * @param $string string + * @returns void + */ + protected function parseBlock($string) + { + if (strpos($string, 'filename') !== false) { + $this->file($string); + return; + } + + // This may never be called, if an octet stream + // has a filename it is catched by the previous + // condition already. + if (strpos($string, 'application/octet-stream') !== false) { + $this->octetStream($string); + return; + } + + $this->post($string); + } + + /** + * Parse a raw octet stream + * + * @param $string + * @return array + */ + protected function octetStream($string) + { + preg_match('/name=\"([^\"]*)\".*stream[\n|\r]+([^\n\r].*)?$/s', $string, $match); + + $this->addResolved('post', $match[1], $match[2]); + } + + /** + * Parse a file + * + * @param $string + * @return array + */ + protected function file($string) + { + preg_match('/name=\"([^\"]*)\"; filename=\"([^\"]*)\"[\n|\r]+([^\n\r].*)?\r$/s', $string, $match); + preg_match('/Content-Type: (.*)?/', $match[3], $mime); + + $content = preg_replace('/Content-Type: (.*)[^\n\r]/', '', $match[3]); + $content = ltrim($content, "\r\n"); + + $path = tempnam(sys_get_temp_dir(), "php"); + $err = file_put_contents($path, $content); + + $data = [ + 'name' => $match[2], + 'type' => trim($mime[1]), + 'tmp_name' => $path, + 'error' => ($err === false) ? UPLOAD_ERR_NO_FILE : UPLOAD_ERR_OK, + 'size' => filesize($path), + ]; + + $this->addResolved('files', $match[1], $data); + } + + /** + * Parse POST values + * + * @param $string + * @return array + */ + protected function post($string) + { + preg_match('/name=\"([^\"]*)\"[\n|\r]+([^\n\r].*)?\r$/s', $string, $match); + + $this->addResolved('post', $match[1], $match[2]); + } + + /** + * Put the file or post where it belongs, + * The key names can be simple, or containing [] + * it can also be a named key + * + * @param $type + * @param $key + * @param $content + */ + protected function addResolved($type, $key, $content) + { + if (preg_match('/^(.*)\[(.*)\]$/i', $key, $tmp)) { + if (!empty($tmp[2])) { + $this->{$type}[$tmp[1]][$tmp[2]] = $content; + } else { + $this->{$type}[$tmp[1]][] = $content; + } + } else { + $this->{$type}[$key] = $content; + } + } +} diff --git a/src/Request.php b/src/Request.php index 605b909e..ddd62ad8 100644 --- a/src/Request.php +++ b/src/Request.php @@ -11,21 +11,25 @@ class Request extends EventEmitter implements ReadableStreamInterface { private $readable = true; private $method; - private $path; + private $url; private $query; private $httpVersion; private $headers; + private $body; + private $post = []; + private $files = []; // metadata, implicitly added externally public $remoteAddress; - public function __construct($method, $path, $query = array(), $httpVersion = '1.1', $headers = array()) + public function __construct($method, $url, $query = array(), $httpVersion = '1.1', $headers = array(), $body = '') { $this->method = $method; - $this->path = $path; + $this->url = $url; $this->query = $query; $this->httpVersion = $httpVersion; $this->headers = $headers; + $this->body = $body; } public function getMethod() @@ -35,7 +39,12 @@ public function getMethod() public function getPath() { - return $this->path; + return $this->url['path']; + } + + public function getUrl() + { + return $this->url; } public function getQuery() @@ -53,6 +62,41 @@ public function getHeaders() return $this->headers; } + public function getBody() + { + return $this->body; + } + + public function setBody($body) + { + $this->body = $body; + } + + public function getFiles() + { + return $this->files; + } + + public function setFiles($files) + { + $this->files = $files; + } + + public function getPost() + { + return $this->post; + } + + public function setPost($post) + { + $this->post = $post; + } + + public function getRemoteAddress() + { + return $this->remoteAddress; + } + public function expectsContinue() { return isset($this->headers['Expect']) && '100-continue' === $this->headers['Expect']; diff --git a/src/RequestParser.php b/src/RequestParser.php index 15b06c29..c33c1fc2 100644 --- a/src/RequestParser.php +++ b/src/RequestParser.php @@ -14,44 +14,127 @@ class RequestParser extends EventEmitter private $buffer = ''; private $maxSize = 4096; + /** + * @var Request + */ + private $request; + private $length = 0; + public function feed($data) { - if (strlen($this->buffer) + strlen($data) > $this->maxSize) { - $this->emit('error', array(new \OverflowException("Maximum header size of {$this->maxSize} exceeded."), $this)); + $this->buffer .= $data; + + if (!$this->request && false !== strpos($this->buffer, "\r\n\r\n")) { + + // Extract the header from the buffer + // in case the content isn't complete + list($headers, $this->buffer) = explode("\r\n\r\n", $this->buffer, 2); + + // Fail before parsing if the + if (strlen($headers) > $this->maxSize) { + $this->headerSizeExceeded(); + return; + } + $this->request = $this->parseHeaders($headers . "\r\n\r\n"); + } + + // if there is a request (meaning the headers are parsed) and + // we have the right content size, we can finish the parsing + if ($this->request && $this->isRequestComplete()) { + $this->parseBody(substr($this->buffer, 0, $this->length)); + $this->finishParsing(); return; } - $this->buffer .= $data; + // fail if the header hasn't finished but it is already too large + if (!$this->request && strlen($this->buffer) > $this->maxSize) { + $this->headerSizeExceeded(); + return; + } + } + + protected function isRequestComplete() + { + $headers = $this->request->getHeaders(); - if (false !== strpos($this->buffer, "\r\n\r\n")) { - list($request, $bodyBuffer) = $this->parseRequest($this->buffer); + // if there is no content length, there should + // be no content so we can say it's done + if (!array_key_exists('Content-Length', $headers)) { + return true; + } + + // if the content is present and has the + // right length, we're good to go + if (array_key_exists('Content-Length', $headers) && strlen($this->buffer) >= $headers['Content-Length']) { - $this->emit('headers', array($request, $bodyBuffer)); - $this->removeAllListeners(); + // store the expected content length + $this->length = $this->request->getHeaders()['Content-Length']; + + return true; } + + return false; } - public function parseRequest($data) + protected function finishParsing() { - list($headers, $bodyBuffer) = explode("\r\n\r\n", $data, 2); + $this->emit('headers', array($this->request, $this->request->getBody())); + $this->removeAllListeners(); + $this->request = null; + } + + protected function headerSizeExceeded() + { + $this->emit('error', array(new \OverflowException("Maximum header size of {$this->maxSize} exceeded."), $this)); + } + public function parseHeaders($data) + { $parser = new MessageParser(); - $parsed = $parser->parseRequest($headers."\r\n\r\n"); + $parsed = $parser->parseRequest($data); $parsedQuery = array(); if ($parsed['request_url']['query']) { parse_str($parsed['request_url']['query'], $parsedQuery); } - $request = new Request( + return new Request( $parsed['method'], - $parsed['request_url']['path'], + $parsed['request_url'], $parsedQuery, $parsed['version'], $parsed['headers'] ); + } + + public function parseBody($content) + { + $headers = $this->request->getHeaders(); + + if (array_key_exists('Content-Type', $headers)) { + if (strpos($headers['Content-Type'], 'multipart/') === 0) { + preg_match("/boundary=\"?(.*)\"?$/", $headers['Content-Type'], $matches); + $boundary = $matches[1]; + + $parser = new MultipartParser($content, $boundary); + $parser->parse(); + + $this->request->setPost($parser->getPost()); + $this->request->setFiles($parser->getFiles()); + return; + } + + if (strtolower($headers['Content-Type']) == 'application/x-www-form-urlencoded') { + parse_str(urldecode($content), $result); + $this->request->setPost($result); + + return; + } + } + + - return array($request, $bodyBuffer); + $this->request->setBody($content); } } diff --git a/src/Server.php b/src/Server.php index d58d2894..5e420a40 100644 --- a/src/Server.php +++ b/src/Server.php @@ -15,10 +15,9 @@ public function __construct(SocketServerInterface $io) { $this->io = $io; - $this->io->on('connection', function ($conn) { + $this->io->on('connection', function (ConnectionInterface $conn) { // TODO: http 1.1 keep-alive // TODO: chunked transfer encoding (also for outgoing data) - // TODO: multipart parsing $parser = new RequestParser(); $parser->on('headers', function (Request $request, $bodyBuffer) use ($conn, $parser) { diff --git a/tests/MultipartParserTest.php b/tests/MultipartParserTest.php new file mode 100644 index 00000000..0bffd5c3 --- /dev/null +++ b/tests/MultipartParserTest.php @@ -0,0 +1,99 @@ +parse(); + + $this->assertEmpty($parser->getFiles()); + $this->assertEquals( + ['users' => ['one' => 'single', 'two' => 'second']], + $parser->getPost() + ); + } + + public function testFileUpload() { + + $file = base64_decode("R0lGODlhAQABAIAAAP///wAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw=="); + + $boundary = "---------------------------12758086162038677464950549563"; + + $data = "--$boundary\r\n"; + $data .= "Content-Disposition: form-data; name=\"user\"\r\n"; + $data .= "\r\n"; + $data .= "single\r\n"; + $data .= "--$boundary\r\n"; + $data .= "Content-Disposition: form-data; name=\"user2\"\r\n"; + $data .= "\r\n"; + $data .= "second\r\n"; + $data .= "--$boundary\r\n"; + $data .= "Content-Disposition: form-data; name=\"users[]\"\r\n"; + $data .= "\r\n"; + $data .= "first in array\r\n"; + $data .= "--$boundary\r\n"; + $data .= "Content-Disposition: form-data; name=\"users[]\"\r\n"; + $data .= "\r\n"; + $data .= "second in array\r\n"; + $data .= "--$boundary\r\n"; + $data .= "Content-Disposition: form-data; name=\"file\"; filename=\"User.php\"\r\n"; + $data .= "Content-Type: text/php\r\n"; + $data .= "\r\n"; + $data .= "parse(); + + $this->assertEquals(2, count($parser->getFiles())); + $this->assertEquals(2, count($parser->getFiles()['files'])); + $this->assertEquals( + ['user' => 'single', 'user2' => 'second', 'users' => ['first in array', 'second in array']], + $parser->getPost() + ); + + $uploaded_blank = $parser->getFiles()['files'][0]; + + $this->assertEquals($file, file_get_contents($uploaded_blank['tmp_name'])); + + $uploaded_blank['tmp_name'] = 'file'; //override the filename as it is random + $expected_file = [ + 'name' => 'blank.gif', + 'type' => 'image/gif', + 'tmp_name' => 'file', + 'error' => 0, + 'size' => 43, + ]; + + $this->assertEquals($expected_file, $uploaded_blank); + } +} diff --git a/tests/RequestParserTest.php b/tests/RequestParserTest.php index 814bbe4d..728166b3 100644 --- a/tests/RequestParserTest.php +++ b/tests/RequestParserTest.php @@ -41,8 +41,7 @@ public function testHeadersEventShouldReturnRequestAndBodyBuffer() $bodyBuffer = $parsedBodyBuffer; }); - $data = $this->createGetRequest(); - $data .= 'RANDOM DATA'; + $data = $this->createGetRequest('RANDOM DATA', 11); $parser->feed($data); $this->assertInstanceOf('React\Http\Request', $request); @@ -50,7 +49,10 @@ public function testHeadersEventShouldReturnRequestAndBodyBuffer() $this->assertSame('/', $request->getPath()); $this->assertSame(array(), $request->getQuery()); $this->assertSame('1.1', $request->getHttpVersion()); - $this->assertSame(array('Host' => 'example.com:80', 'Connection' => 'close'), $request->getHeaders()); + $this->assertSame( + array('Host' => 'example.com:80', 'Connection' => 'close', 'Content-Length' => '11'), + $request->getHeaders() + ); $this->assertSame('RANDOM DATA', $bodyBuffer); } @@ -64,8 +66,7 @@ public function testHeadersEventShouldReturnBinaryBodyBuffer() $bodyBuffer = $parsedBodyBuffer; }); - $data = $this->createGetRequest(); - $data .= "\0x01\0x02\0x03\0x04\0x05"; + $data = $this->createGetRequest("\0x01\0x02\0x03\0x04\0x05", strlen("\0x01\0x02\0x03\0x04\0x05")); $parser->feed($data); $this->assertSame("\0x01\0x02\0x03\0x04\0x05", $bodyBuffer); @@ -96,6 +97,73 @@ public function testHeadersEventShouldParsePathAndQueryString() $this->assertSame($headers, $request->getHeaders()); } + public function testShouldReceiveBodyContent() + { + $content1 = "{\"test\":"; $content2 = " \"value\"}"; + + $request = null; + $body = null; + + $parser = new RequestParser(); + $parser->on('headers', function ($parsedRequest, $parsedBodyBuffer) use (&$request, &$body) { + $request = $parsedRequest; + $body = $parsedBodyBuffer; + }); + + $data = $this->createAdvancedPostRequest('', 17); + $parser->feed($data); + $parser->feed($content1); + $parser->feed($content2 . "\r\n"); + + $this->assertInstanceOf('React\Http\Request', $request); + $this->assertEquals($content1 . $content2, $request->getBody()); + $this->assertSame($body, $request->getBody()); + } + + public function testShouldReceiveMultiPartBody() + { + + $request = null; + $body = null; + + $parser = new RequestParser(); + $parser->on('headers', function ($parsedRequest, $parsedBodyBuffer) use (&$request, &$body) { + $request = $parsedRequest; + $body = $parsedBodyBuffer; + }); + + $parser->feed($this->createMultipartRequest()); + + $this->assertInstanceOf('React\Http\Request', $request); + $this->assertEquals( + $request->getPost(), + ['user' => 'single', 'user2' => 'second', 'users' => ['first in array', 'second in array']] + ); + $this->assertEquals(2, count($request->getFiles())); + $this->assertEquals(2, count($request->getFiles()['files'])); + } + + public function testShouldReceivePostInBody() + { + $request = null; + $body = null; + + $parser = new RequestParser(); + $parser->on('headers', function ($parsedRequest, $parsedBodyBuffer) use (&$request, &$body) { + $request = $parsedRequest; + $body = $parsedBodyBuffer; + }); + + $parser->feed($this->createPostWithContent()); + + $this->assertInstanceOf('React\Http\Request', $request); + $this->assertSame('', $body); + $this->assertEquals( + $request->getPost(), + ['user' => 'single', 'user2' => 'second', 'users' => ['first in array', 'second in array']] + ); + } + public function testHeaderOverflowShouldEmitError() { $error = null; @@ -113,23 +181,128 @@ public function testHeaderOverflowShouldEmitError() $this->assertSame('Maximum header size of 4096 exceeded.', $error->getMessage()); } - private function createGetRequest() + public function testOnePassHeaderTooLarge() { - $data = "GET / HTTP/1.1\r\n"; + $error = null; + + $parser = new RequestParser(); + $parser->on('headers', $this->expectCallableNever()); + $parser->on('error', function ($message) use (&$error) { + $error = $message; + }); + + $data = "POST /foo?bar=baz HTTP/1.1\r\n"; + $data .= "Host: example.com:80\r\n"; + $data .= "Cookie: " . str_repeat('A', 4097) . "\r\n"; + $data .= "\r\n"; + $parser->feed($data); + + $this->assertInstanceOf('OverflowException', $error); + $this->assertSame('Maximum header size of 4096 exceeded.', $error->getMessage()); + } + + public function testBodyShouldNotOverflowHeader() + { + $error = null; + + $parser = new RequestParser(); + $parser->on('headers', $this->expectCallableOnce()); + $parser->on('error', function ($message) use (&$error) { + $error = $message; + }); + + $data = str_repeat('A', 4097); + $parser->feed($this->createAdvancedPostRequest() . $data); + + $this->assertNull($error); + } + + private function createGetRequest($content = '', $len = 0) + { + $data = "GET / HTTP/1.1\r\n"; $data .= "Host: example.com:80\r\n"; $data .= "Connection: close\r\n"; + if($len) { + $data .= "Content-Length: $len\r\n"; + } $data .= "\r\n"; + $data .= $content; return $data; } - private function createAdvancedPostRequest() + private function createAdvancedPostRequest($content = '', $len = 0) { - $data = "POST /foo?bar=baz HTTP/1.1\r\n"; + $data = "POST /foo?bar=baz HTTP/1.1\r\n"; $data .= "Host: example.com:80\r\n"; $data .= "User-Agent: react/alpha\r\n"; $data .= "Connection: close\r\n"; + if($len) { + $data .= "Content-Length: $len\r\n"; + } + $data .= "\r\n"; + $data .= $content; + + return $data; + } + + private function createPostWithContent() + { + $data = "POST /foo?bar=baz HTTP/1.1\r\n"; + $data .= "Host: localhost:8080\r\n"; + $data .= "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.7; rv:32.0) Gecko/20100101 Firefox/32.0\r\n"; + $data .= "Connection: close\r\n"; + $data .= "Content-Type: application/x-www-form-urlencoded\r\n"; + $data .= "Content-Length: 79\r\n"; + $data .= "\r\n"; + $data .= "user=single&user2=second&users%5B%5D=first+in+array&users%5B%5D=second+in+array\r\n"; + + return $data; + } + + private function createMultipartRequest() + { + $data = "POST / HTTP/1.1\r\n"; + $data .= "Host: localhost:8080\r\n"; + $data .= "Connection: close\r\n"; + $data .= "Content-Type: multipart/form-data; boundary=---------------------------12758086162038677464950549563\r\n"; + $data .= "Content-Length: 1097\r\n"; + $data .= "\r\n"; + + $data .= "-----------------------------12758086162038677464950549563\r\n"; + $data .= "Content-Disposition: form-data; name=\"user\"\r\n"; + $data .= "\r\n"; + $data .= "single\r\n"; + $data .= "-----------------------------12758086162038677464950549563\r\n"; + $data .= "Content-Disposition: form-data; name=\"user2\"\r\n"; + $data .= "\r\n"; + $data .= "second\r\n"; + $data .= "-----------------------------12758086162038677464950549563\r\n"; + $data .= "Content-Disposition: form-data; name=\"users[]\"\r\n"; + $data .= "\r\n"; + $data .= "first in array\r\n"; + $data .= "-----------------------------12758086162038677464950549563\r\n"; + $data .= "Content-Disposition: form-data; name=\"users[]\"\r\n"; + $data .= "\r\n"; + $data .= "second in array\r\n"; + $data .= "-----------------------------12758086162038677464950549563\r\n"; + $data .= "Content-Disposition: form-data; name=\"file\"; filename=\"User.php\"\r\n"; + $data .= "Content-Type: text/php\r\n"; + $data .= "\r\n"; + $data .= "assertInstanceOf('React\Http\Request', $request); $this->assertSame('/', $request->getPath()); $this->assertSame('GET', $request->getMethod()); - $this->assertSame('127.0.0.1', $request->remoteAddress); + $this->assertSame('127.0.0.1', $request->getRemoteAddress()); $this->assertInstanceOf('React\Http\Response', $response); }); From 56ea21e71d8e07944749183c4946405e73dd82e9 Mon Sep 17 00:00:00 2001 From: Michael Arenzon Date: Sat, 29 Nov 2014 00:57:51 +0200 Subject: [PATCH 005/456] just for better ide completion --- src/Server.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Server.php b/src/Server.php index 4bf6cc28..e1abfd8e 100644 --- a/src/Server.php +++ b/src/Server.php @@ -15,7 +15,7 @@ public function __construct(SocketServerInterface $io) { $this->io = $io; - $this->io->on('connection', function ($conn) { + $this->io->on('connection', function (ConnectionInterface $conn) { // TODO: http 1.1 keep-alive // TODO: chunked transfer encoding (also for outgoing data) // TODO: multipart parsing From 2aaeef16b8afe7a92db9ec058ff8181e9537227b Mon Sep 17 00:00:00 2001 From: e3betht Date: Thu, 18 Dec 2014 11:51:46 -0600 Subject: [PATCH 006/456] Adding Code Climate badge to readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e1279b0e..f39b025f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Http Component -[![Build Status](https://secure.travis-ci.org/reactphp/http.png?branch=master)](http://travis-ci.org/reactphp/http) +[![Build Status](https://secure.travis-ci.org/reactphp/http.png?branch=master)](http://travis-ci.org/reactphp/http) [![Code Climate](https://codeclimate.com/github/reactphp/http/badges/gpa.svg)](https://codeclimate.com/github/reactphp/http) Library for building an evented http server. From 6ac71e4848e52f10594e7378d4fcd37ca11da0be Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Wed, 15 Apr 2015 21:05:44 +0200 Subject: [PATCH 007/456] Test against PHP7 --- .travis.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.travis.yml b/.travis.yml index d2fb7563..f5449647 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,11 +4,15 @@ php: - 5.4 - 5.5 - 5.6 + - 7 - hhvm + - hhvm-nightly matrix: allow_failures: + - php: 7 - php: hhvm + - php: hhvm-nightly before_script: - composer install --dev --prefer-source From 10af1f5b364d2bc877a06995cee252460d02be47 Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Wed, 20 May 2015 20:41:51 -0400 Subject: [PATCH 008/456] Use new Guzzle PSR7 parser --- composer.json | 8 ++++---- src/RequestHeaderParser.php | 27 +++++++++++++++++---------- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/composer.json b/composer.json index c15f6be9..cd1194da 100644 --- a/composer.json +++ b/composer.json @@ -5,10 +5,10 @@ "license": "MIT", "require": { "php": ">=5.4.0", - "guzzle/parser": "~3.0", - "react/socket": "0.4.*", - "react/stream": "0.4.*", - "evenement/evenement": "~2.0" + "guzzlehttp/psr7": "^1.0", + "react/socket": "^0.4", + "react/stream": "^0.4", + "evenement/evenement": "^2.0" }, "autoload": { "psr-4": { diff --git a/src/RequestHeaderParser.php b/src/RequestHeaderParser.php index 39bc127a..60a3aa1e 100644 --- a/src/RequestHeaderParser.php +++ b/src/RequestHeaderParser.php @@ -3,7 +3,7 @@ namespace React\Http; use Evenement\EventEmitter; -use Guzzle\Parser\Message\MessageParser; +use GuzzleHttp\Psr7 as g7; /** * @event headers @@ -36,20 +36,27 @@ public function parseRequest($data) { list($headers, $bodyBuffer) = explode("\r\n\r\n", $data, 2); - $parser = new MessageParser(); - $parsed = $parser->parseRequest($headers."\r\n\r\n"); + $psrRequest = g7\parse_request($headers); - $parsedQuery = array(); - if ($parsed['request_url']['query']) { - parse_str($parsed['request_url']['query'], $parsedQuery); + $parsedQuery = []; + $queryString = $psrRequest->getUri()->getQuery(); + if ($queryString) { + parse_str($queryString, $parsedQuery); } + $headers = $psrRequest->getHeaders(); + array_walk($headers, function(&$val) { + if (1 === count($val)) { + $val = $val[0]; + } + }); + $request = new Request( - $parsed['method'], - $parsed['request_url']['path'], + $psrRequest->getMethod(), + $psrRequest ->getUri()->getPath(), $parsedQuery, - $parsed['version'], - $parsed['headers'] + $psrRequest->getProtocolVersion(), + $headers ); return array($request, $bodyBuffer); From f575989d67b7db0a65f5dd7e431d8f47af6c2f7b Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Thu, 21 May 2015 22:12:09 +0200 Subject: [PATCH 009/456] Added 0.4.1 --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a2251cd0..c66b2ad6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 0.4.1 (2014-05-21) +* Replaced guzzle/parser with guzzlehttp/psr7 by @cboden +* FIX Continue Header by @iannsp +* Missing type hint by @marenzo + ## 0.4.0 (2014-02-02) * BC break: Bump minimum PHP version to PHP 5.4, remove 5.3 specific hacks From 8602944a939dd7d6433e9e48b2d4896050a202cd Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Sat, 27 Jun 2015 10:26:45 +0200 Subject: [PATCH 010/456] Removed a blank as pointed out in this comment: https://github.com/reactphp/http/commit/10af1f5b364d2bc877a06995cee252460d02be47#commitcomment-11894795 --- src/RequestHeaderParser.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/RequestHeaderParser.php b/src/RequestHeaderParser.php index 60a3aa1e..5f0dc1f0 100644 --- a/src/RequestHeaderParser.php +++ b/src/RequestHeaderParser.php @@ -53,7 +53,7 @@ public function parseRequest($data) $request = new Request( $psrRequest->getMethod(), - $psrRequest ->getUri()->getPath(), + $psrRequest->getUri()->getPath(), $parsedQuery, $psrRequest->getProtocolVersion(), $headers From 5bea2e703822b46c2b0872535eb5e1b8974a6fd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ste=CC=81phane=20Goetz?= Date: Tue, 28 Jul 2015 22:52:47 +0200 Subject: [PATCH 011/456] Do not write file uploads to files, keep in memory --- src/MultipartParser.php | 20 +++++++++++++++----- tests/MultipartParserTest.php | 9 ++++++--- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/MultipartParser.php b/src/MultipartParser.php index 681bf231..d4c0f4b9 100644 --- a/src/MultipartParser.php +++ b/src/MultipartParser.php @@ -142,17 +142,27 @@ protected function file($string) $content = preg_replace('/Content-Type: (.*)[^\n\r]/', '', $match[3]); $content = ltrim($content, "\r\n"); - $path = tempnam(sys_get_temp_dir(), "php"); - $err = file_put_contents($path, $content); + // Put content in a stream + $stream = fopen('php://memory', 'r+'); + if ($content !== '') { + fwrite($stream, $content); + fseek($stream, 0); + } $data = [ 'name' => $match[2], 'type' => trim($mime[1]), - 'tmp_name' => $path, - 'error' => ($err === false) ? UPLOAD_ERR_NO_FILE : UPLOAD_ERR_OK, - 'size' => filesize($path), + 'stream' => $stream, // Instead of writing to a file, we write to a stream. + 'error' => UPLOAD_ERR_OK, + 'size' => function_exists('mb_strlen')? mb_strlen($content, '8bit') : strlen($content), ]; + //TODO :: have an option to write to files to emulate the same functionality as a real php server + //$path = tempnam(sys_get_temp_dir(), "php"); + //$err = file_put_contents($path, $content); + //$data['tmp_name'] = $path; + //$data['error'] = ($err === false) ? UPLOAD_ERR_NO_FILE : UPLOAD_ERR_OK; + $this->addResolved('files', $match[1], $data); } diff --git a/tests/MultipartParserTest.php b/tests/MultipartParserTest.php index 0bffd5c3..39a02c71 100644 --- a/tests/MultipartParserTest.php +++ b/tests/MultipartParserTest.php @@ -83,13 +83,16 @@ public function testFileUpload() { $uploaded_blank = $parser->getFiles()['files'][0]; - $this->assertEquals($file, file_get_contents($uploaded_blank['tmp_name'])); + // The original test was `file_get_contents($uploaded_blank['tmp_name'])` + // but as we moved to resources, we can't use that anymore, this is the only + // difference with a stock php implementation + $this->assertEquals($file, stream_get_contents($uploaded_blank['stream'])); - $uploaded_blank['tmp_name'] = 'file'; //override the filename as it is random + $uploaded_blank['stream'] = 'file'; //override the resource as it is random $expected_file = [ 'name' => 'blank.gif', 'type' => 'image/gif', - 'tmp_name' => 'file', + 'stream' => 'file', 'error' => 0, 'size' => 43, ]; From a3cf90adb69bee244d703aecdc9a7c4b923255d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ste=CC=81phane=20Goetz?= Date: Tue, 28 Jul 2015 22:52:56 +0200 Subject: [PATCH 012/456] Added Todo for the future --- src/RequestParser.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/RequestParser.php b/src/RequestParser.php index cb2c5ed1..c213ec23 100644 --- a/src/RequestParser.php +++ b/src/RequestParser.php @@ -121,6 +121,7 @@ public function parseBody($content) if (array_key_exists('Content-Type', $headers)) { if (strpos($headers['Content-Type'], 'multipart/') === 0) { + //TODO :: parse the content while it is streaming preg_match("/boundary=\"?(.*)\"?$/", $headers['Content-Type'], $matches); $boundary = $matches[1]; From 2ab98d2b16b198c43d3c02b05662544f41d20f11 Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Sun, 23 Aug 2015 14:53:29 -0400 Subject: [PATCH 013/456] Naming, immutable array manipulation --- src/RequestParser.php | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/RequestParser.php b/src/RequestParser.php index c213ec23..7a0996af 100644 --- a/src/RequestParser.php +++ b/src/RequestParser.php @@ -3,7 +3,7 @@ namespace React\Http; use Evenement\EventEmitter; -use GuzzleHttp\Psr7 as g7; +use GuzzleHttp\Psr7 as gPsr; /** * @event headers @@ -91,7 +91,7 @@ protected function headerSizeExceeded() public function parseHeaders($data) { - $psrRequest = g7\parse_request($data); + $psrRequest = gPsr\parse_request($data); $parsedQuery = []; $queryString = $psrRequest->getUri()->getQuery(); @@ -99,12 +99,13 @@ public function parseHeaders($data) parse_str($queryString, $parsedQuery); } - $headers = $psrRequest->getHeaders(); - array_walk($headers, function(&$val) { + $headers = array_map(function(&$val) { if (1 === count($val)) { $val = $val[0]; } - }); + + return $val; + }, $psrRequest->getHeaders()); return new Request( $psrRequest->getMethod(), From 75ff84ad24cddeb37977241cb9419d0a1f2d5950 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Mon, 24 Aug 2015 17:40:47 +0200 Subject: [PATCH 014/456] Removed underutilized & --- src/RequestParser.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/RequestParser.php b/src/RequestParser.php index 7a0996af..2a5c2873 100644 --- a/src/RequestParser.php +++ b/src/RequestParser.php @@ -99,7 +99,7 @@ public function parseHeaders($data) parse_str($queryString, $parsedQuery); } - $headers = array_map(function(&$val) { + $headers = array_map(function($val) { if (1 === count($val)) { $val = $val[0]; } From 7c52e53aaa2448900e5feb9bcc8e77c167632580 Mon Sep 17 00:00:00 2001 From: Grigoriy Ostrovskiy Date: Tue, 20 Oct 2015 14:42:13 +0300 Subject: [PATCH 015/456] Fixes reactphp/http#44 - postfields parsing without urldecode --- src/RequestParser.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/RequestParser.php b/src/RequestParser.php index 2a5c2873..0655ff8b 100644 --- a/src/RequestParser.php +++ b/src/RequestParser.php @@ -135,7 +135,7 @@ public function parseBody($content) } if (strtolower($headers['Content-Type']) == 'application/x-www-form-urlencoded') { - parse_str(urldecode($content), $result); + parse_str($content, $result); $this->request->setPost($result); return; From 6ad3e68da9fc0a9b9e920d737ed78c2cf20ff0dd Mon Sep 17 00:00:00 2001 From: Slava Vishnyakov Date: Sat, 19 Dec 2015 00:51:54 +0300 Subject: [PATCH 016/456] Expect Continue --- src/RequestParser.php | 4 ++++ src/Server.php | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/src/RequestParser.php b/src/RequestParser.php index 0655ff8b..c4b913b8 100644 --- a/src/RequestParser.php +++ b/src/RequestParser.php @@ -37,6 +37,10 @@ public function feed($data) } $this->request = $this->parseHeaders($headers . "\r\n\r\n"); + + if($this->request->expectsContinue()) { + $this->emit('expects_continue'); + } } // if there is a request (meaning the headers are parsed) and diff --git a/src/Server.php b/src/Server.php index 5e420a40..2948128c 100644 --- a/src/Server.php +++ b/src/Server.php @@ -42,6 +42,10 @@ public function __construct(SocketServerInterface $io) }); $conn->on('data', array($parser, 'feed')); + + $parser->on('expects_continue', function() use($conn) { + $conn->write("HTTP/1.1 100 Continue\r\n\r\n"); + }); }); } From 15e746eed4a4273de8b78dfc8d9f31c5c8d3c96a Mon Sep 17 00:00:00 2001 From: Slava Vishnyakov Date: Sat, 19 Dec 2015 22:24:15 +0300 Subject: [PATCH 017/456] Test for 100-continue --- tests/ServerTest.php | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 4d4f1c74..baf988bf 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -75,4 +75,47 @@ private function createGetRequest() return $data; } + + public function testServerRespondsToExpectContinue() + { + $io = new ServerStub(); + $server = new Server($io); + $conn = new ConnectionStub(); + $io->emit('connection', array($conn)); + + $requestReceived = false; + $postBody = '{"React":true}'; + $httpRequestText = $this->createPostRequestWithExpect($postBody); + + $conn->emit('data', array($httpRequestText)); + + $server->on('request', function ($request, $_) use (&$requestReceived, $postBody) { + $requestReceived = true; + $this->assertEquals($postBody, $request->getBody()); + }); + + // If server received Expect: 100-continue - the client won't send the body right away + $this->assertEquals(false, $requestReceived); + + $this->assertEquals("HTTP/1.1 100 Continue\r\n\r\n", $conn->getData()); + + $conn->emit('data', array($postBody)); + + $this->assertEquals(true, $requestReceived); + + } + + private function createPostRequestWithExpect($postBody) + { + $data = "POST / HTTP/1.1\r\n"; + $data .= "Host: example.com:80\r\n"; + $data .= "Content-Type: application/json\r\n"; + $data .= "Content-Length: " . strlen($postBody) . "\r\n"; + $data .= "Expect: 100-continue\r\n"; + $data .= "\r\n"; + + return $data; + } + + } From 37c57c2b7a1d3b24e4a8e3327f273c89a419ed19 Mon Sep 17 00:00:00 2001 From: Slava Vishnyakov Date: Sat, 19 Dec 2015 22:24:52 +0300 Subject: [PATCH 018/456] Minor whitespace correction --- tests/ServerTest.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/ServerTest.php b/tests/ServerTest.php index baf988bf..449e3793 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -116,6 +116,4 @@ private function createPostRequestWithExpect($postBody) return $data; } - - } From a03d4df5d618af2f4b2250e21594a90b048cde01 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Wed, 6 Jul 2016 17:43:06 +0200 Subject: [PATCH 019/456] Remove branch-alias definition as per reactphp/react#343 --- composer.json | 5 ----- 1 file changed, 5 deletions(-) diff --git a/composer.json b/composer.json index cd1194da..c186927d 100644 --- a/composer.json +++ b/composer.json @@ -14,10 +14,5 @@ "psr-4": { "React\\Http\\": "src" } - }, - "extra": { - "branch-alias": { - "dev-master": "0.5-dev" - } } } From 545b05f46265fe416873bbc845b4b0e8c9a24453 Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Sun, 23 Aug 2015 14:53:29 -0400 Subject: [PATCH 020/456] Naming, immutable array manipulation This is a sqashed version to add back the changes from #37: - 2ab98d2b16b198c43d3c02b05662544f41d20f11 - 75ff84ad24cddeb37977241cb9419d0a1f2d5950 --- src/RequestHeaderParser.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/RequestHeaderParser.php b/src/RequestHeaderParser.php index 5f0dc1f0..3d0a4a7c 100644 --- a/src/RequestHeaderParser.php +++ b/src/RequestHeaderParser.php @@ -44,12 +44,13 @@ public function parseRequest($data) parse_str($queryString, $parsedQuery); } - $headers = $psrRequest->getHeaders(); - array_walk($headers, function(&$val) { + $headers = array_map(function($val) { if (1 === count($val)) { $val = $val[0]; } - }); + + return $val; + }, $psrRequest->getHeaders()); $request = new Request( $psrRequest->getMethod(), From f0000a55dca55e874d33602dc7d1037c8194685e Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Wed, 6 Jul 2016 17:43:06 +0200 Subject: [PATCH 021/456] Remove branch-alias definition as per reactphp/react#343 --- composer.json | 5 ----- 1 file changed, 5 deletions(-) diff --git a/composer.json b/composer.json index cd1194da..c186927d 100644 --- a/composer.json +++ b/composer.json @@ -14,10 +14,5 @@ "psr-4": { "React\\Http\\": "src" } - }, - "extra": { - "branch-alias": { - "dev-master": "0.5-dev" - } } } From c763ac94456c9b0d4048299063732d68d3f79d91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 2 Sep 2016 22:32:14 +0200 Subject: [PATCH 022/456] Add functional example to ease getting started --- README.md | 24 ++++++++++++++---------- examples/01-hello-world.php | 23 +++++++++++++++++++++++ 2 files changed, 37 insertions(+), 10 deletions(-) create mode 100644 examples/01-hello-world.php diff --git a/README.md b/README.md index f39b025f..9bad8ad4 100644 --- a/README.md +++ b/README.md @@ -15,19 +15,23 @@ are the main concepts: * **Response** A `WritableStream` which streams the response body. You can set the status code and response headers via the `writeHead()` method. -## Usage + +## Quickstart example This is an HTTP server which responds with `Hello World` to every request. + ```php - $loop = React\EventLoop\Factory::create(); - $socket = new React\Socket\Server($loop); +$loop = React\EventLoop\Factory::create(); +$socket = new React\Socket\Server($loop); - $http = new React\Http\Server($socket); - $http->on('request', function ($request, $response) { - $response->writeHead(200, array('Content-Type' => 'text/plain')); - $response->end("Hello World!\n"); - }); +$http = new React\Http\Server($socket); +$http->on('request', function ($request, $response) { + $response->writeHead(200, array('Content-Type' => 'text/plain')); + $response->end("Hello World!\n"); +}); - $socket->listen(1337); - $loop->run(); +$socket->listen(1337); +$loop->run(); ``` + +See also the [examples](examples). diff --git a/examples/01-hello-world.php b/examples/01-hello-world.php new file mode 100644 index 00000000..0a8e9d56 --- /dev/null +++ b/examples/01-hello-world.php @@ -0,0 +1,23 @@ +on('request', function (Request $reques, Response $response) { + $response->writeHead(200, array('Content-Type' => 'text/plain')); + $response->end("Hello world!\n"); +}); + +$socket->listen(isset($argv[1]) ? $argv[1] : 0, '0.0.0.0'); + +echo 'Listening on ' . $socket->getPort() . PHP_EOL; + +$loop->run(); From 777cfef6a2a54259da495ec6cb11e2bd04756a60 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Fri, 9 Sep 2016 12:41:43 +0200 Subject: [PATCH 023/456] Catch Guzzle parse request errors (#65) * Catch Guzzle parse request errors and emit them as error * Assert removeAllListeners --- src/RequestHeaderParser.php | 14 ++++++++++++-- tests/RequestHeaderParserTest.php | 21 +++++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/RequestHeaderParser.php b/src/RequestHeaderParser.php index 3d0a4a7c..4e4db46f 100644 --- a/src/RequestHeaderParser.php +++ b/src/RequestHeaderParser.php @@ -3,6 +3,7 @@ namespace React\Http; use Evenement\EventEmitter; +use Exception; use GuzzleHttp\Psr7 as g7; /** @@ -25,13 +26,22 @@ public function feed($data) $this->buffer .= $data; if (false !== strpos($this->buffer, "\r\n\r\n")) { - list($request, $bodyBuffer) = $this->parseRequest($this->buffer); + try { + $this->parseAndEmitRequest(); + } catch (Exception $exception) { + $this->emit('error', [$exception]); + } - $this->emit('headers', array($request, $bodyBuffer)); $this->removeAllListeners(); } } + protected function parseAndEmitRequest() + { + list($request, $bodyBuffer) = $this->parseRequest($this->buffer); + $this->emit('headers', array($request, $bodyBuffer)); + } + public function parseRequest($data) { list($headers, $bodyBuffer) = explode("\r\n\r\n", $data, 2); diff --git a/tests/RequestHeaderParserTest.php b/tests/RequestHeaderParserTest.php index dd3a0cbe..b2fa87dc 100644 --- a/tests/RequestHeaderParserTest.php +++ b/tests/RequestHeaderParserTest.php @@ -113,6 +113,27 @@ public function testHeaderOverflowShouldEmitError() $this->assertSame('Maximum header size of 4096 exceeded.', $error->getMessage()); } + public function testGuzzleRequestParseException() + { + $error = null; + + $parser = new RequestHeaderParser(); + $parser->on('headers', $this->expectCallableNever()); + $parser->on('error', function ($message) use (&$error) { + $error = $message; + }); + + $this->assertSame(1, count($parser->listeners('headers'))); + $this->assertSame(1, count($parser->listeners('error'))); + + $parser->feed("\r\n\r\n"); + + $this->assertInstanceOf('InvalidArgumentException', $error); + $this->assertSame('Invalid message', $error->getMessage()); + $this->assertSame(0, count($parser->listeners('headers'))); + $this->assertSame(0, count($parser->listeners('error'))); + } + private function createGetRequest() { $data = "GET / HTTP/1.1\r\n"; From 24a2af6d76878da836d0ca084f3680661c458249 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Wed, 9 Nov 2016 14:11:00 +0100 Subject: [PATCH 024/456] Remove all listeners after emitting error in RequestHeaderParser (#68) * Ensure removeAllListeners on all error event in RequestHeaderParser * Ensure all error events from RequestHeaderParser emit $this as second item in event * Reverted emitting $this with error --- src/RequestHeaderParser.php | 2 +- tests/RequestHeaderParserTest.php | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/RequestHeaderParser.php b/src/RequestHeaderParser.php index 4e4db46f..ca9d57f7 100644 --- a/src/RequestHeaderParser.php +++ b/src/RequestHeaderParser.php @@ -19,7 +19,7 @@ public function feed($data) { if (strlen($this->buffer) + strlen($data) > $this->maxSize) { $this->emit('error', array(new \OverflowException("Maximum header size of {$this->maxSize} exceeded."), $this)); - + $this->removeAllListeners(); return; } diff --git a/tests/RequestHeaderParserTest.php b/tests/RequestHeaderParserTest.php index b2fa87dc..807af42c 100644 --- a/tests/RequestHeaderParserTest.php +++ b/tests/RequestHeaderParserTest.php @@ -99,18 +99,26 @@ public function testHeadersEventShouldParsePathAndQueryString() public function testHeaderOverflowShouldEmitError() { $error = null; + $passedParser = null; $parser = new RequestHeaderParser(); $parser->on('headers', $this->expectCallableNever()); - $parser->on('error', function ($message) use (&$error) { + $parser->on('error', function ($message, $parser) use (&$error, &$passedParser) { $error = $message; + $passedParser = $parser; }); + $this->assertSame(1, count($parser->listeners('headers'))); + $this->assertSame(1, count($parser->listeners('error'))); + $data = str_repeat('A', 4097); $parser->feed($data); $this->assertInstanceOf('OverflowException', $error); $this->assertSame('Maximum header size of 4096 exceeded.', $error->getMessage()); + $this->assertSame($parser, $passedParser); + $this->assertSame(0, count($parser->listeners('headers'))); + $this->assertSame(0, count($parser->listeners('error'))); } public function testGuzzleRequestParseException() From abedac54967d7ea237ad104cff8274e2c4077cf4 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Wed, 9 Nov 2016 16:20:39 +0100 Subject: [PATCH 025/456] Added 0.4.2 to changelog --- CHANGELOG.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c66b2ad6..c0167b52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,15 @@ # Changelog -## 0.4.1 (2014-05-21) +## 0.4.2 (2016-11-09) + +* Remove all listeners after emitting error in RequestHeaderParser #68 @WyriHaximus +* Catch Guzzle parse request errors #65 @WyriHaximus +* Remove branch-alias definition as per reactphp/react#343 #58 @WyriHaximus +* Add functional example to ease getting started #64 by @clue +* Naming, immutable array manipulation #37 @cboden + +## 0.4.1 (2015-05-21) + * Replaced guzzle/parser with guzzlehttp/psr7 by @cboden * FIX Continue Header by @iannsp * Missing type hint by @marenzo From 1fa41f84f183c14fb0c2fa005c911e501bd19fbb Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Wed, 30 Nov 2016 19:03:45 +0100 Subject: [PATCH 026/456] Removed testing against HHVM nightly (#66) * Test against PHP 7.1 and not against HHVM nightly anymore * Removed 7.1 to not overly bloat the test matrix * Changed 7.0 back to 7 so the effective changes are only what the PR is about now --- .travis.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index f5449647..8414e4d0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,13 +6,11 @@ php: - 5.6 - 7 - hhvm - - hhvm-nightly matrix: allow_failures: - php: 7 - php: hhvm - - php: hhvm-nightly before_script: - composer install --dev --prefer-source From 4aacf2e48c2faeb22548ecb5e94728c42b0cab1d Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Fri, 16 Dec 2016 17:26:47 +0100 Subject: [PATCH 027/456] Typo in $reques, should be $request --- examples/01-hello-world.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/01-hello-world.php b/examples/01-hello-world.php index 0a8e9d56..70107616 100644 --- a/examples/01-hello-world.php +++ b/examples/01-hello-world.php @@ -11,7 +11,7 @@ $socket = new Server($loop); $server = new \React\Http\Server($socket); -$server->on('request', function (Request $reques, Response $response) { +$server->on('request', function (Request $request, Response $response) { $response->writeHead(200, array('Content-Type' => 'text/plain')); $response->end("Hello world!\n"); }); From 6768e13bdcea8bc8770b99f41ee689b4b7c139c0 Mon Sep 17 00:00:00 2001 From: Bohdan Yurov Date: Sun, 5 Feb 2017 20:33:03 +0200 Subject: [PATCH 028/456] Fixes #81 issue: data listener is removed if HeaderParser emits error (#83) --- src/Server.php | 8 +++++++- tests/ServerTest.php | 23 +++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/Server.php b/src/Server.php index e1abfd8e..49535060 100644 --- a/src/Server.php +++ b/src/Server.php @@ -42,7 +42,13 @@ public function __construct(SocketServerInterface $io) }); }); - $conn->on('data', array($parser, 'feed')); + $listener = [$parser, 'feed']; + $conn->on('data', $listener); + $parser->on('error', function() use ($conn, $listener) { + // TODO: return 400 response + $conn->removeListener('data', $listener); + $this->emit('error', func_get_args()); + }); }); } diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 65c13ccb..552b562f 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -2,6 +2,7 @@ namespace React\Tests\Http; +use React\Http\RequestHeaderParser; use React\Http\Server; class ServerTest extends TestCase @@ -66,6 +67,28 @@ public function testResponseContainsPoweredByHeader() $this->assertContains("\r\nX-Powered-By: React/alpha\r\n", $conn->getData()); } + public function testParserErrorEmitted() + { + $io = new ServerStub(); + + $error = null; + $server = new Server($io); + $server->on('headers', $this->expectCallableNever()); + $server->on('error', function ($message) use (&$error) { + $error = $message; + }); + + $conn = new ConnectionStub(); + $io->emit('connection', [$conn]); + + $data = $this->createGetRequest(); + $data = str_pad($data, 4096 * 4); + $conn->emit('data', [$data]); + + $this->assertInstanceOf('OverflowException', $error); + $this->assertEquals('', $conn->getData()); + } + private function createGetRequest() { $data = "GET / HTTP/1.1\r\n"; From 5dc78e7f7e9c2278390bc36f0cedffdbe9528ff5 Mon Sep 17 00:00:00 2001 From: Dan Revel Date: Sun, 5 Feb 2017 09:34:57 -0800 Subject: [PATCH 029/456] check max header size --- src/RequestHeaderParser.php | 17 ++++++++++++----- tests/RequestHeaderParserTest.php | 27 +++++++++++++++++++++++++++ tests/ServerTest.php | 4 ++-- 3 files changed, 41 insertions(+), 7 deletions(-) diff --git a/src/RequestHeaderParser.php b/src/RequestHeaderParser.php index ca9d57f7..af70ed1b 100644 --- a/src/RequestHeaderParser.php +++ b/src/RequestHeaderParser.php @@ -17,21 +17,28 @@ class RequestHeaderParser extends EventEmitter public function feed($data) { - if (strlen($this->buffer) + strlen($data) > $this->maxSize) { + $this->buffer .= $data; + + $endOfHeader = strpos($this->buffer, "\r\n\r\n"); + + if (false !== $endOfHeader) { + $currentHeaderSize = $endOfHeader; + } else { + $currentHeaderSize = strlen($this->buffer); + } + + if ($currentHeaderSize > $this->maxSize) { $this->emit('error', array(new \OverflowException("Maximum header size of {$this->maxSize} exceeded."), $this)); $this->removeAllListeners(); return; } - $this->buffer .= $data; - - if (false !== strpos($this->buffer, "\r\n\r\n")) { + if (false !== $endOfHeader) { try { $this->parseAndEmitRequest(); } catch (Exception $exception) { $this->emit('error', [$exception]); } - $this->removeAllListeners(); } } diff --git a/tests/RequestHeaderParserTest.php b/tests/RequestHeaderParserTest.php index 807af42c..2c22c4a5 100644 --- a/tests/RequestHeaderParserTest.php +++ b/tests/RequestHeaderParserTest.php @@ -121,6 +121,33 @@ public function testHeaderOverflowShouldEmitError() $this->assertSame(0, count($parser->listeners('error'))); } + public function testHeaderOverflowShouldNotEmitErrorWhenDataExceedsMaxHeaderSize() + { + $request = null; + $bodyBuffer = null; + + $parser = new RequestHeaderParser(); + $parser->on('headers', function ($parsedRequest, $parsedBodyBuffer) use (&$request, &$bodyBuffer) { + $request = $parsedRequest; + $bodyBuffer = $parsedBodyBuffer; + }); + + $data = $this->createAdvancedPostRequest(); + $body = str_repeat('A', 4097 - strlen($data)); + $data .= $body; + + $parser->feed($data); + + $headers = array( + 'Host' => 'example.com:80', + 'User-Agent' => 'react/alpha', + 'Connection' => 'close', + ); + $this->assertSame($headers, $request->getHeaders()); + + $this->assertSame($body, $bodyBuffer); + } + public function testGuzzleRequestParseException() { $error = null; diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 552b562f..190d6fa2 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -81,8 +81,8 @@ public function testParserErrorEmitted() $conn = new ConnectionStub(); $io->emit('connection', [$conn]); - $data = $this->createGetRequest(); - $data = str_pad($data, 4096 * 4); + $data = "GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\nX-DATA: "; + $data .= str_repeat('A', 4097 - strlen($data)) . "\r\n\r\n"; $conn->emit('data', [$data]); $this->assertInstanceOf('OverflowException', $error); From f6140c4c27a363d9fc03b5a3b46b5f866436e842 Mon Sep 17 00:00:00 2001 From: Dan Revel Date: Sun, 5 Feb 2017 23:37:29 -0800 Subject: [PATCH 030/456] add phpunit 4.8 to require-dev, force travisci to use local phpunit --- .travis.yml | 2 +- composer.json | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 8414e4d0..55c8e85f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,4 +16,4 @@ before_script: - composer install --dev --prefer-source script: - - phpunit --coverage-text + - ./vendor/bin/phpunit --coverage-text diff --git a/composer.json b/composer.json index c186927d..cfc6f04a 100644 --- a/composer.json +++ b/composer.json @@ -14,5 +14,8 @@ "psr-4": { "React\\Http\\": "src" } + }, + "require-dev": { + "phpunit/phpunit": "~4.8" } } From f30e5b0e016264bda4bdaa7bc6767e331428a75d Mon Sep 17 00:00:00 2001 From: Dan Revel Date: Sun, 5 Feb 2017 23:58:28 -0800 Subject: [PATCH 031/456] replace getMock for forward compatibility --- tests/ResponseTest.php | 40 ++++++++++++++++++++++++++++++---------- tests/TestCase.php | 4 +++- 2 files changed, 33 insertions(+), 11 deletions(-) diff --git a/tests/ResponseTest.php b/tests/ResponseTest.php index 7692fb83..11407f28 100644 --- a/tests/ResponseTest.php +++ b/tests/ResponseTest.php @@ -14,7 +14,9 @@ public function testResponseShouldBeChunkedByDefault() $expected .= "Transfer-Encoding: chunked\r\n"; $expected .= "\r\n"; - $conn = $this->getMock('React\Socket\ConnectionInterface'); + $conn = $this + ->getMockBuilder('React\Socket\ConnectionInterface') + ->getMock(); $conn ->expects($this->once()) ->method('write') @@ -32,7 +34,9 @@ public function testResponseShouldNotBeChunkedWithContentLength() $expected .= "Content-Length: 22\r\n"; $expected .= "\r\n"; - $conn = $this->getMock('React\Socket\ConnectionInterface'); + $conn = $this + ->getMockBuilder('React\Socket\ConnectionInterface') + ->getMock(); $conn ->expects($this->once()) ->method('write') @@ -44,7 +48,9 @@ public function testResponseShouldNotBeChunkedWithContentLength() public function testResponseBodyShouldBeChunkedCorrectly() { - $conn = $this->getMock('React\Socket\ConnectionInterface'); + $conn = $this + ->getMockBuilder('React\Socket\ConnectionInterface') + ->getMock(); $conn ->expects($this->at(4)) ->method('write') @@ -75,7 +81,9 @@ public function testResponseShouldEmitEndOnStreamEnd() { $ended = false; - $conn = $this->getMock('React\Socket\ConnectionInterface'); + $conn = $this + ->getMockBuilder('React\Socket\ConnectionInterface') + ->getMock(); $response = new Response($conn); $response->on('end', function () use (&$ended) { @@ -89,7 +97,9 @@ public function testResponseShouldEmitEndOnStreamEnd() /** @test */ public function writeContinueShouldSendContinueLineBeforeRealHeaders() { - $conn = $this->getMock('React\Socket\ConnectionInterface'); + $conn = $this + ->getMockBuilder('React\Socket\ConnectionInterface') + ->getMock(); $conn ->expects($this->at(3)) ->method('write') @@ -107,7 +117,9 @@ public function writeContinueShouldSendContinueLineBeforeRealHeaders() /** @test */ public function shouldForwardEndDrainAndErrorEvents() { - $conn = $this->getMock('React\Socket\ConnectionInterface'); + $conn = $this + ->getMockBuilder('React\Socket\ConnectionInterface') + ->getMock(); $conn ->expects($this->at(0)) ->method('on') @@ -134,7 +146,9 @@ public function shouldRemoveNewlinesFromHeaders() $expected .= "Transfer-Encoding: chunked\r\n"; $expected .= "\r\n"; - $conn = $this->getMock('React\Socket\ConnectionInterface'); + $conn = $this + ->getMockBuilder('React\Socket\ConnectionInterface') + ->getMock(); $conn ->expects($this->once()) ->method('write') @@ -153,7 +167,9 @@ public function missingStatusCodeTextShouldResultInNumberOnlyStatus() $expected .= "Transfer-Encoding: chunked\r\n"; $expected .= "\r\n"; - $conn = $this->getMock('React\Socket\ConnectionInterface'); + $conn = $this + ->getMockBuilder('React\Socket\ConnectionInterface') + ->getMock(); $conn ->expects($this->once()) ->method('write') @@ -174,7 +190,9 @@ public function shouldAllowArrayHeaderValues() $expected .= "Transfer-Encoding: chunked\r\n"; $expected .= "\r\n"; - $conn = $this->getMock('React\Socket\ConnectionInterface'); + $conn = $this + ->getMockBuilder('React\Socket\ConnectionInterface') + ->getMock(); $conn ->expects($this->once()) ->method('write') @@ -193,7 +211,9 @@ public function shouldIgnoreHeadersWithNullValues() $expected .= "Transfer-Encoding: chunked\r\n"; $expected .= "\r\n"; - $conn = $this->getMock('React\Socket\ConnectionInterface'); + $conn = $this + ->getMockBuilder('React\Socket\ConnectionInterface') + ->getMock(); $conn ->expects($this->once()) ->method('write') diff --git a/tests/TestCase.php b/tests/TestCase.php index a08675c9..24fe27f2 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -36,6 +36,8 @@ protected function expectCallableNever() protected function createCallableMock() { - return $this->getMock('React\Tests\Http\CallableStub'); + return $this + ->getMockBuilder('React\Tests\Http\CallableStub') + ->getMock(); } } From a918f7e7b5b7513b067bb083fc1785985d2581af Mon Sep 17 00:00:00 2001 From: Niels Theen Date: Tue, 7 Feb 2017 12:47:10 +0100 Subject: [PATCH 032/456] ServerInterface is unnecessary --- src/Server.php | 2 +- src/ServerInterface.php | 9 --------- 2 files changed, 1 insertion(+), 10 deletions(-) delete mode 100644 src/ServerInterface.php diff --git a/src/Server.php b/src/Server.php index 49535060..4af205e5 100644 --- a/src/Server.php +++ b/src/Server.php @@ -7,7 +7,7 @@ use React\Socket\ConnectionInterface; /** @event request */ -class Server extends EventEmitter implements ServerInterface +class Server extends EventEmitter { private $io; diff --git a/src/ServerInterface.php b/src/ServerInterface.php deleted file mode 100644 index 56dd61fe..00000000 --- a/src/ServerInterface.php +++ /dev/null @@ -1,9 +0,0 @@ - Date: Tue, 7 Feb 2017 12:02:29 +0100 Subject: [PATCH 033/456] Fix assertion in ServerTests --- tests/ServerTest.php | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 190d6fa2..66880260 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -2,8 +2,9 @@ namespace React\Tests\Http; -use React\Http\RequestHeaderParser; use React\Http\Server; +use React\Http\Response; +use React\Http\Request; class ServerTest extends TestCase { @@ -26,17 +27,14 @@ public function testRequestEvent() $io = new ServerStub(); $i = 0; + $requestAssertion = null; + $responseAssertion = null; $server = new Server($io); - $server->on('request', function ($request, $response) use (&$i) { + $server->on('request', function (Request $request, Response $response) use (&$i, &$requestAssertion, &$responseAssertion) { $i++; - - $this->assertInstanceOf('React\Http\Request', $request); - $this->assertSame('/', $request->getPath()); - $this->assertSame('GET', $request->getMethod()); - $this->assertSame('127.0.0.1', $request->remoteAddress); - - $this->assertInstanceOf('React\Http\Response', $response); + $requestAssertion = $request; + $responseAssertion = $response; }); $conn = new ConnectionStub(); @@ -46,6 +44,12 @@ public function testRequestEvent() $conn->emit('data', array($data)); $this->assertSame(1, $i); + $this->assertInstanceOf('React\Http\Request', $requestAssertion); + $this->assertSame('/', $requestAssertion->getPath()); + $this->assertSame('GET', $requestAssertion->getMethod()); + $this->assertSame('127.0.0.1', $requestAssertion->remoteAddress); + + $this->assertInstanceOf('React\Http\Response', $responseAssertion); } public function testResponseContainsPoweredByHeader() @@ -53,7 +57,7 @@ public function testResponseContainsPoweredByHeader() $io = new ServerStub(); $server = new Server($io); - $server->on('request', function ($request, $response) { + $server->on('request', function (Request $request, Response $response) { $response->writeHead(); $response->end(); }); From 945ca813678e2c69ac40154308b8667aa7cfc717 Mon Sep 17 00:00:00 2001 From: Niels Theen Date: Tue, 7 Feb 2017 13:01:18 +0100 Subject: [PATCH 034/456] Remove unneeded type hints --- tests/ServerTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 66880260..1ea905ca 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -31,7 +31,7 @@ public function testRequestEvent() $responseAssertion = null; $server = new Server($io); - $server->on('request', function (Request $request, Response $response) use (&$i, &$requestAssertion, &$responseAssertion) { + $server->on('request', function ($request, $response) use (&$i, &$requestAssertion, &$responseAssertion) { $i++; $requestAssertion = $request; $responseAssertion = $response; From beb143e0647da2c2a449d31107e07ae12a139ee8 Mon Sep 17 00:00:00 2001 From: Niels Theen Date: Thu, 9 Feb 2017 11:59:50 +0100 Subject: [PATCH 035/456] Remove stubs from server tests --- tests/ServerTest.php | 79 +++++++++++++++++++++++++++++++------------- 1 file changed, 56 insertions(+), 23 deletions(-) diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 1ea905ca..204ed072 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -5,43 +5,72 @@ use React\Http\Server; use React\Http\Response; use React\Http\Request; +use React\Socket\Server as Socket; class ServerTest extends TestCase { + private $connection; + private $loop; + + public function setUp() + { + $this->loop = \React\EventLoop\Factory::create(); + + $this->connection = $this->getMockBuilder('React\Socket\Connection') + ->disableOriginalConstructor() + ->setMethods( + array( + 'write', + 'end', + 'close', + 'pause', + 'resume', + 'isReadable', + 'isWritable', + 'getRemoteAddress', + 'pipe' + ) + ) + ->getMock(); + } + public function testRequestEventIsEmitted() { - $io = new ServerStub(); + $socket = new Socket($this->loop); - $server = new Server($io); + $server = new Server($socket); $server->on('request', $this->expectCallableOnce()); - $conn = new ConnectionStub(); - $io->emit('connection', array($conn)); + $socket->emit('connection', array($this->connection)); $data = $this->createGetRequest(); - $conn->emit('data', array($data)); + $this->connection->emit('data', array($data)); } public function testRequestEvent() { - $io = new ServerStub(); + $socket = new Socket($this->loop); $i = 0; $requestAssertion = null; $responseAssertion = null; - $server = new Server($io); + $server = new Server($socket); $server->on('request', function ($request, $response) use (&$i, &$requestAssertion, &$responseAssertion) { $i++; $requestAssertion = $request; $responseAssertion = $response; }); - $conn = new ConnectionStub(); - $io->emit('connection', array($conn)); + $this->connection + ->expects($this->once()) + ->method('getRemoteAddress') + ->willReturn('127.0.0.1'); + + $socket->emit('connection', array($this->connection)); $data = $this->createGetRequest(); - $conn->emit('data', array($data)); + $this->connection->emit('data', array($data)); $this->assertSame(1, $i); $this->assertInstanceOf('React\Http\Request', $requestAssertion); @@ -54,43 +83,47 @@ public function testRequestEvent() public function testResponseContainsPoweredByHeader() { - $io = new ServerStub(); + $socket = new Socket($this->loop); - $server = new Server($io); + $server = new Server($socket); $server->on('request', function (Request $request, Response $response) { $response->writeHead(); $response->end(); }); - $conn = new ConnectionStub(); - $io->emit('connection', array($conn)); + $this->connection + ->expects($this->exactly(2)) + ->method('write') + ->withConsecutive( + array($this->equalTo("HTTP/1.1 200 OK\r\nX-Powered-By: React/alpha\r\nTransfer-Encoding: chunked\r\n\r\n")), + array($this->equalTo("0\r\n\r\n")) + ); - $data = $this->createGetRequest(); - $conn->emit('data', array($data)); + $socket->emit('connection', array($this->connection)); - $this->assertContains("\r\nX-Powered-By: React/alpha\r\n", $conn->getData()); + $data = $this->createGetRequest(); + $this->connection->emit('data', array($data)); } public function testParserErrorEmitted() { - $io = new ServerStub(); + $socket = new Socket($this->loop); $error = null; - $server = new Server($io); + $server = new Server($socket); $server->on('headers', $this->expectCallableNever()); $server->on('error', function ($message) use (&$error) { $error = $message; }); - $conn = new ConnectionStub(); - $io->emit('connection', [$conn]); + $socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\nX-DATA: "; $data .= str_repeat('A', 4097 - strlen($data)) . "\r\n\r\n"; - $conn->emit('data', [$data]); + $this->connection->emit('data', [$data]); $this->assertInstanceOf('OverflowException', $error); - $this->assertEquals('', $conn->getData()); + $this->connection->expects($this->never())->method('write'); } private function createGetRequest() From 42325234d6bc3c697a03ace9d0883db54c67d09b Mon Sep 17 00:00:00 2001 From: Niels Theen Date: Thu, 9 Feb 2017 12:06:16 +0100 Subject: [PATCH 036/456] Remove unneeded stubs --- tests/ConnectionStub.php | 63 ---------------------------------------- tests/ServerStub.php | 22 -------------- 2 files changed, 85 deletions(-) delete mode 100644 tests/ConnectionStub.php delete mode 100644 tests/ServerStub.php diff --git a/tests/ConnectionStub.php b/tests/ConnectionStub.php deleted file mode 100644 index 9ddfb052..00000000 --- a/tests/ConnectionStub.php +++ /dev/null @@ -1,63 +0,0 @@ -data .= $data; - - return true; - } - - public function end($data = null) - { - } - - public function close() - { - } - - public function getData() - { - return $this->data; - } - - public function getRemoteAddress() - { - return '127.0.0.1'; - } -} diff --git a/tests/ServerStub.php b/tests/ServerStub.php deleted file mode 100644 index fc55e972..00000000 --- a/tests/ServerStub.php +++ /dev/null @@ -1,22 +0,0 @@ - Date: Thu, 9 Feb 2017 13:26:23 +0100 Subject: [PATCH 037/456] First class support for PHP7 and HHVM --- .travis.yml | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index 55c8e85f..94d55bc3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,13 +7,8 @@ php: - 7 - hhvm -matrix: - allow_failures: - - php: 7 - - php: hhvm - -before_script: - - composer install --dev --prefer-source +install: + - composer install --no-interaction script: - ./vendor/bin/phpunit --coverage-text From ac82b8f80a0a592a499bacf91504b7e4a35ebab9 Mon Sep 17 00:00:00 2001 From: Niels Theen Date: Thu, 9 Feb 2017 14:15:36 +0100 Subject: [PATCH 038/456] Mock React\Socket\Server in tests --- tests/ServerTest.php | 49 ++++++++++++++++++++++---------------------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 204ed072..5cef7cfc 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -5,17 +5,14 @@ use React\Http\Server; use React\Http\Response; use React\Http\Request; -use React\Socket\Server as Socket; class ServerTest extends TestCase { private $connection; - private $loop; + private $socket; public function setUp() { - $this->loop = \React\EventLoop\Factory::create(); - $this->connection = $this->getMockBuilder('React\Socket\Connection') ->disableOriginalConstructor() ->setMethods( @@ -32,16 +29,19 @@ public function setUp() ) ) ->getMock(); + + $this->socket = $this->getMockBuilder('React\Socket\Server') + ->disableOriginalConstructor() + ->setMethods(null) + ->getMock(); } public function testRequestEventIsEmitted() { - $socket = new Socket($this->loop); - - $server = new Server($socket); + $server = new Server($this->socket); $server->on('request', $this->expectCallableOnce()); - $socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', array($this->connection)); $data = $this->createGetRequest(); $this->connection->emit('data', array($data)); @@ -49,13 +49,11 @@ public function testRequestEventIsEmitted() public function testRequestEvent() { - $socket = new Socket($this->loop); - $i = 0; $requestAssertion = null; $responseAssertion = null; - $server = new Server($socket); + $server = new Server($this->socket); $server->on('request', function ($request, $response) use (&$i, &$requestAssertion, &$responseAssertion) { $i++; $requestAssertion = $request; @@ -67,7 +65,7 @@ public function testRequestEvent() ->method('getRemoteAddress') ->willReturn('127.0.0.1'); - $socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', array($this->connection)); $data = $this->createGetRequest(); $this->connection->emit('data', array($data)); @@ -83,40 +81,43 @@ public function testRequestEvent() public function testResponseContainsPoweredByHeader() { - $socket = new Socket($this->loop); - - $server = new Server($socket); + $server = new Server($this->socket); $server->on('request', function (Request $request, Response $response) { $response->writeHead(); $response->end(); }); + $buffer = ''; + $this->connection - ->expects($this->exactly(2)) + ->expects($this->any()) ->method('write') - ->withConsecutive( - array($this->equalTo("HTTP/1.1 200 OK\r\nX-Powered-By: React/alpha\r\nTransfer-Encoding: chunked\r\n\r\n")), - array($this->equalTo("0\r\n\r\n")) + ->will( + $this->returnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } + ) ); - $socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', array($this->connection)); $data = $this->createGetRequest(); $this->connection->emit('data', array($data)); + + $this->assertContains("\r\nX-Powered-By: React/alpha\r\n", $buffer); } public function testParserErrorEmitted() { - $socket = new Socket($this->loop); - $error = null; - $server = new Server($socket); + $server = new Server($this->socket); $server->on('headers', $this->expectCallableNever()); $server->on('error', function ($message) use (&$error) { $error = $message; }); - $socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\nX-DATA: "; $data .= str_repeat('A', 4097 - strlen($data)) . "\r\n\r\n"; From 714991f992bead6402300ba2f84adbc9e0087db4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 1 Apr 2016 12:04:40 +0200 Subject: [PATCH 039/456] 2016-04-01: Let's support PHP 5.3, again.. --- .travis.yml | 1 + composer.json | 6 +++--- src/RequestHeaderParser.php | 6 +++--- src/Response.php | 16 ++++++++-------- src/Server.php | 13 +++++++------ tests/ServerTest.php | 2 +- 6 files changed, 23 insertions(+), 21 deletions(-) diff --git a/.travis.yml b/.travis.yml index 94d55bc3..db37918b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,7 @@ language: php php: + - 5.3 - 5.4 - 5.5 - 5.6 diff --git a/composer.json b/composer.json index cfc6f04a..3d431527 100644 --- a/composer.json +++ b/composer.json @@ -4,11 +4,11 @@ "keywords": ["http"], "license": "MIT", "require": { - "php": ">=5.4.0", - "guzzlehttp/psr7": "^1.0", + "php": ">=5.3.0", + "ringcentral/psr7": "^1.0", "react/socket": "^0.4", "react/stream": "^0.4", - "evenement/evenement": "^2.0" + "evenement/evenement": "^2.0 || ^1.0" }, "autoload": { "psr-4": { diff --git a/src/RequestHeaderParser.php b/src/RequestHeaderParser.php index af70ed1b..7c44ab02 100644 --- a/src/RequestHeaderParser.php +++ b/src/RequestHeaderParser.php @@ -4,7 +4,7 @@ use Evenement\EventEmitter; use Exception; -use GuzzleHttp\Psr7 as g7; +use RingCentral\Psr7 as g7; /** * @event headers @@ -37,7 +37,7 @@ public function feed($data) try { $this->parseAndEmitRequest(); } catch (Exception $exception) { - $this->emit('error', [$exception]); + $this->emit('error', array($exception)); } $this->removeAllListeners(); } @@ -55,7 +55,7 @@ public function parseRequest($data) $psrRequest = g7\parse_request($headers); - $parsedQuery = []; + $parsedQuery = array(); $queryString = $psrRequest->getUri()->getQuery(); if ($queryString) { parse_str($queryString, $parsedQuery); diff --git a/src/Response.php b/src/Response.php index 36e375fc..bab5fc0f 100644 --- a/src/Response.php +++ b/src/Response.php @@ -17,18 +17,18 @@ class Response extends EventEmitter implements WritableStreamInterface public function __construct(ConnectionInterface $conn) { $this->conn = $conn; - - $this->conn->on('end', function () { - $this->close(); + $that = $this; + $this->conn->on('end', function () use ($that) { + $that->close(); }); - $this->conn->on('error', function ($error) { - $this->emit('error', array($error, $this)); - $this->close(); + $this->conn->on('error', function ($error) use ($that) { + $that->emit('error', array($error, $that)); + $that->close(); }); - $this->conn->on('drain', function () { - $this->emit('drain'); + $this->conn->on('drain', function () use ($that) { + $that->emit('drain'); }); } diff --git a/src/Server.php b/src/Server.php index 49535060..2f5f641a 100644 --- a/src/Server.php +++ b/src/Server.php @@ -14,18 +14,19 @@ class Server extends EventEmitter implements ServerInterface public function __construct(SocketServerInterface $io) { $this->io = $io; + $that = $this; - $this->io->on('connection', function (ConnectionInterface $conn) { + $this->io->on('connection', function (ConnectionInterface $conn) use ($that) { // TODO: http 1.1 keep-alive // TODO: chunked transfer encoding (also for outgoing data) // TODO: multipart parsing $parser = new RequestHeaderParser(); - $parser->on('headers', function (Request $request, $bodyBuffer) use ($conn, $parser) { + $parser->on('headers', function (Request $request, $bodyBuffer) use ($conn, $parser, $that) { // attach remote ip to the request as metadata $request->remoteAddress = $conn->getRemoteAddress(); - $this->handleRequest($conn, $request, $bodyBuffer); + $that->handleRequest($conn, $request, $bodyBuffer); $conn->removeListener('data', array($parser, 'feed')); $conn->on('end', function () use ($request) { @@ -42,12 +43,12 @@ public function __construct(SocketServerInterface $io) }); }); - $listener = [$parser, 'feed']; + $listener = array($parser, 'feed'); $conn->on('data', $listener); - $parser->on('error', function() use ($conn, $listener) { + $parser->on('error', function() use ($conn, $listener, $that) { // TODO: return 400 response $conn->removeListener('data', $listener); - $this->emit('error', func_get_args()); + $that->emit('error', func_get_args()); }); }); } diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 5cef7cfc..e79347e9 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -121,7 +121,7 @@ public function testParserErrorEmitted() $data = "GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\nX-DATA: "; $data .= str_repeat('A', 4097 - strlen($data)) . "\r\n\r\n"; - $this->connection->emit('data', [$data]); + $this->connection->emit('data', array($data)); $this->assertInstanceOf('OverflowException', $error); $this->connection->expects($this->never())->method('write'); From 142fe540e30e5c93f8fb9460b6d29cb0317cf3d5 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Fri, 10 Feb 2017 07:45:16 +0100 Subject: [PATCH 040/456] Added v0.4.3 to the changelog --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c0167b52..27639e09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## 0.4.3 (2017-02-10) + +* First class support for PHP7 and HHVM #102 @clue +* Improve compatibility with legacy versions #101 @clue +* Remove unneeded stubs from tests #100 @legionth +* Replace PHPUnit's getMock() for forward compatibility #93 @nopolabs +* Add PHPUnit 4.8 to require-dev #92 @nopolabs +* Fix checking maximum header size, do not take start of body into account #88 @nopolabs +* data listener is removed if HeaderParser emits error #83 @nick4fake +* Removed testing against HHVM nightly #66 @WyriHaximus + ## 0.4.2 (2016-11-09) * Remove all listeners after emitting error in RequestHeaderParser #68 @WyriHaximus From 04794ae226acad33e30af31fae8163b72f041f22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 10 Feb 2017 12:36:14 +0100 Subject: [PATCH 041/456] Prepare v0.4.3 release --- CHANGELOG.md | 21 +++++++++++++-------- README.md | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 27639e09..8068f512 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,14 +2,19 @@ ## 0.4.3 (2017-02-10) -* First class support for PHP7 and HHVM #102 @clue -* Improve compatibility with legacy versions #101 @clue -* Remove unneeded stubs from tests #100 @legionth -* Replace PHPUnit's getMock() for forward compatibility #93 @nopolabs -* Add PHPUnit 4.8 to require-dev #92 @nopolabs -* Fix checking maximum header size, do not take start of body into account #88 @nopolabs -* data listener is removed if HeaderParser emits error #83 @nick4fake -* Removed testing against HHVM nightly #66 @WyriHaximus +* Fix: Do not take start of body into account when checking maximum header size + (#88 by @nopolabs) + +* Fix: Remove `data` listener if `HeaderParser` emits an error + (#83 by @nick4fake) + +* First class support for PHP 5.3 through PHP 7 and HHVM + (#101 and #102 by @clue, #66 by @WyriHaximus) + +* Improve test suite by adding PHPUnit to require-dev, + improving forward compatibility with newer PHPUnit versions + and replacing unneeded test stubs + (#92 and #93 by @nopolabs, #100 by @legionth) ## 0.4.2 (2016-11-09) diff --git a/README.md b/README.md index 9bad8ad4..f73f9ab7 100644 --- a/README.md +++ b/README.md @@ -35,3 +35,35 @@ $loop->run(); ``` See also the [examples](examples). + +## Install + +The recommended way to install this library is [through Composer](http://getcomposer.org). +[New to Composer?](http://getcomposer.org/doc/00-intro.md) + +This will install the latest supported version: + +```bash +$ composer require react/http:^0.4.3 +``` + +More details about version upgrades can be found in the [CHANGELOG](CHANGELOG.md). + +## Tests + +To run the test suite, you first need to clone this repo and then install all +dependencies [through Composer](http://getcomposer.org): + +```bash +$ composer install +``` + +To run the test suite, go to the project root and run: + +```bash +$ php vendor/bin/phpunit +``` + +## License + +MIT, see [LICENSE file](LICENSE). From 0a55272ddebbf25e331da02ece6f2a6b555ead24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 13 Oct 2016 17:14:16 +0200 Subject: [PATCH 042/456] =?UTF-8?q?Add=20request=20header=20accessors=20(?= =?UTF-8?q?=C3=A0=20la=20PSR-7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 45 ++++++++++++++++++++++++++++++++++ src/Request.php | 56 +++++++++++++++++++++++++++++++++++++++++++ tests/RequestTest.php | 34 ++++++++++++++++++++++++++ 3 files changed, 135 insertions(+) diff --git a/README.md b/README.md index f73f9ab7..1a8cafe6 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,51 @@ $loop->run(); See also the [examples](examples). +## Usage + +### Server + +See the above usage example and the class outline for details. + +### Request + +See the above usage example and the class outline for details. + +#### getHeaders() + +The `getHeaders(): array` method can be used to +return ALL headers. + +This will return an (possibly empty) assoc array with header names as +key and header values as value. The header value will be a string if +there's only a single value or an array of strings if this header has +multiple values. + +Note that this differs from the PSR-7 implementation of this method. + +#### getHeader() + +The `getHeader(string $name): string[]` method can be used to +retrieve a message header value by the given case-insensitive name. + +Returns a list of all values for this header name or an empty array if header was not found + +#### getHeaderLine() + +The `getHeaderLine(string $name): string` method can be used to +retrieve a comma-separated string of the values for a single header. + +Returns a comma-separated list of all values for this header name or an empty string if header was not found + +#### hasHeader() + +The `hasHeader(string $name): bool` method can be used to +check if a header exists by the given case-insensitive name. + +### Response + +See the above usage example and the class outline for details. + ## Install The recommended way to install this library is [through Composer](http://getcomposer.org). diff --git a/src/Request.php b/src/Request.php index 605b909e..af11fc91 100644 --- a/src/Request.php +++ b/src/Request.php @@ -48,11 +48,67 @@ public function getHttpVersion() return $this->httpVersion; } + /** + * Returns ALL headers + * + * This will return an (possibly empty) assoc array with header names as + * key and header values as value. The header value will be a string if + * there's only a single value or an array of strings if this header has + * multiple values. + * + * Note that this differs from the PSR-7 implementation of this method. + * + * @return array + */ public function getHeaders() { return $this->headers; } + /** + * Retrieves a message header value by the given case-insensitive name. + * + * @param string $name + * @return string[] a list of all values for this header name or an empty array if header was not found + */ + public function getHeader($name) + { + $found = array(); + + $name = strtolower($name); + foreach ($this->headers as $key => $value) { + if (strtolower($key) === $name) { + foreach((array)$value as $one) { + $found []= $one; + } + } + } + + return $found; + } + + /** + * Retrieves a comma-separated string of the values for a single header. + * + * @param string $name + * @return string a comma-separated list of all values for this header name or an empty string if header was not found + */ + public function getHeaderLine($name) + { + return implode(', ', $this->getHeader($name)); + } + + /** + * Checks if a header exists by the given case-insensitive name. + * + * @param string $name + * @return bool + */ + public function hasHeader($name) + { + return !!$this->getHeader($name); + } + public function expectsContinue() { return isset($this->headers['Expect']) && '100-continue' === $this->headers['Expect']; diff --git a/tests/RequestTest.php b/tests/RequestTest.php index 1ad85221..d7dd2496 100644 --- a/tests/RequestTest.php +++ b/tests/RequestTest.php @@ -23,4 +23,38 @@ public function expectsContinueShouldBeTrueIfContinueExpected() $this->assertTrue($request->expectsContinue()); } + + public function testEmptyHeader() + { + $request = new Request('GET', '/'); + + $this->assertEquals(array(), $request->getHeaders()); + $this->assertFalse($request->hasHeader('Test')); + $this->assertEquals(array(), $request->getHeader('Test')); + $this->assertEquals('', $request->getHeaderLine('Test')); + } + + public function testHeaderIsCaseInsensitive() + { + $request = new Request('GET', '/', array(), '1.1', array( + 'TEST' => 'Yes', + )); + + $this->assertEquals(array('TEST' => 'Yes'), $request->getHeaders()); + $this->assertTrue($request->hasHeader('Test')); + $this->assertEquals(array('Yes'), $request->getHeader('Test')); + $this->assertEquals('Yes', $request->getHeaderLine('Test')); + } + + public function testHeaderWithMultipleValues() + { + $request = new Request('GET', '/', array(), '1.1', array( + 'Test' => array('a', 'b'), + )); + + $this->assertEquals(array('Test' => array('a', 'b')), $request->getHeaders()); + $this->assertTrue($request->hasHeader('Test')); + $this->assertEquals(array('a', 'b'), $request->getHeader('Test')); + $this->assertEquals('a, b', $request->getHeaderLine('Test')); + } } From 0a0f6833c0c4cfe4d1967aed6095153c51fd2791 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 9 Feb 2017 21:48:59 +0100 Subject: [PATCH 043/456] Be explicit about differences with PSR-7 --- README.md | 14 ++++++++------ src/Request.php | 16 +++++++++------- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 1a8cafe6..1076eac6 100644 --- a/README.md +++ b/README.md @@ -49,14 +49,16 @@ See the above usage example and the class outline for details. #### getHeaders() The `getHeaders(): array` method can be used to -return ALL headers. +return an array with ALL headers. -This will return an (possibly empty) assoc array with header names as -key and header values as value. The header value will be a string if -there's only a single value or an array of strings if this header has -multiple values. +The keys represent the header name in the exact case in which they were +originally specified. The values will be a string if there's only a single +value for the respective header name or an array of strings if this header +has multiple values. -Note that this differs from the PSR-7 implementation of this method. +> Note that this differs from the PSR-7 implementation of this method, +which always returns an array for each header name, even if it only has a +single value. #### getHeader() diff --git a/src/Request.php b/src/Request.php index af11fc91..ec2041d6 100644 --- a/src/Request.php +++ b/src/Request.php @@ -49,14 +49,16 @@ public function getHttpVersion() } /** - * Returns ALL headers + * Returns an array with ALL headers * - * This will return an (possibly empty) assoc array with header names as - * key and header values as value. The header value will be a string if - * there's only a single value or an array of strings if this header has - * multiple values. + * The keys represent the header name in the exact case in which they were + * originally specified. The values will be a string if there's only a single + * value for the respective header name or an array of strings if this header + * has multiple values. * - * Note that this differs from the PSR-7 implementation of this method. + * Note that this differs from the PSR-7 implementation of this method, + * which always returns an array for each header name, even if it only has a + * single value. * * @return array */ @@ -79,7 +81,7 @@ public function getHeader($name) foreach ($this->headers as $key => $value) { if (strtolower($key) === $name) { foreach((array)$value as $one) { - $found []= $one; + $found[] = $one; } } } From 9d34ed76e94b72bc1246fb07c9674101ca8cf3da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 9 Feb 2017 15:11:46 +0100 Subject: [PATCH 044/456] Fix headers to be handled case insensitive --- src/Request.php | 2 +- src/Response.php | 24 +++++++++++--- tests/RequestTest.php | 9 ++++++ tests/ResponseTest.php | 72 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 101 insertions(+), 6 deletions(-) diff --git a/src/Request.php b/src/Request.php index ec2041d6..bde145b0 100644 --- a/src/Request.php +++ b/src/Request.php @@ -113,7 +113,7 @@ public function hasHeader($name) public function expectsContinue() { - return isset($this->headers['Expect']) && '100-continue' === $this->headers['Expect']; + return '100-continue' === $this->getHeaderLine('Expect'); } public function isReadable() diff --git a/src/Response.php b/src/Response.php index bab5fc0f..851aeb98 100644 --- a/src/Response.php +++ b/src/Response.php @@ -52,15 +52,29 @@ public function writeHead($status = 200, array $headers = array()) throw new \Exception('Response head has already been written.'); } - if (isset($headers['Content-Length'])) { + $lower = array_change_key_case($headers); + + // disable chunked encoding if content-length is given + if (isset($lower['content-length'])) { $this->chunkedEncoding = false; } - $headers = array_merge( - array('X-Powered-By' => 'React/alpha'), - $headers - ); + // assign default "X-Powered-By" header as first for history reasons + if (!isset($lower['x-powered-by'])) { + $headers = array_merge( + array('X-Powered-By' => 'React/alpha'), + $headers + ); + } + + // assign chunked transfer-encoding if chunked encoding is used if ($this->chunkedEncoding) { + foreach($headers as $name => $value) { + if (strtolower($name) === 'transfer-encoding') { + unset($headers[$name]); + } + } + $headers['Transfer-Encoding'] = 'chunked'; } diff --git a/tests/RequestTest.php b/tests/RequestTest.php index d7dd2496..004f7891 100644 --- a/tests/RequestTest.php +++ b/tests/RequestTest.php @@ -24,6 +24,15 @@ public function expectsContinueShouldBeTrueIfContinueExpected() $this->assertTrue($request->expectsContinue()); } + /** @test */ + public function expectsContinueShouldBeTrueIfContinueExpectedCaseInsensitive() + { + $headers = array('EXPECT' => '100-continue'); + $request = new Request('GET', '/', array(), '1.1', $headers); + + $this->assertTrue($request->expectsContinue()); + } + public function testEmptyHeader() { $request = new Request('GET', '/'); diff --git a/tests/ResponseTest.php b/tests/ResponseTest.php index 11407f28..20ccfa6a 100644 --- a/tests/ResponseTest.php +++ b/tests/ResponseTest.php @@ -26,6 +26,25 @@ public function testResponseShouldBeChunkedByDefault() $response->writeHead(); } + public function testResponseShouldBeChunkedEvenWithOtherTransferEncoding() + { + $expected = ''; + $expected .= "HTTP/1.1 200 OK\r\n"; + $expected .= "X-Powered-By: React/alpha\r\n"; + $expected .= "Transfer-Encoding: chunked\r\n"; + $expected .= "\r\n"; + + $conn = $this->getMock('React\Socket\ConnectionInterface'); + $conn + ->expects($this->once()) + ->method('write') + ->with($expected); + + $response = new Response($conn); + $response->writeHead(200, array('transfer-encoding' => 'custom')); + } + + public function testResponseShouldNotBeChunkedWithContentLength() { $expected = ''; @@ -46,6 +65,59 @@ public function testResponseShouldNotBeChunkedWithContentLength() $response->writeHead(200, array('Content-Length' => 22)); } + public function testResponseShouldNotBeChunkedWithContentLengthCaseInsensitive() + { + $expected = ''; + $expected .= "HTTP/1.1 200 OK\r\n"; + $expected .= "X-Powered-By: React/alpha\r\n"; + $expected .= "CONTENT-LENGTH: 0\r\n"; + $expected .= "\r\n"; + + $conn = $this->getMock('React\Socket\ConnectionInterface'); + $conn + ->expects($this->once()) + ->method('write') + ->with($expected); + + $response = new Response($conn); + $response->writeHead(200, array('CONTENT-LENGTH' => 0)); + } + + public function testResponseShouldIncludeCustomByPoweredAsFirstHeaderIfGivenExplicitly() + { + $expected = ''; + $expected .= "HTTP/1.1 200 OK\r\n"; + $expected .= "Content-Length: 0\r\n"; + $expected .= "X-POWERED-BY: demo\r\n"; + $expected .= "\r\n"; + + $conn = $this->getMock('React\Socket\ConnectionInterface'); + $conn + ->expects($this->once()) + ->method('write') + ->with($expected); + + $response = new Response($conn); + $response->writeHead(200, array('Content-Length' => 0, 'X-POWERED-BY' => 'demo')); + } + + public function testResponseShouldNotIncludePoweredByIfGivenEmptyArray() + { + $expected = ''; + $expected .= "HTTP/1.1 200 OK\r\n"; + $expected .= "Content-Length: 0\r\n"; + $expected .= "\r\n"; + + $conn = $this->getMock('React\Socket\ConnectionInterface'); + $conn + ->expects($this->once()) + ->method('write') + ->with($expected); + + $response = new Response($conn); + $response->writeHead(200, array('Content-Length' => 0, 'X-Powered-By' => array())); + } + public function testResponseBodyShouldBeChunkedCorrectly() { $conn = $this From 20f7e69a43fb356016ba408f0d8e9c4cf0283804 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 10 Feb 2017 13:57:58 +0100 Subject: [PATCH 045/456] Documentation for writeContinue() and writeHead() --- README.md | 105 +++++++++++++++++++++++++++++++++++++++++++++++ src/Request.php | 10 +++++ src/Response.php | 91 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 206 insertions(+) diff --git a/README.md b/README.md index 1076eac6..8caa3741 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,11 @@ See the above usage example and the class outline for details. ### Request +The `Request` class is responsible for streaming the incoming request body +and contains meta data which was parsed from the request headers. + +It implements the `ReadableStreamInterface`. + See the above usage example and the class outline for details. #### getHeaders() @@ -79,10 +84,110 @@ Returns a comma-separated list of all values for this header name or an empty st The `hasHeader(string $name): bool` method can be used to check if a header exists by the given case-insensitive name. +#### expectsContinue() + +The `expectsContinue(): bool` method can be used to +check if the request headers contain the `Expect: 100-continue` header. + +This header MAY be included when an HTTP/1.1 client wants to send a bigger +request body. +See [`writeContinue()`](#writecontinue) for more details. + ### Response +The `Response` class is responsible for streaming the outgoing response body. + +It implements the `WritableStreamInterface`. + See the above usage example and the class outline for details. +#### writeContinue() + +The `writeContinue(): void` method can be used to +send an intermediary `HTTP/1.1 100 continue` response. + +This is a feature that is implemented by *many* HTTP/1.1 clients. +When clients want to send a bigger request body, they MAY send only the request +headers with an additional `Expect: 100-continue` header and wait before +sending the actual (large) message body. + +The server side MAY use this header to verify if the request message is +acceptable by checking the request headers (such as `Content-Length` or HTTP +authentication) and then ask the client to continue with sending the message body. +Otherwise, the server can send a normal HTTP response message and save the +client from transfering the whole body at all. + +This method is mostly useful in combination with the +[`expectsContinue()`](#expectscontinue) method like this: + +```php +$http->on('request', function (Request $request, Response $response) { + if ($request->expectsContinue()) { + $response->writeContinue(); + } + + $response->writeHead(200, array('Content-Type' => 'text/plain')); + $response->end("Hello World!\n"); +}); +``` + +Note that calling this method is strictly optional. +If you do not use it, then the client MUST continue sending the request body +after waiting some time. + +This method MUST NOT be invoked after calling `writeHead()`. +Calling this method after sending the headers will result in an `Exception`. + +#### writeHead() + +The `writeHead(int $status = 200, array $headers = array(): void` method can be used to +write the given HTTP message header. + +This method MUST be invoked once before calling `write()` or `end()` to send +the actual HTTP message body: + +```php +$response->writeHead(200, array( + 'Content-Type' => 'text/plain' +)); +$response->end('Hello World!'); +``` + +Calling this method more than once will result in an `Exception`. + +Unless you specify a `Content-Length` header yourself, the response message +will automatically use chunked transfer encoding and send the respective header +(`Transfer-Encoding: chunked`) automatically. If you know the length of your +body, you MAY specify it like this instead: + +```php +$data = 'Hello World!'; + +$response->writeHead(200, array( + 'Content-Type' => 'text/plain', + 'Content-Length' => strlen($data) +)); +$response->end($data); +``` + +Note that it will automatically assume a `X-Powered-By: react/alpha` header +unless your specify a custom `X-Powered-By` header yourself: + +```php +$response->writeHead(200, array( + 'X-Powered-By' => 'PHP 3' +)); +``` + +If you do not want to send this header at all, you can use an empty array as +value like this: + +```php +$response->writeHead(200, array( + 'X-Powered-By' => array() +)); +``` + ## Install The recommended way to install this library is [through Composer](http://getcomposer.org). diff --git a/src/Request.php b/src/Request.php index bde145b0..e6745dda 100644 --- a/src/Request.php +++ b/src/Request.php @@ -111,6 +111,16 @@ public function hasHeader($name) return !!$this->getHeader($name); } + /** + * Checks if the request headers contain the `Expect: 100-continue` header. + * + * This header MAY be included when an HTTP/1.1 client wants to send a bigger + * request body. + * See [`writeContinue()`] for more details. + * + * @return bool + * @see Response::writeContinue() + */ public function expectsContinue() { return '100-continue' === $this->getHeaderLine('Expect'); diff --git a/src/Response.php b/src/Response.php index 851aeb98..34c0606d 100644 --- a/src/Response.php +++ b/src/Response.php @@ -37,6 +37,45 @@ public function isWritable() return $this->writable; } + /** + * Sends an intermediary `HTTP/1.1 100 continue` response. + * + * This is a feature that is implemented by *many* HTTP/1.1 clients. + * When clients want to send a bigger request body, they MAY send only the request + * headers with an additional `Expect: 100-continue` header and wait before + * sending the actual (large) message body. + * + * The server side MAY use this header to verify if the request message is + * acceptable by checking the request headers (such as `Content-Length` or HTTP + * authentication) and then ask the client to continue with sending the message body. + * Otherwise, the server can send a normal HTTP response message and save the + * client from transfering the whole body at all. + * + * This method is mostly useful in combination with the + * [`expectsContinue()`] method like this: + * + * ```php + * $http->on('request', function (Request $request, Response $response) { + * if ($request->expectsContinue()) { + * $response->writeContinue(); + * } + * + * $response->writeHead(200, array('Content-Type' => 'text/plain')); + * $response->end("Hello World!\n"); + * }); + * ``` + * + * Note that calling this method is strictly optional. + * If you do not use it, then the client MUST continue sending the request body + * after waiting some time. + * + * This method MUST NOT be invoked after calling `writeHead()`. + * Calling this method after sending the headers will result in an `Exception`. + * + * @return void + * @throws \Exception + * @see Request::expectsContinue() + */ public function writeContinue() { if ($this->headWritten) { @@ -46,6 +85,58 @@ public function writeContinue() $this->conn->write("HTTP/1.1 100 Continue\r\n\r\n"); } + /** + * Writes the given HTTP message header. + * + * This method MUST be invoked once before calling `write()` or `end()` to send + * the actual HTTP message body: + * + * ```php + * $response->writeHead(200, array( + * 'Content-Type' => 'text/plain' + * )); + * $response->end('Hello World!'); + * ``` + * + * Calling this method more than once will result in an `Exception`. + * + * Unless you specify a `Content-Length` header yourself, the response message + * will automatically use chunked transfer encoding and send the respective header + * (`Transfer-Encoding: chunked`) automatically. If you know the length of your + * body, you MAY specify it like this instead: + * + * ```php + * $data = 'Hello World!'; + * + * $response->writeHead(200, array( + * 'Content-Type' => 'text/plain', + * 'Content-Length' => strlen($data) + * )); + * $response->end($data); + * ``` + * + * Note that it will automatically assume a `X-Powered-By: react/alpha` header + * unless your specify a custom `X-Powered-By` header yourself: + * + * ```php + * $response->writeHead(200, array( + * 'X-Powered-By' => 'PHP 3' + * )); + * ``` + * + * If you do not want to send this header at all, you can use an empty array as + * value like this: + * + * ```php + * $response->writeHead(200, array( + * 'X-Powered-By' => array() + * )); + * ``` + * + * @param int $status + * @param array $headers + * @throws \Exception + */ public function writeHead($status = 200, array $headers = array()) { if ($this->headWritten) { From 14e82b979da09e34e30019c6b8effd5c6a7c8a60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 10 Feb 2017 15:53:54 +0100 Subject: [PATCH 046/456] Forward pause/resume from request to connection --- src/Server.php | 10 ++++------ tests/ServerTest.php | 28 ++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/src/Server.php b/src/Server.php index 2f5f641a..f6e45a53 100644 --- a/src/Server.php +++ b/src/Server.php @@ -26,6 +26,10 @@ public function __construct(SocketServerInterface $io) // attach remote ip to the request as metadata $request->remoteAddress = $conn->getRemoteAddress(); + // forward pause/resume calls to underlying connection + $request->on('pause', array($conn, 'pause')); + $request->on('resume', array($conn, 'resume')); + $that->handleRequest($conn, $request, $bodyBuffer); $conn->removeListener('data', array($parser, 'feed')); @@ -35,12 +39,6 @@ public function __construct(SocketServerInterface $io) $conn->on('data', function ($data) use ($request) { $request->emit('data', array($data)); }); - $request->on('pause', function () use ($conn) { - $conn->emit('pause'); - }); - $request->on('resume', function () use ($conn) { - $conn->emit('resume'); - }); }); $listener = array($parser, 'feed'); diff --git a/tests/ServerTest.php b/tests/ServerTest.php index e79347e9..b9234521 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -79,6 +79,34 @@ public function testRequestEvent() $this->assertInstanceOf('React\Http\Response', $responseAssertion); } + public function testRequestPauseWillbeForwardedToConnection() + { + $server = new Server($this->socket); + $server->on('request', function (Request $request) { + $request->pause(); + }); + + $this->connection->expects($this->once())->method('pause'); + $this->socket->emit('connection', array($this->connection)); + + $data = $this->createGetRequest(); + $this->connection->emit('data', array($data)); + } + + public function testRequestResumeWillbeForwardedToConnection() + { + $server = new Server($this->socket); + $server->on('request', function (Request $request) { + $request->resume(); + }); + + $this->connection->expects($this->once())->method('resume'); + $this->socket->emit('connection', array($this->connection)); + + $data = $this->createGetRequest(); + $this->connection->emit('data', array($data)); + } + public function testResponseContainsPoweredByHeader() { $server = new Server($this->socket); From 7333aebffa7da8ff587b40739ae01ccf9ec6bd6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 10 Feb 2017 16:17:25 +0100 Subject: [PATCH 047/456] The Expect field-value is case-insensitive. --- src/Request.php | 2 +- tests/RequestTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Request.php b/src/Request.php index e6745dda..af98ed24 100644 --- a/src/Request.php +++ b/src/Request.php @@ -123,7 +123,7 @@ public function hasHeader($name) */ public function expectsContinue() { - return '100-continue' === $this->getHeaderLine('Expect'); + return '100-continue' === strtolower($this->getHeaderLine('Expect')); } public function isReadable() diff --git a/tests/RequestTest.php b/tests/RequestTest.php index 004f7891..ee79ec1f 100644 --- a/tests/RequestTest.php +++ b/tests/RequestTest.php @@ -27,7 +27,7 @@ public function expectsContinueShouldBeTrueIfContinueExpected() /** @test */ public function expectsContinueShouldBeTrueIfContinueExpectedCaseInsensitive() { - $headers = array('EXPECT' => '100-continue'); + $headers = array('EXPECT' => '100-CONTINUE'); $request = new Request('GET', '/', array(), '1.1', $headers); $this->assertTrue($request->expectsContinue()); From fabc828113a98de12e1376e71ef7f4e47e60ddb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 10 Feb 2017 16:22:16 +0100 Subject: [PATCH 048/456] Ignore 100-continue expectation for HTTP/1.0 requests --- README.md | 3 +++ src/Request.php | 5 ++++- tests/RequestTest.php | 9 +++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8caa3741..04db55aa 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,9 @@ This header MAY be included when an HTTP/1.1 client wants to send a bigger request body. See [`writeContinue()`](#writecontinue) for more details. +This will always be `false` for HTTP/1.0 requests, regardless of what +any header values say. + ### Response The `Response` class is responsible for streaming the outgoing response body. diff --git a/src/Request.php b/src/Request.php index af98ed24..c9afe22a 100644 --- a/src/Request.php +++ b/src/Request.php @@ -118,12 +118,15 @@ public function hasHeader($name) * request body. * See [`writeContinue()`] for more details. * + * This will always be `false` for HTTP/1.0 requests, regardless of what + * any header values say. + * * @return bool * @see Response::writeContinue() */ public function expectsContinue() { - return '100-continue' === strtolower($this->getHeaderLine('Expect')); + return $this->httpVersion !== '1.0' && '100-continue' === strtolower($this->getHeaderLine('Expect')); } public function isReadable() diff --git a/tests/RequestTest.php b/tests/RequestTest.php index ee79ec1f..e5696749 100644 --- a/tests/RequestTest.php +++ b/tests/RequestTest.php @@ -33,6 +33,15 @@ public function expectsContinueShouldBeTrueIfContinueExpectedCaseInsensitive() $this->assertTrue($request->expectsContinue()); } + /** @test */ + public function expectsContinueShouldBeFalseForHttp10() + { + $headers = array('Expect' => '100-continue'); + $request = new Request('GET', '/', array(), '1.0', $headers); + + $this->assertFalse($request->expectsContinue()); + } + public function testEmptyHeader() { $request = new Request('GET', '/'); From 9c6dcdf12ffd6c0f3044f9fa38d951fc980e8bd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 9 Feb 2017 16:43:22 +0100 Subject: [PATCH 049/456] Do not emit empty data events --- composer.json | 2 +- src/Server.php | 5 +++- tests/ServerTest.php | 68 ++++++++++++++++++++++++++++++++++++++++++++ tests/TestCase.php | 11 +++++++ 4 files changed, 84 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 3d431527..3cee9484 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ "php": ">=5.3.0", "ringcentral/psr7": "^1.0", "react/socket": "^0.4", - "react/stream": "^0.4", + "react/stream": "^0.4.4", "evenement/evenement": "^2.0 || ^1.0" }, "autoload": { diff --git a/src/Server.php b/src/Server.php index f6e45a53..ba9daed5 100644 --- a/src/Server.php +++ b/src/Server.php @@ -63,6 +63,9 @@ public function handleRequest(ConnectionInterface $conn, Request $request, $body } $this->emit('request', array($request, $response)); - $request->emit('data', array($bodyBuffer)); + + if ($bodyBuffer !== '') { + $request->emit('data', array($bodyBuffer)); + } } } diff --git a/tests/ServerTest.php b/tests/ServerTest.php index b9234521..6974e2c9 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -36,6 +36,18 @@ public function setUp() ->getMock(); } + public function testRequestEventWillNotBeEmittedForIncompleteHeaders() + { + $server = new Server($this->socket); + $server->on('request', $this->expectCallableNever()); + + $this->socket->emit('connection', array($this->connection)); + + $data = ''; + $data .= "GET / HTTP/1.1\r\n"; + $this->connection->emit('data', array($data)); + } + public function testRequestEventIsEmitted() { $server = new Server($this->socket); @@ -107,6 +119,62 @@ public function testRequestResumeWillbeForwardedToConnection() $this->connection->emit('data', array($data)); } + public function testRequestEventWithoutBodyWillNotEmitData() + { + $never = $this->expectCallableNever(); + + $server = new Server($this->socket); + $server->on('request', function (Request $request) use ($never) { + $request->on('data', $never); + }); + + $this->socket->emit('connection', array($this->connection)); + + $data = $this->createGetRequest(); + $this->connection->emit('data', array($data)); + } + + public function testRequestEventWithSecondDataEventWillEmitBodyData() + { + $once = $this->expectCallableOnceWith('incomplete'); + + $server = new Server($this->socket); + $server->on('request', function (Request $request) use ($once) { + $request->on('data', $once); + }); + + $this->socket->emit('connection', array($this->connection)); + + $data = ''; + $data .= "POST / HTTP/1.1\r\n"; + $data .= "Content-Length: 100\r\n"; + $data .= "\r\n"; + $data .= "incomplete"; + $this->connection->emit('data', array($data)); + } + + public function testRequestEventWithPartialBodyWillEmitData() + { + $once = $this->expectCallableOnceWith('incomplete'); + + $server = new Server($this->socket); + $server->on('request', function (Request $request) use ($once) { + $request->on('data', $once); + }); + + $this->socket->emit('connection', array($this->connection)); + + $data = ''; + $data .= "POST / HTTP/1.1\r\n"; + $data .= "Content-Length: 100\r\n"; + $data .= "\r\n"; + $this->connection->emit('data', array($data)); + + $data = ''; + $data .= "incomplete"; + $this->connection->emit('data', array($data)); + } + public function testResponseContainsPoweredByHeader() { $server = new Server($this->socket); diff --git a/tests/TestCase.php b/tests/TestCase.php index 24fe27f2..73bb401e 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -24,6 +24,17 @@ protected function expectCallableOnce() return $mock; } + protected function expectCallableOnceWith($value) + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($value); + + return $mock; + } + protected function expectCallableNever() { $mock = $this->createCallableMock(); From 4734b052d8b484b4bd232f20d3a67f2fefb41bfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 9 Feb 2017 22:51:48 +0100 Subject: [PATCH 050/456] Ignore empty writes to not mess up chunked transfer encoding --- src/Response.php | 14 +++++++++----- tests/ResponseTest.php | 27 +++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/src/Response.php b/src/Response.php index bab5fc0f..968894ab 100644 --- a/src/Response.php +++ b/src/Response.php @@ -96,15 +96,19 @@ public function write($data) throw new \Exception('Response head has not yet been written.'); } + // prefix with chunk length for chunked transfer encoding if ($this->chunkedEncoding) { $len = strlen($data); - $chunk = dechex($len)."\r\n".$data."\r\n"; - $flushed = $this->conn->write($chunk); - } else { - $flushed = $this->conn->write($data); + + // skip empty chunks + if ($len === 0) { + return true; + } + + $data = dechex($len) . "\r\n" . $data . "\r\n"; } - return $flushed; + return $this->conn->write($data); } public function end($data = null) diff --git a/tests/ResponseTest.php b/tests/ResponseTest.php index 11407f28..db47d835 100644 --- a/tests/ResponseTest.php +++ b/tests/ResponseTest.php @@ -77,6 +77,33 @@ public function testResponseBodyShouldBeChunkedCorrectly() $response->end(); } + public function testResponseBodyShouldSkipEmptyChunks() + { + $conn = $this + ->getMockBuilder('React\Socket\ConnectionInterface') + ->getMock(); + $conn + ->expects($this->at(4)) + ->method('write') + ->with("5\r\nHello\r\n"); + $conn + ->expects($this->at(5)) + ->method('write') + ->with("5\r\nWorld\r\n"); + $conn + ->expects($this->at(6)) + ->method('write') + ->with("0\r\n\r\n"); + + $response = new Response($conn); + $response->writeHead(); + + $response->write('Hello'); + $response->write(''); + $response->write('World'); + $response->end(); + } + public function testResponseShouldEmitEndOnStreamEnd() { $ended = false; From 08349046483e85093dac8323933b0fecd18741d0 Mon Sep 17 00:00:00 2001 From: andig Date: Sat, 11 Feb 2017 15:50:36 +0100 Subject: [PATCH 051/456] Upgrade ringcentral/psr7 to 1.2 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 3cee9484..a8d1770c 100644 --- a/composer.json +++ b/composer.json @@ -5,7 +5,7 @@ "license": "MIT", "require": { "php": ">=5.3.0", - "ringcentral/psr7": "^1.0", + "ringcentral/psr7": "^1.2", "react/socket": "^0.4", "react/stream": "^0.4.4", "evenement/evenement": "^2.0 || ^1.0" From 422830373e37e369f2aebe63a5ac77c7fef0f6eb Mon Sep 17 00:00:00 2001 From: andig Date: Sat, 11 Feb 2017 17:01:18 +0100 Subject: [PATCH 052/456] Fix minimum phpunit version and allow v5 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 3cee9484..3664148d 100644 --- a/composer.json +++ b/composer.json @@ -16,6 +16,6 @@ } }, "require-dev": { - "phpunit/phpunit": "~4.8" + "phpunit/phpunit": "^4.8.10||^5.0" } } From a5e16872569dd49ce1d046a29306e153a84369fa Mon Sep 17 00:00:00 2001 From: andig Date: Sun, 12 Feb 2017 13:52:19 +0100 Subject: [PATCH 053/456] PhpUnit 5 compatibility --- tests/ResponseTest.php | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/tests/ResponseTest.php b/tests/ResponseTest.php index 20ccfa6a..4a063fbb 100644 --- a/tests/ResponseTest.php +++ b/tests/ResponseTest.php @@ -34,7 +34,9 @@ public function testResponseShouldBeChunkedEvenWithOtherTransferEncoding() $expected .= "Transfer-Encoding: chunked\r\n"; $expected .= "\r\n"; - $conn = $this->getMock('React\Socket\ConnectionInterface'); + $conn = $this + ->getMockBuilder('React\Socket\ConnectionInterface') + ->getMock(); $conn ->expects($this->once()) ->method('write') @@ -73,7 +75,9 @@ public function testResponseShouldNotBeChunkedWithContentLengthCaseInsensitive() $expected .= "CONTENT-LENGTH: 0\r\n"; $expected .= "\r\n"; - $conn = $this->getMock('React\Socket\ConnectionInterface'); + $conn = $this + ->getMockBuilder('React\Socket\ConnectionInterface') + ->getMock(); $conn ->expects($this->once()) ->method('write') @@ -91,7 +95,9 @@ public function testResponseShouldIncludeCustomByPoweredAsFirstHeaderIfGivenExpl $expected .= "X-POWERED-BY: demo\r\n"; $expected .= "\r\n"; - $conn = $this->getMock('React\Socket\ConnectionInterface'); + $conn = $this + ->getMockBuilder('React\Socket\ConnectionInterface') + ->getMock(); $conn ->expects($this->once()) ->method('write') @@ -108,7 +114,9 @@ public function testResponseShouldNotIncludePoweredByIfGivenEmptyArray() $expected .= "Content-Length: 0\r\n"; $expected .= "\r\n"; - $conn = $this->getMock('React\Socket\ConnectionInterface'); + $conn = $this + ->getMockBuilder('React\Socket\ConnectionInterface') + ->getMock(); $conn ->expects($this->once()) ->method('write') From e350f92dd479784aa588c3acac57a871dd8a0761 Mon Sep 17 00:00:00 2001 From: andig Date: Sat, 11 Feb 2017 15:55:12 +0100 Subject: [PATCH 054/456] Test lowest version constraints --- .travis.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.travis.yml b/.travis.yml index db37918b..f67b7d54 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,8 +8,18 @@ php: - 7 - hhvm +matrix: + include: + - php: 5.3 + env: + - DEPENDENCIES=lowest + - php: 7.0 + env: + - DEPENDENCIES=lowest + install: - composer install --no-interaction + - if [ "$DEPENDENCIES" = "lowest" ]; then composer update --prefer-lowest -n; fi script: - ./vendor/bin/phpunit --coverage-text From aac319bd789cbc7b478d42cde2d03596e97e3222 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 13 Feb 2017 15:12:50 +0100 Subject: [PATCH 055/456] Prepare v0.4.4 release --- CHANGELOG.md | 45 +++++++++++++++++++++++++++++++++++++++++++++ README.md | 2 +- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8068f512..851b1d65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,50 @@ # Changelog +## 0.4.4 (2017-02-13) + +* Feature: Add request header accessors (à la PSR-7) + (#103 by @clue) + + ```php + // get value of host header + $host = $request->getHeaderLine('Host'); + + // get list of all cookie headers + $cookies = $request->getHeader('Cookie'); + ``` + +* Feature: Forward `pause()` and `resume()` from `Request` to underlying connection + (#110 by @clue) + + ```php + // support back-pressure when piping request into slower destination + $request->pipe($dest); + + // manually pause/resume request + $request->pause(); + $request->resume(); + ``` + +* Fix: Fix `100-continue` to be handled case-insensitive and ignore it for HTTP/1.0. + Similarly, outgoing response headers are now handled case-insensitive, e.g + we no longer apply chunked transfer encoding with mixed-case `Content-Length`. + (#107 by @clue) + + ```php + // now handled case-insensitive + $request->expectsContinue(); + + // now works just like properly-cased header + $response->writeHead($status, array('content-length' => 0)); + ``` + +* Fix: Do not emit empty `data` events and ignore empty writes in order to + not mess up chunked transfer encoding + (#108 and #112 by @clue) + +* Lock and test minimum required dependency versions and support PHPUnit v5 + (#113, #115 and #114 by @andig) + ## 0.4.3 (2017-02-10) * Fix: Do not take start of body into account when checking maximum header size diff --git a/README.md b/README.md index 04db55aa..19dab73e 100644 --- a/README.md +++ b/README.md @@ -199,7 +199,7 @@ The recommended way to install this library is [through Composer](http://getcomp This will install the latest supported version: ```bash -$ composer require react/http:^0.4.3 +$ composer require react/http:^0.4.4 ``` More details about version upgrades can be found in the [CHANGELOG](CHANGELOG.md). From 0084ed43a5b8d1f232f25075d345d7e707fbd407 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 5 Feb 2017 15:59:57 +0100 Subject: [PATCH 056/456] Update Socket component to v0.5 --- README.md | 3 +-- composer.json | 2 +- examples/01-hello-world.php | 6 ++---- src/Server.php | 5 ++++- tests/ServerTest.php | 6 ++---- tests/SocketServerStub.php | 19 +++++++++++++++++++ 6 files changed, 29 insertions(+), 12 deletions(-) create mode 100644 tests/SocketServerStub.php diff --git a/README.md b/README.md index 19dab73e..f8917f27 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ This is an HTTP server which responds with `Hello World` to every request. ```php $loop = React\EventLoop\Factory::create(); -$socket = new React\Socket\Server($loop); +$socket = new React\Socket\Server(8080, $loop); $http = new React\Http\Server($socket); $http->on('request', function ($request, $response) { @@ -30,7 +30,6 @@ $http->on('request', function ($request, $response) { $response->end("Hello World!\n"); }); -$socket->listen(1337); $loop->run(); ``` diff --git a/composer.json b/composer.json index f1fea030..40c0582b 100644 --- a/composer.json +++ b/composer.json @@ -6,7 +6,7 @@ "require": { "php": ">=5.3.0", "ringcentral/psr7": "^1.2", - "react/socket": "^0.4", + "react/socket": "^0.5", "react/stream": "^0.4.4", "evenement/evenement": "^2.0 || ^1.0" }, diff --git a/examples/01-hello-world.php b/examples/01-hello-world.php index 70107616..424a9c1e 100644 --- a/examples/01-hello-world.php +++ b/examples/01-hello-world.php @@ -8,7 +8,7 @@ require __DIR__ . '/../vendor/autoload.php'; $loop = Factory::create(); -$socket = new Server($loop); +$socket = new Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); $server = new \React\Http\Server($socket); $server->on('request', function (Request $request, Response $response) { @@ -16,8 +16,6 @@ $response->end("Hello world!\n"); }); -$socket->listen(isset($argv[1]) ? $argv[1] : 0, '0.0.0.0'); - -echo 'Listening on ' . $socket->getPort() . PHP_EOL; +echo 'Listening on http://' . $socket->getAddress() . PHP_EOL; $loop->run(); diff --git a/src/Server.php b/src/Server.php index ba9daed5..000ade81 100644 --- a/src/Server.php +++ b/src/Server.php @@ -24,7 +24,10 @@ public function __construct(SocketServerInterface $io) $parser = new RequestHeaderParser(); $parser->on('headers', function (Request $request, $bodyBuffer) use ($conn, $parser, $that) { // attach remote ip to the request as metadata - $request->remoteAddress = $conn->getRemoteAddress(); + $request->remoteAddress = trim( + parse_url('tcp://' . $conn->getRemoteAddress(), PHP_URL_HOST), + '[]' + ); // forward pause/resume calls to underlying connection $request->on('pause', array($conn, 'pause')); diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 6974e2c9..0b934da3 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -25,15 +25,13 @@ public function setUp() 'isReadable', 'isWritable', 'getRemoteAddress', + 'getLocalAddress', 'pipe' ) ) ->getMock(); - $this->socket = $this->getMockBuilder('React\Socket\Server') - ->disableOriginalConstructor() - ->setMethods(null) - ->getMock(); + $this->socket = new SocketServerStub(); } public function testRequestEventWillNotBeEmittedForIncompleteHeaders() diff --git a/tests/SocketServerStub.php b/tests/SocketServerStub.php new file mode 100644 index 00000000..bdbb7ac2 --- /dev/null +++ b/tests/SocketServerStub.php @@ -0,0 +1,19 @@ + Date: Tue, 14 Feb 2017 22:22:49 +0100 Subject: [PATCH 057/456] Change Request methods to be in line with PSR-7 * Rename getQuery() to getQueryParams() * Rename getHttpVersion() to getProtocolVersion() * Change `getHeaders()` to always return an array of string values for each header --- README.md | 29 ++++++++++++++++++++------- src/Request.php | 33 ++++++++++++++++++++++--------- src/RequestHeaderParser.php | 10 +--------- tests/RequestHeaderParserTest.php | 22 ++++++++++----------- tests/RequestTest.php | 10 +++++----- 5 files changed, 63 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 19dab73e..f6f7950a 100644 --- a/README.md +++ b/README.md @@ -51,19 +51,34 @@ It implements the `ReadableStreamInterface`. See the above usage example and the class outline for details. +#### getMethod() + +The `getMethod(): string` method can be used to +return the request method. + +#### getPath() + +The `getPath(): string` method can be used to +return the request path. + +#### getQueryParams() + +The `getQueryParams(): array` method can be used to +return an array with all query parameters ($_GET). + +#### getProtocolVersion() + +The `getProtocolVersion(): string` method can be used to +return the HTTP protocol version (such as "1.0" or "1.1"). + #### getHeaders() The `getHeaders(): array` method can be used to return an array with ALL headers. The keys represent the header name in the exact case in which they were -originally specified. The values will be a string if there's only a single -value for the respective header name or an array of strings if this header -has multiple values. - -> Note that this differs from the PSR-7 implementation of this method, -which always returns an array for each header name, even if it only has a -single value. +originally specified. The values will be an array of strings for each +value for the respective header name. #### getHeader() diff --git a/src/Request.php b/src/Request.php index c9afe22a..55d2d1ea 100644 --- a/src/Request.php +++ b/src/Request.php @@ -28,22 +28,42 @@ public function __construct($method, $path, $query = array(), $httpVersion = '1. $this->headers = $headers; } + /** + * Returns the request method + * + * @return string + */ public function getMethod() { return $this->method; } + /** + * Returns the request path + * + * @return string + */ public function getPath() { return $this->path; } - public function getQuery() + /** + * Returns an array with all query parameters ($_GET) + * + * @return array + */ + public function getQueryParams() { return $this->query; } - public function getHttpVersion() + /** + * Returns the HTTP protocol version (such as "1.0" or "1.1") + * + * @return string + */ + public function getProtocolVersion() { return $this->httpVersion; } @@ -52,13 +72,8 @@ public function getHttpVersion() * Returns an array with ALL headers * * The keys represent the header name in the exact case in which they were - * originally specified. The values will be a string if there's only a single - * value for the respective header name or an array of strings if this header - * has multiple values. - * - * Note that this differs from the PSR-7 implementation of this method, - * which always returns an array for each header name, even if it only has a - * single value. + * originally specified. The values will be an array of strings for each + * value for the respective header name. * * @return array */ diff --git a/src/RequestHeaderParser.php b/src/RequestHeaderParser.php index 7c44ab02..63816bf4 100644 --- a/src/RequestHeaderParser.php +++ b/src/RequestHeaderParser.php @@ -61,20 +61,12 @@ public function parseRequest($data) parse_str($queryString, $parsedQuery); } - $headers = array_map(function($val) { - if (1 === count($val)) { - $val = $val[0]; - } - - return $val; - }, $psrRequest->getHeaders()); - $request = new Request( $psrRequest->getMethod(), $psrRequest->getUri()->getPath(), $parsedQuery, $psrRequest->getProtocolVersion(), - $headers + $psrRequest->getHeaders() ); return array($request, $bodyBuffer); diff --git a/tests/RequestHeaderParserTest.php b/tests/RequestHeaderParserTest.php index 2c22c4a5..1193e220 100644 --- a/tests/RequestHeaderParserTest.php +++ b/tests/RequestHeaderParserTest.php @@ -48,9 +48,9 @@ public function testHeadersEventShouldReturnRequestAndBodyBuffer() $this->assertInstanceOf('React\Http\Request', $request); $this->assertSame('GET', $request->getMethod()); $this->assertSame('/', $request->getPath()); - $this->assertSame(array(), $request->getQuery()); - $this->assertSame('1.1', $request->getHttpVersion()); - $this->assertSame(array('Host' => 'example.com:80', 'Connection' => 'close'), $request->getHeaders()); + $this->assertSame(array(), $request->getQueryParams()); + $this->assertSame('1.1', $request->getProtocolVersion()); + $this->assertSame(array('Host' => array('example.com:80'), 'Connection' => array('close')), $request->getHeaders()); $this->assertSame('RANDOM DATA', $bodyBuffer); } @@ -86,12 +86,12 @@ public function testHeadersEventShouldParsePathAndQueryString() $this->assertInstanceOf('React\Http\Request', $request); $this->assertSame('POST', $request->getMethod()); $this->assertSame('/foo', $request->getPath()); - $this->assertSame(array('bar' => 'baz'), $request->getQuery()); - $this->assertSame('1.1', $request->getHttpVersion()); + $this->assertSame(array('bar' => 'baz'), $request->getQueryParams()); + $this->assertSame('1.1', $request->getProtocolVersion()); $headers = array( - 'Host' => 'example.com:80', - 'User-Agent' => 'react/alpha', - 'Connection' => 'close', + 'Host' => array('example.com:80'), + 'User-Agent' => array('react/alpha'), + 'Connection' => array('close'), ); $this->assertSame($headers, $request->getHeaders()); } @@ -139,9 +139,9 @@ public function testHeaderOverflowShouldNotEmitErrorWhenDataExceedsMaxHeaderSize $parser->feed($data); $headers = array( - 'Host' => 'example.com:80', - 'User-Agent' => 'react/alpha', - 'Connection' => 'close', + 'Host' => array('example.com:80'), + 'User-Agent' => array('react/alpha'), + 'Connection' => array('close'), ); $this->assertSame($headers, $request->getHeaders()); diff --git a/tests/RequestTest.php b/tests/RequestTest.php index e5696749..7e630a3e 100644 --- a/tests/RequestTest.php +++ b/tests/RequestTest.php @@ -18,7 +18,7 @@ public function expectsContinueShouldBeFalseByDefault() /** @test */ public function expectsContinueShouldBeTrueIfContinueExpected() { - $headers = array('Expect' => '100-continue'); + $headers = array('Expect' => array('100-continue')); $request = new Request('GET', '/', array(), '1.1', $headers); $this->assertTrue($request->expectsContinue()); @@ -27,7 +27,7 @@ public function expectsContinueShouldBeTrueIfContinueExpected() /** @test */ public function expectsContinueShouldBeTrueIfContinueExpectedCaseInsensitive() { - $headers = array('EXPECT' => '100-CONTINUE'); + $headers = array('EXPECT' => array('100-CONTINUE')); $request = new Request('GET', '/', array(), '1.1', $headers); $this->assertTrue($request->expectsContinue()); @@ -36,7 +36,7 @@ public function expectsContinueShouldBeTrueIfContinueExpectedCaseInsensitive() /** @test */ public function expectsContinueShouldBeFalseForHttp10() { - $headers = array('Expect' => '100-continue'); + $headers = array('Expect' => array('100-continue')); $request = new Request('GET', '/', array(), '1.0', $headers); $this->assertFalse($request->expectsContinue()); @@ -55,10 +55,10 @@ public function testEmptyHeader() public function testHeaderIsCaseInsensitive() { $request = new Request('GET', '/', array(), '1.1', array( - 'TEST' => 'Yes', + 'TEST' => array('Yes'), )); - $this->assertEquals(array('TEST' => 'Yes'), $request->getHeaders()); + $this->assertEquals(array('TEST' => array('Yes')), $request->getHeaders()); $this->assertTrue($request->hasHeader('Test')); $this->assertEquals(array('Yes'), $request->getHeader('Test')); $this->assertEquals('Yes', $request->getHeaderLine('Test')); From 5f86430e69809c803dcca188fc2d03556497f4a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Wed, 15 Feb 2017 09:08:58 +0100 Subject: [PATCH 058/456] Mark internal APIs as internal or private --- README.md | 33 +++++++++++++++++++++++++++-- src/Request.php | 23 ++++++++++++++++++++ src/RequestHeaderParser.php | 6 ++++-- src/Response.php | 22 +++++++++++++++++++ src/ResponseCodes.php | 2 ++ src/Server.php | 42 ++++++++++++++++++++++++++++++++++++- 6 files changed, 123 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index f8917f27..f5e746d7 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ $loop = React\EventLoop\Factory::create(); $socket = new React\Socket\Server(8080, $loop); $http = new React\Http\Server($socket); -$http->on('request', function ($request, $response) { +$http->on('request', function (Request $request, Response $response) { $response->writeHead(200, array('Content-Type' => 'text/plain')); $response->end("Hello World!\n"); }); @@ -39,7 +39,30 @@ See also the [examples](examples). ### Server -See the above usage example and the class outline for details. +The `Server` class is responsible for handling incoming connections and then +emit a `request` event for each incoming HTTP request. + +It attaches itself to an instance of `React\Socket\ServerInterface` which +emits underlying streaming connections in order to then parse incoming data +as HTTP: + +```php +$socket = new React\Socket\Server(8080, $loop); + +$http = new React\Http\Server($socket); +``` + +For each incoming connection, it emits a `request` event with the respective +[`Request`](#request) and [`Response`](#response) objects: + +```php +$http->on('request', function (Request $request, Response $response) { + $response->writeHead(200, array('Content-Type' => 'text/plain')); + $response->end("Hello World!\n"); +}); +``` + +See also [`Request`](#request) and [`Response`](#response) for more details. ### Request @@ -48,6 +71,9 @@ and contains meta data which was parsed from the request headers. It implements the `ReadableStreamInterface`. +The constructor is internal, you SHOULD NOT call this yourself. +The `Server` is responsible for emitting `Request` and `Response` objects. + See the above usage example and the class outline for details. #### getHeaders() @@ -101,6 +127,9 @@ The `Response` class is responsible for streaming the outgoing response body. It implements the `WritableStreamInterface`. +The constructor is internal, you SHOULD NOT call this yourself. +The `Server` is responsible for emitting `Request` and `Response` objects. + See the above usage example and the class outline for details. #### writeContinue() diff --git a/src/Request.php b/src/Request.php index c9afe22a..79e2c96d 100644 --- a/src/Request.php +++ b/src/Request.php @@ -7,6 +7,20 @@ use React\Stream\WritableStreamInterface; use React\Stream\Util; +/** + * The `Request` class is responsible for streaming the incoming request body + * and contains meta data which was parsed from the request headers. + * + * It implements the `ReadableStreamInterface`. + * + * The constructor is internal, you SHOULD NOT call this yourself. + * The `Server` is responsible for emitting `Request` and `Response` objects. + * + * See the usage examples and the class outline for details. + * + * @see ReadableStreamInterface + * @see Server + */ class Request extends EventEmitter implements ReadableStreamInterface { private $readable = true; @@ -19,6 +33,15 @@ class Request extends EventEmitter implements ReadableStreamInterface // metadata, implicitly added externally public $remoteAddress; + /** + * The constructor is internal, you SHOULD NOT call this yourself. + * + * The `Server` is responsible for emitting `Request` and `Response` objects. + * + * Constructor parameters may change at any time. + * + * @internal + */ public function __construct($method, $path, $query = array(), $httpVersion = '1.1', $headers = array()) { $this->method = $method; diff --git a/src/RequestHeaderParser.php b/src/RequestHeaderParser.php index 7c44ab02..c6c990a6 100644 --- a/src/RequestHeaderParser.php +++ b/src/RequestHeaderParser.php @@ -9,6 +9,8 @@ /** * @event headers * @event error + * + * @internal */ class RequestHeaderParser extends EventEmitter { @@ -43,13 +45,13 @@ public function feed($data) } } - protected function parseAndEmitRequest() + private function parseAndEmitRequest() { list($request, $bodyBuffer) = $this->parseRequest($this->buffer); $this->emit('headers', array($request, $bodyBuffer)); } - public function parseRequest($data) + private function parseRequest($data) { list($headers, $bodyBuffer) = explode("\r\n\r\n", $data, 2); diff --git a/src/Response.php b/src/Response.php index 8b4af6fa..91ae4358 100644 --- a/src/Response.php +++ b/src/Response.php @@ -6,6 +6,19 @@ use React\Socket\ConnectionInterface; use React\Stream\WritableStreamInterface; +/** + * The `Response` class is responsible for streaming the outgoing response body. + * + * It implements the `WritableStreamInterface`. + * + * The constructor is internal, you SHOULD NOT call this yourself. + * The `Server` is responsible for emitting `Request` and `Response` objects. + * + * See the usage examples and the class outline for details. + * + * @see WritableStreamInterface + * @see Server + */ class Response extends EventEmitter implements WritableStreamInterface { private $closed = false; @@ -14,6 +27,15 @@ class Response extends EventEmitter implements WritableStreamInterface private $headWritten = false; private $chunkedEncoding = true; + /** + * The constructor is internal, you SHOULD NOT call this yourself. + * + * The `Server` is responsible for emitting `Request` and `Response` objects. + * + * Constructor parameters may change at any time. + * + * @internal + */ public function __construct(ConnectionInterface $conn) { $this->conn = $conn; diff --git a/src/ResponseCodes.php b/src/ResponseCodes.php index ae241ded..27b29435 100644 --- a/src/ResponseCodes.php +++ b/src/ResponseCodes.php @@ -4,6 +4,8 @@ /** * This is copy-pasted from Symfony2's Response class + * + * @internal */ class ResponseCodes { diff --git a/src/Server.php b/src/Server.php index 06e65347..933bc109 100644 --- a/src/Server.php +++ b/src/Server.php @@ -6,11 +6,50 @@ use React\Socket\ServerInterface as SocketServerInterface; use React\Socket\ConnectionInterface; -/** @event request */ +/** + * The `Server` class is responsible for handling incoming connections and then + * emit a `request` event for each incoming HTTP request. + * + * ```php + * $socket = new React\Socket\Server(8080, $loop); + * + * $http = new React\Http\Server($socket); + * ``` + * + * For each incoming connection, it emits a `request` event with the respective + * [`Request`](#request) and [`Response`](#response) objects: + * + * ```php + * $http->on('request', function (Request $request, Response $response) { + * $response->writeHead(200, array('Content-Type' => 'text/plain')); + * $response->end("Hello World!\n"); + * }); + * ``` + * + * See also [`Request`](#request) and [`Response`](#response) for more details. + * + * @see Request + * @see Response + */ class Server extends EventEmitter { private $io; + /** + * Creates a HTTP server that accepts connections from the given socket. + * + * It attaches itself to an instance of `React\Socket\ServerInterface` which + * emits underlying streaming connections in order to then parse incoming data + * as HTTP: + * + * ```php + * $socket = new React\Socket\Server(8080, $loop); + * + * $http = new React\Http\Server($socket); + * ``` + * + * @param \React\Socket\ServerInterface $io + */ public function __construct(SocketServerInterface $io) { $this->io = $io; @@ -54,6 +93,7 @@ public function __construct(SocketServerInterface $io) }); } + /** @internal */ public function handleRequest(ConnectionInterface $conn, Request $request, $bodyBuffer) { $response = new Response($conn); From e13b7089697d05cdcd1ae53be5510900d8ae7934 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 5 Feb 2017 15:48:19 +0100 Subject: [PATCH 059/456] Secure HTTPS server example --- README.md | 13 ++++++++ examples/02-hello-world-https.php | 27 +++++++++++++++++ examples/localhost.pem | 49 +++++++++++++++++++++++++++++++ src/Server.php | 13 ++++++++ 4 files changed, 102 insertions(+) create mode 100644 examples/02-hello-world-https.php create mode 100644 examples/localhost.pem diff --git a/README.md b/README.md index e2e60d81..96d14f09 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,19 @@ $socket = new React\Socket\Server(8080, $loop); $http = new React\Http\Server($socket); ``` +Similarly, you can also attach this to a +[`React\Socket\SecureServer`](https://github.com/reactphp/socket#secureserver) +in order to start a secure HTTPS server like this: + +```php +$socket = new Server(8080, $loop); +$socket = new SecureServer($socket, $loop, array( + 'local_cert' => __DIR__ . '/localhost.pem' +)); + +$http = new React\Http\Server($socket); +``` + For each incoming connection, it emits a `request` event with the respective [`Request`](#request) and [`Response`](#response) objects: diff --git a/examples/02-hello-world-https.php b/examples/02-hello-world-https.php new file mode 100644 index 00000000..c017a196 --- /dev/null +++ b/examples/02-hello-world-https.php @@ -0,0 +1,27 @@ + isset($argv[2]) ? $argv[2] : __DIR__ . '/localhost.pem' +)); + +$server = new \React\Http\Server($socket); +$server->on('request', function (Request $reques, Response $response) { + $response->writeHead(200, array('Content-Type' => 'text/plain')); + $response->end("Hello world!\n"); +}); + +//$socket->on('error', 'printf'); + +echo 'Listening on https://' . $socket->getAddress() . PHP_EOL; + +$loop->run(); diff --git a/examples/localhost.pem b/examples/localhost.pem new file mode 100644 index 00000000..be692792 --- /dev/null +++ b/examples/localhost.pem @@ -0,0 +1,49 @@ +-----BEGIN CERTIFICATE----- +MIIDfTCCAmWgAwIBAgIBADANBgkqhkiG9w0BAQUFADBZMRIwEAYDVQQDDAkxMjcu +MC4wLjExCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQK +DBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwHhcNMTYxMjMwMTQ1OTA2WhcNMjYx +MjI4MTQ1OTA2WjBZMRIwEAYDVQQDDAkxMjcuMC4wLjExCzAJBgNVBAYTAkFVMRMw +EQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0 +eSBMdGQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC8SZWNS+Ktg0Py +W8dx5uXZ+ZUawd3wnzLMHW7EhoUpIrIdp3kDU9NezF68dOhPMJY/Kh+6btRCxWXN +2OVTqS5Xi826j3TSE07iF83JRLeveW0PcodjUBd+RzdwCWWo2pfMJz4v7x1wu1c9 +zNi6JxxpDAXTFSB4GiWsI4tFu2XmMRhfm6LRK4WPfsZIJKokdiG5fKSPDn7nrVj0 +UUXr2eBsEAzdwL14U9+mwbLdaAkz3qK3fqi8sEC09lEWm95gKMOhkQf5qvXODtT4 +wdVrrKDTyehLv0xaItnUDnXzrkMBU5QS9TQzzqSW6ZaBsSxtONEFUiXiN9dtyXsY +YCUE54G/AgMBAAGjUDBOMB0GA1UdDgQWBBQ2GRz3QsQzdXaTMnPVCKfpigA10DAf +BgNVHSMEGDAWgBQ2GRz3QsQzdXaTMnPVCKfpigA10DAMBgNVHRMEBTADAQH/MA0G +CSqGSIb3DQEBBQUAA4IBAQA77iZ4KrpPY18Ezjt0mngYAuAxunKddXYdLZ2khywN +0uI/VzYnkFVtrsC7y2jLHSxlmE2/viPPGZDUplENV2acN6JNW+tlt7/bsrQHDQw3 +7VCF27EWiDxHsaghhLkqC+kcop5YR5c0oDQTdEWEKSbow2zayUXDYbRRs76SClTe +824Yul+Ts8Mka+AX2PXDg47iZ84fJRN/nKavcJUTJ2iS1uYw0GNnFMge/uwsfMR3 +V47qN0X5emky8fcq99FlMCbcy0gHAeSWAjClgr2dd2i0LDatUbj7YmdmFcskOgII +IwGfvuWR2yPevYGAE0QgFeLHniN3RW8zmpnX/XtrJ4a7 +-----END CERTIFICATE----- +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC8SZWNS+Ktg0Py +W8dx5uXZ+ZUawd3wnzLMHW7EhoUpIrIdp3kDU9NezF68dOhPMJY/Kh+6btRCxWXN +2OVTqS5Xi826j3TSE07iF83JRLeveW0PcodjUBd+RzdwCWWo2pfMJz4v7x1wu1c9 +zNi6JxxpDAXTFSB4GiWsI4tFu2XmMRhfm6LRK4WPfsZIJKokdiG5fKSPDn7nrVj0 +UUXr2eBsEAzdwL14U9+mwbLdaAkz3qK3fqi8sEC09lEWm95gKMOhkQf5qvXODtT4 +wdVrrKDTyehLv0xaItnUDnXzrkMBU5QS9TQzzqSW6ZaBsSxtONEFUiXiN9dtyXsY +YCUE54G/AgMBAAECggEBAKiO/3FE1CMddkCLZVtUp8ShqJgRokx9WI5ecwFApAkV +ZHsjqDQQYRNmxhDUX/w0tOzLGyhde2xjJyZG29YviKsbHwu6zYwbeOzy/mkGOaK/ +g6DmmMmRs9Z6juifoQCu4GIFZ6il2adIL2vF7OeJh+eKudQj/7NFRSB7mXzNrQWK +tZY3eux5zXWmio7pgZrx1HFZQiiL9NVLwT9J7oBnaoO3fREiu5J2xBpljG9Cr0j1 +LLiVLhukWJYRlHDtGt1CzI9w8iKo44PCRzpKyxpbsOrQxeSyEWUYQRv9VHA59LC7 +tVAJTbnTX1BNHkGZkOkoOpoZLwBaM2XbbDtcOGCAZMECgYEA+mTURFQ85/pxawvk +9ndqZ+5He1u/bMLYIJDp0hdB/vgD+vw3gb2UyRwp0I6Wc6Si4FEEnbY7L0pzWsiR +43CpLs+cyLfnD9NycuIasxs5fKb/1s1nGTkRAp7x9x/ZTtEf8v4YTmmMXFHzdo7V +pv+czO89ppEDkxEtMf/b5SifhO8CgYEAwIDIUvXLduGhL+RPDwjc2SKdydXGV6om +OEdt/V8oS801Z7k8l3gHXFm7zL/MpHmh9cag+F9dHK42kw2RSjDGsBlXXiAO1Z0I +2A34OdPw/kow8fmIKWTMu3+28Kca+3RmUqeyaq0vazQ/bWMO9px+Ud3YfLo1Tn5I +li0MecAx8DECgYEAvsLceKYYtL83c09fg2oc1ctSCCgw4WJcGAtvJ9DyRZacKbXH +b/+H/+OF8879zmKqd+0hcCnqUzAMTCisBLPLIM+o6b45ufPkqKObpcJi/JWaKgLY +vf2c+Psw6o4IF6T5Cz4MNIjzF06UBknxecYZpoPJ20F1kLCwVvxPgfl99l8CgYAb +XfOcv67WTstgiJ+oroTfJamy+P5ClkDqvVTosW+EHz9ZaJ8xlXHOcj9do2LPey9I +Rp250azmF+pQS5x9JKQKgv/FtN8HBVUtigbhCb14GUoODICMCfWFLmnumoMefnTR +iV+3BLn6Dqp5vZxx+NuIffZ5/Or5JsDhALSGVomC8QKBgAi3Z/dNQrDHfkXMNn/L ++EAoLuAbFgLs76r9VGgNaRQ/q5gex2bZEGoBj4Sxvs95NUIcfD9wKT7FF8HdxARv +y3o6Bfc8Xp9So9SlFXrje+gkdEJ0rQR67d+XBuJZh86bXJHVrMwpoNL+ahLGdVSe +81oh1uCH1YPLM29hPyaohxL8 +-----END PRIVATE KEY----- diff --git a/src/Server.php b/src/Server.php index 933bc109..4c7eaf9f 100644 --- a/src/Server.php +++ b/src/Server.php @@ -48,6 +48,19 @@ class Server extends EventEmitter * $http = new React\Http\Server($socket); * ``` * + * Similarly, you can also attach this to a + * [`React\Socket\SecureServer`](https://github.com/reactphp/socket#secureserver) + * in order to start a secure HTTPS server like this: + * + * ```php + * $socket = new Server(8080, $loop); + * $socket = new SecureServer($socket, $loop, array( + * 'local_cert' => __DIR__ . '/localhost.pem' + * )); + * + * $http = new React\Http\Server($socket); + * ``` + * * @param \React\Socket\ServerInterface $io */ public function __construct(SocketServerInterface $io) From 3f4821c3021ac7e5a404edb5d371c3d2fdeb38b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 16 Feb 2017 16:01:02 +0100 Subject: [PATCH 060/456] Prepare v0.5.0 release --- CHANGELOG.md | 35 +++++++++++++++++++++++++++++++++++ README.md | 39 +++++++++++++++++++++++++-------------- composer.json | 4 ++-- 3 files changed, 62 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 851b1d65..2c6a1735 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,40 @@ # Changelog +## 0.5.0 (2017-02-16) + +* Feature / BC break: Change `Request` methods to be in line with PSR-7 + (#117 by @clue) + * Rename `getQuery()` to `getQueryParams()` + * Rename `getHttpVersion()` to `getProtocolVersion()` + * Change `getHeaders()` to always return an array of string values + for each header + +* Feature / BC break: Update Socket component to v0.5 and + add secure HTTPS server support + (#90 and #119 by @clue) + + ```php + // old plaintext HTTP server + $socket = new React\Socket\Server($loop); + $socket->listen(8080, '127.0.0.1'); + $http = new React\Http\Server($socket); + + // new plaintext HTTP server + $socket = new React\Socket\Server('127.0.0.1:8080', $loop); + $http = new React\Http\Server($socket); + + // new secure HTTPS server + $socket = new React\Socket\Server('127.0.0.1:8080', $loop); + $socket = new React\Socket\SecureServer($socket, $loop, array( + 'local_cert' => __DIR__ . '/localhost.pem' + )); + $http = new React\Http\Server($socket); + ``` + +* BC break: Mark internal APIs as internal or private and + remove unneeded `ServerInterface` + (#118 by @clue, #95 by @legionth) + ## 0.4.4 (2017-02-13) * Feature: Add request header accessors (à la PSR-7) diff --git a/README.md b/README.md index 96d14f09..cffa7da5 100644 --- a/README.md +++ b/README.md @@ -2,19 +2,30 @@ [![Build Status](https://secure.travis-ci.org/reactphp/http.png?branch=master)](http://travis-ci.org/reactphp/http) [![Code Climate](https://codeclimate.com/github/reactphp/http/badges/gpa.svg)](https://codeclimate.com/github/reactphp/http) -Library for building an evented http server. - -This component builds on top of the `Socket` component to implement HTTP. Here -are the main concepts: - -* **Server**: Attaches itself to an instance of - `React\Socket\ServerInterface`, parses any incoming data as HTTP, emits a - `request` event for each request. -* **Request**: A `ReadableStream` which streams the request body and contains - meta data which was parsed from the request header. -* **Response** A `WritableStream` which streams the response body. You can set - the status code and response headers via the `writeHead()` method. - +Event-driven, streaming plaintext HTTP and secure HTTPS server for [ReactPHP](https://reactphp.org/) + +**Table of Contents** + +* [Quickstart example](#quickstart-example) +* [Usage](#usage) + * [Server](#server) + * [Request](#request) + * [getMethod()](#getmethod) + * [getQueryParams()](#getqueryparams] + * [getProtocolVersion()](#getprotocolversion) + * [getHeaders()](#getheaders) + * [getHeader()](#getheader) + * [getHeaderLine()](#getheaderline) + * [hasHeader()](#hasheader) + * [expectsContinue()](#expectscontinue) + * [Response](#response) + * [writeContinue()](#writecontinue) + * [writeHead()](#writehead) +* [Install](#install) +* [Tests](#tests) +* [License](#license) + +> Note: This project is in beta stage! Feel free to report any issues you encounter. ## Quickstart example @@ -255,7 +266,7 @@ The recommended way to install this library is [through Composer](http://getcomp This will install the latest supported version: ```bash -$ composer require react/http:^0.4.4 +$ composer require react/http:^0.5 ``` More details about version upgrades can be found in the [CHANGELOG](CHANGELOG.md). diff --git a/composer.json b/composer.json index 40c0582b..a7ca133b 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "react/http", - "description": "Library for building an evented http server.", - "keywords": ["http"], + "description": "Event-driven, streaming plaintext HTTP and secure HTTPS server for ReactPHP", + "keywords": ["event-driven", "streaming", "HTTP", "HTTPS", "server", "ReactPHP"], "license": "MIT", "require": { "php": ">=5.3.0", From 1373acbc69ab294398bf42bb49d299d4ce4857ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 16 Feb 2017 16:02:28 +0100 Subject: [PATCH 061/456] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cffa7da5..aefcd2b9 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Event-driven, streaming plaintext HTTP and secure HTTPS server for [ReactPHP](ht * [Server](#server) * [Request](#request) * [getMethod()](#getmethod) - * [getQueryParams()](#getqueryparams] + * [getQueryParams()](#getqueryparams) * [getProtocolVersion()](#getprotocolversion) * [getHeaders()](#getheaders) * [getHeader()](#getheader) From 950bcf9cf09cb2c6b4b61a50ccfae194eb3261b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 16 Feb 2017 23:52:57 +0100 Subject: [PATCH 062/456] Register all event listeners before notifying other listeners --- src/Server.php | 81 +++++++++++++++++++++++++------------------------- 1 file changed, 40 insertions(+), 41 deletions(-) diff --git a/src/Server.php b/src/Server.php index 4c7eaf9f..6762d6b9 100644 --- a/src/Server.php +++ b/src/Server.php @@ -33,8 +33,6 @@ */ class Server extends EventEmitter { - private $io; - /** * Creates a HTTP server that accepts connections from the given socket. * @@ -65,49 +63,36 @@ class Server extends EventEmitter */ public function __construct(SocketServerInterface $io) { - $this->io = $io; - $that = $this; - - $this->io->on('connection', function (ConnectionInterface $conn) use ($that) { - // TODO: http 1.1 keep-alive - // TODO: chunked transfer encoding (also for outgoing data) - // TODO: multipart parsing - - $parser = new RequestHeaderParser(); - $parser->on('headers', function (Request $request, $bodyBuffer) use ($conn, $parser, $that) { - // attach remote ip to the request as metadata - $request->remoteAddress = trim( - parse_url('tcp://' . $conn->getRemoteAddress(), PHP_URL_HOST), - '[]' - ); + $io->on('connection', array($this, 'handleConnection')); + } - // forward pause/resume calls to underlying connection - $request->on('pause', array($conn, 'pause')); - $request->on('resume', array($conn, 'resume')); + /** @internal */ + public function handleConnection(ConnectionInterface $conn) + { + $that = $this; + $parser = new RequestHeaderParser(); + $listener = array($parser, 'feed'); + $parser->on('headers', function (Request $request, $bodyBuffer) use ($conn, $listener, $parser, $that) { + // parsing request completed => stop feeding parser + $conn->removeListener('data', $listener); - $that->handleRequest($conn, $request, $bodyBuffer); + $that->handleRequest($conn, $request); - $conn->removeListener('data', array($parser, 'feed')); - $conn->on('end', function () use ($request) { - $request->emit('end'); - }); - $conn->on('data', function ($data) use ($request) { - $request->emit('data', array($data)); - }); - }); + if ($bodyBuffer !== '') { + $request->emit('data', array($bodyBuffer)); + } + }); - $listener = array($parser, 'feed'); - $conn->on('data', $listener); - $parser->on('error', function() use ($conn, $listener, $that) { - // TODO: return 400 response - $conn->removeListener('data', $listener); - $that->emit('error', func_get_args()); - }); + $conn->on('data', $listener); + $parser->on('error', function() use ($conn, $listener, $that) { + // TODO: return 400 response + $conn->removeListener('data', $listener); + $that->emit('error', func_get_args()); }); } /** @internal */ - public function handleRequest(ConnectionInterface $conn, Request $request, $bodyBuffer) + public function handleRequest(ConnectionInterface $conn, Request $request) { $response = new Response($conn); $response->on('close', array($request, 'close')); @@ -118,10 +103,24 @@ public function handleRequest(ConnectionInterface $conn, Request $request, $body return; } - $this->emit('request', array($request, $response)); + // attach remote ip to the request as metadata + $request->remoteAddress = trim( + parse_url('tcp://' . $conn->getRemoteAddress(), PHP_URL_HOST), + '[]' + ); - if ($bodyBuffer !== '') { - $request->emit('data', array($bodyBuffer)); - } + // forward pause/resume calls to underlying connection + $request->on('pause', array($conn, 'pause')); + $request->on('resume', array($conn, 'resume')); + + // forward connection events to request + $conn->on('end', function () use ($request) { + $request->emit('end'); + }); + $conn->on('data', function ($data) use ($request) { + $request->emit('data', array($data)); + }); + + $this->emit('request', array($request, $response)); } } From 5893c2efa6b2e8e41d30dcec54f2666ffbacfece Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 17 Feb 2017 00:51:18 +0100 Subject: [PATCH 063/456] Send HTTP status code 400 for invalid requests --- src/Server.php | 26 +++++++++++++++++++++++--- tests/ServerTest.php | 34 ++++++++++++++++++++++++++++++++-- 2 files changed, 55 insertions(+), 5 deletions(-) diff --git a/src/Server.php b/src/Server.php index 6762d6b9..3a68a914 100644 --- a/src/Server.php +++ b/src/Server.php @@ -84,10 +84,14 @@ public function handleConnection(ConnectionInterface $conn) }); $conn->on('data', $listener); - $parser->on('error', function() use ($conn, $listener, $that) { - // TODO: return 400 response + $parser->on('error', function(\Exception $e) use ($conn, $listener, $that) { $conn->removeListener('data', $listener); - $that->emit('error', func_get_args()); + $that->emit('error', array($e)); + + $that->writeError( + $conn, + 400 + ); }); } @@ -123,4 +127,20 @@ public function handleRequest(ConnectionInterface $conn, Request $request) $this->emit('request', array($request, $response)); } + + /** @internal */ + public function writeError(ConnectionInterface $conn, $code) + { + $message = 'Error ' . $code; + if (isset(ResponseCodes::$statusTexts[$code])) { + $message .= ': ' . ResponseCodes::$statusTexts[$code]; + } + + $response = new Response($conn); + $response->writeHead($code, array( + 'Content-Length' => strlen($message), + 'Content-Type' => 'text/plain' + )); + $response->end($message); + } } diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 0b934da3..305bb31a 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -206,7 +206,6 @@ public function testParserErrorEmitted() { $error = null; $server = new Server($this->socket); - $server->on('headers', $this->expectCallableNever()); $server->on('error', function ($message) use (&$error) { $error = $message; }); @@ -218,7 +217,38 @@ public function testParserErrorEmitted() $this->connection->emit('data', array($data)); $this->assertInstanceOf('OverflowException', $error); - $this->connection->expects($this->never())->method('write'); + } + + public function testRequestInvalidWillEmitErrorAndSendErrorResponse() + { + $error = null; + $server = new Server($this->socket); + $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; + } + ) + ); + + $this->socket->emit('connection', array($this->connection)); + + $data = "bad request\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); } private function createGetRequest() From 9125a4dfc7b8579051bcc2acb90c928c96944c4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 17 Feb 2017 13:04:52 +0100 Subject: [PATCH 064/456] Closing request will now stop reading from connection --- src/Server.php | 6 ++++++ tests/ServerTest.php | 14 ++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/src/Server.php b/src/Server.php index 6762d6b9..0a623c95 100644 --- a/src/Server.php +++ b/src/Server.php @@ -113,6 +113,12 @@ public function handleRequest(ConnectionInterface $conn, Request $request) $request->on('pause', array($conn, 'pause')); $request->on('resume', array($conn, 'resume')); + // closing the request currently emits an "end" event + // stop reading from the connection by pausing it + $request->on('end', function () use ($conn) { + $conn->pause(); + }); + // forward connection events to request $conn->on('end', function () use ($request) { $request->emit('end'); diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 0b934da3..90a7517d 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -117,6 +117,20 @@ public function testRequestResumeWillbeForwardedToConnection() $this->connection->emit('data', array($data)); } + public function testRequestCloseWillPauseConnection() + { + $server = new Server($this->socket); + $server->on('request', function (Request $request) { + $request->close(); + }); + + $this->connection->expects($this->once())->method('pause'); + $this->socket->emit('connection', array($this->connection)); + + $data = $this->createGetRequest(); + $this->connection->emit('data', array($data)); + } + public function testRequestEventWithoutBodyWillNotEmitData() { $never = $this->expectCallableNever(); From 65433b391be99dfa9352792d7b8d9dd0878c3df5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 18 Feb 2017 01:22:44 +0100 Subject: [PATCH 065/456] Documentation for invalid request messages --- README.md | 9 +++++++++ src/Server.php | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/README.md b/README.md index aefcd2b9..88549055 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,15 @@ $http->on('request', function (Request $request, Response $response) { See also [`Request`](#request) and [`Response`](#response) for more details. +If a client sends an invalid request message, it will emit an `error` event, +send an HTTP error response to the client and close the connection: + +```php +$http->on('error', function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; +}); +``` + ### Request The `Request` class is responsible for streaming the incoming request body diff --git a/src/Server.php b/src/Server.php index 3a68a914..7ff523c1 100644 --- a/src/Server.php +++ b/src/Server.php @@ -28,6 +28,15 @@ * * See also [`Request`](#request) and [`Response`](#response) for more details. * + * If a client sends an invalid request message, it will emit an `error` event, + * send an HTTP error response to the client and close the connection: + * + * ```php + * $http->on('error', function (Exception $e) { + * echo 'Error: ' . $e->getMessage() . PHP_EOL; + * }); + * ``` + * * @see Request * @see Response */ From 81e997dfe72afa70614a057d865558fee1de4afc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 18 Feb 2017 00:21:25 +0100 Subject: [PATCH 066/456] Response uses same HTTP protocol version as corresponding request --- README.md | 3 +++ src/Response.php | 13 +++++++--- src/Server.php | 2 +- tests/ResponseTest.php | 21 ++++++++++++++- tests/ServerTest.php | 58 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 92 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 88549055..be1d0dce 100644 --- a/README.md +++ b/README.md @@ -178,6 +178,9 @@ It implements the `WritableStreamInterface`. The constructor is internal, you SHOULD NOT call this yourself. The `Server` is responsible for emitting `Request` and `Response` objects. +The `Response` will automatically use the same HTTP protocol version as the +corresponding `Request`. + See the above usage example and the class outline for details. #### writeContinue() diff --git a/src/Response.php b/src/Response.php index 91ae4358..541ff92d 100644 --- a/src/Response.php +++ b/src/Response.php @@ -14,6 +14,9 @@ * The constructor is internal, you SHOULD NOT call this yourself. * The `Server` is responsible for emitting `Request` and `Response` objects. * + * The `Response` will automatically use the same HTTP protocol version as the + * corresponding `Request`. + * * See the usage examples and the class outline for details. * * @see WritableStreamInterface @@ -21,9 +24,11 @@ */ class Response extends EventEmitter implements WritableStreamInterface { + private $conn; + private $protocolVersion; + private $closed = false; private $writable = true; - private $conn; private $headWritten = false; private $chunkedEncoding = true; @@ -36,9 +41,11 @@ class Response extends EventEmitter implements WritableStreamInterface * * @internal */ - public function __construct(ConnectionInterface $conn) + public function __construct(ConnectionInterface $conn, $protocolVersion = '1.1') { $this->conn = $conn; + $this->protocolVersion = $protocolVersion; + $that = $this; $this->conn->on('end', function () use ($that) { $that->close(); @@ -201,7 +208,7 @@ private function formatHead($status, array $headers) { $status = (int) $status; $text = isset(ResponseCodes::$statusTexts[$status]) ? ResponseCodes::$statusTexts[$status] : ''; - $data = "HTTP/1.1 $status $text\r\n"; + $data = "HTTP/$this->protocolVersion $status $text\r\n"; foreach ($headers as $name => $value) { $name = str_replace(array("\r", "\n"), '', $name); diff --git a/src/Server.php b/src/Server.php index 7ff523c1..93aaefdf 100644 --- a/src/Server.php +++ b/src/Server.php @@ -107,7 +107,7 @@ public function handleConnection(ConnectionInterface $conn) /** @internal */ public function handleRequest(ConnectionInterface $conn, Request $request) { - $response = new Response($conn); + $response = new Response($conn, $request->getProtocolVersion()); $response->on('close', array($request, 'close')); if (!$this->listeners('request')) { diff --git a/tests/ResponseTest.php b/tests/ResponseTest.php index 27948504..470d8bed 100644 --- a/tests/ResponseTest.php +++ b/tests/ResponseTest.php @@ -26,6 +26,26 @@ public function testResponseShouldBeChunkedByDefault() $response->writeHead(); } + public function testResponseShouldUseGivenProtocolVersion() + { + $expected = ''; + $expected .= "HTTP/1.0 200 OK\r\n"; + $expected .= "X-Powered-By: React/alpha\r\n"; + $expected .= "Transfer-Encoding: chunked\r\n"; + $expected .= "\r\n"; + + $conn = $this + ->getMockBuilder('React\Socket\ConnectionInterface') + ->getMock(); + $conn + ->expects($this->once()) + ->method('write') + ->with($expected); + + $response = new Response($conn, '1.0'); + $response->writeHead(); + } + public function testResponseShouldBeChunkedEvenWithOtherTransferEncoding() { $expected = ''; @@ -46,7 +66,6 @@ public function testResponseShouldBeChunkedEvenWithOtherTransferEncoding() $response->writeHead(200, array('transfer-encoding' => 'custom')); } - public function testResponseShouldNotBeChunkedWithContentLength() { $expected = ''; diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 305bb31a..8b2d9c3d 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -202,6 +202,64 @@ function ($data) use (&$buffer) { $this->assertContains("\r\nX-Powered-By: React/alpha\r\n", $buffer); } + public function testResponseContainsSameRequestProtocolVersionForHttp11() + { + $server = new Server($this->socket); + $server->on('request', function (Request $request, Response $response) { + $response->writeHead(); + $response->end(); + }); + + $buffer = ''; + + $this->connection + ->expects($this->any()) + ->method('write') + ->will( + $this->returnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } + ) + ); + + $this->socket->emit('connection', array($this->connection)); + + $data = "GET / HTTP/1.1\r\n\r\n"; + $this->connection->emit('data', array($data)); + + $this->assertContains("HTTP/1.1 200 OK\r\n", $buffer); + } + + public function testResponseContainsSameRequestProtocolVersionForHttp10() + { + $server = new Server($this->socket); + $server->on('request', function (Request $request, Response $response) { + $response->writeHead(); + $response->end(); + }); + + $buffer = ''; + + $this->connection + ->expects($this->any()) + ->method('write') + ->will( + $this->returnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } + ) + ); + + $this->socket->emit('connection', array($this->connection)); + + $data = "GET / HTTP/1.0\r\n\r\n"; + $this->connection->emit('data', array($data)); + + $this->assertContains("HTTP/1.0 200 OK\r\n", $buffer); + } + public function testParserErrorEmitted() { $error = null; From d6192853c2d9987819b002f5c030f5d11e41895d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 18 Feb 2017 00:41:38 +0100 Subject: [PATCH 067/456] Apply chunked transfer encoding only for HTTP/1.1 responses by default --- README.md | 6 +++++- src/Response.php | 18 +++++++++--------- tests/ResponseTest.php | 3 +-- tests/ServerTest.php | 10 ++++++---- 4 files changed, 21 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index be1d0dce..ce27e302 100644 --- a/README.md +++ b/README.md @@ -181,6 +181,10 @@ The `Server` is responsible for emitting `Request` and `Response` objects. The `Response` will automatically use the same HTTP protocol version as the corresponding `Request`. +HTTP/1.1 responses will automatically apply chunked transfer encoding if +no `Content-Length` header has been set. +See [`writeHead()`](#writehead) for more details. + See the above usage example and the class outline for details. #### writeContinue() @@ -237,7 +241,7 @@ $response->end('Hello World!'); Calling this method more than once will result in an `Exception`. -Unless you specify a `Content-Length` header yourself, the response message +Unless you specify a `Content-Length` header yourself, HTTP/1.1 responses will automatically use chunked transfer encoding and send the respective header (`Transfer-Encoding: chunked`) automatically. If you know the length of your body, you MAY specify it like this instead: diff --git a/src/Response.php b/src/Response.php index 541ff92d..e05cec2c 100644 --- a/src/Response.php +++ b/src/Response.php @@ -17,6 +17,10 @@ * The `Response` will automatically use the same HTTP protocol version as the * corresponding `Request`. * + * HTTP/1.1 responses will automatically apply chunked transfer encoding if + * no `Content-Length` header has been set. + * See `writeHead()` for more details. + * * See the usage examples and the class outline for details. * * @see WritableStreamInterface @@ -30,7 +34,7 @@ class Response extends EventEmitter implements WritableStreamInterface private $closed = false; private $writable = true; private $headWritten = false; - private $chunkedEncoding = true; + private $chunkedEncoding = false; /** * The constructor is internal, you SHOULD NOT call this yourself. @@ -129,7 +133,7 @@ public function writeContinue() * * Calling this method more than once will result in an `Exception`. * - * Unless you specify a `Content-Length` header yourself, the response message + * Unless you specify a `Content-Length` header yourself, HTTP/1.1 responses * will automatically use chunked transfer encoding and send the respective header * (`Transfer-Encoding: chunked`) automatically. If you know the length of your * body, you MAY specify it like this instead: @@ -174,11 +178,6 @@ public function writeHead($status = 200, array $headers = array()) $lower = array_change_key_case($headers); - // disable chunked encoding if content-length is given - if (isset($lower['content-length'])) { - $this->chunkedEncoding = false; - } - // assign default "X-Powered-By" header as first for history reasons if (!isset($lower['x-powered-by'])) { $headers = array_merge( @@ -187,8 +186,8 @@ public function writeHead($status = 200, array $headers = array()) ); } - // assign chunked transfer-encoding if chunked encoding is used - if ($this->chunkedEncoding) { + // assign chunked transfer-encoding if no 'content-length' is given for HTTP/1.1 responses + if (!isset($lower['content-length']) && $this->protocolVersion === '1.1') { foreach($headers as $name => $value) { if (strtolower($name) === 'transfer-encoding') { unset($headers[$name]); @@ -196,6 +195,7 @@ public function writeHead($status = 200, array $headers = array()) } $headers['Transfer-Encoding'] = 'chunked'; + $this->chunkedEncoding = true; } $data = $this->formatHead($status, $headers); diff --git a/tests/ResponseTest.php b/tests/ResponseTest.php index 470d8bed..69a16001 100644 --- a/tests/ResponseTest.php +++ b/tests/ResponseTest.php @@ -26,12 +26,11 @@ public function testResponseShouldBeChunkedByDefault() $response->writeHead(); } - public function testResponseShouldUseGivenProtocolVersion() + public function testResponseShouldNotBeChunkedWhenProtocolVersionIsNot11() { $expected = ''; $expected .= "HTTP/1.0 200 OK\r\n"; $expected .= "X-Powered-By: React/alpha\r\n"; - $expected .= "Transfer-Encoding: chunked\r\n"; $expected .= "\r\n"; $conn = $this diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 8b2d9c3d..f72bfe97 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -202,12 +202,12 @@ function ($data) use (&$buffer) { $this->assertContains("\r\nX-Powered-By: React/alpha\r\n", $buffer); } - public function testResponseContainsSameRequestProtocolVersionForHttp11() + public function testResponseContainsSameRequestProtocolVersionAndChunkedBodyForHttp11() { $server = new Server($this->socket); $server->on('request', function (Request $request, Response $response) { $response->writeHead(); - $response->end(); + $response->end('bye'); }); $buffer = ''; @@ -229,14 +229,15 @@ function ($data) use (&$buffer) { $this->connection->emit('data', array($data)); $this->assertContains("HTTP/1.1 200 OK\r\n", $buffer); + $this->assertContains("\r\n\r\n3\r\nbye\r\n0\r\n\r\n", $buffer); } - public function testResponseContainsSameRequestProtocolVersionForHttp10() + public function testResponseContainsSameRequestProtocolVersionAndRawBodyForHttp10() { $server = new Server($this->socket); $server->on('request', function (Request $request, Response $response) { $response->writeHead(); - $response->end(); + $response->end('bye'); }); $buffer = ''; @@ -258,6 +259,7 @@ function ($data) use (&$buffer) { $this->connection->emit('data', array($data)); $this->assertContains("HTTP/1.0 200 OK\r\n", $buffer); + $this->assertContains("\r\n\r\nbye", $buffer); } public function testParserErrorEmitted() From 6ac0acbc1f44fce1c3bde4295879d3c752b08ec9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 18 Feb 2017 00:51:10 +0100 Subject: [PATCH 068/456] Ensure writeContinue() only works for HTTP/1.1 messages --- README.md | 15 +++++++++------ src/Response.php | 14 ++++++++++---- tests/ResponseTest.php | 14 ++++++++++++++ 3 files changed, 33 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index ce27e302..7e53f7ae 100644 --- a/README.md +++ b/README.md @@ -217,12 +217,15 @@ $http->on('request', function (Request $request, Response $response) { }); ``` -Note that calling this method is strictly optional. -If you do not use it, then the client MUST continue sending the request body -after waiting some time. - -This method MUST NOT be invoked after calling `writeHead()`. -Calling this method after sending the headers will result in an `Exception`. +Note that calling this method is strictly optional for HTTP/1.1 responses. +If you do not use it, then a HTTP/1.1 client MUST continue sending the +request body after waiting some time. + +This method MUST NOT be invoked after calling [`writeHead()`](#writehead). +This method MUST NOT be invoked if this is not a HTTP/1.1 response +(please check [`expectsContinue()`](#expectscontinue) as above). +Calling this method after sending the headers or if this is not a HTTP/1.1 +response is an error that will result in an `Exception`. #### writeHead() diff --git a/src/Response.php b/src/Response.php index e05cec2c..952ae0d6 100644 --- a/src/Response.php +++ b/src/Response.php @@ -98,12 +98,15 @@ public function isWritable() * }); * ``` * - * Note that calling this method is strictly optional. - * If you do not use it, then the client MUST continue sending the request body - * after waiting some time. + * Note that calling this method is strictly optional for HTTP/1.1 responses. + * If you do not use it, then a HTTP/1.1 client MUST continue sending the + * request body after waiting some time. * * This method MUST NOT be invoked after calling `writeHead()`. - * Calling this method after sending the headers will result in an `Exception`. + * This method MUST NOT be invoked if this is not a HTTP/1.1 response + * (please check [`expectsContinue()`] as above). + * Calling this method after sending the headers or if this is not a HTTP/1.1 + * response is an error that will result in an `Exception`. * * @return void * @throws \Exception @@ -111,6 +114,9 @@ public function isWritable() */ public function writeContinue() { + if ($this->protocolVersion !== '1.1') { + throw new \Exception('Continue requires a HTTP/1.1 message'); + } if ($this->headWritten) { throw new \Exception('Response head has already been written.'); } diff --git a/tests/ResponseTest.php b/tests/ResponseTest.php index 69a16001..6200934b 100644 --- a/tests/ResponseTest.php +++ b/tests/ResponseTest.php @@ -239,6 +239,20 @@ public function writeContinueShouldSendContinueLineBeforeRealHeaders() $response->writeHead(); } + /** + * @test + * @expectedException Exception + */ + public function writeContinueShouldThrowForHttp10() + { + $conn = $this + ->getMockBuilder('React\Socket\ConnectionInterface') + ->getMock(); + + $response = new Response($conn, '1.0'); + $response->writeContinue(); + } + /** @test */ public function shouldForwardEndDrainAndErrorEvents() { From 531c466c3dfaa1c16899214713eb229d4b6f827f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 17 Feb 2017 23:05:46 +0100 Subject: [PATCH 069/456] Only support HTTP/1.1 and HTTP/1.0 requests --- README.md | 6 ++++-- src/Server.php | 12 ++++++++++-- tests/ServerTest.php | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 7e53f7ae..26e37536 100644 --- a/README.md +++ b/README.md @@ -88,8 +88,10 @@ $http->on('request', function (Request $request, Response $response) { See also [`Request`](#request) and [`Response`](#response) for more details. -If a client sends an invalid request message, it will emit an `error` event, -send an HTTP error response to the client and close the connection: +The `Server` supports both HTTP/1.1 and HTTP/1.0 request messages. +If a client sends an invalid request message or uses an invalid HTTP protocol +version, it will emit an `error` event, send an HTTP error response to the +client and close the connection: ```php $http->on('error', function (Exception $e) { diff --git a/src/Server.php b/src/Server.php index 93aaefdf..cdc017f7 100644 --- a/src/Server.php +++ b/src/Server.php @@ -28,8 +28,10 @@ * * See also [`Request`](#request) and [`Response`](#response) for more details. * - * If a client sends an invalid request message, it will emit an `error` event, - * send an HTTP error response to the client and close the connection: + * The `Server` supports both HTTP/1.1 and HTTP/1.0 request messages. + * If a client sends an invalid request message or uses an invalid HTTP protocol + * version, it will emit an `error` event, send an HTTP error response to the + * client and close the connection: * * ```php * $http->on('error', function (Exception $e) { @@ -107,6 +109,12 @@ public function handleConnection(ConnectionInterface $conn) /** @internal */ public function handleRequest(ConnectionInterface $conn, Request $request) { + // only support HTTP/1.1 and HTTP/1.0 requests + if ($request->getProtocolVersion() !== '1.1' && $request->getProtocolVersion() !== '1.0') { + $this->emit('error', array(new \InvalidArgumentException('Received request with invalid protocol version'))); + return $this->writeError($conn, 505); + } + $response = new Response($conn, $request->getProtocolVersion()); $response->on('close', array($request, 'close')); diff --git a/tests/ServerTest.php b/tests/ServerTest.php index f72bfe97..bd0fdcf7 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -262,6 +262,38 @@ function ($data) use (&$buffer) { $this->assertContains("\r\n\r\nbye", $buffer); } + public function testRequestInvalidHttpProtocolVersionWillEmitErrorAndSendErrorResponse() + { + $error = null; + $server = new Server($this->socket); + $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; + } + ) + ); + + $this->socket->emit('connection', array($this->connection)); + + $data = "GET / HTTP/1.2\r\nHost: localhost\r\n\r\n"; + $this->connection->emit('data', array($data)); + + $this->assertInstanceOf('InvalidArgumentException', $error); + + $this->assertContains("HTTP/1.1 505 HTTP Version Not Supported\r\n", $buffer); + $this->assertContains("\r\n\r\nError 505: HTTP Version Not Supported", $buffer); + } + public function testParserErrorEmitted() { $error = null; From c72c125cd90918379ae515d8cede214b3379a146 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 19 Feb 2017 13:49:39 +0100 Subject: [PATCH 070/456] Explicitly send Connection: close header for HTTP/1.1 messages Persistent connections (`Connection: keep-alive`) are currently not supported, so make sure we let the client know. --- README.md | 6 ++++++ src/Response.php | 18 ++++++++++++++++++ tests/ResponseTest.php | 31 +++++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+) diff --git a/README.md b/README.md index 26e37536..5aec2a4c 100644 --- a/README.md +++ b/README.md @@ -279,6 +279,12 @@ $response->writeHead(200, array( )); ``` +Note that persistent connections (`Connection: keep-alive`) are currently +not supported. +As such, HTTP/1.1 response messages will automatically include a +`Connection: close` header, irrespective of what header values are +passed explicitly. + ## Install The recommended way to install this library is [through Composer](http://getcomposer.org). diff --git a/src/Response.php b/src/Response.php index 952ae0d6..9483523d 100644 --- a/src/Response.php +++ b/src/Response.php @@ -172,6 +172,12 @@ public function writeContinue() * )); * ``` * + * Note that persistent connections (`Connection: keep-alive`) are currently + * not supported. + * As such, HTTP/1.1 response messages will automatically include a + * `Connection: close` header, irrespective of what header values are + * passed explicitly. + * * @param int $status * @param array $headers * @throws \Exception @@ -204,6 +210,18 @@ public function writeHead($status = 200, array $headers = array()) $this->chunkedEncoding = true; } + // HTTP/1.1 assumes persistent connection support by default + // we do not support persistent connections, so let the client know + if ($this->protocolVersion === '1.1') { + foreach($headers as $name => $value) { + if (strtolower($name) === 'connection') { + unset($headers[$name]); + } + } + + $headers['Connection'] = 'close'; + } + $data = $this->formatHead($status, $headers); $this->conn->write($data); diff --git a/tests/ResponseTest.php b/tests/ResponseTest.php index 6200934b..8c3e9077 100644 --- a/tests/ResponseTest.php +++ b/tests/ResponseTest.php @@ -12,6 +12,7 @@ public function testResponseShouldBeChunkedByDefault() $expected .= "HTTP/1.1 200 OK\r\n"; $expected .= "X-Powered-By: React/alpha\r\n"; $expected .= "Transfer-Encoding: chunked\r\n"; + $expected .= "Connection: close\r\n"; $expected .= "\r\n"; $conn = $this @@ -51,6 +52,7 @@ public function testResponseShouldBeChunkedEvenWithOtherTransferEncoding() $expected .= "HTTP/1.1 200 OK\r\n"; $expected .= "X-Powered-By: React/alpha\r\n"; $expected .= "Transfer-Encoding: chunked\r\n"; + $expected .= "Connection: close\r\n"; $expected .= "\r\n"; $conn = $this @@ -71,6 +73,7 @@ public function testResponseShouldNotBeChunkedWithContentLength() $expected .= "HTTP/1.1 200 OK\r\n"; $expected .= "X-Powered-By: React/alpha\r\n"; $expected .= "Content-Length: 22\r\n"; + $expected .= "Connection: close\r\n"; $expected .= "\r\n"; $conn = $this @@ -91,6 +94,7 @@ public function testResponseShouldNotBeChunkedWithContentLengthCaseInsensitive() $expected .= "HTTP/1.1 200 OK\r\n"; $expected .= "X-Powered-By: React/alpha\r\n"; $expected .= "CONTENT-LENGTH: 0\r\n"; + $expected .= "Connection: close\r\n"; $expected .= "\r\n"; $conn = $this @@ -111,6 +115,7 @@ public function testResponseShouldIncludeCustomByPoweredAsFirstHeaderIfGivenExpl $expected .= "HTTP/1.1 200 OK\r\n"; $expected .= "Content-Length: 0\r\n"; $expected .= "X-POWERED-BY: demo\r\n"; + $expected .= "Connection: close\r\n"; $expected .= "\r\n"; $conn = $this @@ -130,6 +135,7 @@ public function testResponseShouldNotIncludePoweredByIfGivenEmptyArray() $expected = ''; $expected .= "HTTP/1.1 200 OK\r\n"; $expected .= "Content-Length: 0\r\n"; + $expected .= "Connection: close\r\n"; $expected .= "\r\n"; $conn = $this @@ -144,6 +150,27 @@ public function testResponseShouldNotIncludePoweredByIfGivenEmptyArray() $response->writeHead(200, array('Content-Length' => 0, 'X-Powered-By' => array())); } + public function testResponseShouldAlwaysIncludeConnectionCloseIrrespectiveOfExplicitValue() + { + $expected = ''; + $expected .= "HTTP/1.1 200 OK\r\n"; + $expected .= "X-Powered-By: React/alpha\r\n"; + $expected .= "Content-Length: 0\r\n"; + $expected .= "Connection: close\r\n"; + $expected .= "\r\n"; + + $conn = $this + ->getMockBuilder('React\Socket\ConnectionInterface') + ->getMock(); + $conn + ->expects($this->once()) + ->method('write') + ->with($expected); + + $response = new Response($conn); + $response->writeHead(200, array('Content-Length' => 0, 'connection' => 'ignored')); + } + public function testResponseBodyShouldBeChunkedCorrectly() { $conn = $this @@ -283,6 +310,7 @@ public function shouldRemoveNewlinesFromHeaders() $expected .= "X-Powered-By: React/alpha\r\n"; $expected .= "FooBar: BazQux\r\n"; $expected .= "Transfer-Encoding: chunked\r\n"; + $expected .= "Connection: close\r\n"; $expected .= "\r\n"; $conn = $this @@ -304,6 +332,7 @@ public function missingStatusCodeTextShouldResultInNumberOnlyStatus() $expected .= "HTTP/1.1 700 \r\n"; $expected .= "X-Powered-By: React/alpha\r\n"; $expected .= "Transfer-Encoding: chunked\r\n"; + $expected .= "Connection: close\r\n"; $expected .= "\r\n"; $conn = $this @@ -327,6 +356,7 @@ public function shouldAllowArrayHeaderValues() $expected .= "Set-Cookie: foo=bar\r\n"; $expected .= "Set-Cookie: bar=baz\r\n"; $expected .= "Transfer-Encoding: chunked\r\n"; + $expected .= "Connection: close\r\n"; $expected .= "\r\n"; $conn = $this @@ -348,6 +378,7 @@ public function shouldIgnoreHeadersWithNullValues() $expected .= "HTTP/1.1 200 OK\r\n"; $expected .= "X-Powered-By: React/alpha\r\n"; $expected .= "Transfer-Encoding: chunked\r\n"; + $expected .= "Connection: close\r\n"; $expected .= "\r\n"; $conn = $this From 070e3f02291377e1176f74f1c2e091a08dffb737 Mon Sep 17 00:00:00 2001 From: Niels Theen Date: Sat, 11 Feb 2017 07:29:55 +0100 Subject: [PATCH 071/456] Add TestCase methods --- tests/TestCase.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/TestCase.php b/tests/TestCase.php index 73bb401e..74ad0bc7 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -45,6 +45,20 @@ protected function expectCallableNever() return $mock; } + protected function expectCallableConsecutive($numberOfCalls, array $with) + { + $mock = $this->createCallableMock(); + + for ($i = 0; $i < $numberOfCalls; $i++) { + $mock + ->expects($this->at($i)) + ->method('__invoke') + ->with($this->equalTo($with[$i])); + } + + return $mock; + } + protected function createCallableMock() { return $this From 1a1b0aa9ee7b55ff5ddf8ab699f0ba4afa5d5e20 Mon Sep 17 00:00:00 2001 From: Niels Theen Date: Mon, 13 Feb 2017 15:10:02 +0100 Subject: [PATCH 072/456] Add Chunked Decoder class Fix Endless loop Fix Add chunk size check and chunk extension handling Handle potential test cases Add ChunkedDecoder Tests Handle potential threat Rename variable Added test to add verify single characters can be emitted Fixing remarks Use Mockbuilder --- src/ChunkedDecoder.php | 158 ++++++++++++++ tests/ChunkedDecoderTest.php | 399 +++++++++++++++++++++++++++++++++++ 2 files changed, 557 insertions(+) create mode 100644 src/ChunkedDecoder.php create mode 100644 tests/ChunkedDecoderTest.php diff --git a/src/ChunkedDecoder.php b/src/ChunkedDecoder.php new file mode 100644 index 00000000..1d3bc16f --- /dev/null +++ b/src/ChunkedDecoder.php @@ -0,0 +1,158 @@ +input = $input; + + $this->input->on('data', array($this, 'handleData')); + $this->input->on('end', array($this, 'handleEnd')); + $this->input->on('error', array($this, 'handleError')); + $this->input->on('close', array($this, 'close')); + } + + public function isReadable() + { + return !$this->closed && $this->input->isReadable(); + } + + public function pause() + { + $this->input->pause(); + } + + public function resume() + { + $this->input->resume(); + } + + public function pipe(WritableStreamInterface $dest, array $options = array()) + { + Util::pipe($this, $dest, $options); + + return $dest; + } + + public function close() + { + if ($this->closed) { + return; + } + + $this->buffer = ''; + + $this->closed = true; + + $this->input->close(); + + $this->emit('close'); + $this->removeAllListeners(); + } + + /** @internal */ + public function handleEnd() + { + if (!$this->closed) { + $this->handleError(new \Exception('Unexpected end event')); + } + } + + /** @internal */ + public function handleError(\Exception $e) + { + $this->emit('error', array($e)); + $this->close(); + } + + /** @internal */ + public function handleData($data) + { + $this->buffer .= $data; + + while ($this->buffer !== '') { + if (!$this->headerCompleted) { + $positionCrlf = strpos($this->buffer, static::CRLF); + + if ($positionCrlf === false) { + // Header shouldn't be bigger than 1024 bytes + if (isset($this->buffer[static::MAX_CHUNK_HEADER_SIZE])) { + $this->handleError(new \Exception('Chunk header size inclusive extension bigger than' . static::MAX_CHUNK_HEADER_SIZE. ' bytes')); + } + return; + } + + $header = strtolower((string)substr($this->buffer, 0, $positionCrlf)); + $hexValue = $header; + + if (strpos($header, ';') !== false) { + $array = explode(';', $header); + $hexValue = $array[0]; + } + + $this->chunkSize = hexdec($hexValue); + if (dechex($this->chunkSize) !== $hexValue) { + $this->handleError(new \Exception($hexValue . ' is not a valid hexadecimal number')); + return; + } + + $this->buffer = (string)substr($this->buffer, $positionCrlf + 2); + $this->headerCompleted = true; + if ($this->buffer === '') { + return; + } + } + + $chunk = (string)substr($this->buffer, 0, $this->chunkSize - $this->transferredSize); + + if ($chunk !== '') { + $this->transferredSize += strlen($chunk); + $this->emit('data', array($chunk)); + $this->buffer = (string)substr($this->buffer, strlen($chunk)); + } + + $positionCrlf = strpos($this->buffer, static::CRLF); + + if ($positionCrlf === 0) { + if ($this->chunkSize === 0) { + $this->emit('end'); + $this->close(); + return; + } + $this->chunkSize = 0; + $this->headerCompleted = false; + $this->transferredSize = 0; + $this->buffer = (string)substr($this->buffer, 2); + } + + if ($positionCrlf !== 0 && $this->chunkSize === $this->transferredSize && strlen($this->buffer) > 2) { + // the first 2 characters are not CLRF, send error event + $this->handleError(new \Exception('Chunk does not end with a CLRF')); + return; + } + + if ($positionCrlf !== 0 && strlen($this->buffer) < 2) { + // No CLRF found, wait for additional data which could be a CLRF + return; + } + } + } +} diff --git a/tests/ChunkedDecoderTest.php b/tests/ChunkedDecoderTest.php new file mode 100644 index 00000000..6f0c3048 --- /dev/null +++ b/tests/ChunkedDecoderTest.php @@ -0,0 +1,399 @@ +input = new ReadableStream(); + $this->parser = new ChunkedDecoder($this->input); + } + + public function testSimpleChunk() + { + $this->parser->on('data', $this->expectCallableOnceWith('hello')); + $this->parser->on('end', $this->expectCallableNever()); + $this->parser->on('close', $this->expectCallableNever()); + + $this->input->emit('data', array("5\r\nhello\r\n")); + } + + public function testTwoChunks() + { + $this->parser->on('data', $this->expectCallableConsecutive(2, array('hello', 'bla'))); + $this->parser->on('end', $this->expectCallableNever()); + $this->parser->on('close', $this->expectCallableNever()); + + $this->input->emit('data', array("5\r\nhello\r\n3\r\nbla\r\n")); + } + + public function testEnd() + { + $this->parser->on('end', $this->expectCallableOnce()); + $this->parser->on('close', $this->expectCallableOnce()); + $this->parser->on('error', $this->expectCallableNever()); + + $this->input->emit('data', array("0\r\n\r\n")); + } + + public function testParameterWithEnd() + { + $this->parser->on('data', $this->expectCallableConsecutive(2, array('hello', 'bla'))); + $this->parser->on('end', $this->expectCallableOnce()); + $this->parser->on('close', $this->expectCallableOnce()); + $this->parser->on('error', $this->expectCallableNever()); + + $this->input->emit('data', array("5\r\nhello\r\n3\r\nbla\r\n0\r\n\r\n")); + } + + public function testInvalidChunk() + { + $this->parser->on('data', $this->expectCallableNever()); + $this->parser->on('end', $this->expectCallableNever()); + $this->parser->on('close', $this->expectCallableOnce()); + $this->parser->on('error', $this->expectCallableOnce()); + + $this->input->emit('data', array("bla\r\n")); + } + + public function testNeverEnd() + { + $this->parser->on('end', $this->expectCallableNever()); + $this->parser->on('close', $this->expectCallableNever()); + $this->parser->on('error', $this->expectCallableNever()); + + $this->input->emit('data', array("0\r\n")); + } + + public function testWrongChunkHex() + { + $this->parser->on('error', $this->expectCallableOnce()); + $this->parser->on('close', $this->expectCallableOnce()); + $this->parser->on('end', $this->expectCallableNever()); + + $this->input->emit('data', array("2\r\na\r\n5\r\nhello\r\n")); + } + + public function testSplittedChunk() + { + $this->parser->on('data', $this->expectCallableOnceWith('welt')); + $this->parser->on('close', $this->expectCallableNever()); + $this->parser->on('end', $this->expectCallableNever()); + $this->parser->on('error', $this->expectCallableNever()); + + $this->input->emit('data', array("4\r\n")); + $this->input->emit('data', array("welt\r\n")); + } + + public function testSplittedHeader() + { + $this->parser->on('data', $this->expectCallableOnceWith('welt')); + $this->parser->on('close', $this->expectCallableNever()); + $this->parser->on('end', $this->expectCallableNever());# + $this->parser->on('error', $this->expectCallableNever()); + + + $this->input->emit('data', array("4")); + $this->input->emit('data', array("\r\nwelt\r\n")); + } + + public function testSplittedBoth() + { + $this->parser->on('data', $this->expectCallableOnceWith('welt')); + $this->parser->on('close', $this->expectCallableNever()); + $this->parser->on('end', $this->expectCallableNever()); + $this->parser->on('error', $this->expectCallableNever()); + + $this->input->emit('data', array("4")); + $this->input->emit('data', array("\r\n")); + $this->input->emit('data', array("welt\r\n")); + } + + public function testCompletlySplitted() + { + $this->parser->on('data', $this->expectCallableConsecutive(2, array('we', 'lt'))); + $this->parser->on('close', $this->expectCallableNever()); + $this->parser->on('end', $this->expectCallableNever()); + $this->parser->on('error', $this->expectCallableNever()); + + $this->input->emit('data', array("4")); + $this->input->emit('data', array("\r\n")); + $this->input->emit('data', array("we")); + $this->input->emit('data', array("lt\r\n")); + } + + public function testMixed() + { + $this->parser->on('data', $this->expectCallableConsecutive(3, array('we', 'lt', 'hello'))); + $this->parser->on('close', $this->expectCallableNever()); + $this->parser->on('end', $this->expectCallableNever()); + $this->parser->on('error', $this->expectCallableNever()); + + $this->input->emit('data', array("4")); + $this->input->emit('data', array("\r\n")); + $this->input->emit('data', array("we")); + $this->input->emit('data', array("lt\r\n")); + $this->input->emit('data', array("5\r\nhello\r\n")); + } + + public function testBigger() + { + $this->parser->on('data', $this->expectCallableConsecutive(2, array('abcdeabcdeabcdea', 'hello'))); + $this->parser->on('close', $this->expectCallableNever()); + $this->parser->on('end', $this->expectCallableNever()); + $this->parser->on('error', $this->expectCallableNever()); + + $this->input->emit('data', array("1")); + $this->input->emit('data', array("0")); + $this->input->emit('data', array("\r\n")); + $this->input->emit('data', array("abcdeabcdeabcdea\r\n")); + $this->input->emit('data', array("5\r\nhello\r\n")); + } + + public function testOneUnfinished() + { + $this->parser->on('data', $this->expectCallableConsecutive(2, array('bla', 'hello'))); + $this->parser->on('close', $this->expectCallableNever()); + $this->parser->on('end', $this->expectCallableNever()); + $this->parser->on('error', $this->expectCallableNever()); + + $this->input->emit('data', array("3\r\n")); + $this->input->emit('data', array("bla\r\n")); + $this->input->emit('data', array("5\r\nhello")); + } + + public function testChunkIsBiggerThenExpected() + { + $this->parser->on('data', $this->expectCallableOnceWith('hello')); + $this->parser->on('close', $this->expectCallableOnce()); + $this->parser->on('end', $this->expectCallableNever()); + $this->parser->on('error', $this->expectCallableOnce()); + + $this->input->emit('data', array("5\r\n")); + $this->input->emit('data', array("hello world\r\n")); + } + + public function testHandleUnexpectedEnd() + { + $this->parser->on('data', $this->expectCallableNever()); + $this->parser->on('close', $this->expectCallableOnce()); + $this->parser->on('end', $this->expectCallableNever()); + $this->parser->on('error', $this->expectCallableOnce()); + + $this->input->emit('end'); + } + + public function testExtensionWillBeIgnored() + { + $this->parser->on('data', $this->expectCallableOnceWith('bla')); + $this->parser->on('close', $this->expectCallableNever()); + $this->parser->on('end', $this->expectCallableNever()); + $this->parser->on('error', $this->expectCallableNever()); + + $this->input->emit('data', array("3;hello=world;foo=bar\r\nbla")); + } + + public function testChunkHeaderIsTooBig() + { + $this->parser->on('data', $this->expectCallableNever()); + $this->parser->on('close', $this->expectCallableOnce()); + $this->parser->on('end', $this->expectCallableNever()); + $this->parser->on('error', $this->expectCallableOnce()); + + $data = ''; + for ($i = 0; $i < 1025; $i++) { + $data .= 'a'; + } + $this->input->emit('data', array($data)); + } + + public function testChunkIsMaximumSize() + { + $this->parser->on('data', $this->expectCallableNever()); + $this->parser->on('close', $this->expectCallableOnce()); + $this->parser->on('end', $this->expectCallableNever()); + $this->parser->on('error', $this->expectCallableOnce()); + + $data = ''; + for ($i = 0; $i < 1024; $i++) { + $data .= 'a'; + } + $data .= "\r\n"; + + $this->input->emit('data', array($data)); + } + + public function testLateCrlf() + { + $this->parser->on('data', $this->expectCallableOnceWith('late')); + $this->parser->on('close', $this->expectCallableNever()); + $this->parser->on('end', $this->expectCallableNever()); + $this->parser->on('error', $this->expectCallableNever()); + + $this->input->emit('data', array("4\r\nlate")); + $this->input->emit('data', array("\r")); + $this->input->emit('data', array("\n")); + } + + public function testNoCrlfInChunk() + { + $this->parser->on('data', $this->expectCallableOnceWith('no')); + $this->parser->on('close', $this->expectCallableOnce()); + $this->parser->on('end', $this->expectCallableNever()); + $this->parser->on('error', $this->expectCallableOnce()); + + $this->input->emit('data', array("2\r\nno crlf")); + } + + public function testNoCrlfInChunkSplitted() + { + $this->parser->on('data', $this->expectCallableOnceWith('no')); + $this->parser->on('close', $this->expectCallableOnce()); + $this->parser->on('end', $this->expectCallableNever()); + $this->parser->on('error', $this->expectCallableOnce()); + + $this->input->emit('data', array("2\r\n")); + $this->input->emit('data', array("no")); + $this->input->emit('data', array("further")); + $this->input->emit('data', array("clrf")); + } + + public function testEmitEmptyChunkBody() + { + $this->parser->on('data', $this->expectCallableNever()); + $this->parser->on('close', $this->expectCallableNever()); + $this->parser->on('end', $this->expectCallableNever()); + $this->parser->on('error', $this->expectCallableNever()); + + $this->input->emit('data', array("2\r\n")); + $this->input->emit('data', array("")); + $this->input->emit('data', array("")); + } + + public function testEmitCrlfAsChunkBody() + { + $this->parser->on('data', $this->expectCallableOnceWith("\r\n")); + $this->parser->on('close', $this->expectCallableNever()); + $this->parser->on('end', $this->expectCallableNever()); + $this->parser->on('error', $this->expectCallableNever()); + + $this->input->emit('data', array("2\r\n")); + $this->input->emit('data', array("\r\n")); + $this->input->emit('data', array("\r\n")); + } + + public function testNegativeHeader() + { + $this->parser->on('data', $this->expectCallableNever()); + $this->parser->on('close', $this->expectCallableOnce()); + $this->parser->on('end', $this->expectCallableNever()); + $this->parser->on('error', $this->expectCallableOnce()); + + $this->input->emit('data', array("-2\r\n")); + } + + public function testHexDecimalInBodyIsPotentialThread() + { + $this->parser->on('data', $this->expectCallableOnce('test')); + $this->parser->on('close', $this->expectCallableOnce()); + $this->parser->on('end', $this->expectCallableNever()); + $this->parser->on('error', $this->expectCallableOnce()); + + $this->input->emit('data', array("4\r\ntest5\r\nworld")); + } + + public function testHexDecimalInBodyIsPotentialThreadSplitted() + { + $this->parser->on('data', $this->expectCallableOnce('test')); + $this->parser->on('close', $this->expectCallableOnce()); + $this->parser->on('end', $this->expectCallableNever()); + $this->parser->on('error', $this->expectCallableOnce()); + + $this->input->emit('data', array("4")); + $this->input->emit('data', array("\r\n")); + $this->input->emit('data', array("test")); + $this->input->emit('data', array("5")); + $this->input->emit('data', array("\r\n")); + $this->input->emit('data', array("world")); + } + + public function testEmitSingleCharacter() + { + $this->parser->on('data', $this->expectCallableConsecutive(4, array('t', 'e', 's', 't'))); + $this->parser->on('close', $this->expectCallableOnce()); + $this->parser->on('end', $this->expectCallableOnce()); + $this->parser->on('error', $this->expectCallableNever()); + + $array = str_split("4\r\ntest\r\n0\r\n\r\n"); + + foreach ($array as $character) { + $this->input->emit('data', array($character)); + } + } + + public function testHandleError() + { + $this->parser->on('error', $this->expectCallableOnce()); + $this->parser->on('close', $this->expectCallableOnce()); + $this->parser->on('end', $this->expectCallableNever()); + + $this->input->emit('error', array(new \RuntimeException())); + + $this->assertFalse($this->parser->isReadable()); + } + + public function testPauseStream() + { + $input = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $input->expects($this->once())->method('pause'); + + $parser = new ChunkedDecoder($input); + $parser->pause(); + } + + public function testResumeStream() + { + $input = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $input->expects($this->once())->method('pause'); + + $parser = new ChunkedDecoder($input); + $parser->pause(); + $parser->resume(); + } + + public function testPipeStream() + { + $dest = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + + $ret = $this->parser->pipe($dest); + + $this->assertSame($dest, $ret); + } + + public function testHandleClose() + { + $this->parser->on('close', $this->expectCallableOnce()); + + $this->input->close(); + $this->input->emit('end', array()); + + $this->assertFalse($this->parser->isReadable()); + } + + public function testOutputStreamCanCloseInputStream() + { + $input = new ReadableStream(); + $input->on('close', $this->expectCallableOnce()); + + $stream = new ChunkedDecoder($input); + $stream->on('close', $this->expectCallableOnce()); + + $stream->close(); + + $this->assertFalse($input->isReadable()); + } +} From 61d7b69e394dd4ed2bc8413650499e0501af70d4 Mon Sep 17 00:00:00 2001 From: Niels Theen Date: Fri, 10 Feb 2017 15:44:30 +0100 Subject: [PATCH 073/456] Add ChunkedDecoder to Server Add ServerTest Fix Order --- src/Server.php | 16 +++- tests/ServerTest.php | 173 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 186 insertions(+), 3 deletions(-) diff --git a/src/Server.php b/src/Server.php index 1a33bcbd..a9c2d623 100644 --- a/src/Server.php +++ b/src/Server.php @@ -88,7 +88,7 @@ public function handleConnection(ConnectionInterface $conn) $that->handleRequest($conn, $request); if ($bodyBuffer !== '') { - $request->emit('data', array($bodyBuffer)); + $conn->emit('data', array($bodyBuffer)); } }); @@ -122,6 +122,15 @@ public function handleRequest(ConnectionInterface $conn, Request $request) '[]' ); + $stream = $conn; + if ($request->hasHeader('Transfer-Encoding')) { + $transferEncodingHeader = $request->getHeader('Transfer-Encoding'); + // 'chunked' must always be the final value of 'Transfer-Encoding' according to: https://tools.ietf.org/html/rfc7230#section-3.3.1 + if (strtolower(end($transferEncodingHeader)) === 'chunked') { + $stream = new ChunkedDecoder($conn); + } + } + // forward pause/resume calls to underlying connection $request->on('pause', array($conn, 'pause')); $request->on('resume', array($conn, 'resume')); @@ -133,10 +142,11 @@ public function handleRequest(ConnectionInterface $conn, Request $request) }); // forward connection events to request - $conn->on('end', function () use ($request) { + $stream->on('end', function () use ($request) { $request->emit('end'); }); - $conn->on('data', function ($data) use ($request) { + + $stream->on('data', function ($data) use ($request) { $request->emit('data', array($data)); }); diff --git a/tests/ServerTest.php b/tests/ServerTest.php index d2ff7aef..84b33ca9 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -265,6 +265,179 @@ function ($data) use (&$buffer) { $this->assertContains("\r\n\r\nError 400: Bad Request", $buffer); } + public function testBodyDataWillBeSendViaRequestEvent() + { + $server = new Server($this->socket); + + $dataEvent = $this->expectCallableOnceWith('hello'); + $endEvent = $this->expectCallableNever(); + $closeEvent = $this->expectCallableNever(); + $errorEvent = $this->expectCallableNever(); + + $server->on('request', function (Request $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $request->on('data', $dataEvent); + $request->on('end', $endEvent); + $request->on('close', $closeEvent); + $request->on('error', $errorEvent); + }); + + $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 .= "\r\n"; + $data .= "hello"; + + $this->connection->emit('data', array($data)); + } + + public function testChunkedEncodedRequestWillBeParsedForRequestEvent() + { + $server = new Server($this->socket); + + $dataEvent = $this->expectCallableOnceWith('hello'); + $endEvent = $this->expectCallableOnce(); + $closeEvent = $this->expectCallableNever(); + $errorEvent = $this->expectCallableNever(); + + $server->on('request', function (Request $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $request->on('data', $dataEvent); + $request->on('end', $endEvent); + $request->on('close', $closeEvent); + $request->on('error', $errorEvent); + }); + + $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: chunked\r\n"; + $data .= "\r\n"; + $data .= "5\r\nhello\r\n"; + $data .= "0\r\n\r\n"; + + $this->connection->emit('data', array($data)); + } + + public function testChunkedEncodedRequestAdditionalDataWontBeEmitted() + { + $server = new Server($this->socket); + + $dataEvent = $this->expectCallableOnceWith('hello'); + $endEvent = $this->expectCallableOnce(); + $closeEvent = $this->expectCallableNever(); + $errorEvent = $this->expectCallableNever(); + + $server->on('request', function (Request $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $request->on('data', $dataEvent); + $request->on('end', $endEvent); + $request->on('close', $closeEvent); + $request->on('error', $errorEvent); + }); + + $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: chunked\r\n"; + $data .= "\r\n"; + $data .= "5\r\nhello\r\n"; + $data .= "0\r\n\r\n"; + $data .= "2\r\nhi\r\n"; + + $this->connection->emit('data', array($data)); + } + + public function testEmptyChunkedEncodedRequest() + { + $server = new Server($this->socket); + + $dataEvent = $this->expectCallableNever(); + $endEvent = $this->expectCallableOnce(); + $closeEvent = $this->expectCallableNever(); + $errorEvent = $this->expectCallableNever(); + + $server->on('request', function (Request $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $request->on('data', $dataEvent); + $request->on('end', $endEvent); + $request->on('close', $closeEvent); + $request->on('error', $errorEvent); + }); + + $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: chunked\r\n"; + $data .= "\r\n"; + $data .= "0\r\n\r\n"; + + $this->connection->emit('data', array($data)); + } + + public function testChunkedIsUpperCase() + { + $server = new Server($this->socket); + + $dataEvent = $this->expectCallableOnceWith('hello'); + $endEvent = $this->expectCallableOnce(); + $closeEvent = $this->expectCallableNever(); + $errorEvent = $this->expectCallableNever(); + + $server->on('request', function (Request $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $request->on('data', $dataEvent); + $request->on('end', $endEvent); + $request->on('close', $closeEvent); + $request->on('error', $errorEvent); + }); + + $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: CHUNKED\r\n"; + $data .= "\r\n"; + $data .= "5\r\nhello\r\n"; + $data .= "0\r\n\r\n"; + + $this->connection->emit('data', array($data)); + } + + public function testChunkedIsMixedUpperAndLowerCase() + { + $server = new Server($this->socket); + + $dataEvent = $this->expectCallableOnceWith('hello'); + $endEvent = $this->expectCallableOnce(); + $closeEvent = $this->expectCallableNever(); + $errorEvent = $this->expectCallableNever(); + + $server->on('request', function (Request $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $request->on('data', $dataEvent); + $request->on('end', $endEvent); + $request->on('close', $closeEvent); + $request->on('error', $errorEvent); + }); + + $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: CHunKeD\r\n"; + $data .= "\r\n"; + $data .= "5\r\nhello\r\n"; + $data .= "0\r\n\r\n"; + + $this->connection->emit('data', array($data)); + } + private function createGetRequest() { $data = "GET / HTTP/1.1\r\n"; From 16b425b20df0b757b698d0b93e85897d08217e01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 17 Feb 2017 23:02:19 +0100 Subject: [PATCH 074/456] Validate Host header for HTTP/1.1 requests --- src/Server.php | 18 +++++++ tests/ServerTest.php | 111 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 128 insertions(+), 1 deletion(-) diff --git a/src/Server.php b/src/Server.php index 9c6bc331..1ffb3e69 100644 --- a/src/Server.php +++ b/src/Server.php @@ -115,6 +115,24 @@ public function handleRequest(ConnectionInterface $conn, Request $request) return $this->writeError($conn, 505); } + // HTTP/1.1 requests MUST include a valid host header (host and optional port) + // https://tools.ietf.org/html/rfc7230#section-5.4 + if ($request->getProtocolVersion() === '1.1') { + $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 + unset($parts['scheme'], $parts['host'], $parts['port']); + if ($parts === false || $parts) { + $this->emit('error', array(new \InvalidArgumentException('Invalid Host header for HTTP/1.1 request'))); + return $this->writeError($conn, 400); + } + } + $response = new Response($conn, $request->getProtocolVersion()); $response->on('close', array($request, 'close')); diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 47ca3f34..713990b8 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -159,6 +159,7 @@ public function testRequestEventWithSecondDataEventWillEmitBodyData() $data = ''; $data .= "POST / HTTP/1.1\r\n"; + $data .= "Host: localhost\r\n"; $data .= "Content-Length: 100\r\n"; $data .= "\r\n"; $data .= "incomplete"; @@ -178,6 +179,7 @@ public function testRequestEventWithPartialBodyWillEmitData() $data = ''; $data .= "POST / HTTP/1.1\r\n"; + $data .= "Host: localhost\r\n"; $data .= "Content-Length: 100\r\n"; $data .= "\r\n"; $this->connection->emit('data', array($data)); @@ -239,7 +241,7 @@ function ($data) use (&$buffer) { $this->socket->emit('connection', array($this->connection)); - $data = "GET / HTTP/1.1\r\n\r\n"; + $data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"; $this->connection->emit('data', array($data)); $this->assertContains("HTTP/1.1 200 OK\r\n", $buffer); @@ -526,7 +528,114 @@ public function testChunkedIsMixedUpperAndLowerCase() $data .= "\r\n"; $data .= "5\r\nhello\r\n"; $data .= "0\r\n\r\n"; + $this->connection->emit('data', array($data)); + } + + public function testRequestHttp11WithoutHostWillEmitErrorAndSendErrorResponse() + { + $error = null; + $server = new Server($this->socket); + $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; + } + ) + ); + $this->socket->emit('connection', array($this->connection)); + + $data = "GET / HTTP/1.1\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 testRequestHttp11WithMalformedHostWillEmitErrorAndSendErrorResponse() + { + $error = null; + $server = new Server($this->socket); + $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; + } + ) + ); + + $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 testRequestHttp11WithInvalidHostUriComponentsWillEmitErrorAndSendErrorResponse() + { + $error = null; + $server = new Server($this->socket); + $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; + } + ) + ); + + $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 testRequestHttp10WithoutHostEmitsRequestWithNoError() + { + $server = new Server($this->socket); + $server->on('request', $this->expectCallableOnce()); + $server->on('error', $this->expectCallableNever()); + + $this->socket->emit('connection', array($this->connection)); + + $data = "GET / HTTP/1.0\r\n\r\n"; $this->connection->emit('data', array($data)); } From 11841b0c89f25afa669380ba19767db10642c49a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 16 Feb 2017 20:02:48 +0100 Subject: [PATCH 075/456] The Server should always have a `request` listener --- README.md | 4 ++++ src/Server.php | 10 ++++------ tests/ServerTest.php | 22 ++++++++++++++++++++++ 3 files changed, 30 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 26e37536..2155cf56 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,10 @@ $http->on('request', function (Request $request, Response $response) { See also [`Request`](#request) and [`Response`](#response) for more details. +> Note that you SHOULD always listen for the `request` event. +Failing to do so will result in the server parsing the incoming request, +but never sending a response back to the client. + The `Server` supports both HTTP/1.1 and HTTP/1.0 request messages. If a client sends an invalid request message or uses an invalid HTTP protocol version, it will emit an `error` event, send an HTTP error response to the diff --git a/src/Server.php b/src/Server.php index 9c6bc331..3c4e2298 100644 --- a/src/Server.php +++ b/src/Server.php @@ -28,6 +28,10 @@ * * See also [`Request`](#request) and [`Response`](#response) for more details. * + * > Note that you SHOULD always listen for the `request` event. + * Failing to do so will result in the server parsing the incoming request, + * but never sending a response back to the client. + * * The `Server` supports both HTTP/1.1 and HTTP/1.0 request messages. * If a client sends an invalid request message or uses an invalid HTTP protocol * version, it will emit an `error` event, send an HTTP error response to the @@ -118,12 +122,6 @@ public function handleRequest(ConnectionInterface $conn, Request $request) $response = new Response($conn, $request->getProtocolVersion()); $response->on('close', array($request, 'close')); - if (!$this->listeners('request')) { - $response->end(); - - return; - } - // attach remote ip to the request as metadata $request->remoteAddress = trim( parse_url('tcp://' . $conn->getRemoteAddress(), PHP_URL_HOST), diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 47ca3f34..da9584f5 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -308,6 +308,28 @@ function ($data) use (&$buffer) { $this->assertContains("\r\n\r\nError 505: HTTP Version Not Supported", $buffer); } + public function testServerWithNoRequestListenerDoesNotSendAnythingToConnection() + { + $server = new Server($this->socket); + + $this->connection + ->expects($this->never()) + ->method('write'); + + $this->connection + ->expects($this->never()) + ->method('end'); + + $this->connection + ->expects($this->never()) + ->method('close'); + + $this->socket->emit('connection', array($this->connection)); + + $data = $this->createGetRequest(); + $this->connection->emit('data', array($data)); + } + public function testParserErrorEmitted() { $error = null; From 8116edcd7987588320768460ed266354b84ef964 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 20 Feb 2017 13:06:52 +0100 Subject: [PATCH 076/456] Request closes after forwarding close event --- src/Request.php | 14 +++++++++++++- src/Server.php | 14 ++++++-------- tests/RequestTest.php | 39 +++++++++++++++++++++++++++++++++++++++ tests/ServerTest.php | 41 ++++++++++++++++++++++++++++++++++++----- 4 files changed, 94 insertions(+), 14 deletions(-) diff --git a/src/Request.php b/src/Request.php index 27a11668..46bc3639 100644 --- a/src/Request.php +++ b/src/Request.php @@ -174,18 +174,30 @@ public function isReadable() public function pause() { + if (!$this->readable) { + return; + } + $this->emit('pause'); } public function resume() { + if (!$this->readable) { + return; + } + $this->emit('resume'); } public function close() { + if (!$this->readable) { + return; + } + $this->readable = false; - $this->emit('end'); + $this->emit('close'); $this->removeAllListeners(); } diff --git a/src/Server.php b/src/Server.php index 9c6bc331..57e18745 100644 --- a/src/Server.php +++ b/src/Server.php @@ -143,17 +143,15 @@ public function handleRequest(ConnectionInterface $conn, Request $request) $request->on('pause', array($conn, 'pause')); $request->on('resume', array($conn, 'resume')); - // closing the request currently emits an "end" event - // stop reading from the connection by pausing it - $request->on('end', function () use ($conn) { - $conn->pause(); - }); + // request closed => stop reading from the stream by pausing it + // stream closed => close request + $request->on('close', array($stream, 'pause')); + $stream->on('close', array($request, 'close')); - // forward connection events to request - $stream->on('end', function () use ($request) { + // forward data and end events from body stream to request + $stream->on('end', function() use ($request) { $request->emit('end'); }); - $stream->on('data', function ($data) use ($request) { $request->emit('data', array($data)); }); diff --git a/tests/RequestTest.php b/tests/RequestTest.php index 7e630a3e..940a6a68 100644 --- a/tests/RequestTest.php +++ b/tests/RequestTest.php @@ -75,4 +75,43 @@ public function testHeaderWithMultipleValues() $this->assertEquals(array('a', 'b'), $request->getHeader('Test')); $this->assertEquals('a, b', $request->getHeaderLine('Test')); } + + public function testCloseEmitsCloseEvent() + { + $request = new Request('GET', '/'); + + $request->on('close', $this->expectCallableOnce()); + + $request->close(); + } + + public function testCloseMultipleTimesEmitsCloseEventOnce() + { + $request = new Request('GET', '/'); + + $request->on('close', $this->expectCallableOnce()); + + $request->close(); + $request->close(); + } + + public function testIsNotReadableAfterClose() + { + $request = new Request('GET', '/'); + + $request->close(); + + $this->assertFalse($request->isReadable()); + } + + public function testPipeReturnsDest() + { + $dest = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + + $request = new Request('GET', '/'); + + $ret = $request->pipe($dest); + + $this->assertSame($dest, $ret); + } } diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 47ca3f34..0e80e180 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -131,6 +131,37 @@ public function testRequestCloseWillPauseConnection() $this->connection->emit('data', array($data)); } + public function testRequestPauseAfterCloseWillNotBeForwarded() + { + $server = new Server($this->socket); + $server->on('request', function (Request $request) { + $request->close(); + $request->pause(); + }); + + $this->connection->expects($this->once())->method('pause'); + $this->socket->emit('connection', array($this->connection)); + + $data = $this->createGetRequest(); + $this->connection->emit('data', array($data)); + } + + public function testRequestResumeAfterCloseWillNotBeForwarded() + { + $server = new Server($this->socket); + $server->on('request', function (Request $request) { + $request->close(); + $request->resume(); + }); + + $this->connection->expects($this->once())->method('pause'); + $this->connection->expects($this->never())->method('resume'); + $this->socket->emit('connection', array($this->connection)); + + $data = $this->createGetRequest(); + $this->connection->emit('data', array($data)); + } + public function testRequestEventWithoutBodyWillNotEmitData() { $never = $this->expectCallableNever(); @@ -391,7 +422,7 @@ public function testChunkedEncodedRequestWillBeParsedForRequestEvent() $dataEvent = $this->expectCallableOnceWith('hello'); $endEvent = $this->expectCallableOnce(); - $closeEvent = $this->expectCallableNever(); + $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); $server->on('request', function (Request $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { @@ -420,7 +451,7 @@ public function testChunkedEncodedRequestAdditionalDataWontBeEmitted() $dataEvent = $this->expectCallableOnceWith('hello'); $endEvent = $this->expectCallableOnce(); - $closeEvent = $this->expectCallableNever(); + $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); $server->on('request', function (Request $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { @@ -450,7 +481,7 @@ public function testEmptyChunkedEncodedRequest() $dataEvent = $this->expectCallableNever(); $endEvent = $this->expectCallableOnce(); - $closeEvent = $this->expectCallableNever(); + $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); $server->on('request', function (Request $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { @@ -478,7 +509,7 @@ public function testChunkedIsUpperCase() $dataEvent = $this->expectCallableOnceWith('hello'); $endEvent = $this->expectCallableOnce(); - $closeEvent = $this->expectCallableNever(); + $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); $server->on('request', function (Request $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { @@ -507,7 +538,7 @@ public function testChunkedIsMixedUpperAndLowerCase() $dataEvent = $this->expectCallableOnceWith('hello'); $endEvent = $this->expectCallableOnce(); - $closeEvent = $this->expectCallableNever(); + $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); $server->on('request', function (Request $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { From c9e9a0b9b8d9f5e62be92e9517f6375c096a66fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 17 Feb 2017 00:53:44 +0100 Subject: [PATCH 077/456] Send HTTP status code 431 if request header is too large --- src/Server.php | 2 +- tests/ServerTest.php | 18 +++++++++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/Server.php b/src/Server.php index 267eea17..5fc182d2 100644 --- a/src/Server.php +++ b/src/Server.php @@ -105,7 +105,7 @@ public function handleConnection(ConnectionInterface $conn) $that->writeError( $conn, - 400 + ($e instanceof \OverflowException) ? 431 : 400 ); }); } diff --git a/tests/ServerTest.php b/tests/ServerTest.php index bfb5c01a..83330108 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -363,7 +363,7 @@ public function testServerWithNoRequestListenerDoesNotSendAnythingToConnection() $this->connection->emit('data', array($data)); } - public function testParserErrorEmitted() + public function testRequestOverflowWillEmitErrorAndSendErrorResponse() { $error = null; $server = new Server($this->socket); @@ -371,6 +371,19 @@ public function testParserErrorEmitted() $error = $message; }); + $buffer = ''; + + $this->connection + ->expects($this->any()) + ->method('write') + ->will( + $this->returnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } + ) + ); + $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\nX-DATA: "; @@ -378,6 +391,9 @@ public function testParserErrorEmitted() $this->connection->emit('data', array($data)); $this->assertInstanceOf('OverflowException', $error); + + $this->assertContains("HTTP/1.1 431 Request Header Fields Too Large\r\n", $buffer); + $this->assertContains("\r\n\r\nError 431: Request Header Fields Too Large", $buffer); } public function testRequestInvalidWillEmitErrorAndSendErrorResponse() From 3c08836c7ca1c3bbddb16e3e6b51b54a9b8fea61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 20 Feb 2017 15:14:52 +0100 Subject: [PATCH 078/456] Response closes after forwarding close event --- README.md | 8 +- src/Response.php | 41 +++++-- tests/ResponseTest.php | 264 ++++++++++++++++++++++++++++++++++++----- tests/ServerTest.php | 17 +++ 4 files changed, 285 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index a703c046..39f3b901 100644 --- a/README.md +++ b/README.md @@ -231,7 +231,9 @@ This method MUST NOT be invoked after calling [`writeHead()`](#writehead). This method MUST NOT be invoked if this is not a HTTP/1.1 response (please check [`expectsContinue()`](#expectscontinue) as above). Calling this method after sending the headers or if this is not a HTTP/1.1 -response is an error that will result in an `Exception`. +response is an error that will result in an `Exception` +(unless the response has ended/closed already). +Calling this method after the response has ended/closed is a NOOP. #### writeHead() @@ -248,7 +250,9 @@ $response->writeHead(200, array( $response->end('Hello World!'); ``` -Calling this method more than once will result in an `Exception`. +Calling this method more than once will result in an `Exception` +(unless the response has ended/closed already). +Calling this method after the response has ended/closed is a NOOP. Unless you specify a `Content-Length` header yourself, HTTP/1.1 responses will automatically use chunked transfer encoding and send the respective header diff --git a/src/Response.php b/src/Response.php index 9483523d..3283d2a7 100644 --- a/src/Response.php +++ b/src/Response.php @@ -3,7 +3,6 @@ namespace React\Http; use Evenement\EventEmitter; -use React\Socket\ConnectionInterface; use React\Stream\WritableStreamInterface; /** @@ -45,19 +44,16 @@ class Response extends EventEmitter implements WritableStreamInterface * * @internal */ - public function __construct(ConnectionInterface $conn, $protocolVersion = '1.1') + public function __construct(WritableStreamInterface $conn, $protocolVersion = '1.1') { $this->conn = $conn; $this->protocolVersion = $protocolVersion; $that = $this; - $this->conn->on('end', function () use ($that) { - $that->close(); - }); + $this->conn->on('close', array($this, 'close')); $this->conn->on('error', function ($error) use ($that) { - $that->emit('error', array($error, $that)); - $that->close(); + $that->emit('error', array($error)); }); $this->conn->on('drain', function () use ($that) { @@ -106,7 +102,9 @@ public function isWritable() * This method MUST NOT be invoked if this is not a HTTP/1.1 response * (please check [`expectsContinue()`] as above). * Calling this method after sending the headers or if this is not a HTTP/1.1 - * response is an error that will result in an `Exception`. + * response is an error that will result in an `Exception` + * (unless the response has ended/closed already). + * Calling this method after the response has ended/closed is a NOOP. * * @return void * @throws \Exception @@ -114,6 +112,9 @@ public function isWritable() */ public function writeContinue() { + if (!$this->writable) { + return; + } if ($this->protocolVersion !== '1.1') { throw new \Exception('Continue requires a HTTP/1.1 message'); } @@ -137,7 +138,9 @@ public function writeContinue() * $response->end('Hello World!'); * ``` * - * Calling this method more than once will result in an `Exception`. + * Calling this method more than once will result in an `Exception` + * (unless the response has ended/closed already). + * Calling this method after the response has ended/closed is a NOOP. * * Unless you specify a `Content-Length` header yourself, HTTP/1.1 responses * will automatically use chunked transfer encoding and send the respective header @@ -184,6 +187,9 @@ public function writeContinue() */ public function writeHead($status = 200, array $headers = array()) { + if (!$this->writable) { + return; + } if ($this->headWritten) { throw new \Exception('Response head has already been written.'); } @@ -250,6 +256,9 @@ private function formatHead($status, array $headers) public function write($data) { + if (!$this->writable) { + return false; + } if (!$this->headWritten) { throw new \Exception('Response head has not yet been written.'); } @@ -271,6 +280,13 @@ public function write($data) public function end($data = null) { + if (!$this->writable) { + return; + } + if (!$this->headWritten) { + throw new \Exception('Response head has not yet been written.'); + } + if (null !== $data) { $this->write($data); } @@ -279,8 +295,7 @@ public function end($data = null) $this->conn->write("0\r\n\r\n"); } - $this->emit('end'); - $this->removeAllListeners(); + $this->writable = false; $this->conn->end(); } @@ -291,10 +306,10 @@ public function close() } $this->closed = true; - $this->writable = false; + $this->conn->close(); + $this->emit('close'); $this->removeAllListeners(); - $this->conn->close(); } } diff --git a/tests/ResponseTest.php b/tests/ResponseTest.php index 8c3e9077..841bcdd9 100644 --- a/tests/ResponseTest.php +++ b/tests/ResponseTest.php @@ -3,6 +3,7 @@ namespace React\Tests\Http; use React\Http\Response; +use React\Stream\WritableStream; class ResponseTest extends TestCase { @@ -171,6 +172,106 @@ public function testResponseShouldAlwaysIncludeConnectionCloseIrrespectiveOfExpl $response->writeHead(200, array('Content-Length' => 0, 'connection' => 'ignored')); } + /** @expectedException Exception */ + public function testWriteHeadTwiceShouldThrowException() + { + $conn = $this + ->getMockBuilder('React\Socket\ConnectionInterface') + ->getMock(); + $conn + ->expects($this->once()) + ->method('write'); + + $response = new Response($conn); + $response->writeHead(); + $response->writeHead(); + } + + public function testEndWithoutDataWritesEndChunkAndEndsInput() + { + $conn = $this + ->getMockBuilder('React\Socket\ConnectionInterface') + ->getMock(); + $conn + ->expects($this->at(4)) + ->method('write') + ->with("0\r\n\r\n"); + $conn + ->expects($this->once()) + ->method('end'); + + $response = new Response($conn); + $response->writeHead(); + $response->end(); + } + + public function testEndWithDataWritesToInputAndEndsInputWithoutData() + { + $conn = $this + ->getMockBuilder('React\Socket\ConnectionInterface') + ->getMock(); + $conn + ->expects($this->at(4)) + ->method('write') + ->with("3\r\nbye\r\n"); + $conn + ->expects($this->at(5)) + ->method('write') + ->with("0\r\n\r\n"); + $conn + ->expects($this->once()) + ->method('end'); + + $response = new Response($conn); + $response->writeHead(); + $response->end('bye'); + } + + public function testEndWithoutDataWithoutChunkedEncodingWritesNoDataAndEndsInput() + { + $conn = $this + ->getMockBuilder('React\Socket\ConnectionInterface') + ->getMock(); + $conn + ->expects($this->once()) + ->method('write'); + $conn + ->expects($this->once()) + ->method('end'); + + $response = new Response($conn); + $response->writeHead(200, array('Content-Length' => 0)); + $response->end(); + } + + /** @expectedException Exception */ + public function testEndWithoutHeadShouldThrowException() + { + $conn = $this + ->getMockBuilder('React\Socket\ConnectionInterface') + ->getMock(); + $conn + ->expects($this->never()) + ->method('end'); + + $response = new Response($conn); + $response->end(); + } + + /** @expectedException Exception */ + public function testWriteWithoutHeadShouldThrowException() + { + $conn = $this + ->getMockBuilder('React\Socket\ConnectionInterface') + ->getMock(); + $conn + ->expects($this->never()) + ->method('write'); + + $response = new Response($conn); + $response->write('test'); + } + public function testResponseBodyShouldBeChunkedCorrectly() { $conn = $this @@ -229,23 +330,6 @@ public function testResponseBodyShouldSkipEmptyChunks() $response->end(); } - public function testResponseShouldEmitEndOnStreamEnd() - { - $ended = false; - - $conn = $this - ->getMockBuilder('React\Socket\ConnectionInterface') - ->getMock(); - $response = new Response($conn); - - $response->on('end', function () use (&$ended) { - $ended = true; - }); - $response->end(); - - $this->assertTrue($ended); - } - /** @test */ public function writeContinueShouldSendContinueLineBeforeRealHeaders() { @@ -280,26 +364,19 @@ public function writeContinueShouldThrowForHttp10() $response->writeContinue(); } - /** @test */ - public function shouldForwardEndDrainAndErrorEvents() + /** @expectedException Exception */ + public function testWriteContinueAfterWriteHeadShouldThrowException() { $conn = $this ->getMockBuilder('React\Socket\ConnectionInterface') ->getMock(); $conn - ->expects($this->at(0)) - ->method('on') - ->with('end', $this->isInstanceOf('Closure')); - $conn - ->expects($this->at(1)) - ->method('on') - ->with('error', $this->isInstanceOf('Closure')); - $conn - ->expects($this->at(2)) - ->method('on') - ->with('drain', $this->isInstanceOf('Closure')); + ->expects($this->once()) + ->method('write'); $response = new Response($conn); + $response->writeHead(); + $response->writeContinue(); } /** @test */ @@ -392,4 +469,131 @@ public function shouldIgnoreHeadersWithNullValues() $response = new Response($conn); $response->writeHead(200, array("FooBar" => null)); } + + public function testCloseClosesInputAndEmitsCloseEvent() + { + $input = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + $input->expects($this->once())->method('close'); + + $response = new Response($input); + + $response->on('close', $this->expectCallableOnce()); + + $response->close(); + } + + public function testClosingInputEmitsCloseEvent() + { + $input = new WritableStream(); + $response = new Response($input); + + $response->on('close', $this->expectCallableOnce()); + + $input->close(); + } + + public function testCloseMultipleTimesEmitsCloseEventOnce() + { + $input = new WritableStream(); + $response = new Response($input); + + $response->on('close', $this->expectCallableOnce()); + + $response->close(); + $response->close(); + } + + public function testIsNotWritableAfterClose() + { + $input = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + + $response = new Response($input); + + $response->close(); + + $this->assertFalse($response->isWritable()); + } + + public function testCloseAfterEndIsPassedThrough() + { + $input = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + $input->expects($this->once())->method('end'); + $input->expects($this->once())->method('close'); + + $response = new Response($input); + + $response->writeHead(); + $response->end(); + $response->close(); + } + + public function testWriteAfterCloseIsNoOp() + { + $input = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + $input->expects($this->once())->method('close'); + $input->expects($this->never())->method('write'); + + $response = new Response($input); + $response->close(); + + $this->assertFalse($response->write('noop')); + } + + public function testWriteHeadAfterCloseIsNoOp() + { + $input = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + $input->expects($this->once())->method('close'); + $input->expects($this->never())->method('write'); + + $response = new Response($input); + $response->close(); + + $response->writeHead(); + } + + public function testWriteContinueAfterCloseIsNoOp() + { + $input = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + $input->expects($this->once())->method('close'); + $input->expects($this->never())->method('write'); + + $response = new Response($input); + $response->close(); + + $response->writeContinue(); + } + + public function testEndAfterCloseIsNoOp() + { + $input = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + $input->expects($this->once())->method('close'); + $input->expects($this->never())->method('write'); + $input->expects($this->never())->method('end'); + + $response = new Response($input); + $response->close(); + + $response->end('noop'); + } + + public function testErrorEventShouldBeForwardedWithoutClosing() + { + $input = new WritableStream(); + $response = new Response($input); + + $response->on('error', $this->expectCallableOnce()); + $response->on('close', $this->expectCallableNever()); + + $input->emit('error', array(new \RuntimeException())); + } + + public function testDrainEventShouldBeForwarded() + { + $input = new WritableStream(); + $response = new Response($input); + + $response->on('drain', $this->expectCallableOnce()); + + $input->emit('drain'); + } } diff --git a/tests/ServerTest.php b/tests/ServerTest.php index bfb5c01a..f3498fe0 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -249,6 +249,23 @@ function ($data) use (&$buffer) { $this->assertContains("\r\nX-Powered-By: React/alpha\r\n", $buffer); } + public function testClosingResponseDoesNotSendAnyData() + { + $server = new Server($this->socket); + $server->on('request', function (Request $request, Response $response) { + $response->close(); + }); + + $this->connection->expects($this->never())->method('write'); + $this->connection->expects($this->never())->method('end'); + $this->connection->expects($this->once())->method('close'); + + $this->socket->emit('connection', array($this->connection)); + + $data = $this->createGetRequest(); + $this->connection->emit('data', array($data)); + } + public function testResponseContainsSameRequestProtocolVersionAndChunkedBodyForHttp11() { $server = new Server($this->socket); From 26b5505fb6f11ab150619115a7d9a62417b48634 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 21 Feb 2017 10:07:27 +0100 Subject: [PATCH 079/456] RequestHeaderParser returns PSR-7 request --- src/RequestHeaderParser.php | 16 +--------------- src/Server.php | 32 ++++++++++++++++++++++--------- tests/RequestHeaderParserTest.php | 10 ++++------ tests/ServerTest.php | 1 + 4 files changed, 29 insertions(+), 30 deletions(-) diff --git a/src/RequestHeaderParser.php b/src/RequestHeaderParser.php index b75fadbf..d9feda1a 100644 --- a/src/RequestHeaderParser.php +++ b/src/RequestHeaderParser.php @@ -55,21 +55,7 @@ private function parseRequest($data) { list($headers, $bodyBuffer) = explode("\r\n\r\n", $data, 2); - $psrRequest = g7\parse_request($headers); - - $parsedQuery = array(); - $queryString = $psrRequest->getUri()->getQuery(); - if ($queryString) { - parse_str($queryString, $parsedQuery); - } - - $request = new Request( - $psrRequest->getMethod(), - $psrRequest->getUri()->getPath(), - $parsedQuery, - $psrRequest->getProtocolVersion(), - $psrRequest->getHeaders() - ); + $request = g7\parse_request($headers); return array($request, $bodyBuffer); } diff --git a/src/Server.php b/src/Server.php index 5fc182d2..67d46888 100644 --- a/src/Server.php +++ b/src/Server.php @@ -5,6 +5,7 @@ use Evenement\EventEmitter; use React\Socket\ServerInterface as SocketServerInterface; use React\Socket\ConnectionInterface; +use Psr\Http\Message\RequestInterface; /** * The `Server` class is responsible for handling incoming connections and then @@ -87,7 +88,7 @@ public function handleConnection(ConnectionInterface $conn) $that = $this; $parser = new RequestHeaderParser(); $listener = array($parser, 'feed'); - $parser->on('headers', function (Request $request, $bodyBuffer) use ($conn, $listener, $parser, $that) { + $parser->on('headers', function (RequestInterface $request, $bodyBuffer) use ($conn, $listener, $parser, $that) { // parsing request completed => stop feeding parser $conn->removeListener('data', $listener); @@ -111,7 +112,7 @@ public function handleConnection(ConnectionInterface $conn) } /** @internal */ - public function handleRequest(ConnectionInterface $conn, Request $request) + public function handleRequest(ConnectionInterface $conn, RequestInterface $request) { // only support HTTP/1.1 and HTTP/1.0 requests if ($request->getProtocolVersion() !== '1.1' && $request->getProtocolVersion() !== '1.0') { @@ -138,13 +139,6 @@ public function handleRequest(ConnectionInterface $conn, Request $request) } $response = new Response($conn, $request->getProtocolVersion()); - $response->on('close', array($request, 'close')); - - // attach remote ip to the request as metadata - $request->remoteAddress = trim( - parse_url('tcp://' . $conn->getRemoteAddress(), PHP_URL_HOST), - '[]' - ); $stream = $conn; if ($request->hasHeader('Transfer-Encoding')) { @@ -155,6 +149,26 @@ public function handleRequest(ConnectionInterface $conn, Request $request) } } + $parsedQuery = array(); + $queryString = $request->getUri()->getQuery(); + if ($queryString) { + parse_str($queryString, $parsedQuery); + } + + $request = new Request( + $request->getMethod(), + $request->getUri()->getPath(), + $parsedQuery, + $request->getProtocolVersion(), + $request->getHeaders() + ); + + // attach remote ip to the request as metadata + $request->remoteAddress = trim( + parse_url('tcp://' . $conn->getRemoteAddress(), PHP_URL_HOST), + '[]' + ); + // forward pause/resume calls to underlying connection $request->on('pause', array($conn, 'pause')); $request->on('resume', array($conn, 'resume')); diff --git a/tests/RequestHeaderParserTest.php b/tests/RequestHeaderParserTest.php index 1193e220..dfc25f4f 100644 --- a/tests/RequestHeaderParserTest.php +++ b/tests/RequestHeaderParserTest.php @@ -45,10 +45,9 @@ public function testHeadersEventShouldReturnRequestAndBodyBuffer() $data .= 'RANDOM DATA'; $parser->feed($data); - $this->assertInstanceOf('React\Http\Request', $request); + $this->assertInstanceOf('Psr\Http\Message\RequestInterface', $request); $this->assertSame('GET', $request->getMethod()); - $this->assertSame('/', $request->getPath()); - $this->assertSame(array(), $request->getQueryParams()); + $this->assertEquals('http://example.com/', $request->getUri()); $this->assertSame('1.1', $request->getProtocolVersion()); $this->assertSame(array('Host' => array('example.com:80'), 'Connection' => array('close')), $request->getHeaders()); @@ -83,10 +82,9 @@ public function testHeadersEventShouldParsePathAndQueryString() $data = $this->createAdvancedPostRequest(); $parser->feed($data); - $this->assertInstanceOf('React\Http\Request', $request); + $this->assertInstanceOf('Psr\Http\Message\RequestInterface', $request); $this->assertSame('POST', $request->getMethod()); - $this->assertSame('/foo', $request->getPath()); - $this->assertSame(array('bar' => 'baz'), $request->getQueryParams()); + $this->assertEquals('http://example.com/foo?bar=baz', $request->getUri()); $this->assertSame('1.1', $request->getProtocolVersion()); $headers = array( 'Host' => array('example.com:80'), diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 83330108..b66faeea 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -83,6 +83,7 @@ public function testRequestEvent() $this->assertSame(1, $i); $this->assertInstanceOf('React\Http\Request', $requestAssertion); $this->assertSame('/', $requestAssertion->getPath()); + $this->assertSame(array(), $requestAssertion->getQueryParams()); $this->assertSame('GET', $requestAssertion->getMethod()); $this->assertSame('127.0.0.1', $requestAssertion->remoteAddress); From e85d5295d8490c34ec63e364ccd0660dd5d8fdd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 21 Feb 2017 10:54:08 +0100 Subject: [PATCH 080/456] Inject PSR-7 request instance into Request class --- src/Request.php | 47 +++++++++++++++---------------------------- src/Server.php | 14 +------------ tests/RequestTest.php | 27 +++++++++++++------------ 3 files changed, 31 insertions(+), 57 deletions(-) diff --git a/src/Request.php b/src/Request.php index 46bc3639..58d7b214 100644 --- a/src/Request.php +++ b/src/Request.php @@ -6,6 +6,7 @@ use React\Stream\ReadableStreamInterface; use React\Stream\WritableStreamInterface; use React\Stream\Util; +use Psr\Http\Message\RequestInterface; /** * The `Request` class is responsible for streaming the incoming request body @@ -24,11 +25,7 @@ class Request extends EventEmitter implements ReadableStreamInterface { private $readable = true; - private $method; - private $path; - private $query; - private $httpVersion; - private $headers; + private $request; // metadata, implicitly added externally public $remoteAddress; @@ -42,13 +39,9 @@ class Request extends EventEmitter implements ReadableStreamInterface * * @internal */ - public function __construct($method, $path, $query = array(), $httpVersion = '1.1', $headers = array()) + public function __construct(RequestInterface $request) { - $this->method = $method; - $this->path = $path; - $this->query = $query; - $this->httpVersion = $httpVersion; - $this->headers = $headers; + $this->request = $request; } /** @@ -58,7 +51,7 @@ public function __construct($method, $path, $query = array(), $httpVersion = '1. */ public function getMethod() { - return $this->method; + return $this->request->getMethod(); } /** @@ -68,7 +61,7 @@ public function getMethod() */ public function getPath() { - return $this->path; + return $this->request->getUri()->getPath(); } /** @@ -78,7 +71,10 @@ public function getPath() */ public function getQueryParams() { - return $this->query; + $params = array(); + parse_str($this->request->getUri()->getQuery(), $params); + + return $params; } /** @@ -88,7 +84,7 @@ public function getQueryParams() */ public function getProtocolVersion() { - return $this->httpVersion; + return $this->request->getProtocolVersion(); } /** @@ -102,7 +98,7 @@ public function getProtocolVersion() */ public function getHeaders() { - return $this->headers; + return $this->request->getHeaders(); } /** @@ -113,18 +109,7 @@ public function getHeaders() */ public function getHeader($name) { - $found = array(); - - $name = strtolower($name); - foreach ($this->headers as $key => $value) { - if (strtolower($key) === $name) { - foreach((array)$value as $one) { - $found[] = $one; - } - } - } - - return $found; + return $this->request->getHeader($name); } /** @@ -135,7 +120,7 @@ public function getHeader($name) */ public function getHeaderLine($name) { - return implode(', ', $this->getHeader($name)); + return $this->request->getHeaderLine($name); } /** @@ -146,7 +131,7 @@ public function getHeaderLine($name) */ public function hasHeader($name) { - return !!$this->getHeader($name); + return $this->request->hasHeader($name); } /** @@ -164,7 +149,7 @@ public function hasHeader($name) */ public function expectsContinue() { - return $this->httpVersion !== '1.0' && '100-continue' === strtolower($this->getHeaderLine('Expect')); + return $this->getProtocolVersion() !== '1.0' && '100-continue' === strtolower($this->getHeaderLine('Expect')); } public function isReadable() diff --git a/src/Server.php b/src/Server.php index 67d46888..35fef695 100644 --- a/src/Server.php +++ b/src/Server.php @@ -149,19 +149,7 @@ public function handleRequest(ConnectionInterface $conn, RequestInterface $reque } } - $parsedQuery = array(); - $queryString = $request->getUri()->getQuery(); - if ($queryString) { - parse_str($queryString, $parsedQuery); - } - - $request = new Request( - $request->getMethod(), - $request->getUri()->getPath(), - $parsedQuery, - $request->getProtocolVersion(), - $request->getHeaders() - ); + $request = new Request($request); // attach remote ip to the request as metadata $request->remoteAddress = trim( diff --git a/tests/RequestTest.php b/tests/RequestTest.php index 940a6a68..9ff13bed 100644 --- a/tests/RequestTest.php +++ b/tests/RequestTest.php @@ -3,6 +3,7 @@ namespace React\Tests\Http; use React\Http\Request; +use RingCentral\Psr7\Request as Psr; class RequestTest extends TestCase { @@ -10,7 +11,7 @@ class RequestTest extends TestCase public function expectsContinueShouldBeFalseByDefault() { $headers = array(); - $request = new Request('GET', '/', array(), '1.1', $headers); + $request = new Request(new Psr('GET', '/', $headers, null, '1.1')); $this->assertFalse($request->expectsContinue()); } @@ -19,7 +20,7 @@ public function expectsContinueShouldBeFalseByDefault() public function expectsContinueShouldBeTrueIfContinueExpected() { $headers = array('Expect' => array('100-continue')); - $request = new Request('GET', '/', array(), '1.1', $headers); + $request = new Request(new Psr('GET', '/', $headers, null, '1.1')); $this->assertTrue($request->expectsContinue()); } @@ -28,7 +29,7 @@ public function expectsContinueShouldBeTrueIfContinueExpected() public function expectsContinueShouldBeTrueIfContinueExpectedCaseInsensitive() { $headers = array('EXPECT' => array('100-CONTINUE')); - $request = new Request('GET', '/', array(), '1.1', $headers); + $request = new Request(new Psr('GET', '/', $headers, null, '1.1')); $this->assertTrue($request->expectsContinue()); } @@ -37,14 +38,14 @@ public function expectsContinueShouldBeTrueIfContinueExpectedCaseInsensitive() public function expectsContinueShouldBeFalseForHttp10() { $headers = array('Expect' => array('100-continue')); - $request = new Request('GET', '/', array(), '1.0', $headers); + $request = new Request(new Psr('GET', '/', $headers, null, '1.0')); $this->assertFalse($request->expectsContinue()); } public function testEmptyHeader() { - $request = new Request('GET', '/'); + $request = new Request(new Psr('GET', '/', array())); $this->assertEquals(array(), $request->getHeaders()); $this->assertFalse($request->hasHeader('Test')); @@ -54,9 +55,9 @@ public function testEmptyHeader() public function testHeaderIsCaseInsensitive() { - $request = new Request('GET', '/', array(), '1.1', array( + $request = new Request(new Psr('GET', '/', array( 'TEST' => array('Yes'), - )); + ))); $this->assertEquals(array('TEST' => array('Yes')), $request->getHeaders()); $this->assertTrue($request->hasHeader('Test')); @@ -66,9 +67,9 @@ public function testHeaderIsCaseInsensitive() public function testHeaderWithMultipleValues() { - $request = new Request('GET', '/', array(), '1.1', array( + $request = new Request(new Psr('GET', '/', array( 'Test' => array('a', 'b'), - )); + ))); $this->assertEquals(array('Test' => array('a', 'b')), $request->getHeaders()); $this->assertTrue($request->hasHeader('Test')); @@ -78,7 +79,7 @@ public function testHeaderWithMultipleValues() public function testCloseEmitsCloseEvent() { - $request = new Request('GET', '/'); + $request = new Request(new Psr('GET', '/')); $request->on('close', $this->expectCallableOnce()); @@ -87,7 +88,7 @@ public function testCloseEmitsCloseEvent() public function testCloseMultipleTimesEmitsCloseEventOnce() { - $request = new Request('GET', '/'); + $request = new Request(new Psr('GET', '/')); $request->on('close', $this->expectCallableOnce()); @@ -97,7 +98,7 @@ public function testCloseMultipleTimesEmitsCloseEventOnce() public function testIsNotReadableAfterClose() { - $request = new Request('GET', '/'); + $request = new Request(new Psr('GET', '/')); $request->close(); @@ -108,7 +109,7 @@ public function testPipeReturnsDest() { $dest = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); - $request = new Request('GET', '/'); + $request = new Request(new Psr('GET', '/')); $ret = $request->pipe($dest); From e873b2c435077e3d5ec59482b48d788ce291744a Mon Sep 17 00:00:00 2001 From: Niels Theen Date: Wed, 15 Feb 2017 13:09:05 +0100 Subject: [PATCH 081/456] Add LenghtLimitedStream --- src/LengthLimitedStream.php | 103 ++++++++++++++++++++++++++++ tests/LengthLimitedStreamTest.php | 108 ++++++++++++++++++++++++++++++ 2 files changed, 211 insertions(+) create mode 100644 src/LengthLimitedStream.php create mode 100644 tests/LengthLimitedStreamTest.php diff --git a/src/LengthLimitedStream.php b/src/LengthLimitedStream.php new file mode 100644 index 00000000..6a2d4033 --- /dev/null +++ b/src/LengthLimitedStream.php @@ -0,0 +1,103 @@ +stream = $stream; + $this->maxLength = $maxLength; + + $this->stream->on('data', array($this, 'handleData')); + $this->stream->on('end', array($this, 'handleEnd')); + $this->stream->on('error', array($this, 'handleError')); + $this->stream->on('close', array($this, 'close')); + } + + public function isReadable() + { + return !$this->closed && $this->stream->isReadable(); + } + + public function pause() + { + $this->stream->pause(); + } + + public function resume() + { + $this->stream->resume(); + } + + 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->stream->close(); + + $this->emit('close'); + $this->removeAllListeners(); + } + + /** @internal */ + public function handleData($data) + { + if (($this->transferredLength + strlen($data)) > $this->maxLength) { + // Only emit data until the value of 'Content-Length' is reached, the rest will be ignored + $data = (string)substr($data, 0, $this->maxLength - $this->transferredLength); + } + + if ($data !== '') { + $this->transferredLength += strlen($data); + $this->emit('data', array($data)); + } + + if ($this->transferredLength === $this->maxLength) { + // 'Content-Length' reached, stream will end + $this->emit('end'); + $this->close(); + $this->stream->removeListener('data', array($this, 'handleData')); + } + } + + /** @internal */ + public function handleError(\Exception $e) + { + $this->emit('error', array($e)); + $this->close(); + } + + /** @internal */ + public function handleEnd() + { + if (!$this->closed) { + $this->emit('end'); + $this->close(); + } + } + +} diff --git a/tests/LengthLimitedStreamTest.php b/tests/LengthLimitedStreamTest.php new file mode 100644 index 00000000..5ba7be0d --- /dev/null +++ b/tests/LengthLimitedStreamTest.php @@ -0,0 +1,108 @@ +input = new ReadableStream(); + } + + public function testSimpleChunk() + { + $stream = new LengthLimitedStream($this->input, 5); + $stream->on('data', $this->expectCallableOnceWith('hello')); + $stream->on('end', $this->expectCallableOnce()); + $this->input->emit('data', array("hello world")); + } + + public function testInputStreamKeepsEmitting() + { + $stream = new LengthLimitedStream($this->input, 5); + $stream->on('data', $this->expectCallableOnceWith('hello')); + $stream->on('end', $this->expectCallableOnce()); + + $this->input->emit('data', array("hello world")); + $this->input->emit('data', array("world")); + $this->input->emit('data', array("world")); + } + + public function testZeroLengthInContentLengthWillIgnoreEmittedDataEvents() + { + $stream = new LengthLimitedStream($this->input, 0); + $stream->on('data', $this->expectCallableNever()); + $stream->on('end', $this->expectCallableOnce()); + $this->input->emit('data', array("hello world")); + } + + public function testHandleError() + { + $stream = new LengthLimitedStream($this->input, 0); + $stream->on('error', $this->expectCallableOnce()); + $stream->on('close', $this->expectCallableOnce()); + + $this->input->emit('error', array(new \RuntimeException())); + + $this->assertFalse($stream->isReadable()); + } + + public function testPauseStream() + { + $input = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $input->expects($this->once())->method('pause'); + + $stream = new LengthLimitedStream($input, 0); + $stream->pause(); + } + + public function testResumeStream() + { + $input = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $input->expects($this->once())->method('pause'); + + $stream = new LengthLimitedStream($input, 0); + $stream->pause(); + $stream->resume(); + } + + public function testPipeStream() + { + $stream = new LengthLimitedStream($this->input, 0); + $dest = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + + $ret = $stream->pipe($dest); + + $this->assertSame($dest, $ret); + } + + public function testHandleClose() + { + $stream = new LengthLimitedStream($this->input, 0); + $stream->on('close', $this->expectCallableOnce()); + + $this->input->close(); + $this->input->emit('end', array()); + + $this->assertFalse($stream->isReadable()); + } + + public function testOutputStreamCanCloseInputStream() + { + $input = new ReadableStream(); + $input->on('close', $this->expectCallableOnce()); + + $stream = new LengthLimitedStream($input, 0); + $stream->on('close', $this->expectCallableOnce()); + + $stream->close(); + + $this->assertFalse($input->isReadable()); + } +} From 60b7dadf4651bc7bc4cee4fa9061df860c8ed525 Mon Sep 17 00:00:00 2001 From: Niels Theen Date: Wed, 15 Feb 2017 13:10:37 +0100 Subject: [PATCH 082/456] Handle Content-Length requests --- src/Server.php | 19 ++++++ tests/ServerTest.php | 151 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 168 insertions(+), 2 deletions(-) diff --git a/src/Server.php b/src/Server.php index 5fc182d2..d185276d 100644 --- a/src/Server.php +++ b/src/Server.php @@ -155,6 +155,19 @@ public function handleRequest(ConnectionInterface $conn, Request $request) } } + if ($request->hasHeader('Content-Length')) { + $string = $request->getHeaderLine('Content-Length'); + + $contentLength = (int)$string; + if ((string)$contentLength !== (string)$string) { + // Content-Length value is not an integer or not a single integer + $this->emit('error', new \Exception('The value of `Content-Length` is not valid')); + return; + } + + $stream = new LengthLimitedStream($conn, $contentLength); + } + // forward pause/resume calls to underlying connection $request->on('pause', array($conn, 'pause')); $request->on('resume', array($conn, 'resume')); @@ -173,6 +186,12 @@ public function handleRequest(ConnectionInterface $conn, Request $request) }); $this->emit('request', array($request, $response)); + + if ($stream instanceof LengthLimitedStream && $contentLength === 0) { + // stream must emit an 'end' here, because empty data won't be emitted + $stream->emit('end'); + $conn->removeListener('data', array($stream, 'handleData')); + } } /** @internal */ diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 83330108..a1120b17 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -433,8 +433,8 @@ public function testBodyDataWillBeSendViaRequestEvent() $server = new Server($this->socket); $dataEvent = $this->expectCallableOnceWith('hello'); - $endEvent = $this->expectCallableNever(); - $closeEvent = $this->expectCallableNever(); + $endEvent = $this->expectCallableOnce(); + $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); $server->on('request', function (Request $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { @@ -708,6 +708,153 @@ public function testRequestHttp10WithoutHostEmitsRequestWithNoError() $this->connection->emit('data', array($data)); } + public function testWontEmitFurtherDataWhenContentLengthIsReached() + { + $server = new Server($this->socket); + + $dataEvent = $this->expectCallableOnceWith('hello'); + $endEvent = $this->expectCallableOnce(); + $closeEvent = $this->expectCallableOnce(); + $errorEvent = $this->expectCallableNever(); + + $server->on('request', function (Request $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $request->on('data', $dataEvent); + $request->on('end', $endEvent); + $request->on('close', $closeEvent); + $request->on('error', $errorEvent); + }); + + $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 .= "\r\n"; + $data .= "hello"; + $data .= "world"; + + $this->connection->emit('data', array($data)); + } + + public function testWontEmitFurtherDataWhenContentLengthIsReachedSplitted() + { + $server = new Server($this->socket); + + $dataEvent = $this->expectCallableOnceWith('hello'); + $endEvent = $this->expectCallableOnce(); + $closeEvent = $this->expectCallableOnce(); + $errorEvent = $this->expectCallableNever(); + + $server->on('request', function (Request $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $request->on('data', $dataEvent); + $request->on('end', $endEvent); + $request->on('close', $closeEvent); + $request->on('error', $errorEvent); + }); + + $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 .= "\r\n"; + $data .= "hello"; + + $this->connection->emit('data', array($data)); + + $data = "world"; + + $this->connection->emit('data', array($data)); + } + + public function testContentLengthContainsZeroWillEmitEndEvent() + { + $server = new Server($this->socket); + + $dataEvent = $this->expectCallableNever(); + $endEvent = $this->expectCallableOnce(); + $closeEvent = $this->expectCallableNever(); + $errorEvent = $this->expectCallableNever(); + + $server->on('request', function (Request $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $request->on('data', $dataEvent); + $request->on('end', $endEvent); + $request->on('close', $closeEvent); + $request->on('error', $errorEvent); + }); + + $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: 0\r\n"; + $data .= "\r\n"; + + $this->connection->emit('data', array($data)); + } + + public function testContentLengthContainsZeroWillEmitEndEventAdditionalDataWillBeIgnored() + { + $server = new Server($this->socket); + + $dataEvent = $this->expectCallableNever(); + $endEvent = $this->expectCallableOnce(); + $closeEvent = $this->expectCallableNever(); + $errorEvent = $this->expectCallableNever(); + + $server->on('request', function (Request $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $request->on('data', $dataEvent); + $request->on('end', $endEvent); + $request->on('close', $closeEvent); + $request->on('error', $errorEvent); + }); + + $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: 0\r\n"; + $data .= "\r\n"; + $data .= "hello"; + + $this->connection->emit('data', array($data)); + } + + public function testContentLengthContainsZeroWillEmitEndEventAdditionalDataWillBeIgnoredSplitted() + { + $server = new Server($this->socket); + + $dataEvent = $this->expectCallableNever(); + $endEvent = $this->expectCallableOnce(); + $closeEvent = $this->expectCallableNever(); + $errorEvent = $this->expectCallableNever(); + + $server->on('request', function (Request $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $request->on('data', $dataEvent); + $request->on('end', $endEvent); + $request->on('close', $closeEvent); + $request->on('error', $errorEvent); + }); + + $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: 0\r\n"; + $data .= "\r\n"; + + $this->connection->emit('data', array($data)); + + $data = "hello"; + + $this->connection->emit('data', array($data)); + } + private function createGetRequest() { $data = "GET / HTTP/1.1\r\n"; From ff818b5b36fd3202bf8e787516081c7e50fb2778 Mon Sep 17 00:00:00 2001 From: Niels Theen Date: Sun, 19 Feb 2017 18:14:05 +0100 Subject: [PATCH 083/456] Ignore Content-Length if Transfer-Encoding isset instead of replacing --- src/Server.php | 13 ++-- tests/ServerTest.php | 156 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 159 insertions(+), 10 deletions(-) diff --git a/src/Server.php b/src/Server.php index d185276d..ba2ffa4f 100644 --- a/src/Server.php +++ b/src/Server.php @@ -153,16 +153,14 @@ public function handleRequest(ConnectionInterface $conn, Request $request) if (strtolower(end($transferEncodingHeader)) === 'chunked') { $stream = new ChunkedDecoder($conn); } - } - - if ($request->hasHeader('Content-Length')) { + } elseif ($request->hasHeader('Content-Length')) { $string = $request->getHeaderLine('Content-Length'); $contentLength = (int)$string; if ((string)$contentLength !== (string)$string) { // Content-Length value is not an integer or not a single integer - $this->emit('error', new \Exception('The value of `Content-Length` is not valid')); - return; + $this->emit('error', array(new \InvalidArgumentException('The value of `Content-Length` is not valid'))); + return $this->writeError($conn, 400); } $stream = new LengthLimitedStream($conn, $contentLength); @@ -188,9 +186,10 @@ public function handleRequest(ConnectionInterface $conn, Request $request) $this->emit('request', array($request, $response)); if ($stream instanceof LengthLimitedStream && $contentLength === 0) { - // stream must emit an 'end' here, because empty data won't be emitted + // Content-Length is 0 and won't emit further data, + // 'handleData' from LengthLimitedStream won't be called anymore $stream->emit('end'); - $conn->removeListener('data', array($stream, 'handleData')); + $stream->close(); } } diff --git a/tests/ServerTest.php b/tests/ServerTest.php index a1120b17..d1aaed05 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -775,7 +775,7 @@ public function testContentLengthContainsZeroWillEmitEndEvent() $dataEvent = $this->expectCallableNever(); $endEvent = $this->expectCallableOnce(); - $closeEvent = $this->expectCallableNever(); + $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); $server->on('request', function (Request $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { @@ -802,7 +802,7 @@ public function testContentLengthContainsZeroWillEmitEndEventAdditionalDataWillB $dataEvent = $this->expectCallableNever(); $endEvent = $this->expectCallableOnce(); - $closeEvent = $this->expectCallableNever(); + $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); $server->on('request', function (Request $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { @@ -830,7 +830,7 @@ public function testContentLengthContainsZeroWillEmitEndEventAdditionalDataWillB $dataEvent = $this->expectCallableNever(); $endEvent = $this->expectCallableOnce(); - $closeEvent = $this->expectCallableNever(); + $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); $server->on('request', function (Request $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { @@ -855,6 +855,156 @@ public function testContentLengthContainsZeroWillEmitEndEventAdditionalDataWillB $this->connection->emit('data', array($data)); } + public function testContentLengthWillBeIgnoredIfTransferEncodingIsSet() + { + $server = new Server($this->socket); + + $dataEvent = $this->expectCallableOnceWith('hello'); + $endEvent = $this->expectCallableOnce(); + $closeEvent = $this->expectCallableOnce(); + $errorEvent = $this->expectCallableNever(); + + $requestValidation = null; + $server->on('request', function (Request $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent, &$requestValidation) { + $request->on('data', $dataEvent); + $request->on('end', $endEvent); + $request->on('close', $closeEvent); + $request->on('error', $errorEvent); + $requestValidation = $request; + }); + + $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: 4\r\n"; + $data .= "Transfer-Encoding: chunked\r\n"; + $data .= "\r\n"; + + $this->connection->emit('data', array($data)); + + $data = "5\r\nhello\r\n"; + $data .= "0\r\n\r\n"; + + $this->connection->emit('data', array($data)); + $this->assertEquals('4', $requestValidation->getHeaderLine('Content-Length')); + $this->assertEquals('chunked', $requestValidation->getHeaderLine('Transfer-Encoding')); + } + + public function testInvalidContentLengthWillBeIgnoreddIfTransferEncodingIsSet() + { + $server = new Server($this->socket); + + $dataEvent = $this->expectCallableOnceWith('hello'); + $endEvent = $this->expectCallableOnce(); + $closeEvent = $this->expectCallableOnce(); + $errorEvent = $this->expectCallableNever(); + + $requestValidation = null; + $server->on('request', function (Request $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent, &$requestValidation) { + $request->on('data', $dataEvent); + $request->on('end', $endEvent); + $request->on('close', $closeEvent); + $request->on('error', $errorEvent); + $requestValidation = $request; + }); + + $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: hello world\r\n"; + $data .= "Transfer-Encoding: chunked\r\n"; + $data .= "\r\n"; + + $this->connection->emit('data', array($data)); + + $data = "5\r\nhello\r\n"; + $data .= "0\r\n\r\n"; + + $this->connection->emit('data', array($data)); + + // this is valid behavior according to: https://www.ietf.org/rfc/rfc2616.txt chapter 4.4 + $this->assertEquals('hello world', $requestValidation->getHeaderLine('Content-Length')); + $this->assertEquals('chunked', $requestValidation->getHeaderLine('Transfer-Encoding')); + } + + public function testNonIntegerContentLengthValueWillLeadToError() + { + $error = null; + $server = new Server($this->socket); + $server->on('request', $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; + } + ) + ); + + $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 testMultipleIntegerInContentLengthWillLeadToError() + { + $error = null; + $server = new Server($this->socket); + $server->on('request', $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; + } + ) + ); + + $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); + } + private function createGetRequest() { $data = "GET / HTTP/1.1\r\n"; From 39eb913d095477086a6465af49961401e5b5dc4d Mon Sep 17 00:00:00 2001 From: Niels Theen Date: Tue, 21 Feb 2017 11:06:39 +0100 Subject: [PATCH 084/456] Handle unexpected end in LengthLimitedStream --- src/LengthLimitedStream.php | 3 +-- tests/LengthLimitedStreamTest.php | 12 ++++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/LengthLimitedStream.php b/src/LengthLimitedStream.php index 6a2d4033..225f9b0b 100644 --- a/src/LengthLimitedStream.php +++ b/src/LengthLimitedStream.php @@ -95,8 +95,7 @@ public function handleError(\Exception $e) public function handleEnd() { if (!$this->closed) { - $this->emit('end'); - $this->close(); + $this->handleError(new \Exception('Unexpected end event')); } } diff --git a/tests/LengthLimitedStreamTest.php b/tests/LengthLimitedStreamTest.php index 5ba7be0d..8e6375d5 100644 --- a/tests/LengthLimitedStreamTest.php +++ b/tests/LengthLimitedStreamTest.php @@ -105,4 +105,16 @@ public function testOutputStreamCanCloseInputStream() $this->assertFalse($input->isReadable()); } + + public function testHandleUnexpectedEnd() + { + $stream = new LengthLimitedStream($this->input, 5); + + $stream->on('data', $this->expectCallableNever()); + $stream->on('close', $this->expectCallableOnce()); + $stream->on('end', $this->expectCallableNever()); + $stream->on('error', $this->expectCallableOnce()); + + $this->input->emit('end'); + } } From dcc2b95372847852ee610279265deaf278184514 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 21 Feb 2017 11:36:13 +0100 Subject: [PATCH 085/456] Request stream will now be handled internally --- src/Request.php | 24 +++++++++++++++--- src/Server.php | 19 +-------------- tests/RequestTest.php | 57 ++++++++++++++++++++++++++++++++++--------- 3 files changed, 68 insertions(+), 32 deletions(-) diff --git a/src/Request.php b/src/Request.php index 58d7b214..ae8f6303 100644 --- a/src/Request.php +++ b/src/Request.php @@ -26,6 +26,7 @@ class Request extends EventEmitter implements ReadableStreamInterface { private $readable = true; private $request; + private $stream; // metadata, implicitly added externally public $remoteAddress; @@ -39,9 +40,23 @@ class Request extends EventEmitter implements ReadableStreamInterface * * @internal */ - public function __construct(RequestInterface $request) + public function __construct(RequestInterface $request, ReadableStreamInterface $stream) { $this->request = $request; + $this->stream = $stream; + + $that = $this; + // forward data and end events from body stream to request + $stream->on('data', function ($data) use ($that) { + $that->emit('data', array($data)); + }); + $stream->on('end', function () use ($that) { + $that->emit('end'); + }); + $stream->on('error', function ($error) use ($that) { + $that->emit('error', array($error)); + }); + $stream->on('close', array($this, 'close')); } /** @@ -163,7 +178,7 @@ public function pause() return; } - $this->emit('pause'); + $this->stream->pause(); } public function resume() @@ -172,7 +187,7 @@ public function resume() return; } - $this->emit('resume'); + $this->stream->resume(); } public function close() @@ -181,7 +196,10 @@ public function close() return; } + // request closed => stop reading from the stream by pausing it $this->readable = false; + $this->stream->pause(); + $this->emit('close'); $this->removeAllListeners(); } diff --git a/src/Server.php b/src/Server.php index 35fef695..6c16abab 100644 --- a/src/Server.php +++ b/src/Server.php @@ -149,7 +149,7 @@ public function handleRequest(ConnectionInterface $conn, RequestInterface $reque } } - $request = new Request($request); + $request = new Request($request, $stream); // attach remote ip to the request as metadata $request->remoteAddress = trim( @@ -157,23 +157,6 @@ public function handleRequest(ConnectionInterface $conn, RequestInterface $reque '[]' ); - // forward pause/resume calls to underlying connection - $request->on('pause', array($conn, 'pause')); - $request->on('resume', array($conn, 'resume')); - - // request closed => stop reading from the stream by pausing it - // stream closed => close request - $request->on('close', array($stream, 'pause')); - $stream->on('close', array($request, 'close')); - - // forward data and end events from body stream to request - $stream->on('end', function() use ($request) { - $request->emit('end'); - }); - $stream->on('data', function ($data) use ($request) { - $request->emit('data', array($data)); - }); - $this->emit('request', array($request, $response)); } diff --git a/tests/RequestTest.php b/tests/RequestTest.php index 9ff13bed..fa9705d3 100644 --- a/tests/RequestTest.php +++ b/tests/RequestTest.php @@ -7,11 +7,18 @@ class RequestTest extends TestCase { + private $stream; + + public function setUp() + { + $this->stream = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + } + /** @test */ public function expectsContinueShouldBeFalseByDefault() { $headers = array(); - $request = new Request(new Psr('GET', '/', $headers, null, '1.1')); + $request = new Request(new Psr('GET', '/', $headers, null, '1.1'), $this->stream); $this->assertFalse($request->expectsContinue()); } @@ -20,7 +27,7 @@ public function expectsContinueShouldBeFalseByDefault() public function expectsContinueShouldBeTrueIfContinueExpected() { $headers = array('Expect' => array('100-continue')); - $request = new Request(new Psr('GET', '/', $headers, null, '1.1')); + $request = new Request(new Psr('GET', '/', $headers, null, '1.1'), $this->stream); $this->assertTrue($request->expectsContinue()); } @@ -29,7 +36,7 @@ public function expectsContinueShouldBeTrueIfContinueExpected() public function expectsContinueShouldBeTrueIfContinueExpectedCaseInsensitive() { $headers = array('EXPECT' => array('100-CONTINUE')); - $request = new Request(new Psr('GET', '/', $headers, null, '1.1')); + $request = new Request(new Psr('GET', '/', $headers, null, '1.1'), $this->stream); $this->assertTrue($request->expectsContinue()); } @@ -38,14 +45,14 @@ public function expectsContinueShouldBeTrueIfContinueExpectedCaseInsensitive() public function expectsContinueShouldBeFalseForHttp10() { $headers = array('Expect' => array('100-continue')); - $request = new Request(new Psr('GET', '/', $headers, null, '1.0')); + $request = new Request(new Psr('GET', '/', $headers, null, '1.0'), $this->stream); $this->assertFalse($request->expectsContinue()); } public function testEmptyHeader() { - $request = new Request(new Psr('GET', '/', array())); + $request = new Request(new Psr('GET', '/', array()), $this->stream); $this->assertEquals(array(), $request->getHeaders()); $this->assertFalse($request->hasHeader('Test')); @@ -57,7 +64,7 @@ public function testHeaderIsCaseInsensitive() { $request = new Request(new Psr('GET', '/', array( 'TEST' => array('Yes'), - ))); + )), $this->stream); $this->assertEquals(array('TEST' => array('Yes')), $request->getHeaders()); $this->assertTrue($request->hasHeader('Test')); @@ -69,7 +76,7 @@ public function testHeaderWithMultipleValues() { $request = new Request(new Psr('GET', '/', array( 'Test' => array('a', 'b'), - ))); + )), $this->stream); $this->assertEquals(array('Test' => array('a', 'b')), $request->getHeaders()); $this->assertTrue($request->hasHeader('Test')); @@ -79,7 +86,7 @@ public function testHeaderWithMultipleValues() public function testCloseEmitsCloseEvent() { - $request = new Request(new Psr('GET', '/')); + $request = new Request(new Psr('GET', '/'), $this->stream); $request->on('close', $this->expectCallableOnce()); @@ -88,7 +95,7 @@ public function testCloseEmitsCloseEvent() public function testCloseMultipleTimesEmitsCloseEventOnce() { - $request = new Request(new Psr('GET', '/')); + $request = new Request(new Psr('GET', '/'), $this->stream); $request->on('close', $this->expectCallableOnce()); @@ -96,20 +103,48 @@ public function testCloseMultipleTimesEmitsCloseEventOnce() $request->close(); } + public function testCloseWillPauseUnderlyingStream() + { + $this->stream->expects($this->once())->method('pause'); + $this->stream->expects($this->never())->method('close'); + + $request = new Request(new Psr('GET', '/'), $this->stream); + + $request->close(); + } + public function testIsNotReadableAfterClose() { - $request = new Request(new Psr('GET', '/')); + $request = new Request(new Psr('GET', '/'), $this->stream); $request->close(); $this->assertFalse($request->isReadable()); } + public function testPauseWillBeForwarded() + { + $this->stream->expects($this->once())->method('pause'); + + $request = new Request(new Psr('GET', '/'), $this->stream); + + $request->pause(); + } + + public function testResumeWillBeForwarded() + { + $this->stream->expects($this->once())->method('resume'); + + $request = new Request(new Psr('GET', '/'), $this->stream); + + $request->resume(); + } + public function testPipeReturnsDest() { $dest = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); - $request = new Request(new Psr('GET', '/')); + $request = new Request(new Psr('GET', '/'), $this->stream); $ret = $request->pipe($dest); From 76ef78f467db3b13ec74ec7d390b664db024b4dc Mon Sep 17 00:00:00 2001 From: Niels Theen Date: Tue, 28 Feb 2017 23:45:55 +0100 Subject: [PATCH 086/456] Leading zeros are correct chunked encoding --- src/ChunkedDecoder.php | 7 +++++ tests/ChunkedDecoderTest.php | 51 ++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/src/ChunkedDecoder.php b/src/ChunkedDecoder.php index 1d3bc16f..4b6ebc4f 100644 --- a/src/ChunkedDecoder.php +++ b/src/ChunkedDecoder.php @@ -108,6 +108,13 @@ public function handleData($data) $hexValue = $array[0]; } + if ($hexValue !== '') { + $hexValue = ltrim($hexValue, "0"); + if ($hexValue === '') { + $hexValue = "0"; + } + } + $this->chunkSize = hexdec($hexValue); if (dechex($this->chunkSize) !== $hexValue) { $this->handleError(new \Exception($hexValue . ' is not a valid hexadecimal number')); diff --git a/tests/ChunkedDecoderTest.php b/tests/ChunkedDecoderTest.php index 6f0c3048..82806a8a 100644 --- a/tests/ChunkedDecoderTest.php +++ b/tests/ChunkedDecoderTest.php @@ -396,4 +396,55 @@ public function testOutputStreamCanCloseInputStream() $this->assertFalse($input->isReadable()); } + + public function testLeadingZerosWillBeIgnored() + { + $this->parser->on('data', $this->expectCallableConsecutive(2, array('hello', 'hello world'))); + $this->parser->on('error', $this->expectCallableNever()); + $this->parser->on('end', $this->expectCallableNever()); + $this->parser->on('close', $this->expectCallableNever()); + + $this->input->emit('data', array("00005\r\nhello\r\n")); + $this->input->emit('data', array("0000b\r\nhello world\r\n")); + } + + public function testLeadingZerosInEndChunkWillBeIgnored() + { + $this->parser->on('data', $this->expectCallableNever()); + $this->parser->on('error', $this->expectCallableNever()); + $this->parser->on('end', $this->expectCallableOnce()); + $this->parser->on('close', $this->expectCallableOnce()); + + $this->input->emit('data', array("0000\r\n\r\n")); + } + + public function testLeadingZerosInInvalidChunk() + { + $this->parser->on('data', $this->expectCallableNever()); + $this->parser->on('error', $this->expectCallableOnce()); + $this->parser->on('end', $this->expectCallableNever()); + $this->parser->on('close', $this->expectCallableOnce()); + + $this->input->emit('data', array("0000hello\r\n\r\n")); + } + + public function testEmptyHeaderLeadsToError() + { + $this->parser->on('data', $this->expectCallableNever()); + $this->parser->on('error', $this->expectCallableOnce()); + $this->parser->on('end', $this->expectCallableNever()); + $this->parser->on('close', $this->expectCallableOnce()); + + $this->input->emit('data', array("\r\n\r\n")); + } + + public function testEmptyHeaderAndFilledBodyLeadsToError() + { + $this->parser->on('data', $this->expectCallableNever()); + $this->parser->on('error', $this->expectCallableOnce()); + $this->parser->on('end', $this->expectCallableNever()); + $this->parser->on('close', $this->expectCallableOnce()); + + $this->input->emit('data', array("\r\nhello\r\n")); + } } From 220f00a9cf11bbd996f172fae8f8bd740bd86fab Mon Sep 17 00:00:00 2001 From: Niels Theen Date: Wed, 1 Mar 2017 12:46:14 +0100 Subject: [PATCH 087/456] Test error events from other streams on request object --- tests/ServerTest.php | 118 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 32db8ef3..700d34bc 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -1023,6 +1023,124 @@ function ($data) use (&$buffer) { $this->assertInstanceOf('InvalidArgumentException', $error); } + public function testInvalidChunkHeaderResultsInErrorOnRequestStream() + { + $errorEvent = $this->expectCallableOnceWith($this->isInstanceOf('Exception')); + $server = new Server($this->socket); + $server->on('request', function ($request, $response) use ($errorEvent){ + $request->on('error', $errorEvent); + }); + + $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: chunked\r\n"; + $data .= "\r\n"; + $data .= "hello\r\hello\r\n"; + + $this->connection->emit('data', array($data)); + } + + public function testTooLongChunkHeaderResultsInErrorOnRequestStream() + { + $errorEvent = $this->expectCallableOnceWith($this->isInstanceOf('Exception')); + $server = new Server($this->socket); + $server->on('request', function ($request, $response) use ($errorEvent){ + $request->on('error', $errorEvent); + }); + + $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: chunked\r\n"; + $data .= "\r\n"; + for ($i = 0; $i < 1025; $i++) { + $data .= 'a'; + } + + $this->connection->emit('data', array($data)); + } + + public function testTooLongChunkBodyResultsInErrorOnRequestStream() + { + $errorEvent = $this->expectCallableOnceWith($this->isInstanceOf('Exception')); + $server = new Server($this->socket); + $server->on('request', function ($request, $response) use ($errorEvent){ + $request->on('error', $errorEvent); + }); + + $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: chunked\r\n"; + $data .= "\r\n"; + $data .= "5\r\nhello world\r\n"; + + $this->connection->emit('data', array($data)); + } + + public function testUnexpectedEndOfConnectionWillResultsInErrorOnRequestStream() + { + $errorEvent = $this->expectCallableOnceWith($this->isInstanceOf('Exception')); + $server = new Server($this->socket); + $server->on('request', function ($request, $response) use ($errorEvent){ + $request->on('error', $errorEvent); + }); + + $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: chunked\r\n"; + $data .= "\r\n"; + $data .= "5\r\nhello\r\n"; + + $this->connection->emit('data', array($data)); + $this->connection->emit('end'); + } + + public function testErrorInChunkedDecoderNeverClosesConnection() + { + $server = new Server($this->socket); + $server->on('request', $this->expectCallableOnce()); + + $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: chunked\r\n"; + $data .= "\r\n"; + $data .= "hello\r\nhello\r\n"; + + $this->connection->emit('data', array($data)); + } + + public function testErrorInLengthLimitedStreamNeverClosesConnection() + { + $server = new Server($this->socket); + $server->on('request', $this->expectCallableOnce()); + + $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 .= "\r\n"; + $data .= "hello"; + + $this->connection->emit('data', array($data)); + $this->connection->emit('end'); + } + private function createGetRequest() { $data = "GET / HTTP/1.1\r\n"; From f7d45c5327ccbc3ea704d75a07df1f1575c1ced5 Mon Sep 17 00:00:00 2001 From: Niels Theen Date: Tue, 21 Feb 2017 12:04:35 +0100 Subject: [PATCH 088/456] Protect streams against close of other streams on error --- src/CloseProtectionStream.php | 101 +++++++++++++++++++ src/Request.php | 3 +- src/Server.php | 6 +- tests/CloseProtectionStreamTest.php | 146 ++++++++++++++++++++++++++++ tests/RequestTest.php | 5 +- tests/ServerTest.php | 34 +++++++ 6 files changed, 287 insertions(+), 8 deletions(-) create mode 100644 src/CloseProtectionStream.php create mode 100644 tests/CloseProtectionStreamTest.php diff --git a/src/CloseProtectionStream.php b/src/CloseProtectionStream.php new file mode 100644 index 00000000..da4b2625 --- /dev/null +++ b/src/CloseProtectionStream.php @@ -0,0 +1,101 @@ +input = $input; + + $this->input->on('data', array($this, 'handleData')); + $this->input->on('end', array($this, 'handleEnd')); + $this->input->on('error', array($this, 'handleError')); + $this->input->on('close', array($this, 'close')); + } + + public function isReadable() + { + return !$this->closed && $this->input->isReadable(); + } + + public function pause() + { + if ($this->closed) { + return; + } + + $this->input->pause(); + } + + public function resume() + { + if ($this->closed) { + return; + } + + $this->input->resume(); + } + + 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'); + + // 'pause' the stream avoids additional traffic transferred by this stream + $this->input->pause(); + + $this->input->removeListener('data', array($this, 'handleData')); + $this->input->removeListener('error', array($this, 'handleError')); + $this->input->removeListener('end', array($this, 'handleEnd')); + $this->input->removeListener('close', array($this, 'close')); + + $this->removeAllListeners(); + } + + /** @internal */ + public function handleData($data) + { + $this->emit('data', array($data)); + } + + /** @internal */ + public function handleEnd() + { + $this->emit('end'); + $this->close(); + } + + /** @internal */ + public function handleError(\Exception $e) + { + $this->emit('error', array($e)); + } + +} diff --git a/src/Request.php b/src/Request.php index ae8f6303..e3559560 100644 --- a/src/Request.php +++ b/src/Request.php @@ -196,9 +196,8 @@ public function close() return; } - // request closed => stop reading from the stream by pausing it $this->readable = false; - $this->stream->pause(); + $this->stream->close(); $this->emit('close'); $this->removeAllListeners(); diff --git a/src/Server.php b/src/Server.php index 4de7770b..a95028f0 100644 --- a/src/Server.php +++ b/src/Server.php @@ -140,12 +140,12 @@ public function handleRequest(ConnectionInterface $conn, RequestInterface $reque $response = new Response($conn, $request->getProtocolVersion()); - $stream = $conn; + $stream = new CloseProtectionStream($conn); if ($request->hasHeader('Transfer-Encoding')) { $transferEncodingHeader = $request->getHeader('Transfer-Encoding'); // 'chunked' must always be the final value of 'Transfer-Encoding' according to: https://tools.ietf.org/html/rfc7230#section-3.3.1 if (strtolower(end($transferEncodingHeader)) === 'chunked') { - $stream = new ChunkedDecoder($conn); + $stream = new ChunkedDecoder($stream); } } elseif ($request->hasHeader('Content-Length')) { $string = $request->getHeaderLine('Content-Length'); @@ -157,7 +157,7 @@ public function handleRequest(ConnectionInterface $conn, RequestInterface $reque return $this->writeError($conn, 400); } - $stream = new LengthLimitedStream($conn, $contentLength); + $stream = new LengthLimitedStream($stream, $contentLength); } $request = new Request($request, $stream); diff --git a/tests/CloseProtectionStreamTest.php b/tests/CloseProtectionStreamTest.php new file mode 100644 index 00000000..a85e7c10 --- /dev/null +++ b/tests/CloseProtectionStreamTest.php @@ -0,0 +1,146 @@ +getMockBuilder('React\Stream\ReadableStreamInterface')->disableOriginalConstructor()->getMock(); + $input->expects($this->once())->method('pause'); + $input->expects($this->never())->method('close'); + + $protection = new CloseProtectionStream($input); + $protection->close(); + } + + public function testErrorWontCloseStream() + { + $input = new ReadableStream(); + + $protection = new CloseProtectionStream($input); + $protection->on('error', $this->expectCallableOnce()); + $protection->on('close', $this->expectCallableNever()); + + $input->emit('error', array(new \RuntimeException())); + + $this->assertTrue($protection->isReadable()); + $this->assertTrue($input->isReadable()); + } + + public function testResumeStreamWillResumeInputStream() + { + $input = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $input->expects($this->once())->method('pause'); + $input->expects($this->once())->method('resume'); + + $protection = new CloseProtectionStream($input); + $protection->pause(); + $protection->resume(); + } + + public function testInputStreamIsNotReadableAfterClose() + { + $input = new ReadableStream(); + + $protection = new CloseProtectionStream($input); + $protection->on('close', $this->expectCallableOnce()); + + $input->close(); + + $this->assertFalse($protection->isReadable()); + $this->assertFalse($input->isReadable()); + } + + public function testPipeStream() + { + $input = new ReadableStream(); + + $protection = new CloseProtectionStream($input); + $dest = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + + $ret = $protection->pipe($dest); + + $this->assertSame($dest, $ret); + } + + public function testStopEmittingDataAfterClose() + { + $input = new ReadableStream(); + + $protection = new CloseProtectionStream($input); + $protection->on('data', $this->expectCallableNever()); + + $protection->on('close', $this->expectCallableOnce()); + + $protection->close(); + + $input->emit('data', array('hello')); + + $this->assertFalse($protection->isReadable()); + $this->assertTrue($input->isReadable()); + } + + public function testErrorIsNeverCalledAfterClose() + { + $input = new ReadableStream(); + + $protection = new CloseProtectionStream($input); + $protection->on('data', $this->expectCallableNever()); + $protection->on('error', $this->expectCallableNever()); + $protection->on('close', $this->expectCallableOnce()); + + $protection->close(); + + $input->emit('error', array(new \Exception())); + + $this->assertFalse($protection->isReadable()); + $this->assertTrue($input->isReadable()); + } + + public function testEndWontBeEmittedAfterClose() + { + $input = new ReadableStream(); + + $protection = new CloseProtectionStream($input); + $protection->on('data', $this->expectCallableNever()); + $protection->on('close', $this->expectCallableOnce()); + + $protection->close(); + + $input->emit('end', array()); + + $this->assertFalse($protection->isReadable()); + $this->assertTrue($input->isReadable()); + } + + public function testPauseAfterCloseHasNoEffect() + { + $input = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $input->expects($this->once())->method('pause'); + + $protection = new CloseProtectionStream($input); + $protection->on('data', $this->expectCallableNever()); + $protection->on('close', $this->expectCallableOnce()); + + $protection->close(); + $protection->pause(); + } + + public function testResumeAfterCloseHasNoEffect() + { + $input = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $input->expects($this->once())->method('pause'); + $input->expects($this->never())->method('resume'); + + $protection = new CloseProtectionStream($input); + $protection->on('data', $this->expectCallableNever()); + $protection->on('close', $this->expectCallableOnce()); + + $protection->close(); + $protection->resume(); + } +} diff --git a/tests/RequestTest.php b/tests/RequestTest.php index fa9705d3..3d010b7f 100644 --- a/tests/RequestTest.php +++ b/tests/RequestTest.php @@ -103,10 +103,9 @@ public function testCloseMultipleTimesEmitsCloseEventOnce() $request->close(); } - public function testCloseWillPauseUnderlyingStream() + public function testCloseWillCloseUnderlyingStream() { - $this->stream->expects($this->once())->method('pause'); - $this->stream->expects($this->never())->method('close'); + $this->stream->expects($this->once())->method('close'); $request = new Request(new Psr('GET', '/'), $this->stream); diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 700d34bc..73f29e8e 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -1031,6 +1031,9 @@ public function testInvalidChunkHeaderResultsInErrorOnRequestStream() $request->on('error', $errorEvent); }); + $this->connection->expects($this->never())->method('close'); + $this->connection->expects($this->once())->method('pause'); + $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; @@ -1051,6 +1054,9 @@ public function testTooLongChunkHeaderResultsInErrorOnRequestStream() $request->on('error', $errorEvent); }); + $this->connection->expects($this->never())->method('close'); + $this->connection->expects($this->once())->method('pause'); + $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; @@ -1073,6 +1079,9 @@ public function testTooLongChunkBodyResultsInErrorOnRequestStream() $request->on('error', $errorEvent); }); + $this->connection->expects($this->never())->method('close'); + $this->connection->expects($this->once())->method('pause'); + $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; @@ -1093,6 +1102,9 @@ public function testUnexpectedEndOfConnectionWillResultsInErrorOnRequestStream() $request->on('error', $errorEvent); }); + $this->connection->expects($this->never())->method('close'); + $this->connection->expects($this->once())->method('pause'); + $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; @@ -1111,6 +1123,9 @@ public function testErrorInChunkedDecoderNeverClosesConnection() $server = new Server($this->socket); $server->on('request', $this->expectCallableOnce()); + $this->connection->expects($this->never())->method('close'); + $this->connection->expects($this->once())->method('pause'); + $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; @@ -1128,6 +1143,9 @@ public function testErrorInLengthLimitedStreamNeverClosesConnection() $server = new Server($this->socket); $server->on('request', $this->expectCallableOnce()); + $this->connection->expects($this->never())->method('close'); + $this->connection->expects($this->once())->method('pause'); + $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; @@ -1141,6 +1159,22 @@ public function testErrorInLengthLimitedStreamNeverClosesConnection() $this->connection->emit('end'); } + public function testCloseRequestWillPauseConnection() + { + $server = new Server($this->socket); + $server->on('request', function ($request, $response) { + $request->close(); + }); + + $this->connection->expects($this->never())->method('close'); + $this->connection->expects($this->once())->method('pause'); + + $this->socket->emit('connection', array($this->connection)); + + $data = $this->createGetRequest(); + $this->connection->emit('data', array($data)); + } + private function createGetRequest() { $data = "GET / HTTP/1.1\r\n"; From 9969b6792fdc196b0199ae47b519a94785bfad1d Mon Sep 17 00:00:00 2001 From: Niels Theen Date: Wed, 1 Mar 2017 13:09:05 +0100 Subject: [PATCH 089/456] Update README --- README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.md b/README.md index 39f3b901..cedaee25 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,20 @@ $http->on('error', function (Exception $e) { }); ``` +An `error` event will be emitted for the `Request` if the validation of the body data fails. +This can be e.g. invalid chunked decoded data or an unexpected `end` event. + +```php +$http->on('request', function (Request $request, Response $response) { + $request->on('error', function (\Exception $error) { + echo $error->getMessage(); + }); +}); +``` + +Such an error will `pause` the connection instead of closing it. A response message +can still be sent. + ### Request The `Request` class is responsible for streaming the incoming request body From 41bcdb079f0f0ef359cdc3dff52fd161eaab3bbb Mon Sep 17 00:00:00 2001 From: Niels Theen Date: Wed, 22 Feb 2017 15:30:39 +0100 Subject: [PATCH 090/456] Handle events of simple requests --- src/Server.php | 6 ++++++ tests/ServerTest.php | 32 +++++++++++++++++++++++++++++++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/Server.php b/src/Server.php index a95028f0..6ebd9163 100644 --- a/src/Server.php +++ b/src/Server.php @@ -170,6 +170,12 @@ public function handleRequest(ConnectionInterface $conn, RequestInterface $reque $this->emit('request', array($request, $response)); + if ($stream instanceof CloseProtectionStream) { + $request->emit('end'); + $request->close(); + return; + } + if ($stream instanceof LengthLimitedStream && $contentLength === 0) { // Content-Length is 0 and won't emit further data, // 'handleData' from LengthLimitedStream won't be called anymore diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 73f29e8e..4315e890 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -100,7 +100,12 @@ public function testRequestPauseWillbeForwardedToConnection() $this->connection->expects($this->once())->method('pause'); $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)); } @@ -1175,6 +1180,31 @@ public function testCloseRequestWillPauseConnection() $this->connection->emit('data', array($data)); } + public function testEndEventWillBeEmittedOnSimpleRequest() + { + $dataEvent = $this->expectCallableNever(); + $closeEvent = $this->expectCallableOnce(); + $endEvent = $this->expectCallableOnce(); + $errorEvent = $this->expectCallableNever(); + + $server = new Server($this->socket); + $server->on('request', function ($request, $response) use ($dataEvent, $closeEvent, $endEvent, $errorEvent){ + $request->on('data', $dataEvent); + $request->on('close', $closeEvent); + $request->on('end', $endEvent); + $request->on('error', $errorEvent); + }); + + $this->connection->expects($this->once())->method('pause'); + $this->connection->expects($this->never())->method('close'); + + $this->socket->emit('connection', array($this->connection)); + + $data = $this->createGetRequest(); + + $this->connection->emit('data', array($data)); + } + private function createGetRequest() { $data = "GET / HTTP/1.1\r\n"; From 26fb64656b1f4e18557077dfc4eeaedbc642fa5e Mon Sep 17 00:00:00 2001 From: Niels Theen Date: Thu, 2 Mar 2017 16:21:55 +0100 Subject: [PATCH 091/456] Use PSR-7 approach to handle bodiless data --- src/Server.php | 14 +++++--------- tests/ServerTest.php | 24 ++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/src/Server.php b/src/Server.php index 6ebd9163..6ad349c7 100644 --- a/src/Server.php +++ b/src/Server.php @@ -140,12 +140,14 @@ public function handleRequest(ConnectionInterface $conn, RequestInterface $reque $response = new Response($conn, $request->getProtocolVersion()); + $contentLength = 0; $stream = new CloseProtectionStream($conn); if ($request->hasHeader('Transfer-Encoding')) { $transferEncodingHeader = $request->getHeader('Transfer-Encoding'); // 'chunked' must always be the final value of 'Transfer-Encoding' according to: https://tools.ietf.org/html/rfc7230#section-3.3.1 if (strtolower(end($transferEncodingHeader)) === 'chunked') { $stream = new ChunkedDecoder($stream); + $contentLength = null; } } elseif ($request->hasHeader('Content-Length')) { $string = $request->getHeaderLine('Content-Length'); @@ -170,15 +172,9 @@ public function handleRequest(ConnectionInterface $conn, RequestInterface $reque $this->emit('request', array($request, $response)); - if ($stream instanceof CloseProtectionStream) { - $request->emit('end'); - $request->close(); - return; - } - - if ($stream instanceof LengthLimitedStream && $contentLength === 0) { - // Content-Length is 0 and won't emit further data, - // 'handleData' from LengthLimitedStream won't be called anymore + if ($contentLength === 0) { + // If Body is empty or Content-Length is 0 and won't emit further data, + // 'data' events from other streams won't be called anymore $stream->emit('end'); $stream->close(); } diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 4315e890..6eb4fc6f 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -1205,6 +1205,30 @@ public function testEndEventWillBeEmittedOnSimpleRequest() $this->connection->emit('data', array($data)); } + public function testRequestWithoutDefinedLengthWillIgnoreDataEvent() + { + $server = new Server($this->socket); + + $dataEvent = $this->expectCallableNever(); + $endEvent = $this->expectCallableOnce(); + $closeEvent = $this->expectCallableOnce(); + $errorEvent = $this->expectCallableNever(); + + $server->on('request', function (Request $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $request->on('data', $dataEvent); + $request->on('end', $endEvent); + $request->on('close', $closeEvent); + $request->on('error', $errorEvent); + }); + + $this->socket->emit('connection', array($this->connection)); + + $data = $this->createGetRequest(); + $data .= "hello world"; + + $this->connection->emit('data', array($data)); + } + private function createGetRequest() { $data = "GET / HTTP/1.1\r\n"; From 59ec0cacbfc540221635e0202d9b91ecc7275cce Mon Sep 17 00:00:00 2001 From: Niels Theen Date: Fri, 3 Mar 2017 14:55:06 +0100 Subject: [PATCH 092/456] Remove TE and CL header before emitting the request event --- src/Server.php | 4 ++++ tests/ServerTest.php | 17 +++++++++++------ 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/Server.php b/src/Server.php index 6ad349c7..08671444 100644 --- a/src/Server.php +++ b/src/Server.php @@ -147,6 +147,9 @@ public function handleRequest(ConnectionInterface $conn, RequestInterface $reque // 'chunked' must always be the final value of 'Transfer-Encoding' according to: https://tools.ietf.org/html/rfc7230#section-3.3.1 if (strtolower(end($transferEncodingHeader)) === 'chunked') { $stream = new ChunkedDecoder($stream); + + $request = $request->withoutHeader('Transfer-Encoding'); + $contentLength = null; } } elseif ($request->hasHeader('Content-Length')) { @@ -162,6 +165,7 @@ public function handleRequest(ConnectionInterface $conn, RequestInterface $reque $stream = new LengthLimitedStream($stream, $contentLength); } + $request = $request->withoutHeader('Content-Length'); $request = new Request($request, $stream); // attach remote ip to the request as metadata diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 6eb4fc6f..b538f2b4 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -487,12 +487,14 @@ public function testChunkedEncodedRequestWillBeParsedForRequestEvent() $endEvent = $this->expectCallableOnce(); $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); + $requestValidation = null; - $server->on('request', function (Request $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server->on('request', function (Request $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent, &$requestValidation) { $request->on('data', $dataEvent); $request->on('end', $endEvent); $request->on('close', $closeEvent); $request->on('error', $errorEvent); + $requestValidation = $request; }); $this->socket->emit('connection', array($this->connection)); @@ -506,6 +508,8 @@ public function testChunkedEncodedRequestWillBeParsedForRequestEvent() $data .= "0\r\n\r\n"; $this->connection->emit('data', array($data)); + + $this->assertFalse($requestValidation->hasHeader('Transfer-Encoding')); } public function testChunkedEncodedRequestAdditionalDataWontBeEmitted() @@ -911,8 +915,9 @@ public function testContentLengthWillBeIgnoredIfTransferEncodingIsSet() $data .= "0\r\n\r\n"; $this->connection->emit('data', array($data)); - $this->assertEquals('4', $requestValidation->getHeaderLine('Content-Length')); - $this->assertEquals('chunked', $requestValidation->getHeaderLine('Transfer-Encoding')); + + $this->assertFalse($requestValidation->hasHeader('Content-Length')); + $this->assertFalse($requestValidation->hasHeader('Transfer-Encoding')); } public function testInvalidContentLengthWillBeIgnoreddIfTransferEncodingIsSet() @@ -938,6 +943,7 @@ public function testInvalidContentLengthWillBeIgnoreddIfTransferEncodingIsSet() $data = "GET / HTTP/1.1\r\n"; $data .= "Host: example.com:80\r\n"; $data .= "Connection: close\r\n"; + // this is valid behavior according to: https://www.ietf.org/rfc/rfc2616.txt chapter 4.4 $data .= "Content-Length: hello world\r\n"; $data .= "Transfer-Encoding: chunked\r\n"; $data .= "\r\n"; @@ -949,9 +955,8 @@ public function testInvalidContentLengthWillBeIgnoreddIfTransferEncodingIsSet() $this->connection->emit('data', array($data)); - // this is valid behavior according to: https://www.ietf.org/rfc/rfc2616.txt chapter 4.4 - $this->assertEquals('hello world', $requestValidation->getHeaderLine('Content-Length')); - $this->assertEquals('chunked', $requestValidation->getHeaderLine('Transfer-Encoding')); + $this->assertFalse($requestValidation->hasHeader('Content-Length')); + $this->assertFalse($requestValidation->hasHeader('Transfer-Encoding')); } public function testNonIntegerContentLengthValueWillLeadToError() From 287c0c1612dd2326a05011b27b8af2f461077076 Mon Sep 17 00:00:00 2001 From: Niels Theen Date: Fri, 3 Mar 2017 16:39:58 +0100 Subject: [PATCH 093/456] Only remove CL if CL and TE are given --- src/Server.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Server.php b/src/Server.php index 08671444..4a8d17ca 100644 --- a/src/Server.php +++ b/src/Server.php @@ -149,6 +149,7 @@ public function handleRequest(ConnectionInterface $conn, RequestInterface $reque $stream = new ChunkedDecoder($stream); $request = $request->withoutHeader('Transfer-Encoding'); + $request = $request->withoutHeader('Content-Length'); $contentLength = null; } @@ -165,7 +166,6 @@ public function handleRequest(ConnectionInterface $conn, RequestInterface $reque $stream = new LengthLimitedStream($stream, $contentLength); } - $request = $request->withoutHeader('Content-Length'); $request = new Request($request, $stream); // attach remote ip to the request as metadata From bcfbce1726d7ed32a3c4591edc6f66e8710b04eb Mon Sep 17 00:00:00 2001 From: Niels Theen Date: Fri, 3 Mar 2017 16:40:57 +0100 Subject: [PATCH 094/456] Update README --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index cedaee25..d4db2474 100644 --- a/README.md +++ b/README.md @@ -121,6 +121,8 @@ can still be sent. The `Request` class is responsible for streaming the incoming request body and contains meta data which was parsed from the request headers. +If the request body is chunked-encoded, the data will be decoded and emitted on the data event. +The `Transfer-Encoding` header will be removed. It implements the `ReadableStreamInterface`. From b82e8dd6b5cf0ab6b8fedc909f1ecf20fc3bf166 Mon Sep 17 00:00:00 2001 From: Niels Theen Date: Fri, 3 Mar 2017 15:58:37 +0100 Subject: [PATCH 095/456] Handle TE and CL in Response object --- README.md | 5 +- src/Response.php | 13 +++--- tests/ResponseTest.php | 33 ++++++++++++++ tests/ServerTest.php | 101 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 144 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index cedaee25..bd0988a7 100644 --- a/README.md +++ b/README.md @@ -270,8 +270,9 @@ Calling this method after the response has ended/closed is a NOOP. Unless you specify a `Content-Length` header yourself, HTTP/1.1 responses will automatically use chunked transfer encoding and send the respective header -(`Transfer-Encoding: chunked`) automatically. If you know the length of your -body, you MAY specify it like this instead: +(`Transfer-Encoding: chunked`) automatically. The server is responsible for handling +`Transfer-Encoding` so you SHOULD NOT pass it yourself. +If you know the length of your body, you MAY specify it like this instead: ```php $data = 'Hello World!'; diff --git a/src/Response.php b/src/Response.php index 3283d2a7..e1b31297 100644 --- a/src/Response.php +++ b/src/Response.php @@ -204,14 +204,15 @@ public function writeHead($status = 200, array $headers = array()) ); } - // assign chunked transfer-encoding if no 'content-length' is given for HTTP/1.1 responses - if (!isset($lower['content-length']) && $this->protocolVersion === '1.1') { - foreach($headers as $name => $value) { - if (strtolower($name) === 'transfer-encoding') { - unset($headers[$name]); - } + // always remove transfer-encoding + foreach($headers as $name => $value) { + if (strtolower($name) === 'transfer-encoding') { + unset($headers[$name]); } + } + // assign chunked transfer-encoding if no 'content-length' is given for HTTP/1.1 responses + if (!isset($lower['content-length']) && $this->protocolVersion === '1.1') { $headers['Transfer-Encoding'] = 'chunked'; $this->chunkedEncoding = true; } diff --git a/tests/ResponseTest.php b/tests/ResponseTest.php index 841bcdd9..e8633596 100644 --- a/tests/ResponseTest.php +++ b/tests/ResponseTest.php @@ -596,4 +596,37 @@ public function testDrainEventShouldBeForwarded() $input->emit('drain'); } + + public function testContentLengthWillBeRemovedIfTransferEncodingIsGiven() + { + $expectedHeader = ''; + $expectedHeader .= "HTTP/1.1 200 OK\r\n"; + $expectedHeader .= "X-Powered-By: React/alpha\r\n"; + $expectedHeader .= "Content-Length: 4\r\n"; + $expectedHeader .= "Connection: close\r\n"; + $expectedHeader .= "\r\n"; + + $expectedBody = "hello"; + + $conn = $this + ->getMockBuilder('React\Socket\ConnectionInterface') + ->getMock(); + $conn + ->expects($this->exactly(2)) + ->method('write') + ->withConsecutive( + array($expectedHeader), + array($expectedBody) + ); + + $response = new Response($conn, '1.1'); + $response->writeHead( + 200, + array( + 'Content-Length' => 4, + 'Transfer-Encoding' => 'chunked' + ) + ); + $response->write('hello'); + } } diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 6eb4fc6f..8e7feda2 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -1229,6 +1229,107 @@ public function testRequestWithoutDefinedLengthWillIgnoreDataEvent() $this->connection->emit('data', array($data)); } + public function testResponseWillBeChunkDecodedByDefault() + { + $server = new Server($this->socket); + + $server->on('request', function (Request $request, Response $response) { + $response->writeHead(); + $response->write('hello'); + }); + + $this->connection + ->expects($this->exactly(2)) + ->method('write') + ->withConsecutive( + array($this->anything()), + array("5\r\nhello\r\n") + ); + + $this->socket->emit('connection', array($this->connection)); + + $data = $this->createGetRequest(); + + $this->connection->emit('data', array($data)); + } + + public function testContentLengthWillBeRemovedForResponseStream() + { + $server = new Server($this->socket); + + $server->on('request', function (Request $request, Response $response) { + $response->writeHead( + 200, + array( + 'Content-Length' => 4, + 'Transfer-Encoding' => 'chunked' + ) + ); + + $response->write('hello'); + }); + + $buffer = ''; + $this->connection + ->expects($this->exactly(2)) + ->method('write') + ->will( + $this->returnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } + ) + ); + + $this->socket->emit('connection', array($this->connection)); + + $data = $this->createGetRequest(); + + $this->connection->emit('data', array($data)); + + $this->assertNotContains("Transfer-Encoding: chunked", $buffer); + $this->assertContains("Content-Length: 4", $buffer); + $this->assertContains("hello", $buffer); + } + + public function testOnlyAllowChunkedEncoding() + { + $server = new Server($this->socket); + + $server->on('request', function (Request $request, Response $response) { + $response->writeHead( + 200, + array( + 'Transfer-Encoding' => 'custom' + ) + ); + + $response->write('hello'); + }); + + $buffer = ''; + $this->connection + ->expects($this->exactly(2)) + ->method('write') + ->will( + $this->returnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } + ) + ); + + $this->socket->emit('connection', array($this->connection)); + + $data = $this->createGetRequest(); + + $this->connection->emit('data', array($data)); + + $this->assertContains('Transfer-Encoding: chunked', $buffer); + $this->assertNotContains('Transfer-Encoding: custom', $buffer); + $this->assertContains("5\r\nhello\r\n", $buffer); + } + private function createGetRequest() { $data = "GET / HTTP/1.1\r\n"; From b50d7c4c822c4a4df9a4620961e5561e86e13c4f Mon Sep 17 00:00:00 2001 From: Niels Theen Date: Fri, 3 Mar 2017 12:12:08 +0100 Subject: [PATCH 096/456] Add system date 'Date' header if none isset --- README.md | 18 ++++++++ src/Response.php | 6 +++ tests/ResponseTest.php | 94 ++++++++++++++++++++++++++++++++++++------ tests/ServerTest.php | 93 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 198 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 9e6def83..ddc19fbb 100644 --- a/README.md +++ b/README.md @@ -286,6 +286,24 @@ $response->writeHead(200, array( $response->end($data); ``` +A `Date` header will be automatically added with the system date and time if none is given. +You can add a custom `Date` header yourself like this: + +```php +$response->writeHead(200, array( + 'Date' => date('D, d M Y H:i:s T') +)); +``` + +If you don't have a appropriate clock to rely on, you should +unset this header with an empty array: + +```php +$response->writeHead(200, array( + 'Date' => array() +)); +``` + Note that it will automatically assume a `X-Powered-By: react/alpha` header unless your specify a custom `X-Powered-By` header yourself: diff --git a/src/Response.php b/src/Response.php index e1b31297..e7e34182 100644 --- a/src/Response.php +++ b/src/Response.php @@ -211,6 +211,12 @@ public function writeHead($status = 200, array $headers = array()) } } + // assign date header if no 'date' is given, use the current time where this code is running + if (!isset($lower['date'])) { + // IMF-fixdate = day-name "," SP date1 SP time-of-day SP GMT + $headers['Date'] = gmdate('D, d M Y H:i:s') . ' GMT'; + } + // assign chunked transfer-encoding if no 'content-length' is given for HTTP/1.1 responses if (!isset($lower['content-length']) && $this->protocolVersion === '1.1') { $headers['Transfer-Encoding'] = 'chunked'; diff --git a/tests/ResponseTest.php b/tests/ResponseTest.php index e8633596..b6367d37 100644 --- a/tests/ResponseTest.php +++ b/tests/ResponseTest.php @@ -25,7 +25,7 @@ public function testResponseShouldBeChunkedByDefault() ->with($expected); $response = new Response($conn); - $response->writeHead(); + $response->writeHead(200, array('Date' => array())); } public function testResponseShouldNotBeChunkedWhenProtocolVersionIsNot11() @@ -44,7 +44,7 @@ public function testResponseShouldNotBeChunkedWhenProtocolVersionIsNot11() ->with($expected); $response = new Response($conn, '1.0'); - $response->writeHead(); + $response->writeHead(200, array('Date' => array())); } public function testResponseShouldBeChunkedEvenWithOtherTransferEncoding() @@ -65,7 +65,7 @@ public function testResponseShouldBeChunkedEvenWithOtherTransferEncoding() ->with($expected); $response = new Response($conn); - $response->writeHead(200, array('transfer-encoding' => 'custom')); + $response->writeHead(200, array('transfer-encoding' => 'custom', 'Date' => array())); } public function testResponseShouldNotBeChunkedWithContentLength() @@ -86,7 +86,7 @@ public function testResponseShouldNotBeChunkedWithContentLength() ->with($expected); $response = new Response($conn); - $response->writeHead(200, array('Content-Length' => 22)); + $response->writeHead(200, array('Content-Length' => 22, 'Date' => array())); } public function testResponseShouldNotBeChunkedWithContentLengthCaseInsensitive() @@ -107,7 +107,7 @@ public function testResponseShouldNotBeChunkedWithContentLengthCaseInsensitive() ->with($expected); $response = new Response($conn); - $response->writeHead(200, array('CONTENT-LENGTH' => 0)); + $response->writeHead(200, array('CONTENT-LENGTH' => 0, 'Date' => array())); } public function testResponseShouldIncludeCustomByPoweredAsFirstHeaderIfGivenExplicitly() @@ -128,7 +128,7 @@ public function testResponseShouldIncludeCustomByPoweredAsFirstHeaderIfGivenExpl ->with($expected); $response = new Response($conn); - $response->writeHead(200, array('Content-Length' => 0, 'X-POWERED-BY' => 'demo')); + $response->writeHead(200, array('Content-Length' => 0, 'X-POWERED-BY' => 'demo', 'Date' => array())); } public function testResponseShouldNotIncludePoweredByIfGivenEmptyArray() @@ -148,7 +148,7 @@ public function testResponseShouldNotIncludePoweredByIfGivenEmptyArray() ->with($expected); $response = new Response($conn); - $response->writeHead(200, array('Content-Length' => 0, 'X-Powered-By' => array())); + $response->writeHead(200, array('Content-Length' => 0, 'X-Powered-By' => array(), 'Date' => array())); } public function testResponseShouldAlwaysIncludeConnectionCloseIrrespectiveOfExplicitValue() @@ -169,7 +169,7 @@ public function testResponseShouldAlwaysIncludeConnectionCloseIrrespectiveOfExpl ->with($expected); $response = new Response($conn); - $response->writeHead(200, array('Content-Length' => 0, 'connection' => 'ignored')); + $response->writeHead(200, array('Content-Length' => 0, 'connection' => 'ignored', 'Date' => array())); } /** @expectedException Exception */ @@ -399,7 +399,7 @@ public function shouldRemoveNewlinesFromHeaders() ->with($expected); $response = new Response($conn); - $response->writeHead(200, array("Foo\nBar" => "Baz\rQux")); + $response->writeHead(200, array("Foo\nBar" => "Baz\rQux", 'Date' => array())); } /** @test */ @@ -421,7 +421,7 @@ public function missingStatusCodeTextShouldResultInNumberOnlyStatus() ->with($expected); $response = new Response($conn); - $response->writeHead(700); + $response->writeHead(700, array('Date' => array())); } /** @test */ @@ -445,7 +445,7 @@ public function shouldAllowArrayHeaderValues() ->with($expected); $response = new Response($conn); - $response->writeHead(200, array("Set-Cookie" => array("foo=bar", "bar=baz"))); + $response->writeHead(200, array("Set-Cookie" => array("foo=bar", "bar=baz"), 'Date' => array())); } /** @test */ @@ -467,7 +467,7 @@ public function shouldIgnoreHeadersWithNullValues() ->with($expected); $response = new Response($conn); - $response->writeHead(200, array("FooBar" => null)); + $response->writeHead(200, array("FooBar" => null, 'Date' => array())); } public function testCloseClosesInputAndEmitsCloseEvent() @@ -624,9 +624,77 @@ public function testContentLengthWillBeRemovedIfTransferEncodingIsGiven() 200, array( 'Content-Length' => 4, - 'Transfer-Encoding' => 'chunked' + 'Transfer-Encoding' => 'chunked', + 'Date' => array() ) ); $response->write('hello'); } + + public function testDateHeaderWillUseServerTime() + { + $buffer = ''; + $conn = $this + ->getMockBuilder('React\Socket\ConnectionInterface') + ->getMock(); + $conn + ->expects($this->once()) + ->method('write') + ->will( + $this->returnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } + ) + ); + + $response = new Response($conn); + $response->writeHead(); + + $this->assertContains("HTTP/1.1 200 OK\r\n", $buffer); + $this->assertContains("Date:", $buffer); + } + + public function testDateHeaderWithCustomDate() + { + $expected = ''; + $expected .= "HTTP/1.1 200 OK\r\n"; + $expected .= "X-Powered-By: React/alpha\r\n"; + $expected .= "Date: Tue, 15 Nov 1994 08:12:31 GMT\r\n"; + $expected .= "Transfer-Encoding: chunked\r\n"; + $expected .= "Connection: close\r\n"; + $expected .= "\r\n"; + + $conn = $this + ->getMockBuilder('React\Socket\ConnectionInterface') + ->getMock(); + $conn + ->expects($this->once()) + ->method('write') + ->with($expected); + + $response = new Response($conn); + $response->writeHead(200, array("Date" => "Tue, 15 Nov 1994 08:12:31 GMT")); + } + + public function testDateHeaderWillBeRemoved() + { + $expected = ''; + $expected .= "HTTP/1.1 200 OK\r\n"; + $expected .= "X-Powered-By: React/alpha\r\n"; + $expected .= "Transfer-Encoding: chunked\r\n"; + $expected .= "Connection: close\r\n"; + $expected .= "\r\n"; + + $conn = $this + ->getMockBuilder('React\Socket\ConnectionInterface') + ->getMock(); + $conn + ->expects($this->once()) + ->method('write') + ->with($expected); + + $response = new Response($conn); + $response->writeHead(200, array("Date" => array())); + } } diff --git a/tests/ServerTest.php b/tests/ServerTest.php index cdd5e299..0741417a 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -1335,6 +1335,99 @@ function ($data) use (&$buffer) { $this->assertContains("5\r\nhello\r\n", $buffer); } + public function testDateHeaderWillBeAddedWhenNoneIsGiven() + { + $server = new Server($this->socket); + + $server->on('request', function (Request $request, Response $response) { + $response->writeHead(200); + }); + + $buffer = ''; + $this->connection + ->expects($this->once()) + ->method('write') + ->will( + $this->returnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } + ) + ); + + $this->socket->emit('connection', array($this->connection)); + + $data = $this->createGetRequest(); + + $this->connection->emit('data', array($data)); + + $this->assertContains("HTTP/1.1 200 OK\r\n", $buffer); + $this->assertContains("Date:", $buffer); + $this->assertContains("\r\n\r\n", $buffer); + } + + public function testAddCustomDateHeader() + { + $server = new Server($this->socket); + + $server->on('request', function (Request $request, Response $response) { + $response->writeHead(200, array("Date" => "Tue, 15 Nov 1994 08:12:31 GMT")); + }); + + $buffer = ''; + $this->connection + ->expects($this->once()) + ->method('write') + ->will( + $this->returnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } + ) + ); + + $this->socket->emit('connection', array($this->connection)); + + $data = $this->createGetRequest(); + + $this->connection->emit('data', array($data)); + + $this->assertContains("HTTP/1.1 200 OK\r\n", $buffer); + $this->assertContains("Date: Tue, 15 Nov 1994 08:12:31 GMT\r\n", $buffer); + $this->assertContains("\r\n\r\n", $buffer); + } + + public function testRemoveDateHeader() + { + $server = new Server($this->socket); + + $server->on('request', function (Request $request, Response $response) { + $response->writeHead(200, array('Date' => array())); + }); + + $buffer = ''; + $this->connection + ->expects($this->once()) + ->method('write') + ->will( + $this->returnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } + ) + ); + + $this->socket->emit('connection', array($this->connection)); + + $data = $this->createGetRequest(); + + $this->connection->emit('data', array($data)); + + $this->assertContains("HTTP/1.1 200 OK\r\n", $buffer); + $this->assertNotContains("Date:", $buffer); + $this->assertContains("\r\n\r\n", $buffer); + } + private function createGetRequest() { $data = "GET / HTTP/1.1\r\n"; From 8eb88bab980f221fd10e29d18f059626b44e0939 Mon Sep 17 00:00:00 2001 From: Niels Theen Date: Sat, 4 Mar 2017 03:17:08 +0100 Subject: [PATCH 097/456] Only allow chunked-encoding for requests --- README.md | 6 +++--- src/Server.php | 18 ++++++++++-------- tests/ServerTest.php | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index ddc19fbb..78b411af 100644 --- a/README.md +++ b/README.md @@ -93,9 +93,9 @@ Failing to do so will result in the server parsing the incoming request, but never sending a response back to the client. The `Server` supports both HTTP/1.1 and HTTP/1.0 request messages. -If a client sends an invalid request message or uses an invalid HTTP protocol -version, it will emit an `error` event, send an HTTP error response to the -client and close the connection: +If a client sends an invalid request message, uses an invalid HTTP protocol +version or sends an invalid `Transfer-Encoding` in the request header, it will +emit an `error` event, send an HTTP error response to the client and close the connection: ```php $http->on('error', function (Exception $e) { diff --git a/src/Server.php b/src/Server.php index 4a8d17ca..eb64e934 100644 --- a/src/Server.php +++ b/src/Server.php @@ -143,16 +143,18 @@ public function handleRequest(ConnectionInterface $conn, RequestInterface $reque $contentLength = 0; $stream = new CloseProtectionStream($conn); if ($request->hasHeader('Transfer-Encoding')) { - $transferEncodingHeader = $request->getHeader('Transfer-Encoding'); - // 'chunked' must always be the final value of 'Transfer-Encoding' according to: https://tools.ietf.org/html/rfc7230#section-3.3.1 - if (strtolower(end($transferEncodingHeader)) === 'chunked') { - $stream = new ChunkedDecoder($stream); - $request = $request->withoutHeader('Transfer-Encoding'); - $request = $request->withoutHeader('Content-Length'); - - $contentLength = null; + 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); } + + $stream = new ChunkedDecoder($stream); + + $request = $request->withoutHeader('Transfer-Encoding'); + $request = $request->withoutHeader('Content-Length'); + + $contentLength = null; } elseif ($request->hasHeader('Content-Length')) { $string = $request->getHeaderLine('Content-Length'); diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 0741417a..dac9297b 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -1428,6 +1428,42 @@ function ($data) use (&$buffer) { $this->assertContains("\r\n\r\n", $buffer); } + public function testOnlyChunkedEncodingIsAllowedForTransferEncoding() + { + $error = null; + + $server = new Server($this->socket); + $server->on('request', $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; + } + ) + ); + $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); + } + private function createGetRequest() { $data = "GET / HTTP/1.1\r\n"; From 6769334a6bcad72d9bdf8ce4734b3bc7fd7a4c00 Mon Sep 17 00:00:00 2001 From: Niels Theen Date: Mon, 6 Mar 2017 15:06:43 +0100 Subject: [PATCH 098/456] Add example to handle body data --- README.md | 44 +++++++++++++++++++++--------- examples/03-handling-body-data.php | 34 +++++++++++++++++++++++ 2 files changed, 65 insertions(+), 13 deletions(-) create mode 100644 examples/03-handling-body-data.php diff --git a/README.md b/README.md index ddc19fbb..28ce80c3 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,8 @@ See also [`Request`](#request) and [`Response`](#response) for more details. Failing to do so will result in the server parsing the incoming request, but never sending a response back to the client. +Checkout [Request](#request) for details about the request data body. + The `Server` supports both HTTP/1.1 and HTTP/1.0 request messages. If a client sends an invalid request message or uses an invalid HTTP protocol version, it will emit an `error` event, send an HTTP error response to the @@ -103,19 +105,7 @@ $http->on('error', function (Exception $e) { }); ``` -An `error` event will be emitted for the `Request` if the validation of the body data fails. -This can be e.g. invalid chunked decoded data or an unexpected `end` event. - -```php -$http->on('request', function (Request $request, Response $response) { - $request->on('error', function (\Exception $error) { - echo $error->getMessage(); - }); -}); -``` - -Such an error will `pause` the connection instead of closing it. A response message -can still be sent. +The request object can also emit an error. Checkout [Request](#request) for more details. ### Request @@ -126,6 +116,34 @@ The `Transfer-Encoding` header will be removed. It implements the `ReadableStreamInterface`. +Listen on the `data` event and the `end` event of the [Request](#request) +to evaluate the data of the request body: + +```php +$http->on('request', function (Request $request, Response $response) { + $contentLength = 0; + $request->on('data', function ($data) use (&$contentLength) { + $contentLength += strlen($data); + }); + + $request->on('end', function () use ($response, &$contentLength){ + $response->writeHead(200, array('Content-Type' => 'text/plain')); + $response->end("The length of the submitted request body is: " . $contentLength); + }); + + // an error occures e.g. on invalid chunked encoded data or an unexpected 'end' event + $request->on('error', function (\Exception $exception) use ($response, &$contentLength) { + $response->writeHead(400, array('Content-Type' => 'text/plain')); + $response->end("An error occured while reading at length: " . $contentLength); + }); +}); +``` + +An error will just `pause` the connection instead of closing it. A response message +can still be sent. + +A `close` event will be emitted after an `error` or `end` event. + The constructor is internal, you SHOULD NOT call this yourself. The `Server` is responsible for emitting `Request` and `Response` objects. diff --git a/examples/03-handling-body-data.php b/examples/03-handling-body-data.php new file mode 100644 index 00000000..98b474f7 --- /dev/null +++ b/examples/03-handling-body-data.php @@ -0,0 +1,34 @@ +on('request', function (Request $request, Response $response) { + $contentLength = 0; + $request->on('data', function ($data) use (&$contentLength) { + $contentLength += strlen($data); + }); + + $request->on('end', function () use ($response, &$contentLength){ + $response->writeHead(200, array('Content-Type' => 'text/plain')); + $response->end("The length of the submitted request body is: " . $contentLength); + }); + + // an error occures e.g. on invalid chunked encoded data or an unexpected 'end' event + $request->on('error', function (\Exception $exception) use ($response, &$contentLength) { + $response->writeHead(400, array('Content-Type' => 'text/plain')); + $response->end("An error occured while reading at length: " . $contentLength); + }); +}); + +echo 'Listening on http://' . $socket->getAddress() . PHP_EOL; + +$loop->run(); From 206b3e405c3ce7a697c41d2db0bdaaf17ddd0fab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Wed, 8 Mar 2017 16:24:41 +0100 Subject: [PATCH 099/456] Forward compatibility with Stream v0.5 and upcoming v0.6 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index a7ca133b..43608b90 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ "php": ">=5.3.0", "ringcentral/psr7": "^1.2", "react/socket": "^0.5", - "react/stream": "^0.4.4", + "react/stream": "^0.6 || ^0.5 || ^0.4.4", "evenement/evenement": "^2.0 || ^1.0" }, "autoload": { From b5e7d9bfc9effe8ec723af942c66df042ad17fe6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 9 Mar 2017 15:03:48 +0100 Subject: [PATCH 100/456] Prepare v0.6.0 release --- CHANGELOG.md | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 4 +--- 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c6a1735..d6a21581 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,65 @@ # Changelog +## 0.6.0 (2016-03-09) + +* Feature / BC break: The `Request` and `Response` objects now follow strict + stream semantics and their respective methods and events. + (#116, #129, #133, #135, #136, #137, #138, #140, #141 by @legionth and + #122, #123, #130, #131, #132, #142 by @clue) + + This implies that the `Server` now supports proper detection of the request + message body stream, such as supporting decoding chunked transfer encoding, + delimiting requests with an explicit `Content-Length` header + and those with an empty request message body. + + These streaming semantics are compatible with previous Stream v0.5, future + compatible with v0.5 and upcoming v0.6 versions and can be used like this: + + ```php + $http->on('request', function (Request $request, Response $response) { + $contentLength = 0; + $request->on('data', function ($data) use (&$contentLength) { + $contentLength += strlen($data); + }); + + $request->on('end', function () use ($response, &$contentLength){ + $response->writeHead(200, array('Content-Type' => 'text/plain')); + $response->end("The length of the submitted request body is: " . $contentLength); + }); + + // an error occured + // e.g. on invalid chunked encoded data or an unexpected 'end' event + $request->on('error', function (\Exception $exception) use ($response, &$contentLength) { + $response->writeHead(400, array('Content-Type' => 'text/plain')); + $response->end("An error occured while reading at length: " . $contentLength); + }); + }); + ``` + + Similarly, the `Request` and `Response` now strictly follow the + `close()` method and `close` event semantics. + Closing the `Request` does not interrupt the underlying TCP/IP in + order to allow still sending back a valid response message. + Closing the `Response` does terminate the underlying TCP/IP + connection in order to clean up resources. + + You should make sure to always attach a `request` event listener + like above. The `Server` will not respond to an incoming HTTP + request otherwise and keep the TCP/IP connection pending until the + other side chooses to close the connection. + +* Feature: Support `HTTP/1.1` and `HTTP/1.0` for `Request` and `Response`. + (#124, #125, #126, #127, #128 by @clue and #139 by @legionth) + + The outgoing `Response` will automatically use the same HTTP version as the + incoming `Request` message and will only apply `HTTP/1.1` semantics if + applicable. This includes that the `Response` will automatically attach a + `Date` and `Connection: close` header if applicable. + + This implies that the `Server` now automatically responds with HTTP error + messages for invalid requests (status 400) and those exceeding internal + request header limits (status 431). + ## 0.5.0 (2017-02-16) * Feature / BC break: Change `Request` methods to be in line with PSR-7 diff --git a/README.md b/README.md index 5a58c02b..7301aaa7 100644 --- a/README.md +++ b/README.md @@ -25,8 +25,6 @@ Event-driven, streaming plaintext HTTP and secure HTTPS server for [ReactPHP](ht * [Tests](#tests) * [License](#license) -> Note: This project is in beta stage! Feel free to report any issues you encounter. - ## Quickstart example This is an HTTP server which responds with `Hello World` to every request. @@ -354,7 +352,7 @@ The recommended way to install this library is [through Composer](http://getcomp This will install the latest supported version: ```bash -$ composer require react/http:^0.5 +$ composer require react/http:^0.6 ``` More details about version upgrades can be found in the [CHANGELOG](CHANGELOG.md). From a6b458513b34a2ae8c9a7eb6484a82a481eb487e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 9 Mar 2017 15:07:22 +0100 Subject: [PATCH 101/456] Update CHANGELOG.md --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d6a21581..b3575457 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,8 @@ * Feature / BC break: The `Request` and `Response` objects now follow strict stream semantics and their respective methods and events. - (#116, #129, #133, #135, #136, #137, #138, #140, #141 by @legionth and - #122, #123, #130, #131, #132, #142 by @clue) + (#116, #129, #133, #135, #136, #137, #138, #140, #141 by @legionth + and #122, #123, #130, #131, #132, #142 by @clue) This implies that the `Server` now supports proper detection of the request message body stream, such as supporting decoding chunked transfer encoding, From 7ab915444250c4c204e5e4a42576a06d3481bb25 Mon Sep 17 00:00:00 2001 From: Niels Theen Date: Thu, 9 Mar 2017 11:51:53 +0100 Subject: [PATCH 102/456] Send '100 Continue' response automatically via Server --- README.md | 65 +++++-------------------------------- src/Request.php | 18 ---------- src/Response.php | 59 --------------------------------- src/Server.php | 15 +++++++++ tests/RequestTest.php | 36 -------------------- tests/ResponseTest.php | 61 ---------------------------------- tests/ServerTest.php | 74 ++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 98 insertions(+), 230 deletions(-) diff --git a/README.md b/README.md index 5a58c02b..19f7251a 100644 --- a/README.md +++ b/README.md @@ -17,9 +17,7 @@ Event-driven, streaming plaintext HTTP and secure HTTPS server for [ReactPHP](ht * [getHeader()](#getheader) * [getHeaderLine()](#getheaderline) * [hasHeader()](#hasheader) - * [expectsContinue()](#expectscontinue) * [Response](#response) - * [writeContinue()](#writecontinue) * [writeHead()](#writehead) * [Install](#install) * [Tests](#tests) @@ -86,6 +84,15 @@ $http->on('request', function (Request $request, Response $response) { }); ``` +When HTTP/1.1 clients want to send a bigger request body, they MAY send only +the request headers with an additional `Expect: 100-continue` header and +wait before sending the actual (large) message body. +In this case the server will automatically send an intermediary +`HTTP/1.1 100 Continue` response to the client. +This ensures you will receive the request body without a delay as expected. +The [Response](#response) still needs to be created as described in the +examples above. + See also [`Request`](#request) and [`Response`](#response) for more details. > Note that you SHOULD always listen for the `request` event. @@ -197,18 +204,6 @@ Returns a comma-separated list of all values for this header name or an empty st The `hasHeader(string $name): bool` method can be used to check if a header exists by the given case-insensitive name. -#### expectsContinue() - -The `expectsContinue(): bool` method can be used to -check if the request headers contain the `Expect: 100-continue` header. - -This header MAY be included when an HTTP/1.1 client wants to send a bigger -request body. -See [`writeContinue()`](#writecontinue) for more details. - -This will always be `false` for HTTP/1.0 requests, regardless of what -any header values say. - ### Response The `Response` class is responsible for streaming the outgoing response body. @@ -227,48 +222,6 @@ See [`writeHead()`](#writehead) for more details. See the above usage example and the class outline for details. -#### writeContinue() - -The `writeContinue(): void` method can be used to -send an intermediary `HTTP/1.1 100 continue` response. - -This is a feature that is implemented by *many* HTTP/1.1 clients. -When clients want to send a bigger request body, they MAY send only the request -headers with an additional `Expect: 100-continue` header and wait before -sending the actual (large) message body. - -The server side MAY use this header to verify if the request message is -acceptable by checking the request headers (such as `Content-Length` or HTTP -authentication) and then ask the client to continue with sending the message body. -Otherwise, the server can send a normal HTTP response message and save the -client from transfering the whole body at all. - -This method is mostly useful in combination with the -[`expectsContinue()`](#expectscontinue) method like this: - -```php -$http->on('request', function (Request $request, Response $response) { - if ($request->expectsContinue()) { - $response->writeContinue(); - } - - $response->writeHead(200, array('Content-Type' => 'text/plain')); - $response->end("Hello World!\n"); -}); -``` - -Note that calling this method is strictly optional for HTTP/1.1 responses. -If you do not use it, then a HTTP/1.1 client MUST continue sending the -request body after waiting some time. - -This method MUST NOT be invoked after calling [`writeHead()`](#writehead). -This method MUST NOT be invoked if this is not a HTTP/1.1 response -(please check [`expectsContinue()`](#expectscontinue) as above). -Calling this method after sending the headers or if this is not a HTTP/1.1 -response is an error that will result in an `Exception` -(unless the response has ended/closed already). -Calling this method after the response has ended/closed is a NOOP. - #### writeHead() The `writeHead(int $status = 200, array $headers = array(): void` method can be used to diff --git a/src/Request.php b/src/Request.php index e3559560..05df869e 100644 --- a/src/Request.php +++ b/src/Request.php @@ -149,24 +149,6 @@ public function hasHeader($name) return $this->request->hasHeader($name); } - /** - * Checks if the request headers contain the `Expect: 100-continue` header. - * - * This header MAY be included when an HTTP/1.1 client wants to send a bigger - * request body. - * See [`writeContinue()`] for more details. - * - * This will always be `false` for HTTP/1.0 requests, regardless of what - * any header values say. - * - * @return bool - * @see Response::writeContinue() - */ - public function expectsContinue() - { - return $this->getProtocolVersion() !== '1.0' && '100-continue' === strtolower($this->getHeaderLine('Expect')); - } - public function isReadable() { return $this->readable; diff --git a/src/Response.php b/src/Response.php index e7e34182..5442f7a5 100644 --- a/src/Response.php +++ b/src/Response.php @@ -66,65 +66,6 @@ public function isWritable() return $this->writable; } - /** - * Sends an intermediary `HTTP/1.1 100 continue` response. - * - * This is a feature that is implemented by *many* HTTP/1.1 clients. - * When clients want to send a bigger request body, they MAY send only the request - * headers with an additional `Expect: 100-continue` header and wait before - * sending the actual (large) message body. - * - * The server side MAY use this header to verify if the request message is - * acceptable by checking the request headers (such as `Content-Length` or HTTP - * authentication) and then ask the client to continue with sending the message body. - * Otherwise, the server can send a normal HTTP response message and save the - * client from transfering the whole body at all. - * - * This method is mostly useful in combination with the - * [`expectsContinue()`] method like this: - * - * ```php - * $http->on('request', function (Request $request, Response $response) { - * if ($request->expectsContinue()) { - * $response->writeContinue(); - * } - * - * $response->writeHead(200, array('Content-Type' => 'text/plain')); - * $response->end("Hello World!\n"); - * }); - * ``` - * - * Note that calling this method is strictly optional for HTTP/1.1 responses. - * If you do not use it, then a HTTP/1.1 client MUST continue sending the - * request body after waiting some time. - * - * This method MUST NOT be invoked after calling `writeHead()`. - * This method MUST NOT be invoked if this is not a HTTP/1.1 response - * (please check [`expectsContinue()`] as above). - * Calling this method after sending the headers or if this is not a HTTP/1.1 - * response is an error that will result in an `Exception` - * (unless the response has ended/closed already). - * Calling this method after the response has ended/closed is a NOOP. - * - * @return void - * @throws \Exception - * @see Request::expectsContinue() - */ - public function writeContinue() - { - if (!$this->writable) { - return; - } - if ($this->protocolVersion !== '1.1') { - throw new \Exception('Continue requires a HTTP/1.1 message'); - } - if ($this->headWritten) { - throw new \Exception('Response head has already been written.'); - } - - $this->conn->write("HTTP/1.1 100 Continue\r\n\r\n"); - } - /** * Writes the given HTTP message header. * diff --git a/src/Server.php b/src/Server.php index eb64e934..8a4d7472 100644 --- a/src/Server.php +++ b/src/Server.php @@ -27,6 +27,15 @@ * }); * ``` * + * When HTTP/1.1 clients want to send a bigger request body, they MAY send only + * the request headers with an additional `Expect: 100-continue` header and + * wait before sending the actual (large) message body. + * In this case the server will automatically send an intermediary + * `HTTP/1.1 100 Continue` response to the client. + * This ensures you will receive the request body without a delay as expected. + * The [Response](#response) still needs to be created as described in the + * examples above. + * * See also [`Request`](#request) and [`Response`](#response) for more details. * * > Note that you SHOULD always listen for the `request` event. @@ -43,6 +52,8 @@ * echo 'Error: ' . $e->getMessage() . PHP_EOL; * }); * ``` + * The request object can also emit an error. Checkout [Request](#request) + * for more details. * * @see Request * @see Response @@ -170,6 +181,10 @@ public function handleRequest(ConnectionInterface $conn, RequestInterface $reque $request = new Request($request, $stream); + if ($request->getProtocolVersion() !== '1.0' && '100-continue' === strtolower($request->getHeaderLine('Expect'))) { + $conn->write("HTTP/1.1 100 Continue\r\n\r\n"); + } + // attach remote ip to the request as metadata $request->remoteAddress = trim( parse_url('tcp://' . $conn->getRemoteAddress(), PHP_URL_HOST), diff --git a/tests/RequestTest.php b/tests/RequestTest.php index 3d010b7f..bafb17ac 100644 --- a/tests/RequestTest.php +++ b/tests/RequestTest.php @@ -14,42 +14,6 @@ public function setUp() $this->stream = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); } - /** @test */ - public function expectsContinueShouldBeFalseByDefault() - { - $headers = array(); - $request = new Request(new Psr('GET', '/', $headers, null, '1.1'), $this->stream); - - $this->assertFalse($request->expectsContinue()); - } - - /** @test */ - public function expectsContinueShouldBeTrueIfContinueExpected() - { - $headers = array('Expect' => array('100-continue')); - $request = new Request(new Psr('GET', '/', $headers, null, '1.1'), $this->stream); - - $this->assertTrue($request->expectsContinue()); - } - - /** @test */ - public function expectsContinueShouldBeTrueIfContinueExpectedCaseInsensitive() - { - $headers = array('EXPECT' => array('100-CONTINUE')); - $request = new Request(new Psr('GET', '/', $headers, null, '1.1'), $this->stream); - - $this->assertTrue($request->expectsContinue()); - } - - /** @test */ - public function expectsContinueShouldBeFalseForHttp10() - { - $headers = array('Expect' => array('100-continue')); - $request = new Request(new Psr('GET', '/', $headers, null, '1.0'), $this->stream); - - $this->assertFalse($request->expectsContinue()); - } - public function testEmptyHeader() { $request = new Request(new Psr('GET', '/', array()), $this->stream); diff --git a/tests/ResponseTest.php b/tests/ResponseTest.php index b6367d37..65fc0598 100644 --- a/tests/ResponseTest.php +++ b/tests/ResponseTest.php @@ -330,55 +330,6 @@ public function testResponseBodyShouldSkipEmptyChunks() $response->end(); } - /** @test */ - public function writeContinueShouldSendContinueLineBeforeRealHeaders() - { - $conn = $this - ->getMockBuilder('React\Socket\ConnectionInterface') - ->getMock(); - $conn - ->expects($this->at(3)) - ->method('write') - ->with("HTTP/1.1 100 Continue\r\n\r\n"); - $conn - ->expects($this->at(4)) - ->method('write') - ->with($this->stringContains("HTTP/1.1 200 OK\r\n")); - - $response = new Response($conn); - $response->writeContinue(); - $response->writeHead(); - } - - /** - * @test - * @expectedException Exception - */ - public function writeContinueShouldThrowForHttp10() - { - $conn = $this - ->getMockBuilder('React\Socket\ConnectionInterface') - ->getMock(); - - $response = new Response($conn, '1.0'); - $response->writeContinue(); - } - - /** @expectedException Exception */ - public function testWriteContinueAfterWriteHeadShouldThrowException() - { - $conn = $this - ->getMockBuilder('React\Socket\ConnectionInterface') - ->getMock(); - $conn - ->expects($this->once()) - ->method('write'); - - $response = new Response($conn); - $response->writeHead(); - $response->writeContinue(); - } - /** @test */ public function shouldRemoveNewlinesFromHeaders() { @@ -551,18 +502,6 @@ public function testWriteHeadAfterCloseIsNoOp() $response->writeHead(); } - public function testWriteContinueAfterCloseIsNoOp() - { - $input = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); - $input->expects($this->once())->method('close'); - $input->expects($this->never())->method('write'); - - $response = new Response($input); - $response->close(); - - $response->writeContinue(); - } - public function testEndAfterCloseIsNoOp() { $input = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); diff --git a/tests/ServerTest.php b/tests/ServerTest.php index dac9297b..a742d015 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -1464,6 +1464,80 @@ function ($data) use (&$buffer) { $this->assertInstanceOf('InvalidArgumentException', $error); } + public function test100ContinueRequestWillBeHandled() + { + $server = new Server($this->socket); + $server->on('request', $this->expectCallableOnce()); + + $this->connection + ->expects($this->once()) + ->method('write') + ->with("HTTP/1.1 100 Continue\r\n\r\n"); + + $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 .= "Expect: 100-continue\r\n"; + $data .= "\r\n"; + + $this->connection->emit('data', array($data)); + } + + public function testContinueWontBeSendForHttp10() + { + $server = new Server($this->socket); + $server->on('request', $this->expectCallableOnce()); + + $this->connection + ->expects($this->never()) + ->method('write'); + + $this->socket->emit('connection', array($this->connection)); + + $data = "GET / HTTP/1.0\r\n"; + $data .= "Expect: 100-continue\r\n"; + $data .= "\r\n"; + + $this->connection->emit('data', array($data)); + } + + public function testContinueWithLaterResponse() + { + $server = new Server($this->socket); + $server->on('request', function (Request $request, Response $response) { + $response->writeHead(); + $response->end(); + }); + + + $buffer = ''; + $this->connection + ->expects($this->any()) + ->method('write') + ->will( + $this->returnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } + ) + ); + + $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 .= "Expect: 100-continue\r\n"; + $data .= "\r\n"; + + $this->connection->emit('data', array($data)); + + $this->assertContains("HTTP/1.1 100 Continue\r\n\r\n", $buffer); + $this->assertContains("HTTP/1.1 200 OK\r\n", $buffer); + } + private function createGetRequest() { $data = "GET / HTTP/1.1\r\n"; From 19006b1faab3ca2c6746aebf670b5bbc20058b1b Mon Sep 17 00:00:00 2001 From: Niels Theen Date: Sun, 26 Feb 2017 02:09:16 +0100 Subject: [PATCH 103/456] Use callback function instead of request event --- README.md | 46 +++--- examples/01-hello-world.php | 3 +- examples/02-hello-world-https.php | 3 +- examples/03-handling-body-data.php | 3 +- src/Server.php | 77 +++++++--- tests/ServerTest.php | 217 ++++++++++------------------- 6 files changed, 150 insertions(+), 199 deletions(-) diff --git a/README.md b/README.md index 808389d0..5c8528fd 100644 --- a/README.md +++ b/README.md @@ -31,8 +31,7 @@ This is an HTTP server which responds with `Hello World` to every request. $loop = React\EventLoop\Factory::create(); $socket = new React\Socket\Server(8080, $loop); -$http = new React\Http\Server($socket); -$http->on('request', function (Request $request, Response $response) { +$http = new Server($socket, function (Request $request, Response $response) { $response->writeHead(200, array('Content-Type' => 'text/plain')); $response->end("Hello World!\n"); }); @@ -47,16 +46,23 @@ See also the [examples](examples). ### Server The `Server` class is responsible for handling incoming connections and then -emit a `request` event for each incoming HTTP request. +processing each incoming HTTP request. It attaches itself to an instance of `React\Socket\ServerInterface` which emits underlying streaming connections in order to then parse incoming data -as HTTP: +as HTTP. + +For each request, it executes the callback function passed to the +constructor with the respective [`Request`](#request) and +[`Response`](#response) objects: ```php $socket = new React\Socket\Server(8080, $loop); -$http = new React\Http\Server($socket); +$http = new Server($socket, function (Request $request, Response $response) { + $response->writeHead(200, array('Content-Type' => 'text/plain')); + $response->end("Hello World!\n"); +}); ``` Similarly, you can also attach this to a @@ -64,19 +70,12 @@ Similarly, you can also attach this to a in order to start a secure HTTPS server like this: ```php -$socket = new Server(8080, $loop); -$socket = new SecureServer($socket, $loop, array( +$socket = new React\Socket\Server(8080, $loop); +$socket = new React\Socket\SecureServer($socket, $loop, array( 'local_cert' => __DIR__ . '/localhost.pem' )); -$http = new React\Http\Server($socket); -``` - -For each incoming connection, it emits a `request` event with the respective -[`Request`](#request) and [`Response`](#response) objects: - -```php -$http->on('request', function (Request $request, Response $response) { +$http = new Server($socket, function (Request $request, Response $response) { $response->writeHead(200, array('Content-Type' => 'text/plain')); $response->end("Hello World!\n"); }); @@ -91,18 +90,14 @@ This ensures you will receive the request body without a delay as expected. The [Response](#response) still needs to be created as described in the examples above. -See also [`Request`](#request) and [`Response`](#response) for more details. - -> Note that you SHOULD always listen for the `request` event. -Failing to do so will result in the server parsing the incoming request, -but never sending a response back to the client. - -Checkout [Request](#request) for details about the request data body. +See also [`Request`](#request) and [`Response`](#response) +for more details(e.g. the request data body). The `Server` supports both HTTP/1.1 and HTTP/1.0 request messages. If a client sends an invalid request message, uses an invalid HTTP protocol version or sends an invalid `Transfer-Encoding` in the request header, it will -emit an `error` event, send an HTTP error response to the client and close the connection: +emit an `error` event, send an HTTP error response to the client and +close the connection: ```php $http->on('error', function (Exception $e) { @@ -110,7 +105,8 @@ $http->on('error', function (Exception $e) { }); ``` -The request object can also emit an error. Checkout [Request](#request) for more details. +The request object can also emit an error. Checkout [Request](#request) +for more details. ### Request @@ -125,7 +121,7 @@ Listen on the `data` event and the `end` event of the [Request](#request) to evaluate the data of the request body: ```php -$http->on('request', function (Request $request, Response $response) { +$http = new Server($socket, function (Request $request, Response $response) { $contentLength = 0; $request->on('data', function ($data) use (&$contentLength) { $contentLength += strlen($data); diff --git a/examples/01-hello-world.php b/examples/01-hello-world.php index 424a9c1e..d44634ed 100644 --- a/examples/01-hello-world.php +++ b/examples/01-hello-world.php @@ -10,8 +10,7 @@ $loop = Factory::create(); $socket = new Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); -$server = new \React\Http\Server($socket); -$server->on('request', function (Request $request, Response $response) { +$server = new \React\Http\Server($socket, function (Request $request, Response $response) { $response->writeHead(200, array('Content-Type' => 'text/plain')); $response->end("Hello world!\n"); }); diff --git a/examples/02-hello-world-https.php b/examples/02-hello-world-https.php index c017a196..6ca7e809 100644 --- a/examples/02-hello-world-https.php +++ b/examples/02-hello-world-https.php @@ -14,8 +14,7 @@ 'local_cert' => isset($argv[2]) ? $argv[2] : __DIR__ . '/localhost.pem' )); -$server = new \React\Http\Server($socket); -$server->on('request', function (Request $reques, Response $response) { +$server = new \React\Http\Server($socket, function (Request $request, Response $response) { $response->writeHead(200, array('Content-Type' => 'text/plain')); $response->end("Hello world!\n"); }); diff --git a/examples/03-handling-body-data.php b/examples/03-handling-body-data.php index 98b474f7..a016b66e 100644 --- a/examples/03-handling-body-data.php +++ b/examples/03-handling-body-data.php @@ -10,8 +10,7 @@ $loop = Factory::create(); $socket = new Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); -$server = new \React\Http\Server($socket); -$server->on('request', function (Request $request, Response $response) { +$server = new \React\Http\Server($socket, function (Request $request, Response $response) { $contentLength = 0; $request->on('data', function ($data) use (&$contentLength) { $contentLength += strlen($data); diff --git a/src/Server.php b/src/Server.php index 8a4d7472..e35f833b 100644 --- a/src/Server.php +++ b/src/Server.php @@ -9,19 +9,36 @@ /** * The `Server` class is responsible for handling incoming connections and then - * emit a `request` event for each incoming HTTP request. + * processing each incoming HTTP request. + * + * It attaches itself to an instance of `React\Socket\ServerInterface` which + * emits underlying streaming connections in order to then parse incoming data + * as HTTP. + * + * For each request, it executes the callback function passed to the + * constructor with the respective [`Request`](#request) and + * [`Response`](#response) objects: * * ```php * $socket = new React\Socket\Server(8080, $loop); * - * $http = new React\Http\Server($socket); + * $http = new Server($socket, function (Request $request, Response $response) { + * $response->writeHead(200, array('Content-Type' => 'text/plain')); + * $response->end("Hello World!\n"); + * }); * ``` * - * For each incoming connection, it emits a `request` event with the respective - * [`Request`](#request) and [`Response`](#response) objects: + * Similarly, you can also attach this to a + * [`React\Socket\SecureServer`](https://github.com/reactphp/socket#secureserver) + * in order to start a secure HTTPS server like this: * * ```php - * $http->on('request', function (Request $request, Response $response) { + * $socket = new React\Socket\Server(8080, $loop); + * $socket = new React\Socket\SecureServer($socket, $loop, array( + * 'local_cert' => __DIR__ . '/localhost.pem' + * )); + * + * $http = new Server($socket, function (Request $request, Response $response) { * $response->writeHead(200, array('Content-Type' => 'text/plain')); * $response->end("Hello World!\n"); * }); @@ -36,22 +53,21 @@ * The [Response](#response) still needs to be created as described in the * examples above. * - * See also [`Request`](#request) and [`Response`](#response) for more details. - * - * > Note that you SHOULD always listen for the `request` event. - * Failing to do so will result in the server parsing the incoming request, - * but never sending a response back to the client. + * See also [`Request`](#request) and [`Response`](#response) + * for more details(e.g. the request data body). * * The `Server` supports both HTTP/1.1 and HTTP/1.0 request messages. - * If a client sends an invalid request message or uses an invalid HTTP protocol - * version, it will emit an `error` event, send an HTTP error response to the - * client and close the connection: + * If a client sends an invalid request message, uses an invalid HTTP protocol + * version or sends an invalid `Transfer-Encoding` in the request header, it will + * emit an `error` event, send an HTTP error response to the client and + * close the connection: * * ```php * $http->on('error', function (Exception $e) { * echo 'Error: ' . $e->getMessage() . PHP_EOL; * }); * ``` + * * The request object can also emit an error. Checkout [Request](#request) * for more details. * @@ -60,17 +76,26 @@ */ class Server extends EventEmitter { + private $callback; + /** * Creates a HTTP server that accepts connections from the given socket. * * It attaches itself to an instance of `React\Socket\ServerInterface` which * emits underlying streaming connections in order to then parse incoming data - * as HTTP: + * as HTTP. + * + * For each request, it executes the callback function passed to the + * constructor with the respective [`Request`](#request) and + * [`Response`](#response) objects: * * ```php * $socket = new React\Socket\Server(8080, $loop); * - * $http = new React\Http\Server($socket); + * $http = new Server($socket, function (Request $request, Response $response) { + * $response->writeHead(200, array('Content-Type' => 'text/plain')); + * $response->end("Hello World!\n"); + * }); * ``` * * Similarly, you can also attach this to a @@ -78,19 +103,28 @@ class Server extends EventEmitter * in order to start a secure HTTPS server like this: * * ```php - * $socket = new Server(8080, $loop); - * $socket = new SecureServer($socket, $loop, array( + * $socket = new React\Socket\Server(8080, $loop); + * $socket = new React\Socket\SecureServer($socket, $loop, array( * 'local_cert' => __DIR__ . '/localhost.pem' * )); * - * $http = new React\Http\Server($socket); - * ``` + * $http = new Server($socket, function (Request $request, Response $response) { + * $response->writeHead(200, array('Content-Type' => 'text/plain')); + * $response->end("Hello World!\n"); + * }); + *``` * * @param \React\Socket\ServerInterface $io + * @param callable $callback */ - public function __construct(SocketServerInterface $io) + public function __construct(SocketServerInterface $io, $callback) { + if (!is_callable($callback)) { + throw new \InvalidArgumentException(); + } + $io->on('connection', array($this, 'handleConnection')); + $this->callback = $callback; } /** @internal */ @@ -191,7 +225,8 @@ public function handleRequest(ConnectionInterface $conn, RequestInterface $reque '[]' ); - $this->emit('request', array($request, $response)); + $callback = $this->callback; + $callback($request, $response); if ($contentLength === 0) { // If Body is empty or Content-Length is 0 and won't emit further data, diff --git a/tests/ServerTest.php b/tests/ServerTest.php index a742d015..72cdf5bc 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -36,8 +36,7 @@ public function setUp() public function testRequestEventWillNotBeEmittedForIncompleteHeaders() { - $server = new Server($this->socket); - $server->on('request', $this->expectCallableNever()); + $server = new Server($this->socket, $this->expectCallableNever()); $this->socket->emit('connection', array($this->connection)); @@ -48,8 +47,7 @@ public function testRequestEventWillNotBeEmittedForIncompleteHeaders() public function testRequestEventIsEmitted() { - $server = new Server($this->socket); - $server->on('request', $this->expectCallableOnce()); + $server = new Server($this->socket, $this->expectCallableOnce()); $this->socket->emit('connection', array($this->connection)); @@ -63,8 +61,7 @@ public function testRequestEvent() $requestAssertion = null; $responseAssertion = null; - $server = new Server($this->socket); - $server->on('request', function ($request, $response) use (&$i, &$requestAssertion, &$responseAssertion) { + $server = new Server($this->socket, function ($request, $response) use (&$i, &$requestAssertion, &$responseAssertion) { $i++; $requestAssertion = $request; $responseAssertion = $response; @@ -92,8 +89,7 @@ public function testRequestEvent() public function testRequestPauseWillbeForwardedToConnection() { - $server = new Server($this->socket); - $server->on('request', function (Request $request) { + $server = new Server($this->socket, function (Request $request) { $request->pause(); }); @@ -111,8 +107,8 @@ public function testRequestPauseWillbeForwardedToConnection() public function testRequestResumeWillbeForwardedToConnection() { - $server = new Server($this->socket); - $server->on('request', function (Request $request) { + + $server = new Server($this->socket, function (Request $request) { $request->resume(); }); @@ -125,8 +121,7 @@ public function testRequestResumeWillbeForwardedToConnection() public function testRequestCloseWillPauseConnection() { - $server = new Server($this->socket); - $server->on('request', function (Request $request) { + $server = new Server($this->socket, function (Request $request) { $request->close(); }); @@ -139,8 +134,7 @@ public function testRequestCloseWillPauseConnection() public function testRequestPauseAfterCloseWillNotBeForwarded() { - $server = new Server($this->socket); - $server->on('request', function (Request $request) { + $server = new Server($this->socket, function (Request $request) { $request->close(); $request->pause(); }); @@ -154,8 +148,7 @@ public function testRequestPauseAfterCloseWillNotBeForwarded() public function testRequestResumeAfterCloseWillNotBeForwarded() { - $server = new Server($this->socket); - $server->on('request', function (Request $request) { + $server = new Server($this->socket, function (Request $request) { $request->close(); $request->resume(); }); @@ -172,8 +165,7 @@ public function testRequestEventWithoutBodyWillNotEmitData() { $never = $this->expectCallableNever(); - $server = new Server($this->socket); - $server->on('request', function (Request $request) use ($never) { + $server = new Server($this->socket, function (Request $request) use ($never) { $request->on('data', $never); }); @@ -187,8 +179,7 @@ public function testRequestEventWithSecondDataEventWillEmitBodyData() { $once = $this->expectCallableOnceWith('incomplete'); - $server = new Server($this->socket); - $server->on('request', function (Request $request) use ($once) { + $server = new Server($this->socket, function (Request $request) use ($once) { $request->on('data', $once); }); @@ -207,8 +198,7 @@ public function testRequestEventWithPartialBodyWillEmitData() { $once = $this->expectCallableOnceWith('incomplete'); - $server = new Server($this->socket); - $server->on('request', function (Request $request) use ($once) { + $server = new Server($this->socket, function (Request $request) use ($once) { $request->on('data', $once); }); @@ -228,8 +218,7 @@ public function testRequestEventWithPartialBodyWillEmitData() public function testResponseContainsPoweredByHeader() { - $server = new Server($this->socket); - $server->on('request', function (Request $request, Response $response) { + $server = new Server($this->socket, function (Request $request, Response $response) { $response->writeHead(); $response->end(); }); @@ -257,8 +246,7 @@ function ($data) use (&$buffer) { public function testClosingResponseDoesNotSendAnyData() { - $server = new Server($this->socket); - $server->on('request', function (Request $request, Response $response) { + $server = new Server($this->socket, function (Request $request, Response $response) { $response->close(); }); @@ -274,8 +262,7 @@ public function testClosingResponseDoesNotSendAnyData() public function testResponseContainsSameRequestProtocolVersionAndChunkedBodyForHttp11() { - $server = new Server($this->socket); - $server->on('request', function (Request $request, Response $response) { + $server = new Server($this->socket, function (Request $request, Response $response) { $response->writeHead(); $response->end('bye'); }); @@ -304,8 +291,7 @@ function ($data) use (&$buffer) { public function testResponseContainsSameRequestProtocolVersionAndRawBodyForHttp10() { - $server = new Server($this->socket); - $server->on('request', function (Request $request, Response $response) { + $server = new Server($this->socket, function (Request $request, Response $response) { $response->writeHead(); $response->end('bye'); }); @@ -335,7 +321,7 @@ function ($data) use (&$buffer) { public function testRequestInvalidHttpProtocolVersionWillEmitErrorAndSendErrorResponse() { $error = null; - $server = new Server($this->socket); + $server = new Server($this->socket, $this->expectCallableNever()); $server->on('error', function ($message) use (&$error) { $error = $message; }); @@ -364,32 +350,10 @@ function ($data) use (&$buffer) { $this->assertContains("\r\n\r\nError 505: HTTP Version Not Supported", $buffer); } - public function testServerWithNoRequestListenerDoesNotSendAnythingToConnection() - { - $server = new Server($this->socket); - - $this->connection - ->expects($this->never()) - ->method('write'); - - $this->connection - ->expects($this->never()) - ->method('end'); - - $this->connection - ->expects($this->never()) - ->method('close'); - - $this->socket->emit('connection', array($this->connection)); - - $data = $this->createGetRequest(); - $this->connection->emit('data', array($data)); - } - public function testRequestOverflowWillEmitErrorAndSendErrorResponse() { $error = null; - $server = new Server($this->socket); + $server = new Server($this->socket, $this->expectCallableNever()); $server->on('error', function ($message) use (&$error) { $error = $message; }); @@ -422,7 +386,7 @@ function ($data) use (&$buffer) { public function testRequestInvalidWillEmitErrorAndSendErrorResponse() { $error = null; - $server = new Server($this->socket); + $server = new Server($this->socket, $this->expectCallableNever()); $server->on('error', function ($message) use (&$error) { $error = $message; }); @@ -453,14 +417,12 @@ function ($data) use (&$buffer) { public function testBodyDataWillBeSendViaRequestEvent() { - $server = new Server($this->socket); - $dataEvent = $this->expectCallableOnceWith('hello'); $endEvent = $this->expectCallableOnce(); $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server->on('request', function (Request $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new Server($this->socket, function (Request $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->on('data', $dataEvent); $request->on('end', $endEvent); $request->on('close', $closeEvent); @@ -481,15 +443,13 @@ public function testBodyDataWillBeSendViaRequestEvent() public function testChunkedEncodedRequestWillBeParsedForRequestEvent() { - $server = new Server($this->socket); - $dataEvent = $this->expectCallableOnceWith('hello'); $endEvent = $this->expectCallableOnce(); $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); $requestValidation = null; - $server->on('request', function (Request $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent, &$requestValidation) { + $server = new Server($this->socket, function (Request $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent, &$requestValidation) { $request->on('data', $dataEvent); $request->on('end', $endEvent); $request->on('close', $closeEvent); @@ -514,20 +474,19 @@ public function testChunkedEncodedRequestWillBeParsedForRequestEvent() public function testChunkedEncodedRequestAdditionalDataWontBeEmitted() { - $server = new Server($this->socket); - $dataEvent = $this->expectCallableOnceWith('hello'); $endEvent = $this->expectCallableOnce(); $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server->on('request', function (Request $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new Server($this->socket, function (Request $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->on('data', $dataEvent); $request->on('end', $endEvent); $request->on('close', $closeEvent); $request->on('error', $errorEvent); }); + $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; @@ -544,14 +503,12 @@ public function testChunkedEncodedRequestAdditionalDataWontBeEmitted() public function testEmptyChunkedEncodedRequest() { - $server = new Server($this->socket); - $dataEvent = $this->expectCallableNever(); $endEvent = $this->expectCallableOnce(); $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server->on('request', function (Request $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new Server($this->socket, function (Request $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->on('data', $dataEvent); $request->on('end', $endEvent); $request->on('close', $closeEvent); @@ -572,20 +529,19 @@ public function testEmptyChunkedEncodedRequest() public function testChunkedIsUpperCase() { - $server = new Server($this->socket); - $dataEvent = $this->expectCallableOnceWith('hello'); $endEvent = $this->expectCallableOnce(); $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server->on('request', function (Request $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new Server($this->socket, function (Request $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->on('data', $dataEvent); $request->on('end', $endEvent); $request->on('close', $closeEvent); $request->on('error', $errorEvent); }); + $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; @@ -601,20 +557,19 @@ public function testChunkedIsUpperCase() public function testChunkedIsMixedUpperAndLowerCase() { - $server = new Server($this->socket); - $dataEvent = $this->expectCallableOnceWith('hello'); $endEvent = $this->expectCallableOnce(); $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server->on('request', function (Request $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new Server($this->socket, function (Request $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->on('data', $dataEvent); $request->on('end', $endEvent); $request->on('close', $closeEvent); $request->on('error', $errorEvent); }); + $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; @@ -630,7 +585,7 @@ public function testChunkedIsMixedUpperAndLowerCase() public function testRequestHttp11WithoutHostWillEmitErrorAndSendErrorResponse() { $error = null; - $server = new Server($this->socket); + $server = new Server($this->socket, $this->expectCallableNever()); $server->on('error', function ($message) use (&$error) { $error = $message; }); @@ -662,7 +617,7 @@ function ($data) use (&$buffer) { public function testRequestHttp11WithMalformedHostWillEmitErrorAndSendErrorResponse() { $error = null; - $server = new Server($this->socket); + $server = new Server($this->socket, $this->expectCallableNever()); $server->on('error', function ($message) use (&$error) { $error = $message; }); @@ -694,7 +649,7 @@ function ($data) use (&$buffer) { public function testRequestHttp11WithInvalidHostUriComponentsWillEmitErrorAndSendErrorResponse() { $error = null; - $server = new Server($this->socket); + $server = new Server($this->socket, $this->expectCallableNever()); $server->on('error', function ($message) use (&$error) { $error = $message; }); @@ -725,8 +680,7 @@ function ($data) use (&$buffer) { public function testRequestHttp10WithoutHostEmitsRequestWithNoError() { - $server = new Server($this->socket); - $server->on('request', $this->expectCallableOnce()); + $server = new Server($this->socket, $this->expectCallableOnce()); $server->on('error', $this->expectCallableNever()); $this->socket->emit('connection', array($this->connection)); @@ -737,14 +691,12 @@ public function testRequestHttp10WithoutHostEmitsRequestWithNoError() public function testWontEmitFurtherDataWhenContentLengthIsReached() { - $server = new Server($this->socket); - $dataEvent = $this->expectCallableOnceWith('hello'); $endEvent = $this->expectCallableOnce(); $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server->on('request', function (Request $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new Server($this->socket, function (Request $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->on('data', $dataEvent); $request->on('end', $endEvent); $request->on('close', $closeEvent); @@ -766,20 +718,20 @@ public function testWontEmitFurtherDataWhenContentLengthIsReached() public function testWontEmitFurtherDataWhenContentLengthIsReachedSplitted() { - $server = new Server($this->socket); - $dataEvent = $this->expectCallableOnceWith('hello'); $endEvent = $this->expectCallableOnce(); $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server->on('request', function (Request $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + + $server = new Server($this->socket, function (Request $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->on('data', $dataEvent); $request->on('end', $endEvent); $request->on('close', $closeEvent); $request->on('error', $errorEvent); }); + $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; @@ -798,14 +750,13 @@ public function testWontEmitFurtherDataWhenContentLengthIsReachedSplitted() public function testContentLengthContainsZeroWillEmitEndEvent() { - $server = new Server($this->socket); $dataEvent = $this->expectCallableNever(); $endEvent = $this->expectCallableOnce(); $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server->on('request', function (Request $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new Server($this->socket, function (Request $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->on('data', $dataEvent); $request->on('end', $endEvent); $request->on('close', $closeEvent); @@ -825,14 +776,12 @@ public function testContentLengthContainsZeroWillEmitEndEvent() public function testContentLengthContainsZeroWillEmitEndEventAdditionalDataWillBeIgnored() { - $server = new Server($this->socket); - $dataEvent = $this->expectCallableNever(); $endEvent = $this->expectCallableOnce(); $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server->on('request', function (Request $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new Server($this->socket, function (Request $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->on('data', $dataEvent); $request->on('end', $endEvent); $request->on('close', $closeEvent); @@ -853,14 +802,12 @@ public function testContentLengthContainsZeroWillEmitEndEventAdditionalDataWillB public function testContentLengthContainsZeroWillEmitEndEventAdditionalDataWillBeIgnoredSplitted() { - $server = new Server($this->socket); - $dataEvent = $this->expectCallableNever(); $endEvent = $this->expectCallableOnce(); $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server->on('request', function (Request $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new Server($this->socket, function (Request $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->on('data', $dataEvent); $request->on('end', $endEvent); $request->on('close', $closeEvent); @@ -884,15 +831,13 @@ public function testContentLengthContainsZeroWillEmitEndEventAdditionalDataWillB public function testContentLengthWillBeIgnoredIfTransferEncodingIsSet() { - $server = new Server($this->socket); - $dataEvent = $this->expectCallableOnceWith('hello'); $endEvent = $this->expectCallableOnce(); $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); $requestValidation = null; - $server->on('request', function (Request $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent, &$requestValidation) { + $server = new Server($this->socket, function (Request $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent, &$requestValidation) { $request->on('data', $dataEvent); $request->on('end', $endEvent); $request->on('close', $closeEvent); @@ -922,15 +867,13 @@ public function testContentLengthWillBeIgnoredIfTransferEncodingIsSet() public function testInvalidContentLengthWillBeIgnoreddIfTransferEncodingIsSet() { - $server = new Server($this->socket); - $dataEvent = $this->expectCallableOnceWith('hello'); $endEvent = $this->expectCallableOnce(); $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); $requestValidation = null; - $server->on('request', function (Request $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent, &$requestValidation) { + $server = new Server($this->socket, function (Request $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent, &$requestValidation) { $request->on('data', $dataEvent); $request->on('end', $endEvent); $request->on('close', $closeEvent); @@ -962,8 +905,7 @@ public function testInvalidContentLengthWillBeIgnoreddIfTransferEncodingIsSet() public function testNonIntegerContentLengthValueWillLeadToError() { $error = null; - $server = new Server($this->socket); - $server->on('request', $this->expectCallableNever()); + $server = new Server($this->socket, $this->expectCallableNever()); $server->on('error', function ($message) use (&$error) { $error = $message; }); @@ -999,8 +941,7 @@ function ($data) use (&$buffer) { public function testMultipleIntegerInContentLengthWillLeadToError() { $error = null; - $server = new Server($this->socket); - $server->on('request', $this->expectCallableNever()); + $server = new Server($this->socket, $this->expectCallableNever()); $server->on('error', function ($message) use (&$error) { $error = $message; }); @@ -1036,8 +977,7 @@ function ($data) use (&$buffer) { public function testInvalidChunkHeaderResultsInErrorOnRequestStream() { $errorEvent = $this->expectCallableOnceWith($this->isInstanceOf('Exception')); - $server = new Server($this->socket); - $server->on('request', function ($request, $response) use ($errorEvent){ + $server = new Server($this->socket, function ($request, $response) use ($errorEvent){ $request->on('error', $errorEvent); }); @@ -1059,8 +999,7 @@ public function testInvalidChunkHeaderResultsInErrorOnRequestStream() public function testTooLongChunkHeaderResultsInErrorOnRequestStream() { $errorEvent = $this->expectCallableOnceWith($this->isInstanceOf('Exception')); - $server = new Server($this->socket); - $server->on('request', function ($request, $response) use ($errorEvent){ + $server = new Server($this->socket, function ($request, $response) use ($errorEvent){ $request->on('error', $errorEvent); }); @@ -1084,8 +1023,7 @@ public function testTooLongChunkHeaderResultsInErrorOnRequestStream() public function testTooLongChunkBodyResultsInErrorOnRequestStream() { $errorEvent = $this->expectCallableOnceWith($this->isInstanceOf('Exception')); - $server = new Server($this->socket); - $server->on('request', function ($request, $response) use ($errorEvent){ + $server = new Server($this->socket, function ($request, $response) use ($errorEvent){ $request->on('error', $errorEvent); }); @@ -1107,8 +1045,7 @@ public function testTooLongChunkBodyResultsInErrorOnRequestStream() public function testUnexpectedEndOfConnectionWillResultsInErrorOnRequestStream() { $errorEvent = $this->expectCallableOnceWith($this->isInstanceOf('Exception')); - $server = new Server($this->socket); - $server->on('request', function ($request, $response) use ($errorEvent){ + $server = new Server($this->socket, function ($request, $response) use ($errorEvent){ $request->on('error', $errorEvent); }); @@ -1130,8 +1067,7 @@ public function testUnexpectedEndOfConnectionWillResultsInErrorOnRequestStream() public function testErrorInChunkedDecoderNeverClosesConnection() { - $server = new Server($this->socket); - $server->on('request', $this->expectCallableOnce()); + $server = new Server($this->socket, $this->expectCallableOnce()); $this->connection->expects($this->never())->method('close'); $this->connection->expects($this->once())->method('pause'); @@ -1150,8 +1086,7 @@ public function testErrorInChunkedDecoderNeverClosesConnection() public function testErrorInLengthLimitedStreamNeverClosesConnection() { - $server = new Server($this->socket); - $server->on('request', $this->expectCallableOnce()); + $server = new Server($this->socket, $this->expectCallableOnce()); $this->connection->expects($this->never())->method('close'); $this->connection->expects($this->once())->method('pause'); @@ -1171,8 +1106,7 @@ public function testErrorInLengthLimitedStreamNeverClosesConnection() public function testCloseRequestWillPauseConnection() { - $server = new Server($this->socket); - $server->on('request', function ($request, $response) { + $server = new Server($this->socket, function ($request, $response) { $request->close(); }); @@ -1192,8 +1126,7 @@ public function testEndEventWillBeEmittedOnSimpleRequest() $endEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new Server($this->socket); - $server->on('request', function ($request, $response) use ($dataEvent, $closeEvent, $endEvent, $errorEvent){ + $server = new Server($this->socket, function ($request, $response) use ($dataEvent, $closeEvent, $endEvent, $errorEvent){ $request->on('data', $dataEvent); $request->on('close', $closeEvent); $request->on('end', $endEvent); @@ -1212,14 +1145,12 @@ public function testEndEventWillBeEmittedOnSimpleRequest() public function testRequestWithoutDefinedLengthWillIgnoreDataEvent() { - $server = new Server($this->socket); - $dataEvent = $this->expectCallableNever(); $endEvent = $this->expectCallableOnce(); $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server->on('request', function (Request $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new Server($this->socket, function (Request $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->on('data', $dataEvent); $request->on('end', $endEvent); $request->on('close', $closeEvent); @@ -1236,9 +1167,7 @@ public function testRequestWithoutDefinedLengthWillIgnoreDataEvent() public function testResponseWillBeChunkDecodedByDefault() { - $server = new Server($this->socket); - - $server->on('request', function (Request $request, Response $response) { + $server = new Server($this->socket, function (Request $request, Response $response) { $response->writeHead(); $response->write('hello'); }); @@ -1260,9 +1189,7 @@ public function testResponseWillBeChunkDecodedByDefault() public function testContentLengthWillBeRemovedForResponseStream() { - $server = new Server($this->socket); - - $server->on('request', function (Request $request, Response $response) { + $server = new Server($this->socket, function (Request $request, Response $response) { $response->writeHead( 200, array( @@ -1299,9 +1226,7 @@ function ($data) use (&$buffer) { public function testOnlyAllowChunkedEncoding() { - $server = new Server($this->socket); - - $server->on('request', function (Request $request, Response $response) { + $server = new Server($this->socket, function (Request $request, Response $response) { $response->writeHead( 200, array( @@ -1337,9 +1262,7 @@ function ($data) use (&$buffer) { public function testDateHeaderWillBeAddedWhenNoneIsGiven() { - $server = new Server($this->socket); - - $server->on('request', function (Request $request, Response $response) { + $server = new Server($this->socket, function (Request $request, Response $response) { $response->writeHead(200); }); @@ -1368,9 +1291,7 @@ function ($data) use (&$buffer) { public function testAddCustomDateHeader() { - $server = new Server($this->socket); - - $server->on('request', function (Request $request, Response $response) { + $server = new Server($this->socket, function (Request $request, Response $response) { $response->writeHead(200, array("Date" => "Tue, 15 Nov 1994 08:12:31 GMT")); }); @@ -1399,9 +1320,7 @@ function ($data) use (&$buffer) { public function testRemoveDateHeader() { - $server = new Server($this->socket); - - $server->on('request', function (Request $request, Response $response) { + $server = new Server($this->socket, function (Request $request, Response $response) { $response->writeHead(200, array('Date' => array())); }); @@ -1432,8 +1351,7 @@ public function testOnlyChunkedEncodingIsAllowedForTransferEncoding() { $error = null; - $server = new Server($this->socket); - $server->on('request', $this->expectCallableNever()); + $server = new Server($this->socket, $this->expectCallableNever()); $server->on('error', function ($exception) use (&$error) { $error = $exception; }); @@ -1466,8 +1384,7 @@ function ($data) use (&$buffer) { public function test100ContinueRequestWillBeHandled() { - $server = new Server($this->socket); - $server->on('request', $this->expectCallableOnce()); + $server = new Server($this->socket, $this->expectCallableOnce()); $this->connection ->expects($this->once()) @@ -1487,8 +1404,7 @@ public function test100ContinueRequestWillBeHandled() public function testContinueWontBeSendForHttp10() { - $server = new Server($this->socket); - $server->on('request', $this->expectCallableOnce()); + $server = new Server($this->socket, $this->expectCallableOnce()); $this->connection ->expects($this->never()) @@ -1505,8 +1421,7 @@ public function testContinueWontBeSendForHttp10() public function testContinueWithLaterResponse() { - $server = new Server($this->socket); - $server->on('request', function (Request $request, Response $response) { + $server = new Server($this->socket, function (Request $request, Response $response) { $response->writeHead(); $response->end(); }); @@ -1538,6 +1453,14 @@ function ($data) use (&$buffer) { $this->assertContains("HTTP/1.1 200 OK\r\n", $buffer); } + /** + * @expectedException InvalidArgumentException + */ + public function testInvalidCallbackFunctionLeadsToException() + { + $server = new Server($this->socket, 'invalid'); + } + private function createGetRequest() { $data = "GET / HTTP/1.1\r\n"; From 7ec68c59b98aede386415a9d6289a1ed1f44cca4 Mon Sep 17 00:00:00 2001 From: Niels Theen Date: Tue, 7 Mar 2017 18:21:53 +0100 Subject: [PATCH 104/456] Replace Request class with PSR-7 Request --- examples/01-hello-world.php | 4 +- examples/02-hello-world-https.php | 4 +- examples/03-handling-body-data.php | 10 +- src/HttpBodyStream.php | 173 ++++++++++++++++++++++ src/Request.php | 194 ------------------------- src/Server.php | 2 +- tests/HttpBodyStreamTest.php | 187 ++++++++++++++++++++++++ tests/RequestTest.php | 116 --------------- tests/ServerTest.php | 224 ++++++++++++++--------------- 9 files changed, 481 insertions(+), 433 deletions(-) create mode 100644 src/HttpBodyStream.php delete mode 100644 src/Request.php create mode 100644 tests/HttpBodyStreamTest.php delete mode 100644 tests/RequestTest.php diff --git a/examples/01-hello-world.php b/examples/01-hello-world.php index d44634ed..49c12664 100644 --- a/examples/01-hello-world.php +++ b/examples/01-hello-world.php @@ -2,15 +2,15 @@ use React\EventLoop\Factory; use React\Socket\Server; -use React\Http\Request; use React\Http\Response; +use Psr\Http\Message\RequestInterface; require __DIR__ . '/../vendor/autoload.php'; $loop = Factory::create(); $socket = new Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); -$server = new \React\Http\Server($socket, function (Request $request, Response $response) { +$server = new \React\Http\Server($socket, function (RequestInterface $request, Response $response) { $response->writeHead(200, array('Content-Type' => 'text/plain')); $response->end("Hello world!\n"); }); diff --git a/examples/02-hello-world-https.php b/examples/02-hello-world-https.php index 6ca7e809..29e5aab2 100644 --- a/examples/02-hello-world-https.php +++ b/examples/02-hello-world-https.php @@ -2,9 +2,9 @@ use React\EventLoop\Factory; use React\Socket\Server; -use React\Http\Request; use React\Http\Response; use React\Socket\SecureServer; +use Psr\Http\Message\RequestInterface; require __DIR__ . '/../vendor/autoload.php'; @@ -14,7 +14,7 @@ 'local_cert' => isset($argv[2]) ? $argv[2] : __DIR__ . '/localhost.pem' )); -$server = new \React\Http\Server($socket, function (Request $request, Response $response) { +$server = new \React\Http\Server($socket, function (RequestInterface $request, Response $response) { $response->writeHead(200, array('Content-Type' => 'text/plain')); $response->end("Hello world!\n"); }); diff --git a/examples/03-handling-body-data.php b/examples/03-handling-body-data.php index a016b66e..9bd26c1e 100644 --- a/examples/03-handling-body-data.php +++ b/examples/03-handling-body-data.php @@ -2,27 +2,27 @@ use React\EventLoop\Factory; use React\Socket\Server; -use React\Http\Request; use React\Http\Response; +use Psr\Http\Message\RequestInterface; require __DIR__ . '/../vendor/autoload.php'; $loop = Factory::create(); $socket = new Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); -$server = new \React\Http\Server($socket, function (Request $request, Response $response) { +$server = new \React\Http\Server($socket, function (RequestInterface $request, Response $response) { $contentLength = 0; - $request->on('data', function ($data) use (&$contentLength) { + $request->getBody()->on('data', function ($data) use (&$contentLength) { $contentLength += strlen($data); }); - $request->on('end', function () use ($response, &$contentLength){ + $request->getBody()->on('end', function () use ($response, &$contentLength){ $response->writeHead(200, array('Content-Type' => 'text/plain')); $response->end("The length of the submitted request body is: " . $contentLength); }); // an error occures e.g. on invalid chunked encoded data or an unexpected 'end' event - $request->on('error', function (\Exception $exception) use ($response, &$contentLength) { + $request->getBody()->on('error', function (\Exception $exception) use ($response, &$contentLength) { $response->writeHead(400, array('Content-Type' => 'text/plain')); $response->end("An error occured while reading at length: " . $contentLength); }); diff --git a/src/HttpBodyStream.php b/src/HttpBodyStream.php new file mode 100644 index 00000000..a77622ad --- /dev/null +++ b/src/HttpBodyStream.php @@ -0,0 +1,173 @@ +input = $input; + $this->size = $size; + + $this->input->on('data', array($this, 'handleData')); + $this->input->on('end', array($this, 'handleEnd')); + $this->input->on('error', array($this, 'handleError')); + $this->input->on('close', array($this, 'close')); + } + + public function isReadable() + { + return !$this->closed && $this->input->isReadable(); + } + + public function pause() + { + $this->input->pause(); + } + + public function resume() + { + $this->input->resume(); + } + + 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->input->close(); + + $this->emit('close'); + $this->removeAllListeners(); + } + + public function getSize() + { + return $this->size; + } + + /** @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 null; + } + + /** @internal */ + public function handleData($data) + { + $this->emit('data', array($data)); + } + + /** @internal */ + public function handleError(\Exception $e) + { + $this->emit('error', array($e)); + $this->close(); + } + + /** @internal */ + public function handleEnd() + { + if (!$this->closed) { + $this->emit('end'); + $this->close(); + } + } +} diff --git a/src/Request.php b/src/Request.php deleted file mode 100644 index 05df869e..00000000 --- a/src/Request.php +++ /dev/null @@ -1,194 +0,0 @@ -request = $request; - $this->stream = $stream; - - $that = $this; - // forward data and end events from body stream to request - $stream->on('data', function ($data) use ($that) { - $that->emit('data', array($data)); - }); - $stream->on('end', function () use ($that) { - $that->emit('end'); - }); - $stream->on('error', function ($error) use ($that) { - $that->emit('error', array($error)); - }); - $stream->on('close', array($this, 'close')); - } - - /** - * Returns the request method - * - * @return string - */ - public function getMethod() - { - return $this->request->getMethod(); - } - - /** - * Returns the request path - * - * @return string - */ - public function getPath() - { - return $this->request->getUri()->getPath(); - } - - /** - * Returns an array with all query parameters ($_GET) - * - * @return array - */ - public function getQueryParams() - { - $params = array(); - parse_str($this->request->getUri()->getQuery(), $params); - - return $params; - } - - /** - * Returns the HTTP protocol version (such as "1.0" or "1.1") - * - * @return string - */ - public function getProtocolVersion() - { - return $this->request->getProtocolVersion(); - } - - /** - * Returns an array with ALL headers - * - * The keys represent the header name in the exact case in which they were - * originally specified. The values will be an array of strings for each - * value for the respective header name. - * - * @return array - */ - public function getHeaders() - { - return $this->request->getHeaders(); - } - - /** - * Retrieves a message header value by the given case-insensitive name. - * - * @param string $name - * @return string[] a list of all values for this header name or an empty array if header was not found - */ - public function getHeader($name) - { - return $this->request->getHeader($name); - } - - /** - * Retrieves a comma-separated string of the values for a single header. - * - * @param string $name - * @return string a comma-separated list of all values for this header name or an empty string if header was not found - */ - public function getHeaderLine($name) - { - return $this->request->getHeaderLine($name); - } - - /** - * Checks if a header exists by the given case-insensitive name. - * - * @param string $name - * @return bool - */ - public function hasHeader($name) - { - return $this->request->hasHeader($name); - } - - public function isReadable() - { - return $this->readable; - } - - public function pause() - { - if (!$this->readable) { - return; - } - - $this->stream->pause(); - } - - public function resume() - { - if (!$this->readable) { - return; - } - - $this->stream->resume(); - } - - public function close() - { - if (!$this->readable) { - return; - } - - $this->readable = false; - $this->stream->close(); - - $this->emit('close'); - $this->removeAllListeners(); - } - - public function pipe(WritableStreamInterface $dest, array $options = array()) - { - Util::pipe($this, $dest, $options); - - return $dest; - } -} diff --git a/src/Server.php b/src/Server.php index e35f833b..bd3faebf 100644 --- a/src/Server.php +++ b/src/Server.php @@ -213,7 +213,7 @@ public function handleRequest(ConnectionInterface $conn, RequestInterface $reque $stream = new LengthLimitedStream($stream, $contentLength); } - $request = new Request($request, $stream); + $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"); diff --git a/tests/HttpBodyStreamTest.php b/tests/HttpBodyStreamTest.php new file mode 100644 index 00000000..9817384a --- /dev/null +++ b/tests/HttpBodyStreamTest.php @@ -0,0 +1,187 @@ +input = new ReadableStream(); + $this->bodyStream = new HttpBodyStream($this->input, null); + } + + public function testDataEmit() + { + $this->bodyStream->on('data', $this->expectCallableOnce(array("hello"))); + $this->input->emit('data', array("hello")); + } + + public function testPauseStream() + { + $input = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $input->expects($this->once())->method('pause'); + + $bodyStream = new HttpBodyStream($input, null); + $bodyStream->pause(); + } + + public function testResumeStream() + { + $input = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $input->expects($this->once())->method('pause'); + + $bodyStream = new HttpBodyStream($input, null); + $bodyStream->pause(); + $bodyStream->resume(); + } + + public function testPipeStream() + { + $dest = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + + $ret = $this->bodyStream->pipe($dest); + + $this->assertSame($dest, $ret); + } + + public function testHandleClose() + { + $this->bodyStream->on('close', $this->expectCallableOnce()); + + $this->input->close(); + $this->input->emit('end', array()); + + $this->assertFalse($this->bodyStream->isReadable()); + } + + public function testStopDataEmittingAfterClose() + { + $bodyStream = new HttpBodyStream($this->input, null); + $bodyStream->on('close', $this->expectCallableOnce()); + $this->bodyStream->on('data', $this->expectCallableOnce(array("hello"))); + + $this->input->emit('data', array("hello")); + $bodyStream->close(); + $this->input->emit('data', array("world")); + } + + public function testHandleError() + { + $this->bodyStream->on('error', $this->expectCallableOnce()); + $this->bodyStream->on('close', $this->expectCallableOnce()); + + $this->input->emit('error', array(new \RuntimeException())); + + $this->assertFalse($this->bodyStream->isReadable()); + } + + public function testToString() + { + $this->assertEquals('', $this->bodyStream->__toString()); + } + + public function testDetach() + { + $this->assertEquals(null, $this->bodyStream->detach()); + } + + public function testGetSizeDefault() + { + $this->assertEquals(null, $this->bodyStream->getSize()); + } + + public function testGetSizeCustom() + { + $stream = new HttpBodyStream($this->input, 5); + $this->assertEquals(5, $stream->getSize()); + } + + /** + * @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(''); + } + + public function testGetContents() + { + $this->assertEquals('', $this->bodyStream->getContents()); + } + + public function testGetMetaData() + { + $this->assertEquals(null, $this->bodyStream->getMetadata()); + } + + public function testIsReadable() + { + $this->assertTrue($this->bodyStream->isReadable()); + } + + public function testPause() + { + $this->bodyStream->pause(); + } + + public function testResume() + { + $this->bodyStream->resume(); + } + + /** + * @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/RequestTest.php b/tests/RequestTest.php deleted file mode 100644 index bafb17ac..00000000 --- a/tests/RequestTest.php +++ /dev/null @@ -1,116 +0,0 @@ -stream = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); - } - - public function testEmptyHeader() - { - $request = new Request(new Psr('GET', '/', array()), $this->stream); - - $this->assertEquals(array(), $request->getHeaders()); - $this->assertFalse($request->hasHeader('Test')); - $this->assertEquals(array(), $request->getHeader('Test')); - $this->assertEquals('', $request->getHeaderLine('Test')); - } - - public function testHeaderIsCaseInsensitive() - { - $request = new Request(new Psr('GET', '/', array( - 'TEST' => array('Yes'), - )), $this->stream); - - $this->assertEquals(array('TEST' => array('Yes')), $request->getHeaders()); - $this->assertTrue($request->hasHeader('Test')); - $this->assertEquals(array('Yes'), $request->getHeader('Test')); - $this->assertEquals('Yes', $request->getHeaderLine('Test')); - } - - public function testHeaderWithMultipleValues() - { - $request = new Request(new Psr('GET', '/', array( - 'Test' => array('a', 'b'), - )), $this->stream); - - $this->assertEquals(array('Test' => array('a', 'b')), $request->getHeaders()); - $this->assertTrue($request->hasHeader('Test')); - $this->assertEquals(array('a', 'b'), $request->getHeader('Test')); - $this->assertEquals('a, b', $request->getHeaderLine('Test')); - } - - public function testCloseEmitsCloseEvent() - { - $request = new Request(new Psr('GET', '/'), $this->stream); - - $request->on('close', $this->expectCallableOnce()); - - $request->close(); - } - - public function testCloseMultipleTimesEmitsCloseEventOnce() - { - $request = new Request(new Psr('GET', '/'), $this->stream); - - $request->on('close', $this->expectCallableOnce()); - - $request->close(); - $request->close(); - } - - public function testCloseWillCloseUnderlyingStream() - { - $this->stream->expects($this->once())->method('close'); - - $request = new Request(new Psr('GET', '/'), $this->stream); - - $request->close(); - } - - public function testIsNotReadableAfterClose() - { - $request = new Request(new Psr('GET', '/'), $this->stream); - - $request->close(); - - $this->assertFalse($request->isReadable()); - } - - public function testPauseWillBeForwarded() - { - $this->stream->expects($this->once())->method('pause'); - - $request = new Request(new Psr('GET', '/'), $this->stream); - - $request->pause(); - } - - public function testResumeWillBeForwarded() - { - $this->stream->expects($this->once())->method('resume'); - - $request = new Request(new Psr('GET', '/'), $this->stream); - - $request->resume(); - } - - public function testPipeReturnsDest() - { - $dest = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); - - $request = new Request(new Psr('GET', '/'), $this->stream); - - $ret = $request->pipe($dest); - - $this->assertSame($dest, $ret); - } -} diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 72cdf5bc..40434f18 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -4,7 +4,7 @@ use React\Http\Server; use React\Http\Response; -use React\Http\Request; +use Psr\Http\Message\RequestInterface; class ServerTest extends TestCase { @@ -78,9 +78,8 @@ public function testRequestEvent() $this->connection->emit('data', array($data)); $this->assertSame(1, $i); - $this->assertInstanceOf('React\Http\Request', $requestAssertion); - $this->assertSame('/', $requestAssertion->getPath()); - $this->assertSame(array(), $requestAssertion->getQueryParams()); + $this->assertInstanceOf('RingCentral\Psr7\Request', $requestAssertion); + $this->assertSame('/', $requestAssertion->getUri()->getPath()); $this->assertSame('GET', $requestAssertion->getMethod()); $this->assertSame('127.0.0.1', $requestAssertion->remoteAddress); @@ -89,8 +88,8 @@ public function testRequestEvent() public function testRequestPauseWillbeForwardedToConnection() { - $server = new Server($this->socket, function (Request $request) { - $request->pause(); + $server = new Server($this->socket, function (RequestInterface $request) { + $request->getBody()->pause(); }); $this->connection->expects($this->once())->method('pause'); @@ -107,9 +106,8 @@ public function testRequestPauseWillbeForwardedToConnection() public function testRequestResumeWillbeForwardedToConnection() { - - $server = new Server($this->socket, function (Request $request) { - $request->resume(); + $server = new Server($this->socket, function (RequestInterface $request) { + $request->getBody()->resume(); }); $this->connection->expects($this->once())->method('resume'); @@ -121,8 +119,8 @@ public function testRequestResumeWillbeForwardedToConnection() public function testRequestCloseWillPauseConnection() { - $server = new Server($this->socket, function (Request $request) { - $request->close(); + $server = new Server($this->socket, function (RequestInterface $request) { + $request->getBody()->close(); }); $this->connection->expects($this->once())->method('pause'); @@ -134,9 +132,9 @@ public function testRequestCloseWillPauseConnection() public function testRequestPauseAfterCloseWillNotBeForwarded() { - $server = new Server($this->socket, function (Request $request) { - $request->close(); - $request->pause(); + $server = new Server($this->socket, function (RequestInterface $request) { + $request->getBody()->close(); + $request->getBody()->pause(); }); $this->connection->expects($this->once())->method('pause'); @@ -148,9 +146,9 @@ public function testRequestPauseAfterCloseWillNotBeForwarded() public function testRequestResumeAfterCloseWillNotBeForwarded() { - $server = new Server($this->socket, function (Request $request) { - $request->close(); - $request->resume(); + $server = new Server($this->socket, function (RequestInterface $request) { + $request->getBody()->close(); + $request->getBody()->resume(); }); $this->connection->expects($this->once())->method('pause'); @@ -165,8 +163,8 @@ public function testRequestEventWithoutBodyWillNotEmitData() { $never = $this->expectCallableNever(); - $server = new Server($this->socket, function (Request $request) use ($never) { - $request->on('data', $never); + $server = new Server($this->socket, function (RequestInterface $request) use ($never) { + $request->getBody()->on('data', $never); }); $this->socket->emit('connection', array($this->connection)); @@ -179,8 +177,8 @@ public function testRequestEventWithSecondDataEventWillEmitBodyData() { $once = $this->expectCallableOnceWith('incomplete'); - $server = new Server($this->socket, function (Request $request) use ($once) { - $request->on('data', $once); + $server = new Server($this->socket, function (RequestInterface $request) use ($once) { + $request->getBody()->on('data', $once); }); $this->socket->emit('connection', array($this->connection)); @@ -198,8 +196,8 @@ public function testRequestEventWithPartialBodyWillEmitData() { $once = $this->expectCallableOnceWith('incomplete'); - $server = new Server($this->socket, function (Request $request) use ($once) { - $request->on('data', $once); + $server = new Server($this->socket, function (RequestInterface $request) use ($once) { + $request->getBody()->on('data', $once); }); $this->socket->emit('connection', array($this->connection)); @@ -218,7 +216,7 @@ public function testRequestEventWithPartialBodyWillEmitData() public function testResponseContainsPoweredByHeader() { - $server = new Server($this->socket, function (Request $request, Response $response) { + $server = new Server($this->socket, function (RequestInterface $request, Response $response) { $response->writeHead(); $response->end(); }); @@ -246,7 +244,7 @@ function ($data) use (&$buffer) { public function testClosingResponseDoesNotSendAnyData() { - $server = new Server($this->socket, function (Request $request, Response $response) { + $server = new Server($this->socket, function (RequestInterface $request, Response $response) { $response->close(); }); @@ -262,7 +260,7 @@ public function testClosingResponseDoesNotSendAnyData() public function testResponseContainsSameRequestProtocolVersionAndChunkedBodyForHttp11() { - $server = new Server($this->socket, function (Request $request, Response $response) { + $server = new Server($this->socket, function (RequestInterface $request, Response $response) { $response->writeHead(); $response->end('bye'); }); @@ -291,7 +289,7 @@ function ($data) use (&$buffer) { public function testResponseContainsSameRequestProtocolVersionAndRawBodyForHttp10() { - $server = new Server($this->socket, function (Request $request, Response $response) { + $server = new Server($this->socket, function (RequestInterface $request, Response $response) { $response->writeHead(); $response->end('bye'); }); @@ -422,11 +420,11 @@ public function testBodyDataWillBeSendViaRequestEvent() $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new Server($this->socket, function (Request $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { - $request->on('data', $dataEvent); - $request->on('end', $endEvent); - $request->on('close', $closeEvent); - $request->on('error', $errorEvent); + $server = new Server($this->socket, function (RequestInterface $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $request->getBody()->on('data', $dataEvent); + $request->getBody()->on('end', $endEvent); + $request->getBody()->on('close', $closeEvent); + $request->getBody()->on('error', $errorEvent); }); $this->socket->emit('connection', array($this->connection)); @@ -449,11 +447,11 @@ public function testChunkedEncodedRequestWillBeParsedForRequestEvent() $errorEvent = $this->expectCallableNever(); $requestValidation = null; - $server = new Server($this->socket, function (Request $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent, &$requestValidation) { - $request->on('data', $dataEvent); - $request->on('end', $endEvent); - $request->on('close', $closeEvent); - $request->on('error', $errorEvent); + $server = new Server($this->socket, function (RequestInterface $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent, &$requestValidation) { + $request->getBody()->on('data', $dataEvent); + $request->getBody()->on('end', $endEvent); + $request->getBody()->on('close', $closeEvent); + $request->getBody()->on('error', $errorEvent); $requestValidation = $request; }); @@ -479,11 +477,11 @@ public function testChunkedEncodedRequestAdditionalDataWontBeEmitted() $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new Server($this->socket, function (Request $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { - $request->on('data', $dataEvent); - $request->on('end', $endEvent); - $request->on('close', $closeEvent); - $request->on('error', $errorEvent); + $server = new Server($this->socket, function (RequestInterface $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $request->getBody()->on('data', $dataEvent); + $request->getBody()->on('end', $endEvent); + $request->getBody()->on('close', $closeEvent); + $request->getBody()->on('error', $errorEvent); }); @@ -508,11 +506,11 @@ public function testEmptyChunkedEncodedRequest() $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new Server($this->socket, function (Request $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { - $request->on('data', $dataEvent); - $request->on('end', $endEvent); - $request->on('close', $closeEvent); - $request->on('error', $errorEvent); + $server = new Server($this->socket, function (RequestInterface $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $request->getBody()->on('data', $dataEvent); + $request->getBody()->on('end', $endEvent); + $request->getBody()->on('close', $closeEvent); + $request->getBody()->on('error', $errorEvent); }); $this->socket->emit('connection', array($this->connection)); @@ -534,11 +532,11 @@ public function testChunkedIsUpperCase() $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new Server($this->socket, function (Request $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { - $request->on('data', $dataEvent); - $request->on('end', $endEvent); - $request->on('close', $closeEvent); - $request->on('error', $errorEvent); + $server = new Server($this->socket, function (RequestInterface $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $request->getBody()->on('data', $dataEvent); + $request->getBody()->on('end', $endEvent); + $request->getBody()->on('close', $closeEvent); + $request->getBody()->on('error', $errorEvent); }); @@ -562,11 +560,11 @@ public function testChunkedIsMixedUpperAndLowerCase() $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new Server($this->socket, function (Request $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { - $request->on('data', $dataEvent); - $request->on('end', $endEvent); - $request->on('close', $closeEvent); - $request->on('error', $errorEvent); + $server = new Server($this->socket, function (RequestInterface $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $request->getBody()->on('data', $dataEvent); + $request->getBody()->on('end', $endEvent); + $request->getBody()->on('close', $closeEvent); + $request->getBody()->on('error', $errorEvent); }); @@ -696,11 +694,11 @@ public function testWontEmitFurtherDataWhenContentLengthIsReached() $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new Server($this->socket, function (Request $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { - $request->on('data', $dataEvent); - $request->on('end', $endEvent); - $request->on('close', $closeEvent); - $request->on('error', $errorEvent); + $server = new Server($this->socket, function (RequestInterface $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $request->getBody()->on('data', $dataEvent); + $request->getBody()->on('end', $endEvent); + $request->getBody()->on('close', $closeEvent); + $request->getBody()->on('error', $errorEvent); }); $this->socket->emit('connection', array($this->connection)); @@ -724,11 +722,11 @@ public function testWontEmitFurtherDataWhenContentLengthIsReachedSplitted() $errorEvent = $this->expectCallableNever(); - $server = new Server($this->socket, function (Request $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { - $request->on('data', $dataEvent); - $request->on('end', $endEvent); - $request->on('close', $closeEvent); - $request->on('error', $errorEvent); + $server = new Server($this->socket, function (RequestInterface $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $request->getBody()->on('data', $dataEvent); + $request->getBody()->on('end', $endEvent); + $request->getBody()->on('close', $closeEvent); + $request->getBody()->on('error', $errorEvent); }); @@ -756,11 +754,11 @@ public function testContentLengthContainsZeroWillEmitEndEvent() $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new Server($this->socket, function (Request $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { - $request->on('data', $dataEvent); - $request->on('end', $endEvent); - $request->on('close', $closeEvent); - $request->on('error', $errorEvent); + $server = new Server($this->socket, function (RequestInterface $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $request->getBody()->on('data', $dataEvent); + $request->getBody()->on('end', $endEvent); + $request->getBody()->on('close', $closeEvent); + $request->getBody()->on('error', $errorEvent); }); $this->socket->emit('connection', array($this->connection)); @@ -781,11 +779,11 @@ public function testContentLengthContainsZeroWillEmitEndEventAdditionalDataWillB $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new Server($this->socket, function (Request $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { - $request->on('data', $dataEvent); - $request->on('end', $endEvent); - $request->on('close', $closeEvent); - $request->on('error', $errorEvent); + $server = new Server($this->socket, function (RequestInterface $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $request->getBody()->on('data', $dataEvent); + $request->getBody()->on('end', $endEvent); + $request->getBody()->on('close', $closeEvent); + $request->getBody()->on('error', $errorEvent); }); $this->socket->emit('connection', array($this->connection)); @@ -807,11 +805,11 @@ public function testContentLengthContainsZeroWillEmitEndEventAdditionalDataWillB $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new Server($this->socket, function (Request $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { - $request->on('data', $dataEvent); - $request->on('end', $endEvent); - $request->on('close', $closeEvent); - $request->on('error', $errorEvent); + $server = new Server($this->socket, function (RequestInterface $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $request->getBody()->on('data', $dataEvent); + $request->getBody()->on('end', $endEvent); + $request->getBody()->on('close', $closeEvent); + $request->getBody()->on('error', $errorEvent); }); $this->socket->emit('connection', array($this->connection)); @@ -837,11 +835,11 @@ public function testContentLengthWillBeIgnoredIfTransferEncodingIsSet() $errorEvent = $this->expectCallableNever(); $requestValidation = null; - $server = new Server($this->socket, function (Request $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent, &$requestValidation) { - $request->on('data', $dataEvent); - $request->on('end', $endEvent); - $request->on('close', $closeEvent); - $request->on('error', $errorEvent); + $server = new Server($this->socket, function (RequestInterface $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent, &$requestValidation) { + $request->getBody()->on('data', $dataEvent); + $request->getBody()->on('end', $endEvent); + $request->getBody()->on('close', $closeEvent); + $request->getBody()->on('error', $errorEvent); $requestValidation = $request; }); @@ -873,11 +871,11 @@ public function testInvalidContentLengthWillBeIgnoreddIfTransferEncodingIsSet() $errorEvent = $this->expectCallableNever(); $requestValidation = null; - $server = new Server($this->socket, function (Request $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent, &$requestValidation) { - $request->on('data', $dataEvent); - $request->on('end', $endEvent); - $request->on('close', $closeEvent); - $request->on('error', $errorEvent); + $server = new Server($this->socket, function (RequestInterface $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent, &$requestValidation) { + $request->getBody()->on('data', $dataEvent); + $request->getBody()->on('end', $endEvent); + $request->getBody()->on('close', $closeEvent); + $request->getBody()->on('error', $errorEvent); $requestValidation = $request; }); @@ -978,7 +976,7 @@ public function testInvalidChunkHeaderResultsInErrorOnRequestStream() { $errorEvent = $this->expectCallableOnceWith($this->isInstanceOf('Exception')); $server = new Server($this->socket, function ($request, $response) use ($errorEvent){ - $request->on('error', $errorEvent); + $request->getBody()->on('error', $errorEvent); }); $this->connection->expects($this->never())->method('close'); @@ -1000,7 +998,7 @@ public function testTooLongChunkHeaderResultsInErrorOnRequestStream() { $errorEvent = $this->expectCallableOnceWith($this->isInstanceOf('Exception')); $server = new Server($this->socket, function ($request, $response) use ($errorEvent){ - $request->on('error', $errorEvent); + $request->getBody()->on('error', $errorEvent); }); $this->connection->expects($this->never())->method('close'); @@ -1024,7 +1022,7 @@ public function testTooLongChunkBodyResultsInErrorOnRequestStream() { $errorEvent = $this->expectCallableOnceWith($this->isInstanceOf('Exception')); $server = new Server($this->socket, function ($request, $response) use ($errorEvent){ - $request->on('error', $errorEvent); + $request->getBody()->on('error', $errorEvent); }); $this->connection->expects($this->never())->method('close'); @@ -1046,7 +1044,7 @@ public function testUnexpectedEndOfConnectionWillResultsInErrorOnRequestStream() { $errorEvent = $this->expectCallableOnceWith($this->isInstanceOf('Exception')); $server = new Server($this->socket, function ($request, $response) use ($errorEvent){ - $request->on('error', $errorEvent); + $request->getBody()->on('error', $errorEvent); }); $this->connection->expects($this->never())->method('close'); @@ -1107,7 +1105,7 @@ public function testErrorInLengthLimitedStreamNeverClosesConnection() public function testCloseRequestWillPauseConnection() { $server = new Server($this->socket, function ($request, $response) { - $request->close(); + $request->getBody()->close(); }); $this->connection->expects($this->never())->method('close'); @@ -1127,10 +1125,10 @@ public function testEndEventWillBeEmittedOnSimpleRequest() $errorEvent = $this->expectCallableNever(); $server = new Server($this->socket, function ($request, $response) use ($dataEvent, $closeEvent, $endEvent, $errorEvent){ - $request->on('data', $dataEvent); - $request->on('close', $closeEvent); - $request->on('end', $endEvent); - $request->on('error', $errorEvent); + $request->getBody()->on('data', $dataEvent); + $request->getBody()->on('close', $closeEvent); + $request->getBody()->on('end', $endEvent); + $request->getBody()->on('error', $errorEvent); }); $this->connection->expects($this->once())->method('pause'); @@ -1150,11 +1148,11 @@ public function testRequestWithoutDefinedLengthWillIgnoreDataEvent() $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new Server($this->socket, function (Request $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { - $request->on('data', $dataEvent); - $request->on('end', $endEvent); - $request->on('close', $closeEvent); - $request->on('error', $errorEvent); + $server = new Server($this->socket, function (RequestInterface $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $request->getBody()->on('data', $dataEvent); + $request->getBody()->on('end', $endEvent); + $request->getBody()->on('close', $closeEvent); + $request->getBody()->on('error', $errorEvent); }); $this->socket->emit('connection', array($this->connection)); @@ -1167,7 +1165,7 @@ public function testRequestWithoutDefinedLengthWillIgnoreDataEvent() public function testResponseWillBeChunkDecodedByDefault() { - $server = new Server($this->socket, function (Request $request, Response $response) { + $server = new Server($this->socket, function (RequestInterface $request, Response $response) { $response->writeHead(); $response->write('hello'); }); @@ -1189,7 +1187,7 @@ public function testResponseWillBeChunkDecodedByDefault() public function testContentLengthWillBeRemovedForResponseStream() { - $server = new Server($this->socket, function (Request $request, Response $response) { + $server = new Server($this->socket, function (RequestInterface $request, Response $response) { $response->writeHead( 200, array( @@ -1226,7 +1224,7 @@ function ($data) use (&$buffer) { public function testOnlyAllowChunkedEncoding() { - $server = new Server($this->socket, function (Request $request, Response $response) { + $server = new Server($this->socket, function (RequestInterface $request, Response $response) { $response->writeHead( 200, array( @@ -1262,7 +1260,7 @@ function ($data) use (&$buffer) { public function testDateHeaderWillBeAddedWhenNoneIsGiven() { - $server = new Server($this->socket, function (Request $request, Response $response) { + $server = new Server($this->socket, function (RequestInterface $request, Response $response) { $response->writeHead(200); }); @@ -1291,7 +1289,7 @@ function ($data) use (&$buffer) { public function testAddCustomDateHeader() { - $server = new Server($this->socket, function (Request $request, Response $response) { + $server = new Server($this->socket, function (RequestInterface $request, Response $response) { $response->writeHead(200, array("Date" => "Tue, 15 Nov 1994 08:12:31 GMT")); }); @@ -1320,7 +1318,7 @@ function ($data) use (&$buffer) { public function testRemoveDateHeader() { - $server = new Server($this->socket, function (Request $request, Response $response) { + $server = new Server($this->socket, function (RequestInterface $request, Response $response) { $response->writeHead(200, array('Date' => array())); }); @@ -1421,7 +1419,7 @@ public function testContinueWontBeSendForHttp10() public function testContinueWithLaterResponse() { - $server = new Server($this->socket, function (Request $request, Response $response) { + $server = new Server($this->socket, function (RequestInterface $request, Response $response) { $response->writeHead(); $response->end(); }); From 9bfd088c79ffe8aed7aaa9f47ae98fcc93bb1110 Mon Sep 17 00:00:00 2001 From: Niels Theen Date: Mon, 20 Mar 2017 14:30:50 +0100 Subject: [PATCH 105/456] Add tests to test case sensitivity --- tests/ChunkedDecoderTest.php | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests/ChunkedDecoderTest.php b/tests/ChunkedDecoderTest.php index 82806a8a..7f675f42 100644 --- a/tests/ChunkedDecoderTest.php +++ b/tests/ChunkedDecoderTest.php @@ -447,4 +447,36 @@ public function testEmptyHeaderAndFilledBodyLeadsToError() $this->input->emit('data', array("\r\nhello\r\n")); } + + public function testUpperCaseHexWillBeHandled() + { + $this->parser->on('data', $this->expectCallableOnceWith('0123456790')); + $this->parser->on('error', $this->expectCallableNever()); + $this->parser->on('end', $this->expectCallableNever()); + $this->parser->on('close', $this->expectCallableNever()); + + $this->input->emit('data', array("A\r\n0123456790\r\n")); + } + + public function testLowerCaseHexWillBeHandled() + { + $this->parser->on('data', $this->expectCallableOnceWith('0123456790')); + $this->parser->on('error', $this->expectCallableNever()); + $this->parser->on('end', $this->expectCallableNever()); + $this->parser->on('close', $this->expectCallableNever()); + + $this->input->emit('data', array("a\r\n0123456790\r\n")); + } + + public function testMixedUpperAndLowerCaseHexValuesInHeaderWillBeHandled() + { + $data = str_repeat('1', (int)hexdec('AA')); + + $this->parser->on('data', $this->expectCallableOnceWith($data)); + $this->parser->on('error', $this->expectCallableNever()); + $this->parser->on('end', $this->expectCallableNever()); + $this->parser->on('close', $this->expectCallableNever()); + + $this->input->emit('data', array("aA\r\n" . $data . "\r\n")); + } } From a585e5ddfbd066dbf17701b107a571ca2abf6f88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 23 Mar 2017 21:10:11 +0100 Subject: [PATCH 106/456] Update documentation for RequestInterface --- README.md | 178 +++++++++++++++++++++++++++---------------------- src/Server.php | 4 +- 2 files changed, 101 insertions(+), 81 deletions(-) diff --git a/README.md b/README.md index 5c8528fd..38117f22 100644 --- a/README.md +++ b/README.md @@ -10,13 +10,6 @@ Event-driven, streaming plaintext HTTP and secure HTTPS server for [ReactPHP](ht * [Usage](#usage) * [Server](#server) * [Request](#request) - * [getMethod()](#getmethod) - * [getQueryParams()](#getqueryparams) - * [getProtocolVersion()](#getprotocolversion) - * [getHeaders()](#getheaders) - * [getHeader()](#getheader) - * [getHeaderLine()](#getheaderline) - * [hasHeader()](#hasheader) * [Response](#response) * [writeHead()](#writehead) * [Install](#install) @@ -31,7 +24,7 @@ This is an HTTP server which responds with `Hello World` to every request. $loop = React\EventLoop\Factory::create(); $socket = new React\Socket\Server(8080, $loop); -$http = new Server($socket, function (Request $request, Response $response) { +$http = new Server($socket, function (RequestInterface $request, Response $response) { $response->writeHead(200, array('Content-Type' => 'text/plain')); $response->end("Hello World!\n"); }); @@ -53,13 +46,13 @@ emits underlying streaming connections in order to then parse incoming data as HTTP. For each request, it executes the callback function passed to the -constructor with the respective [`Request`](#request) and -[`Response`](#response) objects: +constructor with the respective [request](#request) and +[response](#response) objects: ```php $socket = new React\Socket\Server(8080, $loop); -$http = new Server($socket, function (Request $request, Response $response) { +$http = new Server($socket, function (RequestInterface $request, Response $response) { $response->writeHead(200, array('Content-Type' => 'text/plain')); $response->end("Hello World!\n"); }); @@ -75,7 +68,7 @@ $socket = new React\Socket\SecureServer($socket, $loop, array( 'local_cert' => __DIR__ . '/localhost.pem' )); -$http = new Server($socket, function (Request $request, Response $response) { +$http = new Server($socket, function (RequestInterface $request, Response $response) { $response->writeHead(200, array('Content-Type' => 'text/plain')); $response->end("Hello World!\n"); }); @@ -90,8 +83,8 @@ This ensures you will receive the request body without a delay as expected. The [Response](#response) still needs to be created as described in the examples above. -See also [`Request`](#request) and [`Response`](#response) -for more details(e.g. the request data body). +See also [request](#request) and [response](#response) +for more details (e.g. the request data body). The `Server` supports both HTTP/1.1 and HTTP/1.0 request messages. If a client sends an invalid request message, uses an invalid HTTP protocol @@ -105,98 +98,125 @@ $http->on('error', function (Exception $e) { }); ``` -The request object can also emit an error. Checkout [Request](#request) -for more details. +Note that the request object can also emit an error. +Check out [request](#request) for more details. ### Request -The `Request` class is responsible for streaming the incoming request body -and contains meta data which was parsed from the request headers. -If the request body is chunked-encoded, the data will be decoded and emitted on the data event. -The `Transfer-Encoding` header will be removed. +An seen above, the `Server` class is responsible for handling incoming +connections and then processing each incoming HTTP request. -It implements the `ReadableStreamInterface`. +The request object will be processed once the request headers have +been received by the client. +This request object implements the +[PSR-7 RequestInterface](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-7-http-message.md#32-psrhttpmessagerequestinterface) +and will be passed to the callback function like this. -Listen on the `data` event and the `end` event of the [Request](#request) -to evaluate the data of the request body: + ```php +$http = new Server($socket, function (RequestInterface $request, Response $response) { + $response->writeHead(200, array('Content-Type' => 'text/plain')); + $response->write("The method of the request is: " . $request->getMethod()); + $response->end("The requested path is: " . $request->getUri()->getPath()); +}); +``` + +For more details about the request object, check out the documentation of +[PSR-7 RequestInterface](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-7-http-message.md#32-psrhttpmessagerequestinterface). + +Note that the request object will be processed once the request headers have +been received. +This means that this happens irrespective of (i.e. *before*) receiving the +(potentially much larger) request body. +While this may be uncommon in the PHP ecosystem, this is actually a very powerful +approach that gives you several advantages not otherwise possible: + +* React to requests *before* receiving a large request body, + such as rejecting an unauthenticated request or one that exceeds allowed + message lengths (file uploads). +* Start processing parts of the request body before the remainder of the request + body arrives or if the sender is slowly streaming data. +* Process a large request body without having to buffer anything in memory, + such as accepting a huge file upload or possibly unlimited request body stream. + +The `getBody()` method can be used to access the request body stream. +This method returns a stream instance that implements both the +[PSR-7 StreamInterface](http://www.php-fig.org/psr/psr-7/#psrhttpmessagestreaminterface) +and the [ReactPHP ReadableStreamInterface](https://github.com/reactphp/stream#readablestreaminterface). +However, most of the `PSR-7 StreamInterface` methods have been +designed under the assumption of being in control of the request body. +Given that this does not apply to this server, the following +`PSR-7 StreamInterface` methods are not used and SHOULD NOT be called: +`tell()`, `eof()`, `seek()`, `rewind()`, `write()` and `read()`. +Instead, you should use the `ReactPHP ReadableStreamInterface` which +gives you access to the incoming request body as the individual chunks arrive: ```php -$http = new Server($socket, function (Request $request, Response $response) { +$http = new Server($socket, function (RequestInterface $request, Response $response) { $contentLength = 0; - $request->on('data', function ($data) use (&$contentLength) { + $body = $request->getBody(); + $body->on('data', function ($data) use (&$contentLength) { $contentLength += strlen($data); }); - $request->on('end', function () use ($response, &$contentLength){ + $body->on('end', function () use ($response, &$contentLength){ $response->writeHead(200, array('Content-Type' => 'text/plain')); $response->end("The length of the submitted request body is: " . $contentLength); }); // an error occures e.g. on invalid chunked encoded data or an unexpected 'end' event - $request->on('error', function (\Exception $exception) use ($response, &$contentLength) { + $body->on('error', function (\Exception $exception) use ($response, &$contentLength) { $response->writeHead(400, array('Content-Type' => 'text/plain')); $response->end("An error occured while reading at length: " . $contentLength); }); }); ``` -An error will just `pause` the connection instead of closing it. A response message -can still be sent. - -A `close` event will be emitted after an `error` or `end` event. - -The constructor is internal, you SHOULD NOT call this yourself. -The `Server` is responsible for emitting `Request` and `Response` objects. - -See the above usage example and the class outline for details. - -#### getMethod() - -The `getMethod(): string` method can be used to -return the request method. - -#### getPath() - -The `getPath(): string` method can be used to -return the request path. +The above example simply counts the number of bytes received in the request body. +This can be used as a skeleton for buffering or processing the request body. -#### getQueryParams() +The `data` event will be emitted whenever new data is available on the request +body stream. +The server automatically takes care of decoding chunked transfer encoding +and will only emit the actual payload as data. +In this case, the `Transfer-Encoding` header will be removed. -The `getQueryParams(): array` method can be used to -return an array with all query parameters ($_GET). +The `end` event will be emitted when the request body stream terminates +successfully, i.e. it was read until its expected end. -#### getProtocolVersion() +The `error` event will be emitted in case the request stream contains invalid +chunked data or the connection closes before the complete request stream has +been received. +The server will automatically `pause()` the connection instead of closing it. +A response message can still be sent (unless the connection is already closed). -The `getProtocolVersion(): string` method can be used to -return the HTTP protocol version (such as "1.0" or "1.1"). - -#### getHeaders() - -The `getHeaders(): array` method can be used to -return an array with ALL headers. - -The keys represent the header name in the exact case in which they were -originally specified. The values will be an array of strings for each -value for the respective header name. - -#### getHeader() - -The `getHeader(string $name): string[]` method can be used to -retrieve a message header value by the given case-insensitive name. - -Returns a list of all values for this header name or an empty array if header was not found - -#### getHeaderLine() - -The `getHeaderLine(string $name): string` method can be used to -retrieve a comma-separated string of the values for a single header. - -Returns a comma-separated list of all values for this header name or an empty string if header was not found - -#### hasHeader() +A `close` event will be emitted after an `error` or `end` event. -The `hasHeader(string $name): bool` method can be used to -check if a header exists by the given case-insensitive name. +For more details about the request body stream, check out the documentation of +[ReactPHP ReadableStreamInterface](https://github.com/reactphp/stream#readablestreaminterface). + +The `getSize(): ?int` method can be used if you only want to know the request +body size. +This method returns the complete size of the request body as defined by the +message boundaries. +This value may be `0` if the request message does not contain a request body +(such as a simple `GET` request). +Note that this value may be `null` if the request body size is unknown in +advance because the request message uses chunked transfer encoding. + +```php +$http = new Server($socket, function (RequestInterface $request, Response $response) { + $size = $request->getBody()->getSize(); + if ($size === null) { + $response->writeHead(411, array('Content-Type' => 'text/plain')); + $response->write('The request does not contain an explicit length.'); + $response->write('This server does not accept chunked transfer encoding.'); + $response->end(); + return; + } + $response->writeHead(200, array('Content-Type' => 'text/plain')); + $response->end("Request body size: " . $size . " bytes\n"); +}); +``` ### Response diff --git a/src/Server.php b/src/Server.php index bd3faebf..7387b8b6 100644 --- a/src/Server.php +++ b/src/Server.php @@ -22,7 +22,7 @@ * ```php * $socket = new React\Socket\Server(8080, $loop); * - * $http = new Server($socket, function (Request $request, Response $response) { + * $http = new Server($socket, function (RequestInterface $request, Response $response) { * $response->writeHead(200, array('Content-Type' => 'text/plain')); * $response->end("Hello World!\n"); * }); @@ -38,7 +38,7 @@ * 'local_cert' => __DIR__ . '/localhost.pem' * )); * - * $http = new Server($socket, function (Request $request, Response $response) { + * $http = new Server($socket, function (RequestInterface $request, Response $response) { * $response->writeHead(200, array('Content-Type' => 'text/plain')); * $response->end("Hello World!\n"); * }); From 5487c162778698940588bd9fb676c25f641b1a2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 24 Mar 2017 11:58:20 +0100 Subject: [PATCH 107/456] Simplify examples to ease getting started --- README.md | 8 ++++++ examples/02-count-visitors.php | 21 +++++++++++++++ examples/03-stream-response.php | 27 +++++++++++++++++++ ...ng-body-data.php => 04-stream-request.php} | 0 ...rld-https.php => 11-hello-world-https.php} | 0 5 files changed, 56 insertions(+) create mode 100644 examples/02-count-visitors.php create mode 100644 examples/03-stream-response.php rename examples/{03-handling-body-data.php => 04-stream-request.php} (100%) rename examples/{02-hello-world-https.php => 11-hello-world-https.php} (100%) diff --git a/README.md b/README.md index 38117f22..611fc5b9 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,8 @@ $http = new Server($socket, function (RequestInterface $request, Response $respo }); ``` +See also the [first example](examples) for more details. + Similarly, you can also attach this to a [`React\Socket\SecureServer`](https://github.com/reactphp/socket#secureserver) in order to start a secure HTTPS server like this: @@ -74,6 +76,8 @@ $http = new Server($socket, function (RequestInterface $request, Response $respo }); ``` +See also [example #11](examples) for more details. + When HTTP/1.1 clients want to send a bigger request body, they MAY send only the request headers with an additional `Expect: 100-continue` header and wait before sending the actual (large) message body. @@ -174,6 +178,8 @@ $http = new Server($socket, function (RequestInterface $request, Response $respo The above example simply counts the number of bytes received in the request body. This can be used as a skeleton for buffering or processing the request body. +See also [example #4](examples) for more details. + The `data` event will be emitted whenever new data is available on the request body stream. The server automatically takes care of decoding chunked transfer encoding @@ -224,6 +230,8 @@ The `Response` class is responsible for streaming the outgoing response body. It implements the `WritableStreamInterface`. +See also [example #3](examples) for more details. + The constructor is internal, you SHOULD NOT call this yourself. The `Server` is responsible for emitting `Request` and `Response` objects. diff --git a/examples/02-count-visitors.php b/examples/02-count-visitors.php new file mode 100644 index 00000000..df4bda06 --- /dev/null +++ b/examples/02-count-visitors.php @@ -0,0 +1,21 @@ +writeHead(200, array('Content-Type' => 'text/plain')); + $response->end("Welcome number " . ++$counter . "!\n"); +}); + +echo 'Listening on http://' . $socket->getAddress() . PHP_EOL; + +$loop->run(); diff --git a/examples/03-stream-response.php b/examples/03-stream-response.php new file mode 100644 index 00000000..49617999 --- /dev/null +++ b/examples/03-stream-response.php @@ -0,0 +1,27 @@ +writeHead(200, array('Content-Type' => 'text/plain')); + + $timer = $loop->addPeriodicTimer(0.5, function () use ($response) { + $response->write(microtime(true) . PHP_EOL); + }); + $loop->addTimer(5, function() use ($loop, $timer, $response) { + $loop->cancelTimer($timer); + $response->end(); + }); +}); + +echo 'Listening on http://' . $socket->getAddress() . PHP_EOL; + +$loop->run(); diff --git a/examples/03-handling-body-data.php b/examples/04-stream-request.php similarity index 100% rename from examples/03-handling-body-data.php rename to examples/04-stream-request.php diff --git a/examples/02-hello-world-https.php b/examples/11-hello-world-https.php similarity index 100% rename from examples/02-hello-world-https.php rename to examples/11-hello-world-https.php From 18927826e064cd01083b26cace1dc2efd171f35e Mon Sep 17 00:00:00 2001 From: Niels Theen Date: Fri, 10 Mar 2017 11:12:58 +0100 Subject: [PATCH 108/456] Return Promise with PSR-7 Response resolving --- composer.json | 1 + examples/01-hello-world.php | 12 +- src/ChunkedEncoder.php | 107 ++++++ src/Response.php | 279 ++-------------- src/Server.php | 100 +++++- tests/ChunkedEncoderTest.php | 83 +++++ tests/ResponseTest.php | 632 +---------------------------------- tests/ServerTest.php | 555 ++++++++++++++++++++++++------ 8 files changed, 775 insertions(+), 994 deletions(-) create mode 100644 src/ChunkedEncoder.php create mode 100644 tests/ChunkedEncoderTest.php diff --git a/composer.json b/composer.json index 43608b90..a08eb15f 100644 --- a/composer.json +++ b/composer.json @@ -8,6 +8,7 @@ "ringcentral/psr7": "^1.2", "react/socket": "^0.5", "react/stream": "^0.6 || ^0.5 || ^0.4.4", + "react/promise": "^2.0 || ^1.1", "evenement/evenement": "^2.0 || ^1.0" }, "autoload": { diff --git a/examples/01-hello-world.php b/examples/01-hello-world.php index 49c12664..12c36a56 100644 --- a/examples/01-hello-world.php +++ b/examples/01-hello-world.php @@ -10,9 +10,15 @@ $loop = Factory::create(); $socket = new Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); -$server = new \React\Http\Server($socket, function (RequestInterface $request, Response $response) { - $response->writeHead(200, array('Content-Type' => 'text/plain')); - $response->end("Hello world!\n"); +$server = new \React\Http\Server($socket, function (RequestInterface $request) { + return new Response( + 200, + array( + 'Content-Length' => strlen("Hello world\n"), + 'Content-Type' => 'text/plain' + ), + "Hello world\n" + ); }); echo 'Listening on http://' . $socket->getAddress() . PHP_EOL; diff --git a/src/ChunkedEncoder.php b/src/ChunkedEncoder.php new file mode 100644 index 00000000..69d88ac7 --- /dev/null +++ b/src/ChunkedEncoder.php @@ -0,0 +1,107 @@ +input = $input; + + $this->input->on('data', array($this, 'handleData')); + $this->input->on('end', array($this, 'handleEnd')); + $this->input->on('error', array($this, 'handleError')); + $this->input->on('close', array($this, 'close')); + } + + public function isReadable() + { + return !$this->closed && $this->input->isReadable(); + } + + public function pause() + { + $this->input->pause(); + } + + public function resume() + { + $this->input->resume(); + } + + 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->readable = false; + + $this->emit('close'); + + $this->removeAllListeners(); + } + + /** @internal */ + public function handleData($data) + { + if ($data === '') { + return; + } + + $completeChunk = $this->createChunk($data); + + $this->emit('data', array($completeChunk)); + } + + /** @internal */ + public function handleError(\Exception $e) + { + $this->emit('error', array($e)); + $this->close(); + } + + /** @internal */ + public function handleEnd() + { + $this->emit('data', array("0\r\n\r\n")); + + if (!$this->closed) { + $this->emit('end'); + $this->close(); + } + } + + /** + * @param string $data - string to be transformed in an valid + * HTTP encoded chunk string + * @return string + */ + private function createChunk($data) + { + $byteSize = strlen($data); + $byteSize = dechex($byteSize); + $chunkBeginning = $byteSize . "\r\n"; + + return $chunkBeginning . $data . "\r\n"; + } + +} diff --git a/src/Response.php b/src/Response.php index 5442f7a5..49aadf85 100644 --- a/src/Response.php +++ b/src/Response.php @@ -2,262 +2,35 @@ namespace React\Http; -use Evenement\EventEmitter; -use React\Stream\WritableStreamInterface; +use RingCentral\Psr7\Response as Psr7Response; +use React\Stream\ReadableStreamInterface; +use React\Http\HttpBodyStream; /** - * The `Response` class is responsible for streaming the outgoing response body. - * - * It implements the `WritableStreamInterface`. - * - * The constructor is internal, you SHOULD NOT call this yourself. - * The `Server` is responsible for emitting `Request` and `Response` objects. - * - * The `Response` will automatically use the same HTTP protocol version as the - * corresponding `Request`. - * - * HTTP/1.1 responses will automatically apply chunked transfer encoding if - * no `Content-Length` header has been set. - * See `writeHead()` for more details. - * - * See the usage examples and the class outline for details. - * - * @see WritableStreamInterface - * @see Server + * Implementation of the PSR-7 ResponseInterface + * This class is an extension of RingCentral\Psr7\Response. + * The only difference is that this class will accept implemenations + * of the ReactPHPs ReadableStreamInterface for $body. */ -class Response extends EventEmitter implements WritableStreamInterface +class Response extends Psr7Response { - private $conn; - private $protocolVersion; - - private $closed = false; - private $writable = true; - private $headWritten = false; - private $chunkedEncoding = false; - - /** - * The constructor is internal, you SHOULD NOT call this yourself. - * - * The `Server` is responsible for emitting `Request` and `Response` objects. - * - * Constructor parameters may change at any time. - * - * @internal - */ - public function __construct(WritableStreamInterface $conn, $protocolVersion = '1.1') - { - $this->conn = $conn; - $this->protocolVersion = $protocolVersion; - - $that = $this; - $this->conn->on('close', array($this, 'close')); - - $this->conn->on('error', function ($error) use ($that) { - $that->emit('error', array($error)); - }); - - $this->conn->on('drain', function () use ($that) { - $that->emit('drain'); - }); - } - - public function isWritable() - { - return $this->writable; - } - - /** - * Writes the given HTTP message header. - * - * This method MUST be invoked once before calling `write()` or `end()` to send - * the actual HTTP message body: - * - * ```php - * $response->writeHead(200, array( - * 'Content-Type' => 'text/plain' - * )); - * $response->end('Hello World!'); - * ``` - * - * Calling this method more than once will result in an `Exception` - * (unless the response has ended/closed already). - * Calling this method after the response has ended/closed is a NOOP. - * - * Unless you specify a `Content-Length` header yourself, HTTP/1.1 responses - * will automatically use chunked transfer encoding and send the respective header - * (`Transfer-Encoding: chunked`) automatically. If you know the length of your - * body, you MAY specify it like this instead: - * - * ```php - * $data = 'Hello World!'; - * - * $response->writeHead(200, array( - * 'Content-Type' => 'text/plain', - * 'Content-Length' => strlen($data) - * )); - * $response->end($data); - * ``` - * - * Note that it will automatically assume a `X-Powered-By: react/alpha` header - * unless your specify a custom `X-Powered-By` header yourself: - * - * ```php - * $response->writeHead(200, array( - * 'X-Powered-By' => 'PHP 3' - * )); - * ``` - * - * If you do not want to send this header at all, you can use an empty array as - * value like this: - * - * ```php - * $response->writeHead(200, array( - * 'X-Powered-By' => array() - * )); - * ``` - * - * Note that persistent connections (`Connection: keep-alive`) are currently - * not supported. - * As such, HTTP/1.1 response messages will automatically include a - * `Connection: close` header, irrespective of what header values are - * passed explicitly. - * - * @param int $status - * @param array $headers - * @throws \Exception - */ - public function writeHead($status = 200, array $headers = array()) - { - if (!$this->writable) { - return; - } - if ($this->headWritten) { - throw new \Exception('Response head has already been written.'); - } - - $lower = array_change_key_case($headers); - - // assign default "X-Powered-By" header as first for history reasons - if (!isset($lower['x-powered-by'])) { - $headers = array_merge( - array('X-Powered-By' => 'React/alpha'), - $headers - ); - } - - // always remove transfer-encoding - foreach($headers as $name => $value) { - if (strtolower($name) === 'transfer-encoding') { - unset($headers[$name]); - } - } - - // assign date header if no 'date' is given, use the current time where this code is running - if (!isset($lower['date'])) { - // IMF-fixdate = day-name "," SP date1 SP time-of-day SP GMT - $headers['Date'] = gmdate('D, d M Y H:i:s') . ' GMT'; - } - - // assign chunked transfer-encoding if no 'content-length' is given for HTTP/1.1 responses - if (!isset($lower['content-length']) && $this->protocolVersion === '1.1') { - $headers['Transfer-Encoding'] = 'chunked'; - $this->chunkedEncoding = true; - } - - // HTTP/1.1 assumes persistent connection support by default - // we do not support persistent connections, so let the client know - if ($this->protocolVersion === '1.1') { - foreach($headers as $name => $value) { - if (strtolower($name) === 'connection') { - unset($headers[$name]); - } - } - - $headers['Connection'] = 'close'; - } - - $data = $this->formatHead($status, $headers); - $this->conn->write($data); - - $this->headWritten = true; - } - - private function formatHead($status, array $headers) - { - $status = (int) $status; - $text = isset(ResponseCodes::$statusTexts[$status]) ? ResponseCodes::$statusTexts[$status] : ''; - $data = "HTTP/$this->protocolVersion $status $text\r\n"; - - foreach ($headers as $name => $value) { - $name = str_replace(array("\r", "\n"), '', $name); - - foreach ((array) $value as $val) { - $val = str_replace(array("\r", "\n"), '', $val); - - $data .= "$name: $val\r\n"; - } - } - $data .= "\r\n"; - - return $data; - } - - public function write($data) - { - if (!$this->writable) { - return false; - } - if (!$this->headWritten) { - throw new \Exception('Response head has not yet been written.'); - } - - // prefix with chunk length for chunked transfer encoding - if ($this->chunkedEncoding) { - $len = strlen($data); - - // skip empty chunks - if ($len === 0) { - return true; - } - - $data = dechex($len) . "\r\n" . $data . "\r\n"; - } - - return $this->conn->write($data); - } - - public function end($data = null) - { - if (!$this->writable) { - return; - } - if (!$this->headWritten) { - throw new \Exception('Response head has not yet been written.'); - } - - if (null !== $data) { - $this->write($data); - } - - if ($this->chunkedEncoding) { - $this->conn->write("0\r\n\r\n"); - } - - $this->writable = false; - $this->conn->end(); - } - - public function close() - { - if ($this->closed) { - return; - } - - $this->closed = true; - $this->writable = false; - $this->conn->close(); - - $this->emit('close'); - $this->removeAllListeners(); + public function __construct( + $status = 200, + array $headers = array(), + $body = null, + $version = '1.1', + $reason = null + ) { + if ($body instanceof ReadableStreamInterface) { + $body = new HttpBodyStream($body, null); + } + + parent::__construct( + $status, + $headers, + $body, + $version, + $reason + ); } } diff --git a/src/Server.php b/src/Server.php index 7387b8b6..84159a99 100644 --- a/src/Server.php +++ b/src/Server.php @@ -6,6 +6,10 @@ use React\Socket\ServerInterface as SocketServerInterface; use React\Socket\ConnectionInterface; use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ResponseInterface; +use RingCentral; +use React\Stream\ReadableStream; +use React\Promise\Promise; /** * The `Server` class is responsible for handling incoming connections and then @@ -183,8 +187,6 @@ public function handleRequest(ConnectionInterface $conn, RequestInterface $reque } } - $response = new Response($conn, $request->getProtocolVersion()); - $contentLength = 0; $stream = new CloseProtectionStream($conn); if ($request->hasHeader('Transfer-Encoding')) { @@ -226,7 +228,22 @@ public function handleRequest(ConnectionInterface $conn, RequestInterface $reque ); $callback = $this->callback; - $callback($request, $response); + $promise = \React\Promise\resolve($callback($request)); + + $that = $this; + $promise->then( + function ($response) use ($that, $conn, $request) { + if (!$response instanceof ResponseInterface) { + $that->emit('error', array(new \InvalidArgumentException('Invalid response type'))); + return $that->writeError($conn, 500); + } + $that->handleResponse($conn, $response, $request->getProtocolVersion()); + }, + function ($ex) use ($that, $conn) { + $that->emit('error', array($ex)); + return $that->writeError($conn, 500); + } + ); if ($contentLength === 0) { // If Body is empty or Content-Length is 0 and won't emit further data, @@ -244,11 +261,76 @@ public function writeError(ConnectionInterface $conn, $code) $message .= ': ' . ResponseCodes::$statusTexts[$code]; } - $response = new Response($conn); - $response->writeHead($code, array( - 'Content-Length' => strlen($message), - 'Content-Type' => 'text/plain' - )); - $response->end($message); + $response = new Response( + $code, + array( + 'Content-Length' => strlen($message), + 'Content-Type' => 'text/plain' + ), + $message + ); + + $this->handleResponse($conn, $response, '1.1'); + } + + + /** @internal */ + public function handleResponse(ConnectionInterface $connection, ResponseInterface $response, $protocolVersion) + { + $response = $response->withProtocolVersion($protocolVersion); + + // assign default "X-Powered-By" header as first for history reasons + if (!$response->hasHeader('X-Powered-By')) { + $response = $response->withHeader('X-Powered-By', 'React/alpha'); + } + + if ($response->hasHeader('X-Powered-By') && $response->getHeaderLine('X-Powered-By') === ''){ + $response = $response->withoutHeader('X-Powered-By'); + } + + $response = $response->withoutHeader('Transfer-Encoding'); + + // assign date header if no 'date' is given, use the current time where this code is running + if (!$response->hasHeader('Date')) { + // IMF-fixdate = day-name "," SP date1 SP time-of-day SP GMT + $response = $response->withHeader('Date', gmdate('D, d M Y H:i:s') . ' GMT'); + } + + if ($response->hasHeader('Date') && $response->getHeaderLine('Date') === ''){ + $response = $response->withoutHeader('Date'); + } + + if (!$response->getBody() instanceof HttpBodyStream) { + $response = $response->withHeader('Content-Length', $response->getBody()->getSize()); + } elseif (!$response->hasHeader('Content-Length') && $protocolVersion === '1.1') { + // assign chunked transfer-encoding if no 'content-length' is given for HTTP/1.1 responses + $response = $response->withHeader('Transfer-Encoding', 'chunked'); + } + + // HTTP/1.1 assumes persistent connection support by default + // we do not support persistent connections, so let the client know + if ($protocolVersion === '1.1') { + $response = $response->withHeader('Connection', 'close'); + } + + $this->handleResponseBody($response, $connection); + } + + private function handleResponseBody(ResponseInterface $response, ConnectionInterface $connection) + { + if (!$response->getBody() instanceof HttpBodyStream) { + $connection->write(RingCentral\Psr7\str($response)); + return $connection->end(); + } + + $body = $response->getBody(); + $stream = $body; + + if ($response->getHeaderLine('Transfer-Encoding') === 'chunked') { + $stream = new ChunkedEncoder($body); + } + + $connection->write(RingCentral\Psr7\str($response)); + $stream->pipe($connection); } } diff --git a/tests/ChunkedEncoderTest.php b/tests/ChunkedEncoderTest.php new file mode 100644 index 00000000..ca8dc643 --- /dev/null +++ b/tests/ChunkedEncoderTest.php @@ -0,0 +1,83 @@ +input = new ReadableStream(); + $this->chunkedStream = new ChunkedEncoder($this->input); + } + + public function testChunked() + { + $this->chunkedStream->on('data', $this->expectCallableOnce(array("5\r\nhello\r\n"))); + $this->input->emit('data', array('hello')); + } + + public function testEmptyString() + { + $this->chunkedStream->on('data', $this->expectCallableNever()); + $this->input->emit('data', array('')); + } + + public function testBiggerStringToCheckHexValue() + { + $this->chunkedStream->on('data', $this->expectCallableOnce(array("1a\r\nabcdefghijklmnopqrstuvwxyz\r\n"))); + $this->input->emit('data', array('abcdefghijklmnopqrstuvwxyz')); + } + + public function testHandleClose() + { + $this->chunkedStream->on('close', $this->expectCallableOnce()); + + $this->input->close(); + + $this->assertFalse($this->chunkedStream->isReadable()); + } + + public function testHandleError() + { + $this->chunkedStream->on('error', $this->expectCallableOnce()); + $this->chunkedStream->on('close', $this->expectCallableOnce()); + + $this->input->emit('error', array(new \RuntimeException())); + + $this->assertFalse($this->chunkedStream->isReadable()); + } + + public function testPauseStream() + { + $input = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $input->expects($this->once())->method('pause'); + + $parser = new ChunkedEncoder($input); + $parser->pause(); + } + + public function testResumeStream() + { + $input = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $input->expects($this->once())->method('pause'); + + $parser = new ChunkedEncoder($input); + $parser->pause(); + $parser->resume(); + } + + public function testPipeStream() + { + $dest = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + + $ret = $this->chunkedStream->pipe($dest); + + $this->assertSame($dest, $ret); + } +} diff --git a/tests/ResponseTest.php b/tests/ResponseTest.php index 65fc0598..4d024956 100644 --- a/tests/ResponseTest.php +++ b/tests/ResponseTest.php @@ -3,637 +3,19 @@ namespace React\Tests\Http; use React\Http\Response; -use React\Stream\WritableStream; +use React\Stream\ReadableStream; class ResponseTest extends TestCase { - public function testResponseShouldBeChunkedByDefault() + public function testResponseBodyWillBeHttpBodyStream() { - $expected = ''; - $expected .= "HTTP/1.1 200 OK\r\n"; - $expected .= "X-Powered-By: React/alpha\r\n"; - $expected .= "Transfer-Encoding: chunked\r\n"; - $expected .= "Connection: close\r\n"; - $expected .= "\r\n"; - - $conn = $this - ->getMockBuilder('React\Socket\ConnectionInterface') - ->getMock(); - $conn - ->expects($this->once()) - ->method('write') - ->with($expected); - - $response = new Response($conn); - $response->writeHead(200, array('Date' => array())); - } - - public function testResponseShouldNotBeChunkedWhenProtocolVersionIsNot11() - { - $expected = ''; - $expected .= "HTTP/1.0 200 OK\r\n"; - $expected .= "X-Powered-By: React/alpha\r\n"; - $expected .= "\r\n"; - - $conn = $this - ->getMockBuilder('React\Socket\ConnectionInterface') - ->getMock(); - $conn - ->expects($this->once()) - ->method('write') - ->with($expected); - - $response = new Response($conn, '1.0'); - $response->writeHead(200, array('Date' => array())); - } - - public function testResponseShouldBeChunkedEvenWithOtherTransferEncoding() - { - $expected = ''; - $expected .= "HTTP/1.1 200 OK\r\n"; - $expected .= "X-Powered-By: React/alpha\r\n"; - $expected .= "Transfer-Encoding: chunked\r\n"; - $expected .= "Connection: close\r\n"; - $expected .= "\r\n"; - - $conn = $this - ->getMockBuilder('React\Socket\ConnectionInterface') - ->getMock(); - $conn - ->expects($this->once()) - ->method('write') - ->with($expected); - - $response = new Response($conn); - $response->writeHead(200, array('transfer-encoding' => 'custom', 'Date' => array())); - } - - public function testResponseShouldNotBeChunkedWithContentLength() - { - $expected = ''; - $expected .= "HTTP/1.1 200 OK\r\n"; - $expected .= "X-Powered-By: React/alpha\r\n"; - $expected .= "Content-Length: 22\r\n"; - $expected .= "Connection: close\r\n"; - $expected .= "\r\n"; - - $conn = $this - ->getMockBuilder('React\Socket\ConnectionInterface') - ->getMock(); - $conn - ->expects($this->once()) - ->method('write') - ->with($expected); - - $response = new Response($conn); - $response->writeHead(200, array('Content-Length' => 22, 'Date' => array())); - } - - public function testResponseShouldNotBeChunkedWithContentLengthCaseInsensitive() - { - $expected = ''; - $expected .= "HTTP/1.1 200 OK\r\n"; - $expected .= "X-Powered-By: React/alpha\r\n"; - $expected .= "CONTENT-LENGTH: 0\r\n"; - $expected .= "Connection: close\r\n"; - $expected .= "\r\n"; - - $conn = $this - ->getMockBuilder('React\Socket\ConnectionInterface') - ->getMock(); - $conn - ->expects($this->once()) - ->method('write') - ->with($expected); - - $response = new Response($conn); - $response->writeHead(200, array('CONTENT-LENGTH' => 0, 'Date' => array())); - } - - public function testResponseShouldIncludeCustomByPoweredAsFirstHeaderIfGivenExplicitly() - { - $expected = ''; - $expected .= "HTTP/1.1 200 OK\r\n"; - $expected .= "Content-Length: 0\r\n"; - $expected .= "X-POWERED-BY: demo\r\n"; - $expected .= "Connection: close\r\n"; - $expected .= "\r\n"; - - $conn = $this - ->getMockBuilder('React\Socket\ConnectionInterface') - ->getMock(); - $conn - ->expects($this->once()) - ->method('write') - ->with($expected); - - $response = new Response($conn); - $response->writeHead(200, array('Content-Length' => 0, 'X-POWERED-BY' => 'demo', 'Date' => array())); - } - - public function testResponseShouldNotIncludePoweredByIfGivenEmptyArray() - { - $expected = ''; - $expected .= "HTTP/1.1 200 OK\r\n"; - $expected .= "Content-Length: 0\r\n"; - $expected .= "Connection: close\r\n"; - $expected .= "\r\n"; - - $conn = $this - ->getMockBuilder('React\Socket\ConnectionInterface') - ->getMock(); - $conn - ->expects($this->once()) - ->method('write') - ->with($expected); - - $response = new Response($conn); - $response->writeHead(200, array('Content-Length' => 0, 'X-Powered-By' => array(), 'Date' => array())); - } - - public function testResponseShouldAlwaysIncludeConnectionCloseIrrespectiveOfExplicitValue() - { - $expected = ''; - $expected .= "HTTP/1.1 200 OK\r\n"; - $expected .= "X-Powered-By: React/alpha\r\n"; - $expected .= "Content-Length: 0\r\n"; - $expected .= "Connection: close\r\n"; - $expected .= "\r\n"; - - $conn = $this - ->getMockBuilder('React\Socket\ConnectionInterface') - ->getMock(); - $conn - ->expects($this->once()) - ->method('write') - ->with($expected); - - $response = new Response($conn); - $response->writeHead(200, array('Content-Length' => 0, 'connection' => 'ignored', 'Date' => array())); - } - - /** @expectedException Exception */ - public function testWriteHeadTwiceShouldThrowException() - { - $conn = $this - ->getMockBuilder('React\Socket\ConnectionInterface') - ->getMock(); - $conn - ->expects($this->once()) - ->method('write'); - - $response = new Response($conn); - $response->writeHead(); - $response->writeHead(); - } - - public function testEndWithoutDataWritesEndChunkAndEndsInput() - { - $conn = $this - ->getMockBuilder('React\Socket\ConnectionInterface') - ->getMock(); - $conn - ->expects($this->at(4)) - ->method('write') - ->with("0\r\n\r\n"); - $conn - ->expects($this->once()) - ->method('end'); - - $response = new Response($conn); - $response->writeHead(); - $response->end(); - } - - public function testEndWithDataWritesToInputAndEndsInputWithoutData() - { - $conn = $this - ->getMockBuilder('React\Socket\ConnectionInterface') - ->getMock(); - $conn - ->expects($this->at(4)) - ->method('write') - ->with("3\r\nbye\r\n"); - $conn - ->expects($this->at(5)) - ->method('write') - ->with("0\r\n\r\n"); - $conn - ->expects($this->once()) - ->method('end'); - - $response = new Response($conn); - $response->writeHead(); - $response->end('bye'); - } - - public function testEndWithoutDataWithoutChunkedEncodingWritesNoDataAndEndsInput() - { - $conn = $this - ->getMockBuilder('React\Socket\ConnectionInterface') - ->getMock(); - $conn - ->expects($this->once()) - ->method('write'); - $conn - ->expects($this->once()) - ->method('end'); - - $response = new Response($conn); - $response->writeHead(200, array('Content-Length' => 0)); - $response->end(); - } - - /** @expectedException Exception */ - public function testEndWithoutHeadShouldThrowException() - { - $conn = $this - ->getMockBuilder('React\Socket\ConnectionInterface') - ->getMock(); - $conn - ->expects($this->never()) - ->method('end'); - - $response = new Response($conn); - $response->end(); - } - - /** @expectedException Exception */ - public function testWriteWithoutHeadShouldThrowException() - { - $conn = $this - ->getMockBuilder('React\Socket\ConnectionInterface') - ->getMock(); - $conn - ->expects($this->never()) - ->method('write'); - - $response = new Response($conn); - $response->write('test'); - } - - public function testResponseBodyShouldBeChunkedCorrectly() - { - $conn = $this - ->getMockBuilder('React\Socket\ConnectionInterface') - ->getMock(); - $conn - ->expects($this->at(4)) - ->method('write') - ->with("5\r\nHello\r\n"); - $conn - ->expects($this->at(5)) - ->method('write') - ->with("1\r\n \r\n"); - $conn - ->expects($this->at(6)) - ->method('write') - ->with("6\r\nWorld\n\r\n"); - $conn - ->expects($this->at(7)) - ->method('write') - ->with("0\r\n\r\n"); - - $response = new Response($conn); - $response->writeHead(); - - $response->write('Hello'); - $response->write(' '); - $response->write("World\n"); - $response->end(); - } - - public function testResponseBodyShouldSkipEmptyChunks() - { - $conn = $this - ->getMockBuilder('React\Socket\ConnectionInterface') - ->getMock(); - $conn - ->expects($this->at(4)) - ->method('write') - ->with("5\r\nHello\r\n"); - $conn - ->expects($this->at(5)) - ->method('write') - ->with("5\r\nWorld\r\n"); - $conn - ->expects($this->at(6)) - ->method('write') - ->with("0\r\n\r\n"); - - $response = new Response($conn); - $response->writeHead(); - - $response->write('Hello'); - $response->write(''); - $response->write('World'); - $response->end(); - } - - /** @test */ - public function shouldRemoveNewlinesFromHeaders() - { - $expected = ''; - $expected .= "HTTP/1.1 200 OK\r\n"; - $expected .= "X-Powered-By: React/alpha\r\n"; - $expected .= "FooBar: BazQux\r\n"; - $expected .= "Transfer-Encoding: chunked\r\n"; - $expected .= "Connection: close\r\n"; - $expected .= "\r\n"; - - $conn = $this - ->getMockBuilder('React\Socket\ConnectionInterface') - ->getMock(); - $conn - ->expects($this->once()) - ->method('write') - ->with($expected); - - $response = new Response($conn); - $response->writeHead(200, array("Foo\nBar" => "Baz\rQux", 'Date' => array())); - } - - /** @test */ - public function missingStatusCodeTextShouldResultInNumberOnlyStatus() - { - $expected = ''; - $expected .= "HTTP/1.1 700 \r\n"; - $expected .= "X-Powered-By: React/alpha\r\n"; - $expected .= "Transfer-Encoding: chunked\r\n"; - $expected .= "Connection: close\r\n"; - $expected .= "\r\n"; - - $conn = $this - ->getMockBuilder('React\Socket\ConnectionInterface') - ->getMock(); - $conn - ->expects($this->once()) - ->method('write') - ->with($expected); - - $response = new Response($conn); - $response->writeHead(700, array('Date' => array())); + $response = new Response(200, array(), new ReadableStream()); + $this->assertInstanceOf('React\Http\HttpBodyStream', $response->getBody()); } - /** @test */ - public function shouldAllowArrayHeaderValues() + public function testStringBodyWillBePsr7Stream() { - $expected = ''; - $expected .= "HTTP/1.1 200 OK\r\n"; - $expected .= "X-Powered-By: React/alpha\r\n"; - $expected .= "Set-Cookie: foo=bar\r\n"; - $expected .= "Set-Cookie: bar=baz\r\n"; - $expected .= "Transfer-Encoding: chunked\r\n"; - $expected .= "Connection: close\r\n"; - $expected .= "\r\n"; - - $conn = $this - ->getMockBuilder('React\Socket\ConnectionInterface') - ->getMock(); - $conn - ->expects($this->once()) - ->method('write') - ->with($expected); - - $response = new Response($conn); - $response->writeHead(200, array("Set-Cookie" => array("foo=bar", "bar=baz"), 'Date' => array())); - } - - /** @test */ - public function shouldIgnoreHeadersWithNullValues() - { - $expected = ''; - $expected .= "HTTP/1.1 200 OK\r\n"; - $expected .= "X-Powered-By: React/alpha\r\n"; - $expected .= "Transfer-Encoding: chunked\r\n"; - $expected .= "Connection: close\r\n"; - $expected .= "\r\n"; - - $conn = $this - ->getMockBuilder('React\Socket\ConnectionInterface') - ->getMock(); - $conn - ->expects($this->once()) - ->method('write') - ->with($expected); - - $response = new Response($conn); - $response->writeHead(200, array("FooBar" => null, 'Date' => array())); - } - - public function testCloseClosesInputAndEmitsCloseEvent() - { - $input = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); - $input->expects($this->once())->method('close'); - - $response = new Response($input); - - $response->on('close', $this->expectCallableOnce()); - - $response->close(); - } - - public function testClosingInputEmitsCloseEvent() - { - $input = new WritableStream(); - $response = new Response($input); - - $response->on('close', $this->expectCallableOnce()); - - $input->close(); - } - - public function testCloseMultipleTimesEmitsCloseEventOnce() - { - $input = new WritableStream(); - $response = new Response($input); - - $response->on('close', $this->expectCallableOnce()); - - $response->close(); - $response->close(); - } - - public function testIsNotWritableAfterClose() - { - $input = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); - - $response = new Response($input); - - $response->close(); - - $this->assertFalse($response->isWritable()); - } - - public function testCloseAfterEndIsPassedThrough() - { - $input = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); - $input->expects($this->once())->method('end'); - $input->expects($this->once())->method('close'); - - $response = new Response($input); - - $response->writeHead(); - $response->end(); - $response->close(); - } - - public function testWriteAfterCloseIsNoOp() - { - $input = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); - $input->expects($this->once())->method('close'); - $input->expects($this->never())->method('write'); - - $response = new Response($input); - $response->close(); - - $this->assertFalse($response->write('noop')); - } - - public function testWriteHeadAfterCloseIsNoOp() - { - $input = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); - $input->expects($this->once())->method('close'); - $input->expects($this->never())->method('write'); - - $response = new Response($input); - $response->close(); - - $response->writeHead(); - } - - public function testEndAfterCloseIsNoOp() - { - $input = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); - $input->expects($this->once())->method('close'); - $input->expects($this->never())->method('write'); - $input->expects($this->never())->method('end'); - - $response = new Response($input); - $response->close(); - - $response->end('noop'); - } - - public function testErrorEventShouldBeForwardedWithoutClosing() - { - $input = new WritableStream(); - $response = new Response($input); - - $response->on('error', $this->expectCallableOnce()); - $response->on('close', $this->expectCallableNever()); - - $input->emit('error', array(new \RuntimeException())); - } - - public function testDrainEventShouldBeForwarded() - { - $input = new WritableStream(); - $response = new Response($input); - - $response->on('drain', $this->expectCallableOnce()); - - $input->emit('drain'); - } - - public function testContentLengthWillBeRemovedIfTransferEncodingIsGiven() - { - $expectedHeader = ''; - $expectedHeader .= "HTTP/1.1 200 OK\r\n"; - $expectedHeader .= "X-Powered-By: React/alpha\r\n"; - $expectedHeader .= "Content-Length: 4\r\n"; - $expectedHeader .= "Connection: close\r\n"; - $expectedHeader .= "\r\n"; - - $expectedBody = "hello"; - - $conn = $this - ->getMockBuilder('React\Socket\ConnectionInterface') - ->getMock(); - $conn - ->expects($this->exactly(2)) - ->method('write') - ->withConsecutive( - array($expectedHeader), - array($expectedBody) - ); - - $response = new Response($conn, '1.1'); - $response->writeHead( - 200, - array( - 'Content-Length' => 4, - 'Transfer-Encoding' => 'chunked', - 'Date' => array() - ) - ); - $response->write('hello'); - } - - public function testDateHeaderWillUseServerTime() - { - $buffer = ''; - $conn = $this - ->getMockBuilder('React\Socket\ConnectionInterface') - ->getMock(); - $conn - ->expects($this->once()) - ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) - ); - - $response = new Response($conn); - $response->writeHead(); - - $this->assertContains("HTTP/1.1 200 OK\r\n", $buffer); - $this->assertContains("Date:", $buffer); - } - - public function testDateHeaderWithCustomDate() - { - $expected = ''; - $expected .= "HTTP/1.1 200 OK\r\n"; - $expected .= "X-Powered-By: React/alpha\r\n"; - $expected .= "Date: Tue, 15 Nov 1994 08:12:31 GMT\r\n"; - $expected .= "Transfer-Encoding: chunked\r\n"; - $expected .= "Connection: close\r\n"; - $expected .= "\r\n"; - - $conn = $this - ->getMockBuilder('React\Socket\ConnectionInterface') - ->getMock(); - $conn - ->expects($this->once()) - ->method('write') - ->with($expected); - - $response = new Response($conn); - $response->writeHead(200, array("Date" => "Tue, 15 Nov 1994 08:12:31 GMT")); - } - - public function testDateHeaderWillBeRemoved() - { - $expected = ''; - $expected .= "HTTP/1.1 200 OK\r\n"; - $expected .= "X-Powered-By: React/alpha\r\n"; - $expected .= "Transfer-Encoding: chunked\r\n"; - $expected .= "Connection: close\r\n"; - $expected .= "\r\n"; - - $conn = $this - ->getMockBuilder('React\Socket\ConnectionInterface') - ->getMock(); - $conn - ->expects($this->once()) - ->method('write') - ->with($expected); - - $response = new Response($conn); - $response->writeHead(200, array("Date" => array())); + $response = new Response(200, array(), 'hello'); + $this->assertInstanceOf('RingCentral\Psr7\Stream', $response->getBody()); } } diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 40434f18..d9917375 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -3,8 +3,10 @@ namespace React\Tests\Http; use React\Http\Server; -use React\Http\Response; use Psr\Http\Message\RequestInterface; +use React\Http\Response; +use React\Stream\ReadableStream; +use React\Promise\Promise; class ServerTest extends TestCase { @@ -31,6 +33,9 @@ public function setUp() ) ->getMock(); + $this->connection->method('isWritable')->willReturn(true); + $this->connection->method('isReadable')->willReturn(true); + $this->socket = new SocketServerStub(); } @@ -47,7 +52,9 @@ public function testRequestEventWillNotBeEmittedForIncompleteHeaders() public function testRequestEventIsEmitted() { - $server = new Server($this->socket, $this->expectCallableOnce()); + $server = new Server($this->socket, function (RequestInterface $request) { + return \React\Promise\resolve(new Response()); + }); $this->socket->emit('connection', array($this->connection)); @@ -59,12 +66,24 @@ public function testRequestEvent() { $i = 0; $requestAssertion = null; - $responseAssertion = null; - $server = new Server($this->socket, function ($request, $response) use (&$i, &$requestAssertion, &$responseAssertion) { + $buffer = ''; + + $this->connection + ->expects($this->any()) + ->method('write') + ->will( + $this->returnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } + ) + ); + + $server = new Server($this->socket, function ($request) use (&$i, &$requestAssertion) { $i++; $requestAssertion = $request; - $responseAssertion = $response; + return \React\Promise\resolve(new Response()); }); $this->connection @@ -83,13 +102,13 @@ public function testRequestEvent() $this->assertSame('GET', $requestAssertion->getMethod()); $this->assertSame('127.0.0.1', $requestAssertion->remoteAddress); - $this->assertInstanceOf('React\Http\Response', $responseAssertion); } public function testRequestPauseWillbeForwardedToConnection() { $server = new Server($this->socket, function (RequestInterface $request) { $request->getBody()->pause(); + return \React\Promise\resolve(new Response()); }); $this->connection->expects($this->once())->method('pause'); @@ -108,6 +127,7 @@ public function testRequestResumeWillbeForwardedToConnection() { $server = new Server($this->socket, function (RequestInterface $request) { $request->getBody()->resume(); + return \React\Promise\resolve(new Response()); }); $this->connection->expects($this->once())->method('resume'); @@ -121,6 +141,7 @@ public function testRequestCloseWillPauseConnection() { $server = new Server($this->socket, function (RequestInterface $request) { $request->getBody()->close(); + return \React\Promise\resolve(new Response()); }); $this->connection->expects($this->once())->method('pause'); @@ -134,7 +155,9 @@ public function testRequestPauseAfterCloseWillNotBeForwarded() { $server = new Server($this->socket, function (RequestInterface $request) { $request->getBody()->close(); - $request->getBody()->pause(); + $request->getBody()->pause();# + + return \React\Promise\resolve(new Response()); }); $this->connection->expects($this->once())->method('pause'); @@ -149,6 +172,8 @@ public function testRequestResumeAfterCloseWillNotBeForwarded() $server = new Server($this->socket, function (RequestInterface $request) { $request->getBody()->close(); $request->getBody()->resume(); + + return \React\Promise\resolve(new Response()); }); $this->connection->expects($this->once())->method('pause'); @@ -165,6 +190,8 @@ public function testRequestEventWithoutBodyWillNotEmitData() $server = new Server($this->socket, function (RequestInterface $request) use ($never) { $request->getBody()->on('data', $never); + + return \React\Promise\resolve(new Response()); }); $this->socket->emit('connection', array($this->connection)); @@ -179,6 +206,8 @@ public function testRequestEventWithSecondDataEventWillEmitBodyData() $server = new Server($this->socket, function (RequestInterface $request) use ($once) { $request->getBody()->on('data', $once); + + return \React\Promise\resolve(new Response()); }); $this->socket->emit('connection', array($this->connection)); @@ -198,6 +227,8 @@ public function testRequestEventWithPartialBodyWillEmitData() $server = new Server($this->socket, function (RequestInterface $request) use ($once) { $request->getBody()->on('data', $once); + + return \React\Promise\resolve(new Response()); }); $this->socket->emit('connection', array($this->connection)); @@ -216,9 +247,8 @@ public function testRequestEventWithPartialBodyWillEmitData() public function testResponseContainsPoweredByHeader() { - $server = new Server($this->socket, function (RequestInterface $request, Response $response) { - $response->writeHead(); - $response->end(); + $server = new Server($this->socket, function (RequestInterface $request) { + return \React\Promise\resolve(new Response()); }); $buffer = ''; @@ -242,27 +272,11 @@ function ($data) use (&$buffer) { $this->assertContains("\r\nX-Powered-By: React/alpha\r\n", $buffer); } - public function testClosingResponseDoesNotSendAnyData() - { - $server = new Server($this->socket, function (RequestInterface $request, Response $response) { - $response->close(); - }); - - $this->connection->expects($this->never())->method('write'); - $this->connection->expects($this->never())->method('end'); - $this->connection->expects($this->once())->method('close'); - - $this->socket->emit('connection', array($this->connection)); - - $data = $this->createGetRequest(); - $this->connection->emit('data', array($data)); - } - public function testResponseContainsSameRequestProtocolVersionAndChunkedBodyForHttp11() { - $server = new Server($this->socket, function (RequestInterface $request, Response $response) { - $response->writeHead(); - $response->end('bye'); + $server = new Server($this->socket, function (RequestInterface $request) { + $response = new Response(200, array(), 'bye'); + return \React\Promise\resolve($response); }); $buffer = ''; @@ -284,14 +298,14 @@ function ($data) use (&$buffer) { $this->connection->emit('data', array($data)); $this->assertContains("HTTP/1.1 200 OK\r\n", $buffer); - $this->assertContains("\r\n\r\n3\r\nbye\r\n0\r\n\r\n", $buffer); + $this->assertContains("bye", $buffer); } public function testResponseContainsSameRequestProtocolVersionAndRawBodyForHttp10() { - $server = new Server($this->socket, function (RequestInterface $request, Response $response) { - $response->writeHead(); - $response->end('bye'); + $server = new Server($this->socket, function (RequestInterface $request) { + $response = new Response(200, array(), 'bye'); + return \React\Promise\resolve($response); }); $buffer = ''; @@ -313,7 +327,8 @@ function ($data) use (&$buffer) { $this->connection->emit('data', array($data)); $this->assertContains("HTTP/1.0 200 OK\r\n", $buffer); - $this->assertContains("\r\n\r\nbye", $buffer); + $this->assertContains("\r\n\r\n", $buffer); + $this->assertContains("bye", $buffer); } public function testRequestInvalidHttpProtocolVersionWillEmitErrorAndSendErrorResponse() @@ -344,8 +359,9 @@ function ($data) use (&$buffer) { $this->assertInstanceOf('InvalidArgumentException', $error); - $this->assertContains("HTTP/1.1 505 HTTP Version Not Supported\r\n", $buffer); - $this->assertContains("\r\n\r\nError 505: HTTP Version Not Supported", $buffer); + $this->assertContains("HTTP/1.1 505 HTTP Version not supported\r\n", $buffer); + $this->assertContains("\r\n\r\n", $buffer); + $this->assertContains("Error 505: HTTP Version Not Supported", $buffer); } public function testRequestOverflowWillEmitErrorAndSendErrorResponse() @@ -420,11 +436,13 @@ public function testBodyDataWillBeSendViaRequestEvent() $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new Server($this->socket, function (RequestInterface $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new Server($this->socket, function (RequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); $request->getBody()->on('error', $errorEvent); + + return \React\Promise\resolve(new Response()); }); $this->socket->emit('connection', array($this->connection)); @@ -447,12 +465,14 @@ public function testChunkedEncodedRequestWillBeParsedForRequestEvent() $errorEvent = $this->expectCallableNever(); $requestValidation = null; - $server = new Server($this->socket, function (RequestInterface $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent, &$requestValidation) { + $server = new Server($this->socket, function (RequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent, &$requestValidation) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); $request->getBody()->on('error', $errorEvent); $requestValidation = $request; + + return \React\Promise\resolve(new Response()); }); $this->socket->emit('connection', array($this->connection)); @@ -477,11 +497,13 @@ public function testChunkedEncodedRequestAdditionalDataWontBeEmitted() $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new Server($this->socket, function (RequestInterface $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new Server($this->socket, function (RequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); $request->getBody()->on('error', $errorEvent); + + return \React\Promise\resolve(new Response()); }); @@ -506,11 +528,13 @@ public function testEmptyChunkedEncodedRequest() $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new Server($this->socket, function (RequestInterface $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new Server($this->socket, function (RequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); $request->getBody()->on('error', $errorEvent); + + return \React\Promise\resolve(new Response()); }); $this->socket->emit('connection', array($this->connection)); @@ -532,11 +556,13 @@ public function testChunkedIsUpperCase() $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new Server($this->socket, function (RequestInterface $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new Server($this->socket, function (RequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); $request->getBody()->on('error', $errorEvent); + + return \React\Promise\resolve(new Response()); }); @@ -560,11 +586,13 @@ public function testChunkedIsMixedUpperAndLowerCase() $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new Server($this->socket, function (RequestInterface $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new Server($this->socket, function (RequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); $request->getBody()->on('error', $errorEvent); + + return \React\Promise\resolve(new Response()); }); @@ -678,7 +706,9 @@ function ($data) use (&$buffer) { public function testRequestHttp10WithoutHostEmitsRequestWithNoError() { - $server = new Server($this->socket, $this->expectCallableOnce()); + $server = new Server($this->socket, function (RequestInterface $request) { + return \React\Promise\resolve(new Response()); + }); $server->on('error', $this->expectCallableNever()); $this->socket->emit('connection', array($this->connection)); @@ -694,11 +724,13 @@ public function testWontEmitFurtherDataWhenContentLengthIsReached() $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new Server($this->socket, function (RequestInterface $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new Server($this->socket, function (RequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); $request->getBody()->on('error', $errorEvent); + + return \React\Promise\resolve(new Response()); }); $this->socket->emit('connection', array($this->connection)); @@ -722,11 +754,13 @@ public function testWontEmitFurtherDataWhenContentLengthIsReachedSplitted() $errorEvent = $this->expectCallableNever(); - $server = new Server($this->socket, function (RequestInterface $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new Server($this->socket, function (RequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); $request->getBody()->on('error', $errorEvent); + + return \React\Promise\resolve(new Response()); }); @@ -754,11 +788,13 @@ public function testContentLengthContainsZeroWillEmitEndEvent() $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new Server($this->socket, function (RequestInterface $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new Server($this->socket, function (RequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); $request->getBody()->on('error', $errorEvent); + + return \React\Promise\resolve(new Response()); }); $this->socket->emit('connection', array($this->connection)); @@ -779,11 +815,13 @@ public function testContentLengthContainsZeroWillEmitEndEventAdditionalDataWillB $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new Server($this->socket, function (RequestInterface $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new Server($this->socket, function (RequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); $request->getBody()->on('error', $errorEvent); + + return \React\Promise\resolve(new Response()); }); $this->socket->emit('connection', array($this->connection)); @@ -805,11 +843,13 @@ public function testContentLengthContainsZeroWillEmitEndEventAdditionalDataWillB $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new Server($this->socket, function (RequestInterface $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new Server($this->socket, function (RequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); $request->getBody()->on('error', $errorEvent); + + return \React\Promise\resolve(new Response()); }); $this->socket->emit('connection', array($this->connection)); @@ -835,12 +875,14 @@ public function testContentLengthWillBeIgnoredIfTransferEncodingIsSet() $errorEvent = $this->expectCallableNever(); $requestValidation = null; - $server = new Server($this->socket, function (RequestInterface $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent, &$requestValidation) { + $server = new Server($this->socket, function (RequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent, &$requestValidation) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); $request->getBody()->on('error', $errorEvent); $requestValidation = $request; + + return \React\Promise\resolve(new Response()); }); $this->socket->emit('connection', array($this->connection)); @@ -871,12 +913,14 @@ public function testInvalidContentLengthWillBeIgnoreddIfTransferEncodingIsSet() $errorEvent = $this->expectCallableNever(); $requestValidation = null; - $server = new Server($this->socket, function (RequestInterface $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent, &$requestValidation) { + $server = new Server($this->socket, function (RequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent, &$requestValidation) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); $request->getBody()->on('error', $errorEvent); $requestValidation = $request; + + return \React\Promise\resolve(new Response()); }); $this->socket->emit('connection', array($this->connection)); @@ -975,8 +1019,9 @@ function ($data) use (&$buffer) { public function testInvalidChunkHeaderResultsInErrorOnRequestStream() { $errorEvent = $this->expectCallableOnceWith($this->isInstanceOf('Exception')); - $server = new Server($this->socket, function ($request, $response) use ($errorEvent){ + $server = new Server($this->socket, function ($request) use ($errorEvent){ $request->getBody()->on('error', $errorEvent); + return \React\Promise\resolve(new Response()); }); $this->connection->expects($this->never())->method('close'); @@ -997,8 +1042,9 @@ public function testInvalidChunkHeaderResultsInErrorOnRequestStream() public function testTooLongChunkHeaderResultsInErrorOnRequestStream() { $errorEvent = $this->expectCallableOnceWith($this->isInstanceOf('Exception')); - $server = new Server($this->socket, function ($request, $response) use ($errorEvent){ + $server = new Server($this->socket, function ($request) use ($errorEvent){ $request->getBody()->on('error', $errorEvent); + return \React\Promise\resolve(new Response()); }); $this->connection->expects($this->never())->method('close'); @@ -1021,8 +1067,9 @@ public function testTooLongChunkHeaderResultsInErrorOnRequestStream() public function testTooLongChunkBodyResultsInErrorOnRequestStream() { $errorEvent = $this->expectCallableOnceWith($this->isInstanceOf('Exception')); - $server = new Server($this->socket, function ($request, $response) use ($errorEvent){ + $server = new Server($this->socket, function ($request) use ($errorEvent){ $request->getBody()->on('error', $errorEvent); + return \React\Promise\resolve(new Response()); }); $this->connection->expects($this->never())->method('close'); @@ -1043,8 +1090,9 @@ public function testTooLongChunkBodyResultsInErrorOnRequestStream() public function testUnexpectedEndOfConnectionWillResultsInErrorOnRequestStream() { $errorEvent = $this->expectCallableOnceWith($this->isInstanceOf('Exception')); - $server = new Server($this->socket, function ($request, $response) use ($errorEvent){ + $server = new Server($this->socket, function ($request) use ($errorEvent){ $request->getBody()->on('error', $errorEvent); + return \React\Promise\resolve(new Response()); }); $this->connection->expects($this->never())->method('close'); @@ -1065,7 +1113,9 @@ public function testUnexpectedEndOfConnectionWillResultsInErrorOnRequestStream() public function testErrorInChunkedDecoderNeverClosesConnection() { - $server = new Server($this->socket, $this->expectCallableOnce()); + $server = new Server($this->socket, function (RequestInterface $request) { + return \React\Promise\resolve(new Response()); + }); $this->connection->expects($this->never())->method('close'); $this->connection->expects($this->once())->method('pause'); @@ -1084,7 +1134,9 @@ public function testErrorInChunkedDecoderNeverClosesConnection() public function testErrorInLengthLimitedStreamNeverClosesConnection() { - $server = new Server($this->socket, $this->expectCallableOnce()); + $server = new Server($this->socket, function (RequestInterface $request) { + return \React\Promise\resolve(new Response()); + }); $this->connection->expects($this->never())->method('close'); $this->connection->expects($this->once())->method('pause'); @@ -1104,8 +1156,9 @@ public function testErrorInLengthLimitedStreamNeverClosesConnection() public function testCloseRequestWillPauseConnection() { - $server = new Server($this->socket, function ($request, $response) { + $server = new Server($this->socket, function ($request) { $request->getBody()->close(); + return \React\Promise\resolve(new Response()); }); $this->connection->expects($this->never())->method('close'); @@ -1124,11 +1177,13 @@ public function testEndEventWillBeEmittedOnSimpleRequest() $endEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new Server($this->socket, function ($request, $response) use ($dataEvent, $closeEvent, $endEvent, $errorEvent){ + $server = new Server($this->socket, function ($request) use ($dataEvent, $closeEvent, $endEvent, $errorEvent){ $request->getBody()->on('data', $dataEvent); $request->getBody()->on('close', $closeEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('error', $errorEvent); + + return \React\Promise\resolve(new Response()); }); $this->connection->expects($this->once())->method('pause'); @@ -1148,11 +1203,13 @@ public function testRequestWithoutDefinedLengthWillIgnoreDataEvent() $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new Server($this->socket, function (RequestInterface $request, Response $response) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new Server($this->socket, function (RequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); $request->getBody()->on('error', $errorEvent); + + return \React\Promise\resolve(new Response()); }); $this->socket->emit('connection', array($this->connection)); @@ -1165,17 +1222,22 @@ public function testRequestWithoutDefinedLengthWillIgnoreDataEvent() public function testResponseWillBeChunkDecodedByDefault() { - $server = new Server($this->socket, function (RequestInterface $request, Response $response) { - $response->writeHead(); - $response->write('hello'); + $stream = new ReadableStream(); + $server = new Server($this->socket, function (RequestInterface $request) use ($stream) { + $response = new Response(200, array(), $stream); + return \React\Promise\resolve($response); }); + $buffer = ''; $this->connection - ->expects($this->exactly(2)) + ->expects($this->any()) ->method('write') - ->withConsecutive( - array($this->anything()), - array("5\r\nhello\r\n") + ->will( + $this->returnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } + ) ); $this->socket->emit('connection', array($this->connection)); @@ -1183,25 +1245,30 @@ public function testResponseWillBeChunkDecodedByDefault() $data = $this->createGetRequest(); $this->connection->emit('data', array($data)); + $stream->emit('data', array('hello')); + + $this->assertContains("Transfer-Encoding: chunked", $buffer); + $this->assertContains("hello", $buffer); } public function testContentLengthWillBeRemovedForResponseStream() { - $server = new Server($this->socket, function (RequestInterface $request, Response $response) { - $response->writeHead( + $server = new Server($this->socket, function (RequestInterface $request) { + $response = new Response( 200, array( - 'Content-Length' => 4, + 'Content-Length' => 5, 'Transfer-Encoding' => 'chunked' - ) + ), + 'hello' ); - $response->write('hello'); + return \React\Promise\resolve($response); }); $buffer = ''; $this->connection - ->expects($this->exactly(2)) + ->expects($this->any()) ->method('write') ->will( $this->returnCallback( @@ -1218,40 +1285,43 @@ function ($data) use (&$buffer) { $this->connection->emit('data', array($data)); $this->assertNotContains("Transfer-Encoding: chunked", $buffer); - $this->assertContains("Content-Length: 4", $buffer); + $this->assertContains("Content-Length: 5", $buffer); $this->assertContains("hello", $buffer); } public function testOnlyAllowChunkedEncoding() { - $server = new Server($this->socket, function (RequestInterface $request, Response $response) { - $response->writeHead( + $stream = new ReadableStream(); + $server = new Server($this->socket, function (RequestInterface $request) use ($stream) { + $response = new Response( 200, array( 'Transfer-Encoding' => 'custom' - ) + ), + $stream ); - $response->write('hello'); + return \React\Promise\resolve($response); }); $buffer = ''; $this->connection - ->expects($this->exactly(2)) - ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) - ); + ->expects($this->any()) + ->method('write') + ->will( + $this->returnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } + ) + ); $this->socket->emit('connection', array($this->connection)); $data = $this->createGetRequest(); $this->connection->emit('data', array($data)); + $stream->emit('data', array('hello')); $this->assertContains('Transfer-Encoding: chunked', $buffer); $this->assertNotContains('Transfer-Encoding: custom', $buffer); @@ -1260,13 +1330,13 @@ function ($data) use (&$buffer) { public function testDateHeaderWillBeAddedWhenNoneIsGiven() { - $server = new Server($this->socket, function (RequestInterface $request, Response $response) { - $response->writeHead(200); + $server = new Server($this->socket, function (RequestInterface $request) { + return \React\Promise\resolve(new Response()); }); $buffer = ''; $this->connection - ->expects($this->once()) + ->expects($this->any()) ->method('write') ->will( $this->returnCallback( @@ -1289,13 +1359,14 @@ function ($data) use (&$buffer) { public function testAddCustomDateHeader() { - $server = new Server($this->socket, function (RequestInterface $request, Response $response) { - $response->writeHead(200, array("Date" => "Tue, 15 Nov 1994 08:12:31 GMT")); + $server = new Server($this->socket, function (RequestInterface $request) { + $response = new Response(200, array("Date" => "Tue, 15 Nov 1994 08:12:31 GMT")); + return \React\Promise\resolve($response); }); $buffer = ''; $this->connection - ->expects($this->once()) + ->expects($this->any()) ->method('write') ->will( $this->returnCallback( @@ -1318,13 +1389,14 @@ function ($data) use (&$buffer) { public function testRemoveDateHeader() { - $server = new Server($this->socket, function (RequestInterface $request, Response $response) { - $response->writeHead(200, array('Date' => array())); + $server = new Server($this->socket, function (RequestInterface $request) { + $response = new Response(200, array('Date' => '')); + return \React\Promise\resolve($response); }); $buffer = ''; $this->connection - ->expects($this->once()) + ->expects($this->any()) ->method('write') ->will( $this->returnCallback( @@ -1382,12 +1454,21 @@ function ($data) use (&$buffer) { public function test100ContinueRequestWillBeHandled() { - $server = new Server($this->socket, $this->expectCallableOnce()); + $server = new Server($this->socket, function (RequestInterface $request) { + return \React\Promise\resolve(new Response()); + }); + $buffer = ''; $this->connection - ->expects($this->once()) + ->expects($this->any()) ->method('write') - ->with("HTTP/1.1 100 Continue\r\n\r\n"); + ->will( + $this->returnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } + ) + ); $this->socket->emit('connection', array($this->connection)); @@ -1398,15 +1479,27 @@ public function test100ContinueRequestWillBeHandled() $data .= "\r\n"; $this->connection->emit('data', array($data)); + $this->assertContains("HTTP/1.1 100 Continue\r\n", $buffer); + $this->assertContains("HTTP/1.1 200 OK\r\n", $buffer); } public function testContinueWontBeSendForHttp10() { - $server = new Server($this->socket, $this->expectCallableOnce()); + $server = new Server($this->socket, function (RequestInterface $request) { + return \React\Promise\resolve(new Response()); + }); + $buffer = ''; $this->connection - ->expects($this->never()) - ->method('write'); + ->expects($this->any()) + ->method('write') + ->will( + $this->returnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } + ) + ); $this->socket->emit('connection', array($this->connection)); @@ -1415,13 +1508,14 @@ public function testContinueWontBeSendForHttp10() $data .= "\r\n"; $this->connection->emit('data', array($data)); + $this->assertContains("HTTP/1.0 200 OK\r\n", $buffer); + $this->assertNotContains("HTTP/1.1 100 Continue\r\n\r\n", $buffer); } public function testContinueWithLaterResponse() { - $server = new Server($this->socket, function (RequestInterface $request, Response $response) { - $response->writeHead(); - $response->end(); + $server = new Server($this->socket, function (RequestInterface $request) { + return \React\Promise\resolve(new Response()); }); @@ -1459,6 +1553,259 @@ public function testInvalidCallbackFunctionLeadsToException() $server = new Server($this->socket, 'invalid'); } + public function testHttpBodyStreamAsBodyWillStreamData() + { + $input = new ReadableStream(); + + $server = new Server($this->socket, function (RequestInterface $request) use ($input) { + $response = new Response(200, array(), $input); + return \React\Promise\resolve($response); + }); + + $buffer = ''; + $this->connection + ->expects($this->any()) + ->method('write') + ->will( + $this->returnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } + ) + ); + + $this->socket->emit('connection', array($this->connection)); + + $data = $this->createGetRequest(); + + $this->connection->emit('data', array($data)); + $input->emit('data', array('1')); + $input->emit('data', array('23')); + + $this->assertContains("HTTP/1.1 200 OK\r\n", $buffer); + $this->assertContains("\r\n\r\n", $buffer); + $this->assertContains("1\r\n1\r\n", $buffer); + $this->assertContains("2\r\n23\r\n", $buffer); + } + + public function testHttpBodyStreamWithContentLengthWillStreamTillLength() + { + $input = new ReadableStream(); + + $server = new Server($this->socket, function (RequestInterface $request) use ($input) { + $response = new Response(200, array('Content-Length' => 5), $input); + return \React\Promise\resolve($response); + }); + + $buffer = ''; + $this->connection + ->expects($this->any()) + ->method('write') + ->will( + $this->returnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } + ) + ); + + $this->socket->emit('connection', array($this->connection)); + + $data = $this->createGetRequest(); + + $this->connection->emit('data', array($data)); + $input->emit('data', array('hel')); + $input->emit('data', array('lo')); + + $this->assertContains("HTTP/1.1 200 OK\r\n", $buffer); + $this->assertContains("Content-Length: 5\r\n", $buffer); + $this->assertNotContains("Transfer-Encoding", $buffer); + $this->assertContains("\r\n\r\n", $buffer); + $this->assertContains("hello", $buffer); + } + + public function testCallbackFunctionReturnsPromise() + { + $server = new Server($this->socket, function (RequestInterface $request) { + return \React\Promise\resolve(new Response()); + }); + + $buffer = ''; + $this->connection + ->expects($this->any()) + ->method('write') + ->will( + $this->returnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } + ) + ); + + $this->socket->emit('connection', array($this->connection)); + + $data = $this->createGetRequest(); + + $this->connection->emit('data', array($data)); + $this->assertContains("HTTP/1.1 200 OK\r\n", $buffer); + $this->assertContains("\r\n\r\n", $buffer); + } + + public function testReturnInvalidTypeWillResultInError() + { + $server = new Server($this->socket, function (RequestInterface $request) { + return "invalid"; + }); + + $buffer = ''; + $this->connection + ->expects($this->any()) + ->method('write') + ->will( + $this->returnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } + ) + ); + + $this->socket->emit('connection', array($this->connection)); + + $data = "GET / HTTP/1.0\r\n\r\n"; + + $data = $this->createGetRequest(); + + $this->connection->emit('data', array($data)); + + $this->assertContains("HTTP/1.1 500 Internal Server Error\r\n", $buffer); + } + + public function testResolveWrongTypeInPromiseWillResultInError() + { + $server = new Server($this->socket, function (RequestInterface $request) { + return \React\Promise\resolve("invalid"); + }); + + $buffer = ''; + $this->connection + ->expects($this->any()) + ->method('write') + ->will( + $this->returnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } + ) + ); + + $this->socket->emit('connection', array($this->connection)); + + $data = "GET / HTTP/1.0\r\n\r\n"; + + $data = $this->createGetRequest(); + + $this->connection->emit('data', array($data)); + + $this->assertContains("HTTP/1.1 500 Internal Server Error\r\n", $buffer); + } + + public function testRejectedPromiseWillResultInErrorMessage() + { + $server = new Server($this->socket, function (RequestInterface $request) { + return new Promise(function ($resolve, $reject) { + $reject(new \Exception()); + }); + }); + $server->on('error', $this->expectCallableOnce()); + + $buffer = ''; + $this->connection + ->expects($this->any()) + ->method('write') + ->will( + $this->returnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } + ) + ); + + $this->socket->emit('connection', array($this->connection)); + + $data = "GET / HTTP/1.0\r\n\r\n"; + + $data = $this->createGetRequest(); + + $this->connection->emit('data', array($data)); + + $this->assertContains("HTTP/1.1 500 Internal Server Error\r\n", $buffer); + } + + public function testExcpetionInCallbackWillResultInErrorMessage() + { + $server = new Server($this->socket, function (RequestInterface $request) { + return new Promise(function ($resolve, $reject) { + throw new \Exception('Bad call'); + }); + }); + $server->on('error', $this->expectCallableOnce()); + + $buffer = ''; + $this->connection + ->expects($this->any()) + ->method('write') + ->will( + $this->returnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } + ) + ); + + $this->socket->emit('connection', array($this->connection)); + + $data = "GET / HTTP/1.0\r\n\r\n"; + + $data = $this->createGetRequest(); + + $this->connection->emit('data', array($data)); + + $this->assertContains("HTTP/1.1 500 Internal Server Error\r\n", $buffer); + } + + public function testHeaderWillAlwaysBeContentLengthForStringBody() + { + $server = new Server($this->socket, function (RequestInterface $request) { + return new Response(200, array('Transfer-Encoding' => 'chunked'), 'hello'); + }); + + $buffer = ''; + $this->connection + ->expects($this->any()) + ->method('write') + ->will( + $this->returnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } + ) + ); + + $this->socket->emit('connection', array($this->connection)); + + $data = "GET / HTTP/1.0\r\n\r\n"; + + $data = $this->createGetRequest(); + + $this->connection->emit('data', array($data)); + + $this->assertContains("HTTP/1.1 200 OK\r\n", $buffer); + $this->assertContains("Content-Length: 5\r\n", $buffer); + $this->assertContains("hello", $buffer); + + $this->assertNotContains("Transfer-Encoding", $buffer); + } + private function createGetRequest() { $data = "GET / HTTP/1.1\r\n"; From 909b06ccda46eea46eb8d7df674c1130a5deee94 Mon Sep 17 00:00:00 2001 From: Niels Theen Date: Mon, 27 Mar 2017 12:32:49 +0200 Subject: [PATCH 109/456] Handle Exception in callback function --- src/Server.php | 19 ++++++-- tests/ServerTest.php | 108 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+), 5 deletions(-) diff --git a/src/Server.php b/src/Server.php index 84159a99..7e77e60f 100644 --- a/src/Server.php +++ b/src/Server.php @@ -228,19 +228,29 @@ public function handleRequest(ConnectionInterface $conn, RequestInterface $reque ); $callback = $this->callback; - $promise = \React\Promise\resolve($callback($request)); + $promise = new Promise(function ($resolve, $reject) use ($callback, $request) { + $resolve($callback($request)); + }); $that = $this; $promise->then( function ($response) use ($that, $conn, $request) { if (!$response instanceof ResponseInterface) { - $that->emit('error', array(new \InvalidArgumentException('Invalid response type'))); + $message = 'The response callback is expected to resolve with an object implementing Psr\Http\Message\ResponseInterface, but resolved with "%s" instead.'; + $message = sprintf($message, is_object($response) ? get_class($response) : gettype($response)); + $exception = new \RuntimeException($message); + + $that->emit('error', array($exception)); return $that->writeError($conn, 500); } $that->handleResponse($conn, $response, $request->getProtocolVersion()); }, - function ($ex) use ($that, $conn) { - $that->emit('error', array($ex)); + function ($error) use ($that, $conn) { + $message = 'The response callback is expected to resolve with an object implementing Psr\Http\Message\ResponseInterface, but rejected with "%s" instead.'; + $message = sprintf($message, is_object($error) ? get_class($error) : gettype($error)); + $exception = new \RuntimeException($message, null, $error instanceof \Exception ? $error : null); + + $that->emit('error', array($exception)); return $that->writeError($conn, 500); } ); @@ -264,7 +274,6 @@ public function writeError(ConnectionInterface $conn, $code) $response = new Response( $code, array( - 'Content-Length' => strlen($message), 'Content-Type' => 'text/plain' ), $message diff --git a/tests/ServerTest.php b/tests/ServerTest.php index d9917375..a7d58e35 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -1657,6 +1657,11 @@ public function testReturnInvalidTypeWillResultInError() return "invalid"; }); + $exception = null; + $server->on('error', function (\Exception $ex) use (&$exception) { + $exception = $ex; + }); + $buffer = ''; $this->connection ->expects($this->any()) @@ -1678,6 +1683,7 @@ function ($data) use (&$buffer) { $this->connection->emit('data', array($data)); $this->assertContains("HTTP/1.1 500 Internal Server Error\r\n", $buffer); + $this->assertInstanceOf('RuntimeException', $exception); } public function testResolveWrongTypeInPromiseWillResultInError() @@ -1806,6 +1812,108 @@ function ($data) use (&$buffer) { $this->assertNotContains("Transfer-Encoding", $buffer); } + public function testReturnRequestWillBeHandled() + { + $server = new Server($this->socket, function (RequestInterface $request) { + return new Response(); + }); + + $buffer = ''; + $this->connection + ->expects($this->any()) + ->method('write') + ->will( + $this->returnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } + ) + ); + + $this->socket->emit('connection', array($this->connection)); + + $data = "GET / HTTP/1.0\r\n\r\n"; + + $data = $this->createGetRequest(); + + $this->connection->emit('data', array($data)); + + $this->assertContains("HTTP/1.1 200 OK\r\n", $buffer); + } + + public function testExceptionThrowInCallBackFunctionWillResultInErrorMessage() + { + $server = new Server($this->socket, function (RequestInterface $request) { + throw new \Exception('hello'); + }); + + $exception = null; + $server->on('error', function (\Exception $ex) use (&$exception) { + $exception = $ex; + }); + + $buffer = ''; + $this->connection + ->expects($this->any()) + ->method('write') + ->will( + $this->returnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } + ) + ); + + $this->socket->emit('connection', array($this->connection)); + + $data = "GET / HTTP/1.0\r\n\r\n"; + + $data = $this->createGetRequest(); + + $this->connection->emit('data', array($data)); + + $this->assertInstanceOf('RuntimeException', $exception); + $this->assertContains("HTTP/1.1 500 Internal Server Error\r\n", $buffer); + $this->assertEquals('hello', $exception->getPrevious()->getMessage()); + } + + public function testRejectOfNonExceptionWillResultInErrorMessage() + { + $server = new Server($this->socket, function (RequestInterface $request) { + return new Promise(function ($resolve, $reject) { + $reject('Invalid type'); + }); + }); + + $exception = null; + $server->on('error', function (\Exception $ex) use (&$exception) { + $exception = $ex; + }); + + $buffer = ''; + $this->connection + ->expects($this->any()) + ->method('write') + ->will( + $this->returnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } + ) + ); + + $this->socket->emit('connection', array($this->connection)); + + $data = "GET / HTTP/1.0\r\n\r\n"; + + $data = $this->createGetRequest(); + + $this->connection->emit('data', array($data)); + + $this->assertContains("HTTP/1.1 500 Internal Server Error\r\n", $buffer); + $this->assertInstanceOf('RuntimeException', $exception); + } + private function createGetRequest() { $data = "GET / HTTP/1.1\r\n"; From bc4e9bc264ea8994dcec686741a209a8ff23fa31 Mon Sep 17 00:00:00 2001 From: Niels Theen Date: Mon, 27 Mar 2017 02:41:15 +0200 Subject: [PATCH 110/456] Adapt examples to always returning a promise --- examples/01-hello-world.php | 2 +- examples/02-count-visitors.php | 9 ++++--- examples/03-stream-response.php | 20 ++++++++++----- examples/04-stream-request.php | 41 ++++++++++++++++++++----------- examples/05-error-handling.php | 35 ++++++++++++++++++++++++++ examples/11-hello-world-https.php | 9 ++++--- 6 files changed, 88 insertions(+), 28 deletions(-) create mode 100644 examples/05-error-handling.php diff --git a/examples/01-hello-world.php b/examples/01-hello-world.php index 12c36a56..ed84af11 100644 --- a/examples/01-hello-world.php +++ b/examples/01-hello-world.php @@ -4,6 +4,7 @@ use React\Socket\Server; use React\Http\Response; use Psr\Http\Message\RequestInterface; +use React\Promise\Promise; require __DIR__ . '/../vendor/autoload.php'; @@ -14,7 +15,6 @@ return new Response( 200, array( - 'Content-Length' => strlen("Hello world\n"), 'Content-Type' => 'text/plain' ), "Hello world\n" diff --git a/examples/02-count-visitors.php b/examples/02-count-visitors.php index df4bda06..2c384a3b 100644 --- a/examples/02-count-visitors.php +++ b/examples/02-count-visitors.php @@ -11,9 +11,12 @@ $socket = new Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); $counter = 0; -$server = new \React\Http\Server($socket, function (RequestInterface $request, Response $response) use (&$counter) { - $response->writeHead(200, array('Content-Type' => 'text/plain')); - $response->end("Welcome number " . ++$counter . "!\n"); +$server = new \React\Http\Server($socket, function (RequestInterface $request) use (&$counter) { + return new Response( + 200, + array('Content-Type' => 'text/plain'), + "Welcome number " . ++$counter . "!\n" + ); }); echo 'Listening on http://' . $socket->getAddress() . PHP_EOL; diff --git a/examples/03-stream-response.php b/examples/03-stream-response.php index 49617999..5fd990e8 100644 --- a/examples/03-stream-response.php +++ b/examples/03-stream-response.php @@ -4,22 +4,30 @@ use React\Socket\Server; use React\Http\Response; use Psr\Http\Message\RequestInterface; +use React\Stream\ReadableStream; require __DIR__ . '/../vendor/autoload.php'; $loop = Factory::create(); $socket = new Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); -$server = new \React\Http\Server($socket, function (RequestInterface $request, Response $response) use ($loop) { - $response->writeHead(200, array('Content-Type' => 'text/plain')); +$server = new \React\Http\Server($socket, function (RequestInterface $request) use ($loop) { + $stream = new ReadableStream(); - $timer = $loop->addPeriodicTimer(0.5, function () use ($response) { - $response->write(microtime(true) . PHP_EOL); + $timer = $loop->addPeriodicTimer(0.5, function () use ($stream) { + $stream->emit('data', array(microtime(true) . PHP_EOL)); }); - $loop->addTimer(5, function() use ($loop, $timer, $response) { + + $loop->addTimer(5, function() use ($loop, $timer, $stream) { $loop->cancelTimer($timer); - $response->end(); + $stream->emit('end'); }); + + return new Response( + 200, + array('Content-Type' => 'text/plain'), + $stream + ); }); echo 'Listening on http://' . $socket->getAddress() . PHP_EOL; diff --git a/examples/04-stream-request.php b/examples/04-stream-request.php index 9bd26c1e..481162ef 100644 --- a/examples/04-stream-request.php +++ b/examples/04-stream-request.php @@ -4,27 +4,38 @@ use React\Socket\Server; use React\Http\Response; use Psr\Http\Message\RequestInterface; +use React\Promise\Promise; require __DIR__ . '/../vendor/autoload.php'; $loop = Factory::create(); $socket = new Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); -$server = new \React\Http\Server($socket, function (RequestInterface $request, Response $response) { - $contentLength = 0; - $request->getBody()->on('data', function ($data) use (&$contentLength) { - $contentLength += strlen($data); - }); - - $request->getBody()->on('end', function () use ($response, &$contentLength){ - $response->writeHead(200, array('Content-Type' => 'text/plain')); - $response->end("The length of the submitted request body is: " . $contentLength); - }); - - // an error occures e.g. on invalid chunked encoded data or an unexpected 'end' event - $request->getBody()->on('error', function (\Exception $exception) use ($response, &$contentLength) { - $response->writeHead(400, array('Content-Type' => 'text/plain')); - $response->end("An error occured while reading at length: " . $contentLength); +$server = new \React\Http\Server($socket, function (RequestInterface $request) { + return new Promise(function ($resolve, $reject) use ($request) { + $contentLength = 0; + $request->getBody()->on('data', function ($data) use (&$contentLength) { + $contentLength += strlen($data); + }); + + $request->getBody()->on('end', function () use ($resolve, &$contentLength){ + $response = new Response( + 200, + array('Content-Type' => 'text/plain'), + "The length of the submitted request body is: " . $contentLength + ); + $resolve($response); + }); + + // an error occures e.g. on invalid chunked encoded data or an unexpected 'end' event + $request->getBody()->on('error', function (\Exception $exception) use ($resolve, &$contentLength) { + $response = new Response( + 400, + array('Content-Type' => 'text/plain'), + "An error occured while reading at length: " . $contentLength + ); + $resolve($response); + }); }); }); diff --git a/examples/05-error-handling.php b/examples/05-error-handling.php new file mode 100644 index 00000000..29f54b92 --- /dev/null +++ b/examples/05-error-handling.php @@ -0,0 +1,35 @@ + 'text/plain'), + "Hello World!\n" + ); + + $resolve($response); + }); +}); + +echo 'Listening on http://' . $socket->getAddress() . PHP_EOL; + +$loop->run(); diff --git a/examples/11-hello-world-https.php b/examples/11-hello-world-https.php index 29e5aab2..191e7d62 100644 --- a/examples/11-hello-world-https.php +++ b/examples/11-hello-world-https.php @@ -14,9 +14,12 @@ 'local_cert' => isset($argv[2]) ? $argv[2] : __DIR__ . '/localhost.pem' )); -$server = new \React\Http\Server($socket, function (RequestInterface $request, Response $response) { - $response->writeHead(200, array('Content-Type' => 'text/plain')); - $response->end("Hello world!\n"); +$server = new \React\Http\Server($socket, function (RequestInterface $request) { + return new Response( + 200, + array('Content-Type' => 'text/plain'), + "Hello world!\n" + ); }); //$socket->on('error', 'printf'); From ce27ec6d93d8d884a7261d9a5677da64c5eda191 Mon Sep 17 00:00:00 2001 From: Niels Theen Date: Fri, 24 Mar 2017 13:57:30 +0100 Subject: [PATCH 111/456] Update README --- README.md | 279 ++++++++++++++++++++++++++++++++++--------------- src/Server.php | 34 +++--- 2 files changed, 214 insertions(+), 99 deletions(-) diff --git a/README.md b/README.md index 611fc5b9..c7e14212 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,6 @@ Event-driven, streaming plaintext HTTP and secure HTTPS server for [ReactPHP](ht * [Server](#server) * [Request](#request) * [Response](#response) - * [writeHead()](#writehead) * [Install](#install) * [Tests](#tests) * [License](#license) @@ -24,9 +23,12 @@ This is an HTTP server which responds with `Hello World` to every request. $loop = React\EventLoop\Factory::create(); $socket = new React\Socket\Server(8080, $loop); -$http = new Server($socket, function (RequestInterface $request, Response $response) { - $response->writeHead(200, array('Content-Type' => 'text/plain')); - $response->end("Hello World!\n"); +$http = new Server($socket, function (RequestInterface $request) { + return new Response( + 200, + array('Content-Type' => 'text/plain'), + "Hello World!\n" + ); }); $loop->run(); @@ -52,9 +54,12 @@ constructor with the respective [request](#request) and ```php $socket = new React\Socket\Server(8080, $loop); -$http = new Server($socket, function (RequestInterface $request, Response $response) { - $response->writeHead(200, array('Content-Type' => 'text/plain')); - $response->end("Hello World!\n"); +$http = new Server($socket, function (RequestInterface $request) { + return new Response( + 200, + array('Content-Type' => 'text/plain'), + "Hello World!\n" + ); }); ``` @@ -70,9 +75,12 @@ $socket = new React\Socket\SecureServer($socket, $loop, array( 'local_cert' => __DIR__ . '/localhost.pem' )); -$http = new Server($socket, function (RequestInterface $request, Response $response) { - $response->writeHead(200, array('Content-Type' => 'text/plain')); - $response->end("Hello World!\n"); +$http = new Server($socket, function (RequestInterface $request) { + return new Response( + 200, + array('Content-Type' => 'text/plain'), + "Hello World!\n" + ); }); ``` @@ -102,6 +110,22 @@ $http->on('error', function (Exception $e) { }); ``` +The server will also emit an `error` event if you return an invalid +type in the callback function or have a unhandled `Exception`. +If your callback function throws an exception, +the `Server` will emit a `RuntimeException` and add the thrown exception +as previous: + +```php +$http->on('error', function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; + if ($e->getPrevious() !== null) { + $previousException = $e->getPrevious(); + echo $previousException->getMessage() . PHP_EOL; + } +}); +``` + Note that the request object can also emit an error. Check out [request](#request) for more details. @@ -117,10 +141,15 @@ This request object implements the and will be passed to the callback function like this. ```php -$http = new Server($socket, function (RequestInterface $request, Response $response) { - $response->writeHead(200, array('Content-Type' => 'text/plain')); - $response->write("The method of the request is: " . $request->getMethod()); - $response->end("The requested path is: " . $request->getUri()->getPath()); +$http = new Server($socket, function (RequestInterface $request) { + $body = "The method of the request is: " . $request->getMethod(); + $body .= "The requested path is: " . $request->getUri()->getPath(); + + return new Response( + 200, + array('Content-Type' => 'text/plain'), + $body + ); }); ``` @@ -155,22 +184,31 @@ Instead, you should use the `ReactPHP ReadableStreamInterface` which gives you access to the incoming request body as the individual chunks arrive: ```php -$http = new Server($socket, function (RequestInterface $request, Response $response) { - $contentLength = 0; - $body = $request->getBody(); - $body->on('data', function ($data) use (&$contentLength) { - $contentLength += strlen($data); - }); - - $body->on('end', function () use ($response, &$contentLength){ - $response->writeHead(200, array('Content-Type' => 'text/plain')); - $response->end("The length of the submitted request body is: " . $contentLength); - }); - - // an error occures e.g. on invalid chunked encoded data or an unexpected 'end' event - $body->on('error', function (\Exception $exception) use ($response, &$contentLength) { - $response->writeHead(400, array('Content-Type' => 'text/plain')); - $response->end("An error occured while reading at length: " . $contentLength); +$http = new Server($socket, function (RequestInterface $request) { + return new Promise(function ($resolve, $reject) use ($request) { + $contentLength = 0; + $request->getBody()->on('data', function ($data) use (&$contentLength) { + $contentLength += strlen($data); + }); + + $request->getBody()->on('end', function () use ($resolve, &$contentLength){ + $response = new Response( + 200, + array('Content-Type' => 'text/plain'), + "The length of the submitted request body is: " . $contentLength + ); + $resolve($response); + }); + + // an error occures e.g. on invalid chunked encoded data or an unexpected 'end' event + $request->getBody()->on('error', function (\Exception $exception) use ($resolve, &$contentLength) { + $response = new Response( + 400, + array('Content-Type' => 'text/plain'), + "An error occured while reading at length: " . $contentLength + ); + $resolve($response); + }); }); }); ``` @@ -210,109 +248,176 @@ Note that this value may be `null` if the request body size is unknown in advance because the request message uses chunked transfer encoding. ```php -$http = new Server($socket, function (RequestInterface $request, Response $response) { +$http = new Server($socket, function (RequestInterface $request) { $size = $request->getBody()->getSize(); if ($size === null) { - $response->writeHead(411, array('Content-Type' => 'text/plain')); - $response->write('The request does not contain an explicit length.'); - $response->write('This server does not accept chunked transfer encoding.'); - $response->end(); - return; + $body = 'The request does not contain an explicit length.'; + $body .= 'This server does not accept chunked transfer encoding.'; + + return new Response( + 411, + array('Content-Type' => 'text/plain'), + $body + ); } - $response->writeHead(200, array('Content-Type' => 'text/plain')); - $response->end("Request body size: " . $size . " bytes\n"); + + return new Response( + 200, + array('Content-Type' => 'text/plain'), + "Request body size: " . $size . " bytes\n" + ); }); ``` ### Response -The `Response` class is responsible for streaming the outgoing response body. +The callback function passed to the constructor of the [Server](#server) +is responsible for processing the request and returning a response, +which will be delivered to the client. +This function MUST return an instance imlementing +[PSR-7 ResponseInterface](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-7-http-message.md#33-psrhttpmessageresponseinterface) +object or a +[ReactPHP Promise](https://github.com/reactphp/promise#reactpromise) +which will resolve a `PSR-7 ResponseInterface` object. + +You will find a `Response` class +which implements the `PSR-7 ResponseInterface` in this project. +We use instantiation of this class in our projects, +but feel free to use any implemantation of the +`PSR-7 ResponseInterface` you prefer. -It implements the `WritableStreamInterface`. - -See also [example #3](examples) for more details. +```php +$http = new Server($socket, function (RequestInterface $request) { + return new Response( + 200, + array('Content-Type' => 'text/plain'), + "Hello World!\n" + ); +}); +``` -The constructor is internal, you SHOULD NOT call this yourself. -The `Server` is responsible for emitting `Request` and `Response` objects. +The example above returns the response directly, because it needs +no time to be processed. +Using a database, the file system or long calculations +(in fact every action that will take >=1ms) to create your +response, will slow down the server. +To prevent this you SHOULD use a +[ReactPHP Promise](https://github.com/reactphp/promise#reactpromise). +This example shows how such a long-term action could look like: -The `Response` will automatically use the same HTTP protocol version as the -corresponding `Request`. +```php +$server = new \React\Http\Server($socket, function (RequestInterface $request) use ($loop) { + return new Promise(function ($resolve, $reject) use ($request, $loop) { + $loop->addTimer(1.5, function() use ($loop, $resolve) { + $response = new Response( + 200, + array('Content-Type' => 'text/plain'), + "Hello world" + ); + $resolve($response); + }); + }); +}); +``` -HTTP/1.1 responses will automatically apply chunked transfer encoding if -no `Content-Length` header has been set. -See [`writeHead()`](#writehead) for more details. +The above example will create a response after 1.5 second. +This example shows that you need a promise, +if your response needs time to created. +The `ReactPHP Promise` will resolve in a `Response` object when the request +body ends. -See the above usage example and the class outline for details. +The `Response` class in this project supports to add an instance which implements the +[ReactPHP ReadableStreamInterface](https://github.com/reactphp/stream#readablestreaminterface) +for the response body. +So you are able stream data directly into the response body. +Note that other implementations of the `PSR-7 ResponseInterface` likely +only support string. -#### writeHead() +```php +$server = new Server($socket, function (RequestInterface $request) use ($loop) { + $stream = new ReadableStream(); -The `writeHead(int $status = 200, array $headers = array(): void` method can be used to -write the given HTTP message header. + $timer = $loop->addPeriodicTimer(0.5, function () use ($stream) { + $stream->emit('data', array(microtime(true) . PHP_EOL)); + }); -This method MUST be invoked once before calling `write()` or `end()` to send -the actual HTTP message body: + $loop->addTimer(5, function() use ($loop, $timer, $stream) { + $loop->cancelTimer($timer); + $stream->emit('end'); + }); -```php -$response->writeHead(200, array( - 'Content-Type' => 'text/plain' -)); -$response->end('Hello World!'); + return new Response(200, array('Content-Type' => 'text/plain'), $stream); +}); ``` -Calling this method more than once will result in an `Exception` -(unless the response has ended/closed already). -Calling this method after the response has ended/closed is a NOOP. +The above example will emit every 0.5 seconds the current Unix timestamp +with microseconds as float to the client and will end after 5 seconds. +This is just a example you could use of the streaming, +you could also send a big amount of data via little chunks +or use it for body data that needs to calculated. -Unless you specify a `Content-Length` header yourself, HTTP/1.1 responses -will automatically use chunked transfer encoding and send the respective header +If the response body is a `string` a `Content-Length` header will be added automatically. +Unless you specify a `Content-Length` header for a ReactPHP `ReadableStreamInterface` +response body yourself, HTTP/1.1 responses will automatically use chunked transfer encoding +and send the respective header (`Transfer-Encoding: chunked`) automatically. The server is responsible for handling `Transfer-Encoding` so you SHOULD NOT pass it yourself. -If you know the length of your body, you MAY specify it like this instead: +If you know the length of your stream body, you MAY specify it like this instead: ```php -$data = 'Hello World!'; - -$response->writeHead(200, array( - 'Content-Type' => 'text/plain', - 'Content-Length' => strlen($data) -)); -$response->end($data); +$stream = new ReadableStream() +$server = new Server($socket, function (RequestInterface $request) use ($loop, $stream) { + return new Response( + 200, + array( + 'Content-Length' => '5', + 'Content-Type' => 'text/plain', + ), + $stream + ); +}); ``` +An invalid return value or an unhandled `Exception` in the code of the callback +function, will result in an `500 Internal Server Error` message. +Make sure to catch `Exceptions` to create own response messages. + +After the return in the callback function the response will be processed by the `Server`. +The `Server` will add the protocol version of the request, so you don't have to. A `Date` header will be automatically added with the system date and time if none is given. You can add a custom `Date` header yourself like this: ```php -$response->writeHead(200, array( - 'Date' => date('D, d M Y H:i:s T') -)); +$server = new Server($socket, function (RequestInterface $request) { + return new Response(200, array('Date' => date('D, d M Y H:i:s T'))); +}); ``` If you don't have a appropriate clock to rely on, you should -unset this header with an empty array: +unset this header with an empty string: ```php -$response->writeHead(200, array( - 'Date' => array() -)); +$server = new Server($socket, function (RequestInterface $request) { + return new Response(200, array('Date' => '')); +}); ``` Note that it will automatically assume a `X-Powered-By: react/alpha` header unless your specify a custom `X-Powered-By` header yourself: ```php -$response->writeHead(200, array( - 'X-Powered-By' => 'PHP 3' -)); +$server = new Server($socket, function (RequestInterface $request) { + return new Response(200, array('X-Powered-By' => 'PHP 3')); +}); ``` -If you do not want to send this header at all, you can use an empty array as +If you do not want to send this header at all, you can use an empty string as value like this: ```php -$response->writeHead(200, array( - 'X-Powered-By' => array() -)); +$server = new Server($socket, function (RequestInterface $request) { + return new Response(200, array('X-Powered-By' => '')); +}); ``` Note that persistent connections (`Connection: keep-alive`) are currently diff --git a/src/Server.php b/src/Server.php index 7e77e60f..83bcabd7 100644 --- a/src/Server.php +++ b/src/Server.php @@ -20,18 +20,23 @@ * as HTTP. * * For each request, it executes the callback function passed to the - * constructor with the respective [`Request`](#request) and - * [`Response`](#response) objects: + * constructor with the respective [request](#request) and + * [response](#response) objects: * * ```php * $socket = new React\Socket\Server(8080, $loop); * - * $http = new Server($socket, function (RequestInterface $request, Response $response) { - * $response->writeHead(200, array('Content-Type' => 'text/plain')); - * $response->end("Hello World!\n"); + * $http = new Server($socket, function (RequestInterface $request) { + * return new Response( + * 200, + * array('Content-Type' => 'text/plain'), + * "Hello World!\n" + * ); * }); * ``` * + * See also the [first example](examples) for more details. + * * Similarly, you can also attach this to a * [`React\Socket\SecureServer`](https://github.com/reactphp/socket#secureserver) * in order to start a secure HTTPS server like this: @@ -42,12 +47,17 @@ * 'local_cert' => __DIR__ . '/localhost.pem' * )); * - * $http = new Server($socket, function (RequestInterface $request, Response $response) { - * $response->writeHead(200, array('Content-Type' => 'text/plain')); - * $response->end("Hello World!\n"); + * $http = new Server($socket, function (RequestInterface $request) { + * return new Response( + * 200, + * array('Content-Type' => 'text/plain'), + * "Hello World!\n" + * ); * }); * ``` * + * See also [example #11](examples) for more details. + * * When HTTP/1.1 clients want to send a bigger request body, they MAY send only * the request headers with an additional `Expect: 100-continue` header and * wait before sending the actual (large) message body. @@ -57,8 +67,8 @@ * The [Response](#response) still needs to be created as described in the * examples above. * - * See also [`Request`](#request) and [`Response`](#response) - * for more details(e.g. the request data body). + * See also [request](#request) and [response](#response) + * for more details (e.g. the request data body). * * The `Server` supports both HTTP/1.1 and HTTP/1.0 request messages. * If a client sends an invalid request message, uses an invalid HTTP protocol @@ -72,8 +82,8 @@ * }); * ``` * - * The request object can also emit an error. Checkout [Request](#request) - * for more details. + * Note that the request object can also emit an error. + * Check out [request](#request) for more details. * * @see Request * @see Response From 7cb1ddd2ca404769afe4a46c9da871142b9f959b Mon Sep 17 00:00:00 2001 From: Jan Sorgalla Date: Thu, 30 Mar 2017 15:34:09 +0200 Subject: [PATCH 112/456] Support \Throwable when setting previous exception from server callback --- src/Server.php | 9 ++++++++- tests/ServerTest.php | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/src/Server.php b/src/Server.php index 83bcabd7..256e7867 100644 --- a/src/Server.php +++ b/src/Server.php @@ -258,7 +258,14 @@ function ($response) use ($that, $conn, $request) { function ($error) use ($that, $conn) { $message = 'The response callback is expected to resolve with an object implementing Psr\Http\Message\ResponseInterface, but rejected with "%s" instead.'; $message = sprintf($message, is_object($error) ? get_class($error) : gettype($error)); - $exception = new \RuntimeException($message, null, $error instanceof \Exception ? $error : null); + + $previous = null; + + if ($error instanceof \Throwable || $error instanceof \Exception) { + $previous = $error; + } + + $exception = new \RuntimeException($message, null, $previous); $that->emit('error', array($exception)); return $that->writeError($conn, 500); diff --git a/tests/ServerTest.php b/tests/ServerTest.php index a7d58e35..3b251bff 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -1877,6 +1877,43 @@ function ($data) use (&$buffer) { $this->assertEquals('hello', $exception->getPrevious()->getMessage()); } + /** + * @requires PHP 7 + */ + public function testThrowableThrowInCallBackFunctionWillResultInErrorMessage() + { + $server = new Server($this->socket, function (RequestInterface $request) { + throw new \Error('hello'); + }); + + $exception = null; + $server->on('error', function (\Exception $ex) use (&$exception) { + $exception = $ex; + }); + + $buffer = ''; + $this->connection + ->expects($this->any()) + ->method('write') + ->will( + $this->returnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } + ) + ); + + $this->socket->emit('connection', array($this->connection)); + + $data = $this->createGetRequest(); + + $this->connection->emit('data', array($data)); + + $this->assertInstanceOf('RuntimeException', $exception); + $this->assertContains("HTTP/1.1 500 Internal Server Error\r\n", $buffer); + $this->assertEquals('hello', $exception->getPrevious()->getMessage()); + } + public function testRejectOfNonExceptionWillResultInErrorMessage() { $server = new Server($this->socket, function (RequestInterface $request) { From 9d407e5a145ff6b7eb2a74e0841a6c9c3fefb226 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 30 Mar 2017 15:41:48 +0200 Subject: [PATCH 113/456] Forward compatibility with upcoming Socket v0.6 and v0.7 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index a08eb15f..ba6ac438 100644 --- a/composer.json +++ b/composer.json @@ -6,7 +6,7 @@ "require": { "php": ">=5.3.0", "ringcentral/psr7": "^1.2", - "react/socket": "^0.5", + "react/socket": "^0.7 || ^0.6 || ^0.5", "react/stream": "^0.6 || ^0.5 || ^0.4.4", "react/promise": "^2.0 || ^1.1", "evenement/evenement": "^2.0 || ^1.0" From 738b147774f39f544f28c9f3cd7cc87aec0fc944 Mon Sep 17 00:00:00 2001 From: Jan Sorgalla Date: Thu, 30 Mar 2017 20:39:07 +0200 Subject: [PATCH 114/456] Workaround for react/promise:^1.0 on PHP 7 which does not support \Throwable --- tests/ServerTest.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 3b251bff..e14fca33 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -1907,7 +1907,15 @@ function ($data) use (&$buffer) { $data = $this->createGetRequest(); - $this->connection->emit('data', array($data)); + try { + $this->connection->emit('data', array($data)); + } catch (\Error $e) { + $this->markTestSkipped( + 'A \Throwable bubbled out of the request callback. ' . + 'This happened most probably due to react/promise:^1.0 being installed ' . + 'which does not support \Throwable.' + ); + } $this->assertInstanceOf('RuntimeException', $exception); $this->assertContains("HTTP/1.1 500 Internal Server Error\r\n", $buffer); From ba342e0490574a3097f4b4a4dfb982e44be787aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 30 Mar 2017 23:12:01 +0200 Subject: [PATCH 115/456] Always use same HTTP protocol version for automatic error responses --- src/Server.php | 35 ++++++++++++++++++++--------------- tests/ServerTest.php | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 15 deletions(-) diff --git a/src/Server.php b/src/Server.php index 83bcabd7..221c195f 100644 --- a/src/Server.php +++ b/src/Server.php @@ -8,8 +8,8 @@ use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use RingCentral; -use React\Stream\ReadableStream; use React\Promise\Promise; +use RingCentral\Psr7\Request; /** * The `Server` class is responsible for handling incoming connections and then @@ -176,7 +176,8 @@ public function handleRequest(ConnectionInterface $conn, RequestInterface $reque // only support HTTP/1.1 and HTTP/1.0 requests if ($request->getProtocolVersion() !== '1.1' && $request->getProtocolVersion() !== '1.0') { $this->emit('error', array(new \InvalidArgumentException('Received request with invalid protocol version'))); - return $this->writeError($conn, 505); + $request = $request->withProtocolVersion('1.1'); + return $this->writeError($conn, 505, $request); } // HTTP/1.1 requests MUST include a valid host header (host and optional port) @@ -193,7 +194,7 @@ public function handleRequest(ConnectionInterface $conn, RequestInterface $reque unset($parts['scheme'], $parts['host'], $parts['port']); if ($parts === false || $parts) { $this->emit('error', array(new \InvalidArgumentException('Invalid Host header for HTTP/1.1 request'))); - return $this->writeError($conn, 400); + return $this->writeError($conn, 400, $request); } } @@ -203,7 +204,7 @@ public function handleRequest(ConnectionInterface $conn, RequestInterface $reque 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); + return $this->writeError($conn, 501, $request); } $stream = new ChunkedDecoder($stream); @@ -219,7 +220,7 @@ public function handleRequest(ConnectionInterface $conn, RequestInterface $reque if ((string)$contentLength !== (string)$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); + return $this->writeError($conn, 400, $request); } $stream = new LengthLimitedStream($stream, $contentLength); @@ -251,17 +252,17 @@ function ($response) use ($that, $conn, $request) { $exception = new \RuntimeException($message); $that->emit('error', array($exception)); - return $that->writeError($conn, 500); + return $that->writeError($conn, 500, $request); } - $that->handleResponse($conn, $response, $request->getProtocolVersion()); + $that->handleResponse($conn, $request, $response); }, - function ($error) use ($that, $conn) { + function ($error) use ($that, $conn, $request) { $message = 'The response callback is expected to resolve with an object implementing Psr\Http\Message\ResponseInterface, but rejected with "%s" instead.'; $message = sprintf($message, is_object($error) ? get_class($error) : gettype($error)); $exception = new \RuntimeException($message, null, $error instanceof \Exception ? $error : null); $that->emit('error', array($exception)); - return $that->writeError($conn, 500); + return $that->writeError($conn, 500, $request); } ); @@ -274,7 +275,7 @@ function ($error) use ($that, $conn) { } /** @internal */ - public function writeError(ConnectionInterface $conn, $code) + public function writeError(ConnectionInterface $conn, $code, RequestInterface $request = null) { $message = 'Error ' . $code; if (isset(ResponseCodes::$statusTexts[$code])) { @@ -289,14 +290,18 @@ public function writeError(ConnectionInterface $conn, $code) $message ); - $this->handleResponse($conn, $response, '1.1'); + if ($request === null) { + $request = new Request('GET', '/', array(), null, '1.1'); + } + + $this->handleResponse($conn, $request, $response); } /** @internal */ - public function handleResponse(ConnectionInterface $connection, ResponseInterface $response, $protocolVersion) + public function handleResponse(ConnectionInterface $connection, RequestInterface $request, ResponseInterface $response) { - $response = $response->withProtocolVersion($protocolVersion); + $response = $response->withProtocolVersion($request->getProtocolVersion()); // assign default "X-Powered-By" header as first for history reasons if (!$response->hasHeader('X-Powered-By')) { @@ -321,14 +326,14 @@ public function handleResponse(ConnectionInterface $connection, ResponseInterfac if (!$response->getBody() instanceof HttpBodyStream) { $response = $response->withHeader('Content-Length', $response->getBody()->getSize()); - } elseif (!$response->hasHeader('Content-Length') && $protocolVersion === '1.1') { + } elseif (!$response->hasHeader('Content-Length') && $request->getProtocolVersion() === '1.1') { // assign chunked transfer-encoding if no 'content-length' is given for HTTP/1.1 responses $response = $response->withHeader('Transfer-Encoding', 'chunked'); } // HTTP/1.1 assumes persistent connection support by default // we do not support persistent connections, so let the client know - if ($protocolVersion === '1.1') { + if ($request->getProtocolVersion() === '1.1') { $response = $response->withHeader('Connection', 'close'); } diff --git a/tests/ServerTest.php b/tests/ServerTest.php index a7d58e35..284a6c9d 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -1452,6 +1452,39 @@ function ($data) use (&$buffer) { $this->assertInstanceOf('InvalidArgumentException', $error); } + public function testOnlyChunkedEncodingIsAllowedForTransferEncodingWithHttp10() + { + $error = null; + + $server = new Server($this->socket, $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; + } + ) + ); + $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 test100ContinueRequestWillBeHandled() { $server = new Server($this->socket, function (RequestInterface $request) { From 28c02e0f95cdc0fb9e456fb9a2016fc816d8d217 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 31 Mar 2017 00:03:40 +0200 Subject: [PATCH 116/456] Responses to HEAD requests and certain status codes never contain a body See https://tools.ietf.org/html/rfc7230#section-3.3 --- README.md | 7 +++ src/Server.php | 15 ++++-- tests/ServerTest.php | 117 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 134 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index c7e14212..03b037d8 100644 --- a/README.md +++ b/README.md @@ -377,6 +377,7 @@ $server = new Server($socket, function (RequestInterface $request) use ($loop, $ ); }); ``` + An invalid return value or an unhandled `Exception` in the code of the callback function, will result in an `500 Internal Server Error` message. Make sure to catch `Exceptions` to create own response messages. @@ -384,6 +385,12 @@ Make sure to catch `Exceptions` to create own response messages. After the return in the callback function the response will be processed by the `Server`. The `Server` will add the protocol version of the request, so you don't have to. +Any response to a `HEAD` request and any response with a `1xx` (Informational), +`204` (No Content) or `304` (Not Modified) status code will *not* include a +message body as per the HTTP spec. +This means that your callback does not have to take special care of this and any +response body will simply be ignored. + A `Date` header will be automatically added with the system date and time if none is given. You can add a custom `Date` header yourself like this: diff --git a/src/Server.php b/src/Server.php index 221c195f..f8eb4e8b 100644 --- a/src/Server.php +++ b/src/Server.php @@ -7,9 +7,8 @@ use React\Socket\ConnectionInterface; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; -use RingCentral; use React\Promise\Promise; -use RingCentral\Psr7\Request; +use RingCentral\Psr7 as Psr7Implementation; /** * The `Server` class is responsible for handling incoming connections and then @@ -291,7 +290,7 @@ public function writeError(ConnectionInterface $conn, $code, RequestInterface $r ); if ($request === null) { - $request = new Request('GET', '/', array(), null, '1.1'); + $request = new Psr7Implementation\Request('GET', '/', array(), null, '1.1'); } $this->handleResponse($conn, $request, $response); @@ -337,13 +336,19 @@ public function handleResponse(ConnectionInterface $connection, RequestInterface $response = $response->withHeader('Connection', 'close'); } + // response to HEAD and 1xx, 204 and 304 responses MUST NOT include a body + $code = $response->getStatusCode(); + if ($request->getMethod() === 'HEAD' || ($code >= 100 && $code < 200) || $code === 204 || $code === 304) { + $response = $response->withBody(Psr7Implementation\stream_for('')); + } + $this->handleResponseBody($response, $connection); } private function handleResponseBody(ResponseInterface $response, ConnectionInterface $connection) { if (!$response->getBody() instanceof HttpBodyStream) { - $connection->write(RingCentral\Psr7\str($response)); + $connection->write(Psr7Implementation\str($response)); return $connection->end(); } @@ -354,7 +359,7 @@ private function handleResponseBody(ResponseInterface $response, ConnectionInter $stream = new ChunkedEncoder($body); } - $connection->write(RingCentral\Psr7\str($response)); + $connection->write(Psr7Implementation\str($response)); $stream->pipe($connection); } } diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 284a6c9d..280ad702 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -331,6 +331,87 @@ function ($data) use (&$buffer) { $this->assertContains("bye", $buffer); } + public function testResponseContainsNoResponseBodyForHeadRequest() + { + $server = new Server($this->socket, function (RequestInterface $request) { + return new Response(200, array(), 'bye'); + }); + + $buffer = ''; + $this->connection + ->expects($this->any()) + ->method('write') + ->will( + $this->returnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } + ) + ); + + $this->socket->emit('connection', array($this->connection)); + + $data = "HEAD / HTTP/1.1\r\nHost: localhost\r\n\r\n"; + $this->connection->emit('data', array($data)); + + $this->assertContains("HTTP/1.1 200 OK\r\n", $buffer); + $this->assertNotContains("bye", $buffer); + } + + public function testResponseContainsNoResponseBodyForNoContentStatus() + { + $server = new Server($this->socket, function (RequestInterface $request) { + return new Response(204, array(), 'bye'); + }); + + $buffer = ''; + $this->connection + ->expects($this->any()) + ->method('write') + ->will( + $this->returnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } + ) + ); + + $this->socket->emit('connection', array($this->connection)); + + $data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"; + $this->connection->emit('data', array($data)); + + $this->assertContains("HTTP/1.1 204 No Content\r\n", $buffer); + $this->assertNotContains("bye", $buffer); + } + + public function testResponseContainsNoResponseBodyForNotModifiedStatus() + { + $server = new Server($this->socket, function (RequestInterface $request) { + return new Response(304, array(), 'bye'); + }); + + $buffer = ''; + $this->connection + ->expects($this->any()) + ->method('write') + ->will( + $this->returnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } + ) + ); + + $this->socket->emit('connection', array($this->connection)); + + $data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"; + $this->connection->emit('data', array($data)); + + $this->assertContains("HTTP/1.1 304 Not Modified\r\n", $buffer); + $this->assertNotContains("bye", $buffer); + } + public function testRequestInvalidHttpProtocolVersionWillEmitErrorAndSendErrorResponse() { $error = null; @@ -980,6 +1061,42 @@ function ($data) use (&$buffer) { $this->assertInstanceOf('InvalidArgumentException', $error); } + public function testNonIntegerContentLengthValueWillLeadToErrorWithNoBodyForHeadRequest() + { + $error = null; + $server = new Server($this->socket, $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; + } + ) + ); + + $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 testMultipleIntegerInContentLengthWillLeadToError() { $error = null; From 05786a0784ec4a4be90a13801f7f17f5452f7a72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 11 Mar 2017 17:52:00 +0100 Subject: [PATCH 117/456] Support asterisk-form request target for OPTIONS method See https://tools.ietf.org/html/rfc7230#section-5.3.4 --- src/RequestHeaderParser.php | 12 ++++++++++++ tests/ServerTest.php | 23 ++++++++++++++++++++++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/RequestHeaderParser.php b/src/RequestHeaderParser.php index d9feda1a..b24325f2 100644 --- a/src/RequestHeaderParser.php +++ b/src/RequestHeaderParser.php @@ -55,8 +55,20 @@ private function parseRequest($data) { list($headers, $bodyBuffer) = explode("\r\n\r\n", $data, 2); + $asterisk = false; + if (strpos($headers, 'OPTIONS * ') === 0) { + $asterisk = true; + $headers = 'OPTIONS / ' . substr($headers, 10); + } + $request = g7\parse_request($headers); + if ($asterisk) { + $request = $request->withUri( + $request->getUri()->withPath('') + )->withRequestTarget('*'); + } + return array($request, $bodyBuffer); } } diff --git a/tests/ServerTest.php b/tests/ServerTest.php index a7d58e35..72d5f690 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -98,10 +98,31 @@ function ($data) use (&$buffer) { $this->assertSame(1, $i); $this->assertInstanceOf('RingCentral\Psr7\Request', $requestAssertion); - $this->assertSame('/', $requestAssertion->getUri()->getPath()); $this->assertSame('GET', $requestAssertion->getMethod()); + $this->assertSame('/', $requestAssertion->getRequestTarget()); + $this->assertSame('/', $requestAssertion->getUri()->getPath()); + $this->assertSame('http://example.com/', (string)$requestAssertion->getUri()); $this->assertSame('127.0.0.1', $requestAssertion->remoteAddress); + } + + public function testRequestOptionsAsterisk() + { + $requestAssertion = null; + $server = new Server($this->socket, function ($request) use (&$requestAssertion) { + $requestAssertion = $request; + return new Response(); + }); + + $this->socket->emit('connection', array($this->connection)); + $data = "OPTIONS * HTTP/1.1\r\nHost: example.com\r\n\r\n"; + $this->connection->emit('data', array($data)); + + $this->assertInstanceOf('RingCentral\Psr7\Request', $requestAssertion); + $this->assertSame('OPTIONS', $requestAssertion->getMethod()); + $this->assertSame('*', $requestAssertion->getRequestTarget()); + $this->assertSame('', $requestAssertion->getUri()->getPath()); + $this->assertSame('http://example.com', (string)$requestAssertion->getUri()); } public function testRequestPauseWillbeForwardedToConnection() From 39ab610341a4b388ba7135e07cba35c9f8489a77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 31 Mar 2017 01:21:36 +0200 Subject: [PATCH 118/456] Certain status codes never contain a body length See https://tools.ietf.org/html/rfc7230#section-3.3.1 and https://tools.ietf.org/html/rfc7230#section-3.3.2 --- README.md | 24 +++++++++++++++++------- src/Server.php | 7 ++++++- tests/ServerTest.php | 4 +++- 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 03b037d8..7a5ca69b 100644 --- a/README.md +++ b/README.md @@ -356,12 +356,14 @@ This is just a example you could use of the streaming, you could also send a big amount of data via little chunks or use it for body data that needs to calculated. -If the response body is a `string` a `Content-Length` header will be added automatically. -Unless you specify a `Content-Length` header for a ReactPHP `ReadableStreamInterface` -response body yourself, HTTP/1.1 responses will automatically use chunked transfer encoding -and send the respective header -(`Transfer-Encoding: chunked`) automatically. The server is responsible for handling -`Transfer-Encoding` so you SHOULD NOT pass it yourself. +If the response body is a `string`, a `Content-Length` header will be added +automatically. +If the response body is a ReactPHP `ReadableStreamInterface` and you do not +specify a `Content-Length` header, HTTP/1.1 responses will automatically use +chunked transfer encoding and send the respective header +(`Transfer-Encoding: chunked`) automatically. +The server is responsible for handling `Transfer-Encoding`, so you SHOULD NOT +pass this header yourself. If you know the length of your stream body, you MAY specify it like this instead: ```php @@ -387,10 +389,18 @@ The `Server` will add the protocol version of the request, so you don't have to. Any response to a `HEAD` request and any response with a `1xx` (Informational), `204` (No Content) or `304` (Not Modified) status code will *not* include a -message body as per the HTTP spec. +message body as per the HTTP specs. This means that your callback does not have to take special care of this and any response body will simply be ignored. +Similarly, any response with a `1xx` (Informational) or `204` (No Content) +status code will *not* include a `Content-Length` or `Transfer-Encoding` +header as these do not apply to these messages. +Note that a response to a `HEAD` request and any response with a `304` (Not +Modified) status code MAY include these headers even though +the message does not contain a response body, because these header would apply +to the message if the same request would have used an (unconditional) `GET`. + A `Date` header will be automatically added with the system date and time if none is given. You can add a custom `Date` header yourself like this: diff --git a/src/Server.php b/src/Server.php index f8eb4e8b..b9f421b7 100644 --- a/src/Server.php +++ b/src/Server.php @@ -336,8 +336,13 @@ public function handleResponse(ConnectionInterface $connection, RequestInterface $response = $response->withHeader('Connection', 'close'); } - // response to HEAD and 1xx, 204 and 304 responses MUST NOT include a body + // response code 1xx and 204 MUST NOT include Content-Length or Transfer-Encoding header $code = $response->getStatusCode(); + if (($code >= 100 && $code < 200) || $code === 204) { + $response = $response->withoutHeader('Content-Length')->withoutHeader('Transfer-Encoding'); + } + + // response to HEAD and 1xx, 204 and 304 responses MUST NOT include a body if ($request->getMethod() === 'HEAD' || ($code >= 100 && $code < 200) || $code === 204 || $code === 304) { $response = $response->withBody(Psr7Implementation\stream_for('')); } diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 280ad702..9bab5c89 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -358,7 +358,7 @@ function ($data) use (&$buffer) { $this->assertNotContains("bye", $buffer); } - public function testResponseContainsNoResponseBodyForNoContentStatus() + public function testResponseContainsNoResponseBodyAndNoContentLengthForNoContentStatus() { $server = new Server($this->socket, function (RequestInterface $request) { return new Response(204, array(), 'bye'); @@ -382,6 +382,7 @@ function ($data) use (&$buffer) { $this->connection->emit('data', array($data)); $this->assertContains("HTTP/1.1 204 No Content\r\n", $buffer); + $this->assertNotContains("\r\n\Content-Length: 3\r\n", $buffer); $this->assertNotContains("bye", $buffer); } @@ -409,6 +410,7 @@ function ($data) use (&$buffer) { $this->connection->emit('data', array($data)); $this->assertContains("HTTP/1.1 304 Not Modified\r\n", $buffer); + $this->assertContains("\r\nContent-Length: 3\r\n", $buffer); $this->assertNotContains("bye", $buffer); } From 463675b74a5131c986b235b06aa1e761ffe0ee41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Wed, 29 Mar 2017 20:14:02 +0200 Subject: [PATCH 119/456] Support CONNECT method --- README.md | 38 +++++++- composer.json | 3 +- examples/21-connect-proxy.php | 77 +++++++++++++++ src/RequestHeaderParser.php | 29 +++++- src/Server.php | 23 ++++- tests/ServerTest.php | 174 ++++++++++++++++++++++++++++++---- 6 files changed, 316 insertions(+), 28 deletions(-) create mode 100644 examples/21-connect-proxy.php diff --git a/README.md b/README.md index 7a5ca69b..1006559f 100644 --- a/README.md +++ b/README.md @@ -269,6 +269,27 @@ $http = new Server($socket, function (RequestInterface $request) { }); ``` +Note that the server supports *any* request method (including custom and non- +standard ones) and all request-target formats defined in the HTTP specs for each +respective method. +You can use `getMethod(): string` and `getRequestTarget(): string` to +check this is an accepted request and may want to reject other requests with +an appropriate error code, such as `400` (Bad Request) or `405` (Method Not +Allowed). + +> The `CONNECT` method is useful in a tunneling setup (HTTPS proxy) and not + something most HTTP servers would want to care about. + Note that if you want to handle this method, the client MAY send a different + request-target than the `Host` header field (such as removing default ports) + and the request-target MUST take precendence when forwarding. + The HTTP specs define an opaque "tunneling mode" for this method and make no + use of the message body. + For consistency reasons, this library uses the message body of the request and + response for tunneled application data. + This implies that that a `2xx` (Successful) response to a `CONNECT` request + can in fact use a streaming response body for the tunneled application data. + See also [example #21](examples) for more details. + ### Response The callback function passed to the constructor of the [Server](#server) @@ -393,14 +414,25 @@ message body as per the HTTP specs. This means that your callback does not have to take special care of this and any response body will simply be ignored. -Similarly, any response with a `1xx` (Informational) or `204` (No Content) -status code will *not* include a `Content-Length` or `Transfer-Encoding` -header as these do not apply to these messages. +Similarly, any `2xx` (Successful) response to a `CONNECT` request, any response +with a `1xx` (Informational) or `204` (No Content) status code will *not* +include a `Content-Length` or `Transfer-Encoding` header as these do not apply +to these messages. Note that a response to a `HEAD` request and any response with a `304` (Not Modified) status code MAY include these headers even though the message does not contain a response body, because these header would apply to the message if the same request would have used an (unconditional) `GET`. +> The `CONNECT` method is useful in a tunneling setup (HTTPS proxy) and not + something most HTTP servers would want to care about. + The HTTP specs define an opaque "tunneling mode" for this method and make no + use of the message body. + For consistency reasons, this library uses the message body of the request and + response for tunneled application data. + This implies that that a `2xx` (Successful) response to a `CONNECT` request + can in fact use a streaming response body for the tunneled application data. + See also [example #21](examples) for more details. + A `Date` header will be automatically added with the system date and time if none is given. You can add a custom `Date` header yourself like this: diff --git a/composer.json b/composer.json index ba6ac438..75d769e2 100644 --- a/composer.json +++ b/composer.json @@ -17,6 +17,7 @@ } }, "require-dev": { - "phpunit/phpunit": "^4.8.10||^5.0" + "phpunit/phpunit": "^4.8.10||^5.0", + "react/socket-client": "^0.6" } } diff --git a/examples/21-connect-proxy.php b/examples/21-connect-proxy.php new file mode 100644 index 00000000..6ab54f31 --- /dev/null +++ b/examples/21-connect-proxy.php @@ -0,0 +1,77 @@ +create('8.8.8.8', $loop); +$connector = new DnsConnector(new TcpConnector($loop), $resolver); + +$server = new \React\Http\Server($socket, function (RequestInterface $request) use ($connector) { + if ($request->getMethod() !== 'CONNECT') { + return new Response( + 405, + array('Content-Type' => 'text/plain', 'Allow' => 'CONNECT'), + 'This is a HTTP CONNECT (secure HTTPS) proxy' + ); + } + + // pause consuming request body + $body = $request->getBody(); + $body->pause(); + + $buffer = ''; + $body->on('data', function ($chunk) use (&$buffer) { + $buffer .= $chunk; + }); + + // try to connect to given target host + $promise = $connector->connect($request->getRequestTarget())->then( + function (ConnectionInterface $remote) use ($body, &$buffer) { + // connection established => forward data + $body->pipe($remote); + $body->resume(); + + if ($buffer !== '') { + $remote->write($buffer); + $buffer = ''; + } + + return new Response( + 200, + array(), + $remote + ); + }, + function ($e) { + return new Response( + 502, + array('Content-Type' => 'text/plain'), + 'Unable to connect: ' . $e->getMessage() + ); + } + ); + + // cancel pending connection if request closes prematurely + $body->on('close', function () use ($promise) { + $promise->cancel(); + }); + + return $promise; +}); + +//$server->on('error', 'printf'); + +echo 'Listening on http://' . $socket->getAddress() . PHP_EOL; + +$loop->run(); diff --git a/src/RequestHeaderParser.php b/src/RequestHeaderParser.php index b24325f2..db115248 100644 --- a/src/RequestHeaderParser.php +++ b/src/RequestHeaderParser.php @@ -55,18 +55,37 @@ private function parseRequest($data) { list($headers, $bodyBuffer) = explode("\r\n\r\n", $data, 2); - $asterisk = false; + $originalTarget = null; if (strpos($headers, 'OPTIONS * ') === 0) { - $asterisk = true; + $originalTarget = '*'; $headers = 'OPTIONS / ' . substr($headers, 10); + } elseif (strpos($headers, 'CONNECT ') === 0) { + $parts = explode(' ', $headers, 3); + $uri = parse_url('tcp://' . $parts[1]); + + // check this is a valid authority-form request-target (host:port) + if (isset($uri['scheme'], $uri['host'], $uri['port']) && count($uri) === 3) { + $originalTarget = $parts[1]; + $parts[1] = '/'; + $headers = implode(' ', $parts); + } } $request = g7\parse_request($headers); - if ($asterisk) { + // Do not assume this is HTTPS when this happens to be port 443 + // detecting HTTPS is left up to the socket layer (TLS detection) + if ($request->getUri()->getScheme() === 'https') { + $request = $request->withUri( + $request->getUri()->withScheme('http')->withPort(443) + ); + } + + if ($originalTarget !== null) { $request = $request->withUri( - $request->getUri()->withPath('') - )->withRequestTarget('*'); + $request->getUri()->withPath(''), + true + )->withRequestTarget($originalTarget); } return array($request, $bodyBuffer); diff --git a/src/Server.php b/src/Server.php index b9f421b7..e081fb62 100644 --- a/src/Server.php +++ b/src/Server.php @@ -199,7 +199,24 @@ public function handleRequest(ConnectionInterface $conn, RequestInterface $reque $contentLength = 0; $stream = new CloseProtectionStream($conn); - if ($request->hasHeader('Transfer-Encoding')) { + if ($request->getMethod() === 'CONNECT') { + // CONNECT method MUST use authority-form request target + $parts = parse_url('tcp://' . $request->getRequestTarget()); + if (!isset($parts['scheme'], $parts['host'], $parts['port']) || count($parts) !== 3) { + $this->emit('error', array(new \InvalidArgumentException('CONNECT method MUST use authority-form request target'))); + return $this->writeError($conn, 400); + } + + // CONNECT uses undelimited body until connection closes + $request = $request->withoutHeader('Transfer-Encoding'); + $request = $request->withoutHeader('Content-Length'); + $contentLength = null; + + // emit end event before the actual close event + $stream->on('close', function () use ($stream) { + $stream->emit('end'); + }); + } else 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'))); @@ -336,9 +353,9 @@ public function handleResponse(ConnectionInterface $connection, RequestInterface $response = $response->withHeader('Connection', 'close'); } - // response code 1xx and 204 MUST NOT include Content-Length or Transfer-Encoding header + // 2xx response to CONNECT and 1xx and 204 MUST NOT include Content-Length or Transfer-Encoding header $code = $response->getStatusCode(); - if (($code >= 100 && $code < 200) || $code === 204) { + if (($request->getMethod() === 'CONNECT' && $code >= 200 && $code < 300) || ($code >= 100 && $code < 200) || $code === 204) { $response = $response->withoutHeader('Content-Length')->withoutHeader('Transfer-Encoding'); } diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 4534edaa..b7ddac2f 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -66,21 +66,7 @@ public function testRequestEvent() { $i = 0; $requestAssertion = null; - - $buffer = ''; - - $this->connection - ->expects($this->any()) - ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) - ); - - $server = new Server($this->socket, function ($request) use (&$i, &$requestAssertion) { + $server = new Server($this->socket, function (RequestInterface $request) use (&$i, &$requestAssertion) { $i++; $requestAssertion = $request; return \React\Promise\resolve(new Response()); @@ -102,13 +88,80 @@ function ($data) use (&$buffer) { $this->assertSame('/', $requestAssertion->getRequestTarget()); $this->assertSame('/', $requestAssertion->getUri()->getPath()); $this->assertSame('http://example.com/', (string)$requestAssertion->getUri()); + $this->assertSame('example.com:80', $requestAssertion->getHeaderLine('Host')); $this->assertSame('127.0.0.1', $requestAssertion->remoteAddress); } + public function testRequestGetWithHostAndCustomPort() + { + $requestAssertion = null; + $server = new Server($this->socket, function (RequestInterface $request) use (&$requestAssertion) { + $requestAssertion = $request; + return new Response(); + }); + + $this->socket->emit('connection', array($this->connection)); + + $data = "GET / HTTP/1.1\r\nHost: example.com:8080\r\n\r\n"; + $this->connection->emit('data', array($data)); + + $this->assertInstanceOf('RingCentral\Psr7\Request', $requestAssertion); + $this->assertSame('GET', $requestAssertion->getMethod()); + $this->assertSame('/', $requestAssertion->getRequestTarget()); + $this->assertSame('/', $requestAssertion->getUri()->getPath()); + $this->assertSame('http://example.com:8080/', (string)$requestAssertion->getUri()); + $this->assertSame(8080, $requestAssertion->getUri()->getPort()); + $this->assertSame('example.com:8080', $requestAssertion->getHeaderLine('Host')); + } + + public function testRequestGetWithHostAndHttpsPort() + { + $requestAssertion = null; + $server = new Server($this->socket, function (RequestInterface $request) use (&$requestAssertion) { + $requestAssertion = $request; + return new Response(); + }); + + $this->socket->emit('connection', array($this->connection)); + + $data = "GET / HTTP/1.1\r\nHost: example.com:443\r\n\r\n"; + $this->connection->emit('data', array($data)); + + $this->assertInstanceOf('RingCentral\Psr7\Request', $requestAssertion); + $this->assertSame('GET', $requestAssertion->getMethod()); + $this->assertSame('/', $requestAssertion->getRequestTarget()); + $this->assertSame('/', $requestAssertion->getUri()->getPath()); + $this->assertSame('http://example.com:443/', (string)$requestAssertion->getUri()); + $this->assertSame(443, $requestAssertion->getUri()->getPort()); + $this->assertSame('example.com:443', $requestAssertion->getHeaderLine('Host')); + } + + public function testRequestGetWithHostAndDefaultPortWillBeIgnored() + { + $requestAssertion = null; + $server = new Server($this->socket, function (RequestInterface $request) use (&$requestAssertion) { + $requestAssertion = $request; + return new Response(); + }); + + $this->socket->emit('connection', array($this->connection)); + + $data = "GET / HTTP/1.1\r\nHost: example.com:80\r\n\r\n"; + $this->connection->emit('data', array($data)); + + $this->assertInstanceOf('RingCentral\Psr7\Request', $requestAssertion); + $this->assertSame('GET', $requestAssertion->getMethod()); + $this->assertSame('/', $requestAssertion->getRequestTarget()); + $this->assertSame('/', $requestAssertion->getUri()->getPath()); + $this->assertSame('http://example.com/', (string)$requestAssertion->getUri()); + $this->assertSame(null, $requestAssertion->getUri()->getPort()); + $this->assertSame('example.com:80', $requestAssertion->getHeaderLine('Host')); + } + public function testRequestOptionsAsterisk() { $requestAssertion = null; - $server = new Server($this->socket, function ($request) use (&$requestAssertion) { + $server = new Server($this->socket, function (RequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; return new Response(); }); @@ -123,6 +176,95 @@ public function testRequestOptionsAsterisk() $this->assertSame('*', $requestAssertion->getRequestTarget()); $this->assertSame('', $requestAssertion->getUri()->getPath()); $this->assertSame('http://example.com', (string)$requestAssertion->getUri()); + $this->assertSame('example.com', $requestAssertion->getHeaderLine('Host')); + } + + public function testRequestNonOptionsWithAsteriskRequestTargetWillReject() + { + $server = new Server($this->socket, $this->expectCallableNever()); + $server->on('error', $this->expectCallableOnce()); + + $this->socket->emit('connection', array($this->connection)); + + $data = "GET * HTTP/1.1\r\nHost: example.com\r\n\r\n"; + $this->connection->emit('data', array($data)); + } + + public function testRequestConnectAuthorityForm() + { + $requestAssertion = null; + $server = new Server($this->socket, function (RequestInterface $request) use (&$requestAssertion) { + $requestAssertion = $request; + return new Response(); + }); + + $this->socket->emit('connection', array($this->connection)); + + $data = "CONNECT example.com:443 HTTP/1.1\r\nHost: example.com:443\r\n\r\n"; + $this->connection->emit('data', array($data)); + + $this->assertInstanceOf('RingCentral\Psr7\Request', $requestAssertion); + $this->assertSame('CONNECT', $requestAssertion->getMethod()); + $this->assertSame('example.com:443', $requestAssertion->getRequestTarget()); + $this->assertSame('', $requestAssertion->getUri()->getPath()); + $this->assertSame('http://example.com:443', (string)$requestAssertion->getUri()); + $this->assertSame(443, $requestAssertion->getUri()->getPort()); + $this->assertSame('example.com:443', $requestAssertion->getHeaderLine('host')); + } + + public function testRequestConnectAuthorityFormWithDefaultPortWillBeIgnored() + { + $requestAssertion = null; + $server = new Server($this->socket, function (RequestInterface $request) use (&$requestAssertion) { + $requestAssertion = $request; + return new Response(); + }); + + $this->socket->emit('connection', array($this->connection)); + + $data = "CONNECT example.com:80 HTTP/1.1\r\nHost: example.com:80\r\n\r\n"; + $this->connection->emit('data', array($data)); + + $this->assertInstanceOf('RingCentral\Psr7\Request', $requestAssertion); + $this->assertSame('CONNECT', $requestAssertion->getMethod()); + $this->assertSame('example.com:80', $requestAssertion->getRequestTarget()); + $this->assertSame('', $requestAssertion->getUri()->getPath()); + $this->assertSame('http://example.com', (string)$requestAssertion->getUri()); + $this->assertSame(null, $requestAssertion->getUri()->getPort()); + $this->assertSame('example.com:80', $requestAssertion->getHeaderLine('Host')); + } + + public function testRequestConnectAuthorityFormNonMatchingHostWillBePassedAsIs() + { + $requestAssertion = null; + $server = new Server($this->socket, function (RequestInterface $request) use (&$requestAssertion) { + $requestAssertion = $request; + return new Response(); + }); + + $this->socket->emit('connection', array($this->connection)); + + $data = "CONNECT example.com:80 HTTP/1.1\r\nHost: example.com\r\n\r\n"; + $this->connection->emit('data', array($data)); + + $this->assertInstanceOf('RingCentral\Psr7\Request', $requestAssertion); + $this->assertSame('CONNECT', $requestAssertion->getMethod()); + $this->assertSame('example.com:80', $requestAssertion->getRequestTarget()); + $this->assertSame('', $requestAssertion->getUri()->getPath()); + $this->assertSame('http://example.com', (string)$requestAssertion->getUri()); + $this->assertSame(null, $requestAssertion->getUri()->getPort()); + $this->assertSame('example.com', $requestAssertion->getHeaderLine('host')); + } + + public function testRequestNonConnectWithAuthorityRequestTargetWillReject() + { + $server = new Server($this->socket, $this->expectCallableNever()); + $server->on('error', $this->expectCallableOnce()); + + $this->socket->emit('connection', array($this->connection)); + + $data = "GET example.com:80 HTTP/1.1\r\nHost: example.com\r\n\r\n"; + $this->connection->emit('data', array($data)); } public function testRequestPauseWillbeForwardedToConnection() From a1af759c17d7cd5ccbf10f563e86429f3e67d8af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 3 Apr 2017 17:35:38 +0200 Subject: [PATCH 120/456] Update SocketClient to v0.7 to simplify CONNECT example --- composer.json | 2 +- examples/21-connect-proxy.php | 8 ++------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/composer.json b/composer.json index 75d769e2..9470cd47 100644 --- a/composer.json +++ b/composer.json @@ -18,6 +18,6 @@ }, "require-dev": { "phpunit/phpunit": "^4.8.10||^5.0", - "react/socket-client": "^0.6" + "react/socket-client": "^0.7" } } diff --git a/examples/21-connect-proxy.php b/examples/21-connect-proxy.php index 6ab54f31..8fcba5b3 100644 --- a/examples/21-connect-proxy.php +++ b/examples/21-connect-proxy.php @@ -4,18 +4,14 @@ use React\Socket\Server; use React\Http\Response; use Psr\Http\Message\RequestInterface; -use React\SocketClient\TcpConnector; +use React\SocketClient\Connector; use React\SocketClient\ConnectionInterface; -use React\SocketClient\DnsConnector; require __DIR__ . '/../vendor/autoload.php'; $loop = Factory::create(); $socket = new Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); - -$resolver = new \React\Dns\Resolver\Factory(); -$resolver = $resolver->create('8.8.8.8', $loop); -$connector = new DnsConnector(new TcpConnector($loop), $resolver); +$connector = new Connector($loop); $server = new \React\Http\Server($socket, function (RequestInterface $request) use ($connector) { if ($request->getMethod() !== 'CONNECT') { From 504251bcd6b69791f7a908146dec87aed05d64b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Wed, 5 Apr 2017 00:45:21 +0200 Subject: [PATCH 121/456] Replace deprecated SocketClient with new Socket component --- composer.json | 2 +- examples/21-connect-proxy.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index 9470cd47..4c74c704 100644 --- a/composer.json +++ b/composer.json @@ -18,6 +18,6 @@ }, "require-dev": { "phpunit/phpunit": "^4.8.10||^5.0", - "react/socket-client": "^0.7" + "react/socket": "^0.7" } } diff --git a/examples/21-connect-proxy.php b/examples/21-connect-proxy.php index 8fcba5b3..24d383fc 100644 --- a/examples/21-connect-proxy.php +++ b/examples/21-connect-proxy.php @@ -4,8 +4,8 @@ use React\Socket\Server; use React\Http\Response; use Psr\Http\Message\RequestInterface; -use React\SocketClient\Connector; -use React\SocketClient\ConnectionInterface; +use React\Socket\Connector; +use React\Socket\ConnectionInterface; require __DIR__ . '/../vendor/autoload.php'; From a655b3de4dd4cdc5059cfb80f3624b89674da01c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 6 Apr 2017 22:57:14 +0200 Subject: [PATCH 122/456] Fix tests to support new Socket v0.6 and up --- tests/SocketServerStub.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/SocketServerStub.php b/tests/SocketServerStub.php index bdbb7ac2..a6610243 100644 --- a/tests/SocketServerStub.php +++ b/tests/SocketServerStub.php @@ -16,4 +16,14 @@ public function close() { // NO-OP } + + public function pause() + { + // NO-OP + } + + public function resume() + { + // NO-OP + } } From da66adee596cd7893a6fdc4285c50e28689302f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Wed, 5 Apr 2017 00:48:44 +0200 Subject: [PATCH 123/456] Add benchmarking example --- examples/99-benchmark-download.php | 94 ++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 examples/99-benchmark-download.php diff --git a/examples/99-benchmark-download.php b/examples/99-benchmark-download.php new file mode 100644 index 00000000..0a0e25cd --- /dev/null +++ b/examples/99-benchmark-download.php @@ -0,0 +1,94 @@ + /dev/null +// $ wget http://localhost:8080/10g.bin -O /dev/null +// $ ab -n10 -c10 http://localhost:8080/1g.bin +// $ docker run -it --rm --net=host jordi/ab ab -n10 -c10 http://localhost:8080/1g.bin + +use React\EventLoop\Factory; +use React\Socket\Server; +use React\Http\Response; +use Psr\Http\Message\RequestInterface; +use React\Stream\ReadableStream; + +require __DIR__ . '/../vendor/autoload.php'; + +$loop = Factory::create(); +$socket = new Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); + +/** A readable stream that can emit a lot of data */ +class ChunkRepeater extends ReadableStream +{ + private $chunk; + private $count; + private $position = 0; + private $paused = true; + + public function __construct($chunk, $count) + { + $this->chunk = $chunk; + $this->count = $count; + } + + public function pause() + { + $this->paused = true; + } + + public function resume() + { + if (!$this->paused) { + return; + } + + // keep emitting until stream is paused + $this->paused = false; + while ($this->position < $this->count && !$this->paused) { + ++$this->position; + $this->emit('data', array($this->chunk)); + } + + // end once the last chunk has been written + if ($this->position >= $this->count) { + $this->emit('end'); + $this->close(); + } + } + + public function getSize() + { + return strlen($this->chunk) * $this->count; + } +} + +$server = new \React\Http\Server($socket, function (RequestInterface $request) use ($loop) { + switch ($request->getUri()->getPath()) { + case '/': + return new Response( + 200, + array('Content-Type' => 'text/html'), + '1g.bin
10g.bin' + ); + case '/1g.bin': + $stream = new ChunkRepeater(str_repeat('.', 1000000), 1000); + break; + case '/10g.bin': + $stream = new ChunkRepeater(str_repeat('.', 1000000), 10000); + break; + default: + return new Response(404); + } + + $loop->addTimer(0, array($stream, 'resume')); + + return new Response( + 200, + array('Content-Type' => 'application/octet-data', 'Content-Length' => $stream->getSize()), + $stream + ); +}); + +echo 'Listening on http://' . $socket->getAddress() . PHP_EOL; + +$loop->run(); From 2ec1446b2882ee046d1cefca427a7ad046a6d633 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Mrozi=C5=84ski?= Date: Fri, 7 Apr 2017 18:43:30 +0200 Subject: [PATCH 124/456] Cast getSize() to string for Content-Length header --- src/Server.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Server.php b/src/Server.php index e081fb62..87ae5054 100644 --- a/src/Server.php +++ b/src/Server.php @@ -341,7 +341,7 @@ public function handleResponse(ConnectionInterface $connection, RequestInterface } if (!$response->getBody() instanceof HttpBodyStream) { - $response = $response->withHeader('Content-Length', $response->getBody()->getSize()); + $response = $response->withHeader('Content-Length', (string)$response->getBody()->getSize()); } elseif (!$response->hasHeader('Content-Length') && $request->getProtocolVersion() === '1.1') { // assign chunked transfer-encoding if no 'content-length' is given for HTTP/1.1 responses $response = $response->withHeader('Transfer-Encoding', 'chunked'); From a0208d80936d01a88418840161372cfc96c218e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 3 Apr 2017 18:34:44 +0200 Subject: [PATCH 125/456] Use https-scheme for request URIs if secure TLS is used --- composer.json | 3 +- src/RequestHeaderParser.php | 3 +- src/Server.php | 16 +++ tests/FunctionalServerTest.php | 204 +++++++++++++++++++++++++++++++++ 4 files changed, 224 insertions(+), 2 deletions(-) create mode 100644 tests/FunctionalServerTest.php diff --git a/composer.json b/composer.json index 4c74c704..fccbc1bd 100644 --- a/composer.json +++ b/composer.json @@ -18,6 +18,7 @@ }, "require-dev": { "phpunit/phpunit": "^4.8.10||^5.0", - "react/socket": "^0.7" + "react/socket": "^0.7", + "clue/block-react": "^1.1" } } diff --git a/src/RequestHeaderParser.php b/src/RequestHeaderParser.php index db115248..792a0604 100644 --- a/src/RequestHeaderParser.php +++ b/src/RequestHeaderParser.php @@ -77,7 +77,8 @@ private function parseRequest($data) // detecting HTTPS is left up to the socket layer (TLS detection) if ($request->getUri()->getScheme() === 'https') { $request = $request->withUri( - $request->getUri()->withScheme('http')->withPort(443) + $request->getUri()->withScheme('http')->withPort(443), + true ); } diff --git a/src/Server.php b/src/Server.php index 87ae5054..f8307bab 100644 --- a/src/Server.php +++ b/src/Server.php @@ -254,6 +254,22 @@ public function handleRequest(ConnectionInterface $conn, RequestInterface $reque '[]' ); + // Update request URI to "https" scheme if the connection is encrypted + $meta = isset($conn->stream) ? stream_get_meta_data($conn->stream) : array(); + if (isset($meta['crypto']) && $meta['crypto']) { + // The request URI may omit default ports here, so try to parse port + // from Host header field (if possible) + $port = $request->getUri()->getPort(); + if ($port === null) { + $port = parse_url('tcp://' . $request->getHeaderLine('Host'), PHP_URL_PORT); + } + + $request = $request->withUri( + $request->getUri()->withScheme('https')->withPort($port), + true + ); + } + $callback = $this->callback; $promise = new Promise(function ($resolve, $reject) use ($callback, $request) { $resolve($callback($request)); diff --git a/tests/FunctionalServerTest.php b/tests/FunctionalServerTest.php new file mode 100644 index 00000000..4c7ed6ff --- /dev/null +++ b/tests/FunctionalServerTest.php @@ -0,0 +1,204 @@ +getUri()); + }); + + $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { + $conn->write("GET / HTTP/1.0\r\nHost: " . $conn->getRemoteAddress() . "\r\n\r\n"); + + return BufferedSink::createPromise($conn); + }); + + $response = Block\await($result, $loop, 1.0); + + $this->assertContains("HTTP/1.0 200 OK", $response); + $this->assertContains('http://' . $socket->getAddress() . '/', $response); + + $socket->close(); + } + + public function testSecureHttpsOnRandomPort() + { + if (!function_exists('stream_socket_enable_crypto')) { + $this->markTestSkipped('Not supported on your platform (outdated HHVM?)'); + } + + $loop = Factory::create(); + $socket = new Socket(0, $loop); + $socket = new SecureServer($socket, $loop, array( + 'local_cert' => __DIR__ . '/../examples/localhost.pem' + )); + $connector = new Connector($loop, array( + 'tls' => array('verify_peer' => false) + )); + + $server = new Server($socket, function (RequestInterface $request) { + return new Response(200, array(), (string)$request->getUri()); + }); + + $result = $connector->connect('tls://' . $socket->getAddress())->then(function (ConnectionInterface $conn) { + $conn->write("GET / HTTP/1.0\r\nHost: " . $conn->getRemoteAddress() . "\r\n\r\n"); + + return BufferedSink::createPromise($conn); + }); + + $response = Block\await($result, $loop, 1.0); + + $this->assertContains("HTTP/1.0 200 OK", $response); + $this->assertContains('https://' . $socket->getAddress() . '/', $response); + + $socket->close(); + } + + public function testPlainHttpOnStandardPortReturnsUriWithNoPort() + { + $loop = Factory::create(); + try { + $socket = new Socket(80, $loop); + } catch (\RuntimeException $e) { + $this->markTestSkipped('Listening on port 80 failed (root and unused?)'); + } + $connector = new Connector($loop); + + $server = new Server($socket, function (RequestInterface $request) { + return new Response(200, array(), (string)$request->getUri()); + }); + + $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { + $conn->write("GET / HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n"); + + return BufferedSink::createPromise($conn); + }); + + $response = Block\await($result, $loop, 1.0); + + $this->assertContains("HTTP/1.0 200 OK", $response); + $this->assertContains('http://127.0.0.1/', $response); + + $socket->close(); + } + + public function testSecureHttpsOnStandardPortReturnsUriWithNoPort() + { + if (!function_exists('stream_socket_enable_crypto')) { + $this->markTestSkipped('Not supported on your platform (outdated HHVM?)'); + } + + $loop = Factory::create(); + try { + $socket = new Socket(443, $loop); + } catch (\RuntimeException $e) { + $this->markTestSkipped('Listening on port 443 failed (root and unused?)'); + } + $socket = new SecureServer($socket, $loop, array( + 'local_cert' => __DIR__ . '/../examples/localhost.pem' + )); + $connector = new Connector($loop, array( + 'tls' => array('verify_peer' => false) + )); + + $server = new Server($socket, function (RequestInterface $request) { + return new Response(200, array(), (string)$request->getUri()); + }); + + $result = $connector->connect('tls://' . $socket->getAddress())->then(function (ConnectionInterface $conn) { + $conn->write("GET / HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n"); + + return BufferedSink::createPromise($conn); + }); + + $response = Block\await($result, $loop, 1.0); + + $this->assertContains("HTTP/1.0 200 OK", $response); + $this->assertContains('https://127.0.0.1/', $response); + + $socket->close(); + } + + public function testPlainHttpOnHttpsStandardPortReturnsUriWithPort() + { + $loop = Factory::create(); + try { + $socket = new Socket(443, $loop); + } catch (\RuntimeException $e) { + $this->markTestSkipped('Listening on port 443 failed (root and unused?)'); + } + $connector = new Connector($loop); + + $server = new Server($socket, function (RequestInterface $request) { + return new Response(200, array(), (string)$request->getUri()); + }); + + $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { + $conn->write("GET / HTTP/1.0\r\nHost: " . $conn->getRemoteAddress() . "\r\n\r\n"); + + return BufferedSink::createPromise($conn); + }); + + $response = Block\await($result, $loop, 1.0); + + $this->assertContains("HTTP/1.0 200 OK", $response); + $this->assertContains('http://127.0.0.1:443/', $response); + + $socket->close(); + } + + public function testSecureHttpsOnHttpStandardPortReturnsUriWithPort() + { + if (!function_exists('stream_socket_enable_crypto')) { + $this->markTestSkipped('Not supported on your platform (outdated HHVM?)'); + } + + $loop = Factory::create(); + try { + $socket = new Socket(80, $loop); + } catch (\RuntimeException $e) { + $this->markTestSkipped('Listening on port 80 failed (root and unused?)'); + } + $socket = new SecureServer($socket, $loop, array( + 'local_cert' => __DIR__ . '/../examples/localhost.pem' + )); + $connector = new Connector($loop, array( + 'tls' => array('verify_peer' => false) + )); + + $server = new Server($socket, function (RequestInterface $request) { + return new Response(200, array(), (string)$request->getUri() . 'x' . $request->getHeaderLine('Host')); + }); + + $result = $connector->connect('tls://' . $socket->getAddress())->then(function (ConnectionInterface $conn) { + $conn->write("GET / HTTP/1.0\r\nHost: " . $conn->getRemoteAddress() . "\r\n\r\n"); + + return BufferedSink::createPromise($conn); + }); + + $response = Block\await($result, $loop, 1.0); + + $this->assertContains("HTTP/1.0 200 OK", $response); + $this->assertContains('https://127.0.0.1:80/', $response); + + $socket->close(); + } +} From be55f6c2cc8869b91aa6673766f81e5476129c4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 14 Apr 2017 15:23:28 +0200 Subject: [PATCH 126/456] Work around PHP < 7 not giving access to stream crypto parameters --- src/Server.php | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/src/Server.php b/src/Server.php index f8307bab..4cb6d6ea 100644 --- a/src/Server.php +++ b/src/Server.php @@ -255,8 +255,7 @@ public function handleRequest(ConnectionInterface $conn, RequestInterface $reque ); // Update request URI to "https" scheme if the connection is encrypted - $meta = isset($conn->stream) ? stream_get_meta_data($conn->stream) : array(); - if (isset($meta['crypto']) && $meta['crypto']) { + if ($this->isConnectionEncrypted($conn)) { // The request URI may omit default ports here, so try to parse port // from Host header field (if possible) $port = $request->getUri()->getPort(); @@ -400,4 +399,27 @@ private function handleResponseBody(ResponseInterface $response, ConnectionInter $connection->write(Psr7Implementation\str($response)); $stream->pipe($connection); } + + /** + * @param ConnectionInterface $conn + * @return bool + * @codeCoverageIgnore + */ + private function isConnectionEncrypted(ConnectionInterface $conn) + { + // Legacy PHP < 7 does not offer any direct access to check crypto parameters + // We work around by accessing the context options and assume that only + // secure connections *SHOULD* set the "ssl" context options by default. + if (PHP_VERSION_ID < 70000) { + $context = isset($conn->stream) ? stream_context_get_options($conn->stream) : array(); + + return (isset($context['ssl']) && $context['ssl']); + } + + // Modern PHP 7+ offers more reliable access to check crypto parameters + // by checking stream crypto meta data that is only then made available. + $meta = isset($conn->stream) ? stream_get_meta_data($conn->stream) : array(); + + return (isset($meta['crypto']) && $meta['crypto']); + } } From 6d8474ccabc0c24890a3f22e21ffa879e2480efb Mon Sep 17 00:00:00 2001 From: Niels Theen Date: Thu, 20 Apr 2017 01:32:49 +0200 Subject: [PATCH 127/456] Replace Request with ServerRequest --- examples/01-hello-world.php | 5 +- examples/02-count-visitors.php | 4 +- examples/03-stream-response.php | 4 +- examples/04-stream-request.php | 4 +- examples/05-error-handling.php | 4 +- examples/11-hello-world-https.php | 4 +- examples/21-connect-proxy.php | 4 +- examples/99-benchmark-download.php | 4 +- src/RequestHeaderParser.php | 7 ++ src/Server.php | 9 ++- src/ServerRequest.php | 98 +++++++++++++++++++++++ tests/ServerRequestTest.php | 87 ++++++++++++++++++++ tests/ServerTest.php | 122 ++++++++++++++--------------- 13 files changed, 274 insertions(+), 82 deletions(-) create mode 100644 src/ServerRequest.php create mode 100644 tests/ServerRequestTest.php diff --git a/examples/01-hello-world.php b/examples/01-hello-world.php index ed84af11..cf047944 100644 --- a/examples/01-hello-world.php +++ b/examples/01-hello-world.php @@ -3,15 +3,14 @@ use React\EventLoop\Factory; use React\Socket\Server; use React\Http\Response; -use Psr\Http\Message\RequestInterface; -use React\Promise\Promise; +use Psr\Http\Message\ServerRequestInterface; require __DIR__ . '/../vendor/autoload.php'; $loop = Factory::create(); $socket = new Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); -$server = new \React\Http\Server($socket, function (RequestInterface $request) { +$server = new \React\Http\Server($socket, function (ServerRequestInterface $request) { return new Response( 200, array( diff --git a/examples/02-count-visitors.php b/examples/02-count-visitors.php index 2c384a3b..9f69f797 100644 --- a/examples/02-count-visitors.php +++ b/examples/02-count-visitors.php @@ -3,7 +3,7 @@ use React\EventLoop\Factory; use React\Socket\Server; use React\Http\Response; -use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ServerRequestInterface; require __DIR__ . '/../vendor/autoload.php'; @@ -11,7 +11,7 @@ $socket = new Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); $counter = 0; -$server = new \React\Http\Server($socket, function (RequestInterface $request) use (&$counter) { +$server = new \React\Http\Server($socket, function (ServerRequestInterface $request) use (&$counter) { return new Response( 200, array('Content-Type' => 'text/plain'), diff --git a/examples/03-stream-response.php b/examples/03-stream-response.php index 5fd990e8..8edf7d40 100644 --- a/examples/03-stream-response.php +++ b/examples/03-stream-response.php @@ -3,7 +3,7 @@ use React\EventLoop\Factory; use React\Socket\Server; use React\Http\Response; -use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ServerRequestInterface; use React\Stream\ReadableStream; require __DIR__ . '/../vendor/autoload.php'; @@ -11,7 +11,7 @@ $loop = Factory::create(); $socket = new Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); -$server = new \React\Http\Server($socket, function (RequestInterface $request) use ($loop) { +$server = new \React\Http\Server($socket, function (ServerRequestInterface $request) use ($loop) { $stream = new ReadableStream(); $timer = $loop->addPeriodicTimer(0.5, function () use ($stream) { diff --git a/examples/04-stream-request.php b/examples/04-stream-request.php index 481162ef..fabb5bb7 100644 --- a/examples/04-stream-request.php +++ b/examples/04-stream-request.php @@ -3,7 +3,7 @@ use React\EventLoop\Factory; use React\Socket\Server; use React\Http\Response; -use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ServerRequestInterface; use React\Promise\Promise; require __DIR__ . '/../vendor/autoload.php'; @@ -11,7 +11,7 @@ $loop = Factory::create(); $socket = new Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); -$server = new \React\Http\Server($socket, function (RequestInterface $request) { +$server = new \React\Http\Server($socket, function (ServerRequestInterface $request) { return new Promise(function ($resolve, $reject) use ($request) { $contentLength = 0; $request->getBody()->on('data', function ($data) use (&$contentLength) { diff --git a/examples/05-error-handling.php b/examples/05-error-handling.php index 29f54b92..00ed0cfa 100644 --- a/examples/05-error-handling.php +++ b/examples/05-error-handling.php @@ -3,7 +3,7 @@ use React\EventLoop\Factory; use React\Socket\Server; use React\Http\Response; -use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ServerRequestInterface; use React\Promise\Promise; require __DIR__ . '/../vendor/autoload.php'; @@ -12,7 +12,7 @@ $socket = new Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); $count = 0; -$server = new \React\Http\Server($socket, function (RequestInterface $request) use (&$count) { +$server = new \React\Http\Server($socket, function (ServerRequestInterface $request) use (&$count) { return new Promise(function ($resolve, $reject) use (&$count) { $count++; diff --git a/examples/11-hello-world-https.php b/examples/11-hello-world-https.php index 191e7d62..de958007 100644 --- a/examples/11-hello-world-https.php +++ b/examples/11-hello-world-https.php @@ -4,7 +4,7 @@ use React\Socket\Server; use React\Http\Response; use React\Socket\SecureServer; -use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ServerRequestInterface; require __DIR__ . '/../vendor/autoload.php'; @@ -14,7 +14,7 @@ 'local_cert' => isset($argv[2]) ? $argv[2] : __DIR__ . '/localhost.pem' )); -$server = new \React\Http\Server($socket, function (RequestInterface $request) { +$server = new \React\Http\Server($socket, function (ServerRequestInterface $request) { return new Response( 200, array('Content-Type' => 'text/plain'), diff --git a/examples/21-connect-proxy.php b/examples/21-connect-proxy.php index 24d383fc..03939d29 100644 --- a/examples/21-connect-proxy.php +++ b/examples/21-connect-proxy.php @@ -3,7 +3,7 @@ use React\EventLoop\Factory; use React\Socket\Server; use React\Http\Response; -use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ServerRequestInterface; use React\Socket\Connector; use React\Socket\ConnectionInterface; @@ -13,7 +13,7 @@ $socket = new Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); $connector = new Connector($loop); -$server = new \React\Http\Server($socket, function (RequestInterface $request) use ($connector) { +$server = new \React\Http\Server($socket, function (ServerRequestInterface $request) use ($connector) { if ($request->getMethod() !== 'CONNECT') { return new Response( 405, diff --git a/examples/99-benchmark-download.php b/examples/99-benchmark-download.php index 0a0e25cd..47460b5f 100644 --- a/examples/99-benchmark-download.php +++ b/examples/99-benchmark-download.php @@ -9,7 +9,7 @@ use React\EventLoop\Factory; use React\Socket\Server; use React\Http\Response; -use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ServerRequestInterface; use React\Stream\ReadableStream; require __DIR__ . '/../vendor/autoload.php'; @@ -62,7 +62,7 @@ public function getSize() } } -$server = new \React\Http\Server($socket, function (RequestInterface $request) use ($loop) { +$server = new \React\Http\Server($socket, function (ServerRequestInterface $request) use ($loop) { switch ($request->getUri()->getPath()) { case '/': return new Response( diff --git a/src/RequestHeaderParser.php b/src/RequestHeaderParser.php index 792a0604..510de700 100644 --- a/src/RequestHeaderParser.php +++ b/src/RequestHeaderParser.php @@ -72,6 +72,13 @@ private function parseRequest($data) } $request = g7\parse_request($headers); + $request = new ServerRequest( + $request->getMethod(), + $request->getUri(), + $request->getHeaders(), + $request->getBody(), + $request->getProtocolVersion() + ); // Do not assume this is HTTPS when this happens to be port 443 // detecting HTTPS is left up to the socket layer (TLS detection) diff --git a/src/Server.php b/src/Server.php index 4cb6d6ea..f901bdec 100644 --- a/src/Server.php +++ b/src/Server.php @@ -9,6 +9,7 @@ use Psr\Http\Message\ResponseInterface; use React\Promise\Promise; use RingCentral\Psr7 as Psr7Implementation; +use Psr\Http\Message\ServerRequestInterface; /** * The `Server` class is responsible for handling incoming connections and then @@ -170,7 +171,7 @@ public function handleConnection(ConnectionInterface $conn) } /** @internal */ - public function handleRequest(ConnectionInterface $conn, RequestInterface $request) + public function handleRequest(ConnectionInterface $conn, ServerRequestInterface $request) { // only support HTTP/1.1 and HTTP/1.0 requests if ($request->getProtocolVersion() !== '1.1' && $request->getProtocolVersion() !== '1.0') { @@ -306,7 +307,7 @@ function ($error) use ($that, $conn, $request) { } /** @internal */ - public function writeError(ConnectionInterface $conn, $code, RequestInterface $request = null) + public function writeError(ConnectionInterface $conn, $code, ServerRequestInterface $request = null) { $message = 'Error ' . $code; if (isset(ResponseCodes::$statusTexts[$code])) { @@ -322,7 +323,7 @@ public function writeError(ConnectionInterface $conn, $code, RequestInterface $r ); if ($request === null) { - $request = new Psr7Implementation\Request('GET', '/', array(), null, '1.1'); + $request = new ServerRequest('GET', '/', array(), null, '1.1'); } $this->handleResponse($conn, $request, $response); @@ -330,7 +331,7 @@ public function writeError(ConnectionInterface $conn, $code, RequestInterface $r /** @internal */ - public function handleResponse(ConnectionInterface $connection, RequestInterface $request, ResponseInterface $response) + public function handleResponse(ConnectionInterface $connection, ServerRequestInterface $request, ResponseInterface $response) { $response = $response->withProtocolVersion($request->getProtocolVersion()); diff --git a/src/ServerRequest.php b/src/ServerRequest.php new file mode 100644 index 00000000..44826f6c --- /dev/null +++ b/src/ServerRequest.php @@ -0,0 +1,98 @@ +serverParams; + } + + public function getCookieParams() + { + return $this->cookies; + } + + public function withCookieParams(array $cookies) + { + $new = clone $this; + $new->cookies = $cookies; + return $new; + } + + public function getQueryParams() + { + return $this->queryParams; + } + + public function withQueryParams(array $query) + { + $new = clone $this; + $new->queryParams = $query; + return $new; + } + + public function getUploadedFiles() + { + return $this->fileParams; + } + + public function withUploadedFiles(array $uploadedFiles) + { + $new = clone $this; + $new->fileParams = $uploadedFiles; + return $new; + } + + public function getParsedBody() + { + return $this->parsedBody; + } + + public function withParsedBody($data) + { + $new = clone $this; + $new->parsedBody = $data; + return $new; + } + + public function getAttributes() + { + return $this->attributes; + } + + public function getAttribute($name, $default = null) + { + if (!array_key_exists($name, $this->attributes)) { + return $default; + } + return $this->attributes[$name]; + } + + public function withAttribute($name, $value) + { + $new = clone $this; + $new->attributes[$name] = $value; + return $new; + } + + public function withoutAttribute($name) + { + $new = clone $this; + unset($new->attributes[$name]); + return $new; + } +} diff --git a/tests/ServerRequestTest.php b/tests/ServerRequestTest.php new file mode 100644 index 00000000..fc873439 --- /dev/null +++ b/tests/ServerRequestTest.php @@ -0,0 +1,87 @@ +request = new ServerRequest('GET', 'http://localhost'); + } + + public function testGetNoAttributes() + { + $this->assertEquals(array(), $this->request->getAttributes()); + } + + public function testWithAttribute() + { + $request = $this->request->withAttribute('hello', 'world'); + + $this->assertNotSame($request, $this->request); + $this->assertEquals(array('hello' => 'world'), $request->getAttributes()); + } + + public function testGetAttribute() + { + $request = $this->request->withAttribute('hello', 'world'); + + $this->assertNotSame($request, $this->request); + $this->assertEquals('world', $request->getAttribute('hello')); + } + + public function testGetDefaultAttribute() + { + $request = $this->request->withAttribute('hello', 'world'); + + $this->assertNotSame($request, $this->request); + $this->assertEquals(null, $request->getAttribute('hi', null)); + } + + public function testWithoutAttribute() + { + $request = $this->request->withAttribute('hello', 'world'); + $request = $request->withAttribute('test', 'nice'); + + $request = $request->withoutAttribute('hello'); + + $this->assertNotSame($request, $this->request); + $this->assertEquals(array('test' => 'nice'), $request->getAttributes()); + } + + public function testWithCookieParams() + { + $request = $this->request->withCookieParams(array('test' => 'world')); + + $this->assertNotSame($request, $this->request); + $this->assertEquals(array('test' => 'world'), $request->getCookieParams()); + } + + public function testWithQueryParams() + { + $request = $this->request->withQueryParams(array('test' => 'world')); + + $this->assertNotSame($request, $this->request); + $this->assertEquals(array('test' => 'world'), $request->getQueryParams()); + } + + public function testWithUploadedFiles() + { + $request = $this->request->withUploadedFiles(array('test' => 'world')); + + $this->assertNotSame($request, $this->request); + $this->assertEquals(array('test' => 'world'), $request->getUploadedFiles()); + } + + public function testWithParsedBody() + { + $request = $this->request->withParsedBody(array('test' => 'world')); + + $this->assertNotSame($request, $this->request); + $this->assertEquals(array('test' => 'world'), $request->getParsedBody()); + } +} diff --git a/tests/ServerTest.php b/tests/ServerTest.php index b7ddac2f..d831aa86 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -3,7 +3,7 @@ namespace React\Tests\Http; use React\Http\Server; -use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ServerRequestInterface; use React\Http\Response; use React\Stream\ReadableStream; use React\Promise\Promise; @@ -52,7 +52,7 @@ public function testRequestEventWillNotBeEmittedForIncompleteHeaders() public function testRequestEventIsEmitted() { - $server = new Server($this->socket, function (RequestInterface $request) { + $server = new Server($this->socket, function (ServerRequestInterface $request) { return \React\Promise\resolve(new Response()); }); @@ -66,7 +66,7 @@ public function testRequestEvent() { $i = 0; $requestAssertion = null; - $server = new Server($this->socket, function (RequestInterface $request) use (&$i, &$requestAssertion) { + $server = new Server($this->socket, function (ServerRequestInterface $request) use (&$i, &$requestAssertion) { $i++; $requestAssertion = $request; return \React\Promise\resolve(new Response()); @@ -95,7 +95,7 @@ public function testRequestEvent() public function testRequestGetWithHostAndCustomPort() { $requestAssertion = null; - $server = new Server($this->socket, function (RequestInterface $request) use (&$requestAssertion) { + $server = new Server($this->socket, function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; return new Response(); }); @@ -117,7 +117,7 @@ public function testRequestGetWithHostAndCustomPort() public function testRequestGetWithHostAndHttpsPort() { $requestAssertion = null; - $server = new Server($this->socket, function (RequestInterface $request) use (&$requestAssertion) { + $server = new Server($this->socket, function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; return new Response(); }); @@ -139,7 +139,7 @@ public function testRequestGetWithHostAndHttpsPort() public function testRequestGetWithHostAndDefaultPortWillBeIgnored() { $requestAssertion = null; - $server = new Server($this->socket, function (RequestInterface $request) use (&$requestAssertion) { + $server = new Server($this->socket, function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; return new Response(); }); @@ -161,7 +161,7 @@ public function testRequestGetWithHostAndDefaultPortWillBeIgnored() public function testRequestOptionsAsterisk() { $requestAssertion = null; - $server = new Server($this->socket, function (RequestInterface $request) use (&$requestAssertion) { + $server = new Server($this->socket, function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; return new Response(); }); @@ -193,7 +193,7 @@ public function testRequestNonOptionsWithAsteriskRequestTargetWillReject() public function testRequestConnectAuthorityForm() { $requestAssertion = null; - $server = new Server($this->socket, function (RequestInterface $request) use (&$requestAssertion) { + $server = new Server($this->socket, function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; return new Response(); }); @@ -215,7 +215,7 @@ public function testRequestConnectAuthorityForm() public function testRequestConnectAuthorityFormWithDefaultPortWillBeIgnored() { $requestAssertion = null; - $server = new Server($this->socket, function (RequestInterface $request) use (&$requestAssertion) { + $server = new Server($this->socket, function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; return new Response(); }); @@ -237,7 +237,7 @@ public function testRequestConnectAuthorityFormWithDefaultPortWillBeIgnored() public function testRequestConnectAuthorityFormNonMatchingHostWillBePassedAsIs() { $requestAssertion = null; - $server = new Server($this->socket, function (RequestInterface $request) use (&$requestAssertion) { + $server = new Server($this->socket, function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; return new Response(); }); @@ -269,7 +269,7 @@ public function testRequestNonConnectWithAuthorityRequestTargetWillReject() public function testRequestPauseWillbeForwardedToConnection() { - $server = new Server($this->socket, function (RequestInterface $request) { + $server = new Server($this->socket, function (ServerRequestInterface $request) { $request->getBody()->pause(); return \React\Promise\resolve(new Response()); }); @@ -288,7 +288,7 @@ public function testRequestPauseWillbeForwardedToConnection() public function testRequestResumeWillbeForwardedToConnection() { - $server = new Server($this->socket, function (RequestInterface $request) { + $server = new Server($this->socket, function (ServerRequestInterface $request) { $request->getBody()->resume(); return \React\Promise\resolve(new Response()); }); @@ -302,7 +302,7 @@ public function testRequestResumeWillbeForwardedToConnection() public function testRequestCloseWillPauseConnection() { - $server = new Server($this->socket, function (RequestInterface $request) { + $server = new Server($this->socket, function (ServerRequestInterface $request) { $request->getBody()->close(); return \React\Promise\resolve(new Response()); }); @@ -316,7 +316,7 @@ public function testRequestCloseWillPauseConnection() public function testRequestPauseAfterCloseWillNotBeForwarded() { - $server = new Server($this->socket, function (RequestInterface $request) { + $server = new Server($this->socket, function (ServerRequestInterface $request) { $request->getBody()->close(); $request->getBody()->pause();# @@ -332,7 +332,7 @@ public function testRequestPauseAfterCloseWillNotBeForwarded() public function testRequestResumeAfterCloseWillNotBeForwarded() { - $server = new Server($this->socket, function (RequestInterface $request) { + $server = new Server($this->socket, function (ServerRequestInterface $request) { $request->getBody()->close(); $request->getBody()->resume(); @@ -351,7 +351,7 @@ public function testRequestEventWithoutBodyWillNotEmitData() { $never = $this->expectCallableNever(); - $server = new Server($this->socket, function (RequestInterface $request) use ($never) { + $server = new Server($this->socket, function (ServerRequestInterface $request) use ($never) { $request->getBody()->on('data', $never); return \React\Promise\resolve(new Response()); @@ -367,7 +367,7 @@ public function testRequestEventWithSecondDataEventWillEmitBodyData() { $once = $this->expectCallableOnceWith('incomplete'); - $server = new Server($this->socket, function (RequestInterface $request) use ($once) { + $server = new Server($this->socket, function (ServerRequestInterface $request) use ($once) { $request->getBody()->on('data', $once); return \React\Promise\resolve(new Response()); @@ -388,7 +388,7 @@ public function testRequestEventWithPartialBodyWillEmitData() { $once = $this->expectCallableOnceWith('incomplete'); - $server = new Server($this->socket, function (RequestInterface $request) use ($once) { + $server = new Server($this->socket, function (ServerRequestInterface $request) use ($once) { $request->getBody()->on('data', $once); return \React\Promise\resolve(new Response()); @@ -410,7 +410,7 @@ public function testRequestEventWithPartialBodyWillEmitData() public function testResponseContainsPoweredByHeader() { - $server = new Server($this->socket, function (RequestInterface $request) { + $server = new Server($this->socket, function (ServerRequestInterface $request) { return \React\Promise\resolve(new Response()); }); @@ -437,7 +437,7 @@ function ($data) use (&$buffer) { public function testResponseContainsSameRequestProtocolVersionAndChunkedBodyForHttp11() { - $server = new Server($this->socket, function (RequestInterface $request) { + $server = new Server($this->socket, function (ServerRequestInterface $request) { $response = new Response(200, array(), 'bye'); return \React\Promise\resolve($response); }); @@ -466,7 +466,7 @@ function ($data) use (&$buffer) { public function testResponseContainsSameRequestProtocolVersionAndRawBodyForHttp10() { - $server = new Server($this->socket, function (RequestInterface $request) { + $server = new Server($this->socket, function (ServerRequestInterface $request) { $response = new Response(200, array(), 'bye'); return \React\Promise\resolve($response); }); @@ -496,7 +496,7 @@ function ($data) use (&$buffer) { public function testResponseContainsNoResponseBodyForHeadRequest() { - $server = new Server($this->socket, function (RequestInterface $request) { + $server = new Server($this->socket, function (ServerRequestInterface $request) { return new Response(200, array(), 'bye'); }); @@ -523,7 +523,7 @@ function ($data) use (&$buffer) { public function testResponseContainsNoResponseBodyAndNoContentLengthForNoContentStatus() { - $server = new Server($this->socket, function (RequestInterface $request) { + $server = new Server($this->socket, function (ServerRequestInterface $request) { return new Response(204, array(), 'bye'); }); @@ -551,7 +551,7 @@ function ($data) use (&$buffer) { public function testResponseContainsNoResponseBodyForNotModifiedStatus() { - $server = new Server($this->socket, function (RequestInterface $request) { + $server = new Server($this->socket, function (ServerRequestInterface $request) { return new Response(304, array(), 'bye'); }); @@ -682,7 +682,7 @@ public function testBodyDataWillBeSendViaRequestEvent() $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new Server($this->socket, function (RequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new Server($this->socket, function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -711,7 +711,7 @@ public function testChunkedEncodedRequestWillBeParsedForRequestEvent() $errorEvent = $this->expectCallableNever(); $requestValidation = null; - $server = new Server($this->socket, function (RequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent, &$requestValidation) { + $server = new Server($this->socket, function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent, &$requestValidation) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -743,7 +743,7 @@ public function testChunkedEncodedRequestAdditionalDataWontBeEmitted() $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new Server($this->socket, function (RequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new Server($this->socket, function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -774,7 +774,7 @@ public function testEmptyChunkedEncodedRequest() $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new Server($this->socket, function (RequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new Server($this->socket, function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -802,7 +802,7 @@ public function testChunkedIsUpperCase() $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new Server($this->socket, function (RequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new Server($this->socket, function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -832,7 +832,7 @@ public function testChunkedIsMixedUpperAndLowerCase() $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new Server($this->socket, function (RequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new Server($this->socket, function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -952,7 +952,7 @@ function ($data) use (&$buffer) { public function testRequestHttp10WithoutHostEmitsRequestWithNoError() { - $server = new Server($this->socket, function (RequestInterface $request) { + $server = new Server($this->socket, function (ServerRequestInterface $request) { return \React\Promise\resolve(new Response()); }); $server->on('error', $this->expectCallableNever()); @@ -970,7 +970,7 @@ public function testWontEmitFurtherDataWhenContentLengthIsReached() $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new Server($this->socket, function (RequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new Server($this->socket, function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1000,7 +1000,7 @@ public function testWontEmitFurtherDataWhenContentLengthIsReachedSplitted() $errorEvent = $this->expectCallableNever(); - $server = new Server($this->socket, function (RequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new Server($this->socket, function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1034,7 +1034,7 @@ public function testContentLengthContainsZeroWillEmitEndEvent() $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new Server($this->socket, function (RequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new Server($this->socket, function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1061,7 +1061,7 @@ public function testContentLengthContainsZeroWillEmitEndEventAdditionalDataWillB $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new Server($this->socket, function (RequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new Server($this->socket, function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1089,7 +1089,7 @@ public function testContentLengthContainsZeroWillEmitEndEventAdditionalDataWillB $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new Server($this->socket, function (RequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new Server($this->socket, function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1121,7 +1121,7 @@ public function testContentLengthWillBeIgnoredIfTransferEncodingIsSet() $errorEvent = $this->expectCallableNever(); $requestValidation = null; - $server = new Server($this->socket, function (RequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent, &$requestValidation) { + $server = new Server($this->socket, function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent, &$requestValidation) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1159,7 +1159,7 @@ public function testInvalidContentLengthWillBeIgnoreddIfTransferEncodingIsSet() $errorEvent = $this->expectCallableNever(); $requestValidation = null; - $server = new Server($this->socket, function (RequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent, &$requestValidation) { + $server = new Server($this->socket, function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent, &$requestValidation) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1395,7 +1395,7 @@ public function testUnexpectedEndOfConnectionWillResultsInErrorOnRequestStream() public function testErrorInChunkedDecoderNeverClosesConnection() { - $server = new Server($this->socket, function (RequestInterface $request) { + $server = new Server($this->socket, function (ServerRequestInterface $request) { return \React\Promise\resolve(new Response()); }); @@ -1416,7 +1416,7 @@ public function testErrorInChunkedDecoderNeverClosesConnection() public function testErrorInLengthLimitedStreamNeverClosesConnection() { - $server = new Server($this->socket, function (RequestInterface $request) { + $server = new Server($this->socket, function (ServerRequestInterface $request) { return \React\Promise\resolve(new Response()); }); @@ -1485,7 +1485,7 @@ public function testRequestWithoutDefinedLengthWillIgnoreDataEvent() $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new Server($this->socket, function (RequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new Server($this->socket, function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1505,7 +1505,7 @@ public function testRequestWithoutDefinedLengthWillIgnoreDataEvent() public function testResponseWillBeChunkDecodedByDefault() { $stream = new ReadableStream(); - $server = new Server($this->socket, function (RequestInterface $request) use ($stream) { + $server = new Server($this->socket, function (ServerRequestInterface $request) use ($stream) { $response = new Response(200, array(), $stream); return \React\Promise\resolve($response); }); @@ -1535,7 +1535,7 @@ function ($data) use (&$buffer) { public function testContentLengthWillBeRemovedForResponseStream() { - $server = new Server($this->socket, function (RequestInterface $request) { + $server = new Server($this->socket, function (ServerRequestInterface $request) { $response = new Response( 200, array( @@ -1574,7 +1574,7 @@ function ($data) use (&$buffer) { public function testOnlyAllowChunkedEncoding() { $stream = new ReadableStream(); - $server = new Server($this->socket, function (RequestInterface $request) use ($stream) { + $server = new Server($this->socket, function (ServerRequestInterface $request) use ($stream) { $response = new Response( 200, array( @@ -1612,7 +1612,7 @@ function ($data) use (&$buffer) { public function testDateHeaderWillBeAddedWhenNoneIsGiven() { - $server = new Server($this->socket, function (RequestInterface $request) { + $server = new Server($this->socket, function (ServerRequestInterface $request) { return \React\Promise\resolve(new Response()); }); @@ -1641,7 +1641,7 @@ function ($data) use (&$buffer) { public function testAddCustomDateHeader() { - $server = new Server($this->socket, function (RequestInterface $request) { + $server = new Server($this->socket, function (ServerRequestInterface $request) { $response = new Response(200, array("Date" => "Tue, 15 Nov 1994 08:12:31 GMT")); return \React\Promise\resolve($response); }); @@ -1671,7 +1671,7 @@ function ($data) use (&$buffer) { public function testRemoveDateHeader() { - $server = new Server($this->socket, function (RequestInterface $request) { + $server = new Server($this->socket, function (ServerRequestInterface $request) { $response = new Response(200, array('Date' => '')); return \React\Promise\resolve($response); }); @@ -1769,7 +1769,7 @@ function ($data) use (&$buffer) { public function test100ContinueRequestWillBeHandled() { - $server = new Server($this->socket, function (RequestInterface $request) { + $server = new Server($this->socket, function (ServerRequestInterface $request) { return \React\Promise\resolve(new Response()); }); @@ -1800,7 +1800,7 @@ function ($data) use (&$buffer) { public function testContinueWontBeSendForHttp10() { - $server = new Server($this->socket, function (RequestInterface $request) { + $server = new Server($this->socket, function (ServerRequestInterface $request) { return \React\Promise\resolve(new Response()); }); @@ -1829,7 +1829,7 @@ function ($data) use (&$buffer) { public function testContinueWithLaterResponse() { - $server = new Server($this->socket, function (RequestInterface $request) { + $server = new Server($this->socket, function (ServerRequestInterface $request) { return \React\Promise\resolve(new Response()); }); @@ -1872,7 +1872,7 @@ public function testHttpBodyStreamAsBodyWillStreamData() { $input = new ReadableStream(); - $server = new Server($this->socket, function (RequestInterface $request) use ($input) { + $server = new Server($this->socket, function (ServerRequestInterface $request) use ($input) { $response = new Response(200, array(), $input); return \React\Promise\resolve($response); }); @@ -1907,7 +1907,7 @@ public function testHttpBodyStreamWithContentLengthWillStreamTillLength() { $input = new ReadableStream(); - $server = new Server($this->socket, function (RequestInterface $request) use ($input) { + $server = new Server($this->socket, function (ServerRequestInterface $request) use ($input) { $response = new Response(200, array('Content-Length' => 5), $input); return \React\Promise\resolve($response); }); @@ -1941,7 +1941,7 @@ function ($data) use (&$buffer) { public function testCallbackFunctionReturnsPromise() { - $server = new Server($this->socket, function (RequestInterface $request) { + $server = new Server($this->socket, function (ServerRequestInterface $request) { return \React\Promise\resolve(new Response()); }); @@ -1968,7 +1968,7 @@ function ($data) use (&$buffer) { public function testReturnInvalidTypeWillResultInError() { - $server = new Server($this->socket, function (RequestInterface $request) { + $server = new Server($this->socket, function (ServerRequestInterface $request) { return "invalid"; }); @@ -2003,7 +2003,7 @@ function ($data) use (&$buffer) { public function testResolveWrongTypeInPromiseWillResultInError() { - $server = new Server($this->socket, function (RequestInterface $request) { + $server = new Server($this->socket, function (ServerRequestInterface $request) { return \React\Promise\resolve("invalid"); }); @@ -2032,7 +2032,7 @@ function ($data) use (&$buffer) { public function testRejectedPromiseWillResultInErrorMessage() { - $server = new Server($this->socket, function (RequestInterface $request) { + $server = new Server($this->socket, function (ServerRequestInterface $request) { return new Promise(function ($resolve, $reject) { $reject(new \Exception()); }); @@ -2064,7 +2064,7 @@ function ($data) use (&$buffer) { public function testExcpetionInCallbackWillResultInErrorMessage() { - $server = new Server($this->socket, function (RequestInterface $request) { + $server = new Server($this->socket, function (ServerRequestInterface $request) { return new Promise(function ($resolve, $reject) { throw new \Exception('Bad call'); }); @@ -2096,7 +2096,7 @@ function ($data) use (&$buffer) { public function testHeaderWillAlwaysBeContentLengthForStringBody() { - $server = new Server($this->socket, function (RequestInterface $request) { + $server = new Server($this->socket, function (ServerRequestInterface $request) { return new Response(200, array('Transfer-Encoding' => 'chunked'), 'hello'); }); @@ -2129,7 +2129,7 @@ function ($data) use (&$buffer) { public function testReturnRequestWillBeHandled() { - $server = new Server($this->socket, function (RequestInterface $request) { + $server = new Server($this->socket, function (ServerRequestInterface $request) { return new Response(); }); @@ -2158,7 +2158,7 @@ function ($data) use (&$buffer) { public function testExceptionThrowInCallBackFunctionWillResultInErrorMessage() { - $server = new Server($this->socket, function (RequestInterface $request) { + $server = new Server($this->socket, function (ServerRequestInterface $request) { throw new \Exception('hello'); }); @@ -2194,7 +2194,7 @@ function ($data) use (&$buffer) { public function testRejectOfNonExceptionWillResultInErrorMessage() { - $server = new Server($this->socket, function (RequestInterface $request) { + $server = new Server($this->socket, function (ServerRequestInterface $request) { return new Promise(function ($resolve, $reject) { $reject('Invalid type'); }); From db4fa508683ebfa74f5b1575a9fc6b75befb74af Mon Sep 17 00:00:00 2001 From: Niels Theen Date: Thu, 20 Apr 2017 01:32:59 +0200 Subject: [PATCH 128/456] Update documentation --- README.md | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 1006559f..685791cd 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ This is an HTTP server which responds with `Hello World` to every request. $loop = React\EventLoop\Factory::create(); $socket = new React\Socket\Server(8080, $loop); -$http = new Server($socket, function (RequestInterface $request) { +$http = new Server($socket, function (ServerRequestInterface $request) { return new Response( 200, array('Content-Type' => 'text/plain'), @@ -54,7 +54,7 @@ constructor with the respective [request](#request) and ```php $socket = new React\Socket\Server(8080, $loop); -$http = new Server($socket, function (RequestInterface $request) { +$http = new Server($socket, function (ServerRequestInterface $request) { return new Response( 200, array('Content-Type' => 'text/plain'), @@ -75,7 +75,7 @@ $socket = new React\Socket\SecureServer($socket, $loop, array( 'local_cert' => __DIR__ . '/localhost.pem' )); -$http = new Server($socket, function (RequestInterface $request) { +$http = new Server($socket, function (ServerRequestInterface $request) { return new Response( 200, array('Content-Type' => 'text/plain'), @@ -137,11 +137,13 @@ connections and then processing each incoming HTTP request. The request object will be processed once the request headers have been received by the client. This request object implements the +[PSR-7 ServerRequestInterface](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-7-http-message.md#321-psrhttpmessageserverrequestinterface) +which in turn extends the [PSR-7 RequestInterface](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-7-http-message.md#32-psrhttpmessagerequestinterface) and will be passed to the callback function like this. ```php -$http = new Server($socket, function (RequestInterface $request) { +$http = new Server($socket, function (ServerRequestInterface $request) { $body = "The method of the request is: " . $request->getMethod(); $body .= "The requested path is: " . $request->getUri()->getPath(); @@ -154,8 +156,14 @@ $http = new Server($socket, function (RequestInterface $request) { ``` For more details about the request object, check out the documentation of +[PSR-7 ServerRequestInterface](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-7-http-message.md#321-psrhttpmessageserverrequestinterface) +and [PSR-7 RequestInterface](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-7-http-message.md#32-psrhttpmessagerequestinterface). +> Currently the the server params, cookies and uploaded files are not added by the + `Server`, but you can add these parameters by yourself using the given methods. + The next versions of this project will cover these features. + Note that the request object will be processed once the request headers have been received. This means that this happens irrespective of (i.e. *before*) receiving the @@ -184,7 +192,7 @@ Instead, you should use the `ReactPHP ReadableStreamInterface` which gives you access to the incoming request body as the individual chunks arrive: ```php -$http = new Server($socket, function (RequestInterface $request) { +$http = new Server($socket, function (ServerRequestInterface $request) { return new Promise(function ($resolve, $reject) use ($request) { $contentLength = 0; $request->getBody()->on('data', function ($data) use (&$contentLength) { @@ -248,7 +256,7 @@ Note that this value may be `null` if the request body size is unknown in advance because the request message uses chunked transfer encoding. ```php -$http = new Server($socket, function (RequestInterface $request) { +$http = new Server($socket, function (ServerRequestInterface $request) { $size = $request->getBody()->getSize(); if ($size === null) { $body = 'The request does not contain an explicit length.'; @@ -308,7 +316,7 @@ but feel free to use any implemantation of the `PSR-7 ResponseInterface` you prefer. ```php -$http = new Server($socket, function (RequestInterface $request) { +$http = new Server($socket, function (ServerRequestInterface $request) { return new Response( 200, array('Content-Type' => 'text/plain'), @@ -327,7 +335,7 @@ To prevent this you SHOULD use a This example shows how such a long-term action could look like: ```php -$server = new \React\Http\Server($socket, function (RequestInterface $request) use ($loop) { +$server = new \React\Http\Server($socket, function (ServerRequestInterface $request) use ($loop) { return new Promise(function ($resolve, $reject) use ($request, $loop) { $loop->addTimer(1.5, function() use ($loop, $resolve) { $response = new Response( @@ -355,7 +363,7 @@ Note that other implementations of the `PSR-7 ResponseInterface` likely only support string. ```php -$server = new Server($socket, function (RequestInterface $request) use ($loop) { +$server = new Server($socket, function (ServerRequestInterface $request) use ($loop) { $stream = new ReadableStream(); $timer = $loop->addPeriodicTimer(0.5, function () use ($stream) { @@ -389,7 +397,7 @@ If you know the length of your stream body, you MAY specify it like this instead ```php $stream = new ReadableStream() -$server = new Server($socket, function (RequestInterface $request) use ($loop, $stream) { +$server = new Server($socket, function (ServerRequestInterface $request) use ($loop, $stream) { return new Response( 200, array( @@ -437,7 +445,7 @@ A `Date` header will be automatically added with the system date and time if non You can add a custom `Date` header yourself like this: ```php -$server = new Server($socket, function (RequestInterface $request) { +$server = new Server($socket, function (ServerRequestInterface $request) { return new Response(200, array('Date' => date('D, d M Y H:i:s T'))); }); ``` @@ -446,7 +454,7 @@ If you don't have a appropriate clock to rely on, you should unset this header with an empty string: ```php -$server = new Server($socket, function (RequestInterface $request) { +$server = new Server($socket, function (ServerRequestInterface $request) { return new Response(200, array('Date' => '')); }); ``` @@ -455,7 +463,7 @@ Note that it will automatically assume a `X-Powered-By: react/alpha` header unless your specify a custom `X-Powered-By` header yourself: ```php -$server = new Server($socket, function (RequestInterface $request) { +$server = new Server($socket, function (ServerRequestInterface $request) { return new Response(200, array('X-Powered-By' => 'PHP 3')); }); ``` @@ -464,7 +472,7 @@ If you do not want to send this header at all, you can use an empty string as value like this: ```php -$server = new Server($socket, function (RequestInterface $request) { +$server = new Server($socket, function (ServerRequestInterface $request) { return new Response(200, array('X-Powered-By' => '')); }); ``` From 1296e1194e6734ca24398bf2a186c7ed935d7991 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 3 Mar 2017 10:05:46 +0100 Subject: [PATCH 129/456] Validate proxy requests in absolute-form --- README.md | 10 +- examples/21-http-proxy.php | 45 +++++++ ...connect-proxy.php => 22-connect-proxy.php} | 0 src/RequestHeaderParser.php | 16 +++ tests/RequestHeaderParserTest.php | 32 +++++ tests/ServerTest.php | 111 ++++++++++++++++++ 6 files changed, 212 insertions(+), 2 deletions(-) create mode 100644 examples/21-http-proxy.php rename examples/{21-connect-proxy.php => 22-connect-proxy.php} (100%) diff --git a/README.md b/README.md index 685791cd..74cf2254 100644 --- a/README.md +++ b/README.md @@ -279,7 +279,13 @@ $http = new Server($socket, function (ServerRequestInterface $request) { Note that the server supports *any* request method (including custom and non- standard ones) and all request-target formats defined in the HTTP specs for each -respective method. +respective method, including *normal* `origin-form` requests as well as +proxy requests in `absolute-form` and `authority-form`. +The `getUri(): UriInterface` method can be used to get the effective request +URI which provides you access to individiual URI components. +Note that (depending on the given `request-target`) certain URI components may +or may not be present, for example the `getPath(): string` method will return +an empty string for requests in `asterisk-form` or `authority-form`. You can use `getMethod(): string` and `getRequestTarget(): string` to check this is an accepted request and may want to reject other requests with an appropriate error code, such as `400` (Bad Request) or `405` (Method Not @@ -439,7 +445,7 @@ to the message if the same request would have used an (unconditional) `GET`. response for tunneled application data. This implies that that a `2xx` (Successful) response to a `CONNECT` request can in fact use a streaming response body for the tunneled application data. - See also [example #21](examples) for more details. + See also [example #22](examples) for more details. A `Date` header will be automatically added with the system date and time if none is given. You can add a custom `Date` header yourself like this: diff --git a/examples/21-http-proxy.php b/examples/21-http-proxy.php new file mode 100644 index 00000000..720f51fe --- /dev/null +++ b/examples/21-http-proxy.php @@ -0,0 +1,45 @@ +getRequestTarget(), '://') === false) { + return new Response( + 400, + array('Content-Type' => 'text/plain'), + 'This is a plain HTTP proxy' + ); + } + + // prepare outgoing client request by updating request-target and Host header + $host = (string)$request->getUri()->withScheme('')->withPath('')->withQuery(''); + $target = (string)$request->getUri()->withScheme('')->withHost('')->withPort(null); + if ($target === '') { + $target = $request->getMethod() === 'OPTIONS' ? '*' : '/'; + } + $outgoing = $request->withRequestTarget($target)->withHeader('Host', $host); + + // pseudo code only: simply dump the outgoing request as a string + // left up as an exercise: use an HTTP client to send the outgoing request + // and forward the incoming response to the original client request + return new Response( + 200, + array('Content-Type' => 'text/plain'), + Psr7\str($outgoing) + ); +}); + +//$server->on('error', 'printf'); + +echo 'Listening on http://' . $socket->getAddress() . PHP_EOL; + +$loop->run(); diff --git a/examples/21-connect-proxy.php b/examples/22-connect-proxy.php similarity index 100% rename from examples/21-connect-proxy.php rename to examples/22-connect-proxy.php diff --git a/src/RequestHeaderParser.php b/src/RequestHeaderParser.php index 510de700..97fa7a00 100644 --- a/src/RequestHeaderParser.php +++ b/src/RequestHeaderParser.php @@ -71,7 +71,12 @@ private function parseRequest($data) } } + // parse request headers into obj implementing RequestInterface $request = g7\parse_request($headers); + + // create new obj implementing ServerRequestInterface by preserving all + // previous properties and restoring original request target-target + $target = $request->getRequestTarget(); $request = new ServerRequest( $request->getMethod(), $request->getUri(), @@ -79,6 +84,7 @@ private function parseRequest($data) $request->getBody(), $request->getProtocolVersion() ); + $request = $request->withRequestTarget($target); // Do not assume this is HTTPS when this happens to be port 443 // detecting HTTPS is left up to the socket layer (TLS detection) @@ -96,6 +102,16 @@ private function parseRequest($data) )->withRequestTarget($originalTarget); } + // ensure absolute-form request-target contains a valid URI + if (strpos($request->getRequestTarget(), '://') !== false) { + $parts = parse_url($request->getRequestTarget()); + + // 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'); + } + } + return array($request, $bodyBuffer); } } diff --git a/tests/RequestHeaderParserTest.php b/tests/RequestHeaderParserTest.php index dfc25f4f..f0422361 100644 --- a/tests/RequestHeaderParserTest.php +++ b/tests/RequestHeaderParserTest.php @@ -167,6 +167,38 @@ public function testGuzzleRequestParseException() $this->assertSame(0, count($parser->listeners('error'))); } + public function testInvalidAbsoluteFormSchemeEmitsError() + { + $error = null; + + $parser = new RequestHeaderParser(); + $parser->on('headers', $this->expectCallableNever()); + $parser->on('error', function ($message) use (&$error) { + $error = $message; + }); + + $parser->feed("GET tcp://example.com:80/ HTTP/1.0\r\n\r\n"); + + $this->assertInstanceOf('InvalidArgumentException', $error); + $this->assertSame('Invalid absolute-form request-target', $error->getMessage()); + } + + public function testInvalidAbsoluteFormWithFragmentEmitsError() + { + $error = null; + + $parser = new RequestHeaderParser(); + $parser->on('headers', $this->expectCallableNever()); + $parser->on('error', function ($message) use (&$error) { + $error = $message; + }); + + $parser->feed("GET http://example.com:80/#home HTTP/1.0\r\n\r\n"); + + $this->assertInstanceOf('InvalidArgumentException', $error); + $this->assertSame('Invalid absolute-form request-target', $error->getMessage()); + } + private function createGetRequest() { $data = "GET / HTTP/1.1\r\n"; diff --git a/tests/ServerTest.php b/tests/ServerTest.php index d831aa86..aa4b8f89 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -267,6 +267,117 @@ public function testRequestNonConnectWithAuthorityRequestTargetWillReject() $this->connection->emit('data', array($data)); } + public function testRequestAbsoluteEvent() + { + $requestAssertion = null; + + $server = new Server($this->socket, function (ServerRequestInterface $request) use (&$requestAssertion) { + $requestAssertion = $request; + return new Response(); + }); + + $this->socket->emit('connection', array($this->connection)); + + $data = "GET http://example.com/test HTTP/1.1\r\nHost: example.com\r\n\r\n"; + $this->connection->emit('data', array($data)); + + $this->assertInstanceOf('RingCentral\Psr7\Request', $requestAssertion); + $this->assertSame('GET', $requestAssertion->getMethod()); + $this->assertSame('http://example.com/test', $requestAssertion->getRequestTarget()); + $this->assertEquals('http://example.com/test', $requestAssertion->getUri()); + $this->assertSame('/test', $requestAssertion->getUri()->getPath()); + $this->assertSame('example.com', $requestAssertion->getHeaderLine('Host')); + } + + public function testRequestAbsoluteAddsMissingHostEvent() + { + $requestAssertion = null; + + $server = new Server($this->socket, function (ServerRequestInterface $request) use (&$requestAssertion) { + $requestAssertion = $request; + return new Response(); + }); + $server->on('error', 'printf'); + + $this->socket->emit('connection', array($this->connection)); + + $data = "GET http://example.com:8080/test HTTP/1.0\r\n\r\n"; + $this->connection->emit('data', array($data)); + + $this->assertInstanceOf('RingCentral\Psr7\Request', $requestAssertion); + $this->assertSame('GET', $requestAssertion->getMethod()); + $this->assertSame('http://example.com:8080/test', $requestAssertion->getRequestTarget()); + $this->assertEquals('http://example.com:8080/test', $requestAssertion->getUri()); + $this->assertSame('/test', $requestAssertion->getUri()->getPath()); + $this->assertSame('example.com:8080', $requestAssertion->getHeaderLine('Host')); + } + + public function testRequestAbsoluteNonMatchingHostWillBePassedAsIs() + { + $requestAssertion = null; + + $server = new Server($this->socket, function (ServerRequestInterface $request) use (&$requestAssertion) { + $requestAssertion = $request; + return new Response(); + }); + + $this->socket->emit('connection', array($this->connection)); + + $data = "GET http://example.com/test HTTP/1.1\r\nHost: other.example.org\r\n\r\n"; + $this->connection->emit('data', array($data)); + + $this->assertInstanceOf('RingCentral\Psr7\Request', $requestAssertion); + $this->assertSame('GET', $requestAssertion->getMethod()); + $this->assertSame('http://example.com/test', $requestAssertion->getRequestTarget()); + $this->assertEquals('http://example.com/test', $requestAssertion->getUri()); + $this->assertSame('/test', $requestAssertion->getUri()->getPath()); + $this->assertSame('other.example.org', $requestAssertion->getHeaderLine('Host')); + } + + public function testRequestOptionsAsteriskEvent() + { + $requestAssertion = null; + + $server = new Server($this->socket, function (ServerRequestInterface $request) use (&$requestAssertion) { + $requestAssertion = $request; + return new Response(); + }); + + $this->socket->emit('connection', array($this->connection)); + + $data = "OPTIONS * HTTP/1.1\r\nHost: example.com\r\n\r\n"; + $this->connection->emit('data', array($data)); + + $this->assertInstanceOf('RingCentral\Psr7\Request', $requestAssertion); + $this->assertSame('OPTIONS', $requestAssertion->getMethod()); + $this->assertSame('*', $requestAssertion->getRequestTarget()); + $this->assertEquals('http://example.com', $requestAssertion->getUri()); + $this->assertSame('', $requestAssertion->getUri()->getPath()); + $this->assertSame('example.com', $requestAssertion->getHeaderLine('Host')); + } + + public function testRequestOptionsAbsoluteEvent() + { + $requestAssertion = null; + + $server = new Server($this->socket, function (ServerRequestInterface $request) use (&$requestAssertion) { + $requestAssertion = $request; + return new Response(); + }); + + $this->socket->emit('connection', array($this->connection)); + + $data = "OPTIONS http://example.com HTTP/1.1\r\nHost: example.com\r\n\r\n"; + $this->connection->emit('data', array($data)); + + $this->assertInstanceOf('RingCentral\Psr7\Request', $requestAssertion); + $this->assertSame('OPTIONS', $requestAssertion->getMethod()); + $this->assertSame('http://example.com', $requestAssertion->getRequestTarget()); + $this->assertEquals('http://example.com', $requestAssertion->getUri()); + $this->assertSame('', $requestAssertion->getUri()->getPath()); + $this->assertSame('example.com', $requestAssertion->getHeaderLine('Host')); + } + public function testRequestPauseWillbeForwardedToConnection() { $server = new Server($this->socket, function (ServerRequestInterface $request) { From 5932089388ff5dae1ea09602ebfe67680ba9664b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 18 Apr 2017 15:27:42 +0200 Subject: [PATCH 130/456] Use socket address for URI if Host header is missing --- README.md | 6 ++ src/Server.php | 10 +++ tests/FunctionalServerTest.php | 146 +++++++++++++++++++++++++++++++++ tests/ServerTest.php | 26 ++++++ 4 files changed, 188 insertions(+) diff --git a/README.md b/README.md index 74cf2254..988baec7 100644 --- a/README.md +++ b/README.md @@ -286,6 +286,12 @@ URI which provides you access to individiual URI components. Note that (depending on the given `request-target`) certain URI components may or may not be present, for example the `getPath(): string` method will return an empty string for requests in `asterisk-form` or `authority-form`. +Its `getHost(): string` method will return the host as determined by the +effective request URI, which defaults to the local socket address if a HTTP/1.0 +client did not specify one (i.e. no `Host` header). +Its `getScheme(): string` method will return `http` or `https` depending +on whether the request was made over a secure TLS connection to the target host. + You can use `getMethod(): string` and `getRequestTarget(): string` to check this is an accepted request and may want to reject other requests with an appropriate error code, such as `400` (Bad Request) or `405` (Method Not diff --git a/src/Server.php b/src/Server.php index f901bdec..b47b7900 100644 --- a/src/Server.php +++ b/src/Server.php @@ -198,6 +198,16 @@ public function handleRequest(ConnectionInterface $conn, ServerRequestInterface } } + // set URI components from socket address if not already filled via Host header + if ($request->getUri()->getHost() === '') { + $parts = parse_url('tcp://' . $conn->getLocalAddress()); + + $request = $request->withUri( + $request->getUri()->withScheme('http')->withHost($parts['host'])->withPort($parts['port']), + true + ); + } + $contentLength = 0; $stream = new CloseProtectionStream($conn); if ($request->getMethod() === 'CONNECT') { diff --git a/tests/FunctionalServerTest.php b/tests/FunctionalServerTest.php index 4c7ed6ff..f34f2bc3 100644 --- a/tests/FunctionalServerTest.php +++ b/tests/FunctionalServerTest.php @@ -39,6 +39,54 @@ public function testPlainHttpOnRandomPort() $socket->close(); } + public function testPlainHttpOnRandomPortWithoutHostHeaderUsesSocketUri() + { + $loop = Factory::create(); + $socket = new Socket(0, $loop); + $connector = new Connector($loop); + + $server = new Server($socket, function (RequestInterface $request) { + return new Response(200, array(), (string)$request->getUri()); + }); + + $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { + $conn->write("GET / HTTP/1.0\r\n\r\n"); + + return BufferedSink::createPromise($conn); + }); + + $response = Block\await($result, $loop, 1.0); + + $this->assertContains("HTTP/1.0 200 OK", $response); + $this->assertContains('http://' . $socket->getAddress() . '/', $response); + + $socket->close(); + } + + public function testPlainHttpOnRandomPortWithOtherHostHeaderTakesPrecedence() + { + $loop = Factory::create(); + $socket = new Socket(0, $loop); + $connector = new Connector($loop); + + $server = new Server($socket, function (RequestInterface $request) { + return new Response(200, array(), (string)$request->getUri()); + }); + + $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { + $conn->write("GET / HTTP/1.0\r\nHost: localhost:1000\r\n\r\n"); + + return BufferedSink::createPromise($conn); + }); + + $response = Block\await($result, $loop, 1.0); + + $this->assertContains("HTTP/1.0 200 OK", $response); + $this->assertContains('http://localhost:1000/', $response); + + $socket->close(); + } + public function testSecureHttpsOnRandomPort() { if (!function_exists('stream_socket_enable_crypto')) { @@ -72,6 +120,39 @@ public function testSecureHttpsOnRandomPort() $socket->close(); } + public function testSecureHttpsOnRandomPortWithoutHostHeaderUsesSocketUri() + { + if (!function_exists('stream_socket_enable_crypto')) { + $this->markTestSkipped('Not supported on your platform (outdated HHVM?)'); + } + + $loop = Factory::create(); + $socket = new Socket(0, $loop); + $socket = new SecureServer($socket, $loop, array( + 'local_cert' => __DIR__ . '/../examples/localhost.pem' + )); + $connector = new Connector($loop, array( + 'tls' => array('verify_peer' => false) + )); + + $server = new Server($socket, function (RequestInterface $request) { + return new Response(200, array(), (string)$request->getUri()); + }); + + $result = $connector->connect('tls://' . $socket->getAddress())->then(function (ConnectionInterface $conn) { + $conn->write("GET / HTTP/1.0\r\n\r\n"); + + return BufferedSink::createPromise($conn); + }); + + $response = Block\await($result, $loop, 1.0); + + $this->assertContains("HTTP/1.0 200 OK", $response); + $this->assertContains('https://' . $socket->getAddress() . '/', $response); + + $socket->close(); + } + public function testPlainHttpOnStandardPortReturnsUriWithNoPort() { $loop = Factory::create(); @@ -100,6 +181,34 @@ public function testPlainHttpOnStandardPortReturnsUriWithNoPort() $socket->close(); } + public function testPlainHttpOnStandardPortWithoutHostHeaderReturnsUriWithNoPort() + { + $loop = Factory::create(); + try { + $socket = new Socket(80, $loop); + } catch (\RuntimeException $e) { + $this->markTestSkipped('Listening on port 80 failed (root and unused?)'); + } + $connector = new Connector($loop); + + $server = new Server($socket, function (RequestInterface $request) { + return new Response(200, array(), (string)$request->getUri()); + }); + + $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { + $conn->write("GET / HTTP/1.0\r\n\r\n"); + + return BufferedSink::createPromise($conn); + }); + + $response = Block\await($result, $loop, 1.0); + + $this->assertContains("HTTP/1.0 200 OK", $response); + $this->assertContains('http://127.0.0.1/', $response); + + $socket->close(); + } + public function testSecureHttpsOnStandardPortReturnsUriWithNoPort() { if (!function_exists('stream_socket_enable_crypto')) { @@ -137,6 +246,43 @@ public function testSecureHttpsOnStandardPortReturnsUriWithNoPort() $socket->close(); } + public function testSecureHttpsOnStandardPortWithoutHostHeaderUsesSocketUri() + { + if (!function_exists('stream_socket_enable_crypto')) { + $this->markTestSkipped('Not supported on your platform (outdated HHVM?)'); + } + + $loop = Factory::create(); + try { + $socket = new Socket(443, $loop); + } catch (\RuntimeException $e) { + $this->markTestSkipped('Listening on port 443 failed (root and unused?)'); + } + $socket = new SecureServer($socket, $loop, array( + 'local_cert' => __DIR__ . '/../examples/localhost.pem' + )); + $connector = new Connector($loop, array( + 'tls' => array('verify_peer' => false) + )); + + $server = new Server($socket, function (RequestInterface $request) { + return new Response(200, array(), (string)$request->getUri()); + }); + + $result = $connector->connect('tls://' . $socket->getAddress())->then(function (ConnectionInterface $conn) { + $conn->write("GET / HTTP/1.0\r\n\r\n"); + + return BufferedSink::createPromise($conn); + }); + + $response = Block\await($result, $loop, 1.0); + + $this->assertContains("HTTP/1.0 200 OK", $response); + $this->assertContains('https://127.0.0.1/', $response); + + $socket->close(); + } + public function testPlainHttpOnHttpsStandardPortReturnsUriWithPort() { $loop = Factory::create(); diff --git a/tests/ServerTest.php b/tests/ServerTest.php index aa4b8f89..bd4f3a7b 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -267,6 +267,32 @@ public function testRequestNonConnectWithAuthorityRequestTargetWillReject() $this->connection->emit('data', array($data)); } + public function testRequestWithoutHostEventUsesSocketAddress() + { + $requestAssertion = null; + + $server = new Server($this->socket, function (ServerRequestInterface $request) use (&$requestAssertion) { + $requestAssertion = $request; + return new Response(); + }); + + $this->socket->emit('connection', array($this->connection)); + + $this->connection + ->expects($this->once()) + ->method('getLocalAddress') + ->willReturn('127.0.0.1:80'); + + $data = "GET /test HTTP/1.0\r\n\r\n"; + $this->connection->emit('data', array($data)); + + $this->assertInstanceOf('RingCentral\Psr7\Request', $requestAssertion); + $this->assertSame('GET', $requestAssertion->getMethod()); + $this->assertSame('/test', $requestAssertion->getRequestTarget()); + $this->assertEquals('http://127.0.0.1/test', $requestAssertion->getUri()); + $this->assertSame('/test', $requestAssertion->getUri()->getPath()); + } + public function testRequestAbsoluteEvent() { $requestAssertion = null; From 7d42e92a8a7cdb01302a6ab458dbf425912b2410 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 21 Apr 2017 09:19:42 +0200 Subject: [PATCH 131/456] Simplify request validation by moving logic to RequestHeaderParser --- src/RequestHeaderParser.php | 24 +++++++++++++++++++++- src/Server.php | 27 +------------------------ tests/RequestHeaderParserTest.php | 33 +++++++++++++++++++++++++++++++ tests/ServerTest.php | 11 +++++++++++ 4 files changed, 68 insertions(+), 27 deletions(-) diff --git a/src/RequestHeaderParser.php b/src/RequestHeaderParser.php index 97fa7a00..1566476a 100644 --- a/src/RequestHeaderParser.php +++ b/src/RequestHeaderParser.php @@ -30,7 +30,7 @@ public function feed($data) } if ($currentHeaderSize > $this->maxSize) { - $this->emit('error', array(new \OverflowException("Maximum header size of {$this->maxSize} exceeded."), $this)); + $this->emit('error', array(new \OverflowException("Maximum header size of {$this->maxSize} exceeded.", 431), $this)); $this->removeAllListeners(); return; } @@ -112,6 +112,28 @@ private function parseRequest($data) } } + // only support HTTP/1.1 and HTTP/1.0 requests + if ($request->getProtocolVersion() !== '1.1' && $request->getProtocolVersion() !== '1.0') { + throw new \InvalidArgumentException('Received request with invalid protocol version', 505); + } + + // HTTP/1.1 requests MUST include a valid host header (host and optional port) + // https://tools.ietf.org/html/rfc7230#section-5.4 + if ($request->getProtocolVersion() === '1.1') { + $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 + unset($parts['scheme'], $parts['host'], $parts['port']); + if ($parts === false || $parts) { + throw new \InvalidArgumentException('Invalid Host header for HTTP/1.1 request'); + } + } + return array($request, $bodyBuffer); } } diff --git a/src/Server.php b/src/Server.php index b47b7900..e1aa05a5 100644 --- a/src/Server.php +++ b/src/Server.php @@ -165,7 +165,7 @@ public function handleConnection(ConnectionInterface $conn) $that->writeError( $conn, - ($e instanceof \OverflowException) ? 431 : 400 + $e->getCode() !== 0 ? $e->getCode() : 400 ); }); } @@ -173,31 +173,6 @@ public function handleConnection(ConnectionInterface $conn) /** @internal */ public function handleRequest(ConnectionInterface $conn, ServerRequestInterface $request) { - // only support HTTP/1.1 and HTTP/1.0 requests - if ($request->getProtocolVersion() !== '1.1' && $request->getProtocolVersion() !== '1.0') { - $this->emit('error', array(new \InvalidArgumentException('Received request with invalid protocol version'))); - $request = $request->withProtocolVersion('1.1'); - return $this->writeError($conn, 505, $request); - } - - // HTTP/1.1 requests MUST include a valid host header (host and optional port) - // https://tools.ietf.org/html/rfc7230#section-5.4 - if ($request->getProtocolVersion() === '1.1') { - $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 - unset($parts['scheme'], $parts['host'], $parts['port']); - if ($parts === false || $parts) { - $this->emit('error', array(new \InvalidArgumentException('Invalid Host header for HTTP/1.1 request'))); - return $this->writeError($conn, 400, $request); - } - } - // set URI components from socket address if not already filled via Host header if ($request->getUri()->getHost() === '') { $parts = parse_url('tcp://' . $conn->getLocalAddress()); diff --git a/tests/RequestHeaderParserTest.php b/tests/RequestHeaderParserTest.php index f0422361..9f1f7d75 100644 --- a/tests/RequestHeaderParserTest.php +++ b/tests/RequestHeaderParserTest.php @@ -199,6 +199,39 @@ public function testInvalidAbsoluteFormWithFragmentEmitsError() $this->assertSame('Invalid absolute-form request-target', $error->getMessage()); } + public function testInvalidHostHeaderForHttp11() + { + $error = null; + + $parser = new RequestHeaderParser(); + $parser->on('headers', $this->expectCallableNever()); + $parser->on('error', function ($message) use (&$error) { + $error = $message; + }); + + $parser->feed("GET / HTTP/1.1\r\nHost: a/b/c\r\n\r\n"); + + $this->assertInstanceOf('InvalidArgumentException', $error); + $this->assertSame('Invalid Host header for HTTP/1.1 request', $error->getMessage()); + } + + public function testInvalidHttpVersion() + { + $error = null; + + $parser = new RequestHeaderParser(); + $parser->on('headers', $this->expectCallableNever()); + $parser->on('error', function ($message) use (&$error) { + $error = $message; + }); + + $parser->feed("GET / HTTP/1.2\r\n\r\n"); + + $this->assertInstanceOf('InvalidArgumentException', $error); + $this->assertSame(505, $error->getCode()); + $this->assertSame('Received request with invalid protocol version', $error->getMessage()); + } + private function createGetRequest() { $data = "GET / HTTP/1.1\r\n"; diff --git a/tests/ServerTest.php b/tests/ServerTest.php index bd4f3a7b..af53cc02 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -256,6 +256,17 @@ public function testRequestConnectAuthorityFormNonMatchingHostWillBePassedAsIs() $this->assertSame('example.com', $requestAssertion->getHeaderLine('host')); } + public function testRequestConnectOriginFormRequestTargetWillReject() + { + $server = new Server($this->socket, $this->expectCallableNever()); + $server->on('error', $this->expectCallableOnce()); + + $this->socket->emit('connection', array($this->connection)); + + $data = "CONNECT / HTTP/1.1\r\nHost: example.com\r\n\r\n"; + $this->connection->emit('data', array($data)); + } + public function testRequestNonConnectWithAuthorityRequestTargetWillReject() { $server = new Server($this->socket, $this->expectCallableNever()); From e6b12d6a0043f453af8c3976467f9c4c855927ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 21 Apr 2017 12:57:46 +0200 Subject: [PATCH 132/456] Sanitize Host header value across all requests --- README.md | 5 ++- src/RequestHeaderParser.php | 22 +++++++--- src/Server.php | 33 ++++++++------- tests/RequestHeaderParserTest.php | 22 ++++++++-- tests/ServerTest.php | 67 +++++-------------------------- 5 files changed, 68 insertions(+), 81 deletions(-) diff --git a/README.md b/README.md index 988baec7..d8155817 100644 --- a/README.md +++ b/README.md @@ -292,6 +292,9 @@ client did not specify one (i.e. no `Host` header). Its `getScheme(): string` method will return `http` or `https` depending on whether the request was made over a secure TLS connection to the target host. +The `Host` header value will be sanitized to match this host component plus the +port component only if it is non-standard for this URI scheme. + You can use `getMethod(): string` and `getRequestTarget(): string` to check this is an accepted request and may want to reject other requests with an appropriate error code, such as `400` (Bad Request) or `405` (Method Not @@ -300,7 +303,7 @@ Allowed). > The `CONNECT` method is useful in a tunneling setup (HTTPS proxy) and not something most HTTP servers would want to care about. Note that if you want to handle this method, the client MAY send a different - request-target than the `Host` header field (such as removing default ports) + request-target than the `Host` header value (such as removing default ports) and the request-target MUST take precendence when forwarding. The HTTP specs define an opaque "tunneling mode" for this method and make no use of the message body. diff --git a/src/RequestHeaderParser.php b/src/RequestHeaderParser.php index 1566476a..91c8d2cd 100644 --- a/src/RequestHeaderParser.php +++ b/src/RequestHeaderParser.php @@ -55,6 +55,8 @@ private function parseRequest($data) { list($headers, $bodyBuffer) = explode("\r\n\r\n", $data, 2); + // parser does not support asterisk-form and authority-form + // remember original target and temporarily replace and re-apply below $originalTarget = null; if (strpos($headers, 'OPTIONS * ') === 0) { $originalTarget = '*'; @@ -75,7 +77,7 @@ private function parseRequest($data) $request = g7\parse_request($headers); // create new obj implementing ServerRequestInterface by preserving all - // previous properties and restoring original request target-target + // previous properties and restoring original request-target $target = $request->getRequestTarget(); $request = new ServerRequest( $request->getMethod(), @@ -95,9 +97,18 @@ private function parseRequest($data) ); } + // re-apply actual request target from above if ($originalTarget !== null) { + $uri = $request->getUri()->withPath(''); + + // re-apply host and port from request-target if given + $parts = parse_url('tcp://' . $originalTarget); + if (isset($parts['host'], $parts['port'])) { + $uri = $uri->withHost($parts['host'])->withPort($parts['port']); + } + $request = $request->withUri( - $request->getUri()->withPath(''), + $uri, true )->withRequestTarget($originalTarget); } @@ -117,9 +128,8 @@ private function parseRequest($data) throw new \InvalidArgumentException('Received request with invalid protocol version', 505); } - // HTTP/1.1 requests MUST include a valid host header (host and optional port) - // https://tools.ietf.org/html/rfc7230#section-5.4 - if ($request->getProtocolVersion() === '1.1') { + // Optional Host header value MUST be valid (host and optional port) + if ($request->hasHeader('Host')) { $parts = parse_url('http://' . $request->getHeaderLine('Host')); // make sure value contains valid host component (IP or hostname) @@ -130,7 +140,7 @@ private function parseRequest($data) // make sure value does not contain any other URI component unset($parts['scheme'], $parts['host'], $parts['port']); if ($parts === false || $parts) { - throw new \InvalidArgumentException('Invalid Host header for HTTP/1.1 request'); + throw new \InvalidArgumentException('Invalid Host header value'); } } diff --git a/src/Server.php b/src/Server.php index e1aa05a5..c0212c7c 100644 --- a/src/Server.php +++ b/src/Server.php @@ -183,6 +183,24 @@ public function handleRequest(ConnectionInterface $conn, ServerRequestInterface ); } + // Update request URI to "https" scheme if the connection is encrypted + if ($this->isConnectionEncrypted($conn)) { + // The request URI may omit default ports here, so try to parse port + // from Host header field (if possible) + $port = $request->getUri()->getPort(); + if ($port === null) { + $port = parse_url('tcp://' . $request->getHeaderLine('Host'), PHP_URL_PORT); // @codeCoverageIgnore + } + + $request = $request->withUri( + $request->getUri()->withScheme('https')->withPort($port), + true + ); + } + + // always sanitize Host header because it contains critical routing information + $request = $request->withHeader('Host', $request->getUri()->withUserInfo('')->getAuthority()); + $contentLength = 0; $stream = new CloseProtectionStream($conn); if ($request->getMethod() === 'CONNECT') { @@ -240,21 +258,6 @@ public function handleRequest(ConnectionInterface $conn, ServerRequestInterface '[]' ); - // Update request URI to "https" scheme if the connection is encrypted - if ($this->isConnectionEncrypted($conn)) { - // The request URI may omit default ports here, so try to parse port - // from Host header field (if possible) - $port = $request->getUri()->getPort(); - if ($port === null) { - $port = parse_url('tcp://' . $request->getHeaderLine('Host'), PHP_URL_PORT); - } - - $request = $request->withUri( - $request->getUri()->withScheme('https')->withPort($port), - true - ); - } - $callback = $this->callback; $promise = new Promise(function ($resolve, $reject) use ($callback, $request) { $resolve($callback($request)); diff --git a/tests/RequestHeaderParserTest.php b/tests/RequestHeaderParserTest.php index 9f1f7d75..9ecefddf 100644 --- a/tests/RequestHeaderParserTest.php +++ b/tests/RequestHeaderParserTest.php @@ -199,7 +199,7 @@ public function testInvalidAbsoluteFormWithFragmentEmitsError() $this->assertSame('Invalid absolute-form request-target', $error->getMessage()); } - public function testInvalidHostHeaderForHttp11() + public function testInvalidHeaderContainsFullUri() { $error = null; @@ -209,10 +209,26 @@ public function testInvalidHostHeaderForHttp11() $error = $message; }); - $parser->feed("GET / HTTP/1.1\r\nHost: a/b/c\r\n\r\n"); + $parser->feed("GET / HTTP/1.1\r\nHost: http://user:pass@host/\r\n\r\n"); $this->assertInstanceOf('InvalidArgumentException', $error); - $this->assertSame('Invalid Host header for HTTP/1.1 request', $error->getMessage()); + $this->assertSame('Invalid Host header value', $error->getMessage()); + } + + public function testInvalidAbsoluteFormWithHostHeaderEmpty() + { + $error = null; + + $parser = new RequestHeaderParser(); + $parser->on('headers', $this->expectCallableNever()); + $parser->on('error', function ($message) use (&$error) { + $error = $message; + }); + + $parser->feed("GET http://example.com/ HTTP/1.1\r\nHost: \r\n\r\n"); + + $this->assertInstanceOf('InvalidArgumentException', $error); + $this->assertSame('Invalid Host header value', $error->getMessage()); } public function testInvalidHttpVersion() diff --git a/tests/ServerTest.php b/tests/ServerTest.php index af53cc02..ee435dc7 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -88,7 +88,7 @@ public function testRequestEvent() $this->assertSame('/', $requestAssertion->getRequestTarget()); $this->assertSame('/', $requestAssertion->getUri()->getPath()); $this->assertSame('http://example.com/', (string)$requestAssertion->getUri()); - $this->assertSame('example.com:80', $requestAssertion->getHeaderLine('Host')); + $this->assertSame('example.com', $requestAssertion->getHeaderLine('Host')); $this->assertSame('127.0.0.1', $requestAssertion->remoteAddress); } @@ -155,7 +155,7 @@ public function testRequestGetWithHostAndDefaultPortWillBeIgnored() $this->assertSame('/', $requestAssertion->getUri()->getPath()); $this->assertSame('http://example.com/', (string)$requestAssertion->getUri()); $this->assertSame(null, $requestAssertion->getUri()->getPort()); - $this->assertSame('example.com:80', $requestAssertion->getHeaderLine('Host')); + $this->assertSame('example.com', $requestAssertion->getHeaderLine('Host')); } public function testRequestOptionsAsterisk() @@ -209,7 +209,7 @@ public function testRequestConnectAuthorityForm() $this->assertSame('', $requestAssertion->getUri()->getPath()); $this->assertSame('http://example.com:443', (string)$requestAssertion->getUri()); $this->assertSame(443, $requestAssertion->getUri()->getPort()); - $this->assertSame('example.com:443', $requestAssertion->getHeaderLine('host')); + $this->assertSame('example.com:443', $requestAssertion->getHeaderLine('Host')); } public function testRequestConnectAuthorityFormWithDefaultPortWillBeIgnored() @@ -231,10 +231,10 @@ public function testRequestConnectAuthorityFormWithDefaultPortWillBeIgnored() $this->assertSame('', $requestAssertion->getUri()->getPath()); $this->assertSame('http://example.com', (string)$requestAssertion->getUri()); $this->assertSame(null, $requestAssertion->getUri()->getPort()); - $this->assertSame('example.com:80', $requestAssertion->getHeaderLine('Host')); + $this->assertSame('example.com', $requestAssertion->getHeaderLine('Host')); } - public function testRequestConnectAuthorityFormNonMatchingHostWillBePassedAsIs() + public function testRequestConnectAuthorityFormNonMatchingHostWillBeOverwritten() { $requestAssertion = null; $server = new Server($this->socket, function (ServerRequestInterface $request) use (&$requestAssertion) { @@ -244,7 +244,7 @@ public function testRequestConnectAuthorityFormNonMatchingHostWillBePassedAsIs() $this->socket->emit('connection', array($this->connection)); - $data = "CONNECT example.com:80 HTTP/1.1\r\nHost: example.com\r\n\r\n"; + $data = "CONNECT example.com:80 HTTP/1.1\r\nHost: other.example.org\r\n\r\n"; $this->connection->emit('data', array($data)); $this->assertInstanceOf('RingCentral\Psr7\Request', $requestAssertion); @@ -253,7 +253,7 @@ public function testRequestConnectAuthorityFormNonMatchingHostWillBePassedAsIs() $this->assertSame('', $requestAssertion->getUri()->getPath()); $this->assertSame('http://example.com', (string)$requestAssertion->getUri()); $this->assertSame(null, $requestAssertion->getUri()->getPort()); - $this->assertSame('example.com', $requestAssertion->getHeaderLine('host')); + $this->assertSame('example.com', $requestAssertion->getHeaderLine('Host')); } public function testRequestConnectOriginFormRequestTargetWillReject() @@ -349,7 +349,7 @@ public function testRequestAbsoluteAddsMissingHostEvent() $this->assertSame('example.com:8080', $requestAssertion->getHeaderLine('Host')); } - public function testRequestAbsoluteNonMatchingHostWillBePassedAsIs() + public function testRequestAbsoluteNonMatchingHostWillBeOverwritten() { $requestAssertion = null; @@ -368,7 +368,7 @@ public function testRequestAbsoluteNonMatchingHostWillBePassedAsIs() $this->assertSame('http://example.com/test', $requestAssertion->getRequestTarget()); $this->assertEquals('http://example.com/test', $requestAssertion->getUri()); $this->assertSame('/test', $requestAssertion->getUri()->getPath()); - $this->assertSame('other.example.org', $requestAssertion->getHeaderLine('Host')); + $this->assertSame('example.com', $requestAssertion->getHeaderLine('Host')); } public function testRequestOptionsAsteriskEvent() @@ -1002,39 +1002,7 @@ public function testChunkedIsMixedUpperAndLowerCase() $this->connection->emit('data', array($data)); } - public function testRequestHttp11WithoutHostWillEmitErrorAndSendErrorResponse() - { - $error = null; - $server = new Server($this->socket, $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; - } - ) - ); - - $this->socket->emit('connection', array($this->connection)); - - $data = "GET / HTTP/1.1\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 testRequestHttp11WithMalformedHostWillEmitErrorAndSendErrorResponse() + public function testRequestWithMalformedHostWillEmitErrorAndSendErrorResponse() { $error = null; $server = new Server($this->socket, $this->expectCallableNever()); @@ -1066,7 +1034,7 @@ function ($data) use (&$buffer) { $this->assertContains("\r\n\r\nError 400: Bad Request", $buffer); } - public function testRequestHttp11WithInvalidHostUriComponentsWillEmitErrorAndSendErrorResponse() + public function testRequestWithInvalidHostUriComponentsWillEmitErrorAndSendErrorResponse() { $error = null; $server = new Server($this->socket, $this->expectCallableNever()); @@ -1098,19 +1066,6 @@ function ($data) use (&$buffer) { $this->assertContains("\r\n\r\nError 400: Bad Request", $buffer); } - public function testRequestHttp10WithoutHostEmitsRequestWithNoError() - { - $server = new Server($this->socket, function (ServerRequestInterface $request) { - return \React\Promise\resolve(new Response()); - }); - $server->on('error', $this->expectCallableNever()); - - $this->socket->emit('connection', array($this->connection)); - - $data = "GET / HTTP/1.0\r\n\r\n"; - $this->connection->emit('data', array($data)); - } - public function testWontEmitFurtherDataWhenContentLengthIsReached() { $dataEvent = $this->expectCallableOnceWith('hello'); From a60e20265473ca4492e68c5ad4fe97c2e6258227 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 22 Apr 2017 00:41:16 +0200 Subject: [PATCH 133/456] Move complete URI handling to RequestHeaderParser --- src/RequestHeaderParser.php | 64 ++++++++++++++++++++++++------- src/Server.php | 33 ++-------------- tests/RequestHeaderParserTest.php | 36 +++++++++++++++-- tests/ServerTest.php | 4 +- 4 files changed, 89 insertions(+), 48 deletions(-) diff --git a/src/RequestHeaderParser.php b/src/RequestHeaderParser.php index 91c8d2cd..cb25dcab 100644 --- a/src/RequestHeaderParser.php +++ b/src/RequestHeaderParser.php @@ -17,6 +17,13 @@ class RequestHeaderParser extends EventEmitter private $buffer = ''; private $maxSize = 4096; + private $uri; + + public function __construct($localSocketUri = '') + { + $this->uri = $localSocketUri; + } + public function feed($data) { $this->buffer .= $data; @@ -88,15 +95,6 @@ private function parseRequest($data) ); $request = $request->withRequestTarget($target); - // Do not assume this is HTTPS when this happens to be port 443 - // detecting HTTPS is left up to the socket layer (TLS detection) - if ($request->getUri()->getScheme() === 'https') { - $request = $request->withUri( - $request->getUri()->withScheme('http')->withPort(443), - true - ); - } - // re-apply actual request target from above if ($originalTarget !== null) { $uri = $request->getUri()->withPath(''); @@ -113,6 +111,11 @@ private function parseRequest($data) )->withRequestTarget($originalTarget); } + // only support HTTP/1.1 and HTTP/1.0 requests + if ($request->getProtocolVersion() !== '1.1' && $request->getProtocolVersion() !== '1.0') { + throw new \InvalidArgumentException('Received request with invalid protocol version', 505); + } + // ensure absolute-form request-target contains a valid URI if (strpos($request->getRequestTarget(), '://') !== false) { $parts = parse_url($request->getRequestTarget()); @@ -123,11 +126,6 @@ private function parseRequest($data) } } - // only support HTTP/1.1 and HTTP/1.0 requests - if ($request->getProtocolVersion() !== '1.1' && $request->getProtocolVersion() !== '1.0') { - throw new \InvalidArgumentException('Received request with invalid protocol version', 505); - } - // Optional Host header value MUST be valid (host and optional port) if ($request->hasHeader('Host')) { $parts = parse_url('http://' . $request->getHeaderLine('Host')); @@ -144,6 +142,44 @@ private function parseRequest($data) } } + // set URI components from socket address if not already filled via Host header + if ($request->getUri()->getHost() === '') { + $parts = parse_url($this->uri); + + $request = $request->withUri( + $request->getUri()->withScheme('http')->withHost($parts['host'])->withPort($parts['port']), + true + ); + } + + // Do not assume this is HTTPS when this happens to be port 443 + // detecting HTTPS is left up to the socket layer (TLS detection) + if ($request->getUri()->getScheme() === 'https') { + $request = $request->withUri( + $request->getUri()->withScheme('http')->withPort(443), + true + ); + } + + // Update request URI to "https" scheme if the connection is encrypted + $parts = parse_url($this->uri); + if (isset($parts['scheme']) && $parts['scheme'] === 'https') { + // The request URI may omit default ports here, so try to parse port + // from Host header field (if possible) + $port = $request->getUri()->getPort(); + if ($port === null) { + $port = parse_url('tcp://' . $request->getHeaderLine('Host'), PHP_URL_PORT); // @codeCoverageIgnore + } + + $request = $request->withUri( + $request->getUri()->withScheme('https')->withPort($port), + true + ); + } + + // always sanitize Host header because it contains critical routing information + $request = $request->withUri($request->getUri()->withUserInfo('u')->withUserInfo('')); + return array($request, $bodyBuffer); } } diff --git a/src/Server.php b/src/Server.php index c0212c7c..fa5c91fe 100644 --- a/src/Server.php +++ b/src/Server.php @@ -145,7 +145,10 @@ public function __construct(SocketServerInterface $io, $callback) public function handleConnection(ConnectionInterface $conn) { $that = $this; - $parser = new RequestHeaderParser(); + $parser = new RequestHeaderParser( + ($this->isConnectionEncrypted($conn) ? 'https://' : 'http://') . $conn->getLocalAddress() + ); + $listener = array($parser, 'feed'); $parser->on('headers', function (RequestInterface $request, $bodyBuffer) use ($conn, $listener, $parser, $that) { // parsing request completed => stop feeding parser @@ -173,34 +176,6 @@ public function handleConnection(ConnectionInterface $conn) /** @internal */ public function handleRequest(ConnectionInterface $conn, ServerRequestInterface $request) { - // set URI components from socket address if not already filled via Host header - if ($request->getUri()->getHost() === '') { - $parts = parse_url('tcp://' . $conn->getLocalAddress()); - - $request = $request->withUri( - $request->getUri()->withScheme('http')->withHost($parts['host'])->withPort($parts['port']), - true - ); - } - - // Update request URI to "https" scheme if the connection is encrypted - if ($this->isConnectionEncrypted($conn)) { - // The request URI may omit default ports here, so try to parse port - // from Host header field (if possible) - $port = $request->getUri()->getPort(); - if ($port === null) { - $port = parse_url('tcp://' . $request->getHeaderLine('Host'), PHP_URL_PORT); // @codeCoverageIgnore - } - - $request = $request->withUri( - $request->getUri()->withScheme('https')->withPort($port), - true - ); - } - - // always sanitize Host header because it contains critical routing information - $request = $request->withHeader('Host', $request->getUri()->withUserInfo('')->getAuthority()); - $contentLength = 0; $stream = new CloseProtectionStream($conn); if ($request->getMethod() === 'CONNECT') { diff --git a/tests/RequestHeaderParserTest.php b/tests/RequestHeaderParserTest.php index 9ecefddf..be725294 100644 --- a/tests/RequestHeaderParserTest.php +++ b/tests/RequestHeaderParserTest.php @@ -49,7 +49,7 @@ public function testHeadersEventShouldReturnRequestAndBodyBuffer() $this->assertSame('GET', $request->getMethod()); $this->assertEquals('http://example.com/', $request->getUri()); $this->assertSame('1.1', $request->getProtocolVersion()); - $this->assertSame(array('Host' => array('example.com:80'), 'Connection' => array('close')), $request->getHeaders()); + $this->assertSame(array('Host' => array('example.com'), 'Connection' => array('close')), $request->getHeaders()); $this->assertSame('RANDOM DATA', $bodyBuffer); } @@ -87,13 +87,43 @@ public function testHeadersEventShouldParsePathAndQueryString() $this->assertEquals('http://example.com/foo?bar=baz', $request->getUri()); $this->assertSame('1.1', $request->getProtocolVersion()); $headers = array( - 'Host' => array('example.com:80'), + 'Host' => array('example.com'), 'User-Agent' => array('react/alpha'), 'Connection' => array('close'), ); $this->assertSame($headers, $request->getHeaders()); } + public function testHeaderEventWithShouldApplyDefaultAddressFromConstructor() + { + $request = null; + + $parser = new RequestHeaderParser('http://127.1.1.1:8000'); + $parser->on('headers', function ($parsedRequest) use (&$request) { + $request = $parsedRequest; + }); + + $parser->feed("GET /foo HTTP/1.0\r\n\r\n"); + + $this->assertEquals('http://127.1.1.1:8000/foo', $request->getUri()); + $this->assertEquals('127.1.1.1:8000', $request->getHeaderLine('Host')); + } + + public function testHeaderEventViaHttpsShouldApplySchemeFromConstructor() + { + $request = null; + + $parser = new RequestHeaderParser('https://127.1.1.1:8000'); + $parser->on('headers', function ($parsedRequest) use (&$request) { + $request = $parsedRequest; + }); + + $parser->feed("GET /foo HTTP/1.0\r\nHost: example.com\r\n\r\n"); + + $this->assertEquals('https://example.com/foo', $request->getUri()); + $this->assertEquals('example.com', $request->getHeaderLine('Host')); + } + public function testHeaderOverflowShouldEmitError() { $error = null; @@ -137,7 +167,7 @@ public function testHeaderOverflowShouldNotEmitErrorWhenDataExceedsMaxHeaderSize $parser->feed($data); $headers = array( - 'Host' => array('example.com:80'), + 'Host' => array('example.com'), 'User-Agent' => array('react/alpha'), 'Connection' => array('close'), ); diff --git a/tests/ServerTest.php b/tests/ServerTest.php index ee435dc7..25c01810 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -287,13 +287,13 @@ public function testRequestWithoutHostEventUsesSocketAddress() return new Response(); }); - $this->socket->emit('connection', array($this->connection)); - $this->connection ->expects($this->once()) ->method('getLocalAddress') ->willReturn('127.0.0.1:80'); + $this->socket->emit('connection', array($this->connection)); + $data = "GET /test HTTP/1.0\r\n\r\n"; $this->connection->emit('data', array($data)); From b34b61b8ae5c732b9a3d53a88a4b5406c31345c1 Mon Sep 17 00:00:00 2001 From: Niels Theen Date: Fri, 21 Apr 2017 14:34:39 +0200 Subject: [PATCH 134/456] Add server-side parameters to implementation --- src/RequestHeaderParser.php | 35 +++++++++++--- src/Server.php | 9 +--- src/ServerRequest.php | 22 +++++++++ tests/RequestHeaderParserTest.php | 77 +++++++++++++++++++++++++++++++ tests/ServerRequestTest.php | 21 +++++++++ tests/ServerTest.php | 45 ++++++++++++++++-- 6 files changed, 192 insertions(+), 17 deletions(-) diff --git a/src/RequestHeaderParser.php b/src/RequestHeaderParser.php index cb25dcab..cb6537e2 100644 --- a/src/RequestHeaderParser.php +++ b/src/RequestHeaderParser.php @@ -17,11 +17,13 @@ class RequestHeaderParser extends EventEmitter private $buffer = ''; private $maxSize = 4096; - private $uri; + private $localSocketUri; + private $remoteSocketUri; - public function __construct($localSocketUri = '') + public function __construct($localSocketUri = null, $remoteSocketUri = null) { - $this->uri = $localSocketUri; + $this->localSocketUri = $localSocketUri; + $this->remoteSocketUri = $remoteSocketUri; } public function feed($data) @@ -85,13 +87,34 @@ private function parseRequest($data) // create new obj implementing ServerRequestInterface by preserving all // previous properties and restoring original request-target + $serverParams = array( + 'REQUEST_TIME' => time(), + 'REQUEST_TIME_FLOAT' => microtime(true) + ); + + if ($this->remoteSocketUri !== null) { + $remoteAddress = parse_url($this->remoteSocketUri); + $serverParams['REMOTE_ADDR'] = $remoteAddress['host']; + $serverParams['REMOTE_PORT'] = $remoteAddress['port']; + } + + if ($this->localSocketUri !== null) { + $localAddress = parse_url($this->localSocketUri); + $serverParams['SERVER_ADDR'] = $localAddress['host']; + $serverParams['SERVER_PORT'] = $localAddress['port']; + if (isset($localAddress['scheme']) && $localAddress['scheme'] === 'https') { + $serverParams['HTTPS'] = 'on'; + } + } + $target = $request->getRequestTarget(); $request = new ServerRequest( $request->getMethod(), $request->getUri(), $request->getHeaders(), $request->getBody(), - $request->getProtocolVersion() + $request->getProtocolVersion(), + $serverParams ); $request = $request->withRequestTarget($target); @@ -144,7 +167,7 @@ private function parseRequest($data) // set URI components from socket address if not already filled via Host header if ($request->getUri()->getHost() === '') { - $parts = parse_url($this->uri); + $parts = parse_url($this->localSocketUri); $request = $request->withUri( $request->getUri()->withScheme('http')->withHost($parts['host'])->withPort($parts['port']), @@ -162,7 +185,7 @@ private function parseRequest($data) } // Update request URI to "https" scheme if the connection is encrypted - $parts = parse_url($this->uri); + $parts = parse_url($this->localSocketUri); if (isset($parts['scheme']) && $parts['scheme'] === 'https') { // The request URI may omit default ports here, so try to parse port // from Host header field (if possible) diff --git a/src/Server.php b/src/Server.php index fa5c91fe..51ad186b 100644 --- a/src/Server.php +++ b/src/Server.php @@ -146,7 +146,8 @@ public function handleConnection(ConnectionInterface $conn) { $that = $this; $parser = new RequestHeaderParser( - ($this->isConnectionEncrypted($conn) ? 'https://' : 'http://') . $conn->getLocalAddress() + ($this->isConnectionEncrypted($conn) ? 'https://' : 'http://') . $conn->getLocalAddress(), + 'tcp://' . $conn->getRemoteAddress() ); $listener = array($parser, 'feed'); @@ -227,12 +228,6 @@ public function handleRequest(ConnectionInterface $conn, ServerRequestInterface $conn->write("HTTP/1.1 100 Continue\r\n\r\n"); } - // attach remote ip to the request as metadata - $request->remoteAddress = trim( - parse_url('tcp://' . $conn->getRemoteAddress(), PHP_URL_HOST), - '[]' - ); - $callback = $this->callback; $promise = new Promise(function ($resolve, $reject) use ($callback, $request) { $resolve($callback($request)); diff --git a/src/ServerRequest.php b/src/ServerRequest.php index 44826f6c..ea36a5a9 100644 --- a/src/ServerRequest.php +++ b/src/ServerRequest.php @@ -16,6 +16,28 @@ class ServerRequest extends Request implements ServerRequestInterface private $queryParams = array(); private $parsedBody = null; + /** + * @param null|string $method HTTP method for the request. + * @param null|string|UriInterface $uri URI for the request. + * @param array $headers Headers for the message. + * @param string|resource|StreamInterface $body Message body. + * @param string $protocolVersion HTTP protocol version. + * @param array server-side parameters + * + * @throws InvalidArgumentException for an invalid URI + */ + public function __construct( + $method, + $uri, + array $headers = array(), + $body = null, + $protocolVersion = '1.1', + $serverParams = array() + ) { + $this->serverParams = $serverParams; + parent::__construct($method, $uri, $headers, $body, $protocolVersion); + } + public function getServerParams() { return $this->serverParams; diff --git a/tests/RequestHeaderParserTest.php b/tests/RequestHeaderParserTest.php index be725294..957bb6ac 100644 --- a/tests/RequestHeaderParserTest.php +++ b/tests/RequestHeaderParserTest.php @@ -278,6 +278,83 @@ public function testInvalidHttpVersion() $this->assertSame('Received request with invalid protocol version', $error->getMessage()); } + public function testServerParamsWillBeSetOnHttpsRequest() + { + $request = null; + + $parser = new RequestHeaderParser( + 'https://127.1.1.1:8000', + 'https://192.168.1.1:8001' + ); + + $parser->on('headers', function ($parsedRequest) use (&$request) { + $request = $parsedRequest; + }); + + $parser->feed("GET /foo HTTP/1.0\r\nHost: example.com\r\n\r\n"); + $serverParams = $request->getServerParams(); + + $this->assertEquals('on', $serverParams['HTTPS']); + $this->assertNotEmpty($serverParams['REQUEST_TIME']); + $this->assertNotEmpty($serverParams['REQUEST_TIME_FLOAT']); + + $this->assertEquals('127.1.1.1', $serverParams['SERVER_ADDR']); + $this->assertEquals('8000', $serverParams['SERVER_PORT']); + + $this->assertEquals('192.168.1.1', $serverParams['REMOTE_ADDR']); + $this->assertEquals('8001', $serverParams['REMOTE_PORT']); + } + + public function testServerParamsWillBeSetOnHttpRequest() + { + $request = null; + + $parser = new RequestHeaderParser( + 'http://127.1.1.1:8000', + 'http://192.168.1.1:8001' + ); + + $parser->on('headers', function ($parsedRequest) use (&$request) { + $request = $parsedRequest; + }); + + $parser->feed("GET /foo HTTP/1.0\r\nHost: example.com\r\n\r\n"); + $serverParams = $request->getServerParams(); + + $this->assertArrayNotHasKey('HTTPS', $serverParams); + $this->assertNotEmpty($serverParams['REQUEST_TIME']); + $this->assertNotEmpty($serverParams['REQUEST_TIME_FLOAT']); + + $this->assertEquals('127.1.1.1', $serverParams['SERVER_ADDR']); + $this->assertEquals('8000', $serverParams['SERVER_PORT']); + + $this->assertEquals('192.168.1.1', $serverParams['REMOTE_ADDR']); + $this->assertEquals('8001', $serverParams['REMOTE_PORT']); + } + + public function testServerParamsWontBeSetOnMissingUrls() + { + $request = null; + + $parser = new RequestHeaderParser(); + + $parser->on('headers', function ($parsedRequest) use (&$request) { + $request = $parsedRequest; + }); + + $parser->feed("GET /foo HTTP/1.0\r\nHost: example.com\r\n\r\n"); + $serverParams = $request->getServerParams(); + + $this->assertNotEmpty($serverParams['REQUEST_TIME']); + $this->assertNotEmpty($serverParams['REQUEST_TIME_FLOAT']); + + $this->assertArrayNotHasKey('SERVER_ADDR', $serverParams); + $this->assertArrayNotHasKey('SERVER_PORT', $serverParams); + + $this->assertArrayNotHasKey('REMOTE_ADDR', $serverParams); + $this->assertArrayNotHasKey('REMOTE_PORT', $serverParams); + } + private function createGetRequest() { $data = "GET / HTTP/1.1\r\n"; diff --git a/tests/ServerRequestTest.php b/tests/ServerRequestTest.php index fc873439..14edf3a8 100644 --- a/tests/ServerRequestTest.php +++ b/tests/ServerRequestTest.php @@ -84,4 +84,25 @@ public function testWithParsedBody() $this->assertNotSame($request, $this->request); $this->assertEquals(array('test' => 'world'), $request->getParsedBody()); } + + public function testServerRequestParameter() + { + $body = 'hello=world'; + $request = new ServerRequest( + 'POST', + 'http://127.0.0.1', + array('Content-Length' => strlen($body)), + $body, + '1.0', + array('SERVER_ADDR' => '127.0.0.1') + ); + + $serverParams = $request->getServerParams(); + $this->assertEquals('POST', $request->getMethod()); + $this->assertEquals('http://127.0.0.1', $request->getUri()); + $this->assertEquals('11', $request->getHeaderLine('Content-Length')); + $this->assertEquals('hello=world', $request->getBody()); + $this->assertEquals('1.0', $request->getProtocolVersion()); + $this->assertEquals('127.0.0.1', $serverParams['SERVER_ADDR']); + } } diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 25c01810..3cd88bf8 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -69,19 +69,22 @@ public function testRequestEvent() $server = new Server($this->socket, function (ServerRequestInterface $request) use (&$i, &$requestAssertion) { $i++; $requestAssertion = $request; + return \React\Promise\resolve(new Response()); }); $this->connection - ->expects($this->once()) + ->expects($this->any()) ->method('getRemoteAddress') - ->willReturn('127.0.0.1'); + ->willReturn('127.0.0.1:8080'); $this->socket->emit('connection', array($this->connection)); $data = $this->createGetRequest(); $this->connection->emit('data', array($data)); + $serverParams = $requestAssertion->getServerParams(); + $this->assertSame(1, $i); $this->assertInstanceOf('RingCentral\Psr7\Request', $requestAssertion); $this->assertSame('GET', $requestAssertion->getMethod()); @@ -89,7 +92,7 @@ public function testRequestEvent() $this->assertSame('/', $requestAssertion->getUri()->getPath()); $this->assertSame('http://example.com/', (string)$requestAssertion->getUri()); $this->assertSame('example.com', $requestAssertion->getHeaderLine('Host')); - $this->assertSame('127.0.0.1', $requestAssertion->remoteAddress); + $this->assertSame('127.0.0.1', $serverParams['REMOTE_ADDR']); } public function testRequestGetWithHostAndCustomPort() @@ -288,7 +291,7 @@ public function testRequestWithoutHostEventUsesSocketAddress() }); $this->connection - ->expects($this->once()) + ->expects($this->any()) ->method('getLocalAddress') ->willReturn('127.0.0.1:80'); @@ -2332,6 +2335,40 @@ function ($data) use (&$buffer) { $this->assertInstanceOf('RuntimeException', $exception); } + public function testServerRequestParams() + { + $requestValidation = null; + $server = new Server($this->socket, function (ServerRequestInterface $request) use (&$requestValidation) { + $requestValidation = $request; + return new Response(); + }); + + $this->connection + ->expects($this->any()) + ->method('getRemoteAddress') + ->willReturn('192.168.1.2:80'); + + $this->connection + ->expects($this->any()) + ->method('getLocalAddress') + ->willReturn('127.0.0.1:8080'); + + $this->socket->emit('connection', array($this->connection)); + + $data = $this->createGetRequest(); + + $this->connection->emit('data', array($data)); + + $serverParams = $requestValidation->getServerParams(); + + $this->assertEquals('127.0.0.1', $serverParams['SERVER_ADDR']); + $this->assertEquals('8080', $serverParams['SERVER_PORT']); + $this->assertEquals('192.168.1.2', $serverParams['REMOTE_ADDR']); + $this->assertEquals('80', $serverParams['REMOTE_PORT']); + $this->assertNotNull($serverParams['REQUEST_TIME']); + $this->assertNotNull($serverParams['REQUEST_TIME_FLOAT']); + } + private function createGetRequest() { $data = "GET / HTTP/1.1\r\n"; From 4acbfdb065af0e98c46492c0b1d68c859a126578 Mon Sep 17 00:00:00 2001 From: Niels Theen Date: Sun, 23 Apr 2017 02:19:49 +0200 Subject: [PATCH 135/456] Update docs and add example --- README.md | 37 ++++++++++++++++++- examples/02-client-ip.php | 25 +++++++++++++ ...unt-visitors.php => 03-count-visitors.php} | 0 ...am-response.php => 04-stream-response.php} | 0 ...ream-request.php => 05-stream-request.php} | 0 ...ror-handling.php => 06-error-handling.php} | 0 6 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 examples/02-client-ip.php rename examples/{02-count-visitors.php => 03-count-visitors.php} (100%) rename examples/{03-stream-response.php => 04-stream-response.php} (100%) rename examples/{04-stream-request.php => 05-stream-request.php} (100%) rename examples/{05-error-handling.php => 06-error-handling.php} (100%) diff --git a/README.md b/README.md index d8155817..d4d35e39 100644 --- a/README.md +++ b/README.md @@ -155,12 +155,47 @@ $http = new Server($socket, function (ServerRequestInterface $request) { }); ``` +The `getServerParams(): mixed[]` method can be used to +get server-side parameters similar to the `$_SERVER` variable. +The following parameters are currently available: + +* `REMOTE_ADDR` + The IP address of the request sender +* `REMOTE_PORT` + Port of the request sender +* `SERVER_ADDR` + The IP address of the server +* `SERVER_PORT` + The port of the server +* `REQUEST_TIME` + Unix timestamp when the complete request header has been received, + as integer similar to `time()` +* `REQUEST_TIME_FLOAT` + Unix timestamp when the complete request header has been received, + as float similar to `microtime(true)` +* `HTTPS` + Set to 'on' if the request used HTTPS, otherwise it won't be set + +```php +$http = new Server($socket, function (ServerRequestInterface $request) { + $body = "Your IP is: " . $request->getServerParams()['REMOTE_ADDR']; + + return new Response( + 200, + array('Content-Type' => 'text/plain'), + $body + ); +}); +``` + +See also [example #2](examples). + For more details about the request object, check out the documentation of [PSR-7 ServerRequestInterface](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-7-http-message.md#321-psrhttpmessageserverrequestinterface) and [PSR-7 RequestInterface](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-7-http-message.md#32-psrhttpmessagerequestinterface). -> Currently the the server params, cookies and uploaded files are not added by the +> Currently the cookies and uploaded files are not added by the `Server`, but you can add these parameters by yourself using the given methods. The next versions of this project will cover these features. diff --git a/examples/02-client-ip.php b/examples/02-client-ip.php new file mode 100644 index 00000000..31b7ad32 --- /dev/null +++ b/examples/02-client-ip.php @@ -0,0 +1,25 @@ +getServerParams()['REMOTE_ADDR']; + + return new Response( + 200, + array('Content-Type' => 'text/plain'), + $body + ); +}); + +echo 'Listening on http://' . $socket->getAddress() . PHP_EOL; + +$loop->run(); diff --git a/examples/02-count-visitors.php b/examples/03-count-visitors.php similarity index 100% rename from examples/02-count-visitors.php rename to examples/03-count-visitors.php diff --git a/examples/03-stream-response.php b/examples/04-stream-response.php similarity index 100% rename from examples/03-stream-response.php rename to examples/04-stream-response.php diff --git a/examples/04-stream-request.php b/examples/05-stream-request.php similarity index 100% rename from examples/04-stream-request.php rename to examples/05-stream-request.php diff --git a/examples/05-error-handling.php b/examples/06-error-handling.php similarity index 100% rename from examples/05-error-handling.php rename to examples/06-error-handling.php From be14150a5f97e6bffeda438620e0647b2a4769c7 Mon Sep 17 00:00:00 2001 From: Niels Theen Date: Wed, 26 Apr 2017 14:46:41 +0200 Subject: [PATCH 136/456] Add query parameters to ServerRequest --- src/RequestHeaderParser.php | 8 ++++++++ tests/RequestHeaderParserTest.php | 17 +++++++++++++++++ tests/ServerTest.php | 21 +++++++++++++++++++++ 3 files changed, 46 insertions(+) diff --git a/src/RequestHeaderParser.php b/src/RequestHeaderParser.php index cb6537e2..bb438b37 100644 --- a/src/RequestHeaderParser.php +++ b/src/RequestHeaderParser.php @@ -118,6 +118,14 @@ private function parseRequest($data) ); $request = $request->withRequestTarget($target); + // Add query params + $queryString = $request->getUri()->getQuery(); + if ($queryString !== '') { + $queryParams = array(); + parse_str($queryString, $queryParams); + $request = $request->withQueryParams($queryParams); + } + // re-apply actual request target from above if ($originalTarget !== null) { $uri = $request->getUri()->withPath(''); diff --git a/tests/RequestHeaderParserTest.php b/tests/RequestHeaderParserTest.php index 957bb6ac..956bf4ff 100644 --- a/tests/RequestHeaderParserTest.php +++ b/tests/RequestHeaderParserTest.php @@ -355,6 +355,23 @@ public function testServerParamsWontBeSetOnMissingUrls() $this->assertArrayNotHasKey('REMOTE_PORT', $serverParams); } + public function testQueryParmetersWillBeSet() + { + $request = null; + + $parser = new RequestHeaderParser(); + + $parser->on('headers', function ($parsedRequest) use (&$request) { + $request = $parsedRequest; + }); + + $parser->feed("GET /foo.php?hello=world&test=this HTTP/1.0\r\nHost: example.com\r\n\r\n"); + $queryParams = $request->getQueryParams(); + + $this->assertEquals('world', $queryParams['hello']); + $this->assertEquals('this', $queryParams['test']); + } + private function createGetRequest() { $data = "GET / HTTP/1.1\r\n"; diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 3cd88bf8..a0dd0f5b 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -90,6 +90,7 @@ public function testRequestEvent() $this->assertSame('GET', $requestAssertion->getMethod()); $this->assertSame('/', $requestAssertion->getRequestTarget()); $this->assertSame('/', $requestAssertion->getUri()->getPath()); + $this->assertSame(array(), $requestAssertion->getQueryParams()); $this->assertSame('http://example.com/', (string)$requestAssertion->getUri()); $this->assertSame('example.com', $requestAssertion->getHeaderLine('Host')); $this->assertSame('127.0.0.1', $serverParams['REMOTE_ADDR']); @@ -2369,6 +2370,26 @@ public function testServerRequestParams() $this->assertNotNull($serverParams['REQUEST_TIME_FLOAT']); } + public function testQueryParametersWillBeAddedToRequest() + { + $requestValidation = null; + $server = new Server($this->socket, function (ServerRequestInterface $request) use (&$requestValidation) { + $requestValidation = $request; + return new Response(); + }); + + $this->socket->emit('connection', array($this->connection)); + + $data = "GET /foo.php?hello=world&test=bar HTTP/1.0\r\n\r\n"; + + $this->connection->emit('data', array($data)); + + $queryParams = $requestValidation->getQueryParams(); + + $this->assertEquals('world', $queryParams['hello']); + $this->assertEquals('bar', $queryParams['test']); + } + private function createGetRequest() { $data = "GET / HTTP/1.1\r\n"; From 29886b211f2c9f380d0fc9c72a060259c27c6eb1 Mon Sep 17 00:00:00 2001 From: andig Date: Sat, 29 Apr 2017 15:36:23 +0200 Subject: [PATCH 137/456] Cleanup comments --- src/Server.php | 37 ++----------------------------------- 1 file changed, 2 insertions(+), 35 deletions(-) diff --git a/src/Server.php b/src/Server.php index 51ad186b..6fa1b629 100644 --- a/src/Server.php +++ b/src/Server.php @@ -26,7 +26,7 @@ * ```php * $socket = new React\Socket\Server(8080, $loop); * - * $http = new Server($socket, function (RequestInterface $request) { + * $http = new Server($socket, function (ServerRequestInterface $request) { * return new Response( * 200, * array('Content-Type' => 'text/plain'), @@ -47,7 +47,7 @@ * 'local_cert' => __DIR__ . '/localhost.pem' * )); * - * $http = new Server($socket, function (RequestInterface $request) { + * $http = new Server($socket, function (ServerRequestInterface $request) { * return new Response( * 200, * array('Content-Type' => 'text/plain'), @@ -95,39 +95,6 @@ class Server extends EventEmitter /** * Creates a HTTP server that accepts connections from the given socket. * - * It attaches itself to an instance of `React\Socket\ServerInterface` which - * emits underlying streaming connections in order to then parse incoming data - * as HTTP. - * - * For each request, it executes the callback function passed to the - * constructor with the respective [`Request`](#request) and - * [`Response`](#response) objects: - * - * ```php - * $socket = new React\Socket\Server(8080, $loop); - * - * $http = new Server($socket, function (Request $request, Response $response) { - * $response->writeHead(200, array('Content-Type' => 'text/plain')); - * $response->end("Hello World!\n"); - * }); - * ``` - * - * Similarly, you can also attach this to a - * [`React\Socket\SecureServer`](https://github.com/reactphp/socket#secureserver) - * in order to start a secure HTTPS server like this: - * - * ```php - * $socket = new React\Socket\Server(8080, $loop); - * $socket = new React\Socket\SecureServer($socket, $loop, array( - * 'local_cert' => __DIR__ . '/localhost.pem' - * )); - * - * $http = new Server($socket, function (Request $request, Response $response) { - * $response->writeHead(200, array('Content-Type' => 'text/plain')); - * $response->end("Hello World!\n"); - * }); - *``` - * * @param \React\Socket\ServerInterface $io * @param callable $callback */ From aee0851690491e62373cec084dc3b973a327b29e Mon Sep 17 00:00:00 2001 From: Niels Theen Date: Wed, 26 Apr 2017 14:46:48 +0200 Subject: [PATCH 138/456] Update README --- README.md | 30 +++++++++++++++++ examples/03-query-parameter.php | 32 +++++++++++++++++++ ...unt-visitors.php => 04-count-visitors.php} | 0 ...am-response.php => 05-stream-response.php} | 0 ...ream-request.php => 06-stream-request.php} | 0 ...ror-handling.php => 07-error-handling.php} | 0 6 files changed, 62 insertions(+) create mode 100644 examples/03-query-parameter.php rename examples/{03-count-visitors.php => 04-count-visitors.php} (100%) rename examples/{04-stream-response.php => 05-stream-response.php} (100%) rename examples/{05-stream-request.php => 06-stream-request.php} (100%) rename examples/{06-error-handling.php => 07-error-handling.php} (100%) diff --git a/README.md b/README.md index d4d35e39..7551cda7 100644 --- a/README.md +++ b/README.md @@ -190,6 +190,36 @@ $http = new Server($socket, function (ServerRequestInterface $request) { See also [example #2](examples). +The `getQueryParams(): array` method can be used to get the query parameters +similiar to the `$_GET` variable. + +```php +$http = new Server($socket, function (ServerRequestInterface $request) { + $queryParams = $request->getQueryParams(); + + $body = 'The query parameter "foo" is not set. Click the following link '; + $body .= 'to use query parameter in your request'; + + if (isset($queryParams['foo'])) { + $body = 'The value of "foo" is: ' . htmlspecialchars($queryParams['foo']); + } + + return new Response( + 200, + array('Content-Type' => 'text/html'), + $body + ); +}); +``` + +The response in the above example will return a response body with a link. +The URL contains the query parameter `foo` with the value `bar`. +Use [`htmlentities`](http://php.net/manual/en/function.htmlentities.php) +like in this example to prevent +[Cross-Site Scripting (abbreviated as XSS)](https://en.wikipedia.org/wiki/Cross-site_scripting). + +See also [example #3](examples). + For more details about the request object, check out the documentation of [PSR-7 ServerRequestInterface](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-7-http-message.md#321-psrhttpmessageserverrequestinterface) and diff --git a/examples/03-query-parameter.php b/examples/03-query-parameter.php new file mode 100644 index 00000000..15f6c49a --- /dev/null +++ b/examples/03-query-parameter.php @@ -0,0 +1,32 @@ +getQueryParams(); + + $body = 'The query parameter "foo" is not set. Click the following link '; + $body .= 'to use query parameter in your request'; + + if (isset($queryParams['foo'])) { + $body = 'The value of "foo" is: ' . htmlspecialchars($queryParams['foo']); + } + + return new Response( + 200, + array('Content-Type' => 'text/html'), + $body + ); +}); + +echo 'Listening on http://' . $socket->getAddress() . PHP_EOL; + +$loop->run(); diff --git a/examples/03-count-visitors.php b/examples/04-count-visitors.php similarity index 100% rename from examples/03-count-visitors.php rename to examples/04-count-visitors.php diff --git a/examples/04-stream-response.php b/examples/05-stream-response.php similarity index 100% rename from examples/04-stream-response.php rename to examples/05-stream-response.php diff --git a/examples/05-stream-request.php b/examples/06-stream-request.php similarity index 100% rename from examples/05-stream-request.php rename to examples/06-stream-request.php diff --git a/examples/06-error-handling.php b/examples/07-error-handling.php similarity index 100% rename from examples/06-error-handling.php rename to examples/07-error-handling.php From 71662768585745f4f04a5f9272475b1e84f7d529 Mon Sep 17 00:00:00 2001 From: Niels Theen Date: Fri, 21 Apr 2017 14:47:33 +0200 Subject: [PATCH 139/456] Add function to parse cookies --- src/ServerRequest.php | 29 ++++++++++++++ tests/ServerRequestTest.php | 77 +++++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+) diff --git a/src/ServerRequest.php b/src/ServerRequest.php index ea36a5a9..c3fee8f2 100644 --- a/src/ServerRequest.php +++ b/src/ServerRequest.php @@ -117,4 +117,33 @@ public function withoutAttribute($name) unset($new->attributes[$name]); return $new; } + + /** + * @internal + * @param string $cookie + * @return boolean|mixed[] + */ + public static function parseCookie($cookie) + { + // PSR-7 `getHeadline('Cookies')` will return multiple + // cookie header coma-seperated. Multiple cookie headers + // are not allowed according to https://tools.ietf.org/html/rfc6265#section-5.4 + if (strpos($cookie, ',') !== false) { + return false; + } + + $cookieArray = explode(';', $cookie); + $result = array(); + + foreach ($cookieArray as $pair) { + $nameValuePair = explode('=', $pair, 2); + if (count($nameValuePair) === 2) { + $key = urldecode($nameValuePair[0]); + $value = urldecode($nameValuePair[1]); + $result[$key] = $value; + } + } + + return $result; + } } diff --git a/tests/ServerRequestTest.php b/tests/ServerRequestTest.php index 14edf3a8..dfc44dd9 100644 --- a/tests/ServerRequestTest.php +++ b/tests/ServerRequestTest.php @@ -105,4 +105,81 @@ public function testServerRequestParameter() $this->assertEquals('1.0', $request->getProtocolVersion()); $this->assertEquals('127.0.0.1', $serverParams['SERVER_ADDR']); } + + public function testParseSingleCookieNameValuePairWillReturnValidArray() + { + $cookieString = 'hello=world'; + $cookies = ServerRequest::parseCookie($cookieString); + $this->assertEquals(array('hello' => 'world'), $cookies); + }# + + public function testParseMultipleCookieNameValuePaiWillReturnValidArray() + { + $cookieString = 'hello=world;test=abc'; + $cookies = ServerRequest::parseCookie($cookieString); + $this->assertEquals(array('hello' => 'world', 'test' => 'abc'), $cookies); + } + + public function testParseMultipleCookieNameValuePairWillReturnFalse() + { + // Could be done through multiple 'Cookie' headers + // getHeaderLine('Cookie') will return a value seperated by coma + // e.g. + // GET / HTTP/1.1\r\n + // Host: test.org\r\n + // Cookie: hello=world\r\n + // Cookie: test=abc\r\n\r\n + $cookieString = 'hello=world,test=abc'; + $cookies = ServerRequest::parseCookie($cookieString); + $this->assertEquals(false, $cookies); + } + + public function testOnlyFirstSetWillBeAddedToCookiesArray() + { + $cookieString = 'hello=world;hello=abc'; + $cookies = ServerRequest::parseCookie($cookieString); + $this->assertEquals(array('hello' => 'abc'), $cookies); + } + + public function testOtherEqualSignsWillBeAddedToValueAndWillReturnValidArray() + { + $cookieString = 'hello=world=test=php'; + $cookies = ServerRequest::parseCookie($cookieString); + $this->assertEquals(array('hello' => 'world=test=php'), $cookies); + } + + public function testSingleCookieValueInCookiesReturnsEmptyArray() + { + $cookieString = 'world'; + $cookies = ServerRequest::parseCookie($cookieString); + $this->assertEquals(array(), $cookies); + } + + public function testSingleMutlipleCookieValuesReturnsEmptyArray() + { + $cookieString = 'world;test'; + $cookies = ServerRequest::parseCookie($cookieString); + $this->assertEquals(array(), $cookies); + } + + public function testSingleValueIsValidInMultipleValueCookieWillReturnValidArray() + { + $cookieString = 'world;test=php'; + $cookies = ServerRequest::parseCookie($cookieString); + $this->assertEquals(array('test' => 'php'), $cookies); + } + + public function testUrlEncodingForValueWillReturnValidArray() + { + $cookieString = 'hello=world%21;test=100%25%20coverage'; + $cookies = ServerRequest::parseCookie($cookieString); + $this->assertEquals(array('hello' => 'world!', 'test' => '100% coverage'), $cookies); + } + + public function testUrlEncodingForKeyWillReturnValidArray() + { + $cookieString = 'react%3Bphp=is%20great'; + $cookies = ServerRequest::parseCookie($cookieString); + $this->assertEquals(array('react;php' => 'is great'), $cookies); + } } From d27e1677eede555db1e397124d2569569ad37a39 Mon Sep 17 00:00:00 2001 From: Niels Theen Date: Fri, 21 Apr 2017 14:48:16 +0200 Subject: [PATCH 140/456] Parse cookie for request object --- src/RequestHeaderParser.php | 5 +++ src/ServerRequest.php | 6 ++-- tests/ServerRequestTest.php | 21 ++++++++----- tests/ServerTest.php | 62 +++++++++++++++++++++++++++++++++++++ 4 files changed, 85 insertions(+), 9 deletions(-) diff --git a/src/RequestHeaderParser.php b/src/RequestHeaderParser.php index bb438b37..d7fdc1d2 100644 --- a/src/RequestHeaderParser.php +++ b/src/RequestHeaderParser.php @@ -126,6 +126,11 @@ private function parseRequest($data) $request = $request->withQueryParams($queryParams); } + $cookies = ServerRequest::parseCookie($request->getHeaderLine('Cookie')); + if ($cookies !== false) { + $request = $request->withCookieParams($cookies); + } + // re-apply actual request target from above if ($originalTarget !== null) { $uri = $request->getUri()->withPath(''); diff --git a/src/ServerRequest.php b/src/ServerRequest.php index c3fee8f2..0f31628f 100644 --- a/src/ServerRequest.php +++ b/src/ServerRequest.php @@ -125,8 +125,8 @@ public function withoutAttribute($name) */ public static function parseCookie($cookie) { - // PSR-7 `getHeadline('Cookies')` will return multiple - // cookie header coma-seperated. Multiple cookie headers + // PSR-7 `getHeaderLine('Cookies')` will return multiple + // cookie header comma-seperated. Multiple cookie headers // are not allowed according to https://tools.ietf.org/html/rfc6265#section-5.4 if (strpos($cookie, ',') !== false) { return false; @@ -136,7 +136,9 @@ public static function parseCookie($cookie) $result = array(); foreach ($cookieArray as $pair) { + $pair = trim($pair); $nameValuePair = explode('=', $pair, 2); + if (count($nameValuePair) === 2) { $key = urldecode($nameValuePair[0]); $value = urldecode($nameValuePair[1]); diff --git a/tests/ServerRequestTest.php b/tests/ServerRequestTest.php index dfc44dd9..a2c68a4a 100644 --- a/tests/ServerRequestTest.php +++ b/tests/ServerRequestTest.php @@ -111,11 +111,11 @@ public function testParseSingleCookieNameValuePairWillReturnValidArray() $cookieString = 'hello=world'; $cookies = ServerRequest::parseCookie($cookieString); $this->assertEquals(array('hello' => 'world'), $cookies); - }# + } public function testParseMultipleCookieNameValuePaiWillReturnValidArray() { - $cookieString = 'hello=world;test=abc'; + $cookieString = 'hello=world; test=abc'; $cookies = ServerRequest::parseCookie($cookieString); $this->assertEquals(array('hello' => 'world', 'test' => 'abc'), $cookies); } @@ -123,7 +123,7 @@ public function testParseMultipleCookieNameValuePaiWillReturnValidArray() public function testParseMultipleCookieNameValuePairWillReturnFalse() { // Could be done through multiple 'Cookie' headers - // getHeaderLine('Cookie') will return a value seperated by coma + // getHeaderLine('Cookie') will return a value seperated by comma // e.g. // GET / HTTP/1.1\r\n // Host: test.org\r\n @@ -136,7 +136,7 @@ public function testParseMultipleCookieNameValuePairWillReturnFalse() public function testOnlyFirstSetWillBeAddedToCookiesArray() { - $cookieString = 'hello=world;hello=abc'; + $cookieString = 'hello=world; hello=abc'; $cookies = ServerRequest::parseCookie($cookieString); $this->assertEquals(array('hello' => 'abc'), $cookies); } @@ -157,21 +157,21 @@ public function testSingleCookieValueInCookiesReturnsEmptyArray() public function testSingleMutlipleCookieValuesReturnsEmptyArray() { - $cookieString = 'world;test'; + $cookieString = 'world; test'; $cookies = ServerRequest::parseCookie($cookieString); $this->assertEquals(array(), $cookies); } public function testSingleValueIsValidInMultipleValueCookieWillReturnValidArray() { - $cookieString = 'world;test=php'; + $cookieString = 'world; test=php'; $cookies = ServerRequest::parseCookie($cookieString); $this->assertEquals(array('test' => 'php'), $cookies); } public function testUrlEncodingForValueWillReturnValidArray() { - $cookieString = 'hello=world%21;test=100%25%20coverage'; + $cookieString = 'hello=world%21; test=100%25%20coverage'; $cookies = ServerRequest::parseCookie($cookieString); $this->assertEquals(array('hello' => 'world!', 'test' => '100% coverage'), $cookies); } @@ -182,4 +182,11 @@ public function testUrlEncodingForKeyWillReturnValidArray() $cookies = ServerRequest::parseCookie($cookieString); $this->assertEquals(array('react;php' => 'is great'), $cookies); } + + public function testCookieWithoutSpaceAfterSeparatorWillBeAccepted() + { + $cookieString = 'hello=world;react=php'; + $cookies = ServerRequest::parseCookie($cookieString); + $this->assertEquals(array('hello' => 'world', 'react' => 'php'), $cookies); + } } diff --git a/tests/ServerTest.php b/tests/ServerTest.php index a0dd0f5b..450b90cc 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -2390,6 +2390,68 @@ public function testQueryParametersWillBeAddedToRequest() $this->assertEquals('bar', $queryParams['test']); } + public function testCookieWillBeAddedToServerRequest() + { + $requestValidation = null; + $server = new Server($this->socket, function (ServerRequestInterface $request) use (&$requestValidation) { + $requestValidation = $request; + return new Response(); + }); + + $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 .= "Cookie: hello=world\r\n"; + $data .= "\r\n"; + + $this->connection->emit('data', array($data)); + + $this->assertEquals(array('hello' => 'world'), $requestValidation->getCookieParams()); + } + + public function testMultipleCookiesWontBeAddedToServerRequest() + { + $requestValidation = null; + $server = new Server($this->socket, function (ServerRequestInterface $request) use (&$requestValidation) { + $requestValidation = $request; + return new Response(); + }); + + $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 .= "Cookie: hello=world\r\n"; + $data .= "Cookie: test=failed\r\n"; + $data .= "\r\n"; + + $this->connection->emit('data', array($data)); + $this->assertEquals(array(), $requestValidation->getCookieParams()); + } + + public function testCookieWithSeparatorWillBeAddedToServerRequest() + { + $requestValidation = null; + $server = new Server($this->socket, function (ServerRequestInterface $request) use (&$requestValidation) { + $requestValidation = $request; + return new Response(); + }); + + $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 .= "Cookie: hello=world; test=abc\r\n"; + $data .= "\r\n"; + + $this->connection->emit('data', array($data)); + $this->assertEquals(array('hello' => 'world', 'test' => 'abc'), $requestValidation->getCookieParams()); + } + private function createGetRequest() { $data = "GET / HTTP/1.1\r\n"; From a4ba52805326a2786a916f2f4e1b8b8e60c3a419 Mon Sep 17 00:00:00 2001 From: Niels Theen Date: Sun, 23 Apr 2017 02:00:45 +0200 Subject: [PATCH 141/456] Add description and example --- README.md | 39 ++++++++++++++++++++++++++++++++- examples/06-cookie-handling.php | 38 ++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 examples/06-cookie-handling.php diff --git a/README.md b/README.md index 7551cda7..331082b3 100644 --- a/README.md +++ b/README.md @@ -225,7 +225,7 @@ For more details about the request object, check out the documentation of and [PSR-7 RequestInterface](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-7-http-message.md#32-psrhttpmessagerequestinterface). -> Currently the cookies and uploaded files are not added by the +> Currently the uploaded files are not added by the `Server`, but you can add these parameters by yourself using the given methods. The next versions of this project will cover these features. @@ -378,6 +378,43 @@ Allowed). can in fact use a streaming response body for the tunneled application data. See also [example #21](examples) for more details. +The `getCookieParams(): string[]` method can be used to +get all cookies sent with the current request. + +```php +$http = new Server($socket, function (ServerRequestInterface $request) { + $key = 'react\php'; + + if (isset($request->getCookieParams()[$key])) { + $body = "Your cookie value is: " . $request->getCookieParams()[$key]; + + return new Response( + 200, + array('Content-Type' => 'text/plain'), + $body + ); + } + + return new Response( + 200, + array( + 'Content-Type' => 'text/plain', + 'Set-Cookie' => urlencode($key) . '=' . urlencode('test;more') + ), + "Your cookie has been set." + ); +}); +``` + +The above example will try to set a cookie on first access and +will try to print the cookie value on all subsequent tries. +Note how the example uses the `urlencode()` function to encode +non-alphanumeric characters. +This encoding is also used internally when decoding the name and value of cookies +(which is in line with other implementations, such as PHP's cookie functions). + +See also [example #6](examples) for more details. + ### Response The callback function passed to the constructor of the [Server](#server) diff --git a/examples/06-cookie-handling.php b/examples/06-cookie-handling.php new file mode 100644 index 00000000..67e008bb --- /dev/null +++ b/examples/06-cookie-handling.php @@ -0,0 +1,38 @@ +getCookieParams()[$key])) { + $body = "Your cookie value is: " . $request->getCookieParams()[$key]; + + return new Response( + 200, + array('Content-Type' => 'text/plain'), + $body + ); + } + + return new Response( + 200, + array( + 'Content-Type' => 'text/plain', + 'Set-Cookie' => urlencode($key) . '=' . urlencode('test;more') + ), + "Your cookie has been set." + ); +}); + +echo 'Listening on http://' . $socket->getAddress() . PHP_EOL; + +$loop->run(); From ce9fb0bc268bed18e0d5a9933d73bdca49026254 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 6 May 2017 10:44:46 +0200 Subject: [PATCH 142/456] Forward compatibility with Socket v1.0 and v0.8 --- composer.json | 4 ++-- src/RequestHeaderParser.php | 3 +++ src/Server.php | 22 ++++++++++++++++---- tests/FunctionalServerTest.php | 37 +++++++++++++++++++++------------- 4 files changed, 46 insertions(+), 20 deletions(-) diff --git a/composer.json b/composer.json index fccbc1bd..50e11f88 100644 --- a/composer.json +++ b/composer.json @@ -6,7 +6,7 @@ "require": { "php": ">=5.3.0", "ringcentral/psr7": "^1.2", - "react/socket": "^0.7 || ^0.6 || ^0.5", + "react/socket": "^1.0 || ^0.8 || ^0.7 || ^0.6 || ^0.5", "react/stream": "^0.6 || ^0.5 || ^0.4.4", "react/promise": "^2.0 || ^1.1", "evenement/evenement": "^2.0 || ^1.0" @@ -18,7 +18,7 @@ }, "require-dev": { "phpunit/phpunit": "^4.8.10||^5.0", - "react/socket": "^0.7", + "react/socket": "^1.0 || ^0.8 || ^0.7", "clue/block-react": "^1.1" } } diff --git a/src/RequestHeaderParser.php b/src/RequestHeaderParser.php index d7fdc1d2..0b75ac01 100644 --- a/src/RequestHeaderParser.php +++ b/src/RequestHeaderParser.php @@ -181,6 +181,9 @@ private function parseRequest($data) // set URI components from socket address if not already filled via Host header if ($request->getUri()->getHost() === '') { $parts = parse_url($this->localSocketUri); + if (!isset($parts['host'], $parts['port'])) { + $parts = array('host' => '127.0.0.1', 'port' => 80); + } $request = $request->withUri( $request->getUri()->withScheme('http')->withHost($parts['host'])->withPort($parts['port']), diff --git a/src/Server.php b/src/Server.php index 6fa1b629..61d68953 100644 --- a/src/Server.php +++ b/src/Server.php @@ -111,11 +111,25 @@ public function __construct(SocketServerInterface $io, $callback) /** @internal */ public function handleConnection(ConnectionInterface $conn) { + $uriLocal = $conn->getLocalAddress(); + if ($uriLocal !== null && strpos($uriLocal, '://') === false) { + // local URI known but does not contain a scheme. Should only happen for old Socket < 0.8 + // try to detect transport encryption and assume default application scheme + $uriLocal = ($this->isConnectionEncrypted($conn) ? 'https://' : 'http://') . $uriLocal; + } elseif ($uriLocal !== null) { + // local URI known, so translate transport scheme to application scheme + $uriLocal = strtr($uriLocal, array('tcp://' => 'http://', 'tls://' => 'https://')); + } + + $uriRemote = $conn->getRemoteAddress(); + if ($uriRemote !== null && strpos($uriRemote, '://') === false) { + // local URI known but does not contain a scheme. Should only happen for old Socket < 0.8 + // actual scheme is not evaluated but required for parsing URI + $uriRemote = 'unused://' . $uriRemote; + } + $that = $this; - $parser = new RequestHeaderParser( - ($this->isConnectionEncrypted($conn) ? 'https://' : 'http://') . $conn->getLocalAddress(), - 'tcp://' . $conn->getRemoteAddress() - ); + $parser = new RequestHeaderParser($uriLocal, $uriRemote); $listener = array($parser, 'feed'); $parser->on('headers', function (RequestInterface $request, $bodyBuffer) use ($conn, $listener, $parser, $that) { diff --git a/tests/FunctionalServerTest.php b/tests/FunctionalServerTest.php index f34f2bc3..4b807617 100644 --- a/tests/FunctionalServerTest.php +++ b/tests/FunctionalServerTest.php @@ -13,7 +13,7 @@ use React\Http\Response; use React\Socket\SecureServer; -class FunctionServerTest extends TestCase +class FunctionalServerTest extends TestCase { public function testPlainHttpOnRandomPort() { @@ -26,7 +26,7 @@ public function testPlainHttpOnRandomPort() }); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { - $conn->write("GET / HTTP/1.0\r\nHost: " . $conn->getRemoteAddress() . "\r\n\r\n"); + $conn->write("GET / HTTP/1.0\r\nHost: " . noScheme($conn->getRemoteAddress()) . "\r\n\r\n"); return BufferedSink::createPromise($conn); }); @@ -34,7 +34,7 @@ public function testPlainHttpOnRandomPort() $response = Block\await($result, $loop, 1.0); $this->assertContains("HTTP/1.0 200 OK", $response); - $this->assertContains('http://' . $socket->getAddress() . '/', $response); + $this->assertContains('http://' . noScheme($socket->getAddress()) . '/', $response); $socket->close(); } @@ -58,7 +58,7 @@ public function testPlainHttpOnRandomPortWithoutHostHeaderUsesSocketUri() $response = Block\await($result, $loop, 1.0); $this->assertContains("HTTP/1.0 200 OK", $response); - $this->assertContains('http://' . $socket->getAddress() . '/', $response); + $this->assertContains('http://' . noScheme($socket->getAddress()) . '/', $response); $socket->close(); } @@ -106,8 +106,8 @@ public function testSecureHttpsOnRandomPort() return new Response(200, array(), (string)$request->getUri()); }); - $result = $connector->connect('tls://' . $socket->getAddress())->then(function (ConnectionInterface $conn) { - $conn->write("GET / HTTP/1.0\r\nHost: " . $conn->getRemoteAddress() . "\r\n\r\n"); + $result = $connector->connect('tls://' . noScheme($socket->getAddress()))->then(function (ConnectionInterface $conn) { + $conn->write("GET / HTTP/1.0\r\nHost: " . noScheme($conn->getRemoteAddress()) . "\r\n\r\n"); return BufferedSink::createPromise($conn); }); @@ -115,7 +115,7 @@ public function testSecureHttpsOnRandomPort() $response = Block\await($result, $loop, 1.0); $this->assertContains("HTTP/1.0 200 OK", $response); - $this->assertContains('https://' . $socket->getAddress() . '/', $response); + $this->assertContains('https://' . noScheme($socket->getAddress()) . '/', $response); $socket->close(); } @@ -139,7 +139,7 @@ public function testSecureHttpsOnRandomPortWithoutHostHeaderUsesSocketUri() return new Response(200, array(), (string)$request->getUri()); }); - $result = $connector->connect('tls://' . $socket->getAddress())->then(function (ConnectionInterface $conn) { + $result = $connector->connect('tls://' . noScheme($socket->getAddress()))->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\n\r\n"); return BufferedSink::createPromise($conn); @@ -148,7 +148,7 @@ public function testSecureHttpsOnRandomPortWithoutHostHeaderUsesSocketUri() $response = Block\await($result, $loop, 1.0); $this->assertContains("HTTP/1.0 200 OK", $response); - $this->assertContains('https://' . $socket->getAddress() . '/', $response); + $this->assertContains('https://' . noScheme($socket->getAddress()) . '/', $response); $socket->close(); } @@ -232,7 +232,7 @@ public function testSecureHttpsOnStandardPortReturnsUriWithNoPort() return new Response(200, array(), (string)$request->getUri()); }); - $result = $connector->connect('tls://' . $socket->getAddress())->then(function (ConnectionInterface $conn) { + $result = $connector->connect('tls://' . noScheme($socket->getAddress()))->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n"); return BufferedSink::createPromise($conn); @@ -269,7 +269,7 @@ public function testSecureHttpsOnStandardPortWithoutHostHeaderUsesSocketUri() return new Response(200, array(), (string)$request->getUri()); }); - $result = $connector->connect('tls://' . $socket->getAddress())->then(function (ConnectionInterface $conn) { + $result = $connector->connect('tls://' . noScheme($socket->getAddress()))->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\n\r\n"); return BufferedSink::createPromise($conn); @@ -298,7 +298,7 @@ public function testPlainHttpOnHttpsStandardPortReturnsUriWithPort() }); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { - $conn->write("GET / HTTP/1.0\r\nHost: " . $conn->getRemoteAddress() . "\r\n\r\n"); + $conn->write("GET / HTTP/1.0\r\nHost: " . noScheme($conn->getRemoteAddress()) . "\r\n\r\n"); return BufferedSink::createPromise($conn); }); @@ -334,8 +334,8 @@ public function testSecureHttpsOnHttpStandardPortReturnsUriWithPort() return new Response(200, array(), (string)$request->getUri() . 'x' . $request->getHeaderLine('Host')); }); - $result = $connector->connect('tls://' . $socket->getAddress())->then(function (ConnectionInterface $conn) { - $conn->write("GET / HTTP/1.0\r\nHost: " . $conn->getRemoteAddress() . "\r\n\r\n"); + $result = $connector->connect('tls://' . noScheme($socket->getAddress()))->then(function (ConnectionInterface $conn) { + $conn->write("GET / HTTP/1.0\r\nHost: " . noScheme($conn->getRemoteAddress()) . "\r\n\r\n"); return BufferedSink::createPromise($conn); }); @@ -348,3 +348,12 @@ public function testSecureHttpsOnHttpStandardPortReturnsUriWithPort() $socket->close(); } } + +function noScheme($uri) +{ + $pos = strpos($uri, '://'); + if ($pos !== false) { + $uri = substr($uri, $pos + 3); + } + return $uri; +} From 3210ee67eb2b26e66cce6879c432a68ad900f4b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 12 May 2017 16:22:07 +0200 Subject: [PATCH 143/456] Forward compatibility with Stream v1.0 and v0.7 --- README.md | 6 +-- composer.json | 2 +- examples/05-stream-response.php | 4 +- examples/99-benchmark-download.php | 31 ++++++++++-- tests/ChunkedDecoderTest.php | 6 +-- tests/ChunkedEncoderTest.php | 4 +- tests/CloseProtectionStreamTest.php | 14 +++--- tests/FunctionalServerTest.php | 73 ++++++++++++++++++++--------- tests/HttpBodyStreamTest.php | 4 +- tests/LengthLimitedStreamTest.php | 6 +-- tests/ResponseTest.php | 4 +- tests/ServerTest.php | 10 ++-- 12 files changed, 108 insertions(+), 56 deletions(-) diff --git a/README.md b/README.md index 331082b3..1b6c137e 100644 --- a/README.md +++ b/README.md @@ -477,11 +477,11 @@ The `Response` class in this project supports to add an instance which implement for the response body. So you are able stream data directly into the response body. Note that other implementations of the `PSR-7 ResponseInterface` likely -only support string. +only support strings. ```php $server = new Server($socket, function (ServerRequestInterface $request) use ($loop) { - $stream = new ReadableStream(); + $stream = new ThroughStream(); $timer = $loop->addPeriodicTimer(0.5, function () use ($stream) { $stream->emit('data', array(microtime(true) . PHP_EOL)); @@ -513,7 +513,7 @@ pass this header yourself. If you know the length of your stream body, you MAY specify it like this instead: ```php -$stream = new ReadableStream() +$stream = new ThroughStream() $server = new Server($socket, function (ServerRequestInterface $request) use ($loop, $stream) { return new Response( 200, diff --git a/composer.json b/composer.json index 50e11f88..6dd1f48e 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ "php": ">=5.3.0", "ringcentral/psr7": "^1.2", "react/socket": "^1.0 || ^0.8 || ^0.7 || ^0.6 || ^0.5", - "react/stream": "^0.6 || ^0.5 || ^0.4.4", + "react/stream": "^1.0 || ^0.7 || ^0.6 || ^0.5 || ^0.4.6", "react/promise": "^2.0 || ^1.1", "evenement/evenement": "^2.0 || ^1.0" }, diff --git a/examples/05-stream-response.php b/examples/05-stream-response.php index 8edf7d40..b4ef6962 100644 --- a/examples/05-stream-response.php +++ b/examples/05-stream-response.php @@ -4,7 +4,7 @@ use React\Socket\Server; use React\Http\Response; use Psr\Http\Message\ServerRequestInterface; -use React\Stream\ReadableStream; +use React\Stream\ThroughStream; require __DIR__ . '/../vendor/autoload.php'; @@ -12,7 +12,7 @@ $socket = new Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); $server = new \React\Http\Server($socket, function (ServerRequestInterface $request) use ($loop) { - $stream = new ReadableStream(); + $stream = new ThroughStream(); $timer = $loop->addPeriodicTimer(0.5, function () use ($stream) { $stream->emit('data', array(microtime(true) . PHP_EOL)); diff --git a/examples/99-benchmark-download.php b/examples/99-benchmark-download.php index 47460b5f..0be294b1 100644 --- a/examples/99-benchmark-download.php +++ b/examples/99-benchmark-download.php @@ -10,7 +10,9 @@ use React\Socket\Server; use React\Http\Response; use Psr\Http\Message\ServerRequestInterface; -use React\Stream\ReadableStream; +use Evenement\EventEmitter; +use React\Stream\ReadableStreamInterface; +use React\Stream\WritableStreamInterface; require __DIR__ . '/../vendor/autoload.php'; @@ -18,12 +20,13 @@ $socket = new Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); /** A readable stream that can emit a lot of data */ -class ChunkRepeater extends ReadableStream +class ChunkRepeater extends EventEmitter implements ReadableStreamInterface { private $chunk; private $count; private $position = 0; private $paused = true; + private $closed = false; public function __construct($chunk, $count) { @@ -38,7 +41,7 @@ public function pause() public function resume() { - if (!$this->paused) { + if (!$this->paused || $this->closed) { return; } @@ -56,6 +59,28 @@ public function resume() } } + public function pipe(WritableStreamInterface $dest, array $options = array()) + { + return; + } + + public function isReadable() + { + return !$this->closed; + } + + public function close() + { + if ($this->closed) { + return; + } + + $this->closed = true; + $this->count = 0; + $this->paused = true; + $this->emit('close'); + } + public function getSize() { return strlen($this->chunk) * $this->count; diff --git a/tests/ChunkedDecoderTest.php b/tests/ChunkedDecoderTest.php index 7f675f42..87548f79 100644 --- a/tests/ChunkedDecoderTest.php +++ b/tests/ChunkedDecoderTest.php @@ -2,14 +2,14 @@ namespace React\Tests\Http; -use React\Stream\ReadableStream; +use React\Stream\ThroughStream; use React\Http\ChunkedDecoder; class ChunkedDecoderTest extends TestCase { public function setUp() { - $this->input = new ReadableStream(); + $this->input = new ThroughStream(); $this->parser = new ChunkedDecoder($this->input); } @@ -386,7 +386,7 @@ public function testHandleClose() public function testOutputStreamCanCloseInputStream() { - $input = new ReadableStream(); + $input = new ThroughStream(); $input->on('close', $this->expectCallableOnce()); $stream = new ChunkedDecoder($input); diff --git a/tests/ChunkedEncoderTest.php b/tests/ChunkedEncoderTest.php index ca8dc643..8dcdbdbc 100644 --- a/tests/ChunkedEncoderTest.php +++ b/tests/ChunkedEncoderTest.php @@ -2,7 +2,7 @@ namespace React\Tests\Http; -use React\Stream\ReadableStream; +use React\Stream\ThroughStream; use React\Http\ChunkedEncoder; class ChunkedEncoderTest extends TestCase @@ -12,7 +12,7 @@ class ChunkedEncoderTest extends TestCase public function setUp() { - $this->input = new ReadableStream(); + $this->input = new ThroughStream(); $this->chunkedStream = new ChunkedEncoder($this->input); } diff --git a/tests/CloseProtectionStreamTest.php b/tests/CloseProtectionStreamTest.php index a85e7c10..e0c82596 100644 --- a/tests/CloseProtectionStreamTest.php +++ b/tests/CloseProtectionStreamTest.php @@ -3,7 +3,7 @@ namespace React\Tests\Http; use React\Http\CloseProtectionStream; -use React\Stream\ReadableStream; +use React\Stream\ThroughStream; class CloseProtectionStreamTest extends TestCase { @@ -19,7 +19,7 @@ public function testClosePausesTheInputStreamInsteadOfClosing() public function testErrorWontCloseStream() { - $input = new ReadableStream(); + $input = new ThroughStream(); $protection = new CloseProtectionStream($input); $protection->on('error', $this->expectCallableOnce()); @@ -44,7 +44,7 @@ public function testResumeStreamWillResumeInputStream() public function testInputStreamIsNotReadableAfterClose() { - $input = new ReadableStream(); + $input = new ThroughStream(); $protection = new CloseProtectionStream($input); $protection->on('close', $this->expectCallableOnce()); @@ -57,7 +57,7 @@ public function testInputStreamIsNotReadableAfterClose() public function testPipeStream() { - $input = new ReadableStream(); + $input = new ThroughStream(); $protection = new CloseProtectionStream($input); $dest = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); @@ -69,7 +69,7 @@ public function testPipeStream() public function testStopEmittingDataAfterClose() { - $input = new ReadableStream(); + $input = new ThroughStream(); $protection = new CloseProtectionStream($input); $protection->on('data', $this->expectCallableNever()); @@ -86,7 +86,7 @@ public function testStopEmittingDataAfterClose() public function testErrorIsNeverCalledAfterClose() { - $input = new ReadableStream(); + $input = new ThroughStream(); $protection = new CloseProtectionStream($input); $protection->on('data', $this->expectCallableNever()); @@ -103,7 +103,7 @@ public function testErrorIsNeverCalledAfterClose() public function testEndWontBeEmittedAfterClose() { - $input = new ReadableStream(); + $input = new ThroughStream(); $protection = new CloseProtectionStream($input); $protection->on('data', $this->expectCallableNever()); diff --git a/tests/FunctionalServerTest.php b/tests/FunctionalServerTest.php index 4b807617..042c4585 100644 --- a/tests/FunctionalServerTest.php +++ b/tests/FunctionalServerTest.php @@ -8,10 +8,13 @@ use Psr\Http\Message\RequestInterface; use React\Socket\Connector; use React\Socket\ConnectionInterface; -use React\Stream\BufferedSink; use Clue\React\Block; use React\Http\Response; use React\Socket\SecureServer; +use React\Stream\ReadableStreamInterface; +use React\EventLoop\LoopInterface; +use React\Promise\Promise; +use React\Promise\PromiseInterface; class FunctionalServerTest extends TestCase { @@ -28,10 +31,10 @@ public function testPlainHttpOnRandomPort() $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\nHost: " . noScheme($conn->getRemoteAddress()) . "\r\n\r\n"); - return BufferedSink::createPromise($conn); + return $conn; }); - $response = Block\await($result, $loop, 1.0); + $response = $this->buffer($result, $loop, 1.0); $this->assertContains("HTTP/1.0 200 OK", $response); $this->assertContains('http://' . noScheme($socket->getAddress()) . '/', $response); @@ -52,10 +55,10 @@ public function testPlainHttpOnRandomPortWithoutHostHeaderUsesSocketUri() $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\n\r\n"); - return BufferedSink::createPromise($conn); + return $conn; }); - $response = Block\await($result, $loop, 1.0); + $response = $this->buffer($result, $loop, 1.0); $this->assertContains("HTTP/1.0 200 OK", $response); $this->assertContains('http://' . noScheme($socket->getAddress()) . '/', $response); @@ -76,10 +79,10 @@ public function testPlainHttpOnRandomPortWithOtherHostHeaderTakesPrecedence() $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\nHost: localhost:1000\r\n\r\n"); - return BufferedSink::createPromise($conn); + return $conn; }); - $response = Block\await($result, $loop, 1.0); + $response = $this->buffer($result, $loop, 1.0); $this->assertContains("HTTP/1.0 200 OK", $response); $this->assertContains('http://localhost:1000/', $response); @@ -109,10 +112,10 @@ public function testSecureHttpsOnRandomPort() $result = $connector->connect('tls://' . noScheme($socket->getAddress()))->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\nHost: " . noScheme($conn->getRemoteAddress()) . "\r\n\r\n"); - return BufferedSink::createPromise($conn); + return $conn; }); - $response = Block\await($result, $loop, 1.0); + $response = $this->buffer($result, $loop, 1.0); $this->assertContains("HTTP/1.0 200 OK", $response); $this->assertContains('https://' . noScheme($socket->getAddress()) . '/', $response); @@ -142,10 +145,10 @@ public function testSecureHttpsOnRandomPortWithoutHostHeaderUsesSocketUri() $result = $connector->connect('tls://' . noScheme($socket->getAddress()))->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\n\r\n"); - return BufferedSink::createPromise($conn); + return $conn; }); - $response = Block\await($result, $loop, 1.0); + $response = $this->buffer($result, $loop, 1.0); $this->assertContains("HTTP/1.0 200 OK", $response); $this->assertContains('https://' . noScheme($socket->getAddress()) . '/', $response); @@ -170,10 +173,10 @@ public function testPlainHttpOnStandardPortReturnsUriWithNoPort() $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n"); - return BufferedSink::createPromise($conn); + return $conn; }); - $response = Block\await($result, $loop, 1.0); + $response = $this->buffer($result, $loop, 1.0); $this->assertContains("HTTP/1.0 200 OK", $response); $this->assertContains('http://127.0.0.1/', $response); @@ -198,10 +201,10 @@ public function testPlainHttpOnStandardPortWithoutHostHeaderReturnsUriWithNoPort $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\n\r\n"); - return BufferedSink::createPromise($conn); + return $conn; }); - $response = Block\await($result, $loop, 1.0); + $response = $this->buffer($result, $loop, 1.0); $this->assertContains("HTTP/1.0 200 OK", $response); $this->assertContains('http://127.0.0.1/', $response); @@ -235,10 +238,10 @@ public function testSecureHttpsOnStandardPortReturnsUriWithNoPort() $result = $connector->connect('tls://' . noScheme($socket->getAddress()))->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n"); - return BufferedSink::createPromise($conn); + return $conn; }); - $response = Block\await($result, $loop, 1.0); + $response = $this->buffer($result, $loop, 1.0); $this->assertContains("HTTP/1.0 200 OK", $response); $this->assertContains('https://127.0.0.1/', $response); @@ -272,10 +275,10 @@ public function testSecureHttpsOnStandardPortWithoutHostHeaderUsesSocketUri() $result = $connector->connect('tls://' . noScheme($socket->getAddress()))->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\n\r\n"); - return BufferedSink::createPromise($conn); + return $conn; }); - $response = Block\await($result, $loop, 1.0); + $response = $this->buffer($result, $loop, 1.0); $this->assertContains("HTTP/1.0 200 OK", $response); $this->assertContains('https://127.0.0.1/', $response); @@ -300,10 +303,10 @@ public function testPlainHttpOnHttpsStandardPortReturnsUriWithPort() $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\nHost: " . noScheme($conn->getRemoteAddress()) . "\r\n\r\n"); - return BufferedSink::createPromise($conn); + return $conn; }); - $response = Block\await($result, $loop, 1.0); + $response = $this->buffer($result, $loop, 1.0); $this->assertContains("HTTP/1.0 200 OK", $response); $this->assertContains('http://127.0.0.1:443/', $response); @@ -337,16 +340,40 @@ public function testSecureHttpsOnHttpStandardPortReturnsUriWithPort() $result = $connector->connect('tls://' . noScheme($socket->getAddress()))->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\nHost: " . noScheme($conn->getRemoteAddress()) . "\r\n\r\n"); - return BufferedSink::createPromise($conn); + return $conn; }); - $response = Block\await($result, $loop, 1.0); + $response = $this->buffer($result, $loop, 1.0); $this->assertContains("HTTP/1.0 200 OK", $response); $this->assertContains('https://127.0.0.1:80/', $response); $socket->close(); } + + protected function buffer(PromiseInterface $promise, LoopInterface $loop, $timeout) + { + return Block\await($promise->then(function (ReadableStreamInterface $stream) { + return new Promise( + function ($resolve, $reject) use ($stream) { + $buffer = ''; + $stream->on('data', function ($chunk) use (&$buffer) { + $buffer .= $chunk; + }); + + $stream->on('error', $reject); + + $stream->on('close', function () use (&$buffer, $resolve) { + $resolve($buffer); + }); + }, + function () use ($stream) { + $stream->close(); + throw new \RuntimeException(); + } + ); + }), $loop, $timeout); + } } function noScheme($uri) diff --git a/tests/HttpBodyStreamTest.php b/tests/HttpBodyStreamTest.php index 9817384a..31e168e0 100644 --- a/tests/HttpBodyStreamTest.php +++ b/tests/HttpBodyStreamTest.php @@ -3,7 +3,7 @@ namespace React\Tests\Http; use React\Http\HttpBodyStream; -use React\Stream\ReadableStream; +use React\Stream\ThroughStream; class HttpBodyStreamTest extends TestCase { @@ -12,7 +12,7 @@ class HttpBodyStreamTest extends TestCase public function setUp() { - $this->input = new ReadableStream(); + $this->input = new ThroughStream(); $this->bodyStream = new HttpBodyStream($this->input, null); } diff --git a/tests/LengthLimitedStreamTest.php b/tests/LengthLimitedStreamTest.php index 8e6375d5..61ecdef6 100644 --- a/tests/LengthLimitedStreamTest.php +++ b/tests/LengthLimitedStreamTest.php @@ -3,7 +3,7 @@ namespace React\Tests\Http; use React\Http\LengthLimitedStream; -use React\Stream\ReadableStream; +use React\Stream\ThroughStream; class LengthLimitedStreamTest extends TestCase { @@ -12,7 +12,7 @@ class LengthLimitedStreamTest extends TestCase public function setUp() { - $this->input = new ReadableStream(); + $this->input = new ThroughStream(); } public function testSimpleChunk() @@ -95,7 +95,7 @@ public function testHandleClose() public function testOutputStreamCanCloseInputStream() { - $input = new ReadableStream(); + $input = new ThroughStream(); $input->on('close', $this->expectCallableOnce()); $stream = new LengthLimitedStream($input, 0); diff --git a/tests/ResponseTest.php b/tests/ResponseTest.php index 4d024956..68627626 100644 --- a/tests/ResponseTest.php +++ b/tests/ResponseTest.php @@ -3,13 +3,13 @@ namespace React\Tests\Http; use React\Http\Response; -use React\Stream\ReadableStream; +use React\Stream\ThroughStream; class ResponseTest extends TestCase { public function testResponseBodyWillBeHttpBodyStream() { - $response = new Response(200, array(), new ReadableStream()); + $response = new Response(200, array(), new ThroughStream()); $this->assertInstanceOf('React\Http\HttpBodyStream', $response->getBody()); } diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 450b90cc..e8f6bc19 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -5,7 +5,7 @@ use React\Http\Server; use Psr\Http\Message\ServerRequestInterface; use React\Http\Response; -use React\Stream\ReadableStream; +use React\Stream\ThroughStream; use React\Promise\Promise; class ServerTest extends TestCase @@ -1611,7 +1611,7 @@ public function testRequestWithoutDefinedLengthWillIgnoreDataEvent() public function testResponseWillBeChunkDecodedByDefault() { - $stream = new ReadableStream(); + $stream = new ThroughStream(); $server = new Server($this->socket, function (ServerRequestInterface $request) use ($stream) { $response = new Response(200, array(), $stream); return \React\Promise\resolve($response); @@ -1680,7 +1680,7 @@ function ($data) use (&$buffer) { public function testOnlyAllowChunkedEncoding() { - $stream = new ReadableStream(); + $stream = new ThroughStream(); $server = new Server($this->socket, function (ServerRequestInterface $request) use ($stream) { $response = new Response( 200, @@ -1977,7 +1977,7 @@ public function testInvalidCallbackFunctionLeadsToException() public function testHttpBodyStreamAsBodyWillStreamData() { - $input = new ReadableStream(); + $input = new ThroughStream(); $server = new Server($this->socket, function (ServerRequestInterface $request) use ($input) { $response = new Response(200, array(), $input); @@ -2012,7 +2012,7 @@ function ($data) use (&$buffer) { public function testHttpBodyStreamWithContentLengthWillStreamTillLength() { - $input = new ReadableStream(); + $input = new ThroughStream(); $server = new Server($this->socket, function (ServerRequestInterface $request) use ($input) { $response = new Response(200, array('Content-Length' => 5), $input); From 1b01370b0cc58c4b98d47cc3073cde1a7e537ce6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 15 May 2017 23:51:15 +0200 Subject: [PATCH 144/456] Simplify buffering tests by using new react/promise-stream release --- composer.json | 1 + tests/FunctionalServerTest.php | 69 ++++++++++++---------------------- 2 files changed, 24 insertions(+), 46 deletions(-) diff --git a/composer.json b/composer.json index 6dd1f48e..fd8f5cb8 100644 --- a/composer.json +++ b/composer.json @@ -18,6 +18,7 @@ }, "require-dev": { "phpunit/phpunit": "^4.8.10||^5.0", + "react/promise-stream": "^0.1.1", "react/socket": "^1.0 || ^0.8 || ^0.7", "clue/block-react": "^1.1" } diff --git a/tests/FunctionalServerTest.php b/tests/FunctionalServerTest.php index 042c4585..cc1344e1 100644 --- a/tests/FunctionalServerTest.php +++ b/tests/FunctionalServerTest.php @@ -15,6 +15,7 @@ use React\EventLoop\LoopInterface; use React\Promise\Promise; use React\Promise\PromiseInterface; +use React\Promise\Stream; class FunctionalServerTest extends TestCase { @@ -31,10 +32,10 @@ public function testPlainHttpOnRandomPort() $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\nHost: " . noScheme($conn->getRemoteAddress()) . "\r\n\r\n"); - return $conn; + return Stream\buffer($conn); }); - $response = $this->buffer($result, $loop, 1.0); + $response = Block\await($result, $loop, 1.0); $this->assertContains("HTTP/1.0 200 OK", $response); $this->assertContains('http://' . noScheme($socket->getAddress()) . '/', $response); @@ -55,10 +56,10 @@ public function testPlainHttpOnRandomPortWithoutHostHeaderUsesSocketUri() $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\n\r\n"); - return $conn; + return Stream\buffer($conn); }); - $response = $this->buffer($result, $loop, 1.0); + $response = Block\await($result, $loop, 1.0); $this->assertContains("HTTP/1.0 200 OK", $response); $this->assertContains('http://' . noScheme($socket->getAddress()) . '/', $response); @@ -79,10 +80,10 @@ public function testPlainHttpOnRandomPortWithOtherHostHeaderTakesPrecedence() $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\nHost: localhost:1000\r\n\r\n"); - return $conn; + return Stream\buffer($conn); }); - $response = $this->buffer($result, $loop, 1.0); + $response = Block\await($result, $loop, 1.0); $this->assertContains("HTTP/1.0 200 OK", $response); $this->assertContains('http://localhost:1000/', $response); @@ -112,10 +113,10 @@ public function testSecureHttpsOnRandomPort() $result = $connector->connect('tls://' . noScheme($socket->getAddress()))->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\nHost: " . noScheme($conn->getRemoteAddress()) . "\r\n\r\n"); - return $conn; + return Stream\buffer($conn); }); - $response = $this->buffer($result, $loop, 1.0); + $response = Block\await($result, $loop, 1.0); $this->assertContains("HTTP/1.0 200 OK", $response); $this->assertContains('https://' . noScheme($socket->getAddress()) . '/', $response); @@ -145,10 +146,10 @@ public function testSecureHttpsOnRandomPortWithoutHostHeaderUsesSocketUri() $result = $connector->connect('tls://' . noScheme($socket->getAddress()))->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\n\r\n"); - return $conn; + return Stream\buffer($conn); }); - $response = $this->buffer($result, $loop, 1.0); + $response = Block\await($result, $loop, 1.0); $this->assertContains("HTTP/1.0 200 OK", $response); $this->assertContains('https://' . noScheme($socket->getAddress()) . '/', $response); @@ -173,10 +174,10 @@ public function testPlainHttpOnStandardPortReturnsUriWithNoPort() $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n"); - return $conn; + return Stream\buffer($conn); }); - $response = $this->buffer($result, $loop, 1.0); + $response = Block\await($result, $loop, 1.0); $this->assertContains("HTTP/1.0 200 OK", $response); $this->assertContains('http://127.0.0.1/', $response); @@ -201,10 +202,10 @@ public function testPlainHttpOnStandardPortWithoutHostHeaderReturnsUriWithNoPort $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\n\r\n"); - return $conn; + return Stream\buffer($conn); }); - $response = $this->buffer($result, $loop, 1.0); + $response = Block\await($result, $loop, 1.0); $this->assertContains("HTTP/1.0 200 OK", $response); $this->assertContains('http://127.0.0.1/', $response); @@ -238,10 +239,10 @@ public function testSecureHttpsOnStandardPortReturnsUriWithNoPort() $result = $connector->connect('tls://' . noScheme($socket->getAddress()))->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n"); - return $conn; + return Stream\buffer($conn); }); - $response = $this->buffer($result, $loop, 1.0); + $response = Block\await($result, $loop, 1.0); $this->assertContains("HTTP/1.0 200 OK", $response); $this->assertContains('https://127.0.0.1/', $response); @@ -275,10 +276,10 @@ public function testSecureHttpsOnStandardPortWithoutHostHeaderUsesSocketUri() $result = $connector->connect('tls://' . noScheme($socket->getAddress()))->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\n\r\n"); - return $conn; + return Stream\buffer($conn); }); - $response = $this->buffer($result, $loop, 1.0); + $response = Block\await($result, $loop, 1.0); $this->assertContains("HTTP/1.0 200 OK", $response); $this->assertContains('https://127.0.0.1/', $response); @@ -303,10 +304,10 @@ public function testPlainHttpOnHttpsStandardPortReturnsUriWithPort() $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\nHost: " . noScheme($conn->getRemoteAddress()) . "\r\n\r\n"); - return $conn; + return Stream\buffer($conn); }); - $response = $this->buffer($result, $loop, 1.0); + $response = Block\await($result, $loop, 1.0); $this->assertContains("HTTP/1.0 200 OK", $response); $this->assertContains('http://127.0.0.1:443/', $response); @@ -340,40 +341,16 @@ public function testSecureHttpsOnHttpStandardPortReturnsUriWithPort() $result = $connector->connect('tls://' . noScheme($socket->getAddress()))->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\nHost: " . noScheme($conn->getRemoteAddress()) . "\r\n\r\n"); - return $conn; + return Stream\buffer($conn); }); - $response = $this->buffer($result, $loop, 1.0); + $response = Block\await($result, $loop, 1.0); $this->assertContains("HTTP/1.0 200 OK", $response); $this->assertContains('https://127.0.0.1:80/', $response); $socket->close(); } - - protected function buffer(PromiseInterface $promise, LoopInterface $loop, $timeout) - { - return Block\await($promise->then(function (ReadableStreamInterface $stream) { - return new Promise( - function ($resolve, $reject) use ($stream) { - $buffer = ''; - $stream->on('data', function ($chunk) use (&$buffer) { - $buffer .= $chunk; - }); - - $stream->on('error', $reject); - - $stream->on('close', function () use (&$buffer, $resolve) { - $resolve($buffer); - }); - }, - function () use ($stream) { - $stream->close(); - throw new \RuntimeException(); - } - ); - }), $loop, $timeout); - } } function noScheme($uri) From 193cec2ccd94572d6a28396b53aeb989f9e0ce2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 16 May 2017 00:49:54 +0200 Subject: [PATCH 145/456] Ignore HHVM test failures for now until Travis tests work again --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index f67b7d54..fcd3a2d6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,6 +16,8 @@ matrix: - php: 7.0 env: - DEPENDENCIES=lowest + allow_failures: + - php: hhvm install: - composer install --no-interaction From baa070162e0eee673b5bbd0ca5932fad80e73b85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 21 May 2017 20:50:43 +0200 Subject: [PATCH 146/456] Automatically cancel pending promises once client connection closes --- README.md | 5 ++ composer.json | 2 +- ...unt-visitors.php => 02-count-visitors.php} | 0 .../{02-client-ip.php => 03-client-ip.php} | 0 ...y-parameter.php => 04-query-parameter.php} | 0 ...ie-handling.php => 05-cookie-handling.php} | 0 examples/06-sleep.php | 29 +++++++ ...am-response.php => 08-stream-response.php} | 0 ...ream-request.php => 09-stream-request.php} | 0 examples/22-connect-proxy.php | 9 +-- src/Server.php | 14 +++- tests/ServerTest.php | 77 ++++++++++++++++--- 12 files changed, 116 insertions(+), 20 deletions(-) rename examples/{04-count-visitors.php => 02-count-visitors.php} (100%) rename examples/{02-client-ip.php => 03-client-ip.php} (100%) rename examples/{03-query-parameter.php => 04-query-parameter.php} (100%) rename examples/{06-cookie-handling.php => 05-cookie-handling.php} (100%) create mode 100644 examples/06-sleep.php rename examples/{05-stream-response.php => 08-stream-response.php} (100%) rename examples/{06-stream-request.php => 09-stream-request.php} (100%) diff --git a/README.md b/README.md index 1b6c137e..3a897f20 100644 --- a/README.md +++ b/README.md @@ -471,6 +471,11 @@ This example shows that you need a promise, if your response needs time to created. The `ReactPHP Promise` will resolve in a `Response` object when the request body ends. +If the client closes the connection while the promise is still pending, the +promise will automatically be cancelled. +The promise cancellation handler can be used to clean up any pending resources +allocated in this case (if applicable). +If a promise is resolved after the client closes, it will simply be ignored. The `Response` class in this project supports to add an instance which implements the [ReactPHP ReadableStreamInterface](https://github.com/reactphp/stream#readablestreaminterface) diff --git a/composer.json b/composer.json index fd8f5cb8..e73e29c7 100644 --- a/composer.json +++ b/composer.json @@ -8,7 +8,7 @@ "ringcentral/psr7": "^1.2", "react/socket": "^1.0 || ^0.8 || ^0.7 || ^0.6 || ^0.5", "react/stream": "^1.0 || ^0.7 || ^0.6 || ^0.5 || ^0.4.6", - "react/promise": "^2.0 || ^1.1", + "react/promise": "^2.1 || ^1.2.1", "evenement/evenement": "^2.0 || ^1.0" }, "autoload": { diff --git a/examples/04-count-visitors.php b/examples/02-count-visitors.php similarity index 100% rename from examples/04-count-visitors.php rename to examples/02-count-visitors.php diff --git a/examples/02-client-ip.php b/examples/03-client-ip.php similarity index 100% rename from examples/02-client-ip.php rename to examples/03-client-ip.php diff --git a/examples/03-query-parameter.php b/examples/04-query-parameter.php similarity index 100% rename from examples/03-query-parameter.php rename to examples/04-query-parameter.php diff --git a/examples/06-cookie-handling.php b/examples/05-cookie-handling.php similarity index 100% rename from examples/06-cookie-handling.php rename to examples/05-cookie-handling.php diff --git a/examples/06-sleep.php b/examples/06-sleep.php new file mode 100644 index 00000000..9fb75542 --- /dev/null +++ b/examples/06-sleep.php @@ -0,0 +1,29 @@ +addTimer(1.5, function() use ($loop, $resolve) { + $response = new Response( + 200, + array('Content-Type' => 'text/plain'), + "Hello world" + ); + $resolve($response); + }); + }); +}); + +echo 'Listening on http://' . $socket->getAddress() . PHP_EOL; + +$loop->run(); diff --git a/examples/05-stream-response.php b/examples/08-stream-response.php similarity index 100% rename from examples/05-stream-response.php rename to examples/08-stream-response.php diff --git a/examples/06-stream-request.php b/examples/09-stream-request.php similarity index 100% rename from examples/06-stream-request.php rename to examples/09-stream-request.php diff --git a/examples/22-connect-proxy.php b/examples/22-connect-proxy.php index 03939d29..c9e59c0e 100644 --- a/examples/22-connect-proxy.php +++ b/examples/22-connect-proxy.php @@ -32,7 +32,7 @@ }); // try to connect to given target host - $promise = $connector->connect($request->getRequestTarget())->then( + return $connector->connect($request->getRequestTarget())->then( function (ConnectionInterface $remote) use ($body, &$buffer) { // connection established => forward data $body->pipe($remote); @@ -57,13 +57,6 @@ function ($e) { ); } ); - - // cancel pending connection if request closes prematurely - $body->on('close', function () use ($promise) { - $promise->cancel(); - }); - - return $promise; }); //$server->on('error', 'printf'); diff --git a/src/Server.php b/src/Server.php index 61d68953..637f8450 100644 --- a/src/Server.php +++ b/src/Server.php @@ -10,6 +10,7 @@ use React\Promise\Promise; use RingCentral\Psr7 as Psr7Implementation; use Psr\Http\Message\ServerRequestInterface; +use React\Promise\CancellablePromiseInterface; /** * The `Server` class is responsible for handling incoming connections and then @@ -210,10 +211,19 @@ public function handleRequest(ConnectionInterface $conn, ServerRequestInterface } $callback = $this->callback; - $promise = new Promise(function ($resolve, $reject) use ($callback, $request) { - $resolve($callback($request)); + $cancel = null; + $promise = new Promise(function ($resolve, $reject) use ($callback, $request, &$cancel) { + $cancel = $callback($request); + $resolve($cancel); }); + // cancel pending promise once connection closes + if ($cancel instanceof CancellablePromiseInterface) { + $conn->on('close', function () use ($cancel) { + $cancel->cancel(); + }); + } + $that = $this; $promise->then( function ($response) use ($that, $conn, $request) { diff --git a/tests/ServerTest.php b/tests/ServerTest.php index e8f6bc19..f485b256 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -423,7 +423,7 @@ public function testRequestPauseWillbeForwardedToConnection() { $server = new Server($this->socket, function (ServerRequestInterface $request) { $request->getBody()->pause(); - return \React\Promise\resolve(new Response()); + return new Response(); }); $this->connection->expects($this->once())->method('pause'); @@ -442,7 +442,7 @@ public function testRequestResumeWillbeForwardedToConnection() { $server = new Server($this->socket, function (ServerRequestInterface $request) { $request->getBody()->resume(); - return \React\Promise\resolve(new Response()); + return new Response(); }); $this->connection->expects($this->once())->method('resume'); @@ -456,7 +456,7 @@ public function testRequestCloseWillPauseConnection() { $server = new Server($this->socket, function (ServerRequestInterface $request) { $request->getBody()->close(); - return \React\Promise\resolve(new Response()); + return new Response(); }); $this->connection->expects($this->once())->method('pause'); @@ -472,7 +472,7 @@ public function testRequestPauseAfterCloseWillNotBeForwarded() $request->getBody()->close(); $request->getBody()->pause();# - return \React\Promise\resolve(new Response()); + return new Response(); }); $this->connection->expects($this->once())->method('pause'); @@ -488,7 +488,7 @@ public function testRequestResumeAfterCloseWillNotBeForwarded() $request->getBody()->close(); $request->getBody()->resume(); - return \React\Promise\resolve(new Response()); + return new Response(); }); $this->connection->expects($this->once())->method('pause'); @@ -506,7 +506,7 @@ public function testRequestEventWithoutBodyWillNotEmitData() $server = new Server($this->socket, function (ServerRequestInterface $request) use ($never) { $request->getBody()->on('data', $never); - return \React\Promise\resolve(new Response()); + return new Response(); }); $this->socket->emit('connection', array($this->connection)); @@ -522,7 +522,7 @@ public function testRequestEventWithSecondDataEventWillEmitBodyData() $server = new Server($this->socket, function (ServerRequestInterface $request) use ($once) { $request->getBody()->on('data', $once); - return \React\Promise\resolve(new Response()); + return new Response(); }); $this->socket->emit('connection', array($this->connection)); @@ -543,7 +543,7 @@ public function testRequestEventWithPartialBodyWillEmitData() $server = new Server($this->socket, function (ServerRequestInterface $request) use ($once) { $request->getBody()->on('data', $once); - return \React\Promise\resolve(new Response()); + return new Response(); }); $this->socket->emit('connection', array($this->connection)); @@ -563,7 +563,7 @@ public function testRequestEventWithPartialBodyWillEmitData() public function testResponseContainsPoweredByHeader() { $server = new Server($this->socket, function (ServerRequestInterface $request) { - return \React\Promise\resolve(new Response()); + return new Response(); }); $buffer = ''; @@ -587,6 +587,65 @@ function ($data) use (&$buffer) { $this->assertContains("\r\nX-Powered-By: React/alpha\r\n", $buffer); } + public function testPendingPromiseWillNotSendAnything() + { + $never = $this->expectCallableNever(); + + $server = new Server($this->socket, function (ServerRequestInterface $request) use ($never) { + return new Promise(function () { }, $never); + }); + + $buffer = ''; + + $this->connection + ->expects($this->any()) + ->method('write') + ->will( + $this->returnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } + ) + ); + + $this->socket->emit('connection', array($this->connection)); + + $data = $this->createGetRequest(); + $this->connection->emit('data', array($data)); + + $this->assertEquals('', $buffer); + } + + public function testPendingPromiseWillBeCancelledIfConnectionCloses() + { + $once = $this->expectCallableOnce(); + + $server = new Server($this->socket, function (ServerRequestInterface $request) use ($once) { + return new Promise(function () { }, $once); + }); + + $buffer = ''; + + $this->connection + ->expects($this->any()) + ->method('write') + ->will( + $this->returnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } + ) + ); + + $this->socket->emit('connection', array($this->connection)); + + $data = $this->createGetRequest(); + $this->connection->emit('data', array($data)); + $this->connection->emit('close'); + + $this->assertEquals('', $buffer); + } + public function testResponseContainsSameRequestProtocolVersionAndChunkedBodyForHttp11() { $server = new Server($this->socket, function (ServerRequestInterface $request) { From 693c3c8f79ee123808515f9250ca5ea585d5e4c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 22 May 2017 21:11:44 +0200 Subject: [PATCH 147/456] Send empty response body for closed response body stream --- README.md | 3 ++ src/Server.php | 21 ++++++++---- tests/FunctionalServerTest.php | 28 +++++++++++++++ tests/ServerTest.php | 62 ++++++++++++++++++++++++++++++++++ 4 files changed, 107 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 3a897f20..ea27c075 100644 --- a/README.md +++ b/README.md @@ -507,6 +507,9 @@ This is just a example you could use of the streaming, you could also send a big amount of data via little chunks or use it for body data that needs to calculated. +If the request handler resolves with a response stream that is already closed, +it will simply send an empty response body. + If the response body is a `string`, a `Content-Length` header will be added automatically. If the response body is a ReactPHP `ReadableStreamInterface` and you do not diff --git a/src/Server.php b/src/Server.php index 637f8450..291e5da2 100644 --- a/src/Server.php +++ b/src/Server.php @@ -339,15 +339,22 @@ private function handleResponseBody(ResponseInterface $response, ConnectionInter return $connection->end(); } - $body = $response->getBody(); - $stream = $body; - - if ($response->getHeaderLine('Transfer-Encoding') === 'chunked') { - $stream = new ChunkedEncoder($body); - } + $stream = $response->getBody(); $connection->write(Psr7Implementation\str($response)); - $stream->pipe($connection); + if ($stream->isReadable()) { + if ($response->getHeaderLine('Transfer-Encoding') === 'chunked') { + $stream = new ChunkedEncoder($stream); + } + + $stream->pipe($connection); + } else { + if ($response->getHeaderLine('Transfer-Encoding') === 'chunked') { + $connection->write("0\r\n\r\n"); + } + + $connection->end(); + } } /** diff --git a/tests/FunctionalServerTest.php b/tests/FunctionalServerTest.php index cc1344e1..716cf513 100644 --- a/tests/FunctionalServerTest.php +++ b/tests/FunctionalServerTest.php @@ -16,6 +16,7 @@ use React\Promise\Promise; use React\Promise\PromiseInterface; use React\Promise\Stream; +use React\Stream\ThroughStream; class FunctionalServerTest extends TestCase { @@ -351,6 +352,33 @@ public function testSecureHttpsOnHttpStandardPortReturnsUriWithPort() $socket->close(); } + + public function testClosedStreamFromRequestHandlerWillBeSendEmptyBody() + { + $loop = Factory::create(); + $socket = new Socket(0, $loop); + $connector = new Connector($loop); + + $stream = new ThroughStream(); + $stream->close(); + + $server = new Server($socket, function (RequestInterface $request) use ($stream) { + return new Response(200, array(), $stream); + }); + + $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) use ($loop) { + $conn->write("GET / HTTP/1.0\r\n\r\n"); + + return Stream\buffer($conn); + }); + + $response = Block\await($result, $loop, 1.0); + + $this->assertStringStartsWith("HTTP/1.0 200 OK", $response); + $this->assertStringEndsWith("\r\n\r\n", $response); + + $socket->close(); + } } function noScheme($uri) diff --git a/tests/ServerTest.php b/tests/ServerTest.php index f485b256..567ebfec 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -646,6 +646,68 @@ function ($data) use (&$buffer) { $this->assertEquals('', $buffer); } + public function testStreamAlreadyClosedWillSendEmptyBodyChunkedEncoded() + { + $stream = new ThroughStream(); + $stream->close(); + + $server = new Server($this->socket, function (ServerRequestInterface $request) use ($stream) { + return new Response(200, array(), $stream); + }); + + $buffer = ''; + + $this->connection + ->expects($this->any()) + ->method('write') + ->will( + $this->returnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } + ) + ); + + $this->socket->emit('connection', array($this->connection)); + + $data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"; + $this->connection->emit('data', array($data)); + + $this->assertStringStartsWith("HTTP/1.1 200 OK\r\n", $buffer); + $this->assertStringEndsWith("\r\n\r\n0\r\n\r\n", $buffer); + } + + public function testStreamAlreadyClosedWillSendEmptyBodyPlainHttp10() + { + $stream = new ThroughStream(); + $stream->close(); + + $server = new Server($this->socket, function (ServerRequestInterface $request) use ($stream) { + return new Response(200, array(), $stream); + }); + + $buffer = ''; + + $this->connection + ->expects($this->any()) + ->method('write') + ->will( + $this->returnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } + ) + ); + + $this->socket->emit('connection', array($this->connection)); + + $data = "GET / HTTP/1.0\r\nHost: localhost\r\n\r\n"; + $this->connection->emit('data', array($data)); + + $this->assertStringStartsWith("HTTP/1.0 200 OK\r\n", $buffer); + $this->assertStringEndsWith("\r\n\r\n", $buffer); +} + public function testResponseContainsSameRequestProtocolVersionAndChunkedBodyForHttp11() { $server = new Server($this->socket, function (ServerRequestInterface $request) { From df000d5bc0bf40ddcac1ee59f4a91ed5457d4a6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 23 May 2017 13:11:56 +0200 Subject: [PATCH 148/456] Automatically close response stream if connection closes --- README.md | 6 ++ src/ChunkedEncoder.php | 4 +- src/Server.php | 13 +++++ tests/FunctionalServerTest.php | 69 +++++++++++++++++++++- tests/ServerTest.php | 102 ++++++++++++++++++++++++++++++++- 5 files changed, 188 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index ea27c075..2d524c0d 100644 --- a/README.md +++ b/README.md @@ -509,6 +509,12 @@ or use it for body data that needs to calculated. If the request handler resolves with a response stream that is already closed, it will simply send an empty response body. +If the client closes the connection while the stream is still open, the +response stream will automatically be closed. +If a promise is resolved with a streaming body after the client closes, the +response stream will automatically be closed. +The `close` event can be used to clean up any pending resources allocated +in this case (if applicable). If the response body is a `string`, a `Content-Length` header will be added automatically. diff --git a/src/ChunkedEncoder.php b/src/ChunkedEncoder.php index 69d88ac7..eaa453c8 100644 --- a/src/ChunkedEncoder.php +++ b/src/ChunkedEncoder.php @@ -52,11 +52,9 @@ public function close() } $this->closed = true; - - $this->readable = false; + $this->input->close(); $this->emit('close'); - $this->removeAllListeners(); } diff --git a/src/Server.php b/src/Server.php index 291e5da2..42fc509c 100644 --- a/src/Server.php +++ b/src/Server.php @@ -341,12 +341,25 @@ private function handleResponseBody(ResponseInterface $response, ConnectionInter $stream = $response->getBody(); + // close response stream if connection is already closed + if (!$connection->isWritable()) { + return $stream->close(); + } + $connection->write(Psr7Implementation\str($response)); + if ($stream->isReadable()) { if ($response->getHeaderLine('Transfer-Encoding') === 'chunked') { $stream = new ChunkedEncoder($stream); } + // Close response stream once connection closes. + // Note that this TCP/IP close detection may take some time, + // in particular this may only fire on a later read/write attempt + // because we stop/pause reading from the connection once the + // request has been processed. + $connection->on('close', array($stream, 'close')); + $stream->pipe($connection); } else { if ($response->getHeaderLine('Transfer-Encoding') === 'chunked') { diff --git a/tests/FunctionalServerTest.php b/tests/FunctionalServerTest.php index 716cf513..38716c75 100644 --- a/tests/FunctionalServerTest.php +++ b/tests/FunctionalServerTest.php @@ -353,7 +353,7 @@ public function testSecureHttpsOnHttpStandardPortReturnsUriWithPort() $socket->close(); } - public function testClosedStreamFromRequestHandlerWillBeSendEmptyBody() + public function testClosedStreamFromRequestHandlerWillSendEmptyBody() { $loop = Factory::create(); $socket = new Socket(0, $loop); @@ -379,6 +379,73 @@ public function testClosedStreamFromRequestHandlerWillBeSendEmptyBody() $socket->close(); } + + public function testStreamFromRequestHandlerWillBeClosedIfConnectionClosesWhileSendingBody() + { + $loop = Factory::create(); + $socket = new Socket(0, $loop); + $connector = new Connector($loop); + + $stream = new ThroughStream(); + $stream->on('close', $this->expectCallableOnce()); + + $server = new Server($socket, function (RequestInterface $request) use ($stream) { + return new Response(200, array(), $stream); + }); + + $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) use ($loop) { + $conn->write("GET / HTTP/1.0\r\nContent-Length: 100\r\n\r\n"); + + $loop->addTimer(0.1, function() use ($conn) { + $conn->end(); + }); + + return Stream\buffer($conn); + }); + + $response = Block\await($result, $loop, 1.0); + + $this->assertStringStartsWith("HTTP/1.0 200 OK", $response); + $this->assertStringEndsWith("\r\n\r\n", $response); + + $socket->close(); + } + + public function testStreamFromRequestHandlerWillBeClosedIfConnectionClosesButWillOnlyBeDetectedOnNextWrite() + { + $loop = Factory::create(); + $socket = new Socket(0, $loop); + $connector = new Connector($loop); + + $stream = new ThroughStream(); + $stream->on('close', $this->expectCallableOnce()); + + $server = new Server($socket, function (RequestInterface $request) use ($stream) { + return new Response(200, array(), $stream); + }); + + $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) use ($loop) { + $conn->write("GET / HTTP/1.0\r\n\r\n"); + + $loop->addTimer(0.1, function() use ($conn) { + $conn->end(); + }); + + return Stream\buffer($conn); + }); + + $response = Block\await($result, $loop, 1.0); + + $stream->write('nope'); + Block\sleep(0.1, $loop); + $stream->write('nope'); + Block\sleep(0.1, $loop); + + $this->assertStringStartsWith("HTTP/1.0 200 OK", $response); + $this->assertStringEndsWith("\r\n\r\n", $response); + + $socket->close(); + } } function noScheme($uri) diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 567ebfec..7c2e98fa 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -677,7 +677,39 @@ function ($data) use (&$buffer) { $this->assertStringEndsWith("\r\n\r\n0\r\n\r\n", $buffer); } - public function testStreamAlreadyClosedWillSendEmptyBodyPlainHttp10() + public function testResponseStreamEndingWillSendEmptyBodyChunkedEncoded() + { + $stream = new ThroughStream(); + + $server = new Server($this->socket, function (ServerRequestInterface $request) use ($stream) { + return new Response(200, array(), $stream); + }); + + $buffer = ''; + + $this->connection + ->expects($this->any()) + ->method('write') + ->will( + $this->returnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } + ) + ); + + $this->socket->emit('connection', array($this->connection)); + + $data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"; + $this->connection->emit('data', array($data)); + + $stream->end(); + + $this->assertStringStartsWith("HTTP/1.1 200 OK\r\n", $buffer); + $this->assertStringEndsWith("\r\n\r\n0\r\n\r\n", $buffer); + } + + public function testResponseStreamAlreadyClosedWillSendEmptyBodyPlainHttp10() { $stream = new ThroughStream(); $stream->close(); @@ -706,7 +738,73 @@ function ($data) use (&$buffer) { $this->assertStringStartsWith("HTTP/1.0 200 OK\r\n", $buffer); $this->assertStringEndsWith("\r\n\r\n", $buffer); -} + } + + public function testResponseStreamWillBeClosedIfConnectionIsAlreadyClosed() + { + $stream = new ThroughStream(); + $stream->on('close', $this->expectCallableOnce()); + + $server = new Server($this->socket, function (ServerRequestInterface $request) use ($stream) { + return new Response(200, array(), $stream); + }); + + $buffer = ''; + + $this->connection + ->expects($this->any()) + ->method('write') + ->will( + $this->returnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } + ) + ); + + $this->connection = $this->getMockBuilder('React\Socket\Connection') + ->disableOriginalConstructor() + ->setMethods( + array( + 'write', + 'end', + 'close', + 'pause', + 'resume', + 'isReadable', + 'isWritable', + 'getRemoteAddress', + 'getLocalAddress', + 'pipe' + ) + ) + ->getMock(); + + $this->connection->expects($this->once())->method('isWritable')->willReturn(false); + $this->connection->expects($this->never())->method('write'); + $this->connection->expects($this->never())->method('write'); + + $this->socket->emit('connection', array($this->connection)); + + $data = $this->createGetRequest(); + $this->connection->emit('data', array($data)); + } + + public function testResponseStreamWillBeClosedIfConnectionEmitsCloseEvent() + { + $stream = new ThroughStream(); + $stream->on('close', $this->expectCallableOnce()); + + $server = new Server($this->socket, function (ServerRequestInterface $request) use ($stream) { + return new Response(200, array(), $stream); + }); + + $this->socket->emit('connection', array($this->connection)); + + $data = $this->createGetRequest(); + $this->connection->emit('data', array($data)); + $this->connection->emit('close'); + } public function testResponseContainsSameRequestProtocolVersionAndChunkedBodyForHttp11() { From 43fb5cad0819bb912d69a4abf12d4256f2ea6e8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 22 May 2017 08:49:28 +0200 Subject: [PATCH 149/456] Validate request-target of CONNECT in RequestHeaderParser --- src/RequestHeaderParser.php | 2 ++ src/Server.php | 7 ------- tests/RequestHeaderParserTest.php | 16 ++++++++++++++++ 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/RequestHeaderParser.php b/src/RequestHeaderParser.php index 0b75ac01..02d67e32 100644 --- a/src/RequestHeaderParser.php +++ b/src/RequestHeaderParser.php @@ -79,6 +79,8 @@ private function parseRequest($data) $originalTarget = $parts[1]; $parts[1] = '/'; $headers = implode(' ', $parts); + } else { + throw new \InvalidArgumentException('CONNECT method MUST use authority-form request target'); } } diff --git a/src/Server.php b/src/Server.php index 42fc509c..0cb732f9 100644 --- a/src/Server.php +++ b/src/Server.php @@ -162,13 +162,6 @@ public function handleRequest(ConnectionInterface $conn, ServerRequestInterface $contentLength = 0; $stream = new CloseProtectionStream($conn); if ($request->getMethod() === 'CONNECT') { - // CONNECT method MUST use authority-form request target - $parts = parse_url('tcp://' . $request->getRequestTarget()); - if (!isset($parts['scheme'], $parts['host'], $parts['port']) || count($parts) !== 3) { - $this->emit('error', array(new \InvalidArgumentException('CONNECT method MUST use authority-form request target'))); - return $this->writeError($conn, 400); - } - // CONNECT uses undelimited body until connection closes $request = $request->withoutHeader('Transfer-Encoding'); $request = $request->withoutHeader('Content-Length'); diff --git a/tests/RequestHeaderParserTest.php b/tests/RequestHeaderParserTest.php index 956bf4ff..f4b20a7f 100644 --- a/tests/RequestHeaderParserTest.php +++ b/tests/RequestHeaderParserTest.php @@ -261,6 +261,22 @@ public function testInvalidAbsoluteFormWithHostHeaderEmpty() $this->assertSame('Invalid Host header value', $error->getMessage()); } + public function testInvalidConnectRequestWithNonAuthorityForm() + { + $error = null; + + $parser = new RequestHeaderParser(); + $parser->on('headers', $this->expectCallableNever()); + $parser->on('error', function ($message) use (&$error) { + $error = $message; + }); + + $parser->feed("CONNECT http://example.com:8080/ HTTP/1.1\r\nHost: example.com:8080\r\n\r\n"); + + $this->assertInstanceOf('InvalidArgumentException', $error); + $this->assertSame('CONNECT method MUST use authority-form request target', $error->getMessage()); + } + public function testInvalidHttpVersion() { $error = null; From f3b1d4ae1f87e8748571672901211322f61a2d0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 22 May 2017 15:02:02 +0200 Subject: [PATCH 150/456] Use duplex stream response body for CONNECT requests --- README.md | 36 +++++++---- examples/08-stream-response.php | 4 ++ examples/22-connect-proxy.php | 19 +----- src/HttpBodyStream.php | 2 +- src/Server.php | 32 ++++++---- tests/FunctionalServerTest.php | 106 ++++++++++++++++++++++++++++++++ tests/ServerTest.php | 48 +++++++++++++++ 7 files changed, 205 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index 2d524c0d..c1353347 100644 --- a/README.md +++ b/README.md @@ -370,13 +370,6 @@ Allowed). Note that if you want to handle this method, the client MAY send a different request-target than the `Host` header value (such as removing default ports) and the request-target MUST take precendence when forwarding. - The HTTP specs define an opaque "tunneling mode" for this method and make no - use of the message body. - For consistency reasons, this library uses the message body of the request and - response for tunneled application data. - This implies that that a `2xx` (Successful) response to a `CONNECT` request - can in fact use a streaming response body for the tunneled application data. - See also [example #21](examples) for more details. The `getCookieParams(): string[]` method can be used to get all cookies sent with the current request. @@ -562,14 +555,35 @@ Modified) status code MAY include these headers even though the message does not contain a response body, because these header would apply to the message if the same request would have used an (unconditional) `GET`. +> Note that special care has to be taken if you use a body stream instance that + implements ReactPHP's + [`DuplexStreamInterface`](https://github.com/reactphp/stream#duplexstreaminterface) + (such as the `ThroughStream` in the above example). + +> For *most* cases, this will simply only consume its readable side and forward + (send) any data that is emitted by the stream, thus entirely ignoring the + writable side of the stream. + If however this is a `2xx` (Successful) response to a `CONNECT` method, it + will also *write* data to the writable side of the stream. + This can be avoided by either rejecting all requests with the `CONNECT` + method (which is what most *normal* origin HTTP servers would likely do) or + or ensuring that only ever an instance of `ReadableStreamInterface` is + used. + > The `CONNECT` method is useful in a tunneling setup (HTTPS proxy) and not - something most HTTP servers would want to care about. + something most origin HTTP servers would want to care about. The HTTP specs define an opaque "tunneling mode" for this method and make no use of the message body. - For consistency reasons, this library uses the message body of the request and - response for tunneled application data. + For consistency reasons, this library uses a `DuplexStreamInterface` in the + response body for tunneled application data. This implies that that a `2xx` (Successful) response to a `CONNECT` request - can in fact use a streaming response body for the tunneled application data. + can in fact use a streaming response body for the tunneled application data, + so that any raw data the client sends over the connection will be piped + through the writable stream for consumption. + Note that while the HTTP specs make no use of the request body for `CONNECT` + requests, one may still be present. Normal request body processing applies + here and the connection will only turn to "tunneling mode" after the request + body has been processed (which should be empty in most cases). See also [example #22](examples) for more details. A `Date` header will be automatically added with the system date and time if none is given. diff --git a/examples/08-stream-response.php b/examples/08-stream-response.php index b4ef6962..e563be38 100644 --- a/examples/08-stream-response.php +++ b/examples/08-stream-response.php @@ -12,6 +12,10 @@ $socket = new Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); $server = new \React\Http\Server($socket, function (ServerRequestInterface $request) use ($loop) { + if ($request->getMethod() !== 'GET' || $request->getUri()->getPath() !== '/') { + return new Response(404); + } + $stream = new ThroughStream(); $timer = $loop->addPeriodicTimer(0.5, function () use ($stream) { diff --git a/examples/22-connect-proxy.php b/examples/22-connect-proxy.php index c9e59c0e..7e7acd2c 100644 --- a/examples/22-connect-proxy.php +++ b/examples/22-connect-proxy.php @@ -22,27 +22,10 @@ ); } - // pause consuming request body - $body = $request->getBody(); - $body->pause(); - - $buffer = ''; - $body->on('data', function ($chunk) use (&$buffer) { - $buffer .= $chunk; - }); - // try to connect to given target host return $connector->connect($request->getRequestTarget())->then( - function (ConnectionInterface $remote) use ($body, &$buffer) { + function (ConnectionInterface $remote) { // connection established => forward data - $body->pipe($remote); - $body->resume(); - - if ($buffer !== '') { - $remote->write($buffer); - $buffer = ''; - } - return new Response( 200, array(), diff --git a/src/HttpBodyStream.php b/src/HttpBodyStream.php index a77622ad..8f44f4fc 100644 --- a/src/HttpBodyStream.php +++ b/src/HttpBodyStream.php @@ -17,7 +17,7 @@ */ class HttpBodyStream extends EventEmitter implements StreamInterface, ReadableStreamInterface { - private $input; + public $input; private $closed = false; private $size; diff --git a/src/Server.php b/src/Server.php index 0cb732f9..8b42a7e9 100644 --- a/src/Server.php +++ b/src/Server.php @@ -11,6 +11,7 @@ use RingCentral\Psr7 as Psr7Implementation; use Psr\Http\Message\ServerRequestInterface; use React\Promise\CancellablePromiseInterface; +use React\Stream\WritableStreamInterface; /** * The `Server` class is responsible for handling incoming connections and then @@ -161,18 +162,7 @@ public function handleRequest(ConnectionInterface $conn, ServerRequestInterface { $contentLength = 0; $stream = new CloseProtectionStream($conn); - if ($request->getMethod() === 'CONNECT') { - // CONNECT uses undelimited body until connection closes - $request = $request->withoutHeader('Transfer-Encoding'); - $request = $request->withoutHeader('Content-Length'); - $contentLength = null; - - // emit end event before the actual close event - $stream->on('close', function () use ($stream) { - $stream->emit('end'); - }); - } else if ($request->hasHeader('Transfer-Encoding')) { - + 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); @@ -322,6 +312,24 @@ public function handleResponse(ConnectionInterface $connection, ServerRequestInt $response = $response->withBody(Psr7Implementation\stream_for('')); } + // 2xx reponse to CONNECT forwards tunneled application data through duplex stream + $body = $response->getBody(); + if ($request->getMethod() === 'CONNECT' && $code >= 200 && $code < 300 && $body instanceof HttpBodyStream && $body->input instanceof WritableStreamInterface) { + if ($request->getBody()->isReadable()) { + // request is still streaming => wait for request close before forwarding following data from connection + $request->getBody()->on('close', function () use ($connection, $body) { + if ($body->input->isWritable()) { + $connection->pipe($body->input); + $connection->resume(); + } + }); + } elseif ($body->input->isWritable()) { + // request already closed => forward following data from connection + $connection->pipe($body->input); + $connection->resume(); + } + } + $this->handleResponseBody($response, $connection); } diff --git a/tests/FunctionalServerTest.php b/tests/FunctionalServerTest.php index 38716c75..3579db09 100644 --- a/tests/FunctionalServerTest.php +++ b/tests/FunctionalServerTest.php @@ -446,6 +446,112 @@ public function testStreamFromRequestHandlerWillBeClosedIfConnectionClosesButWil $socket->close(); } + + public function testConnectWithThroughStreamReturnsDataAsGiven() + { + $loop = Factory::create(); + $socket = new Socket(0, $loop); + $connector = new Connector($loop); + + $server = new Server($socket, function (RequestInterface $request) use ($loop) { + $stream = new ThroughStream(); + + $loop->addTimer(0.1, function () use ($stream) { + $stream->end(); + }); + + return new Response(200, array(), $stream); + }); + + $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { + $conn->write("CONNECT example.com:80 HTTP/1.1\r\nHost: example.com:80\r\n\r\n"); + + $conn->once('data', function () use ($conn) { + $conn->write('hello'); + $conn->write('world'); + }); + + return Stream\buffer($conn); + }); + + $response = Block\await($result, $loop, 1.0); + + $this->assertStringStartsWith("HTTP/1.1 200 OK\r\n", $response); + $this->assertStringEndsWith("\r\n\r\nhelloworld", $response); + + $socket->close(); + } + + public function testConnectWithThroughStreamReturnedFromPromiseReturnsDataAsGiven() + { + $loop = Factory::create(); + $socket = new Socket(0, $loop); + $connector = new Connector($loop); + + $server = new Server($socket, function (RequestInterface $request) use ($loop) { + $stream = new ThroughStream(); + + $loop->addTimer(0.1, function () use ($stream) { + $stream->end(); + }); + + return new Promise(function ($resolve) use ($loop, $stream) { + $loop->addTimer(0.001, function () use ($resolve, $stream) { + $resolve(new Response(200, array(), $stream)); + }); + }); + }); + + $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { + $conn->write("CONNECT example.com:80 HTTP/1.1\r\nHost: example.com:80\r\n\r\n"); + + $conn->once('data', function () use ($conn) { + $conn->write('hello'); + $conn->write('world'); + }); + + return Stream\buffer($conn); + }); + + $response = Block\await($result, $loop, 1.0); + + $this->assertStringStartsWith("HTTP/1.1 200 OK\r\n", $response); + $this->assertStringEndsWith("\r\n\r\nhelloworld", $response); + + $socket->close(); + } + + public function testConnectWithClosedThroughStreamReturnsNoData() + { + $loop = Factory::create(); + $socket = new Socket(0, $loop); + $connector = new Connector($loop); + + $server = new Server($socket, function (RequestInterface $request) { + $stream = new ThroughStream(); + $stream->close(); + + return new Response(200, array(), $stream); + }); + + $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { + $conn->write("CONNECT example.com:80 HTTP/1.1\r\nHost: example.com:80\r\n\r\n"); + + $conn->once('data', function () use ($conn) { + $conn->write('hello'); + $conn->write('world'); + }); + + return Stream\buffer($conn); + }); + + $response = Block\await($result, $loop, 1.0); + + $this->assertStringStartsWith("HTTP/1.1 200 OK\r\n", $response); + $this->assertStringEndsWith("\r\n\r\n", $response); + + $socket->close(); + } } function noScheme($uri) diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 7c2e98fa..e7483cab 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -806,6 +806,54 @@ public function testResponseStreamWillBeClosedIfConnectionEmitsCloseEvent() $this->connection->emit('close'); } + public function testConnectResponseStreamWillPipeDataToConnection() + { + $stream = new ThroughStream(); + + $server = new Server($this->socket, function (ServerRequestInterface $request) use ($stream) { + return new Response(200, array(), $stream); + }); + + $buffer = ''; + + $this->connection + ->expects($this->any()) + ->method('write') + ->will( + $this->returnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } + ) + ); + + $this->socket->emit('connection', array($this->connection)); + + $data = "CONNECT example.com:80 HTTP/1.1\r\nHost: example.com:80\r\n\r\n"; + $this->connection->emit('data', array($data)); + + $stream->write('hello'); + $stream->write('world'); + + $this->assertStringEndsWith("\r\n\r\nhelloworld", $buffer); + } + + public function testConnectResponseStreamWillPipeDataFromConnection() + { + $stream = new ThroughStream(); + + $server = new Server($this->socket, function (ServerRequestInterface $request) use ($stream) { + return new Response(200, array(), $stream); + }); + + $this->socket->emit('connection', array($this->connection)); + + $this->connection->expects($this->once())->method('pipe')->with($stream); + + $data = "CONNECT example.com:80 HTTP/1.1\r\nHost: example.com:80\r\n\r\n"; + $this->connection->emit('data', array($data)); + } + public function testResponseContainsSameRequestProtocolVersionAndChunkedBodyForHttp11() { $server = new Server($this->socket, function (ServerRequestInterface $request) { From 01c34c7ea44c39ec7e03676b46bd7c95c8dbff29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 12 May 2017 23:02:12 +0200 Subject: [PATCH 151/456] Support Upgrade header such as `Upgrade: WebSocket` or custom protocols --- README.md | 24 +++++-- examples/31-upgrade-echo.php | 58 +++++++++++++++++ examples/32-upgrade-chat.php | 85 ++++++++++++++++++++++++ src/Server.php | 16 +++-- tests/FunctionalServerTest.php | 35 ++++++++++ tests/ServerTest.php | 116 +++++++++++++++++++++++++++++++++ 6 files changed, 326 insertions(+), 8 deletions(-) create mode 100644 examples/31-upgrade-echo.php create mode 100644 examples/32-upgrade-chat.php diff --git a/README.md b/README.md index c1353347..dd51ada0 100644 --- a/README.md +++ b/README.md @@ -559,17 +559,33 @@ to the message if the same request would have used an (unconditional) `GET`. implements ReactPHP's [`DuplexStreamInterface`](https://github.com/reactphp/stream#duplexstreaminterface) (such as the `ThroughStream` in the above example). - +> > For *most* cases, this will simply only consume its readable side and forward (send) any data that is emitted by the stream, thus entirely ignoring the writable side of the stream. - If however this is a `2xx` (Successful) response to a `CONNECT` method, it - will also *write* data to the writable side of the stream. + If however this is either a `101` (Switching Protocols) response or a `2xx` + (Successful) response to a `CONNECT` method, it will also *write* data to the + writable side of the stream. This can be avoided by either rejecting all requests with the `CONNECT` method (which is what most *normal* origin HTTP servers would likely do) or or ensuring that only ever an instance of `ReadableStreamInterface` is used. - +> +> The `101` (Switching Protocols) response code is useful for the more advanced + `Upgrade` requests, such as upgrading to the WebSocket protocol or + implementing custom protocol logic that is out of scope of the HTTP specs and + this HTTP library. + If you want to handle the `Upgrade: WebSocket` header, you will likely want + to look into using [Ratchet](http://socketo.me/) instead. + If you want to handle a custom protocol, you will likely want to look into the + [HTTP specs](https://tools.ietf.org/html/rfc7230#section-6.7) and also see + [examples #31 and #32](examples) for more details. + In particular, the `101` (Switching Protocols) response code MUST NOT be used + unless you send an `Upgrade` response header value that is also present in + the corresponding HTTP/1.1 `Upgrade` request header value. + The server automatically takes care of sending a `Connection: upgrade` + header value in this case, so you don't have to. +> > The `CONNECT` method is useful in a tunneling setup (HTTPS proxy) and not something most origin HTTP servers would want to care about. The HTTP specs define an opaque "tunneling mode" for this method and make no diff --git a/examples/31-upgrade-echo.php b/examples/31-upgrade-echo.php new file mode 100644 index 00000000..b28e8344 --- /dev/null +++ b/examples/31-upgrade-echo.php @@ -0,0 +1,58 @@ + GET / HTTP/1.1 +> Upgrade: echo +> +< HTTP/1.1 101 Switching Protocols +< Upgrade: echo +< Connection: upgrade +< +> hello +< hello +> world +< world +*/ + +use React\EventLoop\Factory; +use React\Http\Server; +use React\Http\Response; +use Psr\Http\Message\ServerRequestInterface; +use React\Stream\ReadableStream; +use React\Stream\ThroughStream; +use React\Stream\CompositeStream; + +require __DIR__ . '/../vendor/autoload.php'; + +$loop = Factory::create(); +$socket = new React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); + +$server = new Server($socket, function (ServerRequestInterface $request) use ($loop) { + if ($request->getHeaderLine('Upgrade') !== 'echo' || $request->getProtocolVersion() === '1.0') { + return new Response(426, array('Upgrade' => 'echo'), '"Upgrade: echo" required'); + } + + // simply return a duplex ThroughStream here + // it will simply emit any data that is sent to it + // this means that any Upgraded data will simply be sent back to the client + $stream = new ThroughStream(); + + $loop->addTimer(0, function () use ($stream) { + $stream->write("Hello! Anything you send will be piped back." . PHP_EOL); + }); + + return new Response( + 101, + array( + 'Upgrade' => 'echo' + ), + $stream + ); +}); + +echo 'Listening on http://' . $socket->getAddress() . PHP_EOL; + +$loop->run(); diff --git a/examples/32-upgrade-chat.php b/examples/32-upgrade-chat.php new file mode 100644 index 00000000..eb74b674 --- /dev/null +++ b/examples/32-upgrade-chat.php @@ -0,0 +1,85 @@ + GET / HTTP/1.1 +> Upgrade: chat +> +< HTTP/1.1 101 Switching Protocols +< Upgrade: chat +< Connection: upgrade +< +> hello +< user123: hello +> world +< user123: world + +Hint: try this with multiple connections :) +*/ + +use React\EventLoop\Factory; +use React\Http\Server; +use React\Http\Response; +use Psr\Http\Message\ServerRequestInterface; +use React\Stream\ReadableStream; +use React\Stream\ThroughStream; +use React\Stream\CompositeStream; + +require __DIR__ . '/../vendor/autoload.php'; + +$loop = Factory::create(); +$socket = new React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); + +// simply use a shared duplex ThroughStream for all clients +// it will simply emit any data that is sent to it +// this means that any Upgraded data will simply be sent back to the client +$chat = new ThroughStream(); + +$server = new Server($socket, function (ServerRequestInterface $request) use ($loop, $chat) { + if ($request->getHeaderLine('Upgrade') !== 'chat' || $request->getProtocolVersion() === '1.0') { + return new Response(426, array('Upgrade' => 'chat'), '"Upgrade: chat" required'); + } + + // user stream forwards chat data and accepts incoming data + $out = $chat->pipe(new ThroughStream()); + $in = new ThroughStream(); + $stream = new CompositeStream( + $out, + $in + ); + + // assign some name for this new connection + $username = 'user' . mt_rand(); + + // send anything that is received to the whole channel + $in->on('data', function ($data) use ($username, $chat) { + $data = trim(preg_replace('/[^\w\d \.\,\-\!\?]/u', '', $data)); + + $chat->write($username . ': ' . $data . PHP_EOL); + }); + + // say hello to new user + $loop->addTimer(0, function () use ($chat, $username, $out) { + $out->write('Welcome to this chat example, ' . $username . '!' . PHP_EOL); + $chat->write($username . ' joined' . PHP_EOL); + }); + + // send goodbye to channel once connection closes + $stream->on('close', function () use ($username, $chat) { + $chat->write($username . ' left' . PHP_EOL); + }); + + return new Response( + 101, + array( + 'Upgrade' => 'chat' + ), + $stream + ); +}); + +echo 'Listening on http://' . $socket->getAddress() . PHP_EOL; + +$loop->run(); diff --git a/src/Server.php b/src/Server.php index 8b42a7e9..83c273c1 100644 --- a/src/Server.php +++ b/src/Server.php @@ -300,7 +300,6 @@ public function handleResponse(ConnectionInterface $connection, ServerRequestInt if ($request->getProtocolVersion() === '1.1') { $response = $response->withHeader('Connection', 'close'); } - // 2xx response to CONNECT and 1xx and 204 MUST NOT include Content-Length or Transfer-Encoding header $code = $response->getStatusCode(); if (($request->getMethod() === 'CONNECT' && $code >= 200 && $code < 300) || ($code >= 100 && $code < 200) || $code === 204) { @@ -308,13 +307,22 @@ public function handleResponse(ConnectionInterface $connection, ServerRequestInt } // response to HEAD and 1xx, 204 and 304 responses MUST NOT include a body - if ($request->getMethod() === 'HEAD' || ($code >= 100 && $code < 200) || $code === 204 || $code === 304) { + // exclude status 101 (Switching Protocols) here for Upgrade request handling below + if ($request->getMethod() === 'HEAD' || $code === 100 || ($code > 101 && $code < 200) || $code === 204 || $code === 304) { $response = $response->withBody(Psr7Implementation\stream_for('')); } - // 2xx reponse to CONNECT forwards tunneled application data through duplex stream + // 101 (Switching Protocols) response uses Connection: upgrade header + // persistent connections are currently not supported, so do not use + // this for any other replies in order to preserve "Connection: close" + if ($code === 101) { + $response = $response->withHeader('Connection', 'upgrade'); + } + + // 101 (Switching Protocols) response (for Upgrade request) forwards upgraded data through duplex stream + // 2xx (Successful) response to CONNECT forwards tunneled application data through duplex stream $body = $response->getBody(); - if ($request->getMethod() === 'CONNECT' && $code >= 200 && $code < 300 && $body instanceof HttpBodyStream && $body->input instanceof WritableStreamInterface) { + if (($code === 101 || ($request->getMethod() === 'CONNECT' && $code >= 200 && $code < 300)) && $body instanceof HttpBodyStream && $body->input instanceof WritableStreamInterface) { if ($request->getBody()->isReadable()) { // request is still streaming => wait for request close before forwarding following data from connection $request->getBody()->on('close', function () use ($connection, $body) { diff --git a/tests/FunctionalServerTest.php b/tests/FunctionalServerTest.php index 3579db09..e3f365ea 100644 --- a/tests/FunctionalServerTest.php +++ b/tests/FunctionalServerTest.php @@ -447,6 +447,41 @@ public function testStreamFromRequestHandlerWillBeClosedIfConnectionClosesButWil $socket->close(); } + public function testUpgradeWithThroughStreamReturnsDataAsGiven() + { + $loop = Factory::create(); + $socket = new Socket(0, $loop); + $connector = new Connector($loop); + + $server = new Server($socket, function (RequestInterface $request) use ($loop) { + $stream = new ThroughStream(); + + $loop->addTimer(0.1, function () use ($stream) { + $stream->end(); + }); + + return new Response(101, array('Upgrade' => 'echo'), $stream); + }); + + $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { + $conn->write("GET / HTTP/1.1\r\nHost: example.com:80\r\nUpgrade: echo\r\n\r\n"); + + $conn->once('data', function () use ($conn) { + $conn->write('hello'); + $conn->write('world'); + }); + + return Stream\buffer($conn); + }); + + $response = Block\await($result, $loop, 1.0); + + $this->assertStringStartsWith("HTTP/1.1 101 Switching Protocols\r\n", $response); + $this->assertStringEndsWith("\r\n\r\nhelloworld", $response); + + $socket->close(); + } + public function testConnectWithThroughStreamReturnsDataAsGiven() { $loop = Factory::create(); diff --git a/tests/ServerTest.php b/tests/ServerTest.php index e7483cab..60163dd5 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -806,6 +806,121 @@ public function testResponseStreamWillBeClosedIfConnectionEmitsCloseEvent() $this->connection->emit('close'); } + public function testUpgradeInResponseCanBeUsedToAdvertisePossibleUpgrade() + { + $server = new Server($this->socket, function (ServerRequestInterface $request) { + return new Response(200, array('date' => '', 'x-powered-by' => '', 'Upgrade' => 'demo'), 'foo'); + }); + + $buffer = ''; + + $this->connection + ->expects($this->any()) + ->method('write') + ->will( + $this->returnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } + ) + ); + + $this->socket->emit('connection', array($this->connection)); + + $data = "GET / HTTP/1.1\r\n\r\n"; + $this->connection->emit('data', array($data)); + + $this->assertEquals("HTTP/1.1 200 OK\r\nUpgrade: demo\r\nContent-Length: 3\r\nConnection: close\r\n\r\nfoo", $buffer); + } + + public function testUpgradeWishInRequestCanBeIgnoredByReturningNormalResponse() + { + $server = new Server($this->socket, function (ServerRequestInterface $request) { + return new Response(200, array('date' => '', 'x-powered-by' => ''), 'foo'); + }); + + $buffer = ''; + + $this->connection + ->expects($this->any()) + ->method('write') + ->will( + $this->returnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } + ) + ); + + $this->socket->emit('connection', array($this->connection)); + + $data = "GET / HTTP/1.1\r\nUpgrade: demo\r\n\r\n"; + $this->connection->emit('data', array($data)); + + $this->assertEquals("HTTP/1.1 200 OK\r\nContent-Length: 3\r\nConnection: close\r\n\r\nfoo", $buffer); + } + + public function testUpgradeSwitchingProtocolIncludesConnectionUpgradeHeaderWithoutContentLength() + { + $server = new Server($this->socket, function (ServerRequestInterface $request) { + return new Response(101, array('date' => '', 'x-powered-by' => '', 'Upgrade' => 'demo'), 'foo'); + }); + + $server->on('error', 'printf'); + + $buffer = ''; + + $this->connection + ->expects($this->any()) + ->method('write') + ->will( + $this->returnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } + ) + ); + + $this->socket->emit('connection', array($this->connection)); + + $data = "GET / HTTP/1.1\r\nUpgrade: demo\r\n\r\n"; + $this->connection->emit('data', array($data)); + + $this->assertEquals("HTTP/1.1 101 Switching Protocols\r\nUpgrade: demo\r\nConnection: upgrade\r\n\r\nfoo", $buffer); + } + + public function testUpgradeSwitchingProtocolWithStreamWillPipeDataToConnection() + { + $stream = new ThroughStream(); + + $server = new Server($this->socket, function (ServerRequestInterface $request) use ($stream) { + return new Response(101, array('date' => '', 'x-powered-by' => '', 'Upgrade' => 'demo'), $stream); + }); + + $buffer = ''; + + $this->connection + ->expects($this->any()) + ->method('write') + ->will( + $this->returnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } + ) + ); + + $this->socket->emit('connection', array($this->connection)); + + $data = "GET / HTTP/1.1\r\nUpgrade: demo\r\n\r\n"; + $this->connection->emit('data', array($data)); + + $stream->write('hello'); + $stream->write('world'); + + $this->assertEquals("HTTP/1.1 101 Switching Protocols\r\nUpgrade: demo\r\nConnection: upgrade\r\n\r\nhelloworld", $buffer); + } + public function testConnectResponseStreamWillPipeDataToConnection() { $stream = new ThroughStream(); @@ -838,6 +953,7 @@ function ($data) use (&$buffer) { $this->assertStringEndsWith("\r\n\r\nhelloworld", $buffer); } + public function testConnectResponseStreamWillPipeDataFromConnection() { $stream = new ThroughStream(); From d225efb9ca4c3972cebf3c68684d8a0b90cce75b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 26 May 2017 15:00:56 +0200 Subject: [PATCH 152/456] Fix reporting listening addresses in examples --- examples/01-hello-world.php | 2 +- examples/02-count-visitors.php | 2 +- examples/03-client-ip.php | 2 +- examples/04-query-parameter.php | 2 +- examples/05-cookie-handling.php | 2 +- examples/06-sleep.php | 2 +- examples/07-error-handling.php | 2 +- examples/08-stream-response.php | 2 +- examples/09-stream-request.php | 2 +- examples/11-hello-world-https.php | 2 +- examples/21-http-proxy.php | 2 +- examples/22-connect-proxy.php | 2 +- examples/31-upgrade-echo.php | 2 +- examples/32-upgrade-chat.php | 2 +- examples/99-benchmark-download.php | 2 +- 15 files changed, 15 insertions(+), 15 deletions(-) diff --git a/examples/01-hello-world.php b/examples/01-hello-world.php index cf047944..4b34f5af 100644 --- a/examples/01-hello-world.php +++ b/examples/01-hello-world.php @@ -20,6 +20,6 @@ ); }); -echo 'Listening on http://' . $socket->getAddress() . PHP_EOL; +echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; $loop->run(); diff --git a/examples/02-count-visitors.php b/examples/02-count-visitors.php index 9f69f797..a2788510 100644 --- a/examples/02-count-visitors.php +++ b/examples/02-count-visitors.php @@ -19,6 +19,6 @@ ); }); -echo 'Listening on http://' . $socket->getAddress() . PHP_EOL; +echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; $loop->run(); diff --git a/examples/03-client-ip.php b/examples/03-client-ip.php index 31b7ad32..ee6d4f90 100644 --- a/examples/03-client-ip.php +++ b/examples/03-client-ip.php @@ -20,6 +20,6 @@ ); }); -echo 'Listening on http://' . $socket->getAddress() . PHP_EOL; +echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; $loop->run(); diff --git a/examples/04-query-parameter.php b/examples/04-query-parameter.php index 15f6c49a..ab64aef0 100644 --- a/examples/04-query-parameter.php +++ b/examples/04-query-parameter.php @@ -27,6 +27,6 @@ ); }); -echo 'Listening on http://' . $socket->getAddress() . PHP_EOL; +echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; $loop->run(); diff --git a/examples/05-cookie-handling.php b/examples/05-cookie-handling.php index 67e008bb..8b453a6f 100644 --- a/examples/05-cookie-handling.php +++ b/examples/05-cookie-handling.php @@ -33,6 +33,6 @@ ); }); -echo 'Listening on http://' . $socket->getAddress() . PHP_EOL; +echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; $loop->run(); diff --git a/examples/06-sleep.php b/examples/06-sleep.php index 9fb75542..f04b1758 100644 --- a/examples/06-sleep.php +++ b/examples/06-sleep.php @@ -24,6 +24,6 @@ }); }); -echo 'Listening on http://' . $socket->getAddress() . PHP_EOL; +echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; $loop->run(); diff --git a/examples/07-error-handling.php b/examples/07-error-handling.php index 00ed0cfa..265a1e27 100644 --- a/examples/07-error-handling.php +++ b/examples/07-error-handling.php @@ -30,6 +30,6 @@ }); }); -echo 'Listening on http://' . $socket->getAddress() . PHP_EOL; +echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; $loop->run(); diff --git a/examples/08-stream-response.php b/examples/08-stream-response.php index e563be38..a3d578d4 100644 --- a/examples/08-stream-response.php +++ b/examples/08-stream-response.php @@ -34,6 +34,6 @@ ); }); -echo 'Listening on http://' . $socket->getAddress() . PHP_EOL; +echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; $loop->run(); diff --git a/examples/09-stream-request.php b/examples/09-stream-request.php index fabb5bb7..356a6f22 100644 --- a/examples/09-stream-request.php +++ b/examples/09-stream-request.php @@ -39,6 +39,6 @@ }); }); -echo 'Listening on http://' . $socket->getAddress() . PHP_EOL; +echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; $loop->run(); diff --git a/examples/11-hello-world-https.php b/examples/11-hello-world-https.php index de958007..bf1b6d34 100644 --- a/examples/11-hello-world-https.php +++ b/examples/11-hello-world-https.php @@ -24,6 +24,6 @@ //$socket->on('error', 'printf'); -echo 'Listening on https://' . $socket->getAddress() . PHP_EOL; +echo 'Listening on ' . str_replace('tls:', 'https:', $socket->getAddress()) . PHP_EOL; $loop->run(); diff --git a/examples/21-http-proxy.php b/examples/21-http-proxy.php index 720f51fe..1285f5ed 100644 --- a/examples/21-http-proxy.php +++ b/examples/21-http-proxy.php @@ -40,6 +40,6 @@ //$server->on('error', 'printf'); -echo 'Listening on http://' . $socket->getAddress() . PHP_EOL; +echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; $loop->run(); diff --git a/examples/22-connect-proxy.php b/examples/22-connect-proxy.php index 7e7acd2c..5b5af3ed 100644 --- a/examples/22-connect-proxy.php +++ b/examples/22-connect-proxy.php @@ -44,6 +44,6 @@ function ($e) { //$server->on('error', 'printf'); -echo 'Listening on http://' . $socket->getAddress() . PHP_EOL; +echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; $loop->run(); diff --git a/examples/31-upgrade-echo.php b/examples/31-upgrade-echo.php index b28e8344..9e7d61d9 100644 --- a/examples/31-upgrade-echo.php +++ b/examples/31-upgrade-echo.php @@ -53,6 +53,6 @@ ); }); -echo 'Listening on http://' . $socket->getAddress() . PHP_EOL; +echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; $loop->run(); diff --git a/examples/32-upgrade-chat.php b/examples/32-upgrade-chat.php index eb74b674..d3cae956 100644 --- a/examples/32-upgrade-chat.php +++ b/examples/32-upgrade-chat.php @@ -80,6 +80,6 @@ ); }); -echo 'Listening on http://' . $socket->getAddress() . PHP_EOL; +echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; $loop->run(); diff --git a/examples/99-benchmark-download.php b/examples/99-benchmark-download.php index 0be294b1..b34a6b89 100644 --- a/examples/99-benchmark-download.php +++ b/examples/99-benchmark-download.php @@ -114,6 +114,6 @@ public function getSize() ); }); -echo 'Listening on http://' . $socket->getAddress() . PHP_EOL; +echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; $loop->run(); From c81189ed4d225c7709b96f7f838fd39a5f212587 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 27 May 2017 11:51:03 +0200 Subject: [PATCH 153/456] Add listen() method to support multiple listening sockets --- README.md | 77 +++---- examples/01-hello-world.php | 10 +- examples/02-count-visitors.php | 10 +- examples/03-client-ip.php | 10 +- examples/04-query-parameter.php | 10 +- examples/05-cookie-handling.php | 10 +- examples/06-sleep.php | 8 +- examples/07-error-handling.php | 10 +- examples/08-stream-response.php | 10 +- examples/09-stream-request.php | 10 +- examples/11-hello-world-https.php | 17 +- examples/21-http-proxy.php | 10 +- examples/22-connect-proxy.php | 10 +- examples/31-upgrade-echo.php | 12 +- examples/32-upgrade-chat.php | 13 +- examples/99-benchmark-download.php | 12 +- src/Server.php | 98 ++++++--- tests/FunctionalServerTest.php | 109 ++++++---- tests/ServerTest.php | 322 +++++++++++++++++++---------- 19 files changed, 486 insertions(+), 282 deletions(-) diff --git a/README.md b/README.md index dd51ada0..26cb2998 100644 --- a/README.md +++ b/README.md @@ -21,9 +21,8 @@ This is an HTTP server which responds with `Hello World` to every request. ```php $loop = React\EventLoop\Factory::create(); -$socket = new React\Socket\Server(8080, $loop); -$http = new Server($socket, function (ServerRequestInterface $request) { +$server = new Server(function (ServerRequestInterface $request) { return new Response( 200, array('Content-Type' => 'text/plain'), @@ -31,6 +30,9 @@ $http = new Server($socket, function (ServerRequestInterface $request) { ); }); +$socket = new React\Socket\Server(8080, $loop); +$server->listen($socket); + $loop->run(); ``` @@ -43,18 +45,12 @@ See also the [examples](examples). The `Server` class is responsible for handling incoming connections and then processing each incoming HTTP request. -It attaches itself to an instance of `React\Socket\ServerInterface` which -emits underlying streaming connections in order to then parse incoming data -as HTTP. - For each request, it executes the callback function passed to the -constructor with the respective [request](#request) and -[response](#response) objects: +constructor with the respective [request](#request) object and expects +a respective [response](#response) object in return. ```php -$socket = new React\Socket\Server(8080, $loop); - -$http = new Server($socket, function (ServerRequestInterface $request) { +$server = new Server(function (ServerRequestInterface $request) { return new Response( 200, array('Content-Type' => 'text/plain'), @@ -63,25 +59,36 @@ $http = new Server($socket, function (ServerRequestInterface $request) { }); ``` -See also the [first example](examples) for more details. +In order to process any connections, the server needs to be attached to an +instance of `React\Socket\ServerInterface` which emits underlying streaming +connections in order to then parse incoming data as HTTP. + +You can attach this to a +[`React\Socket\Server`](https://github.com/reactphp/socket#server) +in order to start a plaintext HTTP server like this: + +```php +$server = new Server($handler); + +$socket = new React\Socket\Server(8080, $loop); +$server->listen($socket); +``` + +See also the `listen()` method and the [first example](examples) for more details. Similarly, you can also attach this to a [`React\Socket\SecureServer`](https://github.com/reactphp/socket#secureserver) in order to start a secure HTTPS server like this: ```php +$server = new Server($handler); + $socket = new React\Socket\Server(8080, $loop); $socket = new React\Socket\SecureServer($socket, $loop, array( 'local_cert' => __DIR__ . '/localhost.pem' )); -$http = new Server($socket, function (ServerRequestInterface $request) { - return new Response( - 200, - array('Content-Type' => 'text/plain'), - "Hello World!\n" - ); -}); +$server->listen($socket); ``` See also [example #11](examples) for more details. @@ -105,7 +112,7 @@ emit an `error` event, send an HTTP error response to the client and close the connection: ```php -$http->on('error', function (Exception $e) { +$server->on('error', function (Exception $e) { echo 'Error: ' . $e->getMessage() . PHP_EOL; }); ``` @@ -117,7 +124,7 @@ the `Server` will emit a `RuntimeException` and add the thrown exception as previous: ```php -$http->on('error', function (Exception $e) { +$server->on('error', function (Exception $e) { echo 'Error: ' . $e->getMessage() . PHP_EOL; if ($e->getPrevious() !== null) { $previousException = $e->getPrevious(); @@ -143,7 +150,7 @@ which in turn extends the and will be passed to the callback function like this. ```php -$http = new Server($socket, function (ServerRequestInterface $request) { +$server = new Server(function (ServerRequestInterface $request) { $body = "The method of the request is: " . $request->getMethod(); $body .= "The requested path is: " . $request->getUri()->getPath(); @@ -177,7 +184,7 @@ The following parameters are currently available: Set to 'on' if the request used HTTPS, otherwise it won't be set ```php -$http = new Server($socket, function (ServerRequestInterface $request) { +$server = new Server(function (ServerRequestInterface $request) { $body = "Your IP is: " . $request->getServerParams()['REMOTE_ADDR']; return new Response( @@ -194,7 +201,7 @@ The `getQueryParams(): array` method can be used to get the query parameters similiar to the `$_GET` variable. ```php -$http = new Server($socket, function (ServerRequestInterface $request) { +$server = new Server(function (ServerRequestInterface $request) { $queryParams = $request->getQueryParams(); $body = 'The query parameter "foo" is not set. Click the following link '; @@ -257,7 +264,7 @@ Instead, you should use the `ReactPHP ReadableStreamInterface` which gives you access to the incoming request body as the individual chunks arrive: ```php -$http = new Server($socket, function (ServerRequestInterface $request) { +$server = new Server(function (ServerRequestInterface $request) { return new Promise(function ($resolve, $reject) use ($request) { $contentLength = 0; $request->getBody()->on('data', function ($data) use (&$contentLength) { @@ -321,7 +328,7 @@ Note that this value may be `null` if the request body size is unknown in advance because the request message uses chunked transfer encoding. ```php -$http = new Server($socket, function (ServerRequestInterface $request) { +$server = new Server(function (ServerRequestInterface $request) { $size = $request->getBody()->getSize(); if ($size === null) { $body = 'The request does not contain an explicit length.'; @@ -375,7 +382,7 @@ The `getCookieParams(): string[]` method can be used to get all cookies sent with the current request. ```php -$http = new Server($socket, function (ServerRequestInterface $request) { +$server = new Server(function (ServerRequestInterface $request) { $key = 'react\php'; if (isset($request->getCookieParams()[$key])) { @@ -426,7 +433,7 @@ but feel free to use any implemantation of the `PSR-7 ResponseInterface` you prefer. ```php -$http = new Server($socket, function (ServerRequestInterface $request) { +$server = new Server(function (ServerRequestInterface $request) { return new Response( 200, array('Content-Type' => 'text/plain'), @@ -445,7 +452,7 @@ To prevent this you SHOULD use a This example shows how such a long-term action could look like: ```php -$server = new \React\Http\Server($socket, function (ServerRequestInterface $request) use ($loop) { +$server = new Server(function (ServerRequestInterface $request) use ($loop) { return new Promise(function ($resolve, $reject) use ($request, $loop) { $loop->addTimer(1.5, function() use ($loop, $resolve) { $response = new Response( @@ -478,7 +485,7 @@ Note that other implementations of the `PSR-7 ResponseInterface` likely only support strings. ```php -$server = new Server($socket, function (ServerRequestInterface $request) use ($loop) { +$server = new Server(function (ServerRequestInterface $request) use ($loop) { $stream = new ThroughStream(); $timer = $loop->addPeriodicTimer(0.5, function () use ($stream) { @@ -521,7 +528,7 @@ If you know the length of your stream body, you MAY specify it like this instead ```php $stream = new ThroughStream() -$server = new Server($socket, function (ServerRequestInterface $request) use ($loop, $stream) { +$server = new Server(function (ServerRequestInterface $request) use ($stream) { return new Response( 200, array( @@ -606,7 +613,7 @@ A `Date` header will be automatically added with the system date and time if non You can add a custom `Date` header yourself like this: ```php -$server = new Server($socket, function (ServerRequestInterface $request) { +$server = new Server(function (ServerRequestInterface $request) { return new Response(200, array('Date' => date('D, d M Y H:i:s T'))); }); ``` @@ -615,7 +622,7 @@ If you don't have a appropriate clock to rely on, you should unset this header with an empty string: ```php -$server = new Server($socket, function (ServerRequestInterface $request) { +$server = new Server(function (ServerRequestInterface $request) { return new Response(200, array('Date' => '')); }); ``` @@ -624,7 +631,7 @@ Note that it will automatically assume a `X-Powered-By: react/alpha` header unless your specify a custom `X-Powered-By` header yourself: ```php -$server = new Server($socket, function (ServerRequestInterface $request) { +$server = new Server(function (ServerRequestInterface $request) { return new Response(200, array('X-Powered-By' => 'PHP 3')); }); ``` @@ -633,7 +640,7 @@ If you do not want to send this header at all, you can use an empty string as value like this: ```php -$server = new Server($socket, function (ServerRequestInterface $request) { +$server = new Server(function (ServerRequestInterface $request) { return new Response(200, array('X-Powered-By' => '')); }); ``` diff --git a/examples/01-hello-world.php b/examples/01-hello-world.php index 4b34f5af..f703a5d7 100644 --- a/examples/01-hello-world.php +++ b/examples/01-hello-world.php @@ -1,16 +1,15 @@ listen($socket); + echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; $loop->run(); diff --git a/examples/02-count-visitors.php b/examples/02-count-visitors.php index a2788510..5a225110 100644 --- a/examples/02-count-visitors.php +++ b/examples/02-count-visitors.php @@ -1,17 +1,16 @@ 'text/plain'), @@ -19,6 +18,9 @@ ); }); +$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); +$server->listen($socket); + echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; $loop->run(); diff --git a/examples/03-client-ip.php b/examples/03-client-ip.php index ee6d4f90..3fbcabfd 100644 --- a/examples/03-client-ip.php +++ b/examples/03-client-ip.php @@ -1,16 +1,15 @@ getServerParams()['REMOTE_ADDR']; return new Response( @@ -20,6 +19,9 @@ ); }); +$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); +$server->listen($socket); + echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; $loop->run(); diff --git a/examples/04-query-parameter.php b/examples/04-query-parameter.php index ab64aef0..3a60aae8 100644 --- a/examples/04-query-parameter.php +++ b/examples/04-query-parameter.php @@ -1,16 +1,15 @@ getQueryParams(); $body = 'The query parameter "foo" is not set. Click the following link '; @@ -27,6 +26,9 @@ ); }); +$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); +$server->listen($socket); + echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; $loop->run(); diff --git a/examples/05-cookie-handling.php b/examples/05-cookie-handling.php index 8b453a6f..5441adbe 100644 --- a/examples/05-cookie-handling.php +++ b/examples/05-cookie-handling.php @@ -1,16 +1,15 @@ getCookieParams()[$key])) { @@ -33,6 +32,9 @@ ); }); +$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); +$server->listen($socket); + echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; $loop->run(); diff --git a/examples/06-sleep.php b/examples/06-sleep.php index f04b1758..926aac10 100644 --- a/examples/06-sleep.php +++ b/examples/06-sleep.php @@ -3,15 +3,14 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Factory; use React\Http\Response; +use React\Http\Server; use React\Promise\Promise; -use React\Socket\Server; require __DIR__ . '/../vendor/autoload.php'; $loop = Factory::create(); -$socket = new Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); -$server = new \React\Http\Server($socket, function (ServerRequestInterface $request) use ($loop) { +$server = new Server(function (ServerRequestInterface $request) use ($loop) { return new Promise(function ($resolve, $reject) use ($request, $loop) { $loop->addTimer(1.5, function() use ($loop, $resolve) { $response = new Response( @@ -24,6 +23,9 @@ }); }); +$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); +$server->listen($socket); + echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; $loop->run(); diff --git a/examples/07-error-handling.php b/examples/07-error-handling.php index 265a1e27..5dbc6955 100644 --- a/examples/07-error-handling.php +++ b/examples/07-error-handling.php @@ -1,18 +1,17 @@ listen($socket); + echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; $loop->run(); diff --git a/examples/08-stream-response.php b/examples/08-stream-response.php index a3d578d4..399e3a77 100644 --- a/examples/08-stream-response.php +++ b/examples/08-stream-response.php @@ -1,17 +1,16 @@ getMethod() !== 'GET' || $request->getUri()->getPath() !== '/') { return new Response(404); } @@ -34,6 +33,9 @@ ); }); +$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); +$server->listen($socket); + echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; $loop->run(); diff --git a/examples/09-stream-request.php b/examples/09-stream-request.php index 356a6f22..bcf5456b 100644 --- a/examples/09-stream-request.php +++ b/examples/09-stream-request.php @@ -1,17 +1,16 @@ getBody()->on('data', function ($data) use (&$contentLength) { @@ -39,6 +38,9 @@ }); }); +$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); +$server->listen($socket); + echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; $loop->run(); diff --git a/examples/11-hello-world-https.php b/examples/11-hello-world-https.php index bf1b6d34..6610c3e0 100644 --- a/examples/11-hello-world-https.php +++ b/examples/11-hello-world-https.php @@ -1,20 +1,15 @@ isset($argv[2]) ? $argv[2] : __DIR__ . '/localhost.pem' -)); -$server = new \React\Http\Server($socket, function (ServerRequestInterface $request) { +$server = new Server(function (ServerRequestInterface $request) { return new Response( 200, array('Content-Type' => 'text/plain'), @@ -22,6 +17,12 @@ ); }); +$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); +$socket = new \React\Socket\SecureServer($socket, $loop, array( + 'local_cert' => isset($argv[2]) ? $argv[2] : __DIR__ . '/localhost.pem' +)); +$server->listen($socket); + //$socket->on('error', 'printf'); echo 'Listening on ' . str_replace('tls:', 'https:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/21-http-proxy.php b/examples/21-http-proxy.php index 1285f5ed..250cbf7a 100644 --- a/examples/21-http-proxy.php +++ b/examples/21-http-proxy.php @@ -1,17 +1,16 @@ getRequestTarget(), '://') === false) { return new Response( 400, @@ -38,7 +37,8 @@ ); }); -//$server->on('error', 'printf'); +$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); +$server->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/22-connect-proxy.php b/examples/22-connect-proxy.php index 5b5af3ed..ed8e80b0 100644 --- a/examples/22-connect-proxy.php +++ b/examples/22-connect-proxy.php @@ -1,19 +1,18 @@ getMethod() !== 'CONNECT') { return new Response( 405, @@ -42,7 +41,8 @@ function ($e) { ); }); -//$server->on('error', 'printf'); +$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); +$server->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/31-upgrade-echo.php b/examples/31-upgrade-echo.php index 9e7d61d9..b098ef03 100644 --- a/examples/31-upgrade-echo.php +++ b/examples/31-upgrade-echo.php @@ -17,20 +17,17 @@ < world */ +use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Factory; -use React\Http\Server; use React\Http\Response; -use Psr\Http\Message\ServerRequestInterface; -use React\Stream\ReadableStream; +use React\Http\Server; use React\Stream\ThroughStream; -use React\Stream\CompositeStream; require __DIR__ . '/../vendor/autoload.php'; $loop = Factory::create(); -$socket = new React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); -$server = new Server($socket, function (ServerRequestInterface $request) use ($loop) { +$server = new Server(function (ServerRequestInterface $request) use ($loop) { if ($request->getHeaderLine('Upgrade') !== 'echo' || $request->getProtocolVersion() === '1.0') { return new Response(426, array('Upgrade' => 'echo'), '"Upgrade: echo" required'); } @@ -53,6 +50,9 @@ ); }); +$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); +$server->listen($socket); + echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; $loop->run(); diff --git a/examples/32-upgrade-chat.php b/examples/32-upgrade-chat.php index d3cae956..49cb0305 100644 --- a/examples/32-upgrade-chat.php +++ b/examples/32-upgrade-chat.php @@ -19,25 +19,23 @@ Hint: try this with multiple connections :) */ +use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Factory; -use React\Http\Server; use React\Http\Response; -use Psr\Http\Message\ServerRequestInterface; -use React\Stream\ReadableStream; -use React\Stream\ThroughStream; +use React\Http\Server; use React\Stream\CompositeStream; +use React\Stream\ThroughStream; require __DIR__ . '/../vendor/autoload.php'; $loop = Factory::create(); -$socket = new React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); // simply use a shared duplex ThroughStream for all clients // it will simply emit any data that is sent to it // this means that any Upgraded data will simply be sent back to the client $chat = new ThroughStream(); -$server = new Server($socket, function (ServerRequestInterface $request) use ($loop, $chat) { +$server = new Server(function (ServerRequestInterface $request) use ($loop, $chat) { if ($request->getHeaderLine('Upgrade') !== 'chat' || $request->getProtocolVersion() === '1.0') { return new Response(426, array('Upgrade' => 'chat'), '"Upgrade: chat" required'); } @@ -80,6 +78,9 @@ ); }); +$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); +$server->listen($socket); + echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; $loop->run(); diff --git a/examples/99-benchmark-download.php b/examples/99-benchmark-download.php index b34a6b89..a8a6e03a 100644 --- a/examples/99-benchmark-download.php +++ b/examples/99-benchmark-download.php @@ -6,18 +6,17 @@ // $ ab -n10 -c10 http://localhost:8080/1g.bin // $ docker run -it --rm --net=host jordi/ab ab -n10 -c10 http://localhost:8080/1g.bin +use Evenement\EventEmitter; +use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Factory; -use React\Socket\Server; use React\Http\Response; -use Psr\Http\Message\ServerRequestInterface; -use Evenement\EventEmitter; +use React\Http\Server; use React\Stream\ReadableStreamInterface; use React\Stream\WritableStreamInterface; require __DIR__ . '/../vendor/autoload.php'; $loop = Factory::create(); -$socket = new Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); /** A readable stream that can emit a lot of data */ class ChunkRepeater extends EventEmitter implements ReadableStreamInterface @@ -87,7 +86,7 @@ public function getSize() } } -$server = new \React\Http\Server($socket, function (ServerRequestInterface $request) use ($loop) { +$server = new Server(function (ServerRequestInterface $request) use ($loop) { switch ($request->getUri()->getPath()) { case '/': return new Response( @@ -114,6 +113,9 @@ public function getSize() ); }); +$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); +$server->listen($socket); + echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; $loop->run(); diff --git a/src/Server.php b/src/Server.php index 83c273c1..6f9236e4 100644 --- a/src/Server.php +++ b/src/Server.php @@ -3,7 +3,7 @@ namespace React\Http; use Evenement\EventEmitter; -use React\Socket\ServerInterface as SocketServerInterface; +use React\Socket\ServerInterface; use React\Socket\ConnectionInterface; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; @@ -17,18 +17,12 @@ * The `Server` class is responsible for handling incoming connections and then * processing each incoming HTTP request. * - * It attaches itself to an instance of `React\Socket\ServerInterface` which - * emits underlying streaming connections in order to then parse incoming data - * as HTTP. - * * For each request, it executes the callback function passed to the - * constructor with the respective [request](#request) and - * [response](#response) objects: + * constructor with the respective [request](#request) object and expects + * a respective [response](#response) object in return. * * ```php - * $socket = new React\Socket\Server(8080, $loop); - * - * $http = new Server($socket, function (ServerRequestInterface $request) { + * $server = new Server(function (ServerRequestInterface $request) { * return new Response( * 200, * array('Content-Type' => 'text/plain'), @@ -37,28 +31,16 @@ * }); * ``` * - * See also the [first example](examples) for more details. - * - * Similarly, you can also attach this to a - * [`React\Socket\SecureServer`](https://github.com/reactphp/socket#secureserver) - * in order to start a secure HTTPS server like this: + * In order to process any connections, the server needs to be attached to an + * instance of `React\Socket\ServerInterface` which emits underlying streaming + * connections in order to then parse incoming data as HTTP. * * ```php * $socket = new React\Socket\Server(8080, $loop); - * $socket = new React\Socket\SecureServer($socket, $loop, array( - * 'local_cert' => __DIR__ . '/localhost.pem' - * )); - * - * $http = new Server($socket, function (ServerRequestInterface $request) { - * return new Response( - * 200, - * array('Content-Type' => 'text/plain'), - * "Hello World!\n" - * ); - * }); + * $server->listen($socket); * ``` * - * See also [example #11](examples) for more details. + * See also the [listen()](#listen) method and the [first example](examples) for more details. * * When HTTP/1.1 clients want to send a bigger request body, they MAY send only * the request headers with an additional `Expect: 100-continue` header and @@ -79,7 +61,7 @@ * close the connection: * * ```php - * $http->on('error', function (Exception $e) { + * $server->on('error', function (Exception $e) { * echo 'Error: ' . $e->getMessage() . PHP_EOL; * }); * ``` @@ -89,27 +71,79 @@ * * @see Request * @see Response + * @see self::listen() */ class Server extends EventEmitter { private $callback; /** - * Creates a HTTP server that accepts connections from the given socket. + * Creates an HTTP server that invokes the given callback for each incoming HTTP request + * + * In order to process any connections, the server needs to be attached to an + * instance of `React\Socket\ServerInterface` which emits underlying streaming + * connections in order to then parse incoming data as HTTP. + * See also [listen()](#listen) for more details. * - * @param \React\Socket\ServerInterface $io * @param callable $callback + * @see self::listen() */ - public function __construct(SocketServerInterface $io, $callback) + public function __construct($callback) { if (!is_callable($callback)) { throw new \InvalidArgumentException(); } - $io->on('connection', array($this, 'handleConnection')); $this->callback = $callback; } + /** + * Starts listening for HTTP requests on the given socket server instance + * + * The server needs to be attached to an instance of + * `React\Socket\ServerInterface` which emits underlying streaming + * connections in order to then parse incoming data as HTTP. + * For each request, it executes the callback function passed to the + * constructor with the respective [request](#request) object and expects + * a respective [response](#response) object in return. + * + * You can attach this to a + * [`React\Socket\Server`](https://github.com/reactphp/socket#server) + * in order to start a plaintext HTTP server like this: + * + * ```php + * $server = new Server($handler); + * + * $socket = new React\Socket\Server(8080, $loop); + * $server->listen($socket); + * ``` + * + * See also [example #1](examples) for more details. + * + * Similarly, you can also attach this to a + * [`React\Socket\SecureServer`](https://github.com/reactphp/socket#secureserver) + * in order to start a secure HTTPS server like this: + * + * ```php + * $server = new Server($handler); + * + * $socket = new React\Socket\Server(8080, $loop); + * $socket = new React\Socket\SecureServer($socket, $loop, array( + * 'local_cert' => __DIR__ . '/localhost.pem' + * )); + * + * $server->listen($socket); + * ``` + * + * See also [example #11](examples) for more details. + * + * @param ServerInterface $socket + */ + public function listen(ServerInterface $socket) + { + $socket->on('connection', array($this, 'handleConnection')); + } + /** @internal */ public function handleConnection(ConnectionInterface $conn) { diff --git a/tests/FunctionalServerTest.php b/tests/FunctionalServerTest.php index e3f365ea..06f06db9 100644 --- a/tests/FunctionalServerTest.php +++ b/tests/FunctionalServerTest.php @@ -12,7 +12,6 @@ use React\Http\Response; use React\Socket\SecureServer; use React\Stream\ReadableStreamInterface; -use React\EventLoop\LoopInterface; use React\Promise\Promise; use React\Promise\PromiseInterface; use React\Promise\Stream; @@ -23,13 +22,15 @@ class FunctionalServerTest extends TestCase public function testPlainHttpOnRandomPort() { $loop = Factory::create(); - $socket = new Socket(0, $loop); $connector = new Connector($loop); - $server = new Server($socket, function (RequestInterface $request) { + $server = new Server(function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); + $socket = new Socket(0, $loop); + $server->listen($socket); + $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\nHost: " . noScheme($conn->getRemoteAddress()) . "\r\n\r\n"); @@ -47,13 +48,15 @@ public function testPlainHttpOnRandomPort() public function testPlainHttpOnRandomPortWithoutHostHeaderUsesSocketUri() { $loop = Factory::create(); - $socket = new Socket(0, $loop); $connector = new Connector($loop); - $server = new Server($socket, function (RequestInterface $request) { + $server = new Server(function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); + $socket = new Socket(0, $loop); + $server->listen($socket); + $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\n\r\n"); @@ -71,13 +74,15 @@ public function testPlainHttpOnRandomPortWithoutHostHeaderUsesSocketUri() public function testPlainHttpOnRandomPortWithOtherHostHeaderTakesPrecedence() { $loop = Factory::create(); - $socket = new Socket(0, $loop); $connector = new Connector($loop); - $server = new Server($socket, function (RequestInterface $request) { + $server = new Server(function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); + $socket = new Socket(0, $loop); + $server->listen($socket); + $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\nHost: localhost:1000\r\n\r\n"); @@ -99,18 +104,20 @@ public function testSecureHttpsOnRandomPort() } $loop = Factory::create(); - $socket = new Socket(0, $loop); - $socket = new SecureServer($socket, $loop, array( - 'local_cert' => __DIR__ . '/../examples/localhost.pem' - )); $connector = new Connector($loop, array( 'tls' => array('verify_peer' => false) )); - $server = new Server($socket, function (RequestInterface $request) { + $server = new Server(function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); + $socket = new Socket(0, $loop); + $socket = new SecureServer($socket, $loop, array( + 'local_cert' => __DIR__ . '/../examples/localhost.pem' + )); + $server->listen($socket); + $result = $connector->connect('tls://' . noScheme($socket->getAddress()))->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\nHost: " . noScheme($conn->getRemoteAddress()) . "\r\n\r\n"); @@ -132,18 +139,20 @@ public function testSecureHttpsOnRandomPortWithoutHostHeaderUsesSocketUri() } $loop = Factory::create(); - $socket = new Socket(0, $loop); - $socket = new SecureServer($socket, $loop, array( - 'local_cert' => __DIR__ . '/../examples/localhost.pem' - )); $connector = new Connector($loop, array( 'tls' => array('verify_peer' => false) )); - $server = new Server($socket, function (RequestInterface $request) { + $server = new Server(function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); + $socket = new Socket(0, $loop); + $socket = new SecureServer($socket, $loop, array( + 'local_cert' => __DIR__ . '/../examples/localhost.pem' + )); + $server->listen($socket); + $result = $connector->connect('tls://' . noScheme($socket->getAddress()))->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\n\r\n"); @@ -168,10 +177,12 @@ public function testPlainHttpOnStandardPortReturnsUriWithNoPort() } $connector = new Connector($loop); - $server = new Server($socket, function (RequestInterface $request) { + $server = new Server(function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); + $server->listen($socket); + $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n"); @@ -196,10 +207,12 @@ public function testPlainHttpOnStandardPortWithoutHostHeaderReturnsUriWithNoPort } $connector = new Connector($loop); - $server = new Server($socket, function (RequestInterface $request) { + $server = new Server(function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); + $server->listen($socket); + $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\n\r\n"); @@ -233,10 +246,12 @@ public function testSecureHttpsOnStandardPortReturnsUriWithNoPort() 'tls' => array('verify_peer' => false) )); - $server = new Server($socket, function (RequestInterface $request) { + $server = new Server(function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); + $server->listen($socket); + $result = $connector->connect('tls://' . noScheme($socket->getAddress()))->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n"); @@ -270,10 +285,12 @@ public function testSecureHttpsOnStandardPortWithoutHostHeaderUsesSocketUri() 'tls' => array('verify_peer' => false) )); - $server = new Server($socket, function (RequestInterface $request) { + $server = new Server(function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); + $server->listen($socket); + $result = $connector->connect('tls://' . noScheme($socket->getAddress()))->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\n\r\n"); @@ -298,10 +315,12 @@ public function testPlainHttpOnHttpsStandardPortReturnsUriWithPort() } $connector = new Connector($loop); - $server = new Server($socket, function (RequestInterface $request) { + $server = new Server(function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); + $server->listen($socket); + $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\nHost: " . noScheme($conn->getRemoteAddress()) . "\r\n\r\n"); @@ -335,10 +354,12 @@ public function testSecureHttpsOnHttpStandardPortReturnsUriWithPort() 'tls' => array('verify_peer' => false) )); - $server = new Server($socket, function (RequestInterface $request) { + $server = new Server(function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri() . 'x' . $request->getHeaderLine('Host')); }); + $server->listen($socket); + $result = $connector->connect('tls://' . noScheme($socket->getAddress()))->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\nHost: " . noScheme($conn->getRemoteAddress()) . "\r\n\r\n"); @@ -356,16 +377,18 @@ public function testSecureHttpsOnHttpStandardPortReturnsUriWithPort() public function testClosedStreamFromRequestHandlerWillSendEmptyBody() { $loop = Factory::create(); - $socket = new Socket(0, $loop); $connector = new Connector($loop); $stream = new ThroughStream(); $stream->close(); - $server = new Server($socket, function (RequestInterface $request) use ($stream) { + $server = new Server(function (RequestInterface $request) use ($stream) { return new Response(200, array(), $stream); }); + $socket = new Socket(0, $loop); + $server->listen($socket); + $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) use ($loop) { $conn->write("GET / HTTP/1.0\r\n\r\n"); @@ -383,16 +406,18 @@ public function testClosedStreamFromRequestHandlerWillSendEmptyBody() public function testStreamFromRequestHandlerWillBeClosedIfConnectionClosesWhileSendingBody() { $loop = Factory::create(); - $socket = new Socket(0, $loop); $connector = new Connector($loop); $stream = new ThroughStream(); $stream->on('close', $this->expectCallableOnce()); - $server = new Server($socket, function (RequestInterface $request) use ($stream) { + $server = new Server(function (RequestInterface $request) use ($stream) { return new Response(200, array(), $stream); }); + $socket = new Socket(0, $loop); + $server->listen($socket); + $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) use ($loop) { $conn->write("GET / HTTP/1.0\r\nContent-Length: 100\r\n\r\n"); @@ -414,16 +439,18 @@ public function testStreamFromRequestHandlerWillBeClosedIfConnectionClosesWhileS public function testStreamFromRequestHandlerWillBeClosedIfConnectionClosesButWillOnlyBeDetectedOnNextWrite() { $loop = Factory::create(); - $socket = new Socket(0, $loop); $connector = new Connector($loop); $stream = new ThroughStream(); $stream->on('close', $this->expectCallableOnce()); - $server = new Server($socket, function (RequestInterface $request) use ($stream) { + $server = new Server(function (RequestInterface $request) use ($stream) { return new Response(200, array(), $stream); }); + $socket = new Socket(0, $loop); + $server->listen($socket); + $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) use ($loop) { $conn->write("GET / HTTP/1.0\r\n\r\n"); @@ -450,10 +477,9 @@ public function testStreamFromRequestHandlerWillBeClosedIfConnectionClosesButWil public function testUpgradeWithThroughStreamReturnsDataAsGiven() { $loop = Factory::create(); - $socket = new Socket(0, $loop); $connector = new Connector($loop); - $server = new Server($socket, function (RequestInterface $request) use ($loop) { + $server = new Server(function (RequestInterface $request) use ($loop) { $stream = new ThroughStream(); $loop->addTimer(0.1, function () use ($stream) { @@ -463,6 +489,9 @@ public function testUpgradeWithThroughStreamReturnsDataAsGiven() return new Response(101, array('Upgrade' => 'echo'), $stream); }); + $socket = new Socket(0, $loop); + $server->listen($socket); + $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.1\r\nHost: example.com:80\r\nUpgrade: echo\r\n\r\n"); @@ -485,10 +514,9 @@ public function testUpgradeWithThroughStreamReturnsDataAsGiven() public function testConnectWithThroughStreamReturnsDataAsGiven() { $loop = Factory::create(); - $socket = new Socket(0, $loop); $connector = new Connector($loop); - $server = new Server($socket, function (RequestInterface $request) use ($loop) { + $server = new Server(function (RequestInterface $request) use ($loop) { $stream = new ThroughStream(); $loop->addTimer(0.1, function () use ($stream) { @@ -498,6 +526,9 @@ public function testConnectWithThroughStreamReturnsDataAsGiven() return new Response(200, array(), $stream); }); + $socket = new Socket(0, $loop); + $server->listen($socket); + $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("CONNECT example.com:80 HTTP/1.1\r\nHost: example.com:80\r\n\r\n"); @@ -520,10 +551,9 @@ public function testConnectWithThroughStreamReturnsDataAsGiven() public function testConnectWithThroughStreamReturnedFromPromiseReturnsDataAsGiven() { $loop = Factory::create(); - $socket = new Socket(0, $loop); $connector = new Connector($loop); - $server = new Server($socket, function (RequestInterface $request) use ($loop) { + $server = new Server(function (RequestInterface $request) use ($loop) { $stream = new ThroughStream(); $loop->addTimer(0.1, function () use ($stream) { @@ -537,6 +567,9 @@ public function testConnectWithThroughStreamReturnedFromPromiseReturnsDataAsGive }); }); + $socket = new Socket(0, $loop); + $server->listen($socket); + $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("CONNECT example.com:80 HTTP/1.1\r\nHost: example.com:80\r\n\r\n"); @@ -559,16 +592,18 @@ public function testConnectWithThroughStreamReturnedFromPromiseReturnsDataAsGive public function testConnectWithClosedThroughStreamReturnsNoData() { $loop = Factory::create(); - $socket = new Socket(0, $loop); $connector = new Connector($loop); - $server = new Server($socket, function (RequestInterface $request) { + $server = new Server(function (RequestInterface $request) { $stream = new ThroughStream(); $stream->close(); return new Response(200, array(), $stream); }); + $socket = new Socket(0, $loop); + $server->listen($socket); + $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("CONNECT example.com:80 HTTP/1.1\r\nHost: example.com:80\r\n\r\n"); diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 60163dd5..76e2ba13 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -41,8 +41,9 @@ public function setUp() public function testRequestEventWillNotBeEmittedForIncompleteHeaders() { - $server = new Server($this->socket, $this->expectCallableNever()); + $server = new Server($this->expectCallableNever()); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = ''; @@ -52,10 +53,11 @@ public function testRequestEventWillNotBeEmittedForIncompleteHeaders() public function testRequestEventIsEmitted() { - $server = new Server($this->socket, function (ServerRequestInterface $request) { + $server = new Server(function (ServerRequestInterface $request) { return \React\Promise\resolve(new Response()); }); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = $this->createGetRequest(); @@ -66,7 +68,7 @@ public function testRequestEvent() { $i = 0; $requestAssertion = null; - $server = new Server($this->socket, function (ServerRequestInterface $request) use (&$i, &$requestAssertion) { + $server = new Server(function (ServerRequestInterface $request) use (&$i, &$requestAssertion) { $i++; $requestAssertion = $request; @@ -78,6 +80,7 @@ public function testRequestEvent() ->method('getRemoteAddress') ->willReturn('127.0.0.1:8080'); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = $this->createGetRequest(); @@ -99,11 +102,12 @@ public function testRequestEvent() public function testRequestGetWithHostAndCustomPort() { $requestAssertion = null; - $server = new Server($this->socket, function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new Server(function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; return new Response(); }); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\nHost: example.com:8080\r\n\r\n"; @@ -121,11 +125,12 @@ public function testRequestGetWithHostAndCustomPort() public function testRequestGetWithHostAndHttpsPort() { $requestAssertion = null; - $server = new Server($this->socket, function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new Server(function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; return new Response(); }); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\nHost: example.com:443\r\n\r\n"; @@ -143,11 +148,12 @@ public function testRequestGetWithHostAndHttpsPort() public function testRequestGetWithHostAndDefaultPortWillBeIgnored() { $requestAssertion = null; - $server = new Server($this->socket, function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new Server(function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; return new Response(); }); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\nHost: example.com:80\r\n\r\n"; @@ -165,11 +171,12 @@ public function testRequestGetWithHostAndDefaultPortWillBeIgnored() public function testRequestOptionsAsterisk() { $requestAssertion = null; - $server = new Server($this->socket, function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new Server(function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; return new Response(); }); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = "OPTIONS * HTTP/1.1\r\nHost: example.com\r\n\r\n"; @@ -185,9 +192,10 @@ public function testRequestOptionsAsterisk() public function testRequestNonOptionsWithAsteriskRequestTargetWillReject() { - $server = new Server($this->socket, $this->expectCallableNever()); + $server = new Server($this->expectCallableNever()); $server->on('error', $this->expectCallableOnce()); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = "GET * HTTP/1.1\r\nHost: example.com\r\n\r\n"; @@ -197,11 +205,12 @@ public function testRequestNonOptionsWithAsteriskRequestTargetWillReject() public function testRequestConnectAuthorityForm() { $requestAssertion = null; - $server = new Server($this->socket, function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new Server(function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; return new Response(); }); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = "CONNECT example.com:443 HTTP/1.1\r\nHost: example.com:443\r\n\r\n"; @@ -219,11 +228,12 @@ public function testRequestConnectAuthorityForm() public function testRequestConnectAuthorityFormWithDefaultPortWillBeIgnored() { $requestAssertion = null; - $server = new Server($this->socket, function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new Server(function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; return new Response(); }); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = "CONNECT example.com:80 HTTP/1.1\r\nHost: example.com:80\r\n\r\n"; @@ -241,11 +251,12 @@ public function testRequestConnectAuthorityFormWithDefaultPortWillBeIgnored() public function testRequestConnectAuthorityFormNonMatchingHostWillBeOverwritten() { $requestAssertion = null; - $server = new Server($this->socket, function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new Server(function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; return new Response(); }); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = "CONNECT example.com:80 HTTP/1.1\r\nHost: other.example.org\r\n\r\n"; @@ -262,9 +273,10 @@ public function testRequestConnectAuthorityFormNonMatchingHostWillBeOverwritten( public function testRequestConnectOriginFormRequestTargetWillReject() { - $server = new Server($this->socket, $this->expectCallableNever()); + $server = new Server($this->expectCallableNever()); $server->on('error', $this->expectCallableOnce()); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = "CONNECT / HTTP/1.1\r\nHost: example.com\r\n\r\n"; @@ -273,9 +285,10 @@ public function testRequestConnectOriginFormRequestTargetWillReject() public function testRequestNonConnectWithAuthorityRequestTargetWillReject() { - $server = new Server($this->socket, $this->expectCallableNever()); + $server = new Server($this->expectCallableNever()); $server->on('error', $this->expectCallableOnce()); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = "GET example.com:80 HTTP/1.1\r\nHost: example.com\r\n\r\n"; @@ -286,7 +299,7 @@ public function testRequestWithoutHostEventUsesSocketAddress() { $requestAssertion = null; - $server = new Server($this->socket, function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new Server(function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; return new Response(); }); @@ -296,6 +309,7 @@ public function testRequestWithoutHostEventUsesSocketAddress() ->method('getLocalAddress') ->willReturn('127.0.0.1:80'); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = "GET /test HTTP/1.0\r\n\r\n"; @@ -312,11 +326,12 @@ public function testRequestAbsoluteEvent() { $requestAssertion = null; - $server = new Server($this->socket, function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new Server(function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; return new Response(); }); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = "GET http://example.com/test HTTP/1.1\r\nHost: example.com\r\n\r\n"; @@ -334,12 +349,13 @@ public function testRequestAbsoluteAddsMissingHostEvent() { $requestAssertion = null; - $server = new Server($this->socket, function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new Server(function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; return new Response(); }); $server->on('error', 'printf'); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = "GET http://example.com:8080/test HTTP/1.0\r\n\r\n"; @@ -357,11 +373,12 @@ public function testRequestAbsoluteNonMatchingHostWillBeOverwritten() { $requestAssertion = null; - $server = new Server($this->socket, function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new Server(function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; return new Response(); }); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = "GET http://example.com/test HTTP/1.1\r\nHost: other.example.org\r\n\r\n"; @@ -379,11 +396,12 @@ public function testRequestOptionsAsteriskEvent() { $requestAssertion = null; - $server = new Server($this->socket, function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new Server(function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; return new Response(); }); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = "OPTIONS * HTTP/1.1\r\nHost: example.com\r\n\r\n"; @@ -401,11 +419,12 @@ public function testRequestOptionsAbsoluteEvent() { $requestAssertion = null; - $server = new Server($this->socket, function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new Server(function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; return new Response(); }); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = "OPTIONS http://example.com HTTP/1.1\r\nHost: example.com\r\n\r\n"; @@ -421,12 +440,14 @@ public function testRequestOptionsAbsoluteEvent() public function testRequestPauseWillbeForwardedToConnection() { - $server = new Server($this->socket, function (ServerRequestInterface $request) { + $server = new Server(function (ServerRequestInterface $request) { $request->getBody()->pause(); return new Response(); }); $this->connection->expects($this->once())->method('pause'); + + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; @@ -440,12 +461,14 @@ public function testRequestPauseWillbeForwardedToConnection() public function testRequestResumeWillbeForwardedToConnection() { - $server = new Server($this->socket, function (ServerRequestInterface $request) { + $server = new Server(function (ServerRequestInterface $request) { $request->getBody()->resume(); return new Response(); }); $this->connection->expects($this->once())->method('resume'); + + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = $this->createGetRequest(); @@ -454,12 +477,14 @@ public function testRequestResumeWillbeForwardedToConnection() public function testRequestCloseWillPauseConnection() { - $server = new Server($this->socket, function (ServerRequestInterface $request) { + $server = new Server(function (ServerRequestInterface $request) { $request->getBody()->close(); return new Response(); }); $this->connection->expects($this->once())->method('pause'); + + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = $this->createGetRequest(); @@ -468,7 +493,7 @@ public function testRequestCloseWillPauseConnection() public function testRequestPauseAfterCloseWillNotBeForwarded() { - $server = new Server($this->socket, function (ServerRequestInterface $request) { + $server = new Server(function (ServerRequestInterface $request) { $request->getBody()->close(); $request->getBody()->pause();# @@ -476,6 +501,8 @@ public function testRequestPauseAfterCloseWillNotBeForwarded() }); $this->connection->expects($this->once())->method('pause'); + + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = $this->createGetRequest(); @@ -484,7 +511,7 @@ public function testRequestPauseAfterCloseWillNotBeForwarded() public function testRequestResumeAfterCloseWillNotBeForwarded() { - $server = new Server($this->socket, function (ServerRequestInterface $request) { + $server = new Server(function (ServerRequestInterface $request) { $request->getBody()->close(); $request->getBody()->resume(); @@ -493,6 +520,8 @@ public function testRequestResumeAfterCloseWillNotBeForwarded() $this->connection->expects($this->once())->method('pause'); $this->connection->expects($this->never())->method('resume'); + + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = $this->createGetRequest(); @@ -503,12 +532,13 @@ public function testRequestEventWithoutBodyWillNotEmitData() { $never = $this->expectCallableNever(); - $server = new Server($this->socket, function (ServerRequestInterface $request) use ($never) { + $server = new Server(function (ServerRequestInterface $request) use ($never) { $request->getBody()->on('data', $never); return new Response(); }); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = $this->createGetRequest(); @@ -519,12 +549,13 @@ public function testRequestEventWithSecondDataEventWillEmitBodyData() { $once = $this->expectCallableOnceWith('incomplete'); - $server = new Server($this->socket, function (ServerRequestInterface $request) use ($once) { + $server = new Server(function (ServerRequestInterface $request) use ($once) { $request->getBody()->on('data', $once); return new Response(); }); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = ''; @@ -540,12 +571,13 @@ public function testRequestEventWithPartialBodyWillEmitData() { $once = $this->expectCallableOnceWith('incomplete'); - $server = new Server($this->socket, function (ServerRequestInterface $request) use ($once) { + $server = new Server(function (ServerRequestInterface $request) use ($once) { $request->getBody()->on('data', $once); return new Response(); }); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = ''; @@ -562,7 +594,7 @@ public function testRequestEventWithPartialBodyWillEmitData() public function testResponseContainsPoweredByHeader() { - $server = new Server($this->socket, function (ServerRequestInterface $request) { + $server = new Server(function (ServerRequestInterface $request) { return new Response(); }); @@ -579,6 +611,7 @@ function ($data) use (&$buffer) { ) ); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = $this->createGetRequest(); @@ -591,7 +624,7 @@ public function testPendingPromiseWillNotSendAnything() { $never = $this->expectCallableNever(); - $server = new Server($this->socket, function (ServerRequestInterface $request) use ($never) { + $server = new Server(function (ServerRequestInterface $request) use ($never) { return new Promise(function () { }, $never); }); @@ -608,6 +641,7 @@ function ($data) use (&$buffer) { ) ); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = $this->createGetRequest(); @@ -620,7 +654,7 @@ public function testPendingPromiseWillBeCancelledIfConnectionCloses() { $once = $this->expectCallableOnce(); - $server = new Server($this->socket, function (ServerRequestInterface $request) use ($once) { + $server = new Server(function (ServerRequestInterface $request) use ($once) { return new Promise(function () { }, $once); }); @@ -637,6 +671,7 @@ function ($data) use (&$buffer) { ) ); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = $this->createGetRequest(); @@ -651,7 +686,7 @@ public function testStreamAlreadyClosedWillSendEmptyBodyChunkedEncoded() $stream = new ThroughStream(); $stream->close(); - $server = new Server($this->socket, function (ServerRequestInterface $request) use ($stream) { + $server = new Server(function (ServerRequestInterface $request) use ($stream) { return new Response(200, array(), $stream); }); @@ -668,6 +703,7 @@ function ($data) use (&$buffer) { ) ); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"; @@ -681,7 +717,7 @@ public function testResponseStreamEndingWillSendEmptyBodyChunkedEncoded() { $stream = new ThroughStream(); - $server = new Server($this->socket, function (ServerRequestInterface $request) use ($stream) { + $server = new Server(function (ServerRequestInterface $request) use ($stream) { return new Response(200, array(), $stream); }); @@ -698,6 +734,7 @@ function ($data) use (&$buffer) { ) ); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"; @@ -714,7 +751,7 @@ public function testResponseStreamAlreadyClosedWillSendEmptyBodyPlainHttp10() $stream = new ThroughStream(); $stream->close(); - $server = new Server($this->socket, function (ServerRequestInterface $request) use ($stream) { + $server = new Server(function (ServerRequestInterface $request) use ($stream) { return new Response(200, array(), $stream); }); @@ -731,6 +768,7 @@ function ($data) use (&$buffer) { ) ); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.0\r\nHost: localhost\r\n\r\n"; @@ -745,7 +783,7 @@ public function testResponseStreamWillBeClosedIfConnectionIsAlreadyClosed() $stream = new ThroughStream(); $stream->on('close', $this->expectCallableOnce()); - $server = new Server($this->socket, function (ServerRequestInterface $request) use ($stream) { + $server = new Server(function (ServerRequestInterface $request) use ($stream) { return new Response(200, array(), $stream); }); @@ -784,6 +822,7 @@ function ($data) use (&$buffer) { $this->connection->expects($this->never())->method('write'); $this->connection->expects($this->never())->method('write'); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = $this->createGetRequest(); @@ -795,10 +834,11 @@ public function testResponseStreamWillBeClosedIfConnectionEmitsCloseEvent() $stream = new ThroughStream(); $stream->on('close', $this->expectCallableOnce()); - $server = new Server($this->socket, function (ServerRequestInterface $request) use ($stream) { + $server = new Server(function (ServerRequestInterface $request) use ($stream) { return new Response(200, array(), $stream); }); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = $this->createGetRequest(); @@ -808,7 +848,7 @@ public function testResponseStreamWillBeClosedIfConnectionEmitsCloseEvent() public function testUpgradeInResponseCanBeUsedToAdvertisePossibleUpgrade() { - $server = new Server($this->socket, function (ServerRequestInterface $request) { + $server = new Server(function (ServerRequestInterface $request) { return new Response(200, array('date' => '', 'x-powered-by' => '', 'Upgrade' => 'demo'), 'foo'); }); @@ -825,6 +865,7 @@ function ($data) use (&$buffer) { ) ); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n\r\n"; @@ -835,7 +876,7 @@ function ($data) use (&$buffer) { public function testUpgradeWishInRequestCanBeIgnoredByReturningNormalResponse() { - $server = new Server($this->socket, function (ServerRequestInterface $request) { + $server = new Server(function (ServerRequestInterface $request) { return new Response(200, array('date' => '', 'x-powered-by' => ''), 'foo'); }); @@ -852,6 +893,7 @@ function ($data) use (&$buffer) { ) ); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\nUpgrade: demo\r\n\r\n"; @@ -862,7 +904,7 @@ function ($data) use (&$buffer) { public function testUpgradeSwitchingProtocolIncludesConnectionUpgradeHeaderWithoutContentLength() { - $server = new Server($this->socket, function (ServerRequestInterface $request) { + $server = new Server(function (ServerRequestInterface $request) { return new Response(101, array('date' => '', 'x-powered-by' => '', 'Upgrade' => 'demo'), 'foo'); }); @@ -881,6 +923,7 @@ function ($data) use (&$buffer) { ) ); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\nUpgrade: demo\r\n\r\n"; @@ -893,7 +936,7 @@ public function testUpgradeSwitchingProtocolWithStreamWillPipeDataToConnection() { $stream = new ThroughStream(); - $server = new Server($this->socket, function (ServerRequestInterface $request) use ($stream) { + $server = new Server(function (ServerRequestInterface $request) use ($stream) { return new Response(101, array('date' => '', 'x-powered-by' => '', 'Upgrade' => 'demo'), $stream); }); @@ -910,6 +953,7 @@ function ($data) use (&$buffer) { ) ); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\nUpgrade: demo\r\n\r\n"; @@ -925,7 +969,7 @@ public function testConnectResponseStreamWillPipeDataToConnection() { $stream = new ThroughStream(); - $server = new Server($this->socket, function (ServerRequestInterface $request) use ($stream) { + $server = new Server(function (ServerRequestInterface $request) use ($stream) { return new Response(200, array(), $stream); }); @@ -942,6 +986,7 @@ function ($data) use (&$buffer) { ) ); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = "CONNECT example.com:80 HTTP/1.1\r\nHost: example.com:80\r\n\r\n"; @@ -958,10 +1003,11 @@ public function testConnectResponseStreamWillPipeDataFromConnection() { $stream = new ThroughStream(); - $server = new Server($this->socket, function (ServerRequestInterface $request) use ($stream) { + $server = new Server(function (ServerRequestInterface $request) use ($stream) { return new Response(200, array(), $stream); }); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $this->connection->expects($this->once())->method('pipe')->with($stream); @@ -972,7 +1018,7 @@ public function testConnectResponseStreamWillPipeDataFromConnection() public function testResponseContainsSameRequestProtocolVersionAndChunkedBodyForHttp11() { - $server = new Server($this->socket, function (ServerRequestInterface $request) { + $server = new Server(function (ServerRequestInterface $request) { $response = new Response(200, array(), 'bye'); return \React\Promise\resolve($response); }); @@ -990,6 +1036,7 @@ function ($data) use (&$buffer) { ) ); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"; @@ -1001,7 +1048,7 @@ function ($data) use (&$buffer) { public function testResponseContainsSameRequestProtocolVersionAndRawBodyForHttp10() { - $server = new Server($this->socket, function (ServerRequestInterface $request) { + $server = new Server(function (ServerRequestInterface $request) { $response = new Response(200, array(), 'bye'); return \React\Promise\resolve($response); }); @@ -1019,6 +1066,7 @@ function ($data) use (&$buffer) { ) ); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.0\r\n\r\n"; @@ -1031,7 +1079,7 @@ function ($data) use (&$buffer) { public function testResponseContainsNoResponseBodyForHeadRequest() { - $server = new Server($this->socket, function (ServerRequestInterface $request) { + $server = new Server(function (ServerRequestInterface $request) { return new Response(200, array(), 'bye'); }); @@ -1047,6 +1095,7 @@ function ($data) use (&$buffer) { ) ); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = "HEAD / HTTP/1.1\r\nHost: localhost\r\n\r\n"; @@ -1058,7 +1107,7 @@ function ($data) use (&$buffer) { public function testResponseContainsNoResponseBodyAndNoContentLengthForNoContentStatus() { - $server = new Server($this->socket, function (ServerRequestInterface $request) { + $server = new Server(function (ServerRequestInterface $request) { return new Response(204, array(), 'bye'); }); @@ -1074,6 +1123,7 @@ function ($data) use (&$buffer) { ) ); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"; @@ -1086,7 +1136,7 @@ function ($data) use (&$buffer) { public function testResponseContainsNoResponseBodyForNotModifiedStatus() { - $server = new Server($this->socket, function (ServerRequestInterface $request) { + $server = new Server(function (ServerRequestInterface $request) { return new Response(304, array(), 'bye'); }); @@ -1102,6 +1152,7 @@ function ($data) use (&$buffer) { ) ); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"; @@ -1115,7 +1166,7 @@ function ($data) use (&$buffer) { public function testRequestInvalidHttpProtocolVersionWillEmitErrorAndSendErrorResponse() { $error = null; - $server = new Server($this->socket, $this->expectCallableNever()); + $server = new Server($this->expectCallableNever()); $server->on('error', function ($message) use (&$error) { $error = $message; }); @@ -1133,6 +1184,7 @@ function ($data) use (&$buffer) { ) ); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.2\r\nHost: localhost\r\n\r\n"; @@ -1148,7 +1200,7 @@ function ($data) use (&$buffer) { public function testRequestOverflowWillEmitErrorAndSendErrorResponse() { $error = null; - $server = new Server($this->socket, $this->expectCallableNever()); + $server = new Server($this->expectCallableNever()); $server->on('error', function ($message) use (&$error) { $error = $message; }); @@ -1166,6 +1218,7 @@ function ($data) use (&$buffer) { ) ); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\nX-DATA: "; @@ -1181,7 +1234,7 @@ function ($data) use (&$buffer) { public function testRequestInvalidWillEmitErrorAndSendErrorResponse() { $error = null; - $server = new Server($this->socket, $this->expectCallableNever()); + $server = new Server($this->expectCallableNever()); $server->on('error', function ($message) use (&$error) { $error = $message; }); @@ -1199,6 +1252,7 @@ function ($data) use (&$buffer) { ) ); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = "bad request\r\n\r\n"; @@ -1217,7 +1271,7 @@ public function testBodyDataWillBeSendViaRequestEvent() $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new Server($this->socket, function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new Server(function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1226,6 +1280,7 @@ public function testBodyDataWillBeSendViaRequestEvent() return \React\Promise\resolve(new Response()); }); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; @@ -1246,7 +1301,7 @@ public function testChunkedEncodedRequestWillBeParsedForRequestEvent() $errorEvent = $this->expectCallableNever(); $requestValidation = null; - $server = new Server($this->socket, function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent, &$requestValidation) { + $server = new Server(function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent, &$requestValidation) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1256,6 +1311,7 @@ public function testChunkedEncodedRequestWillBeParsedForRequestEvent() return \React\Promise\resolve(new Response()); }); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; @@ -1278,7 +1334,7 @@ public function testChunkedEncodedRequestAdditionalDataWontBeEmitted() $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new Server($this->socket, function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new Server(function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1287,7 +1343,7 @@ public function testChunkedEncodedRequestAdditionalDataWontBeEmitted() return \React\Promise\resolve(new Response()); }); - + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; @@ -1309,7 +1365,7 @@ public function testEmptyChunkedEncodedRequest() $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new Server($this->socket, function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new Server(function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1318,6 +1374,7 @@ public function testEmptyChunkedEncodedRequest() return \React\Promise\resolve(new Response()); }); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; @@ -1337,7 +1394,7 @@ public function testChunkedIsUpperCase() $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new Server($this->socket, function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new Server(function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1346,7 +1403,7 @@ public function testChunkedIsUpperCase() return \React\Promise\resolve(new Response()); }); - + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; @@ -1367,7 +1424,7 @@ public function testChunkedIsMixedUpperAndLowerCase() $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new Server($this->socket, function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new Server(function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1376,7 +1433,7 @@ public function testChunkedIsMixedUpperAndLowerCase() return \React\Promise\resolve(new Response()); }); - + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; @@ -1392,7 +1449,7 @@ public function testChunkedIsMixedUpperAndLowerCase() public function testRequestWithMalformedHostWillEmitErrorAndSendErrorResponse() { $error = null; - $server = new Server($this->socket, $this->expectCallableNever()); + $server = new Server($this->expectCallableNever()); $server->on('error', function ($message) use (&$error) { $error = $message; }); @@ -1410,6 +1467,7 @@ function ($data) use (&$buffer) { ) ); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\nHost: ///\r\n\r\n"; @@ -1424,7 +1482,7 @@ function ($data) use (&$buffer) { public function testRequestWithInvalidHostUriComponentsWillEmitErrorAndSendErrorResponse() { $error = null; - $server = new Server($this->socket, $this->expectCallableNever()); + $server = new Server($this->expectCallableNever()); $server->on('error', function ($message) use (&$error) { $error = $message; }); @@ -1442,6 +1500,7 @@ function ($data) use (&$buffer) { ) ); + $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"; @@ -1460,7 +1519,7 @@ public function testWontEmitFurtherDataWhenContentLengthIsReached() $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new Server($this->socket, function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new Server(function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1469,6 +1528,7 @@ public function testWontEmitFurtherDataWhenContentLengthIsReached() return \React\Promise\resolve(new Response()); }); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; @@ -1490,7 +1550,7 @@ public function testWontEmitFurtherDataWhenContentLengthIsReachedSplitted() $errorEvent = $this->expectCallableNever(); - $server = new Server($this->socket, function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new Server(function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1499,7 +1559,7 @@ public function testWontEmitFurtherDataWhenContentLengthIsReachedSplitted() return \React\Promise\resolve(new Response()); }); - + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; @@ -1524,7 +1584,7 @@ public function testContentLengthContainsZeroWillEmitEndEvent() $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new Server($this->socket, function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new Server(function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1533,6 +1593,7 @@ public function testContentLengthContainsZeroWillEmitEndEvent() return \React\Promise\resolve(new Response()); }); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; @@ -1551,7 +1612,7 @@ public function testContentLengthContainsZeroWillEmitEndEventAdditionalDataWillB $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new Server($this->socket, function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new Server(function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1560,6 +1621,7 @@ public function testContentLengthContainsZeroWillEmitEndEventAdditionalDataWillB return \React\Promise\resolve(new Response()); }); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; @@ -1579,7 +1641,7 @@ public function testContentLengthContainsZeroWillEmitEndEventAdditionalDataWillB $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new Server($this->socket, function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new Server(function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1588,6 +1650,7 @@ public function testContentLengthContainsZeroWillEmitEndEventAdditionalDataWillB return \React\Promise\resolve(new Response()); }); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; @@ -1611,7 +1674,7 @@ public function testContentLengthWillBeIgnoredIfTransferEncodingIsSet() $errorEvent = $this->expectCallableNever(); $requestValidation = null; - $server = new Server($this->socket, function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent, &$requestValidation) { + $server = new Server(function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent, &$requestValidation) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1621,6 +1684,7 @@ public function testContentLengthWillBeIgnoredIfTransferEncodingIsSet() return \React\Promise\resolve(new Response()); }); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; @@ -1649,7 +1713,7 @@ public function testInvalidContentLengthWillBeIgnoreddIfTransferEncodingIsSet() $errorEvent = $this->expectCallableNever(); $requestValidation = null; - $server = new Server($this->socket, function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent, &$requestValidation) { + $server = new Server(function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent, &$requestValidation) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1659,6 +1723,7 @@ public function testInvalidContentLengthWillBeIgnoreddIfTransferEncodingIsSet() return \React\Promise\resolve(new Response()); }); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; @@ -1683,7 +1748,7 @@ public function testInvalidContentLengthWillBeIgnoreddIfTransferEncodingIsSet() public function testNonIntegerContentLengthValueWillLeadToError() { $error = null; - $server = new Server($this->socket, $this->expectCallableNever()); + $server = new Server($this->expectCallableNever()); $server->on('error', function ($message) use (&$error) { $error = $message; }); @@ -1700,6 +1765,7 @@ function ($data) use (&$buffer) { ) ); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; @@ -1719,7 +1785,7 @@ function ($data) use (&$buffer) { public function testNonIntegerContentLengthValueWillLeadToErrorWithNoBodyForHeadRequest() { $error = null; - $server = new Server($this->socket, $this->expectCallableNever()); + $server = new Server($this->expectCallableNever()); $server->on('error', function ($message) use (&$error) { $error = $message; }); @@ -1736,6 +1802,7 @@ function ($data) use (&$buffer) { ) ); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = "HEAD / HTTP/1.1\r\n"; @@ -1755,7 +1822,7 @@ function ($data) use (&$buffer) { public function testMultipleIntegerInContentLengthWillLeadToError() { $error = null; - $server = new Server($this->socket, $this->expectCallableNever()); + $server = new Server($this->expectCallableNever()); $server->on('error', function ($message) use (&$error) { $error = $message; }); @@ -1772,6 +1839,7 @@ function ($data) use (&$buffer) { ) ); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; @@ -1791,7 +1859,7 @@ function ($data) use (&$buffer) { public function testInvalidChunkHeaderResultsInErrorOnRequestStream() { $errorEvent = $this->expectCallableOnceWith($this->isInstanceOf('Exception')); - $server = new Server($this->socket, function ($request) use ($errorEvent){ + $server = new Server(function ($request) use ($errorEvent){ $request->getBody()->on('error', $errorEvent); return \React\Promise\resolve(new Response()); }); @@ -1799,6 +1867,7 @@ public function testInvalidChunkHeaderResultsInErrorOnRequestStream() $this->connection->expects($this->never())->method('close'); $this->connection->expects($this->once())->method('pause'); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; @@ -1814,7 +1883,7 @@ public function testInvalidChunkHeaderResultsInErrorOnRequestStream() public function testTooLongChunkHeaderResultsInErrorOnRequestStream() { $errorEvent = $this->expectCallableOnceWith($this->isInstanceOf('Exception')); - $server = new Server($this->socket, function ($request) use ($errorEvent){ + $server = new Server(function ($request) use ($errorEvent){ $request->getBody()->on('error', $errorEvent); return \React\Promise\resolve(new Response()); }); @@ -1822,6 +1891,7 @@ public function testTooLongChunkHeaderResultsInErrorOnRequestStream() $this->connection->expects($this->never())->method('close'); $this->connection->expects($this->once())->method('pause'); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; @@ -1839,7 +1909,7 @@ public function testTooLongChunkHeaderResultsInErrorOnRequestStream() public function testTooLongChunkBodyResultsInErrorOnRequestStream() { $errorEvent = $this->expectCallableOnceWith($this->isInstanceOf('Exception')); - $server = new Server($this->socket, function ($request) use ($errorEvent){ + $server = new Server(function ($request) use ($errorEvent){ $request->getBody()->on('error', $errorEvent); return \React\Promise\resolve(new Response()); }); @@ -1847,6 +1917,7 @@ public function testTooLongChunkBodyResultsInErrorOnRequestStream() $this->connection->expects($this->never())->method('close'); $this->connection->expects($this->once())->method('pause'); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; @@ -1862,7 +1933,7 @@ public function testTooLongChunkBodyResultsInErrorOnRequestStream() public function testUnexpectedEndOfConnectionWillResultsInErrorOnRequestStream() { $errorEvent = $this->expectCallableOnceWith($this->isInstanceOf('Exception')); - $server = new Server($this->socket, function ($request) use ($errorEvent){ + $server = new Server(function ($request) use ($errorEvent){ $request->getBody()->on('error', $errorEvent); return \React\Promise\resolve(new Response()); }); @@ -1870,6 +1941,7 @@ public function testUnexpectedEndOfConnectionWillResultsInErrorOnRequestStream() $this->connection->expects($this->never())->method('close'); $this->connection->expects($this->once())->method('pause'); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; @@ -1885,13 +1957,14 @@ public function testUnexpectedEndOfConnectionWillResultsInErrorOnRequestStream() public function testErrorInChunkedDecoderNeverClosesConnection() { - $server = new Server($this->socket, function (ServerRequestInterface $request) { + $server = new Server(function (ServerRequestInterface $request) { return \React\Promise\resolve(new Response()); }); $this->connection->expects($this->never())->method('close'); $this->connection->expects($this->once())->method('pause'); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; @@ -1906,13 +1979,14 @@ public function testErrorInChunkedDecoderNeverClosesConnection() public function testErrorInLengthLimitedStreamNeverClosesConnection() { - $server = new Server($this->socket, function (ServerRequestInterface $request) { + $server = new Server(function (ServerRequestInterface $request) { return \React\Promise\resolve(new Response()); }); $this->connection->expects($this->never())->method('close'); $this->connection->expects($this->once())->method('pause'); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; @@ -1928,7 +2002,7 @@ public function testErrorInLengthLimitedStreamNeverClosesConnection() public function testCloseRequestWillPauseConnection() { - $server = new Server($this->socket, function ($request) { + $server = new Server(function ($request) { $request->getBody()->close(); return \React\Promise\resolve(new Response()); }); @@ -1936,6 +2010,7 @@ public function testCloseRequestWillPauseConnection() $this->connection->expects($this->never())->method('close'); $this->connection->expects($this->once())->method('pause'); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = $this->createGetRequest(); @@ -1949,7 +2024,7 @@ public function testEndEventWillBeEmittedOnSimpleRequest() $endEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new Server($this->socket, function ($request) use ($dataEvent, $closeEvent, $endEvent, $errorEvent){ + $server = new Server(function ($request) use ($dataEvent, $closeEvent, $endEvent, $errorEvent){ $request->getBody()->on('data', $dataEvent); $request->getBody()->on('close', $closeEvent); $request->getBody()->on('end', $endEvent); @@ -1961,6 +2036,7 @@ public function testEndEventWillBeEmittedOnSimpleRequest() $this->connection->expects($this->once())->method('pause'); $this->connection->expects($this->never())->method('close'); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = $this->createGetRequest(); @@ -1975,7 +2051,7 @@ public function testRequestWithoutDefinedLengthWillIgnoreDataEvent() $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new Server($this->socket, function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new Server(function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1984,6 +2060,7 @@ public function testRequestWithoutDefinedLengthWillIgnoreDataEvent() return \React\Promise\resolve(new Response()); }); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = $this->createGetRequest(); @@ -1995,7 +2072,7 @@ public function testRequestWithoutDefinedLengthWillIgnoreDataEvent() public function testResponseWillBeChunkDecodedByDefault() { $stream = new ThroughStream(); - $server = new Server($this->socket, function (ServerRequestInterface $request) use ($stream) { + $server = new Server(function (ServerRequestInterface $request) use ($stream) { $response = new Response(200, array(), $stream); return \React\Promise\resolve($response); }); @@ -2012,6 +2089,7 @@ function ($data) use (&$buffer) { ) ); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = $this->createGetRequest(); @@ -2025,7 +2103,7 @@ function ($data) use (&$buffer) { public function testContentLengthWillBeRemovedForResponseStream() { - $server = new Server($this->socket, function (ServerRequestInterface $request) { + $server = new Server(function (ServerRequestInterface $request) { $response = new Response( 200, array( @@ -2050,6 +2128,7 @@ function ($data) use (&$buffer) { ) ); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = $this->createGetRequest(); @@ -2064,7 +2143,7 @@ function ($data) use (&$buffer) { public function testOnlyAllowChunkedEncoding() { $stream = new ThroughStream(); - $server = new Server($this->socket, function (ServerRequestInterface $request) use ($stream) { + $server = new Server(function (ServerRequestInterface $request) use ($stream) { $response = new Response( 200, array( @@ -2088,6 +2167,7 @@ function ($data) use (&$buffer) { ) ); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = $this->createGetRequest(); @@ -2102,7 +2182,7 @@ function ($data) use (&$buffer) { public function testDateHeaderWillBeAddedWhenNoneIsGiven() { - $server = new Server($this->socket, function (ServerRequestInterface $request) { + $server = new Server(function (ServerRequestInterface $request) { return \React\Promise\resolve(new Response()); }); @@ -2118,6 +2198,7 @@ function ($data) use (&$buffer) { ) ); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = $this->createGetRequest(); @@ -2131,7 +2212,7 @@ function ($data) use (&$buffer) { public function testAddCustomDateHeader() { - $server = new Server($this->socket, function (ServerRequestInterface $request) { + $server = new Server(function (ServerRequestInterface $request) { $response = new Response(200, array("Date" => "Tue, 15 Nov 1994 08:12:31 GMT")); return \React\Promise\resolve($response); }); @@ -2148,6 +2229,7 @@ function ($data) use (&$buffer) { ) ); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = $this->createGetRequest(); @@ -2161,7 +2243,7 @@ function ($data) use (&$buffer) { public function testRemoveDateHeader() { - $server = new Server($this->socket, function (ServerRequestInterface $request) { + $server = new Server(function (ServerRequestInterface $request) { $response = new Response(200, array('Date' => '')); return \React\Promise\resolve($response); }); @@ -2178,6 +2260,7 @@ function ($data) use (&$buffer) { ) ); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = $this->createGetRequest(); @@ -2193,7 +2276,7 @@ public function testOnlyChunkedEncodingIsAllowedForTransferEncoding() { $error = null; - $server = new Server($this->socket, $this->expectCallableNever()); + $server = new Server($this->expectCallableNever()); $server->on('error', function ($exception) use (&$error) { $error = $exception; }); @@ -2209,6 +2292,8 @@ function ($data) use (&$buffer) { } ) ); + + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; @@ -2228,7 +2313,7 @@ public function testOnlyChunkedEncodingIsAllowedForTransferEncodingWithHttp10() { $error = null; - $server = new Server($this->socket, $this->expectCallableNever()); + $server = new Server($this->expectCallableNever()); $server->on('error', function ($exception) use (&$error) { $error = $exception; }); @@ -2244,6 +2329,8 @@ function ($data) use (&$buffer) { } ) ); + + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.0\r\n"; @@ -2259,7 +2346,7 @@ function ($data) use (&$buffer) { public function test100ContinueRequestWillBeHandled() { - $server = new Server($this->socket, function (ServerRequestInterface $request) { + $server = new Server(function (ServerRequestInterface $request) { return \React\Promise\resolve(new Response()); }); @@ -2275,6 +2362,7 @@ function ($data) use (&$buffer) { ) ); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; @@ -2290,7 +2378,7 @@ function ($data) use (&$buffer) { public function testContinueWontBeSendForHttp10() { - $server = new Server($this->socket, function (ServerRequestInterface $request) { + $server = new Server(function (ServerRequestInterface $request) { return \React\Promise\resolve(new Response()); }); @@ -2306,6 +2394,7 @@ function ($data) use (&$buffer) { ) ); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.0\r\n"; @@ -2319,7 +2408,7 @@ function ($data) use (&$buffer) { public function testContinueWithLaterResponse() { - $server = new Server($this->socket, function (ServerRequestInterface $request) { + $server = new Server(function (ServerRequestInterface $request) { return \React\Promise\resolve(new Response()); }); @@ -2336,6 +2425,7 @@ function ($data) use (&$buffer) { ) ); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; @@ -2355,14 +2445,14 @@ function ($data) use (&$buffer) { */ public function testInvalidCallbackFunctionLeadsToException() { - $server = new Server($this->socket, 'invalid'); + $server = new Server('invalid'); } public function testHttpBodyStreamAsBodyWillStreamData() { $input = new ThroughStream(); - $server = new Server($this->socket, function (ServerRequestInterface $request) use ($input) { + $server = new Server(function (ServerRequestInterface $request) use ($input) { $response = new Response(200, array(), $input); return \React\Promise\resolve($response); }); @@ -2379,6 +2469,7 @@ function ($data) use (&$buffer) { ) ); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = $this->createGetRequest(); @@ -2397,7 +2488,7 @@ public function testHttpBodyStreamWithContentLengthWillStreamTillLength() { $input = new ThroughStream(); - $server = new Server($this->socket, function (ServerRequestInterface $request) use ($input) { + $server = new Server(function (ServerRequestInterface $request) use ($input) { $response = new Response(200, array('Content-Length' => 5), $input); return \React\Promise\resolve($response); }); @@ -2414,6 +2505,7 @@ function ($data) use (&$buffer) { ) ); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = $this->createGetRequest(); @@ -2431,7 +2523,7 @@ function ($data) use (&$buffer) { public function testCallbackFunctionReturnsPromise() { - $server = new Server($this->socket, function (ServerRequestInterface $request) { + $server = new Server(function (ServerRequestInterface $request) { return \React\Promise\resolve(new Response()); }); @@ -2447,6 +2539,7 @@ function ($data) use (&$buffer) { ) ); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = $this->createGetRequest(); @@ -2458,7 +2551,7 @@ function ($data) use (&$buffer) { public function testReturnInvalidTypeWillResultInError() { - $server = new Server($this->socket, function (ServerRequestInterface $request) { + $server = new Server(function (ServerRequestInterface $request) { return "invalid"; }); @@ -2479,6 +2572,7 @@ function ($data) use (&$buffer) { ) ); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.0\r\n\r\n"; @@ -2493,7 +2587,7 @@ function ($data) use (&$buffer) { public function testResolveWrongTypeInPromiseWillResultInError() { - $server = new Server($this->socket, function (ServerRequestInterface $request) { + $server = new Server(function (ServerRequestInterface $request) { return \React\Promise\resolve("invalid"); }); @@ -2509,6 +2603,7 @@ function ($data) use (&$buffer) { ) ); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.0\r\n\r\n"; @@ -2522,7 +2617,7 @@ function ($data) use (&$buffer) { public function testRejectedPromiseWillResultInErrorMessage() { - $server = new Server($this->socket, function (ServerRequestInterface $request) { + $server = new Server(function (ServerRequestInterface $request) { return new Promise(function ($resolve, $reject) { $reject(new \Exception()); }); @@ -2541,6 +2636,7 @@ function ($data) use (&$buffer) { ) ); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.0\r\n\r\n"; @@ -2554,7 +2650,7 @@ function ($data) use (&$buffer) { public function testExcpetionInCallbackWillResultInErrorMessage() { - $server = new Server($this->socket, function (ServerRequestInterface $request) { + $server = new Server(function (ServerRequestInterface $request) { return new Promise(function ($resolve, $reject) { throw new \Exception('Bad call'); }); @@ -2573,6 +2669,7 @@ function ($data) use (&$buffer) { ) ); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.0\r\n\r\n"; @@ -2586,7 +2683,7 @@ function ($data) use (&$buffer) { public function testHeaderWillAlwaysBeContentLengthForStringBody() { - $server = new Server($this->socket, function (ServerRequestInterface $request) { + $server = new Server(function (ServerRequestInterface $request) { return new Response(200, array('Transfer-Encoding' => 'chunked'), 'hello'); }); @@ -2602,6 +2699,7 @@ function ($data) use (&$buffer) { ) ); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.0\r\n\r\n"; @@ -2619,7 +2717,7 @@ function ($data) use (&$buffer) { public function testReturnRequestWillBeHandled() { - $server = new Server($this->socket, function (ServerRequestInterface $request) { + $server = new Server(function (ServerRequestInterface $request) { return new Response(); }); @@ -2635,6 +2733,7 @@ function ($data) use (&$buffer) { ) ); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.0\r\n\r\n"; @@ -2648,7 +2747,7 @@ function ($data) use (&$buffer) { public function testExceptionThrowInCallBackFunctionWillResultInErrorMessage() { - $server = new Server($this->socket, function (ServerRequestInterface $request) { + $server = new Server(function (ServerRequestInterface $request) { throw new \Exception('hello'); }); @@ -2669,6 +2768,7 @@ function ($data) use (&$buffer) { ) ); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.0\r\n\r\n"; @@ -2684,7 +2784,7 @@ function ($data) use (&$buffer) { public function testRejectOfNonExceptionWillResultInErrorMessage() { - $server = new Server($this->socket, function (ServerRequestInterface $request) { + $server = new Server(function (ServerRequestInterface $request) { return new Promise(function ($resolve, $reject) { $reject('Invalid type'); }); @@ -2707,6 +2807,7 @@ function ($data) use (&$buffer) { ) ); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.0\r\n\r\n"; @@ -2722,7 +2823,7 @@ function ($data) use (&$buffer) { public function testServerRequestParams() { $requestValidation = null; - $server = new Server($this->socket, function (ServerRequestInterface $request) use (&$requestValidation) { + $server = new Server(function (ServerRequestInterface $request) use (&$requestValidation) { $requestValidation = $request; return new Response(); }); @@ -2737,6 +2838,7 @@ public function testServerRequestParams() ->method('getLocalAddress') ->willReturn('127.0.0.1:8080'); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = $this->createGetRequest(); @@ -2756,11 +2858,12 @@ public function testServerRequestParams() public function testQueryParametersWillBeAddedToRequest() { $requestValidation = null; - $server = new Server($this->socket, function (ServerRequestInterface $request) use (&$requestValidation) { + $server = new Server(function (ServerRequestInterface $request) use (&$requestValidation) { $requestValidation = $request; return new Response(); }); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = "GET /foo.php?hello=world&test=bar HTTP/1.0\r\n\r\n"; @@ -2776,11 +2879,12 @@ public function testQueryParametersWillBeAddedToRequest() public function testCookieWillBeAddedToServerRequest() { $requestValidation = null; - $server = new Server($this->socket, function (ServerRequestInterface $request) use (&$requestValidation) { + $server = new Server(function (ServerRequestInterface $request) use (&$requestValidation) { $requestValidation = $request; return new Response(); }); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; @@ -2797,11 +2901,12 @@ public function testCookieWillBeAddedToServerRequest() public function testMultipleCookiesWontBeAddedToServerRequest() { $requestValidation = null; - $server = new Server($this->socket, function (ServerRequestInterface $request) use (&$requestValidation) { + $server = new Server(function (ServerRequestInterface $request) use (&$requestValidation) { $requestValidation = $request; return new Response(); }); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; @@ -2818,11 +2923,12 @@ public function testMultipleCookiesWontBeAddedToServerRequest() public function testCookieWithSeparatorWillBeAddedToServerRequest() { $requestValidation = null; - $server = new Server($this->socket, function (ServerRequestInterface $request) use (&$requestValidation) { + $server = new Server(function (ServerRequestInterface $request) use (&$requestValidation) { $requestValidation = $request; return new Response(); }); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; From 8c806074b6637cfd6cbb7049d5d500b18bb56cd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 29 May 2017 17:41:07 +0200 Subject: [PATCH 154/456] Prepare v0.7.0 release --- CHANGELOG.md | 130 ++++++++++++++++++++++++++++++++++++++++++++++++++- README.md | 2 +- 2 files changed, 130 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b3575457..1a9f09f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,134 @@ # Changelog -## 0.6.0 (2016-03-09) +## 0.7.0 (2017-05-29) + +* Feature / BC break: Use PSR-7 (http-message) standard and + `Request-In-Response-Out`-style request handler callback. + Pass standard PSR-7 `ServerRequestInterface` and expect any standard + PSR-7 `ResponseInterface` in return for the request handler callback. + (#146 and #152 and #170 by @legionth) + + ```php + // old + $app = function (Request $request, Response $response) { + $response->writeHead(200, array('Content-Type' => 'text/plain')); + $response->end("Hello world!\n"); + }; + + // new + $app = function (ServerRequestInterface $request) { + return new Response( + 200, + array('Content-Type' => 'text/plain'), + "Hello world!\n" + ); + }; + ``` + + A `Content-Length` header will automatically be included if the size can be + determined from the response body. + (#164 by @maciejmrozinski) + + The request handler callback will automatically make sure that responses to + HEAD requests and certain status codes, such as `204` (No Content), never + contain a response body. + (#156 by @clue) + + The intermediary `100 Continue` response will automatically be sent if + demanded by a HTTP/1.1 client. + (#144 by @legionth) + + The request handler callback can now return a standard `Promise` if + processing the request needs some time, such as when querying a database. + Similarly, the request handler may return a streaming response if the + response body comes from a `ReadableStreamInterface` or its size is + unknown in advance. + + ```php + // old + $app = function (Request $request, Response $response) use ($db) { + $db->query()->then(function ($result) use ($response) { + $response->writeHead(200, array('Content-Type' => 'text/plain')); + $response->end($result); + }); + }; + + // new + $app = function (ServerRequestInterface $request) use ($db) { + return $db->query()->then(function ($result) { + return new Response( + 200, + array('Content-Type' => 'text/plain'), + $result + ); + }); + }; + ``` + + Pending promies and response streams will automatically be canceled once the + client connection closes. + (#187 and #188 by @clue) + + The `ServerRequestInterface` contains the full effective request URI, + server-side parameters, query parameters and parsed cookies values as + defined in PSR-7. + (#167 by @clue and #174, #175 and #180 by @legionth) + + ```php + $app = function (ServerRequestInterface $request) { + return new Response( + 200, + array('Content-Type' => 'text/plain'), + $request->getUri()->getScheme() + ); + }; + ``` + + Advanced: Support duplex stream response for `Upgrade` requests such as + `Upgrade: WebSocket` or custom protocols and `CONNECT` requests + (#189 and #190 by @clue) + + > Note that the request body will currently not be buffered and parsed by + default, which depending on your particilar use-case, may limit + interoperability with the PSR-7 (http-message) ecosystem. + The provided streaming request body interfaces allow you to perform + buffering and parsing as needed in the request handler callback. + See also the README and examples for more details. + +* Feature / BC break: Replace `request` listener with callback function and + use `listen()` method to support multiple listening sockets + (#97 by @legionth and #193 by @clue) + + ```php + // old + $server = new Server($socket); + $server->on('request', $app); + + // new + $server = new Server($app); + $server->listen($socket); + ``` + +* Feature: Support the more advanced HTTP requests, such as + `OPTIONS * HTTP/1.1` (`OPTIONS` method in asterisk-form), + `GET http://example.com/path HTTP/1.1` (plain proxy requests in absolute-form), + `CONNECT example.com:443 HTTP/1.1` (`CONNECT` proxy requests in authority-form) + and sanitize `Host` header value across all requests. + (#157, #158, #161, #165, #169 and #173 by @clue) + +* Feature: Forward compatibility with Socket v1.0, v0.8, v0.7 and v0.6 and + forward compatibility with Stream v1.0 and v0.7 + (#154, #163, #183, #184 and #191 by @clue) + +* Feature: Simplify examples to ease getting started and + add benchmarking example + (#151 and #162 by @clue) + +* Improve test suite by adding tests for case insensitive chunked transfer + encoding and ignoring HHVM test failures until Travis tests work again. + (#150 by @legionth and #185 by @clue) + +## 0.6.0 (2017-03-09) * Feature / BC break: The `Request` and `Response` objects now follow strict stream semantics and their respective methods and events. diff --git a/README.md b/README.md index 26cb2998..f09371d8 100644 --- a/README.md +++ b/README.md @@ -659,7 +659,7 @@ The recommended way to install this library is [through Composer](http://getcomp This will install the latest supported version: ```bash -$ composer require react/http:^0.6 +$ composer require react/http:^0.7 ``` More details about version upgrades can be found in the [CHANGELOG](CHANGELOG.md). From 40b3c9e0e09e676c8f30deba61102ca76b27e19f Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Wed, 31 May 2017 17:44:21 +0200 Subject: [PATCH 155/456] UploadedFile implementing UploadedFileInterface --- src/UploadedFile.php | 119 +++++++++++++++++++++++++++++++++++++ tests/UploadedFileTest.php | 61 +++++++++++++++++++ 2 files changed, 180 insertions(+) create mode 100644 src/UploadedFile.php create mode 100644 tests/UploadedFileTest.php diff --git a/src/UploadedFile.php b/src/UploadedFile.php new file mode 100644 index 00000000..49248fe2 --- /dev/null +++ b/src/UploadedFile.php @@ -0,0 +1,119 @@ +stream = $stream; + $this->size = $size; + + if (!is_int($error) || !in_array($error, [ + UPLOAD_ERR_OK, + UPLOAD_ERR_INI_SIZE, + UPLOAD_ERR_FORM_SIZE, + UPLOAD_ERR_PARTIAL, + UPLOAD_ERR_NO_FILE, + UPLOAD_ERR_NO_TMP_DIR, + UPLOAD_ERR_CANT_WRITE, + UPLOAD_ERR_EXTENSION, + ])) { + throw new InvalidArgumentException( + 'Invalid error code, must be an UPLOAD_ERR_* constant' + ); + } + $this->error = $error; + $this->filename = $filename; + $this->mediaType = $mediaType; + } + + /** + * {@inheritdoc} + */ + public function getStream() + { + if ($this->error !== UPLOAD_ERR_OK) { + throw new RuntimeException('Cannot retrieve stream due to upload error'); + } + + return $this->stream; + } + + /** + * {@inheritdoc} + */ + public function moveTo($targetPath) + { + throw new RuntimeException('Not implemented'); + } + + /** + * {@inheritdoc} + */ + public function getSize() + { + return $this->size; + } + + /** + * {@inheritdoc} + */ + public function getError() + { + return $this->error; + } + + /** + * {@inheritdoc} + */ + public function getClientFilename() + { + return $this->filename; + } + + /** + * {@inheritdoc} + */ + public function getClientMediaType() + { + return $this->mediaType; + } +} diff --git a/tests/UploadedFileTest.php b/tests/UploadedFileTest.php new file mode 100644 index 00000000..0b16e25d --- /dev/null +++ b/tests/UploadedFileTest.php @@ -0,0 +1,61 @@ +moveTo('bar.foo'); + } + + public function testGetters() + { + $stream = new BufferStream(); + $uploadedFile = new UploadedFile($stream, 0, UPLOAD_ERR_OK, 'foo.bar', 'foo/bar'); + self::assertSame($stream, $uploadedFile->getStream()); + self::assertSame(0, $uploadedFile->getSize()); + self::assertSame(UPLOAD_ERR_OK, $uploadedFile->getError()); + self::assertSame('foo.bar', $uploadedFile->getClientFilename()); + self::assertSame('foo/bar', $uploadedFile->getClientMediaType()); + } + + public function testGetStreamOnFailedUpload() + { + self::expectException('\RuntimeException'); + self::expectExceptionMessage('Cannot retrieve stream due to upload error'); + $stream = new BufferStream(); + $uploadedFile = new UploadedFile($stream, 0, UPLOAD_ERR_NO_FILE, 'foo.bar', 'foo/bar'); + $uploadedFile->getStream(); + } +} From 056eea404a76b7f2100a47ff679f81f5f4bd703c Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Wed, 31 May 2017 19:36:02 +0200 Subject: [PATCH 156/456] Use annotations instead of expect exception methods --- tests/UploadedFileTest.php | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/tests/UploadedFileTest.php b/tests/UploadedFileTest.php index 0b16e25d..abd58a35 100644 --- a/tests/UploadedFileTest.php +++ b/tests/UploadedFileTest.php @@ -21,19 +21,21 @@ public function failtyErrorProvider() /** * @dataProvider failtyErrorProvider + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Invalid error code, must be an UPLOAD_ERR_* constant */ public function testFailtyError($error) { - self::expectException('\InvalidArgumentException'); - self::expectExceptionMessage('Invalid error code, must be an UPLOAD_ERR_* constant'); $stream = new BufferStream(); new UploadedFile($stream, 0, $error, 'foo.bar', 'foo/bar'); } + /** + * @expectedException \RuntimeException + * @expectedExceptionMessage Not implemented + */ public function testNoMoveFile() { - self::expectException('\RuntimeException'); - self::expectExceptionMessage('Not implemented'); $stream = new BufferStream(); $uploadedFile = new UploadedFile($stream, 0, UPLOAD_ERR_OK, 'foo.bar', 'foo/bar'); $uploadedFile->moveTo('bar.foo'); @@ -50,10 +52,12 @@ public function testGetters() self::assertSame('foo/bar', $uploadedFile->getClientMediaType()); } + /** + * @expectedException \RuntimeException + * @expectedExceptionMessage Cannot retrieve stream due to upload error + */ public function testGetStreamOnFailedUpload() { - self::expectException('\RuntimeException'); - self::expectExceptionMessage('Cannot retrieve stream due to upload error'); $stream = new BufferStream(); $uploadedFile = new UploadedFile($stream, 0, UPLOAD_ERR_NO_FILE, 'foo.bar', 'foo/bar'); $uploadedFile->getStream(); From 88450cf038bc8b947256de10408959f04498a8d8 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Wed, 31 May 2017 19:40:14 +0200 Subject: [PATCH 157/456] PHP 5.3 style arrays --- src/UploadedFile.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/UploadedFile.php b/src/UploadedFile.php index 49248fe2..f7c7223d 100644 --- a/src/UploadedFile.php +++ b/src/UploadedFile.php @@ -46,7 +46,7 @@ public function __construct(StreamInterface $stream, $size, $error, $filename, $ $this->stream = $stream; $this->size = $size; - if (!is_int($error) || !in_array($error, [ + if (!is_int($error) || !in_array($error, array( UPLOAD_ERR_OK, UPLOAD_ERR_INI_SIZE, UPLOAD_ERR_FORM_SIZE, @@ -55,7 +55,7 @@ public function __construct(StreamInterface $stream, $size, $error, $filename, $ UPLOAD_ERR_NO_TMP_DIR, UPLOAD_ERR_CANT_WRITE, UPLOAD_ERR_EXTENSION, - ])) { + ))) { throw new InvalidArgumentException( 'Invalid error code, must be an UPLOAD_ERR_* constant' ); From 7cbb5264cc5bb9301a83eea49a9eb1a8a33b9ae2 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Thu, 8 Jun 2017 10:14:39 +0200 Subject: [PATCH 158/456] Marked UploadedFile internal --- src/UploadedFile.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/UploadedFile.php b/src/UploadedFile.php index f7c7223d..b1794a27 100644 --- a/src/UploadedFile.php +++ b/src/UploadedFile.php @@ -7,6 +7,9 @@ use Psr\Http\Message\UploadedFileInterface; use RuntimeException; +/** + * @internal + */ final class UploadedFile implements UploadedFileInterface { /** From 7b0503841248ac04dfd39bc5b11740a1a8f93a92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 15 Jun 2017 20:19:13 +0200 Subject: [PATCH 159/456] Fix parsing CONNECT request without Host header --- src/RequestHeaderParser.php | 12 ++---------- tests/ServerTest.php | 23 +++++++++++++++++++++++ 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/src/RequestHeaderParser.php b/src/RequestHeaderParser.php index 02d67e32..c7754c51 100644 --- a/src/RequestHeaderParser.php +++ b/src/RequestHeaderParser.php @@ -77,7 +77,7 @@ private function parseRequest($data) // check this is a valid authority-form request-target (host:port) if (isset($uri['scheme'], $uri['host'], $uri['port']) && count($uri) === 3) { $originalTarget = $parts[1]; - $parts[1] = '/'; + $parts[1] = 'http://' . $parts[1] . '/'; $headers = implode(' ', $parts); } else { throw new \InvalidArgumentException('CONNECT method MUST use authority-form request target'); @@ -135,16 +135,8 @@ private function parseRequest($data) // re-apply actual request target from above if ($originalTarget !== null) { - $uri = $request->getUri()->withPath(''); - - // re-apply host and port from request-target if given - $parts = parse_url('tcp://' . $originalTarget); - if (isset($parts['host'], $parts['port'])) { - $uri = $uri->withHost($parts['host'])->withPort($parts['port']); - } - $request = $request->withUri( - $uri, + $request->getUri()->withPath(''), true )->withRequestTarget($originalTarget); } diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 76e2ba13..61ef1fbd 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -225,6 +225,29 @@ public function testRequestConnectAuthorityForm() $this->assertSame('example.com:443', $requestAssertion->getHeaderLine('Host')); } + public function testRequestConnectWithoutHostWillBeAdded() + { + $requestAssertion = null; + $server = new Server(function (ServerRequestInterface $request) use (&$requestAssertion) { + $requestAssertion = $request; + return new Response(); + }); + + $server->listen($this->socket); + $this->socket->emit('connection', array($this->connection)); + + $data = "CONNECT example.com:443 HTTP/1.1\r\n\r\n"; + $this->connection->emit('data', array($data)); + + $this->assertInstanceOf('RingCentral\Psr7\Request', $requestAssertion); + $this->assertSame('CONNECT', $requestAssertion->getMethod()); + $this->assertSame('example.com:443', $requestAssertion->getRequestTarget()); + $this->assertSame('', $requestAssertion->getUri()->getPath()); + $this->assertSame('http://example.com:443', (string)$requestAssertion->getUri()); + $this->assertSame(443, $requestAssertion->getUri()->getPort()); + $this->assertSame('example.com:443', $requestAssertion->getHeaderLine('Host')); + } + public function testRequestConnectAuthorityFormWithDefaultPortWillBeIgnored() { $requestAssertion = null; From 95dc6b7da9cbfa880d4869653c06ccf10903a75b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 16 Jun 2017 09:47:53 +0200 Subject: [PATCH 160/456] Simplify splitting request headers from request body --- src/RequestHeaderParser.php | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/RequestHeaderParser.php b/src/RequestHeaderParser.php index 02d67e32..8414588c 100644 --- a/src/RequestHeaderParser.php +++ b/src/RequestHeaderParser.php @@ -46,7 +46,7 @@ public function feed($data) if (false !== $endOfHeader) { try { - $this->parseAndEmitRequest(); + $this->parseAndEmitRequest($endOfHeader); } catch (Exception $exception) { $this->emit('error', array($exception)); } @@ -54,16 +54,15 @@ public function feed($data) } } - private function parseAndEmitRequest() + private function parseAndEmitRequest($endOfHeader) { - list($request, $bodyBuffer) = $this->parseRequest($this->buffer); + $request = $this->parseRequest((string)substr($this->buffer, 0, $endOfHeader)); + $bodyBuffer = isset($this->buffer[$endOfHeader + 4]) ? substr($this->buffer, $endOfHeader + 4) : ''; $this->emit('headers', array($request, $bodyBuffer)); } - private function parseRequest($data) + private function parseRequest($headers) { - list($headers, $bodyBuffer) = explode("\r\n\r\n", $data, 2); - // parser does not support asterisk-form and authority-form // remember original target and temporarily replace and re-apply below $originalTarget = null; @@ -221,6 +220,6 @@ private function parseRequest($data) // always sanitize Host header because it contains critical routing information $request = $request->withUri($request->getUri()->withUserInfo('u')->withUserInfo('')); - return array($request, $bodyBuffer); + return $request; } } From 0be1fc8b03d3305e18950071a088b55c4418f4df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 17 Jun 2017 17:49:32 +0200 Subject: [PATCH 161/456] Prepare v0.7.1 release --- CHANGELOG.md | 8 ++++++++ README.md | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a9f09f0..2d2879d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 0.7.1 (2017-06-17) + +* Fix: Fix parsing CONNECT request without `Host` header + (#201 by @clue) + +* Internal preparation for future PSR-7 `UploadedFileInterface` + (#199 by @WyriHaximus) + ## 0.7.0 (2017-05-29) * Feature / BC break: Use PSR-7 (http-message) standard and diff --git a/README.md b/README.md index f09371d8..52e5948b 100644 --- a/README.md +++ b/README.md @@ -659,7 +659,7 @@ The recommended way to install this library is [through Composer](http://getcomp This will install the latest supported version: ```bash -$ composer require react/http:^0.7 +$ composer require react/http:^0.7.1 ``` More details about version upgrades can be found in the [CHANGELOG](CHANGELOG.md). From 3a95749e0dc5a73902eba66bb0bab9c2cd3eecc1 Mon Sep 17 00:00:00 2001 From: Ujjwal Ojha Date: Wed, 28 Jun 2017 08:01:50 +0545 Subject: [PATCH 162/456] Fixed typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 52e5948b..51bbef1e 100644 --- a/README.md +++ b/README.md @@ -420,7 +420,7 @@ See also [example #6](examples) for more details. The callback function passed to the constructor of the [Server](#server) is responsible for processing the request and returning a response, which will be delivered to the client. -This function MUST return an instance imlementing +This function MUST return an instance implementing [PSR-7 ResponseInterface](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-7-http-message.md#33-psrhttpmessageresponseinterface) object or a [ReactPHP Promise](https://github.com/reactphp/promise#reactpromise) From bbf48c35107bc20b894759b0a5ca4f2cce6e2808 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 4 Jul 2017 11:29:52 +0200 Subject: [PATCH 163/456] Refactor to use HTTP response reason phrases from response object This means we no longer have to maintain a list of HTTP reason phrases ourselves, but instead rely on those already present in our HTTP message abstraction. --- src/ResponseCodes.php | 74 ------------------------------------------- src/Server.php | 15 +++++---- tests/ServerTest.php | 2 +- 3 files changed, 10 insertions(+), 81 deletions(-) delete mode 100644 src/ResponseCodes.php diff --git a/src/ResponseCodes.php b/src/ResponseCodes.php deleted file mode 100644 index 27b29435..00000000 --- a/src/ResponseCodes.php +++ /dev/null @@ -1,74 +0,0 @@ - 'Continue', - 101 => 'Switching Protocols', - 102 => 'Processing', // RFC2518 - 200 => 'OK', - 201 => 'Created', - 202 => 'Accepted', - 203 => 'Non-Authoritative Information', - 204 => 'No Content', - 205 => 'Reset Content', - 206 => 'Partial Content', - 207 => 'Multi-Status', // RFC4918 - 208 => 'Already Reported', // RFC5842 - 226 => 'IM Used', // RFC3229 - 300 => 'Multiple Choices', - 301 => 'Moved Permanently', - 302 => 'Found', - 303 => 'See Other', - 304 => 'Not Modified', - 305 => 'Use Proxy', - 306 => 'Reserved', - 307 => 'Temporary Redirect', - 308 => 'Permanent Redirect', // RFC-reschke-http-status-308-07 - 400 => 'Bad Request', - 401 => 'Unauthorized', - 402 => 'Payment Required', - 403 => 'Forbidden', - 404 => 'Not Found', - 405 => 'Method Not Allowed', - 406 => 'Not Acceptable', - 407 => 'Proxy Authentication Required', - 408 => 'Request Timeout', - 409 => 'Conflict', - 410 => 'Gone', - 411 => 'Length Required', - 412 => 'Precondition Failed', - 413 => 'Request Entity Too Large', - 414 => 'Request-URI Too Long', - 415 => 'Unsupported Media Type', - 416 => 'Requested Range Not Satisfiable', - 417 => 'Expectation Failed', - 418 => 'I\'m a teapot', // RFC2324 - 422 => 'Unprocessable Entity', // RFC4918 - 423 => 'Locked', // RFC4918 - 424 => 'Failed Dependency', // RFC4918 - 425 => 'Reserved for WebDAV advanced collections expired proposal', // RFC2817 - 426 => 'Upgrade Required', // RFC2817 - 428 => 'Precondition Required', // RFC6585 - 429 => 'Too Many Requests', // RFC6585 - 431 => 'Request Header Fields Too Large', // RFC6585 - 500 => 'Internal Server Error', - 501 => 'Not Implemented', - 502 => 'Bad Gateway', - 503 => 'Service Unavailable', - 504 => 'Gateway Timeout', - 505 => 'HTTP Version Not Supported', - 506 => 'Variant Also Negotiates (Experimental)', // RFC2295 - 507 => 'Insufficient Storage', // RFC4918 - 508 => 'Loop Detected', // RFC5842 - 510 => 'Not Extended', // RFC2774 - 511 => 'Network Authentication Required', // RFC6585 - ); -} diff --git a/src/Server.php b/src/Server.php index 6f9236e4..a642bcce 100644 --- a/src/Server.php +++ b/src/Server.php @@ -275,19 +275,22 @@ function ($error) use ($that, $conn, $request) { /** @internal */ public function writeError(ConnectionInterface $conn, $code, ServerRequestInterface $request = null) { - $message = 'Error ' . $code; - if (isset(ResponseCodes::$statusTexts[$code])) { - $message .= ': ' . ResponseCodes::$statusTexts[$code]; - } - $response = new Response( $code, array( 'Content-Type' => 'text/plain' ), - $message + 'Error ' . $code ); + // append reason phrase to response body if known + $reason = $response->getReasonPhrase(); + if ($reason !== '') { + $body = $response->getBody(); + $body->seek(0, SEEK_END); + $body->write(': ' . $reason); + } + if ($request === null) { $request = new ServerRequest('GET', '/', array(), null, '1.1'); } diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 61ef1fbd..94dc9d22 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -1217,7 +1217,7 @@ function ($data) use (&$buffer) { $this->assertContains("HTTP/1.1 505 HTTP Version not supported\r\n", $buffer); $this->assertContains("\r\n\r\n", $buffer); - $this->assertContains("Error 505: HTTP Version Not Supported", $buffer); + $this->assertContains("Error 505: HTTP Version not supported", $buffer); } public function testRequestOverflowWillEmitErrorAndSendErrorResponse() From 952298c5f5de8970964115311c4b3c9ff9a5d83a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 17 Jun 2017 17:46:16 +0200 Subject: [PATCH 164/456] Stricter check for invalid request-line in HTTP requests --- src/RequestHeaderParser.php | 8 ++++++++ tests/RequestHeaderParserTest.php | 25 +++++++++++++++++++++++-- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/RequestHeaderParser.php b/src/RequestHeaderParser.php index 8414588c..d8d6caf1 100644 --- a/src/RequestHeaderParser.php +++ b/src/RequestHeaderParser.php @@ -63,6 +63,14 @@ private function parseAndEmitRequest($endOfHeader) private function parseRequest($headers) { + // additional, stricter safe-guard for request line + // because request parser doesn't properly cope with invalid ones + if (!preg_match('#^[^ ]+ [^ ]+ HTTP/\d\.\d#m', $headers)) { + throw new \InvalidArgumentException('Unable to parse invalid request-line'); + } + + $lines = explode("\r\n", $headers); + // parser does not support asterisk-form and authority-form // remember original target and temporarily replace and re-apply below $originalTarget = null; diff --git a/tests/RequestHeaderParserTest.php b/tests/RequestHeaderParserTest.php index f4b20a7f..cf200fb8 100644 --- a/tests/RequestHeaderParserTest.php +++ b/tests/RequestHeaderParserTest.php @@ -176,7 +176,7 @@ public function testHeaderOverflowShouldNotEmitErrorWhenDataExceedsMaxHeaderSize $this->assertSame($body, $bodyBuffer); } - public function testGuzzleRequestParseException() + public function testInvalidEmptyRequestHeadersParseException() { $error = null; @@ -192,7 +192,28 @@ public function testGuzzleRequestParseException() $parser->feed("\r\n\r\n"); $this->assertInstanceOf('InvalidArgumentException', $error); - $this->assertSame('Invalid message', $error->getMessage()); + $this->assertSame('Unable to parse invalid request-line', $error->getMessage()); + $this->assertSame(0, count($parser->listeners('headers'))); + $this->assertSame(0, count($parser->listeners('error'))); + } + + public function testInvalidMalformedRequestLineParseException() + { + $error = null; + + $parser = new RequestHeaderParser(); + $parser->on('headers', $this->expectCallableNever()); + $parser->on('error', function ($message) use (&$error) { + $error = $message; + }); + + $this->assertSame(1, count($parser->listeners('headers'))); + $this->assertSame(1, count($parser->listeners('error'))); + + $parser->feed("GET /\r\n\r\n"); + + $this->assertInstanceOf('InvalidArgumentException', $error); + $this->assertSame('Unable to parse invalid request-line', $error->getMessage()); $this->assertSame(0, count($parser->listeners('headers'))); $this->assertSame(0, count($parser->listeners('error'))); } From 32f0eb3d445b1871b2ba859480ee1981977598f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 4 Jul 2017 15:15:44 +0200 Subject: [PATCH 165/456] Prepare v0.7.2 release --- CHANGELOG.md | 8 ++++++++ README.md | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d2879d1..cbc335ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 0.7.2 (2017-07-04) + +* Fix: Stricter check for invalid request-line in HTTP requests + (#206 by @clue) + +* Refactor to use HTTP response reason phrases from response object + (#205 by @clue) + ## 0.7.1 (2017-06-17) * Fix: Fix parsing CONNECT request without `Host` header diff --git a/README.md b/README.md index 51bbef1e..6b892d6c 100644 --- a/README.md +++ b/README.md @@ -659,7 +659,7 @@ The recommended way to install this library is [through Composer](http://getcomp This will install the latest supported version: ```bash -$ composer require react/http:^0.7.1 +$ composer require react/http:^0.7.2 ``` More details about version upgrades can be found in the [CHANGELOG](CHANGELOG.md). From c762f48b1cde76f9cab24cc5007fef99c27369cf Mon Sep 17 00:00:00 2001 From: Jan Sorgalla Date: Fri, 14 Jul 2017 23:20:57 +0200 Subject: [PATCH 166/456] Mention Throwables in the error related sections --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 51bbef1e..d705193f 100644 --- a/README.md +++ b/README.md @@ -118,8 +118,8 @@ $server->on('error', function (Exception $e) { ``` The server will also emit an `error` event if you return an invalid -type in the callback function or have a unhandled `Exception`. -If your callback function throws an exception, +type in the callback function or have a unhandled `Exception` or `Throwable`. +If your callback function throws an `Exception` or `Throwable`, the `Server` will emit a `RuntimeException` and add the thrown exception as previous: @@ -540,9 +540,9 @@ $server = new Server(function (ServerRequestInterface $request) use ($stream) { }); ``` -An invalid return value or an unhandled `Exception` in the code of the callback -function, will result in an `500 Internal Server Error` message. -Make sure to catch `Exceptions` to create own response messages. +An invalid return value or an unhandled `Exception` or `Throwable` in the code +of the callback function, will result in an `500 Internal Server Error` message. +Make sure to catch `Exceptions` or `Throwables` to create own response messages. After the return in the callback function the response will be processed by the `Server`. The `Server` will add the protocol version of the request, so you don't have to. From b0290bd947205bcb4c5fca28c2e8061c70208f76 Mon Sep 17 00:00:00 2001 From: Jan Sorgalla Date: Fri, 14 Jul 2017 23:22:29 +0200 Subject: [PATCH 167/456] Bump minimum react/promise 2.x requirement to ^2.3 as this version started supporting Throwables --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index e73e29c7..e040c784 100644 --- a/composer.json +++ b/composer.json @@ -8,7 +8,7 @@ "ringcentral/psr7": "^1.2", "react/socket": "^1.0 || ^0.8 || ^0.7 || ^0.6 || ^0.5", "react/stream": "^1.0 || ^0.7 || ^0.6 || ^0.5 || ^0.4.6", - "react/promise": "^2.1 || ^1.2.1", + "react/promise": "^2.3 || ^1.2.1", "evenement/evenement": "^2.0 || ^1.0" }, "autoload": { From 8551220c06ed1115516d41ab7df7b8b03a986358 Mon Sep 17 00:00:00 2001 From: Jan Sorgalla Date: Fri, 14 Jul 2017 23:29:23 +0200 Subject: [PATCH 168/456] Adjust test for Server changes --- tests/ServerTest.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 6deb5339..4ea1722a 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -2810,7 +2810,7 @@ function ($data) use (&$buffer) { */ public function testThrowableThrowInCallBackFunctionWillResultInErrorMessage() { - $server = new Server($this->socket, function (RequestInterface $request) { + $server = new Server(function (ServerRequestInterface $request) { throw new \Error('hello'); }); @@ -2831,6 +2831,7 @@ function ($data) use (&$buffer) { ) ); + $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = $this->createGetRequest(); From 209f7d3c3b5aab4a9e4c99cdf247ba9c8250d46e Mon Sep 17 00:00:00 2001 From: Aaron Bonneau Date: Wed, 19 Jul 2017 13:31:41 -0400 Subject: [PATCH 169/456] Fixed URI parsing #207 --- src/RequestHeaderParser.php | 2 +- tests/RequestHeaderParserTest.php | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/RequestHeaderParser.php b/src/RequestHeaderParser.php index 6fb7d0b5..54aae35d 100644 --- a/src/RequestHeaderParser.php +++ b/src/RequestHeaderParser.php @@ -158,7 +158,7 @@ private function parseRequest($headers) $parts = parse_url($request->getRequestTarget()); // 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'])) { + if ((!isset($parts['scheme'], $parts['host']) || $parts['scheme'] !== 'http' || isset($parts['fragment'])) && isset($parts['scheme'])) { throw new \InvalidArgumentException('Invalid absolute-form request-target'); } } diff --git a/tests/RequestHeaderParserTest.php b/tests/RequestHeaderParserTest.php index cf200fb8..9e14548d 100644 --- a/tests/RequestHeaderParserTest.php +++ b/tests/RequestHeaderParserTest.php @@ -234,6 +234,28 @@ public function testInvalidAbsoluteFormSchemeEmitsError() $this->assertSame('Invalid absolute-form request-target', $error->getMessage()); } + public function testOriginFormWithSchemeSeparatorInParam() + { + $request = null; + + $parser = new RequestHeaderParser(); + $parser->on('error', $this->expectCallableNever()); + $parser->on('headers', function ($parsedRequest, $parsedBodyBuffer) use (&$request) { + $request = $parsedRequest; + }); + + $parser->feed("GET /somepath?param=http://example.com HTTP/1.1\r\nHost: localhost\r\n\r\n"); + + $this->assertInstanceOf('Psr\Http\Message\RequestInterface', $request); + $this->assertSame('GET', $request->getMethod()); + $this->assertEquals('http://localhost/somepath?param=http://example.com', $request->getUri()); + $this->assertSame('1.1', $request->getProtocolVersion()); + $headers = array( + 'Host' => array('localhost') + ); + $this->assertSame($headers, $request->getHeaders()); + } + public function testInvalidAbsoluteFormWithFragmentEmitsError() { $error = null; From 0c4b5b7286a1baad3ed4974404c6191509da65f9 Mon Sep 17 00:00:00 2001 From: Aaron Bonneau Date: Fri, 28 Jul 2017 11:03:06 -0400 Subject: [PATCH 170/456] Limit absolute-form check to pass not beginning with '/' --- src/RequestHeaderParser.php | 4 ++-- tests/RequestHeaderParserTest.php | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/RequestHeaderParser.php b/src/RequestHeaderParser.php index 54aae35d..fc6d80bb 100644 --- a/src/RequestHeaderParser.php +++ b/src/RequestHeaderParser.php @@ -154,11 +154,11 @@ private function parseRequest($headers) } // ensure absolute-form request-target contains a valid URI - if (strpos($request->getRequestTarget(), '://') !== false) { + if (strpos($request->getRequestTarget(), '://') !== false && substr($request->getRequestTarget(), 0, 1) !== '/') { $parts = parse_url($request->getRequestTarget()); // 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'])) && isset($parts['scheme'])) { + if (!isset($parts['scheme'], $parts['host']) || $parts['scheme'] !== 'http' || isset($parts['fragment'])) { throw new \InvalidArgumentException('Invalid absolute-form request-target'); } } diff --git a/tests/RequestHeaderParserTest.php b/tests/RequestHeaderParserTest.php index 9e14548d..550b0934 100644 --- a/tests/RequestHeaderParserTest.php +++ b/tests/RequestHeaderParserTest.php @@ -256,6 +256,22 @@ public function testOriginFormWithSchemeSeparatorInParam() $this->assertSame($headers, $request->getHeaders()); } + public function testUriStartingWithColonSlashSlashFails() + { + $error = null; + + $parser = new RequestHeaderParser(); + $parser->on('headers', $this->expectCallableNever()); + $parser->on('error', function ($message) use (&$error) { + $error = $message; + }); + + $parser->feed("GET ://example.com:80/ HTTP/1.0\r\n\r\n"); + + $this->assertInstanceOf('InvalidArgumentException', $error); + $this->assertSame('Invalid request string', $error->getMessage()); + } + public function testInvalidAbsoluteFormWithFragmentEmitsError() { $error = null; From 014eae3f3aac3852243e561c2048a8432ceaed62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 23 Jun 2017 12:18:43 +0200 Subject: [PATCH 171/456] Fix HHVM build for now again and ignore future HHVM build errors --- .travis.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index fcd3a2d6..e29b4ba4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,8 +6,9 @@ php: - 5.5 - 5.6 - 7 - - hhvm +# also test against HHVM, but require "trusty" and ignore errors +# also test lowest dependencies on PHP 5.3 and PHP 7 matrix: include: - php: 5.3 @@ -16,6 +17,8 @@ matrix: - php: 7.0 env: - DEPENDENCIES=lowest + - php: hhvm + dist: trusty allow_failures: - php: hhvm From d0b1152d8800daff8137f4fada4861d557986240 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 4 Aug 2017 12:03:17 +0200 Subject: [PATCH 172/456] Lock Travis distro so new future defaults will not break the build --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index e29b4ba4..a69418e5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,6 +7,9 @@ php: - 5.6 - 7 +# lock distro so new future defaults will not break the build +dist: precise + # also test against HHVM, but require "trusty" and ignore errors # also test lowest dependencies on PHP 5.3 and PHP 7 matrix: From 8ee4fe95152fcf3dcc34a61c0ea7e258f4d32d1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 4 Aug 2017 12:09:49 +0200 Subject: [PATCH 173/456] Update Travis distro to precise, except for legacy PHP --- .travis.yml | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/.travis.yml b/.travis.yml index a69418e5..aa1d8cdc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,30 +1,29 @@ language: php php: - - 5.3 +# - 5.3 # requires old distro, see below - 5.4 - 5.5 - 5.6 - 7 + - hhvm # ignore errors, see below # lock distro so new future defaults will not break the build -dist: precise +dist: trusty -# also test against HHVM, but require "trusty" and ignore errors -# also test lowest dependencies on PHP 5.3 and PHP 7 +# also test lowest dependencies on PHP 7 matrix: include: - php: 5.3 - env: - - DEPENDENCIES=lowest + dist: precise - php: 7.0 env: - DEPENDENCIES=lowest - - php: hhvm - dist: trusty allow_failures: - php: hhvm +sudo: false + install: - composer install --no-interaction - if [ "$DEPENDENCIES" = "lowest" ]; then composer update --prefer-lowest -n; fi From ea09c02677279e3211af4d20ee74794461227e16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 14 Aug 2017 10:14:17 +0200 Subject: [PATCH 174/456] Prepare v0.7.3 release --- CHANGELOG.md | 12 ++++++++++++ README.md | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cbc335ce..ff47b540 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## 0.7.3 (2017-08-14) + +* Feature: Support `Throwable` when setting previous exception from server callback + (#155 by @jsor) + +* Fix: Fixed URI parsing for origin-form requests that contain scheme separator + such as `/path?param=http://example.com`. + (#209 by @aaronbonneau) + +* Improve test suite by locking Travis distro so new defaults will not break the build + (#211 by @clue) + ## 0.7.2 (2017-07-04) * Fix: Stricter check for invalid request-line in HTTP requests diff --git a/README.md b/README.md index ec4ba374..30ef680b 100644 --- a/README.md +++ b/README.md @@ -659,7 +659,7 @@ The recommended way to install this library is [through Composer](http://getcomp This will install the latest supported version: ```bash -$ composer require react/http:^0.7.2 +$ composer require react/http:^0.7.3 ``` More details about version upgrades can be found in the [CHANGELOG](CHANGELOG.md). From 801fafdf361de1f5b09c55a3daa342a3fb7151db Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Tue, 15 Aug 2017 17:43:02 +0200 Subject: [PATCH 175/456] Target evenement 3.0 a long side 2.0 and 1.0 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index e040c784..d058e0d6 100644 --- a/composer.json +++ b/composer.json @@ -9,7 +9,7 @@ "react/socket": "^1.0 || ^0.8 || ^0.7 || ^0.6 || ^0.5", "react/stream": "^1.0 || ^0.7 || ^0.6 || ^0.5 || ^0.4.6", "react/promise": "^2.3 || ^1.2.1", - "evenement/evenement": "^2.0 || ^1.0" + "evenement/evenement": "^3.0 || ^2.0 || ^1.0" }, "autoload": { "psr-4": { From 6646135c01097b5316d2cb47bc12e541bf26efae Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Wed, 16 Aug 2017 17:24:39 +0200 Subject: [PATCH 176/456] Prepare v0.7.4 release --- CHANGELOG.md | 5 +++++ README.md | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ff47b540..20aca1f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 0.7.4 (2017-08-16) + +* Improvement: Target evenement 3.0 a long side 2.0 and 1.0 + (#212 by @WyriHaximus) + ## 0.7.3 (2017-08-14) * Feature: Support `Throwable` when setting previous exception from server callback diff --git a/README.md b/README.md index 30ef680b..51405fda 100644 --- a/README.md +++ b/README.md @@ -659,7 +659,7 @@ The recommended way to install this library is [through Composer](http://getcomp This will install the latest supported version: ```bash -$ composer require react/http:^0.7.3 +$ composer require react/http:^0.7.4 ``` More details about version upgrades can be found in the [CHANGELOG](CHANGELOG.md). From 1c82840c507752ecd76249888b0649c5ced5a28d Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Wed, 6 Sep 2017 19:39:30 +0200 Subject: [PATCH 177/456] Middleware Runner --- README.md | 26 +++++++++ src/MiddlewareRunner.php | 53 ++++++++++++++++++ tests/FunctionalServerTest.php | 54 ++++++++++++++++++ tests/Middleware/ProcessStack.php | 28 ++++++++++ tests/MiddlewareRunnerTest.php | 91 +++++++++++++++++++++++++++++++ tests/ServerTest.php | 36 ++++++++++++ 6 files changed, 288 insertions(+) create mode 100644 src/MiddlewareRunner.php create mode 100644 tests/Middleware/ProcessStack.php create mode 100644 tests/MiddlewareRunnerTest.php diff --git a/README.md b/README.md index 51405fda..8f9e09da 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ Event-driven, streaming plaintext HTTP and secure HTTPS server for [ReactPHP](ht * [Server](#server) * [Request](#request) * [Response](#response) + * [Middleware](#middleware) * [Install](#install) * [Tests](#tests) * [License](#license) @@ -651,6 +652,31 @@ As such, HTTP/1.1 response messages will automatically include a `Connection: close` header, irrespective of what header values are passed explicitly. +### Middleware + +Middleware can be added to the server using [`MiddlewareRunner`](src/MiddlewareRunner.php) +instead of the `callable`. A middleware is expected to adhere the following rules: + +* It is a `callable`. +* It accepts `ServerRequestInterface` as first argument and optional `callable` as second argument. +* It returns a `ResponseInterface` (or any promise which can be consumed by [`Promise\resolve`](http://reactphp.org/promise/#resolve) resolving to a `ResponseInterface`) +* It calls `$next($request)` to continue processing the next middleware function or returns explicitly to abort the chain + +The following example adds a middleware that adds the current time to the request as a +header (`Request-Time`) and middleware that always returns a 200 code without a body: + +```php +$server = new Server(new MiddlewareRunner([ + function (ServerRequestInterface $request, callable $next) { + $request = $request->withHeader('Request-Time', time()); + return $next($request); + }, + function (ServerRequestInterface $request, callable $next) { + return new Response(200); + }, +])); +``` + ## Install The recommended way to install this library is [through Composer](http://getcomposer.org). diff --git a/src/MiddlewareRunner.php b/src/MiddlewareRunner.php new file mode 100644 index 00000000..2320ced2 --- /dev/null +++ b/src/MiddlewareRunner.php @@ -0,0 +1,53 @@ +middleware = $middleware; + } + + /** + * @param ServerRequestInterface $request + * @return PromiseInterface + */ + public function __invoke(ServerRequestInterface $request) + { + if (count($this->middleware) === 0) { + return Promise\reject(new \RuntimeException('No middleware to run')); + } + + $middlewareCollection = $this->middleware; + $middleware = array_shift($middlewareCollection); + + $cancel = null; + return new Promise\Promise(function ($resolve, $reject) use ($middleware, $request, $middlewareCollection, &$cancel) { + $cancel = $middleware( + $request, + new MiddlewareRunner( + $middlewareCollection + ) + ); + $resolve($cancel); + }, function () use (&$cancel) { + if ($cancel instanceof Promise\CancellablePromiseInterface) { + $cancel->cancel(); + } + }); + } +} diff --git a/tests/FunctionalServerTest.php b/tests/FunctionalServerTest.php index 06f06db9..a0b6f0b7 100644 --- a/tests/FunctionalServerTest.php +++ b/tests/FunctionalServerTest.php @@ -2,6 +2,7 @@ namespace React\Tests\Http; +use React\Http\MiddlewareRunner; use React\Socket\Server as Socket; use React\EventLoop\Factory; use React\Http\Server; @@ -45,6 +46,59 @@ public function testPlainHttpOnRandomPort() $socket->close(); } + public function testPlainHttpOnRandomPortWithMiddlewareRunner() + { + $loop = Factory::create(); + $connector = new Connector($loop); + + $server = new Server(new MiddlewareRunner(array(function (RequestInterface $request) { + return new Response(200, array(), (string)$request->getUri()); + }))); + + $socket = new Socket(0, $loop); + $server->listen($socket); + + $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { + $conn->write("GET / HTTP/1.0\r\nHost: " . noScheme($conn->getRemoteAddress()) . "\r\n\r\n"); + + return Stream\buffer($conn); + }); + + $response = Block\await($result, $loop, 1.0); + + $this->assertContains("HTTP/1.0 200 OK", $response); + $this->assertContains('http://' . noScheme($socket->getAddress()) . '/', $response); + + $socket->close(); + } + + public function testPlainHttpOnRandomPortWithEmptyMiddlewareRunner() + { + $loop = Factory::create(); + $connector = new Connector($loop); + + $server = new Server(new MiddlewareRunner(array( + function () { + return new Response(404); + }, + ))); + + $socket = new Socket(0, $loop); + $server->listen($socket); + + $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { + $conn->write("GET / HTTP/1.0\r\nHost: " . noScheme($conn->getRemoteAddress()) . "\r\n\r\n"); + + return Stream\buffer($conn); + }); + + $response = Block\await($result, $loop, 1.0); + + $this->assertContains("HTTP/1.0 404 Not Found", $response); + + $socket->close(); + } + public function testPlainHttpOnRandomPortWithoutHostHeaderUsesSocketUri() { $loop = Factory::create(); diff --git a/tests/Middleware/ProcessStack.php b/tests/Middleware/ProcessStack.php new file mode 100644 index 00000000..c0924865 --- /dev/null +++ b/tests/Middleware/ProcessStack.php @@ -0,0 +1,28 @@ +callCount++; + return Promise\resolve($stack($request)); + } + + /** + * @return int + */ + public function getCallCount() + { + return $this->callCount; + } +} diff --git a/tests/MiddlewareRunnerTest.php b/tests/MiddlewareRunnerTest.php new file mode 100644 index 00000000..5b01767e --- /dev/null +++ b/tests/MiddlewareRunnerTest.php @@ -0,0 +1,91 @@ +setExpectedException('\RuntimeException'); + $request = new ServerRequest('GET', 'https://example.com/'); + $middlewares = array(); + $middlewareStack = new MiddlewareRunner($middlewares); + + Block\await($middlewareStack($request), Factory::create()); + } + + public function provideProcessStackMiddlewares() + { + $processStackA = new ProcessStack(); + $processStackB = new ProcessStack(); + $processStackC = new ProcessStack(); + $processStackD = new ProcessStack(); + $responseMiddleware = function () { + return new Response(200); + }; + return array( + array( + array( + $processStackA, + $responseMiddleware, + ), + 1, + ), + array( + array( + $processStackB, + $processStackB, + $responseMiddleware, + ), + 2, + ), + array( + array( + $processStackC, + $processStackC, + $processStackC, + $responseMiddleware, + ), + 3, + ), + array( + array( + $processStackD, + $processStackD, + $processStackD, + $processStackD, + $responseMiddleware, + ), + 4, + ), + ); + } + + /** + * @dataProvider provideProcessStackMiddlewares + */ + public function testProcessStack(array $middlewares, $expectedCallCount) + { + $request = new ServerRequest('GET', 'https://example.com/'); + $middlewareStack = new MiddlewareRunner($middlewares); + + /** @var ResponseInterface $result */ + $result = Block\await($middlewareStack($request), Factory::create()); + $this->assertSame(200, $result->getStatusCode()); + foreach ($middlewares as $middleware) { + if (!($middleware instanceof ProcessStack)) { + continue; + } + + $this->assertSame($expectedCallCount, $middleware->getCallCount()); + } + } +} diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 88d5e54b..2031ae9e 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -2,6 +2,7 @@ namespace React\Tests\Http; +use React\Http\MiddlewareRunner; use React\Http\Server; use Psr\Http\Message\ServerRequestInterface; use React\Http\Response; @@ -99,6 +100,41 @@ public function testRequestEvent() $this->assertSame('127.0.0.1', $serverParams['REMOTE_ADDR']); } + public function testRequestEventWithMiddlewareRunner() + { + $i = 0; + $requestAssertion = null; + $server = new Server(new MiddlewareRunner(array(function (ServerRequestInterface $request) use (&$i, &$requestAssertion) { + $i++; + $requestAssertion = $request; + + return \React\Promise\resolve(new Response()); + }))); + + $this->connection + ->expects($this->any()) + ->method('getRemoteAddress') + ->willReturn('127.0.0.1:8080'); + + $server->listen($this->socket); + $this->socket->emit('connection', array($this->connection)); + + $data = $this->createGetRequest(); + $this->connection->emit('data', array($data)); + + $serverParams = $requestAssertion->getServerParams(); + + $this->assertSame(1, $i); + $this->assertInstanceOf('RingCentral\Psr7\Request', $requestAssertion); + $this->assertSame('GET', $requestAssertion->getMethod()); + $this->assertSame('/', $requestAssertion->getRequestTarget()); + $this->assertSame('/', $requestAssertion->getUri()->getPath()); + $this->assertSame(array(), $requestAssertion->getQueryParams()); + $this->assertSame('http://example.com/', (string)$requestAssertion->getUri()); + $this->assertSame('example.com', $requestAssertion->getHeaderLine('Host')); + $this->assertSame('127.0.0.1', $serverParams['REMOTE_ADDR']); + } + public function testRequestGetWithHostAndCustomPort() { $requestAssertion = null; From ae26b1fa12a59a290dfd2db57fd9d5adc022a493 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Wed, 6 Sep 2017 22:02:29 +0200 Subject: [PATCH 178/456] Buffer Middleware --- README.md | 19 ++++++++ src/Middleware/Buffer.php | 66 ++++++++++++++++++++++++++ tests/Middleware/BufferTest.php | 82 +++++++++++++++++++++++++++++++++ 3 files changed, 167 insertions(+) create mode 100644 src/Middleware/Buffer.php create mode 100644 tests/Middleware/BufferTest.php diff --git a/README.md b/README.md index 8f9e09da..1f6d7506 100644 --- a/README.md +++ b/README.md @@ -677,6 +677,25 @@ $server = new Server(new MiddlewareRunner([ ])); ``` +#### Buffer + +One of the build in middleware is `Buffer` which will buffer the incoming +request body until the reported size has been reached. Then it will +call the middleware stack with the new request instance containing the +full request body. + +Usage: + +```php +$middlewares = new MiddlewareRunner([ + new Buffer(), + function (ServerRequestInterface $request, callable $next) { + // The body from $request->getBody() is now fully available without the need to stream it + return new Response(200); + }, +]); +``` + ## Install The recommended way to install this library is [through Composer](http://getcomposer.org). diff --git a/src/Middleware/Buffer.php b/src/Middleware/Buffer.php new file mode 100644 index 00000000..b76aa400 --- /dev/null +++ b/src/Middleware/Buffer.php @@ -0,0 +1,66 @@ +iniMaxPostSize(); + } + + $this->sizeLimit = $sizeLimit; + } + + public function __invoke(ServerRequestInterface $request, $stack) + { + $size = $request->getBody()->getSize(); + + if ($size === null) { + return new Response(411, array('Content-Type' => 'text/plain'), 'No Content-Length header given'); + } + + if ($size > $this->sizeLimit) { + return new Response(413, array('Content-Type' => 'text/plain'), 'Request body exceeds allowed limit'); + } + + $body = $request->getBody(); + if (!$body instanceof ReadableStreamInterface) { + return $stack($request); + } + + return Stream\buffer($body)->then(function ($buffer) use ($request, $stack) { + $stream = new BufferStream(strlen($buffer)); + $stream->write($buffer); + $request = $request->withBody($stream); + + return $stack($request); + }); + } + + private function iniMaxPostSize() + { + $size = ini_get('post_max_size'); + $suffix = strtoupper(substr($size, -1)); + if ($suffix === 'K') { + return substr($size, 0, -1) * 1024; + } + if ($suffix === 'M') { + return substr($size, 0, -1) * 1024 * 1024; + } + if ($suffix === 'G') { + return substr($size, 0, -1) * 1024 * 1024 * 1024; + } + + return $size; + } +} diff --git a/tests/Middleware/BufferTest.php b/tests/Middleware/BufferTest.php new file mode 100644 index 00000000..9fdcdf7f --- /dev/null +++ b/tests/Middleware/BufferTest.php @@ -0,0 +1,82 @@ +write($body); + $serverRequest = new ServerRequest( + 'GET', + 'https://example.com/', + array(), + $stream + ); + + $exposedRequest = null; + $buffer = new Buffer(); + $buffer( + $serverRequest, + function (ServerRequestInterface $request) use (&$exposedRequest) { + $exposedRequest = $request; + } + ); + + $this->assertSame($body, $exposedRequest->getBody()->getContents()); + } + + public function testToLargeBody() + { + $size = $this->iniMaxPostSize() + 1; + $stream = new BufferStream($size); + $stream->write(str_repeat('x', $size)); + $serverRequest = new ServerRequest( + 'GET', + 'https://example.com/', + array(), + $stream + ); + + $shouldNotHaveBeenCalled = false; + $buffer = new Buffer(); + $response = $buffer( + $serverRequest, + function () use (&$shouldNotHaveBeenCalled) { + $shouldNotHaveBeenCalled = true; + } + ); + + $this->assertFalse($shouldNotHaveBeenCalled); + $this->assertInstanceOf('React\Http\Response', $response); + $this->assertSame(413, $response->getStatusCode()); + $this->assertSame('Request body exceeds allowed limit', (string)$response->getBody()); + } + + private function iniMaxPostSize() + { + $size = ini_get('post_max_size'); + $suffix = strtoupper(substr($size, -1)); + if ($suffix === 'K') { + return substr($size, 0, -1) * 1024; + } + if ($suffix === 'M') { + return substr($size, 0, -1) * 1024 * 1024; + } + if ($suffix === 'G') { + return substr($size, 0, -1) * 1024 * 1024 * 1024; + } + + return $size; + } +} From a68b2a3ac5caa2566e72b11c7fd6f5888440c3c7 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Fri, 8 Sep 2017 09:08:36 +0200 Subject: [PATCH 179/456] Renamed Buffer middleware to RequestBodyBuffer --- README.md | 20 +++-- .../{Buffer.php => RequestBodyBuffer.php} | 16 +++- tests/Middleware/BufferTest.php | 82 ------------------- tests/Middleware/RequestBodyBufferTest.php | 79 ++++++++++++++++++ 4 files changed, 108 insertions(+), 89 deletions(-) rename src/Middleware/{Buffer.php => RequestBodyBuffer.php} (74%) delete mode 100644 tests/Middleware/BufferTest.php create mode 100644 tests/Middleware/RequestBodyBufferTest.php diff --git a/README.md b/README.md index 1f6d7506..53e44213 100644 --- a/README.md +++ b/README.md @@ -677,18 +677,26 @@ $server = new Server(new MiddlewareRunner([ ])); ``` -#### Buffer +#### RequestBodyBuffer -One of the build in middleware is `Buffer` which will buffer the incoming -request body until the reported size has been reached. Then it will -call the middleware stack with the new request instance containing the -full request body. +One of the build in middleware is `RequestBodyBuffer` which will buffer the incoming +request body until the reported size has been reached. Then it will call the next +middleware in line with the new request instance containing the full request body. +The constructor accepts one argument, a maximum request body size. When one isn't +provided it will use `post_max_size` from PHP's configuration. +(**Note that the value from the CLI configuration will be used.**) + +Before buffering the request body the `RequestBodyBuffer` will check if the request +body has a size. When size is null a [HTTP 411](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/411) +response will be send to the client. When size is bigger then supplied to the `RequestBodyBuffer` +constructor or taken from `post_max_size` a [HTTP 413](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/413) +response will be dispatched. Usage: ```php $middlewares = new MiddlewareRunner([ - new Buffer(), + new RequestBodyBuffer(), function (ServerRequestInterface $request, callable $next) { // The body from $request->getBody() is now fully available without the need to stream it return new Response(200); diff --git a/src/Middleware/Buffer.php b/src/Middleware/RequestBodyBuffer.php similarity index 74% rename from src/Middleware/Buffer.php rename to src/Middleware/RequestBodyBuffer.php index b76aa400..3ebb7df8 100644 --- a/src/Middleware/Buffer.php +++ b/src/Middleware/RequestBodyBuffer.php @@ -8,10 +8,16 @@ use React\Stream\ReadableStreamInterface; use RingCentral\Psr7\BufferStream; -final class Buffer +final class RequestBodyBuffer { private $sizeLimit; + /** + * @param int|null $sizeLimit Either an int with the max request body size + * or null to use post_max_size from PHP's + * configuration. (Note that the value from + * the CLI configuration will be used.) + */ public function __construct($sizeLimit = null) { if ($sizeLimit === null) { @@ -47,6 +53,14 @@ public function __invoke(ServerRequestInterface $request, $stack) }); } + /** + * Gets post_max_size from PHP's configuration + * and turns it into bytes up to a maximum of GigaBytes. + * Anything other than configured as Bytes, KiloBytes, MegaBytes, or GigaBytes + * is considered out of range. + * + * @return int + */ private function iniMaxPostSize() { $size = ini_get('post_max_size'); diff --git a/tests/Middleware/BufferTest.php b/tests/Middleware/BufferTest.php deleted file mode 100644 index 9fdcdf7f..00000000 --- a/tests/Middleware/BufferTest.php +++ /dev/null @@ -1,82 +0,0 @@ -write($body); - $serverRequest = new ServerRequest( - 'GET', - 'https://example.com/', - array(), - $stream - ); - - $exposedRequest = null; - $buffer = new Buffer(); - $buffer( - $serverRequest, - function (ServerRequestInterface $request) use (&$exposedRequest) { - $exposedRequest = $request; - } - ); - - $this->assertSame($body, $exposedRequest->getBody()->getContents()); - } - - public function testToLargeBody() - { - $size = $this->iniMaxPostSize() + 1; - $stream = new BufferStream($size); - $stream->write(str_repeat('x', $size)); - $serverRequest = new ServerRequest( - 'GET', - 'https://example.com/', - array(), - $stream - ); - - $shouldNotHaveBeenCalled = false; - $buffer = new Buffer(); - $response = $buffer( - $serverRequest, - function () use (&$shouldNotHaveBeenCalled) { - $shouldNotHaveBeenCalled = true; - } - ); - - $this->assertFalse($shouldNotHaveBeenCalled); - $this->assertInstanceOf('React\Http\Response', $response); - $this->assertSame(413, $response->getStatusCode()); - $this->assertSame('Request body exceeds allowed limit', (string)$response->getBody()); - } - - private function iniMaxPostSize() - { - $size = ini_get('post_max_size'); - $suffix = strtoupper(substr($size, -1)); - if ($suffix === 'K') { - return substr($size, 0, -1) * 1024; - } - if ($suffix === 'M') { - return substr($size, 0, -1) * 1024 * 1024; - } - if ($suffix === 'G') { - return substr($size, 0, -1) * 1024 * 1024 * 1024; - } - - return $size; - } -} diff --git a/tests/Middleware/RequestBodyBufferTest.php b/tests/Middleware/RequestBodyBufferTest.php new file mode 100644 index 00000000..aad88018 --- /dev/null +++ b/tests/Middleware/RequestBodyBufferTest.php @@ -0,0 +1,79 @@ +write($body); + $serverRequest = new ServerRequest( + 'GET', + 'https://example.com/', + array(), + $stream + ); + + $exposedRequest = null; + $buffer = new RequestBodyBuffer(); + $buffer( + $serverRequest, + function (ServerRequestInterface $request) use (&$exposedRequest) { + $exposedRequest = $request; + } + ); + + $this->assertSame($body, $exposedRequest->getBody()->getContents()); + } + + public function test411Error() + { + $body = $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(); + $body->expects($this->once())->method('getSize')->willReturn(null); + + $serverRequest = new ServerRequest( + 'GET', + 'https://example.com/', + array(), + $body + ); + + $buffer = new RequestBodyBuffer(); + $response = $buffer( + $serverRequest, + function () {} + ); + + $this->assertSame(411, $response->getStatusCode()); + } + + public function test413Error() + { + $stream = new BufferStream(2); + $stream->write('aa'); + + $serverRequest = new ServerRequest( + 'GET', + 'https://example.com/', + array(), + $stream + ); + + $buffer = new RequestBodyBuffer(1); + $response = $buffer( + $serverRequest, + function () {} + ); + + $this->assertSame(413, $response->getStatusCode()); + } +} From 604d79a297ea3f654121f0a9e0d516965677dee7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 11 Sep 2017 14:49:17 +0200 Subject: [PATCH 180/456] Rename RequestBodyBuffer to RequestBodyBufferMiddleware --- README.md | 11 ++++++----- ...BodyBuffer.php => RequestBodyBufferMiddleware.php} | 2 +- ...erTest.php => RequestBodyBufferMiddlewareTest.php} | 2 +- 3 files changed, 8 insertions(+), 7 deletions(-) rename src/Middleware/{RequestBodyBuffer.php => RequestBodyBufferMiddleware.php} (98%) rename tests/Middleware/{RequestBodyBufferTest.php => RequestBodyBufferMiddlewareTest.php} (96%) diff --git a/README.md b/README.md index 53e44213..510cdf38 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ Event-driven, streaming plaintext HTTP and secure HTTPS server for [ReactPHP](ht * [Request](#request) * [Response](#response) * [Middleware](#middleware) + * [RequestBodyBufferMiddleware](#requestbodybuffermiddleware) * [Install](#install) * [Tests](#tests) * [License](#license) @@ -677,18 +678,18 @@ $server = new Server(new MiddlewareRunner([ ])); ``` -#### RequestBodyBuffer +#### RequestBodyBufferMiddleware -One of the build in middleware is `RequestBodyBuffer` which will buffer the incoming +One of the build in middleware is `RequestBodyBufferMiddleware` which will buffer the incoming request body until the reported size has been reached. Then it will call the next middleware in line with the new request instance containing the full request body. The constructor accepts one argument, a maximum request body size. When one isn't provided it will use `post_max_size` from PHP's configuration. (**Note that the value from the CLI configuration will be used.**) -Before buffering the request body the `RequestBodyBuffer` will check if the request +Before buffering the request body the `RequestBodyBufferMiddleware` will check if the request body has a size. When size is null a [HTTP 411](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/411) -response will be send to the client. When size is bigger then supplied to the `RequestBodyBuffer` +response will be send to the client. When size is bigger then supplied to the `RequestBodyBufferMiddleware` constructor or taken from `post_max_size` a [HTTP 413](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/413) response will be dispatched. @@ -696,7 +697,7 @@ Usage: ```php $middlewares = new MiddlewareRunner([ - new RequestBodyBuffer(), + new RequestBodyBufferMiddleware(), function (ServerRequestInterface $request, callable $next) { // The body from $request->getBody() is now fully available without the need to stream it return new Response(200); diff --git a/src/Middleware/RequestBodyBuffer.php b/src/Middleware/RequestBodyBufferMiddleware.php similarity index 98% rename from src/Middleware/RequestBodyBuffer.php rename to src/Middleware/RequestBodyBufferMiddleware.php index 3ebb7df8..9bb11750 100644 --- a/src/Middleware/RequestBodyBuffer.php +++ b/src/Middleware/RequestBodyBufferMiddleware.php @@ -8,7 +8,7 @@ use React\Stream\ReadableStreamInterface; use RingCentral\Psr7\BufferStream; -final class RequestBodyBuffer +final class RequestBodyBufferMiddleware { private $sizeLimit; diff --git a/tests/Middleware/RequestBodyBufferTest.php b/tests/Middleware/RequestBodyBufferMiddlewareTest.php similarity index 96% rename from tests/Middleware/RequestBodyBufferTest.php rename to tests/Middleware/RequestBodyBufferMiddlewareTest.php index aad88018..3468e0dc 100644 --- a/tests/Middleware/RequestBodyBufferTest.php +++ b/tests/Middleware/RequestBodyBufferMiddlewareTest.php @@ -8,7 +8,7 @@ use React\Tests\Http\TestCase; use RingCentral\Psr7\BufferStream; -final class RequestBodyBufferTest extends TestCase +final class RequestBodyBufferMiddlewareTest extends TestCase { public function testBuffer() { From c8d66b8a27f0b4a4addbe4138535b4cf44b570da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 11 Sep 2017 19:11:56 +0200 Subject: [PATCH 181/456] Improve documentation for buffering the request body --- README.md | 58 +++++++++++++------ .../RequestBodyBufferMiddleware.php | 7 +-- .../RequestBodyBufferMiddlewareTest.php | 42 +++++++++++--- 3 files changed, 77 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 510cdf38..d113314c 100644 --- a/README.md +++ b/README.md @@ -238,8 +238,9 @@ and `Server`, but you can add these parameters by yourself using the given methods. The next versions of this project will cover these features. -Note that the request object will be processed once the request headers have -been received. +Note that by default, the request object will be processed once the request headers have +been received (see also [`RequestBodyBufferMiddleware`](#requestbodybuffermiddleware) +for an alternative). This means that this happens irrespective of (i.e. *before*) receiving the (potentially much larger) request body. While this may be uncommon in the PHP ecosystem, this is actually a very powerful @@ -254,7 +255,7 @@ approach that gives you several advantages not otherwise possible: such as accepting a huge file upload or possibly unlimited request body stream. The `getBody()` method can be used to access the request body stream. -This method returns a stream instance that implements both the +In the default streaming mode, this method returns a stream instance that implements both the [PSR-7 StreamInterface](http://www.php-fig.org/psr/psr-7/#psrhttpmessagestreaminterface) and the [ReactPHP ReadableStreamInterface](https://github.com/reactphp/stream#readablestreaminterface). However, most of the `PSR-7 StreamInterface` methods have been @@ -262,8 +263,10 @@ designed under the assumption of being in control of the request body. Given that this does not apply to this server, the following `PSR-7 StreamInterface` methods are not used and SHOULD NOT be called: `tell()`, `eof()`, `seek()`, `rewind()`, `write()` and `read()`. -Instead, you should use the `ReactPHP ReadableStreamInterface` which -gives you access to the incoming request body as the individual chunks arrive: +If this is an issue for your use case, it's highly recommended to use the +[`RequestBodyBufferMiddleware`](#requestbodybuffermiddleware) instead. +The `ReactPHP ReadableStreamInterface` gives you access to the incoming +request body as the individual chunks arrive: ```php $server = new Server(function (ServerRequestInterface $request) { @@ -298,7 +301,7 @@ $server = new Server(function (ServerRequestInterface $request) { The above example simply counts the number of bytes received in the request body. This can be used as a skeleton for buffering or processing the request body. -See also [example #4](examples) for more details. +See also [example #9](examples) for more details. The `data` event will be emitted whenever new data is available on the request body stream. @@ -415,7 +418,7 @@ non-alphanumeric characters. This encoding is also used internally when decoding the name and value of cookies (which is in line with other implementations, such as PHP's cookie functions). -See also [example #6](examples) for more details. +See also [example #5](examples) for more details. ### Response @@ -680,24 +683,41 @@ $server = new Server(new MiddlewareRunner([ #### RequestBodyBufferMiddleware -One of the build in middleware is `RequestBodyBufferMiddleware` which will buffer the incoming -request body until the reported size has been reached. Then it will call the next -middleware in line with the new request instance containing the full request body. -The constructor accepts one argument, a maximum request body size. When one isn't -provided it will use `post_max_size` from PHP's configuration. -(**Note that the value from the CLI configuration will be used.**) +One of the built-in middleware is the `RequestBodyBufferMiddleware` which +can be used to buffer the whole incoming request body in memory. +This can be useful if full PSR-7 compatibility is needed for the request handler +and the default streaming request body handling is not needed. +The constructor accepts one optional argument, the maximum request body size. +When one isn't provided it will use `post_max_size` (default 8 MiB) from PHP's +configuration. +(Note that the value from your matching SAPI will be used, which is the CLI +configuration in most cases.) + +Any incoming request that has a request body that exceeds this limit will be +rejected with a `413` (Request Entity Too Large) error message without calling +the next middleware handlers. + +Any incoming request that does not have its size defined and uses the (rare) +`Transfer-Encoding: chunked` will be rejected with a `411` (Length Required) +error message without calling the next middleware handlers. +Note that this only affects incoming requests, the much more common chunked +transfer encoding for outgoing responses is not affected. +It is recommended to define a `Content-Length` header instead. +Note that this does not affect normal requests without a request body +(such as a simple `GET` request). -Before buffering the request body the `RequestBodyBufferMiddleware` will check if the request -body has a size. When size is null a [HTTP 411](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/411) -response will be send to the client. When size is bigger then supplied to the `RequestBodyBufferMiddleware` -constructor or taken from `post_max_size` a [HTTP 413](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/413) -response will be dispatched. +All other requests will be buffered in memory until the request body end has +been reached and then call the next middleware handler with the complete, +buffered request. +Similarly, this will immediately invoke the next middleware handler for requests +that have an empty request body (such as a simple `GET` request) and requests +that are already buffered (such as due to another middleware). Usage: ```php $middlewares = new MiddlewareRunner([ - new RequestBodyBufferMiddleware(), + new RequestBodyBufferMiddleware(16 * 1024 * 1024), // 16 MiB function (ServerRequestInterface $request, callable $next) { // The body from $request->getBody() is now fully available without the need to stream it return new Response(200); diff --git a/src/Middleware/RequestBodyBufferMiddleware.php b/src/Middleware/RequestBodyBufferMiddleware.php index 9bb11750..93ba2126 100644 --- a/src/Middleware/RequestBodyBufferMiddleware.php +++ b/src/Middleware/RequestBodyBufferMiddleware.php @@ -54,12 +54,11 @@ public function __invoke(ServerRequestInterface $request, $stack) } /** - * Gets post_max_size from PHP's configuration - * and turns it into bytes up to a maximum of GigaBytes. - * Anything other than configured as Bytes, KiloBytes, MegaBytes, or GigaBytes - * is considered out of range. + * Gets post_max_size from PHP's configuration expressed in bytes * * @return int + * @link http://php.net/manual/en/ini.core.php#ini.post-max-size + * @codeCoverageIgnore */ private function iniMaxPostSize() { diff --git a/tests/Middleware/RequestBodyBufferMiddlewareTest.php b/tests/Middleware/RequestBodyBufferMiddlewareTest.php index 3468e0dc..337f5c03 100644 --- a/tests/Middleware/RequestBodyBufferMiddlewareTest.php +++ b/tests/Middleware/RequestBodyBufferMiddlewareTest.php @@ -3,14 +3,42 @@ namespace React\Tests\Http\Middleware; use Psr\Http\Message\ServerRequestInterface; -use React\Http\Middleware\RequestBodyBuffer; +use React\Http\Middleware\RequestBodyBufferMiddleware; use React\Http\ServerRequest; use React\Tests\Http\TestCase; use RingCentral\Psr7\BufferStream; +use React\Stream\ThroughStream; +use React\Http\HttpBodyStream; final class RequestBodyBufferMiddlewareTest extends TestCase { - public function testBuffer() + public function testBufferingResolvesWhenStreamEnds() + { + $stream = new ThroughStream(); + $serverRequest = new ServerRequest( + 'GET', + 'https://example.com/', + array(), + new HttpBodyStream($stream, 11) + ); + + $exposedRequest = null; + $buffer = new RequestBodyBufferMiddleware(20); + $buffer( + $serverRequest, + function (ServerRequestInterface $request) use (&$exposedRequest) { + $exposedRequest = $request; + } + ); + + $stream->write('hello'); + $stream->write('world'); + $stream->end('!'); + + $this->assertSame('helloworld!', $exposedRequest->getBody()->getContents()); + } + + public function testAlreadyBufferedResolvesImmediately() { $size = 1024; $body = str_repeat('x', $size); @@ -24,7 +52,7 @@ public function testBuffer() ); $exposedRequest = null; - $buffer = new RequestBodyBuffer(); + $buffer = new RequestBodyBufferMiddleware(); $buffer( $serverRequest, function (ServerRequestInterface $request) use (&$exposedRequest) { @@ -35,7 +63,7 @@ function (ServerRequestInterface $request) use (&$exposedRequest) { $this->assertSame($body, $exposedRequest->getBody()->getContents()); } - public function test411Error() + public function testUnknownSizeReturnsError411() { $body = $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(); $body->expects($this->once())->method('getSize')->willReturn(null); @@ -47,7 +75,7 @@ public function test411Error() $body ); - $buffer = new RequestBodyBuffer(); + $buffer = new RequestBodyBufferMiddleware(); $response = $buffer( $serverRequest, function () {} @@ -56,7 +84,7 @@ function () {} $this->assertSame(411, $response->getStatusCode()); } - public function test413Error() + public function testExcessiveSizeReturnsError413() { $stream = new BufferStream(2); $stream->write('aa'); @@ -68,7 +96,7 @@ public function test413Error() $stream ); - $buffer = new RequestBodyBuffer(1); + $buffer = new RequestBodyBufferMiddleware(1); $response = $buffer( $serverRequest, function () {} From 04dd9d21c7780a1dcd8ff0c32ff47cc499900d5b Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Wed, 20 Sep 2017 22:16:18 +0200 Subject: [PATCH 182/456] react/promise-stream has moved from dev dependency to normal dependency --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index d058e0d6..e32a4762 100644 --- a/composer.json +++ b/composer.json @@ -9,7 +9,8 @@ "react/socket": "^1.0 || ^0.8 || ^0.7 || ^0.6 || ^0.5", "react/stream": "^1.0 || ^0.7 || ^0.6 || ^0.5 || ^0.4.6", "react/promise": "^2.3 || ^1.2.1", - "evenement/evenement": "^3.0 || ^2.0 || ^1.0" + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "react/promise-stream": "^0.1.1" }, "autoload": { "psr-4": { @@ -18,7 +19,6 @@ }, "require-dev": { "phpunit/phpunit": "^4.8.10||^5.0", - "react/promise-stream": "^0.1.1", "react/socket": "^1.0 || ^0.8 || ^0.7", "clue/block-react": "^1.1" } From a4e9092394e7762f7116c55cd80dc6ab7de243a6 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Mon, 11 Sep 2017 22:12:27 +0200 Subject: [PATCH 183/456] RequestBodyParser middleware --- README.md | 34 ++++++ .../RequestBodyParserMiddleware.php | 53 ++++++++ .../RequestBodyParserMiddlewareTest.php | 115 ++++++++++++++++++ 3 files changed, 202 insertions(+) create mode 100644 src/Middleware/RequestBodyParserMiddleware.php create mode 100644 tests/Middleware/RequestBodyParserMiddlewareTest.php diff --git a/README.md b/README.md index d113314c..0106a97c 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ Event-driven, streaming plaintext HTTP and secure HTTPS server for [ReactPHP](ht * [Response](#response) * [Middleware](#middleware) * [RequestBodyBufferMiddleware](#requestbodybuffermiddleware) + * [RequestBodyParserMiddleware](#requestbodyparsermiddleware) * [Install](#install) * [Tests](#tests) * [License](#license) @@ -725,6 +726,39 @@ $middlewares = new MiddlewareRunner([ ]); ``` +#### RequestBodyParserMiddleware + +The `RequestBodyParserMiddleware` takes a fully buffered request body (generally from [`RequestBodyBufferMiddleware`](#requestbodybuffermiddleware)), +and parses the forms and uploaded files from the request body. + +Parsed submitted forms will be available from `$request->getParsedBody()` as array. For example the following submitted body: + +`bar[]=beer&bar[]=wine` + +Results in the following parsed body: + +```php +$parsedBody = [ + 'bar' => [ + 'beer', + 'wine', + ], +]; +``` + +Usage: + +```php +$middlewares = new MiddlewareRunner([ + new RequestBodyBufferMiddleware(16 * 1024 * 1024), // 16 MiB + new RequestBodyParserMiddleware(), + function (ServerRequestInterface $request, callable $next) { + // If any, parsed form fields are now available from $request->getParsedBody() + return new Response(200); + }, +]); +``` + ## Install The recommended way to install this library is [through Composer](http://getcomposer.org). diff --git a/src/Middleware/RequestBodyParserMiddleware.php b/src/Middleware/RequestBodyParserMiddleware.php new file mode 100644 index 00000000..0283d4ff --- /dev/null +++ b/src/Middleware/RequestBodyParserMiddleware.php @@ -0,0 +1,53 @@ +keepOriginalBody = $keepOriginalBody; + $this->addType('application/x-www-form-urlencoded', function (ServerRequestInterface $request) { + $ret = array(); + parse_str((string)$request->getBody(), $ret); + + return $request->withParsedBody($ret); + }); + } + + public function addType($type, $callback) + { + $this->types[$type] = $callback; + } + + public function __invoke(ServerRequestInterface $request, $next) + { + $type = $request->getHeaderLine('Content-Type'); + + if (!isset($this->types[$type])) { + return $next($request); + } + + try { + $value = $this->types[$type]; + /** @var ServerRequestInterface $request */ + $request = $value($request); + } catch (\Exception $e) { + return $next($request); + } + + if (!$this->keepOriginalBody) { + $request = $request->withBody(Psr7\stream_for()); + } + + return $next($request); + } +} diff --git a/tests/Middleware/RequestBodyParserMiddlewareTest.php b/tests/Middleware/RequestBodyParserMiddlewareTest.php new file mode 100644 index 00000000..30475f6b --- /dev/null +++ b/tests/Middleware/RequestBodyParserMiddlewareTest.php @@ -0,0 +1,115 @@ +addType('react/http', function (ServerRequestInterface $request) { + return $request->withParsedBody('parsed'); + }); + + $request = new ServerRequest( + 200, + 'https://example.com/', + array( + 'Content-Type' => 'react/http', + ), + 'not yet parsed' + ); + + /** @var ServerRequestInterface $parsedRequest */ + $parsedRequest = $middleware( + $request, + function (ServerRequestInterface $request) { + return $request; + } + ); + + $this->assertSame('parsed', $parsedRequest->getParsedBody()); + $this->assertSame('', (string)$parsedRequest->getBody()); + } + + public function testParseKeepBody() + { + $middleware = new RequestBodyParserMiddleware(true); + $middleware->addType('react/http', function (ServerRequestInterface $request) { + return $request->withParsedBody('parsed'); + }); + + $request = new ServerRequest( + 200, + 'https://example.com/', + array( + 'Content-Type' => 'react/http', + ), + 'not yet parsed' + ); + + /** @var ServerRequestInterface $parsedRequest */ + $parsedRequest = $middleware( + $request, + function (ServerRequestInterface $request) { + return $request; + } + ); + + $this->assertSame('parsed', $parsedRequest->getParsedBody()); + $this->assertSame('not yet parsed', (string)$parsedRequest->getBody()); + } + + public function testFormUrlencodedParsing() + { + $middleware = new RequestBodyParserMiddleware(); + $request = new ServerRequest( + 200, + 'https://example.com/', + array( + 'Content-Type' => 'application/x-www-form-urlencoded', + ), + 'foo=bar&baz[]=cheese&bar[]=beer&bar[]=wine&market[fish]=salmon&market[meat][]=beef&market[meat][]=chicken&market[]=bazaar' + ); + + /** @var ServerRequestInterface $parsedRequest */ + $parsedRequest = $middleware( + $request, + function (ServerRequestInterface $request) { + return $request; + } + ); + + $this->assertSame( + array( + 'foo' => 'bar', + 'baz' => array( + 'cheese', + ), + 'bar' => array( + 'beer', + 'wine', + ), + 'market' => array( + 'fish' => 'salmon', + 'meat' => array( + 'beef', + 'chicken', + ), + 0 => 'bazaar', + ), + ), + $parsedRequest->getParsedBody() + ); + $this->assertSame('', (string)$parsedRequest->getBody()); + } +} From 485793c3bcd4af6ac60e0b4d2995c54cc70630e1 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Thu, 14 Sep 2017 18:29:58 +0200 Subject: [PATCH 184/456] Removed $keepOriginalBody constructor argument and renamed $value to $parser --- .../RequestBodyParserMiddleware.php | 17 +++------- .../RequestBodyParserMiddlewareTest.php | 31 +------------------ 2 files changed, 6 insertions(+), 42 deletions(-) diff --git a/src/Middleware/RequestBodyParserMiddleware.php b/src/Middleware/RequestBodyParserMiddleware.php index 0283d4ff..d5065118 100644 --- a/src/Middleware/RequestBodyParserMiddleware.php +++ b/src/Middleware/RequestBodyParserMiddleware.php @@ -6,15 +6,10 @@ final class RequestBodyParserMiddleware { - private $keepOriginalBody; private $types = array(); - /** - * @param bool $keepOriginalBody Keep the original body after parsing or not - */ - public function __construct($keepOriginalBody = false) + public function __construct() { - $this->keepOriginalBody = $keepOriginalBody; $this->addType('application/x-www-form-urlencoded', function (ServerRequestInterface $request) { $ret = array(); parse_str((string)$request->getBody(), $ret); @@ -37,15 +32,13 @@ public function __invoke(ServerRequestInterface $request, $next) } try { - $value = $this->types[$type]; + $parser = $this->types[$type]; /** @var ServerRequestInterface $request */ - $request = $value($request); + $request = $parser($request); } catch (\Exception $e) { return $next($request); - } - - if (!$this->keepOriginalBody) { - $request = $request->withBody(Psr7\stream_for()); + } catch (\Throwable $t) { + return $next($request); } return $next($request); diff --git a/tests/Middleware/RequestBodyParserMiddlewareTest.php b/tests/Middleware/RequestBodyParserMiddlewareTest.php index 30475f6b..0ec0a4a4 100644 --- a/tests/Middleware/RequestBodyParserMiddlewareTest.php +++ b/tests/Middleware/RequestBodyParserMiddlewareTest.php @@ -37,38 +37,9 @@ function (ServerRequestInterface $request) { } ); - $this->assertSame('parsed', $parsedRequest->getParsedBody()); - $this->assertSame('', (string)$parsedRequest->getBody()); - } - - public function testParseKeepBody() - { - $middleware = new RequestBodyParserMiddleware(true); - $middleware->addType('react/http', function (ServerRequestInterface $request) { - return $request->withParsedBody('parsed'); - }); - - $request = new ServerRequest( - 200, - 'https://example.com/', - array( - 'Content-Type' => 'react/http', - ), - 'not yet parsed' - ); - - /** @var ServerRequestInterface $parsedRequest */ - $parsedRequest = $middleware( - $request, - function (ServerRequestInterface $request) { - return $request; - } - ); - $this->assertSame('parsed', $parsedRequest->getParsedBody()); $this->assertSame('not yet parsed', (string)$parsedRequest->getBody()); } - public function testFormUrlencodedParsing() { $middleware = new RequestBodyParserMiddleware(); @@ -110,6 +81,6 @@ function (ServerRequestInterface $request) { ), $parsedRequest->getParsedBody() ); - $this->assertSame('', (string)$parsedRequest->getBody()); + $this->assertSame('foo=bar&baz[]=cheese&bar[]=beer&bar[]=wine&market[fish]=salmon&market[meat][]=beef&market[meat][]=chicken&market[]=bazaar', (string)$parsedRequest->getBody()); } } From 3c94b057f0792ff87982823ad159b33ee86d57da Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Tue, 19 Sep 2017 18:04:17 +0200 Subject: [PATCH 185/456] Removed the ability to add custom parsers for custom body types --- .../RequestBodyParserMiddleware.php | 39 ++++---------- .../RequestBodyParserMiddlewareTest.php | 54 +++++++++---------- 2 files changed, 37 insertions(+), 56 deletions(-) diff --git a/src/Middleware/RequestBodyParserMiddleware.php b/src/Middleware/RequestBodyParserMiddleware.php index d5065118..80b520ca 100644 --- a/src/Middleware/RequestBodyParserMiddleware.php +++ b/src/Middleware/RequestBodyParserMiddleware.php @@ -6,41 +6,22 @@ final class RequestBodyParserMiddleware { - private $types = array(); - - public function __construct() - { - $this->addType('application/x-www-form-urlencoded', function (ServerRequestInterface $request) { - $ret = array(); - parse_str((string)$request->getBody(), $ret); - - return $request->withParsedBody($ret); - }); - } - - public function addType($type, $callback) - { - $this->types[$type] = $callback; - } - public function __invoke(ServerRequestInterface $request, $next) { $type = $request->getHeaderLine('Content-Type'); - if (!isset($this->types[$type])) { - return $next($request); - } - - try { - $parser = $this->types[$type]; - /** @var ServerRequestInterface $request */ - $request = $parser($request); - } catch (\Exception $e) { - return $next($request); - } catch (\Throwable $t) { - return $next($request); + if ($type === 'application/x-www-form-urlencoded') { + return $next($this->parseFormUrlencoded($request)); } return $next($request); } + + private function parseFormUrlencoded(ServerRequestInterface $request) + { + $ret = array(); + parse_str((string)$request->getBody(), $ret); + + return $request->withParsedBody($ret); + } } diff --git a/tests/Middleware/RequestBodyParserMiddlewareTest.php b/tests/Middleware/RequestBodyParserMiddlewareTest.php index 0ec0a4a4..a18d49dc 100644 --- a/tests/Middleware/RequestBodyParserMiddlewareTest.php +++ b/tests/Middleware/RequestBodyParserMiddlewareTest.php @@ -13,33 +13,6 @@ final class RequestBodyParserMiddlewareTest extends TestCase { - public function testParse() - { - $middleware = new RequestBodyParserMiddleware(); - $middleware->addType('react/http', function (ServerRequestInterface $request) { - return $request->withParsedBody('parsed'); - }); - - $request = new ServerRequest( - 200, - 'https://example.com/', - array( - 'Content-Type' => 'react/http', - ), - 'not yet parsed' - ); - - /** @var ServerRequestInterface $parsedRequest */ - $parsedRequest = $middleware( - $request, - function (ServerRequestInterface $request) { - return $request; - } - ); - - $this->assertSame('parsed', $parsedRequest->getParsedBody()); - $this->assertSame('not yet parsed', (string)$parsedRequest->getBody()); - } public function testFormUrlencodedParsing() { $middleware = new RequestBodyParserMiddleware(); @@ -83,4 +56,31 @@ function (ServerRequestInterface $request) { ); $this->assertSame('foo=bar&baz[]=cheese&bar[]=beer&bar[]=wine&market[fish]=salmon&market[meat][]=beef&market[meat][]=chicken&market[]=bazaar', (string)$parsedRequest->getBody()); } + + public function testDoesNotParseJsonByDefault() + { + $middleware = new RequestBodyParserMiddleware(); + $request = new ServerRequest( + 'POST', + 'https://example.com/', + array( + 'Content-Type' => 'application/json', + ), + '{"hello":"world"}' + ); + + /** @var ServerRequestInterface $parsedRequest */ + $parsedRequest = $middleware( + $request, + function (ServerRequestInterface $request) { + return $request; + } + ); + + $this->assertSame( + null, + $parsedRequest->getParsedBody() + ); + $this->assertSame('{"hello":"world"}', (string)$parsedRequest->getBody()); + } } From a0c8de779dc8ed09bf1f64d0ae01460d12e1acba Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Wed, 20 Sep 2017 11:19:01 +0200 Subject: [PATCH 186/456] Force Content-Type header contents to lowercase as per https://github.com/reactphp/http/pull/220#discussion_r139883073 --- .../RequestBodyParserMiddleware.php | 2 +- .../RequestBodyParserMiddlewareTest.php | 60 +++++++++++++++++-- 2 files changed, 56 insertions(+), 6 deletions(-) diff --git a/src/Middleware/RequestBodyParserMiddleware.php b/src/Middleware/RequestBodyParserMiddleware.php index 80b520ca..5623e03f 100644 --- a/src/Middleware/RequestBodyParserMiddleware.php +++ b/src/Middleware/RequestBodyParserMiddleware.php @@ -8,7 +8,7 @@ final class RequestBodyParserMiddleware { public function __invoke(ServerRequestInterface $request, $next) { - $type = $request->getHeaderLine('Content-Type'); + $type = strtolower($request->getHeaderLine('Content-Type')); if ($type === 'application/x-www-form-urlencoded') { return $next($this->parseFormUrlencoded($request)); diff --git a/tests/Middleware/RequestBodyParserMiddlewareTest.php b/tests/Middleware/RequestBodyParserMiddlewareTest.php index a18d49dc..e4d2500c 100644 --- a/tests/Middleware/RequestBodyParserMiddlewareTest.php +++ b/tests/Middleware/RequestBodyParserMiddlewareTest.php @@ -3,13 +3,9 @@ namespace React\Tests\Http\Middleware; use Psr\Http\Message\ServerRequestInterface; -use React\Http\Middleware\RequestBodyBufferMiddleware; use React\Http\Middleware\RequestBodyParserMiddleware; use React\Http\ServerRequest; use React\Tests\Http\TestCase; -use RingCentral\Psr7\BufferStream; -use React\Stream\ThroughStream; -use React\Http\HttpBodyStream; final class RequestBodyParserMiddlewareTest extends TestCase { @@ -17,7 +13,61 @@ public function testFormUrlencodedParsing() { $middleware = new RequestBodyParserMiddleware(); $request = new ServerRequest( - 200, + 'POST', + 'https://example.com/', + array( + 'Content-Type' => 'application/x-www-form-urlencoded', + ), + 'hello=world' + ); + + /** @var ServerRequestInterface $parsedRequest */ + $parsedRequest = $middleware( + $request, + function (ServerRequestInterface $request) { + return $request; + } + ); + + $this->assertSame( + array('hello' => 'world'), + $parsedRequest->getParsedBody() + ); + $this->assertSame('hello=world', (string)$parsedRequest->getBody()); + } + + public function testFormUrlencodedParsingIgnoresCaseForHeadersButRespectsContentCase() + { + $middleware = new RequestBodyParserMiddleware(); + $request = new ServerRequest( + 'POST', + 'https://example.com/', + array( + 'CONTENT-TYPE' => 'APPLICATION/X-WWW-Form-URLEncoded', + ), + 'Hello=World' + ); + + /** @var ServerRequestInterface $parsedRequest */ + $parsedRequest = $middleware( + $request, + function (ServerRequestInterface $request) { + return $request; + } + ); + + $this->assertSame( + array('Hello' => 'World'), + $parsedRequest->getParsedBody() + ); + $this->assertSame('Hello=World', (string)$parsedRequest->getBody()); + } + + public function testFormUrlencodedParsingNestedStructure() + { + $middleware = new RequestBodyParserMiddleware(); + $request = new ServerRequest( + 'POST', 'https://example.com/', array( 'Content-Type' => 'application/x-www-form-urlencoded', From 2716db8ba4595f4c98b0e889227f71f8f691faa0 Mon Sep 17 00:00:00 2001 From: Vladimir Reznichenko Date: Wed, 27 Sep 2017 10:37:54 +0200 Subject: [PATCH 187/456] SCA with Php Inspections (EA Extended) --- examples/32-upgrade-chat.php | 2 +- src/RequestHeaderParser.php | 6 ++++-- src/Server.php | 2 +- tests/FunctionalServerTest.php | 2 +- .../RequestBodyParserMiddlewareTest.php | 5 +---- tests/ServerTest.php | 16 ---------------- 6 files changed, 8 insertions(+), 25 deletions(-) diff --git a/examples/32-upgrade-chat.php b/examples/32-upgrade-chat.php index 49cb0305..58150265 100644 --- a/examples/32-upgrade-chat.php +++ b/examples/32-upgrade-chat.php @@ -53,7 +53,7 @@ // send anything that is received to the whole channel $in->on('data', function ($data) use ($username, $chat) { - $data = trim(preg_replace('/[^\w\d \.\,\-\!\?]/u', '', $data)); + $data = trim(preg_replace('/[^\w \.\,\-\!\?]/u', '', $data)); $chat->write($username . ': ' . $data . PHP_EOL); }); diff --git a/src/RequestHeaderParser.php b/src/RequestHeaderParser.php index fc6d80bb..5857eb34 100644 --- a/src/RequestHeaderParser.php +++ b/src/RequestHeaderParser.php @@ -149,12 +149,14 @@ private function parseRequest($headers) } // only support HTTP/1.1 and HTTP/1.0 requests - if ($request->getProtocolVersion() !== '1.1' && $request->getProtocolVersion() !== '1.0') { + $protocolVersion = $request->getProtocolVersion(); + if ($protocolVersion !== '1.1' && $protocolVersion !== '1.0') { throw new \InvalidArgumentException('Received request with invalid protocol version', 505); } // ensure absolute-form request-target contains a valid URI - if (strpos($request->getRequestTarget(), '://') !== false && substr($request->getRequestTarget(), 0, 1) !== '/') { + $requestTarget = $request->getRequestTarget(); + if (strpos($requestTarget, '://') !== false && substr($requestTarget, 0, 1) !== '/') { $parts = parse_url($request->getRequestTarget()); // make sure value contains valid host component (IP or hostname), but no fragment diff --git a/src/Server.php b/src/Server.php index 8f80fa71..59ff6880 100644 --- a/src/Server.php +++ b/src/Server.php @@ -168,7 +168,7 @@ public function handleConnection(ConnectionInterface $conn) $parser = new RequestHeaderParser($uriLocal, $uriRemote); $listener = array($parser, 'feed'); - $parser->on('headers', function (RequestInterface $request, $bodyBuffer) use ($conn, $listener, $parser, $that) { + $parser->on('headers', function (RequestInterface $request, $bodyBuffer) use ($conn, $listener, $that) { // parsing request completed => stop feeding parser $conn->removeListener('data', $listener); diff --git a/tests/FunctionalServerTest.php b/tests/FunctionalServerTest.php index a0b6f0b7..c778589c 100644 --- a/tests/FunctionalServerTest.php +++ b/tests/FunctionalServerTest.php @@ -443,7 +443,7 @@ public function testClosedStreamFromRequestHandlerWillSendEmptyBody() $socket = new Socket(0, $loop); $server->listen($socket); - $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) use ($loop) { + $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\n\r\n"); return Stream\buffer($conn); diff --git a/tests/Middleware/RequestBodyParserMiddlewareTest.php b/tests/Middleware/RequestBodyParserMiddlewareTest.php index e4d2500c..6990d1fb 100644 --- a/tests/Middleware/RequestBodyParserMiddlewareTest.php +++ b/tests/Middleware/RequestBodyParserMiddlewareTest.php @@ -127,10 +127,7 @@ function (ServerRequestInterface $request) { } ); - $this->assertSame( - null, - $parsedRequest->getParsedBody() - ); + $this->assertNull($parsedRequest->getParsedBody()); $this->assertSame('{"hello":"world"}', (string)$parsedRequest->getBody()); } } diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 2031ae9e..bdeb759d 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -2634,8 +2634,6 @@ function ($data) use (&$buffer) { $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); - $data = "GET / HTTP/1.0\r\n\r\n"; - $data = $this->createGetRequest(); $this->connection->emit('data', array($data)); @@ -2665,8 +2663,6 @@ function ($data) use (&$buffer) { $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); - $data = "GET / HTTP/1.0\r\n\r\n"; - $data = $this->createGetRequest(); $this->connection->emit('data', array($data)); @@ -2698,8 +2694,6 @@ function ($data) use (&$buffer) { $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); - $data = "GET / HTTP/1.0\r\n\r\n"; - $data = $this->createGetRequest(); $this->connection->emit('data', array($data)); @@ -2731,8 +2725,6 @@ function ($data) use (&$buffer) { $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); - $data = "GET / HTTP/1.0\r\n\r\n"; - $data = $this->createGetRequest(); $this->connection->emit('data', array($data)); @@ -2761,8 +2753,6 @@ function ($data) use (&$buffer) { $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); - $data = "GET / HTTP/1.0\r\n\r\n"; - $data = $this->createGetRequest(); $this->connection->emit('data', array($data)); @@ -2795,8 +2785,6 @@ function ($data) use (&$buffer) { $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); - $data = "GET / HTTP/1.0\r\n\r\n"; - $data = $this->createGetRequest(); $this->connection->emit('data', array($data)); @@ -2830,8 +2818,6 @@ function ($data) use (&$buffer) { $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); - $data = "GET / HTTP/1.0\r\n\r\n"; - $data = $this->createGetRequest(); $this->connection->emit('data', array($data)); @@ -2915,8 +2901,6 @@ function ($data) use (&$buffer) { $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); - $data = "GET / HTTP/1.0\r\n\r\n"; - $data = $this->createGetRequest(); $this->connection->emit('data', array($data)); From eec4bea42c05845471fa2595ffc824f95579916c Mon Sep 17 00:00:00 2001 From: Vladimir Reznichenko Date: Wed, 27 Sep 2017 10:46:47 +0200 Subject: [PATCH 188/456] SCA with Php Inspections (EA Extended) --- src/RequestHeaderParser.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/RequestHeaderParser.php b/src/RequestHeaderParser.php index 5857eb34..a6126da9 100644 --- a/src/RequestHeaderParser.php +++ b/src/RequestHeaderParser.php @@ -157,7 +157,7 @@ private function parseRequest($headers) // ensure absolute-form request-target contains a valid URI $requestTarget = $request->getRequestTarget(); if (strpos($requestTarget, '://') !== false && substr($requestTarget, 0, 1) !== '/') { - $parts = parse_url($request->getRequestTarget()); + $parts = parse_url($requestTarget); // 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'])) { From 327fc2092ddf8b673074a66c292d76c95944f0cd Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Wed, 27 Sep 2017 22:08:18 +0200 Subject: [PATCH 189/456] Link to the Middleware wiki page from the readme --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 0106a97c..196e75e3 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ Event-driven, streaming plaintext HTTP and secure HTTPS server for [ReactPHP](ht * [Middleware](#middleware) * [RequestBodyBufferMiddleware](#requestbodybuffermiddleware) * [RequestBodyParserMiddleware](#requestbodyparsermiddleware) + * [Third-Party Middleware](#third-party-middleware) * [Install](#install) * [Tests](#tests) * [License](#license) @@ -759,6 +760,10 @@ $middlewares = new MiddlewareRunner([ ]); ``` +#### Third-Party Middleware + +A non-exhaustive list of third-party middleware can be found at the [`Middleware`](https://github.com/reactphp/http/wiki/Middleware) wiki page. + ## Install The recommended way to install this library is [through Composer](http://getcomposer.org). From 7b7a5266de6e5df0e61b13a68869e9ec326a62dd Mon Sep 17 00:00:00 2001 From: Jan Sorgalla Date: Mon, 2 Oct 2017 09:29:48 +0200 Subject: [PATCH 190/456] README improvements * Use svg travis badge * Shorten title * Add missing . --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 0106a97c..179b8fae 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,9 @@ -# Http Component +# Http -[![Build Status](https://secure.travis-ci.org/reactphp/http.png?branch=master)](http://travis-ci.org/reactphp/http) [![Code Climate](https://codeclimate.com/github/reactphp/http/badges/gpa.svg)](https://codeclimate.com/github/reactphp/http) +[![Build Status](https://travis-ci.org/reactphp/http.svg?branch=master)](https://travis-ci.org/reactphp/http) +[![Code Climate](https://codeclimate.com/github/reactphp/http/badges/gpa.svg)](https://codeclimate.com/github/reactphp/http) -Event-driven, streaming plaintext HTTP and secure HTTPS server for [ReactPHP](https://reactphp.org/) +Event-driven, streaming plaintext HTTP and secure HTTPS server for [ReactPHP](https://reactphp.org/). **Table of Contents** From bd8b4ffef1321978772b125024fb910cd0b1d18e Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Thu, 14 Sep 2017 17:53:45 +0200 Subject: [PATCH 191/456] Added multipart support to the RequestBodyParserMiddleware --- README.md | 28 +- src/Middleware/MultipartParser.php | 253 ++++++++++++++++++ .../RequestBodyParserMiddleware.php | 5 + tests/Middleware/MultipartParserTest.php | 142 ++++++++++ .../RequestBodyParserMiddlewareTest.php | 41 +++ 5 files changed, 463 insertions(+), 6 deletions(-) create mode 100644 src/Middleware/MultipartParser.php create mode 100644 tests/Middleware/MultipartParserTest.php diff --git a/README.md b/README.md index 179b8fae..ff78d123 100644 --- a/README.md +++ b/README.md @@ -236,10 +236,6 @@ For more details about the request object, check out the documentation of and [PSR-7 RequestInterface](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-7-http-message.md#32-psrhttpmessagerequestinterface). -> Currently the uploaded files are not added by the - `Server`, but you can add these parameters by yourself using the given methods. - The next versions of this project will cover these features. - Note that by default, the request object will be processed once the request headers have been received (see also [`RequestBodyBufferMiddleware`](#requestbodybuffermiddleware) for an alternative). @@ -265,7 +261,8 @@ designed under the assumption of being in control of the request body. Given that this does not apply to this server, the following `PSR-7 StreamInterface` methods are not used and SHOULD NOT be called: `tell()`, `eof()`, `seek()`, `rewind()`, `write()` and `read()`. -If this is an issue for your use case, it's highly recommended to use the +If this is an issue for your use case and/or you want to access uploaded files, +it's highly recommended to use the [`RequestBodyBufferMiddleware`](#requestbodybuffermiddleware) instead. The `ReactPHP ReadableStreamInterface` gives you access to the incoming request body as the individual chunks arrive: @@ -732,7 +729,9 @@ $middlewares = new MiddlewareRunner([ The `RequestBodyParserMiddleware` takes a fully buffered request body (generally from [`RequestBodyBufferMiddleware`](#requestbodybuffermiddleware)), and parses the forms and uploaded files from the request body. -Parsed submitted forms will be available from `$request->getParsedBody()` as array. For example the following submitted body: +Parsed submitted forms will be available from `$request->getParsedBody()` as +array. +For example the following submitted body (`application/x-www-form-urlencoded`): `bar[]=beer&bar[]=wine` @@ -747,6 +746,23 @@ $parsedBody = [ ]; ``` +Aside from `application/x-www-form-urlencoded`, this middleware handler +also supports `multipart/form-data`, thus supporting uploaded files available +through `$request->getUploadedFiles()`. + +The `$request->getUploadedFiles(): array` will return an array with all +uploaded files formatted like this: + +```php +$uploadedFiles = [ + 'avatar' => new UploadedFile(/**...**/), + 'screenshots' => [ + new UploadedFile(/**...**/), + new UploadedFile(/**...**/), + ], +]; +``` + Usage: ```php diff --git a/src/Middleware/MultipartParser.php b/src/Middleware/MultipartParser.php new file mode 100644 index 00000000..ec6e334f --- /dev/null +++ b/src/Middleware/MultipartParser.php @@ -0,0 +1,253 @@ +parse(); + } + + private function __construct(ServerRequestInterface $request) + { + $this->request = $request; + } + + private function parse() + { + $this->buffer = (string)$this->request->getBody(); + + $this->determineStartMethod(); + + return $this->request; + } + + private function determineStartMethod() + { + if (!$this->request->hasHeader('content-type')) { + $this->findBoundary(); + return; + } + + $contentType = $this->request->getHeaderLine('content-type'); + preg_match('/boundary="?(.*)"?$/', $contentType, $matches); + if (isset($matches[1])) { + $this->boundary = $matches[1]; + $this->parseBuffer(); + return; + } + + $this->findBoundary(); + } + + private function findBoundary() + { + if (substr($this->buffer, 0, 3) === '---' && strpos($this->buffer, "\r\n") !== false) { + $boundary = substr($this->buffer, 2, strpos($this->buffer, "\r\n")); + $boundary = substr($boundary, 0, -2); + $this->boundary = $boundary; + $this->parseBuffer(); + } + } + + private function parseBuffer() + { + $chunks = explode('--' . $this->boundary, $this->buffer); + $this->buffer = array_pop($chunks); + foreach ($chunks as $chunk) { + $chunk = $this->stripTrailingEOL($chunk); + $this->parseChunk($chunk); + } + } + + private function parseChunk($chunk) + { + if ($chunk === '') { + return; + } + + list ($header, $body) = explode("\r\n\r\n", $chunk, 2); + $headers = $this->parseHeaders($header); + + if (!isset($headers['content-disposition'])) { + return; + } + + if ($this->headerStartsWith($headers['content-disposition'], 'filename')) { + $this->parseFile($headers, $body); + return; + } + + if ($this->headerStartsWith($headers['content-disposition'], 'name')) { + $this->parsePost($headers, $body); + return; + } + } + + private function parseFile($headers, $body) + { + if ( + !$this->headerContains($headers['content-disposition'], 'name=') || + !$this->headerContains($headers['content-disposition'], 'filename=') + ) { + return; + } + + $this->request = $this->request->withUploadedFiles($this->extractPost( + $this->request->getUploadedFiles(), + $this->getFieldFromHeader($headers['content-disposition'], 'name'), + new UploadedFile( + Psr7\stream_for($body), + strlen($body), + UPLOAD_ERR_OK, + $this->getFieldFromHeader($headers['content-disposition'], 'filename'), + $headers['content-type'][0] + ) + )); + } + + private function parsePost($headers, $body) + { + foreach ($headers['content-disposition'] as $part) { + if (strpos($part, 'name') === 0) { + preg_match('/name="?(.*)"$/', $part, $matches); + $this->request = $this->request->withParsedBody($this->extractPost( + $this->request->getParsedBody(), + $matches[1], + $body + )); + } + } + } + + private function parseHeaders($header) + { + $headers = array(); + + foreach (explode("\r\n", trim($header)) as $line) { + list($key, $values) = explode(':', $line, 2); + $key = trim($key); + $key = strtolower($key); + $values = explode(';', $values); + $values = array_map('trim', $values); + $headers[$key] = $values; + } + + return $headers; + } + + private function headerStartsWith(array $header, $needle) + { + foreach ($header as $part) { + if (strpos($part, $needle) === 0) { + return true; + } + } + + return false; + } + + private function headerContains(array $header, $needle) + { + foreach ($header as $part) { + if (strpos($part, $needle) !== false) { + return true; + } + } + + return false; + } + + private function getFieldFromHeader(array $header, $field) + { + foreach ($header as $part) { + if (strpos($part, $field) === 0) { + preg_match('/' . $field . '="?(.*)"$/', $part, $matches); + return $matches[1]; + } + } + + return ''; + } + + private function stripTrailingEOL($chunk) + { + if (substr($chunk, -2) === "\r\n") { + return substr($chunk, 0, -2); + } + + return $chunk; + } + + private function extractPost($postFields, $key, $value) + { + $chunks = explode('[', $key); + if (count($chunks) == 1) { + $postFields[$key] = $value; + return $postFields; + } + + $chunkKey = $chunks[0]; + if (!isset($postFields[$chunkKey])) { + $postFields[$chunkKey] = array(); + } + + $parent = &$postFields; + for ($i = 1; $i < count($chunks); $i++) { + $previousChunkKey = $chunkKey; + if (!isset($parent[$previousChunkKey])) { + $parent[$previousChunkKey] = array(); + } + $parent = &$parent[$previousChunkKey]; + $chunkKey = $chunks[$i]; + + if ($chunkKey == ']') { + $parent[] = $value; + return $postFields; + } + + $chunkKey = rtrim($chunkKey, ']'); + if ($i == count($chunks) - 1) { + $parent[$chunkKey] = $value; + return $postFields; + } + } + + return $postFields; + } +} diff --git a/src/Middleware/RequestBodyParserMiddleware.php b/src/Middleware/RequestBodyParserMiddleware.php index 5623e03f..697817f7 100644 --- a/src/Middleware/RequestBodyParserMiddleware.php +++ b/src/Middleware/RequestBodyParserMiddleware.php @@ -9,11 +9,16 @@ final class RequestBodyParserMiddleware public function __invoke(ServerRequestInterface $request, $next) { $type = strtolower($request->getHeaderLine('Content-Type')); + list ($type) = explode(';', $type); if ($type === 'application/x-www-form-urlencoded') { return $next($this->parseFormUrlencoded($request)); } + if ($type === 'multipart/form-data') { + return $next(MultipartParser::parseRequest($request)); + } + return $next($request); } diff --git a/tests/Middleware/MultipartParserTest.php b/tests/Middleware/MultipartParserTest.php new file mode 100644 index 00000000..8ecaca1d --- /dev/null +++ b/tests/Middleware/MultipartParserTest.php @@ -0,0 +1,142 @@ + 'multipart/mixed; boundary=' . $boundary, + ), $data, 1.1); + + $parsedRequest = MultipartParser::parseRequest($request); + + $this->assertEmpty($parsedRequest->getUploadedFiles()); + $this->assertSame( + array( + 'users' => array( + 'one' => 'single', + 'two' => 'second', + ), + ), + $parsedRequest->getParsedBody() + ); + } + + public function testFileUpload() + { + $boundary = "---------------------------12758086162038677464950549563"; + + $file = base64_decode("R0lGODlhAQABAIAAAP///wAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw=="); + + $data = "--$boundary\r\n"; + $data .= "Content-Disposition: form-data; name=\"users[one]\"\r\n"; + $data .= "\r\n"; + $data .= "single\r\n"; + $data .= "--$boundary\r\n"; + $data .= "Content-Disposition: form-data; name=\"users[two]\"\r\n"; + $data .= "\r\n"; + $data .= "second\r\n"; + $data .= "--$boundary\r\n"; + $data .= "Content-disposition: form-data; name=\"user\"\r\n"; + $data .= "\r\n"; + $data .= "single\r\n"; + $data .= "--$boundary\r\n"; + $data .= "content-Disposition: form-data; name=\"user2\"\r\n"; + $data .= "\r\n"; + $data .= "second\r\n"; + $data .= "--$boundary\r\n"; + $data .= "Content-Disposition: form-data; name=\"users[]\"\r\n"; + $data .= "\r\n"; + $data .= "first in array\r\n"; + $data .= "--$boundary\r\n"; + $data .= "Content-Disposition: form-data; name=\"users[]\"\r\n"; + $data .= "\r\n"; + $data .= "second in array\r\n"; + $data .= "--$boundary\r\n"; + $data .= "Content-Disposition: form-data; name=\"file\"; filename=\"Us er.php\"\r\n"; + $data .= "Content-type: text/php\r\n"; + $data .= "\r\n"; + $data .= " 'multipart/form-data', + ), $data, 1.1); + + $parsedRequest = MultipartParser::parseRequest($request); + + $this->assertSame( + array( + 'users' => array( + 'one' => 'single', + 'two' => 'second', + 0 => 'first in array', + 1 => 'second in array', + ), + 'user' => 'single', + 'user2' => 'second', + ), + $parsedRequest->getParsedBody() + ); + + $files = $parsedRequest->getUploadedFiles(); + + $this->assertTrue(isset($files['file'])); + $this->assertCount(3, $files['files']); + + $this->assertSame('Us er.php', $files['file']->getClientFilename()); + $this->assertSame('text/php', $files['file']->getClientMediaType()); + $this->assertSame("getStream()); + + $this->assertSame('blank.gif', $files['files'][0]->getClientFilename()); + $this->assertSame('image/gif', $files['files'][0]->getClientMediaType()); + $this->assertSame($file, (string)$files['files'][0]->getStream()); + + $this->assertSame('User.php', $files['files'][1]->getClientFilename()); + $this->assertSame('text/php', $files['files'][1]->getClientMediaType()); + $this->assertSame("getStream()); + + $this->assertSame('Owner.php', $files['files'][2]->getClientFilename()); + $this->assertSame('text/php', $files['files'][2]->getClientMediaType()); + $this->assertSame("getStream()); + } +} \ No newline at end of file diff --git a/tests/Middleware/RequestBodyParserMiddlewareTest.php b/tests/Middleware/RequestBodyParserMiddlewareTest.php index e4d2500c..987b12f3 100644 --- a/tests/Middleware/RequestBodyParserMiddlewareTest.php +++ b/tests/Middleware/RequestBodyParserMiddlewareTest.php @@ -133,4 +133,45 @@ function (ServerRequestInterface $request) { ); $this->assertSame('{"hello":"world"}', (string)$parsedRequest->getBody()); } + + public function testMultipartFormDataParsing() + { + $middleware = new RequestBodyParserMiddleware(); + + $boundary = "---------------------------12758086162038677464950549563"; + + $data = "--$boundary\r\n"; + $data .= "Content-Disposition: form-data; name=\"users[one]\"\r\n"; + $data .= "\r\n"; + $data .= "single\r\n"; + $data .= "--$boundary\r\n"; + $data .= "Content-Disposition: form-data; name=\"users[two]\"\r\n"; + $data .= "\r\n"; + $data .= "second\r\n"; + $data .= "--$boundary\r\n"; + + + $request = new ServerRequest('POST', 'http://example.com/', array( + 'Content-Type' => 'multipart/form-data', + ), $data, 1.1); + + /** @var ServerRequestInterface $parsedRequest */ + $parsedRequest = $middleware( + $request, + function (ServerRequestInterface $request) { + return $request; + } + ); + + $this->assertSame( + array( + 'users' => array( + 'one' => 'single', + 'two' => 'second', + ), + ), + $parsedRequest->getParsedBody() + ); + $this->assertSame($data, (string)$parsedRequest->getBody()); + } } From 11fe49d146f7bf02ae63dc2df3a45388ddab8351 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Sun, 1 Oct 2017 01:26:32 +0200 Subject: [PATCH 192/456] Middleware runner benchmark --- .travis.yml | 1 + tests/benchmark-middleware-runner.php | 32 +++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 tests/benchmark-middleware-runner.php diff --git a/.travis.yml b/.travis.yml index aa1d8cdc..16b5d1c3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -30,3 +30,4 @@ install: script: - ./vendor/bin/phpunit --coverage-text + - if [ "$DEPENDENCIES" = "lowest" ]; then php -n tests/benchmark-middleware-runner.php; fi diff --git a/tests/benchmark-middleware-runner.php b/tests/benchmark-middleware-runner.php new file mode 100644 index 00000000..050f8c2c --- /dev/null +++ b/tests/benchmark-middleware-runner.php @@ -0,0 +1,32 @@ + Date: Wed, 11 Oct 2017 11:12:42 +0200 Subject: [PATCH 193/456] Add MiddlewareRunner test ensuring multiple runs always invoke all middleware in correct order --- tests/MiddlewareRunnerTest.php | 46 ++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/tests/MiddlewareRunnerTest.php b/tests/MiddlewareRunnerTest.php index 5b01767e..3b1ac720 100644 --- a/tests/MiddlewareRunnerTest.php +++ b/tests/MiddlewareRunnerTest.php @@ -3,6 +3,7 @@ namespace React\Tests\Http; use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Factory; use React\Http\MiddlewareRunner; use React\Http\ServerRequest; @@ -88,4 +89,49 @@ public function testProcessStack(array $middlewares, $expectedCallCount) $this->assertSame($expectedCallCount, $middleware->getCallCount()); } } + + public function testMultipleRunsInvokeAllMiddlewareInCorrectOrder() + { + $requests = array( + new ServerRequest('GET', 'https://example.com/1'), + new ServerRequest('GET', 'https://example.com/2'), + new ServerRequest('GET', 'https://example.com/3') + ); + + $receivedRequests = array(); + + $middlewareRunner = new MiddlewareRunner(array( + function (ServerRequestInterface $request, $next) use (&$receivedRequests) { + $receivedRequests[] = 'middleware1: ' . $request->getUri(); + return $next($request); + }, + function (ServerRequestInterface $request, $next) use (&$receivedRequests) { + $receivedRequests[] = 'middleware2: ' . $request->getUri(); + return $next($request); + }, + function (ServerRequestInterface $request, $next) use (&$receivedRequests) { + $receivedRequests[] = 'middleware3: ' . $request->getUri(); + return $next($request); + } + )); + + foreach ($requests as $request) { + $middlewareRunner($request); + } + + $this->assertEquals( + array( + 'middleware1: https://example.com/1', + 'middleware2: https://example.com/1', + 'middleware3: https://example.com/1', + 'middleware1: https://example.com/2', + 'middleware2: https://example.com/2', + 'middleware3: https://example.com/2', + 'middleware1: https://example.com/3', + 'middleware2: https://example.com/3', + 'middleware3: https://example.com/3' + ), + $receivedRequests + ); + } } From 57e8ac23ab934c880442c6f1cf0b5a1f5e8943ea Mon Sep 17 00:00:00 2001 From: Christoph Kluge Date: Sun, 15 Oct 2017 12:10:25 +0200 Subject: [PATCH 194/456] Removed unused explode from RequestHeaderParser --- src/RequestHeaderParser.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/RequestHeaderParser.php b/src/RequestHeaderParser.php index a6126da9..6d167526 100644 --- a/src/RequestHeaderParser.php +++ b/src/RequestHeaderParser.php @@ -69,8 +69,6 @@ private function parseRequest($headers) throw new \InvalidArgumentException('Unable to parse invalid request-line'); } - $lines = explode("\r\n", $headers); - // parser does not support asterisk-form and authority-form // remember original target and temporarily replace and re-apply below $originalTarget = null; From bd3a5874ab0f0fc1945c914dd1a37d7d7ec0664b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 14 Nov 2017 13:49:23 +0100 Subject: [PATCH 195/456] Update Socket to send secure HTTPS responses with PHP < 7.1.4 --- composer.json | 5 ++-- src/Server.php | 34 +--------------------- tests/FunctionalServerTest.php | 53 +++++++++++++++++++++++++++++----- 3 files changed, 49 insertions(+), 43 deletions(-) diff --git a/composer.json b/composer.json index e32a4762..a4bf76fc 100644 --- a/composer.json +++ b/composer.json @@ -6,8 +6,8 @@ "require": { "php": ">=5.3.0", "ringcentral/psr7": "^1.2", - "react/socket": "^1.0 || ^0.8 || ^0.7 || ^0.6 || ^0.5", - "react/stream": "^1.0 || ^0.7 || ^0.6 || ^0.5 || ^0.4.6", + "react/socket": "^1.0 || ^0.8.3", + "react/stream": "^1.0 || ^0.7.1", "react/promise": "^2.3 || ^1.2.1", "evenement/evenement": "^3.0 || ^2.0 || ^1.0", "react/promise-stream": "^0.1.1" @@ -19,7 +19,6 @@ }, "require-dev": { "phpunit/phpunit": "^4.8.10||^5.0", - "react/socket": "^1.0 || ^0.8 || ^0.7", "clue/block-react": "^1.1" } } diff --git a/src/Server.php b/src/Server.php index 59ff6880..2c48e8cd 100644 --- a/src/Server.php +++ b/src/Server.php @@ -148,21 +148,12 @@ public function listen(ServerInterface $socket) public function handleConnection(ConnectionInterface $conn) { $uriLocal = $conn->getLocalAddress(); - if ($uriLocal !== null && strpos($uriLocal, '://') === false) { - // local URI known but does not contain a scheme. Should only happen for old Socket < 0.8 - // try to detect transport encryption and assume default application scheme - $uriLocal = ($this->isConnectionEncrypted($conn) ? 'https://' : 'http://') . $uriLocal; - } elseif ($uriLocal !== null) { + if ($uriLocal !== null) { // local URI known, so translate transport scheme to application scheme $uriLocal = strtr($uriLocal, array('tcp://' => 'http://', 'tls://' => 'https://')); } $uriRemote = $conn->getRemoteAddress(); - if ($uriRemote !== null && strpos($uriRemote, '://') === false) { - // local URI known but does not contain a scheme. Should only happen for old Socket < 0.8 - // actual scheme is not evaluated but required for parsing URI - $uriRemote = 'unused://' . $uriRemote; - } $that = $this; $parser = new RequestHeaderParser($uriLocal, $uriRemote); @@ -422,27 +413,4 @@ private function handleResponseBody(ResponseInterface $response, ConnectionInter $connection->end(); } } - - /** - * @param ConnectionInterface $conn - * @return bool - * @codeCoverageIgnore - */ - private function isConnectionEncrypted(ConnectionInterface $conn) - { - // Legacy PHP < 7 does not offer any direct access to check crypto parameters - // We work around by accessing the context options and assume that only - // secure connections *SHOULD* set the "ssl" context options by default. - if (PHP_VERSION_ID < 70000) { - $context = isset($conn->stream) ? stream_context_get_options($conn->stream) : array(); - - return (isset($context['ssl']) && $context['ssl']); - } - - // Modern PHP 7+ offers more reliable access to check crypto parameters - // by checking stream crypto meta data that is only then made available. - $meta = isset($conn->stream) ? stream_get_meta_data($conn->stream) : array(); - - return (isset($meta['crypto']) && $meta['crypto']); - } } diff --git a/tests/FunctionalServerTest.php b/tests/FunctionalServerTest.php index c778589c..b4c3fa2e 100644 --- a/tests/FunctionalServerTest.php +++ b/tests/FunctionalServerTest.php @@ -12,9 +12,7 @@ use Clue\React\Block; use React\Http\Response; use React\Socket\SecureServer; -use React\Stream\ReadableStreamInterface; use React\Promise\Promise; -use React\Promise\PromiseInterface; use React\Promise\Stream; use React\Stream\ThroughStream; @@ -172,7 +170,7 @@ public function testSecureHttpsOnRandomPort() )); $server->listen($socket); - $result = $connector->connect('tls://' . noScheme($socket->getAddress()))->then(function (ConnectionInterface $conn) { + $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\nHost: " . noScheme($conn->getRemoteAddress()) . "\r\n\r\n"); return Stream\buffer($conn); @@ -186,6 +184,47 @@ public function testSecureHttpsOnRandomPort() $socket->close(); } + public function testSecureHttpsReturnsData() + { + if (!function_exists('stream_socket_enable_crypto')) { + $this->markTestSkipped('Not supported on your platform (outdated HHVM?)'); + } + + $loop = Factory::create(); + + $server = new Server(function (RequestInterface $request) { + return new Response( + 200, + array(), + str_repeat('.', 33000) + ); + }); + + $socket = new Socket(0, $loop); + $socket = new SecureServer($socket, $loop, array( + 'local_cert' => __DIR__ . '/../examples/localhost.pem' + )); + $server->listen($socket); + + $connector = new Connector($loop, array( + 'tls' => array('verify_peer' => false) + )); + + $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { + $conn->write("GET / HTTP/1.0\r\nHost: " . noScheme($conn->getRemoteAddress()) . "\r\n\r\n"); + + return Stream\buffer($conn); + }); + + $response = Block\await($result, $loop, 1.0); + + $this->assertContains("HTTP/1.0 200 OK", $response); + $this->assertContains("\r\nContent-Length: 33000\r\n", $response); + $this->assertStringEndsWith("\r\n". str_repeat('.', 33000), $response); + + $socket->close(); + } + public function testSecureHttpsOnRandomPortWithoutHostHeaderUsesSocketUri() { if (!function_exists('stream_socket_enable_crypto')) { @@ -207,7 +246,7 @@ public function testSecureHttpsOnRandomPortWithoutHostHeaderUsesSocketUri() )); $server->listen($socket); - $result = $connector->connect('tls://' . noScheme($socket->getAddress()))->then(function (ConnectionInterface $conn) { + $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\n\r\n"); return Stream\buffer($conn); @@ -306,7 +345,7 @@ public function testSecureHttpsOnStandardPortReturnsUriWithNoPort() $server->listen($socket); - $result = $connector->connect('tls://' . noScheme($socket->getAddress()))->then(function (ConnectionInterface $conn) { + $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n"); return Stream\buffer($conn); @@ -345,7 +384,7 @@ public function testSecureHttpsOnStandardPortWithoutHostHeaderUsesSocketUri() $server->listen($socket); - $result = $connector->connect('tls://' . noScheme($socket->getAddress()))->then(function (ConnectionInterface $conn) { + $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\n\r\n"); return Stream\buffer($conn); @@ -414,7 +453,7 @@ public function testSecureHttpsOnHttpStandardPortReturnsUriWithPort() $server->listen($socket); - $result = $connector->connect('tls://' . noScheme($socket->getAddress()))->then(function (ConnectionInterface $conn) { + $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\nHost: " . noScheme($conn->getRemoteAddress()) . "\r\n\r\n"); return Stream\buffer($conn); From a729b3632b5abf8866b942dd0b394ea6b03752a5 Mon Sep 17 00:00:00 2001 From: Gabriel Caruso Date: Sun, 12 Nov 2017 00:14:42 -0200 Subject: [PATCH 196/456] Use PHPUnit\Framework\TestCase instead of PHPUnit_Framework_TestCase --- composer.json | 2 +- tests/TestCase.php | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index a4bf76fc..a4faf833 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,7 @@ } }, "require-dev": { - "phpunit/phpunit": "^4.8.10||^5.0", + "phpunit/phpunit": "^4.8.35 || ^5.7", "clue/block-react": "^1.1" } } diff --git a/tests/TestCase.php b/tests/TestCase.php index 74ad0bc7..2b6d265d 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -2,7 +2,9 @@ namespace React\Tests\Http; -class TestCase extends \PHPUnit_Framework_TestCase +use PHPUnit\Framework\TestCase as BaseTestCase; + +class TestCase extends BaseTestCase { protected function expectCallableExactly($amount) { From 0b3e251e66f2e012fe5d353e36b5e61477eebddd Mon Sep 17 00:00:00 2001 From: Gabriel Caruso Date: Tue, 14 Nov 2017 07:48:12 -0200 Subject: [PATCH 197/456] Support PHPUnit 6 --- composer.json | 4 ++-- tests/MiddlewareRunnerTest.php | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index a4faf833..6e577877 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,7 @@ } }, "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.7", - "clue/block-react": "^1.1" + "clue/block-react": "^1.1", + "phpunit/phpunit": "^6.4 || ^5.7 || ^4.8.35" } } diff --git a/tests/MiddlewareRunnerTest.php b/tests/MiddlewareRunnerTest.php index 3b1ac720..699244cc 100644 --- a/tests/MiddlewareRunnerTest.php +++ b/tests/MiddlewareRunnerTest.php @@ -13,9 +13,12 @@ final class MiddlewareRunnerTest extends TestCase { + /** + * @expectedException RuntimeException + * @expectedExceptionMessage No middleware to run + */ public function testDefaultResponse() { - $this->setExpectedException('\RuntimeException'); $request = new ServerRequest('GET', 'https://example.com/'); $middlewares = array(); $middlewareStack = new MiddlewareRunner($middlewares); From 345b28e8520156e8cb0e9714218a4339abdac365 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Tue, 14 Nov 2017 17:33:20 +0100 Subject: [PATCH 198/456] Test middleware runner to ensure next handler can be called more than once --- tests/MiddlewareRunnerTest.php | 36 ++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/tests/MiddlewareRunnerTest.php b/tests/MiddlewareRunnerTest.php index 699244cc..4067c4e7 100644 --- a/tests/MiddlewareRunnerTest.php +++ b/tests/MiddlewareRunnerTest.php @@ -93,6 +93,42 @@ public function testProcessStack(array $middlewares, $expectedCallCount) } } + public function testNextCanBeRunMoreThanOnceWithoutCorruptingTheMiddlewareStack() + { + $exception = new \RuntimeException('exception'); + $retryCalled = 0; + $error = null; + $retry = function ($request, $next) use (&$error, &$retryCalled) { + return $next($request)->then(null, function ($et) use (&$error, $request, $next, &$retryCalled) { + $retryCalled++; + $error = $et; + // the $next failed. discard $error and retry once again: + return $next($request); + }); + }; + + $response = new Response(); + $called = 0; + $runner = new MiddlewareRunner(array( + $retry, + function () use (&$called, $response, $exception) { + $called++; + if ($called === 1) { + throw $exception; + } + + return $response; + } + )); + + $request = new ServerRequest('GET', 'https://example.com/'); + + $this->assertSame($response, Block\await($runner($request), Factory::create())); + $this->assertSame(1, $retryCalled); + $this->assertSame(2, $called); + $this->assertSame($exception, $error); + } + public function testMultipleRunsInvokeAllMiddlewareInCorrectOrder() { $requests = array( From 75f1a5a569c9a66d05648670de0ca92601d3c2cb Mon Sep 17 00:00:00 2001 From: Jan Sorgalla Date: Thu, 16 Nov 2017 20:46:37 +0100 Subject: [PATCH 199/456] Extend test to ensure next handler can be run more than once when handler rejects --- tests/MiddlewareRunnerTest.php | 38 ++++++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/tests/MiddlewareRunnerTest.php b/tests/MiddlewareRunnerTest.php index 4067c4e7..cba6af98 100644 --- a/tests/MiddlewareRunnerTest.php +++ b/tests/MiddlewareRunnerTest.php @@ -7,6 +7,7 @@ use React\EventLoop\Factory; use React\Http\MiddlewareRunner; use React\Http\ServerRequest; +use React\Promise; use React\Tests\Http\Middleware\ProcessStack; use RingCentral\Psr7\Response; use Clue\React\Block; @@ -93,7 +94,26 @@ public function testProcessStack(array $middlewares, $expectedCallCount) } } - public function testNextCanBeRunMoreThanOnceWithoutCorruptingTheMiddlewareStack() + public function provideErrorHandler() + { + return array( + array( + function (\Exception $e) { + throw $e; + } + ), + array( + function (\Exception $e) { + return Promise\reject($e); + } + ) + ); + } + + /** + * @dataProvider provideErrorHandler + */ + public function testNextCanBeRunMoreThanOnceWithoutCorruptingTheMiddlewareStack($errorHandler) { $exception = new \RuntimeException('exception'); $retryCalled = 0; @@ -110,15 +130,15 @@ public function testNextCanBeRunMoreThanOnceWithoutCorruptingTheMiddlewareStack( $response = new Response(); $called = 0; $runner = new MiddlewareRunner(array( - $retry, - function () use (&$called, $response, $exception) { - $called++; - if ($called === 1) { - throw $exception; + $retry, + function () use ($errorHandler, &$called, $response, $exception) { + $called++; + if ($called === 1) { + return $errorHandler($exception); + } + + return $response; } - - return $response; - } )); $request = new ServerRequest('GET', 'https://example.com/'); From 94a07f80b7fc439127f22e677b8ae7ee751d5c29 Mon Sep 17 00:00:00 2001 From: Jan Sorgalla Date: Fri, 17 Nov 2017 10:03:16 +0100 Subject: [PATCH 200/456] Add test for uncommon middleware array formats --- tests/MiddlewareRunnerTest.php | 140 +++++++++++++++++++++++++++++++++ 1 file changed, 140 insertions(+) diff --git a/tests/MiddlewareRunnerTest.php b/tests/MiddlewareRunnerTest.php index cba6af98..79278510 100644 --- a/tests/MiddlewareRunnerTest.php +++ b/tests/MiddlewareRunnerTest.php @@ -193,4 +193,144 @@ function (ServerRequestInterface $request, $next) use (&$receivedRequests) { $receivedRequests ); } + + public function provideUncommonMiddlewareArrayFormats() + { + return array( + array( + function () { + $sequence = ''; + + // Numeric index gap + return array( + 0 => function (ServerRequestInterface $request, $next) use (&$sequence) { + $sequence .= 'A'; + + return $next($request); + }, + 2 => function (ServerRequestInterface $request, $next) use (&$sequence) { + $sequence .= 'B'; + + return $next($request); + }, + 3 => function () use (&$sequence) { + return new Response(200, array(), $sequence . 'C'); + }, + ); + }, + 'ABC', + ), + array( + function () { + $sequence = ''; + + // Reversed numeric indexes + return array( + 2 => function (ServerRequestInterface $request, $next) use (&$sequence) { + $sequence .= 'A'; + + return $next($request); + }, + 1 => function (ServerRequestInterface $request, $next) use (&$sequence) { + $sequence .= 'B'; + + return $next($request); + }, + 0 => function () use (&$sequence) { + return new Response(200, array(), $sequence . 'C'); + }, + ); + }, + 'ABC', + ), + array( + function () { + $sequence = ''; + + // Associative array + return array( + 'middleware1' => function (ServerRequestInterface $request, $next) use (&$sequence) { + $sequence .= 'A'; + + return $next($request); + }, + 'middleware2' => function (ServerRequestInterface $request, $next) use (&$sequence) { + $sequence .= 'B'; + + return $next($request); + }, + 'middleware3' => function () use (&$sequence) { + return new Response(200, array(), $sequence . 'C'); + }, + ); + }, + 'ABC', + ), + array( + function () { + $sequence = ''; + + // Associative array with empty or trimmable string keys + return array( + '' => function (ServerRequestInterface $request, $next) use (&$sequence) { + $sequence .= 'A'; + + return $next($request); + }, + ' ' => function (ServerRequestInterface $request, $next) use (&$sequence) { + $sequence .= 'B'; + + return $next($request); + }, + ' ' => function () use (&$sequence) { + return new Response(200, array(), $sequence . 'C'); + }, + ); + }, + 'ABC', + ), + array( + function () { + $sequence = ''; + + // Mixed array keys + return array( + '' => function (ServerRequestInterface $request, $next) use (&$sequence) { + $sequence .= 'A'; + + return $next($request); + }, + 0 => function (ServerRequestInterface $request, $next) use (&$sequence) { + $sequence .= 'B'; + + return $next($request); + }, + 'foo' => function (ServerRequestInterface $request, $next) use (&$sequence) { + $sequence .= 'C'; + + return $next($request); + }, + 2 => function () use (&$sequence) { + return new Response(200, array(), $sequence . 'D'); + }, + ); + }, + 'ABCD', + ), + ); + } + + /** + * @dataProvider provideUncommonMiddlewareArrayFormats + */ + public function testUncommonMiddlewareArrayFormats($middlewareFactory, $expectedSequence) + { + $request = new ServerRequest('GET', 'https://example.com/'); + $middlewareStack = new MiddlewareRunner($middlewareFactory()); + + /** @var ResponseInterface $response */ + $response = Block\await($middlewareStack($request), Factory::create()); + + $this->assertSame($expectedSequence, (string) $response->getBody()); + } } From 703d4de190fc92cd911031f2d16c3dc699f893f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 14 Nov 2017 23:03:37 +0100 Subject: [PATCH 201/456] Support sending same response header multiple times (e.g. Set-Cookie) --- src/Server.php | 24 ++++++++++++++++-------- tests/ServerTest.php | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 8 deletions(-) diff --git a/src/Server.php b/src/Server.php index 2c48e8cd..d1082f43 100644 --- a/src/Server.php +++ b/src/Server.php @@ -3,15 +3,16 @@ namespace React\Http; use Evenement\EventEmitter; -use React\Socket\ServerInterface; -use React\Socket\ConnectionInterface; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; -use React\Promise\Promise; -use RingCentral\Psr7 as Psr7Implementation; use Psr\Http\Message\ServerRequestInterface; use React\Promise\CancellablePromiseInterface; +use React\Promise\Promise; +use React\Socket\ConnectionInterface; +use React\Socket\ServerInterface; +use React\Stream\ReadableStreamInterface; use React\Stream\WritableStreamInterface; +use RingCentral\Psr7 as Psr7Implementation; /** * The `Server` class is responsible for handling incoming connections and then @@ -378,19 +379,26 @@ public function handleResponse(ConnectionInterface $connection, ServerRequestInt private function handleResponseBody(ResponseInterface $response, ConnectionInterface $connection) { - if (!$response->getBody() instanceof HttpBodyStream) { - $connection->write(Psr7Implementation\str($response)); - return $connection->end(); + $headers = "HTTP/" . $response->getProtocolVersion() . " " . $response->getStatusCode() . " " . $response->getReasonPhrase() . "\r\n"; + foreach ($response->getHeaders() as $name => $values) { + foreach ($values as $value) { + $headers .= $name . ": " . $value . "\r\n"; + } } $stream = $response->getBody(); + if (!$stream instanceof ReadableStreamInterface) { + $connection->write($headers . "\r\n" . $stream); + return $connection->end(); + } + // close response stream if connection is already closed if (!$connection->isWritable()) { return $stream->close(); } - $connection->write(Psr7Implementation\str($response)); + $connection->write($headers . "\r\n"); if ($stream->isReadable()) { if ($response->getHeaderLine('Transfer-Encoding') === 'chunked') { diff --git a/tests/ServerTest.php b/tests/ServerTest.php index bdeb759d..db7f0785 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -2331,6 +2331,44 @@ function ($data) use (&$buffer) { $this->assertContains("\r\n\r\n", $buffer); } + public function testAddMultipleCookieHeaders() + { + $server = new Server(function (ServerRequestInterface $request) { + return new Response( + 200, + array( + 'Set-Cookie' => array( + 'name=test', + 'session=abc' + ), + 'Date' => '', + 'X-Powered-By' => '' + ) + ); + }); + + $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 = $this->createGetRequest(); + + $this->connection->emit('data', array($data)); + + $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 testOnlyChunkedEncodingIsAllowedForTransferEncoding() { $error = null; From 70b7a4838b720408a83460fca6fde3fecf4d7f52 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Sat, 30 Sep 2017 19:41:55 +0200 Subject: [PATCH 202/456] Rewrote MiddlewareRunner to iterate over the array of middleware instead of creating a new instance of itself for each middleware ran --- src/MiddlewareRunner.php | 51 +++++++++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 19 deletions(-) diff --git a/src/MiddlewareRunner.php b/src/MiddlewareRunner.php index 2320ced2..849b644c 100644 --- a/src/MiddlewareRunner.php +++ b/src/MiddlewareRunner.php @@ -11,15 +11,16 @@ final class MiddlewareRunner { /** * @var callable[] + * @internal */ - private $middleware = array(); + public $middleware = array(); /** * @param callable[] $middleware */ public function __construct(array $middleware) { - $this->middleware = $middleware; + $this->middleware = array_values($middleware); } /** @@ -32,22 +33,34 @@ public function __invoke(ServerRequestInterface $request) return Promise\reject(new \RuntimeException('No middleware to run')); } - $middlewareCollection = $this->middleware; - $middleware = array_shift($middlewareCollection); - - $cancel = null; - return new Promise\Promise(function ($resolve, $reject) use ($middleware, $request, $middlewareCollection, &$cancel) { - $cancel = $middleware( - $request, - new MiddlewareRunner( - $middlewareCollection - ) - ); - $resolve($cancel); - }, function () use (&$cancel) { - if ($cancel instanceof Promise\CancellablePromiseInterface) { - $cancel->cancel(); - } - }); + $position = 0; + + $that = $this; + $func = function (ServerRequestInterface $request) use (&$func, &$position, &$that) { + $middleware = $that->middleware[$position]; + $response = null; + $promise = new Promise\Promise(function ($resolve) use ($middleware, $request, $func, &$response, &$position) { + $position++; + + $response = $middleware( + $request, + $func + ); + + $resolve($response); + }, function () use (&$response) { + if ($response instanceof Promise\CancellablePromiseInterface) { + $response->cancel(); + } + }); + + return $promise->then(null, function ($error) use (&$position) { + $position--; + + return Promise\reject($error); + }); + }; + + return $func($request); } } From 468aaa9ed4d790e508d300df9ef7b20d49137a36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 17 Nov 2017 18:54:36 +0100 Subject: [PATCH 203/456] Removed risky and duplicate tests --- tests/HttpBodyStreamTest.php | 13 +- tests/ServerTest.php | 454 ++++++++++++++++------------------- 2 files changed, 202 insertions(+), 265 deletions(-) diff --git a/tests/HttpBodyStreamTest.php b/tests/HttpBodyStreamTest.php index 31e168e0..4b54183b 100644 --- a/tests/HttpBodyStreamTest.php +++ b/tests/HttpBodyStreamTest.php @@ -34,10 +34,9 @@ public function testPauseStream() public function testResumeStream() { $input = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); - $input->expects($this->once())->method('pause'); + $input->expects($this->once())->method('resume'); $bodyStream = new HttpBodyStream($input, null); - $bodyStream->pause(); $bodyStream->resume(); } @@ -154,16 +153,6 @@ public function testIsReadable() $this->assertTrue($this->bodyStream->isReadable()); } - public function testPause() - { - $this->bodyStream->pause(); - } - - public function testResume() - { - $this->bodyStream->resume(); - } - /** * @expectedException BadMethodCallException */ diff --git a/tests/ServerTest.php b/tests/ServerTest.php index db7f0785..3a1b9362 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -54,9 +54,7 @@ public function testRequestEventWillNotBeEmittedForIncompleteHeaders() public function testRequestEventIsEmitted() { - $server = new Server(function (ServerRequestInterface $request) { - return \React\Promise\resolve(new Response()); - }); + $server = new Server($this->expectCallableOnce()); $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); @@ -72,8 +70,6 @@ public function testRequestEvent() $server = new Server(function (ServerRequestInterface $request) use (&$i, &$requestAssertion) { $i++; $requestAssertion = $request; - - return \React\Promise\resolve(new Response()); }); $this->connection @@ -107,8 +103,6 @@ public function testRequestEventWithMiddlewareRunner() $server = new Server(new MiddlewareRunner(array(function (ServerRequestInterface $request) use (&$i, &$requestAssertion) { $i++; $requestAssertion = $request; - - return \React\Promise\resolve(new Response()); }))); $this->connection @@ -140,7 +134,6 @@ public function testRequestGetWithHostAndCustomPort() $requestAssertion = null; $server = new Server(function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; - return new Response(); }); $server->listen($this->socket); @@ -163,7 +156,6 @@ public function testRequestGetWithHostAndHttpsPort() $requestAssertion = null; $server = new Server(function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; - return new Response(); }); $server->listen($this->socket); @@ -186,7 +178,6 @@ public function testRequestGetWithHostAndDefaultPortWillBeIgnored() $requestAssertion = null; $server = new Server(function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; - return new Response(); }); $server->listen($this->socket); @@ -209,7 +200,6 @@ public function testRequestOptionsAsterisk() $requestAssertion = null; $server = new Server(function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; - return new Response(); }); $server->listen($this->socket); @@ -243,7 +233,6 @@ public function testRequestConnectAuthorityForm() $requestAssertion = null; $server = new Server(function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; - return new Response(); }); $server->listen($this->socket); @@ -266,7 +255,6 @@ public function testRequestConnectWithoutHostWillBeAdded() $requestAssertion = null; $server = new Server(function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; - return new Response(); }); $server->listen($this->socket); @@ -289,7 +277,6 @@ public function testRequestConnectAuthorityFormWithDefaultPortWillBeIgnored() $requestAssertion = null; $server = new Server(function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; - return new Response(); }); $server->listen($this->socket); @@ -312,7 +299,6 @@ public function testRequestConnectAuthorityFormNonMatchingHostWillBeOverwritten( $requestAssertion = null; $server = new Server(function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; - return new Response(); }); $server->listen($this->socket); @@ -360,7 +346,6 @@ public function testRequestWithoutHostEventUsesSocketAddress() $server = new Server(function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; - return new Response(); }); $this->connection @@ -387,7 +372,6 @@ public function testRequestAbsoluteEvent() $server = new Server(function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; - return new Response(); }); $server->listen($this->socket); @@ -410,9 +394,7 @@ public function testRequestAbsoluteAddsMissingHostEvent() $server = new Server(function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; - return new Response(); }); - $server->on('error', 'printf'); $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); @@ -434,7 +416,6 @@ public function testRequestAbsoluteNonMatchingHostWillBeOverwritten() $server = new Server(function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; - return new Response(); }); $server->listen($this->socket); @@ -457,7 +438,6 @@ public function testRequestOptionsAsteriskEvent() $server = new Server(function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; - return new Response(); }); $server->listen($this->socket); @@ -480,7 +460,6 @@ public function testRequestOptionsAbsoluteEvent() $server = new Server(function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; - return new Response(); }); $server->listen($this->socket); @@ -501,7 +480,6 @@ public function testRequestPauseWillbeForwardedToConnection() { $server = new Server(function (ServerRequestInterface $request) { $request->getBody()->pause(); - return new Response(); }); $this->connection->expects($this->once())->method('pause'); @@ -522,7 +500,6 @@ public function testRequestResumeWillbeForwardedToConnection() { $server = new Server(function (ServerRequestInterface $request) { $request->getBody()->resume(); - return new Response(); }); $this->connection->expects($this->once())->method('resume'); @@ -538,7 +515,6 @@ public function testRequestCloseWillPauseConnection() { $server = new Server(function (ServerRequestInterface $request) { $request->getBody()->close(); - return new Response(); }); $this->connection->expects($this->once())->method('pause'); @@ -554,9 +530,7 @@ public function testRequestPauseAfterCloseWillNotBeForwarded() { $server = new Server(function (ServerRequestInterface $request) { $request->getBody()->close(); - $request->getBody()->pause();# - - return new Response(); + $request->getBody()->pause(); }); $this->connection->expects($this->once())->method('pause'); @@ -573,8 +547,6 @@ public function testRequestResumeAfterCloseWillNotBeForwarded() $server = new Server(function (ServerRequestInterface $request) { $request->getBody()->close(); $request->getBody()->resume(); - - return new Response(); }); $this->connection->expects($this->once())->method('pause'); @@ -593,8 +565,6 @@ public function testRequestEventWithoutBodyWillNotEmitData() $server = new Server(function (ServerRequestInterface $request) use ($never) { $request->getBody()->on('data', $never); - - return new Response(); }); $server->listen($this->socket); @@ -610,8 +580,6 @@ public function testRequestEventWithSecondDataEventWillEmitBodyData() $server = new Server(function (ServerRequestInterface $request) use ($once) { $request->getBody()->on('data', $once); - - return new Response(); }); $server->listen($this->socket); @@ -632,8 +600,6 @@ public function testRequestEventWithPartialBodyWillEmitData() $server = new Server(function (ServerRequestInterface $request) use ($once) { $request->getBody()->on('data', $once); - - return new Response(); }); $server->listen($this->socket); @@ -679,7 +645,7 @@ function ($data) use (&$buffer) { $this->assertContains("\r\nX-Powered-By: React/alpha\r\n", $buffer); } - public function testPendingPromiseWillNotSendAnything() + public function testResponsePendingPromiseWillNotSendAnything() { $never = $this->expectCallableNever(); @@ -709,7 +675,7 @@ function ($data) use (&$buffer) { $this->assertEquals('', $buffer); } - public function testPendingPromiseWillBeCancelledIfConnectionCloses() + public function testResponsePendingPromiseWillBeCancelledIfConnectionCloses() { $once = $this->expectCallableOnce(); @@ -740,13 +706,17 @@ function ($data) use (&$buffer) { $this->assertEquals('', $buffer); } - public function testStreamAlreadyClosedWillSendEmptyBodyChunkedEncoded() + public function testRespomseBodyStreamAlreadyClosedWillSendEmptyBodyChunkedEncoded() { $stream = new ThroughStream(); $stream->close(); $server = new Server(function (ServerRequestInterface $request) use ($stream) { - return new Response(200, array(), $stream); + return new Response( + 200, + array(), + $stream + ); }); $buffer = ''; @@ -772,12 +742,16 @@ function ($data) use (&$buffer) { $this->assertStringEndsWith("\r\n\r\n0\r\n\r\n", $buffer); } - public function testResponseStreamEndingWillSendEmptyBodyChunkedEncoded() + public function testResponseBodyStreamEndingWillSendEmptyBodyChunkedEncoded() { $stream = new ThroughStream(); $server = new Server(function (ServerRequestInterface $request) use ($stream) { - return new Response(200, array(), $stream); + return new Response( + 200, + array(), + $stream + ); }); $buffer = ''; @@ -805,13 +779,17 @@ function ($data) use (&$buffer) { $this->assertStringEndsWith("\r\n\r\n0\r\n\r\n", $buffer); } - public function testResponseStreamAlreadyClosedWillSendEmptyBodyPlainHttp10() + public function testResponseBodyStreamAlreadyClosedWillSendEmptyBodyPlainHttp10() { $stream = new ThroughStream(); $stream->close(); $server = new Server(function (ServerRequestInterface $request) use ($stream) { - return new Response(200, array(), $stream); + return new Response( + 200, + array(), + $stream + ); }); $buffer = ''; @@ -843,7 +821,11 @@ public function testResponseStreamWillBeClosedIfConnectionIsAlreadyClosed() $stream->on('close', $this->expectCallableOnce()); $server = new Server(function (ServerRequestInterface $request) use ($stream) { - return new Response(200, array(), $stream); + return new Response( + 200, + array(), + $stream + ); }); $buffer = ''; @@ -888,13 +870,17 @@ function ($data) use (&$buffer) { $this->connection->emit('data', array($data)); } - public function testResponseStreamWillBeClosedIfConnectionEmitsCloseEvent() + public function testResponseBodyStreamWillBeClosedIfConnectionEmitsCloseEvent() { $stream = new ThroughStream(); $stream->on('close', $this->expectCallableOnce()); $server = new Server(function (ServerRequestInterface $request) use ($stream) { - return new Response(200, array(), $stream); + return new Response( + 200, + array(), + $stream + ); }); $server->listen($this->socket); @@ -905,10 +891,18 @@ public function testResponseStreamWillBeClosedIfConnectionEmitsCloseEvent() $this->connection->emit('close'); } - public function testUpgradeInResponseCanBeUsedToAdvertisePossibleUpgrade() + public function testResponseUpgradeInResponseCanBeUsedToAdvertisePossibleUpgrade() { $server = new Server(function (ServerRequestInterface $request) { - return new Response(200, array('date' => '', 'x-powered-by' => '', 'Upgrade' => 'demo'), 'foo'); + return new Response( + 200, + array( + 'date' => '', + 'x-powered-by' => '', + 'Upgrade' => 'demo' + ), + 'foo' + ); }); $buffer = ''; @@ -933,10 +927,17 @@ function ($data) use (&$buffer) { $this->assertEquals("HTTP/1.1 200 OK\r\nUpgrade: demo\r\nContent-Length: 3\r\nConnection: close\r\n\r\nfoo", $buffer); } - public function testUpgradeWishInRequestCanBeIgnoredByReturningNormalResponse() + public function testResponseUpgradeWishInRequestCanBeIgnoredByReturningNormalResponse() { $server = new Server(function (ServerRequestInterface $request) { - return new Response(200, array('date' => '', 'x-powered-by' => ''), 'foo'); + return new Response( + 200, + array( + 'date' => '', + 'x-powered-by' => '' + ), + 'foo' + ); }); $buffer = ''; @@ -961,10 +962,18 @@ function ($data) use (&$buffer) { $this->assertEquals("HTTP/1.1 200 OK\r\nContent-Length: 3\r\nConnection: close\r\n\r\nfoo", $buffer); } - public function testUpgradeSwitchingProtocolIncludesConnectionUpgradeHeaderWithoutContentLength() + public function testResponseUpgradeSwitchingProtocolIncludesConnectionUpgradeHeaderWithoutContentLength() { $server = new Server(function (ServerRequestInterface $request) { - return new Response(101, array('date' => '', 'x-powered-by' => '', 'Upgrade' => 'demo'), 'foo'); + return new Response( + 101, + array( + 'date' => '', + 'x-powered-by' => '', + 'Upgrade' => 'demo' + ), + 'foo' + ); }); $server->on('error', 'printf'); @@ -991,12 +1000,20 @@ function ($data) use (&$buffer) { $this->assertEquals("HTTP/1.1 101 Switching Protocols\r\nUpgrade: demo\r\nConnection: upgrade\r\n\r\nfoo", $buffer); } - public function testUpgradeSwitchingProtocolWithStreamWillPipeDataToConnection() + public function testResponseUpgradeSwitchingProtocolWithStreamWillPipeDataToConnection() { $stream = new ThroughStream(); $server = new Server(function (ServerRequestInterface $request) use ($stream) { - return new Response(101, array('date' => '', 'x-powered-by' => '', 'Upgrade' => 'demo'), $stream); + return new Response( + 101, + array( + 'date' => '', + 'x-powered-by' => '', + 'Upgrade' => 'demo' + ), + $stream + ); }); $buffer = ''; @@ -1024,12 +1041,16 @@ function ($data) use (&$buffer) { $this->assertEquals("HTTP/1.1 101 Switching Protocols\r\nUpgrade: demo\r\nConnection: upgrade\r\n\r\nhelloworld", $buffer); } - public function testConnectResponseStreamWillPipeDataToConnection() + public function testResponseConnectMethodStreamWillPipeDataToConnection() { $stream = new ThroughStream(); $server = new Server(function (ServerRequestInterface $request) use ($stream) { - return new Response(200, array(), $stream); + return new Response( + 200, + array(), + $stream + ); }); $buffer = ''; @@ -1058,12 +1079,16 @@ function ($data) use (&$buffer) { } - public function testConnectResponseStreamWillPipeDataFromConnection() + public function testResponseConnectMethodStreamWillPipeDataFromConnection() { $stream = new ThroughStream(); $server = new Server(function (ServerRequestInterface $request) use ($stream) { - return new Response(200, array(), $stream); + return new Response( + 200, + array(), + $stream + ); }); $server->listen($this->socket); @@ -1078,8 +1103,11 @@ public function testConnectResponseStreamWillPipeDataFromConnection() public function testResponseContainsSameRequestProtocolVersionAndChunkedBodyForHttp11() { $server = new Server(function (ServerRequestInterface $request) { - $response = new Response(200, array(), 'bye'); - return \React\Promise\resolve($response); + return new Response( + 200, + array(), + 'bye' + ); }); $buffer = ''; @@ -1108,8 +1136,11 @@ function ($data) use (&$buffer) { public function testResponseContainsSameRequestProtocolVersionAndRawBodyForHttp10() { $server = new Server(function (ServerRequestInterface $request) { - $response = new Response(200, array(), 'bye'); - return \React\Promise\resolve($response); + return new Response( + 200, + array(), + 'bye' + ); }); $buffer = ''; @@ -1139,7 +1170,11 @@ function ($data) use (&$buffer) { public function testResponseContainsNoResponseBodyForHeadRequest() { $server = new Server(function (ServerRequestInterface $request) { - return new Response(200, array(), 'bye'); + return new Response( + 200, + array(), + 'bye' + ); }); $buffer = ''; @@ -1167,7 +1202,11 @@ function ($data) use (&$buffer) { public function testResponseContainsNoResponseBodyAndNoContentLengthForNoContentStatus() { $server = new Server(function (ServerRequestInterface $request) { - return new Response(204, array(), 'bye'); + return new Response( + 204, + array(), + 'bye' + ); }); $buffer = ''; @@ -1196,7 +1235,11 @@ function ($data) use (&$buffer) { public function testResponseContainsNoResponseBodyForNotModifiedStatus() { $server = new Server(function (ServerRequestInterface $request) { - return new Response(304, array(), 'bye'); + return new Response( + 304, + array(), + 'bye' + ); }); $buffer = ''; @@ -1323,7 +1366,7 @@ function ($data) use (&$buffer) { $this->assertContains("\r\n\r\nError 400: Bad Request", $buffer); } - public function testBodyDataWillBeSendViaRequestEvent() + public function testRequestContentLengthBodyDataWillEmitDataEventOnRequestStream() { $dataEvent = $this->expectCallableOnceWith('hello'); $endEvent = $this->expectCallableOnce(); @@ -1335,8 +1378,6 @@ public function testBodyDataWillBeSendViaRequestEvent() $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); $request->getBody()->on('error', $errorEvent); - - return \React\Promise\resolve(new Response()); }); $server->listen($this->socket); @@ -1352,7 +1393,7 @@ public function testBodyDataWillBeSendViaRequestEvent() $this->connection->emit('data', array($data)); } - public function testChunkedEncodedRequestWillBeParsedForRequestEvent() + public function testRequestChunkedTransferEncodingRequestWillEmitDecodedDataEventOnRequestStream() { $dataEvent = $this->expectCallableOnceWith('hello'); $endEvent = $this->expectCallableOnce(); @@ -1366,8 +1407,6 @@ public function testChunkedEncodedRequestWillBeParsedForRequestEvent() $request->getBody()->on('close', $closeEvent); $request->getBody()->on('error', $errorEvent); $requestValidation = $request; - - return \React\Promise\resolve(new Response()); }); $server->listen($this->socket); @@ -1386,7 +1425,7 @@ public function testChunkedEncodedRequestWillBeParsedForRequestEvent() $this->assertFalse($requestValidation->hasHeader('Transfer-Encoding')); } - public function testChunkedEncodedRequestAdditionalDataWontBeEmitted() + public function testRequestChunkedTransferEncodingWithAdditionalDataWontBeEmitted() { $dataEvent = $this->expectCallableOnceWith('hello'); $endEvent = $this->expectCallableOnce(); @@ -1398,8 +1437,6 @@ public function testChunkedEncodedRequestAdditionalDataWontBeEmitted() $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); $request->getBody()->on('error', $errorEvent); - - return \React\Promise\resolve(new Response()); }); $server->listen($this->socket); @@ -1417,7 +1454,7 @@ public function testChunkedEncodedRequestAdditionalDataWontBeEmitted() $this->connection->emit('data', array($data)); } - public function testEmptyChunkedEncodedRequest() + public function testRequestChunkedTransferEncodingEmpty() { $dataEvent = $this->expectCallableNever(); $endEvent = $this->expectCallableOnce(); @@ -1429,8 +1466,6 @@ public function testEmptyChunkedEncodedRequest() $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); $request->getBody()->on('error', $errorEvent); - - return \React\Promise\resolve(new Response()); }); $server->listen($this->socket); @@ -1446,7 +1481,7 @@ public function testEmptyChunkedEncodedRequest() $this->connection->emit('data', array($data)); } - public function testChunkedIsUpperCase() + public function testRequestChunkedTransferEncodingHeaderCanBeUpperCase() { $dataEvent = $this->expectCallableOnceWith('hello'); $endEvent = $this->expectCallableOnce(); @@ -1458,8 +1493,6 @@ public function testChunkedIsUpperCase() $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); $request->getBody()->on('error', $errorEvent); - - return \React\Promise\resolve(new Response()); }); $server->listen($this->socket); @@ -1476,7 +1509,7 @@ public function testChunkedIsUpperCase() $this->connection->emit('data', array($data)); } - public function testChunkedIsMixedUpperAndLowerCase() + public function testRequestChunkedTransferEncodingCanBeMixedUpperAndLowerCase() { $dataEvent = $this->expectCallableOnceWith('hello'); $endEvent = $this->expectCallableOnce(); @@ -1488,8 +1521,6 @@ public function testChunkedIsMixedUpperAndLowerCase() $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); $request->getBody()->on('error', $errorEvent); - - return \React\Promise\resolve(new Response()); }); $server->listen($this->socket); @@ -1571,7 +1602,7 @@ function ($data) use (&$buffer) { $this->assertContains("\r\n\r\nError 400: Bad Request", $buffer); } - public function testWontEmitFurtherDataWhenContentLengthIsReached() + public function testRequestContentLengthWillEmitDataEventAndEndEventAndAdditionalDataWillBeIgnored() { $dataEvent = $this->expectCallableOnceWith('hello'); $endEvent = $this->expectCallableOnce(); @@ -1601,7 +1632,7 @@ public function testWontEmitFurtherDataWhenContentLengthIsReached() $this->connection->emit('data', array($data)); } - public function testWontEmitFurtherDataWhenContentLengthIsReachedSplitted() + public function testRequestContentLengthWillEmitDataEventAndEndEventAndAdditionalDataWillBeIgnoredSplitted() { $dataEvent = $this->expectCallableOnceWith('hello'); $endEvent = $this->expectCallableOnce(); @@ -1614,8 +1645,6 @@ public function testWontEmitFurtherDataWhenContentLengthIsReachedSplitted() $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); $request->getBody()->on('error', $errorEvent); - - return \React\Promise\resolve(new Response()); }); $server->listen($this->socket); @@ -1635,7 +1664,7 @@ public function testWontEmitFurtherDataWhenContentLengthIsReachedSplitted() $this->connection->emit('data', array($data)); } - public function testContentLengthContainsZeroWillEmitEndEvent() + public function testRequestZeroContentLengthWillEmitEndEvent() { $dataEvent = $this->expectCallableNever(); @@ -1648,8 +1677,6 @@ public function testContentLengthContainsZeroWillEmitEndEvent() $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); $request->getBody()->on('error', $errorEvent); - - return \React\Promise\resolve(new Response()); }); $server->listen($this->socket); @@ -1664,7 +1691,7 @@ public function testContentLengthContainsZeroWillEmitEndEvent() $this->connection->emit('data', array($data)); } - public function testContentLengthContainsZeroWillEmitEndEventAdditionalDataWillBeIgnored() + public function testRequestZeroContentLengthWillEmitEndAndAdditionalDataWillBeIgnored() { $dataEvent = $this->expectCallableNever(); $endEvent = $this->expectCallableOnce(); @@ -1676,8 +1703,6 @@ public function testContentLengthContainsZeroWillEmitEndEventAdditionalDataWillB $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); $request->getBody()->on('error', $errorEvent); - - return \React\Promise\resolve(new Response()); }); $server->listen($this->socket); @@ -1693,7 +1718,7 @@ public function testContentLengthContainsZeroWillEmitEndEventAdditionalDataWillB $this->connection->emit('data', array($data)); } - public function testContentLengthContainsZeroWillEmitEndEventAdditionalDataWillBeIgnoredSplitted() + public function testRequestZeroContentLengthWillEmitEndAndAdditionalDataWillBeIgnoredSplitted() { $dataEvent = $this->expectCallableNever(); $endEvent = $this->expectCallableOnce(); @@ -1705,8 +1730,6 @@ public function testContentLengthContainsZeroWillEmitEndEventAdditionalDataWillB $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); $request->getBody()->on('error', $errorEvent); - - return \React\Promise\resolve(new Response()); }); $server->listen($this->socket); @@ -1725,7 +1748,7 @@ public function testContentLengthContainsZeroWillEmitEndEventAdditionalDataWillB $this->connection->emit('data', array($data)); } - public function testContentLengthWillBeIgnoredIfTransferEncodingIsSet() + public function testRequestContentLengthWillBeIgnoredIfTransferEncodingIsSet() { $dataEvent = $this->expectCallableOnceWith('hello'); $endEvent = $this->expectCallableOnce(); @@ -1739,8 +1762,6 @@ public function testContentLengthWillBeIgnoredIfTransferEncodingIsSet() $request->getBody()->on('close', $closeEvent); $request->getBody()->on('error', $errorEvent); $requestValidation = $request; - - return \React\Promise\resolve(new Response()); }); $server->listen($this->socket); @@ -1764,7 +1785,7 @@ public function testContentLengthWillBeIgnoredIfTransferEncodingIsSet() $this->assertFalse($requestValidation->hasHeader('Transfer-Encoding')); } - public function testInvalidContentLengthWillBeIgnoreddIfTransferEncodingIsSet() + public function testRequestInvalidContentLengthWillBeIgnoreddIfTransferEncodingIsSet() { $dataEvent = $this->expectCallableOnceWith('hello'); $endEvent = $this->expectCallableOnce(); @@ -1778,8 +1799,6 @@ public function testInvalidContentLengthWillBeIgnoreddIfTransferEncodingIsSet() $request->getBody()->on('close', $closeEvent); $request->getBody()->on('error', $errorEvent); $requestValidation = $request; - - return \React\Promise\resolve(new Response()); }); $server->listen($this->socket); @@ -1804,7 +1823,7 @@ public function testInvalidContentLengthWillBeIgnoreddIfTransferEncodingIsSet() $this->assertFalse($requestValidation->hasHeader('Transfer-Encoding')); } - public function testNonIntegerContentLengthValueWillLeadToError() + public function testRequestInvalidNonIntegerContentLengthWillEmitServerErrorAndSendResponse() { $error = null; $server = new Server($this->expectCallableNever()); @@ -1841,7 +1860,7 @@ function ($data) use (&$buffer) { $this->assertInstanceOf('InvalidArgumentException', $error); } - public function testNonIntegerContentLengthValueWillLeadToErrorWithNoBodyForHeadRequest() + public function testRequestInvalidHeadRequestWithInvalidNonIntegerContentLengthWillEmitServerErrorAndSendResponseWithoutBody() { $error = null; $server = new Server($this->expectCallableNever()); @@ -1878,7 +1897,7 @@ function ($data) use (&$buffer) { $this->assertInstanceOf('InvalidArgumentException', $error); } - public function testMultipleIntegerInContentLengthWillLeadToError() + public function testRequestInvalidMultipleContentLengthWillEmitErrorOnServer() { $error = null; $server = new Server($this->expectCallableNever()); @@ -1915,31 +1934,7 @@ function ($data) use (&$buffer) { $this->assertInstanceOf('InvalidArgumentException', $error); } - public function testInvalidChunkHeaderResultsInErrorOnRequestStream() - { - $errorEvent = $this->expectCallableOnceWith($this->isInstanceOf('Exception')); - $server = new Server(function ($request) use ($errorEvent){ - $request->getBody()->on('error', $errorEvent); - return \React\Promise\resolve(new Response()); - }); - - $this->connection->expects($this->never())->method('close'); - $this->connection->expects($this->once())->method('pause'); - - $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: chunked\r\n"; - $data .= "\r\n"; - $data .= "hello\r\hello\r\n"; - - $this->connection->emit('data', array($data)); - } - - public function testTooLongChunkHeaderResultsInErrorOnRequestStream() + public function testRequestInvalidChunkHeaderTooLongWillEmitErrorOnRequestStream() { $errorEvent = $this->expectCallableOnceWith($this->isInstanceOf('Exception')); $server = new Server(function ($request) use ($errorEvent){ @@ -1965,12 +1960,11 @@ public function testTooLongChunkHeaderResultsInErrorOnRequestStream() $this->connection->emit('data', array($data)); } - public function testTooLongChunkBodyResultsInErrorOnRequestStream() + public function testRequestInvalidChunkBodyTooLongWillEmitErrorOnRequestStream() { $errorEvent = $this->expectCallableOnceWith($this->isInstanceOf('Exception')); $server = new Server(function ($request) use ($errorEvent){ $request->getBody()->on('error', $errorEvent); - return \React\Promise\resolve(new Response()); }); $this->connection->expects($this->never())->method('close'); @@ -1989,12 +1983,11 @@ public function testTooLongChunkBodyResultsInErrorOnRequestStream() $this->connection->emit('data', array($data)); } - public function testUnexpectedEndOfConnectionWillResultsInErrorOnRequestStream() + public function testRequestUnexpectedEndOfRequestWithChunkedTransferConnectionWillEmitErrorOnRequestStream() { $errorEvent = $this->expectCallableOnceWith($this->isInstanceOf('Exception')); $server = new Server(function ($request) use ($errorEvent){ $request->getBody()->on('error', $errorEvent); - return \React\Promise\resolve(new Response()); }); $this->connection->expects($this->never())->method('close'); @@ -2014,10 +2007,11 @@ public function testUnexpectedEndOfConnectionWillResultsInErrorOnRequestStream() $this->connection->emit('end'); } - public function testErrorInChunkedDecoderNeverClosesConnection() + public function testRequestInvalidChunkHeaderWillEmitErrorOnRequestStream() { - $server = new Server(function (ServerRequestInterface $request) { - return \React\Promise\resolve(new Response()); + $errorEvent = $this->expectCallableOnceWith($this->isInstanceOf('Exception')); + $server = new Server(function ($request) use ($errorEvent){ + $request->getBody()->on('error', $errorEvent); }); $this->connection->expects($this->never())->method('close'); @@ -2036,10 +2030,11 @@ public function testErrorInChunkedDecoderNeverClosesConnection() $this->connection->emit('data', array($data)); } - public function testErrorInLengthLimitedStreamNeverClosesConnection() + public function testRequestUnexpectedEndOfRequestWithContentLengthWillEmitErrorOnRequestStream() { - $server = new Server(function (ServerRequestInterface $request) { - return \React\Promise\resolve(new Response()); + $errorEvent = $this->expectCallableOnceWith($this->isInstanceOf('Exception')); + $server = new Server(function ($request) use ($errorEvent){ + $request->getBody()->on('error', $errorEvent); }); $this->connection->expects($this->never())->method('close'); @@ -2051,32 +2046,15 @@ public function testErrorInLengthLimitedStreamNeverClosesConnection() $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 .= "Content-Length: 500\r\n"; $data .= "\r\n"; - $data .= "hello"; + $data .= "incomplete"; $this->connection->emit('data', array($data)); $this->connection->emit('end'); } - public function testCloseRequestWillPauseConnection() - { - $server = new Server(function ($request) { - $request->getBody()->close(); - return \React\Promise\resolve(new Response()); - }); - - $this->connection->expects($this->never())->method('close'); - $this->connection->expects($this->once())->method('pause'); - - $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); - - $data = $this->createGetRequest(); - $this->connection->emit('data', array($data)); - } - - public function testEndEventWillBeEmittedOnSimpleRequest() + public function testRequestWithoutBodyWillEmitEndOnRequestStream() { $dataEvent = $this->expectCallableNever(); $closeEvent = $this->expectCallableOnce(); @@ -2088,8 +2066,6 @@ public function testEndEventWillBeEmittedOnSimpleRequest() $request->getBody()->on('close', $closeEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('error', $errorEvent); - - return \React\Promise\resolve(new Response()); }); $this->connection->expects($this->once())->method('pause'); @@ -2115,8 +2091,6 @@ public function testRequestWithoutDefinedLengthWillIgnoreDataEvent() $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); $request->getBody()->on('error', $errorEvent); - - return \React\Promise\resolve(new Response()); }); $server->listen($this->socket); @@ -2128,12 +2102,15 @@ public function testRequestWithoutDefinedLengthWillIgnoreDataEvent() $this->connection->emit('data', array($data)); } - public function testResponseWillBeChunkDecodedByDefault() + public function testResponseWithBodyStreamWillUseChunkedTransferEncodingByDefault() { $stream = new ThroughStream(); $server = new Server(function (ServerRequestInterface $request) use ($stream) { - $response = new Response(200, array(), $stream); - return \React\Promise\resolve($response); + return new Response( + 200, + array(), + $stream + ); }); $buffer = ''; @@ -2160,19 +2137,17 @@ function ($data) use (&$buffer) { $this->assertContains("hello", $buffer); } - public function testContentLengthWillBeRemovedForResponseStream() + public function testResponseWithBodyStringWillOverwriteExplicitContentLengthAndTransferEncoding() { $server = new Server(function (ServerRequestInterface $request) { - $response = new Response( + return new Response( 200, array( - 'Content-Length' => 5, + 'Content-Length' => 1000, 'Transfer-Encoding' => 'chunked' ), 'hello' ); - - return \React\Promise\resolve($response); }); $buffer = ''; @@ -2199,19 +2174,17 @@ function ($data) use (&$buffer) { $this->assertContains("hello", $buffer); } - public function testOnlyAllowChunkedEncoding() + public function testResponseWithCustomTransferEncodingWillBeIgnoredAndUseChunkedTransferEncodingInstead() { $stream = new ThroughStream(); $server = new Server(function (ServerRequestInterface $request) use ($stream) { - $response = new Response( + return new Response( 200, array( 'Transfer-Encoding' => 'custom' ), $stream ); - - return \React\Promise\resolve($response); }); $buffer = ''; @@ -2239,10 +2212,10 @@ function ($data) use (&$buffer) { $this->assertContains("5\r\nhello\r\n", $buffer); } - public function testDateHeaderWillBeAddedWhenNoneIsGiven() + public function testResponseWithoutExplicitDateHeaderWillAddCurrentDate() { $server = new Server(function (ServerRequestInterface $request) { - return \React\Promise\resolve(new Response()); + return new Response(); }); $buffer = ''; @@ -2269,11 +2242,13 @@ function ($data) use (&$buffer) { $this->assertContains("\r\n\r\n", $buffer); } - public function testAddCustomDateHeader() + public function testResponseWIthCustomDateHeaderOverwritesDefault() { $server = new Server(function (ServerRequestInterface $request) { - $response = new Response(200, array("Date" => "Tue, 15 Nov 1994 08:12:31 GMT")); - return \React\Promise\resolve($response); + return new Response( + 200, + array("Date" => "Tue, 15 Nov 1994 08:12:31 GMT") + ); }); $buffer = ''; @@ -2300,11 +2275,13 @@ function ($data) use (&$buffer) { $this->assertContains("\r\n\r\n", $buffer); } - public function testRemoveDateHeader() + public function testResponseWithEmptyDateHeaderRemovesDateHeader() { $server = new Server(function (ServerRequestInterface $request) { - $response = new Response(200, array('Date' => '')); - return \React\Promise\resolve($response); + return new Response( + 200, + array('Date' => '') + ); }); $buffer = ''; @@ -2331,7 +2308,7 @@ function ($data) use (&$buffer) { $this->assertContains("\r\n\r\n", $buffer); } - public function testAddMultipleCookieHeaders() + public function testResponseCanContainMultipleCookieHeaders() { $server = new Server(function (ServerRequestInterface $request) { return new Response( @@ -2369,7 +2346,7 @@ 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 testOnlyChunkedEncodingIsAllowedForTransferEncoding() + public function testRequestOnlyChunkedEncodingIsAllowedForTransferEncoding() { $error = null; @@ -2406,7 +2383,7 @@ function ($data) use (&$buffer) { $this->assertInstanceOf('InvalidArgumentException', $error); } - public function testOnlyChunkedEncodingIsAllowedForTransferEncodingWithHttp10() + public function testRequestOnlyChunkedEncodingIsAllowedForTransferEncodingWithHttp10() { $error = null; @@ -2441,10 +2418,10 @@ function ($data) use (&$buffer) { $this->assertInstanceOf('InvalidArgumentException', $error); } - public function test100ContinueRequestWillBeHandled() + public function testReponseWithExpectContinueRequestContainsContinueWithLaterResponse() { $server = new Server(function (ServerRequestInterface $request) { - return \React\Promise\resolve(new Response()); + return new Response(); }); $buffer = ''; @@ -2473,10 +2450,10 @@ function ($data) use (&$buffer) { $this->assertContains("HTTP/1.1 200 OK\r\n", $buffer); } - public function testContinueWontBeSendForHttp10() + public function testResponseWithExpectContinueRequestWontSendContinueForHttp10() { $server = new Server(function (ServerRequestInterface $request) { - return \React\Promise\resolve(new Response()); + return new Response(); }); $buffer = ''; @@ -2503,40 +2480,6 @@ function ($data) use (&$buffer) { $this->assertNotContains("HTTP/1.1 100 Continue\r\n\r\n", $buffer); } - public function testContinueWithLaterResponse() - { - $server = new Server(function (ServerRequestInterface $request) { - return \React\Promise\resolve(new Response()); - }); - - - $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 .= "Expect: 100-continue\r\n"; - $data .= "\r\n"; - - $this->connection->emit('data', array($data)); - - $this->assertContains("HTTP/1.1 100 Continue\r\n\r\n", $buffer); - $this->assertContains("HTTP/1.1 200 OK\r\n", $buffer); - } - /** * @expectedException InvalidArgumentException */ @@ -2545,13 +2488,16 @@ public function testInvalidCallbackFunctionLeadsToException() $server = new Server('invalid'); } - public function testHttpBodyStreamAsBodyWillStreamData() + public function testResponseBodyStreamWillStreamDataWithChunkedTransferEncoding() { $input = new ThroughStream(); $server = new Server(function (ServerRequestInterface $request) use ($input) { - $response = new Response(200, array(), $input); - return \React\Promise\resolve($response); + return new Response( + 200, + array(), + $input + ); }); $buffer = ''; @@ -2581,13 +2527,16 @@ function ($data) use (&$buffer) { $this->assertContains("2\r\n23\r\n", $buffer); } - public function testHttpBodyStreamWithContentLengthWillStreamTillLength() + public function testResponseBodyStreamWithContentLengthWillStreamTillLengthWithoutTransferEncoding() { $input = new ThroughStream(); $server = new Server(function (ServerRequestInterface $request) use ($input) { - $response = new Response(200, array('Content-Length' => 5), $input); - return \React\Promise\resolve($response); + return new Response( + 200, + array('Content-Length' => 5), + $input + ); }); $buffer = ''; @@ -2618,7 +2567,7 @@ function ($data) use (&$buffer) { $this->assertContains("hello", $buffer); } - public function testCallbackFunctionReturnsPromise() + public function testResponseWithResponsePromise() { $server = new Server(function (ServerRequestInterface $request) { return \React\Promise\resolve(new Response()); @@ -2646,7 +2595,7 @@ function ($data) use (&$buffer) { $this->assertContains("\r\n\r\n", $buffer); } - public function testReturnInvalidTypeWillResultInError() + public function testResponseReturnInvalidTypeWillResultInError() { $server = new Server(function (ServerRequestInterface $request) { return "invalid"; @@ -2680,7 +2629,7 @@ function ($data) use (&$buffer) { $this->assertInstanceOf('RuntimeException', $exception); } - public function testResolveWrongTypeInPromiseWillResultInError() + public function testResponseResolveWrongTypeInPromiseWillResultInError() { $server = new Server(function (ServerRequestInterface $request) { return \React\Promise\resolve("invalid"); @@ -2708,7 +2657,7 @@ function ($data) use (&$buffer) { $this->assertContains("HTTP/1.1 500 Internal Server Error\r\n", $buffer); } - public function testRejectedPromiseWillResultInErrorMessage() + public function testResponseRejectedPromiseWillResultInErrorMessage() { $server = new Server(function (ServerRequestInterface $request) { return new Promise(function ($resolve, $reject) { @@ -2739,7 +2688,7 @@ function ($data) use (&$buffer) { $this->assertContains("HTTP/1.1 500 Internal Server Error\r\n", $buffer); } - public function testExcpetionInCallbackWillResultInErrorMessage() + public function testResponseExceptionInCallbackWillResultInErrorMessage() { $server = new Server(function (ServerRequestInterface $request) { return new Promise(function ($resolve, $reject) { @@ -2770,10 +2719,14 @@ function ($data) use (&$buffer) { $this->assertContains("HTTP/1.1 500 Internal Server Error\r\n", $buffer); } - public function testHeaderWillAlwaysBeContentLengthForStringBody() + public function testResponseWithContentLengthHeaderForStringBodyOverwritesTransferEncoding() { $server = new Server(function (ServerRequestInterface $request) { - return new Response(200, array('Transfer-Encoding' => 'chunked'), 'hello'); + return new Response( + 200, + array('Transfer-Encoding' => 'chunked'), + 'hello' + ); }); $buffer = ''; @@ -2802,7 +2755,7 @@ function ($data) use (&$buffer) { $this->assertNotContains("Transfer-Encoding", $buffer); } - public function testReturnRequestWillBeHandled() + public function testResponseWillBeHandled() { $server = new Server(function (ServerRequestInterface $request) { return new Response(); @@ -2830,7 +2783,7 @@ function ($data) use (&$buffer) { $this->assertContains("HTTP/1.1 200 OK\r\n", $buffer); } - public function testExceptionThrowInCallBackFunctionWillResultInErrorMessage() + public function testResponseExceptionThrowInCallBackFunctionWillResultInErrorMessage() { $server = new Server(function (ServerRequestInterface $request) { throw new \Exception('hello'); @@ -2868,7 +2821,7 @@ function ($data) use (&$buffer) { /** * @requires PHP 7 */ - public function testThrowableThrowInCallBackFunctionWillResultInErrorMessage() + public function testResponseThrowableThrowInCallBackFunctionWillResultInErrorMessage() { $server = new Server(function (ServerRequestInterface $request) { throw new \Error('hello'); @@ -2911,7 +2864,7 @@ function ($data) use (&$buffer) { $this->assertEquals('hello', $exception->getPrevious()->getMessage()); } - public function testRejectOfNonExceptionWillResultInErrorMessage() + public function testResponseRejectOfNonExceptionWillResultInErrorMessage() { $server = new Server(function (ServerRequestInterface $request) { return new Promise(function ($resolve, $reject) { @@ -2947,12 +2900,11 @@ function ($data) use (&$buffer) { $this->assertInstanceOf('RuntimeException', $exception); } - public function testServerRequestParams() + public function testRequestServerRequestParams() { $requestValidation = null; $server = new Server(function (ServerRequestInterface $request) use (&$requestValidation) { $requestValidation = $request; - return new Response(); }); $this->connection @@ -2982,12 +2934,11 @@ public function testServerRequestParams() $this->assertNotNull($serverParams['REQUEST_TIME_FLOAT']); } - public function testQueryParametersWillBeAddedToRequest() + public function testRequestQueryParametersWillBeAddedToRequest() { $requestValidation = null; $server = new Server(function (ServerRequestInterface $request) use (&$requestValidation) { $requestValidation = $request; - return new Response(); }); $server->listen($this->socket); @@ -3003,12 +2954,11 @@ public function testQueryParametersWillBeAddedToRequest() $this->assertEquals('bar', $queryParams['test']); } - public function testCookieWillBeAddedToServerRequest() + public function testRequestCookieWillBeAddedToServerRequest() { $requestValidation = null; $server = new Server(function (ServerRequestInterface $request) use (&$requestValidation) { $requestValidation = $request; - return new Response(); }); $server->listen($this->socket); @@ -3025,12 +2975,11 @@ public function testCookieWillBeAddedToServerRequest() $this->assertEquals(array('hello' => 'world'), $requestValidation->getCookieParams()); } - public function testMultipleCookiesWontBeAddedToServerRequest() + public function testRequestInvalidMultipleCookiesWontBeAddedToServerRequest() { $requestValidation = null; $server = new Server(function (ServerRequestInterface $request) use (&$requestValidation) { $requestValidation = $request; - return new Response(); }); $server->listen($this->socket); @@ -3047,12 +2996,11 @@ public function testMultipleCookiesWontBeAddedToServerRequest() $this->assertEquals(array(), $requestValidation->getCookieParams()); } - public function testCookieWithSeparatorWillBeAddedToServerRequest() + public function testRequestCookieWithSeparatorWillBeAddedToServerRequest() { $requestValidation = null; $server = new Server(function (ServerRequestInterface $request) use (&$requestValidation) { $requestValidation = $request; - return new Response(); }); $server->listen($this->socket); From 836b5034656262d4fb0a6a8fc135a2ae8a95db54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 17 Nov 2017 19:17:49 +0100 Subject: [PATCH 204/456] Move @internal classes to Io namespace --- src/{ => Io}/ChunkedDecoder.php | 5 +++-- src/{ => Io}/ChunkedEncoder.php | 4 ++-- src/{ => Io}/CloseProtectionStream.php | 7 ++++--- src/{ => Io}/HttpBodyStream.php | 7 ++++--- src/{ => Io}/LengthLimitedStream.php | 4 ++-- src/{Middleware => Io}/MultipartParser.php | 4 +--- src/{ => Io}/RequestHeaderParser.php | 4 ++-- src/{ => Io}/ServerRequest.php | 2 +- src/{ => Io}/UploadedFile.php | 4 ++-- src/Middleware/RequestBodyParserMiddleware.php | 1 + src/Response.php | 4 ++-- src/Server.php | 7 +++++++ tests/{ => Io}/ChunkedDecoderTest.php | 5 +++-- tests/{ => Io}/ChunkedEncoderTest.php | 5 +++-- tests/{ => Io}/CloseProtectionStreamTest.php | 5 +++-- tests/{ => Io}/HttpBodyStreamTest.php | 5 +++-- tests/{ => Io}/LengthLimitedStreamTest.php | 5 +++-- tests/{Middleware => Io}/MultipartParserTest.php | 6 +++--- tests/{ => Io}/RequestHeaderParserTest.php | 5 +++-- tests/{ => Io}/ServerRequestTest.php | 5 +++-- tests/{ => Io}/UploadedFileTest.php | 7 +++---- tests/Middleware/RequestBodyBufferMiddlewareTest.php | 6 +++--- tests/Middleware/RequestBodyParserMiddlewareTest.php | 2 +- tests/MiddlewareRunnerTest.php | 4 ++-- tests/ResponseTest.php | 2 +- tests/benchmark-middleware-runner.php | 2 +- 26 files changed, 66 insertions(+), 51 deletions(-) rename src/{ => Io}/ChunkedDecoder.php (99%) rename src/{ => Io}/ChunkedEncoder.php (98%) rename src/{ => Io}/CloseProtectionStream.php (98%) rename src/{ => Io}/HttpBodyStream.php (99%) rename src/{ => Io}/LengthLimitedStream.php (99%) rename src/{Middleware => Io}/MultipartParser.php (98%) rename src/{ => Io}/RequestHeaderParser.php (99%) rename src/{ => Io}/ServerRequest.php (99%) rename src/{ => Io}/UploadedFile.php (98%) rename tests/{ => Io}/ChunkedDecoderTest.php (99%) rename tests/{ => Io}/ChunkedEncoderTest.php (95%) rename tests/{ => Io}/CloseProtectionStreamTest.php (97%) rename tests/{ => Io}/HttpBodyStreamTest.php (97%) rename tests/{ => Io}/LengthLimitedStreamTest.php (97%) rename tests/{Middleware => Io}/MultipartParserTest.php (97%) rename tests/{ => Io}/RequestHeaderParserTest.php (99%) rename tests/{ => Io}/ServerRequestTest.php (98%) rename tests/{ => Io}/UploadedFileTest.php (94%) diff --git a/src/ChunkedDecoder.php b/src/Io/ChunkedDecoder.php similarity index 99% rename from src/ChunkedDecoder.php rename to src/Io/ChunkedDecoder.php index 4b6ebc4f..5ee6bb12 100644 --- a/src/ChunkedDecoder.php +++ b/src/Io/ChunkedDecoder.php @@ -1,10 +1,11 @@ assertInstanceOf('React\Http\HttpBodyStream', $response->getBody()); + $this->assertInstanceOf('React\Http\Io\HttpBodyStream', $response->getBody()); } public function testStringBodyWillBePsr7Stream() diff --git a/tests/benchmark-middleware-runner.php b/tests/benchmark-middleware-runner.php index 050f8c2c..4f01ace1 100644 --- a/tests/benchmark-middleware-runner.php +++ b/tests/benchmark-middleware-runner.php @@ -1,9 +1,9 @@ Date: Fri, 17 Nov 2017 19:59:28 +0100 Subject: [PATCH 205/456] Add basic documentation for internal classes --- src/Io/ChunkedDecoder.php | 8 +++++++- src/Io/ChunkedEncoder.php | 8 +++++++- src/Io/CloseProtectionStream.php | 6 +++++- src/Io/HttpBodyStream.php | 22 +++++++++++++++------- src/Io/LengthLimitedStream.php | 9 ++++++++- src/Io/MultipartParser.php | 5 +++++ src/Io/RequestHeaderParser.php | 5 +++++ src/Io/ServerRequest.php | 15 ++++++++++++++- src/Io/UploadedFile.php | 8 ++++++++ 9 files changed, 74 insertions(+), 12 deletions(-) diff --git a/src/Io/ChunkedDecoder.php b/src/Io/ChunkedDecoder.php index 5ee6bb12..ef2ad915 100644 --- a/src/Io/ChunkedDecoder.php +++ b/src/Io/ChunkedDecoder.php @@ -8,7 +8,13 @@ use React\Stream\WritableStreamInterface; use Exception; -/** @internal */ +/** + * [Internal] Decodes "Transfer-Encoding: chunked" from given stream and returns only payload data. + * + * This is used internally to decode incoming requests with this encoding. + * + * @internal + */ class ChunkedDecoder extends EventEmitter implements ReadableStreamInterface { const CRLF = "\r\n"; diff --git a/src/Io/ChunkedEncoder.php b/src/Io/ChunkedEncoder.php index 42f93802..c075bdbc 100644 --- a/src/Io/ChunkedEncoder.php +++ b/src/Io/ChunkedEncoder.php @@ -7,7 +7,13 @@ use React\Stream\Util; use React\Stream\WritableStreamInterface; -/** @internal */ +/** + * [Internal] Encodes given payload stream with "Transfer-Encoding: chunked" and emits encoded data + * + * This is used internally to encode outgoing requests with this encoding. + * + * @internal + */ class ChunkedEncoder extends EventEmitter implements ReadableStreamInterface { private $input; diff --git a/src/Io/CloseProtectionStream.php b/src/Io/CloseProtectionStream.php index a9615063..018747fb 100644 --- a/src/Io/CloseProtectionStream.php +++ b/src/Io/CloseProtectionStream.php @@ -8,8 +8,12 @@ use React\Stream\WritableStreamInterface; /** + * [Internal] Protects a given stream from actually closing and only pauses it instead. + * + * This is used internally to prevent the underlying connection from closing, so + * that we can still send back a response over the same stream. + * * @internal - * This stream is used to protect the passed stream against closing. * */ class CloseProtectionStream extends EventEmitter implements ReadableStreamInterface { diff --git a/src/Io/HttpBodyStream.php b/src/Io/HttpBodyStream.php index f45b2144..25d15a18 100644 --- a/src/Io/HttpBodyStream.php +++ b/src/Io/HttpBodyStream.php @@ -9,12 +9,20 @@ use React\Stream\WritableStreamInterface; /** + * [Internal] Bridge between StreamInterface from PSR-7 and ReadableStreamInterface from ReactPHP + * + * This class is used in the server to stream the body of an incoming response + * from the client. This allows us to stream big amounts of data without having + * to buffer this data. Similarly, this used to stream the body of an outgoing + * request body to the client. The data will be sent directly to the client. + * + * Note that this is an internal class only and nothing you should usually care + * about. See the `StreamInterface` and `ReadableStreamInterface` for more + * details. + * + * @see StreamInterface + * @see ReadableStreamInterface * @internal - * Uses a StreamInterface from PSR-7 and a ReadableStreamInterface from ReactPHP - * to use PSR-7 objects and use ReactPHP streams - * This is class is used in `HttpServer` to stream the body of a response - * to the client. Because of this you can stream big amount of data without - * necessity of buffering this data. The data will be send directly to the client. */ class HttpBodyStream extends EventEmitter implements StreamInterface, ReadableStreamInterface { @@ -23,8 +31,8 @@ class HttpBodyStream extends EventEmitter implements StreamInterface, ReadableSt private $size; /** - * @param ReadableStreamInterface $input - Stream data from $stream as a body of a PSR-7 object4 - * @param int|null $size - size of the data body + * @param ReadableStreamInterface $input Stream data from $stream as a body of a PSR-7 object4 + * @param int|null $size size of the data body */ public function __construct(ReadableStreamInterface $input, $size) { diff --git a/src/Io/LengthLimitedStream.php b/src/Io/LengthLimitedStream.php index 347df621..e1d93a02 100644 --- a/src/Io/LengthLimitedStream.php +++ b/src/Io/LengthLimitedStream.php @@ -7,7 +7,14 @@ use React\Stream\Util; use React\Stream\WritableStreamInterface; -/** @internal */ +/** + * [Internal] Limits the amount of data the given stream can emit + * + * This is used internally to limit the size of the underlying connection stream + * to the size defined by the "Content-Length" header of the incoming request. + * + * @internal + */ class LengthLimitedStream extends EventEmitter implements ReadableStreamInterface { private $stream; diff --git a/src/Io/MultipartParser.php b/src/Io/MultipartParser.php index 696c0fb3..211f084d 100644 --- a/src/Io/MultipartParser.php +++ b/src/Io/MultipartParser.php @@ -6,6 +6,11 @@ use RingCentral\Psr7; /** + * [Internal] Parses a string body with "Content-Type: multipart/form-data" into structured data + * + * This is use internally to parse incoming request bodies into structured data + * that resembles PHP's `$_POST` and `$_FILES` superglobals. + * * @internal */ final class MultipartParser diff --git a/src/Io/RequestHeaderParser.php b/src/Io/RequestHeaderParser.php index 9d25ce90..63263b52 100644 --- a/src/Io/RequestHeaderParser.php +++ b/src/Io/RequestHeaderParser.php @@ -7,6 +7,11 @@ use Exception; /** + * [Internal] Parses an incoming request header from an input stream + * + * This is used internally to parse the request header from the connection and + * then process the remaining connection as the request body. + * * @event headers * @event error * diff --git a/src/Io/ServerRequest.php b/src/Io/ServerRequest.php index f1df9540..a9bb8a45 100644 --- a/src/Io/ServerRequest.php +++ b/src/Io/ServerRequest.php @@ -5,7 +5,20 @@ use Psr\Http\Message\ServerRequestInterface; use RingCentral\Psr7\Request; -/** @internal */ +/** + * [Internal] Implementation of the PSR-7 `ServerRequestInterface` + * + * This is used internally to represent each incoming request message. + * + * This implementation builds on top of an existing outgoing request message and + * only adds required server methods. + * + * Note that this is an internal class only and nothing you should usually care + * about. See the `ServerRequestInterface` for more details. + * + * @see ServerRequestInterface + * @internal + */ class ServerRequest extends Request implements ServerRequestInterface { private $attributes = array(); diff --git a/src/Io/UploadedFile.php b/src/Io/UploadedFile.php index ad843445..64717b7d 100644 --- a/src/Io/UploadedFile.php +++ b/src/Io/UploadedFile.php @@ -8,6 +8,14 @@ use RuntimeException; /** + * [Internal] Implementation of the PSR-7 `UploadedFileInterface` + * + * This is used internally to represent each incoming file upload. + * + * Note that this is an internal class only and nothing you should usually care + * about. See the `UploadedFileInterface` for more details. + * + * @see UploadedFileInterface * @internal */ final class UploadedFile implements UploadedFileInterface From cdf9debf1796124157877581123195a42e6cf0de Mon Sep 17 00:00:00 2001 From: andig Date: Sun, 15 Oct 2017 11:31:55 +0200 Subject: [PATCH 206/456] Implement buffering for requests with chunked transfer encoding --- README.md | 13 ++--- composer.json | 2 +- .../RequestBodyBufferMiddleware.php | 20 ++++---- .../RequestBodyBufferMiddlewareTest.php | 47 +++++++++++++------ 4 files changed, 50 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 3396c345..0286f3e3 100644 --- a/README.md +++ b/README.md @@ -697,16 +697,11 @@ Any incoming request that has a request body that exceeds this limit will be rejected with a `413` (Request Entity Too Large) error message without calling the next middleware handlers. -Any incoming request that does not have its size defined and uses the (rare) -`Transfer-Encoding: chunked` will be rejected with a `411` (Length Required) -error message without calling the next middleware handlers. -Note that this only affects incoming requests, the much more common chunked -transfer encoding for outgoing responses is not affected. -It is recommended to define a `Content-Length` header instead. -Note that this does not affect normal requests without a request body -(such as a simple `GET` request). +The `RequestBodyBufferMiddleware` will buffer requests with bodies of known size +(i.e. with `Content-Length` header specified) as well as requests with bodies of +unknown size (i.e. with `Transfer-Encoding: chunked` header). -All other requests will be buffered in memory until the request body end has +All requests will be buffered in memory until the request body end has been reached and then call the next middleware handler with the complete, buffered request. Similarly, this will immediately invoke the next middleware handler for requests diff --git a/composer.json b/composer.json index 6e577877..f6a4fe24 100644 --- a/composer.json +++ b/composer.json @@ -10,7 +10,7 @@ "react/stream": "^1.0 || ^0.7.1", "react/promise": "^2.3 || ^1.2.1", "evenement/evenement": "^3.0 || ^2.0 || ^1.0", - "react/promise-stream": "^0.1.1" + "react/promise-stream": "^1.0 || ^0.1.2" }, "autoload": { "psr-4": { diff --git a/src/Middleware/RequestBodyBufferMiddleware.php b/src/Middleware/RequestBodyBufferMiddleware.php index 93ba2126..9a5a3a48 100644 --- a/src/Middleware/RequestBodyBufferMiddleware.php +++ b/src/Middleware/RequestBodyBufferMiddleware.php @@ -7,6 +7,7 @@ use React\Promise\Stream; use React\Stream\ReadableStreamInterface; use RingCentral\Psr7\BufferStream; +use OverflowException; final class RequestBodyBufferMiddleware { @@ -29,27 +30,30 @@ public function __construct($sizeLimit = null) public function __invoke(ServerRequestInterface $request, $stack) { - $size = $request->getBody()->getSize(); - - if ($size === null) { - return new Response(411, array('Content-Type' => 'text/plain'), 'No Content-Length header given'); - } + $body = $request->getBody(); - if ($size > $this->sizeLimit) { + // request body of known size exceeding limit + if ($body->getSize() > $this->sizeLimit) { return new Response(413, array('Content-Type' => 'text/plain'), 'Request body exceeds allowed limit'); } - $body = $request->getBody(); if (!$body instanceof ReadableStreamInterface) { return $stack($request); } - return Stream\buffer($body)->then(function ($buffer) use ($request, $stack) { + return Stream\buffer($body, $this->sizeLimit)->then(function ($buffer) use ($request, $stack) { $stream = new BufferStream(strlen($buffer)); $stream->write($buffer); $request = $request->withBody($stream); return $stack($request); + }, function($error) { + // request body of unknown size exceeding limit during buffering + if ($error instanceof OverflowException) { + return new Response(413, array('Content-Type' => 'text/plain'), 'Request body exceeds allowed limit'); + } + + return $error; }); } diff --git a/tests/Middleware/RequestBodyBufferMiddlewareTest.php b/tests/Middleware/RequestBodyBufferMiddlewareTest.php index 8afc9fa1..f5ef7f05 100644 --- a/tests/Middleware/RequestBodyBufferMiddlewareTest.php +++ b/tests/Middleware/RequestBodyBufferMiddlewareTest.php @@ -3,12 +3,14 @@ namespace React\Tests\Http\Middleware; use Psr\Http\Message\ServerRequestInterface; +use React\EventLoop\Factory; use React\Http\Io\HttpBodyStream; use React\Http\Io\ServerRequest; use React\Http\Middleware\RequestBodyBufferMiddleware; use React\Stream\ThroughStream; use React\Tests\Http\TestCase; use RingCentral\Psr7\BufferStream; +use Clue\React\Block; final class RequestBodyBufferMiddlewareTest extends TestCase { @@ -63,45 +65,62 @@ function (ServerRequestInterface $request) use (&$exposedRequest) { $this->assertSame($body, $exposedRequest->getBody()->getContents()); } - public function testUnknownSizeReturnsError411() + public function testExcessiveSizeImmediatelyReturnsError413ForKnownSize() { - $body = $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(); - $body->expects($this->once())->method('getSize')->willReturn(null); - + $loop = Factory::create(); + + $stream = new ThroughStream(); + $stream->end('aa'); $serverRequest = new ServerRequest( 'GET', 'https://example.com/', array(), - $body + new HttpBodyStream($stream, 2) ); - $buffer = new RequestBodyBufferMiddleware(); + $buffer = new RequestBodyBufferMiddleware(1); $response = $buffer( $serverRequest, - function () {} + function (ServerRequestInterface $request) { + return $request; + } ); - $this->assertSame(411, $response->getStatusCode()); + $this->assertSame(413, $response->getStatusCode()); } public function testExcessiveSizeReturnsError413() { - $stream = new BufferStream(2); - $stream->write('aa'); + $loop = Factory::create(); + $stream = new ThroughStream(); $serverRequest = new ServerRequest( 'GET', 'https://example.com/', array(), - $stream + new HttpBodyStream($stream, null) ); $buffer = new RequestBodyBufferMiddleware(1); - $response = $buffer( + $promise = $buffer( $serverRequest, - function () {} + function (ServerRequestInterface $request) { + return $request; + } ); - $this->assertSame(413, $response->getStatusCode()); + $stream->end('aa'); + + $exposedResponse = null; + $promise->then( + function($response) use (&$exposedResponse) { + $exposedResponse = $response; + }, + $this->expectCallableNever() + ); + + $this->assertSame(413, $exposedResponse->getStatusCode()); + + Block\await($promise, $loop); } } From cbf14ebe27c2a056ab7e5ca36671937d1937c441 Mon Sep 17 00:00:00 2001 From: andig Date: Tue, 14 Nov 2017 14:50:30 +0100 Subject: [PATCH 207/456] Handle errors during buffering --- .../RequestBodyBufferMiddleware.php | 2 +- .../RequestBodyBufferMiddlewareTest.php | 28 +++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/Middleware/RequestBodyBufferMiddleware.php b/src/Middleware/RequestBodyBufferMiddleware.php index 9a5a3a48..b1d0ff47 100644 --- a/src/Middleware/RequestBodyBufferMiddleware.php +++ b/src/Middleware/RequestBodyBufferMiddleware.php @@ -53,7 +53,7 @@ public function __invoke(ServerRequestInterface $request, $stack) return new Response(413, array('Content-Type' => 'text/plain'), 'Request body exceeds allowed limit'); } - return $error; + throw $error; }); } diff --git a/tests/Middleware/RequestBodyBufferMiddlewareTest.php b/tests/Middleware/RequestBodyBufferMiddlewareTest.php index f5ef7f05..0e682435 100644 --- a/tests/Middleware/RequestBodyBufferMiddlewareTest.php +++ b/tests/Middleware/RequestBodyBufferMiddlewareTest.php @@ -123,4 +123,32 @@ function($response) use (&$exposedResponse) { Block\await($promise, $loop); } + + /** + * @expectedException RuntimeException + */ + public function testBufferingErrorThrows() + { + $loop = Factory::create(); + + $stream = new ThroughStream(); + $serverRequest = new ServerRequest( + 'GET', + 'https://example.com/', + array(), + new HttpBodyStream($stream, null) + ); + + $buffer = new RequestBodyBufferMiddleware(1); + $promise = $buffer( + $serverRequest, + function (ServerRequestInterface $request) { + return $request; + } + ); + + $stream->emit('error', array(new \RuntimeException())); + + Block\await($promise, $loop); + } } From 37faecba41911cafa369a8eca47432ae62687f24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 21 Nov 2017 16:54:33 +0100 Subject: [PATCH 208/456] Raise maximum request header size to 8k to match common implementations --- src/Io/RequestHeaderParser.php | 11 ++--------- tests/Io/RequestHeaderParserTest.php | 6 +++--- tests/ServerTest.php | 2 +- 3 files changed, 6 insertions(+), 13 deletions(-) diff --git a/src/Io/RequestHeaderParser.php b/src/Io/RequestHeaderParser.php index 63263b52..21d2d322 100644 --- a/src/Io/RequestHeaderParser.php +++ b/src/Io/RequestHeaderParser.php @@ -20,7 +20,7 @@ class RequestHeaderParser extends EventEmitter { private $buffer = ''; - private $maxSize = 4096; + private $maxSize = 8192; private $localSocketUri; private $remoteSocketUri; @@ -34,16 +34,9 @@ public function __construct($localSocketUri = null, $remoteSocketUri = null) public function feed($data) { $this->buffer .= $data; - $endOfHeader = strpos($this->buffer, "\r\n\r\n"); - if (false !== $endOfHeader) { - $currentHeaderSize = $endOfHeader; - } else { - $currentHeaderSize = strlen($this->buffer); - } - - if ($currentHeaderSize > $this->maxSize) { + if ($endOfHeader > $this->maxSize || ($endOfHeader === false && isset($this->buffer[$this->maxSize]))) { $this->emit('error', array(new \OverflowException("Maximum header size of {$this->maxSize} exceeded.", 431), $this)); $this->removeAllListeners(); return; diff --git a/tests/Io/RequestHeaderParserTest.php b/tests/Io/RequestHeaderParserTest.php index 0aea1af7..6b885bd4 100644 --- a/tests/Io/RequestHeaderParserTest.php +++ b/tests/Io/RequestHeaderParserTest.php @@ -140,11 +140,11 @@ public function testHeaderOverflowShouldEmitError() $this->assertSame(1, count($parser->listeners('headers'))); $this->assertSame(1, count($parser->listeners('error'))); - $data = str_repeat('A', 4097); + $data = str_repeat('A', 8193); $parser->feed($data); $this->assertInstanceOf('OverflowException', $error); - $this->assertSame('Maximum header size of 4096 exceeded.', $error->getMessage()); + $this->assertSame('Maximum header size of 8192 exceeded.', $error->getMessage()); $this->assertSame($parser, $passedParser); $this->assertSame(0, count($parser->listeners('headers'))); $this->assertSame(0, count($parser->listeners('error'))); @@ -162,7 +162,7 @@ public function testHeaderOverflowShouldNotEmitErrorWhenDataExceedsMaxHeaderSize }); $data = $this->createAdvancedPostRequest(); - $body = str_repeat('A', 4097 - strlen($data)); + $body = str_repeat('A', 8193 - strlen($data)); $data .= $body; $parser->feed($data); diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 3a1b9362..2c42f654 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -1324,7 +1324,7 @@ function ($data) use (&$buffer) { $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\nX-DATA: "; - $data .= str_repeat('A', 4097 - strlen($data)) . "\r\n\r\n"; + $data .= str_repeat('A', 8193 - strlen($data)) . "\r\n\r\n"; $this->connection->emit('data', array($data)); $this->assertInstanceOf('OverflowException', $error); From 19fd073532e291d64ec7268cb4df373c864df37d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 21 Nov 2017 16:41:59 +0100 Subject: [PATCH 209/456] Add file upload example and documentation --- README.md | 99 ++++++++++++++++++------------- examples/12-upload.php | 132 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 189 insertions(+), 42 deletions(-) create mode 100644 examples/12-upload.php diff --git a/README.md b/README.md index 0286f3e3..2ad17a46 100644 --- a/README.md +++ b/README.md @@ -722,56 +722,71 @@ $middlewares = new MiddlewareRunner([ #### RequestBodyParserMiddleware -The `RequestBodyParserMiddleware` takes a fully buffered request body (generally from [`RequestBodyBufferMiddleware`](#requestbodybuffermiddleware)), -and parses the forms and uploaded files from the request body. - -Parsed submitted forms will be available from `$request->getParsedBody()` as -array. -For example the following submitted body (`application/x-www-form-urlencoded`): - -`bar[]=beer&bar[]=wine` - -Results in the following parsed body: +The `RequestBodyParserMiddleware` takes a fully buffered request body +(generally from [`RequestBodyBufferMiddleware`](#requestbodybuffermiddleware)), +and parses the form values and file uploads from the incoming HTTP request body. + +This middleware handler takes care of applying values from HTTP +requests that use `Content-Type: application/x-www-form-urlencoded` or +`Content-Type: multipart/form-data` to resemble PHP's default superglobals +`$_POST` and `$_FILES`. +Instead of relying on these superglobals, you can use the +`$request->getParsedBody()` and `$request->getUploadedFiles()` methods +as defined by PSR-7. + +Accordingly, each file upload will be represented as instance implementing [`UploadedFileInterface`](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-7-http-message.md#36-psrhttpmessageuploadedfileinterface). +Due to its blocking nature, the `moveTo()` method is not available and throws +a `RuntimeException` instead. +You can use `$contents = (string)$file->getStream();` to access the file +contents and persist this to your favorite data store. ```php -$parsedBody = [ - 'bar' => [ - 'beer', - 'wine', - ], -]; -``` - -Aside from `application/x-www-form-urlencoded`, this middleware handler -also supports `multipart/form-data`, thus supporting uploaded files available -through `$request->getUploadedFiles()`. - -The `$request->getUploadedFiles(): array` will return an array with all -uploaded files formatted like this: - -```php -$uploadedFiles = [ - 'avatar' => new UploadedFile(/**...**/), - 'screenshots' => [ - new UploadedFile(/**...**/), - new UploadedFile(/**...**/), - ], -]; -``` +$handler = function (ServerRequestInterface $request) { + // If any, parsed form fields are now available from $request->getParsedBody() + $body = $request->getParsedBody(); + $name = isset($body['name']) ? $body['name'] : 'unnamed'; + + $files = $request->getUploadedFiles(); + $avatar = isset($files['avatar']) ? $files['avatar'] : null; + if ($avatar instanceof UploadedFileInterface) { + if ($avatar->getError() === UPLOAD_ERR_OK) { + $uploaded = $avatar->getSize() . ' bytes'; + } else { + $uploaded = 'with error'; + } + } else { + $uploaded = 'nothing'; + } -Usage: + return new Response( + 200, + array( + 'Content-Type' => 'text/plain' + ), + $name . ' uploaded ' . $uploaded + ); +}; -```php -$middlewares = new MiddlewareRunner([ +$server = new Server(new MiddlewareRunner([ new RequestBodyBufferMiddleware(16 * 1024 * 1024), // 16 MiB new RequestBodyParserMiddleware(), - function (ServerRequestInterface $request, callable $next) { - // If any, parsed form fields are now available from $request->getParsedBody() - return new Response(200); - }, -]); + $handler +])); ``` +See also [example #12](examples) for more details. + +> Note that this middleware handler simply parses everything that is already + buffered in the request body. + It is imperative that the request body is buffered by a prior middleware + handler as given in the example above. + This previous middleware handler is also responsible for rejecting incoming + requests that exceed allowed message sizes (such as big file uploads). + If you use this middleware without buffering first, it will try to parse an + empty (streaming) body and may thus assume an empty data structure. + See also [`RequestBodyBufferMiddleware`](#requestbodybuffermiddleware) for + more details. + #### Third-Party Middleware A non-exhaustive list of third-party middleware can be found at the [`Middleware`](https://github.com/reactphp/http/wiki/Middleware) wiki page. diff --git a/examples/12-upload.php b/examples/12-upload.php new file mode 100644 index 00000000..d723261f --- /dev/null +++ b/examples/12-upload.php @@ -0,0 +1,132 @@ +getMethod() === 'POST') { + // Take form input values from POST values (for illustration purposes only!) + // Does not actually validate data here + $body = $request->getParsedBody(); + $name = isset($body['name']) && is_string($body['name']) ? htmlspecialchars($body['name']) : 'n/a'; + $age = isset($body['age']) && is_string($body['age']) ? (int)$body['age'] : 'n/a'; + + // Show uploaded avatar as image (for illustration purposes only!) + // Real applications should validate the file data to ensure this is + // actually an image and not rely on the client media type. + $avatar = 'n/a'; + $uploads = $request->getUploadedFiles(); + if (isset($uploads['avatar']) && $uploads['avatar'] instanceof UploadedFileInterface) { + /* @var $file UploadedFileInterface */ + $file = $uploads['avatar']; + if ($file->getError() === UPLOAD_ERR_OK) { + // Note that moveFile() is not available due to its blocking nature. + // You can use your favorite data store to simply dump the file + // contents via `(string)$file->getStream()` instead. + // Here, we simply use an inline image to send back to client: + $avatar = ' (' . $file->getSize() . ' bytes)'; + } else { + // Real applications should probably check the error number and + // should print some human-friendly text + $avatar = 'upload error ' . $file->getError(); + } + } + + $dump = htmlspecialchars( + var_export($request->getParsedBody(), true) . + PHP_EOL . + var_export($request->getUploadedFiles(), true) + ); + + $body = << +$dump + +BODY; + } else { + $body = << + + + + + + + + +BODY; + } + + $html = << + + + + +$body + + + +HTML; + + return new Response( + 200, + array('Content-Type' => 'text/html; charset=UTF-8'), + $html + ); +}; + +// buffer and parse HTTP request body before running our request handler +$server = new Server(new MiddlewareRunner(array( + new RequestBodyBufferMiddleware(100000), // 100 KB max + new RequestBodyParserMiddleware(), + $handler +))); + +$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); +$server->listen($socket); + +echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; + +$loop->run(); From fe0e1da70944dea8b0937594fa3c8369cef5b40c Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Tue, 21 Nov 2017 22:48:50 +0100 Subject: [PATCH 210/456] Respect MAX_FILE_SIZE POST field in MultipartParser --- README.md | 2 + src/Io/MultipartParser.php | 41 +++++++++++--- tests/Io/MultipartParserTest.php | 93 ++++++++++++++++++++++++++++++++ 3 files changed, 130 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 2ad17a46..c8d25c93 100644 --- a/README.md +++ b/README.md @@ -786,6 +786,8 @@ See also [example #12](examples) for more details. empty (streaming) body and may thus assume an empty data structure. See also [`RequestBodyBufferMiddleware`](#requestbodybuffermiddleware) for more details. + +> PHP's `MAX_FILE_SIZE` hidden field is respected by this middleware. #### Third-Party Middleware diff --git a/src/Io/MultipartParser.php b/src/Io/MultipartParser.php index 211f084d..f70052f9 100644 --- a/src/Io/MultipartParser.php +++ b/src/Io/MultipartParser.php @@ -40,6 +40,11 @@ final class MultipartParser */ protected $onDataCallable; + /** + * @var int|null + */ + protected $maxFileSize; + public static function parseRequest(ServerRequestInterface $request) { $parser = new self($request); @@ -134,14 +139,30 @@ private function parseFile($headers, $body) $this->request = $this->request->withUploadedFiles($this->extractPost( $this->request->getUploadedFiles(), $this->getFieldFromHeader($headers['content-disposition'], 'name'), - new UploadedFile( - Psr7\stream_for($body), - strlen($body), - UPLOAD_ERR_OK, + $this->parseUploadedFile($headers, $body) + )); + } + + private function parseUploadedFile($headers, $body) + { + $bodyLength = strlen($body); + if ($this->maxFileSize !== null && $bodyLength > $this->maxFileSize) { + return new UploadedFile( + Psr7\stream_for(''), + $bodyLength, + UPLOAD_ERR_FORM_SIZE, $this->getFieldFromHeader($headers['content-disposition'], 'filename'), $headers['content-type'][0] - ) - )); + ); + } + + return new UploadedFile( + Psr7\stream_for($body), + $bodyLength, + UPLOAD_ERR_OK, + $this->getFieldFromHeader($headers['content-disposition'], 'filename'), + $headers['content-type'][0] + ); } private function parsePost($headers, $body) @@ -154,6 +175,14 @@ private function parsePost($headers, $body) $matches[1], $body )); + + if (strtoupper($matches[1]) === 'MAX_FILE_SIZE') { + $this->maxFileSize = (int)$body; + + if ($this->maxFileSize === 0) { + $this->maxFileSize = null; + } + } } } } diff --git a/tests/Io/MultipartParserTest.php b/tests/Io/MultipartParserTest.php index 63c2e8eb..72ab8e8f 100644 --- a/tests/Io/MultipartParserTest.php +++ b/tests/Io/MultipartParserTest.php @@ -48,6 +48,10 @@ public function testFileUpload() $file = base64_decode("R0lGODlhAQABAIAAAP///wAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw=="); $data = "--$boundary\r\n"; + $data .= "Content-Disposition: form-data; name=\"MAX_FILE_SIZE\"\r\n"; + $data .= "\r\n"; + $data .= "12000\r\n"; + $data .= "--$boundary\r\n"; $data .= "Content-Disposition: form-data; name=\"users[one]\"\r\n"; $data .= "\r\n"; $data .= "single\r\n"; @@ -106,6 +110,7 @@ public function testFileUpload() $this->assertSame( array( + 'MAX_FILE_SIZE' => '12000', 'users' => array( 'one' => 'single', 'two' => 'second', @@ -139,4 +144,92 @@ public function testFileUpload() $this->assertSame('text/php', $files['files'][2]->getClientMediaType()); $this->assertSame("getStream()); } + + public function testPostMaxFileSize() + { + $boundary = "---------------------------12758086162038677464950549563"; + + $data = "--$boundary\r\n"; + $data .= "Content-Disposition: form-data; name=\"MAX_FILE_SIZE\"\r\n"; + $data .= "\r\n"; + $data .= "12\r\n"; + $data .= "--$boundary\r\n"; + $data .= "Content-Disposition: form-data; name=\"file\"; filename=\"User.php\"\r\n"; + $data .= "Content-type: text/php\r\n"; + $data .= "\r\n"; + $data .= " 'multipart/form-data', + ), $data, 1.1); + + $parsedRequest = MultipartParser::parseRequest($request); + + $files = $parsedRequest->getUploadedFiles(); + + $this->assertTrue(isset($files['file'])); + $this->assertSame(UPLOAD_ERR_FORM_SIZE, $files['file']->getError()); + } + + public function testPostMaxFileSizeIgnoredByFilesComingBeforeIt() + { + $boundary = "---------------------------12758086162038677464950549563"; + + $data = "--$boundary\r\n"; + $data .= "Content-Disposition: form-data; name=\"file\"; filename=\"User-one.php\"\r\n"; + $data .= "Content-type: text/php\r\n"; + $data .= "\r\n"; + $data .= " 'multipart/form-data', + ), $data, 1.1); + + $parsedRequest = MultipartParser::parseRequest($request); + + $files = $parsedRequest->getUploadedFiles(); + + $this->assertTrue(isset($files['file'])); + $this->assertSame(UPLOAD_ERR_OK, $files['file']->getError()); + $this->assertTrue(isset($files['file2'])); + $this->assertSame(UPLOAD_ERR_OK, $files['file2']->getError()); + $this->assertTrue(isset($files['file3'])); + $this->assertSame(UPLOAD_ERR_FORM_SIZE, $files['file3']->getError()); + $this->assertTrue(isset($files['file4'])); + $this->assertSame(UPLOAD_ERR_OK, $files['file4']->getError()); + } } \ No newline at end of file From 628a1217f7552d126fec1c8ca43136220a49df68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Wed, 22 Nov 2017 21:56:27 +0100 Subject: [PATCH 211/456] Report UPLOAD_ERR_NO_FILE if no file is selected for upload --- src/Io/MultipartParser.php | 18 ++++- tests/Io/MultipartParserTest.php | 121 +++++++++++++++++++++++++++++++ 2 files changed, 137 insertions(+), 2 deletions(-) diff --git a/src/Io/MultipartParser.php b/src/Io/MultipartParser.php index f70052f9..8fd965b5 100644 --- a/src/Io/MultipartParser.php +++ b/src/Io/MultipartParser.php @@ -145,13 +145,27 @@ private function parseFile($headers, $body) private function parseUploadedFile($headers, $body) { + $filename = $this->getFieldFromHeader($headers['content-disposition'], 'filename'); $bodyLength = strlen($body); + + // no file selected (zero size and empty filename) + if ($bodyLength === 0 && $filename === '') { + return new UploadedFile( + Psr7\stream_for(''), + $bodyLength, + UPLOAD_ERR_NO_FILE, + $filename, + $headers['content-type'][0] + ); + } + + // file exceeds MAX_FILE_SIZE value if ($this->maxFileSize !== null && $bodyLength > $this->maxFileSize) { return new UploadedFile( Psr7\stream_for(''), $bodyLength, UPLOAD_ERR_FORM_SIZE, - $this->getFieldFromHeader($headers['content-disposition'], 'filename'), + $filename, $headers['content-type'][0] ); } @@ -160,7 +174,7 @@ private function parseUploadedFile($headers, $body) Psr7\stream_for($body), $bodyLength, UPLOAD_ERR_OK, - $this->getFieldFromHeader($headers['content-disposition'], 'filename'), + $filename, $headers['content-type'][0] ); } diff --git a/tests/Io/MultipartParserTest.php b/tests/Io/MultipartParserTest.php index 72ab8e8f..acd10792 100644 --- a/tests/Io/MultipartParserTest.php +++ b/tests/Io/MultipartParserTest.php @@ -41,6 +41,58 @@ public function testPostKey() ); } + public function testEmptyPostValue() + { + $boundary = "---------------------------5844729766471062541057622570"; + + $data = "--$boundary\r\n"; + $data .= "Content-Disposition: form-data; name=\"key\"\r\n"; + $data .= "\r\n"; + $data .= "\r\n"; + $data .= "--$boundary--\r\n"; + + + $request = new ServerRequest('POST', 'http://example.com/', array( + 'Content-Type' => 'multipart/mixed; boundary=' . $boundary, + ), $data, 1.1); + + $parsedRequest = MultipartParser::parseRequest($request); + + $this->assertEmpty($parsedRequest->getUploadedFiles()); + $this->assertSame( + array( + 'key' => '' + ), + $parsedRequest->getParsedBody() + ); + } + + public function testEmptyPostKey() + { + $boundary = "---------------------------5844729766471062541057622570"; + + $data = "--$boundary\r\n"; + $data .= "Content-Disposition: form-data; name=\"\"\r\n"; + $data .= "\r\n"; + $data .= "value\r\n"; + $data .= "--$boundary--\r\n"; + + + $request = new ServerRequest('POST', 'http://example.com/', array( + 'Content-Type' => 'multipart/mixed; boundary=' . $boundary, + ), $data, 1.1); + + $parsedRequest = MultipartParser::parseRequest($request); + + $this->assertEmpty($parsedRequest->getUploadedFiles()); + $this->assertSame( + array( + '' => 'value' + ), + $parsedRequest->getParsedBody() + ); + } + public function testFileUpload() { $boundary = "---------------------------12758086162038677464950549563"; @@ -145,6 +197,75 @@ public function testFileUpload() $this->assertSame("getStream()); } + public function testUploadEmptyFile() + { + $boundary = "---------------------------12758086162038677464950549563"; + + $file = base64_decode("R0lGODlhAQABAIAAAP///wAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw=="); + + $data = "--$boundary\r\n"; + $data .= "Content-Disposition: form-data; name=\"file\"; filename=\"empty\"\r\n"; + $data .= "Content-type: text/plain\r\n"; + $data .= "\r\n"; + $data .= "\r\n"; + $data .= "--$boundary--\r\n"; + + $request = new ServerRequest('POST', 'http://example.com/', array( + 'Content-Type' => 'multipart/form-data', + ), $data, 1.1); + + $parsedRequest = MultipartParser::parseRequest($request); + + $files = $parsedRequest->getUploadedFiles(); + + $this->assertCount(1, $files); + $this->assertTrue(isset($files['file'])); + $this->assertInstanceOf('Psr\Http\Message\UploadedFileInterface', $files['file']); + + /* @var $file \Psr\Http\Message\UploadedFileInterface */ + $file = $files['file']; + + $this->assertSame('empty', $file->getClientFilename()); + $this->assertSame('text/plain', $file->getClientMediaType()); + $this->assertSame(0, $file->getSize()); + $this->assertSame(UPLOAD_ERR_OK, $file->getError()); + $this->assertSame('', (string)$file->getStream()); + } + + public function testUploadNoFile() + { + $boundary = "---------------------------12758086162038677464950549563"; + + $file = base64_decode("R0lGODlhAQABAIAAAP///wAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw=="); + + $data = "--$boundary\r\n"; + $data .= "Content-Disposition: form-data; name=\"file\"; filename=\"\"\r\n"; + $data .= "Content-type: application/octet-stream\r\n"; + $data .= "\r\n"; + $data .= "\r\n"; + $data .= "--$boundary--\r\n"; + + $request = new ServerRequest('POST', 'http://example.com/', array( + 'Content-Type' => 'multipart/form-data', + ), $data, 1.1); + + $parsedRequest = MultipartParser::parseRequest($request); + + $files = $parsedRequest->getUploadedFiles(); + + $this->assertCount(1, $files); + $this->assertTrue(isset($files['file'])); + $this->assertInstanceOf('Psr\Http\Message\UploadedFileInterface', $files['file']); + + /* @var $file \Psr\Http\Message\UploadedFileInterface */ + $file = $files['file']; + + $this->assertSame('', $file->getClientFilename()); + $this->assertSame('application/octet-stream', $file->getClientMediaType()); + $this->assertSame(0, $file->getSize()); + $this->assertSame(UPLOAD_ERR_NO_FILE, $file->getError()); + } + public function testPostMaxFileSize() { $boundary = "---------------------------12758086162038677464950549563"; From d205a8e30736ab1098a78681f49c561e72f809e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 24 Nov 2017 17:21:33 +0100 Subject: [PATCH 212/456] Ignore multipart chunks without name --- src/Io/MultipartParser.php | 40 ++++++++--------------------- tests/Io/MultipartParserTest.php | 44 +++++++++++++++++++++++++++++--- 2 files changed, 51 insertions(+), 33 deletions(-) diff --git a/src/Io/MultipartParser.php b/src/Io/MultipartParser.php index 8fd965b5..826ae4fc 100644 --- a/src/Io/MultipartParser.php +++ b/src/Io/MultipartParser.php @@ -116,36 +116,29 @@ private function parseChunk($chunk) return; } - if ($this->headerStartsWith($headers['content-disposition'], 'filename')) { - $this->parseFile($headers, $body); + if (!$this->headerContainsParameter($headers['content-disposition'], 'name')) { return; } - if ($this->headerStartsWith($headers['content-disposition'], 'name')) { + if ($this->headerContainsParameter($headers['content-disposition'], 'filename')) { + $this->parseFile($headers, $body); + } else { $this->parsePost($headers, $body); - return; } } private function parseFile($headers, $body) { - if ( - !$this->headerContains($headers['content-disposition'], 'name=') || - !$this->headerContains($headers['content-disposition'], 'filename=') - ) { - return; - } - $this->request = $this->request->withUploadedFiles($this->extractPost( $this->request->getUploadedFiles(), - $this->getFieldFromHeader($headers['content-disposition'], 'name'), + $this->getParameterFromHeader($headers['content-disposition'], 'name'), $this->parseUploadedFile($headers, $body) )); } private function parseUploadedFile($headers, $body) { - $filename = $this->getFieldFromHeader($headers['content-disposition'], 'filename'); + $filename = $this->getParameterFromHeader($headers['content-disposition'], 'filename'); $bodyLength = strlen($body); // no file selected (zero size and empty filename) @@ -217,21 +210,10 @@ private function parseHeaders($header) return $headers; } - private function headerStartsWith(array $header, $needle) - { - foreach ($header as $part) { - if (strpos($part, $needle) === 0) { - return true; - } - } - - return false; - } - - private function headerContains(array $header, $needle) + private function headerContainsParameter(array $header, $parameter) { foreach ($header as $part) { - if (strpos($part, $needle) !== false) { + if (strpos($part, $parameter . '=') === 0) { return true; } } @@ -239,11 +221,11 @@ private function headerContains(array $header, $needle) return false; } - private function getFieldFromHeader(array $header, $field) + private function getParameterFromHeader(array $header, $parameter) { foreach ($header as $part) { - if (strpos($part, $field) === 0) { - preg_match('/' . $field . '="?(.*)"$/', $part, $matches); + if (strpos($part, $parameter) === 0) { + preg_match('/' . $parameter . '="?(.*)"$/', $part, $matches); return $matches[1]; } } diff --git a/tests/Io/MultipartParserTest.php b/tests/Io/MultipartParserTest.php index acd10792..c47a6b1b 100644 --- a/tests/Io/MultipartParserTest.php +++ b/tests/Io/MultipartParserTest.php @@ -197,12 +197,50 @@ public function testFileUpload() $this->assertSame("getStream()); } + public function testInvalidContentDispositionMissingWillBeIgnored() + { + $boundary = "---------------------------5844729766471062541057622570"; + + $data = "--$boundary\r\n"; + $data .= "Content-Type: text/plain\r\n"; + $data .= "\r\n"; + $data .= "hello"; + $data .= "--$boundary--\r\n"; + + $request = new ServerRequest('POST', 'http://example.com/', array( + 'Content-Type' => 'multipart/mixed; boundary=' . $boundary, + ), $data, 1.1); + + $parsedRequest = MultipartParser::parseRequest($request); + + $this->assertEmpty($parsedRequest->getUploadedFiles()); + $this->assertEmpty($parsedRequest->getParsedBody()); + } + + public function testInvalidContentDispositionWithoutNameWillBeIgnored() + { + $boundary = "---------------------------5844729766471062541057622570"; + + $data = "--$boundary\r\n"; + $data .= "Content-Disposition: form-data; something=\"key\"\r\n"; + $data .= "\r\n"; + $data .= "value"; + $data .= "--$boundary--\r\n"; + + $request = new ServerRequest('POST', 'http://example.com/', array( + 'Content-Type' => 'multipart/mixed; boundary=' . $boundary, + ), $data, 1.1); + + $parsedRequest = MultipartParser::parseRequest($request); + + $this->assertEmpty($parsedRequest->getUploadedFiles()); + $this->assertEmpty($parsedRequest->getParsedBody()); + } + public function testUploadEmptyFile() { $boundary = "---------------------------12758086162038677464950549563"; - $file = base64_decode("R0lGODlhAQABAIAAAP///wAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw=="); - $data = "--$boundary\r\n"; $data .= "Content-Disposition: form-data; name=\"file\"; filename=\"empty\"\r\n"; $data .= "Content-type: text/plain\r\n"; @@ -236,8 +274,6 @@ public function testUploadNoFile() { $boundary = "---------------------------12758086162038677464950549563"; - $file = base64_decode("R0lGODlhAQABAIAAAP///wAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw=="); - $data = "--$boundary\r\n"; $data .= "Content-Disposition: form-data; name=\"file\"; filename=\"\"\r\n"; $data .= "Content-type: application/octet-stream\r\n"; From 8496516b4c0d71a5b55a5d619cde49da65406762 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 25 Nov 2017 11:41:16 +0100 Subject: [PATCH 213/456] Fix parsing deeply nested POST structures --- src/Io/MultipartParser.php | 26 +++++++------- tests/Io/MultipartParserTest.php | 60 ++++++++++++++++++++++++++++++-- 2 files changed, 70 insertions(+), 16 deletions(-) diff --git a/src/Io/MultipartParser.php b/src/Io/MultipartParser.php index 826ae4fc..b28410a9 100644 --- a/src/Io/MultipartParser.php +++ b/src/Io/MultipartParser.php @@ -250,29 +250,27 @@ private function extractPost($postFields, $key, $value) return $postFields; } - $chunkKey = $chunks[0]; - if (!isset($postFields[$chunkKey])) { - $postFields[$chunkKey] = array(); - } - + $chunkKey = rtrim($chunks[0], ']'); $parent = &$postFields; for ($i = 1; $i < count($chunks); $i++) { $previousChunkKey = $chunkKey; - if (!isset($parent[$previousChunkKey])) { + if ($previousChunkKey !== '' && !isset($parent[$previousChunkKey])) { $parent[$previousChunkKey] = array(); } - $parent = &$parent[$previousChunkKey]; - $chunkKey = $chunks[$i]; - if ($chunkKey == ']') { - $parent[] = $value; - return $postFields; + if ($previousChunkKey === '') { + $parent = &$parent[0]; + } else { + $parent = &$parent[$previousChunkKey]; } - $chunkKey = rtrim($chunkKey, ']'); + $chunkKey = rtrim($chunks[$i], ']'); if ($i == count($chunks) - 1) { - $parent[$chunkKey] = $value; - return $postFields; + if ($chunkKey === '') { + $parent[] = $value; + } else { + $parent[$chunkKey] = $value; + } } } diff --git a/tests/Io/MultipartParserTest.php b/tests/Io/MultipartParserTest.php index c47a6b1b..9daf6e74 100644 --- a/tests/Io/MultipartParserTest.php +++ b/tests/Io/MultipartParserTest.php @@ -51,7 +51,6 @@ public function testEmptyPostValue() $data .= "\r\n"; $data .= "--$boundary--\r\n"; - $request = new ServerRequest('POST', 'http://example.com/', array( 'Content-Type' => 'multipart/mixed; boundary=' . $boundary, ), $data, 1.1); @@ -77,7 +76,6 @@ public function testEmptyPostKey() $data .= "value\r\n"; $data .= "--$boundary--\r\n"; - $request = new ServerRequest('POST', 'http://example.com/', array( 'Content-Type' => 'multipart/mixed; boundary=' . $boundary, ), $data, 1.1); @@ -93,6 +91,64 @@ public function testEmptyPostKey() ); } + public function testNestedPostKeyAssoc() + { + $boundary = "---------------------------5844729766471062541057622570"; + + $data = "--$boundary\r\n"; + $data .= "Content-Disposition: form-data; name=\"a[b][c]\"\r\n"; + $data .= "\r\n"; + $data .= "value\r\n"; + $data .= "--$boundary--\r\n"; + + $request = new ServerRequest('POST', 'http://example.com/', array( + 'Content-Type' => 'multipart/mixed; boundary=' . $boundary, + ), $data, 1.1); + + $parsedRequest = MultipartParser::parseRequest($request); + + $this->assertEmpty($parsedRequest->getUploadedFiles()); + $this->assertSame( + array( + 'a' => array( + 'b' => array( + 'c' => 'value' + ) + ) + ), + $parsedRequest->getParsedBody() + ); + } + + public function testNestedPostKeyVector() + { + $boundary = "---------------------------5844729766471062541057622570"; + + $data = "--$boundary\r\n"; + $data .= "Content-Disposition: form-data; name=\"a[][]\"\r\n"; + $data .= "\r\n"; + $data .= "value\r\n"; + $data .= "--$boundary--\r\n"; + + $request = new ServerRequest('POST', 'http://example.com/', array( + 'Content-Type' => 'multipart/mixed; boundary=' . $boundary, + ), $data, 1.1); + + $parsedRequest = MultipartParser::parseRequest($request); + + $this->assertEmpty($parsedRequest->getUploadedFiles()); + $this->assertSame( + array( + 'a' => array( + array( + 'value' + ) + ) + ), + $parsedRequest->getParsedBody() + ); + } + public function testFileUpload() { $boundary = "---------------------------12758086162038677464950549563"; From e708ab4077800c333788ac4ef8e94f37b5f933a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 25 Nov 2017 14:27:54 +0100 Subject: [PATCH 214/456] Fix overwriting existing nested array structures --- src/Io/MultipartParser.php | 25 +++--- tests/Io/MultipartParserTest.php | 128 ++++++++++++++++++++++++++++++- 2 files changed, 140 insertions(+), 13 deletions(-) diff --git a/src/Io/MultipartParser.php b/src/Io/MultipartParser.php index b28410a9..b6f3c947 100644 --- a/src/Io/MultipartParser.php +++ b/src/Io/MultipartParser.php @@ -252,26 +252,27 @@ private function extractPost($postFields, $key, $value) $chunkKey = rtrim($chunks[0], ']'); $parent = &$postFields; - for ($i = 1; $i < count($chunks); $i++) { + for ($i = 1; isset($chunks[$i]); $i++) { $previousChunkKey = $chunkKey; - if ($previousChunkKey !== '' && !isset($parent[$previousChunkKey])) { - $parent[$previousChunkKey] = array(); - } if ($previousChunkKey === '') { - $parent = &$parent[0]; + $parent[] = array(); + end($parent); + $parent = &$parent[key($parent)]; } else { + if (!isset($parent[$previousChunkKey]) || !is_array($parent[$previousChunkKey])) { + $parent[$previousChunkKey] = array(); + } $parent = &$parent[$previousChunkKey]; } $chunkKey = rtrim($chunks[$i], ']'); - if ($i == count($chunks) - 1) { - if ($chunkKey === '') { - $parent[] = $value; - } else { - $parent[$chunkKey] = $value; - } - } + } + + if ($chunkKey === '') { + $parent[] = $value; + } else { + $parent[$chunkKey] = $value; } return $postFields; diff --git a/tests/Io/MultipartParserTest.php b/tests/Io/MultipartParserTest.php index 9daf6e74..9e6a227e 100644 --- a/tests/Io/MultipartParserTest.php +++ b/tests/Io/MultipartParserTest.php @@ -22,7 +22,6 @@ public function testPostKey() $data .= "second\r\n"; $data .= "--$boundary--\r\n"; - $request = new ServerRequest('POST', 'http://example.com/', array( 'Content-Type' => 'multipart/mixed; boundary=' . $boundary, ), $data, 1.1); @@ -41,6 +40,133 @@ public function testPostKey() ); } + public function testPostStringOverwritesMap() + { + $boundary = "---------------------------5844729766471062541057622570"; + + $data = "--$boundary\r\n"; + $data .= "Content-Disposition: form-data; name=\"users[one]\"\r\n"; + $data .= "\r\n"; + $data .= "ignored\r\n"; + $data .= "--$boundary\r\n"; + $data .= "Content-Disposition: form-data; name=\"users\"\r\n"; + $data .= "\r\n"; + $data .= "2\r\n"; + $data .= "--$boundary--\r\n"; + + $request = new ServerRequest('POST', 'http://example.com/', array( + 'Content-Type' => 'multipart/mixed; boundary=' . $boundary, + ), $data, 1.1); + + $parsedRequest = MultipartParser::parseRequest($request); + + $this->assertEmpty($parsedRequest->getUploadedFiles()); + $this->assertSame( + array( + 'users' => '2' + ), + $parsedRequest->getParsedBody() + ); + } + + public function testPostMapOverwritesString() + { + $boundary = "---------------------------5844729766471062541057622570"; + + $data = "--$boundary\r\n"; + $data .= "Content-Disposition: form-data; name=\"users\"\r\n"; + $data .= "\r\n"; + $data .= "ignored\r\n"; + $data .= "--$boundary\r\n"; + $data .= "Content-Disposition: form-data; name=\"users[two]\"\r\n"; + $data .= "\r\n"; + $data .= "2\r\n"; + $data .= "--$boundary--\r\n"; + + $request = new ServerRequest('POST', 'http://example.com/', array( + 'Content-Type' => 'multipart/mixed; boundary=' . $boundary, + ), $data, 1.1); + + $parsedRequest = MultipartParser::parseRequest($request); + + $this->assertEmpty($parsedRequest->getUploadedFiles()); + $this->assertSame( + array( + 'users' => array( + 'two' => '2', + ), + ), + $parsedRequest->getParsedBody() + ); + } + + public function testPostVectorOverwritesString() + { + $boundary = "---------------------------5844729766471062541057622570"; + + $data = "--$boundary\r\n"; + $data .= "Content-Disposition: form-data; name=\"users\"\r\n"; + $data .= "\r\n"; + $data .= "ignored\r\n"; + $data .= "--$boundary\r\n"; + $data .= "Content-Disposition: form-data; name=\"users[]\"\r\n"; + $data .= "\r\n"; + $data .= "2\r\n"; + $data .= "--$boundary--\r\n"; + + $request = new ServerRequest('POST', 'http://example.com/', array( + 'Content-Type' => 'multipart/mixed; boundary=' . $boundary, + ), $data, 1.1); + + $parsedRequest = MultipartParser::parseRequest($request); + + $this->assertEmpty($parsedRequest->getUploadedFiles()); + $this->assertSame( + array( + 'users' => array( + '2', + ), + ), + $parsedRequest->getParsedBody() + ); + } + + public function testPostDeeplyNestedArray() + { + $boundary = "---------------------------5844729766471062541057622570"; + + $data = "--$boundary\r\n"; + $data .= "Content-Disposition: form-data; name=\"users[][]\"\r\n"; + $data .= "\r\n"; + $data .= "1\r\n"; + $data .= "--$boundary\r\n"; + $data .= "Content-Disposition: form-data; name=\"users[][]\"\r\n"; + $data .= "\r\n"; + $data .= "2\r\n"; + $data .= "--$boundary--\r\n"; + + $request = new ServerRequest('POST', 'http://example.com/', array( + 'Content-Type' => 'multipart/mixed; boundary=' . $boundary, + ), $data, 1.1); + + $parsedRequest = MultipartParser::parseRequest($request); + + $this->assertEmpty($parsedRequest->getUploadedFiles()); + $this->assertSame( + array( + 'users' => array( + array( + '1' + ), + array( + '2' + ) + ), + ), + $parsedRequest->getParsedBody() + ); + } + public function testEmptyPostValue() { $boundary = "---------------------------5844729766471062541057622570"; From 1a76f2b095188e40563ada5342c10b3796663249 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Sat, 25 Nov 2017 14:19:46 +0100 Subject: [PATCH 215/456] Renamed Server to StreamingServer --- README.md | 58 ++--- examples/01-hello-world.php | 4 +- examples/02-count-visitors.php | 4 +- examples/03-client-ip.php | 4 +- examples/04-query-parameter.php | 4 +- examples/05-cookie-handling.php | 4 +- examples/06-sleep.php | 4 +- examples/07-error-handling.php | 4 +- examples/08-stream-response.php | 4 +- examples/09-stream-request.php | 4 +- examples/11-hello-world-https.php | 4 +- examples/12-upload.php | 4 +- examples/21-http-proxy.php | 4 +- examples/22-connect-proxy.php | 4 +- examples/31-upgrade-echo.php | 4 +- examples/32-upgrade-chat.php | 4 +- examples/99-benchmark-download.php | 4 +- src/{Server.php => StreamingServer.php} | 8 +- tests/FunctionalServerTest.php | 44 ++-- ...ServerTest.php => StreamingServerTest.php} | 214 +++++++++--------- 20 files changed, 194 insertions(+), 194 deletions(-) rename src/{Server.php => StreamingServer.php} (98%) rename tests/{ServerTest.php => StreamingServerTest.php} (90%) diff --git a/README.md b/README.md index c8d25c93..0fe15df2 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Event-driven, streaming plaintext HTTP and secure HTTPS server for [ReactPHP](ht * [Quickstart example](#quickstart-example) * [Usage](#usage) - * [Server](#server) + * [StreamingServer](#streamingserver) * [Request](#request) * [Response](#response) * [Middleware](#middleware) @@ -27,7 +27,7 @@ This is an HTTP server which responds with `Hello World` to every request. ```php $loop = React\EventLoop\Factory::create(); -$server = new Server(function (ServerRequestInterface $request) { +$server = new StreamingServer(function (ServerRequestInterface $request) { return new Response( 200, array('Content-Type' => 'text/plain'), @@ -45,9 +45,9 @@ See also the [examples](examples). ## Usage -### Server +### StreamingServer -The `Server` class is responsible for handling incoming connections and then +The `StreamingServer` class is responsible for handling incoming connections and then processing each incoming HTTP request. For each request, it executes the callback function passed to the @@ -55,7 +55,7 @@ constructor with the respective [request](#request) object and expects a respective [response](#response) object in return. ```php -$server = new Server(function (ServerRequestInterface $request) { +$server = new StreamingServer(function (ServerRequestInterface $request) { return new Response( 200, array('Content-Type' => 'text/plain'), @@ -73,7 +73,7 @@ You can attach this to a in order to start a plaintext HTTP server like this: ```php -$server = new Server($handler); +$server = new StreamingServer($handler); $socket = new React\Socket\Server(8080, $loop); $server->listen($socket); @@ -86,7 +86,7 @@ Similarly, you can also attach this to a in order to start a secure HTTPS server like this: ```php -$server = new Server($handler); +$server = new StreamingServer($handler); $socket = new React\Socket\Server(8080, $loop); $socket = new React\Socket\SecureServer($socket, $loop, array( @@ -110,7 +110,7 @@ examples above. See also [request](#request) and [response](#response) for more details (e.g. the request data body). -The `Server` supports both HTTP/1.1 and HTTP/1.0 request messages. +The `StreamingServer` supports both HTTP/1.1 and HTTP/1.0 request messages. If a client sends an invalid request message, uses an invalid HTTP protocol version or sends an invalid `Transfer-Encoding` in the request header, it will emit an `error` event, send an HTTP error response to the client and @@ -125,7 +125,7 @@ $server->on('error', function (Exception $e) { The server will also emit an `error` event if you return an invalid type in the callback function or have a unhandled `Exception` or `Throwable`. If your callback function throws an `Exception` or `Throwable`, -the `Server` will emit a `RuntimeException` and add the thrown exception +the `StreamingServer` will emit a `RuntimeException` and add the thrown exception as previous: ```php @@ -143,7 +143,7 @@ Check out [request](#request) for more details. ### Request -An seen above, the `Server` class is responsible for handling incoming +An seen above, the `StreamingServer` class is responsible for handling incoming connections and then processing each incoming HTTP request. The request object will be processed once the request headers have @@ -155,7 +155,7 @@ which in turn extends the and will be passed to the callback function like this. ```php -$server = new Server(function (ServerRequestInterface $request) { +$server = new StreamingServer(function (ServerRequestInterface $request) { $body = "The method of the request is: " . $request->getMethod(); $body .= "The requested path is: " . $request->getUri()->getPath(); @@ -189,7 +189,7 @@ The following parameters are currently available: Set to 'on' if the request used HTTPS, otherwise it won't be set ```php -$server = new Server(function (ServerRequestInterface $request) { +$server = new StreamingServer(function (ServerRequestInterface $request) { $body = "Your IP is: " . $request->getServerParams()['REMOTE_ADDR']; return new Response( @@ -206,7 +206,7 @@ The `getQueryParams(): array` method can be used to get the query parameters similiar to the `$_GET` variable. ```php -$server = new Server(function (ServerRequestInterface $request) { +$server = new StreamingServer(function (ServerRequestInterface $request) { $queryParams = $request->getQueryParams(); $body = 'The query parameter "foo" is not set. Click the following link '; @@ -269,7 +269,7 @@ The `ReactPHP ReadableStreamInterface` gives you access to the incoming request body as the individual chunks arrive: ```php -$server = new Server(function (ServerRequestInterface $request) { +$server = new StreamingServer(function (ServerRequestInterface $request) { return new Promise(function ($resolve, $reject) use ($request) { $contentLength = 0; $request->getBody()->on('data', function ($data) use (&$contentLength) { @@ -333,7 +333,7 @@ Note that this value may be `null` if the request body size is unknown in advance because the request message uses chunked transfer encoding. ```php -$server = new Server(function (ServerRequestInterface $request) { +$server = new StreamingServer(function (ServerRequestInterface $request) { $size = $request->getBody()->getSize(); if ($size === null) { $body = 'The request does not contain an explicit length.'; @@ -387,7 +387,7 @@ The `getCookieParams(): string[]` method can be used to get all cookies sent with the current request. ```php -$server = new Server(function (ServerRequestInterface $request) { +$server = new StreamingServer(function (ServerRequestInterface $request) { $key = 'react\php'; if (isset($request->getCookieParams()[$key])) { @@ -422,7 +422,7 @@ See also [example #5](examples) for more details. ### Response -The callback function passed to the constructor of the [Server](#server) +The callback function passed to the constructor of the [StreamingServer](#server) is responsible for processing the request and returning a response, which will be delivered to the client. This function MUST return an instance implementing @@ -438,7 +438,7 @@ but feel free to use any implemantation of the `PSR-7 ResponseInterface` you prefer. ```php -$server = new Server(function (ServerRequestInterface $request) { +$server = new StreamingServer(function (ServerRequestInterface $request) { return new Response( 200, array('Content-Type' => 'text/plain'), @@ -457,7 +457,7 @@ To prevent this you SHOULD use a This example shows how such a long-term action could look like: ```php -$server = new Server(function (ServerRequestInterface $request) use ($loop) { +$server = new StreamingServer(function (ServerRequestInterface $request) use ($loop) { return new Promise(function ($resolve, $reject) use ($request, $loop) { $loop->addTimer(1.5, function() use ($loop, $resolve) { $response = new Response( @@ -490,7 +490,7 @@ Note that other implementations of the `PSR-7 ResponseInterface` likely only support strings. ```php -$server = new Server(function (ServerRequestInterface $request) use ($loop) { +$server = new StreamingServer(function (ServerRequestInterface $request) use ($loop) { $stream = new ThroughStream(); $timer = $loop->addPeriodicTimer(0.5, function () use ($stream) { @@ -533,7 +533,7 @@ If you know the length of your stream body, you MAY specify it like this instead ```php $stream = new ThroughStream() -$server = new Server(function (ServerRequestInterface $request) use ($stream) { +$server = new StreamingServer(function (ServerRequestInterface $request) use ($stream) { return new Response( 200, array( @@ -549,8 +549,8 @@ An invalid return value or an unhandled `Exception` or `Throwable` in the code of the callback function, will result in an `500 Internal Server Error` message. Make sure to catch `Exceptions` or `Throwables` to create own response messages. -After the return in the callback function the response will be processed by the `Server`. -The `Server` will add the protocol version of the request, so you don't have to. +After the return in the callback function the response will be processed by the `StreamingServer`. +The `StreamingServer` will add the protocol version of the request, so you don't have to. Any response to a `HEAD` request and any response with a `1xx` (Informational), `204` (No Content) or `304` (Not Modified) status code will *not* include a @@ -618,7 +618,7 @@ A `Date` header will be automatically added with the system date and time if non You can add a custom `Date` header yourself like this: ```php -$server = new Server(function (ServerRequestInterface $request) { +$server = new StreamingServer(function (ServerRequestInterface $request) { return new Response(200, array('Date' => date('D, d M Y H:i:s T'))); }); ``` @@ -627,7 +627,7 @@ If you don't have a appropriate clock to rely on, you should unset this header with an empty string: ```php -$server = new Server(function (ServerRequestInterface $request) { +$server = new StreamingServer(function (ServerRequestInterface $request) { return new Response(200, array('Date' => '')); }); ``` @@ -636,7 +636,7 @@ Note that it will automatically assume a `X-Powered-By: react/alpha` header unless your specify a custom `X-Powered-By` header yourself: ```php -$server = new Server(function (ServerRequestInterface $request) { +$server = new StreamingServer(function (ServerRequestInterface $request) { return new Response(200, array('X-Powered-By' => 'PHP 3')); }); ``` @@ -645,7 +645,7 @@ If you do not want to send this header at all, you can use an empty string as value like this: ```php -$server = new Server(function (ServerRequestInterface $request) { +$server = new StreamingServer(function (ServerRequestInterface $request) { return new Response(200, array('X-Powered-By' => '')); }); ``` @@ -670,7 +670,7 @@ The following example adds a middleware that adds the current time to the reques header (`Request-Time`) and middleware that always returns a 200 code without a body: ```php -$server = new Server(new MiddlewareRunner([ +$server = new StreamingServer(new MiddlewareRunner([ function (ServerRequestInterface $request, callable $next) { $request = $request->withHeader('Request-Time', time()); return $next($request); @@ -767,7 +767,7 @@ $handler = function (ServerRequestInterface $request) { ); }; -$server = new Server(new MiddlewareRunner([ +$server = new StreamingServer(new MiddlewareRunner([ new RequestBodyBufferMiddleware(16 * 1024 * 1024), // 16 MiB new RequestBodyParserMiddleware(), $handler diff --git a/examples/01-hello-world.php b/examples/01-hello-world.php index f703a5d7..34126c0f 100644 --- a/examples/01-hello-world.php +++ b/examples/01-hello-world.php @@ -3,13 +3,13 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Factory; use React\Http\Response; -use React\Http\Server; +use React\Http\StreamingServer; require __DIR__ . '/../vendor/autoload.php'; $loop = Factory::create(); -$server = new Server(function (ServerRequestInterface $request) { +$server = new StreamingServer(function (ServerRequestInterface $request) { return new Response( 200, array( diff --git a/examples/02-count-visitors.php b/examples/02-count-visitors.php index 5a225110..ee8e2d22 100644 --- a/examples/02-count-visitors.php +++ b/examples/02-count-visitors.php @@ -3,14 +3,14 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Factory; use React\Http\Response; -use React\Http\Server; +use React\Http\StreamingServer; require __DIR__ . '/../vendor/autoload.php'; $loop = Factory::create(); $counter = 0; -$server = new Server(function (ServerRequestInterface $request) use (&$counter) { +$server = new StreamingServer(function (ServerRequestInterface $request) use (&$counter) { return new Response( 200, array('Content-Type' => 'text/plain'), diff --git a/examples/03-client-ip.php b/examples/03-client-ip.php index 3fbcabfd..eca6e4d6 100644 --- a/examples/03-client-ip.php +++ b/examples/03-client-ip.php @@ -3,13 +3,13 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Factory; use React\Http\Response; -use React\Http\Server; +use React\Http\StreamingServer; require __DIR__ . '/../vendor/autoload.php'; $loop = Factory::create(); -$server = new Server(function (ServerRequestInterface $request) { +$server = new StreamingServer(function (ServerRequestInterface $request) { $body = "Your IP is: " . $request->getServerParams()['REMOTE_ADDR']; return new Response( diff --git a/examples/04-query-parameter.php b/examples/04-query-parameter.php index 3a60aae8..cc665d8a 100644 --- a/examples/04-query-parameter.php +++ b/examples/04-query-parameter.php @@ -3,13 +3,13 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Factory; use React\Http\Response; -use React\Http\Server; +use React\Http\StreamingServer; require __DIR__ . '/../vendor/autoload.php'; $loop = Factory::create(); -$server = new Server(function (ServerRequestInterface $request) { +$server = new StreamingServer(function (ServerRequestInterface $request) { $queryParams = $request->getQueryParams(); $body = 'The query parameter "foo" is not set. Click the following link '; diff --git a/examples/05-cookie-handling.php b/examples/05-cookie-handling.php index 5441adbe..539421c1 100644 --- a/examples/05-cookie-handling.php +++ b/examples/05-cookie-handling.php @@ -3,13 +3,13 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Factory; use React\Http\Response; -use React\Http\Server; +use React\Http\StreamingServer; require __DIR__ . '/../vendor/autoload.php'; $loop = Factory::create(); -$server = new Server(function (ServerRequestInterface $request) { +$server = new StreamingServer(function (ServerRequestInterface $request) { $key = 'react\php'; if (isset($request->getCookieParams()[$key])) { diff --git a/examples/06-sleep.php b/examples/06-sleep.php index 926aac10..b3acf9ff 100644 --- a/examples/06-sleep.php +++ b/examples/06-sleep.php @@ -3,14 +3,14 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Factory; use React\Http\Response; -use React\Http\Server; +use React\Http\StreamingServer; use React\Promise\Promise; require __DIR__ . '/../vendor/autoload.php'; $loop = Factory::create(); -$server = new Server(function (ServerRequestInterface $request) use ($loop) { +$server = new StreamingServer(function (ServerRequestInterface $request) use ($loop) { return new Promise(function ($resolve, $reject) use ($request, $loop) { $loop->addTimer(1.5, function() use ($loop, $resolve) { $response = new Response( diff --git a/examples/07-error-handling.php b/examples/07-error-handling.php index 5dbc6955..affd3ae4 100644 --- a/examples/07-error-handling.php +++ b/examples/07-error-handling.php @@ -3,7 +3,7 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Factory; use React\Http\Response; -use React\Http\Server; +use React\Http\StreamingServer; use React\Promise\Promise; require __DIR__ . '/../vendor/autoload.php'; @@ -11,7 +11,7 @@ $loop = Factory::create(); $count = 0; -$server = new Server(function (ServerRequestInterface $request) use (&$count) { +$server = new StreamingServer(function (ServerRequestInterface $request) use (&$count) { return new Promise(function ($resolve, $reject) use (&$count) { $count++; diff --git a/examples/08-stream-response.php b/examples/08-stream-response.php index 399e3a77..aec8b23e 100644 --- a/examples/08-stream-response.php +++ b/examples/08-stream-response.php @@ -3,14 +3,14 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Factory; use React\Http\Response; -use React\Http\Server; +use React\Http\StreamingServer; use React\Stream\ThroughStream; require __DIR__ . '/../vendor/autoload.php'; $loop = Factory::create(); -$server = new Server($loop,function (ServerRequestInterface $request) use ($loop) { +$server = new StreamingServer($loop,function (ServerRequestInterface $request) use ($loop) { if ($request->getMethod() !== 'GET' || $request->getUri()->getPath() !== '/') { return new Response(404); } diff --git a/examples/09-stream-request.php b/examples/09-stream-request.php index bcf5456b..ed900ead 100644 --- a/examples/09-stream-request.php +++ b/examples/09-stream-request.php @@ -3,14 +3,14 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Factory; use React\Http\Response; -use React\Http\Server; +use React\Http\StreamingServer; use React\Promise\Promise; require __DIR__ . '/../vendor/autoload.php'; $loop = Factory::create(); -$server = new Server(function (ServerRequestInterface $request) { +$server = new StreamingServer(function (ServerRequestInterface $request) { return new Promise(function ($resolve, $reject) use ($request) { $contentLength = 0; $request->getBody()->on('data', function ($data) use (&$contentLength) { diff --git a/examples/11-hello-world-https.php b/examples/11-hello-world-https.php index 6610c3e0..da21bdb4 100644 --- a/examples/11-hello-world-https.php +++ b/examples/11-hello-world-https.php @@ -3,13 +3,13 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Factory; use React\Http\Response; -use React\Http\Server; +use React\Http\StreamingServer; require __DIR__ . '/../vendor/autoload.php'; $loop = Factory::create(); -$server = new Server(function (ServerRequestInterface $request) { +$server = new StreamingServer(function (ServerRequestInterface $request) { return new Response( 200, array('Content-Type' => 'text/plain'), diff --git a/examples/12-upload.php b/examples/12-upload.php index d723261f..68c810ff 100644 --- a/examples/12-upload.php +++ b/examples/12-upload.php @@ -14,7 +14,7 @@ use React\Http\Middleware\RequestBodyBufferMiddleware; use React\Http\Middleware\RequestBodyParserMiddleware; use React\Http\Response; -use React\Http\Server; +use React\Http\StreamingServer; require __DIR__ . '/../vendor/autoload.php'; @@ -118,7 +118,7 @@ }; // buffer and parse HTTP request body before running our request handler -$server = new Server(new MiddlewareRunner(array( +$server = new StreamingServer(new MiddlewareRunner(array( new RequestBodyBufferMiddleware(100000), // 100 KB max new RequestBodyParserMiddleware(), $handler diff --git a/examples/21-http-proxy.php b/examples/21-http-proxy.php index 250cbf7a..0e390ff1 100644 --- a/examples/21-http-proxy.php +++ b/examples/21-http-proxy.php @@ -3,14 +3,14 @@ use Psr\Http\Message\RequestInterface; use React\EventLoop\Factory; use React\Http\Response; -use React\Http\Server; +use React\Http\StreamingServer; use RingCentral\Psr7; require __DIR__ . '/../vendor/autoload.php'; $loop = Factory::create(); -$server = new Server(function (RequestInterface $request) { +$server = new StreamingServer(function (RequestInterface $request) { if (strpos($request->getRequestTarget(), '://') === false) { return new Response( 400, diff --git a/examples/22-connect-proxy.php b/examples/22-connect-proxy.php index ed8e80b0..b5ded9a7 100644 --- a/examples/22-connect-proxy.php +++ b/examples/22-connect-proxy.php @@ -3,7 +3,7 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Factory; use React\Http\Response; -use React\Http\Server; +use React\Http\StreamingServer; use React\Socket\Connector; use React\Socket\ConnectionInterface; @@ -12,7 +12,7 @@ $loop = Factory::create(); $connector = new Connector($loop); -$server = new Server(function (ServerRequestInterface $request) use ($connector) { +$server = new StreamingServer(function (ServerRequestInterface $request) use ($connector) { if ($request->getMethod() !== 'CONNECT') { return new Response( 405, diff --git a/examples/31-upgrade-echo.php b/examples/31-upgrade-echo.php index b098ef03..74634f97 100644 --- a/examples/31-upgrade-echo.php +++ b/examples/31-upgrade-echo.php @@ -20,14 +20,14 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Factory; use React\Http\Response; -use React\Http\Server; +use React\Http\StreamingServer; use React\Stream\ThroughStream; require __DIR__ . '/../vendor/autoload.php'; $loop = Factory::create(); -$server = new Server(function (ServerRequestInterface $request) use ($loop) { +$server = new StreamingServer(function (ServerRequestInterface $request) use ($loop) { if ($request->getHeaderLine('Upgrade') !== 'echo' || $request->getProtocolVersion() === '1.0') { return new Response(426, array('Upgrade' => 'echo'), '"Upgrade: echo" required'); } diff --git a/examples/32-upgrade-chat.php b/examples/32-upgrade-chat.php index 58150265..7db38250 100644 --- a/examples/32-upgrade-chat.php +++ b/examples/32-upgrade-chat.php @@ -22,7 +22,7 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Factory; use React\Http\Response; -use React\Http\Server; +use React\Http\StreamingServer; use React\Stream\CompositeStream; use React\Stream\ThroughStream; @@ -35,7 +35,7 @@ // this means that any Upgraded data will simply be sent back to the client $chat = new ThroughStream(); -$server = new Server(function (ServerRequestInterface $request) use ($loop, $chat) { +$server = new StreamingServer(function (ServerRequestInterface $request) use ($loop, $chat) { if ($request->getHeaderLine('Upgrade') !== 'chat' || $request->getProtocolVersion() === '1.0') { return new Response(426, array('Upgrade' => 'chat'), '"Upgrade: chat" required'); } diff --git a/examples/99-benchmark-download.php b/examples/99-benchmark-download.php index a8a6e03a..079a78c1 100644 --- a/examples/99-benchmark-download.php +++ b/examples/99-benchmark-download.php @@ -10,7 +10,7 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Factory; use React\Http\Response; -use React\Http\Server; +use React\Http\StreamingServer; use React\Stream\ReadableStreamInterface; use React\Stream\WritableStreamInterface; @@ -86,7 +86,7 @@ public function getSize() } } -$server = new Server(function (ServerRequestInterface $request) use ($loop) { +$server = new StreamingServer(function (ServerRequestInterface $request) use ($loop) { switch ($request->getUri()->getPath()) { case '/': return new Response( diff --git a/src/Server.php b/src/StreamingServer.php similarity index 98% rename from src/Server.php rename to src/StreamingServer.php index 93e5af9f..b6285006 100644 --- a/src/Server.php +++ b/src/StreamingServer.php @@ -30,7 +30,7 @@ * a respective [response](#response) object in return. * * ```php - * $server = new Server(function (ServerRequestInterface $request) { + * $server = new StreamingServer(function (ServerRequestInterface $request) { * return new Response( * 200, * array('Content-Type' => 'text/plain'), @@ -81,7 +81,7 @@ * @see Response * @see self::listen() */ -class Server extends EventEmitter +class StreamingServer extends EventEmitter { private $callback; @@ -120,7 +120,7 @@ public function __construct($callback) * in order to start a plaintext HTTP server like this: * * ```php - * $server = new Server($handler); + * $server = new StreamingServer($handler); * * $socket = new React\Socket\Server(8080, $loop); * $server->listen($socket); @@ -133,7 +133,7 @@ public function __construct($callback) * in order to start a secure HTTPS server like this: * * ```php - * $server = new Server($handler); + * $server = new StreamingServer($handler); * * $socket = new React\Socket\Server(8080, $loop); * $socket = new React\Socket\SecureServer($socket, $loop, array( diff --git a/tests/FunctionalServerTest.php b/tests/FunctionalServerTest.php index b4c3fa2e..d7b9d420 100644 --- a/tests/FunctionalServerTest.php +++ b/tests/FunctionalServerTest.php @@ -5,7 +5,7 @@ use React\Http\MiddlewareRunner; use React\Socket\Server as Socket; use React\EventLoop\Factory; -use React\Http\Server; +use React\Http\StreamingServer; use Psr\Http\Message\RequestInterface; use React\Socket\Connector; use React\Socket\ConnectionInterface; @@ -23,7 +23,7 @@ public function testPlainHttpOnRandomPort() $loop = Factory::create(); $connector = new Connector($loop); - $server = new Server(function (RequestInterface $request) { + $server = new StreamingServer(function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); @@ -49,7 +49,7 @@ public function testPlainHttpOnRandomPortWithMiddlewareRunner() $loop = Factory::create(); $connector = new Connector($loop); - $server = new Server(new MiddlewareRunner(array(function (RequestInterface $request) { + $server = new StreamingServer(new MiddlewareRunner(array(function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }))); @@ -75,7 +75,7 @@ public function testPlainHttpOnRandomPortWithEmptyMiddlewareRunner() $loop = Factory::create(); $connector = new Connector($loop); - $server = new Server(new MiddlewareRunner(array( + $server = new StreamingServer(new MiddlewareRunner(array( function () { return new Response(404); }, @@ -102,7 +102,7 @@ public function testPlainHttpOnRandomPortWithoutHostHeaderUsesSocketUri() $loop = Factory::create(); $connector = new Connector($loop); - $server = new Server(function (RequestInterface $request) { + $server = new StreamingServer(function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); @@ -128,7 +128,7 @@ public function testPlainHttpOnRandomPortWithOtherHostHeaderTakesPrecedence() $loop = Factory::create(); $connector = new Connector($loop); - $server = new Server(function (RequestInterface $request) { + $server = new StreamingServer(function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); @@ -160,7 +160,7 @@ public function testSecureHttpsOnRandomPort() 'tls' => array('verify_peer' => false) )); - $server = new Server(function (RequestInterface $request) { + $server = new StreamingServer(function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); @@ -192,7 +192,7 @@ public function testSecureHttpsReturnsData() $loop = Factory::create(); - $server = new Server(function (RequestInterface $request) { + $server = new StreamingServer(function (RequestInterface $request) { return new Response( 200, array(), @@ -236,7 +236,7 @@ public function testSecureHttpsOnRandomPortWithoutHostHeaderUsesSocketUri() 'tls' => array('verify_peer' => false) )); - $server = new Server(function (RequestInterface $request) { + $server = new StreamingServer(function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); @@ -270,7 +270,7 @@ public function testPlainHttpOnStandardPortReturnsUriWithNoPort() } $connector = new Connector($loop); - $server = new Server(function (RequestInterface $request) { + $server = new StreamingServer(function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); @@ -300,7 +300,7 @@ public function testPlainHttpOnStandardPortWithoutHostHeaderReturnsUriWithNoPort } $connector = new Connector($loop); - $server = new Server(function (RequestInterface $request) { + $server = new StreamingServer(function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); @@ -339,7 +339,7 @@ public function testSecureHttpsOnStandardPortReturnsUriWithNoPort() 'tls' => array('verify_peer' => false) )); - $server = new Server(function (RequestInterface $request) { + $server = new StreamingServer(function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); @@ -378,7 +378,7 @@ public function testSecureHttpsOnStandardPortWithoutHostHeaderUsesSocketUri() 'tls' => array('verify_peer' => false) )); - $server = new Server(function (RequestInterface $request) { + $server = new StreamingServer(function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); @@ -408,7 +408,7 @@ public function testPlainHttpOnHttpsStandardPortReturnsUriWithPort() } $connector = new Connector($loop); - $server = new Server(function (RequestInterface $request) { + $server = new StreamingServer(function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); @@ -447,7 +447,7 @@ public function testSecureHttpsOnHttpStandardPortReturnsUriWithPort() 'tls' => array('verify_peer' => false) )); - $server = new Server(function (RequestInterface $request) { + $server = new StreamingServer(function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri() . 'x' . $request->getHeaderLine('Host')); }); @@ -475,7 +475,7 @@ public function testClosedStreamFromRequestHandlerWillSendEmptyBody() $stream = new ThroughStream(); $stream->close(); - $server = new Server(function (RequestInterface $request) use ($stream) { + $server = new StreamingServer(function (RequestInterface $request) use ($stream) { return new Response(200, array(), $stream); }); @@ -504,7 +504,7 @@ public function testStreamFromRequestHandlerWillBeClosedIfConnectionClosesWhileS $stream = new ThroughStream(); $stream->on('close', $this->expectCallableOnce()); - $server = new Server(function (RequestInterface $request) use ($stream) { + $server = new StreamingServer(function (RequestInterface $request) use ($stream) { return new Response(200, array(), $stream); }); @@ -537,7 +537,7 @@ public function testStreamFromRequestHandlerWillBeClosedIfConnectionClosesButWil $stream = new ThroughStream(); $stream->on('close', $this->expectCallableOnce()); - $server = new Server(function (RequestInterface $request) use ($stream) { + $server = new StreamingServer(function (RequestInterface $request) use ($stream) { return new Response(200, array(), $stream); }); @@ -572,7 +572,7 @@ public function testUpgradeWithThroughStreamReturnsDataAsGiven() $loop = Factory::create(); $connector = new Connector($loop); - $server = new Server(function (RequestInterface $request) use ($loop) { + $server = new StreamingServer(function (RequestInterface $request) use ($loop) { $stream = new ThroughStream(); $loop->addTimer(0.1, function () use ($stream) { @@ -609,7 +609,7 @@ public function testConnectWithThroughStreamReturnsDataAsGiven() $loop = Factory::create(); $connector = new Connector($loop); - $server = new Server(function (RequestInterface $request) use ($loop) { + $server = new StreamingServer(function (RequestInterface $request) use ($loop) { $stream = new ThroughStream(); $loop->addTimer(0.1, function () use ($stream) { @@ -646,7 +646,7 @@ public function testConnectWithThroughStreamReturnedFromPromiseReturnsDataAsGive $loop = Factory::create(); $connector = new Connector($loop); - $server = new Server(function (RequestInterface $request) use ($loop) { + $server = new StreamingServer(function (RequestInterface $request) use ($loop) { $stream = new ThroughStream(); $loop->addTimer(0.1, function () use ($stream) { @@ -687,7 +687,7 @@ public function testConnectWithClosedThroughStreamReturnsNoData() $loop = Factory::create(); $connector = new Connector($loop); - $server = new Server(function (RequestInterface $request) { + $server = new StreamingServer(function (RequestInterface $request) { $stream = new ThroughStream(); $stream->close(); diff --git a/tests/ServerTest.php b/tests/StreamingServerTest.php similarity index 90% rename from tests/ServerTest.php rename to tests/StreamingServerTest.php index 2c42f654..69868437 100644 --- a/tests/ServerTest.php +++ b/tests/StreamingServerTest.php @@ -3,13 +3,13 @@ namespace React\Tests\Http; use React\Http\MiddlewareRunner; -use React\Http\Server; +use React\Http\StreamingServer; use Psr\Http\Message\ServerRequestInterface; use React\Http\Response; use React\Stream\ThroughStream; use React\Promise\Promise; -class ServerTest extends TestCase +class StreamingServerTest extends TestCase { private $connection; private $socket; @@ -42,7 +42,7 @@ public function setUp() public function testRequestEventWillNotBeEmittedForIncompleteHeaders() { - $server = new Server($this->expectCallableNever()); + $server = new StreamingServer($this->expectCallableNever()); $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); @@ -54,7 +54,7 @@ public function testRequestEventWillNotBeEmittedForIncompleteHeaders() public function testRequestEventIsEmitted() { - $server = new Server($this->expectCallableOnce()); + $server = new StreamingServer($this->expectCallableOnce()); $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); @@ -67,7 +67,7 @@ public function testRequestEvent() { $i = 0; $requestAssertion = null; - $server = new Server(function (ServerRequestInterface $request) use (&$i, &$requestAssertion) { + $server = new StreamingServer(function (ServerRequestInterface $request) use (&$i, &$requestAssertion) { $i++; $requestAssertion = $request; }); @@ -100,7 +100,7 @@ public function testRequestEventWithMiddlewareRunner() { $i = 0; $requestAssertion = null; - $server = new Server(new MiddlewareRunner(array(function (ServerRequestInterface $request) use (&$i, &$requestAssertion) { + $server = new StreamingServer(new MiddlewareRunner(array(function (ServerRequestInterface $request) use (&$i, &$requestAssertion) { $i++; $requestAssertion = $request; }))); @@ -132,7 +132,7 @@ public function testRequestEventWithMiddlewareRunner() public function testRequestGetWithHostAndCustomPort() { $requestAssertion = null; - $server = new Server(function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new StreamingServer(function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; }); @@ -154,7 +154,7 @@ public function testRequestGetWithHostAndCustomPort() public function testRequestGetWithHostAndHttpsPort() { $requestAssertion = null; - $server = new Server(function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new StreamingServer(function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; }); @@ -176,7 +176,7 @@ public function testRequestGetWithHostAndHttpsPort() public function testRequestGetWithHostAndDefaultPortWillBeIgnored() { $requestAssertion = null; - $server = new Server(function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new StreamingServer(function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; }); @@ -198,7 +198,7 @@ public function testRequestGetWithHostAndDefaultPortWillBeIgnored() public function testRequestOptionsAsterisk() { $requestAssertion = null; - $server = new Server(function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new StreamingServer(function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; }); @@ -218,7 +218,7 @@ public function testRequestOptionsAsterisk() public function testRequestNonOptionsWithAsteriskRequestTargetWillReject() { - $server = new Server($this->expectCallableNever()); + $server = new StreamingServer($this->expectCallableNever()); $server->on('error', $this->expectCallableOnce()); $server->listen($this->socket); @@ -231,7 +231,7 @@ public function testRequestNonOptionsWithAsteriskRequestTargetWillReject() public function testRequestConnectAuthorityForm() { $requestAssertion = null; - $server = new Server(function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new StreamingServer(function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; }); @@ -253,7 +253,7 @@ public function testRequestConnectAuthorityForm() public function testRequestConnectWithoutHostWillBeAdded() { $requestAssertion = null; - $server = new Server(function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new StreamingServer(function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; }); @@ -275,7 +275,7 @@ public function testRequestConnectWithoutHostWillBeAdded() public function testRequestConnectAuthorityFormWithDefaultPortWillBeIgnored() { $requestAssertion = null; - $server = new Server(function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new StreamingServer(function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; }); @@ -297,7 +297,7 @@ public function testRequestConnectAuthorityFormWithDefaultPortWillBeIgnored() public function testRequestConnectAuthorityFormNonMatchingHostWillBeOverwritten() { $requestAssertion = null; - $server = new Server(function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new StreamingServer(function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; }); @@ -318,7 +318,7 @@ public function testRequestConnectAuthorityFormNonMatchingHostWillBeOverwritten( public function testRequestConnectOriginFormRequestTargetWillReject() { - $server = new Server($this->expectCallableNever()); + $server = new StreamingServer($this->expectCallableNever()); $server->on('error', $this->expectCallableOnce()); $server->listen($this->socket); @@ -330,7 +330,7 @@ public function testRequestConnectOriginFormRequestTargetWillReject() public function testRequestNonConnectWithAuthorityRequestTargetWillReject() { - $server = new Server($this->expectCallableNever()); + $server = new StreamingServer($this->expectCallableNever()); $server->on('error', $this->expectCallableOnce()); $server->listen($this->socket); @@ -344,7 +344,7 @@ public function testRequestWithoutHostEventUsesSocketAddress() { $requestAssertion = null; - $server = new Server(function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new StreamingServer(function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; }); @@ -370,7 +370,7 @@ public function testRequestAbsoluteEvent() { $requestAssertion = null; - $server = new Server(function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new StreamingServer(function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; }); @@ -392,7 +392,7 @@ public function testRequestAbsoluteAddsMissingHostEvent() { $requestAssertion = null; - $server = new Server(function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new StreamingServer(function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; }); @@ -414,7 +414,7 @@ public function testRequestAbsoluteNonMatchingHostWillBeOverwritten() { $requestAssertion = null; - $server = new Server(function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new StreamingServer(function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; }); @@ -436,7 +436,7 @@ public function testRequestOptionsAsteriskEvent() { $requestAssertion = null; - $server = new Server(function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new StreamingServer(function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; }); @@ -458,7 +458,7 @@ public function testRequestOptionsAbsoluteEvent() { $requestAssertion = null; - $server = new Server(function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new StreamingServer(function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; }); @@ -478,7 +478,7 @@ public function testRequestOptionsAbsoluteEvent() public function testRequestPauseWillbeForwardedToConnection() { - $server = new Server(function (ServerRequestInterface $request) { + $server = new StreamingServer(function (ServerRequestInterface $request) { $request->getBody()->pause(); }); @@ -498,7 +498,7 @@ public function testRequestPauseWillbeForwardedToConnection() public function testRequestResumeWillbeForwardedToConnection() { - $server = new Server(function (ServerRequestInterface $request) { + $server = new StreamingServer(function (ServerRequestInterface $request) { $request->getBody()->resume(); }); @@ -513,7 +513,7 @@ public function testRequestResumeWillbeForwardedToConnection() public function testRequestCloseWillPauseConnection() { - $server = new Server(function (ServerRequestInterface $request) { + $server = new StreamingServer(function (ServerRequestInterface $request) { $request->getBody()->close(); }); @@ -528,7 +528,7 @@ public function testRequestCloseWillPauseConnection() public function testRequestPauseAfterCloseWillNotBeForwarded() { - $server = new Server(function (ServerRequestInterface $request) { + $server = new StreamingServer(function (ServerRequestInterface $request) { $request->getBody()->close(); $request->getBody()->pause(); }); @@ -544,7 +544,7 @@ public function testRequestPauseAfterCloseWillNotBeForwarded() public function testRequestResumeAfterCloseWillNotBeForwarded() { - $server = new Server(function (ServerRequestInterface $request) { + $server = new StreamingServer(function (ServerRequestInterface $request) { $request->getBody()->close(); $request->getBody()->resume(); }); @@ -563,7 +563,7 @@ public function testRequestEventWithoutBodyWillNotEmitData() { $never = $this->expectCallableNever(); - $server = new Server(function (ServerRequestInterface $request) use ($never) { + $server = new StreamingServer(function (ServerRequestInterface $request) use ($never) { $request->getBody()->on('data', $never); }); @@ -578,7 +578,7 @@ public function testRequestEventWithSecondDataEventWillEmitBodyData() { $once = $this->expectCallableOnceWith('incomplete'); - $server = new Server(function (ServerRequestInterface $request) use ($once) { + $server = new StreamingServer(function (ServerRequestInterface $request) use ($once) { $request->getBody()->on('data', $once); }); @@ -598,7 +598,7 @@ public function testRequestEventWithPartialBodyWillEmitData() { $once = $this->expectCallableOnceWith('incomplete'); - $server = new Server(function (ServerRequestInterface $request) use ($once) { + $server = new StreamingServer(function (ServerRequestInterface $request) use ($once) { $request->getBody()->on('data', $once); }); @@ -619,7 +619,7 @@ public function testRequestEventWithPartialBodyWillEmitData() public function testResponseContainsPoweredByHeader() { - $server = new Server(function (ServerRequestInterface $request) { + $server = new StreamingServer(function (ServerRequestInterface $request) { return new Response(); }); @@ -649,7 +649,7 @@ public function testResponsePendingPromiseWillNotSendAnything() { $never = $this->expectCallableNever(); - $server = new Server(function (ServerRequestInterface $request) use ($never) { + $server = new StreamingServer(function (ServerRequestInterface $request) use ($never) { return new Promise(function () { }, $never); }); @@ -679,7 +679,7 @@ public function testResponsePendingPromiseWillBeCancelledIfConnectionCloses() { $once = $this->expectCallableOnce(); - $server = new Server(function (ServerRequestInterface $request) use ($once) { + $server = new StreamingServer(function (ServerRequestInterface $request) use ($once) { return new Promise(function () { }, $once); }); @@ -711,7 +711,7 @@ public function testRespomseBodyStreamAlreadyClosedWillSendEmptyBodyChunkedEncod $stream = new ThroughStream(); $stream->close(); - $server = new Server(function (ServerRequestInterface $request) use ($stream) { + $server = new StreamingServer(function (ServerRequestInterface $request) use ($stream) { return new Response( 200, array(), @@ -746,7 +746,7 @@ public function testResponseBodyStreamEndingWillSendEmptyBodyChunkedEncoded() { $stream = new ThroughStream(); - $server = new Server(function (ServerRequestInterface $request) use ($stream) { + $server = new StreamingServer(function (ServerRequestInterface $request) use ($stream) { return new Response( 200, array(), @@ -784,7 +784,7 @@ public function testResponseBodyStreamAlreadyClosedWillSendEmptyBodyPlainHttp10( $stream = new ThroughStream(); $stream->close(); - $server = new Server(function (ServerRequestInterface $request) use ($stream) { + $server = new StreamingServer(function (ServerRequestInterface $request) use ($stream) { return new Response( 200, array(), @@ -820,7 +820,7 @@ public function testResponseStreamWillBeClosedIfConnectionIsAlreadyClosed() $stream = new ThroughStream(); $stream->on('close', $this->expectCallableOnce()); - $server = new Server(function (ServerRequestInterface $request) use ($stream) { + $server = new StreamingServer(function (ServerRequestInterface $request) use ($stream) { return new Response( 200, array(), @@ -875,7 +875,7 @@ public function testResponseBodyStreamWillBeClosedIfConnectionEmitsCloseEvent() $stream = new ThroughStream(); $stream->on('close', $this->expectCallableOnce()); - $server = new Server(function (ServerRequestInterface $request) use ($stream) { + $server = new StreamingServer(function (ServerRequestInterface $request) use ($stream) { return new Response( 200, array(), @@ -893,7 +893,7 @@ public function testResponseBodyStreamWillBeClosedIfConnectionEmitsCloseEvent() public function testResponseUpgradeInResponseCanBeUsedToAdvertisePossibleUpgrade() { - $server = new Server(function (ServerRequestInterface $request) { + $server = new StreamingServer(function (ServerRequestInterface $request) { return new Response( 200, array( @@ -929,7 +929,7 @@ function ($data) use (&$buffer) { public function testResponseUpgradeWishInRequestCanBeIgnoredByReturningNormalResponse() { - $server = new Server(function (ServerRequestInterface $request) { + $server = new StreamingServer(function (ServerRequestInterface $request) { return new Response( 200, array( @@ -964,7 +964,7 @@ function ($data) use (&$buffer) { public function testResponseUpgradeSwitchingProtocolIncludesConnectionUpgradeHeaderWithoutContentLength() { - $server = new Server(function (ServerRequestInterface $request) { + $server = new StreamingServer(function (ServerRequestInterface $request) { return new Response( 101, array( @@ -1004,7 +1004,7 @@ public function testResponseUpgradeSwitchingProtocolWithStreamWillPipeDataToConn { $stream = new ThroughStream(); - $server = new Server(function (ServerRequestInterface $request) use ($stream) { + $server = new StreamingServer(function (ServerRequestInterface $request) use ($stream) { return new Response( 101, array( @@ -1045,7 +1045,7 @@ public function testResponseConnectMethodStreamWillPipeDataToConnection() { $stream = new ThroughStream(); - $server = new Server(function (ServerRequestInterface $request) use ($stream) { + $server = new StreamingServer(function (ServerRequestInterface $request) use ($stream) { return new Response( 200, array(), @@ -1083,7 +1083,7 @@ public function testResponseConnectMethodStreamWillPipeDataFromConnection() { $stream = new ThroughStream(); - $server = new Server(function (ServerRequestInterface $request) use ($stream) { + $server = new StreamingServer(function (ServerRequestInterface $request) use ($stream) { return new Response( 200, array(), @@ -1102,7 +1102,7 @@ public function testResponseConnectMethodStreamWillPipeDataFromConnection() public function testResponseContainsSameRequestProtocolVersionAndChunkedBodyForHttp11() { - $server = new Server(function (ServerRequestInterface $request) { + $server = new StreamingServer(function (ServerRequestInterface $request) { return new Response( 200, array(), @@ -1135,7 +1135,7 @@ function ($data) use (&$buffer) { public function testResponseContainsSameRequestProtocolVersionAndRawBodyForHttp10() { - $server = new Server(function (ServerRequestInterface $request) { + $server = new StreamingServer(function (ServerRequestInterface $request) { return new Response( 200, array(), @@ -1169,7 +1169,7 @@ function ($data) use (&$buffer) { public function testResponseContainsNoResponseBodyForHeadRequest() { - $server = new Server(function (ServerRequestInterface $request) { + $server = new StreamingServer(function (ServerRequestInterface $request) { return new Response( 200, array(), @@ -1201,7 +1201,7 @@ function ($data) use (&$buffer) { public function testResponseContainsNoResponseBodyAndNoContentLengthForNoContentStatus() { - $server = new Server(function (ServerRequestInterface $request) { + $server = new StreamingServer(function (ServerRequestInterface $request) { return new Response( 204, array(), @@ -1234,7 +1234,7 @@ function ($data) use (&$buffer) { public function testResponseContainsNoResponseBodyForNotModifiedStatus() { - $server = new Server(function (ServerRequestInterface $request) { + $server = new StreamingServer(function (ServerRequestInterface $request) { return new Response( 304, array(), @@ -1268,7 +1268,7 @@ function ($data) use (&$buffer) { public function testRequestInvalidHttpProtocolVersionWillEmitErrorAndSendErrorResponse() { $error = null; - $server = new Server($this->expectCallableNever()); + $server = new StreamingServer($this->expectCallableNever()); $server->on('error', function ($message) use (&$error) { $error = $message; }); @@ -1302,7 +1302,7 @@ function ($data) use (&$buffer) { public function testRequestOverflowWillEmitErrorAndSendErrorResponse() { $error = null; - $server = new Server($this->expectCallableNever()); + $server = new StreamingServer($this->expectCallableNever()); $server->on('error', function ($message) use (&$error) { $error = $message; }); @@ -1336,7 +1336,7 @@ function ($data) use (&$buffer) { public function testRequestInvalidWillEmitErrorAndSendErrorResponse() { $error = null; - $server = new Server($this->expectCallableNever()); + $server = new StreamingServer($this->expectCallableNever()); $server->on('error', function ($message) use (&$error) { $error = $message; }); @@ -1373,7 +1373,7 @@ public function testRequestContentLengthBodyDataWillEmitDataEventOnRequestStream $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new Server(function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new StreamingServer(function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1401,7 +1401,7 @@ public function testRequestChunkedTransferEncodingRequestWillEmitDecodedDataEven $errorEvent = $this->expectCallableNever(); $requestValidation = null; - $server = new Server(function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent, &$requestValidation) { + $server = new StreamingServer(function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent, &$requestValidation) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1432,7 +1432,7 @@ public function testRequestChunkedTransferEncodingWithAdditionalDataWontBeEmitte $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new Server(function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new StreamingServer(function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1461,7 +1461,7 @@ public function testRequestChunkedTransferEncodingEmpty() $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new Server(function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new StreamingServer(function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1488,7 +1488,7 @@ public function testRequestChunkedTransferEncodingHeaderCanBeUpperCase() $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new Server(function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new StreamingServer(function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1516,7 +1516,7 @@ public function testRequestChunkedTransferEncodingCanBeMixedUpperAndLowerCase() $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new Server(function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new StreamingServer(function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1539,7 +1539,7 @@ public function testRequestChunkedTransferEncodingCanBeMixedUpperAndLowerCase() public function testRequestWithMalformedHostWillEmitErrorAndSendErrorResponse() { $error = null; - $server = new Server($this->expectCallableNever()); + $server = new StreamingServer($this->expectCallableNever()); $server->on('error', function ($message) use (&$error) { $error = $message; }); @@ -1572,7 +1572,7 @@ function ($data) use (&$buffer) { public function testRequestWithInvalidHostUriComponentsWillEmitErrorAndSendErrorResponse() { $error = null; - $server = new Server($this->expectCallableNever()); + $server = new StreamingServer($this->expectCallableNever()); $server->on('error', function ($message) use (&$error) { $error = $message; }); @@ -1609,7 +1609,7 @@ public function testRequestContentLengthWillEmitDataEventAndEndEventAndAdditiona $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new Server(function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new StreamingServer(function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1640,7 +1640,7 @@ public function testRequestContentLengthWillEmitDataEventAndEndEventAndAdditiona $errorEvent = $this->expectCallableNever(); - $server = new Server(function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new StreamingServer(function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1672,7 +1672,7 @@ public function testRequestZeroContentLengthWillEmitEndEvent() $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new Server(function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new StreamingServer(function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1698,7 +1698,7 @@ public function testRequestZeroContentLengthWillEmitEndAndAdditionalDataWillBeIg $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new Server(function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new StreamingServer(function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1725,7 +1725,7 @@ public function testRequestZeroContentLengthWillEmitEndAndAdditionalDataWillBeIg $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new Server(function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new StreamingServer(function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1756,7 +1756,7 @@ public function testRequestContentLengthWillBeIgnoredIfTransferEncodingIsSet() $errorEvent = $this->expectCallableNever(); $requestValidation = null; - $server = new Server(function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent, &$requestValidation) { + $server = new StreamingServer(function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent, &$requestValidation) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1793,7 +1793,7 @@ public function testRequestInvalidContentLengthWillBeIgnoreddIfTransferEncodingI $errorEvent = $this->expectCallableNever(); $requestValidation = null; - $server = new Server(function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent, &$requestValidation) { + $server = new StreamingServer(function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent, &$requestValidation) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1826,7 +1826,7 @@ public function testRequestInvalidContentLengthWillBeIgnoreddIfTransferEncodingI public function testRequestInvalidNonIntegerContentLengthWillEmitServerErrorAndSendResponse() { $error = null; - $server = new Server($this->expectCallableNever()); + $server = new StreamingServer($this->expectCallableNever()); $server->on('error', function ($message) use (&$error) { $error = $message; }); @@ -1863,7 +1863,7 @@ function ($data) use (&$buffer) { public function testRequestInvalidHeadRequestWithInvalidNonIntegerContentLengthWillEmitServerErrorAndSendResponseWithoutBody() { $error = null; - $server = new Server($this->expectCallableNever()); + $server = new StreamingServer($this->expectCallableNever()); $server->on('error', function ($message) use (&$error) { $error = $message; }); @@ -1900,7 +1900,7 @@ function ($data) use (&$buffer) { public function testRequestInvalidMultipleContentLengthWillEmitErrorOnServer() { $error = null; - $server = new Server($this->expectCallableNever()); + $server = new StreamingServer($this->expectCallableNever()); $server->on('error', function ($message) use (&$error) { $error = $message; }); @@ -1937,7 +1937,7 @@ function ($data) use (&$buffer) { public function testRequestInvalidChunkHeaderTooLongWillEmitErrorOnRequestStream() { $errorEvent = $this->expectCallableOnceWith($this->isInstanceOf('Exception')); - $server = new Server(function ($request) use ($errorEvent){ + $server = new StreamingServer(function ($request) use ($errorEvent){ $request->getBody()->on('error', $errorEvent); return \React\Promise\resolve(new Response()); }); @@ -1963,7 +1963,7 @@ public function testRequestInvalidChunkHeaderTooLongWillEmitErrorOnRequestStream public function testRequestInvalidChunkBodyTooLongWillEmitErrorOnRequestStream() { $errorEvent = $this->expectCallableOnceWith($this->isInstanceOf('Exception')); - $server = new Server(function ($request) use ($errorEvent){ + $server = new StreamingServer(function ($request) use ($errorEvent){ $request->getBody()->on('error', $errorEvent); }); @@ -1986,7 +1986,7 @@ public function testRequestInvalidChunkBodyTooLongWillEmitErrorOnRequestStream() public function testRequestUnexpectedEndOfRequestWithChunkedTransferConnectionWillEmitErrorOnRequestStream() { $errorEvent = $this->expectCallableOnceWith($this->isInstanceOf('Exception')); - $server = new Server(function ($request) use ($errorEvent){ + $server = new StreamingServer(function ($request) use ($errorEvent){ $request->getBody()->on('error', $errorEvent); }); @@ -2010,7 +2010,7 @@ public function testRequestUnexpectedEndOfRequestWithChunkedTransferConnectionWi public function testRequestInvalidChunkHeaderWillEmitErrorOnRequestStream() { $errorEvent = $this->expectCallableOnceWith($this->isInstanceOf('Exception')); - $server = new Server(function ($request) use ($errorEvent){ + $server = new StreamingServer(function ($request) use ($errorEvent){ $request->getBody()->on('error', $errorEvent); }); @@ -2033,7 +2033,7 @@ public function testRequestInvalidChunkHeaderWillEmitErrorOnRequestStream() public function testRequestUnexpectedEndOfRequestWithContentLengthWillEmitErrorOnRequestStream() { $errorEvent = $this->expectCallableOnceWith($this->isInstanceOf('Exception')); - $server = new Server(function ($request) use ($errorEvent){ + $server = new StreamingServer(function ($request) use ($errorEvent){ $request->getBody()->on('error', $errorEvent); }); @@ -2061,7 +2061,7 @@ public function testRequestWithoutBodyWillEmitEndOnRequestStream() $endEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new Server(function ($request) use ($dataEvent, $closeEvent, $endEvent, $errorEvent){ + $server = new StreamingServer(function ($request) use ($dataEvent, $closeEvent, $endEvent, $errorEvent){ $request->getBody()->on('data', $dataEvent); $request->getBody()->on('close', $closeEvent); $request->getBody()->on('end', $endEvent); @@ -2086,7 +2086,7 @@ public function testRequestWithoutDefinedLengthWillIgnoreDataEvent() $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new Server(function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new StreamingServer(function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -2105,7 +2105,7 @@ public function testRequestWithoutDefinedLengthWillIgnoreDataEvent() public function testResponseWithBodyStreamWillUseChunkedTransferEncodingByDefault() { $stream = new ThroughStream(); - $server = new Server(function (ServerRequestInterface $request) use ($stream) { + $server = new StreamingServer(function (ServerRequestInterface $request) use ($stream) { return new Response( 200, array(), @@ -2139,7 +2139,7 @@ function ($data) use (&$buffer) { public function testResponseWithBodyStringWillOverwriteExplicitContentLengthAndTransferEncoding() { - $server = new Server(function (ServerRequestInterface $request) { + $server = new StreamingServer(function (ServerRequestInterface $request) { return new Response( 200, array( @@ -2177,7 +2177,7 @@ function ($data) use (&$buffer) { public function testResponseWithCustomTransferEncodingWillBeIgnoredAndUseChunkedTransferEncodingInstead() { $stream = new ThroughStream(); - $server = new Server(function (ServerRequestInterface $request) use ($stream) { + $server = new StreamingServer(function (ServerRequestInterface $request) use ($stream) { return new Response( 200, array( @@ -2214,7 +2214,7 @@ function ($data) use (&$buffer) { public function testResponseWithoutExplicitDateHeaderWillAddCurrentDate() { - $server = new Server(function (ServerRequestInterface $request) { + $server = new StreamingServer(function (ServerRequestInterface $request) { return new Response(); }); @@ -2244,7 +2244,7 @@ function ($data) use (&$buffer) { public function testResponseWIthCustomDateHeaderOverwritesDefault() { - $server = new Server(function (ServerRequestInterface $request) { + $server = new StreamingServer(function (ServerRequestInterface $request) { return new Response( 200, array("Date" => "Tue, 15 Nov 1994 08:12:31 GMT") @@ -2277,7 +2277,7 @@ function ($data) use (&$buffer) { public function testResponseWithEmptyDateHeaderRemovesDateHeader() { - $server = new Server(function (ServerRequestInterface $request) { + $server = new StreamingServer(function (ServerRequestInterface $request) { return new Response( 200, array('Date' => '') @@ -2310,7 +2310,7 @@ function ($data) use (&$buffer) { public function testResponseCanContainMultipleCookieHeaders() { - $server = new Server(function (ServerRequestInterface $request) { + $server = new StreamingServer(function (ServerRequestInterface $request) { return new Response( 200, array( @@ -2350,7 +2350,7 @@ public function testRequestOnlyChunkedEncodingIsAllowedForTransferEncoding() { $error = null; - $server = new Server($this->expectCallableNever()); + $server = new StreamingServer($this->expectCallableNever()); $server->on('error', function ($exception) use (&$error) { $error = $exception; }); @@ -2387,7 +2387,7 @@ public function testRequestOnlyChunkedEncodingIsAllowedForTransferEncodingWithHt { $error = null; - $server = new Server($this->expectCallableNever()); + $server = new StreamingServer($this->expectCallableNever()); $server->on('error', function ($exception) use (&$error) { $error = $exception; }); @@ -2420,7 +2420,7 @@ function ($data) use (&$buffer) { public function testReponseWithExpectContinueRequestContainsContinueWithLaterResponse() { - $server = new Server(function (ServerRequestInterface $request) { + $server = new StreamingServer(function (ServerRequestInterface $request) { return new Response(); }); @@ -2452,7 +2452,7 @@ function ($data) use (&$buffer) { public function testResponseWithExpectContinueRequestWontSendContinueForHttp10() { - $server = new Server(function (ServerRequestInterface $request) { + $server = new StreamingServer(function (ServerRequestInterface $request) { return new Response(); }); @@ -2485,14 +2485,14 @@ function ($data) use (&$buffer) { */ public function testInvalidCallbackFunctionLeadsToException() { - $server = new Server('invalid'); + $server = new StreamingServer('invalid'); } public function testResponseBodyStreamWillStreamDataWithChunkedTransferEncoding() { $input = new ThroughStream(); - $server = new Server(function (ServerRequestInterface $request) use ($input) { + $server = new StreamingServer(function (ServerRequestInterface $request) use ($input) { return new Response( 200, array(), @@ -2531,7 +2531,7 @@ public function testResponseBodyStreamWithContentLengthWillStreamTillLengthWitho { $input = new ThroughStream(); - $server = new Server(function (ServerRequestInterface $request) use ($input) { + $server = new StreamingServer(function (ServerRequestInterface $request) use ($input) { return new Response( 200, array('Content-Length' => 5), @@ -2569,7 +2569,7 @@ function ($data) use (&$buffer) { public function testResponseWithResponsePromise() { - $server = new Server(function (ServerRequestInterface $request) { + $server = new StreamingServer(function (ServerRequestInterface $request) { return \React\Promise\resolve(new Response()); }); @@ -2597,7 +2597,7 @@ function ($data) use (&$buffer) { public function testResponseReturnInvalidTypeWillResultInError() { - $server = new Server(function (ServerRequestInterface $request) { + $server = new StreamingServer(function (ServerRequestInterface $request) { return "invalid"; }); @@ -2631,7 +2631,7 @@ function ($data) use (&$buffer) { public function testResponseResolveWrongTypeInPromiseWillResultInError() { - $server = new Server(function (ServerRequestInterface $request) { + $server = new StreamingServer(function (ServerRequestInterface $request) { return \React\Promise\resolve("invalid"); }); @@ -2659,7 +2659,7 @@ function ($data) use (&$buffer) { public function testResponseRejectedPromiseWillResultInErrorMessage() { - $server = new Server(function (ServerRequestInterface $request) { + $server = new StreamingServer(function (ServerRequestInterface $request) { return new Promise(function ($resolve, $reject) { $reject(new \Exception()); }); @@ -2690,7 +2690,7 @@ function ($data) use (&$buffer) { public function testResponseExceptionInCallbackWillResultInErrorMessage() { - $server = new Server(function (ServerRequestInterface $request) { + $server = new StreamingServer(function (ServerRequestInterface $request) { return new Promise(function ($resolve, $reject) { throw new \Exception('Bad call'); }); @@ -2721,7 +2721,7 @@ function ($data) use (&$buffer) { public function testResponseWithContentLengthHeaderForStringBodyOverwritesTransferEncoding() { - $server = new Server(function (ServerRequestInterface $request) { + $server = new StreamingServer(function (ServerRequestInterface $request) { return new Response( 200, array('Transfer-Encoding' => 'chunked'), @@ -2757,7 +2757,7 @@ function ($data) use (&$buffer) { public function testResponseWillBeHandled() { - $server = new Server(function (ServerRequestInterface $request) { + $server = new StreamingServer(function (ServerRequestInterface $request) { return new Response(); }); @@ -2785,7 +2785,7 @@ function ($data) use (&$buffer) { public function testResponseExceptionThrowInCallBackFunctionWillResultInErrorMessage() { - $server = new Server(function (ServerRequestInterface $request) { + $server = new StreamingServer(function (ServerRequestInterface $request) { throw new \Exception('hello'); }); @@ -2823,7 +2823,7 @@ function ($data) use (&$buffer) { */ public function testResponseThrowableThrowInCallBackFunctionWillResultInErrorMessage() { - $server = new Server(function (ServerRequestInterface $request) { + $server = new StreamingServer(function (ServerRequestInterface $request) { throw new \Error('hello'); }); @@ -2866,7 +2866,7 @@ function ($data) use (&$buffer) { public function testResponseRejectOfNonExceptionWillResultInErrorMessage() { - $server = new Server(function (ServerRequestInterface $request) { + $server = new StreamingServer(function (ServerRequestInterface $request) { return new Promise(function ($resolve, $reject) { $reject('Invalid type'); }); @@ -2903,7 +2903,7 @@ function ($data) use (&$buffer) { public function testRequestServerRequestParams() { $requestValidation = null; - $server = new Server(function (ServerRequestInterface $request) use (&$requestValidation) { + $server = new StreamingServer(function (ServerRequestInterface $request) use (&$requestValidation) { $requestValidation = $request; }); @@ -2937,7 +2937,7 @@ public function testRequestServerRequestParams() public function testRequestQueryParametersWillBeAddedToRequest() { $requestValidation = null; - $server = new Server(function (ServerRequestInterface $request) use (&$requestValidation) { + $server = new StreamingServer(function (ServerRequestInterface $request) use (&$requestValidation) { $requestValidation = $request; }); @@ -2957,7 +2957,7 @@ public function testRequestQueryParametersWillBeAddedToRequest() public function testRequestCookieWillBeAddedToServerRequest() { $requestValidation = null; - $server = new Server(function (ServerRequestInterface $request) use (&$requestValidation) { + $server = new StreamingServer(function (ServerRequestInterface $request) use (&$requestValidation) { $requestValidation = $request; }); @@ -2978,7 +2978,7 @@ public function testRequestCookieWillBeAddedToServerRequest() public function testRequestInvalidMultipleCookiesWontBeAddedToServerRequest() { $requestValidation = null; - $server = new Server(function (ServerRequestInterface $request) use (&$requestValidation) { + $server = new StreamingServer(function (ServerRequestInterface $request) use (&$requestValidation) { $requestValidation = $request; }); @@ -2999,7 +2999,7 @@ public function testRequestInvalidMultipleCookiesWontBeAddedToServerRequest() public function testRequestCookieWithSeparatorWillBeAddedToServerRequest() { $requestValidation = null; - $server = new Server(function (ServerRequestInterface $request) use (&$requestValidation) { + $server = new StreamingServer(function (ServerRequestInterface $request) use (&$requestValidation) { $requestValidation = $request; }); From 7c790efac2d726b88bef8a9e9bd4b7d420c5ec7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 26 Nov 2017 16:51:22 +0100 Subject: [PATCH 216/456] Handle file uploads with missing or duplicate Content-Type header --- src/Io/MultipartParser.php | 7 ++-- tests/Io/MultipartParserTest.php | 66 ++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 3 deletions(-) diff --git a/src/Io/MultipartParser.php b/src/Io/MultipartParser.php index b6f3c947..abe69f38 100644 --- a/src/Io/MultipartParser.php +++ b/src/Io/MultipartParser.php @@ -140,6 +140,7 @@ private function parseUploadedFile($headers, $body) { $filename = $this->getParameterFromHeader($headers['content-disposition'], 'filename'); $bodyLength = strlen($body); + $contentType = isset($headers['content-type'][0]) ? $headers['content-type'][0] : null; // no file selected (zero size and empty filename) if ($bodyLength === 0 && $filename === '') { @@ -148,7 +149,7 @@ private function parseUploadedFile($headers, $body) $bodyLength, UPLOAD_ERR_NO_FILE, $filename, - $headers['content-type'][0] + $contentType ); } @@ -159,7 +160,7 @@ private function parseUploadedFile($headers, $body) $bodyLength, UPLOAD_ERR_FORM_SIZE, $filename, - $headers['content-type'][0] + $contentType ); } @@ -168,7 +169,7 @@ private function parseUploadedFile($headers, $body) $bodyLength, UPLOAD_ERR_OK, $filename, - $headers['content-type'][0] + $contentType ); } diff --git a/tests/Io/MultipartParserTest.php b/tests/Io/MultipartParserTest.php index 9e6a227e..c6515c24 100644 --- a/tests/Io/MultipartParserTest.php +++ b/tests/Io/MultipartParserTest.php @@ -419,6 +419,72 @@ public function testInvalidContentDispositionWithoutNameWillBeIgnored() $this->assertEmpty($parsedRequest->getParsedBody()); } + public function testInvalidUploadFileWithoutContentTypeUsesNullValue() + { + $boundary = "---------------------------12758086162038677464950549563"; + + $data = "--$boundary\r\n"; + $data .= "Content-Disposition: form-data; name=\"file\"; filename=\"hello.txt\"\r\n"; + $data .= "\r\n"; + $data .= "world\r\n"; + $data .= "--$boundary--\r\n"; + + $request = new ServerRequest('POST', 'http://example.com/', array( + 'Content-Type' => 'multipart/mixed; boundary=' . $boundary, + ), $data, 1.1); + + $parsedRequest = MultipartParser::parseRequest($request); + + $files = $parsedRequest->getUploadedFiles(); + + $this->assertCount(1, $files); + $this->assertTrue(isset($files['file'])); + $this->assertInstanceOf('Psr\Http\Message\UploadedFileInterface', $files['file']); + + /* @var $file \Psr\Http\Message\UploadedFileInterface */ + $file = $files['file']; + + $this->assertSame('hello.txt', $file->getClientFilename()); + $this->assertSame(null, $file->getClientMediaType()); + $this->assertSame(5, $file->getSize()); + $this->assertSame(UPLOAD_ERR_OK, $file->getError()); + $this->assertSame('world', (string)$file->getStream()); + } + + public function testInvalidUploadFileWithoutMultipleContentTypeUsesLastValue() + { + $boundary = "---------------------------12758086162038677464950549563"; + + $data = "--$boundary\r\n"; + $data .= "Content-Disposition: form-data; name=\"file\"; filename=\"hello.txt\"\r\n"; + $data .= "Content-Type: text/ignored\r\n"; + $data .= "Content-Type: text/plain\r\n"; + $data .= "\r\n"; + $data .= "world\r\n"; + $data .= "--$boundary--\r\n"; + + $request = new ServerRequest('POST', 'http://example.com/', array( + 'Content-Type' => 'multipart/mixed; boundary=' . $boundary, + ), $data, 1.1); + + $parsedRequest = MultipartParser::parseRequest($request); + + $files = $parsedRequest->getUploadedFiles(); + + $this->assertCount(1, $files); + $this->assertTrue(isset($files['file'])); + $this->assertInstanceOf('Psr\Http\Message\UploadedFileInterface', $files['file']); + + /* @var $file \Psr\Http\Message\UploadedFileInterface */ + $file = $files['file']; + + $this->assertSame('hello.txt', $file->getClientFilename()); + $this->assertSame('text/plain', $file->getClientMediaType()); + $this->assertSame(5, $file->getSize()); + $this->assertSame(UPLOAD_ERR_OK, $file->getError()); + $this->assertSame('world', (string)$file->getStream()); + } + public function testUploadEmptyFile() { $boundary = "---------------------------12758086162038677464950549563"; From 8c281d472ba687a248f2f04940bc95a273fc77ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 27 Nov 2017 18:20:46 +0100 Subject: [PATCH 217/456] Fix ignoring invalid/incomplete parts --- src/Io/MultipartParser.php | 7 +- tests/Io/MultipartParserTest.php | 109 +++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+), 3 deletions(-) diff --git a/src/Io/MultipartParser.php b/src/Io/MultipartParser.php index abe69f38..85ed42db 100644 --- a/src/Io/MultipartParser.php +++ b/src/Io/MultipartParser.php @@ -105,12 +105,13 @@ private function parseBuffer() private function parseChunk($chunk) { - if ($chunk === '') { + $pos = strpos($chunk, "\r\n\r\n"); + if ($pos === false) { return; } - list ($header, $body) = explode("\r\n\r\n", $chunk, 2); - $headers = $this->parseHeaders($header); + $headers = $this->parseHeaders((string)substr($chunk, 0, $pos)); + $body = (string)substr($chunk, $pos + 4); if (!isset($headers['content-disposition'])) { return; diff --git a/tests/Io/MultipartParserTest.php b/tests/Io/MultipartParserTest.php index c6515c24..8396ac22 100644 --- a/tests/Io/MultipartParserTest.php +++ b/tests/Io/MultipartParserTest.php @@ -379,6 +379,93 @@ public function testFileUpload() $this->assertSame("getStream()); } + public function testInvalidDoubleContentDispositionUsesLast() + { + $boundary = "---------------------------5844729766471062541057622570"; + + $data = "--$boundary\r\n"; + $data .= "Content-Disposition: form-data; name=\"ignored\"\r\n"; + $data .= "Content-Disposition: form-data; name=\"key\"\r\n"; + $data .= "\r\n"; + $data .= "value\r\n"; + $data .= "--$boundary--\r\n"; + + $request = new ServerRequest('POST', 'http://example.com/', array( + 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, + ), $data, 1.1); + + $parsedRequest = MultipartParser::parseRequest($request); + + $this->assertEmpty($parsedRequest->getUploadedFiles()); + $this->assertSame( + array( + 'key' => 'value' + ), + $parsedRequest->getParsedBody() + ); + } + + public function testInvalidMissingNewlineAfterValueDoesNotMatter() + { + $boundary = "---------------------------5844729766471062541057622570"; + + $data = "--$boundary\r\n"; + $data .= "Content-Disposition: form-data; name=\"key\"\r\n"; + $data .= "\r\n"; + $data .= "value"; + $data .= "--$boundary--\r\n"; + + $request = new ServerRequest('POST', 'http://example.com/', array( + 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, + ), $data, 1.1); + + $parsedRequest = MultipartParser::parseRequest($request); + + $this->assertEmpty($parsedRequest->getUploadedFiles()); + $this->assertSame( + array( + 'key' => 'value' + ), + $parsedRequest->getParsedBody() + ); + } + + public function testInvalidMissingValueWillBeIgnored() + { + $boundary = "---------------------------5844729766471062541057622570"; + + $data = "--$boundary\r\n"; + $data .= "Content-Disposition: form-data; name=\"key\"\r\n"; + $data .= "\r\n"; + $data .= "--$boundary--\r\n"; + + $request = new ServerRequest('POST', 'http://example.com/', array( + 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, + ), $data, 1.1); + + $parsedRequest = MultipartParser::parseRequest($request); + + $this->assertEmpty($parsedRequest->getUploadedFiles()); + $this->assertEmpty($parsedRequest->getParsedBody()); + } + + public function testInvalidMissingValueAndEndBoundaryWillBeIgnored() + { + $boundary = "---------------------------5844729766471062541057622570"; + + $data = "--$boundary\r\n"; + $data .= "Content-Disposition: form-data; name=\"key\"\r\n"; + + $request = new ServerRequest('POST', 'http://example.com/', array( + 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, + ), $data, 1.1); + + $parsedRequest = MultipartParser::parseRequest($request); + + $this->assertEmpty($parsedRequest->getUploadedFiles()); + $this->assertEmpty($parsedRequest->getParsedBody()); + } + public function testInvalidContentDispositionMissingWillBeIgnored() { $boundary = "---------------------------5844729766471062541057622570"; @@ -419,6 +506,28 @@ public function testInvalidContentDispositionWithoutNameWillBeIgnored() $this->assertEmpty($parsedRequest->getParsedBody()); } + public function testInvalidMissingEndBoundaryWillBeIgnored() + { + $boundary = "---------------------------5844729766471062541057622570"; + + $data = "--$boundary\r\n"; + $data .= "Content-Disposition: form-data; name=\"key\"\r\n"; + $data .= "\r\n"; + $data .= "value\r\n"; + + $request = new ServerRequest('POST', 'http://example.com/', array( + 'Content-Type' => 'multipart/mixed; boundary=' . $boundary, + ), $data, 1.1); + + $parsedRequest = MultipartParser::parseRequest($request); + + $this->assertEmpty($parsedRequest->getUploadedFiles()); + $this->assertSame( + null, + $parsedRequest->getParsedBody() + ); + } + public function testInvalidUploadFileWithoutContentTypeUsesNullValue() { $boundary = "---------------------------12758086162038677464950549563"; From 5da1a87a4c46a762895d56610497ca5d0beb0c44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 28 Nov 2017 14:54:46 +0100 Subject: [PATCH 218/456] Rely on required boundary=X parameter and do not try to guess --- src/Io/MultipartParser.php | 32 ++-------- tests/Io/MultipartParserTest.php | 62 +++++++++++++------ .../RequestBodyParserMiddlewareTest.php | 2 +- 3 files changed, 49 insertions(+), 47 deletions(-) diff --git a/src/Io/MultipartParser.php b/src/Io/MultipartParser.php index 85ed42db..a5bacb3c 100644 --- a/src/Io/MultipartParser.php +++ b/src/Io/MultipartParser.php @@ -60,37 +60,15 @@ private function parse() { $this->buffer = (string)$this->request->getBody(); - $this->determineStartMethod(); - - return $this->request; - } - - private function determineStartMethod() - { - if (!$this->request->hasHeader('content-type')) { - $this->findBoundary(); - return; - } - $contentType = $this->request->getHeaderLine('content-type'); - preg_match('/boundary="?(.*)"?$/', $contentType, $matches); - if (isset($matches[1])) { - $this->boundary = $matches[1]; - $this->parseBuffer(); - return; + if(!preg_match('/boundary="?(.*)"?$/', $contentType, $matches)) { + return $this->request; } - $this->findBoundary(); - } + $this->boundary = $matches[1]; + $this->parseBuffer(); - private function findBoundary() - { - if (substr($this->buffer, 0, 3) === '---' && strpos($this->buffer, "\r\n") !== false) { - $boundary = substr($this->buffer, 2, strpos($this->buffer, "\r\n")); - $boundary = substr($boundary, 0, -2); - $this->boundary = $boundary; - $this->parseBuffer(); - } + return $this->request; } private function parseBuffer() diff --git a/tests/Io/MultipartParserTest.php b/tests/Io/MultipartParserTest.php index 8396ac22..ef6179fa 100644 --- a/tests/Io/MultipartParserTest.php +++ b/tests/Io/MultipartParserTest.php @@ -8,6 +8,30 @@ final class MultipartParserTest extends TestCase { + public function testDoesNotParseWithoutMultipartFormDataContentType() + { + $boundary = "---------------------------5844729766471062541057622570"; + + $data = "--$boundary\r\n"; + $data .= "Content-Disposition: form-data; name=\"single\"\r\n"; + $data .= "\r\n"; + $data .= "single\r\n"; + $data .= "--$boundary\r\n"; + $data .= "Content-Disposition: form-data; name=\"second\"\r\n"; + $data .= "\r\n"; + $data .= "second\r\n"; + $data .= "--$boundary--\r\n"; + + $request = new ServerRequest('POST', 'http://example.com/', array( + 'Content-Type' => 'multipart/form-data', + ), $data, 1.1); + + $parsedRequest = MultipartParser::parseRequest($request); + + $this->assertEmpty($parsedRequest->getUploadedFiles()); + $this->assertEmpty($parsedRequest->getParsedBody()); + } + public function testPostKey() { $boundary = "---------------------------5844729766471062541057622570"; @@ -23,7 +47,7 @@ public function testPostKey() $data .= "--$boundary--\r\n"; $request = new ServerRequest('POST', 'http://example.com/', array( - 'Content-Type' => 'multipart/mixed; boundary=' . $boundary, + 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, ), $data, 1.1); $parsedRequest = MultipartParser::parseRequest($request); @@ -55,7 +79,7 @@ public function testPostStringOverwritesMap() $data .= "--$boundary--\r\n"; $request = new ServerRequest('POST', 'http://example.com/', array( - 'Content-Type' => 'multipart/mixed; boundary=' . $boundary, + 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, ), $data, 1.1); $parsedRequest = MultipartParser::parseRequest($request); @@ -84,7 +108,7 @@ public function testPostMapOverwritesString() $data .= "--$boundary--\r\n"; $request = new ServerRequest('POST', 'http://example.com/', array( - 'Content-Type' => 'multipart/mixed; boundary=' . $boundary, + 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, ), $data, 1.1); $parsedRequest = MultipartParser::parseRequest($request); @@ -115,7 +139,7 @@ public function testPostVectorOverwritesString() $data .= "--$boundary--\r\n"; $request = new ServerRequest('POST', 'http://example.com/', array( - 'Content-Type' => 'multipart/mixed; boundary=' . $boundary, + 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, ), $data, 1.1); $parsedRequest = MultipartParser::parseRequest($request); @@ -146,7 +170,7 @@ public function testPostDeeplyNestedArray() $data .= "--$boundary--\r\n"; $request = new ServerRequest('POST', 'http://example.com/', array( - 'Content-Type' => 'multipart/mixed; boundary=' . $boundary, + 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, ), $data, 1.1); $parsedRequest = MultipartParser::parseRequest($request); @@ -178,7 +202,7 @@ public function testEmptyPostValue() $data .= "--$boundary--\r\n"; $request = new ServerRequest('POST', 'http://example.com/', array( - 'Content-Type' => 'multipart/mixed; boundary=' . $boundary, + 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, ), $data, 1.1); $parsedRequest = MultipartParser::parseRequest($request); @@ -203,7 +227,7 @@ public function testEmptyPostKey() $data .= "--$boundary--\r\n"; $request = new ServerRequest('POST', 'http://example.com/', array( - 'Content-Type' => 'multipart/mixed; boundary=' . $boundary, + 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, ), $data, 1.1); $parsedRequest = MultipartParser::parseRequest($request); @@ -228,7 +252,7 @@ public function testNestedPostKeyAssoc() $data .= "--$boundary--\r\n"; $request = new ServerRequest('POST', 'http://example.com/', array( - 'Content-Type' => 'multipart/mixed; boundary=' . $boundary, + 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, ), $data, 1.1); $parsedRequest = MultipartParser::parseRequest($request); @@ -257,7 +281,7 @@ public function testNestedPostKeyVector() $data .= "--$boundary--\r\n"; $request = new ServerRequest('POST', 'http://example.com/', array( - 'Content-Type' => 'multipart/mixed; boundary=' . $boundary, + 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, ), $data, 1.1); $parsedRequest = MultipartParser::parseRequest($request); @@ -337,7 +361,7 @@ public function testFileUpload() $data .= "--$boundary--\r\n"; $request = new ServerRequest('POST', 'http://example.com/', array( - 'Content-Type' => 'multipart/form-data', + 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, ), $data, 1.1); $parsedRequest = MultipartParser::parseRequest($request); @@ -477,7 +501,7 @@ public function testInvalidContentDispositionMissingWillBeIgnored() $data .= "--$boundary--\r\n"; $request = new ServerRequest('POST', 'http://example.com/', array( - 'Content-Type' => 'multipart/mixed; boundary=' . $boundary, + 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, ), $data, 1.1); $parsedRequest = MultipartParser::parseRequest($request); @@ -497,7 +521,7 @@ public function testInvalidContentDispositionWithoutNameWillBeIgnored() $data .= "--$boundary--\r\n"; $request = new ServerRequest('POST', 'http://example.com/', array( - 'Content-Type' => 'multipart/mixed; boundary=' . $boundary, + 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, ), $data, 1.1); $parsedRequest = MultipartParser::parseRequest($request); @@ -516,7 +540,7 @@ public function testInvalidMissingEndBoundaryWillBeIgnored() $data .= "value\r\n"; $request = new ServerRequest('POST', 'http://example.com/', array( - 'Content-Type' => 'multipart/mixed; boundary=' . $boundary, + 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, ), $data, 1.1); $parsedRequest = MultipartParser::parseRequest($request); @@ -539,7 +563,7 @@ public function testInvalidUploadFileWithoutContentTypeUsesNullValue() $data .= "--$boundary--\r\n"; $request = new ServerRequest('POST', 'http://example.com/', array( - 'Content-Type' => 'multipart/mixed; boundary=' . $boundary, + 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, ), $data, 1.1); $parsedRequest = MultipartParser::parseRequest($request); @@ -573,7 +597,7 @@ public function testInvalidUploadFileWithoutMultipleContentTypeUsesLastValue() $data .= "--$boundary--\r\n"; $request = new ServerRequest('POST', 'http://example.com/', array( - 'Content-Type' => 'multipart/mixed; boundary=' . $boundary, + 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, ), $data, 1.1); $parsedRequest = MultipartParser::parseRequest($request); @@ -606,7 +630,7 @@ public function testUploadEmptyFile() $data .= "--$boundary--\r\n"; $request = new ServerRequest('POST', 'http://example.com/', array( - 'Content-Type' => 'multipart/form-data', + 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, ), $data, 1.1); $parsedRequest = MultipartParser::parseRequest($request); @@ -639,7 +663,7 @@ public function testUploadNoFile() $data .= "--$boundary--\r\n"; $request = new ServerRequest('POST', 'http://example.com/', array( - 'Content-Type' => 'multipart/form-data', + 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, ), $data, 1.1); $parsedRequest = MultipartParser::parseRequest($request); @@ -676,7 +700,7 @@ public function testPostMaxFileSize() $data .= "--$boundary--\r\n"; $request = new ServerRequest('POST', 'http://example.com/', array( - 'Content-Type' => 'multipart/form-data', + 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, ), $data, 1.1); $parsedRequest = MultipartParser::parseRequest($request); @@ -730,7 +754,7 @@ public function testPostMaxFileSizeIgnoredByFilesComingBeforeIt() $data .= "--$boundary--\r\n"; $request = new ServerRequest('POST', 'http://example.com/', array( - 'Content-Type' => 'multipart/form-data', + 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, ), $data, 1.1); $parsedRequest = MultipartParser::parseRequest($request); diff --git a/tests/Middleware/RequestBodyParserMiddlewareTest.php b/tests/Middleware/RequestBodyParserMiddlewareTest.php index 68fad434..67a02c90 100644 --- a/tests/Middleware/RequestBodyParserMiddlewareTest.php +++ b/tests/Middleware/RequestBodyParserMiddlewareTest.php @@ -149,7 +149,7 @@ public function testMultipartFormDataParsing() $request = new ServerRequest('POST', 'http://example.com/', array( - 'Content-Type' => 'multipart/form-data', + 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, ), $data, 1.1); /** @var ServerRequestInterface $parsedRequest */ From 129df64e9e3194b742936611523f607f5942b3e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 28 Nov 2017 16:08:32 +0100 Subject: [PATCH 219/456] Simplify parameter parsing by removing duplicate logic --- src/Io/MultipartParser.php | 84 ++++++++++++++++---------------------- 1 file changed, 36 insertions(+), 48 deletions(-) diff --git a/src/Io/MultipartParser.php b/src/Io/MultipartParser.php index a5bacb3c..a4584015 100644 --- a/src/Io/MultipartParser.php +++ b/src/Io/MultipartParser.php @@ -95,37 +95,42 @@ private function parseChunk($chunk) return; } - if (!$this->headerContainsParameter($headers['content-disposition'], 'name')) { + $name = $this->getParameterFromHeader($headers['content-disposition'], 'name'); + if ($name === null) { return; } - if ($this->headerContainsParameter($headers['content-disposition'], 'filename')) { - $this->parseFile($headers, $body); + $filename = $this->getParameterFromHeader($headers['content-disposition'], 'filename'); + if ($filename !== null) { + $this->parseFile( + $name, + $filename, + isset($headers['content-type'][0]) ? $headers['content-type'][0] : null, + $body + ); } else { - $this->parsePost($headers, $body); + $this->parsePost($name, $body); } } - private function parseFile($headers, $body) + private function parseFile($name, $filename, $contentType, $contents) { $this->request = $this->request->withUploadedFiles($this->extractPost( $this->request->getUploadedFiles(), - $this->getParameterFromHeader($headers['content-disposition'], 'name'), - $this->parseUploadedFile($headers, $body) + $name, + $this->parseUploadedFile($filename, $contentType, $contents) )); } - private function parseUploadedFile($headers, $body) + private function parseUploadedFile($filename, $contentType, $contents) { - $filename = $this->getParameterFromHeader($headers['content-disposition'], 'filename'); - $bodyLength = strlen($body); - $contentType = isset($headers['content-type'][0]) ? $headers['content-type'][0] : null; + $size = strlen($contents); // no file selected (zero size and empty filename) - if ($bodyLength === 0 && $filename === '') { + if ($size === 0 && $filename === '') { return new UploadedFile( Psr7\stream_for(''), - $bodyLength, + $size, UPLOAD_ERR_NO_FILE, $filename, $contentType @@ -133,10 +138,10 @@ private function parseUploadedFile($headers, $body) } // file exceeds MAX_FILE_SIZE value - if ($this->maxFileSize !== null && $bodyLength > $this->maxFileSize) { + if ($this->maxFileSize !== null && $size > $this->maxFileSize) { return new UploadedFile( Psr7\stream_for(''), - $bodyLength, + $size, UPLOAD_ERR_FORM_SIZE, $filename, $contentType @@ -144,32 +149,27 @@ private function parseUploadedFile($headers, $body) } return new UploadedFile( - Psr7\stream_for($body), - $bodyLength, + Psr7\stream_for($contents), + $size, UPLOAD_ERR_OK, $filename, $contentType ); } - private function parsePost($headers, $body) + private function parsePost($name, $value) { - foreach ($headers['content-disposition'] as $part) { - if (strpos($part, 'name') === 0) { - preg_match('/name="?(.*)"$/', $part, $matches); - $this->request = $this->request->withParsedBody($this->extractPost( - $this->request->getParsedBody(), - $matches[1], - $body - )); - - if (strtoupper($matches[1]) === 'MAX_FILE_SIZE') { - $this->maxFileSize = (int)$body; - - if ($this->maxFileSize === 0) { - $this->maxFileSize = null; - } - } + $this->request = $this->request->withParsedBody($this->extractPost( + $this->request->getParsedBody(), + $name, + $value + )); + + if (strtoupper($name) === 'MAX_FILE_SIZE') { + $this->maxFileSize = (int)$value; + + if ($this->maxFileSize === 0) { + $this->maxFileSize = null; } } } @@ -190,27 +190,15 @@ private function parseHeaders($header) return $headers; } - private function headerContainsParameter(array $header, $parameter) - { - foreach ($header as $part) { - if (strpos($part, $parameter . '=') === 0) { - return true; - } - } - - return false; - } - private function getParameterFromHeader(array $header, $parameter) { foreach ($header as $part) { - if (strpos($part, $parameter) === 0) { - preg_match('/' . $parameter . '="?(.*)"$/', $part, $matches); + if (preg_match('/' . $parameter . '="?(.*)"$/', $part, $matches)) { return $matches[1]; } } - return ''; + return null; } private function stripTrailingEOL($chunk) From 7ed5a80d8ff486f8b0f630cfe6ab46ea685c0ed5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 28 Nov 2017 17:05:01 +0100 Subject: [PATCH 220/456] Avoid excessive member variables --- src/Io/MultipartParser.php | 32 +++++--------------------------- 1 file changed, 5 insertions(+), 27 deletions(-) diff --git a/src/Io/MultipartParser.php b/src/Io/MultipartParser.php index a4584015..2e77bdbc 100644 --- a/src/Io/MultipartParser.php +++ b/src/Io/MultipartParser.php @@ -15,31 +15,11 @@ */ final class MultipartParser { - /** - * @var string - */ - protected $buffer = ''; - - /** - * @var string - */ - protected $boundary; - /** * @var ServerRequestInterface */ protected $request; - /** - * @var HttpBodyStream - */ - protected $body; - - /** - * @var callable - */ - protected $onDataCallable; - /** * @var int|null */ @@ -58,23 +38,21 @@ private function __construct(ServerRequestInterface $request) private function parse() { - $this->buffer = (string)$this->request->getBody(); - $contentType = $this->request->getHeaderLine('content-type'); if(!preg_match('/boundary="?(.*)"?$/', $contentType, $matches)) { return $this->request; } - $this->boundary = $matches[1]; - $this->parseBuffer(); + $this->parseBuffer($matches[1], (string)$this->request->getBody()); return $this->request; } - private function parseBuffer() + private function parseBuffer($boundary, $buffer) { - $chunks = explode('--' . $this->boundary, $this->buffer); - $this->buffer = array_pop($chunks); + $chunks = explode('--' . $boundary, $buffer); + array_pop($chunks); + foreach ($chunks as $chunk) { $chunk = $this->stripTrailingEOL($chunk); $this->parseChunk($chunk); From cd5d3c80b11ea48074b7dc75be034721805e3470 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 28 Nov 2017 18:25:43 +0100 Subject: [PATCH 221/456] Strictly obey newline definitions as per RFC 7578 and RFC 2046 --- src/Io/MultipartParser.php | 49 ++++++++++++++++++-------------- tests/Io/MultipartParserTest.php | 33 +++++++++++++++------ 2 files changed, 52 insertions(+), 30 deletions(-) diff --git a/src/Io/MultipartParser.php b/src/Io/MultipartParser.php index 2e77bdbc..d2088db5 100644 --- a/src/Io/MultipartParser.php +++ b/src/Io/MultipartParser.php @@ -12,6 +12,8 @@ * that resembles PHP's `$_POST` and `$_FILES` superglobals. * * @internal + * @link https://tools.ietf.org/html/rfc7578 + * @link https://tools.ietf.org/html/rfc2046#section-5.1.1 */ final class MultipartParser { @@ -43,23 +45,34 @@ private function parse() return $this->request; } - $this->parseBuffer($matches[1], (string)$this->request->getBody()); + $this->parseBody('--' . $matches[1], (string)$this->request->getBody()); return $this->request; } - private function parseBuffer($boundary, $buffer) + private function parseBody($boundary, $buffer) { - $chunks = explode('--' . $boundary, $buffer); - array_pop($chunks); + $len = strlen($boundary); + + // ignore everything before initial boundary (SHOULD be empty) + $start = strpos($buffer, $boundary . "\r\n"); + + while ($start !== false) { + // search following boundary (preceded by newline) + // ignore last if not followed by boundary (SHOULD end with "--") + $start += $len + 2; + $end = strpos($buffer, "\r\n" . $boundary, $start); + if ($end === false) { + break; + } - foreach ($chunks as $chunk) { - $chunk = $this->stripTrailingEOL($chunk); - $this->parseChunk($chunk); + // parse one part and continue searching for next + $this->parsePart(substr($buffer, $start, $end - $start)); + $start = $end; } } - private function parseChunk($chunk) + private function parsePart($chunk) { $pos = strpos($chunk, "\r\n\r\n"); if ($pos === false) { @@ -157,10 +170,13 @@ private function parseHeaders($header) $headers = array(); foreach (explode("\r\n", trim($header)) as $line) { - list($key, $values) = explode(':', $line, 2); - $key = trim($key); - $key = strtolower($key); - $values = explode(';', $values); + $parts = explode(':', $line, 2); + if (!isset($parts[1])) { + continue; + } + + $key = strtolower(trim($parts[0])); + $values = explode(';', $parts[1]); $values = array_map('trim', $values); $headers[$key] = $values; } @@ -179,15 +195,6 @@ private function getParameterFromHeader(array $header, $parameter) return null; } - private function stripTrailingEOL($chunk) - { - if (substr($chunk, -2) === "\r\n") { - return substr($chunk, 0, -2); - } - - return $chunk; - } - private function extractPost($postFields, $key, $value) { $chunks = explode('[', $key); diff --git a/tests/Io/MultipartParserTest.php b/tests/Io/MultipartParserTest.php index ef6179fa..30dbcf99 100644 --- a/tests/Io/MultipartParserTest.php +++ b/tests/Io/MultipartParserTest.php @@ -429,7 +429,7 @@ public function testInvalidDoubleContentDispositionUsesLast() ); } - public function testInvalidMissingNewlineAfterValueDoesNotMatter() + public function testInvalidMissingNewlineAfterValueWillBeIgnored() { $boundary = "---------------------------5844729766471062541057622570"; @@ -446,12 +446,7 @@ public function testInvalidMissingNewlineAfterValueDoesNotMatter() $parsedRequest = MultipartParser::parseRequest($request); $this->assertEmpty($parsedRequest->getUploadedFiles()); - $this->assertSame( - array( - 'key' => 'value' - ), - $parsedRequest->getParsedBody() - ); + $this->assertEmpty($parsedRequest->getParsedBody()); } public function testInvalidMissingValueWillBeIgnored() @@ -497,7 +492,27 @@ public function testInvalidContentDispositionMissingWillBeIgnored() $data = "--$boundary\r\n"; $data .= "Content-Type: text/plain\r\n"; $data .= "\r\n"; - $data .= "hello"; + $data .= "hello\r\n"; + $data .= "--$boundary--\r\n"; + + $request = new ServerRequest('POST', 'http://example.com/', array( + 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, + ), $data, 1.1); + + $parsedRequest = MultipartParser::parseRequest($request); + + $this->assertEmpty($parsedRequest->getUploadedFiles()); + $this->assertEmpty($parsedRequest->getParsedBody()); + } + + public function testInvalidContentDispositionMissingValueWillBeIgnored() + { + $boundary = "---------------------------5844729766471062541057622570"; + + $data = "--$boundary\r\n"; + $data .= "Content-Disposition\r\n"; + $data .= "\r\n"; + $data .= "value\r\n"; $data .= "--$boundary--\r\n"; $request = new ServerRequest('POST', 'http://example.com/', array( @@ -517,7 +532,7 @@ public function testInvalidContentDispositionWithoutNameWillBeIgnored() $data = "--$boundary\r\n"; $data .= "Content-Disposition: form-data; something=\"key\"\r\n"; $data .= "\r\n"; - $data .= "value"; + $data .= "value\r\n"; $data .= "--$boundary--\r\n"; $request = new ServerRequest('POST', 'http://example.com/', array( From 21fd349e263088a727121de8841e4e7cf036ce6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 24 Nov 2017 13:33:18 +0100 Subject: [PATCH 222/456] Respect max_input_vars and max_input_nesting_level ini settings --- README.md | 6 ++ .../RequestBodyParserMiddleware.php | 4 +- .../RequestBodyParserMiddlewareTest.php | 70 +++++++++++++++++++ 3 files changed, 79 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0fe15df2..d667f196 100644 --- a/README.md +++ b/README.md @@ -789,6 +789,12 @@ See also [example #12](examples) for more details. > PHP's `MAX_FILE_SIZE` hidden field is respected by this middleware. +> This middleware respects the + [`max_input_vars`](http://php.net/manual/en/info.configuration.php#ini.max-input-vars) + (default `1000`) and + [`max_input_nesting_level`](http://php.net/manual/en/info.configuration.php#ini.max-input-nesting-level) + (default `64`) ini settings. + #### Third-Party Middleware A non-exhaustive list of third-party middleware can be found at the [`Middleware`](https://github.com/reactphp/http/wiki/Middleware) wiki page. diff --git a/src/Middleware/RequestBodyParserMiddleware.php b/src/Middleware/RequestBodyParserMiddleware.php index dce74708..0c0544eb 100644 --- a/src/Middleware/RequestBodyParserMiddleware.php +++ b/src/Middleware/RequestBodyParserMiddleware.php @@ -25,8 +25,10 @@ public function __invoke(ServerRequestInterface $request, $next) private function parseFormUrlencoded(ServerRequestInterface $request) { + // parse string into array structure + // ignore warnings due to excessive data structures (max_input_vars and max_input_nesting_level) $ret = array(); - parse_str((string)$request->getBody(), $ret); + @parse_str((string)$request->getBody(), $ret); return $request->withParsedBody($ret); } diff --git a/tests/Middleware/RequestBodyParserMiddlewareTest.php b/tests/Middleware/RequestBodyParserMiddlewareTest.php index 67a02c90..a8755e27 100644 --- a/tests/Middleware/RequestBodyParserMiddlewareTest.php +++ b/tests/Middleware/RequestBodyParserMiddlewareTest.php @@ -107,6 +107,76 @@ function (ServerRequestInterface $request) { $this->assertSame('foo=bar&baz[]=cheese&bar[]=beer&bar[]=wine&market[fish]=salmon&market[meat][]=beef&market[meat][]=chicken&market[]=bazaar', (string)$parsedRequest->getBody()); } + public function testFormUrlencodedIgnoresBodyWithExcessiveNesting() + { + // supported in all Zend PHP versions and HHVM + // ini setting does exist everywhere but HHVM: https://3v4l.org/hXLiK + // HHVM limits to 64 and returns an empty array structure: https://3v4l.org/j3DK2 + if (defined('HHVM_VERSION')) { + $this->markTestSkipped('Not supported on HHVM (limited to depth 64, but keeps empty array structure)'); + } + + $allowed = (int)ini_get('max_input_nesting_level'); + + $middleware = new RequestBodyParserMiddleware(); + $request = new ServerRequest( + 'POST', + 'https://example.com/', + array( + 'Content-Type' => 'application/x-www-form-urlencoded', + ), + 'hello' . str_repeat('[]', $allowed + 1) . '=world' + ); + + /** @var ServerRequestInterface $parsedRequest */ + $parsedRequest = $middleware( + $request, + function (ServerRequestInterface $request) { + return $request; + } + ); + + $this->assertSame( + array(), + $parsedRequest->getParsedBody() + ); + } + + public function testFormUrlencodedTruncatesBodyWithExcessiveLength() + { + // supported as of PHP 5.3.11, no HHVM support: https://3v4l.org/PiqnQ + // ini setting already exists in PHP 5.3.9: https://3v4l.org/VF6oV + if (defined('HHVM_VERSION') || PHP_VERSION_ID < 50311) { + $this->markTestSkipped('Not supported on HHVM and PHP < 5.3.11 (unlimited length)'); + } + + $allowed = (int)ini_get('max_input_vars'); + + $middleware = new RequestBodyParserMiddleware(); + $request = new ServerRequest( + 'POST', + 'https://example.com/', + array( + 'Content-Type' => 'application/x-www-form-urlencoded', + ), + str_repeat('a[]=b&', $allowed + 1) + ); + + /** @var ServerRequestInterface $parsedRequest */ + $parsedRequest = $middleware( + $request, + function (ServerRequestInterface $request) { + return $request; + } + ); + + $body = $parsedRequest->getParsedBody(); + + $this->assertCount(1, $body); + $this->assertTrue(isset($body['a'])); + $this->assertCount($allowed, $body['a']); + } + public function testDoesNotParseJsonByDefault() { $middleware = new RequestBodyParserMiddleware(); From 7447fbff9169d6bbdce7b55c8861c3c8864272d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Wed, 29 Nov 2017 19:47:29 +0100 Subject: [PATCH 223/456] Handle excessive data structures for multipart/form-data --- src/Io/MultipartParser.php | 41 ++++++++++ .../RequestBodyParserMiddlewareTest.php | 79 ++++++++++++++++++- 2 files changed, 118 insertions(+), 2 deletions(-) diff --git a/src/Io/MultipartParser.php b/src/Io/MultipartParser.php index d2088db5..55e7b971 100644 --- a/src/Io/MultipartParser.php +++ b/src/Io/MultipartParser.php @@ -27,6 +27,28 @@ final class MultipartParser */ protected $maxFileSize; + /** + * ini setting "max_input_vars" + * + * Does not exist in PHP < 5.3.9 or HHVM, so assume PHP's default 1000 here. + * + * @var int + * @link http://php.net/manual/en/info.configuration.php#ini.max-input-vars + */ + private $maxInputVars = 1000; + + /** + * ini setting "max_input_nesting_level" + * + * Does not exist in HHVM, but assumes hard coded to 64 (PHP's default). + * + * @var int + * @link http://php.net/manual/en/info.configuration.php#ini.max-input-nesting-level + */ + private $maxInputNestingLevel = 64; + + private $postCount = 0; + public static function parseRequest(ServerRequestInterface $request) { $parser = new self($request); @@ -36,6 +58,15 @@ public static function parseRequest(ServerRequestInterface $request) private function __construct(ServerRequestInterface $request) { $this->request = $request; + + $var = ini_get('max_input_vars'); + if ($var !== false) { + $this->maxInputVars = (int)$var; + } + $var = ini_get('max_input_nesting_level'); + if ($var !== false) { + $this->maxInputNestingLevel = (int)$var; + } } private function parse() @@ -150,6 +181,11 @@ private function parseUploadedFile($filename, $contentType, $contents) private function parsePost($name, $value) { + // ignore excessive number of post fields + if (++$this->postCount > $this->maxInputVars) { + return; + } + $this->request = $this->request->withParsedBody($this->extractPost( $this->request->getParsedBody(), $name, @@ -203,6 +239,11 @@ private function extractPost($postFields, $key, $value) return $postFields; } + // ignore this key if maximum nesting level is exceeded + if (isset($chunks[$this->maxInputNestingLevel])) { + return $postFields; + } + $chunkKey = rtrim($chunks[0], ']'); $parent = &$postFields; for ($i = 1; isset($chunks[$i]); $i++) { diff --git a/tests/Middleware/RequestBodyParserMiddlewareTest.php b/tests/Middleware/RequestBodyParserMiddlewareTest.php index a8755e27..dc818ee9 100644 --- a/tests/Middleware/RequestBodyParserMiddlewareTest.php +++ b/tests/Middleware/RequestBodyParserMiddlewareTest.php @@ -215,8 +215,7 @@ public function testMultipartFormDataParsing() $data .= "Content-Disposition: form-data; name=\"users[two]\"\r\n"; $data .= "\r\n"; $data .= "second\r\n"; - $data .= "--$boundary\r\n"; - + $data .= "--$boundary--\r\n"; $request = new ServerRequest('POST', 'http://example.com/', array( 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, @@ -241,4 +240,80 @@ function (ServerRequestInterface $request) { ); $this->assertSame($data, (string)$parsedRequest->getBody()); } + + public function testMultipartFormDataIgnoresFieldWithExcessiveNesting() + { + // supported in all Zend PHP versions and HHVM + // ini setting does exist everywhere but HHVM: https://3v4l.org/hXLiK + // HHVM limits to 64 and otherwise returns an empty array structure + $allowed = (int)ini_get('max_input_nesting_level'); + if ($allowed === 0) { + $allowed = 64; + } + + $middleware = new RequestBodyParserMiddleware(); + + $boundary = "---------------------------12758086162038677464950549563"; + + $data = "--$boundary\r\n"; + $data .= "Content-Disposition: form-data; name=\"hello" . str_repeat("[]", $allowed + 1) . "\"\r\n"; + $data .= "\r\n"; + $data .= "world\r\n"; + $data .= "--$boundary--\r\n"; + + $request = new ServerRequest('POST', 'http://example.com/', array( + 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, + ), $data, 1.1); + + /** @var ServerRequestInterface $parsedRequest */ + $parsedRequest = $middleware( + $request, + function (ServerRequestInterface $request) { + return $request; + } + ); + + $this->assertEmpty($parsedRequest->getParsedBody()); + } + + public function testMultipartFormDataTruncatesBodyWithExcessiveLength() + { + // ini setting exists in PHP 5.3.9, not in HHVM: https://3v4l.org/VF6oV + // otherwise default to 1000 as implemented within + $allowed = (int)ini_get('max_input_vars'); + if ($allowed === 0) { + $allowed = 1000; + } + + $middleware = new RequestBodyParserMiddleware(); + + $boundary = "---------------------------12758086162038677464950549563"; + + $data = ""; + for ($i = 0; $i < $allowed + 1; ++$i) { + $data .= "--$boundary\r\n"; + $data .= "Content-Disposition: form-data; name=\"a[]\"\r\n"; + $data .= "\r\n"; + $data .= "b\r\n"; + } + $data .= "--$boundary--\r\n"; + + $request = new ServerRequest('POST', 'http://example.com/', array( + 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, + ), $data, 1.1); + + /** @var ServerRequestInterface $parsedRequest */ + $parsedRequest = $middleware( + $request, + function (ServerRequestInterface $request) { + return $request; + } + ); + + $body = $parsedRequest->getParsedBody(); + + $this->assertCount(1, $body); + $this->assertTrue(isset($body['a'])); + $this->assertCount($allowed, $body['a']); + } } From 1f80ba2141857f646cfedc1fa27f6eaad4fd7964 Mon Sep 17 00:00:00 2001 From: Gabriel Caruso Date: Fri, 1 Dec 2017 10:58:22 -0200 Subject: [PATCH 224/456] Test against PHP 7.1 and 7.2 --- .travis.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 16b5d1c3..a881bc0f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,9 @@ php: - 5.4 - 5.5 - 5.6 - - 7 + - 7.0 + - 7.1 + - 7.2 - hhvm # ignore errors, see below # lock distro so new future defaults will not break the build From fef6ad4e1db56c5c44500e5a3c41188e089ca0b3 Mon Sep 17 00:00:00 2001 From: Gabriel Caruso Date: Fri, 1 Dec 2017 11:11:33 -0200 Subject: [PATCH 225/456] Refactoring tests --- tests/Io/HttpBodyStreamTest.php | 6 +++--- tests/Io/MultipartParserTest.php | 2 +- tests/Io/ServerRequestTest.php | 2 +- tests/StreamingServerTest.php | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/Io/HttpBodyStreamTest.php b/tests/Io/HttpBodyStreamTest.php index ad0a9131..343a75a5 100644 --- a/tests/Io/HttpBodyStreamTest.php +++ b/tests/Io/HttpBodyStreamTest.php @@ -88,12 +88,12 @@ public function testToString() public function testDetach() { - $this->assertEquals(null, $this->bodyStream->detach()); + $this->assertNull($this->bodyStream->detach()); } public function testGetSizeDefault() { - $this->assertEquals(null, $this->bodyStream->getSize()); + $this->assertNull($this->bodyStream->getSize()); } public function testGetSizeCustom() @@ -146,7 +146,7 @@ public function testGetContents() public function testGetMetaData() { - $this->assertEquals(null, $this->bodyStream->getMetadata()); + $this->assertNull($this->bodyStream->getMetadata()); } public function testIsReadable() diff --git a/tests/Io/MultipartParserTest.php b/tests/Io/MultipartParserTest.php index 30dbcf99..3a034afb 100644 --- a/tests/Io/MultipartParserTest.php +++ b/tests/Io/MultipartParserTest.php @@ -593,7 +593,7 @@ public function testInvalidUploadFileWithoutContentTypeUsesNullValue() $file = $files['file']; $this->assertSame('hello.txt', $file->getClientFilename()); - $this->assertSame(null, $file->getClientMediaType()); + $this->assertNull($file->getClientMediaType()); $this->assertSame(5, $file->getSize()); $this->assertSame(UPLOAD_ERR_OK, $file->getError()); $this->assertSame('world', (string)$file->getStream()); diff --git a/tests/Io/ServerRequestTest.php b/tests/Io/ServerRequestTest.php index 9c04a4ea..b9fc8444 100644 --- a/tests/Io/ServerRequestTest.php +++ b/tests/Io/ServerRequestTest.php @@ -40,7 +40,7 @@ public function testGetDefaultAttribute() $request = $this->request->withAttribute('hello', 'world'); $this->assertNotSame($request, $this->request); - $this->assertEquals(null, $request->getAttribute('hi', null)); + $this->assertNull($request->getAttribute('hi', null)); } public function testWithoutAttribute() diff --git a/tests/StreamingServerTest.php b/tests/StreamingServerTest.php index 69868437..3396c227 100644 --- a/tests/StreamingServerTest.php +++ b/tests/StreamingServerTest.php @@ -191,7 +191,7 @@ public function testRequestGetWithHostAndDefaultPortWillBeIgnored() $this->assertSame('/', $requestAssertion->getRequestTarget()); $this->assertSame('/', $requestAssertion->getUri()->getPath()); $this->assertSame('http://example.com/', (string)$requestAssertion->getUri()); - $this->assertSame(null, $requestAssertion->getUri()->getPort()); + $this->assertNull($requestAssertion->getUri()->getPort()); $this->assertSame('example.com', $requestAssertion->getHeaderLine('Host')); } @@ -290,7 +290,7 @@ public function testRequestConnectAuthorityFormWithDefaultPortWillBeIgnored() $this->assertSame('example.com:80', $requestAssertion->getRequestTarget()); $this->assertSame('', $requestAssertion->getUri()->getPath()); $this->assertSame('http://example.com', (string)$requestAssertion->getUri()); - $this->assertSame(null, $requestAssertion->getUri()->getPort()); + $this->assertNull($requestAssertion->getUri()->getPort()); $this->assertSame('example.com', $requestAssertion->getHeaderLine('Host')); } @@ -312,7 +312,7 @@ public function testRequestConnectAuthorityFormNonMatchingHostWillBeOverwritten( $this->assertSame('example.com:80', $requestAssertion->getRequestTarget()); $this->assertSame('', $requestAssertion->getUri()->getPath()); $this->assertSame('http://example.com', (string)$requestAssertion->getUri()); - $this->assertSame(null, $requestAssertion->getUri()->getPort()); + $this->assertNull($requestAssertion->getUri()->getPort()); $this->assertSame('example.com', $requestAssertion->getHeaderLine('Host')); } From 328fe95e8eed6c8677ac7e8168baf99da923dcc3 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Wed, 22 Nov 2017 21:46:06 +0100 Subject: [PATCH 226/456] RequestBodyParserMiddleware does not reject requests that exceed post_max_size --- README.md | 3 +- examples/12-upload.php | 2 +- .../RequestBodyBufferMiddleware.php | 24 +++++--- .../RequestBodyBufferMiddlewareTest.php | 58 +++++++++++++------ 4 files changed, 60 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index d667f196..2ea87e96 100644 --- a/README.md +++ b/README.md @@ -694,8 +694,7 @@ configuration. configuration in most cases.) Any incoming request that has a request body that exceeds this limit will be -rejected with a `413` (Request Entity Too Large) error message without calling -the next middleware handlers. +accepted but their request body will not be added to the request. The `RequestBodyBufferMiddleware` will buffer requests with bodies of known size (i.e. with `Content-Length` header specified) as well as requests with bodies of diff --git a/examples/12-upload.php b/examples/12-upload.php index 68c810ff..3fa82ec9 100644 --- a/examples/12-upload.php +++ b/examples/12-upload.php @@ -119,7 +119,7 @@ // buffer and parse HTTP request body before running our request handler $server = new StreamingServer(new MiddlewareRunner(array( - new RequestBodyBufferMiddleware(100000), // 100 KB max + new RequestBodyBufferMiddleware(100000), // 100 KB max, ignore body otherwise new RequestBodyParserMiddleware(), $handler ))); diff --git a/src/Middleware/RequestBodyBufferMiddleware.php b/src/Middleware/RequestBodyBufferMiddleware.php index b1d0ff47..030681fb 100644 --- a/src/Middleware/RequestBodyBufferMiddleware.php +++ b/src/Middleware/RequestBodyBufferMiddleware.php @@ -2,12 +2,12 @@ namespace React\Http\Middleware; +use OverflowException; use Psr\Http\Message\ServerRequestInterface; -use React\Http\Response; +use React\Promise\Promise; use React\Promise\Stream; use React\Stream\ReadableStreamInterface; use RingCentral\Psr7\BufferStream; -use OverflowException; final class RequestBodyBufferMiddleware { @@ -30,27 +30,37 @@ public function __construct($sizeLimit = null) public function __invoke(ServerRequestInterface $request, $stack) { + $sizeLimit = $this->sizeLimit; $body = $request->getBody(); // request body of known size exceeding limit if ($body->getSize() > $this->sizeLimit) { - return new Response(413, array('Content-Type' => 'text/plain'), 'Request body exceeds allowed limit'); + $sizeLimit = 0; } if (!$body instanceof ReadableStreamInterface) { return $stack($request); } - return Stream\buffer($body, $this->sizeLimit)->then(function ($buffer) use ($request, $stack) { + return Stream\buffer($body, $sizeLimit)->then(function ($buffer) use ($request, $stack) { $stream = new BufferStream(strlen($buffer)); $stream->write($buffer); $request = $request->withBody($stream); return $stack($request); - }, function($error) { - // request body of unknown size exceeding limit during buffering + }, function ($error) use ($stack, $request, $body) { + // On buffer overflow keep the request body stream in, + // but ignore the contents and wait for the close event + // before passing the request on to the next middleware. if ($error instanceof OverflowException) { - return new Response(413, array('Content-Type' => 'text/plain'), 'Request body exceeds allowed limit'); + return new Promise(function ($resolve, $reject) use ($stack, $request, $body) { + $body->on('error', function ($error) use ($reject) { + $reject($error); + }); + $body->on('close', function () use ($stack, $request, $resolve) { + $resolve($stack($request)); + }); + }); } throw $error; diff --git a/tests/Middleware/RequestBodyBufferMiddlewareTest.php b/tests/Middleware/RequestBodyBufferMiddlewareTest.php index 0e682435..2e480991 100644 --- a/tests/Middleware/RequestBodyBufferMiddlewareTest.php +++ b/tests/Middleware/RequestBodyBufferMiddlewareTest.php @@ -2,15 +2,16 @@ namespace React\Tests\Http\Middleware; +use Clue\React\Block; use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Factory; use React\Http\Io\HttpBodyStream; use React\Http\Io\ServerRequest; use React\Http\Middleware\RequestBodyBufferMiddleware; +use React\Http\Response; use React\Stream\ThroughStream; use React\Tests\Http\TestCase; use RingCentral\Psr7\BufferStream; -use Clue\React\Block; final class RequestBodyBufferMiddlewareTest extends TestCase { @@ -65,7 +66,7 @@ function (ServerRequestInterface $request) use (&$exposedRequest) { $this->assertSame($body, $exposedRequest->getBody()->getContents()); } - public function testExcessiveSizeImmediatelyReturnsError413ForKnownSize() + public function testKnownExcessiveSizedBodyIsDisgardedTheRequestIsPassedDownToTheNextMiddleware() { $loop = Factory::create(); @@ -79,17 +80,18 @@ public function testExcessiveSizeImmediatelyReturnsError413ForKnownSize() ); $buffer = new RequestBodyBufferMiddleware(1); - $response = $buffer( + $response = Block\await($buffer( $serverRequest, function (ServerRequestInterface $request) { - return $request; + return new Response(200, array(), $request->getBody()->getContents()); } - ); + ), $loop); - $this->assertSame(413, $response->getStatusCode()); + $this->assertSame(200, $response->getStatusCode()); + $this->assertSame('', $response->getBody()->getContents()); } - public function testExcessiveSizeReturnsError413() + public function testExcessiveSizeBodyIsDiscardedAndTheRequestIsPassedDownToTheNextMiddleware() { $loop = Factory::create(); @@ -105,23 +107,19 @@ public function testExcessiveSizeReturnsError413() $promise = $buffer( $serverRequest, function (ServerRequestInterface $request) { - return $request; + return new Response(200, array(), $request->getBody()->getContents()); } ); $stream->end('aa'); - $exposedResponse = null; - $promise->then( - function($response) use (&$exposedResponse) { - $exposedResponse = $response; - }, + $exposedResponse = Block\await($promise->then( + null, $this->expectCallableNever() - ); - - $this->assertSame(413, $exposedResponse->getStatusCode()); + ), $loop); - Block\await($promise, $loop); + $this->assertSame(200, $exposedResponse->getStatusCode()); + $this->assertSame('', $exposedResponse->getBody()->getContents()); } /** @@ -151,4 +149,30 @@ function (ServerRequestInterface $request) { Block\await($promise, $loop); } + + public function testFullBodyStreamedBeforeCallingNextMiddleware() + { + $promiseResolved = false; + $middleware = new RequestBodyBufferMiddleware(3); + $stream = new ThroughStream(); + $serverRequest = new ServerRequest( + 'GET', + 'https://example.com/', + array(), + new HttpBodyStream($stream, null) + ); + + $middleware($serverRequest, function () { + return new Response(); + })->then(function () use (&$promiseResolved) { + $promiseResolved = true; + }); + + $stream->write('aaa'); + $this->assertFalse($promiseResolved); + $stream->write('aaa'); + $this->assertFalse($promiseResolved); + $stream->end('aaa'); + $this->assertTrue($promiseResolved); + } } From 7c9a6ec397868ae5336571b77971b7a9f049d203 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Tue, 28 Nov 2017 17:42:23 +0100 Subject: [PATCH 227/456] Use Stream\first() instead of wrapping our own promise around it --- composer.json | 2 +- src/Middleware/RequestBodyBufferMiddleware.php | 10 ++-------- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/composer.json b/composer.json index f6a4fe24..c3d46a9f 100644 --- a/composer.json +++ b/composer.json @@ -10,7 +10,7 @@ "react/stream": "^1.0 || ^0.7.1", "react/promise": "^2.3 || ^1.2.1", "evenement/evenement": "^3.0 || ^2.0 || ^1.0", - "react/promise-stream": "^1.0 || ^0.1.2" + "react/promise-stream": "^1.1" }, "autoload": { "psr-4": { diff --git a/src/Middleware/RequestBodyBufferMiddleware.php b/src/Middleware/RequestBodyBufferMiddleware.php index 030681fb..c7ec5289 100644 --- a/src/Middleware/RequestBodyBufferMiddleware.php +++ b/src/Middleware/RequestBodyBufferMiddleware.php @@ -4,7 +4,6 @@ use OverflowException; use Psr\Http\Message\ServerRequestInterface; -use React\Promise\Promise; use React\Promise\Stream; use React\Stream\ReadableStreamInterface; use RingCentral\Psr7\BufferStream; @@ -53,13 +52,8 @@ public function __invoke(ServerRequestInterface $request, $stack) // but ignore the contents and wait for the close event // before passing the request on to the next middleware. if ($error instanceof OverflowException) { - return new Promise(function ($resolve, $reject) use ($stack, $request, $body) { - $body->on('error', function ($error) use ($reject) { - $reject($error); - }); - $body->on('close', function () use ($stack, $request, $resolve) { - $resolve($stack($request)); - }); + return Stream\first($body, 'close')->then(function () use ($stack, $request) { + return $stack($request); }); } From 802ee34158ef42d1bf6abea62040f86432333421 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 8 Dec 2017 14:10:47 +0100 Subject: [PATCH 228/456] Discard buffered request body if size limit is exceeded --- README.md | 11 +++++++- .../RequestBodyBufferMiddleware.php | 16 +++++++---- .../RequestBodyBufferMiddlewareTest.php | 28 +++++++++++++++++-- 3 files changed, 47 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 2ea87e96..7ee90d93 100644 --- a/README.md +++ b/README.md @@ -694,7 +694,14 @@ configuration. configuration in most cases.) Any incoming request that has a request body that exceeds this limit will be -accepted but their request body will not be added to the request. +accepted, but its request body will be discarded (empty request body). +This is done in order to avoid having to keep an incoming request with an +excessive size (for example, think of a 2 GB file upload) in memory. +This allows the next middleware handler to still handle this request, but it +will see an empty request body. +This is similar to PHP's default behavior, where the body will not be parsed +if this limit is exceeded. However, unlike PHP's default behavior, the raw +request body is not available via `php://input`. The `RequestBodyBufferMiddleware` will buffer requests with bodies of known size (i.e. with `Content-Length` header specified) as well as requests with bodies of @@ -781,6 +788,8 @@ See also [example #12](examples) for more details. handler as given in the example above. This previous middleware handler is also responsible for rejecting incoming requests that exceed allowed message sizes (such as big file uploads). + The [`RequestBodyBufferMiddleware`](#requestbodybuffermiddleware) used above + simply discards excessive request bodies, resulting in an empty body. If you use this middleware without buffering first, it will try to parse an empty (streaming) body and may thus assume an empty data structure. See also [`RequestBodyBufferMiddleware`](#requestbodybuffermiddleware) for diff --git a/src/Middleware/RequestBodyBufferMiddleware.php b/src/Middleware/RequestBodyBufferMiddleware.php index c7ec5289..7ae3f894 100644 --- a/src/Middleware/RequestBodyBufferMiddleware.php +++ b/src/Middleware/RequestBodyBufferMiddleware.php @@ -29,18 +29,24 @@ public function __construct($sizeLimit = null) public function __invoke(ServerRequestInterface $request, $stack) { - $sizeLimit = $this->sizeLimit; $body = $request->getBody(); + // skip if body is already buffered + if (!$body instanceof ReadableStreamInterface) { + // replace with empty buffer if size limit is exceeded + if ($body->getSize() > $this->sizeLimit) { + $request = $request->withBody(new BufferStream(0)); + } + + return $stack($request); + } + // request body of known size exceeding limit + $sizeLimit = $this->sizeLimit; if ($body->getSize() > $this->sizeLimit) { $sizeLimit = 0; } - if (!$body instanceof ReadableStreamInterface) { - return $stack($request); - } - return Stream\buffer($body, $sizeLimit)->then(function ($buffer) use ($request, $stack) { $stream = new BufferStream(strlen($buffer)); $stream->write($buffer); diff --git a/tests/Middleware/RequestBodyBufferMiddlewareTest.php b/tests/Middleware/RequestBodyBufferMiddlewareTest.php index 2e480991..91bd14cc 100644 --- a/tests/Middleware/RequestBodyBufferMiddlewareTest.php +++ b/tests/Middleware/RequestBodyBufferMiddlewareTest.php @@ -38,6 +38,7 @@ function (ServerRequestInterface $request) use (&$exposedRequest) { $stream->write('world'); $stream->end('!'); + $this->assertSame(11, $exposedRequest->getBody()->getSize()); $this->assertSame('helloworld!', $exposedRequest->getBody()->getContents()); } @@ -63,13 +64,14 @@ function (ServerRequestInterface $request) use (&$exposedRequest) { } ); + $this->assertSame($size, $exposedRequest->getBody()->getSize()); $this->assertSame($body, $exposedRequest->getBody()->getContents()); } public function testKnownExcessiveSizedBodyIsDisgardedTheRequestIsPassedDownToTheNextMiddleware() { $loop = Factory::create(); - + $stream = new ThroughStream(); $stream->end('aa'); $serverRequest = new ServerRequest( @@ -91,6 +93,28 @@ function (ServerRequestInterface $request) { $this->assertSame('', $response->getBody()->getContents()); } + public function testAlreadyBufferedExceedingSizeResolvesImmediatelyWithEmptyBody() + { + $serverRequest = new ServerRequest( + 'GET', + 'https://example.com/', + array(), + 'hello' + ); + + $exposedRequest = null; + $buffer = new RequestBodyBufferMiddleware(1); + $buffer( + $serverRequest, + function (ServerRequestInterface $request) use (&$exposedRequest) { + $exposedRequest = $request; + } + ); + + $this->assertSame(0, $exposedRequest->getBody()->getSize()); + $this->assertSame('', $exposedRequest->getBody()->getContents()); + } + public function testExcessiveSizeBodyIsDiscardedAndTheRequestIsPassedDownToTheNextMiddleware() { $loop = Factory::create(); @@ -128,7 +152,7 @@ function (ServerRequestInterface $request) { public function testBufferingErrorThrows() { $loop = Factory::create(); - + $stream = new ThroughStream(); $serverRequest = new ServerRequest( 'GET', From 837e67d7b2ec1882cd15c9f2fbeb4b782373b0b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 30 Nov 2017 14:57:01 +0100 Subject: [PATCH 229/456] Internal refactoring to instantiate and inject MultipartParser --- src/Io/MultipartParser.php | 26 +++---- .../RequestBodyParserMiddleware.php | 9 ++- tests/Io/MultipartParserTest.php | 75 ++++++++++++------- 3 files changed, 70 insertions(+), 40 deletions(-) diff --git a/src/Io/MultipartParser.php b/src/Io/MultipartParser.php index 55e7b971..d5297164 100644 --- a/src/Io/MultipartParser.php +++ b/src/Io/MultipartParser.php @@ -49,16 +49,8 @@ final class MultipartParser private $postCount = 0; - public static function parseRequest(ServerRequestInterface $request) + public function __construct() { - $parser = new self($request); - return $parser->parse(); - } - - private function __construct(ServerRequestInterface $request) - { - $this->request = $request; - $var = ini_get('max_input_vars'); if ($var !== false) { $this->maxInputVars = (int)$var; @@ -69,16 +61,22 @@ private function __construct(ServerRequestInterface $request) } } - private function parse() + public function parse(ServerRequestInterface $request) { - $contentType = $this->request->getHeaderLine('content-type'); + $contentType = $request->getHeaderLine('content-type'); if(!preg_match('/boundary="?(.*)"?$/', $contentType, $matches)) { - return $this->request; + return $request; } - $this->parseBody('--' . $matches[1], (string)$this->request->getBody()); + $this->request = $request; + $this->parseBody('--' . $matches[1], (string)$request->getBody()); + + $request = $this->request; + $this->request = null; + $this->postCount = 0; + $this->maxFileSize = null; - return $this->request; + return $request; } private function parseBody($boundary, $buffer) diff --git a/src/Middleware/RequestBodyParserMiddleware.php b/src/Middleware/RequestBodyParserMiddleware.php index 0c0544eb..639f2b00 100644 --- a/src/Middleware/RequestBodyParserMiddleware.php +++ b/src/Middleware/RequestBodyParserMiddleware.php @@ -7,6 +7,13 @@ final class RequestBodyParserMiddleware { + private $multipart; + + public function __construct() + { + $this->multipart = new MultipartParser(); + } + public function __invoke(ServerRequestInterface $request, $next) { $type = strtolower($request->getHeaderLine('Content-Type')); @@ -17,7 +24,7 @@ public function __invoke(ServerRequestInterface $request, $next) } if ($type === 'multipart/form-data') { - return $next(MultipartParser::parseRequest($request)); + return $next($this->multipart->parse($request)); } return $next($request); diff --git a/tests/Io/MultipartParserTest.php b/tests/Io/MultipartParserTest.php index 3a034afb..44c195d6 100644 --- a/tests/Io/MultipartParserTest.php +++ b/tests/Io/MultipartParserTest.php @@ -26,7 +26,8 @@ public function testDoesNotParseWithoutMultipartFormDataContentType() 'Content-Type' => 'multipart/form-data', ), $data, 1.1); - $parsedRequest = MultipartParser::parseRequest($request); + $parser = new MultipartParser(); + $parsedRequest = $parser->parse($request); $this->assertEmpty($parsedRequest->getUploadedFiles()); $this->assertEmpty($parsedRequest->getParsedBody()); @@ -50,7 +51,8 @@ public function testPostKey() 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, ), $data, 1.1); - $parsedRequest = MultipartParser::parseRequest($request); + $parser = new MultipartParser(); + $parsedRequest = $parser->parse($request); $this->assertEmpty($parsedRequest->getUploadedFiles()); $this->assertSame( @@ -82,7 +84,8 @@ public function testPostStringOverwritesMap() 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, ), $data, 1.1); - $parsedRequest = MultipartParser::parseRequest($request); + $parser = new MultipartParser(); + $parsedRequest = $parser->parse($request); $this->assertEmpty($parsedRequest->getUploadedFiles()); $this->assertSame( @@ -111,7 +114,8 @@ public function testPostMapOverwritesString() 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, ), $data, 1.1); - $parsedRequest = MultipartParser::parseRequest($request); + $parser = new MultipartParser(); + $parsedRequest = $parser->parse($request); $this->assertEmpty($parsedRequest->getUploadedFiles()); $this->assertSame( @@ -142,7 +146,8 @@ public function testPostVectorOverwritesString() 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, ), $data, 1.1); - $parsedRequest = MultipartParser::parseRequest($request); + $parser = new MultipartParser(); + $parsedRequest = $parser->parse($request); $this->assertEmpty($parsedRequest->getUploadedFiles()); $this->assertSame( @@ -173,7 +178,8 @@ public function testPostDeeplyNestedArray() 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, ), $data, 1.1); - $parsedRequest = MultipartParser::parseRequest($request); + $parser = new MultipartParser(); + $parsedRequest = $parser->parse($request); $this->assertEmpty($parsedRequest->getUploadedFiles()); $this->assertSame( @@ -205,7 +211,8 @@ public function testEmptyPostValue() 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, ), $data, 1.1); - $parsedRequest = MultipartParser::parseRequest($request); + $parser = new MultipartParser(); + $parsedRequest = $parser->parse($request); $this->assertEmpty($parsedRequest->getUploadedFiles()); $this->assertSame( @@ -230,7 +237,8 @@ public function testEmptyPostKey() 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, ), $data, 1.1); - $parsedRequest = MultipartParser::parseRequest($request); + $parser = new MultipartParser(); + $parsedRequest = $parser->parse($request); $this->assertEmpty($parsedRequest->getUploadedFiles()); $this->assertSame( @@ -255,7 +263,8 @@ public function testNestedPostKeyAssoc() 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, ), $data, 1.1); - $parsedRequest = MultipartParser::parseRequest($request); + $parser = new MultipartParser(); + $parsedRequest = $parser->parse($request); $this->assertEmpty($parsedRequest->getUploadedFiles()); $this->assertSame( @@ -284,7 +293,8 @@ public function testNestedPostKeyVector() 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, ), $data, 1.1); - $parsedRequest = MultipartParser::parseRequest($request); + $parser = new MultipartParser(); + $parsedRequest = $parser->parse($request); $this->assertEmpty($parsedRequest->getUploadedFiles()); $this->assertSame( @@ -364,7 +374,8 @@ public function testFileUpload() 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, ), $data, 1.1); - $parsedRequest = MultipartParser::parseRequest($request); + $parser = new MultipartParser(); + $parsedRequest = $parser->parse($request); $this->assertSame( array( @@ -418,7 +429,8 @@ public function testInvalidDoubleContentDispositionUsesLast() 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, ), $data, 1.1); - $parsedRequest = MultipartParser::parseRequest($request); + $parser = new MultipartParser(); + $parsedRequest = $parser->parse($request); $this->assertEmpty($parsedRequest->getUploadedFiles()); $this->assertSame( @@ -443,7 +455,8 @@ public function testInvalidMissingNewlineAfterValueWillBeIgnored() 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, ), $data, 1.1); - $parsedRequest = MultipartParser::parseRequest($request); + $parser = new MultipartParser(); + $parsedRequest = $parser->parse($request); $this->assertEmpty($parsedRequest->getUploadedFiles()); $this->assertEmpty($parsedRequest->getParsedBody()); @@ -462,7 +475,8 @@ public function testInvalidMissingValueWillBeIgnored() 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, ), $data, 1.1); - $parsedRequest = MultipartParser::parseRequest($request); + $parser = new MultipartParser(); + $parsedRequest = $parser->parse($request); $this->assertEmpty($parsedRequest->getUploadedFiles()); $this->assertEmpty($parsedRequest->getParsedBody()); @@ -479,7 +493,8 @@ public function testInvalidMissingValueAndEndBoundaryWillBeIgnored() 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, ), $data, 1.1); - $parsedRequest = MultipartParser::parseRequest($request); + $parser = new MultipartParser(); + $parsedRequest = $parser->parse($request); $this->assertEmpty($parsedRequest->getUploadedFiles()); $this->assertEmpty($parsedRequest->getParsedBody()); @@ -499,7 +514,8 @@ public function testInvalidContentDispositionMissingWillBeIgnored() 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, ), $data, 1.1); - $parsedRequest = MultipartParser::parseRequest($request); + $parser = new MultipartParser(); + $parsedRequest = $parser->parse($request); $this->assertEmpty($parsedRequest->getUploadedFiles()); $this->assertEmpty($parsedRequest->getParsedBody()); @@ -519,7 +535,8 @@ public function testInvalidContentDispositionMissingValueWillBeIgnored() 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, ), $data, 1.1); - $parsedRequest = MultipartParser::parseRequest($request); + $parser = new MultipartParser(); + $parsedRequest = $parser->parse($request); $this->assertEmpty($parsedRequest->getUploadedFiles()); $this->assertEmpty($parsedRequest->getParsedBody()); @@ -539,7 +556,8 @@ public function testInvalidContentDispositionWithoutNameWillBeIgnored() 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, ), $data, 1.1); - $parsedRequest = MultipartParser::parseRequest($request); + $parser = new MultipartParser(); + $parsedRequest = $parser->parse($request); $this->assertEmpty($parsedRequest->getUploadedFiles()); $this->assertEmpty($parsedRequest->getParsedBody()); @@ -558,7 +576,8 @@ public function testInvalidMissingEndBoundaryWillBeIgnored() 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, ), $data, 1.1); - $parsedRequest = MultipartParser::parseRequest($request); + $parser = new MultipartParser(); + $parsedRequest = $parser->parse($request); $this->assertEmpty($parsedRequest->getUploadedFiles()); $this->assertSame( @@ -581,7 +600,8 @@ public function testInvalidUploadFileWithoutContentTypeUsesNullValue() 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, ), $data, 1.1); - $parsedRequest = MultipartParser::parseRequest($request); + $parser = new MultipartParser(); + $parsedRequest = $parser->parse($request); $files = $parsedRequest->getUploadedFiles(); @@ -615,7 +635,8 @@ public function testInvalidUploadFileWithoutMultipleContentTypeUsesLastValue() 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, ), $data, 1.1); - $parsedRequest = MultipartParser::parseRequest($request); + $parser = new MultipartParser(); + $parsedRequest = $parser->parse($request); $files = $parsedRequest->getUploadedFiles(); @@ -648,7 +669,8 @@ public function testUploadEmptyFile() 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, ), $data, 1.1); - $parsedRequest = MultipartParser::parseRequest($request); + $parser = new MultipartParser(); + $parsedRequest = $parser->parse($request); $files = $parsedRequest->getUploadedFiles(); @@ -681,7 +703,8 @@ public function testUploadNoFile() 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, ), $data, 1.1); - $parsedRequest = MultipartParser::parseRequest($request); + $parser = new MultipartParser(); + $parsedRequest = $parser->parse($request); $files = $parsedRequest->getUploadedFiles(); @@ -718,7 +741,8 @@ public function testPostMaxFileSize() 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, ), $data, 1.1); - $parsedRequest = MultipartParser::parseRequest($request); + $parser = new MultipartParser(); + $parsedRequest = $parser->parse($request); $files = $parsedRequest->getUploadedFiles(); @@ -772,7 +796,8 @@ public function testPostMaxFileSizeIgnoredByFilesComingBeforeIt() 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, ), $data, 1.1); - $parsedRequest = MultipartParser::parseRequest($request); + $parser = new MultipartParser(); + $parsedRequest = $parser->parse($request); $files = $parsedRequest->getUploadedFiles(); From e24e59d9dc54ac3a29b80bf488efbc1881e09fbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 4 Dec 2017 20:58:24 +0100 Subject: [PATCH 230/456] Respect upload_max_filesize ini setting --- README.md | 4 ++++ examples/12-upload.php | 2 ++ src/Io/MultipartParser.php | 44 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 50 insertions(+) diff --git a/README.md b/README.md index 7ee90d93..9cb7730b 100644 --- a/README.md +++ b/README.md @@ -757,6 +757,8 @@ $handler = function (ServerRequestInterface $request) { if ($avatar instanceof UploadedFileInterface) { if ($avatar->getError() === UPLOAD_ERR_OK) { $uploaded = $avatar->getSize() . ' bytes'; + } elseif ($avatar->getError() === UPLOAD_ERR_INI_SIZE) { + $uploaded = 'file too large'; } else { $uploaded = 'with error'; } @@ -798,6 +800,8 @@ See also [example #12](examples) for more details. > PHP's `MAX_FILE_SIZE` hidden field is respected by this middleware. > This middleware respects the + [`upload_max_filesize`](http://php.net/manual/en/ini.core.php#ini.upload-max-filesize) + (default `2M`), [`max_input_vars`](http://php.net/manual/en/info.configuration.php#ini.max-input-vars) (default `1000`) and [`max_input_nesting_level`](http://php.net/manual/en/info.configuration.php#ini.max-input-nesting-level) diff --git a/examples/12-upload.php b/examples/12-upload.php index 3fa82ec9..420b2a76 100644 --- a/examples/12-upload.php +++ b/examples/12-upload.php @@ -42,6 +42,8 @@ // contents via `(string)$file->getStream()` instead. // Here, we simply use an inline image to send back to client: $avatar = ' (' . $file->getSize() . ' bytes)'; + } elseif ($file->getError() === UPLOAD_ERR_INI_SIZE) { + $avatar = 'upload exceeds file size limit'; } else { // Real applications should probably check the error number and // should print some human-friendly text diff --git a/src/Io/MultipartParser.php b/src/Io/MultipartParser.php index d5297164..b2e2ef37 100644 --- a/src/Io/MultipartParser.php +++ b/src/Io/MultipartParser.php @@ -47,6 +47,13 @@ final class MultipartParser */ private $maxInputNestingLevel = 64; + /** + * ini setting "upload_max_filesize" + * + * @var int + */ + private $uploadMaxFilesize; + private $postCount = 0; public function __construct() @@ -59,6 +66,8 @@ public function __construct() if ($var !== false) { $this->maxInputNestingLevel = (int)$var; } + + $this->uploadMaxFilesize = $this->iniUploadMaxFilesize(); } public function parse(ServerRequestInterface $request) @@ -157,6 +166,17 @@ private function parseUploadedFile($filename, $contentType, $contents) ); } + // file exceeds "upload_max_filesize" ini setting + if ($size > $this->uploadMaxFilesize) { + return new UploadedFile( + Psr7\stream_for(''), + $size, + UPLOAD_ERR_INI_SIZE, + $filename, + $contentType + ); + } + // file exceeds MAX_FILE_SIZE value if ($this->maxFileSize !== null && $size > $this->maxFileSize) { return new UploadedFile( @@ -269,4 +289,28 @@ private function extractPost($postFields, $key, $value) return $postFields; } + + /** + * Gets upload_max_filesize from PHP's configuration expressed in bytes + * + * @return int + * @link http://php.net/manual/en/ini.core.php#ini.upload-max-filesize + * @codeCoverageIgnore + */ + private function iniUploadMaxFilesize() + { + $size = ini_get('upload_max_filesize'); + $suffix = strtoupper(substr($size, -1)); + if ($suffix === 'K') { + return substr($size, 0, -1) * 1024; + } + if ($suffix === 'M') { + return substr($size, 0, -1) * 1024 * 1024; + } + if ($suffix === 'G') { + return substr($size, 0, -1) * 1024 * 1024 * 1024; + } + + return $size; + } } From 73569a93e43655b8abc0cce3b59ecce251945766 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 8 Dec 2017 15:32:54 +0100 Subject: [PATCH 231/456] Add parameter to control maximum file upload size --- README.md | 15 +++++++-- examples/12-upload.php | 4 +-- src/Io/MultipartParser.php | 7 ++-- .../RequestBodyParserMiddleware.php | 7 ++-- tests/Io/MultipartParserTest.php | 33 +++++++++++++++++++ 5 files changed, 58 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 9cb7730b..2c8e23d5 100644 --- a/README.md +++ b/README.md @@ -784,6 +784,18 @@ $server = new StreamingServer(new MiddlewareRunner([ See also [example #12](examples) for more details. +By default, this middleware respects the +[`upload_max_filesize`](http://php.net/manual/en/ini.core.php#ini.upload-max-filesize) +(default `2M`) ini setting. +Files that exceed this limit will be rejected with an `UPLOAD_ERR_INI_SIZE` error. +You can control the maximum filesize for each individual file upload by +explicitly passing the maximum filesize in bytes as the first parameter to the +constructor like this: + +```php +new RequestBodyParserMiddleware(8 * 1024 * 1024); // 8 MiB limit per file +``` + > Note that this middleware handler simply parses everything that is already buffered in the request body. It is imperative that the request body is buffered by a prior middleware @@ -798,10 +810,9 @@ See also [example #12](examples) for more details. more details. > PHP's `MAX_FILE_SIZE` hidden field is respected by this middleware. + Files that exceed this limit will be rejected with an `UPLOAD_ERR_FORM_SIZE` error. > This middleware respects the - [`upload_max_filesize`](http://php.net/manual/en/ini.core.php#ini.upload-max-filesize) - (default `2M`), [`max_input_vars`](http://php.net/manual/en/info.configuration.php#ini.max-input-vars) (default `1000`) and [`max_input_nesting_level`](http://php.net/manual/en/info.configuration.php#ini.max-input-nesting-level) diff --git a/examples/12-upload.php b/examples/12-upload.php index 420b2a76..0cb6d3ec 100644 --- a/examples/12-upload.php +++ b/examples/12-upload.php @@ -121,8 +121,8 @@ // buffer and parse HTTP request body before running our request handler $server = new StreamingServer(new MiddlewareRunner(array( - new RequestBodyBufferMiddleware(100000), // 100 KB max, ignore body otherwise - new RequestBodyParserMiddleware(), + new RequestBodyBufferMiddleware(8 * 1024 * 1024), // 8 MiB max, ignore body otherwise + new RequestBodyParserMiddleware(100 * 1024), // 100 KiB max, reject upload otherwise $handler ))); diff --git a/src/Io/MultipartParser.php b/src/Io/MultipartParser.php index b2e2ef37..bd6a7b0a 100644 --- a/src/Io/MultipartParser.php +++ b/src/Io/MultipartParser.php @@ -56,7 +56,10 @@ final class MultipartParser private $postCount = 0; - public function __construct() + /** + * @param int|null $uploadMaxFilesize + */ + public function __construct($uploadMaxFilesize = null) { $var = ini_get('max_input_vars'); if ($var !== false) { @@ -67,7 +70,7 @@ public function __construct() $this->maxInputNestingLevel = (int)$var; } - $this->uploadMaxFilesize = $this->iniUploadMaxFilesize(); + $this->uploadMaxFilesize = $uploadMaxFilesize === null ? $this->iniUploadMaxFilesize() : (int)$uploadMaxFilesize; } public function parse(ServerRequestInterface $request) diff --git a/src/Middleware/RequestBodyParserMiddleware.php b/src/Middleware/RequestBodyParserMiddleware.php index 639f2b00..2e02a859 100644 --- a/src/Middleware/RequestBodyParserMiddleware.php +++ b/src/Middleware/RequestBodyParserMiddleware.php @@ -9,9 +9,12 @@ final class RequestBodyParserMiddleware { private $multipart; - public function __construct() + /** + * @param int|null $uploadMaxFilesize + */ + public function __construct($uploadMaxFilesize = null) { - $this->multipart = new MultipartParser(); + $this->multipart = new MultipartParser($uploadMaxFilesize); } public function __invoke(ServerRequestInterface $request, $next) diff --git a/tests/Io/MultipartParserTest.php b/tests/Io/MultipartParserTest.php index 44c195d6..b982560d 100644 --- a/tests/Io/MultipartParserTest.php +++ b/tests/Io/MultipartParserTest.php @@ -688,6 +688,39 @@ public function testUploadEmptyFile() $this->assertSame('', (string)$file->getStream()); } + public function testUploadTooLargeFile() + { + $boundary = "---------------------------12758086162038677464950549563"; + + $data = "--$boundary\r\n"; + $data .= "Content-Disposition: form-data; name=\"file\"; filename=\"hello\"\r\n"; + $data .= "Content-type: text/plain\r\n"; + $data .= "\r\n"; + $data .= "world\r\n"; + $data .= "--$boundary--\r\n"; + + $request = new ServerRequest('POST', 'http://example.com/', array( + 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, + ), $data, 1.1); + + $parser = new MultipartParser(4); + $parsedRequest = $parser->parse($request); + + $files = $parsedRequest->getUploadedFiles(); + + $this->assertCount(1, $files); + $this->assertTrue(isset($files['file'])); + $this->assertInstanceOf('Psr\Http\Message\UploadedFileInterface', $files['file']); + + /* @var $file \Psr\Http\Message\UploadedFileInterface */ + $file = $files['file']; + + $this->assertSame('hello', $file->getClientFilename()); + $this->assertSame('text/plain', $file->getClientMediaType()); + $this->assertSame(5, $file->getSize()); + $this->assertSame(UPLOAD_ERR_INI_SIZE, $file->getError()); + } + public function testUploadNoFile() { $boundary = "---------------------------12758086162038677464950549563"; From 85fd8f9cf96f652eb383d4d40a1f2964ed8761a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 8 Dec 2017 16:39:32 +0100 Subject: [PATCH 232/456] Add parameter to control maximum number of file uploads per request --- README.md | 12 +++++++ examples/12-upload.php | 2 +- src/Io/MultipartParser.php | 25 +++++++++++-- .../RequestBodyParserMiddleware.php | 5 +-- tests/Io/MultipartParserTest.php | 36 +++++++++++++++++++ 5 files changed, 75 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 2c8e23d5..f7726bba 100644 --- a/README.md +++ b/README.md @@ -796,6 +796,18 @@ constructor like this: new RequestBodyParserMiddleware(8 * 1024 * 1024); // 8 MiB limit per file ``` +By default, this middleware respects the +[`max_file_uploads`](http://php.net/manual/en/ini.core.php#ini.max-file-uploads) +(default `20`) ini setting. +If you upload more files in a single request, additional files will be ignored +and the `getUploadedFiles()` method returns a truncated array. +You can control the maximum number of file uploads per request by explicitly +passing the second parameter to the constructor like this: + +```php +new RequestBodyParserMiddleware(10 * 1024, 100); // 100 files with 10 KiB each +``` + > Note that this middleware handler simply parses everything that is already buffered in the request body. It is imperative that the request body is buffered by a prior middleware diff --git a/examples/12-upload.php b/examples/12-upload.php index 0cb6d3ec..5e9c84db 100644 --- a/examples/12-upload.php +++ b/examples/12-upload.php @@ -122,7 +122,7 @@ // buffer and parse HTTP request body before running our request handler $server = new StreamingServer(new MiddlewareRunner(array( new RequestBodyBufferMiddleware(8 * 1024 * 1024), // 8 MiB max, ignore body otherwise - new RequestBodyParserMiddleware(100 * 1024), // 100 KiB max, reject upload otherwise + new RequestBodyParserMiddleware(100 * 1024, 1), // 1 file with 100 KiB max, reject upload otherwise $handler ))); diff --git a/src/Io/MultipartParser.php b/src/Io/MultipartParser.php index bd6a7b0a..3eaf9b69 100644 --- a/src/Io/MultipartParser.php +++ b/src/Io/MultipartParser.php @@ -54,12 +54,21 @@ final class MultipartParser */ private $uploadMaxFilesize; + /** + * ini setting "max_file_uploads" + * + * @var int + */ + private $maxFileUploads; + private $postCount = 0; + private $filesCount = 0; /** * @param int|null $uploadMaxFilesize + * @param int|null $maxFileUploads */ - public function __construct($uploadMaxFilesize = null) + public function __construct($uploadMaxFilesize = null, $maxFileUploads = null) { $var = ini_get('max_input_vars'); if ($var !== false) { @@ -71,6 +80,7 @@ public function __construct($uploadMaxFilesize = null) } $this->uploadMaxFilesize = $uploadMaxFilesize === null ? $this->iniUploadMaxFilesize() : (int)$uploadMaxFilesize; + $this->maxFileUploads = $maxFileUploads === null ? (int)ini_get('max_file_uploads') : (int)$maxFileUploads; } public function parse(ServerRequestInterface $request) @@ -86,6 +96,7 @@ public function parse(ServerRequestInterface $request) $request = $this->request; $this->request = null; $this->postCount = 0; + $this->filesCount = 0; $this->maxFileSize = null; return $request; @@ -147,15 +158,25 @@ private function parsePart($chunk) private function parseFile($name, $filename, $contentType, $contents) { + $file = $this->parseUploadedFile($filename, $contentType, $contents); + if ($file === null) { + return; + } + $this->request = $this->request->withUploadedFiles($this->extractPost( $this->request->getUploadedFiles(), $name, - $this->parseUploadedFile($filename, $contentType, $contents) + $file )); } private function parseUploadedFile($filename, $contentType, $contents) { + // ignore excessive number of file uploads + if (++$this->filesCount > $this->maxFileUploads) { + return; + } + $size = strlen($contents); // no file selected (zero size and empty filename) diff --git a/src/Middleware/RequestBodyParserMiddleware.php b/src/Middleware/RequestBodyParserMiddleware.php index 2e02a859..63759642 100644 --- a/src/Middleware/RequestBodyParserMiddleware.php +++ b/src/Middleware/RequestBodyParserMiddleware.php @@ -11,10 +11,11 @@ final class RequestBodyParserMiddleware /** * @param int|null $uploadMaxFilesize + * @param int|null $maxFileUploads */ - public function __construct($uploadMaxFilesize = null) + public function __construct($uploadMaxFilesize = null, $maxFileUploads = null) { - $this->multipart = new MultipartParser($uploadMaxFilesize); + $this->multipart = new MultipartParser($uploadMaxFilesize, $maxFileUploads); } public function __invoke(ServerRequestInterface $request, $next) diff --git a/tests/Io/MultipartParserTest.php b/tests/Io/MultipartParserTest.php index b982560d..f8b53cc1 100644 --- a/tests/Io/MultipartParserTest.php +++ b/tests/Io/MultipartParserTest.php @@ -754,6 +754,42 @@ public function testUploadNoFile() $this->assertSame(UPLOAD_ERR_NO_FILE, $file->getError()); } + public function testUploadTooManyFilesReturnsTruncatedList() + { + $boundary = "---------------------------12758086162038677464950549563"; + + $data = "--$boundary\r\n"; + $data .= "Content-Disposition: form-data; name=\"first\"; filename=\"first\"\r\n"; + $data .= "Content-type: text/plain\r\n"; + $data .= "\r\n"; + $data .= "hello\r\n"; + $data .= "--$boundary\r\n"; + $data .= "Content-Disposition: form-data; name=\"second\"; filename=\"second\"\r\n"; + $data .= "Content-type: text/plain\r\n"; + $data .= "\r\n"; + $data .= "world\r\n"; + $data .= "--$boundary--\r\n"; + + $request = new ServerRequest('POST', 'http://example.com/', array( + 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, + ), $data, 1.1); + + $parser = new MultipartParser(100, 1); + $parsedRequest = $parser->parse($request); + + $files = $parsedRequest->getUploadedFiles(); + + $this->assertCount(1, $files); + $this->assertTrue(isset($files['first'])); + + $file = $files['first']; + $this->assertSame('first', $file->getClientFilename()); + $this->assertSame('text/plain', $file->getClientMediaType()); + $this->assertSame(5, $file->getSize()); + $this->assertSame(UPLOAD_ERR_OK, $file->getError()); + $this->assertSame('hello', (string)$file->getStream()); + } + public function testPostMaxFileSize() { $boundary = "---------------------------12758086162038677464950549563"; From c1e9c32c327ad25e8e13f30fcd1188ea41c755fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 8 Dec 2017 16:47:50 +0100 Subject: [PATCH 233/456] Upload fields left blank on submission do not count towards max uploads --- README.md | 3 +- src/Io/MultipartParser.php | 10 +++---- tests/Io/MultipartParserTest.php | 48 ++++++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index f7726bba..ccf5293f 100644 --- a/README.md +++ b/README.md @@ -800,7 +800,8 @@ By default, this middleware respects the [`max_file_uploads`](http://php.net/manual/en/ini.core.php#ini.max-file-uploads) (default `20`) ini setting. If you upload more files in a single request, additional files will be ignored -and the `getUploadedFiles()` method returns a truncated array. +and the `getUploadedFiles()` method returns a truncated array. +Note that upload fields left blank on submission do not count towards this limit. You can control the maximum number of file uploads per request by explicitly passing the second parameter to the constructor like this: diff --git a/src/Io/MultipartParser.php b/src/Io/MultipartParser.php index 3eaf9b69..c875f553 100644 --- a/src/Io/MultipartParser.php +++ b/src/Io/MultipartParser.php @@ -172,11 +172,6 @@ private function parseFile($name, $filename, $contentType, $contents) private function parseUploadedFile($filename, $contentType, $contents) { - // ignore excessive number of file uploads - if (++$this->filesCount > $this->maxFileUploads) { - return; - } - $size = strlen($contents); // no file selected (zero size and empty filename) @@ -190,6 +185,11 @@ private function parseUploadedFile($filename, $contentType, $contents) ); } + // ignore excessive number of file uploads + if (++$this->filesCount > $this->maxFileUploads) { + return; + } + // file exceeds "upload_max_filesize" ini setting if ($size > $this->uploadMaxFilesize) { return new UploadedFile( diff --git a/tests/Io/MultipartParserTest.php b/tests/Io/MultipartParserTest.php index f8b53cc1..b070c93b 100644 --- a/tests/Io/MultipartParserTest.php +++ b/tests/Io/MultipartParserTest.php @@ -790,6 +790,54 @@ public function testUploadTooManyFilesReturnsTruncatedList() $this->assertSame('hello', (string)$file->getStream()); } + public function testUploadTooManyFilesIgnoresEmptyFilesAndIncludesThemDespiteTruncatedList() + { + $boundary = "---------------------------12758086162038677464950549563"; + + $data = "--$boundary\r\n"; + $data .= "Content-Disposition: form-data; name=\"first\"; filename=\"first\"\r\n"; + $data .= "Content-type: text/plain\r\n"; + $data .= "\r\n"; + $data .= "hello\r\n"; + $data .= "--$boundary\r\n"; + $data .= "Content-Disposition: form-data; name=\"empty\"; filename=\"\"\r\n"; + $data .= "Content-type: text/plain\r\n"; + $data .= "\r\n"; + $data .= "\r\n"; + $data .= "--$boundary\r\n"; + $data .= "Content-Disposition: form-data; name=\"second\"; filename=\"second\"\r\n"; + $data .= "Content-type: text/plain\r\n"; + $data .= "\r\n"; + $data .= "world\r\n"; + $data .= "--$boundary--\r\n"; + + $request = new ServerRequest('POST', 'http://example.com/', array( + 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, + ), $data, 1.1); + + $parser = new MultipartParser(100, 1); + $parsedRequest = $parser->parse($request); + + $files = $parsedRequest->getUploadedFiles(); + + $this->assertCount(2, $files); + $this->assertTrue(isset($files['first'])); + $this->assertTrue(isset($files['empty'])); + + $file = $files['first']; + $this->assertSame('first', $file->getClientFilename()); + $this->assertSame('text/plain', $file->getClientMediaType()); + $this->assertSame(5, $file->getSize()); + $this->assertSame(UPLOAD_ERR_OK, $file->getError()); + $this->assertSame('hello', (string)$file->getStream()); + + $file = $files['empty']; + $this->assertSame('', $file->getClientFilename()); + $this->assertSame('text/plain', $file->getClientMediaType()); + $this->assertSame(0, $file->getSize()); + $this->assertSame(UPLOAD_ERR_NO_FILE, $file->getError()); + } + public function testPostMaxFileSize() { $boundary = "---------------------------12758086162038677464950549563"; From aad224bfb219ef4e0e1c3b42106634528beb3f1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 8 Dec 2017 16:59:06 +0100 Subject: [PATCH 234/456] Ignore excessive number of empty file uploads --- src/Io/MultipartParser.php | 7 ++++ .../RequestBodyParserMiddlewareTest.php | 40 +++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/src/Io/MultipartParser.php b/src/Io/MultipartParser.php index c875f553..9c91cf13 100644 --- a/src/Io/MultipartParser.php +++ b/src/Io/MultipartParser.php @@ -63,6 +63,7 @@ final class MultipartParser private $postCount = 0; private $filesCount = 0; + private $emptyCount = 0; /** * @param int|null $uploadMaxFilesize @@ -97,6 +98,7 @@ public function parse(ServerRequestInterface $request) $this->request = null; $this->postCount = 0; $this->filesCount = 0; + $this->emptyCount = 0; $this->maxFileSize = null; return $request; @@ -176,6 +178,11 @@ private function parseUploadedFile($filename, $contentType, $contents) // no file selected (zero size and empty filename) if ($size === 0 && $filename === '') { + // ignore excessive number of empty file uploads + if (++$this->emptyCount + $this->filesCount > $this->maxInputVars) { + return; + } + return new UploadedFile( Psr7\stream_for(''), $size, diff --git a/tests/Middleware/RequestBodyParserMiddlewareTest.php b/tests/Middleware/RequestBodyParserMiddlewareTest.php index dc818ee9..994d562c 100644 --- a/tests/Middleware/RequestBodyParserMiddlewareTest.php +++ b/tests/Middleware/RequestBodyParserMiddlewareTest.php @@ -316,4 +316,44 @@ function (ServerRequestInterface $request) { $this->assertTrue(isset($body['a'])); $this->assertCount($allowed, $body['a']); } + + public function testMultipartFormDataTruncatesExcessiveNumberOfEmptyFileUploads() + { + // ini setting exists in PHP 5.3.9, not in HHVM: https://3v4l.org/VF6oV + // otherwise default to 1000 as implemented within + $allowed = (int)ini_get('max_input_vars'); + if ($allowed === 0) { + $allowed = 1000; + } + + $middleware = new RequestBodyParserMiddleware(); + + $boundary = "---------------------------12758086162038677464950549563"; + + $data = ""; + for ($i = 0; $i < $allowed + 1; ++$i) { + $data .= "--$boundary\r\n"; + $data .= "Content-Disposition: form-data; name=\"empty[]\"; filename=\"\"\r\n"; + $data .= "\r\n"; + $data .= "\r\n"; + } + $data .= "--$boundary--\r\n"; + + $request = new ServerRequest('POST', 'http://example.com/', array( + 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, + ), $data, 1.1); + + /** @var ServerRequestInterface $parsedRequest */ + $parsedRequest = $middleware( + $request, + function (ServerRequestInterface $request) { + return $request; + } + ); + + $body = $parsedRequest->getUploadedFiles(); + $this->assertCount(1, $body); + $this->assertTrue(isset($body['empty'])); + $this->assertCount($allowed, $body['empty']); + } } From da1e4a51d209eae1d5c8b0e0e4e4cb3d6988d9ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 8 Dec 2017 17:44:23 +0100 Subject: [PATCH 235/456] Respect file_uploads ini setting --- README.md | 5 ++++- src/Io/MultipartParser.php | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ccf5293f..c709d193 100644 --- a/README.md +++ b/README.md @@ -797,8 +797,11 @@ new RequestBodyParserMiddleware(8 * 1024 * 1024); // 8 MiB limit per file ``` By default, this middleware respects the +[`file_uploads`](http://php.net/manual/en/ini.core.php#ini.file-uploads) +(default `1`) and [`max_file_uploads`](http://php.net/manual/en/ini.core.php#ini.max-file-uploads) -(default `20`) ini setting. +(default `20`) ini settings. +These settings control if any and how many files can be uploaded in a single request. If you upload more files in a single request, additional files will be ignored and the `getUploadedFiles()` method returns a truncated array. Note that upload fields left blank on submission do not count towards this limit. diff --git a/src/Io/MultipartParser.php b/src/Io/MultipartParser.php index 9c91cf13..ff27d11f 100644 --- a/src/Io/MultipartParser.php +++ b/src/Io/MultipartParser.php @@ -57,6 +57,8 @@ final class MultipartParser /** * ini setting "max_file_uploads" * + * Additionally, setting "file_uploads = off" effectively sets this to zero. + * * @var int */ private $maxFileUploads; @@ -81,7 +83,7 @@ public function __construct($uploadMaxFilesize = null, $maxFileUploads = null) } $this->uploadMaxFilesize = $uploadMaxFilesize === null ? $this->iniUploadMaxFilesize() : (int)$uploadMaxFilesize; - $this->maxFileUploads = $maxFileUploads === null ? (int)ini_get('max_file_uploads') : (int)$maxFileUploads; + $this->maxFileUploads = $maxFileUploads === null ? (ini_get('file_uploads') === '' ? 0 : (int)ini_get('max_file_uploads')) : (int)$maxFileUploads; } public function parse(ServerRequestInterface $request) From aa77ac321a0dd9e02bf721abb671fd40a46b9c60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 8 Dec 2017 17:49:57 +0100 Subject: [PATCH 236/456] Add documentation for enable_post_data_reading ini setting --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index c709d193..aa85b58b 100644 --- a/README.md +++ b/README.md @@ -834,6 +834,13 @@ new RequestBodyParserMiddleware(10 * 1024, 100); // 100 files with 10 KiB each [`max_input_nesting_level`](http://php.net/manual/en/info.configuration.php#ini.max-input-nesting-level) (default `64`) ini settings. +> Note that this middleware ignores the + [`enable_post_data_reading`](http://php.net/manual/en/ini.core.php#ini.enable-post-data-reading) + (default `1`) ini setting because it makes little sense to respect here and + is left up to higher-level implementations. + If you want to respect this setting, you have to check its value and + effectively avoid using this middleware entirely. + #### Third-Party Middleware A non-exhaustive list of third-party middleware can be found at the [`Middleware`](https://github.com/reactphp/http/wiki/Middleware) wiki page. From c15a687a2b14253cda34d609ce00ceee124c709f Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Thu, 7 Sep 2017 21:47:37 +0200 Subject: [PATCH 237/456] Skeleton for LimitHandlersMiddleware --- src/Middleware/LimitHandlersMiddleware.php | 74 +++++++++++++ .../LimitHandlersMiddlewareTest.php | 100 ++++++++++++++++++ 2 files changed, 174 insertions(+) create mode 100644 src/Middleware/LimitHandlersMiddleware.php create mode 100644 tests/Middleware/LimitHandlersMiddlewareTest.php diff --git a/src/Middleware/LimitHandlersMiddleware.php b/src/Middleware/LimitHandlersMiddleware.php new file mode 100644 index 00000000..a674060e --- /dev/null +++ b/src/Middleware/LimitHandlersMiddleware.php @@ -0,0 +1,74 @@ +limit = $limit; + $this->queued = new SplQueue(); + } + + public function __invoke(ServerRequestInterface $request, $next) + { + $body = $request->getBody(); + if ($body instanceof ReadableStreamInterface) { + $body->pause(); + } + $deferred = new Deferred(); + $this->queued->enqueue($deferred); + + $this->processQueue(); + + $that = $this; + $pending = &$this->pending; + return $deferred->promise()->then(function () use ($request, $next, &$pending) { + $pending++; + $body = $request->getBody(); + if ($body instanceof ReadableStreamInterface) { + $body->resume(); + } + return $next($request); + })->then(function ($response) use ($that, &$pending) { + $pending--; + $that->processQueue(); + return $response; + }, function ($error) use ($that, &$pending) { + $pending--; + $that->processQueue(); + return $error; + }); + } + + /** + * @internal + */ + public function processQueue() + { + if ($this->pending >= $this->limit) { + return; + } + + if ($this->queued->count() === 0) { + return; + } + + $this->queued->dequeue()->resolve(); + } +} diff --git a/tests/Middleware/LimitHandlersMiddlewareTest.php b/tests/Middleware/LimitHandlersMiddlewareTest.php new file mode 100644 index 00000000..732023cf --- /dev/null +++ b/tests/Middleware/LimitHandlersMiddlewareTest.php @@ -0,0 +1,100 @@ +promise(); + }; + + /** + * The second request + */ + $requestB = new ServerRequest('GET', 'https://www.example.com/'); + $deferredB = new Deferred(); + $calledB = false; + $nextB = function () use (&$calledB, $deferredB) { + $calledB = true; + return $deferredB->promise(); + }; + + /** + * The third request + */ + $requestC = new ServerRequest('GET', 'https://www.example.com/'); + $calledC = false; + $nextC = function () use (&$calledC) { + $calledC = true; + }; + + /** + * The handler + * + */ + $limitHandlers = new LimitHandlersMiddleware(1); + + $this->assertFalse($calledA); + $this->assertFalse($calledB); + $this->assertFalse($calledC); + + $limitHandlers($requestA, $nextA); + + $this->assertTrue($calledA); + $this->assertFalse($calledB); + $this->assertFalse($calledC); + + $limitHandlers($requestB, $nextB); + + $this->assertTrue($calledA); + $this->assertFalse($calledB); + $this->assertFalse($calledC); + + $limitHandlers($requestC, $nextC); + + $this->assertTrue($calledA); + $this->assertFalse($calledB); + $this->assertFalse($calledC); + + /** + * Ensure resolve frees up a slot + */ + $deferredA->resolve(); + + $this->assertTrue($calledA); + $this->assertTrue($calledB); + $this->assertFalse($calledC); + + /** + * Ensure reject also frees up a slot + */ + $deferredB->reject(); + + $this->assertTrue($calledA); + $this->assertTrue($calledB); + $this->assertTrue($calledC); + } + + public function testStreamPauseAndResume() + { + $body = $this->getMockBuilder('React\Http\Io\HttpBodyStream')->disableOriginalConstructor()->getMock(); + $body->expects($this->once())->method('pause'); + $body->expects($this->once())->method('resume'); + $limitHandlers = new LimitHandlersMiddleware(1); + $limitHandlers(new ServerRequest('GET', 'https://example.com/', array(), $body), function () {}); + } +} From 9e484fd289acf221368586fbe0f306f4d470be19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 23 Sep 2017 22:16:34 +0200 Subject: [PATCH 238/456] Add PauseBufferStream decorator to buffer events for paused streams --- src/Io/PauseBufferStream.php | 162 ++++++++++++++++++++ tests/Io/PauseBufferStreamTest.php | 235 +++++++++++++++++++++++++++++ 2 files changed, 397 insertions(+) create mode 100644 src/Io/PauseBufferStream.php create mode 100644 tests/Io/PauseBufferStreamTest.php diff --git a/src/Io/PauseBufferStream.php b/src/Io/PauseBufferStream.php new file mode 100644 index 00000000..24ca081c --- /dev/null +++ b/src/Io/PauseBufferStream.php @@ -0,0 +1,162 @@ +input = $input; + + $this->input->on('data', array($this, 'handleData')); + $this->input->on('end', array($this, 'handleEnd')); + $this->input->on('error', array($this, 'handleError')); + $this->input->on('close', array($this, 'handleClose')); + } + + public function isReadable() + { + return !$this->closed; + } + + public function pause() + { + if ($this->closed) { + return; + } + + $this->input->pause(); + $this->paused = true; + } + + public function resume() + { + if ($this->closed) { + return; + } + + $this->paused = false; + + if ($this->dataPaused !== '') { + $this->emit('data', array($this->dataPaused)); + $this->dataPaused = ''; + } + + if ($this->errorPaused) { + $this->emit('error', array($this->errorPaused)); + return $this->close(); + } + + if ($this->endPaused) { + $this->endPaused = false; + $this->emit('end'); + return $this->close(); + } + + if ($this->closePaused) { + $this->closePaused = false; + return $this->close(); + } + + $this->input->resume(); + } + + 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->dataPaused = ''; + $this->endPaused = $this->closePaused = false; + $this->errorPaused = null; + + $this->input->close(); + + $this->emit('close'); + $this->removeAllListeners(); + } + + /** @internal */ + public function handleData($data) + { + if ($this->paused) { + $this->dataPaused .= $data; + return; + } + + $this->emit('data', array($data)); + } + + /** @internal */ + public function handleError(\Exception $e) + { + if ($this->paused) { + $this->errorPaused = $e; + return; + } + + $this->emit('error', array($e)); + $this->close(); + } + + /** @internal */ + public function handleEnd() + { + if ($this->paused) { + $this->endPaused = true; + return; + } + + if (!$this->closed) { + $this->emit('end'); + $this->close(); + } + } + + /** @internal */ + public function handleClose() + { + if ($this->paused) { + $this->closePaused = true; + return; + } + + $this->close(); + } +} diff --git a/tests/Io/PauseBufferStreamTest.php b/tests/Io/PauseBufferStreamTest.php new file mode 100644 index 00000000..a9678a78 --- /dev/null +++ b/tests/Io/PauseBufferStreamTest.php @@ -0,0 +1,235 @@ +getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $input->expects($this->once())->method('pause'); + + $stream = new PauseBufferStream($input); + $stream->pause(); + } + + public function testCloseMethodWillBePassedThroughToInput() + { + $input = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $input->expects($this->once())->method('close'); + + $stream = new PauseBufferStream($input); + $stream->close(); + } + + public function testPauseMethodWillNotBePassedThroughToInputAfterClose() + { + $input = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $input->expects($this->never())->method('pause'); + + $stream = new PauseBufferStream($input); + $stream->close(); + $stream->pause(); + } + + public function testDataEventWillBePassedThroughAsIs() + { + $input = new ThroughStream(); + $stream = new PauseBufferStream($input); + + $stream->on('data', $this->expectCallableOnceWith('hello')); + $input->write('hello'); + } + + public function testDataEventWillBePipedThroughAsIs() + { + $input = new ThroughStream(); + $stream = new PauseBufferStream($input); + + $output = new ThroughStream($this->expectCallableOnceWith('hello')); + $stream->pipe($output); + + $input->write('hello'); + } + + public function testPausedStreamWillNotPassThroughDataEvent() + { + $input = new ThroughStream(); + $stream = new PauseBufferStream($input); + + $stream->pause(); + $stream->on('data', $this->expectCallableNever()); + $input->write('hello'); + } + + public function testPauseStreamWillNotPipeThroughDataEvent() + { + $input = new ThroughStream(); + $stream = new PauseBufferStream($input); + + $output = new ThroughStream($this->expectCallableNever()); + $stream->pipe($output); + + $stream->pause(); + $input->write('hello'); + } + + public function testPausedStreamWillPassThroughDataEventOnResume() + { + $input = new ThroughStream(); + $stream = new PauseBufferStream($input); + + $stream->pause(); + $input->write('hello'); + + $stream->on('data', $this->expectCallableOnceWith('hello')); + $stream->resume(); + } + + public function testEndEventWillBePassedThroughAsIs() + { + $input = new ThroughStream(); + $stream = new PauseBufferStream($input); + + $stream->on('data', $this->expectCallableOnceWith('hello')); + $stream->on('end', $this->expectCallableOnce()); + $stream->on('close', $this->expectCallableOnce()); + $input->end('hello'); + + $this->assertFalse($stream->isReadable()); + } + + public function testPausedStreamWillNotPassThroughEndEvent() + { + $input = new ThroughStream(); + $stream = new PauseBufferStream($input); + + $stream->pause(); + $stream->on('data', $this->expectCallableNever()); + $stream->on('end', $this->expectCallableNever()); + $stream->on('close', $this->expectCallableNever()); + $input->end('hello'); + + $this->assertTrue($stream->isReadable()); + } + + public function testPausedStreamWillPassThroughEndEventOnResume() + { + $input = new ThroughStream(); + $stream = new PauseBufferStream($input); + + $stream->pause(); + $input->end('hello'); + + $stream->on('data', $this->expectCallableOnceWith('hello')); + $stream->on('end', $this->expectCallableOnce()); + $stream->on('close', $this->expectCallableOnce()); + $stream->resume(); + + $this->assertFalse($stream->isReadable()); + } + + public function testPausedStreamWillNotPassThroughEndEventOnExplicitClose() + { + $input = new ThroughStream(); + $stream = new PauseBufferStream($input); + + $stream->pause(); + $stream->on('data', $this->expectCallableNever()); + $stream->on('end', $this->expectCallableNever()); + $stream->on('close', $this->expectCallableOnce()); + $input->end('hello'); + + $stream->close(); + + $this->assertFalse($stream->isReadable()); + } + + public function testErrorEventWillBePassedThroughAsIs() + { + $input = new ThroughStream(); + $stream = new PauseBufferStream($input); + + $stream->on('error', $this->expectCallableOnce()); + $stream->on('close', $this->expectCallableOnce()); + $input->emit('error', array(new \RuntimeException())); + } + + public function testPausedStreamWillNotPassThroughErrorEvent() + { + $input = new ThroughStream(); + $stream = new PauseBufferStream($input); + + $stream->pause(); + $stream->on('error', $this->expectCallableNever()); + $stream->on('close', $this->expectCallableNever()); + $input->emit('error', array(new \RuntimeException())); + } + + public function testPausedStreamWillPassThroughErrorEventOnResume() + { + $input = new ThroughStream(); + $stream = new PauseBufferStream($input); + + $stream->pause(); + $input->emit('error', array(new \RuntimeException())); + + $stream->on('error', $this->expectCallableOnce()); + $stream->on('close', $this->expectCallableOnce()); + $stream->resume(); + } + + public function testPausedStreamWillNotPassThroughErrorEventOnExplicitClose() + { + $input = new ThroughStream(); + $stream = new PauseBufferStream($input); + + $stream->pause(); + $stream->on('error', $this->expectCallableNever()); + $stream->on('close', $this->expectCallableOnce()); + $input->emit('error', array(new \RuntimeException())); + + $stream->close(); + } + + public function testCloseEventWillBePassedThroughAsIs() + { + $input = new ThroughStream(); + $stream = new PauseBufferStream($input); + + $stream->on('data', $this->expectCallableNever()); + $stream->on('end', $this->expectCallableNever()); + $stream->on('close', $this->expectCallableOnce()); + $input->close(); + } + + public function testPausedStreamWillNotPassThroughCloseEvent() + { + $input = new ThroughStream(); + $stream = new PauseBufferStream($input); + + $stream->pause(); + $stream->on('data', $this->expectCallableNever()); + $stream->on('end', $this->expectCallableNever()); + $stream->on('close', $this->expectCallableNever()); + $input->close(); + } + + public function testPausedStreamWillPassThroughCloseEventOnResume() + { + $input = new ThroughStream(); + $stream = new PauseBufferStream($input); + + $stream->pause(); + $input->close(); + + $stream->on('data', $this->expectCallableNever()); + $stream->on('end', $this->expectCallableNever()); + $stream->on('close', $this->expectCallableOnce()); + $stream->resume(); + } +} From 136f78b2ff77550dafc4c798a994ad2164348cc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 25 Nov 2017 17:50:24 +0100 Subject: [PATCH 239/456] Ensure LimitHandlersMiddleware pauses streaming requests --- README.md | 63 +++++++ examples/12-upload.php | 2 + src/Middleware/LimitHandlersMiddleware.php | 70 ++++++- tests/FunctionalServerTest.php | 54 +++++- .../LimitHandlersMiddlewareTest.php | 171 +++++++++++++++++- 5 files changed, 356 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index aa85b58b..bda8c98f 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ Event-driven, streaming plaintext HTTP and secure HTTPS server for [ReactPHP](ht * [Request](#request) * [Response](#response) * [Middleware](#middleware) + * [LimitHandlersMiddleware](#limithandlersmiddleware) * [RequestBodyBufferMiddleware](#requestbodybuffermiddleware) * [RequestBodyParserMiddleware](#requestbodyparsermiddleware) * [Third-Party Middleware](#third-party-middleware) @@ -681,6 +682,59 @@ $server = new StreamingServer(new MiddlewareRunner([ ])); ``` +#### LimitHandlersMiddleware + +The `LimitHandlersMiddleware` can be used to +limit how many next handlers can be executed concurrently. + +If this middleware is invoked, it will check if the number of pending +handlers is below the allowed limit and then simply invoke the next handler +and it will return whatever the next handler returns (or throws). + +If the number of pending handlers exceeds the allowed limit, the request will +be queued (and its streaming body will be paused) and it will return a pending +promise. +Once a pending handler returns (or throws), it will pick the oldest request +from this queue and invokes the next handler (and its streaming body will be +resumed). + +The following example shows how this middleware can be used to ensure no more +than 10 handlers will be invoked at once: + +```php +$server = new StreamingServer(new MiddlewareRunner([ + new LimitHandlersMiddleware(10), + $handler +])); +``` + +Similarly, this middleware is often used in combination with the +[`RequestBodyBufferMiddleware`](#requestbodybuffermiddleware) (see below) +to limit the total number of requests that can be buffered at once: + +```php +$server = new StreamingServer(new MiddlewareRunner([ + new LimitHandlersMiddleware(100), // 100 concurrent buffering handlers + new RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request + new RequestBodyParserMiddleware(), + $handler +])); +``` + +More sophisticated examples include limiting the total number of requests +that can be buffered at once and then ensure the actual request handler only +processes one request after another without any concurrency: + +```php +$server = new StreamingServer(new MiddlewareRunner([ + new LimitHandlersMiddleware(100), // 100 concurrent buffering handlers + new RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request + new RequestBodyParserMiddleware(), + new LimitHandlersMiddleware(1), // only execute 1 handler (no concurrency) + $handler +])); +``` + #### RequestBodyBufferMiddleware One of the built-in middleware is the `RequestBodyBufferMiddleware` which @@ -714,10 +768,18 @@ Similarly, this will immediately invoke the next middleware handler for requests that have an empty request body (such as a simple `GET` request) and requests that are already buffered (such as due to another middleware). +Note that the given buffer size limit is applied to each request individually. +This means that if you allow a 2 MiB limit and then receive 1000 concurrent +requests, up to 2000 MiB may be allocated for these buffers alone. +As such, it's highly recommended to use this along with the +[`LimitHandlersMiddleware`](#limithandlersmiddleware) (see above) to limit +the total number of concurrent requests. + Usage: ```php $middlewares = new MiddlewareRunner([ + new LimitHandlersMiddleware(100), // 100 concurrent buffering handlers new RequestBodyBufferMiddleware(16 * 1024 * 1024), // 16 MiB function (ServerRequestInterface $request, callable $next) { // The body from $request->getBody() is now fully available without the need to stream it @@ -776,6 +838,7 @@ $handler = function (ServerRequestInterface $request) { }; $server = new StreamingServer(new MiddlewareRunner([ + new LimitHandlersMiddleware(100), // 100 concurrent buffering handlers new RequestBodyBufferMiddleware(16 * 1024 * 1024), // 16 MiB new RequestBodyParserMiddleware(), $handler diff --git a/examples/12-upload.php b/examples/12-upload.php index 5e9c84db..ab8d2e1d 100644 --- a/examples/12-upload.php +++ b/examples/12-upload.php @@ -11,6 +11,7 @@ use Psr\Http\Message\UploadedFileInterface; use React\EventLoop\Factory; use React\Http\MiddlewareRunner; +use React\Http\Middleware\LimitHandlersMiddleware; use React\Http\Middleware\RequestBodyBufferMiddleware; use React\Http\Middleware\RequestBodyParserMiddleware; use React\Http\Response; @@ -121,6 +122,7 @@ // buffer and parse HTTP request body before running our request handler $server = new StreamingServer(new MiddlewareRunner(array( + new LimitHandlersMiddleware(100), // 100 concurrent buffering handlers, queue otherwise new RequestBodyBufferMiddleware(8 * 1024 * 1024), // 8 MiB max, ignore body otherwise new RequestBodyParserMiddleware(100 * 1024, 1), // 1 file with 100 KiB max, reject upload otherwise $handler diff --git a/src/Middleware/LimitHandlersMiddleware.php b/src/Middleware/LimitHandlersMiddleware.php index a674060e..7d7ef122 100644 --- a/src/Middleware/LimitHandlersMiddleware.php +++ b/src/Middleware/LimitHandlersMiddleware.php @@ -3,10 +3,65 @@ namespace React\Http\Middleware; use Psr\Http\Message\ServerRequestInterface; +use React\Http\Io\HttpBodyStream; +use React\Http\Io\PauseBufferStream; use React\Promise\Deferred; use React\Stream\ReadableStreamInterface; use SplQueue; +/** + * Limits how many next handlers can be executed concurrently. + * + * If this middleware is invoked, it will check if the number of pending + * handlers is below the allowed limit and then simply invoke the next handler + * and it will return whatever the next handler returns (or throws). + * + * If the number of pending handlers exceeds the allowed limit, the request will + * be queued (and its streaming body will be paused) and it will return a pending + * promise. + * Once a pending handler returns (or throws), it will pick the oldest request + * from this queue and invokes the next handler (and its streaming body will be + * resumed). + * + * The following example shows how this middleware can be used to ensure no more + * than 10 handlers will be invoked at once: + * + * ```php + * $server = new StreamingServer(new MiddlewareRunner([ + * new LimitHandlersMiddleware(10), + * $handler + * ])); + * ``` + * + * Similarly, this middleware is often used in combination with the + * [`RequestBodyBufferMiddleware`](#requestbodybuffermiddleware) (see below) + * to limit the total number of requests that can be buffered at once: + * + * ```php + * $server = new StreamingServer(new MiddlewareRunner([ + * new LimitHandlersMiddleware(100), // 100 concurrent buffering handlers + * new RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request + * new RequestBodyParserMiddleware(), + * $handler + * ])); + * ``` + * + * More sophisticated examples include limiting the total number of requests + * that can be buffered at once and then ensure the actual request handler only + * processes one request after another without any concurrency: + * + * ```php + * $server = new StreamingServer(new MiddlewareRunner([ + * new LimitHandlersMiddleware(100), // 100 concurrent buffering handlers + * new RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request + * new RequestBodyParserMiddleware(), + * new LimitHandlersMiddleware(1), // only execute 1 handler (no concurrency) + * $handler + * ])); + * ``` + * + * @see RequestBodyBufferMiddleware + */ final class LimitHandlersMiddleware { private $limit; @@ -29,6 +84,14 @@ public function __invoke(ServerRequestInterface $request, $next) { $body = $request->getBody(); if ($body instanceof ReadableStreamInterface) { + // replace with buffering body to ensure any readable events will be buffered + $body = new HttpBodyStream( + new PauseBufferStream($body), + $body->getSize() + ); + + // pause actual body to stop emitting data until the handler is called + $request = $request->withBody($body); $body->pause(); } $deferred = new Deferred(); @@ -40,11 +103,16 @@ public function __invoke(ServerRequestInterface $request, $next) $pending = &$this->pending; return $deferred->promise()->then(function () use ($request, $next, &$pending) { $pending++; + + $ret = $next($request); + + // resume readable stream and replay buffered events $body = $request->getBody(); if ($body instanceof ReadableStreamInterface) { $body->resume(); } - return $next($request); + + return $ret; })->then(function ($response) use ($that, &$pending) { $pending--; $that->processQueue(); diff --git a/tests/FunctionalServerTest.php b/tests/FunctionalServerTest.php index d7b9d420..b7a18880 100644 --- a/tests/FunctionalServerTest.php +++ b/tests/FunctionalServerTest.php @@ -2,6 +2,9 @@ namespace React\Tests\Http; +use Psr\Http\Message\ServerRequestInterface; +use React\Http\Middleware\LimitHandlersMiddleware; +use React\Http\Middleware\RequestBodyBufferMiddleware; use React\Http\MiddlewareRunner; use React\Socket\Server as Socket; use React\EventLoop\Factory; @@ -12,7 +15,7 @@ use Clue\React\Block; use React\Http\Response; use React\Socket\SecureServer; -use React\Promise\Promise; +use React\Promise; use React\Promise\Stream; use React\Stream\ThroughStream; @@ -653,7 +656,7 @@ public function testConnectWithThroughStreamReturnedFromPromiseReturnsDataAsGive $stream->end(); }); - return new Promise(function ($resolve) use ($loop, $stream) { + return new Promise\Promise(function ($resolve) use ($loop, $stream) { $loop->addTimer(0.001, function () use ($resolve, $stream) { $resolve(new Response(200, array(), $stream)); }); @@ -715,6 +718,53 @@ public function testConnectWithClosedThroughStreamReturnsNoData() $socket->close(); } + + public function testLimitHandlersMiddlewareRequestStreamPausing() + { + $loop = Factory::create(); + $connector = new Connector($loop); + + $server = new StreamingServer(new MiddlewareRunner(array( + new LimitHandlersMiddleware(5), + new RequestBodyBufferMiddleware(16 * 1024 * 1024), // 16 MiB + function (ServerRequestInterface $request, $next) use ($loop) { + return new Promise\Promise(function ($resolve) use ($request, $loop, $next) { + $loop->addTimer(0.1, function () use ($request, $resolve, $next) { + $resolve($next($request)); + }); + }); + }, + function (ServerRequestInterface $request) { + return new Response(200, array(), (string)strlen((string)$request->getBody())); + } + ))); + + $socket = new Socket(0, $loop); + $server->listen($socket); + + $result = array(); + for ($i = 0; $i < 6; $i++) { + $result[] = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { + $conn->write( + "GET / HTTP/1.0\r\nContent-Length: 1024\r\nHost: " . noScheme($conn->getRemoteAddress()) . "\r\n\r\n" . + str_repeat('a', 1024) . + "\r\n\r\n" + ); + + return Stream\buffer($conn); + }); + } + + $responses = Block\await(Promise\all($result), $loop, 1.0); + + foreach ($responses as $response) { + $this->assertContains("HTTP/1.0 200 OK", $response, $response); + $this->assertTrue(substr($response, -4) == 1024, $response); + } + + $socket->close(); + } + } function noScheme($uri) diff --git a/tests/Middleware/LimitHandlersMiddlewareTest.php b/tests/Middleware/LimitHandlersMiddlewareTest.php index 732023cf..c4355cca 100644 --- a/tests/Middleware/LimitHandlersMiddlewareTest.php +++ b/tests/Middleware/LimitHandlersMiddlewareTest.php @@ -2,9 +2,13 @@ namespace React\Tests\Http\Middleware; -use React\Http\Middleware\LimitHandlersMiddleware; +use Psr\Http\Message\ServerRequestInterface; +use React\Http\Io\HttpBodyStream; use React\Http\Io\ServerRequest; +use React\Http\Middleware\LimitHandlersMiddleware; use React\Promise\Deferred; +use React\Promise\Promise; +use React\Stream\ThroughStream; use React\Tests\Http\TestCase; final class LimitHandlersMiddlewareTest extends TestCase @@ -97,4 +101,169 @@ public function testStreamPauseAndResume() $limitHandlers = new LimitHandlersMiddleware(1); $limitHandlers(new ServerRequest('GET', 'https://example.com/', array(), $body), function () {}); } + + public function testReceivesBufferedRequestSameInstance() + { + $request = new ServerRequest( + 'POST', + 'http://example.com/', + array(), + 'hello' + ); + + $req = null; + $middleware = new LimitHandlersMiddleware(1); + $middleware($request, function (ServerRequestInterface $request) use (&$req) { + $req = $request; + }); + + $this->assertSame($request, $req); + } + + public function testReceivesStreamingBodyChangesInstanceWithCustomBodyButSameData() + { + $stream = new ThroughStream(); + $request = new ServerRequest( + 'POST', + 'http://example.com/', + array(), + new HttpBodyStream($stream, 5) + ); + + $req = null; + $middleware = new LimitHandlersMiddleware(1); + $middleware($request, function (ServerRequestInterface $request) use (&$req) { + $req = $request; + }); + + $this->assertNotSame($request, $req); + $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $req); + + $body = $req->getBody(); + $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); + /* @var $body \React\Stream\ReadableStreamInterface */ + + $this->assertEquals(5, $body->getSize()); + + $body->on('data', $this->expectCallableOnce('hello')); + $stream->write('hello'); + } + + public function testReceivesRequestsSequentially() + { + $request = new ServerRequest( + 'POST', + 'http://example.com/', + array(), + 'hello' + ); + + $middleware = new LimitHandlersMiddleware(1); + $middleware($request, $this->expectCallableOnceWith($request)); + $middleware($request, $this->expectCallableOnceWith($request)); + $middleware($request, $this->expectCallableOnceWith($request)); + } + + public function testDoesNotReceiveNextRequestIfHandlerIsPending() + { + $request = new ServerRequest( + 'POST', + 'http://example.com/', + array(), + 'hello' + ); + + $middleware = new LimitHandlersMiddleware(1); + $middleware($request, function () { + return new Promise(function () { + // NO-OP: pending promise + }); + }); + + $middleware($request, $this->expectCallableNever()); + } + + public function testReceivesNextRequestAfterPreviousHandlerIsSettled() + { + $request = new ServerRequest( + 'POST', + 'http://example.com/', + array(), + 'hello' + ); + + $deferred = new Deferred(); + $middleware = new LimitHandlersMiddleware(1); + $middleware($request, function () use ($deferred) { + return $deferred->promise(); + }); + + $deferred->reject(new \RuntimeException()); + + $middleware($request, $this->expectCallableOnceWith($request)); + } + + public function testReceivesNextStreamingBodyWithSameDataAfterPreviousHandlerIsSettled() + { + $stream = new ThroughStream(); + $request = new ServerRequest( + 'POST', + 'http://example.com/', + array(), + new HttpBodyStream($stream, 5) + ); + + $deferred = new Deferred(); + $middleware = new LimitHandlersMiddleware(1); + $middleware($request, function () use ($deferred) { + return $deferred->promise(); + }); + + $deferred->reject(new \RuntimeException()); + + $req = null; + $middleware($request, function (ServerRequestInterface $request) use (&$req) { + $req = $request; + }); + + $this->assertNotSame($request, $req); + $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $req); + + $body = $req->getBody(); + $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); + /* @var $body \React\Stream\ReadableStreamInterface */ + + $this->assertEquals(5, $body->getSize()); + + $body->on('data', $this->expectCallableOnce('hello')); + $stream->write('hello'); + } + + public function testReceivesNextStreamingBodyWithBufferedDataAfterPreviousHandlerIsSettled() + { + $deferred = new Deferred(); + $middleware = new LimitHandlersMiddleware(1); + $middleware(new ServerRequest('GET', 'http://example.com/'), function () use ($deferred) { + return $deferred->promise(); + }); + + $stream = new ThroughStream(); + $request = new ServerRequest( + 'POST', + 'http://example.com/', + array(), + new HttpBodyStream($stream, 10) + ); + + $req = null; + $once = $this->expectCallableOnceWith('helloworld'); + $middleware($request, function (ServerRequestInterface $request) use ($once) { + $request->getBody()->on('data', $once); + }); + + $stream->write('hello'); + $stream->write('world'); + + $deferred->reject(new \RuntimeException()); + } } From 955bf73bfd8340676de053c691ab07ac48ee52c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 5 Dec 2017 12:47:01 +0100 Subject: [PATCH 240/456] Do not resume() stream if handler explicitly calls pause() --- src/Io/PauseBufferStream.php | 26 ++++++ src/Middleware/LimitHandlersMiddleware.php | 25 +++--- .../LimitHandlersMiddlewareTest.php | 87 ++++++++++++++++++- 3 files changed, 125 insertions(+), 13 deletions(-) diff --git a/src/Io/PauseBufferStream.php b/src/Io/PauseBufferStream.php index 24ca081c..6e2e3da4 100644 --- a/src/Io/PauseBufferStream.php +++ b/src/Io/PauseBufferStream.php @@ -30,6 +30,7 @@ class PauseBufferStream extends EventEmitter implements ReadableStreamInterface private $endPaused = false; private $closePaused = false; private $errorPaused = null; + private $implicit = false; public function __construct(ReadableStreamInterface $input) { @@ -41,6 +42,29 @@ public function __construct(ReadableStreamInterface $input) $this->input->on('close', array($this, 'handleClose')); } + /** + * pause and remember this was not explicitly from user control + * + * @internal + */ + public function pauseImplicit() + { + $this->pause(); + $this->implicit = true; + } + + /** + * resume only if this was previously paused implicitly and not explicitly from user control + * + * @internal + */ + public function resumeImplicit() + { + if ($this->implicit) { + $this->resume(); + } + } + public function isReadable() { return !$this->closed; @@ -54,6 +78,7 @@ public function pause() $this->input->pause(); $this->paused = true; + $this->implicit = false; } public function resume() @@ -63,6 +88,7 @@ public function resume() } $this->paused = false; + $this->implicit = false; if ($this->dataPaused !== '') { $this->emit('data', array($this->dataPaused)); diff --git a/src/Middleware/LimitHandlersMiddleware.php b/src/Middleware/LimitHandlersMiddleware.php index 7d7ef122..69dd5c81 100644 --- a/src/Middleware/LimitHandlersMiddleware.php +++ b/src/Middleware/LimitHandlersMiddleware.php @@ -84,16 +84,18 @@ public function __invoke(ServerRequestInterface $request, $next) { $body = $request->getBody(); if ($body instanceof ReadableStreamInterface) { - // replace with buffering body to ensure any readable events will be buffered - $body = new HttpBodyStream( - new PauseBufferStream($body), - $body->getSize() - ); - // pause actual body to stop emitting data until the handler is called - $request = $request->withBody($body); - $body->pause(); + $size = $body->getSize(); + $body = new PauseBufferStream($body); + $body->pauseImplicit(); + + // replace with buffering body to ensure any readable events will be buffered + $request = $request->withBody(new HttpBodyStream( + $body, + $size + )); } + $deferred = new Deferred(); $this->queued->enqueue($deferred); @@ -101,15 +103,14 @@ public function __invoke(ServerRequestInterface $request, $next) $that = $this; $pending = &$this->pending; - return $deferred->promise()->then(function () use ($request, $next, &$pending) { + return $deferred->promise()->then(function () use ($request, $next, $body, &$pending) { $pending++; $ret = $next($request); // resume readable stream and replay buffered events - $body = $request->getBody(); - if ($body instanceof ReadableStreamInterface) { - $body->resume(); + if ($body instanceof PauseBufferStream) { + $body->resumeImplicit(); } return $ret; diff --git a/tests/Middleware/LimitHandlersMiddlewareTest.php b/tests/Middleware/LimitHandlersMiddlewareTest.php index c4355cca..b11490a3 100644 --- a/tests/Middleware/LimitHandlersMiddlewareTest.php +++ b/tests/Middleware/LimitHandlersMiddlewareTest.php @@ -255,7 +255,6 @@ public function testReceivesNextStreamingBodyWithBufferedDataAfterPreviousHandle new HttpBodyStream($stream, 10) ); - $req = null; $once = $this->expectCallableOnceWith('helloworld'); $middleware($request, function (ServerRequestInterface $request) use ($once) { $request->getBody()->on('data', $once); @@ -266,4 +265,90 @@ public function testReceivesNextStreamingBodyWithBufferedDataAfterPreviousHandle $deferred->reject(new \RuntimeException()); } + + public function testReceivesNextStreamingBodyAndDoesNotEmitDataIfExplicitlyClosed() + { + $deferred = new Deferred(); + $middleware = new LimitHandlersMiddleware(1); + $middleware(new ServerRequest('GET', 'http://example.com/'), function () use ($deferred) { + return $deferred->promise(); + }); + + $stream = new ThroughStream(); + $request = new ServerRequest( + 'POST', + 'http://example.com/', + array(), + new HttpBodyStream($stream, 10) + ); + + $never = $this->expectCallableNever(); + $middleware($request, function (ServerRequestInterface $request) use ($never) { + $request->getBody()->close(); + $request->getBody()->on('data', $never); + }); + + $stream->write('hello'); + $stream->write('world'); + + $deferred->reject(new \RuntimeException()); + } + + public function testReceivesNextStreamingBodyAndDoesNotEmitDataIfExplicitlyPaused() + { + $deferred = new Deferred(); + $middleware = new LimitHandlersMiddleware(1); + $middleware(new ServerRequest('GET', 'http://example.com/'), function () use ($deferred) { + return $deferred->promise(); + }); + + $stream = new ThroughStream(); + $request = new ServerRequest( + 'POST', + 'http://example.com/', + array(), + new HttpBodyStream($stream, 10) + ); + + $never = $this->expectCallableNever(); + $middleware($request, function (ServerRequestInterface $request) use ($never) { + $request->getBody()->pause(); + $request->getBody()->on('data', $never); + }); + + $stream->write('hello'); + $stream->write('world'); + + $deferred->reject(new \RuntimeException()); + } + + public function testReceivesNextStreamingBodyAndDoesEmitDataImmediatelyIfExplicitlyResumed() + { + $deferred = new Deferred(); + $middleware = new LimitHandlersMiddleware(1); + $middleware(new ServerRequest('GET', 'http://example.com/'), function () use ($deferred) { + return $deferred->promise(); + }); + + $stream = new ThroughStream(); + $request = new ServerRequest( + 'POST', + 'http://example.com/', + array(), + new HttpBodyStream($stream, 10) + ); + + $once = $this->expectCallableOnceWith('helloworld'); + $never = $this->expectCallableNever(); + $middleware($request, function (ServerRequestInterface $request) use ($once, $never) { + $request->getBody()->on('data', $once); + $request->getBody()->resume(); + $request->getBody()->on('data', $never); + }); + + $stream->write('hello'); + $stream->write('world'); + + $deferred->reject(new \RuntimeException()); + } } From e02e369ae4ca37fcc4761dd48cba84c150f00905 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Wed, 6 Dec 2017 19:03:09 +0100 Subject: [PATCH 241/456] Support promise cancellation for queued next handlers --- src/Middleware/LimitHandlersMiddleware.php | 32 ++++++--- .../LimitHandlersMiddlewareTest.php | 70 +++++++++++++++++++ 2 files changed, 94 insertions(+), 8 deletions(-) diff --git a/src/Middleware/LimitHandlersMiddleware.php b/src/Middleware/LimitHandlersMiddleware.php index 69dd5c81..7d52a4a6 100644 --- a/src/Middleware/LimitHandlersMiddleware.php +++ b/src/Middleware/LimitHandlersMiddleware.php @@ -5,9 +5,9 @@ use Psr\Http\Message\ServerRequestInterface; use React\Http\Io\HttpBodyStream; use React\Http\Io\PauseBufferStream; +use React\Promise; use React\Promise\Deferred; use React\Stream\ReadableStreamInterface; -use SplQueue; /** * Limits how many next handlers can be executed concurrently. @@ -66,7 +66,7 @@ final class LimitHandlersMiddleware { private $limit; private $pending = 0; - private $queued; + private $queue = array(); /** * @param int $limit Maximum amount of concurrent requests handled. @@ -77,7 +77,6 @@ final class LimitHandlersMiddleware public function __construct($limit) { $this->limit = $limit; - $this->queued = new SplQueue(); } public function __invoke(ServerRequestInterface $request, $next) @@ -96,9 +95,21 @@ public function __invoke(ServerRequestInterface $request, $next) )); } - $deferred = new Deferred(); - $this->queued->enqueue($deferred); + // get next queue position + $queue =& $this->queue; + $queue[] = null; + end($queue); + $id = key($queue); + $deferred = new Deferred(function ($_, $reject) use (&$queue, $id) { + // queued promise cancelled before its next handler is invoked + // remove from queue and reject explicitly + unset($queue[$id]); + $reject(new \RuntimeException('Cancelled queued next handler')); + }); + + // queue request and process queue if pending does not exceed limit + $queue[$id] = $deferred; $this->processQueue(); $that = $this; @@ -117,11 +128,13 @@ public function __invoke(ServerRequestInterface $request, $next) })->then(function ($response) use ($that, &$pending) { $pending--; $that->processQueue(); + return $response; }, function ($error) use ($that, &$pending) { $pending--; $that->processQueue(); - return $error; + + return Promise\reject($error); }); } @@ -134,10 +147,13 @@ public function processQueue() return; } - if ($this->queued->count() === 0) { + if (!$this->queue) { return; } - $this->queued->dequeue()->resolve(); + $first = reset($this->queue); + unset($this->queue[key($this->queue)]); + + $first->resolve(); } } diff --git a/tests/Middleware/LimitHandlersMiddlewareTest.php b/tests/Middleware/LimitHandlersMiddlewareTest.php index b11490a3..d85fb3af 100644 --- a/tests/Middleware/LimitHandlersMiddlewareTest.php +++ b/tests/Middleware/LimitHandlersMiddlewareTest.php @@ -10,6 +10,7 @@ use React\Promise\Promise; use React\Stream\ThroughStream; use React\Tests\Http\TestCase; +use React\Promise\PromiseInterface; final class LimitHandlersMiddlewareTest extends TestCase { @@ -203,6 +204,75 @@ public function testReceivesNextRequestAfterPreviousHandlerIsSettled() $middleware($request, $this->expectCallableOnceWith($request)); } + public function testPendingRequestCanBeCancelledAndForwardsCancellationToInnerPromise() + { + $request = new ServerRequest( + 'POST', + 'http://example.com/', + array(), + 'hello' + ); + + $once = $this->expectCallableOnce(); + $deferred = new Deferred(function () use ($once) { + $once(); + throw new \RuntimeException('Cancelled'); + }); + $middleware = new LimitHandlersMiddleware(1); + $promise = $middleware($request, function () use ($deferred) { + return $deferred->promise(); + }); + + $this->assertTrue($promise instanceof PromiseInterface); + $promise->cancel(); + } + + public function testQueuedRequestCanBeCancelledBeforeItStartsProcessing() + { + $request = new ServerRequest( + 'POST', + 'http://example.com/', + array(), + 'hello' + ); + + $deferred = new Deferred(); + $middleware = new LimitHandlersMiddleware(1); + $middleware($request, function () use ($deferred) { + return $deferred->promise(); + }); + + $promise = $middleware($request, $this->expectCallableNever()); + + $this->assertTrue($promise instanceof PromiseInterface); + $promise->cancel(); + $promise->then(null, $this->expectCallableOnce()); + } + + public function testReceivesNextRequestAfterPreviousHandlerIsCancelled() + { + $request = new ServerRequest( + 'POST', + 'http://example.com/', + array(), + 'hello' + ); + + $deferred = new Deferred(function () { + throw new \RuntimeException('Cancelled'); + }); + $middleware = new LimitHandlersMiddleware(1); + $promise = $middleware($request, function () use ($deferred) { + return $deferred->promise(); + }); + + $this->assertTrue($promise instanceof PromiseInterface); + $promise->cancel(); + $promise->then(null, $this->expectCallableOnce()); + + $middleware($request, $this->expectCallableOnceWith($request)); + } + public function testReceivesNextStreamingBodyWithSameDataAfterPreviousHandlerIsSettled() { $stream = new ThroughStream(); From dc60cdd1eb72450a5f22e5474fd0b75ba282c266 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 8 Dec 2017 09:36:44 +0100 Subject: [PATCH 242/456] Rename to LimitConcurrentRequestsMiddleware --- README.md | 20 +++++------ examples/12-upload.php | 4 +-- ... => LimitConcurrentRequestsMiddleware.php} | 10 +++--- tests/FunctionalServerTest.php | 6 ++-- ...LimitConcurrentRequestsMiddlewareTest.php} | 34 +++++++++---------- 5 files changed, 37 insertions(+), 37 deletions(-) rename src/Middleware/{LimitHandlersMiddleware.php => LimitConcurrentRequestsMiddleware.php} (92%) rename tests/Middleware/{LimitHandlersMiddlewareTest.php => LimitConcurrentRequestsMiddlewareTest.php} (91%) diff --git a/README.md b/README.md index bda8c98f..bf4e51d2 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Event-driven, streaming plaintext HTTP and secure HTTPS server for [ReactPHP](ht * [Request](#request) * [Response](#response) * [Middleware](#middleware) - * [LimitHandlersMiddleware](#limithandlersmiddleware) + * [LimitConcurrentRequestsMiddleware](#limitconcurrentrequestsmiddleware) * [RequestBodyBufferMiddleware](#requestbodybuffermiddleware) * [RequestBodyParserMiddleware](#requestbodyparsermiddleware) * [Third-Party Middleware](#third-party-middleware) @@ -682,9 +682,9 @@ $server = new StreamingServer(new MiddlewareRunner([ ])); ``` -#### LimitHandlersMiddleware +#### LimitConcurrentRequestsMiddleware -The `LimitHandlersMiddleware` can be used to +The `LimitConcurrentRequestsMiddleware` can be used to limit how many next handlers can be executed concurrently. If this middleware is invoked, it will check if the number of pending @@ -703,7 +703,7 @@ than 10 handlers will be invoked at once: ```php $server = new StreamingServer(new MiddlewareRunner([ - new LimitHandlersMiddleware(10), + new LimitConcurrentRequestsMiddleware(10), $handler ])); ``` @@ -714,7 +714,7 @@ to limit the total number of requests that can be buffered at once: ```php $server = new StreamingServer(new MiddlewareRunner([ - new LimitHandlersMiddleware(100), // 100 concurrent buffering handlers + new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers new RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request new RequestBodyParserMiddleware(), $handler @@ -727,10 +727,10 @@ processes one request after another without any concurrency: ```php $server = new StreamingServer(new MiddlewareRunner([ - new LimitHandlersMiddleware(100), // 100 concurrent buffering handlers + new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers new RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request new RequestBodyParserMiddleware(), - new LimitHandlersMiddleware(1), // only execute 1 handler (no concurrency) + new LimitConcurrentRequestsMiddleware(1), // only execute 1 handler (no concurrency) $handler ])); ``` @@ -772,14 +772,14 @@ Note that the given buffer size limit is applied to each request individually. This means that if you allow a 2 MiB limit and then receive 1000 concurrent requests, up to 2000 MiB may be allocated for these buffers alone. As such, it's highly recommended to use this along with the -[`LimitHandlersMiddleware`](#limithandlersmiddleware) (see above) to limit +[`LimitConcurrentRequestsMiddleware`](#limitconcurrentrequestsmiddleware) (see above) to limit the total number of concurrent requests. Usage: ```php $middlewares = new MiddlewareRunner([ - new LimitHandlersMiddleware(100), // 100 concurrent buffering handlers + new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers new RequestBodyBufferMiddleware(16 * 1024 * 1024), // 16 MiB function (ServerRequestInterface $request, callable $next) { // The body from $request->getBody() is now fully available without the need to stream it @@ -838,7 +838,7 @@ $handler = function (ServerRequestInterface $request) { }; $server = new StreamingServer(new MiddlewareRunner([ - new LimitHandlersMiddleware(100), // 100 concurrent buffering handlers + new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers new RequestBodyBufferMiddleware(16 * 1024 * 1024), // 16 MiB new RequestBodyParserMiddleware(), $handler diff --git a/examples/12-upload.php b/examples/12-upload.php index ab8d2e1d..a2dc0d43 100644 --- a/examples/12-upload.php +++ b/examples/12-upload.php @@ -11,7 +11,7 @@ use Psr\Http\Message\UploadedFileInterface; use React\EventLoop\Factory; use React\Http\MiddlewareRunner; -use React\Http\Middleware\LimitHandlersMiddleware; +use React\Http\Middleware\LimitConcurrentRequestsMiddleware; use React\Http\Middleware\RequestBodyBufferMiddleware; use React\Http\Middleware\RequestBodyParserMiddleware; use React\Http\Response; @@ -122,7 +122,7 @@ // buffer and parse HTTP request body before running our request handler $server = new StreamingServer(new MiddlewareRunner(array( - new LimitHandlersMiddleware(100), // 100 concurrent buffering handlers, queue otherwise + new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers, queue otherwise new RequestBodyBufferMiddleware(8 * 1024 * 1024), // 8 MiB max, ignore body otherwise new RequestBodyParserMiddleware(100 * 1024, 1), // 1 file with 100 KiB max, reject upload otherwise $handler diff --git a/src/Middleware/LimitHandlersMiddleware.php b/src/Middleware/LimitConcurrentRequestsMiddleware.php similarity index 92% rename from src/Middleware/LimitHandlersMiddleware.php rename to src/Middleware/LimitConcurrentRequestsMiddleware.php index 7d52a4a6..862b39a5 100644 --- a/src/Middleware/LimitHandlersMiddleware.php +++ b/src/Middleware/LimitConcurrentRequestsMiddleware.php @@ -28,7 +28,7 @@ * * ```php * $server = new StreamingServer(new MiddlewareRunner([ - * new LimitHandlersMiddleware(10), + * new LimitConcurrentRequestsMiddleware(10), * $handler * ])); * ``` @@ -39,7 +39,7 @@ * * ```php * $server = new StreamingServer(new MiddlewareRunner([ - * new LimitHandlersMiddleware(100), // 100 concurrent buffering handlers + * new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers * new RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request * new RequestBodyParserMiddleware(), * $handler @@ -52,17 +52,17 @@ * * ```php * $server = new StreamingServer(new MiddlewareRunner([ - * new LimitHandlersMiddleware(100), // 100 concurrent buffering handlers + * new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers * new RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request * new RequestBodyParserMiddleware(), - * new LimitHandlersMiddleware(1), // only execute 1 handler (no concurrency) + * new LimitConcurrentRequestsMiddleware(1), // only execute 1 handler (no concurrency) * $handler * ])); * ``` * * @see RequestBodyBufferMiddleware */ -final class LimitHandlersMiddleware +final class LimitConcurrentRequestsMiddleware { private $limit; private $pending = 0; diff --git a/tests/FunctionalServerTest.php b/tests/FunctionalServerTest.php index b7a18880..c30c7f88 100644 --- a/tests/FunctionalServerTest.php +++ b/tests/FunctionalServerTest.php @@ -3,7 +3,7 @@ namespace React\Tests\Http; use Psr\Http\Message\ServerRequestInterface; -use React\Http\Middleware\LimitHandlersMiddleware; +use React\Http\Middleware\LimitConcurrentRequestsMiddleware; use React\Http\Middleware\RequestBodyBufferMiddleware; use React\Http\MiddlewareRunner; use React\Socket\Server as Socket; @@ -719,13 +719,13 @@ public function testConnectWithClosedThroughStreamReturnsNoData() $socket->close(); } - public function testLimitHandlersMiddlewareRequestStreamPausing() + public function testLimitConcurrentRequestsMiddlewareRequestStreamPausing() { $loop = Factory::create(); $connector = new Connector($loop); $server = new StreamingServer(new MiddlewareRunner(array( - new LimitHandlersMiddleware(5), + new LimitConcurrentRequestsMiddleware(5), new RequestBodyBufferMiddleware(16 * 1024 * 1024), // 16 MiB function (ServerRequestInterface $request, $next) use ($loop) { return new Promise\Promise(function ($resolve) use ($request, $loop, $next) { diff --git a/tests/Middleware/LimitHandlersMiddlewareTest.php b/tests/Middleware/LimitConcurrentRequestsMiddlewareTest.php similarity index 91% rename from tests/Middleware/LimitHandlersMiddlewareTest.php rename to tests/Middleware/LimitConcurrentRequestsMiddlewareTest.php index d85fb3af..ffe6facd 100644 --- a/tests/Middleware/LimitHandlersMiddlewareTest.php +++ b/tests/Middleware/LimitConcurrentRequestsMiddlewareTest.php @@ -5,14 +5,14 @@ use Psr\Http\Message\ServerRequestInterface; use React\Http\Io\HttpBodyStream; use React\Http\Io\ServerRequest; -use React\Http\Middleware\LimitHandlersMiddleware; +use React\Http\Middleware\LimitConcurrentRequestsMiddleware; use React\Promise\Deferred; use React\Promise\Promise; use React\Stream\ThroughStream; use React\Tests\Http\TestCase; use React\Promise\PromiseInterface; -final class LimitHandlersMiddlewareTest extends TestCase +final class LimitConcurrentRequestsMiddlewareTest extends TestCase { public function testLimitOneRequestConcurrently() { @@ -51,7 +51,7 @@ public function testLimitOneRequestConcurrently() * The handler * */ - $limitHandlers = new LimitHandlersMiddleware(1); + $limitHandlers = new LimitConcurrentRequestsMiddleware(1); $this->assertFalse($calledA); $this->assertFalse($calledB); @@ -99,7 +99,7 @@ public function testStreamPauseAndResume() $body = $this->getMockBuilder('React\Http\Io\HttpBodyStream')->disableOriginalConstructor()->getMock(); $body->expects($this->once())->method('pause'); $body->expects($this->once())->method('resume'); - $limitHandlers = new LimitHandlersMiddleware(1); + $limitHandlers = new LimitConcurrentRequestsMiddleware(1); $limitHandlers(new ServerRequest('GET', 'https://example.com/', array(), $body), function () {}); } @@ -113,7 +113,7 @@ public function testReceivesBufferedRequestSameInstance() ); $req = null; - $middleware = new LimitHandlersMiddleware(1); + $middleware = new LimitConcurrentRequestsMiddleware(1); $middleware($request, function (ServerRequestInterface $request) use (&$req) { $req = $request; }); @@ -132,7 +132,7 @@ public function testReceivesStreamingBodyChangesInstanceWithCustomBodyButSameDat ); $req = null; - $middleware = new LimitHandlersMiddleware(1); + $middleware = new LimitConcurrentRequestsMiddleware(1); $middleware($request, function (ServerRequestInterface $request) use (&$req) { $req = $request; }); @@ -159,7 +159,7 @@ public function testReceivesRequestsSequentially() 'hello' ); - $middleware = new LimitHandlersMiddleware(1); + $middleware = new LimitConcurrentRequestsMiddleware(1); $middleware($request, $this->expectCallableOnceWith($request)); $middleware($request, $this->expectCallableOnceWith($request)); $middleware($request, $this->expectCallableOnceWith($request)); @@ -174,7 +174,7 @@ public function testDoesNotReceiveNextRequestIfHandlerIsPending() 'hello' ); - $middleware = new LimitHandlersMiddleware(1); + $middleware = new LimitConcurrentRequestsMiddleware(1); $middleware($request, function () { return new Promise(function () { // NO-OP: pending promise @@ -194,7 +194,7 @@ public function testReceivesNextRequestAfterPreviousHandlerIsSettled() ); $deferred = new Deferred(); - $middleware = new LimitHandlersMiddleware(1); + $middleware = new LimitConcurrentRequestsMiddleware(1); $middleware($request, function () use ($deferred) { return $deferred->promise(); }); @@ -218,7 +218,7 @@ public function testPendingRequestCanBeCancelledAndForwardsCancellationToInnerPr $once(); throw new \RuntimeException('Cancelled'); }); - $middleware = new LimitHandlersMiddleware(1); + $middleware = new LimitConcurrentRequestsMiddleware(1); $promise = $middleware($request, function () use ($deferred) { return $deferred->promise(); }); @@ -237,7 +237,7 @@ public function testQueuedRequestCanBeCancelledBeforeItStartsProcessing() ); $deferred = new Deferred(); - $middleware = new LimitHandlersMiddleware(1); + $middleware = new LimitConcurrentRequestsMiddleware(1); $middleware($request, function () use ($deferred) { return $deferred->promise(); }); @@ -261,7 +261,7 @@ public function testReceivesNextRequestAfterPreviousHandlerIsCancelled() $deferred = new Deferred(function () { throw new \RuntimeException('Cancelled'); }); - $middleware = new LimitHandlersMiddleware(1); + $middleware = new LimitConcurrentRequestsMiddleware(1); $promise = $middleware($request, function () use ($deferred) { return $deferred->promise(); }); @@ -284,7 +284,7 @@ public function testReceivesNextStreamingBodyWithSameDataAfterPreviousHandlerIsS ); $deferred = new Deferred(); - $middleware = new LimitHandlersMiddleware(1); + $middleware = new LimitConcurrentRequestsMiddleware(1); $middleware($request, function () use ($deferred) { return $deferred->promise(); }); @@ -312,7 +312,7 @@ public function testReceivesNextStreamingBodyWithSameDataAfterPreviousHandlerIsS public function testReceivesNextStreamingBodyWithBufferedDataAfterPreviousHandlerIsSettled() { $deferred = new Deferred(); - $middleware = new LimitHandlersMiddleware(1); + $middleware = new LimitConcurrentRequestsMiddleware(1); $middleware(new ServerRequest('GET', 'http://example.com/'), function () use ($deferred) { return $deferred->promise(); }); @@ -339,7 +339,7 @@ public function testReceivesNextStreamingBodyWithBufferedDataAfterPreviousHandle public function testReceivesNextStreamingBodyAndDoesNotEmitDataIfExplicitlyClosed() { $deferred = new Deferred(); - $middleware = new LimitHandlersMiddleware(1); + $middleware = new LimitConcurrentRequestsMiddleware(1); $middleware(new ServerRequest('GET', 'http://example.com/'), function () use ($deferred) { return $deferred->promise(); }); @@ -367,7 +367,7 @@ public function testReceivesNextStreamingBodyAndDoesNotEmitDataIfExplicitlyClose public function testReceivesNextStreamingBodyAndDoesNotEmitDataIfExplicitlyPaused() { $deferred = new Deferred(); - $middleware = new LimitHandlersMiddleware(1); + $middleware = new LimitConcurrentRequestsMiddleware(1); $middleware(new ServerRequest('GET', 'http://example.com/'), function () use ($deferred) { return $deferred->promise(); }); @@ -395,7 +395,7 @@ public function testReceivesNextStreamingBodyAndDoesNotEmitDataIfExplicitlyPause public function testReceivesNextStreamingBodyAndDoesEmitDataImmediatelyIfExplicitlyResumed() { $deferred = new Deferred(); - $middleware = new LimitHandlersMiddleware(1); + $middleware = new LimitConcurrentRequestsMiddleware(1); $middleware(new ServerRequest('GET', 'http://example.com/'), function () use ($deferred) { return $deferred->promise(); }); From 1439879983595945c07796042bb1b0c2bd785dee Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Sat, 9 Dec 2017 20:42:46 +0100 Subject: [PATCH 243/456] Util::iniSizeToBytes that converts ini size settings to bytes --- src/Io/IniUtil.php | 48 ++++++++++++ src/Io/MultipartParser.php | 30 ++------ .../RequestBodyBufferMiddleware.php | 27 +------ tests/Io/IniUtilTest.php | 77 +++++++++++++++++++ 4 files changed, 132 insertions(+), 50 deletions(-) create mode 100644 src/Io/IniUtil.php create mode 100644 tests/Io/IniUtilTest.php diff --git a/src/Io/IniUtil.php b/src/Io/IniUtil.php new file mode 100644 index 00000000..d8bce2d3 --- /dev/null +++ b/src/Io/IniUtil.php @@ -0,0 +1,48 @@ +maxInputNestingLevel = (int)$var; } - $this->uploadMaxFilesize = $uploadMaxFilesize === null ? $this->iniUploadMaxFilesize() : (int)$uploadMaxFilesize; + if ($uploadMaxFilesize === null) { + $uploadMaxFilesize = IniUtil::iniSizeToBytes(ini_get('upload_max_filesize')); + } + + $this->uploadMaxFilesize = (int)$uploadMaxFilesize; $this->maxFileUploads = $maxFileUploads === null ? (ini_get('file_uploads') === '' ? 0 : (int)ini_get('max_file_uploads')) : (int)$maxFileUploads; } @@ -322,28 +326,4 @@ private function extractPost($postFields, $key, $value) return $postFields; } - - /** - * Gets upload_max_filesize from PHP's configuration expressed in bytes - * - * @return int - * @link http://php.net/manual/en/ini.core.php#ini.upload-max-filesize - * @codeCoverageIgnore - */ - private function iniUploadMaxFilesize() - { - $size = ini_get('upload_max_filesize'); - $suffix = strtoupper(substr($size, -1)); - if ($suffix === 'K') { - return substr($size, 0, -1) * 1024; - } - if ($suffix === 'M') { - return substr($size, 0, -1) * 1024 * 1024; - } - if ($suffix === 'G') { - return substr($size, 0, -1) * 1024 * 1024 * 1024; - } - - return $size; - } } diff --git a/src/Middleware/RequestBodyBufferMiddleware.php b/src/Middleware/RequestBodyBufferMiddleware.php index 7ae3f894..7ef2503b 100644 --- a/src/Middleware/RequestBodyBufferMiddleware.php +++ b/src/Middleware/RequestBodyBufferMiddleware.php @@ -4,6 +4,7 @@ use OverflowException; use Psr\Http\Message\ServerRequestInterface; +use React\Http\Io\IniUtil; use React\Promise\Stream; use React\Stream\ReadableStreamInterface; use RingCentral\Psr7\BufferStream; @@ -21,7 +22,7 @@ final class RequestBodyBufferMiddleware public function __construct($sizeLimit = null) { if ($sizeLimit === null) { - $sizeLimit = $this->iniMaxPostSize(); + $sizeLimit = IniUtil::iniSizeToBytes(ini_get('post_max_size')); } $this->sizeLimit = $sizeLimit; @@ -66,28 +67,4 @@ public function __invoke(ServerRequestInterface $request, $stack) throw $error; }); } - - /** - * Gets post_max_size from PHP's configuration expressed in bytes - * - * @return int - * @link http://php.net/manual/en/ini.core.php#ini.post-max-size - * @codeCoverageIgnore - */ - private function iniMaxPostSize() - { - $size = ini_get('post_max_size'); - $suffix = strtoupper(substr($size, -1)); - if ($suffix === 'K') { - return substr($size, 0, -1) * 1024; - } - if ($suffix === 'M') { - return substr($size, 0, -1) * 1024 * 1024; - } - if ($suffix === 'G') { - return substr($size, 0, -1) * 1024 * 1024 * 1024; - } - - return $size; - } } diff --git a/tests/Io/IniUtilTest.php b/tests/Io/IniUtilTest.php new file mode 100644 index 00000000..390abab1 --- /dev/null +++ b/tests/Io/IniUtilTest.php @@ -0,0 +1,77 @@ +assertEquals($output, IniUtil::iniSizeToBytes($input)); + } + + public function provideInvalidInputIniSizeToBytes() + { + return array( + array('-1G'), + array('0G'), + array(null), + array('foo'), + array('fooK'), + array('1ooL'), + array('1ooL'), + ); + } + + /** + * @dataProvider provideInvalidInputIniSizeToBytes + * @expectedException InvalidArgumentException + */ + public function testInvalidInputIniSizeToBytes($input) + { + IniUtil::iniSizeToBytes($input); + } +} From 0a75b4b627641f5ad8548f3e36af165adb09a9d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 9 Dec 2017 20:36:31 +0100 Subject: [PATCH 244/456] Support middleware request handlers as arrays by default --- README.md | 54 +++++++++++++------ examples/12-upload.php | 5 +- .../LimitConcurrentRequestsMiddleware.php | 14 ++--- src/StreamingServer.php | 10 ++-- tests/FunctionalServerTest.php | 37 ++----------- tests/StreamingServerTest.php | 11 ++-- 6 files changed, 63 insertions(+), 68 deletions(-) diff --git a/README.md b/README.md index bf4e51d2..38fb6e7e 100644 --- a/README.md +++ b/README.md @@ -659,19 +659,41 @@ passed explicitly. ### Middleware -Middleware can be added to the server using [`MiddlewareRunner`](src/MiddlewareRunner.php) -instead of the `callable`. A middleware is expected to adhere the following rules: +As documented above, the [`StreamingServer`](#streamingserver) accepts a single +request handler argument that is responsible for processing an incoming +HTTP request and then creating and returning an outgoing HTTP response. + +Many common use cases involve validating, processing, manipulating the incoming +HTTP request before passing it to the final business logic request handler. +As such, this project supports the concept of middleware request handlers. + +A middleware request handler is expected to adhere the following rules: * It is a `callable`. * It accepts `ServerRequestInterface` as first argument and optional `callable` as second argument. * It returns a `ResponseInterface` (or any promise which can be consumed by [`Promise\resolve`](http://reactphp.org/promise/#resolve) resolving to a `ResponseInterface`) * It calls `$next($request)` to continue processing the next middleware function or returns explicitly to abort the chain -The following example adds a middleware that adds the current time to the request as a -header (`Request-Time`) and middleware that always returns a 200 code without a body: +Note that this very simple definition allows you to use either anonymous +functions or any classes that use the magic `__invoke()` method. +This allows you to easily create custom middleware request handlers on the fly +or use a class based approach to ease using existing middleware implementations. + +While this project does provide the means to *use* middleware implementations, +it does not aim to *define* how middleware implementations should look like. +We realize that there's a vivid ecosystem of middleware implementations and +ongoing effort to standardize interfaces between these and support this goal. +As such, this project only bundles a few middleware implementations that are +required to match PHP's request behavior (see below) and otherwise actively +encourages [Third-Party Middleware](#third-party-middleware) implementations. + +In order to use middleware request handlers, simply pass an array with all +callables as defined above to the [`StreamingServer`](#streamingserver). +The following example adds a middleware request handler that adds the current time to the request as a +header (`Request-Time`) and a final request handler that always returns a 200 code without a body: ```php -$server = new StreamingServer(new MiddlewareRunner([ +$server = new StreamingServer(array( function (ServerRequestInterface $request, callable $next) { $request = $request->withHeader('Request-Time', time()); return $next($request); @@ -679,7 +701,7 @@ $server = new StreamingServer(new MiddlewareRunner([ function (ServerRequestInterface $request, callable $next) { return new Response(200); }, -])); +)); ``` #### LimitConcurrentRequestsMiddleware @@ -702,10 +724,10 @@ The following example shows how this middleware can be used to ensure no more than 10 handlers will be invoked at once: ```php -$server = new StreamingServer(new MiddlewareRunner([ +$server = new StreamingServer(array( new LimitConcurrentRequestsMiddleware(10), $handler -])); +)); ``` Similarly, this middleware is often used in combination with the @@ -713,12 +735,12 @@ Similarly, this middleware is often used in combination with the to limit the total number of requests that can be buffered at once: ```php -$server = new StreamingServer(new MiddlewareRunner([ +$server = new StreamingServer(array( new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers new RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request new RequestBodyParserMiddleware(), $handler -])); +)); ``` More sophisticated examples include limiting the total number of requests @@ -726,13 +748,13 @@ that can be buffered at once and then ensure the actual request handler only processes one request after another without any concurrency: ```php -$server = new StreamingServer(new MiddlewareRunner([ +$server = new StreamingServer(array( new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers new RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request new RequestBodyParserMiddleware(), new LimitConcurrentRequestsMiddleware(1), // only execute 1 handler (no concurrency) $handler -])); +)); ``` #### RequestBodyBufferMiddleware @@ -778,14 +800,14 @@ the total number of concurrent requests. Usage: ```php -$middlewares = new MiddlewareRunner([ +$server = new StreamServer(array( new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers new RequestBodyBufferMiddleware(16 * 1024 * 1024), // 16 MiB function (ServerRequestInterface $request, callable $next) { // The body from $request->getBody() is now fully available without the need to stream it return new Response(200); }, -]); +)); ``` #### RequestBodyParserMiddleware @@ -837,12 +859,12 @@ $handler = function (ServerRequestInterface $request) { ); }; -$server = new StreamingServer(new MiddlewareRunner([ +$server = new StreamingServer(array(( new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers new RequestBodyBufferMiddleware(16 * 1024 * 1024), // 16 MiB new RequestBodyParserMiddleware(), $handler -])); +)); ``` See also [example #12](examples) for more details. diff --git a/examples/12-upload.php b/examples/12-upload.php index a2dc0d43..5d0a5636 100644 --- a/examples/12-upload.php +++ b/examples/12-upload.php @@ -10,7 +10,6 @@ use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\UploadedFileInterface; use React\EventLoop\Factory; -use React\Http\MiddlewareRunner; use React\Http\Middleware\LimitConcurrentRequestsMiddleware; use React\Http\Middleware\RequestBodyBufferMiddleware; use React\Http\Middleware\RequestBodyParserMiddleware; @@ -121,12 +120,12 @@ }; // buffer and parse HTTP request body before running our request handler -$server = new StreamingServer(new MiddlewareRunner(array( +$server = new StreamingServer(array( new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers, queue otherwise new RequestBodyBufferMiddleware(8 * 1024 * 1024), // 8 MiB max, ignore body otherwise new RequestBodyParserMiddleware(100 * 1024, 1), // 1 file with 100 KiB max, reject upload otherwise $handler -))); +)); $socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); $server->listen($socket); diff --git a/src/Middleware/LimitConcurrentRequestsMiddleware.php b/src/Middleware/LimitConcurrentRequestsMiddleware.php index 862b39a5..3e16a95f 100644 --- a/src/Middleware/LimitConcurrentRequestsMiddleware.php +++ b/src/Middleware/LimitConcurrentRequestsMiddleware.php @@ -27,10 +27,10 @@ * than 10 handlers will be invoked at once: * * ```php - * $server = new StreamingServer(new MiddlewareRunner([ + * $server = new StreamingServer(array( * new LimitConcurrentRequestsMiddleware(10), * $handler - * ])); + * )); * ``` * * Similarly, this middleware is often used in combination with the @@ -38,12 +38,12 @@ * to limit the total number of requests that can be buffered at once: * * ```php - * $server = new StreamingServer(new MiddlewareRunner([ + * $server = new StreamingServer(array( * new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers * new RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request * new RequestBodyParserMiddleware(), - * $handler - * ])); + * $handler + * )); * ``` * * More sophisticated examples include limiting the total number of requests @@ -51,13 +51,13 @@ * processes one request after another without any concurrency: * * ```php - * $server = new StreamingServer(new MiddlewareRunner([ + * $server = new StreamingServer(array( * new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers * new RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request * new RequestBodyParserMiddleware(), * new LimitConcurrentRequestsMiddleware(1), // only execute 1 handler (no concurrency) * $handler - * ])); + * )); * ``` * * @see RequestBodyBufferMiddleware diff --git a/src/StreamingServer.php b/src/StreamingServer.php index b6285006..9fc1f5b1 100644 --- a/src/StreamingServer.php +++ b/src/StreamingServer.php @@ -93,16 +93,18 @@ class StreamingServer extends EventEmitter * connections in order to then parse incoming data as HTTP. * See also [listen()](#listen) for more details. * - * @param callable $callback + * @param callable|callable[] $requestHandler * @see self::listen() */ - public function __construct($callback) + public function __construct($requestHandler) { - if (!is_callable($callback)) { + if (is_array($requestHandler)) { + $requestHandler = new MiddlewareRunner($requestHandler); + } elseif (!is_callable($requestHandler)) { throw new \InvalidArgumentException(); } - $this->callback = $callback; + $this->callback = $requestHandler; } /** diff --git a/tests/FunctionalServerTest.php b/tests/FunctionalServerTest.php index c30c7f88..7bbb8ff3 100644 --- a/tests/FunctionalServerTest.php +++ b/tests/FunctionalServerTest.php @@ -5,7 +5,6 @@ use Psr\Http\Message\ServerRequestInterface; use React\Http\Middleware\LimitConcurrentRequestsMiddleware; use React\Http\Middleware\RequestBodyBufferMiddleware; -use React\Http\MiddlewareRunner; use React\Socket\Server as Socket; use React\EventLoop\Factory; use React\Http\StreamingServer; @@ -47,42 +46,16 @@ public function testPlainHttpOnRandomPort() $socket->close(); } - public function testPlainHttpOnRandomPortWithMiddlewareRunner() + public function testPlainHttpOnRandomPortWithSingleRequestHandlerArray() { $loop = Factory::create(); $connector = new Connector($loop); - $server = new StreamingServer(new MiddlewareRunner(array(function (RequestInterface $request) { - return new Response(200, array(), (string)$request->getUri()); - }))); - - $socket = new Socket(0, $loop); - $server->listen($socket); - - $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { - $conn->write("GET / HTTP/1.0\r\nHost: " . noScheme($conn->getRemoteAddress()) . "\r\n\r\n"); - - return Stream\buffer($conn); - }); - - $response = Block\await($result, $loop, 1.0); - - $this->assertContains("HTTP/1.0 200 OK", $response); - $this->assertContains('http://' . noScheme($socket->getAddress()) . '/', $response); - - $socket->close(); - } - - public function testPlainHttpOnRandomPortWithEmptyMiddlewareRunner() - { - $loop = Factory::create(); - $connector = new Connector($loop); - - $server = new StreamingServer(new MiddlewareRunner(array( + $server = new StreamingServer(array( function () { return new Response(404); }, - ))); + )); $socket = new Socket(0, $loop); $server->listen($socket); @@ -724,7 +697,7 @@ public function testLimitConcurrentRequestsMiddlewareRequestStreamPausing() $loop = Factory::create(); $connector = new Connector($loop); - $server = new StreamingServer(new MiddlewareRunner(array( + $server = new StreamingServer(array( new LimitConcurrentRequestsMiddleware(5), new RequestBodyBufferMiddleware(16 * 1024 * 1024), // 16 MiB function (ServerRequestInterface $request, $next) use ($loop) { @@ -737,7 +710,7 @@ function (ServerRequestInterface $request, $next) use ($loop) { function (ServerRequestInterface $request) { return new Response(200, array(), (string)strlen((string)$request->getBody())); } - ))); + )); $socket = new Socket(0, $loop); $server->listen($socket); diff --git a/tests/StreamingServerTest.php b/tests/StreamingServerTest.php index 3396c227..f28d7e28 100644 --- a/tests/StreamingServerTest.php +++ b/tests/StreamingServerTest.php @@ -2,12 +2,11 @@ namespace React\Tests\Http; -use React\Http\MiddlewareRunner; -use React\Http\StreamingServer; use Psr\Http\Message\ServerRequestInterface; use React\Http\Response; -use React\Stream\ThroughStream; +use React\Http\StreamingServer; use React\Promise\Promise; +use React\Stream\ThroughStream; class StreamingServerTest extends TestCase { @@ -96,14 +95,14 @@ public function testRequestEvent() $this->assertSame('127.0.0.1', $serverParams['REMOTE_ADDR']); } - public function testRequestEventWithMiddlewareRunner() + public function testRequestEventWithSingleRequestHandlerArray() { $i = 0; $requestAssertion = null; - $server = new StreamingServer(new MiddlewareRunner(array(function (ServerRequestInterface $request) use (&$i, &$requestAssertion) { + $server = new StreamingServer(array(function (ServerRequestInterface $request) use (&$i, &$requestAssertion) { $i++; $requestAssertion = $request; - }))); + })); $this->connection ->expects($this->any()) From cd333373ce052ed9a0c548532058d56eaca623ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 10 Dec 2017 14:50:16 +0100 Subject: [PATCH 245/456] Mark MiddlewareRunner as internal and move to Io namespace --- src/{ => Io}/MiddlewareRunner.php | 7 ++++++- src/StreamingServer.php | 1 + tests/{ => Io}/MiddlewareRunnerTest.php | 5 +++-- tests/benchmark-middleware-runner.php | 2 +- 4 files changed, 11 insertions(+), 4 deletions(-) rename src/{ => Io}/MiddlewareRunner.php (91%) rename tests/{ => Io}/MiddlewareRunnerTest.php (99%) diff --git a/src/MiddlewareRunner.php b/src/Io/MiddlewareRunner.php similarity index 91% rename from src/MiddlewareRunner.php rename to src/Io/MiddlewareRunner.php index 849b644c..496c71a4 100644 --- a/src/MiddlewareRunner.php +++ b/src/Io/MiddlewareRunner.php @@ -1,12 +1,17 @@ Date: Sun, 3 Dec 2017 21:15:12 +0100 Subject: [PATCH 246/456] Server Facade --- README.md | 16 ++++++- src/Server.php | 95 +++++++++++++++++++++++++++++++++++++++ tests/ServerTest.php | 103 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 213 insertions(+), 1 deletion(-) create mode 100644 src/Server.php create mode 100644 tests/ServerTest.php diff --git a/README.md b/README.md index bf4e51d2..dbc1c802 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ Event-driven, streaming plaintext HTTP and secure HTTPS server for [ReactPHP](ht * [Quickstart example](#quickstart-example) * [Usage](#usage) + * [Server](#server) * [StreamingServer](#streamingserver) * [Request](#request) * [Response](#response) @@ -28,7 +29,7 @@ This is an HTTP server which responds with `Hello World` to every request. ```php $loop = React\EventLoop\Factory::create(); -$server = new StreamingServer(function (ServerRequestInterface $request) { +$server = new Server(function (ServerRequestInterface $request) { return new Response( 200, array('Content-Type' => 'text/plain'), @@ -46,6 +47,19 @@ See also the [examples](examples). ## Usage +### Server + +For most users a server that buffers and parses a requests before handling it over as a +PSR-7 request is what they want. The `Server` facade takes care of that, and takes the more +advanced configuration out of hand. Under the hood it uses [StreamingServer](#streamingserver) +with the the three stock middleware using default settings from `php.ini`. + +The [LimitConcurrentRequestsMiddleware](#limitconcurrentrequestsmiddleware) requires a limit, +as such the `Server` facade uses the `memory_limit` and `post_max_size` ini settings to +calculate a sensible limit. It assumes a maximum of a quarter of the `memory_limit` for +buffering and the other three quarter for parsing and handling the requests. The limit is +division of half of `memory_limit` by `memory_limit` rounded up. + ### StreamingServer The `StreamingServer` class is responsible for handling incoming connections and then diff --git a/src/Server.php b/src/Server.php new file mode 100644 index 00000000..f465a643 --- /dev/null +++ b/src/Server.php @@ -0,0 +1,95 @@ +getConcurrentRequestsLimit()); + $middleware[] = new RequestBodyBufferMiddleware(); + // Checking for an empty string because that is what a boolean + // false is returned as by ini_get depending on the PHP version. + // @link http://php.net/manual/en/ini.core.php#ini.enable-post-data-reading + // @link http://php.net/manual/en/function.ini-get.php#refsect1-function.ini-get-notes + // @link https://3v4l.org/qJtsa + $enablePostDataReading = ini_get('enable_post_data_reading'); + if ($enablePostDataReading !== '') { + $middleware[] = new RequestBodyParserMiddleware(); + } + $middleware[] = $callback; + + $this->streamingServer = new StreamingServer(new MiddlewareRunner($middleware)); + } + + /** + * @see StreamingServer::listen() + */ + public function listen(ServerInterface $server) + { + $this->streamingServer->listen($server); + } + + private function getConcurrentRequestsLimit() + { + if (ini_get('memory_limit') == -1) { + return self::MAXIMUM_CONCURRENT_REQUESTS; + } + + $availableMemory = IniUtil::iniSizeToBytes(ini_get('memory_limit')) / 4; + $concurrentRequests = $availableMemory / IniUtil::iniSizeToBytes(ini_get('post_max_size')); + $concurrentRequests = ceil($concurrentRequests); + + if ($concurrentRequests >= self::MAXIMUM_CONCURRENT_REQUESTS) { + return self::MAXIMUM_CONCURRENT_REQUESTS; + } + + return $concurrentRequests; + } +} diff --git a/tests/ServerTest.php b/tests/ServerTest.php new file mode 100644 index 00000000..ca8f9f6b --- /dev/null +++ b/tests/ServerTest.php @@ -0,0 +1,103 @@ +connection = $this->getMockBuilder('React\Socket\Connection') + ->disableOriginalConstructor() + ->setMethods( + array( + 'write', + 'end', + 'close', + 'pause', + 'resume', + 'isReadable', + 'isWritable', + 'getRemoteAddress', + 'getLocalAddress', + 'pipe' + ) + ) + ->getMock(); + + $this->connection->method('isWritable')->willReturn(true); + $this->connection->method('isReadable')->willReturn(true); + + $this->socket = new SocketServerStub(); + } + + public function testPostFileUpload() + { + $loop = Factory::create(); + $deferred = new Deferred(); + $server = new Server(function (ServerRequestInterface $request) use ($deferred) { + $deferred->resolve($request); + }); + + $server->listen($this->socket); + $this->socket->emit('connection', array($this->connection)); + + $connection = $this->connection; + $data = $this->createPostFileUploadRequest(); + $loop->addPeriodicTimer(0.01, function (TimerInterface $timer) use ($loop, &$data, $connection) { + $line = array_shift($data); + $connection->emit('data', array($line)); + + if (count($data) === 0) { + $loop->cancelTimer($timer); + } + }); + + $parsedRequest = Block\await($deferred->promise(), $loop); + $this->assertNotEmpty($parsedRequest->getUploadedFiles()); + $this->assertEmpty($parsedRequest->getParsedBody()); + + $files = $parsedRequest->getUploadedFiles(); + + $this->assertTrue(isset($files['file'])); + $this->assertCount(1, $files); + + $this->assertSame('hello.txt', $files['file']->getClientFilename()); + $this->assertSame('text/plain', $files['file']->getClientMediaType()); + $this->assertSame("hello\r\n", (string)$files['file']->getStream()); + } + + private function createPostFileUploadRequest() + { + $boundary = "---------------------------5844729766471062541057622570"; + + $data = array(); + $data[] = "POST / HTTP/1.1\r\n"; + $data[] = "Content-Type: multipart/form-data; boundary=" . $boundary . "\r\n"; + $data[] = "Content-Length: 220\r\n"; + $data[] = "\r\n"; + $data[] = "--$boundary\r\n"; + $data[] = "Content-Disposition: form-data; name=\"file\"; filename=\"hello.txt\"\r\n"; + $data[] = "Content-type: text/plain\r\n"; + $data[] = "\r\n"; + $data[] = "hello\r\n"; + $data[] = "\r\n"; + $data[] = "--$boundary--\r\n"; + + return $data; + } +} From cdecf8992e18743223c3d47cd40bcc4acdd1ccf8 Mon Sep 17 00:00:00 2001 From: Jan Sorgalla Date: Mon, 11 Dec 2017 08:01:50 +0100 Subject: [PATCH 247/456] Add missing use statement for MiddlewareRunner after moving to Io subnamespace --- src/Server.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Server.php b/src/Server.php index f465a643..8a2e5492 100644 --- a/src/Server.php +++ b/src/Server.php @@ -3,6 +3,7 @@ namespace React\Http; use React\Http\Io\IniUtil; +use React\Http\Io\MiddlewareRunner; use React\Http\Middleware\LimitConcurrentRequestsMiddleware; use React\Http\Middleware\RequestBodyBufferMiddleware; use React\Http\Middleware\RequestBodyParserMiddleware; From 3d891f4afadd0396f59390e1eb593649052b05be Mon Sep 17 00:00:00 2001 From: andig Date: Mon, 11 Dec 2017 08:45:33 +0100 Subject: [PATCH 248/456] Fix typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bf7c26c3..51a7df0b 100644 --- a/README.md +++ b/README.md @@ -814,7 +814,7 @@ the total number of concurrent requests. Usage: ```php -$server = new StreamServer(array( +$server = new StreamingServer(array( new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers new RequestBodyBufferMiddleware(16 * 1024 * 1024), // 16 MiB function (ServerRequestInterface $request, callable $next) { From 5caba4f0f407df68dd95e7d7ef37bc323f84d313 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Mon, 11 Dec 2017 15:13:23 +0100 Subject: [PATCH 249/456] Let StreamingServer wrap Server middleware list --- src/Server.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Server.php b/src/Server.php index 8a2e5492..9936348e 100644 --- a/src/Server.php +++ b/src/Server.php @@ -3,7 +3,6 @@ namespace React\Http; use React\Http\Io\IniUtil; -use React\Http\Io\MiddlewareRunner; use React\Http\Middleware\LimitConcurrentRequestsMiddleware; use React\Http\Middleware\RequestBodyBufferMiddleware; use React\Http\Middleware\RequestBodyParserMiddleware; @@ -66,7 +65,7 @@ public function __construct($callback) } $middleware[] = $callback; - $this->streamingServer = new StreamingServer(new MiddlewareRunner($middleware)); + $this->streamingServer = new StreamingServer($middleware); } /** From 0b171c8e7e1a91162b95ef15bed78382bd48b127 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Sun, 10 Dec 2017 15:37:21 +0100 Subject: [PATCH 250/456] Allow ini-like filesize for RequestBodyBufferMiddleware and MultipartParser --- src/Io/MultipartParser.php | 6 ++-- .../RequestBodyBufferMiddleware.php | 13 ++++---- .../RequestBodyParserMiddleware.php | 2 +- tests/Io/MultipartParserTest.php | 33 +++++++++++++++++++ .../RequestBodyBufferMiddlewareTest.php | 29 +++++++++++++++- 5 files changed, 72 insertions(+), 11 deletions(-) diff --git a/src/Io/MultipartParser.php b/src/Io/MultipartParser.php index 47422009..0c196f97 100644 --- a/src/Io/MultipartParser.php +++ b/src/Io/MultipartParser.php @@ -68,7 +68,7 @@ final class MultipartParser private $emptyCount = 0; /** - * @param int|null $uploadMaxFilesize + * @param int|string|null $uploadMaxFilesize * @param int|null $maxFileUploads */ public function __construct($uploadMaxFilesize = null, $maxFileUploads = null) @@ -83,10 +83,10 @@ public function __construct($uploadMaxFilesize = null, $maxFileUploads = null) } if ($uploadMaxFilesize === null) { - $uploadMaxFilesize = IniUtil::iniSizeToBytes(ini_get('upload_max_filesize')); + $uploadMaxFilesize = ini_get('upload_max_filesize'); } - $this->uploadMaxFilesize = (int)$uploadMaxFilesize; + $this->uploadMaxFilesize = IniUtil::iniSizeToBytes($uploadMaxFilesize); $this->maxFileUploads = $maxFileUploads === null ? (ini_get('file_uploads') === '' ? 0 : (int)ini_get('max_file_uploads')) : (int)$maxFileUploads; } diff --git a/src/Middleware/RequestBodyBufferMiddleware.php b/src/Middleware/RequestBodyBufferMiddleware.php index 7ef2503b..503bc9fd 100644 --- a/src/Middleware/RequestBodyBufferMiddleware.php +++ b/src/Middleware/RequestBodyBufferMiddleware.php @@ -14,18 +14,19 @@ final class RequestBodyBufferMiddleware private $sizeLimit; /** - * @param int|null $sizeLimit Either an int with the max request body size - * or null to use post_max_size from PHP's - * configuration. (Note that the value from - * the CLI configuration will be used.) + * @param int|string|null $sizeLimit Either an int with the max request body size + * in bytes or an ini like size string + * or null to use post_max_size from PHP's + * configuration. (Note that the value from + * the CLI configuration will be used.) */ public function __construct($sizeLimit = null) { if ($sizeLimit === null) { - $sizeLimit = IniUtil::iniSizeToBytes(ini_get('post_max_size')); + $sizeLimit = ini_get('post_max_size'); } - $this->sizeLimit = $sizeLimit; + $this->sizeLimit = IniUtil::iniSizeToBytes($sizeLimit); } public function __invoke(ServerRequestInterface $request, $stack) diff --git a/src/Middleware/RequestBodyParserMiddleware.php b/src/Middleware/RequestBodyParserMiddleware.php index 63759642..005c28cd 100644 --- a/src/Middleware/RequestBodyParserMiddleware.php +++ b/src/Middleware/RequestBodyParserMiddleware.php @@ -10,7 +10,7 @@ final class RequestBodyParserMiddleware private $multipart; /** - * @param int|null $uploadMaxFilesize + * @param int|string|null $uploadMaxFilesize * @param int|null $maxFileUploads */ public function __construct($uploadMaxFilesize = null, $maxFileUploads = null) diff --git a/tests/Io/MultipartParserTest.php b/tests/Io/MultipartParserTest.php index b070c93b..45ac0c4d 100644 --- a/tests/Io/MultipartParserTest.php +++ b/tests/Io/MultipartParserTest.php @@ -721,6 +721,39 @@ public function testUploadTooLargeFile() $this->assertSame(UPLOAD_ERR_INI_SIZE, $file->getError()); } + public function testUploadTooLargeFileWithIniLikeSize() + { + $boundary = "---------------------------12758086162038677464950549563"; + + $data = "--$boundary\r\n"; + $data .= "Content-Disposition: form-data; name=\"file\"; filename=\"hello\"\r\n"; + $data .= "Content-type: text/plain\r\n"; + $data .= "\r\n"; + $data .= str_repeat('world', 1024) . "\r\n"; + $data .= "--$boundary--\r\n"; + + $request = new ServerRequest('POST', 'http://example.com/', array( + 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, + ), $data, 1.1); + + $parser = new MultipartParser('1K'); + $parsedRequest = $parser->parse($request); + + $files = $parsedRequest->getUploadedFiles(); + + $this->assertCount(1, $files); + $this->assertTrue(isset($files['file'])); + $this->assertInstanceOf('Psr\Http\Message\UploadedFileInterface', $files['file']); + + /* @var $file \Psr\Http\Message\UploadedFileInterface */ + $file = $files['file']; + + $this->assertSame('hello', $file->getClientFilename()); + $this->assertSame('text/plain', $file->getClientMediaType()); + $this->assertSame(5120, $file->getSize()); + $this->assertSame(UPLOAD_ERR_INI_SIZE, $file->getError()); + } + public function testUploadNoFile() { $boundary = "---------------------------12758086162038677464950549563"; diff --git a/tests/Middleware/RequestBodyBufferMiddlewareTest.php b/tests/Middleware/RequestBodyBufferMiddlewareTest.php index 91bd14cc..913a733f 100644 --- a/tests/Middleware/RequestBodyBufferMiddlewareTest.php +++ b/tests/Middleware/RequestBodyBufferMiddlewareTest.php @@ -46,7 +46,7 @@ public function testAlreadyBufferedResolvesImmediately() { $size = 1024; $body = str_repeat('x', $size); - $stream = new BufferStream($size); + $stream = new BufferStream(1024); $stream->write($body); $serverRequest = new ServerRequest( 'GET', @@ -93,6 +93,33 @@ function (ServerRequestInterface $request) { $this->assertSame('', $response->getBody()->getContents()); } + public function testKnownExcessiveSizedWithIniLikeSize() + { + $loop = Factory::create(); + + $stream = new ThroughStream(); + $loop->addTimer(0.001, function () use ($stream) { + $stream->end(str_repeat('a', 2048)); + }); + $serverRequest = new ServerRequest( + 'GET', + 'https://example.com/', + array(), + new HttpBodyStream($stream, 2048) + ); + + $buffer = new RequestBodyBufferMiddleware('1K'); + $response = Block\await($buffer( + $serverRequest, + function (ServerRequestInterface $request) { + return new Response(200, array(), $request->getBody()->getContents()); + } + ), $loop); + + $this->assertSame(200, $response->getStatusCode()); + $this->assertSame('', $response->getBody()->getContents()); + } + public function testAlreadyBufferedExceedingSizeResolvesImmediatelyWithEmptyBody() { $serverRequest = new ServerRequest( From 5d17fd969249c91d8e15afb448f3d569637726b0 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Tue, 12 Dec 2017 01:33:14 +0100 Subject: [PATCH 251/456] Mark StreamingServer final --- src/StreamingServer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/StreamingServer.php b/src/StreamingServer.php index 723c508a..f2e78ab4 100644 --- a/src/StreamingServer.php +++ b/src/StreamingServer.php @@ -82,7 +82,7 @@ * @see Response * @see self::listen() */ -class StreamingServer extends EventEmitter +final class StreamingServer extends EventEmitter { private $callback; From 9cb64ff0f7be5b6934c18719970b4faff160e0b7 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Mon, 11 Dec 2017 15:34:16 +0100 Subject: [PATCH 252/456] Server forward the error event from StreamingServer --- README.md | 2 ++ src/Server.php | 10 +++++++++- tests/ServerTest.php | 28 +++++++++++++++++++++++----- 3 files changed, 34 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 51a7df0b..ec8d0c4b 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,8 @@ calculate a sensible limit. It assumes a maximum of a quarter of the `memory_lim buffering and the other three quarter for parsing and handling the requests. The limit is division of half of `memory_limit` by `memory_limit` rounded up. +> Note that any errors emitted by the wrapped `StreamingServer` are forwarded by `Server`. + ### StreamingServer The `StreamingServer` class is responsible for handling incoming connections and then diff --git a/src/Server.php b/src/Server.php index 9936348e..f77d2281 100644 --- a/src/Server.php +++ b/src/Server.php @@ -2,6 +2,7 @@ namespace React\Http; +use Evenement\EventEmitter; use React\Http\Io\IniUtil; use React\Http\Middleware\LimitConcurrentRequestsMiddleware; use React\Http\Middleware\RequestBodyBufferMiddleware; @@ -29,8 +30,10 @@ * - file_uploads * - max_file_uploads * - enable_post_data_reading + * + * Forwards the error event coming from StreamingServer. */ -final class Server +final class Server extends EventEmitter { /** * @internal @@ -66,6 +69,11 @@ public function __construct($callback) $middleware[] = $callback; $this->streamingServer = new StreamingServer($middleware); + + $that = $this; + $this->streamingServer->on('error', function ($error) use ($that) { + $that->emit('error', array($error)); + }); } /** diff --git a/tests/ServerTest.php b/tests/ServerTest.php index ca8f9f6b..fa7cb71f 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -4,15 +4,11 @@ use React\EventLoop\Factory; use React\EventLoop\Timer\TimerInterface; -use React\Http\MiddlewareRunner; use React\Http\Server; -use React\Http\StreamingServer; use Psr\Http\Message\ServerRequestInterface; -use React\Http\Response; use React\Promise\Deferred; use Clue\React\Block; -use React\Stream\ThroughStream; -use React\Promise\Promise; +use React\Promise; final class ServerTest extends TestCase { @@ -81,6 +77,28 @@ public function testPostFileUpload() $this->assertSame("hello\r\n", (string)$files['file']->getStream()); } + public function testForwardErrors() + { + $exception = new \Exception(); + $capturedException = null; + $server = new Server(function () use ($exception) { + return Promise\reject($exception); + }); + $server->on('error', function ($error) use (&$capturedException) { + $capturedException = $error; + }); + + $server->listen($this->socket); + $this->socket->emit('connection', array($this->connection)); + + $data = $this->createPostFileUploadRequest(); + $this->connection->emit('data', array(implode('', $data))); + + $this->assertInstanceOf('RuntimeException', $capturedException); + $this->assertInstanceOf('Exception', $capturedException->getPrevious()); + $this->assertSame($exception, $capturedException->getPrevious()); + } + private function createPostFileUploadRequest() { $boundary = "---------------------------5844729766471062541057622570"; From 7eb899510f6e27b72a8922c3430d98baeafe5d2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 12 Dec 2017 11:05:17 +0100 Subject: [PATCH 253/456] Recommend Server by default instead of advanced StreamingServer --- README.md | 149 ++++++++++++++++++++++------- examples/01-hello-world.php | 4 +- examples/02-count-visitors.php | 8 +- examples/03-client-ip.php | 8 +- examples/04-query-parameter.php | 8 +- examples/05-cookie-handling.php | 8 +- examples/06-sleep.php | 8 +- examples/07-error-handling.php | 8 +- examples/08-stream-response.php | 10 +- examples/09-stream-request.php | 11 ++- examples/11-hello-world-https.php | 8 +- examples/12-upload.php | 7 +- examples/21-http-proxy.php | 16 +++- examples/22-connect-proxy.php | 17 +++- examples/31-upgrade-echo.php | 15 ++- examples/32-upgrade-chat.php | 15 ++- examples/99-benchmark-download.php | 15 ++- 17 files changed, 231 insertions(+), 84 deletions(-) diff --git a/README.md b/README.md index ec8d0c4b..16db8e23 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,9 @@ $loop = React\EventLoop\Factory::create(); $server = new Server(function (ServerRequestInterface $request) { return new Response( 200, - array('Content-Type' => 'text/plain'), + array( + 'Content-Type' => 'text/plain' + ), "Hello World!\n" ); }); @@ -49,6 +51,28 @@ See also the [examples](examples). ### Server +The `Server` class is responsible for handling incoming connections and then +processing each incoming HTTP request. + +It buffers and parses the complete incoming HTTP request in memory. Once the +complete request has been received, it will invoke the request handler. + +For each request, it executes the callback function passed to the +constructor with the respective [request](#request) object and expects +a respective [response](#response) object in return. + +```php +$server = new Server(function (ServerRequestInterface $request) { + return new Response( + 200, + array( + 'Content-Type' => 'text/plain' + ), + "Hello World!\n" + ); +}); +``` + For most users a server that buffers and parses a requests before handling it over as a PSR-7 request is what they want. The `Server` facade takes care of that, and takes the more advanced configuration out of hand. Under the hood it uses [StreamingServer](#streamingserver) @@ -64,9 +88,13 @@ division of half of `memory_limit` by `memory_limit` rounded up. ### StreamingServer -The `StreamingServer` class is responsible for handling incoming connections and then +The advanced `StreamingServer` class is responsible for handling incoming connections and then processing each incoming HTTP request. +Unlike the [`Server`](#server) class, it does not buffer and parse the incoming +HTTP request body by default. This means that the request handler will be +invoked with a streaming request body. + For each request, it executes the callback function passed to the constructor with the respective [request](#request) object and expects a respective [response](#response) object in return. @@ -75,7 +103,9 @@ a respective [response](#response) object in return. $server = new StreamingServer(function (ServerRequestInterface $request) { return new Response( 200, - array('Content-Type' => 'text/plain'), + array( + 'Content-Type' => 'text/plain' + ), "Hello World!\n" ); }); @@ -160,10 +190,11 @@ Check out [request](#request) for more details. ### Request -An seen above, the `StreamingServer` class is responsible for handling incoming -connections and then processing each incoming HTTP request. +As seen above, the [`Server`](#server) and [`StreamingServer`](#streamingserver) +classes are responsible for handling incoming connections and then processing +each incoming HTTP request. -The request object will be processed once the request headers have +The request object will be processed once the request has been received by the client. This request object implements the [PSR-7 ServerRequestInterface](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-7-http-message.md#321-psrhttpmessageserverrequestinterface) @@ -172,13 +203,15 @@ which in turn extends the and will be passed to the callback function like this. ```php -$server = new StreamingServer(function (ServerRequestInterface $request) { +$server = new Server(function (ServerRequestInterface $request) { $body = "The method of the request is: " . $request->getMethod(); $body .= "The requested path is: " . $request->getUri()->getPath(); return new Response( 200, - array('Content-Type' => 'text/plain'), + array( + 'Content-Type' => 'text/plain' + ), $body ); }); @@ -206,12 +239,14 @@ The following parameters are currently available: Set to 'on' if the request used HTTPS, otherwise it won't be set ```php -$server = new StreamingServer(function (ServerRequestInterface $request) { +$server = new Server(function (ServerRequestInterface $request) { $body = "Your IP is: " . $request->getServerParams()['REMOTE_ADDR']; return new Response( 200, - array('Content-Type' => 'text/plain'), + array( + 'Content-Type' => 'text/plain' + ), $body ); }); @@ -223,7 +258,7 @@ The `getQueryParams(): array` method can be used to get the query parameters similiar to the `$_GET` variable. ```php -$server = new StreamingServer(function (ServerRequestInterface $request) { +$server = new Server(function (ServerRequestInterface $request) { $queryParams = $request->getQueryParams(); $body = 'The query parameter "foo" is not set. Click the following link '; @@ -235,7 +270,9 @@ $server = new StreamingServer(function (ServerRequestInterface $request) { return new Response( 200, - array('Content-Type' => 'text/html'), + array( + 'Content-Type' => 'text/html' + ), $body ); }); @@ -296,7 +333,9 @@ $server = new StreamingServer(function (ServerRequestInterface $request) { $request->getBody()->on('end', function () use ($resolve, &$contentLength){ $response = new Response( 200, - array('Content-Type' => 'text/plain'), + array( + 'Content-Type' => 'text/plain' + ), "The length of the submitted request body is: " . $contentLength ); $resolve($response); @@ -306,7 +345,9 @@ $server = new StreamingServer(function (ServerRequestInterface $request) { $request->getBody()->on('error', function (\Exception $exception) use ($resolve, &$contentLength) { $response = new Response( 400, - array('Content-Type' => 'text/plain'), + array( + 'Content-Type' => 'text/plain' + ), "An error occured while reading at length: " . $contentLength ); $resolve($response); @@ -358,14 +399,18 @@ $server = new StreamingServer(function (ServerRequestInterface $request) { return new Response( 411, - array('Content-Type' => 'text/plain'), + array( + 'Content-Type' => 'text/plain' + ), $body ); } return new Response( 200, - array('Content-Type' => 'text/plain'), + array( + 'Content-Type' => 'text/plain' + ), "Request body size: " . $size . " bytes\n" ); }); @@ -404,7 +449,7 @@ The `getCookieParams(): string[]` method can be used to get all cookies sent with the current request. ```php -$server = new StreamingServer(function (ServerRequestInterface $request) { +$server = new Server(function (ServerRequestInterface $request) { $key = 'react\php'; if (isset($request->getCookieParams()[$key])) { @@ -412,7 +457,9 @@ $server = new StreamingServer(function (ServerRequestInterface $request) { return new Response( 200, - array('Content-Type' => 'text/plain'), + array( + 'Content-Type' => 'text/plain' + ), $body ); } @@ -439,9 +486,9 @@ See also [example #5](examples) for more details. ### Response -The callback function passed to the constructor of the [StreamingServer](#server) -is responsible for processing the request and returning a response, -which will be delivered to the client. +The callback function passed to the constructor of the [`Server`](#server) or +advanced [`StreamingServer`](#server) is responsible for processing the request +and returning a response, which will be delivered to the client. This function MUST return an instance implementing [PSR-7 ResponseInterface](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-7-http-message.md#33-psrhttpmessageresponseinterface) object or a @@ -455,10 +502,12 @@ but feel free to use any implemantation of the `PSR-7 ResponseInterface` you prefer. ```php -$server = new StreamingServer(function (ServerRequestInterface $request) { +$server = new Server(function (ServerRequestInterface $request) { return new Response( 200, - array('Content-Type' => 'text/plain'), + array( + 'Content-Type' => 'text/plain' + ), "Hello World!\n" ); }); @@ -474,12 +523,14 @@ To prevent this you SHOULD use a This example shows how such a long-term action could look like: ```php -$server = new StreamingServer(function (ServerRequestInterface $request) use ($loop) { +$server = new Server(function (ServerRequestInterface $request) use ($loop) { return new Promise(function ($resolve, $reject) use ($request, $loop) { $loop->addTimer(1.5, function() use ($loop, $resolve) { $response = new Response( 200, - array('Content-Type' => 'text/plain'), + array( + 'Content-Type' => 'text/plain' + ), "Hello world" ); $resolve($response); @@ -507,7 +558,7 @@ Note that other implementations of the `PSR-7 ResponseInterface` likely only support strings. ```php -$server = new StreamingServer(function (ServerRequestInterface $request) use ($loop) { +$server = new Server(function (ServerRequestInterface $request) use ($loop) { $stream = new ThroughStream(); $timer = $loop->addPeriodicTimer(0.5, function () use ($stream) { @@ -519,7 +570,13 @@ $server = new StreamingServer(function (ServerRequestInterface $request) use ($l $stream->emit('end'); }); - return new Response(200, array('Content-Type' => 'text/plain'), $stream); + return new Response( + 200, + array( + 'Content-Type' => 'text/plain' + ), + $stream + ); }); ``` @@ -550,7 +607,7 @@ If you know the length of your stream body, you MAY specify it like this instead ```php $stream = new ThroughStream() -$server = new StreamingServer(function (ServerRequestInterface $request) use ($stream) { +$server = new Server(function (ServerRequestInterface $request) use ($stream) { return new Response( 200, array( @@ -635,8 +692,13 @@ A `Date` header will be automatically added with the system date and time if non You can add a custom `Date` header yourself like this: ```php -$server = new StreamingServer(function (ServerRequestInterface $request) { - return new Response(200, array('Date' => date('D, d M Y H:i:s T'))); +$server = new Server(function (ServerRequestInterface $request) { + return new Response( + 200, + array( + 'Date' => date('D, d M Y H:i:s T') + ) + ); }); ``` @@ -644,8 +706,13 @@ If you don't have a appropriate clock to rely on, you should unset this header with an empty string: ```php -$server = new StreamingServer(function (ServerRequestInterface $request) { - return new Response(200, array('Date' => '')); +$server = new Server(function (ServerRequestInterface $request) { + return new Response( + 200, + array( + 'Date' => '' + ) + ); }); ``` @@ -653,8 +720,13 @@ Note that it will automatically assume a `X-Powered-By: react/alpha` header unless your specify a custom `X-Powered-By` header yourself: ```php -$server = new StreamingServer(function (ServerRequestInterface $request) { - return new Response(200, array('X-Powered-By' => 'PHP 3')); +$server = new Server(function (ServerRequestInterface $request) { + return new Response( + 200, + array( + 'X-Powered-By' => 'PHP 3' + ) + ); }); ``` @@ -662,8 +734,13 @@ If you do not want to send this header at all, you can use an empty string as value like this: ```php -$server = new StreamingServer(function (ServerRequestInterface $request) { - return new Response(200, array('X-Powered-By' => '')); +$server = new Server(function (ServerRequestInterface $request) { + return new Response( + 200, + array( + 'X-Powered-By' => '' + ) + ); }); ``` diff --git a/examples/01-hello-world.php b/examples/01-hello-world.php index 34126c0f..f703a5d7 100644 --- a/examples/01-hello-world.php +++ b/examples/01-hello-world.php @@ -3,13 +3,13 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Factory; use React\Http\Response; -use React\Http\StreamingServer; +use React\Http\Server; require __DIR__ . '/../vendor/autoload.php'; $loop = Factory::create(); -$server = new StreamingServer(function (ServerRequestInterface $request) { +$server = new Server(function (ServerRequestInterface $request) { return new Response( 200, array( diff --git a/examples/02-count-visitors.php b/examples/02-count-visitors.php index ee8e2d22..5dec93f9 100644 --- a/examples/02-count-visitors.php +++ b/examples/02-count-visitors.php @@ -3,17 +3,19 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Factory; use React\Http\Response; -use React\Http\StreamingServer; +use React\Http\Server; require __DIR__ . '/../vendor/autoload.php'; $loop = Factory::create(); $counter = 0; -$server = new StreamingServer(function (ServerRequestInterface $request) use (&$counter) { +$server = new Server(function (ServerRequestInterface $request) use (&$counter) { return new Response( 200, - array('Content-Type' => 'text/plain'), + array( + 'Content-Type' => 'text/plain' + ), "Welcome number " . ++$counter . "!\n" ); }); diff --git a/examples/03-client-ip.php b/examples/03-client-ip.php index eca6e4d6..25e3d408 100644 --- a/examples/03-client-ip.php +++ b/examples/03-client-ip.php @@ -3,18 +3,20 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Factory; use React\Http\Response; -use React\Http\StreamingServer; +use React\Http\Server; require __DIR__ . '/../vendor/autoload.php'; $loop = Factory::create(); -$server = new StreamingServer(function (ServerRequestInterface $request) { +$server = new Server(function (ServerRequestInterface $request) { $body = "Your IP is: " . $request->getServerParams()['REMOTE_ADDR']; return new Response( 200, - array('Content-Type' => 'text/plain'), + array( + 'Content-Type' => 'text/plain' + ), $body ); }); diff --git a/examples/04-query-parameter.php b/examples/04-query-parameter.php index cc665d8a..13015430 100644 --- a/examples/04-query-parameter.php +++ b/examples/04-query-parameter.php @@ -3,13 +3,13 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Factory; use React\Http\Response; -use React\Http\StreamingServer; +use React\Http\Server; require __DIR__ . '/../vendor/autoload.php'; $loop = Factory::create(); -$server = new StreamingServer(function (ServerRequestInterface $request) { +$server = new Server(function (ServerRequestInterface $request) { $queryParams = $request->getQueryParams(); $body = 'The query parameter "foo" is not set. Click the following link '; @@ -21,7 +21,9 @@ return new Response( 200, - array('Content-Type' => 'text/html'), + array( + 'Content-Type' => 'text/html' + ), $body ); }); diff --git a/examples/05-cookie-handling.php b/examples/05-cookie-handling.php index 539421c1..e09d9277 100644 --- a/examples/05-cookie-handling.php +++ b/examples/05-cookie-handling.php @@ -3,13 +3,13 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Factory; use React\Http\Response; -use React\Http\StreamingServer; +use React\Http\Server; require __DIR__ . '/../vendor/autoload.php'; $loop = Factory::create(); -$server = new StreamingServer(function (ServerRequestInterface $request) { +$server = new Server(function (ServerRequestInterface $request) { $key = 'react\php'; if (isset($request->getCookieParams()[$key])) { @@ -17,7 +17,9 @@ return new Response( 200, - array('Content-Type' => 'text/plain'), + array( + 'Content-Type' => 'text/plain' + ), $body ); } diff --git a/examples/06-sleep.php b/examples/06-sleep.php index b3acf9ff..3b7a85f0 100644 --- a/examples/06-sleep.php +++ b/examples/06-sleep.php @@ -3,19 +3,21 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Factory; use React\Http\Response; -use React\Http\StreamingServer; +use React\Http\Server; use React\Promise\Promise; require __DIR__ . '/../vendor/autoload.php'; $loop = Factory::create(); -$server = new StreamingServer(function (ServerRequestInterface $request) use ($loop) { +$server = new Server(function (ServerRequestInterface $request) use ($loop) { return new Promise(function ($resolve, $reject) use ($request, $loop) { $loop->addTimer(1.5, function() use ($loop, $resolve) { $response = new Response( 200, - array('Content-Type' => 'text/plain'), + array( + 'Content-Type' => 'text/plain' + ), "Hello world" ); $resolve($response); diff --git a/examples/07-error-handling.php b/examples/07-error-handling.php index affd3ae4..76544a6b 100644 --- a/examples/07-error-handling.php +++ b/examples/07-error-handling.php @@ -3,7 +3,7 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Factory; use React\Http\Response; -use React\Http\StreamingServer; +use React\Http\Server; use React\Promise\Promise; require __DIR__ . '/../vendor/autoload.php'; @@ -11,7 +11,7 @@ $loop = Factory::create(); $count = 0; -$server = new StreamingServer(function (ServerRequestInterface $request) use (&$count) { +$server = new Server(function (ServerRequestInterface $request) use (&$count) { return new Promise(function ($resolve, $reject) use (&$count) { $count++; @@ -21,7 +21,9 @@ $response = new Response( 200, - array('Content-Type' => 'text/plain'), + array( + 'Content-Type' => 'text/plain' + ), "Hello World!\n" ); diff --git a/examples/08-stream-response.php b/examples/08-stream-response.php index aec8b23e..9e3cf1ab 100644 --- a/examples/08-stream-response.php +++ b/examples/08-stream-response.php @@ -3,14 +3,16 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Factory; use React\Http\Response; -use React\Http\StreamingServer; +use React\Http\Server; use React\Stream\ThroughStream; require __DIR__ . '/../vendor/autoload.php'; $loop = Factory::create(); -$server = new StreamingServer($loop,function (ServerRequestInterface $request) use ($loop) { +// Note how this example still uses `Server` instead of `StreamingServer`. +// The `StreamingServer` is only required for streaming *incoming* requests. +$server = new Server($loop,function (ServerRequestInterface $request) use ($loop) { if ($request->getMethod() !== 'GET' || $request->getUri()->getPath() !== '/') { return new Response(404); } @@ -28,7 +30,9 @@ return new Response( 200, - array('Content-Type' => 'text/plain'), + array( + 'Content-Type' => 'text/plain' + ), $stream ); }); diff --git a/examples/09-stream-request.php b/examples/09-stream-request.php index ed900ead..af4fb580 100644 --- a/examples/09-stream-request.php +++ b/examples/09-stream-request.php @@ -10,6 +10,9 @@ $loop = Factory::create(); +// Note how this example uses the advanced `StreamingServer` to allow streaming +// the incoming HTTP request. This very simple example merely counts the size +// of the streaming body, it does not otherwise buffer its contents in memory. $server = new StreamingServer(function (ServerRequestInterface $request) { return new Promise(function ($resolve, $reject) use ($request) { $contentLength = 0; @@ -20,7 +23,9 @@ $request->getBody()->on('end', function () use ($resolve, &$contentLength){ $response = new Response( 200, - array('Content-Type' => 'text/plain'), + array( + 'Content-Type' => 'text/plain' + ), "The length of the submitted request body is: " . $contentLength ); $resolve($response); @@ -30,7 +35,9 @@ $request->getBody()->on('error', function (\Exception $exception) use ($resolve, &$contentLength) { $response = new Response( 400, - array('Content-Type' => 'text/plain'), + array( + 'Content-Type' => 'text/plain' + ), "An error occured while reading at length: " . $contentLength ); $resolve($response); diff --git a/examples/11-hello-world-https.php b/examples/11-hello-world-https.php index da21bdb4..c8bc52e8 100644 --- a/examples/11-hello-world-https.php +++ b/examples/11-hello-world-https.php @@ -3,16 +3,18 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Factory; use React\Http\Response; -use React\Http\StreamingServer; +use React\Http\Server; require __DIR__ . '/../vendor/autoload.php'; $loop = Factory::create(); -$server = new StreamingServer(function (ServerRequestInterface $request) { +$server = new Server(function (ServerRequestInterface $request) { return new Response( 200, - array('Content-Type' => 'text/plain'), + array( + 'Content-Type' => 'text/plain' + ), "Hello world!\n" ); }); diff --git a/examples/12-upload.php b/examples/12-upload.php index 5d0a5636..4e21bdb0 100644 --- a/examples/12-upload.php +++ b/examples/12-upload.php @@ -114,12 +114,15 @@ return new Response( 200, - array('Content-Type' => 'text/html; charset=UTF-8'), + array( + 'Content-Type' => 'text/html; charset=UTF-8' + ), $html ); }; -// buffer and parse HTTP request body before running our request handler +// Note how this example explicitly uses the advanced `StreamingServer` to apply +// custom request buffering limits below before running our request handler. $server = new StreamingServer(array( new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers, queue otherwise new RequestBodyBufferMiddleware(8 * 1024 * 1024), // 8 MiB max, ignore body otherwise diff --git a/examples/21-http-proxy.php b/examples/21-http-proxy.php index 0e390ff1..a881c050 100644 --- a/examples/21-http-proxy.php +++ b/examples/21-http-proxy.php @@ -3,18 +3,24 @@ use Psr\Http\Message\RequestInterface; use React\EventLoop\Factory; use React\Http\Response; -use React\Http\StreamingServer; +use React\Http\Server; use RingCentral\Psr7; require __DIR__ . '/../vendor/autoload.php'; $loop = Factory::create(); -$server = new StreamingServer(function (RequestInterface $request) { +// Note how this example uses the `Server` instead of `StreamingServer`. +// This means that this proxy buffers the whole request before "processing" it. +// As such, this is store-and-forward proxy. This could also use the advanced +// `StreamingServer` to forward the incoming request as it comes in. +$server = new Server(function (RequestInterface $request) { if (strpos($request->getRequestTarget(), '://') === false) { return new Response( 400, - array('Content-Type' => 'text/plain'), + array( + 'Content-Type' => 'text/plain' + ), 'This is a plain HTTP proxy' ); } @@ -32,7 +38,9 @@ // and forward the incoming response to the original client request return new Response( 200, - array('Content-Type' => 'text/plain'), + array( + 'Content-Type' => 'text/plain' + ), Psr7\str($outgoing) ); }); diff --git a/examples/22-connect-proxy.php b/examples/22-connect-proxy.php index b5ded9a7..63c20833 100644 --- a/examples/22-connect-proxy.php +++ b/examples/22-connect-proxy.php @@ -3,7 +3,7 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Factory; use React\Http\Response; -use React\Http\StreamingServer; +use React\Http\Server; use React\Socket\Connector; use React\Socket\ConnectionInterface; @@ -12,11 +12,18 @@ $loop = Factory::create(); $connector = new Connector($loop); -$server = new StreamingServer(function (ServerRequestInterface $request) use ($connector) { +// Note how this example uses the `Server` instead of `StreamingServer`. +// Unlike the plain HTTP proxy, the CONNECT method does not contain a body +// and we establish an end-to-end connection over the stream object, so this +// doesn't have to store any payload data in memory at all. +$server = new Server(function (ServerRequestInterface $request) use ($connector) { if ($request->getMethod() !== 'CONNECT') { return new Response( 405, - array('Content-Type' => 'text/plain', 'Allow' => 'CONNECT'), + array( + 'Content-Type' => 'text/plain', + 'Allow' => 'CONNECT' + ), 'This is a HTTP CONNECT (secure HTTPS) proxy' ); } @@ -34,7 +41,9 @@ function (ConnectionInterface $remote) { function ($e) { return new Response( 502, - array('Content-Type' => 'text/plain'), + array( + 'Content-Type' => 'text/plain' + ), 'Unable to connect: ' . $e->getMessage() ); } diff --git a/examples/31-upgrade-echo.php b/examples/31-upgrade-echo.php index 74634f97..21d6eb67 100644 --- a/examples/31-upgrade-echo.php +++ b/examples/31-upgrade-echo.php @@ -20,16 +20,25 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Factory; use React\Http\Response; -use React\Http\StreamingServer; +use React\Http\Server; use React\Stream\ThroughStream; require __DIR__ . '/../vendor/autoload.php'; $loop = Factory::create(); -$server = new StreamingServer(function (ServerRequestInterface $request) use ($loop) { +// Note how this example uses the `Server` instead of `StreamingServer`. +// The initial incoming request does not contain a body and we upgrade to a +// stream object below. +$server = new Server(function (ServerRequestInterface $request) use ($loop) { if ($request->getHeaderLine('Upgrade') !== 'echo' || $request->getProtocolVersion() === '1.0') { - return new Response(426, array('Upgrade' => 'echo'), '"Upgrade: echo" required'); + return new Response( + 426, + array( + 'Upgrade' => 'echo' + ), + '"Upgrade: echo" required' + ); } // simply return a duplex ThroughStream here diff --git a/examples/32-upgrade-chat.php b/examples/32-upgrade-chat.php index 7db38250..89230f31 100644 --- a/examples/32-upgrade-chat.php +++ b/examples/32-upgrade-chat.php @@ -22,7 +22,7 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Factory; use React\Http\Response; -use React\Http\StreamingServer; +use React\Http\Server; use React\Stream\CompositeStream; use React\Stream\ThroughStream; @@ -35,9 +35,18 @@ // this means that any Upgraded data will simply be sent back to the client $chat = new ThroughStream(); -$server = new StreamingServer(function (ServerRequestInterface $request) use ($loop, $chat) { +// Note how this example uses the `Server` instead of `StreamingServer`. +// The initial incoming request does not contain a body and we upgrade to a +// stream object below. +$server = new Server(function (ServerRequestInterface $request) use ($loop, $chat) { if ($request->getHeaderLine('Upgrade') !== 'chat' || $request->getProtocolVersion() === '1.0') { - return new Response(426, array('Upgrade' => 'chat'), '"Upgrade: chat" required'); + return new Response( + 426, + array( + 'Upgrade' => 'chat' + ), + '"Upgrade: chat" required' + ); } // user stream forwards chat data and accepts incoming data diff --git a/examples/99-benchmark-download.php b/examples/99-benchmark-download.php index 079a78c1..692bd810 100644 --- a/examples/99-benchmark-download.php +++ b/examples/99-benchmark-download.php @@ -10,7 +10,7 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Factory; use React\Http\Response; -use React\Http\StreamingServer; +use React\Http\Server; use React\Stream\ReadableStreamInterface; use React\Stream\WritableStreamInterface; @@ -86,12 +86,16 @@ public function getSize() } } -$server = new StreamingServer(function (ServerRequestInterface $request) use ($loop) { +// Note how this example still uses `Server` instead of `StreamingServer`. +// The `StreamingServer` is only required for streaming *incoming* requests. +$server = new Server(function (ServerRequestInterface $request) use ($loop) { switch ($request->getUri()->getPath()) { case '/': return new Response( 200, - array('Content-Type' => 'text/html'), + array( + 'Content-Type' => 'text/html' + ), '1g.bin
10g.bin' ); case '/1g.bin': @@ -108,7 +112,10 @@ public function getSize() return new Response( 200, - array('Content-Type' => 'application/octet-data', 'Content-Length' => $stream->getSize()), + array( + 'Content-Type' => 'application/octet-data', + 'Content-Length' => $stream->getSize() + ), $stream ); }); From 2119dd5c56a9ee104466c2f63a6df21134f18a1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 12 Dec 2017 11:38:10 +0100 Subject: [PATCH 254/456] Add more sections to README --- README.md | 145 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 90 insertions(+), 55 deletions(-) diff --git a/README.md b/README.md index 16db8e23..26386dcc 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,17 @@ Event-driven, streaming plaintext HTTP and secure HTTPS server for [ReactPHP](ht * [Server](#server) * [StreamingServer](#streamingserver) * [Request](#request) + * [Request parameters](#request-parameters) + * [Query parameters](#query-parameters) + * [Streaming request](#streaming-request) + * [Request method](#request-method) + * [Cookie parameters](#cookie-parameters) * [Response](#response) + * [Deferred response](#deferred-response) + * [Streaming response](#streaming-response) + * [Response length](#response-length) + * [Invalid response](#invalid-response) + * [Default response headers](#default-response-headers) * [Middleware](#middleware) * [LimitConcurrentRequestsMiddleware](#limitconcurrentrequestsmiddleware) * [RequestBodyBufferMiddleware](#requestbodybuffermiddleware) @@ -217,6 +227,13 @@ $server = new Server(function (ServerRequestInterface $request) { }); ``` +For more details about the request object, also check out the documentation of +[PSR-7 ServerRequestInterface](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-7-http-message.md#321-psrhttpmessageserverrequestinterface) +and +[PSR-7 RequestInterface](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-7-http-message.md#32-psrhttpmessagerequestinterface). + +#### Request parameters + The `getServerParams(): mixed[]` method can be used to get server-side parameters similar to the `$_SERVER` variable. The following parameters are currently available: @@ -252,7 +269,9 @@ $server = new Server(function (ServerRequestInterface $request) { }); ``` -See also [example #2](examples). +See also [example #3](examples). + +#### Query parameters The `getQueryParams(): array` method can be used to get the query parameters similiar to the `$_GET` variable. @@ -284,18 +303,19 @@ Use [`htmlentities`](http://php.net/manual/en/function.htmlentities.php) like in this example to prevent [Cross-Site Scripting (abbreviated as XSS)](https://en.wikipedia.org/wiki/Cross-site_scripting). -See also [example #3](examples). +See also [example #4](examples). -For more details about the request object, check out the documentation of -[PSR-7 ServerRequestInterface](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-7-http-message.md#321-psrhttpmessageserverrequestinterface) -and -[PSR-7 RequestInterface](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-7-http-message.md#32-psrhttpmessagerequestinterface). +#### Streaming request -Note that by default, the request object will be processed once the request headers have -been received (see also [`RequestBodyBufferMiddleware`](#requestbodybuffermiddleware) -for an alternative). +If you're using the [`Server`](#server), then the request object will be +buffered and parsed in memory and contains the full request body. +This includes the parsed request body and any file uploads. + +If you're using the advanced [`StreamingServer`](#streamingserver), the +request object will be processed once the request headers have been received. This means that this happens irrespective of (i.e. *before*) receiving the (potentially much larger) request body. + While this may be uncommon in the PHP ecosystem, this is actually a very powerful approach that gives you several advantages not otherwise possible: @@ -416,6 +436,8 @@ $server = new StreamingServer(function (ServerRequestInterface $request) { }); ``` +#### Request method + Note that the server supports *any* request method (including custom and non- standard ones) and all request-target formats defined in the HTTP specs for each respective method, including *normal* `origin-form` requests as well as @@ -445,6 +467,8 @@ Allowed). request-target than the `Host` header value (such as removing default ports) and the request-target MUST take precendence when forwarding. +#### Cookie parameters + The `getCookieParams(): string[]` method can be used to get all cookies sent with the current request. @@ -513,6 +537,8 @@ $server = new Server(function (ServerRequestInterface $request) { }); ``` +#### Deferred response + The example above returns the response directly, because it needs no time to be processed. Using a database, the file system or long calculations @@ -550,6 +576,8 @@ The promise cancellation handler can be used to clean up any pending resources allocated in this case (if applicable). If a promise is resolved after the client closes, it will simply be ignored. +#### Streaming response + The `Response` class in this project supports to add an instance which implements the [ReactPHP ReadableStreamInterface](https://github.com/reactphp/stream#readablestreaminterface) for the response body. @@ -595,52 +623,6 @@ response stream will automatically be closed. The `close` event can be used to clean up any pending resources allocated in this case (if applicable). -If the response body is a `string`, a `Content-Length` header will be added -automatically. -If the response body is a ReactPHP `ReadableStreamInterface` and you do not -specify a `Content-Length` header, HTTP/1.1 responses will automatically use -chunked transfer encoding and send the respective header -(`Transfer-Encoding: chunked`) automatically. -The server is responsible for handling `Transfer-Encoding`, so you SHOULD NOT -pass this header yourself. -If you know the length of your stream body, you MAY specify it like this instead: - -```php -$stream = new ThroughStream() -$server = new Server(function (ServerRequestInterface $request) use ($stream) { - return new Response( - 200, - array( - 'Content-Length' => '5', - 'Content-Type' => 'text/plain', - ), - $stream - ); -}); -``` - -An invalid return value or an unhandled `Exception` or `Throwable` in the code -of the callback function, will result in an `500 Internal Server Error` message. -Make sure to catch `Exceptions` or `Throwables` to create own response messages. - -After the return in the callback function the response will be processed by the `StreamingServer`. -The `StreamingServer` will add the protocol version of the request, so you don't have to. - -Any response to a `HEAD` request and any response with a `1xx` (Informational), -`204` (No Content) or `304` (Not Modified) status code will *not* include a -message body as per the HTTP specs. -This means that your callback does not have to take special care of this and any -response body will simply be ignored. - -Similarly, any `2xx` (Successful) response to a `CONNECT` request, any response -with a `1xx` (Informational) or `204` (No Content) status code will *not* -include a `Content-Length` or `Transfer-Encoding` header as these do not apply -to these messages. -Note that a response to a `HEAD` request and any response with a `304` (Not -Modified) status code MAY include these headers even though -the message does not contain a response body, because these header would apply -to the message if the same request would have used an (unconditional) `GET`. - > Note that special care has to be taken if you use a body stream instance that implements ReactPHP's [`DuplexStreamInterface`](https://github.com/reactphp/stream#duplexstreaminterface) @@ -688,6 +670,59 @@ to the message if the same request would have used an (unconditional) `GET`. body has been processed (which should be empty in most cases). See also [example #22](examples) for more details. +#### Response length + +If the response body is a `string`, a `Content-Length` header will be added +automatically. +If the response body is a ReactPHP `ReadableStreamInterface` and you do not +specify a `Content-Length` header, HTTP/1.1 responses will automatically use +chunked transfer encoding and send the respective header +(`Transfer-Encoding: chunked`) automatically. +The server is responsible for handling `Transfer-Encoding`, so you SHOULD NOT +pass this header yourself. +If you know the length of your stream body, you MAY specify it like this instead: + +```php +$stream = new ThroughStream() +$server = new Server(function (ServerRequestInterface $request) use ($stream) { + return new Response( + 200, + array( + 'Content-Length' => '5', + 'Content-Type' => 'text/plain', + ), + $stream + ); +}); +``` + +Any response to a `HEAD` request and any response with a `1xx` (Informational), +`204` (No Content) or `304` (Not Modified) status code will *not* include a +message body as per the HTTP specs. +This means that your callback does not have to take special care of this and any +response body will simply be ignored. + +Similarly, any `2xx` (Successful) response to a `CONNECT` request, any response +with a `1xx` (Informational) or `204` (No Content) status code will *not* +include a `Content-Length` or `Transfer-Encoding` header as these do not apply +to these messages. +Note that a response to a `HEAD` request and any response with a `304` (Not +Modified) status code MAY include these headers even though +the message does not contain a response body, because these header would apply +to the message if the same request would have used an (unconditional) `GET`. + +#### Invalid response + +An invalid return value or an unhandled `Exception` or `Throwable` in the code +of the callback function, will result in an `500 Internal Server Error` message. +Make sure to catch `Exceptions` or `Throwables` to create own response messages. + +#### Default response headers + +After the return in the callback function the response will be processed by the +[`Server`](#server) or [`StreamingServer`](#streamingserver) respectively. +They will add the protocol version of the request, so you don't have to. + A `Date` header will be automatically added with the system date and time if none is given. You can add a custom `Date` header yourself like this: From 7d2317f56cf89ce556bbe8b430efa0c3808c1629 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 12 Dec 2017 12:48:26 +0100 Subject: [PATCH 255/456] Both Server and StreamingServer accept middleware arrays --- README.md | 10 +++--- src/Server.php | 19 +++++++--- src/StreamingServer.php | 6 ++-- tests/ServerTest.php | 65 +++++++++++++++++++++++++++++++++++ tests/StreamingServerTest.php | 22 ++++++++++++ 5 files changed, 111 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index ec8d0c4b..f37ded49 100644 --- a/README.md +++ b/README.md @@ -675,7 +675,8 @@ passed explicitly. ### Middleware -As documented above, the [`StreamingServer`](#streamingserver) accepts a single +As documented above, the [`Server`](#server) and advanced +[`StreamingServer`](#streamingserver) accept a single request handler argument that is responsible for processing an incoming HTTP request and then creating and returning an outgoing HTTP response. @@ -704,12 +705,13 @@ required to match PHP's request behavior (see below) and otherwise actively encourages [Third-Party Middleware](#third-party-middleware) implementations. In order to use middleware request handlers, simply pass an array with all -callables as defined above to the [`StreamingServer`](#streamingserver). +callables as defined above to the [`Server`](#server) or +[`StreamingServer`](#streamingserver) respectively. The following example adds a middleware request handler that adds the current time to the request as a header (`Request-Time`) and a final request handler that always returns a 200 code without a body: ```php -$server = new StreamingServer(array( +$server = new Server(array( function (ServerRequestInterface $request, callable $next) { $request = $request->withHeader('Request-Time', time()); return $next($request); @@ -740,7 +742,7 @@ The following example shows how this middleware can be used to ensure no more than 10 handlers will be invoked at once: ```php -$server = new StreamingServer(array( +$server = new Server(array( new LimitConcurrentRequestsMiddleware(10), $handler )); diff --git a/src/Server.php b/src/Server.php index f77d2281..999409b3 100644 --- a/src/Server.php +++ b/src/Server.php @@ -48,10 +48,10 @@ final class Server extends EventEmitter /** * @see StreamingServer::__construct() */ - public function __construct($callback) + public function __construct($requestHandler) { - if (!is_callable($callback)) { - throw new \InvalidArgumentException(); + if (!is_callable($requestHandler) && !is_array($requestHandler)) { + throw new \InvalidArgumentException('Invalid request handler given'); } $middleware = array(); @@ -66,7 +66,14 @@ public function __construct($callback) if ($enablePostDataReading !== '') { $middleware[] = new RequestBodyParserMiddleware(); } - $middleware[] = $callback; + + if (is_callable($requestHandler)) { + $middleware[] = $requestHandler; + } else { + foreach ($requestHandler as $one) { + $middleware[] = $one; + } + } $this->streamingServer = new StreamingServer($middleware); @@ -84,6 +91,10 @@ public function listen(ServerInterface $server) $this->streamingServer->listen($server); } + /** + * @return int + * @codeCoverageIgnore + */ private function getConcurrentRequestsLimit() { if (ini_get('memory_limit') == -1) { diff --git a/src/StreamingServer.php b/src/StreamingServer.php index f2e78ab4..1013cb4d 100644 --- a/src/StreamingServer.php +++ b/src/StreamingServer.php @@ -99,10 +99,10 @@ final class StreamingServer extends EventEmitter */ public function __construct($requestHandler) { - if (is_array($requestHandler)) { - $requestHandler = new MiddlewareRunner($requestHandler); + if (!is_callable($requestHandler) && !is_array($requestHandler)) { + throw new \InvalidArgumentException('Invalid request handler given'); } elseif (!is_callable($requestHandler)) { - throw new \InvalidArgumentException(); + $requestHandler = new MiddlewareRunner($requestHandler); } $this->callback = $requestHandler; diff --git a/tests/ServerTest.php b/tests/ServerTest.php index fa7cb71f..d53cc8aa 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -41,6 +41,71 @@ public function setUp() $this->socket = new SocketServerStub(); } + /** + * @expectedException InvalidArgumentException + */ + public function testInvalidCallbackFunctionLeadsToException() + { + new Server('invalid'); + } + + public function testSimpleRequestCallsRequestHandlerOnce() + { + $called = null; + $server = new Server(function (ServerRequestInterface $request) use (&$called) { + ++$called; + }); + + $server->listen($this->socket); + $this->socket->emit('connection', array($this->connection)); + $this->connection->emit('data', array("GET / HTTP/1.0\r\n\r\n")); + + $this->assertSame(1, $called); + } + + /** + * @requires PHP 5.4 + */ + public function testSimpleRequestCallsArrayRequestHandlerOnce() + { + $this->called = null; + $server = new Server(array($this, 'helperCallableOnce')); + + $server->listen($this->socket); + $this->socket->emit('connection', array($this->connection)); + $this->connection->emit('data', array("GET / HTTP/1.0\r\n\r\n")); + + $this->assertSame(1, $this->called); + } + + public function helperCallableOnce() + { + ++$this->called; + } + + public function testSimpleRequestWithMiddlewareArrayProcessesMiddlewareStack() + { + $called = null; + $server = new Server(array( + function (ServerRequestInterface $request, $next) use (&$called) { + $called = 'before'; + $ret = $next($request->withHeader('Demo', 'ok')); + $called .= 'after'; + + return $ret; + }, + function (ServerRequestInterface $request) use (&$called) { + $called .= $request->getHeaderLine('Demo'); + } + )); + + $server->listen($this->socket); + $this->socket->emit('connection', array($this->connection)); + $this->connection->emit('data', array("GET / HTTP/1.0\r\n\r\n")); + + $this->assertSame('beforeokafter', $called); + } + public function testPostFileUpload() { $loop = Factory::create(); diff --git a/tests/StreamingServerTest.php b/tests/StreamingServerTest.php index f28d7e28..6f4bd1dc 100644 --- a/tests/StreamingServerTest.php +++ b/tests/StreamingServerTest.php @@ -62,6 +62,28 @@ public function testRequestEventIsEmitted() $this->connection->emit('data', array($data)); } + /** + * @requires PHP 5.4 + */ + public function testRequestEventIsEmittedForArrayCallable() + { + $this->called = null; + $server = new StreamingServer(array($this, 'helperCallableOnce')); + + $server->listen($this->socket); + $this->socket->emit('connection', array($this->connection)); + + $data = $this->createGetRequest(); + $this->connection->emit('data', array($data)); + + $this->assertEquals(1, $this->called); + } + + public function helperCallableOnce() + { + ++$this->called; + } + public function testRequestEvent() { $i = 0; From 0410c94463bdae4bdf34d60bff91e9e1eb4a6652 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 12 Dec 2017 14:01:48 +0100 Subject: [PATCH 256/456] Prepare v0.8.0 release --- CHANGELOG.md | 102 +++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 9 ++++- 2 files changed, 109 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 20aca1f0..87d5b5c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,107 @@ # Changelog +## 0.8.0 (2017-12-12) + +* Feature / BC break: Add new `Server` facade that buffers and parses incoming + HTTP requests. This provides full PSR-7 compatibility, including support for + form submissions with POST fields and file uploads. + The old `Server` has been renamed to `StreamingServer` for advanced usage + and is used internally. + (#266, #271, #281, #282, #283 and #284 by @WyriHaximus and @clue) + + ```php + // old: handle incomplete/streaming requests + $server = new Server($handler); + + // new: handle complete, buffered and parsed requests + // new: full PSR-7 support, including POST fields and file uploads + $server = new Server($handler); + + // new: handle incomplete/streaming requests + $server = new StreamingServer($handler); + ``` + + > While this is technically a small BC break, this should in fact not break + most consuming code. If you rely on the old request streaming, you can + explicitly use the advanced `StreamingServer` to restore old behavior. + +* Feature: Add support for middleware request handler arrays + (#215, #228, #229, #236, #237, #238, #246, #247, #277, #279 and #285 by @WyriHaximus, @clue and @jsor) + + ```php + // new: middleware request handler arrays + $server = new Server(array( + function (ServerRequestInterface $request, callable $next) { + $request = $request->withHeader('Processed', time()); + return $next($request); + }, + function (ServerRequestInterface $request) { + return new Response(); + } + )); + ``` + +* Feature: Add support for limiting how many next request handlers can be + executed concurrently (`LimitConcurrentRequestsMiddleware`) + (#272 by @clue and @WyriHaximus) + + ```php + // new: explicitly limit concurrency + $server = new Server(array( + new LimitConcurrentRequestsMiddleware(10), + $handler + )); + ``` + +* Feature: Add support for buffering the incoming request body + (`RequestBodyBufferMiddleware`). + This feature mimics PHP's default behavior and respects its `post_max_size` + ini setting by default and allows explicit configuration. + (#216, #224, #263, #276 and #278 by @WyriHaximus and #235 by @andig) + + ```php + // new: buffer up to 10 requests with 8 MiB each + $server = new StreamingServer(array( + new LimitConcurrentRequestsMiddleware(10), + new RequestBodyBufferMiddleware('8M'), + $handler + )); + ``` + +* Feature: Add support for parsing form submissions with POST fields and file + uploads (`RequestBodyParserMiddleware`). + This feature mimics PHP's default behavior and respects its ini settings and + `MAX_FILE_SIZE` POST fields by default and allows explicit configuration. + (#220, #226, #252, #261, #264, #265, #267, #268, #274 by @WyriHaximus and @clue) + + ```php + // new: buffer up to 10 requests with 8 MiB each + // and limit to 4 uploads with 2 MiB each + $server = new StreamingServer(array( + new LimitConcurrentRequestsMiddleware(10), + new RequestBodyBufferMiddleware('8M'), + new RequestBodyParserMiddleware('2M', 4) + $handler + )); + ``` + +* Feature: Update Socket to work around sending secure HTTPS responses with PHP < 7.1.4 + (#244 by @clue) + +* Feature: Support sending same response header multiple times (e.g. `Set-Cookie`) + (#248 by @clue) + +* Feature: Raise maximum request header size to 8k to match common implementations + (#253 by @clue) + +* Improve test suite by adding forward compatibility with PHPUnit 6, test + against PHP 7.1 and PHP 7.2 and refactor and remove risky and duplicate tests. + (#243, #269 and #270 by @carusogabriel and #249 by @clue) + +* Minor code refactoring to move internal classes to `React\Http\Io` namespace + and clean up minor code and documentation issues + (#251 by @clue, #227 by @kalessil, #240 by @christoph-kluge, #230 by @jsor and #280 by @andig) + ## 0.7.4 (2017-08-16) * Improvement: Target evenement 3.0 a long side 2.0 and 1.0 diff --git a/README.md b/README.md index 99d89ec5..9675ba1a 100644 --- a/README.md +++ b/README.md @@ -1068,10 +1068,15 @@ The recommended way to install this library is [through Composer](http://getcomp This will install the latest supported version: ```bash -$ composer require react/http:^0.7.4 +$ composer require react/http:^0.8 ``` -More details about version upgrades can be found in the [CHANGELOG](CHANGELOG.md). +See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. + +This project aims to run on any platform and thus does not require any PHP +extensions and supports running on legacy PHP 5.3 through current PHP 7+ and +HHVM. +It's *highly recommended to use PHP 7+* for this project. ## Tests From 1d73220c35153026d774983c9f052fdc90396e57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 23 Dec 2017 16:22:36 +0100 Subject: [PATCH 257/456] Fix test suite forward compatibility with upcoming EventLoop releases --- tests/ServerTest.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/ServerTest.php b/tests/ServerTest.php index d53cc8aa..e788b8b5 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -3,7 +3,6 @@ namespace React\Tests\Http; use React\EventLoop\Factory; -use React\EventLoop\Timer\TimerInterface; use React\Http\Server; use Psr\Http\Message\ServerRequestInterface; use React\Promise\Deferred; @@ -119,7 +118,7 @@ public function testPostFileUpload() $connection = $this->connection; $data = $this->createPostFileUploadRequest(); - $loop->addPeriodicTimer(0.01, function (TimerInterface $timer) use ($loop, &$data, $connection) { + $loop->addPeriodicTimer(0.01, function ($timer) use ($loop, &$data, $connection) { $line = array_shift($data); $connection->emit('data', array($line)); From df3fce0261294fb56f057a726beb33e69ceaaa48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 28 Dec 2017 12:52:58 +0100 Subject: [PATCH 258/456] Improve functional tests to be less fragile by waiting for remote end --- tests/FunctionalServerTest.php | 41 ++++++++++++++++++++++++++-------- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/tests/FunctionalServerTest.php b/tests/FunctionalServerTest.php index 7bbb8ff3..d4e0d0ec 100644 --- a/tests/FunctionalServerTest.php +++ b/tests/FunctionalServerTest.php @@ -472,13 +472,38 @@ public function testClosedStreamFromRequestHandlerWillSendEmptyBody() $socket->close(); } + public function testRequestHandlerWillReceiveCloseEventIfConnectionClosesWhileSendingBody() + { + $loop = Factory::create(); + $connector = new Connector($loop); + + $once = $this->expectCallableOnce(); + $server = new StreamingServer(function (RequestInterface $request) use ($once) { + $request->getBody()->on('close', $once); + }); + + $socket = new Socket(0, $loop); + $server->listen($socket); + + $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) use ($loop) { + $conn->write("GET / HTTP/1.0\r\nContent-Length: 100\r\n\r\n"); + + $loop->addTimer(0.001, function() use ($conn) { + $conn->end(); + }); + }); + + Block\sleep(0.1, $loop); + + $socket->close(); + } + public function testStreamFromRequestHandlerWillBeClosedIfConnectionClosesWhileSendingBody() { $loop = Factory::create(); $connector = new Connector($loop); $stream = new ThroughStream(); - $stream->on('close', $this->expectCallableOnce()); $server = new StreamingServer(function (RequestInterface $request) use ($stream) { return new Response(200, array(), $stream); @@ -487,22 +512,20 @@ public function testStreamFromRequestHandlerWillBeClosedIfConnectionClosesWhileS $socket = new Socket(0, $loop); $server->listen($socket); - $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) use ($loop) { + $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) use ($loop) { $conn->write("GET / HTTP/1.0\r\nContent-Length: 100\r\n\r\n"); - $loop->addTimer(0.1, function() use ($conn) { + $loop->addTimer(0.001, function() use ($conn) { $conn->end(); }); - - return Stream\buffer($conn); }); - $response = Block\await($result, $loop, 1.0); - - $this->assertStringStartsWith("HTTP/1.0 200 OK", $response); - $this->assertStringEndsWith("\r\n\r\n", $response); + // stream will be closed within 0.1s + $ret = Block\await(Stream\first($stream, 'close'), $loop, 0.1); $socket->close(); + + $this->assertNull($ret); } public function testStreamFromRequestHandlerWillBeClosedIfConnectionClosesButWillOnlyBeDetectedOnNextWrite() From 3ee71c49955f9233bee1e26f48437714ba4b7661 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 28 Dec 2017 13:10:13 +0100 Subject: [PATCH 259/456] Reset stateful tests between tests runs to support --repeat=X --- tests/Io/MiddlewareRunnerTest.php | 8 ++++++++ tests/Middleware/ProcessStack.php | 5 +++++ 2 files changed, 13 insertions(+) diff --git a/tests/Io/MiddlewareRunnerTest.php b/tests/Io/MiddlewareRunnerTest.php index 362d7ca5..5710be2b 100644 --- a/tests/Io/MiddlewareRunnerTest.php +++ b/tests/Io/MiddlewareRunnerTest.php @@ -80,6 +80,14 @@ public function provideProcessStackMiddlewares() */ public function testProcessStack(array $middlewares, $expectedCallCount) { + // the ProcessStack middleware instances are stateful, so reset these + // before running the test, to not fail with --repeat=100 + foreach ($middlewares as $middleware) { + if ($middleware instanceof ProcessStack) { + $middleware->reset(); + } + } + $request = new ServerRequest('GET', 'https://example.com/'); $middlewareStack = new MiddlewareRunner($middlewares); diff --git a/tests/Middleware/ProcessStack.php b/tests/Middleware/ProcessStack.php index c0924865..69bf34a8 100644 --- a/tests/Middleware/ProcessStack.php +++ b/tests/Middleware/ProcessStack.php @@ -25,4 +25,9 @@ public function getCallCount() { return $this->callCount; } + + public function reset() + { + $this->callCount = 0; + } } From ceba7308ad2b30f8b0623317a75439aa47e7cae2 Mon Sep 17 00:00:00 2001 From: Zhuk Sergey Date: Wed, 13 Dec 2017 12:10:00 +0300 Subject: [PATCH 260/456] small code improvements --- src/Io/ChunkedDecoder.php | 10 +++++----- src/Io/CloseProtectionStream.php | 2 +- src/Io/LengthLimitedStream.php | 1 - src/Io/MultipartParser.php | 6 +++--- src/Io/ServerRequest.php | 4 +++- src/StreamingServer.php | 4 ++-- 6 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/Io/ChunkedDecoder.php b/src/Io/ChunkedDecoder.php index ef2ad915..9b0c089f 100644 --- a/src/Io/ChunkedDecoder.php +++ b/src/Io/ChunkedDecoder.php @@ -79,12 +79,12 @@ public function close() public function handleEnd() { if (!$this->closed) { - $this->handleError(new \Exception('Unexpected end event')); + $this->handleError(new Exception('Unexpected end event')); } } /** @internal */ - public function handleError(\Exception $e) + public function handleError(Exception $e) { $this->emit('error', array($e)); $this->close(); @@ -102,7 +102,7 @@ public function handleData($data) if ($positionCrlf === false) { // Header shouldn't be bigger than 1024 bytes if (isset($this->buffer[static::MAX_CHUNK_HEADER_SIZE])) { - $this->handleError(new \Exception('Chunk header size inclusive extension bigger than' . static::MAX_CHUNK_HEADER_SIZE. ' bytes')); + $this->handleError(new Exception('Chunk header size inclusive extension bigger than' . static::MAX_CHUNK_HEADER_SIZE. ' bytes')); } return; } @@ -124,7 +124,7 @@ public function handleData($data) $this->chunkSize = hexdec($hexValue); if (dechex($this->chunkSize) !== $hexValue) { - $this->handleError(new \Exception($hexValue . ' is not a valid hexadecimal number')); + $this->handleError(new Exception($hexValue . ' is not a valid hexadecimal number')); return; } @@ -159,7 +159,7 @@ public function handleData($data) if ($positionCrlf !== 0 && $this->chunkSize === $this->transferredSize && strlen($this->buffer) > 2) { // the first 2 characters are not CLRF, send error event - $this->handleError(new \Exception('Chunk does not end with a CLRF')); + $this->handleError(new Exception('Chunk does not end with a CLRF')); return; } diff --git a/src/Io/CloseProtectionStream.php b/src/Io/CloseProtectionStream.php index 018747fb..f30ff838 100644 --- a/src/Io/CloseProtectionStream.php +++ b/src/Io/CloseProtectionStream.php @@ -17,7 +17,7 @@ * */ class CloseProtectionStream extends EventEmitter implements ReadableStreamInterface { - private $connection; + private $input; private $closed = false; /** diff --git a/src/Io/LengthLimitedStream.php b/src/Io/LengthLimitedStream.php index e1d93a02..e155bdc9 100644 --- a/src/Io/LengthLimitedStream.php +++ b/src/Io/LengthLimitedStream.php @@ -19,7 +19,6 @@ class LengthLimitedStream extends EventEmitter implements ReadableStreamInterfac { private $stream; private $closed = false; - private $encoder; private $transferredLength = 0; private $maxLength; diff --git a/src/Io/MultipartParser.php b/src/Io/MultipartParser.php index 0c196f97..283e8c2f 100644 --- a/src/Io/MultipartParser.php +++ b/src/Io/MultipartParser.php @@ -190,7 +190,7 @@ private function parseUploadedFile($filename, $contentType, $contents) } return new UploadedFile( - Psr7\stream_for(''), + Psr7\stream_for(), $size, UPLOAD_ERR_NO_FILE, $filename, @@ -206,7 +206,7 @@ private function parseUploadedFile($filename, $contentType, $contents) // file exceeds "upload_max_filesize" ini setting if ($size > $this->uploadMaxFilesize) { return new UploadedFile( - Psr7\stream_for(''), + Psr7\stream_for(), $size, UPLOAD_ERR_INI_SIZE, $filename, @@ -217,7 +217,7 @@ private function parseUploadedFile($filename, $contentType, $contents) // file exceeds MAX_FILE_SIZE value if ($this->maxFileSize !== null && $size > $this->maxFileSize) { return new UploadedFile( - Psr7\stream_for(''), + Psr7\stream_for(), $size, UPLOAD_ERR_FORM_SIZE, $filename, diff --git a/src/Io/ServerRequest.php b/src/Io/ServerRequest.php index a9bb8a45..0c9040ae 100644 --- a/src/Io/ServerRequest.php +++ b/src/Io/ServerRequest.php @@ -3,6 +3,8 @@ namespace React\Http\Io; use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Message\StreamInterface; +use Psr\Http\Message\UriInterface; use RingCentral\Psr7\Request; /** @@ -37,7 +39,7 @@ class ServerRequest extends Request implements ServerRequestInterface * @param string $protocolVersion HTTP protocol version. * @param array server-side parameters * - * @throws InvalidArgumentException for an invalid URI + * @throws \InvalidArgumentException for an invalid URI */ public function __construct( $method, diff --git a/src/StreamingServer.php b/src/StreamingServer.php index 1013cb4d..434a8a10 100644 --- a/src/StreamingServer.php +++ b/src/StreamingServer.php @@ -214,7 +214,7 @@ public function handleRequest(ConnectionInterface $conn, ServerRequestInterface $string = $request->getHeaderLine('Content-Length'); $contentLength = (int)$string; - if ((string)$contentLength !== (string)$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); @@ -355,7 +355,7 @@ public function handleResponse(ConnectionInterface $connection, ServerRequestInt // response to HEAD and 1xx, 204 and 304 responses MUST NOT include a body // exclude status 101 (Switching Protocols) here for Upgrade request handling below if ($request->getMethod() === 'HEAD' || $code === 100 || ($code > 101 && $code < 200) || $code === 204 || $code === 304) { - $response = $response->withBody(Psr7Implementation\stream_for('')); + $response = $response->withBody(Psr7Implementation\stream_for()); } // 101 (Switching Protocols) response uses Connection: upgrade header From 1c31d20ad8daf160718fe978b279605e7b54fcd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 30 Dec 2017 15:08:51 +0100 Subject: [PATCH 261/456] Improve performance by skipping buffering empty request body --- .../RequestBodyBufferMiddleware.php | 11 +++-- .../RequestBodyBufferMiddlewareTest.php | 48 +++++++++++++++++++ 2 files changed, 54 insertions(+), 5 deletions(-) diff --git a/src/Middleware/RequestBodyBufferMiddleware.php b/src/Middleware/RequestBodyBufferMiddleware.php index 503bc9fd..cf379c79 100644 --- a/src/Middleware/RequestBodyBufferMiddleware.php +++ b/src/Middleware/RequestBodyBufferMiddleware.php @@ -32,11 +32,12 @@ public function __construct($sizeLimit = null) public function __invoke(ServerRequestInterface $request, $stack) { $body = $request->getBody(); + $size = $body->getSize(); - // skip if body is already buffered - if (!$body instanceof ReadableStreamInterface) { - // replace with empty buffer if size limit is exceeded - if ($body->getSize() > $this->sizeLimit) { + // happy path: skip if body is known to be empty (or is already buffered) + if ($size === 0 || !$body instanceof ReadableStreamInterface) { + // replace with empty body if body is streaming (or buffered size exceeds limit) + if ($body instanceof ReadableStreamInterface || $size > $this->sizeLimit) { $request = $request->withBody(new BufferStream(0)); } @@ -45,7 +46,7 @@ public function __invoke(ServerRequestInterface $request, $stack) // request body of known size exceeding limit $sizeLimit = $this->sizeLimit; - if ($body->getSize() > $this->sizeLimit) { + if ($size > $this->sizeLimit) { $sizeLimit = 0; } diff --git a/tests/Middleware/RequestBodyBufferMiddlewareTest.php b/tests/Middleware/RequestBodyBufferMiddlewareTest.php index 913a733f..28450f6c 100644 --- a/tests/Middleware/RequestBodyBufferMiddlewareTest.php +++ b/tests/Middleware/RequestBodyBufferMiddlewareTest.php @@ -68,6 +68,54 @@ function (ServerRequestInterface $request) use (&$exposedRequest) { $this->assertSame($body, $exposedRequest->getBody()->getContents()); } + public function testEmptyStreamingResolvesImmediatelyWithEmptyBufferedBody() + { + $stream = new ThroughStream(); + $serverRequest = new ServerRequest( + 'GET', + 'https://example.com/', + array(), + $body = new HttpBodyStream($stream, 0) + ); + + $exposedRequest = null; + $buffer = new RequestBodyBufferMiddleware(); + $buffer( + $serverRequest, + function (ServerRequestInterface $request) use (&$exposedRequest) { + $exposedRequest = $request; + } + ); + + $this->assertSame(0, $exposedRequest->getBody()->getSize()); + $this->assertSame('', $exposedRequest->getBody()->getContents()); + $this->assertNotSame($body, $exposedRequest->getBody()); + } + + public function testEmptyBufferedResolvesImmediatelyWithSameBody() + { + $serverRequest = new ServerRequest( + 'GET', + 'https://example.com/', + array(), + '' + ); + $body = $serverRequest->getBody(); + + $exposedRequest = null; + $buffer = new RequestBodyBufferMiddleware(); + $buffer( + $serverRequest, + function (ServerRequestInterface $request) use (&$exposedRequest) { + $exposedRequest = $request; + } + ); + + $this->assertSame(0, $exposedRequest->getBody()->getSize()); + $this->assertSame('', $exposedRequest->getBody()->getContents()); + $this->assertSame($body, $exposedRequest->getBody()); + } + public function testKnownExcessiveSizedBodyIsDisgardedTheRequestIsPassedDownToTheNextMiddleware() { $loop = Factory::create(); From 0e29aac1049d0d82a9c332ccdf541fcdd8fc015b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 17 Nov 2017 22:02:22 +0100 Subject: [PATCH 262/456] Improve performance by avoiding unneeded promise wrapping --- src/StreamingServer.php | 49 ++++++++++++++++++++++------------ tests/FunctionalServerTest.php | 38 ++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 17 deletions(-) diff --git a/src/StreamingServer.php b/src/StreamingServer.php index 434a8a10..a47d48a5 100644 --- a/src/StreamingServer.php +++ b/src/StreamingServer.php @@ -14,8 +14,9 @@ use React\Http\Io\MiddlewareRunner; use React\Http\Io\RequestHeaderParser; use React\Http\Io\ServerRequest; +use React\Promise; use React\Promise\CancellablePromiseInterface; -use React\Promise\Promise; +use React\Promise\PromiseInterface; use React\Socket\ConnectionInterface; use React\Socket\ServerInterface; use React\Stream\ReadableStreamInterface; @@ -229,22 +230,43 @@ public function handleRequest(ConnectionInterface $conn, ServerRequestInterface $conn->write("HTTP/1.1 100 Continue\r\n\r\n"); } + // execute request handler callback $callback = $this->callback; - $cancel = null; - $promise = new Promise(function ($resolve, $reject) use ($callback, $request, &$cancel) { - $cancel = $callback($request); - $resolve($cancel); - }); + try { + $response = $callback($request); + } catch (\Exception $error) { + // request handler callback throws an Exception + $response = Promise\reject($error); + } catch (\Throwable $error) { // @codeCoverageIgnoreStart + // request handler callback throws a PHP7+ Error + $response = Promise\reject($error); // @codeCoverageIgnoreEnd + } // cancel pending promise once connection closes - if ($cancel instanceof CancellablePromiseInterface) { - $conn->on('close', function () use ($cancel) { - $cancel->cancel(); + if ($response instanceof CancellablePromiseInterface) { + $conn->on('close', function () use ($response) { + $response->cancel(); }); } + // 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); + } + + // did not return a promise? this is an error, convert into one for rejection below. + if (!$response instanceof PromiseInterface) { + $response = Promise\resolve($response); + } + $that = $this; - $promise->then( + $response->then( function ($response) use ($that, $conn, $request) { if (!$response instanceof ResponseInterface) { $message = 'The response callback is expected to resolve with an object implementing Psr\Http\Message\ResponseInterface, but resolved with "%s" instead.'; @@ -272,13 +294,6 @@ function ($error) use ($that, $conn, $request) { return $that->writeError($conn, 500, $request); } ); - - if ($contentLength === 0) { - // If Body is empty or Content-Length is 0 and won't emit further data, - // 'data' events from other streams won't be called anymore - $stream->emit('end'); - $stream->close(); - } } /** @internal */ diff --git a/tests/FunctionalServerTest.php b/tests/FunctionalServerTest.php index d4e0d0ec..bdccf889 100644 --- a/tests/FunctionalServerTest.php +++ b/tests/FunctionalServerTest.php @@ -603,6 +603,44 @@ public function testUpgradeWithThroughStreamReturnsDataAsGiven() $socket->close(); } + public function testUpgradeWithRequestBodyAndThroughStreamReturnsDataAsGiven() + { + $loop = Factory::create(); + $connector = new Connector($loop); + + $server = new StreamingServer(function (RequestInterface $request) use ($loop) { + $stream = new ThroughStream(); + + $loop->addTimer(0.1, function () use ($stream) { + $stream->end(); + }); + + return new Response(101, array('Upgrade' => 'echo'), $stream); + }); + + $socket = new Socket(0, $loop); + $server->listen($socket); + + $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { + $conn->write("POST / HTTP/1.1\r\nHost: example.com:80\r\nUpgrade: echo\r\nContent-Length: 3\r\n\r\n"); + $conn->write('hoh'); + + $conn->once('data', function () use ($conn) { + $conn->write('hello'); + $conn->write('world'); + }); + + return Stream\buffer($conn); + }); + + $response = Block\await($result, $loop, 1.0); + + $this->assertStringStartsWith("HTTP/1.1 101 Switching Protocols\r\n", $response); + $this->assertStringEndsWith("\r\n\r\nhelloworld", $response); + + $socket->close(); + } + public function testConnectWithThroughStreamReturnsDataAsGiven() { $loop = Factory::create(); From da623f383323b97ebba1f0bbada2d7ffdd6a5a95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 30 Dec 2017 21:46:25 +0100 Subject: [PATCH 263/456] Fix concurrent next request handlers and recurse next request handlers --- src/Io/MiddlewareRunner.php | 45 ++++++-------- tests/Io/MiddlewareRunnerTest.php | 100 ++++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+), 27 deletions(-) diff --git a/src/Io/MiddlewareRunner.php b/src/Io/MiddlewareRunner.php index 496c71a4..89ba5b4d 100644 --- a/src/Io/MiddlewareRunner.php +++ b/src/Io/MiddlewareRunner.php @@ -16,9 +16,8 @@ final class MiddlewareRunner { /** * @var callable[] - * @internal */ - public $middleware = array(); + private $middleware = array(); /** * @param callable[] $middleware @@ -38,34 +37,26 @@ public function __invoke(ServerRequestInterface $request) return Promise\reject(new \RuntimeException('No middleware to run')); } - $position = 0; + return $this->call($request, 0); + } + /** @internal */ + public function call(ServerRequestInterface $request, $position) + { $that = $this; - $func = function (ServerRequestInterface $request) use (&$func, &$position, &$that) { - $middleware = $that->middleware[$position]; - $response = null; - $promise = new Promise\Promise(function ($resolve) use ($middleware, $request, $func, &$response, &$position) { - $position++; - - $response = $middleware( - $request, - $func - ); - - $resolve($response); - }, function () use (&$response) { - if ($response instanceof Promise\CancellablePromiseInterface) { - $response->cancel(); - } - }); - - return $promise->then(null, function ($error) use (&$position) { - $position--; - - return Promise\reject($error); - }); + $next = function (ServerRequestInterface $request) use ($that, $position) { + return $that->call($request, $position + 1); }; - return $func($request); + $handler = $this->middleware[$position]; + try { + return Promise\resolve($handler($request, $next)); + } catch (\Exception $error) { + // request handler callback throws an Exception + return Promise\reject($error); + } catch (\Throwable $error) { // @codeCoverageIgnoreStart + // request handler callback throws a PHP7+ Error + return Promise\reject($error); // @codeCoverageIgnoreEnd + } } } diff --git a/tests/Io/MiddlewareRunnerTest.php b/tests/Io/MiddlewareRunnerTest.php index 5710be2b..adfdc347 100644 --- a/tests/Io/MiddlewareRunnerTest.php +++ b/tests/Io/MiddlewareRunnerTest.php @@ -12,6 +12,8 @@ use React\Tests\Http\Middleware\ProcessStack; use React\Tests\Http\TestCase; use RingCentral\Psr7\Response; +use Psr\Http\Message\RequestInterface; +use React\Promise\CancellablePromiseInterface; final class MiddlewareRunnerTest extends TestCase { @@ -28,6 +30,39 @@ public function testDefaultResponse() Block\await($middlewareStack($request), Factory::create()); } + public function testReturnsRejectedPromiseIfHandlerThrowsException() + { + $middleware = new MiddlewareRunner(array( + function (ServerRequestInterface $request) { + throw new \RuntimeException('hello'); + } + )); + + $request = new ServerRequest('GET', 'http://example.com/'); + + $promise = $middleware($request); + + $promise->then(null, $this->expectCallableOnceWith($this->isInstanceOf('RuntimeException'))); + } + + /** + * @requires PHP 7 + */ + public function testReturnsRejectedPromiseIfHandlerThrowsThrowable() + { + $middleware = new MiddlewareRunner(array( + function (ServerRequestInterface $request) { + throw new \Error('hello'); + } + )); + + $request = new ServerRequest('GET', 'http://example.com/'); + + $promise = $middleware($request); + + $promise->then(null, $this->expectCallableOnceWith($this->isInstanceOf('Throwable'))); + } + public function provideProcessStackMiddlewares() { $processStackA = new ProcessStack(); @@ -342,4 +377,69 @@ public function testUncommonMiddlewareArrayFormats($middlewareFactory, $expected $this->assertSame($expectedSequence, (string) $response->getBody()); } + + public function testPendingNextRequestHandlersCanBeCalledConcurrently() + { + $called = 0; + $middleware = new MiddlewareRunner(array( + function (RequestInterface $request, $next) { + $first = $next($request); + $second = $next($request); + + return new Response(); + }, + function (RequestInterface $request) use (&$called) { + ++$called; + + return new Promise\Promise(function () { }); + } + )); + + $request = new ServerRequest('GET', 'http://example.com/'); + + $response = Block\await($middleware($request), Factory::create()); + + $this->assertTrue($response instanceof ResponseInterface); + $this->assertEquals(2, $called); + } + + public function testCancelPendingNextHandler() + { + $once = $this->expectCallableOnce(); + $middleware = new MiddlewareRunner(array( + function (RequestInterface $request, $next) { + $ret = $next($request); + $ret->cancel(); + + return $ret; + }, + function (RequestInterface $request) use ($once) { + return new Promise\Promise(function () { }, $once); + } + )); + + $request = new ServerRequest('GET', 'http://example.com/'); + + $middleware($request); + } + + public function testCancelResultingPromiseWillCancelPendingNextHandler() + { + $once = $this->expectCallableOnce(); + $middleware = new MiddlewareRunner(array( + function (RequestInterface $request, $next) { + return $next($request); + }, + function (RequestInterface $request) use ($once) { + return new Promise\Promise(function () { }, $once); + } + )); + + $request = new ServerRequest('GET', 'http://example.com/'); + + $promise = $middleware($request); + + $this->assertTrue($promise instanceof CancellablePromiseInterface); + $promise->cancel(); + } } From 505dd30fa6eaa4906a988df7fbcbd0f363300633 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 31 Dec 2017 11:28:21 +0100 Subject: [PATCH 264/456] Avoid promise wrapping for middleware next request handlers --- src/Io/MiddlewareRunner.php | 16 +++-------- tests/Io/MiddlewareRunnerTest.php | 48 +++++++++++++++++++------------ 2 files changed, 34 insertions(+), 30 deletions(-) diff --git a/src/Io/MiddlewareRunner.php b/src/Io/MiddlewareRunner.php index 89ba5b4d..8ba6a31b 100644 --- a/src/Io/MiddlewareRunner.php +++ b/src/Io/MiddlewareRunner.php @@ -4,7 +4,6 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; -use React\Promise; use React\Promise\PromiseInterface; /** @@ -29,12 +28,13 @@ public function __construct(array $middleware) /** * @param ServerRequestInterface $request - * @return PromiseInterface + * @return ResponseInterface|PromiseInterface + * @throws Exception */ public function __invoke(ServerRequestInterface $request) { if (count($this->middleware) === 0) { - return Promise\reject(new \RuntimeException('No middleware to run')); + throw new \RuntimeException('No middleware to run'); } return $this->call($request, 0); @@ -49,14 +49,6 @@ public function call(ServerRequestInterface $request, $position) }; $handler = $this->middleware[$position]; - try { - return Promise\resolve($handler($request, $next)); - } catch (\Exception $error) { - // request handler callback throws an Exception - return Promise\reject($error); - } catch (\Throwable $error) { // @codeCoverageIgnoreStart - // request handler callback throws a PHP7+ Error - return Promise\reject($error); // @codeCoverageIgnoreEnd - } + return $handler($request, $next); } } diff --git a/tests/Io/MiddlewareRunnerTest.php b/tests/Io/MiddlewareRunnerTest.php index adfdc347..757749c8 100644 --- a/tests/Io/MiddlewareRunnerTest.php +++ b/tests/Io/MiddlewareRunnerTest.php @@ -14,6 +14,7 @@ use RingCentral\Psr7\Response; use Psr\Http\Message\RequestInterface; use React\Promise\CancellablePromiseInterface; +use React\Promise\PromiseInterface; final class MiddlewareRunnerTest extends TestCase { @@ -21,16 +22,20 @@ final class MiddlewareRunnerTest extends TestCase * @expectedException RuntimeException * @expectedExceptionMessage No middleware to run */ - public function testDefaultResponse() + public function testEmptyMiddlewareStackThrowsException() { $request = new ServerRequest('GET', 'https://example.com/'); $middlewares = array(); $middlewareStack = new MiddlewareRunner($middlewares); - Block\await($middlewareStack($request), Factory::create()); + $middlewareStack($request); } - public function testReturnsRejectedPromiseIfHandlerThrowsException() + /** + * @expectedException RuntimeException + * @expectedExceptionMessage hello + */ + public function testThrowsIfHandlerThrowsException() { $middleware = new MiddlewareRunner(array( function (ServerRequestInterface $request) { @@ -40,15 +45,15 @@ function (ServerRequestInterface $request) { $request = new ServerRequest('GET', 'http://example.com/'); - $promise = $middleware($request); - - $promise->then(null, $this->expectCallableOnceWith($this->isInstanceOf('RuntimeException'))); + $middleware($request); } /** * @requires PHP 7 + * @expectedException Throwable + * @expectedExceptionMessage hello */ - public function testReturnsRejectedPromiseIfHandlerThrowsThrowable() + public function testThrowsIfHandlerThrowsThrowable() { $middleware = new MiddlewareRunner(array( function (ServerRequestInterface $request) { @@ -58,9 +63,7 @@ function (ServerRequestInterface $request) { $request = new ServerRequest('GET', 'http://example.com/'); - $promise = $middleware($request); - - $promise->then(null, $this->expectCallableOnceWith($this->isInstanceOf('Throwable'))); + $middleware($request); } public function provideProcessStackMiddlewares() @@ -126,9 +129,14 @@ public function testProcessStack(array $middlewares, $expectedCallCount) $request = new ServerRequest('GET', 'https://example.com/'); $middlewareStack = new MiddlewareRunner($middlewares); - /** @var ResponseInterface $result */ - $result = Block\await($middlewareStack($request), Factory::create()); - $this->assertSame(200, $result->getStatusCode()); + $response = $middlewareStack($request); + + $this->assertTrue($response instanceof PromiseInterface); + $response = Block\await($response, Factory::create()); + + $this->assertTrue($response instanceof ResponseInterface); + $this->assertSame(200, $response->getStatusCode()); + foreach ($middlewares as $middleware) { if (!($middleware instanceof ProcessStack)) { continue; @@ -163,7 +171,11 @@ public function testNextCanBeRunMoreThanOnceWithoutCorruptingTheMiddlewareStack( $retryCalled = 0; $error = null; $retry = function ($request, $next) use (&$error, &$retryCalled) { - return $next($request)->then(null, function ($et) use (&$error, $request, $next, &$retryCalled) { + $promise = new \React\Promise\Promise(function ($resolve) use ($request, $next) { + $resolve($next($request)); + }); + + return $promise->then(null, function ($et) use (&$error, $request, $next, &$retryCalled) { $retryCalled++; $error = $et; // the $next failed. discard $error and retry once again: @@ -214,7 +226,7 @@ function (ServerRequestInterface $request, $next) use (&$receivedRequests) { }, function (ServerRequestInterface $request, $next) use (&$receivedRequests) { $receivedRequests[] = 'middleware3: ' . $request->getUri(); - return $next($request); + return new \React\Promise\Promise(function () { }); } )); @@ -372,9 +384,9 @@ public function testUncommonMiddlewareArrayFormats($middlewareFactory, $expected $request = new ServerRequest('GET', 'https://example.com/'); $middlewareStack = new MiddlewareRunner($middlewareFactory()); - /** @var ResponseInterface $response */ - $response = Block\await($middlewareStack($request), Factory::create()); + $response = $middlewareStack($request); + $this->assertTrue($response instanceof ResponseInterface); $this->assertSame($expectedSequence, (string) $response->getBody()); } @@ -397,7 +409,7 @@ function (RequestInterface $request) use (&$called) { $request = new ServerRequest('GET', 'http://example.com/'); - $response = Block\await($middleware($request), Factory::create()); + $response = $middleware($request); $this->assertTrue($response instanceof ResponseInterface); $this->assertEquals(2, $called); From cfd5d5f185cd64843f4670f29ba061a46d2a3834 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 31 Dec 2017 12:35:16 +0100 Subject: [PATCH 265/456] Documentation for consuming response from next request handler function --- README.md | 78 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 74 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 9675ba1a..e974eb72 100644 --- a/README.md +++ b/README.md @@ -798,10 +798,27 @@ As such, this project supports the concept of middleware request handlers. A middleware request handler is expected to adhere the following rules: -* It is a `callable`. -* It accepts `ServerRequestInterface` as first argument and optional `callable` as second argument. -* It returns a `ResponseInterface` (or any promise which can be consumed by [`Promise\resolve`](http://reactphp.org/promise/#resolve) resolving to a `ResponseInterface`) -* It calls `$next($request)` to continue processing the next middleware function or returns explicitly to abort the chain +* It is a valid `callable`. +* It accepts `ServerRequestInterface` as first argument and an optional + `callable` as second argument. +* It returns either: + * An instance implementing `ResponseInterface` for direct consumption. + * Any promise which can be consumed by + [`Promise\resolve()`](http://reactphp.org/promise/#resolve) resolving to a + `ResponseInterface` for deferred consumption. + * It MAY throw an `Exception` (or return a rejected promise) in order to + signal an error condition and abort the chain. +* It calls `$next($request)` to continue processing the next middleware + request handler function or returns explicitly without calling `$next` to + abort the chain. + * The `$next` request handler function (recursively) invokes the next request + handler from the chain with the same logic as above and returns (or throws) + as above. + * The `$request` may be modified prior to calling `$next($request)` to + change the incoming request the next middleware operates on. + * The `$next` return value may be consumed to modify the outgoing response. + * The `$next` request handler MAY be called more than once if you want to + implement custom "retry" logic etc. Note that this very simple definition allows you to use either anonymous functions or any classes that use the magic `__invoke()` method. @@ -828,9 +845,62 @@ $server = new Server(array( $request = $request->withHeader('Request-Time', time()); return $next($request); }, + function (ServerRequestInterface $request) { + return new Response(200); + } +)); +``` + +Similarly, you can use the result of the `$next` middleware request handler +function to modify the outgoing response. +Note that as per the above documentation, the `$next` method may return a +`ResponseInterface` directly or one wrapped in a promise for deferred +resolution. +In order to simplify handling both paths, you can simply wrap this in a +[`Promise\resolve()`](http://reactphp.org/promise/#resolve) call like this: + +```php +$server = new Server(array( function (ServerRequestInterface $request, callable $next) { + $promise = React\Promise\resolve($next($request)); + return $promise->then(function (ResponseInterface $response) { + return $response->withHeader('Content-Type', 'text/html'); + }); + }, + function (ServerRequestInterface $request) { return new Response(200); + } +)); +``` + +Note that the `$next` middleware request handler function may also throw an +`Exception` (or return a rejected promise) as described above. +The previous example does not catch any exceptions and would thus signal an +error condition to the `Server`. +Alternatively, you can also catch any `Exception` to implement custom error +handling logic (or logging etc.) by wrapping this in a +[`Promise`](http://reactphp.org/promise/#promise) like this: + +```php +$server = new Server(array( + function (ServerRequestInterface $request, callable $next) { + $promise = new React\Promise\Promise(function ($resolve) use ($next, $request) { + $resolve($next($request)); + }); + return $promise->then(null, function (Exception $e) { + return new Response( + 500, + array(), + 'Internal error: ' . $e->getMessage() + ); + }); }, + function (ServerRequestInterface $request) { + if (mt_rand(0, 1) === 1) { + throw new RuntimeException('Database error'); + } + return new Response(200); + } )); ``` From 485769d65b939e4827b38f12aac0657e6d4aee98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 2 Jan 2018 12:57:34 +0100 Subject: [PATCH 266/456] Improve performance by using happy path when concurrency is below limit --- .../LimitConcurrentRequestsMiddleware.php | 58 +++++++-- .../LimitConcurrentRequestsMiddlewareTest.php | 121 ++++++++++++++++-- 2 files changed, 152 insertions(+), 27 deletions(-) diff --git a/src/Middleware/LimitConcurrentRequestsMiddleware.php b/src/Middleware/LimitConcurrentRequestsMiddleware.php index 3e16a95f..2fab437b 100644 --- a/src/Middleware/LimitConcurrentRequestsMiddleware.php +++ b/src/Middleware/LimitConcurrentRequestsMiddleware.php @@ -2,10 +2,12 @@ namespace React\Http\Middleware; +use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use React\Http\Io\HttpBodyStream; use React\Http\Io\PauseBufferStream; use React\Promise; +use React\Promise\PromiseInterface; use React\Promise\Deferred; use React\Stream\ReadableStreamInterface; @@ -81,6 +83,35 @@ public function __construct($limit) public function __invoke(ServerRequestInterface $request, $next) { + // happy path: simply invoke next request handler if we're below limit + if ($this->pending < $this->limit) { + ++$this->pending; + + try { + $response = $next($request); + } catch (\Exception $e) { + $this->processQueue(); + throw $e; + } catch (\Throwable $e) { // @codeCoverageIgnoreStart + // handle Errors just like Exceptions (PHP 7+ only) + $this->processQueue(); + throw $e; // @codeCoverageIgnoreEnd + } + + // happy path: if next request handler returned immediately, + // we can simply try to invoke the next queued request + if ($response instanceof ResponseInterface) { + $this->processQueue(); + return $response; + } + + // if the next handler returns a pending promise, we have to + // await its resolution before invoking next queued request + return $this->await(Promise\resolve($response)); + } + + // if we reach this point, then this request will need to be queued + // check if the body is streaming, in which case we need to buffer everything $body = $request->getBody(); if ($body instanceof ReadableStreamInterface) { // pause actual body to stop emitting data until the handler is called @@ -110,13 +141,12 @@ public function __invoke(ServerRequestInterface $request, $next) // queue request and process queue if pending does not exceed limit $queue[$id] = $deferred; - $this->processQueue(); $that = $this; $pending = &$this->pending; - return $deferred->promise()->then(function () use ($request, $next, $body, &$pending) { - $pending++; - + return $this->await($deferred->promise()->then(function () use ($request, $next, $body, &$pending) { + // invoke next request handler + ++$pending; $ret = $next($request); // resume readable stream and replay buffered events @@ -125,13 +155,18 @@ public function __invoke(ServerRequestInterface $request, $next) } return $ret; - })->then(function ($response) use ($that, &$pending) { - $pending--; + })); + } + + private function await(PromiseInterface $promise) + { + $that = $this; + + return $promise->then(function ($response) use ($that) { $that->processQueue(); return $response; - }, function ($error) use ($that, &$pending) { - $pending--; + }, function ($error) use ($that) { $that->processQueue(); return Promise\reject($error); @@ -143,11 +178,8 @@ public function __invoke(ServerRequestInterface $request, $next) */ public function processQueue() { - if ($this->pending >= $this->limit) { - return; - } - - if (!$this->queue) { + // skip if we're still above concurrency limit or there's no queued request waiting + if (--$this->pending >= $this->limit || !$this->queue) { return; } diff --git a/tests/Middleware/LimitConcurrentRequestsMiddlewareTest.php b/tests/Middleware/LimitConcurrentRequestsMiddlewareTest.php index ffe6facd..98fe1b9a 100644 --- a/tests/Middleware/LimitConcurrentRequestsMiddlewareTest.php +++ b/tests/Middleware/LimitConcurrentRequestsMiddlewareTest.php @@ -11,6 +11,7 @@ use React\Stream\ThroughStream; use React\Tests\Http\TestCase; use React\Promise\PromiseInterface; +use React\Http\Response; final class LimitConcurrentRequestsMiddlewareTest extends TestCase { @@ -94,13 +95,110 @@ public function testLimitOneRequestConcurrently() $this->assertTrue($calledC); } - public function testStreamPauseAndResume() + public function testReturnsResponseDirectlyFromMiddlewareWhenBelowLimit() + { + $middleware = new LimitConcurrentRequestsMiddleware(1); + + $response = new Response(); + $ret = $middleware(new ServerRequest('GET', 'https://example.com/'), function () use ($response) { + return $response; + }); + + $this->assertSame($response, $ret); + } + + /** + * @expectedException RuntimeException + * @expectedExceptionMessage demo + */ + public function testThrowsExceptionDirectlyFromMiddlewareWhenBelowLimit() + { + $middleware = new LimitConcurrentRequestsMiddleware(1); + + $middleware(new ServerRequest('GET', 'https://example.com/'), function () { + throw new \RuntimeException('demo'); + }); + } + + /** + * @requires PHP 7 + * @expectedException Error + * @expectedExceptionMessage demo + */ + public function testThrowsErrorDirectlyFromMiddlewareWhenBelowLimit() + { + $middleware = new LimitConcurrentRequestsMiddleware(1); + + $middleware(new ServerRequest('GET', 'https://example.com/'), function () { + throw new \Error('demo'); + }); + } + + public function testReturnsPendingPromiseChainedFromMiddlewareWhenBelowLimit() + { + $middleware = new LimitConcurrentRequestsMiddleware(1); + + $deferred = new Deferred(); + $ret = $middleware(new ServerRequest('GET', 'https://example.com/'), function () use ($deferred) { + return $deferred->promise(); + }); + + $this->assertTrue($ret instanceof PromiseInterface); + } + + public function testReturnsPendingPromiseFromMiddlewareWhenAboveLimit() + { + $middleware = new LimitConcurrentRequestsMiddleware(1); + + $middleware(new ServerRequest('GET', 'https://example.com/'), function () { + return new Promise(function () { }); + }); + + $ret = $middleware(new ServerRequest('GET', 'https://example.com/'), function () { + return new Response(); + }); + + $this->assertTrue($ret instanceof PromiseInterface); + } + + public function testStreamDoesNotPauseOrResumeWhenBelowLimit() + { + $body = $this->getMockBuilder('React\Http\Io\HttpBodyStream')->disableOriginalConstructor()->getMock(); + $body->expects($this->never())->method('pause'); + $body->expects($this->never())->method('resume'); + $limitHandlers = new LimitConcurrentRequestsMiddleware(1); + $limitHandlers(new ServerRequest('GET', 'https://example.com/', array(), $body), function () {}); + } + + public function testStreamDoesPauseWhenAboveLimit() + { + $body = $this->getMockBuilder('React\Http\Io\HttpBodyStream')->disableOriginalConstructor()->getMock(); + $body->expects($this->once())->method('pause'); + $body->expects($this->never())->method('resume'); + $limitHandlers = new LimitConcurrentRequestsMiddleware(1); + + $limitHandlers(new ServerRequest('GET', 'https://example.com'), function () { + return new Promise(function () { }); + }); + + $limitHandlers(new ServerRequest('GET', 'https://example.com/', array(), $body), function () {}); + } + + public function testStreamDoesPauseAndThenResumeWhenDequeued() { $body = $this->getMockBuilder('React\Http\Io\HttpBodyStream')->disableOriginalConstructor()->getMock(); $body->expects($this->once())->method('pause'); $body->expects($this->once())->method('resume'); $limitHandlers = new LimitConcurrentRequestsMiddleware(1); + + $deferred = new Deferred(); + $limitHandlers(new ServerRequest('GET', 'https://example.com'), function () use ($deferred) { + return $deferred->promise(); + }); + $limitHandlers(new ServerRequest('GET', 'https://example.com/', array(), $body), function () {}); + + $deferred->reject(); } public function testReceivesBufferedRequestSameInstance() @@ -121,7 +219,7 @@ public function testReceivesBufferedRequestSameInstance() $this->assertSame($request, $req); } - public function testReceivesStreamingBodyChangesInstanceWithCustomBodyButSameData() + public function testReceivesStreamingBodyRequestSameInstanceWhenBelowLimit() { $stream = new ThroughStream(); $request = new ServerRequest( @@ -137,15 +235,9 @@ public function testReceivesStreamingBodyChangesInstanceWithCustomBodyButSameDat $req = $request; }); - $this->assertNotSame($request, $req); - $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $req); + $this->assertSame($request, $req); $body = $req->getBody(); - $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); - /* @var $body \React\Stream\ReadableStreamInterface */ - - $this->assertEquals(5, $body->getSize()); - $body->on('data', $this->expectCallableOnce('hello')); $stream->write('hello'); } @@ -273,7 +365,7 @@ public function testReceivesNextRequestAfterPreviousHandlerIsCancelled() $middleware($request, $this->expectCallableOnceWith($request)); } - public function testReceivesNextStreamingBodyWithSameDataAfterPreviousHandlerIsSettled() + public function testReceivesStreamingBodyChangesInstanceWithCustomBodyButSameDataWhenDequeued() { $stream = new ThroughStream(); $request = new ServerRequest( @@ -283,19 +375,20 @@ public function testReceivesNextStreamingBodyWithSameDataAfterPreviousHandlerIsS new HttpBodyStream($stream, 5) ); - $deferred = new Deferred(); $middleware = new LimitConcurrentRequestsMiddleware(1); - $middleware($request, function () use ($deferred) { + + $deferred = new Deferred(); + $middleware(new ServerRequest('GET', 'https://example.com/'), function () use ($deferred) { return $deferred->promise(); }); - $deferred->reject(new \RuntimeException()); - $req = null; $middleware($request, function (ServerRequestInterface $request) use (&$req) { $req = $request; }); + $deferred->reject(); + $this->assertNotSame($request, $req); $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $req); From 92deba861d5a1320584450cfa61622a2969b00ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 5 Jan 2018 16:30:50 +0100 Subject: [PATCH 267/456] Prepare v0.8.1 release --- CHANGELOG.md | 18 ++++++++++++++++++ README.md | 11 +++++------ 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 87d5b5c1..1c4ab06c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # Changelog +## 0.8.1 (2018-01-05) + +* Major request handler performance improvement. Benchmarks suggest number of + requests/s improved by more than 50% for common `GET` requests! + We now avoid queuing, buffering and wrapping incoming requests in promises + when we're below limits and instead can directly process common requests. + (#291, #292, #293, #294 and #296 by @clue) + +* Fix: Fix concurrent invoking next middleware request handlers + (#293 by @clue) + +* Small code improvements + (#286 by @seregazhuk) + +* Improve test suite to be less fragile when using `ext-event` and + fix test suite forward compatibility with upcoming EventLoop releases + (#288 and #290 by @clue) + ## 0.8.0 (2017-12-12) * Feature / BC break: Add new `Server` facade that buffers and parses incoming diff --git a/README.md b/README.md index e974eb72..054be8f8 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ # Http [![Build Status](https://travis-ci.org/reactphp/http.svg?branch=master)](https://travis-ci.org/reactphp/http) -[![Code Climate](https://codeclimate.com/github/reactphp/http/badges/gpa.svg)](https://codeclimate.com/github/reactphp/http) Event-driven, streaming plaintext HTTP and secure HTTPS server for [ReactPHP](https://reactphp.org/). @@ -34,7 +33,7 @@ Event-driven, streaming plaintext HTTP and secure HTTPS server for [ReactPHP](ht ## Quickstart example -This is an HTTP server which responds with `Hello World` to every request. +This is an HTTP server which responds with `Hello World!` to every request. ```php $loop = React\EventLoop\Factory::create(); @@ -1132,13 +1131,13 @@ A non-exhaustive list of third-party middleware can be found at the [`Middleware ## Install -The recommended way to install this library is [through Composer](http://getcomposer.org). -[New to Composer?](http://getcomposer.org/doc/00-intro.md) +The recommended way to install this library is [through Composer](https://getcomposer.org). +[New to Composer?](https://getcomposer.org/doc/00-intro.md) This will install the latest supported version: ```bash -$ composer require react/http:^0.8 +$ composer require react/http:^0.8.1 ``` See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. @@ -1151,7 +1150,7 @@ It's *highly recommended to use PHP 7+* for this project. ## Tests To run the test suite, you first need to clone this repo and then install all -dependencies [through Composer](http://getcomposer.org): +dependencies [through Composer](https://getcomposer.org): ```bash $ composer install From c69a20565fc9bafdde06d2b2f921788fc5077c3c Mon Sep 17 00:00:00 2001 From: Zhuk Sergey Date: Sun, 7 Jan 2018 11:43:32 +0300 Subject: [PATCH 268/456] documentation improvement --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 054be8f8..0e08270d 100644 --- a/README.md +++ b/README.md @@ -808,9 +808,9 @@ A middleware request handler is expected to adhere the following rules: * It MAY throw an `Exception` (or return a rejected promise) in order to signal an error condition and abort the chain. * It calls `$next($request)` to continue processing the next middleware - request handler function or returns explicitly without calling `$next` to + request handler or returns explicitly without calling `$next` to abort the chain. - * The `$next` request handler function (recursively) invokes the next request + * The `$next` request handler (recursively) invokes the next request handler from the chain with the same logic as above and returns (or throws) as above. * The `$request` may be modified prior to calling `$next($request)` to From b0a4d1086f3855c0f40d33fbcf697a1983abe168 Mon Sep 17 00:00:00 2001 From: Zhuk Sergey Date: Mon, 8 Jan 2018 15:37:43 +0300 Subject: [PATCH 269/456] documentation examples improvement --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 0e08270d..388e3857 100644 --- a/README.md +++ b/README.md @@ -549,8 +549,8 @@ This example shows how such a long-term action could look like: ```php $server = new Server(function (ServerRequestInterface $request) use ($loop) { - return new Promise(function ($resolve, $reject) use ($request, $loop) { - $loop->addTimer(1.5, function() use ($loop, $resolve) { + return new Promise(function ($resolve, $reject) use ($loop) { + $loop->addTimer(1.5, function() use ($resolve) { $response = new Response( 200, array( @@ -682,7 +682,7 @@ pass this header yourself. If you know the length of your stream body, you MAY specify it like this instead: ```php -$stream = new ThroughStream() +$stream = new ThroughStream(); $server = new Server(function (ServerRequestInterface $request) use ($stream) { return new Response( 200, From 865114601fd5fabfd3101f475e65497005eb04bc Mon Sep 17 00:00:00 2001 From: Zhuk Sergey Date: Tue, 9 Jan 2018 23:06:41 +0300 Subject: [PATCH 270/456] some code cleanup --- examples/06-sleep.php | 4 ++-- src/Io/IniUtil.php | 6 +++--- src/Io/MiddlewareRunner.php | 6 +++--- src/Io/PauseBufferStream.php | 2 +- src/Io/ServerRequest.php | 4 ++-- src/Middleware/LimitConcurrentRequestsMiddleware.php | 1 - src/Server.php | 4 +--- 7 files changed, 12 insertions(+), 15 deletions(-) diff --git a/examples/06-sleep.php b/examples/06-sleep.php index 3b7a85f0..ae465fb5 100644 --- a/examples/06-sleep.php +++ b/examples/06-sleep.php @@ -11,8 +11,8 @@ $loop = Factory::create(); $server = new Server(function (ServerRequestInterface $request) use ($loop) { - return new Promise(function ($resolve, $reject) use ($request, $loop) { - $loop->addTimer(1.5, function() use ($loop, $resolve) { + return new Promise(function ($resolve, $reject) use ($loop) { + $loop->addTimer(1.5, function() use ($resolve) { $response = new Response( 200, array( diff --git a/src/Io/IniUtil.php b/src/Io/IniUtil.php index d8bce2d3..7430b9f9 100644 --- a/src/Io/IniUtil.php +++ b/src/Io/IniUtil.php @@ -10,7 +10,7 @@ final class IniUtil /** * Convert a ini like size to a numeric size in bytes. * - * @param string $iniSetting + * @param string $size * @return int */ public static function iniSizeToBytes($size) @@ -23,11 +23,11 @@ public static function iniSizeToBytes($size) $strippedSize = substr($size, 0, -1); if (!is_numeric($strippedSize)) { - throw new \InvalidArgumentException('"' . $size . '" is not a valid ini size'); + throw new \InvalidArgumentException("$size is not a valid ini size"); } if ($strippedSize <= 0) { - throw new \InvalidArgumentException('Expect "' . $size . '" to be higher isn\'t zero or lower'); + throw new \InvalidArgumentException("Expect $size to be higher isn't zero or lower"); } if ($suffix === 'K') { diff --git a/src/Io/MiddlewareRunner.php b/src/Io/MiddlewareRunner.php index 8ba6a31b..de589ae3 100644 --- a/src/Io/MiddlewareRunner.php +++ b/src/Io/MiddlewareRunner.php @@ -16,7 +16,7 @@ final class MiddlewareRunner /** * @var callable[] */ - private $middleware = array(); + private $middleware; /** * @param callable[] $middleware @@ -29,11 +29,11 @@ public function __construct(array $middleware) /** * @param ServerRequestInterface $request * @return ResponseInterface|PromiseInterface - * @throws Exception + * @throws \Exception */ public function __invoke(ServerRequestInterface $request) { - if (count($this->middleware) === 0) { + if (empty($this->middleware)) { throw new \RuntimeException('No middleware to run'); } diff --git a/src/Io/PauseBufferStream.php b/src/Io/PauseBufferStream.php index 6e2e3da4..fb5ed456 100644 --- a/src/Io/PauseBufferStream.php +++ b/src/Io/PauseBufferStream.php @@ -29,7 +29,7 @@ class PauseBufferStream extends EventEmitter implements ReadableStreamInterface private $dataPaused = ''; private $endPaused = false; private $closePaused = false; - private $errorPaused = null; + private $errorPaused; private $implicit = false; public function __construct(ReadableStreamInterface $input) diff --git a/src/Io/ServerRequest.php b/src/Io/ServerRequest.php index 0c9040ae..af6cccda 100644 --- a/src/Io/ServerRequest.php +++ b/src/Io/ServerRequest.php @@ -25,11 +25,11 @@ class ServerRequest extends Request implements ServerRequestInterface { private $attributes = array(); - private $serverParams = array(); + private $serverParams; private $fileParams = array(); private $cookies = array(); private $queryParams = array(); - private $parsedBody = null; + private $parsedBody; /** * @param null|string $method HTTP method for the request. diff --git a/src/Middleware/LimitConcurrentRequestsMiddleware.php b/src/Middleware/LimitConcurrentRequestsMiddleware.php index 2fab437b..827ce21d 100644 --- a/src/Middleware/LimitConcurrentRequestsMiddleware.php +++ b/src/Middleware/LimitConcurrentRequestsMiddleware.php @@ -142,7 +142,6 @@ public function __invoke(ServerRequestInterface $request, $next) // queue request and process queue if pending does not exceed limit $queue[$id] = $deferred; - $that = $this; $pending = &$this->pending; return $this->await($deferred->promise()->then(function () use ($request, $next, $body, &$pending) { // invoke next request handler diff --git a/src/Server.php b/src/Server.php index 999409b3..6d5b83df 100644 --- a/src/Server.php +++ b/src/Server.php @@ -70,9 +70,7 @@ public function __construct($requestHandler) if (is_callable($requestHandler)) { $middleware[] = $requestHandler; } else { - foreach ($requestHandler as $one) { - $middleware[] = $one; - } + $middleware = array_merge($middleware, $requestHandler); } $this->streamingServer = new StreamingServer($middleware); From 0ffd0c6ebbb5fe98f00d8be086a310744c22f91d Mon Sep 17 00:00:00 2001 From: Vladimir Reznichenko Date: Wed, 24 Jan 2018 11:11:36 +0100 Subject: [PATCH 271/456] SCA: minor control flow tweaks --- examples/09-stream-request.php | 7 ++++--- src/Io/ChunkedEncoder.php | 3 +-- src/Server.php | 3 +-- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/examples/09-stream-request.php b/examples/09-stream-request.php index af4fb580..07cd21ed 100644 --- a/examples/09-stream-request.php +++ b/examples/09-stream-request.php @@ -16,11 +16,12 @@ $server = new StreamingServer(function (ServerRequestInterface $request) { return new Promise(function ($resolve, $reject) use ($request) { $contentLength = 0; - $request->getBody()->on('data', function ($data) use (&$contentLength) { + $requestBody = $request->getBody(); + $requestBody->on('data', function ($data) use (&$contentLength) { $contentLength += strlen($data); }); - $request->getBody()->on('end', function () use ($resolve, &$contentLength){ + $requestBody->on('end', function () use ($resolve, &$contentLength){ $response = new Response( 200, array( @@ -32,7 +33,7 @@ }); // an error occures e.g. on invalid chunked encoded data or an unexpected 'end' event - $request->getBody()->on('error', function (\Exception $exception) use ($resolve, &$contentLength) { + $requestBody->on('error', function (\Exception $exception) use ($resolve, &$contentLength) { $response = new Response( 400, array( diff --git a/src/Io/ChunkedEncoder.php b/src/Io/ChunkedEncoder.php index c075bdbc..8a7f8cfb 100644 --- a/src/Io/ChunkedEncoder.php +++ b/src/Io/ChunkedEncoder.php @@ -101,8 +101,7 @@ public function handleEnd() */ private function createChunk($data) { - $byteSize = strlen($data); - $byteSize = dechex($byteSize); + $byteSize = dechex(strlen($data)); $chunkBeginning = $byteSize . "\r\n"; return $chunkBeginning . $data . "\r\n"; diff --git a/src/Server.php b/src/Server.php index 999409b3..d413bddf 100644 --- a/src/Server.php +++ b/src/Server.php @@ -102,8 +102,7 @@ private function getConcurrentRequestsLimit() } $availableMemory = IniUtil::iniSizeToBytes(ini_get('memory_limit')) / 4; - $concurrentRequests = $availableMemory / IniUtil::iniSizeToBytes(ini_get('post_max_size')); - $concurrentRequests = ceil($concurrentRequests); + $concurrentRequests = ceil($availableMemory / IniUtil::iniSizeToBytes(ini_get('post_max_size'))); if ($concurrentRequests >= self::MAXIMUM_CONCURRENT_REQUESTS) { return self::MAXIMUM_CONCURRENT_REQUESTS; From 6de0a8cc2356ff3b17d2ee56e7e29ca4f04de036 Mon Sep 17 00:00:00 2001 From: Vladimir Reznichenko Date: Wed, 24 Jan 2018 11:13:27 +0100 Subject: [PATCH 272/456] SCA: constant time string starts with check --- src/Io/RequestHeaderParser.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Io/RequestHeaderParser.php b/src/Io/RequestHeaderParser.php index 21d2d322..23183fe5 100644 --- a/src/Io/RequestHeaderParser.php +++ b/src/Io/RequestHeaderParser.php @@ -70,10 +70,10 @@ private function parseRequest($headers) // parser does not support asterisk-form and authority-form // remember original target and temporarily replace and re-apply below $originalTarget = null; - if (strpos($headers, 'OPTIONS * ') === 0) { + if (strncmp($headers, 'OPTIONS * ', 10) === 0) { $originalTarget = '*'; $headers = 'OPTIONS / ' . substr($headers, 10); - } elseif (strpos($headers, 'CONNECT ') === 0) { + } elseif (strncmp($headers, 'CONNECT ', 8) === 0) { $parts = explode(' ', $headers, 3); $uri = parse_url('tcp://' . $parts[1]); From c90efb64b46c5a236701d8495b769a68d906c9c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Wed, 7 Mar 2018 19:02:41 +0100 Subject: [PATCH 273/456] Fix Server to skip SERVER_ADDR params for Unix domain sockets (UDS) --- README.md | 4 ++++ src/Io/RequestHeaderParser.php | 11 +++++++++-- tests/Io/RequestHeaderParserTest.php | 27 +++++++++++++++++++++++++++ 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 388e3857..273b67b7 100644 --- a/README.md +++ b/README.md @@ -270,6 +270,10 @@ $server = new Server(function (ServerRequestInterface $request) { See also [example #3](examples). +> Advanced: Note that address parameters will not be set if you're listening on + a Unix domain socket (UDS) path as this protocol lacks the concept of + host/port. + #### Query parameters The `getQueryParams(): array` method can be used to get the query parameters diff --git a/src/Io/RequestHeaderParser.php b/src/Io/RequestHeaderParser.php index 23183fe5..1264371b 100644 --- a/src/Io/RequestHeaderParser.php +++ b/src/Io/RequestHeaderParser.php @@ -97,16 +97,23 @@ private function parseRequest($headers) 'REQUEST_TIME_FLOAT' => microtime(true) ); + // apply REMOTE_ADDR and REMOTE_PORT if source address is known + // address should always be known, unless this is over Unix domain sockets (UDS) if ($this->remoteSocketUri !== null) { $remoteAddress = parse_url($this->remoteSocketUri); $serverParams['REMOTE_ADDR'] = $remoteAddress['host']; $serverParams['REMOTE_PORT'] = $remoteAddress['port']; } + // 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 if ($this->localSocketUri !== null) { $localAddress = parse_url($this->localSocketUri); - $serverParams['SERVER_ADDR'] = $localAddress['host']; - $serverParams['SERVER_PORT'] = $localAddress['port']; + if (isset($localAddress['host'], $localAddress['port'])) { + $serverParams['SERVER_ADDR'] = $localAddress['host']; + $serverParams['SERVER_PORT'] = $localAddress['port']; + } if (isset($localAddress['scheme']) && $localAddress['scheme'] === 'https') { $serverParams['HTTPS'] = 'on'; } diff --git a/tests/Io/RequestHeaderParserTest.php b/tests/Io/RequestHeaderParserTest.php index 6b885bd4..b47eac15 100644 --- a/tests/Io/RequestHeaderParserTest.php +++ b/tests/Io/RequestHeaderParserTest.php @@ -408,6 +408,33 @@ public function testServerParamsWillBeSetOnHttpRequest() $this->assertEquals('8001', $serverParams['REMOTE_PORT']); } + public function testServerParamsWillNotSetRemoteAddressForUnixDomainSockets() + { + $request = null; + + $parser = new RequestHeaderParser( + 'unix://./server.sock', + null + ); + + $parser->on('headers', function ($parsedRequest) use (&$request) { + $request = $parsedRequest; + }); + + $parser->feed("GET /foo HTTP/1.0\r\nHost: example.com\r\n\r\n"); + $serverParams = $request->getServerParams(); + + $this->assertArrayNotHasKey('HTTPS', $serverParams); + $this->assertNotEmpty($serverParams['REQUEST_TIME']); + $this->assertNotEmpty($serverParams['REQUEST_TIME_FLOAT']); + + $this->assertArrayNotHasKey('SERVER_ADDR', $serverParams); + $this->assertArrayNotHasKey('SERVER_PORT', $serverParams); + + $this->assertArrayNotHasKey('REMOTE_ADDR', $serverParams); + $this->assertArrayNotHasKey('REMOTE_PORT', $serverParams); + } + public function testServerParamsWontBeSetOnMissingUrls() { $request = null; From 465168f98aca96e557927ca64f43b0bc89f3abd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 13 Mar 2018 16:49:47 +0100 Subject: [PATCH 274/456] Do not pass $next handler to final request handler --- README.md | 8 +++++-- src/Io/MiddlewareRunner.php | 9 ++++++- tests/Io/MiddlewareRunnerTest.php | 39 ++++++++++++++++++++++++++++++- 3 files changed, 52 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 388e3857..69b91638 100644 --- a/README.md +++ b/README.md @@ -850,9 +850,13 @@ $server = new Server(array( )); ``` +> Note how the middleware request handler and the final request handler have a + very simple (and similar) interface. The only difference is that the final + request handler does not receive a `$next` handler. + Similarly, you can use the result of the `$next` middleware request handler function to modify the outgoing response. -Note that as per the above documentation, the `$next` method may return a +Note that as per the above documentation, the `$next` function may return a `ResponseInterface` directly or one wrapped in a promise for deferred resolution. In order to simplify handling both paths, you can simply wrap this in a @@ -1002,7 +1006,7 @@ Usage: $server = new StreamingServer(array( new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers new RequestBodyBufferMiddleware(16 * 1024 * 1024), // 16 MiB - function (ServerRequestInterface $request, callable $next) { + function (ServerRequestInterface $request) { // The body from $request->getBody() is now fully available without the need to stream it return new Response(200); }, diff --git a/src/Io/MiddlewareRunner.php b/src/Io/MiddlewareRunner.php index 8ba6a31b..67d492d4 100644 --- a/src/Io/MiddlewareRunner.php +++ b/src/Io/MiddlewareRunner.php @@ -29,7 +29,7 @@ public function __construct(array $middleware) /** * @param ServerRequestInterface $request * @return ResponseInterface|PromiseInterface - * @throws Exception + * @throws \Exception */ public function __invoke(ServerRequestInterface $request) { @@ -43,11 +43,18 @@ public function __invoke(ServerRequestInterface $request) /** @internal */ public function call(ServerRequestInterface $request, $position) { + // final request handler will be invoked without a next handler + if (!isset($this->middleware[$position + 1])) { + $handler = $this->middleware[$position]; + return $handler($request); + } + $that = $this; $next = function (ServerRequestInterface $request) use ($that, $position) { return $that->call($request, $position + 1); }; + // invoke middleware request handler with next handler $handler = $this->middleware[$position]; return $handler($request, $next); } diff --git a/tests/Io/MiddlewareRunnerTest.php b/tests/Io/MiddlewareRunnerTest.php index 757749c8..91b927c9 100644 --- a/tests/Io/MiddlewareRunnerTest.php +++ b/tests/Io/MiddlewareRunnerTest.php @@ -31,6 +31,43 @@ public function testEmptyMiddlewareStackThrowsException() $middlewareStack($request); } + public function testMiddlewareHandlerReceivesTwoArguments() + { + $args = null; + $middleware = new MiddlewareRunner(array( + function (ServerRequestInterface $request, $next) use (&$args) { + $args = func_num_args(); + return $next($request); + }, + function (ServerRequestInterface $request) { + return null; + } + )); + + $request = new ServerRequest('GET', 'http://example.com/'); + + $middleware($request); + + $this->assertEquals(2, $args); + } + + public function testFinalHandlerReceivesOneArgument() + { + $args = null; + $middleware = new MiddlewareRunner(array( + function (ServerRequestInterface $request) use (&$args) { + $args = func_num_args(); + return null; + } + )); + + $request = new ServerRequest('GET', 'http://example.com/'); + + $middleware($request); + + $this->assertEquals(1, $args); + } + /** * @expectedException RuntimeException * @expectedExceptionMessage hello @@ -224,7 +261,7 @@ function (ServerRequestInterface $request, $next) use (&$receivedRequests) { $receivedRequests[] = 'middleware2: ' . $request->getUri(); return $next($request); }, - function (ServerRequestInterface $request, $next) use (&$receivedRequests) { + function (ServerRequestInterface $request) use (&$receivedRequests) { $receivedRequests[] = 'middleware3: ' . $request->getUri(); return new \React\Promise\Promise(function () { }); } From ca549797034042bd72363f175bdea86f6af9ceb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Jo=C3=A1n=20Iglesias?= Date: Thu, 4 Jan 2018 22:17:29 +0100 Subject: [PATCH 275/456] Minor code improvements (cherry-picked from #295 by @PabloJoan) --- src/Io/IniUtil.php | 4 ++-- src/Io/MultipartParser.php | 6 +++--- src/Io/ServerRequest.php | 2 +- src/StreamingServer.php | 3 +-- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/Io/IniUtil.php b/src/Io/IniUtil.php index 7430b9f9..d085a823 100644 --- a/src/Io/IniUtil.php +++ b/src/Io/IniUtil.php @@ -16,7 +16,7 @@ final class IniUtil public static function iniSizeToBytes($size) { if (is_numeric($size)) { - return $size; + return (int)$size; } $suffix = strtoupper(substr($size, -1)); @@ -43,6 +43,6 @@ public static function iniSizeToBytes($size) return $strippedSize * 1024 * 1024 * 1024 * 1024; } - return $size; + return (int)$size; } } diff --git a/src/Io/MultipartParser.php b/src/Io/MultipartParser.php index 283e8c2f..dbbfd732 100644 --- a/src/Io/MultipartParser.php +++ b/src/Io/MultipartParser.php @@ -18,14 +18,14 @@ final class MultipartParser { /** - * @var ServerRequestInterface + * @var ServerRequestInterface|null */ - protected $request; + private $request; /** * @var int|null */ - protected $maxFileSize; + private $maxFileSize; /** * ini setting "max_input_vars" diff --git a/src/Io/ServerRequest.php b/src/Io/ServerRequest.php index af6cccda..96c90e75 100644 --- a/src/Io/ServerRequest.php +++ b/src/Io/ServerRequest.php @@ -37,7 +37,7 @@ class ServerRequest extends Request implements ServerRequestInterface * @param array $headers Headers for the message. * @param string|resource|StreamInterface $body Message body. * @param string $protocolVersion HTTP protocol version. - * @param array server-side parameters + * @param array $serverParams server-side parameters * * @throws \InvalidArgumentException for an invalid URI */ diff --git a/src/StreamingServer.php b/src/StreamingServer.php index a47d48a5..1d0e0220 100644 --- a/src/StreamingServer.php +++ b/src/StreamingServer.php @@ -3,7 +3,6 @@ namespace React\Http; use Evenement\EventEmitter; -use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use React\Http\Io\ChunkedDecoder; @@ -171,7 +170,7 @@ public function handleConnection(ConnectionInterface $conn) $parser = new RequestHeaderParser($uriLocal, $uriRemote); $listener = array($parser, 'feed'); - $parser->on('headers', function (RequestInterface $request, $bodyBuffer) use ($conn, $listener, $that) { + $parser->on('headers', function (ServerRequestInterface $request, $bodyBuffer) use ($conn, $listener, $that) { // parsing request completed => stop feeding parser $conn->removeListener('data', $listener); From 80fa528a71c9bbdb7a9e786897a375f65ef32e67 Mon Sep 17 00:00:00 2001 From: Zhuk Sergey Date: Thu, 22 Mar 2018 14:14:55 +0300 Subject: [PATCH 276/456] Documentation fix Remove word `function` from middleware description --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4d17ebe7..36e1c3b8 100644 --- a/README.md +++ b/README.md @@ -860,7 +860,7 @@ $server = new Server(array( Similarly, you can use the result of the `$next` middleware request handler function to modify the outgoing response. -Note that as per the above documentation, the `$next` function may return a +Note that as per the above documentation, the `$next` middleware request handler may return a `ResponseInterface` directly or one wrapped in a promise for deferred resolution. In order to simplify handling both paths, you can simply wrap this in a @@ -880,7 +880,7 @@ $server = new Server(array( )); ``` -Note that the `$next` middleware request handler function may also throw an +Note that the `$next` middleware request handler may also throw an `Exception` (or return a rejected promise) as described above. The previous example does not catch any exceptions and would thus signal an error condition to the `Server`. From bab47153c53e2cb3f07432b2215b66a5c503bf60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 26 Mar 2018 23:16:57 +0200 Subject: [PATCH 277/456] Fix awaiting queued handlers when cancelling a queued handler --- .../LimitConcurrentRequestsMiddleware.php | 28 ++++++-- .../LimitConcurrentRequestsMiddlewareTest.php | 69 +++++++++++++++++++ 2 files changed, 92 insertions(+), 5 deletions(-) diff --git a/src/Middleware/LimitConcurrentRequestsMiddleware.php b/src/Middleware/LimitConcurrentRequestsMiddleware.php index 827ce21d..4831fa2c 100644 --- a/src/Middleware/LimitConcurrentRequestsMiddleware.php +++ b/src/Middleware/LimitConcurrentRequestsMiddleware.php @@ -143,21 +143,39 @@ public function __invoke(ServerRequestInterface $request, $next) $queue[$id] = $deferred; $pending = &$this->pending; - return $this->await($deferred->promise()->then(function () use ($request, $next, $body, &$pending) { + $that = $this; + return $deferred->promise()->then(function () use ($request, $next, $body, &$pending, $that) { // invoke next request handler ++$pending; - $ret = $next($request); + + try { + $response = $next($request); + } catch (\Exception $e) { + $that->processQueue(); + throw $e; + } catch (\Throwable $e) { // @codeCoverageIgnoreStart + // handle Errors just like Exceptions (PHP 7+ only) + $that->processQueue(); + throw $e; // @codeCoverageIgnoreEnd + } // resume readable stream and replay buffered events if ($body instanceof PauseBufferStream) { $body->resumeImplicit(); } - return $ret; - })); + // if the next handler returns a pending promise, we have to + // await its resolution before invoking next queued request + return $that->await(Promise\resolve($response)); + }); } - private function await(PromiseInterface $promise) + /** + * @internal + * @param PromiseInterface $promise + * @return PromiseInterface + */ + public function await(PromiseInterface $promise) { $that = $this; diff --git a/tests/Middleware/LimitConcurrentRequestsMiddlewareTest.php b/tests/Middleware/LimitConcurrentRequestsMiddlewareTest.php index 98fe1b9a..859b82e7 100644 --- a/tests/Middleware/LimitConcurrentRequestsMiddlewareTest.php +++ b/tests/Middleware/LimitConcurrentRequestsMiddlewareTest.php @@ -296,6 +296,31 @@ public function testReceivesNextRequestAfterPreviousHandlerIsSettled() $middleware($request, $this->expectCallableOnceWith($request)); } + public function testReceivesNextRequestWhichThrowsAfterPreviousHandlerIsSettled() + { + $request = new ServerRequest( + 'POST', + 'http://example.com/', + array(), + 'hello' + ); + + $deferred = new Deferred(); + $middleware = new LimitConcurrentRequestsMiddleware(1); + $middleware($request, function () use ($deferred) { + return $deferred->promise(); + }); + + $second = $middleware($request, function () { + throw new \RuntimeException(); + }); + + $this->assertTrue($second instanceof PromiseInterface); + $second->then(null, $this->expectCallableOnce()); + + $deferred->reject(new \RuntimeException()); + } + public function testPendingRequestCanBeCancelledAndForwardsCancellationToInnerPromise() { $request = new ServerRequest( @@ -365,6 +390,50 @@ public function testReceivesNextRequestAfterPreviousHandlerIsCancelled() $middleware($request, $this->expectCallableOnceWith($request)); } + public function testRejectsWhenQueuedPromiseIsCancelled() + { + $request = new ServerRequest( + 'POST', + 'http://example.com/', + array(), + 'hello' + ); + + $deferred = new Deferred(); + $middleware = new LimitConcurrentRequestsMiddleware(1); + $first = $middleware($request, function () use ($deferred) { + return $deferred->promise(); + }); + + $second = $middleware($request, $this->expectCallableNever()); + + $this->assertTrue($second instanceof PromiseInterface); + $second->cancel(); + $second->then(null, $this->expectCallableOnce()); + } + + public function testDoesNotInvokeNextHandlersWhenQueuedPromiseIsCancelled() + { + $request = new ServerRequest( + 'POST', + 'http://example.com/', + array(), + 'hello' + ); + + $deferred = new Deferred(); + $middleware = new LimitConcurrentRequestsMiddleware(1); + $first = $middleware($request, function () use ($deferred) { + return $deferred->promise(); + }); + + $second = $middleware($request, $this->expectCallableNever()); + /* $third = */ $middleware($request, $this->expectCallableNever()); + + $this->assertTrue($second instanceof PromiseInterface); + $second->cancel(); + } + public function testReceivesStreamingBodyChangesInstanceWithCustomBodyButSameDataWhenDequeued() { $stream = new ThroughStream(); From aad8a4855a926ccdb5bdc30b99ebe8fa4cb05716 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Wed, 28 Mar 2018 18:05:09 +0200 Subject: [PATCH 278/456] Documentation for PSR-15 middleware --- README.md | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 36e1c3b8..f7f72e37 100644 --- a/README.md +++ b/README.md @@ -831,7 +831,9 @@ or use a class based approach to ease using existing middleware implementations. While this project does provide the means to *use* middleware implementations, it does not aim to *define* how middleware implementations should look like. We realize that there's a vivid ecosystem of middleware implementations and -ongoing effort to standardize interfaces between these and support this goal. +ongoing effort to standardize interfaces between these with +[PSR-15](https://www.php-fig.org/psr/psr-15/) (HTTP Server Request Handlers) +and support this goal. As such, this project only bundles a few middleware implementations that are required to match PHP's request behavior (see below) and otherwise actively encourages [Third-Party Middleware](#third-party-middleware) implementations. @@ -1135,7 +1137,31 @@ new RequestBodyParserMiddleware(10 * 1024, 100); // 100 files with 10 KiB each #### Third-Party Middleware -A non-exhaustive list of third-party middleware can be found at the [`Middleware`](https://github.com/reactphp/http/wiki/Middleware) wiki page. +While this project does provide the means to *use* middleware implementations +(see above), it does not aim to *define* how middleware implementations should +look like. We realize that there's a vivid ecosystem of middleware +implementations and ongoing effort to standardize interfaces between these with +[PSR-15](https://www.php-fig.org/psr/psr-15/) (HTTP Server Request Handlers) +and support this goal. +As such, this project only bundles a few middleware implementations that are +required to match PHP's request behavior (see above) and otherwise actively +encourages third-party middleware implementations. + +While we would love to support PSR-15 directy in `react/http`, we understand +that this interface does not specifically target async APIs and as such does +not take advantage of promises for [deferred responses](#deferred-response). +The gist of this is that where PSR-15 enforces a `ResponseInterface` return +value, we also accept a `PromiseInterface`. +As such, we suggest using the external +[PSR-15 middleware adapter](https://github.com/friends-of-reactphp/http-middleware-psr15-adapter) +that uses on the fly monkey patching of these return values which makes using +most PSR-15 middleware possible with this package without any changes required. + +Other than that, you can also use the above [middleware definition](#middleware) +to create custom middleware. A non-exhaustive list of third-party middleware can +be found at the [middleware wiki](https://github.com/reactphp/http/wiki/Middleware). +If you build or know a custom middleware, make sure to let the world know and +feel free to add it to this list. ## Install From 9cdef7252f30076992c952e0eff84e66ef9233b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 6 Apr 2018 10:11:39 +0200 Subject: [PATCH 279/456] Prepare v0.8.2 release --- CHANGELOG.md | 17 +++++++++++++++++ README.md | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c4ab06c..0162cc0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +## 0.8.2 (2018-04-06) + +* Fix: Do not pass `$next` handler to final request handler. + (#308 by @clue) + +* Fix: Fix awaiting queued handlers when cancelling a queued handler. + (#313 by @clue) + +* Fix: Fix Server to skip `SERVER_ADDR` params for Unix domain sockets (UDS). + (#307 by @clue) + +* Documentation for PSR-15 middleware and minor documentation improvements. + (#314 by @clue and #297, #298 and #310 by @seregazhuk) + +* Minor code improvements and micro optimizations. + (#301 by @seregazhuk and #305 by @kalessil) + ## 0.8.1 (2018-01-05) * Major request handler performance improvement. Benchmarks suggest number of diff --git a/README.md b/README.md index f7f72e37..ee79c37f 100644 --- a/README.md +++ b/README.md @@ -1171,7 +1171,7 @@ The recommended way to install this library is [through Composer](https://getcom This will install the latest supported version: ```bash -$ composer require react/http:^0.8.1 +$ composer require react/http:^0.8.2 ``` See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. From 07619517939f55d721a8835c8b43a6ff5b8a057b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 29 Dec 2017 23:35:25 +0100 Subject: [PATCH 280/456] Simplify closing response body when connection is already closed --- src/StreamingServer.php | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/StreamingServer.php b/src/StreamingServer.php index 1d0e0220..08228f1d 100644 --- a/src/StreamingServer.php +++ b/src/StreamingServer.php @@ -325,6 +325,13 @@ public function writeError(ConnectionInterface $conn, $code, ServerRequestInterf /** @internal */ public function handleResponse(ConnectionInterface $connection, ServerRequestInterface $request, ResponseInterface $response) { + // return early and close response body if connection is already closed + $body = $response->getBody(); + if (!$connection->isWritable()) { + $body->close(); + return; + } + $response = $response->withProtocolVersion($request->getProtocolVersion()); // assign default "X-Powered-By" header as first for history reasons @@ -348,8 +355,8 @@ public function handleResponse(ConnectionInterface $connection, ServerRequestInt $response = $response->withoutHeader('Date'); } - if (!$response->getBody() instanceof HttpBodyStream) { - $response = $response->withHeader('Content-Length', (string)$response->getBody()->getSize()); + if (!$body instanceof HttpBodyStream) { + $response = $response->withHeader('Content-Length', (string)$body->getSize()); } elseif (!$response->hasHeader('Content-Length') && $request->getProtocolVersion() === '1.1') { // assign chunked transfer-encoding if no 'content-length' is given for HTTP/1.1 responses $response = $response->withHeader('Transfer-Encoding', 'chunked'); @@ -381,7 +388,6 @@ public function handleResponse(ConnectionInterface $connection, ServerRequestInt // 101 (Switching Protocols) response (for Upgrade request) forwards upgraded data through duplex stream // 2xx (Successful) response to CONNECT forwards tunneled application data through duplex stream - $body = $response->getBody(); if (($code === 101 || ($request->getMethod() === 'CONNECT' && $code >= 200 && $code < 300)) && $body instanceof HttpBodyStream && $body->input instanceof WritableStreamInterface) { if ($request->getBody()->isReadable()) { // request is still streaming => wait for request close before forwarding following data from connection @@ -417,11 +423,6 @@ private function handleResponseBody(ResponseInterface $response, ConnectionInter return $connection->end(); } - // close response stream if connection is already closed - if (!$connection->isWritable()) { - return $stream->close(); - } - $connection->write($headers . "\r\n"); if ($stream->isReadable()) { From 918e063a267c334aa5b23543613e8c1485259259 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 29 Dec 2017 17:13:53 +0100 Subject: [PATCH 281/456] Simplify enforcing empty response body --- src/StreamingServer.php | 61 ++++++++++++++++++----------------------- 1 file changed, 27 insertions(+), 34 deletions(-) diff --git a/src/StreamingServer.php b/src/StreamingServer.php index 08228f1d..7e431515 100644 --- a/src/StreamingServer.php +++ b/src/StreamingServer.php @@ -20,7 +20,6 @@ use React\Socket\ServerInterface; use React\Stream\ReadableStreamInterface; use React\Stream\WritableStreamInterface; -use RingCentral\Psr7 as Psr7Implementation; /** * The `Server` class is responsible for handling incoming connections and then @@ -373,12 +372,6 @@ public function handleResponse(ConnectionInterface $connection, ServerRequestInt $response = $response->withoutHeader('Content-Length')->withoutHeader('Transfer-Encoding'); } - // response to HEAD and 1xx, 204 and 304 responses MUST NOT include a body - // exclude status 101 (Switching Protocols) here for Upgrade request handling below - if ($request->getMethod() === 'HEAD' || $code === 100 || ($code > 101 && $code < 200) || $code === 204 || $code === 304) { - $response = $response->withBody(Psr7Implementation\stream_for()); - } - // 101 (Switching Protocols) response uses Connection: upgrade header // persistent connections are currently not supported, so do not use // this for any other replies in order to preserve "Connection: close" @@ -404,11 +397,7 @@ public function handleResponse(ConnectionInterface $connection, ServerRequestInt } } - $this->handleResponseBody($response, $connection); - } - - private function handleResponseBody(ResponseInterface $response, ConnectionInterface $connection) - { + // build HTTP response header by appending status line and header fields $headers = "HTTP/" . $response->getProtocolVersion() . " " . $response->getStatusCode() . " " . $response->getReasonPhrase() . "\r\n"; foreach ($response->getHeaders() as $name => $values) { foreach ($values as $value) { @@ -416,34 +405,38 @@ private function handleResponseBody(ResponseInterface $response, ConnectionInter } } - $stream = $response->getBody(); + // response to HEAD and 1xx, 204 and 304 responses MUST NOT include a body + // exclude status 101 (Switching Protocols) here for Upgrade request handling above + if ($request->getMethod() === 'HEAD' || $code === 100 || ($code > 101 && $code < 200) || $code === 204 || $code === 304) { + $body = ''; + } - if (!$stream instanceof ReadableStreamInterface) { - $connection->write($headers . "\r\n" . $stream); - return $connection->end(); + // this is a non-streaming response body or the body stream already closed? + if (!$body instanceof ReadableStreamInterface || !$body->isReadable()) { + // add final chunk if a streaming body is already closed and uses `Transfer-Encoding: chunked` + if ($body instanceof ReadableStreamInterface && $response->getHeaderLine('Transfer-Encoding') === 'chunked') { + $body = "0\r\n\r\n"; + } + + // end connection after writing response headers and body + $connection->write($headers . "\r\n" . $body); + $connection->end(); + return; } $connection->write($headers . "\r\n"); - if ($stream->isReadable()) { - if ($response->getHeaderLine('Transfer-Encoding') === 'chunked') { - $stream = new ChunkedEncoder($stream); - } + if ($response->getHeaderLine('Transfer-Encoding') === 'chunked') { + $body = new ChunkedEncoder($body); + } - // Close response stream once connection closes. - // Note that this TCP/IP close detection may take some time, - // in particular this may only fire on a later read/write attempt - // because we stop/pause reading from the connection once the - // request has been processed. - $connection->on('close', array($stream, 'close')); - - $stream->pipe($connection); - } else { - if ($response->getHeaderLine('Transfer-Encoding') === 'chunked') { - $connection->write("0\r\n\r\n"); - } + // Close response stream once connection closes. + // Note that this TCP/IP close detection may take some time, + // in particular this may only fire on a later read/write attempt + // because we stop/pause reading from the connection once the + // request has been processed. + $connection->on('close', array($body, 'close')); - $connection->end(); - } + $body->pipe($connection); } } From 3e4597295699011f4b63859529f88201331c69a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 29 Mar 2018 18:28:57 +0200 Subject: [PATCH 282/456] Do not pause connection stream to detect closed connections immediately --- README.md | 3 ++- examples/08-stream-response.php | 14 ++++++++---- src/Io/CloseProtectionStream.php | 21 +++++++++++------- src/StreamingServer.php | 4 +--- tests/FunctionalServerTest.php | 30 ++++++++++---------------- tests/Io/CloseProtectionStreamTest.php | 21 ++++++++++++++---- tests/StreamingServerTest.php | 19 ++++++---------- 7 files changed, 61 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index ee79c37f..55f5b1a7 100644 --- a/README.md +++ b/README.md @@ -396,7 +396,8 @@ successfully, i.e. it was read until its expected end. The `error` event will be emitted in case the request stream contains invalid chunked data or the connection closes before the complete request stream has been received. -The server will automatically `pause()` the connection instead of closing it. +The server will automatically stop reading from the connection and discard all +incoming data instead of closing it. A response message can still be sent (unless the connection is already closed). A `close` event will be emitted after an `error` or `end` event. diff --git a/examples/08-stream-response.php b/examples/08-stream-response.php index 9e3cf1ab..ab0ea0ec 100644 --- a/examples/08-stream-response.php +++ b/examples/08-stream-response.php @@ -12,20 +12,26 @@ // Note how this example still uses `Server` instead of `StreamingServer`. // The `StreamingServer` is only required for streaming *incoming* requests. -$server = new Server($loop,function (ServerRequestInterface $request) use ($loop) { +$server = new Server(function (ServerRequestInterface $request) use ($loop) { if ($request->getMethod() !== 'GET' || $request->getUri()->getPath() !== '/') { return new Response(404); } $stream = new ThroughStream(); + // send some data every once in a while with periodic timer $timer = $loop->addPeriodicTimer(0.5, function () use ($stream) { - $stream->emit('data', array(microtime(true) . PHP_EOL)); + $stream->write(microtime(true) . PHP_EOL); }); - $loop->addTimer(5, function() use ($loop, $timer, $stream) { + // demo for ending stream after a few seconds + $loop->addTimer(5.0, function() use ($stream) { + $stream->end(); + }); + + // stop timer if stream is closed (such as when connection is closed) + $stream->on('close', function () use ($loop, $timer) { $loop->cancelTimer($timer); - $stream->emit('end'); }); return new Response( diff --git a/src/Io/CloseProtectionStream.php b/src/Io/CloseProtectionStream.php index f30ff838..2e1ed6e4 100644 --- a/src/Io/CloseProtectionStream.php +++ b/src/Io/CloseProtectionStream.php @@ -8,7 +8,7 @@ use React\Stream\WritableStreamInterface; /** - * [Internal] Protects a given stream from actually closing and only pauses it instead. + * [Internal] Protects a given stream from actually closing and only discards its incoming data instead. * * This is used internally to prevent the underlying connection from closing, so * that we can still send back a response over the same stream. @@ -19,9 +19,10 @@ class CloseProtectionStream extends EventEmitter implements ReadableStreamInterf { private $input; private $closed = false; + private $paused = false; /** - * @param ReadableStreamInterface $input stream that will be paused instead of closed on an 'close' event. + * @param ReadableStreamInterface $input stream that will be discarded instead of closing it on an 'close' event. */ public function __construct(ReadableStreamInterface $input) { @@ -44,6 +45,7 @@ public function pause() return; } + $this->paused = true; $this->input->pause(); } @@ -53,6 +55,7 @@ public function resume() return; } + $this->paused = false; $this->input->resume(); } @@ -71,16 +74,19 @@ public function close() $this->closed = true; - $this->emit('close'); - - // 'pause' the stream avoids additional traffic transferred by this stream - $this->input->pause(); - + // stop listening for incoming events $this->input->removeListener('data', array($this, 'handleData')); $this->input->removeListener('error', array($this, 'handleError')); $this->input->removeListener('end', array($this, 'handleEnd')); $this->input->removeListener('close', array($this, 'close')); + // resume the stream to ensure we discard everything from incoming connection + if ($this->paused) { + $this->paused = false; + $this->input->resume(); + } + + $this->emit('close'); $this->removeAllListeners(); } @@ -102,5 +108,4 @@ public function handleError(\Exception $e) { $this->emit('error', array($e)); } - } diff --git a/src/StreamingServer.php b/src/StreamingServer.php index 7e431515..498cf98d 100644 --- a/src/StreamingServer.php +++ b/src/StreamingServer.php @@ -432,9 +432,7 @@ public function handleResponse(ConnectionInterface $connection, ServerRequestInt // Close response stream once connection closes. // Note that this TCP/IP close detection may take some time, - // in particular this may only fire on a later read/write attempt - // because we stop/pause reading from the connection once the - // request has been processed. + // in particular this may only fire on a later read/write attempt. $connection->on('close', array($body, 'close')); $body->pipe($connection); diff --git a/tests/FunctionalServerTest.php b/tests/FunctionalServerTest.php index bdccf889..6202a99f 100644 --- a/tests/FunctionalServerTest.php +++ b/tests/FunctionalServerTest.php @@ -5,14 +5,14 @@ use Psr\Http\Message\ServerRequestInterface; use React\Http\Middleware\LimitConcurrentRequestsMiddleware; use React\Http\Middleware\RequestBodyBufferMiddleware; +use React\Http\Response; +use React\Http\StreamingServer; use React\Socket\Server as Socket; use React\EventLoop\Factory; -use React\Http\StreamingServer; use Psr\Http\Message\RequestInterface; use React\Socket\Connector; use React\Socket\ConnectionInterface; use Clue\React\Block; -use React\Http\Response; use React\Socket\SecureServer; use React\Promise; use React\Promise\Stream; @@ -498,7 +498,7 @@ public function testRequestHandlerWillReceiveCloseEventIfConnectionClosesWhileSe $socket->close(); } - public function testStreamFromRequestHandlerWillBeClosedIfConnectionClosesWhileSendingBody() + public function testStreamFromRequestHandlerWillBeClosedIfConnectionClosesWhileSendingRequestBody() { $loop = Factory::create(); $connector = new Connector($loop); @@ -528,13 +528,12 @@ public function testStreamFromRequestHandlerWillBeClosedIfConnectionClosesWhileS $this->assertNull($ret); } - public function testStreamFromRequestHandlerWillBeClosedIfConnectionClosesButWillOnlyBeDetectedOnNextWrite() + public function testStreamFromRequestHandlerWillBeClosedIfConnectionCloses() { $loop = Factory::create(); $connector = new Connector($loop); $stream = new ThroughStream(); - $stream->on('close', $this->expectCallableOnce()); $server = new StreamingServer(function (RequestInterface $request) use ($stream) { return new Response(200, array(), $stream); @@ -543,27 +542,20 @@ public function testStreamFromRequestHandlerWillBeClosedIfConnectionClosesButWil $socket = new Socket(0, $loop); $server->listen($socket); - $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) use ($loop) { + $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) use ($loop) { $conn->write("GET / HTTP/1.0\r\n\r\n"); - $loop->addTimer(0.1, function() use ($conn) { - $conn->end(); + $loop->addTimer(0.1, function () use ($conn) { + $conn->close(); }); - - return Stream\buffer($conn); }); - $response = Block\await($result, $loop, 1.0); - - $stream->write('nope'); - Block\sleep(0.1, $loop); - $stream->write('nope'); - Block\sleep(0.1, $loop); - - $this->assertStringStartsWith("HTTP/1.0 200 OK", $response); - $this->assertStringEndsWith("\r\n\r\n", $response); + // await response stream to be closed + $ret = Block\await(Stream\first($stream, 'close'), $loop, 1.0); $socket->close(); + + $this->assertNull($ret); } public function testUpgradeWithThroughStreamReturnsDataAsGiven() diff --git a/tests/Io/CloseProtectionStreamTest.php b/tests/Io/CloseProtectionStreamTest.php index 9f3cfb3f..8490daff 100644 --- a/tests/Io/CloseProtectionStreamTest.php +++ b/tests/Io/CloseProtectionStreamTest.php @@ -8,10 +8,11 @@ class CloseProtectionStreamTest extends TestCase { - public function testClosePausesTheInputStreamInsteadOfClosing() + public function testCloseDoesNotCloseTheInputStream() { $input = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->disableOriginalConstructor()->getMock(); - $input->expects($this->once())->method('pause'); + $input->expects($this->never())->method('pause'); + $input->expects($this->never())->method('resume'); $input->expects($this->never())->method('close'); $protection = new CloseProtectionStream($input); @@ -43,6 +44,17 @@ public function testResumeStreamWillResumeInputStream() $protection->resume(); } + public function testCloseResumesInputStreamIfItWasPreviouslyPaused() + { + $input = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $input->expects($this->once())->method('pause'); + $input->expects($this->once())->method('resume'); + + $protection = new CloseProtectionStream($input); + $protection->pause(); + $protection->close(); + } + public function testInputStreamIsNotReadableAfterClose() { $input = new ThroughStream(); @@ -121,7 +133,8 @@ public function testEndWontBeEmittedAfterClose() public function testPauseAfterCloseHasNoEffect() { $input = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); - $input->expects($this->once())->method('pause'); + $input->expects($this->never())->method('pause'); + $input->expects($this->never())->method('resume'); $protection = new CloseProtectionStream($input); $protection->on('data', $this->expectCallableNever()); @@ -134,7 +147,7 @@ public function testPauseAfterCloseHasNoEffect() public function testResumeAfterCloseHasNoEffect() { $input = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); - $input->expects($this->once())->method('pause'); + $input->expects($this->never())->method('pause'); $input->expects($this->never())->method('resume'); $protection = new CloseProtectionStream($input); diff --git a/tests/StreamingServerTest.php b/tests/StreamingServerTest.php index 6f4bd1dc..bf24a4e6 100644 --- a/tests/StreamingServerTest.php +++ b/tests/StreamingServerTest.php @@ -497,7 +497,7 @@ public function testRequestOptionsAbsoluteEvent() $this->assertSame('example.com', $requestAssertion->getHeaderLine('Host')); } - public function testRequestPauseWillbeForwardedToConnection() + public function testRequestPauseWillBeForwardedToConnection() { $server = new StreamingServer(function (ServerRequestInterface $request) { $request->getBody()->pause(); @@ -517,7 +517,7 @@ public function testRequestPauseWillbeForwardedToConnection() $this->connection->emit('data', array($data)); } - public function testRequestResumeWillbeForwardedToConnection() + public function testRequestResumeWillBeForwardedToConnection() { $server = new StreamingServer(function (ServerRequestInterface $request) { $request->getBody()->resume(); @@ -532,13 +532,13 @@ public function testRequestResumeWillbeForwardedToConnection() $this->connection->emit('data', array($data)); } - public function testRequestCloseWillPauseConnection() + public function testRequestCloseWillNotCloseConnection() { $server = new StreamingServer(function (ServerRequestInterface $request) { $request->getBody()->close(); }); - $this->connection->expects($this->once())->method('pause'); + $this->connection->expects($this->never())->method('close'); $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); @@ -554,7 +554,8 @@ public function testRequestPauseAfterCloseWillNotBeForwarded() $request->getBody()->pause(); }); - $this->connection->expects($this->once())->method('pause'); + $this->connection->expects($this->never())->method('close'); + $this->connection->expects($this->never())->method('pause'); $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); @@ -570,7 +571,7 @@ public function testRequestResumeAfterCloseWillNotBeForwarded() $request->getBody()->resume(); }); - $this->connection->expects($this->once())->method('pause'); + $this->connection->expects($this->never())->method('close'); $this->connection->expects($this->never())->method('resume'); $server->listen($this->socket); @@ -1964,7 +1965,6 @@ public function testRequestInvalidChunkHeaderTooLongWillEmitErrorOnRequestStream }); $this->connection->expects($this->never())->method('close'); - $this->connection->expects($this->once())->method('pause'); $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); @@ -1989,7 +1989,6 @@ public function testRequestInvalidChunkBodyTooLongWillEmitErrorOnRequestStream() }); $this->connection->expects($this->never())->method('close'); - $this->connection->expects($this->once())->method('pause'); $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); @@ -2012,7 +2011,6 @@ public function testRequestUnexpectedEndOfRequestWithChunkedTransferConnectionWi }); $this->connection->expects($this->never())->method('close'); - $this->connection->expects($this->once())->method('pause'); $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); @@ -2036,7 +2034,6 @@ public function testRequestInvalidChunkHeaderWillEmitErrorOnRequestStream() }); $this->connection->expects($this->never())->method('close'); - $this->connection->expects($this->once())->method('pause'); $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); @@ -2059,7 +2056,6 @@ public function testRequestUnexpectedEndOfRequestWithContentLengthWillEmitErrorO }); $this->connection->expects($this->never())->method('close'); - $this->connection->expects($this->once())->method('pause'); $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); @@ -2089,7 +2085,6 @@ public function testRequestWithoutBodyWillEmitEndOnRequestStream() $request->getBody()->on('error', $errorEvent); }); - $this->connection->expects($this->once())->method('pause'); $this->connection->expects($this->never())->method('close'); $server->listen($this->socket); From 885618fa6c47b58b0ef66148311194e27aab7122 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 6 Apr 2018 15:41:24 +0200 Subject: [PATCH 283/456] Keep incoming `Transfer-Encoding: chunked` request header --- README.md | 17 +++++++++-------- src/StreamingServer.php | 2 -- tests/StreamingServerTest.php | 11 +++++++---- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 55f5b1a7..2ddd5c11 100644 --- a/README.md +++ b/README.md @@ -386,16 +386,15 @@ See also [example #9](examples) for more details. The `data` event will be emitted whenever new data is available on the request body stream. -The server automatically takes care of decoding chunked transfer encoding -and will only emit the actual payload as data. -In this case, the `Transfer-Encoding` header will be removed. +The server also automatically takes care of decoding any incoming requests using +`Transfer-Encoding: chunked` and will only emit the actual payload as data. The `end` event will be emitted when the request body stream terminates successfully, i.e. it was read until its expected end. The `error` event will be emitted in case the request stream contains invalid -chunked data or the connection closes before the complete request stream has -been received. +data for `Transfer-Encoding: chunked` or when the connection closes before +the complete request stream has been received. The server will automatically stop reading from the connection and discard all incoming data instead of closing it. A response message can still be sent (unless the connection is already closed). @@ -678,12 +677,14 @@ in this case (if applicable). If the response body is a `string`, a `Content-Length` header will be added automatically. + If the response body is a ReactPHP `ReadableStreamInterface` and you do not -specify a `Content-Length` header, HTTP/1.1 responses will automatically use -chunked transfer encoding and send the respective header -(`Transfer-Encoding: chunked`) automatically. +specify a `Content-Length` header, outgoing HTTP/1.1 response messages will +automatically use `Transfer-Encoding: chunked` and send the respective header +automatically. The server is responsible for handling `Transfer-Encoding`, so you SHOULD NOT pass this header yourself. + If you know the length of your stream body, you MAY specify it like this instead: ```php diff --git a/src/StreamingServer.php b/src/StreamingServer.php index 498cf98d..d30e0daf 100644 --- a/src/StreamingServer.php +++ b/src/StreamingServer.php @@ -204,8 +204,6 @@ public function handleRequest(ConnectionInterface $conn, ServerRequestInterface } $stream = new ChunkedDecoder($stream); - - $request = $request->withoutHeader('Transfer-Encoding'); $request = $request->withoutHeader('Content-Length'); $contentLength = null; diff --git a/tests/StreamingServerTest.php b/tests/StreamingServerTest.php index bf24a4e6..6c3a21b6 100644 --- a/tests/StreamingServerTest.php +++ b/tests/StreamingServerTest.php @@ -1444,7 +1444,7 @@ public function testRequestChunkedTransferEncodingRequestWillEmitDecodedDataEven $this->connection->emit('data', array($data)); - $this->assertFalse($requestValidation->hasHeader('Transfer-Encoding')); + $this->assertEquals('chunked', $requestValidation->getHeaderLine('Transfer-Encoding')); } public function testRequestChunkedTransferEncodingWithAdditionalDataWontBeEmitted() @@ -1509,12 +1509,14 @@ public function testRequestChunkedTransferEncodingHeaderCanBeUpperCase() $endEvent = $this->expectCallableOnce(); $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); + $requestValidation = null; - $server = new StreamingServer(function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new StreamingServer(function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent, &$requestValidation) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); $request->getBody()->on('error', $errorEvent); + $requestValidation = $request; }); $server->listen($this->socket); @@ -1529,6 +1531,7 @@ public function testRequestChunkedTransferEncodingHeaderCanBeUpperCase() $data .= "0\r\n\r\n"; $this->connection->emit('data', array($data)); + $this->assertEquals('CHUNKED', $requestValidation->getHeaderLine('Transfer-Encoding')); } public function testRequestChunkedTransferEncodingCanBeMixedUpperAndLowerCase() @@ -1804,7 +1807,7 @@ public function testRequestContentLengthWillBeIgnoredIfTransferEncodingIsSet() $this->connection->emit('data', array($data)); $this->assertFalse($requestValidation->hasHeader('Content-Length')); - $this->assertFalse($requestValidation->hasHeader('Transfer-Encoding')); + $this->assertEquals('chunked', $requestValidation->getHeaderLine('Transfer-Encoding')); } public function testRequestInvalidContentLengthWillBeIgnoreddIfTransferEncodingIsSet() @@ -1842,7 +1845,7 @@ public function testRequestInvalidContentLengthWillBeIgnoreddIfTransferEncodingI $this->connection->emit('data', array($data)); $this->assertFalse($requestValidation->hasHeader('Content-Length')); - $this->assertFalse($requestValidation->hasHeader('Transfer-Encoding')); + $this->assertEquals('chunked', $requestValidation->getHeaderLine('Transfer-Encoding')); } public function testRequestInvalidNonIntegerContentLengthWillEmitServerErrorAndSendResponse() From 59b4715f0a1abbf8e0a941d3713aee05b40c3711 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 6 Apr 2018 15:37:23 +0200 Subject: [PATCH 284/456] Reject requests that contain both Content-Length and Transfer-Encoding --- README.md | 4 +- src/StreamingServer.php | 9 +++- tests/StreamingServerTest.php | 79 ++++++++++------------------------- 3 files changed, 30 insertions(+), 62 deletions(-) diff --git a/README.md b/README.md index 2ddd5c11..303f99db 100644 --- a/README.md +++ b/README.md @@ -411,14 +411,14 @@ message boundaries. This value may be `0` if the request message does not contain a request body (such as a simple `GET` request). Note that this value may be `null` if the request body size is unknown in -advance because the request message uses chunked transfer encoding. +advance because the request message uses `Transfer-Encoding: chunked`. ```php $server = new StreamingServer(function (ServerRequestInterface $request) { $size = $request->getBody()->getSize(); if ($size === null) { $body = 'The request does not contain an explicit length.'; - $body .= 'This server does not accept chunked transfer encoding.'; + $body .= 'This example does not accept chunked transfer encoding.'; return new Response( 411, diff --git a/src/StreamingServer.php b/src/StreamingServer.php index d30e0daf..3084e328 100644 --- a/src/StreamingServer.php +++ b/src/StreamingServer.php @@ -203,9 +203,14 @@ public function handleRequest(ConnectionInterface $conn, ServerRequestInterface return $this->writeError($conn, 501, $request); } - $stream = new ChunkedDecoder($stream); - $request = $request->withoutHeader('Content-Length'); + // 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'); diff --git a/tests/StreamingServerTest.php b/tests/StreamingServerTest.php index 6c3a21b6..22749e17 100644 --- a/tests/StreamingServerTest.php +++ b/tests/StreamingServerTest.php @@ -1773,58 +1773,25 @@ public function testRequestZeroContentLengthWillEmitEndAndAdditionalDataWillBeIg $this->connection->emit('data', array($data)); } - public function testRequestContentLengthWillBeIgnoredIfTransferEncodingIsSet() + public function testRequestWithBothContentLengthAndTransferEncodingWillEmitServerErrorAndSendResponse() { - $dataEvent = $this->expectCallableOnceWith('hello'); - $endEvent = $this->expectCallableOnce(); - $closeEvent = $this->expectCallableOnce(); - $errorEvent = $this->expectCallableNever(); - - $requestValidation = null; - $server = new StreamingServer(function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent, &$requestValidation) { - $request->getBody()->on('data', $dataEvent); - $request->getBody()->on('end', $endEvent); - $request->getBody()->on('close', $closeEvent); - $request->getBody()->on('error', $errorEvent); - $requestValidation = $request; + $error = null; + $server = new StreamingServer($this->expectCallableNever()); + $server->on('error', function ($message) use (&$error) { + $error = $message; }); - $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: 4\r\n"; - $data .= "Transfer-Encoding: chunked\r\n"; - $data .= "\r\n"; - - $this->connection->emit('data', array($data)); - - $data = "5\r\nhello\r\n"; - $data .= "0\r\n\r\n"; - - $this->connection->emit('data', array($data)); - - $this->assertFalse($requestValidation->hasHeader('Content-Length')); - $this->assertEquals('chunked', $requestValidation->getHeaderLine('Transfer-Encoding')); - } - - public function testRequestInvalidContentLengthWillBeIgnoreddIfTransferEncodingIsSet() - { - $dataEvent = $this->expectCallableOnceWith('hello'); - $endEvent = $this->expectCallableOnce(); - $closeEvent = $this->expectCallableOnce(); - $errorEvent = $this->expectCallableNever(); - - $requestValidation = null; - $server = new StreamingServer(function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent, &$requestValidation) { - $request->getBody()->on('data', $dataEvent); - $request->getBody()->on('end', $endEvent); - $request->getBody()->on('close', $closeEvent); - $request->getBody()->on('error', $errorEvent); - $requestValidation = $request; - }); + $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)); @@ -1832,20 +1799,16 @@ public function testRequestInvalidContentLengthWillBeIgnoreddIfTransferEncodingI $data = "GET / HTTP/1.1\r\n"; $data .= "Host: example.com:80\r\n"; $data .= "Connection: close\r\n"; - // this is valid behavior according to: https://www.ietf.org/rfc/rfc2616.txt chapter 4.4 - $data .= "Content-Length: hello world\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)); - $data = "5\r\nhello\r\n"; - $data .= "0\r\n\r\n"; - - $this->connection->emit('data', array($data)); - - $this->assertFalse($requestValidation->hasHeader('Content-Length')); - $this->assertEquals('chunked', $requestValidation->getHeaderLine('Transfer-Encoding')); + $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() From f8bcdab2dc0ecd94f35ff9657a263028b96f0c46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Wed, 11 Apr 2018 17:03:27 +0200 Subject: [PATCH 285/456] Prepare v0.8.3 release --- CHANGELOG.md | 14 ++++++++++++++ README.md | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0162cc0d..748387a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## 0.8.3 (2018-04-11) + +* Feature: Do not pause connection stream to detect closed connections immediately. + (#315 by @clue) + +* Feature: Keep incoming `Transfer-Encoding: chunked` request header. + (#316 by @clue) + +* Feature: Reject invalid requests that contain both `Content-Length` and `Transfer-Encoding` request headers. + (#318 by @clue) + +* Minor internal refactoring to simplify connection close logic after sending response. + (#317 by @clue) + ## 0.8.2 (2018-04-06) * Fix: Do not pass `$next` handler to final request handler. diff --git a/README.md b/README.md index 303f99db..c8107242 100644 --- a/README.md +++ b/README.md @@ -1173,7 +1173,7 @@ The recommended way to install this library is [through Composer](https://getcom This will install the latest supported version: ```bash -$ composer require react/http:^0.8.2 +$ composer require react/http:^0.8.3 ``` See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. From 5ea0858692f32b8615f244358eaf367db68939de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 21 Jul 2018 11:39:00 +0200 Subject: [PATCH 286/456] Simplify sending error response for invalid requests --- src/StreamingServer.php | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/StreamingServer.php b/src/StreamingServer.php index 3084e328..1ce65033 100644 --- a/src/StreamingServer.php +++ b/src/StreamingServer.php @@ -185,9 +185,11 @@ public function handleConnection(ConnectionInterface $conn) $conn->removeListener('data', $listener); $that->emit('error', array($e)); + // parsing failed => assume dummy request and send appropriate error $that->writeError( $conn, - $e->getCode() !== 0 ? $e->getCode() : 400 + $e->getCode() !== 0 ? $e->getCode() : 400, + new ServerRequest('GET', '/') ); }); } @@ -298,7 +300,7 @@ function ($error) use ($that, $conn, $request) { } /** @internal */ - public function writeError(ConnectionInterface $conn, $code, ServerRequestInterface $request = null) + public function writeError(ConnectionInterface $conn, $code, ServerRequestInterface $request) { $response = new Response( $code, @@ -316,10 +318,6 @@ public function writeError(ConnectionInterface $conn, $code, ServerRequestInterf $body->write(': ' . $reason); } - if ($request === null) { - $request = new ServerRequest('GET', '/', array(), null, '1.1'); - } - $this->handleResponse($conn, $request, $response); } From f5f3cc0cd55727c686521673033da98ae368e7c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 22 Jul 2018 14:11:32 +0200 Subject: [PATCH 287/456] Simplify assigning response headers by avoiding duplicate assignments --- src/StreamingServer.php | 70 ++++++++++++++++++++++------------------- 1 file changed, 37 insertions(+), 33 deletions(-) diff --git a/src/StreamingServer.php b/src/StreamingServer.php index 1ce65033..6abbd86c 100644 --- a/src/StreamingServer.php +++ b/src/StreamingServer.php @@ -332,57 +332,61 @@ public function handleResponse(ConnectionInterface $connection, ServerRequestInt return; } - $response = $response->withProtocolVersion($request->getProtocolVersion()); + $code = $response->getStatusCode(); + $method = $request->getMethod(); + + // assign HTTP protocol version from request automatically + $version = $request->getProtocolVersion(); + $response = $response->withProtocolVersion($version); - // assign default "X-Powered-By" header as first for history reasons + // assign default "X-Powered-By" header automatically if (!$response->hasHeader('X-Powered-By')) { $response = $response->withHeader('X-Powered-By', 'React/alpha'); - } - - if ($response->hasHeader('X-Powered-By') && $response->getHeaderLine('X-Powered-By') === ''){ + } elseif ($response->getHeaderLine('X-Powered-By') === ''){ $response = $response->withoutHeader('X-Powered-By'); } - $response = $response->withoutHeader('Transfer-Encoding'); - - // assign date header if no 'date' is given, use the current time where this code is running + // assign default "Date" header from current time automatically if (!$response->hasHeader('Date')) { // IMF-fixdate = day-name "," SP date1 SP time-of-day SP GMT $response = $response->withHeader('Date', gmdate('D, d M Y H:i:s') . ' GMT'); - } - - if ($response->hasHeader('Date') && $response->getHeaderLine('Date') === ''){ + } elseif ($response->getHeaderLine('Date') === ''){ $response = $response->withoutHeader('Date'); } - if (!$body instanceof HttpBodyStream) { - $response = $response->withHeader('Content-Length', (string)$body->getSize()); - } elseif (!$response->hasHeader('Content-Length') && $request->getProtocolVersion() === '1.1') { + // assign "Content-Length" and "Transfer-Encoding" headers automatically + $chunked = false; + if (($method === 'CONNECT' && $code >= 200 && $code < 300) || ($code >= 100 && $code < 200) || $code === 204) { + // 2xx response to CONNECT and 1xx and 204 MUST NOT include Content-Length or Transfer-Encoding header + $response = $response->withoutHeader('Content-Length')->withoutHeader('Transfer-Encoding'); + } elseif (!$body instanceof HttpBodyStream) { + // assign Content-Length header when using a "normal" buffered body string + $response = $response->withHeader('Content-Length', (string)$body->getSize())->withoutHeader('Transfer-Encoding'); + } elseif (!$response->hasHeader('Content-Length') && $version === '1.1') { // assign chunked transfer-encoding if no 'content-length' is given for HTTP/1.1 responses $response = $response->withHeader('Transfer-Encoding', 'chunked'); + $chunked = true; + } else { + // remove any Transfer-Encoding headers unless automatically enabled above + $response = $response->withoutHeader('Transfer-Encoding'); } - // HTTP/1.1 assumes persistent connection support by default - // we do not support persistent connections, so let the client know - if ($request->getProtocolVersion() === '1.1') { - $response = $response->withHeader('Connection', 'close'); - } - // 2xx response to CONNECT and 1xx and 204 MUST NOT include Content-Length or Transfer-Encoding header - $code = $response->getStatusCode(); - if (($request->getMethod() === 'CONNECT' && $code >= 200 && $code < 300) || ($code >= 100 && $code < 200) || $code === 204) { - $response = $response->withoutHeader('Content-Length')->withoutHeader('Transfer-Encoding'); - } - - // 101 (Switching Protocols) response uses Connection: upgrade header - // persistent connections are currently not supported, so do not use - // this for any other replies in order to preserve "Connection: close" + // assign "Connection" header automatically if ($code === 101) { + // 101 (Switching Protocols) response uses Connection: upgrade header $response = $response->withHeader('Connection', 'upgrade'); + } elseif ($version === '1.1') { + // HTTP/1.1 assumes persistent connection support by default + // we do not support persistent connections, so let the client know + $response = $response->withHeader('Connection', 'close'); + } else { + // remove any Connection headers unless automatically enabled above + $response = $response->withoutHeader('Connection'); } // 101 (Switching Protocols) response (for Upgrade request) forwards upgraded data through duplex stream // 2xx (Successful) response to CONNECT forwards tunneled application data through duplex stream - if (($code === 101 || ($request->getMethod() === 'CONNECT' && $code >= 200 && $code < 300)) && $body instanceof HttpBodyStream && $body->input instanceof WritableStreamInterface) { + if (($code === 101 || ($method === 'CONNECT' && $code >= 200 && $code < 300)) && $body instanceof HttpBodyStream && $body->input instanceof WritableStreamInterface) { if ($request->getBody()->isReadable()) { // request is still streaming => wait for request close before forwarding following data from connection $request->getBody()->on('close', function () use ($connection, $body) { @@ -399,7 +403,7 @@ public function handleResponse(ConnectionInterface $connection, ServerRequestInt } // build HTTP response header by appending status line and header fields - $headers = "HTTP/" . $response->getProtocolVersion() . " " . $response->getStatusCode() . " " . $response->getReasonPhrase() . "\r\n"; + $headers = "HTTP/" . $version . " " . $code . " " . $response->getReasonPhrase() . "\r\n"; foreach ($response->getHeaders() as $name => $values) { foreach ($values as $value) { $headers .= $name . ": " . $value . "\r\n"; @@ -408,14 +412,14 @@ public function handleResponse(ConnectionInterface $connection, ServerRequestInt // response to HEAD and 1xx, 204 and 304 responses MUST NOT include a body // exclude status 101 (Switching Protocols) here for Upgrade request handling above - if ($request->getMethod() === 'HEAD' || $code === 100 || ($code > 101 && $code < 200) || $code === 204 || $code === 304) { + if ($method === 'HEAD' || $code === 100 || ($code > 101 && $code < 200) || $code === 204 || $code === 304) { $body = ''; } // this is a non-streaming response body or the body stream already closed? if (!$body instanceof ReadableStreamInterface || !$body->isReadable()) { // add final chunk if a streaming body is already closed and uses `Transfer-Encoding: chunked` - if ($body instanceof ReadableStreamInterface && $response->getHeaderLine('Transfer-Encoding') === 'chunked') { + if ($body instanceof ReadableStreamInterface && $chunked) { $body = "0\r\n\r\n"; } @@ -427,7 +431,7 @@ public function handleResponse(ConnectionInterface $connection, ServerRequestInt $connection->write($headers . "\r\n"); - if ($response->getHeaderLine('Transfer-Encoding') === 'chunked') { + if ($chunked) { $body = new ChunkedEncoder($body); } From b66ee6aed22020bece6f0588851b63e05345537b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 12 Nov 2018 12:25:28 +0100 Subject: [PATCH 288/456] Assign Content-Length response header automatically only when size known --- README.md | 44 ++++++++++++++------- src/StreamingServer.php | 2 +- tests/StreamingServerTest.php | 74 +++++++++++++++++++++++++++++++++++ 3 files changed, 105 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index c8107242..dd255866 100644 --- a/README.md +++ b/README.md @@ -593,12 +593,12 @@ $server = new Server(function (ServerRequestInterface $request) use ($loop) { $stream = new ThroughStream(); $timer = $loop->addPeriodicTimer(0.5, function () use ($stream) { - $stream->emit('data', array(microtime(true) . PHP_EOL)); + $stream->write(microtime(true) . PHP_EOL); }); $loop->addTimer(5, function() use ($loop, $timer, $stream) { $loop->cancelTimer($timer); - $stream->emit('end'); + $stream->end(); }); return new Response( @@ -675,25 +675,41 @@ in this case (if applicable). #### Response length -If the response body is a `string`, a `Content-Length` header will be added -automatically. +If the response body size is known, a `Content-Length` response header will be +added automatically. This is the most common use case, for example when using +a `string` response body like this: -If the response body is a ReactPHP `ReadableStreamInterface` and you do not -specify a `Content-Length` header, outgoing HTTP/1.1 response messages will -automatically use `Transfer-Encoding: chunked` and send the respective header -automatically. -The server is responsible for handling `Transfer-Encoding`, so you SHOULD NOT -pass this header yourself. +```php +$server = new Server(function (ServerRequestInterface $request) { + return new Response( + 200, + array( + 'Content-Type' => 'text/plain' + ), + "Hello World!\n" + ); +}); +``` -If you know the length of your stream body, you MAY specify it like this instead: +If the response body size is unknown, a `Content-Length` response header can not +be added automatically. When using a [streaming response](#streaming-response) +without an explicit `Content-Length` response header, outgoing HTTP/1.1 response +messages will automatically use `Transfer-Encoding: chunked` while legacy HTTP/1.0 +response messages will contain the plain response body. If you know the length +of your streaming response body, you MAY want to specify it explicitly like this: ```php -$stream = new ThroughStream(); -$server = new Server(function (ServerRequestInterface $request) use ($stream) { +$server = new Server(function (ServerRequestInterface $request) use ($loop) { + $stream = new ThroughStream(); + + $loop->addTimer(2.0, function () use ($stream) { + $stream->end("Hello World!\n"); + }); + return new Response( 200, array( - 'Content-Length' => '5', + 'Content-Length' => '13', 'Content-Type' => 'text/plain', ), $stream diff --git a/src/StreamingServer.php b/src/StreamingServer.php index 6abbd86c..ea85860c 100644 --- a/src/StreamingServer.php +++ b/src/StreamingServer.php @@ -359,7 +359,7 @@ public function handleResponse(ConnectionInterface $connection, ServerRequestInt if (($method === 'CONNECT' && $code >= 200 && $code < 300) || ($code >= 100 && $code < 200) || $code === 204) { // 2xx response to CONNECT and 1xx and 204 MUST NOT include Content-Length or Transfer-Encoding header $response = $response->withoutHeader('Content-Length')->withoutHeader('Transfer-Encoding'); - } elseif (!$body instanceof HttpBodyStream) { + } elseif ($body->getSize() !== null) { // assign Content-Length header when using a "normal" buffered body string $response = $response->withHeader('Content-Length', (string)$body->getSize())->withoutHeader('Transfer-Encoding'); } elseif (!$response->hasHeader('Content-Length') && $version === '1.1') { diff --git a/tests/StreamingServerTest.php b/tests/StreamingServerTest.php index 22749e17..a7b40029 100644 --- a/tests/StreamingServerTest.php +++ b/tests/StreamingServerTest.php @@ -2156,6 +2156,80 @@ function ($data) use (&$buffer) { $this->assertContains("hello", $buffer); } + public function testResponseContainsResponseBodyWithTransferEncodingChunkedForBodyWithUnknownSize() + { + $body = $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(); + $body->expects($this->once())->method('getSize')->willReturn(null); + $body->expects($this->once())->method('__toString')->willReturn('body'); + + $server = new StreamingServer(function (ServerRequestInterface $request) use ($body) { + return new Response( + 200, + array(), + $body + ); + }); + + $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\r\n\r\n"; + $this->connection->emit('data', array($data)); + + $this->assertContains("Transfer-Encoding: chunked", $buffer); + $this->assertNotContains("Content-Length:", $buffer); + $this->assertContains("body", $buffer); + } + + public function testResponseContainsResponseBodyWithPlainBodyWithUnknownSizeForLegacyHttp10() + { + $body = $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(); + $body->expects($this->once())->method('getSize')->willReturn(null); + $body->expects($this->once())->method('__toString')->willReturn('body'); + + $server = new StreamingServer(function (ServerRequestInterface $request) use ($body) { + return new Response( + 200, + array(), + $body + ); + }); + + $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\nHost: localhost\r\n\r\n"; + $this->connection->emit('data', array($data)); + + $this->assertNotContains("Transfer-Encoding: chunked", $buffer); + $this->assertNotContains("Content-Length:", $buffer); + $this->assertContains("body", $buffer); + } + public function testResponseWithCustomTransferEncodingWillBeIgnoredAndUseChunkedTransferEncodingInstead() { $stream = new ThroughStream(); From 91e020042ced99dca49fa9a806d5baaa20f2aabb Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Tue, 13 Nov 2018 08:05:26 +0100 Subject: [PATCH 289/456] Improve performance by prefixing all global functions calls with \ to skip the look up and resolve process and go straight to the global function --- src/Io/ChunkedDecoder.php | 30 +++++----- src/Io/ChunkedEncoder.php | 2 +- src/Io/IniUtil.php | 8 +-- src/Io/LengthLimitedStream.php | 6 +- src/Io/MiddlewareRunner.php | 2 +- src/Io/MultipartParser.php | 60 +++++++++---------- src/Io/RequestHeaderParser.php | 40 ++++++------- src/Io/ServerRequest.php | 16 ++--- src/Io/UploadedFile.php | 20 +++---- .../LimitConcurrentRequestsMiddleware.php | 6 +- .../RequestBodyBufferMiddleware.php | 4 +- .../RequestBodyParserMiddleware.php | 6 +- src/Server.php | 14 ++--- src/StreamingServer.php | 14 ++--- 14 files changed, 114 insertions(+), 114 deletions(-) diff --git a/src/Io/ChunkedDecoder.php b/src/Io/ChunkedDecoder.php index 9b0c089f..4625349b 100644 --- a/src/Io/ChunkedDecoder.php +++ b/src/Io/ChunkedDecoder.php @@ -97,7 +97,7 @@ public function handleData($data) while ($this->buffer !== '') { if (!$this->headerCompleted) { - $positionCrlf = strpos($this->buffer, static::CRLF); + $positionCrlf = \strpos($this->buffer, static::CRLF); if ($positionCrlf === false) { // Header shouldn't be bigger than 1024 bytes @@ -107,43 +107,43 @@ public function handleData($data) return; } - $header = strtolower((string)substr($this->buffer, 0, $positionCrlf)); + $header = \strtolower((string)\substr($this->buffer, 0, $positionCrlf)); $hexValue = $header; - if (strpos($header, ';') !== false) { - $array = explode(';', $header); + if (\strpos($header, ';') !== false) { + $array = \explode(';', $header); $hexValue = $array[0]; } if ($hexValue !== '') { - $hexValue = ltrim($hexValue, "0"); + $hexValue = \ltrim($hexValue, "0"); if ($hexValue === '') { $hexValue = "0"; } } - $this->chunkSize = hexdec($hexValue); - if (dechex($this->chunkSize) !== $hexValue) { + $this->chunkSize = \hexdec($hexValue); + if (\dechex($this->chunkSize) !== $hexValue) { $this->handleError(new Exception($hexValue . ' is not a valid hexadecimal number')); return; } - $this->buffer = (string)substr($this->buffer, $positionCrlf + 2); + $this->buffer = (string)\substr($this->buffer, $positionCrlf + 2); $this->headerCompleted = true; if ($this->buffer === '') { return; } } - $chunk = (string)substr($this->buffer, 0, $this->chunkSize - $this->transferredSize); + $chunk = (string)\substr($this->buffer, 0, $this->chunkSize - $this->transferredSize); if ($chunk !== '') { - $this->transferredSize += strlen($chunk); + $this->transferredSize += \strlen($chunk); $this->emit('data', array($chunk)); - $this->buffer = (string)substr($this->buffer, strlen($chunk)); + $this->buffer = (string)\substr($this->buffer, \strlen($chunk)); } - $positionCrlf = strpos($this->buffer, static::CRLF); + $positionCrlf = \strpos($this->buffer, static::CRLF); if ($positionCrlf === 0) { if ($this->chunkSize === 0) { @@ -154,16 +154,16 @@ public function handleData($data) $this->chunkSize = 0; $this->headerCompleted = false; $this->transferredSize = 0; - $this->buffer = (string)substr($this->buffer, 2); + $this->buffer = (string)\substr($this->buffer, 2); } - if ($positionCrlf !== 0 && $this->chunkSize === $this->transferredSize && strlen($this->buffer) > 2) { + if ($positionCrlf !== 0 && $this->chunkSize === $this->transferredSize && \strlen($this->buffer) > 2) { // the first 2 characters are not CLRF, send error event $this->handleError(new Exception('Chunk does not end with a CLRF')); return; } - if ($positionCrlf !== 0 && strlen($this->buffer) < 2) { + if ($positionCrlf !== 0 && \strlen($this->buffer) < 2) { // No CLRF found, wait for additional data which could be a CLRF return; } diff --git a/src/Io/ChunkedEncoder.php b/src/Io/ChunkedEncoder.php index 8a7f8cfb..d4e53b91 100644 --- a/src/Io/ChunkedEncoder.php +++ b/src/Io/ChunkedEncoder.php @@ -101,7 +101,7 @@ public function handleEnd() */ private function createChunk($data) { - $byteSize = dechex(strlen($data)); + $byteSize = \dechex(\strlen($data)); $chunkBeginning = $byteSize . "\r\n"; return $chunkBeginning . $data . "\r\n"; diff --git a/src/Io/IniUtil.php b/src/Io/IniUtil.php index d085a823..612aae29 100644 --- a/src/Io/IniUtil.php +++ b/src/Io/IniUtil.php @@ -15,14 +15,14 @@ final class IniUtil */ public static function iniSizeToBytes($size) { - if (is_numeric($size)) { + if (\is_numeric($size)) { return (int)$size; } - $suffix = strtoupper(substr($size, -1)); - $strippedSize = substr($size, 0, -1); + $suffix = \strtoupper(\substr($size, -1)); + $strippedSize = \substr($size, 0, -1); - if (!is_numeric($strippedSize)) { + if (!\is_numeric($strippedSize)) { throw new \InvalidArgumentException("$size is not a valid ini size"); } diff --git a/src/Io/LengthLimitedStream.php b/src/Io/LengthLimitedStream.php index e155bdc9..bc64c54b 100644 --- a/src/Io/LengthLimitedStream.php +++ b/src/Io/LengthLimitedStream.php @@ -72,13 +72,13 @@ public function close() /** @internal */ public function handleData($data) { - if (($this->transferredLength + strlen($data)) > $this->maxLength) { + if (($this->transferredLength + \strlen($data)) > $this->maxLength) { // Only emit data until the value of 'Content-Length' is reached, the rest will be ignored - $data = (string)substr($data, 0, $this->maxLength - $this->transferredLength); + $data = (string)\substr($data, 0, $this->maxLength - $this->transferredLength); } if ($data !== '') { - $this->transferredLength += strlen($data); + $this->transferredLength += \strlen($data); $this->emit('data', array($data)); } diff --git a/src/Io/MiddlewareRunner.php b/src/Io/MiddlewareRunner.php index b2c1c781..dedf6ff1 100644 --- a/src/Io/MiddlewareRunner.php +++ b/src/Io/MiddlewareRunner.php @@ -23,7 +23,7 @@ final class MiddlewareRunner */ public function __construct(array $middleware) { - $this->middleware = array_values($middleware); + $this->middleware = \array_values($middleware); } /** diff --git a/src/Io/MultipartParser.php b/src/Io/MultipartParser.php index dbbfd732..8749b6c5 100644 --- a/src/Io/MultipartParser.php +++ b/src/Io/MultipartParser.php @@ -73,27 +73,27 @@ final class MultipartParser */ public function __construct($uploadMaxFilesize = null, $maxFileUploads = null) { - $var = ini_get('max_input_vars'); + $var = \ini_get('max_input_vars'); if ($var !== false) { $this->maxInputVars = (int)$var; } - $var = ini_get('max_input_nesting_level'); + $var = \ini_get('max_input_nesting_level'); if ($var !== false) { $this->maxInputNestingLevel = (int)$var; } if ($uploadMaxFilesize === null) { - $uploadMaxFilesize = ini_get('upload_max_filesize'); + $uploadMaxFilesize = \ini_get('upload_max_filesize'); } $this->uploadMaxFilesize = IniUtil::iniSizeToBytes($uploadMaxFilesize); - $this->maxFileUploads = $maxFileUploads === null ? (ini_get('file_uploads') === '' ? 0 : (int)ini_get('max_file_uploads')) : (int)$maxFileUploads; + $this->maxFileUploads = $maxFileUploads === null ? (\ini_get('file_uploads') === '' ? 0 : (int)\ini_get('max_file_uploads')) : (int)$maxFileUploads; } public function parse(ServerRequestInterface $request) { $contentType = $request->getHeaderLine('content-type'); - if(!preg_match('/boundary="?(.*)"?$/', $contentType, $matches)) { + if(!\preg_match('/boundary="?(.*)"?$/', $contentType, $matches)) { return $request; } @@ -112,35 +112,35 @@ public function parse(ServerRequestInterface $request) private function parseBody($boundary, $buffer) { - $len = strlen($boundary); + $len = \strlen($boundary); // ignore everything before initial boundary (SHOULD be empty) - $start = strpos($buffer, $boundary . "\r\n"); + $start = \strpos($buffer, $boundary . "\r\n"); while ($start !== false) { // search following boundary (preceded by newline) // ignore last if not followed by boundary (SHOULD end with "--") $start += $len + 2; - $end = strpos($buffer, "\r\n" . $boundary, $start); + $end = \strpos($buffer, "\r\n" . $boundary, $start); if ($end === false) { break; } // parse one part and continue searching for next - $this->parsePart(substr($buffer, $start, $end - $start)); + $this->parsePart(\substr($buffer, $start, $end - $start)); $start = $end; } } private function parsePart($chunk) { - $pos = strpos($chunk, "\r\n\r\n"); + $pos = \strpos($chunk, "\r\n\r\n"); if ($pos === false) { return; } $headers = $this->parseHeaders((string)substr($chunk, 0, $pos)); - $body = (string)substr($chunk, $pos + 4); + $body = (string)\substr($chunk, $pos + 4); if (!isset($headers['content-disposition'])) { return; @@ -180,7 +180,7 @@ private function parseFile($name, $filename, $contentType, $contents) private function parseUploadedFile($filename, $contentType, $contents) { - $size = strlen($contents); + $size = \strlen($contents); // no file selected (zero size and empty filename) if ($size === 0 && $filename === '') { @@ -192,7 +192,7 @@ private function parseUploadedFile($filename, $contentType, $contents) return new UploadedFile( Psr7\stream_for(), $size, - UPLOAD_ERR_NO_FILE, + \UPLOAD_ERR_NO_FILE, $filename, $contentType ); @@ -208,7 +208,7 @@ private function parseUploadedFile($filename, $contentType, $contents) return new UploadedFile( Psr7\stream_for(), $size, - UPLOAD_ERR_INI_SIZE, + \UPLOAD_ERR_INI_SIZE, $filename, $contentType ); @@ -219,7 +219,7 @@ private function parseUploadedFile($filename, $contentType, $contents) return new UploadedFile( Psr7\stream_for(), $size, - UPLOAD_ERR_FORM_SIZE, + \UPLOAD_ERR_FORM_SIZE, $filename, $contentType ); @@ -228,7 +228,7 @@ private function parseUploadedFile($filename, $contentType, $contents) return new UploadedFile( Psr7\stream_for($contents), $size, - UPLOAD_ERR_OK, + \UPLOAD_ERR_OK, $filename, $contentType ); @@ -247,7 +247,7 @@ private function parsePost($name, $value) $value )); - if (strtoupper($name) === 'MAX_FILE_SIZE') { + if (\strtoupper($name) === 'MAX_FILE_SIZE') { $this->maxFileSize = (int)$value; if ($this->maxFileSize === 0) { @@ -260,15 +260,15 @@ private function parseHeaders($header) { $headers = array(); - foreach (explode("\r\n", trim($header)) as $line) { - $parts = explode(':', $line, 2); + foreach (\explode("\r\n", \trim($header)) as $line) { + $parts = \explode(':', $line, 2); if (!isset($parts[1])) { continue; } - $key = strtolower(trim($parts[0])); - $values = explode(';', $parts[1]); - $values = array_map('trim', $values); + $key = \strtolower(trim($parts[0])); + $values = \explode(';', $parts[1]); + $values = \array_map('trim', $values); $headers[$key] = $values; } @@ -278,7 +278,7 @@ private function parseHeaders($header) private function getParameterFromHeader(array $header, $parameter) { foreach ($header as $part) { - if (preg_match('/' . $parameter . '="?(.*)"$/', $part, $matches)) { + if (\preg_match('/' . $parameter . '="?(.*)"$/', $part, $matches)) { return $matches[1]; } } @@ -288,8 +288,8 @@ private function getParameterFromHeader(array $header, $parameter) private function extractPost($postFields, $key, $value) { - $chunks = explode('[', $key); - if (count($chunks) == 1) { + $chunks = \explode('[', $key); + if (\count($chunks) == 1) { $postFields[$key] = $value; return $postFields; } @@ -299,23 +299,23 @@ private function extractPost($postFields, $key, $value) return $postFields; } - $chunkKey = rtrim($chunks[0], ']'); + $chunkKey = \rtrim($chunks[0], ']'); $parent = &$postFields; for ($i = 1; isset($chunks[$i]); $i++) { $previousChunkKey = $chunkKey; if ($previousChunkKey === '') { $parent[] = array(); - end($parent); - $parent = &$parent[key($parent)]; + \end($parent); + $parent = &$parent[\key($parent)]; } else { - if (!isset($parent[$previousChunkKey]) || !is_array($parent[$previousChunkKey])) { + if (!isset($parent[$previousChunkKey]) || !\is_array($parent[$previousChunkKey])) { $parent[$previousChunkKey] = array(); } $parent = &$parent[$previousChunkKey]; } - $chunkKey = rtrim($chunks[$i], ']'); + $chunkKey = \rtrim($chunks[$i], ']'); } if ($chunkKey === '') { diff --git a/src/Io/RequestHeaderParser.php b/src/Io/RequestHeaderParser.php index 1264371b..cfd1cdc0 100644 --- a/src/Io/RequestHeaderParser.php +++ b/src/Io/RequestHeaderParser.php @@ -34,7 +34,7 @@ public function __construct($localSocketUri = null, $remoteSocketUri = null) public function feed($data) { $this->buffer .= $data; - $endOfHeader = strpos($this->buffer, "\r\n\r\n"); + $endOfHeader = \strpos($this->buffer, "\r\n\r\n"); if ($endOfHeader > $this->maxSize || ($endOfHeader === false && isset($this->buffer[$this->maxSize]))) { $this->emit('error', array(new \OverflowException("Maximum header size of {$this->maxSize} exceeded.", 431), $this)); @@ -54,8 +54,8 @@ public function feed($data) private function parseAndEmitRequest($endOfHeader) { - $request = $this->parseRequest((string)substr($this->buffer, 0, $endOfHeader)); - $bodyBuffer = isset($this->buffer[$endOfHeader + 4]) ? substr($this->buffer, $endOfHeader + 4) : ''; + $request = $this->parseRequest((string)\substr($this->buffer, 0, $endOfHeader)); + $bodyBuffer = isset($this->buffer[$endOfHeader + 4]) ? \substr($this->buffer, $endOfHeader + 4) : ''; $this->emit('headers', array($request, $bodyBuffer)); } @@ -63,19 +63,19 @@ private function parseRequest($headers) { // additional, stricter safe-guard for request line // because request parser doesn't properly cope with invalid ones - if (!preg_match('#^[^ ]+ [^ ]+ HTTP/\d\.\d#m', $headers)) { + if (!\preg_match('#^[^ ]+ [^ ]+ HTTP/\d\.\d#m', $headers)) { throw new \InvalidArgumentException('Unable to parse invalid request-line'); } // parser does not support asterisk-form and authority-form // remember original target and temporarily replace and re-apply below $originalTarget = null; - if (strncmp($headers, 'OPTIONS * ', 10) === 0) { + if (\strncmp($headers, 'OPTIONS * ', 10) === 0) { $originalTarget = '*'; - $headers = 'OPTIONS / ' . substr($headers, 10); - } elseif (strncmp($headers, 'CONNECT ', 8) === 0) { - $parts = explode(' ', $headers, 3); - $uri = parse_url('tcp://' . $parts[1]); + $headers = 'OPTIONS / ' . \substr($headers, 10); + } elseif (\strncmp($headers, 'CONNECT ', 8) === 0) { + $parts = \explode(' ', $headers, 3); + $uri = \parse_url('tcp://' . $parts[1]); // check this is a valid authority-form request-target (host:port) if (isset($uri['scheme'], $uri['host'], $uri['port']) && count($uri) === 3) { @@ -93,14 +93,14 @@ private function parseRequest($headers) // create new obj implementing ServerRequestInterface by preserving all // previous properties and restoring original request-target $serverParams = array( - 'REQUEST_TIME' => time(), - 'REQUEST_TIME_FLOAT' => microtime(true) + 'REQUEST_TIME' => \time(), + 'REQUEST_TIME_FLOAT' => \microtime(true) ); // apply REMOTE_ADDR and REMOTE_PORT if source address is known // address should always be known, unless this is over Unix domain sockets (UDS) if ($this->remoteSocketUri !== null) { - $remoteAddress = parse_url($this->remoteSocketUri); + $remoteAddress = \parse_url($this->remoteSocketUri); $serverParams['REMOTE_ADDR'] = $remoteAddress['host']; $serverParams['REMOTE_PORT'] = $remoteAddress['port']; } @@ -109,7 +109,7 @@ private function parseRequest($headers) // 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 if ($this->localSocketUri !== null) { - $localAddress = parse_url($this->localSocketUri); + $localAddress = \parse_url($this->localSocketUri); if (isset($localAddress['host'], $localAddress['port'])) { $serverParams['SERVER_ADDR'] = $localAddress['host']; $serverParams['SERVER_PORT'] = $localAddress['port']; @@ -134,7 +134,7 @@ private function parseRequest($headers) $queryString = $request->getUri()->getQuery(); if ($queryString !== '') { $queryParams = array(); - parse_str($queryString, $queryParams); + \parse_str($queryString, $queryParams); $request = $request->withQueryParams($queryParams); } @@ -159,8 +159,8 @@ private function parseRequest($headers) // ensure absolute-form request-target contains a valid URI $requestTarget = $request->getRequestTarget(); - if (strpos($requestTarget, '://') !== false && substr($requestTarget, 0, 1) !== '/') { - $parts = parse_url($requestTarget); + if (\strpos($requestTarget, '://') !== false && \substr($requestTarget, 0, 1) !== '/') { + $parts = \parse_url($requestTarget); // 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'])) { @@ -170,7 +170,7 @@ private function parseRequest($headers) // Optional Host header value MUST be valid (host and optional port) if ($request->hasHeader('Host')) { - $parts = parse_url('http://' . $request->getHeaderLine('Host')); + $parts = \parse_url('http://' . $request->getHeaderLine('Host')); // make sure value contains valid host component (IP or hostname) if (!$parts || !isset($parts['scheme'], $parts['host'])) { @@ -186,7 +186,7 @@ private function parseRequest($headers) // set URI components from socket address if not already filled via Host header if ($request->getUri()->getHost() === '') { - $parts = parse_url($this->localSocketUri); + $parts = \parse_url($this->localSocketUri); if (!isset($parts['host'], $parts['port'])) { $parts = array('host' => '127.0.0.1', 'port' => 80); } @@ -207,13 +207,13 @@ private function parseRequest($headers) } // Update request URI to "https" scheme if the connection is encrypted - $parts = parse_url($this->localSocketUri); + $parts = \parse_url($this->localSocketUri); if (isset($parts['scheme']) && $parts['scheme'] === 'https') { // The request URI may omit default ports here, so try to parse port // from Host header field (if possible) $port = $request->getUri()->getPort(); if ($port === null) { - $port = parse_url('tcp://' . $request->getHeaderLine('Host'), PHP_URL_PORT); // @codeCoverageIgnore + $port = \parse_url('tcp://' . $request->getHeaderLine('Host'), PHP_URL_PORT); // @codeCoverageIgnore } $request = $request->withUri( diff --git a/src/Io/ServerRequest.php b/src/Io/ServerRequest.php index 96c90e75..14e37a00 100644 --- a/src/Io/ServerRequest.php +++ b/src/Io/ServerRequest.php @@ -113,7 +113,7 @@ public function getAttributes() public function getAttribute($name, $default = null) { - if (!array_key_exists($name, $this->attributes)) { + if (!\array_key_exists($name, $this->attributes)) { return $default; } return $this->attributes[$name]; @@ -143,20 +143,20 @@ public static function parseCookie($cookie) // PSR-7 `getHeaderLine('Cookies')` will return multiple // cookie header comma-seperated. Multiple cookie headers // are not allowed according to https://tools.ietf.org/html/rfc6265#section-5.4 - if (strpos($cookie, ',') !== false) { + if (\strpos($cookie, ',') !== false) { return false; } - $cookieArray = explode(';', $cookie); + $cookieArray = \explode(';', $cookie); $result = array(); foreach ($cookieArray as $pair) { - $pair = trim($pair); - $nameValuePair = explode('=', $pair, 2); + $pair = \trim($pair); + $nameValuePair = \explode('=', $pair, 2); - if (count($nameValuePair) === 2) { - $key = urldecode($nameValuePair[0]); - $value = urldecode($nameValuePair[1]); + if (\count($nameValuePair) === 2) { + $key = \urldecode($nameValuePair[0]); + $value = \urldecode($nameValuePair[1]); $result[$key] = $value; } } diff --git a/src/Io/UploadedFile.php b/src/Io/UploadedFile.php index 64717b7d..f2a6c9e7 100644 --- a/src/Io/UploadedFile.php +++ b/src/Io/UploadedFile.php @@ -57,15 +57,15 @@ public function __construct(StreamInterface $stream, $size, $error, $filename, $ $this->stream = $stream; $this->size = $size; - if (!is_int($error) || !in_array($error, array( - UPLOAD_ERR_OK, - UPLOAD_ERR_INI_SIZE, - UPLOAD_ERR_FORM_SIZE, - UPLOAD_ERR_PARTIAL, - UPLOAD_ERR_NO_FILE, - UPLOAD_ERR_NO_TMP_DIR, - UPLOAD_ERR_CANT_WRITE, - UPLOAD_ERR_EXTENSION, + if (!\is_int($error) || !\in_array($error, array( + \UPLOAD_ERR_OK, + \UPLOAD_ERR_INI_SIZE, + \UPLOAD_ERR_FORM_SIZE, + \UPLOAD_ERR_PARTIAL, + \UPLOAD_ERR_NO_FILE, + \UPLOAD_ERR_NO_TMP_DIR, + \UPLOAD_ERR_CANT_WRITE, + \UPLOAD_ERR_EXTENSION, ))) { throw new InvalidArgumentException( 'Invalid error code, must be an UPLOAD_ERR_* constant' @@ -81,7 +81,7 @@ public function __construct(StreamInterface $stream, $size, $error, $filename, $ */ public function getStream() { - if ($this->error !== UPLOAD_ERR_OK) { + if ($this->error !== \UPLOAD_ERR_OK) { throw new RuntimeException('Cannot retrieve stream due to upload error'); } diff --git a/src/Middleware/LimitConcurrentRequestsMiddleware.php b/src/Middleware/LimitConcurrentRequestsMiddleware.php index 4831fa2c..f816f70b 100644 --- a/src/Middleware/LimitConcurrentRequestsMiddleware.php +++ b/src/Middleware/LimitConcurrentRequestsMiddleware.php @@ -129,8 +129,8 @@ public function __invoke(ServerRequestInterface $request, $next) // get next queue position $queue =& $this->queue; $queue[] = null; - end($queue); - $id = key($queue); + \end($queue); + $id = \key($queue); $deferred = new Deferred(function ($_, $reject) use (&$queue, $id) { // queued promise cancelled before its next handler is invoked @@ -200,7 +200,7 @@ public function processQueue() return; } - $first = reset($this->queue); + $first = \reset($this->queue); unset($this->queue[key($this->queue)]); $first->resolve(); diff --git a/src/Middleware/RequestBodyBufferMiddleware.php b/src/Middleware/RequestBodyBufferMiddleware.php index cf379c79..0e6f5145 100644 --- a/src/Middleware/RequestBodyBufferMiddleware.php +++ b/src/Middleware/RequestBodyBufferMiddleware.php @@ -23,7 +23,7 @@ final class RequestBodyBufferMiddleware public function __construct($sizeLimit = null) { if ($sizeLimit === null) { - $sizeLimit = ini_get('post_max_size'); + $sizeLimit = \ini_get('post_max_size'); } $this->sizeLimit = IniUtil::iniSizeToBytes($sizeLimit); @@ -51,7 +51,7 @@ public function __invoke(ServerRequestInterface $request, $stack) } return Stream\buffer($body, $sizeLimit)->then(function ($buffer) use ($request, $stack) { - $stream = new BufferStream(strlen($buffer)); + $stream = new BufferStream(\strlen($buffer)); $stream->write($buffer); $request = $request->withBody($stream); diff --git a/src/Middleware/RequestBodyParserMiddleware.php b/src/Middleware/RequestBodyParserMiddleware.php index 005c28cd..be5ba16f 100644 --- a/src/Middleware/RequestBodyParserMiddleware.php +++ b/src/Middleware/RequestBodyParserMiddleware.php @@ -20,8 +20,8 @@ public function __construct($uploadMaxFilesize = null, $maxFileUploads = null) public function __invoke(ServerRequestInterface $request, $next) { - $type = strtolower($request->getHeaderLine('Content-Type')); - list ($type) = explode(';', $type); + $type = \strtolower($request->getHeaderLine('Content-Type')); + list ($type) = \explode(';', $type); if ($type === 'application/x-www-form-urlencoded') { return $next($this->parseFormUrlencoded($request)); @@ -39,7 +39,7 @@ private function parseFormUrlencoded(ServerRequestInterface $request) // parse string into array structure // ignore warnings due to excessive data structures (max_input_vars and max_input_nesting_level) $ret = array(); - @parse_str((string)$request->getBody(), $ret); + @\parse_str((string)$request->getBody(), $ret); return $request->withParsedBody($ret); } diff --git a/src/Server.php b/src/Server.php index 8931be9e..940b968d 100644 --- a/src/Server.php +++ b/src/Server.php @@ -50,7 +50,7 @@ final class Server extends EventEmitter */ public function __construct($requestHandler) { - if (!is_callable($requestHandler) && !is_array($requestHandler)) { + if (!\is_callable($requestHandler) && !\is_array($requestHandler)) { throw new \InvalidArgumentException('Invalid request handler given'); } @@ -62,15 +62,15 @@ public function __construct($requestHandler) // @link http://php.net/manual/en/ini.core.php#ini.enable-post-data-reading // @link http://php.net/manual/en/function.ini-get.php#refsect1-function.ini-get-notes // @link https://3v4l.org/qJtsa - $enablePostDataReading = ini_get('enable_post_data_reading'); + $enablePostDataReading = \ini_get('enable_post_data_reading'); if ($enablePostDataReading !== '') { $middleware[] = new RequestBodyParserMiddleware(); } - if (is_callable($requestHandler)) { + if (\is_callable($requestHandler)) { $middleware[] = $requestHandler; } else { - $middleware = array_merge($middleware, $requestHandler); + $middleware = \array_merge($middleware, $requestHandler); } $this->streamingServer = new StreamingServer($middleware); @@ -95,12 +95,12 @@ public function listen(ServerInterface $server) */ private function getConcurrentRequestsLimit() { - if (ini_get('memory_limit') == -1) { + if (\ini_get('memory_limit') == -1) { return self::MAXIMUM_CONCURRENT_REQUESTS; } - $availableMemory = IniUtil::iniSizeToBytes(ini_get('memory_limit')) / 4; - $concurrentRequests = ceil($availableMemory / IniUtil::iniSizeToBytes(ini_get('post_max_size'))); + $availableMemory = IniUtil::iniSizeToBytes(\ini_get('memory_limit')) / 4; + $concurrentRequests = \ceil($availableMemory / IniUtil::iniSizeToBytes(\ini_get('post_max_size'))); if ($concurrentRequests >= self::MAXIMUM_CONCURRENT_REQUESTS) { return self::MAXIMUM_CONCURRENT_REQUESTS; diff --git a/src/StreamingServer.php b/src/StreamingServer.php index 6abbd86c..4eae4b68 100644 --- a/src/StreamingServer.php +++ b/src/StreamingServer.php @@ -98,9 +98,9 @@ final class StreamingServer extends EventEmitter */ public function __construct($requestHandler) { - if (!is_callable($requestHandler) && !is_array($requestHandler)) { + if (!\is_callable($requestHandler) && !\is_array($requestHandler)) { throw new \InvalidArgumentException('Invalid request handler given'); - } elseif (!is_callable($requestHandler)) { + } elseif (!\is_callable($requestHandler)) { $requestHandler = new MiddlewareRunner($requestHandler); } @@ -160,7 +160,7 @@ public function handleConnection(ConnectionInterface $conn) $uriLocal = $conn->getLocalAddress(); if ($uriLocal !== null) { // local URI known, so translate transport scheme to application scheme - $uriLocal = strtr($uriLocal, array('tcp://' => 'http://', 'tls://' => 'https://')); + $uriLocal = \strtr($uriLocal, array('tcp://' => 'http://', 'tls://' => 'https://')); } $uriRemote = $conn->getRemoteAddress(); @@ -200,7 +200,7 @@ public function handleRequest(ConnectionInterface $conn, ServerRequestInterface $contentLength = 0; $stream = new CloseProtectionStream($conn); if ($request->hasHeader('Transfer-Encoding')) { - if (strtolower($request->getHeaderLine('Transfer-Encoding')) !== 'chunked') { + 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); } @@ -229,7 +229,7 @@ public function handleRequest(ConnectionInterface $conn, ServerRequestInterface $request = $request->withBody(new HttpBodyStream($stream, $contentLength)); - if ($request->getProtocolVersion() !== '1.0' && '100-continue' === strtolower($request->getHeaderLine('Expect'))) { + if ($request->getProtocolVersion() !== '1.0' && '100-continue' === \strtolower($request->getHeaderLine('Expect'))) { $conn->write("HTTP/1.1 100 Continue\r\n\r\n"); } @@ -273,7 +273,7 @@ public function handleRequest(ConnectionInterface $conn, ServerRequestInterface function ($response) use ($that, $conn, $request) { if (!$response instanceof ResponseInterface) { $message = 'The response callback is expected to resolve with an object implementing Psr\Http\Message\ResponseInterface, but resolved with "%s" instead.'; - $message = sprintf($message, is_object($response) ? get_class($response) : gettype($response)); + $message = \sprintf($message, \is_object($response) ? \get_class($response) : \gettype($response)); $exception = new \RuntimeException($message); $that->emit('error', array($exception)); @@ -283,7 +283,7 @@ function ($response) use ($that, $conn, $request) { }, function ($error) use ($that, $conn, $request) { $message = 'The response callback is expected to resolve with an object implementing Psr\Http\Message\ResponseInterface, but rejected with "%s" instead.'; - $message = sprintf($message, is_object($error) ? get_class($error) : gettype($error)); + $message = \sprintf($message, \is_object($error) ? \get_class($error) : \gettype($error)); $previous = null; From b29ab96557ac5c53e738fcb26f73f631a3f81f1a Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Wed, 16 Jan 2019 08:26:32 +0100 Subject: [PATCH 290/456] Prepare v0.8.4 release Signed-off-by: Cees-Jan Kiewiet --- CHANGELOG.md | 11 +++++++++++ README.md | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 748387a8..5099d6b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## 0.8.4 (2019-01-16) + +* Improvement: Internal refactoring to simplify response header logic. + (#321 by @clue) + +* Improvement: Assign Content-Length response header automatically only when size is known. + (#329 by @clue) + +* Improvement: Import global functions for better performance. + (#330 by @WyriHaximus) + ## 0.8.3 (2018-04-11) * Feature: Do not pause connection stream to detect closed connections immediately. diff --git a/README.md b/README.md index dd255866..7bab95a2 100644 --- a/README.md +++ b/README.md @@ -1189,7 +1189,7 @@ The recommended way to install this library is [through Composer](https://getcom This will install the latest supported version: ```bash -$ composer require react/http:^0.8.3 +$ composer require react/http:^0.8.4 ``` See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. From 227306f1f499145217d51a901d5b8d183a20a9ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 13 Apr 2019 18:47:01 +0200 Subject: [PATCH 291/456] Documentation and example for JSON/XML request body --- README.md | 131 +++++++++++++++--- examples/09-json-api.php | 64 +++++++++ ...ream-request.php => 13-stream-request.php} | 0 3 files changed, 178 insertions(+), 17 deletions(-) create mode 100644 examples/09-json-api.php rename examples/{09-stream-request.php => 13-stream-request.php} (100%) diff --git a/README.md b/README.md index 7bab95a2..8232f176 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ Event-driven, streaming plaintext HTTP and secure HTTPS server for [ReactPHP](ht * [Request](#request) * [Request parameters](#request-parameters) * [Query parameters](#query-parameters) + * [Request body](#request-body) * [Streaming request](#streaming-request) * [Request method](#request-method) * [Cookie parameters](#cookie-parameters) @@ -308,17 +309,112 @@ like in this example to prevent See also [example #4](examples). -#### Streaming request +#### Request body If you're using the [`Server`](#server), then the request object will be buffered and parsed in memory and contains the full request body. This includes the parsed request body and any file uploads. +> If you're using the advanced [`StreamingServer`](#streamingserver) class, jump + to the next chapter to learn more about how to process a + [streaming request](#streaming-request). + +As stated above, each incoming HTTP request is always represented by the +[PSR-7 `ServerRequestInterface`](https://www.php-fig.org/psr/psr-7/#321-psrhttpmessageserverrequestinterface). +This interface provides several methods that are useful when working with the +incoming request body as described below. + +The `getParsedBody(): null|array|object` method can be used to +get the parsed request body, similar to +[PHP's `$_POST` variable](https://www.php.net/manual/en/reserved.variables.post.php). +This method may return a (possibly nested) array structure with all body +parameters or a `null` value if the request body could not be parsed. +By default, this method will only return parsed data for requests using +`Content-Type: application/x-www-form-urlencoded` or `Content-Type: multipart/form-data` +request headers (commonly used for `POST` requests for HTML form submission data). + +```php +$server = new Server(function (ServerRequestInterface $request) { + $name = $request->getParsedBody()['name'] ?? 'anonymous'; + + return new Response( + 200, + array(), + "Hello $name!\n" + ); +}); +``` + +See also [example #12](examples) for more details. + +The `getBody(): StreamInterface` method can be used to +get the raw data from this request body, similar to +[PHP's `php://input` stream](https://www.php.net/manual/en/wrappers.php.php#wrappers.php.input). +This method returns an instance of the request body represented by the +[PSR-7 `StreamInterface`](https://www.php-fig.org/psr/psr-7/#34-psrhttpmessagestreaminterface). +This is particularly useful when using a custom request body that will not +otherwise be parsed by default, such as a JSON (`Content-Type: application/json`) or +an XML (`Content-Type: application/xml`) request body (which is commonly used for +`POST`, `PUT` or `PATCH` requests in JSON-based or RESTful/RESTish APIs). + +```php +$server = new Server(function (ServerRequestInterface $request) { + $data = json_decode((string)$request->getBody()); + $name = $data->name ?? 'anonymous'; + + return new Response( + 200, + array('Content-Type' => 'application/json'), + json_encode(['message' => "Hello $name!"]) + ); +}); +``` + +See also [example #9](examples) for more details. + +The `getUploadedFiles(): array` method can be used to +get the uploaded files in this request, similar to +[PHP's `$_FILES` variable](https://www.php.net/manual/en/reserved.variables.files.php). +This method returns a (possibly nested) array structure with all file uploads, each represented by the +[PSR-7 `UploadedFileInterface`](https://www.php-fig.org/psr/psr-7/#36-psrhttpmessageuploadedfileinterface). +This array will only be filled when using the `Content-Type: multipart/form-data` +request header (commonly used for `POST` requests for HTML file uploads). + +```php +$server = new Server(function (ServerRequestInterface $request) { + $files = $request->getUploadedFiles(); + $name = isset($files['avatar']) ? $files['avatar']->getClientFilename() : 'nothing'; + + return new Response( + 200, + array(), + "Uploaded $name\n" + ); +}); +``` + +See also [example #12](examples) for more details. + +The `getSize(): ?int` method can be used to +get the size of the request body, similar to PHP's `$_SERVER['CONTENT_LENGTH']` variable. +This method returns the complete size of the request body measured in number +of bytes as defined by the message boundaries. +This value may be `0` if the request message does not contain a request body +(such as a simple `GET` request). +This method operates on the buffered request body, i.e. the request body size +is always known, even when the request does not specify a `Content-Length` request +header or when using `Transfer-Encoding: chunked` for HTTP/1.1 requests. + +#### Streaming request + If you're using the advanced [`StreamingServer`](#streamingserver), the request object will be processed once the request headers have been received. This means that this happens irrespective of (i.e. *before*) receiving the (potentially much larger) request body. +> If you're using the [`Server`](#server) class, jump to the previous chapter + to learn more about how to process a buffered [request body](#request-body). + While this may be uncommon in the PHP ecosystem, this is actually a very powerful approach that gives you several advantages not otherwise possible: @@ -330,19 +426,20 @@ approach that gives you several advantages not otherwise possible: * Process a large request body without having to buffer anything in memory, such as accepting a huge file upload or possibly unlimited request body stream. -The `getBody()` method can be used to access the request body stream. -In the default streaming mode, this method returns a stream instance that implements both the -[PSR-7 StreamInterface](http://www.php-fig.org/psr/psr-7/#psrhttpmessagestreaminterface) -and the [ReactPHP ReadableStreamInterface](https://github.com/reactphp/stream#readablestreaminterface). -However, most of the `PSR-7 StreamInterface` methods have been -designed under the assumption of being in control of the request body. +The `getBody(): StreamInterface` method can be used to +access the request body stream. +In the streaming mode, this method returns a stream instance that implements both the +[PSR-7 `StreamInterface`](https://www.php-fig.org/psr/psr-7/#34-psrhttpmessagestreaminterface) +and the [ReactPHP `ReadableStreamInterface`](https://github.com/reactphp/stream#readablestreaminterface). +However, most of the PSR-7 `StreamInterface` methods have been +designed under the assumption of being in control of a synchronous request body. Given that this does not apply to this server, the following -`PSR-7 StreamInterface` methods are not used and SHOULD NOT be called: +PSR-7 `StreamInterface` methods are not used and SHOULD NOT be called: `tell()`, `eof()`, `seek()`, `rewind()`, `write()` and `read()`. If this is an issue for your use case and/or you want to access uploaded files, -it's highly recommended to use the +it's highly recommended to use a buffered [request body](#request-body) or use the [`RequestBodyBufferMiddleware`](#requestbodybuffermiddleware) instead. -The `ReactPHP ReadableStreamInterface` gives you access to the incoming +The ReactPHP `ReadableStreamInterface` gives you access to the incoming request body as the individual chunks arrive: ```php @@ -382,7 +479,7 @@ $server = new StreamingServer(function (ServerRequestInterface $request) { The above example simply counts the number of bytes received in the request body. This can be used as a skeleton for buffering or processing the request body. -See also [example #9](examples) for more details. +See also [example #13](examples) for more details. The `data` event will be emitted whenever new data is available on the request body stream. @@ -404,14 +501,14 @@ A `close` event will be emitted after an `error` or `end` event. For more details about the request body stream, check out the documentation of [ReactPHP ReadableStreamInterface](https://github.com/reactphp/stream#readablestreaminterface). -The `getSize(): ?int` method can be used if you only want to know the request -body size. -This method returns the complete size of the request body as defined by the -message boundaries. +The `getSize(): ?int` method can be used to +get the size of the request body, similar to PHP's `$_SERVER['CONTENT_LENGTH']` variable. +This method returns the complete size of the request body measured in number +of bytes as defined by the message boundaries. This value may be `0` if the request message does not contain a request body (such as a simple `GET` request). -Note that this value may be `null` if the request body size is unknown in -advance because the request message uses `Transfer-Encoding: chunked`. +This method operates on the streaming request body, i.e. the request body size +may be unknown (`null`) when using `Transfer-Encoding: chunked` for HTTP/1.1 requests. ```php $server = new StreamingServer(function (ServerRequestInterface $request) { diff --git a/examples/09-json-api.php b/examples/09-json-api.php new file mode 100644 index 00000000..a3b50d2b --- /dev/null +++ b/examples/09-json-api.php @@ -0,0 +1,64 @@ +getHeaderLine('Content-Type') !== 'application/json') { + return new Response( + 415, // Unsupported Media Type + array( + 'Content-Type' => 'application/json' + ), + json_encode(array('error' => 'Only supports application/json')) . "\n" + ); + } + + $input = json_decode($request->getBody()->getContents()); + if (json_last_error() !== JSON_ERROR_NONE) { + return new Response( + 400, // Bad Request + array( + 'Content-Type' => 'application/json' + ), + json_encode(array('error' => 'Invalid JSON data given')) . "\n" + ); + } + + if (!isset($input->name) || !is_string($input->name)) { + return new Response( + 422, // Unprocessable Entity + array( + 'Content-Type' => 'application/json' + ), + json_encode(array('error' => 'JSON data does not contain a string "name" property')) . "\n" + ); + } + + return new Response( + 200, + array( + 'Content-Type' => 'application/json' + ), + json_encode(array('message' => 'Hello ' . $input->name)) . "\n" + ); +}); + +$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); +$server->listen($socket); + +echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; + +$loop->run(); diff --git a/examples/09-stream-request.php b/examples/13-stream-request.php similarity index 100% rename from examples/09-stream-request.php rename to examples/13-stream-request.php From 89793e566d2ce903f2258242f16bac6574a7cf90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 14 Apr 2019 23:13:15 +0200 Subject: [PATCH 292/456] Documentation for concurrency and streaming requests --- README.md | 203 +++++++++++++++++++++++++++++++--------- src/Server.php | 110 ++++++++++++++++++---- src/StreamingServer.php | 71 +++++++------- 3 files changed, 289 insertions(+), 95 deletions(-) diff --git a/README.md b/README.md index 8232f176..8864733b 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,8 @@ Event-driven, streaming plaintext HTTP and secure HTTPS server for [ReactPHP](ht * [Usage](#usage) * [Server](#server) * [StreamingServer](#streamingserver) + * [listen()](#listen) + * [error event](#error-event) * [Request](#request) * [Request parameters](#request-parameters) * [Query parameters](#query-parameters) @@ -65,11 +67,10 @@ The `Server` class is responsible for handling incoming connections and then processing each incoming HTTP request. It buffers and parses the complete incoming HTTP request in memory. Once the -complete request has been received, it will invoke the request handler. - -For each request, it executes the callback function passed to the -constructor with the respective [request](#request) object and expects -a respective [response](#response) object in return. +complete request has been received, it will invoke the request handler function. +This request handler function needs to be passed to the constructor and will be +invoked with the respective [request](#request) object and expects a +[response](#response) object in return: ```php $server = new Server(function (ServerRequestInterface $request) { @@ -83,18 +84,79 @@ $server = new Server(function (ServerRequestInterface $request) { }); ``` -For most users a server that buffers and parses a requests before handling it over as a -PSR-7 request is what they want. The `Server` facade takes care of that, and takes the more -advanced configuration out of hand. Under the hood it uses [StreamingServer](#streamingserver) -with the the three stock middleware using default settings from `php.ini`. +Each incoming HTTP request message is always represented by the +[PSR-7 `ServerRequestInterface`](https://www.php-fig.org/psr/psr-7/#321-psrhttpmessageserverrequestinterface), +see also following [request](#request) chapter for more details. +Each outgoing HTTP response message is always represented by the +[PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface), +see also following [response](#response) chapter for more details. -The [LimitConcurrentRequestsMiddleware](#limitconcurrentrequestsmiddleware) requires a limit, -as such the `Server` facade uses the `memory_limit` and `post_max_size` ini settings to -calculate a sensible limit. It assumes a maximum of a quarter of the `memory_limit` for -buffering and the other three quarter for parsing and handling the requests. The limit is -division of half of `memory_limit` by `memory_limit` rounded up. +In order to process any connections, the server needs to be attached to an +instance of `React\Socket\ServerInterface` through the [`listen()`](#listen) method +as described in the following chapter. In its most simple form, you can attach +this to a [`React\Socket\Server`](https://github.com/reactphp/socket#server) +in order to start a plaintext HTTP server like this: -> Note that any errors emitted by the wrapped `StreamingServer` are forwarded by `Server`. +```php +$server = new Server($handler); + +$socket = new React\Socket\Server('0.0.0.0:8080', $loop); +$server->listen($socket); +``` + +See also the [`listen()`](#listen) method and the [first example](examples) for more details. + +The `Server` class is built as a facade around the underlying +[`StreamingServer`](#streamingserver) to provide sane defaults for 80% of the +use cases and is the recommended way to use this library unless you're sure +you know what you're doing. + +Unlike the underlying [`StreamingServer`](#streamingserver), this class +buffers and parses the complete incoming HTTP request in memory. Once the +complete request has been received, it will invoke the request handler +function. This means the [request](#request) passed to your request handler +function will be fully compatible with PSR-7. + +On the other hand, buffering complete HTTP requests in memory until they can +be processed by your request handler function means that this class has to +employ a number of limits to avoid consuming too much memory. In order to +take the more advanced configuration out your hand, it respects setting from +your [`php.ini`](https://www.php.net/manual/en/ini.core.php) to apply its +default settings. This is a list of PHP settings this class respects with +their respective default values: + +``` +memory_limit 128M +post_max_size 8M +enable_post_data_reading 1 +max_input_nesting_level 64 +max_input_vars 1000 + +file_uploads 1 +upload_max_filesize 2M +max_file_uploads 20 +``` + +In particular, the `post_max_size` setting limits how much memory a single HTTP +request is allowed to consume while buffering its request body. On top of +this, this class will try to avoid consuming more than 1/4 of your +`memory_limit` for buffering multiple concurrent HTTP requests. As such, with +the above default settings of `128M` max, it will try to consume no more than +`32M` for buffering multiple concurrent HTTP requests. As a consequence, it +will limit the concurrency to 4 HTTP requests with the above defaults. + +It is imperative that you assign reasonable values to your PHP ini settings. +It is usually recommended to either reduce the memory a single request is +allowed to take (set `post_max_size 1M` or less) or to increase the total memory +limit to allow for more concurrent requests (set `memory_limit 512M` or more). +Failure to do so means that this class may have to disable concurrency and +only handle one request at a time. + +Internally, this class automatically assigns these limits to the +[middleware](#middleware) request handlers as described below. For more +advanced use cases, you may also use the advanced +[`StreamingServer`](#streamingserver) and assign these middleware request +handlers yourself as described in the following chapters. ### StreamingServer @@ -103,11 +165,11 @@ processing each incoming HTTP request. Unlike the [`Server`](#server) class, it does not buffer and parse the incoming HTTP request body by default. This means that the request handler will be -invoked with a streaming request body. - -For each request, it executes the callback function passed to the -constructor with the respective [request](#request) object and expects -a respective [response](#response) object in return. +invoked with a streaming request body. Once the request headers have been +received, it will invoke the request handler function. This request handler +function needs to be passed to the constructor and will be invoked with the +respective [request](#request) object and expects a [response](#response) +object in return: ```php $server = new StreamingServer(function (ServerRequestInterface $request) { @@ -121,51 +183,90 @@ $server = new StreamingServer(function (ServerRequestInterface $request) { }); ``` +Each incoming HTTP request message is always represented by the +[PSR-7 `ServerRequestInterface`](https://www.php-fig.org/psr/psr-7/#321-psrhttpmessageserverrequestinterface), +see also following [request](#request) chapter for more details. +Each outgoing HTTP response message is always represented by the +[PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface), +see also following [response](#response) chapter for more details. + In order to process any connections, the server needs to be attached to an -instance of `React\Socket\ServerInterface` which emits underlying streaming -connections in order to then parse incoming data as HTTP. +instance of `React\Socket\ServerInterface` through the [`listen()`](#listen) method +as described in the following chapter. In its most simple form, you can attach +this to a [`React\Socket\Server`](https://github.com/reactphp/socket#server) +in order to start a plaintext HTTP server like this: + +```php +$server = new StreamingServer($handler); + +$socket = new React\Socket\Server('0.0.0.0:8080', $loop); +$server->listen($socket); +``` -You can attach this to a +See also the [`listen()`](#listen) method and the [first example](examples) for more details. + +The `StreamingServer` class is considered advanced usage and unless you know +what you're doing, you're recommended to use the [`Server`](#server) class +instead. The `StreamingServer` class is specifically designed to help with +more advanced use cases where you want to have full control over consuming +the incoming HTTP request body and concurrency settings. + +In particular, this class does not buffer and parse the incoming HTTP request +in memory. It will invoke the request handler function once the HTTP request +headers have been received, i.e. before receiving the potentially much larger +HTTP request body. This means the [request](#request) passed to your request +handler function may not be fully compatible with PSR-7. See also +[streaming request](#streaming-request) below for more details. + +### listen() + +The `listen(React\Socket\ServerInterface $socket): void` method can be used to +start processing connections from the given socket server. +The given [`React\Socket\ServerInterface`](https://github.com/reactphp/socket#serverinterface) +is responsible for emitting the underlying streaming connections. +This HTTP server needs to be attached to it in order to process any connections +and pase incoming streaming data as incoming HTTP request messages. +In its most common form, you can attach this to a [`React\Socket\Server`](https://github.com/reactphp/socket#server) in order to start a plaintext HTTP server like this: ```php +$server = new Server($handler); +// or $server = new StreamingServer($handler); -$socket = new React\Socket\Server(8080, $loop); +$socket = new React\Socket\Server('0.0.0.0:8080', $loop); $server->listen($socket); ``` -See also the `listen()` method and the [first example](examples) for more details. +This example will start listening for HTTP requests on the alternative HTTP port +`8080` on all interfaces (publicly). As an alternative, it is very common to use +a reverse proxy and let this HTTP server listen on the localhost (loopback) +interface only by using the listen address `127.0.0.1:8080` instead. This way, you +host your application(s) on the default HTTP port `80` and only route specific +requests to this HTTP server. -Similarly, you can also attach this to a -[`React\Socket\SecureServer`](https://github.com/reactphp/socket#secureserver) -in order to start a secure HTTPS server like this: +Likewise, it's usually recommended to use a reverse proxy setup to accept +secure HTTPS requests on default HTTPS port `443` (TLS termination) and only +route plaintext requests to this HTTP server. As an alternative, you can also +accept secure HTTPS requests with this HTTP server by attaching this to a +[`React\Socket\Server`](https://github.com/reactphp/socket#server) using a +secure TLS listen address, a certificate file and optional `passphrase` like this: ```php +$server = new Server($handler); +// or $server = new StreamingServer($handler); -$socket = new React\Socket\Server(8080, $loop); -$socket = new React\Socket\SecureServer($socket, $loop, array( +$socket = new React\Socket\Server('tls://0.0.0.0:8443', $loop, array( 'local_cert' => __DIR__ . '/localhost.pem' )); - $server->listen($socket); ``` See also [example #11](examples) for more details. -When HTTP/1.1 clients want to send a bigger request body, they MAY send only -the request headers with an additional `Expect: 100-continue` header and -wait before sending the actual (large) message body. -In this case the server will automatically send an intermediary -`HTTP/1.1 100 Continue` response to the client. -This ensures you will receive the request body without a delay as expected. -The [Response](#response) still needs to be created as described in the -examples above. - -See also [request](#request) and [response](#response) -for more details (e.g. the request data body). +### error event The `StreamingServer` supports both HTTP/1.1 and HTTP/1.0 request messages. If a client sends an invalid request message, uses an invalid HTTP protocol @@ -198,6 +299,8 @@ $server->on('error', function (Exception $e) { Note that the request object can also emit an error. Check out [request](#request) for more details. +> Note that any errors emitted by the wrapped `StreamingServer` are forwarded by `Server`. + ### Request As seen above, the [`Server`](#server) and [`StreamingServer`](#streamingserver) @@ -405,6 +508,14 @@ This method operates on the buffered request body, i.e. the request body size is always known, even when the request does not specify a `Content-Length` request header or when using `Transfer-Encoding: chunked` for HTTP/1.1 requests. +> Note: The `Server` automatically takes care of handling requests with the + additional `Expect: 100-continue` request header. When HTTP/1.1 clients want to + send a bigger request body, they MAY send only the request headers with an + additional `Expect: 100-continue` request header and wait before sending the actual + (large) message body. In this case the server will automatically send an + intermediary `HTTP/1.1 100 Continue` response to the client. This ensures you + will receive the request body without a delay as expected. + #### Streaming request If you're using the advanced [`StreamingServer`](#streamingserver), the @@ -536,6 +647,14 @@ $server = new StreamingServer(function (ServerRequestInterface $request) { }); ``` +> Note: The `StreamingServer` automatically takes care of handling requests with the + additional `Expect: 100-continue` request header. When HTTP/1.1 clients want to + send a bigger request body, they MAY send only the request headers with an + additional `Expect: 100-continue` request header and wait before sending the actual + (large) message body. In this case the server will automatically send an + intermediary `HTTP/1.1 100 Continue` response to the client. This ensures you + will receive the streaming request body without a delay as expected. + #### Request method Note that the server supports *any* request method (including custom and non- diff --git a/src/Server.php b/src/Server.php index 940b968d..76428aaa 100644 --- a/src/Server.php +++ b/src/Server.php @@ -10,28 +10,100 @@ use React\Socket\ServerInterface; /** - * Facade around StreamingServer with sane defaults for 80% of the use cases. - * The requests passed to your callable are fully compatible with PSR-7 because - * the body of the requests are fully buffered and parsed, unlike StreamingServer - * where the body is a raw ReactPHP stream. + * The `Server` class is responsible for handling incoming connections and then + * processing each incoming HTTP request. * - * Wraps StreamingServer with the following middleware: - * - LimitConcurrentRequestsMiddleware - * - RequestBodyBufferMiddleware - * - RequestBodyParserMiddleware (only when enable_post_data_reading is true (default)) - * - The callable you in passed as first constructor argument + * It buffers and parses the complete incoming HTTP request in memory. Once the + * complete request has been received, it will invoke the request handler function. + * This request handler function needs to be passed to the constructor and will be + * invoked with the respective [request](#request) object and expects a + * [response](#response) object in return: * - * All middleware use their default configuration, which can be controlled with - * the the following configuration directives from php.ini: - * - upload_max_filesize - * - post_max_size - * - max_input_nesting_level - * - max_input_vars - * - file_uploads - * - max_file_uploads - * - enable_post_data_reading + * ```php + * $server = new Server(function (ServerRequestInterface $request) { + * return new Response( + * 200, + * array( + * 'Content-Type' => 'text/plain' + * ), + * "Hello World!\n" + * ); + * }); + * ``` * - * Forwards the error event coming from StreamingServer. + * Each incoming HTTP request message is always represented by the + * [PSR-7 `ServerRequestInterface`](https://www.php-fig.org/psr/psr-7/#321-psrhttpmessageserverrequestinterface), + * see also following [request](#request) chapter for more details. + * Each outgoing HTTP response message is always represented by the + * [PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface), + * see also following [response](#response) chapter for more details. + * + * In order to process any connections, the server needs to be attached to an + * instance of `React\Socket\ServerInterface` through the [`listen()`](#listen) method + * as described in the following chapter. In its most simple form, you can attach + * this to a [`React\Socket\Server`](https://github.com/reactphp/socket#server) + * in order to start a plaintext HTTP server like this: + * + * ```php + * $server = new Server($handler); + * + * $socket = new React\Socket\Server('0.0.0.0:8080', $loop); + * $server->listen($socket); + * ``` + * + * See also the [`listen()`](#listen) method and the [first example](examples) for more details. + * + * The `Server` class is built as a facade around the underlying + * [`StreamingServer`](#streamingserver) to provide sane defaults for 80% of the + * use cases and is the recommended way to use this library unless you're sure + * you know what you're doing. + * + * Unlike the underlying [`StreamingServer`](#streamingserver), this class + * buffers and parses the complete incoming HTTP request in memory. Once the + * complete request has been received, it will invoke the request handler + * function. This means the [request](#request) passed to your request handler + * function will be fully compatible with PSR-7. + * + * On the other hand, buffering complete HTTP requests in memory until they can + * be processed by your request handler function means that this class has to + * employ a number of limits to avoid consuming too much memory. In order to + * take the more advanced configuration out your hand, it respects setting from + * your [`php.ini`](https://www.php.net/manual/en/ini.core.php) to apply its + * default settings. This is a list of PHP settings this class respects with + * their respective default values: + * + * ``` + * memory_limit 128M + * post_max_size 8M + * enable_post_data_reading 1 + * max_input_nesting_level 64 + * max_input_vars 1000 + * + * file_uploads 1 + * upload_max_filesize 2M + * max_file_uploads 20 + * ``` + * + * In particular, the `post_max_size` setting limits how much memory a single HTTP + * request is allowed to consume while buffering its request body. On top of + * this, this class will try to avoid consuming more than 1/4 of your + * `memory_limit` for buffering multiple concurrent HTTP requests. As such, with + * the above default settings of `128M` max, it will try to consume no more than + * `32M` for buffering multiple concurrent HTTP requests. As a consequence, it + * will limit the concurrency to 4 HTTP requests with the above defaults. + * + * It is imperative that you assign reasonable values to your PHP ini settings. + * It is usually recommended to either reduce the memory a single request is + * allowed to take (set `post_max_size 1M` or less) or to increase the total memory + * limit to allow for more concurrent requests (set `memory_limit 512M` or more). + * Failure to do so means that this class may have to disable concurrency and + * only handle one request at a time. + * + * Internally, this class automatically assigns these limits to the + * [middleware](#middleware) request handlers as described below. For more + * advanced use cases, you may also use the advanced + * [`StreamingServer`](#streamingserver) and assign these middleware request + * handlers yourself as described in the following chapters. */ final class Server extends EventEmitter { diff --git a/src/StreamingServer.php b/src/StreamingServer.php index 21dd4a52..64a82af2 100644 --- a/src/StreamingServer.php +++ b/src/StreamingServer.php @@ -22,60 +22,63 @@ use React\Stream\WritableStreamInterface; /** - * The `Server` class is responsible for handling incoming connections and then + * The advanced `StreamingServer` class is responsible for handling incoming connections and then * processing each incoming HTTP request. * - * For each request, it executes the callback function passed to the - * constructor with the respective [request](#request) object and expects - * a respective [response](#response) object in return. + * Unlike the [`Server`](#server) class, it does not buffer and parse the incoming + * HTTP request body by default. This means that the request handler will be + * invoked with a streaming request body. Once the request headers have been + * received, it will invoke the request handler function. This request handler + * function needs to be passed to the constructor and will be invoked with the + * respective [request](#request) object and expects a [response](#response) + * object in return: * * ```php * $server = new StreamingServer(function (ServerRequestInterface $request) { * return new Response( * 200, - * array('Content-Type' => 'text/plain'), + * array( + * 'Content-Type' => 'text/plain' + * ), * "Hello World!\n" * ); * }); * ``` * + * Each incoming HTTP request message is always represented by the + * [PSR-7 `ServerRequestInterface`](https://www.php-fig.org/psr/psr-7/#321-psrhttpmessageserverrequestinterface), + * see also following [request](#request) chapter for more details. + * Each outgoing HTTP response message is always represented by the + * [PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface), + * see also following [response](#response) chapter for more details. + * * In order to process any connections, the server needs to be attached to an - * instance of `React\Socket\ServerInterface` which emits underlying streaming - * connections in order to then parse incoming data as HTTP. + * instance of `React\Socket\ServerInterface` through the [`listen()`](#listen) method + * as described in the following chapter. In its most simple form, you can attach + * this to a [`React\Socket\Server`](https://github.com/reactphp/socket#server) + * in order to start a plaintext HTTP server like this: * * ```php - * $socket = new React\Socket\Server(8080, $loop); + * $server = new StreamingServer($handler); + * + * $socket = new React\Socket\Server('0.0.0.0:8080', $loop); * $server->listen($socket); * ``` * - * See also the [listen()](#listen) method and the [first example](examples) for more details. - * - * When HTTP/1.1 clients want to send a bigger request body, they MAY send only - * the request headers with an additional `Expect: 100-continue` header and - * wait before sending the actual (large) message body. - * In this case the server will automatically send an intermediary - * `HTTP/1.1 100 Continue` response to the client. - * This ensures you will receive the request body without a delay as expected. - * The [Response](#response) still needs to be created as described in the - * examples above. - * - * See also [request](#request) and [response](#response) - * for more details (e.g. the request data body). + * See also the [`listen()`](#listen) method and the [first example](examples) for more details. * - * The `Server` supports both HTTP/1.1 and HTTP/1.0 request messages. - * If a client sends an invalid request message, uses an invalid HTTP protocol - * version or sends an invalid `Transfer-Encoding` in the request header, it will - * emit an `error` event, send an HTTP error response to the client and - * close the connection: - * - * ```php - * $server->on('error', function (Exception $e) { - * echo 'Error: ' . $e->getMessage() . PHP_EOL; - * }); - * ``` + * The `StreamingServer` class is considered advanced usage and unless you know + * what you're doing, you're recommended to use the [`Server`](#server) class + * instead. The `StreamingServer` class is specifically designed to help with + * more advanced use cases where you want to have full control over consuming + * the incoming HTTP request body and concurrency settings. * - * Note that the request object can also emit an error. - * Check out [request](#request) for more details. + * In particular, this class does not buffer and parse the incoming HTTP request + * in memory. It will invoke the request handler function once the HTTP request + * headers have been received, i.e. before receiving the potentially much larger + * HTTP request body. This means the [request](#request) passed to your request + * handler function may not be fully compatible with PSR-7. See also + * [streaming request](#streaming-request) below for more details. * * @see Request * @see Response From ff78fd03c7032ed63f163c6196179dcb59515cfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 15 Apr 2019 12:46:16 +0200 Subject: [PATCH 293/456] Improve documentation for error handling --- README.md | 91 +++++++++++++++++++++++++++++++------------------------ 1 file changed, 52 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index 8864733b..7eb0ae0e 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,6 @@ Event-driven, streaming plaintext HTTP and secure HTTPS server for [ReactPHP](ht * [Server](#server) * [StreamingServer](#streamingserver) * [listen()](#listen) - * [error event](#error-event) * [Request](#request) * [Request parameters](#request-parameters) * [Query parameters](#query-parameters) @@ -19,6 +18,7 @@ Event-driven, streaming plaintext HTTP and secure HTTPS server for [ReactPHP](ht * [Streaming request](#streaming-request) * [Request method](#request-method) * [Cookie parameters](#cookie-parameters) + * [Invalid request](#invalid-request) * [Response](#response) * [Deferred response](#deferred-response) * [Streaming response](#streaming-response) @@ -266,41 +266,6 @@ $server->listen($socket); See also [example #11](examples) for more details. -### error event - -The `StreamingServer` supports both HTTP/1.1 and HTTP/1.0 request messages. -If a client sends an invalid request message, uses an invalid HTTP protocol -version or sends an invalid `Transfer-Encoding` in the request header, it will -emit an `error` event, send an HTTP error response to the client and -close the connection: - -```php -$server->on('error', function (Exception $e) { - echo 'Error: ' . $e->getMessage() . PHP_EOL; -}); -``` - -The server will also emit an `error` event if you return an invalid -type in the callback function or have a unhandled `Exception` or `Throwable`. -If your callback function throws an `Exception` or `Throwable`, -the `StreamingServer` will emit a `RuntimeException` and add the thrown exception -as previous: - -```php -$server->on('error', function (Exception $e) { - echo 'Error: ' . $e->getMessage() . PHP_EOL; - if ($e->getPrevious() !== null) { - $previousException = $e->getPrevious(); - echo $previousException->getMessage() . PHP_EOL; - } -}); -``` - -Note that the request object can also emit an error. -Check out [request](#request) for more details. - -> Note that any errors emitted by the wrapped `StreamingServer` are forwarded by `Server`. - ### Request As seen above, the [`Server`](#server) and [`StreamingServer`](#streamingserver) @@ -727,6 +692,26 @@ This encoding is also used internally when decoding the name and value of cookie See also [example #5](examples) for more details. +#### Invalid request + +The `Server` and `StreamingServer` classes support both HTTP/1.1 and HTTP/1.0 request +messages. If a client sends an invalid request message, uses an invalid HTTP +protocol version or sends an invalid `Transfer-Encoding` request header value, +the server will automatically send a `400` (Bad Request) HTTP error response +to the client and close the connection. +On top of this, it will emit an `error` event that can be used for logging +purposes like this: + +```php +$server->on('error', function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; +}); +``` + +Note that the server will also emit an `error` event if you do not return a +valid response object from your request handler function. See also +[invalid response](#invalid-response) for more details. + ### Response The callback function passed to the constructor of the [`Server`](#server) or @@ -950,9 +935,37 @@ to the message if the same request would have used an (unconditional) `GET`. #### Invalid response -An invalid return value or an unhandled `Exception` or `Throwable` in the code -of the callback function, will result in an `500 Internal Server Error` message. -Make sure to catch `Exceptions` or `Throwables` to create own response messages. +As stated above, each outgoing HTTP response is always represented by the +[PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface). +If your request handler function returns an invalid value or throws an +unhandled `Exception` or `Throwable`, the server will automatically send a `500` +(Internal Server Error) HTTP error response to the client. +On top of this, it will emit an `error` event that can be used for logging +purposes like this: + +```php +$server->on('error', function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; + if ($e->getPrevious() !== null) { + echo 'Previous: ' . $e->getPrevious()->getMessage() . PHP_EOL; + } +}); +``` + +Note that the server will also emit an `error` event if the client sends an +invalid HTTP request that never reaches your request handler function. See +also [invalid request](#invalid-request) for more details. +Additionally, a [streaming request](#streaming-request) body can also emit +an `error` event on the request body. + +The server will only send a very generic `500` (Interval Server Error) HTTP +error response without any further details to the client if an unhandled +error occurs. While we understand this might make initial debugging harder, +it also means that the server does not leak any application details or stack +traces to the outside by default. It is usually recommended to catch any +`Exception` or `Throwable` within your request handler function or alternatively +use a [`middleware`](#middleware) to avoid this generic error handling and +create your own HTTP response message instead. #### Default response headers From 29741b08d2672bb41ddc53d29702b8752a330450 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 2 Apr 2019 00:53:55 +0200 Subject: [PATCH 294/456] Minor internal refactoring to apply server params during request ctor This changeset is in preparation for upcoming refactorings to move unrelated logic out of the parser class to prepare for persistent HTTP connections in follow-up PR. This changeset does not affect the public API and happens to improves performance slightly from around 8800 req/s to 9000 req/s on my machine (best of 5). --- src/Io/RequestHeaderParser.php | 13 ---- src/Io/ServerRequest.php | 18 +++-- tests/Io/ServerRequestTest.php | 136 +++++++++++++++++++++++++-------- 3 files changed, 115 insertions(+), 52 deletions(-) diff --git a/src/Io/RequestHeaderParser.php b/src/Io/RequestHeaderParser.php index cfd1cdc0..dbf54132 100644 --- a/src/Io/RequestHeaderParser.php +++ b/src/Io/RequestHeaderParser.php @@ -130,19 +130,6 @@ private function parseRequest($headers) ); $request = $request->withRequestTarget($target); - // Add query params - $queryString = $request->getUri()->getQuery(); - if ($queryString !== '') { - $queryParams = array(); - \parse_str($queryString, $queryParams); - $request = $request->withQueryParams($queryParams); - } - - $cookies = ServerRequest::parseCookie($request->getHeaderLine('Cookie')); - if ($cookies !== false) { - $request = $request->withCookieParams($cookies); - } - // re-apply actual request target from above if ($originalTarget !== null) { $request = $request->withUri( diff --git a/src/Io/ServerRequest.php b/src/Io/ServerRequest.php index 14e37a00..787960dd 100644 --- a/src/Io/ServerRequest.php +++ b/src/Io/ServerRequest.php @@ -51,6 +51,13 @@ public function __construct( ) { $this->serverParams = $serverParams; parent::__construct($method, $uri, $headers, $body, $protocolVersion); + + $query = $this->getUri()->getQuery(); + if ($query !== '') { + \parse_str($query, $this->queryParams); + } + + $this->cookies = $this->parseCookie($this->getHeaderLine('Cookie')); } public function getServerParams() @@ -134,17 +141,16 @@ public function withoutAttribute($name) } /** - * @internal * @param string $cookie - * @return boolean|mixed[] + * @return array */ - public static function parseCookie($cookie) + private function parseCookie($cookie) { - // PSR-7 `getHeaderLine('Cookies')` will return multiple + // PSR-7 `getHeaderLine('Cookie')` will return multiple // cookie header comma-seperated. Multiple cookie headers // are not allowed according to https://tools.ietf.org/html/rfc6265#section-5.4 - if (\strpos($cookie, ',') !== false) { - return false; + if ($cookie === '' || \strpos($cookie, ',') !== false) { + return array(); } $cookieArray = \explode(';', $cookie); diff --git a/tests/Io/ServerRequestTest.php b/tests/Io/ServerRequestTest.php index b9fc8444..7a7b241a 100644 --- a/tests/Io/ServerRequestTest.php +++ b/tests/Io/ServerRequestTest.php @@ -54,6 +54,13 @@ public function testWithoutAttribute() $this->assertEquals(array('test' => 'nice'), $request->getAttributes()); } + public function testGetQueryParamsFromConstructorUri() + { + $this->request = new ServerRequest('GET', 'http://localhost/?test=world'); + + $this->assertEquals(array('test' => 'world'), $this->request->getQueryParams()); + } + public function testWithCookieParams() { $request = $this->request->withCookieParams(array('test' => 'world')); @@ -62,6 +69,13 @@ public function testWithCookieParams() $this->assertEquals(array('test' => 'world'), $request->getCookieParams()); } + public function testGetQueryParamsFromConstructorUriUrlencoded() + { + $this->request = new ServerRequest('GET', 'http://localhost/?test=hello+world%21'); + + $this->assertEquals(array('test' => 'hello world!'), $this->request->getQueryParams()); + } + public function testWithQueryParams() { $request = $this->request->withQueryParams(array('test' => 'world')); @@ -70,6 +84,14 @@ public function testWithQueryParams() $this->assertEquals(array('test' => 'world'), $request->getQueryParams()); } + public function testWithQueryParamsWithoutSpecialEncoding() + { + $request = $this->request->withQueryParams(array('test' => 'hello world!')); + + $this->assertNotSame($request, $this->request); + $this->assertEquals(array('test' => 'hello world!'), $request->getQueryParams()); + } + public function testWithUploadedFiles() { $request = $this->request->withUploadedFiles(array('test' => 'world')); @@ -109,85 +131,133 @@ public function testServerRequestParameter() public function testParseSingleCookieNameValuePairWillReturnValidArray() { - $cookieString = 'hello=world'; - $cookies = ServerRequest::parseCookie($cookieString); + $this->request = new ServerRequest( + 'GET', + 'http://localhost', + array('Cookie' => 'hello=world') + ); + + $cookies = $this->request->getCookieParams(); $this->assertEquals(array('hello' => 'world'), $cookies); } - public function testParseMultipleCookieNameValuePaiWillReturnValidArray() + public function testParseMultipleCookieNameValuePairWillReturnValidArray() { - $cookieString = 'hello=world; test=abc'; - $cookies = ServerRequest::parseCookie($cookieString); + $this->request = new ServerRequest( + 'GET', + 'http://localhost', + array('Cookie' => 'hello=world; test=abc') + ); + + $cookies = $this->request->getCookieParams(); $this->assertEquals(array('hello' => 'world', 'test' => 'abc'), $cookies); } - public function testParseMultipleCookieNameValuePairWillReturnFalse() + public function testParseMultipleCookieHeadersAreNotAllowedAndWillReturnEmptyArray() { - // Could be done through multiple 'Cookie' headers - // getHeaderLine('Cookie') will return a value seperated by comma - // e.g. - // GET / HTTP/1.1\r\n - // Host: test.org\r\n - // Cookie: hello=world\r\n - // Cookie: test=abc\r\n\r\n - $cookieString = 'hello=world,test=abc'; - $cookies = ServerRequest::parseCookie($cookieString); - $this->assertEquals(false, $cookies); + $this->request = new ServerRequest( + 'GET', + 'http://localhost', + array('Cookie' => array('hello=world', 'test=abc')) + ); + + $cookies = $this->request->getCookieParams(); + $this->assertEquals(array(), $cookies); } - public function testOnlyFirstSetWillBeAddedToCookiesArray() + public function testMultipleCookiesWithSameNameWillReturnLastValue() { - $cookieString = 'hello=world; hello=abc'; - $cookies = ServerRequest::parseCookie($cookieString); + $this->request = new ServerRequest( + 'GET', + 'http://localhost', + array('Cookie' => 'hello=world; hello=abc') + ); + + $cookies = $this->request->getCookieParams(); $this->assertEquals(array('hello' => 'abc'), $cookies); } public function testOtherEqualSignsWillBeAddedToValueAndWillReturnValidArray() { - $cookieString = 'hello=world=test=php'; - $cookies = ServerRequest::parseCookie($cookieString); + $this->request = new ServerRequest( + 'GET', + 'http://localhost', + array('Cookie' => 'hello=world=test=php') + ); + + $cookies = $this->request->getCookieParams(); $this->assertEquals(array('hello' => 'world=test=php'), $cookies); } public function testSingleCookieValueInCookiesReturnsEmptyArray() { - $cookieString = 'world'; - $cookies = ServerRequest::parseCookie($cookieString); + $this->request = new ServerRequest( + 'GET', + 'http://localhost', + array('Cookie' => 'world') + ); + + $cookies = $this->request->getCookieParams(); $this->assertEquals(array(), $cookies); } public function testSingleMutlipleCookieValuesReturnsEmptyArray() { - $cookieString = 'world; test'; - $cookies = ServerRequest::parseCookie($cookieString); + $this->request = new ServerRequest( + 'GET', + 'http://localhost', + array('Cookie' => 'world; test') + ); + + $cookies = $this->request->getCookieParams(); $this->assertEquals(array(), $cookies); } public function testSingleValueIsValidInMultipleValueCookieWillReturnValidArray() { - $cookieString = 'world; test=php'; - $cookies = ServerRequest::parseCookie($cookieString); + $this->request = new ServerRequest( + 'GET', + 'http://localhost', + array('Cookie' => 'world; test=php') + ); + + $cookies = $this->request->getCookieParams(); $this->assertEquals(array('test' => 'php'), $cookies); } public function testUrlEncodingForValueWillReturnValidArray() { - $cookieString = 'hello=world%21; test=100%25%20coverage'; - $cookies = ServerRequest::parseCookie($cookieString); + $this->request = new ServerRequest( + 'GET', + 'http://localhost', + array('Cookie' => 'hello=world%21; test=100%25%20coverage') + ); + + $cookies = $this->request->getCookieParams(); $this->assertEquals(array('hello' => 'world!', 'test' => '100% coverage'), $cookies); } public function testUrlEncodingForKeyWillReturnValidArray() { - $cookieString = 'react%3Bphp=is%20great'; - $cookies = ServerRequest::parseCookie($cookieString); + $this->request = new ServerRequest( + 'GET', + 'http://localhost', + array('Cookie' => 'react%3Bphp=is%20great') + ); + + $cookies = $this->request->getCookieParams(); $this->assertEquals(array('react;php' => 'is great'), $cookies); } public function testCookieWithoutSpaceAfterSeparatorWillBeAccepted() { - $cookieString = 'hello=world;react=php'; - $cookies = ServerRequest::parseCookie($cookieString); + $this->request = new ServerRequest( + 'GET', + 'http://localhost', + array('Cookie' => 'hello=world;react=php') + ); + + $cookies = $this->request->getCookieParams(); $this->assertEquals(array('hello' => 'world', 'react' => 'php'), $cookies); } } From 2fe19c46c4e0c52c630665ba998229122c33a8fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 19 Apr 2019 01:24:11 +0200 Subject: [PATCH 295/456] Refactor request parser to simply emitting request headers --- src/Io/RequestHeaderParser.php | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/Io/RequestHeaderParser.php b/src/Io/RequestHeaderParser.php index dbf54132..6220f022 100644 --- a/src/Io/RequestHeaderParser.php +++ b/src/Io/RequestHeaderParser.php @@ -3,6 +3,7 @@ namespace React\Http\Io; use Evenement\EventEmitter; +use Psr\Http\Message\ServerRequestInterface; use RingCentral\Psr7 as g7; use Exception; @@ -44,21 +45,24 @@ public function feed($data) if (false !== $endOfHeader) { try { - $this->parseAndEmitRequest($endOfHeader); + $request = $this->parseRequest((string)\substr($this->buffer, 0, $endOfHeader)); } catch (Exception $exception) { $this->emit('error', array($exception)); + $this->removeAllListeners(); + return; } + + $bodyBuffer = isset($this->buffer[$endOfHeader + 4]) ? \substr($this->buffer, $endOfHeader + 4) : ''; + $this->emit('headers', array($request, $bodyBuffer)); $this->removeAllListeners(); } } - private function parseAndEmitRequest($endOfHeader) - { - $request = $this->parseRequest((string)\substr($this->buffer, 0, $endOfHeader)); - $bodyBuffer = isset($this->buffer[$endOfHeader + 4]) ? \substr($this->buffer, $endOfHeader + 4) : ''; - $this->emit('headers', array($request, $bodyBuffer)); - } - + /** + * @param string $headers buffer string containing request headers only + * @throws \InvalidArgumentException + * @return ServerRequestInterface + */ private function parseRequest($headers) { // additional, stricter safe-guard for request line From 6e7dd6634b66bf23c1b1205e4e1e256541d359e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 19 Apr 2019 14:53:16 +0200 Subject: [PATCH 296/456] Reuse single request parser for all requests This changeset is in preparation for upcoming refactorings to move unrelated logic out of the parser class to prepare for persistent HTTP connections in follow-up PR. This changeset does not affect the public API and happens to improves performance slightly from around 9000 req/s to 9200 req/s on my machine (best of 5). --- src/Io/RequestHeaderParser.php | 94 +++++++----- src/StreamingServer.php | 64 +++----- tests/Io/RequestHeaderParserTest.php | 210 +++++++++++++++++++-------- 3 files changed, 230 insertions(+), 138 deletions(-) diff --git a/src/Io/RequestHeaderParser.php b/src/Io/RequestHeaderParser.php index 6220f022..88a554d7 100644 --- a/src/Io/RequestHeaderParser.php +++ b/src/Io/RequestHeaderParser.php @@ -4,6 +4,7 @@ use Evenement\EventEmitter; use Psr\Http\Message\ServerRequestInterface; +use React\Socket\ConnectionInterface; use RingCentral\Psr7 as g7; use Exception; @@ -20,50 +21,73 @@ */ class RequestHeaderParser extends EventEmitter { - private $buffer = ''; private $maxSize = 8192; - private $localSocketUri; - private $remoteSocketUri; - - public function __construct($localSocketUri = null, $remoteSocketUri = null) + public function handle(ConnectionInterface $conn) { - $this->localSocketUri = $localSocketUri; - $this->remoteSocketUri = $remoteSocketUri; - } + $buffer = ''; + $maxSize = $this->maxSize; + $that = $this; + $conn->on('data', $fn = function ($data) use (&$buffer, &$fn, $conn, $maxSize, $that) { + // append chunk of data to buffer and look for end of request headers + $buffer .= $data; + $endOfHeader = \strpos($buffer, "\r\n\r\n"); + + // reject request if buffer size is exceeded + if ($endOfHeader > $maxSize || ($endOfHeader === false && isset($buffer[$maxSize]))) { + $conn->removeListener('data', $fn); + $fn = null; + + $that->emit('error', array( + new \OverflowException("Maximum header size of {$maxSize} exceeded.", 431), + $conn + )); + return; + } - public function feed($data) - { - $this->buffer .= $data; - $endOfHeader = \strpos($this->buffer, "\r\n\r\n"); + // ignore incomplete requests + if ($endOfHeader === false) { + return; + } - if ($endOfHeader > $this->maxSize || ($endOfHeader === false && isset($this->buffer[$this->maxSize]))) { - $this->emit('error', array(new \OverflowException("Maximum header size of {$this->maxSize} exceeded.", 431), $this)); - $this->removeAllListeners(); - return; - } + // request headers received => try to parse request + $conn->removeListener('data', $fn); + $fn = null; - if (false !== $endOfHeader) { try { - $request = $this->parseRequest((string)\substr($this->buffer, 0, $endOfHeader)); + $request = $that->parseRequest( + (string)\substr($buffer, 0, $endOfHeader), + $conn->getRemoteAddress(), + $conn->getLocalAddress() + ); } catch (Exception $exception) { - $this->emit('error', array($exception)); - $this->removeAllListeners(); + $buffer = ''; + $that->emit('error', array( + $exception, + $conn + )); return; } - $bodyBuffer = isset($this->buffer[$endOfHeader + 4]) ? \substr($this->buffer, $endOfHeader + 4) : ''; - $this->emit('headers', array($request, $bodyBuffer)); - $this->removeAllListeners(); - } + $bodyBuffer = isset($buffer[$endOfHeader + 4]) ? \substr($buffer, $endOfHeader + 4) : ''; + $buffer = ''; + $that->emit('headers', array($request, $bodyBuffer, $conn)); + }); + + $conn->on('close', function () use (&$buffer, &$fn) { + $fn = $buffer = null; + }); } /** * @param string $headers buffer string containing request headers only - * @throws \InvalidArgumentException + * @param ?string $remoteSocketUri + * @param ?string $localSocketUri * @return ServerRequestInterface + * @throws \InvalidArgumentException + * @internal */ - private function parseRequest($headers) + public function parseRequest($headers, $remoteSocketUri, $localSocketUri) { // additional, stricter safe-guard for request line // because request parser doesn't properly cope with invalid ones @@ -103,8 +127,8 @@ private function parseRequest($headers) // apply REMOTE_ADDR and REMOTE_PORT if source address is known // address should always be known, unless this is over Unix domain sockets (UDS) - if ($this->remoteSocketUri !== null) { - $remoteAddress = \parse_url($this->remoteSocketUri); + if ($remoteSocketUri !== null) { + $remoteAddress = \parse_url($remoteSocketUri); $serverParams['REMOTE_ADDR'] = $remoteAddress['host']; $serverParams['REMOTE_PORT'] = $remoteAddress['port']; } @@ -112,13 +136,13 @@ private function parseRequest($headers) // 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 - if ($this->localSocketUri !== null) { - $localAddress = \parse_url($this->localSocketUri); + if ($localSocketUri !== null) { + $localAddress = \parse_url($localSocketUri); if (isset($localAddress['host'], $localAddress['port'])) { $serverParams['SERVER_ADDR'] = $localAddress['host']; $serverParams['SERVER_PORT'] = $localAddress['port']; } - if (isset($localAddress['scheme']) && $localAddress['scheme'] === 'https') { + if (isset($localAddress['scheme']) && $localAddress['scheme'] === 'tls') { $serverParams['HTTPS'] = 'on'; } } @@ -177,7 +201,7 @@ private function parseRequest($headers) // set URI components from socket address if not already filled via Host header if ($request->getUri()->getHost() === '') { - $parts = \parse_url($this->localSocketUri); + $parts = \parse_url($localSocketUri); if (!isset($parts['host'], $parts['port'])) { $parts = array('host' => '127.0.0.1', 'port' => 80); } @@ -198,8 +222,8 @@ private function parseRequest($headers) } // Update request URI to "https" scheme if the connection is encrypted - $parts = \parse_url($this->localSocketUri); - if (isset($parts['scheme']) && $parts['scheme'] === 'https') { + $parts = \parse_url($localSocketUri); + if (isset($parts['scheme']) && $parts['scheme'] === 'tls') { // The request URI may omit default ports here, so try to parse port // from Host header field (if possible) $port = $request->getUri()->getPort(); diff --git a/src/StreamingServer.php b/src/StreamingServer.php index 64a82af2..fa3f9777 100644 --- a/src/StreamingServer.php +++ b/src/StreamingServer.php @@ -87,6 +87,7 @@ final class StreamingServer extends EventEmitter { private $callback; + private $parser; /** * Creates an HTTP server that invokes the given callback for each incoming HTTP request @@ -108,6 +109,27 @@ public function __construct($requestHandler) } $this->callback = $requestHandler; + $this->parser = new RequestHeaderParser(); + + $that = $this; + $this->parser->on('headers', function (ServerRequestInterface $request, $bodyBuffer, 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) { + $that->emit('error', array($e)); + + // parsing failed => assume dummy request and send appropriate error + $that->writeError( + $conn, + $e->getCode() !== 0 ? $e->getCode() : 400, + new ServerRequest('GET', '/') + ); + }); } /** @@ -154,47 +176,7 @@ public function __construct($requestHandler) */ public function listen(ServerInterface $socket) { - $socket->on('connection', array($this, 'handleConnection')); - } - - /** @internal */ - public function handleConnection(ConnectionInterface $conn) - { - $uriLocal = $conn->getLocalAddress(); - if ($uriLocal !== null) { - // local URI known, so translate transport scheme to application scheme - $uriLocal = \strtr($uriLocal, array('tcp://' => 'http://', 'tls://' => 'https://')); - } - - $uriRemote = $conn->getRemoteAddress(); - - $that = $this; - $parser = new RequestHeaderParser($uriLocal, $uriRemote); - - $listener = array($parser, 'feed'); - $parser->on('headers', function (ServerRequestInterface $request, $bodyBuffer) use ($conn, $listener, $that) { - // parsing request completed => stop feeding parser - $conn->removeListener('data', $listener); - - $that->handleRequest($conn, $request); - - if ($bodyBuffer !== '') { - $conn->emit('data', array($bodyBuffer)); - } - }); - - $conn->on('data', $listener); - $parser->on('error', function(\Exception $e) use ($conn, $listener, $that) { - $conn->removeListener('data', $listener); - $that->emit('error', array($e)); - - // parsing failed => assume dummy request and send appropriate error - $that->writeError( - $conn, - $e->getCode() !== 0 ? $e->getCode() : 400, - new ServerRequest('GET', '/') - ); - }); + $socket->on('connection', array($this->parser, 'handle')); } /** @internal */ diff --git a/tests/Io/RequestHeaderParserTest.php b/tests/Io/RequestHeaderParserTest.php index b47eac15..dbf44e69 100644 --- a/tests/Io/RequestHeaderParserTest.php +++ b/tests/Io/RequestHeaderParserTest.php @@ -12,14 +12,18 @@ public function testSplitShouldHappenOnDoubleCrlf() $parser = new RequestHeaderParser(); $parser->on('headers', $this->expectCallableNever()); - $parser->feed("GET / HTTP/1.1\r\n"); - $parser->feed("Host: example.com:80\r\n"); - $parser->feed("Connection: close\r\n"); + $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + + $parser->handle($connection); + + $connection->emit('data', array("GET / HTTP/1.1\r\n")); + $connection->emit('data', array("Host: example.com:80\r\n")); + $connection->emit('data', array("Connection: close\r\n")); $parser->removeAllListeners(); $parser->on('headers', $this->expectCallableOnce()); - $parser->feed("\r\n"); + $connection->emit('data', array("\r\n")); } public function testFeedInOneGo() @@ -27,24 +31,53 @@ public function testFeedInOneGo() $parser = new RequestHeaderParser(); $parser->on('headers', $this->expectCallableOnce()); + $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $parser->handle($connection); + + $data = $this->createGetRequest(); + $connection->emit('data', array($data)); + } + + public function testFeedTwoRequestsOnSeparateConnections() + { + $parser = new RequestHeaderParser(); + + $called = 0; + $parser->on('headers', function () use (&$called) { + ++$called; + }); + + $connection1 = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $connection2 = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $parser->handle($connection1); + $parser->handle($connection2); + $data = $this->createGetRequest(); - $parser->feed($data); + $connection1->emit('data', array($data)); + $connection2->emit('data', array($data)); + + $this->assertEquals(2, $called); } - public function testHeadersEventShouldReturnRequestAndBodyBuffer() + public function testHeadersEventShouldReturnRequestAndBodyBufferAndConnection() { $request = null; $bodyBuffer = null; + $conn = null; $parser = new RequestHeaderParser(); - $parser->on('headers', function ($parsedRequest, $parsedBodyBuffer) use (&$request, &$bodyBuffer) { + $parser->on('headers', function ($parsedRequest, $parsedBodyBuffer, $connection) use (&$request, &$bodyBuffer, &$conn) { $request = $parsedRequest; $bodyBuffer = $parsedBodyBuffer; + $conn = $connection; }); + $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $parser->handle($connection); + $data = $this->createGetRequest(); $data .= 'RANDOM DATA'; - $parser->feed($data); + $connection->emit('data', array($data)); $this->assertInstanceOf('Psr\Http\Message\RequestInterface', $request); $this->assertSame('GET', $request->getMethod()); @@ -53,6 +86,8 @@ public function testHeadersEventShouldReturnRequestAndBodyBuffer() $this->assertSame(array('Host' => array('example.com'), 'Connection' => array('close')), $request->getHeaders()); $this->assertSame('RANDOM DATA', $bodyBuffer); + + $this->assertSame($connection, $conn); } public function testHeadersEventShouldReturnBinaryBodyBuffer() @@ -64,9 +99,12 @@ public function testHeadersEventShouldReturnBinaryBodyBuffer() $bodyBuffer = $parsedBodyBuffer; }); + $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $parser->handle($connection); + $data = $this->createGetRequest(); $data .= "\0x01\0x02\0x03\0x04\0x05"; - $parser->feed($data); + $connection->emit('data', array($data)); $this->assertSame("\0x01\0x02\0x03\0x04\0x05", $bodyBuffer); } @@ -80,8 +118,11 @@ public function testHeadersEventShouldParsePathAndQueryString() $request = $parsedRequest; }); + $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $parser->handle($connection); + $data = $this->createAdvancedPostRequest(); - $parser->feed($data); + $connection->emit('data', array($data)); $this->assertInstanceOf('Psr\Http\Message\RequestInterface', $request); $this->assertSame('POST', $request->getMethod()); @@ -95,31 +136,39 @@ public function testHeadersEventShouldParsePathAndQueryString() $this->assertSame($headers, $request->getHeaders()); } - public function testHeaderEventWithShouldApplyDefaultAddressFromConstructor() + public function testHeaderEventWithShouldApplyDefaultAddressFromLocalConnectionAddress() { $request = null; - $parser = new RequestHeaderParser('http://127.1.1.1:8000'); + $parser = new RequestHeaderParser(); $parser->on('headers', function ($parsedRequest) use (&$request) { $request = $parsedRequest; }); - $parser->feed("GET /foo HTTP/1.0\r\n\r\n"); + $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('getLocalAddress'))->getMock(); + $connection->expects($this->once())->method('getLocalAddress')->willReturn('tcp://127.1.1.1:8000'); + $parser->handle($connection); + + $connection->emit('data', array("GET /foo HTTP/1.0\r\n\r\n")); $this->assertEquals('http://127.1.1.1:8000/foo', $request->getUri()); $this->assertEquals('127.1.1.1:8000', $request->getHeaderLine('Host')); } - public function testHeaderEventViaHttpsShouldApplySchemeFromConstructor() + public function testHeaderEventViaHttpsShouldApplyHttpsSchemeFromLocalTlsConnectionAddress() { $request = null; - $parser = new RequestHeaderParser('https://127.1.1.1:8000'); + $parser = new RequestHeaderParser(); $parser->on('headers', function ($parsedRequest) use (&$request) { $request = $parsedRequest; }); - $parser->feed("GET /foo HTTP/1.0\r\nHost: example.com\r\n\r\n"); + $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('getLocalAddress'))->getMock(); + $connection->expects($this->once())->method('getLocalAddress')->willReturn('tls://127.1.1.1:8000'); + $parser->handle($connection); + + $connection->emit('data', array("GET /foo HTTP/1.0\r\nHost: example.com\r\n\r\n")); $this->assertEquals('https://example.com/foo', $request->getUri()); $this->assertEquals('example.com', $request->getHeaderLine('Host')); @@ -128,26 +177,24 @@ public function testHeaderEventViaHttpsShouldApplySchemeFromConstructor() public function testHeaderOverflowShouldEmitError() { $error = null; - $passedParser = null; + $passedConnection = null; $parser = new RequestHeaderParser(); $parser->on('headers', $this->expectCallableNever()); - $parser->on('error', function ($message, $parser) use (&$error, &$passedParser) { + $parser->on('error', function ($message, $connection) use (&$error, &$passedConnection) { $error = $message; - $passedParser = $parser; + $passedConnection = $connection; }); - $this->assertSame(1, count($parser->listeners('headers'))); - $this->assertSame(1, count($parser->listeners('error'))); + $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $parser->handle($connection); $data = str_repeat('A', 8193); - $parser->feed($data); + $connection->emit('data', array($data)); $this->assertInstanceOf('OverflowException', $error); $this->assertSame('Maximum header size of 8192 exceeded.', $error->getMessage()); - $this->assertSame($parser, $passedParser); - $this->assertSame(0, count($parser->listeners('headers'))); - $this->assertSame(0, count($parser->listeners('error'))); + $this->assertSame($connection, $passedConnection); } public function testHeaderOverflowShouldNotEmitErrorWhenDataExceedsMaxHeaderSize() @@ -161,11 +208,13 @@ public function testHeaderOverflowShouldNotEmitErrorWhenDataExceedsMaxHeaderSize $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; - - $parser->feed($data); + $connection->emit('data', array($data)); $headers = array( 'Host' => array('example.com'), @@ -187,15 +236,13 @@ public function testInvalidEmptyRequestHeadersParseException() $error = $message; }); - $this->assertSame(1, count($parser->listeners('headers'))); - $this->assertSame(1, count($parser->listeners('error'))); + $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $parser->handle($connection); - $parser->feed("\r\n\r\n"); + $connection->emit('data', array("\r\n\r\n")); $this->assertInstanceOf('InvalidArgumentException', $error); $this->assertSame('Unable to parse invalid request-line', $error->getMessage()); - $this->assertSame(0, count($parser->listeners('headers'))); - $this->assertSame(0, count($parser->listeners('error'))); } public function testInvalidMalformedRequestLineParseException() @@ -208,15 +255,13 @@ public function testInvalidMalformedRequestLineParseException() $error = $message; }); - $this->assertSame(1, count($parser->listeners('headers'))); - $this->assertSame(1, count($parser->listeners('error'))); + $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $parser->handle($connection); - $parser->feed("GET /\r\n\r\n"); + $connection->emit('data', array("GET /\r\n\r\n")); $this->assertInstanceOf('InvalidArgumentException', $error); $this->assertSame('Unable to parse invalid request-line', $error->getMessage()); - $this->assertSame(0, count($parser->listeners('headers'))); - $this->assertSame(0, count($parser->listeners('error'))); } public function testInvalidAbsoluteFormSchemeEmitsError() @@ -229,7 +274,10 @@ public function testInvalidAbsoluteFormSchemeEmitsError() $error = $message; }); - $parser->feed("GET tcp://example.com:80/ HTTP/1.0\r\n\r\n"); + $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $parser->handle($connection); + + $connection->emit('data', array("GET tcp://example.com:80/ HTTP/1.0\r\n\r\n")); $this->assertInstanceOf('InvalidArgumentException', $error); $this->assertSame('Invalid absolute-form request-target', $error->getMessage()); @@ -245,7 +293,10 @@ public function testOriginFormWithSchemeSeparatorInParam() $request = $parsedRequest; }); - $parser->feed("GET /somepath?param=http://example.com HTTP/1.1\r\nHost: localhost\r\n\r\n"); + $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $parser->handle($connection); + + $connection->emit('data', array("GET /somepath?param=http://example.com HTTP/1.1\r\nHost: localhost\r\n\r\n")); $this->assertInstanceOf('Psr\Http\Message\RequestInterface', $request); $this->assertSame('GET', $request->getMethod()); @@ -267,7 +318,10 @@ public function testUriStartingWithColonSlashSlashFails() $error = $message; }); - $parser->feed("GET ://example.com:80/ HTTP/1.0\r\n\r\n"); + $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $parser->handle($connection); + + $connection->emit('data', array("GET ://example.com:80/ HTTP/1.0\r\n\r\n")); $this->assertInstanceOf('InvalidArgumentException', $error); $this->assertSame('Invalid request string', $error->getMessage()); @@ -283,7 +337,10 @@ public function testInvalidAbsoluteFormWithFragmentEmitsError() $error = $message; }); - $parser->feed("GET http://example.com:80/#home HTTP/1.0\r\n\r\n"); + $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $parser->handle($connection); + + $connection->emit('data', array("GET http://example.com:80/#home HTTP/1.0\r\n\r\n")); $this->assertInstanceOf('InvalidArgumentException', $error); $this->assertSame('Invalid absolute-form request-target', $error->getMessage()); @@ -299,7 +356,10 @@ public function testInvalidHeaderContainsFullUri() $error = $message; }); - $parser->feed("GET / HTTP/1.1\r\nHost: http://user:pass@host/\r\n\r\n"); + $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $parser->handle($connection); + + $connection->emit('data', array("GET / HTTP/1.1\r\nHost: http://user:pass@host/\r\n\r\n")); $this->assertInstanceOf('InvalidArgumentException', $error); $this->assertSame('Invalid Host header value', $error->getMessage()); @@ -315,7 +375,10 @@ public function testInvalidAbsoluteFormWithHostHeaderEmpty() $error = $message; }); - $parser->feed("GET http://example.com/ HTTP/1.1\r\nHost: \r\n\r\n"); + $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $parser->handle($connection); + + $connection->emit('data', array("GET http://example.com/ HTTP/1.1\r\nHost: \r\n\r\n")); $this->assertInstanceOf('InvalidArgumentException', $error); $this->assertSame('Invalid Host header value', $error->getMessage()); @@ -331,7 +394,10 @@ public function testInvalidConnectRequestWithNonAuthorityForm() $error = $message; }); - $parser->feed("CONNECT http://example.com:8080/ HTTP/1.1\r\nHost: example.com:8080\r\n\r\n"); + $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $parser->handle($connection); + + $connection->emit('data', array("CONNECT http://example.com:8080/ HTTP/1.1\r\nHost: example.com:8080\r\n\r\n")); $this->assertInstanceOf('InvalidArgumentException', $error); $this->assertSame('CONNECT method MUST use authority-form request target', $error->getMessage()); @@ -347,7 +413,10 @@ public function testInvalidHttpVersion() $error = $message; }); - $parser->feed("GET / HTTP/1.2\r\n\r\n"); + $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $parser->handle($connection); + + $connection->emit('data', array("GET / HTTP/1.2\r\n\r\n")); $this->assertInstanceOf('InvalidArgumentException', $error); $this->assertSame(505, $error->getCode()); @@ -358,16 +427,19 @@ public function testServerParamsWillBeSetOnHttpsRequest() { $request = null; - $parser = new RequestHeaderParser( - 'https://127.1.1.1:8000', - 'https://192.168.1.1:8001' - ); + $parser = new RequestHeaderParser(); $parser->on('headers', function ($parsedRequest) use (&$request) { $request = $parsedRequest; }); - $parser->feed("GET /foo HTTP/1.0\r\nHost: example.com\r\n\r\n"); + $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('getLocalAddress', 'getRemoteAddress'))->getMock(); + $connection->expects($this->once())->method('getLocalAddress')->willReturn('tls://127.1.1.1:8000'); + $connection->expects($this->once())->method('getRemoteAddress')->willReturn('tls://192.168.1.1:8001'); + $parser->handle($connection); + + $connection->emit('data', array("GET /foo HTTP/1.0\r\nHost: example.com\r\n\r\n")); + $serverParams = $request->getServerParams(); $this->assertEquals('on', $serverParams['HTTPS']); @@ -385,16 +457,19 @@ public function testServerParamsWillBeSetOnHttpRequest() { $request = null; - $parser = new RequestHeaderParser( - 'http://127.1.1.1:8000', - 'http://192.168.1.1:8001' - ); + $parser = new RequestHeaderParser(); $parser->on('headers', function ($parsedRequest) use (&$request) { $request = $parsedRequest; }); - $parser->feed("GET /foo HTTP/1.0\r\nHost: example.com\r\n\r\n"); + $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('getLocalAddress', 'getRemoteAddress'))->getMock(); + $connection->expects($this->once())->method('getLocalAddress')->willReturn('tcp://127.1.1.1:8000'); + $connection->expects($this->once())->method('getRemoteAddress')->willReturn('tcp://192.168.1.1:8001'); + $parser->handle($connection); + + $connection->emit('data', array("GET /foo HTTP/1.0\r\nHost: example.com\r\n\r\n")); + $serverParams = $request->getServerParams(); $this->assertArrayNotHasKey('HTTPS', $serverParams); @@ -412,16 +487,19 @@ public function testServerParamsWillNotSetRemoteAddressForUnixDomainSockets() { $request = null; - $parser = new RequestHeaderParser( - 'unix://./server.sock', - null - ); + $parser = new RequestHeaderParser(); $parser->on('headers', function ($parsedRequest) use (&$request) { $request = $parsedRequest; }); - $parser->feed("GET /foo HTTP/1.0\r\nHost: example.com\r\n\r\n"); + $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('getLocalAddress', 'getRemoteAddress'))->getMock(); + $connection->expects($this->once())->method('getLocalAddress')->willReturn('unix://./server.sock'); + $connection->expects($this->once())->method('getRemoteAddress')->willReturn(null); + $parser->handle($connection); + + $connection->emit('data', array("GET /foo HTTP/1.0\r\nHost: example.com\r\n\r\n")); + $serverParams = $request->getServerParams(); $this->assertArrayNotHasKey('HTTPS', $serverParams); @@ -445,7 +523,11 @@ public function testServerParamsWontBeSetOnMissingUrls() $request = $parsedRequest; }); - $parser->feed("GET /foo HTTP/1.0\r\nHost: example.com\r\n\r\n"); + $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $parser->handle($connection); + + $connection->emit('data', array("GET /foo HTTP/1.0\r\nHost: example.com\r\n\r\n")); + $serverParams = $request->getServerParams(); $this->assertNotEmpty($serverParams['REQUEST_TIME']); @@ -468,7 +550,11 @@ public function testQueryParmetersWillBeSet() $request = $parsedRequest; }); - $parser->feed("GET /foo.php?hello=world&test=this HTTP/1.0\r\nHost: example.com\r\n\r\n"); + $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $parser->handle($connection); + + $connection->emit('data', array("GET /foo.php?hello=world&test=this HTTP/1.0\r\nHost: example.com\r\n\r\n")); + $queryParams = $request->getQueryParams(); $this->assertEquals('world', $queryParams['hello']); From 8e5e77ad338479ece651da8b29fe968fa511d426 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 16 Jun 2019 19:06:44 +0200 Subject: [PATCH 297/456] Refactor to validate message boundaries while parsing request headers --- src/Io/RequestHeaderParser.php | 20 ++ src/StreamingServer.php | 24 +-- tests/Io/RequestHeaderParserTest.php | 80 ++++++++ tests/StreamingServerTest.php | 287 --------------------------- 4 files changed, 102 insertions(+), 309 deletions(-) diff --git a/src/Io/RequestHeaderParser.php b/src/Io/RequestHeaderParser.php index 88a554d7..78cb90b0 100644 --- a/src/Io/RequestHeaderParser.php +++ b/src/Io/RequestHeaderParser.php @@ -199,6 +199,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..ece81afe 100644 --- a/src/StreamingServer.php +++ b/src/StreamingServer.php @@ -185,30 +185,10 @@ public function handleRequest(ConnectionInterface $conn, ServerRequestInterface $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; + $stream = new ChunkedDecoder($stream); } 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); - } - + $contentLength = (int)$request->getHeaderLine('Content-Length'); $stream = new LengthLimitedStream($stream, $contentLength); } diff --git a/tests/Io/RequestHeaderParserTest.php b/tests/Io/RequestHeaderParserTest.php index dbf44e69..647fc9ea 100644 --- a/tests/Io/RequestHeaderParserTest.php +++ b/tests/Io/RequestHeaderParserTest.php @@ -423,6 +423,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..6c34f6f3 100644 --- a/tests/StreamingServerTest.php +++ b/tests/StreamingServerTest.php @@ -1561,72 +1561,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 +1707,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 +2187,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) { From 16bd9204cb70ae0da9bd6ba10d2227183e749d85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 16 Jun 2019 19:01:50 +0200 Subject: [PATCH 298/456] Refactor to move request body delimiting to request parser --- src/Io/RequestHeaderParser.php | 27 ++++- src/StreamingServer.php | 24 +--- tests/Io/RequestHeaderParserTest.php | 164 ++++++++++++++++++++------- 3 files changed, 147 insertions(+), 68 deletions(-) diff --git a/src/Io/RequestHeaderParser.php b/src/Io/RequestHeaderParser.php index 78cb90b0..cb1b3dcd 100644 --- a/src/Io/RequestHeaderParser.php +++ b/src/Io/RequestHeaderParser.php @@ -69,9 +69,34 @@ public function handle(ConnectionInterface $conn) return; } + $contentLength = 0; + $stream = new CloseProtectionStream($conn); + if ($request->hasHeader('Transfer-Encoding')) { + $contentLength = null; + $stream = new ChunkedDecoder($stream); + } elseif ($request->hasHeader('Content-Length')) { + $contentLength = (int)$request->getHeaderLine('Content-Length'); + } + + if ($contentLength !== null) { + $stream = new LengthLimitedStream($stream, $contentLength); + } + + $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) { diff --git a/src/StreamingServer.php b/src/StreamingServer.php index ece81afe..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,18 +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')) { - $contentLength = null; - $stream = new ChunkedDecoder($stream); - } elseif ($request->hasHeader('Content-Length')) { - $contentLength = (int)$request->getHeaderLine('Content-Length'); - $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"); } @@ -217,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/RequestHeaderParserTest.php b/tests/Io/RequestHeaderParserTest.php index 647fc9ea..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('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("\0x01\0x02\0x03\0x04\0x05", $bodyBuffer); + $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; From 658ca6839efb37b62faaf64020c66f5c5fcbbca0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Wed, 19 Jun 2019 09:25:47 +0200 Subject: [PATCH 299/456] Optimize handling empty requst body --- src/Io/EmptyBodyStream.php | 142 +++++++++++++++++++++++++++++ src/Io/RequestHeaderParser.php | 22 +++-- tests/Io/EmptyBodyStreamTest.php | 152 +++++++++++++++++++++++++++++++ tests/StreamingServerTest.php | 7 +- 4 files changed, 315 insertions(+), 8 deletions(-) create mode 100644 src/Io/EmptyBodyStream.php create mode 100644 tests/Io/EmptyBodyStreamTest.php 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 cb1b3dcd..adcc184a 100644 --- a/src/Io/RequestHeaderParser.php +++ b/src/Io/RequestHeaderParser.php @@ -70,20 +70,28 @@ public function handle(ConnectionInterface $conn) } $contentLength = 0; - $stream = new CloseProtectionStream($conn); if ($request->hasHeader('Transfer-Encoding')) { $contentLength = null; - $stream = new ChunkedDecoder($stream); } elseif ($request->hasHeader('Content-Length')) { $contentLength = (int)$request->getHeaderLine('Content-Length'); } - if ($contentLength !== null) { - $stream = new LengthLimitedStream($stream, $contentLength); + 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)); } - $request = $request->withBody(new HttpBodyStream($stream, $contentLength)); - $bodyBuffer = isset($buffer[$endOfHeader + 4]) ? \substr($buffer, $endOfHeader + 4) : ''; $buffer = ''; $that->emit('headers', array($request, $conn)); @@ -160,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'])) { 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/StreamingServerTest.php b/tests/StreamingServerTest.php index 6c34f6f3..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)); } From 35cc108deb59b1d0c780b63f69837e53e55133b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 11 Aug 2019 21:00:37 +0200 Subject: [PATCH 300/456] Change parser to use single regular expression to match all headers --- src/Io/RequestHeaderParser.php | 177 ++++++++++++--------------- tests/Io/RequestHeaderParserTest.php | 46 ++++++- 2 files changed, 123 insertions(+), 100 deletions(-) diff --git a/src/Io/RequestHeaderParser.php b/src/Io/RequestHeaderParser.php index adcc184a..f7f77e7e 100644 --- a/src/Io/RequestHeaderParser.php +++ b/src/Io/RequestHeaderParser.php @@ -5,7 +5,6 @@ use Evenement\EventEmitter; use Psr\Http\Message\ServerRequestInterface; use React\Socket\ConnectionInterface; -use RingCentral\Psr7 as g7; use Exception; /** @@ -56,7 +55,7 @@ public function handle(ConnectionInterface $conn) try { $request = $that->parseRequest( - (string)\substr($buffer, 0, $endOfHeader), + (string)\substr($buffer, 0, $endOfHeader + 2), $conn->getRemoteAddress(), $conn->getLocalAddress() ); @@ -124,32 +123,36 @@ public function parseRequest($headers, $remoteSocketUri, $localSocketUri) { // additional, stricter safe-guard for request line // because request parser doesn't properly cope with invalid ones - if (!\preg_match('#^[^ ]+ [^ ]+ HTTP/\d\.\d#m', $headers)) { + $start = array(); + if (!\preg_match('#^(?[^ ]+) (?[^ ]+) HTTP/(?\d\.\d)#m', $headers, $start)) { throw new \InvalidArgumentException('Unable to parse invalid request-line'); } - // parser does not support asterisk-form and authority-form - // remember original target and temporarily replace and re-apply below - $originalTarget = null; - if (\strncmp($headers, 'OPTIONS * ', 10) === 0) { - $originalTarget = '*'; - $headers = 'OPTIONS / ' . \substr($headers, 10); - } elseif (\strncmp($headers, 'CONNECT ', 8) === 0) { - $parts = \explode(' ', $headers, 3); - $uri = \parse_url('tcp://' . $parts[1]); + // 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', 505); + } - // check this is a valid authority-form request-target (host:port) - if (isset($uri['scheme'], $uri['host'], $uri['port']) && count($uri) === 3) { - $originalTarget = $parts[1]; - $parts[1] = 'http://' . $parts[1] . '/'; - $headers = implode(' ', $parts); - } else { - throw new \InvalidArgumentException('CONNECT method MUST use authority-form request target'); - } + // 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'); } - // parse request headers into obj implementing RequestInterface - $request = g7\parse_request($headers); + // 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]; + } + } // create new obj implementing ServerRequestInterface by preserving all // previous properties and restoring original request-target @@ -158,6 +161,48 @@ public function parseRequest($headers, $remoteSocketUri, $localSocketUri) 'REQUEST_TIME_FLOAT' => \microtime(true) ); + // scheme is `http` unless TLS is used + $localParts = \parse_url($localSocketUri); + if (isset($localParts['scheme']) && $localParts['scheme'] === 'tls') { + $scheme = 'https://'; + $serverParams['HTTPS'] = 'on'; + } else { + $scheme = 'http://'; + } + + // default host if unset comes from local socket address or defaults to localhost + if ($host === null) { + $host = isset($localParts['host'], $localParts['port']) ? $localParts['host'] . ':' . $localParts['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']; + } + } + // apply REMOTE_ADDR and REMOTE_PORT if source address is known // address should always be known, unless this is over Unix domain sockets (UDS) if ($remoteSocketUri !== null) { @@ -169,51 +214,23 @@ 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. - if ($localSocketUri !== null) { - $localAddress = \parse_url($localSocketUri); - if (isset($localAddress['host'], $localAddress['port'])) { - $serverParams['SERVER_ADDR'] = $localAddress['host']; - $serverParams['SERVER_PORT'] = $localAddress['port']; - } - if (isset($localAddress['scheme']) && $localAddress['scheme'] === 'tls') { - $serverParams['HTTPS'] = 'on'; - } + if ($localSocketUri !== null && isset($localParts['host'], $localParts['port'])) { + $serverParams['SERVER_ADDR'] = $localParts['host']; + $serverParams['SERVER_PORT'] = $localParts['port']; } - $target = $request->getRequestTarget(); $request = new ServerRequest( - $request->getMethod(), - $request->getUri(), - $request->getHeaders(), - $request->getBody(), - $request->getProtocolVersion(), + $start['method'], + $uri, + $fields, + null, + $start['version'], $serverParams ); - $request = $request->withRequestTarget($target); - - // re-apply actual request target from above - if ($originalTarget !== null) { - $request = $request->withUri( - $request->getUri()->withPath(''), - true - )->withRequestTarget($originalTarget); - } - - // only support HTTP/1.1 and HTTP/1.0 requests - $protocolVersion = $request->getProtocolVersion(); - if ($protocolVersion !== '1.1' && $protocolVersion !== '1.0') { - throw new \InvalidArgumentException('Received request with invalid protocol version', 505); - } - - // ensure absolute-form request-target contains a valid URI - $requestTarget = $request->getRequestTarget(); - if (\strpos($requestTarget, '://') !== false && \substr($requestTarget, 0, 1) !== '/') { - $parts = \parse_url($requestTarget); - // 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'); - } + // 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']); } // Optional Host header value MUST be valid (host and optional port) @@ -252,44 +269,6 @@ public function parseRequest($headers, $remoteSocketUri, $localSocketUri) } } - // set URI components from socket address if not already filled via Host header - if ($request->getUri()->getHost() === '') { - $parts = \parse_url($localSocketUri); - if (!isset($parts['host'], $parts['port'])) { - $parts = array('host' => '127.0.0.1', 'port' => 80); - } - - $request = $request->withUri( - $request->getUri()->withScheme('http')->withHost($parts['host'])->withPort($parts['port']), - true - ); - } - - // Do not assume this is HTTPS when this happens to be port 443 - // detecting HTTPS is left up to the socket layer (TLS detection) - if ($request->getUri()->getScheme() === 'https') { - $request = $request->withUri( - $request->getUri()->withScheme('http')->withPort(443), - true - ); - } - - // Update request URI to "https" scheme if the connection is encrypted - $parts = \parse_url($localSocketUri); - if (isset($parts['scheme']) && $parts['scheme'] === 'tls') { - // The request URI may omit default ports here, so try to parse port - // from Host header field (if possible) - $port = $request->getUri()->getPort(); - if ($port === null) { - $port = \parse_url('tcp://' . $request->getHeaderLine('Host'), PHP_URL_PORT); // @codeCoverageIgnore - } - - $request = $request->withUri( - $request->getUri()->withScheme('https')->withPort($port), - true - ); - } - // always sanitize Host header because it contains critical routing information $request = $request->withUri($request->getUri()->withUserInfo('u')->withUserInfo('')); diff --git a/tests/Io/RequestHeaderParserTest.php b/tests/Io/RequestHeaderParserTest.php index 8bbb1603..baf7215a 100644 --- a/tests/Io/RequestHeaderParserTest.php +++ b/tests/Io/RequestHeaderParserTest.php @@ -340,6 +340,50 @@ public function testInvalidMalformedRequestLineParseException() $this->assertSame('Unable to parse invalid request-line', $error->getMessage()); } + /** + * @group a + */ + public function testInvalidMalformedRequestHeadersThrowsParseException() + { + $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\nHost : yes\r\n\r\n")); + + $this->assertInstanceOf('InvalidArgumentException', $error); + $this->assertSame('Unable to parse invalid request header fields', $error->getMessage()); + } + + /** + * @group a + */ + public function testInvalidMalformedRequestHeadersWhitespaceThrowsParseException() + { + $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\nHost: yes\rFoo: bar\r\n\r\n")); + + $this->assertInstanceOf('InvalidArgumentException', $error); + $this->assertSame('Unable to parse invalid request header fields', $error->getMessage()); + } + public function testInvalidAbsoluteFormSchemeEmitsError() { $error = null; @@ -400,7 +444,7 @@ public function testUriStartingWithColonSlashSlashFails() $connection->emit('data', array("GET ://example.com:80/ HTTP/1.0\r\n\r\n")); $this->assertInstanceOf('InvalidArgumentException', $error); - $this->assertSame('Invalid request string', $error->getMessage()); + $this->assertSame('Invalid absolute-form request-target', $error->getMessage()); } public function testInvalidAbsoluteFormWithFragmentEmitsError() From 5f2ce4da6d30779ab1e6b95a0afac3e0e5595f64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 29 Oct 2019 15:17:24 +0100 Subject: [PATCH 301/456] Prepare v0.8.5 release --- CHANGELOG.md | 10 ++++++++++ README.md | 22 +++++++++++----------- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5099d6b8..5f0980ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## 0.8.5 (2019-10-29) + +* Internal refactorings and optimizations to improve request parsing performance. + Benchmarks suggest number of requests/s improved by ~30% for common `GET` requests. + (#345, #346, #349 and #350 by @clue) + +* Add documentation and example for JSON/XML request body and + improve documentation for concurrency and streaming requests and for error handling. + (#341 and #342 by @clue) + ## 0.8.4 (2019-01-16) * Improvement: Internal refactoring to simplify response header logic. diff --git a/README.md b/README.md index 7eb0ae0e..4fb2febe 100644 --- a/README.md +++ b/README.md @@ -371,7 +371,7 @@ $server = new Server(function (ServerRequestInterface $request) { The response in the above example will return a response body with a link. The URL contains the query parameter `foo` with the value `bar`. -Use [`htmlentities`](http://php.net/manual/en/function.htmlentities.php) +Use [`htmlentities`](https://www.php.net/manual/en/function.htmlentities.php) like in this example to prevent [Cross-Site Scripting (abbreviated as XSS)](https://en.wikipedia.org/wiki/Cross-site_scripting). @@ -1054,7 +1054,7 @@ A middleware request handler is expected to adhere the following rules: * It returns either: * An instance implementing `ResponseInterface` for direct consumption. * Any promise which can be consumed by - [`Promise\resolve()`](http://reactphp.org/promise/#resolve) resolving to a + [`Promise\resolve()`](https://reactphp.org/promise/#resolve) resolving to a `ResponseInterface` for deferred consumption. * It MAY throw an `Exception` (or return a rejected promise) in order to signal an error condition and abort the chain. @@ -1113,7 +1113,7 @@ Note that as per the above documentation, the `$next` middleware request handler `ResponseInterface` directly or one wrapped in a promise for deferred resolution. In order to simplify handling both paths, you can simply wrap this in a -[`Promise\resolve()`](http://reactphp.org/promise/#resolve) call like this: +[`Promise\resolve()`](https://reactphp.org/promise/#resolve) call like this: ```php $server = new Server(array( @@ -1135,7 +1135,7 @@ The previous example does not catch any exceptions and would thus signal an error condition to the `Server`. Alternatively, you can also catch any `Exception` to implement custom error handling logic (or logging etc.) by wrapping this in a -[`Promise`](http://reactphp.org/promise/#promise) like this: +[`Promise`](https://reactphp.org/promise/#promise) like this: ```php $server = new Server(array( @@ -1326,7 +1326,7 @@ $server = new StreamingServer(array(( See also [example #12](examples) for more details. By default, this middleware respects the -[`upload_max_filesize`](http://php.net/manual/en/ini.core.php#ini.upload-max-filesize) +[`upload_max_filesize`](https://www.php.net/manual/en/ini.core.php#ini.upload-max-filesize) (default `2M`) ini setting. Files that exceed this limit will be rejected with an `UPLOAD_ERR_INI_SIZE` error. You can control the maximum filesize for each individual file upload by @@ -1338,9 +1338,9 @@ new RequestBodyParserMiddleware(8 * 1024 * 1024); // 8 MiB limit per file ``` By default, this middleware respects the -[`file_uploads`](http://php.net/manual/en/ini.core.php#ini.file-uploads) +[`file_uploads`](https://www.php.net/manual/en/ini.core.php#ini.file-uploads) (default `1`) and -[`max_file_uploads`](http://php.net/manual/en/ini.core.php#ini.max-file-uploads) +[`max_file_uploads`](https://www.php.net/manual/en/ini.core.php#ini.max-file-uploads) (default `20`) ini settings. These settings control if any and how many files can be uploaded in a single request. If you upload more files in a single request, additional files will be ignored @@ -1370,13 +1370,13 @@ new RequestBodyParserMiddleware(10 * 1024, 100); // 100 files with 10 KiB each Files that exceed this limit will be rejected with an `UPLOAD_ERR_FORM_SIZE` error. > This middleware respects the - [`max_input_vars`](http://php.net/manual/en/info.configuration.php#ini.max-input-vars) + [`max_input_vars`](https://www.php.net/manual/en/info.configuration.php#ini.max-input-vars) (default `1000`) and - [`max_input_nesting_level`](http://php.net/manual/en/info.configuration.php#ini.max-input-nesting-level) + [`max_input_nesting_level`](https://www.php.net/manual/en/info.configuration.php#ini.max-input-nesting-level) (default `64`) ini settings. > Note that this middleware ignores the - [`enable_post_data_reading`](http://php.net/manual/en/ini.core.php#ini.enable-post-data-reading) + [`enable_post_data_reading`](https://www.php.net/manual/en/ini.core.php#ini.enable-post-data-reading) (default `1`) ini setting because it makes little sense to respect here and is left up to higher-level implementations. If you want to respect this setting, you have to check its value and @@ -1418,7 +1418,7 @@ The recommended way to install this library is [through Composer](https://getcom This will install the latest supported version: ```bash -$ composer require react/http:^0.8.4 +$ composer require react/http:^0.8.5 ``` See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. From 5894f24ff23c988ef924c8529c3e1357b08e21fc Mon Sep 17 00:00:00 2001 From: fisk Date: Sat, 30 Nov 2019 09:59:22 +0000 Subject: [PATCH 302/456] Fix handling of multiple cookie headers and comma values --- src/Io/ServerRequest.php | 13 ++++++++----- tests/StreamingServerTest.php | 19 +++++++++++++++++++ 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/Io/ServerRequest.php b/src/Io/ServerRequest.php index 787960dd..28a8c5db 100644 --- a/src/Io/ServerRequest.php +++ b/src/Io/ServerRequest.php @@ -57,7 +57,13 @@ public function __construct( \parse_str($query, $this->queryParams); } - $this->cookies = $this->parseCookie($this->getHeaderLine('Cookie')); + // Multiple cookie headers are not allowed according + // to https://tools.ietf.org/html/rfc6265#section-5.4 + $cookieHeaders = $this->getHeader("Cookie"); + + if (count($cookieHeaders) === 1) { + $this->cookies = $this->parseCookie($cookieHeaders[0]); + } } public function getServerParams() @@ -146,10 +152,7 @@ public function withoutAttribute($name) */ private function parseCookie($cookie) { - // PSR-7 `getHeaderLine('Cookie')` will return multiple - // cookie header comma-seperated. Multiple cookie headers - // are not allowed according to https://tools.ietf.org/html/rfc6265#section-5.4 - if ($cookie === '' || \strpos($cookie, ',') !== false) { + if ($cookie === '') { return array(); } diff --git a/tests/StreamingServerTest.php b/tests/StreamingServerTest.php index 1e7f7330..8423e3ea 100644 --- a/tests/StreamingServerTest.php +++ b/tests/StreamingServerTest.php @@ -2790,6 +2790,25 @@ public function testRequestCookieWithSeparatorWillBeAddedToServerRequest() $this->assertEquals(array('hello' => 'world', 'test' => 'abc'), $requestValidation->getCookieParams()); } + public function testRequestCookieWithCommaValueWillBeAddedToServerRequest() { + $requestValidation = null; + $server = new StreamingServer(function (ServerRequestInterface $request) use (&$requestValidation) { + $requestValidation = $request; + }); + + $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 .= "Cookie: test=abc,def; hello=world\r\n"; + $data .= "\r\n"; + + $this->connection->emit('data', array($data)); + $this->assertEquals(array('test' => 'abc,def', 'hello' => 'world'), $requestValidation->getCookieParams()); + } + private function createGetRequest() { $data = "GET / HTTP/1.1\r\n"; From 2aa1a90a97f458c2a18bf4e9afb5ac720e771485 Mon Sep 17 00:00:00 2001 From: Sam Reed Date: Sun, 1 Dec 2019 01:59:57 +0000 Subject: [PATCH 303/456] Add .gitattributes to exclude dev files from exports --- .gitattributes | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..f2f51ddf --- /dev/null +++ b/.gitattributes @@ -0,0 +1,6 @@ +/.gitattributes export-ignore +/.gitignore export-ignore +/.travis.yml export-ignore +/examples export-ignore +/phpunit.xml.dist export-ignore +/tests export-ignore From 3cf9dc0eb7bb87ed30c7af84a74d04e320182898 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Sat, 11 Jan 2020 21:37:38 +0100 Subject: [PATCH 304/456] Avoid unneeded warning when decoding invalid data on PHP 7.4 --- .travis.yml | 24 ++++++++++-------------- composer.json | 2 +- phpunit.xml.dist | 1 - src/Io/ChunkedDecoder.php | 2 +- 4 files changed, 12 insertions(+), 17 deletions(-) diff --git a/.travis.yml b/.travis.yml index a881bc0f..59b46fbd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,35 +1,31 @@ language: php -php: -# - 5.3 # requires old distro, see below - - 5.4 - - 5.5 - - 5.6 - - 7.0 - - 7.1 - - 7.2 - - hhvm # ignore errors, see below - # lock distro so new future defaults will not break the build dist: trusty -# also test lowest dependencies on PHP 7 matrix: include: - php: 5.3 dist: precise + - php: 5.4 + - php: 5.5 + - php: 5.6 + - php: 7.0 - php: 7.0 env: - DEPENDENCIES=lowest + - php: 7.1 + - php: 7.2 + - php: 7.3 + - php: 7.4 + - php: hhvm allow_failures: - php: hhvm -sudo: false - install: - composer install --no-interaction - if [ "$DEPENDENCIES" = "lowest" ]; then composer update --prefer-lowest -n; fi - + script: - ./vendor/bin/phpunit --coverage-text - if [ "$DEPENDENCIES" = "lowest" ]; then php -n tests/benchmark-middleware-runner.php; fi diff --git a/composer.json b/composer.json index c3d46a9f..ab26ee88 100644 --- a/composer.json +++ b/composer.json @@ -19,6 +19,6 @@ }, "require-dev": { "clue/block-react": "^1.1", - "phpunit/phpunit": "^6.4 || ^5.7 || ^4.8.35" + "phpunit/phpunit": "^7.0 || ^6.4 || ^5.7 || ^4.8.35" } } diff --git a/phpunit.xml.dist b/phpunit.xml.dist index cba6d4dd..79c0ee66 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -8,7 +8,6 @@ convertWarningsToExceptions="true" processIsolation="false" stopOnFailure="false" - syntaxCheck="false" bootstrap="tests/bootstrap.php" > diff --git a/src/Io/ChunkedDecoder.php b/src/Io/ChunkedDecoder.php index 4625349b..f7bbe603 100644 --- a/src/Io/ChunkedDecoder.php +++ b/src/Io/ChunkedDecoder.php @@ -122,7 +122,7 @@ public function handleData($data) } } - $this->chunkSize = \hexdec($hexValue); + $this->chunkSize = @\hexdec($hexValue); if (\dechex($this->chunkSize) !== $hexValue) { $this->handleError(new Exception($hexValue . ' is not a valid hexadecimal number')); return; From aeef894447a2f935294003b1433c6db3b9fb2ffb Mon Sep 17 00:00:00 2001 From: Eddie Bimmel Date: Fri, 26 Jun 2020 10:53:23 +0200 Subject: [PATCH 305/456] Greedy matching caused boundary string to contain quotation mark, making it invalid for determining part boundaries --- src/Io/MultipartParser.php | 2 +- tests/Io/MultipartParserTest.php | 33 ++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/Io/MultipartParser.php b/src/Io/MultipartParser.php index 8749b6c5..c9edf43a 100644 --- a/src/Io/MultipartParser.php +++ b/src/Io/MultipartParser.php @@ -93,7 +93,7 @@ public function __construct($uploadMaxFilesize = null, $maxFileUploads = null) public function parse(ServerRequestInterface $request) { $contentType = $request->getHeaderLine('content-type'); - if(!\preg_match('/boundary="?(.*)"?$/', $contentType, $matches)) { + if(!\preg_match('/boundary="?(.*?)"?$/', $contentType, $matches)) { return $request; } diff --git a/tests/Io/MultipartParserTest.php b/tests/Io/MultipartParserTest.php index 45ac0c4d..2b883889 100644 --- a/tests/Io/MultipartParserTest.php +++ b/tests/Io/MultipartParserTest.php @@ -66,6 +66,39 @@ public function testPostKey() ); } + public function testPostWithQuotationMarkEncapsulatedBoundary() + { + $boundary = "---------------------------5844729766471062541057622570"; + + $data = "--$boundary\r\n"; + $data .= "Content-Disposition: form-data; name=\"users[one]\"\r\n"; + $data .= "\r\n"; + $data .= "single\r\n"; + $data .= "--$boundary\r\n"; + $data .= "Content-Disposition: form-data; name=\"users[two]\"\r\n"; + $data .= "\r\n"; + $data .= "second\r\n"; + $data .= "--$boundary--\r\n"; + + $request = new ServerRequest('POST', 'http://example.com/', array( + 'Content-Type' => 'multipart/form-data; boundary="' . $boundary . '"', + ), $data, 1.1); + + $parser = new MultipartParser(); + $parsedRequest = $parser->parse($request); + + $this->assertEmpty($parsedRequest->getUploadedFiles()); + $this->assertSame( + array( + 'users' => array( + 'one' => 'single', + 'two' => 'second', + ), + ), + $parsedRequest->getParsedBody() + ); + } + public function testPostStringOverwritesMap() { $boundary = "---------------------------5844729766471062541057622570"; From f07a923d105051c57e4ada03296bf67f9bb13fb0 Mon Sep 17 00:00:00 2001 From: Eddie Bimmel Date: Fri, 26 Jun 2020 11:14:08 +0200 Subject: [PATCH 306/456] Regex would only work for header parameters that are encapsulate between quotation marks, causing form field names without not to be matched. --- src/Io/MultipartParser.php | 2 +- tests/Io/MultipartParserTest.php | 33 ++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/Io/MultipartParser.php b/src/Io/MultipartParser.php index c9edf43a..d868ca88 100644 --- a/src/Io/MultipartParser.php +++ b/src/Io/MultipartParser.php @@ -278,7 +278,7 @@ private function parseHeaders($header) private function getParameterFromHeader(array $header, $parameter) { foreach ($header as $part) { - if (\preg_match('/' . $parameter . '="?(.*)"$/', $part, $matches)) { + if (\preg_match('/' . $parameter . '="?(.*?)"?$/', $part, $matches)) { return $matches[1]; } } diff --git a/tests/Io/MultipartParserTest.php b/tests/Io/MultipartParserTest.php index 2b883889..91ec832d 100644 --- a/tests/Io/MultipartParserTest.php +++ b/tests/Io/MultipartParserTest.php @@ -99,6 +99,39 @@ public function testPostWithQuotationMarkEncapsulatedBoundary() ); } + public function testPostFormDataNamesWithoutQuotationMark() + { + $boundary = "---------------------------5844729766471062541057622570"; + + $data = "--$boundary\r\n"; + $data .= "Content-Disposition: form-data; name=users[one]\r\n"; + $data .= "\r\n"; + $data .= "single\r\n"; + $data .= "--$boundary\r\n"; + $data .= "Content-Disposition: form-data; name=users[two]\r\n"; + $data .= "\r\n"; + $data .= "second\r\n"; + $data .= "--$boundary--\r\n"; + + $request = new ServerRequest('POST', 'http://example.com/', array( + 'Content-Type' => 'multipart/form-data; boundary="' . $boundary . '"', + ), $data, 1.1); + + $parser = new MultipartParser(); + $parsedRequest = $parser->parse($request); + + $this->assertEmpty($parsedRequest->getUploadedFiles()); + $this->assertSame( + array( + 'users' => array( + 'one' => 'single', + 'two' => 'second', + ), + ), + $parsedRequest->getParsedBody() + ); + } + public function testPostStringOverwritesMap() { $boundary = "---------------------------5844729766471062541057622570"; From 390ca707ecdf097a0039b99bff2210a204ef7063 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Sun, 28 Jun 2020 18:23:31 +0200 Subject: [PATCH 307/456] Update for v0.8.6 release --- CHANGELOG.md | 6 ++++++ README.md | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f0980ce..d1564383 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 0.8.6 (2020-01-12) + +* Fix parsing Cookie request header with comma in its values (#352 by @fiskie) +* Add .gitattributes to exclude dev files from exports (#353 by @reedy) +* Avoid unneeded warning when decoding invalid data on PHP 7.4 (#357 by @WyriHaximus) + ## 0.8.5 (2019-10-29) * Internal refactorings and optimizations to improve request parsing performance. diff --git a/README.md b/README.md index 4fb2febe..36d80d98 100644 --- a/README.md +++ b/README.md @@ -1418,7 +1418,7 @@ The recommended way to install this library is [through Composer](https://getcom This will install the latest supported version: ```bash -$ composer require react/http:^0.8.5 +$ composer require react/http:^0.8.6 ``` See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. From 4d91caecd93c10fb08a77c30854ea820299f3fa0 Mon Sep 17 00:00:00 2001 From: Simon Frings Date: Tue, 30 Jun 2020 10:19:24 +0200 Subject: [PATCH 308/456] Run tests on PHPUnit 9 --- composer.json | 2 +- tests/CallableStub.php | 10 -- tests/FunctionalServerTest.php | 52 +++--- tests/Io/ChunkedDecoderTest.php | 6 +- tests/Io/ChunkedEncoderTest.php | 5 +- tests/Io/EmptyBodyStreamTest.php | 29 ++-- tests/Io/HttpBodyStreamTest.php | 29 ++-- tests/Io/IniUtilTest.php | 2 +- tests/Io/LengthLimitedStreamTest.php | 5 +- tests/Io/MiddlewareRunnerTest.php | 13 +- tests/Io/ServerRequestTest.php | 5 +- tests/Io/UploadedFileTest.php | 16 +- .../LimitConcurrentRequestsMiddlewareTest.php | 8 +- .../RequestBodyBufferMiddlewareTest.php | 4 +- tests/ServerTest.php | 9 +- tests/StreamingServerTest.php | 151 +++++++++--------- tests/TestCase.php | 50 +++++- 17 files changed, 205 insertions(+), 191 deletions(-) delete mode 100644 tests/CallableStub.php diff --git a/composer.json b/composer.json index ab26ee88..5330d775 100644 --- a/composer.json +++ b/composer.json @@ -19,6 +19,6 @@ }, "require-dev": { "clue/block-react": "^1.1", - "phpunit/phpunit": "^7.0 || ^6.4 || ^5.7 || ^4.8.35" + "phpunit/phpunit": "^9.0 || ^5.7 || ^4.8.35" } } diff --git a/tests/CallableStub.php b/tests/CallableStub.php deleted file mode 100644 index cfb8acfa..00000000 --- a/tests/CallableStub.php +++ /dev/null @@ -1,10 +0,0 @@ -assertContains("HTTP/1.0 200 OK", $response); - $this->assertContains('http://' . noScheme($socket->getAddress()) . '/', $response); + $this->assertContainsString("HTTP/1.0 200 OK", $response); + $this->assertContainsString('http://' . noScheme($socket->getAddress()) . '/', $response); $socket->close(); } @@ -68,7 +68,7 @@ function () { $response = Block\await($result, $loop, 1.0); - $this->assertContains("HTTP/1.0 404 Not Found", $response); + $this->assertContainsString("HTTP/1.0 404 Not Found", $response); $socket->close(); } @@ -93,8 +93,8 @@ public function testPlainHttpOnRandomPortWithoutHostHeaderUsesSocketUri() $response = Block\await($result, $loop, 1.0); - $this->assertContains("HTTP/1.0 200 OK", $response); - $this->assertContains('http://' . noScheme($socket->getAddress()) . '/', $response); + $this->assertContainsString("HTTP/1.0 200 OK", $response); + $this->assertContainsString('http://' . noScheme($socket->getAddress()) . '/', $response); $socket->close(); } @@ -119,8 +119,8 @@ public function testPlainHttpOnRandomPortWithOtherHostHeaderTakesPrecedence() $response = Block\await($result, $loop, 1.0); - $this->assertContains("HTTP/1.0 200 OK", $response); - $this->assertContains('http://localhost:1000/', $response); + $this->assertContainsString("HTTP/1.0 200 OK", $response); + $this->assertContainsString('http://localhost:1000/', $response); $socket->close(); } @@ -154,8 +154,8 @@ public function testSecureHttpsOnRandomPort() $response = Block\await($result, $loop, 1.0); - $this->assertContains("HTTP/1.0 200 OK", $response); - $this->assertContains('https://' . noScheme($socket->getAddress()) . '/', $response); + $this->assertContainsString("HTTP/1.0 200 OK", $response); + $this->assertContainsString('https://' . noScheme($socket->getAddress()) . '/', $response); $socket->close(); } @@ -194,8 +194,8 @@ public function testSecureHttpsReturnsData() $response = Block\await($result, $loop, 1.0); - $this->assertContains("HTTP/1.0 200 OK", $response); - $this->assertContains("\r\nContent-Length: 33000\r\n", $response); + $this->assertContainsString("HTTP/1.0 200 OK", $response); + $this->assertContainsString("\r\nContent-Length: 33000\r\n", $response); $this->assertStringEndsWith("\r\n". str_repeat('.', 33000), $response); $socket->close(); @@ -230,8 +230,8 @@ public function testSecureHttpsOnRandomPortWithoutHostHeaderUsesSocketUri() $response = Block\await($result, $loop, 1.0); - $this->assertContains("HTTP/1.0 200 OK", $response); - $this->assertContains('https://' . noScheme($socket->getAddress()) . '/', $response); + $this->assertContainsString("HTTP/1.0 200 OK", $response); + $this->assertContainsString('https://' . noScheme($socket->getAddress()) . '/', $response); $socket->close(); } @@ -260,8 +260,8 @@ public function testPlainHttpOnStandardPortReturnsUriWithNoPort() $response = Block\await($result, $loop, 1.0); - $this->assertContains("HTTP/1.0 200 OK", $response); - $this->assertContains('http://127.0.0.1/', $response); + $this->assertContainsString("HTTP/1.0 200 OK", $response); + $this->assertContainsString('http://127.0.0.1/', $response); $socket->close(); } @@ -290,8 +290,8 @@ public function testPlainHttpOnStandardPortWithoutHostHeaderReturnsUriWithNoPort $response = Block\await($result, $loop, 1.0); - $this->assertContains("HTTP/1.0 200 OK", $response); - $this->assertContains('http://127.0.0.1/', $response); + $this->assertContainsString("HTTP/1.0 200 OK", $response); + $this->assertContainsString('http://127.0.0.1/', $response); $socket->close(); } @@ -329,8 +329,8 @@ public function testSecureHttpsOnStandardPortReturnsUriWithNoPort() $response = Block\await($result, $loop, 1.0); - $this->assertContains("HTTP/1.0 200 OK", $response); - $this->assertContains('https://127.0.0.1/', $response); + $this->assertContainsString("HTTP/1.0 200 OK", $response); + $this->assertContainsString('https://127.0.0.1/', $response); $socket->close(); } @@ -368,8 +368,8 @@ public function testSecureHttpsOnStandardPortWithoutHostHeaderUsesSocketUri() $response = Block\await($result, $loop, 1.0); - $this->assertContains("HTTP/1.0 200 OK", $response); - $this->assertContains('https://127.0.0.1/', $response); + $this->assertContainsString("HTTP/1.0 200 OK", $response); + $this->assertContainsString('https://127.0.0.1/', $response); $socket->close(); } @@ -398,8 +398,8 @@ public function testPlainHttpOnHttpsStandardPortReturnsUriWithPort() $response = Block\await($result, $loop, 1.0); - $this->assertContains("HTTP/1.0 200 OK", $response); - $this->assertContains('http://127.0.0.1:443/', $response); + $this->assertContainsString("HTTP/1.0 200 OK", $response); + $this->assertContainsString('http://127.0.0.1:443/', $response); $socket->close(); } @@ -437,8 +437,8 @@ public function testSecureHttpsOnHttpStandardPortReturnsUriWithPort() $response = Block\await($result, $loop, 1.0); - $this->assertContains("HTTP/1.0 200 OK", $response); - $this->assertContains('https://127.0.0.1:80/', $response); + $this->assertContainsString("HTTP/1.0 200 OK", $response); + $this->assertContainsString('https://127.0.0.1:80/', $response); $socket->close(); } @@ -784,7 +784,7 @@ function (ServerRequestInterface $request) { $responses = Block\await(Promise\all($result), $loop, 1.0); foreach ($responses as $response) { - $this->assertContains("HTTP/1.0 200 OK", $response, $response); + $this->assertContainsString("HTTP/1.0 200 OK", $response, $response); $this->assertTrue(substr($response, -4) == 1024, $response); } diff --git a/tests/Io/ChunkedDecoderTest.php b/tests/Io/ChunkedDecoderTest.php index 72a98a70..c7889b3f 100644 --- a/tests/Io/ChunkedDecoderTest.php +++ b/tests/Io/ChunkedDecoderTest.php @@ -8,7 +8,11 @@ class ChunkedDecoderTest extends TestCase { - public function setUp() + + /** + * @before + */ + public function setUpParser() { $this->input = new ThroughStream(); $this->parser = new ChunkedDecoder($this->input); diff --git a/tests/Io/ChunkedEncoderTest.php b/tests/Io/ChunkedEncoderTest.php index ef542097..75d43d4a 100644 --- a/tests/Io/ChunkedEncoderTest.php +++ b/tests/Io/ChunkedEncoderTest.php @@ -11,7 +11,10 @@ class ChunkedEncoderTest extends TestCase private $input; private $chunkedStream; - public function setUp() + /** + * @before + */ + public function setUpChunkedStream() { $this->input = new ThroughStream(); $this->chunkedStream = new ChunkedEncoder($this->input); diff --git a/tests/Io/EmptyBodyStreamTest.php b/tests/Io/EmptyBodyStreamTest.php index 6f309fa9..8430239d 100644 --- a/tests/Io/EmptyBodyStreamTest.php +++ b/tests/Io/EmptyBodyStreamTest.php @@ -10,7 +10,10 @@ class EmptyBodyStreamTest extends TestCase private $input; private $bodyStream; - public function setUp() + /** + * @before + */ + public function setUpBodyStream() { $this->bodyStream = new EmptyBodyStream(); } @@ -65,19 +68,15 @@ public function testCloseTwiceEmitsCloseEventAndClearsListeners() $this->assertEquals(array(), $this->bodyStream->listeners('close')); } - /** - * @expectedException BadMethodCallException - */ public function testTell() { + $this->setExpectedException('BadMethodCallException'); $this->bodyStream->tell(); } - /** - * @expectedException BadMethodCallException - */ public function testEof() { + $this->setExpectedException('BadMethodCallException'); $this->bodyStream->eof(); } @@ -86,19 +85,15 @@ public function testIsSeekable() $this->assertFalse($this->bodyStream->isSeekable()); } - /** - * @expectedException BadMethodCallException - */ public function testWrite() { + $this->setExpectedException('BadMethodCallException'); $this->bodyStream->write(''); } - /** - * @expectedException BadMethodCallException - */ public function testRead() { + $this->setExpectedException('BadMethodCallException'); $this->bodyStream->read(1); } @@ -129,19 +124,15 @@ public function testIsReadableReturnsFalseWhenAlreadyClosed() $this->assertFalse($this->bodyStream->isReadable()); } - /** - * @expectedException BadMethodCallException - */ public function testSeek() { + $this->setExpectedException('BadMethodCallException'); $this->bodyStream->seek(''); } - /** - * @expectedException BadMethodCallException - */ public function testRewind() { + $this->setExpectedException('BadMethodCallException'); $this->bodyStream->rewind(); } diff --git a/tests/Io/HttpBodyStreamTest.php b/tests/Io/HttpBodyStreamTest.php index 343a75a5..db21dcf8 100644 --- a/tests/Io/HttpBodyStreamTest.php +++ b/tests/Io/HttpBodyStreamTest.php @@ -11,7 +11,10 @@ class HttpBodyStreamTest extends TestCase private $input; private $bodyStream; - public function setUp() + /** + * @before + */ + public function setUpBodyStream() { $this->input = new ThroughStream(); $this->bodyStream = new HttpBodyStream($this->input, null); @@ -102,19 +105,15 @@ public function testGetSizeCustom() $this->assertEquals(5, $stream->getSize()); } - /** - * @expectedException BadMethodCallException - */ public function testTell() { + $this->setExpectedException('BadMethodCallException'); $this->bodyStream->tell(); } - /** - * @expectedException BadMethodCallException - */ public function testEof() { + $this->setExpectedException('BadMethodCallException'); $this->bodyStream->eof(); } @@ -123,19 +122,15 @@ public function testIsSeekable() $this->assertFalse($this->bodyStream->isSeekable()); } - /** - * @expectedException BadMethodCallException - */ public function testWrite() { + $this->setExpectedException('BadMethodCallException'); $this->bodyStream->write(''); } - /** - * @expectedException BadMethodCallException - */ public function testRead() { + $this->setExpectedException('BadMethodCallException'); $this->bodyStream->read(''); } @@ -154,19 +149,15 @@ public function testIsReadable() $this->assertTrue($this->bodyStream->isReadable()); } - /** - * @expectedException BadMethodCallException - */ public function testSeek() { + $this->setExpectedException('BadMethodCallException'); $this->bodyStream->seek(''); } - /** - * @expectedException BadMethodCallException - */ public function testRewind() { + $this->setExpectedException('BadMethodCallException'); $this->bodyStream->rewind(); } diff --git a/tests/Io/IniUtilTest.php b/tests/Io/IniUtilTest.php index 390abab1..80bc422b 100644 --- a/tests/Io/IniUtilTest.php +++ b/tests/Io/IniUtilTest.php @@ -68,10 +68,10 @@ public function provideInvalidInputIniSizeToBytes() /** * @dataProvider provideInvalidInputIniSizeToBytes - * @expectedException InvalidArgumentException */ public function testInvalidInputIniSizeToBytes($input) { + $this->setExpectedException('InvalidArgumentException'); IniUtil::iniSizeToBytes($input); } } diff --git a/tests/Io/LengthLimitedStreamTest.php b/tests/Io/LengthLimitedStreamTest.php index 9a88ba7c..b415269c 100644 --- a/tests/Io/LengthLimitedStreamTest.php +++ b/tests/Io/LengthLimitedStreamTest.php @@ -11,7 +11,10 @@ class LengthLimitedStreamTest extends TestCase private $input; private $stream; - public function setUp() + /** + * @before + */ + public function setUpInput() { $this->input = new ThroughStream(); } diff --git a/tests/Io/MiddlewareRunnerTest.php b/tests/Io/MiddlewareRunnerTest.php index 91b927c9..f43231b0 100644 --- a/tests/Io/MiddlewareRunnerTest.php +++ b/tests/Io/MiddlewareRunnerTest.php @@ -18,16 +18,13 @@ final class MiddlewareRunnerTest extends TestCase { - /** - * @expectedException RuntimeException - * @expectedExceptionMessage No middleware to run - */ public function testEmptyMiddlewareStackThrowsException() { $request = new ServerRequest('GET', 'https://example.com/'); $middlewares = array(); $middlewareStack = new MiddlewareRunner($middlewares); + $this->setExpectedException('RuntimeException', 'No middleware to run'); $middlewareStack($request); } @@ -68,10 +65,6 @@ function (ServerRequestInterface $request) use (&$args) { $this->assertEquals(1, $args); } - /** - * @expectedException RuntimeException - * @expectedExceptionMessage hello - */ public function testThrowsIfHandlerThrowsException() { $middleware = new MiddlewareRunner(array( @@ -82,13 +75,12 @@ function (ServerRequestInterface $request) { $request = new ServerRequest('GET', 'http://example.com/'); + $this->setExpectedException('RuntimeException', 'hello'); $middleware($request); } /** * @requires PHP 7 - * @expectedException Throwable - * @expectedExceptionMessage hello */ public function testThrowsIfHandlerThrowsThrowable() { @@ -100,6 +92,7 @@ function (ServerRequestInterface $request) { $request = new ServerRequest('GET', 'http://example.com/'); + $this->setExpectedException('Throwable', 'hello'); $middleware($request); } diff --git a/tests/Io/ServerRequestTest.php b/tests/Io/ServerRequestTest.php index 7a7b241a..47346cd1 100644 --- a/tests/Io/ServerRequestTest.php +++ b/tests/Io/ServerRequestTest.php @@ -9,7 +9,10 @@ class ServerRequestTest extends TestCase { private $request; - public function setUp() + /** + * @before + */ + public function setUpRequest() { $this->request = new ServerRequest('GET', 'http://localhost'); } diff --git a/tests/Io/UploadedFileTest.php b/tests/Io/UploadedFileTest.php index 383b686b..9ff623da 100644 --- a/tests/Io/UploadedFileTest.php +++ b/tests/Io/UploadedFileTest.php @@ -20,23 +20,21 @@ public function failtyErrorProvider() /** * @dataProvider failtyErrorProvider - * @expectedException \InvalidArgumentException - * @expectedExceptionMessage Invalid error code, must be an UPLOAD_ERR_* constant */ public function testFailtyError($error) { $stream = new BufferStream(); + + $this->setExpectedException('InvalidArgumentException', 'Invalid error code, must be an UPLOAD_ERR_* constant'); new UploadedFile($stream, 0, $error, 'foo.bar', 'foo/bar'); } - /** - * @expectedException \RuntimeException - * @expectedExceptionMessage Not implemented - */ public function testNoMoveFile() { $stream = new BufferStream(); $uploadedFile = new UploadedFile($stream, 0, UPLOAD_ERR_OK, 'foo.bar', 'foo/bar'); + + $this->setExpectedException('RuntimeException', 'Not implemented'); $uploadedFile->moveTo('bar.foo'); } @@ -51,14 +49,12 @@ public function testGetters() self::assertSame('foo/bar', $uploadedFile->getClientMediaType()); } - /** - * @expectedException \RuntimeException - * @expectedExceptionMessage Cannot retrieve stream due to upload error - */ public function testGetStreamOnFailedUpload() { $stream = new BufferStream(); $uploadedFile = new UploadedFile($stream, 0, UPLOAD_ERR_NO_FILE, 'foo.bar', 'foo/bar'); + + $this->setExpectedException('RuntimeException', 'Cannot retrieve stream due to upload error'); $uploadedFile->getStream(); } } diff --git a/tests/Middleware/LimitConcurrentRequestsMiddlewareTest.php b/tests/Middleware/LimitConcurrentRequestsMiddlewareTest.php index 859b82e7..b4c82390 100644 --- a/tests/Middleware/LimitConcurrentRequestsMiddlewareTest.php +++ b/tests/Middleware/LimitConcurrentRequestsMiddlewareTest.php @@ -107,14 +107,11 @@ public function testReturnsResponseDirectlyFromMiddlewareWhenBelowLimit() $this->assertSame($response, $ret); } - /** - * @expectedException RuntimeException - * @expectedExceptionMessage demo - */ public function testThrowsExceptionDirectlyFromMiddlewareWhenBelowLimit() { $middleware = new LimitConcurrentRequestsMiddleware(1); + $this->setExpectedException('RuntimeException', 'demo'); $middleware(new ServerRequest('GET', 'https://example.com/'), function () { throw new \RuntimeException('demo'); }); @@ -122,13 +119,12 @@ public function testThrowsExceptionDirectlyFromMiddlewareWhenBelowLimit() /** * @requires PHP 7 - * @expectedException Error - * @expectedExceptionMessage demo */ public function testThrowsErrorDirectlyFromMiddlewareWhenBelowLimit() { $middleware = new LimitConcurrentRequestsMiddleware(1); + $this->setExpectedException('Error', 'demo'); $middleware(new ServerRequest('GET', 'https://example.com/'), function () { throw new \Error('demo'); }); diff --git a/tests/Middleware/RequestBodyBufferMiddlewareTest.php b/tests/Middleware/RequestBodyBufferMiddlewareTest.php index 28450f6c..98fdfbec 100644 --- a/tests/Middleware/RequestBodyBufferMiddlewareTest.php +++ b/tests/Middleware/RequestBodyBufferMiddlewareTest.php @@ -221,9 +221,6 @@ function (ServerRequestInterface $request) { $this->assertSame('', $exposedResponse->getBody()->getContents()); } - /** - * @expectedException RuntimeException - */ public function testBufferingErrorThrows() { $loop = Factory::create(); @@ -246,6 +243,7 @@ function (ServerRequestInterface $request) { $stream->emit('error', array(new \RuntimeException())); + $this->setExpectedException('RuntimeException'); Block\await($promise, $loop); } diff --git a/tests/ServerTest.php b/tests/ServerTest.php index e788b8b5..d1d84f59 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -14,7 +14,10 @@ final class ServerTest extends TestCase private $connection; private $socket; - public function setUp() + /** + * @before + */ + public function setUpConnectionMockAndSocket() { $this->connection = $this->getMockBuilder('React\Socket\Connection') ->disableOriginalConstructor() @@ -40,11 +43,9 @@ public function setUp() $this->socket = new SocketServerStub(); } - /** - * @expectedException InvalidArgumentException - */ public function testInvalidCallbackFunctionLeadsToException() { + $this->setExpectedException('InvalidArgumentException'); new Server('invalid'); } diff --git a/tests/StreamingServerTest.php b/tests/StreamingServerTest.php index 8423e3ea..b620061a 100644 --- a/tests/StreamingServerTest.php +++ b/tests/StreamingServerTest.php @@ -13,7 +13,10 @@ class StreamingServerTest extends TestCase private $connection; private $socket; - public function setUp() + /** + * @before + */ + public function setUpConnectionMockAndSocket() { $this->connection = $this->getMockBuilder('React\Socket\Connection') ->disableOriginalConstructor() @@ -669,7 +672,7 @@ function ($data) use (&$buffer) { $data = $this->createGetRequest(); $this->connection->emit('data', array($data)); - $this->assertContains("\r\nX-Powered-By: React/alpha\r\n", $buffer); + $this->assertContainsString("\r\nX-Powered-By: React/alpha\r\n", $buffer); } public function testResponsePendingPromiseWillNotSendAnything() @@ -1156,8 +1159,8 @@ function ($data) use (&$buffer) { $data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"; $this->connection->emit('data', array($data)); - $this->assertContains("HTTP/1.1 200 OK\r\n", $buffer); - $this->assertContains("bye", $buffer); + $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); + $this->assertContainsString("bye", $buffer); } public function testResponseContainsSameRequestProtocolVersionAndRawBodyForHttp10() @@ -1189,9 +1192,9 @@ function ($data) use (&$buffer) { $data = "GET / HTTP/1.0\r\n\r\n"; $this->connection->emit('data', array($data)); - $this->assertContains("HTTP/1.0 200 OK\r\n", $buffer); - $this->assertContains("\r\n\r\n", $buffer); - $this->assertContains("bye", $buffer); + $this->assertContainsString("HTTP/1.0 200 OK\r\n", $buffer); + $this->assertContainsString("\r\n\r\n", $buffer); + $this->assertContainsString("bye", $buffer); } public function testResponseContainsNoResponseBodyForHeadRequest() @@ -1222,8 +1225,8 @@ function ($data) use (&$buffer) { $data = "HEAD / HTTP/1.1\r\nHost: localhost\r\n\r\n"; $this->connection->emit('data', array($data)); - $this->assertContains("HTTP/1.1 200 OK\r\n", $buffer); - $this->assertNotContains("bye", $buffer); + $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); + $this->assertNotContainsString("bye", $buffer); } public function testResponseContainsNoResponseBodyAndNoContentLengthForNoContentStatus() @@ -1254,9 +1257,9 @@ function ($data) use (&$buffer) { $data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"; $this->connection->emit('data', array($data)); - $this->assertContains("HTTP/1.1 204 No Content\r\n", $buffer); - $this->assertNotContains("\r\n\Content-Length: 3\r\n", $buffer); - $this->assertNotContains("bye", $buffer); + $this->assertContainsString("HTTP/1.1 204 No Content\r\n", $buffer); + $this->assertNotContainsString("\r\n\Content-Length: 3\r\n", $buffer); + $this->assertNotContainsString("bye", $buffer); } public function testResponseContainsNoResponseBodyForNotModifiedStatus() @@ -1287,9 +1290,9 @@ function ($data) use (&$buffer) { $data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"; $this->connection->emit('data', array($data)); - $this->assertContains("HTTP/1.1 304 Not Modified\r\n", $buffer); - $this->assertContains("\r\nContent-Length: 3\r\n", $buffer); - $this->assertNotContains("bye", $buffer); + $this->assertContainsString("HTTP/1.1 304 Not Modified\r\n", $buffer); + $this->assertContainsString("\r\nContent-Length: 3\r\n", $buffer); + $this->assertNotContainsString("bye", $buffer); } public function testRequestInvalidHttpProtocolVersionWillEmitErrorAndSendErrorResponse() @@ -1321,9 +1324,9 @@ function ($data) use (&$buffer) { $this->assertInstanceOf('InvalidArgumentException', $error); - $this->assertContains("HTTP/1.1 505 HTTP Version not supported\r\n", $buffer); - $this->assertContains("\r\n\r\n", $buffer); - $this->assertContains("Error 505: HTTP Version not supported", $buffer); + $this->assertContainsString("HTTP/1.1 505 HTTP Version not supported\r\n", $buffer); + $this->assertContainsString("\r\n\r\n", $buffer); + $this->assertContainsString("Error 505: HTTP Version not supported", $buffer); } public function testRequestOverflowWillEmitErrorAndSendErrorResponse() @@ -1356,8 +1359,8 @@ function ($data) use (&$buffer) { $this->assertInstanceOf('OverflowException', $error); - $this->assertContains("HTTP/1.1 431 Request Header Fields Too Large\r\n", $buffer); - $this->assertContains("\r\n\r\nError 431: Request Header Fields Too Large", $buffer); + $this->assertContainsString("HTTP/1.1 431 Request Header Fields Too Large\r\n", $buffer); + $this->assertContainsString("\r\n\r\nError 431: Request Header Fields Too Large", $buffer); } public function testRequestInvalidWillEmitErrorAndSendErrorResponse() @@ -1389,8 +1392,8 @@ function ($data) use (&$buffer) { $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); + $this->assertContainsString("HTTP/1.1 400 Bad Request\r\n", $buffer); + $this->assertContainsString("\r\n\r\nError 400: Bad Request", $buffer); } public function testRequestContentLengthBodyDataWillEmitDataEventOnRequestStream() @@ -1905,8 +1908,8 @@ function ($data) use (&$buffer) { $this->connection->emit('data', array($data)); $stream->emit('data', array('hello')); - $this->assertContains("Transfer-Encoding: chunked", $buffer); - $this->assertContains("hello", $buffer); + $this->assertContainsString("Transfer-Encoding: chunked", $buffer); + $this->assertContainsString("hello", $buffer); } public function testResponseWithBodyStringWillOverwriteExplicitContentLengthAndTransferEncoding() @@ -1941,9 +1944,9 @@ function ($data) use (&$buffer) { $this->connection->emit('data', array($data)); - $this->assertNotContains("Transfer-Encoding: chunked", $buffer); - $this->assertContains("Content-Length: 5", $buffer); - $this->assertContains("hello", $buffer); + $this->assertNotContainsString("Transfer-Encoding: chunked", $buffer); + $this->assertContainsString("Content-Length: 5", $buffer); + $this->assertContainsString("hello", $buffer); } public function testResponseContainsResponseBodyWithTransferEncodingChunkedForBodyWithUnknownSize() @@ -1978,9 +1981,9 @@ function ($data) use (&$buffer) { $data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"; $this->connection->emit('data', array($data)); - $this->assertContains("Transfer-Encoding: chunked", $buffer); - $this->assertNotContains("Content-Length:", $buffer); - $this->assertContains("body", $buffer); + $this->assertContainsString("Transfer-Encoding: chunked", $buffer); + $this->assertNotContainsString("Content-Length:", $buffer); + $this->assertContainsString("body", $buffer); } public function testResponseContainsResponseBodyWithPlainBodyWithUnknownSizeForLegacyHttp10() @@ -2015,9 +2018,9 @@ function ($data) use (&$buffer) { $data = "GET / HTTP/1.0\r\nHost: localhost\r\n\r\n"; $this->connection->emit('data', array($data)); - $this->assertNotContains("Transfer-Encoding: chunked", $buffer); - $this->assertNotContains("Content-Length:", $buffer); - $this->assertContains("body", $buffer); + $this->assertNotContainsString("Transfer-Encoding: chunked", $buffer); + $this->assertNotContainsString("Content-Length:", $buffer); + $this->assertContainsString("body", $buffer); } public function testResponseWithCustomTransferEncodingWillBeIgnoredAndUseChunkedTransferEncodingInstead() @@ -2053,9 +2056,9 @@ function ($data) use (&$buffer) { $this->connection->emit('data', array($data)); $stream->emit('data', array('hello')); - $this->assertContains('Transfer-Encoding: chunked', $buffer); - $this->assertNotContains('Transfer-Encoding: custom', $buffer); - $this->assertContains("5\r\nhello\r\n", $buffer); + $this->assertContainsString('Transfer-Encoding: chunked', $buffer); + $this->assertNotContainsString('Transfer-Encoding: custom', $buffer); + $this->assertContainsString("5\r\nhello\r\n", $buffer); } public function testResponseWithoutExplicitDateHeaderWillAddCurrentDate() @@ -2083,9 +2086,9 @@ function ($data) use (&$buffer) { $this->connection->emit('data', array($data)); - $this->assertContains("HTTP/1.1 200 OK\r\n", $buffer); - $this->assertContains("Date:", $buffer); - $this->assertContains("\r\n\r\n", $buffer); + $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); + $this->assertContainsString("Date:", $buffer); + $this->assertContainsString("\r\n\r\n", $buffer); } public function testResponseWIthCustomDateHeaderOverwritesDefault() @@ -2116,9 +2119,9 @@ function ($data) use (&$buffer) { $this->connection->emit('data', array($data)); - $this->assertContains("HTTP/1.1 200 OK\r\n", $buffer); - $this->assertContains("Date: Tue, 15 Nov 1994 08:12:31 GMT\r\n", $buffer); - $this->assertContains("\r\n\r\n", $buffer); + $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); + $this->assertContainsString("Date: Tue, 15 Nov 1994 08:12:31 GMT\r\n", $buffer); + $this->assertContainsString("\r\n\r\n", $buffer); } public function testResponseWithEmptyDateHeaderRemovesDateHeader() @@ -2149,9 +2152,9 @@ function ($data) use (&$buffer) { $this->connection->emit('data', array($data)); - $this->assertContains("HTTP/1.1 200 OK\r\n", $buffer); - $this->assertNotContains("Date:", $buffer); - $this->assertContains("\r\n\r\n", $buffer); + $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); + $this->assertNotContainsString("Date:", $buffer); + $this->assertContainsString("\r\n\r\n", $buffer); } public function testResponseCanContainMultipleCookieHeaders() @@ -2220,8 +2223,8 @@ function ($data) use (&$buffer) { $data .= "\r\n"; $this->connection->emit('data', array($data)); - $this->assertContains("HTTP/1.1 100 Continue\r\n", $buffer); - $this->assertContains("HTTP/1.1 200 OK\r\n", $buffer); + $this->assertContainsString("HTTP/1.1 100 Continue\r\n", $buffer); + $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); } public function testResponseWithExpectContinueRequestWontSendContinueForHttp10() @@ -2250,15 +2253,13 @@ function ($data) use (&$buffer) { $data .= "\r\n"; $this->connection->emit('data', array($data)); - $this->assertContains("HTTP/1.0 200 OK\r\n", $buffer); - $this->assertNotContains("HTTP/1.1 100 Continue\r\n\r\n", $buffer); + $this->assertContainsString("HTTP/1.0 200 OK\r\n", $buffer); + $this->assertNotContainsString("HTTP/1.1 100 Continue\r\n\r\n", $buffer); } - /** - * @expectedException InvalidArgumentException - */ public function testInvalidCallbackFunctionLeadsToException() { + $this->setExpectedException('InvalidArgumentException'); $server = new StreamingServer('invalid'); } @@ -2295,10 +2296,10 @@ function ($data) use (&$buffer) { $input->emit('data', array('1')); $input->emit('data', array('23')); - $this->assertContains("HTTP/1.1 200 OK\r\n", $buffer); - $this->assertContains("\r\n\r\n", $buffer); - $this->assertContains("1\r\n1\r\n", $buffer); - $this->assertContains("2\r\n23\r\n", $buffer); + $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); + $this->assertContainsString("\r\n\r\n", $buffer); + $this->assertContainsString("1\r\n1\r\n", $buffer); + $this->assertContainsString("2\r\n23\r\n", $buffer); } public function testResponseBodyStreamWithContentLengthWillStreamTillLengthWithoutTransferEncoding() @@ -2334,11 +2335,11 @@ function ($data) use (&$buffer) { $input->emit('data', array('hel')); $input->emit('data', array('lo')); - $this->assertContains("HTTP/1.1 200 OK\r\n", $buffer); - $this->assertContains("Content-Length: 5\r\n", $buffer); - $this->assertNotContains("Transfer-Encoding", $buffer); - $this->assertContains("\r\n\r\n", $buffer); - $this->assertContains("hello", $buffer); + $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); + $this->assertContainsString("Content-Length: 5\r\n", $buffer); + $this->assertNotContainsString("Transfer-Encoding", $buffer); + $this->assertContainsString("\r\n\r\n", $buffer); + $this->assertContainsString("hello", $buffer); } public function testResponseWithResponsePromise() @@ -2365,8 +2366,8 @@ function ($data) use (&$buffer) { $data = $this->createGetRequest(); $this->connection->emit('data', array($data)); - $this->assertContains("HTTP/1.1 200 OK\r\n", $buffer); - $this->assertContains("\r\n\r\n", $buffer); + $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); + $this->assertContainsString("\r\n\r\n", $buffer); } public function testResponseReturnInvalidTypeWillResultInError() @@ -2399,7 +2400,7 @@ function ($data) use (&$buffer) { $this->connection->emit('data', array($data)); - $this->assertContains("HTTP/1.1 500 Internal Server Error\r\n", $buffer); + $this->assertContainsString("HTTP/1.1 500 Internal Server Error\r\n", $buffer); $this->assertInstanceOf('RuntimeException', $exception); } @@ -2428,7 +2429,7 @@ function ($data) use (&$buffer) { $this->connection->emit('data', array($data)); - $this->assertContains("HTTP/1.1 500 Internal Server Error\r\n", $buffer); + $this->assertContainsString("HTTP/1.1 500 Internal Server Error\r\n", $buffer); } public function testResponseRejectedPromiseWillResultInErrorMessage() @@ -2459,7 +2460,7 @@ function ($data) use (&$buffer) { $this->connection->emit('data', array($data)); - $this->assertContains("HTTP/1.1 500 Internal Server Error\r\n", $buffer); + $this->assertContainsString("HTTP/1.1 500 Internal Server Error\r\n", $buffer); } public function testResponseExceptionInCallbackWillResultInErrorMessage() @@ -2490,7 +2491,7 @@ function ($data) use (&$buffer) { $this->connection->emit('data', array($data)); - $this->assertContains("HTTP/1.1 500 Internal Server Error\r\n", $buffer); + $this->assertContainsString("HTTP/1.1 500 Internal Server Error\r\n", $buffer); } public function testResponseWithContentLengthHeaderForStringBodyOverwritesTransferEncoding() @@ -2522,11 +2523,11 @@ function ($data) use (&$buffer) { $this->connection->emit('data', array($data)); - $this->assertContains("HTTP/1.1 200 OK\r\n", $buffer); - $this->assertContains("Content-Length: 5\r\n", $buffer); - $this->assertContains("hello", $buffer); + $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); + $this->assertContainsString("Content-Length: 5\r\n", $buffer); + $this->assertContainsString("hello", $buffer); - $this->assertNotContains("Transfer-Encoding", $buffer); + $this->assertNotContainsString("Transfer-Encoding", $buffer); } public function testResponseWillBeHandled() @@ -2554,7 +2555,7 @@ function ($data) use (&$buffer) { $this->connection->emit('data', array($data)); - $this->assertContains("HTTP/1.1 200 OK\r\n", $buffer); + $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); } public function testResponseExceptionThrowInCallBackFunctionWillResultInErrorMessage() @@ -2588,7 +2589,7 @@ function ($data) use (&$buffer) { $this->connection->emit('data', array($data)); $this->assertInstanceOf('RuntimeException', $exception); - $this->assertContains("HTTP/1.1 500 Internal Server Error\r\n", $buffer); + $this->assertContainsString("HTTP/1.1 500 Internal Server Error\r\n", $buffer); $this->assertEquals('hello', $exception->getPrevious()->getMessage()); } @@ -2634,7 +2635,7 @@ function ($data) use (&$buffer) { } $this->assertInstanceOf('RuntimeException', $exception); - $this->assertContains("HTTP/1.1 500 Internal Server Error\r\n", $buffer); + $this->assertContainsString("HTTP/1.1 500 Internal Server Error\r\n", $buffer); $this->assertEquals('hello', $exception->getPrevious()->getMessage()); } @@ -2670,7 +2671,7 @@ function ($data) use (&$buffer) { $this->connection->emit('data', array($data)); - $this->assertContains("HTTP/1.1 500 Internal Server Error\r\n", $buffer); + $this->assertContainsString("HTTP/1.1 500 Internal Server Error\r\n", $buffer); $this->assertInstanceOf('RuntimeException', $exception); } diff --git a/tests/TestCase.php b/tests/TestCase.php index 2b6d265d..6295e871 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -63,8 +63,52 @@ protected function expectCallableConsecutive($numberOfCalls, array $with) protected function createCallableMock() { - return $this - ->getMockBuilder('React\Tests\Http\CallableStub') - ->getMock(); + if (method_exists('PHPUnit\Framework\MockObject\MockBuilder', 'addMethods')) { + // PHPUnit 10+ + return $this->getMockBuilder('stdClass')->addMethods(array('__invoke'))->getMock(); + } else { + // legacy PHPUnit 4 - PHPUnit 9 + return $this->getMockBuilder('stdClass')->setMethods(array('__invoke'))->getMock(); + } + } + + public function assertContainsString($needle, $haystack) + { + if (method_exists($this, 'assertStringContainsString')) { + // PHPUnit 7.5+ + $this->assertStringContainsString($needle, $haystack); + } else { + // legacy PHPUnit 4 - PHPUnit 7.5 + $this->assertContains($needle, $haystack); + } + } + + public function assertNotContainsString($needle, $haystack) + { + if (method_exists($this, 'assertStringNotContainsString')) { + // PHPUnit 7.5+ + $this->assertStringNotContainsString($needle, $haystack); + } else { + // legacy PHPUnit 4 - PHPUnit 7.5 + $this->assertNotContains($needle, $haystack); + } } + + public function setExpectedException($exception, $exceptionMessage = '', $exceptionCode = null) + { + if (method_exists($this, 'expectException')) { + // PHPUnit 5+ + $this->expectException($exception); + if ($exceptionMessage !== '') { + $this->expectExceptionMessage($exceptionMessage); + } + if ($exceptionCode !== null) { + $this->expectExceptionCode($exceptionCode); + } + } else { + // legacy PHPUnit 4 + parent::setExpectedException($exception, $exceptionMessage, $exceptionCode); + } + } + } From 087c420163da1b1bfd433fcc645ecc869bdd9887 Mon Sep 17 00:00:00 2001 From: Simon Frings Date: Tue, 30 Jun 2020 10:31:43 +0200 Subject: [PATCH 309/456] Clean up test suite --- .travis.yml | 4 ++-- composer.json | 11 ++++++----- phpunit.xml.dist | 11 +---------- tests/FunctionalServerTest.php | 12 ++++++------ tests/Io/RequestHeaderParserTest.php | 4 ++++ tests/bootstrap.php | 7 ------- 6 files changed, 19 insertions(+), 30 deletions(-) delete mode 100644 tests/bootstrap.php diff --git a/.travis.yml b/.travis.yml index 59b46fbd..d3de1e55 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,9 +18,9 @@ matrix: - php: 7.2 - php: 7.3 - php: 7.4 - - php: hhvm + - php: hhvm-3.18 allow_failures: - - php: hhvm + - php: hhvm-3.18 install: - composer install --no-interaction diff --git a/composer.json b/composer.json index 5330d775..d750445a 100644 --- a/composer.json +++ b/composer.json @@ -12,13 +12,14 @@ "evenement/evenement": "^3.0 || ^2.0 || ^1.0", "react/promise-stream": "^1.1" }, - "autoload": { - "psr-4": { - "React\\Http\\": "src" - } - }, "require-dev": { "clue/block-react": "^1.1", "phpunit/phpunit": "^9.0 || ^5.7 || ^4.8.35" + }, + "autoload": { + "psr-4": { "React\\Http\\": "src" } + }, + "autoload-dev": { + "psr-4": { "React\\Tests\\Http\\": "tests" } } } diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 79c0ee66..0e947b87 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,15 +1,6 @@ - + ./tests/ diff --git a/tests/FunctionalServerTest.php b/tests/FunctionalServerTest.php index 187c541d..e0feb476 100644 --- a/tests/FunctionalServerTest.php +++ b/tests/FunctionalServerTest.php @@ -127,8 +127,8 @@ public function testPlainHttpOnRandomPortWithOtherHostHeaderTakesPrecedence() public function testSecureHttpsOnRandomPort() { - if (!function_exists('stream_socket_enable_crypto')) { - $this->markTestSkipped('Not supported on your platform (outdated HHVM?)'); + if (defined('HHVM_VERSION')) { + $this->markTestSkipped('Not supported on HHVM'); } $loop = Factory::create(); @@ -162,8 +162,8 @@ public function testSecureHttpsOnRandomPort() public function testSecureHttpsReturnsData() { - if (!function_exists('stream_socket_enable_crypto')) { - $this->markTestSkipped('Not supported on your platform (outdated HHVM?)'); + if (defined('HHVM_VERSION')) { + $this->markTestSkipped('Not supported on HHVM'); } $loop = Factory::create(); @@ -203,8 +203,8 @@ public function testSecureHttpsReturnsData() public function testSecureHttpsOnRandomPortWithoutHostHeaderUsesSocketUri() { - if (!function_exists('stream_socket_enable_crypto')) { - $this->markTestSkipped('Not supported on your platform (outdated HHVM?)'); + if (defined('HHVM_VERSION')) { + $this->markTestSkipped('Not supported on HHVM'); } $loop = Factory::create(); diff --git a/tests/Io/RequestHeaderParserTest.php b/tests/Io/RequestHeaderParserTest.php index baf7215a..ca18df13 100644 --- a/tests/Io/RequestHeaderParserTest.php +++ b/tests/Io/RequestHeaderParserTest.php @@ -715,6 +715,10 @@ public function testServerParamsWillNotSetRemoteAddressForUnixDomainSockets() public function testServerParamsWontBeSetOnMissingUrls() { + if (defined('HHVM_VERSION')) { + $this->markTestSkipped('Not supported on HHVM'); + } + $request = null; $parser = new RequestHeaderParser(); diff --git a/tests/bootstrap.php b/tests/bootstrap.php deleted file mode 100644 index cc300c15..00000000 --- a/tests/bootstrap.php +++ /dev/null @@ -1,7 +0,0 @@ -addPsr4('React\\Tests\\Http\\', __DIR__); From 19b1230b117fd8ee58c1acd09390fcd72e70f2f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 4 Jul 2020 11:37:55 +0200 Subject: [PATCH 310/456] Fix calculating concurrency when `post_max_size` ini is unlimited --- src/Server.php | 19 +++++++++++------ tests/Io/IniUtilTest.php | 5 +++++ tests/ServerTest.php | 44 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 6 deletions(-) diff --git a/src/Server.php b/src/Server.php index 76428aaa..cf3f9cac 100644 --- a/src/Server.php +++ b/src/Server.php @@ -127,7 +127,9 @@ public function __construct($requestHandler) } $middleware = array(); - $middleware[] = new LimitConcurrentRequestsMiddleware($this->getConcurrentRequestsLimit()); + $middleware[] = new LimitConcurrentRequestsMiddleware( + $this->getConcurrentRequestsLimit(\ini_get('memory_limit'), \ini_get('post_max_size')) + ); $middleware[] = new RequestBodyBufferMiddleware(); // Checking for an empty string because that is what a boolean // false is returned as by ini_get depending on the PHP version. @@ -162,17 +164,22 @@ public function listen(ServerInterface $server) } /** + * @param string $memory_limit + * @param string $post_max_size * @return int - * @codeCoverageIgnore */ - private function getConcurrentRequestsLimit() + private function getConcurrentRequestsLimit($memory_limit, $post_max_size) { - if (\ini_get('memory_limit') == -1) { + if ($memory_limit == -1) { return self::MAXIMUM_CONCURRENT_REQUESTS; } - $availableMemory = IniUtil::iniSizeToBytes(\ini_get('memory_limit')) / 4; - $concurrentRequests = \ceil($availableMemory / IniUtil::iniSizeToBytes(\ini_get('post_max_size'))); + if ($post_max_size == 0) { + return 1; + } + + $availableMemory = IniUtil::iniSizeToBytes($memory_limit) / 4; + $concurrentRequests = (int) \ceil($availableMemory / IniUtil::iniSizeToBytes($post_max_size)); if ($concurrentRequests >= self::MAXIMUM_CONCURRENT_REQUESTS) { return self::MAXIMUM_CONCURRENT_REQUESTS; diff --git a/tests/Io/IniUtilTest.php b/tests/Io/IniUtilTest.php index 80bc422b..22374eb4 100644 --- a/tests/Io/IniUtilTest.php +++ b/tests/Io/IniUtilTest.php @@ -53,6 +53,11 @@ public function testIniSizeToBytes($input, $output) $this->assertEquals($output, IniUtil::iniSizeToBytes($input)); } + public function testIniSizeToBytesWithInvalidSuffixReturnsNumberWithoutSuffix() + { + $this->assertEquals('2', IniUtil::iniSizeToBytes('2x')); + } + public function provideInvalidInputIniSizeToBytes() { return array( diff --git a/tests/ServerTest.php b/tests/ServerTest.php index d1d84f59..4ca81e16 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -183,4 +183,48 @@ private function createPostFileUploadRequest() return $data; } + + public function provideIniSettingsForConcurrency() + { + return array( + 'default settings' => array( + '128M', + '8M', + 4 + ), + 'unlimited memory_limit limited to maximum concurrency' => array( + '-1', + '8M', + 100 + ), + 'unlimited post_max_size' => array( + '128M', + '0', + 1 + ), + 'small post_max_size limited to maximum concurrency' => array( + '128M', + '1k', + 100 + ) + ); + } + + /** + * @param string $memory_limit + * @param string $post_max_size + * @param int $expectedConcurrency + * @dataProvider provideIniSettingsForConcurrency + */ + public function testServerConcurrency($memory_limit, $post_max_size, $expectedConcurrency) + { + $server = new Server(function () { }); + + $ref = new \ReflectionMethod($server, 'getConcurrentRequestsLimit'); + $ref->setAccessible(true); + + $value = $ref->invoke($server, $memory_limit, $post_max_size); + + $this->assertEquals($expectedConcurrency, $value); + } } From 9aa446fd86745403a09c315c90d4e464fd84382b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 5 Jul 2020 13:32:28 +0200 Subject: [PATCH 311/456] Prepare v0.8.7 release --- CHANGELOG.md | 22 +++++++++++++++++++--- README.md | 2 +- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d1564383..465356cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,26 @@ # Changelog +## 0.8.7 (2020-07-05) + +* Fix: Fix parsing multipart request body with quoted header parameters (dot net). + (#363 by @ebimmel) + +* Fix: Fix calculating concurrency when `post_max_size` ini is unlimited. + (#365 by @clue) + +* Improve test suite to run tests on PHPUnit 9 and clean up test suite. + (#364 by @SimonFrings) + ## 0.8.6 (2020-01-12) -* Fix parsing Cookie request header with comma in its values (#352 by @fiskie) -* Add .gitattributes to exclude dev files from exports (#353 by @reedy) -* Avoid unneeded warning when decoding invalid data on PHP 7.4 (#357 by @WyriHaximus) +* Fix: Fix parsing `Cookie` request header with comma in its values. + (#352 by @fiskie) + +* Fix: Avoid unneeded warning when decoding invalid data on PHP 7.4. + (#357 by @WyriHaximus) + +* Add .gitattributes to exclude dev files from exports. + (#353 by @reedy) ## 0.8.5 (2019-10-29) diff --git a/README.md b/README.md index 36d80d98..bdeaa50e 100644 --- a/README.md +++ b/README.md @@ -1418,7 +1418,7 @@ The recommended way to install this library is [through Composer](https://getcom This will install the latest supported version: ```bash -$ composer require react/http:^0.8.6 +$ composer require react/http:^0.8.7 ``` See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. From 4ea37ff9e8cbcbcf1b79b34f9c62340eb1a61ec3 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Sun, 5 Jul 2020 17:37:34 +0200 Subject: [PATCH 312/456] Add ReactPHP core team as authors to composer.json --- composer.json | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/composer.json b/composer.json index ab26ee88..22bb4642 100644 --- a/composer.json +++ b/composer.json @@ -3,6 +3,28 @@ "description": "Event-driven, streaming plaintext HTTP and secure HTTPS server for ReactPHP", "keywords": ["event-driven", "streaming", "HTTP", "HTTPS", "server", "ReactPHP"], "license": "MIT", + "authors": [ + { + "name": "Christian Lück", + "homepage": "https://clue.engineering/", + "email": "christian@clue.engineering" + }, + { + "name": "Cees-Jan Kiewiet", + "homepage": "https://wyrihaximus.net/", + "email": "reactphp@ceesjankiewiet.nl" + }, + { + "name": "Jan Sorgalla", + "homepage": "https://sorgalla.com/", + "email": "jsorgalla@gmail.com" + }, + { + "name": "Chris Boden", + "homepage": "https://cboden.dev/", + "email": "cboden@gmail.com" + } + ], "require": { "php": ">=5.3.0", "ringcentral/psr7": "^1.2", From ebd66e8531ac14889a2510b375bf0b7d971405f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 6 Jul 2020 18:23:04 +0200 Subject: [PATCH 313/456] Move middleware implementations to new API section --- README.md | 111 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 61 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index bdeaa50e..1d74a43d 100644 --- a/README.md +++ b/README.md @@ -8,28 +8,32 @@ Event-driven, streaming plaintext HTTP and secure HTTPS server for [ReactPHP](ht * [Quickstart example](#quickstart-example) * [Usage](#usage) - * [Server](#server) - * [StreamingServer](#streamingserver) - * [listen()](#listen) - * [Request](#request) - * [Request parameters](#request-parameters) - * [Query parameters](#query-parameters) - * [Request body](#request-body) - * [Streaming request](#streaming-request) - * [Request method](#request-method) - * [Cookie parameters](#cookie-parameters) - * [Invalid request](#invalid-request) - * [Response](#response) - * [Deferred response](#deferred-response) - * [Streaming response](#streaming-response) - * [Response length](#response-length) - * [Invalid response](#invalid-response) - * [Default response headers](#default-response-headers) - * [Middleware](#middleware) - * [LimitConcurrentRequestsMiddleware](#limitconcurrentrequestsmiddleware) - * [RequestBodyBufferMiddleware](#requestbodybuffermiddleware) - * [RequestBodyParserMiddleware](#requestbodyparsermiddleware) - * [Third-Party Middleware](#third-party-middleware) + * [Server](#server) + * [StreamingServer](#streamingserver) + * [listen()](#listen) + * [Request](#request) + * [Request parameters](#request-parameters) + * [Query parameters](#query-parameters) + * [Request body](#request-body) + * [Streaming request](#streaming-request) + * [Request method](#request-method) + * [Cookie parameters](#cookie-parameters) + * [Invalid request](#invalid-request) + * [Response](#response) + * [Deferred response](#deferred-response) + * [Streaming response](#streaming-response) + * [Response length](#response-length) + * [Invalid response](#invalid-response) + * [Default response headers](#default-response-headers) + * [Middleware](#middleware) + * [Custom middleware](#custom-middleware) + * [Third-Party Middleware](#third-party-middleware) +* [API](#api) + * [React\Http\Middleware](#reacthttpmiddleware) + * [StreamingRequestMiddleware](#streamingrequestmiddleware) + * [LimitConcurrentRequestsMiddleware](#limitconcurrentrequestsmiddleware) + * [RequestBodyBufferMiddleware](#requestbodybuffermiddleware) + * [RequestBodyParserMiddleware](#requestbodyparsermiddleware) * [Install](#install) * [Tests](#tests) * [License](#license) @@ -1046,6 +1050,8 @@ Many common use cases involve validating, processing, manipulating the incoming HTTP request before passing it to the final business logic request handler. As such, this project supports the concept of middleware request handlers. +#### Custom middleware + A middleware request handler is expected to adhere the following rules: * It is a valid `callable`. @@ -1160,6 +1166,39 @@ $server = new Server(array( )); ``` +#### Third-Party Middleware + +While this project does provide the means to *use* middleware implementations +(see above), it does not aim to *define* how middleware implementations should +look like. We realize that there's a vivid ecosystem of middleware +implementations and ongoing effort to standardize interfaces between these with +[PSR-15](https://www.php-fig.org/psr/psr-15/) (HTTP Server Request Handlers) +and support this goal. +As such, this project only bundles a few middleware implementations that are +required to match PHP's request behavior (see +[middleware implementations](#react-http-middleware)) and otherwise actively +encourages third-party middleware implementations. + +While we would love to support PSR-15 directly in `react/http`, we understand +that this interface does not specifically target async APIs and as such does +not take advantage of promises for [deferred responses](#deferred-response). +The gist of this is that where PSR-15 enforces a `ResponseInterface` return +value, we also accept a `PromiseInterface`. +As such, we suggest using the external +[PSR-15 middleware adapter](https://github.com/friends-of-reactphp/http-middleware-psr15-adapter) +that uses on the fly monkey patching of these return values which makes using +most PSR-15 middleware possible with this package without any changes required. + +Other than that, you can also use the above [middleware definition](#middleware) +to create custom middleware. A non-exhaustive list of third-party middleware can +be found at the [middleware wiki](https://github.com/reactphp/reactphp/wiki/Users#http-middleware). +If you build or know a custom middleware, make sure to let the world know and +feel free to add it to this list. + +## API + +### React\Http\Middleware + #### LimitConcurrentRequestsMiddleware The `LimitConcurrentRequestsMiddleware` can be used to @@ -1382,34 +1421,6 @@ new RequestBodyParserMiddleware(10 * 1024, 100); // 100 files with 10 KiB each If you want to respect this setting, you have to check its value and effectively avoid using this middleware entirely. -#### Third-Party Middleware - -While this project does provide the means to *use* middleware implementations -(see above), it does not aim to *define* how middleware implementations should -look like. We realize that there's a vivid ecosystem of middleware -implementations and ongoing effort to standardize interfaces between these with -[PSR-15](https://www.php-fig.org/psr/psr-15/) (HTTP Server Request Handlers) -and support this goal. -As such, this project only bundles a few middleware implementations that are -required to match PHP's request behavior (see above) and otherwise actively -encourages third-party middleware implementations. - -While we would love to support PSR-15 directy in `react/http`, we understand -that this interface does not specifically target async APIs and as such does -not take advantage of promises for [deferred responses](#deferred-response). -The gist of this is that where PSR-15 enforces a `ResponseInterface` return -value, we also accept a `PromiseInterface`. -As such, we suggest using the external -[PSR-15 middleware adapter](https://github.com/friends-of-reactphp/http-middleware-psr15-adapter) -that uses on the fly monkey patching of these return values which makes using -most PSR-15 middleware possible with this package without any changes required. - -Other than that, you can also use the above [middleware definition](#middleware) -to create custom middleware. A non-exhaustive list of third-party middleware can -be found at the [middleware wiki](https://github.com/reactphp/http/wiki/Middleware). -If you build or know a custom middleware, make sure to let the world know and -feel free to add it to this list. - ## Install The recommended way to install this library is [through Composer](https://getcomposer.org). From 05e5fadf59b3757bed36927063e8b8ae6af9099c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 6 Jul 2020 12:33:14 +0200 Subject: [PATCH 314/456] Add `StreamingRequestMiddleware` to stream incoming requests This middleware can be used to process incoming requests with a streaming request body (without buffering). This will replace the existing `StreamingServer` class. --- README.md | 82 ++++++++++++++++--- src/Middleware/StreamingRequestMiddleware.php | 69 ++++++++++++++++ src/Server.php | 35 +++++--- .../StreamingRequestMiddlewareTest.php | 23 ++++++ tests/ServerTest.php | 33 ++++++++ 5 files changed, 220 insertions(+), 22 deletions(-) create mode 100644 src/Middleware/StreamingRequestMiddleware.php create mode 100644 tests/Middleware/StreamingRequestMiddlewareTest.php diff --git a/README.md b/README.md index 1d74a43d..98d2968f 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Event-driven, streaming plaintext HTTP and secure HTTPS server for [ReactPHP](https://reactphp.org/). -**Table of Contents** +**Table of contents** * [Quickstart example](#quickstart-example) * [Usage](#usage) @@ -15,13 +15,13 @@ Event-driven, streaming plaintext HTTP and secure HTTPS server for [ReactPHP](ht * [Request parameters](#request-parameters) * [Query parameters](#query-parameters) * [Request body](#request-body) - * [Streaming request](#streaming-request) + * [Streaming incoming request](#streaming-incoming-request) * [Request method](#request-method) * [Cookie parameters](#cookie-parameters) * [Invalid request](#invalid-request) * [Response](#response) * [Deferred response](#deferred-response) - * [Streaming response](#streaming-response) + * [Streaming outgoing response](#streaming-outgoing-response) * [Response length](#response-length) * [Invalid response](#invalid-response) * [Default response headers](#default-response-headers) @@ -220,7 +220,7 @@ in memory. It will invoke the request handler function once the HTTP request headers have been received, i.e. before receiving the potentially much larger HTTP request body. This means the [request](#request) passed to your request handler function may not be fully compatible with PSR-7. See also -[streaming request](#streaming-request) below for more details. +[streaming incoming request](#streaming-incoming-request) below for more details. ### listen() @@ -389,7 +389,7 @@ This includes the parsed request body and any file uploads. > If you're using the advanced [`StreamingServer`](#streamingserver) class, jump to the next chapter to learn more about how to process a - [streaming request](#streaming-request). + [streaming incoming request](#streaming-incoming-request). As stated above, each incoming HTTP request is always represented by the [PSR-7 `ServerRequestInterface`](https://www.php-fig.org/psr/psr-7/#321-psrhttpmessageserverrequestinterface). @@ -485,7 +485,9 @@ header or when using `Transfer-Encoding: chunked` for HTTP/1.1 requests. intermediary `HTTP/1.1 100 Continue` response to the client. This ensures you will receive the request body without a delay as expected. -#### Streaming request +#### Streaming incoming request + + If you're using the advanced [`StreamingServer`](#streamingserver), the request object will be processed once the request headers have been received. @@ -784,7 +786,9 @@ The promise cancellation handler can be used to clean up any pending resources allocated in this case (if applicable). If a promise is resolved after the client closes, it will simply be ignored. -#### Streaming response +#### Streaming outgoing response + + The `Response` class in this project supports to add an instance which implements the [ReactPHP ReadableStreamInterface](https://github.com/reactphp/stream#readablestreaminterface) @@ -897,7 +901,7 @@ $server = new Server(function (ServerRequestInterface $request) { ``` If the response body size is unknown, a `Content-Length` response header can not -be added automatically. When using a [streaming response](#streaming-response) +be added automatically. When using a [streaming outgoing response](#streaming-outgoing-response) without an explicit `Content-Length` response header, outgoing HTTP/1.1 response messages will automatically use `Transfer-Encoding: chunked` while legacy HTTP/1.0 response messages will contain the plain response body. If you know the length @@ -959,8 +963,8 @@ $server->on('error', function (Exception $e) { Note that the server will also emit an `error` event if the client sends an invalid HTTP request that never reaches your request handler function. See also [invalid request](#invalid-request) for more details. -Additionally, a [streaming request](#streaming-request) body can also emit -an `error` event on the request body. +Additionally, a [streaming incoming request](#streaming-incoming-request) body +can also emit an `error` event on the request body. The server will only send a very generic `500` (Interval Server Error) HTTP error response without any further details to the client if an unhandled @@ -1199,6 +1203,64 @@ feel free to add it to this list. ### React\Http\Middleware +#### StreamingRequestMiddleware + +The `StreamingRequestMiddleware` can be used to +process incoming requests with a streaming request body (without buffering). + +This allows you to process requests of any size without buffering the request +body in memory. Instead, it will represent the request body as a +[`ReadableStreamInterface`](https://github.com/reactphp/stream#readablestreaminterface) +that emit chunks of incoming data as it is received: + +```php +$server = new React\Http\Server(array( + new React\Http\Middleware\StreamingRequestMiddleware(), + function (Psr\Http\Message\ServerRequestInterface $request) { + $body = $request->getBody(); + assert($body instanceof Psr\Http\Message\StreamInterface); + assert($body instanceof React\Stream\ReadableStreamInterface); + + return new React\Promise\Promise(function ($resolve) use ($body) { + $bytes = 0; + $body->on('data', function ($chunk) use (&$bytes) { + $bytes += \count($chunk); + }); + $body->on('close', function () use (&$bytes, $resolve) { + $resolve(new React\Http\Response( + 200, + [], + "Received $bytes bytes\n" + )); + }); + }); + } +)); +``` + +See also [streaming incoming request](#streaming-incoming-request) +for more details. + +Additionally, this middleware can be used in combination with the +[`LimitConcurrentRequestsMiddleware`](#limitconcurrentrequestsmiddleware) and +[`RequestBodyBufferMiddleware`](#requestbodybuffermiddleware) (see below) +to explicitly configure the total number of requests that can be handled at +once: + +```php +$server = new React\Http\Server(array( + new React\Http\Middleware\StreamingRequestMiddleware(), + new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers + new React\Http\Middleware\RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request + new React\Http\Middleware\RequestBodyParserMiddleware(), + $handler +)); +``` + +> Internally, this class is used as a "marker" to not trigger the default + request buffering behavior in the `Server`. It does not implement any logic + on its own. + #### LimitConcurrentRequestsMiddleware The `LimitConcurrentRequestsMiddleware` can be used to diff --git a/src/Middleware/StreamingRequestMiddleware.php b/src/Middleware/StreamingRequestMiddleware.php new file mode 100644 index 00000000..a68454f6 --- /dev/null +++ b/src/Middleware/StreamingRequestMiddleware.php @@ -0,0 +1,69 @@ +getBody(); + * assert($body instanceof Psr\Http\Message\StreamInterface); + * assert($body instanceof React\Stream\ReadableStreamInterface); + * + * return new React\Promise\Promise(function ($resolve) use ($body) { + * $bytes = 0; + * $body->on('data', function ($chunk) use (&$bytes) { + * $bytes += \count($chunk); + * }); + * $body->on('close', function () use (&$bytes, $resolve) { + * $resolve(new React\Http\Response( + * 200, + * [], + * "Received $bytes bytes\n" + * )); + * }); + * }); + * } + * )); + * ``` + * + * See also [streaming incoming request](../../README.md#streaming-incoming-request) + * for more details. + * + * Additionally, this middleware can be used in combination with the + * [`LimitConcurrentRequestsMiddleware`](#limitconcurrentrequestsmiddleware) and + * [`RequestBodyBufferMiddleware`](#requestbodybuffermiddleware) (see below) + * to explicitly configure the total number of requests that can be handled at + * once: + * + * ```php + * $server = new React\Http\Server(array( + * new React\Http\Middleware\StreamingRequestMiddleware(), + * new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers + * new React\Http\Middleware\RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request + * new React\Http\Middleware\RequestBodyParserMiddleware(), + * $handler + * )); + * ``` + * + * > Internally, this class is used as a "marker" to not trigger the default + * request buffering behavior in the `Server`. It does not implement any logic + * on its own. + */ +final class StreamingRequestMiddleware +{ + public function __invoke(ServerRequestInterface $request, $next) + { + return $next($request); + } +} diff --git a/src/Server.php b/src/Server.php index cf3f9cac..8d159a0a 100644 --- a/src/Server.php +++ b/src/Server.php @@ -5,6 +5,7 @@ use Evenement\EventEmitter; use React\Http\Io\IniUtil; use React\Http\Middleware\LimitConcurrentRequestsMiddleware; +use React\Http\Middleware\StreamingRequestMiddleware; use React\Http\Middleware\RequestBodyBufferMiddleware; use React\Http\Middleware\RequestBodyParserMiddleware; use React\Socket\ServerInterface; @@ -126,19 +127,29 @@ public function __construct($requestHandler) throw new \InvalidArgumentException('Invalid request handler given'); } + $streaming = false; + foreach ((array) $requestHandler as $handler) { + if ($handler instanceof StreamingRequestMiddleware) { + $streaming = true; + break; + } + } + $middleware = array(); - $middleware[] = new LimitConcurrentRequestsMiddleware( - $this->getConcurrentRequestsLimit(\ini_get('memory_limit'), \ini_get('post_max_size')) - ); - $middleware[] = new RequestBodyBufferMiddleware(); - // Checking for an empty string because that is what a boolean - // false is returned as by ini_get depending on the PHP version. - // @link http://php.net/manual/en/ini.core.php#ini.enable-post-data-reading - // @link http://php.net/manual/en/function.ini-get.php#refsect1-function.ini-get-notes - // @link https://3v4l.org/qJtsa - $enablePostDataReading = \ini_get('enable_post_data_reading'); - if ($enablePostDataReading !== '') { - $middleware[] = new RequestBodyParserMiddleware(); + if (!$streaming) { + $middleware[] = new LimitConcurrentRequestsMiddleware( + $this->getConcurrentRequestsLimit(\ini_get('memory_limit'), \ini_get('post_max_size')) + ); + $middleware[] = new RequestBodyBufferMiddleware(); + // Checking for an empty string because that is what a boolean + // false is returned as by ini_get depending on the PHP version. + // @link http://php.net/manual/en/ini.core.php#ini.enable-post-data-reading + // @link http://php.net/manual/en/function.ini-get.php#refsect1-function.ini-get-notes + // @link https://3v4l.org/qJtsa + $enablePostDataReading = \ini_get('enable_post_data_reading'); + if ($enablePostDataReading !== '') { + $middleware[] = new RequestBodyParserMiddleware(); + } } if (\is_callable($requestHandler)) { diff --git a/tests/Middleware/StreamingRequestMiddlewareTest.php b/tests/Middleware/StreamingRequestMiddlewareTest.php new file mode 100644 index 00000000..c78ab3af --- /dev/null +++ b/tests/Middleware/StreamingRequestMiddlewareTest.php @@ -0,0 +1,23 @@ +assertSame($response, $ret); + } +} diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 4ca81e16..02845769 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -8,6 +8,8 @@ use React\Promise\Deferred; use Clue\React\Block; use React\Promise; +use React\Http\Middleware\StreamingRequestMiddleware; +use React\Stream\ReadableStreamInterface; final class ServerTest extends TestCase { @@ -142,6 +144,37 @@ public function testPostFileUpload() $this->assertSame("hello\r\n", (string)$files['file']->getStream()); } + public function testServerReceivesBufferedRequestByDefault() + { + $streaming = null; + $server = new Server(function (ServerRequestInterface $request) use (&$streaming) { + $streaming = $request->getBody() instanceof ReadableStreamInterface; + }); + + $server->listen($this->socket); + $this->socket->emit('connection', array($this->connection)); + $this->connection->emit('data', array("GET / HTTP/1.0\r\n\r\n")); + + $this->assertEquals(false, $streaming); + } + + public function testServerWithStreamingRequestMiddlewareReceivesStreamingRequest() + { + $streaming = null; + $server = new Server(array( + new StreamingRequestMiddleware(), + function (ServerRequestInterface $request) use (&$streaming) { + $streaming = $request->getBody() instanceof ReadableStreamInterface; + } + )); + + $server->listen($this->socket); + $this->socket->emit('connection', array($this->connection)); + $this->connection->emit('data', array("GET / HTTP/1.0\r\n\r\n")); + + $this->assertEquals(true, $streaming); + } + public function testForwardErrors() { $exception = new \Exception(); From f3efa6bedba88b53234a5ce24c128c248ce5597b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 6 Jul 2020 22:12:00 +0200 Subject: [PATCH 315/456] Mark `StreamingServer` as internal, use `StreamingRequestMiddleware` --- README.md | 365 +++++++++--------- examples/08-stream-response.php | 2 - examples/12-upload.php | 8 +- examples/13-stream-request.php | 72 ++-- examples/21-http-proxy.php | 4 +- examples/22-connect-proxy.php | 2 +- examples/31-upgrade-echo.php | 2 +- examples/32-upgrade-chat.php | 2 +- examples/99-benchmark-download.php | 2 - src/{ => Io}/StreamingServer.php | 53 +-- .../LimitConcurrentRequestsMiddleware.php | 9 +- src/Server.php | 169 ++++++-- tests/FunctionalServerTest.php | 67 ++-- tests/{ => Io}/StreamingServerTest.php | 6 +- 14 files changed, 406 insertions(+), 357 deletions(-) rename src/{ => Io}/StreamingServer.php (89%) rename tests/{ => Io}/StreamingServerTest.php (99%) diff --git a/README.md b/README.md index 98d2968f..d53e1540 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,6 @@ Event-driven, streaming plaintext HTTP and secure HTTPS server for [ReactPHP](ht * [Quickstart example](#quickstart-example) * [Usage](#usage) * [Server](#server) - * [StreamingServer](#streamingserver) * [listen()](#listen) * [Request](#request) * [Request parameters](#request-parameters) @@ -70,15 +69,14 @@ See also the [examples](examples). The `Server` class is responsible for handling incoming connections and then processing each incoming HTTP request. -It buffers and parses the complete incoming HTTP request in memory. Once the -complete request has been received, it will invoke the request handler function. -This request handler function needs to be passed to the constructor and will be -invoked with the respective [request](#request) object and expects a -[response](#response) object in return: +When a complete HTTP request has been received, it will invoke the given +request handler function. This request handler function needs to be passed to +the constructor and will be invoked with the respective [request](#request) +object and expects a [response](#response) object in return: ```php -$server = new Server(function (ServerRequestInterface $request) { - return new Response( +$server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterface $request) { + return new React\Http\Response( 200, array( 'Content-Type' => 'text/plain' @@ -91,35 +89,36 @@ $server = new Server(function (ServerRequestInterface $request) { Each incoming HTTP request message is always represented by the [PSR-7 `ServerRequestInterface`](https://www.php-fig.org/psr/psr-7/#321-psrhttpmessageserverrequestinterface), see also following [request](#request) chapter for more details. + Each outgoing HTTP response message is always represented by the [PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface), see also following [response](#response) chapter for more details. -In order to process any connections, the server needs to be attached to an -instance of `React\Socket\ServerInterface` through the [`listen()`](#listen) method -as described in the following chapter. In its most simple form, you can attach -this to a [`React\Socket\Server`](https://github.com/reactphp/socket#server) -in order to start a plaintext HTTP server like this: +In order to start listening for any incoming connections, the `Server` needs +to be attached to an instance of +[`React\Socket\ServerInterface`](https://github.com/reactphp/socket#serverinterface) +through the [`listen()`](#listen) method as described in the following +chapter. In its most simple form, you can attach this to a +[`React\Socket\Server`](https://github.com/reactphp/socket#server) in order +to start a plaintext HTTP server like this: ```php -$server = new Server($handler); +$server = new React\Http\Server($handler); $socket = new React\Socket\Server('0.0.0.0:8080', $loop); $server->listen($socket); ``` -See also the [`listen()`](#listen) method and the [first example](examples) for more details. - -The `Server` class is built as a facade around the underlying -[`StreamingServer`](#streamingserver) to provide sane defaults for 80% of the -use cases and is the recommended way to use this library unless you're sure -you know what you're doing. +See also the [`listen()`](#listen) method and the [first example](../examples/) +for more details. -Unlike the underlying [`StreamingServer`](#streamingserver), this class -buffers and parses the complete incoming HTTP request in memory. Once the -complete request has been received, it will invoke the request handler -function. This means the [request](#request) passed to your request handler -function will be fully compatible with PSR-7. +By default, the `Server` buffers and parses the complete incoming HTTP +request in memory. It will invoke the given request handler function when the +complete request headers and request body has been received. This means the +[request](#request) object passed to your request handler function will be +fully compatible with PSR-7 (http-message). This provides sane defaults for +80% of the use cases and is the recommended way to use this library unless +you're sure you know what you're doing. On the other hand, buffering complete HTTP requests in memory until they can be processed by your request handler function means that this class has to @@ -141,9 +140,9 @@ upload_max_filesize 2M max_file_uploads 20 ``` -In particular, the `post_max_size` setting limits how much memory a single HTTP -request is allowed to consume while buffering its request body. On top of -this, this class will try to avoid consuming more than 1/4 of your +In particular, the `post_max_size` setting limits how much memory a single +HTTP request is allowed to consume while buffering its request body. On top +of this, this class will try to avoid consuming more than 1/4 of your `memory_limit` for buffering multiple concurrent HTTP requests. As such, with the above default settings of `128M` max, it will try to consume no more than `32M` for buffering multiple concurrent HTTP requests. As a consequence, it @@ -151,116 +150,93 @@ will limit the concurrency to 4 HTTP requests with the above defaults. It is imperative that you assign reasonable values to your PHP ini settings. It is usually recommended to either reduce the memory a single request is -allowed to take (set `post_max_size 1M` or less) or to increase the total memory -limit to allow for more concurrent requests (set `memory_limit 512M` or more). -Failure to do so means that this class may have to disable concurrency and -only handle one request at a time. - -Internally, this class automatically assigns these limits to the -[middleware](#middleware) request handlers as described below. For more -advanced use cases, you may also use the advanced -[`StreamingServer`](#streamingserver) and assign these middleware request -handlers yourself as described in the following chapters. +allowed to take (set `post_max_size 1M` or less) or to increase the total +memory limit to allow for more concurrent requests (set `memory_limit 512M` +or more). Failure to do so means that this class may have to disable +concurrency and only handle one request at a time. -### StreamingServer - -The advanced `StreamingServer` class is responsible for handling incoming connections and then -processing each incoming HTTP request. - -Unlike the [`Server`](#server) class, it does not buffer and parse the incoming -HTTP request body by default. This means that the request handler will be -invoked with a streaming request body. Once the request headers have been -received, it will invoke the request handler function. This request handler -function needs to be passed to the constructor and will be invoked with the -respective [request](#request) object and expects a [response](#response) -object in return: +As an alternative to the above buffering defaults, you can also configure +the `Server` explicitly to override these defaults. You can use the +[`LimitConcurrentRequestsMiddleware`](#limitconcurrentrequestsmiddleware) and +[`RequestBodyBufferMiddleware`](#requestbodybuffermiddleware) (see below) +to explicitly configure the total number of requests that can be handled at +once like this: ```php -$server = new StreamingServer(function (ServerRequestInterface $request) { - return new Response( - 200, - array( - 'Content-Type' => 'text/plain' - ), - "Hello World!\n" - ); -}); +$server = new React\Http\Server(array( + new React\Http\Middleware\StreamingRequestMiddleware(), + new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers + new React\Http\Middleware\RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request + new React\Http\Middleware\RequestBodyParserMiddleware(), + $handler +)); ``` -Each incoming HTTP request message is always represented by the -[PSR-7 `ServerRequestInterface`](https://www.php-fig.org/psr/psr-7/#321-psrhttpmessageserverrequestinterface), -see also following [request](#request) chapter for more details. -Each outgoing HTTP response message is always represented by the -[PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface), -see also following [response](#response) chapter for more details. +> Internally, this class automatically assigns these middleware handlers + automatically when no [`StreamingRequestMiddleware`](#streamingrequestmiddleware) + is given. Accordingly, you can use this example to override all default + settings to implement custom limits. -In order to process any connections, the server needs to be attached to an -instance of `React\Socket\ServerInterface` through the [`listen()`](#listen) method -as described in the following chapter. In its most simple form, you can attach -this to a [`React\Socket\Server`](https://github.com/reactphp/socket#server) -in order to start a plaintext HTTP server like this: +As an alternative to buffering the complete request body in memory, you can +also use a streaming approach where only small chunks of data have to be kept +in memory: ```php -$server = new StreamingServer($handler); - -$socket = new React\Socket\Server('0.0.0.0:8080', $loop); -$server->listen($socket); +$server = new React\Http\Server(array( + new React\Http\Middleware\StreamingRequestMiddleware(), + $handler +)); ``` -See also the [`listen()`](#listen) method and the [first example](examples) for more details. - -The `StreamingServer` class is considered advanced usage and unless you know -what you're doing, you're recommended to use the [`Server`](#server) class -instead. The `StreamingServer` class is specifically designed to help with -more advanced use cases where you want to have full control over consuming -the incoming HTTP request body and concurrency settings. - -In particular, this class does not buffer and parse the incoming HTTP request -in memory. It will invoke the request handler function once the HTTP request -headers have been received, i.e. before receiving the potentially much larger -HTTP request body. This means the [request](#request) passed to your request -handler function may not be fully compatible with PSR-7. See also -[streaming incoming request](#streaming-incoming-request) below for more details. +In this case, it will invoke the request handler function once the HTTP +request headers have been received, i.e. before receiving the potentially +much larger HTTP request body. This means the [request](#request) passed to +your request handler function may not be fully compatible with PSR-7. This is +specifically designed to help with more advanced use cases where you want to +have full control over consuming the incoming HTTP request body and +concurrency settings. See also [streaming incoming request](#streaming-incoming-request) +below for more details. ### listen() The `listen(React\Socket\ServerInterface $socket): void` method can be used to -start processing connections from the given socket server. +start listening for HTTP requests on the given socket server instance. + The given [`React\Socket\ServerInterface`](https://github.com/reactphp/socket#serverinterface) -is responsible for emitting the underlying streaming connections. -This HTTP server needs to be attached to it in order to process any connections -and pase incoming streaming data as incoming HTTP request messages. -In its most common form, you can attach this to a -[`React\Socket\Server`](https://github.com/reactphp/socket#server) -in order to start a plaintext HTTP server like this: +is responsible for emitting the underlying streaming connections. This +HTTP server needs to be attached to it in order to process any +connections and pase incoming streaming data as incoming HTTP request +messages. In its most common form, you can attach this to a +[`React\Socket\Server`](https://github.com/reactphp/socket#server) in +order to start a plaintext HTTP server like this: ```php -$server = new Server($handler); -// or -$server = new StreamingServer($handler); +$server = new React\Http\Server($handler); $socket = new React\Socket\Server('0.0.0.0:8080', $loop); $server->listen($socket); ``` -This example will start listening for HTTP requests on the alternative HTTP port -`8080` on all interfaces (publicly). As an alternative, it is very common to use -a reverse proxy and let this HTTP server listen on the localhost (loopback) -interface only by using the listen address `127.0.0.1:8080` instead. This way, you -host your application(s) on the default HTTP port `80` and only route specific -requests to this HTTP server. +See also [example #1](examples) for more details. + +This example will start listening for HTTP requests on the alternative +HTTP port `8080` on all interfaces (publicly). As an alternative, it is +very common to use a reverse proxy and let this HTTP server listen on the +localhost (loopback) interface only by using the listen address +`127.0.0.1:8080` instead. This way, you host your application(s) on the +default HTTP port `80` and only route specific requests to this HTTP +server. Likewise, it's usually recommended to use a reverse proxy setup to accept -secure HTTPS requests on default HTTPS port `443` (TLS termination) and only -route plaintext requests to this HTTP server. As an alternative, you can also -accept secure HTTPS requests with this HTTP server by attaching this to a -[`React\Socket\Server`](https://github.com/reactphp/socket#server) using a -secure TLS listen address, a certificate file and optional `passphrase` like this: +secure HTTPS requests on default HTTPS port `443` (TLS termination) and +only route plaintext requests to this HTTP server. As an alternative, you +can also accept secure HTTPS requests with this HTTP server by attaching +this to a [`React\Socket\Server`](https://github.com/reactphp/socket#server) +using a secure TLS listen address, a certificate file and optional +`passphrase` like this: ```php -$server = new Server($handler); -// or -$server = new StreamingServer($handler); +$server = new React\Http\Server($handler); $socket = new React\Socket\Server('tls://0.0.0.0:8443', $loop, array( 'local_cert' => __DIR__ . '/localhost.pem' @@ -272,9 +248,8 @@ See also [example #11](examples) for more details. ### Request -As seen above, the [`Server`](#server) and [`StreamingServer`](#streamingserver) -classes are responsible for handling incoming connections and then processing -each incoming HTTP request. +As seen above, the [`Server`](#server) class is responsible for handling +incoming connections and then processing each incoming HTTP request. The request object will be processed once the request has been received by the client. @@ -383,12 +358,13 @@ See also [example #4](examples). #### Request body -If you're using the [`Server`](#server), then the request object will be -buffered and parsed in memory and contains the full request body. -This includes the parsed request body and any file uploads. +By default, the [`Server`](#server) will buffer and parse the full request body +in memory. This means the given request object includes the parsed request body +and any file uploads. -> If you're using the advanced [`StreamingServer`](#streamingserver) class, jump - to the next chapter to learn more about how to process a +> As an alternative to the default buffering logic, you can also use the + [`StreamingRequestMiddleware`](#streamingrequestmiddleware). Jump to the next + chapter to learn more about how to process a [streaming incoming request](#streaming-incoming-request). As stated above, each incoming HTTP request is always represented by the @@ -489,13 +465,13 @@ header or when using `Transfer-Encoding: chunked` for HTTP/1.1 requests. -If you're using the advanced [`StreamingServer`](#streamingserver), the -request object will be processed once the request headers have been received. +If you're using the advanced [`StreamingRequestMiddleware`](#streamingrequestmiddleware), +the request object will be processed once the request headers have been received. This means that this happens irrespective of (i.e. *before*) receiving the (potentially much larger) request body. -> If you're using the [`Server`](#server) class, jump to the previous chapter - to learn more about how to process a buffered [request body](#request-body). +> Note that this is non-standard behavior considered advanced usage. Jump to the + previous chapter to learn more about how to process a buffered [request body](#request-body). While this may be uncommon in the PHP ecosystem, this is actually a very powerful approach that gives you several advantages not otherwise possible: @@ -525,37 +501,42 @@ The ReactPHP `ReadableStreamInterface` gives you access to the incoming request body as the individual chunks arrive: ```php -$server = new StreamingServer(function (ServerRequestInterface $request) { - return new Promise(function ($resolve, $reject) use ($request) { - $contentLength = 0; - $request->getBody()->on('data', function ($data) use (&$contentLength) { - $contentLength += strlen($data); - }); +$server = new React\Http\Server(array( + new React\Http\Middleware\StreamingRequestMiddleware(), + function (Psr\Http\Message\ServerRequestInterface $request) { + $body = $request->getBody(); + assert($body instanceof Psr\Http\Message\StreamInterface); + assert($body instanceof React\Stream\ReadableStreamInterface); - $request->getBody()->on('end', function () use ($resolve, &$contentLength){ - $response = new Response( - 200, - array( - 'Content-Type' => 'text/plain' - ), - "The length of the submitted request body is: " . $contentLength - ); - $resolve($response); - }); + return new React\Promise\Promise(function ($resolve, $reject) use ($body) { + $bytes = 0; + $body->on('data', function ($data) use (&$bytes) { + $bytes += strlen($data); + }); - // an error occures e.g. on invalid chunked encoded data or an unexpected 'end' event - $request->getBody()->on('error', function (\Exception $exception) use ($resolve, &$contentLength) { - $response = new Response( - 400, - array( - 'Content-Type' => 'text/plain' - ), - "An error occured while reading at length: " . $contentLength - ); - $resolve($response); + $body->on('end', function () use ($resolve, &$bytes){ + $resolve(new React\Http\Response( + 200, + array( + 'Content-Type' => 'text/plain' + ), + "Received $bytes bytes\n" + )); + }); + + // an error occures e.g. on invalid chunked encoded data or an unexpected 'end' event + $body->on('error', function (\Exception $exception) use ($resolve, &$bytes) { + $resolve(new React\Http\Response( + 400, + array( + 'Content-Type' => 'text/plain' + ), + "Encountered error after $bytes bytes: {$exception->getMessage()}\n" + )); + }); }); - }); -}); + } +)); ``` The above example simply counts the number of bytes received in the request body. @@ -593,32 +574,35 @@ This method operates on the streaming request body, i.e. the request body size may be unknown (`null`) when using `Transfer-Encoding: chunked` for HTTP/1.1 requests. ```php -$server = new StreamingServer(function (ServerRequestInterface $request) { - $size = $request->getBody()->getSize(); - if ($size === null) { - $body = 'The request does not contain an explicit length.'; - $body .= 'This example does not accept chunked transfer encoding.'; +$server = new React\Http\Server(array( + new React\Http\Middleware\StreamingRequestMiddleware(), + function (Psr\Http\Message\ServerRequestInterface $request) { + $size = $request->getBody()->getSize(); + if ($size === null) { + $body = 'The request does not contain an explicit length.'; + $body .= 'This example does not accept chunked transfer encoding.'; - return new Response( - 411, + return new React\Http\Response( + 411, + array( + 'Content-Type' => 'text/plain' + ), + $body + ); + } + + return new React\Http\Response( + 200, array( 'Content-Type' => 'text/plain' ), - $body + "Request body size: " . $size . " bytes\n" ); } - - return new Response( - 200, - array( - 'Content-Type' => 'text/plain' - ), - "Request body size: " . $size . " bytes\n" - ); -}); +)); ``` -> Note: The `StreamingServer` automatically takes care of handling requests with the +> Note: The `Server` automatically takes care of handling requests with the additional `Expect: 100-continue` request header. When HTTP/1.1 clients want to send a bigger request body, they MAY send only the request headers with an additional `Expect: 100-continue` request header and wait before sending the actual @@ -700,8 +684,8 @@ See also [example #5](examples) for more details. #### Invalid request -The `Server` and `StreamingServer` classes support both HTTP/1.1 and HTTP/1.0 request -messages. If a client sends an invalid request message, uses an invalid HTTP +The `Server` class supports both HTTP/1.1 and HTTP/1.0 request messages. +If a client sends an invalid request message, uses an invalid HTTP protocol version or sends an invalid `Transfer-Encoding` request header value, the server will automatically send a `400` (Bad Request) HTTP error response to the client and close the connection. @@ -720,10 +704,9 @@ valid response object from your request handler function. See also ### Response -The callback function passed to the constructor of the [`Server`](#server) or -advanced [`StreamingServer`](#server) is responsible for processing the request -and returning a response, which will be delivered to the client. -This function MUST return an instance implementing +The callback function passed to the constructor of the [`Server`](#server) is +responsible for processing the request and returning a response, which will be +delivered to the client. This function MUST return an instance implementing [PSR-7 ResponseInterface](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-7-http-message.md#33-psrhttpmessageresponseinterface) object or a [ReactPHP Promise](https://github.com/reactphp/promise#reactpromise) @@ -977,9 +960,11 @@ create your own HTTP response message instead. #### Default response headers -After the return in the callback function the response will be processed by the -[`Server`](#server) or [`StreamingServer`](#streamingserver) respectively. -They will add the protocol version of the request, so you don't have to. +When a response is returned from the request handler function, it will be +processed by the [`Server`](#server) and then sent back to the client. + +The `Server` will automatically add the protocol version of the request, so you +don't have to. A `Date` header will be automatically added with the system date and time if none is given. You can add a custom `Date` header yourself like this: @@ -1045,10 +1030,9 @@ passed explicitly. ### Middleware -As documented above, the [`Server`](#server) and advanced -[`StreamingServer`](#streamingserver) accept a single -request handler argument that is responsible for processing an incoming -HTTP request and then creating and returning an outgoing HTTP response. +As documented above, the [`Server`](#server) accepts a single request handler +argument that is responsible for processing an incoming HTTP request and then +creating and returning an outgoing HTTP response. Many common use cases involve validating, processing, manipulating the incoming HTTP request before passing it to the final business logic request handler. @@ -1096,8 +1080,7 @@ required to match PHP's request behavior (see below) and otherwise actively encourages [Third-Party Middleware](#third-party-middleware) implementations. In order to use middleware request handlers, simply pass an array with all -callables as defined above to the [`Server`](#server) or -[`StreamingServer`](#streamingserver) respectively. +callables as defined above to the [`Server`](#server). The following example adds a middleware request handler that adds the current time to the request as a header (`Request-Time`) and a final request handler that always returns a 200 code without a body: @@ -1292,7 +1275,8 @@ Similarly, this middleware is often used in combination with the to limit the total number of requests that can be buffered at once: ```php -$server = new StreamingServer(array( +$server = new Server(array( + new StreamingRequestMiddleware(), new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers new RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request new RequestBodyParserMiddleware(), @@ -1305,7 +1289,8 @@ that can be buffered at once and then ensure the actual request handler only processes one request after another without any concurrency: ```php -$server = new StreamingServer(array( +$server = new Server(array( + new StreamingRequestMiddleware(), new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers new RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request new RequestBodyParserMiddleware(), @@ -1357,7 +1342,8 @@ the total number of concurrent requests. Usage: ```php -$server = new StreamingServer(array( +$server = new Server(array( + new StreamingRequestMiddleware(), new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers new RequestBodyBufferMiddleware(16 * 1024 * 1024), // 16 MiB function (ServerRequestInterface $request) { @@ -1416,7 +1402,8 @@ $handler = function (ServerRequestInterface $request) { ); }; -$server = new StreamingServer(array(( +$server = new Server(array( + new StreamingRequestMiddleware(), new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers new RequestBodyBufferMiddleware(16 * 1024 * 1024), // 16 MiB new RequestBodyParserMiddleware(), diff --git a/examples/08-stream-response.php b/examples/08-stream-response.php index ab0ea0ec..dce54b2b 100644 --- a/examples/08-stream-response.php +++ b/examples/08-stream-response.php @@ -10,8 +10,6 @@ $loop = Factory::create(); -// Note how this example still uses `Server` instead of `StreamingServer`. -// The `StreamingServer` is only required for streaming *incoming* requests. $server = new Server(function (ServerRequestInterface $request) use ($loop) { if ($request->getMethod() !== 'GET' || $request->getUri()->getPath() !== '/') { return new Response(404); diff --git a/examples/12-upload.php b/examples/12-upload.php index 4e21bdb0..38d8c708 100644 --- a/examples/12-upload.php +++ b/examples/12-upload.php @@ -13,8 +13,9 @@ use React\Http\Middleware\LimitConcurrentRequestsMiddleware; use React\Http\Middleware\RequestBodyBufferMiddleware; use React\Http\Middleware\RequestBodyParserMiddleware; +use React\Http\Middleware\StreamingRequestMiddleware; use React\Http\Response; -use React\Http\StreamingServer; +use React\Http\Server; require __DIR__ . '/../vendor/autoload.php'; @@ -121,9 +122,10 @@ ); }; -// Note how this example explicitly uses the advanced `StreamingServer` to apply +// Note how this example explicitly uses the advanced `StreamingRequestMiddleware` to apply // custom request buffering limits below before running our request handler. -$server = new StreamingServer(array( +$server = new Server(array( + new StreamingRequestMiddleware(), new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers, queue otherwise new RequestBodyBufferMiddleware(8 * 1024 * 1024), // 8 MiB max, ignore body otherwise new RequestBodyParserMiddleware(100 * 1024, 1), // 1 file with 100 KiB max, reject upload otherwise diff --git a/examples/13-stream-request.php b/examples/13-stream-request.php index 07cd21ed..6b9c8cec 100644 --- a/examples/13-stream-request.php +++ b/examples/13-stream-request.php @@ -1,50 +1,52 @@ getBody(); - $requestBody->on('data', function ($data) use (&$contentLength) { - $contentLength += strlen($data); +$server = new React\Http\Server(array( + new React\Http\Middleware\StreamingRequestMiddleware(), + function (Psr\Http\Message\ServerRequestInterface $request) { + $body = $request->getBody(); + assert($body instanceof Psr\Http\Message\StreamInterface); + assert($body instanceof React\Stream\ReadableStreamInterface); + + return new React\Promise\Promise(function ($resolve, $reject) use ($body) { + $bytes = 0; + $body->on('data', function ($data) use (&$bytes) { + $bytes += strlen($data); + }); + + $body->on('end', function () use ($resolve, &$bytes){ + $resolve(new React\Http\Response( + 200, + array( + 'Content-Type' => 'text/plain' + ), + "Received $bytes bytes\n" + )); + }); + + // an error occures e.g. on invalid chunked encoded data or an unexpected 'end' event + $body->on('error', function (\Exception $exception) use ($resolve, &$bytes) { + $resolve(new React\Http\Response( + 400, + array( + 'Content-Type' => 'text/plain' + ), + "Encountered error after $bytes bytes: {$exception->getMessage()}\n" + )); + }); }); + } +)); - $requestBody->on('end', function () use ($resolve, &$contentLength){ - $response = new Response( - 200, - array( - 'Content-Type' => 'text/plain' - ), - "The length of the submitted request body is: " . $contentLength - ); - $resolve($response); - }); - - // an error occures e.g. on invalid chunked encoded data or an unexpected 'end' event - $requestBody->on('error', function (\Exception $exception) use ($resolve, &$contentLength) { - $response = new Response( - 400, - array( - 'Content-Type' => 'text/plain' - ), - "An error occured while reading at length: " . $contentLength - ); - $resolve($response); - }); - }); -}); +$server->on('error', 'printf'); $socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); $server->listen($socket); diff --git a/examples/21-http-proxy.php b/examples/21-http-proxy.php index a881c050..1fe32e36 100644 --- a/examples/21-http-proxy.php +++ b/examples/21-http-proxy.php @@ -10,10 +10,10 @@ $loop = Factory::create(); -// Note how this example uses the `Server` instead of `StreamingServer`. +// Note how this example uses the `Server` without the `StreamingRequestMiddleware`. // This means that this proxy buffers the whole request before "processing" it. // As such, this is store-and-forward proxy. This could also use the advanced -// `StreamingServer` to forward the incoming request as it comes in. +// `StreamingRequestMiddleware` to forward the incoming request as it comes in. $server = new Server(function (RequestInterface $request) { if (strpos($request->getRequestTarget(), '://') === false) { return new Response( diff --git a/examples/22-connect-proxy.php b/examples/22-connect-proxy.php index 63c20833..85b1a7c9 100644 --- a/examples/22-connect-proxy.php +++ b/examples/22-connect-proxy.php @@ -12,7 +12,7 @@ $loop = Factory::create(); $connector = new Connector($loop); -// Note how this example uses the `Server` instead of `StreamingServer`. +// Note how this example uses the `Server` without the `StreamingRequestMiddleware`. // Unlike the plain HTTP proxy, the CONNECT method does not contain a body // and we establish an end-to-end connection over the stream object, so this // doesn't have to store any payload data in memory at all. diff --git a/examples/31-upgrade-echo.php b/examples/31-upgrade-echo.php index 21d6eb67..df572d50 100644 --- a/examples/31-upgrade-echo.php +++ b/examples/31-upgrade-echo.php @@ -27,7 +27,7 @@ $loop = Factory::create(); -// Note how this example uses the `Server` instead of `StreamingServer`. +// Note how this example uses the `Server` without the `StreamingRequestMiddleware`. // The initial incoming request does not contain a body and we upgrade to a // stream object below. $server = new Server(function (ServerRequestInterface $request) use ($loop) { diff --git a/examples/32-upgrade-chat.php b/examples/32-upgrade-chat.php index 89230f31..5d60154c 100644 --- a/examples/32-upgrade-chat.php +++ b/examples/32-upgrade-chat.php @@ -35,7 +35,7 @@ // this means that any Upgraded data will simply be sent back to the client $chat = new ThroughStream(); -// Note how this example uses the `Server` instead of `StreamingServer`. +// Note how this example uses the `Server` without the `StreamingRequestMiddleware`. // The initial incoming request does not contain a body and we upgrade to a // stream object below. $server = new Server(function (ServerRequestInterface $request) use ($loop, $chat) { diff --git a/examples/99-benchmark-download.php b/examples/99-benchmark-download.php index 692bd810..393f87b1 100644 --- a/examples/99-benchmark-download.php +++ b/examples/99-benchmark-download.php @@ -86,8 +86,6 @@ public function getSize() } } -// Note how this example still uses `Server` instead of `StreamingServer`. -// The `StreamingServer` is only required for streaming *incoming* requests. $server = new Server(function (ServerRequestInterface $request) use ($loop) { switch ($request->getUri()->getPath()) { case '/': diff --git a/src/StreamingServer.php b/src/Io/StreamingServer.php similarity index 89% rename from src/StreamingServer.php rename to src/Io/StreamingServer.php index 3826dbc3..30ab7705 100644 --- a/src/StreamingServer.php +++ b/src/Io/StreamingServer.php @@ -1,18 +1,11 @@ listen($socket); - * ``` - * - * See also [example #1](examples) for more details. - * - * Similarly, you can also attach this to a - * [`React\Socket\SecureServer`](https://github.com/reactphp/socket#secureserver) - * in order to start a secure HTTPS server like this: - * - * ```php - * $server = new StreamingServer($handler); - * - * $socket = new React\Socket\Server(8080, $loop); - * $socket = new React\Socket\SecureServer($socket, $loop, array( - * 'local_cert' => __DIR__ . '/localhost.pem' - * )); - * - * $server->listen($socket); - * ``` - * - * See also [example #11](examples) for more details. - * * @param ServerInterface $socket + * @see \React\Http\Server::listen() */ public function listen(ServerInterface $socket) { diff --git a/src/Middleware/LimitConcurrentRequestsMiddleware.php b/src/Middleware/LimitConcurrentRequestsMiddleware.php index f816f70b..d16402df 100644 --- a/src/Middleware/LimitConcurrentRequestsMiddleware.php +++ b/src/Middleware/LimitConcurrentRequestsMiddleware.php @@ -29,7 +29,8 @@ * than 10 handlers will be invoked at once: * * ```php - * $server = new StreamingServer(array( + * $server = new Server(array( + * new StreamingRequestMiddleware(), * new LimitConcurrentRequestsMiddleware(10), * $handler * )); @@ -40,7 +41,8 @@ * to limit the total number of requests that can be buffered at once: * * ```php - * $server = new StreamingServer(array( + * $server = new Server(array( + * new StreamingRequestMiddleware(), * new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers * new RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request * new RequestBodyParserMiddleware(), @@ -53,7 +55,8 @@ * processes one request after another without any concurrency: * * ```php - * $server = new StreamingServer(array( + * $server = new Server(array( + * new StreamingRequestMiddleware(), * new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers * new RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request * new RequestBodyParserMiddleware(), diff --git a/src/Server.php b/src/Server.php index 8d159a0a..7c4724c5 100644 --- a/src/Server.php +++ b/src/Server.php @@ -4,6 +4,7 @@ use Evenement\EventEmitter; use React\Http\Io\IniUtil; +use React\Http\Io\StreamingServer; use React\Http\Middleware\LimitConcurrentRequestsMiddleware; use React\Http\Middleware\StreamingRequestMiddleware; use React\Http\Middleware\RequestBodyBufferMiddleware; @@ -14,15 +15,14 @@ * The `Server` class is responsible for handling incoming connections and then * processing each incoming HTTP request. * - * It buffers and parses the complete incoming HTTP request in memory. Once the - * complete request has been received, it will invoke the request handler function. - * This request handler function needs to be passed to the constructor and will be - * invoked with the respective [request](#request) object and expects a - * [response](#response) object in return: + * When a complete HTTP request has been received, it will invoke the given + * request handler function. This request handler function needs to be passed to + * the constructor and will be invoked with the respective [request](#request) + * object and expects a [response](#response) object in return: * * ```php - * $server = new Server(function (ServerRequestInterface $request) { - * return new Response( + * $server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterface $request) { + * return new React\Http\Response( * 200, * array( * 'Content-Type' => 'text/plain' @@ -35,35 +35,36 @@ * Each incoming HTTP request message is always represented by the * [PSR-7 `ServerRequestInterface`](https://www.php-fig.org/psr/psr-7/#321-psrhttpmessageserverrequestinterface), * see also following [request](#request) chapter for more details. + * * Each outgoing HTTP response message is always represented by the * [PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface), * see also following [response](#response) chapter for more details. * - * In order to process any connections, the server needs to be attached to an - * instance of `React\Socket\ServerInterface` through the [`listen()`](#listen) method - * as described in the following chapter. In its most simple form, you can attach - * this to a [`React\Socket\Server`](https://github.com/reactphp/socket#server) - * in order to start a plaintext HTTP server like this: + * In order to start listening for any incoming connections, the `Server` needs + * to be attached to an instance of + * [`React\Socket\ServerInterface`](https://github.com/reactphp/socket#serverinterface) + * through the [`listen()`](#listen) method as described in the following + * chapter. In its most simple form, you can attach this to a + * [`React\Socket\Server`](https://github.com/reactphp/socket#server) in order + * to start a plaintext HTTP server like this: * * ```php - * $server = new Server($handler); + * $server = new React\Http\Server($handler); * * $socket = new React\Socket\Server('0.0.0.0:8080', $loop); * $server->listen($socket); * ``` * - * See also the [`listen()`](#listen) method and the [first example](examples) for more details. - * - * The `Server` class is built as a facade around the underlying - * [`StreamingServer`](#streamingserver) to provide sane defaults for 80% of the - * use cases and is the recommended way to use this library unless you're sure - * you know what you're doing. + * See also the [`listen()`](#listen) method and the [first example](../examples/) + * for more details. * - * Unlike the underlying [`StreamingServer`](#streamingserver), this class - * buffers and parses the complete incoming HTTP request in memory. Once the - * complete request has been received, it will invoke the request handler - * function. This means the [request](#request) passed to your request handler - * function will be fully compatible with PSR-7. + * By default, the `Server` buffers and parses the complete incoming HTTP + * request in memory. It will invoke the given request handler function when the + * complete request headers and request body has been received. This means the + * [request](#request) object passed to your request handler function will be + * fully compatible with PSR-7 (http-message). This provides sane defaults for + * 80% of the use cases and is the recommended way to use this library unless + * you're sure you know what you're doing. * * On the other hand, buffering complete HTTP requests in memory until they can * be processed by your request handler function means that this class has to @@ -85,9 +86,9 @@ * max_file_uploads 20 * ``` * - * In particular, the `post_max_size` setting limits how much memory a single HTTP - * request is allowed to consume while buffering its request body. On top of - * this, this class will try to avoid consuming more than 1/4 of your + * In particular, the `post_max_size` setting limits how much memory a single + * HTTP request is allowed to consume while buffering its request body. On top + * of this, this class will try to avoid consuming more than 1/4 of your * `memory_limit` for buffering multiple concurrent HTTP requests. As such, with * the above default settings of `128M` max, it will try to consume no more than * `32M` for buffering multiple concurrent HTTP requests. As a consequence, it @@ -95,16 +96,52 @@ * * It is imperative that you assign reasonable values to your PHP ini settings. * It is usually recommended to either reduce the memory a single request is - * allowed to take (set `post_max_size 1M` or less) or to increase the total memory - * limit to allow for more concurrent requests (set `memory_limit 512M` or more). - * Failure to do so means that this class may have to disable concurrency and - * only handle one request at a time. - * - * Internally, this class automatically assigns these limits to the - * [middleware](#middleware) request handlers as described below. For more - * advanced use cases, you may also use the advanced - * [`StreamingServer`](#streamingserver) and assign these middleware request - * handlers yourself as described in the following chapters. + * allowed to take (set `post_max_size 1M` or less) or to increase the total + * memory limit to allow for more concurrent requests (set `memory_limit 512M` + * or more). Failure to do so means that this class may have to disable + * concurrency and only handle one request at a time. + * + * As an alternative to the above buffering defaults, you can also configure + * the `Server` explicitly to override these defaults. You can use the + * [`LimitConcurrentRequestsMiddleware`](#limitconcurrentrequestsmiddleware) and + * [`RequestBodyBufferMiddleware`](#requestbodybuffermiddleware) (see below) + * to explicitly configure the total number of requests that can be handled at + * once like this: + * + * ```php + * $server = new React\Http\Server(array( + * new React\Http\Middleware\StreamingRequestMiddleware(), + * new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers + * new React\Http\Middleware\RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request + * new React\Http\Middleware\RequestBodyParserMiddleware(), + * $handler + * )); + * ``` + * + * > Internally, this class automatically assigns these middleware handlers + * automatically when no [`StreamingRequestMiddleware`](#streamingrequestmiddleware) + * is given. Accordingly, you can use this example to override all default + * settings to implement custom limits. + * + * As an alternative to buffering the complete request body in memory, you can + * also use a streaming approach where only small chunks of data have to be kept + * in memory: + * + * ```php + * $server = new React\Http\Server(array( + * new React\Http\Middleware\StreamingRequestMiddleware(), + * $handler + * )); + * ``` + * + * In this case, it will invoke the request handler function once the HTTP + * request headers have been received, i.e. before receiving the potentially + * much larger HTTP request body. This means the [request](#request) passed to + * your request handler function may not be fully compatible with PSR-7. This is + * specifically designed to help with more advanced use cases where you want to + * have full control over consuming the incoming HTTP request body and + * concurrency settings. See also [streaming incoming request](#streaming-incoming-request) + * below for more details. */ final class Server extends EventEmitter { @@ -119,7 +156,15 @@ final class Server extends EventEmitter private $streamingServer; /** - * @see StreamingServer::__construct() + * Creates an HTTP server that invokes the given callback for each incoming HTTP request + * + * In order to process any connections, the server needs to be attached to an + * instance of `React\Socket\ServerInterface` which emits underlying streaming + * connections in order to then parse incoming data as HTTP. + * See also [listen()](#listen) for more details. + * + * @param callable|callable[] $requestHandler + * @see self::listen() */ public function __construct($requestHandler) { @@ -167,7 +212,53 @@ public function __construct($requestHandler) } /** - * @see StreamingServer::listen() + * Starts listening for HTTP requests on the given socket server instance + * + * The given [`React\Socket\ServerInterface`](https://github.com/reactphp/socket#serverinterface) + * is responsible for emitting the underlying streaming connections. This + * HTTP server needs to be attached to it in order to process any + * connections and pase incoming streaming data as incoming HTTP request + * messages. In its most common form, you can attach this to a + * [`React\Socket\Server`](https://github.com/reactphp/socket#server) in + * order to start a plaintext HTTP server like this: + * + * ```php + * $server = new React\Http\Server($handler); + * + * $socket = new React\Socket\Server(8080, $loop); + * $server->listen($socket); + * ``` + * + * See also [example #1](examples) for more details. + * + * This example will start listening for HTTP requests on the alternative + * HTTP port `8080` on all interfaces (publicly). As an alternative, it is + * very common to use a reverse proxy and let this HTTP server listen on the + * localhost (loopback) interface only by using the listen address + * `127.0.0.1:8080` instead. This way, you host your application(s) on the + * default HTTP port `80` and only route specific requests to this HTTP + * server. + * + * Likewise, it's usually recommended to use a reverse proxy setup to accept + * secure HTTPS requests on default HTTPS port `443` (TLS termination) and + * only route plaintext requests to this HTTP server. As an alternative, you + * can also accept secure HTTPS requests with this HTTP server by attaching + * this to a [`React\Socket\Server`](https://github.com/reactphp/socket#server) + * using a secure TLS listen address, a certificate file and optional + * `passphrase` like this: + * + * ```php + * $server = new React\Http\Server($handler); + * + * $socket = new React\Socket\Server('tls://0.0.0.0:8443', $loop, array( + * 'local_cert' => __DIR__ . '/localhost.pem' + * )); + * $server->listen($socket); + * ``` + * + * See also [example #11](examples) for more details. + * + * @param ServerInterface $socket */ public function listen(ServerInterface $server) { diff --git a/tests/FunctionalServerTest.php b/tests/FunctionalServerTest.php index e0feb476..2de43ce8 100644 --- a/tests/FunctionalServerTest.php +++ b/tests/FunctionalServerTest.php @@ -6,7 +6,7 @@ use React\Http\Middleware\LimitConcurrentRequestsMiddleware; use React\Http\Middleware\RequestBodyBufferMiddleware; use React\Http\Response; -use React\Http\StreamingServer; +use React\Http\Server; use React\Socket\Server as Socket; use React\EventLoop\Factory; use Psr\Http\Message\RequestInterface; @@ -17,6 +17,7 @@ use React\Promise; use React\Promise\Stream; use React\Stream\ThroughStream; +use React\Http\Middleware\StreamingRequestMiddleware; class FunctionalServerTest extends TestCase { @@ -25,7 +26,7 @@ public function testPlainHttpOnRandomPort() $loop = Factory::create(); $connector = new Connector($loop); - $server = new StreamingServer(function (RequestInterface $request) { + $server = new Server(function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); @@ -51,7 +52,7 @@ public function testPlainHttpOnRandomPortWithSingleRequestHandlerArray() $loop = Factory::create(); $connector = new Connector($loop); - $server = new StreamingServer(array( + $server = new Server(array( function () { return new Response(404); }, @@ -78,7 +79,7 @@ public function testPlainHttpOnRandomPortWithoutHostHeaderUsesSocketUri() $loop = Factory::create(); $connector = new Connector($loop); - $server = new StreamingServer(function (RequestInterface $request) { + $server = new Server(function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); @@ -104,7 +105,7 @@ public function testPlainHttpOnRandomPortWithOtherHostHeaderTakesPrecedence() $loop = Factory::create(); $connector = new Connector($loop); - $server = new StreamingServer(function (RequestInterface $request) { + $server = new Server(function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); @@ -136,7 +137,7 @@ public function testSecureHttpsOnRandomPort() 'tls' => array('verify_peer' => false) )); - $server = new StreamingServer(function (RequestInterface $request) { + $server = new Server(function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); @@ -168,7 +169,7 @@ public function testSecureHttpsReturnsData() $loop = Factory::create(); - $server = new StreamingServer(function (RequestInterface $request) { + $server = new Server(function (RequestInterface $request) { return new Response( 200, array(), @@ -212,7 +213,7 @@ public function testSecureHttpsOnRandomPortWithoutHostHeaderUsesSocketUri() 'tls' => array('verify_peer' => false) )); - $server = new StreamingServer(function (RequestInterface $request) { + $server = new Server(function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); @@ -246,7 +247,7 @@ public function testPlainHttpOnStandardPortReturnsUriWithNoPort() } $connector = new Connector($loop); - $server = new StreamingServer(function (RequestInterface $request) { + $server = new Server(function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); @@ -276,7 +277,7 @@ public function testPlainHttpOnStandardPortWithoutHostHeaderReturnsUriWithNoPort } $connector = new Connector($loop); - $server = new StreamingServer(function (RequestInterface $request) { + $server = new Server(function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); @@ -315,7 +316,7 @@ public function testSecureHttpsOnStandardPortReturnsUriWithNoPort() 'tls' => array('verify_peer' => false) )); - $server = new StreamingServer(function (RequestInterface $request) { + $server = new Server(function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); @@ -354,7 +355,7 @@ public function testSecureHttpsOnStandardPortWithoutHostHeaderUsesSocketUri() 'tls' => array('verify_peer' => false) )); - $server = new StreamingServer(function (RequestInterface $request) { + $server = new Server(function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); @@ -384,7 +385,7 @@ public function testPlainHttpOnHttpsStandardPortReturnsUriWithPort() } $connector = new Connector($loop); - $server = new StreamingServer(function (RequestInterface $request) { + $server = new Server(function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); @@ -423,7 +424,7 @@ public function testSecureHttpsOnHttpStandardPortReturnsUriWithPort() 'tls' => array('verify_peer' => false) )); - $server = new StreamingServer(function (RequestInterface $request) { + $server = new Server(function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri() . 'x' . $request->getHeaderLine('Host')); }); @@ -451,7 +452,7 @@ public function testClosedStreamFromRequestHandlerWillSendEmptyBody() $stream = new ThroughStream(); $stream->close(); - $server = new StreamingServer(function (RequestInterface $request) use ($stream) { + $server = new Server(function (RequestInterface $request) use ($stream) { return new Response(200, array(), $stream); }); @@ -472,15 +473,18 @@ public function testClosedStreamFromRequestHandlerWillSendEmptyBody() $socket->close(); } - public function testRequestHandlerWillReceiveCloseEventIfConnectionClosesWhileSendingBody() + public function testRequestHandlerWithStreamingRequestWillReceiveCloseEventIfConnectionClosesWhileSendingBody() { $loop = Factory::create(); $connector = new Connector($loop); $once = $this->expectCallableOnce(); - $server = new StreamingServer(function (RequestInterface $request) use ($once) { - $request->getBody()->on('close', $once); - }); + $server = new Server(array( + new StreamingRequestMiddleware(), + function (RequestInterface $request) use ($once) { + $request->getBody()->on('close', $once); + } + )); $socket = new Socket(0, $loop); $server->listen($socket); @@ -498,16 +502,19 @@ public function testRequestHandlerWillReceiveCloseEventIfConnectionClosesWhileSe $socket->close(); } - public function testStreamFromRequestHandlerWillBeClosedIfConnectionClosesWhileSendingRequestBody() + public function testStreamFromRequestHandlerWillBeClosedIfConnectionClosesWhileSendingStreamingRequestBody() { $loop = Factory::create(); $connector = new Connector($loop); $stream = new ThroughStream(); - $server = new StreamingServer(function (RequestInterface $request) use ($stream) { - return new Response(200, array(), $stream); - }); + $server = new Server(array( + new StreamingRequestMiddleware(), + function (RequestInterface $request) use ($stream) { + return new Response(200, array(), $stream); + } + )); $socket = new Socket(0, $loop); $server->listen($socket); @@ -535,7 +542,7 @@ public function testStreamFromRequestHandlerWillBeClosedIfConnectionCloses() $stream = new ThroughStream(); - $server = new StreamingServer(function (RequestInterface $request) use ($stream) { + $server = new Server(function (RequestInterface $request) use ($stream) { return new Response(200, array(), $stream); }); @@ -563,7 +570,7 @@ public function testUpgradeWithThroughStreamReturnsDataAsGiven() $loop = Factory::create(); $connector = new Connector($loop); - $server = new StreamingServer(function (RequestInterface $request) use ($loop) { + $server = new Server(function (RequestInterface $request) use ($loop) { $stream = new ThroughStream(); $loop->addTimer(0.1, function () use ($stream) { @@ -600,7 +607,7 @@ public function testUpgradeWithRequestBodyAndThroughStreamReturnsDataAsGiven() $loop = Factory::create(); $connector = new Connector($loop); - $server = new StreamingServer(function (RequestInterface $request) use ($loop) { + $server = new Server(function (RequestInterface $request) use ($loop) { $stream = new ThroughStream(); $loop->addTimer(0.1, function () use ($stream) { @@ -638,7 +645,7 @@ public function testConnectWithThroughStreamReturnsDataAsGiven() $loop = Factory::create(); $connector = new Connector($loop); - $server = new StreamingServer(function (RequestInterface $request) use ($loop) { + $server = new Server(function (RequestInterface $request) use ($loop) { $stream = new ThroughStream(); $loop->addTimer(0.1, function () use ($stream) { @@ -675,7 +682,7 @@ public function testConnectWithThroughStreamReturnedFromPromiseReturnsDataAsGive $loop = Factory::create(); $connector = new Connector($loop); - $server = new StreamingServer(function (RequestInterface $request) use ($loop) { + $server = new Server(function (RequestInterface $request) use ($loop) { $stream = new ThroughStream(); $loop->addTimer(0.1, function () use ($stream) { @@ -716,7 +723,7 @@ public function testConnectWithClosedThroughStreamReturnsNoData() $loop = Factory::create(); $connector = new Connector($loop); - $server = new StreamingServer(function (RequestInterface $request) { + $server = new Server(function (RequestInterface $request) { $stream = new ThroughStream(); $stream->close(); @@ -750,7 +757,7 @@ public function testLimitConcurrentRequestsMiddlewareRequestStreamPausing() $loop = Factory::create(); $connector = new Connector($loop); - $server = new StreamingServer(array( + $server = new Server(array( new LimitConcurrentRequestsMiddleware(5), new RequestBodyBufferMiddleware(16 * 1024 * 1024), // 16 MiB function (ServerRequestInterface $request, $next) use ($loop) { diff --git a/tests/StreamingServerTest.php b/tests/Io/StreamingServerTest.php similarity index 99% rename from tests/StreamingServerTest.php rename to tests/Io/StreamingServerTest.php index b620061a..35396f79 100644 --- a/tests/StreamingServerTest.php +++ b/tests/Io/StreamingServerTest.php @@ -1,12 +1,14 @@ Date: Tue, 7 Jul 2020 09:04:20 +0200 Subject: [PATCH 316/456] Prepare TOC to avoid name collisions with HTTP client --- README.md | 12 ++++++------ src/Server.php | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index d53e1540..84af8325 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Event-driven, streaming plaintext HTTP and secure HTTPS server for [ReactPHP](ht * [Usage](#usage) * [Server](#server) * [listen()](#listen) - * [Request](#request) + * [Server Request](#server-request) * [Request parameters](#request-parameters) * [Query parameters](#query-parameters) * [Request body](#request-body) @@ -71,7 +71,7 @@ processing each incoming HTTP request. When a complete HTTP request has been received, it will invoke the given request handler function. This request handler function needs to be passed to -the constructor and will be invoked with the respective [request](#request) +the constructor and will be invoked with the respective [request](#server-request) object and expects a [response](#response) object in return: ```php @@ -88,7 +88,7 @@ $server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterfac Each incoming HTTP request message is always represented by the [PSR-7 `ServerRequestInterface`](https://www.php-fig.org/psr/psr-7/#321-psrhttpmessageserverrequestinterface), -see also following [request](#request) chapter for more details. +see also following [request](#server-request) chapter for more details. Each outgoing HTTP response message is always represented by the [PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface), @@ -115,7 +115,7 @@ for more details. By default, the `Server` buffers and parses the complete incoming HTTP request in memory. It will invoke the given request handler function when the complete request headers and request body has been received. This means the -[request](#request) object passed to your request handler function will be +[request](#server-request) object passed to your request handler function will be fully compatible with PSR-7 (http-message). This provides sane defaults for 80% of the use cases and is the recommended way to use this library unless you're sure you know what you're doing. @@ -190,7 +190,7 @@ $server = new React\Http\Server(array( In this case, it will invoke the request handler function once the HTTP request headers have been received, i.e. before receiving the potentially -much larger HTTP request body. This means the [request](#request) passed to +much larger HTTP request body. This means the [request](#server-request) passed to your request handler function may not be fully compatible with PSR-7. This is specifically designed to help with more advanced use cases where you want to have full control over consuming the incoming HTTP request body and @@ -246,7 +246,7 @@ $server->listen($socket); See also [example #11](examples) for more details. -### Request +### Server Request As seen above, the [`Server`](#server) class is responsible for handling incoming connections and then processing each incoming HTTP request. diff --git a/src/Server.php b/src/Server.php index 7c4724c5..3fe942c9 100644 --- a/src/Server.php +++ b/src/Server.php @@ -17,7 +17,7 @@ * * When a complete HTTP request has been received, it will invoke the given * request handler function. This request handler function needs to be passed to - * the constructor and will be invoked with the respective [request](#request) + * the constructor and will be invoked with the respective [request](#server-request) * object and expects a [response](#response) object in return: * * ```php @@ -34,7 +34,7 @@ * * Each incoming HTTP request message is always represented by the * [PSR-7 `ServerRequestInterface`](https://www.php-fig.org/psr/psr-7/#321-psrhttpmessageserverrequestinterface), - * see also following [request](#request) chapter for more details. + * see also following [request](#server-request) chapter for more details. * * Each outgoing HTTP response message is always represented by the * [PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface), @@ -61,7 +61,7 @@ * By default, the `Server` buffers and parses the complete incoming HTTP * request in memory. It will invoke the given request handler function when the * complete request headers and request body has been received. This means the - * [request](#request) object passed to your request handler function will be + * [request](#server-request) object passed to your request handler function will be * fully compatible with PSR-7 (http-message). This provides sane defaults for * 80% of the use cases and is the recommended way to use this library unless * you're sure you know what you're doing. @@ -136,7 +136,7 @@ * * In this case, it will invoke the request handler function once the HTTP * request headers have been received, i.e. before receiving the potentially - * much larger HTTP request body. This means the [request](#request) passed to + * much larger HTTP request body. This means the [request](#server-request) passed to * your request handler function may not be fully compatible with PSR-7. This is * specifically designed to help with more advanced use cases where you want to * have full control over consuming the incoming HTTP request body and From 2538e61eead1b438d73f4362dc2d819d1318cfb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 3 Jul 2020 22:02:52 +0200 Subject: [PATCH 317/456] Import clue/reactphp-buzz v2.9.0 Change namespace from `Clue\React\Buzz` to `React\Http` and update all tests with merged namespaces. See https://github.com/clue/reactphp-buzz for original repo. --- LICENSE | 3 + README.md | 1328 +++++++++++++++++++++- composer.json | 16 +- examples/01-google.php | 15 + examples/02-concurrent.php | 23 + examples/03-any.php | 32 + examples/04-post-json.php | 29 + examples/05-put-xml.php | 26 + examples/11-http-proxy.php | 29 + examples/12-socks-proxy.php | 29 + examples/13-ssh-proxy.php | 29 + examples/14-unix-domain-sockets.php | 27 + examples/21-stream-forwarding.php | 33 + examples/22-stream-stdin.php | 27 + examples/91-benchmark-download.php | 61 + examples/92-benchmark-upload.php | 125 ++ src/Browser.php | 867 ++++++++++++++ src/Io/ChunkedEncoder.php | 30 +- src/Io/Sender.php | 161 +++ src/Io/Transaction.php | 305 +++++ src/Message/MessageFactory.php | 139 +++ src/Message/ReadableBodyStream.php | 153 +++ src/Message/ResponseException.php | 43 + tests/BrowserTest.php | 414 +++++++ tests/FunctionalBrowserTest.php | 645 +++++++++++ tests/Io/ChunkedEncoderTest.php | 4 +- tests/Io/SenderTest.php | 393 +++++++ tests/Io/TransactionTest.php | 861 ++++++++++++++ tests/Message/MessageFactoryTest.php | 197 ++++ tests/Message/ReadableBodyStreamTest.php | 255 +++++ tests/Message/ResponseExceptionTest.php | 23 + tests/TestCase.php | 14 +- 32 files changed, 6291 insertions(+), 45 deletions(-) create mode 100644 examples/01-google.php create mode 100644 examples/02-concurrent.php create mode 100644 examples/03-any.php create mode 100644 examples/04-post-json.php create mode 100644 examples/05-put-xml.php create mode 100644 examples/11-http-proxy.php create mode 100644 examples/12-socks-proxy.php create mode 100644 examples/13-ssh-proxy.php create mode 100644 examples/14-unix-domain-sockets.php create mode 100644 examples/21-stream-forwarding.php create mode 100644 examples/22-stream-stdin.php create mode 100644 examples/91-benchmark-download.php create mode 100644 examples/92-benchmark-upload.php create mode 100644 src/Browser.php create mode 100644 src/Io/Sender.php create mode 100644 src/Io/Transaction.php create mode 100644 src/Message/MessageFactory.php create mode 100644 src/Message/ReadableBodyStream.php create mode 100644 src/Message/ResponseException.php create mode 100644 tests/BrowserTest.php create mode 100644 tests/FunctionalBrowserTest.php create mode 100644 tests/Io/SenderTest.php create mode 100644 tests/Io/TransactionTest.php create mode 100644 tests/Message/MessageFactoryTest.php create mode 100644 tests/Message/ReadableBodyStreamTest.php create mode 100644 tests/Message/ResponseExceptionTest.php diff --git a/LICENSE b/LICENSE index a808108c..0ca9208a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,3 +1,6 @@ +The MIT License (MIT) + +Copyright (c) 2013 Christian Lück Copyright (c) 2012 Igor Wiedler, Chris Boden Permission is hereby granted, free of charge, to any person obtaining a copy diff --git a/README.md b/README.md index 84af8325..af2fada9 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,22 @@ Event-driven, streaming plaintext HTTP and secure HTTPS server for [ReactPHP](ht **Table of contents** * [Quickstart example](#quickstart-example) -* [Usage](#usage) +* [Client Usage](#client-usage) + * [Request methods](#request-methods) + * [Promises](#promises) + * [Cancellation](#cancellation) + * [Timeouts](#timeouts) + * [Authentication](#authentication) + * [Redirects](#redirects) + * [Blocking](#blocking) + * [Concurrency](#concurrency) + * [Streaming response](#streaming-response) + * [Streaming request](#streaming-request) + * [HTTP proxy](#http-proxy) + * [SOCKS proxy](#socks-proxy) + * [SSH proxy](#ssh-proxy) + * [Unix domain sockets](#unix-domain-sockets) +* [Server Usage](#server-usage) * [Server](#server) * [listen()](#listen) * [Server Request](#server-request) @@ -28,17 +43,54 @@ Event-driven, streaming plaintext HTTP and secure HTTPS server for [ReactPHP](ht * [Custom middleware](#custom-middleware) * [Third-Party Middleware](#third-party-middleware) * [API](#api) + * [Browser](#browser) + * [get()](#get) + * [post()](#post) + * [head()](#head) + * [patch()](#patch) + * [put()](#put) + * [delete()](#delete) + * [request()](#request) + * [requestStreaming()](#requeststreaming) + * [~~submit()~~](#submit) + * [~~send()~~](#send) + * [withTimeout()](#withtimeout) + * [withFollowRedirects()](#withfollowredirects) + * [withRejectErrorResponse()](#withrejecterrorresponse) + * [withBase()](#withbase) + * [withProtocolVersion()](#withprotocolversion) + * [withResponseBuffer()](#withresponsebuffer) + * [~~withOptions()~~](#withoptions) + * [~~withoutBase()~~](#withoutbase) * [React\Http\Middleware](#reacthttpmiddleware) * [StreamingRequestMiddleware](#streamingrequestmiddleware) * [LimitConcurrentRequestsMiddleware](#limitconcurrentrequestsmiddleware) * [RequestBodyBufferMiddleware](#requestbodybuffermiddleware) * [RequestBodyParserMiddleware](#requestbodyparsermiddleware) + * [ResponseInterface](#responseinterface) + * [RequestInterface](#requestinterface) + * [UriInterface](#uriinterface) + * [ResponseException](#responseexception) * [Install](#install) * [Tests](#tests) * [License](#license) ## Quickstart example +Once [installed](#install), you can use the following code to access a +HTTP webserver and send some simple HTTP GET requests: + +```php +$loop = React\EventLoop\Factory::create(); +$client = new React\Http\Browser($loop); + +$client->get('http://www.google.com/')->then(function (Psr\Http\Message\ResponseInterface $response) { + var_dump($response->getHeaders(), (string)$response->getBody()); +}); + +$loop->run(); +``` + This is an HTTP server which responds with `Hello World!` to every request. ```php @@ -62,7 +114,597 @@ $loop->run(); See also the [examples](examples). -## Usage +## Client Usage + +### Request methods + + + +Most importantly, this project provides a [`Browser`](#browser) object that +offers several methods that resemble the HTTP protocol methods: + +```php +$browser->get($url, array $headers = array()); +$browser->head($url, array $headers = array()); +$browser->post($url, array $headers = array(), string|ReadableStreamInterface $contents = ''); +$browser->delete($url, array $headers = array(), string|ReadableStreamInterface $contents = ''); +$browser->put($url, array $headers = array(), string|ReadableStreamInterface $contents = ''); +$browser->patch($url, array $headers = array(), string|ReadableStreamInterface $contents = ''); +``` + +Each of these methods requires a `$url` and some optional parameters to send an +HTTP request. Each of these method names matches the respective HTTP request +method, for example the [`get()`](#get) method sends an HTTP `GET` request. + +You can optionally pass an associative array of additional `$headers` that will be +sent with this HTTP request. Additionally, each method will automatically add a +matching `Content-Length` request header if an outgoing request body is given and its +size is known and non-empty. For an empty request body, if will only include a +`Content-Length: 0` request header if the request method usually expects a request +body (only applies to `POST`, `PUT` and `PATCH` HTTP request methods). + +If you're using a [streaming request body](#streaming-request), it will default +to using `Transfer-Encoding: chunked` unless you explicitly pass in a matching `Content-Length` +request header. See also [streaming request](#streaming-request) for more details. + +By default, all of the above methods default to sending requests using the +HTTP/1.1 protocol version. If you want to explicitly use the legacy HTTP/1.0 +protocol version, you can use the [`withProtocolVersion()`](#withprotocolversion) +method. If you want to use any other or even custom HTTP request method, you can +use the [`request()`](#request) method. + +Each of the above methods supports async operation and either *fulfills* with a +[`ResponseInterface`](#responseinterface) or *rejects* with an `Exception`. +Please see the following chapter about [promises](#promises) for more details. + +### Promises + +Sending requests is async (non-blocking), so you can actually send multiple +requests in parallel. +The `Browser` will respond to each request with a [`ResponseInterface`](#responseinterface) +message, the order is not guaranteed. +Sending requests uses a [Promise](https://github.com/reactphp/promise)-based +interface that makes it easy to react to when an HTTP request is completed +(i.e. either successfully fulfilled or rejected with an error): + +```php +$browser->get($url)->then( + function (Psr\Http\Message\ResponseInterface $response) { + var_dump('Response received', $response); + }, + function (Exception $error) { + var_dump('There was an error', $error->getMessage()); + } +); +``` + +If this looks strange to you, you can also use the more traditional [blocking API](#blocking). + +Keep in mind that resolving the Promise with the full response message means the +whole response body has to be kept in memory. +This is easy to get started and works reasonably well for smaller responses +(such as common HTML pages or RESTful or JSON API requests). + +You may also want to look into the [streaming API](#streaming-response): + +* If you're dealing with lots of concurrent requests (100+) or +* If you want to process individual data chunks as they happen (without having to wait for the full response body) or +* If you're expecting a big response body size (1 MiB or more, for example when downloading binary files) or +* If you're unsure about the response body size (better be safe than sorry when accessing arbitrary remote HTTP endpoints and the response body size is unknown in advance). + +### Cancellation + +The returned Promise is implemented in such a way that it can be cancelled +when it is still pending. +Cancelling a pending promise will reject its value with an Exception and +clean up any underlying resources. + +```php +$promise = $browser->get($url); + +$loop->addTimer(2.0, function () use ($promise) { + $promise->cancel(); +}); +``` + +### Timeouts + +This library uses a very efficient HTTP implementation, so most HTTP requests +should usually be completed in mere milliseconds. However, when sending HTTP +requests over an unreliable network (the internet), there are a number of things +that can go wrong and may cause the request to fail after a time. As such, this +library respects PHP's `default_socket_timeout` setting (default 60s) as a timeout +for sending the outgoing HTTP request and waiting for a successful response and +will otherwise cancel the pending request and reject its value with an Exception. + +Note that this timeout value covers creating the underlying transport connection, +sending the HTTP request, receiving the HTTP response headers and its full +response body and following any eventual [redirects](#redirects). See also +[redirects](#redirects) below to configure the number of redirects to follow (or +disable following redirects altogether) and also [streaming](#streaming-response) +below to not take receiving large response bodies into account for this timeout. + +You can use the [`withTimeout()` method](#withtimeout) to pass a custom timeout +value in seconds like this: + +```php +$browser = $browser->withTimeout(10.0); + +$browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + // response received within 10 seconds maximum + var_dump($response->getHeaders()); +}); +``` + +Similarly, you can use a bool `false` to not apply a timeout at all +or use a bool `true` value to restore the default handling. +See [`withTimeout()`](#withtimeout) for more details. + +If you're using a [streaming response body](#streaming-response), the time it +takes to receive the response body stream will not be included in the timeout. +This allows you to keep this incoming stream open for a longer time, such as +when downloading a very large stream or when streaming data over a long-lived +connection. + +If you're using a [streaming request body](#streaming-request), the time it +takes to send the request body stream will not be included in the timeout. This +allows you to keep this outgoing stream open for a longer time, such as when +uploading a very large stream. + +Note that this timeout handling applies to the higher-level HTTP layer. Lower +layers such as socket and DNS may also apply (different) timeout values. In +particular, the underlying socket connection uses the same `default_socket_timeout` +setting to establish the underlying transport connection. To control this +connection timeout behavior, you can [inject a custom `Connector`](#browser) +like this: + +```php +$browser = new React\Http\Browser( + $loop, + new React\Socket\Connector( + $loop, + array( + 'timeout' => 5 + ) + ) +); +``` + +### Authentication + +This library supports [HTTP Basic Authentication](https://en.wikipedia.org/wiki/Basic_access_authentication) +using the `Authorization: Basic …` request header or allows you to set an explicit +`Authorization` request header. + +By default, this library does not include an outgoing `Authorization` request +header. If the server requires authentication, if may return a `401` (Unauthorized) +status code which will reject the request by default (see also the +[`withRejectErrorResponse()` method](#withrejecterrorresponse) below). + +In order to pass authentication details, you can simple pass the username and +password as part of the request URL like this: + +```php +$promise = $browser->get('https://user:pass@example.com/api'); +``` + +Note that special characters in the authentication details have to be +percent-encoded, see also [`rawurlencode()`](https://www.php.net/manual/en/function.rawurlencode.php). +This example will automatically pass the base64-encoded authentication details +using the outgoing `Authorization: Basic …` request header. If the HTTP endpoint +you're talking to requires any other authentication scheme, you can also pass +this header explicitly. This is common when using (RESTful) HTTP APIs that use +OAuth access tokens or JSON Web Tokens (JWT): + +```php +$token = 'abc123'; + +$promise = $browser->get( + 'https://example.com/api', + array( + 'Authorization' => 'Bearer ' . $token + ) +); +``` + +When following redirects, the `Authorization` request header will never be sent +to any remote hosts by default. When following a redirect where the `Location` +response header contains authentication details, these details will be sent for +following requests. See also [redirects](#redirects) below. + +### Redirects + +By default, this library follows any redirects and obeys `3xx` (Redirection) +status codes using the `Location` response header from the remote server. +The promise will be fulfilled with the last response from the chain of redirects. + +```php +$browser->get($url, $headers)->then(function (Psr\Http\Message\ResponseInterface $response) { + // the final response will end up here + var_dump($response->getHeaders()); +}); +``` + +Any redirected requests will follow the semantics of the original request and +will include the same request headers as the original request except for those +listed below. +If the original request contained a request body, this request body will never +be passed to the redirected request. Accordingly, each redirected request will +remove any `Content-Length` and `Content-Type` request headers. + +If the original request used HTTP authentication with an `Authorization` request +header, this request header will only be passed as part of the redirected +request if the redirected URL is using the same host. In other words, the +`Authorizaton` request header will not be forwarded to other foreign hosts due to +possible privacy/security concerns. When following a redirect where the `Location` +response header contains authentication details, these details will be sent for +following requests. + +You can use the [`withFollowRedirects()`](#withfollowredirects) method to +control the maximum number of redirects to follow or to return any redirect +responses as-is and apply custom redirection logic like this: + +```php +$browser = $browser->withFollowRedirects(false); + +$browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + // any redirects will now end up here + var_dump($response->getHeaders()); +}); +``` + +See also [`withFollowRedirects()`](#withfollowredirects) for more details. + +### Blocking + +As stated above, this library provides you a powerful, async API by default. + +If, however, you want to integrate this into your traditional, blocking environment, +you should look into also using [clue/reactphp-block](https://github.com/clue/reactphp-block). + +The resulting blocking code could look something like this: + +```php +use Clue\React\Block; + +$loop = React\EventLoop\Factory::create(); +$browser = new React\Http\Browser($loop); + +$promise = $browser->get('http://example.com/'); + +try { + $response = Block\await($promise, $loop); + // response successfully received +} catch (Exception $e) { + // an error occured while performing the request +} +``` + +Similarly, you can also process multiple requests concurrently and await an array of `Response` objects: + +```php +$promises = array( + $browser->get('http://example.com/'), + $browser->get('http://www.example.org/'), +); + +$responses = Block\awaitAll($promises, $loop); +``` + +Please refer to [clue/reactphp-block](https://github.com/clue/reactphp-block#readme) for more details. + +Keep in mind the above remark about buffering the whole response message in memory. +As an alternative, you may also see one of the following chapters for the +[streaming API](#streaming-response). + +### Concurrency + +As stated above, this library provides you a powerful, async API. Being able to +send a large number of requests at once is one of the core features of this +project. For instance, you can easily send 100 requests concurrently while +processing SQL queries at the same time. + +Remember, with great power comes great responsibility. Sending an excessive +number of requests may either take up all resources on your side or it may even +get you banned by the remote side if it sees an unreasonable number of requests +from your side. + +```php +// watch out if array contains many elements +foreach ($urls as $url) { + $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + var_dump($response->getHeaders()); + }); +} +``` + +As a consequence, it's usually recommended to limit concurrency on the sending +side to a reasonable value. It's common to use a rather small limit, as doing +more than a dozen of things at once may easily overwhelm the receiving side. You +can use [clue/reactphp-mq](https://github.com/clue/reactphp-mq) as a lightweight +in-memory queue to concurrently do many (but not too many) things at once: + +```php +// wraps Browser in a Queue object that executes no more than 10 operations at once +$q = new Clue\React\Mq\Queue(10, null, function ($url) use ($browser) { + return $browser->get($url); +}); + +foreach ($urls as $url) { + $q($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + var_dump($response->getHeaders()); + }); +} +``` + +Additional requests that exceed the concurrency limit will automatically be +enqueued until one of the pending requests completes. This integrates nicely +with the existing [Promise-based API](#promises). Please refer to +[clue/reactphp-mq](https://github.com/clue/reactphp-mq) for more details. + +This in-memory approach works reasonably well for some thousand outstanding +requests. If you're processing a very large input list (think millions of rows +in a CSV or NDJSON file), you may want to look into using a streaming approach +instead. See [clue/reactphp-flux](https://github.com/clue/reactphp-flux) for +more details. + +### Streaming response + + + +All of the above examples assume you want to store the whole response body in memory. +This is easy to get started and works reasonably well for smaller responses. + +However, there are several situations where it's usually a better idea to use a +streaming approach, where only small chunks have to be kept in memory: + +* If you're dealing with lots of concurrent requests (100+) or +* If you want to process individual data chunks as they happen (without having to wait for the full response body) or +* If you're expecting a big response body size (1 MiB or more, for example when downloading binary files) or +* If you're unsure about the response body size (better be safe than sorry when accessing arbitrary remote HTTP endpoints and the response body size is unknown in advance). + +You can use the [`requestStreaming()`](#requeststreaming) method to send an +arbitrary HTTP request and receive a streaming response. It uses the same HTTP +message API, but does not buffer the response body in memory. It only processes +the response body in small chunks as data is received and forwards this data +through [ReactPHP's Stream API](https://github.com/reactphp/stream). This works +for (any number of) responses of arbitrary sizes. + +This means it resolves with a normal [`ResponseInterface`](#responseinterface), +which can be used to access the response message parameters as usual. +You can access the message body as usual, however it now also +implements ReactPHP's [`ReadableStreamInterface`](https://github.com/reactphp/stream#readablestreaminterface) +as well as parts of the PSR-7's [`StreamInterface`](https://www.php-fig.org/psr/psr-7/#3-4-psr-http-message-streaminterface). + +```php +$browser->requestStreaming('GET', $url)->then(function (Psr\Http\Message\ResponseInterface $response) { + $body = $response->getBody(); + assert($body instanceof Psr\Http\Message\StreamInterface); + assert($body instanceof React\Stream\ReadableStreamInterface); + + $body->on('data', function ($chunk) { + echo $chunk; + }); + + $body->on('error', function (Exception $error) { + echo 'Error: ' . $error->getMessage() . PHP_EOL; + }); + + $body->on('close', function () { + echo '[DONE]' . PHP_EOL; + }); +}); +``` + +See also the [stream download example](examples/91-benchmark-download.php) and +the [stream forwarding example](examples/21-stream-forwarding.php). + +You can invoke the following methods on the message body: + +```php +$body->on($event, $callback); +$body->eof(); +$body->isReadable(); +$body->pipe(React\Stream\WritableStreamInterface $dest, array $options = array()); +$body->close(); +$body->pause(); +$body->resume(); +``` + +Because the message body is in a streaming state, invoking the following methods +doesn't make much sense: + +```php +$body->__toString(); // '' +$body->detach(); // throws BadMethodCallException +$body->getSize(); // null +$body->tell(); // throws BadMethodCallException +$body->isSeekable(); // false +$body->seek(); // throws BadMethodCallException +$body->rewind(); // throws BadMethodCallException +$body->isWritable(); // false +$body->write(); // throws BadMethodCallException +$body->read(); // throws BadMethodCallException +$body->getContents(); // throws BadMethodCallException +``` + +Note how [timeouts](#timeouts) apply slightly differently when using streaming. +In streaming mode, the timeout value covers creating the underlying transport +connection, sending the HTTP request, receiving the HTTP response headers and +following any eventual [redirects](#redirects). In particular, the timeout value +does not take receiving (possibly large) response bodies into account. + +If you want to integrate the streaming response into a higher level API, then +working with Promise objects that resolve with Stream objects is often inconvenient. +Consider looking into also using [react/promise-stream](https://github.com/reactphp/promise-stream). +The resulting streaming code could look something like this: + +```php +use React\Promise\Stream; + +function download(Browser $browser, string $url): React\Stream\ReadableStreamInterface { + return Stream\unwrapReadable( + $browser->requestStreaming('GET', $url)->then(function (Psr\Http\Message\ResponseInterface $response) { + return $response->getBody(); + }) + ); +} + +$stream = download($browser, $url); +$stream->on('data', function ($data) { + echo $data; +}); +``` + +See also the [`requestStreaming()`](#requeststreaming) method for more details. + +> Legacy info: Legacy versions prior to v2.9.0 used the legacy + [`streaming` option](#withoptions). This option is now deprecated but otherwise + continues to show the exact same behavior. + +### Streaming request + +Besides streaming the response body, you can also stream the request body. +This can be useful if you want to send big POST requests (uploading files etc.) +or process many outgoing streams at once. +Instead of passing the body as a string, you can simply pass an instance +implementing ReactPHP's [`ReadableStreamInterface`](https://github.com/reactphp/stream#readablestreaminterface) +to the [request methods](#request-methods) like this: + +```php +$browser->post($url, array(), $stream)->then(function (Psr\Http\Message\ResponseInterface $response) { + echo 'Successfully sent.'; +}); +``` + +If you're using a streaming request body (`React\Stream\ReadableStreamInterface`), it will +default to using `Transfer-Encoding: chunked` or you have to explicitly pass in a +matching `Content-Length` request header like so: + +```php +$body = new React\Stream\ThroughStream(); +$loop->addTimer(1.0, function () use ($body) { + $body->end("hello world"); +}); + +$browser->post($url, array('Content-Length' => '11'), $body); +``` + +If the streaming request body emits an `error` event or is explicitly closed +without emitting a successful `end` event first, the request will automatically +be closed and rejected. + +### HTTP proxy + +You can also establish your outgoing connections through an HTTP CONNECT proxy server +by adding a dependency to [clue/reactphp-http-proxy](https://github.com/clue/reactphp-http-proxy). + +HTTP CONNECT proxy servers (also commonly known as "HTTPS proxy" or "SSL proxy") +are commonly used to tunnel HTTPS traffic through an intermediary ("proxy"), to +conceal the origin address (anonymity) or to circumvent address blocking +(geoblocking). While many (public) HTTP CONNECT proxy servers often limit this +to HTTPS port`443` only, this can technically be used to tunnel any TCP/IP-based +protocol, such as plain HTTP and TLS-encrypted HTTPS. + +```php +$proxy = new Clue\React\HttpProxy\ProxyConnector( + 'http://127.0.0.1:8080', + new React\Socket\Connector($loop) +); + +$connector = new React\Socket\Connector($loop, array( + 'tcp' => $proxy, + 'dns' => false +)); + +$browser = new React\Http\Browser($loop, $connector); +``` + +See also the [HTTP CONNECT proxy example](examples/11-http-proxy.php). + +### SOCKS proxy + +You can also establish your outgoing connections through a SOCKS proxy server +by adding a dependency to [clue/reactphp-socks](https://github.com/clue/reactphp-socks). + +The SOCKS proxy protocol family (SOCKS5, SOCKS4 and SOCKS4a) is commonly used to +tunnel HTTP(S) traffic through an intermediary ("proxy"), to conceal the origin +address (anonymity) or to circumvent address blocking (geoblocking). While many +(public) SOCKS proxy servers often limit this to HTTP(S) port `80` and `443` +only, this can technically be used to tunnel any TCP/IP-based protocol. + +```php +$proxy = new Clue\React\Socks\Client( + 'socks://127.0.0.1:1080', + new React\Socket\Connector($loop) +); + +$connector = new React\Socket\Connector($loop, array( + 'tcp' => $proxy, + 'dns' => false +)); + +$browser = new React\Http\Browser($loop, $connector); +``` + +See also the [SOCKS proxy example](examples/12-socks-proxy.php). + +### SSH proxy + +You can also establish your outgoing connections through an SSH server +by adding a dependency to [clue/reactphp-ssh-proxy](https://github.com/clue/reactphp-ssh-proxy). + +[Secure Shell (SSH)](https://en.wikipedia.org/wiki/Secure_Shell) is a secure +network protocol that is most commonly used to access a login shell on a remote +server. Its architecture allows it to use multiple secure channels over a single +connection. Among others, this can also be used to create an "SSH tunnel", which +is commonly used to tunnel HTTP(S) traffic through an intermediary ("proxy"), to +conceal the origin address (anonymity) or to circumvent address blocking +(geoblocking). This can be used to tunnel any TCP/IP-based protocol (HTTP, SMTP, +IMAP etc.), allows you to access local services that are otherwise not accessible +from the outside (database behind firewall) and as such can also be used for +plain HTTP and TLS-encrypted HTTPS. + +```php +$proxy = new Clue\React\SshProxy\SshSocksConnector('me@localhost:22', $loop); + +$connector = new React\Socket\Connector($loop, array( + 'tcp' => $proxy, + 'dns' => false +)); + +$browser = new React\Http\Browser($loop, $connector); +``` + +See also the [SSH proxy example](examples/13-ssh-proxy.php). + +### Unix domain sockets + +By default, this library supports transport over plaintext TCP/IP and secure +TLS connections for the `http://` and `https://` URL schemes respectively. +This library also supports Unix domain sockets (UDS) when explicitly configured. + +In order to use a UDS path, you have to explicitly configure the connector to +override the destination URL so that the hostname given in the request URL will +no longer be used to establish the connection: + +```php +$connector = new React\Socket\FixedUriConnector( + 'unix:///var/run/docker.sock', + new React\Socket\UnixConnector($loop) +); + +$browser = new Browser($loop, $connector); + +$client->get('http://localhost/info')->then(function (Psr\Http\Message\ResponseInterface $response) { + var_dump($response->getHeaders(), (string)$response->getBody()); +}); +``` + +See also the [Unix Domain Sockets (UDS) example](examples/14-unix-domain-sockets.php). + + +## Server Usage ### Server @@ -1184,6 +1826,643 @@ feel free to add it to this list. ## API +### Browser + +The `React\Http\Browser` is responsible for sending HTTP requests to your HTTP server +and keeps track of pending incoming HTTP responses. +It also registers everything with the main [`EventLoop`](https://github.com/reactphp/event-loop#usage). + +```php +$loop = React\EventLoop\Factory::create(); + +$browser = new React\Http\Browser($loop); +``` + +If you need custom connector settings (DNS resolution, TLS parameters, timeouts, +proxy servers etc.), you can explicitly pass a custom instance of the +[`ConnectorInterface`](https://github.com/reactphp/socket#connectorinterface): + +```php +$connector = new React\Socket\Connector($loop, array( + 'dns' => '127.0.0.1', + 'tcp' => array( + 'bindto' => '192.168.10.1:0' + ), + 'tls' => array( + 'verify_peer' => false, + 'verify_peer_name' => false + ) +)); + +$browser = new React\Http\Browser($loop, $connector); +``` + +#### get() + +The `get(string|UriInterface $url, array $headers = array()): PromiseInterface` method can be used to +send an HTTP GET request. + +```php +$browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + var_dump((string)$response->getBody()); +}); +``` + +See also [example 01](examples/01-google.php). + +> For BC reasons, this method accepts the `$url` as either a `string` + value or as an `UriInterface`. It's recommended to explicitly cast any + objects implementing `UriInterface` to `string`. + +#### post() + +The `post(string|UriInterface $url, array $headers = array(), string|ReadableStreamInterface $contents = ''): PromiseInterface` method can be used to +send an HTTP POST request. + +```php +$browser->post( + $url, + [ + 'Content-Type' => 'application/json' + ], + json_encode($data) +)->then(function (Psr\Http\Message\ResponseInterface $response) { + var_dump(json_decode((string)$response->getBody())); +}); +``` + +See also [example 04](examples/04-post-json.php). + +This method is also commonly used to submit HTML form data: + +```php +$data = [ + 'user' => 'Alice', + 'password' => 'secret' +]; + +$browser->post( + $url, + [ + 'Content-Type' => 'application/x-www-form-urlencoded' + ], + http_build_query($data) +); +``` + +This method will automatically add a matching `Content-Length` request +header if the outgoing request body is a `string`. If you're using a +streaming request body (`ReadableStreamInterface`), it will default to +using `Transfer-Encoding: chunked` or you have to explicitly pass in a +matching `Content-Length` request header like so: + +```php +$body = new React\Stream\ThroughStream(); +$loop->addTimer(1.0, function () use ($body) { + $body->end("hello world"); +}); + +$browser->post($url, array('Content-Length' => '11'), $body); +``` + +> For BC reasons, this method accepts the `$url` as either a `string` + value or as an `UriInterface`. It's recommended to explicitly cast any + objects implementing `UriInterface` to `string`. + +#### head() + +The `head(string|UriInterface $url, array $headers = array()): PromiseInterface` method can be used to +send an HTTP HEAD request. + +```php +$browser->head($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + var_dump($response->getHeaders()); +}); +``` + +> For BC reasons, this method accepts the `$url` as either a `string` + value or as an `UriInterface`. It's recommended to explicitly cast any + objects implementing `UriInterface` to `string`. + +#### patch() + +The `patch(string|UriInterface $url, array $headers = array(), string|ReadableStreamInterface $contents = ''): PromiseInterface` method can be used to +send an HTTP PATCH request. + +```php +$browser->patch( + $url, + [ + 'Content-Type' => 'application/json' + ], + json_encode($data) +)->then(function (Psr\Http\Message\ResponseInterface $response) { + var_dump(json_decode((string)$response->getBody())); +}); +``` + +This method will automatically add a matching `Content-Length` request +header if the outgoing request body is a `string`. If you're using a +streaming request body (`ReadableStreamInterface`), it will default to +using `Transfer-Encoding: chunked` or you have to explicitly pass in a +matching `Content-Length` request header like so: + +```php +$body = new React\Stream\ThroughStream(); +$loop->addTimer(1.0, function () use ($body) { + $body->end("hello world"); +}); + +$browser->patch($url, array('Content-Length' => '11'), $body); +``` + +> For BC reasons, this method accepts the `$url` as either a `string` + value or as an `UriInterface`. It's recommended to explicitly cast any + objects implementing `UriInterface` to `string`. + +#### put() + +The `put(string|UriInterface $url, array $headers = array()): PromiseInterface` method can be used to +send an HTTP PUT request. + +```php +$browser->put( + $url, + [ + 'Content-Type' => 'text/xml' + ], + $xml->asXML() +)->then(function (Psr\Http\Message\ResponseInterface $response) { + var_dump((string)$response->getBody()); +}); +``` + +See also [example 05](examples/05-put-xml.php). + +This method will automatically add a matching `Content-Length` request +header if the outgoing request body is a `string`. If you're using a +streaming request body (`ReadableStreamInterface`), it will default to +using `Transfer-Encoding: chunked` or you have to explicitly pass in a +matching `Content-Length` request header like so: + +```php +$body = new React\Stream\ThroughStream(); +$loop->addTimer(1.0, function () use ($body) { + $body->end("hello world"); +}); + +$browser->put($url, array('Content-Length' => '11'), $body); +``` + +> For BC reasons, this method accepts the `$url` as either a `string` + value or as an `UriInterface`. It's recommended to explicitly cast any + objects implementing `UriInterface` to `string`. + +#### delete() + +The `delete(string|UriInterface $url, array $headers = array()): PromiseInterface` method can be used to +send an HTTP DELETE request. + +```php +$browser->delete($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + var_dump((string)$response->getBody()); +}); +``` + +> For BC reasons, this method accepts the `$url` as either a `string` + value or as an `UriInterface`. It's recommended to explicitly cast any + objects implementing `UriInterface` to `string`. + +#### request() + +The `request(string $method, string $url, array $headers = array(), string|ReadableStreamInterface $body = ''): PromiseInterface` method can be used to +send an arbitrary HTTP request. + +The preferred way to send an HTTP request is by using the above +[request methods](#request-methods), for example the [`get()`](#get) +method to send an HTTP `GET` request. + +As an alternative, if you want to use a custom HTTP request method, you +can use this method: + +```php +$browser->request('OPTIONS', $url)->then(function (Psr\Http\Message\ResponseInterface $response) { + var_dump((string)$response->getBody()); +}); +``` + +This method will automatically add a matching `Content-Length` request +header if the size of the outgoing request body is known and non-empty. +For an empty request body, if will only include a `Content-Length: 0` +request header if the request method usually expects a request body (only +applies to `POST`, `PUT` and `PATCH`). + +If you're using a streaming request body (`ReadableStreamInterface`), it +will default to using `Transfer-Encoding: chunked` or you have to +explicitly pass in a matching `Content-Length` request header like so: + +```php +$body = new React\Stream\ThroughStream(); +$loop->addTimer(1.0, function () use ($body) { + $body->end("hello world"); +}); + +$browser->request('POST', $url, array('Content-Length' => '11'), $body); +``` + +> Note that this method is available as of v2.9.0 and always buffers the + response body before resolving. + It does not respect the deprecated [`streaming` option](#withoptions). + If you want to stream the response body, you can use the + [`requestStreaming()`](#requeststreaming) method instead. + +#### requestStreaming() + +The `requestStreaming(string $method, string $url, array $headers = array(), string|ReadableStreamInterface $body = ''): PromiseInterface` method can be used to +send an arbitrary HTTP request and receive a streaming response without buffering the response body. + +The preferred way to send an HTTP request is by using the above +[request methods](#request-methods), for example the [`get()`](#get) +method to send an HTTP `GET` request. Each of these methods will buffer +the whole response body in memory by default. This is easy to get started +and works reasonably well for smaller responses. + +In some situations, it's a better idea to use a streaming approach, where +only small chunks have to be kept in memory. You can use this method to +send an arbitrary HTTP request and receive a streaming response. It uses +the same HTTP message API, but does not buffer the response body in +memory. It only processes the response body in small chunks as data is +received and forwards this data through [ReactPHP's Stream API](https://github.com/reactphp/stream). +This works for (any number of) responses of arbitrary sizes. + +```php +$browser->requestStreaming('GET', $url)->then(function (Psr\Http\Message\ResponseInterface $response) { + $body = $response->getBody(); + assert($body instanceof Psr\Http\Message\StreamInterface); + assert($body instanceof React\Stream\ReadableStreamInterface); + + $body->on('data', function ($chunk) { + echo $chunk; + }); + + $body->on('error', function (Exception $error) { + echo 'Error: ' . $error->getMessage() . PHP_EOL; + }); + + $body->on('close', function () { + echo '[DONE]' . PHP_EOL; + }); +}); +``` + +See also [`ReadableStreamInterface`](https://github.com/reactphp/stream#readablestreaminterface) +and the [streaming response](#streaming-response) for more details, +examples and possible use-cases. + +This method will automatically add a matching `Content-Length` request +header if the size of the outgoing request body is known and non-empty. +For an empty request body, if will only include a `Content-Length: 0` +request header if the request method usually expects a request body (only +applies to `POST`, `PUT` and `PATCH`). + +If you're using a streaming request body (`ReadableStreamInterface`), it +will default to using `Transfer-Encoding: chunked` or you have to +explicitly pass in a matching `Content-Length` request header like so: + +```php +$body = new React\Stream\ThroughStream(); +$loop->addTimer(1.0, function () use ($body) { + $body->end("hello world"); +}); + +$browser->requestStreaming('POST', $url, array('Content-Length' => '11'), $body); +``` + +> Note that this method is available as of v2.9.0 and always resolves the + response without buffering the response body. + It does not respect the deprecated [`streaming` option](#withoptions). + If you want to buffer the response body, use can use the + [`request()`](#request) method instead. + +#### ~~submit()~~ + +> Deprecated since v2.9.0, see [`post()`](#post) instead. + +The deprecated `submit(string|UriInterface $url, array $fields, array $headers = array(), string $method = 'POST'): PromiseInterface` method can be used to +submit an array of field values similar to submitting a form (`application/x-www-form-urlencoded`). + +```php +// deprecated: see post() instead +$browser->submit($url, array('user' => 'test', 'password' => 'secret')); +``` + +> For BC reasons, this method accepts the `$url` as either a `string` + value or as an `UriInterface`. It's recommended to explicitly cast any + objects implementing `UriInterface` to `string`. + +#### ~~send()~~ + +> Deprecated since v2.9.0, see [`request()`](#request) instead. + +The deprecated `send(RequestInterface $request): PromiseInterface` method can be used to +send an arbitrary instance implementing the [`RequestInterface`](#requestinterface) (PSR-7). + +The preferred way to send an HTTP request is by using the above +[request methods](#request-methods), for example the [`get()`](#get) +method to send an HTTP `GET` request. + +As an alternative, if you want to use a custom HTTP request method, you +can use this method: + +```php +$request = new Request('OPTIONS', $url); + +// deprecated: see request() instead +$browser->send($request)->then(…); +``` + +This method will automatically add a matching `Content-Length` request +header if the size of the outgoing request body is known and non-empty. +For an empty request body, if will only include a `Content-Length: 0` +request header if the request method usually expects a request body (only +applies to `POST`, `PUT` and `PATCH`). + +#### withTimeout() + +The `withTimeout(bool|number $timeout): Browser` method can be used to +change the maximum timeout used for waiting for pending requests. + +You can pass in the number of seconds to use as a new timeout value: + +```php +$browser = $browser->withTimeout(10.0); +``` + +You can pass in a bool `false` to disable any timeouts. In this case, +requests can stay pending forever: + +```php +$browser = $browser->withTimeout(false); +``` + +You can pass in a bool `true` to re-enable default timeout handling. This +will respects PHP's `default_socket_timeout` setting (default 60s): + +```php +$browser = $browser->withTimeout(true); +``` + +See also [timeouts](#timeouts) for more details about timeout handling. + +Notice that the [`Browser`](#browser) is an immutable object, i.e. this +method actually returns a *new* [`Browser`](#browser) instance with the +given timeout value applied. + +#### withFollowRedirects() + +The `withTimeout(bool|int $$followRedirects): Browser` method can be used to +change how HTTP redirects will be followed. + +You can pass in the maximum number of redirects to follow: + +```php +$new = $browser->withFollowRedirects(5); +``` + +The request will automatically be rejected when the number of redirects +is exceeded. You can pass in a `0` to reject the request for any +redirects encountered: + +```php +$browser = $browser->withFollowRedirects(0); + +$browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + // only non-redirected responses will now end up here + var_dump($response->getHeaders()); +}); +``` + +You can pass in a bool `false` to disable following any redirects. In +this case, requests will resolve with the redirection response instead +of following the `Location` response header: + +```php +$browser = $browser->withFollowRedirects(false); + +$browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + // any redirects will now end up here + var_dump($response->getHeaderLine('Location')); +}); +``` + +You can pass in a bool `true` to re-enable default redirect handling. +This defaults to following a maximum of 10 redirects: + +```php +$browser = $browser->withFollowRedirects(true); +``` + +See also [redirects](#redirects) for more details about redirect handling. + +Notice that the [`Browser`](#browser) is an immutable object, i.e. this +method actually returns a *new* [`Browser`](#browser) instance with the +given redirect setting applied. + +#### withRejectErrorResponse() + +The `withRejectErrorResponse(bool $obeySuccessCode): Browser` method can be used to +change whether non-successful HTTP response status codes (4xx and 5xx) will be rejected. + +You can pass in a bool `false` to disable rejecting incoming responses +that use a 4xx or 5xx response status code. In this case, requests will +resolve with the response message indicating an error condition: + +```php +$browser = $browser->withRejectErrorResponse(false); + +$browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + // any HTTP response will now end up here + var_dump($response->getStatusCode(), $response->getReasonPhrase()); +}); +``` + +You can pass in a bool `true` to re-enable default status code handling. +This defaults to rejecting any response status codes in the 4xx or 5xx +range with a [`ResponseException`](#responseexception): + +```php +$browser = $browser->withRejectErrorResponse(true); + +$browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + // any successful HTTP response will now end up here + var_dump($response->getStatusCode(), $response->getReasonPhrase()); +}, function (Exception $e) { + if ($e instanceof React\Http\Message\ResponseException) { + // any HTTP response error message will now end up here + $response = $e->getResponse(); + var_dump($response->getStatusCode(), $response->getReasonPhrase()); + } else { + var_dump($e->getMessage()); + } +}); +``` + +Notice that the [`Browser`](#browser) is an immutable object, i.e. this +method actually returns a *new* [`Browser`](#browser) instance with the +given setting applied. + +#### withBase() + +The `withBase(string|null|UriInterface $baseUrl): Browser` method can be used to +change the base URL used to resolve relative URLs to. + +If you configure a base URL, any requests to relative URLs will be +processed by first prepending this absolute base URL. Note that this +merely prepends the base URL and does *not* resolve any relative path +references (like `../` etc.). This is mostly useful for (RESTful) API +calls where all endpoints (URLs) are located under a common base URL. + +```php +$browser = $browser->withBase('http://api.example.com/v3'); + +// will request http://api.example.com/v3/example +$browser->get('/example')->then(…); +``` + +You can pass in a `null` base URL to return a new instance that does not +use a base URL: + +```php +$browser = $browser->withBase(null); +``` + +Accordingly, any requests using relative URLs to a browser that does not +use a base URL can not be completed and will be rejected without sending +a request. + +This method will throw an `InvalidArgumentException` if the given +`$baseUrl` argument is not a valid URL. + +Notice that the [`Browser`](#browser) is an immutable object, i.e. the `withBase()` method +actually returns a *new* [`Browser`](#browser) instance with the given base URL applied. + +> For BC reasons, this method accepts the `$baseUrl` as either a `string` + value or as an `UriInterface`. It's recommended to explicitly cast any + objects implementing `UriInterface` to `string`. + +> Changelog: As of v2.9.0 this method accepts a `null` value to reset the + base URL. Earlier versions had to use the deprecated `withoutBase()` + method to reset the base URL. + +#### withProtocolVersion() + +The `withProtocolVersion(string $protocolVersion): Browser` method can be used to +change the HTTP protocol version that will be used for all subsequent requests. + +All the above [request methods](#request-methods) default to sending +requests as HTTP/1.1. This is the preferred HTTP protocol version which +also provides decent backwards-compatibility with legacy HTTP/1.0 +servers. As such, there should rarely be a need to explicitly change this +protocol version. + +If you want to explicitly use the legacy HTTP/1.0 protocol version, you +can use this method: + +```php +$newBrowser = $browser->withProtocolVersion('1.0'); + +$newBrowser->get($url)->then(…); +``` + +Notice that the [`Browser`](#browser) is an immutable object, i.e. this +method actually returns a *new* [`Browser`](#browser) instance with the +new protocol version applied. + +#### withResponseBuffer() + +The `withRespomseBuffer(int $maximumSize): Browser` method can be used to +change the maximum size for buffering a response body. + +The preferred way to send an HTTP request is by using the above +[request methods](#request-methods), for example the [`get()`](#get) +method to send an HTTP `GET` request. Each of these methods will buffer +the whole response body in memory by default. This is easy to get started +and works reasonably well for smaller responses. + +By default, the response body buffer will be limited to 16 MiB. If the +response body exceeds this maximum size, the request will be rejected. + +You can pass in the maximum number of bytes to buffer: + +```php +$browser = $browser->withResponseBuffer(1024 * 1024); + +$browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + // response body will not exceed 1 MiB + var_dump($response->getHeaders(), (string) $response->getBody()); +}); +``` + +Note that the response body buffer has to be kept in memory for each +pending request until its transfer is completed and it will only be freed +after a pending request is fulfilled. As such, increasing this maximum +buffer size to allow larger response bodies is usually not recommended. +Instead, you can use the [`requestStreaming()` method](#requeststreaming) +to receive responses with arbitrary sizes without buffering. Accordingly, +this maximum buffer size setting has no effect on streaming responses. + +Notice that the [`Browser`](#browser) is an immutable object, i.e. this +method actually returns a *new* [`Browser`](#browser) instance with the +given setting applied. + +#### ~~withOptions()~~ + +> Deprecated since v2.9.0, see [`withTimeout()`](#withtimeout), [`withFollowRedirects()`](#withfollowredirects) + and [`withRejectErrorResponse()`](#withrejecterrorresponse) instead. + +The deprecated `withOptions(array $options): Browser` method can be used to +change the options to use: + +The [`Browser`](#browser) class exposes several options for the handling of +HTTP transactions. These options resemble some of PHP's +[HTTP context options](https://www.php.net/manual/en/context.http.php) and +can be controlled via the following API (and their defaults): + +```php +// deprecated +$newBrowser = $browser->withOptions(array( + 'timeout' => null, // see withTimeout() instead + 'followRedirects' => true, // see withFollowRedirects() instead + 'maxRedirects' => 10, // see withFollowRedirects() instead + 'obeySuccessCode' => true, // see withRejectErrorResponse() instead + 'streaming' => false, // deprecated, see requestStreaming() instead +)); +``` + +See also [timeouts](#timeouts), [redirects](#redirects) and +[streaming](#streaming-response) for more details. + +Notice that the [`Browser`](#browser) is an immutable object, i.e. this +method actually returns a *new* [`Browser`](#browser) instance with the +options applied. + +#### ~~withoutBase()~~ + +> Deprecated since v2.9.0, see [`withBase()`](#withbase) instead. + +The deprecated `withoutBase(): Browser` method can be used to +remove the base URL. + +```php +// deprecated: see withBase() instead +$newBrowser = $browser->withoutBase(); +``` + +Notice that the [`Browser`](#browser) is an immutable object, i.e. the `withoutBase()` method +actually returns a *new* [`Browser`](#browser) instance without any base URL applied. + +See also [`withBase()`](#withbase). + ### React\Http\Middleware #### StreamingRequestMiddleware @@ -1470,6 +2749,51 @@ new RequestBodyParserMiddleware(10 * 1024, 100); // 100 files with 10 KiB each If you want to respect this setting, you have to check its value and effectively avoid using this middleware entirely. +### ResponseInterface + +The `Psr\Http\Message\ResponseInterface` represents the incoming response received from the [`Browser`](#browser). + +This is a standard interface defined in +[PSR-7: HTTP message interfaces](https://www.php-fig.org/psr/psr-7/), see its +[`ResponseInterface` definition](https://www.php-fig.org/psr/psr-7/#3-3-psr-http-message-responseinterface) +which in turn extends the +[`MessageInterface` definition](https://www.php-fig.org/psr/psr-7/#3-1-psr-http-message-messageinterface). + +### RequestInterface + +The `Psr\Http\Message\RequestInterface` represents the outgoing request to be sent via the [`Browser`](#browser). + +This is a standard interface defined in +[PSR-7: HTTP message interfaces](https://www.php-fig.org/psr/psr-7/), see its +[`RequestInterface` definition](https://www.php-fig.org/psr/psr-7/#3-2-psr-http-message-requestinterface) +which in turn extends the +[`MessageInterface` definition](https://www.php-fig.org/psr/psr-7/#3-1-psr-http-message-messageinterface). + +### UriInterface + +The `Psr\Http\Message\UriInterface` represents an absolute or relative URI (aka URL). + +This is a standard interface defined in +[PSR-7: HTTP message interfaces](https://www.php-fig.org/psr/psr-7/), see its +[`UriInterface` definition](https://www.php-fig.org/psr/psr-7/#3-5-psr-http-message-uriinterface). + +> For BC reasons, the request methods accept the URL as either a `string` + value or as an `UriInterface`. It's recommended to explicitly cast any + objects implementing `UriInterface` to `string`. + +### ResponseException + +The `ResponseException` is an `Exception` sub-class that will be used to reject +a request promise if the remote server returns a non-success status code +(anything but 2xx or 3xx). +You can control this behavior via the [`withRejectErrorResponse()` method](#withrejecterrorresponse). + +The `getCode(): int` method can be used to +return the HTTP response status code. + +The `getResponse(): ResponseInterface` method can be used to +access its underlying [`ResponseInterface`](#responseinterface) object. + ## Install The recommended way to install this library is [through Composer](https://getcomposer.org). diff --git a/composer.json b/composer.json index d750445a..755e5d82 100644 --- a/composer.json +++ b/composer.json @@ -5,15 +5,21 @@ "license": "MIT", "require": { "php": ">=5.3.0", - "ringcentral/psr7": "^1.2", - "react/socket": "^1.0 || ^0.8.3", - "react/stream": "^1.0 || ^0.7.1", - "react/promise": "^2.3 || ^1.2.1", "evenement/evenement": "^3.0 || ^2.0 || ^1.0", - "react/promise-stream": "^1.1" + "psr/http-message": "^1.0", + "react/event-loop": "^1.0 || ^0.5", + "react/http-client": "^0.5.10", + "react/promise": "^2.3 || ^1.2.1", + "react/promise-stream": "^1.1", + "react/socket": "^1.1", + "react/stream": "^1.0 || ^0.7.5", + "ringcentral/psr7": "^1.2" }, "require-dev": { "clue/block-react": "^1.1", + "clue/http-proxy-react": "^1.3", + "clue/reactphp-ssh-proxy": "^1.0", + "clue/socks-react": "^1.0", "phpunit/phpunit": "^9.0 || ^5.7 || ^4.8.35" }, "autoload": { diff --git a/examples/01-google.php b/examples/01-google.php new file mode 100644 index 00000000..31a82606 --- /dev/null +++ b/examples/01-google.php @@ -0,0 +1,15 @@ +get('http://google.com/')->then(function (ResponseInterface $response) { + var_dump($response->getHeaders(), (string)$response->getBody()); +}); + +$loop->run(); diff --git a/examples/02-concurrent.php b/examples/02-concurrent.php new file mode 100644 index 00000000..5a9e4258 --- /dev/null +++ b/examples/02-concurrent.php @@ -0,0 +1,23 @@ +head('http://www.github.com/clue/http-react')->then(function (ResponseInterface $response) { + var_dump($response->getHeaders(), (string)$response->getBody()); +}); + +$client->get('http://google.com/')->then(function (ResponseInterface $response) { + var_dump($response->getHeaders(), (string)$response->getBody()); +}); + +$client->get('http://www.lueck.tv/psocksd')->then(function (ResponseInterface $response) { + var_dump($response->getHeaders(), (string)$response->getBody()); +}); + +$loop->run(); diff --git a/examples/03-any.php b/examples/03-any.php new file mode 100644 index 00000000..881dabfc --- /dev/null +++ b/examples/03-any.php @@ -0,0 +1,32 @@ +head('http://www.github.com/clue/http-react'), + $client->get('https://httpbin.org/'), + $client->get('https://google.com'), + $client->get('http://www.lueck.tv/psocksd'), + $client->get('http://www.httpbin.org/absolute-redirect/5') +); + +React\Promise\any($promises)->then(function (ResponseInterface $response) use ($promises) { + // first response arrived => cancel all other pending requests + foreach ($promises as $promise) { + $promise->cancel(); + } + + var_dump($response->getHeaders()); + echo PHP_EOL . $response->getBody(); +}); + +$loop->run(); diff --git a/examples/04-post-json.php b/examples/04-post-json.php new file mode 100644 index 00000000..818dc9bc --- /dev/null +++ b/examples/04-post-json.php @@ -0,0 +1,29 @@ + array( + 'first' => 'Alice', + 'name' => 'Smith' + ), + 'email' => 'alice@example.com' +); + +$client->post( + 'https://httpbin.org/post', + array( + 'Content-Type' => 'application/json' + ), + json_encode($data) +)->then(function (ResponseInterface $response) { + echo (string)$response->getBody(); +}, 'printf'); + +$loop->run(); diff --git a/examples/05-put-xml.php b/examples/05-put-xml.php new file mode 100644 index 00000000..7c23182d --- /dev/null +++ b/examples/05-put-xml.php @@ -0,0 +1,26 @@ +'); +$child = $xml->addChild('user'); +$child->alias = 'clue'; +$child->name = 'Christian Lück'; + +$client->put( + 'https://httpbin.org/put', + array( + 'Content-Type' => 'text/xml' + ), + $xml->asXML() +)->then(function (ResponseInterface $response) { + echo (string)$response->getBody(); +}, 'printf'); + +$loop->run(); diff --git a/examples/11-http-proxy.php b/examples/11-http-proxy.php new file mode 100644 index 00000000..d1ad9cf5 --- /dev/null +++ b/examples/11-http-proxy.php @@ -0,0 +1,29 @@ + $proxy, + 'dns' => false +)); +$browser = new Browser($loop, $connector); + +// demo fetching HTTP headers (or bail out otherwise) +$browser->get('https://www.google.com/')->then(function (ResponseInterface $response) { + echo RingCentral\Psr7\str($response); +}, 'printf'); + +$loop->run(); diff --git a/examples/12-socks-proxy.php b/examples/12-socks-proxy.php new file mode 100644 index 00000000..3b694804 --- /dev/null +++ b/examples/12-socks-proxy.php @@ -0,0 +1,29 @@ + $proxy, + 'dns' => false +)); +$browser = new Browser($loop, $connector); + +// demo fetching HTTP headers (or bail out otherwise) +$browser->get('https://www.google.com/')->then(function (ResponseInterface $response) { + echo RingCentral\Psr7\str($response); +}, 'printf'); + +$loop->run(); diff --git a/examples/13-ssh-proxy.php b/examples/13-ssh-proxy.php new file mode 100644 index 00000000..d0424fea --- /dev/null +++ b/examples/13-ssh-proxy.php @@ -0,0 +1,29 @@ + $proxy, + 'dns' => false +)); +$browser = new Browser($loop, $connector); + +// demo fetching HTTP headers (or bail out otherwise) +$browser->get('https://www.google.com/')->then(function (ResponseInterface $response) { + echo RingCentral\Psr7\str($response); +}, 'printf'); + +$loop->run(); diff --git a/examples/14-unix-domain-sockets.php b/examples/14-unix-domain-sockets.php new file mode 100644 index 00000000..8881321e --- /dev/null +++ b/examples/14-unix-domain-sockets.php @@ -0,0 +1,27 @@ +get('http://localhost/info')->then(function (ResponseInterface $response) { + echo Psr7\str($response); +}, 'printf'); + +$loop->run(); diff --git a/examples/21-stream-forwarding.php b/examples/21-stream-forwarding.php new file mode 100644 index 00000000..b7873775 --- /dev/null +++ b/examples/21-stream-forwarding.php @@ -0,0 +1,33 @@ +write('Requesting ' . $url . '…' . PHP_EOL); + +$client->requestStreaming('GET', $url)->then(function (ResponseInterface $response) use ($info, $out) { + $info->write('Received' . PHP_EOL . Psr7\str($response)); + + $body = $response->getBody(); + assert($body instanceof ReadableStreamInterface); + $body->pipe($out); +}, 'printf'); + +$loop->run(); diff --git a/examples/22-stream-stdin.php b/examples/22-stream-stdin.php new file mode 100644 index 00000000..4a36df91 --- /dev/null +++ b/examples/22-stream-stdin.php @@ -0,0 +1,27 @@ +post($url, array(), $in)->then(function (ResponseInterface $response) { + echo 'Received' . PHP_EOL . Psr7\str($response); +}, 'printf'); + +$loop->run(); diff --git a/examples/91-benchmark-download.php b/examples/91-benchmark-download.php new file mode 100644 index 00000000..10ad8e00 --- /dev/null +++ b/examples/91-benchmark-download.php @@ -0,0 +1,61 @@ +requestStreaming('GET', $url)->then(function (ResponseInterface $response) use ($loop) { + echo 'Headers received' . PHP_EOL; + echo RingCentral\Psr7\str($response); + + $stream = $response->getBody(); + assert($stream instanceof ReadableStreamInterface); + + // count number of bytes received + $bytes = 0; + $stream->on('data', function ($chunk) use (&$bytes) { + $bytes += strlen($chunk); + }); + + // report progress every 0.1s + $timer = $loop->addPeriodicTimer(0.1, function () use (&$bytes) { + echo "\rDownloaded " . $bytes . " bytes…"; + }); + + // report results once the stream closes + $time = microtime(true); + $stream->on('close', function() use (&$bytes, $timer, $loop, $time) { + $loop->cancelTimer($timer); + + $time = microtime(true) - $time; + + echo "\r" . 'Downloaded ' . $bytes . ' bytes in ' . round($time, 3) . 's => ' . round($bytes / $time / 1000000, 1) . ' MB/s' . PHP_EOL; + }); +}, 'printf'); + +$loop->run(); diff --git a/examples/92-benchmark-upload.php b/examples/92-benchmark-upload.php new file mode 100644 index 00000000..2b4e7ed6 --- /dev/null +++ b/examples/92-benchmark-upload.php @@ -0,0 +1,125 @@ +chunk = $chunk; + $this->count = $count; + } + + public function pause() + { + $this->paused = true; + } + + public function resume() + { + if (!$this->paused || $this->closed) { + return; + } + + // keep emitting until stream is paused + $this->paused = false; + while ($this->position < $this->count && !$this->paused) { + ++$this->position; + $this->emit('data', array($this->chunk)); + } + + // end once the last chunk has been written + if ($this->position >= $this->count) { + $this->emit('end'); + $this->close(); + } + } + + public function pipe(WritableStreamInterface $dest, array $options = array()) + { + return Util::pipe($this, $dest, $options); + } + + public function isReadable() + { + return !$this->closed; + } + + public function close() + { + if ($this->closed) { + return; + } + + $this->closed = true; + $this->count = 0; + $this->paused = true; + $this->emit('close'); + } + + public function getPosition() + { + return $this->position * strlen($this->chunk); + } +} + +$loop = React\EventLoop\Factory::create(); +$client = new Browser($loop); + +$url = isset($argv[1]) ? $argv[1] : 'http://httpbin.org/post'; +$n = isset($argv[2]) ? $argv[2] : 10; +$source = new ChunkRepeater(str_repeat('x', 1000000), $n); +$loop->futureTick(function () use ($source) { + $source->resume(); +}); + +echo 'POSTing ' . $n . ' MB to ' . $url . PHP_EOL; + +$start = microtime(true); +$report = $loop->addPeriodicTimer(0.05, function () use ($source, $start) { + printf("\r%d bytes in %0.3fs...", $source->getPosition(), microtime(true) - $start); +}); + +$client->post($url, array('Content-Length' => $n * 1000000), $source)->then(function (ResponseInterface $response) use ($source, $report, $loop, $start) { + $now = microtime(true); + $loop->cancelTimer($report); + + printf("\r%d bytes in %0.3fs => %.1f MB/s\n", $source->getPosition(), $now - $start, $source->getPosition() / ($now - $start) / 1000000); + + echo rtrim(preg_replace('/x{5,}/','x…', (string) $response->getBody()), PHP_EOL) . PHP_EOL; +}, function ($e) use ($loop, $report) { + $loop->cancelTimer($report); + echo 'Error: ' . $e->getMessage() . PHP_EOL; +}); + +$loop->run(); diff --git a/src/Browser.php b/src/Browser.php new file mode 100644 index 00000000..70e875a2 --- /dev/null +++ b/src/Browser.php @@ -0,0 +1,867 @@ + '127.0.0.1', + * 'tcp' => array( + * 'bindto' => '192.168.10.1:0' + * ), + * 'tls' => array( + * 'verify_peer' => false, + * 'verify_peer_name' => false + * ) + * )); + * + * $browser = new React\Http\Browser($loop, $connector); + * ``` + * + * @param LoopInterface $loop + * @param ConnectorInterface|null $connector [optional] Connector to use. + * Should be `null` in order to use default Connector. + */ + public function __construct(LoopInterface $loop, ConnectorInterface $connector = null) + { + $this->messageFactory = new MessageFactory(); + $this->transaction = new Transaction( + Sender::createFromLoop($loop, $connector, $this->messageFactory), + $this->messageFactory, + $loop + ); + } + + /** + * Sends an HTTP GET request + * + * ```php + * $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + * var_dump((string)$response->getBody()); + * }); + * ``` + * + * See also [example 01](../examples/01-google.php). + * + * > For BC reasons, this method accepts the `$url` as either a `string` + * value or as an `UriInterface`. It's recommended to explicitly cast any + * objects implementing `UriInterface` to `string`. + * + * @param string|UriInterface $url URL for the request. + * @param array $headers + * @return PromiseInterface + */ + public function get($url, array $headers = array()) + { + return $this->requestMayBeStreaming('GET', $url, $headers); + } + + /** + * Sends an HTTP POST request + * + * ```php + * $browser->post( + * $url, + * [ + * 'Content-Type' => 'application/json' + * ], + * json_encode($data) + * )->then(function (Psr\Http\Message\ResponseInterface $response) { + * var_dump(json_decode((string)$response->getBody())); + * }); + * ``` + * + * See also [example 04](../examples/04-post-json.php). + * + * This method is also commonly used to submit HTML form data: + * + * ```php + * $data = [ + * 'user' => 'Alice', + * 'password' => 'secret' + * ]; + * + * $browser->post( + * $url, + * [ + * 'Content-Type' => 'application/x-www-form-urlencoded' + * ], + * http_build_query($data) + * ); + * ``` + * + * This method will automatically add a matching `Content-Length` request + * header if the outgoing request body is a `string`. If you're using a + * streaming request body (`ReadableStreamInterface`), it will default to + * using `Transfer-Encoding: chunked` or you have to explicitly pass in a + * matching `Content-Length` request header like so: + * + * ```php + * $body = new React\Stream\ThroughStream(); + * $loop->addTimer(1.0, function () use ($body) { + * $body->end("hello world"); + * }); + * + * $browser->post($url, array('Content-Length' => '11'), $body); + * ``` + * + * > For BC reasons, this method accepts the `$url` as either a `string` + * value or as an `UriInterface`. It's recommended to explicitly cast any + * objects implementing `UriInterface` to `string`. + * + * @param string|UriInterface $url URL for the request. + * @param array $headers + * @param string|ReadableStreamInterface $contents + * @return PromiseInterface + */ + public function post($url, array $headers = array(), $contents = '') + { + return $this->requestMayBeStreaming('POST', $url, $headers, $contents); + } + + /** + * Sends an HTTP HEAD request + * + * ```php + * $browser->head($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + * var_dump($response->getHeaders()); + * }); + * ``` + * + * > For BC reasons, this method accepts the `$url` as either a `string` + * value or as an `UriInterface`. It's recommended to explicitly cast any + * objects implementing `UriInterface` to `string`. + * + * @param string|UriInterface $url URL for the request. + * @param array $headers + * @return PromiseInterface + */ + public function head($url, array $headers = array()) + { + return $this->requestMayBeStreaming('HEAD', $url, $headers); + } + + /** + * Sends an HTTP PATCH request + * + * ```php + * $browser->patch( + * $url, + * [ + * 'Content-Type' => 'application/json' + * ], + * json_encode($data) + * )->then(function (Psr\Http\Message\ResponseInterface $response) { + * var_dump(json_decode((string)$response->getBody())); + * }); + * ``` + * + * This method will automatically add a matching `Content-Length` request + * header if the outgoing request body is a `string`. If you're using a + * streaming request body (`ReadableStreamInterface`), it will default to + * using `Transfer-Encoding: chunked` or you have to explicitly pass in a + * matching `Content-Length` request header like so: + * + * ```php + * $body = new React\Stream\ThroughStream(); + * $loop->addTimer(1.0, function () use ($body) { + * $body->end("hello world"); + * }); + * + * $browser->patch($url, array('Content-Length' => '11'), $body); + * ``` + * + * > For BC reasons, this method accepts the `$url` as either a `string` + * value or as an `UriInterface`. It's recommended to explicitly cast any + * objects implementing `UriInterface` to `string`. + * + * @param string|UriInterface $url URL for the request. + * @param array $headers + * @param string|ReadableStreamInterface $contents + * @return PromiseInterface + */ + public function patch($url, array $headers = array(), $contents = '') + { + return $this->requestMayBeStreaming('PATCH', $url , $headers, $contents); + } + + /** + * Sends an HTTP PUT request + * + * ```php + * $browser->put( + * $url, + * [ + * 'Content-Type' => 'text/xml' + * ], + * $xml->asXML() + * )->then(function (Psr\Http\Message\ResponseInterface $response) { + * var_dump((string)$response->getBody()); + * }); + * ``` + * + * See also [example 05](../examples/05-put-xml.php). + * + * This method will automatically add a matching `Content-Length` request + * header if the outgoing request body is a `string`. If you're using a + * streaming request body (`ReadableStreamInterface`), it will default to + * using `Transfer-Encoding: chunked` or you have to explicitly pass in a + * matching `Content-Length` request header like so: + * + * ```php + * $body = new React\Stream\ThroughStream(); + * $loop->addTimer(1.0, function () use ($body) { + * $body->end("hello world"); + * }); + * + * $browser->put($url, array('Content-Length' => '11'), $body); + * ``` + * + * > For BC reasons, this method accepts the `$url` as either a `string` + * value or as an `UriInterface`. It's recommended to explicitly cast any + * objects implementing `UriInterface` to `string`. + * + * @param string|UriInterface $url URL for the request. + * @param array $headers + * @param string|ReadableStreamInterface $contents + * @return PromiseInterface + */ + public function put($url, array $headers = array(), $contents = '') + { + return $this->requestMayBeStreaming('PUT', $url, $headers, $contents); + } + + /** + * Sends an HTTP DELETE request + * + * ```php + * $browser->delete($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + * var_dump((string)$response->getBody()); + * }); + * ``` + * + * > For BC reasons, this method accepts the `$url` as either a `string` + * value or as an `UriInterface`. It's recommended to explicitly cast any + * objects implementing `UriInterface` to `string`. + * + * @param string|UriInterface $url URL for the request. + * @param array $headers + * @param string|ReadableStreamInterface $contents + * @return PromiseInterface + */ + public function delete($url, array $headers = array(), $contents = '') + { + return $this->requestMayBeStreaming('DELETE', $url, $headers, $contents); + } + + /** + * Sends an arbitrary HTTP request. + * + * The preferred way to send an HTTP request is by using the above + * [request methods](#request-methods), for example the [`get()`](#get) + * method to send an HTTP `GET` request. + * + * As an alternative, if you want to use a custom HTTP request method, you + * can use this method: + * + * ```php + * $browser->request('OPTIONS', $url)->then(function (Psr\Http\Message\ResponseInterface $response) { + * var_dump((string)$response->getBody()); + * }); + * ``` + * + * This method will automatically add a matching `Content-Length` request + * header if the size of the outgoing request body is known and non-empty. + * For an empty request body, if will only include a `Content-Length: 0` + * request header if the request method usually expects a request body (only + * applies to `POST`, `PUT` and `PATCH`). + * + * If you're using a streaming request body (`ReadableStreamInterface`), it + * will default to using `Transfer-Encoding: chunked` or you have to + * explicitly pass in a matching `Content-Length` request header like so: + * + * ```php + * $body = new React\Stream\ThroughStream(); + * $loop->addTimer(1.0, function () use ($body) { + * $body->end("hello world"); + * }); + * + * $browser->request('POST', $url, array('Content-Length' => '11'), $body); + * ``` + * + * > Note that this method is available as of v2.9.0 and always buffers the + * response body before resolving. + * It does not respect the deprecated [`streaming` option](#withoptions). + * If you want to stream the response body, you can use the + * [`requestStreaming()`](#requeststreaming) method instead. + * + * @param string $method HTTP request method, e.g. GET/HEAD/POST etc. + * @param string $url URL for the request + * @param array $headers Additional request headers + * @param string|ReadableStreamInterface $body HTTP request body contents + * @return PromiseInterface + * @since 2.9.0 + */ + public function request($method, $url, array $headers = array(), $body = '') + { + return $this->withOptions(array('streaming' => false))->requestMayBeStreaming($method, $url, $headers, $body); + } + + /** + * Sends an arbitrary HTTP request and receives a streaming response without buffering the response body. + * + * The preferred way to send an HTTP request is by using the above + * [request methods](#request-methods), for example the [`get()`](#get) + * method to send an HTTP `GET` request. Each of these methods will buffer + * the whole response body in memory by default. This is easy to get started + * and works reasonably well for smaller responses. + * + * In some situations, it's a better idea to use a streaming approach, where + * only small chunks have to be kept in memory. You can use this method to + * send an arbitrary HTTP request and receive a streaming response. It uses + * the same HTTP message API, but does not buffer the response body in + * memory. It only processes the response body in small chunks as data is + * received and forwards this data through [ReactPHP's Stream API](https://github.com/reactphp/stream). + * This works for (any number of) responses of arbitrary sizes. + * + * ```php + * $browser->requestStreaming('GET', $url)->then(function (Psr\Http\Message\ResponseInterface $response) { + * $body = $response->getBody(); + * assert($body instanceof Psr\Http\Message\StreamInterface); + * assert($body instanceof React\Stream\ReadableStreamInterface); + * + * $body->on('data', function ($chunk) { + * echo $chunk; + * }); + * + * $body->on('error', function (Exception $error) { + * echo 'Error: ' . $error->getMessage() . PHP_EOL; + * }); + * + * $body->on('close', function () { + * echo '[DONE]' . PHP_EOL; + * }); + * }); + * ``` + * + * See also [`ReadableStreamInterface`](https://github.com/reactphp/stream#readablestreaminterface) + * and the [streaming response](#streaming-response) for more details, + * examples and possible use-cases. + * + * This method will automatically add a matching `Content-Length` request + * header if the size of the outgoing request body is known and non-empty. + * For an empty request body, if will only include a `Content-Length: 0` + * request header if the request method usually expects a request body (only + * applies to `POST`, `PUT` and `PATCH`). + * + * If you're using a streaming request body (`ReadableStreamInterface`), it + * will default to using `Transfer-Encoding: chunked` or you have to + * explicitly pass in a matching `Content-Length` request header like so: + * + * ```php + * $body = new React\Stream\ThroughStream(); + * $loop->addTimer(1.0, function () use ($body) { + * $body->end("hello world"); + * }); + * + * $browser->requestStreaming('POST', $url, array('Content-Length' => '11'), $body); + * ``` + * + * > Note that this method is available as of v2.9.0 and always resolves the + * response without buffering the response body. + * It does not respect the deprecated [`streaming` option](#withoptions). + * If you want to buffer the response body, use can use the + * [`request()`](#request) method instead. + * + * @param string $method HTTP request method, e.g. GET/HEAD/POST etc. + * @param string $url URL for the request + * @param array $headers Additional request headers + * @param string|ReadableStreamInterface $body HTTP request body contents + * @return PromiseInterface + * @since 2.9.0 + */ + public function requestStreaming($method, $url, $headers = array(), $contents = '') + { + return $this->withOptions(array('streaming' => true))->requestMayBeStreaming($method, $url, $headers, $contents); + } + + /** + * [Deprecated] Submits an array of field values similar to submitting a form (`application/x-www-form-urlencoded`). + * + * ```php + * // deprecated: see post() instead + * $browser->submit($url, array('user' => 'test', 'password' => 'secret')); + * ``` + * + * This method will automatically add a matching `Content-Length` request + * header for the encoded length of the given `$fields`. + * + * > For BC reasons, this method accepts the `$url` as either a `string` + * value or as an `UriInterface`. It's recommended to explicitly cast any + * objects implementing `UriInterface` to `string`. + * + * @param string|UriInterface $url URL for the request. + * @param array $fields + * @param array $headers + * @param string $method + * @return PromiseInterface + * @deprecated 2.9.0 See self::post() instead. + * @see self::post() + */ + public function submit($url, array $fields, $headers = array(), $method = 'POST') + { + $headers['Content-Type'] = 'application/x-www-form-urlencoded'; + $contents = http_build_query($fields); + + return $this->requestMayBeStreaming($method, $url, $headers, $contents); + } + + /** + * [Deprecated] Sends an arbitrary instance implementing the [`RequestInterface`](#requestinterface) (PSR-7). + * + * The preferred way to send an HTTP request is by using the above + * [request methods](#request-methods), for example the [`get()`](#get) + * method to send an HTTP `GET` request. + * + * As an alternative, if you want to use a custom HTTP request method, you + * can use this method: + * + * ```php + * $request = new Request('OPTIONS', $url); + * + * // deprecated: see request() instead + * $browser->send($request)->then(…); + * ``` + * + * This method will automatically add a matching `Content-Length` request + * header if the size of the outgoing request body is known and non-empty. + * For an empty request body, if will only include a `Content-Length: 0` + * request header if the request method usually expects a request body (only + * applies to `POST`, `PUT` and `PATCH`). + * + * @param RequestInterface $request + * @return PromiseInterface + * @deprecated 2.9.0 See self::request() instead. + * @see self::request() + */ + public function send(RequestInterface $request) + { + if ($this->baseUrl !== null) { + // ensure we're actually below the base URL + $request = $request->withUri($this->messageFactory->expandBase($request->getUri(), $this->baseUrl)); + } + + return $this->transaction->send($request); + } + + /** + * Changes the maximum timeout used for waiting for pending requests. + * + * You can pass in the number of seconds to use as a new timeout value: + * + * ```php + * $browser = $browser->withTimeout(10.0); + * ``` + * + * You can pass in a bool `false` to disable any timeouts. In this case, + * requests can stay pending forever: + * + * ```php + * $browser = $browser->withTimeout(false); + * ``` + * + * You can pass in a bool `true` to re-enable default timeout handling. This + * will respects PHP's `default_socket_timeout` setting (default 60s): + * + * ```php + * $browser = $browser->withTimeout(true); + * ``` + * + * See also [timeouts](#timeouts) for more details about timeout handling. + * + * Notice that the [`Browser`](#browser) is an immutable object, i.e. this + * method actually returns a *new* [`Browser`](#browser) instance with the + * given timeout value applied. + * + * @param bool|number $timeout + * @return self + */ + public function withTimeout($timeout) + { + if ($timeout === true) { + $timeout = null; + } elseif ($timeout === false) { + $timeout = -1; + } elseif ($timeout < 0) { + $timeout = 0; + } + + return $this->withOptions(array( + 'timeout' => $timeout, + )); + } + + /** + * Changes how HTTP redirects will be followed. + * + * You can pass in the maximum number of redirects to follow: + * + * ```php + * $new = $browser->withFollowRedirects(5); + * ``` + * + * The request will automatically be rejected when the number of redirects + * is exceeded. You can pass in a `0` to reject the request for any + * redirects encountered: + * + * ```php + * $browser = $browser->withFollowRedirects(0); + * + * $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + * // only non-redirected responses will now end up here + * var_dump($response->getHeaders()); + * }); + * ``` + * + * You can pass in a bool `false` to disable following any redirects. In + * this case, requests will resolve with the redirection response instead + * of following the `Location` response header: + * + * ```php + * $browser = $browser->withFollowRedirects(false); + * + * $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + * // any redirects will now end up here + * var_dump($response->getHeaderLine('Location')); + * }); + * ``` + * + * You can pass in a bool `true` to re-enable default redirect handling. + * This defaults to following a maximum of 10 redirects: + * + * ```php + * $browser = $browser->withFollowRedirects(true); + * ``` + * + * See also [redirects](#redirects) for more details about redirect handling. + * + * Notice that the [`Browser`](#browser) is an immutable object, i.e. this + * method actually returns a *new* [`Browser`](#browser) instance with the + * given redirect setting applied. + * + * @param bool|int $followRedirects + * @return self + */ + public function withFollowRedirects($followRedirects) + { + return $this->withOptions(array( + 'followRedirects' => $followRedirects !== false, + 'maxRedirects' => \is_bool($followRedirects) ? null : $followRedirects + )); + } + + /** + * Changes whether non-successful HTTP response status codes (4xx and 5xx) will be rejected. + * + * You can pass in a bool `false` to disable rejecting incoming responses + * that use a 4xx or 5xx response status code. In this case, requests will + * resolve with the response message indicating an error condition: + * + * ```php + * $browser = $browser->withRejectErrorResponse(false); + * + * $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + * // any HTTP response will now end up here + * var_dump($response->getStatusCode(), $response->getReasonPhrase()); + * }); + * ``` + * + * You can pass in a bool `true` to re-enable default status code handling. + * This defaults to rejecting any response status codes in the 4xx or 5xx + * range: + * + * ```php + * $browser = $browser->withRejectErrorResponse(true); + * + * $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + * // any successful HTTP response will now end up here + * var_dump($response->getStatusCode(), $response->getReasonPhrase()); + * }, function (Exception $e) { + * if ($e instanceof React\Http\Message\ResponseException) { + * // any HTTP response error message will now end up here + * $response = $e->getResponse(); + * var_dump($response->getStatusCode(), $response->getReasonPhrase()); + * } else { + * var_dump($e->getMessage()); + * } + * }); + * ``` + * + * Notice that the [`Browser`](#browser) is an immutable object, i.e. this + * method actually returns a *new* [`Browser`](#browser) instance with the + * given setting applied. + * + * @param bool $obeySuccessCode + * @return self + */ + public function withRejectErrorResponse($obeySuccessCode) + { + return $this->withOptions(array( + 'obeySuccessCode' => $obeySuccessCode, + )); + } + + /** + * Changes the base URL used to resolve relative URLs to. + * + * If you configure a base URL, any requests to relative URLs will be + * processed by first prepending this absolute base URL. Note that this + * merely prepends the base URL and does *not* resolve any relative path + * references (like `../` etc.). This is mostly useful for (RESTful) API + * calls where all endpoints (URLs) are located under a common base URL. + * + * ```php + * $browser = $browser->withBase('http://api.example.com/v3'); + * + * // will request http://api.example.com/v3/example + * $browser->get('/example')->then(…); + * ``` + * + * You can pass in a `null` base URL to return a new instance that does not + * use a base URL: + * + * ```php + * $browser = $browser->withBase(null); + * ``` + * + * Accordingly, any requests using relative URLs to a browser that does not + * use a base URL can not be completed and will be rejected without sending + * a request. + * + * This method will throw an `InvalidArgumentException` if the given + * `$baseUrl` argument is not a valid URL. + * + * Notice that the [`Browser`](#browser) is an immutable object, i.e. the `withBase()` method + * actually returns a *new* [`Browser`](#browser) instance with the given base URL applied. + * + * > For BC reasons, this method accepts the `$baseUrl` as either a `string` + * value or as an `UriInterface`. It's recommended to explicitly cast any + * objects implementing `UriInterface` to `string`. + * + * > Changelog: As of v2.9.0 this method accepts a `null` value to reset the + * base URL. Earlier versions had to use the deprecated `withoutBase()` + * method to reset the base URL. + * + * @param string|null|UriInterface $baseUrl absolute base URL + * @return self + * @throws InvalidArgumentException if the given $baseUrl is not a valid absolute URL + * @see self::withoutBase() + */ + public function withBase($baseUrl) + { + $browser = clone $this; + if ($baseUrl === null) { + $browser->baseUrl = null; + return $browser; + } + + $browser->baseUrl = $this->messageFactory->uri($baseUrl); + if (!\in_array($browser->baseUrl->getScheme(), array('http', 'https')) || $browser->baseUrl->getHost() === '') { + throw new \InvalidArgumentException('Base URL must be absolute'); + } + + return $browser; + } + + /** + * Changes the HTTP protocol version that will be used for all subsequent requests. + * + * All the above [request methods](#request-methods) default to sending + * requests as HTTP/1.1. This is the preferred HTTP protocol version which + * also provides decent backwards-compatibility with legacy HTTP/1.0 + * servers. As such, there should rarely be a need to explicitly change this + * protocol version. + * + * If you want to explicitly use the legacy HTTP/1.0 protocol version, you + * can use this method: + * + * ```php + * $newBrowser = $browser->withProtocolVersion('1.0'); + * + * $newBrowser->get($url)->then(…); + * ``` + * + * Notice that the [`Browser`](#browser) is an immutable object, i.e. this + * method actually returns a *new* [`Browser`](#browser) instance with the + * new protocol version applied. + * + * @param string $protocolVersion HTTP protocol version to use, must be one of "1.1" or "1.0" + * @return self + * @throws InvalidArgumentException + * @since 2.8.0 + */ + public function withProtocolVersion($protocolVersion) + { + if (!\in_array($protocolVersion, array('1.0', '1.1'), true)) { + throw new InvalidArgumentException('Invalid HTTP protocol version, must be one of "1.1" or "1.0"'); + } + + $browser = clone $this; + $browser->protocolVersion = (string) $protocolVersion; + + return $browser; + } + + /** + * Changes the maximum size for buffering a response body. + * + * The preferred way to send an HTTP request is by using the above + * [request methods](#request-methods), for example the [`get()`](#get) + * method to send an HTTP `GET` request. Each of these methods will buffer + * the whole response body in memory by default. This is easy to get started + * and works reasonably well for smaller responses. + * + * By default, the response body buffer will be limited to 16 MiB. If the + * response body exceeds this maximum size, the request will be rejected. + * + * You can pass in the maximum number of bytes to buffer: + * + * ```php + * $browser = $browser->withResponseBuffer(1024 * 1024); + * + * $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + * // response body will not exceed 1 MiB + * var_dump($response->getHeaders(), (string) $response->getBody()); + * }); + * ``` + * + * Note that the response body buffer has to be kept in memory for each + * pending request until its transfer is completed and it will only be freed + * after a pending request is fulfilled. As such, increasing this maximum + * buffer size to allow larger response bodies is usually not recommended. + * Instead, you can use the [`requestStreaming()` method](#requeststreaming) + * to receive responses with arbitrary sizes without buffering. Accordingly, + * this maximum buffer size setting has no effect on streaming responses. + * + * Notice that the [`Browser`](#browser) is an immutable object, i.e. this + * method actually returns a *new* [`Browser`](#browser) instance with the + * given setting applied. + * + * @param int $maximumSize + * @return self + * @see self::requestStreaming() + */ + public function withResponseBuffer($maximumSize) + { + return $this->withOptions(array( + 'maximumSize' => $maximumSize + )); + } + + /** + * [Deprecated] Changes the [options](#options) to use: + * + * The [`Browser`](#browser) class exposes several options for the handling of + * HTTP transactions. These options resemble some of PHP's + * [HTTP context options](http://php.net/manual/en/context.http.php) and + * can be controlled via the following API (and their defaults): + * + * ```php + * // deprecated + * $newBrowser = $browser->withOptions(array( + * 'timeout' => null, // see withTimeout() instead + * 'followRedirects' => true, // see withFollowRedirects() instead + * 'maxRedirects' => 10, // see withFollowRedirects() instead + * 'obeySuccessCode' => true, // see withRejectErrorResponse() instead + * 'streaming' => false, // deprecated, see requestStreaming() instead + * )); + * ``` + * + * See also [timeouts](#timeouts), [redirects](#redirects) and + * [streaming](#streaming) for more details. + * + * Notice that the [`Browser`](#browser) is an immutable object, i.e. this + * method actually returns a *new* [`Browser`](#browser) instance with the + * options applied. + * + * @param array $options + * @return self + * @deprecated 2.9.0 See self::withTimeout(), self::withFollowRedirects() and self::withRejectErrorResponse() instead. + * @see self::withTimeout() + * @see self::withFollowRedirects() + * @see self::withRejectErrorResponse() + */ + public function withOptions(array $options) + { + $browser = clone $this; + $browser->transaction = $this->transaction->withOptions($options); + + return $browser; + } + + /** + * [Deprecated] Removes the base URL. + * + * ```php + * // deprecated: see withBase() instead + * $newBrowser = $browser->withoutBase(); + * ``` + * + * Notice that the [`Browser`](#browser) is an immutable object, i.e. the `withoutBase()` method + * actually returns a *new* [`Browser`](#browser) instance without any base URL applied. + * + * See also [`withBase()`](#withbase). + * + * @return self + * @deprecated 2.9.0 See self::withBase() instead. + * @see self::withBase() + */ + public function withoutBase() + { + return $this->withBase(null); + } + + /** + * @param string $method + * @param string|UriInterface $url + * @param array $headers + * @param string|ReadableStreamInterface $contents + * @return PromiseInterface + */ + private function requestMayBeStreaming($method, $url, array $headers = array(), $contents = '') + { + return $this->send($this->messageFactory->request($method, $url, $headers, $contents, $this->protocolVersion)); + } +} diff --git a/src/Io/ChunkedEncoder.php b/src/Io/ChunkedEncoder.php index d4e53b91..c84ef54f 100644 --- a/src/Io/ChunkedEncoder.php +++ b/src/Io/ChunkedEncoder.php @@ -17,7 +17,7 @@ class ChunkedEncoder extends EventEmitter implements ReadableStreamInterface { private $input; - private $closed; + private $closed = false; public function __construct(ReadableStreamInterface $input) { @@ -46,9 +46,7 @@ public function resume() public function pipe(WritableStreamInterface $dest, array $options = array()) { - Util::pipe($this, $dest, $options); - - return $dest; + return Util::pipe($this, $dest, $options); } public function close() @@ -67,13 +65,11 @@ public function close() /** @internal */ public function handleData($data) { - if ($data === '') { - return; + if ($data !== '') { + $this->emit('data', array( + \dechex(\strlen($data)) . "\r\n" . $data . "\r\n" + )); } - - $completeChunk = $this->createChunk($data); - - $this->emit('data', array($completeChunk)); } /** @internal */ @@ -93,18 +89,4 @@ public function handleEnd() $this->close(); } } - - /** - * @param string $data - string to be transformed in an valid - * HTTP encoded chunk string - * @return string - */ - private function createChunk($data) - { - $byteSize = \dechex(\strlen($data)); - $chunkBeginning = $byteSize . "\r\n"; - - return $chunkBeginning . $data . "\r\n"; - } - } diff --git a/src/Io/Sender.php b/src/Io/Sender.php new file mode 100644 index 00000000..e9c0a600 --- /dev/null +++ b/src/Io/Sender.php @@ -0,0 +1,161 @@ +http = $http; + $this->messageFactory = $messageFactory; + } + + /** + * + * @internal + * @param RequestInterface $request + * @return PromiseInterface Promise + */ + public function send(RequestInterface $request) + { + $body = $request->getBody(); + $size = $body->getSize(); + + if ($size !== null && $size !== 0) { + // automatically assign a "Content-Length" request header if the body size is known and non-empty + $request = $request->withHeader('Content-Length', (string)$size); + } elseif ($size === 0 && \in_array($request->getMethod(), array('POST', 'PUT', 'PATCH'))) { + // only assign a "Content-Length: 0" request header if the body is expected for certain methods + $request = $request->withHeader('Content-Length', '0'); + } elseif ($body instanceof ReadableStreamInterface && $body->isReadable() && !$request->hasHeader('Content-Length')) { + // use "Transfer-Encoding: chunked" when this is a streaming body and body size is unknown + $request = $request->withHeader('Transfer-Encoding', 'chunked'); + } else { + // do not use chunked encoding if size is known or if this is an empty request body + $size = 0; + } + + $headers = array(); + foreach ($request->getHeaders() as $name => $values) { + $headers[$name] = implode(', ', $values); + } + + $requestStream = $this->http->request($request->getMethod(), (string)$request->getUri(), $headers, $request->getProtocolVersion()); + + $deferred = new Deferred(function ($_, $reject) use ($requestStream) { + // close request stream if request is cancelled + $reject(new \RuntimeException('Request cancelled')); + $requestStream->close(); + }); + + $requestStream->on('error', function($error) use ($deferred) { + $deferred->reject($error); + }); + + $messageFactory = $this->messageFactory; + $requestStream->on('response', function (ResponseStream $responseStream) use ($deferred, $messageFactory, $request) { + // apply response header values from response stream + $deferred->resolve($messageFactory->response( + $responseStream->getVersion(), + $responseStream->getCode(), + $responseStream->getReasonPhrase(), + $responseStream->getHeaders(), + $responseStream, + $request->getMethod() + )); + }); + + if ($body instanceof ReadableStreamInterface) { + if ($body->isReadable()) { + // length unknown => apply chunked transfer-encoding + if ($size === null) { + $body = new ChunkedEncoder($body); + } + + // pipe body into request stream + // add dummy write to immediately start request even if body does not emit any data yet + $body->pipe($requestStream); + $requestStream->write(''); + + $body->on('close', $close = function () use ($deferred, $requestStream) { + $deferred->reject(new \RuntimeException('Request failed because request body closed unexpectedly')); + $requestStream->close(); + }); + $body->on('error', function ($e) use ($deferred, $requestStream, $close, $body) { + $body->removeListener('close', $close); + $deferred->reject(new \RuntimeException('Request failed because request body reported an error', 0, $e)); + $requestStream->close(); + }); + $body->on('end', function () use ($close, $body) { + $body->removeListener('close', $close); + }); + } else { + // stream is not readable => end request without body + $requestStream->end(); + } + } else { + // body is fully buffered => write as one chunk + $requestStream->end((string)$body); + } + + return $deferred->promise(); + } +} diff --git a/src/Io/Transaction.php b/src/Io/Transaction.php new file mode 100644 index 00000000..4b8cd390 --- /dev/null +++ b/src/Io/Transaction.php @@ -0,0 +1,305 @@ +sender = $sender; + $this->messageFactory = $messageFactory; + $this->loop = $loop; + } + + /** + * @param array $options + * @return self returns new instance, without modifying existing instance + */ + public function withOptions(array $options) + { + $transaction = clone $this; + foreach ($options as $name => $value) { + if (property_exists($transaction, $name)) { + // restore default value if null is given + if ($value === null) { + $default = new self($this->sender, $this->messageFactory, $this->loop); + $value = $default->$name; + } + + $transaction->$name = $value; + } + } + + return $transaction; + } + + public function send(RequestInterface $request) + { + $deferred = new Deferred(function () use (&$deferred) { + if (isset($deferred->pending)) { + $deferred->pending->cancel(); + unset($deferred->pending); + } + }); + + $deferred->numRequests = 0; + + // use timeout from options or default to PHP's default_socket_timeout (60) + $timeout = (float)($this->timeout !== null ? $this->timeout : ini_get("default_socket_timeout")); + + $loop = $this->loop; + $this->next($request, $deferred)->then( + function (ResponseInterface $response) use ($deferred, $loop, &$timeout) { + if (isset($deferred->timeout)) { + $loop->cancelTimer($deferred->timeout); + unset($deferred->timeout); + } + $timeout = -1; + $deferred->resolve($response); + }, + function ($e) use ($deferred, $loop, &$timeout) { + if (isset($deferred->timeout)) { + $loop->cancelTimer($deferred->timeout); + unset($deferred->timeout); + } + $timeout = -1; + $deferred->reject($e); + } + ); + + if ($timeout < 0) { + return $deferred->promise(); + } + + $body = $request->getBody(); + if ($body instanceof ReadableStreamInterface && $body->isReadable()) { + $that = $this; + $body->on('close', function () use ($that, $deferred, &$timeout) { + if ($timeout >= 0) { + $that->applyTimeout($deferred, $timeout); + } + }); + } else { + $this->applyTimeout($deferred, $timeout); + } + + return $deferred->promise(); + } + + /** + * @internal + * @param Deferred $deferred + * @param number $timeout + * @return void + */ + public function applyTimeout(Deferred $deferred, $timeout) + { + $deferred->timeout = $this->loop->addTimer($timeout, function () use ($timeout, $deferred) { + $deferred->reject(new \RuntimeException( + 'Request timed out after ' . $timeout . ' seconds' + )); + if (isset($deferred->pending)) { + $deferred->pending->cancel(); + unset($deferred->pending); + } + }); + } + + private function next(RequestInterface $request, Deferred $deferred) + { + $this->progress('request', array($request)); + + $that = $this; + ++$deferred->numRequests; + + $promise = $this->sender->send($request); + + if (!$this->streaming) { + $promise = $promise->then(function ($response) use ($deferred, $that) { + return $that->bufferResponse($response, $deferred); + }); + } + + $deferred->pending = $promise; + + return $promise->then( + function (ResponseInterface $response) use ($request, $that, $deferred) { + return $that->onResponse($response, $request, $deferred); + } + ); + } + + /** + * @internal + * @param ResponseInterface $response + * @return PromiseInterface Promise + */ + public function bufferResponse(ResponseInterface $response, $deferred) + { + $stream = $response->getBody(); + + $size = $stream->getSize(); + if ($size !== null && $size > $this->maximumSize) { + $stream->close(); + return \React\Promise\reject(new \OverflowException( + 'Response body size of ' . $size . ' bytes exceeds maximum of ' . $this->maximumSize . ' bytes', + \defined('SOCKET_EMSGSIZE') ? \SOCKET_EMSGSIZE : 0 + )); + } + + // body is not streaming => already buffered + if (!$stream instanceof ReadableStreamInterface) { + return \React\Promise\resolve($response); + } + + // buffer stream and resolve with buffered body + $messageFactory = $this->messageFactory; + $maximumSize = $this->maximumSize; + $promise = \React\Promise\Stream\buffer($stream, $maximumSize)->then( + function ($body) use ($response, $messageFactory) { + return $response->withBody($messageFactory->body($body)); + }, + function ($e) use ($stream, $maximumSize) { + // try to close stream if buffering fails (or is cancelled) + $stream->close(); + + if ($e instanceof \OverflowException) { + $e = new \OverflowException( + 'Response body size exceeds maximum of ' . $maximumSize . ' bytes', + \defined('SOCKET_EMSGSIZE') ? \SOCKET_EMSGSIZE : 0 + ); + } + + throw $e; + } + ); + + $deferred->pending = $promise; + + return $promise; + } + + /** + * @internal + * @param ResponseInterface $response + * @param RequestInterface $request + * @throws ResponseException + * @return ResponseInterface|PromiseInterface + */ + public function onResponse(ResponseInterface $response, RequestInterface $request, $deferred) + { + $this->progress('response', array($response, $request)); + + // follow 3xx (Redirection) response status codes if Location header is present and not explicitly disabled + // @link https://tools.ietf.org/html/rfc7231#section-6.4 + if ($this->followRedirects && ($response->getStatusCode() >= 300 && $response->getStatusCode() < 400) && $response->hasHeader('Location')) { + return $this->onResponseRedirect($response, $request, $deferred); + } + + // only status codes 200-399 are considered to be valid, reject otherwise + if ($this->obeySuccessCode && ($response->getStatusCode() < 200 || $response->getStatusCode() >= 400)) { + throw new ResponseException($response); + } + + // resolve our initial promise + return $response; + } + + /** + * @param ResponseInterface $response + * @param RequestInterface $request + * @return PromiseInterface + * @throws \RuntimeException + */ + private function onResponseRedirect(ResponseInterface $response, RequestInterface $request, Deferred $deferred) + { + // resolve location relative to last request URI + $location = $this->messageFactory->uriRelative($request->getUri(), $response->getHeaderLine('Location')); + + $request = $this->makeRedirectRequest($request, $location); + $this->progress('redirect', array($request)); + + if ($deferred->numRequests >= $this->maxRedirects) { + throw new \RuntimeException('Maximum number of redirects (' . $this->maxRedirects . ') exceeded'); + } + + return $this->next($request, $deferred); + } + + /** + * @param RequestInterface $request + * @param UriInterface $location + * @return RequestInterface + */ + private function makeRedirectRequest(RequestInterface $request, UriInterface $location) + { + $originalHost = $request->getUri()->getHost(); + $request = $request + ->withoutHeader('Host') + ->withoutHeader('Content-Type') + ->withoutHeader('Content-Length'); + + // Remove authorization if changing hostnames (but not if just changing ports or protocols). + if ($location->getHost() !== $originalHost) { + $request = $request->withoutHeader('Authorization'); + } + + // naïve approach.. + $method = ($request->getMethod() === 'HEAD') ? 'HEAD' : 'GET'; + + return $this->messageFactory->request($method, $location, $request->getHeaders()); + } + + private function progress($name, array $args = array()) + { + return; + + echo $name; + + foreach ($args as $arg) { + echo ' '; + if ($arg instanceof ResponseInterface) { + echo 'HTTP/' . $arg->getProtocolVersion() . ' ' . $arg->getStatusCode() . ' ' . $arg->getReasonPhrase(); + } elseif ($arg instanceof RequestInterface) { + echo $arg->getMethod() . ' ' . $arg->getRequestTarget() . ' HTTP/' . $arg->getProtocolVersion(); + } else { + echo $arg; + } + } + + echo PHP_EOL; + } +} diff --git a/src/Message/MessageFactory.php b/src/Message/MessageFactory.php new file mode 100644 index 00000000..eaa144cd --- /dev/null +++ b/src/Message/MessageFactory.php @@ -0,0 +1,139 @@ +body($content), $protocolVersion); + } + + /** + * Creates a new instance of ResponseInterface for the given response parameters + * + * @param string $protocolVersion + * @param int $status + * @param string $reason + * @param array $headers + * @param ReadableStreamInterface|string $body + * @param ?string $requestMethod + * @return Response + * @uses self::body() + */ + public function response($protocolVersion, $status, $reason, $headers = array(), $body = '', $requestMethod = null) + { + $response = new Response($status, $headers, $body instanceof ReadableStreamInterface ? null : $body, $protocolVersion, $reason); + + if ($body instanceof ReadableStreamInterface) { + $length = null; + $code = $response->getStatusCode(); + if ($requestMethod === 'HEAD' || ($code >= 100 && $code < 200) || $code == 204 || $code == 304) { + $length = 0; + } elseif (\strtolower($response->getHeaderLine('Transfer-Encoding')) === 'chunked') { + $length = null; + } elseif ($response->hasHeader('Content-Length')) { + $length = (int)$response->getHeaderLine('Content-Length'); + } + + $response = $response->withBody(new ReadableBodyStream($body, $length)); + } + + return $response; + } + + /** + * Creates a new instance of StreamInterface for the given body contents + * + * @param ReadableStreamInterface|string $body + * @return StreamInterface + */ + public function body($body) + { + if ($body instanceof ReadableStreamInterface) { + return new ReadableBodyStream($body); + } + + return \RingCentral\Psr7\stream_for($body); + } + + /** + * Creates a new instance of UriInterface for the given URI string + * + * @param string $uri + * @return UriInterface + */ + public function uri($uri) + { + return new Uri($uri); + } + + /** + * Creates a new instance of UriInterface for the given URI string relative to the given base URI + * + * @param UriInterface $base + * @param string $uri + * @return UriInterface + */ + public function uriRelative(UriInterface $base, $uri) + { + return Uri::resolve($base, $uri); + } + + /** + * Resolves the given relative or absolute $uri by appending it behind $this base URI + * + * The given $uri parameter can be either a relative or absolute URI and + * as such can not contain any URI template placeholders. + * + * As such, the outcome of this method represents a valid, absolute URI + * which will be returned as an instance implementing `UriInterface`. + * + * If the given $uri is a relative URI, it will simply be appended behind $base URI. + * + * If the given $uri is an absolute URI, it will simply be returned as-is. + * + * @param UriInterface $uri + * @param UriInterface $base + * @return UriInterface + */ + public function expandBase(UriInterface $uri, UriInterface $base) + { + if ($uri->getScheme() !== '') { + return $uri; + } + + $uri = (string)$uri; + $base = (string)$base; + + if ($uri !== '' && substr($base, -1) !== '/' && substr($uri, 0, 1) !== '?') { + $base .= '/'; + } + + if (isset($uri[0]) && $uri[0] === '/') { + $uri = substr($uri, 1); + } + + return $this->uri($base . $uri); + } +} diff --git a/src/Message/ReadableBodyStream.php b/src/Message/ReadableBodyStream.php new file mode 100644 index 00000000..bb0064e0 --- /dev/null +++ b/src/Message/ReadableBodyStream.php @@ -0,0 +1,153 @@ +input = $input; + $this->size = $size; + + $that = $this; + $pos =& $this->position; + $input->on('data', function ($data) use ($that, &$pos, $size) { + $that->emit('data', array($data)); + + $pos += \strlen($data); + if ($size !== null && $pos >= $size) { + $that->handleEnd(); + } + }); + $input->on('error', function ($error) use ($that) { + $that->emit('error', array($error)); + $that->close(); + }); + $input->on('end', array($that, 'handleEnd')); + $input->on('close', array($that, 'close')); + } + + public function close() + { + if (!$this->closed) { + $this->closed = true; + $this->input->close(); + + $this->emit('close'); + $this->removeAllListeners(); + } + } + + public function isReadable() + { + return $this->input->isReadable(); + } + + public function pause() + { + $this->input->pause(); + } + + public function resume() + { + $this->input->resume(); + } + + public function pipe(WritableStreamInterface $dest, array $options = array()) + { + Util::pipe($this, $dest, $options); + + return $dest; + } + + public function eof() + { + return !$this->isReadable(); + } + + public function __toString() + { + return ''; + } + + public function detach() + { + throw new \BadMethodCallException(); + } + + public function getSize() + { + return $this->size; + } + + public function tell() + { + throw new \BadMethodCallException(); + } + + public function isSeekable() + { + return false; + } + + public function seek($offset, $whence = SEEK_SET) + { + throw new \BadMethodCallException(); + } + + public function rewind() + { + throw new \BadMethodCallException(); + } + + public function isWritable() + { + return false; + } + + public function write($string) + { + throw new \BadMethodCallException(); + } + + public function read($length) + { + throw new \BadMethodCallException(); + } + + public function getContents() + { + throw new \BadMethodCallException(); + } + + public function getMetadata($key = null) + { + return ($key === null) ? array() : null; + } + + /** @internal */ + public function handleEnd() + { + if ($this->position !== $this->size && $this->size !== null) { + $this->emit('error', array(new \UnderflowException('Unexpected end of response body after ' . $this->position . '/' . $this->size . ' bytes'))); + } else { + $this->emit('end'); + } + + $this->close(); + } +} diff --git a/src/Message/ResponseException.php b/src/Message/ResponseException.php new file mode 100644 index 00000000..88272242 --- /dev/null +++ b/src/Message/ResponseException.php @@ -0,0 +1,43 @@ +getStatusCode() . ' (' . $response->getReasonPhrase() . ')'; + } + if ($code === null) { + $code = $response->getStatusCode(); + } + parent::__construct($message, $code, $previous); + + $this->response = $response; + } + + /** + * Access its underlying [`ResponseInterface`](#responseinterface) object. + * + * @return ResponseInterface + */ + public function getResponse() + { + return $this->response; + } +} diff --git a/tests/BrowserTest.php b/tests/BrowserTest.php new file mode 100644 index 00000000..56a28303 --- /dev/null +++ b/tests/BrowserTest.php @@ -0,0 +1,414 @@ +loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $this->sender = $this->getMockBuilder('React\Http\Io\Transaction')->disableOriginalConstructor()->getMock(); + $this->browser = new Browser($this->loop); + + $ref = new \ReflectionProperty($this->browser, 'transaction'); + $ref->setAccessible(true); + $ref->setValue($this->browser, $this->sender); + } + + public function testGetSendsGetRequest() + { + $that = $this; + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { + $that->assertEquals('GET', $request->getMethod()); + return true; + }))->willReturn(new Promise(function () { })); + + $this->browser->get('http://example.com/'); + } + + public function testPostSendsPostRequest() + { + $that = $this; + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { + $that->assertEquals('POST', $request->getMethod()); + return true; + }))->willReturn(new Promise(function () { })); + + $this->browser->post('http://example.com/'); + } + + public function testHeadSendsHeadRequest() + { + $that = $this; + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { + $that->assertEquals('HEAD', $request->getMethod()); + return true; + }))->willReturn(new Promise(function () { })); + + $this->browser->head('http://example.com/'); + } + + public function testPatchSendsPatchRequest() + { + $that = $this; + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { + $that->assertEquals('PATCH', $request->getMethod()); + return true; + }))->willReturn(new Promise(function () { })); + + $this->browser->patch('http://example.com/'); + } + + public function testPutSendsPutRequest() + { + $that = $this; + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { + $that->assertEquals('PUT', $request->getMethod()); + return true; + }))->willReturn(new Promise(function () { })); + + $this->browser->put('http://example.com/'); + } + + public function testDeleteSendsDeleteRequest() + { + $that = $this; + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { + $that->assertEquals('DELETE', $request->getMethod()); + return true; + }))->willReturn(new Promise(function () { })); + + $this->browser->delete('http://example.com/'); + } + + public function testRequestOptionsSendsPutRequestWithStreamingExplicitlyDisabled() + { + $this->sender->expects($this->once())->method('withOptions')->with(array('streaming' => false))->willReturnSelf(); + + $that = $this; + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { + $that->assertEquals('OPTIONS', $request->getMethod()); + return true; + }))->willReturn(new Promise(function () { })); + + $this->browser->request('OPTIONS', 'http://example.com/'); + } + + public function testRequestStreamingGetSendsGetRequestWithStreamingExplicitlyEnabled() + { + $this->sender->expects($this->once())->method('withOptions')->with(array('streaming' => true))->willReturnSelf(); + + $that = $this; + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { + $that->assertEquals('GET', $request->getMethod()); + return true; + }))->willReturn(new Promise(function () { })); + + $this->browser->requestStreaming('GET', 'http://example.com/'); + } + + public function testSubmitSendsPostRequest() + { + $that = $this; + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { + $that->assertEquals('POST', $request->getMethod()); + $that->assertEquals('application/x-www-form-urlencoded', $request->getHeaderLine('Content-Type')); + $that->assertEquals('', (string)$request->getBody()); + return true; + }))->willReturn(new Promise(function () { })); + + $this->browser->submit('http://example.com/', array()); + } + + public function testWithTimeoutTrueSetsDefaultTimeoutOption() + { + $this->sender->expects($this->once())->method('withOptions')->with(array('timeout' => null))->willReturnSelf(); + + $this->browser->withTimeout(true); + } + + public function testWithTimeoutFalseSetsNegativeTimeoutOption() + { + $this->sender->expects($this->once())->method('withOptions')->with(array('timeout' => -1))->willReturnSelf(); + + $this->browser->withTimeout(false); + } + + public function testWithTimeout10SetsTimeoutOption() + { + $this->sender->expects($this->once())->method('withOptions')->with(array('timeout' => 10))->willReturnSelf(); + + $this->browser->withTimeout(10); + } + + public function testWithTimeoutNegativeSetsZeroTimeoutOption() + { + $this->sender->expects($this->once())->method('withOptions')->with(array('timeout' => null))->willReturnSelf(); + + $this->browser->withTimeout(-10); + } + + public function testWithFollowRedirectsTrueSetsSenderOption() + { + $this->sender->expects($this->once())->method('withOptions')->with(array('followRedirects' => true, 'maxRedirects' => null))->willReturnSelf(); + + $this->browser->withFollowRedirects(true); + } + + public function testWithFollowRedirectsFalseSetsSenderOption() + { + $this->sender->expects($this->once())->method('withOptions')->with(array('followRedirects' => false, 'maxRedirects' => null))->willReturnSelf(); + + $this->browser->withFollowRedirects(false); + } + + public function testWithFollowRedirectsTenSetsSenderOption() + { + $this->sender->expects($this->once())->method('withOptions')->with(array('followRedirects' => true, 'maxRedirects' => 10))->willReturnSelf(); + + $this->browser->withFollowRedirects(10); + } + + public function testWithFollowRedirectsZeroSetsSenderOption() + { + $this->sender->expects($this->once())->method('withOptions')->with(array('followRedirects' => true, 'maxRedirects' => 0))->willReturnSelf(); + + $this->browser->withFollowRedirects(0); + } + + public function testWithRejectErrorResponseTrueSetsSenderOption() + { + $this->sender->expects($this->once())->method('withOptions')->with(array('obeySuccessCode' => true))->willReturnSelf(); + + $this->browser->withRejectErrorResponse(true); + } + + public function testWithRejectErrorResponseFalseSetsSenderOption() + { + $this->sender->expects($this->once())->method('withOptions')->with(array('obeySuccessCode' => false))->willReturnSelf(); + + $this->browser->withRejectErrorResponse(false); + } + + public function testWithResponseBufferThousandSetsSenderOption() + { + $this->sender->expects($this->once())->method('withOptions')->with(array('maximumSize' => 1000))->willReturnSelf(); + + $this->browser->withResponseBuffer(1000); + } + + public function testWithBase() + { + $browser = $this->browser->withBase('http://example.com/root'); + + $this->assertInstanceOf('React\Http\Browser', $browser); + $this->assertNotSame($this->browser, $browser); + } + + public function provideOtherUris() + { + return array( + 'empty returns base' => array( + 'http://example.com/base', + '', + 'http://example.com/base', + ), + 'absolute same as base returns base' => array( + 'http://example.com/base', + 'http://example.com/base', + 'http://example.com/base', + ), + 'absolute below base returns absolute' => array( + 'http://example.com/base', + 'http://example.com/base/another', + 'http://example.com/base/another', + ), + 'slash returns added slash' => array( + 'http://example.com/base', + '/', + 'http://example.com/base/', + ), + 'slash does not add duplicate slash if base already ends with slash' => array( + 'http://example.com/base/', + '/', + 'http://example.com/base/', + ), + 'relative is added behind base' => array( + 'http://example.com/base/', + 'test', + 'http://example.com/base/test', + ), + 'relative with slash is added behind base without duplicate slashes' => array( + 'http://example.com/base/', + '/test', + 'http://example.com/base/test', + ), + 'relative is added behind base with automatic slash inbetween' => array( + 'http://example.com/base', + 'test', + 'http://example.com/base/test', + ), + 'relative with slash is added behind base' => array( + 'http://example.com/base', + '/test', + 'http://example.com/base/test', + ), + 'query string is added behind base' => array( + 'http://example.com/base', + '?key=value', + 'http://example.com/base?key=value', + ), + 'query string is added behind base with slash' => array( + 'http://example.com/base/', + '?key=value', + 'http://example.com/base/?key=value', + ), + 'query string with slash is added behind base' => array( + 'http://example.com/base', + '/?key=value', + 'http://example.com/base/?key=value', + ), + 'absolute with query string below base is returned as-is' => array( + 'http://example.com/base', + 'http://example.com/base?test', + 'http://example.com/base?test', + ), + 'urlencoded special chars will stay as-is' => array( + 'http://example.com/%7Bversion%7D/', + '', + 'http://example.com/%7Bversion%7D/' + ), + 'special chars will be urlencoded' => array( + 'http://example.com/{version}/', + '', + 'http://example.com/%7Bversion%7D/' + ), + 'other domain' => array( + 'http://example.com/base/', + 'http://example.org/base/', + 'http://example.org/base/' + ), + 'other scheme' => array( + 'http://example.com/base/', + 'https://example.com/base/', + 'https://example.com/base/' + ), + 'other port' => array( + 'http://example.com/base/', + 'http://example.com:81/base/', + 'http://example.com:81/base/' + ), + 'other path' => array( + 'http://example.com/base/', + 'http://example.com/other/', + 'http://example.com/other/' + ), + 'other path due to missing slash' => array( + 'http://example.com/base/', + 'http://example.com/other', + 'http://example.com/other' + ), + ); + } + + /** + * @dataProvider provideOtherUris + * @param string $uri + * @param string $expected + */ + public function testResolveUriWithBaseEndsWithoutSlash($base, $uri, $expectedAbsolute) + { + $browser = $this->browser->withBase($base); + + $that = $this; + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($expectedAbsolute, $that) { + $that->assertEquals($expectedAbsolute, $request->getUri()); + return true; + }))->willReturn(new Promise(function () { })); + + $browser->get($uri); + } + + public function testWithBaseUrlNotAbsoluteFails() + { + $this->setExpectedException('InvalidArgumentException'); + $this->browser->withBase('hello'); + } + + public function testWithBaseUrlInvalidSchemeFails() + { + $this->setExpectedException('InvalidArgumentException'); + $this->browser->withBase('ftp://example.com'); + } + + public function testWithoutBaseFollowedByGetRequestTriesToSendIncompleteRequestUrl() + { + $this->browser = $this->browser->withBase('http://example.com')->withoutBase(); + + $that = $this; + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { + $that->assertEquals('path', $request->getUri()); + return true; + }))->willReturn(new Promise(function () { })); + + $this->browser->get('path'); + } + + public function testWithProtocolVersionFollowedByGetRequestSendsRequestWithProtocolVersion() + { + $this->browser = $this->browser->withProtocolVersion('1.0'); + + $that = $this; + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { + $that->assertEquals('1.0', $request->getProtocolVersion()); + return true; + }))->willReturn(new Promise(function () { })); + + $this->browser->get('http://example.com/'); + } + + public function testWithProtocolVersionFollowedBySubmitRequestSendsRequestWithProtocolVersion() + { + $this->browser = $this->browser->withProtocolVersion('1.0'); + + $that = $this; + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { + $that->assertEquals('1.0', $request->getProtocolVersion()); + return true; + }))->willReturn(new Promise(function () { })); + + $this->browser->submit('http://example.com/', array()); + } + + public function testWithProtocolVersionInvalidThrows() + { + $this->setExpectedException('InvalidArgumentException'); + $this->browser->withProtocolVersion('1.2'); + } + + public function testCancelGetRequestShouldCancelUnderlyingSocketConnection() + { + $pending = new Promise(function () { }, $this->expectCallableOnce()); + + $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $connector->expects($this->once())->method('connect')->with('example.com:80')->willReturn($pending); + + $this->browser = new Browser($this->loop, $connector); + + $promise = $this->browser->get('http://example.com/'); + $promise->cancel(); + } +} diff --git a/tests/FunctionalBrowserTest.php b/tests/FunctionalBrowserTest.php new file mode 100644 index 00000000..16293fbb --- /dev/null +++ b/tests/FunctionalBrowserTest.php @@ -0,0 +1,645 @@ +loop = $loop = Factory::create(); + $this->browser = new Browser($this->loop); + + $server = new Server(array(new StreamingRequestMiddleware(), function (ServerRequestInterface $request) use ($loop) { + $path = $request->getUri()->getPath(); + + $headers = array(); + foreach ($request->getHeaders() as $name => $values) { + $headers[$name] = implode(', ', $values); + } + + if ($path === '/get') { + return new Response( + 200, + array(), + 'hello' + ); + } + + if ($path === '/redirect-to') { + $params = $request->getQueryParams(); + return new Response( + 302, + array('Location' => $params['url']) + ); + } + + if ($path === '/basic-auth/user/pass') { + return new Response( + $request->getHeaderLine('Authorization') === 'Basic dXNlcjpwYXNz' ? 200 : 401, + array(), + '' + ); + } + + if ($path === '/status/300') { + return new Response( + 300, + array(), + '' + ); + } + + if ($path === '/status/404') { + return new Response( + 404, + array(), + '' + ); + } + + if ($path === '/delay/10') { + return new Promise(function ($resolve) use ($loop) { + $loop->addTimer(10, function () use ($resolve) { + $resolve(new Response( + 200, + array(), + 'hello' + )); + }); + }); + } + + if ($path === '/post') { + return new Promise(function ($resolve) use ($request, $headers) { + $body = $request->getBody(); + assert($body instanceof ReadableStreamInterface); + + $buffer = ''; + $body->on('data', function ($data) use (&$buffer) { + $buffer .= $data; + }); + + $body->on('close', function () use (&$buffer, $resolve, $headers) { + $resolve(new Response( + 200, + array(), + json_encode(array( + 'data' => $buffer, + 'headers' => $headers + )) + )); + }); + }); + } + + if ($path === '/stream/1') { + $stream = new ThroughStream(); + + $loop->futureTick(function () use ($stream, $headers) { + $stream->end(json_encode(array( + 'headers' => $headers + ))); + }); + + return new Response( + 200, + array(), + $stream + ); + } + + var_dump($path); + })); + $socket = new \React\Socket\Server(0, $this->loop); + $server->listen($socket); + + $this->base = str_replace('tcp:', 'http:', $socket->getAddress()) . '/'; + } + + /** + * @doesNotPerformAssertions + */ + public function testSimpleRequest() + { + Block\await($this->browser->get($this->base . 'get'), $this->loop); + } + + public function testGetRequestWithRelativeAddressRejects() + { + $promise = $this->browser->get('delay'); + + $this->setExpectedException('InvalidArgumentException', 'Invalid request URL given'); + Block\await($promise, $this->loop); + } + + /** + * @doesNotPerformAssertions + */ + public function testGetRequestWithBaseAndRelativeAddressResolves() + { + Block\await($this->browser->withBase($this->base)->get('get'), $this->loop); + } + + /** + * @doesNotPerformAssertions + */ + public function testGetRequestWithBaseAndFullAddressResolves() + { + Block\await($this->browser->withBase('http://example.com/')->get($this->base . 'get'), $this->loop); + } + + public function testCancelGetRequestWillRejectRequest() + { + $promise = $this->browser->get($this->base . 'get'); + $promise->cancel(); + + $this->setExpectedException('RuntimeException'); + Block\await($promise, $this->loop); + } + + public function testCancelSendWithPromiseFollowerWillRejectRequest() + { + $promise = $this->browser->send(new Request('GET', $this->base . 'get'))->then(function () { + var_dump('noop'); + }); + $promise->cancel(); + + $this->setExpectedException('RuntimeException'); + Block\await($promise, $this->loop); + } + + public function testRequestWithoutAuthenticationFails() + { + $this->setExpectedException('RuntimeException'); + Block\await($this->browser->get($this->base . 'basic-auth/user/pass'), $this->loop); + } + + /** + * @doesNotPerformAssertions + */ + public function testRequestWithAuthenticationSucceeds() + { + $base = str_replace('://', '://user:pass@', $this->base); + + Block\await($this->browser->get($base . 'basic-auth/user/pass'), $this->loop); + } + + /** + * ```bash + * $ curl -vL "http://httpbin.org/redirect-to?url=http://user:pass@httpbin.org/basic-auth/user/pass" + * ``` + * + * @doesNotPerformAssertions + */ + public function testRedirectToPageWithAuthenticationSendsAuthenticationFromLocationHeader() + { + $target = str_replace('://', '://user:pass@', $this->base) . 'basic-auth/user/pass'; + + Block\await($this->browser->get($this->base . 'redirect-to?url=' . urlencode($target)), $this->loop); + } + + /** + * ```bash + * $ curl -vL "http://unknown:invalid@httpbin.org/redirect-to?url=http://user:pass@httpbin.org/basic-auth/user/pass" + * ``` + * + * @doesNotPerformAssertions + */ + public function testRedirectFromPageWithInvalidAuthToPageWithCorrectAuthenticationSucceeds() + { + $base = str_replace('://', '://unknown:invalid@', $this->base); + $target = str_replace('://', '://user:pass@', $this->base) . 'basic-auth/user/pass'; + + Block\await($this->browser->get($base . 'redirect-to?url=' . urlencode($target)), $this->loop); + } + + public function testCancelRedirectedRequestShouldReject() + { + $promise = $this->browser->get($this->base . 'redirect-to?url=delay%2F10'); + + $this->loop->addTimer(0.1, function () use ($promise) { + $promise->cancel(); + }); + + $this->setExpectedException('RuntimeException', 'Request cancelled'); + Block\await($promise, $this->loop); + } + + public function testTimeoutDelayedResponseShouldReject() + { + $promise = $this->browser->withTimeout(0.1)->get($this->base . 'delay/10'); + + $this->setExpectedException('RuntimeException', 'Request timed out after 0.1 seconds'); + Block\await($promise, $this->loop); + } + + public function testTimeoutDelayedResponseAfterStreamingRequestShouldReject() + { + $stream = new ThroughStream(); + $promise = $this->browser->withTimeout(0.1)->post($this->base . 'delay/10', array(), $stream); + $stream->end(); + + $this->setExpectedException('RuntimeException', 'Request timed out after 0.1 seconds'); + Block\await($promise, $this->loop); + } + + /** + * @doesNotPerformAssertions + */ + public function testTimeoutFalseShouldResolveSuccessfully() + { + Block\await($this->browser->withTimeout(false)->get($this->base . 'get'), $this->loop); + } + + /** + * @doesNotPerformAssertions + */ + public function testRedirectRequestRelative() + { + Block\await($this->browser->get($this->base . 'redirect-to?url=get'), $this->loop); + } + + /** + * @doesNotPerformAssertions + */ + public function testRedirectRequestAbsolute() + { + Block\await($this->browser->get($this->base . 'redirect-to?url=' . urlencode($this->base . 'get')), $this->loop); + } + + /** + * @doesNotPerformAssertions + */ + public function testFollowingRedirectsFalseResolvesWithRedirectResult() + { + $browser = $this->browser->withFollowRedirects(false); + + Block\await($browser->get($this->base . 'redirect-to?url=get'), $this->loop); + } + + public function testFollowRedirectsZeroRejectsOnRedirect() + { + $browser = $this->browser->withFollowRedirects(0); + + $this->setExpectedException('RuntimeException'); + Block\await($browser->get($this->base . 'redirect-to?url=get'), $this->loop); + } + + /** + * @doesNotPerformAssertions + */ + public function testResponseStatus300WithoutLocationShouldResolveWithoutFollowingRedirect() + { + Block\await($this->browser->get($this->base . 'status/300'), $this->loop); + } + + /** + * @doesNotPerformAssertions + */ + public function testGetRequestWithResponseBufferMatchedExactlyResolves() + { + $promise = $this->browser->withResponseBuffer(5)->get($this->base . 'get'); + + Block\await($promise, $this->loop); + } + + public function testGetRequestWithResponseBufferExceededRejects() + { + $promise = $this->browser->withResponseBuffer(4)->get($this->base . 'get'); + + $this->setExpectedException( + 'OverflowException', + 'Response body size of 5 bytes exceeds maximum of 4 bytes', + defined('SOCKET_EMSGSIZE') ? SOCKET_EMSGSIZE : 0 + ); + Block\await($promise, $this->loop); + } + + public function testGetRequestWithResponseBufferExceededDuringStreamingRejects() + { + $promise = $this->browser->withResponseBuffer(4)->get($this->base . 'stream/1'); + + $this->setExpectedException( + 'OverflowException', + 'Response body size exceeds maximum of 4 bytes', + defined('SOCKET_EMSGSIZE') ? SOCKET_EMSGSIZE : 0 + ); + Block\await($promise, $this->loop); + } + + /** + * @group online + * @doesNotPerformAssertions + */ + public function testCanAccessHttps() + { + if (!function_exists('stream_socket_enable_crypto')) { + $this->markTestSkipped('Not supported on your platform (outdated HHVM?)'); + } + + Block\await($this->browser->get('https://www.google.com/'), $this->loop); + } + + /** + * @group online + */ + public function testVerifyPeerEnabledForBadSslRejects() + { + if (!function_exists('stream_socket_enable_crypto')) { + $this->markTestSkipped('Not supported on your platform (outdated HHVM?)'); + } + + $connector = new Connector($this->loop, array( + 'tls' => array( + 'verify_peer' => true + ) + )); + + $browser = new Browser($this->loop, $connector); + + $this->setExpectedException('RuntimeException'); + Block\await($browser->get('https://self-signed.badssl.com/'), $this->loop); + } + + /** + * @group online + * @doesNotPerformAssertions + */ + public function testVerifyPeerDisabledForBadSslResolves() + { + if (!function_exists('stream_socket_enable_crypto')) { + $this->markTestSkipped('Not supported on your platform (outdated HHVM?)'); + } + + $connector = new Connector($this->loop, array( + 'tls' => array( + 'verify_peer' => false + ) + )); + + $browser = new Browser($this->loop, $connector); + + Block\await($browser->get('https://self-signed.badssl.com/'), $this->loop); + } + + /** + * @group online + */ + public function testInvalidPort() + { + $this->setExpectedException('RuntimeException'); + Block\await($this->browser->get('http://www.google.com:443/'), $this->loop); + } + + public function testErrorStatusCodeRejectsWithResponseException() + { + try { + Block\await($this->browser->get($this->base . 'status/404'), $this->loop); + $this->fail(); + } catch (ResponseException $e) { + $this->assertEquals(404, $e->getCode()); + + $this->assertInstanceOf('Psr\Http\Message\ResponseInterface', $e->getResponse()); + $this->assertEquals(404, $e->getResponse()->getStatusCode()); + } + } + + public function testErrorStatusCodeDoesNotRejectWithRejectErrorResponseFalse() + { + $response = Block\await($this->browser->withRejectErrorResponse(false)->get($this->base . 'status/404'), $this->loop); + + $this->assertEquals(404, $response->getStatusCode()); + } + + public function testPostString() + { + $response = Block\await($this->browser->post($this->base . 'post', array(), 'hello world'), $this->loop); + $data = json_decode((string)$response->getBody(), true); + + $this->assertEquals('hello world', $data['data']); + } + + public function testReceiveStreamUntilConnectionsEndsForHttp10() + { + $response = Block\await($this->browser->withProtocolVersion('1.0')->get($this->base . 'stream/1'), $this->loop); + + $this->assertEquals('1.0', $response->getProtocolVersion()); + $this->assertFalse($response->hasHeader('Transfer-Encoding')); + + $this->assertStringStartsWith('{', (string) $response->getBody()); + $this->assertStringEndsWith('}', (string) $response->getBody()); + } + + public function testReceiveStreamChunkedForHttp11() + { + $response = Block\await($this->browser->send(new Request('GET', $this->base . 'stream/1', array(), null, '1.1')), $this->loop); + + $this->assertEquals('1.1', $response->getProtocolVersion()); + + // underlying http-client automatically decodes and doesn't expose header + // @link https://github.com/reactphp/http-client/pull/58 + // $this->assertEquals('chunked', $response->getHeaderLine('Transfer-Encoding')); + $this->assertFalse($response->hasHeader('Transfer-Encoding')); + + $this->assertStringStartsWith('{', (string) $response->getBody()); + $this->assertStringEndsWith('}', (string) $response->getBody()); + } + + public function testReceiveStreamAndExplicitlyCloseConnectionEvenWhenServerKeepsConnectionOpen() + { + $closed = new \React\Promise\Deferred(); + $socket = new \React\Socket\Server(0, $this->loop); + $socket->on('connection', function (\React\Socket\ConnectionInterface $connection) use ($closed) { + $connection->on('data', function () use ($connection) { + $connection->write("HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello"); + }); + $connection->on('close', function () use ($closed) { + $closed->resolve(true); + }); + }); + + $this->base = str_replace('tcp:', 'http:', $socket->getAddress()) . '/'; + + $response = Block\await($this->browser->get($this->base . 'get', array()), $this->loop); + $this->assertEquals('hello', (string)$response->getBody()); + + $ret = Block\await($closed->promise(), $this->loop, 0.1); + $this->assertTrue($ret); + + $socket->close(); + } + + public function testPostStreamChunked() + { + $stream = new ThroughStream(); + + $this->loop->addTimer(0.001, function () use ($stream) { + $stream->end('hello world'); + }); + + $response = Block\await($this->browser->post($this->base . 'post', array(), $stream), $this->loop); + $data = json_decode((string)$response->getBody(), true); + + $this->assertEquals('hello world', $data['data']); + $this->assertFalse(isset($data['headers']['Content-Length'])); + $this->assertEquals('chunked', $data['headers']['Transfer-Encoding']); + } + + public function testPostStreamKnownLength() + { + $stream = new ThroughStream(); + + $this->loop->addTimer(0.001, function () use ($stream) { + $stream->end('hello world'); + }); + + $response = Block\await($this->browser->post($this->base . 'post', array('Content-Length' => 11), $stream), $this->loop); + $data = json_decode((string)$response->getBody(), true); + + $this->assertEquals('hello world', $data['data']); + } + + /** + * @doesNotPerformAssertions + */ + public function testPostStreamWillStartSendingRequestEvenWhenBodyDoesNotEmitData() + { + $server = new Server(array(new StreamingRequestMiddleware(), function (ServerRequestInterface $request) { + return new Response(200); + })); + $socket = new \React\Socket\Server(0, $this->loop); + $server->listen($socket); + + $this->base = str_replace('tcp:', 'http:', $socket->getAddress()) . '/'; + + $stream = new ThroughStream(); + Block\await($this->browser->post($this->base . 'post', array(), $stream), $this->loop); + + $socket->close(); + } + + public function testPostStreamClosed() + { + $stream = new ThroughStream(); + $stream->close(); + + $response = Block\await($this->browser->post($this->base . 'post', array(), $stream), $this->loop); + $data = json_decode((string)$response->getBody(), true); + + $this->assertEquals('', $data['data']); + } + + public function testSendsHttp11ByDefault() + { + $server = new Server(function (ServerRequestInterface $request) { + return new Response( + 200, + array(), + $request->getProtocolVersion() + ); + }); + $socket = new \React\Socket\Server(0, $this->loop); + $server->listen($socket); + + $this->base = str_replace('tcp:', 'http:', $socket->getAddress()) . '/'; + + $response = Block\await($this->browser->get($this->base), $this->loop); + $this->assertEquals('1.1', (string)$response->getBody()); + + $socket->close(); + } + + public function testSendsExplicitHttp10Request() + { + $server = new Server(function (ServerRequestInterface $request) { + return new Response( + 200, + array(), + $request->getProtocolVersion() + ); + }); + $socket = new \React\Socket\Server(0, $this->loop); + $server->listen($socket); + + $this->base = str_replace('tcp:', 'http:', $socket->getAddress()) . '/'; + + $response = Block\await($this->browser->withProtocolVersion('1.0')->get($this->base), $this->loop); + $this->assertEquals('1.0', (string)$response->getBody()); + + $socket->close(); + } + + public function testHeadRequestReceivesResponseWithEmptyBodyButWithContentLengthResponseHeader() + { + $response = Block\await($this->browser->head($this->base . 'get'), $this->loop); + $this->assertEquals('', (string)$response->getBody()); + $this->assertEquals(0, $response->getBody()->getSize()); + $this->assertEquals('5', $response->getHeaderLine('Content-Length')); + } + + public function testRequestGetReceivesBufferedResponseEvenWhenStreamingOptionHasBeenTurnedOn() + { + $response = Block\await( + $this->browser->withOptions(array('streaming' => true))->request('GET', $this->base . 'get'), + $this->loop + ); + $this->assertEquals('hello', (string)$response->getBody()); + } + + public function testRequestStreamingGetReceivesStreamingResponseBody() + { + $buffer = Block\await( + $this->browser->requestStreaming('GET', $this->base . 'get')->then(function (ResponseInterface $response) { + return Stream\buffer($response->getBody()); + }), + $this->loop + ); + + $this->assertEquals('hello', $buffer); + } + + public function testRequestStreamingGetReceivesStreamingResponseEvenWhenStreamingOptionHasBeenTurnedOff() + { + $response = Block\await( + $this->browser->withOptions(array('streaming' => false))->requestStreaming('GET', $this->base . 'get'), + $this->loop + ); + $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $response->getBody()); + $this->assertEquals('', (string)$response->getBody()); + } + + public function testRequestStreamingGetReceivesStreamingResponseBodyEvenWhenResponseBufferExceeded() + { + $buffer = Block\await( + $this->browser->withResponseBuffer(4)->requestStreaming('GET', $this->base . 'get')->then(function (ResponseInterface $response) { + return Stream\buffer($response->getBody()); + }), + $this->loop + ); + + $this->assertEquals('hello', $buffer); + } +} diff --git a/tests/Io/ChunkedEncoderTest.php b/tests/Io/ChunkedEncoderTest.php index 75d43d4a..87ce44c4 100644 --- a/tests/Io/ChunkedEncoderTest.php +++ b/tests/Io/ChunkedEncoderTest.php @@ -22,7 +22,7 @@ public function setUpChunkedStream() public function testChunked() { - $this->chunkedStream->on('data', $this->expectCallableOnce(array("5\r\nhello\r\n"))); + $this->chunkedStream->on('data', $this->expectCallableOnceWith("5\r\nhello\r\n")); $this->input->emit('data', array('hello')); } @@ -34,7 +34,7 @@ public function testEmptyString() public function testBiggerStringToCheckHexValue() { - $this->chunkedStream->on('data', $this->expectCallableOnce(array("1a\r\nabcdefghijklmnopqrstuvwxyz\r\n"))); + $this->chunkedStream->on('data', $this->expectCallableOnceWith("1a\r\nabcdefghijklmnopqrstuvwxyz\r\n")); $this->input->emit('data', array('abcdefghijklmnopqrstuvwxyz')); } diff --git a/tests/Io/SenderTest.php b/tests/Io/SenderTest.php new file mode 100644 index 00000000..aaf93ce1 --- /dev/null +++ b/tests/Io/SenderTest.php @@ -0,0 +1,393 @@ +loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + } + + public function testCreateFromLoop() + { + $sender = Sender::createFromLoop($this->loop, null, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); + + $this->assertInstanceOf('React\Http\Io\Sender', $sender); + } + + public function testSenderRejectsInvalidUri() + { + $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $connector->expects($this->never())->method('connect'); + + $sender = new Sender(new HttpClient($this->loop, $connector), $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); + + $request = new Request('GET', 'www.google.com'); + + $promise = $sender->send($request); + + $this->setExpectedException('InvalidArgumentException'); + Block\await($promise, $this->loop); + } + + public function testSenderConnectorRejection() + { + $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $connector->expects($this->once())->method('connect')->willReturn(Promise\reject(new \RuntimeException('Rejected'))); + + $sender = new Sender(new HttpClient($this->loop, $connector), $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); + + $request = new Request('GET', 'http://www.google.com/'); + + $promise = $sender->send($request); + + $this->setExpectedException('RuntimeException'); + Block\await($promise, $this->loop); + } + + public function testSendPostWillAutomaticallySendContentLengthHeader() + { + $client = $this->getMockBuilder('React\HttpClient\Client')->disableOriginalConstructor()->getMock(); + $client->expects($this->once())->method('request')->with( + 'POST', + 'http://www.google.com/', + array('Host' => 'www.google.com', 'Content-Length' => '5'), + '1.1' + )->willReturn($this->getMockBuilder('React\HttpClient\Request')->disableOriginalConstructor()->getMock()); + + $sender = new Sender($client, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); + + $request = new Request('POST', 'http://www.google.com/', array(), 'hello'); + $sender->send($request); + } + + public function testSendPostWillAutomaticallySendContentLengthZeroHeaderForEmptyRequestBody() + { + $client = $this->getMockBuilder('React\HttpClient\Client')->disableOriginalConstructor()->getMock(); + $client->expects($this->once())->method('request')->with( + 'POST', + 'http://www.google.com/', + array('Host' => 'www.google.com', 'Content-Length' => '0'), + '1.1' + )->willReturn($this->getMockBuilder('React\HttpClient\Request')->disableOriginalConstructor()->getMock()); + + $sender = new Sender($client, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); + + $request = new Request('POST', 'http://www.google.com/', array(), ''); + $sender->send($request); + } + + public function testSendPostStreamWillAutomaticallySendTransferEncodingChunked() + { + $outgoing = $this->getMockBuilder('React\HttpClient\Request')->disableOriginalConstructor()->getMock(); + $outgoing->expects($this->once())->method('write')->with(""); + + $client = $this->getMockBuilder('React\HttpClient\Client')->disableOriginalConstructor()->getMock(); + $client->expects($this->once())->method('request')->with( + 'POST', + 'http://www.google.com/', + array('Host' => 'www.google.com', 'Transfer-Encoding' => 'chunked'), + '1.1' + )->willReturn($outgoing); + + $sender = new Sender($client, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); + + $stream = new ThroughStream(); + $request = new Request('POST', 'http://www.google.com/', array(), new ReadableBodyStream($stream)); + $sender->send($request); + } + + public function testSendPostStreamWillAutomaticallyPipeChunkEncodeBodyForWriteAndRespectRequestThrottling() + { + $outgoing = $this->getMockBuilder('React\HttpClient\Request')->disableOriginalConstructor()->getMock(); + $outgoing->expects($this->once())->method('isWritable')->willReturn(true); + $outgoing->expects($this->exactly(2))->method('write')->withConsecutive(array(""), array("5\r\nhello\r\n"))->willReturn(false); + + $client = $this->getMockBuilder('React\HttpClient\Client')->disableOriginalConstructor()->getMock(); + $client->expects($this->once())->method('request')->willReturn($outgoing); + + $sender = new Sender($client, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); + + $stream = new ThroughStream(); + $request = new Request('POST', 'http://www.google.com/', array(), new ReadableBodyStream($stream)); + $sender->send($request); + + $ret = $stream->write('hello'); + $this->assertFalse($ret); + } + + public function testSendPostStreamWillAutomaticallyPipeChunkEncodeBodyForEnd() + { + $outgoing = $this->getMockBuilder('React\HttpClient\Request')->disableOriginalConstructor()->getMock(); + $outgoing->expects($this->once())->method('isWritable')->willReturn(true); + $outgoing->expects($this->exactly(2))->method('write')->withConsecutive(array(""), array("0\r\n\r\n"))->willReturn(false); + $outgoing->expects($this->once())->method('end')->with(null); + + $client = $this->getMockBuilder('React\HttpClient\Client')->disableOriginalConstructor()->getMock(); + $client->expects($this->once())->method('request')->willReturn($outgoing); + + $sender = new Sender($client, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); + + $stream = new ThroughStream(); + $request = new Request('POST', 'http://www.google.com/', array(), new ReadableBodyStream($stream)); + $sender->send($request); + + $stream->end(); + } + + public function testSendPostStreamWillRejectWhenRequestBodyEmitsErrorEvent() + { + $outgoing = $this->getMockBuilder('React\HttpClient\Request')->disableOriginalConstructor()->getMock(); + $outgoing->expects($this->once())->method('isWritable')->willReturn(true); + $outgoing->expects($this->once())->method('write')->with("")->willReturn(false); + $outgoing->expects($this->never())->method('end'); + $outgoing->expects($this->once())->method('close'); + + $client = $this->getMockBuilder('React\HttpClient\Client')->disableOriginalConstructor()->getMock(); + $client->expects($this->once())->method('request')->willReturn($outgoing); + + $sender = new Sender($client, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); + + $expected = new \RuntimeException(); + $stream = new ThroughStream(); + $request = new Request('POST', 'http://www.google.com/', array(), new ReadableBodyStream($stream)); + $promise = $sender->send($request); + + $stream->emit('error', array($expected)); + + $exception = null; + $promise->then(null, function ($e) use (&$exception) { + $exception = $e; + }); + + $this->assertInstanceOf('RuntimeException', $exception); + $this->assertEquals('Request failed because request body reported an error', $exception->getMessage()); + $this->assertSame($expected, $exception->getPrevious()); + } + + public function testSendPostStreamWillRejectWhenRequestBodyClosesWithoutEnd() + { + $outgoing = $this->getMockBuilder('React\HttpClient\Request')->disableOriginalConstructor()->getMock(); + $outgoing->expects($this->once())->method('isWritable')->willReturn(true); + $outgoing->expects($this->once())->method('write')->with("")->willReturn(false); + $outgoing->expects($this->never())->method('end'); + $outgoing->expects($this->once())->method('close'); + + $client = $this->getMockBuilder('React\HttpClient\Client')->disableOriginalConstructor()->getMock(); + $client->expects($this->once())->method('request')->willReturn($outgoing); + + $sender = new Sender($client, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); + + $stream = new ThroughStream(); + $request = new Request('POST', 'http://www.google.com/', array(), new ReadableBodyStream($stream)); + $promise = $sender->send($request); + + $stream->close(); + + $exception = null; + $promise->then(null, function ($e) use (&$exception) { + $exception = $e; + }); + + $this->assertInstanceOf('RuntimeException', $exception); + $this->assertEquals('Request failed because request body closed unexpectedly', $exception->getMessage()); + } + + public function testSendPostStreamWillNotRejectWhenRequestBodyClosesAfterEnd() + { + $outgoing = $this->getMockBuilder('React\HttpClient\Request')->disableOriginalConstructor()->getMock(); + $outgoing->expects($this->once())->method('isWritable')->willReturn(true); + $outgoing->expects($this->exactly(2))->method('write')->withConsecutive(array(""), array("0\r\n\r\n"))->willReturn(false); + $outgoing->expects($this->once())->method('end'); + $outgoing->expects($this->never())->method('close'); + + $client = $this->getMockBuilder('React\HttpClient\Client')->disableOriginalConstructor()->getMock(); + $client->expects($this->once())->method('request')->willReturn($outgoing); + + $sender = new Sender($client, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); + + $stream = new ThroughStream(); + $request = new Request('POST', 'http://www.google.com/', array(), new ReadableBodyStream($stream)); + $promise = $sender->send($request); + + $stream->end(); + $stream->close(); + + $exception = null; + $promise->then(null, function ($e) use (&$exception) { + $exception = $e; + }); + + $this->assertNull($exception); + } + + public function testSendPostStreamWithExplicitContentLengthWillSendHeaderAsIs() + { + $client = $this->getMockBuilder('React\HttpClient\Client')->disableOriginalConstructor()->getMock(); + $client->expects($this->once())->method('request')->with( + 'POST', + 'http://www.google.com/', + array('Host' => 'www.google.com', 'Content-Length' => '100'), + '1.1' + )->willReturn($this->getMockBuilder('React\HttpClient\Request')->disableOriginalConstructor()->getMock()); + + $sender = new Sender($client, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); + + $stream = new ThroughStream(); + $request = new Request('POST', 'http://www.google.com/', array('Content-Length' => '100'), new ReadableBodyStream($stream)); + $sender->send($request); + } + + public function testSendGetWillNotPassContentLengthHeaderForEmptyRequestBody() + { + $client = $this->getMockBuilder('React\HttpClient\Client')->disableOriginalConstructor()->getMock(); + $client->expects($this->once())->method('request')->with( + 'GET', + 'http://www.google.com/', + array('Host' => 'www.google.com'), + '1.1' + )->willReturn($this->getMockBuilder('React\HttpClient\Request')->disableOriginalConstructor()->getMock()); + + $sender = new Sender($client, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); + + $request = new Request('GET', 'http://www.google.com/'); + $sender->send($request); + } + + public function testSendCustomMethodWillNotPassContentLengthHeaderForEmptyRequestBody() + { + $client = $this->getMockBuilder('React\HttpClient\Client')->disableOriginalConstructor()->getMock(); + $client->expects($this->once())->method('request')->with( + 'CUSTOM', + 'http://www.google.com/', + array('Host' => 'www.google.com'), + '1.1' + )->willReturn($this->getMockBuilder('React\HttpClient\Request')->disableOriginalConstructor()->getMock()); + + $sender = new Sender($client, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); + + $request = new Request('CUSTOM', 'http://www.google.com/'); + $sender->send($request); + } + + public function testSendCustomMethodWithExplicitContentLengthZeroWillBePassedAsIs() + { + $client = $this->getMockBuilder('React\HttpClient\Client')->disableOriginalConstructor()->getMock(); + $client->expects($this->once())->method('request')->with( + 'CUSTOM', + 'http://www.google.com/', + array('Host' => 'www.google.com', 'Content-Length' => '0'), + '1.1' + )->willReturn($this->getMockBuilder('React\HttpClient\Request')->disableOriginalConstructor()->getMock()); + + $sender = new Sender($client, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); + + $request = new Request('CUSTOM', 'http://www.google.com/', array('Content-Length' => '0')); + $sender->send($request); + } + + public function testCancelRequestWillCancelConnector() + { + $promise = new \React\Promise\Promise(function () { }, function () { + throw new \RuntimeException(); + }); + + $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $connector->expects($this->once())->method('connect')->willReturn($promise); + + $sender = new Sender(new HttpClient($this->loop, $connector), $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); + + $request = new Request('GET', 'http://www.google.com/'); + + $promise = $sender->send($request); + $promise->cancel(); + + $this->setExpectedException('RuntimeException'); + Block\await($promise, $this->loop); + } + + public function testCancelRequestWillCloseConnection() + { + $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection->expects($this->once())->method('close'); + + $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $connector->expects($this->once())->method('connect')->willReturn(Promise\resolve($connection)); + + $sender = new Sender(new HttpClient($this->loop, $connector), $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); + + $request = new Request('GET', 'http://www.google.com/'); + + $promise = $sender->send($request); + $promise->cancel(); + + $this->setExpectedException('RuntimeException'); + Block\await($promise, $this->loop); + } + + public function provideRequestProtocolVersion() + { + return array( + array( + new Request('GET', 'http://www.google.com/'), + 'GET', + 'http://www.google.com/', + array( + 'Host' => 'www.google.com', + ), + '1.1', + ), + array( + new Request('GET', 'http://www.google.com/', array(), '', '1.0'), + 'GET', + 'http://www.google.com/', + array( + 'Host' => 'www.google.com', + ), + '1.0', + ), + ); + } + + /** + * @dataProvider provideRequestProtocolVersion + */ + public function testRequestProtocolVersion(Request $Request, $method, $uri, $headers, $protocolVersion) + { + $http = $this->getMockBuilder('React\HttpClient\Client') + ->setMethods(array( + 'request', + )) + ->setConstructorArgs(array( + $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(), + ))->getMock(); + + $request = $this->getMockBuilder('React\HttpClient\Request') + ->setMethods(array()) + ->setConstructorArgs(array( + $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(), + new RequestData($method, $uri, $headers, $protocolVersion), + ))->getMock(); + + $http->expects($this->once())->method('request')->with($method, $uri, $headers, $protocolVersion)->willReturn($request); + + $sender = new Sender($http, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); + $sender->send($Request); + } +} diff --git a/tests/Io/TransactionTest.php b/tests/Io/TransactionTest.php new file mode 100644 index 00000000..882b1860 --- /dev/null +++ b/tests/Io/TransactionTest.php @@ -0,0 +1,861 @@ +makeSenderMock(); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $transaction = new Transaction($sender, new MessageFactory(), $loop); + + $new = $transaction->withOptions(array('followRedirects' => false)); + + $this->assertInstanceOf('React\Http\Io\Transaction', $new); + $this->assertNotSame($transaction, $new); + + $ref = new \ReflectionProperty($new, 'followRedirects'); + $ref->setAccessible(true); + + $this->assertFalse($ref->getValue($new)); + } + + public function testWithOptionsDoesNotChangeOriginalInstance() + { + $sender = $this->makeSenderMock(); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $transaction = new Transaction($sender, new MessageFactory(), $loop); + + $transaction->withOptions(array('followRedirects' => false)); + + $ref = new \ReflectionProperty($transaction, 'followRedirects'); + $ref->setAccessible(true); + + $this->assertTrue($ref->getValue($transaction)); + } + + public function testWithOptionsNullValueReturnsNewInstanceWithDefaultOption() + { + $sender = $this->makeSenderMock(); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $transaction = new Transaction($sender, new MessageFactory(), $loop); + + $transaction = $transaction->withOptions(array('followRedirects' => false)); + $transaction = $transaction->withOptions(array('followRedirects' => null)); + + $ref = new \ReflectionProperty($transaction, 'followRedirects'); + $ref->setAccessible(true); + + $this->assertTrue($ref->getValue($transaction)); + } + + public function testTimeoutExplicitOptionWillStartTimeoutTimer() + { + $messageFactory = new MessageFactory(); + + $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->once())->method('addTimer')->with(2, $this->anything())->willReturn($timer); + $loop->expects($this->never())->method('cancelTimer'); + + $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); + + $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); + $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new \React\Promise\Promise(function () { })); + + $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = $transaction->withOptions(array('timeout' => 2)); + $promise = $transaction->send($request); + + $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); + } + + public function testTimeoutImplicitFromIniWillStartTimeoutTimer() + { + $messageFactory = new MessageFactory(); + + $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->once())->method('addTimer')->with(2, $this->anything())->willReturn($timer); + $loop->expects($this->never())->method('cancelTimer'); + + $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); + + $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); + $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new \React\Promise\Promise(function () { })); + + $transaction = new Transaction($sender, $messageFactory, $loop); + + $old = ini_get('default_socket_timeout'); + ini_set('default_socket_timeout', '2'); + $promise = $transaction->send($request); + ini_set('default_socket_timeout', $old); + + $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); + } + + public function testTimeoutExplicitOptionWillRejectWhenTimerFires() + { + $messageFactory = new MessageFactory(); + + $timeout = null; + $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->once())->method('addTimer')->with(2, $this->callback(function ($cb) use (&$timeout) { + $timeout = $cb; + return true; + }))->willReturn($timer); + $loop->expects($this->never())->method('cancelTimer'); + + $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); + + $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); + $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new \React\Promise\Promise(function () { })); + + $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = $transaction->withOptions(array('timeout' => 2)); + $promise = $transaction->send($request); + + $this->assertNotNull($timeout); + $timeout(); + + $exception = null; + $promise->then(null, function ($e) use (&$exception) { + $exception = $e; + }); + + $this->assertInstanceOf('RuntimeException', $exception); + $this->assertEquals('Request timed out after 2 seconds', $exception->getMessage()); + } + + public function testTimeoutExplicitOptionWillNotStartTimeoutWhenSenderResolvesImmediately() + { + $messageFactory = new MessageFactory(); + + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->never())->method('addTimer'); + + $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); + $response = $messageFactory->response(1.0, 200, 'OK', array(), ''); + + $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); + $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(Promise\resolve($response)); + + $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = $transaction->withOptions(array('timeout' => 0.001)); + $promise = $transaction->send($request); + + $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); + $promise->then($this->expectCallableOnceWith($response)); + } + + public function testTimeoutExplicitOptionWillCancelTimeoutTimerWhenSenderResolvesLaterOn() + { + $messageFactory = new MessageFactory(); + + $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->once())->method('addTimer')->willReturn($timer); + $loop->expects($this->once())->method('cancelTimer')->with($timer); + + $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); + $response = $messageFactory->response(1.0, 200, 'OK', array(), ''); + + $deferred = new Deferred(); + $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); + $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn($deferred->promise()); + + $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = $transaction->withOptions(array('timeout' => 0.001)); + $promise = $transaction->send($request); + + $deferred->resolve($response); + + $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); + $promise->then($this->expectCallableOnceWith($response)); + } + + public function testTimeoutExplicitOptionWillNotStartTimeoutWhenSenderRejectsImmediately() + { + $messageFactory = new MessageFactory(); + + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->never())->method('addTimer'); + + $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); + $exception = new \RuntimeException(); + + $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); + $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(Promise\reject($exception)); + + $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = $transaction->withOptions(array('timeout' => 0.001)); + $promise = $transaction->send($request); + + $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); + $promise->then(null, $this->expectCallableOnceWith($exception)); + } + + public function testTimeoutExplicitOptionWillCancelTimeoutTimerWhenSenderRejectsLaterOn() + { + $messageFactory = new MessageFactory(); + + $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->once())->method('addTimer')->willReturn($timer); + $loop->expects($this->once())->method('cancelTimer')->with($timer); + + $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); + + $deferred = new Deferred(); + $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); + $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn($deferred->promise()); + + $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = $transaction->withOptions(array('timeout' => 0.001)); + $promise = $transaction->send($request); + + $exception = new \RuntimeException(); + $deferred->reject($exception); + + $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); + $promise->then(null, $this->expectCallableOnceWith($exception)); + } + + public function testTimeoutExplicitNegativeWillNotStartTimeoutTimer() + { + $messageFactory = new MessageFactory(); + + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->never())->method('addTimer'); + + $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); + + $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); + $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new \React\Promise\Promise(function () { })); + + $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = $transaction->withOptions(array('timeout' => -1)); + $promise = $transaction->send($request); + + $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); + } + + public function testTimeoutExplicitOptionWillNotStartTimeoutTimerWhenRequestBodyIsStreaming() + { + $messageFactory = new MessageFactory(); + + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->never())->method('addTimer'); + + $stream = new ThroughStream(); + $request = $messageFactory->request('POST', 'http://example.com', array(), $stream); + + $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); + $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new \React\Promise\Promise(function () { })); + + $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = $transaction->withOptions(array('timeout' => 2)); + $promise = $transaction->send($request); + + $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); + } + + public function testTimeoutExplicitOptionWillStartTimeoutTimerWhenStreamingRequestBodyIsAlreadyClosed() + { + $messageFactory = new MessageFactory(); + + $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->once())->method('addTimer')->with(2, $this->anything())->willReturn($timer); + $loop->expects($this->never())->method('cancelTimer'); + + $stream = new ThroughStream(); + $stream->close(); + $request = $messageFactory->request('POST', 'http://example.com', array(), $stream); + + $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); + $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new \React\Promise\Promise(function () { })); + + $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = $transaction->withOptions(array('timeout' => 2)); + $promise = $transaction->send($request); + + $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); + } + + public function testTimeoutExplicitOptionWillStartTimeoutTimerWhenStreamingRequestBodyClosesWhileSenderIsStillPending() + { + $messageFactory = new MessageFactory(); + + $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->once())->method('addTimer')->with(2, $this->anything())->willReturn($timer); + $loop->expects($this->never())->method('cancelTimer'); + + $stream = new ThroughStream(); + $request = $messageFactory->request('POST', 'http://example.com', array(), $stream); + + $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); + $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new \React\Promise\Promise(function () { })); + + $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = $transaction->withOptions(array('timeout' => 2)); + $promise = $transaction->send($request); + + $stream->close(); + + $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); + } + + public function testTimeoutExplicitOptionWillNotStartTimeoutTimerWhenStreamingRequestBodyClosesAfterSenderRejects() + { + $messageFactory = new MessageFactory(); + + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->never())->method('addTimer'); + + $stream = new ThroughStream(); + $request = $messageFactory->request('POST', 'http://example.com', array(), $stream); + + $deferred = new Deferred(); + $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); + $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn($deferred->promise()); + + $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = $transaction->withOptions(array('timeout' => 2)); + $promise = $transaction->send($request); + + $deferred->reject(new \RuntimeException('Request failed')); + $stream->close(); + + $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); + } + + public function testTimeoutExplicitOptionWillRejectWhenTimerFiresAfterStreamingRequestBodyCloses() + { + $messageFactory = new MessageFactory(); + + $timeout = null; + $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->once())->method('addTimer')->with(2, $this->callback(function ($cb) use (&$timeout) { + $timeout = $cb; + return true; + }))->willReturn($timer); + $loop->expects($this->never())->method('cancelTimer'); + + $stream = new ThroughStream(); + $request = $messageFactory->request('POST', 'http://example.com', array(), $stream); + + $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); + $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new \React\Promise\Promise(function () { })); + + $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = $transaction->withOptions(array('timeout' => 2)); + $promise = $transaction->send($request); + + $stream->close(); + + $this->assertNotNull($timeout); + $timeout(); + + $exception = null; + $promise->then(null, function ($e) use (&$exception) { + $exception = $e; + }); + + $this->assertInstanceOf('RuntimeException', $exception); + $this->assertEquals('Request timed out after 2 seconds', $exception->getMessage()); + } + + public function testReceivingErrorResponseWillRejectWithResponseException() + { + $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); + $response = new Response(404); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + // mock sender to resolve promise with the given $response in response to the given $request + $sender = $this->makeSenderMock(); + $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(Promise\resolve($response)); + + $transaction = new Transaction($sender, new MessageFactory(), $loop); + $transaction = $transaction->withOptions(array('timeout' => -1)); + $promise = $transaction->send($request); + + try { + Block\await($promise, $loop); + $this->fail(); + } catch (ResponseException $exception) { + $this->assertEquals(404, $exception->getCode()); + $this->assertSame($response, $exception->getResponse()); + } + } + + public function testReceivingStreamingBodyWillResolveWithBufferedResponseByDefault() + { + $messageFactory = new MessageFactory(); + $loop = Factory::create(); + + $stream = new ThroughStream(); + $loop->addTimer(0.001, function () use ($stream) { + $stream->emit('data', array('hello world')); + $stream->close(); + }); + + $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); + $response = $messageFactory->response(1.0, 200, 'OK', array(), $stream); + + // mock sender to resolve promise with the given $response in response to the given $request + $sender = $this->makeSenderMock(); + $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(Promise\resolve($response)); + + $transaction = new Transaction($sender, $messageFactory, $loop); + $promise = $transaction->send($request); + + $response = Block\await($promise, $loop); + + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals('hello world', (string)$response->getBody()); + } + + public function testReceivingStreamingBodyWithSizeExceedingMaximumResponseBufferWillRejectAndCloseResponseStream() + { + $messageFactory = new MessageFactory(); + $loop = Factory::create(); + + $stream = new ThroughStream(); + $stream->on('close', $this->expectCallableOnce()); + + $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); + + $response = $messageFactory->response(1.0, 200, 'OK', array('Content-Length' => '100000000'), $stream); + + // mock sender to resolve promise with the given $response in response to the given $request + $sender = $this->makeSenderMock(); + $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(Promise\resolve($response)); + + $transaction = new Transaction($sender, $messageFactory, $loop); + $promise = $transaction->send($request); + + $this->setExpectedException('OverflowException'); + Block\await($promise, $loop, 0.001); + } + + public function testCancelBufferingResponseWillCloseStreamAndReject() + { + $messageFactory = new MessageFactory(); + $loop = Factory::create(); + + $stream = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $stream->expects($this->any())->method('isReadable')->willReturn(true); + $stream->expects($this->once())->method('close'); + + $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); + $response = $messageFactory->response(1.0, 200, 'OK', array(), $stream); + + // mock sender to resolve promise with the given $response in response to the given $request + $sender = $this->makeSenderMock(); + $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(Promise\resolve($response)); + + $transaction = new Transaction($sender, $messageFactory, $loop); + $promise = $transaction->send($request); + $promise->cancel(); + + $this->setExpectedException('RuntimeException'); + Block\await($promise, $loop, 0.001); + } + + public function testReceivingStreamingBodyWillResolveWithStreamingResponseIfStreamingIsEnabled() + { + $messageFactory = new MessageFactory(); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); + $response = $messageFactory->response(1.0, 200, 'OK', array(), $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock()); + + // mock sender to resolve promise with the given $response in response to the given $request + $sender = $this->makeSenderMock(); + $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(Promise\resolve($response)); + + $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = $transaction->withOptions(array('streaming' => true, 'timeout' => -1)); + $promise = $transaction->send($request); + + $response = Block\await($promise, $loop); + + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals('', (string)$response->getBody()); + } + + public function testResponseCode304WithoutLocationWillResolveWithResponseAsIs() + { + $messageFactory = new MessageFactory(); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + // conditional GET request will respond with 304 (Not Modified + $request = $messageFactory->request('GET', 'http://example.com', array('If-None-Match' => '"abc"')); + $response = $messageFactory->response(1.0, 304, null, array('ETag' => '"abc"')); + $sender = $this->makeSenderMock(); + $sender->expects($this->once())->method('send')->with($request)->willReturn(Promise\resolve($response)); + + $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = $transaction->withOptions(array('timeout' => -1)); + $promise = $transaction->send($request); + + $promise->then($this->expectCallableOnceWith($response)); + } + + public function testCustomRedirectResponseCode333WillFollowLocationHeaderAndSendRedirectedRequest() + { + $messageFactory = new MessageFactory(); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + // original GET request will respond with custom 333 redirect status code and follow location header + $requestOriginal = $messageFactory->request('GET', 'http://example.com'); + $response = $messageFactory->response(1.0, 333, null, array('Location' => 'foo')); + $sender = $this->makeSenderMock(); + $sender->expects($this->exactly(2))->method('send')->withConsecutive( + array($requestOriginal), + array($this->callback(function (RequestInterface $request) { + return $request->getMethod() === 'GET' && (string)$request->getUri() === 'http://example.com/foo'; + })) + )->willReturnOnConsecutiveCalls( + Promise\resolve($response), + new \React\Promise\Promise(function () { }) + ); + + $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction->send($requestOriginal); + } + + public function testFollowingRedirectWithSpecifiedHeaders() + { + $messageFactory = new MessageFactory(); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $customHeaders = array('User-Agent' => 'Chrome'); + $requestWithUserAgent = $messageFactory->request('GET', 'http://example.com', $customHeaders); + $sender = $this->makeSenderMock(); + + // mock sender to resolve promise with the given $redirectResponse in + // response to the given $requestWithUserAgent + $redirectResponse = $messageFactory->response(1.0, 301, null, array('Location' => 'http://redirect.com')); + $sender->expects($this->at(0))->method('send')->willReturn(Promise\resolve($redirectResponse)); + + // mock sender to resolve promise with the given $okResponse in + // response to the given $requestWithUserAgent + $okResponse = $messageFactory->response(1.0, 200, 'OK'); + $that = $this; + $sender->expects($this->at(1)) + ->method('send') + ->with($this->callback(function (RequestInterface $request) use ($that) { + $that->assertEquals(array('Chrome'), $request->getHeader('User-Agent')); + return true; + }))->willReturn(Promise\resolve($okResponse)); + + $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction->send($requestWithUserAgent); + } + + public function testRemovingAuthorizationHeaderWhenChangingHostnamesDuringRedirect() + { + $messageFactory = new MessageFactory(); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $customHeaders = array('Authorization' => 'secret'); + $requestWithAuthorization = $messageFactory->request('GET', 'http://example.com', $customHeaders); + $sender = $this->makeSenderMock(); + + // mock sender to resolve promise with the given $redirectResponse in + // response to the given $requestWithAuthorization + $redirectResponse = $messageFactory->response(1.0, 301, null, array('Location' => 'http://redirect.com')); + $sender->expects($this->at(0))->method('send')->willReturn(Promise\resolve($redirectResponse)); + + // mock sender to resolve promise with the given $okResponse in + // response to the given $requestWithAuthorization + $okResponse = $messageFactory->response(1.0, 200, 'OK'); + $that = $this; + $sender->expects($this->at(1)) + ->method('send') + ->with($this->callback(function (RequestInterface $request) use ($that) { + $that->assertFalse($request->hasHeader('Authorization')); + return true; + }))->willReturn(Promise\resolve($okResponse)); + + $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction->send($requestWithAuthorization); + } + + public function testAuthorizationHeaderIsForwardedWhenRedirectingToSameDomain() + { + $messageFactory = new MessageFactory(); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $customHeaders = array('Authorization' => 'secret'); + $requestWithAuthorization = $messageFactory->request('GET', 'http://example.com', $customHeaders); + $sender = $this->makeSenderMock(); + + // mock sender to resolve promise with the given $redirectResponse in + // response to the given $requestWithAuthorization + $redirectResponse = $messageFactory->response(1.0, 301, null, array('Location' => 'http://example.com/new')); + $sender->expects($this->at(0))->method('send')->willReturn(Promise\resolve($redirectResponse)); + + // mock sender to resolve promise with the given $okResponse in + // response to the given $requestWithAuthorization + $okResponse = $messageFactory->response(1.0, 200, 'OK'); + $that = $this; + $sender->expects($this->at(1)) + ->method('send') + ->with($this->callback(function (RequestInterface $request) use ($that) { + $that->assertEquals(array('secret'), $request->getHeader('Authorization')); + return true; + }))->willReturn(Promise\resolve($okResponse)); + + $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction->send($requestWithAuthorization); + } + + public function testAuthorizationHeaderIsForwardedWhenLocationContainsAuthentication() + { + $messageFactory = new MessageFactory(); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $request = $messageFactory->request('GET', 'http://example.com'); + $sender = $this->makeSenderMock(); + + // mock sender to resolve promise with the given $redirectResponse in + // response to the given $requestWithAuthorization + $redirectResponse = $messageFactory->response(1.0, 301, null, array('Location' => 'http://user:pass@example.com/new')); + $sender->expects($this->at(0))->method('send')->willReturn(Promise\resolve($redirectResponse)); + + // mock sender to resolve promise with the given $okResponse in + // response to the given $requestWithAuthorization + $okResponse = $messageFactory->response(1.0, 200, 'OK'); + $that = $this; + $sender->expects($this->at(1)) + ->method('send') + ->with($this->callback(function (RequestInterface $request) use ($that) { + $that->assertEquals('user:pass', $request->getUri()->getUserInfo()); + $that->assertFalse($request->hasHeader('Authorization')); + return true; + }))->willReturn(Promise\resolve($okResponse)); + + $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction->send($request); + } + + public function testSomeRequestHeadersShouldBeRemovedWhenRedirecting() + { + $messageFactory = new MessageFactory(); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $customHeaders = array( + 'Content-Type' => 'text/html; charset=utf-8', + 'Content-Length' => '111', + ); + + $requestWithCustomHeaders = $messageFactory->request('GET', 'http://example.com', $customHeaders); + $sender = $this->makeSenderMock(); + + // mock sender to resolve promise with the given $redirectResponse in + // response to the given $requestWithCustomHeaders + $redirectResponse = $messageFactory->response(1.0, 301, null, array('Location' => 'http://example.com/new')); + $sender->expects($this->at(0))->method('send')->willReturn(Promise\resolve($redirectResponse)); + + // mock sender to resolve promise with the given $okResponse in + // response to the given $requestWithCustomHeaders + $okResponse = $messageFactory->response(1.0, 200, 'OK'); + $that = $this; + $sender->expects($this->at(1)) + ->method('send') + ->with($this->callback(function (RequestInterface $request) use ($that) { + $that->assertFalse($request->hasHeader('Content-Type')); + $that->assertFalse($request->hasHeader('Content-Length')); + return true; + }))->willReturn(Promise\resolve($okResponse)); + + $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction->send($requestWithCustomHeaders); + } + + public function testCancelTransactionWillCancelRequest() + { + $messageFactory = new MessageFactory(); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $request = $messageFactory->request('GET', 'http://example.com'); + $sender = $this->makeSenderMock(); + + $pending = new \React\Promise\Promise(function () { }, $this->expectCallableOnce()); + + // mock sender to return pending promise which should be cancelled when cancelling result + $sender->expects($this->once())->method('send')->willReturn($pending); + + $transaction = new Transaction($sender, $messageFactory, $loop); + $promise = $transaction->send($request); + + $promise->cancel(); + } + + public function testCancelTransactionWillCancelTimeoutTimer() + { + $messageFactory = new MessageFactory(); + + $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->once())->method('addTimer')->willReturn($timer); + $loop->expects($this->once())->method('cancelTimer')->with($timer); + + $request = $messageFactory->request('GET', 'http://example.com'); + $sender = $this->makeSenderMock(); + + $pending = new \React\Promise\Promise(function () { }, function () { throw new \RuntimeException(); }); + + // mock sender to return pending promise which should be cancelled when cancelling result + $sender->expects($this->once())->method('send')->willReturn($pending); + + $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = $transaction->withOptions(array('timeout' => 2)); + $promise = $transaction->send($request); + + $promise->cancel(); + } + + public function testCancelTransactionWillCancelRedirectedRequest() + { + $messageFactory = new MessageFactory(); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $request = $messageFactory->request('GET', 'http://example.com'); + $sender = $this->makeSenderMock(); + + // mock sender to resolve promise with the given $redirectResponse in + $redirectResponse = $messageFactory->response(1.0, 301, null, array('Location' => 'http://example.com/new')); + $sender->expects($this->at(0))->method('send')->willReturn(Promise\resolve($redirectResponse)); + + $pending = new \React\Promise\Promise(function () { }, $this->expectCallableOnce()); + + // mock sender to return pending promise which should be cancelled when cancelling result + $sender->expects($this->at(1))->method('send')->willReturn($pending); + + $transaction = new Transaction($sender, $messageFactory, $loop); + $promise = $transaction->send($request); + + $promise->cancel(); + } + + public function testCancelTransactionWillCancelRedirectedRequestAgain() + { + $messageFactory = new MessageFactory(); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $request = $messageFactory->request('GET', 'http://example.com'); + $sender = $this->makeSenderMock(); + + // mock sender to resolve promise with the given $redirectResponse in + $first = new Deferred(); + $sender->expects($this->at(0))->method('send')->willReturn($first->promise()); + + $second = new \React\Promise\Promise(function () { }, $this->expectCallableOnce()); + + // mock sender to return pending promise which should be cancelled when cancelling result + $sender->expects($this->at(1))->method('send')->willReturn($second); + + $transaction = new Transaction($sender, $messageFactory, $loop); + $promise = $transaction->send($request); + + // mock sender to resolve promise with the given $redirectResponse in + $first->resolve($messageFactory->response(1.0, 301, null, array('Location' => 'http://example.com/new'))); + + $promise->cancel(); + } + + public function testCancelTransactionWillCloseBufferingStream() + { + $messageFactory = new MessageFactory(); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $request = $messageFactory->request('GET', 'http://example.com'); + $sender = $this->makeSenderMock(); + + $body = new ThroughStream(); + $body->on('close', $this->expectCallableOnce()); + + // mock sender to resolve promise with the given $redirectResponse in + $redirectResponse = $messageFactory->response(1.0, 301, null, array('Location' => 'http://example.com/new'), $body); + $sender->expects($this->once())->method('send')->willReturn(Promise\resolve($redirectResponse)); + + $transaction = new Transaction($sender, $messageFactory, $loop); + $promise = $transaction->send($request); + + $promise->cancel(); + } + + public function testCancelTransactionWillCloseBufferingStreamAgain() + { + $messageFactory = new MessageFactory(); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $request = $messageFactory->request('GET', 'http://example.com'); + $sender = $this->makeSenderMock(); + + $first = new Deferred(); + $sender->expects($this->once())->method('send')->willReturn($first->promise()); + + $transaction = new Transaction($sender, $messageFactory, $loop); + $promise = $transaction->send($request); + + $body = new ThroughStream(); + $body->on('close', $this->expectCallableOnce()); + + // mock sender to resolve promise with the given $redirectResponse in + $first->resolve($messageFactory->response(1.0, 301, null, array('Location' => 'http://example.com/new'), $body)); + $promise->cancel(); + } + + public function testCancelTransactionShouldCancelSendingPromise() + { + $messageFactory = new MessageFactory(); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $request = $messageFactory->request('GET', 'http://example.com'); + $sender = $this->makeSenderMock(); + + // mock sender to resolve promise with the given $redirectResponse in + $redirectResponse = $messageFactory->response(1.0, 301, null, array('Location' => 'http://example.com/new')); + $sender->expects($this->at(0))->method('send')->willReturn(Promise\resolve($redirectResponse)); + + $pending = new \React\Promise\Promise(function () { }, $this->expectCallableOnce()); + + // mock sender to return pending promise which should be cancelled when cancelling result + $sender->expects($this->at(1))->method('send')->willReturn($pending); + + $transaction = new Transaction($sender, $messageFactory, $loop); + $promise = $transaction->send($request); + + $promise->cancel(); + } + + /** + * @return MockObject + */ + private function makeSenderMock() + { + return $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); + } +} diff --git a/tests/Message/MessageFactoryTest.php b/tests/Message/MessageFactoryTest.php new file mode 100644 index 00000000..c06418bf --- /dev/null +++ b/tests/Message/MessageFactoryTest.php @@ -0,0 +1,197 @@ +messageFactory = new MessageFactory(); + } + + public function testUriSimple() + { + $uri = $this->messageFactory->uri('http://www.lueck.tv/'); + + $this->assertEquals('http', $uri->getScheme()); + $this->assertEquals('www.lueck.tv', $uri->getHost()); + $this->assertEquals('/', $uri->getPath()); + + $this->assertEquals(null, $uri->getPort()); + $this->assertEquals('', $uri->getQuery()); + } + + public function testUriComplete() + { + $uri = $this->messageFactory->uri('https://example.com:8080/?just=testing'); + + $this->assertEquals('https', $uri->getScheme()); + $this->assertEquals('example.com', $uri->getHost()); + $this->assertEquals(8080, $uri->getPort()); + $this->assertEquals('/', $uri->getPath()); + $this->assertEquals('just=testing', $uri->getQuery()); + } + + public function testPlaceholdersInUriWillBeEscaped() + { + $uri = $this->messageFactory->uri('http://example.com/{version}'); + + $this->assertEquals('/%7Bversion%7D', $uri->getPath()); + } + + public function testEscapedPlaceholdersInUriWillStayEscaped() + { + $uri = $this->messageFactory->uri('http://example.com/%7Bversion%7D'); + + $this->assertEquals('/%7Bversion%7D', $uri->getPath()); + } + + public function testResolveRelative() + { + $base = $this->messageFactory->uri('http://example.com/base/'); + + $this->assertEquals('http://example.com/base/', $this->messageFactory->uriRelative($base, '')); + $this->assertEquals('http://example.com/', $this->messageFactory->uriRelative($base, '/')); + + $this->assertEquals('http://example.com/base/a', $this->messageFactory->uriRelative($base, 'a')); + $this->assertEquals('http://example.com/a', $this->messageFactory->uriRelative($base, '../a')); + } + + public function testResolveAbsolute() + { + $base = $this->messageFactory->uri('http://example.org/'); + + $this->assertEquals('http://www.example.com/', $this->messageFactory->uriRelative($base, 'http://www.example.com/')); + } + + public function testResolveUri() + { + $base = $this->messageFactory->uri('http://example.org/'); + + $this->assertEquals('http://www.example.com/', $this->messageFactory->uriRelative($base, $this->messageFactory->uri('http://www.example.com/'))); + } + + public function testBodyString() + { + $body = $this->messageFactory->body('hi'); + + $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $body); + $this->assertNotInstanceOf('React\Stream\ReadableStreamInterface', $body); + $this->assertEquals(2, $body->getSize()); + $this->assertEquals('hi', (string)$body); + } + + public function testBodyReadableStream() + { + $stream = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $body = $this->messageFactory->body($stream); + + $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $body); + $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); + $this->assertEquals(null, $body->getSize()); + $this->assertEquals('', (string)$body); + } + + public function testResponseWithBodyString() + { + $response = $this->messageFactory->response('1.1', 200, 'OK', array(), 'hi'); + + $body = $response->getBody(); + $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $body); + $this->assertNotInstanceOf('React\Stream\ReadableStreamInterface', $body); + $this->assertEquals(2, $body->getSize()); + $this->assertEquals('hi', (string)$body); + } + + public function testResponseWithStreamingBodyHasUnknownSizeByDefault() + { + $stream = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $response = $this->messageFactory->response('1.1', 200, 'OK', array(), $stream); + + $body = $response->getBody(); + $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $body); + $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); + $this->assertNull($body->getSize()); + $this->assertEquals('', (string)$body); + } + + public function testResponseWithStreamingBodyHasSizeFromContentLengthHeader() + { + $stream = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $response = $this->messageFactory->response('1.1', 200, 'OK', array('Content-Length' => '100'), $stream); + + $body = $response->getBody(); + $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $body); + $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); + $this->assertEquals(100, $body->getSize()); + $this->assertEquals('', (string)$body); + } + + public function testResponseWithStreamingBodyHasUnknownSizeWithTransferEncodingChunkedHeader() + { + $stream = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $response = $this->messageFactory->response('1.1', 200, 'OK', array('Transfer-Encoding' => 'chunked'), $stream); + + $body = $response->getBody(); + $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $body); + $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); + $this->assertNull($body->getSize()); + $this->assertEquals('', (string)$body); + } + + public function testResponseWithStreamingBodyHasZeroSizeForInformationalResponse() + { + $stream = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $response = $this->messageFactory->response('1.1', 101, 'OK', array('Content-Length' => '100'), $stream); + + $body = $response->getBody(); + $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $body); + $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); + $this->assertEquals(0, $body->getSize()); + $this->assertEquals('', (string)$body); + } + + public function testResponseWithStreamingBodyHasZeroSizeForNoContentResponse() + { + $stream = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $response = $this->messageFactory->response('1.1', 204, 'OK', array('Content-Length' => '100'), $stream); + + $body = $response->getBody(); + $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $body); + $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); + $this->assertEquals(0, $body->getSize()); + $this->assertEquals('', (string)$body); + } + + public function testResponseWithStreamingBodyHasZeroSizeForNotModifiedResponse() + { + $stream = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $response = $this->messageFactory->response('1.1', 304, 'OK', array('Content-Length' => '100'), $stream); + + $body = $response->getBody(); + $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $body); + $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); + $this->assertEquals(0, $body->getSize()); + $this->assertEquals('', (string)$body); + } + + public function testResponseWithStreamingBodyHasZeroSizeForHeadRequestMethod() + { + $stream = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $response = $this->messageFactory->response('1.1', 200, 'OK', array('Content-Length' => '100'), $stream, 'HEAD'); + + $body = $response->getBody(); + $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $body); + $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); + $this->assertEquals(0, $body->getSize()); + $this->assertEquals('', (string)$body); + } +} diff --git a/tests/Message/ReadableBodyStreamTest.php b/tests/Message/ReadableBodyStreamTest.php new file mode 100644 index 00000000..b540b888 --- /dev/null +++ b/tests/Message/ReadableBodyStreamTest.php @@ -0,0 +1,255 @@ +input = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $this->stream = new ReadableBodyStream($this->input); + } + + public function testIsReadableIfInputIsReadable() + { + $this->input->expects($this->once())->method('isReadable')->willReturn(true); + + $this->assertTrue($this->stream->isReadable()); + } + + public function testIsEofIfInputIsNotReadable() + { + $this->input->expects($this->once())->method('isReadable')->willReturn(false); + + $this->assertTrue($this->stream->eof()); + } + + public function testCloseWillCloseInputStream() + { + $this->input->expects($this->once())->method('close'); + + $this->stream->close(); + } + + public function testCloseWillEmitCloseEvent() + { + $this->input = new ThroughStream(); + $this->stream = new ReadableBodyStream($this->input); + + $called = 0; + $this->stream->on('close', function () use (&$called) { + ++$called; + }); + + $this->stream->close(); + $this->stream->close(); + + $this->assertEquals(1, $called); + } + + public function testCloseInputWillEmitCloseEvent() + { + $this->input = new ThroughStream(); + $this->stream = new ReadableBodyStream($this->input); + + $called = 0; + $this->stream->on('close', function () use (&$called) { + ++$called; + }); + + $this->input->close(); + $this->input->close(); + + $this->assertEquals(1, $called); + } + + public function testEndInputWillEmitCloseEvent() + { + $this->input = new ThroughStream(); + $this->stream = new ReadableBodyStream($this->input); + + $called = 0; + $this->stream->on('close', function () use (&$called) { + ++$called; + }); + + $this->input->end(); + $this->input->end(); + + $this->assertEquals(1, $called); + } + + public function testEndInputWillEmitErrorEventWhenDataDoesNotReachExpectedLength() + { + $this->input = new ThroughStream(); + $this->stream = new ReadableBodyStream($this->input, 5); + + $called = null; + $this->stream->on('error', function ($e) use (&$called) { + $called = $e; + }); + + $this->input->write('hi'); + $this->input->end(); + + $this->assertInstanceOf('UnderflowException', $called); + $this->assertSame('Unexpected end of response body after 2/5 bytes', $called->getMessage()); + } + + public function testDataEventOnInputWillEmitDataEvent() + { + $this->input = new ThroughStream(); + $this->stream = new ReadableBodyStream($this->input); + + $called = null; + $this->stream->on('data', function ($data) use (&$called) { + $called = $data; + }); + + $this->input->write('hello'); + + $this->assertEquals('hello', $called); + } + + public function testDataEventOnInputWillEmitEndWhenDataReachesExpectedLength() + { + $this->input = new ThroughStream(); + $this->stream = new ReadableBodyStream($this->input, 5); + + $called = null; + $this->stream->on('end', function () use (&$called) { + ++$called; + }); + + $this->input->write('hello'); + + $this->assertEquals(1, $called); + } + + public function testEndEventOnInputWillEmitEndOnlyOnceWhenDataAlreadyReachedExpectedLength() + { + $this->input = new ThroughStream(); + $this->stream = new ReadableBodyStream($this->input, 5); + + $called = null; + $this->stream->on('end', function () use (&$called) { + ++$called; + }); + + $this->input->write('hello'); + $this->input->end(); + + $this->assertEquals(1, $called); + } + + public function testDataEventOnInputWillNotEmitEndWhenDataDoesNotReachExpectedLength() + { + $this->input = new ThroughStream(); + $this->stream = new ReadableBodyStream($this->input, 5); + + $called = null; + $this->stream->on('end', function () use (&$called) { + ++$called; + }); + + $this->input->write('hi'); + + $this->assertNull($called); + } + + public function testPauseWillPauseInputStream() + { + $this->input->expects($this->once())->method('pause'); + + $this->stream->pause(); + } + + public function testResumeWillResumeInputStream() + { + $this->input->expects($this->once())->method('resume'); + + $this->stream->resume(); + } + + public function testPointlessTostringReturnsEmptyString() + { + $this->assertEquals('', (string)$this->stream); + } + + public function testPointlessDetachThrows() + { + $this->setExpectedException('BadMethodCallException'); + $this->stream->detach(); + } + + public function testPointlessGetSizeReturnsNull() + { + $this->assertEquals(null, $this->stream->getSize()); + } + + public function testPointlessTellThrows() + { + $this->setExpectedException('BadMethodCallException'); + $this->stream->tell(); + } + + public function testPointlessIsSeekableReturnsFalse() + { + $this->assertEquals(false, $this->stream->isSeekable()); + } + + public function testPointlessSeekThrows() + { + $this->setExpectedException('BadMethodCallException'); + $this->stream->seek(0); + } + + public function testPointlessRewindThrows() + { + $this->setExpectedException('BadMethodCallException'); + $this->stream->rewind(); + } + + public function testPointlessIsWritableReturnsFalse() + { + $this->assertEquals(false, $this->stream->isWritable()); + } + + public function testPointlessWriteThrows() + { + $this->setExpectedException('BadMethodCallException'); + $this->stream->write(''); + } + + public function testPointlessReadThrows() + { + $this->setExpectedException('BadMethodCallException'); + $this->stream->read(8192); + } + + public function testPointlessGetContentsThrows() + { + $this->setExpectedException('BadMethodCallException'); + $this->stream->getContents(); + } + + public function testPointlessGetMetadataReturnsNullWhenKeyIsGiven() + { + $this->assertEquals(null, $this->stream->getMetadata('unknown')); + } + + public function testPointlessGetMetadataReturnsEmptyArrayWhenNoKeyIsGiven() + { + $this->assertEquals(array(), $this->stream->getMetadata()); + } +} diff --git a/tests/Message/ResponseExceptionTest.php b/tests/Message/ResponseExceptionTest.php new file mode 100644 index 00000000..33eeea9e --- /dev/null +++ b/tests/Message/ResponseExceptionTest.php @@ -0,0 +1,23 @@ +withStatus(404, 'File not found'); + + $e = new ResponseException($response); + + $this->assertEquals(404, $e->getCode()); + $this->assertEquals('HTTP status code 404 (File not found)', $e->getMessage()); + + $this->assertSame($response, $e->getResponse()); + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php index 6295e871..575ac274 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -6,16 +6,6 @@ class TestCase extends BaseTestCase { - protected function expectCallableExactly($amount) - { - $mock = $this->createCallableMock(); - $mock - ->expects($this->exactly($amount)) - ->method('__invoke'); - - return $mock; - } - protected function expectCallableOnce() { $mock = $this->createCallableMock(); @@ -64,10 +54,10 @@ protected function expectCallableConsecutive($numberOfCalls, array $with) protected function createCallableMock() { if (method_exists('PHPUnit\Framework\MockObject\MockBuilder', 'addMethods')) { - // PHPUnit 10+ + // PHPUnit 9+ return $this->getMockBuilder('stdClass')->addMethods(array('__invoke'))->getMock(); } else { - // legacy PHPUnit 4 - PHPUnit 9 + // legacy PHPUnit 4 - PHPUnit 8 return $this->getMockBuilder('stdClass')->setMethods(array('__invoke'))->getMock(); } } From a71880c803028ca9669f0197d2f0f138ea343d6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 3 Jul 2020 22:08:20 +0200 Subject: [PATCH 318/456] Remove deprecated APIs and legacy references --- README.md | 178 ++------------------------------ src/Browser.php | 169 ++++-------------------------- tests/BrowserTest.php | 28 +---- tests/FunctionalBrowserTest.php | 25 +---- 4 files changed, 29 insertions(+), 371 deletions(-) diff --git a/README.md b/README.md index af2fada9..92877f5a 100644 --- a/README.md +++ b/README.md @@ -52,16 +52,12 @@ Event-driven, streaming plaintext HTTP and secure HTTPS server for [ReactPHP](ht * [delete()](#delete) * [request()](#request) * [requestStreaming()](#requeststreaming) - * [~~submit()~~](#submit) - * [~~send()~~](#send) * [withTimeout()](#withtimeout) * [withFollowRedirects()](#withfollowredirects) * [withRejectErrorResponse()](#withrejecterrorresponse) * [withBase()](#withbase) * [withProtocolVersion()](#withprotocolversion) * [withResponseBuffer()](#withresponsebuffer) - * [~~withOptions()~~](#withoptions) - * [~~withoutBase()~~](#withoutbase) * [React\Http\Middleware](#reacthttpmiddleware) * [StreamingRequestMiddleware](#streamingrequestmiddleware) * [LimitConcurrentRequestsMiddleware](#limitconcurrentrequestsmiddleware) @@ -69,7 +65,6 @@ Event-driven, streaming plaintext HTTP and secure HTTPS server for [ReactPHP](ht * [RequestBodyParserMiddleware](#requestbodyparsermiddleware) * [ResponseInterface](#responseinterface) * [RequestInterface](#requestinterface) - * [UriInterface](#uriinterface) * [ResponseException](#responseexception) * [Install](#install) * [Tests](#tests) @@ -118,8 +113,6 @@ See also the [examples](examples). ### Request methods - - Most importantly, this project provides a [`Browser`](#browser) object that offers several methods that resemble the HTTP protocol methods: @@ -450,8 +443,6 @@ more details. ### Streaming response - - All of the above examples assume you want to store the whole response body in memory. This is easy to get started and works reasonably well for smaller responses. @@ -558,10 +549,6 @@ $stream->on('data', function ($data) { See also the [`requestStreaming()`](#requeststreaming) method for more details. -> Legacy info: Legacy versions prior to v2.9.0 used the legacy - [`streaming` option](#withoptions). This option is now deprecated but otherwise - continues to show the exact same behavior. - ### Streaming request Besides streaming the response body, you can also stream the request body. @@ -1105,8 +1092,6 @@ header or when using `Transfer-Encoding: chunked` for HTTP/1.1 requests. #### Streaming incoming request - - If you're using the advanced [`StreamingRequestMiddleware`](#streamingrequestmiddleware), the request object will be processed once the request headers have been received. This means that this happens irrespective of (i.e. *before*) receiving the @@ -1413,8 +1398,6 @@ If a promise is resolved after the client closes, it will simply be ignored. #### Streaming outgoing response - - The `Response` class in this project supports to add an instance which implements the [ReactPHP ReadableStreamInterface](https://github.com/reactphp/stream#readablestreaminterface) for the response body. @@ -1859,7 +1842,7 @@ $browser = new React\Http\Browser($loop, $connector); #### get() -The `get(string|UriInterface $url, array $headers = array()): PromiseInterface` method can be used to +The `get(string $url, array $headers = array()): PromiseInterface` method can be used to send an HTTP GET request. ```php @@ -1870,13 +1853,9 @@ $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response See also [example 01](examples/01-google.php). -> For BC reasons, this method accepts the `$url` as either a `string` - value or as an `UriInterface`. It's recommended to explicitly cast any - objects implementing `UriInterface` to `string`. - #### post() -The `post(string|UriInterface $url, array $headers = array(), string|ReadableStreamInterface $contents = ''): PromiseInterface` method can be used to +The `post(string $url, array $headers = array(), string|ReadableStreamInterface $contents = ''): PromiseInterface` method can be used to send an HTTP POST request. ```php @@ -1925,13 +1904,9 @@ $loop->addTimer(1.0, function () use ($body) { $browser->post($url, array('Content-Length' => '11'), $body); ``` -> For BC reasons, this method accepts the `$url` as either a `string` - value or as an `UriInterface`. It's recommended to explicitly cast any - objects implementing `UriInterface` to `string`. - #### head() -The `head(string|UriInterface $url, array $headers = array()): PromiseInterface` method can be used to +The `head(string $url, array $headers = array()): PromiseInterface` method can be used to send an HTTP HEAD request. ```php @@ -1940,13 +1915,9 @@ $browser->head($url)->then(function (Psr\Http\Message\ResponseInterface $respons }); ``` -> For BC reasons, this method accepts the `$url` as either a `string` - value or as an `UriInterface`. It's recommended to explicitly cast any - objects implementing `UriInterface` to `string`. - #### patch() -The `patch(string|UriInterface $url, array $headers = array(), string|ReadableStreamInterface $contents = ''): PromiseInterface` method can be used to +The `patch(string $url, array $headers = array(), string|ReadableStreamInterface $contents = ''): PromiseInterface` method can be used to send an HTTP PATCH request. ```php @@ -1976,13 +1947,9 @@ $loop->addTimer(1.0, function () use ($body) { $browser->patch($url, array('Content-Length' => '11'), $body); ``` -> For BC reasons, this method accepts the `$url` as either a `string` - value or as an `UriInterface`. It's recommended to explicitly cast any - objects implementing `UriInterface` to `string`. - #### put() -The `put(string|UriInterface $url, array $headers = array()): PromiseInterface` method can be used to +The `put(string $url, array $headers = array()): PromiseInterface` method can be used to send an HTTP PUT request. ```php @@ -2014,13 +1981,9 @@ $loop->addTimer(1.0, function () use ($body) { $browser->put($url, array('Content-Length' => '11'), $body); ``` -> For BC reasons, this method accepts the `$url` as either a `string` - value or as an `UriInterface`. It's recommended to explicitly cast any - objects implementing `UriInterface` to `string`. - #### delete() -The `delete(string|UriInterface $url, array $headers = array()): PromiseInterface` method can be used to +The `delete(string $url, array $headers = array()): PromiseInterface` method can be used to send an HTTP DELETE request. ```php @@ -2029,10 +1992,6 @@ $browser->delete($url)->then(function (Psr\Http\Message\ResponseInterface $respo }); ``` -> For BC reasons, this method accepts the `$url` as either a `string` - value or as an `UriInterface`. It's recommended to explicitly cast any - objects implementing `UriInterface` to `string`. - #### request() The `request(string $method, string $url, array $headers = array(), string|ReadableStreamInterface $body = ''): PromiseInterface` method can be used to @@ -2070,12 +2029,6 @@ $loop->addTimer(1.0, function () use ($body) { $browser->request('POST', $url, array('Content-Length' => '11'), $body); ``` -> Note that this method is available as of v2.9.0 and always buffers the - response body before resolving. - It does not respect the deprecated [`streaming` option](#withoptions). - If you want to stream the response body, you can use the - [`requestStreaming()`](#requeststreaming) method instead. - #### requestStreaming() The `requestStreaming(string $method, string $url, array $headers = array(), string|ReadableStreamInterface $body = ''): PromiseInterface` method can be used to @@ -2138,55 +2091,6 @@ $loop->addTimer(1.0, function () use ($body) { $browser->requestStreaming('POST', $url, array('Content-Length' => '11'), $body); ``` -> Note that this method is available as of v2.9.0 and always resolves the - response without buffering the response body. - It does not respect the deprecated [`streaming` option](#withoptions). - If you want to buffer the response body, use can use the - [`request()`](#request) method instead. - -#### ~~submit()~~ - -> Deprecated since v2.9.0, see [`post()`](#post) instead. - -The deprecated `submit(string|UriInterface $url, array $fields, array $headers = array(), string $method = 'POST'): PromiseInterface` method can be used to -submit an array of field values similar to submitting a form (`application/x-www-form-urlencoded`). - -```php -// deprecated: see post() instead -$browser->submit($url, array('user' => 'test', 'password' => 'secret')); -``` - -> For BC reasons, this method accepts the `$url` as either a `string` - value or as an `UriInterface`. It's recommended to explicitly cast any - objects implementing `UriInterface` to `string`. - -#### ~~send()~~ - -> Deprecated since v2.9.0, see [`request()`](#request) instead. - -The deprecated `send(RequestInterface $request): PromiseInterface` method can be used to -send an arbitrary instance implementing the [`RequestInterface`](#requestinterface) (PSR-7). - -The preferred way to send an HTTP request is by using the above -[request methods](#request-methods), for example the [`get()`](#get) -method to send an HTTP `GET` request. - -As an alternative, if you want to use a custom HTTP request method, you -can use this method: - -```php -$request = new Request('OPTIONS', $url); - -// deprecated: see request() instead -$browser->send($request)->then(…); -``` - -This method will automatically add a matching `Content-Length` request -header if the size of the outgoing request body is known and non-empty. -For an empty request body, if will only include a `Content-Length: 0` -request header if the request method usually expects a request body (only -applies to `POST`, `PUT` and `PATCH`). - #### withTimeout() The `withTimeout(bool|number $timeout): Browser` method can be used to @@ -2313,7 +2217,7 @@ given setting applied. #### withBase() -The `withBase(string|null|UriInterface $baseUrl): Browser` method can be used to +The `withBase(string|null $baseUrl): Browser` method can be used to change the base URL used to resolve relative URLs to. If you configure a base URL, any requests to relative URLs will be @@ -2346,14 +2250,6 @@ This method will throw an `InvalidArgumentException` if the given Notice that the [`Browser`](#browser) is an immutable object, i.e. the `withBase()` method actually returns a *new* [`Browser`](#browser) instance with the given base URL applied. -> For BC reasons, this method accepts the `$baseUrl` as either a `string` - value or as an `UriInterface`. It's recommended to explicitly cast any - objects implementing `UriInterface` to `string`. - -> Changelog: As of v2.9.0 this method accepts a `null` value to reset the - base URL. Earlier versions had to use the deprecated `withoutBase()` - method to reset the base URL. - #### withProtocolVersion() The `withProtocolVersion(string $protocolVersion): Browser` method can be used to @@ -2415,54 +2311,6 @@ Notice that the [`Browser`](#browser) is an immutable object, i.e. this method actually returns a *new* [`Browser`](#browser) instance with the given setting applied. -#### ~~withOptions()~~ - -> Deprecated since v2.9.0, see [`withTimeout()`](#withtimeout), [`withFollowRedirects()`](#withfollowredirects) - and [`withRejectErrorResponse()`](#withrejecterrorresponse) instead. - -The deprecated `withOptions(array $options): Browser` method can be used to -change the options to use: - -The [`Browser`](#browser) class exposes several options for the handling of -HTTP transactions. These options resemble some of PHP's -[HTTP context options](https://www.php.net/manual/en/context.http.php) and -can be controlled via the following API (and their defaults): - -```php -// deprecated -$newBrowser = $browser->withOptions(array( - 'timeout' => null, // see withTimeout() instead - 'followRedirects' => true, // see withFollowRedirects() instead - 'maxRedirects' => 10, // see withFollowRedirects() instead - 'obeySuccessCode' => true, // see withRejectErrorResponse() instead - 'streaming' => false, // deprecated, see requestStreaming() instead -)); -``` - -See also [timeouts](#timeouts), [redirects](#redirects) and -[streaming](#streaming-response) for more details. - -Notice that the [`Browser`](#browser) is an immutable object, i.e. this -method actually returns a *new* [`Browser`](#browser) instance with the -options applied. - -#### ~~withoutBase()~~ - -> Deprecated since v2.9.0, see [`withBase()`](#withbase) instead. - -The deprecated `withoutBase(): Browser` method can be used to -remove the base URL. - -```php -// deprecated: see withBase() instead -$newBrowser = $browser->withoutBase(); -``` - -Notice that the [`Browser`](#browser) is an immutable object, i.e. the `withoutBase()` method -actually returns a *new* [`Browser`](#browser) instance without any base URL applied. - -See also [`withBase()`](#withbase). - ### React\Http\Middleware #### StreamingRequestMiddleware @@ -2769,18 +2617,6 @@ This is a standard interface defined in which in turn extends the [`MessageInterface` definition](https://www.php-fig.org/psr/psr-7/#3-1-psr-http-message-messageinterface). -### UriInterface - -The `Psr\Http\Message\UriInterface` represents an absolute or relative URI (aka URL). - -This is a standard interface defined in -[PSR-7: HTTP message interfaces](https://www.php-fig.org/psr/psr-7/), see its -[`UriInterface` definition](https://www.php-fig.org/psr/psr-7/#3-5-psr-http-message-uriinterface). - -> For BC reasons, the request methods accept the URL as either a `string` - value or as an `UriInterface`. It's recommended to explicitly cast any - objects implementing `UriInterface` to `string`. - ### ResponseException The `ResponseException` is an `Exception` sub-class that will be used to reject diff --git a/src/Browser.php b/src/Browser.php index 70e875a2..38479c86 100644 --- a/src/Browser.php +++ b/src/Browser.php @@ -75,12 +75,8 @@ public function __construct(LoopInterface $loop, ConnectorInterface $connector = * * See also [example 01](../examples/01-google.php). * - * > For BC reasons, this method accepts the `$url` as either a `string` - * value or as an `UriInterface`. It's recommended to explicitly cast any - * objects implementing `UriInterface` to `string`. - * - * @param string|UriInterface $url URL for the request. - * @param array $headers + * @param string $url URL for the request. + * @param array $headers * @return PromiseInterface */ public function get($url, array $headers = array()) @@ -137,11 +133,7 @@ public function get($url, array $headers = array()) * $browser->post($url, array('Content-Length' => '11'), $body); * ``` * - * > For BC reasons, this method accepts the `$url` as either a `string` - * value or as an `UriInterface`. It's recommended to explicitly cast any - * objects implementing `UriInterface` to `string`. - * - * @param string|UriInterface $url URL for the request. + * @param string $url URL for the request. * @param array $headers * @param string|ReadableStreamInterface $contents * @return PromiseInterface @@ -160,12 +152,8 @@ public function post($url, array $headers = array(), $contents = '') * }); * ``` * - * > For BC reasons, this method accepts the `$url` as either a `string` - * value or as an `UriInterface`. It's recommended to explicitly cast any - * objects implementing `UriInterface` to `string`. - * - * @param string|UriInterface $url URL for the request. - * @param array $headers + * @param string $url URL for the request. + * @param array $headers * @return PromiseInterface */ public function head($url, array $headers = array()) @@ -203,11 +191,7 @@ public function head($url, array $headers = array()) * $browser->patch($url, array('Content-Length' => '11'), $body); * ``` * - * > For BC reasons, this method accepts the `$url` as either a `string` - * value or as an `UriInterface`. It's recommended to explicitly cast any - * objects implementing `UriInterface` to `string`. - * - * @param string|UriInterface $url URL for the request. + * @param string $url URL for the request. * @param array $headers * @param string|ReadableStreamInterface $contents * @return PromiseInterface @@ -249,11 +233,7 @@ public function patch($url, array $headers = array(), $contents = '') * $browser->put($url, array('Content-Length' => '11'), $body); * ``` * - * > For BC reasons, this method accepts the `$url` as either a `string` - * value or as an `UriInterface`. It's recommended to explicitly cast any - * objects implementing `UriInterface` to `string`. - * - * @param string|UriInterface $url URL for the request. + * @param string $url URL for the request. * @param array $headers * @param string|ReadableStreamInterface $contents * @return PromiseInterface @@ -272,11 +252,7 @@ public function put($url, array $headers = array(), $contents = '') * }); * ``` * - * > For BC reasons, this method accepts the `$url` as either a `string` - * value or as an `UriInterface`. It's recommended to explicitly cast any - * objects implementing `UriInterface` to `string`. - * - * @param string|UriInterface $url URL for the request. + * @param string $url URL for the request. * @param array $headers * @param string|ReadableStreamInterface $contents * @return PromiseInterface @@ -321,18 +297,11 @@ public function delete($url, array $headers = array(), $contents = '') * $browser->request('POST', $url, array('Content-Length' => '11'), $body); * ``` * - * > Note that this method is available as of v2.9.0 and always buffers the - * response body before resolving. - * It does not respect the deprecated [`streaming` option](#withoptions). - * If you want to stream the response body, you can use the - * [`requestStreaming()`](#requeststreaming) method instead. - * * @param string $method HTTP request method, e.g. GET/HEAD/POST etc. * @param string $url URL for the request * @param array $headers Additional request headers * @param string|ReadableStreamInterface $body HTTP request body contents * @return PromiseInterface - * @since 2.9.0 */ public function request($method, $url, array $headers = array(), $body = '') { @@ -399,93 +368,17 @@ public function request($method, $url, array $headers = array(), $body = '') * $browser->requestStreaming('POST', $url, array('Content-Length' => '11'), $body); * ``` * - * > Note that this method is available as of v2.9.0 and always resolves the - * response without buffering the response body. - * It does not respect the deprecated [`streaming` option](#withoptions). - * If you want to buffer the response body, use can use the - * [`request()`](#request) method instead. - * * @param string $method HTTP request method, e.g. GET/HEAD/POST etc. * @param string $url URL for the request * @param array $headers Additional request headers * @param string|ReadableStreamInterface $body HTTP request body contents * @return PromiseInterface - * @since 2.9.0 */ public function requestStreaming($method, $url, $headers = array(), $contents = '') { return $this->withOptions(array('streaming' => true))->requestMayBeStreaming($method, $url, $headers, $contents); } - /** - * [Deprecated] Submits an array of field values similar to submitting a form (`application/x-www-form-urlencoded`). - * - * ```php - * // deprecated: see post() instead - * $browser->submit($url, array('user' => 'test', 'password' => 'secret')); - * ``` - * - * This method will automatically add a matching `Content-Length` request - * header for the encoded length of the given `$fields`. - * - * > For BC reasons, this method accepts the `$url` as either a `string` - * value or as an `UriInterface`. It's recommended to explicitly cast any - * objects implementing `UriInterface` to `string`. - * - * @param string|UriInterface $url URL for the request. - * @param array $fields - * @param array $headers - * @param string $method - * @return PromiseInterface - * @deprecated 2.9.0 See self::post() instead. - * @see self::post() - */ - public function submit($url, array $fields, $headers = array(), $method = 'POST') - { - $headers['Content-Type'] = 'application/x-www-form-urlencoded'; - $contents = http_build_query($fields); - - return $this->requestMayBeStreaming($method, $url, $headers, $contents); - } - - /** - * [Deprecated] Sends an arbitrary instance implementing the [`RequestInterface`](#requestinterface) (PSR-7). - * - * The preferred way to send an HTTP request is by using the above - * [request methods](#request-methods), for example the [`get()`](#get) - * method to send an HTTP `GET` request. - * - * As an alternative, if you want to use a custom HTTP request method, you - * can use this method: - * - * ```php - * $request = new Request('OPTIONS', $url); - * - * // deprecated: see request() instead - * $browser->send($request)->then(…); - * ``` - * - * This method will automatically add a matching `Content-Length` request - * header if the size of the outgoing request body is known and non-empty. - * For an empty request body, if will only include a `Content-Length: 0` - * request header if the request method usually expects a request body (only - * applies to `POST`, `PUT` and `PATCH`). - * - * @param RequestInterface $request - * @return PromiseInterface - * @deprecated 2.9.0 See self::request() instead. - * @see self::request() - */ - public function send(RequestInterface $request) - { - if ($this->baseUrl !== null) { - // ensure we're actually below the base URL - $request = $request->withUri($this->messageFactory->expandBase($request->getUri(), $this->baseUrl)); - } - - return $this->transaction->send($request); - } - /** * Changes the maximum timeout used for waiting for pending requests. * @@ -676,15 +569,7 @@ public function withRejectErrorResponse($obeySuccessCode) * Notice that the [`Browser`](#browser) is an immutable object, i.e. the `withBase()` method * actually returns a *new* [`Browser`](#browser) instance with the given base URL applied. * - * > For BC reasons, this method accepts the `$baseUrl` as either a `string` - * value or as an `UriInterface`. It's recommended to explicitly cast any - * objects implementing `UriInterface` to `string`. - * - * > Changelog: As of v2.9.0 this method accepts a `null` value to reset the - * base URL. Earlier versions had to use the deprecated `withoutBase()` - * method to reset the base URL. - * - * @param string|null|UriInterface $baseUrl absolute base URL + * @param string|null $baseUrl absolute base URL * @return self * @throws InvalidArgumentException if the given $baseUrl is not a valid absolute URL * @see self::withoutBase() @@ -730,7 +615,6 @@ public function withBase($baseUrl) * @param string $protocolVersion HTTP protocol version to use, must be one of "1.1" or "1.0" * @return self * @throws InvalidArgumentException - * @since 2.8.0 */ public function withProtocolVersion($protocolVersion) { @@ -791,7 +675,7 @@ public function withResponseBuffer($maximumSize) } /** - * [Deprecated] Changes the [options](#options) to use: + * Changes the [options](#options) to use: * * The [`Browser`](#browser) class exposes several options for the handling of * HTTP transactions. These options resemble some of PHP's @@ -818,12 +702,11 @@ public function withResponseBuffer($maximumSize) * * @param array $options * @return self - * @deprecated 2.9.0 See self::withTimeout(), self::withFollowRedirects() and self::withRejectErrorResponse() instead. * @see self::withTimeout() * @see self::withFollowRedirects() * @see self::withRejectErrorResponse() */ - public function withOptions(array $options) + private function withOptions(array $options) { $browser = clone $this; $browser->transaction = $this->transaction->withOptions($options); @@ -831,28 +714,6 @@ public function withOptions(array $options) return $browser; } - /** - * [Deprecated] Removes the base URL. - * - * ```php - * // deprecated: see withBase() instead - * $newBrowser = $browser->withoutBase(); - * ``` - * - * Notice that the [`Browser`](#browser) is an immutable object, i.e. the `withoutBase()` method - * actually returns a *new* [`Browser`](#browser) instance without any base URL applied. - * - * See also [`withBase()`](#withbase). - * - * @return self - * @deprecated 2.9.0 See self::withBase() instead. - * @see self::withBase() - */ - public function withoutBase() - { - return $this->withBase(null); - } - /** * @param string $method * @param string|UriInterface $url @@ -862,6 +723,12 @@ public function withoutBase() */ private function requestMayBeStreaming($method, $url, array $headers = array(), $contents = '') { - return $this->send($this->messageFactory->request($method, $url, $headers, $contents, $this->protocolVersion)); + $request = $this->messageFactory->request($method, $url, $headers, $contents, $this->protocolVersion); + if ($this->baseUrl !== null) { + // ensure we're actually below the base URL + $request = $request->withUri($this->messageFactory->expandBase($request->getUri(), $this->baseUrl)); + } + + return $this->transaction->send($request); } } diff --git a/tests/BrowserTest.php b/tests/BrowserTest.php index 56a28303..88ef107e 100644 --- a/tests/BrowserTest.php +++ b/tests/BrowserTest.php @@ -120,19 +120,6 @@ public function testRequestStreamingGetSendsGetRequestWithStreamingExplicitlyEna $this->browser->requestStreaming('GET', 'http://example.com/'); } - public function testSubmitSendsPostRequest() - { - $that = $this; - $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { - $that->assertEquals('POST', $request->getMethod()); - $that->assertEquals('application/x-www-form-urlencoded', $request->getHeaderLine('Content-Type')); - $that->assertEquals('', (string)$request->getBody()); - return true; - }))->willReturn(new Promise(function () { })); - - $this->browser->submit('http://example.com/', array()); - } - public function testWithTimeoutTrueSetsDefaultTimeoutOption() { $this->sender->expects($this->once())->method('withOptions')->with(array('timeout' => null))->willReturnSelf(); @@ -356,7 +343,7 @@ public function testWithBaseUrlInvalidSchemeFails() public function testWithoutBaseFollowedByGetRequestTriesToSendIncompleteRequestUrl() { - $this->browser = $this->browser->withBase('http://example.com')->withoutBase(); + $this->browser = $this->browser->withBase('http://example.com')->withBase(null); $that = $this; $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { @@ -380,19 +367,6 @@ public function testWithProtocolVersionFollowedByGetRequestSendsRequestWithProto $this->browser->get('http://example.com/'); } - public function testWithProtocolVersionFollowedBySubmitRequestSendsRequestWithProtocolVersion() - { - $this->browser = $this->browser->withProtocolVersion('1.0'); - - $that = $this; - $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { - $that->assertEquals('1.0', $request->getProtocolVersion()); - return true; - }))->willReturn(new Promise(function () { })); - - $this->browser->submit('http://example.com/', array()); - } - public function testWithProtocolVersionInvalidThrows() { $this->setExpectedException('InvalidArgumentException'); diff --git a/tests/FunctionalBrowserTest.php b/tests/FunctionalBrowserTest.php index 16293fbb..c4bbe523 100644 --- a/tests/FunctionalBrowserTest.php +++ b/tests/FunctionalBrowserTest.php @@ -180,9 +180,9 @@ public function testCancelGetRequestWillRejectRequest() Block\await($promise, $this->loop); } - public function testCancelSendWithPromiseFollowerWillRejectRequest() + public function testCancelRequestWithPromiseFollowerWillRejectRequest() { - $promise = $this->browser->send(new Request('GET', $this->base . 'get'))->then(function () { + $promise = $this->browser->request('GET', $this->base . 'get')->then(function () { var_dump('noop'); }); $promise->cancel(); @@ -455,7 +455,7 @@ public function testReceiveStreamUntilConnectionsEndsForHttp10() public function testReceiveStreamChunkedForHttp11() { - $response = Block\await($this->browser->send(new Request('GET', $this->base . 'stream/1', array(), null, '1.1')), $this->loop); + $response = Block\await($this->browser->request('GET', $this->base . 'stream/1'), $this->loop); $this->assertEquals('1.1', $response->getProtocolVersion()); @@ -600,15 +600,6 @@ public function testHeadRequestReceivesResponseWithEmptyBodyButWithContentLength $this->assertEquals('5', $response->getHeaderLine('Content-Length')); } - public function testRequestGetReceivesBufferedResponseEvenWhenStreamingOptionHasBeenTurnedOn() - { - $response = Block\await( - $this->browser->withOptions(array('streaming' => true))->request('GET', $this->base . 'get'), - $this->loop - ); - $this->assertEquals('hello', (string)$response->getBody()); - } - public function testRequestStreamingGetReceivesStreamingResponseBody() { $buffer = Block\await( @@ -621,16 +612,6 @@ public function testRequestStreamingGetReceivesStreamingResponseBody() $this->assertEquals('hello', $buffer); } - public function testRequestStreamingGetReceivesStreamingResponseEvenWhenStreamingOptionHasBeenTurnedOff() - { - $response = Block\await( - $this->browser->withOptions(array('streaming' => false))->requestStreaming('GET', $this->base . 'get'), - $this->loop - ); - $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $response->getBody()); - $this->assertEquals('', (string)$response->getBody()); - } - public function testRequestStreamingGetReceivesStreamingResponseBodyEvenWhenResponseBufferExceeded() { $buffer = Block\await( From a7c1585306e16cc6826c11c7bf896946d3bab05f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 3 Jul 2020 22:52:24 +0200 Subject: [PATCH 319/456] Import react/http-client v0.5.10 Change namespace from `React\HttpClient` to `React\Http\Client` and mark all classes as internal only. See https://github.com/reactphp/http-client for original repo. --- composer.json | 1 - src/Client/ChunkedStreamDecoder.php | 207 ++++++ src/Client/Client.php | 31 + src/Client/Request.php | 295 +++++++++ src/Client/RequestData.php | 128 ++++ src/Client/Response.php | 175 +++++ src/Io/Sender.php | 4 +- tests/Client/DecodeChunkedStreamTest.php | 227 +++++++ tests/Client/FunctionalIntegrationTest.php | 170 +++++ tests/Client/RequestDataTest.php | 154 +++++ tests/Client/RequestTest.php | 714 +++++++++++++++++++++ tests/Client/ResponseTest.php | 168 +++++ tests/Io/SenderTest.php | 58 +- 13 files changed, 2300 insertions(+), 32 deletions(-) create mode 100644 src/Client/ChunkedStreamDecoder.php create mode 100644 src/Client/Client.php create mode 100644 src/Client/Request.php create mode 100644 src/Client/RequestData.php create mode 100644 src/Client/Response.php create mode 100644 tests/Client/DecodeChunkedStreamTest.php create mode 100644 tests/Client/FunctionalIntegrationTest.php create mode 100644 tests/Client/RequestDataTest.php create mode 100644 tests/Client/RequestTest.php create mode 100644 tests/Client/ResponseTest.php diff --git a/composer.json b/composer.json index 755e5d82..50afc4db 100644 --- a/composer.json +++ b/composer.json @@ -8,7 +8,6 @@ "evenement/evenement": "^3.0 || ^2.0 || ^1.0", "psr/http-message": "^1.0", "react/event-loop": "^1.0 || ^0.5", - "react/http-client": "^0.5.10", "react/promise": "^2.3 || ^1.2.1", "react/promise-stream": "^1.1", "react/socket": "^1.1", diff --git a/src/Client/ChunkedStreamDecoder.php b/src/Client/ChunkedStreamDecoder.php new file mode 100644 index 00000000..02cab52a --- /dev/null +++ b/src/Client/ChunkedStreamDecoder.php @@ -0,0 +1,207 @@ +stream = $stream; + $this->stream->on('data', array($this, 'handleData')); + $this->stream->on('end', array($this, 'handleEnd')); + Util::forwardEvents($this->stream, $this, array( + 'error', + )); + } + + /** @internal */ + public function handleData($data) + { + $this->buffer .= $data; + + do { + $bufferLength = strlen($this->buffer); + $continue = $this->iterateBuffer(); + $iteratedBufferLength = strlen($this->buffer); + } while ( + $continue && + $bufferLength !== $iteratedBufferLength && + $iteratedBufferLength > 0 + ); + + if ($this->buffer === false) { + $this->buffer = ''; + } + } + + protected function iterateBuffer() + { + if (strlen($this->buffer) <= 1) { + return false; + } + + if ($this->nextChunkIsLength) { + $crlfPosition = strpos($this->buffer, static::CRLF); + if ($crlfPosition === false && strlen($this->buffer) > 1024) { + $this->emit('error', array( + new Exception('Chunk length header longer then 1024 bytes'), + )); + $this->close(); + return false; + } + if ($crlfPosition === false) { + return false; // Chunk header hasn't completely come in yet + } + $lengthChunk = substr($this->buffer, 0, $crlfPosition); + if (strpos($lengthChunk, ';') !== false) { + list($lengthChunk) = explode(';', $lengthChunk, 2); + } + if ($lengthChunk !== '') { + $lengthChunk = ltrim(trim($lengthChunk), "0"); + if ($lengthChunk === '') { + // We've reached the end of the stream + $this->reachedEnd = true; + $this->emit('end'); + $this->close(); + return false; + } + } + $this->nextChunkIsLength = false; + if (dechex(@hexdec($lengthChunk)) !== strtolower($lengthChunk)) { + $this->emit('error', array( + new Exception('Unable to validate "' . $lengthChunk . '" as chunk length header'), + )); + $this->close(); + return false; + } + $this->remainingLength = hexdec($lengthChunk); + $this->buffer = substr($this->buffer, $crlfPosition + 2); + return true; + } + + if ($this->remainingLength > 0) { + $chunkLength = $this->getChunkLength(); + if ($chunkLength === 0) { + return true; + } + $this->emit('data', array( + substr($this->buffer, 0, $chunkLength), + $this + )); + $this->remainingLength -= $chunkLength; + $this->buffer = substr($this->buffer, $chunkLength); + return true; + } + + $this->nextChunkIsLength = true; + $this->buffer = substr($this->buffer, 2); + return true; + } + + protected function getChunkLength() + { + $bufferLength = strlen($this->buffer); + + if ($bufferLength >= $this->remainingLength) { + return $this->remainingLength; + } + + return $bufferLength; + } + + public function pause() + { + $this->stream->pause(); + } + + public function resume() + { + $this->stream->resume(); + } + + public function isReadable() + { + return $this->stream->isReadable(); + } + + public function pipe(WritableStreamInterface $dest, array $options = array()) + { + Util::pipe($this, $dest, $options); + + return $dest; + } + + public function close() + { + $this->closed = true; + return $this->stream->close(); + } + + /** @internal */ + public function handleEnd() + { + $this->handleData(''); + + if ($this->closed) { + return; + } + + if ($this->buffer === '' && $this->reachedEnd) { + $this->emit('end'); + $this->close(); + return; + } + + $this->emit( + 'error', + array( + new Exception('Stream ended with incomplete control code') + ) + ); + $this->close(); + } +} diff --git a/src/Client/Client.php b/src/Client/Client.php new file mode 100644 index 00000000..f28ec289 --- /dev/null +++ b/src/Client/Client.php @@ -0,0 +1,31 @@ +connector = $connector; + } + + public function request($method, $url, array $headers = array(), $protocolVersion = '1.0') + { + $requestData = new RequestData($method, $url, $headers, $protocolVersion); + + return new Request($this->connector, $requestData); + } +} diff --git a/src/Client/Request.php b/src/Client/Request.php new file mode 100644 index 00000000..7ebb627f --- /dev/null +++ b/src/Client/Request.php @@ -0,0 +1,295 @@ +connector = $connector; + $this->requestData = $requestData; + } + + public function isWritable() + { + return self::STATE_END > $this->state && !$this->ended; + } + + private function writeHead() + { + $this->state = self::STATE_WRITING_HEAD; + + $requestData = $this->requestData; + $streamRef = &$this->stream; + $stateRef = &$this->state; + $pendingWrites = &$this->pendingWrites; + $that = $this; + + $promise = $this->connect(); + $promise->then( + function (ConnectionInterface $stream) use ($requestData, &$streamRef, &$stateRef, &$pendingWrites, $that) { + $streamRef = $stream; + + $stream->on('drain', array($that, 'handleDrain')); + $stream->on('data', array($that, 'handleData')); + $stream->on('end', array($that, 'handleEnd')); + $stream->on('error', array($that, 'handleError')); + $stream->on('close', array($that, 'handleClose')); + + $headers = (string) $requestData; + + $more = $stream->write($headers . $pendingWrites); + + $stateRef = Request::STATE_HEAD_WRITTEN; + + // clear pending writes if non-empty + if ($pendingWrites !== '') { + $pendingWrites = ''; + + if ($more) { + $that->emit('drain'); + } + } + }, + array($this, 'closeError') + ); + + $this->on('close', function() use ($promise) { + $promise->cancel(); + }); + } + + public function write($data) + { + if (!$this->isWritable()) { + return false; + } + + // write directly to connection stream if already available + if (self::STATE_HEAD_WRITTEN <= $this->state) { + return $this->stream->write($data); + } + + // otherwise buffer and try to establish connection + $this->pendingWrites .= $data; + if (self::STATE_WRITING_HEAD > $this->state) { + $this->writeHead(); + } + + return false; + } + + public function end($data = null) + { + if (!$this->isWritable()) { + return; + } + + if (null !== $data) { + $this->write($data); + } else if (self::STATE_WRITING_HEAD > $this->state) { + $this->writeHead(); + } + + $this->ended = true; + } + + /** @internal */ + public function handleDrain() + { + $this->emit('drain'); + } + + /** @internal */ + 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")) { + try { + list($response, $bodyChunk) = $this->parseResponse($this->buffer); + } catch (\InvalidArgumentException $exception) { + $this->emit('error', array($exception)); + } + + $this->buffer = null; + + $this->stream->removeListener('drain', array($this, 'handleDrain')); + $this->stream->removeListener('data', array($this, 'handleData')); + $this->stream->removeListener('end', array($this, 'handleEnd')); + $this->stream->removeListener('error', array($this, 'handleError')); + $this->stream->removeListener('close', array($this, 'handleClose')); + + if (!isset($response)) { + return; + } + + $response->on('close', array($this, 'close')); + $that = $this; + $response->on('error', function (\Exception $error) use ($that) { + $that->closeError(new \RuntimeException( + "An error occured in the response", + 0, + $error + )); + }); + + $this->emit('response', array($response, $this)); + + $this->stream->emit('data', array($bodyChunk)); + } + } + + /** @internal */ + public function handleEnd() + { + $this->closeError(new \RuntimeException( + "Connection ended before receiving response" + )); + } + + /** @internal */ + public function handleError(\Exception $error) + { + $this->closeError(new \RuntimeException( + "An error occurred in the underlying stream", + 0, + $error + )); + } + + /** @internal */ + public function handleClose() + { + $this->close(); + } + + /** @internal */ + public function closeError(\Exception $error) + { + if (self::STATE_END <= $this->state) { + return; + } + $this->emit('error', array($error)); + $this->close(); + } + + public function close() + { + if (self::STATE_END <= $this->state) { + return; + } + + $this->state = self::STATE_END; + $this->pendingWrites = ''; + + if ($this->stream) { + $this->stream->close(); + } + + $this->emit('close'); + $this->removeAllListeners(); + } + + protected function parseResponse($data) + { + $psrResponse = gPsr\parse_response($data); + $headers = array_map(function($val) { + if (1 === count($val)) { + $val = $val[0]; + } + + return $val; + }, $psrResponse->getHeaders()); + + $factory = $this->getResponseFactory(); + + $response = $factory( + 'HTTP', + $psrResponse->getProtocolVersion(), + $psrResponse->getStatusCode(), + $psrResponse->getReasonPhrase(), + $headers + ); + + return array($response, (string)($psrResponse->getBody())); + } + + protected function connect() + { + $scheme = $this->requestData->getScheme(); + if ($scheme !== 'https' && $scheme !== 'http') { + return Promise\reject( + new \InvalidArgumentException('Invalid request URL given') + ); + } + + $host = $this->requestData->getHost(); + $port = $this->requestData->getPort(); + + if ($scheme === 'https') { + $host = 'tls://' . $host; + } + + return $this->connector + ->connect($host . ':' . $port); + } + + public function setResponseFactory($factory) + { + $this->responseFactory = $factory; + } + + public function getResponseFactory() + { + if (null === $factory = $this->responseFactory) { + $stream = $this->stream; + + $factory = function ($protocol, $version, $code, $reasonPhrase, $headers) use ($stream) { + return new Response( + $stream, + $protocol, + $version, + $code, + $reasonPhrase, + $headers + ); + }; + + $this->responseFactory = $factory; + } + + return $factory; + } +} diff --git a/src/Client/RequestData.php b/src/Client/RequestData.php new file mode 100644 index 00000000..55efaa9b --- /dev/null +++ b/src/Client/RequestData.php @@ -0,0 +1,128 @@ +method = $method; + $this->url = $url; + $this->headers = $headers; + $this->protocolVersion = $protocolVersion; + } + + private function mergeDefaultheaders(array $headers) + { + $port = ($this->getDefaultPort() === $this->getPort()) ? '' : ":{$this->getPort()}"; + $connectionHeaders = ('1.1' === $this->protocolVersion) ? array('Connection' => 'close') : array(); + $authHeaders = $this->getAuthHeaders(); + + $defaults = array_merge( + array( + 'Host' => $this->getHost().$port, + 'User-Agent' => 'React/alpha', + ), + $connectionHeaders, + $authHeaders + ); + + // remove all defaults that already exist in $headers + $lower = array_change_key_case($headers, CASE_LOWER); + foreach ($defaults as $key => $_) { + if (isset($lower[strtolower($key)])) { + unset($defaults[$key]); + } + } + + return array_merge($defaults, $headers); + } + + public function getScheme() + { + return parse_url($this->url, PHP_URL_SCHEME); + } + + public function getHost() + { + return parse_url($this->url, PHP_URL_HOST); + } + + public function getPort() + { + return (int) parse_url($this->url, PHP_URL_PORT) ?: $this->getDefaultPort(); + } + + public function getDefaultPort() + { + return ('https' === $this->getScheme()) ? 443 : 80; + } + + public function getPath() + { + $path = parse_url($this->url, PHP_URL_PATH); + $queryString = parse_url($this->url, PHP_URL_QUERY); + + // assume "/" path by default, but allow "OPTIONS *" + if ($path === null) { + $path = ($this->method === 'OPTIONS' && $queryString === null) ? '*': '/'; + } + if ($queryString !== null) { + $path .= '?' . $queryString; + } + + return $path; + } + + public function setProtocolVersion($version) + { + $this->protocolVersion = $version; + } + + public function __toString() + { + $headers = $this->mergeDefaultheaders($this->headers); + + $data = ''; + $data .= "{$this->method} {$this->getPath()} HTTP/{$this->protocolVersion}\r\n"; + foreach ($headers as $name => $values) { + foreach ((array)$values as $value) { + $data .= "$name: $value\r\n"; + } + } + $data .= "\r\n"; + + return $data; + } + + private function getUrlUserPass() + { + $components = parse_url($this->url); + + if (isset($components['user'])) { + return array( + 'user' => $components['user'], + 'pass' => isset($components['pass']) ? $components['pass'] : null, + ); + } + } + + private function getAuthHeaders() + { + if (null !== $auth = $this->getUrlUserPass()) { + return array( + 'Authorization' => 'Basic ' . base64_encode($auth['user'].':'.$auth['pass']), + ); + } + + return array(); + } +} diff --git a/src/Client/Response.php b/src/Client/Response.php new file mode 100644 index 00000000..be19eb4c --- /dev/null +++ b/src/Client/Response.php @@ -0,0 +1,175 @@ +stream = $stream; + $this->protocol = $protocol; + $this->version = $version; + $this->code = $code; + $this->reasonPhrase = $reasonPhrase; + $this->headers = $headers; + + if (strtolower($this->getHeaderLine('Transfer-Encoding')) === 'chunked') { + $this->stream = new ChunkedStreamDecoder($stream); + $this->removeHeader('Transfer-Encoding'); + } + + $this->stream->on('data', array($this, 'handleData')); + $this->stream->on('error', array($this, 'handleError')); + $this->stream->on('end', array($this, 'handleEnd')); + $this->stream->on('close', array($this, 'handleClose')); + } + + public function getProtocol() + { + return $this->protocol; + } + + public function getVersion() + { + return $this->version; + } + + public function getCode() + { + return $this->code; + } + + public function getReasonPhrase() + { + return $this->reasonPhrase; + } + + public function getHeaders() + { + return $this->headers; + } + + private function removeHeader($name) + { + foreach ($this->headers as $key => $value) { + if (strcasecmp($name, $key) === 0) { + unset($this->headers[$key]); + break; + } + } + } + + private function getHeader($name) + { + $name = strtolower($name); + $normalized = array_change_key_case($this->headers, CASE_LOWER); + + return isset($normalized[$name]) ? (array)$normalized[$name] : array(); + } + + private function getHeaderLine($name) + { + return implode(', ' , $this->getHeader($name)); + } + + /** @internal */ + public function handleData($data) + { + if ($this->readable) { + $this->emit('data', array($data)); + } + } + + /** @internal */ + public function handleEnd() + { + if (!$this->readable) { + return; + } + $this->emit('end'); + $this->close(); + } + + /** @internal */ + public function handleError(\Exception $error) + { + if (!$this->readable) { + return; + } + $this->emit('error', array(new \RuntimeException( + "An error occurred in the underlying stream", + 0, + $error + ))); + + $this->close(); + } + + /** @internal */ + public function handleClose() + { + $this->close(); + } + + public function close() + { + if (!$this->readable) { + return; + } + + $this->readable = false; + $this->stream->close(); + + $this->emit('close'); + $this->removeAllListeners(); + } + + public function isReadable() + { + return $this->readable; + } + + public function pause() + { + if (!$this->readable) { + return; + } + + $this->stream->pause(); + } + + public function resume() + { + if (!$this->readable) { + return; + } + + $this->stream->resume(); + } + + public function pipe(WritableStreamInterface $dest, array $options = array()) + { + Util::pipe($this, $dest, $options); + + return $dest; + } +} diff --git a/src/Io/Sender.php b/src/Io/Sender.php index e9c0a600..d16b09d0 100644 --- a/src/Io/Sender.php +++ b/src/Io/Sender.php @@ -5,8 +5,8 @@ use React\Http\Message\MessageFactory; use Psr\Http\Message\RequestInterface; use React\EventLoop\LoopInterface; -use React\HttpClient\Client as HttpClient; -use React\HttpClient\Response as ResponseStream; +use React\Http\Client\Client as HttpClient; +use React\Http\Client\Response as ResponseStream; use React\Promise\PromiseInterface; use React\Promise\Deferred; use React\Socket\ConnectorInterface; diff --git a/tests/Client/DecodeChunkedStreamTest.php b/tests/Client/DecodeChunkedStreamTest.php new file mode 100644 index 00000000..f238fb6b --- /dev/null +++ b/tests/Client/DecodeChunkedStreamTest.php @@ -0,0 +1,227 @@ + array( + array("4\r\nWiki\r\n5\r\npedia\r\ne\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n"), + ), + 'data-set-2' => array( + array("4\r\nWiki\r\n", "5\r\npedia\r\ne\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n"), + ), + 'data-set-3' => array( + array("4\r\nWiki\r\n", "5\r\n", "pedia\r\ne\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n"), + ), + 'data-set-4' => array( + array("4\r\nWiki\r\n", "5\r\n", "pedia\r\ne\r\n in\r\n", "\r\nchunks.\r\n0\r\n\r\n"), + ), + 'data-set-5' => array( + array("4\r\n", "Wiki\r\n", "5\r\n", "pedia\r\ne\r\n in\r\n", "\r\nchunks.\r\n0\r\n\r\n"), + ), + 'data-set-6' => array( + array("4\r\n", "Wiki\r\n", "5\r\n", "pedia\r\ne; foo=[bar,beer,pool,cue,win,won]\r\n", " in\r\n", "\r\nchunks.\r\n0\r\n\r\n"), + ), + 'header-fields' => array( + array("4; foo=bar\r\n", "Wiki\r\n", "5\r\n", "pedia\r\ne\r\n", " in\r\n", "\r\nchunks.\r\n", "0\r\n\r\n"), + ), + 'character-for-charactrr' => array( + str_split("4\r\nWiki\r\n5\r\npedia\r\ne\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n"), + ), + 'extra-newline-in-wiki-character-for-chatacter' => array( + str_split("6\r\nWi\r\nki\r\n5\r\npedia\r\ne\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n"), + "Wi\r\nkipedia in\r\n\r\nchunks." + ), + 'extra-newline-in-wiki' => array( + array("6\r\nWi\r\n", "ki\r\n5\r\npedia\r\ne\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n"), + "Wi\r\nkipedia in\r\n\r\nchunks." + ), + 'varnish-type-response-1' => array( + array("0017\r\nWikipedia in\r\n\r\nchunks.\r\n0\r\n\r\n") + ), + 'varnish-type-response-2' => array( + array("000017\r\nWikipedia in\r\n\r\nchunks.\r\n0\r\n\r\n") + ), + 'varnish-type-response-3' => array( + array("017\r\nWikipedia in\r\n\r\nchunks.\r\n0\r\n\r\n") + ), + 'varnish-type-response-4' => array( + array("004\r\nWiki\r\n005\r\npedia\r\n00e\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n") + ), + 'varnish-type-response-5' => array( + array("000004\r\nWiki\r\n00005\r\npedia\r\n000e\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n") + ), + 'varnish-type-response-extra-line' => array( + array("006\r\nWi\r\nki\r\n005\r\npedia\r\n00e\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n"), + "Wi\r\nkipedia in\r\n\r\nchunks." + ), + 'varnish-type-response-random' => array( + array(str_repeat("0", rand(0, 10)), "4\r\nWiki\r\n", str_repeat("0", rand(0, 10)), "5\r\npedia\r\n", str_repeat("0", rand(0, 10)), "e\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n") + ), + 'end-chunk-zero-check-1' => array( + array("4\r\nWiki\r\n5\r\npedia\r\ne\r\n in\r\n\r\nchunks.\r\n00\r\n\r\n") + ), + 'end-chunk-zero-check-2' => array( + array("4\r\nWiki\r\n5\r\npedia\r\ne\r\n in\r\n\r\nchunks.\r\n000\r\n\r\n") + ), + 'end-chunk-zero-check-3' => array( + array("00004\r\nWiki\r\n005\r\npedia\r\ne\r\n in\r\n\r\nchunks.\r\n0000\r\n\r\n") + ), + 'uppercase-chunk' => array( + array("4\r\nWiki\r\n5\r\npedia\r\nE\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n"), + ), + 'extra-space-in-length-chunk' => array( + array(" 04 \r\nWiki\r\n5\r\npedia\r\nE\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n"), + ), + 'only-whitespace-is-final-chunk' => array( + array(" \r\n\r\n"), + "" + ) + ); + } + + /** + * @test + * @dataProvider provideChunkedEncoding + */ + public function testChunkedEncoding(array $strings, $expected = "Wikipedia in\r\n\r\nchunks.") + { + $stream = new ThroughStream(); + $response = new ChunkedStreamDecoder($stream); + $buffer = ''; + $response->on('data', function ($data) use (&$buffer) { + $buffer .= $data; + }); + $response->on('error', function ($error) { + $this->fail((string)$error); + }); + foreach ($strings as $string) { + $stream->write($string); + } + $this->assertSame($expected, $buffer); + } + + public function provideInvalidChunkedEncoding() + { + return array( + 'chunk-body-longer-than-header-suggests' => array( + array("4\r\nWiwot40n98w3498tw3049nyn039409t34\r\n", "ki\r\n5\r\npedia\r\ne\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n"), + ), + 'invalid-header-charactrrs' => array( + str_split("xyz\r\nWi\r\nki\r\n5\r\npedia\r\ne\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n") + ), + 'header-chunk-to-long' => array( + str_split(str_repeat('a', 2015) . "\r\nWi\r\nki\r\n5\r\npedia\r\ne\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n") + ) + ); + } + + /** + * @test + * @dataProvider provideInvalidChunkedEncoding + */ + public function testInvalidChunkedEncoding(array $strings) + { + $stream = new ThroughStream(); + $response = new ChunkedStreamDecoder($stream); + $response->on('error', function (Exception $exception) { + throw $exception; + }); + + $this->setExpectedException('Exception'); + foreach ($strings as $string) { + $stream->write($string); + } + } + + public function provideZeroChunk() + { + return array( + array('1-zero' => "0\r\n\r\n"), + array('random-zero' => str_repeat("0", rand(2, 10))."\r\n\r\n") + ); + } + + /** + * @test + * @dataProvider provideZeroChunk + */ + public function testHandleEnd($zeroChunk) + { + $ended = false; + $stream = new ThroughStream(); + $response = new ChunkedStreamDecoder($stream); + $response->on('error', function ($error) { + $this->fail((string)$error); + }); + $response->on('end', function () use (&$ended) { + $ended = true; + }); + + $stream->write("4\r\nWiki\r\n".$zeroChunk); + + $this->assertTrue($ended); + } + + public function testHandleEndIncomplete() + { + $exception = null; + $stream = new ThroughStream(); + $response = new ChunkedStreamDecoder($stream); + $response->on('error', function ($e) use (&$exception) { + $exception = $e; + }); + + $stream->end("4\r\nWiki"); + + $this->assertInstanceOf('Exception', $exception); + } + + public function testHandleEndTrailers() + { + $ended = false; + $stream = new ThroughStream(); + $response = new ChunkedStreamDecoder($stream); + $response->on('error', function ($error) { + $this->fail((string)$error); + }); + $response->on('end', function () use (&$ended) { + $ended = true; + }); + + $stream->write("4\r\nWiki\r\n0\r\nabc: def\r\nghi: klm\r\n\r\n"); + + $this->assertTrue($ended); + } + + /** + * @test + * @dataProvider provideZeroChunk + */ + public function testHandleEndEnsureNoError($zeroChunk) + { + $ended = false; + $stream = new ThroughStream(); + $response = new ChunkedStreamDecoder($stream); + $response->on('error', function ($error) { + $this->fail((string)$error); + }); + $response->on('end', function () use (&$ended) { + $ended = true; + }); + + $stream->write("4\r\nWiki\r\n"); + $stream->write($zeroChunk); + $stream->end(); + + $this->assertTrue($ended); + } +} diff --git a/tests/Client/FunctionalIntegrationTest.php b/tests/Client/FunctionalIntegrationTest.php new file mode 100644 index 00000000..d6cc4b0f --- /dev/null +++ b/tests/Client/FunctionalIntegrationTest.php @@ -0,0 +1,170 @@ +on('connection', $this->expectCallableOnce()); + $server->on('connection', function (ConnectionInterface $conn) use ($server) { + $conn->end("HTTP/1.1 200 OK\r\n\r\nOk"); + $server->close(); + }); + $port = parse_url($server->getAddress(), PHP_URL_PORT); + + $client = new Client($loop); + $request = $client->request('GET', 'http://localhost:' . $port); + + $promise = Stream\first($request, 'close'); + $request->end(); + + Block\await($promise, $loop, self::TIMEOUT_LOCAL); + } + + public function testRequestLegacyHttpServerWithOnlyLineFeedReturnsSuccessfulResponse() + { + $loop = Factory::create(); + + $server = new Server(0, $loop); + $server->on('connection', function (ConnectionInterface $conn) use ($server) { + $conn->end("HTTP/1.0 200 OK\n\nbody"); + $server->close(); + }); + + $client = new Client($loop); + $request = $client->request('GET', str_replace('tcp:', 'http:', $server->getAddress())); + + $once = $this->expectCallableOnceWith('body'); + $request->on('response', function (Response $response) use ($once) { + $response->on('data', $once); + }); + + $promise = Stream\first($request, 'close'); + $request->end(); + + Block\await($promise, $loop, self::TIMEOUT_LOCAL); + } + + /** @group internet */ + public function testSuccessfulResponseEmitsEnd() + { + $loop = Factory::create(); + $client = new Client($loop); + + $request = $client->request('GET', 'http://www.google.com/'); + + $once = $this->expectCallableOnce(); + $request->on('response', function (Response $response) use ($once) { + $response->on('end', $once); + }); + + $promise = Stream\first($request, 'close'); + $request->end(); + + Block\await($promise, $loop, self::TIMEOUT_REMOTE); + } + + /** @group internet */ + public function testPostDataReturnsData() + { + $loop = Factory::create(); + $client = new Client($loop); + + $data = str_repeat('.', 33000); + $request = $client->request('POST', 'https://' . (mt_rand(0, 1) === 0 ? 'eu.' : '') . 'httpbin.org/post', array('Content-Length' => strlen($data))); + + $deferred = new Deferred(); + $request->on('response', function (Response $response) use ($deferred) { + $deferred->resolve(Stream\buffer($response)); + }); + + $request->on('error', 'printf'); + $request->on('error', $this->expectCallableNever()); + + $request->end($data); + + $buffer = Block\await($deferred->promise(), $loop, self::TIMEOUT_REMOTE); + + $this->assertNotEquals('', $buffer); + + $parsed = json_decode($buffer, true); + $this->assertTrue(is_array($parsed) && isset($parsed['data'])); + $this->assertEquals(strlen($data), strlen($parsed['data'])); + $this->assertEquals($data, $parsed['data']); + } + + /** @group internet */ + public function testPostJsonReturnsData() + { + $loop = Factory::create(); + $client = new Client($loop); + + $data = json_encode(array('numbers' => range(1, 50))); + $request = $client->request('POST', 'https://httpbin.org/post', array('Content-Length' => strlen($data), 'Content-Type' => 'application/json')); + + $deferred = new Deferred(); + $request->on('response', function (Response $response) use ($deferred) { + $deferred->resolve(Stream\buffer($response)); + }); + + $request->on('error', 'printf'); + $request->on('error', $this->expectCallableNever()); + + $request->end($data); + + $buffer = Block\await($deferred->promise(), $loop, self::TIMEOUT_REMOTE); + + $this->assertNotEquals('', $buffer); + + $parsed = json_decode($buffer, true); + $this->assertTrue(is_array($parsed) && isset($parsed['json'])); + $this->assertEquals(json_decode($data, true), $parsed['json']); + } + + /** @group internet */ + public function testCancelPendingConnectionEmitsClose() + { + $loop = Factory::create(); + $client = new Client($loop); + + $request = $client->request('GET', 'http://www.google.com/'); + $request->on('error', $this->expectCallableNever()); + $request->on('close', $this->expectCallableOnce()); + $request->end(); + $request->close(); + } +} diff --git a/tests/Client/RequestDataTest.php b/tests/Client/RequestDataTest.php new file mode 100644 index 00000000..313e140f --- /dev/null +++ b/tests/Client/RequestDataTest.php @@ -0,0 +1,154 @@ +assertSame($expected, $requestData->__toString()); + } + + /** @test */ + public function toStringReturnsHTTPRequestMessageWithEmptyQueryString() + { + $requestData = new RequestData('GET', 'http://www.example.com/path?hello=world'); + + $expected = "GET /path?hello=world HTTP/1.0\r\n" . + "Host: www.example.com\r\n" . + "User-Agent: React/alpha\r\n" . + "\r\n"; + + $this->assertSame($expected, $requestData->__toString()); + } + + /** @test */ + public function toStringReturnsHTTPRequestMessageWithZeroQueryStringAndRootPath() + { + $requestData = new RequestData('GET', 'http://www.example.com?0'); + + $expected = "GET /?0 HTTP/1.0\r\n" . + "Host: www.example.com\r\n" . + "User-Agent: React/alpha\r\n" . + "\r\n"; + + $this->assertSame($expected, $requestData->__toString()); + } + + /** @test */ + public function toStringReturnsHTTPRequestMessageWithOptionsAbsoluteRequestForm() + { + $requestData = new RequestData('OPTIONS', 'http://www.example.com/'); + + $expected = "OPTIONS / HTTP/1.0\r\n" . + "Host: www.example.com\r\n" . + "User-Agent: React/alpha\r\n" . + "\r\n"; + + $this->assertSame($expected, $requestData->__toString()); + } + + /** @test */ + public function toStringReturnsHTTPRequestMessageWithOptionsAsteriskRequestForm() + { + $requestData = new RequestData('OPTIONS', 'http://www.example.com'); + + $expected = "OPTIONS * HTTP/1.0\r\n" . + "Host: www.example.com\r\n" . + "User-Agent: React/alpha\r\n" . + "\r\n"; + + $this->assertSame($expected, $requestData->__toString()); + } + + /** @test */ + public function toStringReturnsHTTPRequestMessageWithProtocolVersion() + { + $requestData = new RequestData('GET', 'http://www.example.com'); + $requestData->setProtocolVersion('1.1'); + + $expected = "GET / HTTP/1.1\r\n" . + "Host: www.example.com\r\n" . + "User-Agent: React/alpha\r\n" . + "Connection: close\r\n" . + "\r\n"; + + $this->assertSame($expected, $requestData->__toString()); + } + + /** @test */ + public function toStringReturnsHTTPRequestMessageWithHeaders() + { + $requestData = new RequestData('GET', 'http://www.example.com', array( + 'User-Agent' => array(), + 'Via' => array( + 'first', + 'second' + ) + )); + + $expected = "GET / HTTP/1.0\r\n" . + "Host: www.example.com\r\n" . + "Via: first\r\n" . + "Via: second\r\n" . + "\r\n"; + + $this->assertSame($expected, $requestData->__toString()); + } + + /** @test */ + public function toStringReturnsHTTPRequestMessageWithHeadersInCustomCase() + { + $requestData = new RequestData('GET', 'http://www.example.com', array( + 'user-agent' => 'Hello', + 'LAST' => 'World' + )); + + $expected = "GET / HTTP/1.0\r\n" . + "Host: www.example.com\r\n" . + "user-agent: Hello\r\n" . + "LAST: World\r\n" . + "\r\n"; + + $this->assertSame($expected, $requestData->__toString()); + } + + /** @test */ + public function toStringReturnsHTTPRequestMessageWithProtocolVersionThroughConstructor() + { + $requestData = new RequestData('GET', 'http://www.example.com', array(), '1.1'); + + $expected = "GET / HTTP/1.1\r\n" . + "Host: www.example.com\r\n" . + "User-Agent: React/alpha\r\n" . + "Connection: close\r\n" . + "\r\n"; + + $this->assertSame($expected, $requestData->__toString()); + } + + /** @test */ + public function toStringUsesUserPassFromURL() + { + $requestData = new RequestData('GET', 'http://john:dummy@www.example.com'); + + $expected = "GET / HTTP/1.0\r\n" . + "Host: www.example.com\r\n" . + "User-Agent: React/alpha\r\n" . + "Authorization: Basic am9objpkdW1teQ==\r\n" . + "\r\n"; + + $this->assertSame($expected, $requestData->__toString()); + } +} diff --git a/tests/Client/RequestTest.php b/tests/Client/RequestTest.php new file mode 100644 index 00000000..e702d315 --- /dev/null +++ b/tests/Client/RequestTest.php @@ -0,0 +1,714 @@ +stream = $this->getMockBuilder('React\Socket\ConnectionInterface') + ->disableOriginalConstructor() + ->getMock(); + + $this->connector = $this->getMockBuilder('React\Socket\ConnectorInterface') + ->getMock(); + + $this->response = $this->getMockBuilder('React\Http\Client\Response') + ->disableOriginalConstructor() + ->getMock(); + } + + /** @test */ + public function requestShouldBindToStreamEventsAndUseconnector() + { + $requestData = new RequestData('GET', 'http://www.example.com'); + $request = new Request($this->connector, $requestData); + + $this->successfulConnectionMock(); + + $this->stream + ->expects($this->at(0)) + ->method('on') + ->with('drain', $this->identicalTo(array($request, 'handleDrain'))); + $this->stream + ->expects($this->at(1)) + ->method('on') + ->with('data', $this->identicalTo(array($request, 'handleData'))); + $this->stream + ->expects($this->at(2)) + ->method('on') + ->with('end', $this->identicalTo(array($request, 'handleEnd'))); + $this->stream + ->expects($this->at(3)) + ->method('on') + ->with('error', $this->identicalTo(array($request, 'handleError'))); + $this->stream + ->expects($this->at(4)) + ->method('on') + ->with('close', $this->identicalTo(array($request, 'handleClose'))); + $this->stream + ->expects($this->at(6)) + ->method('removeListener') + ->with('drain', $this->identicalTo(array($request, 'handleDrain'))); + $this->stream + ->expects($this->at(7)) + ->method('removeListener') + ->with('data', $this->identicalTo(array($request, 'handleData'))); + $this->stream + ->expects($this->at(8)) + ->method('removeListener') + ->with('end', $this->identicalTo(array($request, 'handleEnd'))); + $this->stream + ->expects($this->at(9)) + ->method('removeListener') + ->with('error', $this->identicalTo(array($request, 'handleError'))); + $this->stream + ->expects($this->at(10)) + ->method('removeListener') + ->with('close', $this->identicalTo(array($request, 'handleClose'))); + + $response = $this->response; + + $this->stream->expects($this->once()) + ->method('emit') + ->with('data', $this->identicalTo(array('body'))); + + $response->expects($this->at(0)) + ->method('on') + ->with('close', $this->anything()) + ->will($this->returnCallback(function ($event, $cb) use (&$endCallback) { + $endCallback = $cb; + })); + + $factory = $this->createCallableMock(); + $factory->expects($this->once()) + ->method('__invoke') + ->with('HTTP', '1.0', '200', 'OK', array('Content-Type' => 'text/plain')) + ->will($this->returnValue($response)); + + $request->setResponseFactory($factory); + + $handler = $this->createCallableMock(); + $handler->expects($this->once()) + ->method('__invoke') + ->with($response); + + $request->on('response', $handler); + $request->on('end', $this->expectCallableNever()); + + $handler = $this->createCallableMock(); + $handler->expects($this->once()) + ->method('__invoke'); + + $request->on('close', $handler); + $request->end(); + + $request->handleData("HTTP/1.0 200 OK\r\n"); + $request->handleData("Content-Type: text/plain\r\n"); + $request->handleData("\r\nbody"); + + $this->assertNotNull($endCallback); + call_user_func($endCallback); + } + + /** @test */ + public function requestShouldEmitErrorIfConnectionFails() + { + $requestData = new RequestData('GET', 'http://www.example.com'); + $request = new Request($this->connector, $requestData); + + $this->rejectedConnectionMock(); + + $handler = $this->createCallableMock(); + $handler->expects($this->once()) + ->method('__invoke') + ->with( + $this->isInstanceOf('RuntimeException') + ); + + $request->on('error', $handler); + + $handler = $this->createCallableMock(); + $handler->expects($this->once()) + ->method('__invoke'); + + $request->on('close', $handler); + $request->on('end', $this->expectCallableNever()); + + $request->end(); + } + + /** @test */ + public function requestShouldEmitErrorIfConnectionClosesBeforeResponseIsParsed() + { + $requestData = new RequestData('GET', 'http://www.example.com'); + $request = new Request($this->connector, $requestData); + + $this->successfulConnectionMock(); + + $handler = $this->createCallableMock(); + $handler->expects($this->once()) + ->method('__invoke') + ->with( + $this->isInstanceOf('RuntimeException') + ); + + $request->on('error', $handler); + + $handler = $this->createCallableMock(); + $handler->expects($this->once()) + ->method('__invoke'); + + $request->on('close', $handler); + $request->on('end', $this->expectCallableNever()); + + $request->end(); + $request->handleEnd(); + } + + /** @test */ + public function requestShouldEmitErrorIfConnectionEmitsError() + { + $requestData = new RequestData('GET', 'http://www.example.com'); + $request = new Request($this->connector, $requestData); + + $this->successfulConnectionMock(); + + $handler = $this->createCallableMock(); + $handler->expects($this->once()) + ->method('__invoke') + ->with( + $this->isInstanceOf('Exception') + ); + + $request->on('error', $handler); + + $handler = $this->createCallableMock(); + $handler->expects($this->once()) + ->method('__invoke'); + + $request->on('close', $handler); + $request->on('end', $this->expectCallableNever()); + + $request->end(); + $request->handleError(new \Exception('test')); + } + + /** @test */ + public function requestShouldEmitErrorIfGuzzleParseThrowsException() + { + $requestData = new RequestData('GET', 'http://www.example.com'); + $request = new Request($this->connector, $requestData); + + $this->successfulConnectionMock(); + + $handler = $this->createCallableMock(); + $handler->expects($this->once()) + ->method('__invoke') + ->with( + $this->isInstanceOf('\InvalidArgumentException') + ); + + $request->on('error', $handler); + + $request->end(); + $request->handleData("\r\n\r\n"); + } + + /** + * @test + */ + public function requestShouldEmitErrorIfUrlIsInvalid() + { + $requestData = new RequestData('GET', 'ftp://www.example.com'); + $request = new Request($this->connector, $requestData); + + $handler = $this->createCallableMock(); + $handler->expects($this->once()) + ->method('__invoke') + ->with( + $this->isInstanceOf('\InvalidArgumentException') + ); + + $request->on('error', $handler); + + $this->connector->expects($this->never()) + ->method('connect'); + + $request->end(); + } + + /** + * @test + */ + public function requestShouldEmitErrorIfUrlHasNoScheme() + { + $requestData = new RequestData('GET', 'www.example.com'); + $request = new Request($this->connector, $requestData); + + $handler = $this->createCallableMock(); + $handler->expects($this->once()) + ->method('__invoke') + ->with( + $this->isInstanceOf('\InvalidArgumentException') + ); + + $request->on('error', $handler); + + $this->connector->expects($this->never()) + ->method('connect'); + + $request->end(); + } + + /** @test */ + public function postRequestShouldSendAPostRequest() + { + $requestData = new RequestData('POST', 'http://www.example.com'); + $request = new Request($this->connector, $requestData); + + $this->successfulConnectionMock(); + + $this->stream + ->expects($this->once()) + ->method('write') + ->with($this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\nUser-Agent:.*\r\n\r\nsome post data$#")); + + $factory = $this->createCallableMock(); + $factory->expects($this->once()) + ->method('__invoke') + ->will($this->returnValue($this->response)); + + $request->setResponseFactory($factory); + $request->end('some post data'); + + $request->handleData("HTTP/1.0 200 OK\r\n"); + $request->handleData("Content-Type: text/plain\r\n"); + $request->handleData("\r\nbody"); + } + + /** @test */ + public function writeWithAPostRequestShouldSendToTheStream() + { + $requestData = new RequestData('POST', 'http://www.example.com'); + $request = new Request($this->connector, $requestData); + + $this->successfulConnectionMock(); + + $this->stream + ->expects($this->at(5)) + ->method('write') + ->with($this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\nUser-Agent:.*\r\n\r\nsome$#")); + $this->stream + ->expects($this->at(6)) + ->method('write') + ->with($this->identicalTo("post")); + $this->stream + ->expects($this->at(7)) + ->method('write') + ->with($this->identicalTo("data")); + + $factory = $this->createCallableMock(); + $factory->expects($this->once()) + ->method('__invoke') + ->will($this->returnValue($this->response)); + + $request->setResponseFactory($factory); + + $request->write("some"); + $request->write("post"); + $request->end("data"); + + $request->handleData("HTTP/1.0 200 OK\r\n"); + $request->handleData("Content-Type: text/plain\r\n"); + $request->handleData("\r\nbody"); + } + + /** @test */ + public function writeWithAPostRequestShouldSendBodyAfterHeadersAndEmitDrainEvent() + { + $requestData = new RequestData('POST', 'http://www.example.com'); + $request = new Request($this->connector, $requestData); + + $resolveConnection = $this->successfulAsyncConnectionMock(); + + $this->stream + ->expects($this->at(5)) + ->method('write') + ->with($this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\nUser-Agent:.*\r\n\r\nsomepost$#")) + ->willReturn(true); + $this->stream + ->expects($this->at(6)) + ->method('write') + ->with($this->identicalTo("data")); + + $factory = $this->createCallableMock(); + $factory->expects($this->once()) + ->method('__invoke') + ->will($this->returnValue($this->response)); + + $request->setResponseFactory($factory); + + $this->assertFalse($request->write("some")); + $this->assertFalse($request->write("post")); + + $request->on('drain', $this->expectCallableOnce()); + $request->once('drain', function () use ($request) { + $request->write("data"); + $request->end(); + }); + + $resolveConnection(); + + $request->handleData("HTTP/1.0 200 OK\r\n"); + $request->handleData("Content-Type: text/plain\r\n"); + $request->handleData("\r\nbody"); + } + + /** @test */ + public function writeWithAPostRequestShouldForwardDrainEventIfFirstChunkExceedsBuffer() + { + $requestData = new RequestData('POST', 'http://www.example.com'); + $request = new Request($this->connector, $requestData); + + $this->stream = $this->getMockBuilder('React\Socket\Connection') + ->disableOriginalConstructor() + ->setMethods(array('write')) + ->getMock(); + + $resolveConnection = $this->successfulAsyncConnectionMock(); + + $this->stream + ->expects($this->at(0)) + ->method('write') + ->with($this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\nUser-Agent:.*\r\n\r\nsomepost$#")) + ->willReturn(false); + $this->stream + ->expects($this->at(1)) + ->method('write') + ->with($this->identicalTo("data")); + + $factory = $this->createCallableMock(); + $factory->expects($this->once()) + ->method('__invoke') + ->will($this->returnValue($this->response)); + + $request->setResponseFactory($factory); + + $this->assertFalse($request->write("some")); + $this->assertFalse($request->write("post")); + + $request->on('drain', $this->expectCallableOnce()); + $request->once('drain', function () use ($request) { + $request->write("data"); + $request->end(); + }); + + $resolveConnection(); + $this->stream->emit('drain'); + + $request->handleData("HTTP/1.0 200 OK\r\n"); + $request->handleData("Content-Type: text/plain\r\n"); + $request->handleData("\r\nbody"); + } + + /** @test */ + public function pipeShouldPipeDataIntoTheRequestBody() + { + $requestData = new RequestData('POST', 'http://www.example.com'); + $request = new Request($this->connector, $requestData); + + $this->successfulConnectionMock(); + + $this->stream + ->expects($this->at(5)) + ->method('write') + ->with($this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\nUser-Agent:.*\r\n\r\nsome$#")); + $this->stream + ->expects($this->at(6)) + ->method('write') + ->with($this->identicalTo("post")); + $this->stream + ->expects($this->at(7)) + ->method('write') + ->with($this->identicalTo("data")); + + $factory = $this->createCallableMock(); + $factory->expects($this->once()) + ->method('__invoke') + ->will($this->returnValue($this->response)); + + $loop = $this + ->getMockBuilder('React\EventLoop\LoopInterface') + ->getMock(); + + $request->setResponseFactory($factory); + + $stream = fopen('php://memory', 'r+'); + $stream = new DuplexResourceStream($stream, $loop); + + $stream->pipe($request); + $stream->emit('data', array('some')); + $stream->emit('data', array('post')); + $stream->emit('data', array('data')); + + $request->handleData("HTTP/1.0 200 OK\r\n"); + $request->handleData("Content-Type: text/plain\r\n"); + $request->handleData("\r\nbody"); + } + + /** + * @test + */ + public function writeShouldStartConnecting() + { + $requestData = new RequestData('POST', 'http://www.example.com'); + $request = new Request($this->connector, $requestData); + + $this->connector->expects($this->once()) + ->method('connect') + ->with('www.example.com:80') + ->willReturn(new Promise(function () { })); + + $request->write('test'); + } + + /** + * @test + */ + public function endShouldStartConnectingAndChangeStreamIntoNonWritableMode() + { + $requestData = new RequestData('POST', 'http://www.example.com'); + $request = new Request($this->connector, $requestData); + + $this->connector->expects($this->once()) + ->method('connect') + ->with('www.example.com:80') + ->willReturn(new Promise(function () { })); + + $request->end(); + + $this->assertFalse($request->isWritable()); + } + + /** + * @test + */ + public function closeShouldEmitCloseEvent() + { + $requestData = new RequestData('POST', 'http://www.example.com'); + $request = new Request($this->connector, $requestData); + + $request->on('close', $this->expectCallableOnce()); + $request->close(); + } + + /** + * @test + */ + public function writeAfterCloseReturnsFalse() + { + $requestData = new RequestData('POST', 'http://www.example.com'); + $request = new Request($this->connector, $requestData); + + $request->close(); + + $this->assertFalse($request->isWritable()); + $this->assertFalse($request->write('nope')); + } + + /** + * @test + */ + public function endAfterCloseIsNoOp() + { + $requestData = new RequestData('POST', 'http://www.example.com'); + $request = new Request($this->connector, $requestData); + + $this->connector->expects($this->never()) + ->method('connect'); + + $request->close(); + $request->end(); + } + + /** + * @test + */ + public function closeShouldCancelPendingConnectionAttempt() + { + $requestData = new RequestData('POST', 'http://www.example.com'); + $request = new Request($this->connector, $requestData); + + $promise = new Promise(function () {}, function () { + throw new \RuntimeException(); + }); + + $this->connector->expects($this->once()) + ->method('connect') + ->with('www.example.com:80') + ->willReturn($promise); + + $request->end(); + + $request->on('error', $this->expectCallableNever()); + $request->on('close', $this->expectCallableOnce()); + + $request->close(); + $request->close(); + } + + /** @test */ + public function requestShouldRelayErrorEventsFromResponse() + { + $requestData = new RequestData('GET', 'http://www.example.com'); + $request = new Request($this->connector, $requestData); + + $this->successfulConnectionMock(); + + $response = $this->response; + + $response->expects($this->at(0)) + ->method('on') + ->with('close', $this->anything()); + $response->expects($this->at(1)) + ->method('on') + ->with('error', $this->anything()) + ->will($this->returnCallback(function ($event, $cb) use (&$errorCallback) { + $errorCallback = $cb; + })); + + $factory = $this->createCallableMock(); + $factory->expects($this->once()) + ->method('__invoke') + ->with('HTTP', '1.0', '200', 'OK', array('Content-Type' => 'text/plain')) + ->will($this->returnValue($response)); + + $request->setResponseFactory($factory); + $request->end(); + + $request->handleData("HTTP/1.0 200 OK\r\n"); + $request->handleData("Content-Type: text/plain\r\n"); + $request->handleData("\r\nbody"); + + $this->assertNotNull($errorCallback); + call_user_func($errorCallback, new \Exception('test')); + } + + /** @test */ + public function requestShouldRemoveAllListenerAfterClosed() + { + $requestData = new RequestData('GET', 'http://www.example.com'); + $request = new Request($this->connector, $requestData); + + $request->on('close', function () {}); + $this->assertCount(1, $request->listeners('close')); + + $request->close(); + $this->assertCount(0, $request->listeners('close')); + } + + private function successfulConnectionMock() + { + call_user_func($this->successfulAsyncConnectionMock()); + } + + private function successfulAsyncConnectionMock() + { + $deferred = new Deferred(); + + $this->connector + ->expects($this->once()) + ->method('connect') + ->with('www.example.com:80') + ->will($this->returnValue($deferred->promise())); + + $stream = $this->stream; + return function () use ($deferred, $stream) { + $deferred->resolve($stream); + }; + } + + private function rejectedConnectionMock() + { + $this->connector + ->expects($this->once()) + ->method('connect') + ->with('www.example.com:80') + ->will($this->returnValue(new RejectedPromise(new \RuntimeException()))); + } + + /** @test */ + public function multivalueHeader() + { + $requestData = new RequestData('GET', 'http://www.example.com'); + $request = new Request($this->connector, $requestData); + + $this->successfulConnectionMock(); + + $response = $this->response; + + $response->expects($this->at(0)) + ->method('on') + ->with('close', $this->anything()); + $response->expects($this->at(1)) + ->method('on') + ->with('error', $this->anything()) + ->will($this->returnCallback(function ($event, $cb) use (&$errorCallback) { + $errorCallback = $cb; + })); + + $factory = $this->createCallableMock(); + $factory->expects($this->once()) + ->method('__invoke') + ->with('HTTP', '1.0', '200', 'OK', array('Content-Type' => 'text/plain', 'X-Xss-Protection' => '1; mode=block', 'Cache-Control' => 'public, must-revalidate, max-age=0')) + ->will($this->returnValue($response)); + + $request->setResponseFactory($factory); + $request->end(); + + $request->handleData("HTTP/1.0 200 OK\r\n"); + $request->handleData("Content-Type: text/plain\r\n"); + $request->handleData("X-Xss-Protection:1; mode=block\r\n"); + $request->handleData("Cache-Control:public, must-revalidate, max-age=0\r\n"); + $request->handleData("\r\nbody"); + + $this->assertNotNull($errorCallback); + call_user_func($errorCallback, new \Exception('test')); + } + + /** @test */ + public function chunkedStreamDecoder() + { + $requestData = new RequestData('GET', 'http://www.example.com'); + $request = new Request($this->connector, $requestData); + + $this->successfulConnectionMock(); + + $request->end(); + + $this->stream->expects($this->once()) + ->method('emit') + ->with('data', array("1\r\nb\r")); + + $request->handleData("HTTP/1.0 200 OK\r\n"); + $request->handleData("Transfer-Encoding: chunked\r\n"); + $request->handleData("\r\n1\r\nb\r"); + $request->handleData("\n3\t\nody\r\n0\t\n\r\n"); + + } +} diff --git a/tests/Client/ResponseTest.php b/tests/Client/ResponseTest.php new file mode 100644 index 00000000..14467239 --- /dev/null +++ b/tests/Client/ResponseTest.php @@ -0,0 +1,168 @@ +stream = $this->getMockBuilder('React\Stream\DuplexStreamInterface') + ->getMock(); + } + + /** @test */ + public function responseShouldEmitEndEventOnEnd() + { + $this->stream + ->expects($this->at(0)) + ->method('on') + ->with('data', $this->anything()); + $this->stream + ->expects($this->at(1)) + ->method('on') + ->with('error', $this->anything()); + $this->stream + ->expects($this->at(2)) + ->method('on') + ->with('end', $this->anything()); + $this->stream + ->expects($this->at(3)) + ->method('on') + ->with('close', $this->anything()); + + $response = new Response($this->stream, 'HTTP', '1.0', '200', 'OK', array('Content-Type' => 'text/plain')); + + $handler = $this->createCallableMock(); + $handler->expects($this->once()) + ->method('__invoke') + ->with('some data'); + + $response->on('data', $handler); + + $handler = $this->createCallableMock(); + $handler->expects($this->once()) + ->method('__invoke'); + + $response->on('end', $handler); + + $handler = $this->createCallableMock(); + $handler->expects($this->once()) + ->method('__invoke'); + + $response->on('close', $handler); + + $this->stream + ->expects($this->at(0)) + ->method('close'); + + $response->handleData('some data'); + $response->handleEnd(); + + $this->assertSame( + array( + 'Content-Type' => 'text/plain' + ), + $response->getHeaders() + ); + } + + /** @test */ + public function closedResponseShouldNotBeResumedOrPaused() + { + $response = new Response($this->stream, 'http', '1.0', '200', 'ok', array('content-type' => 'text/plain')); + + $this->stream + ->expects($this->never()) + ->method('pause'); + $this->stream + ->expects($this->never()) + ->method('resume'); + + $response->handleEnd(); + + $response->resume(); + $response->pause(); + + $this->assertSame( + array( + 'content-type' => 'text/plain', + ), + $response->getHeaders() + ); + } + + /** @test */ + public function chunkedEncodingResponse() + { + $stream = new ThroughStream(); + $response = new Response( + $stream, + 'http', + '1.0', + '200', + 'ok', + array( + 'content-type' => 'text/plain', + 'transfer-encoding' => 'chunked', + ) + ); + + $buffer = ''; + $response->on('data', function ($data) use (&$buffer) { + $buffer.= $data; + }); + $this->assertSame('', $buffer); + $stream->write("4; abc=def\r\n"); + $this->assertSame('', $buffer); + $stream->write("Wiki\r\n"); + $this->assertSame('Wiki', $buffer); + + $this->assertSame( + array( + 'content-type' => 'text/plain', + ), + $response->getHeaders() + ); + } + + /** @test */ + public function doubleChunkedEncodingResponseWillBePassedAsIs() + { + $stream = new ThroughStream(); + $response = new Response( + $stream, + 'http', + '1.0', + '200', + 'ok', + array( + 'content-type' => 'text/plain', + 'transfer-encoding' => array( + 'chunked', + 'chunked' + ) + ) + ); + + $this->assertSame( + array( + 'content-type' => 'text/plain', + 'transfer-encoding' => array( + 'chunked', + 'chunked' + ) + ), + $response->getHeaders() + ); + } +} + diff --git a/tests/Io/SenderTest.php b/tests/Io/SenderTest.php index aaf93ce1..8a04d1f3 100644 --- a/tests/Io/SenderTest.php +++ b/tests/Io/SenderTest.php @@ -3,13 +3,13 @@ namespace React\Tests\Http\Io; use Clue\React\Block; +use React\Http\Client\Client as HttpClient; +use React\Http\Client\RequestData; use React\Http\Io\Sender; use React\Http\Message\ReadableBodyStream; -use React\Tests\Http\TestCase; -use React\HttpClient\Client as HttpClient; -use React\HttpClient\RequestData; use React\Promise; use React\Stream\ThroughStream; +use React\Tests\Http\TestCase; use RingCentral\Psr7\Request; class SenderTest extends TestCase @@ -63,13 +63,13 @@ public function testSenderConnectorRejection() public function testSendPostWillAutomaticallySendContentLengthHeader() { - $client = $this->getMockBuilder('React\HttpClient\Client')->disableOriginalConstructor()->getMock(); + $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); $client->expects($this->once())->method('request')->with( 'POST', 'http://www.google.com/', array('Host' => 'www.google.com', 'Content-Length' => '5'), '1.1' - )->willReturn($this->getMockBuilder('React\HttpClient\Request')->disableOriginalConstructor()->getMock()); + )->willReturn($this->getMockBuilder('React\Http\Client\Request')->disableOriginalConstructor()->getMock()); $sender = new Sender($client, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); @@ -79,13 +79,13 @@ public function testSendPostWillAutomaticallySendContentLengthHeader() public function testSendPostWillAutomaticallySendContentLengthZeroHeaderForEmptyRequestBody() { - $client = $this->getMockBuilder('React\HttpClient\Client')->disableOriginalConstructor()->getMock(); + $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); $client->expects($this->once())->method('request')->with( 'POST', 'http://www.google.com/', array('Host' => 'www.google.com', 'Content-Length' => '0'), '1.1' - )->willReturn($this->getMockBuilder('React\HttpClient\Request')->disableOriginalConstructor()->getMock()); + )->willReturn($this->getMockBuilder('React\Http\Client\Request')->disableOriginalConstructor()->getMock()); $sender = new Sender($client, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); @@ -95,10 +95,10 @@ public function testSendPostWillAutomaticallySendContentLengthZeroHeaderForEmpty public function testSendPostStreamWillAutomaticallySendTransferEncodingChunked() { - $outgoing = $this->getMockBuilder('React\HttpClient\Request')->disableOriginalConstructor()->getMock(); + $outgoing = $this->getMockBuilder('React\Http\Client\Request')->disableOriginalConstructor()->getMock(); $outgoing->expects($this->once())->method('write')->with(""); - $client = $this->getMockBuilder('React\HttpClient\Client')->disableOriginalConstructor()->getMock(); + $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); $client->expects($this->once())->method('request')->with( 'POST', 'http://www.google.com/', @@ -115,11 +115,11 @@ public function testSendPostStreamWillAutomaticallySendTransferEncodingChunked() public function testSendPostStreamWillAutomaticallyPipeChunkEncodeBodyForWriteAndRespectRequestThrottling() { - $outgoing = $this->getMockBuilder('React\HttpClient\Request')->disableOriginalConstructor()->getMock(); + $outgoing = $this->getMockBuilder('React\Http\Client\Request')->disableOriginalConstructor()->getMock(); $outgoing->expects($this->once())->method('isWritable')->willReturn(true); $outgoing->expects($this->exactly(2))->method('write')->withConsecutive(array(""), array("5\r\nhello\r\n"))->willReturn(false); - $client = $this->getMockBuilder('React\HttpClient\Client')->disableOriginalConstructor()->getMock(); + $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); $client->expects($this->once())->method('request')->willReturn($outgoing); $sender = new Sender($client, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); @@ -134,12 +134,12 @@ public function testSendPostStreamWillAutomaticallyPipeChunkEncodeBodyForWriteAn public function testSendPostStreamWillAutomaticallyPipeChunkEncodeBodyForEnd() { - $outgoing = $this->getMockBuilder('React\HttpClient\Request')->disableOriginalConstructor()->getMock(); + $outgoing = $this->getMockBuilder('React\Http\Client\Request')->disableOriginalConstructor()->getMock(); $outgoing->expects($this->once())->method('isWritable')->willReturn(true); $outgoing->expects($this->exactly(2))->method('write')->withConsecutive(array(""), array("0\r\n\r\n"))->willReturn(false); $outgoing->expects($this->once())->method('end')->with(null); - $client = $this->getMockBuilder('React\HttpClient\Client')->disableOriginalConstructor()->getMock(); + $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); $client->expects($this->once())->method('request')->willReturn($outgoing); $sender = new Sender($client, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); @@ -153,13 +153,13 @@ public function testSendPostStreamWillAutomaticallyPipeChunkEncodeBodyForEnd() public function testSendPostStreamWillRejectWhenRequestBodyEmitsErrorEvent() { - $outgoing = $this->getMockBuilder('React\HttpClient\Request')->disableOriginalConstructor()->getMock(); + $outgoing = $this->getMockBuilder('React\Http\Client\Request')->disableOriginalConstructor()->getMock(); $outgoing->expects($this->once())->method('isWritable')->willReturn(true); $outgoing->expects($this->once())->method('write')->with("")->willReturn(false); $outgoing->expects($this->never())->method('end'); $outgoing->expects($this->once())->method('close'); - $client = $this->getMockBuilder('React\HttpClient\Client')->disableOriginalConstructor()->getMock(); + $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); $client->expects($this->once())->method('request')->willReturn($outgoing); $sender = new Sender($client, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); @@ -183,13 +183,13 @@ public function testSendPostStreamWillRejectWhenRequestBodyEmitsErrorEvent() public function testSendPostStreamWillRejectWhenRequestBodyClosesWithoutEnd() { - $outgoing = $this->getMockBuilder('React\HttpClient\Request')->disableOriginalConstructor()->getMock(); + $outgoing = $this->getMockBuilder('React\Http\Client\Request')->disableOriginalConstructor()->getMock(); $outgoing->expects($this->once())->method('isWritable')->willReturn(true); $outgoing->expects($this->once())->method('write')->with("")->willReturn(false); $outgoing->expects($this->never())->method('end'); $outgoing->expects($this->once())->method('close'); - $client = $this->getMockBuilder('React\HttpClient\Client')->disableOriginalConstructor()->getMock(); + $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); $client->expects($this->once())->method('request')->willReturn($outgoing); $sender = new Sender($client, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); @@ -211,13 +211,13 @@ public function testSendPostStreamWillRejectWhenRequestBodyClosesWithoutEnd() public function testSendPostStreamWillNotRejectWhenRequestBodyClosesAfterEnd() { - $outgoing = $this->getMockBuilder('React\HttpClient\Request')->disableOriginalConstructor()->getMock(); + $outgoing = $this->getMockBuilder('React\Http\Client\Request')->disableOriginalConstructor()->getMock(); $outgoing->expects($this->once())->method('isWritable')->willReturn(true); $outgoing->expects($this->exactly(2))->method('write')->withConsecutive(array(""), array("0\r\n\r\n"))->willReturn(false); $outgoing->expects($this->once())->method('end'); $outgoing->expects($this->never())->method('close'); - $client = $this->getMockBuilder('React\HttpClient\Client')->disableOriginalConstructor()->getMock(); + $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); $client->expects($this->once())->method('request')->willReturn($outgoing); $sender = new Sender($client, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); @@ -239,13 +239,13 @@ public function testSendPostStreamWillNotRejectWhenRequestBodyClosesAfterEnd() public function testSendPostStreamWithExplicitContentLengthWillSendHeaderAsIs() { - $client = $this->getMockBuilder('React\HttpClient\Client')->disableOriginalConstructor()->getMock(); + $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); $client->expects($this->once())->method('request')->with( 'POST', 'http://www.google.com/', array('Host' => 'www.google.com', 'Content-Length' => '100'), '1.1' - )->willReturn($this->getMockBuilder('React\HttpClient\Request')->disableOriginalConstructor()->getMock()); + )->willReturn($this->getMockBuilder('React\Http\Client\Request')->disableOriginalConstructor()->getMock()); $sender = new Sender($client, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); @@ -256,13 +256,13 @@ public function testSendPostStreamWithExplicitContentLengthWillSendHeaderAsIs() public function testSendGetWillNotPassContentLengthHeaderForEmptyRequestBody() { - $client = $this->getMockBuilder('React\HttpClient\Client')->disableOriginalConstructor()->getMock(); + $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); $client->expects($this->once())->method('request')->with( 'GET', 'http://www.google.com/', array('Host' => 'www.google.com'), '1.1' - )->willReturn($this->getMockBuilder('React\HttpClient\Request')->disableOriginalConstructor()->getMock()); + )->willReturn($this->getMockBuilder('React\Http\Client\Request')->disableOriginalConstructor()->getMock()); $sender = new Sender($client, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); @@ -272,13 +272,13 @@ public function testSendGetWillNotPassContentLengthHeaderForEmptyRequestBody() public function testSendCustomMethodWillNotPassContentLengthHeaderForEmptyRequestBody() { - $client = $this->getMockBuilder('React\HttpClient\Client')->disableOriginalConstructor()->getMock(); + $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); $client->expects($this->once())->method('request')->with( 'CUSTOM', 'http://www.google.com/', array('Host' => 'www.google.com'), '1.1' - )->willReturn($this->getMockBuilder('React\HttpClient\Request')->disableOriginalConstructor()->getMock()); + )->willReturn($this->getMockBuilder('React\Http\Client\Request')->disableOriginalConstructor()->getMock()); $sender = new Sender($client, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); @@ -288,13 +288,13 @@ public function testSendCustomMethodWillNotPassContentLengthHeaderForEmptyReques public function testSendCustomMethodWithExplicitContentLengthZeroWillBePassedAsIs() { - $client = $this->getMockBuilder('React\HttpClient\Client')->disableOriginalConstructor()->getMock(); + $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); $client->expects($this->once())->method('request')->with( 'CUSTOM', 'http://www.google.com/', array('Host' => 'www.google.com', 'Content-Length' => '0'), '1.1' - )->willReturn($this->getMockBuilder('React\HttpClient\Request')->disableOriginalConstructor()->getMock()); + )->willReturn($this->getMockBuilder('React\Http\Client\Request')->disableOriginalConstructor()->getMock()); $sender = new Sender($client, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); @@ -370,7 +370,7 @@ public function provideRequestProtocolVersion() */ public function testRequestProtocolVersion(Request $Request, $method, $uri, $headers, $protocolVersion) { - $http = $this->getMockBuilder('React\HttpClient\Client') + $http = $this->getMockBuilder('React\Http\Client\Client') ->setMethods(array( 'request', )) @@ -378,7 +378,7 @@ public function testRequestProtocolVersion(Request $Request, $method, $uri, $hea $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(), ))->getMock(); - $request = $this->getMockBuilder('React\HttpClient\Request') + $request = $this->getMockBuilder('React\Http\Client\Request') ->setMethods(array()) ->setConstructorArgs(array( $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(), From 785ded0cce0b8ad6fa641ba9239e9ff43e6d52bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 7 Jul 2020 15:17:00 +0200 Subject: [PATCH 320/456] Organize and rename client and server examples --- README.md | 61 +++++++++++-------- composer.json | 4 +- ...1-google.php => 01-client-get-request.php} | 0 ....php => 02-client-concurrent-requests.php} | 0 .../{03-any.php => 03-client-request-any.php} | 0 ...-post-json.php => 04-client-post-json.php} | 0 .../{05-put-xml.php => 05-client-put-xml.php} | 0 ...y.php => 11-client-http-connect-proxy.php} | 7 ++- ...ks-proxy.php => 12-client-socks-proxy.php} | 4 +- ...-ssh-proxy.php => 13-client-ssh-proxy.php} | 0 ....php => 14-client-unix-domain-sockets.php} | 0 ...21-client-request-streaming-to-stdout.php} | 0 ...=> 22-client-stream-upload-from-stdin.php} | 0 ...lo-world.php => 51-server-hello-world.php} | 0 ...itors.php => 52-server-count-visitors.php} | 0 ...-client-ip.php => 53-server-whatsmyip.php} | 0 ...eter.php => 54-server-query-parameter.php} | 0 ...ling.php => 55-server-cookie-handling.php} | 0 .../{06-sleep.php => 56-server-sleep.php} | 0 ...dling.php => 57-server-error-handling.php} | 0 ...onse.php => 58-server-stream-response.php} | 0 ...09-json-api.php => 59-server-json-api.php} | 2 +- ...ps.php => 61-server-hello-world-https.php} | 0 ...2-upload.php => 62-server-form-upload.php} | 2 +- ...st.php => 63-server-streaming-request.php} | 0 ...ttp-proxy.php => 71-server-http-proxy.php} | 3 + ...y.php => 72-server-http-connect-proxy.php} | 3 + ...de-echo.php => 81-server-upgrade-echo.php} | 0 ...de-chat.php => 82-server-upgrade-chat.php} | 0 ...d.php => 91-client-benchmark-download.php} | 9 ++- ...oad.php => 92-client-benchmark-upload.php} | 9 ++- ...d.php => 99-server-benchmark-download.php} | 7 ++- src/Browser.php | 6 +- src/Server.php | 9 ++- 34 files changed, 77 insertions(+), 49 deletions(-) rename examples/{01-google.php => 01-client-get-request.php} (100%) rename examples/{02-concurrent.php => 02-client-concurrent-requests.php} (100%) rename examples/{03-any.php => 03-client-request-any.php} (100%) rename examples/{04-post-json.php => 04-client-post-json.php} (100%) rename examples/{05-put-xml.php => 05-client-put-xml.php} (100%) rename examples/{11-http-proxy.php => 11-client-http-connect-proxy.php} (82%) rename examples/{12-socks-proxy.php => 12-client-socks-proxy.php} (89%) rename examples/{13-ssh-proxy.php => 13-client-ssh-proxy.php} (100%) rename examples/{14-unix-domain-sockets.php => 14-client-unix-domain-sockets.php} (100%) rename examples/{21-stream-forwarding.php => 21-client-request-streaming-to-stdout.php} (100%) rename examples/{22-stream-stdin.php => 22-client-stream-upload-from-stdin.php} (100%) rename examples/{01-hello-world.php => 51-server-hello-world.php} (100%) rename examples/{02-count-visitors.php => 52-server-count-visitors.php} (100%) rename examples/{03-client-ip.php => 53-server-whatsmyip.php} (100%) rename examples/{04-query-parameter.php => 54-server-query-parameter.php} (100%) rename examples/{05-cookie-handling.php => 55-server-cookie-handling.php} (100%) rename examples/{06-sleep.php => 56-server-sleep.php} (100%) rename examples/{07-error-handling.php => 57-server-error-handling.php} (100%) rename examples/{08-stream-response.php => 58-server-stream-response.php} (100%) rename examples/{09-json-api.php => 59-server-json-api.php} (97%) rename examples/{11-hello-world-https.php => 61-server-hello-world-https.php} (100%) rename examples/{12-upload.php => 62-server-form-upload.php} (98%) rename examples/{13-stream-request.php => 63-server-streaming-request.php} (100%) rename examples/{21-http-proxy.php => 71-server-http-proxy.php} (94%) rename examples/{22-connect-proxy.php => 72-server-http-connect-proxy.php} (93%) rename examples/{31-upgrade-echo.php => 81-server-upgrade-echo.php} (100%) rename examples/{32-upgrade-chat.php => 82-server-upgrade-chat.php} (100%) rename examples/{91-benchmark-download.php => 91-client-benchmark-download.php} (85%) rename examples/{92-benchmark-upload.php => 92-client-benchmark-upload.php} (92%) rename examples/{99-benchmark-download.php => 99-server-benchmark-download.php} (91%) diff --git a/README.md b/README.md index 92877f5a..e4dc0e81 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,18 @@ -# Http +# HTTP [![Build Status](https://travis-ci.org/reactphp/http.svg?branch=master)](https://travis-ci.org/reactphp/http) -Event-driven, streaming plaintext HTTP and secure HTTPS server for [ReactPHP](https://reactphp.org/). +Event-driven, streaming HTTP client and server implementation for [ReactPHP](https://reactphp.org/). + +This HTTP library provides re-usable implementations for an HTTP client and +server based on ReactPHP's [`Socket`](https://github.com/reactphp/socket) and +[`EventLoop`](https://github.com/reactphp/event-loop) components. +Its client component allows you to send any number of async HTTP/HTTPS requests +concurrently. +Its server component allows you to build plaintext HTTP and secure HTTPS servers +that accept incoming HTTP requests from HTTP clients (such as web browsers). +This library provides async, streaming means for all of this, so you can handle +multiple concurrent HTTP requests without blocking. **Table of contents** @@ -91,8 +101,8 @@ This is an HTTP server which responds with `Hello World!` to every request. ```php $loop = React\EventLoop\Factory::create(); -$server = new Server(function (ServerRequestInterface $request) { - return new Response( +$server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterface $request) { + return new React\Http\Response( 200, array( 'Content-Type' => 'text/plain' @@ -107,7 +117,7 @@ $server->listen($socket); $loop->run(); ``` -See also the [examples](examples). +See also the [examples](examples/). ## Client Usage @@ -487,8 +497,8 @@ $browser->requestStreaming('GET', $url)->then(function (Psr\Http\Message\Respons }); ``` -See also the [stream download example](examples/91-benchmark-download.php) and -the [stream forwarding example](examples/21-stream-forwarding.php). +See also the [stream download benchmark example](examples/91-client-benchmark-download.php) and +the [stream forwarding example](examples/21-client-request-streaming-to-stdout.php). You can invoke the following methods on the message body: @@ -607,7 +617,7 @@ $connector = new React\Socket\Connector($loop, array( $browser = new React\Http\Browser($loop, $connector); ``` -See also the [HTTP CONNECT proxy example](examples/11-http-proxy.php). +See also the [HTTP CONNECT proxy example](examples/11-http-connect-proxy.php). ### SOCKS proxy @@ -738,7 +748,8 @@ $socket = new React\Socket\Server('0.0.0.0:8080', $loop); $server->listen($socket); ``` -See also the [`listen()`](#listen) method and the [first example](../examples/) +See also the [`listen()`](#listen) method and the +[hello world server example](examples/51-server-hello-world.php) for more details. By default, the `Server` buffers and parses the complete incoming HTTP @@ -846,7 +857,8 @@ $socket = new React\Socket\Server('0.0.0.0:8080', $loop); $server->listen($socket); ``` -See also [example #1](examples) for more details. +See also [hello world server example](examples/51-server-hello-world.php) +for more details. This example will start listening for HTTP requests on the alternative HTTP port `8080` on all interfaces (publicly). As an alternative, it is @@ -873,7 +885,8 @@ $socket = new React\Socket\Server('tls://0.0.0.0:8443', $loop, array( $server->listen($socket); ``` -See also [example #11](examples) for more details. +See also [hello world HTTPS example](examples/61-server-hello-world-https.php) +for more details. ### Server Request @@ -945,7 +958,7 @@ $server = new Server(function (ServerRequestInterface $request) { }); ``` -See also [example #3](examples). +See also [whatsmyip server example](examples/53-server-whatsmyip.php). > Advanced: Note that address parameters will not be set if you're listening on a Unix domain socket (UDS) path as this protocol lacks the concept of @@ -983,7 +996,7 @@ Use [`htmlentities`](https://www.php.net/manual/en/function.htmlentities.php) like in this example to prevent [Cross-Site Scripting (abbreviated as XSS)](https://en.wikipedia.org/wiki/Cross-site_scripting). -See also [example #4](examples). +See also [server query parameters example](examples/54-server-query-parameter.php). #### Request body @@ -1022,7 +1035,7 @@ $server = new Server(function (ServerRequestInterface $request) { }); ``` -See also [example #12](examples) for more details. +See also [form upload example](examples/62-server-form-upload.php) for more details. The `getBody(): StreamInterface` method can be used to get the raw data from this request body, similar to @@ -1047,7 +1060,7 @@ $server = new Server(function (ServerRequestInterface $request) { }); ``` -See also [example #9](examples) for more details. +See also [JSON API server example](examples/59-server-json-api.php) for more details. The `getUploadedFiles(): array` method can be used to get the uploaded files in this request, similar to @@ -1070,7 +1083,7 @@ $server = new Server(function (ServerRequestInterface $request) { }); ``` -See also [example #12](examples) for more details. +See also [form upload server example](examples/62-server-form-upload.php) for more details. The `getSize(): ?int` method can be used to get the size of the request body, similar to PHP's `$_SERVER['CONTENT_LENGTH']` variable. @@ -1169,7 +1182,7 @@ $server = new React\Http\Server(array( The above example simply counts the number of bytes received in the request body. This can be used as a skeleton for buffering or processing the request body. -See also [example #13](examples) for more details. +See also [streaming request server example](examples/63-server-streaming-request.php) for more details. The `data` event will be emitted whenever new data is available on the request body stream. @@ -1307,7 +1320,7 @@ non-alphanumeric characters. This encoding is also used internally when decoding the name and value of cookies (which is in line with other implementations, such as PHP's cookie functions). -See also [example #5](examples) for more details. +See also [cookie server example](examples/55-server-cookie-handling.php) for more details. #### Invalid request @@ -1467,7 +1480,7 @@ in this case (if applicable). to look into using [Ratchet](http://socketo.me/) instead. If you want to handle a custom protocol, you will likely want to look into the [HTTP specs](https://tools.ietf.org/html/rfc7230#section-6.7) and also see - [examples #31 and #32](examples) for more details. + [examples #81 and #82](examples/) for more details. In particular, the `101` (Switching Protocols) response code MUST NOT be used unless you send an `Upgrade` response header value that is also present in the corresponding HTTP/1.1 `Upgrade` request header value. @@ -1488,7 +1501,7 @@ in this case (if applicable). requests, one may still be present. Normal request body processing applies here and the connection will only turn to "tunneling mode" after the request body has been processed (which should be empty in most cases). - See also [example #22](examples) for more details. + See also [HTTP CONNECT server example](examples/72-server-http-connect-proxy.php) for more details. #### Response length @@ -1851,7 +1864,7 @@ $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response }); ``` -See also [example 01](examples/01-google.php). +See also [GET request client example](examples/01-client-get-request.php). #### post() @@ -1870,7 +1883,7 @@ $browser->post( }); ``` -See also [example 04](examples/04-post-json.php). +See also [POST JSON client example](examples/04-client-post-json.php). This method is also commonly used to submit HTML form data: @@ -1964,7 +1977,7 @@ $browser->put( }); ``` -See also [example 05](examples/05-put-xml.php). +See also [PUT XML client example](examples/05-client-put-xml.php). This method will automatically add a matching `Content-Length` request header if the outgoing request body is a `string`. If you're using a @@ -2538,7 +2551,7 @@ $server = new Server(array( )); ``` -See also [example #12](examples) for more details. +See also [form upload server example](examples/62-server-form-upload.php) for more details. By default, this middleware respects the [`upload_max_filesize`](https://www.php.net/manual/en/ini.core.php#ini.upload-max-filesize) diff --git a/composer.json b/composer.json index 50afc4db..711d3156 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "react/http", - "description": "Event-driven, streaming plaintext HTTP and secure HTTPS server for ReactPHP", - "keywords": ["event-driven", "streaming", "HTTP", "HTTPS", "server", "ReactPHP"], + "description": "Event-driven, streaming HTTP client and server implementation for ReactPHP", + "keywords": ["HTTP client", "HTTP server", "HTTP", "HTTPS", "event-driven", "streaming", "client", "server", "PSR-7", "async", "ReactPHP"], "license": "MIT", "require": { "php": ">=5.3.0", diff --git a/examples/01-google.php b/examples/01-client-get-request.php similarity index 100% rename from examples/01-google.php rename to examples/01-client-get-request.php diff --git a/examples/02-concurrent.php b/examples/02-client-concurrent-requests.php similarity index 100% rename from examples/02-concurrent.php rename to examples/02-client-concurrent-requests.php diff --git a/examples/03-any.php b/examples/03-client-request-any.php similarity index 100% rename from examples/03-any.php rename to examples/03-client-request-any.php diff --git a/examples/04-post-json.php b/examples/04-client-post-json.php similarity index 100% rename from examples/04-post-json.php rename to examples/04-client-post-json.php diff --git a/examples/05-put-xml.php b/examples/05-client-put-xml.php similarity index 100% rename from examples/05-put-xml.php rename to examples/05-client-put-xml.php diff --git a/examples/11-http-proxy.php b/examples/11-client-http-connect-proxy.php similarity index 82% rename from examples/11-http-proxy.php rename to examples/11-client-http-connect-proxy.php index d1ad9cf5..53d2e91a 100644 --- a/examples/11-http-proxy.php +++ b/examples/11-client-http-connect-proxy.php @@ -1,5 +1,11 @@ /dev/null // $ wget http://localhost:8080/10g.bin -O /dev/null // $ ab -n10 -c10 http://localhost:8080/1g.bin -// $ docker run -it --rm --net=host jordi/ab ab -n10 -c10 http://localhost:8080/1g.bin +// $ docker run -it --rm --net=host jordi/ab -n100000 -c10 http://localhost:8080/ +// $ docker run -it --rm --net=host jordi/ab -n10 -c10 http://localhost:8080/1g.bin use Evenement\EventEmitter; use Psr\Http\Message\ServerRequestInterface; @@ -118,7 +119,7 @@ public function getSize() ); }); -$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); +$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop, array('tcp' => array('backlog' => 511))); $server->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/src/Browser.php b/src/Browser.php index 38479c86..28f90f87 100644 --- a/src/Browser.php +++ b/src/Browser.php @@ -73,7 +73,7 @@ public function __construct(LoopInterface $loop, ConnectorInterface $connector = * }); * ``` * - * See also [example 01](../examples/01-google.php). + * See also [GET request client example](../examples/01-client-get-request.php). * * @param string $url URL for the request. * @param array $headers @@ -99,7 +99,7 @@ public function get($url, array $headers = array()) * }); * ``` * - * See also [example 04](../examples/04-post-json.php). + * See also [POST JSON client example](../examples/04-client-post-json.php). * * This method is also commonly used to submit HTML form data: * @@ -216,7 +216,7 @@ public function patch($url, array $headers = array(), $contents = '') * }); * ``` * - * See also [example 05](../examples/05-put-xml.php). + * See also [PUT XML client example](../examples/05-client-put-xml.php). * * This method will automatically add a matching `Content-Length` request * header if the outgoing request body is a `string`. If you're using a diff --git a/src/Server.php b/src/Server.php index 3fe942c9..81b6bd0a 100644 --- a/src/Server.php +++ b/src/Server.php @@ -55,7 +55,8 @@ * $server->listen($socket); * ``` * - * See also the [`listen()`](#listen) method and the [first example](../examples/) + * See also the [`listen()`](#listen) method and + * [hello world server example](../examples/51-server-hello-world.php) * for more details. * * By default, the `Server` buffers and parses the complete incoming HTTP @@ -229,7 +230,8 @@ public function __construct($requestHandler) * $server->listen($socket); * ``` * - * See also [example #1](examples) for more details. + * See also [hello world server example](../examples/51-server-hello-world.php) + * for more details. * * This example will start listening for HTTP requests on the alternative * HTTP port `8080` on all interfaces (publicly). As an alternative, it is @@ -256,7 +258,8 @@ public function __construct($requestHandler) * $server->listen($socket); * ``` * - * See also [example #11](examples) for more details. + * See also [hello world HTTPS example](../examples/61-server-hello-world-https.php) + * for more details. * * @param ServerInterface $socket */ From 9be6091857adf6ddf14cca5d312ca8aa64fddb3d Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Tue, 7 Jul 2020 18:55:22 +0200 Subject: [PATCH 321/456] Add full core team to the license --- LICENSE | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/LICENSE b/LICENSE index 0ca9208a..d6f8901f 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,6 @@ The MIT License (MIT) -Copyright (c) 2013 Christian Lück -Copyright (c) 2012 Igor Wiedler, Chris Boden +Copyright (c) 2012 Christian Lück, Cees-Jan Kiewiet, Jan Sorgalla, Chris Boden, Igor Wiedler Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From c0fb86ee81011857ff28c8828b62ef93511f4cf3 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Wed, 24 Jun 2020 18:18:42 +0200 Subject: [PATCH 322/456] Add the event loop as required constructor argument For now we won't use it just yet. But this will prepare our API for future features like keep alive connections, dead connection clean up etc. Even though we won't use it in the next release, it will put it in the API so we don't have to break it again when we add features that require the event loop. As a result of this requirement the event loop has been added as a dependency, and the socket and stream packages have been bumped to their stable version. --- README.md | 66 +++---- examples/51-server-hello-world.php | 2 +- examples/52-server-count-visitors.php | 2 +- examples/53-server-whatsmyip.php | 2 +- examples/54-server-query-parameter.php | 2 +- examples/55-server-cookie-handling.php | 2 +- examples/56-server-sleep.php | 2 +- examples/57-server-error-handling.php | 2 +- examples/58-server-stream-response.php | 2 +- examples/59-server-json-api.php | 2 +- examples/61-server-hello-world-https.php | 2 +- examples/62-server-form-upload.php | 2 +- examples/71-server-http-proxy.php | 2 +- examples/72-server-http-connect-proxy.php | 2 +- examples/81-server-upgrade-echo.php | 2 +- examples/82-server-upgrade-chat.php | 2 +- examples/99-server-benchmark-download.php | 2 +- src/Io/StreamingServer.php | 7 +- src/Server.php | 6 +- tests/FunctionalBrowserTest.php | 8 +- tests/FunctionalServerTest.php | 46 ++--- tests/Io/StreamingServerTest.php | 201 +++++++++++----------- tests/ServerTest.php | 18 +- 23 files changed, 196 insertions(+), 188 deletions(-) diff --git a/README.md b/README.md index e4dc0e81..afdaeae1 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,7 @@ This is an HTTP server which responds with `Hello World!` to every request. ```php $loop = React\EventLoop\Factory::create(); -$server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterface $request) { +$server = new React\Http\Server($loop, function (Psr\Http\Message\ServerRequestInterface $request) { return new React\Http\Response( 200, array( @@ -714,7 +714,7 @@ the constructor and will be invoked with the respective [request](#server-reques object and expects a [response](#response) object in return: ```php -$server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterface $request) { +$server = new React\Http\Server($loop, function (Psr\Http\Message\ServerRequestInterface $request) { return new React\Http\Response( 200, array( @@ -742,7 +742,7 @@ chapter. In its most simple form, you can attach this to a to start a plaintext HTTP server like this: ```php -$server = new React\Http\Server($handler); +$server = new React\Http\Server($loop, $handler); $socket = new React\Socket\Server('0.0.0.0:8080', $loop); $server->listen($socket); @@ -803,7 +803,7 @@ to explicitly configure the total number of requests that can be handled at once like this: ```php -$server = new React\Http\Server(array( +$server = new React\Http\Server($loop, array( new React\Http\Middleware\StreamingRequestMiddleware(), new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers new React\Http\Middleware\RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request @@ -822,7 +822,7 @@ also use a streaming approach where only small chunks of data have to be kept in memory: ```php -$server = new React\Http\Server(array( +$server = new React\Http\Server($loop, array( new React\Http\Middleware\StreamingRequestMiddleware(), $handler )); @@ -851,7 +851,7 @@ messages. In its most common form, you can attach this to a order to start a plaintext HTTP server like this: ```php -$server = new React\Http\Server($handler); +$server = new React\Http\Server($loop, $handler); $socket = new React\Socket\Server('0.0.0.0:8080', $loop); $server->listen($socket); @@ -877,7 +877,7 @@ using a secure TLS listen address, a certificate file and optional `passphrase` like this: ```php -$server = new React\Http\Server($handler); +$server = new React\Http\Server($loop, $handler); $socket = new React\Socket\Server('tls://0.0.0.0:8443', $loop, array( 'local_cert' => __DIR__ . '/localhost.pem' @@ -902,7 +902,7 @@ which in turn extends the and will be passed to the callback function like this. ```php -$server = new Server(function (ServerRequestInterface $request) { +$server = new Server($loop, function (ServerRequestInterface $request) { $body = "The method of the request is: " . $request->getMethod(); $body .= "The requested path is: " . $request->getUri()->getPath(); @@ -945,7 +945,7 @@ The following parameters are currently available: Set to 'on' if the request used HTTPS, otherwise it won't be set ```php -$server = new Server(function (ServerRequestInterface $request) { +$server = new Server($loop, function (ServerRequestInterface $request) { $body = "Your IP is: " . $request->getServerParams()['REMOTE_ADDR']; return new Response( @@ -970,7 +970,7 @@ The `getQueryParams(): array` method can be used to get the query parameters similiar to the `$_GET` variable. ```php -$server = new Server(function (ServerRequestInterface $request) { +$server = new Server($loop, function (ServerRequestInterface $request) { $queryParams = $request->getQueryParams(); $body = 'The query parameter "foo" is not set. Click the following link '; @@ -1024,7 +1024,7 @@ By default, this method will only return parsed data for requests using request headers (commonly used for `POST` requests for HTML form submission data). ```php -$server = new Server(function (ServerRequestInterface $request) { +$server = new Server($loop, function (ServerRequestInterface $request) { $name = $request->getParsedBody()['name'] ?? 'anonymous'; return new Response( @@ -1048,7 +1048,7 @@ an XML (`Content-Type: application/xml`) request body (which is commonly used fo `POST`, `PUT` or `PATCH` requests in JSON-based or RESTful/RESTish APIs). ```php -$server = new Server(function (ServerRequestInterface $request) { +$server = new Server($loop, function (ServerRequestInterface $request) { $data = json_decode((string)$request->getBody()); $name = $data->name ?? 'anonymous'; @@ -1071,7 +1071,7 @@ This array will only be filled when using the `Content-Type: multipart/form-data request header (commonly used for `POST` requests for HTML file uploads). ```php -$server = new Server(function (ServerRequestInterface $request) { +$server = new Server($loop, function (ServerRequestInterface $request) { $files = $request->getUploadedFiles(); $name = isset($files['avatar']) ? $files['avatar']->getClientFilename() : 'nothing'; @@ -1141,7 +1141,7 @@ The ReactPHP `ReadableStreamInterface` gives you access to the incoming request body as the individual chunks arrive: ```php -$server = new React\Http\Server(array( +$server = new React\Http\Server($loop, array( new React\Http\Middleware\StreamingRequestMiddleware(), function (Psr\Http\Message\ServerRequestInterface $request) { $body = $request->getBody(); @@ -1214,7 +1214,7 @@ This method operates on the streaming request body, i.e. the request body size may be unknown (`null`) when using `Transfer-Encoding: chunked` for HTTP/1.1 requests. ```php -$server = new React\Http\Server(array( +$server = new React\Http\Server($loop, array( new React\Http\Middleware\StreamingRequestMiddleware(), function (Psr\Http\Message\ServerRequestInterface $request) { $size = $request->getBody()->getSize(); @@ -1287,7 +1287,7 @@ The `getCookieParams(): string[]` method can be used to get all cookies sent with the current request. ```php -$server = new Server(function (ServerRequestInterface $request) { +$server = new Server($loop, function (ServerRequestInterface $request) { $key = 'react\php'; if (isset($request->getCookieParams()[$key])) { @@ -1359,7 +1359,7 @@ but feel free to use any implemantation of the `PSR-7 ResponseInterface` you prefer. ```php -$server = new Server(function (ServerRequestInterface $request) { +$server = new Server($loop, function (ServerRequestInterface $request) { return new Response( 200, array( @@ -1382,7 +1382,7 @@ To prevent this you SHOULD use a This example shows how such a long-term action could look like: ```php -$server = new Server(function (ServerRequestInterface $request) use ($loop) { +$server = new Server($loop, function (ServerRequestInterface $request) use ($loop) { return new Promise(function ($resolve, $reject) use ($loop) { $loop->addTimer(1.5, function() use ($resolve) { $response = new Response( @@ -1419,7 +1419,7 @@ Note that other implementations of the `PSR-7 ResponseInterface` likely only support strings. ```php -$server = new Server(function (ServerRequestInterface $request) use ($loop) { +$server = new Server($loop, function (ServerRequestInterface $request) use ($loop) { $stream = new ThroughStream(); $timer = $loop->addPeriodicTimer(0.5, function () use ($stream) { @@ -1510,7 +1510,7 @@ added automatically. This is the most common use case, for example when using a `string` response body like this: ```php -$server = new Server(function (ServerRequestInterface $request) { +$server = new Server($loop, function (ServerRequestInterface $request) { return new Response( 200, array( @@ -1529,7 +1529,7 @@ response messages will contain the plain response body. If you know the length of your streaming response body, you MAY want to specify it explicitly like this: ```php -$server = new Server(function (ServerRequestInterface $request) use ($loop) { +$server = new Server($loop, function (ServerRequestInterface $request) use ($loop) { $stream = new ThroughStream(); $loop->addTimer(2.0, function () use ($stream) { @@ -1608,7 +1608,7 @@ A `Date` header will be automatically added with the system date and time if non You can add a custom `Date` header yourself like this: ```php -$server = new Server(function (ServerRequestInterface $request) { +$server = new Server($loop, function (ServerRequestInterface $request) { return new Response( 200, array( @@ -1622,7 +1622,7 @@ If you don't have a appropriate clock to rely on, you should unset this header with an empty string: ```php -$server = new Server(function (ServerRequestInterface $request) { +$server = new Server($loop, function (ServerRequestInterface $request) { return new Response( 200, array( @@ -1636,7 +1636,7 @@ Note that it will automatically assume a `X-Powered-By: react/alpha` header unless your specify a custom `X-Powered-By` header yourself: ```php -$server = new Server(function (ServerRequestInterface $request) { +$server = new Server($loop, function (ServerRequestInterface $request) { return new Response( 200, array( @@ -1650,7 +1650,7 @@ If you do not want to send this header at all, you can use an empty string as value like this: ```php -$server = new Server(function (ServerRequestInterface $request) { +$server = new Server($loop, function (ServerRequestInterface $request) { return new Response( 200, array( @@ -1723,7 +1723,7 @@ The following example adds a middleware request handler that adds the current ti header (`Request-Time`) and a final request handler that always returns a 200 code without a body: ```php -$server = new Server(array( +$server = new Server($loop, array( function (ServerRequestInterface $request, callable $next) { $request = $request->withHeader('Request-Time', time()); return $next($request); @@ -1747,7 +1747,7 @@ In order to simplify handling both paths, you can simply wrap this in a [`Promise\resolve()`](https://reactphp.org/promise/#resolve) call like this: ```php -$server = new Server(array( +$server = new Server($loop, array( function (ServerRequestInterface $request, callable $next) { $promise = React\Promise\resolve($next($request)); return $promise->then(function (ResponseInterface $response) { @@ -1769,7 +1769,7 @@ handling logic (or logging etc.) by wrapping this in a [`Promise`](https://reactphp.org/promise/#promise) like this: ```php -$server = new Server(array( +$server = new Server($loop, array( function (ServerRequestInterface $request, callable $next) { $promise = new React\Promise\Promise(function ($resolve) use ($next, $request) { $resolve($next($request)); @@ -2404,7 +2404,7 @@ The following example shows how this middleware can be used to ensure no more than 10 handlers will be invoked at once: ```php -$server = new Server(array( +$server = new Server($loop, array( new LimitConcurrentRequestsMiddleware(10), $handler )); @@ -2415,7 +2415,7 @@ Similarly, this middleware is often used in combination with the to limit the total number of requests that can be buffered at once: ```php -$server = new Server(array( +$server = new Server($loop, array( new StreamingRequestMiddleware(), new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers new RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request @@ -2429,7 +2429,7 @@ that can be buffered at once and then ensure the actual request handler only processes one request after another without any concurrency: ```php -$server = new Server(array( +$server = new Server($loop, array( new StreamingRequestMiddleware(), new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers new RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request @@ -2482,7 +2482,7 @@ the total number of concurrent requests. Usage: ```php -$server = new Server(array( +$server = new Server($loop, array( new StreamingRequestMiddleware(), new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers new RequestBodyBufferMiddleware(16 * 1024 * 1024), // 16 MiB @@ -2542,7 +2542,7 @@ $handler = function (ServerRequestInterface $request) { ); }; -$server = new Server(array( +$server = new Server($loop, array( new StreamingRequestMiddleware(), new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers new RequestBodyBufferMiddleware(16 * 1024 * 1024), // 16 MiB diff --git a/examples/51-server-hello-world.php b/examples/51-server-hello-world.php index f703a5d7..c0ad6741 100644 --- a/examples/51-server-hello-world.php +++ b/examples/51-server-hello-world.php @@ -9,7 +9,7 @@ $loop = Factory::create(); -$server = new Server(function (ServerRequestInterface $request) { +$server = new Server($loop, function (ServerRequestInterface $request) { return new Response( 200, array( diff --git a/examples/52-server-count-visitors.php b/examples/52-server-count-visitors.php index 5dec93f9..1fa051f3 100644 --- a/examples/52-server-count-visitors.php +++ b/examples/52-server-count-visitors.php @@ -10,7 +10,7 @@ $loop = Factory::create(); $counter = 0; -$server = new Server(function (ServerRequestInterface $request) use (&$counter) { +$server = new Server($loop, function (ServerRequestInterface $request) use (&$counter) { return new Response( 200, array( diff --git a/examples/53-server-whatsmyip.php b/examples/53-server-whatsmyip.php index 25e3d408..512334eb 100644 --- a/examples/53-server-whatsmyip.php +++ b/examples/53-server-whatsmyip.php @@ -9,7 +9,7 @@ $loop = Factory::create(); -$server = new Server(function (ServerRequestInterface $request) { +$server = new Server($loop, function (ServerRequestInterface $request) { $body = "Your IP is: " . $request->getServerParams()['REMOTE_ADDR']; return new Response( diff --git a/examples/54-server-query-parameter.php b/examples/54-server-query-parameter.php index 13015430..aaee50e6 100644 --- a/examples/54-server-query-parameter.php +++ b/examples/54-server-query-parameter.php @@ -9,7 +9,7 @@ $loop = Factory::create(); -$server = new Server(function (ServerRequestInterface $request) { +$server = new Server($loop, function (ServerRequestInterface $request) { $queryParams = $request->getQueryParams(); $body = 'The query parameter "foo" is not set. Click the following link '; diff --git a/examples/55-server-cookie-handling.php b/examples/55-server-cookie-handling.php index e09d9277..d96d4a85 100644 --- a/examples/55-server-cookie-handling.php +++ b/examples/55-server-cookie-handling.php @@ -9,7 +9,7 @@ $loop = Factory::create(); -$server = new Server(function (ServerRequestInterface $request) { +$server = new Server($loop, function (ServerRequestInterface $request) { $key = 'react\php'; if (isset($request->getCookieParams()[$key])) { diff --git a/examples/56-server-sleep.php b/examples/56-server-sleep.php index ae465fb5..141db6ea 100644 --- a/examples/56-server-sleep.php +++ b/examples/56-server-sleep.php @@ -10,7 +10,7 @@ $loop = Factory::create(); -$server = new Server(function (ServerRequestInterface $request) use ($loop) { +$server = new Server($loop, function (ServerRequestInterface $request) use ($loop) { return new Promise(function ($resolve, $reject) use ($loop) { $loop->addTimer(1.5, function() use ($resolve) { $response = new Response( diff --git a/examples/57-server-error-handling.php b/examples/57-server-error-handling.php index 76544a6b..625fda17 100644 --- a/examples/57-server-error-handling.php +++ b/examples/57-server-error-handling.php @@ -11,7 +11,7 @@ $loop = Factory::create(); $count = 0; -$server = new Server(function (ServerRequestInterface $request) use (&$count) { +$server = new Server($loop, function (ServerRequestInterface $request) use (&$count) { return new Promise(function ($resolve, $reject) use (&$count) { $count++; diff --git a/examples/58-server-stream-response.php b/examples/58-server-stream-response.php index dce54b2b..d246a237 100644 --- a/examples/58-server-stream-response.php +++ b/examples/58-server-stream-response.php @@ -10,7 +10,7 @@ $loop = Factory::create(); -$server = new Server(function (ServerRequestInterface $request) use ($loop) { +$server = new Server($loop, function (ServerRequestInterface $request) use ($loop) { if ($request->getMethod() !== 'GET' || $request->getUri()->getPath() !== '/') { return new Response(404); } diff --git a/examples/59-server-json-api.php b/examples/59-server-json-api.php index 3702c69c..79f87db4 100644 --- a/examples/59-server-json-api.php +++ b/examples/59-server-json-api.php @@ -15,7 +15,7 @@ $loop = Factory::create(); -$server = new Server(function (ServerRequestInterface $request) { +$server = new Server($loop, function (ServerRequestInterface $request) { if ($request->getHeaderLine('Content-Type') !== 'application/json') { return new Response( 415, // Unsupported Media Type diff --git a/examples/61-server-hello-world-https.php b/examples/61-server-hello-world-https.php index c8bc52e8..19eff50b 100644 --- a/examples/61-server-hello-world-https.php +++ b/examples/61-server-hello-world-https.php @@ -9,7 +9,7 @@ $loop = Factory::create(); -$server = new Server(function (ServerRequestInterface $request) { +$server = new Server($loop, function (ServerRequestInterface $request) { return new Response( 200, array( diff --git a/examples/62-server-form-upload.php b/examples/62-server-form-upload.php index 4290fb4c..4db8c5d7 100644 --- a/examples/62-server-form-upload.php +++ b/examples/62-server-form-upload.php @@ -124,7 +124,7 @@ // Note how this example explicitly uses the advanced `StreamingRequestMiddleware` to apply // custom request buffering limits below before running our request handler. -$server = new Server(array( +$server = new Server($loop, array( new StreamingRequestMiddleware(), new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers, queue otherwise new RequestBodyBufferMiddleware(8 * 1024 * 1024), // 8 MiB max, ignore body otherwise diff --git a/examples/71-server-http-proxy.php b/examples/71-server-http-proxy.php index 57ed8e50..5b829a73 100644 --- a/examples/71-server-http-proxy.php +++ b/examples/71-server-http-proxy.php @@ -17,7 +17,7 @@ // This means that this proxy buffers the whole request before "processing" it. // As such, this is store-and-forward proxy. This could also use the advanced // `StreamingRequestMiddleware` to forward the incoming request as it comes in. -$server = new Server(function (RequestInterface $request) { +$server = new Server($loop, function (RequestInterface $request) { if (strpos($request->getRequestTarget(), '://') === false) { return new Response( 400, diff --git a/examples/72-server-http-connect-proxy.php b/examples/72-server-http-connect-proxy.php index e2fa42a4..6a0364e8 100644 --- a/examples/72-server-http-connect-proxy.php +++ b/examples/72-server-http-connect-proxy.php @@ -19,7 +19,7 @@ // Unlike the plain HTTP proxy, the CONNECT method does not contain a body // and we establish an end-to-end connection over the stream object, so this // doesn't have to store any payload data in memory at all. -$server = new Server(function (ServerRequestInterface $request) use ($connector) { +$server = new Server($loop, function (ServerRequestInterface $request) use ($connector) { if ($request->getMethod() !== 'CONNECT') { return new Response( 405, diff --git a/examples/81-server-upgrade-echo.php b/examples/81-server-upgrade-echo.php index df572d50..6ae2ce8e 100644 --- a/examples/81-server-upgrade-echo.php +++ b/examples/81-server-upgrade-echo.php @@ -30,7 +30,7 @@ // Note how this example uses the `Server` without the `StreamingRequestMiddleware`. // The initial incoming request does not contain a body and we upgrade to a // stream object below. -$server = new Server(function (ServerRequestInterface $request) use ($loop) { +$server = new Server($loop, function (ServerRequestInterface $request) use ($loop) { if ($request->getHeaderLine('Upgrade') !== 'echo' || $request->getProtocolVersion() === '1.0') { return new Response( 426, diff --git a/examples/82-server-upgrade-chat.php b/examples/82-server-upgrade-chat.php index 5d60154c..ee4ce146 100644 --- a/examples/82-server-upgrade-chat.php +++ b/examples/82-server-upgrade-chat.php @@ -38,7 +38,7 @@ // Note how this example uses the `Server` without the `StreamingRequestMiddleware`. // The initial incoming request does not contain a body and we upgrade to a // stream object below. -$server = new Server(function (ServerRequestInterface $request) use ($loop, $chat) { +$server = new Server($loop, function (ServerRequestInterface $request) use ($loop, $chat) { if ($request->getHeaderLine('Upgrade') !== 'chat' || $request->getProtocolVersion() === '1.0') { return new Response( 426, diff --git a/examples/99-server-benchmark-download.php b/examples/99-server-benchmark-download.php index 5b5a5abe..bd4acde6 100644 --- a/examples/99-server-benchmark-download.php +++ b/examples/99-server-benchmark-download.php @@ -87,7 +87,7 @@ public function getSize() } } -$server = new Server(function (ServerRequestInterface $request) use ($loop) { +$server = new Server($loop, function (ServerRequestInterface $request) use ($loop) { switch ($request->getUri()->getPath()) { case '/': return new Response( diff --git a/src/Io/StreamingServer.php b/src/Io/StreamingServer.php index 30ab7705..4c20e39a 100644 --- a/src/Io/StreamingServer.php +++ b/src/Io/StreamingServer.php @@ -5,6 +5,7 @@ use Evenement\EventEmitter; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; +use React\EventLoop\LoopInterface; use React\Http\Response; use React\Promise; use React\Promise\CancellablePromiseInterface; @@ -83,6 +84,7 @@ final class StreamingServer extends EventEmitter { private $callback; private $parser; + private $loop; /** * Creates an HTTP server that invokes the given callback for each incoming HTTP request @@ -92,11 +94,14 @@ final class StreamingServer extends EventEmitter * connections in order to then parse incoming data as HTTP. * See also [listen()](#listen) for more details. * + * @param LoopInterface $loop * @param callable|callable[] $requestHandler * @see self::listen() */ - public function __construct($requestHandler) + public function __construct(LoopInterface $loop, $requestHandler) { + $this->loop = $loop; + if (!\is_callable($requestHandler) && !\is_array($requestHandler)) { throw new \InvalidArgumentException('Invalid request handler given'); } elseif (!\is_callable($requestHandler)) { diff --git a/src/Server.php b/src/Server.php index 81b6bd0a..ee1f1f65 100644 --- a/src/Server.php +++ b/src/Server.php @@ -3,6 +3,7 @@ namespace React\Http; use Evenement\EventEmitter; +use React\EventLoop\LoopInterface; use React\Http\Io\IniUtil; use React\Http\Io\StreamingServer; use React\Http\Middleware\LimitConcurrentRequestsMiddleware; @@ -164,10 +165,11 @@ final class Server extends EventEmitter * connections in order to then parse incoming data as HTTP. * See also [listen()](#listen) for more details. * + * @param LoopInterface $loop * @param callable|callable[] $requestHandler * @see self::listen() */ - public function __construct($requestHandler) + public function __construct(LoopInterface $loop, $requestHandler) { if (!\is_callable($requestHandler) && !\is_array($requestHandler)) { throw new \InvalidArgumentException('Invalid request handler given'); @@ -204,7 +206,7 @@ public function __construct($requestHandler) $middleware = \array_merge($middleware, $requestHandler); } - $this->streamingServer = new StreamingServer($middleware); + $this->streamingServer = new StreamingServer($loop, $middleware); $that = $this; $this->streamingServer->on('error', function ($error) use ($that) { diff --git a/tests/FunctionalBrowserTest.php b/tests/FunctionalBrowserTest.php index c4bbe523..de5a9f9a 100644 --- a/tests/FunctionalBrowserTest.php +++ b/tests/FunctionalBrowserTest.php @@ -32,7 +32,7 @@ public function setUpBrowserAndServer() $this->loop = $loop = Factory::create(); $this->browser = new Browser($this->loop); - $server = new Server(array(new StreamingRequestMiddleware(), function (ServerRequestInterface $request) use ($loop) { + $server = new Server($this->loop, array(new StreamingRequestMiddleware(), function (ServerRequestInterface $request) use ($loop) { $path = $request->getUri()->getPath(); $headers = array(); @@ -527,7 +527,7 @@ public function testPostStreamKnownLength() */ public function testPostStreamWillStartSendingRequestEvenWhenBodyDoesNotEmitData() { - $server = new Server(array(new StreamingRequestMiddleware(), function (ServerRequestInterface $request) { + $server = new Server($this->loop, array(new StreamingRequestMiddleware(), function (ServerRequestInterface $request) { return new Response(200); })); $socket = new \React\Socket\Server(0, $this->loop); @@ -554,7 +554,7 @@ public function testPostStreamClosed() public function testSendsHttp11ByDefault() { - $server = new Server(function (ServerRequestInterface $request) { + $server = new Server($this->loop, function (ServerRequestInterface $request) { return new Response( 200, array(), @@ -574,7 +574,7 @@ public function testSendsHttp11ByDefault() public function testSendsExplicitHttp10Request() { - $server = new Server(function (ServerRequestInterface $request) { + $server = new Server($this->loop, function (ServerRequestInterface $request) { return new Response( 200, array(), diff --git a/tests/FunctionalServerTest.php b/tests/FunctionalServerTest.php index 2de43ce8..f92fb2b0 100644 --- a/tests/FunctionalServerTest.php +++ b/tests/FunctionalServerTest.php @@ -26,7 +26,7 @@ public function testPlainHttpOnRandomPort() $loop = Factory::create(); $connector = new Connector($loop); - $server = new Server(function (RequestInterface $request) { + $server = new Server($loop, function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); @@ -52,7 +52,7 @@ public function testPlainHttpOnRandomPortWithSingleRequestHandlerArray() $loop = Factory::create(); $connector = new Connector($loop); - $server = new Server(array( + $server = new Server($loop, array( function () { return new Response(404); }, @@ -79,7 +79,7 @@ public function testPlainHttpOnRandomPortWithoutHostHeaderUsesSocketUri() $loop = Factory::create(); $connector = new Connector($loop); - $server = new Server(function (RequestInterface $request) { + $server = new Server($loop, function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); @@ -105,7 +105,7 @@ public function testPlainHttpOnRandomPortWithOtherHostHeaderTakesPrecedence() $loop = Factory::create(); $connector = new Connector($loop); - $server = new Server(function (RequestInterface $request) { + $server = new Server($loop, function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); @@ -137,7 +137,7 @@ public function testSecureHttpsOnRandomPort() 'tls' => array('verify_peer' => false) )); - $server = new Server(function (RequestInterface $request) { + $server = new Server($loop, function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); @@ -169,7 +169,7 @@ public function testSecureHttpsReturnsData() $loop = Factory::create(); - $server = new Server(function (RequestInterface $request) { + $server = new Server($loop, function (RequestInterface $request) { return new Response( 200, array(), @@ -213,7 +213,7 @@ public function testSecureHttpsOnRandomPortWithoutHostHeaderUsesSocketUri() 'tls' => array('verify_peer' => false) )); - $server = new Server(function (RequestInterface $request) { + $server = new Server($loop, function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); @@ -247,7 +247,7 @@ public function testPlainHttpOnStandardPortReturnsUriWithNoPort() } $connector = new Connector($loop); - $server = new Server(function (RequestInterface $request) { + $server = new Server($loop, function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); @@ -277,7 +277,7 @@ public function testPlainHttpOnStandardPortWithoutHostHeaderReturnsUriWithNoPort } $connector = new Connector($loop); - $server = new Server(function (RequestInterface $request) { + $server = new Server($loop, function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); @@ -316,7 +316,7 @@ public function testSecureHttpsOnStandardPortReturnsUriWithNoPort() 'tls' => array('verify_peer' => false) )); - $server = new Server(function (RequestInterface $request) { + $server = new Server($loop, function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); @@ -355,7 +355,7 @@ public function testSecureHttpsOnStandardPortWithoutHostHeaderUsesSocketUri() 'tls' => array('verify_peer' => false) )); - $server = new Server(function (RequestInterface $request) { + $server = new Server($loop, function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); @@ -385,7 +385,7 @@ public function testPlainHttpOnHttpsStandardPortReturnsUriWithPort() } $connector = new Connector($loop); - $server = new Server(function (RequestInterface $request) { + $server = new Server($loop, function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); @@ -424,7 +424,7 @@ public function testSecureHttpsOnHttpStandardPortReturnsUriWithPort() 'tls' => array('verify_peer' => false) )); - $server = new Server(function (RequestInterface $request) { + $server = new Server($loop, function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri() . 'x' . $request->getHeaderLine('Host')); }); @@ -452,7 +452,7 @@ public function testClosedStreamFromRequestHandlerWillSendEmptyBody() $stream = new ThroughStream(); $stream->close(); - $server = new Server(function (RequestInterface $request) use ($stream) { + $server = new Server($loop, function (RequestInterface $request) use ($stream) { return new Response(200, array(), $stream); }); @@ -479,7 +479,7 @@ public function testRequestHandlerWithStreamingRequestWillReceiveCloseEventIfCon $connector = new Connector($loop); $once = $this->expectCallableOnce(); - $server = new Server(array( + $server = new Server($loop, array( new StreamingRequestMiddleware(), function (RequestInterface $request) use ($once) { $request->getBody()->on('close', $once); @@ -509,7 +509,7 @@ public function testStreamFromRequestHandlerWillBeClosedIfConnectionClosesWhileS $stream = new ThroughStream(); - $server = new Server(array( + $server = new Server($loop, array( new StreamingRequestMiddleware(), function (RequestInterface $request) use ($stream) { return new Response(200, array(), $stream); @@ -542,7 +542,7 @@ public function testStreamFromRequestHandlerWillBeClosedIfConnectionCloses() $stream = new ThroughStream(); - $server = new Server(function (RequestInterface $request) use ($stream) { + $server = new Server($loop, function (RequestInterface $request) use ($stream) { return new Response(200, array(), $stream); }); @@ -570,7 +570,7 @@ public function testUpgradeWithThroughStreamReturnsDataAsGiven() $loop = Factory::create(); $connector = new Connector($loop); - $server = new Server(function (RequestInterface $request) use ($loop) { + $server = new Server($loop, function (RequestInterface $request) use ($loop) { $stream = new ThroughStream(); $loop->addTimer(0.1, function () use ($stream) { @@ -607,7 +607,7 @@ public function testUpgradeWithRequestBodyAndThroughStreamReturnsDataAsGiven() $loop = Factory::create(); $connector = new Connector($loop); - $server = new Server(function (RequestInterface $request) use ($loop) { + $server = new Server($loop, function (RequestInterface $request) use ($loop) { $stream = new ThroughStream(); $loop->addTimer(0.1, function () use ($stream) { @@ -645,7 +645,7 @@ public function testConnectWithThroughStreamReturnsDataAsGiven() $loop = Factory::create(); $connector = new Connector($loop); - $server = new Server(function (RequestInterface $request) use ($loop) { + $server = new Server($loop, function (RequestInterface $request) use ($loop) { $stream = new ThroughStream(); $loop->addTimer(0.1, function () use ($stream) { @@ -682,7 +682,7 @@ public function testConnectWithThroughStreamReturnedFromPromiseReturnsDataAsGive $loop = Factory::create(); $connector = new Connector($loop); - $server = new Server(function (RequestInterface $request) use ($loop) { + $server = new Server($loop, function (RequestInterface $request) use ($loop) { $stream = new ThroughStream(); $loop->addTimer(0.1, function () use ($stream) { @@ -723,7 +723,7 @@ public function testConnectWithClosedThroughStreamReturnsNoData() $loop = Factory::create(); $connector = new Connector($loop); - $server = new Server(function (RequestInterface $request) { + $server = new Server($loop, function (RequestInterface $request) { $stream = new ThroughStream(); $stream->close(); @@ -757,7 +757,7 @@ public function testLimitConcurrentRequestsMiddlewareRequestStreamPausing() $loop = Factory::create(); $connector = new Connector($loop); - $server = new Server(array( + $server = new Server($loop, array( new LimitConcurrentRequestsMiddleware(5), new RequestBodyBufferMiddleware(16 * 1024 * 1024), // 16 MiB function (ServerRequestInterface $request, $next) use ($loop) { diff --git a/tests/Io/StreamingServerTest.php b/tests/Io/StreamingServerTest.php index 35396f79..3559c1d2 100644 --- a/tests/Io/StreamingServerTest.php +++ b/tests/Io/StreamingServerTest.php @@ -4,6 +4,7 @@ use Psr\Http\Message\ServerRequestInterface; use React\Http\Io\StreamingServer; +use React\EventLoop\Factory; use React\Http\Response; use React\Promise\Promise; use React\Stream\ThroughStream; @@ -46,7 +47,7 @@ public function setUpConnectionMockAndSocket() public function testRequestEventWillNotBeEmittedForIncompleteHeaders() { - $server = new StreamingServer($this->expectCallableNever()); + $server = new StreamingServer(Factory::create(), $this->expectCallableNever()); $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); @@ -58,7 +59,7 @@ public function testRequestEventWillNotBeEmittedForIncompleteHeaders() public function testRequestEventIsEmitted() { - $server = new StreamingServer($this->expectCallableOnce()); + $server = new StreamingServer(Factory::create(), $this->expectCallableOnce()); $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); @@ -73,7 +74,7 @@ public function testRequestEventIsEmitted() public function testRequestEventIsEmittedForArrayCallable() { $this->called = null; - $server = new StreamingServer(array($this, 'helperCallableOnce')); + $server = new StreamingServer(Factory::create(), array($this, 'helperCallableOnce')); $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); @@ -93,7 +94,7 @@ public function testRequestEvent() { $i = 0; $requestAssertion = null; - $server = new StreamingServer(function (ServerRequestInterface $request) use (&$i, &$requestAssertion) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$i, &$requestAssertion) { $i++; $requestAssertion = $request; }); @@ -126,7 +127,7 @@ public function testRequestEventWithSingleRequestHandlerArray() { $i = 0; $requestAssertion = null; - $server = new StreamingServer(array(function (ServerRequestInterface $request) use (&$i, &$requestAssertion) { + $server = new StreamingServer(Factory::create(), array(function (ServerRequestInterface $request) use (&$i, &$requestAssertion) { $i++; $requestAssertion = $request; })); @@ -158,7 +159,7 @@ public function testRequestEventWithSingleRequestHandlerArray() public function testRequestGetWithHostAndCustomPort() { $requestAssertion = null; - $server = new StreamingServer(function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; }); @@ -180,7 +181,7 @@ public function testRequestGetWithHostAndCustomPort() public function testRequestGetWithHostAndHttpsPort() { $requestAssertion = null; - $server = new StreamingServer(function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; }); @@ -202,7 +203,7 @@ public function testRequestGetWithHostAndHttpsPort() public function testRequestGetWithHostAndDefaultPortWillBeIgnored() { $requestAssertion = null; - $server = new StreamingServer(function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; }); @@ -224,7 +225,7 @@ public function testRequestGetWithHostAndDefaultPortWillBeIgnored() public function testRequestOptionsAsterisk() { $requestAssertion = null; - $server = new StreamingServer(function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; }); @@ -244,7 +245,7 @@ public function testRequestOptionsAsterisk() public function testRequestNonOptionsWithAsteriskRequestTargetWillReject() { - $server = new StreamingServer($this->expectCallableNever()); + $server = new StreamingServer(Factory::create(), $this->expectCallableNever()); $server->on('error', $this->expectCallableOnce()); $server->listen($this->socket); @@ -257,7 +258,7 @@ public function testRequestNonOptionsWithAsteriskRequestTargetWillReject() public function testRequestConnectAuthorityForm() { $requestAssertion = null; - $server = new StreamingServer(function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; }); @@ -279,7 +280,7 @@ public function testRequestConnectAuthorityForm() public function testRequestConnectWithoutHostWillBeAdded() { $requestAssertion = null; - $server = new StreamingServer(function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; }); @@ -301,7 +302,7 @@ public function testRequestConnectWithoutHostWillBeAdded() public function testRequestConnectAuthorityFormWithDefaultPortWillBeIgnored() { $requestAssertion = null; - $server = new StreamingServer(function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; }); @@ -323,7 +324,7 @@ public function testRequestConnectAuthorityFormWithDefaultPortWillBeIgnored() public function testRequestConnectAuthorityFormNonMatchingHostWillBeOverwritten() { $requestAssertion = null; - $server = new StreamingServer(function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; }); @@ -344,7 +345,7 @@ public function testRequestConnectAuthorityFormNonMatchingHostWillBeOverwritten( public function testRequestConnectOriginFormRequestTargetWillReject() { - $server = new StreamingServer($this->expectCallableNever()); + $server = new StreamingServer(Factory::create(), $this->expectCallableNever()); $server->on('error', $this->expectCallableOnce()); $server->listen($this->socket); @@ -356,7 +357,7 @@ public function testRequestConnectOriginFormRequestTargetWillReject() public function testRequestNonConnectWithAuthorityRequestTargetWillReject() { - $server = new StreamingServer($this->expectCallableNever()); + $server = new StreamingServer(Factory::create(), $this->expectCallableNever()); $server->on('error', $this->expectCallableOnce()); $server->listen($this->socket); @@ -370,7 +371,7 @@ public function testRequestWithoutHostEventUsesSocketAddress() { $requestAssertion = null; - $server = new StreamingServer(function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; }); @@ -396,7 +397,7 @@ public function testRequestAbsoluteEvent() { $requestAssertion = null; - $server = new StreamingServer(function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; }); @@ -418,7 +419,7 @@ public function testRequestAbsoluteAddsMissingHostEvent() { $requestAssertion = null; - $server = new StreamingServer(function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; }); @@ -440,7 +441,7 @@ public function testRequestAbsoluteNonMatchingHostWillBeOverwritten() { $requestAssertion = null; - $server = new StreamingServer(function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; }); @@ -462,7 +463,7 @@ public function testRequestOptionsAsteriskEvent() { $requestAssertion = null; - $server = new StreamingServer(function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; }); @@ -484,7 +485,7 @@ public function testRequestOptionsAbsoluteEvent() { $requestAssertion = null; - $server = new StreamingServer(function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; }); @@ -504,7 +505,7 @@ public function testRequestOptionsAbsoluteEvent() public function testRequestPauseWillBeForwardedToConnection() { - $server = new StreamingServer(function (ServerRequestInterface $request) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { $request->getBody()->pause(); }); @@ -524,7 +525,7 @@ public function testRequestPauseWillBeForwardedToConnection() public function testRequestResumeWillBeForwardedToConnection() { - $server = new StreamingServer(function (ServerRequestInterface $request) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { $request->getBody()->resume(); }); @@ -544,7 +545,7 @@ public function testRequestResumeWillBeForwardedToConnection() public function testRequestCloseWillNotCloseConnection() { - $server = new StreamingServer(function (ServerRequestInterface $request) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { $request->getBody()->close(); }); @@ -559,7 +560,7 @@ public function testRequestCloseWillNotCloseConnection() public function testRequestPauseAfterCloseWillNotBeForwarded() { - $server = new StreamingServer(function (ServerRequestInterface $request) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { $request->getBody()->close(); $request->getBody()->pause(); }); @@ -576,7 +577,7 @@ public function testRequestPauseAfterCloseWillNotBeForwarded() public function testRequestResumeAfterCloseWillNotBeForwarded() { - $server = new StreamingServer(function (ServerRequestInterface $request) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { $request->getBody()->close(); $request->getBody()->resume(); }); @@ -595,7 +596,7 @@ public function testRequestEventWithoutBodyWillNotEmitData() { $never = $this->expectCallableNever(); - $server = new StreamingServer(function (ServerRequestInterface $request) use ($never) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($never) { $request->getBody()->on('data', $never); }); @@ -610,7 +611,7 @@ public function testRequestEventWithSecondDataEventWillEmitBodyData() { $once = $this->expectCallableOnceWith('incomplete'); - $server = new StreamingServer(function (ServerRequestInterface $request) use ($once) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($once) { $request->getBody()->on('data', $once); }); @@ -630,7 +631,7 @@ public function testRequestEventWithPartialBodyWillEmitData() { $once = $this->expectCallableOnceWith('incomplete'); - $server = new StreamingServer(function (ServerRequestInterface $request) use ($once) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($once) { $request->getBody()->on('data', $once); }); @@ -651,7 +652,7 @@ public function testRequestEventWithPartialBodyWillEmitData() public function testResponseContainsPoweredByHeader() { - $server = new StreamingServer(function (ServerRequestInterface $request) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { return new Response(); }); @@ -681,7 +682,7 @@ public function testResponsePendingPromiseWillNotSendAnything() { $never = $this->expectCallableNever(); - $server = new StreamingServer(function (ServerRequestInterface $request) use ($never) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($never) { return new Promise(function () { }, $never); }); @@ -711,7 +712,7 @@ public function testResponsePendingPromiseWillBeCancelledIfConnectionCloses() { $once = $this->expectCallableOnce(); - $server = new StreamingServer(function (ServerRequestInterface $request) use ($once) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($once) { return new Promise(function () { }, $once); }); @@ -743,7 +744,7 @@ public function testRespomseBodyStreamAlreadyClosedWillSendEmptyBodyChunkedEncod $stream = new ThroughStream(); $stream->close(); - $server = new StreamingServer(function (ServerRequestInterface $request) use ($stream) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($stream) { return new Response( 200, array(), @@ -778,7 +779,7 @@ public function testResponseBodyStreamEndingWillSendEmptyBodyChunkedEncoded() { $stream = new ThroughStream(); - $server = new StreamingServer(function (ServerRequestInterface $request) use ($stream) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($stream) { return new Response( 200, array(), @@ -816,7 +817,7 @@ public function testResponseBodyStreamAlreadyClosedWillSendEmptyBodyPlainHttp10( $stream = new ThroughStream(); $stream->close(); - $server = new StreamingServer(function (ServerRequestInterface $request) use ($stream) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($stream) { return new Response( 200, array(), @@ -852,7 +853,7 @@ public function testResponseStreamWillBeClosedIfConnectionIsAlreadyClosed() $stream = new ThroughStream(); $stream->on('close', $this->expectCallableOnce()); - $server = new StreamingServer(function (ServerRequestInterface $request) use ($stream) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($stream) { return new Response( 200, array(), @@ -907,7 +908,7 @@ public function testResponseBodyStreamWillBeClosedIfConnectionEmitsCloseEvent() $stream = new ThroughStream(); $stream->on('close', $this->expectCallableOnce()); - $server = new StreamingServer(function (ServerRequestInterface $request) use ($stream) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($stream) { return new Response( 200, array(), @@ -925,7 +926,7 @@ public function testResponseBodyStreamWillBeClosedIfConnectionEmitsCloseEvent() public function testResponseUpgradeInResponseCanBeUsedToAdvertisePossibleUpgrade() { - $server = new StreamingServer(function (ServerRequestInterface $request) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { return new Response( 200, array( @@ -961,7 +962,7 @@ function ($data) use (&$buffer) { public function testResponseUpgradeWishInRequestCanBeIgnoredByReturningNormalResponse() { - $server = new StreamingServer(function (ServerRequestInterface $request) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { return new Response( 200, array( @@ -996,7 +997,7 @@ function ($data) use (&$buffer) { public function testResponseUpgradeSwitchingProtocolIncludesConnectionUpgradeHeaderWithoutContentLength() { - $server = new StreamingServer(function (ServerRequestInterface $request) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { return new Response( 101, array( @@ -1036,7 +1037,7 @@ public function testResponseUpgradeSwitchingProtocolWithStreamWillPipeDataToConn { $stream = new ThroughStream(); - $server = new StreamingServer(function (ServerRequestInterface $request) use ($stream) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($stream) { return new Response( 101, array( @@ -1077,7 +1078,7 @@ public function testResponseConnectMethodStreamWillPipeDataToConnection() { $stream = new ThroughStream(); - $server = new StreamingServer(function (ServerRequestInterface $request) use ($stream) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($stream) { return new Response( 200, array(), @@ -1115,7 +1116,7 @@ public function testResponseConnectMethodStreamWillPipeDataFromConnection() { $stream = new ThroughStream(); - $server = new StreamingServer(function (ServerRequestInterface $request) use ($stream) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($stream) { return new Response( 200, array(), @@ -1134,7 +1135,7 @@ public function testResponseConnectMethodStreamWillPipeDataFromConnection() public function testResponseContainsSameRequestProtocolVersionAndChunkedBodyForHttp11() { - $server = new StreamingServer(function (ServerRequestInterface $request) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { return new Response( 200, array(), @@ -1167,7 +1168,7 @@ function ($data) use (&$buffer) { public function testResponseContainsSameRequestProtocolVersionAndRawBodyForHttp10() { - $server = new StreamingServer(function (ServerRequestInterface $request) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { return new Response( 200, array(), @@ -1201,7 +1202,7 @@ function ($data) use (&$buffer) { public function testResponseContainsNoResponseBodyForHeadRequest() { - $server = new StreamingServer(function (ServerRequestInterface $request) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { return new Response( 200, array(), @@ -1233,7 +1234,7 @@ function ($data) use (&$buffer) { public function testResponseContainsNoResponseBodyAndNoContentLengthForNoContentStatus() { - $server = new StreamingServer(function (ServerRequestInterface $request) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { return new Response( 204, array(), @@ -1266,7 +1267,7 @@ function ($data) use (&$buffer) { public function testResponseContainsNoResponseBodyForNotModifiedStatus() { - $server = new StreamingServer(function (ServerRequestInterface $request) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { return new Response( 304, array(), @@ -1300,7 +1301,7 @@ function ($data) use (&$buffer) { public function testRequestInvalidHttpProtocolVersionWillEmitErrorAndSendErrorResponse() { $error = null; - $server = new StreamingServer($this->expectCallableNever()); + $server = new StreamingServer(Factory::create(), $this->expectCallableNever()); $server->on('error', function ($message) use (&$error) { $error = $message; }); @@ -1334,7 +1335,7 @@ function ($data) use (&$buffer) { public function testRequestOverflowWillEmitErrorAndSendErrorResponse() { $error = null; - $server = new StreamingServer($this->expectCallableNever()); + $server = new StreamingServer(Factory::create(), $this->expectCallableNever()); $server->on('error', function ($message) use (&$error) { $error = $message; }); @@ -1368,7 +1369,7 @@ function ($data) use (&$buffer) { public function testRequestInvalidWillEmitErrorAndSendErrorResponse() { $error = null; - $server = new StreamingServer($this->expectCallableNever()); + $server = new StreamingServer(Factory::create(), $this->expectCallableNever()); $server->on('error', function ($message) use (&$error) { $error = $message; }); @@ -1405,7 +1406,7 @@ public function testRequestContentLengthBodyDataWillEmitDataEventOnRequestStream $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new StreamingServer(function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1433,7 +1434,7 @@ public function testRequestChunkedTransferEncodingRequestWillEmitDecodedDataEven $errorEvent = $this->expectCallableNever(); $requestValidation = null; - $server = new StreamingServer(function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent, &$requestValidation) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent, &$requestValidation) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1464,7 +1465,7 @@ public function testRequestChunkedTransferEncodingWithAdditionalDataWontBeEmitte $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new StreamingServer(function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1493,7 +1494,7 @@ public function testRequestChunkedTransferEncodingEmpty() $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new StreamingServer(function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1521,7 +1522,7 @@ public function testRequestChunkedTransferEncodingHeaderCanBeUpperCase() $errorEvent = $this->expectCallableNever(); $requestValidation = null; - $server = new StreamingServer(function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent, &$requestValidation) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent, &$requestValidation) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1551,7 +1552,7 @@ public function testRequestChunkedTransferEncodingCanBeMixedUpperAndLowerCase() $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new StreamingServer(function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1578,7 +1579,7 @@ public function testRequestContentLengthWillEmitDataEventAndEndEventAndAdditiona $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new StreamingServer(function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1609,7 +1610,7 @@ public function testRequestContentLengthWillEmitDataEventAndEndEventAndAdditiona $errorEvent = $this->expectCallableNever(); - $server = new StreamingServer(function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1641,7 +1642,7 @@ public function testRequestZeroContentLengthWillEmitEndEvent() $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new StreamingServer(function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1667,7 +1668,7 @@ public function testRequestZeroContentLengthWillEmitEndAndAdditionalDataWillBeIg $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new StreamingServer(function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1694,7 +1695,7 @@ public function testRequestZeroContentLengthWillEmitEndAndAdditionalDataWillBeIg $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new StreamingServer(function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1720,7 +1721,7 @@ public function testRequestZeroContentLengthWillEmitEndAndAdditionalDataWillBeIg public function testRequestInvalidChunkHeaderTooLongWillEmitErrorOnRequestStream() { $errorEvent = $this->expectCallableOnceWith($this->isInstanceOf('Exception')); - $server = new StreamingServer(function ($request) use ($errorEvent){ + $server = new StreamingServer(Factory::create(), function ($request) use ($errorEvent){ $request->getBody()->on('error', $errorEvent); return \React\Promise\resolve(new Response()); }); @@ -1745,7 +1746,7 @@ public function testRequestInvalidChunkHeaderTooLongWillEmitErrorOnRequestStream public function testRequestInvalidChunkBodyTooLongWillEmitErrorOnRequestStream() { $errorEvent = $this->expectCallableOnceWith($this->isInstanceOf('Exception')); - $server = new StreamingServer(function ($request) use ($errorEvent){ + $server = new StreamingServer(Factory::create(), function ($request) use ($errorEvent){ $request->getBody()->on('error', $errorEvent); }); @@ -1767,7 +1768,7 @@ public function testRequestInvalidChunkBodyTooLongWillEmitErrorOnRequestStream() public function testRequestUnexpectedEndOfRequestWithChunkedTransferConnectionWillEmitErrorOnRequestStream() { $errorEvent = $this->expectCallableOnceWith($this->isInstanceOf('Exception')); - $server = new StreamingServer(function ($request) use ($errorEvent){ + $server = new StreamingServer(Factory::create(), function ($request) use ($errorEvent){ $request->getBody()->on('error', $errorEvent); }); @@ -1790,7 +1791,7 @@ public function testRequestUnexpectedEndOfRequestWithChunkedTransferConnectionWi public function testRequestInvalidChunkHeaderWillEmitErrorOnRequestStream() { $errorEvent = $this->expectCallableOnceWith($this->isInstanceOf('Exception')); - $server = new StreamingServer(function ($request) use ($errorEvent){ + $server = new StreamingServer(Factory::create(), function ($request) use ($errorEvent){ $request->getBody()->on('error', $errorEvent); }); @@ -1812,7 +1813,7 @@ public function testRequestInvalidChunkHeaderWillEmitErrorOnRequestStream() public function testRequestUnexpectedEndOfRequestWithContentLengthWillEmitErrorOnRequestStream() { $errorEvent = $this->expectCallableOnceWith($this->isInstanceOf('Exception')); - $server = new StreamingServer(function ($request) use ($errorEvent){ + $server = new StreamingServer(Factory::create(), function ($request) use ($errorEvent){ $request->getBody()->on('error', $errorEvent); }); @@ -1839,7 +1840,7 @@ public function testRequestWithoutBodyWillEmitEndOnRequestStream() $endEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new StreamingServer(function ($request) use ($dataEvent, $closeEvent, $endEvent, $errorEvent){ + $server = new StreamingServer(Factory::create(), function ($request) use ($dataEvent, $closeEvent, $endEvent, $errorEvent){ $request->getBody()->on('data', $dataEvent); $request->getBody()->on('close', $closeEvent); $request->getBody()->on('end', $endEvent); @@ -1863,7 +1864,7 @@ public function testRequestWithoutDefinedLengthWillIgnoreDataEvent() $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new StreamingServer(function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1882,7 +1883,7 @@ public function testRequestWithoutDefinedLengthWillIgnoreDataEvent() public function testResponseWithBodyStreamWillUseChunkedTransferEncodingByDefault() { $stream = new ThroughStream(); - $server = new StreamingServer(function (ServerRequestInterface $request) use ($stream) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($stream) { return new Response( 200, array(), @@ -1916,7 +1917,7 @@ function ($data) use (&$buffer) { public function testResponseWithBodyStringWillOverwriteExplicitContentLengthAndTransferEncoding() { - $server = new StreamingServer(function (ServerRequestInterface $request) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { return new Response( 200, array( @@ -1957,7 +1958,7 @@ public function testResponseContainsResponseBodyWithTransferEncodingChunkedForBo $body->expects($this->once())->method('getSize')->willReturn(null); $body->expects($this->once())->method('__toString')->willReturn('body'); - $server = new StreamingServer(function (ServerRequestInterface $request) use ($body) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($body) { return new Response( 200, array(), @@ -1994,7 +1995,7 @@ public function testResponseContainsResponseBodyWithPlainBodyWithUnknownSizeForL $body->expects($this->once())->method('getSize')->willReturn(null); $body->expects($this->once())->method('__toString')->willReturn('body'); - $server = new StreamingServer(function (ServerRequestInterface $request) use ($body) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($body) { return new Response( 200, array(), @@ -2028,7 +2029,7 @@ function ($data) use (&$buffer) { public function testResponseWithCustomTransferEncodingWillBeIgnoredAndUseChunkedTransferEncodingInstead() { $stream = new ThroughStream(); - $server = new StreamingServer(function (ServerRequestInterface $request) use ($stream) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($stream) { return new Response( 200, array( @@ -2065,7 +2066,7 @@ function ($data) use (&$buffer) { public function testResponseWithoutExplicitDateHeaderWillAddCurrentDate() { - $server = new StreamingServer(function (ServerRequestInterface $request) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { return new Response(); }); @@ -2095,7 +2096,7 @@ function ($data) use (&$buffer) { public function testResponseWIthCustomDateHeaderOverwritesDefault() { - $server = new StreamingServer(function (ServerRequestInterface $request) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { return new Response( 200, array("Date" => "Tue, 15 Nov 1994 08:12:31 GMT") @@ -2128,7 +2129,7 @@ function ($data) use (&$buffer) { public function testResponseWithEmptyDateHeaderRemovesDateHeader() { - $server = new StreamingServer(function (ServerRequestInterface $request) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { return new Response( 200, array('Date' => '') @@ -2161,7 +2162,7 @@ function ($data) use (&$buffer) { public function testResponseCanContainMultipleCookieHeaders() { - $server = new StreamingServer(function (ServerRequestInterface $request) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { return new Response( 200, array( @@ -2199,7 +2200,7 @@ function ($data) use (&$buffer) { public function testReponseWithExpectContinueRequestContainsContinueWithLaterResponse() { - $server = new StreamingServer(function (ServerRequestInterface $request) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { return new Response(); }); @@ -2231,7 +2232,7 @@ function ($data) use (&$buffer) { public function testResponseWithExpectContinueRequestWontSendContinueForHttp10() { - $server = new StreamingServer(function (ServerRequestInterface $request) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { return new Response(); }); @@ -2262,14 +2263,14 @@ function ($data) use (&$buffer) { public function testInvalidCallbackFunctionLeadsToException() { $this->setExpectedException('InvalidArgumentException'); - $server = new StreamingServer('invalid'); + $server = new StreamingServer(Factory::create(), 'invalid'); } public function testResponseBodyStreamWillStreamDataWithChunkedTransferEncoding() { $input = new ThroughStream(); - $server = new StreamingServer(function (ServerRequestInterface $request) use ($input) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($input) { return new Response( 200, array(), @@ -2308,7 +2309,7 @@ public function testResponseBodyStreamWithContentLengthWillStreamTillLengthWitho { $input = new ThroughStream(); - $server = new StreamingServer(function (ServerRequestInterface $request) use ($input) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($input) { return new Response( 200, array('Content-Length' => 5), @@ -2346,7 +2347,7 @@ function ($data) use (&$buffer) { public function testResponseWithResponsePromise() { - $server = new StreamingServer(function (ServerRequestInterface $request) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { return \React\Promise\resolve(new Response()); }); @@ -2374,7 +2375,7 @@ function ($data) use (&$buffer) { public function testResponseReturnInvalidTypeWillResultInError() { - $server = new StreamingServer(function (ServerRequestInterface $request) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { return "invalid"; }); @@ -2408,7 +2409,7 @@ function ($data) use (&$buffer) { public function testResponseResolveWrongTypeInPromiseWillResultInError() { - $server = new StreamingServer(function (ServerRequestInterface $request) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { return \React\Promise\resolve("invalid"); }); @@ -2436,7 +2437,7 @@ function ($data) use (&$buffer) { public function testResponseRejectedPromiseWillResultInErrorMessage() { - $server = new StreamingServer(function (ServerRequestInterface $request) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { return new Promise(function ($resolve, $reject) { $reject(new \Exception()); }); @@ -2467,7 +2468,7 @@ function ($data) use (&$buffer) { public function testResponseExceptionInCallbackWillResultInErrorMessage() { - $server = new StreamingServer(function (ServerRequestInterface $request) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { return new Promise(function ($resolve, $reject) { throw new \Exception('Bad call'); }); @@ -2498,7 +2499,7 @@ function ($data) use (&$buffer) { public function testResponseWithContentLengthHeaderForStringBodyOverwritesTransferEncoding() { - $server = new StreamingServer(function (ServerRequestInterface $request) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { return new Response( 200, array('Transfer-Encoding' => 'chunked'), @@ -2534,7 +2535,7 @@ function ($data) use (&$buffer) { public function testResponseWillBeHandled() { - $server = new StreamingServer(function (ServerRequestInterface $request) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { return new Response(); }); @@ -2562,7 +2563,7 @@ function ($data) use (&$buffer) { public function testResponseExceptionThrowInCallBackFunctionWillResultInErrorMessage() { - $server = new StreamingServer(function (ServerRequestInterface $request) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { throw new \Exception('hello'); }); @@ -2600,7 +2601,7 @@ function ($data) use (&$buffer) { */ public function testResponseThrowableThrowInCallBackFunctionWillResultInErrorMessage() { - $server = new StreamingServer(function (ServerRequestInterface $request) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { throw new \Error('hello'); }); @@ -2643,7 +2644,7 @@ function ($data) use (&$buffer) { public function testResponseRejectOfNonExceptionWillResultInErrorMessage() { - $server = new StreamingServer(function (ServerRequestInterface $request) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { return new Promise(function ($resolve, $reject) { $reject('Invalid type'); }); @@ -2680,7 +2681,7 @@ function ($data) use (&$buffer) { public function testRequestServerRequestParams() { $requestValidation = null; - $server = new StreamingServer(function (ServerRequestInterface $request) use (&$requestValidation) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestValidation) { $requestValidation = $request; }); @@ -2714,7 +2715,7 @@ public function testRequestServerRequestParams() public function testRequestQueryParametersWillBeAddedToRequest() { $requestValidation = null; - $server = new StreamingServer(function (ServerRequestInterface $request) use (&$requestValidation) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestValidation) { $requestValidation = $request; }); @@ -2734,7 +2735,7 @@ public function testRequestQueryParametersWillBeAddedToRequest() public function testRequestCookieWillBeAddedToServerRequest() { $requestValidation = null; - $server = new StreamingServer(function (ServerRequestInterface $request) use (&$requestValidation) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestValidation) { $requestValidation = $request; }); @@ -2755,7 +2756,7 @@ public function testRequestCookieWillBeAddedToServerRequest() public function testRequestInvalidMultipleCookiesWontBeAddedToServerRequest() { $requestValidation = null; - $server = new StreamingServer(function (ServerRequestInterface $request) use (&$requestValidation) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestValidation) { $requestValidation = $request; }); @@ -2776,7 +2777,7 @@ public function testRequestInvalidMultipleCookiesWontBeAddedToServerRequest() public function testRequestCookieWithSeparatorWillBeAddedToServerRequest() { $requestValidation = null; - $server = new StreamingServer(function (ServerRequestInterface $request) use (&$requestValidation) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestValidation) { $requestValidation = $request; }); @@ -2795,7 +2796,7 @@ public function testRequestCookieWithSeparatorWillBeAddedToServerRequest() public function testRequestCookieWithCommaValueWillBeAddedToServerRequest() { $requestValidation = null; - $server = new StreamingServer(function (ServerRequestInterface $request) use (&$requestValidation) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestValidation) { $requestValidation = $request; }); diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 02845769..8d8cac5d 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -48,13 +48,13 @@ public function setUpConnectionMockAndSocket() public function testInvalidCallbackFunctionLeadsToException() { $this->setExpectedException('InvalidArgumentException'); - new Server('invalid'); + new Server(Factory::create(), 'invalid'); } public function testSimpleRequestCallsRequestHandlerOnce() { $called = null; - $server = new Server(function (ServerRequestInterface $request) use (&$called) { + $server = new Server(Factory::create(), function (ServerRequestInterface $request) use (&$called) { ++$called; }); @@ -71,7 +71,7 @@ public function testSimpleRequestCallsRequestHandlerOnce() public function testSimpleRequestCallsArrayRequestHandlerOnce() { $this->called = null; - $server = new Server(array($this, 'helperCallableOnce')); + $server = new Server(Factory::create(), array($this, 'helperCallableOnce')); $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); @@ -88,7 +88,7 @@ public function helperCallableOnce() public function testSimpleRequestWithMiddlewareArrayProcessesMiddlewareStack() { $called = null; - $server = new Server(array( + $server = new Server(Factory::create(), array( function (ServerRequestInterface $request, $next) use (&$called) { $called = 'before'; $ret = $next($request->withHeader('Demo', 'ok')); @@ -112,7 +112,7 @@ public function testPostFileUpload() { $loop = Factory::create(); $deferred = new Deferred(); - $server = new Server(function (ServerRequestInterface $request) use ($deferred) { + $server = new Server($loop, function (ServerRequestInterface $request) use ($deferred) { $deferred->resolve($request); }); @@ -147,7 +147,7 @@ public function testPostFileUpload() public function testServerReceivesBufferedRequestByDefault() { $streaming = null; - $server = new Server(function (ServerRequestInterface $request) use (&$streaming) { + $server = new Server(Factory::create(), function (ServerRequestInterface $request) use (&$streaming) { $streaming = $request->getBody() instanceof ReadableStreamInterface; }); @@ -161,7 +161,7 @@ public function testServerReceivesBufferedRequestByDefault() public function testServerWithStreamingRequestMiddlewareReceivesStreamingRequest() { $streaming = null; - $server = new Server(array( + $server = new Server(Factory::create(), array( new StreamingRequestMiddleware(), function (ServerRequestInterface $request) use (&$streaming) { $streaming = $request->getBody() instanceof ReadableStreamInterface; @@ -179,7 +179,7 @@ public function testForwardErrors() { $exception = new \Exception(); $capturedException = null; - $server = new Server(function () use ($exception) { + $server = new Server(Factory::create(), function () use ($exception) { return Promise\reject($exception); }); $server->on('error', function ($error) use (&$capturedException) { @@ -251,7 +251,7 @@ public function provideIniSettingsForConcurrency() */ public function testServerConcurrency($memory_limit, $post_max_size, $expectedConcurrency) { - $server = new Server(function () { }); + $server = new Server(Factory::create(), function () { }); $ref = new \ReflectionMethod($server, 'getConcurrentRequestsLimit'); $ref->setAccessible(true); From 19b03ff6345998b9e90952834409500a7cbe7575 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Wed, 8 Jul 2020 20:10:37 +0200 Subject: [PATCH 323/456] Move `Response` class to `React\Http\Message\Response` --- README.md | 76 +++++++++++++------ examples/51-server-hello-world.php | 2 +- examples/52-server-count-visitors.php | 2 +- examples/53-server-whatsmyip.php | 2 +- examples/54-server-query-parameter.php | 2 +- examples/55-server-cookie-handling.php | 2 +- examples/56-server-sleep.php | 2 +- examples/57-server-error-handling.php | 2 +- examples/58-server-stream-response.php | 2 +- examples/59-server-json-api.php | 2 +- examples/61-server-hello-world-https.php | 2 +- examples/62-server-form-upload.php | 2 +- examples/63-server-streaming-request.php | 4 +- examples/71-server-http-proxy.php | 2 +- examples/72-server-http-connect-proxy.php | 2 +- examples/81-server-upgrade-echo.php | 2 +- examples/82-server-upgrade-chat.php | 2 +- examples/99-server-benchmark-download.php | 2 +- src/Io/StreamingServer.php | 5 +- src/Message/Response.php | 56 ++++++++++++++ src/Response.php | 36 --------- src/Server.php | 6 +- tests/FunctionalBrowserTest.php | 2 +- tests/FunctionalServerTest.php | 2 +- tests/Io/StreamingServerTest.php | 2 +- tests/{ => Message}/ResponseTest.php | 5 +- .../LimitConcurrentRequestsMiddlewareTest.php | 2 +- .../RequestBodyBufferMiddlewareTest.php | 2 +- .../StreamingRequestMiddlewareTest.php | 2 +- tests/benchmark-middleware-runner.php | 2 +- 30 files changed, 143 insertions(+), 91 deletions(-) create mode 100644 src/Message/Response.php delete mode 100644 src/Response.php rename tests/{ => Message}/ResponseTest.php (83%) diff --git a/README.md b/README.md index afdaeae1..73539d7a 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ multiple concurrent HTTP requests without blocking. * [Request method](#request-method) * [Cookie parameters](#cookie-parameters) * [Invalid request](#invalid-request) - * [Response](#response) + * [Server Response](#server-response) * [Deferred response](#deferred-response) * [Streaming outgoing response](#streaming-outgoing-response) * [Response length](#response-length) @@ -68,6 +68,8 @@ multiple concurrent HTTP requests without blocking. * [withBase()](#withbase) * [withProtocolVersion()](#withprotocolversion) * [withResponseBuffer()](#withresponsebuffer) + * [React\Http\Message](#reacthttpmessage) + * [Response](#response) * [React\Http\Middleware](#reacthttpmiddleware) * [StreamingRequestMiddleware](#streamingrequestmiddleware) * [LimitConcurrentRequestsMiddleware](#limitconcurrentrequestsmiddleware) @@ -102,7 +104,7 @@ This is an HTTP server which responds with `Hello World!` to every request. $loop = React\EventLoop\Factory::create(); $server = new React\Http\Server($loop, function (Psr\Http\Message\ServerRequestInterface $request) { - return new React\Http\Response( + return new React\Http\Message\Response( 200, array( 'Content-Type' => 'text/plain' @@ -711,11 +713,11 @@ processing each incoming HTTP request. When a complete HTTP request has been received, it will invoke the given request handler function. This request handler function needs to be passed to the constructor and will be invoked with the respective [request](#server-request) -object and expects a [response](#response) object in return: +object and expects a [response](#server-response) object in return: ```php $server = new React\Http\Server($loop, function (Psr\Http\Message\ServerRequestInterface $request) { - return new React\Http\Response( + return new React\Http\Message\Response( 200, array( 'Content-Type' => 'text/plain' @@ -731,7 +733,7 @@ see also following [request](#server-request) chapter for more details. Each outgoing HTTP response message is always represented by the [PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface), -see also following [response](#response) chapter for more details. +see also following [response](#server-response) chapter for more details. In order to start listening for any incoming connections, the `Server` needs to be attached to an instance of @@ -1155,7 +1157,7 @@ $server = new React\Http\Server($loop, array( }); $body->on('end', function () use ($resolve, &$bytes){ - $resolve(new React\Http\Response( + $resolve(new React\Http\Message\Response( 200, array( 'Content-Type' => 'text/plain' @@ -1166,7 +1168,7 @@ $server = new React\Http\Server($loop, array( // an error occures e.g. on invalid chunked encoded data or an unexpected 'end' event $body->on('error', function (\Exception $exception) use ($resolve, &$bytes) { - $resolve(new React\Http\Response( + $resolve(new React\Http\Message\Response( 400, array( 'Content-Type' => 'text/plain' @@ -1222,7 +1224,7 @@ $server = new React\Http\Server($loop, array( $body = 'The request does not contain an explicit length.'; $body .= 'This example does not accept chunked transfer encoding.'; - return new React\Http\Response( + return new React\Http\Message\Response( 411, array( 'Content-Type' => 'text/plain' @@ -1231,7 +1233,7 @@ $server = new React\Http\Server($loop, array( ); } - return new React\Http\Response( + return new React\Http\Message\Response( 200, array( 'Content-Type' => 'text/plain' @@ -1342,25 +1344,24 @@ Note that the server will also emit an `error` event if you do not return a valid response object from your request handler function. See also [invalid response](#invalid-response) for more details. -### Response +### Server Response The callback function passed to the constructor of the [`Server`](#server) is responsible for processing the request and returning a response, which will be -delivered to the client. This function MUST return an instance implementing -[PSR-7 ResponseInterface](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-7-http-message.md#33-psrhttpmessageresponseinterface) +delivered to the client. + +This function MUST return an instance implementing +[PSR-7 `ResponseInterface`](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-7-http-message.md#33-psrhttpmessageresponseinterface) object or a -[ReactPHP Promise](https://github.com/reactphp/promise#reactpromise) -which will resolve a `PSR-7 ResponseInterface` object. +[ReactPHP Promise](https://github.com/reactphp/promise) +which resolves with a PSR-7 `ResponseInterface` object. -You will find a `Response` class -which implements the `PSR-7 ResponseInterface` in this project. -We use instantiation of this class in our projects, -but feel free to use any implemantation of the -`PSR-7 ResponseInterface` you prefer. +This projects ships a [`Response` class](#response) which implements the PSR-7 +`ResponseInterface`. In its most simple form, you can use it like this: ```php -$server = new Server($loop, function (ServerRequestInterface $request) { - return new Response( +$server = new React\Http\Server($loop, function (ServerRequestInterface $request) { + return new React\Http\Message\Response( 200, array( 'Content-Type' => 'text/plain' @@ -1370,6 +1371,10 @@ $server = new Server($loop, function (ServerRequestInterface $request) { }); ``` +We use this [`Response` class](#response) throughout our project examples, but +feel free to use any other implementation of the PSR-7 `ResponseInterface`. +See also the [`Response` class](#response) for more details. + #### Deferred response The example above returns the response directly, because it needs @@ -2324,6 +2329,33 @@ Notice that the [`Browser`](#browser) is an immutable object, i.e. this method actually returns a *new* [`Browser`](#browser) instance with the given setting applied. +### React\Http\Message + +#### Response + +The `Response` class can be used to +represent an outgoing server response message. + +```php +$response = new React\Http\Message\Response( + 200, + array( + 'Content-Type' => 'text/html' + ), + "Hello world!\n" +); +``` + +This class implements the +[PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface) +which in turn extends the +[PSR-7 `MessageInterface`](https://www.php-fig.org/psr/psr-7/#31-psrhttpmessagemessageinterface). + +> Internally, this class extends the underlying `\RingCentral\Psr7\Response` + class. The only difference is that this class will accept implemenations + of ReactPHPs `ReadableStreamInterface` for the `$body` argument. This base + class is considered an implementation detail that may change in the future. + ### React\Http\Middleware #### StreamingRequestMiddleware @@ -2350,7 +2382,7 @@ $server = new React\Http\Server(array( $bytes += \count($chunk); }); $body->on('close', function () use (&$bytes, $resolve) { - $resolve(new React\Http\Response( + $resolve(new React\Http\Message\Response( 200, [], "Received $bytes bytes\n" diff --git a/examples/51-server-hello-world.php b/examples/51-server-hello-world.php index c0ad6741..f6903cff 100644 --- a/examples/51-server-hello-world.php +++ b/examples/51-server-hello-world.php @@ -2,7 +2,7 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Factory; -use React\Http\Response; +use React\Http\Message\Response; use React\Http\Server; require __DIR__ . '/../vendor/autoload.php'; diff --git a/examples/52-server-count-visitors.php b/examples/52-server-count-visitors.php index 1fa051f3..2b8e897c 100644 --- a/examples/52-server-count-visitors.php +++ b/examples/52-server-count-visitors.php @@ -2,7 +2,7 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Factory; -use React\Http\Response; +use React\Http\Message\Response; use React\Http\Server; require __DIR__ . '/../vendor/autoload.php'; diff --git a/examples/53-server-whatsmyip.php b/examples/53-server-whatsmyip.php index 512334eb..18f7504e 100644 --- a/examples/53-server-whatsmyip.php +++ b/examples/53-server-whatsmyip.php @@ -2,7 +2,7 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Factory; -use React\Http\Response; +use React\Http\Message\Response; use React\Http\Server; require __DIR__ . '/../vendor/autoload.php'; diff --git a/examples/54-server-query-parameter.php b/examples/54-server-query-parameter.php index aaee50e6..2786f380 100644 --- a/examples/54-server-query-parameter.php +++ b/examples/54-server-query-parameter.php @@ -2,7 +2,7 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Factory; -use React\Http\Response; +use React\Http\Message\Response; use React\Http\Server; require __DIR__ . '/../vendor/autoload.php'; diff --git a/examples/55-server-cookie-handling.php b/examples/55-server-cookie-handling.php index d96d4a85..6faf6be7 100644 --- a/examples/55-server-cookie-handling.php +++ b/examples/55-server-cookie-handling.php @@ -2,7 +2,7 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Factory; -use React\Http\Response; +use React\Http\Message\Response; use React\Http\Server; require __DIR__ . '/../vendor/autoload.php'; diff --git a/examples/56-server-sleep.php b/examples/56-server-sleep.php index 141db6ea..3da6963b 100644 --- a/examples/56-server-sleep.php +++ b/examples/56-server-sleep.php @@ -2,7 +2,7 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Factory; -use React\Http\Response; +use React\Http\Message\Response; use React\Http\Server; use React\Promise\Promise; diff --git a/examples/57-server-error-handling.php b/examples/57-server-error-handling.php index 625fda17..c8e99ee4 100644 --- a/examples/57-server-error-handling.php +++ b/examples/57-server-error-handling.php @@ -2,7 +2,7 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Factory; -use React\Http\Response; +use React\Http\Message\Response; use React\Http\Server; use React\Promise\Promise; diff --git a/examples/58-server-stream-response.php b/examples/58-server-stream-response.php index d246a237..518c2cb4 100644 --- a/examples/58-server-stream-response.php +++ b/examples/58-server-stream-response.php @@ -2,7 +2,7 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Factory; -use React\Http\Response; +use React\Http\Message\Response; use React\Http\Server; use React\Stream\ThroughStream; diff --git a/examples/59-server-json-api.php b/examples/59-server-json-api.php index 79f87db4..8602a889 100644 --- a/examples/59-server-json-api.php +++ b/examples/59-server-json-api.php @@ -8,7 +8,7 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Factory; -use React\Http\Response; +use React\Http\Message\Response; use React\Http\Server; require __DIR__ . '/../vendor/autoload.php'; diff --git a/examples/61-server-hello-world-https.php b/examples/61-server-hello-world-https.php index 19eff50b..dfe3e941 100644 --- a/examples/61-server-hello-world-https.php +++ b/examples/61-server-hello-world-https.php @@ -2,7 +2,7 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Factory; -use React\Http\Response; +use React\Http\Message\Response; use React\Http\Server; require __DIR__ . '/../vendor/autoload.php'; diff --git a/examples/62-server-form-upload.php b/examples/62-server-form-upload.php index 4db8c5d7..c997a465 100644 --- a/examples/62-server-form-upload.php +++ b/examples/62-server-form-upload.php @@ -10,11 +10,11 @@ use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\UploadedFileInterface; use React\EventLoop\Factory; +use React\Http\Message\Response; use React\Http\Middleware\LimitConcurrentRequestsMiddleware; use React\Http\Middleware\RequestBodyBufferMiddleware; use React\Http\Middleware\RequestBodyParserMiddleware; use React\Http\Middleware\StreamingRequestMiddleware; -use React\Http\Response; use React\Http\Server; require __DIR__ . '/../vendor/autoload.php'; diff --git a/examples/63-server-streaming-request.php b/examples/63-server-streaming-request.php index 6b9c8cec..9c1a9758 100644 --- a/examples/63-server-streaming-request.php +++ b/examples/63-server-streaming-request.php @@ -23,7 +23,7 @@ function (Psr\Http\Message\ServerRequestInterface $request) { }); $body->on('end', function () use ($resolve, &$bytes){ - $resolve(new React\Http\Response( + $resolve(new React\Http\Message\Response( 200, array( 'Content-Type' => 'text/plain' @@ -34,7 +34,7 @@ function (Psr\Http\Message\ServerRequestInterface $request) { // an error occures e.g. on invalid chunked encoded data or an unexpected 'end' event $body->on('error', function (\Exception $exception) use ($resolve, &$bytes) { - $resolve(new React\Http\Response( + $resolve(new React\Http\Message\Response( 400, array( 'Content-Type' => 'text/plain' diff --git a/examples/71-server-http-proxy.php b/examples/71-server-http-proxy.php index 5b829a73..b959b7bf 100644 --- a/examples/71-server-http-proxy.php +++ b/examples/71-server-http-proxy.php @@ -5,7 +5,7 @@ use Psr\Http\Message\RequestInterface; use React\EventLoop\Factory; -use React\Http\Response; +use React\Http\Message\Response; use React\Http\Server; use RingCentral\Psr7; diff --git a/examples/72-server-http-connect-proxy.php b/examples/72-server-http-connect-proxy.php index 6a0364e8..e786da76 100644 --- a/examples/72-server-http-connect-proxy.php +++ b/examples/72-server-http-connect-proxy.php @@ -5,7 +5,7 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Factory; -use React\Http\Response; +use React\Http\Message\Response; use React\Http\Server; use React\Socket\Connector; use React\Socket\ConnectionInterface; diff --git a/examples/81-server-upgrade-echo.php b/examples/81-server-upgrade-echo.php index 6ae2ce8e..34e85f6c 100644 --- a/examples/81-server-upgrade-echo.php +++ b/examples/81-server-upgrade-echo.php @@ -19,7 +19,7 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Factory; -use React\Http\Response; +use React\Http\Message\Response; use React\Http\Server; use React\Stream\ThroughStream; diff --git a/examples/82-server-upgrade-chat.php b/examples/82-server-upgrade-chat.php index ee4ce146..5e49ce37 100644 --- a/examples/82-server-upgrade-chat.php +++ b/examples/82-server-upgrade-chat.php @@ -21,7 +21,7 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Factory; -use React\Http\Response; +use React\Http\Message\Response; use React\Http\Server; use React\Stream\CompositeStream; use React\Stream\ThroughStream; diff --git a/examples/99-server-benchmark-download.php b/examples/99-server-benchmark-download.php index bd4acde6..536f4515 100644 --- a/examples/99-server-benchmark-download.php +++ b/examples/99-server-benchmark-download.php @@ -10,7 +10,7 @@ use Evenement\EventEmitter; use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Factory; -use React\Http\Response; +use React\Http\Message\Response; use React\Http\Server; use React\Stream\ReadableStreamInterface; use React\Stream\WritableStreamInterface; diff --git a/src/Io/StreamingServer.php b/src/Io/StreamingServer.php index 4c20e39a..e2044d2c 100644 --- a/src/Io/StreamingServer.php +++ b/src/Io/StreamingServer.php @@ -6,7 +6,7 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\LoopInterface; -use React\Http\Response; +use React\Http\Message\Response; use React\Promise; use React\Promise\CancellablePromiseInterface; use React\Promise\PromiseInterface; @@ -75,8 +75,7 @@ * [streaming request](#streaming-request) below for more details. * * @see \React\Http\Server - * @see Request - * @see Response + * @see \React\Http\Message\Response * @see self::listen() * @internal */ diff --git a/src/Message/Response.php b/src/Message/Response.php new file mode 100644 index 00000000..a4aabe38 --- /dev/null +++ b/src/Message/Response.php @@ -0,0 +1,56 @@ + 'text/html' + * ), + * "Hello world!\n" + * ); + * ``` + * + * This class implements the + * [PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface) + * which in turn extends the + * [PSR-7 `MessageInterface`](https://www.php-fig.org/psr/psr-7/#31-psrhttpmessagemessageinterface). + * + * > Internally, this class extends the underlying `\RingCentral\Psr7\Response` + * class. The only difference is that this class will accept implemenations + * of ReactPHPs `ReadableStreamInterface` for the `$body` argument. This base + * class is considered an implementation detail that may change in the future. + * + * @see \Psr\Http\Message\ResponseInterface + */ +class Response extends Psr7Response +{ + public function __construct( + $status = 200, + array $headers = array(), + $body = null, + $version = '1.1', + $reason = null + ) { + if ($body instanceof ReadableStreamInterface) { + $body = new HttpBodyStream($body, null); + } + + parent::__construct( + $status, + $headers, + $body, + $version, + $reason + ); + } +} diff --git a/src/Response.php b/src/Response.php deleted file mode 100644 index 0964dac9..00000000 --- a/src/Response.php +++ /dev/null @@ -1,36 +0,0 @@ - 'text/plain' @@ -39,7 +39,7 @@ * * Each outgoing HTTP response message is always represented by the * [PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface), - * see also following [response](#response) chapter for more details. + * see also following [response](#server-response) chapter for more details. * * In order to start listening for any incoming connections, the `Server` needs * to be attached to an instance of diff --git a/tests/FunctionalBrowserTest.php b/tests/FunctionalBrowserTest.php index de5a9f9a..6225b2f1 100644 --- a/tests/FunctionalBrowserTest.php +++ b/tests/FunctionalBrowserTest.php @@ -9,7 +9,7 @@ use React\Http\Browser; use React\Http\Message\ResponseException; use React\Http\Middleware\StreamingRequestMiddleware; -use React\Http\Response; +use React\Http\Message\Response; use React\Http\Server; use React\Promise\Promise; use React\Promise\Stream; diff --git a/tests/FunctionalServerTest.php b/tests/FunctionalServerTest.php index f92fb2b0..a57a5dce 100644 --- a/tests/FunctionalServerTest.php +++ b/tests/FunctionalServerTest.php @@ -5,7 +5,7 @@ use Psr\Http\Message\ServerRequestInterface; use React\Http\Middleware\LimitConcurrentRequestsMiddleware; use React\Http\Middleware\RequestBodyBufferMiddleware; -use React\Http\Response; +use React\Http\Message\Response; use React\Http\Server; use React\Socket\Server as Socket; use React\EventLoop\Factory; diff --git a/tests/Io/StreamingServerTest.php b/tests/Io/StreamingServerTest.php index 3559c1d2..6df6cf79 100644 --- a/tests/Io/StreamingServerTest.php +++ b/tests/Io/StreamingServerTest.php @@ -5,7 +5,7 @@ use Psr\Http\Message\ServerRequestInterface; use React\Http\Io\StreamingServer; use React\EventLoop\Factory; -use React\Http\Response; +use React\Http\Message\Response; use React\Promise\Promise; use React\Stream\ThroughStream; use React\Tests\Http\SocketServerStub; diff --git a/tests/ResponseTest.php b/tests/Message/ResponseTest.php similarity index 83% rename from tests/ResponseTest.php rename to tests/Message/ResponseTest.php index 146278f1..13646bd8 100644 --- a/tests/ResponseTest.php +++ b/tests/Message/ResponseTest.php @@ -1,9 +1,10 @@ Date: Wed, 8 Jul 2020 22:16:10 +0200 Subject: [PATCH 324/456] Validate `Response` body type --- src/Message/Response.php | 14 ++++++++++-- tests/Message/ResponseTest.php | 40 +++++++++++++++++++++++++++++----- 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/src/Message/Response.php b/src/Message/Response.php index a4aabe38..45d007ee 100644 --- a/src/Message/Response.php +++ b/src/Message/Response.php @@ -34,15 +34,25 @@ */ class Response extends Psr7Response { + /** + * @param int $status HTTP status code (e.g. 200/404) + * @param array $headers additional response headers + * @param string|ReadableStreamInterface|StreamInterface $body response body + * @param string $version HTTP protocol version (e.g. 1.1/1.0) + * @param ?string $reason custom HTTP response phrase + * @throws \InvalidArgumentException for an invalid body + */ public function __construct( $status = 200, array $headers = array(), - $body = null, + $body = '', $version = '1.1', $reason = null ) { - if ($body instanceof ReadableStreamInterface) { + if ($body instanceof ReadableStreamInterface && !$body instanceof StreamInterface) { $body = new HttpBodyStream($body, null); + } elseif (!\is_string($body) && !$body instanceof StreamInterface) { + throw new \InvalidArgumentException('Invalid response body given'); } parent::__construct( diff --git a/tests/Message/ResponseTest.php b/tests/Message/ResponseTest.php index 13646bd8..457618e9 100644 --- a/tests/Message/ResponseTest.php +++ b/tests/Message/ResponseTest.php @@ -2,21 +2,51 @@ namespace React\Tests\Http\Message; +use React\Http\Io\HttpBodyStream; use React\Http\Message\Response; use React\Stream\ThroughStream; use React\Tests\Http\TestCase; class ResponseTest extends TestCase { - public function testResponseBodyWillBeHttpBodyStream() - { - $response = new Response(200, array(), new ThroughStream()); - $this->assertInstanceOf('React\Http\Io\HttpBodyStream', $response->getBody()); - } public function testStringBodyWillBePsr7Stream() { $response = new Response(200, array(), 'hello'); $this->assertInstanceOf('RingCentral\Psr7\Stream', $response->getBody()); } + + public function testConstructWithStreamingBodyWillReturnReadableBodyStream() + { + $response = new Response(200, array(), new ThroughStream()); + + $body = $response->getBody(); + $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $body); + $this->assertInstanceof('React\Stream\ReadableStreamInterface', $body); + $this->assertInstanceOf('React\Http\Io\HttpBodyStream', $body); + $this->assertNull($body->getSize()); + } + + public function testConstructWithHttpBodyStreamReturnsBodyAsIs() + { + $response = new Response( + 200, + array(), + $body = new HttpBodyStream(new ThroughStream(), 100) + ); + + $this->assertSame($body, $response->getBody()); + } + + public function testFloatBodyWillThrow() + { + $this->setExpectedException('InvalidArgumentException'); + new Response(200, array(), 1.0); + } + + public function testResourceBodyWillThrow() + { + $this->setExpectedException('InvalidArgumentException'); + new Response(200, array(), tmpfile()); + } } From 76fb422e841c372a97262c04549e717ca385ad67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Wed, 8 Jul 2020 22:28:21 +0200 Subject: [PATCH 325/456] Move `ServerRequest` class to `React\Http\Message\ServerRequest` --- README.md | 21 ++++++++++++++++++ src/Io/RequestHeaderParser.php | 1 + src/Io/StreamingServer.php | 1 + src/{Io => Message}/ServerRequest.php | 22 ++++++++++++------- tests/Io/MiddlewareRunnerTest.php | 8 +++---- tests/Io/MultipartParserTest.php | 2 +- tests/Io/StreamingServerTest.php | 2 +- tests/{Io => Message}/ServerRequestTest.php | 4 ++-- .../LimitConcurrentRequestsMiddlewareTest.php | 6 ++--- .../RequestBodyBufferMiddlewareTest.php | 4 ++-- .../RequestBodyParserMiddlewareTest.php | 2 +- .../StreamingRequestMiddlewareTest.php | 2 +- tests/benchmark-middleware-runner.php | 4 ++-- 13 files changed, 54 insertions(+), 25 deletions(-) rename src/{Io => Message}/ServerRequest.php (81%) rename tests/{Io => Message}/ServerRequestTest.php (99%) diff --git a/README.md b/README.md index 73539d7a..452b94b4 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,7 @@ multiple concurrent HTTP requests without blocking. * [withResponseBuffer()](#withresponsebuffer) * [React\Http\Message](#reacthttpmessage) * [Response](#response) + * [ServerRequest](#serverrequest) * [React\Http\Middleware](#reacthttpmiddleware) * [StreamingRequestMiddleware](#streamingrequestmiddleware) * [LimitConcurrentRequestsMiddleware](#limitconcurrentrequestsmiddleware) @@ -2356,6 +2357,26 @@ which in turn extends the of ReactPHPs `ReadableStreamInterface` for the `$body` argument. This base class is considered an implementation detail that may change in the future. +#### ServerRequest + +The `ServerRequest` class can be used to +respresent an incoming server request message. + +This class implements the +[PSR-7 `ServerRequestInterface`](https://www.php-fig.org/psr/psr-7/#321-psrhttpmessageserverrequestinterface) +which extends the +[PSR-7 `RequestInterface`](https://www.php-fig.org/psr/psr-7/#32-psrhttpmessagerequestinterface) +which in turn extends the +[PSR-7 `MessageInterface`](https://www.php-fig.org/psr/psr-7/#31-psrhttpmessagemessageinterface). + +This is mostly used internally to represent each incoming request message. +Likewise, you can also use this class in test cases to test how your web +application reacts to certain HTTP requests. + +> Internally, this implementation builds on top of an existing outgoing + request message and only adds required server methods. This base class is + considered an implementation detail that may change in the future. + ### React\Http\Middleware #### StreamingRequestMiddleware diff --git a/src/Io/RequestHeaderParser.php b/src/Io/RequestHeaderParser.php index f7f77e7e..d26d6548 100644 --- a/src/Io/RequestHeaderParser.php +++ b/src/Io/RequestHeaderParser.php @@ -4,6 +4,7 @@ use Evenement\EventEmitter; use Psr\Http\Message\ServerRequestInterface; +use React\Http\Message\ServerRequest; use React\Socket\ConnectionInterface; use Exception; diff --git a/src/Io/StreamingServer.php b/src/Io/StreamingServer.php index e2044d2c..8b104597 100644 --- a/src/Io/StreamingServer.php +++ b/src/Io/StreamingServer.php @@ -7,6 +7,7 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\LoopInterface; use React\Http\Message\Response; +use React\Http\Message\ServerRequest; use React\Promise; use React\Promise\CancellablePromiseInterface; use React\Promise\PromiseInterface; diff --git a/src/Io/ServerRequest.php b/src/Message/ServerRequest.php similarity index 81% rename from src/Io/ServerRequest.php rename to src/Message/ServerRequest.php index 28a8c5db..71d10358 100644 --- a/src/Io/ServerRequest.php +++ b/src/Message/ServerRequest.php @@ -1,6 +1,6 @@ Internally, this implementation builds on top of an existing outgoing + * request message and only adds required server methods. This base class is + * considered an implementation detail that may change in the future. * * @see ServerRequestInterface - * @internal */ class ServerRequest extends Request implements ServerRequestInterface { diff --git a/tests/Io/MiddlewareRunnerTest.php b/tests/Io/MiddlewareRunnerTest.php index f43231b0..eda61012 100644 --- a/tests/Io/MiddlewareRunnerTest.php +++ b/tests/Io/MiddlewareRunnerTest.php @@ -3,18 +3,18 @@ namespace React\Tests\Http\Io; use Clue\React\Block; +use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Factory; use React\Http\Io\MiddlewareRunner; -use React\Http\Io\ServerRequest; +use React\Http\Message\ServerRequest; use React\Promise; +use React\Promise\CancellablePromiseInterface; +use React\Promise\PromiseInterface; use React\Tests\Http\Middleware\ProcessStack; use React\Tests\Http\TestCase; use RingCentral\Psr7\Response; -use Psr\Http\Message\RequestInterface; -use React\Promise\CancellablePromiseInterface; -use React\Promise\PromiseInterface; final class MiddlewareRunnerTest extends TestCase { diff --git a/tests/Io/MultipartParserTest.php b/tests/Io/MultipartParserTest.php index 91ec832d..14550f57 100644 --- a/tests/Io/MultipartParserTest.php +++ b/tests/Io/MultipartParserTest.php @@ -3,7 +3,7 @@ namespace React\Tests\Http\Io\Middleware; use React\Http\Io\MultipartParser; -use React\Http\Io\ServerRequest; +use React\Http\Message\ServerRequest; use React\Tests\Http\TestCase; final class MultipartParserTest extends TestCase diff --git a/tests/Io/StreamingServerTest.php b/tests/Io/StreamingServerTest.php index 6df6cf79..e4e4a1f3 100644 --- a/tests/Io/StreamingServerTest.php +++ b/tests/Io/StreamingServerTest.php @@ -3,8 +3,8 @@ namespace React\Tests\Http\Io; use Psr\Http\Message\ServerRequestInterface; -use React\Http\Io\StreamingServer; use React\EventLoop\Factory; +use React\Http\Io\StreamingServer; use React\Http\Message\Response; use React\Promise\Promise; use React\Stream\ThroughStream; diff --git a/tests/Io/ServerRequestTest.php b/tests/Message/ServerRequestTest.php similarity index 99% rename from tests/Io/ServerRequestTest.php rename to tests/Message/ServerRequestTest.php index 47346cd1..4785486d 100644 --- a/tests/Io/ServerRequestTest.php +++ b/tests/Message/ServerRequestTest.php @@ -1,8 +1,8 @@ Date: Thu, 9 Jul 2020 11:11:55 +0200 Subject: [PATCH 326/456] Validate `ServerRequest` body type --- src/Io/RequestHeaderParser.php | 2 +- src/Message/ServerRequest.php | 45 ++++++++----- tests/Message/ServerRequestTest.php | 99 +++++++++++++++++++++++++++++ 3 files changed, 129 insertions(+), 17 deletions(-) diff --git a/src/Io/RequestHeaderParser.php b/src/Io/RequestHeaderParser.php index d26d6548..53f7ff09 100644 --- a/src/Io/RequestHeaderParser.php +++ b/src/Io/RequestHeaderParser.php @@ -224,7 +224,7 @@ public function parseRequest($headers, $remoteSocketUri, $localSocketUri) $start['method'], $uri, $fields, - null, + '', $start['version'], $serverParams ); diff --git a/src/Message/ServerRequest.php b/src/Message/ServerRequest.php index 71d10358..b0d64498 100644 --- a/src/Message/ServerRequest.php +++ b/src/Message/ServerRequest.php @@ -5,6 +5,8 @@ use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\StreamInterface; use Psr\Http\Message\UriInterface; +use React\Http\Io\HttpBodyStream; +use React\Stream\ReadableStreamInterface; use RingCentral\Psr7\Request; /** @@ -38,25 +40,40 @@ class ServerRequest extends Request implements ServerRequestInterface private $parsedBody; /** - * @param null|string $method HTTP method for the request. - * @param null|string|UriInterface $uri URI for the request. - * @param array $headers Headers for the message. - * @param string|resource|StreamInterface $body Message body. - * @param string $protocolVersion HTTP protocol version. - * @param array $serverParams server-side parameters - * - * @throws \InvalidArgumentException for an invalid URI + * @param string $method HTTP method for the request. + * @param string|UriInterface $url URL for the request. + * @param array $headers Headers for the message. + * @param string|ReadableStreamInterface|StreamInterface $body Message body. + * @param string $version HTTP protocol version. + * @param array $serverParams server-side parameters + * @throws \InvalidArgumentException for an invalid URL or body */ public function __construct( $method, - $uri, + $url, array $headers = array(), - $body = null, - $protocolVersion = '1.1', + $body = '', + $version = '1.1', $serverParams = array() ) { + $stream = null; + if ($body instanceof ReadableStreamInterface && !$body instanceof StreamInterface) { + $stream = $body; + $body = null; + } elseif (!\is_string($body) && !$body instanceof StreamInterface) { + throw new \InvalidArgumentException('Invalid server request body given'); + } + $this->serverParams = $serverParams; - parent::__construct($method, $uri, $headers, $body, $protocolVersion); + parent::__construct($method, $url, $headers, $body, $version); + + if ($stream !== null) { + $size = (int) $this->getHeaderLine('Content-Length'); + if (\strtolower($this->getHeaderLine('Transfer-Encoding')) === 'chunked') { + $size = null; + } + $this->stream = new HttpBodyStream($stream, $size); + } $query = $this->getUri()->getQuery(); if ($query !== '') { @@ -158,10 +175,6 @@ public function withoutAttribute($name) */ private function parseCookie($cookie) { - if ($cookie === '') { - return array(); - } - $cookieArray = \explode(';', $cookie); $result = array(); diff --git a/tests/Message/ServerRequestTest.php b/tests/Message/ServerRequestTest.php index 4785486d..37cc1879 100644 --- a/tests/Message/ServerRequestTest.php +++ b/tests/Message/ServerRequestTest.php @@ -2,7 +2,9 @@ namespace React\Tests\Http\Message; +use React\Http\Io\HttpBodyStream; use React\Http\Message\ServerRequest; +use React\Stream\ThroughStream; use React\Tests\Http\TestCase; class ServerRequestTest extends TestCase @@ -263,4 +265,101 @@ public function testCookieWithoutSpaceAfterSeparatorWillBeAccepted() $cookies = $this->request->getCookieParams(); $this->assertEquals(array('hello' => 'world', 'react' => 'php'), $cookies); } + + public function testConstructWithStringRequestBodyReturnsStringBodyWithAutomaticSize() + { + $request = new ServerRequest( + 'GET', + 'http://localhost', + array(), + 'foo' + ); + + $body = $request->getBody(); + $this->assertSame(3, $body->getSize()); + $this->assertEquals('foo', (string) $body); + } + + public function testConstructWithHttpBodyStreamReturnsBodyAsIs() + { + $request = new ServerRequest( + 'GET', + 'http://localhost', + array(), + $body = new HttpBodyStream(new ThroughStream(), 100) + ); + + $this->assertSame($body, $request->getBody()); + } + + public function testConstructWithStreamingRequestBodyReturnsBodyWhichImplementsReadableStreamInterfaceWithSizeZeroDefault() + { + $request = new ServerRequest( + 'GET', + 'http://localhost', + array(), + new ThroughStream() + ); + + $body = $request->getBody(); + $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $body); + $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); + $this->assertSame(0, $body->getSize()); + } + + public function testConstructWithStreamingRequestBodyReturnsBodyWithSizeFromContentLengthHeader() + { + $request = new ServerRequest( + 'GET', + 'http://localhost', + array( + 'Content-Length' => 100 + ), + new ThroughStream() + ); + + $body = $request->getBody(); + $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $body); + $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); + $this->assertSame(100, $body->getSize()); + } + + public function testConstructWithStreamingRequestBodyReturnsBodyWithSizeUnknownForTransferEncodingChunked() + { + $request = new ServerRequest( + 'GET', + 'http://localhost', + array( + 'Transfer-Encoding' => 'Chunked' + ), + new ThroughStream() + ); + + $body = $request->getBody(); + $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $body); + $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); + $this->assertNull($body->getSize()); + } + + public function testConstructWithFloatRequestBodyThrows() + { + $this->setExpectedException('InvalidArgumentException'); + new ServerRequest( + 'GET', + 'http://localhost', + array(), + 1.0 + ); + } + + public function testConstructWithResourceRequestBodyThrows() + { + $this->setExpectedException('InvalidArgumentException'); + new ServerRequest( + 'GET', + 'http://localhost', + array(), + tmpfile() + ); + } } From 1666348682f82c169b6215b67b84995db157267e Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Wed, 24 Jun 2020 18:48:59 +0200 Subject: [PATCH 327/456] Drop array request handler support Instead use func_get_args() and turn any callable added after the event loop as constructor argument into a middleware and use the last one as request handler. --- README.md | 60 ++++++++++++++++++------------ examples/62-server-form-upload.php | 5 ++- src/Io/StreamingServer.php | 10 ++--- src/Server.php | 20 +++++----- tests/FunctionalBrowserTest.php | 8 ++-- tests/FunctionalServerTest.php | 22 ++++++----- tests/Io/StreamingServerTest.php | 4 +- tests/ServerTest.php | 10 +++-- 8 files changed, 78 insertions(+), 61 deletions(-) diff --git a/README.md b/README.md index afdaeae1..891c9f7e 100644 --- a/README.md +++ b/README.md @@ -803,13 +803,14 @@ to explicitly configure the total number of requests that can be handled at once like this: ```php -$server = new React\Http\Server($loop, array( +$server = new React\Http\Server( + $loop, new React\Http\Middleware\StreamingRequestMiddleware(), new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers new React\Http\Middleware\RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request new React\Http\Middleware\RequestBodyParserMiddleware(), $handler -)); +); ``` > Internally, this class automatically assigns these middleware handlers @@ -822,10 +823,11 @@ also use a streaming approach where only small chunks of data have to be kept in memory: ```php -$server = new React\Http\Server($loop, array( +$server = new React\Http\Server( + $loop, new React\Http\Middleware\StreamingRequestMiddleware(), $handler -)); +); ``` In this case, it will invoke the request handler function once the HTTP @@ -1141,7 +1143,8 @@ The ReactPHP `ReadableStreamInterface` gives you access to the incoming request body as the individual chunks arrive: ```php -$server = new React\Http\Server($loop, array( +$server = new React\Http\Server( + $loop, new React\Http\Middleware\StreamingRequestMiddleware(), function (Psr\Http\Message\ServerRequestInterface $request) { $body = $request->getBody(); @@ -1176,7 +1179,7 @@ $server = new React\Http\Server($loop, array( }); }); } -)); +); ``` The above example simply counts the number of bytes received in the request body. @@ -1214,7 +1217,8 @@ This method operates on the streaming request body, i.e. the request body size may be unknown (`null`) when using `Transfer-Encoding: chunked` for HTTP/1.1 requests. ```php -$server = new React\Http\Server($loop, array( +$server = new React\Http\Server( + $loop, new React\Http\Middleware\StreamingRequestMiddleware(), function (Psr\Http\Message\ServerRequestInterface $request) { $size = $request->getBody()->getSize(); @@ -1239,7 +1243,7 @@ $server = new React\Http\Server($loop, array( "Request body size: " . $size . " bytes\n" ); } -)); +); ``` > Note: The `Server` automatically takes care of handling requests with the @@ -1723,7 +1727,8 @@ The following example adds a middleware request handler that adds the current ti header (`Request-Time`) and a final request handler that always returns a 200 code without a body: ```php -$server = new Server($loop, array( +$server = new Server( + $loop, function (ServerRequestInterface $request, callable $next) { $request = $request->withHeader('Request-Time', time()); return $next($request); @@ -1731,7 +1736,7 @@ $server = new Server($loop, array( function (ServerRequestInterface $request) { return new Response(200); } -)); +); ``` > Note how the middleware request handler and the final request handler have a @@ -1747,7 +1752,8 @@ In order to simplify handling both paths, you can simply wrap this in a [`Promise\resolve()`](https://reactphp.org/promise/#resolve) call like this: ```php -$server = new Server($loop, array( +$server = new Server( + $loop, function (ServerRequestInterface $request, callable $next) { $promise = React\Promise\resolve($next($request)); return $promise->then(function (ResponseInterface $response) { @@ -1757,7 +1763,7 @@ $server = new Server($loop, array( function (ServerRequestInterface $request) { return new Response(200); } -)); +); ``` Note that the `$next` middleware request handler may also throw an @@ -1769,7 +1775,8 @@ handling logic (or logging etc.) by wrapping this in a [`Promise`](https://reactphp.org/promise/#promise) like this: ```php -$server = new Server($loop, array( +$server = new Server( + $loop, function (ServerRequestInterface $request, callable $next) { $promise = new React\Promise\Promise(function ($resolve) use ($next, $request) { $resolve($next($request)); @@ -1788,7 +1795,7 @@ $server = new Server($loop, array( } return new Response(200); } -)); +); ``` #### Third-Party Middleware @@ -2404,10 +2411,11 @@ The following example shows how this middleware can be used to ensure no more than 10 handlers will be invoked at once: ```php -$server = new Server($loop, array( +$server = new Server( + $loop, new LimitConcurrentRequestsMiddleware(10), $handler -)); +); ``` Similarly, this middleware is often used in combination with the @@ -2415,13 +2423,14 @@ Similarly, this middleware is often used in combination with the to limit the total number of requests that can be buffered at once: ```php -$server = new Server($loop, array( +$server = new Server( + $loop, new StreamingRequestMiddleware(), new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers new RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request new RequestBodyParserMiddleware(), $handler -)); +); ``` More sophisticated examples include limiting the total number of requests @@ -2429,14 +2438,15 @@ that can be buffered at once and then ensure the actual request handler only processes one request after another without any concurrency: ```php -$server = new Server($loop, array( +$server = new Server( + $loop, new StreamingRequestMiddleware(), new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers new RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request new RequestBodyParserMiddleware(), new LimitConcurrentRequestsMiddleware(1), // only execute 1 handler (no concurrency) $handler -)); +); ``` #### RequestBodyBufferMiddleware @@ -2482,7 +2492,8 @@ the total number of concurrent requests. Usage: ```php -$server = new Server($loop, array( +$server = new Server( + $loop, new StreamingRequestMiddleware(), new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers new RequestBodyBufferMiddleware(16 * 1024 * 1024), // 16 MiB @@ -2490,7 +2501,7 @@ $server = new Server($loop, array( // The body from $request->getBody() is now fully available without the need to stream it return new Response(200); }, -)); +); ``` #### RequestBodyParserMiddleware @@ -2542,13 +2553,14 @@ $handler = function (ServerRequestInterface $request) { ); }; -$server = new Server($loop, array( +$server = new Server( + $loop, new StreamingRequestMiddleware(), new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers new RequestBodyBufferMiddleware(16 * 1024 * 1024), // 16 MiB new RequestBodyParserMiddleware(), $handler -)); +); ``` See also [form upload server example](examples/62-server-form-upload.php) for more details. diff --git a/examples/62-server-form-upload.php b/examples/62-server-form-upload.php index 4db8c5d7..85d948fc 100644 --- a/examples/62-server-form-upload.php +++ b/examples/62-server-form-upload.php @@ -124,13 +124,14 @@ // Note how this example explicitly uses the advanced `StreamingRequestMiddleware` to apply // custom request buffering limits below before running our request handler. -$server = new Server($loop, array( +$server = new Server( + $loop, new StreamingRequestMiddleware(), new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers, queue otherwise new RequestBodyBufferMiddleware(8 * 1024 * 1024), // 8 MiB max, ignore body otherwise new RequestBodyParserMiddleware(100 * 1024, 1), // 1 file with 100 KiB max, reject upload otherwise $handler -)); +); $socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); $server->listen($socket); diff --git a/src/Io/StreamingServer.php b/src/Io/StreamingServer.php index 4c20e39a..3ec791b2 100644 --- a/src/Io/StreamingServer.php +++ b/src/Io/StreamingServer.php @@ -95,19 +95,17 @@ final class StreamingServer extends EventEmitter * See also [listen()](#listen) for more details. * * @param LoopInterface $loop - * @param callable|callable[] $requestHandler + * @param callable $requestHandler * @see self::listen() */ public function __construct(LoopInterface $loop, $requestHandler) { - $this->loop = $loop; - - if (!\is_callable($requestHandler) && !\is_array($requestHandler)) { + if (!\is_callable($requestHandler)) { throw new \InvalidArgumentException('Invalid request handler given'); - } elseif (!\is_callable($requestHandler)) { - $requestHandler = new MiddlewareRunner($requestHandler); } + $this->loop = $loop; + $this->callback = $requestHandler; $this->parser = new RequestHeaderParser(); diff --git a/src/Server.php b/src/Server.php index ee1f1f65..68ce3542 100644 --- a/src/Server.php +++ b/src/Server.php @@ -5,6 +5,7 @@ use Evenement\EventEmitter; use React\EventLoop\LoopInterface; use React\Http\Io\IniUtil; +use React\Http\Io\MiddlewareRunner; use React\Http\Io\StreamingServer; use React\Http\Middleware\LimitConcurrentRequestsMiddleware; use React\Http\Middleware\StreamingRequestMiddleware; @@ -166,17 +167,20 @@ final class Server extends EventEmitter * See also [listen()](#listen) for more details. * * @param LoopInterface $loop - * @param callable|callable[] $requestHandler + * @param callable[] ...$requestHandler * @see self::listen() */ - public function __construct(LoopInterface $loop, $requestHandler) + public function __construct(LoopInterface $loop) { - if (!\is_callable($requestHandler) && !\is_array($requestHandler)) { + $requestHandlers = \func_get_args(); + \array_shift($requestHandlers); + $requestHandlersCount = \count($requestHandlers); + if ($requestHandlersCount === 0 || \count(\array_filter($requestHandlers, 'is_callable')) < $requestHandlersCount) { throw new \InvalidArgumentException('Invalid request handler given'); } $streaming = false; - foreach ((array) $requestHandler as $handler) { + foreach ((array) $requestHandlers as $handler) { if ($handler instanceof StreamingRequestMiddleware) { $streaming = true; break; @@ -200,13 +204,9 @@ public function __construct(LoopInterface $loop, $requestHandler) } } - if (\is_callable($requestHandler)) { - $middleware[] = $requestHandler; - } else { - $middleware = \array_merge($middleware, $requestHandler); - } + $middleware = \array_merge($middleware, $requestHandlers); - $this->streamingServer = new StreamingServer($loop, $middleware); + $this->streamingServer = new StreamingServer($loop, new MiddlewareRunner($middleware)); $that = $this; $this->streamingServer->on('error', function ($error) use ($that) { diff --git a/tests/FunctionalBrowserTest.php b/tests/FunctionalBrowserTest.php index de5a9f9a..69c60ba7 100644 --- a/tests/FunctionalBrowserTest.php +++ b/tests/FunctionalBrowserTest.php @@ -32,7 +32,7 @@ public function setUpBrowserAndServer() $this->loop = $loop = Factory::create(); $this->browser = new Browser($this->loop); - $server = new Server($this->loop, array(new StreamingRequestMiddleware(), function (ServerRequestInterface $request) use ($loop) { + $server = new Server($this->loop, new StreamingRequestMiddleware(), function (ServerRequestInterface $request) use ($loop) { $path = $request->getUri()->getPath(); $headers = array(); @@ -132,7 +132,7 @@ public function setUpBrowserAndServer() } var_dump($path); - })); + }); $socket = new \React\Socket\Server(0, $this->loop); $server->listen($socket); @@ -527,9 +527,9 @@ public function testPostStreamKnownLength() */ public function testPostStreamWillStartSendingRequestEvenWhenBodyDoesNotEmitData() { - $server = new Server($this->loop, array(new StreamingRequestMiddleware(), function (ServerRequestInterface $request) { + $server = new Server($this->loop, new StreamingRequestMiddleware(), function (ServerRequestInterface $request) { return new Response(200); - })); + }); $socket = new \React\Socket\Server(0, $this->loop); $server->listen($socket); diff --git a/tests/FunctionalServerTest.php b/tests/FunctionalServerTest.php index f92fb2b0..ba834082 100644 --- a/tests/FunctionalServerTest.php +++ b/tests/FunctionalServerTest.php @@ -52,11 +52,12 @@ public function testPlainHttpOnRandomPortWithSingleRequestHandlerArray() $loop = Factory::create(); $connector = new Connector($loop); - $server = new Server($loop, array( + $server = new Server( + $loop, function () { return new Response(404); - }, - )); + } + ); $socket = new Socket(0, $loop); $server->listen($socket); @@ -479,12 +480,13 @@ public function testRequestHandlerWithStreamingRequestWillReceiveCloseEventIfCon $connector = new Connector($loop); $once = $this->expectCallableOnce(); - $server = new Server($loop, array( + $server = new Server( + $loop, new StreamingRequestMiddleware(), function (RequestInterface $request) use ($once) { $request->getBody()->on('close', $once); } - )); + ); $socket = new Socket(0, $loop); $server->listen($socket); @@ -509,12 +511,13 @@ public function testStreamFromRequestHandlerWillBeClosedIfConnectionClosesWhileS $stream = new ThroughStream(); - $server = new Server($loop, array( + $server = new Server( + $loop, new StreamingRequestMiddleware(), function (RequestInterface $request) use ($stream) { return new Response(200, array(), $stream); } - )); + ); $socket = new Socket(0, $loop); $server->listen($socket); @@ -757,7 +760,8 @@ public function testLimitConcurrentRequestsMiddlewareRequestStreamPausing() $loop = Factory::create(); $connector = new Connector($loop); - $server = new Server($loop, array( + $server = new Server( + $loop, new LimitConcurrentRequestsMiddleware(5), new RequestBodyBufferMiddleware(16 * 1024 * 1024), // 16 MiB function (ServerRequestInterface $request, $next) use ($loop) { @@ -770,7 +774,7 @@ function (ServerRequestInterface $request, $next) use ($loop) { function (ServerRequestInterface $request) { return new Response(200, array(), (string)strlen((string)$request->getBody())); } - )); + ); $socket = new Socket(0, $loop); $server->listen($socket); diff --git a/tests/Io/StreamingServerTest.php b/tests/Io/StreamingServerTest.php index 3559c1d2..b903a850 100644 --- a/tests/Io/StreamingServerTest.php +++ b/tests/Io/StreamingServerTest.php @@ -127,10 +127,10 @@ public function testRequestEventWithSingleRequestHandlerArray() { $i = 0; $requestAssertion = null; - $server = new StreamingServer(Factory::create(), array(function (ServerRequestInterface $request) use (&$i, &$requestAssertion) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$i, &$requestAssertion) { $i++; $requestAssertion = $request; - })); + }); $this->connection ->expects($this->any()) diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 8d8cac5d..59452815 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -88,7 +88,8 @@ public function helperCallableOnce() public function testSimpleRequestWithMiddlewareArrayProcessesMiddlewareStack() { $called = null; - $server = new Server(Factory::create(), array( + $server = new Server( + Factory::create(), function (ServerRequestInterface $request, $next) use (&$called) { $called = 'before'; $ret = $next($request->withHeader('Demo', 'ok')); @@ -99,7 +100,7 @@ function (ServerRequestInterface $request, $next) use (&$called) { function (ServerRequestInterface $request) use (&$called) { $called .= $request->getHeaderLine('Demo'); } - )); + ); $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); @@ -161,12 +162,13 @@ public function testServerReceivesBufferedRequestByDefault() public function testServerWithStreamingRequestMiddlewareReceivesStreamingRequest() { $streaming = null; - $server = new Server(Factory::create(), array( + $server = new Server( + Factory::create(), new StreamingRequestMiddleware(), function (ServerRequestInterface $request) use (&$streaming) { $streaming = $request->getBody() instanceof ReadableStreamInterface; } - )); + ); $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); From 49b4799d56895b79cc3e1fa05b070f06674b5979 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 10 Jul 2020 12:40:31 +0200 Subject: [PATCH 328/456] Improve default concurrency and cap default request buffer at 64K --- README.md | 41 +++++-- src/Io/StreamingServer.php | 4 +- .../LimitConcurrentRequestsMiddleware.php | 15 ++- src/Server.php | 100 +++++++++------ tests/ServerTest.php | 116 ++++++++++++++++-- 5 files changed, 208 insertions(+), 68 deletions(-) diff --git a/README.md b/README.md index f95214e5..2888e500 100644 --- a/README.md +++ b/README.md @@ -773,7 +773,8 @@ their respective default values: ``` memory_limit 128M -post_max_size 8M +post_max_size 8M // capped at 64K + enable_post_data_reading 1 max_input_nesting_level 64 max_input_vars 1000 @@ -784,22 +785,30 @@ max_file_uploads 20 ``` In particular, the `post_max_size` setting limits how much memory a single -HTTP request is allowed to consume while buffering its request body. On top -of this, this class will try to avoid consuming more than 1/4 of your +HTTP request is allowed to consume while buffering its request body. This +needs to be limited because the server can process a large number of requests +concurrently, so the server may potentially consume a large amount of memory +otherwise. To support higher concurrency by default, this value is capped +at `64K`. If you assign a higher value, it will only allow `64K` by default. +If a request exceeds this limit, its request body will be ignored and it will +be processed like a request with no request body at all. See below for +explicit configuration to override this setting. + +By default, this class will try to avoid consuming more than half of your `memory_limit` for buffering multiple concurrent HTTP requests. As such, with the above default settings of `128M` max, it will try to consume no more than -`32M` for buffering multiple concurrent HTTP requests. As a consequence, it -will limit the concurrency to 4 HTTP requests with the above defaults. +`64M` for buffering multiple concurrent HTTP requests. As a consequence, it +will limit the concurrency to `1024` HTTP requests with the above defaults. It is imperative that you assign reasonable values to your PHP ini settings. -It is usually recommended to either reduce the memory a single request is -allowed to take (set `post_max_size 1M` or less) or to increase the total -memory limit to allow for more concurrent requests (set `memory_limit 512M` -or more). Failure to do so means that this class may have to disable -concurrency and only handle one request at a time. - -As an alternative to the above buffering defaults, you can also configure -the `Server` explicitly to override these defaults. You can use the +It is usually recommended to not support buffering incoming HTTP requests +with a large HTTP request body (e.g. large file uploads). If you want to +increase this buffer size, you will have to also increase the total memory +limit to allow for more concurrent requests (set `memory_limit 512M` or more) +or explicitly limit concurrency. + +In order to override the above buffering defaults, you can configure the +`Server` explicitly. You can use the [`LimitConcurrentRequestsMiddleware`](#limitconcurrentrequestsmiddleware) and [`RequestBodyBufferMiddleware`](#requestbodybuffermiddleware) (see below) to explicitly configure the total number of requests that can be handled at @@ -816,6 +825,12 @@ $server = new React\Http\Server( ); ``` +In this example, we allow processing up to 100 concurrent requests at once +and each request can buffer up to `2M`. This means you may have to keep a +maximum of `200M` of memory for incoming request body buffers. Accordingly, +you need to adjust the `memory_limit` ini setting to allow for these buffers +plus your actual application logic memory requirements (think `512M` or more). + > Internally, this class automatically assigns these middleware handlers automatically when no [`StreamingRequestMiddleware`](#streamingrequestmiddleware) is given. Accordingly, you can use this example to override all default diff --git a/src/Io/StreamingServer.php b/src/Io/StreamingServer.php index 1d41a9c3..166cd4e0 100644 --- a/src/Io/StreamingServer.php +++ b/src/Io/StreamingServer.php @@ -29,7 +29,7 @@ * object in return: * * ```php - * $server = new StreamingServer(function (ServerRequestInterface $request) { + * $server = new StreamingServer($loop, function (ServerRequestInterface $request) { * return new Response( * 200, * array( @@ -54,7 +54,7 @@ * in order to start a plaintext HTTP server like this: * * ```php - * $server = new StreamingServer($handler); + * $server = new StreamingServer($loop, $handler); * * $socket = new React\Socket\Server('0.0.0.0:8080', $loop); * $server->listen($socket); diff --git a/src/Middleware/LimitConcurrentRequestsMiddleware.php b/src/Middleware/LimitConcurrentRequestsMiddleware.php index d16402df..d6760e95 100644 --- a/src/Middleware/LimitConcurrentRequestsMiddleware.php +++ b/src/Middleware/LimitConcurrentRequestsMiddleware.php @@ -29,11 +29,12 @@ * than 10 handlers will be invoked at once: * * ```php - * $server = new Server(array( + * $server = new Server( + * $loop, * new StreamingRequestMiddleware(), * new LimitConcurrentRequestsMiddleware(10), * $handler - * )); + * ); * ``` * * Similarly, this middleware is often used in combination with the @@ -41,13 +42,14 @@ * to limit the total number of requests that can be buffered at once: * * ```php - * $server = new Server(array( + * $server = new Server( + * $loop, * new StreamingRequestMiddleware(), * new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers * new RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request * new RequestBodyParserMiddleware(), * $handler - * )); + * ); * ``` * * More sophisticated examples include limiting the total number of requests @@ -55,14 +57,15 @@ * processes one request after another without any concurrency: * * ```php - * $server = new Server(array( + * $server = new Server( + * $loop, * new StreamingRequestMiddleware(), * new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers * new RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request * new RequestBodyParserMiddleware(), * new LimitConcurrentRequestsMiddleware(1), // only execute 1 handler (no concurrency) * $handler - * )); + * ); * ``` * * @see RequestBodyBufferMiddleware diff --git a/src/Server.php b/src/Server.php index 7b165c37..3b5d70be 100644 --- a/src/Server.php +++ b/src/Server.php @@ -23,7 +23,7 @@ * object and expects a [response](#server-response) object in return: * * ```php - * $server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterface $request) { + * $server = new React\Http\Server($loop, function (Psr\Http\Message\ServerRequestInterface $request) { * return new React\Http\Message\Response( * 200, * array( @@ -51,7 +51,7 @@ * to start a plaintext HTTP server like this: * * ```php - * $server = new React\Http\Server($handler); + * $server = new React\Http\Server($loop, $handler); * * $socket = new React\Socket\Server('0.0.0.0:8080', $loop); * $server->listen($socket); @@ -79,7 +79,8 @@ * * ``` * memory_limit 128M - * post_max_size 8M + * post_max_size 8M // capped at 64K + * * enable_post_data_reading 1 * max_input_nesting_level 64 * max_input_vars 1000 @@ -90,29 +91,38 @@ * ``` * * In particular, the `post_max_size` setting limits how much memory a single - * HTTP request is allowed to consume while buffering its request body. On top - * of this, this class will try to avoid consuming more than 1/4 of your + * HTTP request is allowed to consume while buffering its request body. This + * needs to be limited because the server can process a large number of requests + * concurrently, so the server may potentially consume a large amount of memory + * otherwise. To support higher concurrency by default, this value is capped + * at `64K`. If you assign a higher value, it will only allow `64K` by default. + * If a request exceeds this limit, its request body will be ignored and it will + * be processed like a request with no request body at all. See below for + * explicit configuration to override this setting. + * + * By default, this class will try to avoid consuming more than half of your * `memory_limit` for buffering multiple concurrent HTTP requests. As such, with * the above default settings of `128M` max, it will try to consume no more than - * `32M` for buffering multiple concurrent HTTP requests. As a consequence, it - * will limit the concurrency to 4 HTTP requests with the above defaults. + * `64M` for buffering multiple concurrent HTTP requests. As a consequence, it + * will limit the concurrency to `1024` HTTP requests with the above defaults. * * It is imperative that you assign reasonable values to your PHP ini settings. - * It is usually recommended to either reduce the memory a single request is - * allowed to take (set `post_max_size 1M` or less) or to increase the total - * memory limit to allow for more concurrent requests (set `memory_limit 512M` - * or more). Failure to do so means that this class may have to disable - * concurrency and only handle one request at a time. + * It is usually recommended to not support buffering incoming HTTP requests + * with a large HTTP request body (e.g. large file uploads). If you want to + * increase this buffer size, you will have to also increase the total memory + * limit to allow for more concurrent requests (set `memory_limit 512M` or more) + * or explicitly limit concurrency. * - * As an alternative to the above buffering defaults, you can also configure - * the `Server` explicitly to override these defaults. You can use the + * In order to override the above buffering defaults, you can configure the + * `Server` explicitly. You can use the * [`LimitConcurrentRequestsMiddleware`](#limitconcurrentrequestsmiddleware) and * [`RequestBodyBufferMiddleware`](#requestbodybuffermiddleware) (see below) * to explicitly configure the total number of requests that can be handled at * once like this: * * ```php - * $server = new React\Http\Server(array( + * $server = new React\Http\Server( + * $loop, * new React\Http\Middleware\StreamingRequestMiddleware(), * new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers * new React\Http\Middleware\RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request @@ -121,6 +131,12 @@ * )); * ``` * + * In this example, we allow processing up to 100 concurrent requests at once + * and each request can buffer up to `2M`. This means you may have to keep a + * maximum of `200M` of memory for incoming request body buffers. Accordingly, + * you need to adjust the `memory_limit` ini setting to allow for these buffers + * plus your actual application logic memory requirements (think `512M` or more). + * * > Internally, this class automatically assigns these middleware handlers * automatically when no [`StreamingRequestMiddleware`](#streamingrequestmiddleware) * is given. Accordingly, you can use this example to override all default @@ -131,10 +147,11 @@ * in memory: * * ```php - * $server = new React\Http\Server(array( + * $server = new React\Http\Server( + * $loop, * new React\Http\Middleware\StreamingRequestMiddleware(), * $handler - * )); + * ); * ``` * * In this case, it will invoke the request handler function once the HTTP @@ -149,9 +166,17 @@ final class Server extends EventEmitter { /** + * The maximum buffer size used for each request. + * + * This needs to be limited because the server can process a large number of + * requests concurrently, so the server may potentially consume a large + * amount of memory otherwise. + * + * See `RequestBodyBufferMiddleware` to override this setting. + * * @internal */ - const MAXIMUM_CONCURRENT_REQUESTS = 100; + const MAXIMUM_BUFFER_SIZE = 65536; // 64 KiB /** * @var StreamingServer @@ -189,10 +214,12 @@ public function __construct(LoopInterface $loop) $middleware = array(); if (!$streaming) { - $middleware[] = new LimitConcurrentRequestsMiddleware( - $this->getConcurrentRequestsLimit(\ini_get('memory_limit'), \ini_get('post_max_size')) - ); - $middleware[] = new RequestBodyBufferMiddleware(); + $maxSize = $this->getMaxRequestSize(); + $concurrency = $this->getConcurrentRequestsLimit(\ini_get('memory_limit'), $maxSize); + if ($concurrency !== null) { + $middleware[] = new LimitConcurrentRequestsMiddleware($concurrency); + } + $middleware[] = new RequestBodyBufferMiddleware($maxSize); // Checking for an empty string because that is what a boolean // false is returned as by ini_get depending on the PHP version. // @link http://php.net/manual/en/ini.core.php#ini.enable-post-data-reading @@ -226,7 +253,7 @@ public function __construct(LoopInterface $loop) * order to start a plaintext HTTP server like this: * * ```php - * $server = new React\Http\Server($handler); + * $server = new React\Http\Server($loop, $handler); * * $socket = new React\Socket\Server(8080, $loop); * $server->listen($socket); @@ -252,7 +279,7 @@ public function __construct(LoopInterface $loop) * `passphrase` like this: * * ```php - * $server = new React\Http\Server($handler); + * $server = new React\Http\Server($loop, $handler); * * $socket = new React\Socket\Server('tls://0.0.0.0:8443', $loop, array( * 'local_cert' => __DIR__ . '/localhost.pem' @@ -273,25 +300,28 @@ public function listen(ServerInterface $server) /** * @param string $memory_limit * @param string $post_max_size - * @return int + * @return ?int */ private function getConcurrentRequestsLimit($memory_limit, $post_max_size) { if ($memory_limit == -1) { - return self::MAXIMUM_CONCURRENT_REQUESTS; - } - - if ($post_max_size == 0) { - return 1; + return null; } - $availableMemory = IniUtil::iniSizeToBytes($memory_limit) / 4; + $availableMemory = IniUtil::iniSizeToBytes($memory_limit) / 2; $concurrentRequests = (int) \ceil($availableMemory / IniUtil::iniSizeToBytes($post_max_size)); - if ($concurrentRequests >= self::MAXIMUM_CONCURRENT_REQUESTS) { - return self::MAXIMUM_CONCURRENT_REQUESTS; - } - return $concurrentRequests; } + + /** + * @param ?string $post_max_size + * @return int + */ + private function getMaxRequestSize($post_max_size = null) + { + $maxSize = IniUtil::iniSizeToBytes($post_max_size === null ? \ini_get('post_max_size') : $post_max_size); + + return ($maxSize === 0 || $maxSize >= self::MAXIMUM_BUFFER_SIZE) ? self::MAXIMUM_BUFFER_SIZE : $maxSize; + } } diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 59452815..ce19dda9 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -10,6 +10,7 @@ use React\Promise; use React\Http\Middleware\StreamingRequestMiddleware; use React\Stream\ReadableStreamInterface; +use React\Http\Io\IniUtil; final class ServerTest extends TestCase { @@ -224,23 +225,18 @@ public function provideIniSettingsForConcurrency() return array( 'default settings' => array( '128M', - '8M', - 4 + '64K', // 8M capped at maximum + 1024 ), - 'unlimited memory_limit limited to maximum concurrency' => array( + 'unlimited memory_limit has no concurrency limit' => array( '-1', '8M', - 100 - ), - 'unlimited post_max_size' => array( - '128M', - '0', - 1 + null ), - 'small post_max_size limited to maximum concurrency' => array( + 'small post_max_size results in high concurrency' => array( '128M', '1k', - 100 + 65536 ) ); } @@ -248,7 +244,7 @@ public function provideIniSettingsForConcurrency() /** * @param string $memory_limit * @param string $post_max_size - * @param int $expectedConcurrency + * @param ?int $expectedConcurrency * @dataProvider provideIniSettingsForConcurrency */ public function testServerConcurrency($memory_limit, $post_max_size, $expectedConcurrency) @@ -262,4 +258,100 @@ public function testServerConcurrency($memory_limit, $post_max_size, $expectedCo $this->assertEquals($expectedConcurrency, $value); } + + public function testServerGetPostMaxSizeReturnsSizeFromGivenIniSetting() + { + $server = new Server(Factory::create(), function () { }); + + $ref = new \ReflectionMethod($server, 'getMaxRequestSize'); + $ref->setAccessible(true); + + $value = $ref->invoke($server, '1k'); + + $this->assertEquals(1024, $value); + } + + public function testServerGetPostMaxSizeReturnsSizeCappedFromGivenIniSetting() + { + $server = new Server(Factory::create(), function () { }); + + $ref = new \ReflectionMethod($server, 'getMaxRequestSize'); + $ref->setAccessible(true); + + $value = $ref->invoke($server, '1M'); + + $this->assertEquals(64 * 1024, $value); + } + + public function testServerGetPostMaxSizeFromIniIsCapped() + { + if (IniUtil::iniSizeToBytes(ini_get('post_max_size')) < 64 * 1024) { + $this->markTestSkipped(); + } + + $server = new Server(Factory::create(), function () { }); + + $ref = new \ReflectionMethod($server, 'getMaxRequestSize'); + $ref->setAccessible(true); + + $value = $ref->invoke($server); + + $this->assertEquals(64 * 1024, $value); + } + + public function testConstructServerWithUnlimitedMemoryLimitDoesNotLimitConcurrency() + { + $old = ini_get('memory_limit'); + ini_set('memory_limit', '-1'); + + $server = new Server(Factory::create(), function () { }); + + ini_set('memory_limit', $old); + + $ref = new \ReflectionProperty($server, 'streamingServer'); + $ref->setAccessible(true); + + $streamingServer = $ref->getValue($server); + + $ref = new \ReflectionProperty($streamingServer, 'callback'); + $ref->setAccessible(true); + + $middlewareRunner = $ref->getValue($streamingServer); + + $ref = new \ReflectionProperty($middlewareRunner, 'middleware'); + $ref->setAccessible(true); + + $middleware = $ref->getValue($middlewareRunner); + + $this->assertTrue(is_array($middleware)); + $this->assertInstanceOf('React\Http\Middleware\RequestBodyBufferMiddleware', $middleware[0]); + } + + public function testConstructServerWithMemoryLimitDoesLimitConcurrency() + { + $old = ini_get('memory_limit'); + ini_set('memory_limit', '100M'); + + $server = new Server(Factory::create(), function () { }); + + ini_set('memory_limit', $old); + + $ref = new \ReflectionProperty($server, 'streamingServer'); + $ref->setAccessible(true); + + $streamingServer = $ref->getValue($server); + + $ref = new \ReflectionProperty($streamingServer, 'callback'); + $ref->setAccessible(true); + + $middlewareRunner = $ref->getValue($streamingServer); + + $ref = new \ReflectionProperty($middlewareRunner, 'middleware'); + $ref->setAccessible(true); + + $middleware = $ref->getValue($middlewareRunner); + + $this->assertTrue(is_array($middleware)); + $this->assertInstanceOf('React\Http\Middleware\LimitConcurrentRequestsMiddleware', $middleware[0]); + } } From b7a9251428c06cb16294aa785cf0184a44ce768a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 10 Jul 2020 13:08:34 +0200 Subject: [PATCH 329/456] Support skipping all online tests with --exclude-group internet --- README.md | 8 ++++++++ tests/Client/FunctionalIntegrationTest.php | 8 ++++++++ tests/FunctionalBrowserTest.php | 20 ++++++++++---------- tests/Io/RequestHeaderParserTest.php | 6 ------ 4 files changed, 26 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index f95214e5..9de7bcd5 100644 --- a/README.md +++ b/README.md @@ -2741,6 +2741,14 @@ To run the test suite, go to the project root and run: $ php vendor/bin/phpunit ``` +The test suite also contains a number of functional integration tests that rely +on a stable internet connection. +If you do not want to run these, they can simply be skipped like this: + +```bash +$ php vendor/bin/phpunit --exclude-group internet +``` + ## License MIT, see [LICENSE file](LICENSE). diff --git a/tests/Client/FunctionalIntegrationTest.php b/tests/Client/FunctionalIntegrationTest.php index d6cc4b0f..db82b1f1 100644 --- a/tests/Client/FunctionalIntegrationTest.php +++ b/tests/Client/FunctionalIntegrationTest.php @@ -101,6 +101,10 @@ public function testSuccessfulResponseEmitsEnd() /** @group internet */ public function testPostDataReturnsData() { + if (defined('HHVM_VERSION')) { + $this->markTestSkipped('Not supported on HHVM'); + } + $loop = Factory::create(); $client = new Client($loop); @@ -130,6 +134,10 @@ public function testPostDataReturnsData() /** @group internet */ public function testPostJsonReturnsData() { + if (defined('HHVM_VERSION')) { + $this->markTestSkipped('Not supported on HHVM'); + } + $loop = Factory::create(); $client = new Client($loop); diff --git a/tests/FunctionalBrowserTest.php b/tests/FunctionalBrowserTest.php index 3cf9293a..f4495565 100644 --- a/tests/FunctionalBrowserTest.php +++ b/tests/FunctionalBrowserTest.php @@ -351,25 +351,25 @@ public function testGetRequestWithResponseBufferExceededDuringStreamingRejects() } /** - * @group online + * @group internet * @doesNotPerformAssertions */ public function testCanAccessHttps() { - if (!function_exists('stream_socket_enable_crypto')) { - $this->markTestSkipped('Not supported on your platform (outdated HHVM?)'); + if (defined('HHVM_VERSION')) { + $this->markTestSkipped('Not supported on HHVM'); } Block\await($this->browser->get('https://www.google.com/'), $this->loop); } /** - * @group online + * @group internet */ public function testVerifyPeerEnabledForBadSslRejects() { - if (!function_exists('stream_socket_enable_crypto')) { - $this->markTestSkipped('Not supported on your platform (outdated HHVM?)'); + if (defined('HHVM_VERSION')) { + $this->markTestSkipped('Not supported on HHVM'); } $connector = new Connector($this->loop, array( @@ -385,13 +385,13 @@ public function testVerifyPeerEnabledForBadSslRejects() } /** - * @group online + * @group internet * @doesNotPerformAssertions */ public function testVerifyPeerDisabledForBadSslResolves() { - if (!function_exists('stream_socket_enable_crypto')) { - $this->markTestSkipped('Not supported on your platform (outdated HHVM?)'); + if (defined('HHVM_VERSION')) { + $this->markTestSkipped('Not supported on HHVM'); } $connector = new Connector($this->loop, array( @@ -406,7 +406,7 @@ public function testVerifyPeerDisabledForBadSslResolves() } /** - * @group online + * @group internet */ public function testInvalidPort() { diff --git a/tests/Io/RequestHeaderParserTest.php b/tests/Io/RequestHeaderParserTest.php index ca18df13..b0a339ed 100644 --- a/tests/Io/RequestHeaderParserTest.php +++ b/tests/Io/RequestHeaderParserTest.php @@ -340,9 +340,6 @@ public function testInvalidMalformedRequestLineParseException() $this->assertSame('Unable to parse invalid request-line', $error->getMessage()); } - /** - * @group a - */ public function testInvalidMalformedRequestHeadersThrowsParseException() { $error = null; @@ -362,9 +359,6 @@ public function testInvalidMalformedRequestHeadersThrowsParseException() $this->assertSame('Unable to parse invalid request header fields', $error->getMessage()); } - /** - * @group a - */ public function testInvalidMalformedRequestHeadersWhitespaceThrowsParseException() { $error = null; From 89a1d418973ecb9d340b9af4a95f63d0cd9d0818 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 10 Jul 2020 21:44:32 +0200 Subject: [PATCH 330/456] Expose ReactPHP in `User-Agent` and `Server` header The `Browser` now sends a `User-Agent: ReactPHP/1` request header (was `User-Agent: React/alpha`) by default. The `Server` now sends a `Server: ReactPHP/1` response header (was `X-Powered-By: React/alpha`) by default. Both can be overridden by explicitly assigning other header values as usual. --- README.md | 51 ++++++++++++++++++-------------- src/Client/RequestData.php | 2 +- src/Io/StreamingServer.php | 10 +++---- tests/Client/RequestDataTest.php | 16 +++++----- tests/Io/StreamingServerTest.php | 14 ++++----- 5 files changed, 49 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index f95214e5..fe82f20b 100644 --- a/README.md +++ b/README.md @@ -1611,65 +1611,70 @@ create your own HTTP response message instead. When a response is returned from the request handler function, it will be processed by the [`Server`](#server) and then sent back to the client. -The `Server` will automatically add the protocol version of the request, so you -don't have to. - -A `Date` header will be automatically added with the system date and time if none is given. -You can add a custom `Date` header yourself like this: +A `Server: ReactPHP/1` response header will be added automatically. You can add +a custom `Server` response header like this: ```php -$server = new Server($loop, function (ServerRequestInterface $request) { - return new Response( +$server = new React\Http\Server($loop, function (ServerRequestInterface $request) { + return new React\Http\Message\Response( 200, array( - 'Date' => date('D, d M Y H:i:s T') + 'Server' => 'PHP/3' ) ); }); ``` -If you don't have a appropriate clock to rely on, you should -unset this header with an empty string: +If you do not want to send this `Sever` response header at all (such as when you +don't want to expose the underlying server software), you can use an empty +string value like this: ```php -$server = new Server($loop, function (ServerRequestInterface $request) { - return new Response( +$server = new React\Http\Server($loop, function (ServerRequestInterface $request) { + return new React\Http\Message\Response( 200, array( - 'Date' => '' + 'Server' => '' ) ); }); ``` -Note that it will automatically assume a `X-Powered-By: react/alpha` header -unless your specify a custom `X-Powered-By` header yourself: +A `Date` response header will be added automatically with the current system +date and time if none is given. You can add a custom `Date` response header +like this: ```php -$server = new Server($loop, function (ServerRequestInterface $request) { - return new Response( +$server = new React\Http\Server($loop, function (ServerRequestInterface $request) { + return new React\Http\Message\Response( 200, array( - 'X-Powered-By' => 'PHP 3' + 'Date' => gmdate('D, d M Y H:i:s \G\M\T') ) ); }); ``` -If you do not want to send this header at all, you can use an empty string as -value like this: +If you do not want to send this `Date` response header at all (such as when you +don't have an appropriate clock to rely on), you can use an empty string value +like this: ```php -$server = new Server($loop, function (ServerRequestInterface $request) { - return new Response( +$server = new React\Http\Server($loop, function (ServerRequestInterface $request) { + return new React\Http\Message\Response( 200, array( - 'X-Powered-By' => '' + 'Date' => '' ) ); }); ``` +The `Server` class will automatically add the protocol version of the request, +so you don't have to. For instance, if the client sends the request using the +HTTP/1.1 protocol version, the response message will also use the same protocol +version, no matter what version is returned from the request handler function. + Note that persistent connections (`Connection: keep-alive`) are currently not supported. As such, HTTP/1.1 response messages will automatically include a diff --git a/src/Client/RequestData.php b/src/Client/RequestData.php index 55efaa9b..a5908a08 100644 --- a/src/Client/RequestData.php +++ b/src/Client/RequestData.php @@ -29,7 +29,7 @@ private function mergeDefaultheaders(array $headers) $defaults = array_merge( array( 'Host' => $this->getHost().$port, - 'User-Agent' => 'React/alpha', + 'User-Agent' => 'ReactPHP/1', ), $connectionHeaders, $authHeaders diff --git a/src/Io/StreamingServer.php b/src/Io/StreamingServer.php index 1d41a9c3..c056ac56 100644 --- a/src/Io/StreamingServer.php +++ b/src/Io/StreamingServer.php @@ -244,11 +244,11 @@ public function handleResponse(ConnectionInterface $connection, ServerRequestInt $version = $request->getProtocolVersion(); $response = $response->withProtocolVersion($version); - // assign default "X-Powered-By" header automatically - if (!$response->hasHeader('X-Powered-By')) { - $response = $response->withHeader('X-Powered-By', 'React/alpha'); - } elseif ($response->getHeaderLine('X-Powered-By') === ''){ - $response = $response->withoutHeader('X-Powered-By'); + // assign default "Server" header automatically + if (!$response->hasHeader('Server')) { + $response = $response->withHeader('Server', 'ReactPHP/1'); + } elseif ($response->getHeaderLine('Server') === ''){ + $response = $response->withoutHeader('Server'); } // assign default "Date" header from current time automatically diff --git a/tests/Client/RequestDataTest.php b/tests/Client/RequestDataTest.php index 313e140f..7f96e152 100644 --- a/tests/Client/RequestDataTest.php +++ b/tests/Client/RequestDataTest.php @@ -14,7 +14,7 @@ public function toStringReturnsHTTPRequestMessage() $expected = "GET / HTTP/1.0\r\n" . "Host: www.example.com\r\n" . - "User-Agent: React/alpha\r\n" . + "User-Agent: ReactPHP/1\r\n" . "\r\n"; $this->assertSame($expected, $requestData->__toString()); @@ -27,7 +27,7 @@ public function toStringReturnsHTTPRequestMessageWithEmptyQueryString() $expected = "GET /path?hello=world HTTP/1.0\r\n" . "Host: www.example.com\r\n" . - "User-Agent: React/alpha\r\n" . + "User-Agent: ReactPHP/1\r\n" . "\r\n"; $this->assertSame($expected, $requestData->__toString()); @@ -40,7 +40,7 @@ public function toStringReturnsHTTPRequestMessageWithZeroQueryStringAndRootPath( $expected = "GET /?0 HTTP/1.0\r\n" . "Host: www.example.com\r\n" . - "User-Agent: React/alpha\r\n" . + "User-Agent: ReactPHP/1\r\n" . "\r\n"; $this->assertSame($expected, $requestData->__toString()); @@ -53,7 +53,7 @@ public function toStringReturnsHTTPRequestMessageWithOptionsAbsoluteRequestForm( $expected = "OPTIONS / HTTP/1.0\r\n" . "Host: www.example.com\r\n" . - "User-Agent: React/alpha\r\n" . + "User-Agent: ReactPHP/1\r\n" . "\r\n"; $this->assertSame($expected, $requestData->__toString()); @@ -66,7 +66,7 @@ public function toStringReturnsHTTPRequestMessageWithOptionsAsteriskRequestForm( $expected = "OPTIONS * HTTP/1.0\r\n" . "Host: www.example.com\r\n" . - "User-Agent: React/alpha\r\n" . + "User-Agent: ReactPHP/1\r\n" . "\r\n"; $this->assertSame($expected, $requestData->__toString()); @@ -80,7 +80,7 @@ public function toStringReturnsHTTPRequestMessageWithProtocolVersion() $expected = "GET / HTTP/1.1\r\n" . "Host: www.example.com\r\n" . - "User-Agent: React/alpha\r\n" . + "User-Agent: ReactPHP/1\r\n" . "Connection: close\r\n" . "\r\n"; @@ -131,7 +131,7 @@ public function toStringReturnsHTTPRequestMessageWithProtocolVersionThroughConst $expected = "GET / HTTP/1.1\r\n" . "Host: www.example.com\r\n" . - "User-Agent: React/alpha\r\n" . + "User-Agent: ReactPHP/1\r\n" . "Connection: close\r\n" . "\r\n"; @@ -145,7 +145,7 @@ public function toStringUsesUserPassFromURL() $expected = "GET / HTTP/1.0\r\n" . "Host: www.example.com\r\n" . - "User-Agent: React/alpha\r\n" . + "User-Agent: ReactPHP/1\r\n" . "Authorization: Basic am9objpkdW1teQ==\r\n" . "\r\n"; diff --git a/tests/Io/StreamingServerTest.php b/tests/Io/StreamingServerTest.php index 5dff1f0c..d2401a06 100644 --- a/tests/Io/StreamingServerTest.php +++ b/tests/Io/StreamingServerTest.php @@ -650,7 +650,7 @@ public function testRequestEventWithPartialBodyWillEmitData() $this->connection->emit('data', array($data)); } - public function testResponseContainsPoweredByHeader() + public function testResponseContainsServerHeader() { $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { return new Response(); @@ -675,7 +675,7 @@ function ($data) use (&$buffer) { $data = $this->createGetRequest(); $this->connection->emit('data', array($data)); - $this->assertContainsString("\r\nX-Powered-By: React/alpha\r\n", $buffer); + $this->assertContainsString("\r\nServer: ReactPHP/1\r\n", $buffer); } public function testResponsePendingPromiseWillNotSendAnything() @@ -931,7 +931,7 @@ public function testResponseUpgradeInResponseCanBeUsedToAdvertisePossibleUpgrade 200, array( 'date' => '', - 'x-powered-by' => '', + 'server' => '', 'Upgrade' => 'demo' ), 'foo' @@ -967,7 +967,7 @@ public function testResponseUpgradeWishInRequestCanBeIgnoredByReturningNormalRes 200, array( 'date' => '', - 'x-powered-by' => '' + 'server' => '' ), 'foo' ); @@ -1002,7 +1002,7 @@ public function testResponseUpgradeSwitchingProtocolIncludesConnectionUpgradeHea 101, array( 'date' => '', - 'x-powered-by' => '', + 'server' => '', 'Upgrade' => 'demo' ), 'foo' @@ -1042,7 +1042,7 @@ public function testResponseUpgradeSwitchingProtocolWithStreamWillPipeDataToConn 101, array( 'date' => '', - 'x-powered-by' => '', + 'server' => '', 'Upgrade' => 'demo' ), $stream @@ -2171,7 +2171,7 @@ public function testResponseCanContainMultipleCookieHeaders() 'session=abc' ), 'Date' => '', - 'X-Powered-By' => '' + 'Server' => '' ) ); }); From b9ed1e62d7423da97e8d2fbc38f2ce2761951ea2 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Fri, 10 Jul 2020 19:27:52 +0200 Subject: [PATCH 331/456] Tag Browser as final to discourage inheriting from it --- README.md | 2 ++ src/Browser.php | 3 +++ 2 files changed, 5 insertions(+) diff --git a/README.md b/README.md index f95214e5..1026206f 100644 --- a/README.md +++ b/README.md @@ -1866,6 +1866,8 @@ $connector = new React\Socket\Connector($loop, array( $browser = new React\Http\Browser($loop, $connector); ``` +> Note that the browser class is final and shouldn't be extended, it is likely to be marked final in a future release. + #### get() The `get(string $url, array $headers = array()): PromiseInterface` method can be used to diff --git a/src/Browser.php b/src/Browser.php index 28f90f87..9da950ba 100644 --- a/src/Browser.php +++ b/src/Browser.php @@ -13,6 +13,9 @@ use React\Socket\ConnectorInterface; use React\Stream\ReadableStreamInterface; +/** + * @final This class is final and shouldn't be extended as it is likely to be marked final in a future relase. + */ class Browser { private $transaction; From c3e3da687a8e6709ba1094f84f54ddd92ecf9824 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Fri, 10 Jul 2020 22:10:44 +0200 Subject: [PATCH 332/456] Mark all non-internal Message classes final --- src/Message/Response.php | 2 +- src/Message/ResponseException.php | 2 +- src/Message/ServerRequest.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Message/Response.php b/src/Message/Response.php index 45d007ee..429cb120 100644 --- a/src/Message/Response.php +++ b/src/Message/Response.php @@ -32,7 +32,7 @@ * * @see \Psr\Http\Message\ResponseInterface */ -class Response extends Psr7Response +final class Response extends Psr7Response { /** * @param int $status HTTP status code (e.g. 200/404) diff --git a/src/Message/ResponseException.php b/src/Message/ResponseException.php index 88272242..932290d4 100644 --- a/src/Message/ResponseException.php +++ b/src/Message/ResponseException.php @@ -14,7 +14,7 @@ * The `getCode(): int` method can be used to * return the HTTP response status code. */ -class ResponseException extends RuntimeException +final class ResponseException extends RuntimeException { private $response; diff --git a/src/Message/ServerRequest.php b/src/Message/ServerRequest.php index b0d64498..5c01819b 100644 --- a/src/Message/ServerRequest.php +++ b/src/Message/ServerRequest.php @@ -29,7 +29,7 @@ * * @see ServerRequestInterface */ -class ServerRequest extends Request implements ServerRequestInterface +final class ServerRequest extends Request implements ServerRequestInterface { private $attributes = array(); From 86e0f8003f88d527d71513d4e28f5db77e64fe1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 11 Jul 2020 00:21:02 +0200 Subject: [PATCH 333/456] Update all documentation references to PSR-7 interfaces --- README.md | 127 ++++++++++++++---------------- src/Browser.php | 9 +-- src/Message/Response.php | 7 +- src/Message/ResponseException.php | 2 +- 4 files changed, 68 insertions(+), 77 deletions(-) diff --git a/README.md b/README.md index a17b2521..763f025b 100644 --- a/README.md +++ b/README.md @@ -76,8 +76,6 @@ multiple concurrent HTTP requests without blocking. * [LimitConcurrentRequestsMiddleware](#limitconcurrentrequestsmiddleware) * [RequestBodyBufferMiddleware](#requestbodybuffermiddleware) * [RequestBodyParserMiddleware](#requestbodyparsermiddleware) - * [ResponseInterface](#responseinterface) - * [RequestInterface](#requestinterface) * [ResponseException](#responseexception) * [Install](#install) * [Tests](#tests) @@ -160,14 +158,16 @@ method. If you want to use any other or even custom HTTP request method, you can use the [`request()`](#request) method. Each of the above methods supports async operation and either *fulfills* with a -[`ResponseInterface`](#responseinterface) or *rejects* with an `Exception`. +[PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface) +or *rejects* with an `Exception`. Please see the following chapter about [promises](#promises) for more details. ### Promises Sending requests is async (non-blocking), so you can actually send multiple requests in parallel. -The `Browser` will respond to each request with a [`ResponseInterface`](#responseinterface) +The `Browser` will respond to each request with a +[PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface) message, the order is not guaranteed. Sending requests uses a [Promise](https://github.com/reactphp/promise)-based interface that makes it easy to react to when an HTTP request is completed @@ -474,11 +474,12 @@ the response body in small chunks as data is received and forwards this data through [ReactPHP's Stream API](https://github.com/reactphp/stream). This works for (any number of) responses of arbitrary sizes. -This means it resolves with a normal [`ResponseInterface`](#responseinterface), +This means it resolves with a normal +[PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface), which can be used to access the response message parameters as usual. You can access the message body as usual, however it now also -implements ReactPHP's [`ReadableStreamInterface`](https://github.com/reactphp/stream#readablestreaminterface) -as well as parts of the PSR-7's [`StreamInterface`](https://www.php-fig.org/psr/psr-7/#3-4-psr-http-message-streaminterface). +implements [ReactPHP's `ReadableStreamInterface`](https://github.com/reactphp/stream#readablestreaminterface) +as well as parts of the [PSR-7 `StreamInterface`](https://www.php-fig.org/psr/psr-7/#34-psrhttpmessagestreaminterface). ```php $browser->requestStreaming('GET', $url)->then(function (Psr\Http\Message\ResponseInterface $response) { @@ -568,7 +569,7 @@ Besides streaming the response body, you can also stream the request body. This can be useful if you want to send big POST requests (uploading files etc.) or process many outgoing streams at once. Instead of passing the body as a string, you can simply pass an instance -implementing ReactPHP's [`ReadableStreamInterface`](https://github.com/reactphp/stream#readablestreaminterface) +implementing [ReactPHP's `ReadableStreamInterface`](https://github.com/reactphp/stream#readablestreaminterface) to the [request methods](#request-methods) like this: ```php @@ -620,7 +621,7 @@ $connector = new React\Socket\Connector($loop, array( $browser = new React\Http\Browser($loop, $connector); ``` -See also the [HTTP CONNECT proxy example](examples/11-http-connect-proxy.php). +See also the [HTTP CONNECT proxy example](examples/11-client-http-connect-proxy.php). ### SOCKS proxy @@ -647,7 +648,7 @@ $connector = new React\Socket\Connector($loop, array( $browser = new React\Http\Browser($loop, $connector); ``` -See also the [SOCKS proxy example](examples/12-socks-proxy.php). +See also the [SOCKS proxy example](examples/12-client-socks-proxy.php). ### SSH proxy @@ -676,7 +677,7 @@ $connector = new React\Socket\Connector($loop, array( $browser = new React\Http\Browser($loop, $connector); ``` -See also the [SSH proxy example](examples/13-ssh-proxy.php). +See also the [SSH proxy example](examples/13-client-ssh-proxy.php). ### Unix domain sockets @@ -701,7 +702,7 @@ $client->get('http://localhost/info')->then(function (Psr\Http\Message\ResponseI }); ``` -See also the [Unix Domain Sockets (UDS) example](examples/14-unix-domain-sockets.php). +See also the [Unix Domain Sockets (UDS) example](examples/14-client-unix-domain-sockets.php). ## Server Usage @@ -916,9 +917,9 @@ incoming connections and then processing each incoming HTTP request. The request object will be processed once the request has been received by the client. This request object implements the -[PSR-7 ServerRequestInterface](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-7-http-message.md#321-psrhttpmessageserverrequestinterface) +[PSR-7 `ServerRequestInterface`](https://www.php-fig.org/psr/psr-7/#321-psrhttpmessageserverrequestinterface) which in turn extends the -[PSR-7 RequestInterface](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-7-http-message.md#32-psrhttpmessagerequestinterface) +[PSR-7 `RequestInterface`](https://www.php-fig.org/psr/psr-7/#32-psrhttpmessagerequestinterface) and will be passed to the callback function like this. ```php @@ -937,9 +938,9 @@ $server = new Server($loop, function (ServerRequestInterface $request) { ``` For more details about the request object, also check out the documentation of -[PSR-7 ServerRequestInterface](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-7-http-message.md#321-psrhttpmessageserverrequestinterface) +[PSR-7 `ServerRequestInterface`](https://www.php-fig.org/psr/psr-7/#321-psrhttpmessageserverrequestinterface) and -[PSR-7 RequestInterface](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-7-http-message.md#32-psrhttpmessagerequestinterface). +[PSR-7 `RequestInterface`](https://www.php-fig.org/psr/psr-7/#32-psrhttpmessagerequestinterface). #### Request parameters @@ -1149,16 +1150,19 @@ access the request body stream. In the streaming mode, this method returns a stream instance that implements both the [PSR-7 `StreamInterface`](https://www.php-fig.org/psr/psr-7/#34-psrhttpmessagestreaminterface) and the [ReactPHP `ReadableStreamInterface`](https://github.com/reactphp/stream#readablestreaminterface). -However, most of the PSR-7 `StreamInterface` methods have been -designed under the assumption of being in control of a synchronous request body. +However, most of the +[PSR-7 `StreamInterface`](https://www.php-fig.org/psr/psr-7/#34-psrhttpmessagestreaminterface) +methods have been designed under the assumption of being in control of a +synchronous request body. Given that this does not apply to this server, the following -PSR-7 `StreamInterface` methods are not used and SHOULD NOT be called: +[PSR-7 `StreamInterface`](https://www.php-fig.org/psr/psr-7/#34-psrhttpmessagestreaminterface) +methods are not used and SHOULD NOT be called: `tell()`, `eof()`, `seek()`, `rewind()`, `write()` and `read()`. If this is an issue for your use case and/or you want to access uploaded files, it's highly recommended to use a buffered [request body](#request-body) or use the [`RequestBodyBufferMiddleware`](#requestbodybuffermiddleware) instead. -The ReactPHP `ReadableStreamInterface` gives you access to the incoming -request body as the individual chunks arrive: +The [ReactPHP `ReadableStreamInterface`](https://github.com/reactphp/stream#readablestreaminterface) +gives you access to the incoming request body as the individual chunks arrive: ```php $server = new React\Http\Server( @@ -1223,7 +1227,7 @@ A response message can still be sent (unless the connection is already closed). A `close` event will be emitted after an `error` or `end` event. For more details about the request body stream, check out the documentation of -[ReactPHP ReadableStreamInterface](https://github.com/reactphp/stream#readablestreaminterface). +[ReactPHP `ReadableStreamInterface`](https://github.com/reactphp/stream#readablestreaminterface). The `getSize(): ?int` method can be used to get the size of the request body, similar to PHP's `$_SERVER['CONTENT_LENGTH']` variable. @@ -1371,13 +1375,14 @@ responsible for processing the request and returning a response, which will be delivered to the client. This function MUST return an instance implementing -[PSR-7 `ResponseInterface`](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-7-http-message.md#33-psrhttpmessageresponseinterface) +[PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface) object or a [ReactPHP Promise](https://github.com/reactphp/promise) -which resolves with a PSR-7 `ResponseInterface` object. +which resolves with a [PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface) object. -This projects ships a [`Response` class](#response) which implements the PSR-7 -`ResponseInterface`. In its most simple form, you can use it like this: +This projects ships a [`Response` class](#response) which implements the +[PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface). +In its most simple form, you can use it like this: ```php $server = new React\Http\Server($loop, function (ServerRequestInterface $request) { @@ -1392,7 +1397,8 @@ $server = new React\Http\Server($loop, function (ServerRequestInterface $request ``` We use this [`Response` class](#response) throughout our project examples, but -feel free to use any other implementation of the PSR-7 `ResponseInterface`. +feel free to use any other implementation of the +[PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface). See also the [`Response` class](#response) for more details. #### Deferred response @@ -1437,11 +1443,12 @@ If a promise is resolved after the client closes, it will simply be ignored. #### Streaming outgoing response The `Response` class in this project supports to add an instance which implements the -[ReactPHP ReadableStreamInterface](https://github.com/reactphp/stream#readablestreaminterface) +[ReactPHP `ReadableStreamInterface`](https://github.com/reactphp/stream#readablestreaminterface) for the response body. So you are able stream data directly into the response body. -Note that other implementations of the `PSR-7 ResponseInterface` likely -only support strings. +Note that other implementations of the +[PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface) +may only support strings. ```php $server = new Server($loop, function (ServerRequestInterface $request) use ($loop) { @@ -1494,8 +1501,9 @@ in this case (if applicable). writable side of the stream. This can be avoided by either rejecting all requests with the `CONNECT` method (which is what most *normal* origin HTTP servers would likely do) or - or ensuring that only ever an instance of `ReadableStreamInterface` is - used. + or ensuring that only ever an instance of + [ReactPHP's `ReadableStreamInterface`](https://github.com/reactphp/stream#readablestreaminterface) + is used. > > The `101` (Switching Protocols) response code is useful for the more advanced `Upgrade` requests, such as upgrading to the WebSocket protocol or @@ -1711,13 +1719,17 @@ As such, this project supports the concept of middleware request handlers. A middleware request handler is expected to adhere the following rules: * It is a valid `callable`. -* It accepts `ServerRequestInterface` as first argument and an optional - `callable` as second argument. +* It accepts an instance implementing + [PSR-7 `ServerRequestInterface`](https://www.php-fig.org/psr/psr-7/#321-psrhttpmessageserverrequestinterface) + as first argument and an optional `callable` as second argument. * It returns either: - * An instance implementing `ResponseInterface` for direct consumption. + * An instance implementing + [PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface) + for direct consumption. * Any promise which can be consumed by [`Promise\resolve()`](https://reactphp.org/promise/#resolve) resolving to a - `ResponseInterface` for deferred consumption. + [PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface) + for deferred consumption. * It MAY throw an `Exception` (or return a rejected promise) in order to signal an error condition and abort the chain. * It calls `$next($request)` to continue processing the next middleware @@ -1772,8 +1784,8 @@ $server = new Server( Similarly, you can use the result of the `$next` middleware request handler function to modify the outgoing response. Note that as per the above documentation, the `$next` middleware request handler may return a -`ResponseInterface` directly or one wrapped in a promise for deferred -resolution. +[PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface) +directly or one wrapped in a promise for deferred resolution. In order to simplify handling both paths, you can simply wrap this in a [`Promise\resolve()`](https://reactphp.org/promise/#resolve) call like this: @@ -1840,8 +1852,9 @@ encourages third-party middleware implementations. While we would love to support PSR-15 directly in `react/http`, we understand that this interface does not specifically target async APIs and as such does not take advantage of promises for [deferred responses](#deferred-response). -The gist of this is that where PSR-15 enforces a `ResponseInterface` return -value, we also accept a `PromiseInterface`. +The gist of this is that where PSR-15 enforces a +[PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface) +return value, we also accept a `PromiseInterface`. As such, we suggest using the external [PSR-15 middleware adapter](https://github.com/friends-of-reactphp/http-middleware-psr15-adapter) that uses on the fly monkey patching of these return values which makes using @@ -2116,7 +2129,7 @@ $browser->requestStreaming('GET', $url)->then(function (Psr\Http\Message\Respons }); ``` -See also [`ReadableStreamInterface`](https://github.com/reactphp/stream#readablestreaminterface) +See also [ReactPHP's `ReadableStreamInterface`](https://github.com/reactphp/stream#readablestreaminterface) and the [streaming response](#streaming-response) for more details, examples and possible use-cases. @@ -2381,10 +2394,9 @@ This class implements the which in turn extends the [PSR-7 `MessageInterface`](https://www.php-fig.org/psr/psr-7/#31-psrhttpmessagemessageinterface). -> Internally, this class extends the underlying `\RingCentral\Psr7\Response` - class. The only difference is that this class will accept implemenations - of ReactPHPs `ReadableStreamInterface` for the `$body` argument. This base - class is considered an implementation detail that may change in the future. +> Internally, this implementation builds on top of an existing incoming + response message and only adds required streaming support. This base class is + considered an implementation detail that may change in the future. #### ServerRequest @@ -2593,7 +2605,8 @@ Instead of relying on these superglobals, you can use the `$request->getParsedBody()` and `$request->getUploadedFiles()` methods as defined by PSR-7. -Accordingly, each file upload will be represented as instance implementing [`UploadedFileInterface`](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-7-http-message.md#36-psrhttpmessageuploadedfileinterface). +Accordingly, each file upload will be represented as instance implementing the +[PSR-7 `UploadedFileInterface`](https://www.php-fig.org/psr/psr-7/#36-psrhttpmessageuploadedfileinterface). Due to its blocking nature, the `moveTo()` method is not available and throws a `RuntimeException` instead. You can use `$contents = (string)$file->getStream();` to access the file @@ -2697,26 +2710,6 @@ new RequestBodyParserMiddleware(10 * 1024, 100); // 100 files with 10 KiB each If you want to respect this setting, you have to check its value and effectively avoid using this middleware entirely. -### ResponseInterface - -The `Psr\Http\Message\ResponseInterface` represents the incoming response received from the [`Browser`](#browser). - -This is a standard interface defined in -[PSR-7: HTTP message interfaces](https://www.php-fig.org/psr/psr-7/), see its -[`ResponseInterface` definition](https://www.php-fig.org/psr/psr-7/#3-3-psr-http-message-responseinterface) -which in turn extends the -[`MessageInterface` definition](https://www.php-fig.org/psr/psr-7/#3-1-psr-http-message-messageinterface). - -### RequestInterface - -The `Psr\Http\Message\RequestInterface` represents the outgoing request to be sent via the [`Browser`](#browser). - -This is a standard interface defined in -[PSR-7: HTTP message interfaces](https://www.php-fig.org/psr/psr-7/), see its -[`RequestInterface` definition](https://www.php-fig.org/psr/psr-7/#3-2-psr-http-message-requestinterface) -which in turn extends the -[`MessageInterface` definition](https://www.php-fig.org/psr/psr-7/#3-1-psr-http-message-messageinterface). - ### ResponseException The `ResponseException` is an `Exception` sub-class that will be used to reject @@ -2728,7 +2721,7 @@ The `getCode(): int` method can be used to return the HTTP response status code. The `getResponse(): ResponseInterface` method can be used to -access its underlying [`ResponseInterface`](#responseinterface) object. +access its underlying response object. ## Install diff --git a/src/Browser.php b/src/Browser.php index 9da950ba..d222ac38 100644 --- a/src/Browser.php +++ b/src/Browser.php @@ -2,16 +2,15 @@ namespace React\Http; +use Psr\Http\Message\ResponseInterface; +use React\EventLoop\LoopInterface; use React\Http\Io\Sender; use React\Http\Io\Transaction; use React\Http\Message\MessageFactory; -use InvalidArgumentException; -use Psr\Http\Message\RequestInterface; -use Psr\Http\Message\UriInterface; -use React\EventLoop\LoopInterface; use React\Promise\PromiseInterface; use React\Socket\ConnectorInterface; use React\Stream\ReadableStreamInterface; +use InvalidArgumentException; /** * @final This class is final and shouldn't be extended as it is likely to be marked final in a future relase. @@ -719,7 +718,7 @@ private function withOptions(array $options) /** * @param string $method - * @param string|UriInterface $url + * @param string $url * @param array $headers * @param string|ReadableStreamInterface $contents * @return PromiseInterface diff --git a/src/Message/Response.php b/src/Message/Response.php index 429cb120..5d799c58 100644 --- a/src/Message/Response.php +++ b/src/Message/Response.php @@ -25,10 +25,9 @@ * which in turn extends the * [PSR-7 `MessageInterface`](https://www.php-fig.org/psr/psr-7/#31-psrhttpmessagemessageinterface). * - * > Internally, this class extends the underlying `\RingCentral\Psr7\Response` - * class. The only difference is that this class will accept implemenations - * of ReactPHPs `ReadableStreamInterface` for the `$body` argument. This base - * class is considered an implementation detail that may change in the future. + * > Internally, this implementation builds on top of an existing incoming + * response message and only adds required streaming support. This base class is + * considered an implementation detail that may change in the future. * * @see \Psr\Http\Message\ResponseInterface */ diff --git a/src/Message/ResponseException.php b/src/Message/ResponseException.php index 932290d4..ab488a1b 100644 --- a/src/Message/ResponseException.php +++ b/src/Message/ResponseException.php @@ -32,7 +32,7 @@ public function __construct(ResponseInterface $response, $message = null, $code } /** - * Access its underlying [`ResponseInterface`](#responseinterface) object. + * Access its underlying response object. * * @return ResponseInterface */ From cc94133d4c3156b0442c963c093814c456415ddb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 11 Jul 2020 00:35:27 +0200 Subject: [PATCH 334/456] Use fully-qualified class names throughout the documentation --- README.md | 148 +++++++++--------- src/Message/ResponseException.php | 2 +- .../LimitConcurrentRequestsMiddleware.php | 28 ++-- src/Server.php | 6 +- 4 files changed, 92 insertions(+), 92 deletions(-) diff --git a/README.md b/README.md index 763f025b..1e405917 100644 --- a/README.md +++ b/README.md @@ -709,7 +709,7 @@ See also the [Unix Domain Sockets (UDS) example](examples/14-client-unix-domain- ### Server -The `Server` class is responsible for handling incoming connections and then +The `React\Http\Server` class is responsible for handling incoming connections and then processing each incoming HTTP request. When a complete HTTP request has been received, it will invoke the given @@ -923,11 +923,11 @@ which in turn extends the and will be passed to the callback function like this. ```php -$server = new Server($loop, function (ServerRequestInterface $request) { +$server = new React\Http\Server($loop, function (Psr\Http\Message\ServerRequestInterface $request) { $body = "The method of the request is: " . $request->getMethod(); $body .= "The requested path is: " . $request->getUri()->getPath(); - return new Response( + return new React\Http\Message\Response( 200, array( 'Content-Type' => 'text/plain' @@ -966,10 +966,10 @@ The following parameters are currently available: Set to 'on' if the request used HTTPS, otherwise it won't be set ```php -$server = new Server($loop, function (ServerRequestInterface $request) { +$server = new React\Http\Server($loop, function (Psr\Http\Message\ServerRequestInterface $request) { $body = "Your IP is: " . $request->getServerParams()['REMOTE_ADDR']; - return new Response( + return new React\Http\Message\Response( 200, array( 'Content-Type' => 'text/plain' @@ -991,7 +991,7 @@ The `getQueryParams(): array` method can be used to get the query parameters similiar to the `$_GET` variable. ```php -$server = new Server($loop, function (ServerRequestInterface $request) { +$server = new React\Http\Server($loop, function (Psr\Http\Message\ServerRequestInterface $request) { $queryParams = $request->getQueryParams(); $body = 'The query parameter "foo" is not set. Click the following link '; @@ -1001,7 +1001,7 @@ $server = new Server($loop, function (ServerRequestInterface $request) { $body = 'The value of "foo" is: ' . htmlspecialchars($queryParams['foo']); } - return new Response( + return new React\Http\Message\Response( 200, array( 'Content-Type' => 'text/html' @@ -1045,10 +1045,10 @@ By default, this method will only return parsed data for requests using request headers (commonly used for `POST` requests for HTML form submission data). ```php -$server = new Server($loop, function (ServerRequestInterface $request) { +$server = new React\Http\Server($loop, function (Psr\Http\Message\ServerRequestInterface $request) { $name = $request->getParsedBody()['name'] ?? 'anonymous'; - return new Response( + return new React\Http\Message\Response( 200, array(), "Hello $name!\n" @@ -1069,11 +1069,11 @@ an XML (`Content-Type: application/xml`) request body (which is commonly used fo `POST`, `PUT` or `PATCH` requests in JSON-based or RESTful/RESTish APIs). ```php -$server = new Server($loop, function (ServerRequestInterface $request) { +$server = new React\Http\Server($loop, function (Psr\Http\Message\ServerRequestInterface $request) { $data = json_decode((string)$request->getBody()); $name = $data->name ?? 'anonymous'; - return new Response( + return new React\Http\Message\Response( 200, array('Content-Type' => 'application/json'), json_encode(['message' => "Hello $name!"]) @@ -1092,11 +1092,11 @@ This array will only be filled when using the `Content-Type: multipart/form-data request header (commonly used for `POST` requests for HTML file uploads). ```php -$server = new Server($loop, function (ServerRequestInterface $request) { +$server = new React\Http\Server($loop, function (Psr\Http\Message\ServerRequestInterface $request) { $files = $request->getUploadedFiles(); $name = isset($files['avatar']) ? $files['avatar']->getClientFilename() : 'nothing'; - return new Response( + return new React\Http\Message\Response( 200, array(), "Uploaded $name\n" @@ -1313,13 +1313,13 @@ The `getCookieParams(): string[]` method can be used to get all cookies sent with the current request. ```php -$server = new Server($loop, function (ServerRequestInterface $request) { +$server = new React\Http\Server($loop, function (Psr\Http\Message\ServerRequestInterface $request) { $key = 'react\php'; if (isset($request->getCookieParams()[$key])) { $body = "Your cookie value is: " . $request->getCookieParams()[$key]; - return new Response( + return new React\Http\Message\Response( 200, array( 'Content-Type' => 'text/plain' @@ -1328,7 +1328,7 @@ $server = new Server($loop, function (ServerRequestInterface $request) { ); } - return new Response( + return new React\Http\Message\Response( 200, array( 'Content-Type' => 'text/plain', @@ -1385,7 +1385,7 @@ This projects ships a [`Response` class](#response) which implements the In its most simple form, you can use it like this: ```php -$server = new React\Http\Server($loop, function (ServerRequestInterface $request) { +$server = new React\Http\Server($loop, function (Psr\Http\Message\ServerRequestInterface $request) { return new React\Http\Message\Response( 200, array( @@ -1413,10 +1413,10 @@ To prevent this you SHOULD use a This example shows how such a long-term action could look like: ```php -$server = new Server($loop, function (ServerRequestInterface $request) use ($loop) { +$server = new React\Http\Server($loop, function (Psr\Http\Message\ServerRequestInterface $request) use ($loop) { return new Promise(function ($resolve, $reject) use ($loop) { $loop->addTimer(1.5, function() use ($resolve) { - $response = new Response( + $response = new React\Http\Message\Response( 200, array( 'Content-Type' => 'text/plain' @@ -1451,7 +1451,7 @@ Note that other implementations of the may only support strings. ```php -$server = new Server($loop, function (ServerRequestInterface $request) use ($loop) { +$server = new React\Http\Server($loop, function (Psr\Http\Message\ServerRequestInterface $request) use ($loop) { $stream = new ThroughStream(); $timer = $loop->addPeriodicTimer(0.5, function () use ($stream) { @@ -1463,7 +1463,7 @@ $server = new Server($loop, function (ServerRequestInterface $request) use ($loo $stream->end(); }); - return new Response( + return new React\Http\Message\Response( 200, array( 'Content-Type' => 'text/plain' @@ -1543,8 +1543,8 @@ added automatically. This is the most common use case, for example when using a `string` response body like this: ```php -$server = new Server($loop, function (ServerRequestInterface $request) { - return new Response( +$server = new React\Http\Server($loop, function (Psr\Http\Message\ServerRequestInterface $request) { + return new React\Http\Message\Response( 200, array( 'Content-Type' => 'text/plain' @@ -1562,14 +1562,14 @@ response messages will contain the plain response body. If you know the length of your streaming response body, you MAY want to specify it explicitly like this: ```php -$server = new Server($loop, function (ServerRequestInterface $request) use ($loop) { +$server = new React\Http\Server($loop, function (Psr\Http\Message\ServerRequestInterface $request) use ($loop) { $stream = new ThroughStream(); $loop->addTimer(2.0, function () use ($stream) { $stream->end("Hello World!\n"); }); - return new Response( + return new React\Http\Message\Response( 200, array( 'Content-Length' => '13', @@ -1765,14 +1765,14 @@ The following example adds a middleware request handler that adds the current ti header (`Request-Time`) and a final request handler that always returns a 200 code without a body: ```php -$server = new Server( +$server = new React\Http\Server( $loop, - function (ServerRequestInterface $request, callable $next) { + function (Psr\Http\Message\ServerRequestInterface $request, callable $next) { $request = $request->withHeader('Request-Time', time()); return $next($request); }, - function (ServerRequestInterface $request) { - return new Response(200); + function (Psr\Http\Message\ServerRequestInterface $request) { + return new React\Http\Message\Response(200); } ); ``` @@ -1790,16 +1790,16 @@ In order to simplify handling both paths, you can simply wrap this in a [`Promise\resolve()`](https://reactphp.org/promise/#resolve) call like this: ```php -$server = new Server( +$server = new React\Http\Server( $loop, - function (ServerRequestInterface $request, callable $next) { + function (Psr\Http\Message\ServerRequestInterface $request, callable $next) { $promise = React\Promise\resolve($next($request)); return $promise->then(function (ResponseInterface $response) { return $response->withHeader('Content-Type', 'text/html'); }); }, - function (ServerRequestInterface $request) { - return new Response(200); + function (Psr\Http\Message\ServerRequestInterface $request) { + return new React\Http\Message\Response(200); } ); ``` @@ -1813,25 +1813,25 @@ handling logic (or logging etc.) by wrapping this in a [`Promise`](https://reactphp.org/promise/#promise) like this: ```php -$server = new Server( +$server = new React\Http\Server( $loop, - function (ServerRequestInterface $request, callable $next) { + function (Psr\Http\Message\ServerRequestInterface $request, callable $next) { $promise = new React\Promise\Promise(function ($resolve) use ($next, $request) { $resolve($next($request)); }); return $promise->then(null, function (Exception $e) { - return new Response( + return new React\Http\Message\Response( 500, array(), 'Internal error: ' . $e->getMessage() ); }); }, - function (ServerRequestInterface $request) { + function (Psr\Http\Message\ServerRequestInterface $request) { if (mt_rand(0, 1) === 1) { throw new RuntimeException('Database error'); } - return new Response(200); + return new React\Http\Message\Response(200); } ); ``` @@ -2376,7 +2376,7 @@ given setting applied. #### Response -The `Response` class can be used to +The `React\Http\Message\Response` class can be used to represent an outgoing server response message. ```php @@ -2400,7 +2400,7 @@ which in turn extends the #### ServerRequest -The `ServerRequest` class can be used to +The `React\Http\Message\ServerRequest` class can be used to respresent an incoming server request message. This class implements the @@ -2422,7 +2422,7 @@ application reacts to certain HTTP requests. #### StreamingRequestMiddleware -The `StreamingRequestMiddleware` can be used to +The `React\Http\Middleware\StreamingRequestMiddleware` can be used to process incoming requests with a streaming request body (without buffering). This allows you to process requests of any size without buffering the request @@ -2480,7 +2480,7 @@ $server = new React\Http\Server(array( #### LimitConcurrentRequestsMiddleware -The `LimitConcurrentRequestsMiddleware` can be used to +The `React\Http\Middleware\LimitConcurrentRequestsMiddleware` can be used to limit how many next handlers can be executed concurrently. If this middleware is invoked, it will check if the number of pending @@ -2498,9 +2498,9 @@ The following example shows how this middleware can be used to ensure no more than 10 handlers will be invoked at once: ```php -$server = new Server( +$server = new React\Http\Server( $loop, - new LimitConcurrentRequestsMiddleware(10), + new React\Http\Middleware\LimitConcurrentRequestsMiddleware(10), $handler ); ``` @@ -2510,12 +2510,12 @@ Similarly, this middleware is often used in combination with the to limit the total number of requests that can be buffered at once: ```php -$server = new Server( +$server = new React\Http\Server( $loop, - new StreamingRequestMiddleware(), - new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers - new RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request - new RequestBodyParserMiddleware(), + new React\Http\Middleware\StreamingRequestMiddleware(), + new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers + new React\Http\Middleware\RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request + new React\Http\Middleware\RequestBodyParserMiddleware(), $handler ); ``` @@ -2525,20 +2525,20 @@ that can be buffered at once and then ensure the actual request handler only processes one request after another without any concurrency: ```php -$server = new Server( +$server = new React\Http\Server( $loop, - new StreamingRequestMiddleware(), - new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers - new RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request - new RequestBodyParserMiddleware(), - new LimitConcurrentRequestsMiddleware(1), // only execute 1 handler (no concurrency) + new React\Http\Middleware\StreamingRequestMiddleware(), + new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers + new React\Http\Middleware\RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request + new React\Http\Middleware\RequestBodyParserMiddleware(), + new React\Http\Middleware\LimitConcurrentRequestsMiddleware(1), // only execute 1 handler (no concurrency) $handler ); ``` #### RequestBodyBufferMiddleware -One of the built-in middleware is the `RequestBodyBufferMiddleware` which +One of the built-in middleware is the `React\Http\Middleware\RequestBodyBufferMiddleware` which can be used to buffer the whole incoming request body in memory. This can be useful if full PSR-7 compatibility is needed for the request handler and the default streaming request body handling is not needed. @@ -2579,21 +2579,21 @@ the total number of concurrent requests. Usage: ```php -$server = new Server( +$server = new React\Http\Server( $loop, - new StreamingRequestMiddleware(), - new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers - new RequestBodyBufferMiddleware(16 * 1024 * 1024), // 16 MiB - function (ServerRequestInterface $request) { + new React\Http\Middleware\StreamingRequestMiddleware(), + new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers + new React\Http\Middleware\RequestBodyBufferMiddleware(16 * 1024 * 1024), // 16 MiB + function (Psr\Http\Message\ServerRequestInterface $request) { // The body from $request->getBody() is now fully available without the need to stream it - return new Response(200); + return new React\Http\Message\Response(200); }, ); ``` #### RequestBodyParserMiddleware -The `RequestBodyParserMiddleware` takes a fully buffered request body +The `React\Http\Middleware\RequestBodyParserMiddleware` takes a fully buffered request body (generally from [`RequestBodyBufferMiddleware`](#requestbodybuffermiddleware)), and parses the form values and file uploads from the incoming HTTP request body. @@ -2613,14 +2613,14 @@ You can use `$contents = (string)$file->getStream();` to access the file contents and persist this to your favorite data store. ```php -$handler = function (ServerRequestInterface $request) { +$handler = function (Psr\Http\Message\ServerRequestInterface $request) { // If any, parsed form fields are now available from $request->getParsedBody() $body = $request->getParsedBody(); $name = isset($body['name']) ? $body['name'] : 'unnamed'; $files = $request->getUploadedFiles(); $avatar = isset($files['avatar']) ? $files['avatar'] : null; - if ($avatar instanceof UploadedFileInterface) { + if ($avatar instanceof Psr\Http\Message\UploadedFileInterface) { if ($avatar->getError() === UPLOAD_ERR_OK) { $uploaded = $avatar->getSize() . ' bytes'; } elseif ($avatar->getError() === UPLOAD_ERR_INI_SIZE) { @@ -2632,7 +2632,7 @@ $handler = function (ServerRequestInterface $request) { $uploaded = 'nothing'; } - return new Response( + return new React\Http\Message\Response( 200, array( 'Content-Type' => 'text/plain' @@ -2641,12 +2641,12 @@ $handler = function (ServerRequestInterface $request) { ); }; -$server = new Server( +$server = new React\Http\Server( $loop, - new StreamingRequestMiddleware(), - new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers - new RequestBodyBufferMiddleware(16 * 1024 * 1024), // 16 MiB - new RequestBodyParserMiddleware(), + new React\Http\Middleware\StreamingRequestMiddleware(), + new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers + new React\Http\Middleware\RequestBodyBufferMiddleware(16 * 1024 * 1024), // 16 MiB + new React\Http\Middleware\RequestBodyParserMiddleware(), $handler ); ``` @@ -2662,7 +2662,7 @@ explicitly passing the maximum filesize in bytes as the first parameter to the constructor like this: ```php -new RequestBodyParserMiddleware(8 * 1024 * 1024); // 8 MiB limit per file +new React\Http\Middleware\RequestBodyParserMiddleware(8 * 1024 * 1024); // 8 MiB limit per file ``` By default, this middleware respects the @@ -2678,7 +2678,7 @@ You can control the maximum number of file uploads per request by explicitly passing the second parameter to the constructor like this: ```php -new RequestBodyParserMiddleware(10 * 1024, 100); // 100 files with 10 KiB each +new React\Http\Middleware\RequestBodyParserMiddleware(10 * 1024, 100); // 100 files with 10 KiB each ``` > Note that this middleware handler simply parses everything that is already @@ -2712,7 +2712,7 @@ new RequestBodyParserMiddleware(10 * 1024, 100); // 100 files with 10 KiB each ### ResponseException -The `ResponseException` is an `Exception` sub-class that will be used to reject +The `React\Http\Message\ResponseException` is an `Exception` sub-class that will be used to reject a request promise if the remote server returns a non-success status code (anything but 2xx or 3xx). You can control this behavior via the [`withRejectErrorResponse()` method](#withrejecterrorresponse). diff --git a/src/Message/ResponseException.php b/src/Message/ResponseException.php index ab488a1b..f4912c90 100644 --- a/src/Message/ResponseException.php +++ b/src/Message/ResponseException.php @@ -6,7 +6,7 @@ use Psr\Http\Message\ResponseInterface; /** - * The `ResponseException` is an `Exception` sub-class that will be used to reject + * The `React\Http\Message\ResponseException` is an `Exception` sub-class that will be used to reject * a request promise if the remote server returns a non-success status code * (anything but 2xx or 3xx). * You can control this behavior via the [`withRejectErrorResponse()` method](#withrejecterrorresponse). diff --git a/src/Middleware/LimitConcurrentRequestsMiddleware.php b/src/Middleware/LimitConcurrentRequestsMiddleware.php index d6760e95..9aaf5ff2 100644 --- a/src/Middleware/LimitConcurrentRequestsMiddleware.php +++ b/src/Middleware/LimitConcurrentRequestsMiddleware.php @@ -29,10 +29,10 @@ * than 10 handlers will be invoked at once: * * ```php - * $server = new Server( + * $server = new React\Http\Server( * $loop, - * new StreamingRequestMiddleware(), - * new LimitConcurrentRequestsMiddleware(10), + * new React\Http\Middleware\StreamingRequestMiddleware(), + * new React\Http\Middleware\LimitConcurrentRequestsMiddleware(10), * $handler * ); * ``` @@ -42,12 +42,12 @@ * to limit the total number of requests that can be buffered at once: * * ```php - * $server = new Server( + * $server = new React\Http\Server( * $loop, - * new StreamingRequestMiddleware(), - * new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers - * new RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request - * new RequestBodyParserMiddleware(), + * new React\Http\Middleware\StreamingRequestMiddleware(), + * new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers + * new React\Http\Middleware\RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request + * new React\Http\Middleware\RequestBodyParserMiddleware(), * $handler * ); * ``` @@ -57,13 +57,13 @@ * processes one request after another without any concurrency: * * ```php - * $server = new Server( + * $server = new React\Http\Server( * $loop, - * new StreamingRequestMiddleware(), - * new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers - * new RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request - * new RequestBodyParserMiddleware(), - * new LimitConcurrentRequestsMiddleware(1), // only execute 1 handler (no concurrency) + * new React\Http\Middleware\StreamingRequestMiddleware(), + * new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers + * new React\Http\Middleware\RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request + * new React\Http\Middleware\RequestBodyParserMiddleware(), + * new React\Http\Middleware\LimitConcurrentRequestsMiddleware(1), // only execute 1 handler (no concurrency) * $handler * ); * ``` diff --git a/src/Server.php b/src/Server.php index 3b5d70be..1aa5d405 100644 --- a/src/Server.php +++ b/src/Server.php @@ -14,7 +14,7 @@ use React\Socket\ServerInterface; /** - * The `Server` class is responsible for handling incoming connections and then + * The `React\Http\Server` class is responsible for handling incoming connections and then * processing each incoming HTTP request. * * When a complete HTTP request has been received, it will invoke the given @@ -292,9 +292,9 @@ public function __construct(LoopInterface $loop) * * @param ServerInterface $socket */ - public function listen(ServerInterface $server) + public function listen(ServerInterface $socket) { - $this->streamingServer->listen($server); + $this->streamingServer->listen($socket); } /** From 552dbdd477ac894be141eb83c58f972cadde6e44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 11 Jul 2020 00:47:11 +0200 Subject: [PATCH 335/456] Streaming `Message` documentation and move internal message classes --- README.md | 28 +++++++------- src/Browser.php | 2 +- src/{Message => Io}/MessageFactory.php | 2 +- src/{Message => Io}/ReadableBodyStream.php | 2 +- src/Io/Sender.php | 1 - src/Io/Transaction.php | 3 +- tests/{Message => Io}/MessageFactoryTest.php | 4 +- .../ReadableBodyStreamTest.php | 4 +- tests/Io/SenderTest.php | 38 +++++++++---------- tests/Io/TransactionTest.php | 8 ++-- 10 files changed, 45 insertions(+), 47 deletions(-) rename src/{Message => Io}/MessageFactory.php (99%) rename src/{Message => Io}/ReadableBodyStream.php (99%) rename tests/{Message => Io}/MessageFactoryTest.php (99%) rename tests/{Message => Io}/ReadableBodyStreamTest.php (98%) diff --git a/README.md b/README.md index 1e405917..4a72d9e7 100644 --- a/README.md +++ b/README.md @@ -71,12 +71,12 @@ multiple concurrent HTTP requests without blocking. * [React\Http\Message](#reacthttpmessage) * [Response](#response) * [ServerRequest](#serverrequest) + * [ResponseException](#responseexception) * [React\Http\Middleware](#reacthttpmiddleware) * [StreamingRequestMiddleware](#streamingrequestmiddleware) * [LimitConcurrentRequestsMiddleware](#limitconcurrentrequestsmiddleware) * [RequestBodyBufferMiddleware](#requestbodybuffermiddleware) * [RequestBodyParserMiddleware](#requestbodyparsermiddleware) - * [ResponseException](#responseexception) * [Install](#install) * [Tests](#tests) * [License](#license) @@ -2418,6 +2418,19 @@ application reacts to certain HTTP requests. request message and only adds required server methods. This base class is considered an implementation detail that may change in the future. +#### ResponseException + +The `React\Http\Message\ResponseException` is an `Exception` sub-class that will be used to reject +a request promise if the remote server returns a non-success status code +(anything but 2xx or 3xx). +You can control this behavior via the [`withRejectErrorResponse()` method](#withrejecterrorresponse). + +The `getCode(): int` method can be used to +return the HTTP response status code. + +The `getResponse(): ResponseInterface` method can be used to +access its underlying response object. + ### React\Http\Middleware #### StreamingRequestMiddleware @@ -2710,19 +2723,6 @@ new React\Http\Middleware\RequestBodyParserMiddleware(10 * 1024, 100); // 100 fi If you want to respect this setting, you have to check its value and effectively avoid using this middleware entirely. -### ResponseException - -The `React\Http\Message\ResponseException` is an `Exception` sub-class that will be used to reject -a request promise if the remote server returns a non-success status code -(anything but 2xx or 3xx). -You can control this behavior via the [`withRejectErrorResponse()` method](#withrejecterrorresponse). - -The `getCode(): int` method can be used to -return the HTTP response status code. - -The `getResponse(): ResponseInterface` method can be used to -access its underlying response object. - ## Install The recommended way to install this library is [through Composer](https://getcomposer.org). diff --git a/src/Browser.php b/src/Browser.php index d222ac38..46a5d115 100644 --- a/src/Browser.php +++ b/src/Browser.php @@ -4,9 +4,9 @@ use Psr\Http\Message\ResponseInterface; use React\EventLoop\LoopInterface; +use React\Http\Io\MessageFactory; use React\Http\Io\Sender; use React\Http\Io\Transaction; -use React\Http\Message\MessageFactory; use React\Promise\PromiseInterface; use React\Socket\ConnectorInterface; use React\Stream\ReadableStreamInterface; diff --git a/src/Message/MessageFactory.php b/src/Io/MessageFactory.php similarity index 99% rename from src/Message/MessageFactory.php rename to src/Io/MessageFactory.php index eaa144cd..14591e80 100644 --- a/src/Message/MessageFactory.php +++ b/src/Io/MessageFactory.php @@ -1,6 +1,6 @@ loop, null, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); + $sender = Sender::createFromLoop($this->loop, null, $this->getMockBuilder('React\Http\Io\MessageFactory')->getMock()); $this->assertInstanceOf('React\Http\Io\Sender', $sender); } @@ -36,7 +36,7 @@ public function testSenderRejectsInvalidUri() $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); $connector->expects($this->never())->method('connect'); - $sender = new Sender(new HttpClient($this->loop, $connector), $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); + $sender = new Sender(new HttpClient($this->loop, $connector), $this->getMockBuilder('React\Http\Io\MessageFactory')->getMock()); $request = new Request('GET', 'www.google.com'); @@ -51,7 +51,7 @@ public function testSenderConnectorRejection() $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); $connector->expects($this->once())->method('connect')->willReturn(Promise\reject(new \RuntimeException('Rejected'))); - $sender = new Sender(new HttpClient($this->loop, $connector), $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); + $sender = new Sender(new HttpClient($this->loop, $connector), $this->getMockBuilder('React\Http\Io\MessageFactory')->getMock()); $request = new Request('GET', 'http://www.google.com/'); @@ -71,7 +71,7 @@ public function testSendPostWillAutomaticallySendContentLengthHeader() '1.1' )->willReturn($this->getMockBuilder('React\Http\Client\Request')->disableOriginalConstructor()->getMock()); - $sender = new Sender($client, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); + $sender = new Sender($client, $this->getMockBuilder('React\Http\Io\MessageFactory')->getMock()); $request = new Request('POST', 'http://www.google.com/', array(), 'hello'); $sender->send($request); @@ -87,7 +87,7 @@ public function testSendPostWillAutomaticallySendContentLengthZeroHeaderForEmpty '1.1' )->willReturn($this->getMockBuilder('React\Http\Client\Request')->disableOriginalConstructor()->getMock()); - $sender = new Sender($client, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); + $sender = new Sender($client, $this->getMockBuilder('React\Http\Io\MessageFactory')->getMock()); $request = new Request('POST', 'http://www.google.com/', array(), ''); $sender->send($request); @@ -106,7 +106,7 @@ public function testSendPostStreamWillAutomaticallySendTransferEncodingChunked() '1.1' )->willReturn($outgoing); - $sender = new Sender($client, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); + $sender = new Sender($client, $this->getMockBuilder('React\Http\Io\MessageFactory')->getMock()); $stream = new ThroughStream(); $request = new Request('POST', 'http://www.google.com/', array(), new ReadableBodyStream($stream)); @@ -122,7 +122,7 @@ public function testSendPostStreamWillAutomaticallyPipeChunkEncodeBodyForWriteAn $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); $client->expects($this->once())->method('request')->willReturn($outgoing); - $sender = new Sender($client, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); + $sender = new Sender($client, $this->getMockBuilder('React\Http\Io\MessageFactory')->getMock()); $stream = new ThroughStream(); $request = new Request('POST', 'http://www.google.com/', array(), new ReadableBodyStream($stream)); @@ -142,7 +142,7 @@ public function testSendPostStreamWillAutomaticallyPipeChunkEncodeBodyForEnd() $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); $client->expects($this->once())->method('request')->willReturn($outgoing); - $sender = new Sender($client, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); + $sender = new Sender($client, $this->getMockBuilder('React\Http\Io\MessageFactory')->getMock()); $stream = new ThroughStream(); $request = new Request('POST', 'http://www.google.com/', array(), new ReadableBodyStream($stream)); @@ -162,7 +162,7 @@ public function testSendPostStreamWillRejectWhenRequestBodyEmitsErrorEvent() $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); $client->expects($this->once())->method('request')->willReturn($outgoing); - $sender = new Sender($client, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); + $sender = new Sender($client, $this->getMockBuilder('React\Http\Io\MessageFactory')->getMock()); $expected = new \RuntimeException(); $stream = new ThroughStream(); @@ -192,7 +192,7 @@ public function testSendPostStreamWillRejectWhenRequestBodyClosesWithoutEnd() $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); $client->expects($this->once())->method('request')->willReturn($outgoing); - $sender = new Sender($client, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); + $sender = new Sender($client, $this->getMockBuilder('React\Http\Io\MessageFactory')->getMock()); $stream = new ThroughStream(); $request = new Request('POST', 'http://www.google.com/', array(), new ReadableBodyStream($stream)); @@ -220,7 +220,7 @@ public function testSendPostStreamWillNotRejectWhenRequestBodyClosesAfterEnd() $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); $client->expects($this->once())->method('request')->willReturn($outgoing); - $sender = new Sender($client, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); + $sender = new Sender($client, $this->getMockBuilder('React\Http\Io\MessageFactory')->getMock()); $stream = new ThroughStream(); $request = new Request('POST', 'http://www.google.com/', array(), new ReadableBodyStream($stream)); @@ -247,7 +247,7 @@ public function testSendPostStreamWithExplicitContentLengthWillSendHeaderAsIs() '1.1' )->willReturn($this->getMockBuilder('React\Http\Client\Request')->disableOriginalConstructor()->getMock()); - $sender = new Sender($client, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); + $sender = new Sender($client, $this->getMockBuilder('React\Http\Io\MessageFactory')->getMock()); $stream = new ThroughStream(); $request = new Request('POST', 'http://www.google.com/', array('Content-Length' => '100'), new ReadableBodyStream($stream)); @@ -264,7 +264,7 @@ public function testSendGetWillNotPassContentLengthHeaderForEmptyRequestBody() '1.1' )->willReturn($this->getMockBuilder('React\Http\Client\Request')->disableOriginalConstructor()->getMock()); - $sender = new Sender($client, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); + $sender = new Sender($client, $this->getMockBuilder('React\Http\Io\MessageFactory')->getMock()); $request = new Request('GET', 'http://www.google.com/'); $sender->send($request); @@ -280,7 +280,7 @@ public function testSendCustomMethodWillNotPassContentLengthHeaderForEmptyReques '1.1' )->willReturn($this->getMockBuilder('React\Http\Client\Request')->disableOriginalConstructor()->getMock()); - $sender = new Sender($client, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); + $sender = new Sender($client, $this->getMockBuilder('React\Http\Io\MessageFactory')->getMock()); $request = new Request('CUSTOM', 'http://www.google.com/'); $sender->send($request); @@ -296,7 +296,7 @@ public function testSendCustomMethodWithExplicitContentLengthZeroWillBePassedAsI '1.1' )->willReturn($this->getMockBuilder('React\Http\Client\Request')->disableOriginalConstructor()->getMock()); - $sender = new Sender($client, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); + $sender = new Sender($client, $this->getMockBuilder('React\Http\Io\MessageFactory')->getMock()); $request = new Request('CUSTOM', 'http://www.google.com/', array('Content-Length' => '0')); $sender->send($request); @@ -311,7 +311,7 @@ public function testCancelRequestWillCancelConnector() $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); $connector->expects($this->once())->method('connect')->willReturn($promise); - $sender = new Sender(new HttpClient($this->loop, $connector), $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); + $sender = new Sender(new HttpClient($this->loop, $connector), $this->getMockBuilder('React\Http\Io\MessageFactory')->getMock()); $request = new Request('GET', 'http://www.google.com/'); @@ -330,7 +330,7 @@ public function testCancelRequestWillCloseConnection() $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); $connector->expects($this->once())->method('connect')->willReturn(Promise\resolve($connection)); - $sender = new Sender(new HttpClient($this->loop, $connector), $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); + $sender = new Sender(new HttpClient($this->loop, $connector), $this->getMockBuilder('React\Http\Io\MessageFactory')->getMock()); $request = new Request('GET', 'http://www.google.com/'); @@ -387,7 +387,7 @@ public function testRequestProtocolVersion(Request $Request, $method, $uri, $hea $http->expects($this->once())->method('request')->with($method, $uri, $headers, $protocolVersion)->willReturn($request); - $sender = new Sender($http, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); + $sender = new Sender($http, $this->getMockBuilder('React\Http\Io\MessageFactory')->getMock()); $sender->send($Request); } } diff --git a/tests/Io/TransactionTest.php b/tests/Io/TransactionTest.php index 882b1860..88320bf0 100644 --- a/tests/Io/TransactionTest.php +++ b/tests/Io/TransactionTest.php @@ -3,16 +3,16 @@ namespace React\Tests\Http\Io; use Clue\React\Block; -use React\Http\Io\Transaction; -use React\Http\Message\MessageFactory; -use React\Http\Message\ResponseException; -use React\Tests\Http\TestCase; use PHPUnit\Framework\MockObject\MockObject; use Psr\Http\Message\RequestInterface; +use React\Http\Io\MessageFactory; +use React\Http\Io\Transaction; +use React\Http\Message\ResponseException; use React\EventLoop\Factory; use React\Promise; use React\Promise\Deferred; use React\Stream\ThroughStream; +use React\Tests\Http\TestCase; use RingCentral\Psr7\Response; class TransactionTest extends TestCase From 865694453c95122f8972b9ed7961efb3c517fc5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 11 Jul 2020 15:29:43 +0200 Subject: [PATCH 336/456] Prepare v1.0.0 release --- CHANGELOG.md | 99 ++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 3 +- 2 files changed, 101 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 465356cc..aadbfa7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,104 @@ # Changelog +## 1.0.0 (2020-07-11) + +A major new feature release, see [**release announcement**](https://clue.engineering/2020/announcing-reactphp-http). + +* First stable LTS release, now following [SemVer](https://semver.org/). + We'd like to emphasize that this component is production ready and battle-tested. + We plan to support all long-term support (LTS) releases for at least 24 months, + so you have a rock-solid foundation to build on top of. + +This update involves some major new features and a number of BC breaks due to +some necessary API cleanup. We've tried hard to avoid BC breaks where possible +and minimize impact otherwise. We expect that most consumers of this package +will be affected by BC breaks, but updating should take no longer than a few +minutes. See below for more details: + +* Feature: Add async HTTP client implementation. + (#368 by @clue) + + ```php + $browser = new React\Http\Browser($loop); + $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + echo $response->getBody(); + }); + ``` + + The code has been imported as-is from [clue/reactphp-buzz v2.9.0](https://github.com/clue/reactphp-buzz), + with only minor changes to the namespace and we otherwise leave all the existing APIs unchanged. + Upgrading from [clue/reactphp-buzz v2.9.0](https://github.com/clue/reactphp-buzz) + to this release should be a matter of updating some namespace references only: + + ```php + // old + $browser = new Clue\React\Buzz\Browser($loop); + + // new + $browser = new React\Http\Browser($loop); + ``` + +* Feature / BC break: Add `LoopInterface` as required first constructor argument to `Server` and + change `Server` to accept variadic middleware handlers instead of `array`. + (#361 and #362 by @WyriHaximus) + + ```php + // old + $server = new React\Http\Server($handler); + $server = new React\Http\Server([$middleware, $handler]); + + // new + $server = new React\Http\Server($loop, $handler); + $server = new React\Http\Server($loop, $middleware, $handler); + ``` + +* Feature / BC break: Move `Response` class to `React\Http\Message\Response` and + expose `ServerRequest` class to `React\Http\Message\ServerRequest`. + (#370 by @clue) + + ```php + // old + $response = new React\Http\Response(200, [], 'Hello!'); + + // new + $response = new React\Http\Message\Response(200, [], 'Hello!'); + ``` + +* Feature / BC break: Add `StreamingRequestMiddleware` to stream incoming requests, mark `StreamingServer` as internal. + (#367 by @clue) + + ```php + // old: advanced StreamingServer is now internal only + $server = new React\Http\StreamingServer($handler); + + // new: use StreamingRequestMiddleware instead of StreamingServer + $server = new React\Http\Server( + $loop, + new React\Http\Middleware\StreamingRequestMiddleware(), + $handler + ); + ``` + +* Feature / BC break: Improve default concurrency to 1024 requests and cap default request buffer at 64K. + (#371 by @clue) + + This improves default concurrency to 1024 requests and caps the default request buffer at 64K. + The previous defaults resulted in just 4 concurrent requests with a request buffer of 8M. + See [`Server`](../README.md#server) for details on how to override these defaults. + +* Feature: Expose ReactPHP in `User-Agent` client-side request header and in `Server` server-side response header. + (#374 by @clue) + +* Mark all classes as `final` to discourage inheriting from it. + (#373 by @WyriHaximus) + +* Improve documentation and use fully-qualified class names throughout the documentation and + add ReactPHP core team as authors to `composer.json` and license file. + (#366 and #369 by @WyriHaximus and #375 by @clue) + +* Improve test suite and support skipping all online tests with `--exclude-group internet`. + (#372 by @clue) + ## 0.8.7 (2020-07-05) * Fix: Fix parsing multipart request body with quoted header parameters (dot net). diff --git a/README.md b/README.md index 4a72d9e7..54fd9196 100644 --- a/README.md +++ b/README.md @@ -2728,10 +2728,11 @@ new React\Http\Middleware\RequestBodyParserMiddleware(10 * 1024, 100); // 100 fi The recommended way to install this library is [through Composer](https://getcomposer.org). [New to Composer?](https://getcomposer.org/doc/00-intro.md) +This project follows [SemVer](https://semver.org/). This will install the latest supported version: ```bash -$ composer require react/http:^0.8.7 +$ composer require react/http:^1.0 ``` See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. From 0daf98f59f6873a9778e70a30f39d9fce24ca7c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 25 Jul 2020 01:30:21 +0200 Subject: [PATCH 337/456] Consistently resolve base URL according to HTTP specs --- README.md | 14 +++++++------- src/Browser.php | 20 +++++++++++--------- src/Io/MessageFactory.php | 37 ------------------------------------- tests/BrowserTest.php | 31 +++++++++++++------------------ 4 files changed, 31 insertions(+), 71 deletions(-) diff --git a/README.md b/README.md index 54fd9196..78709bba 100644 --- a/README.md +++ b/README.md @@ -2282,16 +2282,16 @@ The `withBase(string|null $baseUrl): Browser` method can be used to change the base URL used to resolve relative URLs to. If you configure a base URL, any requests to relative URLs will be -processed by first prepending this absolute base URL. Note that this -merely prepends the base URL and does *not* resolve any relative path -references (like `../` etc.). This is mostly useful for (RESTful) API -calls where all endpoints (URLs) are located under a common base URL. +processed by first resolving this relative to the given absolute base +URL. This supports resolving relative path references (like `../` etc.). +This is particularly useful for (RESTful) API calls where all endpoints +(URLs) are located under a common base URL. ```php -$browser = $browser->withBase('http://api.example.com/v3'); +$browser = $browser->withBase('http://api.example.com/v3/'); -// will request http://api.example.com/v3/example -$browser->get('/example')->then(…); +// will request http://api.example.com/v3/users +$browser->get('users')->then(…); ``` You can pass in a `null` base URL to return a new instance that does not diff --git a/src/Browser.php b/src/Browser.php index 46a5d115..622404bf 100644 --- a/src/Browser.php +++ b/src/Browser.php @@ -3,6 +3,7 @@ namespace React\Http; use Psr\Http\Message\ResponseInterface; +use RingCentral\Psr7\Uri; use React\EventLoop\LoopInterface; use React\Http\Io\MessageFactory; use React\Http\Io\Sender; @@ -542,16 +543,16 @@ public function withRejectErrorResponse($obeySuccessCode) * Changes the base URL used to resolve relative URLs to. * * If you configure a base URL, any requests to relative URLs will be - * processed by first prepending this absolute base URL. Note that this - * merely prepends the base URL and does *not* resolve any relative path - * references (like `../` etc.). This is mostly useful for (RESTful) API - * calls where all endpoints (URLs) are located under a common base URL. + * processed by first resolving this relative to the given absolute base + * URL. This supports resolving relative path references (like `../` etc.). + * This is particularly useful for (RESTful) API calls where all endpoints + * (URLs) are located under a common base URL. * * ```php - * $browser = $browser->withBase('http://api.example.com/v3'); + * $browser = $browser->withBase('http://api.example.com/v3/'); * - * // will request http://api.example.com/v3/example - * $browser->get('/example')->then(…); + * // will request http://api.example.com/v3/users + * $browser->get('users')->then(…); * ``` * * You can pass in a `null` base URL to return a new instance that does not @@ -725,12 +726,13 @@ private function withOptions(array $options) */ private function requestMayBeStreaming($method, $url, array $headers = array(), $contents = '') { - $request = $this->messageFactory->request($method, $url, $headers, $contents, $this->protocolVersion); if ($this->baseUrl !== null) { // ensure we're actually below the base URL - $request = $request->withUri($this->messageFactory->expandBase($request->getUri(), $this->baseUrl)); + $url = Uri::resolve($this->baseUrl, $url); } + $request = $this->messageFactory->request($method, $url, $headers, $contents, $this->protocolVersion); + return $this->transaction->send($request); } } diff --git a/src/Io/MessageFactory.php b/src/Io/MessageFactory.php index 14591e80..5b7a2b35 100644 --- a/src/Io/MessageFactory.php +++ b/src/Io/MessageFactory.php @@ -99,41 +99,4 @@ public function uriRelative(UriInterface $base, $uri) { return Uri::resolve($base, $uri); } - - /** - * Resolves the given relative or absolute $uri by appending it behind $this base URI - * - * The given $uri parameter can be either a relative or absolute URI and - * as such can not contain any URI template placeholders. - * - * As such, the outcome of this method represents a valid, absolute URI - * which will be returned as an instance implementing `UriInterface`. - * - * If the given $uri is a relative URI, it will simply be appended behind $base URI. - * - * If the given $uri is an absolute URI, it will simply be returned as-is. - * - * @param UriInterface $uri - * @param UriInterface $base - * @return UriInterface - */ - public function expandBase(UriInterface $uri, UriInterface $base) - { - if ($uri->getScheme() !== '') { - return $uri; - } - - $uri = (string)$uri; - $base = (string)$base; - - if ($uri !== '' && substr($base, -1) !== '/' && substr($uri, 0, 1) !== '?') { - $base .= '/'; - } - - if (isset($uri[0]) && $uri[0] === '/') { - $uri = substr($uri, 1); - } - - return $this->uri($base . $uri); - } } diff --git a/tests/BrowserTest.php b/tests/BrowserTest.php index 88ef107e..612875fc 100644 --- a/tests/BrowserTest.php +++ b/tests/BrowserTest.php @@ -223,35 +223,30 @@ public function provideOtherUris() 'http://example.com/base/another', 'http://example.com/base/another', ), - 'slash returns added slash' => array( + 'slash returns base without path' => array( 'http://example.com/base', '/', - 'http://example.com/base/', - ), - 'slash does not add duplicate slash if base already ends with slash' => array( - 'http://example.com/base/', - '/', - 'http://example.com/base/', + 'http://example.com/', ), 'relative is added behind base' => array( 'http://example.com/base/', 'test', 'http://example.com/base/test', ), - 'relative with slash is added behind base without duplicate slashes' => array( - 'http://example.com/base/', - '/test', - 'http://example.com/base/test', - ), - 'relative is added behind base with automatic slash inbetween' => array( + 'relative is added behind base without path' => array( 'http://example.com/base', 'test', - 'http://example.com/base/test', + 'http://example.com/test', ), - 'relative with slash is added behind base' => array( + 'relative level up is added behind parent path' => array( + 'http://example.com/base/foo/', + '../bar', + 'http://example.com/base/bar', + ), + 'absolute with slash is added behind base without path' => array( 'http://example.com/base', '/test', - 'http://example.com/base/test', + 'http://example.com/test', ), 'query string is added behind base' => array( 'http://example.com/base', @@ -263,10 +258,10 @@ public function provideOtherUris() '?key=value', 'http://example.com/base/?key=value', ), - 'query string with slash is added behind base' => array( + 'query string with slash is added behind base without path' => array( 'http://example.com/base', '/?key=value', - 'http://example.com/base/?key=value', + 'http://example.com/?key=value', ), 'absolute with query string below base is returned as-is' => array( 'http://example.com/base', From 4ce74d248187a9c7026edb43fb3a21e39b01ba1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 25 Jul 2020 01:33:12 +0200 Subject: [PATCH 338/456] Remove unneeded MessageFactory helper methods --- src/Browser.php | 2 +- src/Io/MessageFactory.php | 24 ------------- src/Io/Transaction.php | 3 +- tests/Io/MessageFactoryTest.php | 62 --------------------------------- 4 files changed, 3 insertions(+), 88 deletions(-) diff --git a/src/Browser.php b/src/Browser.php index 622404bf..48e64fcb 100644 --- a/src/Browser.php +++ b/src/Browser.php @@ -585,7 +585,7 @@ public function withBase($baseUrl) return $browser; } - $browser->baseUrl = $this->messageFactory->uri($baseUrl); + $browser->baseUrl = new Uri($baseUrl); if (!\in_array($browser->baseUrl->getScheme(), array('http', 'https')) || $browser->baseUrl->getHost() === '') { throw new \InvalidArgumentException('Base URL must be absolute'); } diff --git a/src/Io/MessageFactory.php b/src/Io/MessageFactory.php index 5b7a2b35..f3d0993d 100644 --- a/src/Io/MessageFactory.php +++ b/src/Io/MessageFactory.php @@ -6,7 +6,6 @@ use Psr\Http\Message\UriInterface; use RingCentral\Psr7\Request; use RingCentral\Psr7\Response; -use RingCentral\Psr7\Uri; use React\Stream\ReadableStreamInterface; /** @@ -76,27 +75,4 @@ public function body($body) return \RingCentral\Psr7\stream_for($body); } - - /** - * Creates a new instance of UriInterface for the given URI string - * - * @param string $uri - * @return UriInterface - */ - public function uri($uri) - { - return new Uri($uri); - } - - /** - * Creates a new instance of UriInterface for the given URI string relative to the given base URI - * - * @param UriInterface $base - * @param string $uri - * @return UriInterface - */ - public function uriRelative(UriInterface $base, $uri) - { - return Uri::resolve($base, $uri); - } } diff --git a/src/Io/Transaction.php b/src/Io/Transaction.php index a593e684..93741dcc 100644 --- a/src/Io/Transaction.php +++ b/src/Io/Transaction.php @@ -5,6 +5,7 @@ use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\UriInterface; +use RingCentral\Psr7\Uri; use React\EventLoop\LoopInterface; use React\Http\Message\ResponseException; use React\Promise\Deferred; @@ -246,7 +247,7 @@ public function onResponse(ResponseInterface $response, RequestInterface $reques private function onResponseRedirect(ResponseInterface $response, RequestInterface $request, Deferred $deferred) { // resolve location relative to last request URI - $location = $this->messageFactory->uriRelative($request->getUri(), $response->getHeaderLine('Location')); + $location = Uri::resolve($request->getUri(), $response->getHeaderLine('Location')); $request = $this->makeRedirectRequest($request, $location); $this->progress('redirect', array($request)); diff --git a/tests/Io/MessageFactoryTest.php b/tests/Io/MessageFactoryTest.php index b09a4af8..ae2dcdf9 100644 --- a/tests/Io/MessageFactoryTest.php +++ b/tests/Io/MessageFactoryTest.php @@ -17,68 +17,6 @@ public function setUpMessageFactory() $this->messageFactory = new MessageFactory(); } - public function testUriSimple() - { - $uri = $this->messageFactory->uri('http://www.lueck.tv/'); - - $this->assertEquals('http', $uri->getScheme()); - $this->assertEquals('www.lueck.tv', $uri->getHost()); - $this->assertEquals('/', $uri->getPath()); - - $this->assertEquals(null, $uri->getPort()); - $this->assertEquals('', $uri->getQuery()); - } - - public function testUriComplete() - { - $uri = $this->messageFactory->uri('https://example.com:8080/?just=testing'); - - $this->assertEquals('https', $uri->getScheme()); - $this->assertEquals('example.com', $uri->getHost()); - $this->assertEquals(8080, $uri->getPort()); - $this->assertEquals('/', $uri->getPath()); - $this->assertEquals('just=testing', $uri->getQuery()); - } - - public function testPlaceholdersInUriWillBeEscaped() - { - $uri = $this->messageFactory->uri('http://example.com/{version}'); - - $this->assertEquals('/%7Bversion%7D', $uri->getPath()); - } - - public function testEscapedPlaceholdersInUriWillStayEscaped() - { - $uri = $this->messageFactory->uri('http://example.com/%7Bversion%7D'); - - $this->assertEquals('/%7Bversion%7D', $uri->getPath()); - } - - public function testResolveRelative() - { - $base = $this->messageFactory->uri('http://example.com/base/'); - - $this->assertEquals('http://example.com/base/', $this->messageFactory->uriRelative($base, '')); - $this->assertEquals('http://example.com/', $this->messageFactory->uriRelative($base, '/')); - - $this->assertEquals('http://example.com/base/a', $this->messageFactory->uriRelative($base, 'a')); - $this->assertEquals('http://example.com/a', $this->messageFactory->uriRelative($base, '../a')); - } - - public function testResolveAbsolute() - { - $base = $this->messageFactory->uri('http://example.org/'); - - $this->assertEquals('http://www.example.com/', $this->messageFactory->uriRelative($base, 'http://www.example.com/')); - } - - public function testResolveUri() - { - $base = $this->messageFactory->uri('http://example.org/'); - - $this->assertEquals('http://www.example.com/', $this->messageFactory->uriRelative($base, $this->messageFactory->uri('http://www.example.com/'))); - } - public function testBodyString() { $body = $this->messageFactory->body('hi'); From 788526eec3b44b80d830b822e3a5573d85465564 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 25 Jul 2020 13:15:01 +0200 Subject: [PATCH 339/456] Internal refactoring, remove unneeded MessageFactory helper class This is an internal preparation only and does not affect any public APIs. Some internal logic has been refactored and moved to classes with better cohesion. This is done in preparation for upcoming improvements to the `Transfer-Encoding: chunked` response header. --- src/Browser.php | 20 ++-- src/Client/Response.php | 15 ++- src/Io/MessageFactory.php | 78 ------------ src/Io/Sender.php | 29 +++-- src/Io/Transaction.php | 14 +-- tests/FunctionalBrowserTest.php | 62 ++++++++-- tests/Io/MessageFactoryTest.php | 135 --------------------- tests/Io/SenderTest.php | 36 +++--- tests/Io/TransactionTest.php | 204 +++++++++++++------------------- 9 files changed, 199 insertions(+), 394 deletions(-) delete mode 100644 src/Io/MessageFactory.php delete mode 100644 tests/Io/MessageFactoryTest.php diff --git a/src/Browser.php b/src/Browser.php index 48e64fcb..91604994 100644 --- a/src/Browser.php +++ b/src/Browser.php @@ -3,9 +3,10 @@ namespace React\Http; use Psr\Http\Message\ResponseInterface; +use RingCentral\Psr7\Request; use RingCentral\Psr7\Uri; use React\EventLoop\LoopInterface; -use React\Http\Io\MessageFactory; +use React\Http\Io\ReadableBodyStream; use React\Http\Io\Sender; use React\Http\Io\Transaction; use React\Promise\PromiseInterface; @@ -19,7 +20,6 @@ class Browser { private $transaction; - private $messageFactory; private $baseUrl; private $protocolVersion = '1.1'; @@ -59,10 +59,8 @@ class Browser */ public function __construct(LoopInterface $loop, ConnectorInterface $connector = null) { - $this->messageFactory = new MessageFactory(); $this->transaction = new Transaction( - Sender::createFromLoop($loop, $connector, $this->messageFactory), - $this->messageFactory, + Sender::createFromLoop($loop, $connector), $loop ); } @@ -721,18 +719,22 @@ private function withOptions(array $options) * @param string $method * @param string $url * @param array $headers - * @param string|ReadableStreamInterface $contents + * @param string|ReadableStreamInterface $body * @return PromiseInterface */ - private function requestMayBeStreaming($method, $url, array $headers = array(), $contents = '') + private function requestMayBeStreaming($method, $url, array $headers = array(), $body = '') { if ($this->baseUrl !== null) { // ensure we're actually below the base URL $url = Uri::resolve($this->baseUrl, $url); } - $request = $this->messageFactory->request($method, $url, $headers, $contents, $this->protocolVersion); + if ($body instanceof ReadableStreamInterface) { + $body = new ReadableBodyStream($body); + } - return $this->transaction->send($request); + return $this->transaction->send( + new Request($method, $url, $headers, $body, $this->protocolVersion) + ); } } diff --git a/src/Client/Response.php b/src/Client/Response.php index be19eb4c..2de64bb0 100644 --- a/src/Client/Response.php +++ b/src/Client/Response.php @@ -86,11 +86,24 @@ private function getHeader($name) return isset($normalized[$name]) ? (array)$normalized[$name] : array(); } - private function getHeaderLine($name) + /** + * @param string $name + * @return string + */ + public function getHeaderLine($name) { return implode(', ' , $this->getHeader($name)); } + /** + * @param string $name + * @return bool + */ + public function hasHeader($name) + { + return $this->getHeader($name) !== array(); + } + /** @internal */ public function handleData($data) { diff --git a/src/Io/MessageFactory.php b/src/Io/MessageFactory.php deleted file mode 100644 index f3d0993d..00000000 --- a/src/Io/MessageFactory.php +++ /dev/null @@ -1,78 +0,0 @@ -body($content), $protocolVersion); - } - - /** - * Creates a new instance of ResponseInterface for the given response parameters - * - * @param string $protocolVersion - * @param int $status - * @param string $reason - * @param array $headers - * @param ReadableStreamInterface|string $body - * @param ?string $requestMethod - * @return Response - * @uses self::body() - */ - public function response($protocolVersion, $status, $reason, $headers = array(), $body = '', $requestMethod = null) - { - $response = new Response($status, $headers, $body instanceof ReadableStreamInterface ? null : $body, $protocolVersion, $reason); - - if ($body instanceof ReadableStreamInterface) { - $length = null; - $code = $response->getStatusCode(); - if ($requestMethod === 'HEAD' || ($code >= 100 && $code < 200) || $code == 204 || $code == 304) { - $length = 0; - } elseif (\strtolower($response->getHeaderLine('Transfer-Encoding')) === 'chunked') { - $length = null; - } elseif ($response->hasHeader('Content-Length')) { - $length = (int)$response->getHeaderLine('Content-Length'); - } - - $response = $response->withBody(new ReadableBodyStream($body, $length)); - } - - return $response; - } - - /** - * Creates a new instance of StreamInterface for the given body contents - * - * @param ReadableStreamInterface|string $body - * @return StreamInterface - */ - public function body($body) - { - if ($body instanceof ReadableStreamInterface) { - return new ReadableBodyStream($body); - } - - return \RingCentral\Psr7\stream_for($body); - } -} diff --git a/src/Io/Sender.php b/src/Io/Sender.php index 6f3367e5..6cba0495 100644 --- a/src/Io/Sender.php +++ b/src/Io/Sender.php @@ -6,6 +6,7 @@ use React\EventLoop\LoopInterface; use React\Http\Client\Client as HttpClient; use React\Http\Client\Response as ResponseStream; +use React\Http\Message\Response; use React\Promise\PromiseInterface; use React\Promise\Deferred; use React\Socket\ConnectorInterface; @@ -47,13 +48,12 @@ class Sender * @param ConnectorInterface|null $connector * @return self */ - public static function createFromLoop(LoopInterface $loop, ConnectorInterface $connector = null, MessageFactory $messageFactory) + public static function createFromLoop(LoopInterface $loop, ConnectorInterface $connector = null) { - return new self(new HttpClient($loop, $connector), $messageFactory); + return new self(new HttpClient($loop, $connector)); } private $http; - private $messageFactory; /** * [internal] Instantiate Sender @@ -61,10 +61,9 @@ public static function createFromLoop(LoopInterface $loop, ConnectorInterface $c * @param HttpClient $http * @internal */ - public function __construct(HttpClient $http, MessageFactory $messageFactory) + public function __construct(HttpClient $http) { $this->http = $http; - $this->messageFactory = $messageFactory; } /** @@ -109,16 +108,22 @@ public function send(RequestInterface $request) $deferred->reject($error); }); - $messageFactory = $this->messageFactory; - $requestStream->on('response', function (ResponseStream $responseStream) use ($deferred, $messageFactory, $request) { + $requestStream->on('response', function (ResponseStream $responseStream) use ($deferred, $request) { + $length = null; + $code = $responseStream->getCode(); + if ($request->getMethod() === 'HEAD' || ($code >= 100 && $code < 200) || $code == 204 || $code == 304) { + $length = 0; + } elseif ($responseStream->hasHeader('Content-Length')) { + $length = (int) $responseStream->getHeaderLine('Content-Length'); + } + // apply response header values from response stream - $deferred->resolve($messageFactory->response( - $responseStream->getVersion(), + $deferred->resolve(new Response( $responseStream->getCode(), - $responseStream->getReasonPhrase(), $responseStream->getHeaders(), - $responseStream, - $request->getMethod() + new ReadableBodyStream($responseStream, $length), + $responseStream->getVersion(), + $responseStream->getReasonPhrase() )); }); diff --git a/src/Io/Transaction.php b/src/Io/Transaction.php index 93741dcc..9449503f 100644 --- a/src/Io/Transaction.php +++ b/src/Io/Transaction.php @@ -5,6 +5,7 @@ use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\UriInterface; +use RingCentral\Psr7\Request; use RingCentral\Psr7\Uri; use React\EventLoop\LoopInterface; use React\Http\Message\ResponseException; @@ -18,7 +19,6 @@ class Transaction { private $sender; - private $messageFactory; private $loop; // context: http.timeout (ini_get('default_socket_timeout'): 60) @@ -37,10 +37,9 @@ class Transaction private $maximumSize = 16777216; // 16 MiB = 2^24 bytes - public function __construct(Sender $sender, MessageFactory $messageFactory, LoopInterface $loop) + public function __construct(Sender $sender, LoopInterface $loop) { $this->sender = $sender; - $this->messageFactory = $messageFactory; $this->loop = $loop; } @@ -55,7 +54,7 @@ public function withOptions(array $options) if (property_exists($transaction, $name)) { // restore default value if null is given if ($value === null) { - $default = new self($this->sender, $this->messageFactory, $this->loop); + $default = new self($this->sender, $this->loop); $value = $default->$name; } @@ -186,11 +185,10 @@ public function bufferResponse(ResponseInterface $response, $deferred) } // buffer stream and resolve with buffered body - $messageFactory = $this->messageFactory; $maximumSize = $this->maximumSize; $promise = \React\Promise\Stream\buffer($stream, $maximumSize)->then( - function ($body) use ($response, $messageFactory) { - return $response->withBody($messageFactory->body($body)); + function ($body) use ($response) { + return $response->withBody(\RingCentral\Psr7\stream_for($body)); }, function ($e) use ($stream, $maximumSize) { // try to close stream if buffering fails (or is cancelled) @@ -280,7 +278,7 @@ private function makeRedirectRequest(RequestInterface $request, UriInterface $lo // naïve approach.. $method = ($request->getMethod() === 'HEAD') ? 'HEAD' : 'GET'; - return $this->messageFactory->request($method, $location, $request->getHeaders()); + return new Request($method, $location, $request->getHeaders()); } private function progress($name, array $args = array()) diff --git a/tests/FunctionalBrowserTest.php b/tests/FunctionalBrowserTest.php index f4495565..59cba0b9 100644 --- a/tests/FunctionalBrowserTest.php +++ b/tests/FunctionalBrowserTest.php @@ -64,14 +64,22 @@ public function setUpBrowserAndServer() ); } - if ($path === '/status/300') { + if ($path === '/status/204') { return new Response( - 300, + 204, array(), '' ); } + if ($path === '/status/304') { + return new Response( + 304, + array(), + 'Not modified' + ); + } + if ($path === '/status/404') { return new Response( 404, @@ -308,12 +316,24 @@ public function testFollowRedirectsZeroRejectsOnRedirect() Block\await($browser->get($this->base . 'redirect-to?url=get'), $this->loop); } - /** - * @doesNotPerformAssertions - */ - public function testResponseStatus300WithoutLocationShouldResolveWithoutFollowingRedirect() + public function testResponseStatus204ShouldResolveWithEmptyBody() { - Block\await($this->browser->get($this->base . 'status/300'), $this->loop); + $response = Block\await($this->browser->get($this->base . 'status/204'), $this->loop); + $this->assertFalse($response->hasHeader('Content-Length')); + + $body = $response->getBody(); + $this->assertEquals(0, $body->getSize()); + $this->assertEquals('', (string) $body); + } + + public function testResponseStatus304ShouldResolveWithEmptyBodyButContentLengthResponseHeader() + { + $response = Block\await($this->browser->get($this->base . 'status/304'), $this->loop); + $this->assertEquals('12', $response->getHeaderLine('Content-Length')); + + $body = $response->getBody(); + $this->assertEquals(0, $body->getSize()); + $this->assertEquals('', (string) $body); } /** @@ -595,9 +615,33 @@ public function testSendsExplicitHttp10Request() public function testHeadRequestReceivesResponseWithEmptyBodyButWithContentLengthResponseHeader() { $response = Block\await($this->browser->head($this->base . 'get'), $this->loop); - $this->assertEquals('', (string)$response->getBody()); - $this->assertEquals(0, $response->getBody()->getSize()); $this->assertEquals('5', $response->getHeaderLine('Content-Length')); + + $body = $response->getBody(); + $this->assertEquals(0, $body->getSize()); + $this->assertEquals('', (string) $body); + } + + public function testRequestStreamingGetReceivesResponseWithStreamingBodyAndKnownSize() + { + $response = Block\await($this->browser->requestStreaming('GET', $this->base . 'get'), $this->loop); + $this->assertEquals('5', $response->getHeaderLine('Content-Length')); + + $body = $response->getBody(); + $this->assertEquals(5, $body->getSize()); + $this->assertEquals('', (string) $body); + $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); + } + + public function testRequestStreamingGetReceivesResponseWithStreamingBodyAndUnknownSizeFromStreamingEndpoint() + { + $response = Block\await($this->browser->requestStreaming('GET', $this->base . 'stream/1'), $this->loop); + $this->assertFalse($response->hasHeader('Content-Length')); + + $body = $response->getBody(); + $this->assertNull($body->getSize()); + $this->assertEquals('', (string) $body); + $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); } public function testRequestStreamingGetReceivesStreamingResponseBody() diff --git a/tests/Io/MessageFactoryTest.php b/tests/Io/MessageFactoryTest.php deleted file mode 100644 index ae2dcdf9..00000000 --- a/tests/Io/MessageFactoryTest.php +++ /dev/null @@ -1,135 +0,0 @@ -messageFactory = new MessageFactory(); - } - - public function testBodyString() - { - $body = $this->messageFactory->body('hi'); - - $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $body); - $this->assertNotInstanceOf('React\Stream\ReadableStreamInterface', $body); - $this->assertEquals(2, $body->getSize()); - $this->assertEquals('hi', (string)$body); - } - - public function testBodyReadableStream() - { - $stream = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); - $body = $this->messageFactory->body($stream); - - $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $body); - $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); - $this->assertEquals(null, $body->getSize()); - $this->assertEquals('', (string)$body); - } - - public function testResponseWithBodyString() - { - $response = $this->messageFactory->response('1.1', 200, 'OK', array(), 'hi'); - - $body = $response->getBody(); - $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $body); - $this->assertNotInstanceOf('React\Stream\ReadableStreamInterface', $body); - $this->assertEquals(2, $body->getSize()); - $this->assertEquals('hi', (string)$body); - } - - public function testResponseWithStreamingBodyHasUnknownSizeByDefault() - { - $stream = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); - $response = $this->messageFactory->response('1.1', 200, 'OK', array(), $stream); - - $body = $response->getBody(); - $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $body); - $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); - $this->assertNull($body->getSize()); - $this->assertEquals('', (string)$body); - } - - public function testResponseWithStreamingBodyHasSizeFromContentLengthHeader() - { - $stream = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); - $response = $this->messageFactory->response('1.1', 200, 'OK', array('Content-Length' => '100'), $stream); - - $body = $response->getBody(); - $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $body); - $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); - $this->assertEquals(100, $body->getSize()); - $this->assertEquals('', (string)$body); - } - - public function testResponseWithStreamingBodyHasUnknownSizeWithTransferEncodingChunkedHeader() - { - $stream = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); - $response = $this->messageFactory->response('1.1', 200, 'OK', array('Transfer-Encoding' => 'chunked'), $stream); - - $body = $response->getBody(); - $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $body); - $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); - $this->assertNull($body->getSize()); - $this->assertEquals('', (string)$body); - } - - public function testResponseWithStreamingBodyHasZeroSizeForInformationalResponse() - { - $stream = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); - $response = $this->messageFactory->response('1.1', 101, 'OK', array('Content-Length' => '100'), $stream); - - $body = $response->getBody(); - $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $body); - $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); - $this->assertEquals(0, $body->getSize()); - $this->assertEquals('', (string)$body); - } - - public function testResponseWithStreamingBodyHasZeroSizeForNoContentResponse() - { - $stream = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); - $response = $this->messageFactory->response('1.1', 204, 'OK', array('Content-Length' => '100'), $stream); - - $body = $response->getBody(); - $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $body); - $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); - $this->assertEquals(0, $body->getSize()); - $this->assertEquals('', (string)$body); - } - - public function testResponseWithStreamingBodyHasZeroSizeForNotModifiedResponse() - { - $stream = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); - $response = $this->messageFactory->response('1.1', 304, 'OK', array('Content-Length' => '100'), $stream); - - $body = $response->getBody(); - $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $body); - $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); - $this->assertEquals(0, $body->getSize()); - $this->assertEquals('', (string)$body); - } - - public function testResponseWithStreamingBodyHasZeroSizeForHeadRequestMethod() - { - $stream = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); - $response = $this->messageFactory->response('1.1', 200, 'OK', array('Content-Length' => '100'), $stream, 'HEAD'); - - $body = $response->getBody(); - $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $body); - $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); - $this->assertEquals(0, $body->getSize()); - $this->assertEquals('', (string)$body); - } -} diff --git a/tests/Io/SenderTest.php b/tests/Io/SenderTest.php index 35eb22e7..1c6d1d6b 100644 --- a/tests/Io/SenderTest.php +++ b/tests/Io/SenderTest.php @@ -26,7 +26,7 @@ public function setUpLoop() public function testCreateFromLoop() { - $sender = Sender::createFromLoop($this->loop, null, $this->getMockBuilder('React\Http\Io\MessageFactory')->getMock()); + $sender = Sender::createFromLoop($this->loop, null); $this->assertInstanceOf('React\Http\Io\Sender', $sender); } @@ -36,7 +36,7 @@ public function testSenderRejectsInvalidUri() $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); $connector->expects($this->never())->method('connect'); - $sender = new Sender(new HttpClient($this->loop, $connector), $this->getMockBuilder('React\Http\Io\MessageFactory')->getMock()); + $sender = new Sender(new HttpClient($this->loop, $connector)); $request = new Request('GET', 'www.google.com'); @@ -51,7 +51,7 @@ public function testSenderConnectorRejection() $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); $connector->expects($this->once())->method('connect')->willReturn(Promise\reject(new \RuntimeException('Rejected'))); - $sender = new Sender(new HttpClient($this->loop, $connector), $this->getMockBuilder('React\Http\Io\MessageFactory')->getMock()); + $sender = new Sender(new HttpClient($this->loop, $connector)); $request = new Request('GET', 'http://www.google.com/'); @@ -71,7 +71,7 @@ public function testSendPostWillAutomaticallySendContentLengthHeader() '1.1' )->willReturn($this->getMockBuilder('React\Http\Client\Request')->disableOriginalConstructor()->getMock()); - $sender = new Sender($client, $this->getMockBuilder('React\Http\Io\MessageFactory')->getMock()); + $sender = new Sender($client); $request = new Request('POST', 'http://www.google.com/', array(), 'hello'); $sender->send($request); @@ -87,7 +87,7 @@ public function testSendPostWillAutomaticallySendContentLengthZeroHeaderForEmpty '1.1' )->willReturn($this->getMockBuilder('React\Http\Client\Request')->disableOriginalConstructor()->getMock()); - $sender = new Sender($client, $this->getMockBuilder('React\Http\Io\MessageFactory')->getMock()); + $sender = new Sender($client); $request = new Request('POST', 'http://www.google.com/', array(), ''); $sender->send($request); @@ -106,7 +106,7 @@ public function testSendPostStreamWillAutomaticallySendTransferEncodingChunked() '1.1' )->willReturn($outgoing); - $sender = new Sender($client, $this->getMockBuilder('React\Http\Io\MessageFactory')->getMock()); + $sender = new Sender($client); $stream = new ThroughStream(); $request = new Request('POST', 'http://www.google.com/', array(), new ReadableBodyStream($stream)); @@ -122,7 +122,7 @@ public function testSendPostStreamWillAutomaticallyPipeChunkEncodeBodyForWriteAn $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); $client->expects($this->once())->method('request')->willReturn($outgoing); - $sender = new Sender($client, $this->getMockBuilder('React\Http\Io\MessageFactory')->getMock()); + $sender = new Sender($client); $stream = new ThroughStream(); $request = new Request('POST', 'http://www.google.com/', array(), new ReadableBodyStream($stream)); @@ -142,7 +142,7 @@ public function testSendPostStreamWillAutomaticallyPipeChunkEncodeBodyForEnd() $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); $client->expects($this->once())->method('request')->willReturn($outgoing); - $sender = new Sender($client, $this->getMockBuilder('React\Http\Io\MessageFactory')->getMock()); + $sender = new Sender($client); $stream = new ThroughStream(); $request = new Request('POST', 'http://www.google.com/', array(), new ReadableBodyStream($stream)); @@ -162,7 +162,7 @@ public function testSendPostStreamWillRejectWhenRequestBodyEmitsErrorEvent() $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); $client->expects($this->once())->method('request')->willReturn($outgoing); - $sender = new Sender($client, $this->getMockBuilder('React\Http\Io\MessageFactory')->getMock()); + $sender = new Sender($client); $expected = new \RuntimeException(); $stream = new ThroughStream(); @@ -192,7 +192,7 @@ public function testSendPostStreamWillRejectWhenRequestBodyClosesWithoutEnd() $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); $client->expects($this->once())->method('request')->willReturn($outgoing); - $sender = new Sender($client, $this->getMockBuilder('React\Http\Io\MessageFactory')->getMock()); + $sender = new Sender($client); $stream = new ThroughStream(); $request = new Request('POST', 'http://www.google.com/', array(), new ReadableBodyStream($stream)); @@ -220,7 +220,7 @@ public function testSendPostStreamWillNotRejectWhenRequestBodyClosesAfterEnd() $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); $client->expects($this->once())->method('request')->willReturn($outgoing); - $sender = new Sender($client, $this->getMockBuilder('React\Http\Io\MessageFactory')->getMock()); + $sender = new Sender($client); $stream = new ThroughStream(); $request = new Request('POST', 'http://www.google.com/', array(), new ReadableBodyStream($stream)); @@ -247,7 +247,7 @@ public function testSendPostStreamWithExplicitContentLengthWillSendHeaderAsIs() '1.1' )->willReturn($this->getMockBuilder('React\Http\Client\Request')->disableOriginalConstructor()->getMock()); - $sender = new Sender($client, $this->getMockBuilder('React\Http\Io\MessageFactory')->getMock()); + $sender = new Sender($client); $stream = new ThroughStream(); $request = new Request('POST', 'http://www.google.com/', array('Content-Length' => '100'), new ReadableBodyStream($stream)); @@ -264,7 +264,7 @@ public function testSendGetWillNotPassContentLengthHeaderForEmptyRequestBody() '1.1' )->willReturn($this->getMockBuilder('React\Http\Client\Request')->disableOriginalConstructor()->getMock()); - $sender = new Sender($client, $this->getMockBuilder('React\Http\Io\MessageFactory')->getMock()); + $sender = new Sender($client); $request = new Request('GET', 'http://www.google.com/'); $sender->send($request); @@ -280,7 +280,7 @@ public function testSendCustomMethodWillNotPassContentLengthHeaderForEmptyReques '1.1' )->willReturn($this->getMockBuilder('React\Http\Client\Request')->disableOriginalConstructor()->getMock()); - $sender = new Sender($client, $this->getMockBuilder('React\Http\Io\MessageFactory')->getMock()); + $sender = new Sender($client); $request = new Request('CUSTOM', 'http://www.google.com/'); $sender->send($request); @@ -296,7 +296,7 @@ public function testSendCustomMethodWithExplicitContentLengthZeroWillBePassedAsI '1.1' )->willReturn($this->getMockBuilder('React\Http\Client\Request')->disableOriginalConstructor()->getMock()); - $sender = new Sender($client, $this->getMockBuilder('React\Http\Io\MessageFactory')->getMock()); + $sender = new Sender($client); $request = new Request('CUSTOM', 'http://www.google.com/', array('Content-Length' => '0')); $sender->send($request); @@ -311,7 +311,7 @@ public function testCancelRequestWillCancelConnector() $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); $connector->expects($this->once())->method('connect')->willReturn($promise); - $sender = new Sender(new HttpClient($this->loop, $connector), $this->getMockBuilder('React\Http\Io\MessageFactory')->getMock()); + $sender = new Sender(new HttpClient($this->loop, $connector)); $request = new Request('GET', 'http://www.google.com/'); @@ -330,7 +330,7 @@ public function testCancelRequestWillCloseConnection() $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); $connector->expects($this->once())->method('connect')->willReturn(Promise\resolve($connection)); - $sender = new Sender(new HttpClient($this->loop, $connector), $this->getMockBuilder('React\Http\Io\MessageFactory')->getMock()); + $sender = new Sender(new HttpClient($this->loop, $connector)); $request = new Request('GET', 'http://www.google.com/'); @@ -387,7 +387,7 @@ public function testRequestProtocolVersion(Request $Request, $method, $uri, $hea $http->expects($this->once())->method('request')->with($method, $uri, $headers, $protocolVersion)->willReturn($request); - $sender = new Sender($http, $this->getMockBuilder('React\Http\Io\MessageFactory')->getMock()); + $sender = new Sender($http); $sender->send($Request); } } diff --git a/tests/Io/TransactionTest.php b/tests/Io/TransactionTest.php index 88320bf0..384a74a6 100644 --- a/tests/Io/TransactionTest.php +++ b/tests/Io/TransactionTest.php @@ -5,7 +5,7 @@ use Clue\React\Block; use PHPUnit\Framework\MockObject\MockObject; use Psr\Http\Message\RequestInterface; -use React\Http\Io\MessageFactory; +use RingCentral\Psr7\Response; use React\Http\Io\Transaction; use React\Http\Message\ResponseException; use React\EventLoop\Factory; @@ -13,7 +13,8 @@ use React\Promise\Deferred; use React\Stream\ThroughStream; use React\Tests\Http\TestCase; -use RingCentral\Psr7\Response; +use RingCentral\Psr7\Request; +use React\Http\Io\ReadableBodyStream; class TransactionTest extends TestCase { @@ -21,7 +22,7 @@ public function testWithOptionsReturnsNewInstanceWithChangedOption() { $sender = $this->makeSenderMock(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $transaction = new Transaction($sender, new MessageFactory(), $loop); + $transaction = new Transaction($sender, $loop); $new = $transaction->withOptions(array('followRedirects' => false)); @@ -38,7 +39,7 @@ public function testWithOptionsDoesNotChangeOriginalInstance() { $sender = $this->makeSenderMock(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $transaction = new Transaction($sender, new MessageFactory(), $loop); + $transaction = new Transaction($sender, $loop); $transaction->withOptions(array('followRedirects' => false)); @@ -52,7 +53,7 @@ public function testWithOptionsNullValueReturnsNewInstanceWithDefaultOption() { $sender = $this->makeSenderMock(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $transaction = new Transaction($sender, new MessageFactory(), $loop); + $transaction = new Transaction($sender, $loop); $transaction = $transaction->withOptions(array('followRedirects' => false)); $transaction = $transaction->withOptions(array('followRedirects' => null)); @@ -65,8 +66,6 @@ public function testWithOptionsNullValueReturnsNewInstanceWithDefaultOption() public function testTimeoutExplicitOptionWillStartTimeoutTimer() { - $messageFactory = new MessageFactory(); - $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $loop->expects($this->once())->method('addTimer')->with(2, $this->anything())->willReturn($timer); @@ -77,7 +76,7 @@ public function testTimeoutExplicitOptionWillStartTimeoutTimer() $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new \React\Promise\Promise(function () { })); - $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = new Transaction($sender, $loop); $transaction = $transaction->withOptions(array('timeout' => 2)); $promise = $transaction->send($request); @@ -86,8 +85,6 @@ public function testTimeoutExplicitOptionWillStartTimeoutTimer() public function testTimeoutImplicitFromIniWillStartTimeoutTimer() { - $messageFactory = new MessageFactory(); - $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $loop->expects($this->once())->method('addTimer')->with(2, $this->anything())->willReturn($timer); @@ -98,7 +95,7 @@ public function testTimeoutImplicitFromIniWillStartTimeoutTimer() $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new \React\Promise\Promise(function () { })); - $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = new Transaction($sender, $loop); $old = ini_get('default_socket_timeout'); ini_set('default_socket_timeout', '2'); @@ -110,8 +107,6 @@ public function testTimeoutImplicitFromIniWillStartTimeoutTimer() public function testTimeoutExplicitOptionWillRejectWhenTimerFires() { - $messageFactory = new MessageFactory(); - $timeout = null; $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); @@ -126,7 +121,7 @@ public function testTimeoutExplicitOptionWillRejectWhenTimerFires() $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new \React\Promise\Promise(function () { })); - $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = new Transaction($sender, $loop); $transaction = $transaction->withOptions(array('timeout' => 2)); $promise = $transaction->send($request); @@ -144,18 +139,16 @@ public function testTimeoutExplicitOptionWillRejectWhenTimerFires() public function testTimeoutExplicitOptionWillNotStartTimeoutWhenSenderResolvesImmediately() { - $messageFactory = new MessageFactory(); - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $loop->expects($this->never())->method('addTimer'); $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); - $response = $messageFactory->response(1.0, 200, 'OK', array(), ''); + $response = new Response(200, array(), ''); $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(Promise\resolve($response)); - $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = new Transaction($sender, $loop); $transaction = $transaction->withOptions(array('timeout' => 0.001)); $promise = $transaction->send($request); @@ -165,21 +158,19 @@ public function testTimeoutExplicitOptionWillNotStartTimeoutWhenSenderResolvesIm public function testTimeoutExplicitOptionWillCancelTimeoutTimerWhenSenderResolvesLaterOn() { - $messageFactory = new MessageFactory(); - $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $loop->expects($this->once())->method('addTimer')->willReturn($timer); $loop->expects($this->once())->method('cancelTimer')->with($timer); $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); - $response = $messageFactory->response(1.0, 200, 'OK', array(), ''); + $response = new Response(200, array(), ''); $deferred = new Deferred(); $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn($deferred->promise()); - $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = new Transaction($sender, $loop); $transaction = $transaction->withOptions(array('timeout' => 0.001)); $promise = $transaction->send($request); @@ -191,8 +182,6 @@ public function testTimeoutExplicitOptionWillCancelTimeoutTimerWhenSenderResolve public function testTimeoutExplicitOptionWillNotStartTimeoutWhenSenderRejectsImmediately() { - $messageFactory = new MessageFactory(); - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $loop->expects($this->never())->method('addTimer'); @@ -202,7 +191,7 @@ public function testTimeoutExplicitOptionWillNotStartTimeoutWhenSenderRejectsImm $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(Promise\reject($exception)); - $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = new Transaction($sender, $loop); $transaction = $transaction->withOptions(array('timeout' => 0.001)); $promise = $transaction->send($request); @@ -212,8 +201,6 @@ public function testTimeoutExplicitOptionWillNotStartTimeoutWhenSenderRejectsImm public function testTimeoutExplicitOptionWillCancelTimeoutTimerWhenSenderRejectsLaterOn() { - $messageFactory = new MessageFactory(); - $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $loop->expects($this->once())->method('addTimer')->willReturn($timer); @@ -225,7 +212,7 @@ public function testTimeoutExplicitOptionWillCancelTimeoutTimerWhenSenderRejects $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn($deferred->promise()); - $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = new Transaction($sender, $loop); $transaction = $transaction->withOptions(array('timeout' => 0.001)); $promise = $transaction->send($request); @@ -238,8 +225,6 @@ public function testTimeoutExplicitOptionWillCancelTimeoutTimerWhenSenderRejects public function testTimeoutExplicitNegativeWillNotStartTimeoutTimer() { - $messageFactory = new MessageFactory(); - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $loop->expects($this->never())->method('addTimer'); @@ -248,7 +233,7 @@ public function testTimeoutExplicitNegativeWillNotStartTimeoutTimer() $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new \React\Promise\Promise(function () { })); - $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = new Transaction($sender, $loop); $transaction = $transaction->withOptions(array('timeout' => -1)); $promise = $transaction->send($request); @@ -257,18 +242,16 @@ public function testTimeoutExplicitNegativeWillNotStartTimeoutTimer() public function testTimeoutExplicitOptionWillNotStartTimeoutTimerWhenRequestBodyIsStreaming() { - $messageFactory = new MessageFactory(); - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $loop->expects($this->never())->method('addTimer'); $stream = new ThroughStream(); - $request = $messageFactory->request('POST', 'http://example.com', array(), $stream); + $request = new Request('POST', 'http://example.com', array(), new ReadableBodyStream($stream)); $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new \React\Promise\Promise(function () { })); - $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = new Transaction($sender, $loop); $transaction = $transaction->withOptions(array('timeout' => 2)); $promise = $transaction->send($request); @@ -277,8 +260,6 @@ public function testTimeoutExplicitOptionWillNotStartTimeoutTimerWhenRequestBody public function testTimeoutExplicitOptionWillStartTimeoutTimerWhenStreamingRequestBodyIsAlreadyClosed() { - $messageFactory = new MessageFactory(); - $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $loop->expects($this->once())->method('addTimer')->with(2, $this->anything())->willReturn($timer); @@ -286,12 +267,12 @@ public function testTimeoutExplicitOptionWillStartTimeoutTimerWhenStreamingReque $stream = new ThroughStream(); $stream->close(); - $request = $messageFactory->request('POST', 'http://example.com', array(), $stream); + $request = new Request('POST', 'http://example.com', array(), new ReadableBodyStream($stream)); $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new \React\Promise\Promise(function () { })); - $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = new Transaction($sender, $loop); $transaction = $transaction->withOptions(array('timeout' => 2)); $promise = $transaction->send($request); @@ -300,20 +281,18 @@ public function testTimeoutExplicitOptionWillStartTimeoutTimerWhenStreamingReque public function testTimeoutExplicitOptionWillStartTimeoutTimerWhenStreamingRequestBodyClosesWhileSenderIsStillPending() { - $messageFactory = new MessageFactory(); - $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $loop->expects($this->once())->method('addTimer')->with(2, $this->anything())->willReturn($timer); $loop->expects($this->never())->method('cancelTimer'); $stream = new ThroughStream(); - $request = $messageFactory->request('POST', 'http://example.com', array(), $stream); + $request = new Request('POST', 'http://example.com', array(), new ReadableBodyStream($stream)); $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new \React\Promise\Promise(function () { })); - $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = new Transaction($sender, $loop); $transaction = $transaction->withOptions(array('timeout' => 2)); $promise = $transaction->send($request); @@ -324,19 +303,17 @@ public function testTimeoutExplicitOptionWillStartTimeoutTimerWhenStreamingReque public function testTimeoutExplicitOptionWillNotStartTimeoutTimerWhenStreamingRequestBodyClosesAfterSenderRejects() { - $messageFactory = new MessageFactory(); - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $loop->expects($this->never())->method('addTimer'); $stream = new ThroughStream(); - $request = $messageFactory->request('POST', 'http://example.com', array(), $stream); + $request = new Request('POST', 'http://example.com', array(), new ReadableBodyStream($stream)); $deferred = new Deferred(); $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn($deferred->promise()); - $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = new Transaction($sender, $loop); $transaction = $transaction->withOptions(array('timeout' => 2)); $promise = $transaction->send($request); @@ -348,8 +325,6 @@ public function testTimeoutExplicitOptionWillNotStartTimeoutTimerWhenStreamingRe public function testTimeoutExplicitOptionWillRejectWhenTimerFiresAfterStreamingRequestBodyCloses() { - $messageFactory = new MessageFactory(); - $timeout = null; $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); @@ -360,12 +335,12 @@ public function testTimeoutExplicitOptionWillRejectWhenTimerFiresAfterStreamingR $loop->expects($this->never())->method('cancelTimer'); $stream = new ThroughStream(); - $request = $messageFactory->request('POST', 'http://example.com', array(), $stream); + $request = new Request('POST', 'http://example.com', array(), new ReadableBodyStream($stream)); $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new \React\Promise\Promise(function () { })); - $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = new Transaction($sender, $loop); $transaction = $transaction->withOptions(array('timeout' => 2)); $promise = $transaction->send($request); @@ -393,7 +368,7 @@ public function testReceivingErrorResponseWillRejectWithResponseException() $sender = $this->makeSenderMock(); $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(Promise\resolve($response)); - $transaction = new Transaction($sender, new MessageFactory(), $loop); + $transaction = new Transaction($sender, $loop); $transaction = $transaction->withOptions(array('timeout' => -1)); $promise = $transaction->send($request); @@ -408,7 +383,6 @@ public function testReceivingErrorResponseWillRejectWithResponseException() public function testReceivingStreamingBodyWillResolveWithBufferedResponseByDefault() { - $messageFactory = new MessageFactory(); $loop = Factory::create(); $stream = new ThroughStream(); @@ -418,13 +392,13 @@ public function testReceivingStreamingBodyWillResolveWithBufferedResponseByDefau }); $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); - $response = $messageFactory->response(1.0, 200, 'OK', array(), $stream); + $response = new Response(200, array(), new ReadableBodyStream($stream)); // mock sender to resolve promise with the given $response in response to the given $request $sender = $this->makeSenderMock(); $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(Promise\resolve($response)); - $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = new Transaction($sender, $loop); $promise = $transaction->send($request); $response = Block\await($promise, $loop); @@ -435,7 +409,6 @@ public function testReceivingStreamingBodyWillResolveWithBufferedResponseByDefau public function testReceivingStreamingBodyWithSizeExceedingMaximumResponseBufferWillRejectAndCloseResponseStream() { - $messageFactory = new MessageFactory(); $loop = Factory::create(); $stream = new ThroughStream(); @@ -443,13 +416,13 @@ public function testReceivingStreamingBodyWithSizeExceedingMaximumResponseBuffer $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); - $response = $messageFactory->response(1.0, 200, 'OK', array('Content-Length' => '100000000'), $stream); + $response = new Response(200, array('Content-Length' => '100000000'), new ReadableBodyStream($stream, 100000000)); // mock sender to resolve promise with the given $response in response to the given $request $sender = $this->makeSenderMock(); $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(Promise\resolve($response)); - $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = new Transaction($sender, $loop); $promise = $transaction->send($request); $this->setExpectedException('OverflowException'); @@ -458,7 +431,6 @@ public function testReceivingStreamingBodyWithSizeExceedingMaximumResponseBuffer public function testCancelBufferingResponseWillCloseStreamAndReject() { - $messageFactory = new MessageFactory(); $loop = Factory::create(); $stream = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); @@ -466,13 +438,13 @@ public function testCancelBufferingResponseWillCloseStreamAndReject() $stream->expects($this->once())->method('close'); $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); - $response = $messageFactory->response(1.0, 200, 'OK', array(), $stream); + $response = new Response(200, array(), new ReadableBodyStream($stream)); // mock sender to resolve promise with the given $response in response to the given $request $sender = $this->makeSenderMock(); $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(Promise\resolve($response)); - $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = new Transaction($sender, $loop); $promise = $transaction->send($request); $promise->cancel(); @@ -482,17 +454,16 @@ public function testCancelBufferingResponseWillCloseStreamAndReject() public function testReceivingStreamingBodyWillResolveWithStreamingResponseIfStreamingIsEnabled() { - $messageFactory = new MessageFactory(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); - $response = $messageFactory->response(1.0, 200, 'OK', array(), $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock()); + $response = new Response(200, array(), new ReadableBodyStream($this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock())); // mock sender to resolve promise with the given $response in response to the given $request $sender = $this->makeSenderMock(); $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(Promise\resolve($response)); - $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = new Transaction($sender, $loop); $transaction = $transaction->withOptions(array('streaming' => true, 'timeout' => -1)); $promise = $transaction->send($request); @@ -504,16 +475,15 @@ public function testReceivingStreamingBodyWillResolveWithStreamingResponseIfStre public function testResponseCode304WithoutLocationWillResolveWithResponseAsIs() { - $messageFactory = new MessageFactory(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); // conditional GET request will respond with 304 (Not Modified - $request = $messageFactory->request('GET', 'http://example.com', array('If-None-Match' => '"abc"')); - $response = $messageFactory->response(1.0, 304, null, array('ETag' => '"abc"')); + $request = new Request('GET', 'http://example.com', array('If-None-Match' => '"abc"')); + $response = new Response(304, array('ETag' => '"abc"')); $sender = $this->makeSenderMock(); $sender->expects($this->once())->method('send')->with($request)->willReturn(Promise\resolve($response)); - $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = new Transaction($sender, $loop); $transaction = $transaction->withOptions(array('timeout' => -1)); $promise = $transaction->send($request); @@ -522,12 +492,11 @@ public function testResponseCode304WithoutLocationWillResolveWithResponseAsIs() public function testCustomRedirectResponseCode333WillFollowLocationHeaderAndSendRedirectedRequest() { - $messageFactory = new MessageFactory(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); // original GET request will respond with custom 333 redirect status code and follow location header - $requestOriginal = $messageFactory->request('GET', 'http://example.com'); - $response = $messageFactory->response(1.0, 333, null, array('Location' => 'foo')); + $requestOriginal = new Request('GET', 'http://example.com'); + $response = new Response(333, array('Location' => 'foo')); $sender = $this->makeSenderMock(); $sender->expects($this->exactly(2))->method('send')->withConsecutive( array($requestOriginal), @@ -539,27 +508,26 @@ public function testCustomRedirectResponseCode333WillFollowLocationHeaderAndSend new \React\Promise\Promise(function () { }) ); - $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = new Transaction($sender, $loop); $transaction->send($requestOriginal); } public function testFollowingRedirectWithSpecifiedHeaders() { - $messageFactory = new MessageFactory(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $customHeaders = array('User-Agent' => 'Chrome'); - $requestWithUserAgent = $messageFactory->request('GET', 'http://example.com', $customHeaders); + $requestWithUserAgent = new Request('GET', 'http://example.com', $customHeaders); $sender = $this->makeSenderMock(); // mock sender to resolve promise with the given $redirectResponse in // response to the given $requestWithUserAgent - $redirectResponse = $messageFactory->response(1.0, 301, null, array('Location' => 'http://redirect.com')); + $redirectResponse = new Response(301, array('Location' => 'http://redirect.com')); $sender->expects($this->at(0))->method('send')->willReturn(Promise\resolve($redirectResponse)); // mock sender to resolve promise with the given $okResponse in // response to the given $requestWithUserAgent - $okResponse = $messageFactory->response(1.0, 200, 'OK'); + $okResponse = new Response(200); $that = $this; $sender->expects($this->at(1)) ->method('send') @@ -568,27 +536,26 @@ public function testFollowingRedirectWithSpecifiedHeaders() return true; }))->willReturn(Promise\resolve($okResponse)); - $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = new Transaction($sender, $loop); $transaction->send($requestWithUserAgent); } public function testRemovingAuthorizationHeaderWhenChangingHostnamesDuringRedirect() { - $messageFactory = new MessageFactory(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $customHeaders = array('Authorization' => 'secret'); - $requestWithAuthorization = $messageFactory->request('GET', 'http://example.com', $customHeaders); + $requestWithAuthorization = new Request('GET', 'http://example.com', $customHeaders); $sender = $this->makeSenderMock(); // mock sender to resolve promise with the given $redirectResponse in // response to the given $requestWithAuthorization - $redirectResponse = $messageFactory->response(1.0, 301, null, array('Location' => 'http://redirect.com')); + $redirectResponse = new Response(301, array('Location' => 'http://redirect.com')); $sender->expects($this->at(0))->method('send')->willReturn(Promise\resolve($redirectResponse)); // mock sender to resolve promise with the given $okResponse in // response to the given $requestWithAuthorization - $okResponse = $messageFactory->response(1.0, 200, 'OK'); + $okResponse = new Response(200); $that = $this; $sender->expects($this->at(1)) ->method('send') @@ -597,27 +564,26 @@ public function testRemovingAuthorizationHeaderWhenChangingHostnamesDuringRedire return true; }))->willReturn(Promise\resolve($okResponse)); - $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = new Transaction($sender, $loop); $transaction->send($requestWithAuthorization); } public function testAuthorizationHeaderIsForwardedWhenRedirectingToSameDomain() { - $messageFactory = new MessageFactory(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $customHeaders = array('Authorization' => 'secret'); - $requestWithAuthorization = $messageFactory->request('GET', 'http://example.com', $customHeaders); + $requestWithAuthorization = new Request('GET', 'http://example.com', $customHeaders); $sender = $this->makeSenderMock(); // mock sender to resolve promise with the given $redirectResponse in // response to the given $requestWithAuthorization - $redirectResponse = $messageFactory->response(1.0, 301, null, array('Location' => 'http://example.com/new')); + $redirectResponse = new Response(301, array('Location' => 'http://example.com/new')); $sender->expects($this->at(0))->method('send')->willReturn(Promise\resolve($redirectResponse)); // mock sender to resolve promise with the given $okResponse in // response to the given $requestWithAuthorization - $okResponse = $messageFactory->response(1.0, 200, 'OK'); + $okResponse = new Response(200); $that = $this; $sender->expects($this->at(1)) ->method('send') @@ -626,26 +592,25 @@ public function testAuthorizationHeaderIsForwardedWhenRedirectingToSameDomain() return true; }))->willReturn(Promise\resolve($okResponse)); - $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = new Transaction($sender, $loop); $transaction->send($requestWithAuthorization); } public function testAuthorizationHeaderIsForwardedWhenLocationContainsAuthentication() { - $messageFactory = new MessageFactory(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $request = $messageFactory->request('GET', 'http://example.com'); + $request = new Request('GET', 'http://example.com'); $sender = $this->makeSenderMock(); // mock sender to resolve promise with the given $redirectResponse in // response to the given $requestWithAuthorization - $redirectResponse = $messageFactory->response(1.0, 301, null, array('Location' => 'http://user:pass@example.com/new')); + $redirectResponse = new Response(301, array('Location' => 'http://user:pass@example.com/new')); $sender->expects($this->at(0))->method('send')->willReturn(Promise\resolve($redirectResponse)); // mock sender to resolve promise with the given $okResponse in // response to the given $requestWithAuthorization - $okResponse = $messageFactory->response(1.0, 200, 'OK'); + $okResponse = new Response(200); $that = $this; $sender->expects($this->at(1)) ->method('send') @@ -655,13 +620,12 @@ public function testAuthorizationHeaderIsForwardedWhenLocationContainsAuthentica return true; }))->willReturn(Promise\resolve($okResponse)); - $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = new Transaction($sender, $loop); $transaction->send($request); } public function testSomeRequestHeadersShouldBeRemovedWhenRedirecting() { - $messageFactory = new MessageFactory(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $customHeaders = array( @@ -669,17 +633,17 @@ public function testSomeRequestHeadersShouldBeRemovedWhenRedirecting() 'Content-Length' => '111', ); - $requestWithCustomHeaders = $messageFactory->request('GET', 'http://example.com', $customHeaders); + $requestWithCustomHeaders = new Request('GET', 'http://example.com', $customHeaders); $sender = $this->makeSenderMock(); // mock sender to resolve promise with the given $redirectResponse in // response to the given $requestWithCustomHeaders - $redirectResponse = $messageFactory->response(1.0, 301, null, array('Location' => 'http://example.com/new')); + $redirectResponse = new Response(301, array('Location' => 'http://example.com/new')); $sender->expects($this->at(0))->method('send')->willReturn(Promise\resolve($redirectResponse)); // mock sender to resolve promise with the given $okResponse in // response to the given $requestWithCustomHeaders - $okResponse = $messageFactory->response(1.0, 200, 'OK'); + $okResponse = new Response(200); $that = $this; $sender->expects($this->at(1)) ->method('send') @@ -689,16 +653,15 @@ public function testSomeRequestHeadersShouldBeRemovedWhenRedirecting() return true; }))->willReturn(Promise\resolve($okResponse)); - $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = new Transaction($sender, $loop); $transaction->send($requestWithCustomHeaders); } public function testCancelTransactionWillCancelRequest() { - $messageFactory = new MessageFactory(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $request = $messageFactory->request('GET', 'http://example.com'); + $request = new Request('GET', 'http://example.com'); $sender = $this->makeSenderMock(); $pending = new \React\Promise\Promise(function () { }, $this->expectCallableOnce()); @@ -706,7 +669,7 @@ public function testCancelTransactionWillCancelRequest() // mock sender to return pending promise which should be cancelled when cancelling result $sender->expects($this->once())->method('send')->willReturn($pending); - $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = new Transaction($sender, $loop); $promise = $transaction->send($request); $promise->cancel(); @@ -714,14 +677,12 @@ public function testCancelTransactionWillCancelRequest() public function testCancelTransactionWillCancelTimeoutTimer() { - $messageFactory = new MessageFactory(); - $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $loop->expects($this->once())->method('addTimer')->willReturn($timer); $loop->expects($this->once())->method('cancelTimer')->with($timer); - $request = $messageFactory->request('GET', 'http://example.com'); + $request = new Request('GET', 'http://example.com'); $sender = $this->makeSenderMock(); $pending = new \React\Promise\Promise(function () { }, function () { throw new \RuntimeException(); }); @@ -729,7 +690,7 @@ public function testCancelTransactionWillCancelTimeoutTimer() // mock sender to return pending promise which should be cancelled when cancelling result $sender->expects($this->once())->method('send')->willReturn($pending); - $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = new Transaction($sender, $loop); $transaction = $transaction->withOptions(array('timeout' => 2)); $promise = $transaction->send($request); @@ -738,14 +699,13 @@ public function testCancelTransactionWillCancelTimeoutTimer() public function testCancelTransactionWillCancelRedirectedRequest() { - $messageFactory = new MessageFactory(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $request = $messageFactory->request('GET', 'http://example.com'); + $request = new Request('GET', 'http://example.com'); $sender = $this->makeSenderMock(); // mock sender to resolve promise with the given $redirectResponse in - $redirectResponse = $messageFactory->response(1.0, 301, null, array('Location' => 'http://example.com/new')); + $redirectResponse = new Response(301, array('Location' => 'http://example.com/new')); $sender->expects($this->at(0))->method('send')->willReturn(Promise\resolve($redirectResponse)); $pending = new \React\Promise\Promise(function () { }, $this->expectCallableOnce()); @@ -753,7 +713,7 @@ public function testCancelTransactionWillCancelRedirectedRequest() // mock sender to return pending promise which should be cancelled when cancelling result $sender->expects($this->at(1))->method('send')->willReturn($pending); - $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = new Transaction($sender, $loop); $promise = $transaction->send($request); $promise->cancel(); @@ -761,10 +721,9 @@ public function testCancelTransactionWillCancelRedirectedRequest() public function testCancelTransactionWillCancelRedirectedRequestAgain() { - $messageFactory = new MessageFactory(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $request = $messageFactory->request('GET', 'http://example.com'); + $request = new Request('GET', 'http://example.com'); $sender = $this->makeSenderMock(); // mock sender to resolve promise with the given $redirectResponse in @@ -776,31 +735,30 @@ public function testCancelTransactionWillCancelRedirectedRequestAgain() // mock sender to return pending promise which should be cancelled when cancelling result $sender->expects($this->at(1))->method('send')->willReturn($second); - $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = new Transaction($sender, $loop); $promise = $transaction->send($request); // mock sender to resolve promise with the given $redirectResponse in - $first->resolve($messageFactory->response(1.0, 301, null, array('Location' => 'http://example.com/new'))); + $first->resolve(new Response(301, array('Location' => 'http://example.com/new'))); $promise->cancel(); } public function testCancelTransactionWillCloseBufferingStream() { - $messageFactory = new MessageFactory(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $request = $messageFactory->request('GET', 'http://example.com'); + $request = new Request('GET', 'http://example.com'); $sender = $this->makeSenderMock(); $body = new ThroughStream(); $body->on('close', $this->expectCallableOnce()); // mock sender to resolve promise with the given $redirectResponse in - $redirectResponse = $messageFactory->response(1.0, 301, null, array('Location' => 'http://example.com/new'), $body); + $redirectResponse = new Response(301, array('Location' => 'http://example.com/new'), new ReadableBodyStream($body)); $sender->expects($this->once())->method('send')->willReturn(Promise\resolve($redirectResponse)); - $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = new Transaction($sender, $loop); $promise = $transaction->send($request); $promise->cancel(); @@ -808,36 +766,34 @@ public function testCancelTransactionWillCloseBufferingStream() public function testCancelTransactionWillCloseBufferingStreamAgain() { - $messageFactory = new MessageFactory(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $request = $messageFactory->request('GET', 'http://example.com'); + $request = new Request('GET', 'http://example.com'); $sender = $this->makeSenderMock(); $first = new Deferred(); $sender->expects($this->once())->method('send')->willReturn($first->promise()); - $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = new Transaction($sender, $loop); $promise = $transaction->send($request); $body = new ThroughStream(); $body->on('close', $this->expectCallableOnce()); // mock sender to resolve promise with the given $redirectResponse in - $first->resolve($messageFactory->response(1.0, 301, null, array('Location' => 'http://example.com/new'), $body)); + $first->resolve(new Response(301, array('Location' => 'http://example.com/new'), new ReadableBodyStream($body))); $promise->cancel(); } public function testCancelTransactionShouldCancelSendingPromise() { - $messageFactory = new MessageFactory(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $request = $messageFactory->request('GET', 'http://example.com'); + $request = new Request('GET', 'http://example.com'); $sender = $this->makeSenderMock(); // mock sender to resolve promise with the given $redirectResponse in - $redirectResponse = $messageFactory->response(1.0, 301, null, array('Location' => 'http://example.com/new')); + $redirectResponse = new Response(301, array('Location' => 'http://example.com/new')); $sender->expects($this->at(0))->method('send')->willReturn(Promise\resolve($redirectResponse)); $pending = new \React\Promise\Promise(function () { }, $this->expectCallableOnce()); @@ -845,7 +801,7 @@ public function testCancelTransactionShouldCancelSendingPromise() // mock sender to return pending promise which should be cancelled when cancelling result $sender->expects($this->at(1))->method('send')->willReturn($pending); - $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = new Transaction($sender, $loop); $promise = $transaction->send($request); $promise->cancel(); From a9505c6e0f8ea66db6498a606d4b8f7a65400224 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 14 Jul 2020 16:05:54 +0200 Subject: [PATCH 340/456] Unify chunked decoder and always skip trailers also for HTTP server Remove dedicated client-side chunked decoder in favor of existing server-side chunked decoder. Unify handling to always skip (unused) trailers for both sides. --- src/Client/ChunkedStreamDecoder.php | 207 --------------------- src/Client/Response.php | 3 +- src/Io/ChunkedDecoder.php | 13 +- tests/Client/DecodeChunkedStreamTest.php | 227 ----------------------- tests/Io/ChunkedDecoderTest.php | 32 +++- 5 files changed, 40 insertions(+), 442 deletions(-) delete mode 100644 src/Client/ChunkedStreamDecoder.php delete mode 100644 tests/Client/DecodeChunkedStreamTest.php diff --git a/src/Client/ChunkedStreamDecoder.php b/src/Client/ChunkedStreamDecoder.php deleted file mode 100644 index 02cab52a..00000000 --- a/src/Client/ChunkedStreamDecoder.php +++ /dev/null @@ -1,207 +0,0 @@ -stream = $stream; - $this->stream->on('data', array($this, 'handleData')); - $this->stream->on('end', array($this, 'handleEnd')); - Util::forwardEvents($this->stream, $this, array( - 'error', - )); - } - - /** @internal */ - public function handleData($data) - { - $this->buffer .= $data; - - do { - $bufferLength = strlen($this->buffer); - $continue = $this->iterateBuffer(); - $iteratedBufferLength = strlen($this->buffer); - } while ( - $continue && - $bufferLength !== $iteratedBufferLength && - $iteratedBufferLength > 0 - ); - - if ($this->buffer === false) { - $this->buffer = ''; - } - } - - protected function iterateBuffer() - { - if (strlen($this->buffer) <= 1) { - return false; - } - - if ($this->nextChunkIsLength) { - $crlfPosition = strpos($this->buffer, static::CRLF); - if ($crlfPosition === false && strlen($this->buffer) > 1024) { - $this->emit('error', array( - new Exception('Chunk length header longer then 1024 bytes'), - )); - $this->close(); - return false; - } - if ($crlfPosition === false) { - return false; // Chunk header hasn't completely come in yet - } - $lengthChunk = substr($this->buffer, 0, $crlfPosition); - if (strpos($lengthChunk, ';') !== false) { - list($lengthChunk) = explode(';', $lengthChunk, 2); - } - if ($lengthChunk !== '') { - $lengthChunk = ltrim(trim($lengthChunk), "0"); - if ($lengthChunk === '') { - // We've reached the end of the stream - $this->reachedEnd = true; - $this->emit('end'); - $this->close(); - return false; - } - } - $this->nextChunkIsLength = false; - if (dechex(@hexdec($lengthChunk)) !== strtolower($lengthChunk)) { - $this->emit('error', array( - new Exception('Unable to validate "' . $lengthChunk . '" as chunk length header'), - )); - $this->close(); - return false; - } - $this->remainingLength = hexdec($lengthChunk); - $this->buffer = substr($this->buffer, $crlfPosition + 2); - return true; - } - - if ($this->remainingLength > 0) { - $chunkLength = $this->getChunkLength(); - if ($chunkLength === 0) { - return true; - } - $this->emit('data', array( - substr($this->buffer, 0, $chunkLength), - $this - )); - $this->remainingLength -= $chunkLength; - $this->buffer = substr($this->buffer, $chunkLength); - return true; - } - - $this->nextChunkIsLength = true; - $this->buffer = substr($this->buffer, 2); - return true; - } - - protected function getChunkLength() - { - $bufferLength = strlen($this->buffer); - - if ($bufferLength >= $this->remainingLength) { - return $this->remainingLength; - } - - return $bufferLength; - } - - public function pause() - { - $this->stream->pause(); - } - - public function resume() - { - $this->stream->resume(); - } - - public function isReadable() - { - return $this->stream->isReadable(); - } - - public function pipe(WritableStreamInterface $dest, array $options = array()) - { - Util::pipe($this, $dest, $options); - - return $dest; - } - - public function close() - { - $this->closed = true; - return $this->stream->close(); - } - - /** @internal */ - public function handleEnd() - { - $this->handleData(''); - - if ($this->closed) { - return; - } - - if ($this->buffer === '' && $this->reachedEnd) { - $this->emit('end'); - $this->close(); - return; - } - - $this->emit( - 'error', - array( - new Exception('Stream ended with incomplete control code') - ) - ); - $this->close(); - } -} diff --git a/src/Client/Response.php b/src/Client/Response.php index 2de64bb0..0510d60b 100644 --- a/src/Client/Response.php +++ b/src/Client/Response.php @@ -3,6 +3,7 @@ namespace React\Http\Client; use Evenement\EventEmitter; +use React\Http\Io\ChunkedDecoder; use React\Stream\ReadableStreamInterface; use React\Stream\Util; use React\Stream\WritableStreamInterface; @@ -33,7 +34,7 @@ public function __construct(ReadableStreamInterface $stream, $protocol, $version $this->headers = $headers; if (strtolower($this->getHeaderLine('Transfer-Encoding')) === 'chunked') { - $this->stream = new ChunkedStreamDecoder($stream); + $this->stream = new ChunkedDecoder($stream); $this->removeHeader('Transfer-Encoding'); } diff --git a/src/Io/ChunkedDecoder.php b/src/Io/ChunkedDecoder.php index f7bbe603..45642a63 100644 --- a/src/Io/ChunkedDecoder.php +++ b/src/Io/ChunkedDecoder.php @@ -116,7 +116,7 @@ public function handleData($data) } if ($hexValue !== '') { - $hexValue = \ltrim($hexValue, "0"); + $hexValue = \ltrim(\trim($hexValue), "0"); if ($hexValue === '') { $hexValue = "0"; } @@ -155,16 +155,19 @@ public function handleData($data) $this->headerCompleted = false; $this->transferredSize = 0; $this->buffer = (string)\substr($this->buffer, 2); + } elseif ($this->chunkSize === 0) { + // end chunk received, skip all trailer data + $this->buffer = (string)\substr($this->buffer, $positionCrlf); } - if ($positionCrlf !== 0 && $this->chunkSize === $this->transferredSize && \strlen($this->buffer) > 2) { - // the first 2 characters are not CLRF, send error event - $this->handleError(new Exception('Chunk does not end with a CLRF')); + if ($positionCrlf !== 0 && $this->chunkSize !== 0 && $this->chunkSize === $this->transferredSize && \strlen($this->buffer) > 2) { + // the first 2 characters are not CRLF, send error event + $this->handleError(new Exception('Chunk does not end with a CRLF')); return; } if ($positionCrlf !== 0 && \strlen($this->buffer) < 2) { - // No CLRF found, wait for additional data which could be a CLRF + // No CRLF found, wait for additional data which could be a CRLF return; } } diff --git a/tests/Client/DecodeChunkedStreamTest.php b/tests/Client/DecodeChunkedStreamTest.php deleted file mode 100644 index f238fb6b..00000000 --- a/tests/Client/DecodeChunkedStreamTest.php +++ /dev/null @@ -1,227 +0,0 @@ - array( - array("4\r\nWiki\r\n5\r\npedia\r\ne\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n"), - ), - 'data-set-2' => array( - array("4\r\nWiki\r\n", "5\r\npedia\r\ne\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n"), - ), - 'data-set-3' => array( - array("4\r\nWiki\r\n", "5\r\n", "pedia\r\ne\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n"), - ), - 'data-set-4' => array( - array("4\r\nWiki\r\n", "5\r\n", "pedia\r\ne\r\n in\r\n", "\r\nchunks.\r\n0\r\n\r\n"), - ), - 'data-set-5' => array( - array("4\r\n", "Wiki\r\n", "5\r\n", "pedia\r\ne\r\n in\r\n", "\r\nchunks.\r\n0\r\n\r\n"), - ), - 'data-set-6' => array( - array("4\r\n", "Wiki\r\n", "5\r\n", "pedia\r\ne; foo=[bar,beer,pool,cue,win,won]\r\n", " in\r\n", "\r\nchunks.\r\n0\r\n\r\n"), - ), - 'header-fields' => array( - array("4; foo=bar\r\n", "Wiki\r\n", "5\r\n", "pedia\r\ne\r\n", " in\r\n", "\r\nchunks.\r\n", "0\r\n\r\n"), - ), - 'character-for-charactrr' => array( - str_split("4\r\nWiki\r\n5\r\npedia\r\ne\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n"), - ), - 'extra-newline-in-wiki-character-for-chatacter' => array( - str_split("6\r\nWi\r\nki\r\n5\r\npedia\r\ne\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n"), - "Wi\r\nkipedia in\r\n\r\nchunks." - ), - 'extra-newline-in-wiki' => array( - array("6\r\nWi\r\n", "ki\r\n5\r\npedia\r\ne\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n"), - "Wi\r\nkipedia in\r\n\r\nchunks." - ), - 'varnish-type-response-1' => array( - array("0017\r\nWikipedia in\r\n\r\nchunks.\r\n0\r\n\r\n") - ), - 'varnish-type-response-2' => array( - array("000017\r\nWikipedia in\r\n\r\nchunks.\r\n0\r\n\r\n") - ), - 'varnish-type-response-3' => array( - array("017\r\nWikipedia in\r\n\r\nchunks.\r\n0\r\n\r\n") - ), - 'varnish-type-response-4' => array( - array("004\r\nWiki\r\n005\r\npedia\r\n00e\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n") - ), - 'varnish-type-response-5' => array( - array("000004\r\nWiki\r\n00005\r\npedia\r\n000e\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n") - ), - 'varnish-type-response-extra-line' => array( - array("006\r\nWi\r\nki\r\n005\r\npedia\r\n00e\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n"), - "Wi\r\nkipedia in\r\n\r\nchunks." - ), - 'varnish-type-response-random' => array( - array(str_repeat("0", rand(0, 10)), "4\r\nWiki\r\n", str_repeat("0", rand(0, 10)), "5\r\npedia\r\n", str_repeat("0", rand(0, 10)), "e\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n") - ), - 'end-chunk-zero-check-1' => array( - array("4\r\nWiki\r\n5\r\npedia\r\ne\r\n in\r\n\r\nchunks.\r\n00\r\n\r\n") - ), - 'end-chunk-zero-check-2' => array( - array("4\r\nWiki\r\n5\r\npedia\r\ne\r\n in\r\n\r\nchunks.\r\n000\r\n\r\n") - ), - 'end-chunk-zero-check-3' => array( - array("00004\r\nWiki\r\n005\r\npedia\r\ne\r\n in\r\n\r\nchunks.\r\n0000\r\n\r\n") - ), - 'uppercase-chunk' => array( - array("4\r\nWiki\r\n5\r\npedia\r\nE\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n"), - ), - 'extra-space-in-length-chunk' => array( - array(" 04 \r\nWiki\r\n5\r\npedia\r\nE\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n"), - ), - 'only-whitespace-is-final-chunk' => array( - array(" \r\n\r\n"), - "" - ) - ); - } - - /** - * @test - * @dataProvider provideChunkedEncoding - */ - public function testChunkedEncoding(array $strings, $expected = "Wikipedia in\r\n\r\nchunks.") - { - $stream = new ThroughStream(); - $response = new ChunkedStreamDecoder($stream); - $buffer = ''; - $response->on('data', function ($data) use (&$buffer) { - $buffer .= $data; - }); - $response->on('error', function ($error) { - $this->fail((string)$error); - }); - foreach ($strings as $string) { - $stream->write($string); - } - $this->assertSame($expected, $buffer); - } - - public function provideInvalidChunkedEncoding() - { - return array( - 'chunk-body-longer-than-header-suggests' => array( - array("4\r\nWiwot40n98w3498tw3049nyn039409t34\r\n", "ki\r\n5\r\npedia\r\ne\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n"), - ), - 'invalid-header-charactrrs' => array( - str_split("xyz\r\nWi\r\nki\r\n5\r\npedia\r\ne\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n") - ), - 'header-chunk-to-long' => array( - str_split(str_repeat('a', 2015) . "\r\nWi\r\nki\r\n5\r\npedia\r\ne\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n") - ) - ); - } - - /** - * @test - * @dataProvider provideInvalidChunkedEncoding - */ - public function testInvalidChunkedEncoding(array $strings) - { - $stream = new ThroughStream(); - $response = new ChunkedStreamDecoder($stream); - $response->on('error', function (Exception $exception) { - throw $exception; - }); - - $this->setExpectedException('Exception'); - foreach ($strings as $string) { - $stream->write($string); - } - } - - public function provideZeroChunk() - { - return array( - array('1-zero' => "0\r\n\r\n"), - array('random-zero' => str_repeat("0", rand(2, 10))."\r\n\r\n") - ); - } - - /** - * @test - * @dataProvider provideZeroChunk - */ - public function testHandleEnd($zeroChunk) - { - $ended = false; - $stream = new ThroughStream(); - $response = new ChunkedStreamDecoder($stream); - $response->on('error', function ($error) { - $this->fail((string)$error); - }); - $response->on('end', function () use (&$ended) { - $ended = true; - }); - - $stream->write("4\r\nWiki\r\n".$zeroChunk); - - $this->assertTrue($ended); - } - - public function testHandleEndIncomplete() - { - $exception = null; - $stream = new ThroughStream(); - $response = new ChunkedStreamDecoder($stream); - $response->on('error', function ($e) use (&$exception) { - $exception = $e; - }); - - $stream->end("4\r\nWiki"); - - $this->assertInstanceOf('Exception', $exception); - } - - public function testHandleEndTrailers() - { - $ended = false; - $stream = new ThroughStream(); - $response = new ChunkedStreamDecoder($stream); - $response->on('error', function ($error) { - $this->fail((string)$error); - }); - $response->on('end', function () use (&$ended) { - $ended = true; - }); - - $stream->write("4\r\nWiki\r\n0\r\nabc: def\r\nghi: klm\r\n\r\n"); - - $this->assertTrue($ended); - } - - /** - * @test - * @dataProvider provideZeroChunk - */ - public function testHandleEndEnsureNoError($zeroChunk) - { - $ended = false; - $stream = new ThroughStream(); - $response = new ChunkedStreamDecoder($stream); - $response->on('error', function ($error) { - $this->fail((string)$error); - }); - $response->on('end', function () use (&$ended) { - $ended = true; - }); - - $stream->write("4\r\nWiki\r\n"); - $stream->write($zeroChunk); - $stream->end(); - - $this->assertTrue($ended); - } -} diff --git a/tests/Io/ChunkedDecoderTest.php b/tests/Io/ChunkedDecoderTest.php index c7889b3f..b78a4cdd 100644 --- a/tests/Io/ChunkedDecoderTest.php +++ b/tests/Io/ChunkedDecoderTest.php @@ -8,7 +8,6 @@ class ChunkedDecoderTest extends TestCase { - /** * @before */ @@ -101,7 +100,6 @@ public function testSplittedHeader() $this->parser->on('end', $this->expectCallableNever());# $this->parser->on('error', $this->expectCallableNever()); - $this->input->emit('data', array("4")); $this->input->emit('data', array("\r\nwelt\r\n")); } @@ -423,6 +421,36 @@ public function testLeadingZerosInEndChunkWillBeIgnored() $this->input->emit('data', array("0000\r\n\r\n")); } + public function testAdditionalWhitespaceInEndChunkWillBeIgnored() + { + $this->parser->on('data', $this->expectCallableNever()); + $this->parser->on('error', $this->expectCallableNever()); + $this->parser->on('end', $this->expectCallableOnce()); + $this->parser->on('close', $this->expectCallableOnce()); + + $this->input->emit('data', array(" 0 \r\n\r\n")); + } + + public function testEndChunkWithTrailersWillBeIgnored() + { + $this->parser->on('data', $this->expectCallableNever()); + $this->parser->on('error', $this->expectCallableNever()); + $this->parser->on('end', $this->expectCallableOnce()); + $this->parser->on('close', $this->expectCallableOnce()); + + $this->input->emit('data', array("0\r\nFoo: bar\r\n\r\n")); + } + + public function testEndChunkWithMultipleTrailersWillBeIgnored() + { + $this->parser->on('data', $this->expectCallableNever()); + $this->parser->on('error', $this->expectCallableNever()); + $this->parser->on('end', $this->expectCallableOnce()); + $this->parser->on('close', $this->expectCallableOnce()); + + $this->input->emit('data', array("0\r\nFoo: a\r\nBar: b\r\nBaz: c\r\n\r\n")); + } + public function testLeadingZerosInInvalidChunk() { $this->parser->on('data', $this->expectCallableNever()); From bbe6cd5cc3beb56449a491b23df6c6a07e1e922c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 24 Jul 2020 18:56:26 +0200 Subject: [PATCH 341/456] Expose `Transfer-Encoding: chunked` header and fix chunked HEAD response --- src/Client/Response.php | 16 -------- src/Io/Sender.php | 5 ++- tests/Client/ResponseTest.php | 65 --------------------------------- tests/FunctionalBrowserTest.php | 40 ++++++++++++++++---- 4 files changed, 37 insertions(+), 89 deletions(-) diff --git a/src/Client/Response.php b/src/Client/Response.php index 0510d60b..132e0a54 100644 --- a/src/Client/Response.php +++ b/src/Client/Response.php @@ -3,7 +3,6 @@ namespace React\Http\Client; use Evenement\EventEmitter; -use React\Http\Io\ChunkedDecoder; use React\Stream\ReadableStreamInterface; use React\Stream\Util; use React\Stream\WritableStreamInterface; @@ -33,11 +32,6 @@ public function __construct(ReadableStreamInterface $stream, $protocol, $version $this->reasonPhrase = $reasonPhrase; $this->headers = $headers; - if (strtolower($this->getHeaderLine('Transfer-Encoding')) === 'chunked') { - $this->stream = new ChunkedDecoder($stream); - $this->removeHeader('Transfer-Encoding'); - } - $this->stream->on('data', array($this, 'handleData')); $this->stream->on('error', array($this, 'handleError')); $this->stream->on('end', array($this, 'handleEnd')); @@ -69,16 +63,6 @@ public function getHeaders() return $this->headers; } - private function removeHeader($name) - { - foreach ($this->headers as $key => $value) { - if (strcasecmp($name, $key) === 0) { - unset($this->headers[$key]); - break; - } - } - } - private function getHeader($name) { $name = strtolower($name); diff --git a/src/Io/Sender.php b/src/Io/Sender.php index 6cba0495..353fa536 100644 --- a/src/Io/Sender.php +++ b/src/Io/Sender.php @@ -110,9 +110,12 @@ public function send(RequestInterface $request) $requestStream->on('response', function (ResponseStream $responseStream) use ($deferred, $request) { $length = null; + $body = $responseStream; $code = $responseStream->getCode(); if ($request->getMethod() === 'HEAD' || ($code >= 100 && $code < 200) || $code == 204 || $code == 304) { $length = 0; + } elseif (\strtolower($responseStream->getHeaderLine('Transfer-Encoding')) === 'chunked') { + $body = new ChunkedDecoder($body); } elseif ($responseStream->hasHeader('Content-Length')) { $length = (int) $responseStream->getHeaderLine('Content-Length'); } @@ -121,7 +124,7 @@ public function send(RequestInterface $request) $deferred->resolve(new Response( $responseStream->getCode(), $responseStream->getHeaders(), - new ReadableBodyStream($responseStream, $length), + new ReadableBodyStream($body, $length), $responseStream->getVersion(), $responseStream->getReasonPhrase() )); diff --git a/tests/Client/ResponseTest.php b/tests/Client/ResponseTest.php index 14467239..0e757619 100644 --- a/tests/Client/ResponseTest.php +++ b/tests/Client/ResponseTest.php @@ -99,70 +99,5 @@ public function closedResponseShouldNotBeResumedOrPaused() $response->getHeaders() ); } - - /** @test */ - public function chunkedEncodingResponse() - { - $stream = new ThroughStream(); - $response = new Response( - $stream, - 'http', - '1.0', - '200', - 'ok', - array( - 'content-type' => 'text/plain', - 'transfer-encoding' => 'chunked', - ) - ); - - $buffer = ''; - $response->on('data', function ($data) use (&$buffer) { - $buffer.= $data; - }); - $this->assertSame('', $buffer); - $stream->write("4; abc=def\r\n"); - $this->assertSame('', $buffer); - $stream->write("Wiki\r\n"); - $this->assertSame('Wiki', $buffer); - - $this->assertSame( - array( - 'content-type' => 'text/plain', - ), - $response->getHeaders() - ); - } - - /** @test */ - public function doubleChunkedEncodingResponseWillBePassedAsIs() - { - $stream = new ThroughStream(); - $response = new Response( - $stream, - 'http', - '1.0', - '200', - 'ok', - array( - 'content-type' => 'text/plain', - 'transfer-encoding' => array( - 'chunked', - 'chunked' - ) - ) - ); - - $this->assertSame( - array( - 'content-type' => 'text/plain', - 'transfer-encoding' => array( - 'chunked', - 'chunked' - ) - ), - $response->getHeaders() - ); - } } diff --git a/tests/FunctionalBrowserTest.php b/tests/FunctionalBrowserTest.php index 59cba0b9..f5b7f324 100644 --- a/tests/FunctionalBrowserTest.php +++ b/tests/FunctionalBrowserTest.php @@ -462,7 +462,7 @@ public function testPostString() $this->assertEquals('hello world', $data['data']); } - public function testReceiveStreamUntilConnectionsEndsForHttp10() + public function testRequestStreamReturnsResponseBodyUntilConnectionsEndsForHttp10() { $response = Block\await($this->browser->withProtocolVersion('1.0')->get($this->base . 'stream/1'), $this->loop); @@ -473,21 +473,47 @@ public function testReceiveStreamUntilConnectionsEndsForHttp10() $this->assertStringEndsWith('}', (string) $response->getBody()); } - public function testReceiveStreamChunkedForHttp11() + public function testRequestStreamReturnsResponseWithTransferEncodingChunkedAndResponseBodyDecodedForHttp11() { - $response = Block\await($this->browser->request('GET', $this->base . 'stream/1'), $this->loop); + $response = Block\await($this->browser->get($this->base . 'stream/1'), $this->loop); $this->assertEquals('1.1', $response->getProtocolVersion()); - // underlying http-client automatically decodes and doesn't expose header - // @link https://github.com/reactphp/http-client/pull/58 - // $this->assertEquals('chunked', $response->getHeaderLine('Transfer-Encoding')); - $this->assertFalse($response->hasHeader('Transfer-Encoding')); + $this->assertEquals('chunked', $response->getHeaderLine('Transfer-Encoding')); $this->assertStringStartsWith('{', (string) $response->getBody()); $this->assertStringEndsWith('}', (string) $response->getBody()); } + public function testRequestStreamWithHeadRequestReturnsEmptyResponseBodWithTransferEncodingChunkedForHttp11() + { + $response = Block\await($this->browser->head($this->base . 'stream/1'), $this->loop); + + $this->assertEquals('1.1', $response->getProtocolVersion()); + + $this->assertEquals('chunked', $response->getHeaderLine('Transfer-Encoding')); + $this->assertEquals('', (string) $response->getBody()); + } + + public function testRequestStreamReturnsResponseWithResponseBodyUndecodedWhenResponseHasDoubleTransferEncoding() + { + $socket = new \React\Socket\Server(0, $this->loop); + $socket->on('connection', function (\React\Socket\ConnectionInterface $connection) { + $connection->on('data', function () use ($connection) { + $connection->end("HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked, chunked\r\nConnection: close\r\n\r\nhello"); + }); + }); + + $this->base = str_replace('tcp:', 'http:', $socket->getAddress()) . '/'; + + $response = Block\await($this->browser->get($this->base . 'stream/1'), $this->loop); + + $this->assertEquals('1.1', $response->getProtocolVersion()); + + $this->assertEquals('chunked, chunked', $response->getHeaderLine('Transfer-Encoding')); + $this->assertEquals('hello', (string) $response->getBody()); + } + public function testReceiveStreamAndExplicitlyCloseConnectionEvenWhenServerKeepsConnectionOpen() { $closed = new \React\Promise\Deferred(); From a58996a7673dd05739ca3a26c37e53084012c6e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 4 Aug 2020 13:49:29 +0200 Subject: [PATCH 342/456] Minor documentation improvements --- README.md | 10 +++++----- src/Browser.php | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 78709bba..e3532555 100644 --- a/README.md +++ b/README.md @@ -2185,13 +2185,13 @@ given timeout value applied. #### withFollowRedirects() -The `withTimeout(bool|int $$followRedirects): Browser` method can be used to +The `withTimeout(bool|int $followRedirects): Browser` method can be used to change how HTTP redirects will be followed. You can pass in the maximum number of redirects to follow: ```php -$new = $browser->withFollowRedirects(5); +$browser = $browser->withFollowRedirects(5); ``` The request will automatically be rejected when the number of redirects @@ -2326,9 +2326,9 @@ If you want to explicitly use the legacy HTTP/1.0 protocol version, you can use this method: ```php -$newBrowser = $browser->withProtocolVersion('1.0'); +$browser = $browser->withProtocolVersion('1.0'); -$newBrowser->get($url)->then(…); +$browser->get($url)->then(…); ``` Notice that the [`Browser`](#browser) is an immutable object, i.e. this @@ -2337,7 +2337,7 @@ new protocol version applied. #### withResponseBuffer() -The `withRespomseBuffer(int $maximumSize): Browser` method can be used to +The `withResponseBuffer(int $maximumSize): Browser` method can be used to change the maximum size for buffering a response body. The preferred way to send an HTTP request is by using the above diff --git a/src/Browser.php b/src/Browser.php index 91604994..816b86ba 100644 --- a/src/Browser.php +++ b/src/Browser.php @@ -433,7 +433,7 @@ public function withTimeout($timeout) * You can pass in the maximum number of redirects to follow: * * ```php - * $new = $browser->withFollowRedirects(5); + * $browser = $browser->withFollowRedirects(5); * ``` * * The request will automatically be rejected when the number of redirects @@ -604,9 +604,9 @@ public function withBase($baseUrl) * can use this method: * * ```php - * $newBrowser = $browser->withProtocolVersion('1.0'); + * $browser = $browser->withProtocolVersion('1.0'); * - * $newBrowser->get($url)->then(…); + * $browser->get($url)->then(…); * ``` * * Notice that the [`Browser`](#browser) is an immutable object, i.e. this From 0c980e466e30e79e776c41afd734714e82e55e90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 21 Aug 2020 10:27:55 +0200 Subject: [PATCH 343/456] Remove internal Response class, use PSR-7 response instead --- src/Client/Request.php | 66 +------- src/Client/Response.php | 173 --------------------- src/Io/Sender.php | 23 +-- tests/Client/FunctionalIntegrationTest.php | 19 +-- tests/Client/RequestTest.php | 168 ++------------------ tests/Client/ResponseTest.php | 103 ------------ 6 files changed, 36 insertions(+), 516 deletions(-) delete mode 100644 src/Client/Response.php delete mode 100644 tests/Client/ResponseTest.php diff --git a/src/Client/Request.php b/src/Client/Request.php index 7ebb627f..51e03313 100644 --- a/src/Client/Request.php +++ b/src/Client/Request.php @@ -138,7 +138,8 @@ public function handleData($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")) { try { - list($response, $bodyChunk) = $this->parseResponse($this->buffer); + $response = gPsr\parse_response($this->buffer); + $bodyChunk = (string) $response->getBody(); } catch (\InvalidArgumentException $exception) { $this->emit('error', array($exception)); } @@ -155,17 +156,9 @@ public function handleData($data) return; } - $response->on('close', array($this, 'close')); - $that = $this; - $response->on('error', function (\Exception $error) use ($that) { - $that->closeError(new \RuntimeException( - "An error occured in the response", - 0, - $error - )); - }); + $this->stream->on('close', array($this, 'handleClose')); - $this->emit('response', array($response, $this)); + $this->emit('response', array($response, $this->stream)); $this->stream->emit('data', array($bodyChunk)); } @@ -222,30 +215,6 @@ public function close() $this->removeAllListeners(); } - protected function parseResponse($data) - { - $psrResponse = gPsr\parse_response($data); - $headers = array_map(function($val) { - if (1 === count($val)) { - $val = $val[0]; - } - - return $val; - }, $psrResponse->getHeaders()); - - $factory = $this->getResponseFactory(); - - $response = $factory( - 'HTTP', - $psrResponse->getProtocolVersion(), - $psrResponse->getStatusCode(), - $psrResponse->getReasonPhrase(), - $headers - ); - - return array($response, (string)($psrResponse->getBody())); - } - protected function connect() { $scheme = $this->requestData->getScheme(); @@ -265,31 +234,4 @@ protected function connect() return $this->connector ->connect($host . ':' . $port); } - - public function setResponseFactory($factory) - { - $this->responseFactory = $factory; - } - - public function getResponseFactory() - { - if (null === $factory = $this->responseFactory) { - $stream = $this->stream; - - $factory = function ($protocol, $version, $code, $reasonPhrase, $headers) use ($stream) { - return new Response( - $stream, - $protocol, - $version, - $code, - $reasonPhrase, - $headers - ); - }; - - $this->responseFactory = $factory; - } - - return $factory; - } } diff --git a/src/Client/Response.php b/src/Client/Response.php deleted file mode 100644 index 132e0a54..00000000 --- a/src/Client/Response.php +++ /dev/null @@ -1,173 +0,0 @@ -stream = $stream; - $this->protocol = $protocol; - $this->version = $version; - $this->code = $code; - $this->reasonPhrase = $reasonPhrase; - $this->headers = $headers; - - $this->stream->on('data', array($this, 'handleData')); - $this->stream->on('error', array($this, 'handleError')); - $this->stream->on('end', array($this, 'handleEnd')); - $this->stream->on('close', array($this, 'handleClose')); - } - - public function getProtocol() - { - return $this->protocol; - } - - public function getVersion() - { - return $this->version; - } - - public function getCode() - { - return $this->code; - } - - public function getReasonPhrase() - { - return $this->reasonPhrase; - } - - public function getHeaders() - { - return $this->headers; - } - - private function getHeader($name) - { - $name = strtolower($name); - $normalized = array_change_key_case($this->headers, CASE_LOWER); - - return isset($normalized[$name]) ? (array)$normalized[$name] : array(); - } - - /** - * @param string $name - * @return string - */ - public function getHeaderLine($name) - { - return implode(', ' , $this->getHeader($name)); - } - - /** - * @param string $name - * @return bool - */ - public function hasHeader($name) - { - return $this->getHeader($name) !== array(); - } - - /** @internal */ - public function handleData($data) - { - if ($this->readable) { - $this->emit('data', array($data)); - } - } - - /** @internal */ - public function handleEnd() - { - if (!$this->readable) { - return; - } - $this->emit('end'); - $this->close(); - } - - /** @internal */ - public function handleError(\Exception $error) - { - if (!$this->readable) { - return; - } - $this->emit('error', array(new \RuntimeException( - "An error occurred in the underlying stream", - 0, - $error - ))); - - $this->close(); - } - - /** @internal */ - public function handleClose() - { - $this->close(); - } - - public function close() - { - if (!$this->readable) { - return; - } - - $this->readable = false; - $this->stream->close(); - - $this->emit('close'); - $this->removeAllListeners(); - } - - public function isReadable() - { - return $this->readable; - } - - public function pause() - { - if (!$this->readable) { - return; - } - - $this->stream->pause(); - } - - public function resume() - { - if (!$this->readable) { - return; - } - - $this->stream->resume(); - } - - public function pipe(WritableStreamInterface $dest, array $options = array()) - { - Util::pipe($this, $dest, $options); - - return $dest; - } -} diff --git a/src/Io/Sender.php b/src/Io/Sender.php index 353fa536..1eb098c6 100644 --- a/src/Io/Sender.php +++ b/src/Io/Sender.php @@ -3,10 +3,9 @@ namespace React\Http\Io; use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ResponseInterface; use React\EventLoop\LoopInterface; use React\Http\Client\Client as HttpClient; -use React\Http\Client\Response as ResponseStream; -use React\Http\Message\Response; use React\Promise\PromiseInterface; use React\Promise\Deferred; use React\Socket\ConnectorInterface; @@ -108,26 +107,18 @@ public function send(RequestInterface $request) $deferred->reject($error); }); - $requestStream->on('response', function (ResponseStream $responseStream) use ($deferred, $request) { + $requestStream->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($deferred, $request) { $length = null; - $body = $responseStream; - $code = $responseStream->getCode(); + $code = $response->getStatusCode(); if ($request->getMethod() === 'HEAD' || ($code >= 100 && $code < 200) || $code == 204 || $code == 304) { $length = 0; - } elseif (\strtolower($responseStream->getHeaderLine('Transfer-Encoding')) === 'chunked') { + } elseif (\strtolower($response->getHeaderLine('Transfer-Encoding')) === 'chunked') { $body = new ChunkedDecoder($body); - } elseif ($responseStream->hasHeader('Content-Length')) { - $length = (int) $responseStream->getHeaderLine('Content-Length'); + } elseif ($response->hasHeader('Content-Length')) { + $length = (int) $response->getHeaderLine('Content-Length'); } - // apply response header values from response stream - $deferred->resolve(new Response( - $responseStream->getCode(), - $responseStream->getHeaders(), - new ReadableBodyStream($body, $length), - $responseStream->getVersion(), - $responseStream->getReasonPhrase() - )); + $deferred->resolve($response->withBody(new ReadableBodyStream($body, $length))); }); if ($body instanceof ReadableStreamInterface) { diff --git a/tests/Client/FunctionalIntegrationTest.php b/tests/Client/FunctionalIntegrationTest.php index db82b1f1..57861f2c 100644 --- a/tests/Client/FunctionalIntegrationTest.php +++ b/tests/Client/FunctionalIntegrationTest.php @@ -3,13 +3,14 @@ namespace React\Tests\Http\Client; use Clue\React\Block; +use Psr\Http\Message\ResponseInterface; use React\EventLoop\Factory; use React\Http\Client\Client; -use React\Http\Client\Response; use React\Promise\Deferred; use React\Promise\Stream; use React\Socket\Server; use React\Socket\ConnectionInterface; +use React\Stream\ReadableStreamInterface; use React\Tests\Http\TestCase; class FunctionalIntegrationTest extends TestCase @@ -69,8 +70,8 @@ public function testRequestLegacyHttpServerWithOnlyLineFeedReturnsSuccessfulResp $request = $client->request('GET', str_replace('tcp:', 'http:', $server->getAddress())); $once = $this->expectCallableOnceWith('body'); - $request->on('response', function (Response $response) use ($once) { - $response->on('data', $once); + $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($once) { + $body->on('data', $once); }); $promise = Stream\first($request, 'close'); @@ -88,8 +89,8 @@ public function testSuccessfulResponseEmitsEnd() $request = $client->request('GET', 'http://www.google.com/'); $once = $this->expectCallableOnce(); - $request->on('response', function (Response $response) use ($once) { - $response->on('end', $once); + $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($once) { + $body->on('end', $once); }); $promise = Stream\first($request, 'close'); @@ -112,8 +113,8 @@ public function testPostDataReturnsData() $request = $client->request('POST', 'https://' . (mt_rand(0, 1) === 0 ? 'eu.' : '') . 'httpbin.org/post', array('Content-Length' => strlen($data))); $deferred = new Deferred(); - $request->on('response', function (Response $response) use ($deferred) { - $deferred->resolve(Stream\buffer($response)); + $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($deferred) { + $deferred->resolve(Stream\buffer($body)); }); $request->on('error', 'printf'); @@ -145,8 +146,8 @@ public function testPostJsonReturnsData() $request = $client->request('POST', 'https://httpbin.org/post', array('Content-Length' => strlen($data), 'Content-Type' => 'application/json')); $deferred = new Deferred(); - $request->on('response', function (Response $response) use ($deferred) { - $deferred->resolve(Stream\buffer($response)); + $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($deferred) { + $deferred->resolve(Stream\buffer($body)); }); $request->on('error', 'printf'); diff --git a/tests/Client/RequestTest.php b/tests/Client/RequestTest.php index e702d315..207aa15f 100644 --- a/tests/Client/RequestTest.php +++ b/tests/Client/RequestTest.php @@ -26,10 +26,6 @@ public function setUpStream() $this->connector = $this->getMockBuilder('React\Socket\ConnectorInterface') ->getMock(); - - $this->response = $this->getMockBuilder('React\Http\Client\Response') - ->disableOriginalConstructor() - ->getMock(); } /** @test */ @@ -81,48 +77,13 @@ public function requestShouldBindToStreamEventsAndUseconnector() ->method('removeListener') ->with('close', $this->identicalTo(array($request, 'handleClose'))); - $response = $this->response; - - $this->stream->expects($this->once()) - ->method('emit') - ->with('data', $this->identicalTo(array('body'))); - - $response->expects($this->at(0)) - ->method('on') - ->with('close', $this->anything()) - ->will($this->returnCallback(function ($event, $cb) use (&$endCallback) { - $endCallback = $cb; - })); - - $factory = $this->createCallableMock(); - $factory->expects($this->once()) - ->method('__invoke') - ->with('HTTP', '1.0', '200', 'OK', array('Content-Type' => 'text/plain')) - ->will($this->returnValue($response)); - - $request->setResponseFactory($factory); - - $handler = $this->createCallableMock(); - $handler->expects($this->once()) - ->method('__invoke') - ->with($response); - - $request->on('response', $handler); $request->on('end', $this->expectCallableNever()); - $handler = $this->createCallableMock(); - $handler->expects($this->once()) - ->method('__invoke'); - - $request->on('close', $handler); $request->end(); $request->handleData("HTTP/1.0 200 OK\r\n"); $request->handleData("Content-Type: text/plain\r\n"); $request->handleData("\r\nbody"); - - $this->assertNotNull($endCallback); - call_user_func($endCallback); } /** @test */ @@ -209,7 +170,7 @@ public function requestShouldEmitErrorIfConnectionEmitsError() } /** @test */ - public function requestShouldEmitErrorIfGuzzleParseThrowsException() + public function requestShouldEmitErrorIfRequestParserThrowsException() { $requestData = new RequestData('GET', 'http://www.example.com'); $request = new Request($this->connector, $requestData); @@ -288,12 +249,6 @@ public function postRequestShouldSendAPostRequest() ->method('write') ->with($this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\nUser-Agent:.*\r\n\r\nsome post data$#")); - $factory = $this->createCallableMock(); - $factory->expects($this->once()) - ->method('__invoke') - ->will($this->returnValue($this->response)); - - $request->setResponseFactory($factory); $request->end('some post data'); $request->handleData("HTTP/1.0 200 OK\r\n"); @@ -322,13 +277,6 @@ public function writeWithAPostRequestShouldSendToTheStream() ->method('write') ->with($this->identicalTo("data")); - $factory = $this->createCallableMock(); - $factory->expects($this->once()) - ->method('__invoke') - ->will($this->returnValue($this->response)); - - $request->setResponseFactory($factory); - $request->write("some"); $request->write("post"); $request->end("data"); @@ -356,13 +304,6 @@ public function writeWithAPostRequestShouldSendBodyAfterHeadersAndEmitDrainEvent ->method('write') ->with($this->identicalTo("data")); - $factory = $this->createCallableMock(); - $factory->expects($this->once()) - ->method('__invoke') - ->will($this->returnValue($this->response)); - - $request->setResponseFactory($factory); - $this->assertFalse($request->write("some")); $this->assertFalse($request->write("post")); @@ -402,13 +343,6 @@ public function writeWithAPostRequestShouldForwardDrainEventIfFirstChunkExceedsB ->method('write') ->with($this->identicalTo("data")); - $factory = $this->createCallableMock(); - $factory->expects($this->once()) - ->method('__invoke') - ->will($this->returnValue($this->response)); - - $request->setResponseFactory($factory); - $this->assertFalse($request->write("some")); $this->assertFalse($request->write("post")); @@ -447,17 +381,10 @@ public function pipeShouldPipeDataIntoTheRequestBody() ->method('write') ->with($this->identicalTo("data")); - $factory = $this->createCallableMock(); - $factory->expects($this->once()) - ->method('__invoke') - ->will($this->returnValue($this->response)); - $loop = $this ->getMockBuilder('React\EventLoop\LoopInterface') ->getMock(); - $request->setResponseFactory($factory); - $stream = fopen('php://memory', 'r+'); $stream = new DuplexResourceStream($stream, $loop); @@ -572,43 +499,6 @@ public function closeShouldCancelPendingConnectionAttempt() $request->close(); } - /** @test */ - public function requestShouldRelayErrorEventsFromResponse() - { - $requestData = new RequestData('GET', 'http://www.example.com'); - $request = new Request($this->connector, $requestData); - - $this->successfulConnectionMock(); - - $response = $this->response; - - $response->expects($this->at(0)) - ->method('on') - ->with('close', $this->anything()); - $response->expects($this->at(1)) - ->method('on') - ->with('error', $this->anything()) - ->will($this->returnCallback(function ($event, $cb) use (&$errorCallback) { - $errorCallback = $cb; - })); - - $factory = $this->createCallableMock(); - $factory->expects($this->once()) - ->method('__invoke') - ->with('HTTP', '1.0', '200', 'OK', array('Content-Type' => 'text/plain')) - ->will($this->returnValue($response)); - - $request->setResponseFactory($factory); - $request->end(); - - $request->handleData("HTTP/1.0 200 OK\r\n"); - $request->handleData("Content-Type: text/plain\r\n"); - $request->handleData("\r\nbody"); - - $this->assertNotNull($errorCallback); - call_user_func($errorCallback, new \Exception('test')); - } - /** @test */ public function requestShouldRemoveAllListenerAfterClosed() { @@ -660,25 +550,12 @@ public function multivalueHeader() $this->successfulConnectionMock(); - $response = $this->response; - - $response->expects($this->at(0)) - ->method('on') - ->with('close', $this->anything()); - $response->expects($this->at(1)) - ->method('on') - ->with('error', $this->anything()) - ->will($this->returnCallback(function ($event, $cb) use (&$errorCallback) { - $errorCallback = $cb; - })); - - $factory = $this->createCallableMock(); - $factory->expects($this->once()) - ->method('__invoke') - ->with('HTTP', '1.0', '200', 'OK', array('Content-Type' => 'text/plain', 'X-Xss-Protection' => '1; mode=block', 'Cache-Control' => 'public, must-revalidate, max-age=0')) - ->will($this->returnValue($response)); - - $request->setResponseFactory($factory); + $response = null; + $request->on('response', $this->expectCallableOnce()); + $request->on('response', function ($value) use (&$response) { + $response = $value; + }); + $request->end(); $request->handleData("HTTP/1.0 200 OK\r\n"); @@ -687,28 +564,13 @@ public function multivalueHeader() $request->handleData("Cache-Control:public, must-revalidate, max-age=0\r\n"); $request->handleData("\r\nbody"); - $this->assertNotNull($errorCallback); - call_user_func($errorCallback, new \Exception('test')); - } - - /** @test */ - public function chunkedStreamDecoder() - { - $requestData = new RequestData('GET', 'http://www.example.com'); - $request = new Request($this->connector, $requestData); - - $this->successfulConnectionMock(); - - $request->end(); - - $this->stream->expects($this->once()) - ->method('emit') - ->with('data', array("1\r\nb\r")); - - $request->handleData("HTTP/1.0 200 OK\r\n"); - $request->handleData("Transfer-Encoding: chunked\r\n"); - $request->handleData("\r\n1\r\nb\r"); - $request->handleData("\n3\t\nody\r\n0\t\n\r\n"); - + /** @var \Psr\Http\Message\ResponseInterface $response */ + $this->assertInstanceOf('Psr\Http\Message\ResponseInterface', $response); + $this->assertEquals('1.0', $response->getProtocolVersion()); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals('OK', $response->getReasonPhrase()); + $this->assertEquals('text/plain', $response->getHeaderLine('Content-Type')); + $this->assertEquals('1; mode=block', $response->getHeaderLine('X-Xss-Protection')); + $this->assertEquals('public, must-revalidate, max-age=0', $response->getHeaderLine('Cache-Control')); } } diff --git a/tests/Client/ResponseTest.php b/tests/Client/ResponseTest.php deleted file mode 100644 index 0e757619..00000000 --- a/tests/Client/ResponseTest.php +++ /dev/null @@ -1,103 +0,0 @@ -stream = $this->getMockBuilder('React\Stream\DuplexStreamInterface') - ->getMock(); - } - - /** @test */ - public function responseShouldEmitEndEventOnEnd() - { - $this->stream - ->expects($this->at(0)) - ->method('on') - ->with('data', $this->anything()); - $this->stream - ->expects($this->at(1)) - ->method('on') - ->with('error', $this->anything()); - $this->stream - ->expects($this->at(2)) - ->method('on') - ->with('end', $this->anything()); - $this->stream - ->expects($this->at(3)) - ->method('on') - ->with('close', $this->anything()); - - $response = new Response($this->stream, 'HTTP', '1.0', '200', 'OK', array('Content-Type' => 'text/plain')); - - $handler = $this->createCallableMock(); - $handler->expects($this->once()) - ->method('__invoke') - ->with('some data'); - - $response->on('data', $handler); - - $handler = $this->createCallableMock(); - $handler->expects($this->once()) - ->method('__invoke'); - - $response->on('end', $handler); - - $handler = $this->createCallableMock(); - $handler->expects($this->once()) - ->method('__invoke'); - - $response->on('close', $handler); - - $this->stream - ->expects($this->at(0)) - ->method('close'); - - $response->handleData('some data'); - $response->handleEnd(); - - $this->assertSame( - array( - 'Content-Type' => 'text/plain' - ), - $response->getHeaders() - ); - } - - /** @test */ - public function closedResponseShouldNotBeResumedOrPaused() - { - $response = new Response($this->stream, 'http', '1.0', '200', 'ok', array('content-type' => 'text/plain')); - - $this->stream - ->expects($this->never()) - ->method('pause'); - $this->stream - ->expects($this->never()) - ->method('resume'); - - $response->handleEnd(); - - $response->resume(); - $response->pause(); - - $this->assertSame( - array( - 'content-type' => 'text/plain', - ), - $response->getHeaders() - ); - } -} - From f584fa42c463ec6b1dcf932de95fa94fc5c0a119 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 21 Aug 2020 15:19:06 +0200 Subject: [PATCH 344/456] Clean up tests --- tests/Client/RequestTest.php | 96 ++++++++-------------------------- tests/FunctionalServerTest.php | 12 ++--- 2 files changed, 29 insertions(+), 79 deletions(-) diff --git a/tests/Client/RequestTest.php b/tests/Client/RequestTest.php index 207aa15f..d7e43c3a 100644 --- a/tests/Client/RequestTest.php +++ b/tests/Client/RequestTest.php @@ -86,28 +86,30 @@ public function requestShouldBindToStreamEventsAndUseconnector() $request->handleData("\r\nbody"); } + /** + * @test + */ + public function requestShouldConnectViaTlsIfUrlUsesHttpsScheme() + { + $requestData = new RequestData('GET', 'https://www.example.com'); + $request = new Request($this->connector, $requestData); + + $this->connector->expects($this->once())->method('connect')->with('tls://www.example.com:443')->willReturn(new Promise(function () { })); + + $request->end(); + } + /** @test */ public function requestShouldEmitErrorIfConnectionFails() { $requestData = new RequestData('GET', 'http://www.example.com'); $request = new Request($this->connector, $requestData); - $this->rejectedConnectionMock(); - - $handler = $this->createCallableMock(); - $handler->expects($this->once()) - ->method('__invoke') - ->with( - $this->isInstanceOf('RuntimeException') - ); - - $request->on('error', $handler); + $this->connector->expects($this->once())->method('connect')->willReturn(\React\Promise\reject(new \RuntimeException())); - $handler = $this->createCallableMock(); - $handler->expects($this->once()) - ->method('__invoke'); + $request->on('error', $this->expectCallableOnceWith($this->isInstanceOf('RuntimeException'))); - $request->on('close', $handler); + $request->on('close', $this->expectCallableOnce()); $request->on('end', $this->expectCallableNever()); $request->end(); @@ -121,20 +123,9 @@ public function requestShouldEmitErrorIfConnectionClosesBeforeResponseIsParsed() $this->successfulConnectionMock(); - $handler = $this->createCallableMock(); - $handler->expects($this->once()) - ->method('__invoke') - ->with( - $this->isInstanceOf('RuntimeException') - ); - - $request->on('error', $handler); - - $handler = $this->createCallableMock(); - $handler->expects($this->once()) - ->method('__invoke'); + $request->on('error', $this->expectCallableOnceWith($this->isInstanceOf('RuntimeException'))); - $request->on('close', $handler); + $request->on('close', $this->expectCallableOnce()); $request->on('end', $this->expectCallableNever()); $request->end(); @@ -149,20 +140,9 @@ public function requestShouldEmitErrorIfConnectionEmitsError() $this->successfulConnectionMock(); - $handler = $this->createCallableMock(); - $handler->expects($this->once()) - ->method('__invoke') - ->with( - $this->isInstanceOf('Exception') - ); - - $request->on('error', $handler); + $request->on('error', $this->expectCallableOnceWith($this->isInstanceOf('Exception'))); - $handler = $this->createCallableMock(); - $handler->expects($this->once()) - ->method('__invoke'); - - $request->on('close', $handler); + $request->on('close', $this->expectCallableOnce()); $request->on('end', $this->expectCallableNever()); $request->end(); @@ -177,14 +157,7 @@ public function requestShouldEmitErrorIfRequestParserThrowsException() $this->successfulConnectionMock(); - $handler = $this->createCallableMock(); - $handler->expects($this->once()) - ->method('__invoke') - ->with( - $this->isInstanceOf('\InvalidArgumentException') - ); - - $request->on('error', $handler); + $request->on('error', $this->expectCallableOnceWith($this->isInstanceOf('InvalidArgumentException'))); $request->end(); $request->handleData("\r\n\r\n"); @@ -198,14 +171,7 @@ public function requestShouldEmitErrorIfUrlIsInvalid() $requestData = new RequestData('GET', 'ftp://www.example.com'); $request = new Request($this->connector, $requestData); - $handler = $this->createCallableMock(); - $handler->expects($this->once()) - ->method('__invoke') - ->with( - $this->isInstanceOf('\InvalidArgumentException') - ); - - $request->on('error', $handler); + $request->on('error', $this->expectCallableOnceWith($this->isInstanceOf('InvalidArgumentException'))); $this->connector->expects($this->never()) ->method('connect'); @@ -221,14 +187,7 @@ public function requestShouldEmitErrorIfUrlHasNoScheme() $requestData = new RequestData('GET', 'www.example.com'); $request = new Request($this->connector, $requestData); - $handler = $this->createCallableMock(); - $handler->expects($this->once()) - ->method('__invoke') - ->with( - $this->isInstanceOf('\InvalidArgumentException') - ); - - $request->on('error', $handler); + $request->on('error', $this->expectCallableOnceWith($this->isInstanceOf('InvalidArgumentException'))); $this->connector->expects($this->never()) ->method('connect'); @@ -533,15 +492,6 @@ private function successfulAsyncConnectionMock() }; } - private function rejectedConnectionMock() - { - $this->connector - ->expects($this->once()) - ->method('connect') - ->with('www.example.com:80') - ->will($this->returnValue(new RejectedPromise(new \RuntimeException()))); - } - /** @test */ public function multivalueHeader() { diff --git a/tests/FunctionalServerTest.php b/tests/FunctionalServerTest.php index 3355b732..bd127ab7 100644 --- a/tests/FunctionalServerTest.php +++ b/tests/FunctionalServerTest.php @@ -300,8 +300,8 @@ public function testPlainHttpOnStandardPortWithoutHostHeaderReturnsUriWithNoPort public function testSecureHttpsOnStandardPortReturnsUriWithNoPort() { - if (!function_exists('stream_socket_enable_crypto')) { - $this->markTestSkipped('Not supported on your platform (outdated HHVM?)'); + if (defined('HHVM_VERSION')) { + $this->markTestSkipped('Not supported on HHVM'); } $loop = Factory::create(); @@ -339,8 +339,8 @@ public function testSecureHttpsOnStandardPortReturnsUriWithNoPort() public function testSecureHttpsOnStandardPortWithoutHostHeaderUsesSocketUri() { - if (!function_exists('stream_socket_enable_crypto')) { - $this->markTestSkipped('Not supported on your platform (outdated HHVM?)'); + if (defined('HHVM_VERSION')) { + $this->markTestSkipped('Not supported on HHVM'); } $loop = Factory::create(); @@ -408,8 +408,8 @@ public function testPlainHttpOnHttpsStandardPortReturnsUriWithPort() public function testSecureHttpsOnHttpStandardPortReturnsUriWithPort() { - if (!function_exists('stream_socket_enable_crypto')) { - $this->markTestSkipped('Not supported on your platform (outdated HHVM?)'); + if (defined('HHVM_VERSION')) { + $this->markTestSkipped('Not supported on HHVM'); } $loop = Factory::create(); From db00f68c84a9553692ddbfa1814695c95ab8ee75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 29 Aug 2020 13:21:21 +0200 Subject: [PATCH 345/456] Update to reactphp/socket v1.6 (support PHP 8) --- composer.json | 4 ++-- examples/99-server-benchmark-download.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index e7739754..4f4d4aa8 100644 --- a/composer.json +++ b/composer.json @@ -32,8 +32,8 @@ "react/event-loop": "^1.0 || ^0.5", "react/promise": "^2.3 || ^1.2.1", "react/promise-stream": "^1.1", - "react/socket": "^1.1", - "react/stream": "^1.0 || ^0.7.5", + "react/socket": "^1.6", + "react/stream": "^1.1", "ringcentral/psr7": "^1.2" }, "require-dev": { diff --git a/examples/99-server-benchmark-download.php b/examples/99-server-benchmark-download.php index 536f4515..5fdd55c9 100644 --- a/examples/99-server-benchmark-download.php +++ b/examples/99-server-benchmark-download.php @@ -119,7 +119,7 @@ public function getSize() ); }); -$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop, array('tcp' => array('backlog' => 511))); +$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); $server->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; From cb90a6f3a7b533cecd954eeb59e97e5204e4924f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 30 Aug 2020 21:18:21 +0200 Subject: [PATCH 346/456] Fix checking chunked size exceeding integer bounds (PHP 8) --- src/Io/ChunkedDecoder.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Io/ChunkedDecoder.php b/src/Io/ChunkedDecoder.php index 45642a63..2f58f42b 100644 --- a/src/Io/ChunkedDecoder.php +++ b/src/Io/ChunkedDecoder.php @@ -123,7 +123,7 @@ public function handleData($data) } $this->chunkSize = @\hexdec($hexValue); - if (\dechex($this->chunkSize) !== $hexValue) { + if (!\is_int($this->chunkSize) || \dechex($this->chunkSize) !== $hexValue) { $this->handleError(new Exception($hexValue . ' is not a valid hexadecimal number')); return; } From 4b182a9f564d2a422fb08ef45d9564e37b9f40c7 Mon Sep 17 00:00:00 2001 From: Simon Frings Date: Mon, 31 Aug 2020 15:14:32 +0200 Subject: [PATCH 347/456] Update PHPUnit configuration schema for PHPUnit 9.3 --- .gitattributes | 1 + .travis.yml | 7 ++++--- composer.json | 2 +- phpunit.xml.dist | 16 ++++++++++------ phpunit.xml.legacy | 18 ++++++++++++++++++ 5 files changed, 34 insertions(+), 10 deletions(-) create mode 100644 phpunit.xml.legacy diff --git a/.gitattributes b/.gitattributes index f2f51ddf..64ab6e0f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -3,4 +3,5 @@ /.travis.yml export-ignore /examples export-ignore /phpunit.xml.dist export-ignore +/phpunit.xml.legacy export-ignore /tests export-ignore diff --git a/.travis.yml b/.travis.yml index d3de1e55..3ccbd4c1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,7 @@ language: php # lock distro so new future defaults will not break the build dist: trusty -matrix: +jobs: include: - php: 5.3 dist: precise @@ -23,9 +23,10 @@ matrix: - php: hhvm-3.18 install: - - composer install --no-interaction + - composer install - if [ "$DEPENDENCIES" = "lowest" ]; then composer update --prefer-lowest -n; fi script: - - ./vendor/bin/phpunit --coverage-text + - if [[ "$TRAVIS_PHP_VERSION" > "7.2" ]]; then vendor/bin/phpunit --coverage-text; fi + - if [[ "$TRAVIS_PHP_VERSION" < "7.3" ]]; then vendor/bin/phpunit --coverage-text -c phpunit.xml.legacy; fi - if [ "$DEPENDENCIES" = "lowest" ]; then php -n tests/benchmark-middleware-runner.php; fi diff --git a/composer.json b/composer.json index 4f4d4aa8..6924ebff 100644 --- a/composer.json +++ b/composer.json @@ -41,7 +41,7 @@ "clue/http-proxy-react": "^1.3", "clue/reactphp-ssh-proxy": "^1.0", "clue/socks-react": "^1.0", - "phpunit/phpunit": "^9.0 || ^5.7 || ^4.8.35" + "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35" }, "autoload": { "psr-4": { "React\\Http\\": "src" } diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 0e947b87..fa88e7e0 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,15 +1,19 @@ - + + ./tests/ - - - + + ./src/ - - + + diff --git a/phpunit.xml.legacy b/phpunit.xml.legacy new file mode 100644 index 00000000..fbb43e85 --- /dev/null +++ b/phpunit.xml.legacy @@ -0,0 +1,18 @@ + + + + + + + ./tests/ + + + + + ./src/ + + + From 7658f54072d1ca65020f70ccc1627f803063bd1b Mon Sep 17 00:00:00 2001 From: Simon Frings Date: Wed, 2 Sep 2020 09:30:27 +0200 Subject: [PATCH 348/456] Replace deprecated at() Mocks --- tests/Client/RequestTest.php | 119 +++++++++++------------------------ tests/Io/TransactionTest.php | 94 +++++++++++++++++---------- tests/TestCase.php | 26 ++++++-- 3 files changed, 120 insertions(+), 119 deletions(-) diff --git a/tests/Client/RequestTest.php b/tests/Client/RequestTest.php index d7e43c3a..fb2dc884 100644 --- a/tests/Client/RequestTest.php +++ b/tests/Client/RequestTest.php @@ -36,46 +36,21 @@ public function requestShouldBindToStreamEventsAndUseconnector() $this->successfulConnectionMock(); - $this->stream - ->expects($this->at(0)) - ->method('on') - ->with('drain', $this->identicalTo(array($request, 'handleDrain'))); - $this->stream - ->expects($this->at(1)) - ->method('on') - ->with('data', $this->identicalTo(array($request, 'handleData'))); - $this->stream - ->expects($this->at(2)) - ->method('on') - ->with('end', $this->identicalTo(array($request, 'handleEnd'))); - $this->stream - ->expects($this->at(3)) - ->method('on') - ->with('error', $this->identicalTo(array($request, 'handleError'))); - $this->stream - ->expects($this->at(4)) - ->method('on') - ->with('close', $this->identicalTo(array($request, 'handleClose'))); - $this->stream - ->expects($this->at(6)) - ->method('removeListener') - ->with('drain', $this->identicalTo(array($request, 'handleDrain'))); - $this->stream - ->expects($this->at(7)) - ->method('removeListener') - ->with('data', $this->identicalTo(array($request, 'handleData'))); - $this->stream - ->expects($this->at(8)) - ->method('removeListener') - ->with('end', $this->identicalTo(array($request, 'handleEnd'))); - $this->stream - ->expects($this->at(9)) - ->method('removeListener') - ->with('error', $this->identicalTo(array($request, 'handleError'))); - $this->stream - ->expects($this->at(10)) - ->method('removeListener') - ->with('close', $this->identicalTo(array($request, 'handleClose'))); + $this->stream->expects($this->exactly(6))->method('on')->withConsecutive( + array('drain', $this->identicalTo(array($request, 'handleDrain'))), + array('data', $this->identicalTo(array($request, 'handleData'))), + array('end', $this->identicalTo(array($request, 'handleEnd'))), + array('error', $this->identicalTo(array($request, 'handleError'))), + array('close', $this->identicalTo(array($request, 'handleClose'))) + ); + + $this->stream->expects($this->exactly(5))->method('removeListener')->withConsecutive( + array('drain', $this->identicalTo(array($request, 'handleDrain'))), + array('data', $this->identicalTo(array($request, 'handleData'))), + array('end', $this->identicalTo(array($request, 'handleEnd'))), + array('error', $this->identicalTo(array($request, 'handleError'))), + array('close', $this->identicalTo(array($request, 'handleClose'))) + ); $request->on('end', $this->expectCallableNever()); @@ -223,18 +198,11 @@ public function writeWithAPostRequestShouldSendToTheStream() $this->successfulConnectionMock(); - $this->stream - ->expects($this->at(5)) - ->method('write') - ->with($this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\nUser-Agent:.*\r\n\r\nsome$#")); - $this->stream - ->expects($this->at(6)) - ->method('write') - ->with($this->identicalTo("post")); - $this->stream - ->expects($this->at(7)) - ->method('write') - ->with($this->identicalTo("data")); + $this->stream->expects($this->exactly(3))->method('write')->withConsecutive( + array($this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\nUser-Agent:.*\r\n\r\nsome$#")), + array($this->identicalTo("post")), + array($this->identicalTo("data")) + ); $request->write("some"); $request->write("post"); @@ -253,15 +221,12 @@ public function writeWithAPostRequestShouldSendBodyAfterHeadersAndEmitDrainEvent $resolveConnection = $this->successfulAsyncConnectionMock(); - $this->stream - ->expects($this->at(5)) - ->method('write') - ->with($this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\nUser-Agent:.*\r\n\r\nsomepost$#")) - ->willReturn(true); - $this->stream - ->expects($this->at(6)) - ->method('write') - ->with($this->identicalTo("data")); + $this->stream->expects($this->exactly(2))->method('write')->withConsecutive( + array($this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\nUser-Agent:.*\r\n\r\nsomepost$#")), + array($this->identicalTo("data")) + )->willReturn( + true + ); $this->assertFalse($request->write("some")); $this->assertFalse($request->write("post")); @@ -292,15 +257,12 @@ public function writeWithAPostRequestShouldForwardDrainEventIfFirstChunkExceedsB $resolveConnection = $this->successfulAsyncConnectionMock(); - $this->stream - ->expects($this->at(0)) - ->method('write') - ->with($this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\nUser-Agent:.*\r\n\r\nsomepost$#")) - ->willReturn(false); - $this->stream - ->expects($this->at(1)) - ->method('write') - ->with($this->identicalTo("data")); + $this->stream->expects($this->exactly(2))->method('write')->withConsecutive( + array($this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\nUser-Agent:.*\r\n\r\nsomepost$#")), + array($this->identicalTo("data")) + )->willReturn( + false + ); $this->assertFalse($request->write("some")); $this->assertFalse($request->write("post")); @@ -327,18 +289,11 @@ public function pipeShouldPipeDataIntoTheRequestBody() $this->successfulConnectionMock(); - $this->stream - ->expects($this->at(5)) - ->method('write') - ->with($this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\nUser-Agent:.*\r\n\r\nsome$#")); - $this->stream - ->expects($this->at(6)) - ->method('write') - ->with($this->identicalTo("post")); - $this->stream - ->expects($this->at(7)) - ->method('write') - ->with($this->identicalTo("data")); + $this->stream->expects($this->exactly(3))->method('write')->withConsecutive( + array($this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\nUser-Agent:.*\r\n\r\nsome$#")), + array($this->identicalTo("post")), + array($this->identicalTo("data")) + ); $loop = $this ->getMockBuilder('React\EventLoop\LoopInterface') diff --git a/tests/Io/TransactionTest.php b/tests/Io/TransactionTest.php index 384a74a6..12e128cc 100644 --- a/tests/Io/TransactionTest.php +++ b/tests/Io/TransactionTest.php @@ -523,18 +523,21 @@ public function testFollowingRedirectWithSpecifiedHeaders() // mock sender to resolve promise with the given $redirectResponse in // response to the given $requestWithUserAgent $redirectResponse = new Response(301, array('Location' => 'http://redirect.com')); - $sender->expects($this->at(0))->method('send')->willReturn(Promise\resolve($redirectResponse)); // mock sender to resolve promise with the given $okResponse in // response to the given $requestWithUserAgent $okResponse = new Response(200); $that = $this; - $sender->expects($this->at(1)) - ->method('send') - ->with($this->callback(function (RequestInterface $request) use ($that) { + $sender->expects($this->exactly(2))->method('send')->withConsecutive( + array($this->anything()), + array($this->callback(function (RequestInterface $request) use ($that) { $that->assertEquals(array('Chrome'), $request->getHeader('User-Agent')); return true; - }))->willReturn(Promise\resolve($okResponse)); + })) + )->willReturnOnConsecutiveCalls( + Promise\resolve($redirectResponse), + Promise\resolve($okResponse) + ); $transaction = new Transaction($sender, $loop); $transaction->send($requestWithUserAgent); @@ -551,18 +554,21 @@ public function testRemovingAuthorizationHeaderWhenChangingHostnamesDuringRedire // mock sender to resolve promise with the given $redirectResponse in // response to the given $requestWithAuthorization $redirectResponse = new Response(301, array('Location' => 'http://redirect.com')); - $sender->expects($this->at(0))->method('send')->willReturn(Promise\resolve($redirectResponse)); // mock sender to resolve promise with the given $okResponse in // response to the given $requestWithAuthorization $okResponse = new Response(200); $that = $this; - $sender->expects($this->at(1)) - ->method('send') - ->with($this->callback(function (RequestInterface $request) use ($that) { + $sender->expects($this->exactly(2))->method('send')->withConsecutive( + array($this->anything()), + array($this->callback(function (RequestInterface $request) use ($that) { $that->assertFalse($request->hasHeader('Authorization')); return true; - }))->willReturn(Promise\resolve($okResponse)); + })) + )->willReturnOnConsecutiveCalls( + Promise\resolve($redirectResponse), + Promise\resolve($okResponse) + ); $transaction = new Transaction($sender, $loop); $transaction->send($requestWithAuthorization); @@ -579,18 +585,21 @@ public function testAuthorizationHeaderIsForwardedWhenRedirectingToSameDomain() // mock sender to resolve promise with the given $redirectResponse in // response to the given $requestWithAuthorization $redirectResponse = new Response(301, array('Location' => 'http://example.com/new')); - $sender->expects($this->at(0))->method('send')->willReturn(Promise\resolve($redirectResponse)); // mock sender to resolve promise with the given $okResponse in // response to the given $requestWithAuthorization $okResponse = new Response(200); $that = $this; - $sender->expects($this->at(1)) - ->method('send') - ->with($this->callback(function (RequestInterface $request) use ($that) { + $sender->expects($this->exactly(2))->method('send')->withConsecutive( + array($this->anything()), + array($this->callback(function (RequestInterface $request) use ($that) { $that->assertEquals(array('secret'), $request->getHeader('Authorization')); return true; - }))->willReturn(Promise\resolve($okResponse)); + })) + )->willReturnOnConsecutiveCalls( + Promise\resolve($redirectResponse), + Promise\resolve($okResponse) + ); $transaction = new Transaction($sender, $loop); $transaction->send($requestWithAuthorization); @@ -606,19 +615,22 @@ public function testAuthorizationHeaderIsForwardedWhenLocationContainsAuthentica // mock sender to resolve promise with the given $redirectResponse in // response to the given $requestWithAuthorization $redirectResponse = new Response(301, array('Location' => 'http://user:pass@example.com/new')); - $sender->expects($this->at(0))->method('send')->willReturn(Promise\resolve($redirectResponse)); // mock sender to resolve promise with the given $okResponse in // response to the given $requestWithAuthorization $okResponse = new Response(200); $that = $this; - $sender->expects($this->at(1)) - ->method('send') - ->with($this->callback(function (RequestInterface $request) use ($that) { + $sender->expects($this->exactly(2))->method('send')->withConsecutive( + array($this->anything()), + array($this->callback(function (RequestInterface $request) use ($that) { $that->assertEquals('user:pass', $request->getUri()->getUserInfo()); $that->assertFalse($request->hasHeader('Authorization')); return true; - }))->willReturn(Promise\resolve($okResponse)); + })) + )->willReturnOnConsecutiveCalls( + Promise\resolve($redirectResponse), + Promise\resolve($okResponse) + ); $transaction = new Transaction($sender, $loop); $transaction->send($request); @@ -639,19 +651,22 @@ public function testSomeRequestHeadersShouldBeRemovedWhenRedirecting() // mock sender to resolve promise with the given $redirectResponse in // response to the given $requestWithCustomHeaders $redirectResponse = new Response(301, array('Location' => 'http://example.com/new')); - $sender->expects($this->at(0))->method('send')->willReturn(Promise\resolve($redirectResponse)); // mock sender to resolve promise with the given $okResponse in // response to the given $requestWithCustomHeaders $okResponse = new Response(200); $that = $this; - $sender->expects($this->at(1)) - ->method('send') - ->with($this->callback(function (RequestInterface $request) use ($that) { + $sender->expects($this->exactly(2))->method('send')->withConsecutive( + array($this->anything()), + array($this->callback(function (RequestInterface $request) use ($that) { $that->assertFalse($request->hasHeader('Content-Type')); $that->assertFalse($request->hasHeader('Content-Length')); - return true; - }))->willReturn(Promise\resolve($okResponse)); + return true;; + })) + )->willReturnOnConsecutiveCalls( + Promise\resolve($redirectResponse), + Promise\resolve($okResponse) + ); $transaction = new Transaction($sender, $loop); $transaction->send($requestWithCustomHeaders); @@ -706,12 +721,17 @@ public function testCancelTransactionWillCancelRedirectedRequest() // mock sender to resolve promise with the given $redirectResponse in $redirectResponse = new Response(301, array('Location' => 'http://example.com/new')); - $sender->expects($this->at(0))->method('send')->willReturn(Promise\resolve($redirectResponse)); $pending = new \React\Promise\Promise(function () { }, $this->expectCallableOnce()); // mock sender to return pending promise which should be cancelled when cancelling result - $sender->expects($this->at(1))->method('send')->willReturn($pending); + $sender->expects($this->exactly(2))->method('send')->withConsecutive( + array($this->anything()), + array($this->anything()) + )->willReturnOnConsecutiveCalls( + Promise\resolve($redirectResponse), + $pending + ); $transaction = new Transaction($sender, $loop); $promise = $transaction->send($request); @@ -728,12 +748,17 @@ public function testCancelTransactionWillCancelRedirectedRequestAgain() // mock sender to resolve promise with the given $redirectResponse in $first = new Deferred(); - $sender->expects($this->at(0))->method('send')->willReturn($first->promise()); $second = new \React\Promise\Promise(function () { }, $this->expectCallableOnce()); // mock sender to return pending promise which should be cancelled when cancelling result - $sender->expects($this->at(1))->method('send')->willReturn($second); + $sender->expects($this->exactly(2))->method('send')->withConsecutive( + array($this->anything()), + array($this->anything()) + )->willReturnOnConsecutiveCalls( + $first->promise(), + $second + ); $transaction = new Transaction($sender, $loop); $promise = $transaction->send($request); @@ -794,12 +819,17 @@ public function testCancelTransactionShouldCancelSendingPromise() // mock sender to resolve promise with the given $redirectResponse in $redirectResponse = new Response(301, array('Location' => 'http://example.com/new')); - $sender->expects($this->at(0))->method('send')->willReturn(Promise\resolve($redirectResponse)); $pending = new \React\Promise\Promise(function () { }, $this->expectCallableOnce()); // mock sender to return pending promise which should be cancelled when cancelling result - $sender->expects($this->at(1))->method('send')->willReturn($pending); + $sender->expects($this->exactly(2))->method('send')->withConsecutive( + array($this->anything()), + array($this->anything()) + )->willReturnOnConsecutiveCalls( + Promise\resolve($redirectResponse), + $pending + ); $transaction = new Transaction($sender, $loop); $promise = $transaction->send($request); diff --git a/tests/TestCase.php b/tests/TestCase.php index 575ac274..a905c324 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -41,13 +41,29 @@ protected function expectCallableConsecutive($numberOfCalls, array $with) { $mock = $this->createCallableMock(); - for ($i = 0; $i < $numberOfCalls; $i++) { - $mock - ->expects($this->at($i)) - ->method('__invoke') - ->with($this->equalTo($with[$i])); + if($numberOfCalls == 2){ + $mock->expects($this->exactly($numberOfCalls))->method('__invoke')->withConsecutive( + array($this->equalTo($with[0])), + array($this->equalTo($with[1])) + ); } + if($numberOfCalls == 3){ + $mock->expects($this->exactly($numberOfCalls))->method('__invoke')->withConsecutive( + array($this->equalTo($with[0])), + array($this->equalTo($with[1])), + array($this->equalTo($with[2])) + ); + } + + if($numberOfCalls == 4){ + $mock->expects($this->exactly($numberOfCalls))->method('__invoke')->withConsecutive( + array($this->equalTo($with[0])), + array($this->equalTo($with[1])), + array($this->equalTo($with[2])), + array($this->equalTo($with[3])) + ); + } return $mock; } From c1cbd3bf006e82776eb7b538f2b590d27ea10005 Mon Sep 17 00:00:00 2001 From: Simon Frings Date: Thu, 3 Sep 2020 13:17:03 +0200 Subject: [PATCH 349/456] Refactor tests --- tests/Io/ChunkedDecoderTest.php | 69 ++++++++++++++++++++++++++++----- tests/TestCase.php | 30 -------------- 2 files changed, 59 insertions(+), 40 deletions(-) diff --git a/tests/Io/ChunkedDecoderTest.php b/tests/Io/ChunkedDecoderTest.php index b78a4cdd..822ceaa6 100644 --- a/tests/Io/ChunkedDecoderTest.php +++ b/tests/Io/ChunkedDecoderTest.php @@ -8,6 +8,9 @@ class ChunkedDecoderTest extends TestCase { + private $input; + private $parser; + /** * @before */ @@ -28,11 +31,17 @@ public function testSimpleChunk() public function testTwoChunks() { - $this->parser->on('data', $this->expectCallableConsecutive(2, array('hello', 'bla'))); + $buffer = array(); + $this->parser->on('data', function ($data) use (&$buffer) { + $buffer[] = $data; + }); + $this->parser->on('end', $this->expectCallableNever()); $this->parser->on('close', $this->expectCallableNever()); $this->input->emit('data', array("5\r\nhello\r\n3\r\nbla\r\n")); + + $this->assertEquals(array('hello', 'bla'), $buffer); } public function testEnd() @@ -46,12 +55,18 @@ public function testEnd() public function testParameterWithEnd() { - $this->parser->on('data', $this->expectCallableConsecutive(2, array('hello', 'bla'))); + $buffer = array(); + $this->parser->on('data', function ($data) use (&$buffer) { + $buffer[] = $data; + }); + $this->parser->on('end', $this->expectCallableOnce()); $this->parser->on('close', $this->expectCallableOnce()); $this->parser->on('error', $this->expectCallableNever()); $this->input->emit('data', array("5\r\nhello\r\n3\r\nbla\r\n0\r\n\r\n")); + + $this->assertEquals(array('hello', 'bla'), $buffer); } public function testInvalidChunk() @@ -118,7 +133,11 @@ public function testSplittedBoth() public function testCompletlySplitted() { - $this->parser->on('data', $this->expectCallableConsecutive(2, array('we', 'lt'))); + $buffer = array(); + $this->parser->on('data', function ($data) use (&$buffer) { + $buffer[] = $data; + }); + $this->parser->on('close', $this->expectCallableNever()); $this->parser->on('end', $this->expectCallableNever()); $this->parser->on('error', $this->expectCallableNever()); @@ -127,25 +146,36 @@ public function testCompletlySplitted() $this->input->emit('data', array("\r\n")); $this->input->emit('data', array("we")); $this->input->emit('data', array("lt\r\n")); + + $this->assertEquals(array('we', 'lt'), $buffer); } public function testMixed() { - $this->parser->on('data', $this->expectCallableConsecutive(3, array('we', 'lt', 'hello'))); + $buffer = array(); + $this->parser->on('data', function ($data) use (&$buffer) { + $buffer[] = $data; + }); + $this->parser->on('close', $this->expectCallableNever()); $this->parser->on('end', $this->expectCallableNever()); $this->parser->on('error', $this->expectCallableNever()); $this->input->emit('data', array("4")); $this->input->emit('data', array("\r\n")); - $this->input->emit('data', array("we")); - $this->input->emit('data', array("lt\r\n")); + $this->input->emit('data', array("welt\r\n")); $this->input->emit('data', array("5\r\nhello\r\n")); + + $this->assertEquals(array('welt', 'hello'), $buffer); } public function testBigger() { - $this->parser->on('data', $this->expectCallableConsecutive(2, array('abcdeabcdeabcdea', 'hello'))); + $buffer = array(); + $this->parser->on('data', function ($data) use (&$buffer) { + $buffer[] = $data; + }); + $this->parser->on('close', $this->expectCallableNever()); $this->parser->on('end', $this->expectCallableNever()); $this->parser->on('error', $this->expectCallableNever()); @@ -155,11 +185,17 @@ public function testBigger() $this->input->emit('data', array("\r\n")); $this->input->emit('data', array("abcdeabcdeabcdea\r\n")); $this->input->emit('data', array("5\r\nhello\r\n")); + + $this->assertEquals(array('abcdeabcdeabcdea', 'hello'), $buffer); } public function testOneUnfinished() { - $this->parser->on('data', $this->expectCallableConsecutive(2, array('bla', 'hello'))); + $buffer = array(); + $this->parser->on('data', function ($data) use (&$buffer) { + $buffer[] = $data; + }); + $this->parser->on('close', $this->expectCallableNever()); $this->parser->on('end', $this->expectCallableNever()); $this->parser->on('error', $this->expectCallableNever()); @@ -167,6 +203,8 @@ public function testOneUnfinished() $this->input->emit('data', array("3\r\n")); $this->input->emit('data', array("bla\r\n")); $this->input->emit('data', array("5\r\nhello")); + + $this->assertEquals(array('bla', 'hello'), $buffer); } public function testChunkIsBiggerThenExpected() @@ -326,7 +364,10 @@ public function testHexDecimalInBodyIsPotentialThreadSplitted() public function testEmitSingleCharacter() { - $this->parser->on('data', $this->expectCallableConsecutive(4, array('t', 'e', 's', 't'))); + $buffer = array(); + $this->parser->on('data', function ($data) use (&$buffer) { + $buffer[] = $data; + }); $this->parser->on('close', $this->expectCallableOnce()); $this->parser->on('end', $this->expectCallableOnce()); $this->parser->on('error', $this->expectCallableNever()); @@ -336,6 +377,8 @@ public function testEmitSingleCharacter() foreach ($array as $character) { $this->input->emit('data', array($character)); } + + $this->assertEquals(array('t', 'e', 's', 't'), $buffer); } public function testHandleError() @@ -402,13 +445,19 @@ public function testOutputStreamCanCloseInputStream() public function testLeadingZerosWillBeIgnored() { - $this->parser->on('data', $this->expectCallableConsecutive(2, array('hello', 'hello world'))); + $buffer = array(); + $this->parser->on('data', function ($data) use (&$buffer) { + $buffer[] = $data; + }); + $this->parser->on('error', $this->expectCallableNever()); $this->parser->on('end', $this->expectCallableNever()); $this->parser->on('close', $this->expectCallableNever()); $this->input->emit('data', array("00005\r\nhello\r\n")); $this->input->emit('data', array("0000b\r\nhello world\r\n")); + + $this->assertEquals(array('hello', 'hello world'), $buffer); } public function testLeadingZerosInEndChunkWillBeIgnored() diff --git a/tests/TestCase.php b/tests/TestCase.php index a905c324..1938ed89 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -37,36 +37,6 @@ protected function expectCallableNever() return $mock; } - protected function expectCallableConsecutive($numberOfCalls, array $with) - { - $mock = $this->createCallableMock(); - - if($numberOfCalls == 2){ - $mock->expects($this->exactly($numberOfCalls))->method('__invoke')->withConsecutive( - array($this->equalTo($with[0])), - array($this->equalTo($with[1])) - ); - } - - if($numberOfCalls == 3){ - $mock->expects($this->exactly($numberOfCalls))->method('__invoke')->withConsecutive( - array($this->equalTo($with[0])), - array($this->equalTo($with[1])), - array($this->equalTo($with[2])) - ); - } - - if($numberOfCalls == 4){ - $mock->expects($this->exactly($numberOfCalls))->method('__invoke')->withConsecutive( - array($this->equalTo($with[0])), - array($this->equalTo($with[1])), - array($this->equalTo($with[2])), - array($this->equalTo($with[3])) - ); - } - return $mock; - } - protected function createCallableMock() { if (method_exists('PHPUnit\Framework\MockObject\MockBuilder', 'addMethods')) { From 754b0c18545d258922ffa907f3b18598280fdecd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 11 Sep 2020 13:01:51 +0200 Subject: [PATCH 350/456] Prepare v1.1.0 release --- CHANGELOG.md | 19 ++++++++++++++++++- README.md | 2 +- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aadbfa7c..8718c68d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +## 1.1.0 (2020-09-11) + +* Feature: Support upcoming PHP 8 release, update to reactphp/socket v1.6 and adjust type checks for invalid chunk headers. + (#391 by @clue) + +* Feature: Consistently resolve base URL according to HTTP specs. + (#379 by @clue) + +* Feature / Fix: Expose `Transfer-Encoding: chunked` response header and fix chunked responses for `HEAD` requests. + (#381 by @clue) + +* Internal refactoring to remove unneeded `MessageFactory` and `Response` classes. + (#380 and #389 by @clue) + +* Minor documentation improvements and improve test suite, update to support PHPUnit 9.3. + (#385 by @clue and #393 by @SimonFrings) + ## 1.0.0 (2020-07-11) A major new feature release, see [**release announcement**](https://clue.engineering/2020/announcing-reactphp-http). @@ -84,7 +101,7 @@ minutes. See below for more details: This improves default concurrency to 1024 requests and caps the default request buffer at 64K. The previous defaults resulted in just 4 concurrent requests with a request buffer of 8M. - See [`Server`](../README.md#server) for details on how to override these defaults. + See [`Server`](README.md#server) for details on how to override these defaults. * Feature: Expose ReactPHP in `User-Agent` client-side request header and in `Server` server-side response header. (#374 by @clue) diff --git a/README.md b/README.md index e3532555..a1061b6a 100644 --- a/README.md +++ b/README.md @@ -2732,7 +2732,7 @@ This project follows [SemVer](https://semver.org/). This will install the latest supported version: ```bash -$ composer require react/http:^1.0 +$ composer require react/http:^1.1 ``` See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. From a19a34dad90a1fa728e8233a07675f0c6d780ab2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 13 Sep 2020 13:20:39 +0200 Subject: [PATCH 351/456] Add BufferedBody PSR-7 message body implementation --- src/Io/BufferedBody.php | 176 ++++++++++++++++++++ tests/Io/BufferedBodyTest.php | 300 ++++++++++++++++++++++++++++++++++ 2 files changed, 476 insertions(+) create mode 100644 src/Io/BufferedBody.php create mode 100644 tests/Io/BufferedBodyTest.php diff --git a/src/Io/BufferedBody.php b/src/Io/BufferedBody.php new file mode 100644 index 00000000..2f81bc56 --- /dev/null +++ b/src/Io/BufferedBody.php @@ -0,0 +1,176 @@ +buffer = $buffer; + } + + public function __toString() + { + if ($this->closed) { + return ''; + } + + $this->seek(0); + + return $this->getContents(); + } + + public function close() + { + $this->buffer = ''; + $this->position = 0; + $this->closed = true; + } + + public function detach() + { + $this->close(); + + return null; + } + + public function getSize() + { + return $this->closed ? null : \strlen($this->buffer); + } + + public function tell() + { + if ($this->closed) { + throw new \RuntimeException('Unable to tell position of closed stream'); + } + + return $this->position; + } + + public function eof() + { + return $this->position >= \strlen($this->buffer); + } + + public function isSeekable() + { + return !$this->closed; + } + + public function seek($offset, $whence = \SEEK_SET) + { + if ($this->closed) { + throw new \RuntimeException('Unable to seek on closed stream'); + } + + $old = $this->position; + + if ($whence === \SEEK_SET) { + $this->position = $offset; + } elseif ($whence === \SEEK_CUR) { + $this->position += $offset; + } elseif ($whence === \SEEK_END) { + $this->position = \strlen($this->buffer) + $offset; + } else { + throw new \InvalidArgumentException('Invalid seek mode given'); + } + + if (!\is_int($this->position) || $this->position < 0) { + $this->position = $old; + throw new \RuntimeException('Unable to seek to position'); + } + } + + public function rewind() + { + $this->seek(0); + } + + public function isWritable() + { + return !$this->closed; + } + + public function write($string) + { + if ($this->closed) { + throw new \RuntimeException('Unable to write to closed stream'); + } + + if ($string === '') { + return 0; + } + + if ($this->position > 0 && !isset($this->buffer[$this->position - 1])) { + $this->buffer = \str_pad($this->buffer, $this->position, "\0"); + } + + $len = \strlen($string); + $this->buffer = \substr($this->buffer, 0, $this->position) . $string . \substr($this->buffer, $this->position + $len); + $this->position += $len; + + return $len; + } + + public function isReadable() + { + return !$this->closed; + } + + public function read($length) + { + if ($this->closed) { + throw new \RuntimeException('Unable to read from closed stream'); + } + + if ($length < 1) { + throw new \InvalidArgumentException('Invalid read length given'); + } + + if ($this->position + $length > \strlen($this->buffer)) { + $length = \strlen($this->buffer) - $this->position; + } + + if (!isset($this->buffer[$this->position])) { + return ''; + } + + $pos = $this->position; + $this->position += $length; + + return \substr($this->buffer, $pos, $length); + } + + public function getContents() + { + if ($this->closed) { + throw new \RuntimeException('Unable to read from closed stream'); + } + + if (!isset($this->buffer[$this->position])) { + return ''; + } + + $pos = $this->position; + $this->position = \strlen($this->buffer); + + return \substr($this->buffer, $pos); + } + + public function getMetadata($key = null) + { + return $key === null ? array() : null; + } +} diff --git a/tests/Io/BufferedBodyTest.php b/tests/Io/BufferedBodyTest.php new file mode 100644 index 00000000..01154e71 --- /dev/null +++ b/tests/Io/BufferedBodyTest.php @@ -0,0 +1,300 @@ +assertTrue($stream->isReadable()); + $this->assertTrue($stream->isWritable()); + $this->assertTrue($stream->isSeekable()); + $this->assertSame(0, $stream->getSize()); + $this->assertSame('', $stream->getContents()); + $this->assertSame('', (string) $stream); + } + + public function testClose() + { + $stream = new BufferedBody('hello'); + $stream->close(); + + $this->assertFalse($stream->isReadable()); + $this->assertFalse($stream->isWritable()); + $this->assertFalse($stream->isSeekable()); + $this->assertTrue($stream->eof()); + $this->assertNull($stream->getSize()); + $this->assertSame('', (string) $stream); + } + + public function testDetachReturnsNullAndCloses() + { + $stream = new BufferedBody('hello'); + $this->assertNull($stream->detach()); + + $this->assertFalse($stream->isReadable()); + $this->assertFalse($stream->isWritable()); + $this->assertFalse($stream->isSeekable()); + $this->assertTrue($stream->eof()); + $this->assertNull($stream->getSize()); + $this->assertSame('', (string) $stream); + } + + public function testSeekAndTellPosition() + { + $stream = new BufferedBody('hello'); + + $this->assertSame(0, $stream->tell()); + $this->assertFalse($stream->eof()); + + $stream->seek(1); + $this->assertSame(1, $stream->tell()); + $this->assertFalse($stream->eof()); + + $stream->seek(2, SEEK_CUR); + $this->assertSame(3, $stream->tell()); + $this->assertFalse($stream->eof()); + + $stream->seek(-1, SEEK_END); + $this->assertSame(4, $stream->tell()); + $this->assertFalse($stream->eof()); + + $stream->seek(0, SEEK_END); + $this->assertSame(5, $stream->tell()); + $this->assertTrue($stream->eof()); + } + + public function testSeekAfterEndIsPermitted() + { + $stream = new BufferedBody('hello'); + + $stream->seek(1000); + $this->assertSame(1000, $stream->tell()); + $this->assertTrue($stream->eof()); + + $stream->seek(0, SEEK_END); + $this->assertSame(5, $stream->tell()); + $this->assertTrue($stream->eof()); + } + + public function testSeekBeforeStartThrows() + { + $stream = new BufferedBody('hello'); + + try { + $stream->seek(-10, SEEK_CUR); + } catch (\RuntimeException $e) { + $this->assertSame(0, $stream->tell()); + + $this->setExpectedException('RuntimeException'); + throw $e; + } + } + + public function testSeekWithInvalidModeThrows() + { + $stream = new BufferedBody('hello'); + + $this->setExpectedException('InvalidArgumentException'); + $stream->seek(1, 12345); + } + + public function testSeekAfterCloseThrows() + { + $stream = new BufferedBody('hello'); + $stream->close(); + + $this->setExpectedException('RuntimeException'); + $stream->seek(0); + } + + public function testTellAfterCloseThrows() + { + $stream = new BufferedBody('hello'); + $stream->close(); + + $this->setExpectedException('RuntimeException'); + $stream->tell(); + } + + public function testRewindSeeksToStartPosition() + { + $stream = new BufferedBody('hello'); + + $stream->seek(1); + $stream->rewind(); + $this->assertSame(0, $stream->tell()); + } + + public function testRewindAfterCloseThrows() + { + $stream = new BufferedBody('hello'); + $stream->close(); + + $this->setExpectedException('RuntimeException'); + $stream->rewind(); + } + + public function testGetContentsMultipleTimesReturnsBodyOnlyOnce() + { + $stream = new BufferedBody('hello'); + + $this->assertSame(5, $stream->getSize()); + $this->assertSame('hello', $stream->getContents()); + $this->assertSame('', $stream->getContents()); + } + + public function testReadReturnsChunkAndAdvancesPosition() + { + $stream = new BufferedBody('hello'); + + $this->assertSame('he', $stream->read(2)); + $this->assertSame(2, $stream->tell()); + + $this->assertSame('ll', $stream->read(2)); + $this->assertSame(4, $stream->tell()); + + $this->assertSame('o', $stream->read(2)); + $this->assertSame(5, $stream->tell()); + + $this->assertSame('', $stream->read(2)); + $this->assertSame(5, $stream->tell()); + } + + public function testReadAfterEndReturnsEmptyStringWithoutChangingPosition() + { + $stream = new BufferedBody('hello'); + + $stream->seek(1000); + + $this->assertSame('', $stream->read(2)); + $this->assertSame(1000, $stream->tell()); + } + + public function testReadZeroThrows() + { + $stream = new BufferedBody('hello'); + + $this->setExpectedException('InvalidArgumentException'); + $stream->read(0); + } + + public function testReadAfterCloseThrows() + { + $stream = new BufferedBody('hello'); + $stream->close(); + + $this->setExpectedException('RuntimeException'); + $stream->read(10); + } + + public function testGetContentsReturnsWholeBufferAndAdvancesPositionToEof() + { + $stream = new BufferedBody('hello'); + + $this->assertSame('hello', $stream->getContents()); + $this->assertSame(5, $stream->tell()); + $this->assertTrue($stream->eof()); + } + + public function testGetContentsAfterEndsReturnsEmptyStringWithoutChangingPosition() + { + $stream = new BufferedBody('hello'); + + $stream->seek(100); + + $this->assertSame('', $stream->getContents()); + $this->assertSame(100, $stream->tell()); + $this->assertTrue($stream->eof()); + } + + public function testGetContentsAfterCloseThrows() + { + $stream = new BufferedBody('hello'); + $stream->close(); + + $this->setExpectedException('RuntimeException'); + $stream->getContents(); + } + + public function testWriteAdvancesPosition() + { + $stream = new BufferedBody(''); + + $this->assertSame(2, $stream->write('he')); + $this->assertSame(2, $stream->tell()); + + $this->assertSame(2, $stream->write('ll')); + $this->assertSame(4, $stream->tell()); + + $this->assertSame(1, $stream->write('o')); + $this->assertSame(5, $stream->tell()); + + $this->assertSame(0, $stream->write('')); + $this->assertSame(5, $stream->tell()); + } + + public function testWriteInMiddleOfBufferOverwrites() + { + $stream = new BufferedBody('hello'); + + $stream->seek(1); + $this->assertSame(1, $stream->write('a')); + + $this->assertSame(2, $stream->tell()); + $this->assertsame(5, $stream->getSize()); + $this->assertSame('hallo', (string) $stream); + } + + public function testWriteOverEndOverwritesAndAppends() + { + $stream = new BufferedBody('hello'); + + $stream->seek(4); + $this->assertSame(2, $stream->write('au')); + + $this->assertSame(6, $stream->tell()); + $this->assertsame(6, $stream->getSize()); + $this->assertSame('hellau', (string) $stream); + } + + public function testWriteAfterEndAppendsAndFillsWithNullBytes() + { + $stream = new BufferedBody('hello'); + + $stream->seek(6); + $this->assertSame(6, $stream->write('binary')); + + $this->assertSame(12, $stream->tell()); + $this->assertsame(12, $stream->getSize()); + $this->assertSame('hello' . "\0" . 'binary', (string) $stream); + } + + public function testWriteAfterCloseThrows() + { + $stream = new BufferedBody('hello'); + $stream->close(); + + $this->setExpectedException('RuntimeException'); + $stream->write('foo'); + } + + public function testGetMetadataWithoutKeyReturnsEmptyArray() + { + $stream = new BufferedBody('hello'); + + $this->assertEquals(array(), $stream->getMetadata()); + } + + public function testGetMetadataWithKeyReturnsNull() + { + $stream = new BufferedBody('hello'); + + $this->assertNull($stream->getMetadata('key')); + } +} From 6fec25b0d317a496e7669708192a09dd6d403eac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 14 Sep 2020 17:00:54 +0200 Subject: [PATCH 352/456] Keep request body in memory also after consuming request body --- .../RequestBodyBufferMiddleware.php | 8 +-- tests/ServerTest.php | 71 +++++++++++++++++-- 2 files changed, 70 insertions(+), 9 deletions(-) diff --git a/src/Middleware/RequestBodyBufferMiddleware.php b/src/Middleware/RequestBodyBufferMiddleware.php index 0e6f5145..c13a5dec 100644 --- a/src/Middleware/RequestBodyBufferMiddleware.php +++ b/src/Middleware/RequestBodyBufferMiddleware.php @@ -4,10 +4,10 @@ use OverflowException; use Psr\Http\Message\ServerRequestInterface; +use React\Http\Io\BufferedBody; use React\Http\Io\IniUtil; use React\Promise\Stream; use React\Stream\ReadableStreamInterface; -use RingCentral\Psr7\BufferStream; final class RequestBodyBufferMiddleware { @@ -38,7 +38,7 @@ public function __invoke(ServerRequestInterface $request, $stack) if ($size === 0 || !$body instanceof ReadableStreamInterface) { // replace with empty body if body is streaming (or buffered size exceeds limit) if ($body instanceof ReadableStreamInterface || $size > $this->sizeLimit) { - $request = $request->withBody(new BufferStream(0)); + $request = $request->withBody(new BufferedBody('')); } return $stack($request); @@ -51,9 +51,7 @@ public function __invoke(ServerRequestInterface $request, $stack) } return Stream\buffer($body, $sizeLimit)->then(function ($buffer) use ($request, $stack) { - $stream = new BufferStream(\strlen($buffer)); - $stream->write($buffer); - $request = $request->withBody($stream); + $request = $request->withBody(new BufferedBody($buffer)); return $stack($request); }, function ($error) use ($stack, $request, $body) { diff --git a/tests/ServerTest.php b/tests/ServerTest.php index ce19dda9..84d93eb7 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -110,6 +110,35 @@ function (ServerRequestInterface $request) use (&$called) { $this->assertSame('beforeokafter', $called); } + public function testPostFormData() + { + $loop = Factory::create(); + $deferred = new Deferred(); + $server = new Server($loop, function (ServerRequestInterface $request) use ($deferred) { + $deferred->resolve($request); + }); + + $server->listen($this->socket); + $this->socket->emit('connection', array($this->connection)); + $this->connection->emit('data', array("POST / HTTP/1.0\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: 7\r\n\r\nfoo=bar")); + + $request = Block\await($deferred->promise(), $loop); + assert($request instanceof ServerRequestInterface); + + $form = $request->getParsedBody(); + + $this->assertTrue(isset($form['foo'])); + $this->assertEquals('bar', $form['foo']); + + $this->assertEquals(array(), $request->getUploadedFiles()); + + $body = $request->getBody(); + + $this->assertSame(7, $body->getSize()); + $this->assertSame(7, $body->tell()); + $this->assertSame('foo=bar', (string) $body); + } + public function testPostFileUpload() { $loop = Factory::create(); @@ -132,11 +161,14 @@ public function testPostFileUpload() } }); - $parsedRequest = Block\await($deferred->promise(), $loop); - $this->assertNotEmpty($parsedRequest->getUploadedFiles()); - $this->assertEmpty($parsedRequest->getParsedBody()); + $request = Block\await($deferred->promise(), $loop); + assert($request instanceof ServerRequestInterface); + + $this->assertEmpty($request->getParsedBody()); + + $this->assertNotEmpty($request->getUploadedFiles()); - $files = $parsedRequest->getUploadedFiles(); + $files = $request->getUploadedFiles(); $this->assertTrue(isset($files['file'])); $this->assertCount(1, $files); @@ -144,6 +176,37 @@ public function testPostFileUpload() $this->assertSame('hello.txt', $files['file']->getClientFilename()); $this->assertSame('text/plain', $files['file']->getClientMediaType()); $this->assertSame("hello\r\n", (string)$files['file']->getStream()); + + $body = $request->getBody(); + + $this->assertSame(220, $body->getSize()); + $this->assertSame(220, $body->tell()); + } + + public function testPostJsonWillNotBeParsedByDefault() + { + $loop = Factory::create(); + $deferred = new Deferred(); + $server = new Server($loop, function (ServerRequestInterface $request) use ($deferred) { + $deferred->resolve($request); + }); + + $server->listen($this->socket); + $this->socket->emit('connection', array($this->connection)); + $this->connection->emit('data', array("POST / HTTP/1.0\r\nContent-Type: application/json\r\nContent-Length: 6\r\n\r\n[true]")); + + $request = Block\await($deferred->promise(), $loop); + assert($request instanceof ServerRequestInterface); + + $this->assertNull($request->getParsedBody()); + + $this->assertSame(array(), $request->getUploadedFiles()); + + $body = $request->getBody(); + + $this->assertSame(6, $body->getSize()); + $this->assertSame(0, $body->tell()); + $this->assertSame('[true]', (string) $body); } public function testServerReceivesBufferedRequestByDefault() From badb0a87890e14b9cdfa3aec3ba1aafd900401ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 4 Dec 2020 13:57:33 +0100 Subject: [PATCH 353/456] Prepare v1.2.0 release --- CHANGELOG.md | 9 +++++++++ README.md | 6 +++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8718c68d..9734f11a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 1.2.0 (2020-12-04) + +* Feature: Keep request body in memory also after consuming request body. + (#395 by @clue) + + This means consumers can now always access the complete request body as + detailed in the documentation. This allows building custom parsers and more + advanced processing models without having to mess with the default parsers. + ## 1.1.0 (2020-09-11) * Feature: Support upcoming PHP 8 release, update to reactphp/socket v1.6 and adjust type checks for invalid chunk headers. diff --git a/README.md b/README.md index a1061b6a..9b50acd9 100644 --- a/README.md +++ b/README.md @@ -287,7 +287,7 @@ header. If the server requires authentication, if may return a `401` (Unauthoriz status code which will reject the request by default (see also the [`withRejectErrorResponse()` method](#withrejecterrorresponse) below). -In order to pass authentication details, you can simple pass the username and +In order to pass authentication details, you can simply pass the username and password as part of the request URL like this: ```php @@ -2185,7 +2185,7 @@ given timeout value applied. #### withFollowRedirects() -The `withTimeout(bool|int $followRedirects): Browser` method can be used to +The `withFollowRedirects(bool|int $followRedirects): Browser` method can be used to change how HTTP redirects will be followed. You can pass in the maximum number of redirects to follow: @@ -2732,7 +2732,7 @@ This project follows [SemVer](https://semver.org/). This will install the latest supported version: ```bash -$ composer require react/http:^1.1 +$ composer require react/http:^1.2 ``` See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. From 8dc1e4e4be1c679267d7916f6f13323bc074b4ac Mon Sep 17 00:00:00 2001 From: Paul Vogel Date: Wed, 6 Jan 2021 17:52:39 +0100 Subject: [PATCH 354/456] Fix some typos, a mixed up param name and add forgotten namespace backslash --- src/Browser.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Browser.php b/src/Browser.php index 816b86ba..188320e8 100644 --- a/src/Browser.php +++ b/src/Browser.php @@ -15,7 +15,7 @@ use InvalidArgumentException; /** - * @final This class is final and shouldn't be extended as it is likely to be marked final in a future relase. + * @final This class is final and shouldn't be extended as it is likely to be marked final in a future release. */ class Browser { @@ -302,7 +302,7 @@ public function delete($url, array $headers = array(), $contents = '') * @param string $url URL for the request * @param array $headers Additional request headers * @param string|ReadableStreamInterface $body HTTP request body contents - * @return PromiseInterface + * @return PromiseInterface */ public function request($method, $url, array $headers = array(), $body = '') { @@ -373,11 +373,11 @@ public function request($method, $url, array $headers = array(), $body = '') * @param string $url URL for the request * @param array $headers Additional request headers * @param string|ReadableStreamInterface $body HTTP request body contents - * @return PromiseInterface + * @return PromiseInterface */ - public function requestStreaming($method, $url, $headers = array(), $contents = '') + public function requestStreaming($method, $url, $headers = array(), $body = '') { - return $this->withOptions(array('streaming' => true))->requestMayBeStreaming($method, $url, $headers, $contents); + return $this->withOptions(array('streaming' => true))->requestMayBeStreaming($method, $url, $headers, $body); } /** @@ -720,7 +720,7 @@ private function withOptions(array $options) * @param string $url * @param array $headers * @param string|ReadableStreamInterface $body - * @return PromiseInterface + * @return PromiseInterface */ private function requestMayBeStreaming($method, $url, array $headers = array(), $body = '') { From 161550b2d2fd1a6da90c0b299f0f52b105f33d89 Mon Sep 17 00:00:00 2001 From: Paul Vogel Date: Wed, 6 Jan 2021 18:47:26 +0100 Subject: [PATCH 355/456] Update example 63 to support v1.0.0+ Add LoopInterface as first constructor argument to Server and change Server to accept variadic middleware handlers instead of array. --- examples/63-server-streaming-request.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/examples/63-server-streaming-request.php b/examples/63-server-streaming-request.php index 9c1a9758..45eb0dea 100644 --- a/examples/63-server-streaming-request.php +++ b/examples/63-server-streaming-request.php @@ -9,7 +9,8 @@ // Note how this example uses the advanced `StreamingRequestMiddleware` to allow streaming // the incoming HTTP request. This very simple example merely counts the size // of the streaming body, it does not otherwise buffer its contents in memory. -$server = new React\Http\Server(array( +$server = new React\Http\Server( + $loop, new React\Http\Middleware\StreamingRequestMiddleware(), function (Psr\Http\Message\ServerRequestInterface $request) { $body = $request->getBody(); @@ -44,7 +45,7 @@ function (Psr\Http\Message\ServerRequestInterface $request) { }); }); } -)); +); $server->on('error', 'printf'); From 8fba44a2908599899e0cc6c435dce6b5d3594abb Mon Sep 17 00:00:00 2001 From: Simon Frings Date: Wed, 3 Feb 2021 11:57:36 +0100 Subject: [PATCH 356/456] Use GitHub actions for continuous integration (CI) Bye bye Travis CI, you've served us well. --- .gitattributes | 2 +- .github/workflows/ci.yml | 45 ++++++++++++++++++++++++++++++++++++++++ .travis.yml | 32 ---------------------------- README.md | 2 +- 4 files changed, 47 insertions(+), 34 deletions(-) create mode 100644 .github/workflows/ci.yml delete mode 100644 .travis.yml diff --git a/.gitattributes b/.gitattributes index 64ab6e0f..f658344c 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,6 +1,6 @@ /.gitattributes export-ignore +/.github/ export-ignore /.gitignore export-ignore -/.travis.yml export-ignore /examples export-ignore /phpunit.xml.dist export-ignore /phpunit.xml.legacy export-ignore diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..d321f381 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,45 @@ +name: CI + +on: + push: + pull_request: + +jobs: + PHPUnit: + name: PHPUnit (PHP ${{ matrix.php }}) + runs-on: ubuntu-20.04 + strategy: + matrix: + php: + - 7.4 + - 7.3 + - 7.2 + - 7.1 + - 7.0 + - 5.6 + - 5.5 + - 5.4 + - 5.3 + steps: + - uses: actions/checkout@v2 + - uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: xdebug + - run: composer install + - run: vendor/bin/phpunit --coverage-text + if: ${{ matrix.php >= 7.3 }} + - run: vendor/bin/phpunit --coverage-text -c phpunit.xml.legacy + if: ${{ matrix.php < 7.3 }} + + PHPUnit-hhvm: + name: PHPUnit (HHVM) + runs-on: ubuntu-18.04 + continue-on-error: true + steps: + - uses: actions/checkout@v2 + - uses: azjezz/setup-hhvm@v1 + with: + version: lts-3.30 + - run: hhvm $(which composer) install + - run: hhvm vendor/bin/phpunit diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 3ccbd4c1..00000000 --- a/.travis.yml +++ /dev/null @@ -1,32 +0,0 @@ -language: php - -# lock distro so new future defaults will not break the build -dist: trusty - -jobs: - include: - - php: 5.3 - dist: precise - - php: 5.4 - - php: 5.5 - - php: 5.6 - - php: 7.0 - - php: 7.0 - env: - - DEPENDENCIES=lowest - - php: 7.1 - - php: 7.2 - - php: 7.3 - - php: 7.4 - - php: hhvm-3.18 - allow_failures: - - php: hhvm-3.18 - -install: - - composer install - - if [ "$DEPENDENCIES" = "lowest" ]; then composer update --prefer-lowest -n; fi - -script: - - if [[ "$TRAVIS_PHP_VERSION" > "7.2" ]]; then vendor/bin/phpunit --coverage-text; fi - - if [[ "$TRAVIS_PHP_VERSION" < "7.3" ]]; then vendor/bin/phpunit --coverage-text -c phpunit.xml.legacy; fi - - if [ "$DEPENDENCIES" = "lowest" ]; then php -n tests/benchmark-middleware-runner.php; fi diff --git a/README.md b/README.md index 9b50acd9..2f4867c9 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # HTTP -[![Build Status](https://travis-ci.org/reactphp/http.svg?branch=master)](https://travis-ci.org/reactphp/http) +[![CI status](https://github.com/reactphp/http/workflows/CI/badge.svg)](https://github.com/reactphp/http/actions) Event-driven, streaming HTTP client and server implementation for [ReactPHP](https://reactphp.org/). From 57f4e5f8c6519549fd53056646c0b355da91f351 Mon Sep 17 00:00:00 2001 From: Simon Frings Date: Wed, 3 Feb 2021 12:21:32 +0100 Subject: [PATCH 357/456] Support PHP 8 --- .github/workflows/ci.yml | 1 + README.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d321f381..cf214c83 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,6 +11,7 @@ jobs: strategy: matrix: php: + - 8.0 - 7.4 - 7.3 - 7.2 diff --git a/README.md b/README.md index 2f4867c9..19109f8c 100644 --- a/README.md +++ b/README.md @@ -2738,7 +2738,7 @@ $ composer require react/http:^1.2 See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. This project aims to run on any platform and thus does not require any PHP -extensions and supports running on legacy PHP 5.3 through current PHP 7+ and +extensions and supports running on legacy PHP 5.3 through current PHP 8+ and HHVM. It's *highly recommended to use PHP 7+* for this project. From aaca1b3e0eb991b19c410cf7468a87d5db6d46bb Mon Sep 17 00:00:00 2001 From: Simon Frings Date: Wed, 3 Feb 2021 12:36:34 +0100 Subject: [PATCH 358/456] Set Xdebug's stack limit to 256 for legacy PHP --- tests/Client/FunctionalIntegrationTest.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/Client/FunctionalIntegrationTest.php b/tests/Client/FunctionalIntegrationTest.php index 57861f2c..2db75b35 100644 --- a/tests/Client/FunctionalIntegrationTest.php +++ b/tests/Client/FunctionalIntegrationTest.php @@ -167,6 +167,9 @@ public function testPostJsonReturnsData() /** @group internet */ public function testCancelPendingConnectionEmitsClose() { + // max_nesting_level was set to 100 for PHP Versions < 5.4 which resulted in failing test for legacy PHP + ini_set('xdebug.max_nesting_level', 256); + $loop = Factory::create(); $client = new Client($loop); From f76e1473a2dc0810035caf216543f1024275e31c Mon Sep 17 00:00:00 2001 From: Fritz Gerneth Date: Mon, 28 Dec 2020 14:48:56 +0100 Subject: [PATCH 359/456] Fix broken anchor link in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 19109f8c..09a4ecd2 100644 --- a/README.md +++ b/README.md @@ -1846,7 +1846,7 @@ implementations and ongoing effort to standardize interfaces between these with and support this goal. As such, this project only bundles a few middleware implementations that are required to match PHP's request behavior (see -[middleware implementations](#react-http-middleware)) and otherwise actively +[middleware implementations](#reacthttpmiddleware)) and otherwise actively encourages third-party middleware implementations. While we would love to support PSR-15 directly in `react/http`, we understand From 6bcbe54d965025d4b456008604f9046b9cee420b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Wed, 24 Feb 2021 18:04:52 +0100 Subject: [PATCH 360/456] Require Host request header for HTTP/1.1 requests All HTTP/1.1 requests require a Host request header as per RFC 7230. The proxy CONNECT method is an exception to this rule because we do not want to break compatibility with common HTTP proxy clients that do not strictly follow the RFCs. This does not affect valid HTTP/1.1 requests and has no effect on HTTP/1.0 requests. Additionally, make sure we do not include a default Host request header in the parsed request object if the incoming request does not make use of the Host request header. --- src/Io/RequestHeaderParser.php | 14 ++- tests/Io/RequestHeaderParserTest.php | 14 +-- tests/Io/StreamingServerTest.php | 133 ++++++++++++++++----------- tests/ServerTest.php | 1 + 4 files changed, 96 insertions(+), 66 deletions(-) diff --git a/src/Io/RequestHeaderParser.php b/src/Io/RequestHeaderParser.php index 53f7ff09..2c9b121c 100644 --- a/src/Io/RequestHeaderParser.php +++ b/src/Io/RequestHeaderParser.php @@ -172,6 +172,7 @@ public function parseRequest($headers, $remoteSocketUri, $localSocketUri) } // default host if unset comes from local socket address or defaults to localhost + $hasHost = $host !== null; if ($host === null) { $host = isset($localParts['host'], $localParts['port']) ? $localParts['host'] . ':' . $localParts['port'] : '127.0.0.1'; } @@ -234,8 +235,8 @@ public function parseRequest($headers, $remoteSocketUri, $localSocketUri) $request = $request->withRequestTarget($start['target']); } - // Optional Host header value MUST be valid (host and optional port) - if ($request->hasHeader('Host')) { + 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) @@ -248,6 +249,12 @@ public function parseRequest($headers, $remoteSocketUri, $localSocketUri) 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 @@ -270,9 +277,6 @@ public function parseRequest($headers, $remoteSocketUri, $localSocketUri) } } - // always sanitize Host header because it contains critical routing information - $request = $request->withUri($request->getUri()->withUserInfo('u')->withUserInfo('')); - return $request; } } diff --git a/tests/Io/RequestHeaderParserTest.php b/tests/Io/RequestHeaderParserTest.php index b0a339ed..356443fb 100644 --- a/tests/Io/RequestHeaderParserTest.php +++ b/tests/Io/RequestHeaderParserTest.php @@ -257,7 +257,7 @@ public function testHeaderEventWithShouldApplyDefaultAddressFromLocalConnectionA $connection->emit('data', array("GET /foo HTTP/1.0\r\n\r\n")); $this->assertEquals('http://127.1.1.1:8000/foo', $request->getUri()); - $this->assertEquals('127.1.1.1:8000', $request->getHeaderLine('Host')); + $this->assertFalse($request->hasHeader('Host')); } public function testHeaderEventViaHttpsShouldApplyHttpsSchemeFromLocalTlsConnectionAddress() @@ -550,7 +550,7 @@ public function testInvalidContentLengthRequestHeaderWillEmitError() $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")); + $connection->emit('data', array("GET / HTTP/1.1\r\nHost: localhost\r\nContent-Length: foo\r\n\r\n")); $this->assertInstanceOf('InvalidArgumentException', $error); $this->assertSame(400, $error->getCode()); @@ -570,7 +570,7 @@ public function testInvalidRequestWithMultipleContentLengthRequestHeadersWillEmi $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")); + $connection->emit('data', array("GET / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 4\r\nContent-Length: 5\r\n\r\n")); $this->assertInstanceOf('InvalidArgumentException', $error); $this->assertSame(400, $error->getCode()); @@ -590,7 +590,7 @@ public function testInvalidTransferEncodingRequestHeaderWillEmitError() $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")); + $connection->emit('data', array("GET / HTTP/1.1\r\nHost: localhost\r\nTransfer-Encoding: foo\r\n\r\n")); $this->assertInstanceOf('InvalidArgumentException', $error); $this->assertSame(501, $error->getCode()); @@ -610,7 +610,7 @@ public function testInvalidRequestWithBothTransferEncodingAndContentLengthWillEm $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")); + $connection->emit('data', array("GET / HTTP/1.1\r\nHost: localhost\r\nTransfer-Encoding: chunked\r\nContent-Length: 0\r\n\r\n")); $this->assertInstanceOf('InvalidArgumentException', $error); $this->assertSame(400, $error->getCode()); @@ -762,7 +762,7 @@ public function testQueryParmetersWillBeSet() private function createGetRequest() { $data = "GET / HTTP/1.1\r\n"; - $data .= "Host: example.com:80\r\n"; + $data .= "Host: example.com\r\n"; $data .= "Connection: close\r\n"; $data .= "\r\n"; @@ -772,7 +772,7 @@ private function createGetRequest() private function createAdvancedPostRequest() { $data = "POST /foo?bar=baz HTTP/1.1\r\n"; - $data .= "Host: example.com:80\r\n"; + $data .= "Host: example.com\r\n"; $data .= "User-Agent: react/alpha\r\n"; $data .= "Connection: close\r\n"; $data .= "\r\n"; diff --git a/tests/Io/StreamingServerTest.php b/tests/Io/StreamingServerTest.php index d2401a06..c771330b 100644 --- a/tests/Io/StreamingServerTest.php +++ b/tests/Io/StreamingServerTest.php @@ -210,7 +210,7 @@ public function testRequestGetWithHostAndDefaultPortWillBeIgnored() $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); - $data = "GET / HTTP/1.1\r\nHost: example.com:80\r\n\r\n"; + $data = "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n"; $this->connection->emit('data', array($data)); $this->assertInstanceOf('RingCentral\Psr7\Request', $requestAssertion); @@ -222,6 +222,41 @@ public function testRequestGetWithHostAndDefaultPortWillBeIgnored() $this->assertSame('example.com', $requestAssertion->getHeaderLine('Host')); } + public function testRequestGetHttp10WithoutHostWillBeIgnored() + { + $requestAssertion = null; + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestAssertion) { + $requestAssertion = $request; + }); + + $server->listen($this->socket); + $this->socket->emit('connection', array($this->connection)); + + $data = "GET / HTTP/1.0\r\n\r\n"; + $this->connection->emit('data', array($data)); + + $this->assertInstanceOf('RingCentral\Psr7\Request', $requestAssertion); + $this->assertSame('GET', $requestAssertion->getMethod()); + $this->assertSame('/', $requestAssertion->getRequestTarget()); + $this->assertSame('/', $requestAssertion->getUri()->getPath()); + $this->assertSame('http://127.0.0.1/', (string)$requestAssertion->getUri()); + $this->assertNull($requestAssertion->getUri()->getPort()); + $this->assertEquals('1.0', $requestAssertion->getProtocolVersion()); + $this->assertSame('', $requestAssertion->getHeaderLine('Host')); + } + + public function testRequestGetHttp11WithoutHostWillReject() + { + $server = new StreamingServer(Factory::create(), 'var_dump'); + $server->on('error', $this->expectCallableOnce()); + + $server->listen($this->socket); + $this->socket->emit('connection', array($this->connection)); + + $data = "GET / HTTP/1.1\r\n\r\n"; + $this->connection->emit('data', array($data)); + } + public function testRequestOptionsAsterisk() { $requestAssertion = null; @@ -277,7 +312,7 @@ public function testRequestConnectAuthorityForm() $this->assertSame('example.com:443', $requestAssertion->getHeaderLine('Host')); } - public function testRequestConnectWithoutHostWillBeAdded() + public function testRequestConnectWithoutHostWillBePassesAsIs() { $requestAssertion = null; $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestAssertion) { @@ -296,10 +331,10 @@ public function testRequestConnectWithoutHostWillBeAdded() $this->assertSame('', $requestAssertion->getUri()->getPath()); $this->assertSame('http://example.com:443', (string)$requestAssertion->getUri()); $this->assertSame(443, $requestAssertion->getUri()->getPort()); - $this->assertSame('example.com:443', $requestAssertion->getHeaderLine('Host')); + $this->assertFalse($requestAssertion->hasHeader('Host')); } - public function testRequestConnectAuthorityFormWithDefaultPortWillBeIgnored() + public function testRequestConnectAuthorityFormWithDefaultPortWillBePassedAsIs() { $requestAssertion = null; $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestAssertion) { @@ -318,10 +353,10 @@ public function testRequestConnectAuthorityFormWithDefaultPortWillBeIgnored() $this->assertSame('', $requestAssertion->getUri()->getPath()); $this->assertSame('http://example.com', (string)$requestAssertion->getUri()); $this->assertNull($requestAssertion->getUri()->getPort()); - $this->assertSame('example.com', $requestAssertion->getHeaderLine('Host')); + $this->assertSame('example.com:80', $requestAssertion->getHeaderLine('Host')); } - public function testRequestConnectAuthorityFormNonMatchingHostWillBeOverwritten() + public function testRequestConnectAuthorityFormNonMatchingHostWillBePassedAsIs() { $requestAssertion = null; $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestAssertion) { @@ -340,7 +375,7 @@ public function testRequestConnectAuthorityFormNonMatchingHostWillBeOverwritten( $this->assertSame('', $requestAssertion->getUri()->getPath()); $this->assertSame('http://example.com', (string)$requestAssertion->getUri()); $this->assertNull($requestAssertion->getUri()->getPort()); - $this->assertSame('example.com', $requestAssertion->getHeaderLine('Host')); + $this->assertSame('other.example.org', $requestAssertion->getHeaderLine('Host')); } public function testRequestConnectOriginFormRequestTargetWillReject() @@ -415,7 +450,7 @@ public function testRequestAbsoluteEvent() $this->assertSame('example.com', $requestAssertion->getHeaderLine('Host')); } - public function testRequestAbsoluteAddsMissingHostEvent() + public function testRequestAbsoluteNonMatchingHostWillBePassedAsIs() { $requestAssertion = null; @@ -426,37 +461,27 @@ public function testRequestAbsoluteAddsMissingHostEvent() $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); - $data = "GET http://example.com:8080/test HTTP/1.0\r\n\r\n"; + $data = "GET http://example.com/test HTTP/1.1\r\nHost: other.example.org\r\n\r\n"; $this->connection->emit('data', array($data)); $this->assertInstanceOf('RingCentral\Psr7\Request', $requestAssertion); $this->assertSame('GET', $requestAssertion->getMethod()); - $this->assertSame('http://example.com:8080/test', $requestAssertion->getRequestTarget()); - $this->assertEquals('http://example.com:8080/test', $requestAssertion->getUri()); + $this->assertSame('http://example.com/test', $requestAssertion->getRequestTarget()); + $this->assertEquals('http://example.com/test', $requestAssertion->getUri()); $this->assertSame('/test', $requestAssertion->getUri()->getPath()); - $this->assertSame('example.com:8080', $requestAssertion->getHeaderLine('Host')); + $this->assertSame('other.example.org', $requestAssertion->getHeaderLine('Host')); } - public function testRequestAbsoluteNonMatchingHostWillBeOverwritten() + public function testRequestAbsoluteWithoutHostWillReject() { - $requestAssertion = null; - - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestAssertion) { - $requestAssertion = $request; - }); + $server = new StreamingServer(Factory::create(), $this->expectCallableNever()); + $server->on('error', $this->expectCallableOnce()); $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); - $data = "GET http://example.com/test HTTP/1.1\r\nHost: other.example.org\r\n\r\n"; + $data = "GET http://example.com:8080/test HTTP/1.1\r\n\r\n"; $this->connection->emit('data', array($data)); - - $this->assertInstanceOf('RingCentral\Psr7\Request', $requestAssertion); - $this->assertSame('GET', $requestAssertion->getMethod()); - $this->assertSame('http://example.com/test', $requestAssertion->getRequestTarget()); - $this->assertEquals('http://example.com/test', $requestAssertion->getUri()); - $this->assertSame('/test', $requestAssertion->getUri()->getPath()); - $this->assertSame('example.com', $requestAssertion->getHeaderLine('Host')); } public function testRequestOptionsAsteriskEvent() @@ -515,7 +540,7 @@ public function testRequestPauseWillBeForwardedToConnection() $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; - $data .= "Host: example.com:80\r\n"; + $data .= "Host: example.com\r\n"; $data .= "Connection: close\r\n"; $data .= "Content-Length: 5\r\n"; $data .= "\r\n"; @@ -535,7 +560,7 @@ public function testRequestResumeWillBeForwardedToConnection() $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; - $data .= "Host: example.com:80\r\n"; + $data .= "Host: example.com\r\n"; $data .= "Connection: close\r\n"; $data .= "Content-Length: 5\r\n"; $data .= "\r\n"; @@ -954,7 +979,7 @@ function ($data) use (&$buffer) { $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); - $data = "GET / HTTP/1.1\r\n\r\n"; + $data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"; $this->connection->emit('data', array($data)); $this->assertEquals("HTTP/1.1 200 OK\r\nUpgrade: demo\r\nContent-Length: 3\r\nConnection: close\r\n\r\nfoo", $buffer); @@ -989,7 +1014,7 @@ function ($data) use (&$buffer) { $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); - $data = "GET / HTTP/1.1\r\nUpgrade: demo\r\n\r\n"; + $data = "GET / HTTP/1.1\r\nHost: localhost\r\nUpgrade: demo\r\n\r\n"; $this->connection->emit('data', array($data)); $this->assertEquals("HTTP/1.1 200 OK\r\nContent-Length: 3\r\nConnection: close\r\n\r\nfoo", $buffer); @@ -1027,7 +1052,7 @@ function ($data) use (&$buffer) { $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); - $data = "GET / HTTP/1.1\r\nUpgrade: demo\r\n\r\n"; + $data = "GET / HTTP/1.1\r\nHost: localhost\r\nUpgrade: demo\r\n\r\n"; $this->connection->emit('data', array($data)); $this->assertEquals("HTTP/1.1 101 Switching Protocols\r\nUpgrade: demo\r\nConnection: upgrade\r\n\r\nfoo", $buffer); @@ -1065,7 +1090,7 @@ function ($data) use (&$buffer) { $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); - $data = "GET / HTTP/1.1\r\nUpgrade: demo\r\n\r\n"; + $data = "GET / HTTP/1.1\r\nHost: localhost\r\nUpgrade: demo\r\n\r\n"; $this->connection->emit('data', array($data)); $stream->write('hello'); @@ -1417,7 +1442,7 @@ public function testRequestContentLengthBodyDataWillEmitDataEventOnRequestStream $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; - $data .= "Host: example.com:80\r\n"; + $data .= "Host: example.com\r\n"; $data .= "Connection: close\r\n"; $data .= "Content-Length: 5\r\n"; $data .= "\r\n"; @@ -1446,7 +1471,7 @@ public function testRequestChunkedTransferEncodingRequestWillEmitDecodedDataEven $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; - $data .= "Host: example.com:80\r\n"; + $data .= "Host: example.com\r\n"; $data .= "Connection: close\r\n"; $data .= "Transfer-Encoding: chunked\r\n"; $data .= "\r\n"; @@ -1476,7 +1501,7 @@ public function testRequestChunkedTransferEncodingWithAdditionalDataWontBeEmitte $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; - $data .= "Host: example.com:80\r\n"; + $data .= "Host: example.com\r\n"; $data .= "Connection: close\r\n"; $data .= "Transfer-Encoding: chunked\r\n"; $data .= "\r\n"; @@ -1505,7 +1530,7 @@ public function testRequestChunkedTransferEncodingEmpty() $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; - $data .= "Host: example.com:80\r\n"; + $data .= "Host: example.com\r\n"; $data .= "Connection: close\r\n"; $data .= "Transfer-Encoding: chunked\r\n"; $data .= "\r\n"; @@ -1534,7 +1559,7 @@ public function testRequestChunkedTransferEncodingHeaderCanBeUpperCase() $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; - $data .= "Host: example.com:80\r\n"; + $data .= "Host: example.com\r\n"; $data .= "Connection: close\r\n"; $data .= "Transfer-Encoding: CHUNKED\r\n"; $data .= "\r\n"; @@ -1563,7 +1588,7 @@ public function testRequestChunkedTransferEncodingCanBeMixedUpperAndLowerCase() $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; - $data .= "Host: example.com:80\r\n"; + $data .= "Host: example.com\r\n"; $data .= "Connection: close\r\n"; $data .= "Transfer-Encoding: CHunKeD\r\n"; $data .= "\r\n"; @@ -1592,7 +1617,7 @@ public function testRequestContentLengthWillEmitDataEventAndEndEventAndAdditiona $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; - $data .= "Host: example.com:80\r\n"; + $data .= "Host: example.com\r\n"; $data .= "Connection: close\r\n"; $data .= "Content-Length: 5\r\n"; $data .= "\r\n"; @@ -1621,7 +1646,7 @@ public function testRequestContentLengthWillEmitDataEventAndEndEventAndAdditiona $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; - $data .= "Host: example.com:80\r\n"; + $data .= "Host: example.com\r\n"; $data .= "Connection: close\r\n"; $data .= "Content-Length: 5\r\n"; $data .= "\r\n"; @@ -1653,7 +1678,7 @@ public function testRequestZeroContentLengthWillEmitEndEvent() $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; - $data .= "Host: example.com:80\r\n"; + $data .= "Host: example.com\r\n"; $data .= "Connection: close\r\n"; $data .= "Content-Length: 0\r\n"; $data .= "\r\n"; @@ -1679,7 +1704,7 @@ public function testRequestZeroContentLengthWillEmitEndAndAdditionalDataWillBeIg $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; - $data .= "Host: example.com:80\r\n"; + $data .= "Host: example.com\r\n"; $data .= "Connection: close\r\n"; $data .= "Content-Length: 0\r\n"; $data .= "\r\n"; @@ -1706,7 +1731,7 @@ public function testRequestZeroContentLengthWillEmitEndAndAdditionalDataWillBeIg $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; - $data .= "Host: example.com:80\r\n"; + $data .= "Host: example.com\r\n"; $data .= "Connection: close\r\n"; $data .= "Content-Length: 0\r\n"; $data .= "\r\n"; @@ -1732,7 +1757,7 @@ public function testRequestInvalidChunkHeaderTooLongWillEmitErrorOnRequestStream $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; - $data .= "Host: example.com:80\r\n"; + $data .= "Host: example.com\r\n"; $data .= "Connection: close\r\n"; $data .= "Transfer-Encoding: chunked\r\n"; $data .= "\r\n"; @@ -1756,7 +1781,7 @@ public function testRequestInvalidChunkBodyTooLongWillEmitErrorOnRequestStream() $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; - $data .= "Host: example.com:80\r\n"; + $data .= "Host: example.com\r\n"; $data .= "Connection: close\r\n"; $data .= "Transfer-Encoding: chunked\r\n"; $data .= "\r\n"; @@ -1778,7 +1803,7 @@ public function testRequestUnexpectedEndOfRequestWithChunkedTransferConnectionWi $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; - $data .= "Host: example.com:80\r\n"; + $data .= "Host: example.com\r\n"; $data .= "Connection: close\r\n"; $data .= "Transfer-Encoding: chunked\r\n"; $data .= "\r\n"; @@ -1801,7 +1826,7 @@ public function testRequestInvalidChunkHeaderWillEmitErrorOnRequestStream() $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; - $data .= "Host: example.com:80\r\n"; + $data .= "Host: example.com\r\n"; $data .= "Connection: close\r\n"; $data .= "Transfer-Encoding: chunked\r\n"; $data .= "\r\n"; @@ -1823,7 +1848,7 @@ public function testRequestUnexpectedEndOfRequestWithContentLengthWillEmitErrorO $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; - $data .= "Host: example.com:80\r\n"; + $data .= "Host: example.com\r\n"; $data .= "Connection: close\r\n"; $data .= "Content-Length: 500\r\n"; $data .= "\r\n"; @@ -2220,7 +2245,7 @@ function ($data) use (&$buffer) { $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; - $data .= "Host: example.com:80\r\n"; + $data .= "Host: example.com\r\n"; $data .= "Connection: close\r\n"; $data .= "Expect: 100-continue\r\n"; $data .= "\r\n"; @@ -2743,7 +2768,7 @@ public function testRequestCookieWillBeAddedToServerRequest() $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; - $data .= "Host: example.com:80\r\n"; + $data .= "Host: example.com\r\n"; $data .= "Connection: close\r\n"; $data .= "Cookie: hello=world\r\n"; $data .= "\r\n"; @@ -2764,7 +2789,7 @@ public function testRequestInvalidMultipleCookiesWontBeAddedToServerRequest() $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; - $data .= "Host: example.com:80\r\n"; + $data .= "Host: example.com\r\n"; $data .= "Connection: close\r\n"; $data .= "Cookie: hello=world\r\n"; $data .= "Cookie: test=failed\r\n"; @@ -2785,7 +2810,7 @@ public function testRequestCookieWithSeparatorWillBeAddedToServerRequest() $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; - $data .= "Host: example.com:80\r\n"; + $data .= "Host: example.com\r\n"; $data .= "Connection: close\r\n"; $data .= "Cookie: hello=world; test=abc\r\n"; $data .= "\r\n"; @@ -2804,7 +2829,7 @@ public function testRequestCookieWithCommaValueWillBeAddedToServerRequest() { $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; - $data .= "Host: example.com:80\r\n"; + $data .= "Host: example.com\r\n"; $data .= "Connection: close\r\n"; $data .= "Cookie: test=abc,def; hello=world\r\n"; $data .= "\r\n"; @@ -2816,7 +2841,7 @@ public function testRequestCookieWithCommaValueWillBeAddedToServerRequest() { private function createGetRequest() { $data = "GET / HTTP/1.1\r\n"; - $data .= "Host: example.com:80\r\n"; + $data .= "Host: example.com\r\n"; $data .= "Connection: close\r\n"; $data .= "\r\n"; diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 84d93eb7..ff2cd9c1 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -269,6 +269,7 @@ private function createPostFileUploadRequest() $data = array(); $data[] = "POST / HTTP/1.1\r\n"; + $data[] = "Host: localhost\r\n"; $data[] = "Content-Type: multipart/form-data; boundary=" . $boundary . "\r\n"; $data[] = "Content-Length: 220\r\n"; $data[] = "\r\n"; From 7aa08f01583c765261d08d1191f0239324f18012 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 6 Apr 2018 17:33:44 +0200 Subject: [PATCH 361/456] Support persistent connections (Connection: keep-alive) --- README.md | 11 +- src/Io/StreamingServer.php | 41 +++++-- tests/FunctionalServerTest.php | 6 +- tests/Io/StreamingServerTest.php | 195 ++++++++++++++++++++++++++++++- 4 files changed, 236 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 09a4ecd2..4625a81e 100644 --- a/README.md +++ b/README.md @@ -1698,11 +1698,12 @@ so you don't have to. For instance, if the client sends the request using the HTTP/1.1 protocol version, the response message will also use the same protocol version, no matter what version is returned from the request handler function. -Note that persistent connections (`Connection: keep-alive`) are currently -not supported. -As such, HTTP/1.1 response messages will automatically include a -`Connection: close` header, irrespective of what header values are -passed explicitly. +The server supports persistent connections. An appropriate `Connection: keep-alive` +or `Connection: close` response header will be added automatically, respecting the +matching request header value and HTTP default header values. The server is +responsible for handling the `Connection` response header, so you SHOULD NOT pass +this response header yourself, unless you explicitly want to override the user's +choice with a `Connection: close` response header. ### Middleware diff --git a/src/Io/StreamingServer.php b/src/Io/StreamingServer.php index 0674d960..076a4ff0 100644 --- a/src/Io/StreamingServer.php +++ b/src/Io/StreamingServer.php @@ -210,7 +210,8 @@ public function writeError(ConnectionInterface $conn, $code, ServerRequestInterf $response = new Response( $code, array( - 'Content-Type' => 'text/plain' + 'Content-Type' => 'text/plain', + 'Connection' => 'close' // we do not want to keep the connection open after an error ), 'Error ' . $code ); @@ -273,17 +274,28 @@ public function handleResponse(ConnectionInterface $connection, ServerRequestInt $chunked = true; } else { // remove any Transfer-Encoding headers unless automatically enabled above + // we do not want to keep connection alive, so pretend we received "Connection: close" request header $response = $response->withoutHeader('Transfer-Encoding'); + $request = $request->withHeader('Connection', 'close'); } // assign "Connection" header automatically + $persist = false; if ($code === 101) { // 101 (Switching Protocols) response uses Connection: upgrade header + // This implies that this stream now uses another protocol and we + // may not persist this connection for additional requests. $response = $response->withHeader('Connection', 'upgrade'); - } elseif ($version === '1.1') { - // HTTP/1.1 assumes persistent connection support by default - // we do not support persistent connections, so let the client know + } elseif (\strtolower($request->getHeaderLine('Connection')) === 'close' || \strtolower($response->getHeaderLine('Connection')) === 'close') { + // obey explicit "Connection: close" request header or response header if present $response = $response->withHeader('Connection', 'close'); + } elseif ($version === '1.1') { + // HTTP/1.1 assumes persistent connection support by default, so we don't need to inform client + $persist = true; + } elseif (strtolower($request->getHeaderLine('Connection')) === 'keep-alive') { + // obey explicit "Connection: keep-alive" request header and inform client + $persist = true; + $response = $response->withHeader('Connection', 'keep-alive'); } else { // remove any Connection headers unless automatically enabled above $response = $response->withoutHeader('Connection'); @@ -328,9 +340,15 @@ public function handleResponse(ConnectionInterface $connection, ServerRequestInt $body = "0\r\n\r\n"; } - // end connection after writing response headers and body + // write response headers and body $connection->write($headers . "\r\n" . $body); - $connection->end(); + + // either wait for next request over persistent connection or end connection + if ($persist) { + $this->parser->handle($connection); + } else { + $connection->end(); + } return; } @@ -345,6 +363,15 @@ public function handleResponse(ConnectionInterface $connection, ServerRequestInt // in particular this may only fire on a later read/write attempt. $connection->on('close', array($body, 'close')); - $body->pipe($connection); + // write streaming body and then wait for next request over persistent connection + if ($persist) { + $body->pipe($connection, array('end' => false)); + $parser = $this->parser; + $body->on('end', function () use ($connection, $parser) { + $parser->handle($connection); + }); + } else { + $body->pipe($connection); + } } } diff --git a/tests/FunctionalServerTest.php b/tests/FunctionalServerTest.php index bd127ab7..41cf31db 100644 --- a/tests/FunctionalServerTest.php +++ b/tests/FunctionalServerTest.php @@ -662,7 +662,7 @@ public function testConnectWithThroughStreamReturnsDataAsGiven() $server->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { - $conn->write("CONNECT example.com:80 HTTP/1.1\r\nHost: example.com:80\r\n\r\n"); + $conn->write("CONNECT example.com:80 HTTP/1.1\r\nHost: example.com:80\r\nConnection: close\r\n\r\n"); $conn->once('data', function () use ($conn) { $conn->write('hello'); @@ -703,7 +703,7 @@ public function testConnectWithThroughStreamReturnedFromPromiseReturnsDataAsGive $server->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { - $conn->write("CONNECT example.com:80 HTTP/1.1\r\nHost: example.com:80\r\n\r\n"); + $conn->write("CONNECT example.com:80 HTTP/1.1\r\nHost: example.com:80\r\nConnection: close\r\n\r\n"); $conn->once('data', function () use ($conn) { $conn->write('hello'); @@ -737,7 +737,7 @@ public function testConnectWithClosedThroughStreamReturnsNoData() $server->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { - $conn->write("CONNECT example.com:80 HTTP/1.1\r\nHost: example.com:80\r\n\r\n"); + $conn->write("CONNECT example.com:80 HTTP/1.1\r\nHost: example.com:80\r\nConnection: close\r\n\r\n"); $conn->once('data', function () use ($conn) { $conn->write('hello'); diff --git a/tests/Io/StreamingServerTest.php b/tests/Io/StreamingServerTest.php index d2401a06..40187f06 100644 --- a/tests/Io/StreamingServerTest.php +++ b/tests/Io/StreamingServerTest.php @@ -6,6 +6,7 @@ use React\EventLoop\Factory; use React\Http\Io\StreamingServer; use React\Http\Message\Response; +use React\Http\Message\ServerRequest; use React\Promise\Promise; use React\Stream\ThroughStream; use React\Tests\Http\SocketServerStub; @@ -957,7 +958,7 @@ function ($data) use (&$buffer) { $data = "GET / HTTP/1.1\r\n\r\n"; $this->connection->emit('data', array($data)); - $this->assertEquals("HTTP/1.1 200 OK\r\nUpgrade: demo\r\nContent-Length: 3\r\nConnection: close\r\n\r\nfoo", $buffer); + $this->assertEquals("HTTP/1.1 200 OK\r\nUpgrade: demo\r\nContent-Length: 3\r\n\r\nfoo", $buffer); } public function testResponseUpgradeWishInRequestCanBeIgnoredByReturningNormalResponse() @@ -992,7 +993,7 @@ function ($data) use (&$buffer) { $data = "GET / HTTP/1.1\r\nUpgrade: demo\r\n\r\n"; $this->connection->emit('data', array($data)); - $this->assertEquals("HTTP/1.1 200 OK\r\nContent-Length: 3\r\nConnection: close\r\n\r\nfoo", $buffer); + $this->assertEquals("HTTP/1.1 200 OK\r\nContent-Length: 3\r\n\r\nfoo", $buffer); } public function testResponseUpgradeSwitchingProtocolIncludesConnectionUpgradeHeaderWithoutContentLength() @@ -2813,6 +2814,196 @@ public function testRequestCookieWithCommaValueWillBeAddedToServerRequest() { $this->assertEquals(array('test' => 'abc,def', 'hello' => 'world'), $requestValidation->getCookieParams()); } + public function testNewConnectionWillInvokeParserOnce() + { + $server = new StreamingServer(Factory::create(), $this->expectCallableNever()); + + $parser = $this->getMockBuilder('React\Http\Io\RequestHeaderParser')->getMock(); + $parser->expects($this->once())->method('handle'); + + $ref = new \ReflectionProperty($server, 'parser'); + $ref->setAccessible(true); + $ref->setValue($server, $parser); + + $server->listen($this->socket); + $this->socket->emit('connection', array($this->connection)); + } + + public function testNewConnectionWillInvokeParserOnceAndInvokeRequestHandlerWhenParserIsDoneForHttp10() + { + $request = new ServerRequest('GET', 'http://localhost/', array(), '', '1.0'); + + $server = new StreamingServer(Factory::create(), $this->expectCallableOnceWith($request)); + + $parser = $this->getMockBuilder('React\Http\Io\RequestHeaderParser')->getMock(); + $parser->expects($this->once())->method('handle'); + + $ref = new \ReflectionProperty($server, 'parser'); + $ref->setAccessible(true); + $ref->setValue($server, $parser); + + $server->listen($this->socket); + $this->socket->emit('connection', array($this->connection)); + + $this->connection->expects($this->once())->method('write'); + $this->connection->expects($this->once())->method('end'); + + // pretend parser just finished parsing + $server->handleRequest($this->connection, $request); + } + + public function testNewConnectionWillInvokeParserOnceAndInvokeRequestHandlerWhenParserIsDoneForHttp11ConnectionClose() + { + $request = new ServerRequest('GET', 'http://localhost/', array('Connection' => 'close')); + + $server = new StreamingServer(Factory::create(), $this->expectCallableOnceWith($request)); + + $parser = $this->getMockBuilder('React\Http\Io\RequestHeaderParser')->getMock(); + $parser->expects($this->once())->method('handle'); + + $ref = new \ReflectionProperty($server, 'parser'); + $ref->setAccessible(true); + $ref->setValue($server, $parser); + + $server->listen($this->socket); + $this->socket->emit('connection', array($this->connection)); + + $this->connection->expects($this->once())->method('write'); + $this->connection->expects($this->once())->method('end'); + + // pretend parser just finished parsing + $server->handleRequest($this->connection, $request); + } + + public function testNewConnectionWillInvokeParserOnceAndInvokeRequestHandlerWhenParserIsDoneAndRequestHandlerReturnsConnectionClose() + { + $request = new ServerRequest('GET', 'http://localhost/'); + + $server = new StreamingServer(Factory::create(), function () { + return new Response(200, array('Connection' => 'close')); + }); + + $parser = $this->getMockBuilder('React\Http\Io\RequestHeaderParser')->getMock(); + $parser->expects($this->once())->method('handle'); + + $ref = new \ReflectionProperty($server, 'parser'); + $ref->setAccessible(true); + $ref->setValue($server, $parser); + + $server->listen($this->socket); + $this->socket->emit('connection', array($this->connection)); + + $this->connection->expects($this->once())->method('write'); + $this->connection->expects($this->once())->method('end'); + + // pretend parser just finished parsing + $server->handleRequest($this->connection, $request); + } + + public function testNewConnectionWillInvokeParserTwiceAfterInvokingRequestHandlerWhenConnectionCanBeKeptAliveForHttp11Default() + { + $request = new ServerRequest('GET', 'http://localhost/'); + + $server = new StreamingServer(Factory::create(), function () { + return new Response(); + }); + + $parser = $this->getMockBuilder('React\Http\Io\RequestHeaderParser')->getMock(); + $parser->expects($this->exactly(2))->method('handle'); + + $ref = new \ReflectionProperty($server, 'parser'); + $ref->setAccessible(true); + $ref->setValue($server, $parser); + + $server->listen($this->socket); + $this->socket->emit('connection', array($this->connection)); + + $this->connection->expects($this->once())->method('write'); + $this->connection->expects($this->never())->method('end'); + + // pretend parser just finished parsing + $server->handleRequest($this->connection, $request); + } + + public function testNewConnectionWillInvokeParserTwiceAfterInvokingRequestHandlerWhenConnectionCanBeKeptAliveForHttp10ConnectionKeepAlive() + { + $request = new ServerRequest('GET', 'http://localhost/', array('Connection' => 'keep-alive'), '', '1.0'); + + $server = new StreamingServer(Factory::create(), function () { + return new Response(); + }); + + $parser = $this->getMockBuilder('React\Http\Io\RequestHeaderParser')->getMock(); + $parser->expects($this->exactly(2))->method('handle'); + + $ref = new \ReflectionProperty($server, 'parser'); + $ref->setAccessible(true); + $ref->setValue($server, $parser); + + $server->listen($this->socket); + $this->socket->emit('connection', array($this->connection)); + + $this->connection->expects($this->once())->method('write'); + $this->connection->expects($this->never())->method('end'); + + // pretend parser just finished parsing + $server->handleRequest($this->connection, $request); + } + + public function testNewConnectionWillInvokeParserOnceAfterInvokingRequestHandlerWhenStreamingResponseBodyKeepsStreaming() + { + $request = new ServerRequest('GET', 'http://localhost/'); + + $body = new ThroughStream(); + $server = new StreamingServer(Factory::create(), function () use ($body) { + return new Response(200, array(), $body); + }); + + $parser = $this->getMockBuilder('React\Http\Io\RequestHeaderParser')->getMock(); + $parser->expects($this->once())->method('handle'); + + $ref = new \ReflectionProperty($server, 'parser'); + $ref->setAccessible(true); + $ref->setValue($server, $parser); + + $server->listen($this->socket); + $this->socket->emit('connection', array($this->connection)); + + $this->connection->expects($this->once())->method('write'); + $this->connection->expects($this->never())->method('end'); + + // pretend parser just finished parsing + $server->handleRequest($this->connection, $request); + } + + public function testNewConnectionWillInvokeParserTwiceAfterInvokingRequestHandlerWhenStreamingResponseBodyEnds() + { + $request = new ServerRequest('GET', 'http://localhost/'); + + $body = new ThroughStream(); + $server = new StreamingServer(Factory::create(), function () use ($body) { + return new Response(200, array(), $body); + }); + + $parser = $this->getMockBuilder('React\Http\Io\RequestHeaderParser')->getMock(); + $parser->expects($this->exactly(2))->method('handle'); + + $ref = new \ReflectionProperty($server, 'parser'); + $ref->setAccessible(true); + $ref->setValue($server, $parser); + + $server->listen($this->socket); + $this->socket->emit('connection', array($this->connection)); + + $this->connection->expects($this->exactly(2))->method('write'); + $this->connection->expects($this->never())->method('end'); + + // pretend parser just finished parsing + $server->handleRequest($this->connection, $request); + + $body->end(); + } + private function createGetRequest() { $data = "GET / HTTP/1.1\r\n"; From 84fbe7882e0b948ae03abd67a57c5e88f2eac055 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 23 Mar 2021 19:54:15 +0100 Subject: [PATCH 362/456] Improve benchmarking instructions and dangling memory references --- examples/99-server-benchmark-download.php | 13 ++++++++++--- src/Io/RequestHeaderParser.php | 4 ---- src/Io/StreamingServer.php | 3 ++- tests/Io/StreamingServerTest.php | 2 ++ 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/examples/99-server-benchmark-download.php b/examples/99-server-benchmark-download.php index 5fdd55c9..1a49df72 100644 --- a/examples/99-server-benchmark-download.php +++ b/examples/99-server-benchmark-download.php @@ -1,11 +1,18 @@ /dev/null // $ wget http://localhost:8080/10g.bin -O /dev/null -// $ ab -n10 -c10 http://localhost:8080/1g.bin -// $ docker run -it --rm --net=host jordi/ab -n100000 -c10 http://localhost:8080/ -// $ docker run -it --rm --net=host jordi/ab -n10 -c10 http://localhost:8080/1g.bin +// $ ab -n10 -c10 -k http://localhost:8080/1g.bin +// $ docker run -it --rm --net=host jordi/ab -n100000 -c10 -k http://localhost:8080/ +// $ docker run -it --rm --net=host jordi/ab -n10 -c10 -k http://localhost:8080/1g.bin +// $ docker run -it --rm --net=host skandyla/wrk -t8 -c10 -d20 http://localhost:8080/ use Evenement\EventEmitter; use Psr\Http\Message\ServerRequestInterface; diff --git a/src/Io/RequestHeaderParser.php b/src/Io/RequestHeaderParser.php index 53f7ff09..5125c77f 100644 --- a/src/Io/RequestHeaderParser.php +++ b/src/Io/RequestHeaderParser.php @@ -106,10 +106,6 @@ public function handle(ConnectionInterface $conn) $stream->close(); } }); - - $conn->on('close', function () use (&$buffer, &$fn) { - $fn = $buffer = null; - }); } /** diff --git a/src/Io/StreamingServer.php b/src/Io/StreamingServer.php index 076a4ff0..e20ddf48 100644 --- a/src/Io/StreamingServer.php +++ b/src/Io/StreamingServer.php @@ -367,7 +367,8 @@ public function handleResponse(ConnectionInterface $connection, ServerRequestInt if ($persist) { $body->pipe($connection, array('end' => false)); $parser = $this->parser; - $body->on('end', function () use ($connection, $parser) { + $body->on('end', function () use ($connection, $parser, $body) { + $connection->removeListener('close', array($body, 'close')); $parser->handle($connection); }); } else { diff --git a/tests/Io/StreamingServerTest.php b/tests/Io/StreamingServerTest.php index 40187f06..0dde7a0c 100644 --- a/tests/Io/StreamingServerTest.php +++ b/tests/Io/StreamingServerTest.php @@ -3001,7 +3001,9 @@ public function testNewConnectionWillInvokeParserTwiceAfterInvokingRequestHandle // pretend parser just finished parsing $server->handleRequest($this->connection, $request); + $this->assertCount(2, $this->connection->listeners('close')); $body->end(); + $this->assertCount(1, $this->connection->listeners('close')); } private function createGetRequest() From bc537273d11ee769c723a830e63aa33c0c35a530 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 11 Apr 2021 20:07:28 +0200 Subject: [PATCH 363/456] Prepare v1.3.0 release --- CHANGELOG.md | 19 +++++++++++++++++++ README.md | 2 +- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9734f11a..4ae6bc15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,24 @@ # Changelog +## 1.3.0 (2021-04-11) + +* Feature: Support persistent connections (`Connection: keep-alive`). + (#405 by @clue) + + This shows a noticeable performance improvement especially when benchmarking + using persistent connections (which is the default pretty much everywhere). + Together with other changes in this release, this improves benchmarking + performance by around 100%. + +* Feature: Require `Host` request header for HTTP/1.1 requests. + (#404 by @clue) + +* Minor documentation improvements. + (#398 by @fritz-gerneth and #399 and #400 by @pavog) + +* Improve test suite, use GitHub actions for continuous integration (CI). + (#402 by @SimonFrings) + ## 1.2.0 (2020-12-04) * Feature: Keep request body in memory also after consuming request body. diff --git a/README.md b/README.md index 4625a81e..768b3f8c 100644 --- a/README.md +++ b/README.md @@ -2733,7 +2733,7 @@ This project follows [SemVer](https://semver.org/). This will install the latest supported version: ```bash -$ composer require react/http:^1.2 +$ composer require react/http:^1.3 ``` See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. From 5417e836ec34ffb7601002b8dcad8234c73b0d72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 8 Jul 2021 16:41:37 +0200 Subject: [PATCH 364/456] Simplify usage by supporting new default loop --- .github/workflows/ci.yml | 3 + README.md | 151 ++++++++---------- composer.json | 23 ++- examples/01-client-get-request.php | 5 +- examples/02-client-concurrent-requests.php | 5 +- examples/03-client-request-any.php | 5 +- examples/04-client-post-json.php | 5 +- examples/05-client-put-xml.php | 5 +- examples/11-client-http-connect-proxy.php | 13 +- examples/12-client-socks-proxy.php | 13 +- examples/13-client-ssh-proxy.php | 14 +- examples/14-client-unix-domain-sockets.php | 11 +- .../21-client-request-streaming-to-stdout.php | 9 +- .../22-client-stream-upload-from-stdin.php | 9 +- examples/51-server-hello-world.php | 9 +- examples/52-server-count-visitors.php | 9 +- examples/53-server-whatsmyip.php | 9 +- examples/54-server-query-parameter.php | 9 +- examples/55-server-cookie-handling.php | 9 +- examples/56-server-sleep.php | 14 +- examples/57-server-error-handling.php | 9 +- examples/58-server-stream-response.php | 18 +-- examples/59-server-json-api.php | 9 +- examples/61-server-hello-world-https.php | 11 +- examples/62-server-form-upload.php | 8 +- examples/63-server-streaming-request.php | 9 +- examples/71-server-http-proxy.php | 8 +- examples/72-server-http-connect-proxy.php | 10 +- examples/81-server-upgrade-echo.php | 12 +- examples/82-server-upgrade-chat.php | 12 +- examples/91-client-benchmark-download.php | 16 +- examples/92-client-benchmark-upload.php | 20 ++- examples/99-server-benchmark-download.php | 11 +- src/Browser.php | 33 ++-- .../LimitConcurrentRequestsMiddleware.php | 3 - src/Server.php | 34 ++-- tests/BrowserTest.php | 15 ++ tests/ServerTest.php | 15 ++ 38 files changed, 253 insertions(+), 330 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cf214c83..a08971b8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,6 +27,8 @@ jobs: with: php-version: ${{ matrix.php }} coverage: xdebug + env: + COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: composer install - run: vendor/bin/phpunit --coverage-text if: ${{ matrix.php >= 7.3 }} @@ -37,6 +39,7 @@ jobs: name: PHPUnit (HHVM) runs-on: ubuntu-18.04 continue-on-error: true + if: false # temporarily skipped until https://github.com/azjezz/setup-hhvm/issues/3 is addressed steps: - uses: actions/checkout@v2 - uses: azjezz/setup-hhvm@v1 diff --git a/README.md b/README.md index 768b3f8c..126fdcaf 100644 --- a/README.md +++ b/README.md @@ -87,22 +87,17 @@ Once [installed](#install), you can use the following code to access a HTTP webserver and send some simple HTTP GET requests: ```php -$loop = React\EventLoop\Factory::create(); -$client = new React\Http\Browser($loop); +$client = new React\Http\Browser(); $client->get('http://www.google.com/')->then(function (Psr\Http\Message\ResponseInterface $response) { var_dump($response->getHeaders(), (string)$response->getBody()); }); - -$loop->run(); ``` This is an HTTP server which responds with `Hello World!` to every request. ```php -$loop = React\EventLoop\Factory::create(); - -$server = new React\Http\Server($loop, function (Psr\Http\Message\ServerRequestInterface $request) { +$server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterface $request) { return new React\Http\Message\Response( 200, array( @@ -112,10 +107,8 @@ $server = new React\Http\Server($loop, function (Psr\Http\Message\ServerRequestI ); }); -$socket = new React\Socket\Server(8080, $loop); +$socket = new React\Socket\Server(8080); $server->listen($socket); - -$loop->run(); ``` See also the [examples](examples/). @@ -208,7 +201,7 @@ clean up any underlying resources. ```php $promise = $browser->get($url); -$loop->addTimer(2.0, function () use ($promise) { +Loop::addTimer(2.0, function () use ($promise) { $promise->cancel(); }); ``` @@ -266,9 +259,9 @@ like this: ```php $browser = new React\Http\Browser( - $loop, + null, new React\Socket\Connector( - $loop, + null, array( 'timeout' => 5 ) @@ -373,13 +366,12 @@ The resulting blocking code could look something like this: ```php use Clue\React\Block; -$loop = React\EventLoop\Factory::create(); -$browser = new React\Http\Browser($loop); +$browser = new React\Http\Browser(); $promise = $browser->get('http://example.com/'); try { - $response = Block\await($promise, $loop); + $response = Block\await($promise, Loop::get()); // response successfully received } catch (Exception $e) { // an error occured while performing the request @@ -394,7 +386,7 @@ $promises = array( $browser->get('http://www.example.org/'), ); -$responses = Block\awaitAll($promises, $loop); +$responses = Block\awaitAll($promises, Loop::get()); ``` Please refer to [clue/reactphp-block](https://github.com/clue/reactphp-block#readme) for more details. @@ -584,7 +576,7 @@ matching `Content-Length` request header like so: ```php $body = new React\Stream\ThroughStream(); -$loop->addTimer(1.0, function () use ($body) { +Loop::addTimer(1.0, function () use ($body) { $body->end("hello world"); }); @@ -610,15 +602,15 @@ protocol, such as plain HTTP and TLS-encrypted HTTPS. ```php $proxy = new Clue\React\HttpProxy\ProxyConnector( 'http://127.0.0.1:8080', - new React\Socket\Connector($loop) + new React\Socket\Connector() ); -$connector = new React\Socket\Connector($loop, array( +$connector = new React\Socket\Connector(null, array( 'tcp' => $proxy, 'dns' => false )); -$browser = new React\Http\Browser($loop, $connector); +$browser = new React\Http\Browser(null, $connector); ``` See also the [HTTP CONNECT proxy example](examples/11-client-http-connect-proxy.php). @@ -637,15 +629,15 @@ only, this can technically be used to tunnel any TCP/IP-based protocol. ```php $proxy = new Clue\React\Socks\Client( 'socks://127.0.0.1:1080', - new React\Socket\Connector($loop) + new React\Socket\Connector() ); -$connector = new React\Socket\Connector($loop, array( +$connector = new React\Socket\Connector(null, array( 'tcp' => $proxy, 'dns' => false )); -$browser = new React\Http\Browser($loop, $connector); +$browser = new React\Http\Browser(null, $connector); ``` See also the [SOCKS proxy example](examples/12-client-socks-proxy.php). @@ -667,14 +659,14 @@ from the outside (database behind firewall) and as such can also be used for plain HTTP and TLS-encrypted HTTPS. ```php -$proxy = new Clue\React\SshProxy\SshSocksConnector('me@localhost:22', $loop); +$proxy = new Clue\React\SshProxy\SshSocksConnector('me@localhost:22', Loop::get()); -$connector = new React\Socket\Connector($loop, array( +$connector = new React\Socket\Connector(null, array( 'tcp' => $proxy, 'dns' => false )); -$browser = new React\Http\Browser($loop, $connector); +$browser = new React\Http\Browser(null, $connector); ``` See also the [SSH proxy example](examples/13-client-ssh-proxy.php). @@ -692,10 +684,10 @@ no longer be used to establish the connection: ```php $connector = new React\Socket\FixedUriConnector( 'unix:///var/run/docker.sock', - new React\Socket\UnixConnector($loop) + new React\Socket\UnixConnector() ); -$browser = new Browser($loop, $connector); +$browser = new Browser(null, $connector); $client->get('http://localhost/info')->then(function (Psr\Http\Message\ResponseInterface $response) { var_dump($response->getHeaders(), (string)$response->getBody()); @@ -718,7 +710,7 @@ the constructor and will be invoked with the respective [request](#server-reques object and expects a [response](#server-response) object in return: ```php -$server = new React\Http\Server($loop, function (Psr\Http\Message\ServerRequestInterface $request) { +$server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterface $request) { return new React\Http\Message\Response( 200, array( @@ -737,6 +729,12 @@ Each outgoing HTTP response message is always represented by the [PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface), see also following [response](#server-response) chapter for more details. +This class takes an optional `LoopInterface|null $loop` parameter that can be used to +pass the event loop instance to use for this object. You can use a `null` value +here in order to use the [default loop](https://github.com/reactphp/event-loop#loop). +This value SHOULD NOT be given unless you're sure you want to explicitly use a +given event loop instance. + In order to start listening for any incoming connections, the `Server` needs to be attached to an instance of [`React\Socket\ServerInterface`](https://github.com/reactphp/socket#serverinterface) @@ -746,9 +744,9 @@ chapter. In its most simple form, you can attach this to a to start a plaintext HTTP server like this: ```php -$server = new React\Http\Server($loop, $handler); +$server = new React\Http\Server($handler); -$socket = new React\Socket\Server('0.0.0.0:8080', $loop); +$socket = new React\Socket\Server('0.0.0.0:8080'); $server->listen($socket); ``` @@ -817,7 +815,6 @@ once like this: ```php $server = new React\Http\Server( - $loop, new React\Http\Middleware\StreamingRequestMiddleware(), new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers new React\Http\Middleware\RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request @@ -843,7 +840,6 @@ in memory: ```php $server = new React\Http\Server( - $loop, new React\Http\Middleware\StreamingRequestMiddleware(), $handler ); @@ -872,9 +868,9 @@ messages. In its most common form, you can attach this to a order to start a plaintext HTTP server like this: ```php -$server = new React\Http\Server($loop, $handler); +$server = new React\Http\Server($handler); -$socket = new React\Socket\Server('0.0.0.0:8080', $loop); +$socket = new React\Socket\Server('0.0.0.0:8080'); $server->listen($socket); ``` @@ -898,9 +894,9 @@ using a secure TLS listen address, a certificate file and optional `passphrase` like this: ```php -$server = new React\Http\Server($loop, $handler); +$server = new React\Http\Server($handler); -$socket = new React\Socket\Server('tls://0.0.0.0:8443', $loop, array( +$socket = new React\Socket\Server('tls://0.0.0.0:8443', null, array( 'local_cert' => __DIR__ . '/localhost.pem' )); $server->listen($socket); @@ -923,7 +919,7 @@ which in turn extends the and will be passed to the callback function like this. ```php -$server = new React\Http\Server($loop, function (Psr\Http\Message\ServerRequestInterface $request) { +$server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterface $request) { $body = "The method of the request is: " . $request->getMethod(); $body .= "The requested path is: " . $request->getUri()->getPath(); @@ -966,7 +962,7 @@ The following parameters are currently available: Set to 'on' if the request used HTTPS, otherwise it won't be set ```php -$server = new React\Http\Server($loop, function (Psr\Http\Message\ServerRequestInterface $request) { +$server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterface $request) { $body = "Your IP is: " . $request->getServerParams()['REMOTE_ADDR']; return new React\Http\Message\Response( @@ -991,7 +987,7 @@ The `getQueryParams(): array` method can be used to get the query parameters similiar to the `$_GET` variable. ```php -$server = new React\Http\Server($loop, function (Psr\Http\Message\ServerRequestInterface $request) { +$server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterface $request) { $queryParams = $request->getQueryParams(); $body = 'The query parameter "foo" is not set. Click the following link '; @@ -1045,7 +1041,7 @@ By default, this method will only return parsed data for requests using request headers (commonly used for `POST` requests for HTML form submission data). ```php -$server = new React\Http\Server($loop, function (Psr\Http\Message\ServerRequestInterface $request) { +$server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterface $request) { $name = $request->getParsedBody()['name'] ?? 'anonymous'; return new React\Http\Message\Response( @@ -1069,7 +1065,7 @@ an XML (`Content-Type: application/xml`) request body (which is commonly used fo `POST`, `PUT` or `PATCH` requests in JSON-based or RESTful/RESTish APIs). ```php -$server = new React\Http\Server($loop, function (Psr\Http\Message\ServerRequestInterface $request) { +$server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterface $request) { $data = json_decode((string)$request->getBody()); $name = $data->name ?? 'anonymous'; @@ -1092,7 +1088,7 @@ This array will only be filled when using the `Content-Type: multipart/form-data request header (commonly used for `POST` requests for HTML file uploads). ```php -$server = new React\Http\Server($loop, function (Psr\Http\Message\ServerRequestInterface $request) { +$server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterface $request) { $files = $request->getUploadedFiles(); $name = isset($files['avatar']) ? $files['avatar']->getClientFilename() : 'nothing'; @@ -1166,7 +1162,6 @@ gives you access to the incoming request body as the individual chunks arrive: ```php $server = new React\Http\Server( - $loop, new React\Http\Middleware\StreamingRequestMiddleware(), function (Psr\Http\Message\ServerRequestInterface $request) { $body = $request->getBody(); @@ -1240,7 +1235,6 @@ may be unknown (`null`) when using `Transfer-Encoding: chunked` for HTTP/1.1 req ```php $server = new React\Http\Server( - $loop, new React\Http\Middleware\StreamingRequestMiddleware(), function (Psr\Http\Message\ServerRequestInterface $request) { $size = $request->getBody()->getSize(); @@ -1313,7 +1307,7 @@ The `getCookieParams(): string[]` method can be used to get all cookies sent with the current request. ```php -$server = new React\Http\Server($loop, function (Psr\Http\Message\ServerRequestInterface $request) { +$server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterface $request) { $key = 'react\php'; if (isset($request->getCookieParams()[$key])) { @@ -1385,7 +1379,7 @@ This projects ships a [`Response` class](#response) which implements the In its most simple form, you can use it like this: ```php -$server = new React\Http\Server($loop, function (Psr\Http\Message\ServerRequestInterface $request) { +$server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterface $request) { return new React\Http\Message\Response( 200, array( @@ -1413,9 +1407,9 @@ To prevent this you SHOULD use a This example shows how such a long-term action could look like: ```php -$server = new React\Http\Server($loop, function (Psr\Http\Message\ServerRequestInterface $request) use ($loop) { - return new Promise(function ($resolve, $reject) use ($loop) { - $loop->addTimer(1.5, function() use ($resolve) { +$server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterface $request) { + return new Promise(function ($resolve, $reject) { + Loop::addTimer(1.5, function() use ($resolve) { $response = new React\Http\Message\Response( 200, array( @@ -1451,15 +1445,15 @@ Note that other implementations of the may only support strings. ```php -$server = new React\Http\Server($loop, function (Psr\Http\Message\ServerRequestInterface $request) use ($loop) { +$server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterface $request) { $stream = new ThroughStream(); - $timer = $loop->addPeriodicTimer(0.5, function () use ($stream) { + $timer = Loop::addPeriodicTimer(0.5, function () use ($stream) { $stream->write(microtime(true) . PHP_EOL); }); - $loop->addTimer(5, function() use ($loop, $timer, $stream) { - $loop->cancelTimer($timer); + Loop::addTimer(5, function() use ($timer, $stream) { + Loop::cancelTimer($timer); $stream->end(); }); @@ -1543,7 +1537,7 @@ added automatically. This is the most common use case, for example when using a `string` response body like this: ```php -$server = new React\Http\Server($loop, function (Psr\Http\Message\ServerRequestInterface $request) { +$server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterface $request) { return new React\Http\Message\Response( 200, array( @@ -1562,10 +1556,10 @@ response messages will contain the plain response body. If you know the length of your streaming response body, you MAY want to specify it explicitly like this: ```php -$server = new React\Http\Server($loop, function (Psr\Http\Message\ServerRequestInterface $request) use ($loop) { +$server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterface $request) { $stream = new ThroughStream(); - $loop->addTimer(2.0, function () use ($stream) { + Loop::addTimer(2.0, function () use ($stream) { $stream->end("Hello World!\n"); }); @@ -1638,7 +1632,7 @@ A `Server: ReactPHP/1` response header will be added automatically. You can add a custom `Server` response header like this: ```php -$server = new React\Http\Server($loop, function (ServerRequestInterface $request) { +$server = new React\Http\Server(function (ServerRequestInterface $request) { return new React\Http\Message\Response( 200, array( @@ -1653,7 +1647,7 @@ don't want to expose the underlying server software), you can use an empty string value like this: ```php -$server = new React\Http\Server($loop, function (ServerRequestInterface $request) { +$server = new React\Http\Server(function (ServerRequestInterface $request) { return new React\Http\Message\Response( 200, array( @@ -1668,7 +1662,7 @@ date and time if none is given. You can add a custom `Date` response header like this: ```php -$server = new React\Http\Server($loop, function (ServerRequestInterface $request) { +$server = new React\Http\Server(function (ServerRequestInterface $request) { return new React\Http\Message\Response( 200, array( @@ -1683,7 +1677,7 @@ don't have an appropriate clock to rely on), you can use an empty string value like this: ```php -$server = new React\Http\Server($loop, function (ServerRequestInterface $request) { +$server = new React\Http\Server(function (ServerRequestInterface $request) { return new React\Http\Message\Response( 200, array( @@ -1767,7 +1761,6 @@ header (`Request-Time`) and a final request handler that always returns a 200 co ```php $server = new React\Http\Server( - $loop, function (Psr\Http\Message\ServerRequestInterface $request, callable $next) { $request = $request->withHeader('Request-Time', time()); return $next($request); @@ -1792,7 +1785,6 @@ In order to simplify handling both paths, you can simply wrap this in a ```php $server = new React\Http\Server( - $loop, function (Psr\Http\Message\ServerRequestInterface $request, callable $next) { $promise = React\Promise\resolve($next($request)); return $promise->then(function (ResponseInterface $response) { @@ -1815,7 +1807,6 @@ handling logic (or logging etc.) by wrapping this in a ```php $server = new React\Http\Server( - $loop, function (Psr\Http\Message\ServerRequestInterface $request, callable $next) { $promise = new React\Promise\Promise(function ($resolve) use ($next, $request) { $resolve($next($request)); @@ -1873,20 +1864,23 @@ feel free to add it to this list. The `React\Http\Browser` is responsible for sending HTTP requests to your HTTP server and keeps track of pending incoming HTTP responses. -It also registers everything with the main [`EventLoop`](https://github.com/reactphp/event-loop#usage). ```php -$loop = React\EventLoop\Factory::create(); - -$browser = new React\Http\Browser($loop); +$browser = new React\Http\Browser(); ``` +This class takes an optional `LoopInterface|null $loop` parameter that can be used to +pass the event loop instance to use for this object. You can use a `null` value +here in order to use the [default loop](https://github.com/reactphp/event-loop#loop). +This value SHOULD NOT be given unless you're sure you want to explicitly use a +given event loop instance. + If you need custom connector settings (DNS resolution, TLS parameters, timeouts, proxy servers etc.), you can explicitly pass a custom instance of the [`ConnectorInterface`](https://github.com/reactphp/socket#connectorinterface): ```php -$connector = new React\Socket\Connector($loop, array( +$connector = new React\Socket\Connector(null, array( 'dns' => '127.0.0.1', 'tcp' => array( 'bindto' => '192.168.10.1:0' @@ -1897,7 +1891,7 @@ $connector = new React\Socket\Connector($loop, array( ) )); -$browser = new React\Http\Browser($loop, $connector); +$browser = new React\Http\Browser(null, $connector); ``` > Note that the browser class is final and shouldn't be extended, it is likely to be marked final in a future release. @@ -1959,7 +1953,7 @@ matching `Content-Length` request header like so: ```php $body = new React\Stream\ThroughStream(); -$loop->addTimer(1.0, function () use ($body) { +Loop::addTimer(1.0, function () use ($body) { $body->end("hello world"); }); @@ -2002,7 +1996,7 @@ matching `Content-Length` request header like so: ```php $body = new React\Stream\ThroughStream(); -$loop->addTimer(1.0, function () use ($body) { +Loop::addTimer(1.0, function () use ($body) { $body->end("hello world"); }); @@ -2036,7 +2030,7 @@ matching `Content-Length` request header like so: ```php $body = new React\Stream\ThroughStream(); -$loop->addTimer(1.0, function () use ($body) { +Loop::addTimer(1.0, function () use ($body) { $body->end("hello world"); }); @@ -2084,7 +2078,7 @@ explicitly pass in a matching `Content-Length` request header like so: ```php $body = new React\Stream\ThroughStream(); -$loop->addTimer(1.0, function () use ($body) { +Loop::addTimer(1.0, function () use ($body) { $body->end("hello world"); }); @@ -2146,7 +2140,7 @@ explicitly pass in a matching `Content-Length` request header like so: ```php $body = new React\Stream\ThroughStream(); -$loop->addTimer(1.0, function () use ($body) { +Loop::addTimer(1.0, function () use ($body) { $body->end("hello world"); }); @@ -2513,7 +2507,6 @@ than 10 handlers will be invoked at once: ```php $server = new React\Http\Server( - $loop, new React\Http\Middleware\LimitConcurrentRequestsMiddleware(10), $handler ); @@ -2525,7 +2518,6 @@ to limit the total number of requests that can be buffered at once: ```php $server = new React\Http\Server( - $loop, new React\Http\Middleware\StreamingRequestMiddleware(), new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers new React\Http\Middleware\RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request @@ -2540,7 +2532,6 @@ processes one request after another without any concurrency: ```php $server = new React\Http\Server( - $loop, new React\Http\Middleware\StreamingRequestMiddleware(), new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers new React\Http\Middleware\RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request @@ -2594,7 +2585,6 @@ Usage: ```php $server = new React\Http\Server( - $loop, new React\Http\Middleware\StreamingRequestMiddleware(), new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers new React\Http\Middleware\RequestBodyBufferMiddleware(16 * 1024 * 1024), // 16 MiB @@ -2656,7 +2646,6 @@ $handler = function (Psr\Http\Message\ServerRequestInterface $request) { }; $server = new React\Http\Server( - $loop, new React\Http\Middleware\StreamingRequestMiddleware(), new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers new React\Http\Middleware\RequestBodyBufferMiddleware(16 * 1024 * 1024), // 16 MiB diff --git a/composer.json b/composer.json index 6924ebff..9e30f4cf 100644 --- a/composer.json +++ b/composer.json @@ -29,11 +29,12 @@ "php": ">=5.3.0", "evenement/evenement": "^3.0 || ^2.0 || ^1.0", "psr/http-message": "^1.0", - "react/event-loop": "^1.0 || ^0.5", + "react/dns": "dev-default-loop#28e5df1 as 1.8.0", + "react/event-loop": "dev-master#78f7f43 as 1.2.0", "react/promise": "^2.3 || ^1.2.1", "react/promise-stream": "^1.1", - "react/socket": "^1.6", - "react/stream": "^1.1", + "react/socket": "dev-default-loop#b471dc7 as 1.8.0", + "react/stream": "dev-default-loop#e617d63 as 1.2.0", "ringcentral/psr7": "^1.2" }, "require-dev": { @@ -48,5 +49,19 @@ }, "autoload-dev": { "psr-4": { "React\\Tests\\Http\\": "tests" } - } + }, + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/clue-labs/dns" + }, + { + "type": "vcs", + "url": "https://github.com/clue-labs/socket" + }, + { + "type": "vcs", + "url": "https://github.com/clue-labs/stream" + } + ] } diff --git a/examples/01-client-get-request.php b/examples/01-client-get-request.php index 31a82606..8e232398 100644 --- a/examples/01-client-get-request.php +++ b/examples/01-client-get-request.php @@ -5,11 +5,8 @@ require __DIR__ . '/../vendor/autoload.php'; -$loop = React\EventLoop\Factory::create(); -$client = new Browser($loop); +$client = new Browser(); $client->get('http://google.com/')->then(function (ResponseInterface $response) { var_dump($response->getHeaders(), (string)$response->getBody()); }); - -$loop->run(); diff --git a/examples/02-client-concurrent-requests.php b/examples/02-client-concurrent-requests.php index 5a9e4258..dca1d9c1 100644 --- a/examples/02-client-concurrent-requests.php +++ b/examples/02-client-concurrent-requests.php @@ -5,8 +5,7 @@ require __DIR__ . '/../vendor/autoload.php'; -$loop = React\EventLoop\Factory::create(); -$client = new Browser($loop); +$client = new Browser(); $client->head('http://www.github.com/clue/http-react')->then(function (ResponseInterface $response) { var_dump($response->getHeaders(), (string)$response->getBody()); @@ -19,5 +18,3 @@ $client->get('http://www.lueck.tv/psocksd')->then(function (ResponseInterface $response) { var_dump($response->getHeaders(), (string)$response->getBody()); }); - -$loop->run(); diff --git a/examples/03-client-request-any.php b/examples/03-client-request-any.php index 881dabfc..a3bd2831 100644 --- a/examples/03-client-request-any.php +++ b/examples/03-client-request-any.php @@ -8,8 +8,7 @@ require __DIR__ . '/../vendor/autoload.php'; -$loop = React\EventLoop\Factory::create(); -$client = new Browser($loop); +$client = new Browser(); $promises = array( $client->head('http://www.github.com/clue/http-react'), @@ -28,5 +27,3 @@ var_dump($response->getHeaders()); echo PHP_EOL . $response->getBody(); }); - -$loop->run(); diff --git a/examples/04-client-post-json.php b/examples/04-client-post-json.php index 818dc9bc..400b1a13 100644 --- a/examples/04-client-post-json.php +++ b/examples/04-client-post-json.php @@ -5,8 +5,7 @@ require __DIR__ . '/../vendor/autoload.php'; -$loop = React\EventLoop\Factory::create(); -$client = new Browser($loop); +$client = new Browser(); $data = array( 'name' => array( @@ -25,5 +24,3 @@ )->then(function (ResponseInterface $response) { echo (string)$response->getBody(); }, 'printf'); - -$loop->run(); diff --git a/examples/05-client-put-xml.php b/examples/05-client-put-xml.php index 7c23182d..05804f23 100644 --- a/examples/05-client-put-xml.php +++ b/examples/05-client-put-xml.php @@ -5,8 +5,7 @@ require __DIR__ . '/../vendor/autoload.php'; -$loop = React\EventLoop\Factory::create(); -$client = new Browser($loop); +$client = new Browser(); $xml = new SimpleXMLElement(''); $child = $xml->addChild('user'); @@ -22,5 +21,3 @@ )->then(function (ResponseInterface $response) { echo (string)$response->getBody(); }, 'printf'); - -$loop->run(); diff --git a/examples/11-client-http-connect-proxy.php b/examples/11-client-http-connect-proxy.php index 53d2e91a..afa22e83 100644 --- a/examples/11-client-http-connect-proxy.php +++ b/examples/11-client-http-connect-proxy.php @@ -6,29 +6,24 @@ // $ php examples/72-server-http-connect-proxy.php 8080 // $ php examples/11-client-http-connect-proxy.php -use React\Http\Browser; use Clue\React\HttpProxy\ProxyConnector as HttpConnectClient; use Psr\Http\Message\ResponseInterface; -use React\EventLoop\Factory as LoopFactory; +use React\Http\Browser; use React\Socket\Connector; require __DIR__ . '/../vendor/autoload.php'; -$loop = LoopFactory::create(); - // create a new HTTP CONNECT proxy client which connects to a HTTP CONNECT proxy server listening on localhost:8080 -$proxy = new HttpConnectClient('127.0.0.1:8080', new Connector($loop)); +$proxy = new HttpConnectClient('127.0.0.1:8080', new Connector()); // create a Browser object that uses the HTTP CONNECT proxy client for connections -$connector = new Connector($loop, array( +$connector = new Connector(null, array( 'tcp' => $proxy, 'dns' => false )); -$browser = new Browser($loop, $connector); +$browser = new Browser(null, $connector); // demo fetching HTTP headers (or bail out otherwise) $browser->get('https://www.google.com/')->then(function (ResponseInterface $response) { echo RingCentral\Psr7\str($response); }, 'printf'); - -$loop->run(); diff --git a/examples/12-client-socks-proxy.php b/examples/12-client-socks-proxy.php index 49827fa2..ec2375c5 100644 --- a/examples/12-client-socks-proxy.php +++ b/examples/12-client-socks-proxy.php @@ -3,29 +3,24 @@ // not already running a SOCKS proxy server? // Try LeProxy.org or this: `ssh -D 1080 localhost` -use React\Http\Browser; use Clue\React\Socks\Client as SocksClient; use Psr\Http\Message\ResponseInterface; -use React\EventLoop\Factory as LoopFactory; +use React\Http\Browser; use React\Socket\Connector; require __DIR__ . '/../vendor/autoload.php'; -$loop = LoopFactory::create(); - // create a new SOCKS proxy client which connects to a SOCKS proxy server listening on localhost:1080 -$proxy = new SocksClient('127.0.0.1:1080', new Connector($loop)); +$proxy = new SocksClient('127.0.0.1:1080', new Connector()); // create a Browser object that uses the SOCKS proxy client for connections -$connector = new Connector($loop, array( +$connector = new Connector(null, array( 'tcp' => $proxy, 'dns' => false )); -$browser = new Browser($loop, $connector); +$browser = new Browser(null, $connector); // demo fetching HTTP headers (or bail out otherwise) $browser->get('https://www.google.com/')->then(function (ResponseInterface $response) { echo RingCentral\Psr7\str($response); }, 'printf'); - -$loop->run(); diff --git a/examples/13-client-ssh-proxy.php b/examples/13-client-ssh-proxy.php index d0424fea..d4c8dcea 100644 --- a/examples/13-client-ssh-proxy.php +++ b/examples/13-client-ssh-proxy.php @@ -1,29 +1,25 @@ $proxy, 'dns' => false )); -$browser = new Browser($loop, $connector); +$browser = new Browser(null, $connector); // demo fetching HTTP headers (or bail out otherwise) $browser->get('https://www.google.com/')->then(function (ResponseInterface $response) { echo RingCentral\Psr7\str($response); }, 'printf'); - -$loop->run(); diff --git a/examples/14-client-unix-domain-sockets.php b/examples/14-client-unix-domain-sockets.php index 8881321e..f60dd9f7 100644 --- a/examples/14-client-unix-domain-sockets.php +++ b/examples/14-client-unix-domain-sockets.php @@ -1,27 +1,22 @@ get('http://localhost/info')->then(function (ResponseInterface $response) { echo Psr7\str($response); }, 'printf'); - -$loop->run(); diff --git a/examples/21-client-request-streaming-to-stdout.php b/examples/21-client-request-streaming-to-stdout.php index b7873775..3d2110a2 100644 --- a/examples/21-client-request-streaming-to-stdout.php +++ b/examples/21-client-request-streaming-to-stdout.php @@ -13,11 +13,10 @@ exit(1); } -$loop = React\EventLoop\Factory::create(); -$client = new Browser($loop); +$client = new Browser(); -$out = new WritableResourceStream(STDOUT, $loop); -$info = new WritableResourceStream(STDERR, $loop); +$out = new WritableResourceStream(STDOUT); +$info = new WritableResourceStream(STDERR); $url = isset($argv[1]) ? $argv[1] : 'http://google.com/'; $info->write('Requesting ' . $url . '…' . PHP_EOL); @@ -29,5 +28,3 @@ assert($body instanceof ReadableStreamInterface); $body->pipe($out); }, 'printf'); - -$loop->run(); diff --git a/examples/22-client-stream-upload-from-stdin.php b/examples/22-client-stream-upload-from-stdin.php index 4a36df91..a0857feb 100644 --- a/examples/22-client-stream-upload-from-stdin.php +++ b/examples/22-client-stream-upload-from-stdin.php @@ -1,7 +1,7 @@ post($url, array(), $in)->then(function (ResponseInterface $response) { echo 'Received' . PHP_EOL . Psr7\str($response); }, 'printf'); - -$loop->run(); diff --git a/examples/51-server-hello-world.php b/examples/51-server-hello-world.php index f6903cff..2cfd5649 100644 --- a/examples/51-server-hello-world.php +++ b/examples/51-server-hello-world.php @@ -1,15 +1,12 @@ listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; - -$loop->run(); diff --git a/examples/52-server-count-visitors.php b/examples/52-server-count-visitors.php index 2b8e897c..ae173bbd 100644 --- a/examples/52-server-count-visitors.php +++ b/examples/52-server-count-visitors.php @@ -1,16 +1,13 @@ listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; - -$loop->run(); diff --git a/examples/53-server-whatsmyip.php b/examples/53-server-whatsmyip.php index 18f7504e..82f34742 100644 --- a/examples/53-server-whatsmyip.php +++ b/examples/53-server-whatsmyip.php @@ -1,15 +1,12 @@ getServerParams()['REMOTE_ADDR']; return new Response( @@ -21,9 +18,7 @@ ); }); -$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); +$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); $server->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; - -$loop->run(); diff --git a/examples/54-server-query-parameter.php b/examples/54-server-query-parameter.php index 2786f380..dbddfbc0 100644 --- a/examples/54-server-query-parameter.php +++ b/examples/54-server-query-parameter.php @@ -1,15 +1,12 @@ getQueryParams(); $body = 'The query parameter "foo" is not set. Click the following link '; @@ -28,9 +25,7 @@ ); }); -$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); +$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); $server->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; - -$loop->run(); diff --git a/examples/55-server-cookie-handling.php b/examples/55-server-cookie-handling.php index 6faf6be7..55370c3b 100644 --- a/examples/55-server-cookie-handling.php +++ b/examples/55-server-cookie-handling.php @@ -1,15 +1,12 @@ getCookieParams()[$key])) { @@ -34,9 +31,7 @@ ); }); -$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); +$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); $server->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; - -$loop->run(); diff --git a/examples/56-server-sleep.php b/examples/56-server-sleep.php index 3da6963b..9f149c4c 100644 --- a/examples/56-server-sleep.php +++ b/examples/56-server-sleep.php @@ -1,18 +1,16 @@ addTimer(1.5, function() use ($resolve) { +$server = new Server(function (ServerRequestInterface $request) { + return new Promise(function ($resolve, $reject) { + Loop::addTimer(1.5, function() use ($resolve) { $response = new Response( 200, array( @@ -25,9 +23,7 @@ }); }); -$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); +$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); $server->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; - -$loop->run(); diff --git a/examples/57-server-error-handling.php b/examples/57-server-error-handling.php index c8e99ee4..6952a559 100644 --- a/examples/57-server-error-handling.php +++ b/examples/57-server-error-handling.php @@ -1,17 +1,14 @@ listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; - -$loop->run(); diff --git a/examples/58-server-stream-response.php b/examples/58-server-stream-response.php index 518c2cb4..941ded1f 100644 --- a/examples/58-server-stream-response.php +++ b/examples/58-server-stream-response.php @@ -1,16 +1,14 @@ getMethod() !== 'GET' || $request->getUri()->getPath() !== '/') { return new Response(404); } @@ -18,18 +16,18 @@ $stream = new ThroughStream(); // send some data every once in a while with periodic timer - $timer = $loop->addPeriodicTimer(0.5, function () use ($stream) { + $timer = Loop::addPeriodicTimer(0.5, function () use ($stream) { $stream->write(microtime(true) . PHP_EOL); }); // demo for ending stream after a few seconds - $loop->addTimer(5.0, function() use ($stream) { + Loop::addTimer(5.0, function() use ($stream) { $stream->end(); }); // stop timer if stream is closed (such as when connection is closed) - $stream->on('close', function () use ($loop, $timer) { - $loop->cancelTimer($timer); + $stream->on('close', function () use ($timer) { + Loop::cancelTimer($timer); }); return new Response( @@ -41,9 +39,7 @@ ); }); -$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); +$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); $server->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; - -$loop->run(); diff --git a/examples/59-server-json-api.php b/examples/59-server-json-api.php index 8602a889..c1b4d305 100644 --- a/examples/59-server-json-api.php +++ b/examples/59-server-json-api.php @@ -7,15 +7,12 @@ // $ curl -v http://localhost:8080/ -H 'Content-Type: application/json' -d '{"name":"Alice"}' use Psr\Http\Message\ServerRequestInterface; -use React\EventLoop\Factory; use React\Http\Message\Response; use React\Http\Server; require __DIR__ . '/../vendor/autoload.php'; -$loop = Factory::create(); - -$server = new Server($loop, function (ServerRequestInterface $request) { +$server = new Server(function (ServerRequestInterface $request) { if ($request->getHeaderLine('Content-Type') !== 'application/json') { return new Response( 415, // Unsupported Media Type @@ -56,9 +53,7 @@ ); }); -$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); +$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); $server->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; - -$loop->run(); diff --git a/examples/61-server-hello-world-https.php b/examples/61-server-hello-world-https.php index dfe3e941..5b671618 100644 --- a/examples/61-server-hello-world-https.php +++ b/examples/61-server-hello-world-https.php @@ -1,15 +1,12 @@ isset($argv[2]) ? $argv[2] : __DIR__ . '/localhost.pem' )); $server->listen($socket); @@ -28,5 +25,3 @@ //$socket->on('error', 'printf'); echo 'Listening on ' . str_replace('tls:', 'https:', $socket->getAddress()) . PHP_EOL; - -$loop->run(); diff --git a/examples/62-server-form-upload.php b/examples/62-server-form-upload.php index d7eef4f3..b1f0d8ee 100644 --- a/examples/62-server-form-upload.php +++ b/examples/62-server-form-upload.php @@ -9,7 +9,6 @@ use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\UploadedFileInterface; -use React\EventLoop\Factory; use React\Http\Message\Response; use React\Http\Middleware\LimitConcurrentRequestsMiddleware; use React\Http\Middleware\RequestBodyBufferMiddleware; @@ -19,8 +18,6 @@ require __DIR__ . '/../vendor/autoload.php'; -$loop = Factory::create(); - $handler = function (ServerRequestInterface $request) { if ($request->getMethod() === 'POST') { // Take form input values from POST values (for illustration purposes only!) @@ -125,7 +122,6 @@ // Note how this example explicitly uses the advanced `StreamingRequestMiddleware` to apply // custom request buffering limits below before running our request handler. $server = new Server( - $loop, new StreamingRequestMiddleware(), new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers, queue otherwise new RequestBodyBufferMiddleware(8 * 1024 * 1024), // 8 MiB max, ignore body otherwise @@ -133,9 +129,7 @@ $handler ); -$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); +$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', null); $server->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; - -$loop->run(); diff --git a/examples/63-server-streaming-request.php b/examples/63-server-streaming-request.php index 45eb0dea..c2416e15 100644 --- a/examples/63-server-streaming-request.php +++ b/examples/63-server-streaming-request.php @@ -1,16 +1,11 @@ getBody(); @@ -49,9 +44,7 @@ function (Psr\Http\Message\ServerRequestInterface $request) { $server->on('error', 'printf'); -$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); +$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); $server->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; - -$loop->run(); diff --git a/examples/71-server-http-proxy.php b/examples/71-server-http-proxy.php index b959b7bf..95c2f411 100644 --- a/examples/71-server-http-proxy.php +++ b/examples/71-server-http-proxy.php @@ -11,13 +11,11 @@ require __DIR__ . '/../vendor/autoload.php'; -$loop = Factory::create(); - // Note how this example uses the `Server` without the `StreamingRequestMiddleware`. // This means that this proxy buffers the whole request before "processing" it. // As such, this is store-and-forward proxy. This could also use the advanced // `StreamingRequestMiddleware` to forward the incoming request as it comes in. -$server = new Server($loop, function (RequestInterface $request) { +$server = new Server(function (RequestInterface $request) { if (strpos($request->getRequestTarget(), '://') === false) { return new Response( 400, @@ -48,9 +46,7 @@ ); }); -$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); +$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); $server->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; - -$loop->run(); diff --git a/examples/72-server-http-connect-proxy.php b/examples/72-server-http-connect-proxy.php index e786da76..a1f33983 100644 --- a/examples/72-server-http-connect-proxy.php +++ b/examples/72-server-http-connect-proxy.php @@ -4,7 +4,6 @@ // $ curl -v --proxy http://localhost:8080 https://reactphp.org/ use Psr\Http\Message\ServerRequestInterface; -use React\EventLoop\Factory; use React\Http\Message\Response; use React\Http\Server; use React\Socket\Connector; @@ -12,14 +11,13 @@ require __DIR__ . '/../vendor/autoload.php'; -$loop = Factory::create(); -$connector = new Connector($loop); +$connector = new Connector(); // Note how this example uses the `Server` without the `StreamingRequestMiddleware`. // Unlike the plain HTTP proxy, the CONNECT method does not contain a body // and we establish an end-to-end connection over the stream object, so this // doesn't have to store any payload data in memory at all. -$server = new Server($loop, function (ServerRequestInterface $request) use ($connector) { +$server = new Server(function (ServerRequestInterface $request) use ($connector) { if ($request->getMethod() !== 'CONNECT') { return new Response( 405, @@ -53,9 +51,7 @@ function ($e) { ); }); -$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); +$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); $server->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; - -$loop->run(); diff --git a/examples/81-server-upgrade-echo.php b/examples/81-server-upgrade-echo.php index 34e85f6c..6a20181a 100644 --- a/examples/81-server-upgrade-echo.php +++ b/examples/81-server-upgrade-echo.php @@ -18,19 +18,17 @@ */ use Psr\Http\Message\ServerRequestInterface; -use React\EventLoop\Factory; +use React\EventLoop\Loop; use React\Http\Message\Response; use React\Http\Server; use React\Stream\ThroughStream; require __DIR__ . '/../vendor/autoload.php'; -$loop = Factory::create(); - // Note how this example uses the `Server` without the `StreamingRequestMiddleware`. // The initial incoming request does not contain a body and we upgrade to a // stream object below. -$server = new Server($loop, function (ServerRequestInterface $request) use ($loop) { +$server = new Server(function (ServerRequestInterface $request) { if ($request->getHeaderLine('Upgrade') !== 'echo' || $request->getProtocolVersion() === '1.0') { return new Response( 426, @@ -46,7 +44,7 @@ // this means that any Upgraded data will simply be sent back to the client $stream = new ThroughStream(); - $loop->addTimer(0, function () use ($stream) { + Loop::addTimer(0, function () use ($stream) { $stream->write("Hello! Anything you send will be piped back." . PHP_EOL); }); @@ -59,9 +57,7 @@ ); }); -$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); +$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); $server->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; - -$loop->run(); diff --git a/examples/82-server-upgrade-chat.php b/examples/82-server-upgrade-chat.php index 5e49ce37..2c6f08a8 100644 --- a/examples/82-server-upgrade-chat.php +++ b/examples/82-server-upgrade-chat.php @@ -20,7 +20,7 @@ */ use Psr\Http\Message\ServerRequestInterface; -use React\EventLoop\Factory; +use React\EventLoop\Loop; use React\Http\Message\Response; use React\Http\Server; use React\Stream\CompositeStream; @@ -28,8 +28,6 @@ require __DIR__ . '/../vendor/autoload.php'; -$loop = Factory::create(); - // simply use a shared duplex ThroughStream for all clients // it will simply emit any data that is sent to it // this means that any Upgraded data will simply be sent back to the client @@ -38,7 +36,7 @@ // Note how this example uses the `Server` without the `StreamingRequestMiddleware`. // The initial incoming request does not contain a body and we upgrade to a // stream object below. -$server = new Server($loop, function (ServerRequestInterface $request) use ($loop, $chat) { +$server = new Server(function (ServerRequestInterface $request) use ($chat) { if ($request->getHeaderLine('Upgrade') !== 'chat' || $request->getProtocolVersion() === '1.0') { return new Response( 426, @@ -68,7 +66,7 @@ }); // say hello to new user - $loop->addTimer(0, function () use ($chat, $username, $out) { + Loop::addTimer(0, function () use ($chat, $username, $out) { $out->write('Welcome to this chat example, ' . $username . '!' . PHP_EOL); $chat->write($username . ' joined' . PHP_EOL); }); @@ -87,9 +85,7 @@ ); }); -$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); +$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); $server->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; - -$loop->run(); diff --git a/examples/91-client-benchmark-download.php b/examples/91-client-benchmark-download.php index 4fd39b9d..2f76a3f3 100644 --- a/examples/91-client-benchmark-download.php +++ b/examples/91-client-benchmark-download.php @@ -11,8 +11,9 @@ // b2) run HTTP client receiving a 10 GB download: // $ php examples/91-client-benchmark-download.php http://localhost:8080/10g.bin -use React\Http\Browser; use Psr\Http\Message\ResponseInterface; +use React\EventLoop\Loop; +use React\Http\Browser; use React\Stream\ReadableStreamInterface; $url = isset($argv[1]) ? $argv[1] : 'http://google.com/'; @@ -23,12 +24,11 @@ echo 'NOTICE: The "xdebug" extension is loaded, this has a major impact on performance.' . PHP_EOL; } -$loop = React\EventLoop\Factory::create(); -$client = new Browser($loop); +$client = new Browser(); echo 'Requesting ' . $url . '…' . PHP_EOL; -$client->requestStreaming('GET', $url)->then(function (ResponseInterface $response) use ($loop) { +$client->requestStreaming('GET', $url)->then(function (ResponseInterface $response) { echo 'Headers received' . PHP_EOL; echo RingCentral\Psr7\str($response); @@ -42,19 +42,17 @@ }); // report progress every 0.1s - $timer = $loop->addPeriodicTimer(0.1, function () use (&$bytes) { + $timer = Loop::addPeriodicTimer(0.1, function () use (&$bytes) { echo "\rDownloaded " . $bytes . " bytes…"; }); // report results once the stream closes $time = microtime(true); - $stream->on('close', function() use (&$bytes, $timer, $loop, $time) { - $loop->cancelTimer($timer); + $stream->on('close', function() use (&$bytes, $timer, $time) { + Loop::cancelTimer($timer); $time = microtime(true) - $time; echo "\r" . 'Downloaded ' . $bytes . ' bytes in ' . round($time, 3) . 's => ' . round($bytes / $time / 1000000, 1) . ' MB/s' . PHP_EOL; }); }, 'printf'); - -$loop->run(); diff --git a/examples/92-client-benchmark-upload.php b/examples/92-client-benchmark-upload.php index cc2cf0c0..bd767966 100644 --- a/examples/92-client-benchmark-upload.php +++ b/examples/92-client-benchmark-upload.php @@ -11,9 +11,10 @@ // b2) run HTTP client sending a 10 GB upload // $ php examples/92-client-benchmark-upload.php http://localhost:8080/ 10000 -use React\Http\Browser; use Evenement\EventEmitter; use Psr\Http\Message\ResponseInterface; +use React\EventLoop\Loop; +use React\Http\Browser; use React\Stream\ReadableStreamInterface; use React\Stream\Util; use React\Stream\WritableStreamInterface; @@ -92,33 +93,30 @@ public function getPosition() } } -$loop = React\EventLoop\Factory::create(); -$client = new Browser($loop); +$client = new Browser(); $url = isset($argv[1]) ? $argv[1] : 'http://httpbin.org/post'; $n = isset($argv[2]) ? $argv[2] : 10; $source = new ChunkRepeater(str_repeat('x', 1000000), $n); -$loop->futureTick(function () use ($source) { +Loop::futureTick(function () use ($source) { $source->resume(); }); echo 'POSTing ' . $n . ' MB to ' . $url . PHP_EOL; $start = microtime(true); -$report = $loop->addPeriodicTimer(0.05, function () use ($source, $start) { +$report = Loop::addPeriodicTimer(0.05, function () use ($source, $start) { printf("\r%d bytes in %0.3fs...", $source->getPosition(), microtime(true) - $start); }); -$client->post($url, array('Content-Length' => $n * 1000000), $source)->then(function (ResponseInterface $response) use ($source, $report, $loop, $start) { +$client->post($url, array('Content-Length' => $n * 1000000), $source)->then(function (ResponseInterface $response) use ($source, $report, $start) { $now = microtime(true); - $loop->cancelTimer($report); + Loop::cancelTimer($report); printf("\r%d bytes in %0.3fs => %.1f MB/s\n", $source->getPosition(), $now - $start, $source->getPosition() / ($now - $start) / 1000000); echo rtrim(preg_replace('/x{5,}/','x…', (string) $response->getBody()), PHP_EOL) . PHP_EOL; -}, function ($e) use ($loop, $report) { - $loop->cancelTimer($report); +}, function ($e) use ($report) { + Loop::cancelTimer($report); echo 'Error: ' . $e->getMessage() . PHP_EOL; }); - -$loop->run(); diff --git a/examples/99-server-benchmark-download.php b/examples/99-server-benchmark-download.php index 1a49df72..a6b4e9c1 100644 --- a/examples/99-server-benchmark-download.php +++ b/examples/99-server-benchmark-download.php @@ -16,7 +16,6 @@ use Evenement\EventEmitter; use Psr\Http\Message\ServerRequestInterface; -use React\EventLoop\Factory; use React\Http\Message\Response; use React\Http\Server; use React\Stream\ReadableStreamInterface; @@ -24,8 +23,6 @@ require __DIR__ . '/../vendor/autoload.php'; -$loop = Factory::create(); - /** A readable stream that can emit a lot of data */ class ChunkRepeater extends EventEmitter implements ReadableStreamInterface { @@ -94,7 +91,7 @@ public function getSize() } } -$server = new Server($loop, function (ServerRequestInterface $request) use ($loop) { +$server = new Server(function (ServerRequestInterface $request) { switch ($request->getUri()->getPath()) { case '/': return new Response( @@ -114,7 +111,7 @@ public function getSize() return new Response(404); } - $loop->addTimer(0, array($stream, 'resume')); + React\EventLoop\Loop::addTimer(0, array($stream, 'resume')); return new Response( 200, @@ -126,9 +123,7 @@ public function getSize() ); }); -$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); +$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); $server->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; - -$loop->run(); diff --git a/src/Browser.php b/src/Browser.php index 188320e8..ed83689b 100644 --- a/src/Browser.php +++ b/src/Browser.php @@ -5,6 +5,7 @@ use Psr\Http\Message\ResponseInterface; use RingCentral\Psr7\Request; use RingCentral\Psr7\Uri; +use React\EventLoop\Loop; use React\EventLoop\LoopInterface; use React\Http\Io\ReadableBodyStream; use React\Http\Io\Sender; @@ -26,20 +27,23 @@ class Browser /** * The `Browser` is responsible for sending HTTP requests to your HTTP server * and keeps track of pending incoming HTTP responses. - * It also registers everything with the main [`EventLoop`](https://github.com/reactphp/event-loop#usage). * * ```php - * $loop = React\EventLoop\Factory::create(); - * - * $browser = new React\Http\Browser($loop); + * $browser = new React\Http\Browser(); * ``` * + * This class takes an optional `LoopInterface|null $loop` parameter that can be used to + * pass the event loop instance to use for this object. You can use a `null` value + * here in order to use the [default loop](https://github.com/reactphp/event-loop#loop). + * This value SHOULD NOT be given unless you're sure you want to explicitly use a + * given event loop instance. + * * If you need custom connector settings (DNS resolution, TLS parameters, timeouts, * proxy servers etc.), you can explicitly pass a custom instance of the * [`ConnectorInterface`](https://github.com/reactphp/socket#connectorinterface): * * ```php - * $connector = new React\Socket\Connector($loop, array( + * $connector = new React\Socket\Connector(null, array( * 'dns' => '127.0.0.1', * 'tcp' => array( * 'bindto' => '192.168.10.1:0' @@ -50,15 +54,16 @@ class Browser * ) * )); * - * $browser = new React\Http\Browser($loop, $connector); + * $browser = new React\Http\Browser(null, $connector); * ``` * - * @param LoopInterface $loop - * @param ConnectorInterface|null $connector [optional] Connector to use. + * @param ?LoopInterface $loop + * @param ?ConnectorInterface $connector [optional] Connector to use. * Should be `null` in order to use default Connector. */ - public function __construct(LoopInterface $loop, ConnectorInterface $connector = null) + public function __construct(LoopInterface $loop = null, ConnectorInterface $connector = null) { + $loop = $loop ?: Loop::get(); $this->transaction = new Transaction( Sender::createFromLoop($loop, $connector), $loop @@ -127,7 +132,7 @@ public function get($url, array $headers = array()) * * ```php * $body = new React\Stream\ThroughStream(); - * $loop->addTimer(1.0, function () use ($body) { + * Loop::addTimer(1.0, function () use ($body) { * $body->end("hello world"); * }); * @@ -185,7 +190,7 @@ public function head($url, array $headers = array()) * * ```php * $body = new React\Stream\ThroughStream(); - * $loop->addTimer(1.0, function () use ($body) { + * Loop::addTimer(1.0, function () use ($body) { * $body->end("hello world"); * }); * @@ -227,7 +232,7 @@ public function patch($url, array $headers = array(), $contents = '') * * ```php * $body = new React\Stream\ThroughStream(); - * $loop->addTimer(1.0, function () use ($body) { + * Loop::addTimer(1.0, function () use ($body) { * $body->end("hello world"); * }); * @@ -291,7 +296,7 @@ public function delete($url, array $headers = array(), $contents = '') * * ```php * $body = new React\Stream\ThroughStream(); - * $loop->addTimer(1.0, function () use ($body) { + * Loop::addTimer(1.0, function () use ($body) { * $body->end("hello world"); * }); * @@ -362,7 +367,7 @@ public function request($method, $url, array $headers = array(), $body = '') * * ```php * $body = new React\Stream\ThroughStream(); - * $loop->addTimer(1.0, function () use ($body) { + * Loop::addTimer(1.0, function () use ($body) { * $body->end("hello world"); * }); * diff --git a/src/Middleware/LimitConcurrentRequestsMiddleware.php b/src/Middleware/LimitConcurrentRequestsMiddleware.php index 9aaf5ff2..cc1dfc42 100644 --- a/src/Middleware/LimitConcurrentRequestsMiddleware.php +++ b/src/Middleware/LimitConcurrentRequestsMiddleware.php @@ -30,7 +30,6 @@ * * ```php * $server = new React\Http\Server( - * $loop, * new React\Http\Middleware\StreamingRequestMiddleware(), * new React\Http\Middleware\LimitConcurrentRequestsMiddleware(10), * $handler @@ -43,7 +42,6 @@ * * ```php * $server = new React\Http\Server( - * $loop, * new React\Http\Middleware\StreamingRequestMiddleware(), * new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers * new React\Http\Middleware\RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request @@ -58,7 +56,6 @@ * * ```php * $server = new React\Http\Server( - * $loop, * new React\Http\Middleware\StreamingRequestMiddleware(), * new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers * new React\Http\Middleware\RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request diff --git a/src/Server.php b/src/Server.php index 1aa5d405..c2e3f5a6 100644 --- a/src/Server.php +++ b/src/Server.php @@ -3,6 +3,7 @@ namespace React\Http; use Evenement\EventEmitter; +use React\EventLoop\Loop; use React\EventLoop\LoopInterface; use React\Http\Io\IniUtil; use React\Http\Io\MiddlewareRunner; @@ -23,7 +24,7 @@ * object and expects a [response](#server-response) object in return: * * ```php - * $server = new React\Http\Server($loop, function (Psr\Http\Message\ServerRequestInterface $request) { + * $server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterface $request) { * return new React\Http\Message\Response( * 200, * array( @@ -42,6 +43,12 @@ * [PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface), * see also following [response](#server-response) chapter for more details. * + * This class takes an optional `LoopInterface|null $loop` parameter that can be used to + * pass the event loop instance to use for this object. You can use a `null` value + * here in order to use the [default loop](https://github.com/reactphp/event-loop#loop). + * This value SHOULD NOT be given unless you're sure you want to explicitly use a + * given event loop instance. + * * In order to start listening for any incoming connections, the `Server` needs * to be attached to an instance of * [`React\Socket\ServerInterface`](https://github.com/reactphp/socket#serverinterface) @@ -51,9 +58,9 @@ * to start a plaintext HTTP server like this: * * ```php - * $server = new React\Http\Server($loop, $handler); + * $server = new React\Http\Server($handler); * - * $socket = new React\Socket\Server('0.0.0.0:8080', $loop); + * $socket = new React\Socket\Server('0.0.0.0:8080'); * $server->listen($socket); * ``` * @@ -122,7 +129,6 @@ * * ```php * $server = new React\Http\Server( - * $loop, * new React\Http\Middleware\StreamingRequestMiddleware(), * new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers * new React\Http\Middleware\RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request @@ -148,7 +154,6 @@ * * ```php * $server = new React\Http\Server( - * $loop, * new React\Http\Middleware\StreamingRequestMiddleware(), * $handler * ); @@ -191,14 +196,19 @@ final class Server extends EventEmitter * connections in order to then parse incoming data as HTTP. * See also [listen()](#listen) for more details. * - * @param LoopInterface $loop + * @param callable|LoopInterface $requestHandlerOrLoop * @param callable[] ...$requestHandler * @see self::listen() */ - public function __construct(LoopInterface $loop) + public function __construct($requestHandlerOrLoop) { $requestHandlers = \func_get_args(); - \array_shift($requestHandlers); + if (reset($requestHandlers) instanceof LoopInterface) { + $loop = \array_shift($requestHandlers); + } else { + $loop = Loop::get(); + } + $requestHandlersCount = \count($requestHandlers); if ($requestHandlersCount === 0 || \count(\array_filter($requestHandlers, 'is_callable')) < $requestHandlersCount) { throw new \InvalidArgumentException('Invalid request handler given'); @@ -253,9 +263,9 @@ public function __construct(LoopInterface $loop) * order to start a plaintext HTTP server like this: * * ```php - * $server = new React\Http\Server($loop, $handler); + * $server = new React\Http\Server($handler); * - * $socket = new React\Socket\Server(8080, $loop); + * $socket = new React\Socket\Server(8080); * $server->listen($socket); * ``` * @@ -279,9 +289,9 @@ public function __construct(LoopInterface $loop) * `passphrase` like this: * * ```php - * $server = new React\Http\Server($loop, $handler); + * $server = new React\Http\Server($handler); * - * $socket = new React\Socket\Server('tls://0.0.0.0:8443', $loop, array( + * $socket = new React\Socket\Server('tls://0.0.0.0:8443', null, array( * 'local_cert' => __DIR__ . '/localhost.pem' * )); * $server->listen($socket); diff --git a/tests/BrowserTest.php b/tests/BrowserTest.php index 612875fc..5df2b837 100644 --- a/tests/BrowserTest.php +++ b/tests/BrowserTest.php @@ -28,6 +28,21 @@ public function setUpBrowser() $ref->setValue($this->browser, $this->sender); } + public function testConstructWithoutLoopAssignsLoopAutomatically() + { + $browser = new Browser(); + + $ref = new \ReflectionProperty($browser, 'transaction'); + $ref->setAccessible(true); + $transaction = $ref->getValue($browser); + + $ref = new \ReflectionProperty($transaction, 'loop'); + $ref->setAccessible(true); + $loop = $ref->getValue($transaction); + + $this->assertInstanceOf('React\EventLoop\LoopInterface', $loop); + } + public function testGetSendsGetRequest() { $that = $this; diff --git a/tests/ServerTest.php b/tests/ServerTest.php index ff2cd9c1..6cb1b9b5 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -46,6 +46,21 @@ public function setUpConnectionMockAndSocket() $this->socket = new SocketServerStub(); } + public function testConstructWithoutLoopAssignsLoopAutomatically() + { + $server = new Server(function () { }); + + $ref = new \ReflectionProperty($server, 'streamingServer'); + $ref->setAccessible(true); + $streamingServer = $ref->getValue($server); + + $ref = new \ReflectionProperty($streamingServer, 'loop'); + $ref->setAccessible(true); + $loop = $ref->getValue($streamingServer); + + $this->assertInstanceOf('React\EventLoop\LoopInterface', $loop); + } + public function testInvalidCallbackFunctionLeadsToException() { $this->setExpectedException('InvalidArgumentException'); From 6b1a8261b2ac18ab219c9c949c789bb827b099e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 8 Jul 2021 16:46:55 +0200 Subject: [PATCH 365/456] Update to stable reactphp/event-loop v1.2.0 & updated stream and socket --- .github/workflows/ci.yml | 3 --- composer.json | 23 ++++------------------- 2 files changed, 4 insertions(+), 22 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a08971b8..cf214c83 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,8 +27,6 @@ jobs: with: php-version: ${{ matrix.php }} coverage: xdebug - env: - COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: composer install - run: vendor/bin/phpunit --coverage-text if: ${{ matrix.php >= 7.3 }} @@ -39,7 +37,6 @@ jobs: name: PHPUnit (HHVM) runs-on: ubuntu-18.04 continue-on-error: true - if: false # temporarily skipped until https://github.com/azjezz/setup-hhvm/issues/3 is addressed steps: - uses: actions/checkout@v2 - uses: azjezz/setup-hhvm@v1 diff --git a/composer.json b/composer.json index 9e30f4cf..6673b232 100644 --- a/composer.json +++ b/composer.json @@ -29,12 +29,11 @@ "php": ">=5.3.0", "evenement/evenement": "^3.0 || ^2.0 || ^1.0", "psr/http-message": "^1.0", - "react/dns": "dev-default-loop#28e5df1 as 1.8.0", - "react/event-loop": "dev-master#78f7f43 as 1.2.0", + "react/event-loop": "^1.2", "react/promise": "^2.3 || ^1.2.1", "react/promise-stream": "^1.1", - "react/socket": "dev-default-loop#b471dc7 as 1.8.0", - "react/stream": "dev-default-loop#e617d63 as 1.2.0", + "react/socket": "^1.8", + "react/stream": "^1.2", "ringcentral/psr7": "^1.2" }, "require-dev": { @@ -49,19 +48,5 @@ }, "autoload-dev": { "psr-4": { "React\\Tests\\Http\\": "tests" } - }, - "repositories": [ - { - "type": "vcs", - "url": "https://github.com/clue-labs/dns" - }, - { - "type": "vcs", - "url": "https://github.com/clue-labs/socket" - }, - { - "type": "vcs", - "url": "https://github.com/clue-labs/stream" - } - ] + } } From d9401a5c1be1ed967ee4d94187371cedfe1e8d85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 11 Jul 2021 14:57:54 +0200 Subject: [PATCH 366/456] Work around failing tests on legacy PHP 5.3 --- tests/Client/FunctionalIntegrationTest.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/Client/FunctionalIntegrationTest.php b/tests/Client/FunctionalIntegrationTest.php index 2db75b35..3e07803a 100644 --- a/tests/Client/FunctionalIntegrationTest.php +++ b/tests/Client/FunctionalIntegrationTest.php @@ -83,6 +83,9 @@ public function testRequestLegacyHttpServerWithOnlyLineFeedReturnsSuccessfulResp /** @group internet */ public function testSuccessfulResponseEmitsEnd() { + // max_nesting_level was set to 100 for PHP Versions < 5.4 which resulted in failing test for legacy PHP + ini_set('xdebug.max_nesting_level', 256); + $loop = Factory::create(); $client = new Client($loop); @@ -106,6 +109,9 @@ public function testPostDataReturnsData() $this->markTestSkipped('Not supported on HHVM'); } + // max_nesting_level was set to 100 for PHP Versions < 5.4 which resulted in failing test for legacy PHP + ini_set('xdebug.max_nesting_level', 256); + $loop = Factory::create(); $client = new Client($loop); From b008fb855ac446e46fab9f3eebb23a797f311595 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 11 Jul 2021 15:03:19 +0200 Subject: [PATCH 367/456] Prepare v1.4.0 release --- CHANGELOG.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ae6bc15..f562f66d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +## 1.4.0 (2021-07-11) + +A major new feature release, see [**release announcement**](https://clue.engineering/2021/announcing-reactphp-default-loop). + +* Feature: Simplify usage by supporting new [default loop](https://reactphp.org/event-loop/#loop). + (#410 by @clue) + + ```php + // old (still supported) + $browser = new React\Http\Browser($loop); + $server = new React\Http\Server($loop, $handler); + + // new (using default loop) + $browser = new React\Http\Browser(); + $server = new React\Http\Server($handler); + ``` + ## 1.3.0 (2021-04-11) * Feature: Support persistent connections (`Connection: keep-alive`). From e654f59a267e72a157916a8adeb48c6f961bfea2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 24 Jul 2021 14:46:19 +0200 Subject: [PATCH 368/456] Rename `Server` to `HttpServer` to avoid class name collisions --- README.md | 128 +++++++++--------- examples/51-server-hello-world.php | 7 +- examples/52-server-count-visitors.php | 7 +- examples/53-server-whatsmyip.php | 7 +- examples/54-server-query-parameter.php | 7 +- examples/55-server-cookie-handling.php | 7 +- examples/56-server-sleep.php | 7 +- examples/57-server-error-handling.php | 7 +- examples/58-server-stream-response.php | 7 +- examples/59-server-json-api.php | 7 +- examples/61-server-hello-world-https.php | 9 +- examples/62-server-form-upload.php | 7 +- examples/63-server-streaming-request.php | 8 +- examples/71-server-http-proxy.php | 10 +- examples/72-server-http-connect-proxy.php | 9 +- examples/81-server-upgrade-echo.php | 9 +- examples/82-server-upgrade-chat.php | 9 +- examples/99-server-benchmark-download.php | 7 +- src/{Server.php => HttpServer.php} | 32 +++-- src/Io/StreamingServer.php | 8 +- .../LimitConcurrentRequestsMiddleware.php | 6 +- src/Middleware/StreamingRequestMiddleware.php | 10 +- tests/FunctionalBrowserTest.php | 18 +-- ...rTest.php => FunctionalHttpServerTest.php} | 106 +++++++-------- tests/{ServerTest.php => HttpServerTest.php} | 98 +++++++------- 25 files changed, 264 insertions(+), 273 deletions(-) rename src/{Server.php => HttpServer.php} (93%) rename tests/{FunctionalServerTest.php => FunctionalHttpServerTest.php} (90%) rename tests/{ServerTest.php => HttpServerTest.php} (81%) diff --git a/README.md b/README.md index 126fdcaf..d0fe6961 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ multiple concurrent HTTP requests without blocking. * [SSH proxy](#ssh-proxy) * [Unix domain sockets](#unix-domain-sockets) * [Server Usage](#server-usage) - * [Server](#server) + * [HttpServer](#httpserver) * [listen()](#listen) * [Server Request](#server-request) * [Request parameters](#request-parameters) @@ -97,7 +97,7 @@ $client->get('http://www.google.com/')->then(function (Psr\Http\Message\Response This is an HTTP server which responds with `Hello World!` to every request. ```php -$server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterface $request) { +$http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { return new React\Http\Message\Response( 200, array( @@ -108,7 +108,7 @@ $server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterfac }); $socket = new React\Socket\Server(8080); -$server->listen($socket); +$http->listen($socket); ``` See also the [examples](examples/). @@ -699,9 +699,11 @@ See also the [Unix Domain Sockets (UDS) example](examples/14-client-unix-domain- ## Server Usage -### Server +### HttpServer -The `React\Http\Server` class is responsible for handling incoming connections and then + + +The `React\Http\HttpServer` class is responsible for handling incoming connections and then processing each incoming HTTP request. When a complete HTTP request has been received, it will invoke the given @@ -710,7 +712,7 @@ the constructor and will be invoked with the respective [request](#server-reques object and expects a [response](#server-response) object in return: ```php -$server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterface $request) { +$http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { return new React\Http\Message\Response( 200, array( @@ -735,7 +737,7 @@ here in order to use the [default loop](https://github.com/reactphp/event-loop#l This value SHOULD NOT be given unless you're sure you want to explicitly use a given event loop instance. -In order to start listening for any incoming connections, the `Server` needs +In order to start listening for any incoming connections, the `HttpServer` needs to be attached to an instance of [`React\Socket\ServerInterface`](https://github.com/reactphp/socket#serverinterface) through the [`listen()`](#listen) method as described in the following @@ -744,17 +746,17 @@ chapter. In its most simple form, you can attach this to a to start a plaintext HTTP server like this: ```php -$server = new React\Http\Server($handler); +$http = new React\Http\HttpServer($handler); $socket = new React\Socket\Server('0.0.0.0:8080'); -$server->listen($socket); +$http->listen($socket); ``` See also the [`listen()`](#listen) method and the [hello world server example](examples/51-server-hello-world.php) for more details. -By default, the `Server` buffers and parses the complete incoming HTTP +By default, the `HttpServer` buffers and parses the complete incoming HTTP request in memory. It will invoke the given request handler function when the complete request headers and request body has been received. This means the [request](#server-request) object passed to your request handler function will be @@ -807,14 +809,14 @@ limit to allow for more concurrent requests (set `memory_limit 512M` or more) or explicitly limit concurrency. In order to override the above buffering defaults, you can configure the -`Server` explicitly. You can use the +`HttpServer` explicitly. You can use the [`LimitConcurrentRequestsMiddleware`](#limitconcurrentrequestsmiddleware) and [`RequestBodyBufferMiddleware`](#requestbodybuffermiddleware) (see below) to explicitly configure the total number of requests that can be handled at once like this: ```php -$server = new React\Http\Server( +$http = new React\Http\HttpServer( new React\Http\Middleware\StreamingRequestMiddleware(), new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers new React\Http\Middleware\RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request @@ -839,7 +841,7 @@ also use a streaming approach where only small chunks of data have to be kept in memory: ```php -$server = new React\Http\Server( +$http = new React\Http\HttpServer( new React\Http\Middleware\StreamingRequestMiddleware(), $handler ); @@ -854,6 +856,10 @@ have full control over consuming the incoming HTTP request body and concurrency settings. See also [streaming incoming request](#streaming-incoming-request) below for more details. +> Changelog v1.5.0: This class has been renamed to `HttpServer` from the + previous `Server` class in order to avoid any ambiguities. + The previous name has been deprecated and should not be used anymore. + ### listen() The `listen(React\Socket\ServerInterface $socket): void` method can be used to @@ -868,10 +874,10 @@ messages. In its most common form, you can attach this to a order to start a plaintext HTTP server like this: ```php -$server = new React\Http\Server($handler); +$http = new React\Http\HttpServer($handler); $socket = new React\Socket\Server('0.0.0.0:8080'); -$server->listen($socket); +$http->listen($socket); ``` See also [hello world server example](examples/51-server-hello-world.php) @@ -894,12 +900,12 @@ using a secure TLS listen address, a certificate file and optional `passphrase` like this: ```php -$server = new React\Http\Server($handler); +$http = new React\Http\HttpServer($handler); $socket = new React\Socket\Server('tls://0.0.0.0:8443', null, array( 'local_cert' => __DIR__ . '/localhost.pem' )); -$server->listen($socket); +$http->listen($socket); ``` See also [hello world HTTPS example](examples/61-server-hello-world-https.php) @@ -907,7 +913,7 @@ for more details. ### Server Request -As seen above, the [`Server`](#server) class is responsible for handling +As seen above, the [`HttpServer`](#httpserver) class is responsible for handling incoming connections and then processing each incoming HTTP request. The request object will be processed once the request has @@ -919,7 +925,7 @@ which in turn extends the and will be passed to the callback function like this. ```php -$server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterface $request) { +$http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { $body = "The method of the request is: " . $request->getMethod(); $body .= "The requested path is: " . $request->getUri()->getPath(); @@ -962,7 +968,7 @@ The following parameters are currently available: Set to 'on' if the request used HTTPS, otherwise it won't be set ```php -$server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterface $request) { +$http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { $body = "Your IP is: " . $request->getServerParams()['REMOTE_ADDR']; return new React\Http\Message\Response( @@ -987,7 +993,7 @@ The `getQueryParams(): array` method can be used to get the query parameters similiar to the `$_GET` variable. ```php -$server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterface $request) { +$http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { $queryParams = $request->getQueryParams(); $body = 'The query parameter "foo" is not set. Click the following link '; @@ -1017,7 +1023,7 @@ See also [server query parameters example](examples/54-server-query-parameter.ph #### Request body -By default, the [`Server`](#server) will buffer and parse the full request body +By default, the [`Server`](#httpserver) will buffer and parse the full request body in memory. This means the given request object includes the parsed request body and any file uploads. @@ -1041,7 +1047,7 @@ By default, this method will only return parsed data for requests using request headers (commonly used for `POST` requests for HTML form submission data). ```php -$server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterface $request) { +$http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { $name = $request->getParsedBody()['name'] ?? 'anonymous'; return new React\Http\Message\Response( @@ -1065,7 +1071,7 @@ an XML (`Content-Type: application/xml`) request body (which is commonly used fo `POST`, `PUT` or `PATCH` requests in JSON-based or RESTful/RESTish APIs). ```php -$server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterface $request) { +$http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { $data = json_decode((string)$request->getBody()); $name = $data->name ?? 'anonymous'; @@ -1088,7 +1094,7 @@ This array will only be filled when using the `Content-Type: multipart/form-data request header (commonly used for `POST` requests for HTML file uploads). ```php -$server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterface $request) { +$http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { $files = $request->getUploadedFiles(); $name = isset($files['avatar']) ? $files['avatar']->getClientFilename() : 'nothing'; @@ -1112,7 +1118,7 @@ This method operates on the buffered request body, i.e. the request body size is always known, even when the request does not specify a `Content-Length` request header or when using `Transfer-Encoding: chunked` for HTTP/1.1 requests. -> Note: The `Server` automatically takes care of handling requests with the +> Note: The `HttpServer` automatically takes care of handling requests with the additional `Expect: 100-continue` request header. When HTTP/1.1 clients want to send a bigger request body, they MAY send only the request headers with an additional `Expect: 100-continue` request header and wait before sending the actual @@ -1161,7 +1167,7 @@ The [ReactPHP `ReadableStreamInterface`](https://github.com/reactphp/stream#read gives you access to the incoming request body as the individual chunks arrive: ```php -$server = new React\Http\Server( +$http = new React\Http\HttpServer( new React\Http\Middleware\StreamingRequestMiddleware(), function (Psr\Http\Message\ServerRequestInterface $request) { $body = $request->getBody(); @@ -1234,7 +1240,7 @@ This method operates on the streaming request body, i.e. the request body size may be unknown (`null`) when using `Transfer-Encoding: chunked` for HTTP/1.1 requests. ```php -$server = new React\Http\Server( +$http = new React\Http\HttpServer( new React\Http\Middleware\StreamingRequestMiddleware(), function (Psr\Http\Message\ServerRequestInterface $request) { $size = $request->getBody()->getSize(); @@ -1262,7 +1268,7 @@ $server = new React\Http\Server( ); ``` -> Note: The `Server` automatically takes care of handling requests with the +> Note: The `HttpServer` automatically takes care of handling requests with the additional `Expect: 100-continue` request header. When HTTP/1.1 clients want to send a bigger request body, they MAY send only the request headers with an additional `Expect: 100-continue` request header and wait before sending the actual @@ -1307,7 +1313,7 @@ The `getCookieParams(): string[]` method can be used to get all cookies sent with the current request. ```php -$server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterface $request) { +$http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { $key = 'react\php'; if (isset($request->getCookieParams()[$key])) { @@ -1344,7 +1350,7 @@ See also [cookie server example](examples/55-server-cookie-handling.php) for mor #### Invalid request -The `Server` class supports both HTTP/1.1 and HTTP/1.0 request messages. +The `HttpServer` class supports both HTTP/1.1 and HTTP/1.0 request messages. If a client sends an invalid request message, uses an invalid HTTP protocol version or sends an invalid `Transfer-Encoding` request header value, the server will automatically send a `400` (Bad Request) HTTP error response @@ -1353,7 +1359,7 @@ On top of this, it will emit an `error` event that can be used for logging purposes like this: ```php -$server->on('error', function (Exception $e) { +$http->on('error', function (Exception $e) { echo 'Error: ' . $e->getMessage() . PHP_EOL; }); ``` @@ -1364,7 +1370,7 @@ valid response object from your request handler function. See also ### Server Response -The callback function passed to the constructor of the [`Server`](#server) is +The callback function passed to the constructor of the [`HttpServer`](#httpserver) is responsible for processing the request and returning a response, which will be delivered to the client. @@ -1379,7 +1385,7 @@ This projects ships a [`Response` class](#response) which implements the In its most simple form, you can use it like this: ```php -$server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterface $request) { +$http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { return new React\Http\Message\Response( 200, array( @@ -1407,7 +1413,7 @@ To prevent this you SHOULD use a This example shows how such a long-term action could look like: ```php -$server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterface $request) { +$http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { return new Promise(function ($resolve, $reject) { Loop::addTimer(1.5, function() use ($resolve) { $response = new React\Http\Message\Response( @@ -1445,7 +1451,7 @@ Note that other implementations of the may only support strings. ```php -$server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterface $request) { +$http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { $stream = new ThroughStream(); $timer = Loop::addPeriodicTimer(0.5, function () use ($stream) { @@ -1537,7 +1543,7 @@ added automatically. This is the most common use case, for example when using a `string` response body like this: ```php -$server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterface $request) { +$http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { return new React\Http\Message\Response( 200, array( @@ -1556,7 +1562,7 @@ response messages will contain the plain response body. If you know the length of your streaming response body, you MAY want to specify it explicitly like this: ```php -$server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterface $request) { +$http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { $stream = new ThroughStream(); Loop::addTimer(2.0, function () use ($stream) { @@ -1600,7 +1606,7 @@ On top of this, it will emit an `error` event that can be used for logging purposes like this: ```php -$server->on('error', function (Exception $e) { +$http->on('error', function (Exception $e) { echo 'Error: ' . $e->getMessage() . PHP_EOL; if ($e->getPrevious() !== null) { echo 'Previous: ' . $e->getPrevious()->getMessage() . PHP_EOL; @@ -1626,13 +1632,13 @@ create your own HTTP response message instead. #### Default response headers When a response is returned from the request handler function, it will be -processed by the [`Server`](#server) and then sent back to the client. +processed by the [`HttpServer`](#httpserver) and then sent back to the client. A `Server: ReactPHP/1` response header will be added automatically. You can add a custom `Server` response header like this: ```php -$server = new React\Http\Server(function (ServerRequestInterface $request) { +$http = new React\Http\HttpServer(function (ServerRequestInterface $request) { return new React\Http\Message\Response( 200, array( @@ -1647,7 +1653,7 @@ don't want to expose the underlying server software), you can use an empty string value like this: ```php -$server = new React\Http\Server(function (ServerRequestInterface $request) { +$http = new React\Http\HttpServer(function (ServerRequestInterface $request) { return new React\Http\Message\Response( 200, array( @@ -1662,7 +1668,7 @@ date and time if none is given. You can add a custom `Date` response header like this: ```php -$server = new React\Http\Server(function (ServerRequestInterface $request) { +$http = new React\Http\HttpServer(function (ServerRequestInterface $request) { return new React\Http\Message\Response( 200, array( @@ -1677,7 +1683,7 @@ don't have an appropriate clock to rely on), you can use an empty string value like this: ```php -$server = new React\Http\Server(function (ServerRequestInterface $request) { +$http = new React\Http\HttpServer(function (ServerRequestInterface $request) { return new React\Http\Message\Response( 200, array( @@ -1687,7 +1693,7 @@ $server = new React\Http\Server(function (ServerRequestInterface $request) { }); ``` -The `Server` class will automatically add the protocol version of the request, +The `HttpServer` class will automatically add the protocol version of the request, so you don't have to. For instance, if the client sends the request using the HTTP/1.1 protocol version, the response message will also use the same protocol version, no matter what version is returned from the request handler function. @@ -1701,7 +1707,7 @@ choice with a `Connection: close` response header. ### Middleware -As documented above, the [`Server`](#server) accepts a single request handler +As documented above, the [`HttpServer`](#httpserver) accepts a single request handler argument that is responsible for processing an incoming HTTP request and then creating and returning an outgoing HTTP response. @@ -1755,12 +1761,12 @@ required to match PHP's request behavior (see below) and otherwise actively encourages [Third-Party Middleware](#third-party-middleware) implementations. In order to use middleware request handlers, simply pass an array with all -callables as defined above to the [`Server`](#server). +callables as defined above to the [`HttpServer`](#httpserver). The following example adds a middleware request handler that adds the current time to the request as a header (`Request-Time`) and a final request handler that always returns a 200 code without a body: ```php -$server = new React\Http\Server( +$http = new React\Http\HttpServer( function (Psr\Http\Message\ServerRequestInterface $request, callable $next) { $request = $request->withHeader('Request-Time', time()); return $next($request); @@ -1784,7 +1790,7 @@ In order to simplify handling both paths, you can simply wrap this in a [`Promise\resolve()`](https://reactphp.org/promise/#resolve) call like this: ```php -$server = new React\Http\Server( +$http = new React\Http\HttpServer( function (Psr\Http\Message\ServerRequestInterface $request, callable $next) { $promise = React\Promise\resolve($next($request)); return $promise->then(function (ResponseInterface $response) { @@ -1800,13 +1806,13 @@ $server = new React\Http\Server( Note that the `$next` middleware request handler may also throw an `Exception` (or return a rejected promise) as described above. The previous example does not catch any exceptions and would thus signal an -error condition to the `Server`. +error condition to the `HttpServer`. Alternatively, you can also catch any `Exception` to implement custom error handling logic (or logging etc.) by wrapping this in a [`Promise`](https://reactphp.org/promise/#promise) like this: ```php -$server = new React\Http\Server( +$http = new React\Http\HttpServer( function (Psr\Http\Message\ServerRequestInterface $request, callable $next) { $promise = new React\Promise\Promise(function ($resolve) use ($next, $request) { $resolve($next($request)); @@ -2439,7 +2445,7 @@ body in memory. Instead, it will represent the request body as a that emit chunks of incoming data as it is received: ```php -$server = new React\Http\Server(array( +$http = new React\Http\HttpServer( new React\Http\Middleware\StreamingRequestMiddleware(), function (Psr\Http\Message\ServerRequestInterface $request) { $body = $request->getBody(); @@ -2460,7 +2466,7 @@ $server = new React\Http\Server(array( }); }); } -)); +); ``` See also [streaming incoming request](#streaming-incoming-request) @@ -2473,17 +2479,17 @@ to explicitly configure the total number of requests that can be handled at once: ```php -$server = new React\Http\Server(array( +$http = new React\Http\HttpServer( new React\Http\Middleware\StreamingRequestMiddleware(), new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers new React\Http\Middleware\RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request new React\Http\Middleware\RequestBodyParserMiddleware(), $handler -)); +); ``` > Internally, this class is used as a "marker" to not trigger the default - request buffering behavior in the `Server`. It does not implement any logic + request buffering behavior in the `HttpServer`. It does not implement any logic on its own. #### LimitConcurrentRequestsMiddleware @@ -2506,7 +2512,7 @@ The following example shows how this middleware can be used to ensure no more than 10 handlers will be invoked at once: ```php -$server = new React\Http\Server( +$http = new React\Http\HttpServer( new React\Http\Middleware\LimitConcurrentRequestsMiddleware(10), $handler ); @@ -2517,7 +2523,7 @@ Similarly, this middleware is often used in combination with the to limit the total number of requests that can be buffered at once: ```php -$server = new React\Http\Server( +$http = new React\Http\HttpServer( new React\Http\Middleware\StreamingRequestMiddleware(), new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers new React\Http\Middleware\RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request @@ -2531,7 +2537,7 @@ that can be buffered at once and then ensure the actual request handler only processes one request after another without any concurrency: ```php -$server = new React\Http\Server( +$http = new React\Http\HttpServer( new React\Http\Middleware\StreamingRequestMiddleware(), new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers new React\Http\Middleware\RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request @@ -2584,7 +2590,7 @@ the total number of concurrent requests. Usage: ```php -$server = new React\Http\Server( +$http = new React\Http\HttpServer( new React\Http\Middleware\StreamingRequestMiddleware(), new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers new React\Http\Middleware\RequestBodyBufferMiddleware(16 * 1024 * 1024), // 16 MiB @@ -2645,7 +2651,7 @@ $handler = function (Psr\Http\Message\ServerRequestInterface $request) { ); }; -$server = new React\Http\Server( +$http = new React\Http\HttpServer( new React\Http\Middleware\StreamingRequestMiddleware(), new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers new React\Http\Middleware\RequestBodyBufferMiddleware(16 * 1024 * 1024), // 16 MiB diff --git a/examples/51-server-hello-world.php b/examples/51-server-hello-world.php index 2cfd5649..50c82396 100644 --- a/examples/51-server-hello-world.php +++ b/examples/51-server-hello-world.php @@ -2,11 +2,10 @@ use Psr\Http\Message\ServerRequestInterface; use React\Http\Message\Response; -use React\Http\Server; require __DIR__ . '/../vendor/autoload.php'; -$server = new Server(function (ServerRequestInterface $request) { +$http = new React\Http\HttpServer(function (ServerRequestInterface $request) { return new Response( 200, array( @@ -16,7 +15,7 @@ ); }); -$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); -$server->listen($socket); +$socket = new React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/52-server-count-visitors.php b/examples/52-server-count-visitors.php index ae173bbd..1884acb0 100644 --- a/examples/52-server-count-visitors.php +++ b/examples/52-server-count-visitors.php @@ -2,12 +2,11 @@ use Psr\Http\Message\ServerRequestInterface; use React\Http\Message\Response; -use React\Http\Server; require __DIR__ . '/../vendor/autoload.php'; $counter = 0; -$server = new Server(function (ServerRequestInterface $request) use (&$counter) { +$http = new React\Http\HttpServer(function (ServerRequestInterface $request) use (&$counter) { return new Response( 200, array( @@ -17,7 +16,7 @@ ); }); -$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); -$server->listen($socket); +$socket = new React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/53-server-whatsmyip.php b/examples/53-server-whatsmyip.php index 82f34742..89dad25c 100644 --- a/examples/53-server-whatsmyip.php +++ b/examples/53-server-whatsmyip.php @@ -2,11 +2,10 @@ use Psr\Http\Message\ServerRequestInterface; use React\Http\Message\Response; -use React\Http\Server; require __DIR__ . '/../vendor/autoload.php'; -$server = new Server(function (ServerRequestInterface $request) { +$http = new React\Http\HttpServer(function (ServerRequestInterface $request) { $body = "Your IP is: " . $request->getServerParams()['REMOTE_ADDR']; return new Response( @@ -18,7 +17,7 @@ ); }); -$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); -$server->listen($socket); +$socket = new React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/54-server-query-parameter.php b/examples/54-server-query-parameter.php index dbddfbc0..2ab16647 100644 --- a/examples/54-server-query-parameter.php +++ b/examples/54-server-query-parameter.php @@ -2,11 +2,10 @@ use Psr\Http\Message\ServerRequestInterface; use React\Http\Message\Response; -use React\Http\Server; require __DIR__ . '/../vendor/autoload.php'; -$server = new Server(function (ServerRequestInterface $request) { +$http = new React\Http\HttpServer(function (ServerRequestInterface $request) { $queryParams = $request->getQueryParams(); $body = 'The query parameter "foo" is not set. Click the following link '; @@ -25,7 +24,7 @@ ); }); -$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); -$server->listen($socket); +$socket = new React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/55-server-cookie-handling.php b/examples/55-server-cookie-handling.php index 55370c3b..ff89fe1e 100644 --- a/examples/55-server-cookie-handling.php +++ b/examples/55-server-cookie-handling.php @@ -2,11 +2,10 @@ use Psr\Http\Message\ServerRequestInterface; use React\Http\Message\Response; -use React\Http\Server; require __DIR__ . '/../vendor/autoload.php'; -$server = new Server(function (ServerRequestInterface $request) { +$http = new React\Http\HttpServer(function (ServerRequestInterface $request) { $key = 'react\php'; if (isset($request->getCookieParams()[$key])) { @@ -31,7 +30,7 @@ ); }); -$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); -$server->listen($socket); +$socket = new React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/56-server-sleep.php b/examples/56-server-sleep.php index 9f149c4c..bd2ea694 100644 --- a/examples/56-server-sleep.php +++ b/examples/56-server-sleep.php @@ -3,12 +3,11 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Loop; use React\Http\Message\Response; -use React\Http\Server; use React\Promise\Promise; require __DIR__ . '/../vendor/autoload.php'; -$server = new Server(function (ServerRequestInterface $request) { +$http = new React\Http\HttpServer(function (ServerRequestInterface $request) { return new Promise(function ($resolve, $reject) { Loop::addTimer(1.5, function() use ($resolve) { $response = new Response( @@ -23,7 +22,7 @@ }); }); -$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); -$server->listen($socket); +$socket = new React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/57-server-error-handling.php b/examples/57-server-error-handling.php index 6952a559..f5281c53 100644 --- a/examples/57-server-error-handling.php +++ b/examples/57-server-error-handling.php @@ -2,13 +2,12 @@ use Psr\Http\Message\ServerRequestInterface; use React\Http\Message\Response; -use React\Http\Server; use React\Promise\Promise; require __DIR__ . '/../vendor/autoload.php'; $count = 0; -$server = new Server(function (ServerRequestInterface $request) use (&$count) { +$http = new React\Http\HttpServer(function (ServerRequestInterface $request) use (&$count) { return new Promise(function ($resolve, $reject) use (&$count) { $count++; @@ -28,7 +27,7 @@ }); }); -$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); -$server->listen($socket); +$socket = new React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/58-server-stream-response.php b/examples/58-server-stream-response.php index 941ded1f..d965e306 100644 --- a/examples/58-server-stream-response.php +++ b/examples/58-server-stream-response.php @@ -3,12 +3,11 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Loop; use React\Http\Message\Response; -use React\Http\Server; use React\Stream\ThroughStream; require __DIR__ . '/../vendor/autoload.php'; -$server = new Server(function (ServerRequestInterface $request) { +$http = new React\Http\HttpServer(function (ServerRequestInterface $request) { if ($request->getMethod() !== 'GET' || $request->getUri()->getPath() !== '/') { return new Response(404); } @@ -39,7 +38,7 @@ ); }); -$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); -$server->listen($socket); +$socket = new React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/59-server-json-api.php b/examples/59-server-json-api.php index c1b4d305..c06a9702 100644 --- a/examples/59-server-json-api.php +++ b/examples/59-server-json-api.php @@ -8,11 +8,10 @@ use Psr\Http\Message\ServerRequestInterface; use React\Http\Message\Response; -use React\Http\Server; require __DIR__ . '/../vendor/autoload.php'; -$server = new Server(function (ServerRequestInterface $request) { +$http = new React\Http\HttpServer(function (ServerRequestInterface $request) { if ($request->getHeaderLine('Content-Type') !== 'application/json') { return new Response( 415, // Unsupported Media Type @@ -53,7 +52,7 @@ ); }); -$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); -$server->listen($socket); +$socket = new React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/61-server-hello-world-https.php b/examples/61-server-hello-world-https.php index 5b671618..34f1a8bd 100644 --- a/examples/61-server-hello-world-https.php +++ b/examples/61-server-hello-world-https.php @@ -2,11 +2,10 @@ use Psr\Http\Message\ServerRequestInterface; use React\Http\Message\Response; -use React\Http\Server; require __DIR__ . '/../vendor/autoload.php'; -$server = new Server(function (ServerRequestInterface $request) { +$http = new React\Http\HttpServer(function (ServerRequestInterface $request) { return new Response( 200, array( @@ -16,11 +15,11 @@ ); }); -$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); -$socket = new \React\Socket\SecureServer($socket, null, array( +$socket = new React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$socket = new React\Socket\SecureServer($socket, null, array( 'local_cert' => isset($argv[2]) ? $argv[2] : __DIR__ . '/localhost.pem' )); -$server->listen($socket); +$http->listen($socket); //$socket->on('error', 'printf'); diff --git a/examples/62-server-form-upload.php b/examples/62-server-form-upload.php index b1f0d8ee..5a0fdace 100644 --- a/examples/62-server-form-upload.php +++ b/examples/62-server-form-upload.php @@ -14,7 +14,6 @@ use React\Http\Middleware\RequestBodyBufferMiddleware; use React\Http\Middleware\RequestBodyParserMiddleware; use React\Http\Middleware\StreamingRequestMiddleware; -use React\Http\Server; require __DIR__ . '/../vendor/autoload.php'; @@ -121,7 +120,7 @@ // Note how this example explicitly uses the advanced `StreamingRequestMiddleware` to apply // custom request buffering limits below before running our request handler. -$server = new Server( +$http = new React\Http\HttpServer( new StreamingRequestMiddleware(), new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers, queue otherwise new RequestBodyBufferMiddleware(8 * 1024 * 1024), // 8 MiB max, ignore body otherwise @@ -129,7 +128,7 @@ $handler ); -$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', null); -$server->listen($socket); +$socket = new React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/63-server-streaming-request.php b/examples/63-server-streaming-request.php index c2416e15..693cf2c0 100644 --- a/examples/63-server-streaming-request.php +++ b/examples/63-server-streaming-request.php @@ -5,7 +5,7 @@ // Note how this example uses the advanced `StreamingRequestMiddleware` to allow streaming // the incoming HTTP request. This very simple example merely counts the size // of the streaming body, it does not otherwise buffer its contents in memory. -$server = new React\Http\Server( +$http = new React\Http\HttpServer( new React\Http\Middleware\StreamingRequestMiddleware(), function (Psr\Http\Message\ServerRequestInterface $request) { $body = $request->getBody(); @@ -42,9 +42,9 @@ function (Psr\Http\Message\ServerRequestInterface $request) { } ); -$server->on('error', 'printf'); +$http->on('error', 'printf'); -$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); -$server->listen($socket); +$socket = new React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/71-server-http-proxy.php b/examples/71-server-http-proxy.php index 95c2f411..f6ab5ff5 100644 --- a/examples/71-server-http-proxy.php +++ b/examples/71-server-http-proxy.php @@ -4,18 +4,16 @@ // $ curl -v --proxy http://localhost:8080 http://reactphp.org/ use Psr\Http\Message\RequestInterface; -use React\EventLoop\Factory; use React\Http\Message\Response; -use React\Http\Server; use RingCentral\Psr7; require __DIR__ . '/../vendor/autoload.php'; -// Note how this example uses the `Server` without the `StreamingRequestMiddleware`. +// Note how this example uses the `HttpServer` without the `StreamingRequestMiddleware`. // This means that this proxy buffers the whole request before "processing" it. // As such, this is store-and-forward proxy. This could also use the advanced // `StreamingRequestMiddleware` to forward the incoming request as it comes in. -$server = new Server(function (RequestInterface $request) { +$http = new React\Http\HttpServer(function (RequestInterface $request) { if (strpos($request->getRequestTarget(), '://') === false) { return new Response( 400, @@ -46,7 +44,7 @@ ); }); -$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); -$server->listen($socket); +$socket = new React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/72-server-http-connect-proxy.php b/examples/72-server-http-connect-proxy.php index a1f33983..4399614b 100644 --- a/examples/72-server-http-connect-proxy.php +++ b/examples/72-server-http-connect-proxy.php @@ -5,7 +5,6 @@ use Psr\Http\Message\ServerRequestInterface; use React\Http\Message\Response; -use React\Http\Server; use React\Socket\Connector; use React\Socket\ConnectionInterface; @@ -13,11 +12,11 @@ $connector = new Connector(); -// Note how this example uses the `Server` without the `StreamingRequestMiddleware`. +// Note how this example uses the `HttpServer` without the `StreamingRequestMiddleware`. // Unlike the plain HTTP proxy, the CONNECT method does not contain a body // and we establish an end-to-end connection over the stream object, so this // doesn't have to store any payload data in memory at all. -$server = new Server(function (ServerRequestInterface $request) use ($connector) { +$http = new React\Http\HttpServer(function (ServerRequestInterface $request) use ($connector) { if ($request->getMethod() !== 'CONNECT') { return new Response( 405, @@ -51,7 +50,7 @@ function ($e) { ); }); -$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); -$server->listen($socket); +$socket = new React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/81-server-upgrade-echo.php b/examples/81-server-upgrade-echo.php index 6a20181a..cb5d0b0d 100644 --- a/examples/81-server-upgrade-echo.php +++ b/examples/81-server-upgrade-echo.php @@ -20,15 +20,14 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Loop; use React\Http\Message\Response; -use React\Http\Server; use React\Stream\ThroughStream; require __DIR__ . '/../vendor/autoload.php'; -// Note how this example uses the `Server` without the `StreamingRequestMiddleware`. +// Note how this example uses the `HttpServer` without the `StreamingRequestMiddleware`. // The initial incoming request does not contain a body and we upgrade to a // stream object below. -$server = new Server(function (ServerRequestInterface $request) { +$http = new React\Http\HttpServer(function (ServerRequestInterface $request) { if ($request->getHeaderLine('Upgrade') !== 'echo' || $request->getProtocolVersion() === '1.0') { return new Response( 426, @@ -57,7 +56,7 @@ ); }); -$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); -$server->listen($socket); +$socket = new React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/82-server-upgrade-chat.php b/examples/82-server-upgrade-chat.php index 2c6f08a8..22fed927 100644 --- a/examples/82-server-upgrade-chat.php +++ b/examples/82-server-upgrade-chat.php @@ -22,7 +22,6 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Loop; use React\Http\Message\Response; -use React\Http\Server; use React\Stream\CompositeStream; use React\Stream\ThroughStream; @@ -33,10 +32,10 @@ // this means that any Upgraded data will simply be sent back to the client $chat = new ThroughStream(); -// Note how this example uses the `Server` without the `StreamingRequestMiddleware`. +// Note how this example uses the `HttpServer` without the `StreamingRequestMiddleware`. // The initial incoming request does not contain a body and we upgrade to a // stream object below. -$server = new Server(function (ServerRequestInterface $request) use ($chat) { +$http = new React\Http\HttpServer(function (ServerRequestInterface $request) use ($chat) { if ($request->getHeaderLine('Upgrade') !== 'chat' || $request->getProtocolVersion() === '1.0') { return new Response( 426, @@ -85,7 +84,7 @@ ); }); -$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); -$server->listen($socket); +$socket = new React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/99-server-benchmark-download.php b/examples/99-server-benchmark-download.php index a6b4e9c1..bdc6735a 100644 --- a/examples/99-server-benchmark-download.php +++ b/examples/99-server-benchmark-download.php @@ -17,7 +17,6 @@ use Evenement\EventEmitter; use Psr\Http\Message\ServerRequestInterface; use React\Http\Message\Response; -use React\Http\Server; use React\Stream\ReadableStreamInterface; use React\Stream\WritableStreamInterface; @@ -91,7 +90,7 @@ public function getSize() } } -$server = new Server(function (ServerRequestInterface $request) { +$http = new React\Http\HttpServer(function (ServerRequestInterface $request) { switch ($request->getUri()->getPath()) { case '/': return new Response( @@ -123,7 +122,7 @@ public function getSize() ); }); -$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); -$server->listen($socket); +$socket = new React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/src/Server.php b/src/HttpServer.php similarity index 93% rename from src/Server.php rename to src/HttpServer.php index c2e3f5a6..582ea380 100644 --- a/src/Server.php +++ b/src/HttpServer.php @@ -15,7 +15,7 @@ use React\Socket\ServerInterface; /** - * The `React\Http\Server` class is responsible for handling incoming connections and then + * The `React\Http\HttpServer` class is responsible for handling incoming connections and then * processing each incoming HTTP request. * * When a complete HTTP request has been received, it will invoke the given @@ -24,7 +24,7 @@ * object and expects a [response](#server-response) object in return: * * ```php - * $server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterface $request) { + * $http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { * return new React\Http\Message\Response( * 200, * array( @@ -49,7 +49,7 @@ * This value SHOULD NOT be given unless you're sure you want to explicitly use a * given event loop instance. * - * In order to start listening for any incoming connections, the `Server` needs + * In order to start listening for any incoming connections, the `HttpServer` needs * to be attached to an instance of * [`React\Socket\ServerInterface`](https://github.com/reactphp/socket#serverinterface) * through the [`listen()`](#listen) method as described in the following @@ -58,17 +58,17 @@ * to start a plaintext HTTP server like this: * * ```php - * $server = new React\Http\Server($handler); + * $http = new React\Http\HttpServer($handler); * * $socket = new React\Socket\Server('0.0.0.0:8080'); - * $server->listen($socket); + * $http->listen($socket); * ``` * * See also the [`listen()`](#listen) method and * [hello world server example](../examples/51-server-hello-world.php) * for more details. * - * By default, the `Server` buffers and parses the complete incoming HTTP + * By default, the `HttpServer` buffers and parses the complete incoming HTTP * request in memory. It will invoke the given request handler function when the * complete request headers and request body has been received. This means the * [request](#server-request) object passed to your request handler function will be @@ -121,14 +121,14 @@ * or explicitly limit concurrency. * * In order to override the above buffering defaults, you can configure the - * `Server` explicitly. You can use the + * `HttpServer` explicitly. You can use the * [`LimitConcurrentRequestsMiddleware`](#limitconcurrentrequestsmiddleware) and * [`RequestBodyBufferMiddleware`](#requestbodybuffermiddleware) (see below) * to explicitly configure the total number of requests that can be handled at * once like this: * * ```php - * $server = new React\Http\Server( + * $http = new React\Http\HttpServer( * new React\Http\Middleware\StreamingRequestMiddleware(), * new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers * new React\Http\Middleware\RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request @@ -153,7 +153,7 @@ * in memory: * * ```php - * $server = new React\Http\Server( + * $http = new React\Http\HttpServer( * new React\Http\Middleware\StreamingRequestMiddleware(), * $handler * ); @@ -167,8 +167,12 @@ * have full control over consuming the incoming HTTP request body and * concurrency settings. See also [streaming incoming request](#streaming-incoming-request) * below for more details. + * + * > Changelog v1.5.0: This class has been renamed to `HttpServer` from the + * previous `Server` class in order to avoid any ambiguities. + * The previous name has been deprecated and should not be used anymore. */ -final class Server extends EventEmitter +final class HttpServer extends EventEmitter { /** * The maximum buffer size used for each request. @@ -263,10 +267,10 @@ public function __construct($requestHandlerOrLoop) * order to start a plaintext HTTP server like this: * * ```php - * $server = new React\Http\Server($handler); + * $http = new React\Http\HttpServer($handler); * * $socket = new React\Socket\Server(8080); - * $server->listen($socket); + * $http->listen($socket); * ``` * * See also [hello world server example](../examples/51-server-hello-world.php) @@ -289,12 +293,12 @@ public function __construct($requestHandlerOrLoop) * `passphrase` like this: * * ```php - * $server = new React\Http\Server($handler); + * $http = new React\Http\HttpServer($handler); * * $socket = new React\Socket\Server('tls://0.0.0.0:8443', null, array( * 'local_cert' => __DIR__ . '/localhost.pem' * )); - * $server->listen($socket); + * $http->listen($socket); * ``` * * See also [hello world HTTPS example](../examples/61-server-hello-world-https.php) diff --git a/src/Io/StreamingServer.php b/src/Io/StreamingServer.php index e20ddf48..95e0e4c6 100644 --- a/src/Io/StreamingServer.php +++ b/src/Io/StreamingServer.php @@ -20,7 +20,7 @@ * The internal `StreamingServer` class is responsible for handling incoming connections and then * processing each incoming HTTP request. * - * Unlike the [`Server`](#server) class, it does not buffer and parse the incoming + * Unlike the [`HttpServer`](#server) class, it does not buffer and parse the incoming * HTTP request body by default. This means that the request handler will be * invoked with a streaming request body. Once the request headers have been * received, it will invoke the request handler function. This request handler @@ -63,7 +63,7 @@ * See also the [`listen()`](#listen) method and the [first example](examples) for more details. * * The `StreamingServer` class is considered advanced usage and unless you know - * what you're doing, you're recommended to use the [`Server`](#server) class + * what you're doing, you're recommended to use the [`HttpServer`](#httpserver) class * instead. The `StreamingServer` class is specifically designed to help with * more advanced use cases where you want to have full control over consuming * the incoming HTTP request body and concurrency settings. @@ -75,7 +75,7 @@ * handler function may not be fully compatible with PSR-7. See also * [streaming request](#streaming-request) below for more details. * - * @see \React\Http\Server + * @see \React\Http\HttpServer * @see \React\Http\Message\Response * @see self::listen() * @internal @@ -130,7 +130,7 @@ public function __construct(LoopInterface $loop, $requestHandler) * Starts listening for HTTP requests on the given socket server instance * * @param ServerInterface $socket - * @see \React\Http\Server::listen() + * @see \React\Http\HttpServer::listen() */ public function listen(ServerInterface $socket) { diff --git a/src/Middleware/LimitConcurrentRequestsMiddleware.php b/src/Middleware/LimitConcurrentRequestsMiddleware.php index cc1dfc42..53338100 100644 --- a/src/Middleware/LimitConcurrentRequestsMiddleware.php +++ b/src/Middleware/LimitConcurrentRequestsMiddleware.php @@ -29,7 +29,7 @@ * than 10 handlers will be invoked at once: * * ```php - * $server = new React\Http\Server( + * $http = new React\Http\HttpServer( * new React\Http\Middleware\StreamingRequestMiddleware(), * new React\Http\Middleware\LimitConcurrentRequestsMiddleware(10), * $handler @@ -41,7 +41,7 @@ * to limit the total number of requests that can be buffered at once: * * ```php - * $server = new React\Http\Server( + * $http = new React\Http\HttpServer( * new React\Http\Middleware\StreamingRequestMiddleware(), * new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers * new React\Http\Middleware\RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request @@ -55,7 +55,7 @@ * processes one request after another without any concurrency: * * ```php - * $server = new React\Http\Server( + * $http = new React\Http\HttpServer( * new React\Http\Middleware\StreamingRequestMiddleware(), * new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers * new React\Http\Middleware\RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request diff --git a/src/Middleware/StreamingRequestMiddleware.php b/src/Middleware/StreamingRequestMiddleware.php index a68454f6..6ab74b71 100644 --- a/src/Middleware/StreamingRequestMiddleware.php +++ b/src/Middleware/StreamingRequestMiddleware.php @@ -13,7 +13,7 @@ * that emit chunks of incoming data as it is received: * * ```php - * $server = new React\Http\Server(array( + * $http = new React\Http\HttpServer( * new React\Http\Middleware\StreamingRequestMiddleware(), * function (Psr\Http\Message\ServerRequestInterface $request) { * $body = $request->getBody(); @@ -34,7 +34,7 @@ * }); * }); * } - * )); + * ); * ``` * * See also [streaming incoming request](../../README.md#streaming-incoming-request) @@ -47,17 +47,17 @@ * once: * * ```php - * $server = new React\Http\Server(array( + * $http = new React\Http\HttpServer( * new React\Http\Middleware\StreamingRequestMiddleware(), * new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers * new React\Http\Middleware\RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request * new React\Http\Middleware\RequestBodyParserMiddleware(), * $handler - * )); + * ); * ``` * * > Internally, this class is used as a "marker" to not trigger the default - * request buffering behavior in the `Server`. It does not implement any logic + * request buffering behavior in the `HttpServer`. It does not implement any logic * on its own. */ final class StreamingRequestMiddleware diff --git a/tests/FunctionalBrowserTest.php b/tests/FunctionalBrowserTest.php index f5b7f324..edb7f40b 100644 --- a/tests/FunctionalBrowserTest.php +++ b/tests/FunctionalBrowserTest.php @@ -7,10 +7,10 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Factory; use React\Http\Browser; +use React\Http\HttpServer; use React\Http\Message\ResponseException; use React\Http\Middleware\StreamingRequestMiddleware; use React\Http\Message\Response; -use React\Http\Server; use React\Promise\Promise; use React\Promise\Stream; use React\Socket\Connector; @@ -32,7 +32,7 @@ public function setUpBrowserAndServer() $this->loop = $loop = Factory::create(); $this->browser = new Browser($this->loop); - $server = new Server($this->loop, new StreamingRequestMiddleware(), function (ServerRequestInterface $request) use ($loop) { + $http = new HttpServer($this->loop, new StreamingRequestMiddleware(), function (ServerRequestInterface $request) use ($loop) { $path = $request->getUri()->getPath(); $headers = array(); @@ -142,7 +142,7 @@ public function setUpBrowserAndServer() var_dump($path); }); $socket = new \React\Socket\Server(0, $this->loop); - $server->listen($socket); + $http->listen($socket); $this->base = str_replace('tcp:', 'http:', $socket->getAddress()) . '/'; } @@ -573,11 +573,11 @@ public function testPostStreamKnownLength() */ public function testPostStreamWillStartSendingRequestEvenWhenBodyDoesNotEmitData() { - $server = new Server($this->loop, new StreamingRequestMiddleware(), function (ServerRequestInterface $request) { + $http = new HttpServer($this->loop, new StreamingRequestMiddleware(), function (ServerRequestInterface $request) { return new Response(200); }); $socket = new \React\Socket\Server(0, $this->loop); - $server->listen($socket); + $http->listen($socket); $this->base = str_replace('tcp:', 'http:', $socket->getAddress()) . '/'; @@ -600,7 +600,7 @@ public function testPostStreamClosed() public function testSendsHttp11ByDefault() { - $server = new Server($this->loop, function (ServerRequestInterface $request) { + $http = new HttpServer($this->loop, function (ServerRequestInterface $request) { return new Response( 200, array(), @@ -608,7 +608,7 @@ public function testSendsHttp11ByDefault() ); }); $socket = new \React\Socket\Server(0, $this->loop); - $server->listen($socket); + $http->listen($socket); $this->base = str_replace('tcp:', 'http:', $socket->getAddress()) . '/'; @@ -620,7 +620,7 @@ public function testSendsHttp11ByDefault() public function testSendsExplicitHttp10Request() { - $server = new Server($this->loop, function (ServerRequestInterface $request) { + $http = new HttpServer($this->loop, function (ServerRequestInterface $request) { return new Response( 200, array(), @@ -628,7 +628,7 @@ public function testSendsExplicitHttp10Request() ); }); $socket = new \React\Socket\Server(0, $this->loop); - $server->listen($socket); + $http->listen($socket); $this->base = str_replace('tcp:', 'http:', $socket->getAddress()) . '/'; diff --git a/tests/FunctionalServerTest.php b/tests/FunctionalHttpServerTest.php similarity index 90% rename from tests/FunctionalServerTest.php rename to tests/FunctionalHttpServerTest.php index 41cf31db..f7aa8392 100644 --- a/tests/FunctionalServerTest.php +++ b/tests/FunctionalHttpServerTest.php @@ -2,36 +2,36 @@ namespace React\Tests\Http; +use Clue\React\Block; +use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ServerRequestInterface; +use React\EventLoop\Factory; +use React\Http\HttpServer; +use React\Http\Message\Response; use React\Http\Middleware\LimitConcurrentRequestsMiddleware; use React\Http\Middleware\RequestBodyBufferMiddleware; -use React\Http\Message\Response; -use React\Http\Server; +use React\Http\Middleware\StreamingRequestMiddleware; use React\Socket\Server as Socket; -use React\EventLoop\Factory; -use Psr\Http\Message\RequestInterface; use React\Socket\Connector; use React\Socket\ConnectionInterface; -use Clue\React\Block; use React\Socket\SecureServer; use React\Promise; use React\Promise\Stream; use React\Stream\ThroughStream; -use React\Http\Middleware\StreamingRequestMiddleware; -class FunctionalServerTest extends TestCase +class FunctionalHttpServerTest extends TestCase { public function testPlainHttpOnRandomPort() { $loop = Factory::create(); $connector = new Connector($loop); - $server = new Server($loop, function (RequestInterface $request) { + $http = new HttpServer($loop, function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); $socket = new Socket(0, $loop); - $server->listen($socket); + $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\nHost: " . noScheme($conn->getRemoteAddress()) . "\r\n\r\n"); @@ -52,7 +52,7 @@ public function testPlainHttpOnRandomPortWithSingleRequestHandlerArray() $loop = Factory::create(); $connector = new Connector($loop); - $server = new Server( + $http = new HttpServer( $loop, function () { return new Response(404); @@ -60,7 +60,7 @@ function () { ); $socket = new Socket(0, $loop); - $server->listen($socket); + $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\nHost: " . noScheme($conn->getRemoteAddress()) . "\r\n\r\n"); @@ -80,12 +80,12 @@ public function testPlainHttpOnRandomPortWithoutHostHeaderUsesSocketUri() $loop = Factory::create(); $connector = new Connector($loop); - $server = new Server($loop, function (RequestInterface $request) { + $http = new HttpServer($loop, function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); $socket = new Socket(0, $loop); - $server->listen($socket); + $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\n\r\n"); @@ -106,12 +106,12 @@ public function testPlainHttpOnRandomPortWithOtherHostHeaderTakesPrecedence() $loop = Factory::create(); $connector = new Connector($loop); - $server = new Server($loop, function (RequestInterface $request) { + $http = new HttpServer($loop, function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); $socket = new Socket(0, $loop); - $server->listen($socket); + $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\nHost: localhost:1000\r\n\r\n"); @@ -138,7 +138,7 @@ public function testSecureHttpsOnRandomPort() 'tls' => array('verify_peer' => false) )); - $server = new Server($loop, function (RequestInterface $request) { + $http = new HttpServer($loop, function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); @@ -146,7 +146,7 @@ public function testSecureHttpsOnRandomPort() $socket = new SecureServer($socket, $loop, array( 'local_cert' => __DIR__ . '/../examples/localhost.pem' )); - $server->listen($socket); + $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\nHost: " . noScheme($conn->getRemoteAddress()) . "\r\n\r\n"); @@ -170,7 +170,7 @@ public function testSecureHttpsReturnsData() $loop = Factory::create(); - $server = new Server($loop, function (RequestInterface $request) { + $http = new HttpServer($loop, function (RequestInterface $request) { return new Response( 200, array(), @@ -182,7 +182,7 @@ public function testSecureHttpsReturnsData() $socket = new SecureServer($socket, $loop, array( 'local_cert' => __DIR__ . '/../examples/localhost.pem' )); - $server->listen($socket); + $http->listen($socket); $connector = new Connector($loop, array( 'tls' => array('verify_peer' => false) @@ -214,7 +214,7 @@ public function testSecureHttpsOnRandomPortWithoutHostHeaderUsesSocketUri() 'tls' => array('verify_peer' => false) )); - $server = new Server($loop, function (RequestInterface $request) { + $http = new HttpServer($loop, function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); @@ -222,7 +222,7 @@ public function testSecureHttpsOnRandomPortWithoutHostHeaderUsesSocketUri() $socket = new SecureServer($socket, $loop, array( 'local_cert' => __DIR__ . '/../examples/localhost.pem' )); - $server->listen($socket); + $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\n\r\n"); @@ -248,11 +248,11 @@ public function testPlainHttpOnStandardPortReturnsUriWithNoPort() } $connector = new Connector($loop); - $server = new Server($loop, function (RequestInterface $request) { + $http = new HttpServer($loop, function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); - $server->listen($socket); + $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n"); @@ -278,11 +278,11 @@ public function testPlainHttpOnStandardPortWithoutHostHeaderReturnsUriWithNoPort } $connector = new Connector($loop); - $server = new Server($loop, function (RequestInterface $request) { + $http = new HttpServer($loop, function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); - $server->listen($socket); + $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\n\r\n"); @@ -317,11 +317,11 @@ public function testSecureHttpsOnStandardPortReturnsUriWithNoPort() 'tls' => array('verify_peer' => false) )); - $server = new Server($loop, function (RequestInterface $request) { + $http = new HttpServer($loop, function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); - $server->listen($socket); + $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n"); @@ -356,11 +356,11 @@ public function testSecureHttpsOnStandardPortWithoutHostHeaderUsesSocketUri() 'tls' => array('verify_peer' => false) )); - $server = new Server($loop, function (RequestInterface $request) { + $http = new HttpServer($loop, function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); - $server->listen($socket); + $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\n\r\n"); @@ -386,11 +386,11 @@ public function testPlainHttpOnHttpsStandardPortReturnsUriWithPort() } $connector = new Connector($loop); - $server = new Server($loop, function (RequestInterface $request) { + $http = new HttpServer($loop, function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); - $server->listen($socket); + $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\nHost: " . noScheme($conn->getRemoteAddress()) . "\r\n\r\n"); @@ -425,11 +425,11 @@ public function testSecureHttpsOnHttpStandardPortReturnsUriWithPort() 'tls' => array('verify_peer' => false) )); - $server = new Server($loop, function (RequestInterface $request) { + $http = new HttpServer($loop, function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri() . 'x' . $request->getHeaderLine('Host')); }); - $server->listen($socket); + $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\nHost: " . noScheme($conn->getRemoteAddress()) . "\r\n\r\n"); @@ -453,12 +453,12 @@ public function testClosedStreamFromRequestHandlerWillSendEmptyBody() $stream = new ThroughStream(); $stream->close(); - $server = new Server($loop, function (RequestInterface $request) use ($stream) { + $http = new HttpServer($loop, function (RequestInterface $request) use ($stream) { return new Response(200, array(), $stream); }); $socket = new Socket(0, $loop); - $server->listen($socket); + $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\n\r\n"); @@ -480,7 +480,7 @@ public function testRequestHandlerWithStreamingRequestWillReceiveCloseEventIfCon $connector = new Connector($loop); $once = $this->expectCallableOnce(); - $server = new Server( + $http = new HttpServer( $loop, new StreamingRequestMiddleware(), function (RequestInterface $request) use ($once) { @@ -489,7 +489,7 @@ function (RequestInterface $request) use ($once) { ); $socket = new Socket(0, $loop); - $server->listen($socket); + $http->listen($socket); $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) use ($loop) { $conn->write("GET / HTTP/1.0\r\nContent-Length: 100\r\n\r\n"); @@ -511,7 +511,7 @@ public function testStreamFromRequestHandlerWillBeClosedIfConnectionClosesWhileS $stream = new ThroughStream(); - $server = new Server( + $http = new HttpServer( $loop, new StreamingRequestMiddleware(), function (RequestInterface $request) use ($stream) { @@ -520,7 +520,7 @@ function (RequestInterface $request) use ($stream) { ); $socket = new Socket(0, $loop); - $server->listen($socket); + $http->listen($socket); $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) use ($loop) { $conn->write("GET / HTTP/1.0\r\nContent-Length: 100\r\n\r\n"); @@ -545,12 +545,12 @@ public function testStreamFromRequestHandlerWillBeClosedIfConnectionCloses() $stream = new ThroughStream(); - $server = new Server($loop, function (RequestInterface $request) use ($stream) { + $http = new HttpServer($loop, function (RequestInterface $request) use ($stream) { return new Response(200, array(), $stream); }); $socket = new Socket(0, $loop); - $server->listen($socket); + $http->listen($socket); $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) use ($loop) { $conn->write("GET / HTTP/1.0\r\n\r\n"); @@ -573,7 +573,7 @@ public function testUpgradeWithThroughStreamReturnsDataAsGiven() $loop = Factory::create(); $connector = new Connector($loop); - $server = new Server($loop, function (RequestInterface $request) use ($loop) { + $http = new HttpServer($loop, function (RequestInterface $request) use ($loop) { $stream = new ThroughStream(); $loop->addTimer(0.1, function () use ($stream) { @@ -584,7 +584,7 @@ public function testUpgradeWithThroughStreamReturnsDataAsGiven() }); $socket = new Socket(0, $loop); - $server->listen($socket); + $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.1\r\nHost: example.com:80\r\nUpgrade: echo\r\n\r\n"); @@ -610,7 +610,7 @@ public function testUpgradeWithRequestBodyAndThroughStreamReturnsDataAsGiven() $loop = Factory::create(); $connector = new Connector($loop); - $server = new Server($loop, function (RequestInterface $request) use ($loop) { + $http = new HttpServer($loop, function (RequestInterface $request) use ($loop) { $stream = new ThroughStream(); $loop->addTimer(0.1, function () use ($stream) { @@ -621,7 +621,7 @@ public function testUpgradeWithRequestBodyAndThroughStreamReturnsDataAsGiven() }); $socket = new Socket(0, $loop); - $server->listen($socket); + $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("POST / HTTP/1.1\r\nHost: example.com:80\r\nUpgrade: echo\r\nContent-Length: 3\r\n\r\n"); @@ -648,7 +648,7 @@ public function testConnectWithThroughStreamReturnsDataAsGiven() $loop = Factory::create(); $connector = new Connector($loop); - $server = new Server($loop, function (RequestInterface $request) use ($loop) { + $http = new HttpServer($loop, function (RequestInterface $request) use ($loop) { $stream = new ThroughStream(); $loop->addTimer(0.1, function () use ($stream) { @@ -659,7 +659,7 @@ public function testConnectWithThroughStreamReturnsDataAsGiven() }); $socket = new Socket(0, $loop); - $server->listen($socket); + $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("CONNECT example.com:80 HTTP/1.1\r\nHost: example.com:80\r\nConnection: close\r\n\r\n"); @@ -685,7 +685,7 @@ public function testConnectWithThroughStreamReturnedFromPromiseReturnsDataAsGive $loop = Factory::create(); $connector = new Connector($loop); - $server = new Server($loop, function (RequestInterface $request) use ($loop) { + $http = new HttpServer($loop, function (RequestInterface $request) use ($loop) { $stream = new ThroughStream(); $loop->addTimer(0.1, function () use ($stream) { @@ -700,7 +700,7 @@ public function testConnectWithThroughStreamReturnedFromPromiseReturnsDataAsGive }); $socket = new Socket(0, $loop); - $server->listen($socket); + $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("CONNECT example.com:80 HTTP/1.1\r\nHost: example.com:80\r\nConnection: close\r\n\r\n"); @@ -726,7 +726,7 @@ public function testConnectWithClosedThroughStreamReturnsNoData() $loop = Factory::create(); $connector = new Connector($loop); - $server = new Server($loop, function (RequestInterface $request) { + $http = new HttpServer($loop, function (RequestInterface $request) { $stream = new ThroughStream(); $stream->close(); @@ -734,7 +734,7 @@ public function testConnectWithClosedThroughStreamReturnsNoData() }); $socket = new Socket(0, $loop); - $server->listen($socket); + $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("CONNECT example.com:80 HTTP/1.1\r\nHost: example.com:80\r\nConnection: close\r\n\r\n"); @@ -760,7 +760,7 @@ public function testLimitConcurrentRequestsMiddlewareRequestStreamPausing() $loop = Factory::create(); $connector = new Connector($loop); - $server = new Server( + $http = new HttpServer( $loop, new LimitConcurrentRequestsMiddleware(5), new RequestBodyBufferMiddleware(16 * 1024 * 1024), // 16 MiB @@ -777,7 +777,7 @@ function (ServerRequestInterface $request) { ); $socket = new Socket(0, $loop); - $server->listen($socket); + $http->listen($socket); $result = array(); for ($i = 0; $i < 6; $i++) { diff --git a/tests/ServerTest.php b/tests/HttpServerTest.php similarity index 81% rename from tests/ServerTest.php rename to tests/HttpServerTest.php index 6cb1b9b5..9a0a789a 100644 --- a/tests/ServerTest.php +++ b/tests/HttpServerTest.php @@ -2,17 +2,17 @@ namespace React\Tests\Http; -use React\EventLoop\Factory; -use React\Http\Server; -use Psr\Http\Message\ServerRequestInterface; -use React\Promise\Deferred; use Clue\React\Block; -use React\Promise; +use Psr\Http\Message\ServerRequestInterface; +use React\EventLoop\Factory; +use React\Http\HttpServer; +use React\Http\Io\IniUtil; use React\Http\Middleware\StreamingRequestMiddleware; +use React\Promise; +use React\Promise\Deferred; use React\Stream\ReadableStreamInterface; -use React\Http\Io\IniUtil; -final class ServerTest extends TestCase +final class HttpServerTest extends TestCase { private $connection; private $socket; @@ -48,11 +48,11 @@ public function setUpConnectionMockAndSocket() public function testConstructWithoutLoopAssignsLoopAutomatically() { - $server = new Server(function () { }); + $http = new HttpServer(function () { }); - $ref = new \ReflectionProperty($server, 'streamingServer'); + $ref = new \ReflectionProperty($http, 'streamingServer'); $ref->setAccessible(true); - $streamingServer = $ref->getValue($server); + $streamingServer = $ref->getValue($http); $ref = new \ReflectionProperty($streamingServer, 'loop'); $ref->setAccessible(true); @@ -64,17 +64,17 @@ public function testConstructWithoutLoopAssignsLoopAutomatically() public function testInvalidCallbackFunctionLeadsToException() { $this->setExpectedException('InvalidArgumentException'); - new Server(Factory::create(), 'invalid'); + new HttpServer('invalid'); } public function testSimpleRequestCallsRequestHandlerOnce() { $called = null; - $server = new Server(Factory::create(), function (ServerRequestInterface $request) use (&$called) { + $http = new HttpServer(function (ServerRequestInterface $request) use (&$called) { ++$called; }); - $server->listen($this->socket); + $http->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $this->connection->emit('data', array("GET / HTTP/1.0\r\n\r\n")); @@ -87,9 +87,9 @@ public function testSimpleRequestCallsRequestHandlerOnce() public function testSimpleRequestCallsArrayRequestHandlerOnce() { $this->called = null; - $server = new Server(Factory::create(), array($this, 'helperCallableOnce')); + $http = new HttpServer(array($this, 'helperCallableOnce')); - $server->listen($this->socket); + $http->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $this->connection->emit('data', array("GET / HTTP/1.0\r\n\r\n")); @@ -104,8 +104,7 @@ public function helperCallableOnce() public function testSimpleRequestWithMiddlewareArrayProcessesMiddlewareStack() { $called = null; - $server = new Server( - Factory::create(), + $http = new HttpServer( function (ServerRequestInterface $request, $next) use (&$called) { $called = 'before'; $ret = $next($request->withHeader('Demo', 'ok')); @@ -118,7 +117,7 @@ function (ServerRequestInterface $request) use (&$called) { } ); - $server->listen($this->socket); + $http->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $this->connection->emit('data', array("GET / HTTP/1.0\r\n\r\n")); @@ -129,11 +128,11 @@ public function testPostFormData() { $loop = Factory::create(); $deferred = new Deferred(); - $server = new Server($loop, function (ServerRequestInterface $request) use ($deferred) { + $http = new HttpServer($loop, function (ServerRequestInterface $request) use ($deferred) { $deferred->resolve($request); }); - $server->listen($this->socket); + $http->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $this->connection->emit('data', array("POST / HTTP/1.0\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: 7\r\n\r\nfoo=bar")); @@ -158,11 +157,11 @@ public function testPostFileUpload() { $loop = Factory::create(); $deferred = new Deferred(); - $server = new Server($loop, function (ServerRequestInterface $request) use ($deferred) { + $http = new HttpServer($loop, function (ServerRequestInterface $request) use ($deferred) { $deferred->resolve($request); }); - $server->listen($this->socket); + $http->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $connection = $this->connection; @@ -202,11 +201,11 @@ public function testPostJsonWillNotBeParsedByDefault() { $loop = Factory::create(); $deferred = new Deferred(); - $server = new Server($loop, function (ServerRequestInterface $request) use ($deferred) { + $http = new HttpServer($loop, function (ServerRequestInterface $request) use ($deferred) { $deferred->resolve($request); }); - $server->listen($this->socket); + $http->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $this->connection->emit('data', array("POST / HTTP/1.0\r\nContent-Type: application/json\r\nContent-Length: 6\r\n\r\n[true]")); @@ -227,11 +226,11 @@ public function testPostJsonWillNotBeParsedByDefault() public function testServerReceivesBufferedRequestByDefault() { $streaming = null; - $server = new Server(Factory::create(), function (ServerRequestInterface $request) use (&$streaming) { + $http = new HttpServer(function (ServerRequestInterface $request) use (&$streaming) { $streaming = $request->getBody() instanceof ReadableStreamInterface; }); - $server->listen($this->socket); + $http->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $this->connection->emit('data', array("GET / HTTP/1.0\r\n\r\n")); @@ -241,15 +240,14 @@ public function testServerReceivesBufferedRequestByDefault() public function testServerWithStreamingRequestMiddlewareReceivesStreamingRequest() { $streaming = null; - $server = new Server( - Factory::create(), + $http = new HttpServer( new StreamingRequestMiddleware(), function (ServerRequestInterface $request) use (&$streaming) { $streaming = $request->getBody() instanceof ReadableStreamInterface; } ); - $server->listen($this->socket); + $http->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $this->connection->emit('data', array("GET / HTTP/1.0\r\n\r\n")); @@ -260,14 +258,14 @@ public function testForwardErrors() { $exception = new \Exception(); $capturedException = null; - $server = new Server(Factory::create(), function () use ($exception) { + $http = new HttpServer(function () use ($exception) { return Promise\reject($exception); }); - $server->on('error', function ($error) use (&$capturedException) { + $http->on('error', function ($error) use (&$capturedException) { $capturedException = $error; }); - $server->listen($this->socket); + $http->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = $this->createPostFileUploadRequest(); @@ -328,36 +326,36 @@ public function provideIniSettingsForConcurrency() */ public function testServerConcurrency($memory_limit, $post_max_size, $expectedConcurrency) { - $server = new Server(Factory::create(), function () { }); + $http = new HttpServer(function () { }); - $ref = new \ReflectionMethod($server, 'getConcurrentRequestsLimit'); + $ref = new \ReflectionMethod($http, 'getConcurrentRequestsLimit'); $ref->setAccessible(true); - $value = $ref->invoke($server, $memory_limit, $post_max_size); + $value = $ref->invoke($http, $memory_limit, $post_max_size); $this->assertEquals($expectedConcurrency, $value); } public function testServerGetPostMaxSizeReturnsSizeFromGivenIniSetting() { - $server = new Server(Factory::create(), function () { }); + $http = new HttpServer(function () { }); - $ref = new \ReflectionMethod($server, 'getMaxRequestSize'); + $ref = new \ReflectionMethod($http, 'getMaxRequestSize'); $ref->setAccessible(true); - $value = $ref->invoke($server, '1k'); + $value = $ref->invoke($http, '1k'); $this->assertEquals(1024, $value); } public function testServerGetPostMaxSizeReturnsSizeCappedFromGivenIniSetting() { - $server = new Server(Factory::create(), function () { }); + $http = new HttpServer(function () { }); - $ref = new \ReflectionMethod($server, 'getMaxRequestSize'); + $ref = new \ReflectionMethod($http, 'getMaxRequestSize'); $ref->setAccessible(true); - $value = $ref->invoke($server, '1M'); + $value = $ref->invoke($http, '1M'); $this->assertEquals(64 * 1024, $value); } @@ -368,12 +366,12 @@ public function testServerGetPostMaxSizeFromIniIsCapped() $this->markTestSkipped(); } - $server = new Server(Factory::create(), function () { }); + $http = new HttpServer(function () { }); - $ref = new \ReflectionMethod($server, 'getMaxRequestSize'); + $ref = new \ReflectionMethod($http, 'getMaxRequestSize'); $ref->setAccessible(true); - $value = $ref->invoke($server); + $value = $ref->invoke($http); $this->assertEquals(64 * 1024, $value); } @@ -383,14 +381,14 @@ public function testConstructServerWithUnlimitedMemoryLimitDoesNotLimitConcurren $old = ini_get('memory_limit'); ini_set('memory_limit', '-1'); - $server = new Server(Factory::create(), function () { }); + $http = new HttpServer(function () { }); ini_set('memory_limit', $old); - $ref = new \ReflectionProperty($server, 'streamingServer'); + $ref = new \ReflectionProperty($http, 'streamingServer'); $ref->setAccessible(true); - $streamingServer = $ref->getValue($server); + $streamingServer = $ref->getValue($http); $ref = new \ReflectionProperty($streamingServer, 'callback'); $ref->setAccessible(true); @@ -411,14 +409,14 @@ public function testConstructServerWithMemoryLimitDoesLimitConcurrency() $old = ini_get('memory_limit'); ini_set('memory_limit', '100M'); - $server = new Server(Factory::create(), function () { }); + $http = new HttpServer(function () { }); ini_set('memory_limit', $old); - $ref = new \ReflectionProperty($server, 'streamingServer'); + $ref = new \ReflectionProperty($http, 'streamingServer'); $ref->setAccessible(true); - $streamingServer = $ref->getValue($server); + $streamingServer = $ref->getValue($http); $ref = new \ReflectionProperty($streamingServer, 'callback'); $ref->setAccessible(true); From 255e89173a6a1636fbcd19d472ea7d492cbbc859 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 25 Jul 2021 20:13:37 +0200 Subject: [PATCH 369/456] Add deprecated `Server` alias for new `HttpServer` class to ensure BC --- src/Server.php | 18 ++++++++++++++++++ tests/ServerTest.php | 15 +++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 src/Server.php create mode 100644 tests/ServerTest.php diff --git a/src/Server.php b/src/Server.php new file mode 100644 index 00000000..9bb9cf7f --- /dev/null +++ b/src/Server.php @@ -0,0 +1,18 @@ +assertInstanceOf('React\Http\HttpServer', $http); + } +} From 12a4946268ebe61f9a15de5acc495351778e70b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 27 Jul 2021 10:37:57 +0200 Subject: [PATCH 370/456] Update `Browser` signature to take `$connector` as first argument --- README.md | 31 ++++-- examples/11-client-http-connect-proxy.php | 3 +- examples/12-client-socks-proxy.php | 3 +- examples/13-client-ssh-proxy.php | 3 +- examples/14-client-unix-domain-sockets.php | 2 +- src/Browser.php | 41 ++++++-- tests/BrowserTest.php | 114 ++++++++++++++++++++- tests/FunctionalBrowserTest.php | 6 +- 8 files changed, 172 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 126fdcaf..e82307cf 100644 --- a/README.md +++ b/README.md @@ -259,7 +259,6 @@ like this: ```php $browser = new React\Http\Browser( - null, new React\Socket\Connector( null, array( @@ -610,7 +609,7 @@ $connector = new React\Socket\Connector(null, array( 'dns' => false )); -$browser = new React\Http\Browser(null, $connector); +$browser = new React\Http\Browser($connector); ``` See also the [HTTP CONNECT proxy example](examples/11-client-http-connect-proxy.php). @@ -637,7 +636,7 @@ $connector = new React\Socket\Connector(null, array( 'dns' => false )); -$browser = new React\Http\Browser(null, $connector); +$browser = new React\Http\Browser($connector); ``` See also the [SOCKS proxy example](examples/12-client-socks-proxy.php). @@ -666,7 +665,7 @@ $connector = new React\Socket\Connector(null, array( 'dns' => false )); -$browser = new React\Http\Browser(null, $connector); +$browser = new React\Http\Browser($connector); ``` See also the [SSH proxy example](examples/13-client-ssh-proxy.php). @@ -687,7 +686,7 @@ $connector = new React\Socket\FixedUriConnector( new React\Socket\UnixConnector() ); -$browser = new Browser(null, $connector); +$browser = new React\Http\Browser($connector); $client->get('http://localhost/info')->then(function (Psr\Http\Message\ResponseInterface $response) { var_dump($response->getHeaders(), (string)$response->getBody()); @@ -1869,11 +1868,15 @@ and keeps track of pending incoming HTTP responses. $browser = new React\Http\Browser(); ``` -This class takes an optional `LoopInterface|null $loop` parameter that can be used to -pass the event loop instance to use for this object. You can use a `null` value -here in order to use the [default loop](https://github.com/reactphp/event-loop#loop). -This value SHOULD NOT be given unless you're sure you want to explicitly use a -given event loop instance. +This class takes two optional arguments for more advanced usage: + +```php +// constructor signature as of v1.5.0 +$browser = new React\Http\Browser(?ConnectorInterface $connector = null, ?LoopInterface $loop = null); + +// legacy constructor signature before v1.5.0 +$browser = new React\Http\Browser(?LoopInterface $loop = null, ?ConnectorInterface $connector = null); +``` If you need custom connector settings (DNS resolution, TLS parameters, timeouts, proxy servers etc.), you can explicitly pass a custom instance of the @@ -1891,9 +1894,15 @@ $connector = new React\Socket\Connector(null, array( ) )); -$browser = new React\Http\Browser(null, $connector); +$browser = new React\Http\Browser($connector); ``` +This class takes an optional `LoopInterface|null $loop` parameter that can be used to +pass the event loop instance to use for this object. You can use a `null` value +here in order to use the [default loop](https://github.com/reactphp/event-loop#loop). +This value SHOULD NOT be given unless you're sure you want to explicitly use a +given event loop instance. + > Note that the browser class is final and shouldn't be extended, it is likely to be marked final in a future release. #### get() diff --git a/examples/11-client-http-connect-proxy.php b/examples/11-client-http-connect-proxy.php index afa22e83..ca7104aa 100644 --- a/examples/11-client-http-connect-proxy.php +++ b/examples/11-client-http-connect-proxy.php @@ -21,7 +21,8 @@ 'tcp' => $proxy, 'dns' => false )); -$browser = new Browser(null, $connector); + +$browser = new Browser($connector); // demo fetching HTTP headers (or bail out otherwise) $browser->get('https://www.google.com/')->then(function (ResponseInterface $response) { diff --git a/examples/12-client-socks-proxy.php b/examples/12-client-socks-proxy.php index ec2375c5..5801b138 100644 --- a/examples/12-client-socks-proxy.php +++ b/examples/12-client-socks-proxy.php @@ -18,7 +18,8 @@ 'tcp' => $proxy, 'dns' => false )); -$browser = new Browser(null, $connector); + +$browser = new Browser($connector); // demo fetching HTTP headers (or bail out otherwise) $browser->get('https://www.google.com/')->then(function (ResponseInterface $response) { diff --git a/examples/13-client-ssh-proxy.php b/examples/13-client-ssh-proxy.php index d4c8dcea..764fb30f 100644 --- a/examples/13-client-ssh-proxy.php +++ b/examples/13-client-ssh-proxy.php @@ -17,7 +17,8 @@ 'tcp' => $proxy, 'dns' => false )); -$browser = new Browser(null, $connector); + +$browser = new Browser($connector); // demo fetching HTTP headers (or bail out otherwise) $browser->get('https://www.google.com/')->then(function (ResponseInterface $response) { diff --git a/examples/14-client-unix-domain-sockets.php b/examples/14-client-unix-domain-sockets.php index f60dd9f7..36bfecda 100644 --- a/examples/14-client-unix-domain-sockets.php +++ b/examples/14-client-unix-domain-sockets.php @@ -14,7 +14,7 @@ new UnixConnector() ); -$browser = new Browser(null, $connector); +$browser = new Browser($connector); // demo fetching HTTP headers (or bail out otherwise) $browser->get('http://localhost/info')->then(function (ResponseInterface $response) { diff --git a/src/Browser.php b/src/Browser.php index ed83689b..11519094 100644 --- a/src/Browser.php +++ b/src/Browser.php @@ -32,11 +32,15 @@ class Browser * $browser = new React\Http\Browser(); * ``` * - * This class takes an optional `LoopInterface|null $loop` parameter that can be used to - * pass the event loop instance to use for this object. You can use a `null` value - * here in order to use the [default loop](https://github.com/reactphp/event-loop#loop). - * This value SHOULD NOT be given unless you're sure you want to explicitly use a - * given event loop instance. + * This class takes two optional arguments for more advanced usage: + * + * ```php + * // constructor signature as of v1.5.0 + * $browser = new React\Http\Browser(?ConnectorInterface $connector = null, ?LoopInterface $loop = null); + * + * // legacy constructor signature before v1.5.0 + * $browser = new React\Http\Browser(?LoopInterface $loop = null, ?ConnectorInterface $connector = null); + * ``` * * If you need custom connector settings (DNS resolution, TLS parameters, timeouts, * proxy servers etc.), you can explicitly pass a custom instance of the @@ -54,15 +58,32 @@ class Browser * ) * )); * - * $browser = new React\Http\Browser(null, $connector); + * $browser = new React\Http\Browser($connector); * ``` * - * @param ?LoopInterface $loop - * @param ?ConnectorInterface $connector [optional] Connector to use. - * Should be `null` in order to use default Connector. + * This class takes an optional `LoopInterface|null $loop` parameter that can be used to + * pass the event loop instance to use for this object. You can use a `null` value + * here in order to use the [default loop](https://github.com/reactphp/event-loop#loop). + * This value SHOULD NOT be given unless you're sure you want to explicitly use a + * given event loop instance. + * + * @param null|ConnectorInterface|LoopInterface $connector + * @param null|LoopInterface|ConnectorInterface $loop + * @throws \InvalidArgumentException for invalid arguments */ - public function __construct(LoopInterface $loop = null, ConnectorInterface $connector = null) + public function __construct($connector = null, $loop = null) { + // swap arguments for legacy constructor signature + if (($connector instanceof LoopInterface || $connector === null) && ($loop instanceof ConnectorInterface || $loop === null)) { + $swap = $loop; + $loop = $connector; + $connector = $swap; + } + + if (($connector !== null && !$connector instanceof ConnectorInterface) || ($loop !== null && !$loop instanceof LoopInterface)) { + throw new \InvalidArgumentException('Expected "?ConnectorInterface $connector" and "?LoopInterface $loop" arguments'); + } + $loop = $loop ?: Loop::get(); $this->transaction = new Transaction( Sender::createFromLoop($loop, $connector), diff --git a/tests/BrowserTest.php b/tests/BrowserTest.php index 5df2b837..39be453a 100644 --- a/tests/BrowserTest.php +++ b/tests/BrowserTest.php @@ -3,8 +3,8 @@ namespace React\Tests\Http; use Clue\React\Block; -use React\Http\Browser; use Psr\Http\Message\RequestInterface; +use React\Http\Browser; use React\Promise\Promise; use RingCentral\Psr7\Uri; @@ -21,7 +21,7 @@ public function setUpBrowser() { $this->loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $this->sender = $this->getMockBuilder('React\Http\Io\Transaction')->disableOriginalConstructor()->getMock(); - $this->browser = new Browser($this->loop); + $this->browser = new Browser(null, $this->loop); $ref = new \ReflectionProperty($this->browser, 'transaction'); $ref->setAccessible(true); @@ -43,6 +43,114 @@ public function testConstructWithoutLoopAssignsLoopAutomatically() $this->assertInstanceOf('React\EventLoop\LoopInterface', $loop); } + public function testConstructWithConnectorAssignsGivenConnector() + { + $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + + $browser = new Browser($connector); + + $ref = new \ReflectionProperty($browser, 'transaction'); + $ref->setAccessible(true); + $transaction = $ref->getValue($browser); + + $ref = new \ReflectionProperty($transaction, 'sender'); + $ref->setAccessible(true); + $sender = $ref->getValue($transaction); + + $ref = new \ReflectionProperty($sender, 'http'); + $ref->setAccessible(true); + $client = $ref->getValue($sender); + + $ref = new \ReflectionProperty($client, 'connector'); + $ref->setAccessible(true); + $ret = $ref->getValue($client); + + $this->assertSame($connector, $ret); + } + + public function testConstructWithConnectorWithLegacySignatureAssignsGivenConnector() + { + $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + + $browser = new Browser(null, $connector); + + $ref = new \ReflectionProperty($browser, 'transaction'); + $ref->setAccessible(true); + $transaction = $ref->getValue($browser); + + $ref = new \ReflectionProperty($transaction, 'sender'); + $ref->setAccessible(true); + $sender = $ref->getValue($transaction); + + $ref = new \ReflectionProperty($sender, 'http'); + $ref->setAccessible(true); + $client = $ref->getValue($sender); + + $ref = new \ReflectionProperty($client, 'connector'); + $ref->setAccessible(true); + $ret = $ref->getValue($client); + + $this->assertSame($connector, $ret); + } + + public function testConstructWithLoopAssignsGivenLoop() + { + $browser = new Browser(null, $this->loop); + + $ref = new \ReflectionProperty($browser, 'transaction'); + $ref->setAccessible(true); + $transaction = $ref->getValue($browser); + + $ref = new \ReflectionProperty($transaction, 'loop'); + $ref->setAccessible(true); + $loop = $ref->getValue($transaction); + + $this->assertSame($this->loop, $loop); + } + + public function testConstructWithLoopWithLegacySignatureAssignsGivenLoop() + { + $browser = new Browser($this->loop); + + $ref = new \ReflectionProperty($browser, 'transaction'); + $ref->setAccessible(true); + $transaction = $ref->getValue($browser); + + $ref = new \ReflectionProperty($transaction, 'loop'); + $ref->setAccessible(true); + $loop = $ref->getValue($transaction); + + $this->assertSame($this->loop, $loop); + } + + public function testConstructWithInvalidConnectorThrows() + { + $this->setExpectedException('InvalidArgumentException'); + new Browser('foo'); + } + + public function testConstructWithInvalidLoopThrows() + { + $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + + $this->setExpectedException('InvalidArgumentException'); + new Browser($connector, 'foo'); + } + + public function testConstructWithConnectorTwiceThrows() + { + $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + + $this->setExpectedException('InvalidArgumentException'); + new Browser($connector, $connector); + } + + public function testConstructWithLoopTwiceThrows() + { + $this->setExpectedException('InvalidArgumentException'); + new Browser($this->loop, $this->loop); + } + public function testGetSendsGetRequest() { $that = $this; @@ -390,7 +498,7 @@ public function testCancelGetRequestShouldCancelUnderlyingSocketConnection() $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); $connector->expects($this->once())->method('connect')->with('example.com:80')->willReturn($pending); - $this->browser = new Browser($this->loop, $connector); + $this->browser = new Browser($connector, $this->loop); $promise = $this->browser->get('http://example.com/'); $promise->cancel(); diff --git a/tests/FunctionalBrowserTest.php b/tests/FunctionalBrowserTest.php index f5b7f324..666bd9a2 100644 --- a/tests/FunctionalBrowserTest.php +++ b/tests/FunctionalBrowserTest.php @@ -30,7 +30,7 @@ class FunctionalBrowserTest extends TestCase public function setUpBrowserAndServer() { $this->loop = $loop = Factory::create(); - $this->browser = new Browser($this->loop); + $this->browser = new Browser(null, $this->loop); $server = new Server($this->loop, new StreamingRequestMiddleware(), function (ServerRequestInterface $request) use ($loop) { $path = $request->getUri()->getPath(); @@ -398,7 +398,7 @@ public function testVerifyPeerEnabledForBadSslRejects() ) )); - $browser = new Browser($this->loop, $connector); + $browser = new Browser($connector, $this->loop); $this->setExpectedException('RuntimeException'); Block\await($browser->get('https://self-signed.badssl.com/'), $this->loop); @@ -420,7 +420,7 @@ public function testVerifyPeerDisabledForBadSslResolves() ) )); - $browser = new Browser($this->loop, $connector); + $browser = new Browser($connector, $this->loop); Block\await($browser->get('https://self-signed.badssl.com/'), $this->loop); } From 30c33fe4673357cb083e65965ab7bf7de1e59cd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 27 Jul 2021 17:34:50 +0200 Subject: [PATCH 371/456] Simplify usage by supporting new Socket API without nullable loop args --- README.md | 31 ++--- composer.json | 2 +- examples/11-client-http-connect-proxy.php | 2 +- examples/12-client-socks-proxy.php | 2 +- examples/13-client-ssh-proxy.php | 2 +- examples/51-server-hello-world.php | 2 +- examples/52-server-count-visitors.php | 2 +- examples/53-server-whatsmyip.php | 2 +- examples/54-server-query-parameter.php | 2 +- examples/55-server-cookie-handling.php | 2 +- examples/56-server-sleep.php | 2 +- examples/57-server-error-handling.php | 2 +- examples/58-server-stream-response.php | 2 +- examples/59-server-json-api.php | 2 +- examples/61-server-hello-world-https.php | 10 +- examples/62-server-form-upload.php | 2 +- examples/63-server-streaming-request.php | 2 +- examples/71-server-http-proxy.php | 2 +- examples/72-server-http-connect-proxy.php | 2 +- examples/81-server-upgrade-echo.php | 2 +- examples/82-server-upgrade-chat.php | 2 +- examples/99-server-benchmark-download.php | 2 +- src/Browser.php | 2 +- src/Client/Client.php | 2 +- src/HttpServer.php | 20 +-- src/Io/Sender.php | 2 +- src/Io/StreamingServer.php | 6 +- tests/Client/FunctionalIntegrationTest.php | 20 +-- tests/FunctionalBrowserTest.php | 21 ++-- tests/FunctionalHttpServerTest.php | 136 ++++++++++----------- 30 files changed, 146 insertions(+), 144 deletions(-) diff --git a/README.md b/README.md index 0cfd0307..49d8ecbc 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,7 @@ $http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterf ); }); -$socket = new React\Socket\Server(8080); +$socket = new React\Socket\SocketServer('127.0.0.1:8080'); $http->listen($socket); ``` @@ -260,7 +260,6 @@ like this: ```php $browser = new React\Http\Browser( new React\Socket\Connector( - null, array( 'timeout' => 5 ) @@ -604,7 +603,7 @@ $proxy = new Clue\React\HttpProxy\ProxyConnector( new React\Socket\Connector() ); -$connector = new React\Socket\Connector(null, array( +$connector = new React\Socket\Connector(array( 'tcp' => $proxy, 'dns' => false )); @@ -631,7 +630,7 @@ $proxy = new Clue\React\Socks\Client( new React\Socket\Connector() ); -$connector = new React\Socket\Connector(null, array( +$connector = new React\Socket\Connector(array( 'tcp' => $proxy, 'dns' => false )); @@ -660,7 +659,7 @@ plain HTTP and TLS-encrypted HTTPS. ```php $proxy = new Clue\React\SshProxy\SshSocksConnector('me@localhost:22', Loop::get()); -$connector = new React\Socket\Connector(null, array( +$connector = new React\Socket\Connector(array( 'tcp' => $proxy, 'dns' => false )); @@ -741,13 +740,13 @@ to be attached to an instance of [`React\Socket\ServerInterface`](https://github.com/reactphp/socket#serverinterface) through the [`listen()`](#listen) method as described in the following chapter. In its most simple form, you can attach this to a -[`React\Socket\Server`](https://github.com/reactphp/socket#server) in order -to start a plaintext HTTP server like this: +[`React\Socket\SocketServer`](https://github.com/reactphp/socket#socketserver) +in order to start a plaintext HTTP server like this: ```php $http = new React\Http\HttpServer($handler); -$socket = new React\Socket\Server('0.0.0.0:8080'); +$socket = new React\Socket\SocketServer('0.0.0.0:8080'); $http->listen($socket); ``` @@ -869,13 +868,13 @@ is responsible for emitting the underlying streaming connections. This HTTP server needs to be attached to it in order to process any connections and pase incoming streaming data as incoming HTTP request messages. In its most common form, you can attach this to a -[`React\Socket\Server`](https://github.com/reactphp/socket#server) in -order to start a plaintext HTTP server like this: +[`React\Socket\SocketServer`](https://github.com/reactphp/socket#socketserver) +in order to start a plaintext HTTP server like this: ```php $http = new React\Http\HttpServer($handler); -$socket = new React\Socket\Server('0.0.0.0:8080'); +$socket = new React\Socket\SocketServer('0.0.0.0:8080'); $http->listen($socket); ``` @@ -894,15 +893,17 @@ Likewise, it's usually recommended to use a reverse proxy setup to accept secure HTTPS requests on default HTTPS port `443` (TLS termination) and only route plaintext requests to this HTTP server. As an alternative, you can also accept secure HTTPS requests with this HTTP server by attaching -this to a [`React\Socket\Server`](https://github.com/reactphp/socket#server) +this to a [`React\Socket\SocketServer`](https://github.com/reactphp/socket#socketserver) using a secure TLS listen address, a certificate file and optional `passphrase` like this: ```php $http = new React\Http\HttpServer($handler); -$socket = new React\Socket\Server('tls://0.0.0.0:8443', null, array( - 'local_cert' => __DIR__ . '/localhost.pem' +$socket = new React\Socket\SocketServer('tls://0.0.0.0:8443', array( + 'tls' => array( + 'local_cert' => __DIR__ . '/localhost.pem' + ) )); $http->listen($socket); ``` @@ -1889,7 +1890,7 @@ proxy servers etc.), you can explicitly pass a custom instance of the [`ConnectorInterface`](https://github.com/reactphp/socket#connectorinterface): ```php -$connector = new React\Socket\Connector(null, array( +$connector = new React\Socket\Connector(array( 'dns' => '127.0.0.1', 'tcp' => array( 'bindto' => '192.168.10.1:0' diff --git a/composer.json b/composer.json index 6673b232..44387e53 100644 --- a/composer.json +++ b/composer.json @@ -32,7 +32,7 @@ "react/event-loop": "^1.2", "react/promise": "^2.3 || ^1.2.1", "react/promise-stream": "^1.1", - "react/socket": "^1.8", + "react/socket": "^1.9", "react/stream": "^1.2", "ringcentral/psr7": "^1.2" }, diff --git a/examples/11-client-http-connect-proxy.php b/examples/11-client-http-connect-proxy.php index ca7104aa..39b0cbcb 100644 --- a/examples/11-client-http-connect-proxy.php +++ b/examples/11-client-http-connect-proxy.php @@ -17,7 +17,7 @@ $proxy = new HttpConnectClient('127.0.0.1:8080', new Connector()); // create a Browser object that uses the HTTP CONNECT proxy client for connections -$connector = new Connector(null, array( +$connector = new Connector(array( 'tcp' => $proxy, 'dns' => false )); diff --git a/examples/12-client-socks-proxy.php b/examples/12-client-socks-proxy.php index 5801b138..ce020ad8 100644 --- a/examples/12-client-socks-proxy.php +++ b/examples/12-client-socks-proxy.php @@ -14,7 +14,7 @@ $proxy = new SocksClient('127.0.0.1:1080', new Connector()); // create a Browser object that uses the SOCKS proxy client for connections -$connector = new Connector(null, array( +$connector = new Connector(array( 'tcp' => $proxy, 'dns' => false )); diff --git a/examples/13-client-ssh-proxy.php b/examples/13-client-ssh-proxy.php index 764fb30f..d4acaba0 100644 --- a/examples/13-client-ssh-proxy.php +++ b/examples/13-client-ssh-proxy.php @@ -13,7 +13,7 @@ $proxy = new SshSocksConnector(isset($argv[1]) ? $argv[1] : 'localhost:22', Loop::get()); // create a Browser object that uses the SSH proxy client for connections -$connector = new Connector(null, array( +$connector = new Connector(array( 'tcp' => $proxy, 'dns' => false )); diff --git a/examples/51-server-hello-world.php b/examples/51-server-hello-world.php index 50c82396..f549ece8 100644 --- a/examples/51-server-hello-world.php +++ b/examples/51-server-hello-world.php @@ -15,7 +15,7 @@ ); }); -$socket = new React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); $http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/52-server-count-visitors.php b/examples/52-server-count-visitors.php index 1884acb0..d52285d0 100644 --- a/examples/52-server-count-visitors.php +++ b/examples/52-server-count-visitors.php @@ -16,7 +16,7 @@ ); }); -$socket = new React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); $http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/53-server-whatsmyip.php b/examples/53-server-whatsmyip.php index 89dad25c..5df1050d 100644 --- a/examples/53-server-whatsmyip.php +++ b/examples/53-server-whatsmyip.php @@ -17,7 +17,7 @@ ); }); -$socket = new React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); $http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/54-server-query-parameter.php b/examples/54-server-query-parameter.php index 2ab16647..22be7566 100644 --- a/examples/54-server-query-parameter.php +++ b/examples/54-server-query-parameter.php @@ -24,7 +24,7 @@ ); }); -$socket = new React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); $http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/55-server-cookie-handling.php b/examples/55-server-cookie-handling.php index ff89fe1e..a6858061 100644 --- a/examples/55-server-cookie-handling.php +++ b/examples/55-server-cookie-handling.php @@ -30,7 +30,7 @@ ); }); -$socket = new React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); $http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/56-server-sleep.php b/examples/56-server-sleep.php index bd2ea694..caa22644 100644 --- a/examples/56-server-sleep.php +++ b/examples/56-server-sleep.php @@ -22,7 +22,7 @@ }); }); -$socket = new React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); $http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/57-server-error-handling.php b/examples/57-server-error-handling.php index f5281c53..4a1b6757 100644 --- a/examples/57-server-error-handling.php +++ b/examples/57-server-error-handling.php @@ -27,7 +27,7 @@ }); }); -$socket = new React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); $http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/58-server-stream-response.php b/examples/58-server-stream-response.php index d965e306..2069b7a8 100644 --- a/examples/58-server-stream-response.php +++ b/examples/58-server-stream-response.php @@ -38,7 +38,7 @@ ); }); -$socket = new React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); $http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/59-server-json-api.php b/examples/59-server-json-api.php index c06a9702..7fa8cc66 100644 --- a/examples/59-server-json-api.php +++ b/examples/59-server-json-api.php @@ -52,7 +52,7 @@ ); }); -$socket = new React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); $http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/61-server-hello-world-https.php b/examples/61-server-hello-world-https.php index 34f1a8bd..e5e0ed84 100644 --- a/examples/61-server-hello-world-https.php +++ b/examples/61-server-hello-world-https.php @@ -15,12 +15,14 @@ ); }); -$socket = new React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); -$socket = new React\Socket\SecureServer($socket, null, array( - 'local_cert' => isset($argv[2]) ? $argv[2] : __DIR__ . '/localhost.pem' +$uri = 'tls://' . (isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$socket = new React\Socket\SocketServer($uri, array( + 'tls' => array( + 'local_cert' => isset($argv[2]) ? $argv[2] : __DIR__ . '/localhost.pem' + ) )); $http->listen($socket); -//$socket->on('error', 'printf'); +$socket->on('error', 'printf'); echo 'Listening on ' . str_replace('tls:', 'https:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/62-server-form-upload.php b/examples/62-server-form-upload.php index 5a0fdace..6984b4e3 100644 --- a/examples/62-server-form-upload.php +++ b/examples/62-server-form-upload.php @@ -128,7 +128,7 @@ $handler ); -$socket = new React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); $http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/63-server-streaming-request.php b/examples/63-server-streaming-request.php index 693cf2c0..2b5f8a6c 100644 --- a/examples/63-server-streaming-request.php +++ b/examples/63-server-streaming-request.php @@ -44,7 +44,7 @@ function (Psr\Http\Message\ServerRequestInterface $request) { $http->on('error', 'printf'); -$socket = new React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); $http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/71-server-http-proxy.php b/examples/71-server-http-proxy.php index f6ab5ff5..c4fe244e 100644 --- a/examples/71-server-http-proxy.php +++ b/examples/71-server-http-proxy.php @@ -44,7 +44,7 @@ ); }); -$socket = new React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); $http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/72-server-http-connect-proxy.php b/examples/72-server-http-connect-proxy.php index 4399614b..205dd1da 100644 --- a/examples/72-server-http-connect-proxy.php +++ b/examples/72-server-http-connect-proxy.php @@ -50,7 +50,7 @@ function ($e) { ); }); -$socket = new React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); $http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/81-server-upgrade-echo.php b/examples/81-server-upgrade-echo.php index cb5d0b0d..2f77172f 100644 --- a/examples/81-server-upgrade-echo.php +++ b/examples/81-server-upgrade-echo.php @@ -56,7 +56,7 @@ ); }); -$socket = new React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); $http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/82-server-upgrade-chat.php b/examples/82-server-upgrade-chat.php index 22fed927..42635e8c 100644 --- a/examples/82-server-upgrade-chat.php +++ b/examples/82-server-upgrade-chat.php @@ -84,7 +84,7 @@ ); }); -$socket = new React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); $http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/99-server-benchmark-download.php b/examples/99-server-benchmark-download.php index bdc6735a..df0e69e7 100644 --- a/examples/99-server-benchmark-download.php +++ b/examples/99-server-benchmark-download.php @@ -122,7 +122,7 @@ public function getSize() ); }); -$socket = new React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); $http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/src/Browser.php b/src/Browser.php index 11519094..5879b977 100644 --- a/src/Browser.php +++ b/src/Browser.php @@ -47,7 +47,7 @@ class Browser * [`ConnectorInterface`](https://github.com/reactphp/socket#connectorinterface): * * ```php - * $connector = new React\Socket\Connector(null, array( + * $connector = new React\Socket\Connector(array( * 'dns' => '127.0.0.1', * 'tcp' => array( * 'bindto' => '192.168.10.1:0' diff --git a/src/Client/Client.php b/src/Client/Client.php index f28ec289..7a97349c 100644 --- a/src/Client/Client.php +++ b/src/Client/Client.php @@ -16,7 +16,7 @@ class Client public function __construct(LoopInterface $loop, ConnectorInterface $connector = null) { if ($connector === null) { - $connector = new Connector($loop); + $connector = new Connector(array(), $loop); } $this->connector = $connector; diff --git a/src/HttpServer.php b/src/HttpServer.php index 582ea380..d5f947d5 100644 --- a/src/HttpServer.php +++ b/src/HttpServer.php @@ -54,13 +54,13 @@ * [`React\Socket\ServerInterface`](https://github.com/reactphp/socket#serverinterface) * through the [`listen()`](#listen) method as described in the following * chapter. In its most simple form, you can attach this to a - * [`React\Socket\Server`](https://github.com/reactphp/socket#server) in order - * to start a plaintext HTTP server like this: + * [`React\Socket\SocketServer`](https://github.com/reactphp/socket#socketserver) + * in order to start a plaintext HTTP server like this: * * ```php * $http = new React\Http\HttpServer($handler); * - * $socket = new React\Socket\Server('0.0.0.0:8080'); + * $socket = new React\Socket\SocketServer('0.0.0.0:8080'); * $http->listen($socket); * ``` * @@ -263,13 +263,13 @@ public function __construct($requestHandlerOrLoop) * HTTP server needs to be attached to it in order to process any * connections and pase incoming streaming data as incoming HTTP request * messages. In its most common form, you can attach this to a - * [`React\Socket\Server`](https://github.com/reactphp/socket#server) in - * order to start a plaintext HTTP server like this: + * [`React\Socket\SocketServer`](https://github.com/reactphp/socket#socketserver) + * in order to start a plaintext HTTP server like this: * * ```php * $http = new React\Http\HttpServer($handler); * - * $socket = new React\Socket\Server(8080); + * $socket = new React\Socket\SocketServer('0.0.0.0:8080'); * $http->listen($socket); * ``` * @@ -288,15 +288,17 @@ public function __construct($requestHandlerOrLoop) * secure HTTPS requests on default HTTPS port `443` (TLS termination) and * only route plaintext requests to this HTTP server. As an alternative, you * can also accept secure HTTPS requests with this HTTP server by attaching - * this to a [`React\Socket\Server`](https://github.com/reactphp/socket#server) + * this to a [`React\Socket\SocketServer`](https://github.com/reactphp/socket#socketserver) * using a secure TLS listen address, a certificate file and optional * `passphrase` like this: * * ```php * $http = new React\Http\HttpServer($handler); * - * $socket = new React\Socket\Server('tls://0.0.0.0:8443', null, array( - * 'local_cert' => __DIR__ . '/localhost.pem' + * $socket = new React\Socket\SocketServer('tls://0.0.0.0:8443', array( + * 'tls' => array( + * 'local_cert' => __DIR__ . '/localhost.pem' + * ) * )); * $http->listen($socket); * ``` diff --git a/src/Io/Sender.php b/src/Io/Sender.php index 1eb098c6..c1bbab42 100644 --- a/src/Io/Sender.php +++ b/src/Io/Sender.php @@ -39,7 +39,7 @@ class Sender * settings. You can use this method manually like this: * * ```php - * $connector = new \React\Socket\Connector($loop); + * $connector = new \React\Socket\Connector(array(), $loop); * $sender = \React\Http\Io\Sender::createFromLoop($loop, $connector); * ``` * diff --git a/src/Io/StreamingServer.php b/src/Io/StreamingServer.php index 95e0e4c6..2c912dfa 100644 --- a/src/Io/StreamingServer.php +++ b/src/Io/StreamingServer.php @@ -20,7 +20,7 @@ * The internal `StreamingServer` class is responsible for handling incoming connections and then * processing each incoming HTTP request. * - * Unlike the [`HttpServer`](#server) class, it does not buffer and parse the incoming + * Unlike the [`HttpServer`](#httpserver) class, it does not buffer and parse the incoming * HTTP request body by default. This means that the request handler will be * invoked with a streaming request body. Once the request headers have been * received, it will invoke the request handler function. This request handler @@ -50,13 +50,13 @@ * In order to process any connections, the server needs to be attached to an * instance of `React\Socket\ServerInterface` through the [`listen()`](#listen) method * as described in the following chapter. In its most simple form, you can attach - * this to a [`React\Socket\Server`](https://github.com/reactphp/socket#server) + * this to a [`React\Socket\SocketServer`](https://github.com/reactphp/socket#socketserver) * in order to start a plaintext HTTP server like this: * * ```php * $server = new StreamingServer($loop, $handler); * - * $socket = new React\Socket\Server('0.0.0.0:8080', $loop); + * $socket = new React\Socket\SocketServer('0.0.0.0:8080', array(), $loop); * $server->listen($socket); * ``` * diff --git a/tests/Client/FunctionalIntegrationTest.php b/tests/Client/FunctionalIntegrationTest.php index 3e07803a..6a62be93 100644 --- a/tests/Client/FunctionalIntegrationTest.php +++ b/tests/Client/FunctionalIntegrationTest.php @@ -8,8 +8,8 @@ use React\Http\Client\Client; use React\Promise\Deferred; use React\Promise\Stream; -use React\Socket\Server; use React\Socket\ConnectionInterface; +use React\Socket\SocketServer; use React\Stream\ReadableStreamInterface; use React\Tests\Http\TestCase; @@ -39,13 +39,13 @@ public function testRequestToLocalhostEmitsSingleRemoteConnection() { $loop = Factory::create(); - $server = new Server(0, $loop); - $server->on('connection', $this->expectCallableOnce()); - $server->on('connection', function (ConnectionInterface $conn) use ($server) { + $socket = new SocketServer('127.0.0.1:0', array(), $loop); + $socket->on('connection', $this->expectCallableOnce()); + $socket->on('connection', function (ConnectionInterface $conn) use ($socket) { $conn->end("HTTP/1.1 200 OK\r\n\r\nOk"); - $server->close(); + $socket->close(); }); - $port = parse_url($server->getAddress(), PHP_URL_PORT); + $port = parse_url($socket->getAddress(), PHP_URL_PORT); $client = new Client($loop); $request = $client->request('GET', 'http://localhost:' . $port); @@ -60,14 +60,14 @@ public function testRequestLegacyHttpServerWithOnlyLineFeedReturnsSuccessfulResp { $loop = Factory::create(); - $server = new Server(0, $loop); - $server->on('connection', function (ConnectionInterface $conn) use ($server) { + $socket = new SocketServer('127.0.0.1:0', array(), $loop); + $socket->on('connection', function (ConnectionInterface $conn) use ($socket) { $conn->end("HTTP/1.0 200 OK\n\nbody"); - $server->close(); + $socket->close(); }); $client = new Client($loop); - $request = $client->request('GET', str_replace('tcp:', 'http:', $server->getAddress())); + $request = $client->request('GET', str_replace('tcp:', 'http:', $socket->getAddress())); $once = $this->expectCallableOnceWith('body'); $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($once) { diff --git a/tests/FunctionalBrowserTest.php b/tests/FunctionalBrowserTest.php index a46e74a6..5db62003 100644 --- a/tests/FunctionalBrowserTest.php +++ b/tests/FunctionalBrowserTest.php @@ -14,6 +14,7 @@ use React\Promise\Promise; use React\Promise\Stream; use React\Socket\Connector; +use React\Socket\SocketServer; use React\Stream\ReadableStreamInterface; use React\Stream\ThroughStream; use RingCentral\Psr7\Request; @@ -141,7 +142,7 @@ public function setUpBrowserAndServer() var_dump($path); }); - $socket = new \React\Socket\Server(0, $this->loop); + $socket = new SocketServer('127.0.0.1:0', array(), $this->loop); $http->listen($socket); $this->base = str_replace('tcp:', 'http:', $socket->getAddress()) . '/'; @@ -392,11 +393,11 @@ public function testVerifyPeerEnabledForBadSslRejects() $this->markTestSkipped('Not supported on HHVM'); } - $connector = new Connector($this->loop, array( + $connector = new Connector(array( 'tls' => array( 'verify_peer' => true ) - )); + ), $this->loop); $browser = new Browser($connector, $this->loop); @@ -414,11 +415,11 @@ public function testVerifyPeerDisabledForBadSslResolves() $this->markTestSkipped('Not supported on HHVM'); } - $connector = new Connector($this->loop, array( + $connector = new Connector(array( 'tls' => array( 'verify_peer' => false ) - )); + ), $this->loop); $browser = new Browser($connector, $this->loop); @@ -497,7 +498,7 @@ public function testRequestStreamWithHeadRequestReturnsEmptyResponseBodWithTrans public function testRequestStreamReturnsResponseWithResponseBodyUndecodedWhenResponseHasDoubleTransferEncoding() { - $socket = new \React\Socket\Server(0, $this->loop); + $socket = new SocketServer('127.0.0.1:0', array(), $this->loop); $socket->on('connection', function (\React\Socket\ConnectionInterface $connection) { $connection->on('data', function () use ($connection) { $connection->end("HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked, chunked\r\nConnection: close\r\n\r\nhello"); @@ -517,7 +518,7 @@ public function testRequestStreamReturnsResponseWithResponseBodyUndecodedWhenRes public function testReceiveStreamAndExplicitlyCloseConnectionEvenWhenServerKeepsConnectionOpen() { $closed = new \React\Promise\Deferred(); - $socket = new \React\Socket\Server(0, $this->loop); + $socket = new SocketServer('127.0.0.1:0', array(), $this->loop); $socket->on('connection', function (\React\Socket\ConnectionInterface $connection) use ($closed) { $connection->on('data', function () use ($connection) { $connection->write("HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello"); @@ -576,7 +577,7 @@ public function testPostStreamWillStartSendingRequestEvenWhenBodyDoesNotEmitData $http = new HttpServer($this->loop, new StreamingRequestMiddleware(), function (ServerRequestInterface $request) { return new Response(200); }); - $socket = new \React\Socket\Server(0, $this->loop); + $socket = new SocketServer('127.0.0.1:0', array(), $this->loop); $http->listen($socket); $this->base = str_replace('tcp:', 'http:', $socket->getAddress()) . '/'; @@ -607,7 +608,7 @@ public function testSendsHttp11ByDefault() $request->getProtocolVersion() ); }); - $socket = new \React\Socket\Server(0, $this->loop); + $socket = new SocketServer('127.0.0.1:0', array(), $this->loop); $http->listen($socket); $this->base = str_replace('tcp:', 'http:', $socket->getAddress()) . '/'; @@ -627,7 +628,7 @@ public function testSendsExplicitHttp10Request() $request->getProtocolVersion() ); }); - $socket = new \React\Socket\Server(0, $this->loop); + $socket = new SocketServer('127.0.0.1:0', array(), $this->loop); $http->listen($socket); $this->base = str_replace('tcp:', 'http:', $socket->getAddress()) . '/'; diff --git a/tests/FunctionalHttpServerTest.php b/tests/FunctionalHttpServerTest.php index f7aa8392..fe0e1936 100644 --- a/tests/FunctionalHttpServerTest.php +++ b/tests/FunctionalHttpServerTest.php @@ -11,10 +11,9 @@ use React\Http\Middleware\LimitConcurrentRequestsMiddleware; use React\Http\Middleware\RequestBodyBufferMiddleware; use React\Http\Middleware\StreamingRequestMiddleware; -use React\Socket\Server as Socket; -use React\Socket\Connector; use React\Socket\ConnectionInterface; -use React\Socket\SecureServer; +use React\Socket\Connector; +use React\Socket\SocketServer; use React\Promise; use React\Promise\Stream; use React\Stream\ThroughStream; @@ -24,13 +23,13 @@ class FunctionalHttpServerTest extends TestCase public function testPlainHttpOnRandomPort() { $loop = Factory::create(); - $connector = new Connector($loop); + $connector = new Connector(array(), $loop); $http = new HttpServer($loop, function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); - $socket = new Socket(0, $loop); + $socket = new SocketServer('127.0.0.1:0', array(), $loop); $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { @@ -50,7 +49,7 @@ public function testPlainHttpOnRandomPort() public function testPlainHttpOnRandomPortWithSingleRequestHandlerArray() { $loop = Factory::create(); - $connector = new Connector($loop); + $connector = new Connector(array(), $loop); $http = new HttpServer( $loop, @@ -59,7 +58,7 @@ function () { } ); - $socket = new Socket(0, $loop); + $socket = new SocketServer('127.0.0.1:0', array(), $loop); $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { @@ -78,13 +77,13 @@ function () { public function testPlainHttpOnRandomPortWithoutHostHeaderUsesSocketUri() { $loop = Factory::create(); - $connector = new Connector($loop); + $connector = new Connector(array(), $loop); $http = new HttpServer($loop, function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); - $socket = new Socket(0, $loop); + $socket = new SocketServer('127.0.0.1:0', array(), $loop); $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { @@ -104,13 +103,13 @@ public function testPlainHttpOnRandomPortWithoutHostHeaderUsesSocketUri() public function testPlainHttpOnRandomPortWithOtherHostHeaderTakesPrecedence() { $loop = Factory::create(); - $connector = new Connector($loop); + $connector = new Connector(array(), $loop); $http = new HttpServer($loop, function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); - $socket = new Socket(0, $loop); + $socket = new SocketServer('127.0.0.1:0', array(), $loop); $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { @@ -134,18 +133,17 @@ public function testSecureHttpsOnRandomPort() } $loop = Factory::create(); - $connector = new Connector($loop, array( + $connector = new Connector(array( 'tls' => array('verify_peer' => false) - )); + ), $loop); $http = new HttpServer($loop, function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); - $socket = new Socket(0, $loop); - $socket = new SecureServer($socket, $loop, array( + $socket = new SocketServer('tls://127.0.0.1:0', array('tls' => array( 'local_cert' => __DIR__ . '/../examples/localhost.pem' - )); + )), $loop); $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { @@ -178,15 +176,14 @@ public function testSecureHttpsReturnsData() ); }); - $socket = new Socket(0, $loop); - $socket = new SecureServer($socket, $loop, array( + $socket = new SocketServer('tls://127.0.0.1:0', array('tls' => array( 'local_cert' => __DIR__ . '/../examples/localhost.pem' - )); + )), $loop); $http->listen($socket); - $connector = new Connector($loop, array( + $connector = new Connector(array( 'tls' => array('verify_peer' => false) - )); + ), $loop); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\nHost: " . noScheme($conn->getRemoteAddress()) . "\r\n\r\n"); @@ -210,18 +207,17 @@ public function testSecureHttpsOnRandomPortWithoutHostHeaderUsesSocketUri() } $loop = Factory::create(); - $connector = new Connector($loop, array( + $connector = new Connector(array( 'tls' => array('verify_peer' => false) - )); + ), $loop); $http = new HttpServer($loop, function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); - $socket = new Socket(0, $loop); - $socket = new SecureServer($socket, $loop, array( + $socket = new SocketServer('tls://127.0.0.1:0', array('tls' => array( 'local_cert' => __DIR__ . '/../examples/localhost.pem' - )); + )), $loop); $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { @@ -242,11 +238,11 @@ public function testPlainHttpOnStandardPortReturnsUriWithNoPort() { $loop = Factory::create(); try { - $socket = new Socket(80, $loop); + $socket = new SocketServer('127.0.0.1:80', array(), $loop); } catch (\RuntimeException $e) { $this->markTestSkipped('Listening on port 80 failed (root and unused?)'); } - $connector = new Connector($loop); + $connector = new Connector(array(), $loop); $http = new HttpServer($loop, function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); @@ -272,11 +268,11 @@ public function testPlainHttpOnStandardPortWithoutHostHeaderReturnsUriWithNoPort { $loop = Factory::create(); try { - $socket = new Socket(80, $loop); + $socket = new SocketServer('127.0.0.1:80', array(), $loop); } catch (\RuntimeException $e) { $this->markTestSkipped('Listening on port 80 failed (root and unused?)'); } - $connector = new Connector($loop); + $connector = new Connector(array(), $loop); $http = new HttpServer($loop, function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); @@ -306,16 +302,16 @@ public function testSecureHttpsOnStandardPortReturnsUriWithNoPort() $loop = Factory::create(); try { - $socket = new Socket(443, $loop); + $socket = new SocketServer('127.0.0.1:443', array('tls' => array( + 'local_cert' => __DIR__ . '/../examples/localhost.pem' + )), $loop); } catch (\RuntimeException $e) { $this->markTestSkipped('Listening on port 443 failed (root and unused?)'); } - $socket = new SecureServer($socket, $loop, array( - 'local_cert' => __DIR__ . '/../examples/localhost.pem' - )); - $connector = new Connector($loop, array( + + $connector = new Connector(array( 'tls' => array('verify_peer' => false) - )); + ), $loop); $http = new HttpServer($loop, function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); @@ -345,16 +341,16 @@ public function testSecureHttpsOnStandardPortWithoutHostHeaderUsesSocketUri() $loop = Factory::create(); try { - $socket = new Socket(443, $loop); + $socket = new SocketServer('127.0.0.1:443', array('tls' => array( + 'local_cert' => __DIR__ . '/../examples/localhost.pem' + )), $loop); } catch (\RuntimeException $e) { $this->markTestSkipped('Listening on port 443 failed (root and unused?)'); } - $socket = new SecureServer($socket, $loop, array( - 'local_cert' => __DIR__ . '/../examples/localhost.pem' - )); - $connector = new Connector($loop, array( + + $connector = new Connector(array( 'tls' => array('verify_peer' => false) - )); + ), $loop); $http = new HttpServer($loop, function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); @@ -380,11 +376,11 @@ public function testPlainHttpOnHttpsStandardPortReturnsUriWithPort() { $loop = Factory::create(); try { - $socket = new Socket(443, $loop); + $socket = new SocketServer('127.0.0.1:443', array(), $loop); } catch (\RuntimeException $e) { $this->markTestSkipped('Listening on port 443 failed (root and unused?)'); } - $connector = new Connector($loop); + $connector = new Connector(array(), $loop); $http = new HttpServer($loop, function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); @@ -414,16 +410,16 @@ public function testSecureHttpsOnHttpStandardPortReturnsUriWithPort() $loop = Factory::create(); try { - $socket = new Socket(80, $loop); + $socket = new SocketServer('127.0.0.1:80', array('tls' => array( + 'local_cert' => __DIR__ . '/../examples/localhost.pem' + )), $loop); } catch (\RuntimeException $e) { $this->markTestSkipped('Listening on port 80 failed (root and unused?)'); } - $socket = new SecureServer($socket, $loop, array( - 'local_cert' => __DIR__ . '/../examples/localhost.pem' - )); - $connector = new Connector($loop, array( + + $connector = new Connector(array( 'tls' => array('verify_peer' => false) - )); + ), $loop); $http = new HttpServer($loop, function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri() . 'x' . $request->getHeaderLine('Host')); @@ -448,7 +444,7 @@ public function testSecureHttpsOnHttpStandardPortReturnsUriWithPort() public function testClosedStreamFromRequestHandlerWillSendEmptyBody() { $loop = Factory::create(); - $connector = new Connector($loop); + $connector = new Connector(array(), $loop); $stream = new ThroughStream(); $stream->close(); @@ -457,7 +453,7 @@ public function testClosedStreamFromRequestHandlerWillSendEmptyBody() return new Response(200, array(), $stream); }); - $socket = new Socket(0, $loop); + $socket = new SocketServer('127.0.0.1:0', array(), $loop); $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { @@ -477,7 +473,7 @@ public function testClosedStreamFromRequestHandlerWillSendEmptyBody() public function testRequestHandlerWithStreamingRequestWillReceiveCloseEventIfConnectionClosesWhileSendingBody() { $loop = Factory::create(); - $connector = new Connector($loop); + $connector = new Connector(array(), $loop); $once = $this->expectCallableOnce(); $http = new HttpServer( @@ -488,7 +484,7 @@ function (RequestInterface $request) use ($once) { } ); - $socket = new Socket(0, $loop); + $socket = new SocketServer('127.0.0.1:0', array(), $loop); $http->listen($socket); $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) use ($loop) { @@ -507,7 +503,7 @@ function (RequestInterface $request) use ($once) { public function testStreamFromRequestHandlerWillBeClosedIfConnectionClosesWhileSendingStreamingRequestBody() { $loop = Factory::create(); - $connector = new Connector($loop); + $connector = new Connector(array(), $loop); $stream = new ThroughStream(); @@ -519,7 +515,7 @@ function (RequestInterface $request) use ($stream) { } ); - $socket = new Socket(0, $loop); + $socket = new SocketServer('127.0.0.1:0', array(), $loop); $http->listen($socket); $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) use ($loop) { @@ -541,7 +537,7 @@ function (RequestInterface $request) use ($stream) { public function testStreamFromRequestHandlerWillBeClosedIfConnectionCloses() { $loop = Factory::create(); - $connector = new Connector($loop); + $connector = new Connector(array(), $loop); $stream = new ThroughStream(); @@ -549,7 +545,7 @@ public function testStreamFromRequestHandlerWillBeClosedIfConnectionCloses() return new Response(200, array(), $stream); }); - $socket = new Socket(0, $loop); + $socket = new SocketServer('127.0.0.1:0', array(), $loop); $http->listen($socket); $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) use ($loop) { @@ -571,7 +567,7 @@ public function testStreamFromRequestHandlerWillBeClosedIfConnectionCloses() public function testUpgradeWithThroughStreamReturnsDataAsGiven() { $loop = Factory::create(); - $connector = new Connector($loop); + $connector = new Connector(array(), $loop); $http = new HttpServer($loop, function (RequestInterface $request) use ($loop) { $stream = new ThroughStream(); @@ -583,7 +579,7 @@ public function testUpgradeWithThroughStreamReturnsDataAsGiven() return new Response(101, array('Upgrade' => 'echo'), $stream); }); - $socket = new Socket(0, $loop); + $socket = new SocketServer('127.0.0.1:0', array(), $loop); $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { @@ -608,7 +604,7 @@ public function testUpgradeWithThroughStreamReturnsDataAsGiven() public function testUpgradeWithRequestBodyAndThroughStreamReturnsDataAsGiven() { $loop = Factory::create(); - $connector = new Connector($loop); + $connector = new Connector(array(), $loop); $http = new HttpServer($loop, function (RequestInterface $request) use ($loop) { $stream = new ThroughStream(); @@ -620,7 +616,7 @@ public function testUpgradeWithRequestBodyAndThroughStreamReturnsDataAsGiven() return new Response(101, array('Upgrade' => 'echo'), $stream); }); - $socket = new Socket(0, $loop); + $socket = new SocketServer('127.0.0.1:0', array(), $loop); $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { @@ -646,7 +642,7 @@ public function testUpgradeWithRequestBodyAndThroughStreamReturnsDataAsGiven() public function testConnectWithThroughStreamReturnsDataAsGiven() { $loop = Factory::create(); - $connector = new Connector($loop); + $connector = new Connector(array(), $loop); $http = new HttpServer($loop, function (RequestInterface $request) use ($loop) { $stream = new ThroughStream(); @@ -658,7 +654,7 @@ public function testConnectWithThroughStreamReturnsDataAsGiven() return new Response(200, array(), $stream); }); - $socket = new Socket(0, $loop); + $socket = new SocketServer('127.0.0.1:0', array(), $loop); $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { @@ -683,7 +679,7 @@ public function testConnectWithThroughStreamReturnsDataAsGiven() public function testConnectWithThroughStreamReturnedFromPromiseReturnsDataAsGiven() { $loop = Factory::create(); - $connector = new Connector($loop); + $connector = new Connector(array(), $loop); $http = new HttpServer($loop, function (RequestInterface $request) use ($loop) { $stream = new ThroughStream(); @@ -699,7 +695,7 @@ public function testConnectWithThroughStreamReturnedFromPromiseReturnsDataAsGive }); }); - $socket = new Socket(0, $loop); + $socket = new SocketServer('127.0.0.1:0', array(), $loop); $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { @@ -724,7 +720,7 @@ public function testConnectWithThroughStreamReturnedFromPromiseReturnsDataAsGive public function testConnectWithClosedThroughStreamReturnsNoData() { $loop = Factory::create(); - $connector = new Connector($loop); + $connector = new Connector(array(), $loop); $http = new HttpServer($loop, function (RequestInterface $request) { $stream = new ThroughStream(); @@ -733,7 +729,7 @@ public function testConnectWithClosedThroughStreamReturnsNoData() return new Response(200, array(), $stream); }); - $socket = new Socket(0, $loop); + $socket = new SocketServer('127.0.0.1:0', array(), $loop); $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { @@ -758,7 +754,7 @@ public function testConnectWithClosedThroughStreamReturnsNoData() public function testLimitConcurrentRequestsMiddlewareRequestStreamPausing() { $loop = Factory::create(); - $connector = new Connector($loop); + $connector = new Connector(array(), $loop); $http = new HttpServer( $loop, @@ -776,7 +772,7 @@ function (ServerRequestInterface $request) { } ); - $socket = new Socket(0, $loop); + $socket = new SocketServer('127.0.0.1:0', array(), $loop); $http->listen($socket); $result = array(); From 8a0fd7c0aa74f0db3008b1e47ca86c613cbb040e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Wed, 4 Aug 2021 14:24:55 +0200 Subject: [PATCH 372/456] Prepare v1.5.0 release --- CHANGELOG.md | 33 +++++++++++++++++++++++++++++++++ README.md | 2 +- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f562f66d..e12b787e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,38 @@ # Changelog +## 1.5.0 (2021-08-04) + +* Feature: Update `Browser` signature to take optional `$connector` as first argument and + to match new Socket API without nullable loop arguments. + (#418 and #419 by @clue) + + ```php + // unchanged + $browser = new React\Http\Browser(); + + // deprecated + $browser = new React\Http\Browser(null, $connector); + $browser = new React\Http\Browser($loop, $connector); + + // new + $browser = new React\Http\Browser($connector); + $browser = new React\Http\Browser($connector, $loop); + ``` + +* Feature: Rename `Server` to `HttpServer` to avoid class name collisions and + to avoid any ambiguities with regards to the new `SocketServer` API. + (#417 and #419 by @clue) + + ```php + // deprecated + $server = new React\Http\Server($handler); + $server->listen(new React\Socket\Server(8080)); + + // new + $http = new React\Http\HttpServer($handler); + $http->listen(new React\Socket\SocketServer('127.0.0.1:8080')); + ``` + ## 1.4.0 (2021-07-11) A major new feature release, see [**release announcement**](https://clue.engineering/2021/announcing-reactphp-default-loop). diff --git a/README.md b/README.md index 49d8ecbc..63865d5c 100644 --- a/README.md +++ b/README.md @@ -2738,7 +2738,7 @@ This project follows [SemVer](https://semver.org/). This will install the latest supported version: ```bash -$ composer require react/http:^1.3 +$ composer require react/http:^1.5 ``` See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. From 40ecb8f6dfa5c424a291c5784a1f6086913e1a6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 25 Jul 2021 21:27:57 +0200 Subject: [PATCH 373/456] Update proxy examples to use simplified APIs thanks to default loop --- README.md | 14 ++++---------- composer.json | 6 +++--- ...-connect-proxy.php => 11-client-http-proxy.php} | 9 ++++----- examples/12-client-socks-proxy.php | 10 ++++++---- examples/13-client-ssh-proxy.php | 9 ++++----- 5 files changed, 21 insertions(+), 27 deletions(-) rename examples/{11-client-http-connect-proxy.php => 11-client-http-proxy.php} (70%) diff --git a/README.md b/README.md index 63865d5c..5d44199c 100644 --- a/README.md +++ b/README.md @@ -598,10 +598,7 @@ to HTTPS port`443` only, this can technically be used to tunnel any TCP/IP-based protocol, such as plain HTTP and TLS-encrypted HTTPS. ```php -$proxy = new Clue\React\HttpProxy\ProxyConnector( - 'http://127.0.0.1:8080', - new React\Socket\Connector() -); +$proxy = new Clue\React\HttpProxy\ProxyConnector('127.0.0.1:8080'); $connector = new React\Socket\Connector(array( 'tcp' => $proxy, @@ -611,7 +608,7 @@ $connector = new React\Socket\Connector(array( $browser = new React\Http\Browser($connector); ``` -See also the [HTTP CONNECT proxy example](examples/11-client-http-connect-proxy.php). +See also the [HTTP proxy example](examples/11-client-http-proxy.php). ### SOCKS proxy @@ -625,10 +622,7 @@ address (anonymity) or to circumvent address blocking (geoblocking). While many only, this can technically be used to tunnel any TCP/IP-based protocol. ```php -$proxy = new Clue\React\Socks\Client( - 'socks://127.0.0.1:1080', - new React\Socket\Connector() -); +$proxy = new Clue\React\Socks\Client('127.0.0.1:1080'); $connector = new React\Socket\Connector(array( 'tcp' => $proxy, @@ -657,7 +651,7 @@ from the outside (database behind firewall) and as such can also be used for plain HTTP and TLS-encrypted HTTPS. ```php -$proxy = new Clue\React\SshProxy\SshSocksConnector('me@localhost:22', Loop::get()); +$proxy = new Clue\React\SshProxy\SshSocksConnector('alice@example.com'); $connector = new React\Socket\Connector(array( 'tcp' => $proxy, diff --git a/composer.json b/composer.json index 44387e53..25f96db5 100644 --- a/composer.json +++ b/composer.json @@ -38,9 +38,9 @@ }, "require-dev": { "clue/block-react": "^1.1", - "clue/http-proxy-react": "^1.3", - "clue/reactphp-ssh-proxy": "^1.0", - "clue/socks-react": "^1.0", + "clue/http-proxy-react": "^1.7", + "clue/reactphp-ssh-proxy": "^1.3", + "clue/socks-react": "^1.3", "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35" }, "autoload": { diff --git a/examples/11-client-http-connect-proxy.php b/examples/11-client-http-proxy.php similarity index 70% rename from examples/11-client-http-connect-proxy.php rename to examples/11-client-http-proxy.php index 39b0cbcb..127ee477 100644 --- a/examples/11-client-http-connect-proxy.php +++ b/examples/11-client-http-proxy.php @@ -3,18 +3,17 @@ // not already running a HTTP CONNECT proxy server? // Try LeProxy.org or this: // -// $ php examples/72-server-http-connect-proxy.php 8080 -// $ php examples/11-client-http-connect-proxy.php +// $ php examples/72-server-http-connect-proxy.php 127.0.0.1:8080 +// $ http_proxy=127.0.0.1:8080 php examples/11-client-http-connect-proxy.php -use Clue\React\HttpProxy\ProxyConnector as HttpConnectClient; use Psr\Http\Message\ResponseInterface; use React\Http\Browser; use React\Socket\Connector; require __DIR__ . '/../vendor/autoload.php'; -// create a new HTTP CONNECT proxy client which connects to a HTTP CONNECT proxy server listening on localhost:8080 -$proxy = new HttpConnectClient('127.0.0.1:8080', new Connector()); +// create a new HTTP CONNECT proxy client which connects to a HTTP CONNECT proxy server listening on 127.0.0.1:8080 +$proxy = new Clue\React\HttpProxy\ProxyConnector(getenv('http_proxy') ?: '127.0.0.1:8080'); // create a Browser object that uses the HTTP CONNECT proxy client for connections $connector = new Connector(array( diff --git a/examples/12-client-socks-proxy.php b/examples/12-client-socks-proxy.php index ce020ad8..c3e662a9 100644 --- a/examples/12-client-socks-proxy.php +++ b/examples/12-client-socks-proxy.php @@ -1,17 +1,19 @@ Date: Tue, 17 Aug 2021 20:35:10 +0200 Subject: [PATCH 374/456] Don't run requests through configuration middleware While working on another PR that introduces another configuration middleware it dawned on me that we preferably don't requests through middleware that don't do anything. This change will filter it out those middleware before they are passed into the MiddlewareRunner. While running benchmarks I gained around a 10% requests per second gain when running it against example 63. --- src/HttpServer.php | 8 ++++++++ tests/HttpServerTest.php | 23 +++++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/src/HttpServer.php b/src/HttpServer.php index d5f947d5..95ccc5ff 100644 --- a/src/HttpServer.php +++ b/src/HttpServer.php @@ -247,6 +247,14 @@ public function __construct($requestHandlerOrLoop) $middleware = \array_merge($middleware, $requestHandlers); + /** + * Filter out any configuration middleware, no need to run requests through something that isn't + * doing anything with the request. + */ + $middleware = \array_filter($middleware, function ($handler) { + return !($handler instanceof StreamingRequestMiddleware); + }); + $this->streamingServer = new StreamingServer($loop, new MiddlewareRunner($middleware)); $that = $this; diff --git a/tests/HttpServerTest.php b/tests/HttpServerTest.php index 9a0a789a..9200aa66 100644 --- a/tests/HttpServerTest.php +++ b/tests/HttpServerTest.php @@ -431,4 +431,27 @@ public function testConstructServerWithMemoryLimitDoesLimitConcurrency() $this->assertTrue(is_array($middleware)); $this->assertInstanceOf('React\Http\Middleware\LimitConcurrentRequestsMiddleware', $middleware[0]); } + + public function testConstructFiltersOutConfigurationMiddlewareBefore() + { + $http = new HttpServer(new StreamingRequestMiddleware(), function () { }); + + $ref = new \ReflectionProperty($http, 'streamingServer'); + $ref->setAccessible(true); + + $streamingServer = $ref->getValue($http); + + $ref = new \ReflectionProperty($streamingServer, 'callback'); + $ref->setAccessible(true); + + $middlewareRunner = $ref->getValue($streamingServer); + + $ref = new \ReflectionProperty($middlewareRunner, 'middleware'); + $ref->setAccessible(true); + + $middleware = $ref->getValue($middlewareRunner); + + $this->assertTrue(is_array($middleware)); + $this->assertCount(1, $middleware); + } } From bf32f168b28a12e5666928ee884d591939e448d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 21 Aug 2021 13:39:53 +0200 Subject: [PATCH 375/456] Fix docs for missing `$body` argument for `put()` and `delete()` methods --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5d44199c..7bc96076 100644 --- a/README.md +++ b/README.md @@ -2015,7 +2015,7 @@ $browser->patch($url, array('Content-Length' => '11'), $body); #### put() -The `put(string $url, array $headers = array()): PromiseInterface` method can be used to +The `put(string $url, array $headers = array(), string|ReadableStreamInterface $body = ''): PromiseInterface` method can be used to send an HTTP PUT request. ```php @@ -2049,7 +2049,7 @@ $browser->put($url, array('Content-Length' => '11'), $body); #### delete() -The `delete(string $url, array $headers = array()): PromiseInterface` method can be used to +The `delete(string $url, array $headers = array(), string|ReadableStreamInterface $body = ''): PromiseInterface` method can be used to send an HTTP DELETE request. ```php From 6785514d6cef6304822bd6aeac837cd97c5e8cf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Wed, 25 Aug 2021 16:40:34 +0200 Subject: [PATCH 376/456] Consistently use `$body` argument name for request body --- README.md | 12 ++++++------ src/Browser.php | 24 ++++++++++++------------ 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 7bc96076..ba7c5cf9 100644 --- a/README.md +++ b/README.md @@ -123,10 +123,10 @@ offers several methods that resemble the HTTP protocol methods: ```php $browser->get($url, array $headers = array()); $browser->head($url, array $headers = array()); -$browser->post($url, array $headers = array(), string|ReadableStreamInterface $contents = ''); -$browser->delete($url, array $headers = array(), string|ReadableStreamInterface $contents = ''); -$browser->put($url, array $headers = array(), string|ReadableStreamInterface $contents = ''); -$browser->patch($url, array $headers = array(), string|ReadableStreamInterface $contents = ''); +$browser->post($url, array $headers = array(), string|ReadableStreamInterface $body = ''); +$browser->delete($url, array $headers = array(), string|ReadableStreamInterface $body = ''); +$browser->put($url, array $headers = array(), string|ReadableStreamInterface $body = ''); +$browser->patch($url, array $headers = array(), string|ReadableStreamInterface $body = ''); ``` Each of these methods requires a `$url` and some optional parameters to send an @@ -1921,7 +1921,7 @@ See also [GET request client example](examples/01-client-get-request.php). #### post() -The `post(string $url, array $headers = array(), string|ReadableStreamInterface $contents = ''): PromiseInterface` method can be used to +The `post(string $url, array $headers = array(), string|ReadableStreamInterface $body = ''): PromiseInterface` method can be used to send an HTTP POST request. ```php @@ -1983,7 +1983,7 @@ $browser->head($url)->then(function (Psr\Http\Message\ResponseInterface $respons #### patch() -The `patch(string $url, array $headers = array(), string|ReadableStreamInterface $contents = ''): PromiseInterface` method can be used to +The `patch(string $url, array $headers = array(), string|ReadableStreamInterface $body = ''): PromiseInterface` method can be used to send an HTTP PATCH request. ```php diff --git a/src/Browser.php b/src/Browser.php index 5879b977..657b43b0 100644 --- a/src/Browser.php +++ b/src/Browser.php @@ -162,12 +162,12 @@ public function get($url, array $headers = array()) * * @param string $url URL for the request. * @param array $headers - * @param string|ReadableStreamInterface $contents + * @param string|ReadableStreamInterface $body * @return PromiseInterface */ - public function post($url, array $headers = array(), $contents = '') + public function post($url, array $headers = array(), $body = '') { - return $this->requestMayBeStreaming('POST', $url, $headers, $contents); + return $this->requestMayBeStreaming('POST', $url, $headers, $body); } /** @@ -220,12 +220,12 @@ public function head($url, array $headers = array()) * * @param string $url URL for the request. * @param array $headers - * @param string|ReadableStreamInterface $contents + * @param string|ReadableStreamInterface $body * @return PromiseInterface */ - public function patch($url, array $headers = array(), $contents = '') + public function patch($url, array $headers = array(), $body = '') { - return $this->requestMayBeStreaming('PATCH', $url , $headers, $contents); + return $this->requestMayBeStreaming('PATCH', $url , $headers, $body); } /** @@ -262,12 +262,12 @@ public function patch($url, array $headers = array(), $contents = '') * * @param string $url URL for the request. * @param array $headers - * @param string|ReadableStreamInterface $contents + * @param string|ReadableStreamInterface $body * @return PromiseInterface */ - public function put($url, array $headers = array(), $contents = '') + public function put($url, array $headers = array(), $body = '') { - return $this->requestMayBeStreaming('PUT', $url, $headers, $contents); + return $this->requestMayBeStreaming('PUT', $url, $headers, $body); } /** @@ -281,12 +281,12 @@ public function put($url, array $headers = array(), $contents = '') * * @param string $url URL for the request. * @param array $headers - * @param string|ReadableStreamInterface $contents + * @param string|ReadableStreamInterface $body * @return PromiseInterface */ - public function delete($url, array $headers = array(), $contents = '') + public function delete($url, array $headers = array(), $body = '') { - return $this->requestMayBeStreaming('DELETE', $url, $headers, $contents); + return $this->requestMayBeStreaming('DELETE', $url, $headers, $body); } /** From f0e7249ccf3c6186eb1044f9e78247ce11f564c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 25 Sep 2021 13:38:38 +0200 Subject: [PATCH 377/456] Minor documentation improvements --- README.md | 18 +++++++++++++----- examples/11-client-http-proxy.php | 4 ++-- examples/72-server-http-connect-proxy.php | 2 +- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index ba7c5cf9..0f3919db 100644 --- a/README.md +++ b/README.md @@ -83,10 +83,14 @@ multiple concurrent HTTP requests without blocking. ## Quickstart example -Once [installed](#install), you can use the following code to access a -HTTP webserver and send some simple HTTP GET requests: +Once [installed](#install), you can use the following code to access an +HTTP web server and send some simple HTTP GET requests: ```php +get('http://www.google.com/')->then(function (Psr\Http\Message\ResponseInterface $response) { @@ -97,6 +101,10 @@ $client->get('http://www.google.com/')->then(function (Psr\Http\Message\Response This is an HTTP server which responds with `Hello World!` to every request. ```php + 'text/plain', 'Allow' => 'CONNECT' ), - 'This is a HTTP CONNECT (secure HTTPS) proxy' + 'This is an HTTP CONNECT (secure HTTPS) proxy' ); } From fb82de172af8a460083747ae1fa9253ebea3e68a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 27 Aug 2021 21:38:48 +0200 Subject: [PATCH 378/456] Improve error reporting in examples and documentation --- README.md | 63 ++++++++++++++++--- examples/01-client-get-request.php | 2 + examples/02-client-concurrent-requests.php | 6 ++ examples/03-client-request-any.php | 8 +++ examples/04-client-post-json.php | 4 +- examples/05-client-put-xml.php | 4 +- examples/11-client-http-proxy.php | 4 +- examples/12-client-socks-proxy.php | 4 +- examples/13-client-ssh-proxy.php | 4 +- examples/14-client-unix-domain-sockets.php | 4 +- .../21-client-request-streaming-to-stdout.php | 4 +- .../22-client-stream-upload-from-stdin.php | 4 +- examples/61-server-hello-world-https.php | 4 +- examples/63-server-streaming-request.php | 8 ++- examples/72-server-http-connect-proxy.php | 2 +- examples/91-client-benchmark-download.php | 4 +- examples/92-client-benchmark-upload.php | 2 +- src/Browser.php | 30 ++++++++- 18 files changed, 134 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index ba7c5cf9..b2348039 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,8 @@ $client = new React\Http\Browser(); $client->get('http://www.google.com/')->then(function (Psr\Http\Message\ResponseInterface $response) { var_dump($response->getHeaders(), (string)$response->getBody()); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; }); ``` @@ -171,8 +173,8 @@ $browser->get($url)->then( function (Psr\Http\Message\ResponseInterface $response) { var_dump('Response received', $response); }, - function (Exception $error) { - var_dump('There was an error', $error->getMessage()); + function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; } ); ``` @@ -232,6 +234,8 @@ $browser = $browser->withTimeout(10.0); $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { // response received within 10 seconds maximum var_dump($response->getHeaders()); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; }); ``` @@ -319,6 +323,8 @@ The promise will be fulfilled with the last response from the chain of redirects $browser->get($url, $headers)->then(function (Psr\Http\Message\ResponseInterface $response) { // the final response will end up here var_dump($response->getHeaders()); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; }); ``` @@ -347,6 +353,8 @@ $browser = $browser->withFollowRedirects(false); $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { // any redirects will now end up here var_dump($response->getHeaders()); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; }); ``` @@ -410,6 +418,8 @@ from your side. foreach ($urls as $url) { $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { var_dump($response->getHeaders()); + }, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; }); } ``` @@ -429,6 +439,8 @@ $q = new Clue\React\Mq\Queue(10, null, function ($url) use ($browser) { foreach ($urls as $url) { $q($url)->then(function (Psr\Http\Message\ResponseInterface $response) { var_dump($response->getHeaders()); + }, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; }); } ``` @@ -481,13 +493,15 @@ $browser->requestStreaming('GET', $url)->then(function (Psr\Http\Message\Respons echo $chunk; }); - $body->on('error', function (Exception $error) { - echo 'Error: ' . $error->getMessage() . PHP_EOL; + $body->on('error', function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; }); $body->on('close', function () { echo '[DONE]' . PHP_EOL; }); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; }); ``` @@ -549,6 +563,9 @@ $stream = download($browser, $url); $stream->on('data', function ($data) { echo $data; }); +$stream->on('error', function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; +}); ``` See also the [`requestStreaming()`](#requeststreaming) method for more details. @@ -565,6 +582,8 @@ to the [request methods](#request-methods) like this: ```php $browser->post($url, array(), $stream)->then(function (Psr\Http\Message\ResponseInterface $response) { echo 'Successfully sent.'; +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; }); ``` @@ -683,6 +702,8 @@ $browser = new React\Http\Browser($connector); $client->get('http://localhost/info')->then(function (Psr\Http\Message\ResponseInterface $response) { var_dump($response->getHeaders(), (string)$response->getBody()); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; }); ``` @@ -1185,13 +1206,13 @@ $http = new React\Http\HttpServer( }); // an error occures e.g. on invalid chunked encoded data or an unexpected 'end' event - $body->on('error', function (\Exception $exception) use ($resolve, &$bytes) { + $body->on('error', function (Exception $e) use ($resolve, &$bytes) { $resolve(new React\Http\Message\Response( 400, array( 'Content-Type' => 'text/plain' ), - "Encountered error after $bytes bytes: {$exception->getMessage()}\n" + "Encountered error after $bytes bytes: {$e->getMessage()}\n" )); }); }); @@ -1914,6 +1935,8 @@ send an HTTP GET request. ```php $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { var_dump((string)$response->getBody()); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; }); ``` @@ -1933,6 +1956,8 @@ $browser->post( json_encode($data) )->then(function (Psr\Http\Message\ResponseInterface $response) { var_dump(json_decode((string)$response->getBody())); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; }); ``` @@ -1978,6 +2003,8 @@ send an HTTP HEAD request. ```php $browser->head($url)->then(function (Psr\Http\Message\ResponseInterface $response) { var_dump($response->getHeaders()); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; }); ``` @@ -1995,6 +2022,8 @@ $browser->patch( json_encode($data) )->then(function (Psr\Http\Message\ResponseInterface $response) { var_dump(json_decode((string)$response->getBody())); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; }); ``` @@ -2027,6 +2056,8 @@ $browser->put( $xml->asXML() )->then(function (Psr\Http\Message\ResponseInterface $response) { var_dump((string)$response->getBody()); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; }); ``` @@ -2055,6 +2086,8 @@ send an HTTP DELETE request. ```php $browser->delete($url)->then(function (Psr\Http\Message\ResponseInterface $response) { var_dump((string)$response->getBody()); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; }); ``` @@ -2073,6 +2106,8 @@ can use this method: ```php $browser->request('OPTIONS', $url)->then(function (Psr\Http\Message\ResponseInterface $response) { var_dump((string)$response->getBody()); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; }); ``` @@ -2124,13 +2159,15 @@ $browser->requestStreaming('GET', $url)->then(function (Psr\Http\Message\Respons echo $chunk; }); - $body->on('error', function (Exception $error) { - echo 'Error: ' . $error->getMessage() . PHP_EOL; + $body->on('error', function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; }); $body->on('close', function () { echo '[DONE]' . PHP_EOL; }); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; }); ``` @@ -2209,6 +2246,8 @@ $browser = $browser->withFollowRedirects(0); $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { // only non-redirected responses will now end up here var_dump($response->getHeaders()); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; }); ``` @@ -2222,6 +2261,8 @@ $browser = $browser->withFollowRedirects(false); $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { // any redirects will now end up here var_dump($response->getHeaderLine('Location')); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; }); ``` @@ -2253,6 +2294,8 @@ $browser = $browser->withRejectErrorResponse(false); $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { // any HTTP response will now end up here var_dump($response->getStatusCode(), $response->getReasonPhrase()); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; }); ``` @@ -2272,7 +2315,7 @@ $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response $response = $e->getResponse(); var_dump($response->getStatusCode(), $response->getReasonPhrase()); } else { - var_dump($e->getMessage()); + echo 'Error: ' . $e->getMessage() . PHP_EOL; } }); ``` @@ -2362,6 +2405,8 @@ $browser = $browser->withResponseBuffer(1024 * 1024); $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { // response body will not exceed 1 MiB var_dump($response->getHeaders(), (string) $response->getBody()); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; }); ``` diff --git a/examples/01-client-get-request.php b/examples/01-client-get-request.php index 8e232398..34a79bbb 100644 --- a/examples/01-client-get-request.php +++ b/examples/01-client-get-request.php @@ -9,4 +9,6 @@ $client->get('http://google.com/')->then(function (ResponseInterface $response) { var_dump($response->getHeaders(), (string)$response->getBody()); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; }); diff --git a/examples/02-client-concurrent-requests.php b/examples/02-client-concurrent-requests.php index dca1d9c1..7b1b77a0 100644 --- a/examples/02-client-concurrent-requests.php +++ b/examples/02-client-concurrent-requests.php @@ -9,12 +9,18 @@ $client->head('http://www.github.com/clue/http-react')->then(function (ResponseInterface $response) { var_dump($response->getHeaders(), (string)$response->getBody()); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; }); $client->get('http://google.com/')->then(function (ResponseInterface $response) { var_dump($response->getHeaders(), (string)$response->getBody()); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; }); $client->get('http://www.lueck.tv/psocksd')->then(function (ResponseInterface $response) { var_dump($response->getHeaders(), (string)$response->getBody()); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; }); diff --git a/examples/03-client-request-any.php b/examples/03-client-request-any.php index a3bd2831..0c96b684 100644 --- a/examples/03-client-request-any.php +++ b/examples/03-client-request-any.php @@ -26,4 +26,12 @@ var_dump($response->getHeaders()); echo PHP_EOL . $response->getBody(); +}, function ($e) { + // Promise v1 and v2 reject with an array of Exceptions here, Promise v3 will use an Exception object instead + if (is_array($e)) { + $e = end($e); + } + assert($e instanceof Exception); + + echo 'Error: ' . $e->getMessage() . PHP_EOL; }); diff --git a/examples/04-client-post-json.php b/examples/04-client-post-json.php index 400b1a13..b01ada13 100644 --- a/examples/04-client-post-json.php +++ b/examples/04-client-post-json.php @@ -23,4 +23,6 @@ json_encode($data) )->then(function (ResponseInterface $response) { echo (string)$response->getBody(); -}, 'printf'); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; +}); diff --git a/examples/05-client-put-xml.php b/examples/05-client-put-xml.php index 05804f23..231e2ca4 100644 --- a/examples/05-client-put-xml.php +++ b/examples/05-client-put-xml.php @@ -20,4 +20,6 @@ $xml->asXML() )->then(function (ResponseInterface $response) { echo (string)$response->getBody(); -}, 'printf'); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; +}); diff --git a/examples/11-client-http-proxy.php b/examples/11-client-http-proxy.php index 127ee477..65eb8d0f 100644 --- a/examples/11-client-http-proxy.php +++ b/examples/11-client-http-proxy.php @@ -26,4 +26,6 @@ // demo fetching HTTP headers (or bail out otherwise) $browser->get('https://www.google.com/')->then(function (ResponseInterface $response) { echo RingCentral\Psr7\str($response); -}, 'printf'); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; +}); diff --git a/examples/12-client-socks-proxy.php b/examples/12-client-socks-proxy.php index c3e662a9..ecedf242 100644 --- a/examples/12-client-socks-proxy.php +++ b/examples/12-client-socks-proxy.php @@ -26,4 +26,6 @@ // demo fetching HTTP headers (or bail out otherwise) $browser->get('https://www.google.com/')->then(function (ResponseInterface $response) { echo RingCentral\Psr7\str($response); -}, 'printf'); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; +}); diff --git a/examples/13-client-ssh-proxy.php b/examples/13-client-ssh-proxy.php index 54bb9208..64d0c282 100644 --- a/examples/13-client-ssh-proxy.php +++ b/examples/13-client-ssh-proxy.php @@ -22,4 +22,6 @@ // demo fetching HTTP headers (or bail out otherwise) $browser->get('https://www.google.com/')->then(function (ResponseInterface $response) { echo RingCentral\Psr7\str($response); -}, 'printf'); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; +}); diff --git a/examples/14-client-unix-domain-sockets.php b/examples/14-client-unix-domain-sockets.php index 36bfecda..e9718141 100644 --- a/examples/14-client-unix-domain-sockets.php +++ b/examples/14-client-unix-domain-sockets.php @@ -19,4 +19,6 @@ // demo fetching HTTP headers (or bail out otherwise) $browser->get('http://localhost/info')->then(function (ResponseInterface $response) { echo Psr7\str($response); -}, 'printf'); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; +}); diff --git a/examples/21-client-request-streaming-to-stdout.php b/examples/21-client-request-streaming-to-stdout.php index 3d2110a2..2f24d035 100644 --- a/examples/21-client-request-streaming-to-stdout.php +++ b/examples/21-client-request-streaming-to-stdout.php @@ -27,4 +27,6 @@ $body = $response->getBody(); assert($body instanceof ReadableStreamInterface); $body->pipe($out); -}, 'printf'); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; +}); diff --git a/examples/22-client-stream-upload-from-stdin.php b/examples/22-client-stream-upload-from-stdin.php index a0857feb..b00fbc5e 100644 --- a/examples/22-client-stream-upload-from-stdin.php +++ b/examples/22-client-stream-upload-from-stdin.php @@ -21,4 +21,6 @@ $client->post($url, array(), $in)->then(function (ResponseInterface $response) { echo 'Received' . PHP_EOL . Psr7\str($response); -}, 'printf'); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; +}); diff --git a/examples/61-server-hello-world-https.php b/examples/61-server-hello-world-https.php index e5e0ed84..01182fdd 100644 --- a/examples/61-server-hello-world-https.php +++ b/examples/61-server-hello-world-https.php @@ -23,6 +23,8 @@ )); $http->listen($socket); -$socket->on('error', 'printf'); +$socket->on('error', function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; +}); echo 'Listening on ' . str_replace('tls:', 'https:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/63-server-streaming-request.php b/examples/63-server-streaming-request.php index 2b5f8a6c..c1e6ac89 100644 --- a/examples/63-server-streaming-request.php +++ b/examples/63-server-streaming-request.php @@ -29,20 +29,22 @@ function (Psr\Http\Message\ServerRequestInterface $request) { }); // an error occures e.g. on invalid chunked encoded data or an unexpected 'end' event - $body->on('error', function (\Exception $exception) use ($resolve, &$bytes) { + $body->on('error', function (Exception $e) use ($resolve, &$bytes) { $resolve(new React\Http\Message\Response( 400, array( 'Content-Type' => 'text/plain' ), - "Encountered error after $bytes bytes: {$exception->getMessage()}\n" + "Encountered error after $bytes bytes: {$e->getMessage()}\n" )); }); }); } ); -$http->on('error', 'printf'); +$http->on('error', function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; +}); $socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); $http->listen($socket); diff --git a/examples/72-server-http-connect-proxy.php b/examples/72-server-http-connect-proxy.php index 205dd1da..58143482 100644 --- a/examples/72-server-http-connect-proxy.php +++ b/examples/72-server-http-connect-proxy.php @@ -38,7 +38,7 @@ function (ConnectionInterface $remote) { $remote ); }, - function ($e) { + function (Exception $e) { return new Response( 502, array( diff --git a/examples/91-client-benchmark-download.php b/examples/91-client-benchmark-download.php index 2f76a3f3..49693baf 100644 --- a/examples/91-client-benchmark-download.php +++ b/examples/91-client-benchmark-download.php @@ -55,4 +55,6 @@ echo "\r" . 'Downloaded ' . $bytes . ' bytes in ' . round($time, 3) . 's => ' . round($bytes / $time / 1000000, 1) . ' MB/s' . PHP_EOL; }); -}, 'printf'); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; +}); diff --git a/examples/92-client-benchmark-upload.php b/examples/92-client-benchmark-upload.php index bd767966..9fa1848a 100644 --- a/examples/92-client-benchmark-upload.php +++ b/examples/92-client-benchmark-upload.php @@ -116,7 +116,7 @@ public function getPosition() printf("\r%d bytes in %0.3fs => %.1f MB/s\n", $source->getPosition(), $now - $start, $source->getPosition() / ($now - $start) / 1000000); echo rtrim(preg_replace('/x{5,}/','x…', (string) $response->getBody()), PHP_EOL) . PHP_EOL; -}, function ($e) use ($report) { +}, function (Exception $e) use ($report) { Loop::cancelTimer($report); echo 'Error: ' . $e->getMessage() . PHP_EOL; }); diff --git a/src/Browser.php b/src/Browser.php index 657b43b0..72847f66 100644 --- a/src/Browser.php +++ b/src/Browser.php @@ -97,6 +97,8 @@ public function __construct($connector = null, $loop = null) * ```php * $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { * var_dump((string)$response->getBody()); + * }, function (Exception $e) { + * echo 'Error: ' . $e->getMessage() . PHP_EOL; * }); * ``` * @@ -123,6 +125,8 @@ public function get($url, array $headers = array()) * json_encode($data) * )->then(function (Psr\Http\Message\ResponseInterface $response) { * var_dump(json_decode((string)$response->getBody())); + * }, function (Exception $e) { + * echo 'Error: ' . $e->getMessage() . PHP_EOL; * }); * ``` * @@ -176,6 +180,8 @@ public function post($url, array $headers = array(), $body = '') * ```php * $browser->head($url)->then(function (Psr\Http\Message\ResponseInterface $response) { * var_dump($response->getHeaders()); + * }, function (Exception $e) { + * echo 'Error: ' . $e->getMessage() . PHP_EOL; * }); * ``` * @@ -200,6 +206,8 @@ public function head($url, array $headers = array()) * json_encode($data) * )->then(function (Psr\Http\Message\ResponseInterface $response) { * var_dump(json_decode((string)$response->getBody())); + * }, function (Exception $e) { + * echo 'Error: ' . $e->getMessage() . PHP_EOL; * }); * ``` * @@ -240,6 +248,8 @@ public function patch($url, array $headers = array(), $body = '') * $xml->asXML() * )->then(function (Psr\Http\Message\ResponseInterface $response) { * var_dump((string)$response->getBody()); + * }, function (Exception $e) { + * echo 'Error: ' . $e->getMessage() . PHP_EOL; * }); * ``` * @@ -276,6 +286,8 @@ public function put($url, array $headers = array(), $body = '') * ```php * $browser->delete($url)->then(function (Psr\Http\Message\ResponseInterface $response) { * var_dump((string)$response->getBody()); + * }, function (Exception $e) { + * echo 'Error: ' . $e->getMessage() . PHP_EOL; * }); * ``` * @@ -302,6 +314,8 @@ public function delete($url, array $headers = array(), $body = '') * ```php * $browser->request('OPTIONS', $url)->then(function (Psr\Http\Message\ResponseInterface $response) { * var_dump((string)$response->getBody()); + * }, function (Exception $e) { + * echo 'Error: ' . $e->getMessage() . PHP_EOL; * }); * ``` * @@ -362,13 +376,15 @@ public function request($method, $url, array $headers = array(), $body = '') * echo $chunk; * }); * - * $body->on('error', function (Exception $error) { - * echo 'Error: ' . $error->getMessage() . PHP_EOL; + * $body->on('error', function (Exception $e) { + * echo 'Error: ' . $e->getMessage() . PHP_EOL; * }); * * $body->on('close', function () { * echo '[DONE]' . PHP_EOL; * }); + * }, function (Exception $e) { + * echo 'Error: ' . $e->getMessage() . PHP_EOL; * }); * ``` * @@ -472,6 +488,8 @@ public function withTimeout($timeout) * $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { * // only non-redirected responses will now end up here * var_dump($response->getHeaders()); + * }, function (Exception $e) { + * echo 'Error: ' . $e->getMessage() . PHP_EOL; * }); * ``` * @@ -485,6 +503,8 @@ public function withTimeout($timeout) * $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { * // any redirects will now end up here * var_dump($response->getHeaderLine('Location')); + * }, function (Exception $e) { + * echo 'Error: ' . $e->getMessage() . PHP_EOL; * }); * ``` * @@ -525,6 +545,8 @@ public function withFollowRedirects($followRedirects) * $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { * // any HTTP response will now end up here * var_dump($response->getStatusCode(), $response->getReasonPhrase()); + * }, function (Exception $e) { + * echo 'Error: ' . $e->getMessage() . PHP_EOL; * }); * ``` * @@ -544,7 +566,7 @@ public function withFollowRedirects($followRedirects) * $response = $e->getResponse(); * var_dump($response->getStatusCode(), $response->getReasonPhrase()); * } else { - * var_dump($e->getMessage()); + * echo 'Error: ' . $e->getMessage() . PHP_EOL; * } * }); * ``` @@ -675,6 +697,8 @@ public function withProtocolVersion($protocolVersion) * $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { * // response body will not exceed 1 MiB * var_dump($response->getHeaders(), (string) $response->getBody()); + * }, function (Exception $e) { + * echo 'Error: ' . $e->getMessage() . PHP_EOL; * }); * ``` * From eba1e1e976f971734909d4d228f7d3825678404a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 4 Nov 2021 16:41:55 +0100 Subject: [PATCH 379/456] Explicitly close streaming response body when body MUST be empty --- src/Io/StreamingServer.php | 1 + tests/Io/StreamingServerTest.php | 110 ++++++++++++++++++++++++++++++- 2 files changed, 109 insertions(+), 2 deletions(-) diff --git a/src/Io/StreamingServer.php b/src/Io/StreamingServer.php index 2c912dfa..0a35896d 100644 --- a/src/Io/StreamingServer.php +++ b/src/Io/StreamingServer.php @@ -330,6 +330,7 @@ public function handleResponse(ConnectionInterface $connection, ServerRequestInt // response to HEAD and 1xx, 204 and 304 responses MUST NOT include a body // exclude status 101 (Switching Protocols) here for Upgrade request handling above if ($method === 'HEAD' || $code === 100 || ($code > 101 && $code < 200) || $code === 204 || $code === 304) { + $body->close(); $body = ''; } diff --git a/tests/Io/StreamingServerTest.php b/tests/Io/StreamingServerTest.php index 0311304e..a541de9f 100644 --- a/tests/Io/StreamingServerTest.php +++ b/tests/Io/StreamingServerTest.php @@ -765,7 +765,7 @@ function ($data) use (&$buffer) { $this->assertEquals('', $buffer); } - public function testRespomseBodyStreamAlreadyClosedWillSendEmptyBodyChunkedEncoded() + public function testResponseBodyStreamAlreadyClosedWillSendEmptyBodyChunkedEncoded() { $stream = new ThroughStream(); $stream->close(); @@ -1255,9 +1255,45 @@ function ($data) use (&$buffer) { $this->connection->emit('data', array($data)); $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); + $this->assertContainsString("\r\nContent-Length: 3\r\n", $buffer); $this->assertNotContainsString("bye", $buffer); } + public function testResponseContainsNoResponseBodyForHeadRequestWithStreamingResponse() + { + $stream = new ThroughStream(); + $stream->on('close', $this->expectCallableOnce()); + + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($stream) { + return new Response( + 200, + array('Content-Length' => '3'), + $stream + ); + }); + + $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\nHost: localhost\r\n\r\n"; + $this->connection->emit('data', array($data)); + + $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); + $this->assertContainsString("\r\nContent-Length: 3\r\n", $buffer); + } + public function testResponseContainsNoResponseBodyAndNoContentLengthForNoContentStatus() { $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { @@ -1287,10 +1323,45 @@ function ($data) use (&$buffer) { $this->connection->emit('data', array($data)); $this->assertContainsString("HTTP/1.1 204 No Content\r\n", $buffer); - $this->assertNotContainsString("\r\n\Content-Length: 3\r\n", $buffer); + $this->assertNotContainsString("\r\nContent-Length: 3\r\n", $buffer); $this->assertNotContainsString("bye", $buffer); } + public function testResponseContainsNoResponseBodyAndNoContentLengthForNoContentStatusResponseWithStreamingBody() + { + $stream = new ThroughStream(); + $stream->on('close', $this->expectCallableOnce()); + + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($stream) { + return new Response( + 204, + array('Content-Length' => '3'), + $stream + ); + }); + + $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\nHost: localhost\r\n\r\n"; + $this->connection->emit('data', array($data)); + + $this->assertContainsString("HTTP/1.1 204 No Content\r\n", $buffer); + $this->assertNotContainsString("\r\nContent-Length: 3\r\n", $buffer); + } + public function testResponseContainsNoResponseBodyForNotModifiedStatus() { $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { @@ -1324,6 +1395,41 @@ function ($data) use (&$buffer) { $this->assertNotContainsString("bye", $buffer); } + public function testResponseContainsNoResponseBodyForNotModifiedStatusWithStreamingBody() + { + $stream = new ThroughStream(); + $stream->on('close', $this->expectCallableOnce()); + + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($stream) { + return new Response( + 304, + array('Content-Length' => '3'), + $stream + ); + }); + + $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\r\n\r\n"; + $this->connection->emit('data', array($data)); + + $this->assertContainsString("HTTP/1.1 304 Not Modified\r\n", $buffer); + $this->assertContainsString("\r\nContent-Length: 3\r\n", $buffer); + } + public function testRequestInvalidHttpProtocolVersionWillEmitErrorAndSendErrorResponse() { $error = null; From 5f795a0d6d97dc624c5a9a79308d6e28f59db0d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 5 Nov 2021 08:57:40 +0100 Subject: [PATCH 380/456] Improve assigning `Content-Length` for `304 Not Modified` response --- src/Io/StreamingServer.php | 16 +++++--- tests/Io/StreamingServerTest.php | 64 ++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 6 deletions(-) diff --git a/src/Io/StreamingServer.php b/src/Io/StreamingServer.php index 0a35896d..5f9632e9 100644 --- a/src/Io/StreamingServer.php +++ b/src/Io/StreamingServer.php @@ -260,23 +260,27 @@ public function handleResponse(ConnectionInterface $connection, ServerRequestInt $response = $response->withoutHeader('Date'); } - // assign "Content-Length" and "Transfer-Encoding" headers automatically + // assign "Content-Length" header automatically $chunked = false; if (($method === 'CONNECT' && $code >= 200 && $code < 300) || ($code >= 100 && $code < 200) || $code === 204) { // 2xx response to CONNECT and 1xx and 204 MUST NOT include Content-Length or Transfer-Encoding header - $response = $response->withoutHeader('Content-Length')->withoutHeader('Transfer-Encoding'); + $response = $response->withoutHeader('Content-Length'); + } elseif ($code === 304 && ($response->hasHeader('Content-Length') || $body->getSize() === 0)) { + // 304 Not Modified: preserve explicit Content-Length and preserve missing header if body is empty } elseif ($body->getSize() !== null) { // assign Content-Length header when using a "normal" buffered body string - $response = $response->withHeader('Content-Length', (string)$body->getSize())->withoutHeader('Transfer-Encoding'); + $response = $response->withHeader('Content-Length', (string)$body->getSize()); } elseif (!$response->hasHeader('Content-Length') && $version === '1.1') { // assign chunked transfer-encoding if no 'content-length' is given for HTTP/1.1 responses - $response = $response->withHeader('Transfer-Encoding', 'chunked'); $chunked = true; + } + + // assign "Transfer-Encoding" header automatically + if ($chunked) { + $response = $response->withHeader('Transfer-Encoding', 'chunked'); } else { // remove any Transfer-Encoding headers unless automatically enabled above - // we do not want to keep connection alive, so pretend we received "Connection: close" request header $response = $response->withoutHeader('Transfer-Encoding'); - $request = $request->withHeader('Connection', 'close'); } // assign "Connection" header automatically diff --git a/tests/Io/StreamingServerTest.php b/tests/Io/StreamingServerTest.php index a541de9f..ccafe338 100644 --- a/tests/Io/StreamingServerTest.php +++ b/tests/Io/StreamingServerTest.php @@ -1362,6 +1362,70 @@ function ($data) use (&$buffer) { $this->assertNotContainsString("\r\nContent-Length: 3\r\n", $buffer); } + public function testResponseContainsNoContentLengthHeaderForNotModifiedStatus() + { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { + return new Response( + 304, + array(), + '' + ); + }); + + $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\r\n\r\n"; + $this->connection->emit('data', array($data)); + + $this->assertContainsString("HTTP/1.1 304 Not Modified\r\n", $buffer); + $this->assertNotContainsString("\r\nContent-Length: 0\r\n", $buffer); + } + + public function testResponseContainsExplicitContentLengthHeaderForNotModifiedStatus() + { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { + return new Response( + 304, + array('Content-Length' => 3), + '' + ); + }); + + $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\r\n\r\n"; + $this->connection->emit('data', array($data)); + + $this->assertContainsString("HTTP/1.1 304 Not Modified\r\n", $buffer); + $this->assertContainsString("\r\nContent-Length: 3\r\n", $buffer); + } + public function testResponseContainsNoResponseBodyForNotModifiedStatus() { $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { From 9ed03c4fcc0573b9c9043b2f4824ed22fe9ef845 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 3 Dec 2020 09:43:36 +0100 Subject: [PATCH 381/456] Internal refactoring and improvement to hold message body in memory --- src/Io/BufferedBody.php | 3 +++ src/Io/MultipartParser.php | 9 ++++----- src/Io/Transaction.php | 2 +- src/Message/Response.php | 9 ++++++--- src/Message/ServerRequest.php | 7 +++++-- tests/Io/UploadedFileTest.php | 10 +++++----- tests/Message/ResponseTest.php | 12 ++++++++---- 7 files changed, 32 insertions(+), 20 deletions(-) diff --git a/src/Io/BufferedBody.php b/src/Io/BufferedBody.php index 2f81bc56..4a4d8393 100644 --- a/src/Io/BufferedBody.php +++ b/src/Io/BufferedBody.php @@ -15,6 +15,9 @@ class BufferedBody implements StreamInterface private $position = 0; private $closed = false; + /** + * @param string $buffer + */ public function __construct($buffer) { $this->buffer = $buffer; diff --git a/src/Io/MultipartParser.php b/src/Io/MultipartParser.php index d868ca88..536694fd 100644 --- a/src/Io/MultipartParser.php +++ b/src/Io/MultipartParser.php @@ -3,7 +3,6 @@ namespace React\Http\Io; use Psr\Http\Message\ServerRequestInterface; -use RingCentral\Psr7; /** * [Internal] Parses a string body with "Content-Type: multipart/form-data" into structured data @@ -190,7 +189,7 @@ private function parseUploadedFile($filename, $contentType, $contents) } return new UploadedFile( - Psr7\stream_for(), + new BufferedBody(''), $size, \UPLOAD_ERR_NO_FILE, $filename, @@ -206,7 +205,7 @@ private function parseUploadedFile($filename, $contentType, $contents) // file exceeds "upload_max_filesize" ini setting if ($size > $this->uploadMaxFilesize) { return new UploadedFile( - Psr7\stream_for(), + new BufferedBody(''), $size, \UPLOAD_ERR_INI_SIZE, $filename, @@ -217,7 +216,7 @@ private function parseUploadedFile($filename, $contentType, $contents) // file exceeds MAX_FILE_SIZE value if ($this->maxFileSize !== null && $size > $this->maxFileSize) { return new UploadedFile( - Psr7\stream_for(), + new BufferedBody(''), $size, \UPLOAD_ERR_FORM_SIZE, $filename, @@ -226,7 +225,7 @@ private function parseUploadedFile($filename, $contentType, $contents) } return new UploadedFile( - Psr7\stream_for($contents), + new BufferedBody($contents), $size, \UPLOAD_ERR_OK, $filename, diff --git a/src/Io/Transaction.php b/src/Io/Transaction.php index 9449503f..7bf7008f 100644 --- a/src/Io/Transaction.php +++ b/src/Io/Transaction.php @@ -188,7 +188,7 @@ public function bufferResponse(ResponseInterface $response, $deferred) $maximumSize = $this->maximumSize; $promise = \React\Promise\Stream\buffer($stream, $maximumSize)->then( function ($body) use ($response) { - return $response->withBody(\RingCentral\Psr7\stream_for($body)); + return $response->withBody(new BufferedBody($body)); }, function ($e) use ($stream, $maximumSize) { // try to close stream if buffering fails (or is cancelled) diff --git a/src/Message/Response.php b/src/Message/Response.php index 5d799c58..a9710170 100644 --- a/src/Message/Response.php +++ b/src/Message/Response.php @@ -2,10 +2,11 @@ namespace React\Http\Message; +use Psr\Http\Message\StreamInterface; +use React\Http\Io\BufferedBody; use React\Http\Io\HttpBodyStream; use React\Stream\ReadableStreamInterface; use RingCentral\Psr7\Response as Psr7Response; -use Psr\Http\Message\StreamInterface; /** * Represents an outgoing server response message. @@ -48,9 +49,11 @@ public function __construct( $version = '1.1', $reason = null ) { - if ($body instanceof ReadableStreamInterface && !$body instanceof StreamInterface) { + if (\is_string($body)) { + $body = new BufferedBody($body); + } elseif ($body instanceof ReadableStreamInterface && !$body instanceof StreamInterface) { $body = new HttpBodyStream($body, null); - } elseif (!\is_string($body) && !$body instanceof StreamInterface) { + } elseif (!$body instanceof StreamInterface) { throw new \InvalidArgumentException('Invalid response body given'); } diff --git a/src/Message/ServerRequest.php b/src/Message/ServerRequest.php index 5c01819b..f446f24e 100644 --- a/src/Message/ServerRequest.php +++ b/src/Message/ServerRequest.php @@ -5,6 +5,7 @@ use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\StreamInterface; use Psr\Http\Message\UriInterface; +use React\Http\Io\BufferedBody; use React\Http\Io\HttpBodyStream; use React\Stream\ReadableStreamInterface; use RingCentral\Psr7\Request; @@ -57,10 +58,12 @@ public function __construct( $serverParams = array() ) { $stream = null; - if ($body instanceof ReadableStreamInterface && !$body instanceof StreamInterface) { + if (\is_string($body)) { + $body = new BufferedBody($body); + } elseif ($body instanceof ReadableStreamInterface && !$body instanceof StreamInterface) { $stream = $body; $body = null; - } elseif (!\is_string($body) && !$body instanceof StreamInterface) { + } elseif (!$body instanceof StreamInterface) { throw new \InvalidArgumentException('Invalid server request body given'); } diff --git a/tests/Io/UploadedFileTest.php b/tests/Io/UploadedFileTest.php index 9ff623da..4e9c0dd5 100644 --- a/tests/Io/UploadedFileTest.php +++ b/tests/Io/UploadedFileTest.php @@ -2,9 +2,9 @@ namespace React\Tests\Http\Io; +use React\Http\Io\BufferedBody; use React\Http\Io\UploadedFile; Use React\Tests\Http\TestCase; -use RingCentral\Psr7\BufferStream; class UploadedFileTest extends TestCase { @@ -23,7 +23,7 @@ public function failtyErrorProvider() */ public function testFailtyError($error) { - $stream = new BufferStream(); + $stream = new BufferedBody(''); $this->setExpectedException('InvalidArgumentException', 'Invalid error code, must be an UPLOAD_ERR_* constant'); new UploadedFile($stream, 0, $error, 'foo.bar', 'foo/bar'); @@ -31,7 +31,7 @@ public function testFailtyError($error) public function testNoMoveFile() { - $stream = new BufferStream(); + $stream = new BufferedBody(''); $uploadedFile = new UploadedFile($stream, 0, UPLOAD_ERR_OK, 'foo.bar', 'foo/bar'); $this->setExpectedException('RuntimeException', 'Not implemented'); @@ -40,7 +40,7 @@ public function testNoMoveFile() public function testGetters() { - $stream = new BufferStream(); + $stream = new BufferedBody(''); $uploadedFile = new UploadedFile($stream, 0, UPLOAD_ERR_OK, 'foo.bar', 'foo/bar'); self::assertSame($stream, $uploadedFile->getStream()); self::assertSame(0, $uploadedFile->getSize()); @@ -51,7 +51,7 @@ public function testGetters() public function testGetStreamOnFailedUpload() { - $stream = new BufferStream(); + $stream = new BufferedBody(''); $uploadedFile = new UploadedFile($stream, 0, UPLOAD_ERR_NO_FILE, 'foo.bar', 'foo/bar'); $this->setExpectedException('RuntimeException', 'Cannot retrieve stream due to upload error'); diff --git a/tests/Message/ResponseTest.php b/tests/Message/ResponseTest.php index 457618e9..3c6ad3fd 100644 --- a/tests/Message/ResponseTest.php +++ b/tests/Message/ResponseTest.php @@ -9,18 +9,22 @@ class ResponseTest extends TestCase { - - public function testStringBodyWillBePsr7Stream() + public function testConstructWithStringBodyWillReturnStreamInstance() { $response = new Response(200, array(), 'hello'); - $this->assertInstanceOf('RingCentral\Psr7\Stream', $response->getBody()); + $body = $response->getBody(); + + /** @var \Psr\Http\Message\StreamInterface $body */ + $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $body); + $this->assertEquals('hello', (string) $body); } public function testConstructWithStreamingBodyWillReturnReadableBodyStream() { $response = new Response(200, array(), new ThroughStream()); - $body = $response->getBody(); + + /** @var \Psr\Http\Message\StreamInterface $body */ $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $body); $this->assertInstanceof('React\Stream\ReadableStreamInterface', $body); $this->assertInstanceOf('React\Http\Io\HttpBodyStream', $body); From fc1cf25b7187bbcb64509fee24888ab602b23c18 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Mon, 12 Apr 2021 23:20:36 +0200 Subject: [PATCH 382/456] Introduce using PSR-7 HTTP Status code constants Introducing using these constants makes it easier to identify a specific HTTP status used in our code. And for our users to use readable status codes in their own code. --- composer.json | 3 ++- examples/51-server-hello-world.php | 3 ++- examples/52-server-count-visitors.php | 3 ++- examples/53-server-whatsmyip.php | 3 ++- examples/54-server-query-parameter.php | 3 ++- examples/55-server-cookie-handling.php | 5 +++-- examples/56-server-sleep.php | 3 ++- examples/57-server-error-handling.php | 3 ++- examples/58-server-stream-response.php | 5 +++-- examples/59-server-json-api.php | 9 +++++---- examples/61-server-hello-world-https.php | 3 ++- examples/62-server-form-upload.php | 3 ++- examples/63-server-streaming-request.php | 6 ++++-- examples/71-server-http-proxy.php | 5 +++-- examples/72-server-http-connect-proxy.php | 7 ++++--- examples/81-server-upgrade-echo.php | 5 +++-- examples/82-server-upgrade-chat.php | 5 +++-- examples/99-server-benchmark-download.php | 7 ++++--- src/Io/RequestHeaderParser.php | 11 ++++++----- src/Io/Sender.php | 3 ++- src/Io/StreamingServer.php | 13 +++++++------ src/Message/Response.php | 3 ++- 22 files changed, 67 insertions(+), 44 deletions(-) diff --git a/composer.json b/composer.json index 25f96db5..1939accf 100644 --- a/composer.json +++ b/composer.json @@ -34,7 +34,8 @@ "react/promise-stream": "^1.1", "react/socket": "^1.9", "react/stream": "^1.2", - "ringcentral/psr7": "^1.2" + "ringcentral/psr7": "^1.2", + "fig/http-message-util": "^1.1" }, "require-dev": { "clue/block-react": "^1.1", diff --git a/examples/51-server-hello-world.php b/examples/51-server-hello-world.php index f549ece8..2d9dc766 100644 --- a/examples/51-server-hello-world.php +++ b/examples/51-server-hello-world.php @@ -1,5 +1,6 @@ 'text/plain' ), diff --git a/examples/52-server-count-visitors.php b/examples/52-server-count-visitors.php index d52285d0..8e219ad0 100644 --- a/examples/52-server-count-visitors.php +++ b/examples/52-server-count-visitors.php @@ -1,5 +1,6 @@ 'text/plain' ), diff --git a/examples/53-server-whatsmyip.php b/examples/53-server-whatsmyip.php index 5df1050d..14ad2da8 100644 --- a/examples/53-server-whatsmyip.php +++ b/examples/53-server-whatsmyip.php @@ -1,5 +1,6 @@ getServerParams()['REMOTE_ADDR']; return new Response( - 200, + StatusCodeInterface::STATUS_OK, array( 'Content-Type' => 'text/plain' ), diff --git a/examples/54-server-query-parameter.php b/examples/54-server-query-parameter.php index 22be7566..e3854626 100644 --- a/examples/54-server-query-parameter.php +++ b/examples/54-server-query-parameter.php @@ -1,5 +1,6 @@ 'text/html' ), diff --git a/examples/55-server-cookie-handling.php b/examples/55-server-cookie-handling.php index a6858061..7a093d24 100644 --- a/examples/55-server-cookie-handling.php +++ b/examples/55-server-cookie-handling.php @@ -1,5 +1,6 @@ getCookieParams()[$key]; return new Response( - 200, + StatusCodeInterface::STATUS_OK, array( 'Content-Type' => 'text/plain' ), @@ -21,7 +22,7 @@ } return new Response( - 200, + StatusCodeInterface::STATUS_OK, array( 'Content-Type' => 'text/plain', 'Set-Cookie' => urlencode($key) . '=' . urlencode('test;more') diff --git a/examples/56-server-sleep.php b/examples/56-server-sleep.php index caa22644..cfc805d3 100644 --- a/examples/56-server-sleep.php +++ b/examples/56-server-sleep.php @@ -1,5 +1,6 @@ 'text/plain' ), diff --git a/examples/57-server-error-handling.php b/examples/57-server-error-handling.php index 4a1b6757..72b9c02b 100644 --- a/examples/57-server-error-handling.php +++ b/examples/57-server-error-handling.php @@ -1,5 +1,6 @@ 'text/plain' ), diff --git a/examples/58-server-stream-response.php b/examples/58-server-stream-response.php index 2069b7a8..596ca6fc 100644 --- a/examples/58-server-stream-response.php +++ b/examples/58-server-stream-response.php @@ -1,5 +1,6 @@ getMethod() !== 'GET' || $request->getUri()->getPath() !== '/') { - return new Response(404); + return new Response(StatusCodeInterface::STATUS_NOT_FOUND); } $stream = new ThroughStream(); @@ -30,7 +31,7 @@ }); return new Response( - 200, + StatusCodeInterface::STATUS_OK, array( 'Content-Type' => 'text/plain' ), diff --git a/examples/59-server-json-api.php b/examples/59-server-json-api.php index 7fa8cc66..8a1ba358 100644 --- a/examples/59-server-json-api.php +++ b/examples/59-server-json-api.php @@ -6,6 +6,7 @@ // $ php examples/59-server-json-api.php 8080 // $ curl -v http://localhost:8080/ -H 'Content-Type: application/json' -d '{"name":"Alice"}' +use Fig\Http\Message\StatusCodeInterface; use Psr\Http\Message\ServerRequestInterface; use React\Http\Message\Response; @@ -14,7 +15,7 @@ $http = new React\Http\HttpServer(function (ServerRequestInterface $request) { if ($request->getHeaderLine('Content-Type') !== 'application/json') { return new Response( - 415, // Unsupported Media Type + StatusCodeInterface::STATUS_UNSUPPORTED_MEDIA_TYPE, array( 'Content-Type' => 'application/json' ), @@ -25,7 +26,7 @@ $input = json_decode($request->getBody()->getContents()); if (json_last_error() !== JSON_ERROR_NONE) { return new Response( - 400, // Bad Request + StatusCodeInterface::STATUS_BAD_REQUEST, array( 'Content-Type' => 'application/json' ), @@ -35,7 +36,7 @@ if (!isset($input->name) || !is_string($input->name)) { return new Response( - 422, // Unprocessable Entity + StatusCodeInterface::STATUS_UNPROCESSABLE_ENTITY, array( 'Content-Type' => 'application/json' ), @@ -44,7 +45,7 @@ } return new Response( - 200, + StatusCodeInterface::STATUS_OK, array( 'Content-Type' => 'application/json' ), diff --git a/examples/61-server-hello-world-https.php b/examples/61-server-hello-world-https.php index 01182fdd..f563282e 100644 --- a/examples/61-server-hello-world-https.php +++ b/examples/61-server-hello-world-https.php @@ -1,5 +1,6 @@ 'text/plain' ), diff --git a/examples/62-server-form-upload.php b/examples/62-server-form-upload.php index 6984b4e3..b09f178d 100644 --- a/examples/62-server-form-upload.php +++ b/examples/62-server-form-upload.php @@ -7,6 +7,7 @@ // $ curl --form name=test --form age=30 http://localhost:8080/ // $ curl --form name=hi --form avatar=@avatar.png http://localhost:8080/ +use Fig\Http\Message\StatusCodeInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\UploadedFileInterface; use React\Http\Message\Response; @@ -110,7 +111,7 @@ HTML; return new Response( - 200, + StatusCodeInterface::STATUS_OK, array( 'Content-Type' => 'text/html; charset=UTF-8' ), diff --git a/examples/63-server-streaming-request.php b/examples/63-server-streaming-request.php index c1e6ac89..073d0e0e 100644 --- a/examples/63-server-streaming-request.php +++ b/examples/63-server-streaming-request.php @@ -1,5 +1,7 @@ on('end', function () use ($resolve, &$bytes){ $resolve(new React\Http\Message\Response( - 200, + StatusCodeInterface::STATUS_OK, array( 'Content-Type' => 'text/plain' ), @@ -31,7 +33,7 @@ function (Psr\Http\Message\ServerRequestInterface $request) { // an error occures e.g. on invalid chunked encoded data or an unexpected 'end' event $body->on('error', function (Exception $e) use ($resolve, &$bytes) { $resolve(new React\Http\Message\Response( - 400, + StatusCodeInterface::STATUS_BAD_REQUEST, array( 'Content-Type' => 'text/plain' ), diff --git a/examples/71-server-http-proxy.php b/examples/71-server-http-proxy.php index c4fe244e..3029dd7f 100644 --- a/examples/71-server-http-proxy.php +++ b/examples/71-server-http-proxy.php @@ -3,6 +3,7 @@ // $ php examples/71-server-http-proxy.php 8080 // $ curl -v --proxy http://localhost:8080 http://reactphp.org/ +use Fig\Http\Message\StatusCodeInterface; use Psr\Http\Message\RequestInterface; use React\Http\Message\Response; use RingCentral\Psr7; @@ -16,7 +17,7 @@ $http = new React\Http\HttpServer(function (RequestInterface $request) { if (strpos($request->getRequestTarget(), '://') === false) { return new Response( - 400, + StatusCodeInterface::STATUS_BAD_REQUEST, array( 'Content-Type' => 'text/plain' ), @@ -36,7 +37,7 @@ // left up as an exercise: use an HTTP client to send the outgoing request // and forward the incoming response to the original client request return new Response( - 200, + StatusCodeInterface::STATUS_OK, array( 'Content-Type' => 'text/plain' ), diff --git a/examples/72-server-http-connect-proxy.php b/examples/72-server-http-connect-proxy.php index ac033370..55b03a84 100644 --- a/examples/72-server-http-connect-proxy.php +++ b/examples/72-server-http-connect-proxy.php @@ -3,6 +3,7 @@ // $ php examples/72-server-http-connect-proxy.php 8080 // $ curl -v --proxy http://localhost:8080 https://reactphp.org/ +use Fig\Http\Message\StatusCodeInterface; use Psr\Http\Message\ServerRequestInterface; use React\Http\Message\Response; use React\Socket\Connector; @@ -19,7 +20,7 @@ $http = new React\Http\HttpServer(function (ServerRequestInterface $request) use ($connector) { if ($request->getMethod() !== 'CONNECT') { return new Response( - 405, + StatusCodeInterface::STATUS_METHOD_NOT_ALLOWED, array( 'Content-Type' => 'text/plain', 'Allow' => 'CONNECT' @@ -33,14 +34,14 @@ function (ConnectionInterface $remote) { // connection established => forward data return new Response( - 200, + StatusCodeInterface::STATUS_OK, array(), $remote ); }, function (Exception $e) { return new Response( - 502, + StatusCodeInterface::STATUS_BAD_GATEWAY, array( 'Content-Type' => 'text/plain' ), diff --git a/examples/81-server-upgrade-echo.php b/examples/81-server-upgrade-echo.php index 2f77172f..4fa54def 100644 --- a/examples/81-server-upgrade-echo.php +++ b/examples/81-server-upgrade-echo.php @@ -17,6 +17,7 @@ < world */ +use Fig\Http\Message\StatusCodeInterface; use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Loop; use React\Http\Message\Response; @@ -30,7 +31,7 @@ $http = new React\Http\HttpServer(function (ServerRequestInterface $request) { if ($request->getHeaderLine('Upgrade') !== 'echo' || $request->getProtocolVersion() === '1.0') { return new Response( - 426, + StatusCodeInterface::STATUS_UPGRADE_REQUIRED, array( 'Upgrade' => 'echo' ), @@ -48,7 +49,7 @@ }); return new Response( - 101, + StatusCodeInterface::STATUS_SWITCHING_PROTOCOLS, array( 'Upgrade' => 'echo' ), diff --git a/examples/82-server-upgrade-chat.php b/examples/82-server-upgrade-chat.php index 42635e8c..84117203 100644 --- a/examples/82-server-upgrade-chat.php +++ b/examples/82-server-upgrade-chat.php @@ -19,6 +19,7 @@ Hint: try this with multiple connections :) */ +use Fig\Http\Message\StatusCodeInterface; use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Loop; use React\Http\Message\Response; @@ -38,7 +39,7 @@ $http = new React\Http\HttpServer(function (ServerRequestInterface $request) use ($chat) { if ($request->getHeaderLine('Upgrade') !== 'chat' || $request->getProtocolVersion() === '1.0') { return new Response( - 426, + StatusCodeInterface::STATUS_UPGRADE_REQUIRED, array( 'Upgrade' => 'chat' ), @@ -76,7 +77,7 @@ }); return new Response( - 101, + StatusCodeInterface::STATUS_SWITCHING_PROTOCOLS, array( 'Upgrade' => 'chat' ), diff --git a/examples/99-server-benchmark-download.php b/examples/99-server-benchmark-download.php index df0e69e7..f7117aa5 100644 --- a/examples/99-server-benchmark-download.php +++ b/examples/99-server-benchmark-download.php @@ -15,6 +15,7 @@ // $ docker run -it --rm --net=host skandyla/wrk -t8 -c10 -d20 http://localhost:8080/ use Evenement\EventEmitter; +use Fig\Http\Message\StatusCodeInterface; use Psr\Http\Message\ServerRequestInterface; use React\Http\Message\Response; use React\Stream\ReadableStreamInterface; @@ -94,7 +95,7 @@ public function getSize() switch ($request->getUri()->getPath()) { case '/': return new Response( - 200, + StatusCodeInterface::STATUS_OK, array( 'Content-Type' => 'text/html' ), @@ -107,13 +108,13 @@ public function getSize() $stream = new ChunkRepeater(str_repeat('.', 1000000), 10000); break; default: - return new Response(404); + return new Response(StatusCodeInterface::STATUS_NOT_FOUND); } React\EventLoop\Loop::addTimer(0, array($stream, 'resume')); return new Response( - 200, + StatusCodeInterface::STATUS_OK, array( 'Content-Type' => 'application/octet-data', 'Content-Length' => $stream->getSize() diff --git a/src/Io/RequestHeaderParser.php b/src/Io/RequestHeaderParser.php index 64b5dcdb..a6136e2f 100644 --- a/src/Io/RequestHeaderParser.php +++ b/src/Io/RequestHeaderParser.php @@ -3,6 +3,7 @@ namespace React\Http\Io; use Evenement\EventEmitter; +use Fig\Http\Message\StatusCodeInterface; use Psr\Http\Message\ServerRequestInterface; use React\Http\Message\ServerRequest; use React\Socket\ConnectionInterface; @@ -39,7 +40,7 @@ public function handle(ConnectionInterface $conn) $fn = null; $that->emit('error', array( - new \OverflowException("Maximum header size of {$maxSize} exceeded.", 431), + new \OverflowException("Maximum header size of {$maxSize} exceeded.", StatusCodeInterface::STATUS_REQUEST_HEADER_FIELDS_TOO_LARGE), $conn )); return; @@ -127,7 +128,7 @@ public function parseRequest($headers, $remoteSocketUri, $localSocketUri) // 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', 505); + throw new \InvalidArgumentException('Received request with invalid protocol version', StatusCodeInterface::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 @@ -256,20 +257,20 @@ 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); + throw new \InvalidArgumentException('Only chunked-encoding is allowed for Transfer-Encoding', StatusCodeInterface::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', 400); + throw new \InvalidArgumentException('Using both `Transfer-Encoding: chunked` and `Content-Length` is not allowed', StatusCodeInterface::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', 400); + throw new \InvalidArgumentException('The value of `Content-Length` is not valid', StatusCodeInterface::STATUS_BAD_REQUEST); } } diff --git a/src/Io/Sender.php b/src/Io/Sender.php index c1bbab42..6e2d661e 100644 --- a/src/Io/Sender.php +++ b/src/Io/Sender.php @@ -2,6 +2,7 @@ namespace React\Http\Io; +use Fig\Http\Message\StatusCodeInterface; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use React\EventLoop\LoopInterface; @@ -110,7 +111,7 @@ public function send(RequestInterface $request) $requestStream->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($deferred, $request) { $length = null; $code = $response->getStatusCode(); - if ($request->getMethod() === 'HEAD' || ($code >= 100 && $code < 200) || $code == 204 || $code == 304) { + if ($request->getMethod() === 'HEAD' || ($code >= 100 && $code < 200) || $code == StatusCodeInterface::STATUS_NO_CONTENT || $code == StatusCodeInterface::STATUS_NOT_MODIFIED) { $length = 0; } elseif (\strtolower($response->getHeaderLine('Transfer-Encoding')) === 'chunked') { $body = new ChunkedDecoder($body); diff --git a/src/Io/StreamingServer.php b/src/Io/StreamingServer.php index 5f9632e9..c7039f50 100644 --- a/src/Io/StreamingServer.php +++ b/src/Io/StreamingServer.php @@ -3,6 +3,7 @@ namespace React\Http\Io; use Evenement\EventEmitter; +use Fig\Http\Message\StatusCodeInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\LoopInterface; @@ -120,7 +121,7 @@ public function __construct(LoopInterface $loop, $requestHandler) // parsing failed => assume dummy request and send appropriate error $that->writeError( $conn, - $e->getCode() !== 0 ? $e->getCode() : 400, + $e->getCode() !== 0 ? $e->getCode() : StatusCodeInterface::STATUS_BAD_REQUEST, new ServerRequest('GET', '/') ); }); @@ -199,7 +200,7 @@ function ($error) use ($that, $conn, $request) { $exception = new \RuntimeException($message, null, $previous); $that->emit('error', array($exception)); - return $that->writeError($conn, 500, $request); + return $that->writeError($conn, StatusCodeInterface::STATUS_INTERNAL_SERVER_ERROR, $request); } ); } @@ -262,7 +263,7 @@ public function handleResponse(ConnectionInterface $connection, ServerRequestInt // assign "Content-Length" header automatically $chunked = false; - if (($method === 'CONNECT' && $code >= 200 && $code < 300) || ($code >= 100 && $code < 200) || $code === 204) { + if (($method === 'CONNECT' && $code >= 200 && $code < 300) || ($code >= 100 && $code < 200) || $code === StatusCodeInterface::STATUS_NO_CONTENT) { // 2xx response to CONNECT and 1xx and 204 MUST NOT include Content-Length or Transfer-Encoding header $response = $response->withoutHeader('Content-Length'); } elseif ($code === 304 && ($response->hasHeader('Content-Length') || $body->getSize() === 0)) { @@ -285,7 +286,7 @@ public function handleResponse(ConnectionInterface $connection, ServerRequestInt // assign "Connection" header automatically $persist = false; - if ($code === 101) { + if ($code === StatusCodeInterface::STATUS_SWITCHING_PROTOCOLS) { // 101 (Switching Protocols) response uses Connection: upgrade header // This implies that this stream now uses another protocol and we // may not persist this connection for additional requests. @@ -307,7 +308,7 @@ public function handleResponse(ConnectionInterface $connection, ServerRequestInt // 101 (Switching Protocols) response (for Upgrade request) forwards upgraded data through duplex stream // 2xx (Successful) response to CONNECT forwards tunneled application data through duplex stream - if (($code === 101 || ($method === 'CONNECT' && $code >= 200 && $code < 300)) && $body instanceof HttpBodyStream && $body->input instanceof WritableStreamInterface) { + if (($code === StatusCodeInterface::STATUS_SWITCHING_PROTOCOLS || ($method === 'CONNECT' && $code >= 200 && $code < 300)) && $body instanceof HttpBodyStream && $body->input instanceof WritableStreamInterface) { if ($request->getBody()->isReadable()) { // request is still streaming => wait for request close before forwarding following data from connection $request->getBody()->on('close', function () use ($connection, $body) { @@ -333,7 +334,7 @@ public function handleResponse(ConnectionInterface $connection, ServerRequestInt // response to HEAD and 1xx, 204 and 304 responses MUST NOT include a body // exclude status 101 (Switching Protocols) here for Upgrade request handling above - if ($method === 'HEAD' || $code === 100 || ($code > 101 && $code < 200) || $code === 204 || $code === 304) { + if ($method === 'HEAD' || $code === 100 || ($code > StatusCodeInterface::STATUS_SWITCHING_PROTOCOLS && $code < 200) || $code === StatusCodeInterface::STATUS_NO_CONTENT || $code === StatusCodeInterface::STATUS_NOT_MODIFIED) { $body->close(); $body = ''; } diff --git a/src/Message/Response.php b/src/Message/Response.php index a9710170..46c91501 100644 --- a/src/Message/Response.php +++ b/src/Message/Response.php @@ -2,6 +2,7 @@ namespace React\Http\Message; +use Fig\Http\Message\StatusCodeInterface; use Psr\Http\Message\StreamInterface; use React\Http\Io\BufferedBody; use React\Http\Io\HttpBodyStream; @@ -43,7 +44,7 @@ final class Response extends Psr7Response * @throws \InvalidArgumentException for an invalid body */ public function __construct( - $status = 200, + $status = StatusCodeInterface::STATUS_OK, array $headers = array(), $body = '', $version = '1.1', From 50af5634826dba7bfa3a243750402d306bd3a490 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 26 Sep 2021 19:19:35 +0200 Subject: [PATCH 383/456] Expose all status code constants via `Response` class --- README.md | 71 +++++++++++++---------- composer.json | 4 +- examples/51-server-hello-world.php | 3 +- examples/52-server-count-visitors.php | 3 +- examples/53-server-whatsmyip.php | 3 +- examples/54-server-query-parameter.php | 3 +- examples/55-server-cookie-handling.php | 5 +- examples/56-server-sleep.php | 3 +- examples/57-server-error-handling.php | 3 +- examples/58-server-stream-response.php | 5 +- examples/59-server-json-api.php | 9 ++- examples/61-server-hello-world-https.php | 3 +- examples/62-server-form-upload.php | 3 +- examples/63-server-streaming-request.php | 6 +- examples/71-server-http-proxy.php | 5 +- examples/72-server-http-connect-proxy.php | 7 +-- examples/81-server-upgrade-echo.php | 5 +- examples/82-server-upgrade-chat.php | 5 +- examples/99-server-benchmark-download.php | 7 +-- src/HttpServer.php | 2 +- src/Io/RequestHeaderParser.php | 12 ++-- src/Io/Sender.php | 4 +- src/Io/StreamingServer.php | 19 +++--- src/Message/Response.php | 15 +++-- 24 files changed, 100 insertions(+), 105 deletions(-) diff --git a/README.md b/README.md index 23010002..50797fe6 100644 --- a/README.md +++ b/README.md @@ -109,7 +109,7 @@ require __DIR__ . '/vendor/autoload.php'; $http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { return new React\Http\Message\Response( - 200, + React\Http\Message\Response::STATUS_OK, array( 'Content-Type' => 'text/plain' ), @@ -735,7 +735,7 @@ object and expects a [response](#server-response) object in return: ```php $http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { return new React\Http\Message\Response( - 200, + React\Http\Message\Response::STATUS_OK, array( 'Content-Type' => 'text/plain' ), @@ -953,7 +953,7 @@ $http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterf $body .= "The requested path is: " . $request->getUri()->getPath(); return new React\Http\Message\Response( - 200, + React\Http\Message\Response::STATUS_OK, array( 'Content-Type' => 'text/plain' ), @@ -995,7 +995,7 @@ $http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterf $body = "Your IP is: " . $request->getServerParams()['REMOTE_ADDR']; return new React\Http\Message\Response( - 200, + React\Http\Message\Response::STATUS_OK, array( 'Content-Type' => 'text/plain' ), @@ -1027,7 +1027,7 @@ $http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterf } return new React\Http\Message\Response( - 200, + React\Http\Message\Response::STATUS_OK, array( 'Content-Type' => 'text/html' ), @@ -1074,7 +1074,7 @@ $http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterf $name = $request->getParsedBody()['name'] ?? 'anonymous'; return new React\Http\Message\Response( - 200, + React\Http\Message\Response::STATUS_OK, array(), "Hello $name!\n" ); @@ -1099,7 +1099,7 @@ $http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterf $name = $data->name ?? 'anonymous'; return new React\Http\Message\Response( - 200, + React\Http\Message\Response::STATUS_OK, array('Content-Type' => 'application/json'), json_encode(['message' => "Hello $name!"]) ); @@ -1122,7 +1122,7 @@ $http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterf $name = isset($files['avatar']) ? $files['avatar']->getClientFilename() : 'nothing'; return new React\Http\Message\Response( - 200, + React\Http\Message\Response::STATUS_OK, array(), "Uploaded $name\n" ); @@ -1205,7 +1205,7 @@ $http = new React\Http\HttpServer( $body->on('end', function () use ($resolve, &$bytes){ $resolve(new React\Http\Message\Response( - 200, + React\Http\Message\Response::STATUS_OK, array( 'Content-Type' => 'text/plain' ), @@ -1216,7 +1216,7 @@ $http = new React\Http\HttpServer( // an error occures e.g. on invalid chunked encoded data or an unexpected 'end' event $body->on('error', function (Exception $e) use ($resolve, &$bytes) { $resolve(new React\Http\Message\Response( - 400, + React\Http\Message\Response::STATUS_BAD_REQUEST, array( 'Content-Type' => 'text/plain' ), @@ -1272,7 +1272,7 @@ $http = new React\Http\HttpServer( $body .= 'This example does not accept chunked transfer encoding.'; return new React\Http\Message\Response( - 411, + React\Http\Message\Response::STATUS_LENGTH_REQUIRED, array( 'Content-Type' => 'text/plain' ), @@ -1281,7 +1281,7 @@ $http = new React\Http\HttpServer( } return new React\Http\Message\Response( - 200, + React\Http\Message\Response::STATUS_OK, array( 'Content-Type' => 'text/plain' ), @@ -1343,7 +1343,7 @@ $http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterf $body = "Your cookie value is: " . $request->getCookieParams()[$key]; return new React\Http\Message\Response( - 200, + React\Http\Message\Response::STATUS_OK, array( 'Content-Type' => 'text/plain' ), @@ -1352,7 +1352,7 @@ $http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterf } return new React\Http\Message\Response( - 200, + React\Http\Message\Response::STATUS_OK, array( 'Content-Type' => 'text/plain', 'Set-Cookie' => urlencode($key) . '=' . urlencode('test;more') @@ -1410,7 +1410,7 @@ In its most simple form, you can use it like this: ```php $http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { return new React\Http\Message\Response( - 200, + React\Http\Message\Response::STATUS_OK, array( 'Content-Type' => 'text/plain' ), @@ -1440,7 +1440,7 @@ $http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterf return new Promise(function ($resolve, $reject) { Loop::addTimer(1.5, function() use ($resolve) { $response = new React\Http\Message\Response( - 200, + React\Http\Message\Response::STATUS_OK, array( 'Content-Type' => 'text/plain' ), @@ -1487,7 +1487,7 @@ $http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterf }); return new React\Http\Message\Response( - 200, + React\Http\Message\Response::STATUS_OK, array( 'Content-Type' => 'text/plain' ), @@ -1568,7 +1568,7 @@ a `string` response body like this: ```php $http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { return new React\Http\Message\Response( - 200, + React\Http\Message\Response::STATUS_OK, array( 'Content-Type' => 'text/plain' ), @@ -1593,7 +1593,7 @@ $http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterf }); return new React\Http\Message\Response( - 200, + React\Http\Message\Response::STATUS_OK, array( 'Content-Length' => '13', 'Content-Type' => 'text/plain', @@ -1663,7 +1663,7 @@ a custom `Server` response header like this: ```php $http = new React\Http\HttpServer(function (ServerRequestInterface $request) { return new React\Http\Message\Response( - 200, + React\Http\Message\Response::STATUS_OK, array( 'Server' => 'PHP/3' ) @@ -1678,7 +1678,7 @@ string value like this: ```php $http = new React\Http\HttpServer(function (ServerRequestInterface $request) { return new React\Http\Message\Response( - 200, + React\Http\Message\Response::STATUS_OK, array( 'Server' => '' ) @@ -1693,7 +1693,7 @@ like this: ```php $http = new React\Http\HttpServer(function (ServerRequestInterface $request) { return new React\Http\Message\Response( - 200, + React\Http\Message\Response::STATUS_OK, array( 'Date' => gmdate('D, d M Y H:i:s \G\M\T') ) @@ -1708,7 +1708,7 @@ like this: ```php $http = new React\Http\HttpServer(function (ServerRequestInterface $request) { return new React\Http\Message\Response( - 200, + React\Http\Message\Response::STATUS_OK, array( 'Date' => '' ) @@ -1786,7 +1786,7 @@ encourages [Third-Party Middleware](#third-party-middleware) implementations. In order to use middleware request handlers, simply pass a list of all callables as defined above to the [`HttpServer`](#httpserver). The following example adds a middleware request handler that adds the current time to the request as a -header (`Request-Time`) and a final request handler that always returns a 200 code without a body: +header (`Request-Time`) and a final request handler that always returns a `200 OK` status code without a body: ```php $http = new React\Http\HttpServer( @@ -1795,7 +1795,7 @@ $http = new React\Http\HttpServer( return $next($request); }, function (Psr\Http\Message\ServerRequestInterface $request) { - return new React\Http\Message\Response(200); + return new React\Http\Message\Response(React\Http\Message\Response::STATUS_OK); } ); ``` @@ -1821,7 +1821,7 @@ $http = new React\Http\HttpServer( }); }, function (Psr\Http\Message\ServerRequestInterface $request) { - return new React\Http\Message\Response(200); + return new React\Http\Message\Response(React\Http\Message\Response::STATUS_OK); } ); ``` @@ -1842,7 +1842,7 @@ $http = new React\Http\HttpServer( }); return $promise->then(null, function (Exception $e) { return new React\Http\Message\Response( - 500, + React\Http\Message\Response::STATUS_INTERNAL_SERVER_ERROR, array(), 'Internal error: ' . $e->getMessage() ); @@ -1852,7 +1852,7 @@ $http = new React\Http\HttpServer( if (mt_rand(0, 1) === 1) { throw new RuntimeException('Database error'); } - return new React\Http\Message\Response(200); + return new React\Http\Message\Response(React\Http\Message\Response::STATUS_OK); } ); ``` @@ -2439,7 +2439,7 @@ represent an outgoing server response message. ```php $response = new React\Http\Message\Response( - 200, + React\Http\Message\Response::STATUS_OK, array( 'Content-Type' => 'text/html' ), @@ -2452,6 +2452,13 @@ This class implements the which in turn extends the [PSR-7 `MessageInterface`](https://www.php-fig.org/psr/psr-7/#31-psrhttpmessagemessageinterface). +On top of this, this class implements the +[PSR-7 Message Util `StatusCodeInterface`](https://github.com/php-fig/http-message-util/blob/master/src/StatusCodeInterface.php) +which means that most common HTTP status codes are available as class +constants with the `STATUS_*` prefix. For instance, the `200 OK` and +`404 Not Found` status codes can used as `Response::STATUS_OK` and +`Response::STATUS_NOT_FOUND` respectively. + > Internally, this implementation builds on top of an existing incoming response message and only adds required streaming support. This base class is considered an implementation detail that may change in the future. @@ -2516,7 +2523,7 @@ $http = new React\Http\HttpServer( }); $body->on('close', function () use (&$bytes, $resolve) { $resolve(new React\Http\Message\Response( - 200, + React\Http\Message\Response::STATUS_OK, [], "Received $bytes bytes\n" )); @@ -2653,7 +2660,7 @@ $http = new React\Http\HttpServer( new React\Http\Middleware\RequestBodyBufferMiddleware(16 * 1024 * 1024), // 16 MiB function (Psr\Http\Message\ServerRequestInterface $request) { // The body from $request->getBody() is now fully available without the need to stream it - return new React\Http\Message\Response(200); + return new React\Http\Message\Response(React\Http\Message\Response::STATUS_OK); }, ); ``` @@ -2700,7 +2707,7 @@ $handler = function (Psr\Http\Message\ServerRequestInterface $request) { } return new React\Http\Message\Response( - 200, + React\Http\Message\Response::STATUS_OK, array( 'Content-Type' => 'text/plain' ), diff --git a/composer.json b/composer.json index 1939accf..8a5c7df6 100644 --- a/composer.json +++ b/composer.json @@ -28,14 +28,14 @@ "require": { "php": ">=5.3.0", "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "fig/http-message-util": "^1.1", "psr/http-message": "^1.0", "react/event-loop": "^1.2", "react/promise": "^2.3 || ^1.2.1", "react/promise-stream": "^1.1", "react/socket": "^1.9", "react/stream": "^1.2", - "ringcentral/psr7": "^1.2", - "fig/http-message-util": "^1.1" + "ringcentral/psr7": "^1.2" }, "require-dev": { "clue/block-react": "^1.1", diff --git a/examples/51-server-hello-world.php b/examples/51-server-hello-world.php index 2d9dc766..88831525 100644 --- a/examples/51-server-hello-world.php +++ b/examples/51-server-hello-world.php @@ -1,6 +1,5 @@ 'text/plain' ), diff --git a/examples/52-server-count-visitors.php b/examples/52-server-count-visitors.php index 8e219ad0..bdd53af9 100644 --- a/examples/52-server-count-visitors.php +++ b/examples/52-server-count-visitors.php @@ -1,6 +1,5 @@ 'text/plain' ), diff --git a/examples/53-server-whatsmyip.php b/examples/53-server-whatsmyip.php index 14ad2da8..e0835a82 100644 --- a/examples/53-server-whatsmyip.php +++ b/examples/53-server-whatsmyip.php @@ -1,6 +1,5 @@ getServerParams()['REMOTE_ADDR']; return new Response( - StatusCodeInterface::STATUS_OK, + Response::STATUS_OK, array( 'Content-Type' => 'text/plain' ), diff --git a/examples/54-server-query-parameter.php b/examples/54-server-query-parameter.php index e3854626..18dd56b0 100644 --- a/examples/54-server-query-parameter.php +++ b/examples/54-server-query-parameter.php @@ -1,6 +1,5 @@ 'text/html' ), diff --git a/examples/55-server-cookie-handling.php b/examples/55-server-cookie-handling.php index 7a093d24..8260fc33 100644 --- a/examples/55-server-cookie-handling.php +++ b/examples/55-server-cookie-handling.php @@ -1,6 +1,5 @@ getCookieParams()[$key]; return new Response( - StatusCodeInterface::STATUS_OK, + Response::STATUS_OK, array( 'Content-Type' => 'text/plain' ), @@ -22,7 +21,7 @@ } return new Response( - StatusCodeInterface::STATUS_OK, + Response::STATUS_OK, array( 'Content-Type' => 'text/plain', 'Set-Cookie' => urlencode($key) . '=' . urlencode('test;more') diff --git a/examples/56-server-sleep.php b/examples/56-server-sleep.php index cfc805d3..6bb6f82b 100644 --- a/examples/56-server-sleep.php +++ b/examples/56-server-sleep.php @@ -1,6 +1,5 @@ 'text/plain' ), diff --git a/examples/57-server-error-handling.php b/examples/57-server-error-handling.php index 72b9c02b..71cbad15 100644 --- a/examples/57-server-error-handling.php +++ b/examples/57-server-error-handling.php @@ -1,6 +1,5 @@ 'text/plain' ), diff --git a/examples/58-server-stream-response.php b/examples/58-server-stream-response.php index 596ca6fc..015ddd9a 100644 --- a/examples/58-server-stream-response.php +++ b/examples/58-server-stream-response.php @@ -1,6 +1,5 @@ getMethod() !== 'GET' || $request->getUri()->getPath() !== '/') { - return new Response(StatusCodeInterface::STATUS_NOT_FOUND); + return new Response(Response::STATUS_NOT_FOUND); } $stream = new ThroughStream(); @@ -31,7 +30,7 @@ }); return new Response( - StatusCodeInterface::STATUS_OK, + Response::STATUS_OK, array( 'Content-Type' => 'text/plain' ), diff --git a/examples/59-server-json-api.php b/examples/59-server-json-api.php index 8a1ba358..0d50b52b 100644 --- a/examples/59-server-json-api.php +++ b/examples/59-server-json-api.php @@ -6,7 +6,6 @@ // $ php examples/59-server-json-api.php 8080 // $ curl -v http://localhost:8080/ -H 'Content-Type: application/json' -d '{"name":"Alice"}' -use Fig\Http\Message\StatusCodeInterface; use Psr\Http\Message\ServerRequestInterface; use React\Http\Message\Response; @@ -15,7 +14,7 @@ $http = new React\Http\HttpServer(function (ServerRequestInterface $request) { if ($request->getHeaderLine('Content-Type') !== 'application/json') { return new Response( - StatusCodeInterface::STATUS_UNSUPPORTED_MEDIA_TYPE, + Response::STATUS_UNSUPPORTED_MEDIA_TYPE, array( 'Content-Type' => 'application/json' ), @@ -26,7 +25,7 @@ $input = json_decode($request->getBody()->getContents()); if (json_last_error() !== JSON_ERROR_NONE) { return new Response( - StatusCodeInterface::STATUS_BAD_REQUEST, + Response::STATUS_BAD_REQUEST, array( 'Content-Type' => 'application/json' ), @@ -36,7 +35,7 @@ if (!isset($input->name) || !is_string($input->name)) { return new Response( - StatusCodeInterface::STATUS_UNPROCESSABLE_ENTITY, + Response::STATUS_UNPROCESSABLE_ENTITY, array( 'Content-Type' => 'application/json' ), @@ -45,7 +44,7 @@ } return new Response( - StatusCodeInterface::STATUS_OK, + Response::STATUS_OK, array( 'Content-Type' => 'application/json' ), diff --git a/examples/61-server-hello-world-https.php b/examples/61-server-hello-world-https.php index f563282e..2fd6f9af 100644 --- a/examples/61-server-hello-world-https.php +++ b/examples/61-server-hello-world-https.php @@ -1,6 +1,5 @@ 'text/plain' ), diff --git a/examples/62-server-form-upload.php b/examples/62-server-form-upload.php index b09f178d..899caa0a 100644 --- a/examples/62-server-form-upload.php +++ b/examples/62-server-form-upload.php @@ -7,7 +7,6 @@ // $ curl --form name=test --form age=30 http://localhost:8080/ // $ curl --form name=hi --form avatar=@avatar.png http://localhost:8080/ -use Fig\Http\Message\StatusCodeInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\UploadedFileInterface; use React\Http\Message\Response; @@ -111,7 +110,7 @@ HTML; return new Response( - StatusCodeInterface::STATUS_OK, + Response::STATUS_OK, array( 'Content-Type' => 'text/html; charset=UTF-8' ), diff --git a/examples/63-server-streaming-request.php b/examples/63-server-streaming-request.php index 073d0e0e..b20b8f08 100644 --- a/examples/63-server-streaming-request.php +++ b/examples/63-server-streaming-request.php @@ -1,7 +1,5 @@ on('end', function () use ($resolve, &$bytes){ $resolve(new React\Http\Message\Response( - StatusCodeInterface::STATUS_OK, + React\Http\Message\Response::STATUS_OK, array( 'Content-Type' => 'text/plain' ), @@ -33,7 +31,7 @@ function (Psr\Http\Message\ServerRequestInterface $request) { // an error occures e.g. on invalid chunked encoded data or an unexpected 'end' event $body->on('error', function (Exception $e) use ($resolve, &$bytes) { $resolve(new React\Http\Message\Response( - StatusCodeInterface::STATUS_BAD_REQUEST, + React\Http\Message\Response::STATUS_BAD_REQUEST, array( 'Content-Type' => 'text/plain' ), diff --git a/examples/71-server-http-proxy.php b/examples/71-server-http-proxy.php index 3029dd7f..e0bf8404 100644 --- a/examples/71-server-http-proxy.php +++ b/examples/71-server-http-proxy.php @@ -3,7 +3,6 @@ // $ php examples/71-server-http-proxy.php 8080 // $ curl -v --proxy http://localhost:8080 http://reactphp.org/ -use Fig\Http\Message\StatusCodeInterface; use Psr\Http\Message\RequestInterface; use React\Http\Message\Response; use RingCentral\Psr7; @@ -17,7 +16,7 @@ $http = new React\Http\HttpServer(function (RequestInterface $request) { if (strpos($request->getRequestTarget(), '://') === false) { return new Response( - StatusCodeInterface::STATUS_BAD_REQUEST, + Response::STATUS_BAD_REQUEST, array( 'Content-Type' => 'text/plain' ), @@ -37,7 +36,7 @@ // left up as an exercise: use an HTTP client to send the outgoing request // and forward the incoming response to the original client request return new Response( - StatusCodeInterface::STATUS_OK, + Response::STATUS_OK, array( 'Content-Type' => 'text/plain' ), diff --git a/examples/72-server-http-connect-proxy.php b/examples/72-server-http-connect-proxy.php index 55b03a84..0500822a 100644 --- a/examples/72-server-http-connect-proxy.php +++ b/examples/72-server-http-connect-proxy.php @@ -3,7 +3,6 @@ // $ php examples/72-server-http-connect-proxy.php 8080 // $ curl -v --proxy http://localhost:8080 https://reactphp.org/ -use Fig\Http\Message\StatusCodeInterface; use Psr\Http\Message\ServerRequestInterface; use React\Http\Message\Response; use React\Socket\Connector; @@ -20,7 +19,7 @@ $http = new React\Http\HttpServer(function (ServerRequestInterface $request) use ($connector) { if ($request->getMethod() !== 'CONNECT') { return new Response( - StatusCodeInterface::STATUS_METHOD_NOT_ALLOWED, + Response::STATUS_METHOD_NOT_ALLOWED, array( 'Content-Type' => 'text/plain', 'Allow' => 'CONNECT' @@ -34,14 +33,14 @@ function (ConnectionInterface $remote) { // connection established => forward data return new Response( - StatusCodeInterface::STATUS_OK, + Response::STATUS_OK, array(), $remote ); }, function (Exception $e) { return new Response( - StatusCodeInterface::STATUS_BAD_GATEWAY, + Response::STATUS_BAD_GATEWAY, array( 'Content-Type' => 'text/plain' ), diff --git a/examples/81-server-upgrade-echo.php b/examples/81-server-upgrade-echo.php index 4fa54def..cd3dc156 100644 --- a/examples/81-server-upgrade-echo.php +++ b/examples/81-server-upgrade-echo.php @@ -17,7 +17,6 @@ < world */ -use Fig\Http\Message\StatusCodeInterface; use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Loop; use React\Http\Message\Response; @@ -31,7 +30,7 @@ $http = new React\Http\HttpServer(function (ServerRequestInterface $request) { if ($request->getHeaderLine('Upgrade') !== 'echo' || $request->getProtocolVersion() === '1.0') { return new Response( - StatusCodeInterface::STATUS_UPGRADE_REQUIRED, + Response::STATUS_UPGRADE_REQUIRED, array( 'Upgrade' => 'echo' ), @@ -49,7 +48,7 @@ }); return new Response( - StatusCodeInterface::STATUS_SWITCHING_PROTOCOLS, + Response::STATUS_SWITCHING_PROTOCOLS, array( 'Upgrade' => 'echo' ), diff --git a/examples/82-server-upgrade-chat.php b/examples/82-server-upgrade-chat.php index 84117203..bd791fb0 100644 --- a/examples/82-server-upgrade-chat.php +++ b/examples/82-server-upgrade-chat.php @@ -19,7 +19,6 @@ Hint: try this with multiple connections :) */ -use Fig\Http\Message\StatusCodeInterface; use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Loop; use React\Http\Message\Response; @@ -39,7 +38,7 @@ $http = new React\Http\HttpServer(function (ServerRequestInterface $request) use ($chat) { if ($request->getHeaderLine('Upgrade') !== 'chat' || $request->getProtocolVersion() === '1.0') { return new Response( - StatusCodeInterface::STATUS_UPGRADE_REQUIRED, + Response::STATUS_UPGRADE_REQUIRED, array( 'Upgrade' => 'chat' ), @@ -77,7 +76,7 @@ }); return new Response( - StatusCodeInterface::STATUS_SWITCHING_PROTOCOLS, + Response::STATUS_SWITCHING_PROTOCOLS, array( 'Upgrade' => 'chat' ), diff --git a/examples/99-server-benchmark-download.php b/examples/99-server-benchmark-download.php index f7117aa5..6c737605 100644 --- a/examples/99-server-benchmark-download.php +++ b/examples/99-server-benchmark-download.php @@ -15,7 +15,6 @@ // $ docker run -it --rm --net=host skandyla/wrk -t8 -c10 -d20 http://localhost:8080/ use Evenement\EventEmitter; -use Fig\Http\Message\StatusCodeInterface; use Psr\Http\Message\ServerRequestInterface; use React\Http\Message\Response; use React\Stream\ReadableStreamInterface; @@ -95,7 +94,7 @@ public function getSize() switch ($request->getUri()->getPath()) { case '/': return new Response( - StatusCodeInterface::STATUS_OK, + Response::STATUS_OK, array( 'Content-Type' => 'text/html' ), @@ -108,13 +107,13 @@ public function getSize() $stream = new ChunkRepeater(str_repeat('.', 1000000), 10000); break; default: - return new Response(StatusCodeInterface::STATUS_NOT_FOUND); + return new Response(Response::STATUS_NOT_FOUND); } React\EventLoop\Loop::addTimer(0, array($stream, 'resume')); return new Response( - StatusCodeInterface::STATUS_OK, + Response::STATUS_OK, array( 'Content-Type' => 'application/octet-data', 'Content-Length' => $stream->getSize() diff --git a/src/HttpServer.php b/src/HttpServer.php index 95ccc5ff..f2334733 100644 --- a/src/HttpServer.php +++ b/src/HttpServer.php @@ -26,7 +26,7 @@ * ```php * $http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { * return new React\Http\Message\Response( - * 200, + * React\Http\Message\Response::STATUS_OK, * array( * 'Content-Type' => 'text/plain' * ), diff --git a/src/Io/RequestHeaderParser.php b/src/Io/RequestHeaderParser.php index a6136e2f..743c006c 100644 --- a/src/Io/RequestHeaderParser.php +++ b/src/Io/RequestHeaderParser.php @@ -3,8 +3,8 @@ namespace React\Http\Io; use Evenement\EventEmitter; -use Fig\Http\Message\StatusCodeInterface; use Psr\Http\Message\ServerRequestInterface; +use React\Http\Message\Response; use React\Http\Message\ServerRequest; use React\Socket\ConnectionInterface; use Exception; @@ -40,7 +40,7 @@ public function handle(ConnectionInterface $conn) $fn = null; $that->emit('error', array( - new \OverflowException("Maximum header size of {$maxSize} exceeded.", StatusCodeInterface::STATUS_REQUEST_HEADER_FIELDS_TOO_LARGE), + new \OverflowException("Maximum header size of {$maxSize} exceeded.", Response::STATUS_REQUEST_HEADER_FIELDS_TOO_LARGE), $conn )); return; @@ -128,7 +128,7 @@ public function parseRequest($headers, $remoteSocketUri, $localSocketUri) // 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', StatusCodeInterface::STATUS_VERSION_NOT_SUPPORTED); + 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 @@ -257,20 +257,20 @@ 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', StatusCodeInterface::STATUS_NOT_IMPLEMENTED); + 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', StatusCodeInterface::STATUS_BAD_REQUEST); + 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', StatusCodeInterface::STATUS_BAD_REQUEST); + throw new \InvalidArgumentException('The value of `Content-Length` is not valid', Response::STATUS_BAD_REQUEST); } } diff --git a/src/Io/Sender.php b/src/Io/Sender.php index 6e2d661e..2f04c797 100644 --- a/src/Io/Sender.php +++ b/src/Io/Sender.php @@ -2,11 +2,11 @@ namespace React\Http\Io; -use Fig\Http\Message\StatusCodeInterface; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use React\EventLoop\LoopInterface; use React\Http\Client\Client as HttpClient; +use React\Http\Message\Response; use React\Promise\PromiseInterface; use React\Promise\Deferred; use React\Socket\ConnectorInterface; @@ -111,7 +111,7 @@ public function send(RequestInterface $request) $requestStream->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($deferred, $request) { $length = null; $code = $response->getStatusCode(); - if ($request->getMethod() === 'HEAD' || ($code >= 100 && $code < 200) || $code == StatusCodeInterface::STATUS_NO_CONTENT || $code == StatusCodeInterface::STATUS_NOT_MODIFIED) { + if ($request->getMethod() === 'HEAD' || ($code >= 100 && $code < 200) || $code == Response::STATUS_NO_CONTENT || $code == Response::STATUS_NOT_MODIFIED) { $length = 0; } elseif (\strtolower($response->getHeaderLine('Transfer-Encoding')) === 'chunked') { $body = new ChunkedDecoder($body); diff --git a/src/Io/StreamingServer.php b/src/Io/StreamingServer.php index c7039f50..dd4c0584 100644 --- a/src/Io/StreamingServer.php +++ b/src/Io/StreamingServer.php @@ -3,7 +3,6 @@ namespace React\Http\Io; use Evenement\EventEmitter; -use Fig\Http\Message\StatusCodeInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\LoopInterface; @@ -32,7 +31,7 @@ * ```php * $server = new StreamingServer($loop, function (ServerRequestInterface $request) { * return new Response( - * 200, + * Response::STATUS_OK, * array( * 'Content-Type' => 'text/plain' * ), @@ -121,7 +120,7 @@ public function __construct(LoopInterface $loop, $requestHandler) // parsing failed => assume dummy request and send appropriate error $that->writeError( $conn, - $e->getCode() !== 0 ? $e->getCode() : StatusCodeInterface::STATUS_BAD_REQUEST, + $e->getCode() !== 0 ? $e->getCode() : Response::STATUS_BAD_REQUEST, new ServerRequest('GET', '/') ); }); @@ -183,7 +182,7 @@ function ($response) use ($that, $conn, $request) { $exception = new \RuntimeException($message); $that->emit('error', array($exception)); - return $that->writeError($conn, 500, $request); + return $that->writeError($conn, Response::STATUS_INTERNAL_SERVER_ERROR, $request); } $that->handleResponse($conn, $request, $response); }, @@ -200,7 +199,7 @@ function ($error) use ($that, $conn, $request) { $exception = new \RuntimeException($message, null, $previous); $that->emit('error', array($exception)); - return $that->writeError($conn, StatusCodeInterface::STATUS_INTERNAL_SERVER_ERROR, $request); + return $that->writeError($conn, Response::STATUS_INTERNAL_SERVER_ERROR, $request); } ); } @@ -263,10 +262,10 @@ public function handleResponse(ConnectionInterface $connection, ServerRequestInt // assign "Content-Length" header automatically $chunked = false; - if (($method === 'CONNECT' && $code >= 200 && $code < 300) || ($code >= 100 && $code < 200) || $code === StatusCodeInterface::STATUS_NO_CONTENT) { + if (($method === 'CONNECT' && $code >= 200 && $code < 300) || ($code >= 100 && $code < 200) || $code === Response::STATUS_NO_CONTENT) { // 2xx response to CONNECT and 1xx and 204 MUST NOT include Content-Length or Transfer-Encoding header $response = $response->withoutHeader('Content-Length'); - } elseif ($code === 304 && ($response->hasHeader('Content-Length') || $body->getSize() === 0)) { + } elseif ($code === Response::STATUS_NOT_MODIFIED && ($response->hasHeader('Content-Length') || $body->getSize() === 0)) { // 304 Not Modified: preserve explicit Content-Length and preserve missing header if body is empty } elseif ($body->getSize() !== null) { // assign Content-Length header when using a "normal" buffered body string @@ -286,7 +285,7 @@ public function handleResponse(ConnectionInterface $connection, ServerRequestInt // assign "Connection" header automatically $persist = false; - if ($code === StatusCodeInterface::STATUS_SWITCHING_PROTOCOLS) { + if ($code === Response::STATUS_SWITCHING_PROTOCOLS) { // 101 (Switching Protocols) response uses Connection: upgrade header // This implies that this stream now uses another protocol and we // may not persist this connection for additional requests. @@ -308,7 +307,7 @@ public function handleResponse(ConnectionInterface $connection, ServerRequestInt // 101 (Switching Protocols) response (for Upgrade request) forwards upgraded data through duplex stream // 2xx (Successful) response to CONNECT forwards tunneled application data through duplex stream - if (($code === StatusCodeInterface::STATUS_SWITCHING_PROTOCOLS || ($method === 'CONNECT' && $code >= 200 && $code < 300)) && $body instanceof HttpBodyStream && $body->input instanceof WritableStreamInterface) { + if (($code === Response::STATUS_SWITCHING_PROTOCOLS || ($method === 'CONNECT' && $code >= 200 && $code < 300)) && $body instanceof HttpBodyStream && $body->input instanceof WritableStreamInterface) { if ($request->getBody()->isReadable()) { // request is still streaming => wait for request close before forwarding following data from connection $request->getBody()->on('close', function () use ($connection, $body) { @@ -334,7 +333,7 @@ public function handleResponse(ConnectionInterface $connection, ServerRequestInt // response to HEAD and 1xx, 204 and 304 responses MUST NOT include a body // exclude status 101 (Switching Protocols) here for Upgrade request handling above - if ($method === 'HEAD' || $code === 100 || ($code > StatusCodeInterface::STATUS_SWITCHING_PROTOCOLS && $code < 200) || $code === StatusCodeInterface::STATUS_NO_CONTENT || $code === StatusCodeInterface::STATUS_NOT_MODIFIED) { + if ($method === 'HEAD' || ($code >= 100 && $code < 200 && $code !== Response::STATUS_SWITCHING_PROTOCOLS) || $code === Response::STATUS_NO_CONTENT || $code === Response::STATUS_NOT_MODIFIED) { $body->close(); $body = ''; } diff --git a/src/Message/Response.php b/src/Message/Response.php index 46c91501..0ce8bef0 100644 --- a/src/Message/Response.php +++ b/src/Message/Response.php @@ -14,7 +14,7 @@ * * ```php * $response = new React\Http\Message\Response( - * 200, + * React\Http\Message\Response::STATUS_OK, * array( * 'Content-Type' => 'text/html' * ), @@ -27,16 +27,23 @@ * which in turn extends the * [PSR-7 `MessageInterface`](https://www.php-fig.org/psr/psr-7/#31-psrhttpmessagemessageinterface). * + * On top of this, this class implements the + * [PSR-7 Message Util `StatusCodeInterface`](https://github.com/php-fig/http-message-util/blob/master/src/StatusCodeInterface.php) + * which means that most common HTTP status codes are available as class + * constants with the `STATUS_*` prefix. For instance, the `200 OK` and + * `404 Not Found` status codes can used as `Response::STATUS_OK` and + * `Response::STATUS_NOT_FOUND` respectively. + * * > Internally, this implementation builds on top of an existing incoming * response message and only adds required streaming support. This base class is * considered an implementation detail that may change in the future. * * @see \Psr\Http\Message\ResponseInterface */ -final class Response extends Psr7Response +final class Response extends Psr7Response implements StatusCodeInterface { /** - * @param int $status HTTP status code (e.g. 200/404) + * @param int $status HTTP status code (e.g. 200/404), see `self::STATUS_*` constants * @param array $headers additional response headers * @param string|ReadableStreamInterface|StreamInterface $body response body * @param string $version HTTP protocol version (e.g. 1.1/1.0) @@ -44,7 +51,7 @@ final class Response extends Psr7Response * @throws \InvalidArgumentException for an invalid body */ public function __construct( - $status = StatusCodeInterface::STATUS_OK, + $status = self::STATUS_OK, array $headers = array(), $body = '', $version = '1.1', From 17dd30d2dad082c18c818878c598313d0cfce76a Mon Sep 17 00:00:00 2001 From: Simon Frings Date: Mon, 15 Nov 2021 15:27:51 +0100 Subject: [PATCH 384/456] Support PHP 8.1 --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cf214c83..c64ef6ab 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,6 +11,7 @@ jobs: strategy: matrix: php: + - 8.1 - 8.0 - 7.4 - 7.3 From aaa6afbe6780497f0a74caf8302f1adab21d39ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 4 Dec 2021 16:31:15 +0100 Subject: [PATCH 385/456] Support PHP 8.1 --- phpunit.xml.dist | 3 ++- src/Io/RequestHeaderParser.php | 6 ++++-- src/Io/StreamingServer.php | 2 +- tests/Io/IniUtilTest.php | 1 - 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index fa88e7e0..fd6a9234 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -5,7 +5,8 @@ xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd" bootstrap="vendor/autoload.php" colors="true" - cacheResult="false"> + cacheResult="false" + convertDeprecationsToExceptions="true"> ./tests/ diff --git a/src/Io/RequestHeaderParser.php b/src/Io/RequestHeaderParser.php index 743c006c..e5554c46 100644 --- a/src/Io/RequestHeaderParser.php +++ b/src/Io/RequestHeaderParser.php @@ -160,7 +160,7 @@ public function parseRequest($headers, $remoteSocketUri, $localSocketUri) ); // scheme is `http` unless TLS is used - $localParts = \parse_url($localSocketUri); + $localParts = $localSocketUri === null ? array() : \parse_url($localSocketUri); if (isset($localParts['scheme']) && $localParts['scheme'] === 'tls') { $scheme = 'https://'; $serverParams['HTTPS'] = 'on'; @@ -242,7 +242,9 @@ public function parseRequest($headers, $remoteSocketUri, $localSocketUri) } // make sure value does not contain any other URI component - unset($parts['scheme'], $parts['host'], $parts['port']); + if (\is_array($parts)) { + unset($parts['scheme'], $parts['host'], $parts['port']); + } if ($parts === false || $parts) { throw new \InvalidArgumentException('Invalid Host header value'); } diff --git a/src/Io/StreamingServer.php b/src/Io/StreamingServer.php index dd4c0584..7818f0bd 100644 --- a/src/Io/StreamingServer.php +++ b/src/Io/StreamingServer.php @@ -196,7 +196,7 @@ function ($error) use ($that, $conn, $request) { $previous = $error; } - $exception = new \RuntimeException($message, null, $previous); + $exception = new \RuntimeException($message, 0, $previous); $that->emit('error', array($exception)); return $that->writeError($conn, Response::STATUS_INTERNAL_SERVER_ERROR, $request); diff --git a/tests/Io/IniUtilTest.php b/tests/Io/IniUtilTest.php index 22374eb4..155c6ed2 100644 --- a/tests/Io/IniUtilTest.php +++ b/tests/Io/IniUtilTest.php @@ -63,7 +63,6 @@ public function provideInvalidInputIniSizeToBytes() return array( array('-1G'), array('0G'), - array(null), array('foo'), array('fooK'), array('1ooL'), From 5d37d3769b8c35ce1ad67f29ceb496607dc9b1e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 23 Jan 2022 11:13:33 +0100 Subject: [PATCH 386/456] Update `Host` header tests requiring root access --- phpunit.xml.dist | 2 +- tests/FunctionalHttpServerTest.php | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index fd6a9234..93a36f6b 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -4,8 +4,8 @@ diff --git a/tests/FunctionalHttpServerTest.php b/tests/FunctionalHttpServerTest.php index fe0e1936..a5274533 100644 --- a/tests/FunctionalHttpServerTest.php +++ b/tests/FunctionalHttpServerTest.php @@ -302,7 +302,7 @@ public function testSecureHttpsOnStandardPortReturnsUriWithNoPort() $loop = Factory::create(); try { - $socket = new SocketServer('127.0.0.1:443', array('tls' => array( + $socket = new SocketServer('tls://127.0.0.1:443', array('tls' => array( 'local_cert' => __DIR__ . '/../examples/localhost.pem' )), $loop); } catch (\RuntimeException $e) { @@ -341,7 +341,7 @@ public function testSecureHttpsOnStandardPortWithoutHostHeaderUsesSocketUri() $loop = Factory::create(); try { - $socket = new SocketServer('127.0.0.1:443', array('tls' => array( + $socket = new SocketServer('tls://127.0.0.1:443', array('tls' => array( 'local_cert' => __DIR__ . '/../examples/localhost.pem' )), $loop); } catch (\RuntimeException $e) { @@ -410,7 +410,7 @@ public function testSecureHttpsOnHttpStandardPortReturnsUriWithPort() $loop = Factory::create(); try { - $socket = new SocketServer('127.0.0.1:80', array('tls' => array( + $socket = new SocketServer('tls://127.0.0.1:80', array('tls' => array( 'local_cert' => __DIR__ . '/../examples/localhost.pem' )), $loop); } catch (\RuntimeException $e) { From 5d3013560286abdb8bb5f30072e6c8c4b662698f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 23 Jan 2022 11:38:37 +0100 Subject: [PATCH 387/456] Update test suite to use default loop --- composer.json | 2 +- tests/Client/FunctionalIntegrationTest.php | 36 ++- tests/FunctionalBrowserTest.php | 142 +++++------ tests/FunctionalHttpServerTest.php | 236 ++++++++---------- tests/HttpServerTest.php | 21 +- tests/Io/MiddlewareRunnerTest.php | 5 +- tests/Io/StreamingServerTest.php | 232 ++++++++--------- tests/Io/TransactionTest.php | 22 +- .../RequestBodyBufferMiddlewareTest.php | 20 +- 9 files changed, 329 insertions(+), 387 deletions(-) diff --git a/composer.json b/composer.json index 8a5c7df6..4c9a0383 100644 --- a/composer.json +++ b/composer.json @@ -38,7 +38,7 @@ "ringcentral/psr7": "^1.2" }, "require-dev": { - "clue/block-react": "^1.1", + "clue/block-react": "^1.5", "clue/http-proxy-react": "^1.7", "clue/reactphp-ssh-proxy": "^1.3", "clue/socks-react": "^1.3", diff --git a/tests/Client/FunctionalIntegrationTest.php b/tests/Client/FunctionalIntegrationTest.php index 6a62be93..64a3ea8a 100644 --- a/tests/Client/FunctionalIntegrationTest.php +++ b/tests/Client/FunctionalIntegrationTest.php @@ -4,7 +4,7 @@ use Clue\React\Block; use Psr\Http\Message\ResponseInterface; -use React\EventLoop\Factory; +use React\EventLoop\Loop; use React\Http\Client\Client; use React\Promise\Deferred; use React\Promise\Stream; @@ -37,9 +37,7 @@ class FunctionalIntegrationTest extends TestCase public function testRequestToLocalhostEmitsSingleRemoteConnection() { - $loop = Factory::create(); - - $socket = new SocketServer('127.0.0.1:0', array(), $loop); + $socket = new SocketServer('127.0.0.1:0'); $socket->on('connection', $this->expectCallableOnce()); $socket->on('connection', function (ConnectionInterface $conn) use ($socket) { $conn->end("HTTP/1.1 200 OK\r\n\r\nOk"); @@ -47,26 +45,24 @@ public function testRequestToLocalhostEmitsSingleRemoteConnection() }); $port = parse_url($socket->getAddress(), PHP_URL_PORT); - $client = new Client($loop); + $client = new Client(Loop::get()); $request = $client->request('GET', 'http://localhost:' . $port); $promise = Stream\first($request, 'close'); $request->end(); - Block\await($promise, $loop, self::TIMEOUT_LOCAL); + Block\await($promise, null, self::TIMEOUT_LOCAL); } public function testRequestLegacyHttpServerWithOnlyLineFeedReturnsSuccessfulResponse() { - $loop = Factory::create(); - - $socket = new SocketServer('127.0.0.1:0', array(), $loop); + $socket = new SocketServer('127.0.0.1:0'); $socket->on('connection', function (ConnectionInterface $conn) use ($socket) { $conn->end("HTTP/1.0 200 OK\n\nbody"); $socket->close(); }); - $client = new Client($loop); + $client = new Client(Loop::get()); $request = $client->request('GET', str_replace('tcp:', 'http:', $socket->getAddress())); $once = $this->expectCallableOnceWith('body'); @@ -77,7 +73,7 @@ public function testRequestLegacyHttpServerWithOnlyLineFeedReturnsSuccessfulResp $promise = Stream\first($request, 'close'); $request->end(); - Block\await($promise, $loop, self::TIMEOUT_LOCAL); + Block\await($promise, null, self::TIMEOUT_LOCAL); } /** @group internet */ @@ -86,8 +82,7 @@ public function testSuccessfulResponseEmitsEnd() // max_nesting_level was set to 100 for PHP Versions < 5.4 which resulted in failing test for legacy PHP ini_set('xdebug.max_nesting_level', 256); - $loop = Factory::create(); - $client = new Client($loop); + $client = new Client(Loop::get()); $request = $client->request('GET', 'http://www.google.com/'); @@ -99,7 +94,7 @@ public function testSuccessfulResponseEmitsEnd() $promise = Stream\first($request, 'close'); $request->end(); - Block\await($promise, $loop, self::TIMEOUT_REMOTE); + Block\await($promise, null, self::TIMEOUT_REMOTE); } /** @group internet */ @@ -112,8 +107,7 @@ public function testPostDataReturnsData() // max_nesting_level was set to 100 for PHP Versions < 5.4 which resulted in failing test for legacy PHP ini_set('xdebug.max_nesting_level', 256); - $loop = Factory::create(); - $client = new Client($loop); + $client = new Client(Loop::get()); $data = str_repeat('.', 33000); $request = $client->request('POST', 'https://' . (mt_rand(0, 1) === 0 ? 'eu.' : '') . 'httpbin.org/post', array('Content-Length' => strlen($data))); @@ -128,7 +122,7 @@ public function testPostDataReturnsData() $request->end($data); - $buffer = Block\await($deferred->promise(), $loop, self::TIMEOUT_REMOTE); + $buffer = Block\await($deferred->promise(), null, self::TIMEOUT_REMOTE); $this->assertNotEquals('', $buffer); @@ -145,8 +139,7 @@ public function testPostJsonReturnsData() $this->markTestSkipped('Not supported on HHVM'); } - $loop = Factory::create(); - $client = new Client($loop); + $client = new Client(Loop::get()); $data = json_encode(array('numbers' => range(1, 50))); $request = $client->request('POST', 'https://httpbin.org/post', array('Content-Length' => strlen($data), 'Content-Type' => 'application/json')); @@ -161,7 +154,7 @@ public function testPostJsonReturnsData() $request->end($data); - $buffer = Block\await($deferred->promise(), $loop, self::TIMEOUT_REMOTE); + $buffer = Block\await($deferred->promise(), null, self::TIMEOUT_REMOTE); $this->assertNotEquals('', $buffer); @@ -176,8 +169,7 @@ public function testCancelPendingConnectionEmitsClose() // max_nesting_level was set to 100 for PHP Versions < 5.4 which resulted in failing test for legacy PHP ini_set('xdebug.max_nesting_level', 256); - $loop = Factory::create(); - $client = new Client($loop); + $client = new Client(Loop::get()); $request = $client->request('GET', 'http://www.google.com/'); $request->on('error', $this->expectCallableNever()); diff --git a/tests/FunctionalBrowserTest.php b/tests/FunctionalBrowserTest.php index 5db62003..2b7bd58c 100644 --- a/tests/FunctionalBrowserTest.php +++ b/tests/FunctionalBrowserTest.php @@ -5,7 +5,7 @@ use Clue\React\Block; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; -use React\EventLoop\Factory; +use React\EventLoop\Loop; use React\Http\Browser; use React\Http\HttpServer; use React\Http\Message\ResponseException; @@ -21,7 +21,6 @@ class FunctionalBrowserTest extends TestCase { - private $loop; private $browser; private $base; @@ -30,10 +29,9 @@ class FunctionalBrowserTest extends TestCase */ public function setUpBrowserAndServer() { - $this->loop = $loop = Factory::create(); - $this->browser = new Browser(null, $this->loop); + $this->browser = new Browser(); - $http = new HttpServer($this->loop, new StreamingRequestMiddleware(), function (ServerRequestInterface $request) use ($loop) { + $http = new HttpServer(new StreamingRequestMiddleware(), function (ServerRequestInterface $request) { $path = $request->getUri()->getPath(); $headers = array(); @@ -90,8 +88,8 @@ public function setUpBrowserAndServer() } if ($path === '/delay/10') { - return new Promise(function ($resolve) use ($loop) { - $loop->addTimer(10, function () use ($resolve) { + return new Promise(function ($resolve) { + Loop::addTimer(10, function () use ($resolve) { $resolve(new Response( 200, array(), @@ -127,7 +125,7 @@ public function setUpBrowserAndServer() if ($path === '/stream/1') { $stream = new ThroughStream(); - $loop->futureTick(function () use ($stream, $headers) { + Loop::futureTick(function () use ($stream, $headers) { $stream->end(json_encode(array( 'headers' => $headers ))); @@ -142,7 +140,7 @@ public function setUpBrowserAndServer() var_dump($path); }); - $socket = new SocketServer('127.0.0.1:0', array(), $this->loop); + $socket = new SocketServer('127.0.0.1:0'); $http->listen($socket); $this->base = str_replace('tcp:', 'http:', $socket->getAddress()) . '/'; @@ -153,7 +151,7 @@ public function setUpBrowserAndServer() */ public function testSimpleRequest() { - Block\await($this->browser->get($this->base . 'get'), $this->loop); + Block\await($this->browser->get($this->base . 'get')); } public function testGetRequestWithRelativeAddressRejects() @@ -161,7 +159,7 @@ public function testGetRequestWithRelativeAddressRejects() $promise = $this->browser->get('delay'); $this->setExpectedException('InvalidArgumentException', 'Invalid request URL given'); - Block\await($promise, $this->loop); + Block\await($promise); } /** @@ -169,7 +167,7 @@ public function testGetRequestWithRelativeAddressRejects() */ public function testGetRequestWithBaseAndRelativeAddressResolves() { - Block\await($this->browser->withBase($this->base)->get('get'), $this->loop); + Block\await($this->browser->withBase($this->base)->get('get')); } /** @@ -177,7 +175,7 @@ public function testGetRequestWithBaseAndRelativeAddressResolves() */ public function testGetRequestWithBaseAndFullAddressResolves() { - Block\await($this->browser->withBase('http://example.com/')->get($this->base . 'get'), $this->loop); + Block\await($this->browser->withBase('http://example.com/')->get($this->base . 'get')); } public function testCancelGetRequestWillRejectRequest() @@ -186,7 +184,7 @@ public function testCancelGetRequestWillRejectRequest() $promise->cancel(); $this->setExpectedException('RuntimeException'); - Block\await($promise, $this->loop); + Block\await($promise); } public function testCancelRequestWithPromiseFollowerWillRejectRequest() @@ -197,13 +195,13 @@ public function testCancelRequestWithPromiseFollowerWillRejectRequest() $promise->cancel(); $this->setExpectedException('RuntimeException'); - Block\await($promise, $this->loop); + Block\await($promise); } public function testRequestWithoutAuthenticationFails() { $this->setExpectedException('RuntimeException'); - Block\await($this->browser->get($this->base . 'basic-auth/user/pass'), $this->loop); + Block\await($this->browser->get($this->base . 'basic-auth/user/pass')); } /** @@ -213,7 +211,7 @@ public function testRequestWithAuthenticationSucceeds() { $base = str_replace('://', '://user:pass@', $this->base); - Block\await($this->browser->get($base . 'basic-auth/user/pass'), $this->loop); + Block\await($this->browser->get($base . 'basic-auth/user/pass')); } /** @@ -227,7 +225,7 @@ public function testRedirectToPageWithAuthenticationSendsAuthenticationFromLocat { $target = str_replace('://', '://user:pass@', $this->base) . 'basic-auth/user/pass'; - Block\await($this->browser->get($this->base . 'redirect-to?url=' . urlencode($target)), $this->loop); + Block\await($this->browser->get($this->base . 'redirect-to?url=' . urlencode($target))); } /** @@ -242,19 +240,19 @@ public function testRedirectFromPageWithInvalidAuthToPageWithCorrectAuthenticati $base = str_replace('://', '://unknown:invalid@', $this->base); $target = str_replace('://', '://user:pass@', $this->base) . 'basic-auth/user/pass'; - Block\await($this->browser->get($base . 'redirect-to?url=' . urlencode($target)), $this->loop); + Block\await($this->browser->get($base . 'redirect-to?url=' . urlencode($target))); } public function testCancelRedirectedRequestShouldReject() { $promise = $this->browser->get($this->base . 'redirect-to?url=delay%2F10'); - $this->loop->addTimer(0.1, function () use ($promise) { + Loop::addTimer(0.1, function () use ($promise) { $promise->cancel(); }); $this->setExpectedException('RuntimeException', 'Request cancelled'); - Block\await($promise, $this->loop); + Block\await($promise); } public function testTimeoutDelayedResponseShouldReject() @@ -262,7 +260,7 @@ public function testTimeoutDelayedResponseShouldReject() $promise = $this->browser->withTimeout(0.1)->get($this->base . 'delay/10'); $this->setExpectedException('RuntimeException', 'Request timed out after 0.1 seconds'); - Block\await($promise, $this->loop); + Block\await($promise); } public function testTimeoutDelayedResponseAfterStreamingRequestShouldReject() @@ -272,7 +270,7 @@ public function testTimeoutDelayedResponseAfterStreamingRequestShouldReject() $stream->end(); $this->setExpectedException('RuntimeException', 'Request timed out after 0.1 seconds'); - Block\await($promise, $this->loop); + Block\await($promise); } /** @@ -280,7 +278,7 @@ public function testTimeoutDelayedResponseAfterStreamingRequestShouldReject() */ public function testTimeoutFalseShouldResolveSuccessfully() { - Block\await($this->browser->withTimeout(false)->get($this->base . 'get'), $this->loop); + Block\await($this->browser->withTimeout(false)->get($this->base . 'get')); } /** @@ -288,7 +286,7 @@ public function testTimeoutFalseShouldResolveSuccessfully() */ public function testRedirectRequestRelative() { - Block\await($this->browser->get($this->base . 'redirect-to?url=get'), $this->loop); + Block\await($this->browser->get($this->base . 'redirect-to?url=get')); } /** @@ -296,7 +294,7 @@ public function testRedirectRequestRelative() */ public function testRedirectRequestAbsolute() { - Block\await($this->browser->get($this->base . 'redirect-to?url=' . urlencode($this->base . 'get')), $this->loop); + Block\await($this->browser->get($this->base . 'redirect-to?url=' . urlencode($this->base . 'get'))); } /** @@ -306,7 +304,7 @@ public function testFollowingRedirectsFalseResolvesWithRedirectResult() { $browser = $this->browser->withFollowRedirects(false); - Block\await($browser->get($this->base . 'redirect-to?url=get'), $this->loop); + Block\await($browser->get($this->base . 'redirect-to?url=get')); } public function testFollowRedirectsZeroRejectsOnRedirect() @@ -314,12 +312,12 @@ public function testFollowRedirectsZeroRejectsOnRedirect() $browser = $this->browser->withFollowRedirects(0); $this->setExpectedException('RuntimeException'); - Block\await($browser->get($this->base . 'redirect-to?url=get'), $this->loop); + Block\await($browser->get($this->base . 'redirect-to?url=get')); } public function testResponseStatus204ShouldResolveWithEmptyBody() { - $response = Block\await($this->browser->get($this->base . 'status/204'), $this->loop); + $response = Block\await($this->browser->get($this->base . 'status/204')); $this->assertFalse($response->hasHeader('Content-Length')); $body = $response->getBody(); @@ -329,7 +327,7 @@ public function testResponseStatus204ShouldResolveWithEmptyBody() public function testResponseStatus304ShouldResolveWithEmptyBodyButContentLengthResponseHeader() { - $response = Block\await($this->browser->get($this->base . 'status/304'), $this->loop); + $response = Block\await($this->browser->get($this->base . 'status/304')); $this->assertEquals('12', $response->getHeaderLine('Content-Length')); $body = $response->getBody(); @@ -344,7 +342,7 @@ public function testGetRequestWithResponseBufferMatchedExactlyResolves() { $promise = $this->browser->withResponseBuffer(5)->get($this->base . 'get'); - Block\await($promise, $this->loop); + Block\await($promise); } public function testGetRequestWithResponseBufferExceededRejects() @@ -356,7 +354,7 @@ public function testGetRequestWithResponseBufferExceededRejects() 'Response body size of 5 bytes exceeds maximum of 4 bytes', defined('SOCKET_EMSGSIZE') ? SOCKET_EMSGSIZE : 0 ); - Block\await($promise, $this->loop); + Block\await($promise); } public function testGetRequestWithResponseBufferExceededDuringStreamingRejects() @@ -368,7 +366,7 @@ public function testGetRequestWithResponseBufferExceededDuringStreamingRejects() 'Response body size exceeds maximum of 4 bytes', defined('SOCKET_EMSGSIZE') ? SOCKET_EMSGSIZE : 0 ); - Block\await($promise, $this->loop); + Block\await($promise); } /** @@ -381,7 +379,7 @@ public function testCanAccessHttps() $this->markTestSkipped('Not supported on HHVM'); } - Block\await($this->browser->get('https://www.google.com/'), $this->loop); + Block\await($this->browser->get('https://www.google.com/')); } /** @@ -397,12 +395,12 @@ public function testVerifyPeerEnabledForBadSslRejects() 'tls' => array( 'verify_peer' => true ) - ), $this->loop); + )); - $browser = new Browser($connector, $this->loop); + $browser = new Browser($connector); $this->setExpectedException('RuntimeException'); - Block\await($browser->get('https://self-signed.badssl.com/'), $this->loop); + Block\await($browser->get('https://self-signed.badssl.com/')); } /** @@ -419,11 +417,11 @@ public function testVerifyPeerDisabledForBadSslResolves() 'tls' => array( 'verify_peer' => false ) - ), $this->loop); + )); - $browser = new Browser($connector, $this->loop); + $browser = new Browser($connector); - Block\await($browser->get('https://self-signed.badssl.com/'), $this->loop); + Block\await($browser->get('https://self-signed.badssl.com/')); } /** @@ -432,13 +430,13 @@ public function testVerifyPeerDisabledForBadSslResolves() public function testInvalidPort() { $this->setExpectedException('RuntimeException'); - Block\await($this->browser->get('http://www.google.com:443/'), $this->loop); + Block\await($this->browser->get('http://www.google.com:443/')); } public function testErrorStatusCodeRejectsWithResponseException() { try { - Block\await($this->browser->get($this->base . 'status/404'), $this->loop); + Block\await($this->browser->get($this->base . 'status/404')); $this->fail(); } catch (ResponseException $e) { $this->assertEquals(404, $e->getCode()); @@ -450,14 +448,14 @@ public function testErrorStatusCodeRejectsWithResponseException() public function testErrorStatusCodeDoesNotRejectWithRejectErrorResponseFalse() { - $response = Block\await($this->browser->withRejectErrorResponse(false)->get($this->base . 'status/404'), $this->loop); + $response = Block\await($this->browser->withRejectErrorResponse(false)->get($this->base . 'status/404')); $this->assertEquals(404, $response->getStatusCode()); } public function testPostString() { - $response = Block\await($this->browser->post($this->base . 'post', array(), 'hello world'), $this->loop); + $response = Block\await($this->browser->post($this->base . 'post', array(), 'hello world')); $data = json_decode((string)$response->getBody(), true); $this->assertEquals('hello world', $data['data']); @@ -465,7 +463,7 @@ public function testPostString() public function testRequestStreamReturnsResponseBodyUntilConnectionsEndsForHttp10() { - $response = Block\await($this->browser->withProtocolVersion('1.0')->get($this->base . 'stream/1'), $this->loop); + $response = Block\await($this->browser->withProtocolVersion('1.0')->get($this->base . 'stream/1')); $this->assertEquals('1.0', $response->getProtocolVersion()); $this->assertFalse($response->hasHeader('Transfer-Encoding')); @@ -476,7 +474,7 @@ public function testRequestStreamReturnsResponseBodyUntilConnectionsEndsForHttp1 public function testRequestStreamReturnsResponseWithTransferEncodingChunkedAndResponseBodyDecodedForHttp11() { - $response = Block\await($this->browser->get($this->base . 'stream/1'), $this->loop); + $response = Block\await($this->browser->get($this->base . 'stream/1')); $this->assertEquals('1.1', $response->getProtocolVersion()); @@ -488,7 +486,7 @@ public function testRequestStreamReturnsResponseWithTransferEncodingChunkedAndRe public function testRequestStreamWithHeadRequestReturnsEmptyResponseBodWithTransferEncodingChunkedForHttp11() { - $response = Block\await($this->browser->head($this->base . 'stream/1'), $this->loop); + $response = Block\await($this->browser->head($this->base . 'stream/1')); $this->assertEquals('1.1', $response->getProtocolVersion()); @@ -498,7 +496,7 @@ public function testRequestStreamWithHeadRequestReturnsEmptyResponseBodWithTrans public function testRequestStreamReturnsResponseWithResponseBodyUndecodedWhenResponseHasDoubleTransferEncoding() { - $socket = new SocketServer('127.0.0.1:0', array(), $this->loop); + $socket = new SocketServer('127.0.0.1:0'); $socket->on('connection', function (\React\Socket\ConnectionInterface $connection) { $connection->on('data', function () use ($connection) { $connection->end("HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked, chunked\r\nConnection: close\r\n\r\nhello"); @@ -507,7 +505,7 @@ public function testRequestStreamReturnsResponseWithResponseBodyUndecodedWhenRes $this->base = str_replace('tcp:', 'http:', $socket->getAddress()) . '/'; - $response = Block\await($this->browser->get($this->base . 'stream/1'), $this->loop); + $response = Block\await($this->browser->get($this->base . 'stream/1')); $this->assertEquals('1.1', $response->getProtocolVersion()); @@ -518,7 +516,7 @@ public function testRequestStreamReturnsResponseWithResponseBodyUndecodedWhenRes public function testReceiveStreamAndExplicitlyCloseConnectionEvenWhenServerKeepsConnectionOpen() { $closed = new \React\Promise\Deferred(); - $socket = new SocketServer('127.0.0.1:0', array(), $this->loop); + $socket = new SocketServer('127.0.0.1:0'); $socket->on('connection', function (\React\Socket\ConnectionInterface $connection) use ($closed) { $connection->on('data', function () use ($connection) { $connection->write("HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello"); @@ -530,10 +528,10 @@ public function testReceiveStreamAndExplicitlyCloseConnectionEvenWhenServerKeeps $this->base = str_replace('tcp:', 'http:', $socket->getAddress()) . '/'; - $response = Block\await($this->browser->get($this->base . 'get', array()), $this->loop); + $response = Block\await($this->browser->get($this->base . 'get', array())); $this->assertEquals('hello', (string)$response->getBody()); - $ret = Block\await($closed->promise(), $this->loop, 0.1); + $ret = Block\await($closed->promise(), null, 0.1); $this->assertTrue($ret); $socket->close(); @@ -543,11 +541,11 @@ public function testPostStreamChunked() { $stream = new ThroughStream(); - $this->loop->addTimer(0.001, function () use ($stream) { + Loop::addTimer(0.001, function () use ($stream) { $stream->end('hello world'); }); - $response = Block\await($this->browser->post($this->base . 'post', array(), $stream), $this->loop); + $response = Block\await($this->browser->post($this->base . 'post', array(), $stream)); $data = json_decode((string)$response->getBody(), true); $this->assertEquals('hello world', $data['data']); @@ -559,11 +557,11 @@ public function testPostStreamKnownLength() { $stream = new ThroughStream(); - $this->loop->addTimer(0.001, function () use ($stream) { + Loop::addTimer(0.001, function () use ($stream) { $stream->end('hello world'); }); - $response = Block\await($this->browser->post($this->base . 'post', array('Content-Length' => 11), $stream), $this->loop); + $response = Block\await($this->browser->post($this->base . 'post', array('Content-Length' => 11), $stream)); $data = json_decode((string)$response->getBody(), true); $this->assertEquals('hello world', $data['data']); @@ -574,16 +572,16 @@ public function testPostStreamKnownLength() */ public function testPostStreamWillStartSendingRequestEvenWhenBodyDoesNotEmitData() { - $http = new HttpServer($this->loop, new StreamingRequestMiddleware(), function (ServerRequestInterface $request) { + $http = new HttpServer(new StreamingRequestMiddleware(), function (ServerRequestInterface $request) { return new Response(200); }); - $socket = new SocketServer('127.0.0.1:0', array(), $this->loop); + $socket = new SocketServer('127.0.0.1:0'); $http->listen($socket); $this->base = str_replace('tcp:', 'http:', $socket->getAddress()) . '/'; $stream = new ThroughStream(); - Block\await($this->browser->post($this->base . 'post', array(), $stream), $this->loop); + Block\await($this->browser->post($this->base . 'post', array(), $stream)); $socket->close(); } @@ -593,7 +591,7 @@ public function testPostStreamClosed() $stream = new ThroughStream(); $stream->close(); - $response = Block\await($this->browser->post($this->base . 'post', array(), $stream), $this->loop); + $response = Block\await($this->browser->post($this->base . 'post', array(), $stream)); $data = json_decode((string)$response->getBody(), true); $this->assertEquals('', $data['data']); @@ -601,19 +599,19 @@ public function testPostStreamClosed() public function testSendsHttp11ByDefault() { - $http = new HttpServer($this->loop, function (ServerRequestInterface $request) { + $http = new HttpServer(function (ServerRequestInterface $request) { return new Response( 200, array(), $request->getProtocolVersion() ); }); - $socket = new SocketServer('127.0.0.1:0', array(), $this->loop); + $socket = new SocketServer('127.0.0.1:0'); $http->listen($socket); $this->base = str_replace('tcp:', 'http:', $socket->getAddress()) . '/'; - $response = Block\await($this->browser->get($this->base), $this->loop); + $response = Block\await($this->browser->get($this->base)); $this->assertEquals('1.1', (string)$response->getBody()); $socket->close(); @@ -621,19 +619,19 @@ public function testSendsHttp11ByDefault() public function testSendsExplicitHttp10Request() { - $http = new HttpServer($this->loop, function (ServerRequestInterface $request) { + $http = new HttpServer(function (ServerRequestInterface $request) { return new Response( 200, array(), $request->getProtocolVersion() ); }); - $socket = new SocketServer('127.0.0.1:0', array(), $this->loop); + $socket = new SocketServer('127.0.0.1:0'); $http->listen($socket); $this->base = str_replace('tcp:', 'http:', $socket->getAddress()) . '/'; - $response = Block\await($this->browser->withProtocolVersion('1.0')->get($this->base), $this->loop); + $response = Block\await($this->browser->withProtocolVersion('1.0')->get($this->base)); $this->assertEquals('1.0', (string)$response->getBody()); $socket->close(); @@ -641,7 +639,7 @@ public function testSendsExplicitHttp10Request() public function testHeadRequestReceivesResponseWithEmptyBodyButWithContentLengthResponseHeader() { - $response = Block\await($this->browser->head($this->base . 'get'), $this->loop); + $response = Block\await($this->browser->head($this->base . 'get')); $this->assertEquals('5', $response->getHeaderLine('Content-Length')); $body = $response->getBody(); @@ -651,7 +649,7 @@ public function testHeadRequestReceivesResponseWithEmptyBodyButWithContentLength public function testRequestStreamingGetReceivesResponseWithStreamingBodyAndKnownSize() { - $response = Block\await($this->browser->requestStreaming('GET', $this->base . 'get'), $this->loop); + $response = Block\await($this->browser->requestStreaming('GET', $this->base . 'get')); $this->assertEquals('5', $response->getHeaderLine('Content-Length')); $body = $response->getBody(); @@ -662,7 +660,7 @@ public function testRequestStreamingGetReceivesResponseWithStreamingBodyAndKnown public function testRequestStreamingGetReceivesResponseWithStreamingBodyAndUnknownSizeFromStreamingEndpoint() { - $response = Block\await($this->browser->requestStreaming('GET', $this->base . 'stream/1'), $this->loop); + $response = Block\await($this->browser->requestStreaming('GET', $this->base . 'stream/1')); $this->assertFalse($response->hasHeader('Content-Length')); $body = $response->getBody(); @@ -676,8 +674,7 @@ public function testRequestStreamingGetReceivesStreamingResponseBody() $buffer = Block\await( $this->browser->requestStreaming('GET', $this->base . 'get')->then(function (ResponseInterface $response) { return Stream\buffer($response->getBody()); - }), - $this->loop + }) ); $this->assertEquals('hello', $buffer); @@ -688,8 +685,7 @@ public function testRequestStreamingGetReceivesStreamingResponseBodyEvenWhenResp $buffer = Block\await( $this->browser->withResponseBuffer(4)->requestStreaming('GET', $this->base . 'get')->then(function (ResponseInterface $response) { return Stream\buffer($response->getBody()); - }), - $this->loop + }) ); $this->assertEquals('hello', $buffer); diff --git a/tests/FunctionalHttpServerTest.php b/tests/FunctionalHttpServerTest.php index a5274533..6fa85903 100644 --- a/tests/FunctionalHttpServerTest.php +++ b/tests/FunctionalHttpServerTest.php @@ -5,7 +5,7 @@ use Clue\React\Block; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ServerRequestInterface; -use React\EventLoop\Factory; +use React\EventLoop\Loop; use React\Http\HttpServer; use React\Http\Message\Response; use React\Http\Middleware\LimitConcurrentRequestsMiddleware; @@ -22,14 +22,13 @@ class FunctionalHttpServerTest extends TestCase { public function testPlainHttpOnRandomPort() { - $loop = Factory::create(); - $connector = new Connector(array(), $loop); + $connector = new Connector(); - $http = new HttpServer($loop, function (RequestInterface $request) { + $http = new HttpServer(function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); - $socket = new SocketServer('127.0.0.1:0', array(), $loop); + $socket = new SocketServer('127.0.0.1:0'); $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { @@ -38,7 +37,7 @@ public function testPlainHttpOnRandomPort() return Stream\buffer($conn); }); - $response = Block\await($result, $loop, 1.0); + $response = Block\await($result, null, 1.0); $this->assertContainsString("HTTP/1.0 200 OK", $response); $this->assertContainsString('http://' . noScheme($socket->getAddress()) . '/', $response); @@ -48,17 +47,15 @@ public function testPlainHttpOnRandomPort() public function testPlainHttpOnRandomPortWithSingleRequestHandlerArray() { - $loop = Factory::create(); - $connector = new Connector(array(), $loop); + $connector = new Connector(); $http = new HttpServer( - $loop, function () { return new Response(404); } ); - $socket = new SocketServer('127.0.0.1:0', array(), $loop); + $socket = new SocketServer('127.0.0.1:0'); $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { @@ -67,7 +64,7 @@ function () { return Stream\buffer($conn); }); - $response = Block\await($result, $loop, 1.0); + $response = Block\await($result, null, 1.0); $this->assertContainsString("HTTP/1.0 404 Not Found", $response); @@ -76,14 +73,13 @@ function () { public function testPlainHttpOnRandomPortWithoutHostHeaderUsesSocketUri() { - $loop = Factory::create(); - $connector = new Connector(array(), $loop); + $connector = new Connector(); - $http = new HttpServer($loop, function (RequestInterface $request) { + $http = new HttpServer(function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); - $socket = new SocketServer('127.0.0.1:0', array(), $loop); + $socket = new SocketServer('127.0.0.1:0'); $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { @@ -92,7 +88,7 @@ public function testPlainHttpOnRandomPortWithoutHostHeaderUsesSocketUri() return Stream\buffer($conn); }); - $response = Block\await($result, $loop, 1.0); + $response = Block\await($result, null, 1.0); $this->assertContainsString("HTTP/1.0 200 OK", $response); $this->assertContainsString('http://' . noScheme($socket->getAddress()) . '/', $response); @@ -102,14 +98,13 @@ public function testPlainHttpOnRandomPortWithoutHostHeaderUsesSocketUri() public function testPlainHttpOnRandomPortWithOtherHostHeaderTakesPrecedence() { - $loop = Factory::create(); - $connector = new Connector(array(), $loop); + $connector = new Connector(); - $http = new HttpServer($loop, function (RequestInterface $request) { + $http = new HttpServer(function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); - $socket = new SocketServer('127.0.0.1:0', array(), $loop); + $socket = new SocketServer('127.0.0.1:0'); $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { @@ -118,7 +113,7 @@ public function testPlainHttpOnRandomPortWithOtherHostHeaderTakesPrecedence() return Stream\buffer($conn); }); - $response = Block\await($result, $loop, 1.0); + $response = Block\await($result, null, 1.0); $this->assertContainsString("HTTP/1.0 200 OK", $response); $this->assertContainsString('http://localhost:1000/', $response); @@ -132,18 +127,17 @@ public function testSecureHttpsOnRandomPort() $this->markTestSkipped('Not supported on HHVM'); } - $loop = Factory::create(); $connector = new Connector(array( 'tls' => array('verify_peer' => false) - ), $loop); + )); - $http = new HttpServer($loop, function (RequestInterface $request) { + $http = new HttpServer(function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); $socket = new SocketServer('tls://127.0.0.1:0', array('tls' => array( 'local_cert' => __DIR__ . '/../examples/localhost.pem' - )), $loop); + ))); $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { @@ -152,7 +146,7 @@ public function testSecureHttpsOnRandomPort() return Stream\buffer($conn); }); - $response = Block\await($result, $loop, 1.0); + $response = Block\await($result, null, 1.0); $this->assertContainsString("HTTP/1.0 200 OK", $response); $this->assertContainsString('https://' . noScheme($socket->getAddress()) . '/', $response); @@ -166,9 +160,7 @@ public function testSecureHttpsReturnsData() $this->markTestSkipped('Not supported on HHVM'); } - $loop = Factory::create(); - - $http = new HttpServer($loop, function (RequestInterface $request) { + $http = new HttpServer(function (RequestInterface $request) { return new Response( 200, array(), @@ -178,12 +170,12 @@ public function testSecureHttpsReturnsData() $socket = new SocketServer('tls://127.0.0.1:0', array('tls' => array( 'local_cert' => __DIR__ . '/../examples/localhost.pem' - )), $loop); + ))); $http->listen($socket); $connector = new Connector(array( 'tls' => array('verify_peer' => false) - ), $loop); + )); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\nHost: " . noScheme($conn->getRemoteAddress()) . "\r\n\r\n"); @@ -191,7 +183,7 @@ public function testSecureHttpsReturnsData() return Stream\buffer($conn); }); - $response = Block\await($result, $loop, 1.0); + $response = Block\await($result, null, 1.0); $this->assertContainsString("HTTP/1.0 200 OK", $response); $this->assertContainsString("\r\nContent-Length: 33000\r\n", $response); @@ -206,18 +198,17 @@ public function testSecureHttpsOnRandomPortWithoutHostHeaderUsesSocketUri() $this->markTestSkipped('Not supported on HHVM'); } - $loop = Factory::create(); $connector = new Connector(array( 'tls' => array('verify_peer' => false) - ), $loop); + )); - $http = new HttpServer($loop, function (RequestInterface $request) { + $http = new HttpServer(function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); $socket = new SocketServer('tls://127.0.0.1:0', array('tls' => array( 'local_cert' => __DIR__ . '/../examples/localhost.pem' - )), $loop); + ))); $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { @@ -226,7 +217,7 @@ public function testSecureHttpsOnRandomPortWithoutHostHeaderUsesSocketUri() return Stream\buffer($conn); }); - $response = Block\await($result, $loop, 1.0); + $response = Block\await($result, null, 1.0); $this->assertContainsString("HTTP/1.0 200 OK", $response); $this->assertContainsString('https://' . noScheme($socket->getAddress()) . '/', $response); @@ -236,15 +227,14 @@ public function testSecureHttpsOnRandomPortWithoutHostHeaderUsesSocketUri() public function testPlainHttpOnStandardPortReturnsUriWithNoPort() { - $loop = Factory::create(); try { - $socket = new SocketServer('127.0.0.1:80', array(), $loop); + $socket = new SocketServer('127.0.0.1:80'); } catch (\RuntimeException $e) { $this->markTestSkipped('Listening on port 80 failed (root and unused?)'); } - $connector = new Connector(array(), $loop); + $connector = new Connector(); - $http = new HttpServer($loop, function (RequestInterface $request) { + $http = new HttpServer(function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); @@ -256,7 +246,7 @@ public function testPlainHttpOnStandardPortReturnsUriWithNoPort() return Stream\buffer($conn); }); - $response = Block\await($result, $loop, 1.0); + $response = Block\await($result, null, 1.0); $this->assertContainsString("HTTP/1.0 200 OK", $response); $this->assertContainsString('http://127.0.0.1/', $response); @@ -266,15 +256,14 @@ public function testPlainHttpOnStandardPortReturnsUriWithNoPort() public function testPlainHttpOnStandardPortWithoutHostHeaderReturnsUriWithNoPort() { - $loop = Factory::create(); try { - $socket = new SocketServer('127.0.0.1:80', array(), $loop); + $socket = new SocketServer('127.0.0.1:80'); } catch (\RuntimeException $e) { $this->markTestSkipped('Listening on port 80 failed (root and unused?)'); } - $connector = new Connector(array(), $loop); + $connector = new Connector(); - $http = new HttpServer($loop, function (RequestInterface $request) { + $http = new HttpServer(function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); @@ -286,7 +275,7 @@ public function testPlainHttpOnStandardPortWithoutHostHeaderReturnsUriWithNoPort return Stream\buffer($conn); }); - $response = Block\await($result, $loop, 1.0); + $response = Block\await($result, null, 1.0); $this->assertContainsString("HTTP/1.0 200 OK", $response); $this->assertContainsString('http://127.0.0.1/', $response); @@ -300,20 +289,19 @@ public function testSecureHttpsOnStandardPortReturnsUriWithNoPort() $this->markTestSkipped('Not supported on HHVM'); } - $loop = Factory::create(); try { $socket = new SocketServer('tls://127.0.0.1:443', array('tls' => array( 'local_cert' => __DIR__ . '/../examples/localhost.pem' - )), $loop); + ))); } catch (\RuntimeException $e) { $this->markTestSkipped('Listening on port 443 failed (root and unused?)'); } $connector = new Connector(array( 'tls' => array('verify_peer' => false) - ), $loop); + )); - $http = new HttpServer($loop, function (RequestInterface $request) { + $http = new HttpServer(function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); @@ -325,7 +313,7 @@ public function testSecureHttpsOnStandardPortReturnsUriWithNoPort() return Stream\buffer($conn); }); - $response = Block\await($result, $loop, 1.0); + $response = Block\await($result, null, 1.0); $this->assertContainsString("HTTP/1.0 200 OK", $response); $this->assertContainsString('https://127.0.0.1/', $response); @@ -339,20 +327,19 @@ public function testSecureHttpsOnStandardPortWithoutHostHeaderUsesSocketUri() $this->markTestSkipped('Not supported on HHVM'); } - $loop = Factory::create(); try { $socket = new SocketServer('tls://127.0.0.1:443', array('tls' => array( 'local_cert' => __DIR__ . '/../examples/localhost.pem' - )), $loop); + ))); } catch (\RuntimeException $e) { $this->markTestSkipped('Listening on port 443 failed (root and unused?)'); } $connector = new Connector(array( 'tls' => array('verify_peer' => false) - ), $loop); + )); - $http = new HttpServer($loop, function (RequestInterface $request) { + $http = new HttpServer(function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); @@ -364,7 +351,7 @@ public function testSecureHttpsOnStandardPortWithoutHostHeaderUsesSocketUri() return Stream\buffer($conn); }); - $response = Block\await($result, $loop, 1.0); + $response = Block\await($result, null, 1.0); $this->assertContainsString("HTTP/1.0 200 OK", $response); $this->assertContainsString('https://127.0.0.1/', $response); @@ -374,15 +361,14 @@ public function testSecureHttpsOnStandardPortWithoutHostHeaderUsesSocketUri() public function testPlainHttpOnHttpsStandardPortReturnsUriWithPort() { - $loop = Factory::create(); try { - $socket = new SocketServer('127.0.0.1:443', array(), $loop); + $socket = new SocketServer('127.0.0.1:443'); } catch (\RuntimeException $e) { $this->markTestSkipped('Listening on port 443 failed (root and unused?)'); } - $connector = new Connector(array(), $loop); + $connector = new Connector(); - $http = new HttpServer($loop, function (RequestInterface $request) { + $http = new HttpServer(function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); @@ -394,7 +380,7 @@ public function testPlainHttpOnHttpsStandardPortReturnsUriWithPort() return Stream\buffer($conn); }); - $response = Block\await($result, $loop, 1.0); + $response = Block\await($result, null, 1.0); $this->assertContainsString("HTTP/1.0 200 OK", $response); $this->assertContainsString('http://127.0.0.1:443/', $response); @@ -408,20 +394,19 @@ public function testSecureHttpsOnHttpStandardPortReturnsUriWithPort() $this->markTestSkipped('Not supported on HHVM'); } - $loop = Factory::create(); try { $socket = new SocketServer('tls://127.0.0.1:80', array('tls' => array( 'local_cert' => __DIR__ . '/../examples/localhost.pem' - )), $loop); + ))); } catch (\RuntimeException $e) { $this->markTestSkipped('Listening on port 80 failed (root and unused?)'); } $connector = new Connector(array( 'tls' => array('verify_peer' => false) - ), $loop); + )); - $http = new HttpServer($loop, function (RequestInterface $request) { + $http = new HttpServer(function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri() . 'x' . $request->getHeaderLine('Host')); }); @@ -433,7 +418,7 @@ public function testSecureHttpsOnHttpStandardPortReturnsUriWithPort() return Stream\buffer($conn); }); - $response = Block\await($result, $loop, 1.0); + $response = Block\await($result, null, 1.0); $this->assertContainsString("HTTP/1.0 200 OK", $response); $this->assertContainsString('https://127.0.0.1:80/', $response); @@ -443,17 +428,16 @@ public function testSecureHttpsOnHttpStandardPortReturnsUriWithPort() public function testClosedStreamFromRequestHandlerWillSendEmptyBody() { - $loop = Factory::create(); - $connector = new Connector(array(), $loop); + $connector = new Connector(); $stream = new ThroughStream(); $stream->close(); - $http = new HttpServer($loop, function (RequestInterface $request) use ($stream) { + $http = new HttpServer(function (RequestInterface $request) use ($stream) { return new Response(200, array(), $stream); }); - $socket = new SocketServer('127.0.0.1:0', array(), $loop); + $socket = new SocketServer('127.0.0.1:0'); $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { @@ -462,7 +446,7 @@ public function testClosedStreamFromRequestHandlerWillSendEmptyBody() return Stream\buffer($conn); }); - $response = Block\await($result, $loop, 1.0); + $response = Block\await($result, null, 1.0); $this->assertStringStartsWith("HTTP/1.0 200 OK", $response); $this->assertStringEndsWith("\r\n\r\n", $response); @@ -472,62 +456,58 @@ public function testClosedStreamFromRequestHandlerWillSendEmptyBody() public function testRequestHandlerWithStreamingRequestWillReceiveCloseEventIfConnectionClosesWhileSendingBody() { - $loop = Factory::create(); - $connector = new Connector(array(), $loop); + $connector = new Connector(); $once = $this->expectCallableOnce(); $http = new HttpServer( - $loop, new StreamingRequestMiddleware(), function (RequestInterface $request) use ($once) { $request->getBody()->on('close', $once); } ); - $socket = new SocketServer('127.0.0.1:0', array(), $loop); + $socket = new SocketServer('127.0.0.1:0'); $http->listen($socket); - $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) use ($loop) { + $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\nContent-Length: 100\r\n\r\n"); - $loop->addTimer(0.001, function() use ($conn) { + Loop::addTimer(0.001, function() use ($conn) { $conn->end(); }); }); - Block\sleep(0.1, $loop); + Block\sleep(0.1); $socket->close(); } public function testStreamFromRequestHandlerWillBeClosedIfConnectionClosesWhileSendingStreamingRequestBody() { - $loop = Factory::create(); - $connector = new Connector(array(), $loop); + $connector = new Connector(); $stream = new ThroughStream(); $http = new HttpServer( - $loop, new StreamingRequestMiddleware(), function (RequestInterface $request) use ($stream) { return new Response(200, array(), $stream); } ); - $socket = new SocketServer('127.0.0.1:0', array(), $loop); + $socket = new SocketServer('127.0.0.1:0'); $http->listen($socket); - $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) use ($loop) { + $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\nContent-Length: 100\r\n\r\n"); - $loop->addTimer(0.001, function() use ($conn) { + Loop::addTimer(0.001, function() use ($conn) { $conn->end(); }); }); // stream will be closed within 0.1s - $ret = Block\await(Stream\first($stream, 'close'), $loop, 0.1); + $ret = Block\await(Stream\first($stream, 'close'), null, 0.1); $socket->close(); @@ -536,28 +516,27 @@ function (RequestInterface $request) use ($stream) { public function testStreamFromRequestHandlerWillBeClosedIfConnectionCloses() { - $loop = Factory::create(); - $connector = new Connector(array(), $loop); + $connector = new Connector(); $stream = new ThroughStream(); - $http = new HttpServer($loop, function (RequestInterface $request) use ($stream) { + $http = new HttpServer(function (RequestInterface $request) use ($stream) { return new Response(200, array(), $stream); }); - $socket = new SocketServer('127.0.0.1:0', array(), $loop); + $socket = new SocketServer('127.0.0.1:0'); $http->listen($socket); - $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) use ($loop) { + $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\n\r\n"); - $loop->addTimer(0.1, function () use ($conn) { + Loop::addTimer(0.1, function () use ($conn) { $conn->close(); }); }); // await response stream to be closed - $ret = Block\await(Stream\first($stream, 'close'), $loop, 1.0); + $ret = Block\await(Stream\first($stream, 'close'), null, 1.0); $socket->close(); @@ -566,20 +545,19 @@ public function testStreamFromRequestHandlerWillBeClosedIfConnectionCloses() public function testUpgradeWithThroughStreamReturnsDataAsGiven() { - $loop = Factory::create(); - $connector = new Connector(array(), $loop); + $connector = new Connector(); - $http = new HttpServer($loop, function (RequestInterface $request) use ($loop) { + $http = new HttpServer(function (RequestInterface $request) { $stream = new ThroughStream(); - $loop->addTimer(0.1, function () use ($stream) { + Loop::addTimer(0.1, function () use ($stream) { $stream->end(); }); return new Response(101, array('Upgrade' => 'echo'), $stream); }); - $socket = new SocketServer('127.0.0.1:0', array(), $loop); + $socket = new SocketServer('127.0.0.1:0'); $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { @@ -593,7 +571,7 @@ public function testUpgradeWithThroughStreamReturnsDataAsGiven() return Stream\buffer($conn); }); - $response = Block\await($result, $loop, 1.0); + $response = Block\await($result, null, 1.0); $this->assertStringStartsWith("HTTP/1.1 101 Switching Protocols\r\n", $response); $this->assertStringEndsWith("\r\n\r\nhelloworld", $response); @@ -603,20 +581,19 @@ public function testUpgradeWithThroughStreamReturnsDataAsGiven() public function testUpgradeWithRequestBodyAndThroughStreamReturnsDataAsGiven() { - $loop = Factory::create(); - $connector = new Connector(array(), $loop); + $connector = new Connector(); - $http = new HttpServer($loop, function (RequestInterface $request) use ($loop) { + $http = new HttpServer(function (RequestInterface $request) { $stream = new ThroughStream(); - $loop->addTimer(0.1, function () use ($stream) { + Loop::addTimer(0.1, function () use ($stream) { $stream->end(); }); return new Response(101, array('Upgrade' => 'echo'), $stream); }); - $socket = new SocketServer('127.0.0.1:0', array(), $loop); + $socket = new SocketServer('127.0.0.1:0'); $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { @@ -631,7 +608,7 @@ public function testUpgradeWithRequestBodyAndThroughStreamReturnsDataAsGiven() return Stream\buffer($conn); }); - $response = Block\await($result, $loop, 1.0); + $response = Block\await($result, null, 1.0); $this->assertStringStartsWith("HTTP/1.1 101 Switching Protocols\r\n", $response); $this->assertStringEndsWith("\r\n\r\nhelloworld", $response); @@ -641,20 +618,19 @@ public function testUpgradeWithRequestBodyAndThroughStreamReturnsDataAsGiven() public function testConnectWithThroughStreamReturnsDataAsGiven() { - $loop = Factory::create(); - $connector = new Connector(array(), $loop); + $connector = new Connector(); - $http = new HttpServer($loop, function (RequestInterface $request) use ($loop) { + $http = new HttpServer(function (RequestInterface $request) { $stream = new ThroughStream(); - $loop->addTimer(0.1, function () use ($stream) { + Loop::addTimer(0.1, function () use ($stream) { $stream->end(); }); return new Response(200, array(), $stream); }); - $socket = new SocketServer('127.0.0.1:0', array(), $loop); + $socket = new SocketServer('127.0.0.1:0'); $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { @@ -668,7 +644,7 @@ public function testConnectWithThroughStreamReturnsDataAsGiven() return Stream\buffer($conn); }); - $response = Block\await($result, $loop, 1.0); + $response = Block\await($result, null, 1.0); $this->assertStringStartsWith("HTTP/1.1 200 OK\r\n", $response); $this->assertStringEndsWith("\r\n\r\nhelloworld", $response); @@ -678,24 +654,23 @@ public function testConnectWithThroughStreamReturnsDataAsGiven() public function testConnectWithThroughStreamReturnedFromPromiseReturnsDataAsGiven() { - $loop = Factory::create(); - $connector = new Connector(array(), $loop); + $connector = new Connector(); - $http = new HttpServer($loop, function (RequestInterface $request) use ($loop) { + $http = new HttpServer(function (RequestInterface $request) { $stream = new ThroughStream(); - $loop->addTimer(0.1, function () use ($stream) { + Loop::addTimer(0.1, function () use ($stream) { $stream->end(); }); - return new Promise\Promise(function ($resolve) use ($loop, $stream) { - $loop->addTimer(0.001, function () use ($resolve, $stream) { + return new Promise\Promise(function ($resolve) use ($stream) { + Loop::addTimer(0.001, function () use ($resolve, $stream) { $resolve(new Response(200, array(), $stream)); }); }); }); - $socket = new SocketServer('127.0.0.1:0', array(), $loop); + $socket = new SocketServer('127.0.0.1:0'); $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { @@ -709,7 +684,7 @@ public function testConnectWithThroughStreamReturnedFromPromiseReturnsDataAsGive return Stream\buffer($conn); }); - $response = Block\await($result, $loop, 1.0); + $response = Block\await($result, null, 1.0); $this->assertStringStartsWith("HTTP/1.1 200 OK\r\n", $response); $this->assertStringEndsWith("\r\n\r\nhelloworld", $response); @@ -719,17 +694,16 @@ public function testConnectWithThroughStreamReturnedFromPromiseReturnsDataAsGive public function testConnectWithClosedThroughStreamReturnsNoData() { - $loop = Factory::create(); - $connector = new Connector(array(), $loop); + $connector = new Connector(); - $http = new HttpServer($loop, function (RequestInterface $request) { + $http = new HttpServer(function (RequestInterface $request) { $stream = new ThroughStream(); $stream->close(); return new Response(200, array(), $stream); }); - $socket = new SocketServer('127.0.0.1:0', array(), $loop); + $socket = new SocketServer('127.0.0.1:0'); $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { @@ -743,7 +717,7 @@ public function testConnectWithClosedThroughStreamReturnsNoData() return Stream\buffer($conn); }); - $response = Block\await($result, $loop, 1.0); + $response = Block\await($result, null, 1.0); $this->assertStringStartsWith("HTTP/1.1 200 OK\r\n", $response); $this->assertStringEndsWith("\r\n\r\n", $response); @@ -753,16 +727,14 @@ public function testConnectWithClosedThroughStreamReturnsNoData() public function testLimitConcurrentRequestsMiddlewareRequestStreamPausing() { - $loop = Factory::create(); - $connector = new Connector(array(), $loop); + $connector = new Connector(); $http = new HttpServer( - $loop, new LimitConcurrentRequestsMiddleware(5), new RequestBodyBufferMiddleware(16 * 1024 * 1024), // 16 MiB - function (ServerRequestInterface $request, $next) use ($loop) { - return new Promise\Promise(function ($resolve) use ($request, $loop, $next) { - $loop->addTimer(0.1, function () use ($request, $resolve, $next) { + function (ServerRequestInterface $request, $next) { + return new Promise\Promise(function ($resolve) use ($request, $next) { + Loop::addTimer(0.1, function () use ($request, $resolve, $next) { $resolve($next($request)); }); }); @@ -772,7 +744,7 @@ function (ServerRequestInterface $request) { } ); - $socket = new SocketServer('127.0.0.1:0', array(), $loop); + $socket = new SocketServer('127.0.0.1:0'); $http->listen($socket); $result = array(); @@ -788,7 +760,7 @@ function (ServerRequestInterface $request) { }); } - $responses = Block\await(Promise\all($result), $loop, 1.0); + $responses = Block\await(Promise\all($result), null, 1.0); foreach ($responses as $response) { $this->assertContainsString("HTTP/1.0 200 OK", $response, $response); diff --git a/tests/HttpServerTest.php b/tests/HttpServerTest.php index 9200aa66..23dcc758 100644 --- a/tests/HttpServerTest.php +++ b/tests/HttpServerTest.php @@ -4,7 +4,7 @@ use Clue\React\Block; use Psr\Http\Message\ServerRequestInterface; -use React\EventLoop\Factory; +use React\EventLoop\Loop; use React\Http\HttpServer; use React\Http\Io\IniUtil; use React\Http\Middleware\StreamingRequestMiddleware; @@ -126,9 +126,8 @@ function (ServerRequestInterface $request) use (&$called) { public function testPostFormData() { - $loop = Factory::create(); $deferred = new Deferred(); - $http = new HttpServer($loop, function (ServerRequestInterface $request) use ($deferred) { + $http = new HttpServer(function (ServerRequestInterface $request) use ($deferred) { $deferred->resolve($request); }); @@ -136,7 +135,7 @@ public function testPostFormData() $this->socket->emit('connection', array($this->connection)); $this->connection->emit('data', array("POST / HTTP/1.0\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: 7\r\n\r\nfoo=bar")); - $request = Block\await($deferred->promise(), $loop); + $request = Block\await($deferred->promise()); assert($request instanceof ServerRequestInterface); $form = $request->getParsedBody(); @@ -155,9 +154,8 @@ public function testPostFormData() public function testPostFileUpload() { - $loop = Factory::create(); $deferred = new Deferred(); - $http = new HttpServer($loop, function (ServerRequestInterface $request) use ($deferred) { + $http = new HttpServer(function (ServerRequestInterface $request) use ($deferred) { $deferred->resolve($request); }); @@ -166,16 +164,16 @@ public function testPostFileUpload() $connection = $this->connection; $data = $this->createPostFileUploadRequest(); - $loop->addPeriodicTimer(0.01, function ($timer) use ($loop, &$data, $connection) { + Loop::addPeriodicTimer(0.01, function ($timer) use (&$data, $connection) { $line = array_shift($data); $connection->emit('data', array($line)); if (count($data) === 0) { - $loop->cancelTimer($timer); + Loop::cancelTimer($timer); } }); - $request = Block\await($deferred->promise(), $loop); + $request = Block\await($deferred->promise()); assert($request instanceof ServerRequestInterface); $this->assertEmpty($request->getParsedBody()); @@ -199,9 +197,8 @@ public function testPostFileUpload() public function testPostJsonWillNotBeParsedByDefault() { - $loop = Factory::create(); $deferred = new Deferred(); - $http = new HttpServer($loop, function (ServerRequestInterface $request) use ($deferred) { + $http = new HttpServer(function (ServerRequestInterface $request) use ($deferred) { $deferred->resolve($request); }); @@ -209,7 +206,7 @@ public function testPostJsonWillNotBeParsedByDefault() $this->socket->emit('connection', array($this->connection)); $this->connection->emit('data', array("POST / HTTP/1.0\r\nContent-Type: application/json\r\nContent-Length: 6\r\n\r\n[true]")); - $request = Block\await($deferred->promise(), $loop); + $request = Block\await($deferred->promise()); assert($request instanceof ServerRequestInterface); $this->assertNull($request->getParsedBody()); diff --git a/tests/Io/MiddlewareRunnerTest.php b/tests/Io/MiddlewareRunnerTest.php index eda61012..d8f5f232 100644 --- a/tests/Io/MiddlewareRunnerTest.php +++ b/tests/Io/MiddlewareRunnerTest.php @@ -6,7 +6,6 @@ use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; -use React\EventLoop\Factory; use React\Http\Io\MiddlewareRunner; use React\Http\Message\ServerRequest; use React\Promise; @@ -162,7 +161,7 @@ public function testProcessStack(array $middlewares, $expectedCallCount) $response = $middlewareStack($request); $this->assertTrue($response instanceof PromiseInterface); - $response = Block\await($response, Factory::create()); + $response = Block\await($response); $this->assertTrue($response instanceof ResponseInterface); $this->assertSame(200, $response->getStatusCode()); @@ -229,7 +228,7 @@ function () use ($errorHandler, &$called, $response, $exception) { $request = new ServerRequest('GET', 'https://example.com/'); - $this->assertSame($response, Block\await($runner($request), Factory::create())); + $this->assertSame($response, Block\await($runner($request))); $this->assertSame(1, $retryCalled); $this->assertSame(2, $called); $this->assertSame($exception, $error); diff --git a/tests/Io/StreamingServerTest.php b/tests/Io/StreamingServerTest.php index ccafe338..59cfd118 100644 --- a/tests/Io/StreamingServerTest.php +++ b/tests/Io/StreamingServerTest.php @@ -3,7 +3,7 @@ namespace React\Tests\Http\Io; use Psr\Http\Message\ServerRequestInterface; -use React\EventLoop\Factory; +use React\EventLoop\Loop; use React\Http\Io\StreamingServer; use React\Http\Message\Response; use React\Http\Message\ServerRequest; @@ -48,7 +48,7 @@ public function setUpConnectionMockAndSocket() public function testRequestEventWillNotBeEmittedForIncompleteHeaders() { - $server = new StreamingServer(Factory::create(), $this->expectCallableNever()); + $server = new StreamingServer(Loop::get(), $this->expectCallableNever()); $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); @@ -60,7 +60,7 @@ public function testRequestEventWillNotBeEmittedForIncompleteHeaders() public function testRequestEventIsEmitted() { - $server = new StreamingServer(Factory::create(), $this->expectCallableOnce()); + $server = new StreamingServer(Loop::get(), $this->expectCallableOnce()); $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); @@ -75,7 +75,7 @@ public function testRequestEventIsEmitted() public function testRequestEventIsEmittedForArrayCallable() { $this->called = null; - $server = new StreamingServer(Factory::create(), array($this, 'helperCallableOnce')); + $server = new StreamingServer(Loop::get(), array($this, 'helperCallableOnce')); $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); @@ -95,7 +95,7 @@ public function testRequestEvent() { $i = 0; $requestAssertion = null; - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$i, &$requestAssertion) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use (&$i, &$requestAssertion) { $i++; $requestAssertion = $request; }); @@ -128,7 +128,7 @@ public function testRequestEventWithSingleRequestHandlerArray() { $i = 0; $requestAssertion = null; - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$i, &$requestAssertion) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use (&$i, &$requestAssertion) { $i++; $requestAssertion = $request; }); @@ -160,7 +160,7 @@ public function testRequestEventWithSingleRequestHandlerArray() public function testRequestGetWithHostAndCustomPort() { $requestAssertion = null; - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; }); @@ -182,7 +182,7 @@ public function testRequestGetWithHostAndCustomPort() public function testRequestGetWithHostAndHttpsPort() { $requestAssertion = null; - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; }); @@ -204,7 +204,7 @@ public function testRequestGetWithHostAndHttpsPort() public function testRequestGetWithHostAndDefaultPortWillBeIgnored() { $requestAssertion = null; - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; }); @@ -226,7 +226,7 @@ public function testRequestGetWithHostAndDefaultPortWillBeIgnored() public function testRequestGetHttp10WithoutHostWillBeIgnored() { $requestAssertion = null; - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; }); @@ -248,7 +248,7 @@ public function testRequestGetHttp10WithoutHostWillBeIgnored() public function testRequestGetHttp11WithoutHostWillReject() { - $server = new StreamingServer(Factory::create(), 'var_dump'); + $server = new StreamingServer(Loop::get(), 'var_dump'); $server->on('error', $this->expectCallableOnce()); $server->listen($this->socket); @@ -261,7 +261,7 @@ public function testRequestGetHttp11WithoutHostWillReject() public function testRequestOptionsAsterisk() { $requestAssertion = null; - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; }); @@ -281,7 +281,7 @@ public function testRequestOptionsAsterisk() public function testRequestNonOptionsWithAsteriskRequestTargetWillReject() { - $server = new StreamingServer(Factory::create(), $this->expectCallableNever()); + $server = new StreamingServer(Loop::get(), $this->expectCallableNever()); $server->on('error', $this->expectCallableOnce()); $server->listen($this->socket); @@ -294,7 +294,7 @@ public function testRequestNonOptionsWithAsteriskRequestTargetWillReject() public function testRequestConnectAuthorityForm() { $requestAssertion = null; - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; }); @@ -316,7 +316,7 @@ public function testRequestConnectAuthorityForm() public function testRequestConnectWithoutHostWillBePassesAsIs() { $requestAssertion = null; - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; }); @@ -338,7 +338,7 @@ public function testRequestConnectWithoutHostWillBePassesAsIs() public function testRequestConnectAuthorityFormWithDefaultPortWillBePassedAsIs() { $requestAssertion = null; - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; }); @@ -360,7 +360,7 @@ public function testRequestConnectAuthorityFormWithDefaultPortWillBePassedAsIs() public function testRequestConnectAuthorityFormNonMatchingHostWillBePassedAsIs() { $requestAssertion = null; - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; }); @@ -381,7 +381,7 @@ public function testRequestConnectAuthorityFormNonMatchingHostWillBePassedAsIs() public function testRequestConnectOriginFormRequestTargetWillReject() { - $server = new StreamingServer(Factory::create(), $this->expectCallableNever()); + $server = new StreamingServer(Loop::get(), $this->expectCallableNever()); $server->on('error', $this->expectCallableOnce()); $server->listen($this->socket); @@ -393,7 +393,7 @@ public function testRequestConnectOriginFormRequestTargetWillReject() public function testRequestNonConnectWithAuthorityRequestTargetWillReject() { - $server = new StreamingServer(Factory::create(), $this->expectCallableNever()); + $server = new StreamingServer(Loop::get(), $this->expectCallableNever()); $server->on('error', $this->expectCallableOnce()); $server->listen($this->socket); @@ -407,7 +407,7 @@ public function testRequestWithoutHostEventUsesSocketAddress() { $requestAssertion = null; - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; }); @@ -433,7 +433,7 @@ public function testRequestAbsoluteEvent() { $requestAssertion = null; - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; }); @@ -455,7 +455,7 @@ public function testRequestAbsoluteNonMatchingHostWillBePassedAsIs() { $requestAssertion = null; - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; }); @@ -475,7 +475,7 @@ public function testRequestAbsoluteNonMatchingHostWillBePassedAsIs() public function testRequestAbsoluteWithoutHostWillReject() { - $server = new StreamingServer(Factory::create(), $this->expectCallableNever()); + $server = new StreamingServer(Loop::get(), $this->expectCallableNever()); $server->on('error', $this->expectCallableOnce()); $server->listen($this->socket); @@ -489,7 +489,7 @@ public function testRequestOptionsAsteriskEvent() { $requestAssertion = null; - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; }); @@ -511,7 +511,7 @@ public function testRequestOptionsAbsoluteEvent() { $requestAssertion = null; - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; }); @@ -531,7 +531,7 @@ public function testRequestOptionsAbsoluteEvent() public function testRequestPauseWillBeForwardedToConnection() { - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { $request->getBody()->pause(); }); @@ -551,7 +551,7 @@ public function testRequestPauseWillBeForwardedToConnection() public function testRequestResumeWillBeForwardedToConnection() { - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { $request->getBody()->resume(); }); @@ -571,7 +571,7 @@ public function testRequestResumeWillBeForwardedToConnection() public function testRequestCloseWillNotCloseConnection() { - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { $request->getBody()->close(); }); @@ -586,7 +586,7 @@ public function testRequestCloseWillNotCloseConnection() public function testRequestPauseAfterCloseWillNotBeForwarded() { - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { $request->getBody()->close(); $request->getBody()->pause(); }); @@ -603,7 +603,7 @@ public function testRequestPauseAfterCloseWillNotBeForwarded() public function testRequestResumeAfterCloseWillNotBeForwarded() { - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { $request->getBody()->close(); $request->getBody()->resume(); }); @@ -622,7 +622,7 @@ public function testRequestEventWithoutBodyWillNotEmitData() { $never = $this->expectCallableNever(); - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($never) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($never) { $request->getBody()->on('data', $never); }); @@ -637,7 +637,7 @@ public function testRequestEventWithSecondDataEventWillEmitBodyData() { $once = $this->expectCallableOnceWith('incomplete'); - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($once) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($once) { $request->getBody()->on('data', $once); }); @@ -657,7 +657,7 @@ public function testRequestEventWithPartialBodyWillEmitData() { $once = $this->expectCallableOnceWith('incomplete'); - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($once) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($once) { $request->getBody()->on('data', $once); }); @@ -678,7 +678,7 @@ public function testRequestEventWithPartialBodyWillEmitData() public function testResponseContainsServerHeader() { - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Response(); }); @@ -708,7 +708,7 @@ public function testResponsePendingPromiseWillNotSendAnything() { $never = $this->expectCallableNever(); - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($never) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($never) { return new Promise(function () { }, $never); }); @@ -738,7 +738,7 @@ public function testResponsePendingPromiseWillBeCancelledIfConnectionCloses() { $once = $this->expectCallableOnce(); - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($once) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($once) { return new Promise(function () { }, $once); }); @@ -770,7 +770,7 @@ public function testResponseBodyStreamAlreadyClosedWillSendEmptyBodyChunkedEncod $stream = new ThroughStream(); $stream->close(); - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($stream) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($stream) { return new Response( 200, array(), @@ -805,7 +805,7 @@ public function testResponseBodyStreamEndingWillSendEmptyBodyChunkedEncoded() { $stream = new ThroughStream(); - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($stream) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($stream) { return new Response( 200, array(), @@ -843,7 +843,7 @@ public function testResponseBodyStreamAlreadyClosedWillSendEmptyBodyPlainHttp10( $stream = new ThroughStream(); $stream->close(); - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($stream) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($stream) { return new Response( 200, array(), @@ -879,7 +879,7 @@ public function testResponseStreamWillBeClosedIfConnectionIsAlreadyClosed() $stream = new ThroughStream(); $stream->on('close', $this->expectCallableOnce()); - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($stream) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($stream) { return new Response( 200, array(), @@ -934,7 +934,7 @@ public function testResponseBodyStreamWillBeClosedIfConnectionEmitsCloseEvent() $stream = new ThroughStream(); $stream->on('close', $this->expectCallableOnce()); - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($stream) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($stream) { return new Response( 200, array(), @@ -952,7 +952,7 @@ public function testResponseBodyStreamWillBeClosedIfConnectionEmitsCloseEvent() public function testResponseUpgradeInResponseCanBeUsedToAdvertisePossibleUpgrade() { - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Response( 200, array( @@ -988,7 +988,7 @@ function ($data) use (&$buffer) { public function testResponseUpgradeWishInRequestCanBeIgnoredByReturningNormalResponse() { - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Response( 200, array( @@ -1023,7 +1023,7 @@ function ($data) use (&$buffer) { public function testResponseUpgradeSwitchingProtocolIncludesConnectionUpgradeHeaderWithoutContentLength() { - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Response( 101, array( @@ -1063,7 +1063,7 @@ public function testResponseUpgradeSwitchingProtocolWithStreamWillPipeDataToConn { $stream = new ThroughStream(); - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($stream) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($stream) { return new Response( 101, array( @@ -1104,7 +1104,7 @@ public function testResponseConnectMethodStreamWillPipeDataToConnection() { $stream = new ThroughStream(); - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($stream) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($stream) { return new Response( 200, array(), @@ -1142,7 +1142,7 @@ public function testResponseConnectMethodStreamWillPipeDataFromConnection() { $stream = new ThroughStream(); - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($stream) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($stream) { return new Response( 200, array(), @@ -1161,7 +1161,7 @@ public function testResponseConnectMethodStreamWillPipeDataFromConnection() public function testResponseContainsSameRequestProtocolVersionAndChunkedBodyForHttp11() { - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Response( 200, array(), @@ -1194,7 +1194,7 @@ function ($data) use (&$buffer) { public function testResponseContainsSameRequestProtocolVersionAndRawBodyForHttp10() { - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Response( 200, array(), @@ -1228,7 +1228,7 @@ function ($data) use (&$buffer) { public function testResponseContainsNoResponseBodyForHeadRequest() { - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Response( 200, array(), @@ -1264,7 +1264,7 @@ public function testResponseContainsNoResponseBodyForHeadRequestWithStreamingRes $stream = new ThroughStream(); $stream->on('close', $this->expectCallableOnce()); - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($stream) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($stream) { return new Response( 200, array('Content-Length' => '3'), @@ -1296,7 +1296,7 @@ function ($data) use (&$buffer) { public function testResponseContainsNoResponseBodyAndNoContentLengthForNoContentStatus() { - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Response( 204, array(), @@ -1332,7 +1332,7 @@ public function testResponseContainsNoResponseBodyAndNoContentLengthForNoContent $stream = new ThroughStream(); $stream->on('close', $this->expectCallableOnce()); - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($stream) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($stream) { return new Response( 204, array('Content-Length' => '3'), @@ -1364,7 +1364,7 @@ function ($data) use (&$buffer) { public function testResponseContainsNoContentLengthHeaderForNotModifiedStatus() { - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Response( 304, array(), @@ -1396,7 +1396,7 @@ function ($data) use (&$buffer) { public function testResponseContainsExplicitContentLengthHeaderForNotModifiedStatus() { - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Response( 304, array('Content-Length' => 3), @@ -1428,7 +1428,7 @@ function ($data) use (&$buffer) { public function testResponseContainsNoResponseBodyForNotModifiedStatus() { - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Response( 304, array(), @@ -1464,7 +1464,7 @@ public function testResponseContainsNoResponseBodyForNotModifiedStatusWithStream $stream = new ThroughStream(); $stream->on('close', $this->expectCallableOnce()); - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($stream) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($stream) { return new Response( 304, array('Content-Length' => '3'), @@ -1497,7 +1497,7 @@ function ($data) use (&$buffer) { public function testRequestInvalidHttpProtocolVersionWillEmitErrorAndSendErrorResponse() { $error = null; - $server = new StreamingServer(Factory::create(), $this->expectCallableNever()); + $server = new StreamingServer(Loop::get(), $this->expectCallableNever()); $server->on('error', function ($message) use (&$error) { $error = $message; }); @@ -1531,7 +1531,7 @@ function ($data) use (&$buffer) { public function testRequestOverflowWillEmitErrorAndSendErrorResponse() { $error = null; - $server = new StreamingServer(Factory::create(), $this->expectCallableNever()); + $server = new StreamingServer(Loop::get(), $this->expectCallableNever()); $server->on('error', function ($message) use (&$error) { $error = $message; }); @@ -1565,7 +1565,7 @@ function ($data) use (&$buffer) { public function testRequestInvalidWillEmitErrorAndSendErrorResponse() { $error = null; - $server = new StreamingServer(Factory::create(), $this->expectCallableNever()); + $server = new StreamingServer(Loop::get(), $this->expectCallableNever()); $server->on('error', function ($message) use (&$error) { $error = $message; }); @@ -1602,7 +1602,7 @@ public function testRequestContentLengthBodyDataWillEmitDataEventOnRequestStream $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1630,7 +1630,7 @@ public function testRequestChunkedTransferEncodingRequestWillEmitDecodedDataEven $errorEvent = $this->expectCallableNever(); $requestValidation = null; - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent, &$requestValidation) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent, &$requestValidation) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1661,7 +1661,7 @@ public function testRequestChunkedTransferEncodingWithAdditionalDataWontBeEmitte $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1690,7 +1690,7 @@ public function testRequestChunkedTransferEncodingEmpty() $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1718,7 +1718,7 @@ public function testRequestChunkedTransferEncodingHeaderCanBeUpperCase() $errorEvent = $this->expectCallableNever(); $requestValidation = null; - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent, &$requestValidation) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent, &$requestValidation) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1748,7 +1748,7 @@ public function testRequestChunkedTransferEncodingCanBeMixedUpperAndLowerCase() $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1775,7 +1775,7 @@ public function testRequestContentLengthWillEmitDataEventAndEndEventAndAdditiona $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1806,7 +1806,7 @@ public function testRequestContentLengthWillEmitDataEventAndEndEventAndAdditiona $errorEvent = $this->expectCallableNever(); - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1838,7 +1838,7 @@ public function testRequestZeroContentLengthWillEmitEndEvent() $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1864,7 +1864,7 @@ public function testRequestZeroContentLengthWillEmitEndAndAdditionalDataWillBeIg $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1891,7 +1891,7 @@ public function testRequestZeroContentLengthWillEmitEndAndAdditionalDataWillBeIg $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1917,7 +1917,7 @@ public function testRequestZeroContentLengthWillEmitEndAndAdditionalDataWillBeIg public function testRequestInvalidChunkHeaderTooLongWillEmitErrorOnRequestStream() { $errorEvent = $this->expectCallableOnceWith($this->isInstanceOf('Exception')); - $server = new StreamingServer(Factory::create(), function ($request) use ($errorEvent){ + $server = new StreamingServer(Loop::get(), function ($request) use ($errorEvent){ $request->getBody()->on('error', $errorEvent); return \React\Promise\resolve(new Response()); }); @@ -1942,7 +1942,7 @@ public function testRequestInvalidChunkHeaderTooLongWillEmitErrorOnRequestStream public function testRequestInvalidChunkBodyTooLongWillEmitErrorOnRequestStream() { $errorEvent = $this->expectCallableOnceWith($this->isInstanceOf('Exception')); - $server = new StreamingServer(Factory::create(), function ($request) use ($errorEvent){ + $server = new StreamingServer(Loop::get(), function ($request) use ($errorEvent){ $request->getBody()->on('error', $errorEvent); }); @@ -1964,7 +1964,7 @@ public function testRequestInvalidChunkBodyTooLongWillEmitErrorOnRequestStream() public function testRequestUnexpectedEndOfRequestWithChunkedTransferConnectionWillEmitErrorOnRequestStream() { $errorEvent = $this->expectCallableOnceWith($this->isInstanceOf('Exception')); - $server = new StreamingServer(Factory::create(), function ($request) use ($errorEvent){ + $server = new StreamingServer(Loop::get(), function ($request) use ($errorEvent){ $request->getBody()->on('error', $errorEvent); }); @@ -1987,7 +1987,7 @@ public function testRequestUnexpectedEndOfRequestWithChunkedTransferConnectionWi public function testRequestInvalidChunkHeaderWillEmitErrorOnRequestStream() { $errorEvent = $this->expectCallableOnceWith($this->isInstanceOf('Exception')); - $server = new StreamingServer(Factory::create(), function ($request) use ($errorEvent){ + $server = new StreamingServer(Loop::get(), function ($request) use ($errorEvent){ $request->getBody()->on('error', $errorEvent); }); @@ -2009,7 +2009,7 @@ public function testRequestInvalidChunkHeaderWillEmitErrorOnRequestStream() public function testRequestUnexpectedEndOfRequestWithContentLengthWillEmitErrorOnRequestStream() { $errorEvent = $this->expectCallableOnceWith($this->isInstanceOf('Exception')); - $server = new StreamingServer(Factory::create(), function ($request) use ($errorEvent){ + $server = new StreamingServer(Loop::get(), function ($request) use ($errorEvent){ $request->getBody()->on('error', $errorEvent); }); @@ -2036,7 +2036,7 @@ public function testRequestWithoutBodyWillEmitEndOnRequestStream() $endEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new StreamingServer(Factory::create(), function ($request) use ($dataEvent, $closeEvent, $endEvent, $errorEvent){ + $server = new StreamingServer(Loop::get(), function ($request) use ($dataEvent, $closeEvent, $endEvent, $errorEvent){ $request->getBody()->on('data', $dataEvent); $request->getBody()->on('close', $closeEvent); $request->getBody()->on('end', $endEvent); @@ -2060,7 +2060,7 @@ public function testRequestWithoutDefinedLengthWillIgnoreDataEvent() $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -2079,7 +2079,7 @@ public function testRequestWithoutDefinedLengthWillIgnoreDataEvent() public function testResponseWithBodyStreamWillUseChunkedTransferEncodingByDefault() { $stream = new ThroughStream(); - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($stream) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($stream) { return new Response( 200, array(), @@ -2113,7 +2113,7 @@ function ($data) use (&$buffer) { public function testResponseWithBodyStringWillOverwriteExplicitContentLengthAndTransferEncoding() { - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Response( 200, array( @@ -2154,7 +2154,7 @@ public function testResponseContainsResponseBodyWithTransferEncodingChunkedForBo $body->expects($this->once())->method('getSize')->willReturn(null); $body->expects($this->once())->method('__toString')->willReturn('body'); - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($body) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($body) { return new Response( 200, array(), @@ -2191,7 +2191,7 @@ public function testResponseContainsResponseBodyWithPlainBodyWithUnknownSizeForL $body->expects($this->once())->method('getSize')->willReturn(null); $body->expects($this->once())->method('__toString')->willReturn('body'); - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($body) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($body) { return new Response( 200, array(), @@ -2225,7 +2225,7 @@ function ($data) use (&$buffer) { public function testResponseWithCustomTransferEncodingWillBeIgnoredAndUseChunkedTransferEncodingInstead() { $stream = new ThroughStream(); - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($stream) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($stream) { return new Response( 200, array( @@ -2262,7 +2262,7 @@ function ($data) use (&$buffer) { public function testResponseWithoutExplicitDateHeaderWillAddCurrentDate() { - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Response(); }); @@ -2292,7 +2292,7 @@ function ($data) use (&$buffer) { public function testResponseWIthCustomDateHeaderOverwritesDefault() { - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Response( 200, array("Date" => "Tue, 15 Nov 1994 08:12:31 GMT") @@ -2325,7 +2325,7 @@ function ($data) use (&$buffer) { public function testResponseWithEmptyDateHeaderRemovesDateHeader() { - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Response( 200, array('Date' => '') @@ -2358,7 +2358,7 @@ function ($data) use (&$buffer) { public function testResponseCanContainMultipleCookieHeaders() { - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Response( 200, array( @@ -2396,7 +2396,7 @@ function ($data) use (&$buffer) { public function testReponseWithExpectContinueRequestContainsContinueWithLaterResponse() { - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Response(); }); @@ -2428,7 +2428,7 @@ function ($data) use (&$buffer) { public function testResponseWithExpectContinueRequestWontSendContinueForHttp10() { - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Response(); }); @@ -2459,14 +2459,14 @@ function ($data) use (&$buffer) { public function testInvalidCallbackFunctionLeadsToException() { $this->setExpectedException('InvalidArgumentException'); - $server = new StreamingServer(Factory::create(), 'invalid'); + $server = new StreamingServer(Loop::get(), 'invalid'); } public function testResponseBodyStreamWillStreamDataWithChunkedTransferEncoding() { $input = new ThroughStream(); - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($input) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($input) { return new Response( 200, array(), @@ -2505,7 +2505,7 @@ public function testResponseBodyStreamWithContentLengthWillStreamTillLengthWitho { $input = new ThroughStream(); - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($input) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($input) { return new Response( 200, array('Content-Length' => 5), @@ -2543,7 +2543,7 @@ function ($data) use (&$buffer) { public function testResponseWithResponsePromise() { - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return \React\Promise\resolve(new Response()); }); @@ -2571,7 +2571,7 @@ function ($data) use (&$buffer) { public function testResponseReturnInvalidTypeWillResultInError() { - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return "invalid"; }); @@ -2605,7 +2605,7 @@ function ($data) use (&$buffer) { public function testResponseResolveWrongTypeInPromiseWillResultInError() { - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return \React\Promise\resolve("invalid"); }); @@ -2633,7 +2633,7 @@ function ($data) use (&$buffer) { public function testResponseRejectedPromiseWillResultInErrorMessage() { - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Promise(function ($resolve, $reject) { $reject(new \Exception()); }); @@ -2664,7 +2664,7 @@ function ($data) use (&$buffer) { public function testResponseExceptionInCallbackWillResultInErrorMessage() { - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Promise(function ($resolve, $reject) { throw new \Exception('Bad call'); }); @@ -2695,7 +2695,7 @@ function ($data) use (&$buffer) { public function testResponseWithContentLengthHeaderForStringBodyOverwritesTransferEncoding() { - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Response( 200, array('Transfer-Encoding' => 'chunked'), @@ -2731,7 +2731,7 @@ function ($data) use (&$buffer) { public function testResponseWillBeHandled() { - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Response(); }); @@ -2759,7 +2759,7 @@ function ($data) use (&$buffer) { public function testResponseExceptionThrowInCallBackFunctionWillResultInErrorMessage() { - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { throw new \Exception('hello'); }); @@ -2797,7 +2797,7 @@ function ($data) use (&$buffer) { */ public function testResponseThrowableThrowInCallBackFunctionWillResultInErrorMessage() { - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { throw new \Error('hello'); }); @@ -2840,7 +2840,7 @@ function ($data) use (&$buffer) { public function testResponseRejectOfNonExceptionWillResultInErrorMessage() { - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Promise(function ($resolve, $reject) { $reject('Invalid type'); }); @@ -2877,7 +2877,7 @@ function ($data) use (&$buffer) { public function testRequestServerRequestParams() { $requestValidation = null; - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestValidation) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use (&$requestValidation) { $requestValidation = $request; }); @@ -2911,7 +2911,7 @@ public function testRequestServerRequestParams() public function testRequestQueryParametersWillBeAddedToRequest() { $requestValidation = null; - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestValidation) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use (&$requestValidation) { $requestValidation = $request; }); @@ -2931,7 +2931,7 @@ public function testRequestQueryParametersWillBeAddedToRequest() public function testRequestCookieWillBeAddedToServerRequest() { $requestValidation = null; - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestValidation) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use (&$requestValidation) { $requestValidation = $request; }); @@ -2952,7 +2952,7 @@ public function testRequestCookieWillBeAddedToServerRequest() public function testRequestInvalidMultipleCookiesWontBeAddedToServerRequest() { $requestValidation = null; - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestValidation) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use (&$requestValidation) { $requestValidation = $request; }); @@ -2973,7 +2973,7 @@ public function testRequestInvalidMultipleCookiesWontBeAddedToServerRequest() public function testRequestCookieWithSeparatorWillBeAddedToServerRequest() { $requestValidation = null; - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestValidation) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use (&$requestValidation) { $requestValidation = $request; }); @@ -2992,7 +2992,7 @@ public function testRequestCookieWithSeparatorWillBeAddedToServerRequest() public function testRequestCookieWithCommaValueWillBeAddedToServerRequest() { $requestValidation = null; - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestValidation) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use (&$requestValidation) { $requestValidation = $request; }); @@ -3011,7 +3011,7 @@ public function testRequestCookieWithCommaValueWillBeAddedToServerRequest() { public function testNewConnectionWillInvokeParserOnce() { - $server = new StreamingServer(Factory::create(), $this->expectCallableNever()); + $server = new StreamingServer(Loop::get(), $this->expectCallableNever()); $parser = $this->getMockBuilder('React\Http\Io\RequestHeaderParser')->getMock(); $parser->expects($this->once())->method('handle'); @@ -3028,7 +3028,7 @@ public function testNewConnectionWillInvokeParserOnceAndInvokeRequestHandlerWhen { $request = new ServerRequest('GET', 'http://localhost/', array(), '', '1.0'); - $server = new StreamingServer(Factory::create(), $this->expectCallableOnceWith($request)); + $server = new StreamingServer(Loop::get(), $this->expectCallableOnceWith($request)); $parser = $this->getMockBuilder('React\Http\Io\RequestHeaderParser')->getMock(); $parser->expects($this->once())->method('handle'); @@ -3051,7 +3051,7 @@ public function testNewConnectionWillInvokeParserOnceAndInvokeRequestHandlerWhen { $request = new ServerRequest('GET', 'http://localhost/', array('Connection' => 'close')); - $server = new StreamingServer(Factory::create(), $this->expectCallableOnceWith($request)); + $server = new StreamingServer(Loop::get(), $this->expectCallableOnceWith($request)); $parser = $this->getMockBuilder('React\Http\Io\RequestHeaderParser')->getMock(); $parser->expects($this->once())->method('handle'); @@ -3074,7 +3074,7 @@ public function testNewConnectionWillInvokeParserOnceAndInvokeRequestHandlerWhen { $request = new ServerRequest('GET', 'http://localhost/'); - $server = new StreamingServer(Factory::create(), function () { + $server = new StreamingServer(Loop::get(), function () { return new Response(200, array('Connection' => 'close')); }); @@ -3099,7 +3099,7 @@ public function testNewConnectionWillInvokeParserTwiceAfterInvokingRequestHandle { $request = new ServerRequest('GET', 'http://localhost/'); - $server = new StreamingServer(Factory::create(), function () { + $server = new StreamingServer(Loop::get(), function () { return new Response(); }); @@ -3124,7 +3124,7 @@ public function testNewConnectionWillInvokeParserTwiceAfterInvokingRequestHandle { $request = new ServerRequest('GET', 'http://localhost/', array('Connection' => 'keep-alive'), '', '1.0'); - $server = new StreamingServer(Factory::create(), function () { + $server = new StreamingServer(Loop::get(), function () { return new Response(); }); @@ -3150,7 +3150,7 @@ public function testNewConnectionWillInvokeParserOnceAfterInvokingRequestHandler $request = new ServerRequest('GET', 'http://localhost/'); $body = new ThroughStream(); - $server = new StreamingServer(Factory::create(), function () use ($body) { + $server = new StreamingServer(Loop::get(), function () use ($body) { return new Response(200, array(), $body); }); @@ -3176,7 +3176,7 @@ public function testNewConnectionWillInvokeParserTwiceAfterInvokingRequestHandle $request = new ServerRequest('GET', 'http://localhost/'); $body = new ThroughStream(); - $server = new StreamingServer(Factory::create(), function () use ($body) { + $server = new StreamingServer(Loop::get(), function () use ($body) { return new Response(200, array(), $body); }); diff --git a/tests/Io/TransactionTest.php b/tests/Io/TransactionTest.php index 12e128cc..d62147b5 100644 --- a/tests/Io/TransactionTest.php +++ b/tests/Io/TransactionTest.php @@ -8,7 +8,7 @@ use RingCentral\Psr7\Response; use React\Http\Io\Transaction; use React\Http\Message\ResponseException; -use React\EventLoop\Factory; +use React\EventLoop\Loop; use React\Promise; use React\Promise\Deferred; use React\Stream\ThroughStream; @@ -383,10 +383,8 @@ public function testReceivingErrorResponseWillRejectWithResponseException() public function testReceivingStreamingBodyWillResolveWithBufferedResponseByDefault() { - $loop = Factory::create(); - $stream = new ThroughStream(); - $loop->addTimer(0.001, function () use ($stream) { + Loop::addTimer(0.001, function () use ($stream) { $stream->emit('data', array('hello world')); $stream->close(); }); @@ -398,10 +396,10 @@ public function testReceivingStreamingBodyWillResolveWithBufferedResponseByDefau $sender = $this->makeSenderMock(); $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(Promise\resolve($response)); - $transaction = new Transaction($sender, $loop); + $transaction = new Transaction($sender, Loop::get()); $promise = $transaction->send($request); - $response = Block\await($promise, $loop); + $response = Block\await($promise); $this->assertEquals(200, $response->getStatusCode()); $this->assertEquals('hello world', (string)$response->getBody()); @@ -409,8 +407,6 @@ public function testReceivingStreamingBodyWillResolveWithBufferedResponseByDefau public function testReceivingStreamingBodyWithSizeExceedingMaximumResponseBufferWillRejectAndCloseResponseStream() { - $loop = Factory::create(); - $stream = new ThroughStream(); $stream->on('close', $this->expectCallableOnce()); @@ -422,17 +418,15 @@ public function testReceivingStreamingBodyWithSizeExceedingMaximumResponseBuffer $sender = $this->makeSenderMock(); $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(Promise\resolve($response)); - $transaction = new Transaction($sender, $loop); + $transaction = new Transaction($sender, Loop::get()); $promise = $transaction->send($request); $this->setExpectedException('OverflowException'); - Block\await($promise, $loop, 0.001); + Block\await($promise, null, 0.001); } public function testCancelBufferingResponseWillCloseStreamAndReject() { - $loop = Factory::create(); - $stream = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); $stream->expects($this->any())->method('isReadable')->willReturn(true); $stream->expects($this->once())->method('close'); @@ -444,12 +438,12 @@ public function testCancelBufferingResponseWillCloseStreamAndReject() $sender = $this->makeSenderMock(); $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(Promise\resolve($response)); - $transaction = new Transaction($sender, $loop); + $transaction = new Transaction($sender, Loop::get()); $promise = $transaction->send($request); $promise->cancel(); $this->setExpectedException('RuntimeException'); - Block\await($promise, $loop, 0.001); + Block\await($promise, null, 0.001); } public function testReceivingStreamingBodyWillResolveWithStreamingResponseIfStreamingIsEnabled() diff --git a/tests/Middleware/RequestBodyBufferMiddlewareTest.php b/tests/Middleware/RequestBodyBufferMiddlewareTest.php index 9889f501..e073e1f0 100644 --- a/tests/Middleware/RequestBodyBufferMiddlewareTest.php +++ b/tests/Middleware/RequestBodyBufferMiddlewareTest.php @@ -4,7 +4,7 @@ use Clue\React\Block; use Psr\Http\Message\ServerRequestInterface; -use React\EventLoop\Factory; +use React\EventLoop\Loop; use React\Http\Io\HttpBodyStream; use React\Http\Message\Response; use React\Http\Message\ServerRequest; @@ -118,8 +118,6 @@ function (ServerRequestInterface $request) use (&$exposedRequest) { public function testKnownExcessiveSizedBodyIsDisgardedTheRequestIsPassedDownToTheNextMiddleware() { - $loop = Factory::create(); - $stream = new ThroughStream(); $stream->end('aa'); $serverRequest = new ServerRequest( @@ -135,7 +133,7 @@ public function testKnownExcessiveSizedBodyIsDisgardedTheRequestIsPassedDownToTh function (ServerRequestInterface $request) { return new Response(200, array(), $request->getBody()->getContents()); } - ), $loop); + )); $this->assertSame(200, $response->getStatusCode()); $this->assertSame('', $response->getBody()->getContents()); @@ -143,10 +141,8 @@ function (ServerRequestInterface $request) { public function testKnownExcessiveSizedWithIniLikeSize() { - $loop = Factory::create(); - $stream = new ThroughStream(); - $loop->addTimer(0.001, function () use ($stream) { + Loop::addTimer(0.001, function () use ($stream) { $stream->end(str_repeat('a', 2048)); }); $serverRequest = new ServerRequest( @@ -162,7 +158,7 @@ public function testKnownExcessiveSizedWithIniLikeSize() function (ServerRequestInterface $request) { return new Response(200, array(), $request->getBody()->getContents()); } - ), $loop); + )); $this->assertSame(200, $response->getStatusCode()); $this->assertSame('', $response->getBody()->getContents()); @@ -192,8 +188,6 @@ function (ServerRequestInterface $request) use (&$exposedRequest) { public function testExcessiveSizeBodyIsDiscardedAndTheRequestIsPassedDownToTheNextMiddleware() { - $loop = Factory::create(); - $stream = new ThroughStream(); $serverRequest = new ServerRequest( 'GET', @@ -215,7 +209,7 @@ function (ServerRequestInterface $request) { $exposedResponse = Block\await($promise->then( null, $this->expectCallableNever() - ), $loop); + )); $this->assertSame(200, $exposedResponse->getStatusCode()); $this->assertSame('', $exposedResponse->getBody()->getContents()); @@ -223,8 +217,6 @@ function (ServerRequestInterface $request) { public function testBufferingErrorThrows() { - $loop = Factory::create(); - $stream = new ThroughStream(); $serverRequest = new ServerRequest( 'GET', @@ -244,7 +236,7 @@ function (ServerRequestInterface $request) { $stream->emit('error', array(new \RuntimeException())); $this->setExpectedException('RuntimeException'); - Block\await($promise, $loop); + Block\await($promise); } public function testFullBodyStreamedBeforeCallingNextMiddleware() From 6ca832f446a8467b4c730101f5df2200291a64b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Wed, 7 Apr 2021 17:49:31 +0200 Subject: [PATCH 388/456] Add factory methods for common HTML/JSON/plaintext/XML response types --- README.md | 181 +++++++++++++++++++++++++++ src/Message/Response.php | 215 +++++++++++++++++++++++++++++++++ tests/Message/ResponseTest.php | 71 +++++++++++ 3 files changed, 467 insertions(+) diff --git a/README.md b/README.md index 50797fe6..e178ee72 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,10 @@ multiple concurrent HTTP requests without blocking. * [withResponseBuffer()](#withresponsebuffer) * [React\Http\Message](#reacthttpmessage) * [Response](#response) + * [html()](#html) + * [json()](#json) + * [plaintext()](#plaintext) + * [xml()](#xml) * [ServerRequest](#serverrequest) * [ResponseException](#responseexception) * [React\Http\Middleware](#reacthttpmiddleware) @@ -2463,6 +2467,183 @@ constants with the `STATUS_*` prefix. For instance, the `200 OK` and response message and only adds required streaming support. This base class is considered an implementation detail that may change in the future. +##### html() + +The static `html(string $html): Response` method can be used to +create an HTML response. + +```php +$html = << + +Hello wörld! + + +HTML; + +$response = React\Http\Message\Response::html($html); +``` + +This is a convenient shortcut method that returns the equivalent of this: + +``` +$response = new React\Http\Message\Response( + React\Http\Message\Response::STATUS_OK, + [ + 'Content-Type' => 'text/html; charset=utf-8' + ], + $html +); +``` + +This method always returns a response with a `200 OK` status code and +the appropriate `Content-Type` response header for the given HTTP source +string encoded in UTF-8 (Unicode). It's generally recommended to end the +given plaintext string with a trailing newline. + +If you want to use a different status code or custom HTTP response +headers, you can manipulate the returned response object using the +provided PSR-7 methods or directly instantiate a custom HTTP response +object using the `Response` constructor: + +```php +$response = React\Http\Message\Response::html( + "

Error

\n

Invalid user name given.

\n" +)->withStatus(React\Http\Message\Response::STATUS_BAD_REQUEST); +``` + +##### json() + +The static `json(mixed $data): Response` method can be used to +create a JSON response. + +```php +$response = React\Http\Message\Response::json(['name' => 'Alice']); +``` + +This is a convenient shortcut method that returns the equivalent of this: + +``` +$response = new React\Http\Message\Response( + React\Http\Message\Response::STATUS_OK, + [ + 'Content-Type' => 'application/json' + ], + json_encode( + ['name' => 'Alice'], + JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION + ) . "\n" +); +``` + +This method always returns a response with a `200 OK` status code and +the appropriate `Content-Type` response header for the given structured +data encoded as a JSON text. + +The given structured data will be encoded as a JSON text. Any `string` +values in the data must be encoded in UTF-8 (Unicode). If the encoding +fails, this method will throw an `InvalidArgumentException`. + +By default, the given structured data will be encoded with the flags as +shown above. This includes pretty printing (PHP 5.4+) and preserving +zero fractions for `float` values (PHP 5.6.6+) to ease debugging. It is +assumed any additional data overhead is usually compensated by using HTTP +response compression. + +If you want to use a different status code or custom HTTP response +headers, you can manipulate the returned response object using the +provided PSR-7 methods or directly instantiate a custom HTTP response +object using the `Response` constructor: + +```php +$response = React\Http\Message\Response::json( + ['error' => 'Invalid user name given'] +)->withStatus(React\Http\Message\Response::STATUS_BAD_REQUEST); +``` + +##### plaintext() + +The static `plaintext(string $text): Response` method can be used to +create a plaintext response. + +```php +$response = React\Http\Message\Response::plaintext("Hello wörld!\n"); +``` + +This is a convenient shortcut method that returns the equivalent of this: + +``` +$response = new React\Http\Message\Response( + React\Http\Message\Response::STATUS_OK, + [ + 'Content-Type' => 'text/plain; charset=utf-8' + ], + "Hello wörld!\n" +); +``` + +This method always returns a response with a `200 OK` status code and +the appropriate `Content-Type` response header for the given plaintext +string encoded in UTF-8 (Unicode). It's generally recommended to end the +given plaintext string with a trailing newline. + +If you want to use a different status code or custom HTTP response +headers, you can manipulate the returned response object using the +provided PSR-7 methods or directly instantiate a custom HTTP response +object using the `Response` constructor: + +```php +$response = React\Http\Message\Response::plaintext( + "Error: Invalid user name given.\n" +)->withStatus(React\Http\Message\Response::STATUS_BAD_REQUEST); +``` + +##### xml() + +The static `xml(string $xml): Response` method can be used to +create an XML response. + +```php +$xml = << + + Hello wörld! + + +XML; + +$response = React\Http\Message\Response::xml($xml); +``` + +This is a convenient shortcut method that returns the equivalent of this: + +``` +$response = new React\Http\Message\Response( + React\Http\Message\Response::STATUS_OK, + [ + 'Content-Type' => 'application/xml' + ], + $xml +); +``` + +This method always returns a response with a `200 OK` status code and +the appropriate `Content-Type` response header for the given XML source +string. It's generally recommended to use UTF-8 (Unicode) and specify +this as part of the leading XML declaration and to end the given XML +source string with a trailing newline. + +If you want to use a different status code or custom HTTP response +headers, you can manipulate the returned response object using the +provided PSR-7 methods or directly instantiate a custom HTTP response +object using the `Response` constructor: + +```php +$response = React\Http\Message\Response::xml( + "Invalid user name given.\n" +)->withStatus(React\Http\Message\Response::STATUS_BAD_REQUEST); +``` + #### ServerRequest The `React\Http\Message\ServerRequest` class can be used to diff --git a/src/Message/Response.php b/src/Message/Response.php index 0ce8bef0..edd6245b 100644 --- a/src/Message/Response.php +++ b/src/Message/Response.php @@ -42,6 +42,221 @@ */ final class Response extends Psr7Response implements StatusCodeInterface { + /** + * Create an HTML response + * + * ```php + * $html = << + * + * Hello wörld! + * + * + * HTML; + * + * $response = React\Http\Message\Response::html($html); + * ``` + * + * This is a convenient shortcut method that returns the equivalent of this: + * + * ``` + * $response = new React\Http\Message\Response( + * React\Http\Message\Response::STATUS_OK, + * [ + * 'Content-Type' => 'text/html; charset=utf-8' + * ], + * $html + * ); + * ``` + * + * This method always returns a response with a `200 OK` status code and + * the appropriate `Content-Type` response header for the given HTTP source + * string encoded in UTF-8 (Unicode). It's generally recommended to end the + * given plaintext string with a trailing newline. + * + * If you want to use a different status code or custom HTTP response + * headers, you can manipulate the returned response object using the + * provided PSR-7 methods or directly instantiate a custom HTTP response + * object using the `Response` constructor: + * + * ```php + * $response = React\Http\Message\Response::html( + * "

Error

\n

Invalid user name given.

\n" + * )->withStatus(React\Http\Message\Response::STATUS_BAD_REQUEST); + * ``` + * + * @param string $html + * @return self + */ + public static function html($html) + { + return new self(self::STATUS_OK, array('Content-Type' => 'text/html; charset=utf-8'), $html); + } + + /** + * Create a JSON response + * + * ```php + * $response = React\Http\Message\Response::json(['name' => 'Alice']); + * ``` + * + * This is a convenient shortcut method that returns the equivalent of this: + * + * ``` + * $response = new React\Http\Message\Response( + * React\Http\Message\Response::STATUS_OK, + * [ + * 'Content-Type' => 'application/json' + * ], + * json_encode( + * ['name' => 'Alice'], + * JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION + * ) . "\n" + * ); + * ``` + * + * This method always returns a response with a `200 OK` status code and + * the appropriate `Content-Type` response header for the given structured + * data encoded as a JSON text. + * + * The given structured data will be encoded as a JSON text. Any `string` + * values in the data must be encoded in UTF-8 (Unicode). If the encoding + * fails, this method will throw an `InvalidArgumentException`. + * + * By default, the given structured data will be encoded with the flags as + * shown above. This includes pretty printing (PHP 5.4+) and preserving + * zero fractions for `float` values (PHP 5.6.6+) to ease debugging. It is + * assumed any additional data overhead is usually compensated by using HTTP + * response compression. + * + * If you want to use a different status code or custom HTTP response + * headers, you can manipulate the returned response object using the + * provided PSR-7 methods or directly instantiate a custom HTTP response + * object using the `Response` constructor: + * + * ```php + * $response = React\Http\Message\Response::json( + * ['error' => 'Invalid user name given'] + * )->withStatus(React\Http\Message\Response::STATUS_BAD_REQUEST); + * ``` + * + * @param mixed $data + * @return self + * @throws \InvalidArgumentException when encoding fails + */ + public static function json($data) + { + $json = @\json_encode( + $data, + (\defined('JSON_PRETTY_PRINT') ? \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE : 0) | (\defined('JSON_PRESERVE_ZERO_FRACTION') ? \JSON_PRESERVE_ZERO_FRACTION : 0) + ); + + // throw on error, now `false` but used to be `(string) "null"` before PHP 5.5 + if ($json === false || (\PHP_VERSION_ID < 50500 && \json_last_error() !== \JSON_ERROR_NONE)) { + throw new \InvalidArgumentException( + 'Unable to encode given data as JSON' . (\function_exists('json_last_error_msg') ? ': ' . \json_last_error_msg() : ''), + \json_last_error() + ); + } + + return new self(self::STATUS_OK, array('Content-Type' => 'application/json'), $json . "\n"); + } + + /** + * Create a plaintext response + * + * ```php + * $response = React\Http\Message\Response::plaintext("Hello wörld!\n"); + * ``` + * + * This is a convenient shortcut method that returns the equivalent of this: + * + * ``` + * $response = new React\Http\Message\Response( + * React\Http\Message\Response::STATUS_OK, + * [ + * 'Content-Type' => 'text/plain; charset=utf-8' + * ], + * "Hello wörld!\n" + * ); + * ``` + * + * This method always returns a response with a `200 OK` status code and + * the appropriate `Content-Type` response header for the given plaintext + * string encoded in UTF-8 (Unicode). It's generally recommended to end the + * given plaintext string with a trailing newline. + * + * If you want to use a different status code or custom HTTP response + * headers, you can manipulate the returned response object using the + * provided PSR-7 methods or directly instantiate a custom HTTP response + * object using the `Response` constructor: + * + * ```php + * $response = React\Http\Message\Response::plaintext( + * "Error: Invalid user name given.\n" + * )->withStatus(React\Http\Message\Response::STATUS_BAD_REQUEST); + * ``` + * + * @param string $text + * @return self + */ + public static function plaintext($text) + { + return new self(self::STATUS_OK, array('Content-Type' => 'text/plain; charset=utf-8'), $text); + } + + /** + * Create an XML response + * + * ```php + * $xml = << + * + * Hello wörld! + * + * + * XML; + * + * $response = React\Http\Message\Response::xml($xml); + * ``` + * + * This is a convenient shortcut method that returns the equivalent of this: + * + * ``` + * $response = new React\Http\Message\Response( + * React\Http\Message\Response::STATUS_OK, + * [ + * 'Content-Type' => 'application/xml' + * ], + * $xml + * ); + * ``` + * + * This method always returns a response with a `200 OK` status code and + * the appropriate `Content-Type` response header for the given XML source + * string. It's generally recommended to use UTF-8 (Unicode) and specify + * this as part of the leading XML declaration and to end the given XML + * source string with a trailing newline. + * + * If you want to use a different status code or custom HTTP response + * headers, you can manipulate the returned response object using the + * provided PSR-7 methods or directly instantiate a custom HTTP response + * object using the `Response` constructor: + * + * ```php + * $response = React\Http\Message\Response::xml( + * "Invalid user name given.\n" + * )->withStatus(React\Http\Message\Response::STATUS_BAD_REQUEST); + * ``` + * + * @param string $xml + * @return self + */ + public static function xml($xml) + { + return new self(self::STATUS_OK, array('Content-Type' => 'application/xml'), $xml); + } + /** * @param int $status HTTP status code (e.g. 200/404), see `self::STATUS_*` constants * @param array $headers additional response headers diff --git a/tests/Message/ResponseTest.php b/tests/Message/ResponseTest.php index 3c6ad3fd..ed21cdc2 100644 --- a/tests/Message/ResponseTest.php +++ b/tests/Message/ResponseTest.php @@ -53,4 +53,75 @@ public function testResourceBodyWillThrow() $this->setExpectedException('InvalidArgumentException'); new Response(200, array(), tmpfile()); } + + + public function testHtmlMethodReturnsHtmlResponse() + { + $response = Response::html('Hello wörld!'); + + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals('text/html; charset=utf-8', $response->getHeaderLine('Content-Type')); + $this->assertEquals('Hello wörld!', (string) $response->getBody()); + } + + /** + * @requires PHP 5.4 + */ + public function testJsonMethodReturnsPrettyPrintedJsonResponse() + { + $response = Response::json(array('text' => 'Hello wörld!')); + + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals('application/json', $response->getHeaderLine('Content-Type')); + $this->assertEquals("{\n \"text\": \"Hello wörld!\"\n}\n", (string) $response->getBody()); + } + + /** + * @requires PHP 5.6.6 + */ + public function testJsonMethodReturnsZeroFractionsInJsonResponse() + { + $response = Response::json(1.0); + + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals('application/json', $response->getHeaderLine('Content-Type')); + $this->assertEquals("1.0\n", (string) $response->getBody()); + } + + public function testJsonMethodReturnsJsonTextForSimpleString() + { + $response = Response::json('Hello world!'); + + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals('application/json', $response->getHeaderLine('Content-Type')); + $this->assertEquals("\"Hello world!\"\n", (string) $response->getBody()); + } + + public function testJsonMethodThrowsForInvalidString() + { + if (PHP_VERSION_ID < 50500) { + $this->setExpectedException('InvalidArgumentException', 'Unable to encode given data as JSON'); + } else { + $this->setExpectedException('InvalidArgumentException', 'Unable to encode given data as JSON: Malformed UTF-8 characters, possibly incorrectly encoded'); + } + Response::json("Hello w\xF6rld!"); + } + + public function testPlaintextMethodReturnsPlaintextResponse() + { + $response = Response::plaintext("Hello wörld!\n"); + + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals('text/plain; charset=utf-8', $response->getHeaderLine('Content-Type')); + $this->assertEquals("Hello wörld!\n", (string) $response->getBody()); + } + + public function testXmlMethodReturnsXmlResponse() + { + $response = Response::xml('Hello wörld!'); + + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals('application/xml', $response->getHeaderLine('Content-Type')); + $this->assertEquals('Hello wörld!', (string) $response->getBody()); + } } From a49404c6e61dc01623844d887e3533e6a98c3542 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 22 Jan 2022 14:31:58 +0100 Subject: [PATCH 389/456] Update documentation and examples to use new response factory methods --- README.md | 140 ++++++---------------- examples/51-server-hello-world.php | 13 +- examples/52-server-count-visitors.php | 11 +- examples/53-server-whatsmyip.php | 13 +- examples/54-server-query-parameter.php | 11 +- examples/55-server-cookie-handling.php | 24 +--- examples/56-server-sleep.php | 22 ++-- examples/57-server-error-handling.php | 28 ++--- examples/58-server-stream-response.php | 3 +- examples/59-server-json-api.php | 41 ++----- examples/61-server-hello-world-https.php | 13 +- examples/62-server-form-upload.php | 6 +- examples/63-server-streaming-request.php | 14 +-- examples/71-server-http-proxy.php | 24 +--- examples/99-server-benchmark-download.php | 6 +- 15 files changed, 99 insertions(+), 270 deletions(-) diff --git a/README.md b/README.md index e178ee72..23e4b5fa 100644 --- a/README.md +++ b/README.md @@ -112,11 +112,7 @@ This is an HTTP server which responds with `Hello World!` to every request. require __DIR__ . '/vendor/autoload.php'; $http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { - return new React\Http\Message\Response( - React\Http\Message\Response::STATUS_OK, - array( - 'Content-Type' => 'text/plain' - ), + return React\Http\Message\Response::plaintext( "Hello World!\n" ); }); @@ -738,11 +734,7 @@ object and expects a [response](#server-response) object in return: ```php $http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { - return new React\Http\Message\Response( - React\Http\Message\Response::STATUS_OK, - array( - 'Content-Type' => 'text/plain' - ), + return React\Http\Message\Response::plaintext( "Hello World!\n" ); }); @@ -953,14 +945,10 @@ and will be passed to the callback function like this. ```php $http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { - $body = "The method of the request is: " . $request->getMethod(); - $body .= "The requested path is: " . $request->getUri()->getPath(); + $body = "The method of the request is: " . $request->getMethod() . "\n"; + $body .= "The requested path is: " . $request->getUri()->getPath() . "\n"; - return new React\Http\Message\Response( - React\Http\Message\Response::STATUS_OK, - array( - 'Content-Type' => 'text/plain' - ), + return React\Http\Message\Response::plaintext( $body ); }); @@ -996,13 +984,9 @@ The following parameters are currently available: ```php $http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { - $body = "Your IP is: " . $request->getServerParams()['REMOTE_ADDR']; + $body = "Your IP is: " . $request->getServerParams()['REMOTE_ADDR'] . "\n"; - return new React\Http\Message\Response( - React\Http\Message\Response::STATUS_OK, - array( - 'Content-Type' => 'text/plain' - ), + return React\Http\Message\Response::plaintext( $body ); }); @@ -1030,11 +1014,7 @@ $http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterf $body = 'The value of "foo" is: ' . htmlspecialchars($queryParams['foo']); } - return new React\Http\Message\Response( - React\Http\Message\Response::STATUS_OK, - array( - 'Content-Type' => 'text/html' - ), + return React\Http\Message\Response::html( $body ); }); @@ -1077,9 +1057,7 @@ request headers (commonly used for `POST` requests for HTML form submission data $http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { $name = $request->getParsedBody()['name'] ?? 'anonymous'; - return new React\Http\Message\Response( - React\Http\Message\Response::STATUS_OK, - array(), + return React\Http\Message\Response::plaintext( "Hello $name!\n" ); }); @@ -1102,10 +1080,8 @@ $http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterf $data = json_decode((string)$request->getBody()); $name = $data->name ?? 'anonymous'; - return new React\Http\Message\Response( - React\Http\Message\Response::STATUS_OK, - array('Content-Type' => 'application/json'), - json_encode(['message' => "Hello $name!"]) + return React\Http\Message\Response::json( + ['message' => "Hello $name!"] ); }); ``` @@ -1125,9 +1101,7 @@ $http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterf $files = $request->getUploadedFiles(); $name = isset($files['avatar']) ? $files['avatar']->getClientFilename() : 'nothing'; - return new React\Http\Message\Response( - React\Http\Message\Response::STATUS_OK, - array(), + return React\Http\Message\Response::plaintext( "Uploaded $name\n" ); }); @@ -1208,24 +1182,16 @@ $http = new React\Http\HttpServer( }); $body->on('end', function () use ($resolve, &$bytes){ - $resolve(new React\Http\Message\Response( - React\Http\Message\Response::STATUS_OK, - array( - 'Content-Type' => 'text/plain' - ), + $resolve(React\Http\Message\Response::plaintext( "Received $bytes bytes\n" )); }); // an error occures e.g. on invalid chunked encoded data or an unexpected 'end' event $body->on('error', function (Exception $e) use ($resolve, &$bytes) { - $resolve(new React\Http\Message\Response( - React\Http\Message\Response::STATUS_BAD_REQUEST, - array( - 'Content-Type' => 'text/plain' - ), + $resolve(React\Http\Message\Response::plaintext( "Encountered error after $bytes bytes: {$e->getMessage()}\n" - )); + )->withStatus(React\Http\Message\Response::STATUS_BAD_REQUEST)); }); }); } @@ -1272,23 +1238,15 @@ $http = new React\Http\HttpServer( function (Psr\Http\Message\ServerRequestInterface $request) { $size = $request->getBody()->getSize(); if ($size === null) { - $body = 'The request does not contain an explicit length.'; - $body .= 'This example does not accept chunked transfer encoding.'; - - return new React\Http\Message\Response( - React\Http\Message\Response::STATUS_LENGTH_REQUIRED, - array( - 'Content-Type' => 'text/plain' - ), + $body = "The request does not contain an explicit length. "; + $body .= "This example does not accept chunked transfer encoding.\n"; + + return React\Http\Message\Response::plaintext( $body - ); + )->withStatus(React\Http\Message\Response::STATUS_LENGTH_REQUIRED); } - return new React\Http\Message\Response( - React\Http\Message\Response::STATUS_OK, - array( - 'Content-Type' => 'text/plain' - ), + return React\Http\Message\Response::plaintext( "Request body size: " . $size . " bytes\n" ); } @@ -1344,25 +1302,16 @@ $http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterf $key = 'react\php'; if (isset($request->getCookieParams()[$key])) { - $body = "Your cookie value is: " . $request->getCookieParams()[$key]; + $body = "Your cookie value is: " . $request->getCookieParams()[$key] . "\n"; - return new React\Http\Message\Response( - React\Http\Message\Response::STATUS_OK, - array( - 'Content-Type' => 'text/plain' - ), + return React\Http\Message\Response::plaintext( $body ); } - return new React\Http\Message\Response( - React\Http\Message\Response::STATUS_OK, - array( - 'Content-Type' => 'text/plain', - 'Set-Cookie' => urlencode($key) . '=' . urlencode('test;more') - ), - "Your cookie has been set." - ); + return React\Http\Message\Response::plaintext( + "Your cookie has been set.\n" + )->withHeader('Set-Cookie', urlencode($key) . '=' . urlencode('test;more')); }); ``` @@ -1413,11 +1362,7 @@ In its most simple form, you can use it like this: ```php $http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { - return new React\Http\Message\Response( - React\Http\Message\Response::STATUS_OK, - array( - 'Content-Type' => 'text/plain' - ), + return React\Http\Message\Response::plaintext( "Hello World!\n" ); }); @@ -1441,18 +1386,17 @@ This example shows how such a long-term action could look like: ```php $http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { - return new Promise(function ($resolve, $reject) { + $promise = new Promise(function ($resolve, $reject) { Loop::addTimer(1.5, function() use ($resolve) { - $response = new React\Http\Message\Response( - React\Http\Message\Response::STATUS_OK, - array( - 'Content-Type' => 'text/plain' - ), - "Hello world" - ); - $resolve($response); + $resolve(); }); }); + + return $promise->then(function () { + return React\Http\Message\Response::plaintext( + "Hello World!" + ); + }); }); ``` @@ -1571,11 +1515,7 @@ a `string` response body like this: ```php $http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { - return new React\Http\Message\Response( - React\Http\Message\Response::STATUS_OK, - array( - 'Content-Type' => 'text/plain' - ), + return React\Http\Message\Response::plaintext( "Hello World!\n" ); }); @@ -1845,11 +1785,9 @@ $http = new React\Http\HttpServer( $resolve($next($request)); }); return $promise->then(null, function (Exception $e) { - return new React\Http\Message\Response( - React\Http\Message\Response::STATUS_INTERNAL_SERVER_ERROR, - array(), - 'Internal error: ' . $e->getMessage() - ); + return React\Http\Message\Response::plaintext( + 'Internal error: ' . $e->getMessage() . "\n" + )->withStatus(React\Http\Message\Response::STATUS_INTERNAL_SERVER_ERROR); }); }, function (Psr\Http\Message\ServerRequestInterface $request) { diff --git a/examples/51-server-hello-world.php b/examples/51-server-hello-world.php index 88831525..9ff84eee 100644 --- a/examples/51-server-hello-world.php +++ b/examples/51-server-hello-world.php @@ -1,17 +1,10 @@ 'text/plain' - ), - "Hello world\n" +$http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { + return React\Http\Message\Response::plaintext( + "Hello World!\n" ); }); diff --git a/examples/52-server-count-visitors.php b/examples/52-server-count-visitors.php index bdd53af9..341f9498 100644 --- a/examples/52-server-count-visitors.php +++ b/examples/52-server-count-visitors.php @@ -1,17 +1,10 @@ 'text/plain' - ), +$http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) use (&$counter) { + return React\Http\Message\Response::plaintext( "Welcome number " . ++$counter . "!\n" ); }); diff --git a/examples/53-server-whatsmyip.php b/examples/53-server-whatsmyip.php index e0835a82..1e394b9e 100644 --- a/examples/53-server-whatsmyip.php +++ b/examples/53-server-whatsmyip.php @@ -1,18 +1,11 @@ getServerParams()['REMOTE_ADDR']; +$http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { + $body = "Your IP is: " . $request->getServerParams()['REMOTE_ADDR'] . "\n"; - return new Response( - Response::STATUS_OK, - array( - 'Content-Type' => 'text/plain' - ), + return React\Http\Message\Response::plaintext( $body ); }); diff --git a/examples/54-server-query-parameter.php b/examples/54-server-query-parameter.php index 18dd56b0..9b2d5749 100644 --- a/examples/54-server-query-parameter.php +++ b/examples/54-server-query-parameter.php @@ -1,11 +1,8 @@ getQueryParams(); $body = 'The query parameter "foo" is not set. Click the following link '; @@ -15,11 +12,7 @@ $body = 'The value of "foo" is: ' . htmlspecialchars($queryParams['foo']); } - return new Response( - Response::STATUS_OK, - array( - 'Content-Type' => 'text/html' - ), + return React\Http\Message\Response::html( $body ); }); diff --git a/examples/55-server-cookie-handling.php b/examples/55-server-cookie-handling.php index 8260fc33..796da24d 100644 --- a/examples/55-server-cookie-handling.php +++ b/examples/55-server-cookie-handling.php @@ -1,33 +1,21 @@ getCookieParams()[$key])) { - $body = "Your cookie value is: " . $request->getCookieParams()[$key]; + $body = "Your cookie value is: " . $request->getCookieParams()[$key] . "\n"; - return new Response( - Response::STATUS_OK, - array( - 'Content-Type' => 'text/plain' - ), + return React\Http\Message\Response::plaintext( $body ); } - return new Response( - Response::STATUS_OK, - array( - 'Content-Type' => 'text/plain', - 'Set-Cookie' => urlencode($key) . '=' . urlencode('test;more') - ), - "Your cookie has been set." - ); + return React\Http\Message\Response::plaintext( + "Your cookie has been set.\n" + )->withHeader('Set-Cookie', urlencode($key) . '=' . urlencode('test;more')); }); $socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); diff --git a/examples/56-server-sleep.php b/examples/56-server-sleep.php index 6bb6f82b..2a3c9027 100644 --- a/examples/56-server-sleep.php +++ b/examples/56-server-sleep.php @@ -1,25 +1,21 @@ 'text/plain' - ), - "Hello world" - ); - $resolve($response); + $resolve(); }); }); + + return $promise->then(function () { + return React\Http\Message\Response::plaintext( + "Hello world!\n" + ); + }); }); $socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); diff --git a/examples/57-server-error-handling.php b/examples/57-server-error-handling.php index 71cbad15..a9fb6bad 100644 --- a/examples/57-server-error-handling.php +++ b/examples/57-server-error-handling.php @@ -1,30 +1,18 @@ 'text/plain' - ), - "Hello World!\n" - ); + if ($count % 2 === 0) { + throw new Exception('Second call'); + } - $resolve($response); - }); + return React\Http\Message\Response::plaintext( + "Hello World!\n" + ); }); $socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); diff --git a/examples/58-server-stream-response.php b/examples/58-server-stream-response.php index 015ddd9a..cf65a3bf 100644 --- a/examples/58-server-stream-response.php +++ b/examples/58-server-stream-response.php @@ -1,13 +1,12 @@ getMethod() !== 'GET' || $request->getUri()->getPath() !== '/') { return new Response(Response::STATUS_NOT_FOUND); } diff --git a/examples/59-server-json-api.php b/examples/59-server-json-api.php index 0d50b52b..f48be7e3 100644 --- a/examples/59-server-json-api.php +++ b/examples/59-server-json-api.php @@ -6,49 +6,32 @@ // $ php examples/59-server-json-api.php 8080 // $ curl -v http://localhost:8080/ -H 'Content-Type: application/json' -d '{"name":"Alice"}' -use Psr\Http\Message\ServerRequestInterface; use React\Http\Message\Response; require __DIR__ . '/../vendor/autoload.php'; -$http = new React\Http\HttpServer(function (ServerRequestInterface $request) { +$http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { if ($request->getHeaderLine('Content-Type') !== 'application/json') { - return new Response( - Response::STATUS_UNSUPPORTED_MEDIA_TYPE, - array( - 'Content-Type' => 'application/json' - ), - json_encode(array('error' => 'Only supports application/json')) . "\n" - ); + return Response::json( + array('error' => 'Only supports application/json') + )->withStatus(Response::STATUS_UNSUPPORTED_MEDIA_TYPE); } $input = json_decode($request->getBody()->getContents()); if (json_last_error() !== JSON_ERROR_NONE) { - return new Response( - Response::STATUS_BAD_REQUEST, - array( - 'Content-Type' => 'application/json' - ), - json_encode(array('error' => 'Invalid JSON data given')) . "\n" - ); + return Response::json( + array('error' => 'Invalid JSON data given') + )->withStatus(Response::STATUS_BAD_REQUEST); } if (!isset($input->name) || !is_string($input->name)) { - return new Response( - Response::STATUS_UNPROCESSABLE_ENTITY, - array( - 'Content-Type' => 'application/json' - ), - json_encode(array('error' => 'JSON data does not contain a string "name" property')) . "\n" - ); + return Response::json( + array('error' => 'JSON data does not contain a string "name" property') + )->withStatus(Response::STATUS_UNPROCESSABLE_ENTITY); } - return new Response( - Response::STATUS_OK, - array( - 'Content-Type' => 'application/json' - ), - json_encode(array('message' => 'Hello ' . $input->name)) . "\n" + return Response::json( + array('message' => 'Hello ' . $input->name) ); }); diff --git a/examples/61-server-hello-world-https.php b/examples/61-server-hello-world-https.php index 2fd6f9af..23906430 100644 --- a/examples/61-server-hello-world-https.php +++ b/examples/61-server-hello-world-https.php @@ -1,17 +1,10 @@ 'text/plain' - ), - "Hello world!\n" +$http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { + return React\Http\Message\Response::plaintext( + "Hello World!\n" ); }); diff --git a/examples/62-server-form-upload.php b/examples/62-server-form-upload.php index 899caa0a..52864c82 100644 --- a/examples/62-server-form-upload.php +++ b/examples/62-server-form-upload.php @@ -109,11 +109,7 @@ HTML; - return new Response( - Response::STATUS_OK, - array( - 'Content-Type' => 'text/html; charset=UTF-8' - ), + return Response::html( $html ); }; diff --git a/examples/63-server-streaming-request.php b/examples/63-server-streaming-request.php index b20b8f08..fef6f008 100644 --- a/examples/63-server-streaming-request.php +++ b/examples/63-server-streaming-request.php @@ -19,24 +19,16 @@ function (Psr\Http\Message\ServerRequestInterface $request) { }); $body->on('end', function () use ($resolve, &$bytes){ - $resolve(new React\Http\Message\Response( - React\Http\Message\Response::STATUS_OK, - array( - 'Content-Type' => 'text/plain' - ), + $resolve(React\Http\Message\Response::plaintext( "Received $bytes bytes\n" )); }); // an error occures e.g. on invalid chunked encoded data or an unexpected 'end' event $body->on('error', function (Exception $e) use ($resolve, &$bytes) { - $resolve(new React\Http\Message\Response( - React\Http\Message\Response::STATUS_BAD_REQUEST, - array( - 'Content-Type' => 'text/plain' - ), + $resolve(React\Http\Message\Response::plaintext( "Encountered error after $bytes bytes: {$e->getMessage()}\n" - )); + )->withStatus(React\Http\Message\Response::STATUS_BAD_REQUEST)); }); }); } diff --git a/examples/71-server-http-proxy.php b/examples/71-server-http-proxy.php index e0bf8404..cf63c4ae 100644 --- a/examples/71-server-http-proxy.php +++ b/examples/71-server-http-proxy.php @@ -3,25 +3,17 @@ // $ php examples/71-server-http-proxy.php 8080 // $ curl -v --proxy http://localhost:8080 http://reactphp.org/ -use Psr\Http\Message\RequestInterface; -use React\Http\Message\Response; -use RingCentral\Psr7; - require __DIR__ . '/../vendor/autoload.php'; // Note how this example uses the `HttpServer` without the `StreamingRequestMiddleware`. // This means that this proxy buffers the whole request before "processing" it. // As such, this is store-and-forward proxy. This could also use the advanced // `StreamingRequestMiddleware` to forward the incoming request as it comes in. -$http = new React\Http\HttpServer(function (RequestInterface $request) { +$http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { if (strpos($request->getRequestTarget(), '://') === false) { - return new Response( - Response::STATUS_BAD_REQUEST, - array( - 'Content-Type' => 'text/plain' - ), - 'This is a plain HTTP proxy' - ); + return React\Http\Message\Response::plaintext( + "This is a plain HTTP proxy\n" + )->withStatus(React\Http\Message\Response::STATUS_BAD_REQUEST); } // prepare outgoing client request by updating request-target and Host header @@ -35,12 +27,8 @@ // pseudo code only: simply dump the outgoing request as a string // left up as an exercise: use an HTTP client to send the outgoing request // and forward the incoming response to the original client request - return new Response( - Response::STATUS_OK, - array( - 'Content-Type' => 'text/plain' - ), - Psr7\str($outgoing) + return React\Http\Message\Response::plaintext( + RingCentral\Psr7\str($outgoing) ); }); diff --git a/examples/99-server-benchmark-download.php b/examples/99-server-benchmark-download.php index 6c737605..ddd4760a 100644 --- a/examples/99-server-benchmark-download.php +++ b/examples/99-server-benchmark-download.php @@ -93,11 +93,7 @@ public function getSize() $http = new React\Http\HttpServer(function (ServerRequestInterface $request) { switch ($request->getUri()->getPath()) { case '/': - return new Response( - Response::STATUS_OK, - array( - 'Content-Type' => 'text/html' - ), + return Response::html( '1g.bin
10g.bin' ); case '/1g.bin': From 59961cc4a5b14481728f07c591546be18fa3a5c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 3 Feb 2022 14:17:37 +0100 Subject: [PATCH 390/456] Prepare v1.6.0 release --- CHANGELOG.md | 42 ++++++++++++++++++++++++++++++++++++++++++ README.md | 12 ++++++------ 2 files changed, 48 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e12b787e..41079cdb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,47 @@ # Changelog +## 1.6.0 (2022-02-03) + +* Feature: Add factory methods for common HTML/JSON/plaintext/XML response types. + (#439 by @clue) + + ```php + $response = React\Http\Response\html("

Hello wörld!

\n"); + $response = React\Http\Response\json(['message' => 'Hello wörld!']); + $response = React\Http\Response\plaintext("Hello wörld!\n"); + $response = React\Http\Response\xml("Hello wörld!\n"); + $response = React\Http\Response\redirect('https://reactphp.org/'); + ``` + +* Feature: Expose all status code constants via `Response` class. + (#432 by @clue) + + ```php + $response = new React\Http\Message\Response( + React\Http\Message\Response::STATUS_OK, // 200 OK + … + ); + $response = new React\Http\Message\Response( + React\Http\Message\Response::STATUS_NOT_FOUND, // 404 Not Found + … + ); + ``` + +* Feature: Full support for PHP 8.1 release. + (#433 by @SimonFrings and #434 by @clue) + +* Feature / Fix: Improve protocol handling for HTTP responses with no body. + (#429 and #430 by @clue) + +* Internal refactoring and internal improvements for handling requests and responses. + (#422 by @WyriHaximus and #431 by @clue) + +* Improve documentation, update proxy examples, include error reporting in examples. + (#420, #424, #426, and #427 by @clue) + +* Update test suite to use default loop. + (#438 by @clue) + ## 1.5.0 (2021-08-04) * Feature: Update `Browser` signature to take optional `$connector` as first argument and diff --git a/README.md b/README.md index 23e4b5fa..8e58aebc 100644 --- a/README.md +++ b/README.md @@ -2904,14 +2904,14 @@ new React\Http\Middleware\RequestBodyParserMiddleware(10 * 1024, 100); // 100 fi ## Install -The recommended way to install this library is [through Composer](https://getcomposer.org). +The recommended way to install this library is [through Composer](https://getcomposer.org/). [New to Composer?](https://getcomposer.org/doc/00-intro.md) This project follows [SemVer](https://semver.org/). This will install the latest supported version: ```bash -$ composer require react/http:^1.5 +$ composer require react/http:^1.6 ``` See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. @@ -2919,12 +2919,12 @@ See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. This project aims to run on any platform and thus does not require any PHP extensions and supports running on legacy PHP 5.3 through current PHP 8+ and HHVM. -It's *highly recommended to use PHP 7+* for this project. +It's *highly recommended to use the latest supported PHP version* for this project. ## Tests To run the test suite, you first need to clone this repo and then install all -dependencies [through Composer](https://getcomposer.org): +dependencies [through Composer](https://getcomposer.org/): ```bash $ composer install @@ -2933,7 +2933,7 @@ $ composer install To run the test suite, go to the project root and run: ```bash -$ php vendor/bin/phpunit +$ vendor/bin/phpunit ``` The test suite also contains a number of functional integration tests that rely @@ -2941,7 +2941,7 @@ on a stable internet connection. If you do not want to run these, they can simply be skipped like this: ```bash -$ php vendor/bin/phpunit --exclude-group internet +$ vendor/bin/phpunit --exclude-group internet ``` ## License From 83162766662e19f3367d8d30da41e1c07c39c5b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 4 Feb 2022 08:56:40 +0100 Subject: [PATCH 391/456] Skip memory tests when lowering memory limit does not work --- tests/HttpServerTest.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/HttpServerTest.php b/tests/HttpServerTest.php index 23dcc758..a6d8057b 100644 --- a/tests/HttpServerTest.php +++ b/tests/HttpServerTest.php @@ -404,7 +404,9 @@ public function testConstructServerWithUnlimitedMemoryLimitDoesNotLimitConcurren public function testConstructServerWithMemoryLimitDoesLimitConcurrency() { $old = ini_get('memory_limit'); - ini_set('memory_limit', '100M'); + if (@ini_set('memory_limit', '128M') === false) { + $this->markTestSkipped('Unable to change memory limit'); + } $http = new HttpServer(function () { }); From 185680b8ca0704051e5c9ab9e7b98b53820b5aef Mon Sep 17 00:00:00 2001 From: Simon Bennett Date: Fri, 4 Mar 2022 13:47:33 +0000 Subject: [PATCH 392/456] [Bug] Allow explicit assigning of `Content-Length` for `HEAD` requests --- src/Io/StreamingServer.php | 2 ++ tests/Io/StreamingServerTest.php | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/src/Io/StreamingServer.php b/src/Io/StreamingServer.php index 7818f0bd..d73d527d 100644 --- a/src/Io/StreamingServer.php +++ b/src/Io/StreamingServer.php @@ -265,6 +265,8 @@ public function handleResponse(ConnectionInterface $connection, ServerRequestInt if (($method === 'CONNECT' && $code >= 200 && $code < 300) || ($code >= 100 && $code < 200) || $code === Response::STATUS_NO_CONTENT) { // 2xx response to CONNECT and 1xx and 204 MUST NOT include Content-Length or Transfer-Encoding header $response = $response->withoutHeader('Content-Length'); + } elseif ($method === 'HEAD' && $response->hasHeader('Content-Length')) { + // HEAD Request: preserve explicit Content-Length } elseif ($code === Response::STATUS_NOT_MODIFIED && ($response->hasHeader('Content-Length') || $body->getSize() === 0)) { // 304 Not Modified: preserve explicit Content-Length and preserve missing header if body is empty } elseif ($body->getSize() !== null) { diff --git a/tests/Io/StreamingServerTest.php b/tests/Io/StreamingServerTest.php index 59cfd118..45729f2b 100644 --- a/tests/Io/StreamingServerTest.php +++ b/tests/Io/StreamingServerTest.php @@ -1426,6 +1426,38 @@ function ($data) use (&$buffer) { $this->assertContainsString("\r\nContent-Length: 3\r\n", $buffer); } + public function testResponseContainsExplicitContentLengthHeaderForHeadRequests() + { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { + return new Response( + 200, + array('Content-Length' => 3), + '' + ); + }); + + $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\nHost: localhost\r\n\r\n"; + $this->connection->emit('data', array($data)); + + $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); + $this->assertContainsString("\r\nContent-Length: 3\r\n", $buffer); + } + public function testResponseContainsNoResponseBodyForNotModifiedStatus() { $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { From 201021fe3fafa2a6dda9ded7b2759653224418aa Mon Sep 17 00:00:00 2001 From: Simon Frings Date: Tue, 15 Mar 2022 13:31:28 +0100 Subject: [PATCH 393/456] Add badge to show number of project installations --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 8e58aebc..154f4901 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # HTTP [![CI status](https://github.com/reactphp/http/workflows/CI/badge.svg)](https://github.com/reactphp/http/actions) +[![installs on Packagist](https://img.shields.io/packagist/dt/react/http?color=blue&label=installs%20on%20Packagist)](https://packagist.org/packages/react/http) Event-driven, streaming HTTP client and server implementation for [ReactPHP](https://reactphp.org/). From 8fbdc0cec0cd754a6cdeb3e29e501d8dbd28b591 Mon Sep 17 00:00:00 2001 From: Jorrit Schippers Date: Tue, 5 Apr 2022 23:01:57 +0200 Subject: [PATCH 394/456] README.md: fix typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 154f4901..56acb365 100644 --- a/README.md +++ b/README.md @@ -389,7 +389,7 @@ try { $response = Block\await($promise, Loop::get()); // response successfully received } catch (Exception $e) { - // an error occured while performing the request + // an error occurred while performing the request } ``` From dd2cb536e7dc071bebe6d78e93f4f58de6f931a4 Mon Sep 17 00:00:00 2001 From: Simon Frings Date: Tue, 12 Apr 2022 09:41:48 +0200 Subject: [PATCH 395/456] Fix legacy HHVM build by downgrading Composer --- .github/workflows/ci.yml | 1 + README.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c64ef6ab..27577838 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,5 +43,6 @@ jobs: - uses: azjezz/setup-hhvm@v1 with: version: lts-3.30 + - run: composer self-update --2.2 # downgrade Composer for HHVM - run: hhvm $(which composer) install - run: hhvm vendor/bin/phpunit diff --git a/README.md b/README.md index 154f4901..33ec6967 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # HTTP -[![CI status](https://github.com/reactphp/http/workflows/CI/badge.svg)](https://github.com/reactphp/http/actions) +[![CI status](https://github.com/reactphp/http/actions/workflows/ci.yml/badge.svg)](https://github.com/reactphp/http/actions) [![installs on Packagist](https://img.shields.io/packagist/dt/react/http?color=blue&label=installs%20on%20Packagist)](https://packagist.org/packages/react/http) Event-driven, streaming HTTP client and server implementation for [ReactPHP](https://reactphp.org/). From a6bc13dfd46eee82fc45796f0350135a06123f65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 14 Apr 2022 13:28:22 +0200 Subject: [PATCH 396/456] Improve documentation for closing response stream --- README.md | 10 +++++++++- examples/58-server-stream-response.php | 8 +++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 56acb365..aa26deb3 100644 --- a/README.md +++ b/README.md @@ -1426,15 +1426,23 @@ may only support strings. $http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { $stream = new ThroughStream(); + // send some data every once in a while with periodic timer $timer = Loop::addPeriodicTimer(0.5, function () use ($stream) { $stream->write(microtime(true) . PHP_EOL); }); - Loop::addTimer(5, function() use ($timer, $stream) { + // end stream after a few seconds + $timeout = Loop::addTimer(5.0, function() use ($stream, $timer) { Loop::cancelTimer($timer); $stream->end(); }); + // stop timer if stream is closed (such as when connection is closed) + $stream->on('close', function () use ($timer, $timeout) { + Loop::cancelTimer($timer); + Loop::cancelTimer($timeout); + }); + return new React\Http\Message\Response( React\Http\Message\Response::STATUS_OK, array( diff --git a/examples/58-server-stream-response.php b/examples/58-server-stream-response.php index cf65a3bf..9d12461a 100644 --- a/examples/58-server-stream-response.php +++ b/examples/58-server-stream-response.php @@ -18,14 +18,16 @@ $stream->write(microtime(true) . PHP_EOL); }); - // demo for ending stream after a few seconds - Loop::addTimer(5.0, function() use ($stream) { + // end stream after a few seconds + $timeout = Loop::addTimer(5.0, function() use ($stream, $timer) { + Loop::cancelTimer($timer); $stream->end(); }); // stop timer if stream is closed (such as when connection is closed) - $stream->on('close', function () use ($timer) { + $stream->on('close', function () use ($timer, $timeout) { Loop::cancelTimer($timer); + Loop::cancelTimer($timeout); }); return new Response( From 9c2d98f1f5b590082faa1a74aba5549cd0107977 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 19 May 2022 19:30:38 +0200 Subject: [PATCH 397/456] Improve performance, add internal `Clock`, reuse clock in same tick --- src/Io/Clock.php | 54 ++++++++++ src/Io/RequestHeaderParser.php | 12 ++- src/Io/StreamingServer.php | 11 +- tests/HttpServerTest.php | 8 +- tests/Io/ClockTest.php | 43 ++++++++ tests/Io/RequestHeaderParserTest.php | 156 +++++++++++++++++++-------- tests/Io/StreamingServerTest.php | 33 +++--- 7 files changed, 254 insertions(+), 63 deletions(-) create mode 100644 src/Io/Clock.php create mode 100644 tests/Io/ClockTest.php diff --git a/src/Io/Clock.php b/src/Io/Clock.php new file mode 100644 index 00000000..92c1cb09 --- /dev/null +++ b/src/Io/Clock.php @@ -0,0 +1,54 @@ +loop = $loop; + } + + /** @return float */ + public function now() + { + if ($this->now === null) { + $this->now = \microtime(true); + + // remember clock for current loop tick only and update on next tick + $now =& $this->now; + $this->loop->futureTick(function () use (&$now) { + assert($now !== null); + $now = null; + }); + } + + return $this->now; + } +} diff --git a/src/Io/RequestHeaderParser.php b/src/Io/RequestHeaderParser.php index e5554c46..6930afaf 100644 --- a/src/Io/RequestHeaderParser.php +++ b/src/Io/RequestHeaderParser.php @@ -24,6 +24,14 @@ class RequestHeaderParser extends EventEmitter { private $maxSize = 8192; + /** @var Clock */ + private $clock; + + public function __construct(Clock $clock) + { + $this->clock = $clock; + } + public function handle(ConnectionInterface $conn) { $buffer = ''; @@ -155,8 +163,8 @@ public function parseRequest($headers, $remoteSocketUri, $localSocketUri) // create new obj implementing ServerRequestInterface by preserving all // previous properties and restoring original request-target $serverParams = array( - 'REQUEST_TIME' => \time(), - 'REQUEST_TIME_FLOAT' => \microtime(true) + 'REQUEST_TIME' => (int) ($now = $this->clock->now()), + 'REQUEST_TIME_FLOAT' => $now ); // scheme is `http` unless TLS is used diff --git a/src/Io/StreamingServer.php b/src/Io/StreamingServer.php index d73d527d..a054be3d 100644 --- a/src/Io/StreamingServer.php +++ b/src/Io/StreamingServer.php @@ -84,7 +84,9 @@ final class StreamingServer extends EventEmitter { private $callback; private $parser; - private $loop; + + /** @var Clock */ + private $clock; /** * Creates an HTTP server that invokes the given callback for each incoming HTTP request @@ -104,10 +106,9 @@ public function __construct(LoopInterface $loop, $requestHandler) throw new \InvalidArgumentException('Invalid request handler given'); } - $this->loop = $loop; - $this->callback = $requestHandler; - $this->parser = new RequestHeaderParser(); + $this->clock = new Clock($loop); + $this->parser = new RequestHeaderParser($this->clock); $that = $this; $this->parser->on('headers', function (ServerRequestInterface $request, ConnectionInterface $conn) use ($that) { @@ -255,7 +256,7 @@ public function handleResponse(ConnectionInterface $connection, ServerRequestInt // assign default "Date" header from current time automatically if (!$response->hasHeader('Date')) { // IMF-fixdate = day-name "," SP date1 SP time-of-day SP GMT - $response = $response->withHeader('Date', gmdate('D, d M Y H:i:s') . ' GMT'); + $response = $response->withHeader('Date', gmdate('D, d M Y H:i:s', (int) $this->clock->now()) . ' GMT'); } elseif ($response->getHeaderLine('Date') === ''){ $response = $response->withoutHeader('Date'); } diff --git a/tests/HttpServerTest.php b/tests/HttpServerTest.php index a6d8057b..4d00fcef 100644 --- a/tests/HttpServerTest.php +++ b/tests/HttpServerTest.php @@ -54,9 +54,13 @@ public function testConstructWithoutLoopAssignsLoopAutomatically() $ref->setAccessible(true); $streamingServer = $ref->getValue($http); - $ref = new \ReflectionProperty($streamingServer, 'loop'); + $ref = new \ReflectionProperty($streamingServer, 'clock'); $ref->setAccessible(true); - $loop = $ref->getValue($streamingServer); + $clock = $ref->getValue($streamingServer); + + $ref = new \ReflectionProperty($clock, 'loop'); + $ref->setAccessible(true); + $loop = $ref->getValue($clock); $this->assertInstanceOf('React\EventLoop\LoopInterface', $loop); } diff --git a/tests/Io/ClockTest.php b/tests/Io/ClockTest.php new file mode 100644 index 00000000..8f4b90fa --- /dev/null +++ b/tests/Io/ClockTest.php @@ -0,0 +1,43 @@ +getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $clock = new Clock($loop); + + $now = $clock->now(); + $this->assertTrue(is_float($now)); // assertIsFloat() on PHPUnit 8+ + $this->assertEquals($now, $clock->now()); + } + + public function testNowResetsMemoizedTimestampOnFutureTick() + { + $tick = null; + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->once())->method('futureTick')->with($this->callback(function ($cb) use (&$tick) { + $tick = $cb; + return true; + })); + + $clock = new Clock($loop); + + $now = $clock->now(); + + $ref = new \ReflectionProperty($clock, 'now'); + $ref->setAccessible(true); + $this->assertEquals($now, $ref->getValue($clock)); + + $this->assertNotNull($tick); + $tick(); + + $this->assertNull($ref->getValue($clock)); + } +} diff --git a/tests/Io/RequestHeaderParserTest.php b/tests/Io/RequestHeaderParserTest.php index 356443fb..7ba7fe01 100644 --- a/tests/Io/RequestHeaderParserTest.php +++ b/tests/Io/RequestHeaderParserTest.php @@ -10,7 +10,9 @@ class RequestHeaderParserTest extends TestCase { public function testSplitShouldHappenOnDoubleCrlf() { - $parser = new RequestHeaderParser(); + $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + + $parser = new RequestHeaderParser($clock); $parser->on('headers', $this->expectCallableNever()); $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); @@ -29,7 +31,9 @@ public function testSplitShouldHappenOnDoubleCrlf() public function testFeedInOneGo() { - $parser = new RequestHeaderParser(); + $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + + $parser = new RequestHeaderParser($clock); $parser->on('headers', $this->expectCallableOnce()); $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); @@ -41,7 +45,9 @@ public function testFeedInOneGo() public function testFeedTwoRequestsOnSeparateConnections() { - $parser = new RequestHeaderParser(); + $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + + $parser = new RequestHeaderParser($clock); $called = 0; $parser->on('headers', function () use (&$called) { @@ -65,7 +71,9 @@ public function testHeadersEventShouldEmitRequestAndConnection() $request = null; $conn = null; - $parser = new RequestHeaderParser(); + $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + + $parser = new RequestHeaderParser($clock); $parser->on('headers', function ($parsedRequest, $connection) use (&$request, &$conn) { $request = $parsedRequest; $conn = $connection; @@ -88,7 +96,9 @@ public function testHeadersEventShouldEmitRequestAndConnection() public function testHeadersEventShouldEmitRequestWhichShouldEmitEndForStreamingBodyWithoutContentLengthFromInitialRequestBody() { - $parser = new RequestHeaderParser(); + $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + + $parser = new RequestHeaderParser($clock); $ended = false; $that = $this; @@ -112,7 +122,9 @@ public function testHeadersEventShouldEmitRequestWhichShouldEmitEndForStreamingB public function testHeadersEventShouldEmitRequestWhichShouldEmitStreamingBodyDataFromInitialRequestBody() { - $parser = new RequestHeaderParser(); + $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + + $parser = new RequestHeaderParser($clock); $buffer = ''; $that = $this; @@ -140,7 +152,9 @@ public function testHeadersEventShouldEmitRequestWhichShouldEmitStreamingBodyDat public function testHeadersEventShouldEmitRequestWhichShouldEmitStreamingBodyWithPlentyOfDataFromInitialRequestBody() { - $parser = new RequestHeaderParser(); + $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + + $parser = new RequestHeaderParser($clock); $buffer = ''; $that = $this; @@ -166,7 +180,9 @@ public function testHeadersEventShouldEmitRequestWhichShouldEmitStreamingBodyWit public function testHeadersEventShouldEmitRequestWhichShouldNotEmitStreamingBodyDataWithoutContentLengthFromInitialRequestBody() { - $parser = new RequestHeaderParser(); + $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + + $parser = new RequestHeaderParser($clock); $buffer = ''; $that = $this; @@ -191,7 +207,9 @@ public function testHeadersEventShouldEmitRequestWhichShouldNotEmitStreamingBody public function testHeadersEventShouldEmitRequestWhichShouldEmitStreamingBodyDataUntilContentLengthBoundaryFromInitialRequestBody() { - $parser = new RequestHeaderParser(); + $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + + $parser = new RequestHeaderParser($clock); $buffer = ''; $that = $this; @@ -218,7 +236,9 @@ public function testHeadersEventShouldParsePathAndQueryString() { $request = null; - $parser = new RequestHeaderParser(); + $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + + $parser = new RequestHeaderParser($clock); $parser->on('headers', function ($parsedRequest) use (&$request) { $request = $parsedRequest; }); @@ -245,7 +265,9 @@ public function testHeaderEventWithShouldApplyDefaultAddressFromLocalConnectionA { $request = null; - $parser = new RequestHeaderParser(); + $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + + $parser = new RequestHeaderParser($clock); $parser->on('headers', function ($parsedRequest) use (&$request) { $request = $parsedRequest; }); @@ -264,7 +286,9 @@ public function testHeaderEventViaHttpsShouldApplyHttpsSchemeFromLocalTlsConnect { $request = null; - $parser = new RequestHeaderParser(); + $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + + $parser = new RequestHeaderParser($clock); $parser->on('headers', function ($parsedRequest) use (&$request) { $request = $parsedRequest; }); @@ -284,7 +308,9 @@ public function testHeaderOverflowShouldEmitError() $error = null; $passedConnection = null; - $parser = new RequestHeaderParser(); + $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + + $parser = new RequestHeaderParser($clock); $parser->on('headers', $this->expectCallableNever()); $parser->on('error', function ($message, $connection) use (&$error, &$passedConnection) { $error = $message; @@ -306,7 +332,9 @@ public function testInvalidEmptyRequestHeadersParseException() { $error = null; - $parser = new RequestHeaderParser(); + $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + + $parser = new RequestHeaderParser($clock); $parser->on('headers', $this->expectCallableNever()); $parser->on('error', function ($message) use (&$error) { $error = $message; @@ -325,7 +353,9 @@ public function testInvalidMalformedRequestLineParseException() { $error = null; - $parser = new RequestHeaderParser(); + $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + + $parser = new RequestHeaderParser($clock); $parser->on('headers', $this->expectCallableNever()); $parser->on('error', function ($message) use (&$error) { $error = $message; @@ -344,7 +374,9 @@ public function testInvalidMalformedRequestHeadersThrowsParseException() { $error = null; - $parser = new RequestHeaderParser(); + $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + + $parser = new RequestHeaderParser($clock); $parser->on('headers', $this->expectCallableNever()); $parser->on('error', function ($message) use (&$error) { $error = $message; @@ -363,7 +395,9 @@ public function testInvalidMalformedRequestHeadersWhitespaceThrowsParseException { $error = null; - $parser = new RequestHeaderParser(); + $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + + $parser = new RequestHeaderParser($clock); $parser->on('headers', $this->expectCallableNever()); $parser->on('error', function ($message) use (&$error) { $error = $message; @@ -382,7 +416,9 @@ public function testInvalidAbsoluteFormSchemeEmitsError() { $error = null; - $parser = new RequestHeaderParser(); + $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + + $parser = new RequestHeaderParser($clock); $parser->on('headers', $this->expectCallableNever()); $parser->on('error', function ($message) use (&$error) { $error = $message; @@ -401,7 +437,9 @@ public function testOriginFormWithSchemeSeparatorInParam() { $request = null; - $parser = new RequestHeaderParser(); + $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + + $parser = new RequestHeaderParser($clock); $parser->on('error', $this->expectCallableNever()); $parser->on('headers', function ($parsedRequest, $parsedBodyBuffer) use (&$request) { $request = $parsedRequest; @@ -426,7 +464,9 @@ public function testUriStartingWithColonSlashSlashFails() { $error = null; - $parser = new RequestHeaderParser(); + $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + + $parser = new RequestHeaderParser($clock); $parser->on('headers', $this->expectCallableNever()); $parser->on('error', function ($message) use (&$error) { $error = $message; @@ -445,7 +485,9 @@ public function testInvalidAbsoluteFormWithFragmentEmitsError() { $error = null; - $parser = new RequestHeaderParser(); + $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + + $parser = new RequestHeaderParser($clock); $parser->on('headers', $this->expectCallableNever()); $parser->on('error', function ($message) use (&$error) { $error = $message; @@ -464,7 +506,9 @@ public function testInvalidHeaderContainsFullUri() { $error = null; - $parser = new RequestHeaderParser(); + $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + + $parser = new RequestHeaderParser($clock); $parser->on('headers', $this->expectCallableNever()); $parser->on('error', function ($message) use (&$error) { $error = $message; @@ -483,7 +527,9 @@ public function testInvalidAbsoluteFormWithHostHeaderEmpty() { $error = null; - $parser = new RequestHeaderParser(); + $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + + $parser = new RequestHeaderParser($clock); $parser->on('headers', $this->expectCallableNever()); $parser->on('error', function ($message) use (&$error) { $error = $message; @@ -502,7 +548,9 @@ public function testInvalidConnectRequestWithNonAuthorityForm() { $error = null; - $parser = new RequestHeaderParser(); + $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + + $parser = new RequestHeaderParser($clock); $parser->on('headers', $this->expectCallableNever()); $parser->on('error', function ($message) use (&$error) { $error = $message; @@ -521,7 +569,9 @@ public function testInvalidHttpVersion() { $error = null; - $parser = new RequestHeaderParser(); + $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + + $parser = new RequestHeaderParser($clock); $parser->on('headers', $this->expectCallableNever()); $parser->on('error', function ($message) use (&$error) { $error = $message; @@ -541,7 +591,9 @@ public function testInvalidContentLengthRequestHeaderWillEmitError() { $error = null; - $parser = new RequestHeaderParser(); + $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + + $parser = new RequestHeaderParser($clock); $parser->on('headers', $this->expectCallableNever()); $parser->on('error', function ($message) use (&$error) { $error = $message; @@ -561,7 +613,9 @@ public function testInvalidRequestWithMultipleContentLengthRequestHeadersWillEmi { $error = null; - $parser = new RequestHeaderParser(); + $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + + $parser = new RequestHeaderParser($clock); $parser->on('headers', $this->expectCallableNever()); $parser->on('error', function ($message) use (&$error) { $error = $message; @@ -581,7 +635,9 @@ public function testInvalidTransferEncodingRequestHeaderWillEmitError() { $error = null; - $parser = new RequestHeaderParser(); + $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + + $parser = new RequestHeaderParser($clock); $parser->on('headers', $this->expectCallableNever()); $parser->on('error', function ($message) use (&$error) { $error = $message; @@ -601,7 +657,9 @@ public function testInvalidRequestWithBothTransferEncodingAndContentLengthWillEm { $error = null; - $parser = new RequestHeaderParser(); + $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + + $parser = new RequestHeaderParser($clock); $parser->on('headers', $this->expectCallableNever()); $parser->on('error', function ($message) use (&$error) { $error = $message; @@ -621,7 +679,10 @@ public function testServerParamsWillBeSetOnHttpsRequest() { $request = null; - $parser = new RequestHeaderParser(); + $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + $clock->expects($this->once())->method('now')->willReturn(1652972091.3958); + + $parser = new RequestHeaderParser($clock); $parser->on('headers', function ($parsedRequest) use (&$request) { $request = $parsedRequest; @@ -637,8 +698,8 @@ public function testServerParamsWillBeSetOnHttpsRequest() $serverParams = $request->getServerParams(); $this->assertEquals('on', $serverParams['HTTPS']); - $this->assertNotEmpty($serverParams['REQUEST_TIME']); - $this->assertNotEmpty($serverParams['REQUEST_TIME_FLOAT']); + $this->assertEquals(1652972091, $serverParams['REQUEST_TIME']); + $this->assertEquals(1652972091.3958, $serverParams['REQUEST_TIME_FLOAT']); $this->assertEquals('127.1.1.1', $serverParams['SERVER_ADDR']); $this->assertEquals('8000', $serverParams['SERVER_PORT']); @@ -651,7 +712,10 @@ public function testServerParamsWillBeSetOnHttpRequest() { $request = null; - $parser = new RequestHeaderParser(); + $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + $clock->expects($this->once())->method('now')->willReturn(1652972091.3958); + + $parser = new RequestHeaderParser($clock); $parser->on('headers', function ($parsedRequest) use (&$request) { $request = $parsedRequest; @@ -667,8 +731,8 @@ public function testServerParamsWillBeSetOnHttpRequest() $serverParams = $request->getServerParams(); $this->assertArrayNotHasKey('HTTPS', $serverParams); - $this->assertNotEmpty($serverParams['REQUEST_TIME']); - $this->assertNotEmpty($serverParams['REQUEST_TIME_FLOAT']); + $this->assertEquals(1652972091, $serverParams['REQUEST_TIME']); + $this->assertEquals(1652972091.3958, $serverParams['REQUEST_TIME_FLOAT']); $this->assertEquals('127.1.1.1', $serverParams['SERVER_ADDR']); $this->assertEquals('8000', $serverParams['SERVER_PORT']); @@ -681,7 +745,10 @@ public function testServerParamsWillNotSetRemoteAddressForUnixDomainSockets() { $request = null; - $parser = new RequestHeaderParser(); + $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + $clock->expects($this->once())->method('now')->willReturn(1652972091.3958); + + $parser = new RequestHeaderParser($clock); $parser->on('headers', function ($parsedRequest) use (&$request) { $request = $parsedRequest; @@ -697,8 +764,8 @@ public function testServerParamsWillNotSetRemoteAddressForUnixDomainSockets() $serverParams = $request->getServerParams(); $this->assertArrayNotHasKey('HTTPS', $serverParams); - $this->assertNotEmpty($serverParams['REQUEST_TIME']); - $this->assertNotEmpty($serverParams['REQUEST_TIME_FLOAT']); + $this->assertEquals(1652972091, $serverParams['REQUEST_TIME']); + $this->assertEquals(1652972091.3958, $serverParams['REQUEST_TIME_FLOAT']); $this->assertArrayNotHasKey('SERVER_ADDR', $serverParams); $this->assertArrayNotHasKey('SERVER_PORT', $serverParams); @@ -715,7 +782,10 @@ public function testServerParamsWontBeSetOnMissingUrls() $request = null; - $parser = new RequestHeaderParser(); + $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + $clock->expects($this->once())->method('now')->willReturn(1652972091.3958); + + $parser = new RequestHeaderParser($clock); $parser->on('headers', function ($parsedRequest) use (&$request) { $request = $parsedRequest; @@ -728,8 +798,8 @@ public function testServerParamsWontBeSetOnMissingUrls() $serverParams = $request->getServerParams(); - $this->assertNotEmpty($serverParams['REQUEST_TIME']); - $this->assertNotEmpty($serverParams['REQUEST_TIME_FLOAT']); + $this->assertEquals(1652972091, $serverParams['REQUEST_TIME']); + $this->assertEquals(1652972091.3958, $serverParams['REQUEST_TIME_FLOAT']); $this->assertArrayNotHasKey('SERVER_ADDR', $serverParams); $this->assertArrayNotHasKey('SERVER_PORT', $serverParams); @@ -742,7 +812,9 @@ public function testQueryParmetersWillBeSet() { $request = null; - $parser = new RequestHeaderParser(); + $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + + $parser = new RequestHeaderParser($clock); $parser->on('headers', function ($parsedRequest) use (&$request) { $request = $parsedRequest; diff --git a/tests/Io/StreamingServerTest.php b/tests/Io/StreamingServerTest.php index 45729f2b..2703362a 100644 --- a/tests/Io/StreamingServerTest.php +++ b/tests/Io/StreamingServerTest.php @@ -2292,12 +2292,20 @@ function ($data) use (&$buffer) { $this->assertContainsString("5\r\nhello\r\n", $buffer); } - public function testResponseWithoutExplicitDateHeaderWillAddCurrentDate() + public function testResponseWithoutExplicitDateHeaderWillAddCurrentDateFromClock() { $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Response(); }); + $ref = new \ReflectionProperty($server, 'clock'); + $ref->setAccessible(true); + $clock = $ref->getValue($server); + + $ref = new \ReflectionProperty($clock, 'now'); + $ref->setAccessible(true); + $ref->setValue($clock, 1652972091.3958); + $buffer = ''; $this->connection ->expects($this->any()) @@ -2318,11 +2326,11 @@ function ($data) use (&$buffer) { $this->connection->emit('data', array($data)); $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); - $this->assertContainsString("Date:", $buffer); + $this->assertContainsString("Date: Thu, 19 May 2022 14:54:51 GMT\r\n", $buffer); $this->assertContainsString("\r\n\r\n", $buffer); } - public function testResponseWIthCustomDateHeaderOverwritesDefault() + public function testResponseWithCustomDateHeaderOverwritesDefault() { $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Response( @@ -3022,7 +3030,8 @@ public function testRequestCookieWithSeparatorWillBeAddedToServerRequest() $this->assertEquals(array('hello' => 'world', 'test' => 'abc'), $requestValidation->getCookieParams()); } - public function testRequestCookieWithCommaValueWillBeAddedToServerRequest() { + public function testRequestCookieWithCommaValueWillBeAddedToServerRequest() + { $requestValidation = null; $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use (&$requestValidation) { $requestValidation = $request; @@ -3045,7 +3054,7 @@ public function testNewConnectionWillInvokeParserOnce() { $server = new StreamingServer(Loop::get(), $this->expectCallableNever()); - $parser = $this->getMockBuilder('React\Http\Io\RequestHeaderParser')->getMock(); + $parser = $this->getMockBuilder('React\Http\Io\RequestHeaderParser')->disableOriginalConstructor()->getMock(); $parser->expects($this->once())->method('handle'); $ref = new \ReflectionProperty($server, 'parser'); @@ -3062,7 +3071,7 @@ public function testNewConnectionWillInvokeParserOnceAndInvokeRequestHandlerWhen $server = new StreamingServer(Loop::get(), $this->expectCallableOnceWith($request)); - $parser = $this->getMockBuilder('React\Http\Io\RequestHeaderParser')->getMock(); + $parser = $this->getMockBuilder('React\Http\Io\RequestHeaderParser')->disableOriginalConstructor()->getMock(); $parser->expects($this->once())->method('handle'); $ref = new \ReflectionProperty($server, 'parser'); @@ -3085,7 +3094,7 @@ public function testNewConnectionWillInvokeParserOnceAndInvokeRequestHandlerWhen $server = new StreamingServer(Loop::get(), $this->expectCallableOnceWith($request)); - $parser = $this->getMockBuilder('React\Http\Io\RequestHeaderParser')->getMock(); + $parser = $this->getMockBuilder('React\Http\Io\RequestHeaderParser')->disableOriginalConstructor()->getMock(); $parser->expects($this->once())->method('handle'); $ref = new \ReflectionProperty($server, 'parser'); @@ -3110,7 +3119,7 @@ public function testNewConnectionWillInvokeParserOnceAndInvokeRequestHandlerWhen return new Response(200, array('Connection' => 'close')); }); - $parser = $this->getMockBuilder('React\Http\Io\RequestHeaderParser')->getMock(); + $parser = $this->getMockBuilder('React\Http\Io\RequestHeaderParser')->disableOriginalConstructor()->getMock(); $parser->expects($this->once())->method('handle'); $ref = new \ReflectionProperty($server, 'parser'); @@ -3135,7 +3144,7 @@ public function testNewConnectionWillInvokeParserTwiceAfterInvokingRequestHandle return new Response(); }); - $parser = $this->getMockBuilder('React\Http\Io\RequestHeaderParser')->getMock(); + $parser = $this->getMockBuilder('React\Http\Io\RequestHeaderParser')->disableOriginalConstructor()->getMock(); $parser->expects($this->exactly(2))->method('handle'); $ref = new \ReflectionProperty($server, 'parser'); @@ -3160,7 +3169,7 @@ public function testNewConnectionWillInvokeParserTwiceAfterInvokingRequestHandle return new Response(); }); - $parser = $this->getMockBuilder('React\Http\Io\RequestHeaderParser')->getMock(); + $parser = $this->getMockBuilder('React\Http\Io\RequestHeaderParser')->disableOriginalConstructor()->getMock(); $parser->expects($this->exactly(2))->method('handle'); $ref = new \ReflectionProperty($server, 'parser'); @@ -3186,7 +3195,7 @@ public function testNewConnectionWillInvokeParserOnceAfterInvokingRequestHandler return new Response(200, array(), $body); }); - $parser = $this->getMockBuilder('React\Http\Io\RequestHeaderParser')->getMock(); + $parser = $this->getMockBuilder('React\Http\Io\RequestHeaderParser')->disableOriginalConstructor()->getMock(); $parser->expects($this->once())->method('handle'); $ref = new \ReflectionProperty($server, 'parser'); @@ -3212,7 +3221,7 @@ public function testNewConnectionWillInvokeParserTwiceAfterInvokingRequestHandle return new Response(200, array(), $body); }); - $parser = $this->getMockBuilder('React\Http\Io\RequestHeaderParser')->getMock(); + $parser = $this->getMockBuilder('React\Http\Io\RequestHeaderParser')->disableOriginalConstructor()->getMock(); $parser->expects($this->exactly(2))->method('handle'); $ref = new \ReflectionProperty($server, 'parser'); From dc8ca43cc58dd3555d107d89bd5a9e36c13d4e7d Mon Sep 17 00:00:00 2001 From: Nicolas Hedger <649677+nhedger@users.noreply.github.com> Date: Tue, 14 Jun 2022 20:29:41 +0200 Subject: [PATCH 398/456] chore(docs): remove $ sign from shell commands --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index d3a93ceb..2d2eb4ce 100644 --- a/README.md +++ b/README.md @@ -2920,7 +2920,7 @@ This project follows [SemVer](https://semver.org/). This will install the latest supported version: ```bash -$ composer require react/http:^1.6 +composer require react/http:^1.6 ``` See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. @@ -2936,13 +2936,13 @@ To run the test suite, you first need to clone this repo and then install all dependencies [through Composer](https://getcomposer.org/): ```bash -$ composer install +composer install ``` To run the test suite, go to the project root and run: ```bash -$ vendor/bin/phpunit +vendor/bin/phpunit ``` The test suite also contains a number of functional integration tests that rely @@ -2950,7 +2950,7 @@ on a stable internet connection. If you do not want to run these, they can simply be skipped like this: ```bash -$ vendor/bin/phpunit --exclude-group internet +vendor/bin/phpunit --exclude-group internet ``` ## License From 8b2c5c899ba47ac357cd451752a1079a4386d158 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 18 Jun 2022 12:10:39 +0200 Subject: [PATCH 399/456] Refactor internal `Transaction` to avoid assigning dynamic properties --- src/Io/ClientRequestState.php | 16 +++++++ src/Io/Transaction.php | 79 +++++++++++++++----------------- tests/HttpServerTest.php | 3 ++ tests/Io/StreamingServerTest.php | 3 ++ 4 files changed, 59 insertions(+), 42 deletions(-) create mode 100644 src/Io/ClientRequestState.php diff --git a/src/Io/ClientRequestState.php b/src/Io/ClientRequestState.php new file mode 100644 index 00000000..73a63a14 --- /dev/null +++ b/src/Io/ClientRequestState.php @@ -0,0 +1,16 @@ +pending)) { - $deferred->pending->cancel(); - unset($deferred->pending); + $state = new ClientRequestState(); + $deferred = new Deferred(function () use ($state) { + if ($state->pending !== null) { + $state->pending->cancel(); + $state->pending = null; } }); - $deferred->numRequests = 0; - // use timeout from options or default to PHP's default_socket_timeout (60) $timeout = (float)($this->timeout !== null ? $this->timeout : ini_get("default_socket_timeout")); $loop = $this->loop; - $this->next($request, $deferred)->then( - function (ResponseInterface $response) use ($deferred, $loop, &$timeout) { - if (isset($deferred->timeout)) { - $loop->cancelTimer($deferred->timeout); - unset($deferred->timeout); + $this->next($request, $deferred, $state)->then( + function (ResponseInterface $response) use ($state, $deferred, $loop, &$timeout) { + if ($state->timeout !== null) { + $loop->cancelTimer($state->timeout); + $state->timeout = null; } $timeout = -1; $deferred->resolve($response); }, - function ($e) use ($deferred, $loop, &$timeout) { - if (isset($deferred->timeout)) { - $loop->cancelTimer($deferred->timeout); - unset($deferred->timeout); + function ($e) use ($state, $deferred, $loop, &$timeout) { + if ($state->timeout !== null) { + $loop->cancelTimer($state->timeout); + $state->timeout = null; } $timeout = -1; $deferred->reject($e); @@ -106,13 +105,13 @@ function ($e) use ($deferred, $loop, &$timeout) { $body = $request->getBody(); if ($body instanceof ReadableStreamInterface && $body->isReadable()) { $that = $this; - $body->on('close', function () use ($that, $deferred, &$timeout) { + $body->on('close', function () use ($that, $deferred, $state, &$timeout) { if ($timeout >= 0) { - $that->applyTimeout($deferred, $timeout); + $that->applyTimeout($deferred, $state, $timeout); } }); } else { - $this->applyTimeout($deferred, $timeout); + $this->applyTimeout($deferred, $state, $timeout); } return $deferred->promise(); @@ -120,53 +119,51 @@ function ($e) use ($deferred, $loop, &$timeout) { /** * @internal - * @param Deferred $deferred - * @param number $timeout + * @param number $timeout * @return void */ - public function applyTimeout(Deferred $deferred, $timeout) + public function applyTimeout(Deferred $deferred, ClientRequestState $state, $timeout) { - $deferred->timeout = $this->loop->addTimer($timeout, function () use ($timeout, $deferred) { + $state->timeout = $this->loop->addTimer($timeout, function () use ($timeout, $deferred, $state) { $deferred->reject(new \RuntimeException( 'Request timed out after ' . $timeout . ' seconds' )); - if (isset($deferred->pending)) { - $deferred->pending->cancel(); - unset($deferred->pending); + if ($state->pending !== null) { + $state->pending->cancel(); + $state->pending = null; } }); } - private function next(RequestInterface $request, Deferred $deferred) + private function next(RequestInterface $request, Deferred $deferred, ClientRequestState $state) { $this->progress('request', array($request)); $that = $this; - ++$deferred->numRequests; + ++$state->numRequests; $promise = $this->sender->send($request); if (!$this->streaming) { - $promise = $promise->then(function ($response) use ($deferred, $that) { - return $that->bufferResponse($response, $deferred); + $promise = $promise->then(function ($response) use ($deferred, $state, $that) { + return $that->bufferResponse($response, $deferred, $state); }); } - $deferred->pending = $promise; + $state->pending = $promise; return $promise->then( - function (ResponseInterface $response) use ($request, $that, $deferred) { - return $that->onResponse($response, $request, $deferred); + function (ResponseInterface $response) use ($request, $that, $deferred, $state) { + return $that->onResponse($response, $request, $deferred, $state); } ); } /** * @internal - * @param ResponseInterface $response * @return PromiseInterface Promise */ - public function bufferResponse(ResponseInterface $response, $deferred) + public function bufferResponse(ResponseInterface $response, Deferred $deferred, ClientRequestState $state) { $stream = $response->getBody(); @@ -205,26 +202,24 @@ function ($e) use ($stream, $maximumSize) { } ); - $deferred->pending = $promise; + $state->pending = $promise; return $promise; } /** * @internal - * @param ResponseInterface $response - * @param RequestInterface $request * @throws ResponseException * @return ResponseInterface|PromiseInterface */ - public function onResponse(ResponseInterface $response, RequestInterface $request, $deferred) + public function onResponse(ResponseInterface $response, RequestInterface $request, Deferred $deferred, ClientRequestState $state) { $this->progress('response', array($response, $request)); // follow 3xx (Redirection) response status codes if Location header is present and not explicitly disabled // @link https://tools.ietf.org/html/rfc7231#section-6.4 if ($this->followRedirects && ($response->getStatusCode() >= 300 && $response->getStatusCode() < 400) && $response->hasHeader('Location')) { - return $this->onResponseRedirect($response, $request, $deferred); + return $this->onResponseRedirect($response, $request, $deferred, $state); } // only status codes 200-399 are considered to be valid, reject otherwise @@ -242,7 +237,7 @@ public function onResponse(ResponseInterface $response, RequestInterface $reques * @return PromiseInterface * @throws \RuntimeException */ - private function onResponseRedirect(ResponseInterface $response, RequestInterface $request, Deferred $deferred) + private function onResponseRedirect(ResponseInterface $response, RequestInterface $request, Deferred $deferred, ClientRequestState $state) { // resolve location relative to last request URI $location = Uri::resolve($request->getUri(), $response->getHeaderLine('Location')); @@ -250,11 +245,11 @@ private function onResponseRedirect(ResponseInterface $response, RequestInterfac $request = $this->makeRedirectRequest($request, $location); $this->progress('redirect', array($request)); - if ($deferred->numRequests >= $this->maxRedirects) { + if ($state->numRequests >= $this->maxRedirects) { throw new \RuntimeException('Maximum number of redirects (' . $this->maxRedirects . ') exceeded'); } - return $this->next($request, $deferred); + return $this->next($request, $deferred, $state); } /** diff --git a/tests/HttpServerTest.php b/tests/HttpServerTest.php index 4d00fcef..31bc32ee 100644 --- a/tests/HttpServerTest.php +++ b/tests/HttpServerTest.php @@ -17,6 +17,9 @@ final class HttpServerTest extends TestCase private $connection; private $socket; + /** @var ?int */ + private $called = null; + /** * @before */ diff --git a/tests/Io/StreamingServerTest.php b/tests/Io/StreamingServerTest.php index 2703362a..a2700b86 100644 --- a/tests/Io/StreamingServerTest.php +++ b/tests/Io/StreamingServerTest.php @@ -17,6 +17,9 @@ class StreamingServerTest extends TestCase private $connection; private $socket; + /** @var ?int */ + private $called = null; + /** * @before */ From 90413fb088d701f77f12e50e5f471e3f000aca2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 9 Aug 2022 10:50:19 +0200 Subject: [PATCH 400/456] Avoid referencing unneeded explicit loop instance --- README.md | 4 ++-- tests/Io/SenderTest.php | 32 ++++++++++++++++++++++++-------- tests/Io/TransactionTest.php | 26 ++++++++++++++++---------- 3 files changed, 42 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 2d2eb4ce..752c01ff 100644 --- a/README.md +++ b/README.md @@ -386,7 +386,7 @@ $browser = new React\Http\Browser(); $promise = $browser->get('http://example.com/'); try { - $response = Block\await($promise, Loop::get()); + $response = Block\await($promise); // response successfully received } catch (Exception $e) { // an error occurred while performing the request @@ -401,7 +401,7 @@ $promises = array( $browser->get('http://www.example.org/'), ); -$responses = Block\awaitAll($promises, Loop::get()); +$responses = Block\awaitAll($promises); ``` Please refer to [clue/reactphp-block](https://github.com/clue/reactphp-block#readme) for more details. diff --git a/tests/Io/SenderTest.php b/tests/Io/SenderTest.php index 1c6d1d6b..8597f17a 100644 --- a/tests/Io/SenderTest.php +++ b/tests/Io/SenderTest.php @@ -42,8 +42,12 @@ public function testSenderRejectsInvalidUri() $promise = $sender->send($request); - $this->setExpectedException('InvalidArgumentException'); - Block\await($promise, $this->loop); + $exception = null; + $promise->then(null, function ($e) use (&$exception) { + $exception = $e; + }); + + $this->assertInstanceOf('InvalidArgumentException', $exception); } public function testSenderConnectorRejection() @@ -57,8 +61,12 @@ public function testSenderConnectorRejection() $promise = $sender->send($request); - $this->setExpectedException('RuntimeException'); - Block\await($promise, $this->loop); + $exception = null; + $promise->then(null, function ($e) use (&$exception) { + $exception = $e; + }); + + $this->assertInstanceOf('RuntimeException', $exception); } public function testSendPostWillAutomaticallySendContentLengthHeader() @@ -318,8 +326,12 @@ public function testCancelRequestWillCancelConnector() $promise = $sender->send($request); $promise->cancel(); - $this->setExpectedException('RuntimeException'); - Block\await($promise, $this->loop); + $exception = null; + $promise->then(null, function ($e) use (&$exception) { + $exception = $e; + }); + + $this->assertInstanceOf('RuntimeException', $exception); } public function testCancelRequestWillCloseConnection() @@ -337,8 +349,12 @@ public function testCancelRequestWillCloseConnection() $promise = $sender->send($request); $promise->cancel(); - $this->setExpectedException('RuntimeException'); - Block\await($promise, $this->loop); + $exception = null; + $promise->then(null, function ($e) use (&$exception) { + $exception = $e; + }); + + $this->assertInstanceOf('RuntimeException', $exception); } public function provideRequestProtocolVersion() diff --git a/tests/Io/TransactionTest.php b/tests/Io/TransactionTest.php index d62147b5..835e299c 100644 --- a/tests/Io/TransactionTest.php +++ b/tests/Io/TransactionTest.php @@ -5,7 +5,8 @@ use Clue\React\Block; use PHPUnit\Framework\MockObject\MockObject; use Psr\Http\Message\RequestInterface; -use RingCentral\Psr7\Response; +use Psr\Http\Message\ResponseInterface; +use React\Http\Io\ReadableBodyStream; use React\Http\Io\Transaction; use React\Http\Message\ResponseException; use React\EventLoop\Loop; @@ -14,7 +15,7 @@ use React\Stream\ThroughStream; use React\Tests\Http\TestCase; use RingCentral\Psr7\Request; -use React\Http\Io\ReadableBodyStream; +use RingCentral\Psr7\Response; class TransactionTest extends TestCase { @@ -372,13 +373,14 @@ public function testReceivingErrorResponseWillRejectWithResponseException() $transaction = $transaction->withOptions(array('timeout' => -1)); $promise = $transaction->send($request); - try { - Block\await($promise, $loop); - $this->fail(); - } catch (ResponseException $exception) { - $this->assertEquals(404, $exception->getCode()); - $this->assertSame($response, $exception->getResponse()); - } + $exception = null; + $promise->then(null, function ($reason) use (&$exception) { + $exception = $reason; + }); + + assert($exception instanceof ResponseException); + $this->assertEquals(404, $exception->getCode()); + $this->assertSame($response, $exception->getResponse()); } public function testReceivingStreamingBodyWillResolveWithBufferedResponseByDefault() @@ -461,8 +463,12 @@ public function testReceivingStreamingBodyWillResolveWithStreamingResponseIfStre $transaction = $transaction->withOptions(array('streaming' => true, 'timeout' => -1)); $promise = $transaction->send($request); - $response = Block\await($promise, $loop); + $response = null; + $promise->then(function ($value) use (&$response) { + $response = $value; + }); + assert($response instanceof ResponseInterface); $this->assertEquals(200, $response->getStatusCode()); $this->assertEquals('', (string)$response->getBody()); } From a2ae0f1eb655ec9766888c121f4182faba44dac1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 8 Aug 2022 19:20:55 +0200 Subject: [PATCH 401/456] Avoid using deprecated functions from clue/reactphp-block --- composer.json | 3 +- tests/Client/FunctionalIntegrationTest.php | 10 ++--- tests/FunctionalBrowserTest.php | 2 +- tests/FunctionalHttpServerTest.php | 46 +++++++++++----------- tests/Io/TransactionTest.php | 4 +- 5 files changed, 33 insertions(+), 32 deletions(-) diff --git a/composer.json b/composer.json index 4c9a0383..0d23e281 100644 --- a/composer.json +++ b/composer.json @@ -42,7 +42,8 @@ "clue/http-proxy-react": "^1.7", "clue/reactphp-ssh-proxy": "^1.3", "clue/socks-react": "^1.3", - "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35" + "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35", + "react/promise-timer": "^1.9" }, "autoload": { "psr-4": { "React\\Http\\": "src" } diff --git a/tests/Client/FunctionalIntegrationTest.php b/tests/Client/FunctionalIntegrationTest.php index 64a3ea8a..40b40a54 100644 --- a/tests/Client/FunctionalIntegrationTest.php +++ b/tests/Client/FunctionalIntegrationTest.php @@ -51,7 +51,7 @@ public function testRequestToLocalhostEmitsSingleRemoteConnection() $promise = Stream\first($request, 'close'); $request->end(); - Block\await($promise, null, self::TIMEOUT_LOCAL); + Block\await(\React\Promise\Timer\timeout($promise, self::TIMEOUT_LOCAL)); } public function testRequestLegacyHttpServerWithOnlyLineFeedReturnsSuccessfulResponse() @@ -73,7 +73,7 @@ public function testRequestLegacyHttpServerWithOnlyLineFeedReturnsSuccessfulResp $promise = Stream\first($request, 'close'); $request->end(); - Block\await($promise, null, self::TIMEOUT_LOCAL); + Block\await(\React\Promise\Timer\timeout($promise, self::TIMEOUT_LOCAL)); } /** @group internet */ @@ -94,7 +94,7 @@ public function testSuccessfulResponseEmitsEnd() $promise = Stream\first($request, 'close'); $request->end(); - Block\await($promise, null, self::TIMEOUT_REMOTE); + Block\await(\React\Promise\Timer\timeout($promise, self::TIMEOUT_REMOTE)); } /** @group internet */ @@ -122,7 +122,7 @@ public function testPostDataReturnsData() $request->end($data); - $buffer = Block\await($deferred->promise(), null, self::TIMEOUT_REMOTE); + $buffer = Block\await(\React\Promise\Timer\timeout($deferred->promise(), self::TIMEOUT_REMOTE)); $this->assertNotEquals('', $buffer); @@ -154,7 +154,7 @@ public function testPostJsonReturnsData() $request->end($data); - $buffer = Block\await($deferred->promise(), null, self::TIMEOUT_REMOTE); + $buffer = Block\await(\React\Promise\Timer\timeout($deferred->promise(), self::TIMEOUT_REMOTE)); $this->assertNotEquals('', $buffer); diff --git a/tests/FunctionalBrowserTest.php b/tests/FunctionalBrowserTest.php index 2b7bd58c..a9cc7244 100644 --- a/tests/FunctionalBrowserTest.php +++ b/tests/FunctionalBrowserTest.php @@ -531,7 +531,7 @@ public function testReceiveStreamAndExplicitlyCloseConnectionEvenWhenServerKeeps $response = Block\await($this->browser->get($this->base . 'get', array())); $this->assertEquals('hello', (string)$response->getBody()); - $ret = Block\await($closed->promise(), null, 0.1); + $ret = Block\await(\React\Promise\Timer\timeout($closed->promise(), 0.1)); $this->assertTrue($ret); $socket->close(); diff --git a/tests/FunctionalHttpServerTest.php b/tests/FunctionalHttpServerTest.php index 6fa85903..f543fb55 100644 --- a/tests/FunctionalHttpServerTest.php +++ b/tests/FunctionalHttpServerTest.php @@ -37,7 +37,7 @@ public function testPlainHttpOnRandomPort() return Stream\buffer($conn); }); - $response = Block\await($result, null, 1.0); + $response = Block\await(\React\Promise\Timer\timeout($result, 1.0)); $this->assertContainsString("HTTP/1.0 200 OK", $response); $this->assertContainsString('http://' . noScheme($socket->getAddress()) . '/', $response); @@ -64,7 +64,7 @@ function () { return Stream\buffer($conn); }); - $response = Block\await($result, null, 1.0); + $response = Block\await(\React\Promise\Timer\timeout($result, 1.0)); $this->assertContainsString("HTTP/1.0 404 Not Found", $response); @@ -88,7 +88,7 @@ public function testPlainHttpOnRandomPortWithoutHostHeaderUsesSocketUri() return Stream\buffer($conn); }); - $response = Block\await($result, null, 1.0); + $response = Block\await(\React\Promise\Timer\timeout($result, 1.0)); $this->assertContainsString("HTTP/1.0 200 OK", $response); $this->assertContainsString('http://' . noScheme($socket->getAddress()) . '/', $response); @@ -113,7 +113,7 @@ public function testPlainHttpOnRandomPortWithOtherHostHeaderTakesPrecedence() return Stream\buffer($conn); }); - $response = Block\await($result, null, 1.0); + $response = Block\await(\React\Promise\Timer\timeout($result, 1.0)); $this->assertContainsString("HTTP/1.0 200 OK", $response); $this->assertContainsString('http://localhost:1000/', $response); @@ -146,7 +146,7 @@ public function testSecureHttpsOnRandomPort() return Stream\buffer($conn); }); - $response = Block\await($result, null, 1.0); + $response = Block\await(\React\Promise\Timer\timeout($result, 1.0)); $this->assertContainsString("HTTP/1.0 200 OK", $response); $this->assertContainsString('https://' . noScheme($socket->getAddress()) . '/', $response); @@ -183,7 +183,7 @@ public function testSecureHttpsReturnsData() return Stream\buffer($conn); }); - $response = Block\await($result, null, 1.0); + $response = Block\await(\React\Promise\Timer\timeout($result, 1.0)); $this->assertContainsString("HTTP/1.0 200 OK", $response); $this->assertContainsString("\r\nContent-Length: 33000\r\n", $response); @@ -217,7 +217,7 @@ public function testSecureHttpsOnRandomPortWithoutHostHeaderUsesSocketUri() return Stream\buffer($conn); }); - $response = Block\await($result, null, 1.0); + $response = Block\await(\React\Promise\Timer\timeout($result, 1.0)); $this->assertContainsString("HTTP/1.0 200 OK", $response); $this->assertContainsString('https://' . noScheme($socket->getAddress()) . '/', $response); @@ -246,7 +246,7 @@ public function testPlainHttpOnStandardPortReturnsUriWithNoPort() return Stream\buffer($conn); }); - $response = Block\await($result, null, 1.0); + $response = Block\await(\React\Promise\Timer\timeout($result, 1.0)); $this->assertContainsString("HTTP/1.0 200 OK", $response); $this->assertContainsString('http://127.0.0.1/', $response); @@ -275,7 +275,7 @@ public function testPlainHttpOnStandardPortWithoutHostHeaderReturnsUriWithNoPort return Stream\buffer($conn); }); - $response = Block\await($result, null, 1.0); + $response = Block\await(\React\Promise\Timer\timeout($result, 1.0)); $this->assertContainsString("HTTP/1.0 200 OK", $response); $this->assertContainsString('http://127.0.0.1/', $response); @@ -313,7 +313,7 @@ public function testSecureHttpsOnStandardPortReturnsUriWithNoPort() return Stream\buffer($conn); }); - $response = Block\await($result, null, 1.0); + $response = Block\await(\React\Promise\Timer\timeout($result, 1.0)); $this->assertContainsString("HTTP/1.0 200 OK", $response); $this->assertContainsString('https://127.0.0.1/', $response); @@ -351,7 +351,7 @@ public function testSecureHttpsOnStandardPortWithoutHostHeaderUsesSocketUri() return Stream\buffer($conn); }); - $response = Block\await($result, null, 1.0); + $response = Block\await(\React\Promise\Timer\timeout($result, 1.0)); $this->assertContainsString("HTTP/1.0 200 OK", $response); $this->assertContainsString('https://127.0.0.1/', $response); @@ -380,7 +380,7 @@ public function testPlainHttpOnHttpsStandardPortReturnsUriWithPort() return Stream\buffer($conn); }); - $response = Block\await($result, null, 1.0); + $response = Block\await(\React\Promise\Timer\timeout($result, 1.0)); $this->assertContainsString("HTTP/1.0 200 OK", $response); $this->assertContainsString('http://127.0.0.1:443/', $response); @@ -418,7 +418,7 @@ public function testSecureHttpsOnHttpStandardPortReturnsUriWithPort() return Stream\buffer($conn); }); - $response = Block\await($result, null, 1.0); + $response = Block\await(\React\Promise\Timer\timeout($result, 1.0)); $this->assertContainsString("HTTP/1.0 200 OK", $response); $this->assertContainsString('https://127.0.0.1:80/', $response); @@ -446,7 +446,7 @@ public function testClosedStreamFromRequestHandlerWillSendEmptyBody() return Stream\buffer($conn); }); - $response = Block\await($result, null, 1.0); + $response = Block\await(\React\Promise\Timer\timeout($result, 1.0)); $this->assertStringStartsWith("HTTP/1.0 200 OK", $response); $this->assertStringEndsWith("\r\n\r\n", $response); @@ -477,7 +477,7 @@ function (RequestInterface $request) use ($once) { }); }); - Block\sleep(0.1); + \Clue\React\Block\await(\React\Promise\Timer\sleep(0.1)); $socket->close(); } @@ -507,7 +507,7 @@ function (RequestInterface $request) use ($stream) { }); // stream will be closed within 0.1s - $ret = Block\await(Stream\first($stream, 'close'), null, 0.1); + $ret = Block\await(\React\Promise\Timer\timeout(Stream\first($stream, 'close'), 0.1)); $socket->close(); @@ -536,7 +536,7 @@ public function testStreamFromRequestHandlerWillBeClosedIfConnectionCloses() }); // await response stream to be closed - $ret = Block\await(Stream\first($stream, 'close'), null, 1.0); + $ret = Block\await(\React\Promise\Timer\timeout(Stream\first($stream, 'close'), 1.0)); $socket->close(); @@ -571,7 +571,7 @@ public function testUpgradeWithThroughStreamReturnsDataAsGiven() return Stream\buffer($conn); }); - $response = Block\await($result, null, 1.0); + $response = Block\await(\React\Promise\Timer\timeout($result, 1.0)); $this->assertStringStartsWith("HTTP/1.1 101 Switching Protocols\r\n", $response); $this->assertStringEndsWith("\r\n\r\nhelloworld", $response); @@ -608,7 +608,7 @@ public function testUpgradeWithRequestBodyAndThroughStreamReturnsDataAsGiven() return Stream\buffer($conn); }); - $response = Block\await($result, null, 1.0); + $response = Block\await(\React\Promise\Timer\timeout($result, 1.0)); $this->assertStringStartsWith("HTTP/1.1 101 Switching Protocols\r\n", $response); $this->assertStringEndsWith("\r\n\r\nhelloworld", $response); @@ -644,7 +644,7 @@ public function testConnectWithThroughStreamReturnsDataAsGiven() return Stream\buffer($conn); }); - $response = Block\await($result, null, 1.0); + $response = Block\await(\React\Promise\Timer\timeout($result, 1.0)); $this->assertStringStartsWith("HTTP/1.1 200 OK\r\n", $response); $this->assertStringEndsWith("\r\n\r\nhelloworld", $response); @@ -684,7 +684,7 @@ public function testConnectWithThroughStreamReturnedFromPromiseReturnsDataAsGive return Stream\buffer($conn); }); - $response = Block\await($result, null, 1.0); + $response = Block\await(\React\Promise\Timer\timeout($result, 1.0)); $this->assertStringStartsWith("HTTP/1.1 200 OK\r\n", $response); $this->assertStringEndsWith("\r\n\r\nhelloworld", $response); @@ -717,7 +717,7 @@ public function testConnectWithClosedThroughStreamReturnsNoData() return Stream\buffer($conn); }); - $response = Block\await($result, null, 1.0); + $response = Block\await(\React\Promise\Timer\timeout($result, 1.0)); $this->assertStringStartsWith("HTTP/1.1 200 OK\r\n", $response); $this->assertStringEndsWith("\r\n\r\n", $response); @@ -760,7 +760,7 @@ function (ServerRequestInterface $request) { }); } - $responses = Block\await(Promise\all($result), null, 1.0); + $responses = Block\await(\React\Promise\Timer\timeout(Promise\all($result), 1.0)); foreach ($responses as $response) { $this->assertContainsString("HTTP/1.0 200 OK", $response, $response); diff --git a/tests/Io/TransactionTest.php b/tests/Io/TransactionTest.php index 835e299c..56b9d4f4 100644 --- a/tests/Io/TransactionTest.php +++ b/tests/Io/TransactionTest.php @@ -424,7 +424,7 @@ public function testReceivingStreamingBodyWithSizeExceedingMaximumResponseBuffer $promise = $transaction->send($request); $this->setExpectedException('OverflowException'); - Block\await($promise, null, 0.001); + Block\await(\React\Promise\Timer\timeout($promise, 0.001)); } public function testCancelBufferingResponseWillCloseStreamAndReject() @@ -445,7 +445,7 @@ public function testCancelBufferingResponseWillCloseStreamAndReject() $promise->cancel(); $this->setExpectedException('RuntimeException'); - Block\await($promise, null, 0.001); + Block\await(\React\Promise\Timer\timeout($promise, 0.001)); } public function testReceivingStreamingBodyWillResolveWithStreamingResponseIfStreamingIsEnabled() From 9946ba7bb7330cf72f96d744608fd0acea6fc7d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 9 Aug 2022 10:55:31 +0200 Subject: [PATCH 402/456] Update to use new reactphp/async package instead of clue/reactphp-block --- README.md | 20 +-- composer.json | 2 +- tests/BrowserTest.php | 1 - tests/Client/FunctionalIntegrationTest.php | 11 +- tests/FunctionalBrowserTest.php | 123 ++++++++++-------- tests/FunctionalHttpServerTest.php | 47 ++++--- tests/HttpServerTest.php | 7 +- tests/Io/MiddlewareRunnerTest.php | 5 +- tests/Io/SenderTest.php | 1 - tests/Io/TransactionTest.php | 7 +- .../RequestBodyBufferMiddlewareTest.php | 9 +- 11 files changed, 123 insertions(+), 110 deletions(-) diff --git a/README.md b/README.md index 752c01ff..ba8d8330 100644 --- a/README.md +++ b/README.md @@ -373,20 +373,19 @@ See also [`withFollowRedirects()`](#withfollowredirects) for more details. As stated above, this library provides you a powerful, async API by default. -If, however, you want to integrate this into your traditional, blocking environment, -you should look into also using [clue/reactphp-block](https://github.com/clue/reactphp-block). - -The resulting blocking code could look something like this: +You can also integrate this into your traditional, blocking environment by using +[reactphp/async](https://github.com/reactphp/async). This allows you to simply +await async HTTP requests like this: ```php -use Clue\React\Block; +use function React\Async\await; $browser = new React\Http\Browser(); $promise = $browser->get('http://example.com/'); try { - $response = Block\await($promise); + $response = await($promise); // response successfully received } catch (Exception $e) { // an error occurred while performing the request @@ -396,15 +395,20 @@ try { Similarly, you can also process multiple requests concurrently and await an array of `Response` objects: ```php +use function React\Async\await; +use function React\Promise\all; + $promises = array( $browser->get('http://example.com/'), $browser->get('http://www.example.org/'), ); -$responses = Block\awaitAll($promises); +$responses = await(all($promises)); ``` -Please refer to [clue/reactphp-block](https://github.com/clue/reactphp-block#readme) for more details. +This is made possible thanks to fibers available in PHP 8.1+ and our +compatibility API that also works on all supported PHP versions. +Please refer to [reactphp/async](https://github.com/reactphp/async#readme) for more details. Keep in mind the above remark about buffering the whole response message in memory. As an alternative, you may also see one of the following chapters for the diff --git a/composer.json b/composer.json index 0d23e281..57adfc2c 100644 --- a/composer.json +++ b/composer.json @@ -38,11 +38,11 @@ "ringcentral/psr7": "^1.2" }, "require-dev": { - "clue/block-react": "^1.5", "clue/http-proxy-react": "^1.7", "clue/reactphp-ssh-proxy": "^1.3", "clue/socks-react": "^1.3", "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35", + "react/async": "^4 || ^3 || ^2", "react/promise-timer": "^1.9" }, "autoload": { diff --git a/tests/BrowserTest.php b/tests/BrowserTest.php index 39be453a..f14b9ee6 100644 --- a/tests/BrowserTest.php +++ b/tests/BrowserTest.php @@ -2,7 +2,6 @@ namespace React\Tests\Http; -use Clue\React\Block; use Psr\Http\Message\RequestInterface; use React\Http\Browser; use React\Promise\Promise; diff --git a/tests/Client/FunctionalIntegrationTest.php b/tests/Client/FunctionalIntegrationTest.php index 40b40a54..d95bf828 100644 --- a/tests/Client/FunctionalIntegrationTest.php +++ b/tests/Client/FunctionalIntegrationTest.php @@ -2,7 +2,6 @@ namespace React\Tests\Http\Client; -use Clue\React\Block; use Psr\Http\Message\ResponseInterface; use React\EventLoop\Loop; use React\Http\Client\Client; @@ -51,7 +50,7 @@ public function testRequestToLocalhostEmitsSingleRemoteConnection() $promise = Stream\first($request, 'close'); $request->end(); - Block\await(\React\Promise\Timer\timeout($promise, self::TIMEOUT_LOCAL)); + \React\Async\await(\React\Promise\Timer\timeout($promise, self::TIMEOUT_LOCAL)); } public function testRequestLegacyHttpServerWithOnlyLineFeedReturnsSuccessfulResponse() @@ -73,7 +72,7 @@ public function testRequestLegacyHttpServerWithOnlyLineFeedReturnsSuccessfulResp $promise = Stream\first($request, 'close'); $request->end(); - Block\await(\React\Promise\Timer\timeout($promise, self::TIMEOUT_LOCAL)); + \React\Async\await(\React\Promise\Timer\timeout($promise, self::TIMEOUT_LOCAL)); } /** @group internet */ @@ -94,7 +93,7 @@ public function testSuccessfulResponseEmitsEnd() $promise = Stream\first($request, 'close'); $request->end(); - Block\await(\React\Promise\Timer\timeout($promise, self::TIMEOUT_REMOTE)); + \React\Async\await(\React\Promise\Timer\timeout($promise, self::TIMEOUT_REMOTE)); } /** @group internet */ @@ -122,7 +121,7 @@ public function testPostDataReturnsData() $request->end($data); - $buffer = Block\await(\React\Promise\Timer\timeout($deferred->promise(), self::TIMEOUT_REMOTE)); + $buffer = \React\Async\await(\React\Promise\Timer\timeout($deferred->promise(), self::TIMEOUT_REMOTE)); $this->assertNotEquals('', $buffer); @@ -154,7 +153,7 @@ public function testPostJsonReturnsData() $request->end($data); - $buffer = Block\await(\React\Promise\Timer\timeout($deferred->promise(), self::TIMEOUT_REMOTE)); + $buffer = \React\Async\await(\React\Promise\Timer\timeout($deferred->promise(), self::TIMEOUT_REMOTE)); $this->assertNotEquals('', $buffer); diff --git a/tests/FunctionalBrowserTest.php b/tests/FunctionalBrowserTest.php index a9cc7244..95092ac1 100644 --- a/tests/FunctionalBrowserTest.php +++ b/tests/FunctionalBrowserTest.php @@ -2,7 +2,6 @@ namespace React\Tests\Http; -use Clue\React\Block; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Loop; @@ -24,6 +23,9 @@ class FunctionalBrowserTest extends TestCase private $browser; private $base; + /** @var ?SocketServer */ + private $socket; + /** * @before */ @@ -88,14 +90,17 @@ public function setUpBrowserAndServer() } if ($path === '/delay/10') { - return new Promise(function ($resolve) { - Loop::addTimer(10, function () use ($resolve) { + $timer = null; + return new Promise(function ($resolve) use (&$timer) { + $timer = Loop::addTimer(10, function () use ($resolve) { $resolve(new Response( 200, array(), 'hello' )); }); + }, function () use (&$timer) { + Loop::cancelTimer($timer); }); } @@ -140,10 +145,20 @@ public function setUpBrowserAndServer() var_dump($path); }); - $socket = new SocketServer('127.0.0.1:0'); - $http->listen($socket); - $this->base = str_replace('tcp:', 'http:', $socket->getAddress()) . '/'; + $this->socket = new SocketServer('127.0.0.1:0'); + $http->listen($this->socket); + + $this->base = str_replace('tcp:', 'http:', $this->socket->getAddress()) . '/'; + } + + /** + * @after + */ + public function cleanUpSocketServer() + { + $this->socket->close(); + $this->socket = null; } /** @@ -151,7 +166,7 @@ public function setUpBrowserAndServer() */ public function testSimpleRequest() { - Block\await($this->browser->get($this->base . 'get')); + \React\Async\await($this->browser->get($this->base . 'get')); } public function testGetRequestWithRelativeAddressRejects() @@ -159,7 +174,7 @@ public function testGetRequestWithRelativeAddressRejects() $promise = $this->browser->get('delay'); $this->setExpectedException('InvalidArgumentException', 'Invalid request URL given'); - Block\await($promise); + \React\Async\await($promise); } /** @@ -167,7 +182,7 @@ public function testGetRequestWithRelativeAddressRejects() */ public function testGetRequestWithBaseAndRelativeAddressResolves() { - Block\await($this->browser->withBase($this->base)->get('get')); + \React\Async\await($this->browser->withBase($this->base)->get('get')); } /** @@ -175,7 +190,7 @@ public function testGetRequestWithBaseAndRelativeAddressResolves() */ public function testGetRequestWithBaseAndFullAddressResolves() { - Block\await($this->browser->withBase('http://example.com/')->get($this->base . 'get')); + \React\Async\await($this->browser->withBase('http://example.com/')->get($this->base . 'get')); } public function testCancelGetRequestWillRejectRequest() @@ -184,7 +199,7 @@ public function testCancelGetRequestWillRejectRequest() $promise->cancel(); $this->setExpectedException('RuntimeException'); - Block\await($promise); + \React\Async\await($promise); } public function testCancelRequestWithPromiseFollowerWillRejectRequest() @@ -195,13 +210,13 @@ public function testCancelRequestWithPromiseFollowerWillRejectRequest() $promise->cancel(); $this->setExpectedException('RuntimeException'); - Block\await($promise); + \React\Async\await($promise); } public function testRequestWithoutAuthenticationFails() { $this->setExpectedException('RuntimeException'); - Block\await($this->browser->get($this->base . 'basic-auth/user/pass')); + \React\Async\await($this->browser->get($this->base . 'basic-auth/user/pass')); } /** @@ -211,7 +226,7 @@ public function testRequestWithAuthenticationSucceeds() { $base = str_replace('://', '://user:pass@', $this->base); - Block\await($this->browser->get($base . 'basic-auth/user/pass')); + \React\Async\await($this->browser->get($base . 'basic-auth/user/pass')); } /** @@ -225,7 +240,7 @@ public function testRedirectToPageWithAuthenticationSendsAuthenticationFromLocat { $target = str_replace('://', '://user:pass@', $this->base) . 'basic-auth/user/pass'; - Block\await($this->browser->get($this->base . 'redirect-to?url=' . urlencode($target))); + \React\Async\await($this->browser->get($this->base . 'redirect-to?url=' . urlencode($target))); } /** @@ -240,7 +255,7 @@ public function testRedirectFromPageWithInvalidAuthToPageWithCorrectAuthenticati $base = str_replace('://', '://unknown:invalid@', $this->base); $target = str_replace('://', '://user:pass@', $this->base) . 'basic-auth/user/pass'; - Block\await($this->browser->get($base . 'redirect-to?url=' . urlencode($target))); + \React\Async\await($this->browser->get($base . 'redirect-to?url=' . urlencode($target))); } public function testCancelRedirectedRequestShouldReject() @@ -252,7 +267,7 @@ public function testCancelRedirectedRequestShouldReject() }); $this->setExpectedException('RuntimeException', 'Request cancelled'); - Block\await($promise); + \React\Async\await($promise); } public function testTimeoutDelayedResponseShouldReject() @@ -260,7 +275,7 @@ public function testTimeoutDelayedResponseShouldReject() $promise = $this->browser->withTimeout(0.1)->get($this->base . 'delay/10'); $this->setExpectedException('RuntimeException', 'Request timed out after 0.1 seconds'); - Block\await($promise); + \React\Async\await($promise); } public function testTimeoutDelayedResponseAfterStreamingRequestShouldReject() @@ -270,7 +285,7 @@ public function testTimeoutDelayedResponseAfterStreamingRequestShouldReject() $stream->end(); $this->setExpectedException('RuntimeException', 'Request timed out after 0.1 seconds'); - Block\await($promise); + \React\Async\await($promise); } /** @@ -278,7 +293,7 @@ public function testTimeoutDelayedResponseAfterStreamingRequestShouldReject() */ public function testTimeoutFalseShouldResolveSuccessfully() { - Block\await($this->browser->withTimeout(false)->get($this->base . 'get')); + \React\Async\await($this->browser->withTimeout(false)->get($this->base . 'get')); } /** @@ -286,7 +301,7 @@ public function testTimeoutFalseShouldResolveSuccessfully() */ public function testRedirectRequestRelative() { - Block\await($this->browser->get($this->base . 'redirect-to?url=get')); + \React\Async\await($this->browser->get($this->base . 'redirect-to?url=get')); } /** @@ -294,7 +309,7 @@ public function testRedirectRequestRelative() */ public function testRedirectRequestAbsolute() { - Block\await($this->browser->get($this->base . 'redirect-to?url=' . urlencode($this->base . 'get'))); + \React\Async\await($this->browser->get($this->base . 'redirect-to?url=' . urlencode($this->base . 'get'))); } /** @@ -304,7 +319,7 @@ public function testFollowingRedirectsFalseResolvesWithRedirectResult() { $browser = $this->browser->withFollowRedirects(false); - Block\await($browser->get($this->base . 'redirect-to?url=get')); + \React\Async\await($browser->get($this->base . 'redirect-to?url=get')); } public function testFollowRedirectsZeroRejectsOnRedirect() @@ -312,12 +327,12 @@ public function testFollowRedirectsZeroRejectsOnRedirect() $browser = $this->browser->withFollowRedirects(0); $this->setExpectedException('RuntimeException'); - Block\await($browser->get($this->base . 'redirect-to?url=get')); + \React\Async\await($browser->get($this->base . 'redirect-to?url=get')); } public function testResponseStatus204ShouldResolveWithEmptyBody() { - $response = Block\await($this->browser->get($this->base . 'status/204')); + $response = \React\Async\await($this->browser->get($this->base . 'status/204')); $this->assertFalse($response->hasHeader('Content-Length')); $body = $response->getBody(); @@ -327,7 +342,7 @@ public function testResponseStatus204ShouldResolveWithEmptyBody() public function testResponseStatus304ShouldResolveWithEmptyBodyButContentLengthResponseHeader() { - $response = Block\await($this->browser->get($this->base . 'status/304')); + $response = \React\Async\await($this->browser->get($this->base . 'status/304')); $this->assertEquals('12', $response->getHeaderLine('Content-Length')); $body = $response->getBody(); @@ -342,7 +357,7 @@ public function testGetRequestWithResponseBufferMatchedExactlyResolves() { $promise = $this->browser->withResponseBuffer(5)->get($this->base . 'get'); - Block\await($promise); + \React\Async\await($promise); } public function testGetRequestWithResponseBufferExceededRejects() @@ -354,7 +369,7 @@ public function testGetRequestWithResponseBufferExceededRejects() 'Response body size of 5 bytes exceeds maximum of 4 bytes', defined('SOCKET_EMSGSIZE') ? SOCKET_EMSGSIZE : 0 ); - Block\await($promise); + \React\Async\await($promise); } public function testGetRequestWithResponseBufferExceededDuringStreamingRejects() @@ -366,7 +381,7 @@ public function testGetRequestWithResponseBufferExceededDuringStreamingRejects() 'Response body size exceeds maximum of 4 bytes', defined('SOCKET_EMSGSIZE') ? SOCKET_EMSGSIZE : 0 ); - Block\await($promise); + \React\Async\await($promise); } /** @@ -379,7 +394,7 @@ public function testCanAccessHttps() $this->markTestSkipped('Not supported on HHVM'); } - Block\await($this->browser->get('https://www.google.com/')); + \React\Async\await($this->browser->get('https://www.google.com/')); } /** @@ -400,7 +415,7 @@ public function testVerifyPeerEnabledForBadSslRejects() $browser = new Browser($connector); $this->setExpectedException('RuntimeException'); - Block\await($browser->get('https://self-signed.badssl.com/')); + \React\Async\await($browser->get('https://self-signed.badssl.com/')); } /** @@ -421,7 +436,7 @@ public function testVerifyPeerDisabledForBadSslResolves() $browser = new Browser($connector); - Block\await($browser->get('https://self-signed.badssl.com/')); + \React\Async\await($browser->get('https://self-signed.badssl.com/')); } /** @@ -430,13 +445,13 @@ public function testVerifyPeerDisabledForBadSslResolves() public function testInvalidPort() { $this->setExpectedException('RuntimeException'); - Block\await($this->browser->get('http://www.google.com:443/')); + \React\Async\await($this->browser->get('http://www.google.com:443/')); } public function testErrorStatusCodeRejectsWithResponseException() { try { - Block\await($this->browser->get($this->base . 'status/404')); + \React\Async\await($this->browser->get($this->base . 'status/404')); $this->fail(); } catch (ResponseException $e) { $this->assertEquals(404, $e->getCode()); @@ -448,14 +463,14 @@ public function testErrorStatusCodeRejectsWithResponseException() public function testErrorStatusCodeDoesNotRejectWithRejectErrorResponseFalse() { - $response = Block\await($this->browser->withRejectErrorResponse(false)->get($this->base . 'status/404')); + $response = \React\Async\await($this->browser->withRejectErrorResponse(false)->get($this->base . 'status/404')); $this->assertEquals(404, $response->getStatusCode()); } public function testPostString() { - $response = Block\await($this->browser->post($this->base . 'post', array(), 'hello world')); + $response = \React\Async\await($this->browser->post($this->base . 'post', array(), 'hello world')); $data = json_decode((string)$response->getBody(), true); $this->assertEquals('hello world', $data['data']); @@ -463,7 +478,7 @@ public function testPostString() public function testRequestStreamReturnsResponseBodyUntilConnectionsEndsForHttp10() { - $response = Block\await($this->browser->withProtocolVersion('1.0')->get($this->base . 'stream/1')); + $response = \React\Async\await($this->browser->withProtocolVersion('1.0')->get($this->base . 'stream/1')); $this->assertEquals('1.0', $response->getProtocolVersion()); $this->assertFalse($response->hasHeader('Transfer-Encoding')); @@ -474,7 +489,7 @@ public function testRequestStreamReturnsResponseBodyUntilConnectionsEndsForHttp1 public function testRequestStreamReturnsResponseWithTransferEncodingChunkedAndResponseBodyDecodedForHttp11() { - $response = Block\await($this->browser->get($this->base . 'stream/1')); + $response = \React\Async\await($this->browser->get($this->base . 'stream/1')); $this->assertEquals('1.1', $response->getProtocolVersion()); @@ -486,7 +501,7 @@ public function testRequestStreamReturnsResponseWithTransferEncodingChunkedAndRe public function testRequestStreamWithHeadRequestReturnsEmptyResponseBodWithTransferEncodingChunkedForHttp11() { - $response = Block\await($this->browser->head($this->base . 'stream/1')); + $response = \React\Async\await($this->browser->head($this->base . 'stream/1')); $this->assertEquals('1.1', $response->getProtocolVersion()); @@ -505,7 +520,9 @@ public function testRequestStreamReturnsResponseWithResponseBodyUndecodedWhenRes $this->base = str_replace('tcp:', 'http:', $socket->getAddress()) . '/'; - $response = Block\await($this->browser->get($this->base . 'stream/1')); + $response = \React\Async\await($this->browser->get($this->base . 'stream/1')); + + $socket->close(); $this->assertEquals('1.1', $response->getProtocolVersion()); @@ -528,10 +545,10 @@ public function testReceiveStreamAndExplicitlyCloseConnectionEvenWhenServerKeeps $this->base = str_replace('tcp:', 'http:', $socket->getAddress()) . '/'; - $response = Block\await($this->browser->get($this->base . 'get', array())); + $response = \React\Async\await($this->browser->get($this->base . 'get', array())); $this->assertEquals('hello', (string)$response->getBody()); - $ret = Block\await(\React\Promise\Timer\timeout($closed->promise(), 0.1)); + $ret = \React\Async\await(\React\Promise\Timer\timeout($closed->promise(), 0.1)); $this->assertTrue($ret); $socket->close(); @@ -545,7 +562,7 @@ public function testPostStreamChunked() $stream->end('hello world'); }); - $response = Block\await($this->browser->post($this->base . 'post', array(), $stream)); + $response = \React\Async\await($this->browser->post($this->base . 'post', array(), $stream)); $data = json_decode((string)$response->getBody(), true); $this->assertEquals('hello world', $data['data']); @@ -561,7 +578,7 @@ public function testPostStreamKnownLength() $stream->end('hello world'); }); - $response = Block\await($this->browser->post($this->base . 'post', array('Content-Length' => 11), $stream)); + $response = \React\Async\await($this->browser->post($this->base . 'post', array('Content-Length' => 11), $stream)); $data = json_decode((string)$response->getBody(), true); $this->assertEquals('hello world', $data['data']); @@ -581,7 +598,7 @@ public function testPostStreamWillStartSendingRequestEvenWhenBodyDoesNotEmitData $this->base = str_replace('tcp:', 'http:', $socket->getAddress()) . '/'; $stream = new ThroughStream(); - Block\await($this->browser->post($this->base . 'post', array(), $stream)); + \React\Async\await($this->browser->post($this->base . 'post', array(), $stream)); $socket->close(); } @@ -591,7 +608,7 @@ public function testPostStreamClosed() $stream = new ThroughStream(); $stream->close(); - $response = Block\await($this->browser->post($this->base . 'post', array(), $stream)); + $response = \React\Async\await($this->browser->post($this->base . 'post', array(), $stream)); $data = json_decode((string)$response->getBody(), true); $this->assertEquals('', $data['data']); @@ -611,7 +628,7 @@ public function testSendsHttp11ByDefault() $this->base = str_replace('tcp:', 'http:', $socket->getAddress()) . '/'; - $response = Block\await($this->browser->get($this->base)); + $response = \React\Async\await($this->browser->get($this->base)); $this->assertEquals('1.1', (string)$response->getBody()); $socket->close(); @@ -631,7 +648,7 @@ public function testSendsExplicitHttp10Request() $this->base = str_replace('tcp:', 'http:', $socket->getAddress()) . '/'; - $response = Block\await($this->browser->withProtocolVersion('1.0')->get($this->base)); + $response = \React\Async\await($this->browser->withProtocolVersion('1.0')->get($this->base)); $this->assertEquals('1.0', (string)$response->getBody()); $socket->close(); @@ -639,7 +656,7 @@ public function testSendsExplicitHttp10Request() public function testHeadRequestReceivesResponseWithEmptyBodyButWithContentLengthResponseHeader() { - $response = Block\await($this->browser->head($this->base . 'get')); + $response = \React\Async\await($this->browser->head($this->base . 'get')); $this->assertEquals('5', $response->getHeaderLine('Content-Length')); $body = $response->getBody(); @@ -649,7 +666,7 @@ public function testHeadRequestReceivesResponseWithEmptyBodyButWithContentLength public function testRequestStreamingGetReceivesResponseWithStreamingBodyAndKnownSize() { - $response = Block\await($this->browser->requestStreaming('GET', $this->base . 'get')); + $response = \React\Async\await($this->browser->requestStreaming('GET', $this->base . 'get')); $this->assertEquals('5', $response->getHeaderLine('Content-Length')); $body = $response->getBody(); @@ -660,7 +677,7 @@ public function testRequestStreamingGetReceivesResponseWithStreamingBodyAndKnown public function testRequestStreamingGetReceivesResponseWithStreamingBodyAndUnknownSizeFromStreamingEndpoint() { - $response = Block\await($this->browser->requestStreaming('GET', $this->base . 'stream/1')); + $response = \React\Async\await($this->browser->requestStreaming('GET', $this->base . 'stream/1')); $this->assertFalse($response->hasHeader('Content-Length')); $body = $response->getBody(); @@ -671,7 +688,7 @@ public function testRequestStreamingGetReceivesResponseWithStreamingBodyAndUnkno public function testRequestStreamingGetReceivesStreamingResponseBody() { - $buffer = Block\await( + $buffer = \React\Async\await( $this->browser->requestStreaming('GET', $this->base . 'get')->then(function (ResponseInterface $response) { return Stream\buffer($response->getBody()); }) @@ -682,7 +699,7 @@ public function testRequestStreamingGetReceivesStreamingResponseBody() public function testRequestStreamingGetReceivesStreamingResponseBodyEvenWhenResponseBufferExceeded() { - $buffer = Block\await( + $buffer = \React\Async\await( $this->browser->withResponseBuffer(4)->requestStreaming('GET', $this->base . 'get')->then(function (ResponseInterface $response) { return Stream\buffer($response->getBody()); }) diff --git a/tests/FunctionalHttpServerTest.php b/tests/FunctionalHttpServerTest.php index f543fb55..eb3b448c 100644 --- a/tests/FunctionalHttpServerTest.php +++ b/tests/FunctionalHttpServerTest.php @@ -2,7 +2,6 @@ namespace React\Tests\Http; -use Clue\React\Block; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Loop; @@ -37,7 +36,7 @@ public function testPlainHttpOnRandomPort() return Stream\buffer($conn); }); - $response = Block\await(\React\Promise\Timer\timeout($result, 1.0)); + $response = \React\Async\await(\React\Promise\Timer\timeout($result, 1.0)); $this->assertContainsString("HTTP/1.0 200 OK", $response); $this->assertContainsString('http://' . noScheme($socket->getAddress()) . '/', $response); @@ -64,7 +63,7 @@ function () { return Stream\buffer($conn); }); - $response = Block\await(\React\Promise\Timer\timeout($result, 1.0)); + $response = \React\Async\await(\React\Promise\Timer\timeout($result, 1.0)); $this->assertContainsString("HTTP/1.0 404 Not Found", $response); @@ -88,7 +87,7 @@ public function testPlainHttpOnRandomPortWithoutHostHeaderUsesSocketUri() return Stream\buffer($conn); }); - $response = Block\await(\React\Promise\Timer\timeout($result, 1.0)); + $response = \React\Async\await(\React\Promise\Timer\timeout($result, 1.0)); $this->assertContainsString("HTTP/1.0 200 OK", $response); $this->assertContainsString('http://' . noScheme($socket->getAddress()) . '/', $response); @@ -113,7 +112,7 @@ public function testPlainHttpOnRandomPortWithOtherHostHeaderTakesPrecedence() return Stream\buffer($conn); }); - $response = Block\await(\React\Promise\Timer\timeout($result, 1.0)); + $response = \React\Async\await(\React\Promise\Timer\timeout($result, 1.0)); $this->assertContainsString("HTTP/1.0 200 OK", $response); $this->assertContainsString('http://localhost:1000/', $response); @@ -146,7 +145,7 @@ public function testSecureHttpsOnRandomPort() return Stream\buffer($conn); }); - $response = Block\await(\React\Promise\Timer\timeout($result, 1.0)); + $response = \React\Async\await(\React\Promise\Timer\timeout($result, 1.0)); $this->assertContainsString("HTTP/1.0 200 OK", $response); $this->assertContainsString('https://' . noScheme($socket->getAddress()) . '/', $response); @@ -183,7 +182,7 @@ public function testSecureHttpsReturnsData() return Stream\buffer($conn); }); - $response = Block\await(\React\Promise\Timer\timeout($result, 1.0)); + $response = \React\Async\await(\React\Promise\Timer\timeout($result, 1.0)); $this->assertContainsString("HTTP/1.0 200 OK", $response); $this->assertContainsString("\r\nContent-Length: 33000\r\n", $response); @@ -217,7 +216,7 @@ public function testSecureHttpsOnRandomPortWithoutHostHeaderUsesSocketUri() return Stream\buffer($conn); }); - $response = Block\await(\React\Promise\Timer\timeout($result, 1.0)); + $response = \React\Async\await(\React\Promise\Timer\timeout($result, 1.0)); $this->assertContainsString("HTTP/1.0 200 OK", $response); $this->assertContainsString('https://' . noScheme($socket->getAddress()) . '/', $response); @@ -246,7 +245,7 @@ public function testPlainHttpOnStandardPortReturnsUriWithNoPort() return Stream\buffer($conn); }); - $response = Block\await(\React\Promise\Timer\timeout($result, 1.0)); + $response = \React\Async\await(\React\Promise\Timer\timeout($result, 1.0)); $this->assertContainsString("HTTP/1.0 200 OK", $response); $this->assertContainsString('http://127.0.0.1/', $response); @@ -275,7 +274,7 @@ public function testPlainHttpOnStandardPortWithoutHostHeaderReturnsUriWithNoPort return Stream\buffer($conn); }); - $response = Block\await(\React\Promise\Timer\timeout($result, 1.0)); + $response = \React\Async\await(\React\Promise\Timer\timeout($result, 1.0)); $this->assertContainsString("HTTP/1.0 200 OK", $response); $this->assertContainsString('http://127.0.0.1/', $response); @@ -313,7 +312,7 @@ public function testSecureHttpsOnStandardPortReturnsUriWithNoPort() return Stream\buffer($conn); }); - $response = Block\await(\React\Promise\Timer\timeout($result, 1.0)); + $response = \React\Async\await(\React\Promise\Timer\timeout($result, 1.0)); $this->assertContainsString("HTTP/1.0 200 OK", $response); $this->assertContainsString('https://127.0.0.1/', $response); @@ -351,7 +350,7 @@ public function testSecureHttpsOnStandardPortWithoutHostHeaderUsesSocketUri() return Stream\buffer($conn); }); - $response = Block\await(\React\Promise\Timer\timeout($result, 1.0)); + $response = \React\Async\await(\React\Promise\Timer\timeout($result, 1.0)); $this->assertContainsString("HTTP/1.0 200 OK", $response); $this->assertContainsString('https://127.0.0.1/', $response); @@ -380,7 +379,7 @@ public function testPlainHttpOnHttpsStandardPortReturnsUriWithPort() return Stream\buffer($conn); }); - $response = Block\await(\React\Promise\Timer\timeout($result, 1.0)); + $response = \React\Async\await(\React\Promise\Timer\timeout($result, 1.0)); $this->assertContainsString("HTTP/1.0 200 OK", $response); $this->assertContainsString('http://127.0.0.1:443/', $response); @@ -418,7 +417,7 @@ public function testSecureHttpsOnHttpStandardPortReturnsUriWithPort() return Stream\buffer($conn); }); - $response = Block\await(\React\Promise\Timer\timeout($result, 1.0)); + $response = \React\Async\await(\React\Promise\Timer\timeout($result, 1.0)); $this->assertContainsString("HTTP/1.0 200 OK", $response); $this->assertContainsString('https://127.0.0.1:80/', $response); @@ -446,7 +445,7 @@ public function testClosedStreamFromRequestHandlerWillSendEmptyBody() return Stream\buffer($conn); }); - $response = Block\await(\React\Promise\Timer\timeout($result, 1.0)); + $response = \React\Async\await(\React\Promise\Timer\timeout($result, 1.0)); $this->assertStringStartsWith("HTTP/1.0 200 OK", $response); $this->assertStringEndsWith("\r\n\r\n", $response); @@ -477,7 +476,7 @@ function (RequestInterface $request) use ($once) { }); }); - \Clue\React\Block\await(\React\Promise\Timer\sleep(0.1)); + \React\Async\await(\React\Promise\Timer\sleep(0.1)); $socket->close(); } @@ -507,7 +506,7 @@ function (RequestInterface $request) use ($stream) { }); // stream will be closed within 0.1s - $ret = Block\await(\React\Promise\Timer\timeout(Stream\first($stream, 'close'), 0.1)); + $ret = \React\Async\await(\React\Promise\Timer\timeout(Stream\first($stream, 'close'), 0.1)); $socket->close(); @@ -536,7 +535,7 @@ public function testStreamFromRequestHandlerWillBeClosedIfConnectionCloses() }); // await response stream to be closed - $ret = Block\await(\React\Promise\Timer\timeout(Stream\first($stream, 'close'), 1.0)); + $ret = \React\Async\await(\React\Promise\Timer\timeout(Stream\first($stream, 'close'), 1.0)); $socket->close(); @@ -571,7 +570,7 @@ public function testUpgradeWithThroughStreamReturnsDataAsGiven() return Stream\buffer($conn); }); - $response = Block\await(\React\Promise\Timer\timeout($result, 1.0)); + $response = \React\Async\await(\React\Promise\Timer\timeout($result, 1.0)); $this->assertStringStartsWith("HTTP/1.1 101 Switching Protocols\r\n", $response); $this->assertStringEndsWith("\r\n\r\nhelloworld", $response); @@ -608,7 +607,7 @@ public function testUpgradeWithRequestBodyAndThroughStreamReturnsDataAsGiven() return Stream\buffer($conn); }); - $response = Block\await(\React\Promise\Timer\timeout($result, 1.0)); + $response = \React\Async\await(\React\Promise\Timer\timeout($result, 1.0)); $this->assertStringStartsWith("HTTP/1.1 101 Switching Protocols\r\n", $response); $this->assertStringEndsWith("\r\n\r\nhelloworld", $response); @@ -644,7 +643,7 @@ public function testConnectWithThroughStreamReturnsDataAsGiven() return Stream\buffer($conn); }); - $response = Block\await(\React\Promise\Timer\timeout($result, 1.0)); + $response = \React\Async\await(\React\Promise\Timer\timeout($result, 1.0)); $this->assertStringStartsWith("HTTP/1.1 200 OK\r\n", $response); $this->assertStringEndsWith("\r\n\r\nhelloworld", $response); @@ -684,7 +683,7 @@ public function testConnectWithThroughStreamReturnedFromPromiseReturnsDataAsGive return Stream\buffer($conn); }); - $response = Block\await(\React\Promise\Timer\timeout($result, 1.0)); + $response = \React\Async\await(\React\Promise\Timer\timeout($result, 1.0)); $this->assertStringStartsWith("HTTP/1.1 200 OK\r\n", $response); $this->assertStringEndsWith("\r\n\r\nhelloworld", $response); @@ -717,7 +716,7 @@ public function testConnectWithClosedThroughStreamReturnsNoData() return Stream\buffer($conn); }); - $response = Block\await(\React\Promise\Timer\timeout($result, 1.0)); + $response = \React\Async\await(\React\Promise\Timer\timeout($result, 1.0)); $this->assertStringStartsWith("HTTP/1.1 200 OK\r\n", $response); $this->assertStringEndsWith("\r\n\r\n", $response); @@ -760,7 +759,7 @@ function (ServerRequestInterface $request) { }); } - $responses = Block\await(\React\Promise\Timer\timeout(Promise\all($result), 1.0)); + $responses = \React\Async\await(\React\Promise\Timer\timeout(Promise\all($result), 1.0)); foreach ($responses as $response) { $this->assertContainsString("HTTP/1.0 200 OK", $response, $response); diff --git a/tests/HttpServerTest.php b/tests/HttpServerTest.php index 31bc32ee..72d48468 100644 --- a/tests/HttpServerTest.php +++ b/tests/HttpServerTest.php @@ -2,7 +2,6 @@ namespace React\Tests\Http; -use Clue\React\Block; use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Loop; use React\Http\HttpServer; @@ -142,7 +141,7 @@ public function testPostFormData() $this->socket->emit('connection', array($this->connection)); $this->connection->emit('data', array("POST / HTTP/1.0\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: 7\r\n\r\nfoo=bar")); - $request = Block\await($deferred->promise()); + $request = \React\Async\await($deferred->promise()); assert($request instanceof ServerRequestInterface); $form = $request->getParsedBody(); @@ -180,7 +179,7 @@ public function testPostFileUpload() } }); - $request = Block\await($deferred->promise()); + $request = \React\Async\await($deferred->promise()); assert($request instanceof ServerRequestInterface); $this->assertEmpty($request->getParsedBody()); @@ -213,7 +212,7 @@ public function testPostJsonWillNotBeParsedByDefault() $this->socket->emit('connection', array($this->connection)); $this->connection->emit('data', array("POST / HTTP/1.0\r\nContent-Type: application/json\r\nContent-Length: 6\r\n\r\n[true]")); - $request = Block\await($deferred->promise()); + $request = \React\Async\await($deferred->promise()); assert($request instanceof ServerRequestInterface); $this->assertNull($request->getParsedBody()); diff --git a/tests/Io/MiddlewareRunnerTest.php b/tests/Io/MiddlewareRunnerTest.php index d8f5f232..1f49facd 100644 --- a/tests/Io/MiddlewareRunnerTest.php +++ b/tests/Io/MiddlewareRunnerTest.php @@ -2,7 +2,6 @@ namespace React\Tests\Http\Io; -use Clue\React\Block; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; @@ -161,7 +160,7 @@ public function testProcessStack(array $middlewares, $expectedCallCount) $response = $middlewareStack($request); $this->assertTrue($response instanceof PromiseInterface); - $response = Block\await($response); + $response = \React\Async\await($response); $this->assertTrue($response instanceof ResponseInterface); $this->assertSame(200, $response->getStatusCode()); @@ -228,7 +227,7 @@ function () use ($errorHandler, &$called, $response, $exception) { $request = new ServerRequest('GET', 'https://example.com/'); - $this->assertSame($response, Block\await($runner($request))); + $this->assertSame($response, \React\Async\await($runner($request))); $this->assertSame(1, $retryCalled); $this->assertSame(2, $called); $this->assertSame($exception, $error); diff --git a/tests/Io/SenderTest.php b/tests/Io/SenderTest.php index 8597f17a..587ba0c2 100644 --- a/tests/Io/SenderTest.php +++ b/tests/Io/SenderTest.php @@ -2,7 +2,6 @@ namespace React\Tests\Http\Io; -use Clue\React\Block; use React\Http\Client\Client as HttpClient; use React\Http\Client\RequestData; use React\Http\Io\ReadableBodyStream; diff --git a/tests/Io/TransactionTest.php b/tests/Io/TransactionTest.php index 56b9d4f4..83d218c7 100644 --- a/tests/Io/TransactionTest.php +++ b/tests/Io/TransactionTest.php @@ -2,7 +2,6 @@ namespace React\Tests\Http\Io; -use Clue\React\Block; use PHPUnit\Framework\MockObject\MockObject; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; @@ -401,7 +400,7 @@ public function testReceivingStreamingBodyWillResolveWithBufferedResponseByDefau $transaction = new Transaction($sender, Loop::get()); $promise = $transaction->send($request); - $response = Block\await($promise); + $response = \React\Async\await($promise); $this->assertEquals(200, $response->getStatusCode()); $this->assertEquals('hello world', (string)$response->getBody()); @@ -424,7 +423,7 @@ public function testReceivingStreamingBodyWithSizeExceedingMaximumResponseBuffer $promise = $transaction->send($request); $this->setExpectedException('OverflowException'); - Block\await(\React\Promise\Timer\timeout($promise, 0.001)); + \React\Async\await(\React\Promise\Timer\timeout($promise, 0.001)); } public function testCancelBufferingResponseWillCloseStreamAndReject() @@ -445,7 +444,7 @@ public function testCancelBufferingResponseWillCloseStreamAndReject() $promise->cancel(); $this->setExpectedException('RuntimeException'); - Block\await(\React\Promise\Timer\timeout($promise, 0.001)); + \React\Async\await(\React\Promise\Timer\timeout($promise, 0.001)); } public function testReceivingStreamingBodyWillResolveWithStreamingResponseIfStreamingIsEnabled() diff --git a/tests/Middleware/RequestBodyBufferMiddlewareTest.php b/tests/Middleware/RequestBodyBufferMiddlewareTest.php index e073e1f0..0edec7da 100644 --- a/tests/Middleware/RequestBodyBufferMiddlewareTest.php +++ b/tests/Middleware/RequestBodyBufferMiddlewareTest.php @@ -2,7 +2,6 @@ namespace React\Tests\Http\Middleware; -use Clue\React\Block; use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Loop; use React\Http\Io\HttpBodyStream; @@ -128,7 +127,7 @@ public function testKnownExcessiveSizedBodyIsDisgardedTheRequestIsPassedDownToTh ); $buffer = new RequestBodyBufferMiddleware(1); - $response = Block\await($buffer( + $response = \React\Async\await($buffer( $serverRequest, function (ServerRequestInterface $request) { return new Response(200, array(), $request->getBody()->getContents()); @@ -153,7 +152,7 @@ public function testKnownExcessiveSizedWithIniLikeSize() ); $buffer = new RequestBodyBufferMiddleware('1K'); - $response = Block\await($buffer( + $response = \React\Async\await($buffer( $serverRequest, function (ServerRequestInterface $request) { return new Response(200, array(), $request->getBody()->getContents()); @@ -206,7 +205,7 @@ function (ServerRequestInterface $request) { $stream->end('aa'); - $exposedResponse = Block\await($promise->then( + $exposedResponse = \React\Async\await($promise->then( null, $this->expectCallableNever() )); @@ -236,7 +235,7 @@ function (ServerRequestInterface $request) { $stream->emit('error', array(new \RuntimeException())); $this->setExpectedException('RuntimeException'); - Block\await($promise); + \React\Async\await($promise); } public function testFullBodyStreamedBeforeCallingNextMiddleware() From 663c9a3b77b71463fa7fcb76a6676ffd16979dd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 16 Aug 2022 18:30:57 +0200 Subject: [PATCH 403/456] Do not decode cookie names anymore --- README.md | 4 ++-- examples/55-server-cookie-handling.php | 4 ++-- src/Message/ServerRequest.php | 2 +- tests/Message/ServerRequestTest.php | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index ba8d8330..1070349c 100644 --- a/README.md +++ b/README.md @@ -1304,7 +1304,7 @@ get all cookies sent with the current request. ```php $http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { - $key = 'react\php'; + $key = 'greeting'; if (isset($request->getCookieParams()[$key])) { $body = "Your cookie value is: " . $request->getCookieParams()[$key] . "\n"; @@ -1316,7 +1316,7 @@ $http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterf return React\Http\Message\Response::plaintext( "Your cookie has been set.\n" - )->withHeader('Set-Cookie', urlencode($key) . '=' . urlencode('test;more')); + )->withHeader('Set-Cookie', $key . '=' . urlencode('Hello world!')); }); ``` diff --git a/examples/55-server-cookie-handling.php b/examples/55-server-cookie-handling.php index 796da24d..b5e68862 100644 --- a/examples/55-server-cookie-handling.php +++ b/examples/55-server-cookie-handling.php @@ -3,7 +3,7 @@ require __DIR__ . '/../vendor/autoload.php'; $http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { - $key = 'react\php'; + $key = 'greeting'; if (isset($request->getCookieParams()[$key])) { $body = "Your cookie value is: " . $request->getCookieParams()[$key] . "\n"; @@ -15,7 +15,7 @@ return React\Http\Message\Response::plaintext( "Your cookie has been set.\n" - )->withHeader('Set-Cookie', urlencode($key) . '=' . urlencode('test;more')); + )->withHeader('Set-Cookie', $key . '=' . urlencode('Hello world!')); }); $socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); diff --git a/src/Message/ServerRequest.php b/src/Message/ServerRequest.php index f446f24e..fdb3ec5e 100644 --- a/src/Message/ServerRequest.php +++ b/src/Message/ServerRequest.php @@ -186,7 +186,7 @@ private function parseCookie($cookie) $nameValuePair = \explode('=', $pair, 2); if (\count($nameValuePair) === 2) { - $key = \urldecode($nameValuePair[0]); + $key = $nameValuePair[0]; $value = \urldecode($nameValuePair[1]); $result[$key] = $value; } diff --git a/tests/Message/ServerRequestTest.php b/tests/Message/ServerRequestTest.php index 37cc1879..a5919f64 100644 --- a/tests/Message/ServerRequestTest.php +++ b/tests/Message/ServerRequestTest.php @@ -251,7 +251,7 @@ public function testUrlEncodingForKeyWillReturnValidArray() ); $cookies = $this->request->getCookieParams(); - $this->assertEquals(array('react;php' => 'is great'), $cookies); + $this->assertEquals(array('react%3Bphp' => 'is great'), $cookies); } public function testCookieWithoutSpaceAfterSeparatorWillBeAccepted() From d92e564a80e349661abba0d9d80a9a82bf120d49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 16 Jun 2022 22:16:25 +0200 Subject: [PATCH 404/456] Improve performance, reuse server params for same connection --- src/Io/RequestHeaderParser.php | 84 +++++++++++++++++----------- tests/Io/RequestHeaderParserTest.php | 58 ++++++++++++++++++- 2 files changed, 108 insertions(+), 34 deletions(-) diff --git a/src/Io/RequestHeaderParser.php b/src/Io/RequestHeaderParser.php index 6930afaf..b8336f5b 100644 --- a/src/Io/RequestHeaderParser.php +++ b/src/Io/RequestHeaderParser.php @@ -27,6 +27,9 @@ class RequestHeaderParser extends EventEmitter /** @var Clock */ private $clock; + /** @var array> */ + private $connectionParams = array(); + public function __construct(Clock $clock) { $this->clock = $clock; @@ -66,8 +69,7 @@ public function handle(ConnectionInterface $conn) try { $request = $that->parseRequest( (string)\substr($buffer, 0, $endOfHeader + 2), - $conn->getRemoteAddress(), - $conn->getLocalAddress() + $conn ); } catch (Exception $exception) { $buffer = ''; @@ -119,13 +121,12 @@ public function handle(ConnectionInterface $conn) /** * @param string $headers buffer string containing request headers only - * @param ?string $remoteSocketUri - * @param ?string $localSocketUri + * @param ConnectionInterface $connection * @return ServerRequestInterface * @throws \InvalidArgumentException * @internal */ - public function parseRequest($headers, $remoteSocketUri, $localSocketUri) + public function parseRequest($headers, ConnectionInterface $connection) { // additional, stricter safe-guard for request line // because request parser doesn't properly cope with invalid ones @@ -160,26 +161,59 @@ public function parseRequest($headers, $remoteSocketUri, $localSocketUri) } } + // 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])) { + $serverParams = $this->connectionParams[$cid]; + } else { + // assign new server params for new connection + $serverParams = array(); + + // scheme is `http` unless TLS is used + $localSocketUri = $connection->getLocalAddress(); + $localParts = $localSocketUri === null ? array() : \parse_url($localSocketUri); + if (isset($localParts['scheme']) && $localParts['scheme'] === 'tls') { + $serverParams['HTTPS'] = 'on'; + } + + // 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. + if ($localSocketUri !== null && isset($localParts['host'], $localParts['port'])) { + $serverParams['SERVER_ADDR'] = $localParts['host']; + $serverParams['SERVER_PORT'] = $localParts['port']; + } + + // apply REMOTE_ADDR and REMOTE_PORT if source address is known + // address should always be known, unless this is over Unix domain sockets (UDS) + $remoteSocketUri = $connection->getRemoteAddress(); + if ($remoteSocketUri !== null) { + $remoteAddress = \parse_url($remoteSocketUri); + $serverParams['REMOTE_ADDR'] = $remoteAddress['host']; + $serverParams['REMOTE_PORT'] = $remoteAddress['port']; + } + + // remember server params for all requests from this connection, reset on connection close + $this->connectionParams[$cid] = $serverParams; + $params =& $this->connectionParams; + $connection->on('close', function () use (&$params, $cid) { + assert(\is_array($params)); + unset($params[$cid]); + }); + } + // create new obj implementing ServerRequestInterface by preserving all // previous properties and restoring original request-target - $serverParams = array( - 'REQUEST_TIME' => (int) ($now = $this->clock->now()), - 'REQUEST_TIME_FLOAT' => $now - ); + $serverParams['REQUEST_TIME'] = (int) ($now = $this->clock->now()); + $serverParams['REQUEST_TIME_FLOAT'] = $now; // scheme is `http` unless TLS is used - $localParts = $localSocketUri === null ? array() : \parse_url($localSocketUri); - if (isset($localParts['scheme']) && $localParts['scheme'] === 'tls') { - $scheme = 'https://'; - $serverParams['HTTPS'] = 'on'; - } else { - $scheme = 'http://'; - } + $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($localParts['host'], $localParts['port']) ? $localParts['host'] . ':' . $localParts['port'] : '127.0.0.1'; + $host = isset($serverParams['SERVER_ADDR'], $serverParams['SERVER_PORT']) ? $serverParams['SERVER_ADDR'] . ':' . $serverParams['SERVER_PORT'] : '127.0.0.1'; } if ($start['method'] === 'OPTIONS' && $start['target'] === '*') { @@ -210,22 +244,6 @@ public function parseRequest($headers, $remoteSocketUri, $localSocketUri) } } - // apply REMOTE_ADDR and REMOTE_PORT if source address is known - // address should always be known, unless this is over Unix domain sockets (UDS) - if ($remoteSocketUri !== null) { - $remoteAddress = \parse_url($remoteSocketUri); - $serverParams['REMOTE_ADDR'] = $remoteAddress['host']; - $serverParams['REMOTE_PORT'] = $remoteAddress['port']; - } - - // 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. - if ($localSocketUri !== null && isset($localParts['host'], $localParts['port'])) { - $serverParams['SERVER_ADDR'] = $localParts['host']; - $serverParams['SERVER_PORT'] = $localParts['port']; - } - $request = new ServerRequest( $start['method'], $uri, diff --git a/tests/Io/RequestHeaderParserTest.php b/tests/Io/RequestHeaderParserTest.php index 7ba7fe01..87d6bf1b 100644 --- a/tests/Io/RequestHeaderParserTest.php +++ b/tests/Io/RequestHeaderParserTest.php @@ -2,9 +2,9 @@ namespace React\Tests\Http\Io; +use Psr\Http\Message\ServerRequestInterface; use React\Http\Io\RequestHeaderParser; use React\Tests\Http\TestCase; -use Psr\Http\Message\ServerRequestInterface; class RequestHeaderParserTest extends TestCase { @@ -808,6 +808,62 @@ public function testServerParamsWontBeSetOnMissingUrls() $this->assertArrayNotHasKey('REMOTE_PORT', $serverParams); } + public function testServerParamsWillBeReusedForMultipleRequestsFromSameConnection() + { + $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + $clock->expects($this->exactly(2))->method('now')->willReturn(1652972091.3958); + + $parser = new RequestHeaderParser($clock); + + $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('getLocalAddress', 'getRemoteAddress'))->getMock(); + $connection->expects($this->once())->method('getLocalAddress')->willReturn('tcp://127.1.1.1:8000'); + $connection->expects($this->once())->method('getRemoteAddress')->willReturn('tcp://192.168.1.1:8001'); + + $parser->handle($connection); + $connection->emit('data', array("GET /foo HTTP/1.0\r\nHost: example.com\r\n\r\n")); + + $request = null; + $parser->on('headers', function ($parsedRequest) use (&$request) { + $request = $parsedRequest; + }); + + $parser->handle($connection); + $connection->emit('data', array("GET /foo HTTP/1.0\r\nHost: example.com\r\n\r\n")); + + assert($request instanceof ServerRequestInterface); + $serverParams = $request->getServerParams(); + + $this->assertArrayNotHasKey('HTTPS', $serverParams); + $this->assertEquals(1652972091, $serverParams['REQUEST_TIME']); + $this->assertEquals(1652972091.3958, $serverParams['REQUEST_TIME_FLOAT']); + + $this->assertEquals('127.1.1.1', $serverParams['SERVER_ADDR']); + $this->assertEquals('8000', $serverParams['SERVER_PORT']); + + $this->assertEquals('192.168.1.1', $serverParams['REMOTE_ADDR']); + $this->assertEquals('8001', $serverParams['REMOTE_PORT']); + } + + public function testServerParamsWillBeRememberedUntilConnectionIsClosed() + { + $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + + $parser = new RequestHeaderParser($clock); + + $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('getLocalAddress', 'getRemoteAddress'))->getMock(); + + $parser->handle($connection); + $connection->emit('data', array("GET /foo HTTP/1.0\r\nHost: example.com\r\n\r\n")); + + $ref = new \ReflectionProperty($parser, 'connectionParams'); + $ref->setAccessible(true); + + $this->assertCount(1, $ref->getValue($parser)); + + $connection->emit('close'); + $this->assertEquals(array(), $ref->getValue($parser)); + } + public function testQueryParmetersWillBeSet() { $request = null; From f0b4859d9f1728e6df3877b40956f0e3afef2d34 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Sun, 14 Aug 2022 00:29:58 +0200 Subject: [PATCH 405/456] Test on PHP 8.2 With PHP 8.2 coming out later this year, we should be reading for it's release to ensure all out code works on it. Refs: https://github.com/reactphp/event-loop/pull/258 --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 27577838..0724232c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,6 +11,7 @@ jobs: strategy: matrix: php: + - 8.2 - 8.1 - 8.0 - 7.4 From 4a1e85382e8c2a9e0fdb8ac04e94585da2083bfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 23 Aug 2022 14:31:28 +0200 Subject: [PATCH 406/456] Prepare v1.7.0 release --- CHANGELOG.md | 26 +++++++++++++++++++++++++- README.md | 2 +- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41079cdb..5bf17e9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,30 @@ # Changelog +## 1.7.0 (2022-08-23) + +This is a **SECURITY** and feature release for the 1.x series of ReactPHP's HTTP component. + +* Security fix: This release fixes a medium severity security issue in ReactPHP's HTTP server component + that affects all versions between `v0.7.0` and `v1.6.0`. All users are encouraged to upgrade immediately. + Special thanks to Marco Squarcina (TU Wien) for reporting this and working with us to coordinate this release. + (CVE-2022-36032 reported by @lavish and fixed by @clue) + +* Feature: Improve HTTP server performance by ~20%, reuse syscall values for clock time and socket addresses. + (#457 and #467 by @clue) + +* Feature: Full PHP 8.2+ compatibility, refactor internal `Transaction` to avoid assigning dynamic properties. + (#459 by @clue and #466 by @WyriHaximus) + +* Feature / Fix: Allow explicit `Content-Length` response header on `HEAD` requests. + (#444 by @mrsimonbennett) + +* Minor documentation improvements. + (#452 by @clue, #458 by @nhedger, #448 by @jorrit and #446 by @SimonFrings + +* Improve test suite, update to use new reactphp/async package instead of clue/reactphp-block, + skip memory tests when lowering memory limit fails and fix legacy HHVM build. + (#464 and #440 by @clue and #450 by @SimonFrings) + ## 1.6.0 (2022-02-03) * Feature: Add factory methods for common HTML/JSON/plaintext/XML response types. @@ -10,7 +35,6 @@ $response = React\Http\Response\json(['message' => 'Hello wörld!']); $response = React\Http\Response\plaintext("Hello wörld!\n"); $response = React\Http\Response\xml("Hello wörld!\n"); - $response = React\Http\Response\redirect('https://reactphp.org/'); ``` * Feature: Expose all status code constants via `Response` class. diff --git a/README.md b/README.md index 1070349c..659855b0 100644 --- a/README.md +++ b/README.md @@ -2924,7 +2924,7 @@ This project follows [SemVer](https://semver.org/). This will install the latest supported version: ```bash -composer require react/http:^1.6 +composer require react/http:^1.7 ``` See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. From 8ec53f525b7fcfe66cc14007415346458d63f1f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 12 Jun 2022 18:56:25 +0200 Subject: [PATCH 407/456] Forward compatibility with upcoming Promise v3 --- .github/workflows/ci.yml | 3 ++ composer.json | 28 ++++++++++++++----- src/Io/StreamingServer.php | 3 +- .../LimitConcurrentRequestsMiddleware.php | 2 +- tests/FunctionalHttpServerTest.php | 4 +++ tests/Io/MiddlewareRunnerTest.php | 3 +- tests/Io/TransactionTest.php | 14 +++++++--- .../LimitConcurrentRequestsMiddlewareTest.php | 8 +++--- 8 files changed, 45 insertions(+), 20 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0724232c..fafa0ff0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,6 +29,8 @@ jobs: with: php-version: ${{ matrix.php }} coverage: xdebug + env: + COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: composer install - run: vendor/bin/phpunit --coverage-text if: ${{ matrix.php >= 7.3 }} @@ -39,6 +41,7 @@ jobs: name: PHPUnit (HHVM) runs-on: ubuntu-18.04 continue-on-error: true + if: false # temporarily skipped until https://github.com/azjezz/setup-hhvm/issues/3 is addressed steps: - uses: actions/checkout@v2 - uses: azjezz/setup-hhvm@v1 diff --git a/composer.json b/composer.json index 57adfc2c..3ba8a606 100644 --- a/composer.json +++ b/composer.json @@ -31,16 +31,16 @@ "fig/http-message-util": "^1.1", "psr/http-message": "^1.0", "react/event-loop": "^1.2", - "react/promise": "^2.3 || ^1.2.1", - "react/promise-stream": "^1.1", - "react/socket": "^1.9", + "react/promise": "^3@dev || ^2.3 || ^1.2.1", + "react/promise-stream": "^1.4", + "react/socket": "^1.12", "react/stream": "^1.2", "ringcentral/psr7": "^1.2" }, "require-dev": { - "clue/http-proxy-react": "^1.7", - "clue/reactphp-ssh-proxy": "^1.3", - "clue/socks-react": "^1.3", + "clue/http-proxy-react": "dev-promise-v3 as 1.8.0", + "clue/reactphp-ssh-proxy": "dev-promise-v3 as 1.4.0", + "clue/socks-react": "dev-promise-v3 as 1.4.0", "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35", "react/async": "^4 || ^3 || ^2", "react/promise-timer": "^1.9" @@ -50,5 +50,19 @@ }, "autoload-dev": { "psr-4": { "React\\Tests\\Http\\": "tests" } - } + }, + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/clue-labs/reactphp-http-proxy" + }, + { + "type": "vcs", + "url": "https://github.com/clue-labs/reactphp-socks" + }, + { + "type": "vcs", + "url": "https://github.com/clue-labs/reactphp-ssh-proxy" + } + ] } diff --git a/src/Io/StreamingServer.php b/src/Io/StreamingServer.php index a054be3d..13f0b0c4 100644 --- a/src/Io/StreamingServer.php +++ b/src/Io/StreamingServer.php @@ -9,7 +9,6 @@ use React\Http\Message\Response; use React\Http\Message\ServerRequest; use React\Promise; -use React\Promise\CancellablePromiseInterface; use React\Promise\PromiseInterface; use React\Socket\ConnectionInterface; use React\Socket\ServerInterface; @@ -158,7 +157,7 @@ public function handleRequest(ConnectionInterface $conn, ServerRequestInterface } // cancel pending promise once connection closes - if ($response instanceof CancellablePromiseInterface) { + if ($response instanceof PromiseInterface && \method_exists($response, 'cancel')) { $conn->on('close', function () use ($response) { $response->cancel(); }); diff --git a/src/Middleware/LimitConcurrentRequestsMiddleware.php b/src/Middleware/LimitConcurrentRequestsMiddleware.php index 53338100..b1c00da0 100644 --- a/src/Middleware/LimitConcurrentRequestsMiddleware.php +++ b/src/Middleware/LimitConcurrentRequestsMiddleware.php @@ -206,6 +206,6 @@ public function processQueue() $first = \reset($this->queue); unset($this->queue[key($this->queue)]); - $first->resolve(); + $first->resolve(null); } } diff --git a/tests/FunctionalHttpServerTest.php b/tests/FunctionalHttpServerTest.php index eb3b448c..dcd79b3e 100644 --- a/tests/FunctionalHttpServerTest.php +++ b/tests/FunctionalHttpServerTest.php @@ -726,6 +726,10 @@ public function testConnectWithClosedThroughStreamReturnsNoData() public function testLimitConcurrentRequestsMiddlewareRequestStreamPausing() { + if (defined('HHVM_VERSION') && !interface_exists('React\Promise\PromisorInterface')) { + $this->markTestSkipped('Not supported on legacy HHVM with Promise v3'); + } + $connector = new Connector(); $http = new HttpServer( diff --git a/tests/Io/MiddlewareRunnerTest.php b/tests/Io/MiddlewareRunnerTest.php index 1f49facd..ac836f03 100644 --- a/tests/Io/MiddlewareRunnerTest.php +++ b/tests/Io/MiddlewareRunnerTest.php @@ -8,7 +8,6 @@ use React\Http\Io\MiddlewareRunner; use React\Http\Message\ServerRequest; use React\Promise; -use React\Promise\CancellablePromiseInterface; use React\Promise\PromiseInterface; use React\Tests\Http\Middleware\ProcessStack; use React\Tests\Http\TestCase; @@ -479,7 +478,7 @@ function (RequestInterface $request) use ($once) { $promise = $middleware($request); - $this->assertTrue($promise instanceof CancellablePromiseInterface); + $this->assertTrue($promise instanceof PromiseInterface && \method_exists($promise, 'cancel')); $promise->cancel(); } } diff --git a/tests/Io/TransactionTest.php b/tests/Io/TransactionTest.php index 83d218c7..d9ac2178 100644 --- a/tests/Io/TransactionTest.php +++ b/tests/Io/TransactionTest.php @@ -436,11 +436,14 @@ public function testCancelBufferingResponseWillCloseStreamAndReject() $response = new Response(200, array(), new ReadableBodyStream($stream)); // mock sender to resolve promise with the given $response in response to the given $request + $deferred = new Deferred(); $sender = $this->makeSenderMock(); - $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(Promise\resolve($response)); + $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn($deferred->promise()); $transaction = new Transaction($sender, Loop::get()); $promise = $transaction->send($request); + + $deferred->resolve($response); $promise->cancel(); $this->setExpectedException('RuntimeException'); @@ -778,13 +781,16 @@ public function testCancelTransactionWillCloseBufferingStream() $body = new ThroughStream(); $body->on('close', $this->expectCallableOnce()); - // mock sender to resolve promise with the given $redirectResponse in - $redirectResponse = new Response(301, array('Location' => 'http://example.com/new'), new ReadableBodyStream($body)); - $sender->expects($this->once())->method('send')->willReturn(Promise\resolve($redirectResponse)); + // mock sender to resolve promise with the given $redirectResponse + $deferred = new Deferred(); + $sender->expects($this->once())->method('send')->willReturn($deferred->promise()); $transaction = new Transaction($sender, $loop); $promise = $transaction->send($request); + $redirectResponse = new Response(301, array('Location' => 'http://example.com/new'), new ReadableBodyStream($body)); + $deferred->resolve($redirectResponse); + $promise->cancel(); } diff --git a/tests/Middleware/LimitConcurrentRequestsMiddlewareTest.php b/tests/Middleware/LimitConcurrentRequestsMiddlewareTest.php index 7e537391..6c63a94f 100644 --- a/tests/Middleware/LimitConcurrentRequestsMiddlewareTest.php +++ b/tests/Middleware/LimitConcurrentRequestsMiddlewareTest.php @@ -79,7 +79,7 @@ public function testLimitOneRequestConcurrently() /** * Ensure resolve frees up a slot */ - $deferredA->resolve(); + $deferredA->resolve(null); $this->assertTrue($calledA); $this->assertTrue($calledB); @@ -88,7 +88,7 @@ public function testLimitOneRequestConcurrently() /** * Ensure reject also frees up a slot */ - $deferredB->reject(); + $deferredB->reject(new \RuntimeException()); $this->assertTrue($calledA); $this->assertTrue($calledB); @@ -194,7 +194,7 @@ public function testStreamDoesPauseAndThenResumeWhenDequeued() $limitHandlers(new ServerRequest('GET', 'https://example.com/', array(), $body), function () {}); - $deferred->reject(); + $deferred->reject(new \RuntimeException()); } public function testReceivesBufferedRequestSameInstance() @@ -452,7 +452,7 @@ public function testReceivesStreamingBodyChangesInstanceWithCustomBodyButSameDat $req = $request; }); - $deferred->reject(); + $deferred->reject(new \RuntimeException()); $this->assertNotSame($request, $req); $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $req); From c556187f9ad466a241212adcefdcc4fa345ac2b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 2 Sep 2022 16:19:03 +0200 Subject: [PATCH 408/456] Update to stable dev dependencies --- .github/workflows/ci.yml | 3 --- composer.json | 24 +++++------------------- 2 files changed, 5 insertions(+), 22 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fafa0ff0..0724232c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,8 +29,6 @@ jobs: with: php-version: ${{ matrix.php }} coverage: xdebug - env: - COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: composer install - run: vendor/bin/phpunit --coverage-text if: ${{ matrix.php >= 7.3 }} @@ -41,7 +39,6 @@ jobs: name: PHPUnit (HHVM) runs-on: ubuntu-18.04 continue-on-error: true - if: false # temporarily skipped until https://github.com/azjezz/setup-hhvm/issues/3 is addressed steps: - uses: actions/checkout@v2 - uses: azjezz/setup-hhvm@v1 diff --git a/composer.json b/composer.json index 3ba8a606..d92ac820 100644 --- a/composer.json +++ b/composer.json @@ -31,16 +31,16 @@ "fig/http-message-util": "^1.1", "psr/http-message": "^1.0", "react/event-loop": "^1.2", - "react/promise": "^3@dev || ^2.3 || ^1.2.1", + "react/promise": "^3 || ^2.3 || ^1.2.1", "react/promise-stream": "^1.4", "react/socket": "^1.12", "react/stream": "^1.2", "ringcentral/psr7": "^1.2" }, "require-dev": { - "clue/http-proxy-react": "dev-promise-v3 as 1.8.0", - "clue/reactphp-ssh-proxy": "dev-promise-v3 as 1.4.0", - "clue/socks-react": "dev-promise-v3 as 1.4.0", + "clue/http-proxy-react": "^1.8", + "clue/reactphp-ssh-proxy": "^1.4", + "clue/socks-react": "^1.4", "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35", "react/async": "^4 || ^3 || ^2", "react/promise-timer": "^1.9" @@ -50,19 +50,5 @@ }, "autoload-dev": { "psr-4": { "React\\Tests\\Http\\": "tests" } - }, - "repositories": [ - { - "type": "vcs", - "url": "https://github.com/clue-labs/reactphp-http-proxy" - }, - { - "type": "vcs", - "url": "https://github.com/clue-labs/reactphp-socks" - }, - { - "type": "vcs", - "url": "https://github.com/clue-labs/reactphp-ssh-proxy" - } - ] + } } From 0c27d679a64231343563bfb879c8fe89164093cd Mon Sep 17 00:00:00 2001 From: 51imyyy Date: Mon, 12 Sep 2022 09:52:39 +0200 Subject: [PATCH 409/456] added support for default headers in Browser PHP and moved default header user-agent to the default headers. --- README.md | 32 +++++++++++ src/Browser.php | 72 ++++++++++++++++++++++++ src/Client/RequestData.php | 1 - tests/BrowserTest.php | 97 ++++++++++++++++++++++++++++++++ tests/Client/RequestDataTest.php | 8 --- tests/Client/RequestTest.php | 10 ++-- 6 files changed, 206 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index d3a93ceb..0cdbe74e 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,8 @@ multiple concurrent HTTP requests without blocking. * [withBase()](#withbase) * [withProtocolVersion()](#withprotocolversion) * [withResponseBuffer()](#withresponsebuffer) + * [withHeader()](#withheader) + * [withoutHeader()](#withoutheader) * [React\Http\Message](#reacthttpmessage) * [Response](#response) * [html()](#html) @@ -2381,6 +2383,36 @@ Notice that the [`Browser`](#browser) is an immutable object, i.e. this method actually returns a *new* [`Browser`](#browser) instance with the given setting applied. +#### withHeader() + +The `withHeader(string $header, string $value): Browser` method can be used to +add a request header for all following requests. + +```php +$browser = $browser->withHeader('User-Agent', 'ACME'); + +$browser->get($url)->then(…); +``` + +Note that the new header will overwrite any headers previously set with +the same name (case-insensitive). Following requests will use these headers +by default unless they are explicitly set for any requests. + +#### withoutHeader() + +The `withoutHeader(string $header): Browser` method can be used to +remove any default request headers previously set via +the [`withHeader()` method](#withheader). + +```php +$browser = $browser->withoutHeader('User-Agent'); + +$browser->get($url)->then(…); +``` + +Note that this method only affects the headers which were set with the +method `withHeader(string $header, string $value): Browser` + ### React\Http\Message #### Response diff --git a/src/Browser.php b/src/Browser.php index 72847f66..16c98fb3 100644 --- a/src/Browser.php +++ b/src/Browser.php @@ -23,6 +23,9 @@ class Browser private $transaction; private $baseUrl; private $protocolVersion = '1.1'; + private $defaultHeaders = array( + 'User-Agent' => 'ReactPHP/1' + ); /** * The `Browser` is responsible for sending HTTP requests to your HTTP server @@ -725,6 +728,62 @@ public function withResponseBuffer($maximumSize) )); } + /** + * Add a request header for all following requests. + * + * ```php + * $browser = $browser->withHeader('User-Agent', 'ACME'); + * + * $browser->get($url)->then(…); + * ``` + * + * Note that the new header will overwrite any headers previously set with + * the same name (case-insensitive). Following requests will use these headers + * by default unless they are explicitly set for any requests. + * + * @param string $header + * @param string $value + * @return Browser + */ + public function withHeader($header, $value) + { + $browser = $this->withoutHeader($header); + $browser->defaultHeaders[$header] = $value; + + return $browser; + } + + /** + * Remove any default request headers previously set via + * the [`withHeader()` method](#withheader). + * + * ```php + * $browser = $browser->withoutHeader('User-Agent'); + * + * $browser->get($url)->then(…); + * ``` + * + * Note that this method only affects the headers which were set with the + * method `withHeader(string $header, string $value): Browser` + * + * @param string $header + * @return Browser + */ + public function withoutHeader($header) + { + $browser = clone $this; + + /** @var string|int $key */ + foreach (\array_keys($browser->defaultHeaders) as $key) { + if (\strcasecmp($key, $header) === 0) { + unset($browser->defaultHeaders[$key]); + break; + } + } + + return $browser; + } + /** * Changes the [options](#options) to use: * @@ -783,6 +842,19 @@ private function requestMayBeStreaming($method, $url, array $headers = array(), $body = new ReadableBodyStream($body); } + foreach ($this->defaultHeaders as $key => $value) { + $explicitHeaderExists = false; + foreach (\array_keys($headers) as $headerKey) { + if (\strcasecmp($headerKey, $key) === 0) { + $explicitHeaderExists = true; + break; + } + } + if (!$explicitHeaderExists) { + $headers[$key] = $value; + } + } + return $this->transaction->send( new Request($method, $url, $headers, $body, $this->protocolVersion) ); diff --git a/src/Client/RequestData.php b/src/Client/RequestData.php index a5908a08..04bb4cad 100644 --- a/src/Client/RequestData.php +++ b/src/Client/RequestData.php @@ -29,7 +29,6 @@ private function mergeDefaultheaders(array $headers) $defaults = array_merge( array( 'Host' => $this->getHost().$port, - 'User-Agent' => 'ReactPHP/1', ), $connectionHeaders, $authHeaders diff --git a/tests/BrowserTest.php b/tests/BrowserTest.php index 39be453a..75717169 100644 --- a/tests/BrowserTest.php +++ b/tests/BrowserTest.php @@ -503,4 +503,101 @@ public function testCancelGetRequestShouldCancelUnderlyingSocketConnection() $promise = $this->browser->get('http://example.com/'); $promise->cancel(); } + + public function testWithHeaderShouldOverwriteExistingHeader() + { + $this->browser = $this->browser->withHeader('User-Agent', 'ACMC'); //should be overwritten + $this->browser = $this->browser->withHeader('user-agent', 'ABC'); //should be the user-agent + + $that = $this; + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { + $that->assertEquals(array('ABC'), $request->getHeader('UsEr-AgEnT')); + return true; + }))->willReturn(new Promise(function () { })); + + $this->browser->get('http://example.com/'); + } + + public function testWithHeaderShouldBeOverwrittenByExplicitHeaderInGetMethod() + { + $this->browser = $this->browser->withHeader('User-Agent', 'ACMC'); + + $that = $this; + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { + $that->assertEquals(array('ABC'), $request->getHeader('UsEr-AgEnT')); + return true; + }))->willReturn(new Promise(function () { })); + + $this->browser->get('http://example.com/', array('user-Agent' => 'ABC')); //should win + } + + public function testWithMultipleHeadersShouldBeMergedCorrectlyWithMultipleDefaultHeaders() + { + $this->browser = $this->browser->withHeader('User-Agent', 'ACMC'); + $this->browser = $this->browser->withHeader('User-Test', 'Test'); + $this->browser = $this->browser->withHeader('Custom-HEADER', 'custom'); + $this->browser = $this->browser->withHeader('just-a-header', 'header-value'); + + $that = $this; + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { + $expectedHeaders = array( + 'Host' => array('example.com'), + + 'User-Test' => array('Test'), + 'just-a-header' => array('header-value'), + + 'user-Agent' => array('ABC'), + 'another-header' => array('value'), + 'custom-header' => array('data'), + ); + + $that->assertEquals($expectedHeaders, $request->getHeaders()); + return true; + }))->willReturn(new Promise(function () { })); + + $headers = array( + 'user-Agent' => 'ABC', //should overwrite: 'User-Agent', 'ACMC' + 'another-header' => 'value', + 'custom-header' => 'data', //should overwrite: 'Custom-header', 'custom' + ); + $this->browser->get('http://example.com/', $headers); + } + + public function testWithoutHeaderShouldRemoveExistingHeader() + { + $this->browser = $this->browser->withHeader('User-Agent', 'ACMC'); + $this->browser = $this->browser->withoutHeader('UsEr-AgEnT'); //should remove case-insensitive header + + $that = $this; + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { + $that->assertEquals(array(), $request->getHeader('user-agent')); + return true; + }))->willReturn(new Promise(function () { })); + + $this->browser->get('http://example.com/'); + } + + public function testBrowserShouldSendDefaultUserAgentHeader() + { + $that = $this; + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { + $that->assertEquals(array(0 => 'ReactPHP/1'), $request->getHeader('user-agent')); + return true; + }))->willReturn(new Promise(function () { })); + + $this->browser->get('http://example.com/'); + } + + public function testBrowserShouldNotSendDefaultUserAgentHeaderIfWithoutHeaderRemovesUserAgent() + { + $this->browser = $this->browser->withoutHeader('UsEr-AgEnT'); + + $that = $this; + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { + $that->assertEquals(array(), $request->getHeader('User-Agent')); + return true; + }))->willReturn(new Promise(function () { })); + + $this->browser->get('http://example.com/'); + } } diff --git a/tests/Client/RequestDataTest.php b/tests/Client/RequestDataTest.php index 7f96e152..f6713e85 100644 --- a/tests/Client/RequestDataTest.php +++ b/tests/Client/RequestDataTest.php @@ -14,7 +14,6 @@ public function toStringReturnsHTTPRequestMessage() $expected = "GET / HTTP/1.0\r\n" . "Host: www.example.com\r\n" . - "User-Agent: ReactPHP/1\r\n" . "\r\n"; $this->assertSame($expected, $requestData->__toString()); @@ -27,7 +26,6 @@ public function toStringReturnsHTTPRequestMessageWithEmptyQueryString() $expected = "GET /path?hello=world HTTP/1.0\r\n" . "Host: www.example.com\r\n" . - "User-Agent: ReactPHP/1\r\n" . "\r\n"; $this->assertSame($expected, $requestData->__toString()); @@ -40,7 +38,6 @@ public function toStringReturnsHTTPRequestMessageWithZeroQueryStringAndRootPath( $expected = "GET /?0 HTTP/1.0\r\n" . "Host: www.example.com\r\n" . - "User-Agent: ReactPHP/1\r\n" . "\r\n"; $this->assertSame($expected, $requestData->__toString()); @@ -53,7 +50,6 @@ public function toStringReturnsHTTPRequestMessageWithOptionsAbsoluteRequestForm( $expected = "OPTIONS / HTTP/1.0\r\n" . "Host: www.example.com\r\n" . - "User-Agent: ReactPHP/1\r\n" . "\r\n"; $this->assertSame($expected, $requestData->__toString()); @@ -66,7 +62,6 @@ public function toStringReturnsHTTPRequestMessageWithOptionsAsteriskRequestForm( $expected = "OPTIONS * HTTP/1.0\r\n" . "Host: www.example.com\r\n" . - "User-Agent: ReactPHP/1\r\n" . "\r\n"; $this->assertSame($expected, $requestData->__toString()); @@ -80,7 +75,6 @@ public function toStringReturnsHTTPRequestMessageWithProtocolVersion() $expected = "GET / HTTP/1.1\r\n" . "Host: www.example.com\r\n" . - "User-Agent: ReactPHP/1\r\n" . "Connection: close\r\n" . "\r\n"; @@ -131,7 +125,6 @@ public function toStringReturnsHTTPRequestMessageWithProtocolVersionThroughConst $expected = "GET / HTTP/1.1\r\n" . "Host: www.example.com\r\n" . - "User-Agent: ReactPHP/1\r\n" . "Connection: close\r\n" . "\r\n"; @@ -145,7 +138,6 @@ public function toStringUsesUserPassFromURL() $expected = "GET / HTTP/1.0\r\n" . "Host: www.example.com\r\n" . - "User-Agent: ReactPHP/1\r\n" . "Authorization: Basic am9objpkdW1teQ==\r\n" . "\r\n"; diff --git a/tests/Client/RequestTest.php b/tests/Client/RequestTest.php index fb2dc884..cdb209cf 100644 --- a/tests/Client/RequestTest.php +++ b/tests/Client/RequestTest.php @@ -181,7 +181,7 @@ public function postRequestShouldSendAPostRequest() $this->stream ->expects($this->once()) ->method('write') - ->with($this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\nUser-Agent:.*\r\n\r\nsome post data$#")); + ->with($this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\n\r\nsome post data$#")); $request->end('some post data'); @@ -199,7 +199,7 @@ public function writeWithAPostRequestShouldSendToTheStream() $this->successfulConnectionMock(); $this->stream->expects($this->exactly(3))->method('write')->withConsecutive( - array($this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\nUser-Agent:.*\r\n\r\nsome$#")), + array($this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\n\r\nsome$#")), array($this->identicalTo("post")), array($this->identicalTo("data")) ); @@ -222,7 +222,7 @@ public function writeWithAPostRequestShouldSendBodyAfterHeadersAndEmitDrainEvent $resolveConnection = $this->successfulAsyncConnectionMock(); $this->stream->expects($this->exactly(2))->method('write')->withConsecutive( - array($this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\nUser-Agent:.*\r\n\r\nsomepost$#")), + array($this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\n\r\nsomepost$#")), array($this->identicalTo("data")) )->willReturn( true @@ -258,7 +258,7 @@ public function writeWithAPostRequestShouldForwardDrainEventIfFirstChunkExceedsB $resolveConnection = $this->successfulAsyncConnectionMock(); $this->stream->expects($this->exactly(2))->method('write')->withConsecutive( - array($this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\nUser-Agent:.*\r\n\r\nsomepost$#")), + array($this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\n\r\nsomepost$#")), array($this->identicalTo("data")) )->willReturn( false @@ -290,7 +290,7 @@ public function pipeShouldPipeDataIntoTheRequestBody() $this->successfulConnectionMock(); $this->stream->expects($this->exactly(3))->method('write')->withConsecutive( - array($this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\nUser-Agent:.*\r\n\r\nsome$#")), + array($this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\n\r\nsome$#")), array($this->identicalTo("post")), array($this->identicalTo("data")) ); From 2290723e79dcd0fa17d8c9432727d3e17dde37b0 Mon Sep 17 00:00:00 2001 From: Fabian Meyer Date: Mon, 19 Sep 2022 11:24:47 +0200 Subject: [PATCH 410/456] Preserve method on redirect --- README.md | 7 ++- src/Io/Transaction.php | 33 ++++++---- tests/Io/TransactionTest.php | 118 ++++++++++++++++++++++++++++++++++- 3 files changed, 143 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 66ea34a8..f1f6d7cd 100644 --- a/README.md +++ b/README.md @@ -342,9 +342,10 @@ $browser->get($url, $headers)->then(function (Psr\Http\Message\ResponseInterface Any redirected requests will follow the semantics of the original request and will include the same request headers as the original request except for those listed below. -If the original request contained a request body, this request body will never -be passed to the redirected request. Accordingly, each redirected request will -remove any `Content-Length` and `Content-Type` request headers. +If the original request is a temporary (307) or a permanent (308) redirect, request +body and headers will be passed to the redirected request. Otherwise, the request +body will never be passed to the redirected request. Accordingly, each redirected +request will remove any `Content-Length` and `Content-Type` request headers. If the original request used HTTP authentication with an `Authorization` request header, this request header will only be passed as part of the redirected diff --git a/src/Io/Transaction.php b/src/Io/Transaction.php index 330ffed0..b64622a8 100644 --- a/src/Io/Transaction.php +++ b/src/Io/Transaction.php @@ -5,6 +5,7 @@ use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\UriInterface; +use React\Http\Message\Response; use RingCentral\Psr7\Request; use RingCentral\Psr7\Uri; use React\EventLoop\LoopInterface; @@ -234,6 +235,8 @@ public function onResponse(ResponseInterface $response, RequestInterface $reques /** * @param ResponseInterface $response * @param RequestInterface $request + * @param Deferred $deferred + * @param ClientRequestState $state * @return PromiseInterface * @throws \RuntimeException */ @@ -242,7 +245,7 @@ private function onResponseRedirect(ResponseInterface $response, RequestInterfac // resolve location relative to last request URI $location = Uri::resolve($request->getUri(), $response->getHeaderLine('Location')); - $request = $this->makeRedirectRequest($request, $location); + $request = $this->makeRedirectRequest($request, $location, $response->getStatusCode()); $this->progress('redirect', array($request)); if ($state->numRequests >= $this->maxRedirects) { @@ -255,25 +258,33 @@ private function onResponseRedirect(ResponseInterface $response, RequestInterfac /** * @param RequestInterface $request * @param UriInterface $location + * @param int $statusCode * @return RequestInterface + * @throws \RuntimeException */ - private function makeRedirectRequest(RequestInterface $request, UriInterface $location) + private function makeRedirectRequest(RequestInterface $request, UriInterface $location, $statusCode) { - $originalHost = $request->getUri()->getHost(); - $request = $request - ->withoutHeader('Host') - ->withoutHeader('Content-Type') - ->withoutHeader('Content-Length'); - // Remove authorization if changing hostnames (but not if just changing ports or protocols). + $originalHost = $request->getUri()->getHost(); if ($location->getHost() !== $originalHost) { $request = $request->withoutHeader('Authorization'); } - // naïve approach.. - $method = ($request->getMethod() === 'HEAD') ? 'HEAD' : 'GET'; + $request = $request->withoutHeader('Host')->withUri($location); + + if ($statusCode === Response::STATUS_TEMPORARY_REDIRECT || $statusCode === Response::STATUS_PERMANENT_REDIRECT) { + if ($request->getBody() instanceof ReadableStreamInterface) { + throw new \RuntimeException('Unable to redirect request with streaming body'); + } + } else { + $request = $request + ->withMethod($request->getMethod() === 'HEAD' ? 'HEAD' : 'GET') + ->withoutHeader('Content-Type') + ->withoutHeader('Content-Length') + ->withBody(new EmptyBodyStream()); + } - return new Request($method, $location, $request->getHeaders()); + return $request; } private function progress($name, array $args = array()) diff --git a/tests/Io/TransactionTest.php b/tests/Io/TransactionTest.php index d9ac2178..05022009 100644 --- a/tests/Io/TransactionTest.php +++ b/tests/Io/TransactionTest.php @@ -663,7 +663,7 @@ public function testSomeRequestHeadersShouldBeRemovedWhenRedirecting() array($this->callback(function (RequestInterface $request) use ($that) { $that->assertFalse($request->hasHeader('Content-Type')); $that->assertFalse($request->hasHeader('Content-Length')); - return true;; + return true; })) )->willReturnOnConsecutiveCalls( Promise\resolve($redirectResponse), @@ -674,6 +674,122 @@ public function testSomeRequestHeadersShouldBeRemovedWhenRedirecting() $transaction->send($requestWithCustomHeaders); } + public function testRequestMethodShouldBeChangedWhenRedirectingWithSeeOther() + { + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $customHeaders = array( + 'Content-Type' => 'text/html; charset=utf-8', + 'Content-Length' => '111', + ); + + $request = new Request('POST', 'http://example.com', $customHeaders); + $sender = $this->makeSenderMock(); + + // mock sender to resolve promise with the given $redirectResponse in + // response to the given $request + $redirectResponse = new Response(303, array('Location' => 'http://example.com/new')); + + // mock sender to resolve promise with the given $okResponse in + // response to the given $request + $okResponse = new Response(200); + $that = $this; + $sender->expects($this->exactly(2))->method('send')->withConsecutive( + array($this->anything()), + array($this->callback(function (RequestInterface $request) use ($that) { + $that->assertEquals('GET', $request->getMethod()); + $that->assertFalse($request->hasHeader('Content-Type')); + $that->assertFalse($request->hasHeader('Content-Length')); + return true; + })) + )->willReturnOnConsecutiveCalls( + Promise\resolve($redirectResponse), + Promise\resolve($okResponse) + ); + + $transaction = new Transaction($sender, $loop); + $transaction->send($request); + } + + public function testRequestMethodAndBodyShouldNotBeChangedWhenRedirectingWith307Or308() + { + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $customHeaders = array( + 'Content-Type' => 'text/html; charset=utf-8', + 'Content-Length' => '111', + ); + + $request = new Request('POST', 'http://example.com', $customHeaders, '{"key":"value"}'); + $sender = $this->makeSenderMock(); + + // mock sender to resolve promise with the given $redirectResponse in + // response to the given $request + $redirectResponse = new Response(307, array('Location' => 'http://example.com/new')); + + // mock sender to resolve promise with the given $okResponse in + // response to the given $request + $okResponse = new Response(200); + $that = $this; + $sender->expects($this->exactly(2))->method('send')->withConsecutive( + array($this->anything()), + array($this->callback(function (RequestInterface $request) use ($that) { + $that->assertEquals('POST', $request->getMethod()); + $that->assertEquals('{"key":"value"}', (string)$request->getBody()); + $that->assertEquals( + array( + 'Content-Type' => array('text/html; charset=utf-8'), + 'Content-Length' => array('111'), + 'Host' => array('example.com') + ), + $request->getHeaders() + ); + return true; + })) + )->willReturnOnConsecutiveCalls( + Promise\resolve($redirectResponse), + Promise\resolve($okResponse) + ); + + $transaction = new Transaction($sender, $loop); + $transaction->send($request); + } + + public function testRedirectingStreamingBodyWith307Or308ShouldThrowCantRedirectStreamException() + { + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $customHeaders = array( + 'Content-Type' => 'text/html; charset=utf-8', + 'Content-Length' => '111', + ); + + $stream = new ThroughStream(); + $request = new Request('POST', 'http://example.com', $customHeaders, new ReadableBodyStream($stream)); + $sender = $this->makeSenderMock(); + + // mock sender to resolve promise with the given $redirectResponse in + // response to the given $request + $redirectResponse = new Response(307, array('Location' => 'http://example.com/new')); + + $sender->expects($this->once())->method('send')->withConsecutive( + array($this->anything()) + )->willReturnOnConsecutiveCalls( + Promise\resolve($redirectResponse) + ); + + $transaction = new Transaction($sender, $loop); + $promise = $transaction->send($request); + + $exception = null; + $promise->then(null, function ($reason) use (&$exception) { + $exception = $reason; + }); + + assert($exception instanceof \RuntimeException); + $this->assertEquals('Unable to redirect request with streaming body', $exception->getMessage()); + } + public function testCancelTransactionWillCancelRequest() { $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); From aa7512ee17258c88466de30f9cb44ec5f9df3ff3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 29 Sep 2022 14:55:52 +0200 Subject: [PATCH 411/456] Prepare v1.8.0 release --- CHANGELOG.md | 17 ++++++++++++++++- README.md | 2 +- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5bf17e9f..00e2d07e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## 1.8.0 (2022-09-29) + +* Feature: Support for default request headers. + (#461 by @51imyy) + + ```php + $browser = new React\Http\Browser(); + $browser = $browser->withHeader('User-Agent', 'ACME'); + + $browser->get($url)->then(…); + ``` + +* Feature: Forward compatibility with upcoming Promise v3. + (#460 by @clue) + ## 1.7.0 (2022-08-23) This is a **SECURITY** and feature release for the 1.x series of ReactPHP's HTTP component. @@ -19,7 +34,7 @@ This is a **SECURITY** and feature release for the 1.x series of ReactPHP's HTTP (#444 by @mrsimonbennett) * Minor documentation improvements. - (#452 by @clue, #458 by @nhedger, #448 by @jorrit and #446 by @SimonFrings + (#452 by @clue, #458 by @nhedger, #448 by @jorrit and #446 by @SimonFrings) * Improve test suite, update to use new reactphp/async package instead of clue/reactphp-block, skip memory tests when lowering memory limit fails and fix legacy HHVM build. diff --git a/README.md b/README.md index 66ea34a8..1df8873f 100644 --- a/README.md +++ b/README.md @@ -2956,7 +2956,7 @@ This project follows [SemVer](https://semver.org/). This will install the latest supported version: ```bash -composer require react/http:^1.7 +composer require react/http:^1.8 ``` See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. From f2a1446f0d735d2ae2eb5faaa08bc2725e6a9c9d Mon Sep 17 00:00:00 2001 From: Simon Frings Date: Tue, 6 Sep 2022 16:13:29 +0200 Subject: [PATCH 412/456] Add issue template for better orientation --- .github/ISSUE_TEMPLATE/bug.md | 11 +++++++++++ .github/ISSUE_TEMPLATE/config.yml | 11 +++++++++++ 2 files changed, 22 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug.md create mode 100644 .github/ISSUE_TEMPLATE/config.yml diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md new file mode 100644 index 00000000..d26fe152 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.md @@ -0,0 +1,11 @@ +--- +name: Bug report +about: Found a bug in our project? Create a report to help us improve. +labels: bug +--- + + + +```php +// Please add code examples if possible, so we can reproduce your steps +``` diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..4b4a0ea6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,11 @@ +blank_issues_enabled: false +contact_links: + - name: Report a security vulnerability + url: https://reactphp.org/#support + about: 'If you discover a security vulnerability, please send us an email. Do not disclose security-related issues publicly.' + - name: Feature request + url: https://github.com/orgs/reactphp/discussions/categories/ideas + about: 'You have ideas to improve our project? Start a new discussion in our "Ideas" category.' + - name: Questions + url: https://github.com/orgs/reactphp/discussions/categories/q-a + about: 'We are happy to answer your questions! Start a new discussion in our "Q&A" category.' From 44f0a80f7a1616249cc6c817d30921993d5776cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 12 Nov 2022 17:19:44 +0100 Subject: [PATCH 413/456] Update test suite and report failed assertions --- .github/workflows/ci.yml | 22 +++++++++++++--------- composer.json | 10 +++++++--- phpunit.xml.dist | 14 +++++++++++--- phpunit.xml.legacy | 10 +++++++++- 4 files changed, 40 insertions(+), 16 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0724232c..55bbaa5b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,7 +7,7 @@ on: jobs: PHPUnit: name: PHPUnit (PHP ${{ matrix.php }}) - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 strategy: matrix: php: @@ -24,11 +24,12 @@ jobs: - 5.4 - 5.3 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} coverage: xdebug + ini-file: development - run: composer install - run: vendor/bin/phpunit --coverage-text if: ${{ matrix.php >= 7.3 }} @@ -37,13 +38,16 @@ jobs: PHPUnit-hhvm: name: PHPUnit (HHVM) - runs-on: ubuntu-18.04 + runs-on: ubuntu-22.04 continue-on-error: true steps: - - uses: actions/checkout@v2 - - uses: azjezz/setup-hhvm@v1 + - uses: actions/checkout@v3 + - run: cp "$(which composer)" composer.phar && ./composer.phar self-update --2.2 # downgrade Composer for HHVM + - name: Run hhvm composer.phar install + uses: docker://hhvm/hhvm:3.30-lts-latest with: - version: lts-3.30 - - run: composer self-update --2.2 # downgrade Composer for HHVM - - run: hhvm $(which composer) install - - run: hhvm vendor/bin/phpunit + args: hhvm composer.phar install + - name: Run hhvm vendor/bin/phpunit + uses: docker://hhvm/hhvm:3.30-lts-latest + with: + args: hhvm vendor/bin/phpunit diff --git a/composer.json b/composer.json index d92ac820..aeee592b 100644 --- a/composer.json +++ b/composer.json @@ -41,14 +41,18 @@ "clue/http-proxy-react": "^1.8", "clue/reactphp-ssh-proxy": "^1.4", "clue/socks-react": "^1.4", - "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35", + "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35", "react/async": "^4 || ^3 || ^2", "react/promise-timer": "^1.9" }, "autoload": { - "psr-4": { "React\\Http\\": "src" } + "psr-4": { + "React\\Http\\": "src/" + } }, "autoload-dev": { - "psr-4": { "React\\Tests\\Http\\": "tests" } + "psr-4": { + "React\\Tests\\Http\\": "tests/" + } } } diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 93a36f6b..7a9577e9 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,8 +1,8 @@ - - +./src/ + + + + + + + + diff --git a/phpunit.xml.legacy b/phpunit.xml.legacy index fbb43e85..ac5600ae 100644 --- a/phpunit.xml.legacy +++ b/phpunit.xml.legacy @@ -1,6 +1,6 @@ - + ./src/ + + + + + + + + From b8f6efa7225e3da486606ca757554174b8f40ebe Mon Sep 17 00:00:00 2001 From: Simon Frings Date: Thu, 17 Nov 2022 15:27:56 +0100 Subject: [PATCH 414/456] Revert issue template changes to use organisation issue template --- .github/ISSUE_TEMPLATE/bug.md | 11 ----------- .github/ISSUE_TEMPLATE/config.yml | 11 ----------- 2 files changed, 22 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/bug.md delete mode 100644 .github/ISSUE_TEMPLATE/config.yml diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md deleted file mode 100644 index d26fe152..00000000 --- a/.github/ISSUE_TEMPLATE/bug.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -name: Bug report -about: Found a bug in our project? Create a report to help us improve. -labels: bug ---- - - - -```php -// Please add code examples if possible, so we can reproduce your steps -``` diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml deleted file mode 100644 index 4b4a0ea6..00000000 --- a/.github/ISSUE_TEMPLATE/config.yml +++ /dev/null @@ -1,11 +0,0 @@ -blank_issues_enabled: false -contact_links: - - name: Report a security vulnerability - url: https://reactphp.org/#support - about: 'If you discover a security vulnerability, please send us an email. Do not disclose security-related issues publicly.' - - name: Feature request - url: https://github.com/orgs/reactphp/discussions/categories/ideas - about: 'You have ideas to improve our project? Start a new discussion in our "Ideas" category.' - - name: Questions - url: https://github.com/orgs/reactphp/discussions/categories/q-a - about: 'We are happy to answer your questions! Start a new discussion in our "Q&A" category.' From 7a27c49ec600940ab062d7f378a7fed7d3fb54ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 22 Nov 2022 14:40:33 +0100 Subject: [PATCH 415/456] Add `Request` class to represent outgoing HTTP request message --- README.md | 19 ++++++++++ src/Browser.php | 7 +--- src/Io/Transaction.php | 5 ++- src/Message/Request.php | 58 ++++++++++++++++++++++++++++++ src/Message/ServerRequest.php | 4 +-- tests/FunctionalBrowserTest.php | 3 +- tests/Io/SenderTest.php | 2 +- tests/Io/TransactionTest.php | 4 +-- tests/Message/RequestTest.php | 63 +++++++++++++++++++++++++++++++++ 9 files changed, 149 insertions(+), 16 deletions(-) create mode 100644 src/Message/Request.php create mode 100644 tests/Message/RequestTest.php diff --git a/README.md b/README.md index 55ebb8e7..271f5e87 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,7 @@ multiple concurrent HTTP requests without blocking. * [json()](#json) * [plaintext()](#plaintext) * [xml()](#xml) + * [Request](#request-1) * [ServerRequest](#serverrequest) * [ResponseException](#responseexception) * [React\Http\Middleware](#reacthttpmiddleware) @@ -2628,6 +2629,24 @@ $response = React\Http\Message\Response::xml( )->withStatus(React\Http\Message\Response::STATUS_BAD_REQUEST); ``` +#### Request + +The `React\Http\Message\Request` class can be used to +respresent an outgoing HTTP request message. + +This class implements the +[PSR-7 `RequestInterface`](https://www.php-fig.org/psr/psr-7/#32-psrhttpmessagerequestinterface) +which extends the +[PSR-7 `MessageInterface`](https://www.php-fig.org/psr/psr-7/#31-psrhttpmessagemessageinterface). + +This is mostly used internally to represent each outgoing HTTP request +message for the HTTP client implementation. Likewise, you can also use this +class with other HTTP client implementations and for tests. + +> Internally, this implementation builds on top of an existing outgoing + request message and only adds support for streaming. This base class is + considered an implementation detail that may change in the future. + #### ServerRequest The `React\Http\Message\ServerRequest` class can be used to diff --git a/src/Browser.php b/src/Browser.php index 16c98fb3..3e3458af 100644 --- a/src/Browser.php +++ b/src/Browser.php @@ -3,13 +3,12 @@ namespace React\Http; use Psr\Http\Message\ResponseInterface; -use RingCentral\Psr7\Request; use RingCentral\Psr7\Uri; use React\EventLoop\Loop; use React\EventLoop\LoopInterface; -use React\Http\Io\ReadableBodyStream; use React\Http\Io\Sender; use React\Http\Io\Transaction; +use React\Http\Message\Request; use React\Promise\PromiseInterface; use React\Socket\ConnectorInterface; use React\Stream\ReadableStreamInterface; @@ -838,10 +837,6 @@ private function requestMayBeStreaming($method, $url, array $headers = array(), $url = Uri::resolve($this->baseUrl, $url); } - if ($body instanceof ReadableStreamInterface) { - $body = new ReadableBodyStream($body); - } - foreach ($this->defaultHeaders as $key => $value) { $explicitHeaderExists = false; foreach (\array_keys($headers) as $headerKey) { diff --git a/src/Io/Transaction.php b/src/Io/Transaction.php index b64622a8..cbf8f3eb 100644 --- a/src/Io/Transaction.php +++ b/src/Io/Transaction.php @@ -5,14 +5,13 @@ use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\UriInterface; -use React\Http\Message\Response; -use RingCentral\Psr7\Request; -use RingCentral\Psr7\Uri; use React\EventLoop\LoopInterface; +use React\Http\Message\Response; use React\Http\Message\ResponseException; use React\Promise\Deferred; use React\Promise\PromiseInterface; use React\Stream\ReadableStreamInterface; +use RingCentral\Psr7\Uri; /** * @internal diff --git a/src/Message/Request.php b/src/Message/Request.php new file mode 100644 index 00000000..cf59641e --- /dev/null +++ b/src/Message/Request.php @@ -0,0 +1,58 @@ + Internally, this implementation builds on top of an existing outgoing + * request message and only adds support for streaming. This base class is + * considered an implementation detail that may change in the future. + * + * @see RequestInterface + */ +final class Request extends BaseRequest implements RequestInterface +{ + /** + * @param string $method HTTP method for the request. + * @param string|UriInterface $url URL for the request. + * @param array $headers Headers for the message. + * @param string|ReadableStreamInterface|StreamInterface $body Message body. + * @param string $version HTTP protocol version. + * @throws \InvalidArgumentException for an invalid URL or body + */ + public function __construct( + $method, + $url, + array $headers = array(), + $body = '', + $version = '1.1' + ) { + if (\is_string($body)) { + $body = new BufferedBody($body); + } elseif ($body instanceof ReadableStreamInterface && !$body instanceof StreamInterface) { + $body = new ReadableBodyStream($body); + } elseif (!$body instanceof StreamInterface) { + throw new \InvalidArgumentException('Invalid request body given'); + } + + parent::__construct($method, $url, $headers, $body, $version); + } +} diff --git a/src/Message/ServerRequest.php b/src/Message/ServerRequest.php index fdb3ec5e..25532cf4 100644 --- a/src/Message/ServerRequest.php +++ b/src/Message/ServerRequest.php @@ -8,7 +8,7 @@ use React\Http\Io\BufferedBody; use React\Http\Io\HttpBodyStream; use React\Stream\ReadableStreamInterface; -use RingCentral\Psr7\Request; +use RingCentral\Psr7\Request as BaseRequest; /** * Respresents an incoming server request message. @@ -30,7 +30,7 @@ * * @see ServerRequestInterface */ -final class ServerRequest extends Request implements ServerRequestInterface +final class ServerRequest extends BaseRequest implements ServerRequestInterface { private $attributes = array(); diff --git a/tests/FunctionalBrowserTest.php b/tests/FunctionalBrowserTest.php index 95092ac1..6def2ecc 100644 --- a/tests/FunctionalBrowserTest.php +++ b/tests/FunctionalBrowserTest.php @@ -7,16 +7,15 @@ use React\EventLoop\Loop; use React\Http\Browser; use React\Http\HttpServer; +use React\Http\Message\Response; use React\Http\Message\ResponseException; use React\Http\Middleware\StreamingRequestMiddleware; -use React\Http\Message\Response; use React\Promise\Promise; use React\Promise\Stream; use React\Socket\Connector; use React\Socket\SocketServer; use React\Stream\ReadableStreamInterface; use React\Stream\ThroughStream; -use RingCentral\Psr7\Request; class FunctionalBrowserTest extends TestCase { diff --git a/tests/Io/SenderTest.php b/tests/Io/SenderTest.php index 587ba0c2..91b87b30 100644 --- a/tests/Io/SenderTest.php +++ b/tests/Io/SenderTest.php @@ -6,10 +6,10 @@ use React\Http\Client\RequestData; use React\Http\Io\ReadableBodyStream; use React\Http\Io\Sender; +use React\Http\Message\Request; use React\Promise; use React\Stream\ThroughStream; use React\Tests\Http\TestCase; -use RingCentral\Psr7\Request; class SenderTest extends TestCase { diff --git a/tests/Io/TransactionTest.php b/tests/Io/TransactionTest.php index 05022009..e0d04e39 100644 --- a/tests/Io/TransactionTest.php +++ b/tests/Io/TransactionTest.php @@ -7,14 +7,14 @@ use Psr\Http\Message\ResponseInterface; use React\Http\Io\ReadableBodyStream; use React\Http\Io\Transaction; +use React\Http\Message\Request; +use React\Http\Message\Response; use React\Http\Message\ResponseException; use React\EventLoop\Loop; use React\Promise; use React\Promise\Deferred; use React\Stream\ThroughStream; use React\Tests\Http\TestCase; -use RingCentral\Psr7\Request; -use RingCentral\Psr7\Response; class TransactionTest extends TestCase { diff --git a/tests/Message/RequestTest.php b/tests/Message/RequestTest.php new file mode 100644 index 00000000..29baf8a7 --- /dev/null +++ b/tests/Message/RequestTest.php @@ -0,0 +1,63 @@ +getBody(); + $this->assertSame(3, $body->getSize()); + $this->assertEquals('foo', (string) $body); + } + + public function testConstructWithStreamingRequestBodyReturnsBodyWhichImplementsReadableStreamInterfaceWithUnknownSize() + { + $request = new Request( + 'GET', + 'http://localhost', + array(), + new ThroughStream() + ); + + $body = $request->getBody(); + $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $body); + $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); + $this->assertNull($body->getSize()); + } + + public function testConstructWithHttpBodyStreamReturnsBodyAsIs() + { + $request = new Request( + 'GET', + 'http://localhost', + array(), + $body = new HttpBodyStream(new ThroughStream(), 100) + ); + + $this->assertSame($body, $request->getBody()); + } + + public function testConstructWithNullBodyThrows() + { + $this->setExpectedException('InvalidArgumentException', 'Invalid request body given'); + new Request( + 'GET', + 'http://localhost', + array(), + null + ); + } +} From 01228fa89454b00695f68c84e177b400715ff081 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Wed, 23 Nov 2022 14:06:26 +0100 Subject: [PATCH 416/456] Rename internal `Request` to `ClientRequestStream` --- src/Client/Client.php | 6 ++- .../ClientRequestStream.php} | 7 +-- .../ClientRequestStreamTest.php} | 48 +++++++++---------- tests/Io/SenderTest.php | 26 +++++----- 4 files changed, 45 insertions(+), 42 deletions(-) rename src/{Client/Request.php => Io/ClientRequestStream.php} (96%) rename tests/{Client/RequestTest.php => Io/ClientRequestStreamTest.php} (89%) diff --git a/src/Client/Client.php b/src/Client/Client.php index 7a97349c..62caed5f 100644 --- a/src/Client/Client.php +++ b/src/Client/Client.php @@ -3,8 +3,9 @@ namespace React\Http\Client; use React\EventLoop\LoopInterface; -use React\Socket\ConnectorInterface; +use React\Http\Io\ClientRequestStream; use React\Socket\Connector; +use React\Socket\ConnectorInterface; /** * @internal @@ -22,10 +23,11 @@ public function __construct(LoopInterface $loop, ConnectorInterface $connector = $this->connector = $connector; } + /** @return ClientRequestStream */ public function request($method, $url, array $headers = array(), $protocolVersion = '1.0') { $requestData = new RequestData($method, $url, $headers, $protocolVersion); - return new Request($this->connector, $requestData); + return new ClientRequestStream($this->connector, $requestData); } } diff --git a/src/Client/Request.php b/src/Io/ClientRequestStream.php similarity index 96% rename from src/Client/Request.php rename to src/Io/ClientRequestStream.php index 51e03313..2513a89a 100644 --- a/src/Client/Request.php +++ b/src/Io/ClientRequestStream.php @@ -1,8 +1,9 @@ write($headers . $pendingWrites); - $stateRef = Request::STATE_HEAD_WRITTEN; + $stateRef = ClientRequestStream::STATE_HEAD_WRITTEN; // clear pending writes if non-empty if ($pendingWrites !== '') { diff --git a/tests/Client/RequestTest.php b/tests/Io/ClientRequestStreamTest.php similarity index 89% rename from tests/Client/RequestTest.php rename to tests/Io/ClientRequestStreamTest.php index cdb209cf..6e3e16b8 100644 --- a/tests/Client/RequestTest.php +++ b/tests/Io/ClientRequestStreamTest.php @@ -1,16 +1,16 @@ connector, $requestData); + $request = new ClientRequestStream($this->connector, $requestData); $this->successfulConnectionMock(); @@ -67,7 +67,7 @@ public function requestShouldBindToStreamEventsAndUseconnector() public function requestShouldConnectViaTlsIfUrlUsesHttpsScheme() { $requestData = new RequestData('GET', 'https://www.example.com'); - $request = new Request($this->connector, $requestData); + $request = new ClientRequestStream($this->connector, $requestData); $this->connector->expects($this->once())->method('connect')->with('tls://www.example.com:443')->willReturn(new Promise(function () { })); @@ -78,7 +78,7 @@ public function requestShouldConnectViaTlsIfUrlUsesHttpsScheme() public function requestShouldEmitErrorIfConnectionFails() { $requestData = new RequestData('GET', 'http://www.example.com'); - $request = new Request($this->connector, $requestData); + $request = new ClientRequestStream($this->connector, $requestData); $this->connector->expects($this->once())->method('connect')->willReturn(\React\Promise\reject(new \RuntimeException())); @@ -94,7 +94,7 @@ public function requestShouldEmitErrorIfConnectionFails() public function requestShouldEmitErrorIfConnectionClosesBeforeResponseIsParsed() { $requestData = new RequestData('GET', 'http://www.example.com'); - $request = new Request($this->connector, $requestData); + $request = new ClientRequestStream($this->connector, $requestData); $this->successfulConnectionMock(); @@ -111,7 +111,7 @@ public function requestShouldEmitErrorIfConnectionClosesBeforeResponseIsParsed() public function requestShouldEmitErrorIfConnectionEmitsError() { $requestData = new RequestData('GET', 'http://www.example.com'); - $request = new Request($this->connector, $requestData); + $request = new ClientRequestStream($this->connector, $requestData); $this->successfulConnectionMock(); @@ -128,7 +128,7 @@ public function requestShouldEmitErrorIfConnectionEmitsError() public function requestShouldEmitErrorIfRequestParserThrowsException() { $requestData = new RequestData('GET', 'http://www.example.com'); - $request = new Request($this->connector, $requestData); + $request = new ClientRequestStream($this->connector, $requestData); $this->successfulConnectionMock(); @@ -144,7 +144,7 @@ public function requestShouldEmitErrorIfRequestParserThrowsException() public function requestShouldEmitErrorIfUrlIsInvalid() { $requestData = new RequestData('GET', 'ftp://www.example.com'); - $request = new Request($this->connector, $requestData); + $request = new ClientRequestStream($this->connector, $requestData); $request->on('error', $this->expectCallableOnceWith($this->isInstanceOf('InvalidArgumentException'))); @@ -160,7 +160,7 @@ public function requestShouldEmitErrorIfUrlIsInvalid() public function requestShouldEmitErrorIfUrlHasNoScheme() { $requestData = new RequestData('GET', 'www.example.com'); - $request = new Request($this->connector, $requestData); + $request = new ClientRequestStream($this->connector, $requestData); $request->on('error', $this->expectCallableOnceWith($this->isInstanceOf('InvalidArgumentException'))); @@ -174,7 +174,7 @@ public function requestShouldEmitErrorIfUrlHasNoScheme() public function postRequestShouldSendAPostRequest() { $requestData = new RequestData('POST', 'http://www.example.com'); - $request = new Request($this->connector, $requestData); + $request = new ClientRequestStream($this->connector, $requestData); $this->successfulConnectionMock(); @@ -194,7 +194,7 @@ public function postRequestShouldSendAPostRequest() public function writeWithAPostRequestShouldSendToTheStream() { $requestData = new RequestData('POST', 'http://www.example.com'); - $request = new Request($this->connector, $requestData); + $request = new ClientRequestStream($this->connector, $requestData); $this->successfulConnectionMock(); @@ -217,7 +217,7 @@ public function writeWithAPostRequestShouldSendToTheStream() public function writeWithAPostRequestShouldSendBodyAfterHeadersAndEmitDrainEvent() { $requestData = new RequestData('POST', 'http://www.example.com'); - $request = new Request($this->connector, $requestData); + $request = new ClientRequestStream($this->connector, $requestData); $resolveConnection = $this->successfulAsyncConnectionMock(); @@ -248,7 +248,7 @@ public function writeWithAPostRequestShouldSendBodyAfterHeadersAndEmitDrainEvent public function writeWithAPostRequestShouldForwardDrainEventIfFirstChunkExceedsBuffer() { $requestData = new RequestData('POST', 'http://www.example.com'); - $request = new Request($this->connector, $requestData); + $request = new ClientRequestStream($this->connector, $requestData); $this->stream = $this->getMockBuilder('React\Socket\Connection') ->disableOriginalConstructor() @@ -285,7 +285,7 @@ public function writeWithAPostRequestShouldForwardDrainEventIfFirstChunkExceedsB public function pipeShouldPipeDataIntoTheRequestBody() { $requestData = new RequestData('POST', 'http://www.example.com'); - $request = new Request($this->connector, $requestData); + $request = new ClientRequestStream($this->connector, $requestData); $this->successfulConnectionMock(); @@ -318,7 +318,7 @@ public function pipeShouldPipeDataIntoTheRequestBody() public function writeShouldStartConnecting() { $requestData = new RequestData('POST', 'http://www.example.com'); - $request = new Request($this->connector, $requestData); + $request = new ClientRequestStream($this->connector, $requestData); $this->connector->expects($this->once()) ->method('connect') @@ -334,7 +334,7 @@ public function writeShouldStartConnecting() public function endShouldStartConnectingAndChangeStreamIntoNonWritableMode() { $requestData = new RequestData('POST', 'http://www.example.com'); - $request = new Request($this->connector, $requestData); + $request = new ClientRequestStream($this->connector, $requestData); $this->connector->expects($this->once()) ->method('connect') @@ -352,7 +352,7 @@ public function endShouldStartConnectingAndChangeStreamIntoNonWritableMode() public function closeShouldEmitCloseEvent() { $requestData = new RequestData('POST', 'http://www.example.com'); - $request = new Request($this->connector, $requestData); + $request = new ClientRequestStream($this->connector, $requestData); $request->on('close', $this->expectCallableOnce()); $request->close(); @@ -364,7 +364,7 @@ public function closeShouldEmitCloseEvent() public function writeAfterCloseReturnsFalse() { $requestData = new RequestData('POST', 'http://www.example.com'); - $request = new Request($this->connector, $requestData); + $request = new ClientRequestStream($this->connector, $requestData); $request->close(); @@ -378,7 +378,7 @@ public function writeAfterCloseReturnsFalse() public function endAfterCloseIsNoOp() { $requestData = new RequestData('POST', 'http://www.example.com'); - $request = new Request($this->connector, $requestData); + $request = new ClientRequestStream($this->connector, $requestData); $this->connector->expects($this->never()) ->method('connect'); @@ -393,7 +393,7 @@ public function endAfterCloseIsNoOp() public function closeShouldCancelPendingConnectionAttempt() { $requestData = new RequestData('POST', 'http://www.example.com'); - $request = new Request($this->connector, $requestData); + $request = new ClientRequestStream($this->connector, $requestData); $promise = new Promise(function () {}, function () { throw new \RuntimeException(); @@ -417,7 +417,7 @@ public function closeShouldCancelPendingConnectionAttempt() public function requestShouldRemoveAllListenerAfterClosed() { $requestData = new RequestData('GET', 'http://www.example.com'); - $request = new Request($this->connector, $requestData); + $request = new ClientRequestStream($this->connector, $requestData); $request->on('close', function () {}); $this->assertCount(1, $request->listeners('close')); @@ -451,7 +451,7 @@ private function successfulAsyncConnectionMock() public function multivalueHeader() { $requestData = new RequestData('GET', 'http://www.example.com'); - $request = new Request($this->connector, $requestData); + $request = new ClientRequestStream($this->connector, $requestData); $this->successfulConnectionMock(); diff --git a/tests/Io/SenderTest.php b/tests/Io/SenderTest.php index 91b87b30..6d8c3b5f 100644 --- a/tests/Io/SenderTest.php +++ b/tests/Io/SenderTest.php @@ -76,7 +76,7 @@ public function testSendPostWillAutomaticallySendContentLengthHeader() 'http://www.google.com/', array('Host' => 'www.google.com', 'Content-Length' => '5'), '1.1' - )->willReturn($this->getMockBuilder('React\Http\Client\Request')->disableOriginalConstructor()->getMock()); + )->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock()); $sender = new Sender($client); @@ -92,7 +92,7 @@ public function testSendPostWillAutomaticallySendContentLengthZeroHeaderForEmpty 'http://www.google.com/', array('Host' => 'www.google.com', 'Content-Length' => '0'), '1.1' - )->willReturn($this->getMockBuilder('React\Http\Client\Request')->disableOriginalConstructor()->getMock()); + )->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock()); $sender = new Sender($client); @@ -102,7 +102,7 @@ public function testSendPostWillAutomaticallySendContentLengthZeroHeaderForEmpty public function testSendPostStreamWillAutomaticallySendTransferEncodingChunked() { - $outgoing = $this->getMockBuilder('React\Http\Client\Request')->disableOriginalConstructor()->getMock(); + $outgoing = $this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock(); $outgoing->expects($this->once())->method('write')->with(""); $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); @@ -122,7 +122,7 @@ public function testSendPostStreamWillAutomaticallySendTransferEncodingChunked() public function testSendPostStreamWillAutomaticallyPipeChunkEncodeBodyForWriteAndRespectRequestThrottling() { - $outgoing = $this->getMockBuilder('React\Http\Client\Request')->disableOriginalConstructor()->getMock(); + $outgoing = $this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock(); $outgoing->expects($this->once())->method('isWritable')->willReturn(true); $outgoing->expects($this->exactly(2))->method('write')->withConsecutive(array(""), array("5\r\nhello\r\n"))->willReturn(false); @@ -141,7 +141,7 @@ public function testSendPostStreamWillAutomaticallyPipeChunkEncodeBodyForWriteAn public function testSendPostStreamWillAutomaticallyPipeChunkEncodeBodyForEnd() { - $outgoing = $this->getMockBuilder('React\Http\Client\Request')->disableOriginalConstructor()->getMock(); + $outgoing = $this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock(); $outgoing->expects($this->once())->method('isWritable')->willReturn(true); $outgoing->expects($this->exactly(2))->method('write')->withConsecutive(array(""), array("0\r\n\r\n"))->willReturn(false); $outgoing->expects($this->once())->method('end')->with(null); @@ -160,7 +160,7 @@ public function testSendPostStreamWillAutomaticallyPipeChunkEncodeBodyForEnd() public function testSendPostStreamWillRejectWhenRequestBodyEmitsErrorEvent() { - $outgoing = $this->getMockBuilder('React\Http\Client\Request')->disableOriginalConstructor()->getMock(); + $outgoing = $this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock(); $outgoing->expects($this->once())->method('isWritable')->willReturn(true); $outgoing->expects($this->once())->method('write')->with("")->willReturn(false); $outgoing->expects($this->never())->method('end'); @@ -190,7 +190,7 @@ public function testSendPostStreamWillRejectWhenRequestBodyEmitsErrorEvent() public function testSendPostStreamWillRejectWhenRequestBodyClosesWithoutEnd() { - $outgoing = $this->getMockBuilder('React\Http\Client\Request')->disableOriginalConstructor()->getMock(); + $outgoing = $this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock(); $outgoing->expects($this->once())->method('isWritable')->willReturn(true); $outgoing->expects($this->once())->method('write')->with("")->willReturn(false); $outgoing->expects($this->never())->method('end'); @@ -218,7 +218,7 @@ public function testSendPostStreamWillRejectWhenRequestBodyClosesWithoutEnd() public function testSendPostStreamWillNotRejectWhenRequestBodyClosesAfterEnd() { - $outgoing = $this->getMockBuilder('React\Http\Client\Request')->disableOriginalConstructor()->getMock(); + $outgoing = $this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock(); $outgoing->expects($this->once())->method('isWritable')->willReturn(true); $outgoing->expects($this->exactly(2))->method('write')->withConsecutive(array(""), array("0\r\n\r\n"))->willReturn(false); $outgoing->expects($this->once())->method('end'); @@ -252,7 +252,7 @@ public function testSendPostStreamWithExplicitContentLengthWillSendHeaderAsIs() 'http://www.google.com/', array('Host' => 'www.google.com', 'Content-Length' => '100'), '1.1' - )->willReturn($this->getMockBuilder('React\Http\Client\Request')->disableOriginalConstructor()->getMock()); + )->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock()); $sender = new Sender($client); @@ -269,7 +269,7 @@ public function testSendGetWillNotPassContentLengthHeaderForEmptyRequestBody() 'http://www.google.com/', array('Host' => 'www.google.com'), '1.1' - )->willReturn($this->getMockBuilder('React\Http\Client\Request')->disableOriginalConstructor()->getMock()); + )->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock()); $sender = new Sender($client); @@ -285,7 +285,7 @@ public function testSendCustomMethodWillNotPassContentLengthHeaderForEmptyReques 'http://www.google.com/', array('Host' => 'www.google.com'), '1.1' - )->willReturn($this->getMockBuilder('React\Http\Client\Request')->disableOriginalConstructor()->getMock()); + )->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock()); $sender = new Sender($client); @@ -301,7 +301,7 @@ public function testSendCustomMethodWithExplicitContentLengthZeroWillBePassedAsI 'http://www.google.com/', array('Host' => 'www.google.com', 'Content-Length' => '0'), '1.1' - )->willReturn($this->getMockBuilder('React\Http\Client\Request')->disableOriginalConstructor()->getMock()); + )->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock()); $sender = new Sender($client); @@ -393,7 +393,7 @@ public function testRequestProtocolVersion(Request $Request, $method, $uri, $hea $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(), ))->getMock(); - $request = $this->getMockBuilder('React\Http\Client\Request') + $request = $this->getMockBuilder('React\Http\Io\ClientRequestStream') ->setMethods(array()) ->setConstructorArgs(array( $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(), From 212a3bba511f307eb4efb311e5afafb68b3ecd05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 24 Nov 2022 16:45:11 +0100 Subject: [PATCH 417/456] Refactor to remove internal `RequestData` --- src/Client/Client.php | 7 +- src/Client/RequestData.php | 127 -------------- src/Io/ClientRequestStream.php | 39 +++-- src/Io/Sender.php | 16 +- tests/Client/FunctionalIntegrationTest.php | 13 +- tests/Client/RequestDataTest.php | 146 ---------------- tests/Io/ClientRequestStreamTest.php | 87 +++++++--- tests/Io/SenderTest.php | 185 ++++++++++----------- 8 files changed, 205 insertions(+), 415 deletions(-) delete mode 100644 src/Client/RequestData.php delete mode 100644 tests/Client/RequestDataTest.php diff --git a/src/Client/Client.php b/src/Client/Client.php index 62caed5f..c3fd4570 100644 --- a/src/Client/Client.php +++ b/src/Client/Client.php @@ -2,6 +2,7 @@ namespace React\Http\Client; +use Psr\Http\Message\RequestInterface; use React\EventLoop\LoopInterface; use React\Http\Io\ClientRequestStream; use React\Socket\Connector; @@ -24,10 +25,8 @@ public function __construct(LoopInterface $loop, ConnectorInterface $connector = } /** @return ClientRequestStream */ - public function request($method, $url, array $headers = array(), $protocolVersion = '1.0') + public function request(RequestInterface $request) { - $requestData = new RequestData($method, $url, $headers, $protocolVersion); - - return new ClientRequestStream($this->connector, $requestData); + return new ClientRequestStream($this->connector, $request); } } diff --git a/src/Client/RequestData.php b/src/Client/RequestData.php deleted file mode 100644 index 04bb4cad..00000000 --- a/src/Client/RequestData.php +++ /dev/null @@ -1,127 +0,0 @@ -method = $method; - $this->url = $url; - $this->headers = $headers; - $this->protocolVersion = $protocolVersion; - } - - private function mergeDefaultheaders(array $headers) - { - $port = ($this->getDefaultPort() === $this->getPort()) ? '' : ":{$this->getPort()}"; - $connectionHeaders = ('1.1' === $this->protocolVersion) ? array('Connection' => 'close') : array(); - $authHeaders = $this->getAuthHeaders(); - - $defaults = array_merge( - array( - 'Host' => $this->getHost().$port, - ), - $connectionHeaders, - $authHeaders - ); - - // remove all defaults that already exist in $headers - $lower = array_change_key_case($headers, CASE_LOWER); - foreach ($defaults as $key => $_) { - if (isset($lower[strtolower($key)])) { - unset($defaults[$key]); - } - } - - return array_merge($defaults, $headers); - } - - public function getScheme() - { - return parse_url($this->url, PHP_URL_SCHEME); - } - - public function getHost() - { - return parse_url($this->url, PHP_URL_HOST); - } - - public function getPort() - { - return (int) parse_url($this->url, PHP_URL_PORT) ?: $this->getDefaultPort(); - } - - public function getDefaultPort() - { - return ('https' === $this->getScheme()) ? 443 : 80; - } - - public function getPath() - { - $path = parse_url($this->url, PHP_URL_PATH); - $queryString = parse_url($this->url, PHP_URL_QUERY); - - // assume "/" path by default, but allow "OPTIONS *" - if ($path === null) { - $path = ($this->method === 'OPTIONS' && $queryString === null) ? '*': '/'; - } - if ($queryString !== null) { - $path .= '?' . $queryString; - } - - return $path; - } - - public function setProtocolVersion($version) - { - $this->protocolVersion = $version; - } - - public function __toString() - { - $headers = $this->mergeDefaultheaders($this->headers); - - $data = ''; - $data .= "{$this->method} {$this->getPath()} HTTP/{$this->protocolVersion}\r\n"; - foreach ($headers as $name => $values) { - foreach ((array)$values as $value) { - $data .= "$name: $value\r\n"; - } - } - $data .= "\r\n"; - - return $data; - } - - private function getUrlUserPass() - { - $components = parse_url($this->url); - - if (isset($components['user'])) { - return array( - 'user' => $components['user'], - 'pass' => isset($components['pass']) ? $components['pass'] : null, - ); - } - } - - private function getAuthHeaders() - { - if (null !== $auth = $this->getUrlUserPass()) { - return array( - 'Authorization' => 'Basic ' . base64_encode($auth['user'].':'.$auth['pass']), - ); - } - - return array(); - } -} diff --git a/src/Io/ClientRequestStream.php b/src/Io/ClientRequestStream.php index 2513a89a..29536e88 100644 --- a/src/Io/ClientRequestStream.php +++ b/src/Io/ClientRequestStream.php @@ -3,7 +3,7 @@ namespace React\Http\Io; use Evenement\EventEmitter; -use React\Http\Client\RequestData; +use Psr\Http\Message\RequestInterface; use React\Promise; use React\Socket\ConnectionInterface; use React\Socket\ConnectorInterface; @@ -24,10 +24,15 @@ class ClientRequestStream extends EventEmitter implements WritableStreamInterfac const STATE_HEAD_WRITTEN = 2; const STATE_END = 3; + /** @var ConnectorInterface */ private $connector; - private $requestData; + /** @var RequestInterface */ + private $request; + + /** @var ?ConnectionInterface */ private $stream; + private $buffer; private $responseFactory; private $state = self::STATE_INIT; @@ -35,10 +40,10 @@ class ClientRequestStream extends EventEmitter implements WritableStreamInterfac private $pendingWrites = ''; - public function __construct(ConnectorInterface $connector, RequestData $requestData) + public function __construct(ConnectorInterface $connector, RequestInterface $request) { $this->connector = $connector; - $this->requestData = $requestData; + $this->request = $request; } public function isWritable() @@ -50,7 +55,7 @@ private function writeHead() { $this->state = self::STATE_WRITING_HEAD; - $requestData = $this->requestData; + $request = $this->request; $streamRef = &$this->stream; $stateRef = &$this->state; $pendingWrites = &$this->pendingWrites; @@ -58,8 +63,9 @@ private function writeHead() $promise = $this->connect(); $promise->then( - function (ConnectionInterface $stream) use ($requestData, &$streamRef, &$stateRef, &$pendingWrites, $that) { + function (ConnectionInterface $stream) use ($request, &$streamRef, &$stateRef, &$pendingWrites, $that) { $streamRef = $stream; + assert($streamRef instanceof ConnectionInterface); $stream->on('drain', array($that, 'handleDrain')); $stream->on('data', array($that, 'handleData')); @@ -67,10 +73,17 @@ function (ConnectionInterface $stream) use ($requestData, &$streamRef, &$stateRe $stream->on('error', array($that, 'handleError')); $stream->on('close', array($that, 'handleClose')); - $headers = (string) $requestData; + assert($request instanceof RequestInterface); + $headers = "{$request->getMethod()} {$request->getRequestTarget()} HTTP/{$request->getProtocolVersion()}\r\n"; + foreach ($request->getHeaders() as $name => $values) { + foreach ($values as $value) { + $headers .= "$name: $value\r\n"; + } + } - $more = $stream->write($headers . $pendingWrites); + $more = $stream->write($headers . "\r\n" . $pendingWrites); + assert($stateRef === ClientRequestStream::STATE_WRITING_HEAD); $stateRef = ClientRequestStream::STATE_HEAD_WRITTEN; // clear pending writes if non-empty @@ -218,20 +231,24 @@ public function close() protected function connect() { - $scheme = $this->requestData->getScheme(); + $scheme = $this->request->getUri()->getScheme(); if ($scheme !== 'https' && $scheme !== 'http') { return Promise\reject( new \InvalidArgumentException('Invalid request URL given') ); } - $host = $this->requestData->getHost(); - $port = $this->requestData->getPort(); + $host = $this->request->getUri()->getHost(); + $port = $this->request->getUri()->getPort(); if ($scheme === 'https') { $host = 'tls://' . $host; } + if ($port === null) { + $port = $scheme === 'https' ? 443 : 80; + } + return $this->connector ->connect($host . ':' . $port); } diff --git a/src/Io/Sender.php b/src/Io/Sender.php index 2f04c797..2e821f5a 100644 --- a/src/Io/Sender.php +++ b/src/Io/Sender.php @@ -74,6 +74,9 @@ public function __construct(HttpClient $http) */ public function send(RequestInterface $request) { + // support HTTP/1.1 and HTTP/1.0 only, ensured by `Browser` already + assert(\in_array($request->getProtocolVersion(), array('1.0', '1.1'), true)); + $body = $request->getBody(); $size = $body->getSize(); @@ -91,12 +94,17 @@ public function send(RequestInterface $request) $size = 0; } - $headers = array(); - foreach ($request->getHeaders() as $name => $values) { - $headers[$name] = implode(', ', $values); + // automatically add `Connection: close` request header for HTTP/1.1 requests to avoid connection reuse + if ($request->getProtocolVersion() === '1.1' && !$request->hasHeader('Connection')) { + $request = $request->withHeader('Connection', 'close'); + } + + // automatically add `Authorization: Basic …` request header if URL includes `user:pass@host` + if ($request->getUri()->getUserInfo() !== '' && !$request->hasHeader('Authorization')) { + $request = $request->withHeader('Authorization', 'Basic ' . \base64_encode($request->getUri()->getUserInfo())); } - $requestStream = $this->http->request($request->getMethod(), (string)$request->getUri(), $headers, $request->getProtocolVersion()); + $requestStream = $this->http->request($request); $deferred = new Deferred(function ($_, $reject) use ($requestStream) { // close request stream if request is cancelled diff --git a/tests/Client/FunctionalIntegrationTest.php b/tests/Client/FunctionalIntegrationTest.php index d95bf828..90d8444b 100644 --- a/tests/Client/FunctionalIntegrationTest.php +++ b/tests/Client/FunctionalIntegrationTest.php @@ -5,6 +5,7 @@ use Psr\Http\Message\ResponseInterface; use React\EventLoop\Loop; use React\Http\Client\Client; +use React\Http\Message\Request; use React\Promise\Deferred; use React\Promise\Stream; use React\Socket\ConnectionInterface; @@ -45,7 +46,7 @@ public function testRequestToLocalhostEmitsSingleRemoteConnection() $port = parse_url($socket->getAddress(), PHP_URL_PORT); $client = new Client(Loop::get()); - $request = $client->request('GET', 'http://localhost:' . $port); + $request = $client->request(new Request('GET', 'http://localhost:' . $port, array(), '', '1.0')); $promise = Stream\first($request, 'close'); $request->end(); @@ -62,7 +63,7 @@ public function testRequestLegacyHttpServerWithOnlyLineFeedReturnsSuccessfulResp }); $client = new Client(Loop::get()); - $request = $client->request('GET', str_replace('tcp:', 'http:', $socket->getAddress())); + $request = $client->request(new Request('GET', str_replace('tcp:', 'http:', $socket->getAddress()), array(), '', '1.0')); $once = $this->expectCallableOnceWith('body'); $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($once) { @@ -83,7 +84,7 @@ public function testSuccessfulResponseEmitsEnd() $client = new Client(Loop::get()); - $request = $client->request('GET', 'http://www.google.com/'); + $request = $client->request(new Request('GET', 'http://www.google.com/', array(), '', '1.0')); $once = $this->expectCallableOnce(); $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($once) { @@ -109,7 +110,7 @@ public function testPostDataReturnsData() $client = new Client(Loop::get()); $data = str_repeat('.', 33000); - $request = $client->request('POST', 'https://' . (mt_rand(0, 1) === 0 ? 'eu.' : '') . 'httpbin.org/post', array('Content-Length' => strlen($data))); + $request = $client->request(new Request('POST', 'https://' . (mt_rand(0, 1) === 0 ? 'eu.' : '') . 'httpbin.org/post', array('Content-Length' => strlen($data)), '', '1.0')); $deferred = new Deferred(); $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($deferred) { @@ -141,7 +142,7 @@ public function testPostJsonReturnsData() $client = new Client(Loop::get()); $data = json_encode(array('numbers' => range(1, 50))); - $request = $client->request('POST', 'https://httpbin.org/post', array('Content-Length' => strlen($data), 'Content-Type' => 'application/json')); + $request = $client->request(new Request('POST', 'https://httpbin.org/post', array('Content-Length' => strlen($data), 'Content-Type' => 'application/json'), '', '1.0')); $deferred = new Deferred(); $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($deferred) { @@ -170,7 +171,7 @@ public function testCancelPendingConnectionEmitsClose() $client = new Client(Loop::get()); - $request = $client->request('GET', 'http://www.google.com/'); + $request = $client->request(new Request('GET', 'http://www.google.com/', array(), '', '1.0')); $request->on('error', $this->expectCallableNever()); $request->on('close', $this->expectCallableOnce()); $request->end(); diff --git a/tests/Client/RequestDataTest.php b/tests/Client/RequestDataTest.php deleted file mode 100644 index f6713e85..00000000 --- a/tests/Client/RequestDataTest.php +++ /dev/null @@ -1,146 +0,0 @@ -assertSame($expected, $requestData->__toString()); - } - - /** @test */ - public function toStringReturnsHTTPRequestMessageWithEmptyQueryString() - { - $requestData = new RequestData('GET', 'http://www.example.com/path?hello=world'); - - $expected = "GET /path?hello=world HTTP/1.0\r\n" . - "Host: www.example.com\r\n" . - "\r\n"; - - $this->assertSame($expected, $requestData->__toString()); - } - - /** @test */ - public function toStringReturnsHTTPRequestMessageWithZeroQueryStringAndRootPath() - { - $requestData = new RequestData('GET', 'http://www.example.com?0'); - - $expected = "GET /?0 HTTP/1.0\r\n" . - "Host: www.example.com\r\n" . - "\r\n"; - - $this->assertSame($expected, $requestData->__toString()); - } - - /** @test */ - public function toStringReturnsHTTPRequestMessageWithOptionsAbsoluteRequestForm() - { - $requestData = new RequestData('OPTIONS', 'http://www.example.com/'); - - $expected = "OPTIONS / HTTP/1.0\r\n" . - "Host: www.example.com\r\n" . - "\r\n"; - - $this->assertSame($expected, $requestData->__toString()); - } - - /** @test */ - public function toStringReturnsHTTPRequestMessageWithOptionsAsteriskRequestForm() - { - $requestData = new RequestData('OPTIONS', 'http://www.example.com'); - - $expected = "OPTIONS * HTTP/1.0\r\n" . - "Host: www.example.com\r\n" . - "\r\n"; - - $this->assertSame($expected, $requestData->__toString()); - } - - /** @test */ - public function toStringReturnsHTTPRequestMessageWithProtocolVersion() - { - $requestData = new RequestData('GET', 'http://www.example.com'); - $requestData->setProtocolVersion('1.1'); - - $expected = "GET / HTTP/1.1\r\n" . - "Host: www.example.com\r\n" . - "Connection: close\r\n" . - "\r\n"; - - $this->assertSame($expected, $requestData->__toString()); - } - - /** @test */ - public function toStringReturnsHTTPRequestMessageWithHeaders() - { - $requestData = new RequestData('GET', 'http://www.example.com', array( - 'User-Agent' => array(), - 'Via' => array( - 'first', - 'second' - ) - )); - - $expected = "GET / HTTP/1.0\r\n" . - "Host: www.example.com\r\n" . - "Via: first\r\n" . - "Via: second\r\n" . - "\r\n"; - - $this->assertSame($expected, $requestData->__toString()); - } - - /** @test */ - public function toStringReturnsHTTPRequestMessageWithHeadersInCustomCase() - { - $requestData = new RequestData('GET', 'http://www.example.com', array( - 'user-agent' => 'Hello', - 'LAST' => 'World' - )); - - $expected = "GET / HTTP/1.0\r\n" . - "Host: www.example.com\r\n" . - "user-agent: Hello\r\n" . - "LAST: World\r\n" . - "\r\n"; - - $this->assertSame($expected, $requestData->__toString()); - } - - /** @test */ - public function toStringReturnsHTTPRequestMessageWithProtocolVersionThroughConstructor() - { - $requestData = new RequestData('GET', 'http://www.example.com', array(), '1.1'); - - $expected = "GET / HTTP/1.1\r\n" . - "Host: www.example.com\r\n" . - "Connection: close\r\n" . - "\r\n"; - - $this->assertSame($expected, $requestData->__toString()); - } - - /** @test */ - public function toStringUsesUserPassFromURL() - { - $requestData = new RequestData('GET', 'http://john:dummy@www.example.com'); - - $expected = "GET / HTTP/1.0\r\n" . - "Host: www.example.com\r\n" . - "Authorization: Basic am9objpkdW1teQ==\r\n" . - "\r\n"; - - $this->assertSame($expected, $requestData->__toString()); - } -} diff --git a/tests/Io/ClientRequestStreamTest.php b/tests/Io/ClientRequestStreamTest.php index 6e3e16b8..07a4eb73 100644 --- a/tests/Io/ClientRequestStreamTest.php +++ b/tests/Io/ClientRequestStreamTest.php @@ -2,12 +2,11 @@ namespace React\Tests\Http\Io; -use React\Http\Client\RequestData; use React\Http\Io\ClientRequestStream; -use React\Stream\DuplexResourceStream; -use React\Promise\RejectedPromise; +use React\Http\Message\Request; use React\Promise\Deferred; use React\Promise\Promise; +use React\Stream\DuplexResourceStream; use React\Tests\Http\TestCase; class ClientRequestStreamTest extends TestCase @@ -31,7 +30,7 @@ public function setUpStream() /** @test */ public function requestShouldBindToStreamEventsAndUseconnector() { - $requestData = new RequestData('GET', 'http://www.example.com'); + $requestData = new Request('GET', 'http://www.example.com'); $request = new ClientRequestStream($this->connector, $requestData); $this->successfulConnectionMock(); @@ -66,7 +65,7 @@ public function requestShouldBindToStreamEventsAndUseconnector() */ public function requestShouldConnectViaTlsIfUrlUsesHttpsScheme() { - $requestData = new RequestData('GET', 'https://www.example.com'); + $requestData = new Request('GET', 'https://www.example.com'); $request = new ClientRequestStream($this->connector, $requestData); $this->connector->expects($this->once())->method('connect')->with('tls://www.example.com:443')->willReturn(new Promise(function () { })); @@ -77,7 +76,7 @@ public function requestShouldConnectViaTlsIfUrlUsesHttpsScheme() /** @test */ public function requestShouldEmitErrorIfConnectionFails() { - $requestData = new RequestData('GET', 'http://www.example.com'); + $requestData = new Request('GET', 'http://www.example.com'); $request = new ClientRequestStream($this->connector, $requestData); $this->connector->expects($this->once())->method('connect')->willReturn(\React\Promise\reject(new \RuntimeException())); @@ -93,7 +92,7 @@ public function requestShouldEmitErrorIfConnectionFails() /** @test */ public function requestShouldEmitErrorIfConnectionClosesBeforeResponseIsParsed() { - $requestData = new RequestData('GET', 'http://www.example.com'); + $requestData = new Request('GET', 'http://www.example.com'); $request = new ClientRequestStream($this->connector, $requestData); $this->successfulConnectionMock(); @@ -110,7 +109,7 @@ public function requestShouldEmitErrorIfConnectionClosesBeforeResponseIsParsed() /** @test */ public function requestShouldEmitErrorIfConnectionEmitsError() { - $requestData = new RequestData('GET', 'http://www.example.com'); + $requestData = new Request('GET', 'http://www.example.com'); $request = new ClientRequestStream($this->connector, $requestData); $this->successfulConnectionMock(); @@ -127,7 +126,7 @@ public function requestShouldEmitErrorIfConnectionEmitsError() /** @test */ public function requestShouldEmitErrorIfRequestParserThrowsException() { - $requestData = new RequestData('GET', 'http://www.example.com'); + $requestData = new Request('GET', 'http://www.example.com'); $request = new ClientRequestStream($this->connector, $requestData); $this->successfulConnectionMock(); @@ -143,7 +142,7 @@ public function requestShouldEmitErrorIfRequestParserThrowsException() */ public function requestShouldEmitErrorIfUrlIsInvalid() { - $requestData = new RequestData('GET', 'ftp://www.example.com'); + $requestData = new Request('GET', 'ftp://www.example.com'); $request = new ClientRequestStream($this->connector, $requestData); $request->on('error', $this->expectCallableOnceWith($this->isInstanceOf('InvalidArgumentException'))); @@ -159,7 +158,7 @@ public function requestShouldEmitErrorIfUrlIsInvalid() */ public function requestShouldEmitErrorIfUrlHasNoScheme() { - $requestData = new RequestData('GET', 'www.example.com'); + $requestData = new Request('GET', 'www.example.com'); $request = new ClientRequestStream($this->connector, $requestData); $request->on('error', $this->expectCallableOnceWith($this->isInstanceOf('InvalidArgumentException'))); @@ -170,10 +169,50 @@ public function requestShouldEmitErrorIfUrlHasNoScheme() $request->end(); } + /** @test */ + public function getRequestShouldSendAGetRequest() + { + $requestData = new Request('GET', 'http://www.example.com', array(), '', '1.0'); + $request = new ClientRequestStream($this->connector, $requestData); + + $this->successfulConnectionMock(); + + $this->stream->expects($this->once())->method('write')->with("GET / HTTP/1.0\r\nHost: www.example.com\r\n\r\n"); + + $request->end(); + } + + /** @test */ + public function getHttp11RequestShouldSendAGetRequestWithGivenConnectionCloseHeader() + { + $requestData = new Request('GET', 'http://www.example.com', array('Connection' => 'close'), '', '1.1'); + $request = new ClientRequestStream($this->connector, $requestData); + + $this->successfulConnectionMock(); + + $this->stream->expects($this->once())->method('write')->with("GET / HTTP/1.1\r\nHost: www.example.com\r\nConnection: close\r\n\r\n"); + + $request->end(); + } + + /** @test */ + public function getOptionsAsteriskShouldSendAOptionsRequestAsteriskRequestTarget() + { + $requestData = new Request('OPTIONS', 'http://www.example.com', array('Connection' => 'close'), '', '1.1'); + $requestData = $requestData->withRequestTarget('*'); + $request = new ClientRequestStream($this->connector, $requestData); + + $this->successfulConnectionMock(); + + $this->stream->expects($this->once())->method('write')->with("OPTIONS * HTTP/1.1\r\nHost: www.example.com\r\nConnection: close\r\n\r\n"); + + $request->end(); + } + /** @test */ public function postRequestShouldSendAPostRequest() { - $requestData = new RequestData('POST', 'http://www.example.com'); + $requestData = new Request('POST', 'http://www.example.com', array(), '', '1.0'); $request = new ClientRequestStream($this->connector, $requestData); $this->successfulConnectionMock(); @@ -193,7 +232,7 @@ public function postRequestShouldSendAPostRequest() /** @test */ public function writeWithAPostRequestShouldSendToTheStream() { - $requestData = new RequestData('POST', 'http://www.example.com'); + $requestData = new Request('POST', 'http://www.example.com', array(), '', '1.0'); $request = new ClientRequestStream($this->connector, $requestData); $this->successfulConnectionMock(); @@ -216,7 +255,7 @@ public function writeWithAPostRequestShouldSendToTheStream() /** @test */ public function writeWithAPostRequestShouldSendBodyAfterHeadersAndEmitDrainEvent() { - $requestData = new RequestData('POST', 'http://www.example.com'); + $requestData = new Request('POST', 'http://www.example.com', array(), '', '1.0'); $request = new ClientRequestStream($this->connector, $requestData); $resolveConnection = $this->successfulAsyncConnectionMock(); @@ -247,7 +286,7 @@ public function writeWithAPostRequestShouldSendBodyAfterHeadersAndEmitDrainEvent /** @test */ public function writeWithAPostRequestShouldForwardDrainEventIfFirstChunkExceedsBuffer() { - $requestData = new RequestData('POST', 'http://www.example.com'); + $requestData = new Request('POST', 'http://www.example.com', array(), '', '1.0'); $request = new ClientRequestStream($this->connector, $requestData); $this->stream = $this->getMockBuilder('React\Socket\Connection') @@ -284,7 +323,7 @@ public function writeWithAPostRequestShouldForwardDrainEventIfFirstChunkExceedsB /** @test */ public function pipeShouldPipeDataIntoTheRequestBody() { - $requestData = new RequestData('POST', 'http://www.example.com'); + $requestData = new Request('POST', 'http://www.example.com', array(), '', '1.0'); $request = new ClientRequestStream($this->connector, $requestData); $this->successfulConnectionMock(); @@ -317,7 +356,7 @@ public function pipeShouldPipeDataIntoTheRequestBody() */ public function writeShouldStartConnecting() { - $requestData = new RequestData('POST', 'http://www.example.com'); + $requestData = new Request('POST', 'http://www.example.com'); $request = new ClientRequestStream($this->connector, $requestData); $this->connector->expects($this->once()) @@ -333,7 +372,7 @@ public function writeShouldStartConnecting() */ public function endShouldStartConnectingAndChangeStreamIntoNonWritableMode() { - $requestData = new RequestData('POST', 'http://www.example.com'); + $requestData = new Request('POST', 'http://www.example.com'); $request = new ClientRequestStream($this->connector, $requestData); $this->connector->expects($this->once()) @@ -351,7 +390,7 @@ public function endShouldStartConnectingAndChangeStreamIntoNonWritableMode() */ public function closeShouldEmitCloseEvent() { - $requestData = new RequestData('POST', 'http://www.example.com'); + $requestData = new Request('POST', 'http://www.example.com'); $request = new ClientRequestStream($this->connector, $requestData); $request->on('close', $this->expectCallableOnce()); @@ -363,7 +402,7 @@ public function closeShouldEmitCloseEvent() */ public function writeAfterCloseReturnsFalse() { - $requestData = new RequestData('POST', 'http://www.example.com'); + $requestData = new Request('POST', 'http://www.example.com'); $request = new ClientRequestStream($this->connector, $requestData); $request->close(); @@ -377,7 +416,7 @@ public function writeAfterCloseReturnsFalse() */ public function endAfterCloseIsNoOp() { - $requestData = new RequestData('POST', 'http://www.example.com'); + $requestData = new Request('POST', 'http://www.example.com'); $request = new ClientRequestStream($this->connector, $requestData); $this->connector->expects($this->never()) @@ -392,7 +431,7 @@ public function endAfterCloseIsNoOp() */ public function closeShouldCancelPendingConnectionAttempt() { - $requestData = new RequestData('POST', 'http://www.example.com'); + $requestData = new Request('POST', 'http://www.example.com'); $request = new ClientRequestStream($this->connector, $requestData); $promise = new Promise(function () {}, function () { @@ -416,7 +455,7 @@ public function closeShouldCancelPendingConnectionAttempt() /** @test */ public function requestShouldRemoveAllListenerAfterClosed() { - $requestData = new RequestData('GET', 'http://www.example.com'); + $requestData = new Request('GET', 'http://www.example.com'); $request = new ClientRequestStream($this->connector, $requestData); $request->on('close', function () {}); @@ -450,7 +489,7 @@ private function successfulAsyncConnectionMock() /** @test */ public function multivalueHeader() { - $requestData = new RequestData('GET', 'http://www.example.com'); + $requestData = new Request('GET', 'http://www.example.com'); $request = new ClientRequestStream($this->connector, $requestData); $this->successfulConnectionMock(); diff --git a/tests/Io/SenderTest.php b/tests/Io/SenderTest.php index 6d8c3b5f..c2357a1a 100644 --- a/tests/Io/SenderTest.php +++ b/tests/Io/SenderTest.php @@ -2,8 +2,8 @@ namespace React\Tests\Http\Io; +use Psr\Http\Message\RequestInterface; use React\Http\Client\Client as HttpClient; -use React\Http\Client\RequestData; use React\Http\Io\ReadableBodyStream; use React\Http\Io\Sender; use React\Http\Message\Request; @@ -71,12 +71,9 @@ public function testSenderConnectorRejection() public function testSendPostWillAutomaticallySendContentLengthHeader() { $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); - $client->expects($this->once())->method('request')->with( - 'POST', - 'http://www.google.com/', - array('Host' => 'www.google.com', 'Content-Length' => '5'), - '1.1' - )->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock()); + $client->expects($this->once())->method('request')->with($this->callback(function (RequestInterface $request) { + return $request->getHeaderLine('Content-Length') === '5'; + }))->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock()); $sender = new Sender($client); @@ -87,12 +84,9 @@ public function testSendPostWillAutomaticallySendContentLengthHeader() public function testSendPostWillAutomaticallySendContentLengthZeroHeaderForEmptyRequestBody() { $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); - $client->expects($this->once())->method('request')->with( - 'POST', - 'http://www.google.com/', - array('Host' => 'www.google.com', 'Content-Length' => '0'), - '1.1' - )->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock()); + $client->expects($this->once())->method('request')->with($this->callback(function (RequestInterface $request) { + return $request->getHeaderLine('Content-Length') === '0'; + }))->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock()); $sender = new Sender($client); @@ -106,12 +100,9 @@ public function testSendPostStreamWillAutomaticallySendTransferEncodingChunked() $outgoing->expects($this->once())->method('write')->with(""); $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); - $client->expects($this->once())->method('request')->with( - 'POST', - 'http://www.google.com/', - array('Host' => 'www.google.com', 'Transfer-Encoding' => 'chunked'), - '1.1' - )->willReturn($outgoing); + $client->expects($this->once())->method('request')->with($this->callback(function (RequestInterface $request) { + return $request->getHeaderLine('Transfer-Encoding') === 'chunked'; + }))->willReturn($outgoing); $sender = new Sender($client); @@ -247,12 +238,9 @@ public function testSendPostStreamWillNotRejectWhenRequestBodyClosesAfterEnd() public function testSendPostStreamWithExplicitContentLengthWillSendHeaderAsIs() { $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); - $client->expects($this->once())->method('request')->with( - 'POST', - 'http://www.google.com/', - array('Host' => 'www.google.com', 'Content-Length' => '100'), - '1.1' - )->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock()); + $client->expects($this->once())->method('request')->with($this->callback(function (RequestInterface $request) { + return $request->getHeaderLine('Content-Length') === '100'; + }))->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock()); $sender = new Sender($client); @@ -264,12 +252,9 @@ public function testSendPostStreamWithExplicitContentLengthWillSendHeaderAsIs() public function testSendGetWillNotPassContentLengthHeaderForEmptyRequestBody() { $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); - $client->expects($this->once())->method('request')->with( - 'GET', - 'http://www.google.com/', - array('Host' => 'www.google.com'), - '1.1' - )->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock()); + $client->expects($this->once())->method('request')->with($this->callback(function (RequestInterface $request) { + return !$request->hasHeader('Content-Length'); + }))->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock()); $sender = new Sender($client); @@ -280,12 +265,9 @@ public function testSendGetWillNotPassContentLengthHeaderForEmptyRequestBody() public function testSendCustomMethodWillNotPassContentLengthHeaderForEmptyRequestBody() { $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); - $client->expects($this->once())->method('request')->with( - 'CUSTOM', - 'http://www.google.com/', - array('Host' => 'www.google.com'), - '1.1' - )->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock()); + $client->expects($this->once())->method('request')->with($this->callback(function (RequestInterface $request) { + return !$request->hasHeader('Content-Length'); + }))->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock()); $sender = new Sender($client); @@ -296,12 +278,9 @@ public function testSendCustomMethodWillNotPassContentLengthHeaderForEmptyReques public function testSendCustomMethodWithExplicitContentLengthZeroWillBePassedAsIs() { $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); - $client->expects($this->once())->method('request')->with( - 'CUSTOM', - 'http://www.google.com/', - array('Host' => 'www.google.com', 'Content-Length' => '0'), - '1.1' - )->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock()); + $client->expects($this->once())->method('request')->with($this->callback(function (RequestInterface $request) { + return $request->getHeaderLine('Content-Length') === '0'; + }))->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock()); $sender = new Sender($client); @@ -309,6 +288,76 @@ public function testSendCustomMethodWithExplicitContentLengthZeroWillBePassedAsI $sender->send($request); } + /** @test */ + public function getHttp10RequestShouldSendAGetRequestWithoutConnectionHeaderByDefault() + { + $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); + $client->expects($this->once())->method('request')->with($this->callback(function (RequestInterface $request) { + return !$request->hasHeader('Connection'); + }))->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock()); + + $sender = new Sender($client); + + $request = new Request('GET', 'http://www.example.com', array(), '', '1.0'); + $sender->send($request); + } + + /** @test */ + public function getHttp11RequestShouldSendAGetRequestWithConnectionCloseHeaderByDefault() + { + $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); + $client->expects($this->once())->method('request')->with($this->callback(function (RequestInterface $request) { + return $request->getHeaderLine('Connection') === 'close'; + }))->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock()); + + $sender = new Sender($client); + + $request = new Request('GET', 'http://www.example.com', array(), '', '1.1'); + $sender->send($request); + } + + /** @test */ + public function getHttp11RequestShouldSendAGetRequestWithGivenConnectionUpgradeHeader() + { + $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); + $client->expects($this->once())->method('request')->with($this->callback(function (RequestInterface $request) { + return $request->getHeaderLine('Connection') === 'upgrade'; + }))->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock()); + + $sender = new Sender($client); + + $request = new Request('GET', 'http://www.example.com', array('Connection' => 'upgrade'), '', '1.1'); + $sender->send($request); + } + + /** @test */ + public function getRequestWithUserAndPassShouldSendAGetRequestWithBasicAuthorizationHeader() + { + $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); + $client->expects($this->once())->method('request')->with($this->callback(function (RequestInterface $request) { + return $request->getHeaderLine('Authorization') === 'Basic am9objpkdW1teQ=='; + }))->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock()); + + $sender = new Sender($client); + + $request = new Request('GET', 'http://john:dummy@www.example.com'); + $sender->send($request); + } + + /** @test */ + public function getRequestWithUserAndPassShouldSendAGetRequestWithGivenAuthorizationHeaderBasicAuthorizationHeader() + { + $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); + $client->expects($this->once())->method('request')->with($this->callback(function (RequestInterface $request) { + return $request->getHeaderLine('Authorization') === 'bearer abc123'; + }))->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock()); + + $sender = new Sender($client); + + $request = new Request('GET', 'http://john:dummy@www.example.com', array('Authorization' => 'bearer abc123')); + $sender->send($request); + } + public function testCancelRequestWillCancelConnector() { $promise = new \React\Promise\Promise(function () { }, function () { @@ -355,54 +404,4 @@ public function testCancelRequestWillCloseConnection() $this->assertInstanceOf('RuntimeException', $exception); } - - public function provideRequestProtocolVersion() - { - return array( - array( - new Request('GET', 'http://www.google.com/'), - 'GET', - 'http://www.google.com/', - array( - 'Host' => 'www.google.com', - ), - '1.1', - ), - array( - new Request('GET', 'http://www.google.com/', array(), '', '1.0'), - 'GET', - 'http://www.google.com/', - array( - 'Host' => 'www.google.com', - ), - '1.0', - ), - ); - } - - /** - * @dataProvider provideRequestProtocolVersion - */ - public function testRequestProtocolVersion(Request $Request, $method, $uri, $headers, $protocolVersion) - { - $http = $this->getMockBuilder('React\Http\Client\Client') - ->setMethods(array( - 'request', - )) - ->setConstructorArgs(array( - $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(), - ))->getMock(); - - $request = $this->getMockBuilder('React\Http\Io\ClientRequestStream') - ->setMethods(array()) - ->setConstructorArgs(array( - $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(), - new RequestData($method, $uri, $headers, $protocolVersion), - ))->getMock(); - - $http->expects($this->once())->method('request')->with($method, $uri, $headers, $protocolVersion)->willReturn($request); - - $sender = new Sender($http); - $sender->send($Request); - } } From bafa2afaacc813ff2bef8a9e5066adf72876d617 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 3 Sep 2022 11:44:55 +0200 Subject: [PATCH 418/456] Include buffer logic to avoid dependency on reactphp/promise-stream --- composer.json | 2 +- src/Io/Transaction.php | 68 +++++--- .../RequestBodyBufferMiddleware.php | 75 ++++++-- tests/Io/TransactionTest.php | 94 +++++++++- .../RequestBodyBufferMiddlewareTest.php | 164 ++++++++++++++++-- 5 files changed, 346 insertions(+), 57 deletions(-) diff --git a/composer.json b/composer.json index aeee592b..59736ddd 100644 --- a/composer.json +++ b/composer.json @@ -32,7 +32,6 @@ "psr/http-message": "^1.0", "react/event-loop": "^1.2", "react/promise": "^3 || ^2.3 || ^1.2.1", - "react/promise-stream": "^1.4", "react/socket": "^1.12", "react/stream": "^1.2", "ringcentral/psr7": "^1.2" @@ -43,6 +42,7 @@ "clue/socks-react": "^1.4", "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35", "react/async": "^4 || ^3 || ^2", + "react/promise-stream": "^1.4", "react/promise-timer": "^1.9" }, "autoload": { diff --git a/src/Io/Transaction.php b/src/Io/Transaction.php index cbf8f3eb..bfa42241 100644 --- a/src/Io/Transaction.php +++ b/src/Io/Transaction.php @@ -9,6 +9,7 @@ use React\Http\Message\Response; use React\Http\Message\ResponseException; use React\Promise\Deferred; +use React\Promise\Promise; use React\Promise\PromiseInterface; use React\Stream\ReadableStreamInterface; use RingCentral\Psr7\Uri; @@ -165,46 +166,67 @@ function (ResponseInterface $response) use ($request, $that, $deferred, $state) */ public function bufferResponse(ResponseInterface $response, Deferred $deferred, ClientRequestState $state) { - $stream = $response->getBody(); + $body = $response->getBody(); + $size = $body->getSize(); - $size = $stream->getSize(); if ($size !== null && $size > $this->maximumSize) { - $stream->close(); + $body->close(); return \React\Promise\reject(new \OverflowException( 'Response body size of ' . $size . ' bytes exceeds maximum of ' . $this->maximumSize . ' bytes', - \defined('SOCKET_EMSGSIZE') ? \SOCKET_EMSGSIZE : 0 + \defined('SOCKET_EMSGSIZE') ? \SOCKET_EMSGSIZE : 90 )); } // body is not streaming => already buffered - if (!$stream instanceof ReadableStreamInterface) { + if (!$body instanceof ReadableStreamInterface) { return \React\Promise\resolve($response); } - // buffer stream and resolve with buffered body + /** @var ?\Closure $closer */ + $closer = null; $maximumSize = $this->maximumSize; - $promise = \React\Promise\Stream\buffer($stream, $maximumSize)->then( - function ($body) use ($response) { - return $response->withBody(new BufferedBody($body)); - }, - function ($e) use ($stream, $maximumSize) { - // try to close stream if buffering fails (or is cancelled) - $stream->close(); - if ($e instanceof \OverflowException) { - $e = new \OverflowException( + return $state->pending = new Promise(function ($resolve, $reject) use ($body, $maximumSize, $response, &$closer) { + // resolve with current buffer when stream closes successfully + $buffer = ''; + $body->on('close', $closer = function () use (&$buffer, $response, $maximumSize, $resolve, $reject) { + $resolve($response->withBody(new BufferedBody($buffer))); + }); + + // buffer response body data in memory + $body->on('data', function ($data) use (&$buffer, $maximumSize, $body, $closer, $reject) { + $buffer .= $data; + + // close stream and reject promise if limit is exceeded + if (isset($buffer[$maximumSize])) { + $buffer = ''; + assert($closer instanceof \Closure); + $body->removeListener('close', $closer); + $body->close(); + + $reject(new \OverflowException( 'Response body size exceeds maximum of ' . $maximumSize . ' bytes', - \defined('SOCKET_EMSGSIZE') ? \SOCKET_EMSGSIZE : 0 - ); + \defined('SOCKET_EMSGSIZE') ? \SOCKET_EMSGSIZE : 90 + )); } + }); - throw $e; - } - ); - - $state->pending = $promise; + // reject buffering if body emits error + $body->on('error', function (\Exception $e) use ($reject) { + $reject(new \RuntimeException( + 'Error while buffering response body: ' . $e->getMessage(), + $e->getCode(), + $e + )); + }); + }, function () use ($body, &$closer) { + // cancelled buffering: remove close handler to avoid resolving, then close and reject + assert($closer instanceof \Closure); + $body->removeListener('close', $closer); + $body->close(); - return $promise; + throw new \RuntimeException('Cancelled buffering response body'); + }); } /** diff --git a/src/Middleware/RequestBodyBufferMiddleware.php b/src/Middleware/RequestBodyBufferMiddleware.php index c13a5dec..ddb39f5e 100644 --- a/src/Middleware/RequestBodyBufferMiddleware.php +++ b/src/Middleware/RequestBodyBufferMiddleware.php @@ -6,7 +6,7 @@ use Psr\Http\Message\ServerRequestInterface; use React\Http\Io\BufferedBody; use React\Http\Io\IniUtil; -use React\Promise\Stream; +use React\Promise\Promise; use React\Stream\ReadableStreamInterface; final class RequestBodyBufferMiddleware @@ -29,19 +29,19 @@ public function __construct($sizeLimit = null) $this->sizeLimit = IniUtil::iniSizeToBytes($sizeLimit); } - public function __invoke(ServerRequestInterface $request, $stack) + public function __invoke(ServerRequestInterface $request, $next) { $body = $request->getBody(); $size = $body->getSize(); // happy path: skip if body is known to be empty (or is already buffered) - if ($size === 0 || !$body instanceof ReadableStreamInterface) { + if ($size === 0 || !$body instanceof ReadableStreamInterface || !$body->isReadable()) { // replace with empty body if body is streaming (or buffered size exceeds limit) if ($body instanceof ReadableStreamInterface || $size > $this->sizeLimit) { $request = $request->withBody(new BufferedBody('')); } - return $stack($request); + return $next($request); } // request body of known size exceeding limit @@ -50,21 +50,60 @@ public function __invoke(ServerRequestInterface $request, $stack) $sizeLimit = 0; } - return Stream\buffer($body, $sizeLimit)->then(function ($buffer) use ($request, $stack) { - $request = $request->withBody(new BufferedBody($buffer)); - - return $stack($request); - }, function ($error) use ($stack, $request, $body) { - // On buffer overflow keep the request body stream in, - // but ignore the contents and wait for the close event - // before passing the request on to the next middleware. - if ($error instanceof OverflowException) { - return Stream\first($body, 'close')->then(function () use ($stack, $request) { - return $stack($request); - }); - } + /** @var ?\Closure $closer */ + $closer = null; + + return new Promise(function ($resolve, $reject) use ($body, &$closer, $sizeLimit, $request, $next) { + // buffer request body data in memory, discard but keep buffering if limit is reached + $buffer = ''; + $bufferer = null; + $body->on('data', $bufferer = function ($data) use (&$buffer, $sizeLimit, $body, &$bufferer) { + $buffer .= $data; + + // On buffer overflow keep the request body stream in, + // but ignore the contents and wait for the close event + // before passing the request on to the next middleware. + if (isset($buffer[$sizeLimit])) { + assert($bufferer instanceof \Closure); + $body->removeListener('data', $bufferer); + $bufferer = null; + $buffer = ''; + } + }); + + // call $next with current buffer and resolve or reject with its results + $body->on('close', $closer = function () use (&$buffer, $request, $resolve, $reject, $next) { + try { + // resolve with result of next handler + $resolve($next($request->withBody(new BufferedBody($buffer)))); + } catch (\Exception $e) { + $reject($e); + } catch (\Throwable $e) { // @codeCoverageIgnoreStart + // reject Errors just like Exceptions (PHP 7+) + $reject($e); // @codeCoverageIgnoreEnd + } + }); + + // reject buffering if body emits error + $body->on('error', function (\Exception $e) use ($reject, $body, $closer) { + // remove close handler to avoid resolving, then close and reject + assert($closer instanceof \Closure); + $body->removeListener('close', $closer); + $body->close(); + + $reject(new \RuntimeException( + 'Error while buffering request body: ' . $e->getMessage(), + $e->getCode(), + $e + )); + }); + }, function () use ($body, &$closer) { + // cancelled buffering: remove close handler to avoid resolving, then close and reject + assert($closer instanceof \Closure); + $body->removeListener('close', $closer); + $body->close(); - throw $error; + throw new \RuntimeException('Cancelled buffering request body'); }); } } diff --git a/tests/Io/TransactionTest.php b/tests/Io/TransactionTest.php index e0d04e39..140c53e0 100644 --- a/tests/Io/TransactionTest.php +++ b/tests/Io/TransactionTest.php @@ -406,7 +406,7 @@ public function testReceivingStreamingBodyWillResolveWithBufferedResponseByDefau $this->assertEquals('hello world', (string)$response->getBody()); } - public function testReceivingStreamingBodyWithSizeExceedingMaximumResponseBufferWillRejectAndCloseResponseStream() + public function testReceivingStreamingBodyWithContentLengthExceedingMaximumResponseBufferWillRejectAndCloseResponseStreamImmediately() { $stream = new ThroughStream(); $stream->on('close', $this->expectCallableOnce()); @@ -419,11 +419,87 @@ public function testReceivingStreamingBodyWithSizeExceedingMaximumResponseBuffer $sender = $this->makeSenderMock(); $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(Promise\resolve($response)); + $transaction = new Transaction($sender, Loop::get()); + + $promise = $transaction->send($request); + + $exception = null; + $promise->then(null, function ($e) use (&$exception) { + $exception = $e; + }); + + $this->assertFalse($stream->isWritable()); + + assert($exception instanceof \OverflowException); + $this->assertInstanceOf('OverflowException', $exception); + $this->assertEquals('Response body size of 100000000 bytes exceeds maximum of 16777216 bytes', $exception->getMessage()); + $this->assertEquals(defined('SOCKET_EMSGSIZE') ? \SOCKET_EMSGSIZE : 90, $exception->getCode()); + $this->assertNull($exception->getPrevious()); + } + + public function testReceivingStreamingBodyWithContentsExceedingMaximumResponseBufferWillRejectAndCloseResponseStreamWhenBufferExceedsLimit() + { + $stream = new ThroughStream(); + $stream->on('close', $this->expectCallableOnce()); + + $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); + + $response = new Response(200, array(), new ReadableBodyStream($stream)); + + // mock sender to resolve promise with the given $response in response to the given $request + $sender = $this->makeSenderMock(); + $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(Promise\resolve($response)); + + $transaction = new Transaction($sender, Loop::get()); + $transaction = $transaction->withOptions(array('maximumSize' => 10)); + $promise = $transaction->send($request); + + $exception = null; + $promise->then(null, function ($e) use (&$exception) { + $exception = $e; + }); + + $this->assertTrue($stream->isWritable()); + $stream->write('hello wörld'); + $this->assertFalse($stream->isWritable()); + + assert($exception instanceof \OverflowException); + $this->assertInstanceOf('OverflowException', $exception); + $this->assertEquals('Response body size exceeds maximum of 10 bytes', $exception->getMessage()); + $this->assertEquals(defined('SOCKET_EMSGSIZE') ? \SOCKET_EMSGSIZE : 90, $exception->getCode()); + $this->assertNull($exception->getPrevious()); + } + + public function testReceivingStreamingBodyWillRejectWhenStreamEmitsError() + { + $stream = new ThroughStream(function ($data) { + throw new \UnexpectedValueException('Unexpected ' . $data, 42); + }); + + $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); + $response = new Response(200, array(), new ReadableBodyStream($stream)); + + // mock sender to resolve promise with the given $response in response to the given $request + $sender = $this->makeSenderMock(); + $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(Promise\resolve($response)); + $transaction = new Transaction($sender, Loop::get()); $promise = $transaction->send($request); - $this->setExpectedException('OverflowException'); - \React\Async\await(\React\Promise\Timer\timeout($promise, 0.001)); + $exception = null; + $promise->then(null, function ($e) use (&$exception) { + $exception = $e; + }); + + $this->assertTrue($stream->isWritable()); + $stream->write('Foo'); + $this->assertFalse($stream->isWritable()); + + assert($exception instanceof \RuntimeException); + $this->assertInstanceOf('RuntimeException', $exception); + $this->assertEquals('Error while buffering response body: Unexpected Foo', $exception->getMessage()); + $this->assertEquals(42, $exception->getCode()); + $this->assertInstanceOf('UnexpectedValueException', $exception->getPrevious()); } public function testCancelBufferingResponseWillCloseStreamAndReject() @@ -446,8 +522,16 @@ public function testCancelBufferingResponseWillCloseStreamAndReject() $deferred->resolve($response); $promise->cancel(); - $this->setExpectedException('RuntimeException'); - \React\Async\await(\React\Promise\Timer\timeout($promise, 0.001)); + $exception = null; + $promise->then(null, function ($e) use (&$exception) { + $exception = $e; + }); + + assert($exception instanceof \RuntimeException); + $this->assertInstanceOf('RuntimeException', $exception); + $this->assertEquals('Cancelled buffering response body', $exception->getMessage()); + $this->assertEquals(0, $exception->getCode()); + $this->assertNull($exception->getPrevious()); } public function testReceivingStreamingBodyWillResolveWithStreamingResponseIfStreamingIsEnabled() diff --git a/tests/Middleware/RequestBodyBufferMiddlewareTest.php b/tests/Middleware/RequestBodyBufferMiddlewareTest.php index 0edec7da..fd818a8c 100644 --- a/tests/Middleware/RequestBodyBufferMiddlewareTest.php +++ b/tests/Middleware/RequestBodyBufferMiddlewareTest.php @@ -115,10 +115,11 @@ function (ServerRequestInterface $request) use (&$exposedRequest) { $this->assertSame($body, $exposedRequest->getBody()); } - public function testKnownExcessiveSizedBodyIsDisgardedTheRequestIsPassedDownToTheNextMiddleware() + public function testClosedStreamResolvesImmediatelyWithEmptyBody() { $stream = new ThroughStream(); - $stream->end('aa'); + $stream->close(); + $serverRequest = new ServerRequest( 'GET', 'https://example.com/', @@ -126,13 +127,41 @@ public function testKnownExcessiveSizedBodyIsDisgardedTheRequestIsPassedDownToTh new HttpBodyStream($stream, 2) ); + $exposedRequest = null; $buffer = new RequestBodyBufferMiddleware(1); - $response = \React\Async\await($buffer( + $buffer( + $serverRequest, + function (ServerRequestInterface $request) use (&$exposedRequest) { + $exposedRequest = $request; + } + ); + + $this->assertSame(0, $exposedRequest->getBody()->getSize()); + $this->assertSame('', $exposedRequest->getBody()->getContents()); + } + + public function testKnownExcessiveSizedBodyIsDiscardedAndRequestIsPassedDownToTheNextMiddleware() + { + $stream = new ThroughStream(); + $serverRequest = new ServerRequest( + 'GET', + 'https://example.com/', + array(), + new HttpBodyStream($stream, 2) + ); + + $buffer = new RequestBodyBufferMiddleware(1); + + $promise = $buffer( $serverRequest, function (ServerRequestInterface $request) { return new Response(200, array(), $request->getBody()->getContents()); } - )); + ); + + $stream->end('aa'); + + $response = \React\Async\await($promise); $this->assertSame(200, $response->getStatusCode()); $this->assertSame('', $response->getBody()->getContents()); @@ -214,9 +243,10 @@ function (ServerRequestInterface $request) { $this->assertSame('', $exposedResponse->getBody()->getContents()); } - public function testBufferingErrorThrows() + public function testBufferingRejectsWhenNextHandlerThrowsWhenStreamEnds() { $stream = new ThroughStream(); + $serverRequest = new ServerRequest( 'GET', 'https://example.com/', @@ -224,18 +254,101 @@ public function testBufferingErrorThrows() new HttpBodyStream($stream, null) ); - $buffer = new RequestBodyBufferMiddleware(1); + $buffer = new RequestBodyBufferMiddleware(100); $promise = $buffer( $serverRequest, function (ServerRequestInterface $request) { - return $request; + throw new \RuntimeException('Buffered ' . $request->getBody()->getSize(), 42); } ); - $stream->emit('error', array(new \RuntimeException())); + $exception = null; + $promise->then(null, function ($e) use (&$exception) { + $exception = $e; + }); + + $this->assertTrue($stream->isWritable()); + $stream->end('Foo'); + $this->assertFalse($stream->isWritable()); - $this->setExpectedException('RuntimeException'); - \React\Async\await($promise); + assert($exception instanceof \RuntimeException); + $this->assertInstanceOf('RuntimeException', $exception); + $this->assertEquals('Buffered 3', $exception->getMessage()); + $this->assertEquals(42, $exception->getCode()); + $this->assertNull($exception->getPrevious()); + } + + /** + * @requires PHP 7 + */ + public function testBufferingRejectsWhenNextHandlerThrowsErrorWhenStreamEnds() + { + $stream = new ThroughStream(); + + $serverRequest = new ServerRequest( + 'GET', + 'https://example.com/', + array(), + new HttpBodyStream($stream, null) + ); + + $buffer = new RequestBodyBufferMiddleware(100); + $promise = $buffer( + $serverRequest, + function (ServerRequestInterface $request) { + throw new \Error('Buffered ' . $request->getBody()->getSize(), 42); + } + ); + + $exception = null; + $promise->then(null, function ($e) use (&$exception) { + $exception = $e; + }); + + $this->assertTrue($stream->isWritable()); + $stream->end('Foo'); + $this->assertFalse($stream->isWritable()); + + assert($exception instanceof \Error); + $this->assertInstanceOf('Error', $exception); + $this->assertEquals('Buffered 3', $exception->getMessage()); + $this->assertEquals(42, $exception->getCode()); + $this->assertNull($exception->getPrevious()); + } + + public function testBufferingRejectsWhenStreamEmitsError() + { + $stream = new ThroughStream(function ($data) { + throw new \UnexpectedValueException('Unexpected ' . $data, 42); + }); + + $serverRequest = new ServerRequest( + 'GET', + 'https://example.com/', + array(), + new HttpBodyStream($stream, null) + ); + + $buffer = new RequestBodyBufferMiddleware(1); + $promise = $buffer( + $serverRequest, + $this->expectCallableNever() + ); + + $exception = null; + $promise->then(null, function ($e) use (&$exception) { + $exception = $e; + }); + + $this->assertTrue($stream->isWritable()); + $stream->write('Foo'); + $this->assertFalse($stream->isWritable()); + + assert($exception instanceof \RuntimeException); + $this->assertInstanceOf('RuntimeException', $exception); + $this->assertEquals('Error while buffering request body: Unexpected Foo', $exception->getMessage()); + $this->assertEquals(42, $exception->getCode()); + $this->assertInstanceOf('UnexpectedValueException', $exception->getPrevious()); } public function testFullBodyStreamedBeforeCallingNextMiddleware() @@ -263,4 +376,35 @@ public function testFullBodyStreamedBeforeCallingNextMiddleware() $stream->end('aaa'); $this->assertTrue($promiseResolved); } + + public function testCancelBufferingClosesStreamAndRejectsPromise() + { + $stream = new ThroughStream(); + $stream->on('close', $this->expectCallableOnce()); + + $serverRequest = new ServerRequest( + 'GET', + 'https://example.com/', + array(), + new HttpBodyStream($stream, 2) + ); + + $buffer = new RequestBodyBufferMiddleware(2); + + $promise = $buffer($serverRequest, $this->expectCallableNever()); + $promise->cancel(); + + $this->assertFalse($stream->isReadable()); + + $exception = null; + $promise->then(null, function ($e) use (&$exception) { + $exception = $e; + }); + + assert($exception instanceof \RuntimeException); + $this->assertInstanceOf('RuntimeException', $exception); + $this->assertEquals('Cancelled buffering request body', $exception->getMessage()); + $this->assertEquals(0, $exception->getCode()); + $this->assertNull($exception->getPrevious()); + } } From 1fbe922458ccb9233bb78191a8a160bf510799b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Wed, 11 Jan 2023 15:35:07 +0100 Subject: [PATCH 419/456] Refactor to move response body handling to `ClientRequestStream` --- src/Io/ClientRequestStream.php | 22 +++++++++++++++++++++- src/Io/Sender.php | 15 ++------------- tests/Io/ClientRequestStreamTest.php | 3 ++- 3 files changed, 25 insertions(+), 15 deletions(-) diff --git a/src/Io/ClientRequestStream.php b/src/Io/ClientRequestStream.php index 29536e88..04671354 100644 --- a/src/Io/ClientRequestStream.php +++ b/src/Io/ClientRequestStream.php @@ -4,6 +4,8 @@ use Evenement\EventEmitter; use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ResponseInterface; +use React\Http\Message\Response; use React\Promise; use React\Socket\ConnectionInterface; use React\Socket\ConnectorInterface; @@ -172,8 +174,26 @@ public function handleData($data) $this->stream->on('close', array($this, 'handleClose')); - $this->emit('response', array($response, $this->stream)); + assert($response instanceof ResponseInterface); + assert($this->stream instanceof ConnectionInterface); + $body = $this->stream; + + // determine length of response body + $length = null; + $code = $response->getStatusCode(); + if ($this->request->getMethod() === 'HEAD' || ($code >= 100 && $code < 200) || $code == Response::STATUS_NO_CONTENT || $code == Response::STATUS_NOT_MODIFIED) { + $length = 0; + } elseif (\strtolower($response->getHeaderLine('Transfer-Encoding')) === 'chunked') { + $body = new ChunkedDecoder($body); + } elseif ($response->hasHeader('Content-Length')) { + $length = (int) $response->getHeaderLine('Content-Length'); + } + $response = $response->withBody($body = new ReadableBodyStream($body, $length)); + + // emit response with streaming response body (see `Sender`) + $this->emit('response', array($response, $body)); + // re-emit HTTP response body to trigger body parsing if parts of it are buffered $this->stream->emit('data', array($bodyChunk)); } } diff --git a/src/Io/Sender.php b/src/Io/Sender.php index 2e821f5a..0894c574 100644 --- a/src/Io/Sender.php +++ b/src/Io/Sender.php @@ -6,7 +6,6 @@ use Psr\Http\Message\ResponseInterface; use React\EventLoop\LoopInterface; use React\Http\Client\Client as HttpClient; -use React\Http\Message\Response; use React\Promise\PromiseInterface; use React\Promise\Deferred; use React\Socket\ConnectorInterface; @@ -116,18 +115,8 @@ public function send(RequestInterface $request) $deferred->reject($error); }); - $requestStream->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($deferred, $request) { - $length = null; - $code = $response->getStatusCode(); - if ($request->getMethod() === 'HEAD' || ($code >= 100 && $code < 200) || $code == Response::STATUS_NO_CONTENT || $code == Response::STATUS_NOT_MODIFIED) { - $length = 0; - } elseif (\strtolower($response->getHeaderLine('Transfer-Encoding')) === 'chunked') { - $body = new ChunkedDecoder($body); - } elseif ($response->hasHeader('Content-Length')) { - $length = (int) $response->getHeaderLine('Content-Length'); - } - - $deferred->resolve($response->withBody(new ReadableBodyStream($body, $length))); + $requestStream->on('response', function (ResponseInterface $response) use ($deferred, $request) { + $deferred->resolve($response); }); if ($body instanceof ReadableStreamInterface) { diff --git a/tests/Io/ClientRequestStreamTest.php b/tests/Io/ClientRequestStreamTest.php index 07a4eb73..06c650ef 100644 --- a/tests/Io/ClientRequestStreamTest.php +++ b/tests/Io/ClientRequestStreamTest.php @@ -35,11 +35,12 @@ public function requestShouldBindToStreamEventsAndUseconnector() $this->successfulConnectionMock(); - $this->stream->expects($this->exactly(6))->method('on')->withConsecutive( + $this->stream->expects($this->atLeast(6))->method('on')->withConsecutive( array('drain', $this->identicalTo(array($request, 'handleDrain'))), array('data', $this->identicalTo(array($request, 'handleData'))), array('end', $this->identicalTo(array($request, 'handleEnd'))), array('error', $this->identicalTo(array($request, 'handleError'))), + array('close', $this->identicalTo(array($request, 'handleClose'))), array('close', $this->identicalTo(array($request, 'handleClose'))) ); From 165e5b5e2c1cf9704dd2b89defdb58e49db5beaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 16 Jan 2023 14:20:11 +0100 Subject: [PATCH 420/456] Consistently close underlying connection when response stream closes --- src/Io/ClientRequestStream.php | 84 +++-- tests/Io/ClientRequestStreamTest.php | 523 ++++++++++++++++++++------- tests/TestCase.php | 6 +- 3 files changed, 448 insertions(+), 165 deletions(-) diff --git a/src/Io/ClientRequestStream.php b/src/Io/ClientRequestStream.php index 04671354..bdaa54f1 100644 --- a/src/Io/ClientRequestStream.php +++ b/src/Io/ClientRequestStream.php @@ -16,7 +16,7 @@ * @event response * @event drain * @event error - * @event end + * @event close * @internal */ class ClientRequestStream extends EventEmitter implements WritableStreamInterface @@ -33,9 +33,11 @@ class ClientRequestStream extends EventEmitter implements WritableStreamInterfac private $request; /** @var ?ConnectionInterface */ - private $stream; + private $connection; + + /** @var string */ + private $buffer = ''; - private $buffer; private $responseFactory; private $state = self::STATE_INIT; private $ended = false; @@ -58,22 +60,22 @@ private function writeHead() $this->state = self::STATE_WRITING_HEAD; $request = $this->request; - $streamRef = &$this->stream; + $connectionRef = &$this->connection; $stateRef = &$this->state; $pendingWrites = &$this->pendingWrites; $that = $this; $promise = $this->connect(); $promise->then( - function (ConnectionInterface $stream) use ($request, &$streamRef, &$stateRef, &$pendingWrites, $that) { - $streamRef = $stream; - assert($streamRef instanceof ConnectionInterface); + function (ConnectionInterface $connection) use ($request, &$connectionRef, &$stateRef, &$pendingWrites, $that) { + $connectionRef = $connection; + assert($connectionRef instanceof ConnectionInterface); - $stream->on('drain', array($that, 'handleDrain')); - $stream->on('data', array($that, 'handleData')); - $stream->on('end', array($that, 'handleEnd')); - $stream->on('error', array($that, 'handleError')); - $stream->on('close', array($that, 'handleClose')); + $connection->on('drain', array($that, 'handleDrain')); + $connection->on('data', array($that, 'handleData')); + $connection->on('end', array($that, 'handleEnd')); + $connection->on('error', array($that, 'handleError')); + $connection->on('close', array($that, 'close')); assert($request instanceof RequestInterface); $headers = "{$request->getMethod()} {$request->getRequestTarget()} HTTP/{$request->getProtocolVersion()}\r\n"; @@ -83,7 +85,7 @@ function (ConnectionInterface $stream) use ($request, &$streamRef, &$stateRef, & } } - $more = $stream->write($headers . "\r\n" . $pendingWrites); + $more = $connection->write($headers . "\r\n" . $pendingWrites); assert($stateRef === ClientRequestStream::STATE_WRITING_HEAD); $stateRef = ClientRequestStream::STATE_HEAD_WRITTEN; @@ -113,7 +115,7 @@ public function write($data) // write directly to connection stream if already available if (self::STATE_HEAD_WRITTEN <= $this->state) { - return $this->stream->write($data); + return $this->connection->write($data); } // otherwise buffer and try to establish connection @@ -157,26 +159,28 @@ public function handleData($data) $response = gPsr\parse_response($this->buffer); $bodyChunk = (string) $response->getBody(); } catch (\InvalidArgumentException $exception) { - $this->emit('error', array($exception)); - } - - $this->buffer = null; - - $this->stream->removeListener('drain', array($this, 'handleDrain')); - $this->stream->removeListener('data', array($this, 'handleData')); - $this->stream->removeListener('end', array($this, 'handleEnd')); - $this->stream->removeListener('error', array($this, 'handleError')); - $this->stream->removeListener('close', array($this, 'handleClose')); - - if (!isset($response)) { + $this->closeError($exception); return; } - $this->stream->on('close', array($this, 'handleClose')); - - assert($response instanceof ResponseInterface); - assert($this->stream instanceof ConnectionInterface); - $body = $this->stream; + // response headers successfully received => remove listeners for connection events + $connection = $this->connection; + assert($connection instanceof ConnectionInterface); + $connection->removeListener('drain', array($this, 'handleDrain')); + $connection->removeListener('data', array($this, 'handleData')); + $connection->removeListener('end', array($this, 'handleEnd')); + $connection->removeListener('error', array($this, 'handleError')); + $connection->removeListener('close', array($this, 'close')); + $this->connection = null; + $this->buffer = ''; + + // take control over connection handling and close connection once response body closes + $that = $this; + $input = $body = new CloseProtectionStream($connection); + $input->on('close', function () use ($connection, $that) { + $connection->close(); + $that->close(); + }); // determine length of response body $length = null; @@ -194,7 +198,11 @@ public function handleData($data) $this->emit('response', array($response, $body)); // re-emit HTTP response body to trigger body parsing if parts of it are buffered - $this->stream->emit('data', array($bodyChunk)); + if ($bodyChunk !== '') { + $input->handleData($bodyChunk); + } elseif ($length === 0) { + $input->handleEnd(); + } } } @@ -216,12 +224,6 @@ public function handleError(\Exception $error) )); } - /** @internal */ - public function handleClose() - { - $this->close(); - } - /** @internal */ public function closeError(\Exception $error) { @@ -240,9 +242,11 @@ public function close() $this->state = self::STATE_END; $this->pendingWrites = ''; + $this->buffer = ''; - if ($this->stream) { - $this->stream->close(); + if ($this->connection instanceof ConnectionInterface) { + $this->connection->close(); + $this->connection = null; } $this->emit('close'); diff --git a/tests/Io/ClientRequestStreamTest.php b/tests/Io/ClientRequestStreamTest.php index 06c650ef..93220d10 100644 --- a/tests/Io/ClientRequestStreamTest.php +++ b/tests/Io/ClientRequestStreamTest.php @@ -2,27 +2,24 @@ namespace React\Tests\Http\Io; +use Psr\Http\Message\ResponseInterface; use React\Http\Io\ClientRequestStream; use React\Http\Message\Request; use React\Promise\Deferred; use React\Promise\Promise; use React\Stream\DuplexResourceStream; +use React\Stream\ReadableStreamInterface; use React\Tests\Http\TestCase; class ClientRequestStreamTest extends TestCase { private $connector; - private $stream; /** * @before */ public function setUpStream() { - $this->stream = $this->getMockBuilder('React\Socket\ConnectionInterface') - ->disableOriginalConstructor() - ->getMock(); - $this->connector = $this->getMockBuilder('React\Socket\ConnectorInterface') ->getMock(); } @@ -30,30 +27,29 @@ public function setUpStream() /** @test */ public function requestShouldBindToStreamEventsAndUseconnector() { + $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + + $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + $requestData = new Request('GET', 'http://www.example.com'); $request = new ClientRequestStream($this->connector, $requestData); - $this->successfulConnectionMock(); - - $this->stream->expects($this->atLeast(6))->method('on')->withConsecutive( + $connection->expects($this->atLeast(5))->method('on')->withConsecutive( array('drain', $this->identicalTo(array($request, 'handleDrain'))), array('data', $this->identicalTo(array($request, 'handleData'))), array('end', $this->identicalTo(array($request, 'handleEnd'))), array('error', $this->identicalTo(array($request, 'handleError'))), - array('close', $this->identicalTo(array($request, 'handleClose'))), - array('close', $this->identicalTo(array($request, 'handleClose'))) + array('close', $this->identicalTo(array($request, 'close'))) ); - $this->stream->expects($this->exactly(5))->method('removeListener')->withConsecutive( + $connection->expects($this->exactly(5))->method('removeListener')->withConsecutive( array('drain', $this->identicalTo(array($request, 'handleDrain'))), array('data', $this->identicalTo(array($request, 'handleData'))), array('end', $this->identicalTo(array($request, 'handleEnd'))), array('error', $this->identicalTo(array($request, 'handleError'))), - array('close', $this->identicalTo(array($request, 'handleClose'))) + array('close', $this->identicalTo(array($request, 'close'))) ); - $request->on('end', $this->expectCallableNever()); - $request->end(); $request->handleData("HTTP/1.0 200 OK\r\n"); @@ -66,26 +62,24 @@ public function requestShouldBindToStreamEventsAndUseconnector() */ public function requestShouldConnectViaTlsIfUrlUsesHttpsScheme() { + $this->connector->expects($this->once())->method('connect')->with('tls://www.example.com:443')->willReturn(new Promise(function () { })); + $requestData = new Request('GET', 'https://www.example.com'); $request = new ClientRequestStream($this->connector, $requestData); - $this->connector->expects($this->once())->method('connect')->with('tls://www.example.com:443')->willReturn(new Promise(function () { })); - $request->end(); } /** @test */ public function requestShouldEmitErrorIfConnectionFails() { + $this->connector->expects($this->once())->method('connect')->willReturn(\React\Promise\reject(new \RuntimeException())); + $requestData = new Request('GET', 'http://www.example.com'); $request = new ClientRequestStream($this->connector, $requestData); - $this->connector->expects($this->once())->method('connect')->willReturn(\React\Promise\reject(new \RuntimeException())); - $request->on('error', $this->expectCallableOnceWith($this->isInstanceOf('RuntimeException'))); - $request->on('close', $this->expectCallableOnce()); - $request->on('end', $this->expectCallableNever()); $request->end(); } @@ -93,15 +87,15 @@ public function requestShouldEmitErrorIfConnectionFails() /** @test */ public function requestShouldEmitErrorIfConnectionClosesBeforeResponseIsParsed() { + $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + + $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + $requestData = new Request('GET', 'http://www.example.com'); $request = new ClientRequestStream($this->connector, $requestData); - $this->successfulConnectionMock(); - $request->on('error', $this->expectCallableOnceWith($this->isInstanceOf('RuntimeException'))); - $request->on('close', $this->expectCallableOnce()); - $request->on('end', $this->expectCallableNever()); $request->end(); $request->handleEnd(); @@ -110,15 +104,15 @@ public function requestShouldEmitErrorIfConnectionClosesBeforeResponseIsParsed() /** @test */ public function requestShouldEmitErrorIfConnectionEmitsError() { + $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + + $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + $requestData = new Request('GET', 'http://www.example.com'); $request = new ClientRequestStream($this->connector, $requestData); - $this->successfulConnectionMock(); - $request->on('error', $this->expectCallableOnceWith($this->isInstanceOf('Exception'))); - $request->on('close', $this->expectCallableOnce()); - $request->on('end', $this->expectCallableNever()); $request->end(); $request->handleError(new \Exception('test')); @@ -127,12 +121,15 @@ public function requestShouldEmitErrorIfConnectionEmitsError() /** @test */ public function requestShouldEmitErrorIfRequestParserThrowsException() { + $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + + $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + $requestData = new Request('GET', 'http://www.example.com'); $request = new ClientRequestStream($this->connector, $requestData); - $this->successfulConnectionMock(); - $request->on('error', $this->expectCallableOnceWith($this->isInstanceOf('InvalidArgumentException'))); + $request->on('close', $this->expectCallableOnce()); $request->end(); $request->handleData("\r\n\r\n"); @@ -143,13 +140,13 @@ public function requestShouldEmitErrorIfRequestParserThrowsException() */ public function requestShouldEmitErrorIfUrlIsInvalid() { + $this->connector->expects($this->never())->method('connect'); + $requestData = new Request('GET', 'ftp://www.example.com'); $request = new ClientRequestStream($this->connector, $requestData); $request->on('error', $this->expectCallableOnceWith($this->isInstanceOf('InvalidArgumentException'))); - - $this->connector->expects($this->never()) - ->method('connect'); + $request->on('close', $this->expectCallableOnce()); $request->end(); } @@ -159,13 +156,13 @@ public function requestShouldEmitErrorIfUrlIsInvalid() */ public function requestShouldEmitErrorIfUrlHasNoScheme() { + $this->connector->expects($this->never())->method('connect'); + $requestData = new Request('GET', 'www.example.com'); $request = new ClientRequestStream($this->connector, $requestData); $request->on('error', $this->expectCallableOnceWith($this->isInstanceOf('InvalidArgumentException'))); - - $this->connector->expects($this->never()) - ->method('connect'); + $request->on('close', $this->expectCallableOnce()); $request->end(); } @@ -173,12 +170,13 @@ public function requestShouldEmitErrorIfUrlHasNoScheme() /** @test */ public function getRequestShouldSendAGetRequest() { - $requestData = new Request('GET', 'http://www.example.com', array(), '', '1.0'); - $request = new ClientRequestStream($this->connector, $requestData); + $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection->expects($this->once())->method('write')->with("GET / HTTP/1.0\r\nHost: www.example.com\r\n\r\n"); - $this->successfulConnectionMock(); + $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); - $this->stream->expects($this->once())->method('write')->with("GET / HTTP/1.0\r\nHost: www.example.com\r\n\r\n"); + $requestData = new Request('GET', 'http://www.example.com', array(), '', '1.0'); + $request = new ClientRequestStream($this->connector, $requestData); $request->end(); } @@ -186,12 +184,13 @@ public function getRequestShouldSendAGetRequest() /** @test */ public function getHttp11RequestShouldSendAGetRequestWithGivenConnectionCloseHeader() { - $requestData = new Request('GET', 'http://www.example.com', array('Connection' => 'close'), '', '1.1'); - $request = new ClientRequestStream($this->connector, $requestData); + $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection->expects($this->once())->method('write')->with("GET / HTTP/1.1\r\nHost: www.example.com\r\nConnection: close\r\n\r\n"); - $this->successfulConnectionMock(); + $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); - $this->stream->expects($this->once())->method('write')->with("GET / HTTP/1.1\r\nHost: www.example.com\r\nConnection: close\r\n\r\n"); + $requestData = new Request('GET', 'http://www.example.com', array('Connection' => 'close'), '', '1.1'); + $request = new ClientRequestStream($this->connector, $requestData); $request->end(); } @@ -199,29 +198,331 @@ public function getHttp11RequestShouldSendAGetRequestWithGivenConnectionCloseHea /** @test */ public function getOptionsAsteriskShouldSendAOptionsRequestAsteriskRequestTarget() { + $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection->expects($this->once())->method('write')->with("OPTIONS * HTTP/1.1\r\nHost: www.example.com\r\nConnection: close\r\n\r\n"); + + $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + $requestData = new Request('OPTIONS', 'http://www.example.com', array('Connection' => 'close'), '', '1.1'); $requestData = $requestData->withRequestTarget('*'); $request = new ClientRequestStream($this->connector, $requestData); - $this->successfulConnectionMock(); + $request->end(); + } + + public function testStreamShouldEmitResponseWithEmptyBodyWhenResponseContainsContentLengthZero() + { + $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection->expects($this->once())->method('write')->with("GET / HTTP/1.1\r\nHost: www.example.com\r\nConnection: close\r\n\r\n"); + $connection->expects($this->once())->method('close'); + + $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + + $requestData = new Request('GET', 'http://www.example.com', array('Connection' => 'close'), '', '1.1'); + $request = new ClientRequestStream($this->connector, $requestData); + + $that = $this; + $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($that) { + $body->on('data', $that->expectCallableNever()); + $body->on('end', $that->expectCallableOnce()); + $body->on('close', $that->expectCallableOnce()); + }); + $request->on('close', $this->expectCallableOnce()); + + $request->end(); + + $request->handleData("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"); + } + + public function testStreamShouldEmitResponseWithEmptyBodyWhenResponseContainsStatusNoContent() + { + $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection->expects($this->once())->method('write')->with("GET / HTTP/1.1\r\nHost: www.example.com\r\nConnection: close\r\n\r\n"); + $connection->expects($this->once())->method('close'); + + $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + + $requestData = new Request('GET', 'http://www.example.com', array('Connection' => 'close'), '', '1.1'); + $request = new ClientRequestStream($this->connector, $requestData); + + $that = $this; + $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($that) { + $body->on('data', $that->expectCallableNever()); + $body->on('end', $that->expectCallableOnce()); + $body->on('close', $that->expectCallableOnce()); + }); + $request->on('close', $this->expectCallableOnce()); + + $request->end(); + + $request->handleData("HTTP/1.1 204 No Content\r\n\r\n"); + } + + public function testStreamShouldEmitResponseWithEmptyBodyWhenResponseContainsStatusNotModifiedWithContentLengthGiven() + { + $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection->expects($this->once())->method('write')->with("GET / HTTP/1.1\r\nHost: www.example.com\r\nConnection: close\r\n\r\n"); + $connection->expects($this->once())->method('close'); + + $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + + $requestData = new Request('GET', 'http://www.example.com', array('Connection' => 'close'), '', '1.1'); + $request = new ClientRequestStream($this->connector, $requestData); + + $that = $this; + $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($that) { + $body->on('data', $that->expectCallableNever()); + $body->on('end', $that->expectCallableOnce()); + $body->on('close', $that->expectCallableOnce()); + }); + $request->on('close', $this->expectCallableOnce()); + + $request->end(); + + $request->handleData("HTTP/1.1 304 Not Modified\r\nContent-Length: 100\r\n\r\n"); + } + + public function testStreamShouldEmitResponseWithEmptyBodyWhenRequestMethodIsHead() + { + $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection->expects($this->once())->method('write')->with("HEAD / HTTP/1.1\r\nHost: www.example.com\r\nConnection: close\r\n\r\n"); + $connection->expects($this->once())->method('close'); + + $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + + $requestData = new Request('HEAD', 'http://www.example.com', array('Connection' => 'close'), '', '1.1'); + $request = new ClientRequestStream($this->connector, $requestData); + + $that = $this; + $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($that) { + $body->on('data', $that->expectCallableNever()); + $body->on('end', $that->expectCallableOnce()); + $body->on('close', $that->expectCallableOnce()); + }); + $request->on('close', $this->expectCallableOnce()); + + $request->end(); + + $request->handleData("HTTP/1.1 200 OK\r\nContent-Length: 100\r\n\r\n"); + } + + public function testStreamShouldEmitResponseWithStreamingBodyUntilEndWhenResponseContainsContentLengthAndResponseBody() + { + $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection->expects($this->once())->method('write')->with("GET / HTTP/1.1\r\nHost: www.example.com\r\nConnection: close\r\n\r\n"); + $connection->expects($this->once())->method('close'); + + $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + + $requestData = new Request('GET', 'http://www.example.com', array('Connection' => 'close'), '', '1.1'); + $request = new ClientRequestStream($this->connector, $requestData); + + $that = $this; + $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($that) { + $body->on('data', $that->expectCallableOnceWith('OK')); + $body->on('end', $that->expectCallableOnce()); + $body->on('close', $that->expectCallableOnce()); + }); + $request->on('close', $this->expectCallableOnce()); + + $request->end(); + + $request->handleData("HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nOK"); + } + + public function testStreamShouldEmitResponseWithStreamingBodyWithoutDataWhenResponseContainsContentLengthWithoutResponseBody() + { + $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection->expects($this->once())->method('write')->with("GET / HTTP/1.1\r\nHost: www.example.com\r\nConnection: close\r\n\r\n"); + $connection->expects($this->never())->method('close'); + + $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + + $requestData = new Request('GET', 'http://www.example.com', array('Connection' => 'close'), '', '1.1'); + $request = new ClientRequestStream($this->connector, $requestData); + + $that = $this; + $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($that) { + $body->on('data', $that->expectCallableNever()); + $body->on('end', $that->expectCallableNever()); + $body->on('close', $that->expectCallableNever()); + }); + $request->on('close', $this->expectCallableNever()); + + $request->end(); + + $request->handleData("HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\n"); + } + + public function testStreamShouldEmitResponseWithStreamingBodyWithDataWithoutEndWhenResponseContainsContentLengthWithIncompleteResponseBody() + { + $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection->expects($this->once())->method('write')->with("GET / HTTP/1.1\r\nHost: www.example.com\r\nConnection: close\r\n\r\n"); + $connection->expects($this->never())->method('close'); + + $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + + $requestData = new Request('GET', 'http://www.example.com', array('Connection' => 'close'), '', '1.1'); + $request = new ClientRequestStream($this->connector, $requestData); + + $that = $this; + $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($that) { + $body->on('data', $that->expectCallableOnce('O')); + $body->on('end', $that->expectCallableNever()); + $body->on('close', $that->expectCallableNever()); + }); + $request->on('close', $this->expectCallableNever()); + + $request->end(); + + $request->handleData("HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nO"); + } + + public function testStreamShouldEmitResponseWithStreamingBodyUntilEndWhenResponseContainsTransferEncodingChunkedAndResponseBody() + { + $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection->expects($this->once())->method('write')->with("GET / HTTP/1.1\r\nHost: www.example.com\r\nConnection: close\r\n\r\n"); + $connection->expects($this->once())->method('close'); + + $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + + $requestData = new Request('GET', 'http://www.example.com', array('Connection' => 'close'), '', '1.1'); + $request = new ClientRequestStream($this->connector, $requestData); + + $that = $this; + $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($that) { + $body->on('data', $that->expectCallableOnceWith('OK')); + $body->on('end', $that->expectCallableOnce()); + $body->on('close', $that->expectCallableOnce()); + }); + $request->on('close', $this->expectCallableOnce()); + + $request->end(); + + $request->handleData("HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n2\r\nOK\r\n0\r\n\r\n"); + } + + public function testStreamShouldEmitResponseWithStreamingBodyWithoutDataWhenResponseContainsTransferEncodingChunkedWithoutResponseBody() + { + $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection->expects($this->once())->method('write')->with("GET / HTTP/1.1\r\nHost: www.example.com\r\nConnection: close\r\n\r\n"); + $connection->expects($this->never())->method('close'); + + $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + + $requestData = new Request('GET', 'http://www.example.com', array('Connection' => 'close'), '', '1.1'); + $request = new ClientRequestStream($this->connector, $requestData); + + $that = $this; + $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($that) { + $body->on('data', $that->expectCallableNever()); + $body->on('end', $that->expectCallableNever()); + $body->on('close', $that->expectCallableNever()); + }); + $request->on('close', $this->expectCallableNever()); + + $request->end(); + + $request->handleData("HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n"); + } + + public function testStreamShouldEmitResponseWithStreamingBodyWithDataWithoutEndWhenResponseContainsTransferEncodingChunkedWithIncompleteResponseBody() + { + $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection->expects($this->once())->method('write')->with("GET / HTTP/1.1\r\nHost: www.example.com\r\nConnection: close\r\n\r\n"); + $connection->expects($this->never())->method('close'); + + $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + + $requestData = new Request('GET', 'http://www.example.com', array('Connection' => 'close'), '', '1.1'); + $request = new ClientRequestStream($this->connector, $requestData); + + $that = $this; + $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($that) { + $body->on('data', $that->expectCallableOnceWith('O')); + $body->on('end', $that->expectCallableNever()); + $body->on('close', $that->expectCallableNever()); + }); + $request->on('close', $this->expectCallableNever()); + + $request->end(); + + $request->handleData("HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n2\r\nO"); + } + + public function testStreamShouldEmitResponseWithStreamingBodyWithDataWithoutEndWhenResponseContainsNoContentLengthAndIncompleteResponseBody() + { + $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection->expects($this->once())->method('write')->with("GET / HTTP/1.1\r\nHost: www.example.com\r\nConnection: close\r\n\r\n"); + $connection->expects($this->never())->method('close'); - $this->stream->expects($this->once())->method('write')->with("OPTIONS * HTTP/1.1\r\nHost: www.example.com\r\nConnection: close\r\n\r\n"); + $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + + $requestData = new Request('GET', 'http://www.example.com', array('Connection' => 'close'), '', '1.1'); + $request = new ClientRequestStream($this->connector, $requestData); + + $that = $this; + $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($that) { + $body->on('data', $that->expectCallableOnce('O')); + $body->on('end', $that->expectCallableNever()); + $body->on('close', $that->expectCallableNever()); + }); + $request->on('close', $this->expectCallableNever()); $request->end(); + + $request->handleData("HTTP/1.1 200 OK\r\n\r\nO"); + } + + public function testStreamShouldEmitResponseWithStreamingBodyUntilEndWhenResponseContainsNoContentLengthAndResponseBodyTerminatedByConnectionEndEvent() + { + $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection->expects($this->once())->method('write')->with("GET / HTTP/1.1\r\nHost: www.example.com\r\nConnection: close\r\n\r\n"); + $connection->expects($this->once())->method('close'); + + $endEvent = null; + $eventName = null; + $connection->expects($this->any())->method('on')->with($this->callback(function ($name) use (&$eventName) { + $eventName = $name; + return true; + }), $this->callback(function ($cb) use (&$endEvent, &$eventName) { + if ($eventName === 'end') { + $endEvent = $cb; + } + return true; + })); + + $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + + $requestData = new Request('GET', 'http://www.example.com', array('Connection' => 'close'), '', '1.1'); + $request = new ClientRequestStream($this->connector, $requestData); + + $that = $this; + $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($that) { + $body->on('data', $that->expectCallableOnce('OK')); + $body->on('end', $that->expectCallableOnce()); + $body->on('close', $that->expectCallableOnce()); + }); + $request->on('close', $this->expectCallableOnce()); + + $request->end(); + + $request->handleData("HTTP/1.1 200 OK\r\n\r\nOK"); + + $this->assertNotNull($endEvent); + call_user_func($endEvent); // $endEvent() (PHP 5.4+) } /** @test */ public function postRequestShouldSendAPostRequest() { - $requestData = new Request('POST', 'http://www.example.com', array(), '', '1.0'); - $request = new ClientRequestStream($this->connector, $requestData); + $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection->expects($this->once())->method('write')->with($this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\n\r\nsome post data$#")); - $this->successfulConnectionMock(); + $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); - $this->stream - ->expects($this->once()) - ->method('write') - ->with($this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\n\r\nsome post data$#")); + $requestData = new Request('POST', 'http://www.example.com', array(), '', '1.0'); + $request = new ClientRequestStream($this->connector, $requestData); $request->end('some post data'); @@ -233,17 +534,18 @@ public function postRequestShouldSendAPostRequest() /** @test */ public function writeWithAPostRequestShouldSendToTheStream() { - $requestData = new Request('POST', 'http://www.example.com', array(), '', '1.0'); - $request = new ClientRequestStream($this->connector, $requestData); - - $this->successfulConnectionMock(); - - $this->stream->expects($this->exactly(3))->method('write')->withConsecutive( + $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection->expects($this->exactly(3))->method('write')->withConsecutive( array($this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\n\r\nsome$#")), array($this->identicalTo("post")), array($this->identicalTo("data")) ); + $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + + $requestData = new Request('POST', 'http://www.example.com', array(), '', '1.0'); + $request = new ClientRequestStream($this->connector, $requestData); + $request->write("some"); $request->write("post"); $request->end("data"); @@ -256,18 +558,20 @@ public function writeWithAPostRequestShouldSendToTheStream() /** @test */ public function writeWithAPostRequestShouldSendBodyAfterHeadersAndEmitDrainEvent() { - $requestData = new Request('POST', 'http://www.example.com', array(), '', '1.0'); - $request = new ClientRequestStream($this->connector, $requestData); - - $resolveConnection = $this->successfulAsyncConnectionMock(); - - $this->stream->expects($this->exactly(2))->method('write')->withConsecutive( + $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection->expects($this->exactly(2))->method('write')->withConsecutive( array($this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\n\r\nsomepost$#")), array($this->identicalTo("data")) )->willReturn( true ); + $deferred = new Deferred(); + $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn($deferred->promise()); + + $requestData = new Request('POST', 'http://www.example.com', array(), '', '1.0'); + $request = new ClientRequestStream($this->connector, $requestData); + $this->assertFalse($request->write("some")); $this->assertFalse($request->write("post")); @@ -277,7 +581,7 @@ public function writeWithAPostRequestShouldSendBodyAfterHeadersAndEmitDrainEvent $request->end(); }); - $resolveConnection(); + $deferred->resolve($connection); $request->handleData("HTTP/1.0 200 OK\r\n"); $request->handleData("Content-Type: text/plain\r\n"); @@ -287,23 +591,24 @@ public function writeWithAPostRequestShouldSendBodyAfterHeadersAndEmitDrainEvent /** @test */ public function writeWithAPostRequestShouldForwardDrainEventIfFirstChunkExceedsBuffer() { - $requestData = new Request('POST', 'http://www.example.com', array(), '', '1.0'); - $request = new ClientRequestStream($this->connector, $requestData); - - $this->stream = $this->getMockBuilder('React\Socket\Connection') + $connection = $this->getMockBuilder('React\Socket\Connection') ->disableOriginalConstructor() ->setMethods(array('write')) ->getMock(); - $resolveConnection = $this->successfulAsyncConnectionMock(); - - $this->stream->expects($this->exactly(2))->method('write')->withConsecutive( + $connection->expects($this->exactly(2))->method('write')->withConsecutive( array($this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\n\r\nsomepost$#")), array($this->identicalTo("data")) )->willReturn( false ); + $deferred = new Deferred(); + $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn($deferred->promise()); + + $requestData = new Request('POST', 'http://www.example.com', array(), '', '1.0'); + $request = new ClientRequestStream($this->connector, $requestData); + $this->assertFalse($request->write("some")); $this->assertFalse($request->write("post")); @@ -313,8 +618,8 @@ public function writeWithAPostRequestShouldForwardDrainEventIfFirstChunkExceedsB $request->end(); }); - $resolveConnection(); - $this->stream->emit('drain'); + $deferred->resolve($connection); + $connection->emit('drain'); $request->handleData("HTTP/1.0 200 OK\r\n"); $request->handleData("Content-Type: text/plain\r\n"); @@ -324,17 +629,18 @@ public function writeWithAPostRequestShouldForwardDrainEventIfFirstChunkExceedsB /** @test */ public function pipeShouldPipeDataIntoTheRequestBody() { - $requestData = new Request('POST', 'http://www.example.com', array(), '', '1.0'); - $request = new ClientRequestStream($this->connector, $requestData); - - $this->successfulConnectionMock(); - - $this->stream->expects($this->exactly(3))->method('write')->withConsecutive( + $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection->expects($this->exactly(3))->method('write')->withConsecutive( array($this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\n\r\nsome$#")), array($this->identicalTo("post")), array($this->identicalTo("data")) ); + $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + + $requestData = new Request('POST', 'http://www.example.com', array(), '', '1.0'); + $request = new ClientRequestStream($this->connector, $requestData); + $loop = $this ->getMockBuilder('React\EventLoop\LoopInterface') ->getMock(); @@ -357,14 +663,14 @@ public function pipeShouldPipeDataIntoTheRequestBody() */ public function writeShouldStartConnecting() { - $requestData = new Request('POST', 'http://www.example.com'); - $request = new ClientRequestStream($this->connector, $requestData); - $this->connector->expects($this->once()) ->method('connect') ->with('www.example.com:80') ->willReturn(new Promise(function () { })); + $requestData = new Request('POST', 'http://www.example.com'); + $request = new ClientRequestStream($this->connector, $requestData); + $request->write('test'); } @@ -373,14 +679,11 @@ public function writeShouldStartConnecting() */ public function endShouldStartConnectingAndChangeStreamIntoNonWritableMode() { + $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(new Promise(function () { })); + $requestData = new Request('POST', 'http://www.example.com'); $request = new ClientRequestStream($this->connector, $requestData); - $this->connector->expects($this->once()) - ->method('connect') - ->with('www.example.com:80') - ->willReturn(new Promise(function () { })); - $request->end(); $this->assertFalse($request->isWritable()); @@ -417,12 +720,11 @@ public function writeAfterCloseReturnsFalse() */ public function endAfterCloseIsNoOp() { + $this->connector->expects($this->never())->method('connect'); + $requestData = new Request('POST', 'http://www.example.com'); $request = new ClientRequestStream($this->connector, $requestData); - $this->connector->expects($this->never()) - ->method('connect'); - $request->close(); $request->end(); } @@ -432,17 +734,13 @@ public function endAfterCloseIsNoOp() */ public function closeShouldCancelPendingConnectionAttempt() { - $requestData = new Request('POST', 'http://www.example.com'); - $request = new ClientRequestStream($this->connector, $requestData); - $promise = new Promise(function () {}, function () { throw new \RuntimeException(); }); + $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn($promise); - $this->connector->expects($this->once()) - ->method('connect') - ->with('www.example.com:80') - ->willReturn($promise); + $requestData = new Request('POST', 'http://www.example.com'); + $request = new ClientRequestStream($this->connector, $requestData); $request->end(); @@ -466,35 +764,16 @@ public function requestShouldRemoveAllListenerAfterClosed() $this->assertCount(0, $request->listeners('close')); } - private function successfulConnectionMock() - { - call_user_func($this->successfulAsyncConnectionMock()); - } - - private function successfulAsyncConnectionMock() - { - $deferred = new Deferred(); - - $this->connector - ->expects($this->once()) - ->method('connect') - ->with('www.example.com:80') - ->will($this->returnValue($deferred->promise())); - - $stream = $this->stream; - return function () use ($deferred, $stream) { - $deferred->resolve($stream); - }; - } - /** @test */ public function multivalueHeader() { + $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + + $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + $requestData = new Request('GET', 'http://www.example.com'); $request = new ClientRequestStream($this->connector, $requestData); - $this->successfulConnectionMock(); - $response = null; $request->on('response', $this->expectCallableOnce()); $request->on('response', function ($value) use (&$response) { diff --git a/tests/TestCase.php b/tests/TestCase.php index 1938ed89..72b7be8d 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -6,7 +6,7 @@ class TestCase extends BaseTestCase { - protected function expectCallableOnce() + public function expectCallableOnce() // protected (PHP 5.4+) { $mock = $this->createCallableMock(); $mock @@ -16,7 +16,7 @@ protected function expectCallableOnce() return $mock; } - protected function expectCallableOnceWith($value) + public function expectCallableOnceWith($value) // protected (PHP 5.4+) { $mock = $this->createCallableMock(); $mock @@ -27,7 +27,7 @@ protected function expectCallableOnceWith($value) return $mock; } - protected function expectCallableNever() + public function expectCallableNever() // protected (PHP 5.4+) { $mock = $this->createCallableMock(); $mock From 28b598ab09109da412e1935d9560888887cfb86a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 12 Jan 2023 10:58:56 +0100 Subject: [PATCH 421/456] Send `Connection: close` for HTTP/1.1 and no `Connection` for HTTP/1.0 --- src/Io/Sender.php | 4 +++- tests/Io/SenderTest.php | 20 +++++++++++++++++--- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/Io/Sender.php b/src/Io/Sender.php index 0894c574..acbb6e7d 100644 --- a/src/Io/Sender.php +++ b/src/Io/Sender.php @@ -94,8 +94,10 @@ public function send(RequestInterface $request) } // automatically add `Connection: close` request header for HTTP/1.1 requests to avoid connection reuse - if ($request->getProtocolVersion() === '1.1' && !$request->hasHeader('Connection')) { + if ($request->getProtocolVersion() === '1.1') { $request = $request->withHeader('Connection', 'close'); + } else { + $request = $request->withoutHeader('Connection'); } // automatically add `Authorization: Basic …` request header if URL includes `user:pass@host` diff --git a/tests/Io/SenderTest.php b/tests/Io/SenderTest.php index c2357a1a..4ef06442 100644 --- a/tests/Io/SenderTest.php +++ b/tests/Io/SenderTest.php @@ -302,6 +302,20 @@ public function getHttp10RequestShouldSendAGetRequestWithoutConnectionHeaderByDe $sender->send($request); } + /** @test */ + public function getHttp10RequestShouldSendAGetRequestWithoutConnectionHeaderEvenWhenConnectionKeepAliveHeaderIsSpecified() + { + $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); + $client->expects($this->once())->method('request')->with($this->callback(function (RequestInterface $request) { + return !$request->hasHeader('Connection'); + }))->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock()); + + $sender = new Sender($client); + + $request = new Request('GET', 'http://www.example.com', array('Connection' => 'keep-alive'), '', '1.0'); + $sender->send($request); + } + /** @test */ public function getHttp11RequestShouldSendAGetRequestWithConnectionCloseHeaderByDefault() { @@ -317,16 +331,16 @@ public function getHttp11RequestShouldSendAGetRequestWithConnectionCloseHeaderBy } /** @test */ - public function getHttp11RequestShouldSendAGetRequestWithGivenConnectionUpgradeHeader() + public function getHttp11RequestShouldSendAGetRequestWithConnectionCloseHeaderEvenWhenConnectionKeepAliveHeaderIsSpecified() { $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); $client->expects($this->once())->method('request')->with($this->callback(function (RequestInterface $request) { - return $request->getHeaderLine('Connection') === 'upgrade'; + return $request->getHeaderLine('Connection') === 'close'; }))->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock()); $sender = new Sender($client); - $request = new Request('GET', 'http://www.example.com', array('Connection' => 'upgrade'), '', '1.1'); + $request = new Request('GET', 'http://www.example.com', array('Connection' => 'keep-alive'), '', '1.1'); $sender->send($request); } From d8566953c6b699618a500a4589833c19d6985f2b Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Sat, 21 Jan 2023 14:52:26 +0100 Subject: [PATCH 422/456] Template params can only have one argument The fact that a promise can also be rejected with a Throwable and/or Exception is implied and there is no need to also define that here. Refs: https://github.com/reactphp/promise/pull/223 --- src/Browser.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Browser.php b/src/Browser.php index 3e3458af..b7bf4425 100644 --- a/src/Browser.php +++ b/src/Browser.php @@ -344,7 +344,7 @@ public function delete($url, array $headers = array(), $body = '') * @param string $url URL for the request * @param array $headers Additional request headers * @param string|ReadableStreamInterface $body HTTP request body contents - * @return PromiseInterface + * @return PromiseInterface */ public function request($method, $url, array $headers = array(), $body = '') { @@ -417,7 +417,7 @@ public function request($method, $url, array $headers = array(), $body = '') * @param string $url URL for the request * @param array $headers Additional request headers * @param string|ReadableStreamInterface $body HTTP request body contents - * @return PromiseInterface + * @return PromiseInterface */ public function requestStreaming($method, $url, $headers = array(), $body = '') { @@ -828,7 +828,7 @@ private function withOptions(array $options) * @param string $url * @param array $headers * @param string|ReadableStreamInterface $body - * @return PromiseInterface + * @return PromiseInterface */ private function requestMayBeStreaming($method, $url, array $headers = array(), $body = '') { From 1c911d2fff297278d74815008dfe95b0036379b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 10 Jan 2023 15:49:44 +0100 Subject: [PATCH 423/456] Refactor to add new `ClientConnectionManager` to manage HTTP connections --- src/Client/Client.php | 17 +- src/Io/ClientConnectionManager.php | 45 ++++ src/Io/ClientRequestStream.php | 37 +--- src/Io/Sender.php | 7 +- tests/BrowserTest.php | 16 +- tests/Client/FunctionalIntegrationTest.php | 15 +- tests/Io/ClientConnectionManagerTest.php | 83 ++++++++ tests/Io/ClientRequestStreamTest.php | 227 ++++++++++----------- tests/Io/SenderTest.php | 23 +-- 9 files changed, 276 insertions(+), 194 deletions(-) create mode 100644 src/Io/ClientConnectionManager.php create mode 100644 tests/Io/ClientConnectionManagerTest.php diff --git a/src/Client/Client.php b/src/Client/Client.php index c3fd4570..7a5180ab 100644 --- a/src/Client/Client.php +++ b/src/Client/Client.php @@ -3,30 +3,25 @@ namespace React\Http\Client; use Psr\Http\Message\RequestInterface; -use React\EventLoop\LoopInterface; +use React\Http\Io\ClientConnectionManager; use React\Http\Io\ClientRequestStream; -use React\Socket\Connector; -use React\Socket\ConnectorInterface; /** * @internal */ class Client { - private $connector; + /** @var ClientConnectionManager */ + private $connectionManager; - public function __construct(LoopInterface $loop, ConnectorInterface $connector = null) + public function __construct(ClientConnectionManager $connectionManager) { - if ($connector === null) { - $connector = new Connector(array(), $loop); - } - - $this->connector = $connector; + $this->connectionManager = $connectionManager; } /** @return ClientRequestStream */ public function request(RequestInterface $request) { - return new ClientRequestStream($this->connector, $request); + return new ClientRequestStream($this->connectionManager, $request); } } diff --git a/src/Io/ClientConnectionManager.php b/src/Io/ClientConnectionManager.php new file mode 100644 index 00000000..51f937e4 --- /dev/null +++ b/src/Io/ClientConnectionManager.php @@ -0,0 +1,45 @@ +connector = $connector; + } + + /** + * @return PromiseInterface + */ + public function connect(UriInterface $uri) + { + $scheme = $uri->getScheme(); + if ($scheme !== 'https' && $scheme !== 'http') { + return \React\Promise\reject(new \InvalidArgumentException( + 'Invalid request URL given' + )); + } + + $port = $uri->getPort(); + if ($port === null) { + $port = $scheme === 'https' ? 443 : 80; + } + + return $this->connector->connect(($scheme === 'https' ? 'tls://' : '') . $uri->getHost() . ':' . $port); + } +} diff --git a/src/Io/ClientRequestStream.php b/src/Io/ClientRequestStream.php index bdaa54f1..e5eaf298 100644 --- a/src/Io/ClientRequestStream.php +++ b/src/Io/ClientRequestStream.php @@ -4,11 +4,8 @@ use Evenement\EventEmitter; use Psr\Http\Message\RequestInterface; -use Psr\Http\Message\ResponseInterface; use React\Http\Message\Response; -use React\Promise; use React\Socket\ConnectionInterface; -use React\Socket\ConnectorInterface; use React\Stream\WritableStreamInterface; use RingCentral\Psr7 as gPsr; @@ -26,8 +23,8 @@ class ClientRequestStream extends EventEmitter implements WritableStreamInterfac const STATE_HEAD_WRITTEN = 2; const STATE_END = 3; - /** @var ConnectorInterface */ - private $connector; + /** @var ClientConnectionManager */ + private $connectionManager; /** @var RequestInterface */ private $request; @@ -44,9 +41,9 @@ class ClientRequestStream extends EventEmitter implements WritableStreamInterfac private $pendingWrites = ''; - public function __construct(ConnectorInterface $connector, RequestInterface $request) + public function __construct(ClientConnectionManager $connectionManager, RequestInterface $request) { - $this->connector = $connector; + $this->connectionManager = $connectionManager; $this->request = $request; } @@ -65,7 +62,7 @@ private function writeHead() $pendingWrites = &$this->pendingWrites; $that = $this; - $promise = $this->connect(); + $promise = $this->connectionManager->connect($this->request->getUri()); $promise->then( function (ConnectionInterface $connection) use ($request, &$connectionRef, &$stateRef, &$pendingWrites, $that) { $connectionRef = $connection; @@ -252,28 +249,4 @@ public function close() $this->emit('close'); $this->removeAllListeners(); } - - protected function connect() - { - $scheme = $this->request->getUri()->getScheme(); - if ($scheme !== 'https' && $scheme !== 'http') { - return Promise\reject( - new \InvalidArgumentException('Invalid request URL given') - ); - } - - $host = $this->request->getUri()->getHost(); - $port = $this->request->getUri()->getPort(); - - if ($scheme === 'https') { - $host = 'tls://' . $host; - } - - if ($port === null) { - $port = $scheme === 'https' ? 443 : 80; - } - - return $this->connector - ->connect($host . ':' . $port); - } } diff --git a/src/Io/Sender.php b/src/Io/Sender.php index acbb6e7d..68c09322 100644 --- a/src/Io/Sender.php +++ b/src/Io/Sender.php @@ -8,6 +8,7 @@ use React\Http\Client\Client as HttpClient; use React\Promise\PromiseInterface; use React\Promise\Deferred; +use React\Socket\Connector; use React\Socket\ConnectorInterface; use React\Stream\ReadableStreamInterface; @@ -49,7 +50,11 @@ class Sender */ public static function createFromLoop(LoopInterface $loop, ConnectorInterface $connector = null) { - return new self(new HttpClient($loop, $connector)); + if ($connector === null) { + $connector = new Connector(array(), $loop); + } + + return new self(new HttpClient(new ClientConnectionManager($connector))); } private $http; diff --git a/tests/BrowserTest.php b/tests/BrowserTest.php index ad61cf9b..21242a5d 100644 --- a/tests/BrowserTest.php +++ b/tests/BrowserTest.php @@ -60,9 +60,13 @@ public function testConstructWithConnectorAssignsGivenConnector() $ref->setAccessible(true); $client = $ref->getValue($sender); - $ref = new \ReflectionProperty($client, 'connector'); + $ref = new \ReflectionProperty($client, 'connectionManager'); $ref->setAccessible(true); - $ret = $ref->getValue($client); + $connectionManager = $ref->getValue($client); + + $ref = new \ReflectionProperty($connectionManager, 'connector'); + $ref->setAccessible(true); + $ret = $ref->getValue($connectionManager); $this->assertSame($connector, $ret); } @@ -85,9 +89,13 @@ public function testConstructWithConnectorWithLegacySignatureAssignsGivenConnect $ref->setAccessible(true); $client = $ref->getValue($sender); - $ref = new \ReflectionProperty($client, 'connector'); + $ref = new \ReflectionProperty($client, 'connectionManager'); + $ref->setAccessible(true); + $connectionManager = $ref->getValue($client); + + $ref = new \ReflectionProperty($connectionManager, 'connector'); $ref->setAccessible(true); - $ret = $ref->getValue($client); + $ret = $ref->getValue($connectionManager); $this->assertSame($connector, $ret); } diff --git a/tests/Client/FunctionalIntegrationTest.php b/tests/Client/FunctionalIntegrationTest.php index 90d8444b..d5015fd1 100644 --- a/tests/Client/FunctionalIntegrationTest.php +++ b/tests/Client/FunctionalIntegrationTest.php @@ -3,12 +3,13 @@ namespace React\Tests\Http\Client; use Psr\Http\Message\ResponseInterface; -use React\EventLoop\Loop; use React\Http\Client\Client; +use React\Http\Io\ClientConnectionManager; use React\Http\Message\Request; use React\Promise\Deferred; use React\Promise\Stream; use React\Socket\ConnectionInterface; +use React\Socket\Connector; use React\Socket\SocketServer; use React\Stream\ReadableStreamInterface; use React\Tests\Http\TestCase; @@ -45,7 +46,7 @@ public function testRequestToLocalhostEmitsSingleRemoteConnection() }); $port = parse_url($socket->getAddress(), PHP_URL_PORT); - $client = new Client(Loop::get()); + $client = new Client(new ClientConnectionManager(new Connector())); $request = $client->request(new Request('GET', 'http://localhost:' . $port, array(), '', '1.0')); $promise = Stream\first($request, 'close'); @@ -62,7 +63,7 @@ public function testRequestLegacyHttpServerWithOnlyLineFeedReturnsSuccessfulResp $socket->close(); }); - $client = new Client(Loop::get()); + $client = new Client(new ClientConnectionManager(new Connector())); $request = $client->request(new Request('GET', str_replace('tcp:', 'http:', $socket->getAddress()), array(), '', '1.0')); $once = $this->expectCallableOnceWith('body'); @@ -82,7 +83,7 @@ public function testSuccessfulResponseEmitsEnd() // max_nesting_level was set to 100 for PHP Versions < 5.4 which resulted in failing test for legacy PHP ini_set('xdebug.max_nesting_level', 256); - $client = new Client(Loop::get()); + $client = new Client(new ClientConnectionManager(new Connector())); $request = $client->request(new Request('GET', 'http://www.google.com/', array(), '', '1.0')); @@ -107,7 +108,7 @@ public function testPostDataReturnsData() // max_nesting_level was set to 100 for PHP Versions < 5.4 which resulted in failing test for legacy PHP ini_set('xdebug.max_nesting_level', 256); - $client = new Client(Loop::get()); + $client = new Client(new ClientConnectionManager(new Connector())); $data = str_repeat('.', 33000); $request = $client->request(new Request('POST', 'https://' . (mt_rand(0, 1) === 0 ? 'eu.' : '') . 'httpbin.org/post', array('Content-Length' => strlen($data)), '', '1.0')); @@ -139,7 +140,7 @@ public function testPostJsonReturnsData() $this->markTestSkipped('Not supported on HHVM'); } - $client = new Client(Loop::get()); + $client = new Client(new ClientConnectionManager(new Connector())); $data = json_encode(array('numbers' => range(1, 50))); $request = $client->request(new Request('POST', 'https://httpbin.org/post', array('Content-Length' => strlen($data), 'Content-Type' => 'application/json'), '', '1.0')); @@ -169,7 +170,7 @@ public function testCancelPendingConnectionEmitsClose() // max_nesting_level was set to 100 for PHP Versions < 5.4 which resulted in failing test for legacy PHP ini_set('xdebug.max_nesting_level', 256); - $client = new Client(Loop::get()); + $client = new Client(new ClientConnectionManager(new Connector())); $request = $client->request(new Request('GET', 'http://www.google.com/', array(), '', '1.0')); $request->on('error', $this->expectCallableNever()); diff --git a/tests/Io/ClientConnectionManagerTest.php b/tests/Io/ClientConnectionManagerTest.php new file mode 100644 index 00000000..5774e47d --- /dev/null +++ b/tests/Io/ClientConnectionManagerTest.php @@ -0,0 +1,83 @@ +getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $connector->expects($this->once())->method('connect')->with('tls://reactphp.org:443')->willReturn($promise); + + $connectionManager = new ClientConnectionManager($connector); + + $ret = $connectionManager->connect(new Uri('https://reactphp.org/')); + $this->assertSame($promise, $ret); + } + + public function testConnectWithHttpUriShouldConnectToTcpWithDefaultPort() + { + $promise = new Promise(function () { }); + $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $connector->expects($this->once())->method('connect')->with('reactphp.org:80')->willReturn($promise); + + $connectionManager = new ClientConnectionManager($connector); + + $ret = $connectionManager->connect(new Uri('http://reactphp.org/')); + $this->assertSame($promise, $ret); + } + + public function testConnectWithExplicitPortShouldConnectWithGivenPort() + { + $promise = new Promise(function () { }); + $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $connector->expects($this->once())->method('connect')->with('reactphp.org:8080')->willReturn($promise); + + $connectionManager = new ClientConnectionManager($connector); + + $ret = $connectionManager->connect(new Uri('http://reactphp.org:8080/')); + $this->assertSame($promise, $ret); + } + + public function testConnectWithInvalidSchemeShouldRejectWithException() + { + $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $connector->expects($this->never())->method('connect'); + + $connectionManager = new ClientConnectionManager($connector); + + $promise = $connectionManager->connect(new Uri('ftp://reactphp.org/')); + + $exception = null; + $promise->then(null, function ($reason) use (&$exception) { + $exception = $reason; + }); + + $this->assertInstanceOf('InvalidArgumentException', $exception); + $this->assertEquals('Invalid request URL given', $exception->getMessage()); + } + + public function testConnectWithoutSchemeShouldRejectWithException() + { + $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $connector->expects($this->never())->method('connect'); + + $connectionManager = new ClientConnectionManager($connector); + + $promise = $connectionManager->connect(new Uri('reactphp.org')); + + $exception = null; + $promise->then(null, function ($reason) use (&$exception) { + $exception = $reason; + }); + + $this->assertInstanceOf('InvalidArgumentException', $exception); + $this->assertEquals('Invalid request URL given', $exception->getMessage()); + } +} diff --git a/tests/Io/ClientRequestStreamTest.php b/tests/Io/ClientRequestStreamTest.php index 93220d10..da26b4e5 100644 --- a/tests/Io/ClientRequestStreamTest.php +++ b/tests/Io/ClientRequestStreamTest.php @@ -3,6 +3,7 @@ namespace React\Tests\Http\Io; use Psr\Http\Message\ResponseInterface; +use RingCentral\Psr7\Uri; use React\Http\Io\ClientRequestStream; use React\Http\Message\Request; use React\Promise\Deferred; @@ -13,26 +14,17 @@ class ClientRequestStreamTest extends TestCase { - private $connector; - - /** - * @before - */ - public function setUpStream() - { - $this->connector = $this->getMockBuilder('React\Socket\ConnectorInterface') - ->getMock(); - } - /** @test */ - public function requestShouldBindToStreamEventsAndUseconnector() + public function testRequestShouldUseConnectionManagerWithUriFromRequestAndBindToStreamEvents() { $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); - $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + $uri = new Uri('http://www.example.com'); + $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager->expects($this->once())->method('connect')->with($uri)->willReturn(\React\Promise\resolve($connection)); - $requestData = new Request('GET', 'http://www.example.com'); - $request = new ClientRequestStream($this->connector, $requestData); + $requestData = new Request('GET', $uri); + $request = new ClientRequestStream($connectionManager, $requestData); $connection->expects($this->atLeast(5))->method('on')->withConsecutive( array('drain', $this->identicalTo(array($request, 'handleDrain'))), @@ -57,26 +49,14 @@ public function requestShouldBindToStreamEventsAndUseconnector() $request->handleData("\r\nbody"); } - /** - * @test - */ - public function requestShouldConnectViaTlsIfUrlUsesHttpsScheme() - { - $this->connector->expects($this->once())->method('connect')->with('tls://www.example.com:443')->willReturn(new Promise(function () { })); - - $requestData = new Request('GET', 'https://www.example.com'); - $request = new ClientRequestStream($this->connector, $requestData); - - $request->end(); - } - /** @test */ public function requestShouldEmitErrorIfConnectionFails() { - $this->connector->expects($this->once())->method('connect')->willReturn(\React\Promise\reject(new \RuntimeException())); + $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\reject(new \RuntimeException())); $requestData = new Request('GET', 'http://www.example.com'); - $request = new ClientRequestStream($this->connector, $requestData); + $request = new ClientRequestStream($connectionManager, $requestData); $request->on('error', $this->expectCallableOnceWith($this->isInstanceOf('RuntimeException'))); $request->on('close', $this->expectCallableOnce()); @@ -89,10 +69,11 @@ public function requestShouldEmitErrorIfConnectionClosesBeforeResponseIsParsed() { $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); - $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection)); $requestData = new Request('GET', 'http://www.example.com'); - $request = new ClientRequestStream($this->connector, $requestData); + $request = new ClientRequestStream($connectionManager, $requestData); $request->on('error', $this->expectCallableOnceWith($this->isInstanceOf('RuntimeException'))); $request->on('close', $this->expectCallableOnce()); @@ -106,10 +87,11 @@ public function requestShouldEmitErrorIfConnectionEmitsError() { $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); - $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection)); $requestData = new Request('GET', 'http://www.example.com'); - $request = new ClientRequestStream($this->connector, $requestData); + $request = new ClientRequestStream($connectionManager, $requestData); $request->on('error', $this->expectCallableOnceWith($this->isInstanceOf('Exception'))); $request->on('close', $this->expectCallableOnce()); @@ -123,10 +105,11 @@ public function requestShouldEmitErrorIfRequestParserThrowsException() { $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); - $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection)); $requestData = new Request('GET', 'http://www.example.com'); - $request = new ClientRequestStream($this->connector, $requestData); + $request = new ClientRequestStream($connectionManager, $requestData); $request->on('error', $this->expectCallableOnceWith($this->isInstanceOf('InvalidArgumentException'))); $request->on('close', $this->expectCallableOnce()); @@ -135,48 +118,17 @@ public function requestShouldEmitErrorIfRequestParserThrowsException() $request->handleData("\r\n\r\n"); } - /** - * @test - */ - public function requestShouldEmitErrorIfUrlIsInvalid() - { - $this->connector->expects($this->never())->method('connect'); - - $requestData = new Request('GET', 'ftp://www.example.com'); - $request = new ClientRequestStream($this->connector, $requestData); - - $request->on('error', $this->expectCallableOnceWith($this->isInstanceOf('InvalidArgumentException'))); - $request->on('close', $this->expectCallableOnce()); - - $request->end(); - } - - /** - * @test - */ - public function requestShouldEmitErrorIfUrlHasNoScheme() - { - $this->connector->expects($this->never())->method('connect'); - - $requestData = new Request('GET', 'www.example.com'); - $request = new ClientRequestStream($this->connector, $requestData); - - $request->on('error', $this->expectCallableOnceWith($this->isInstanceOf('InvalidArgumentException'))); - $request->on('close', $this->expectCallableOnce()); - - $request->end(); - } - /** @test */ public function getRequestShouldSendAGetRequest() { $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); $connection->expects($this->once())->method('write')->with("GET / HTTP/1.0\r\nHost: www.example.com\r\n\r\n"); - $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection)); $requestData = new Request('GET', 'http://www.example.com', array(), '', '1.0'); - $request = new ClientRequestStream($this->connector, $requestData); + $request = new ClientRequestStream($connectionManager, $requestData); $request->end(); } @@ -187,10 +139,11 @@ public function getHttp11RequestShouldSendAGetRequestWithGivenConnectionCloseHea $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); $connection->expects($this->once())->method('write')->with("GET / HTTP/1.1\r\nHost: www.example.com\r\nConnection: close\r\n\r\n"); - $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection)); $requestData = new Request('GET', 'http://www.example.com', array('Connection' => 'close'), '', '1.1'); - $request = new ClientRequestStream($this->connector, $requestData); + $request = new ClientRequestStream($connectionManager, $requestData); $request->end(); } @@ -201,11 +154,12 @@ public function getOptionsAsteriskShouldSendAOptionsRequestAsteriskRequestTarget $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); $connection->expects($this->once())->method('write')->with("OPTIONS * HTTP/1.1\r\nHost: www.example.com\r\nConnection: close\r\n\r\n"); - $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection)); $requestData = new Request('OPTIONS', 'http://www.example.com', array('Connection' => 'close'), '', '1.1'); $requestData = $requestData->withRequestTarget('*'); - $request = new ClientRequestStream($this->connector, $requestData); + $request = new ClientRequestStream($connectionManager, $requestData); $request->end(); } @@ -216,10 +170,11 @@ public function testStreamShouldEmitResponseWithEmptyBodyWhenResponseContainsCon $connection->expects($this->once())->method('write')->with("GET / HTTP/1.1\r\nHost: www.example.com\r\nConnection: close\r\n\r\n"); $connection->expects($this->once())->method('close'); - $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection)); $requestData = new Request('GET', 'http://www.example.com', array('Connection' => 'close'), '', '1.1'); - $request = new ClientRequestStream($this->connector, $requestData); + $request = new ClientRequestStream($connectionManager, $requestData); $that = $this; $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($that) { @@ -240,10 +195,11 @@ public function testStreamShouldEmitResponseWithEmptyBodyWhenResponseContainsSta $connection->expects($this->once())->method('write')->with("GET / HTTP/1.1\r\nHost: www.example.com\r\nConnection: close\r\n\r\n"); $connection->expects($this->once())->method('close'); - $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection)); $requestData = new Request('GET', 'http://www.example.com', array('Connection' => 'close'), '', '1.1'); - $request = new ClientRequestStream($this->connector, $requestData); + $request = new ClientRequestStream($connectionManager, $requestData); $that = $this; $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($that) { @@ -264,10 +220,11 @@ public function testStreamShouldEmitResponseWithEmptyBodyWhenResponseContainsSta $connection->expects($this->once())->method('write')->with("GET / HTTP/1.1\r\nHost: www.example.com\r\nConnection: close\r\n\r\n"); $connection->expects($this->once())->method('close'); - $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection)); $requestData = new Request('GET', 'http://www.example.com', array('Connection' => 'close'), '', '1.1'); - $request = new ClientRequestStream($this->connector, $requestData); + $request = new ClientRequestStream($connectionManager, $requestData); $that = $this; $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($that) { @@ -288,10 +245,11 @@ public function testStreamShouldEmitResponseWithEmptyBodyWhenRequestMethodIsHead $connection->expects($this->once())->method('write')->with("HEAD / HTTP/1.1\r\nHost: www.example.com\r\nConnection: close\r\n\r\n"); $connection->expects($this->once())->method('close'); - $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection)); $requestData = new Request('HEAD', 'http://www.example.com', array('Connection' => 'close'), '', '1.1'); - $request = new ClientRequestStream($this->connector, $requestData); + $request = new ClientRequestStream($connectionManager, $requestData); $that = $this; $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($that) { @@ -312,10 +270,11 @@ public function testStreamShouldEmitResponseWithStreamingBodyUntilEndWhenRespons $connection->expects($this->once())->method('write')->with("GET / HTTP/1.1\r\nHost: www.example.com\r\nConnection: close\r\n\r\n"); $connection->expects($this->once())->method('close'); - $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection)); $requestData = new Request('GET', 'http://www.example.com', array('Connection' => 'close'), '', '1.1'); - $request = new ClientRequestStream($this->connector, $requestData); + $request = new ClientRequestStream($connectionManager, $requestData); $that = $this; $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($that) { @@ -336,10 +295,11 @@ public function testStreamShouldEmitResponseWithStreamingBodyWithoutDataWhenResp $connection->expects($this->once())->method('write')->with("GET / HTTP/1.1\r\nHost: www.example.com\r\nConnection: close\r\n\r\n"); $connection->expects($this->never())->method('close'); - $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection)); $requestData = new Request('GET', 'http://www.example.com', array('Connection' => 'close'), '', '1.1'); - $request = new ClientRequestStream($this->connector, $requestData); + $request = new ClientRequestStream($connectionManager, $requestData); $that = $this; $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($that) { @@ -360,10 +320,11 @@ public function testStreamShouldEmitResponseWithStreamingBodyWithDataWithoutEndW $connection->expects($this->once())->method('write')->with("GET / HTTP/1.1\r\nHost: www.example.com\r\nConnection: close\r\n\r\n"); $connection->expects($this->never())->method('close'); - $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection)); $requestData = new Request('GET', 'http://www.example.com', array('Connection' => 'close'), '', '1.1'); - $request = new ClientRequestStream($this->connector, $requestData); + $request = new ClientRequestStream($connectionManager, $requestData); $that = $this; $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($that) { @@ -384,10 +345,11 @@ public function testStreamShouldEmitResponseWithStreamingBodyUntilEndWhenRespons $connection->expects($this->once())->method('write')->with("GET / HTTP/1.1\r\nHost: www.example.com\r\nConnection: close\r\n\r\n"); $connection->expects($this->once())->method('close'); - $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection)); $requestData = new Request('GET', 'http://www.example.com', array('Connection' => 'close'), '', '1.1'); - $request = new ClientRequestStream($this->connector, $requestData); + $request = new ClientRequestStream($connectionManager, $requestData); $that = $this; $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($that) { @@ -408,10 +370,11 @@ public function testStreamShouldEmitResponseWithStreamingBodyWithoutDataWhenResp $connection->expects($this->once())->method('write')->with("GET / HTTP/1.1\r\nHost: www.example.com\r\nConnection: close\r\n\r\n"); $connection->expects($this->never())->method('close'); - $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection)); $requestData = new Request('GET', 'http://www.example.com', array('Connection' => 'close'), '', '1.1'); - $request = new ClientRequestStream($this->connector, $requestData); + $request = new ClientRequestStream($connectionManager, $requestData); $that = $this; $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($that) { @@ -432,10 +395,11 @@ public function testStreamShouldEmitResponseWithStreamingBodyWithDataWithoutEndW $connection->expects($this->once())->method('write')->with("GET / HTTP/1.1\r\nHost: www.example.com\r\nConnection: close\r\n\r\n"); $connection->expects($this->never())->method('close'); - $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection)); $requestData = new Request('GET', 'http://www.example.com', array('Connection' => 'close'), '', '1.1'); - $request = new ClientRequestStream($this->connector, $requestData); + $request = new ClientRequestStream($connectionManager, $requestData); $that = $this; $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($that) { @@ -456,10 +420,11 @@ public function testStreamShouldEmitResponseWithStreamingBodyWithDataWithoutEndW $connection->expects($this->once())->method('write')->with("GET / HTTP/1.1\r\nHost: www.example.com\r\nConnection: close\r\n\r\n"); $connection->expects($this->never())->method('close'); - $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection)); $requestData = new Request('GET', 'http://www.example.com', array('Connection' => 'close'), '', '1.1'); - $request = new ClientRequestStream($this->connector, $requestData); + $request = new ClientRequestStream($connectionManager, $requestData); $that = $this; $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($that) { @@ -492,10 +457,11 @@ public function testStreamShouldEmitResponseWithStreamingBodyUntilEndWhenRespons return true; })); - $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection)); $requestData = new Request('GET', 'http://www.example.com', array('Connection' => 'close'), '', '1.1'); - $request = new ClientRequestStream($this->connector, $requestData); + $request = new ClientRequestStream($connectionManager, $requestData); $that = $this; $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($that) { @@ -519,10 +485,11 @@ public function postRequestShouldSendAPostRequest() $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); $connection->expects($this->once())->method('write')->with($this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\n\r\nsome post data$#")); - $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection)); $requestData = new Request('POST', 'http://www.example.com', array(), '', '1.0'); - $request = new ClientRequestStream($this->connector, $requestData); + $request = new ClientRequestStream($connectionManager, $requestData); $request->end('some post data'); @@ -541,10 +508,11 @@ public function writeWithAPostRequestShouldSendToTheStream() array($this->identicalTo("data")) ); - $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection)); $requestData = new Request('POST', 'http://www.example.com', array(), '', '1.0'); - $request = new ClientRequestStream($this->connector, $requestData); + $request = new ClientRequestStream($connectionManager, $requestData); $request->write("some"); $request->write("post"); @@ -567,10 +535,11 @@ public function writeWithAPostRequestShouldSendBodyAfterHeadersAndEmitDrainEvent ); $deferred = new Deferred(); - $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn($deferred->promise()); + $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager->expects($this->once())->method('connect')->willReturn($deferred->promise()); $requestData = new Request('POST', 'http://www.example.com', array(), '', '1.0'); - $request = new ClientRequestStream($this->connector, $requestData); + $request = new ClientRequestStream($connectionManager, $requestData); $this->assertFalse($request->write("some")); $this->assertFalse($request->write("post")); @@ -604,10 +573,11 @@ public function writeWithAPostRequestShouldForwardDrainEventIfFirstChunkExceedsB ); $deferred = new Deferred(); - $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn($deferred->promise()); + $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager->expects($this->once())->method('connect')->willReturn($deferred->promise()); $requestData = new Request('POST', 'http://www.example.com', array(), '', '1.0'); - $request = new ClientRequestStream($this->connector, $requestData); + $request = new ClientRequestStream($connectionManager, $requestData); $this->assertFalse($request->write("some")); $this->assertFalse($request->write("post")); @@ -636,10 +606,11 @@ public function pipeShouldPipeDataIntoTheRequestBody() array($this->identicalTo("data")) ); - $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection)); $requestData = new Request('POST', 'http://www.example.com', array(), '', '1.0'); - $request = new ClientRequestStream($this->connector, $requestData); + $request = new ClientRequestStream($connectionManager, $requestData); $loop = $this ->getMockBuilder('React\EventLoop\LoopInterface') @@ -663,13 +634,11 @@ public function pipeShouldPipeDataIntoTheRequestBody() */ public function writeShouldStartConnecting() { - $this->connector->expects($this->once()) - ->method('connect') - ->with('www.example.com:80') - ->willReturn(new Promise(function () { })); + $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager->expects($this->once())->method('connect')->willReturn(new Promise(function () { })); $requestData = new Request('POST', 'http://www.example.com'); - $request = new ClientRequestStream($this->connector, $requestData); + $request = new ClientRequestStream($connectionManager, $requestData); $request->write('test'); } @@ -679,10 +648,11 @@ public function writeShouldStartConnecting() */ public function endShouldStartConnectingAndChangeStreamIntoNonWritableMode() { - $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(new Promise(function () { })); + $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager->expects($this->once())->method('connect')->willReturn(new Promise(function () { })); $requestData = new Request('POST', 'http://www.example.com'); - $request = new ClientRequestStream($this->connector, $requestData); + $request = new ClientRequestStream($connectionManager, $requestData); $request->end(); @@ -694,8 +664,10 @@ public function endShouldStartConnectingAndChangeStreamIntoNonWritableMode() */ public function closeShouldEmitCloseEvent() { + $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $requestData = new Request('POST', 'http://www.example.com'); - $request = new ClientRequestStream($this->connector, $requestData); + $request = new ClientRequestStream($connectionManager, $requestData); $request->on('close', $this->expectCallableOnce()); $request->close(); @@ -706,8 +678,10 @@ public function closeShouldEmitCloseEvent() */ public function writeAfterCloseReturnsFalse() { + $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $requestData = new Request('POST', 'http://www.example.com'); - $request = new ClientRequestStream($this->connector, $requestData); + $request = new ClientRequestStream($connectionManager, $requestData); $request->close(); @@ -720,10 +694,11 @@ public function writeAfterCloseReturnsFalse() */ public function endAfterCloseIsNoOp() { - $this->connector->expects($this->never())->method('connect'); + $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager->expects($this->never())->method('connect'); $requestData = new Request('POST', 'http://www.example.com'); - $request = new ClientRequestStream($this->connector, $requestData); + $request = new ClientRequestStream($connectionManager, $requestData); $request->close(); $request->end(); @@ -737,10 +712,11 @@ public function closeShouldCancelPendingConnectionAttempt() $promise = new Promise(function () {}, function () { throw new \RuntimeException(); }); - $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn($promise); + $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager->expects($this->once())->method('connect')->willReturn($promise); $requestData = new Request('POST', 'http://www.example.com'); - $request = new ClientRequestStream($this->connector, $requestData); + $request = new ClientRequestStream($connectionManager, $requestData); $request->end(); @@ -754,8 +730,10 @@ public function closeShouldCancelPendingConnectionAttempt() /** @test */ public function requestShouldRemoveAllListenerAfterClosed() { + $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $requestData = new Request('GET', 'http://www.example.com'); - $request = new ClientRequestStream($this->connector, $requestData); + $request = new ClientRequestStream($connectionManager, $requestData); $request->on('close', function () {}); $this->assertCount(1, $request->listeners('close')); @@ -769,10 +747,11 @@ public function multivalueHeader() { $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); - $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection)); $requestData = new Request('GET', 'http://www.example.com'); - $request = new ClientRequestStream($this->connector, $requestData); + $request = new ClientRequestStream($connectionManager, $requestData); $response = null; $request->on('response', $this->expectCallableOnce()); diff --git a/tests/Io/SenderTest.php b/tests/Io/SenderTest.php index 4ef06442..220424e9 100644 --- a/tests/Io/SenderTest.php +++ b/tests/Io/SenderTest.php @@ -4,6 +4,7 @@ use Psr\Http\Message\RequestInterface; use React\Http\Client\Client as HttpClient; +use React\Http\Io\ClientConnectionManager; use React\Http\Io\ReadableBodyStream; use React\Http\Io\Sender; use React\Http\Message\Request; @@ -13,19 +14,11 @@ class SenderTest extends TestCase { - private $loop; - - /** - * @before - */ - public function setUpLoop() - { - $this->loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - } - public function testCreateFromLoop() { - $sender = Sender::createFromLoop($this->loop, null); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $sender = Sender::createFromLoop($loop, null); $this->assertInstanceOf('React\Http\Io\Sender', $sender); } @@ -35,7 +28,7 @@ public function testSenderRejectsInvalidUri() $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); $connector->expects($this->never())->method('connect'); - $sender = new Sender(new HttpClient($this->loop, $connector)); + $sender = new Sender(new HttpClient(new ClientConnectionManager($connector))); $request = new Request('GET', 'www.google.com'); @@ -54,7 +47,7 @@ public function testSenderConnectorRejection() $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); $connector->expects($this->once())->method('connect')->willReturn(Promise\reject(new \RuntimeException('Rejected'))); - $sender = new Sender(new HttpClient($this->loop, $connector)); + $sender = new Sender(new HttpClient(new ClientConnectionManager($connector))); $request = new Request('GET', 'http://www.google.com/'); @@ -381,7 +374,7 @@ public function testCancelRequestWillCancelConnector() $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); $connector->expects($this->once())->method('connect')->willReturn($promise); - $sender = new Sender(new HttpClient($this->loop, $connector)); + $sender = new Sender(new HttpClient(new ClientConnectionManager($connector))); $request = new Request('GET', 'http://www.google.com/'); @@ -404,7 +397,7 @@ public function testCancelRequestWillCloseConnection() $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); $connector->expects($this->once())->method('connect')->willReturn(Promise\resolve($connection)); - $sender = new Sender(new HttpClient($this->loop, $connector)); + $sender = new Sender(new HttpClient(new ClientConnectionManager($connector))); $request = new Request('GET', 'http://www.google.com/'); From ab3bfee58c16cfb51691c136f0ec2f5e53268e79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 17 Jan 2023 10:24:28 +0100 Subject: [PATCH 424/456] Prepare to hand back connections when keep-alive is possible --- src/Io/ClientConnectionManager.php | 8 + src/Io/ClientRequestStream.php | 44 +++- tests/Client/FunctionalIntegrationTest.php | 25 +++ tests/Io/ClientConnectionManagerTest.php | 12 ++ tests/Io/ClientRequestStreamTest.php | 234 +++++++++++++++++++++ 5 files changed, 320 insertions(+), 3 deletions(-) diff --git a/src/Io/ClientConnectionManager.php b/src/Io/ClientConnectionManager.php index 51f937e4..eda2ea44 100644 --- a/src/Io/ClientConnectionManager.php +++ b/src/Io/ClientConnectionManager.php @@ -42,4 +42,12 @@ public function connect(UriInterface $uri) return $this->connector->connect(($scheme === 'https' ? 'tls://' : '') . $uri->getHost() . ':' . $port); } + + /** + * @return void + */ + public function handBack(ConnectionInterface $connection) + { + $connection->close(); + } } diff --git a/src/Io/ClientRequestStream.php b/src/Io/ClientRequestStream.php index e5eaf298..e9716b45 100644 --- a/src/Io/ClientRequestStream.php +++ b/src/Io/ClientRequestStream.php @@ -3,6 +3,7 @@ namespace React\Http\Io; use Evenement\EventEmitter; +use Psr\Http\Message\MessageInterface; use Psr\Http\Message\RequestInterface; use React\Http\Message\Response; use React\Socket\ConnectionInterface; @@ -171,11 +172,20 @@ public function handleData($data) $this->connection = null; $this->buffer = ''; - // take control over connection handling and close connection once response body closes + // take control over connection handling and check if we can reuse the connection once response body closes $that = $this; + $request = $this->request; + $connectionManager = $this->connectionManager; + $successfulEndReceived = false; $input = $body = new CloseProtectionStream($connection); - $input->on('close', function () use ($connection, $that) { - $connection->close(); + $input->on('close', function () use ($connection, $that, $connectionManager, $request, $response, &$successfulEndReceived) { + // only reuse connection after successful response and both request and response allow keep alive + if ($successfulEndReceived && $connection->isReadable() && $that->hasMessageKeepAliveEnabled($response) && $that->hasMessageKeepAliveEnabled($request)) { + $connectionManager->handBack($connection); + } else { + $connection->close(); + } + $that->close(); }); @@ -190,6 +200,9 @@ public function handleData($data) $length = (int) $response->getHeaderLine('Content-Length'); } $response = $response->withBody($body = new ReadableBodyStream($body, $length)); + $body->on('end', function () use (&$successfulEndReceived) { + $successfulEndReceived = true; + }); // emit response with streaming response body (see `Sender`) $this->emit('response', array($response, $body)); @@ -249,4 +262,29 @@ public function close() $this->emit('close'); $this->removeAllListeners(); } + + /** + * @internal + * @return bool + * @link https://www.rfc-editor.org/rfc/rfc9112#section-9.3 + * @link https://www.rfc-editor.org/rfc/rfc7230#section-6.1 + */ + public function hasMessageKeepAliveEnabled(MessageInterface $message) + { + $connectionOptions = \RingCentral\Psr7\normalize_header(\strtolower($message->getHeaderLine('Connection'))); + + if (\in_array('close', $connectionOptions, true)) { + return false; + } + + if ($message->getProtocolVersion() === '1.1') { + return true; + } + + if (\in_array('keep-alive', $connectionOptions, true)) { + return true; + } + + return false; + } } diff --git a/tests/Client/FunctionalIntegrationTest.php b/tests/Client/FunctionalIntegrationTest.php index d5015fd1..1c37d897 100644 --- a/tests/Client/FunctionalIntegrationTest.php +++ b/tests/Client/FunctionalIntegrationTest.php @@ -7,6 +7,7 @@ use React\Http\Io\ClientConnectionManager; use React\Http\Message\Request; use React\Promise\Deferred; +use React\Promise\Promise; use React\Promise\Stream; use React\Socket\ConnectionInterface; use React\Socket\Connector; @@ -55,6 +56,30 @@ public function testRequestToLocalhostEmitsSingleRemoteConnection() \React\Async\await(\React\Promise\Timer\timeout($promise, self::TIMEOUT_LOCAL)); } + public function testRequestToLocalhostWillConnectAndCloseConnectionAfterResponseUntilKeepAliveIsActuallySupported() + { + $socket = new SocketServer('127.0.0.1:0'); + $socket->on('connection', $this->expectCallableOnce()); + + $promise = new Promise(function ($resolve) use ($socket) { + $socket->on('connection', function (ConnectionInterface $conn) use ($socket, $resolve) { + $conn->write("HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nOK"); + $conn->on('close', function () use ($resolve) { + $resolve(null); + }); + $socket->close(); + }); + }); + $port = parse_url($socket->getAddress(), PHP_URL_PORT); + + $client = new Client(new ClientConnectionManager(new Connector())); + $request = $client->request(new Request('GET', 'http://localhost:' . $port, array(), '', '1.1')); + + $request->end(); + + \React\Async\await(\React\Promise\Timer\timeout($promise, self::TIMEOUT_LOCAL)); + } + public function testRequestLegacyHttpServerWithOnlyLineFeedReturnsSuccessfulResponse() { $socket = new SocketServer('127.0.0.1:0'); diff --git a/tests/Io/ClientConnectionManagerTest.php b/tests/Io/ClientConnectionManagerTest.php index 5774e47d..143676e4 100644 --- a/tests/Io/ClientConnectionManagerTest.php +++ b/tests/Io/ClientConnectionManagerTest.php @@ -80,4 +80,16 @@ public function testConnectWithoutSchemeShouldRejectWithException() $this->assertInstanceOf('InvalidArgumentException', $exception); $this->assertEquals('Invalid request URL given', $exception->getMessage()); } + + public function testHandBackWillCloseGivenConnectionUntilKeepAliveIsActuallySupported() + { + $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + + $connectionManager = new ClientConnectionManager($connector); + + $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection->expects($this->once())->method('close'); + + $connectionManager->handBack($connection); + } } diff --git a/tests/Io/ClientRequestStreamTest.php b/tests/Io/ClientRequestStreamTest.php index da26b4e5..d257994b 100644 --- a/tests/Io/ClientRequestStreamTest.php +++ b/tests/Io/ClientRequestStreamTest.php @@ -479,6 +479,240 @@ public function testStreamShouldEmitResponseWithStreamingBodyUntilEndWhenRespons call_user_func($endEvent); // $endEvent() (PHP 5.4+) } + public function testStreamShouldReuseConnectionForHttp11ByDefault() + { + $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection->expects($this->once())->method('write')->with("GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n"); + $connection->expects($this->once())->method('isReadable')->willReturn(true); + $connection->expects($this->never())->method('close'); + + $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection)); + $connectionManager->expects($this->once())->method('handBack')->with($connection); + + $requestData = new Request('GET', 'http://www.example.com', array(), '', '1.1'); + $request = new ClientRequestStream($connectionManager, $requestData); + + $request->on('close', $this->expectCallableOnce()); + + $request->end(); + + $request->handleData("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"); + } + + public function testStreamShouldNotReuseConnectionWhenResponseContainsConnectionClose() + { + $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection->expects($this->once())->method('write')->with("GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n"); + $connection->expects($this->once())->method('isReadable')->willReturn(true); + $connection->expects($this->once())->method('close'); + + $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection)); + + $requestData = new Request('GET', 'http://www.example.com', array(), '', '1.1'); + $request = new ClientRequestStream($connectionManager, $requestData); + + $request->on('close', $this->expectCallableOnce()); + + $request->end(); + + $request->handleData("HTTP/1.1 200 OK\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"); + } + + public function testStreamShouldNotReuseConnectionWhenRequestContainsConnectionCloseWithAdditionalOptions() + { + $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection->expects($this->once())->method('write')->with("GET / HTTP/1.1\r\nHost: www.example.com\r\nConnection: FOO, CLOSE, BAR\r\n\r\n"); + $connection->expects($this->once())->method('isReadable')->willReturn(true); + $connection->expects($this->once())->method('close'); + + $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection)); + + $requestData = new Request('GET', 'http://www.example.com', array('Connection' => 'FOO, CLOSE, BAR'), '', '1.1'); + $request = new ClientRequestStream($connectionManager, $requestData); + + $request->on('close', $this->expectCallableOnce()); + + $request->end(); + + $request->handleData("HTTP/1.1 200 OK\r\nContent-Length: 0\r\nConnection: Foo, Close, Bar\r\n\r\n"); + } + + public function testStreamShouldNotReuseConnectionForHttp10ByDefault() + { + $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection->expects($this->once())->method('write')->with("GET / HTTP/1.0\r\nHost: www.example.com\r\n\r\n"); + $connection->expects($this->once())->method('isReadable')->willReturn(true); + $connection->expects($this->once())->method('close'); + + $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection)); + + $requestData = new Request('GET', 'http://www.example.com', array(), '', '1.0'); + $request = new ClientRequestStream($connectionManager, $requestData); + + $request->on('close', $this->expectCallableOnce()); + + $request->end(); + + $request->handleData("HTTP/1.0 200 OK\r\nContent-Length: 0\r\n\r\n"); + } + + public function testStreamShouldReuseConnectionForHttp10WhenBothRequestAndResponseContainConnectionKeepAlive() + { + $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection->expects($this->once())->method('write')->with("GET / HTTP/1.0\r\nHost: www.example.com\r\nConnection: keep-alive\r\n\r\n"); + $connection->expects($this->once())->method('isReadable')->willReturn(true); + $connection->expects($this->never())->method('close'); + + $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection)); + $connectionManager->expects($this->once())->method('handBack')->with($connection); + + $requestData = new Request('GET', 'http://www.example.com', array('Connection' => 'keep-alive'), '', '1.0'); + $request = new ClientRequestStream($connectionManager, $requestData); + + $request->on('close', $this->expectCallableOnce()); + + $request->end(); + + $request->handleData("HTTP/1.0 200 OK\r\nContent-Length: 0\r\nConnection: keep-alive\r\n\r\n"); + } + + public function testStreamShouldReuseConnectionForHttp10WhenBothRequestAndResponseContainConnectionKeepAliveWithAdditionalOptions() + { + $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection->expects($this->once())->method('write')->with("GET / HTTP/1.0\r\nHost: www.example.com\r\nConnection: FOO, KEEP-ALIVE, BAR\r\n\r\n"); + $connection->expects($this->once())->method('isReadable')->willReturn(true); + $connection->expects($this->never())->method('close'); + + $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection)); + $connectionManager->expects($this->once())->method('handBack')->with($connection); + + $requestData = new Request('GET', 'http://www.example.com', array('Connection' => 'FOO, KEEP-ALIVE, BAR'), '', '1.0'); + $request = new ClientRequestStream($connectionManager, $requestData); + + $request->on('close', $this->expectCallableOnce()); + + $request->end(); + + $request->handleData("HTTP/1.0 200 OK\r\nContent-Length: 0\r\nConnection: Foo, Keep-Alive, Bar\r\n\r\n"); + } + + public function testStreamShouldNotReuseConnectionWhenResponseContainsNoContentLengthAndResponseBodyTerminatedByConnectionEndEvent() + { + $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection->expects($this->once())->method('write')->with("GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n"); + $connection->expects($this->once())->method('isReadable')->willReturn(false); + $connection->expects($this->once())->method('close'); + + $endEvent = null; + $eventName = null; + $connection->expects($this->any())->method('on')->with($this->callback(function ($name) use (&$eventName) { + $eventName = $name; + return true; + }), $this->callback(function ($cb) use (&$endEvent, &$eventName) { + if ($eventName === 'end') { + $endEvent = $cb; + } + return true; + })); + + $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection)); + + $requestData = new Request('GET', 'http://www.example.com', array(), '', '1.1'); + $request = new ClientRequestStream($connectionManager, $requestData); + + $request->on('close', $this->expectCallableOnce()); + + $request->end(); + + $request->handleData("HTTP/1.1 200 OK\r\n\r\n"); + + $this->assertNotNull($endEvent); + call_user_func($endEvent); // $endEvent() (PHP 5.4+) + } + + public function testStreamShouldNotReuseConnectionWhenResponseContainsContentLengthButIsTerminatedByUnexpectedCloseEvent() + { + $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection->expects($this->once())->method('write')->with("GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n"); + $connection->expects($this->atMost(1))->method('isReadable')->willReturn(false); + $connection->expects($this->once())->method('close'); + + $closeEvent = null; + $eventName = null; + $connection->expects($this->any())->method('on')->with($this->callback(function ($name) use (&$eventName) { + $eventName = $name; + return true; + }), $this->callback(function ($cb) use (&$closeEvent, &$eventName) { + if ($eventName === 'close') { + $closeEvent = $cb; + } + return true; + })); + + $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection)); + + $requestData = new Request('GET', 'http://www.example.com', array(), '', '1.1'); + $request = new ClientRequestStream($connectionManager, $requestData); + + $request->on('close', $this->expectCallableOnce()); + + $request->end(); + + $request->handleData("HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\n"); + + $this->assertNotNull($closeEvent); + call_user_func($closeEvent); // $closeEvent() (PHP 5.4+) + } + + public function testStreamShouldReuseConnectionWhenResponseContainsTransferEncodingChunkedAndResponseBody() + { + $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection->expects($this->once())->method('write')->with("GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n"); + $connection->expects($this->once())->method('isReadable')->willReturn(true); + $connection->expects($this->never())->method('close'); + + $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection)); + $connectionManager->expects($this->once())->method('handBack')->with($connection); + + $requestData = new Request('GET', 'http://www.example.com', array(), '', '1.1'); + $request = new ClientRequestStream($connectionManager, $requestData); + + $request->on('close', $this->expectCallableOnce()); + + $request->end(); + + $request->handleData("HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n2\r\nOK\r\n0\r\n\r\n"); + } + + public function testStreamShouldNotReuseConnectionWhenResponseContainsTransferEncodingChunkedAndResponseBodyContainsInvalidData() + { + $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection->expects($this->once())->method('write')->with("GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n"); + $connection->expects($this->atMost(1))->method('isReadable')->willReturn(true); + $connection->expects($this->once())->method('close'); + + $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection)); + + $requestData = new Request('GET', 'http://www.example.com', array(), '', '1.1'); + $request = new ClientRequestStream($connectionManager, $requestData); + + $request->on('close', $this->expectCallableOnce()); + + $request->end(); + + $request->handleData("HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\nINVALID\r\n"); + } + /** @test */ public function postRequestShouldSendAPostRequest() { From 28943f443a54ba4c2576ce68ee76274d190b6162 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Wed, 11 Jan 2023 13:28:15 +0100 Subject: [PATCH 425/456] Reuse existing connections for HTTP keep-alive --- src/Io/ClientConnectionManager.php | 90 +++++- src/Io/ClientRequestStream.php | 2 +- src/Io/Sender.php | 2 +- tests/Client/FunctionalIntegrationTest.php | 45 ++- tests/Io/ClientConnectionManagerTest.php | 310 ++++++++++++++++++++- tests/Io/ClientRequestStreamTest.php | 8 +- tests/Io/SenderTest.php | 23 +- 7 files changed, 448 insertions(+), 32 deletions(-) diff --git a/src/Io/ClientConnectionManager.php b/src/Io/ClientConnectionManager.php index eda2ea44..faac98b6 100644 --- a/src/Io/ClientConnectionManager.php +++ b/src/Io/ClientConnectionManager.php @@ -3,6 +3,8 @@ namespace React\Http\Io; use Psr\Http\Message\UriInterface; +use React\EventLoop\LoopInterface; +use React\EventLoop\TimerInterface; use React\Promise\PromiseInterface; use React\Socket\ConnectionInterface; use React\Socket\ConnectorInterface; @@ -18,9 +20,28 @@ class ClientConnectionManager /** @var ConnectorInterface */ private $connector; - public function __construct(ConnectorInterface $connector) + /** @var LoopInterface */ + private $loop; + + /** @var string[] */ + private $idleUris = array(); + + /** @var ConnectionInterface[] */ + private $idleConnections = array(); + + /** @var TimerInterface[] */ + private $idleTimers = array(); + + /** @var \Closure[] */ + private $idleStreamHandlers = array(); + + /** @var float */ + private $maximumTimeToKeepAliveIdleConnection = 0.001; + + public function __construct(ConnectorInterface $connector, LoopInterface $loop) { $this->connector = $connector; + $this->loop = $loop; } /** @@ -39,15 +60,78 @@ public function connect(UriInterface $uri) if ($port === null) { $port = $scheme === 'https' ? 443 : 80; } + $uri = ($scheme === 'https' ? 'tls://' : '') . $uri->getHost() . ':' . $port; + + // Reuse idle connection for same URI if available + foreach ($this->idleConnections as $id => $connection) { + if ($this->idleUris[$id] === $uri) { + assert($this->idleStreamHandlers[$id] instanceof \Closure); + $connection->removeListener('close', $this->idleStreamHandlers[$id]); + $connection->removeListener('data', $this->idleStreamHandlers[$id]); + $connection->removeListener('error', $this->idleStreamHandlers[$id]); + + assert($this->idleTimers[$id] instanceof TimerInterface); + $this->loop->cancelTimer($this->idleTimers[$id]); + unset($this->idleUris[$id], $this->idleConnections[$id], $this->idleTimers[$id], $this->idleStreamHandlers[$id]); - return $this->connector->connect(($scheme === 'https' ? 'tls://' : '') . $uri->getHost() . ':' . $port); + return \React\Promise\resolve($connection); + } + } + + // Create new connection if no idle connection to same URI is available + return $this->connector->connect($uri); } /** + * Hands back an idle connection to the connection manager for possible future reuse. + * * @return void */ - public function handBack(ConnectionInterface $connection) + public function keepAlive(UriInterface $uri, ConnectionInterface $connection) { + $scheme = $uri->getScheme(); + assert($scheme === 'https' || $scheme === 'http'); + + $port = $uri->getPort(); + if ($port === null) { + $port = $scheme === 'https' ? 443 : 80; + } + + $this->idleUris[] = ($scheme === 'https' ? 'tls://' : '') . $uri->getHost() . ':' . $port; + $this->idleConnections[] = $connection; + + $that = $this; + $cleanUp = function () use ($connection, $that) { + // call public method to support legacy PHP 5.3 + $that->cleanUpConnection($connection); + }; + + // clean up and close connection when maximum time to keep-alive idle connection has passed + $this->idleTimers[] = $this->loop->addTimer($this->maximumTimeToKeepAliveIdleConnection, $cleanUp); + + // clean up and close connection when unexpected close/data/error event happens during idle time + $this->idleStreamHandlers[] = $cleanUp; + $connection->on('close', $cleanUp); + $connection->on('data', $cleanUp); + $connection->on('error', $cleanUp); + } + + /** + * @internal + * @return void + */ + public function cleanUpConnection(ConnectionInterface $connection) // private (PHP 5.4+) + { + $id = \array_search($connection, $this->idleConnections, true); + if ($id === false) { + return; + } + + assert(\is_int($id)); + assert($this->idleTimers[$id] instanceof TimerInterface); + $this->loop->cancelTimer($this->idleTimers[$id]); + unset($this->idleUris[$id], $this->idleConnections[$id], $this->idleTimers[$id], $this->idleStreamHandlers[$id]); + $connection->close(); } } diff --git a/src/Io/ClientRequestStream.php b/src/Io/ClientRequestStream.php index e9716b45..0220f008 100644 --- a/src/Io/ClientRequestStream.php +++ b/src/Io/ClientRequestStream.php @@ -181,7 +181,7 @@ public function handleData($data) $input->on('close', function () use ($connection, $that, $connectionManager, $request, $response, &$successfulEndReceived) { // only reuse connection after successful response and both request and response allow keep alive if ($successfulEndReceived && $connection->isReadable() && $that->hasMessageKeepAliveEnabled($response) && $that->hasMessageKeepAliveEnabled($request)) { - $connectionManager->handBack($connection); + $connectionManager->keepAlive($request->getUri(), $connection); } else { $connection->close(); } diff --git a/src/Io/Sender.php b/src/Io/Sender.php index 68c09322..c117d87d 100644 --- a/src/Io/Sender.php +++ b/src/Io/Sender.php @@ -54,7 +54,7 @@ public static function createFromLoop(LoopInterface $loop, ConnectorInterface $c $connector = new Connector(array(), $loop); } - return new self(new HttpClient(new ClientConnectionManager($connector))); + return new self(new HttpClient(new ClientConnectionManager($connector, $loop))); } private $http; diff --git a/tests/Client/FunctionalIntegrationTest.php b/tests/Client/FunctionalIntegrationTest.php index 1c37d897..4925239c 100644 --- a/tests/Client/FunctionalIntegrationTest.php +++ b/tests/Client/FunctionalIntegrationTest.php @@ -3,6 +3,7 @@ namespace React\Tests\Http\Client; use Psr\Http\Message\ResponseInterface; +use React\EventLoop\Loop; use React\Http\Client\Client; use React\Http\Io\ClientConnectionManager; use React\Http\Message\Request; @@ -47,7 +48,7 @@ public function testRequestToLocalhostEmitsSingleRemoteConnection() }); $port = parse_url($socket->getAddress(), PHP_URL_PORT); - $client = new Client(new ClientConnectionManager(new Connector())); + $client = new Client(new ClientConnectionManager(new Connector(), Loop::get())); $request = $client->request(new Request('GET', 'http://localhost:' . $port, array(), '', '1.0')); $promise = Stream\first($request, 'close'); @@ -56,7 +57,7 @@ public function testRequestToLocalhostEmitsSingleRemoteConnection() \React\Async\await(\React\Promise\Timer\timeout($promise, self::TIMEOUT_LOCAL)); } - public function testRequestToLocalhostWillConnectAndCloseConnectionAfterResponseUntilKeepAliveIsActuallySupported() + public function testRequestToLocalhostWillConnectAndCloseConnectionAfterResponseWhenKeepAliveTimesOut() { $socket = new SocketServer('127.0.0.1:0'); $socket->on('connection', $this->expectCallableOnce()); @@ -72,7 +73,7 @@ public function testRequestToLocalhostWillConnectAndCloseConnectionAfterResponse }); $port = parse_url($socket->getAddress(), PHP_URL_PORT); - $client = new Client(new ClientConnectionManager(new Connector())); + $client = new Client(new ClientConnectionManager(new Connector(), Loop::get())); $request = $client->request(new Request('GET', 'http://localhost:' . $port, array(), '', '1.1')); $request->end(); @@ -80,6 +81,34 @@ public function testRequestToLocalhostWillConnectAndCloseConnectionAfterResponse \React\Async\await(\React\Promise\Timer\timeout($promise, self::TIMEOUT_LOCAL)); } + public function testRequestToLocalhostWillReuseExistingConnectionForSecondRequest() + { + $socket = new SocketServer('127.0.0.1:0'); + $socket->on('connection', $this->expectCallableOnce()); + + $socket->on('connection', function (ConnectionInterface $connection) use ($socket) { + $connection->on('data', function () use ($connection) { + $connection->write("HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nOK"); + }); + $socket->close(); + }); + $port = parse_url($socket->getAddress(), PHP_URL_PORT); + + $client = new Client(new ClientConnectionManager(new Connector(), Loop::get())); + + $request = $client->request(new Request('GET', 'http://localhost:' . $port, array(), '', '1.1')); + $promise = Stream\first($request, 'close'); + $request->end(); + + \React\Async\await(\React\Promise\Timer\timeout($promise, self::TIMEOUT_LOCAL)); + + $request = $client->request(new Request('GET', 'http://localhost:' . $port, array(), '', '1.1')); + $promise = Stream\first($request, 'close'); + $request->end(); + + \React\Async\await(\React\Promise\Timer\timeout($promise, self::TIMEOUT_LOCAL)); + } + public function testRequestLegacyHttpServerWithOnlyLineFeedReturnsSuccessfulResponse() { $socket = new SocketServer('127.0.0.1:0'); @@ -88,7 +117,7 @@ public function testRequestLegacyHttpServerWithOnlyLineFeedReturnsSuccessfulResp $socket->close(); }); - $client = new Client(new ClientConnectionManager(new Connector())); + $client = new Client(new ClientConnectionManager(new Connector(), Loop::get())); $request = $client->request(new Request('GET', str_replace('tcp:', 'http:', $socket->getAddress()), array(), '', '1.0')); $once = $this->expectCallableOnceWith('body'); @@ -108,7 +137,7 @@ public function testSuccessfulResponseEmitsEnd() // max_nesting_level was set to 100 for PHP Versions < 5.4 which resulted in failing test for legacy PHP ini_set('xdebug.max_nesting_level', 256); - $client = new Client(new ClientConnectionManager(new Connector())); + $client = new Client(new ClientConnectionManager(new Connector(), Loop::get())); $request = $client->request(new Request('GET', 'http://www.google.com/', array(), '', '1.0')); @@ -133,7 +162,7 @@ public function testPostDataReturnsData() // max_nesting_level was set to 100 for PHP Versions < 5.4 which resulted in failing test for legacy PHP ini_set('xdebug.max_nesting_level', 256); - $client = new Client(new ClientConnectionManager(new Connector())); + $client = new Client(new ClientConnectionManager(new Connector(), Loop::get())); $data = str_repeat('.', 33000); $request = $client->request(new Request('POST', 'https://' . (mt_rand(0, 1) === 0 ? 'eu.' : '') . 'httpbin.org/post', array('Content-Length' => strlen($data)), '', '1.0')); @@ -165,7 +194,7 @@ public function testPostJsonReturnsData() $this->markTestSkipped('Not supported on HHVM'); } - $client = new Client(new ClientConnectionManager(new Connector())); + $client = new Client(new ClientConnectionManager(new Connector(), Loop::get())); $data = json_encode(array('numbers' => range(1, 50))); $request = $client->request(new Request('POST', 'https://httpbin.org/post', array('Content-Length' => strlen($data), 'Content-Type' => 'application/json'), '', '1.0')); @@ -195,7 +224,7 @@ public function testCancelPendingConnectionEmitsClose() // max_nesting_level was set to 100 for PHP Versions < 5.4 which resulted in failing test for legacy PHP ini_set('xdebug.max_nesting_level', 256); - $client = new Client(new ClientConnectionManager(new Connector())); + $client = new Client(new ClientConnectionManager(new Connector(), Loop::get())); $request = $client->request(new Request('GET', 'http://www.google.com/', array(), '', '1.0')); $request->on('error', $this->expectCallableNever()); diff --git a/tests/Io/ClientConnectionManagerTest.php b/tests/Io/ClientConnectionManagerTest.php index 143676e4..b28c7964 100644 --- a/tests/Io/ClientConnectionManagerTest.php +++ b/tests/Io/ClientConnectionManagerTest.php @@ -5,6 +5,7 @@ use RingCentral\Psr7\Uri; use React\Http\Io\ClientConnectionManager; use React\Promise\Promise; +use React\Promise\PromiseInterface; use React\Tests\Http\TestCase; class ClientConnectionManagerTest extends TestCase @@ -15,9 +16,13 @@ public function testConnectWithHttpsUriShouldConnectToTlsWithDefaultPort() $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); $connector->expects($this->once())->method('connect')->with('tls://reactphp.org:443')->willReturn($promise); - $connectionManager = new ClientConnectionManager($connector); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $connectionManager = new ClientConnectionManager($connector, $loop); $ret = $connectionManager->connect(new Uri('https://reactphp.org/')); + + assert($ret instanceof PromiseInterface); $this->assertSame($promise, $ret); } @@ -27,7 +32,9 @@ public function testConnectWithHttpUriShouldConnectToTcpWithDefaultPort() $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); $connector->expects($this->once())->method('connect')->with('reactphp.org:80')->willReturn($promise); - $connectionManager = new ClientConnectionManager($connector); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $connectionManager = new ClientConnectionManager($connector, $loop); $ret = $connectionManager->connect(new Uri('http://reactphp.org/')); $this->assertSame($promise, $ret); @@ -39,7 +46,9 @@ public function testConnectWithExplicitPortShouldConnectWithGivenPort() $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); $connector->expects($this->once())->method('connect')->with('reactphp.org:8080')->willReturn($promise); - $connectionManager = new ClientConnectionManager($connector); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $connectionManager = new ClientConnectionManager($connector, $loop); $ret = $connectionManager->connect(new Uri('http://reactphp.org:8080/')); $this->assertSame($promise, $ret); @@ -50,7 +59,9 @@ public function testConnectWithInvalidSchemeShouldRejectWithException() $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); $connector->expects($this->never())->method('connect'); - $connectionManager = new ClientConnectionManager($connector); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $connectionManager = new ClientConnectionManager($connector, $loop); $promise = $connectionManager->connect(new Uri('ftp://reactphp.org/')); @@ -68,7 +79,9 @@ public function testConnectWithoutSchemeShouldRejectWithException() $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); $connector->expects($this->never())->method('connect'); - $connectionManager = new ClientConnectionManager($connector); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $connectionManager = new ClientConnectionManager($connector, $loop); $promise = $connectionManager->connect(new Uri('reactphp.org')); @@ -81,15 +94,296 @@ public function testConnectWithoutSchemeShouldRejectWithException() $this->assertEquals('Invalid request URL given', $exception->getMessage()); } - public function testHandBackWillCloseGivenConnectionUntilKeepAliveIsActuallySupported() + public function testConnectReusesIdleConnectionFromPreviousKeepAliveCallWithoutUsingConnectorAndWillAddAndRemoveStreamEventsAndAddAndCancelIdleTimer() + { + $connectionToReuse = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + + $streamHandler = null; + $connectionToReuse->expects($this->exactly(3))->method('on')->withConsecutive( + array( + 'close', + $this->callback(function ($cb) use (&$streamHandler) { + $streamHandler = $cb; + return true; + }) + ), + array( + 'data', + $this->callback(function ($cb) use (&$streamHandler) { + assert($streamHandler instanceof \Closure); + return $cb === $streamHandler; + }) + ), + array( + 'error', + $this->callback(function ($cb) use (&$streamHandler) { + assert($streamHandler instanceof \Closure); + return $cb === $streamHandler; + }) + ) + ); + + $connectionToReuse->expects($this->exactly(3))->method('removeListener')->withConsecutive( + array( + 'close', + $this->callback(function ($cb) use (&$streamHandler) { + assert($streamHandler instanceof \Closure); + return $cb === $streamHandler; + }) + ), + array( + 'data', + $this->callback(function ($cb) use (&$streamHandler) { + assert($streamHandler instanceof \Closure); + return $cb === $streamHandler; + }) + ), + array( + 'error', + $this->callback(function ($cb) use (&$streamHandler) { + assert($streamHandler instanceof \Closure); + return $cb === $streamHandler; + }) + ) + ); + + $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $connector->expects($this->never())->method('connect'); + + $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->once())->method('addTimer')->willReturn($timer); + $loop->expects($this->once())->method('cancelTimer')->with($timer); + + $connectionManager = new ClientConnectionManager($connector, $loop); + + $connectionManager->keepAlive(new Uri('https://reactphp.org/'), $connectionToReuse); + + $promise = $connectionManager->connect(new Uri('https://reactphp.org/')); + assert($promise instanceof PromiseInterface); + + $connection = null; + $promise->then(function ($value) use (&$connection) { + $connection = $value; + }); + + $this->assertSame($connectionToReuse, $connection); + } + + public function testConnectReusesIdleConnectionFromPreviousKeepAliveCallWithoutUsingConnectorAlsoWhenUriPathAndQueryAndFragmentIsDifferent() { + $connectionToReuse = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $connector->expects($this->never())->method('connect'); + + $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->once())->method('addTimer')->willReturn($timer); + $loop->expects($this->once())->method('cancelTimer')->with($timer); + + $connectionManager = new ClientConnectionManager($connector, $loop); + + $connectionManager->keepAlive(new Uri('https://reactphp.org/http?foo#bar'), $connectionToReuse); + + $promise = $connectionManager->connect(new Uri('https://reactphp.org/http/')); + assert($promise instanceof PromiseInterface); + + $connection = null; + $promise->then(function ($value) use (&$connection) { + $connection = $value; + }); + + $this->assertSame($connectionToReuse, $connection); + } + + public function testConnectUsesConnectorWithSameUriAndReturnsPromiseForNewConnectionFromConnectorWhenPreviousKeepAliveCallUsedDifferentUri() + { + $connectionToReuse = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + + $promise = new Promise(function () { }); + $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $connector->expects($this->once())->method('connect')->with('tls://reactphp.org:443')->willReturn($promise); + + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $connectionManager = new ClientConnectionManager($connector, $loop); + + $connectionManager->keepAlive(new Uri('http://reactphp.org/'), $connectionToReuse); + + $ret = $connectionManager->connect(new Uri('https://reactphp.org/')); + + assert($ret instanceof PromiseInterface); + $this->assertSame($promise, $ret); + } + + public function testConnectUsesConnectorForNewConnectionWhenPreviousConnectReusedIdleConnectionFromPreviousKeepAliveCall() + { + $firstConnection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $secondConnection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + + $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $connector->expects($this->once())->method('connect')->with('tls://reactphp.org:443')->willReturn(\React\Promise\resolve($secondConnection)); + + $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->once())->method('addTimer')->willReturn($timer); + $loop->expects($this->once())->method('cancelTimer')->with($timer); + + $connectionManager = new ClientConnectionManager($connector, $loop); - $connectionManager = new ClientConnectionManager($connector); + $connectionManager->keepAlive(new Uri('https://reactphp.org/'), $firstConnection); + $promise = $connectionManager->connect(new Uri('https://reactphp.org/')); + assert($promise instanceof PromiseInterface); + + $promise = $connectionManager->connect(new Uri('https://reactphp.org/')); + assert($promise instanceof PromiseInterface); + + $connection = null; + $promise->then(function ($value) use (&$connection) { + $connection = $value; + }); + + $this->assertSame($secondConnection, $connection); + } + + public function testKeepAliveAddsTimerAndDoesNotCloseConnectionImmediately() + { + $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection->expects($this->never())->method('close'); + + $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->once())->method('addTimer')->with(0.001, $this->anything()); + + $connectionManager = new ClientConnectionManager($connector, $loop); + + $connectionManager->keepAlive(new Uri('https://reactphp.org/'), $connection); + } + + public function testKeepAliveClosesConnectionAfterIdleTimeout() + { $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); $connection->expects($this->once())->method('close'); - $connectionManager->handBack($connection); + $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + + $timerCallback = null; + $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->once())->method('addTimer')->with($this->anything(), $this->callback(function ($cb) use (&$timerCallback) { + $timerCallback = $cb; + return true; + }))->willReturn($timer); + $loop->expects($this->once())->method('cancelTimer')->with($timer); + + $connectionManager = new ClientConnectionManager($connector, $loop); + + $connectionManager->keepAlive(new Uri('https://reactphp.org/'), $connection); + + // manually invoker timer function to emulate time has passed + $this->assertNotNull($timerCallback); + call_user_func($timerCallback); // $timerCallback() (PHP 5.4+) + } + + public function testConnectUsesConnectorForNewConnectionWhenIdleConnectionFromPreviousKeepAliveCallHasAlreadyTimedOut() + { + $firstConnection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $firstConnection->expects($this->once())->method('close'); + + $secondConnection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $secondConnection->expects($this->never())->method('close'); + + $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $connector->expects($this->once())->method('connect')->with('tls://reactphp.org:443')->willReturn(\React\Promise\resolve($secondConnection)); + + $timerCallback = null; + $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->once())->method('addTimer')->with($this->anything(), $this->callback(function ($cb) use (&$timerCallback) { + $timerCallback = $cb; + return true; + }))->willReturn($timer); + $loop->expects($this->once())->method('cancelTimer')->with($timer); + + $connectionManager = new ClientConnectionManager($connector, $loop); + + $connectionManager->keepAlive(new Uri('https://reactphp.org/'), $firstConnection); + + // manually invoker timer function to emulate time has passed + $this->assertNotNull($timerCallback); + call_user_func($timerCallback); // $timerCallback() (PHP 5.4+) + + $promise = $connectionManager->connect(new Uri('https://reactphp.org/')); + assert($promise instanceof PromiseInterface); + + $connection = null; + $promise->then(function ($value) use (&$connection) { + $connection = $value; + }); + + $this->assertSame($secondConnection, $connection); + } + + public function testConnectUsesConnectorForNewConnectionWhenIdleConnectionFromPreviousKeepAliveCallHasAlreadyFiredUnexpectedStreamEventBeforeIdleTimeoutThatClosesConnection() + { + $firstConnection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $firstConnection->expects($this->once())->method('close'); + + $streamHandler = null; + $firstConnection->expects($this->exactly(3))->method('on')->withConsecutive( + array( + 'close', + $this->callback(function ($cb) use (&$streamHandler) { + $streamHandler = $cb; + return true; + }) + ), + array( + 'data', + $this->callback(function ($cb) use (&$streamHandler) { + assert($streamHandler instanceof \Closure); + return $cb === $streamHandler; + }) + ), + array( + 'error', + $this->callback(function ($cb) use (&$streamHandler) { + assert($streamHandler instanceof \Closure); + return $cb === $streamHandler; + }) + ) + ); + + $secondConnection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $secondConnection->expects($this->never())->method('close'); + + $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $connector->expects($this->once())->method('connect')->with('tls://reactphp.org:443')->willReturn(\React\Promise\resolve($secondConnection)); + + $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->once())->method('addTimer')->willReturn($timer); + $loop->expects($this->once())->method('cancelTimer')->with($timer); + + $connectionManager = new ClientConnectionManager($connector, $loop); + + $connectionManager->keepAlive(new Uri('https://reactphp.org/'), $firstConnection); + + // manually invoke connection close to emulate server closing idle connection before idle timeout + $this->assertNotNull($streamHandler); + call_user_func($streamHandler); // $streamHandler() (PHP 5.4+) + + $promise = $connectionManager->connect(new Uri('https://reactphp.org/')); + assert($promise instanceof PromiseInterface); + + $connection = null; + $promise->then(function ($value) use (&$connection) { + $connection = $value; + }); + + $this->assertSame($secondConnection, $connection); } } diff --git a/tests/Io/ClientRequestStreamTest.php b/tests/Io/ClientRequestStreamTest.php index d257994b..4649087a 100644 --- a/tests/Io/ClientRequestStreamTest.php +++ b/tests/Io/ClientRequestStreamTest.php @@ -488,7 +488,7 @@ public function testStreamShouldReuseConnectionForHttp11ByDefault() $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection)); - $connectionManager->expects($this->once())->method('handBack')->with($connection); + $connectionManager->expects($this->once())->method('keepAlive')->with(new Uri('http://www.example.com'), $connection); $requestData = new Request('GET', 'http://www.example.com', array(), '', '1.1'); $request = new ClientRequestStream($connectionManager, $requestData); @@ -569,7 +569,7 @@ public function testStreamShouldReuseConnectionForHttp10WhenBothRequestAndRespon $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection)); - $connectionManager->expects($this->once())->method('handBack')->with($connection); + $connectionManager->expects($this->once())->method('keepAlive')->with(new Uri('http://www.example.com'), $connection); $requestData = new Request('GET', 'http://www.example.com', array('Connection' => 'keep-alive'), '', '1.0'); $request = new ClientRequestStream($connectionManager, $requestData); @@ -590,7 +590,7 @@ public function testStreamShouldReuseConnectionForHttp10WhenBothRequestAndRespon $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection)); - $connectionManager->expects($this->once())->method('handBack')->with($connection); + $connectionManager->expects($this->once())->method('keepAlive')->with(new Uri('http://www.example.com'), $connection); $requestData = new Request('GET', 'http://www.example.com', array('Connection' => 'FOO, KEEP-ALIVE, BAR'), '', '1.0'); $request = new ClientRequestStream($connectionManager, $requestData); @@ -681,7 +681,7 @@ public function testStreamShouldReuseConnectionWhenResponseContainsTransferEncod $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection)); - $connectionManager->expects($this->once())->method('handBack')->with($connection); + $connectionManager->expects($this->once())->method('keepAlive')->with(new Uri('http://www.example.com'), $connection); $requestData = new Request('GET', 'http://www.example.com', array(), '', '1.1'); $request = new ClientRequestStream($connectionManager, $requestData); diff --git a/tests/Io/SenderTest.php b/tests/Io/SenderTest.php index 220424e9..0f555f9c 100644 --- a/tests/Io/SenderTest.php +++ b/tests/Io/SenderTest.php @@ -14,11 +14,20 @@ class SenderTest extends TestCase { - public function testCreateFromLoop() + /** @var \React\EventLoop\LoopInterface */ + private $loop; + + /** + * @before + */ + public function setUpLoop() { - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $this->loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + } - $sender = Sender::createFromLoop($loop, null); + public function testCreateFromLoop() + { + $sender = Sender::createFromLoop($this->loop, null); $this->assertInstanceOf('React\Http\Io\Sender', $sender); } @@ -28,7 +37,7 @@ public function testSenderRejectsInvalidUri() $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); $connector->expects($this->never())->method('connect'); - $sender = new Sender(new HttpClient(new ClientConnectionManager($connector))); + $sender = new Sender(new HttpClient(new ClientConnectionManager($connector, $this->loop))); $request = new Request('GET', 'www.google.com'); @@ -47,7 +56,7 @@ public function testSenderConnectorRejection() $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); $connector->expects($this->once())->method('connect')->willReturn(Promise\reject(new \RuntimeException('Rejected'))); - $sender = new Sender(new HttpClient(new ClientConnectionManager($connector))); + $sender = new Sender(new HttpClient(new ClientConnectionManager($connector, $this->loop))); $request = new Request('GET', 'http://www.google.com/'); @@ -374,7 +383,7 @@ public function testCancelRequestWillCancelConnector() $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); $connector->expects($this->once())->method('connect')->willReturn($promise); - $sender = new Sender(new HttpClient(new ClientConnectionManager($connector))); + $sender = new Sender(new HttpClient(new ClientConnectionManager($connector, $this->loop))); $request = new Request('GET', 'http://www.google.com/'); @@ -397,7 +406,7 @@ public function testCancelRequestWillCloseConnection() $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); $connector->expects($this->once())->method('connect')->willReturn(Promise\resolve($connection)); - $sender = new Sender(new HttpClient(new ClientConnectionManager($connector))); + $sender = new Sender(new HttpClient(new ClientConnectionManager($connector, $this->loop))); $request = new Request('GET', 'http://www.google.com/'); From ebaf6f132cd821230447a2287be79d3c3c23d625 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 19 Jan 2023 19:16:28 +0100 Subject: [PATCH 426/456] Add `Connection: close` default header to allow toggling keep-alive --- src/Browser.php | 1 + src/Io/Sender.php | 7 ----- src/Io/Transaction.php | 2 +- tests/BrowserTest.php | 28 +++++++++++++++++ tests/FunctionalBrowserTest.php | 54 +++++++++++++++++++++++++++++++ tests/Io/SenderTest.php | 56 --------------------------------- 6 files changed, 84 insertions(+), 64 deletions(-) diff --git a/src/Browser.php b/src/Browser.php index 3e3458af..12bce6b5 100644 --- a/src/Browser.php +++ b/src/Browser.php @@ -23,6 +23,7 @@ class Browser private $baseUrl; private $protocolVersion = '1.1'; private $defaultHeaders = array( + 'Connection' => 'close', 'User-Agent' => 'ReactPHP/1' ); diff --git a/src/Io/Sender.php b/src/Io/Sender.php index c117d87d..3598d31a 100644 --- a/src/Io/Sender.php +++ b/src/Io/Sender.php @@ -98,13 +98,6 @@ public function send(RequestInterface $request) $size = 0; } - // automatically add `Connection: close` request header for HTTP/1.1 requests to avoid connection reuse - if ($request->getProtocolVersion() === '1.1') { - $request = $request->withHeader('Connection', 'close'); - } else { - $request = $request->withoutHeader('Connection'); - } - // automatically add `Authorization: Basic …` request header if URL includes `user:pass@host` if ($request->getUri()->getUserInfo() !== '' && !$request->hasHeader('Authorization')) { $request = $request->withHeader('Authorization', 'Basic ' . \base64_encode($request->getUri()->getUserInfo())); diff --git a/src/Io/Transaction.php b/src/Io/Transaction.php index bfa42241..b93c490c 100644 --- a/src/Io/Transaction.php +++ b/src/Io/Transaction.php @@ -302,7 +302,7 @@ private function makeRedirectRequest(RequestInterface $request, UriInterface $lo ->withMethod($request->getMethod() === 'HEAD' ? 'HEAD' : 'GET') ->withoutHeader('Content-Type') ->withoutHeader('Content-Length') - ->withBody(new EmptyBodyStream()); + ->withBody(new BufferedBody('')); } return $request; diff --git a/tests/BrowserTest.php b/tests/BrowserTest.php index 21242a5d..d01de9c5 100644 --- a/tests/BrowserTest.php +++ b/tests/BrowserTest.php @@ -556,6 +556,8 @@ public function testWithMultipleHeadersShouldBeMergedCorrectlyWithMultipleDefaul 'user-Agent' => array('ABC'), 'another-header' => array('value'), 'custom-header' => array('data'), + + 'Connection' => array('close') ); $that->assertEquals($expectedHeaders, $request->getHeaders()); @@ -584,6 +586,32 @@ public function testWithoutHeaderShouldRemoveExistingHeader() $this->browser->get('http://example.com/'); } + public function testWithoutHeaderConnectionShouldRemoveDefaultConnectionHeader() + { + $this->browser = $this->browser->withoutHeader('Connection'); + + $that = $this; + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { + $that->assertEquals(array(), $request->getHeader('Connection')); + return true; + }))->willReturn(new Promise(function () { })); + + $this->browser->get('http://example.com/'); + } + + public function testWithHeaderConnectionShouldOverwriteDefaultConnectionHeader() + { + $this->browser = $this->browser->withHeader('Connection', 'keep-alive'); + + $that = $this; + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { + $that->assertEquals(array('keep-alive'), $request->getHeader('Connection')); + return true; + }))->willReturn(new Promise(function () { })); + + $this->browser->get('http://example.com/'); + } + public function testBrowserShouldSendDefaultUserAgentHeader() { $that = $this; diff --git a/tests/FunctionalBrowserTest.php b/tests/FunctionalBrowserTest.php index 6def2ecc..7ab909de 100644 --- a/tests/FunctionalBrowserTest.php +++ b/tests/FunctionalBrowserTest.php @@ -553,6 +553,60 @@ public function testReceiveStreamAndExplicitlyCloseConnectionEvenWhenServerKeeps $socket->close(); } + public function testRequestWillCreateNewConnectionForSecondRequestByDefaultEvenWhenServerKeepsConnectionOpen() + { + $twice = $this->expectCallableOnce(); + $socket = new SocketServer('127.0.0.1:0'); + $socket->on('connection', function (\React\Socket\ConnectionInterface $connection) use ($socket, $twice) { + $connection->on('data', function () use ($connection) { + $connection->write("HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello"); + }); + + $socket->on('connection', $twice); + $socket->on('connection', function () use ($socket) { + $socket->close(); + }); + }); + + $this->base = str_replace('tcp:', 'http:', $socket->getAddress()) . '/'; + + $response = \React\Async\await($this->browser->get($this->base . 'get')); + assert($response instanceof ResponseInterface); + $this->assertEquals('hello', (string)$response->getBody()); + + $response = \React\Async\await($this->browser->get($this->base . 'get')); + assert($response instanceof ResponseInterface); + $this->assertEquals('hello', (string)$response->getBody()); + } + + public function testRequestWithoutConnectionHeaderWillReuseExistingConnectionForSecondRequest() + { + $this->socket->on('connection', $this->expectCallableOnce()); + + // remove default `Connection: close` request header to enable keep-alive + $this->browser = $this->browser->withoutHeader('Connection'); + + $response = \React\Async\await($this->browser->get($this->base . 'get')); + assert($response instanceof ResponseInterface); + $this->assertEquals('hello', (string)$response->getBody()); + + $response = \React\Async\await($this->browser->get($this->base . 'get')); + assert($response instanceof ResponseInterface); + $this->assertEquals('hello', (string)$response->getBody()); + } + + public function testRequestWithoutConnectionHeaderWillReuseExistingConnectionForRedirectedRequest() + { + $this->socket->on('connection', $this->expectCallableOnce()); + + // remove default `Connection: close` request header to enable keep-alive + $this->browser = $this->browser->withoutHeader('Connection'); + + $response = \React\Async\await($this->browser->get($this->base . 'redirect-to?url=get')); + assert($response instanceof ResponseInterface); + $this->assertEquals('hello', (string)$response->getBody()); + } + public function testPostStreamChunked() { $stream = new ThroughStream(); diff --git a/tests/Io/SenderTest.php b/tests/Io/SenderTest.php index 0f555f9c..3c8c4761 100644 --- a/tests/Io/SenderTest.php +++ b/tests/Io/SenderTest.php @@ -290,62 +290,6 @@ public function testSendCustomMethodWithExplicitContentLengthZeroWillBePassedAsI $sender->send($request); } - /** @test */ - public function getHttp10RequestShouldSendAGetRequestWithoutConnectionHeaderByDefault() - { - $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); - $client->expects($this->once())->method('request')->with($this->callback(function (RequestInterface $request) { - return !$request->hasHeader('Connection'); - }))->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock()); - - $sender = new Sender($client); - - $request = new Request('GET', 'http://www.example.com', array(), '', '1.0'); - $sender->send($request); - } - - /** @test */ - public function getHttp10RequestShouldSendAGetRequestWithoutConnectionHeaderEvenWhenConnectionKeepAliveHeaderIsSpecified() - { - $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); - $client->expects($this->once())->method('request')->with($this->callback(function (RequestInterface $request) { - return !$request->hasHeader('Connection'); - }))->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock()); - - $sender = new Sender($client); - - $request = new Request('GET', 'http://www.example.com', array('Connection' => 'keep-alive'), '', '1.0'); - $sender->send($request); - } - - /** @test */ - public function getHttp11RequestShouldSendAGetRequestWithConnectionCloseHeaderByDefault() - { - $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); - $client->expects($this->once())->method('request')->with($this->callback(function (RequestInterface $request) { - return $request->getHeaderLine('Connection') === 'close'; - }))->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock()); - - $sender = new Sender($client); - - $request = new Request('GET', 'http://www.example.com', array(), '', '1.1'); - $sender->send($request); - } - - /** @test */ - public function getHttp11RequestShouldSendAGetRequestWithConnectionCloseHeaderEvenWhenConnectionKeepAliveHeaderIsSpecified() - { - $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); - $client->expects($this->once())->method('request')->with($this->callback(function (RequestInterface $request) { - return $request->getHeaderLine('Connection') === 'close'; - }))->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock()); - - $sender = new Sender($client); - - $request = new Request('GET', 'http://www.example.com', array('Connection' => 'keep-alive'), '', '1.1'); - $sender->send($request); - } - /** @test */ public function getRequestWithUserAndPassShouldSendAGetRequestWithBasicAuthorizationHeader() { From b3594f7936b92f9fc2d5f9e84dc01bdb95a72167 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Thu, 16 Feb 2023 17:25:39 +0100 Subject: [PATCH 427/456] Stop parsing multipart request bodies once the configured limit of form fields and files has been reached This fix is inspired by how PHP is handling it but without following the ini setting. Such setting isn't needed as the limits on files and form fields are enough. --- src/Io/MultipartParser.php | 15 +++++++++++++++ tests/Io/MultipartParserTest.php | 27 ++++++++++++++++++++++++++- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/src/Io/MultipartParser.php b/src/Io/MultipartParser.php index 536694fd..6a874336 100644 --- a/src/Io/MultipartParser.php +++ b/src/Io/MultipartParser.php @@ -26,6 +26,13 @@ final class MultipartParser */ private $maxFileSize; + /** + * Based on $maxInputVars and $maxFileUploads + * + * @var int + */ + private $maxMultipartBodyParts; + /** * ini setting "max_input_vars" * @@ -62,6 +69,7 @@ final class MultipartParser */ private $maxFileUploads; + private $multipartBodyPartCount = 0; private $postCount = 0; private $filesCount = 0; private $emptyCount = 0; @@ -87,6 +95,8 @@ public function __construct($uploadMaxFilesize = null, $maxFileUploads = null) $this->uploadMaxFilesize = IniUtil::iniSizeToBytes($uploadMaxFilesize); $this->maxFileUploads = $maxFileUploads === null ? (\ini_get('file_uploads') === '' ? 0 : (int)\ini_get('max_file_uploads')) : (int)$maxFileUploads; + + $this->maxMultipartBodyParts = $this->maxInputVars + $this->maxFileUploads; } public function parse(ServerRequestInterface $request) @@ -101,6 +111,7 @@ public function parse(ServerRequestInterface $request) $request = $this->request; $this->request = null; + $this->multipartBodyPartCount = 0; $this->postCount = 0; $this->filesCount = 0; $this->emptyCount = 0; @@ -128,6 +139,10 @@ private function parseBody($boundary, $buffer) // parse one part and continue searching for next $this->parsePart(\substr($buffer, $start, $end - $start)); $start = $end; + + if (++$this->multipartBodyPartCount > $this->maxMultipartBodyParts) { + break; + } } } diff --git a/tests/Io/MultipartParserTest.php b/tests/Io/MultipartParserTest.php index 14550f57..5dfd6e43 100644 --- a/tests/Io/MultipartParserTest.php +++ b/tests/Io/MultipartParserTest.php @@ -1026,4 +1026,29 @@ public function testPostMaxFileSizeIgnoredByFilesComingBeforeIt() $this->assertTrue(isset($files['file4'])); $this->assertSame(UPLOAD_ERR_OK, $files['file4']->getError()); } -} \ No newline at end of file + + public function testWeOnlyParseTheAmountOfMultiPartChunksWeConfigured() + { + $boundary = "---------------------------12758086162038677464950549563"; + + $chunk = "--$boundary\r\n"; + $chunk .= "Content-Disposition: form-data; name=\"f\"\r\n"; + $chunk .= "\r\n"; + $chunk .= "u\r\n"; + $data = ''; + for ($i = 0; $i < 5000000; $i++) { + $data .= $chunk; + } + $data .= "--$boundary--\r\n"; + + $request = new ServerRequest('POST', 'http://example.com/', array( + 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, + ), $data, 1.1); + + $parser = new MultipartParser(); + $startTime = microtime(true); + $parser->parse($request); + $runTime = microtime(true) - $startTime; + $this->assertLessThan(1, $runTime); + } +} From 2942434617ebf896209901748f97083b454e01a0 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Thu, 2 Mar 2023 16:51:50 +0100 Subject: [PATCH 428/456] Improve multipart limits test The PR introducing this test assumed time would be enough to accurately predict behavior. This commit changes it to using introspection to check the exact state of the parser and expected state once finished parsing a multipart request body. To accomplish that it was required to make the cursor in the file an object property, so it can be inspected using reflection. --- src/Io/MultipartParser.php | 14 ++++++++------ tests/Io/MultipartParserTest.php | 29 ++++++++++++++++++++++------- 2 files changed, 30 insertions(+), 13 deletions(-) diff --git a/src/Io/MultipartParser.php b/src/Io/MultipartParser.php index 6a874336..539107ae 100644 --- a/src/Io/MultipartParser.php +++ b/src/Io/MultipartParser.php @@ -73,6 +73,7 @@ final class MultipartParser private $postCount = 0; private $filesCount = 0; private $emptyCount = 0; + private $cursor = 0; /** * @param int|string|null $uploadMaxFilesize @@ -112,6 +113,7 @@ public function parse(ServerRequestInterface $request) $request = $this->request; $this->request = null; $this->multipartBodyPartCount = 0; + $this->cursor = 0; $this->postCount = 0; $this->filesCount = 0; $this->emptyCount = 0; @@ -125,20 +127,20 @@ private function parseBody($boundary, $buffer) $len = \strlen($boundary); // ignore everything before initial boundary (SHOULD be empty) - $start = \strpos($buffer, $boundary . "\r\n"); + $this->cursor = \strpos($buffer, $boundary . "\r\n"); - while ($start !== false) { + while ($this->cursor !== false) { // search following boundary (preceded by newline) // ignore last if not followed by boundary (SHOULD end with "--") - $start += $len + 2; - $end = \strpos($buffer, "\r\n" . $boundary, $start); + $this->cursor += $len + 2; + $end = \strpos($buffer, "\r\n" . $boundary, $this->cursor); if ($end === false) { break; } // parse one part and continue searching for next - $this->parsePart(\substr($buffer, $start, $end - $start)); - $start = $end; + $this->parsePart(\substr($buffer, $this->cursor, $end - $this->cursor)); + $this->cursor = $end; if (++$this->multipartBodyPartCount > $this->maxMultipartBodyParts) { break; diff --git a/tests/Io/MultipartParserTest.php b/tests/Io/MultipartParserTest.php index 5dfd6e43..7f1ec667 100644 --- a/tests/Io/MultipartParserTest.php +++ b/tests/Io/MultipartParserTest.php @@ -1029,6 +1029,7 @@ public function testPostMaxFileSizeIgnoredByFilesComingBeforeIt() public function testWeOnlyParseTheAmountOfMultiPartChunksWeConfigured() { + $chunkCount = 5000000; $boundary = "---------------------------12758086162038677464950549563"; $chunk = "--$boundary\r\n"; @@ -1036,9 +1037,7 @@ public function testWeOnlyParseTheAmountOfMultiPartChunksWeConfigured() $chunk .= "\r\n"; $chunk .= "u\r\n"; $data = ''; - for ($i = 0; $i < 5000000; $i++) { - $data .= $chunk; - } + $data .= str_repeat($chunk, $chunkCount); $data .= "--$boundary--\r\n"; $request = new ServerRequest('POST', 'http://example.com/', array( @@ -1046,9 +1045,25 @@ public function testWeOnlyParseTheAmountOfMultiPartChunksWeConfigured() ), $data, 1.1); $parser = new MultipartParser(); - $startTime = microtime(true); - $parser->parse($request); - $runTime = microtime(true) - $startTime; - $this->assertLessThan(1, $runTime); + + $reflectecClass = new \ReflectionClass('\React\Http\Io\MultipartParser'); + $requestProperty = $reflectecClass->getProperty('request'); + $requestProperty->setAccessible(true); + $cursorProperty = $reflectecClass->getProperty('cursor'); + $cursorProperty->setAccessible(true); + $multipartBodyPartCountProperty = $reflectecClass->getProperty('multipartBodyPartCount'); + $multipartBodyPartCountProperty->setAccessible(true); + $maxMultipartBodyPartsProperty = $reflectecClass->getProperty('maxMultipartBodyParts'); + $maxMultipartBodyPartsProperty->setAccessible(true); + $parseBodyMethod = $reflectecClass->getMethod('parseBody'); + $parseBodyMethod->setAccessible(true); + + $this->assertSame(0, $cursorProperty->getValue($parser)); + + $requestProperty->setValue($parser, $request); + $parseBodyMethod->invoke($parser, '--' . $boundary, $data); + + $this->assertSame(strlen(str_repeat($chunk, $multipartBodyPartCountProperty->getValue($parser))), $cursorProperty->getValue($parser) + 2); + $this->assertSame($multipartBodyPartCountProperty->getValue($parser), $maxMultipartBodyPartsProperty->getValue($parser) + 1); } } From 684421f5d09afaaa0dc1c896f9d17244544a39d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 18 Apr 2023 21:04:41 +0200 Subject: [PATCH 429/456] Enable HTTP keep-alive by default for HTTP client --- src/Browser.php | 1 - tests/BrowserTest.php | 2 -- tests/FunctionalBrowserTest.php | 53 ++++++++++++++++++++++++++++++--- 3 files changed, 49 insertions(+), 7 deletions(-) diff --git a/src/Browser.php b/src/Browser.php index ad9187a6..b7bf4425 100644 --- a/src/Browser.php +++ b/src/Browser.php @@ -23,7 +23,6 @@ class Browser private $baseUrl; private $protocolVersion = '1.1'; private $defaultHeaders = array( - 'Connection' => 'close', 'User-Agent' => 'ReactPHP/1' ); diff --git a/tests/BrowserTest.php b/tests/BrowserTest.php index d01de9c5..b7958016 100644 --- a/tests/BrowserTest.php +++ b/tests/BrowserTest.php @@ -556,8 +556,6 @@ public function testWithMultipleHeadersShouldBeMergedCorrectlyWithMultipleDefaul 'user-Agent' => array('ABC'), 'another-header' => array('value'), 'custom-header' => array('data'), - - 'Connection' => array('close') ); $that->assertEquals($expectedHeaders, $request->getHeaders()); diff --git a/tests/FunctionalBrowserTest.php b/tests/FunctionalBrowserTest.php index 7ab909de..35b96eb6 100644 --- a/tests/FunctionalBrowserTest.php +++ b/tests/FunctionalBrowserTest.php @@ -553,7 +553,7 @@ public function testReceiveStreamAndExplicitlyCloseConnectionEvenWhenServerKeeps $socket->close(); } - public function testRequestWillCreateNewConnectionForSecondRequestByDefaultEvenWhenServerKeepsConnectionOpen() + public function testRequestWithConnectionCloseHeaderWillCreateNewConnectionForSecondRequestEvenWhenServerKeepsConnectionOpen() { $twice = $this->expectCallableOnce(); $socket = new SocketServer('127.0.0.1:0'); @@ -570,6 +570,9 @@ public function testRequestWillCreateNewConnectionForSecondRequestByDefaultEvenW $this->base = str_replace('tcp:', 'http:', $socket->getAddress()) . '/'; + // add `Connection: close` request header to disable HTTP keep-alive + $this->browser = $this->browser->withHeader('Connection', 'close'); + $response = \React\Async\await($this->browser->get($this->base . 'get')); assert($response instanceof ResponseInterface); $this->assertEquals('hello', (string)$response->getBody()); @@ -579,12 +582,54 @@ public function testRequestWillCreateNewConnectionForSecondRequestByDefaultEvenW $this->assertEquals('hello', (string)$response->getBody()); } - public function testRequestWithoutConnectionHeaderWillReuseExistingConnectionForSecondRequest() + public function testRequestWithHttp10WillCreateNewConnectionForSecondRequestEvenWhenServerKeepsConnectionOpen() + { + $twice = $this->expectCallableOnce(); + $socket = new SocketServer('127.0.0.1:0'); + $socket->on('connection', function (\React\Socket\ConnectionInterface $connection) use ($socket, $twice) { + $connection->on('data', function () use ($connection) { + $connection->write("HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello"); + }); + + $socket->on('connection', $twice); + $socket->on('connection', function () use ($socket) { + $socket->close(); + }); + }); + + $this->base = str_replace('tcp:', 'http:', $socket->getAddress()) . '/'; + + // use HTTP/1.0 to disable HTTP keep-alive + $this->browser = $this->browser->withProtocolVersion('1.0'); + + $response = \React\Async\await($this->browser->get($this->base . 'get')); + assert($response instanceof ResponseInterface); + $this->assertEquals('hello', (string)$response->getBody()); + + $response = \React\Async\await($this->browser->get($this->base . 'get')); + assert($response instanceof ResponseInterface); + $this->assertEquals('hello', (string)$response->getBody()); + } + + public function testRequestWillReuseExistingConnectionForSecondRequestByDefault() { $this->socket->on('connection', $this->expectCallableOnce()); - // remove default `Connection: close` request header to enable keep-alive - $this->browser = $this->browser->withoutHeader('Connection'); + $response = \React\Async\await($this->browser->get($this->base . 'get')); + assert($response instanceof ResponseInterface); + $this->assertEquals('hello', (string)$response->getBody()); + + $response = \React\Async\await($this->browser->get($this->base . 'get')); + assert($response instanceof ResponseInterface); + $this->assertEquals('hello', (string)$response->getBody()); + } + + public function testRequestWithHttp10AndConnectionKeepAliveHeaderWillReuseExistingConnectionForSecondRequest() + { + $this->socket->on('connection', $this->expectCallableOnce()); + + $this->browser = $this->browser->withProtocolVersion('1.0'); + $this->browser = $this->browser->withHeader('Connection', 'keep-alive'); $response = \React\Async\await($this->browser->get($this->base . 'get')); assert($response instanceof ResponseInterface); From bb3154dbaf2dfe3f0467f956a05f614a69d5f1d0 Mon Sep 17 00:00:00 2001 From: Simon Frings Date: Wed, 26 Apr 2023 12:29:24 +0200 Subject: [PATCH 430/456] Prepare v1.9.0 release --- CHANGELOG.md | 35 +++++++++++++++++++++++++++++++++++ README.md | 2 +- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 00e2d07e..d19639d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,40 @@ # Changelog +## 1.9.0 (2023-04-26) + +This is a **SECURITY** and feature release for the 1.x series of ReactPHP's HTTP component. + +* Security fix: This release fixes a medium severity security issue in ReactPHP's HTTP server component + that affects all versions between `v0.8.0` and `v1.8.0`. All users are encouraged to upgrade immediately. + (CVE-2023-26044 reported and fixed by @WyriHaximus) + +* Feature: Support HTTP keep-alive for HTTP client (reusing persistent connections). + (#481, #484, #486 and #495 by @clue) + + This feature offers significant performance improvements when sending many + requests to the same host as it avoids recreating the underlying TCP/IP + connection and repeating the TLS handshake for secure HTTPS requests. + + ```php + $browser = new React\Http\Browser(); + + // Up to 300% faster! HTTP keep-alive is enabled by default + $response = React\Async\await($browser->get('https://httpbingo.org/redirect/6')); + assert($response instanceof Psr\Http\Message\ResponseInterface); + ``` + +* Feature: Add `Request` class to represent outgoing HTTP request message. + (#480 by @clue) + +* Feature: Preserve request method and body for `307 Temporary Redirect` and `308 Permanent Redirect`. + (#442 by @dinooo13) + +* Feature: Include buffer logic to avoid dependency on reactphp/promise-stream. + (#482 by @clue) + +* Improve test suite and project setup and report failed assertions. + (#478 by @clue, #487 and #491 by @WyriHaximus and #475 and #479 by @SimonFrings) + ## 1.8.0 (2022-09-29) * Feature: Support for default request headers. diff --git a/README.md b/README.md index 271f5e87..955e0a99 100644 --- a/README.md +++ b/README.md @@ -2976,7 +2976,7 @@ This project follows [SemVer](https://semver.org/). This will install the latest supported version: ```bash -composer require react/http:^1.8 +composer require react/http:^1.9 ``` See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. From 94222ad193ac03265da34124e5681f03e73558c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 29 Jun 2023 00:45:47 +0200 Subject: [PATCH 431/456] Update test suite to avoid unhandled promise rejections --- tests/Io/TransactionTest.php | 2 + .../LimitConcurrentRequestsMiddlewareTest.php | 45 +++++++++++++++---- 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/tests/Io/TransactionTest.php b/tests/Io/TransactionTest.php index 140c53e0..284d059f 100644 --- a/tests/Io/TransactionTest.php +++ b/tests/Io/TransactionTest.php @@ -321,6 +321,8 @@ public function testTimeoutExplicitOptionWillNotStartTimeoutTimerWhenStreamingRe $stream->close(); $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); + + $promise->then(null, $this->expectCallableOnce()); // avoid reporting unhandled rejection } public function testTimeoutExplicitOptionWillRejectWhenTimerFiresAfterStreamingRequestBodyCloses() diff --git a/tests/Middleware/LimitConcurrentRequestsMiddlewareTest.php b/tests/Middleware/LimitConcurrentRequestsMiddlewareTest.php index 6c63a94f..faf27cb6 100644 --- a/tests/Middleware/LimitConcurrentRequestsMiddlewareTest.php +++ b/tests/Middleware/LimitConcurrentRequestsMiddlewareTest.php @@ -64,7 +64,10 @@ public function testLimitOneRequestConcurrently() $this->assertFalse($calledB); $this->assertFalse($calledC); - $limitHandlers($requestB, $nextB); + $promise = $limitHandlers($requestB, $nextB); + + assert($promise instanceof PromiseInterface); + $promise->then(null, $this->expectCallableOnce()); // avoid reporting unhandled rejection $this->assertTrue($calledA); $this->assertFalse($calledB); @@ -188,10 +191,13 @@ public function testStreamDoesPauseAndThenResumeWhenDequeued() $limitHandlers = new LimitConcurrentRequestsMiddleware(1); $deferred = new Deferred(); - $limitHandlers(new ServerRequest('GET', 'https://example.com'), function () use ($deferred) { + $promise = $limitHandlers(new ServerRequest('GET', 'https://example.com'), function () use ($deferred) { return $deferred->promise(); }); + assert($promise instanceof PromiseInterface); + $promise->then(null, $this->expectCallableOnce()); // avoid reporting unhandled rejection + $limitHandlers(new ServerRequest('GET', 'https://example.com/', array(), $body), function () {}); $deferred->reject(new \RuntimeException()); @@ -283,10 +289,13 @@ public function testReceivesNextRequestAfterPreviousHandlerIsSettled() $deferred = new Deferred(); $middleware = new LimitConcurrentRequestsMiddleware(1); - $middleware($request, function () use ($deferred) { + $promise = $middleware($request, function () use ($deferred) { return $deferred->promise(); }); + assert($promise instanceof PromiseInterface); + $promise->then(null, $this->expectCallableOnce()); // avoid reporting unhandled rejection + $deferred->reject(new \RuntimeException()); $middleware($request, $this->expectCallableOnceWith($request)); @@ -303,10 +312,13 @@ public function testReceivesNextRequestWhichThrowsAfterPreviousHandlerIsSettled( $deferred = new Deferred(); $middleware = new LimitConcurrentRequestsMiddleware(1); - $middleware($request, function () use ($deferred) { + $promise = $middleware($request, function () use ($deferred) { return $deferred->promise(); }); + assert($promise instanceof PromiseInterface); + $promise->then(null, $this->expectCallableOnce()); // avoid reporting unhandled rejection + $second = $middleware($request, function () { throw new \RuntimeException(); }); @@ -443,10 +455,13 @@ public function testReceivesStreamingBodyChangesInstanceWithCustomBodyButSameDat $middleware = new LimitConcurrentRequestsMiddleware(1); $deferred = new Deferred(); - $middleware(new ServerRequest('GET', 'https://example.com/'), function () use ($deferred) { + $promise = $middleware(new ServerRequest('GET', 'https://example.com/'), function () use ($deferred) { return $deferred->promise(); }); + assert($promise instanceof PromiseInterface); + $promise->then(null, $this->expectCallableOnce()); // avoid reporting unhandled rejection + $req = null; $middleware($request, function (ServerRequestInterface $request) use (&$req) { $req = $request; @@ -471,10 +486,13 @@ public function testReceivesNextStreamingBodyWithBufferedDataAfterPreviousHandle { $deferred = new Deferred(); $middleware = new LimitConcurrentRequestsMiddleware(1); - $middleware(new ServerRequest('GET', 'http://example.com/'), function () use ($deferred) { + $promise = $middleware(new ServerRequest('GET', 'http://example.com/'), function () use ($deferred) { return $deferred->promise(); }); + assert($promise instanceof PromiseInterface); + $promise->then(null, $this->expectCallableOnce()); // avoid reporting unhandled rejection + $stream = new ThroughStream(); $request = new ServerRequest( 'POST', @@ -498,10 +516,13 @@ public function testReceivesNextStreamingBodyAndDoesNotEmitDataIfExplicitlyClose { $deferred = new Deferred(); $middleware = new LimitConcurrentRequestsMiddleware(1); - $middleware(new ServerRequest('GET', 'http://example.com/'), function () use ($deferred) { + $promise = $middleware(new ServerRequest('GET', 'http://example.com/'), function () use ($deferred) { return $deferred->promise(); }); + assert($promise instanceof PromiseInterface); + $promise->then(null, $this->expectCallableOnce()); // avoid reporting unhandled rejection + $stream = new ThroughStream(); $request = new ServerRequest( 'POST', @@ -526,10 +547,13 @@ public function testReceivesNextStreamingBodyAndDoesNotEmitDataIfExplicitlyPause { $deferred = new Deferred(); $middleware = new LimitConcurrentRequestsMiddleware(1); - $middleware(new ServerRequest('GET', 'http://example.com/'), function () use ($deferred) { + $promise = $middleware(new ServerRequest('GET', 'http://example.com/'), function () use ($deferred) { return $deferred->promise(); }); + assert($promise instanceof PromiseInterface); + $promise->then(null, $this->expectCallableOnce()); // avoid reporting unhandled rejection + $stream = new ThroughStream(); $request = new ServerRequest( 'POST', @@ -554,10 +578,13 @@ public function testReceivesNextStreamingBodyAndDoesEmitDataImmediatelyIfExplici { $deferred = new Deferred(); $middleware = new LimitConcurrentRequestsMiddleware(1); - $middleware(new ServerRequest('GET', 'http://example.com/'), function () use ($deferred) { + $promise = $middleware(new ServerRequest('GET', 'http://example.com/'), function () use ($deferred) { return $deferred->promise(); }); + assert($promise instanceof PromiseInterface); + $promise->then(null, $this->expectCallableOnce()); // avoid reporting unhandled rejection + $stream = new ThroughStream(); $request = new ServerRequest( 'POST', From 7a5b57c2f6e458cb8788c3692486e0756c9fc390 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 7 Jul 2023 13:01:17 +0200 Subject: [PATCH 432/456] Update tests to remove defunct httpbin.org --- examples/03-client-request-any.php | 4 +- examples/04-client-post-json.php | 2 +- examples/05-client-put-xml.php | 2 +- .../22-client-stream-upload-from-stdin.php | 4 +- examples/91-client-benchmark-download.php | 2 +- tests/Client/FunctionalIntegrationTest.php | 66 ------------------- tests/FunctionalBrowserTest.php | 4 +- 7 files changed, 9 insertions(+), 75 deletions(-) diff --git a/examples/03-client-request-any.php b/examples/03-client-request-any.php index 0c96b684..d7558bd6 100644 --- a/examples/03-client-request-any.php +++ b/examples/03-client-request-any.php @@ -12,10 +12,10 @@ $promises = array( $client->head('http://www.github.com/clue/http-react'), - $client->get('https://httpbin.org/'), + $client->get('https://httpbingo.org/'), $client->get('https://google.com'), $client->get('http://www.lueck.tv/psocksd'), - $client->get('http://www.httpbin.org/absolute-redirect/5') + $client->get('http://httpbingo.org/absolute-redirect/5') ); React\Promise\any($promises)->then(function (ResponseInterface $response) use ($promises) { diff --git a/examples/04-client-post-json.php b/examples/04-client-post-json.php index b01ada13..477c3426 100644 --- a/examples/04-client-post-json.php +++ b/examples/04-client-post-json.php @@ -16,7 +16,7 @@ ); $client->post( - 'https://httpbin.org/post', + 'https://httpbingo.org/post', array( 'Content-Type' => 'application/json' ), diff --git a/examples/05-client-put-xml.php b/examples/05-client-put-xml.php index 231e2ca4..6055363a 100644 --- a/examples/05-client-put-xml.php +++ b/examples/05-client-put-xml.php @@ -13,7 +13,7 @@ $child->name = 'Christian Lück'; $client->put( - 'https://httpbin.org/put', + 'https://httpbingo.org/put', array( 'Content-Type' => 'text/xml' ), diff --git a/examples/22-client-stream-upload-from-stdin.php b/examples/22-client-stream-upload-from-stdin.php index b00fbc5e..f29b08ab 100644 --- a/examples/22-client-stream-upload-from-stdin.php +++ b/examples/22-client-stream-upload-from-stdin.php @@ -16,10 +16,10 @@ $in = new ReadableResourceStream(STDIN); -$url = isset($argv[1]) ? $argv[1] : 'https://httpbin.org/post'; +$url = isset($argv[1]) ? $argv[1] : 'https://httpbingo.org/post'; echo 'Sending STDIN as POST to ' . $url . '…' . PHP_EOL; -$client->post($url, array(), $in)->then(function (ResponseInterface $response) { +$client->post($url, array('Content-Type' => 'text/plain'), $in)->then(function (ResponseInterface $response) { echo 'Received' . PHP_EOL . Psr7\str($response); }, function (Exception $e) { echo 'Error: ' . $e->getMessage() . PHP_EOL; diff --git a/examples/91-client-benchmark-download.php b/examples/91-client-benchmark-download.php index 49693baf..44e99087 100644 --- a/examples/91-client-benchmark-download.php +++ b/examples/91-client-benchmark-download.php @@ -1,7 +1,7 @@ markTestSkipped('Not supported on HHVM'); - } - - // max_nesting_level was set to 100 for PHP Versions < 5.4 which resulted in failing test for legacy PHP - ini_set('xdebug.max_nesting_level', 256); - - $client = new Client(new ClientConnectionManager(new Connector(), Loop::get())); - - $data = str_repeat('.', 33000); - $request = $client->request(new Request('POST', 'https://' . (mt_rand(0, 1) === 0 ? 'eu.' : '') . 'httpbin.org/post', array('Content-Length' => strlen($data)), '', '1.0')); - - $deferred = new Deferred(); - $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($deferred) { - $deferred->resolve(Stream\buffer($body)); - }); - - $request->on('error', 'printf'); - $request->on('error', $this->expectCallableNever()); - - $request->end($data); - - $buffer = \React\Async\await(\React\Promise\Timer\timeout($deferred->promise(), self::TIMEOUT_REMOTE)); - - $this->assertNotEquals('', $buffer); - - $parsed = json_decode($buffer, true); - $this->assertTrue(is_array($parsed) && isset($parsed['data'])); - $this->assertEquals(strlen($data), strlen($parsed['data'])); - $this->assertEquals($data, $parsed['data']); - } - - /** @group internet */ - public function testPostJsonReturnsData() - { - if (defined('HHVM_VERSION')) { - $this->markTestSkipped('Not supported on HHVM'); - } - - $client = new Client(new ClientConnectionManager(new Connector(), Loop::get())); - - $data = json_encode(array('numbers' => range(1, 50))); - $request = $client->request(new Request('POST', 'https://httpbin.org/post', array('Content-Length' => strlen($data), 'Content-Type' => 'application/json'), '', '1.0')); - - $deferred = new Deferred(); - $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($deferred) { - $deferred->resolve(Stream\buffer($body)); - }); - - $request->on('error', 'printf'); - $request->on('error', $this->expectCallableNever()); - - $request->end($data); - - $buffer = \React\Async\await(\React\Promise\Timer\timeout($deferred->promise(), self::TIMEOUT_REMOTE)); - - $this->assertNotEquals('', $buffer); - - $parsed = json_decode($buffer, true); - $this->assertTrue(is_array($parsed) && isset($parsed['json'])); - $this->assertEquals(json_decode($data, true), $parsed['json']); - } - /** @group internet */ public function testCancelPendingConnectionEmitsClose() { diff --git a/tests/FunctionalBrowserTest.php b/tests/FunctionalBrowserTest.php index 35b96eb6..7b8ff84b 100644 --- a/tests/FunctionalBrowserTest.php +++ b/tests/FunctionalBrowserTest.php @@ -230,7 +230,7 @@ public function testRequestWithAuthenticationSucceeds() /** * ```bash - * $ curl -vL "http://httpbin.org/redirect-to?url=http://user:pass@httpbin.org/basic-auth/user/pass" + * $ curl -vL "http://httpbingo.org/redirect-to?url=http://user:pass@httpbingo.org/basic-auth/user/pass" * ``` * * @doesNotPerformAssertions @@ -244,7 +244,7 @@ public function testRedirectToPageWithAuthenticationSendsAuthenticationFromLocat /** * ```bash - * $ curl -vL "http://unknown:invalid@httpbin.org/redirect-to?url=http://user:pass@httpbin.org/basic-auth/user/pass" + * $ curl -vL "http://unknown:invalid@httpbingo.org/redirect-to?url=http://user:pass@httpbingo.org/basic-auth/user/pass" * ``` * * @doesNotPerformAssertions From eb83eb06bd5ea052638266396f363fb02891cfac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 19 Sep 2023 17:45:06 +0200 Subject: [PATCH 433/456] Test on PHP 8.3 and update test environment --- .github/workflows/ci.yml | 5 +++-- composer.json | 2 +- phpunit.xml.dist | 6 +++--- phpunit.xml.legacy | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 55bbaa5b..3666cd47 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,6 +11,7 @@ jobs: strategy: matrix: php: + - 8.3 - 8.2 - 8.1 - 8.0 @@ -24,7 +25,7 @@ jobs: - 5.4 - 5.3 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} @@ -41,7 +42,7 @@ jobs: runs-on: ubuntu-22.04 continue-on-error: true steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - run: cp "$(which composer)" composer.phar && ./composer.phar self-update --2.2 # downgrade Composer for HHVM - name: Run hhvm composer.phar install uses: docker://hhvm/hhvm:3.30-lts-latest diff --git a/composer.json b/composer.json index 59736ddd..5198470e 100644 --- a/composer.json +++ b/composer.json @@ -40,7 +40,7 @@ "clue/http-proxy-react": "^1.8", "clue/reactphp-ssh-proxy": "^1.4", "clue/socks-react": "^1.4", - "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35", + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", "react/async": "^4 || ^3 || ^2", "react/promise-stream": "^1.4", "react/promise-timer": "^1.9" diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 7a9577e9..ac542e77 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,8 +1,8 @@ - + - + diff --git a/phpunit.xml.legacy b/phpunit.xml.legacy index ac5600ae..89161168 100644 --- a/phpunit.xml.legacy +++ b/phpunit.xml.legacy @@ -18,7 +18,7 @@ - + From 9bf5456d95a0c4607fac0ab2f4c2926c341029d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 22 Feb 2024 15:11:43 +0100 Subject: [PATCH 434/456] Fix empty streaming request body, omit `Transfer-Encoding: chunked` --- src/Io/Sender.php | 2 +- tests/Io/SenderTest.php | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/Io/Sender.php b/src/Io/Sender.php index 3598d31a..1d563891 100644 --- a/src/Io/Sender.php +++ b/src/Io/Sender.php @@ -90,7 +90,7 @@ public function send(RequestInterface $request) } elseif ($size === 0 && \in_array($request->getMethod(), array('POST', 'PUT', 'PATCH'))) { // only assign a "Content-Length: 0" request header if the body is expected for certain methods $request = $request->withHeader('Content-Length', '0'); - } elseif ($body instanceof ReadableStreamInterface && $body->isReadable() && !$request->hasHeader('Content-Length')) { + } elseif ($body instanceof ReadableStreamInterface && $size !== 0 && $body->isReadable() && !$request->hasHeader('Content-Length')) { // use "Transfer-Encoding: chunked" when this is a streaming body and body size is unknown $request = $request->withHeader('Transfer-Encoding', 'chunked'); } else { diff --git a/tests/Io/SenderTest.php b/tests/Io/SenderTest.php index 3c8c4761..03a9b56e 100644 --- a/tests/Io/SenderTest.php +++ b/tests/Io/SenderTest.php @@ -5,6 +5,7 @@ use Psr\Http\Message\RequestInterface; use React\Http\Client\Client as HttpClient; use React\Http\Io\ClientConnectionManager; +use React\Http\Io\EmptyBodyStream; use React\Http\Io\ReadableBodyStream; use React\Http\Io\Sender; use React\Http\Message\Request; @@ -264,6 +265,21 @@ public function testSendGetWillNotPassContentLengthHeaderForEmptyRequestBody() $sender->send($request); } + public function testSendGetWithEmptyBodyStreamWillNotPassContentLengthOrTransferEncodingHeader() + { + $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); + $client->expects($this->once())->method('request')->with($this->callback(function (RequestInterface $request) { + return !$request->hasHeader('Content-Length') && !$request->hasHeader('Transfer-Encoding'); + }))->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock()); + + $sender = new Sender($client); + + $body = new EmptyBodyStream(); + $request = new Request('GET', 'http://www.google.com/', array(), $body); + + $sender->send($request); + } + public function testSendCustomMethodWillNotPassContentLengthHeaderForEmptyRequestBody() { $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); From 638c5dddd6f4c51c4025844ca3fbebf26e9919a9 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Sat, 17 Feb 2024 11:17:31 +0100 Subject: [PATCH 435/456] [1.x] Ensure connection close handler is cleaned up for each request This changeset resolves a small memory leak that causes roughly 1KB per connection tops. Which isn't a big issue but will make memory fluctuate more. The changeset doesn't introduce any performance degradation. Resolves: #514 Builds on top of: #405, #467, and many others --- src/Io/StreamingServer.php | 13 +++++++--- tests/Io/StreamingServerTest.php | 42 ++++++++++++++++++++++++++------ 2 files changed, 45 insertions(+), 10 deletions(-) diff --git a/src/Io/StreamingServer.php b/src/Io/StreamingServer.php index 13f0b0c4..790c8cc1 100644 --- a/src/Io/StreamingServer.php +++ b/src/Io/StreamingServer.php @@ -157,10 +157,17 @@ public function handleRequest(ConnectionInterface $conn, ServerRequestInterface } // cancel pending promise once connection closes + $connectionOnCloseResponseCancelerHandler = function () {}; if ($response instanceof PromiseInterface && \method_exists($response, 'cancel')) { - $conn->on('close', function () use ($response) { + $connectionOnCloseResponseCanceler = function () use ($response) { $response->cancel(); - }); + }; + $connectionOnCloseResponseCancelerHandler = function () use ($connectionOnCloseResponseCanceler, $conn) { + if ($connectionOnCloseResponseCanceler !== null) { + $conn->removeListener('close', $connectionOnCloseResponseCanceler); + } + }; + $conn->on('close', $connectionOnCloseResponseCanceler); } // happy path: response returned, handle and return immediately @@ -201,7 +208,7 @@ function ($error) use ($that, $conn, $request) { $that->emit('error', array($exception)); return $that->writeError($conn, Response::STATUS_INTERNAL_SERVER_ERROR, $request); } - ); + )->then($connectionOnCloseResponseCancelerHandler, $connectionOnCloseResponseCancelerHandler); } /** @internal */ diff --git a/tests/Io/StreamingServerTest.php b/tests/Io/StreamingServerTest.php index a2700b86..a578797e 100644 --- a/tests/Io/StreamingServerTest.php +++ b/tests/Io/StreamingServerTest.php @@ -25,9 +25,17 @@ class StreamingServerTest extends TestCase */ public function setUpConnectionMockAndSocket() { - $this->connection = $this->getMockBuilder('React\Socket\Connection') + $this->connection = $this->mockConnection(); + + $this->socket = new SocketServerStub(); + } + + + private function mockConnection(array $additionalMethods = null) + { + $connection = $this->getMockBuilder('React\Socket\Connection') ->disableOriginalConstructor() - ->setMethods( + ->setMethods(array_merge( array( 'write', 'end', @@ -39,14 +47,15 @@ public function setUpConnectionMockAndSocket() 'getRemoteAddress', 'getLocalAddress', 'pipe' - ) - ) + ), + (is_array($additionalMethods) ? $additionalMethods : array()) + )) ->getMock(); - $this->connection->method('isWritable')->willReturn(true); - $this->connection->method('isReadable')->willReturn(true); + $connection->method('isWritable')->willReturn(true); + $connection->method('isReadable')->willReturn(true); - $this->socket = new SocketServerStub(); + return $connection; } public function testRequestEventWillNotBeEmittedForIncompleteHeaders() @@ -3245,6 +3254,25 @@ public function testNewConnectionWillInvokeParserTwiceAfterInvokingRequestHandle $this->assertCount(1, $this->connection->listeners('close')); } + public function testCompletingARequestWillRemoveConnectionOnCloseListener() + { + $connection = $this->mockConnection(array('removeListener')); + + $request = new ServerRequest('GET', 'http://localhost/'); + + $server = new StreamingServer(Loop::get(), function () { + return \React\Promise\resolve(new Response()); + }); + + $server->listen($this->socket); + $this->socket->emit('connection', array($connection)); + + $connection->expects($this->once())->method('removeListener'); + + // pretend parser just finished parsing + $server->handleRequest($connection, $request); + } + private function createGetRequest() { $data = "GET / HTTP/1.1\r\n"; From 1bbd7f921f6a762852c9566742c20efba4672ac2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 18 Nov 2022 16:40:33 +0100 Subject: [PATCH 436/456] Update `Response` class to build on top of abstract message class --- src/Message/Response.php | 102 ++++++++++++++++++++++++++++--- tests/Io/StreamingServerTest.php | 4 +- tests/Message/ResponseTest.php | 33 ++++++++++ 3 files changed, 128 insertions(+), 11 deletions(-) diff --git a/src/Message/Response.php b/src/Message/Response.php index edd6245b..c50d0cee 100644 --- a/src/Message/Response.php +++ b/src/Message/Response.php @@ -3,11 +3,12 @@ namespace React\Http\Message; use Fig\Http\Message\StatusCodeInterface; +use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\StreamInterface; use React\Http\Io\BufferedBody; use React\Http\Io\HttpBodyStream; use React\Stream\ReadableStreamInterface; -use RingCentral\Psr7\Response as Psr7Response; +use RingCentral\Psr7\MessageTrait; /** * Represents an outgoing server response message. @@ -40,7 +41,7 @@ * * @see \Psr\Http\Message\ResponseInterface */ -final class Response extends Psr7Response implements StatusCodeInterface +final class Response extends MessageTrait implements ResponseInterface, StatusCodeInterface { /** * Create an HTML response @@ -257,6 +258,41 @@ public static function xml($xml) return new self(self::STATUS_OK, array('Content-Type' => 'application/xml'), $xml); } + /** + * @var bool + * @see self::$phrasesMap + */ + private static $phrasesInitialized = false; + + /** + * Map of standard HTTP status codes to standard reason phrases. + * + * This map will be fully populated with all standard reason phrases on + * first access. By default, it only contains a subset of HTTP status codes + * that have a custom mapping to reason phrases (such as those with dashes + * and all caps words). See `self::STATUS_*` for all possible status code + * constants. + * + * @var array + * @see self::STATUS_* + * @see self::getReasonPhraseForStatusCode() + */ + private static $phrasesMap = array( + 200 => 'OK', + 203 => 'Non-Authoritative Information', + 207 => 'Multi-Status', + 226 => 'IM Used', + 414 => 'URI Too Large', + 418 => 'I\'m a teapot', + 505 => 'HTTP Version Not Supported' + ); + + /** @var int */ + private $statusCode; + + /** @var string */ + private $reasonPhrase; + /** * @param int $status HTTP status code (e.g. 200/404), see `self::STATUS_*` constants * @param array $headers additional response headers @@ -280,12 +316,60 @@ public function __construct( throw new \InvalidArgumentException('Invalid response body given'); } - parent::__construct( - $status, - $headers, - $body, - $version, - $reason - ); + $this->protocol = (string) $version; + $this->setHeaders($headers); + $this->stream = $body; + + $this->statusCode = (int) $status; + $this->reasonPhrase = ($reason !== '' && $reason !== null) ? (string) $reason : self::getReasonPhraseForStatusCode($status); + } + + public function getStatusCode() + { + return $this->statusCode; + } + + public function withStatus($code, $reasonPhrase = '') + { + if ((string) $reasonPhrase === '') { + $reasonPhrase = self::getReasonPhraseForStatusCode($code); + } + + if ($this->statusCode === (int) $code && $this->reasonPhrase === (string) $reasonPhrase) { + return $this; + } + + $response = clone $this; + $response->statusCode = (int) $code; + $response->reasonPhrase = (string) $reasonPhrase; + + return $response; + } + + public function getReasonPhrase() + { + return $this->reasonPhrase; + } + + /** + * @param int $code + * @return string default reason phrase for given status code or empty string if unknown + */ + private static function getReasonPhraseForStatusCode($code) + { + if (!self::$phrasesInitialized) { + self::$phrasesInitialized = true; + + // map all `self::STATUS_` constants from status code to reason phrase + // e.g. `self::STATUS_NOT_FOUND = 404` will be mapped to `404 Not Found` + $ref = new \ReflectionClass(__CLASS__); + foreach ($ref->getConstants() as $name => $value) { + if (!isset(self::$phrasesMap[$value]) && \strpos($name, 'STATUS_') === 0) { + self::$phrasesMap[$value] = \ucwords(\strtolower(\str_replace('_', ' ', \substr($name, 7)))); + } + } + } + + return isset(self::$phrasesMap[$code]) ? self::$phrasesMap[$code] : ''; } } diff --git a/tests/Io/StreamingServerTest.php b/tests/Io/StreamingServerTest.php index a578797e..64566ddc 100644 --- a/tests/Io/StreamingServerTest.php +++ b/tests/Io/StreamingServerTest.php @@ -1567,9 +1567,9 @@ function ($data) use (&$buffer) { $this->assertInstanceOf('InvalidArgumentException', $error); - $this->assertContainsString("HTTP/1.1 505 HTTP Version not supported\r\n", $buffer); + $this->assertContainsString("HTTP/1.1 505 HTTP Version Not Supported\r\n", $buffer); $this->assertContainsString("\r\n\r\n", $buffer); - $this->assertContainsString("Error 505: HTTP Version not supported", $buffer); + $this->assertContainsString("Error 505: HTTP Version Not Supported", $buffer); } public function testRequestOverflowWillEmitErrorAndSendErrorResponse() diff --git a/tests/Message/ResponseTest.php b/tests/Message/ResponseTest.php index ed21cdc2..88b56945 100644 --- a/tests/Message/ResponseTest.php +++ b/tests/Message/ResponseTest.php @@ -54,6 +54,39 @@ public function testResourceBodyWillThrow() new Response(200, array(), tmpfile()); } + public function testWithStatusReturnsNewInstanceWhenStatusIsChanged() + { + $response = new Response(200); + + $new = $response->withStatus(404); + $this->assertNotSame($response, $new); + $this->assertEquals(404, $new->getStatusCode()); + $this->assertEquals('Not Found', $new->getReasonPhrase()); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals('OK', $response->getReasonPhrase()); + } + + public function testWithStatusReturnsSameInstanceWhenStatusIsUnchanged() + { + $response = new Response(200); + + $new = $response->withStatus(200); + $this->assertSame($response, $new); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals('OK', $response->getReasonPhrase()); + } + + public function testWithStatusReturnsNewInstanceWhenStatusIsUnchangedButReasonIsChanged() + { + $response = new Response(200); + + $new = $response->withStatus(200, 'Quite Ok'); + $this->assertNotSame($response, $new); + $this->assertEquals(200, $new->getStatusCode()); + $this->assertEquals('Quite Ok', $new->getReasonPhrase()); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals('OK', $response->getReasonPhrase()); + } public function testHtmlMethodReturnsHtmlResponse() { From 518ca68ca8f03f61e9e662dedb1ce2383307a677 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 15 Sep 2023 20:08:07 +0200 Subject: [PATCH 437/456] Add internal `AbstractMessage` base class (PSR-7) --- README.md | 3 +- src/Io/AbstractMessage.php | 164 +++++++++++++++++ src/Message/Response.php | 11 +- tests/Io/AbstractMessageTest.php | 222 ++++++++++++++++++++++++ tests/Io/MiddlewareRunnerTest.php | 2 +- tests/Message/ResponseExceptionTest.php | 2 +- 6 files changed, 393 insertions(+), 11 deletions(-) create mode 100644 src/Io/AbstractMessage.php create mode 100644 tests/Io/AbstractMessageTest.php diff --git a/README.md b/README.md index 955e0a99..31d0430a 100644 --- a/README.md +++ b/README.md @@ -2448,8 +2448,7 @@ constants with the `STATUS_*` prefix. For instance, the `200 OK` and `404 Not Found` status codes can used as `Response::STATUS_OK` and `Response::STATUS_NOT_FOUND` respectively. -> Internally, this implementation builds on top of an existing incoming - response message and only adds required streaming support. This base class is +> Internally, this implementation builds on top of a base class which is considered an implementation detail that may change in the future. ##### html() diff --git a/src/Io/AbstractMessage.php b/src/Io/AbstractMessage.php new file mode 100644 index 00000000..8523d6cd --- /dev/null +++ b/src/Io/AbstractMessage.php @@ -0,0 +1,164 @@ + */ + private $headers = array(); + + /** @var array */ + private $headerNamesLowerCase = array(); + + /** @var string */ + private $protocolVersion; + + /** @var StreamInterface */ + private $body; + + /** + * @param string $protocolVersion + * @param array $headers + * @param StreamInterface $body + */ + protected function __construct($protocolVersion, array $headers, StreamInterface $body) + { + foreach ($headers as $name => $value) { + if ($value !== array()) { + if (\is_array($value)) { + foreach ($value as &$one) { + $one = (string) $one; + } + } else { + $value = array((string) $value); + } + + $lower = \strtolower($name); + if (isset($this->headerNamesLowerCase[$lower])) { + $value = \array_merge($this->headers[$this->headerNamesLowerCase[$lower]], $value); + unset($this->headers[$this->headerNamesLowerCase[$lower]]); + } + + $this->headers[$name] = $value; + $this->headerNamesLowerCase[$lower] = $name; + } + } + + $this->protocolVersion = (string) $protocolVersion; + $this->body = $body; + } + + public function getProtocolVersion() + { + return $this->protocolVersion; + } + + public function withProtocolVersion($version) + { + if ((string) $version === $this->protocolVersion) { + return $this; + } + + $message = clone $this; + $message->protocolVersion = (string) $version; + + return $message; + } + + public function getHeaders() + { + return $this->headers; + } + + public function hasHeader($name) + { + return isset($this->headerNamesLowerCase[\strtolower($name)]); + } + + public function getHeader($name) + { + $lower = \strtolower($name); + return isset($this->headerNamesLowerCase[$lower]) ? $this->headers[$this->headerNamesLowerCase[$lower]] : array(); + } + + public function getHeaderLine($name) + { + return \implode(', ', $this->getHeader($name)); + } + + public function withHeader($name, $value) + { + if ($value === array()) { + return $this->withoutHeader($name); + } elseif (\is_array($value)) { + foreach ($value as &$one) { + $one = (string) $one; + } + } else { + $value = array((string) $value); + } + + $lower = \strtolower($name); + if (isset($this->headerNamesLowerCase[$lower]) && $this->headerNamesLowerCase[$lower] === (string) $name && $this->headers[$this->headerNamesLowerCase[$lower]] === $value) { + return $this; + } + + $message = clone $this; + if (isset($message->headerNamesLowerCase[$lower])) { + unset($message->headers[$message->headerNamesLowerCase[$lower]]); + } + + $message->headers[$name] = $value; + $message->headerNamesLowerCase[$lower] = $name; + + return $message; + } + + public function withAddedHeader($name, $value) + { + if ($value === array()) { + return $this; + } + + return $this->withHeader($name, \array_merge($this->getHeader($name), \is_array($value) ? $value : array($value))); + } + + public function withoutHeader($name) + { + $lower = \strtolower($name); + if (!isset($this->headerNamesLowerCase[$lower])) { + return $this; + } + + $message = clone $this; + unset($message->headers[$message->headerNamesLowerCase[$lower]], $message->headerNamesLowerCase[$lower]); + + return $message; + } + + public function getBody() + { + return $this->body; + } + + public function withBody(StreamInterface $body) + { + if ($body === $this->body) { + return $this; + } + + $message = clone $this; + $message->body = $body; + + return $message; + } +} diff --git a/src/Message/Response.php b/src/Message/Response.php index c50d0cee..95c82ec8 100644 --- a/src/Message/Response.php +++ b/src/Message/Response.php @@ -5,10 +5,10 @@ use Fig\Http\Message\StatusCodeInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\StreamInterface; +use React\Http\Io\AbstractMessage; use React\Http\Io\BufferedBody; use React\Http\Io\HttpBodyStream; use React\Stream\ReadableStreamInterface; -use RingCentral\Psr7\MessageTrait; /** * Represents an outgoing server response message. @@ -35,13 +35,12 @@ * `404 Not Found` status codes can used as `Response::STATUS_OK` and * `Response::STATUS_NOT_FOUND` respectively. * - * > Internally, this implementation builds on top of an existing incoming - * response message and only adds required streaming support. This base class is + * > Internally, this implementation builds on top a base class which is * considered an implementation detail that may change in the future. * * @see \Psr\Http\Message\ResponseInterface */ -final class Response extends MessageTrait implements ResponseInterface, StatusCodeInterface +final class Response extends AbstractMessage implements ResponseInterface, StatusCodeInterface { /** * Create an HTML response @@ -316,9 +315,7 @@ public function __construct( throw new \InvalidArgumentException('Invalid response body given'); } - $this->protocol = (string) $version; - $this->setHeaders($headers); - $this->stream = $body; + parent::__construct($version, $headers, $body); $this->statusCode = (int) $status; $this->reasonPhrase = ($reason !== '' && $reason !== null) ? (string) $reason : self::getReasonPhraseForStatusCode($status); diff --git a/tests/Io/AbstractMessageTest.php b/tests/Io/AbstractMessageTest.php new file mode 100644 index 00000000..9e2c7d32 --- /dev/null +++ b/tests/Io/AbstractMessageTest.php @@ -0,0 +1,222 @@ + $headers + * @param StreamInterface $body + */ + public function __construct($protocolVersion, array $headers, StreamInterface $body) + { + return parent::__construct($protocolVersion, $headers, $body); + } +} + +class AbstractMessageTest extends TestCase +{ + public function testWithProtocolVersionReturnsNewInstanceWhenProtocolVersionIsChanged() + { + $message = new MessageMock( + '1.1', + array(), + $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock() + ); + + $new = $message->withProtocolVersion('1.0'); + $this->assertNotSame($message, $new); + $this->assertEquals('1.0', $new->getProtocolVersion()); + $this->assertEquals('1.1', $message->getProtocolVersion()); + } + + public function testWithProtocolVersionReturnsSameInstanceWhenProtocolVersionIsUnchanged() + { + $message = new MessageMock( + '1.1', + array(), + $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock() + ); + + $new = $message->withProtocolVersion('1.1'); + $this->assertSame($message, $new); + $this->assertEquals('1.1', $message->getProtocolVersion()); + } + + public function testHeaderWithStringValue() + { + $message = new MessageMock( + '1.1', + array( + 'Content-Type' => 'text/plain' + ), + $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock() + ); + + $this->assertEquals(array('Content-Type' => array('text/plain')), $message->getHeaders()); + + $this->assertEquals(array('text/plain'), $message->getHeader('Content-Type')); + $this->assertEquals(array('text/plain'), $message->getHeader('CONTENT-type')); + + $this->assertEquals('text/plain', $message->getHeaderLine('Content-Type')); + $this->assertEquals('text/plain', $message->getHeaderLine('CONTENT-Type')); + + $this->assertTrue($message->hasHeader('Content-Type')); + $this->assertTrue($message->hasHeader('content-TYPE')); + + $new = $message->withHeader('Content-Type', 'text/plain'); + $this->assertSame($message, $new); + + $new = $message->withHeader('Content-Type', array('text/plain')); + $this->assertSame($message, $new); + + $new = $message->withHeader('content-type', 'text/plain'); + $this->assertNotSame($message, $new); + $this->assertEquals(array('content-type' => array('text/plain')), $new->getHeaders()); + $this->assertEquals(array('Content-Type' => array('text/plain')), $message->getHeaders()); + + $new = $message->withHeader('Content-Type', 'text/html'); + $this->assertNotSame($message, $new); + $this->assertEquals(array('Content-Type' => array('text/html')), $new->getHeaders()); + $this->assertEquals(array('Content-Type' => array('text/plain')), $message->getHeaders()); + + $new = $message->withHeader('Content-Type', array('text/html')); + $this->assertNotSame($message, $new); + $this->assertEquals(array('Content-Type' => array('text/html')), $new->getHeaders()); + $this->assertEquals(array('Content-Type' => array('text/plain')), $message->getHeaders()); + + $new = $message->withAddedHeader('Content-Type', array()); + $this->assertSame($message, $new); + + $new = $message->withoutHeader('Content-Type'); + $this->assertNotSame($message, $new); + $this->assertEquals(array(), $new->getHeaders()); + $this->assertEquals(array('Content-Type' => array('text/plain')), $message->getHeaders()); + } + + public function testHeaderWithMultipleValues() + { + $message = new MessageMock( + '1.1', + array( + 'Set-Cookie' => array( + 'a=1', + 'b=2' + ) + ), + $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock() + ); + + $this->assertEquals(array('Set-Cookie' => array('a=1', 'b=2')), $message->getHeaders()); + + $this->assertEquals(array('a=1', 'b=2'), $message->getHeader('Set-Cookie')); + $this->assertEquals(array('a=1', 'b=2'), $message->getHeader('Set-Cookie')); + + $this->assertEquals('a=1, b=2', $message->getHeaderLine('Set-Cookie')); + $this->assertEquals('a=1, b=2', $message->getHeaderLine('Set-Cookie')); + + $this->assertTrue($message->hasHeader('Set-Cookie')); + $this->assertTrue($message->hasHeader('Set-Cookie')); + + $new = $message->withHeader('Set-Cookie', array('a=1', 'b=2')); + $this->assertSame($message, $new); + + $new = $message->withHeader('Set-Cookie', array('a=1', 'b=2', 'c=3')); + $this->assertNotSame($message, $new); + $this->assertEquals(array('Set-Cookie' => array('a=1', 'b=2', 'c=3')), $new->getHeaders()); + $this->assertEquals(array('Set-Cookie' => array('a=1', 'b=2')), $message->getHeaders()); + + $new = $message->withAddedHeader('Set-Cookie', array()); + $this->assertSame($message, $new); + + $new = $message->withAddedHeader('Set-Cookie', 'c=3'); + $this->assertNotSame($message, $new); + $this->assertEquals(array('Set-Cookie' => array('a=1', 'b=2', 'c=3')), $new->getHeaders()); + $this->assertEquals(array('Set-Cookie' => array('a=1', 'b=2')), $message->getHeaders()); + + $new = $message->withAddedHeader('Set-Cookie', array('c=3')); + $this->assertNotSame($message, $new); + $this->assertEquals(array('Set-Cookie' => array('a=1', 'b=2', 'c=3')), $new->getHeaders()); + $this->assertEquals(array('Set-Cookie' => array('a=1', 'b=2')), $message->getHeaders()); + } + + public function testHeaderWithEmptyValue() + { + $message = new MessageMock( + '1.1', + array( + 'Content-Type' => array() + ), + $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock() + ); + + $this->assertEquals(array(), $message->getHeaders()); + + $this->assertEquals(array(), $message->getHeader('Content-Type')); + $this->assertEquals('', $message->getHeaderLine('Content-Type')); + $this->assertFalse($message->hasHeader('Content-Type')); + + $new = $message->withHeader('Empty', array()); + $this->assertSame($message, $new); + $this->assertFalse($new->hasHeader('Empty')); + + $new = $message->withAddedHeader('Empty', array()); + $this->assertSame($message, $new); + $this->assertFalse($new->hasHeader('Empty')); + + $new = $message->withoutHeader('Empty'); + $this->assertSame($message, $new); + $this->assertFalse($new->hasHeader('Empty')); + } + + public function testHeaderWithMultipleValuesAcrossMixedCaseNamesInConstructorMergesAllValuesWithNameFromLastNonEmptyValue() + { + $message = new MessageMock( + '1.1', + array( + 'SET-Cookie' => 'a=1', + 'set-cookie' => array('b=2'), + 'set-COOKIE' => array() + ), + $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock() + ); + + $this->assertEquals(array('set-cookie' => array('a=1', 'b=2')), $message->getHeaders()); + $this->assertEquals(array('a=1', 'b=2'), $message->getHeader('Set-Cookie')); + } + + public function testWithBodyReturnsNewInstanceWhenBodyIsChanged() + { + $body = $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(); + $message = new MessageMock( + '1.1', + array(), + $body + ); + + $body2 = $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(); + $new = $message->withBody($body2); + $this->assertNotSame($message, $new); + $this->assertSame($body2, $new->getBody()); + $this->assertSame($body, $message->getBody()); + } + + public function testWithBodyReturnsSameInstanceWhenBodyIsUnchanged() + { + $body = $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(); + $message = new MessageMock( + '1.1', + array(), + $body + ); + + $new = $message->withBody($body); + $this->assertSame($message, $new); + $this->assertEquals($body, $message->getBody()); + } +} diff --git a/tests/Io/MiddlewareRunnerTest.php b/tests/Io/MiddlewareRunnerTest.php index ac836f03..762d7bdb 100644 --- a/tests/Io/MiddlewareRunnerTest.php +++ b/tests/Io/MiddlewareRunnerTest.php @@ -6,12 +6,12 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use React\Http\Io\MiddlewareRunner; +use React\Http\Message\Response; use React\Http\Message\ServerRequest; use React\Promise; use React\Promise\PromiseInterface; use React\Tests\Http\Middleware\ProcessStack; use React\Tests\Http\TestCase; -use RingCentral\Psr7\Response; final class MiddlewareRunnerTest extends TestCase { diff --git a/tests/Message/ResponseExceptionTest.php b/tests/Message/ResponseExceptionTest.php index 33eeea9e..b2eaccd3 100644 --- a/tests/Message/ResponseExceptionTest.php +++ b/tests/Message/ResponseExceptionTest.php @@ -2,9 +2,9 @@ namespace React\Tests\Http\Message; +use React\Http\Message\Response; use React\Http\Message\ResponseException; use PHPUnit\Framework\TestCase; -use RingCentral\Psr7\Response; class ResponseExceptionTest extends TestCase { From 3313e1fb7c9a47416458944f7988db3f476892fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 12 Mar 2024 15:24:13 +0100 Subject: [PATCH 438/456] Add internal `AbstractRequest` base class for `Request` class (PSR-7) --- README.md | 3 +- src/Io/AbstractRequest.php | 156 +++++++++++ src/Message/Request.php | 7 +- tests/Io/AbstractRequestTest.php | 449 +++++++++++++++++++++++++++++++ 4 files changed, 609 insertions(+), 6 deletions(-) create mode 100644 src/Io/AbstractRequest.php create mode 100644 tests/Io/AbstractRequestTest.php diff --git a/README.md b/README.md index 31d0430a..067e5b9f 100644 --- a/README.md +++ b/README.md @@ -2642,8 +2642,7 @@ This is mostly used internally to represent each outgoing HTTP request message for the HTTP client implementation. Likewise, you can also use this class with other HTTP client implementations and for tests. -> Internally, this implementation builds on top of an existing outgoing - request message and only adds support for streaming. This base class is +> Internally, this implementation builds on top of a base class which is considered an implementation detail that may change in the future. #### ServerRequest diff --git a/src/Io/AbstractRequest.php b/src/Io/AbstractRequest.php new file mode 100644 index 00000000..51059ac5 --- /dev/null +++ b/src/Io/AbstractRequest.php @@ -0,0 +1,156 @@ + $headers + * @param StreamInterface $body + * @param string unknown $protocolVersion + */ + protected function __construct( + $method, + $uri, + array $headers, + StreamInterface $body, + $protocolVersion + ) { + if (\is_string($uri)) { + $uri = new Uri($uri); + } elseif (!$uri instanceof UriInterface) { + throw new \InvalidArgumentException( + 'Argument #2 ($uri) expected string|Psr\Http\Message\UriInterface' + ); + } + + // assign default `Host` request header from URI unless already given explicitly + $host = $uri->getHost(); + if ($host !== '') { + foreach ($headers as $name => $value) { + if (\strtolower($name) === 'host' && $value !== array()) { + $host = ''; + break; + } + } + if ($host !== '') { + $port = $uri->getPort(); + if ($port !== null && (!($port === 80 && $uri->getScheme() === 'http') || !($port === 443 && $uri->getScheme() === 'https'))) { + $host .= ':' . $port; + } + + $headers = array('Host' => $host) + $headers; + } + } + + parent::__construct($protocolVersion, $headers, $body); + + $this->method = $method; + $this->uri = $uri; + } + + public function getRequestTarget() + { + if ($this->requestTarget !== null) { + return $this->requestTarget; + } + + $target = $this->uri->getPath(); + if ($target === '') { + $target = '/'; + } + if (($query = $this->uri->getQuery()) !== '') { + $target .= '?' . $query; + } + + return $target; + } + + public function withRequestTarget($requestTarget) + { + if ((string) $requestTarget === $this->requestTarget) { + return $this; + } + + $request = clone $this; + $request->requestTarget = (string) $requestTarget; + + return $request; + } + + public function getMethod() + { + return $this->method; + } + + public function withMethod($method) + { + if ((string) $method === $this->method) { + return $this; + } + + $request = clone $this; + $request->method = (string) $method; + + return $request; + } + + public function getUri() + { + return $this->uri; + } + + public function withUri(UriInterface $uri, $preserveHost = false) + { + if ($uri === $this->uri) { + return $this; + } + + $request = clone $this; + $request->uri = $uri; + + $host = $uri->getHost(); + $port = $uri->getPort(); + if ($port !== null && $host !== '' && (!($port === 80 && $uri->getScheme() === 'http') || !($port === 443 && $uri->getScheme() === 'https'))) { + $host .= ':' . $port; + } + + // update `Host` request header if URI contains a new host and `$preserveHost` is false + if ($host !== '' && (!$preserveHost || $request->getHeaderLine('Host') === '')) { + // first remove all headers before assigning `Host` header to ensure it always comes first + foreach (\array_keys($request->getHeaders()) as $name) { + $request = $request->withoutHeader($name); + } + + // add `Host` header first, then all other original headers + $request = $request->withHeader('Host', $host); + foreach ($this->withoutHeader('Host')->getHeaders() as $name => $value) { + $request = $request->withHeader($name, $value); + } + } + + return $request; + } +} diff --git a/src/Message/Request.php b/src/Message/Request.php index cf59641e..3de8c1b3 100644 --- a/src/Message/Request.php +++ b/src/Message/Request.php @@ -5,10 +5,10 @@ use Psr\Http\Message\RequestInterface; use Psr\Http\Message\StreamInterface; use Psr\Http\Message\UriInterface; +use React\Http\Io\AbstractRequest; use React\Http\Io\BufferedBody; use React\Http\Io\ReadableBodyStream; use React\Stream\ReadableStreamInterface; -use RingCentral\Psr7\Request as BaseRequest; /** * Respresents an outgoing HTTP request message. @@ -22,13 +22,12 @@ * message for the HTTP client implementation. Likewise, you can also use this * class with other HTTP client implementations and for tests. * - * > Internally, this implementation builds on top of an existing outgoing - * request message and only adds support for streaming. This base class is + * > Internally, this implementation builds on top of a base class which is * considered an implementation detail that may change in the future. * * @see RequestInterface */ -final class Request extends BaseRequest implements RequestInterface +final class Request extends AbstractRequest implements RequestInterface { /** * @param string $method HTTP method for the request. diff --git a/tests/Io/AbstractRequestTest.php b/tests/Io/AbstractRequestTest.php new file mode 100644 index 00000000..28c9eaf1 --- /dev/null +++ b/tests/Io/AbstractRequestTest.php @@ -0,0 +1,449 @@ + $headers + * @param StreamInterface $body + * @param string $protocolVersion + */ + public function __construct( + $method, + $uri, + array $headers, + StreamInterface $body, + $protocolVersion + ) { + parent::__construct($method, $uri, $headers, $body, $protocolVersion); + } +} + +class AbstractRequestTest extends TestCase +{ + public function testCtorWithInvalidUriThrows() + { + $this->setExpectedException('InvalidArgumentException'); + new RequestMock( + 'GET', + null, + array(), + $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + '1.1' + ); + } + + public function testGetHeadersReturnsHostHeaderFromUri() + { + $request = new RequestMock( + 'GET', + 'http://example.com/', + array(), + $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + '1.1' + ); + + $this->assertEquals(array('Host' => array('example.com')), $request->getHeaders()); + } + + public function testGetHeadersReturnsHostHeaderFromUriWithCustomHttpPort() + { + $request = new RequestMock( + 'GET', + 'http://example.com:8080/', + array(), + $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + '1.1' + ); + + $this->assertEquals(array('Host' => array('example.com:8080')), $request->getHeaders()); + } + + public function testGetHeadersReturnsHostHeaderFromUriWithCustomPortHttpOnHttpsPort() + { + $request = new RequestMock( + 'GET', + 'http://example.com:443/', + array(), + $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + '1.1' + ); + + $this->assertEquals(array('Host' => array('example.com:443')), $request->getHeaders()); + } + + public function testGetHeadersReturnsHostHeaderFromUriWithCustomPortHttpsOnHttpPort() + { + $request = new RequestMock( + 'GET', + 'https://example.com:80/', + array(), + $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + '1.1' + ); + + $this->assertEquals(array('Host' => array('example.com:80')), $request->getHeaders()); + } + + public function testGetHeadersReturnsHostHeaderFromUriWithoutDefaultHttpPort() + { + $request = new RequestMock( + 'GET', + 'http://example.com:80/', + array(), + $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + '1.1' + ); + + $this->assertEquals(array('Host' => array('example.com')), $request->getHeaders()); + } + + public function testGetHeadersReturnsHostHeaderFromUriWithoutDefaultHttpsPort() + { + $request = new RequestMock( + 'GET', + 'https://example.com:443/', + array(), + $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + '1.1' + ); + + $this->assertEquals(array('Host' => array('example.com')), $request->getHeaders()); + } + + public function testGetHeadersReturnsHostHeaderFromUriBeforeOtherHeadersExplicitlyGiven() + { + $request = new RequestMock( + 'GET', + 'http://example.com/', + array( + 'User-Agent' => 'demo' + ), + $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + '1.1' + ); + + $this->assertEquals(array('Host' => array('example.com'), 'User-Agent' => array('demo')), $request->getHeaders()); + } + + public function testGetHeadersReturnsHostHeaderFromHeadersExplicitlyGiven() + { + $request = new RequestMock( + 'GET', + 'http://localhost/', + array( + 'Host' => 'example.com:8080' + ), + $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + '1.1' + ); + + $this->assertEquals(array('Host' => array('example.com:8080')), $request->getHeaders()); + } + + public function testGetHeadersReturnsHostHeaderFromUriWhenHeadersExplicitlyGivenContainEmptyHostArray() + { + $request = new RequestMock( + 'GET', + 'https://example.com/', + array( + 'Host' => array() + ), + $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + '1.1' + ); + + $this->assertEquals(array('Host' => array('example.com')), $request->getHeaders()); + } + + public function testGetRequestTargetReturnsPathAndQueryFromUri() + { + $request = new RequestMock( + 'GET', + 'http://example.com/demo?name=Alice', + array(), + $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + '1.1' + ); + + $this->assertEquals('/demo?name=Alice', $request->getRequestTarget()); + } + + public function testGetRequestTargetReturnsSlashOnlyIfUriHasNoPathOrQuery() + { + $request = new RequestMock( + 'GET', + 'http://example.com', + array(), + $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + '1.1' + ); + + $this->assertEquals('/', $request->getRequestTarget()); + } + + public function testGetRequestTargetReturnsRequestTargetInAbsoluteFormIfGivenExplicitly() + { + $request = new RequestMock( + 'GET', + 'http://example.com/demo?name=Alice', + array(), + $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + '1.1' + ); + $request = $request->withRequestTarget('http://example.com/demo?name=Alice'); + + $this->assertEquals('http://example.com/demo?name=Alice', $request->getRequestTarget()); + } + + public function testWithRequestTargetReturnsNewInstanceWhenRequestTargetIsChanged() + { + $request = new RequestMock( + 'GET', + 'http://example.com/', + array(), + $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + '1.1' + ); + + $new = $request->withRequestTarget('http://example.com/'); + $this->assertNotSame($request, $new); + $this->assertEquals('http://example.com/', $new->getRequestTarget()); + $this->assertEquals('/', $request->getRequestTarget()); + } + + public function testWithRequestTargetReturnsSameInstanceWhenRequestTargetIsUnchanged() + { + $request = new RequestMock( + 'GET', + 'http://example.com/', + array(), + $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + '1.1' + ); + $request = $request->withRequestTarget('/'); + + $new = $request->withRequestTarget('/'); + $this->assertSame($request, $new); + $this->assertEquals('/', $request->getRequestTarget()); + } + + public function testWithMethodReturnsNewInstanceWhenMethodIsChanged() + { + $request = new RequestMock( + 'GET', + 'http://example.com/', + array(), + $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + '1.1' + ); + + $new = $request->withMethod('POST'); + $this->assertNotSame($request, $new); + $this->assertEquals('POST', $new->getMethod()); + $this->assertEquals('GET', $request->getMethod()); + } + + public function testWithMethodReturnsSameInstanceWhenMethodIsUnchanged() + { + $request = new RequestMock( + 'GET', + 'http://example.com/', + array(), + $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + '1.1' + ); + + $new = $request->withMethod('GET'); + $this->assertSame($request, $new); + $this->assertEquals('GET', $request->getMethod()); + } + + public function testGetUriReturnsUriInstanceGivenToCtor() + { + $uri = $this->getMockBuilder('Psr\Http\Message\UriInterface')->getMock(); + + $request = new RequestMock( + 'GET', + $uri, + array(), + $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + '1.1' + ); + + $this->assertSame($uri, $request->getUri()); + } + + public function testGetUriReturnsUriInstanceForUriStringGivenToCtor() + { + $request = new RequestMock( + 'GET', + 'http://example.com/', + array(), + $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + '1.1' + ); + + $uri = $request->getUri(); + $this->assertInstanceOf('Psr\Http\Message\UriInterface', $uri); + $this->assertEquals('http://example.com/', (string) $uri); + } + + public function testWithUriReturnsNewInstanceWhenUriIsChanged() + { + $request = new RequestMock( + 'GET', + 'http://example.com/', + array(), + $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + '1.1' + ); + + $uri = $this->getMockBuilder('Psr\Http\Message\UriInterface')->getMock(); + $new = $request->withUri($uri); + + $this->assertNotSame($request, $new); + $this->assertEquals($uri, $new->getUri()); + $this->assertEquals('http://example.com/', (string) $request->getUri()); + } + + public function testWithUriReturnsSameInstanceWhenUriIsUnchanged() + { + $uri = $this->getMockBuilder('Psr\Http\Message\UriInterface')->getMock(); + + $request = new RequestMock( + 'GET', + $uri, + array(), + $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + '1.1' + ); + + $new = $request->withUri($uri); + $this->assertSame($request, $new); + $this->assertEquals($uri, $request->getUri()); + } + + public function testWithUriReturnsNewInstanceWithHostHeaderChangedIfUriContainsHost() + { + $request = new RequestMock( + 'GET', + 'http://example.com/', + array(), + $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + '1.1' + ); + + $uri = new Uri('http://localhost/'); + $new = $request->withUri($uri); + + $this->assertNotSame($request, $new); + $this->assertEquals('http://localhost/', (string) $new->getUri()); + $this->assertEquals(array('Host' => array('localhost')), $new->getHeaders()); + } + + public function testWithUriReturnsNewInstanceWithHostHeaderChangedIfUriContainsHostWithCustomPort() + { + $request = new RequestMock( + 'GET', + 'http://example.com/', + array(), + $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + '1.1' + ); + + $uri = new Uri('http://localhost:8080/'); + $new = $request->withUri($uri); + + $this->assertNotSame($request, $new); + $this->assertEquals('http://localhost:8080/', (string) $new->getUri()); + $this->assertEquals(array('Host' => array('localhost:8080')), $new->getHeaders()); + } + + public function testWithUriReturnsNewInstanceWithHostHeaderAddedAsFirstHeaderBeforeOthersIfUriContainsHost() + { + $request = new RequestMock( + 'GET', + 'http://example.com/', + array( + 'User-Agent' => 'test' + ), + $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + '1.1' + ); + $request = $request->withoutHeader('Host'); + + $uri = new Uri('http://localhost/'); + $new = $request->withUri($uri); + + $this->assertNotSame($request, $new); + $this->assertEquals('http://localhost/', (string) $new->getUri()); + $this->assertEquals(array('Host' => array('localhost'), 'User-Agent' => array('test')), $new->getHeaders()); + } + + public function testWithUriReturnsNewInstanceWithHostHeaderUnchangedIfUriContainsNoHost() + { + $request = new RequestMock( + 'GET', + 'http://example.com/', + array(), + $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + '1.1' + ); + + $uri = new Uri('/path'); + $new = $request->withUri($uri); + + $this->assertNotSame($request, $new); + $this->assertEquals('/path', (string) $new->getUri()); + $this->assertEquals(array('Host' => array('example.com')), $new->getHeaders()); + } + + public function testWithUriReturnsNewInstanceWithHostHeaderUnchangedIfPreserveHostIsTrue() + { + $request = new RequestMock( + 'GET', + 'http://example.com/', + array(), + $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + '1.1' + ); + + $uri = new Uri('http://localhost/'); + $new = $request->withUri($uri, true); + + $this->assertNotSame($request, $new); + $this->assertEquals('http://localhost/', (string) $new->getUri()); + $this->assertEquals(array('Host' => array('example.com')), $new->getHeaders()); + } + + public function testWithUriReturnsNewInstanceWithHostHeaderAddedAsFirstHeaderNoMatterIfPreserveHostIsTrue() + { + $request = new RequestMock( + 'GET', + 'http://example.com/', + array( + 'User-Agent' => 'test' + ), + $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + '1.1' + ); + $request = $request->withoutHeader('Host'); + + $uri = new Uri('http://example.com/'); + $new = $request->withUri($uri, true); + + $this->assertNotSame($request, $new); + $this->assertEquals('http://example.com/', (string) $new->getUri()); + $this->assertEquals(array('Host' => array('example.com'), 'User-Agent' => array('test')), $new->getHeaders()); + } +} From 0638dcdbea657c3226b90dfadd1737d706ca84e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 12 Mar 2024 18:20:42 +0100 Subject: [PATCH 439/456] Update `ServerRequest` class to build on top of abstract request class --- README.md | 3 +-- src/Message/ServerRequest.php | 25 ++++++++++--------------- tests/Io/StreamingServerTest.php | 32 ++++++++++++++++---------------- 3 files changed, 27 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 067e5b9f..47003770 100644 --- a/README.md +++ b/README.md @@ -2661,8 +2661,7 @@ This is mostly used internally to represent each incoming request message. Likewise, you can also use this class in test cases to test how your web application reacts to certain HTTP requests. -> Internally, this implementation builds on top of an existing outgoing - request message and only adds required server methods. This base class is +> Internally, this implementation builds on top of a base class which is considered an implementation detail that may change in the future. #### ResponseException diff --git a/src/Message/ServerRequest.php b/src/Message/ServerRequest.php index 25532cf4..b5c41413 100644 --- a/src/Message/ServerRequest.php +++ b/src/Message/ServerRequest.php @@ -5,10 +5,10 @@ use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\StreamInterface; use Psr\Http\Message\UriInterface; +use React\Http\Io\AbstractRequest; use React\Http\Io\BufferedBody; use React\Http\Io\HttpBodyStream; use React\Stream\ReadableStreamInterface; -use RingCentral\Psr7\Request as BaseRequest; /** * Respresents an incoming server request message. @@ -24,13 +24,12 @@ * Likewise, you can also use this class in test cases to test how your web * application reacts to certain HTTP requests. * - * > Internally, this implementation builds on top of an existing outgoing - * request message and only adds required server methods. This base class is + * > Internally, this implementation builds on top of a base class which is * considered an implementation detail that may change in the future. * * @see ServerRequestInterface */ -final class ServerRequest extends BaseRequest implements ServerRequestInterface +final class ServerRequest extends AbstractRequest implements ServerRequestInterface { private $attributes = array(); @@ -57,26 +56,22 @@ public function __construct( $version = '1.1', $serverParams = array() ) { - $stream = null; if (\is_string($body)) { $body = new BufferedBody($body); } elseif ($body instanceof ReadableStreamInterface && !$body instanceof StreamInterface) { - $stream = $body; - $body = null; + $temp = new self($method, '', $headers); + $size = (int) $temp->getHeaderLine('Content-Length'); + if (\strtolower($temp->getHeaderLine('Transfer-Encoding')) === 'chunked') { + $size = null; + } + $body = new HttpBodyStream($body, $size); } elseif (!$body instanceof StreamInterface) { throw new \InvalidArgumentException('Invalid server request body given'); } - $this->serverParams = $serverParams; parent::__construct($method, $url, $headers, $body, $version); - if ($stream !== null) { - $size = (int) $this->getHeaderLine('Content-Length'); - if (\strtolower($this->getHeaderLine('Transfer-Encoding')) === 'chunked') { - $size = null; - } - $this->stream = new HttpBodyStream($stream, $size); - } + $this->serverParams = $serverParams; $query = $this->getUri()->getQuery(); if ($query !== '') { diff --git a/tests/Io/StreamingServerTest.php b/tests/Io/StreamingServerTest.php index 64566ddc..afab371e 100644 --- a/tests/Io/StreamingServerTest.php +++ b/tests/Io/StreamingServerTest.php @@ -126,7 +126,7 @@ public function testRequestEvent() $serverParams = $requestAssertion->getServerParams(); $this->assertSame(1, $i); - $this->assertInstanceOf('RingCentral\Psr7\Request', $requestAssertion); + $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion); $this->assertSame('GET', $requestAssertion->getMethod()); $this->assertSame('/', $requestAssertion->getRequestTarget()); $this->assertSame('/', $requestAssertion->getUri()->getPath()); @@ -159,7 +159,7 @@ public function testRequestEventWithSingleRequestHandlerArray() $serverParams = $requestAssertion->getServerParams(); $this->assertSame(1, $i); - $this->assertInstanceOf('RingCentral\Psr7\Request', $requestAssertion); + $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion); $this->assertSame('GET', $requestAssertion->getMethod()); $this->assertSame('/', $requestAssertion->getRequestTarget()); $this->assertSame('/', $requestAssertion->getUri()->getPath()); @@ -182,7 +182,7 @@ public function testRequestGetWithHostAndCustomPort() $data = "GET / HTTP/1.1\r\nHost: example.com:8080\r\n\r\n"; $this->connection->emit('data', array($data)); - $this->assertInstanceOf('RingCentral\Psr7\Request', $requestAssertion); + $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion); $this->assertSame('GET', $requestAssertion->getMethod()); $this->assertSame('/', $requestAssertion->getRequestTarget()); $this->assertSame('/', $requestAssertion->getUri()->getPath()); @@ -204,7 +204,7 @@ public function testRequestGetWithHostAndHttpsPort() $data = "GET / HTTP/1.1\r\nHost: example.com:443\r\n\r\n"; $this->connection->emit('data', array($data)); - $this->assertInstanceOf('RingCentral\Psr7\Request', $requestAssertion); + $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion); $this->assertSame('GET', $requestAssertion->getMethod()); $this->assertSame('/', $requestAssertion->getRequestTarget()); $this->assertSame('/', $requestAssertion->getUri()->getPath()); @@ -226,7 +226,7 @@ public function testRequestGetWithHostAndDefaultPortWillBeIgnored() $data = "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n"; $this->connection->emit('data', array($data)); - $this->assertInstanceOf('RingCentral\Psr7\Request', $requestAssertion); + $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion); $this->assertSame('GET', $requestAssertion->getMethod()); $this->assertSame('/', $requestAssertion->getRequestTarget()); $this->assertSame('/', $requestAssertion->getUri()->getPath()); @@ -248,7 +248,7 @@ public function testRequestGetHttp10WithoutHostWillBeIgnored() $data = "GET / HTTP/1.0\r\n\r\n"; $this->connection->emit('data', array($data)); - $this->assertInstanceOf('RingCentral\Psr7\Request', $requestAssertion); + $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion); $this->assertSame('GET', $requestAssertion->getMethod()); $this->assertSame('/', $requestAssertion->getRequestTarget()); $this->assertSame('/', $requestAssertion->getUri()->getPath()); @@ -283,7 +283,7 @@ public function testRequestOptionsAsterisk() $data = "OPTIONS * HTTP/1.1\r\nHost: example.com\r\n\r\n"; $this->connection->emit('data', array($data)); - $this->assertInstanceOf('RingCentral\Psr7\Request', $requestAssertion); + $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion); $this->assertSame('OPTIONS', $requestAssertion->getMethod()); $this->assertSame('*', $requestAssertion->getRequestTarget()); $this->assertSame('', $requestAssertion->getUri()->getPath()); @@ -316,7 +316,7 @@ public function testRequestConnectAuthorityForm() $data = "CONNECT example.com:443 HTTP/1.1\r\nHost: example.com:443\r\n\r\n"; $this->connection->emit('data', array($data)); - $this->assertInstanceOf('RingCentral\Psr7\Request', $requestAssertion); + $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion); $this->assertSame('CONNECT', $requestAssertion->getMethod()); $this->assertSame('example.com:443', $requestAssertion->getRequestTarget()); $this->assertSame('', $requestAssertion->getUri()->getPath()); @@ -338,7 +338,7 @@ public function testRequestConnectWithoutHostWillBePassesAsIs() $data = "CONNECT example.com:443 HTTP/1.1\r\n\r\n"; $this->connection->emit('data', array($data)); - $this->assertInstanceOf('RingCentral\Psr7\Request', $requestAssertion); + $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion); $this->assertSame('CONNECT', $requestAssertion->getMethod()); $this->assertSame('example.com:443', $requestAssertion->getRequestTarget()); $this->assertSame('', $requestAssertion->getUri()->getPath()); @@ -360,7 +360,7 @@ public function testRequestConnectAuthorityFormWithDefaultPortWillBePassedAsIs() $data = "CONNECT example.com:80 HTTP/1.1\r\nHost: example.com:80\r\n\r\n"; $this->connection->emit('data', array($data)); - $this->assertInstanceOf('RingCentral\Psr7\Request', $requestAssertion); + $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion); $this->assertSame('CONNECT', $requestAssertion->getMethod()); $this->assertSame('example.com:80', $requestAssertion->getRequestTarget()); $this->assertSame('', $requestAssertion->getUri()->getPath()); @@ -382,7 +382,7 @@ public function testRequestConnectAuthorityFormNonMatchingHostWillBePassedAsIs() $data = "CONNECT example.com:80 HTTP/1.1\r\nHost: other.example.org\r\n\r\n"; $this->connection->emit('data', array($data)); - $this->assertInstanceOf('RingCentral\Psr7\Request', $requestAssertion); + $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion); $this->assertSame('CONNECT', $requestAssertion->getMethod()); $this->assertSame('example.com:80', $requestAssertion->getRequestTarget()); $this->assertSame('', $requestAssertion->getUri()->getPath()); @@ -434,7 +434,7 @@ public function testRequestWithoutHostEventUsesSocketAddress() $data = "GET /test HTTP/1.0\r\n\r\n"; $this->connection->emit('data', array($data)); - $this->assertInstanceOf('RingCentral\Psr7\Request', $requestAssertion); + $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion); $this->assertSame('GET', $requestAssertion->getMethod()); $this->assertSame('/test', $requestAssertion->getRequestTarget()); $this->assertEquals('http://127.0.0.1/test', $requestAssertion->getUri()); @@ -455,7 +455,7 @@ public function testRequestAbsoluteEvent() $data = "GET http://example.com/test HTTP/1.1\r\nHost: example.com\r\n\r\n"; $this->connection->emit('data', array($data)); - $this->assertInstanceOf('RingCentral\Psr7\Request', $requestAssertion); + $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion); $this->assertSame('GET', $requestAssertion->getMethod()); $this->assertSame('http://example.com/test', $requestAssertion->getRequestTarget()); $this->assertEquals('http://example.com/test', $requestAssertion->getUri()); @@ -477,7 +477,7 @@ public function testRequestAbsoluteNonMatchingHostWillBePassedAsIs() $data = "GET http://example.com/test HTTP/1.1\r\nHost: other.example.org\r\n\r\n"; $this->connection->emit('data', array($data)); - $this->assertInstanceOf('RingCentral\Psr7\Request', $requestAssertion); + $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion); $this->assertSame('GET', $requestAssertion->getMethod()); $this->assertSame('http://example.com/test', $requestAssertion->getRequestTarget()); $this->assertEquals('http://example.com/test', $requestAssertion->getUri()); @@ -511,7 +511,7 @@ public function testRequestOptionsAsteriskEvent() $data = "OPTIONS * HTTP/1.1\r\nHost: example.com\r\n\r\n"; $this->connection->emit('data', array($data)); - $this->assertInstanceOf('RingCentral\Psr7\Request', $requestAssertion); + $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion); $this->assertSame('OPTIONS', $requestAssertion->getMethod()); $this->assertSame('*', $requestAssertion->getRequestTarget()); $this->assertEquals('http://example.com', $requestAssertion->getUri()); @@ -533,7 +533,7 @@ public function testRequestOptionsAbsoluteEvent() $data = "OPTIONS http://example.com HTTP/1.1\r\nHost: example.com\r\n\r\n"; $this->connection->emit('data', array($data)); - $this->assertInstanceOf('RingCentral\Psr7\Request', $requestAssertion); + $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion); $this->assertSame('OPTIONS', $requestAssertion->getMethod()); $this->assertSame('http://example.com', $requestAssertion->getRequestTarget()); $this->assertEquals('http://example.com', $requestAssertion->getUri()); From 5896f81bbd6a024581c08330ca619060f23656a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 15 Mar 2024 17:19:54 +0100 Subject: [PATCH 440/456] Move parsing incoming HTTP request message to `ServerRequest` --- src/Io/AbstractMessage.php | 8 ++ src/Io/RequestHeaderParser.php | 130 +------------------------- src/Message/ServerRequest.php | 139 ++++++++++++++++++++++++++++ tests/Message/ServerRequestTest.php | 122 ++++++++++++++++++++++++ 4 files changed, 270 insertions(+), 129 deletions(-) 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/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/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/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()); + } } From c0e1f4d90b27b57b839a2cc2fa8ca315d39d8c73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 21 Mar 2024 15:08:15 +0100 Subject: [PATCH 441/456] Move parsing incoming HTTP response message to `Response` --- src/Io/ClientRequestStream.php | 14 +++-- src/Message/Response.php | 42 +++++++++++++++ tests/Message/ResponseTest.php | 94 ++++++++++++++++++++++++++++++++++ 3 files changed, 146 insertions(+), 4 deletions(-) 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/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/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"); + } } From a73e9f78ab31b3e8876371236f26e9d559ea59c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 14 Mar 2024 11:43:50 +0100 Subject: [PATCH 442/456] Add new `Uri` class for new PSR-7 implementation --- README.md | 13 + src/Io/AbstractRequest.php | 2 +- src/Message/Uri.php | 292 ++++++++++++ tests/BrowserTest.php | 1 - tests/Io/AbstractRequestTest.php | 2 +- tests/Io/ClientConnectionManagerTest.php | 2 +- tests/Io/ClientRequestStreamTest.php | 2 +- tests/Message/UriTest.php | 581 +++++++++++++++++++++++ 8 files changed, 890 insertions(+), 5 deletions(-) create mode 100644 src/Message/Uri.php create mode 100644 tests/Message/UriTest.php diff --git a/README.md b/README.md index 47003770..18089464 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,7 @@ multiple concurrent HTTP requests without blocking. * [xml()](#xml) * [Request](#request-1) * [ServerRequest](#serverrequest) + * [Uri](#uri) * [ResponseException](#responseexception) * [React\Http\Middleware](#reacthttpmiddleware) * [StreamingRequestMiddleware](#streamingrequestmiddleware) @@ -2664,6 +2665,18 @@ application reacts to certain HTTP requests. > Internally, this implementation builds on top of a base class which is considered an implementation detail that may change in the future. +#### Uri + +The `React\Http\Message\Uri` class can be used to +respresent a URI (or URL). + +This class implements the +[PSR-7 `UriInterface`](https://www.php-fig.org/psr/psr-7/#35-psrhttpmessageuriinterface). + +This is mostly used internally to represent the URI of each HTTP request +message for our HTTP client and server implementations. Likewise, you may +also use this class with other HTTP implementations and for tests. + #### ResponseException The `React\Http\Message\ResponseException` is an `Exception` sub-class that will be used to reject diff --git a/src/Io/AbstractRequest.php b/src/Io/AbstractRequest.php index 51059ac5..f32307f7 100644 --- a/src/Io/AbstractRequest.php +++ b/src/Io/AbstractRequest.php @@ -5,7 +5,7 @@ use Psr\Http\Message\RequestInterface; use Psr\Http\Message\StreamInterface; use Psr\Http\Message\UriInterface; -use RingCentral\Psr7\Uri; +use React\Http\Message\Uri; /** * [Internal] Abstract HTTP request base class (PSR-7) diff --git a/src/Message/Uri.php b/src/Message/Uri.php new file mode 100644 index 00000000..f2cf7d99 --- /dev/null +++ b/src/Message/Uri.php @@ -0,0 +1,292 @@ +scheme = \strtolower($parts['scheme']); + } + + if (isset($parts['user']) || isset($parts['pass'])) { + $this->userInfo = $this->encode(isset($parts['user']) ? $parts['user'] : '', \PHP_URL_USER) . (isset($parts['pass']) ? ':' . $this->encode($parts['pass'], \PHP_URL_PASS) : ''); + } + + if (isset($parts['host'])) { + $this->host = \strtolower($parts['host']); + } + + if (isset($parts['port']) && !(($parts['port'] === 80 && $this->scheme === 'http') || ($parts['port'] === 443 && $this->scheme === 'https'))) { + $this->port = $parts['port']; + } + + if (isset($parts['path'])) { + $this->path = $this->encode($parts['path'], \PHP_URL_PATH); + } + + if (isset($parts['query'])) { + $this->query = $this->encode($parts['query'], \PHP_URL_QUERY); + } + + if (isset($parts['fragment'])) { + $this->fragment = $this->encode($parts['fragment'], \PHP_URL_FRAGMENT); + } + } + + public function getScheme() + { + return $this->scheme; + } + + public function getAuthority() + { + if ($this->host === '') { + return ''; + } + + return ($this->userInfo !== '' ? $this->userInfo . '@' : '') . $this->host . ($this->port !== null ? ':' . $this->port : ''); + } + + public function getUserInfo() + { + return $this->userInfo; + } + + public function getHost() + { + return $this->host; + } + + public function getPort() + { + return $this->port; + } + + public function getPath() + { + return $this->path; + } + + public function getQuery() + { + return $this->query; + } + + public function getFragment() + { + return $this->fragment; + } + + public function withScheme($scheme) + { + $scheme = \strtolower($scheme); + if ($scheme === $this->scheme) { + return $this; + } + + if (!\preg_match('#^[a-z]*$#', $scheme)) { + throw new \InvalidArgumentException('Invalid URI scheme given'); + } + + $new = clone $this; + $new->scheme = $scheme; + + if (($this->port === 80 && $scheme === 'http') || ($this->port === 443 && $scheme === 'https')) { + $new->port = null; + } + + return $new; + } + + public function withUserInfo($user, $password = null) + { + $userInfo = $this->encode($user, \PHP_URL_USER) . ($password !== null ? ':' . $this->encode($password, \PHP_URL_PASS) : ''); + if ($userInfo === $this->userInfo) { + return $this; + } + + $new = clone $this; + $new->userInfo = $userInfo; + + return $new; + } + + public function withHost($host) + { + $host = \strtolower($host); + if ($host === $this->host) { + return $this; + } + + if (\preg_match('#[\s_%+]#', $host) || ($host !== '' && \parse_url('http://' . $host, \PHP_URL_HOST) !== $host)) { + throw new \InvalidArgumentException('Invalid URI host given'); + } + + $new = clone $this; + $new->host = $host; + + return $new; + } + + public function withPort($port) + { + $port = $port === null ? null : (int) $port; + if (($port === 80 && $this->scheme === 'http') || ($port === 443 && $this->scheme === 'https')) { + $port = null; + } + + if ($port === $this->port) { + return $this; + } + + if ($port !== null && ($port < 1 || $port > 0xffff)) { + throw new \InvalidArgumentException('Invalid URI port given'); + } + + $new = clone $this; + $new->port = $port; + + return $new; + } + + public function withPath($path) + { + $path = $this->encode($path, \PHP_URL_PATH); + if ($path === $this->path) { + return $this; + } + + $new = clone $this; + $new->path = $path; + + return $new; + } + + public function withQuery($query) + { + $query = $this->encode($query, \PHP_URL_QUERY); + if ($query === $this->query) { + return $this; + } + + $new = clone $this; + $new->query = $query; + + return $new; + } + + public function withFragment($fragment) + { + $fragment = $this->encode($fragment, \PHP_URL_FRAGMENT); + if ($fragment === $this->fragment) { + return $this; + } + + $new = clone $this; + $new->fragment = $fragment; + + return $new; + } + + public function __toString() + { + $uri = ''; + if ($this->scheme !== '') { + $uri .= $this->scheme . ':'; + } + + $authority = $this->getAuthority(); + if ($authority !== '') { + $uri .= '//' . $authority; + } + + if ($authority !== '' && isset($this->path[0]) && $this->path[0] !== '/') { + $uri .= '/' . $this->path; + } elseif ($authority === '' && isset($this->path[0]) && $this->path[0] === '/') { + $uri .= '/' . \ltrim($this->path, '/'); + } else { + $uri .= $this->path; + } + + if ($this->query !== '') { + $uri .= '?' . $this->query; + } + + if ($this->fragment !== '') { + $uri .= '#' . $this->fragment; + } + + return $uri; + } + + /** + * @param string $part + * @param int $component + * @return string + */ + private function encode($part, $component) + { + return \preg_replace_callback( + '/(?:[^a-z0-9_\-\.~!\$&\'\(\)\*\+,;=' . ($component === \PHP_URL_PATH ? ':@\/' : ($component === \PHP_URL_QUERY || $component === \PHP_URL_FRAGMENT ? ':@\/\?' : '')) . '%]++|%(?![a-f0-9]{2}))/i', + function (array $match) { + return \rawurlencode($match[0]); + }, + $part + ); + } +} diff --git a/tests/BrowserTest.php b/tests/BrowserTest.php index b7958016..fdd338d9 100644 --- a/tests/BrowserTest.php +++ b/tests/BrowserTest.php @@ -5,7 +5,6 @@ use Psr\Http\Message\RequestInterface; use React\Http\Browser; use React\Promise\Promise; -use RingCentral\Psr7\Uri; class BrowserTest extends TestCase { diff --git a/tests/Io/AbstractRequestTest.php b/tests/Io/AbstractRequestTest.php index 28c9eaf1..7ff4a9a5 100644 --- a/tests/Io/AbstractRequestTest.php +++ b/tests/Io/AbstractRequestTest.php @@ -5,8 +5,8 @@ use Psr\Http\Message\StreamInterface; use Psr\Http\Message\UriInterface; use React\Http\Io\AbstractRequest; +use React\Http\Message\Uri; use React\Tests\Http\TestCase; -use RingCentral\Psr7\Uri; class RequestMock extends AbstractRequest { diff --git a/tests/Io/ClientConnectionManagerTest.php b/tests/Io/ClientConnectionManagerTest.php index b28c7964..6aafa6db 100644 --- a/tests/Io/ClientConnectionManagerTest.php +++ b/tests/Io/ClientConnectionManagerTest.php @@ -2,8 +2,8 @@ namespace React\Tests\Http\Io; -use RingCentral\Psr7\Uri; use React\Http\Io\ClientConnectionManager; +use React\Http\Message\Uri; use React\Promise\Promise; use React\Promise\PromiseInterface; use React\Tests\Http\TestCase; diff --git a/tests/Io/ClientRequestStreamTest.php b/tests/Io/ClientRequestStreamTest.php index 4649087a..181db173 100644 --- a/tests/Io/ClientRequestStreamTest.php +++ b/tests/Io/ClientRequestStreamTest.php @@ -3,9 +3,9 @@ namespace React\Tests\Http\Io; use Psr\Http\Message\ResponseInterface; -use RingCentral\Psr7\Uri; use React\Http\Io\ClientRequestStream; use React\Http\Message\Request; +use React\Http\Message\Uri; use React\Promise\Deferred; use React\Promise\Promise; use React\Stream\DuplexResourceStream; diff --git a/tests/Message/UriTest.php b/tests/Message/UriTest.php new file mode 100644 index 00000000..95c7fa4e --- /dev/null +++ b/tests/Message/UriTest.php @@ -0,0 +1,581 @@ +setExpectedException('InvalidArgumentException'); + new Uri('///'); + } + + public function testCtorWithInvalidSchemeThrows() + { + $this->setExpectedException('InvalidArgumentException'); + new Uri('not+a+scheme://localhost'); + } + + public function testCtorWithInvalidHostThrows() + { + $this->setExpectedException('InvalidArgumentException'); + new Uri('http://not a host/'); + } + + public function testCtorWithInvalidPortThrows() + { + $this->setExpectedException('InvalidArgumentException'); + new Uri('http://localhost:80000/'); + } + + public static function provideValidUris() + { + return array( + array( + 'http://localhost' + ), + array( + 'http://localhost/' + ), + array( + 'http://localhost:8080/' + ), + array( + 'http://127.0.0.1/' + ), + array( + 'http://[::1]:8080/' + ), + array( + 'http://localhost/path' + ), + array( + 'http://localhost/sub/path' + ), + array( + 'http://localhost/with%20space' + ), + array( + 'http://localhost/with%2fslash' + ), + array( + 'http://localhost/?name=Alice' + ), + array( + 'http://localhost/?name=John+Doe' + ), + array( + 'http://localhost/?name=John%20Doe' + ), + array( + 'http://localhost/?name=Alice&age=42' + ), + array( + 'http://localhost/?name=Alice&' + ), + array( + 'http://localhost/?choice=A%26B' + ), + array( + 'http://localhost/?safe=Yes!?' + ), + array( + 'http://localhost/?alias=@home' + ), + array( + 'http://localhost/?assign:=true' + ), + array( + 'http://localhost/?name=' + ), + array( + 'http://localhost/?name' + ), + array( + '' + ), + array( + '/' + ), + array( + '/path' + ), + array( + 'path' + ), + array( + 'http://user@localhost/' + ), + array( + 'http://user:@localhost/' + ), + array( + 'http://:pass@localhost/' + ), + array( + 'http://user:pass@localhost/path?query#fragment' + ), + array( + 'http://user%20name:pass%20word@localhost/path%20name?query%20name#frag%20ment' + ) + ); + } + + /** + * @dataProvider provideValidUris + * @param string $string + */ + public function testToStringReturnsOriginalUriGivenToCtor($string) + { + if (PHP_VERSION_ID < 50519 || (PHP_VERSION_ID < 50603 && PHP_VERSION_ID >= 50606)) { + // @link https://3v4l.org/HdoPG + $this->markTestSkipped('Empty password not supported on legacy PHP'); + } + + $uri = new Uri($string); + + $this->assertEquals($string, (string) $uri); + } + + public static function provideValidUrisThatWillBeTransformed() + { + return array( + array( + 'http://localhost:8080/?', + 'http://localhost:8080/' + ), + array( + 'http://localhost:8080/#', + 'http://localhost:8080/' + ), + array( + 'http://localhost:8080/?#', + 'http://localhost:8080/' + ), + array( + 'http://@localhost:8080/', + 'http://localhost:8080/' + ), + array( + 'http://localhost:8080/?percent=50%', + 'http://localhost:8080/?percent=50%25' + ), + array( + 'http://user name:pass word@localhost/path name?query name#frag ment', + 'http://user%20name:pass%20word@localhost/path%20name?query%20name#frag%20ment' + ), + array( + 'HTTP://USER:PASS@LOCALHOST:8080/PATH?QUERY#FRAGMENT', + 'http://USER:PASS@localhost:8080/PATH?QUERY#FRAGMENT' + ) + ); + } + + /** + * @dataProvider provideValidUrisThatWillBeTransformed + * @param string $string + * @param string $escaped + */ + public function testToStringReturnsTransformedUriFromUriGivenToCtor($string, $escaped = null) + { + $uri = new Uri($string); + + $this->assertEquals($escaped, (string) $uri); + } + + public function testToStringReturnsUriWithPathPrefixedWithSlashWhenPathDoesNotStartWithSlash() + { + $uri = new Uri('http://localhost:8080'); + $uri = $uri->withPath('path'); + + $this->assertEquals('http://localhost:8080/path', (string) $uri); + } + + public function testWithSchemeReturnsNewInstanceWhenSchemeIsChanged() + { + $uri = new Uri('http://localhost'); + + $new = $uri->withScheme('https'); + $this->assertNotSame($uri, $new); + $this->assertEquals('https', $new->getScheme()); + $this->assertEquals('http', $uri->getScheme()); + } + + public function testWithSchemeReturnsNewInstanceWithSchemeToLowerCaseWhenSchemeIsChangedWithUpperCase() + { + $uri = new Uri('http://localhost'); + + $new = $uri->withScheme('HTTPS'); + $this->assertNotSame($uri, $new); + $this->assertEquals('https', $new->getScheme()); + $this->assertEquals('http', $uri->getScheme()); + } + + public function testWithSchemeReturnsNewInstanceWithDefaultPortRemovedWhenSchemeIsChangedToDefaultPortForHttp() + { + $uri = new Uri('https://localhost:80'); + + $new = $uri->withScheme('http'); + $this->assertNotSame($uri, $new); + $this->assertNull($new->getPort()); + $this->assertEquals(80, $uri->getPort()); + } + + public function testWithSchemeReturnsNewInstanceWithDefaultPortRemovedWhenSchemeIsChangedToDefaultPortForHttps() + { + $uri = new Uri('http://localhost:443'); + + $new = $uri->withScheme('https'); + $this->assertNotSame($uri, $new); + $this->assertNull($new->getPort()); + $this->assertEquals(443, $uri->getPort()); + } + + public function testWithSchemeReturnsSameInstanceWhenSchemeIsUnchanged() + { + $uri = new Uri('http://localhost'); + + $new = $uri->withScheme('http'); + $this->assertSame($uri, $new); + $this->assertEquals('http', $uri->getScheme()); + } + + public function testWithSchemeReturnsSameInstanceWhenSchemeToLowerCaseIsUnchanged() + { + $uri = new Uri('http://localhost'); + + $new = $uri->withScheme('HTTP'); + $this->assertSame($uri, $new); + $this->assertEquals('http', $uri->getScheme()); + } + + public function testWithSchemeThrowsWhenSchemeIsInvalid() + { + $uri = new Uri('http://localhost'); + + $this->setExpectedException('InvalidArgumentException'); + $uri->withScheme('invalid+scheme'); + } + + public function testWithUserInfoReturnsNewInstanceWhenUserInfoIsChangedWithNameAndPassword() + { + $uri = new Uri('http://localhost'); + + $new = $uri->withUserInfo('user', 'pass'); + $this->assertNotSame($uri, $new); + $this->assertEquals('user:pass', $new->getUserInfo()); + $this->assertEquals('', $uri->getUserInfo()); + } + + public function testWithUserInfoReturnsNewInstanceWhenUserInfoIsChangedWithNameOnly() + { + $uri = new Uri('http://localhost'); + + $new = $uri->withUserInfo('user'); + $this->assertNotSame($uri, $new); + $this->assertEquals('user', $new->getUserInfo()); + $this->assertEquals('', $uri->getUserInfo()); + } + + public function testWithUserInfoReturnsNewInstanceWhenUserInfoIsChangedWithNameAndEmptyPassword() + { + $uri = new Uri('http://localhost'); + + $new = $uri->withUserInfo('user', ''); + $this->assertNotSame($uri, $new); + $this->assertEquals('user:', $new->getUserInfo()); + $this->assertEquals('', $uri->getUserInfo()); + } + + public function testWithUserInfoReturnsNewInstanceWhenUserInfoIsChangedWithPasswordOnly() + { + $uri = new Uri('http://localhost'); + + $new = $uri->withUserInfo('', 'pass'); + $this->assertNotSame($uri, $new); + $this->assertEquals(':pass', $new->getUserInfo()); + $this->assertEquals('', $uri->getUserInfo()); + } + + public function testWithUserInfoReturnsNewInstanceWhenUserInfoIsChangedWithNameAndPasswordEncoded() + { + $uri = new Uri('http://localhost'); + + $new = $uri->withUserInfo('user:alice', 'pass%20word'); + $this->assertNotSame($uri, $new); + $this->assertEquals('user%3Aalice:pass%20word', $new->getUserInfo()); + $this->assertEquals('', $uri->getUserInfo()); + } + + public function testWithSchemeReturnsSameInstanceWhenSchemeIsUnchangedEmpty() + { + $uri = new Uri('http://localhost'); + + $new = $uri->withUserInfo(''); + $this->assertSame($uri, $new); + $this->assertEquals('', $uri->getUserInfo()); + } + + public function testWithSchemeReturnsSameInstanceWhenSchemeIsUnchangedWithNameAndPassword() + { + $uri = new Uri('http://user:pass@localhost'); + + $new = $uri->withUserInfo('user', 'pass'); + $this->assertSame($uri, $new); + $this->assertEquals('user:pass', $uri->getUserInfo()); + } + + public function testWithHostReturnsNewInstanceWhenHostIsChanged() + { + $uri = new Uri('http://localhost'); + + $new = $uri->withHost('example.com'); + $this->assertNotSame($uri, $new); + $this->assertEquals('example.com', $new->getHost()); + $this->assertEquals('localhost', $uri->getHost()); + } + + public function testWithHostReturnsNewInstanceWithHostToLowerCaseWhenHostIsChangedWithUpperCase() + { + $uri = new Uri('http://localhost'); + + $new = $uri->withHost('EXAMPLE.COM'); + $this->assertNotSame($uri, $new); + $this->assertEquals('example.com', $new->getHost()); + $this->assertEquals('localhost', $uri->getHost()); + } + + public function testWithHostReturnsNewInstanceWhenHostIsChangedToEmptyString() + { + $uri = new Uri('http://localhost'); + + $new = $uri->withHost(''); + $this->assertNotSame($uri, $new); + $this->assertEquals('', $new->getHost()); + $this->assertEquals('localhost', $uri->getHost()); + } + + public function testWithHostReturnsSameInstanceWhenHostIsUnchanged() + { + $uri = new Uri('http://localhost'); + + $new = $uri->withHost('localhost'); + $this->assertSame($uri, $new); + $this->assertEquals('localhost', $uri->getHost()); + } + + public function testWithHostReturnsSameInstanceWhenHostToLowerCaseIsUnchanged() + { + $uri = new Uri('http://localhost'); + + $new = $uri->withHost('LOCALHOST'); + $this->assertSame($uri, $new); + $this->assertEquals('localhost', $uri->getHost()); + } + + public function testWithHostThrowsWhenHostIsInvalidWithPlus() + { + $uri = new Uri('http://localhost'); + + $this->setExpectedException('InvalidArgumentException'); + $uri->withHost('invalid+host'); + } + + public function testWithHostThrowsWhenHostIsInvalidWithSpace() + { + $uri = new Uri('http://localhost'); + + $this->setExpectedException('InvalidArgumentException'); + $uri->withHost('invalid host'); + } + + public function testWithPortReturnsNewInstanceWhenPortIsChanged() + { + $uri = new Uri('http://localhost'); + + $new = $uri->withPort(8080); + $this->assertNotSame($uri, $new); + $this->assertEquals(8080, $new->getPort()); + $this->assertNull($uri->getPort()); + } + + public function testWithPortReturnsNewInstanceWithDefaultPortRemovedWhenPortIsChangedToDefaultPortForHttp() + { + $uri = new Uri('http://localhost:8080'); + + $new = $uri->withPort(80); + $this->assertNotSame($uri, $new); + $this->assertNull($new->getPort()); + $this->assertEquals(8080, $uri->getPort()); + } + + public function testWithPortReturnsNewInstanceWithDefaultPortRemovedWhenPortIsChangedToDefaultPortForHttps() + { + $uri = new Uri('https://localhost:8080'); + + $new = $uri->withPort(443); + $this->assertNotSame($uri, $new); + $this->assertNull($new->getPort()); + $this->assertEquals(8080, $uri->getPort()); + } + + public function testWithPortReturnsSameInstanceWhenPortIsUnchanged() + { + $uri = new Uri('http://localhost:8080'); + + $new = $uri->withPort(8080); + $this->assertSame($uri, $new); + $this->assertEquals(8080, $uri->getPort()); + } + + public function testWithPortReturnsSameInstanceWhenPortIsUnchangedDefaultPortForHttp() + { + $uri = new Uri('http://localhost'); + + $new = $uri->withPort(80); + $this->assertSame($uri, $new); + $this->assertNull($uri->getPort()); + } + + public function testWithPortReturnsSameInstanceWhenPortIsUnchangedDefaultPortForHttps() + { + $uri = new Uri('https://localhost'); + + $new = $uri->withPort(443); + $this->assertSame($uri, $new); + $this->assertNull($uri->getPort()); + } + + public function testWithPortThrowsWhenPortIsInvalidUnderflow() + { + $uri = new Uri('http://localhost'); + + $this->setExpectedException('InvalidArgumentException'); + $uri->withPort(0); + } + + public function testWithPortThrowsWhenPortIsInvalidOverflow() + { + $uri = new Uri('http://localhost'); + + $this->setExpectedException('InvalidArgumentException'); + $uri->withPort(65536); + } + + public function testWithPathReturnsNewInstanceWhenPathIsChanged() + { + $uri = new Uri('http://localhost/'); + + $new = $uri->withPath('/path'); + $this->assertNotSame($uri, $new); + $this->assertEquals('/path', $new->getPath()); + $this->assertEquals('/', $uri->getPath()); + } + + public function testWithPathReturnsNewInstanceWhenPathIsChangedEncoded() + { + $uri = new Uri('http://localhost/'); + + $new = $uri->withPath('/a new/path%20here!'); + $this->assertNotSame($uri, $new); + $this->assertEquals('/a%20new/path%20here!', $new->getPath()); + $this->assertEquals('/', $uri->getPath()); + } + + public function testWithPathReturnsSameInstanceWhenPathIsUnchanged() + { + $uri = new Uri('http://localhost/path'); + + $new = $uri->withPath('/path'); + $this->assertSame($uri, $new); + $this->assertEquals('/path', $uri->getPath()); + } + + public function testWithPathReturnsSameInstanceWhenPathIsUnchangedEncoded() + { + $uri = new Uri('http://localhost/a%20new/path%20here!'); + + $new = $uri->withPath('/a new/path%20here!'); + $this->assertSame($uri, $new); + $this->assertEquals('/a%20new/path%20here!', $uri->getPath()); + } + + public function testWithQueryReturnsNewInstanceWhenQueryIsChanged() + { + $uri = new Uri('http://localhost'); + + $new = $uri->withQuery('foo=bar'); + $this->assertNotSame($uri, $new); + $this->assertEquals('foo=bar', $new->getQuery()); + $this->assertEquals('', $uri->getQuery()); + } + + public function testWithQueryReturnsNewInstanceWhenQueryIsChangedEncoded() + { + $uri = new Uri('http://localhost'); + + $new = $uri->withQuery('foo=a new%20text!'); + $this->assertNotSame($uri, $new); + $this->assertEquals('foo=a%20new%20text!', $new->getQuery()); + $this->assertEquals('', $uri->getQuery()); + } + + public function testWithQueryReturnsSameInstanceWhenQueryIsUnchanged() + { + $uri = new Uri('http://localhost?foo=bar'); + + $new = $uri->withQuery('foo=bar'); + $this->assertSame($uri, $new); + $this->assertEquals('foo=bar', $uri->getQuery()); + } + + public function testWithQueryReturnsSameInstanceWhenQueryIsUnchangedEncoded() + { + $uri = new Uri('http://localhost?foo=a%20new%20text!'); + + $new = $uri->withQuery('foo=a new%20text!'); + $this->assertSame($uri, $new); + $this->assertEquals('foo=a%20new%20text!', $uri->getQuery()); + } + + public function testWithFragmentReturnsNewInstanceWhenFragmentIsChanged() + { + $uri = new Uri('http://localhost'); + + $new = $uri->withFragment('section'); + $this->assertNotSame($uri, $new); + $this->assertEquals('section', $new->getFragment()); + $this->assertEquals('', $uri->getFragment()); + } + + public function testWithFragmentReturnsNewInstanceWhenFragmentIsChangedEncoded() + { + $uri = new Uri('http://localhost'); + + $new = $uri->withFragment('section new%20text!'); + $this->assertNotSame($uri, $new); + $this->assertEquals('section%20new%20text!', $new->getFragment()); + $this->assertEquals('', $uri->getFragment()); + } + + public function testWithFragmentReturnsSameInstanceWhenFragmentIsUnchanged() + { + $uri = new Uri('http://localhost#section'); + + $new = $uri->withFragment('section'); + $this->assertSame($uri, $new); + $this->assertEquals('section', $uri->getFragment()); + } + + public function testWithFragmentReturnsSameInstanceWhenFragmentIsUnchangedEncoded() + { + $uri = new Uri('http://localhost#section%20new%20text!'); + + $new = $uri->withFragment('section new%20text!'); + $this->assertSame($uri, $new); + $this->assertEquals('section%20new%20text!', $uri->getFragment()); + } +} From c6caa1240307f13e7a678332ad0beae7fb909160 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 23 Mar 2024 17:15:23 +0100 Subject: [PATCH 443/456] Add internal `Uri::resolve()` to resolve URIs relative to base URI --- src/Browser.php | 4 +- src/Io/Transaction.php | 4 +- src/Message/Uri.php | 64 ++++++++++++++++++++ tests/Message/UriTest.php | 124 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 192 insertions(+), 4 deletions(-) diff --git a/src/Browser.php b/src/Browser.php index b7bf4425..01a266ca 100644 --- a/src/Browser.php +++ b/src/Browser.php @@ -3,12 +3,12 @@ namespace React\Http; use Psr\Http\Message\ResponseInterface; -use RingCentral\Psr7\Uri; use React\EventLoop\Loop; use React\EventLoop\LoopInterface; use React\Http\Io\Sender; use React\Http\Io\Transaction; use React\Http\Message\Request; +use React\Http\Message\Uri; use React\Promise\PromiseInterface; use React\Socket\ConnectorInterface; use React\Stream\ReadableStreamInterface; @@ -834,7 +834,7 @@ private function requestMayBeStreaming($method, $url, array $headers = array(), { if ($this->baseUrl !== null) { // ensure we're actually below the base URL - $url = Uri::resolve($this->baseUrl, $url); + $url = Uri::resolve($this->baseUrl, new Uri($url)); } foreach ($this->defaultHeaders as $key => $value) { diff --git a/src/Io/Transaction.php b/src/Io/Transaction.php index b93c490c..64738f56 100644 --- a/src/Io/Transaction.php +++ b/src/Io/Transaction.php @@ -8,11 +8,11 @@ use React\EventLoop\LoopInterface; use React\Http\Message\Response; use React\Http\Message\ResponseException; +use React\Http\Message\Uri; use React\Promise\Deferred; use React\Promise\Promise; use React\Promise\PromiseInterface; use React\Stream\ReadableStreamInterface; -use RingCentral\Psr7\Uri; /** * @internal @@ -264,7 +264,7 @@ public function onResponse(ResponseInterface $response, RequestInterface $reques private function onResponseRedirect(ResponseInterface $response, RequestInterface $request, Deferred $deferred, ClientRequestState $state) { // resolve location relative to last request URI - $location = Uri::resolve($request->getUri(), $response->getHeaderLine('Location')); + $location = Uri::resolve($request->getUri(), new Uri($response->getHeaderLine('Location'))); $request = $this->makeRedirectRequest($request, $location, $response->getStatusCode()); $this->progress('redirect', array($request)); diff --git a/src/Message/Uri.php b/src/Message/Uri.php index f2cf7d99..4309bbed 100644 --- a/src/Message/Uri.php +++ b/src/Message/Uri.php @@ -289,4 +289,68 @@ function (array $match) { $part ); } + + /** + * [Internal] Resolve URI relative to base URI and return new absolute URI + * + * @internal + * @param UriInterface $base + * @param UriInterface $rel + * @return UriInterface + * @throws void + */ + public static function resolve(UriInterface $base, UriInterface $rel) + { + if ($rel->getScheme() !== '') { + return $rel->getPath() === '' ? $rel : $rel->withPath(self::removeDotSegments($rel->getPath())); + } + + $reset = false; + $new = $base; + if ($rel->getAuthority() !== '') { + $reset = true; + $userInfo = \explode(':', $rel->getUserInfo(), 2); + $new = $base->withUserInfo($userInfo[0], isset($userInfo[1]) ? $userInfo[1]: null)->withHost($rel->getHost())->withPort($rel->getPort()); + } + + if ($reset && $rel->getPath() === '') { + $new = $new->withPath(''); + } elseif (($path = $rel->getPath()) !== '') { + $start = ''; + if ($path === '' || $path[0] !== '/') { + $start = $base->getPath(); + if (\substr($start, -1) !== '/') { + $start .= '/../'; + } + } + $reset = true; + $new = $new->withPath(self::removeDotSegments($start . $path)); + } + if ($reset || $rel->getQuery() !== '') { + $reset = true; + $new = $new->withQuery($rel->getQuery()); + } + if ($reset || $rel->getFragment() !== '') { + $new = $new->withFragment($rel->getFragment()); + } + + return $new; + } + + /** + * @param string $path + * @return string + */ + private static function removeDotSegments($path) + { + $segments = array(); + foreach (\explode('/', $path) as $segment) { + if ($segment === '..') { + \array_pop($segments); + } elseif ($segment !== '.' && $segment !== '') { + $segments[] = $segment; + } + } + return '/' . \implode('/', $segments) . ($path !== '/' && \substr($path, -1) === '/' ? '/' : ''); + } } diff --git a/tests/Message/UriTest.php b/tests/Message/UriTest.php index 95c7fa4e..05eec723 100644 --- a/tests/Message/UriTest.php +++ b/tests/Message/UriTest.php @@ -578,4 +578,128 @@ public function testWithFragmentReturnsSameInstanceWhenFragmentIsUnchangedEncode $this->assertSame($uri, $new); $this->assertEquals('section%20new%20text!', $uri->getFragment()); } + + public static function provideResolveUris() + { + return array( + array( + 'http://localhost/', + '', + 'http://localhost/' + ), + array( + 'http://localhost/', + 'http://example.com/', + 'http://example.com/' + ), + array( + 'http://localhost/', + 'path', + 'http://localhost/path' + ), + array( + 'http://localhost/', + 'path/', + 'http://localhost/path/' + ), + array( + 'http://localhost/', + 'path//', + 'http://localhost/path/' + ), + array( + 'http://localhost', + 'path', + 'http://localhost/path' + ), + array( + 'http://localhost/a/b', + '/path', + 'http://localhost/path' + ), + array( + 'http://localhost/', + '/a/b/c', + 'http://localhost/a/b/c' + ), + array( + 'http://localhost/a/path', + 'b/c', + 'http://localhost/a/b/c' + ), + array( + 'http://localhost/a/path', + '/b/c', + 'http://localhost/b/c' + ), + array( + 'http://localhost/a/path/', + 'b/c', + 'http://localhost/a/path/b/c' + ), + array( + 'http://localhost/a/path/', + '../b/c', + 'http://localhost/a/b/c' + ), + array( + 'http://localhost', + '../../../a/b', + 'http://localhost/a/b' + ), + array( + 'http://localhost/path', + '?query', + 'http://localhost/path?query' + ), + array( + 'http://localhost/path', + '#fragment', + 'http://localhost/path#fragment' + ), + array( + 'http://localhost/path', + 'http://localhost', + 'http://localhost' + ), + array( + 'http://localhost/path', + 'http://localhost/?query#fragment', + 'http://localhost/?query#fragment' + ), + array( + 'http://localhost/path/?a#fragment', + '?b', + 'http://localhost/path/?b' + ), + array( + 'http://localhost/path', + '//localhost', + 'http://localhost' + ), + array( + 'http://localhost/path', + '//localhost/a?query', + 'http://localhost/a?query' + ), + array( + 'http://localhost/path', + '//LOCALHOST', + 'http://localhost' + ) + ); + } + + /** + * @dataProvider provideResolveUris + * @param string $base + * @param string $rel + * @param string $expected + */ + public function testResolveReturnsResolvedUri($base, $rel, $expected) + { + $uri = Uri::resolve(new Uri($base), new Uri($rel)); + + $this->assertEquals($expected, (string) $uri); + } } From 30c802c9db77d90dc0d6ef43d9a60573863a0969 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 23 Mar 2024 17:42:14 +0100 Subject: [PATCH 444/456] Drop leftover RingCentral PSR-7 dependency, use own PSR-7 implementation --- composer.json | 3 +-- examples/01-client-get-request.php | 2 +- examples/02-client-concurrent-requests.php | 6 +++--- examples/04-client-post-json.php | 2 +- examples/05-client-put-xml.php | 2 +- examples/11-client-http-proxy.php | 2 +- examples/12-client-socks-proxy.php | 2 +- examples/13-client-ssh-proxy.php | 2 +- examples/14-client-unix-domain-sockets.php | 3 +-- examples/21-client-request-streaming-to-stdout.php | 3 +-- examples/22-client-stream-upload-from-stdin.php | 3 +-- examples/71-server-http-proxy.php | 2 +- examples/91-client-benchmark-download.php | 3 +-- src/Io/ClientRequestStream.php | 3 ++- tests/Middleware/RequestBodyBufferMiddlewareTest.php | 5 ++--- 15 files changed, 19 insertions(+), 24 deletions(-) diff --git a/composer.json b/composer.json index 5198470e..23783c0c 100644 --- a/composer.json +++ b/composer.json @@ -33,8 +33,7 @@ "react/event-loop": "^1.2", "react/promise": "^3 || ^2.3 || ^1.2.1", "react/socket": "^1.12", - "react/stream": "^1.2", - "ringcentral/psr7": "^1.2" + "react/stream": "^1.2" }, "require-dev": { "clue/http-proxy-react": "^1.8", diff --git a/examples/01-client-get-request.php b/examples/01-client-get-request.php index 34a79bbb..278f6597 100644 --- a/examples/01-client-get-request.php +++ b/examples/01-client-get-request.php @@ -8,7 +8,7 @@ $client = new Browser(); $client->get('http://google.com/')->then(function (ResponseInterface $response) { - var_dump($response->getHeaders(), (string)$response->getBody()); + echo (string) $response->getBody(); }, function (Exception $e) { echo 'Error: ' . $e->getMessage() . PHP_EOL; }); diff --git a/examples/02-client-concurrent-requests.php b/examples/02-client-concurrent-requests.php index 7b1b77a0..e372515c 100644 --- a/examples/02-client-concurrent-requests.php +++ b/examples/02-client-concurrent-requests.php @@ -8,19 +8,19 @@ $client = new Browser(); $client->head('http://www.github.com/clue/http-react')->then(function (ResponseInterface $response) { - var_dump($response->getHeaders(), (string)$response->getBody()); + echo (string) $response->getBody(); }, function (Exception $e) { echo 'Error: ' . $e->getMessage() . PHP_EOL; }); $client->get('http://google.com/')->then(function (ResponseInterface $response) { - var_dump($response->getHeaders(), (string)$response->getBody()); + echo (string) $response->getBody(); }, function (Exception $e) { echo 'Error: ' . $e->getMessage() . PHP_EOL; }); $client->get('http://www.lueck.tv/psocksd')->then(function (ResponseInterface $response) { - var_dump($response->getHeaders(), (string)$response->getBody()); + echo (string) $response->getBody(); }, function (Exception $e) { echo 'Error: ' . $e->getMessage() . PHP_EOL; }); diff --git a/examples/04-client-post-json.php b/examples/04-client-post-json.php index 477c3426..18fa596d 100644 --- a/examples/04-client-post-json.php +++ b/examples/04-client-post-json.php @@ -22,7 +22,7 @@ ), json_encode($data) )->then(function (ResponseInterface $response) { - echo (string)$response->getBody(); + echo (string) $response->getBody(); }, function (Exception $e) { echo 'Error: ' . $e->getMessage() . PHP_EOL; }); diff --git a/examples/05-client-put-xml.php b/examples/05-client-put-xml.php index 6055363a..10ee46fc 100644 --- a/examples/05-client-put-xml.php +++ b/examples/05-client-put-xml.php @@ -19,7 +19,7 @@ ), $xml->asXML() )->then(function (ResponseInterface $response) { - echo (string)$response->getBody(); + echo (string) $response->getBody(); }, function (Exception $e) { echo 'Error: ' . $e->getMessage() . PHP_EOL; }); diff --git a/examples/11-client-http-proxy.php b/examples/11-client-http-proxy.php index f450fbc2..ec7fc2b6 100644 --- a/examples/11-client-http-proxy.php +++ b/examples/11-client-http-proxy.php @@ -25,7 +25,7 @@ // demo fetching HTTP headers (or bail out otherwise) $browser->get('https://www.google.com/')->then(function (ResponseInterface $response) { - echo RingCentral\Psr7\str($response); + echo (string) $response->getBody(); }, function (Exception $e) { echo 'Error: ' . $e->getMessage() . PHP_EOL; }); diff --git a/examples/12-client-socks-proxy.php b/examples/12-client-socks-proxy.php index ecedf242..8c525509 100644 --- a/examples/12-client-socks-proxy.php +++ b/examples/12-client-socks-proxy.php @@ -25,7 +25,7 @@ // demo fetching HTTP headers (or bail out otherwise) $browser->get('https://www.google.com/')->then(function (ResponseInterface $response) { - echo RingCentral\Psr7\str($response); + echo (string) $response->getBody(); }, function (Exception $e) { echo 'Error: ' . $e->getMessage() . PHP_EOL; }); diff --git a/examples/13-client-ssh-proxy.php b/examples/13-client-ssh-proxy.php index 64d0c282..93e6e256 100644 --- a/examples/13-client-ssh-proxy.php +++ b/examples/13-client-ssh-proxy.php @@ -21,7 +21,7 @@ // demo fetching HTTP headers (or bail out otherwise) $browser->get('https://www.google.com/')->then(function (ResponseInterface $response) { - echo RingCentral\Psr7\str($response); + echo (string) $response->getBody(); }, function (Exception $e) { echo 'Error: ' . $e->getMessage() . PHP_EOL; }); diff --git a/examples/14-client-unix-domain-sockets.php b/examples/14-client-unix-domain-sockets.php index e9718141..5af0394d 100644 --- a/examples/14-client-unix-domain-sockets.php +++ b/examples/14-client-unix-domain-sockets.php @@ -4,7 +4,6 @@ use React\Http\Browser; use React\Socket\FixedUriConnector; use React\Socket\UnixConnector; -use RingCentral\Psr7; require __DIR__ . '/../vendor/autoload.php'; @@ -18,7 +17,7 @@ // demo fetching HTTP headers (or bail out otherwise) $browser->get('http://localhost/info')->then(function (ResponseInterface $response) { - echo Psr7\str($response); + echo (string) $response->getBody(); }, function (Exception $e) { echo 'Error: ' . $e->getMessage() . PHP_EOL; }); diff --git a/examples/21-client-request-streaming-to-stdout.php b/examples/21-client-request-streaming-to-stdout.php index 2f24d035..b3cbbe39 100644 --- a/examples/21-client-request-streaming-to-stdout.php +++ b/examples/21-client-request-streaming-to-stdout.php @@ -4,7 +4,6 @@ use Psr\Http\Message\ResponseInterface; use React\Stream\ReadableStreamInterface; use React\Stream\WritableResourceStream; -use RingCentral\Psr7; require __DIR__ . '/../vendor/autoload.php'; @@ -22,7 +21,7 @@ $info->write('Requesting ' . $url . '…' . PHP_EOL); $client->requestStreaming('GET', $url)->then(function (ResponseInterface $response) use ($info, $out) { - $info->write('Received' . PHP_EOL . Psr7\str($response)); + $info->write('Received ' . $response->getStatusCode() . ' ' . $response->getReasonPhrase() . PHP_EOL); $body = $response->getBody(); assert($body instanceof ReadableStreamInterface); diff --git a/examples/22-client-stream-upload-from-stdin.php b/examples/22-client-stream-upload-from-stdin.php index f29b08ab..f0a68c5f 100644 --- a/examples/22-client-stream-upload-from-stdin.php +++ b/examples/22-client-stream-upload-from-stdin.php @@ -3,7 +3,6 @@ use Psr\Http\Message\ResponseInterface; use React\Http\Browser; use React\Stream\ReadableResourceStream; -use RingCentral\Psr7; require __DIR__ . '/../vendor/autoload.php'; @@ -20,7 +19,7 @@ echo 'Sending STDIN as POST to ' . $url . '…' . PHP_EOL; $client->post($url, array('Content-Type' => 'text/plain'), $in)->then(function (ResponseInterface $response) { - echo 'Received' . PHP_EOL . Psr7\str($response); + echo (string) $response->getBody(); }, function (Exception $e) { echo 'Error: ' . $e->getMessage() . PHP_EOL; }); diff --git a/examples/71-server-http-proxy.php b/examples/71-server-http-proxy.php index cf63c4ae..de9fa10b 100644 --- a/examples/71-server-http-proxy.php +++ b/examples/71-server-http-proxy.php @@ -28,7 +28,7 @@ // left up as an exercise: use an HTTP client to send the outgoing request // and forward the incoming response to the original client request return React\Http\Message\Response::plaintext( - RingCentral\Psr7\str($outgoing) + $outgoing->getMethod() . ' ' . $outgoing->getRequestTarget() . ' HTTP/' . $outgoing->getProtocolVersion() . "\r\n\r\n" . (string) $outgoing->getBody() ); }); diff --git a/examples/91-client-benchmark-download.php b/examples/91-client-benchmark-download.php index 44e99087..712d9f10 100644 --- a/examples/91-client-benchmark-download.php +++ b/examples/91-client-benchmark-download.php @@ -29,8 +29,7 @@ echo 'Requesting ' . $url . '…' . PHP_EOL; $client->requestStreaming('GET', $url)->then(function (ResponseInterface $response) { - echo 'Headers received' . PHP_EOL; - echo RingCentral\Psr7\str($response); + echo 'Received ' . $response->getStatusCode() . ' ' . $response->getReasonPhrase() . PHP_EOL; $stream = $response->getBody(); assert($stream instanceof ReadableStreamInterface); diff --git a/src/Io/ClientRequestStream.php b/src/Io/ClientRequestStream.php index 25c96ea8..12c15caf 100644 --- a/src/Io/ClientRequestStream.php +++ b/src/Io/ClientRequestStream.php @@ -277,7 +277,8 @@ public function close() */ public function hasMessageKeepAliveEnabled(MessageInterface $message) { - $connectionOptions = \RingCentral\Psr7\normalize_header(\strtolower($message->getHeaderLine('Connection'))); + // @link https://www.rfc-editor.org/rfc/rfc9110#section-7.6.1 + $connectionOptions = \array_map('trim', \explode(',', \strtolower($message->getHeaderLine('Connection')))); if (\in_array('close', $connectionOptions, true)) { return false; diff --git a/tests/Middleware/RequestBodyBufferMiddlewareTest.php b/tests/Middleware/RequestBodyBufferMiddlewareTest.php index fd818a8c..40c23378 100644 --- a/tests/Middleware/RequestBodyBufferMiddlewareTest.php +++ b/tests/Middleware/RequestBodyBufferMiddlewareTest.php @@ -4,13 +4,13 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Loop; +use React\Http\Io\BufferedBody; use React\Http\Io\HttpBodyStream; use React\Http\Message\Response; use React\Http\Message\ServerRequest; use React\Http\Middleware\RequestBodyBufferMiddleware; use React\Stream\ThroughStream; use React\Tests\Http\TestCase; -use RingCentral\Psr7\BufferStream; final class RequestBodyBufferMiddlewareTest extends TestCase { @@ -45,8 +45,7 @@ public function testAlreadyBufferedResolvesImmediately() { $size = 1024; $body = str_repeat('x', $size); - $stream = new BufferStream(1024); - $stream->write($body); + $stream = new BufferedBody($body); $serverRequest = new ServerRequest( 'GET', 'https://example.com/', From 27d2e74c0626acb0f6f504d8ba25632036b79818 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Wed, 27 Mar 2024 10:48:52 +0100 Subject: [PATCH 445/456] Validate outgoing HTTP message headers and reject invalid messages --- src/Io/AbstractMessage.php | 2 +- src/Io/ClientRequestStream.php | 30 ++++++---- src/Io/StreamingServer.php | 13 ++++ tests/Io/ClientRequestStreamTest.php | 62 ++++++++++++++++++++ tests/Io/StreamingServerTest.php | 88 +++++++++++++++++++++++++++- 5 files changed, 183 insertions(+), 12 deletions(-) diff --git a/src/Io/AbstractMessage.php b/src/Io/AbstractMessage.php index ab023304..a0706bb1 100644 --- a/src/Io/AbstractMessage.php +++ b/src/Io/AbstractMessage.php @@ -19,7 +19,7 @@ abstract class AbstractMessage implements MessageInterface * @internal * @var string */ - const REGEX_HEADERS = '/^([^()<>@,;:\\\"\/\[\]?={}\x01-\x20\x7F]++):[\x20\x09]*+((?:[\x20\x09]*+[\x21-\x7E\x80-\xFF]++)*+)[\x20\x09]*+[\r]?+\n/m'; + const REGEX_HEADERS = '/^([^()<>@,;:\\\"\/\[\]?={}\x00-\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 25c96ea8..ee0ec760 100644 --- a/src/Io/ClientRequestStream.php +++ b/src/Io/ClientRequestStream.php @@ -56,7 +56,25 @@ private function writeHead() { $this->state = self::STATE_WRITING_HEAD; - $request = $this->request; + $expected = 0; + $headers = "{$this->request->getMethod()} {$this->request->getRequestTarget()} HTTP/{$this->request->getProtocolVersion()}\r\n"; + foreach ($this->request->getHeaders() as $name => $values) { + if (\strpos($name, ':') !== false) { + $expected = -1; + break; + } + foreach ($values as $value) { + $headers .= "$name: $value\r\n"; + ++$expected; + } + } + + /** @var array $m legacy PHP 5.3 only */ + if (!\preg_match('#^\S+ \S+ HTTP/1\.[01]\r\n#m', $headers) || \substr_count($headers, "\n") !== ($expected + 1) || (\PHP_VERSION_ID >= 50400 ? \preg_match_all(AbstractMessage::REGEX_HEADERS, $headers) : \preg_match_all(AbstractMessage::REGEX_HEADERS, $headers, $m)) !== $expected) { + $this->closeError(new \InvalidArgumentException('Unable to send request with invalid request headers')); + return; + } + $connectionRef = &$this->connection; $stateRef = &$this->state; $pendingWrites = &$this->pendingWrites; @@ -64,7 +82,7 @@ private function writeHead() $promise = $this->connectionManager->connect($this->request->getUri()); $promise->then( - function (ConnectionInterface $connection) use ($request, &$connectionRef, &$stateRef, &$pendingWrites, $that) { + function (ConnectionInterface $connection) use ($headers, &$connectionRef, &$stateRef, &$pendingWrites, $that) { $connectionRef = $connection; assert($connectionRef instanceof ConnectionInterface); @@ -74,14 +92,6 @@ function (ConnectionInterface $connection) use ($request, &$connectionRef, &$sta $connection->on('error', array($that, 'handleError')); $connection->on('close', array($that, 'close')); - assert($request instanceof RequestInterface); - $headers = "{$request->getMethod()} {$request->getRequestTarget()} HTTP/{$request->getProtocolVersion()}\r\n"; - foreach ($request->getHeaders() as $name => $values) { - foreach ($values as $value) { - $headers .= "$name: $value\r\n"; - } - } - $more = $connection->write($headers . "\r\n" . $pendingWrites); assert($stateRef === ClientRequestStream::STATE_WRITING_HEAD); diff --git a/src/Io/StreamingServer.php b/src/Io/StreamingServer.php index 790c8cc1..143edaa8 100644 --- a/src/Io/StreamingServer.php +++ b/src/Io/StreamingServer.php @@ -333,13 +333,26 @@ public function handleResponse(ConnectionInterface $connection, ServerRequestInt } // build HTTP response header by appending status line and header fields + $expected = 0; $headers = "HTTP/" . $version . " " . $code . " " . $response->getReasonPhrase() . "\r\n"; foreach ($response->getHeaders() as $name => $values) { + if (\strpos($name, ':') !== false) { + $expected = -1; + break; + } foreach ($values as $value) { $headers .= $name . ": " . $value . "\r\n"; + ++$expected; } } + /** @var array $m legacy PHP 5.3 only */ + if ($code < 100 || $code > 999 || \substr_count($headers, "\n") !== ($expected + 1) || (\PHP_VERSION_ID >= 50400 ? \preg_match_all(AbstractMessage::REGEX_HEADERS, $headers) : \preg_match_all(AbstractMessage::REGEX_HEADERS, $headers, $m)) !== $expected) { + $this->emit('error', array(new \InvalidArgumentException('Unable to send response with invalid response headers'))); + $this->writeError($connection, Response::STATUS_INTERNAL_SERVER_ERROR, $request); + return; + } + // response to HEAD and 1xx, 204 and 304 responses MUST NOT include a body // exclude status 101 (Switching Protocols) here for Upgrade request handling above if ($method === 'HEAD' || ($code >= 100 && $code < 200 && $code !== Response::STATUS_SWITCHING_PROTOCOLS) || $code === Response::STATUS_NO_CONTENT || $code === Response::STATUS_NOT_MODIFIED) { diff --git a/tests/Io/ClientRequestStreamTest.php b/tests/Io/ClientRequestStreamTest.php index 181db173..9a5373a1 100644 --- a/tests/Io/ClientRequestStreamTest.php +++ b/tests/Io/ClientRequestStreamTest.php @@ -2,6 +2,7 @@ namespace React\Tests\Http\Io; +use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use React\Http\Io\ClientRequestStream; use React\Http\Message\Request; @@ -100,6 +101,67 @@ public function requestShouldEmitErrorIfConnectionEmitsError() $request->handleError(new \Exception('test')); } + public static function provideInvalidRequest() + { + $request = new Request('GET' , "http://localhost/"); + + return array( + array( + $request->withMethod("INVA\r\nLID", '') + ), + array( + $request->withRequestTarget('/inva lid') + ), + array( + $request->withHeader('Invalid', "Yes\r\n") + ), + array( + $request->withHeader('Invalid', "Yes\n") + ), + array( + $request->withHeader('Invalid', "Yes\r") + ), + array( + $request->withHeader("Inva\r\nlid", 'Yes') + ), + array( + $request->withHeader("Inva\nlid", 'Yes') + ), + array( + $request->withHeader("Inva\rlid", 'Yes') + ), + array( + $request->withHeader('Inva Lid', 'Yes') + ), + array( + $request->withHeader('Inva:Lid', 'Yes') + ), + array( + $request->withHeader('Invalid', "Val\0ue") + ), + array( + $request->withHeader("Inva\0lid", 'Yes') + ) + ); + } + + /** + * @dataProvider provideInvalidRequest + * @param RequestInterface $request + */ + public function testStreamShouldEmitErrorBeforeCreatingConnectionWhenRequestIsInvalid(RequestInterface $request) + { + $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager->expects($this->never())->method('connect'); + + $stream = new ClientRequestStream($connectionManager, $request); + + $stream->on('error', $this->expectCallableOnceWith($this->isInstanceOf('InvalidArgumentException'))); + $stream->on('close', $this->expectCallableOnce()); + + $stream->end(); + } + /** @test */ public function requestShouldEmitErrorIfRequestParserThrowsException() { diff --git a/tests/Io/StreamingServerTest.php b/tests/Io/StreamingServerTest.php index afab371e..b4e3f2f8 100644 --- a/tests/Io/StreamingServerTest.php +++ b/tests/Io/StreamingServerTest.php @@ -2,6 +2,7 @@ namespace React\Tests\Http\Io; +use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Loop; use React\Http\Io\StreamingServer; @@ -2511,7 +2512,7 @@ function ($data) use (&$buffer) { public function testInvalidCallbackFunctionLeadsToException() { $this->setExpectedException('InvalidArgumentException'); - $server = new StreamingServer(Loop::get(), 'invalid'); + new StreamingServer(Loop::get(), 'invalid'); } public function testResponseBodyStreamWillStreamDataWithChunkedTransferEncoding() @@ -2926,6 +2927,91 @@ function ($data) use (&$buffer) { $this->assertInstanceOf('RuntimeException', $exception); } + public static function provideInvalidResponse() + { + $response = new Response(200, array(), '', '1.1', 'OK'); + + return array( + array( + $response->withStatus(99, 'OK') + ), + array( + $response->withStatus(1000, 'OK') + ), + array( + $response->withStatus(200, "Invald\r\nReason: Yes") + ), + array( + $response->withHeader('Invalid', "Yes\r\n") + ), + array( + $response->withHeader('Invalid', "Yes\n") + ), + array( + $response->withHeader('Invalid', "Yes\r") + ), + array( + $response->withHeader("Inva\r\nlid", 'Yes') + ), + array( + $response->withHeader("Inva\nlid", 'Yes') + ), + array( + $response->withHeader("Inva\rlid", 'Yes') + ), + array( + $response->withHeader('Inva Lid', 'Yes') + ), + array( + $response->withHeader('Inva:Lid', 'Yes') + ), + array( + $response->withHeader('Invalid', "Val\0ue") + ), + array( + $response->withHeader("Inva\0lid", 'Yes') + ) + ); + } + + /** + * @dataProvider provideInvalidResponse + * @param ResponseInterface $response + */ + public function testInvalidResponseObjectWillResultInErrorMessage(ResponseInterface $response) + { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($response) { + return $response; + }); + + $exception = null; + $server->on('error', function (\Exception $ex) use (&$exception) { + $exception = $ex; + }); + + $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 = $this->createGetRequest(); + + $this->connection->emit('data', array($data)); + + $this->assertContainsString("HTTP/1.1 500 Internal Server Error\r\n", $buffer); + $this->assertInstanceOf('InvalidArgumentException', $exception); + } + public function testRequestServerRequestParams() { $requestValidation = null; From 8111281ee57f22b7194f5dba225e609ba7ce4d20 Mon Sep 17 00:00:00 2001 From: Simon Frings Date: Wed, 27 Mar 2024 18:20:46 +0100 Subject: [PATCH 446/456] Prepare v1.10.0 release --- CHANGELOG.md | 32 ++++++++++++++++++++++++++++++++ README.md | 2 +- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d19639d8..f69779c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,37 @@ # Changelog +## 1.10.0 (2024-03-27) + +* Feature: Add new PSR-7 implementation and remove dated RingCentral PSR-7 dependency. + (#518, #519, #520 and #522 by @clue) + + This changeset allows us to maintain our own PSR-7 implementation and reduce + dependencies on external projects. It also improves performance slightly and + does not otherwise affect our public API. If you want to explicitly install + the old RingCentral PSR-7 dependency, you can still install it like this: + + ```bash + composer require ringcentral/psr7 + ``` + +* Feature: Add new `Uri` class for new PSR-7 implementation. + (#521 by @clue) + +* Feature: Validate outgoing HTTP message headers and reject invalid messages. + (#523 by @clue) + +* Feature: Full PHP 8.3 compatibility. + (#508 by @clue) + +* Fix: Fix HTTP client to omit `Transfer-Encoding: chunked` when streaming empty request body. + (#516 by @clue) + +* Fix: Ensure connection close handler is cleaned up for each request. + (#515 by @WyriHaximus) + +* Update test suite and avoid unhandled promise rejections. + (#501 and #502 by @clue) + ## 1.9.0 (2023-04-26) This is a **SECURITY** and feature release for the 1.x series of ReactPHP's HTTP component. diff --git a/README.md b/README.md index 18089464..d0550642 100644 --- a/README.md +++ b/README.md @@ -2986,7 +2986,7 @@ This project follows [SemVer](https://semver.org/). This will install the latest supported version: ```bash -composer require react/http:^1.9 +composer require react/http:^1.10 ``` See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. From dfbeef0fc47d91eeab88e2a38566e18fa6633f1f Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Thu, 16 May 2024 12:10:12 +0200 Subject: [PATCH 447/456] Hello `3.x` development branch Once this PR is merged, we can start working on the new [v3.0.0 milestone](https://github.com/reactphp/http/milestone/37). The default branch will be `3.x` and the old `1.x` branch still stay in place at least until `3.0.0` is released. Refs: Road map ticket for http: #517 Plans for ReactPHP v3: https://github.com/orgs/reactphp/discussions/481 PR templated from: https://github.com/friends-of-reactphp/mysql/pull/185 --- README.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d0550642..8867f798 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,14 @@ Event-driven, streaming HTTP client and server implementation for [ReactPHP](https://reactphp.org/). +> **Development version:** This branch contains the code for the upcoming v3 +> release. For the code of the current stable v1 release, check out the +> [`1.x` branch](https://github.com/reactphp/http/tree/1.x). +> +> The upcoming v3 release will be the way forward for this package. However, +> we will still actively support v1 for those not yet on the latest version. +> See also [installation instructions](#install) for more details. + This HTTP library provides re-usable implementations for an HTTP client and server based on ReactPHP's [`Socket`](https://github.com/reactphp/socket) and [`EventLoop`](https://github.com/reactphp/event-loop) components. @@ -2982,11 +2990,11 @@ new React\Http\Middleware\RequestBodyParserMiddleware(10 * 1024, 100); // 100 fi The recommended way to install this library is [through Composer](https://getcomposer.org/). [New to Composer?](https://getcomposer.org/doc/00-intro.md) -This project follows [SemVer](https://semver.org/). -This will install the latest supported version: +Once released, this project will follow [SemVer](https://semver.org/). +At the moment, this will install the latest development version: ```bash -composer require react/http:^1.10 +composer require react/http:^3@dev ``` See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. From 53f935e5fa788e92d98e137ff5e52116650bd708 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 28 Sep 2023 19:29:35 +0200 Subject: [PATCH 448/456] Drop deprecated `Server` class, use `HttpServer` instead --- README.md | 6 ------ src/HttpServer.php | 4 ---- src/Server.php | 18 ------------------ tests/ServerTest.php | 15 --------------- 4 files changed, 43 deletions(-) delete mode 100644 src/Server.php delete mode 100644 tests/ServerTest.php diff --git a/README.md b/README.md index 8867f798..8e3f29a2 100644 --- a/README.md +++ b/README.md @@ -740,8 +740,6 @@ See also the [Unix Domain Sockets (UDS) example](examples/14-client-unix-domain- ### HttpServer - - The `React\Http\HttpServer` class is responsible for handling incoming connections and then processing each incoming HTTP request. @@ -891,10 +889,6 @@ have full control over consuming the incoming HTTP request body and concurrency settings. See also [streaming incoming request](#streaming-incoming-request) below for more details. -> Changelog v1.5.0: This class has been renamed to `HttpServer` from the - previous `Server` class in order to avoid any ambiguities. - The previous name has been deprecated and should not be used anymore. - ### listen() The `listen(React\Socket\ServerInterface $socket): void` method can be used to diff --git a/src/HttpServer.php b/src/HttpServer.php index f2334733..cd0874cc 100644 --- a/src/HttpServer.php +++ b/src/HttpServer.php @@ -167,10 +167,6 @@ * have full control over consuming the incoming HTTP request body and * concurrency settings. See also [streaming incoming request](#streaming-incoming-request) * below for more details. - * - * > Changelog v1.5.0: This class has been renamed to `HttpServer` from the - * previous `Server` class in order to avoid any ambiguities. - * The previous name has been deprecated and should not be used anymore. */ final class HttpServer extends EventEmitter { diff --git a/src/Server.php b/src/Server.php deleted file mode 100644 index 9bb9cf7f..00000000 --- a/src/Server.php +++ /dev/null @@ -1,18 +0,0 @@ -assertInstanceOf('React\Http\HttpServer', $http); - } -} From 6956fe2de8f37c1027fb3936d829ab66e3e9da38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 29 Sep 2023 12:25:11 +0200 Subject: [PATCH 449/456] Drop deprecated alternative `Browser` constructor argument order --- README.md | 4 --- src/Browser.php | 22 ++----------- tests/BrowserTest.php | 72 ------------------------------------------- 3 files changed, 3 insertions(+), 95 deletions(-) diff --git a/README.md b/README.md index 8867f798..2f7151ac 100644 --- a/README.md +++ b/README.md @@ -1869,11 +1869,7 @@ $browser = new React\Http\Browser(); This class takes two optional arguments for more advanced usage: ```php -// constructor signature as of v1.5.0 $browser = new React\Http\Browser(?ConnectorInterface $connector = null, ?LoopInterface $loop = null); - -// legacy constructor signature before v1.5.0 -$browser = new React\Http\Browser(?LoopInterface $loop = null, ?ConnectorInterface $connector = null); ``` If you need custom connector settings (DNS resolution, TLS parameters, timeouts, diff --git a/src/Browser.php b/src/Browser.php index 01a266ca..a24d24e7 100644 --- a/src/Browser.php +++ b/src/Browser.php @@ -37,11 +37,7 @@ class Browser * This class takes two optional arguments for more advanced usage: * * ```php - * // constructor signature as of v1.5.0 * $browser = new React\Http\Browser(?ConnectorInterface $connector = null, ?LoopInterface $loop = null); - * - * // legacy constructor signature before v1.5.0 - * $browser = new React\Http\Browser(?LoopInterface $loop = null, ?ConnectorInterface $connector = null); * ``` * * If you need custom connector settings (DNS resolution, TLS parameters, timeouts, @@ -69,23 +65,11 @@ class Browser * This value SHOULD NOT be given unless you're sure you want to explicitly use a * given event loop instance. * - * @param null|ConnectorInterface|LoopInterface $connector - * @param null|LoopInterface|ConnectorInterface $loop - * @throws \InvalidArgumentException for invalid arguments + * @param ?ConnectorInterface $connector + * @param ?LoopInterface $loop */ - public function __construct($connector = null, $loop = null) + public function __construct(ConnectorInterface $connector = null, LoopInterface $loop = null) { - // swap arguments for legacy constructor signature - if (($connector instanceof LoopInterface || $connector === null) && ($loop instanceof ConnectorInterface || $loop === null)) { - $swap = $loop; - $loop = $connector; - $connector = $swap; - } - - if (($connector !== null && !$connector instanceof ConnectorInterface) || ($loop !== null && !$loop instanceof LoopInterface)) { - throw new \InvalidArgumentException('Expected "?ConnectorInterface $connector" and "?LoopInterface $loop" arguments'); - } - $loop = $loop ?: Loop::get(); $this->transaction = new Transaction( Sender::createFromLoop($loop, $connector), diff --git a/tests/BrowserTest.php b/tests/BrowserTest.php index fdd338d9..fb1a1beb 100644 --- a/tests/BrowserTest.php +++ b/tests/BrowserTest.php @@ -70,35 +70,6 @@ public function testConstructWithConnectorAssignsGivenConnector() $this->assertSame($connector, $ret); } - public function testConstructWithConnectorWithLegacySignatureAssignsGivenConnector() - { - $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); - - $browser = new Browser(null, $connector); - - $ref = new \ReflectionProperty($browser, 'transaction'); - $ref->setAccessible(true); - $transaction = $ref->getValue($browser); - - $ref = new \ReflectionProperty($transaction, 'sender'); - $ref->setAccessible(true); - $sender = $ref->getValue($transaction); - - $ref = new \ReflectionProperty($sender, 'http'); - $ref->setAccessible(true); - $client = $ref->getValue($sender); - - $ref = new \ReflectionProperty($client, 'connectionManager'); - $ref->setAccessible(true); - $connectionManager = $ref->getValue($client); - - $ref = new \ReflectionProperty($connectionManager, 'connector'); - $ref->setAccessible(true); - $ret = $ref->getValue($connectionManager); - - $this->assertSame($connector, $ret); - } - public function testConstructWithLoopAssignsGivenLoop() { $browser = new Browser(null, $this->loop); @@ -114,49 +85,6 @@ public function testConstructWithLoopAssignsGivenLoop() $this->assertSame($this->loop, $loop); } - public function testConstructWithLoopWithLegacySignatureAssignsGivenLoop() - { - $browser = new Browser($this->loop); - - $ref = new \ReflectionProperty($browser, 'transaction'); - $ref->setAccessible(true); - $transaction = $ref->getValue($browser); - - $ref = new \ReflectionProperty($transaction, 'loop'); - $ref->setAccessible(true); - $loop = $ref->getValue($transaction); - - $this->assertSame($this->loop, $loop); - } - - public function testConstructWithInvalidConnectorThrows() - { - $this->setExpectedException('InvalidArgumentException'); - new Browser('foo'); - } - - public function testConstructWithInvalidLoopThrows() - { - $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); - - $this->setExpectedException('InvalidArgumentException'); - new Browser($connector, 'foo'); - } - - public function testConstructWithConnectorTwiceThrows() - { - $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); - - $this->setExpectedException('InvalidArgumentException'); - new Browser($connector, $connector); - } - - public function testConstructWithLoopTwiceThrows() - { - $this->setExpectedException('InvalidArgumentException'); - new Browser($this->loop, $this->loop); - } - public function testGetSendsGetRequest() { $that = $this; From acd2e1401a96fee53a971a51499de9fb0c7c26c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 25 May 2024 12:58:26 +0200 Subject: [PATCH 450/456] Update to require PHP 7.1+ --- .github/workflows/ci.yml | 21 ----------- README.md | 3 +- composer.json | 6 ++-- phpunit.xml.legacy | 2 +- src/Io/ClientRequestStream.php | 3 +- src/Io/MultipartParser.php | 14 +++----- src/Io/StreamingServer.php | 3 +- src/Message/Response.php | 3 +- src/Message/Uri.php | 11 +----- tests/Client/FunctionalIntegrationTest.php | 6 ---- tests/FunctionalBrowserTest.php | 12 ------- tests/FunctionalHttpServerTest.php | 28 --------------- tests/HttpServerTest.php | 3 -- tests/Io/MiddlewareRunnerTest.php | 3 -- tests/Io/RequestHeaderParserTest.php | 4 --- tests/Io/StreamingServerTest.php | 6 ---- tests/Message/ResponseTest.php | 12 +------ tests/Message/UriTest.php | 5 --- .../LimitConcurrentRequestsMiddlewareTest.php | 3 -- .../RequestBodyBufferMiddlewareTest.php | 3 -- .../RequestBodyParserMiddlewareTest.php | 35 ++----------------- tests/TestCase.php | 18 ++++------ 22 files changed, 23 insertions(+), 181 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3666cd47..6f9cfb41 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,11 +19,6 @@ jobs: - 7.3 - 7.2 - 7.1 - - 7.0 - - 5.6 - - 5.5 - - 5.4 - - 5.3 steps: - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 @@ -36,19 +31,3 @@ jobs: if: ${{ matrix.php >= 7.3 }} - run: vendor/bin/phpunit --coverage-text -c phpunit.xml.legacy if: ${{ matrix.php < 7.3 }} - - PHPUnit-hhvm: - name: PHPUnit (HHVM) - runs-on: ubuntu-22.04 - continue-on-error: true - steps: - - uses: actions/checkout@v4 - - run: cp "$(which composer)" composer.phar && ./composer.phar self-update --2.2 # downgrade Composer for HHVM - - name: Run hhvm composer.phar install - uses: docker://hhvm/hhvm:3.30-lts-latest - with: - args: hhvm composer.phar install - - name: Run hhvm vendor/bin/phpunit - uses: docker://hhvm/hhvm:3.30-lts-latest - with: - args: hhvm vendor/bin/phpunit diff --git a/README.md b/README.md index 46da303d..9cdf7c09 100644 --- a/README.md +++ b/README.md @@ -2990,8 +2990,7 @@ composer require react/http:^3@dev See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. This project aims to run on any platform and thus does not require any PHP -extensions and supports running on legacy PHP 5.3 through current PHP 8+ and -HHVM. +extensions and supports running on PHP 7.1 through current PHP 8+. It's *highly recommended to use the latest supported PHP version* for this project. ## Tests diff --git a/composer.json b/composer.json index 23783c0c..33919186 100644 --- a/composer.json +++ b/composer.json @@ -26,7 +26,7 @@ } ], "require": { - "php": ">=5.3.0", + "php": ">=7.1", "evenement/evenement": "^3.0 || ^2.0 || ^1.0", "fig/http-message-util": "^1.1", "psr/http-message": "^1.0", @@ -39,8 +39,8 @@ "clue/http-proxy-react": "^1.8", "clue/reactphp-ssh-proxy": "^1.4", "clue/socks-react": "^1.4", - "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", - "react/async": "^4 || ^3 || ^2", + "phpunit/phpunit": "^9.6 || ^5.7", + "react/async": "^4 || ^3", "react/promise-stream": "^1.4", "react/promise-timer": "^1.9" }, diff --git a/phpunit.xml.legacy b/phpunit.xml.legacy index 89161168..a018d7ab 100644 --- a/phpunit.xml.legacy +++ b/phpunit.xml.legacy @@ -2,7 +2,7 @@ diff --git a/src/Io/ClientRequestStream.php b/src/Io/ClientRequestStream.php index ff9bf2d4..3bdf9b1f 100644 --- a/src/Io/ClientRequestStream.php +++ b/src/Io/ClientRequestStream.php @@ -69,8 +69,7 @@ private function writeHead() } } - /** @var array $m legacy PHP 5.3 only */ - if (!\preg_match('#^\S+ \S+ HTTP/1\.[01]\r\n#m', $headers) || \substr_count($headers, "\n") !== ($expected + 1) || (\PHP_VERSION_ID >= 50400 ? \preg_match_all(AbstractMessage::REGEX_HEADERS, $headers) : \preg_match_all(AbstractMessage::REGEX_HEADERS, $headers, $m)) !== $expected) { + if (!\preg_match('#^\S+ \S+ HTTP/1\.[01]\r\n#m', $headers) || \substr_count($headers, "\n") !== ($expected + 1) || \preg_match_all(AbstractMessage::REGEX_HEADERS, $headers) !== $expected) { $this->closeError(new \InvalidArgumentException('Unable to send request with invalid request headers')); return; } diff --git a/src/Io/MultipartParser.php b/src/Io/MultipartParser.php index 539107ae..c65bb655 100644 --- a/src/Io/MultipartParser.php +++ b/src/Io/MultipartParser.php @@ -36,7 +36,7 @@ final class MultipartParser /** * ini setting "max_input_vars" * - * Does not exist in PHP < 5.3.9 or HHVM, so assume PHP's default 1000 here. + * Assume PHP' default of 1000 here. * * @var int * @link http://php.net/manual/en/info.configuration.php#ini.max-input-vars @@ -46,7 +46,7 @@ final class MultipartParser /** * ini setting "max_input_nesting_level" * - * Does not exist in HHVM, but assumes hard coded to 64 (PHP's default). + * Assume PHP's default of 64 here. * * @var int * @link http://php.net/manual/en/info.configuration.php#ini.max-input-nesting-level @@ -81,14 +81,8 @@ final class MultipartParser */ public function __construct($uploadMaxFilesize = null, $maxFileUploads = null) { - $var = \ini_get('max_input_vars'); - if ($var !== false) { - $this->maxInputVars = (int)$var; - } - $var = \ini_get('max_input_nesting_level'); - if ($var !== false) { - $this->maxInputNestingLevel = (int)$var; - } + $this->maxInputVars = (int) \ini_get('max_input_vars'); + $this->maxInputNestingLevel = (int) \ini_get('max_input_nesting_level'); if ($uploadMaxFilesize === null) { $uploadMaxFilesize = \ini_get('upload_max_filesize'); diff --git a/src/Io/StreamingServer.php b/src/Io/StreamingServer.php index 143edaa8..eee9f900 100644 --- a/src/Io/StreamingServer.php +++ b/src/Io/StreamingServer.php @@ -346,8 +346,7 @@ public function handleResponse(ConnectionInterface $connection, ServerRequestInt } } - /** @var array $m legacy PHP 5.3 only */ - if ($code < 100 || $code > 999 || \substr_count($headers, "\n") !== ($expected + 1) || (\PHP_VERSION_ID >= 50400 ? \preg_match_all(AbstractMessage::REGEX_HEADERS, $headers) : \preg_match_all(AbstractMessage::REGEX_HEADERS, $headers, $m)) !== $expected) { + if ($code < 100 || $code > 999 || \substr_count($headers, "\n") !== ($expected + 1) || \preg_match_all(AbstractMessage::REGEX_HEADERS, $headers) !== $expected) { $this->emit('error', array(new \InvalidArgumentException('Unable to send response with invalid response headers'))); $this->writeError($connection, Response::STATUS_INTERNAL_SERVER_ERROR, $request); return; diff --git a/src/Message/Response.php b/src/Message/Response.php index fa6366ed..107508a9 100644 --- a/src/Message/Response.php +++ b/src/Message/Response.php @@ -151,8 +151,7 @@ public static function json($data) (\defined('JSON_PRETTY_PRINT') ? \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE : 0) | (\defined('JSON_PRESERVE_ZERO_FRACTION') ? \JSON_PRESERVE_ZERO_FRACTION : 0) ); - // throw on error, now `false` but used to be `(string) "null"` before PHP 5.5 - if ($json === false || (\PHP_VERSION_ID < 50500 && \json_last_error() !== \JSON_ERROR_NONE)) { + if ($json === false) { throw new \InvalidArgumentException( 'Unable to encode given data as JSON' . (\function_exists('json_last_error_msg') ? ': ' . \json_last_error_msg() : ''), \json_last_error() diff --git a/src/Message/Uri.php b/src/Message/Uri.php index 4309bbed..6b77d3e7 100644 --- a/src/Message/Uri.php +++ b/src/Message/Uri.php @@ -45,16 +45,7 @@ final class Uri implements UriInterface */ public function __construct($uri) { - // @codeCoverageIgnoreStart - if (\PHP_VERSION_ID < 50407 && \strpos($uri, '//') === 0) { - // @link https://3v4l.org/UrAQP - $parts = \parse_url('http:' . $uri); - unset($parts['schema']); - } else { - $parts = \parse_url($uri); - } - // @codeCoverageIgnoreEnd - + $parts = \parse_url($uri); if ($parts === false || (isset($parts['scheme']) && !\preg_match('#^[a-z]+$#i', $parts['scheme'])) || (isset($parts['host']) && \preg_match('#[\s_%+]#', $parts['host']))) { throw new \InvalidArgumentException('Invalid URI given'); } diff --git a/tests/Client/FunctionalIntegrationTest.php b/tests/Client/FunctionalIntegrationTest.php index 5405874e..6c49c127 100644 --- a/tests/Client/FunctionalIntegrationTest.php +++ b/tests/Client/FunctionalIntegrationTest.php @@ -134,9 +134,6 @@ public function testRequestLegacyHttpServerWithOnlyLineFeedReturnsSuccessfulResp /** @group internet */ public function testSuccessfulResponseEmitsEnd() { - // max_nesting_level was set to 100 for PHP Versions < 5.4 which resulted in failing test for legacy PHP - ini_set('xdebug.max_nesting_level', 256); - $client = new Client(new ClientConnectionManager(new Connector(), Loop::get())); $request = $client->request(new Request('GET', 'http://www.google.com/', array(), '', '1.0')); @@ -155,9 +152,6 @@ public function testSuccessfulResponseEmitsEnd() /** @group internet */ public function testCancelPendingConnectionEmitsClose() { - // max_nesting_level was set to 100 for PHP Versions < 5.4 which resulted in failing test for legacy PHP - ini_set('xdebug.max_nesting_level', 256); - $client = new Client(new ClientConnectionManager(new Connector(), Loop::get())); $request = $client->request(new Request('GET', 'http://www.google.com/', array(), '', '1.0')); diff --git a/tests/FunctionalBrowserTest.php b/tests/FunctionalBrowserTest.php index 7b8ff84b..6a235703 100644 --- a/tests/FunctionalBrowserTest.php +++ b/tests/FunctionalBrowserTest.php @@ -389,10 +389,6 @@ public function testGetRequestWithResponseBufferExceededDuringStreamingRejects() */ public function testCanAccessHttps() { - if (defined('HHVM_VERSION')) { - $this->markTestSkipped('Not supported on HHVM'); - } - \React\Async\await($this->browser->get('https://www.google.com/')); } @@ -401,10 +397,6 @@ public function testCanAccessHttps() */ public function testVerifyPeerEnabledForBadSslRejects() { - if (defined('HHVM_VERSION')) { - $this->markTestSkipped('Not supported on HHVM'); - } - $connector = new Connector(array( 'tls' => array( 'verify_peer' => true @@ -423,10 +415,6 @@ public function testVerifyPeerEnabledForBadSslRejects() */ public function testVerifyPeerDisabledForBadSslResolves() { - if (defined('HHVM_VERSION')) { - $this->markTestSkipped('Not supported on HHVM'); - } - $connector = new Connector(array( 'tls' => array( 'verify_peer' => false diff --git a/tests/FunctionalHttpServerTest.php b/tests/FunctionalHttpServerTest.php index dcd79b3e..6b153b81 100644 --- a/tests/FunctionalHttpServerTest.php +++ b/tests/FunctionalHttpServerTest.php @@ -122,10 +122,6 @@ public function testPlainHttpOnRandomPortWithOtherHostHeaderTakesPrecedence() public function testSecureHttpsOnRandomPort() { - if (defined('HHVM_VERSION')) { - $this->markTestSkipped('Not supported on HHVM'); - } - $connector = new Connector(array( 'tls' => array('verify_peer' => false) )); @@ -155,10 +151,6 @@ public function testSecureHttpsOnRandomPort() public function testSecureHttpsReturnsData() { - if (defined('HHVM_VERSION')) { - $this->markTestSkipped('Not supported on HHVM'); - } - $http = new HttpServer(function (RequestInterface $request) { return new Response( 200, @@ -193,10 +185,6 @@ public function testSecureHttpsReturnsData() public function testSecureHttpsOnRandomPortWithoutHostHeaderUsesSocketUri() { - if (defined('HHVM_VERSION')) { - $this->markTestSkipped('Not supported on HHVM'); - } - $connector = new Connector(array( 'tls' => array('verify_peer' => false) )); @@ -284,10 +272,6 @@ public function testPlainHttpOnStandardPortWithoutHostHeaderReturnsUriWithNoPort public function testSecureHttpsOnStandardPortReturnsUriWithNoPort() { - if (defined('HHVM_VERSION')) { - $this->markTestSkipped('Not supported on HHVM'); - } - try { $socket = new SocketServer('tls://127.0.0.1:443', array('tls' => array( 'local_cert' => __DIR__ . '/../examples/localhost.pem' @@ -322,10 +306,6 @@ public function testSecureHttpsOnStandardPortReturnsUriWithNoPort() public function testSecureHttpsOnStandardPortWithoutHostHeaderUsesSocketUri() { - if (defined('HHVM_VERSION')) { - $this->markTestSkipped('Not supported on HHVM'); - } - try { $socket = new SocketServer('tls://127.0.0.1:443', array('tls' => array( 'local_cert' => __DIR__ . '/../examples/localhost.pem' @@ -389,10 +369,6 @@ public function testPlainHttpOnHttpsStandardPortReturnsUriWithPort() public function testSecureHttpsOnHttpStandardPortReturnsUriWithPort() { - if (defined('HHVM_VERSION')) { - $this->markTestSkipped('Not supported on HHVM'); - } - try { $socket = new SocketServer('tls://127.0.0.1:80', array('tls' => array( 'local_cert' => __DIR__ . '/../examples/localhost.pem' @@ -726,10 +702,6 @@ public function testConnectWithClosedThroughStreamReturnsNoData() public function testLimitConcurrentRequestsMiddlewareRequestStreamPausing() { - if (defined('HHVM_VERSION') && !interface_exists('React\Promise\PromisorInterface')) { - $this->markTestSkipped('Not supported on legacy HHVM with Promise v3'); - } - $connector = new Connector(); $http = new HttpServer( diff --git a/tests/HttpServerTest.php b/tests/HttpServerTest.php index 72d48468..606c50a6 100644 --- a/tests/HttpServerTest.php +++ b/tests/HttpServerTest.php @@ -87,9 +87,6 @@ public function testSimpleRequestCallsRequestHandlerOnce() $this->assertSame(1, $called); } - /** - * @requires PHP 5.4 - */ public function testSimpleRequestCallsArrayRequestHandlerOnce() { $this->called = null; diff --git a/tests/Io/MiddlewareRunnerTest.php b/tests/Io/MiddlewareRunnerTest.php index 762d7bdb..e742ef6d 100644 --- a/tests/Io/MiddlewareRunnerTest.php +++ b/tests/Io/MiddlewareRunnerTest.php @@ -76,9 +76,6 @@ function (ServerRequestInterface $request) { $middleware($request); } - /** - * @requires PHP 7 - */ public function testThrowsIfHandlerThrowsThrowable() { $middleware = new MiddlewareRunner(array( diff --git a/tests/Io/RequestHeaderParserTest.php b/tests/Io/RequestHeaderParserTest.php index 87d6bf1b..1ed994b7 100644 --- a/tests/Io/RequestHeaderParserTest.php +++ b/tests/Io/RequestHeaderParserTest.php @@ -776,10 +776,6 @@ public function testServerParamsWillNotSetRemoteAddressForUnixDomainSockets() public function testServerParamsWontBeSetOnMissingUrls() { - if (defined('HHVM_VERSION')) { - $this->markTestSkipped('Not supported on HHVM'); - } - $request = null; $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); diff --git a/tests/Io/StreamingServerTest.php b/tests/Io/StreamingServerTest.php index b4e3f2f8..152fece6 100644 --- a/tests/Io/StreamingServerTest.php +++ b/tests/Io/StreamingServerTest.php @@ -82,9 +82,6 @@ public function testRequestEventIsEmitted() $this->connection->emit('data', array($data)); } - /** - * @requires PHP 5.4 - */ public function testRequestEventIsEmittedForArrayCallable() { $this->called = null; @@ -2845,9 +2842,6 @@ function ($data) use (&$buffer) { $this->assertEquals('hello', $exception->getPrevious()->getMessage()); } - /** - * @requires PHP 7 - */ public function testResponseThrowableThrowInCallBackFunctionWillResultInErrorMessage() { $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { diff --git a/tests/Message/ResponseTest.php b/tests/Message/ResponseTest.php index a9a244c2..1c70ae3a 100644 --- a/tests/Message/ResponseTest.php +++ b/tests/Message/ResponseTest.php @@ -97,9 +97,6 @@ public function testHtmlMethodReturnsHtmlResponse() $this->assertEquals('Hello wörld!', (string) $response->getBody()); } - /** - * @requires PHP 5.4 - */ public function testJsonMethodReturnsPrettyPrintedJsonResponse() { $response = Response::json(array('text' => 'Hello wörld!')); @@ -109,9 +106,6 @@ public function testJsonMethodReturnsPrettyPrintedJsonResponse() $this->assertEquals("{\n \"text\": \"Hello wörld!\"\n}\n", (string) $response->getBody()); } - /** - * @requires PHP 5.6.6 - */ public function testJsonMethodReturnsZeroFractionsInJsonResponse() { $response = Response::json(1.0); @@ -132,11 +126,7 @@ public function testJsonMethodReturnsJsonTextForSimpleString() public function testJsonMethodThrowsForInvalidString() { - if (PHP_VERSION_ID < 50500) { - $this->setExpectedException('InvalidArgumentException', 'Unable to encode given data as JSON'); - } else { - $this->setExpectedException('InvalidArgumentException', 'Unable to encode given data as JSON: Malformed UTF-8 characters, possibly incorrectly encoded'); - } + $this->setExpectedException('InvalidArgumentException', 'Unable to encode given data as JSON: Malformed UTF-8 characters, possibly incorrectly encoded'); Response::json("Hello w\xF6rld!"); } diff --git a/tests/Message/UriTest.php b/tests/Message/UriTest.php index 05eec723..10f355df 100644 --- a/tests/Message/UriTest.php +++ b/tests/Message/UriTest.php @@ -130,11 +130,6 @@ public static function provideValidUris() */ public function testToStringReturnsOriginalUriGivenToCtor($string) { - if (PHP_VERSION_ID < 50519 || (PHP_VERSION_ID < 50603 && PHP_VERSION_ID >= 50606)) { - // @link https://3v4l.org/HdoPG - $this->markTestSkipped('Empty password not supported on legacy PHP'); - } - $uri = new Uri($string); $this->assertEquals($string, (string) $uri); diff --git a/tests/Middleware/LimitConcurrentRequestsMiddlewareTest.php b/tests/Middleware/LimitConcurrentRequestsMiddlewareTest.php index faf27cb6..23455e6c 100644 --- a/tests/Middleware/LimitConcurrentRequestsMiddlewareTest.php +++ b/tests/Middleware/LimitConcurrentRequestsMiddlewareTest.php @@ -120,9 +120,6 @@ public function testThrowsExceptionDirectlyFromMiddlewareWhenBelowLimit() }); } - /** - * @requires PHP 7 - */ public function testThrowsErrorDirectlyFromMiddlewareWhenBelowLimit() { $middleware = new LimitConcurrentRequestsMiddleware(1); diff --git a/tests/Middleware/RequestBodyBufferMiddlewareTest.php b/tests/Middleware/RequestBodyBufferMiddlewareTest.php index 40c23378..1c3b0b33 100644 --- a/tests/Middleware/RequestBodyBufferMiddlewareTest.php +++ b/tests/Middleware/RequestBodyBufferMiddlewareTest.php @@ -277,9 +277,6 @@ function (ServerRequestInterface $request) { $this->assertNull($exception->getPrevious()); } - /** - * @requires PHP 7 - */ public function testBufferingRejectsWhenNextHandlerThrowsErrorWhenStreamEnds() { $stream = new ThroughStream(); diff --git a/tests/Middleware/RequestBodyParserMiddlewareTest.php b/tests/Middleware/RequestBodyParserMiddlewareTest.php index 989abc81..b588bdd5 100644 --- a/tests/Middleware/RequestBodyParserMiddlewareTest.php +++ b/tests/Middleware/RequestBodyParserMiddlewareTest.php @@ -109,13 +109,6 @@ function (ServerRequestInterface $request) { public function testFormUrlencodedIgnoresBodyWithExcessiveNesting() { - // supported in all Zend PHP versions and HHVM - // ini setting does exist everywhere but HHVM: https://3v4l.org/hXLiK - // HHVM limits to 64 and returns an empty array structure: https://3v4l.org/j3DK2 - if (defined('HHVM_VERSION')) { - $this->markTestSkipped('Not supported on HHVM (limited to depth 64, but keeps empty array structure)'); - } - $allowed = (int)ini_get('max_input_nesting_level'); $middleware = new RequestBodyParserMiddleware(); @@ -144,12 +137,6 @@ function (ServerRequestInterface $request) { public function testFormUrlencodedTruncatesBodyWithExcessiveLength() { - // supported as of PHP 5.3.11, no HHVM support: https://3v4l.org/PiqnQ - // ini setting already exists in PHP 5.3.9: https://3v4l.org/VF6oV - if (defined('HHVM_VERSION') || PHP_VERSION_ID < 50311) { - $this->markTestSkipped('Not supported on HHVM and PHP < 5.3.11 (unlimited length)'); - } - $allowed = (int)ini_get('max_input_vars'); $middleware = new RequestBodyParserMiddleware(); @@ -243,13 +230,7 @@ function (ServerRequestInterface $request) { public function testMultipartFormDataIgnoresFieldWithExcessiveNesting() { - // supported in all Zend PHP versions and HHVM - // ini setting does exist everywhere but HHVM: https://3v4l.org/hXLiK - // HHVM limits to 64 and otherwise returns an empty array structure - $allowed = (int)ini_get('max_input_nesting_level'); - if ($allowed === 0) { - $allowed = 64; - } + $allowed = (int) ini_get('max_input_nesting_level'); $middleware = new RequestBodyParserMiddleware(); @@ -278,12 +259,7 @@ function (ServerRequestInterface $request) { public function testMultipartFormDataTruncatesBodyWithExcessiveLength() { - // ini setting exists in PHP 5.3.9, not in HHVM: https://3v4l.org/VF6oV - // otherwise default to 1000 as implemented within - $allowed = (int)ini_get('max_input_vars'); - if ($allowed === 0) { - $allowed = 1000; - } + $allowed = (int) ini_get('max_input_vars'); $middleware = new RequestBodyParserMiddleware(); @@ -319,12 +295,7 @@ function (ServerRequestInterface $request) { public function testMultipartFormDataTruncatesExcessiveNumberOfEmptyFileUploads() { - // ini setting exists in PHP 5.3.9, not in HHVM: https://3v4l.org/VF6oV - // otherwise default to 1000 as implemented within - $allowed = (int)ini_get('max_input_vars'); - if ($allowed === 0) { - $allowed = 1000; - } + $allowed = (int) ini_get('max_input_vars'); $middleware = new RequestBodyParserMiddleware(); diff --git a/tests/TestCase.php b/tests/TestCase.php index 72b7be8d..88d8a3df 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -72,18 +72,12 @@ public function assertNotContainsString($needle, $haystack) public function setExpectedException($exception, $exceptionMessage = '', $exceptionCode = null) { - if (method_exists($this, 'expectException')) { - // PHPUnit 5+ - $this->expectException($exception); - if ($exceptionMessage !== '') { - $this->expectExceptionMessage($exceptionMessage); - } - if ($exceptionCode !== null) { - $this->expectExceptionCode($exceptionCode); - } - } else { - // legacy PHPUnit 4 - parent::setExpectedException($exception, $exceptionMessage, $exceptionCode); + $this->expectException($exception); + if ($exceptionMessage !== '') { + $this->expectExceptionMessage($exceptionMessage); + } + if ($exceptionCode !== null) { + $this->expectExceptionCode($exceptionCode); } } From 4cbe56ef079c3bc90eaacc2cffefa46596a09d88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 25 May 2024 16:37:32 +0200 Subject: [PATCH 451/456] Update PHP language syntax and remove legacy workarounds --- README.md | 131 ++-- examples/03-client-request-any.php | 4 +- examples/04-client-post-json.php | 12 +- examples/05-client-put-xml.php | 4 +- examples/11-client-http-proxy.php | 4 +- examples/12-client-socks-proxy.php | 4 +- examples/13-client-ssh-proxy.php | 4 +- .../21-client-request-streaming-to-stdout.php | 2 +- .../22-client-stream-upload-from-stdin.php | 4 +- examples/51-server-hello-world.php | 2 +- examples/52-server-count-visitors.php | 2 +- examples/53-server-whatsmyip.php | 2 +- examples/54-server-query-parameter.php | 2 +- examples/55-server-cookie-handling.php | 2 +- examples/56-server-sleep.php | 2 +- examples/57-server-error-handling.php | 2 +- examples/58-server-stream-response.php | 6 +- examples/59-server-json-api.php | 26 +- examples/61-server-hello-world-https.php | 12 +- examples/62-server-form-upload.php | 2 +- examples/63-server-streaming-request.php | 2 +- examples/71-server-http-proxy.php | 2 +- examples/72-server-http-connect-proxy.php | 12 +- examples/81-server-upgrade-echo.php | 10 +- examples/82-server-upgrade-chat.php | 10 +- examples/91-client-benchmark-download.php | 2 +- examples/92-client-benchmark-upload.php | 10 +- examples/99-server-benchmark-download.php | 12 +- src/Browser.php | 74 +- src/HttpServer.php | 21 +- src/Io/AbstractMessage.php | 18 +- src/Io/AbstractRequest.php | 4 +- src/Io/BufferedBody.php | 2 +- src/Io/ChunkedDecoder.php | 14 +- src/Io/ChunkedEncoder.php | 18 +- src/Io/ClientConnectionManager.php | 27 +- src/Io/ClientRequestStream.php | 59 +- src/Io/Clock.php | 7 +- src/Io/CloseProtectionStream.php | 22 +- src/Io/EmptyBodyStream.php | 4 +- src/Io/HttpBodyStream.php | 14 +- src/Io/LengthLimitedStream.php | 16 +- src/Io/MiddlewareRunner.php | 8 +- src/Io/MultipartParser.php | 8 +- src/Io/PauseBufferStream.php | 18 +- src/Io/ReadableBodyStream.php | 30 +- src/Io/RequestHeaderParser.php | 35 +- src/Io/Sender.php | 8 +- src/Io/StreamingServer.php | 66 +- src/Io/Transaction.php | 50 +- src/Io/UploadedFile.php | 4 +- src/Message/Request.php | 2 +- src/Message/Response.php | 41 +- src/Message/ServerRequest.php | 20 +- src/Message/Uri.php | 8 +- .../LimitConcurrentRequestsMiddleware.php | 57 +- .../RequestBodyBufferMiddleware.php | 5 +- .../RequestBodyParserMiddleware.php | 4 +- tests/BrowserTest.php | 223 +++--- tests/Client/FunctionalIntegrationTest.php | 41 +- tests/FunctionalBrowserTest.php | 182 ++--- tests/FunctionalHttpServerTest.php | 205 +++--- tests/HttpServerTest.php | 78 +-- tests/Io/AbstractMessageTest.php | 100 +-- tests/Io/AbstractRequestTest.php | 96 +-- tests/Io/BufferedBodyTest.php | 2 +- tests/Io/ChunkedDecoderTest.php | 176 ++--- tests/Io/ChunkedEncoderTest.php | 8 +- tests/Io/ClientConnectionManagerTest.php | 43 +- tests/Io/ClientRequestStreamTest.php | 338 +++++---- tests/Io/CloseProtectionStreamTest.php | 8 +- tests/Io/EmptyBodyStreamTest.php | 4 +- tests/Io/HttpBodyStreamTest.php | 14 +- tests/Io/IniUtilTest.php | 52 +- tests/Io/LengthLimitedStreamTest.php | 14 +- tests/Io/MiddlewareRunnerTest.php | 170 ++--- tests/Io/MultipartParserTest.php | 232 +++--- tests/Io/PauseBufferStreamTest.php | 8 +- tests/Io/ReadableBodyStreamTest.php | 2 +- tests/Io/RequestHeaderParserTest.php | 141 ++-- tests/Io/SenderTest.php | 44 +- tests/Io/StreamingServerTest.php | 659 +++++++++--------- tests/Io/TransactionTest.php | 326 +++++---- tests/Io/UploadedFileTest.php | 12 +- tests/Message/RequestTest.php | 8 +- tests/Message/ResponseTest.php | 26 +- tests/Message/ServerRequestTest.php | 132 ++-- tests/Message/UriTest.php | 240 +++---- .../LimitConcurrentRequestsMiddlewareTest.php | 38 +- tests/Middleware/ProcessStack.php | 4 +- .../RequestBodyBufferMiddlewareTest.php | 41 +- .../RequestBodyParserMiddlewareTest.php | 94 +-- tests/TestCase.php | 10 +- tests/benchmark-middleware-runner.php | 2 +- 94 files changed, 2329 insertions(+), 2397 deletions(-) diff --git a/README.md b/README.md index 9cdf7c09..7761a245 100644 --- a/README.md +++ b/README.md @@ -144,12 +144,12 @@ Most importantly, this project provides a [`Browser`](#browser) object that offers several methods that resemble the HTTP protocol methods: ```php -$browser->get($url, array $headers = array()); -$browser->head($url, array $headers = array()); -$browser->post($url, array $headers = array(), string|ReadableStreamInterface $body = ''); -$browser->delete($url, array $headers = array(), string|ReadableStreamInterface $body = ''); -$browser->put($url, array $headers = array(), string|ReadableStreamInterface $body = ''); -$browser->patch($url, array $headers = array(), string|ReadableStreamInterface $body = ''); +$browser->get($url, array $headers = []); +$browser->head($url, array $headers = []); +$browser->post($url, array $headers = [], string|ReadableStreamInterface $body = ''); +$browser->delete($url, array $headers = [], string|ReadableStreamInterface $body = ''); +$browser->put($url, array $headers = [], string|ReadableStreamInterface $body = ''); +$browser->patch($url, array $headers = [], string|ReadableStreamInterface $body = ''); ``` Each of these methods requires a `$url` and some optional parameters to send an @@ -285,9 +285,9 @@ like this: ```php $browser = new React\Http\Browser( new React\Socket\Connector( - array( + [ 'timeout' => 5 - ) + ] ) ); ``` @@ -323,9 +323,9 @@ $token = 'abc123'; $promise = $browser->get( 'https://example.com/api', - array( + [ 'Authorization' => 'Bearer ' . $token - ) + ] ); ``` @@ -411,10 +411,10 @@ Similarly, you can also process multiple requests concurrently and await an arra use function React\Async\await; use function React\Promise\all; -$promises = array( +$promises = [ $browser->get('http://example.com/'), $browser->get('http://www.example.org/'), -); +]; $responses = await(all($promises)); ``` @@ -540,7 +540,7 @@ You can invoke the following methods on the message body: $body->on($event, $callback); $body->eof(); $body->isReadable(); -$body->pipe(React\Stream\WritableStreamInterface $dest, array $options = array()); +$body->pipe(React\Stream\WritableStreamInterface $dest, array $options = []); $body->close(); $body->pause(); $body->resume(); @@ -575,10 +575,10 @@ Consider looking into also using [react/promise-stream](https://github.com/react The resulting streaming code could look something like this: ```php -use React\Promise\Stream; +use function React\Promise\Stream\unwrapReadable; function download(Browser $browser, string $url): React\Stream\ReadableStreamInterface { - return Stream\unwrapReadable( + return unwrapReadable( $browser->requestStreaming('GET', $url)->then(function (Psr\Http\Message\ResponseInterface $response) { return $response->getBody(); }) @@ -606,7 +606,7 @@ implementing [ReactPHP's `ReadableStreamInterface`](https://github.com/reactphp/ to the [request methods](#request-methods) like this: ```php -$browser->post($url, array(), $stream)->then(function (Psr\Http\Message\ResponseInterface $response) { +$browser->post($url, [], $stream)->then(function (Psr\Http\Message\ResponseInterface $response) { echo 'Successfully sent.'; }, function (Exception $e) { echo 'Error: ' . $e->getMessage() . PHP_EOL; @@ -623,7 +623,7 @@ Loop::addTimer(1.0, function () use ($body) { $body->end("hello world"); }); -$browser->post($url, array('Content-Length' => '11'), $body); +$browser->post($url, ['Content-Length' => '11'], $body); ``` If the streaming request body emits an `error` event or is explicitly closed @@ -645,10 +645,10 @@ protocol, such as plain HTTP and TLS-encrypted HTTPS. ```php $proxy = new Clue\React\HttpProxy\ProxyConnector('127.0.0.1:8080'); -$connector = new React\Socket\Connector(array( +$connector = new React\Socket\Connector([ 'tcp' => $proxy, 'dns' => false -)); +]); $browser = new React\Http\Browser($connector); ``` @@ -669,10 +669,10 @@ only, this can technically be used to tunnel any TCP/IP-based protocol. ```php $proxy = new Clue\React\Socks\Client('127.0.0.1:1080'); -$connector = new React\Socket\Connector(array( +$connector = new React\Socket\Connector([ 'tcp' => $proxy, 'dns' => false -)); +]); $browser = new React\Http\Browser($connector); ``` @@ -698,10 +698,10 @@ plain HTTP and TLS-encrypted HTTPS. ```php $proxy = new Clue\React\SshProxy\SshSocksConnector('alice@example.com'); -$connector = new React\Socket\Connector(array( +$connector = new React\Socket\Connector([ 'tcp' => $proxy, 'dns' => false -)); +]); $browser = new React\Http\Browser($connector); ``` @@ -931,11 +931,11 @@ using a secure TLS listen address, a certificate file and optional ```php $http = new React\Http\HttpServer($handler); -$socket = new React\Socket\SocketServer('tls://0.0.0.0:8443', array( - 'tls' => array( +$socket = new React\Socket\SocketServer('tls://0.0.0.0:8443', [ + 'tls' => [ 'local_cert' => __DIR__ . '/localhost.pem' - ) -)); + ] +]); $http->listen($socket); ``` @@ -1456,9 +1456,9 @@ $http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterf return new React\Http\Message\Response( React\Http\Message\Response::STATUS_OK, - array( + [ 'Content-Type' => 'text/plain' - ), + ], $stream ); }); @@ -1558,10 +1558,10 @@ $http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterf return new React\Http\Message\Response( React\Http\Message\Response::STATUS_OK, - array( + [ 'Content-Length' => '13', 'Content-Type' => 'text/plain', - ), + ], $stream ); }); @@ -1628,9 +1628,9 @@ a custom `Server` response header like this: $http = new React\Http\HttpServer(function (ServerRequestInterface $request) { return new React\Http\Message\Response( React\Http\Message\Response::STATUS_OK, - array( + [ 'Server' => 'PHP/3' - ) + ] ); }); ``` @@ -1643,9 +1643,9 @@ string value like this: $http = new React\Http\HttpServer(function (ServerRequestInterface $request) { return new React\Http\Message\Response( React\Http\Message\Response::STATUS_OK, - array( + [ 'Server' => '' - ) + ] ); }); ``` @@ -1658,9 +1658,9 @@ like this: $http = new React\Http\HttpServer(function (ServerRequestInterface $request) { return new React\Http\Message\Response( React\Http\Message\Response::STATUS_OK, - array( + [ 'Date' => gmdate('D, d M Y H:i:s \G\M\T') - ) + ] ); }); ``` @@ -1673,9 +1673,9 @@ like this: $http = new React\Http\HttpServer(function (ServerRequestInterface $request) { return new React\Http\Message\Response( React\Http\Message\Response::STATUS_OK, - array( + [ 'Date' => '' - ) + ] ); }); ``` @@ -1871,16 +1871,16 @@ proxy servers etc.), you can explicitly pass a custom instance of the [`ConnectorInterface`](https://github.com/reactphp/socket#connectorinterface): ```php -$connector = new React\Socket\Connector(array( +$connector = new React\Socket\Connector([ 'dns' => '127.0.0.1', - 'tcp' => array( + 'tcp' => [ 'bindto' => '192.168.10.1:0' - ), - 'tls' => array( + ], + 'tls' => [ 'verify_peer' => false, 'verify_peer_name' => false - ) -)); + ] +]); $browser = new React\Http\Browser($connector); ``` @@ -1895,7 +1895,7 @@ given event loop instance. #### get() -The `get(string $url, array $headers = array()): PromiseInterface` method can be used to +The `get(string $url, array $headers = []): PromiseInterface` method can be used to send an HTTP GET request. ```php @@ -1910,7 +1910,7 @@ See also [GET request client example](examples/01-client-get-request.php). #### post() -The `post(string $url, array $headers = array(), string|ReadableStreamInterface $body = ''): PromiseInterface` method can be used to +The `post(string $url, array $headers = [], string|ReadableStreamInterface $body = ''): PromiseInterface` method can be used to send an HTTP POST request. ```php @@ -1958,12 +1958,12 @@ Loop::addTimer(1.0, function () use ($body) { $body->end("hello world"); }); -$browser->post($url, array('Content-Length' => '11'), $body); +$browser->post($url, ['Content-Length' => '11'), $body); ``` #### head() -The `head(string $url, array $headers = array()): PromiseInterface` method can be used to +The `head(string $url, array $headers = []): PromiseInterface` method can be used to send an HTTP HEAD request. ```php @@ -1976,7 +1976,7 @@ $browser->head($url)->then(function (Psr\Http\Message\ResponseInterface $respons #### patch() -The `patch(string $url, array $headers = array(), string|ReadableStreamInterface $body = ''): PromiseInterface` method can be used to +The `patch(string $url, array $headers = [], string|ReadableStreamInterface $body = ''): PromiseInterface` method can be used to send an HTTP PATCH request. ```php @@ -2005,12 +2005,12 @@ Loop::addTimer(1.0, function () use ($body) { $body->end("hello world"); }); -$browser->patch($url, array('Content-Length' => '11'), $body); +$browser->patch($url, ['Content-Length' => '11'], $body); ``` #### put() -The `put(string $url, array $headers = array(), string|ReadableStreamInterface $body = ''): PromiseInterface` method can be used to +The `put(string $url, array $headers = [], string|ReadableStreamInterface $body = ''): PromiseInterface` method can be used to send an HTTP PUT request. ```php @@ -2041,12 +2041,12 @@ Loop::addTimer(1.0, function () use ($body) { $body->end("hello world"); }); -$browser->put($url, array('Content-Length' => '11'), $body); +$browser->put($url, ['Content-Length' => '11'], $body); ``` #### delete() -The `delete(string $url, array $headers = array(), string|ReadableStreamInterface $body = ''): PromiseInterface` method can be used to +The `delete(string $url, array $headers = [], string|ReadableStreamInterface $body = ''): PromiseInterface` method can be used to send an HTTP DELETE request. ```php @@ -2059,7 +2059,7 @@ $browser->delete($url)->then(function (Psr\Http\Message\ResponseInterface $respo #### request() -The `request(string $method, string $url, array $headers = array(), string|ReadableStreamInterface $body = ''): PromiseInterface` method can be used to +The `request(string $method, string $url, array $headers = [], string|ReadableStreamInterface $body = ''): PromiseInterface` method can be used to send an arbitrary HTTP request. The preferred way to send an HTTP request is by using the above @@ -2093,12 +2093,12 @@ Loop::addTimer(1.0, function () use ($body) { $body->end("hello world"); }); -$browser->request('POST', $url, array('Content-Length' => '11'), $body); +$browser->request('POST', $url, ['Content-Length' => '11'], $body); ``` #### requestStreaming() -The `requestStreaming(string $method, string $url, array $headers = array(), string|ReadableStreamInterface $body = ''): PromiseInterface` method can be used to +The `requestStreaming(string $method, string $url, array $headers = [], string|ReadableStreamInterface $body = ''): PromiseInterface` method can be used to send an arbitrary HTTP request and receive a streaming response without buffering the response body. The preferred way to send an HTTP request is by using the above @@ -2157,7 +2157,7 @@ Loop::addTimer(1.0, function () use ($body) { $body->end("hello world"); }); -$browser->requestStreaming('POST', $url, array('Content-Length' => '11'), $body); +$browser->requestStreaming('POST', $url, ['Content-Length' => '11'], $body); ``` #### withTimeout() @@ -2428,9 +2428,9 @@ represent an outgoing server response message. ```php $response = new React\Http\Message\Response( React\Http\Message\Response::STATUS_OK, - array( + [ 'Content-Type' => 'text/html' - ), + ], "Hello world!\n" ); ``` @@ -2528,10 +2528,9 @@ values in the data must be encoded in UTF-8 (Unicode). If the encoding fails, this method will throw an `InvalidArgumentException`. By default, the given structured data will be encoded with the flags as -shown above. This includes pretty printing (PHP 5.4+) and preserving -zero fractions for `float` values (PHP 5.6.6+) to ease debugging. It is -assumed any additional data overhead is usually compensated by using HTTP -response compression. +shown above. This includes pretty printing and preserving zero fractions +for `float` values to ease debugging. It is assumed any additional data +overhead is usually compensated by using HTTP response compression. If you want to use a different status code or custom HTTP response headers, you can manipulate the returned response object using the @@ -2900,9 +2899,9 @@ $handler = function (Psr\Http\Message\ServerRequestInterface $request) { return new React\Http\Message\Response( React\Http\Message\Response::STATUS_OK, - array( + [ 'Content-Type' => 'text/plain' - ), + ], $name . ' uploaded ' . $uploaded ); }; diff --git a/examples/03-client-request-any.php b/examples/03-client-request-any.php index d7558bd6..9ee80131 100644 --- a/examples/03-client-request-any.php +++ b/examples/03-client-request-any.php @@ -10,13 +10,13 @@ $client = new Browser(); -$promises = array( +$promises = [ $client->head('http://www.github.com/clue/http-react'), $client->get('https://httpbingo.org/'), $client->get('https://google.com'), $client->get('http://www.lueck.tv/psocksd'), $client->get('http://httpbingo.org/absolute-redirect/5') -); +]; React\Promise\any($promises)->then(function (ResponseInterface $response) use ($promises) { // first response arrived => cancel all other pending requests diff --git a/examples/04-client-post-json.php b/examples/04-client-post-json.php index 18fa596d..2ecc0636 100644 --- a/examples/04-client-post-json.php +++ b/examples/04-client-post-json.php @@ -7,19 +7,19 @@ $client = new Browser(); -$data = array( - 'name' => array( +$data = [ + 'name' => [ 'first' => 'Alice', 'name' => 'Smith' - ), + ], 'email' => 'alice@example.com' -); +]; $client->post( 'https://httpbingo.org/post', - array( + [ 'Content-Type' => 'application/json' - ), + ], json_encode($data) )->then(function (ResponseInterface $response) { echo (string) $response->getBody(); diff --git a/examples/05-client-put-xml.php b/examples/05-client-put-xml.php index 10ee46fc..af01c47a 100644 --- a/examples/05-client-put-xml.php +++ b/examples/05-client-put-xml.php @@ -14,9 +14,9 @@ $client->put( 'https://httpbingo.org/put', - array( + [ 'Content-Type' => 'text/xml' - ), + ], $xml->asXML() )->then(function (ResponseInterface $response) { echo (string) $response->getBody(); diff --git a/examples/11-client-http-proxy.php b/examples/11-client-http-proxy.php index ec7fc2b6..f15cf2a0 100644 --- a/examples/11-client-http-proxy.php +++ b/examples/11-client-http-proxy.php @@ -16,10 +16,10 @@ $proxy = new Clue\React\HttpProxy\ProxyConnector(getenv('http_proxy') ?: '127.0.0.1:8080'); // create a Browser object that uses the HTTP CONNECT proxy client for connections -$connector = new Connector(array( +$connector = new Connector([ 'tcp' => $proxy, 'dns' => false -)); +]); $browser = new Browser($connector); diff --git a/examples/12-client-socks-proxy.php b/examples/12-client-socks-proxy.php index 8c525509..0e0039ca 100644 --- a/examples/12-client-socks-proxy.php +++ b/examples/12-client-socks-proxy.php @@ -16,10 +16,10 @@ $proxy = new Clue\React\Socks\Client(getenv('socks_proxy') ?: '127.0.0.1:1080'); // create a Browser object that uses the SOCKS proxy client for connections -$connector = new Connector(array( +$connector = new Connector([ 'tcp' => $proxy, 'dns' => false -)); +]); $browser = new Browser($connector); diff --git a/examples/13-client-ssh-proxy.php b/examples/13-client-ssh-proxy.php index 93e6e256..e387d4fc 100644 --- a/examples/13-client-ssh-proxy.php +++ b/examples/13-client-ssh-proxy.php @@ -12,10 +12,10 @@ $proxy = new Clue\React\SshProxy\SshSocksConnector(getenv('ssh_proxy') ?: 'alice@localhost'); // create a Browser object that uses the SSH proxy client for connections -$connector = new Connector(array( +$connector = new Connector([ 'tcp' => $proxy, 'dns' => false -)); +]); $browser = new Browser($connector); diff --git a/examples/21-client-request-streaming-to-stdout.php b/examples/21-client-request-streaming-to-stdout.php index b3cbbe39..47c2371f 100644 --- a/examples/21-client-request-streaming-to-stdout.php +++ b/examples/21-client-request-streaming-to-stdout.php @@ -17,7 +17,7 @@ $out = new WritableResourceStream(STDOUT); $info = new WritableResourceStream(STDERR); -$url = isset($argv[1]) ? $argv[1] : 'http://google.com/'; +$url = $argv[1] ?? 'http://google.com/'; $info->write('Requesting ' . $url . '…' . PHP_EOL); $client->requestStreaming('GET', $url)->then(function (ResponseInterface $response) use ($info, $out) { diff --git a/examples/22-client-stream-upload-from-stdin.php b/examples/22-client-stream-upload-from-stdin.php index f0a68c5f..438b6280 100644 --- a/examples/22-client-stream-upload-from-stdin.php +++ b/examples/22-client-stream-upload-from-stdin.php @@ -15,10 +15,10 @@ $in = new ReadableResourceStream(STDIN); -$url = isset($argv[1]) ? $argv[1] : 'https://httpbingo.org/post'; +$url = $argv[1] ?? 'https://httpbingo.org/post'; echo 'Sending STDIN as POST to ' . $url . '…' . PHP_EOL; -$client->post($url, array('Content-Type' => 'text/plain'), $in)->then(function (ResponseInterface $response) { +$client->post($url, ['Content-Type' => 'text/plain'], $in)->then(function (ResponseInterface $response) { echo (string) $response->getBody(); }, function (Exception $e) { echo 'Error: ' . $e->getMessage() . PHP_EOL; diff --git a/examples/51-server-hello-world.php b/examples/51-server-hello-world.php index 9ff84eee..e25efc65 100644 --- a/examples/51-server-hello-world.php +++ b/examples/51-server-hello-world.php @@ -8,7 +8,7 @@ ); }); -$socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$socket = new React\Socket\SocketServer($argv[1] ?? '0.0.0.0:0'); $http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/52-server-count-visitors.php b/examples/52-server-count-visitors.php index 341f9498..333a8011 100644 --- a/examples/52-server-count-visitors.php +++ b/examples/52-server-count-visitors.php @@ -9,7 +9,7 @@ ); }); -$socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$socket = new React\Socket\SocketServer($argv[1] ?? '0.0.0.0:0'); $http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/53-server-whatsmyip.php b/examples/53-server-whatsmyip.php index 1e394b9e..d9018a64 100644 --- a/examples/53-server-whatsmyip.php +++ b/examples/53-server-whatsmyip.php @@ -10,7 +10,7 @@ ); }); -$socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$socket = new React\Socket\SocketServer($argv[1] ?? '0.0.0.0:0'); $http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/54-server-query-parameter.php b/examples/54-server-query-parameter.php index 9b2d5749..507e8862 100644 --- a/examples/54-server-query-parameter.php +++ b/examples/54-server-query-parameter.php @@ -17,7 +17,7 @@ ); }); -$socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$socket = new React\Socket\SocketServer($argv[1] ?? '0.0.0.0:0'); $http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/55-server-cookie-handling.php b/examples/55-server-cookie-handling.php index b5e68862..b2b62100 100644 --- a/examples/55-server-cookie-handling.php +++ b/examples/55-server-cookie-handling.php @@ -18,7 +18,7 @@ )->withHeader('Set-Cookie', $key . '=' . urlencode('Hello world!')); }); -$socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$socket = new React\Socket\SocketServer($argv[1] ?? '0.0.0.0:0'); $http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/56-server-sleep.php b/examples/56-server-sleep.php index 2a3c9027..45f64149 100644 --- a/examples/56-server-sleep.php +++ b/examples/56-server-sleep.php @@ -18,7 +18,7 @@ }); }); -$socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$socket = new React\Socket\SocketServer($argv[1] ?? '0.0.0.0:0'); $http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/57-server-error-handling.php b/examples/57-server-error-handling.php index a9fb6bad..c5141161 100644 --- a/examples/57-server-error-handling.php +++ b/examples/57-server-error-handling.php @@ -15,7 +15,7 @@ ); }); -$socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$socket = new React\Socket\SocketServer($argv[1] ?? '0.0.0.0:0'); $http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/58-server-stream-response.php b/examples/58-server-stream-response.php index 9d12461a..d99b1548 100644 --- a/examples/58-server-stream-response.php +++ b/examples/58-server-stream-response.php @@ -32,14 +32,14 @@ return new Response( Response::STATUS_OK, - array( + [ 'Content-Type' => 'text/plain' - ), + ], $stream ); }); -$socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$socket = new React\Socket\SocketServer($argv[1] ?? '0.0.0.0:0'); $http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/59-server-json-api.php b/examples/59-server-json-api.php index f48be7e3..7e7477c0 100644 --- a/examples/59-server-json-api.php +++ b/examples/59-server-json-api.php @@ -12,30 +12,30 @@ $http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { if ($request->getHeaderLine('Content-Type') !== 'application/json') { - return Response::json( - array('error' => 'Only supports application/json') - )->withStatus(Response::STATUS_UNSUPPORTED_MEDIA_TYPE); + return Response::json([ + 'error' => 'Only supports application/json' + ])->withStatus(Response::STATUS_UNSUPPORTED_MEDIA_TYPE); } $input = json_decode($request->getBody()->getContents()); if (json_last_error() !== JSON_ERROR_NONE) { - return Response::json( - array('error' => 'Invalid JSON data given') - )->withStatus(Response::STATUS_BAD_REQUEST); + return Response::json([ + 'error' => 'Invalid JSON data given' + ])->withStatus(Response::STATUS_BAD_REQUEST); } if (!isset($input->name) || !is_string($input->name)) { - return Response::json( - array('error' => 'JSON data does not contain a string "name" property') - )->withStatus(Response::STATUS_UNPROCESSABLE_ENTITY); + return Response::json([ + 'error' => 'JSON data does not contain a string "name" property' + ])->withStatus(Response::STATUS_UNPROCESSABLE_ENTITY); } - return Response::json( - array('message' => 'Hello ' . $input->name) - ); + return Response::json([ + 'message' => 'Hello ' . $input->name + ]); }); -$socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$socket = new React\Socket\SocketServer($argv[1] ?? '0.0.0.0:0'); $http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/61-server-hello-world-https.php b/examples/61-server-hello-world-https.php index 23906430..8ce487c7 100644 --- a/examples/61-server-hello-world-https.php +++ b/examples/61-server-hello-world-https.php @@ -8,12 +8,12 @@ ); }); -$uri = 'tls://' . (isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); -$socket = new React\Socket\SocketServer($uri, array( - 'tls' => array( - 'local_cert' => isset($argv[2]) ? $argv[2] : __DIR__ . '/localhost.pem' - ) -)); +$uri = 'tls://' . ($argv[1] ?? '0.0.0.0:0'); +$socket = new React\Socket\SocketServer($uri, [ + 'tls' => [ + 'local_cert' => $argv[2] ?? __DIR__ . '/localhost.pem' + ] +]); $http->listen($socket); $socket->on('error', function (Exception $e) { diff --git a/examples/62-server-form-upload.php b/examples/62-server-form-upload.php index 52864c82..9c5c8aa3 100644 --- a/examples/62-server-form-upload.php +++ b/examples/62-server-form-upload.php @@ -124,7 +124,7 @@ $handler ); -$socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$socket = new React\Socket\SocketServer($argv[1] ?? '0.0.0.0:0'); $http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/63-server-streaming-request.php b/examples/63-server-streaming-request.php index fef6f008..8ed3a4e1 100644 --- a/examples/63-server-streaming-request.php +++ b/examples/63-server-streaming-request.php @@ -38,7 +38,7 @@ function (Psr\Http\Message\ServerRequestInterface $request) { echo 'Error: ' . $e->getMessage() . PHP_EOL; }); -$socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$socket = new React\Socket\SocketServer($argv[1] ?? '0.0.0.0:0'); $http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/71-server-http-proxy.php b/examples/71-server-http-proxy.php index de9fa10b..d513ede2 100644 --- a/examples/71-server-http-proxy.php +++ b/examples/71-server-http-proxy.php @@ -32,7 +32,7 @@ ); }); -$socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$socket = new React\Socket\SocketServer($argv[1] ?? '0.0.0.0:0'); $http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/72-server-http-connect-proxy.php b/examples/72-server-http-connect-proxy.php index 0500822a..98a21f34 100644 --- a/examples/72-server-http-connect-proxy.php +++ b/examples/72-server-http-connect-proxy.php @@ -20,10 +20,10 @@ if ($request->getMethod() !== 'CONNECT') { return new Response( Response::STATUS_METHOD_NOT_ALLOWED, - array( + [ 'Content-Type' => 'text/plain', 'Allow' => 'CONNECT' - ), + ], 'This is an HTTP CONNECT (secure HTTPS) proxy' ); } @@ -34,23 +34,23 @@ function (ConnectionInterface $remote) { // connection established => forward data return new Response( Response::STATUS_OK, - array(), + [], $remote ); }, function (Exception $e) { return new Response( Response::STATUS_BAD_GATEWAY, - array( + [ 'Content-Type' => 'text/plain' - ), + ], 'Unable to connect: ' . $e->getMessage() ); } ); }); -$socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$socket = new React\Socket\SocketServer($argv[1] ?? '0.0.0.0:0'); $http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/81-server-upgrade-echo.php b/examples/81-server-upgrade-echo.php index cd3dc156..fbc4d75a 100644 --- a/examples/81-server-upgrade-echo.php +++ b/examples/81-server-upgrade-echo.php @@ -31,9 +31,9 @@ if ($request->getHeaderLine('Upgrade') !== 'echo' || $request->getProtocolVersion() === '1.0') { return new Response( Response::STATUS_UPGRADE_REQUIRED, - array( + [ 'Upgrade' => 'echo' - ), + ], '"Upgrade: echo" required' ); } @@ -49,14 +49,14 @@ return new Response( Response::STATUS_SWITCHING_PROTOCOLS, - array( + [ 'Upgrade' => 'echo' - ), + ], $stream ); }); -$socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$socket = new React\Socket\SocketServer($argv[1] ?? '0.0.0.0:0'); $http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/82-server-upgrade-chat.php b/examples/82-server-upgrade-chat.php index bd791fb0..00788922 100644 --- a/examples/82-server-upgrade-chat.php +++ b/examples/82-server-upgrade-chat.php @@ -39,9 +39,9 @@ if ($request->getHeaderLine('Upgrade') !== 'chat' || $request->getProtocolVersion() === '1.0') { return new Response( Response::STATUS_UPGRADE_REQUIRED, - array( + [ 'Upgrade' => 'chat' - ), + ], '"Upgrade: chat" required' ); } @@ -77,14 +77,14 @@ return new Response( Response::STATUS_SWITCHING_PROTOCOLS, - array( + [ 'Upgrade' => 'chat' - ), + ], $stream ); }); -$socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$socket = new React\Socket\SocketServer($argv[1] ?? '0.0.0.0:0'); $http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/91-client-benchmark-download.php b/examples/91-client-benchmark-download.php index 712d9f10..f74b8925 100644 --- a/examples/91-client-benchmark-download.php +++ b/examples/91-client-benchmark-download.php @@ -16,7 +16,7 @@ use React\Http\Browser; use React\Stream\ReadableStreamInterface; -$url = isset($argv[1]) ? $argv[1] : 'http://google.com/'; +$url = $argv[1] ?? 'http://google.com/'; require __DIR__ . '/../vendor/autoload.php'; diff --git a/examples/92-client-benchmark-upload.php b/examples/92-client-benchmark-upload.php index 9fa1848a..10434bfd 100644 --- a/examples/92-client-benchmark-upload.php +++ b/examples/92-client-benchmark-upload.php @@ -55,7 +55,7 @@ public function resume() $this->paused = false; while ($this->position < $this->count && !$this->paused) { ++$this->position; - $this->emit('data', array($this->chunk)); + $this->emit('data', [$this->chunk]); } // end once the last chunk has been written @@ -65,7 +65,7 @@ public function resume() } } - public function pipe(WritableStreamInterface $dest, array $options = array()) + public function pipe(WritableStreamInterface $dest, array $options = []) { return Util::pipe($this, $dest, $options); } @@ -95,8 +95,8 @@ public function getPosition() $client = new Browser(); -$url = isset($argv[1]) ? $argv[1] : 'http://httpbin.org/post'; -$n = isset($argv[2]) ? $argv[2] : 10; +$url = $argv[1] ?? 'http://httpbin.org/post'; +$n = $argv[2] ?? 10; $source = new ChunkRepeater(str_repeat('x', 1000000), $n); Loop::futureTick(function () use ($source) { $source->resume(); @@ -109,7 +109,7 @@ public function getPosition() printf("\r%d bytes in %0.3fs...", $source->getPosition(), microtime(true) - $start); }); -$client->post($url, array('Content-Length' => $n * 1000000), $source)->then(function (ResponseInterface $response) use ($source, $report, $start) { +$client->post($url, ['Content-Length' => $n * 1000000], $source)->then(function (ResponseInterface $response) use ($source, $report, $start) { $now = microtime(true); Loop::cancelTimer($report); diff --git a/examples/99-server-benchmark-download.php b/examples/99-server-benchmark-download.php index ddd4760a..ee1cfc8f 100644 --- a/examples/99-server-benchmark-download.php +++ b/examples/99-server-benchmark-download.php @@ -52,7 +52,7 @@ public function resume() $this->paused = false; while ($this->position < $this->count && !$this->paused) { ++$this->position; - $this->emit('data', array($this->chunk)); + $this->emit('data', [$this->chunk]); } // end once the last chunk has been written @@ -62,7 +62,7 @@ public function resume() } } - public function pipe(WritableStreamInterface $dest, array $options = array()) + public function pipe(WritableStreamInterface $dest, array $options = []) { return; } @@ -106,19 +106,19 @@ public function getSize() return new Response(Response::STATUS_NOT_FOUND); } - React\EventLoop\Loop::addTimer(0, array($stream, 'resume')); + React\EventLoop\Loop::addTimer(0, [$stream, 'resume']); return new Response( Response::STATUS_OK, - array( + [ 'Content-Type' => 'application/octet-data', 'Content-Length' => $stream->getSize() - ), + ], $stream ); }); -$socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$socket = new React\Socket\SocketServer($argv[1] ?? '0.0.0.0:0'); $http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/src/Browser.php b/src/Browser.php index a24d24e7..06e194d9 100644 --- a/src/Browser.php +++ b/src/Browser.php @@ -22,9 +22,9 @@ class Browser private $transaction; private $baseUrl; private $protocolVersion = '1.1'; - private $defaultHeaders = array( + private $defaultHeaders = [ 'User-Agent' => 'ReactPHP/1' - ); + ]; /** * The `Browser` is responsible for sending HTTP requests to your HTTP server @@ -45,16 +45,16 @@ class Browser * [`ConnectorInterface`](https://github.com/reactphp/socket#connectorinterface): * * ```php - * $connector = new React\Socket\Connector(array( + * $connector = new React\Socket\Connector([ * 'dns' => '127.0.0.1', - * 'tcp' => array( + * 'tcp' => [ * 'bindto' => '192.168.10.1:0' - * ), - * 'tls' => array( + * ], + * 'tls' => [ * 'verify_peer' => false, * 'verify_peer_name' => false - * ) - * )); + * ] + * ]); * * $browser = new React\Http\Browser($connector); * ``` @@ -70,7 +70,7 @@ class Browser */ public function __construct(ConnectorInterface $connector = null, LoopInterface $loop = null) { - $loop = $loop ?: Loop::get(); + $loop = $loop ?? Loop::get(); $this->transaction = new Transaction( Sender::createFromLoop($loop, $connector), $loop @@ -94,7 +94,7 @@ public function __construct(ConnectorInterface $connector = null, LoopInterface * @param array $headers * @return PromiseInterface */ - public function get($url, array $headers = array()) + public function get($url, array $headers = []) { return $this->requestMayBeStreaming('GET', $url, $headers); } @@ -147,7 +147,7 @@ public function get($url, array $headers = array()) * $body->end("hello world"); * }); * - * $browser->post($url, array('Content-Length' => '11'), $body); + * $browser->post($url, ['Content-Length' => '11'], $body); * ``` * * @param string $url URL for the request. @@ -155,7 +155,7 @@ public function get($url, array $headers = array()) * @param string|ReadableStreamInterface $body * @return PromiseInterface */ - public function post($url, array $headers = array(), $body = '') + public function post($url, array $headers = [], $body = '') { return $this->requestMayBeStreaming('POST', $url, $headers, $body); } @@ -175,7 +175,7 @@ public function post($url, array $headers = array(), $body = '') * @param array $headers * @return PromiseInterface */ - public function head($url, array $headers = array()) + public function head($url, array $headers = []) { return $this->requestMayBeStreaming('HEAD', $url, $headers); } @@ -209,7 +209,7 @@ public function head($url, array $headers = array()) * $body->end("hello world"); * }); * - * $browser->patch($url, array('Content-Length' => '11'), $body); + * $browser->patch($url, ['Content-Length' => '11'], $body); * ``` * * @param string $url URL for the request. @@ -217,7 +217,7 @@ public function head($url, array $headers = array()) * @param string|ReadableStreamInterface $body * @return PromiseInterface */ - public function patch($url, array $headers = array(), $body = '') + public function patch($url, array $headers = [], $body = '') { return $this->requestMayBeStreaming('PATCH', $url , $headers, $body); } @@ -253,7 +253,7 @@ public function patch($url, array $headers = array(), $body = '') * $body->end("hello world"); * }); * - * $browser->put($url, array('Content-Length' => '11'), $body); + * $browser->put($url, ['Content-Length' => '11'], $body); * ``` * * @param string $url URL for the request. @@ -261,7 +261,7 @@ public function patch($url, array $headers = array(), $body = '') * @param string|ReadableStreamInterface $body * @return PromiseInterface */ - public function put($url, array $headers = array(), $body = '') + public function put($url, array $headers = [], $body = '') { return $this->requestMayBeStreaming('PUT', $url, $headers, $body); } @@ -282,7 +282,7 @@ public function put($url, array $headers = array(), $body = '') * @param string|ReadableStreamInterface $body * @return PromiseInterface */ - public function delete($url, array $headers = array(), $body = '') + public function delete($url, array $headers = [], $body = '') { return $this->requestMayBeStreaming('DELETE', $url, $headers, $body); } @@ -321,7 +321,7 @@ public function delete($url, array $headers = array(), $body = '') * $body->end("hello world"); * }); * - * $browser->request('POST', $url, array('Content-Length' => '11'), $body); + * $browser->request('POST', $url, ['Content-Length' => '11'], $body); * ``` * * @param string $method HTTP request method, e.g. GET/HEAD/POST etc. @@ -330,9 +330,9 @@ public function delete($url, array $headers = array(), $body = '') * @param string|ReadableStreamInterface $body HTTP request body contents * @return PromiseInterface */ - public function request($method, $url, array $headers = array(), $body = '') + public function request($method, $url, array $headers = [], $body = '') { - return $this->withOptions(array('streaming' => false))->requestMayBeStreaming($method, $url, $headers, $body); + return $this->withOptions(['streaming' => false])->requestMayBeStreaming($method, $url, $headers, $body); } /** @@ -394,7 +394,7 @@ public function request($method, $url, array $headers = array(), $body = '') * $body->end("hello world"); * }); * - * $browser->requestStreaming('POST', $url, array('Content-Length' => '11'), $body); + * $browser->requestStreaming('POST', $url, ['Content-Length' => '11'], $body); * ``` * * @param string $method HTTP request method, e.g. GET/HEAD/POST etc. @@ -403,9 +403,9 @@ public function request($method, $url, array $headers = array(), $body = '') * @param string|ReadableStreamInterface $body HTTP request body contents * @return PromiseInterface */ - public function requestStreaming($method, $url, $headers = array(), $body = '') + public function requestStreaming($method, $url, $headers = [], $body = '') { - return $this->withOptions(array('streaming' => true))->requestMayBeStreaming($method, $url, $headers, $body); + return $this->withOptions(['streaming' => true])->requestMayBeStreaming($method, $url, $headers, $body); } /** @@ -450,9 +450,9 @@ public function withTimeout($timeout) $timeout = 0; } - return $this->withOptions(array( + return $this->withOptions([ 'timeout' => $timeout, - )); + ]); } /** @@ -512,10 +512,10 @@ public function withTimeout($timeout) */ public function withFollowRedirects($followRedirects) { - return $this->withOptions(array( + return $this->withOptions([ 'followRedirects' => $followRedirects !== false, 'maxRedirects' => \is_bool($followRedirects) ? null : $followRedirects - )); + ]); } /** @@ -566,9 +566,9 @@ public function withFollowRedirects($followRedirects) */ public function withRejectErrorResponse($obeySuccessCode) { - return $this->withOptions(array( + return $this->withOptions([ 'obeySuccessCode' => $obeySuccessCode, - )); + ]); } /** @@ -618,7 +618,7 @@ public function withBase($baseUrl) } $browser->baseUrl = new Uri($baseUrl); - if (!\in_array($browser->baseUrl->getScheme(), array('http', 'https')) || $browser->baseUrl->getHost() === '') { + if (!\in_array($browser->baseUrl->getScheme(), ['http', 'https']) || $browser->baseUrl->getHost() === '') { throw new \InvalidArgumentException('Base URL must be absolute'); } @@ -653,7 +653,7 @@ public function withBase($baseUrl) */ public function withProtocolVersion($protocolVersion) { - if (!\in_array($protocolVersion, array('1.0', '1.1'), true)) { + if (!\in_array($protocolVersion, ['1.0', '1.1'], true)) { throw new InvalidArgumentException('Invalid HTTP protocol version, must be one of "1.1" or "1.0"'); } @@ -706,9 +706,9 @@ public function withProtocolVersion($protocolVersion) */ public function withResponseBuffer($maximumSize) { - return $this->withOptions(array( + return $this->withOptions([ 'maximumSize' => $maximumSize - )); + ]); } /** @@ -777,13 +777,13 @@ public function withoutHeader($header) * * ```php * // deprecated - * $newBrowser = $browser->withOptions(array( + * $newBrowser = $browser->withOptions([ * 'timeout' => null, // see withTimeout() instead * 'followRedirects' => true, // see withFollowRedirects() instead * 'maxRedirects' => 10, // see withFollowRedirects() instead * 'obeySuccessCode' => true, // see withRejectErrorResponse() instead * 'streaming' => false, // deprecated, see requestStreaming() instead - * )); + * ]); * ``` * * See also [timeouts](#timeouts), [redirects](#redirects) and @@ -814,7 +814,7 @@ private function withOptions(array $options) * @param string|ReadableStreamInterface $body * @return PromiseInterface */ - private function requestMayBeStreaming($method, $url, array $headers = array(), $body = '') + private function requestMayBeStreaming($method, $url, array $headers = [], $body = '') { if ($this->baseUrl !== null) { // ensure we're actually below the base URL diff --git a/src/HttpServer.php b/src/HttpServer.php index cd0874cc..24168cc5 100644 --- a/src/HttpServer.php +++ b/src/HttpServer.php @@ -27,9 +27,9 @@ * $http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { * return new React\Http\Message\Response( * React\Http\Message\Response::STATUS_OK, - * array( + * [ * 'Content-Type' => 'text/plain' - * ), + * ], * "Hello World!\n" * ); * }); @@ -222,7 +222,7 @@ public function __construct($requestHandlerOrLoop) } } - $middleware = array(); + $middleware = []; if (!$streaming) { $maxSize = $this->getMaxRequestSize(); $concurrency = $this->getConcurrentRequestsLimit(\ini_get('memory_limit'), $maxSize); @@ -253,9 +253,8 @@ public function __construct($requestHandlerOrLoop) $this->streamingServer = new StreamingServer($loop, new MiddlewareRunner($middleware)); - $that = $this; - $this->streamingServer->on('error', function ($error) use ($that) { - $that->emit('error', array($error)); + $this->streamingServer->on('error', function ($error) { + $this->emit('error', [$error]); }); } @@ -299,11 +298,11 @@ public function __construct($requestHandlerOrLoop) * ```php * $http = new React\Http\HttpServer($handler); * - * $socket = new React\Socket\SocketServer('tls://0.0.0.0:8443', array( - * 'tls' => array( + * $socket = new React\Socket\SocketServer('tls://0.0.0.0:8443', [ + * 'tls' => [ * 'local_cert' => __DIR__ . '/localhost.pem' - * ) - * )); + * ] + * ]); * $http->listen($socket); * ``` * @@ -340,7 +339,7 @@ private function getConcurrentRequestsLimit($memory_limit, $post_max_size) */ private function getMaxRequestSize($post_max_size = null) { - $maxSize = IniUtil::iniSizeToBytes($post_max_size === null ? \ini_get('post_max_size') : $post_max_size); + $maxSize = IniUtil::iniSizeToBytes($post_max_size ?? \ini_get('post_max_size')); return ($maxSize === 0 || $maxSize >= self::MAXIMUM_BUFFER_SIZE) ? self::MAXIMUM_BUFFER_SIZE : $maxSize; } diff --git a/src/Io/AbstractMessage.php b/src/Io/AbstractMessage.php index a0706bb1..232a5442 100644 --- a/src/Io/AbstractMessage.php +++ b/src/Io/AbstractMessage.php @@ -22,10 +22,10 @@ abstract class AbstractMessage implements MessageInterface const REGEX_HEADERS = '/^([^()<>@,;:\\\"\/\[\]?={}\x00-\x20\x7F]++):[\x20\x09]*+((?:[\x20\x09]*+[\x21-\x7E\x80-\xFF]++)*+)[\x20\x09]*+[\r]?+\n/m'; /** @var array */ - private $headers = array(); + private $headers = []; /** @var array */ - private $headerNamesLowerCase = array(); + private $headerNamesLowerCase = []; /** @var string */ private $protocolVersion; @@ -41,13 +41,13 @@ abstract class AbstractMessage implements MessageInterface protected function __construct($protocolVersion, array $headers, StreamInterface $body) { foreach ($headers as $name => $value) { - if ($value !== array()) { + if ($value !== []) { if (\is_array($value)) { foreach ($value as &$one) { $one = (string) $one; } } else { - $value = array((string) $value); + $value = [(string) $value]; } $lower = \strtolower($name); @@ -95,7 +95,7 @@ public function hasHeader($name) public function getHeader($name) { $lower = \strtolower($name); - return isset($this->headerNamesLowerCase[$lower]) ? $this->headers[$this->headerNamesLowerCase[$lower]] : array(); + return isset($this->headerNamesLowerCase[$lower]) ? $this->headers[$this->headerNamesLowerCase[$lower]] : []; } public function getHeaderLine($name) @@ -105,14 +105,14 @@ public function getHeaderLine($name) public function withHeader($name, $value) { - if ($value === array()) { + if ($value === []) { return $this->withoutHeader($name); } elseif (\is_array($value)) { foreach ($value as &$one) { $one = (string) $one; } } else { - $value = array((string) $value); + $value = [(string) $value]; } $lower = \strtolower($name); @@ -133,11 +133,11 @@ public function withHeader($name, $value) public function withAddedHeader($name, $value) { - if ($value === array()) { + if ($value === []) { return $this; } - return $this->withHeader($name, \array_merge($this->getHeader($name), \is_array($value) ? $value : array($value))); + return $this->withHeader($name, \array_merge($this->getHeader($name), \is_array($value) ? $value : [$value])); } public function withoutHeader($name) diff --git a/src/Io/AbstractRequest.php b/src/Io/AbstractRequest.php index f32307f7..1182f7ab 100644 --- a/src/Io/AbstractRequest.php +++ b/src/Io/AbstractRequest.php @@ -50,7 +50,7 @@ protected function __construct( $host = $uri->getHost(); if ($host !== '') { foreach ($headers as $name => $value) { - if (\strtolower($name) === 'host' && $value !== array()) { + if (\strtolower($name) === 'host' && $value !== []) { $host = ''; break; } @@ -61,7 +61,7 @@ protected function __construct( $host .= ':' . $port; } - $headers = array('Host' => $host) + $headers; + $headers = ['Host' => $host] + $headers; } } diff --git a/src/Io/BufferedBody.php b/src/Io/BufferedBody.php index 4a4d8393..9b1d9887 100644 --- a/src/Io/BufferedBody.php +++ b/src/Io/BufferedBody.php @@ -174,6 +174,6 @@ public function getContents() public function getMetadata($key = null) { - return $key === null ? array() : null; + return $key === null ? [] : null; } } diff --git a/src/Io/ChunkedDecoder.php b/src/Io/ChunkedDecoder.php index 2f58f42b..996484db 100644 --- a/src/Io/ChunkedDecoder.php +++ b/src/Io/ChunkedDecoder.php @@ -31,10 +31,10 @@ public function __construct(ReadableStreamInterface $input) { $this->input = $input; - $this->input->on('data', array($this, 'handleData')); - $this->input->on('end', array($this, 'handleEnd')); - $this->input->on('error', array($this, 'handleError')); - $this->input->on('close', array($this, 'close')); + $this->input->on('data', [$this, 'handleData']); + $this->input->on('end', [$this, 'handleEnd']); + $this->input->on('error', [$this, 'handleError']); + $this->input->on('close', [$this, 'close']); } public function isReadable() @@ -52,7 +52,7 @@ public function resume() $this->input->resume(); } - public function pipe(WritableStreamInterface $dest, array $options = array()) + public function pipe(WritableStreamInterface $dest, array $options = []) { Util::pipe($this, $dest, $options); @@ -86,7 +86,7 @@ public function handleEnd() /** @internal */ public function handleError(Exception $e) { - $this->emit('error', array($e)); + $this->emit('error', [$e]); $this->close(); } @@ -139,7 +139,7 @@ public function handleData($data) if ($chunk !== '') { $this->transferredSize += \strlen($chunk); - $this->emit('data', array($chunk)); + $this->emit('data', [$chunk]); $this->buffer = (string)\substr($this->buffer, \strlen($chunk)); } diff --git a/src/Io/ChunkedEncoder.php b/src/Io/ChunkedEncoder.php index c84ef54f..0bfe34f8 100644 --- a/src/Io/ChunkedEncoder.php +++ b/src/Io/ChunkedEncoder.php @@ -23,10 +23,10 @@ public function __construct(ReadableStreamInterface $input) { $this->input = $input; - $this->input->on('data', array($this, 'handleData')); - $this->input->on('end', array($this, 'handleEnd')); - $this->input->on('error', array($this, 'handleError')); - $this->input->on('close', array($this, 'close')); + $this->input->on('data', [$this, 'handleData']); + $this->input->on('end', [$this, 'handleEnd']); + $this->input->on('error', [$this, 'handleError']); + $this->input->on('close', [$this, 'close']); } public function isReadable() @@ -44,7 +44,7 @@ public function resume() $this->input->resume(); } - public function pipe(WritableStreamInterface $dest, array $options = array()) + public function pipe(WritableStreamInterface $dest, array $options = []) { return Util::pipe($this, $dest, $options); } @@ -66,23 +66,23 @@ public function close() public function handleData($data) { if ($data !== '') { - $this->emit('data', array( + $this->emit('data', [ \dechex(\strlen($data)) . "\r\n" . $data . "\r\n" - )); + ]); } } /** @internal */ public function handleError(\Exception $e) { - $this->emit('error', array($e)); + $this->emit('error', [$e]); $this->close(); } /** @internal */ public function handleEnd() { - $this->emit('data', array("0\r\n\r\n")); + $this->emit('data', ["0\r\n\r\n"]); if (!$this->closed) { $this->emit('end'); diff --git a/src/Io/ClientConnectionManager.php b/src/Io/ClientConnectionManager.php index faac98b6..794c0340 100644 --- a/src/Io/ClientConnectionManager.php +++ b/src/Io/ClientConnectionManager.php @@ -8,6 +8,8 @@ use React\Promise\PromiseInterface; use React\Socket\ConnectionInterface; use React\Socket\ConnectorInterface; +use function React\Promise\reject; +use function React\Promise\resolve; /** * [Internal] Manages outgoing HTTP connections for the HTTP client @@ -24,16 +26,16 @@ class ClientConnectionManager private $loop; /** @var string[] */ - private $idleUris = array(); + private $idleUris = []; /** @var ConnectionInterface[] */ - private $idleConnections = array(); + private $idleConnections = []; /** @var TimerInterface[] */ - private $idleTimers = array(); + private $idleTimers = []; /** @var \Closure[] */ - private $idleStreamHandlers = array(); + private $idleStreamHandlers = []; /** @var float */ private $maximumTimeToKeepAliveIdleConnection = 0.001; @@ -51,7 +53,7 @@ public function connect(UriInterface $uri) { $scheme = $uri->getScheme(); if ($scheme !== 'https' && $scheme !== 'http') { - return \React\Promise\reject(new \InvalidArgumentException( + return reject(new \InvalidArgumentException( 'Invalid request URL given' )); } @@ -74,7 +76,7 @@ public function connect(UriInterface $uri) $this->loop->cancelTimer($this->idleTimers[$id]); unset($this->idleUris[$id], $this->idleConnections[$id], $this->idleTimers[$id], $this->idleStreamHandlers[$id]); - return \React\Promise\resolve($connection); + return resolve($connection); } } @@ -100,10 +102,8 @@ public function keepAlive(UriInterface $uri, ConnectionInterface $connection) $this->idleUris[] = ($scheme === 'https' ? 'tls://' : '') . $uri->getHost() . ':' . $port; $this->idleConnections[] = $connection; - $that = $this; - $cleanUp = function () use ($connection, $that) { - // call public method to support legacy PHP 5.3 - $that->cleanUpConnection($connection); + $cleanUp = function () use ($connection) { + $this->cleanUpConnection($connection); }; // clean up and close connection when maximum time to keep-alive idle connection has passed @@ -116,11 +116,8 @@ public function keepAlive(UriInterface $uri, ConnectionInterface $connection) $connection->on('error', $cleanUp); } - /** - * @internal - * @return void - */ - public function cleanUpConnection(ConnectionInterface $connection) // private (PHP 5.4+) + /** @return void */ + private function cleanUpConnection(ConnectionInterface $connection) { $id = \array_search($connection, $this->idleConnections, true); if ($id === false) { diff --git a/src/Io/ClientRequestStream.php b/src/Io/ClientRequestStream.php index 3bdf9b1f..fb0cd3e5 100644 --- a/src/Io/ClientRequestStream.php +++ b/src/Io/ClientRequestStream.php @@ -74,38 +74,32 @@ private function writeHead() return; } - $connectionRef = &$this->connection; - $stateRef = &$this->state; - $pendingWrites = &$this->pendingWrites; - $that = $this; - $promise = $this->connectionManager->connect($this->request->getUri()); $promise->then( - function (ConnectionInterface $connection) use ($headers, &$connectionRef, &$stateRef, &$pendingWrites, $that) { - $connectionRef = $connection; - assert($connectionRef instanceof ConnectionInterface); + function (ConnectionInterface $connection) use ($headers) { + $this->connection = $connection; - $connection->on('drain', array($that, 'handleDrain')); - $connection->on('data', array($that, 'handleData')); - $connection->on('end', array($that, 'handleEnd')); - $connection->on('error', array($that, 'handleError')); - $connection->on('close', array($that, 'close')); + $connection->on('drain', [$this, 'handleDrain']); + $connection->on('data', [$this, 'handleData']); + $connection->on('end', [$this, 'handleEnd']); + $connection->on('error', [$this, 'handleError']); + $connection->on('close', [$this, 'close']); - $more = $connection->write($headers . "\r\n" . $pendingWrites); + $more = $connection->write($headers . "\r\n" . $this->pendingWrites); - assert($stateRef === ClientRequestStream::STATE_WRITING_HEAD); - $stateRef = ClientRequestStream::STATE_HEAD_WRITTEN; + assert($this->state === ClientRequestStream::STATE_WRITING_HEAD); + $this->state = ClientRequestStream::STATE_HEAD_WRITTEN; // clear pending writes if non-empty - if ($pendingWrites !== '') { - $pendingWrites = ''; + if ($this->pendingWrites !== '') { + $this->pendingWrites = ''; if ($more) { - $that->emit('drain'); + $this->emit('drain'); } } }, - array($this, 'closeError') + [$this, 'closeError'] ); $this->on('close', function() use ($promise) { @@ -179,29 +173,26 @@ public function handleData($data) // response headers successfully received => remove listeners for connection events $connection = $this->connection; assert($connection instanceof ConnectionInterface); - $connection->removeListener('drain', array($this, 'handleDrain')); - $connection->removeListener('data', array($this, 'handleData')); - $connection->removeListener('end', array($this, 'handleEnd')); - $connection->removeListener('error', array($this, 'handleError')); - $connection->removeListener('close', array($this, 'close')); + $connection->removeListener('drain', [$this, 'handleDrain']); + $connection->removeListener('data', [$this, 'handleData']); + $connection->removeListener('end', [$this, 'handleEnd']); + $connection->removeListener('error', [$this, 'handleError']); + $connection->removeListener('close', [$this, 'close']); $this->connection = null; $this->buffer = ''; // take control over connection handling and check if we can reuse the connection once response body closes - $that = $this; - $request = $this->request; - $connectionManager = $this->connectionManager; $successfulEndReceived = false; $input = $body = new CloseProtectionStream($connection); - $input->on('close', function () use ($connection, $that, $connectionManager, $request, $response, &$successfulEndReceived) { + $input->on('close', function () use ($connection, $response, &$successfulEndReceived) { // only reuse connection after successful response and both request and response allow keep alive - if ($successfulEndReceived && $connection->isReadable() && $that->hasMessageKeepAliveEnabled($response) && $that->hasMessageKeepAliveEnabled($request)) { - $connectionManager->keepAlive($request->getUri(), $connection); + if ($successfulEndReceived && $connection->isReadable() && $this->hasMessageKeepAliveEnabled($response) && $this->hasMessageKeepAliveEnabled($this->request)) { + $this->connectionManager->keepAlive($this->request->getUri(), $connection); } else { $connection->close(); } - $that->close(); + $this->close(); }); // determine length of response body @@ -220,7 +211,7 @@ public function handleData($data) }); // emit response with streaming response body (see `Sender`) - $this->emit('response', array($response, $body)); + $this->emit('response', [$response, $body]); // re-emit HTTP response body to trigger body parsing if parts of it are buffered if ($bodyChunk !== '') { @@ -255,7 +246,7 @@ public function closeError(\Exception $error) if (self::STATE_END <= $this->state) { return; } - $this->emit('error', array($error)); + $this->emit('error', [$error]); $this->close(); } diff --git a/src/Io/Clock.php b/src/Io/Clock.php index 92c1cb09..c2445a94 100644 --- a/src/Io/Clock.php +++ b/src/Io/Clock.php @@ -42,10 +42,9 @@ public function now() $this->now = \microtime(true); // remember clock for current loop tick only and update on next tick - $now =& $this->now; - $this->loop->futureTick(function () use (&$now) { - assert($now !== null); - $now = null; + $this->loop->futureTick(function () { + assert($this->now !== null); + $this->now = null; }); } diff --git a/src/Io/CloseProtectionStream.php b/src/Io/CloseProtectionStream.php index 2e1ed6e4..7fae08e7 100644 --- a/src/Io/CloseProtectionStream.php +++ b/src/Io/CloseProtectionStream.php @@ -28,10 +28,10 @@ public function __construct(ReadableStreamInterface $input) { $this->input = $input; - $this->input->on('data', array($this, 'handleData')); - $this->input->on('end', array($this, 'handleEnd')); - $this->input->on('error', array($this, 'handleError')); - $this->input->on('close', array($this, 'close')); + $this->input->on('data', [$this, 'handleData']); + $this->input->on('end', [$this, 'handleEnd']); + $this->input->on('error', [$this, 'handleError']); + $this->input->on('close', [$this, 'close']); } public function isReadable() @@ -59,7 +59,7 @@ public function resume() $this->input->resume(); } - public function pipe(WritableStreamInterface $dest, array $options = array()) + public function pipe(WritableStreamInterface $dest, array $options = []) { Util::pipe($this, $dest, $options); @@ -75,10 +75,10 @@ public function close() $this->closed = true; // stop listening for incoming events - $this->input->removeListener('data', array($this, 'handleData')); - $this->input->removeListener('error', array($this, 'handleError')); - $this->input->removeListener('end', array($this, 'handleEnd')); - $this->input->removeListener('close', array($this, 'close')); + $this->input->removeListener('data', [$this, 'handleData']); + $this->input->removeListener('error', [$this, 'handleError']); + $this->input->removeListener('end', [$this, 'handleEnd']); + $this->input->removeListener('close', [$this, 'close']); // resume the stream to ensure we discard everything from incoming connection if ($this->paused) { @@ -93,7 +93,7 @@ public function close() /** @internal */ public function handleData($data) { - $this->emit('data', array($data)); + $this->emit('data', [$data]); } /** @internal */ @@ -106,6 +106,6 @@ public function handleEnd() /** @internal */ public function handleError(\Exception $e) { - $this->emit('error', array($e)); + $this->emit('error', [$e]); } } diff --git a/src/Io/EmptyBodyStream.php b/src/Io/EmptyBodyStream.php index 5056219c..7f9c8ad0 100644 --- a/src/Io/EmptyBodyStream.php +++ b/src/Io/EmptyBodyStream.php @@ -44,7 +44,7 @@ public function resume() // NOOP } - public function pipe(WritableStreamInterface $dest, array $options = array()) + public function pipe(WritableStreamInterface $dest, array $options = []) { Util::pipe($this, $dest, $options); @@ -137,6 +137,6 @@ public function getContents() /** @ignore */ public function getMetadata($key = null) { - return ($key === null) ? array() : null; + return ($key === null) ? [] : null; } } diff --git a/src/Io/HttpBodyStream.php b/src/Io/HttpBodyStream.php index 25d15a18..8be9b854 100644 --- a/src/Io/HttpBodyStream.php +++ b/src/Io/HttpBodyStream.php @@ -39,10 +39,10 @@ public function __construct(ReadableStreamInterface $input, $size) $this->input = $input; $this->size = $size; - $this->input->on('data', array($this, 'handleData')); - $this->input->on('end', array($this, 'handleEnd')); - $this->input->on('error', array($this, 'handleError')); - $this->input->on('close', array($this, 'close')); + $this->input->on('data', [$this, 'handleData']); + $this->input->on('end', [$this, 'handleEnd']); + $this->input->on('error', [$this, 'handleError']); + $this->input->on('close', [$this, 'close']); } public function isReadable() @@ -60,7 +60,7 @@ public function resume() $this->input->resume(); } - public function pipe(WritableStreamInterface $dest, array $options = array()) + public function pipe(WritableStreamInterface $dest, array $options = []) { Util::pipe($this, $dest, $options); @@ -161,13 +161,13 @@ public function getMetadata($key = null) /** @internal */ public function handleData($data) { - $this->emit('data', array($data)); + $this->emit('data', [$data]); } /** @internal */ public function handleError(\Exception $e) { - $this->emit('error', array($e)); + $this->emit('error', [$e]); $this->close(); } diff --git a/src/Io/LengthLimitedStream.php b/src/Io/LengthLimitedStream.php index bc64c54b..c4a38b13 100644 --- a/src/Io/LengthLimitedStream.php +++ b/src/Io/LengthLimitedStream.php @@ -27,10 +27,10 @@ public function __construct(ReadableStreamInterface $stream, $maxLength) $this->stream = $stream; $this->maxLength = $maxLength; - $this->stream->on('data', array($this, 'handleData')); - $this->stream->on('end', array($this, 'handleEnd')); - $this->stream->on('error', array($this, 'handleError')); - $this->stream->on('close', array($this, 'close')); + $this->stream->on('data', [$this, 'handleData']); + $this->stream->on('end', [$this, 'handleEnd']); + $this->stream->on('error', [$this, 'handleError']); + $this->stream->on('close', [$this, 'close']); } public function isReadable() @@ -48,7 +48,7 @@ public function resume() $this->stream->resume(); } - public function pipe(WritableStreamInterface $dest, array $options = array()) + public function pipe(WritableStreamInterface $dest, array $options = []) { Util::pipe($this, $dest, $options); @@ -79,21 +79,21 @@ public function handleData($data) if ($data !== '') { $this->transferredLength += \strlen($data); - $this->emit('data', array($data)); + $this->emit('data', [$data]); } if ($this->transferredLength === $this->maxLength) { // 'Content-Length' reached, stream will end $this->emit('end'); $this->close(); - $this->stream->removeListener('data', array($this, 'handleData')); + $this->stream->removeListener('data', [$this, 'handleData']); } } /** @internal */ public function handleError(\Exception $e) { - $this->emit('error', array($e)); + $this->emit('error', [$e]); $this->close(); } diff --git a/src/Io/MiddlewareRunner.php b/src/Io/MiddlewareRunner.php index dedf6ff1..c05c5a1a 100644 --- a/src/Io/MiddlewareRunner.php +++ b/src/Io/MiddlewareRunner.php @@ -40,8 +40,7 @@ public function __invoke(ServerRequestInterface $request) return $this->call($request, 0); } - /** @internal */ - public function call(ServerRequestInterface $request, $position) + private function call(ServerRequestInterface $request, $position) { // final request handler will be invoked without a next handler if (!isset($this->middleware[$position + 1])) { @@ -49,9 +48,8 @@ public function call(ServerRequestInterface $request, $position) return $handler($request); } - $that = $this; - $next = function (ServerRequestInterface $request) use ($that, $position) { - return $that->call($request, $position + 1); + $next = function (ServerRequestInterface $request) use ($position) { + return $this->call($request, $position + 1); }; // invoke middleware request handler with next handler diff --git a/src/Io/MultipartParser.php b/src/Io/MultipartParser.php index c65bb655..cdfe189b 100644 --- a/src/Io/MultipartParser.php +++ b/src/Io/MultipartParser.php @@ -166,7 +166,7 @@ private function parsePart($chunk) $this->parseFile( $name, $filename, - isset($headers['content-type'][0]) ? $headers['content-type'][0] : null, + $headers['content-type'][0] ?? null, $body ); } else { @@ -268,7 +268,7 @@ private function parsePost($name, $value) private function parseHeaders($header) { - $headers = array(); + $headers = []; foreach (\explode("\r\n", \trim($header)) as $line) { $parts = \explode(':', $line, 2); @@ -315,12 +315,12 @@ private function extractPost($postFields, $key, $value) $previousChunkKey = $chunkKey; if ($previousChunkKey === '') { - $parent[] = array(); + $parent[] = []; \end($parent); $parent = &$parent[\key($parent)]; } else { if (!isset($parent[$previousChunkKey]) || !\is_array($parent[$previousChunkKey])) { - $parent[$previousChunkKey] = array(); + $parent[$previousChunkKey] = []; } $parent = &$parent[$previousChunkKey]; } diff --git a/src/Io/PauseBufferStream.php b/src/Io/PauseBufferStream.php index fb5ed456..b1132adc 100644 --- a/src/Io/PauseBufferStream.php +++ b/src/Io/PauseBufferStream.php @@ -36,10 +36,10 @@ public function __construct(ReadableStreamInterface $input) { $this->input = $input; - $this->input->on('data', array($this, 'handleData')); - $this->input->on('end', array($this, 'handleEnd')); - $this->input->on('error', array($this, 'handleError')); - $this->input->on('close', array($this, 'handleClose')); + $this->input->on('data', [$this, 'handleData']); + $this->input->on('end', [$this, 'handleEnd']); + $this->input->on('error', [$this, 'handleError']); + $this->input->on('close', [$this, 'handleClose']); } /** @@ -91,12 +91,12 @@ public function resume() $this->implicit = false; if ($this->dataPaused !== '') { - $this->emit('data', array($this->dataPaused)); + $this->emit('data', [$this->dataPaused]); $this->dataPaused = ''; } if ($this->errorPaused) { - $this->emit('error', array($this->errorPaused)); + $this->emit('error', [$this->errorPaused]); return $this->close(); } @@ -114,7 +114,7 @@ public function resume() $this->input->resume(); } - public function pipe(WritableStreamInterface $dest, array $options = array()) + public function pipe(WritableStreamInterface $dest, array $options = []) { Util::pipe($this, $dest, $options); @@ -146,7 +146,7 @@ public function handleData($data) return; } - $this->emit('data', array($data)); + $this->emit('data', [$data]); } /** @internal */ @@ -157,7 +157,7 @@ public function handleError(\Exception $e) return; } - $this->emit('error', array($e)); + $this->emit('error', [$e]); $this->close(); } diff --git a/src/Io/ReadableBodyStream.php b/src/Io/ReadableBodyStream.php index daef45f9..9a8bd105 100644 --- a/src/Io/ReadableBodyStream.php +++ b/src/Io/ReadableBodyStream.php @@ -23,22 +23,20 @@ public function __construct(ReadableStreamInterface $input, $size = null) $this->input = $input; $this->size = $size; - $that = $this; - $pos =& $this->position; - $input->on('data', function ($data) use ($that, &$pos, $size) { - $that->emit('data', array($data)); - - $pos += \strlen($data); - if ($size !== null && $pos >= $size) { - $that->handleEnd(); + $input->on('data', function ($data) use ($size) { + $this->emit('data', [$data]); + + $this->position += \strlen($data); + if ($size !== null && $this->position >= $size) { + $this->handleEnd(); } }); - $input->on('error', function ($error) use ($that) { - $that->emit('error', array($error)); - $that->close(); + $input->on('error', function ($error) { + $this->emit('error', [$error]); + $this->close(); }); - $input->on('end', array($that, 'handleEnd')); - $input->on('close', array($that, 'close')); + $input->on('end', [$this, 'handleEnd']); + $input->on('close', [$this, 'close']); } public function close() @@ -67,7 +65,7 @@ public function resume() $this->input->resume(); } - public function pipe(WritableStreamInterface $dest, array $options = array()) + public function pipe(WritableStreamInterface $dest, array $options = []) { Util::pipe($this, $dest, $options); @@ -136,14 +134,14 @@ public function getContents() public function getMetadata($key = null) { - return ($key === null) ? array() : null; + return ($key === null) ? [] : null; } /** @internal */ public function handleEnd() { if ($this->position !== $this->size && $this->size !== null) { - $this->emit('error', array(new \UnderflowException('Unexpected end of response body after ' . $this->position . '/' . $this->size . ' bytes'))); + $this->emit('error', [new \UnderflowException('Unexpected end of response body after ' . $this->position . '/' . $this->size . ' bytes')]); } else { $this->emit('end'); } diff --git a/src/Io/RequestHeaderParser.php b/src/Io/RequestHeaderParser.php index 8975ce57..403ab0cc 100644 --- a/src/Io/RequestHeaderParser.php +++ b/src/Io/RequestHeaderParser.php @@ -28,7 +28,7 @@ class RequestHeaderParser extends EventEmitter private $clock; /** @var array> */ - private $connectionParams = array(); + private $connectionParams = []; public function __construct(Clock $clock) { @@ -38,22 +38,20 @@ public function __construct(Clock $clock) public function handle(ConnectionInterface $conn) { $buffer = ''; - $maxSize = $this->maxSize; - $that = $this; - $conn->on('data', $fn = function ($data) use (&$buffer, &$fn, $conn, $maxSize, $that) { + $conn->on('data', $fn = function ($data) use (&$buffer, &$fn, $conn) { // append chunk of data to buffer and look for end of request headers $buffer .= $data; $endOfHeader = \strpos($buffer, "\r\n\r\n"); // reject request if buffer size is exceeded - if ($endOfHeader > $maxSize || ($endOfHeader === false && isset($buffer[$maxSize]))) { + if ($endOfHeader > $this->maxSize || ($endOfHeader === false && isset($buffer[$this->maxSize]))) { $conn->removeListener('data', $fn); $fn = null; - $that->emit('error', array( - new \OverflowException("Maximum header size of {$maxSize} exceeded.", Response::STATUS_REQUEST_HEADER_FIELDS_TOO_LARGE), + $this->emit('error', [ + new \OverflowException("Maximum header size of {$this->maxSize} exceeded.", Response::STATUS_REQUEST_HEADER_FIELDS_TOO_LARGE), $conn - )); + ]); return; } @@ -67,16 +65,16 @@ public function handle(ConnectionInterface $conn) $fn = null; try { - $request = $that->parseRequest( + $request = $this->parseRequest( (string)\substr($buffer, 0, $endOfHeader + 2), $conn ); } catch (Exception $exception) { $buffer = ''; - $that->emit('error', array( + $this->emit('error', [ $exception, $conn - )); + ]); return; } @@ -105,10 +103,10 @@ public function handle(ConnectionInterface $conn) $bodyBuffer = isset($buffer[$endOfHeader + 4]) ? \substr($buffer, $endOfHeader + 4) : ''; $buffer = ''; - $that->emit('headers', array($request, $conn)); + $this->emit('headers', [$request, $conn]); if ($bodyBuffer !== '') { - $conn->emit('data', array($bodyBuffer)); + $conn->emit('data', [$bodyBuffer]); } // happy path: request body is known to be empty => immediately end stream @@ -134,11 +132,11 @@ public function parseRequest($headers, ConnectionInterface $connection) $serverParams = $this->connectionParams[$cid]; } else { // assign new server params for new connection - $serverParams = array(); + $serverParams = []; // scheme is `http` unless TLS is used $localSocketUri = $connection->getLocalAddress(); - $localParts = $localSocketUri === null ? array() : \parse_url($localSocketUri); + $localParts = $localSocketUri === null ? [] : \parse_url($localSocketUri); if (isset($localParts['scheme']) && $localParts['scheme'] === 'tls') { $serverParams['HTTPS'] = 'on'; } @@ -162,10 +160,9 @@ public function parseRequest($headers, ConnectionInterface $connection) // remember server params for all requests from this connection, reset on connection close $this->connectionParams[$cid] = $serverParams; - $params =& $this->connectionParams; - $connection->on('close', function () use (&$params, $cid) { - assert(\is_array($params)); - unset($params[$cid]); + $connection->on('close', function () use ($cid) { + assert(\is_array($this->connectionParams[$cid])); + unset($this->connectionParams[$cid]); }); } diff --git a/src/Io/Sender.php b/src/Io/Sender.php index 1d563891..8ece2ee0 100644 --- a/src/Io/Sender.php +++ b/src/Io/Sender.php @@ -40,7 +40,7 @@ class Sender * settings. You can use this method manually like this: * * ```php - * $connector = new \React\Socket\Connector(array(), $loop); + * $connector = new \React\Socket\Connector([], $loop); * $sender = \React\Http\Io\Sender::createFromLoop($loop, $connector); * ``` * @@ -51,7 +51,7 @@ class Sender public static function createFromLoop(LoopInterface $loop, ConnectorInterface $connector = null) { if ($connector === null) { - $connector = new Connector(array(), $loop); + $connector = new Connector([], $loop); } return new self(new HttpClient(new ClientConnectionManager($connector, $loop))); @@ -79,7 +79,7 @@ public function __construct(HttpClient $http) public function send(RequestInterface $request) { // support HTTP/1.1 and HTTP/1.0 only, ensured by `Browser` already - assert(\in_array($request->getProtocolVersion(), array('1.0', '1.1'), true)); + assert(\in_array($request->getProtocolVersion(), ['1.0', '1.1'], true)); $body = $request->getBody(); $size = $body->getSize(); @@ -87,7 +87,7 @@ public function send(RequestInterface $request) if ($size !== null && $size !== 0) { // automatically assign a "Content-Length" request header if the body size is known and non-empty $request = $request->withHeader('Content-Length', (string)$size); - } elseif ($size === 0 && \in_array($request->getMethod(), array('POST', 'PUT', 'PATCH'))) { + } elseif ($size === 0 && \in_array($request->getMethod(), ['POST', 'PUT', 'PATCH'])) { // only assign a "Content-Length: 0" request header if the body is expected for certain methods $request = $request->withHeader('Content-Length', '0'); } elseif ($body instanceof ReadableStreamInterface && $size !== 0 && $body->isReadable() && !$request->hasHeader('Content-Length')) { diff --git a/src/Io/StreamingServer.php b/src/Io/StreamingServer.php index eee9f900..6d12d359 100644 --- a/src/Io/StreamingServer.php +++ b/src/Io/StreamingServer.php @@ -8,12 +8,13 @@ use React\EventLoop\LoopInterface; use React\Http\Message\Response; use React\Http\Message\ServerRequest; -use React\Promise; use React\Promise\PromiseInterface; use React\Socket\ConnectionInterface; use React\Socket\ServerInterface; use React\Stream\ReadableStreamInterface; use React\Stream\WritableStreamInterface; +use function React\Promise\reject; +use function React\Promise\resolve; /** * The internal `StreamingServer` class is responsible for handling incoming connections and then @@ -31,9 +32,9 @@ * $server = new StreamingServer($loop, function (ServerRequestInterface $request) { * return new Response( * Response::STATUS_OK, - * array( + * [ * 'Content-Type' => 'text/plain' - * ), + * ], * "Hello World!\n" * ); * }); @@ -55,7 +56,7 @@ * ```php * $server = new StreamingServer($loop, $handler); * - * $socket = new React\Socket\SocketServer('0.0.0.0:8080', array(), $loop); + * $socket = new React\Socket\SocketServer('0.0.0.0:8080', [], $loop); * $server->listen($socket); * ``` * @@ -109,16 +110,15 @@ public function __construct(LoopInterface $loop, $requestHandler) $this->clock = new Clock($loop); $this->parser = new RequestHeaderParser($this->clock); - $that = $this; - $this->parser->on('headers', function (ServerRequestInterface $request, ConnectionInterface $conn) use ($that) { - $that->handleRequest($conn, $request); + $this->parser->on('headers', function (ServerRequestInterface $request, ConnectionInterface $conn) { + $this->handleRequest($conn, $request); }); - $this->parser->on('error', function(\Exception $e, ConnectionInterface $conn) use ($that) { - $that->emit('error', array($e)); + $this->parser->on('error', function(\Exception $e, ConnectionInterface $conn) { + $this->emit('error', [$e]); // parsing failed => assume dummy request and send appropriate error - $that->writeError( + $this->writeError( $conn, $e->getCode() !== 0 ? $e->getCode() : Response::STATUS_BAD_REQUEST, new ServerRequest('GET', '/') @@ -134,7 +134,7 @@ public function __construct(LoopInterface $loop, $requestHandler) */ public function listen(ServerInterface $socket) { - $socket->on('connection', array($this->parser, 'handle')); + $socket->on('connection', [$this->parser, 'handle']); } /** @internal */ @@ -145,15 +145,11 @@ public function handleRequest(ConnectionInterface $conn, ServerRequestInterface } // execute request handler callback - $callback = $this->callback; try { - $response = $callback($request); - } catch (\Exception $error) { + $response = ($this->callback)($request); + } catch (\Throwable $error) { // request handler callback throws an Exception - $response = Promise\reject($error); - } catch (\Throwable $error) { // @codeCoverageIgnoreStart - // request handler callback throws a PHP7+ Error - $response = Promise\reject($error); // @codeCoverageIgnoreEnd + $response = reject($error); } // cancel pending promise once connection closes @@ -177,23 +173,22 @@ public function handleRequest(ConnectionInterface $conn, ServerRequestInterface // did not return a promise? this is an error, convert into one for rejection below. if (!$response instanceof PromiseInterface) { - $response = Promise\resolve($response); + $response = resolve($response); } - $that = $this; $response->then( - function ($response) use ($that, $conn, $request) { + function ($response) use ($conn, $request) { if (!$response instanceof ResponseInterface) { $message = 'The response callback is expected to resolve with an object implementing Psr\Http\Message\ResponseInterface, but resolved with "%s" instead.'; $message = \sprintf($message, \is_object($response) ? \get_class($response) : \gettype($response)); $exception = new \RuntimeException($message); - $that->emit('error', array($exception)); - return $that->writeError($conn, Response::STATUS_INTERNAL_SERVER_ERROR, $request); + $this->emit('error', [$exception]); + return $this->writeError($conn, Response::STATUS_INTERNAL_SERVER_ERROR, $request); } - $that->handleResponse($conn, $request, $response); + $this->handleResponse($conn, $request, $response); }, - function ($error) use ($that, $conn, $request) { + function ($error) use ($conn, $request) { $message = 'The response callback is expected to resolve with an object implementing Psr\Http\Message\ResponseInterface, but rejected with "%s" instead.'; $message = \sprintf($message, \is_object($error) ? \get_class($error) : \gettype($error)); @@ -205,8 +200,8 @@ function ($error) use ($that, $conn, $request) { $exception = new \RuntimeException($message, 0, $previous); - $that->emit('error', array($exception)); - return $that->writeError($conn, Response::STATUS_INTERNAL_SERVER_ERROR, $request); + $this->emit('error', [$exception]); + return $this->writeError($conn, Response::STATUS_INTERNAL_SERVER_ERROR, $request); } )->then($connectionOnCloseResponseCancelerHandler, $connectionOnCloseResponseCancelerHandler); } @@ -216,10 +211,10 @@ public function writeError(ConnectionInterface $conn, $code, ServerRequestInterf { $response = new Response( $code, - array( + [ 'Content-Type' => 'text/plain', 'Connection' => 'close' // we do not want to keep the connection open after an error - ), + ], 'Error ' . $code ); @@ -347,7 +342,7 @@ public function handleResponse(ConnectionInterface $connection, ServerRequestInt } if ($code < 100 || $code > 999 || \substr_count($headers, "\n") !== ($expected + 1) || \preg_match_all(AbstractMessage::REGEX_HEADERS, $headers) !== $expected) { - $this->emit('error', array(new \InvalidArgumentException('Unable to send response with invalid response headers'))); + $this->emit('error', [new \InvalidArgumentException('Unable to send response with invalid response headers')]); $this->writeError($connection, Response::STATUS_INTERNAL_SERVER_ERROR, $request); return; } @@ -387,15 +382,14 @@ public function handleResponse(ConnectionInterface $connection, ServerRequestInt // Close response stream once connection closes. // Note that this TCP/IP close detection may take some time, // in particular this may only fire on a later read/write attempt. - $connection->on('close', array($body, 'close')); + $connection->on('close', [$body, 'close']); // write streaming body and then wait for next request over persistent connection if ($persist) { - $body->pipe($connection, array('end' => false)); - $parser = $this->parser; - $body->on('end', function () use ($connection, $parser, $body) { - $connection->removeListener('close', array($body, 'close')); - $parser->handle($connection); + $body->pipe($connection, ['end' => false]); + $body->on('end', function () use ($connection, $body) { + $connection->removeListener('close', [$body, 'close']); + $this->parser->handle($connection); }); } else { $body->pipe($connection); diff --git a/src/Io/Transaction.php b/src/Io/Transaction.php index 64738f56..6790cb45 100644 --- a/src/Io/Transaction.php +++ b/src/Io/Transaction.php @@ -13,6 +13,8 @@ use React\Promise\Promise; use React\Promise\PromiseInterface; use React\Stream\ReadableStreamInterface; +use function React\Promise\reject; +use function React\Promise\resolve; /** * @internal @@ -77,21 +79,20 @@ public function send(RequestInterface $request) }); // use timeout from options or default to PHP's default_socket_timeout (60) - $timeout = (float)($this->timeout !== null ? $this->timeout : ini_get("default_socket_timeout")); + $timeout = (float) ($this->timeout ?? ini_get("default_socket_timeout")); - $loop = $this->loop; $this->next($request, $deferred, $state)->then( - function (ResponseInterface $response) use ($state, $deferred, $loop, &$timeout) { + function (ResponseInterface $response) use ($state, $deferred, &$timeout) { if ($state->timeout !== null) { - $loop->cancelTimer($state->timeout); + $this->loop->cancelTimer($state->timeout); $state->timeout = null; } $timeout = -1; $deferred->resolve($response); }, - function ($e) use ($state, $deferred, $loop, &$timeout) { + function ($e) use ($state, $deferred, &$timeout) { if ($state->timeout !== null) { - $loop->cancelTimer($state->timeout); + $this->loop->cancelTimer($state->timeout); $state->timeout = null; } $timeout = -1; @@ -105,10 +106,9 @@ function ($e) use ($state, $deferred, $loop, &$timeout) { $body = $request->getBody(); if ($body instanceof ReadableStreamInterface && $body->isReadable()) { - $that = $this; - $body->on('close', function () use ($that, $deferred, $state, &$timeout) { + $body->on('close', function () use ($deferred, $state, &$timeout) { if ($timeout >= 0) { - $that->applyTimeout($deferred, $state, $timeout); + $this->applyTimeout($deferred, $state, $timeout); } }); } else { @@ -138,24 +138,23 @@ public function applyTimeout(Deferred $deferred, ClientRequestState $state, $tim private function next(RequestInterface $request, Deferred $deferred, ClientRequestState $state) { - $this->progress('request', array($request)); + $this->progress('request', [$request]); - $that = $this; ++$state->numRequests; $promise = $this->sender->send($request); if (!$this->streaming) { - $promise = $promise->then(function ($response) use ($deferred, $state, $that) { - return $that->bufferResponse($response, $deferred, $state); + $promise = $promise->then(function ($response) use ($deferred, $state) { + return $this->bufferResponse($response, $deferred, $state); }); } $state->pending = $promise; return $promise->then( - function (ResponseInterface $response) use ($request, $that, $deferred, $state) { - return $that->onResponse($response, $request, $deferred, $state); + function (ResponseInterface $response) use ($request, $deferred, $state) { + return $this->onResponse($response, $request, $deferred, $state); } ); } @@ -171,7 +170,7 @@ public function bufferResponse(ResponseInterface $response, Deferred $deferred, if ($size !== null && $size > $this->maximumSize) { $body->close(); - return \React\Promise\reject(new \OverflowException( + return reject(new \OverflowException( 'Response body size of ' . $size . ' bytes exceeds maximum of ' . $this->maximumSize . ' bytes', \defined('SOCKET_EMSGSIZE') ? \SOCKET_EMSGSIZE : 90 )); @@ -179,33 +178,32 @@ public function bufferResponse(ResponseInterface $response, Deferred $deferred, // body is not streaming => already buffered if (!$body instanceof ReadableStreamInterface) { - return \React\Promise\resolve($response); + return resolve($response); } /** @var ?\Closure $closer */ $closer = null; - $maximumSize = $this->maximumSize; - return $state->pending = new Promise(function ($resolve, $reject) use ($body, $maximumSize, $response, &$closer) { + return $state->pending = new Promise(function ($resolve, $reject) use ($body, $response, &$closer) { // resolve with current buffer when stream closes successfully $buffer = ''; - $body->on('close', $closer = function () use (&$buffer, $response, $maximumSize, $resolve, $reject) { + $body->on('close', $closer = function () use (&$buffer, $response, $resolve, $reject) { $resolve($response->withBody(new BufferedBody($buffer))); }); // buffer response body data in memory - $body->on('data', function ($data) use (&$buffer, $maximumSize, $body, $closer, $reject) { + $body->on('data', function ($data) use (&$buffer, $body, $closer, $reject) { $buffer .= $data; // close stream and reject promise if limit is exceeded - if (isset($buffer[$maximumSize])) { + if (isset($buffer[$this->maximumSize])) { $buffer = ''; assert($closer instanceof \Closure); $body->removeListener('close', $closer); $body->close(); $reject(new \OverflowException( - 'Response body size exceeds maximum of ' . $maximumSize . ' bytes', + 'Response body size exceeds maximum of ' . $this->maximumSize . ' bytes', \defined('SOCKET_EMSGSIZE') ? \SOCKET_EMSGSIZE : 90 )); } @@ -236,7 +234,7 @@ public function bufferResponse(ResponseInterface $response, Deferred $deferred, */ public function onResponse(ResponseInterface $response, RequestInterface $request, Deferred $deferred, ClientRequestState $state) { - $this->progress('response', array($response, $request)); + $this->progress('response', [$response, $request]); // follow 3xx (Redirection) response status codes if Location header is present and not explicitly disabled // @link https://tools.ietf.org/html/rfc7231#section-6.4 @@ -267,7 +265,7 @@ private function onResponseRedirect(ResponseInterface $response, RequestInterfac $location = Uri::resolve($request->getUri(), new Uri($response->getHeaderLine('Location'))); $request = $this->makeRedirectRequest($request, $location, $response->getStatusCode()); - $this->progress('redirect', array($request)); + $this->progress('redirect', [$request]); if ($state->numRequests >= $this->maxRedirects) { throw new \RuntimeException('Maximum number of redirects (' . $this->maxRedirects . ') exceeded'); @@ -308,7 +306,7 @@ private function makeRedirectRequest(RequestInterface $request, UriInterface $lo return $request; } - private function progress($name, array $args = array()) + private function progress($name, array $args = []) { return; diff --git a/src/Io/UploadedFile.php b/src/Io/UploadedFile.php index f2a6c9e7..b0d0dd98 100644 --- a/src/Io/UploadedFile.php +++ b/src/Io/UploadedFile.php @@ -57,7 +57,7 @@ public function __construct(StreamInterface $stream, $size, $error, $filename, $ $this->stream = $stream; $this->size = $size; - if (!\is_int($error) || !\in_array($error, array( + if (!\is_int($error) || !\in_array($error, [ \UPLOAD_ERR_OK, \UPLOAD_ERR_INI_SIZE, \UPLOAD_ERR_FORM_SIZE, @@ -66,7 +66,7 @@ public function __construct(StreamInterface $stream, $size, $error, $filename, $ \UPLOAD_ERR_NO_TMP_DIR, \UPLOAD_ERR_CANT_WRITE, \UPLOAD_ERR_EXTENSION, - ))) { + ])) { throw new InvalidArgumentException( 'Invalid error code, must be an UPLOAD_ERR_* constant' ); diff --git a/src/Message/Request.php b/src/Message/Request.php index 3de8c1b3..fdba39f5 100644 --- a/src/Message/Request.php +++ b/src/Message/Request.php @@ -40,7 +40,7 @@ final class Request extends AbstractRequest implements RequestInterface public function __construct( $method, $url, - array $headers = array(), + array $headers = [], $body = '', $version = '1.1' ) { diff --git a/src/Message/Response.php b/src/Message/Response.php index 107508a9..93557fab 100644 --- a/src/Message/Response.php +++ b/src/Message/Response.php @@ -16,9 +16,9 @@ * ```php * $response = new React\Http\Message\Response( * React\Http\Message\Response::STATUS_OK, - * array( + * [ * 'Content-Type' => 'text/html' - * ), + * ], * "Hello world!\n" * ); * ``` @@ -90,7 +90,7 @@ final class Response extends AbstractMessage implements ResponseInterface, Statu */ public static function html($html) { - return new self(self::STATUS_OK, array('Content-Type' => 'text/html; charset=utf-8'), $html); + return new self(self::STATUS_OK, ['Content-Type' => 'text/html; charset=utf-8'], $html); } /** @@ -124,10 +124,9 @@ public static function html($html) * fails, this method will throw an `InvalidArgumentException`. * * By default, the given structured data will be encoded with the flags as - * shown above. This includes pretty printing (PHP 5.4+) and preserving - * zero fractions for `float` values (PHP 5.6.6+) to ease debugging. It is - * assumed any additional data overhead is usually compensated by using HTTP - * response compression. + * shown above. This includes pretty printing and preserving zero fractions + * for `float` values to ease debugging. It is assumed any additional data + * overhead is usually compensated by using HTTP response compression. * * If you want to use a different status code or custom HTTP response * headers, you can manipulate the returned response object using the @@ -146,19 +145,19 @@ public static function html($html) */ public static function json($data) { - $json = @\json_encode( + $json = \json_encode( $data, - (\defined('JSON_PRETTY_PRINT') ? \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE : 0) | (\defined('JSON_PRESERVE_ZERO_FRACTION') ? \JSON_PRESERVE_ZERO_FRACTION : 0) + \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE | \JSON_PRESERVE_ZERO_FRACTION ); if ($json === false) { throw new \InvalidArgumentException( - 'Unable to encode given data as JSON' . (\function_exists('json_last_error_msg') ? ': ' . \json_last_error_msg() : ''), + 'Unable to encode given data as JSON: ' . \json_last_error_msg(), \json_last_error() ); } - return new self(self::STATUS_OK, array('Content-Type' => 'application/json'), $json . "\n"); + return new self(self::STATUS_OK, ['Content-Type' => 'application/json'], $json . "\n"); } /** @@ -201,7 +200,7 @@ public static function json($data) */ public static function plaintext($text) { - return new self(self::STATUS_OK, array('Content-Type' => 'text/plain; charset=utf-8'), $text); + return new self(self::STATUS_OK, ['Content-Type' => 'text/plain; charset=utf-8'], $text); } /** @@ -253,7 +252,7 @@ public static function plaintext($text) */ public static function xml($xml) { - return new self(self::STATUS_OK, array('Content-Type' => 'application/xml'), $xml); + return new self(self::STATUS_OK, ['Content-Type' => 'application/xml'], $xml); } /** @@ -275,7 +274,7 @@ public static function xml($xml) * @see self::STATUS_* * @see self::getReasonPhraseForStatusCode() */ - private static $phrasesMap = array( + private static $phrasesMap = [ 200 => 'OK', 203 => 'Non-Authoritative Information', 207 => 'Multi-Status', @@ -283,7 +282,7 @@ public static function xml($xml) 414 => 'URI Too Large', 418 => 'I\'m a teapot', 505 => 'HTTP Version Not Supported' - ); + ]; /** @var int */ private $statusCode; @@ -301,7 +300,7 @@ public static function xml($xml) */ public function __construct( $status = self::STATUS_OK, - array $headers = array(), + array $headers = [], $body = '', $version = '1.1', $reason = null @@ -366,7 +365,7 @@ private static function getReasonPhraseForStatusCode($code) } } - return isset(self::$phrasesMap[$code]) ? self::$phrasesMap[$code] : ''; + return self::$phrasesMap[$code] ?? ''; } /** @@ -379,7 +378,7 @@ private static function getReasonPhraseForStatusCode($code) */ public static function parseMessage($message) { - $start = array(); + $start = []; if (!\preg_match('#^HTTP/(?\d\.\d) (?\d{3})(?: (?[^\r\n]*+))?[\r]?+\n#m', $message, $start)) { throw new \InvalidArgumentException('Unable to parse invalid status-line'); } @@ -390,14 +389,14 @@ public static function parseMessage($message) } // check number of valid header fields matches number of lines + status line - $matches = array(); + $matches = []; $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(); + $headers = []; foreach ($matches as $match) { $headers[$match[1]][] = $match[2]; } @@ -407,7 +406,7 @@ public static function parseMessage($message) $headers, '', $start['version'], - isset($start['reason']) ? $start['reason'] : '' + $start['reason'] ?? '' ); } } diff --git a/src/Message/ServerRequest.php b/src/Message/ServerRequest.php index 32a0f62f..da0d76ab 100644 --- a/src/Message/ServerRequest.php +++ b/src/Message/ServerRequest.php @@ -31,12 +31,12 @@ */ final class ServerRequest extends AbstractRequest implements ServerRequestInterface { - private $attributes = array(); + private $attributes = []; private $serverParams; - private $fileParams = array(); - private $cookies = array(); - private $queryParams = array(); + private $fileParams = []; + private $cookies = []; + private $queryParams = []; private $parsedBody; /** @@ -51,10 +51,10 @@ final class ServerRequest extends AbstractRequest implements ServerRequestInterf public function __construct( $method, $url, - array $headers = array(), + array $headers = [], $body = '', $version = '1.1', - $serverParams = array() + $serverParams = [] ) { if (\is_string($body)) { $body = new BufferedBody($body); @@ -174,7 +174,7 @@ public function withoutAttribute($name) private function parseCookie($cookie) { $cookieArray = \explode(';', $cookie); - $result = array(); + $result = []; foreach ($cookieArray as $pair) { $pair = \trim($pair); @@ -202,7 +202,7 @@ private function parseCookie($cookie) public static function parseMessage($message, array $serverParams) { // parse request line like "GET /path HTTP/1.1" - $start = array(); + $start = []; if (!\preg_match('#^(?[^ ]+) (?[^ ]+) HTTP/(?\d\.\d)#m', $message, $start)) { throw new \InvalidArgumentException('Unable to parse invalid request-line'); } @@ -213,7 +213,7 @@ public static function parseMessage($message, array $serverParams) } // check number of valid header fields matches number of lines + request line - $matches = array(); + $matches = []; $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'); @@ -221,7 +221,7 @@ public static function parseMessage($message, array $serverParams) // format all header fields into associative array $host = null; - $headers = array(); + $headers = []; foreach ($matches as $match) { $headers[$match[1]][] = $match[2]; diff --git a/src/Message/Uri.php b/src/Message/Uri.php index 6b77d3e7..84fc38d8 100644 --- a/src/Message/Uri.php +++ b/src/Message/Uri.php @@ -54,8 +54,8 @@ public function __construct($uri) $this->scheme = \strtolower($parts['scheme']); } - if (isset($parts['user']) || isset($parts['pass'])) { - $this->userInfo = $this->encode(isset($parts['user']) ? $parts['user'] : '', \PHP_URL_USER) . (isset($parts['pass']) ? ':' . $this->encode($parts['pass'], \PHP_URL_PASS) : ''); + if (isset($parts['user'])) { + $this->userInfo = $this->encode($parts['user'], \PHP_URL_USER) . (isset($parts['pass']) ? ':' . $this->encode($parts['pass'], \PHP_URL_PASS) : ''); } if (isset($parts['host'])) { @@ -301,7 +301,7 @@ public static function resolve(UriInterface $base, UriInterface $rel) if ($rel->getAuthority() !== '') { $reset = true; $userInfo = \explode(':', $rel->getUserInfo(), 2); - $new = $base->withUserInfo($userInfo[0], isset($userInfo[1]) ? $userInfo[1]: null)->withHost($rel->getHost())->withPort($rel->getPort()); + $new = $base->withUserInfo($userInfo[0], $userInfo[1] ?? null)->withHost($rel->getHost())->withPort($rel->getPort()); } if ($reset && $rel->getPath() === '') { @@ -334,7 +334,7 @@ public static function resolve(UriInterface $base, UriInterface $rel) */ private static function removeDotSegments($path) { - $segments = array(); + $segments = []; foreach (\explode('/', $path) as $segment) { if ($segment === '..') { \array_pop($segments); diff --git a/src/Middleware/LimitConcurrentRequestsMiddleware.php b/src/Middleware/LimitConcurrentRequestsMiddleware.php index b1c00da0..41477f91 100644 --- a/src/Middleware/LimitConcurrentRequestsMiddleware.php +++ b/src/Middleware/LimitConcurrentRequestsMiddleware.php @@ -6,10 +6,11 @@ use Psr\Http\Message\ServerRequestInterface; use React\Http\Io\HttpBodyStream; use React\Http\Io\PauseBufferStream; -use React\Promise; use React\Promise\PromiseInterface; use React\Promise\Deferred; use React\Stream\ReadableStreamInterface; +use function React\Promise\reject; +use function React\Promise\resolve; /** * Limits how many next handlers can be executed concurrently. @@ -71,7 +72,7 @@ final class LimitConcurrentRequestsMiddleware { private $limit; private $pending = 0; - private $queue = array(); + private $queue = []; /** * @param int $limit Maximum amount of concurrent requests handled. @@ -92,13 +93,9 @@ public function __invoke(ServerRequestInterface $request, $next) try { $response = $next($request); - } catch (\Exception $e) { + } catch (\Throwable $e) { $this->processQueue(); throw $e; - } catch (\Throwable $e) { // @codeCoverageIgnoreStart - // handle Errors just like Exceptions (PHP 7+ only) - $this->processQueue(); - throw $e; // @codeCoverageIgnoreEnd } // happy path: if next request handler returned immediately, @@ -110,7 +107,7 @@ public function __invoke(ServerRequestInterface $request, $next) // if the next handler returns a pending promise, we have to // await its resolution before invoking next queued request - return $this->await(Promise\resolve($response)); + return $this->await(resolve($response)); } // if we reach this point, then this request will need to be queued @@ -130,36 +127,29 @@ public function __invoke(ServerRequestInterface $request, $next) } // get next queue position - $queue =& $this->queue; - $queue[] = null; - \end($queue); - $id = \key($queue); + $this->queue[] = null; + \end($this->queue); + $id = \key($this->queue); - $deferred = new Deferred(function ($_, $reject) use (&$queue, $id) { + $deferred = new Deferred(function ($_, $reject) use ($id) { // queued promise cancelled before its next handler is invoked // remove from queue and reject explicitly - unset($queue[$id]); + unset($this->queue[$id]); $reject(new \RuntimeException('Cancelled queued next handler')); }); // queue request and process queue if pending does not exceed limit - $queue[$id] = $deferred; + $this->queue[$id] = $deferred; - $pending = &$this->pending; - $that = $this; - return $deferred->promise()->then(function () use ($request, $next, $body, &$pending, $that) { + return $deferred->promise()->then(function () use ($request, $next, $body) { // invoke next request handler - ++$pending; + ++$this->pending; try { $response = $next($request); - } catch (\Exception $e) { - $that->processQueue(); + } catch (\Throwable $e) { + $this->processQueue(); throw $e; - } catch (\Throwable $e) { // @codeCoverageIgnoreStart - // handle Errors just like Exceptions (PHP 7+ only) - $that->processQueue(); - throw $e; // @codeCoverageIgnoreEnd } // resume readable stream and replay buffered events @@ -169,27 +159,24 @@ public function __invoke(ServerRequestInterface $request, $next) // if the next handler returns a pending promise, we have to // await its resolution before invoking next queued request - return $that->await(Promise\resolve($response)); + return $this->await(resolve($response)); }); } /** - * @internal * @param PromiseInterface $promise * @return PromiseInterface */ - public function await(PromiseInterface $promise) + private function await(PromiseInterface $promise) { - $that = $this; - - return $promise->then(function ($response) use ($that) { - $that->processQueue(); + return $promise->then(function ($response) { + $this->processQueue(); return $response; - }, function ($error) use ($that) { - $that->processQueue(); + }, function ($error) { + $this->processQueue(); - return Promise\reject($error); + return reject($error); }); } diff --git a/src/Middleware/RequestBodyBufferMiddleware.php b/src/Middleware/RequestBodyBufferMiddleware.php index ddb39f5e..ea889bd3 100644 --- a/src/Middleware/RequestBodyBufferMiddleware.php +++ b/src/Middleware/RequestBodyBufferMiddleware.php @@ -76,11 +76,8 @@ public function __invoke(ServerRequestInterface $request, $next) try { // resolve with result of next handler $resolve($next($request->withBody(new BufferedBody($buffer)))); - } catch (\Exception $e) { + } catch (\Throwable $e) { $reject($e); - } catch (\Throwable $e) { // @codeCoverageIgnoreStart - // reject Errors just like Exceptions (PHP 7+) - $reject($e); // @codeCoverageIgnoreEnd } }); diff --git a/src/Middleware/RequestBodyParserMiddleware.php b/src/Middleware/RequestBodyParserMiddleware.php index be5ba16f..63013337 100644 --- a/src/Middleware/RequestBodyParserMiddleware.php +++ b/src/Middleware/RequestBodyParserMiddleware.php @@ -21,7 +21,7 @@ public function __construct($uploadMaxFilesize = null, $maxFileUploads = null) public function __invoke(ServerRequestInterface $request, $next) { $type = \strtolower($request->getHeaderLine('Content-Type')); - list ($type) = \explode(';', $type); + [$type] = \explode(';', $type); if ($type === 'application/x-www-form-urlencoded') { return $next($this->parseFormUrlencoded($request)); @@ -38,7 +38,7 @@ private function parseFormUrlencoded(ServerRequestInterface $request) { // parse string into array structure // ignore warnings due to excessive data structures (max_input_vars and max_input_nesting_level) - $ret = array(); + $ret = []; @\parse_str((string)$request->getBody(), $ret); return $request->withParsedBody($ret); diff --git a/tests/BrowserTest.php b/tests/BrowserTest.php index fb1a1beb..a7188b2c 100644 --- a/tests/BrowserTest.php +++ b/tests/BrowserTest.php @@ -87,9 +87,8 @@ public function testConstructWithLoopAssignsGivenLoop() public function testGetSendsGetRequest() { - $that = $this; - $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { - $that->assertEquals('GET', $request->getMethod()); + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) { + $this->assertEquals('GET', $request->getMethod()); return true; }))->willReturn(new Promise(function () { })); @@ -98,9 +97,8 @@ public function testGetSendsGetRequest() public function testPostSendsPostRequest() { - $that = $this; - $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { - $that->assertEquals('POST', $request->getMethod()); + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) { + $this->assertEquals('POST', $request->getMethod()); return true; }))->willReturn(new Promise(function () { })); @@ -109,9 +107,8 @@ public function testPostSendsPostRequest() public function testHeadSendsHeadRequest() { - $that = $this; - $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { - $that->assertEquals('HEAD', $request->getMethod()); + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) { + $this->assertEquals('HEAD', $request->getMethod()); return true; }))->willReturn(new Promise(function () { })); @@ -120,9 +117,8 @@ public function testHeadSendsHeadRequest() public function testPatchSendsPatchRequest() { - $that = $this; - $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { - $that->assertEquals('PATCH', $request->getMethod()); + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) { + $this->assertEquals('PATCH', $request->getMethod()); return true; }))->willReturn(new Promise(function () { })); @@ -131,9 +127,8 @@ public function testPatchSendsPatchRequest() public function testPutSendsPutRequest() { - $that = $this; - $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { - $that->assertEquals('PUT', $request->getMethod()); + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) { + $this->assertEquals('PUT', $request->getMethod()); return true; }))->willReturn(new Promise(function () { })); @@ -142,9 +137,8 @@ public function testPutSendsPutRequest() public function testDeleteSendsDeleteRequest() { - $that = $this; - $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { - $that->assertEquals('DELETE', $request->getMethod()); + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) { + $this->assertEquals('DELETE', $request->getMethod()); return true; }))->willReturn(new Promise(function () { })); @@ -153,11 +147,10 @@ public function testDeleteSendsDeleteRequest() public function testRequestOptionsSendsPutRequestWithStreamingExplicitlyDisabled() { - $this->sender->expects($this->once())->method('withOptions')->with(array('streaming' => false))->willReturnSelf(); + $this->sender->expects($this->once())->method('withOptions')->with(['streaming' => false])->willReturnSelf(); - $that = $this; - $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { - $that->assertEquals('OPTIONS', $request->getMethod()); + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) { + $this->assertEquals('OPTIONS', $request->getMethod()); return true; }))->willReturn(new Promise(function () { })); @@ -166,11 +159,10 @@ public function testRequestOptionsSendsPutRequestWithStreamingExplicitlyDisabled public function testRequestStreamingGetSendsGetRequestWithStreamingExplicitlyEnabled() { - $this->sender->expects($this->once())->method('withOptions')->with(array('streaming' => true))->willReturnSelf(); + $this->sender->expects($this->once())->method('withOptions')->with(['streaming' => true])->willReturnSelf(); - $that = $this; - $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { - $that->assertEquals('GET', $request->getMethod()); + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) { + $this->assertEquals('GET', $request->getMethod()); return true; }))->willReturn(new Promise(function () { })); @@ -179,77 +171,77 @@ public function testRequestStreamingGetSendsGetRequestWithStreamingExplicitlyEna public function testWithTimeoutTrueSetsDefaultTimeoutOption() { - $this->sender->expects($this->once())->method('withOptions')->with(array('timeout' => null))->willReturnSelf(); + $this->sender->expects($this->once())->method('withOptions')->with(['timeout' => null])->willReturnSelf(); $this->browser->withTimeout(true); } public function testWithTimeoutFalseSetsNegativeTimeoutOption() { - $this->sender->expects($this->once())->method('withOptions')->with(array('timeout' => -1))->willReturnSelf(); + $this->sender->expects($this->once())->method('withOptions')->with(['timeout' => -1])->willReturnSelf(); $this->browser->withTimeout(false); } public function testWithTimeout10SetsTimeoutOption() { - $this->sender->expects($this->once())->method('withOptions')->with(array('timeout' => 10))->willReturnSelf(); + $this->sender->expects($this->once())->method('withOptions')->with(['timeout' => 10])->willReturnSelf(); $this->browser->withTimeout(10); } public function testWithTimeoutNegativeSetsZeroTimeoutOption() { - $this->sender->expects($this->once())->method('withOptions')->with(array('timeout' => null))->willReturnSelf(); + $this->sender->expects($this->once())->method('withOptions')->with(['timeout' => null])->willReturnSelf(); $this->browser->withTimeout(-10); } public function testWithFollowRedirectsTrueSetsSenderOption() { - $this->sender->expects($this->once())->method('withOptions')->with(array('followRedirects' => true, 'maxRedirects' => null))->willReturnSelf(); + $this->sender->expects($this->once())->method('withOptions')->with(['followRedirects' => true, 'maxRedirects' => null])->willReturnSelf(); $this->browser->withFollowRedirects(true); } public function testWithFollowRedirectsFalseSetsSenderOption() { - $this->sender->expects($this->once())->method('withOptions')->with(array('followRedirects' => false, 'maxRedirects' => null))->willReturnSelf(); + $this->sender->expects($this->once())->method('withOptions')->with(['followRedirects' => false, 'maxRedirects' => null])->willReturnSelf(); $this->browser->withFollowRedirects(false); } public function testWithFollowRedirectsTenSetsSenderOption() { - $this->sender->expects($this->once())->method('withOptions')->with(array('followRedirects' => true, 'maxRedirects' => 10))->willReturnSelf(); + $this->sender->expects($this->once())->method('withOptions')->with(['followRedirects' => true, 'maxRedirects' => 10])->willReturnSelf(); $this->browser->withFollowRedirects(10); } public function testWithFollowRedirectsZeroSetsSenderOption() { - $this->sender->expects($this->once())->method('withOptions')->with(array('followRedirects' => true, 'maxRedirects' => 0))->willReturnSelf(); + $this->sender->expects($this->once())->method('withOptions')->with(['followRedirects' => true, 'maxRedirects' => 0])->willReturnSelf(); $this->browser->withFollowRedirects(0); } public function testWithRejectErrorResponseTrueSetsSenderOption() { - $this->sender->expects($this->once())->method('withOptions')->with(array('obeySuccessCode' => true))->willReturnSelf(); + $this->sender->expects($this->once())->method('withOptions')->with(['obeySuccessCode' => true])->willReturnSelf(); $this->browser->withRejectErrorResponse(true); } public function testWithRejectErrorResponseFalseSetsSenderOption() { - $this->sender->expects($this->once())->method('withOptions')->with(array('obeySuccessCode' => false))->willReturnSelf(); + $this->sender->expects($this->once())->method('withOptions')->with(['obeySuccessCode' => false])->willReturnSelf(); $this->browser->withRejectErrorResponse(false); } public function testWithResponseBufferThousandSetsSenderOption() { - $this->sender->expects($this->once())->method('withOptions')->with(array('maximumSize' => 1000))->willReturnSelf(); + $this->sender->expects($this->once())->method('withOptions')->with(['maximumSize' => 1000])->willReturnSelf(); $this->browser->withResponseBuffer(1000); } @@ -264,103 +256,103 @@ public function testWithBase() public function provideOtherUris() { - return array( - 'empty returns base' => array( + return [ + 'empty returns base' => [ 'http://example.com/base', '', 'http://example.com/base', - ), - 'absolute same as base returns base' => array( + ], + 'absolute same as base returns base' => [ 'http://example.com/base', 'http://example.com/base', 'http://example.com/base', - ), - 'absolute below base returns absolute' => array( + ], + 'absolute below base returns absolute' => [ 'http://example.com/base', 'http://example.com/base/another', 'http://example.com/base/another', - ), - 'slash returns base without path' => array( + ], + 'slash returns base without path' => [ 'http://example.com/base', '/', 'http://example.com/', - ), - 'relative is added behind base' => array( + ], + 'relative is added behind base' => [ 'http://example.com/base/', 'test', 'http://example.com/base/test', - ), - 'relative is added behind base without path' => array( + ], + 'relative is added behind base without path' => [ 'http://example.com/base', 'test', 'http://example.com/test', - ), - 'relative level up is added behind parent path' => array( + ], + 'relative level up is added behind parent path' => [ 'http://example.com/base/foo/', '../bar', 'http://example.com/base/bar', - ), - 'absolute with slash is added behind base without path' => array( + ], + 'absolute with slash is added behind base without path' => [ 'http://example.com/base', '/test', 'http://example.com/test', - ), - 'query string is added behind base' => array( + ], + 'query string is added behind base' => [ 'http://example.com/base', '?key=value', 'http://example.com/base?key=value', - ), - 'query string is added behind base with slash' => array( + ], + 'query string is added behind base with slash' => [ 'http://example.com/base/', '?key=value', 'http://example.com/base/?key=value', - ), - 'query string with slash is added behind base without path' => array( + ], + 'query string with slash is added behind base without path' => [ 'http://example.com/base', '/?key=value', 'http://example.com/?key=value', - ), - 'absolute with query string below base is returned as-is' => array( + ], + 'absolute with query string below base is returned as-is' => [ 'http://example.com/base', 'http://example.com/base?test', 'http://example.com/base?test', - ), - 'urlencoded special chars will stay as-is' => array( + ], + 'urlencoded special chars will stay as-is' => [ 'http://example.com/%7Bversion%7D/', '', 'http://example.com/%7Bversion%7D/' - ), - 'special chars will be urlencoded' => array( + ], + 'special chars will be urlencoded' => [ 'http://example.com/{version}/', '', 'http://example.com/%7Bversion%7D/' - ), - 'other domain' => array( + ], + 'other domain' => [ 'http://example.com/base/', 'http://example.org/base/', 'http://example.org/base/' - ), - 'other scheme' => array( + ], + 'other scheme' => [ 'http://example.com/base/', 'https://example.com/base/', 'https://example.com/base/' - ), - 'other port' => array( + ], + 'other port' => [ 'http://example.com/base/', 'http://example.com:81/base/', 'http://example.com:81/base/' - ), - 'other path' => array( + ], + 'other path' => [ 'http://example.com/base/', 'http://example.com/other/', 'http://example.com/other/' - ), - 'other path due to missing slash' => array( + ], + 'other path due to missing slash' => [ 'http://example.com/base/', 'http://example.com/other', 'http://example.com/other' - ), - ); + ], + ]; } /** @@ -372,9 +364,8 @@ public function testResolveUriWithBaseEndsWithoutSlash($base, $uri, $expectedAbs { $browser = $this->browser->withBase($base); - $that = $this; - $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($expectedAbsolute, $that) { - $that->assertEquals($expectedAbsolute, $request->getUri()); + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($expectedAbsolute) { + $this->assertEquals($expectedAbsolute, $request->getUri()); return true; }))->willReturn(new Promise(function () { })); @@ -397,9 +388,8 @@ public function testWithoutBaseFollowedByGetRequestTriesToSendIncompleteRequestU { $this->browser = $this->browser->withBase('http://example.com')->withBase(null); - $that = $this; - $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { - $that->assertEquals('path', $request->getUri()); + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) { + $this->assertEquals('path', $request->getUri()); return true; }))->willReturn(new Promise(function () { })); @@ -410,9 +400,8 @@ public function testWithProtocolVersionFollowedByGetRequestSendsRequestWithProto { $this->browser = $this->browser->withProtocolVersion('1.0'); - $that = $this; - $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { - $that->assertEquals('1.0', $request->getProtocolVersion()); + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) { + $this->assertEquals('1.0', $request->getProtocolVersion()); return true; }))->willReturn(new Promise(function () { })); @@ -443,9 +432,8 @@ public function testWithHeaderShouldOverwriteExistingHeader() $this->browser = $this->browser->withHeader('User-Agent', 'ACMC'); //should be overwritten $this->browser = $this->browser->withHeader('user-agent', 'ABC'); //should be the user-agent - $that = $this; - $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { - $that->assertEquals(array('ABC'), $request->getHeader('UsEr-AgEnT')); + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) { + $this->assertEquals(['ABC'], $request->getHeader('UsEr-AgEnT')); return true; }))->willReturn(new Promise(function () { })); @@ -456,13 +444,12 @@ public function testWithHeaderShouldBeOverwrittenByExplicitHeaderInGetMethod() { $this->browser = $this->browser->withHeader('User-Agent', 'ACMC'); - $that = $this; - $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { - $that->assertEquals(array('ABC'), $request->getHeader('UsEr-AgEnT')); + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) { + $this->assertEquals(['ABC'], $request->getHeader('UsEr-AgEnT')); return true; }))->willReturn(new Promise(function () { })); - $this->browser->get('http://example.com/', array('user-Agent' => 'ABC')); //should win + $this->browser->get('http://example.com/', ['user-Agent' => 'ABC']); //should win } public function testWithMultipleHeadersShouldBeMergedCorrectlyWithMultipleDefaultHeaders() @@ -472,28 +459,27 @@ public function testWithMultipleHeadersShouldBeMergedCorrectlyWithMultipleDefaul $this->browser = $this->browser->withHeader('Custom-HEADER', 'custom'); $this->browser = $this->browser->withHeader('just-a-header', 'header-value'); - $that = $this; - $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { - $expectedHeaders = array( - 'Host' => array('example.com'), + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) { + $expectedHeaders = [ + 'Host' => ['example.com'], - 'User-Test' => array('Test'), - 'just-a-header' => array('header-value'), + 'User-Test' => ['Test'], + 'just-a-header' => ['header-value'], - 'user-Agent' => array('ABC'), - 'another-header' => array('value'), - 'custom-header' => array('data'), - ); + 'user-Agent' => ['ABC'], + 'another-header' => ['value'], + 'custom-header' => ['data'], + ]; - $that->assertEquals($expectedHeaders, $request->getHeaders()); + $this->assertEquals($expectedHeaders, $request->getHeaders()); return true; }))->willReturn(new Promise(function () { })); - $headers = array( + $headers = [ 'user-Agent' => 'ABC', //should overwrite: 'User-Agent', 'ACMC' 'another-header' => 'value', 'custom-header' => 'data', //should overwrite: 'Custom-header', 'custom' - ); + ]; $this->browser->get('http://example.com/', $headers); } @@ -502,9 +488,8 @@ public function testWithoutHeaderShouldRemoveExistingHeader() $this->browser = $this->browser->withHeader('User-Agent', 'ACMC'); $this->browser = $this->browser->withoutHeader('UsEr-AgEnT'); //should remove case-insensitive header - $that = $this; - $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { - $that->assertEquals(array(), $request->getHeader('user-agent')); + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) { + $this->assertEquals([], $request->getHeader('user-agent')); return true; }))->willReturn(new Promise(function () { })); @@ -515,9 +500,8 @@ public function testWithoutHeaderConnectionShouldRemoveDefaultConnectionHeader() { $this->browser = $this->browser->withoutHeader('Connection'); - $that = $this; - $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { - $that->assertEquals(array(), $request->getHeader('Connection')); + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) { + $this->assertEquals([], $request->getHeader('Connection')); return true; }))->willReturn(new Promise(function () { })); @@ -528,9 +512,8 @@ public function testWithHeaderConnectionShouldOverwriteDefaultConnectionHeader() { $this->browser = $this->browser->withHeader('Connection', 'keep-alive'); - $that = $this; - $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { - $that->assertEquals(array('keep-alive'), $request->getHeader('Connection')); + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) { + $this->assertEquals(['keep-alive'], $request->getHeader('Connection')); return true; }))->willReturn(new Promise(function () { })); @@ -539,9 +522,8 @@ public function testWithHeaderConnectionShouldOverwriteDefaultConnectionHeader() public function testBrowserShouldSendDefaultUserAgentHeader() { - $that = $this; - $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { - $that->assertEquals(array(0 => 'ReactPHP/1'), $request->getHeader('user-agent')); + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) { + $this->assertEquals([0 => 'ReactPHP/1'], $request->getHeader('user-agent')); return true; }))->willReturn(new Promise(function () { })); @@ -552,9 +534,8 @@ public function testBrowserShouldNotSendDefaultUserAgentHeaderIfWithoutHeaderRem { $this->browser = $this->browser->withoutHeader('UsEr-AgEnT'); - $that = $this; - $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { - $that->assertEquals(array(), $request->getHeader('User-Agent')); + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) { + $this->assertEquals([], $request->getHeader('User-Agent')); return true; }))->willReturn(new Promise(function () { })); diff --git a/tests/Client/FunctionalIntegrationTest.php b/tests/Client/FunctionalIntegrationTest.php index 6c49c127..b727a334 100644 --- a/tests/Client/FunctionalIntegrationTest.php +++ b/tests/Client/FunctionalIntegrationTest.php @@ -7,14 +7,15 @@ use React\Http\Client\Client; use React\Http\Io\ClientConnectionManager; use React\Http\Message\Request; -use React\Promise\Deferred; use React\Promise\Promise; -use React\Promise\Stream; use React\Socket\ConnectionInterface; use React\Socket\Connector; use React\Socket\SocketServer; use React\Stream\ReadableStreamInterface; use React\Tests\Http\TestCase; +use function React\Async\await; +use function React\Promise\Stream\first; +use function React\Promise\Timer\timeout; class FunctionalIntegrationTest extends TestCase { @@ -49,12 +50,12 @@ public function testRequestToLocalhostEmitsSingleRemoteConnection() $port = parse_url($socket->getAddress(), PHP_URL_PORT); $client = new Client(new ClientConnectionManager(new Connector(), Loop::get())); - $request = $client->request(new Request('GET', 'http://localhost:' . $port, array(), '', '1.0')); + $request = $client->request(new Request('GET', 'http://localhost:' . $port, [], '', '1.0')); - $promise = Stream\first($request, 'close'); + $promise = first($request, 'close'); $request->end(); - \React\Async\await(\React\Promise\Timer\timeout($promise, self::TIMEOUT_LOCAL)); + await(timeout($promise, self::TIMEOUT_LOCAL)); } public function testRequestToLocalhostWillConnectAndCloseConnectionAfterResponseWhenKeepAliveTimesOut() @@ -74,11 +75,11 @@ public function testRequestToLocalhostWillConnectAndCloseConnectionAfterResponse $port = parse_url($socket->getAddress(), PHP_URL_PORT); $client = new Client(new ClientConnectionManager(new Connector(), Loop::get())); - $request = $client->request(new Request('GET', 'http://localhost:' . $port, array(), '', '1.1')); + $request = $client->request(new Request('GET', 'http://localhost:' . $port, [], '', '1.1')); $request->end(); - \React\Async\await(\React\Promise\Timer\timeout($promise, self::TIMEOUT_LOCAL)); + await(timeout($promise, self::TIMEOUT_LOCAL)); } public function testRequestToLocalhostWillReuseExistingConnectionForSecondRequest() @@ -96,17 +97,17 @@ public function testRequestToLocalhostWillReuseExistingConnectionForSecondReques $client = new Client(new ClientConnectionManager(new Connector(), Loop::get())); - $request = $client->request(new Request('GET', 'http://localhost:' . $port, array(), '', '1.1')); - $promise = Stream\first($request, 'close'); + $request = $client->request(new Request('GET', 'http://localhost:' . $port, [], '', '1.1')); + $promise = first($request, 'close'); $request->end(); - \React\Async\await(\React\Promise\Timer\timeout($promise, self::TIMEOUT_LOCAL)); + await(timeout($promise, self::TIMEOUT_LOCAL)); - $request = $client->request(new Request('GET', 'http://localhost:' . $port, array(), '', '1.1')); - $promise = Stream\first($request, 'close'); + $request = $client->request(new Request('GET', 'http://localhost:' . $port, [], '', '1.1')); + $promise = first($request, 'close'); $request->end(); - \React\Async\await(\React\Promise\Timer\timeout($promise, self::TIMEOUT_LOCAL)); + await(timeout($promise, self::TIMEOUT_LOCAL)); } public function testRequestLegacyHttpServerWithOnlyLineFeedReturnsSuccessfulResponse() @@ -118,17 +119,17 @@ public function testRequestLegacyHttpServerWithOnlyLineFeedReturnsSuccessfulResp }); $client = new Client(new ClientConnectionManager(new Connector(), Loop::get())); - $request = $client->request(new Request('GET', str_replace('tcp:', 'http:', $socket->getAddress()), array(), '', '1.0')); + $request = $client->request(new Request('GET', str_replace('tcp:', 'http:', $socket->getAddress()), [], '', '1.0')); $once = $this->expectCallableOnceWith('body'); $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($once) { $body->on('data', $once); }); - $promise = Stream\first($request, 'close'); + $promise = first($request, 'close'); $request->end(); - \React\Async\await(\React\Promise\Timer\timeout($promise, self::TIMEOUT_LOCAL)); + await(timeout($promise, self::TIMEOUT_LOCAL)); } /** @group internet */ @@ -136,17 +137,17 @@ public function testSuccessfulResponseEmitsEnd() { $client = new Client(new ClientConnectionManager(new Connector(), Loop::get())); - $request = $client->request(new Request('GET', 'http://www.google.com/', array(), '', '1.0')); + $request = $client->request(new Request('GET', 'http://www.google.com/', [], '', '1.0')); $once = $this->expectCallableOnce(); $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($once) { $body->on('end', $once); }); - $promise = Stream\first($request, 'close'); + $promise = first($request, 'close'); $request->end(); - \React\Async\await(\React\Promise\Timer\timeout($promise, self::TIMEOUT_REMOTE)); + await(timeout($promise, self::TIMEOUT_REMOTE)); } /** @group internet */ @@ -154,7 +155,7 @@ public function testCancelPendingConnectionEmitsClose() { $client = new Client(new ClientConnectionManager(new Connector(), Loop::get())); - $request = $client->request(new Request('GET', 'http://www.google.com/', array(), '', '1.0')); + $request = $client->request(new Request('GET', 'http://www.google.com/', [], '', '1.0')); $request->on('error', $this->expectCallableNever()); $request->on('close', $this->expectCallableOnce()); $request->end(); diff --git a/tests/FunctionalBrowserTest.php b/tests/FunctionalBrowserTest.php index 6a235703..92c873d1 100644 --- a/tests/FunctionalBrowserTest.php +++ b/tests/FunctionalBrowserTest.php @@ -10,12 +10,16 @@ use React\Http\Message\Response; use React\Http\Message\ResponseException; use React\Http\Middleware\StreamingRequestMiddleware; +use React\Promise\Deferred; use React\Promise\Promise; -use React\Promise\Stream; +use React\Socket\ConnectionInterface; use React\Socket\Connector; use React\Socket\SocketServer; use React\Stream\ReadableStreamInterface; use React\Stream\ThroughStream; +use function React\Async\await; +use function React\Promise\Stream\buffer; +use function React\Promise\Timer\timeout; class FunctionalBrowserTest extends TestCase { @@ -35,7 +39,7 @@ public function setUpBrowserAndServer() $http = new HttpServer(new StreamingRequestMiddleware(), function (ServerRequestInterface $request) { $path = $request->getUri()->getPath(); - $headers = array(); + $headers = []; foreach ($request->getHeaders() as $name => $values) { $headers[$name] = implode(', ', $values); } @@ -43,7 +47,7 @@ public function setUpBrowserAndServer() if ($path === '/get') { return new Response( 200, - array(), + [], 'hello' ); } @@ -52,14 +56,14 @@ public function setUpBrowserAndServer() $params = $request->getQueryParams(); return new Response( 302, - array('Location' => $params['url']) + ['Location' => $params['url']] ); } if ($path === '/basic-auth/user/pass') { return new Response( $request->getHeaderLine('Authorization') === 'Basic dXNlcjpwYXNz' ? 200 : 401, - array(), + [], '' ); } @@ -67,7 +71,7 @@ public function setUpBrowserAndServer() if ($path === '/status/204') { return new Response( 204, - array(), + [], '' ); } @@ -75,7 +79,7 @@ public function setUpBrowserAndServer() if ($path === '/status/304') { return new Response( 304, - array(), + [], 'Not modified' ); } @@ -83,7 +87,7 @@ public function setUpBrowserAndServer() if ($path === '/status/404') { return new Response( 404, - array(), + [], '' ); } @@ -94,7 +98,7 @@ public function setUpBrowserAndServer() $timer = Loop::addTimer(10, function () use ($resolve) { $resolve(new Response( 200, - array(), + [], 'hello' )); }); @@ -116,11 +120,11 @@ public function setUpBrowserAndServer() $body->on('close', function () use (&$buffer, $resolve, $headers) { $resolve(new Response( 200, - array(), - json_encode(array( + [], + json_encode([ 'data' => $buffer, 'headers' => $headers - )) + ]) )); }); }); @@ -130,14 +134,14 @@ public function setUpBrowserAndServer() $stream = new ThroughStream(); Loop::futureTick(function () use ($stream, $headers) { - $stream->end(json_encode(array( + $stream->end(json_encode([ 'headers' => $headers - ))); + ])); }); return new Response( 200, - array(), + [], $stream ); } @@ -165,7 +169,7 @@ public function cleanUpSocketServer() */ public function testSimpleRequest() { - \React\Async\await($this->browser->get($this->base . 'get')); + await($this->browser->get($this->base . 'get')); } public function testGetRequestWithRelativeAddressRejects() @@ -173,7 +177,7 @@ public function testGetRequestWithRelativeAddressRejects() $promise = $this->browser->get('delay'); $this->setExpectedException('InvalidArgumentException', 'Invalid request URL given'); - \React\Async\await($promise); + await($promise); } /** @@ -181,7 +185,7 @@ public function testGetRequestWithRelativeAddressRejects() */ public function testGetRequestWithBaseAndRelativeAddressResolves() { - \React\Async\await($this->browser->withBase($this->base)->get('get')); + await($this->browser->withBase($this->base)->get('get')); } /** @@ -189,7 +193,7 @@ public function testGetRequestWithBaseAndRelativeAddressResolves() */ public function testGetRequestWithBaseAndFullAddressResolves() { - \React\Async\await($this->browser->withBase('http://example.com/')->get($this->base . 'get')); + await($this->browser->withBase('http://example.com/')->get($this->base . 'get')); } public function testCancelGetRequestWillRejectRequest() @@ -198,7 +202,7 @@ public function testCancelGetRequestWillRejectRequest() $promise->cancel(); $this->setExpectedException('RuntimeException'); - \React\Async\await($promise); + await($promise); } public function testCancelRequestWithPromiseFollowerWillRejectRequest() @@ -209,13 +213,13 @@ public function testCancelRequestWithPromiseFollowerWillRejectRequest() $promise->cancel(); $this->setExpectedException('RuntimeException'); - \React\Async\await($promise); + await($promise); } public function testRequestWithoutAuthenticationFails() { $this->setExpectedException('RuntimeException'); - \React\Async\await($this->browser->get($this->base . 'basic-auth/user/pass')); + await($this->browser->get($this->base . 'basic-auth/user/pass')); } /** @@ -225,7 +229,7 @@ public function testRequestWithAuthenticationSucceeds() { $base = str_replace('://', '://user:pass@', $this->base); - \React\Async\await($this->browser->get($base . 'basic-auth/user/pass')); + await($this->browser->get($base . 'basic-auth/user/pass')); } /** @@ -239,7 +243,7 @@ public function testRedirectToPageWithAuthenticationSendsAuthenticationFromLocat { $target = str_replace('://', '://user:pass@', $this->base) . 'basic-auth/user/pass'; - \React\Async\await($this->browser->get($this->base . 'redirect-to?url=' . urlencode($target))); + await($this->browser->get($this->base . 'redirect-to?url=' . urlencode($target))); } /** @@ -254,7 +258,7 @@ public function testRedirectFromPageWithInvalidAuthToPageWithCorrectAuthenticati $base = str_replace('://', '://unknown:invalid@', $this->base); $target = str_replace('://', '://user:pass@', $this->base) . 'basic-auth/user/pass'; - \React\Async\await($this->browser->get($base . 'redirect-to?url=' . urlencode($target))); + await($this->browser->get($base . 'redirect-to?url=' . urlencode($target))); } public function testCancelRedirectedRequestShouldReject() @@ -266,7 +270,7 @@ public function testCancelRedirectedRequestShouldReject() }); $this->setExpectedException('RuntimeException', 'Request cancelled'); - \React\Async\await($promise); + await($promise); } public function testTimeoutDelayedResponseShouldReject() @@ -274,17 +278,17 @@ public function testTimeoutDelayedResponseShouldReject() $promise = $this->browser->withTimeout(0.1)->get($this->base . 'delay/10'); $this->setExpectedException('RuntimeException', 'Request timed out after 0.1 seconds'); - \React\Async\await($promise); + await($promise); } public function testTimeoutDelayedResponseAfterStreamingRequestShouldReject() { $stream = new ThroughStream(); - $promise = $this->browser->withTimeout(0.1)->post($this->base . 'delay/10', array(), $stream); + $promise = $this->browser->withTimeout(0.1)->post($this->base . 'delay/10', [], $stream); $stream->end(); $this->setExpectedException('RuntimeException', 'Request timed out after 0.1 seconds'); - \React\Async\await($promise); + await($promise); } /** @@ -292,7 +296,7 @@ public function testTimeoutDelayedResponseAfterStreamingRequestShouldReject() */ public function testTimeoutFalseShouldResolveSuccessfully() { - \React\Async\await($this->browser->withTimeout(false)->get($this->base . 'get')); + await($this->browser->withTimeout(false)->get($this->base . 'get')); } /** @@ -300,7 +304,7 @@ public function testTimeoutFalseShouldResolveSuccessfully() */ public function testRedirectRequestRelative() { - \React\Async\await($this->browser->get($this->base . 'redirect-to?url=get')); + await($this->browser->get($this->base . 'redirect-to?url=get')); } /** @@ -308,7 +312,7 @@ public function testRedirectRequestRelative() */ public function testRedirectRequestAbsolute() { - \React\Async\await($this->browser->get($this->base . 'redirect-to?url=' . urlencode($this->base . 'get'))); + await($this->browser->get($this->base . 'redirect-to?url=' . urlencode($this->base . 'get'))); } /** @@ -318,7 +322,7 @@ public function testFollowingRedirectsFalseResolvesWithRedirectResult() { $browser = $this->browser->withFollowRedirects(false); - \React\Async\await($browser->get($this->base . 'redirect-to?url=get')); + await($browser->get($this->base . 'redirect-to?url=get')); } public function testFollowRedirectsZeroRejectsOnRedirect() @@ -326,12 +330,12 @@ public function testFollowRedirectsZeroRejectsOnRedirect() $browser = $this->browser->withFollowRedirects(0); $this->setExpectedException('RuntimeException'); - \React\Async\await($browser->get($this->base . 'redirect-to?url=get')); + await($browser->get($this->base . 'redirect-to?url=get')); } public function testResponseStatus204ShouldResolveWithEmptyBody() { - $response = \React\Async\await($this->browser->get($this->base . 'status/204')); + $response = await($this->browser->get($this->base . 'status/204')); $this->assertFalse($response->hasHeader('Content-Length')); $body = $response->getBody(); @@ -341,7 +345,7 @@ public function testResponseStatus204ShouldResolveWithEmptyBody() public function testResponseStatus304ShouldResolveWithEmptyBodyButContentLengthResponseHeader() { - $response = \React\Async\await($this->browser->get($this->base . 'status/304')); + $response = await($this->browser->get($this->base . 'status/304')); $this->assertEquals('12', $response->getHeaderLine('Content-Length')); $body = $response->getBody(); @@ -356,7 +360,7 @@ public function testGetRequestWithResponseBufferMatchedExactlyResolves() { $promise = $this->browser->withResponseBuffer(5)->get($this->base . 'get'); - \React\Async\await($promise); + await($promise); } public function testGetRequestWithResponseBufferExceededRejects() @@ -368,7 +372,7 @@ public function testGetRequestWithResponseBufferExceededRejects() 'Response body size of 5 bytes exceeds maximum of 4 bytes', defined('SOCKET_EMSGSIZE') ? SOCKET_EMSGSIZE : 0 ); - \React\Async\await($promise); + await($promise); } public function testGetRequestWithResponseBufferExceededDuringStreamingRejects() @@ -380,7 +384,7 @@ public function testGetRequestWithResponseBufferExceededDuringStreamingRejects() 'Response body size exceeds maximum of 4 bytes', defined('SOCKET_EMSGSIZE') ? SOCKET_EMSGSIZE : 0 ); - \React\Async\await($promise); + await($promise); } /** @@ -389,7 +393,7 @@ public function testGetRequestWithResponseBufferExceededDuringStreamingRejects() */ public function testCanAccessHttps() { - \React\Async\await($this->browser->get('https://www.google.com/')); + await($this->browser->get('https://www.google.com/')); } /** @@ -397,16 +401,16 @@ public function testCanAccessHttps() */ public function testVerifyPeerEnabledForBadSslRejects() { - $connector = new Connector(array( - 'tls' => array( + $connector = new Connector([ + 'tls' => [ 'verify_peer' => true - ) - )); + ] + ]); $browser = new Browser($connector); $this->setExpectedException('RuntimeException'); - \React\Async\await($browser->get('https://self-signed.badssl.com/')); + await($browser->get('https://self-signed.badssl.com/')); } /** @@ -415,15 +419,15 @@ public function testVerifyPeerEnabledForBadSslRejects() */ public function testVerifyPeerDisabledForBadSslResolves() { - $connector = new Connector(array( - 'tls' => array( + $connector = new Connector([ + 'tls' => [ 'verify_peer' => false - ) - )); + ] + ]); $browser = new Browser($connector); - \React\Async\await($browser->get('https://self-signed.badssl.com/')); + await($browser->get('https://self-signed.badssl.com/')); } /** @@ -432,13 +436,13 @@ public function testVerifyPeerDisabledForBadSslResolves() public function testInvalidPort() { $this->setExpectedException('RuntimeException'); - \React\Async\await($this->browser->get('http://www.google.com:443/')); + await($this->browser->get('http://www.google.com:443/')); } public function testErrorStatusCodeRejectsWithResponseException() { try { - \React\Async\await($this->browser->get($this->base . 'status/404')); + await($this->browser->get($this->base . 'status/404')); $this->fail(); } catch (ResponseException $e) { $this->assertEquals(404, $e->getCode()); @@ -450,14 +454,14 @@ public function testErrorStatusCodeRejectsWithResponseException() public function testErrorStatusCodeDoesNotRejectWithRejectErrorResponseFalse() { - $response = \React\Async\await($this->browser->withRejectErrorResponse(false)->get($this->base . 'status/404')); + $response = await($this->browser->withRejectErrorResponse(false)->get($this->base . 'status/404')); $this->assertEquals(404, $response->getStatusCode()); } public function testPostString() { - $response = \React\Async\await($this->browser->post($this->base . 'post', array(), 'hello world')); + $response = await($this->browser->post($this->base . 'post', [], 'hello world')); $data = json_decode((string)$response->getBody(), true); $this->assertEquals('hello world', $data['data']); @@ -465,7 +469,7 @@ public function testPostString() public function testRequestStreamReturnsResponseBodyUntilConnectionsEndsForHttp10() { - $response = \React\Async\await($this->browser->withProtocolVersion('1.0')->get($this->base . 'stream/1')); + $response = await($this->browser->withProtocolVersion('1.0')->get($this->base . 'stream/1')); $this->assertEquals('1.0', $response->getProtocolVersion()); $this->assertFalse($response->hasHeader('Transfer-Encoding')); @@ -476,7 +480,7 @@ public function testRequestStreamReturnsResponseBodyUntilConnectionsEndsForHttp1 public function testRequestStreamReturnsResponseWithTransferEncodingChunkedAndResponseBodyDecodedForHttp11() { - $response = \React\Async\await($this->browser->get($this->base . 'stream/1')); + $response = await($this->browser->get($this->base . 'stream/1')); $this->assertEquals('1.1', $response->getProtocolVersion()); @@ -488,7 +492,7 @@ public function testRequestStreamReturnsResponseWithTransferEncodingChunkedAndRe public function testRequestStreamWithHeadRequestReturnsEmptyResponseBodWithTransferEncodingChunkedForHttp11() { - $response = \React\Async\await($this->browser->head($this->base . 'stream/1')); + $response = await($this->browser->head($this->base . 'stream/1')); $this->assertEquals('1.1', $response->getProtocolVersion()); @@ -499,7 +503,7 @@ public function testRequestStreamWithHeadRequestReturnsEmptyResponseBodWithTrans public function testRequestStreamReturnsResponseWithResponseBodyUndecodedWhenResponseHasDoubleTransferEncoding() { $socket = new SocketServer('127.0.0.1:0'); - $socket->on('connection', function (\React\Socket\ConnectionInterface $connection) { + $socket->on('connection', function (ConnectionInterface $connection) { $connection->on('data', function () use ($connection) { $connection->end("HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked, chunked\r\nConnection: close\r\n\r\nhello"); }); @@ -507,7 +511,7 @@ public function testRequestStreamReturnsResponseWithResponseBodyUndecodedWhenRes $this->base = str_replace('tcp:', 'http:', $socket->getAddress()) . '/'; - $response = \React\Async\await($this->browser->get($this->base . 'stream/1')); + $response = await($this->browser->get($this->base . 'stream/1')); $socket->close(); @@ -519,9 +523,9 @@ public function testRequestStreamReturnsResponseWithResponseBodyUndecodedWhenRes public function testReceiveStreamAndExplicitlyCloseConnectionEvenWhenServerKeepsConnectionOpen() { - $closed = new \React\Promise\Deferred(); + $closed = new Deferred(); $socket = new SocketServer('127.0.0.1:0'); - $socket->on('connection', function (\React\Socket\ConnectionInterface $connection) use ($closed) { + $socket->on('connection', function (ConnectionInterface $connection) use ($closed) { $connection->on('data', function () use ($connection) { $connection->write("HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello"); }); @@ -532,10 +536,10 @@ public function testReceiveStreamAndExplicitlyCloseConnectionEvenWhenServerKeeps $this->base = str_replace('tcp:', 'http:', $socket->getAddress()) . '/'; - $response = \React\Async\await($this->browser->get($this->base . 'get', array())); + $response = await($this->browser->get($this->base . 'get', [])); $this->assertEquals('hello', (string)$response->getBody()); - $ret = \React\Async\await(\React\Promise\Timer\timeout($closed->promise(), 0.1)); + $ret = await(timeout($closed->promise(), 0.1)); $this->assertTrue($ret); $socket->close(); @@ -545,7 +549,7 @@ public function testRequestWithConnectionCloseHeaderWillCreateNewConnectionForSe { $twice = $this->expectCallableOnce(); $socket = new SocketServer('127.0.0.1:0'); - $socket->on('connection', function (\React\Socket\ConnectionInterface $connection) use ($socket, $twice) { + $socket->on('connection', function (ConnectionInterface $connection) use ($socket, $twice) { $connection->on('data', function () use ($connection) { $connection->write("HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello"); }); @@ -561,11 +565,11 @@ public function testRequestWithConnectionCloseHeaderWillCreateNewConnectionForSe // add `Connection: close` request header to disable HTTP keep-alive $this->browser = $this->browser->withHeader('Connection', 'close'); - $response = \React\Async\await($this->browser->get($this->base . 'get')); + $response = await($this->browser->get($this->base . 'get')); assert($response instanceof ResponseInterface); $this->assertEquals('hello', (string)$response->getBody()); - $response = \React\Async\await($this->browser->get($this->base . 'get')); + $response = await($this->browser->get($this->base . 'get')); assert($response instanceof ResponseInterface); $this->assertEquals('hello', (string)$response->getBody()); } @@ -574,7 +578,7 @@ public function testRequestWithHttp10WillCreateNewConnectionForSecondRequestEven { $twice = $this->expectCallableOnce(); $socket = new SocketServer('127.0.0.1:0'); - $socket->on('connection', function (\React\Socket\ConnectionInterface $connection) use ($socket, $twice) { + $socket->on('connection', function (ConnectionInterface $connection) use ($socket, $twice) { $connection->on('data', function () use ($connection) { $connection->write("HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello"); }); @@ -590,11 +594,11 @@ public function testRequestWithHttp10WillCreateNewConnectionForSecondRequestEven // use HTTP/1.0 to disable HTTP keep-alive $this->browser = $this->browser->withProtocolVersion('1.0'); - $response = \React\Async\await($this->browser->get($this->base . 'get')); + $response = await($this->browser->get($this->base . 'get')); assert($response instanceof ResponseInterface); $this->assertEquals('hello', (string)$response->getBody()); - $response = \React\Async\await($this->browser->get($this->base . 'get')); + $response = await($this->browser->get($this->base . 'get')); assert($response instanceof ResponseInterface); $this->assertEquals('hello', (string)$response->getBody()); } @@ -603,11 +607,11 @@ public function testRequestWillReuseExistingConnectionForSecondRequestByDefault( { $this->socket->on('connection', $this->expectCallableOnce()); - $response = \React\Async\await($this->browser->get($this->base . 'get')); + $response = await($this->browser->get($this->base . 'get')); assert($response instanceof ResponseInterface); $this->assertEquals('hello', (string)$response->getBody()); - $response = \React\Async\await($this->browser->get($this->base . 'get')); + $response = await($this->browser->get($this->base . 'get')); assert($response instanceof ResponseInterface); $this->assertEquals('hello', (string)$response->getBody()); } @@ -619,11 +623,11 @@ public function testRequestWithHttp10AndConnectionKeepAliveHeaderWillReuseExisti $this->browser = $this->browser->withProtocolVersion('1.0'); $this->browser = $this->browser->withHeader('Connection', 'keep-alive'); - $response = \React\Async\await($this->browser->get($this->base . 'get')); + $response = await($this->browser->get($this->base . 'get')); assert($response instanceof ResponseInterface); $this->assertEquals('hello', (string)$response->getBody()); - $response = \React\Async\await($this->browser->get($this->base . 'get')); + $response = await($this->browser->get($this->base . 'get')); assert($response instanceof ResponseInterface); $this->assertEquals('hello', (string)$response->getBody()); } @@ -635,7 +639,7 @@ public function testRequestWithoutConnectionHeaderWillReuseExistingConnectionFor // remove default `Connection: close` request header to enable keep-alive $this->browser = $this->browser->withoutHeader('Connection'); - $response = \React\Async\await($this->browser->get($this->base . 'redirect-to?url=get')); + $response = await($this->browser->get($this->base . 'redirect-to?url=get')); assert($response instanceof ResponseInterface); $this->assertEquals('hello', (string)$response->getBody()); } @@ -648,7 +652,7 @@ public function testPostStreamChunked() $stream->end('hello world'); }); - $response = \React\Async\await($this->browser->post($this->base . 'post', array(), $stream)); + $response = await($this->browser->post($this->base . 'post', [], $stream)); $data = json_decode((string)$response->getBody(), true); $this->assertEquals('hello world', $data['data']); @@ -664,7 +668,7 @@ public function testPostStreamKnownLength() $stream->end('hello world'); }); - $response = \React\Async\await($this->browser->post($this->base . 'post', array('Content-Length' => 11), $stream)); + $response = await($this->browser->post($this->base . 'post', ['Content-Length' => 11], $stream)); $data = json_decode((string)$response->getBody(), true); $this->assertEquals('hello world', $data['data']); @@ -684,7 +688,7 @@ public function testPostStreamWillStartSendingRequestEvenWhenBodyDoesNotEmitData $this->base = str_replace('tcp:', 'http:', $socket->getAddress()) . '/'; $stream = new ThroughStream(); - \React\Async\await($this->browser->post($this->base . 'post', array(), $stream)); + await($this->browser->post($this->base . 'post', [], $stream)); $socket->close(); } @@ -694,7 +698,7 @@ public function testPostStreamClosed() $stream = new ThroughStream(); $stream->close(); - $response = \React\Async\await($this->browser->post($this->base . 'post', array(), $stream)); + $response = await($this->browser->post($this->base . 'post', [], $stream)); $data = json_decode((string)$response->getBody(), true); $this->assertEquals('', $data['data']); @@ -705,7 +709,7 @@ public function testSendsHttp11ByDefault() $http = new HttpServer(function (ServerRequestInterface $request) { return new Response( 200, - array(), + [], $request->getProtocolVersion() ); }); @@ -714,7 +718,7 @@ public function testSendsHttp11ByDefault() $this->base = str_replace('tcp:', 'http:', $socket->getAddress()) . '/'; - $response = \React\Async\await($this->browser->get($this->base)); + $response = await($this->browser->get($this->base)); $this->assertEquals('1.1', (string)$response->getBody()); $socket->close(); @@ -725,7 +729,7 @@ public function testSendsExplicitHttp10Request() $http = new HttpServer(function (ServerRequestInterface $request) { return new Response( 200, - array(), + [], $request->getProtocolVersion() ); }); @@ -734,7 +738,7 @@ public function testSendsExplicitHttp10Request() $this->base = str_replace('tcp:', 'http:', $socket->getAddress()) . '/'; - $response = \React\Async\await($this->browser->withProtocolVersion('1.0')->get($this->base)); + $response = await($this->browser->withProtocolVersion('1.0')->get($this->base)); $this->assertEquals('1.0', (string)$response->getBody()); $socket->close(); @@ -742,7 +746,7 @@ public function testSendsExplicitHttp10Request() public function testHeadRequestReceivesResponseWithEmptyBodyButWithContentLengthResponseHeader() { - $response = \React\Async\await($this->browser->head($this->base . 'get')); + $response = await($this->browser->head($this->base . 'get')); $this->assertEquals('5', $response->getHeaderLine('Content-Length')); $body = $response->getBody(); @@ -752,7 +756,7 @@ public function testHeadRequestReceivesResponseWithEmptyBodyButWithContentLength public function testRequestStreamingGetReceivesResponseWithStreamingBodyAndKnownSize() { - $response = \React\Async\await($this->browser->requestStreaming('GET', $this->base . 'get')); + $response = await($this->browser->requestStreaming('GET', $this->base . 'get')); $this->assertEquals('5', $response->getHeaderLine('Content-Length')); $body = $response->getBody(); @@ -763,7 +767,7 @@ public function testRequestStreamingGetReceivesResponseWithStreamingBodyAndKnown public function testRequestStreamingGetReceivesResponseWithStreamingBodyAndUnknownSizeFromStreamingEndpoint() { - $response = \React\Async\await($this->browser->requestStreaming('GET', $this->base . 'stream/1')); + $response = await($this->browser->requestStreaming('GET', $this->base . 'stream/1')); $this->assertFalse($response->hasHeader('Content-Length')); $body = $response->getBody(); @@ -774,9 +778,9 @@ public function testRequestStreamingGetReceivesResponseWithStreamingBodyAndUnkno public function testRequestStreamingGetReceivesStreamingResponseBody() { - $buffer = \React\Async\await( + $buffer = await( $this->browser->requestStreaming('GET', $this->base . 'get')->then(function (ResponseInterface $response) { - return Stream\buffer($response->getBody()); + return buffer($response->getBody()); }) ); @@ -785,9 +789,9 @@ public function testRequestStreamingGetReceivesStreamingResponseBody() public function testRequestStreamingGetReceivesStreamingResponseBodyEvenWhenResponseBufferExceeded() { - $buffer = \React\Async\await( + $buffer = await( $this->browser->withResponseBuffer(4)->requestStreaming('GET', $this->base . 'get')->then(function (ResponseInterface $response) { - return Stream\buffer($response->getBody()); + return buffer($response->getBody()); }) ); diff --git a/tests/FunctionalHttpServerTest.php b/tests/FunctionalHttpServerTest.php index 6b153b81..c0fcfe83 100644 --- a/tests/FunctionalHttpServerTest.php +++ b/tests/FunctionalHttpServerTest.php @@ -10,12 +10,17 @@ use React\Http\Middleware\LimitConcurrentRequestsMiddleware; use React\Http\Middleware\RequestBodyBufferMiddleware; use React\Http\Middleware\StreamingRequestMiddleware; +use React\Promise\Promise; use React\Socket\ConnectionInterface; use React\Socket\Connector; use React\Socket\SocketServer; -use React\Promise; -use React\Promise\Stream; use React\Stream\ThroughStream; +use function React\Async\await; +use function React\Promise\all; +use function React\Promise\Stream\buffer; +use function React\Promise\Stream\first; +use function React\Promise\Timer\sleep; +use function React\Promise\Timer\timeout; class FunctionalHttpServerTest extends TestCase { @@ -24,7 +29,7 @@ public function testPlainHttpOnRandomPort() $connector = new Connector(); $http = new HttpServer(function (RequestInterface $request) { - return new Response(200, array(), (string)$request->getUri()); + return new Response(200, [], (string)$request->getUri()); }); $socket = new SocketServer('127.0.0.1:0'); @@ -33,10 +38,10 @@ public function testPlainHttpOnRandomPort() $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\nHost: " . noScheme($conn->getRemoteAddress()) . "\r\n\r\n"); - return Stream\buffer($conn); + return buffer($conn); }); - $response = \React\Async\await(\React\Promise\Timer\timeout($result, 1.0)); + $response = await(timeout($result, 1.0)); $this->assertContainsString("HTTP/1.0 200 OK", $response); $this->assertContainsString('http://' . noScheme($socket->getAddress()) . '/', $response); @@ -60,10 +65,10 @@ function () { $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\nHost: " . noScheme($conn->getRemoteAddress()) . "\r\n\r\n"); - return Stream\buffer($conn); + return buffer($conn); }); - $response = \React\Async\await(\React\Promise\Timer\timeout($result, 1.0)); + $response = await(timeout($result, 1.0)); $this->assertContainsString("HTTP/1.0 404 Not Found", $response); @@ -75,7 +80,7 @@ public function testPlainHttpOnRandomPortWithoutHostHeaderUsesSocketUri() $connector = new Connector(); $http = new HttpServer(function (RequestInterface $request) { - return new Response(200, array(), (string)$request->getUri()); + return new Response(200, [], (string)$request->getUri()); }); $socket = new SocketServer('127.0.0.1:0'); @@ -84,10 +89,10 @@ public function testPlainHttpOnRandomPortWithoutHostHeaderUsesSocketUri() $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\n\r\n"); - return Stream\buffer($conn); + return buffer($conn); }); - $response = \React\Async\await(\React\Promise\Timer\timeout($result, 1.0)); + $response = await(timeout($result, 1.0)); $this->assertContainsString("HTTP/1.0 200 OK", $response); $this->assertContainsString('http://' . noScheme($socket->getAddress()) . '/', $response); @@ -100,7 +105,7 @@ public function testPlainHttpOnRandomPortWithOtherHostHeaderTakesPrecedence() $connector = new Connector(); $http = new HttpServer(function (RequestInterface $request) { - return new Response(200, array(), (string)$request->getUri()); + return new Response(200, [], (string)$request->getUri()); }); $socket = new SocketServer('127.0.0.1:0'); @@ -109,10 +114,10 @@ public function testPlainHttpOnRandomPortWithOtherHostHeaderTakesPrecedence() $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\nHost: localhost:1000\r\n\r\n"); - return Stream\buffer($conn); + return buffer($conn); }); - $response = \React\Async\await(\React\Promise\Timer\timeout($result, 1.0)); + $response = await(timeout($result, 1.0)); $this->assertContainsString("HTTP/1.0 200 OK", $response); $this->assertContainsString('http://localhost:1000/', $response); @@ -122,26 +127,28 @@ public function testPlainHttpOnRandomPortWithOtherHostHeaderTakesPrecedence() public function testSecureHttpsOnRandomPort() { - $connector = new Connector(array( - 'tls' => array('verify_peer' => false) - )); + $connector = new Connector([ + 'tls' => [ + 'verify_peer' => false + ] + ]); $http = new HttpServer(function (RequestInterface $request) { - return new Response(200, array(), (string)$request->getUri()); + return new Response(200, [], (string)$request->getUri()); }); - $socket = new SocketServer('tls://127.0.0.1:0', array('tls' => array( + $socket = new SocketServer('tls://127.0.0.1:0', ['tls' => [ 'local_cert' => __DIR__ . '/../examples/localhost.pem' - ))); + ]]); $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\nHost: " . noScheme($conn->getRemoteAddress()) . "\r\n\r\n"); - return Stream\buffer($conn); + return buffer($conn); }); - $response = \React\Async\await(\React\Promise\Timer\timeout($result, 1.0)); + $response = await(timeout($result, 1.0)); $this->assertContainsString("HTTP/1.0 200 OK", $response); $this->assertContainsString('https://' . noScheme($socket->getAddress()) . '/', $response); @@ -154,27 +161,25 @@ public function testSecureHttpsReturnsData() $http = new HttpServer(function (RequestInterface $request) { return new Response( 200, - array(), + [], str_repeat('.', 33000) ); }); - $socket = new SocketServer('tls://127.0.0.1:0', array('tls' => array( - 'local_cert' => __DIR__ . '/../examples/localhost.pem' - ))); + $socket = new SocketServer('tls://127.0.0.1:0', ['tls' => ['local_cert' => __DIR__ . '/../examples/localhost.pem']]); $http->listen($socket); - $connector = new Connector(array( - 'tls' => array('verify_peer' => false) - )); + $connector = new Connector(['tls' => [ + 'verify_peer' => false + ]]); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\nHost: " . noScheme($conn->getRemoteAddress()) . "\r\n\r\n"); - return Stream\buffer($conn); + return buffer($conn); }); - $response = \React\Async\await(\React\Promise\Timer\timeout($result, 1.0)); + $response = await(timeout($result, 1.0)); $this->assertContainsString("HTTP/1.0 200 OK", $response); $this->assertContainsString("\r\nContent-Length: 33000\r\n", $response); @@ -185,26 +190,26 @@ public function testSecureHttpsReturnsData() public function testSecureHttpsOnRandomPortWithoutHostHeaderUsesSocketUri() { - $connector = new Connector(array( - 'tls' => array('verify_peer' => false) - )); + $connector = new Connector([ + 'tls' => ['verify_peer' => false] + ]); $http = new HttpServer(function (RequestInterface $request) { - return new Response(200, array(), (string)$request->getUri()); + return new Response(200, [], (string)$request->getUri()); }); - $socket = new SocketServer('tls://127.0.0.1:0', array('tls' => array( + $socket = new SocketServer('tls://127.0.0.1:0', ['tls' => [ 'local_cert' => __DIR__ . '/../examples/localhost.pem' - ))); + ]]); $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\n\r\n"); - return Stream\buffer($conn); + return buffer($conn); }); - $response = \React\Async\await(\React\Promise\Timer\timeout($result, 1.0)); + $response = await(timeout($result, 1.0)); $this->assertContainsString("HTTP/1.0 200 OK", $response); $this->assertContainsString('https://' . noScheme($socket->getAddress()) . '/', $response); @@ -222,7 +227,7 @@ public function testPlainHttpOnStandardPortReturnsUriWithNoPort() $connector = new Connector(); $http = new HttpServer(function (RequestInterface $request) { - return new Response(200, array(), (string)$request->getUri()); + return new Response(200, [], (string)$request->getUri()); }); $http->listen($socket); @@ -230,10 +235,10 @@ public function testPlainHttpOnStandardPortReturnsUriWithNoPort() $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n"); - return Stream\buffer($conn); + return buffer($conn); }); - $response = \React\Async\await(\React\Promise\Timer\timeout($result, 1.0)); + $response = await(timeout($result, 1.0)); $this->assertContainsString("HTTP/1.0 200 OK", $response); $this->assertContainsString('http://127.0.0.1/', $response); @@ -251,7 +256,7 @@ public function testPlainHttpOnStandardPortWithoutHostHeaderReturnsUriWithNoPort $connector = new Connector(); $http = new HttpServer(function (RequestInterface $request) { - return new Response(200, array(), (string)$request->getUri()); + return new Response(200, [], (string)$request->getUri()); }); $http->listen($socket); @@ -259,10 +264,10 @@ public function testPlainHttpOnStandardPortWithoutHostHeaderReturnsUriWithNoPort $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\n\r\n"); - return Stream\buffer($conn); + return buffer($conn); }); - $response = \React\Async\await(\React\Promise\Timer\timeout($result, 1.0)); + $response = await(timeout($result, 1.0)); $this->assertContainsString("HTTP/1.0 200 OK", $response); $this->assertContainsString('http://127.0.0.1/', $response); @@ -273,19 +278,19 @@ public function testPlainHttpOnStandardPortWithoutHostHeaderReturnsUriWithNoPort public function testSecureHttpsOnStandardPortReturnsUriWithNoPort() { try { - $socket = new SocketServer('tls://127.0.0.1:443', array('tls' => array( + $socket = new SocketServer('tls://127.0.0.1:443', ['tls' => [ 'local_cert' => __DIR__ . '/../examples/localhost.pem' - ))); + ]]); } catch (\RuntimeException $e) { $this->markTestSkipped('Listening on port 443 failed (root and unused?)'); } - $connector = new Connector(array( - 'tls' => array('verify_peer' => false) - )); + $connector = new Connector([ + 'tls' => ['verify_peer' => false] + ]); $http = new HttpServer(function (RequestInterface $request) { - return new Response(200, array(), (string)$request->getUri()); + return new Response(200, [], (string)$request->getUri()); }); $http->listen($socket); @@ -293,10 +298,10 @@ public function testSecureHttpsOnStandardPortReturnsUriWithNoPort() $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n"); - return Stream\buffer($conn); + return buffer($conn); }); - $response = \React\Async\await(\React\Promise\Timer\timeout($result, 1.0)); + $response = await(timeout($result, 1.0)); $this->assertContainsString("HTTP/1.0 200 OK", $response); $this->assertContainsString('https://127.0.0.1/', $response); @@ -307,19 +312,19 @@ public function testSecureHttpsOnStandardPortReturnsUriWithNoPort() public function testSecureHttpsOnStandardPortWithoutHostHeaderUsesSocketUri() { try { - $socket = new SocketServer('tls://127.0.0.1:443', array('tls' => array( + $socket = new SocketServer('tls://127.0.0.1:443', ['tls' => [ 'local_cert' => __DIR__ . '/../examples/localhost.pem' - ))); + ]]); } catch (\RuntimeException $e) { $this->markTestSkipped('Listening on port 443 failed (root and unused?)'); } - $connector = new Connector(array( - 'tls' => array('verify_peer' => false) - )); + $connector = new Connector([ + 'tls' => ['verify_peer' => false] + ]); $http = new HttpServer(function (RequestInterface $request) { - return new Response(200, array(), (string)$request->getUri()); + return new Response(200, [], (string)$request->getUri()); }); $http->listen($socket); @@ -327,10 +332,10 @@ public function testSecureHttpsOnStandardPortWithoutHostHeaderUsesSocketUri() $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\n\r\n"); - return Stream\buffer($conn); + return buffer($conn); }); - $response = \React\Async\await(\React\Promise\Timer\timeout($result, 1.0)); + $response = await(timeout($result, 1.0)); $this->assertContainsString("HTTP/1.0 200 OK", $response); $this->assertContainsString('https://127.0.0.1/', $response); @@ -348,7 +353,7 @@ public function testPlainHttpOnHttpsStandardPortReturnsUriWithPort() $connector = new Connector(); $http = new HttpServer(function (RequestInterface $request) { - return new Response(200, array(), (string)$request->getUri()); + return new Response(200, [], (string)$request->getUri()); }); $http->listen($socket); @@ -356,10 +361,10 @@ public function testPlainHttpOnHttpsStandardPortReturnsUriWithPort() $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\nHost: " . noScheme($conn->getRemoteAddress()) . "\r\n\r\n"); - return Stream\buffer($conn); + return buffer($conn); }); - $response = \React\Async\await(\React\Promise\Timer\timeout($result, 1.0)); + $response = await(timeout($result, 1.0)); $this->assertContainsString("HTTP/1.0 200 OK", $response); $this->assertContainsString('http://127.0.0.1:443/', $response); @@ -370,19 +375,19 @@ public function testPlainHttpOnHttpsStandardPortReturnsUriWithPort() public function testSecureHttpsOnHttpStandardPortReturnsUriWithPort() { try { - $socket = new SocketServer('tls://127.0.0.1:80', array('tls' => array( + $socket = new SocketServer('tls://127.0.0.1:80', ['tls' => [ 'local_cert' => __DIR__ . '/../examples/localhost.pem' - ))); + ]]); } catch (\RuntimeException $e) { $this->markTestSkipped('Listening on port 80 failed (root and unused?)'); } - $connector = new Connector(array( - 'tls' => array('verify_peer' => false) - )); + $connector = new Connector([ + 'tls' => ['verify_peer' => false] + ]); $http = new HttpServer(function (RequestInterface $request) { - return new Response(200, array(), (string)$request->getUri() . 'x' . $request->getHeaderLine('Host')); + return new Response(200, [], (string)$request->getUri() . 'x' . $request->getHeaderLine('Host')); }); $http->listen($socket); @@ -390,10 +395,10 @@ public function testSecureHttpsOnHttpStandardPortReturnsUriWithPort() $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\nHost: " . noScheme($conn->getRemoteAddress()) . "\r\n\r\n"); - return Stream\buffer($conn); + return buffer($conn); }); - $response = \React\Async\await(\React\Promise\Timer\timeout($result, 1.0)); + $response = await(timeout($result, 1.0)); $this->assertContainsString("HTTP/1.0 200 OK", $response); $this->assertContainsString('https://127.0.0.1:80/', $response); @@ -409,7 +414,7 @@ public function testClosedStreamFromRequestHandlerWillSendEmptyBody() $stream->close(); $http = new HttpServer(function (RequestInterface $request) use ($stream) { - return new Response(200, array(), $stream); + return new Response(200, [], $stream); }); $socket = new SocketServer('127.0.0.1:0'); @@ -418,10 +423,10 @@ public function testClosedStreamFromRequestHandlerWillSendEmptyBody() $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\n\r\n"); - return Stream\buffer($conn); + return buffer($conn); }); - $response = \React\Async\await(\React\Promise\Timer\timeout($result, 1.0)); + $response = await(timeout($result, 1.0)); $this->assertStringStartsWith("HTTP/1.0 200 OK", $response); $this->assertStringEndsWith("\r\n\r\n", $response); @@ -452,7 +457,7 @@ function (RequestInterface $request) use ($once) { }); }); - \React\Async\await(\React\Promise\Timer\sleep(0.1)); + await(sleep(0.1)); $socket->close(); } @@ -466,7 +471,7 @@ public function testStreamFromRequestHandlerWillBeClosedIfConnectionClosesWhileS $http = new HttpServer( new StreamingRequestMiddleware(), function (RequestInterface $request) use ($stream) { - return new Response(200, array(), $stream); + return new Response(200, [], $stream); } ); @@ -482,7 +487,7 @@ function (RequestInterface $request) use ($stream) { }); // stream will be closed within 0.1s - $ret = \React\Async\await(\React\Promise\Timer\timeout(Stream\first($stream, 'close'), 0.1)); + $ret = await(timeout(first($stream, 'close'), 0.1)); $socket->close(); @@ -496,7 +501,7 @@ public function testStreamFromRequestHandlerWillBeClosedIfConnectionCloses() $stream = new ThroughStream(); $http = new HttpServer(function (RequestInterface $request) use ($stream) { - return new Response(200, array(), $stream); + return new Response(200, [], $stream); }); $socket = new SocketServer('127.0.0.1:0'); @@ -511,7 +516,7 @@ public function testStreamFromRequestHandlerWillBeClosedIfConnectionCloses() }); // await response stream to be closed - $ret = \React\Async\await(\React\Promise\Timer\timeout(Stream\first($stream, 'close'), 1.0)); + $ret = await(timeout(first($stream, 'close'), 1.0)); $socket->close(); @@ -529,7 +534,7 @@ public function testUpgradeWithThroughStreamReturnsDataAsGiven() $stream->end(); }); - return new Response(101, array('Upgrade' => 'echo'), $stream); + return new Response(101, ['Upgrade' => 'echo'], $stream); }); $socket = new SocketServer('127.0.0.1:0'); @@ -543,10 +548,10 @@ public function testUpgradeWithThroughStreamReturnsDataAsGiven() $conn->write('world'); }); - return Stream\buffer($conn); + return buffer($conn); }); - $response = \React\Async\await(\React\Promise\Timer\timeout($result, 1.0)); + $response = await(timeout($result, 1.0)); $this->assertStringStartsWith("HTTP/1.1 101 Switching Protocols\r\n", $response); $this->assertStringEndsWith("\r\n\r\nhelloworld", $response); @@ -565,7 +570,7 @@ public function testUpgradeWithRequestBodyAndThroughStreamReturnsDataAsGiven() $stream->end(); }); - return new Response(101, array('Upgrade' => 'echo'), $stream); + return new Response(101, ['Upgrade' => 'echo'], $stream); }); $socket = new SocketServer('127.0.0.1:0'); @@ -580,10 +585,10 @@ public function testUpgradeWithRequestBodyAndThroughStreamReturnsDataAsGiven() $conn->write('world'); }); - return Stream\buffer($conn); + return buffer($conn); }); - $response = \React\Async\await(\React\Promise\Timer\timeout($result, 1.0)); + $response = await(timeout($result, 1.0)); $this->assertStringStartsWith("HTTP/1.1 101 Switching Protocols\r\n", $response); $this->assertStringEndsWith("\r\n\r\nhelloworld", $response); @@ -602,7 +607,7 @@ public function testConnectWithThroughStreamReturnsDataAsGiven() $stream->end(); }); - return new Response(200, array(), $stream); + return new Response(200, [], $stream); }); $socket = new SocketServer('127.0.0.1:0'); @@ -616,10 +621,10 @@ public function testConnectWithThroughStreamReturnsDataAsGiven() $conn->write('world'); }); - return Stream\buffer($conn); + return buffer($conn); }); - $response = \React\Async\await(\React\Promise\Timer\timeout($result, 1.0)); + $response = await(timeout($result, 1.0)); $this->assertStringStartsWith("HTTP/1.1 200 OK\r\n", $response); $this->assertStringEndsWith("\r\n\r\nhelloworld", $response); @@ -638,9 +643,9 @@ public function testConnectWithThroughStreamReturnedFromPromiseReturnsDataAsGive $stream->end(); }); - return new Promise\Promise(function ($resolve) use ($stream) { + return new Promise(function ($resolve) use ($stream) { Loop::addTimer(0.001, function () use ($resolve, $stream) { - $resolve(new Response(200, array(), $stream)); + $resolve(new Response(200, [], $stream)); }); }); }); @@ -656,10 +661,10 @@ public function testConnectWithThroughStreamReturnedFromPromiseReturnsDataAsGive $conn->write('world'); }); - return Stream\buffer($conn); + return buffer($conn); }); - $response = \React\Async\await(\React\Promise\Timer\timeout($result, 1.0)); + $response = await(timeout($result, 1.0)); $this->assertStringStartsWith("HTTP/1.1 200 OK\r\n", $response); $this->assertStringEndsWith("\r\n\r\nhelloworld", $response); @@ -675,7 +680,7 @@ public function testConnectWithClosedThroughStreamReturnsNoData() $stream = new ThroughStream(); $stream->close(); - return new Response(200, array(), $stream); + return new Response(200, [], $stream); }); $socket = new SocketServer('127.0.0.1:0'); @@ -689,10 +694,10 @@ public function testConnectWithClosedThroughStreamReturnsNoData() $conn->write('world'); }); - return Stream\buffer($conn); + return buffer($conn); }); - $response = \React\Async\await(\React\Promise\Timer\timeout($result, 1.0)); + $response = await(timeout($result, 1.0)); $this->assertStringStartsWith("HTTP/1.1 200 OK\r\n", $response); $this->assertStringEndsWith("\r\n\r\n", $response); @@ -708,21 +713,21 @@ public function testLimitConcurrentRequestsMiddlewareRequestStreamPausing() new LimitConcurrentRequestsMiddleware(5), new RequestBodyBufferMiddleware(16 * 1024 * 1024), // 16 MiB function (ServerRequestInterface $request, $next) { - return new Promise\Promise(function ($resolve) use ($request, $next) { + return new Promise(function ($resolve) use ($request, $next) { Loop::addTimer(0.1, function () use ($request, $resolve, $next) { $resolve($next($request)); }); }); }, function (ServerRequestInterface $request) { - return new Response(200, array(), (string)strlen((string)$request->getBody())); + return new Response(200, [], (string)strlen((string)$request->getBody())); } ); $socket = new SocketServer('127.0.0.1:0'); $http->listen($socket); - $result = array(); + $result = []; for ($i = 0; $i < 6; $i++) { $result[] = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write( @@ -731,11 +736,11 @@ function (ServerRequestInterface $request) { "\r\n\r\n" ); - return Stream\buffer($conn); + return buffer($conn); }); } - $responses = \React\Async\await(\React\Promise\Timer\timeout(Promise\all($result), 1.0)); + $responses = await(timeout(all($result), 1.0)); foreach ($responses as $response) { $this->assertContainsString("HTTP/1.0 200 OK", $response, $response); diff --git a/tests/HttpServerTest.php b/tests/HttpServerTest.php index 606c50a6..fc977d9b 100644 --- a/tests/HttpServerTest.php +++ b/tests/HttpServerTest.php @@ -7,9 +7,10 @@ use React\Http\HttpServer; use React\Http\Io\IniUtil; use React\Http\Middleware\StreamingRequestMiddleware; -use React\Promise; use React\Promise\Deferred; use React\Stream\ReadableStreamInterface; +use function React\Async\await; +use function React\Promise\reject; final class HttpServerTest extends TestCase { @@ -27,7 +28,7 @@ public function setUpConnectionMockAndSocket() $this->connection = $this->getMockBuilder('React\Socket\Connection') ->disableOriginalConstructor() ->setMethods( - array( + [ 'write', 'end', 'close', @@ -38,7 +39,7 @@ public function setUpConnectionMockAndSocket() 'getRemoteAddress', 'getLocalAddress', 'pipe' - ) + ] ) ->getMock(); @@ -81,8 +82,8 @@ public function testSimpleRequestCallsRequestHandlerOnce() }); $http->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); - $this->connection->emit('data', array("GET / HTTP/1.0\r\n\r\n")); + $this->socket->emit('connection', [$this->connection]); + $this->connection->emit('data', ["GET / HTTP/1.0\r\n\r\n"]); $this->assertSame(1, $called); } @@ -90,11 +91,11 @@ public function testSimpleRequestCallsRequestHandlerOnce() public function testSimpleRequestCallsArrayRequestHandlerOnce() { $this->called = null; - $http = new HttpServer(array($this, 'helperCallableOnce')); + $http = new HttpServer([$this, 'helperCallableOnce']); $http->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); - $this->connection->emit('data', array("GET / HTTP/1.0\r\n\r\n")); + $this->socket->emit('connection', [$this->connection]); + $this->connection->emit('data', ["GET / HTTP/1.0\r\n\r\n"]); $this->assertSame(1, $this->called); } @@ -121,8 +122,8 @@ function (ServerRequestInterface $request) use (&$called) { ); $http->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); - $this->connection->emit('data', array("GET / HTTP/1.0\r\n\r\n")); + $this->socket->emit('connection', [$this->connection]); + $this->connection->emit('data', ["GET / HTTP/1.0\r\n\r\n"]); $this->assertSame('beforeokafter', $called); } @@ -135,10 +136,10 @@ public function testPostFormData() }); $http->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); - $this->connection->emit('data', array("POST / HTTP/1.0\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: 7\r\n\r\nfoo=bar")); + $this->socket->emit('connection', [$this->connection]); + $this->connection->emit('data', ["POST / HTTP/1.0\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: 7\r\n\r\nfoo=bar"]); - $request = \React\Async\await($deferred->promise()); + $request = await($deferred->promise()); assert($request instanceof ServerRequestInterface); $form = $request->getParsedBody(); @@ -146,7 +147,7 @@ public function testPostFormData() $this->assertTrue(isset($form['foo'])); $this->assertEquals('bar', $form['foo']); - $this->assertEquals(array(), $request->getUploadedFiles()); + $this->assertEquals([], $request->getUploadedFiles()); $body = $request->getBody(); @@ -163,20 +164,19 @@ public function testPostFileUpload() }); $http->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); - $connection = $this->connection; $data = $this->createPostFileUploadRequest(); - Loop::addPeriodicTimer(0.01, function ($timer) use (&$data, $connection) { + Loop::addPeriodicTimer(0.01, function ($timer) use (&$data) { $line = array_shift($data); - $connection->emit('data', array($line)); + $this->connection->emit('data', [$line]); if (count($data) === 0) { Loop::cancelTimer($timer); } }); - $request = \React\Async\await($deferred->promise()); + $request = await($deferred->promise()); assert($request instanceof ServerRequestInterface); $this->assertEmpty($request->getParsedBody()); @@ -206,15 +206,15 @@ public function testPostJsonWillNotBeParsedByDefault() }); $http->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); - $this->connection->emit('data', array("POST / HTTP/1.0\r\nContent-Type: application/json\r\nContent-Length: 6\r\n\r\n[true]")); + $this->socket->emit('connection', [$this->connection]); + $this->connection->emit('data', ["POST / HTTP/1.0\r\nContent-Type: application/json\r\nContent-Length: 6\r\n\r\n[true]"]); - $request = \React\Async\await($deferred->promise()); + $request = await($deferred->promise()); assert($request instanceof ServerRequestInterface); $this->assertNull($request->getParsedBody()); - $this->assertSame(array(), $request->getUploadedFiles()); + $this->assertSame([], $request->getUploadedFiles()); $body = $request->getBody(); @@ -231,8 +231,8 @@ public function testServerReceivesBufferedRequestByDefault() }); $http->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); - $this->connection->emit('data', array("GET / HTTP/1.0\r\n\r\n")); + $this->socket->emit('connection', [$this->connection]); + $this->connection->emit('data', ["GET / HTTP/1.0\r\n\r\n"]); $this->assertEquals(false, $streaming); } @@ -248,8 +248,8 @@ function (ServerRequestInterface $request) use (&$streaming) { ); $http->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); - $this->connection->emit('data', array("GET / HTTP/1.0\r\n\r\n")); + $this->socket->emit('connection', [$this->connection]); + $this->connection->emit('data', ["GET / HTTP/1.0\r\n\r\n"]); $this->assertEquals(true, $streaming); } @@ -259,17 +259,17 @@ public function testForwardErrors() $exception = new \Exception(); $capturedException = null; $http = new HttpServer(function () use ($exception) { - return Promise\reject($exception); + return reject($exception); }); $http->on('error', function ($error) use (&$capturedException) { $capturedException = $error; }); $http->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = $this->createPostFileUploadRequest(); - $this->connection->emit('data', array(implode('', $data))); + $this->connection->emit('data', [implode('', $data)]); $this->assertInstanceOf('RuntimeException', $capturedException); $this->assertInstanceOf('Exception', $capturedException->getPrevious()); @@ -280,7 +280,7 @@ private function createPostFileUploadRequest() { $boundary = "---------------------------5844729766471062541057622570"; - $data = array(); + $data = []; $data[] = "POST / HTTP/1.1\r\n"; $data[] = "Host: localhost\r\n"; $data[] = "Content-Type: multipart/form-data; boundary=" . $boundary . "\r\n"; @@ -299,23 +299,23 @@ private function createPostFileUploadRequest() public function provideIniSettingsForConcurrency() { - return array( - 'default settings' => array( + return [ + 'default settings' => [ '128M', '64K', // 8M capped at maximum 1024 - ), - 'unlimited memory_limit has no concurrency limit' => array( + ], + 'unlimited memory_limit has no concurrency limit' => [ '-1', '8M', null - ), - 'small post_max_size results in high concurrency' => array( + ], + 'small post_max_size results in high concurrency' => [ '128M', '1k', 65536 - ) - ); + ] + ]; } /** diff --git a/tests/Io/AbstractMessageTest.php b/tests/Io/AbstractMessageTest.php index 9e2c7d32..59c170ec 100644 --- a/tests/Io/AbstractMessageTest.php +++ b/tests/Io/AbstractMessageTest.php @@ -25,7 +25,7 @@ public function testWithProtocolVersionReturnsNewInstanceWhenProtocolVersionIsCh { $message = new MessageMock( '1.1', - array(), + [], $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock() ); @@ -39,7 +39,7 @@ public function testWithProtocolVersionReturnsSameInstanceWhenProtocolVersionIsU { $message = new MessageMock( '1.1', - array(), + [], $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock() ); @@ -52,16 +52,16 @@ public function testHeaderWithStringValue() { $message = new MessageMock( '1.1', - array( + [ 'Content-Type' => 'text/plain' - ), + ], $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock() ); - $this->assertEquals(array('Content-Type' => array('text/plain')), $message->getHeaders()); + $this->assertEquals(['Content-Type' => ['text/plain']], $message->getHeaders()); - $this->assertEquals(array('text/plain'), $message->getHeader('Content-Type')); - $this->assertEquals(array('text/plain'), $message->getHeader('CONTENT-type')); + $this->assertEquals(['text/plain'], $message->getHeader('Content-Type')); + $this->assertEquals(['text/plain'], $message->getHeader('CONTENT-type')); $this->assertEquals('text/plain', $message->getHeaderLine('Content-Type')); $this->assertEquals('text/plain', $message->getHeaderLine('CONTENT-Type')); @@ -72,50 +72,50 @@ public function testHeaderWithStringValue() $new = $message->withHeader('Content-Type', 'text/plain'); $this->assertSame($message, $new); - $new = $message->withHeader('Content-Type', array('text/plain')); + $new = $message->withHeader('Content-Type', ['text/plain']); $this->assertSame($message, $new); $new = $message->withHeader('content-type', 'text/plain'); $this->assertNotSame($message, $new); - $this->assertEquals(array('content-type' => array('text/plain')), $new->getHeaders()); - $this->assertEquals(array('Content-Type' => array('text/plain')), $message->getHeaders()); + $this->assertEquals(['content-type' => ['text/plain']], $new->getHeaders()); + $this->assertEquals(['Content-Type' => ['text/plain']], $message->getHeaders()); $new = $message->withHeader('Content-Type', 'text/html'); $this->assertNotSame($message, $new); - $this->assertEquals(array('Content-Type' => array('text/html')), $new->getHeaders()); - $this->assertEquals(array('Content-Type' => array('text/plain')), $message->getHeaders()); + $this->assertEquals(['Content-Type' => ['text/html']], $new->getHeaders()); + $this->assertEquals(['Content-Type' => ['text/plain']], $message->getHeaders()); - $new = $message->withHeader('Content-Type', array('text/html')); + $new = $message->withHeader('Content-Type', ['text/html']); $this->assertNotSame($message, $new); - $this->assertEquals(array('Content-Type' => array('text/html')), $new->getHeaders()); - $this->assertEquals(array('Content-Type' => array('text/plain')), $message->getHeaders()); + $this->assertEquals(['Content-Type' => ['text/html']], $new->getHeaders()); + $this->assertEquals(['Content-Type' => ['text/plain']], $message->getHeaders()); - $new = $message->withAddedHeader('Content-Type', array()); + $new = $message->withAddedHeader('Content-Type', []); $this->assertSame($message, $new); $new = $message->withoutHeader('Content-Type'); $this->assertNotSame($message, $new); - $this->assertEquals(array(), $new->getHeaders()); - $this->assertEquals(array('Content-Type' => array('text/plain')), $message->getHeaders()); + $this->assertEquals([], $new->getHeaders()); + $this->assertEquals(['Content-Type' => ['text/plain']], $message->getHeaders()); } public function testHeaderWithMultipleValues() { $message = new MessageMock( '1.1', - array( - 'Set-Cookie' => array( + [ + 'Set-Cookie' => [ 'a=1', 'b=2' - ) - ), + ] + ], $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock() ); - $this->assertEquals(array('Set-Cookie' => array('a=1', 'b=2')), $message->getHeaders()); + $this->assertEquals(['Set-Cookie' => ['a=1', 'b=2']], $message->getHeaders()); - $this->assertEquals(array('a=1', 'b=2'), $message->getHeader('Set-Cookie')); - $this->assertEquals(array('a=1', 'b=2'), $message->getHeader('Set-Cookie')); + $this->assertEquals(['a=1', 'b=2'], $message->getHeader('Set-Cookie')); + $this->assertEquals(['a=1', 'b=2'], $message->getHeader('Set-Cookie')); $this->assertEquals('a=1, b=2', $message->getHeaderLine('Set-Cookie')); $this->assertEquals('a=1, b=2', $message->getHeaderLine('Set-Cookie')); @@ -123,49 +123,49 @@ public function testHeaderWithMultipleValues() $this->assertTrue($message->hasHeader('Set-Cookie')); $this->assertTrue($message->hasHeader('Set-Cookie')); - $new = $message->withHeader('Set-Cookie', array('a=1', 'b=2')); + $new = $message->withHeader('Set-Cookie', ['a=1', 'b=2']); $this->assertSame($message, $new); - $new = $message->withHeader('Set-Cookie', array('a=1', 'b=2', 'c=3')); + $new = $message->withHeader('Set-Cookie', ['a=1', 'b=2', 'c=3']); $this->assertNotSame($message, $new); - $this->assertEquals(array('Set-Cookie' => array('a=1', 'b=2', 'c=3')), $new->getHeaders()); - $this->assertEquals(array('Set-Cookie' => array('a=1', 'b=2')), $message->getHeaders()); + $this->assertEquals(['Set-Cookie' => ['a=1', 'b=2', 'c=3']], $new->getHeaders()); + $this->assertEquals(['Set-Cookie' => ['a=1', 'b=2']], $message->getHeaders()); - $new = $message->withAddedHeader('Set-Cookie', array()); + $new = $message->withAddedHeader('Set-Cookie', []); $this->assertSame($message, $new); $new = $message->withAddedHeader('Set-Cookie', 'c=3'); $this->assertNotSame($message, $new); - $this->assertEquals(array('Set-Cookie' => array('a=1', 'b=2', 'c=3')), $new->getHeaders()); - $this->assertEquals(array('Set-Cookie' => array('a=1', 'b=2')), $message->getHeaders()); + $this->assertEquals(['Set-Cookie' => ['a=1', 'b=2', 'c=3']], $new->getHeaders()); + $this->assertEquals(['Set-Cookie' => ['a=1', 'b=2']], $message->getHeaders()); - $new = $message->withAddedHeader('Set-Cookie', array('c=3')); + $new = $message->withAddedHeader('Set-Cookie', ['c=3']); $this->assertNotSame($message, $new); - $this->assertEquals(array('Set-Cookie' => array('a=1', 'b=2', 'c=3')), $new->getHeaders()); - $this->assertEquals(array('Set-Cookie' => array('a=1', 'b=2')), $message->getHeaders()); + $this->assertEquals(['Set-Cookie' => ['a=1', 'b=2', 'c=3']], $new->getHeaders()); + $this->assertEquals(['Set-Cookie' => ['a=1', 'b=2']], $message->getHeaders()); } public function testHeaderWithEmptyValue() { $message = new MessageMock( '1.1', - array( - 'Content-Type' => array() - ), + [ + 'Content-Type' => [] + ], $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock() ); - $this->assertEquals(array(), $message->getHeaders()); + $this->assertEquals([], $message->getHeaders()); - $this->assertEquals(array(), $message->getHeader('Content-Type')); + $this->assertEquals([], $message->getHeader('Content-Type')); $this->assertEquals('', $message->getHeaderLine('Content-Type')); $this->assertFalse($message->hasHeader('Content-Type')); - $new = $message->withHeader('Empty', array()); + $new = $message->withHeader('Empty', []); $this->assertSame($message, $new); $this->assertFalse($new->hasHeader('Empty')); - $new = $message->withAddedHeader('Empty', array()); + $new = $message->withAddedHeader('Empty', []); $this->assertSame($message, $new); $this->assertFalse($new->hasHeader('Empty')); @@ -178,16 +178,16 @@ public function testHeaderWithMultipleValuesAcrossMixedCaseNamesInConstructorMer { $message = new MessageMock( '1.1', - array( + [ 'SET-Cookie' => 'a=1', - 'set-cookie' => array('b=2'), - 'set-COOKIE' => array() - ), + 'set-cookie' => ['b=2'], + 'set-COOKIE' => [] + ], $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock() ); - $this->assertEquals(array('set-cookie' => array('a=1', 'b=2')), $message->getHeaders()); - $this->assertEquals(array('a=1', 'b=2'), $message->getHeader('Set-Cookie')); + $this->assertEquals(['set-cookie' => ['a=1', 'b=2']], $message->getHeaders()); + $this->assertEquals(['a=1', 'b=2'], $message->getHeader('Set-Cookie')); } public function testWithBodyReturnsNewInstanceWhenBodyIsChanged() @@ -195,7 +195,7 @@ public function testWithBodyReturnsNewInstanceWhenBodyIsChanged() $body = $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(); $message = new MessageMock( '1.1', - array(), + [], $body ); @@ -211,7 +211,7 @@ public function testWithBodyReturnsSameInstanceWhenBodyIsUnchanged() $body = $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(); $message = new MessageMock( '1.1', - array(), + [], $body ); diff --git a/tests/Io/AbstractRequestTest.php b/tests/Io/AbstractRequestTest.php index 7ff4a9a5..24990622 100644 --- a/tests/Io/AbstractRequestTest.php +++ b/tests/Io/AbstractRequestTest.php @@ -36,7 +36,7 @@ public function testCtorWithInvalidUriThrows() new RequestMock( 'GET', null, - array(), + [], $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), '1.1' ); @@ -47,12 +47,12 @@ public function testGetHeadersReturnsHostHeaderFromUri() $request = new RequestMock( 'GET', 'http://example.com/', - array(), + [], $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), '1.1' ); - $this->assertEquals(array('Host' => array('example.com')), $request->getHeaders()); + $this->assertEquals(['Host' => ['example.com']], $request->getHeaders()); } public function testGetHeadersReturnsHostHeaderFromUriWithCustomHttpPort() @@ -60,12 +60,12 @@ public function testGetHeadersReturnsHostHeaderFromUriWithCustomHttpPort() $request = new RequestMock( 'GET', 'http://example.com:8080/', - array(), + [], $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), '1.1' ); - $this->assertEquals(array('Host' => array('example.com:8080')), $request->getHeaders()); + $this->assertEquals(['Host' => ['example.com:8080']], $request->getHeaders()); } public function testGetHeadersReturnsHostHeaderFromUriWithCustomPortHttpOnHttpsPort() @@ -73,12 +73,12 @@ public function testGetHeadersReturnsHostHeaderFromUriWithCustomPortHttpOnHttpsP $request = new RequestMock( 'GET', 'http://example.com:443/', - array(), + [], $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), '1.1' ); - $this->assertEquals(array('Host' => array('example.com:443')), $request->getHeaders()); + $this->assertEquals(['Host' => ['example.com:443']], $request->getHeaders()); } public function testGetHeadersReturnsHostHeaderFromUriWithCustomPortHttpsOnHttpPort() @@ -86,12 +86,12 @@ public function testGetHeadersReturnsHostHeaderFromUriWithCustomPortHttpsOnHttpP $request = new RequestMock( 'GET', 'https://example.com:80/', - array(), + [], $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), '1.1' ); - $this->assertEquals(array('Host' => array('example.com:80')), $request->getHeaders()); + $this->assertEquals(['Host' => ['example.com:80']], $request->getHeaders()); } public function testGetHeadersReturnsHostHeaderFromUriWithoutDefaultHttpPort() @@ -99,12 +99,12 @@ public function testGetHeadersReturnsHostHeaderFromUriWithoutDefaultHttpPort() $request = new RequestMock( 'GET', 'http://example.com:80/', - array(), + [], $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), '1.1' ); - $this->assertEquals(array('Host' => array('example.com')), $request->getHeaders()); + $this->assertEquals(['Host' => ['example.com']], $request->getHeaders()); } public function testGetHeadersReturnsHostHeaderFromUriWithoutDefaultHttpsPort() @@ -112,12 +112,12 @@ public function testGetHeadersReturnsHostHeaderFromUriWithoutDefaultHttpsPort() $request = new RequestMock( 'GET', 'https://example.com:443/', - array(), + [], $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), '1.1' ); - $this->assertEquals(array('Host' => array('example.com')), $request->getHeaders()); + $this->assertEquals(['Host' => ['example.com']], $request->getHeaders()); } public function testGetHeadersReturnsHostHeaderFromUriBeforeOtherHeadersExplicitlyGiven() @@ -125,14 +125,14 @@ public function testGetHeadersReturnsHostHeaderFromUriBeforeOtherHeadersExplicit $request = new RequestMock( 'GET', 'http://example.com/', - array( + [ 'User-Agent' => 'demo' - ), + ], $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), '1.1' ); - $this->assertEquals(array('Host' => array('example.com'), 'User-Agent' => array('demo')), $request->getHeaders()); + $this->assertEquals(['Host' => ['example.com'], 'User-Agent' => ['demo']], $request->getHeaders()); } public function testGetHeadersReturnsHostHeaderFromHeadersExplicitlyGiven() @@ -140,14 +140,14 @@ public function testGetHeadersReturnsHostHeaderFromHeadersExplicitlyGiven() $request = new RequestMock( 'GET', 'http://localhost/', - array( + [ 'Host' => 'example.com:8080' - ), + ], $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), '1.1' ); - $this->assertEquals(array('Host' => array('example.com:8080')), $request->getHeaders()); + $this->assertEquals(['Host' => ['example.com:8080']], $request->getHeaders()); } public function testGetHeadersReturnsHostHeaderFromUriWhenHeadersExplicitlyGivenContainEmptyHostArray() @@ -155,14 +155,14 @@ public function testGetHeadersReturnsHostHeaderFromUriWhenHeadersExplicitlyGiven $request = new RequestMock( 'GET', 'https://example.com/', - array( - 'Host' => array() - ), + [ + 'Host' => [] + ], $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), '1.1' ); - $this->assertEquals(array('Host' => array('example.com')), $request->getHeaders()); + $this->assertEquals(['Host' => ['example.com']], $request->getHeaders()); } public function testGetRequestTargetReturnsPathAndQueryFromUri() @@ -170,7 +170,7 @@ public function testGetRequestTargetReturnsPathAndQueryFromUri() $request = new RequestMock( 'GET', 'http://example.com/demo?name=Alice', - array(), + [], $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), '1.1' ); @@ -183,7 +183,7 @@ public function testGetRequestTargetReturnsSlashOnlyIfUriHasNoPathOrQuery() $request = new RequestMock( 'GET', 'http://example.com', - array(), + [], $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), '1.1' ); @@ -196,7 +196,7 @@ public function testGetRequestTargetReturnsRequestTargetInAbsoluteFormIfGivenExp $request = new RequestMock( 'GET', 'http://example.com/demo?name=Alice', - array(), + [], $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), '1.1' ); @@ -210,7 +210,7 @@ public function testWithRequestTargetReturnsNewInstanceWhenRequestTargetIsChange $request = new RequestMock( 'GET', 'http://example.com/', - array(), + [], $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), '1.1' ); @@ -226,7 +226,7 @@ public function testWithRequestTargetReturnsSameInstanceWhenRequestTargetIsUncha $request = new RequestMock( 'GET', 'http://example.com/', - array(), + [], $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), '1.1' ); @@ -242,7 +242,7 @@ public function testWithMethodReturnsNewInstanceWhenMethodIsChanged() $request = new RequestMock( 'GET', 'http://example.com/', - array(), + [], $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), '1.1' ); @@ -258,7 +258,7 @@ public function testWithMethodReturnsSameInstanceWhenMethodIsUnchanged() $request = new RequestMock( 'GET', 'http://example.com/', - array(), + [], $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), '1.1' ); @@ -275,7 +275,7 @@ public function testGetUriReturnsUriInstanceGivenToCtor() $request = new RequestMock( 'GET', $uri, - array(), + [], $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), '1.1' ); @@ -288,7 +288,7 @@ public function testGetUriReturnsUriInstanceForUriStringGivenToCtor() $request = new RequestMock( 'GET', 'http://example.com/', - array(), + [], $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), '1.1' ); @@ -303,7 +303,7 @@ public function testWithUriReturnsNewInstanceWhenUriIsChanged() $request = new RequestMock( 'GET', 'http://example.com/', - array(), + [], $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), '1.1' ); @@ -323,7 +323,7 @@ public function testWithUriReturnsSameInstanceWhenUriIsUnchanged() $request = new RequestMock( 'GET', $uri, - array(), + [], $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), '1.1' ); @@ -338,7 +338,7 @@ public function testWithUriReturnsNewInstanceWithHostHeaderChangedIfUriContainsH $request = new RequestMock( 'GET', 'http://example.com/', - array(), + [], $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), '1.1' ); @@ -348,7 +348,7 @@ public function testWithUriReturnsNewInstanceWithHostHeaderChangedIfUriContainsH $this->assertNotSame($request, $new); $this->assertEquals('http://localhost/', (string) $new->getUri()); - $this->assertEquals(array('Host' => array('localhost')), $new->getHeaders()); + $this->assertEquals(['Host' => ['localhost']], $new->getHeaders()); } public function testWithUriReturnsNewInstanceWithHostHeaderChangedIfUriContainsHostWithCustomPort() @@ -356,7 +356,7 @@ public function testWithUriReturnsNewInstanceWithHostHeaderChangedIfUriContainsH $request = new RequestMock( 'GET', 'http://example.com/', - array(), + [], $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), '1.1' ); @@ -366,7 +366,7 @@ public function testWithUriReturnsNewInstanceWithHostHeaderChangedIfUriContainsH $this->assertNotSame($request, $new); $this->assertEquals('http://localhost:8080/', (string) $new->getUri()); - $this->assertEquals(array('Host' => array('localhost:8080')), $new->getHeaders()); + $this->assertEquals(['Host' => ['localhost:8080']], $new->getHeaders()); } public function testWithUriReturnsNewInstanceWithHostHeaderAddedAsFirstHeaderBeforeOthersIfUriContainsHost() @@ -374,9 +374,9 @@ public function testWithUriReturnsNewInstanceWithHostHeaderAddedAsFirstHeaderBef $request = new RequestMock( 'GET', 'http://example.com/', - array( + [ 'User-Agent' => 'test' - ), + ], $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), '1.1' ); @@ -387,7 +387,7 @@ public function testWithUriReturnsNewInstanceWithHostHeaderAddedAsFirstHeaderBef $this->assertNotSame($request, $new); $this->assertEquals('http://localhost/', (string) $new->getUri()); - $this->assertEquals(array('Host' => array('localhost'), 'User-Agent' => array('test')), $new->getHeaders()); + $this->assertEquals(['Host' => ['localhost'], 'User-Agent' => ['test']], $new->getHeaders()); } public function testWithUriReturnsNewInstanceWithHostHeaderUnchangedIfUriContainsNoHost() @@ -395,7 +395,7 @@ public function testWithUriReturnsNewInstanceWithHostHeaderUnchangedIfUriContain $request = new RequestMock( 'GET', 'http://example.com/', - array(), + [], $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), '1.1' ); @@ -405,7 +405,7 @@ public function testWithUriReturnsNewInstanceWithHostHeaderUnchangedIfUriContain $this->assertNotSame($request, $new); $this->assertEquals('/path', (string) $new->getUri()); - $this->assertEquals(array('Host' => array('example.com')), $new->getHeaders()); + $this->assertEquals(['Host' => ['example.com']], $new->getHeaders()); } public function testWithUriReturnsNewInstanceWithHostHeaderUnchangedIfPreserveHostIsTrue() @@ -413,7 +413,7 @@ public function testWithUriReturnsNewInstanceWithHostHeaderUnchangedIfPreserveHo $request = new RequestMock( 'GET', 'http://example.com/', - array(), + [], $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), '1.1' ); @@ -423,7 +423,7 @@ public function testWithUriReturnsNewInstanceWithHostHeaderUnchangedIfPreserveHo $this->assertNotSame($request, $new); $this->assertEquals('http://localhost/', (string) $new->getUri()); - $this->assertEquals(array('Host' => array('example.com')), $new->getHeaders()); + $this->assertEquals(['Host' => ['example.com']], $new->getHeaders()); } public function testWithUriReturnsNewInstanceWithHostHeaderAddedAsFirstHeaderNoMatterIfPreserveHostIsTrue() @@ -431,9 +431,9 @@ public function testWithUriReturnsNewInstanceWithHostHeaderAddedAsFirstHeaderNoM $request = new RequestMock( 'GET', 'http://example.com/', - array( + [ 'User-Agent' => 'test' - ), + ], $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), '1.1' ); @@ -444,6 +444,6 @@ public function testWithUriReturnsNewInstanceWithHostHeaderAddedAsFirstHeaderNoM $this->assertNotSame($request, $new); $this->assertEquals('http://example.com/', (string) $new->getUri()); - $this->assertEquals(array('Host' => array('example.com'), 'User-Agent' => array('test')), $new->getHeaders()); + $this->assertEquals(['Host' => ['example.com'], 'User-Agent' => ['test']], $new->getHeaders()); } } diff --git a/tests/Io/BufferedBodyTest.php b/tests/Io/BufferedBodyTest.php index 01154e71..4f5d042a 100644 --- a/tests/Io/BufferedBodyTest.php +++ b/tests/Io/BufferedBodyTest.php @@ -288,7 +288,7 @@ public function testGetMetadataWithoutKeyReturnsEmptyArray() { $stream = new BufferedBody('hello'); - $this->assertEquals(array(), $stream->getMetadata()); + $this->assertEquals([], $stream->getMetadata()); } public function testGetMetadataWithKeyReturnsNull() diff --git a/tests/Io/ChunkedDecoderTest.php b/tests/Io/ChunkedDecoderTest.php index 822ceaa6..5168f2d0 100644 --- a/tests/Io/ChunkedDecoderTest.php +++ b/tests/Io/ChunkedDecoderTest.php @@ -26,12 +26,12 @@ public function testSimpleChunk() $this->parser->on('end', $this->expectCallableNever()); $this->parser->on('close', $this->expectCallableNever()); - $this->input->emit('data', array("5\r\nhello\r\n")); + $this->input->emit('data', ["5\r\nhello\r\n"]); } public function testTwoChunks() { - $buffer = array(); + $buffer = []; $this->parser->on('data', function ($data) use (&$buffer) { $buffer[] = $data; }); @@ -39,9 +39,9 @@ public function testTwoChunks() $this->parser->on('end', $this->expectCallableNever()); $this->parser->on('close', $this->expectCallableNever()); - $this->input->emit('data', array("5\r\nhello\r\n3\r\nbla\r\n")); + $this->input->emit('data', ["5\r\nhello\r\n3\r\nbla\r\n"]); - $this->assertEquals(array('hello', 'bla'), $buffer); + $this->assertEquals(['hello', 'bla'], $buffer); } public function testEnd() @@ -50,12 +50,12 @@ public function testEnd() $this->parser->on('close', $this->expectCallableOnce()); $this->parser->on('error', $this->expectCallableNever()); - $this->input->emit('data', array("0\r\n\r\n")); + $this->input->emit('data', ["0\r\n\r\n"]); } public function testParameterWithEnd() { - $buffer = array(); + $buffer = []; $this->parser->on('data', function ($data) use (&$buffer) { $buffer[] = $data; }); @@ -64,9 +64,9 @@ public function testParameterWithEnd() $this->parser->on('close', $this->expectCallableOnce()); $this->parser->on('error', $this->expectCallableNever()); - $this->input->emit('data', array("5\r\nhello\r\n3\r\nbla\r\n0\r\n\r\n")); + $this->input->emit('data', ["5\r\nhello\r\n3\r\nbla\r\n0\r\n\r\n"]); - $this->assertEquals(array('hello', 'bla'), $buffer); + $this->assertEquals(['hello', 'bla'], $buffer); } public function testInvalidChunk() @@ -76,7 +76,7 @@ public function testInvalidChunk() $this->parser->on('close', $this->expectCallableOnce()); $this->parser->on('error', $this->expectCallableOnce()); - $this->input->emit('data', array("bla\r\n")); + $this->input->emit('data', ["bla\r\n"]); } public function testNeverEnd() @@ -85,7 +85,7 @@ public function testNeverEnd() $this->parser->on('close', $this->expectCallableNever()); $this->parser->on('error', $this->expectCallableNever()); - $this->input->emit('data', array("0\r\n")); + $this->input->emit('data', ["0\r\n"]); } public function testWrongChunkHex() @@ -94,7 +94,7 @@ public function testWrongChunkHex() $this->parser->on('close', $this->expectCallableOnce()); $this->parser->on('end', $this->expectCallableNever()); - $this->input->emit('data', array("2\r\na\r\n5\r\nhello\r\n")); + $this->input->emit('data', ["2\r\na\r\n5\r\nhello\r\n"]); } public function testSplittedChunk() @@ -104,8 +104,8 @@ public function testSplittedChunk() $this->parser->on('end', $this->expectCallableNever()); $this->parser->on('error', $this->expectCallableNever()); - $this->input->emit('data', array("4\r\n")); - $this->input->emit('data', array("welt\r\n")); + $this->input->emit('data', ["4\r\n"]); + $this->input->emit('data', ["welt\r\n"]); } public function testSplittedHeader() @@ -115,8 +115,8 @@ public function testSplittedHeader() $this->parser->on('end', $this->expectCallableNever());# $this->parser->on('error', $this->expectCallableNever()); - $this->input->emit('data', array("4")); - $this->input->emit('data', array("\r\nwelt\r\n")); + $this->input->emit('data', ["4"]); + $this->input->emit('data', ["\r\nwelt\r\n"]); } public function testSplittedBoth() @@ -126,14 +126,14 @@ public function testSplittedBoth() $this->parser->on('end', $this->expectCallableNever()); $this->parser->on('error', $this->expectCallableNever()); - $this->input->emit('data', array("4")); - $this->input->emit('data', array("\r\n")); - $this->input->emit('data', array("welt\r\n")); + $this->input->emit('data', ["4"]); + $this->input->emit('data', ["\r\n"]); + $this->input->emit('data', ["welt\r\n"]); } public function testCompletlySplitted() { - $buffer = array(); + $buffer = []; $this->parser->on('data', function ($data) use (&$buffer) { $buffer[] = $data; }); @@ -142,17 +142,17 @@ public function testCompletlySplitted() $this->parser->on('end', $this->expectCallableNever()); $this->parser->on('error', $this->expectCallableNever()); - $this->input->emit('data', array("4")); - $this->input->emit('data', array("\r\n")); - $this->input->emit('data', array("we")); - $this->input->emit('data', array("lt\r\n")); + $this->input->emit('data', ["4"]); + $this->input->emit('data', ["\r\n"]); + $this->input->emit('data', ["we"]); + $this->input->emit('data', ["lt\r\n"]); - $this->assertEquals(array('we', 'lt'), $buffer); + $this->assertEquals(['we', 'lt'], $buffer); } public function testMixed() { - $buffer = array(); + $buffer = []; $this->parser->on('data', function ($data) use (&$buffer) { $buffer[] = $data; }); @@ -161,17 +161,17 @@ public function testMixed() $this->parser->on('end', $this->expectCallableNever()); $this->parser->on('error', $this->expectCallableNever()); - $this->input->emit('data', array("4")); - $this->input->emit('data', array("\r\n")); - $this->input->emit('data', array("welt\r\n")); - $this->input->emit('data', array("5\r\nhello\r\n")); + $this->input->emit('data', ["4"]); + $this->input->emit('data', ["\r\n"]); + $this->input->emit('data', ["welt\r\n"]); + $this->input->emit('data', ["5\r\nhello\r\n"]); - $this->assertEquals(array('welt', 'hello'), $buffer); + $this->assertEquals(['welt', 'hello'], $buffer); } public function testBigger() { - $buffer = array(); + $buffer = []; $this->parser->on('data', function ($data) use (&$buffer) { $buffer[] = $data; }); @@ -180,18 +180,18 @@ public function testBigger() $this->parser->on('end', $this->expectCallableNever()); $this->parser->on('error', $this->expectCallableNever()); - $this->input->emit('data', array("1")); - $this->input->emit('data', array("0")); - $this->input->emit('data', array("\r\n")); - $this->input->emit('data', array("abcdeabcdeabcdea\r\n")); - $this->input->emit('data', array("5\r\nhello\r\n")); + $this->input->emit('data', ["1"]); + $this->input->emit('data', ["0"]); + $this->input->emit('data', ["\r\n"]); + $this->input->emit('data', ["abcdeabcdeabcdea\r\n"]); + $this->input->emit('data', ["5\r\nhello\r\n"]); - $this->assertEquals(array('abcdeabcdeabcdea', 'hello'), $buffer); + $this->assertEquals(['abcdeabcdeabcdea', 'hello'], $buffer); } public function testOneUnfinished() { - $buffer = array(); + $buffer = []; $this->parser->on('data', function ($data) use (&$buffer) { $buffer[] = $data; }); @@ -200,11 +200,11 @@ public function testOneUnfinished() $this->parser->on('end', $this->expectCallableNever()); $this->parser->on('error', $this->expectCallableNever()); - $this->input->emit('data', array("3\r\n")); - $this->input->emit('data', array("bla\r\n")); - $this->input->emit('data', array("5\r\nhello")); + $this->input->emit('data', ["3\r\n"]); + $this->input->emit('data', ["bla\r\n"]); + $this->input->emit('data', ["5\r\nhello"]); - $this->assertEquals(array('bla', 'hello'), $buffer); + $this->assertEquals(['bla', 'hello'], $buffer); } public function testChunkIsBiggerThenExpected() @@ -214,8 +214,8 @@ public function testChunkIsBiggerThenExpected() $this->parser->on('end', $this->expectCallableNever()); $this->parser->on('error', $this->expectCallableOnce()); - $this->input->emit('data', array("5\r\n")); - $this->input->emit('data', array("hello world\r\n")); + $this->input->emit('data', ["5\r\n"]); + $this->input->emit('data', ["hello world\r\n"]); } public function testHandleUnexpectedEnd() @@ -235,7 +235,7 @@ public function testExtensionWillBeIgnored() $this->parser->on('end', $this->expectCallableNever()); $this->parser->on('error', $this->expectCallableNever()); - $this->input->emit('data', array("3;hello=world;foo=bar\r\nbla")); + $this->input->emit('data', ["3;hello=world;foo=bar\r\nbla"]); } public function testChunkHeaderIsTooBig() @@ -249,7 +249,7 @@ public function testChunkHeaderIsTooBig() for ($i = 0; $i < 1025; $i++) { $data .= 'a'; } - $this->input->emit('data', array($data)); + $this->input->emit('data', [$data]); } public function testChunkIsMaximumSize() @@ -265,7 +265,7 @@ public function testChunkIsMaximumSize() } $data .= "\r\n"; - $this->input->emit('data', array($data)); + $this->input->emit('data', [$data]); } public function testLateCrlf() @@ -275,9 +275,9 @@ public function testLateCrlf() $this->parser->on('end', $this->expectCallableNever()); $this->parser->on('error', $this->expectCallableNever()); - $this->input->emit('data', array("4\r\nlate")); - $this->input->emit('data', array("\r")); - $this->input->emit('data', array("\n")); + $this->input->emit('data', ["4\r\nlate"]); + $this->input->emit('data', ["\r"]); + $this->input->emit('data', ["\n"]); } public function testNoCrlfInChunk() @@ -287,7 +287,7 @@ public function testNoCrlfInChunk() $this->parser->on('end', $this->expectCallableNever()); $this->parser->on('error', $this->expectCallableOnce()); - $this->input->emit('data', array("2\r\nno crlf")); + $this->input->emit('data', ["2\r\nno crlf"]); } public function testNoCrlfInChunkSplitted() @@ -297,10 +297,10 @@ public function testNoCrlfInChunkSplitted() $this->parser->on('end', $this->expectCallableNever()); $this->parser->on('error', $this->expectCallableOnce()); - $this->input->emit('data', array("2\r\n")); - $this->input->emit('data', array("no")); - $this->input->emit('data', array("further")); - $this->input->emit('data', array("clrf")); + $this->input->emit('data', ["2\r\n"]); + $this->input->emit('data', ["no"]); + $this->input->emit('data', ["further"]); + $this->input->emit('data', ["clrf"]); } public function testEmitEmptyChunkBody() @@ -310,9 +310,9 @@ public function testEmitEmptyChunkBody() $this->parser->on('end', $this->expectCallableNever()); $this->parser->on('error', $this->expectCallableNever()); - $this->input->emit('data', array("2\r\n")); - $this->input->emit('data', array("")); - $this->input->emit('data', array("")); + $this->input->emit('data', ["2\r\n"]); + $this->input->emit('data', [""]); + $this->input->emit('data', [""]); } public function testEmitCrlfAsChunkBody() @@ -322,9 +322,9 @@ public function testEmitCrlfAsChunkBody() $this->parser->on('end', $this->expectCallableNever()); $this->parser->on('error', $this->expectCallableNever()); - $this->input->emit('data', array("2\r\n")); - $this->input->emit('data', array("\r\n")); - $this->input->emit('data', array("\r\n")); + $this->input->emit('data', ["2\r\n"]); + $this->input->emit('data', ["\r\n"]); + $this->input->emit('data', ["\r\n"]); } public function testNegativeHeader() @@ -334,7 +334,7 @@ public function testNegativeHeader() $this->parser->on('end', $this->expectCallableNever()); $this->parser->on('error', $this->expectCallableOnce()); - $this->input->emit('data', array("-2\r\n")); + $this->input->emit('data', ["-2\r\n"]); } public function testHexDecimalInBodyIsPotentialThread() @@ -344,7 +344,7 @@ public function testHexDecimalInBodyIsPotentialThread() $this->parser->on('end', $this->expectCallableNever()); $this->parser->on('error', $this->expectCallableOnce()); - $this->input->emit('data', array("4\r\ntest5\r\nworld")); + $this->input->emit('data', ["4\r\ntest5\r\nworld"]); } public function testHexDecimalInBodyIsPotentialThreadSplitted() @@ -354,17 +354,17 @@ public function testHexDecimalInBodyIsPotentialThreadSplitted() $this->parser->on('end', $this->expectCallableNever()); $this->parser->on('error', $this->expectCallableOnce()); - $this->input->emit('data', array("4")); - $this->input->emit('data', array("\r\n")); - $this->input->emit('data', array("test")); - $this->input->emit('data', array("5")); - $this->input->emit('data', array("\r\n")); - $this->input->emit('data', array("world")); + $this->input->emit('data', ["4"]); + $this->input->emit('data', ["\r\n"]); + $this->input->emit('data', ["test"]); + $this->input->emit('data', ["5"]); + $this->input->emit('data', ["\r\n"]); + $this->input->emit('data', ["world"]); } public function testEmitSingleCharacter() { - $buffer = array(); + $buffer = []; $this->parser->on('data', function ($data) use (&$buffer) { $buffer[] = $data; }); @@ -375,10 +375,10 @@ public function testEmitSingleCharacter() $array = str_split("4\r\ntest\r\n0\r\n\r\n"); foreach ($array as $character) { - $this->input->emit('data', array($character)); + $this->input->emit('data', [$character]); } - $this->assertEquals(array('t', 'e', 's', 't'), $buffer); + $this->assertEquals(['t', 'e', 's', 't'], $buffer); } public function testHandleError() @@ -387,7 +387,7 @@ public function testHandleError() $this->parser->on('close', $this->expectCallableOnce()); $this->parser->on('end', $this->expectCallableNever()); - $this->input->emit('error', array(new \RuntimeException())); + $this->input->emit('error', [new \RuntimeException()]); $this->assertFalse($this->parser->isReadable()); } @@ -425,7 +425,7 @@ public function testHandleClose() $this->parser->on('close', $this->expectCallableOnce()); $this->input->close(); - $this->input->emit('end', array()); + $this->input->emit('end', []); $this->assertFalse($this->parser->isReadable()); } @@ -445,7 +445,7 @@ public function testOutputStreamCanCloseInputStream() public function testLeadingZerosWillBeIgnored() { - $buffer = array(); + $buffer = []; $this->parser->on('data', function ($data) use (&$buffer) { $buffer[] = $data; }); @@ -454,10 +454,10 @@ public function testLeadingZerosWillBeIgnored() $this->parser->on('end', $this->expectCallableNever()); $this->parser->on('close', $this->expectCallableNever()); - $this->input->emit('data', array("00005\r\nhello\r\n")); - $this->input->emit('data', array("0000b\r\nhello world\r\n")); + $this->input->emit('data', ["00005\r\nhello\r\n"]); + $this->input->emit('data', ["0000b\r\nhello world\r\n"]); - $this->assertEquals(array('hello', 'hello world'), $buffer); + $this->assertEquals(['hello', 'hello world'], $buffer); } public function testLeadingZerosInEndChunkWillBeIgnored() @@ -467,7 +467,7 @@ public function testLeadingZerosInEndChunkWillBeIgnored() $this->parser->on('end', $this->expectCallableOnce()); $this->parser->on('close', $this->expectCallableOnce()); - $this->input->emit('data', array("0000\r\n\r\n")); + $this->input->emit('data', ["0000\r\n\r\n"]); } public function testAdditionalWhitespaceInEndChunkWillBeIgnored() @@ -477,7 +477,7 @@ public function testAdditionalWhitespaceInEndChunkWillBeIgnored() $this->parser->on('end', $this->expectCallableOnce()); $this->parser->on('close', $this->expectCallableOnce()); - $this->input->emit('data', array(" 0 \r\n\r\n")); + $this->input->emit('data', [" 0 \r\n\r\n"]); } public function testEndChunkWithTrailersWillBeIgnored() @@ -487,7 +487,7 @@ public function testEndChunkWithTrailersWillBeIgnored() $this->parser->on('end', $this->expectCallableOnce()); $this->parser->on('close', $this->expectCallableOnce()); - $this->input->emit('data', array("0\r\nFoo: bar\r\n\r\n")); + $this->input->emit('data', ["0\r\nFoo: bar\r\n\r\n"]); } public function testEndChunkWithMultipleTrailersWillBeIgnored() @@ -497,7 +497,7 @@ public function testEndChunkWithMultipleTrailersWillBeIgnored() $this->parser->on('end', $this->expectCallableOnce()); $this->parser->on('close', $this->expectCallableOnce()); - $this->input->emit('data', array("0\r\nFoo: a\r\nBar: b\r\nBaz: c\r\n\r\n")); + $this->input->emit('data', ["0\r\nFoo: a\r\nBar: b\r\nBaz: c\r\n\r\n"]); } public function testLeadingZerosInInvalidChunk() @@ -507,7 +507,7 @@ public function testLeadingZerosInInvalidChunk() $this->parser->on('end', $this->expectCallableNever()); $this->parser->on('close', $this->expectCallableOnce()); - $this->input->emit('data', array("0000hello\r\n\r\n")); + $this->input->emit('data', ["0000hello\r\n\r\n"]); } public function testEmptyHeaderLeadsToError() @@ -517,7 +517,7 @@ public function testEmptyHeaderLeadsToError() $this->parser->on('end', $this->expectCallableNever()); $this->parser->on('close', $this->expectCallableOnce()); - $this->input->emit('data', array("\r\n\r\n")); + $this->input->emit('data', ["\r\n\r\n"]); } public function testEmptyHeaderAndFilledBodyLeadsToError() @@ -527,7 +527,7 @@ public function testEmptyHeaderAndFilledBodyLeadsToError() $this->parser->on('end', $this->expectCallableNever()); $this->parser->on('close', $this->expectCallableOnce()); - $this->input->emit('data', array("\r\nhello\r\n")); + $this->input->emit('data', ["\r\nhello\r\n"]); } public function testUpperCaseHexWillBeHandled() @@ -537,7 +537,7 @@ public function testUpperCaseHexWillBeHandled() $this->parser->on('end', $this->expectCallableNever()); $this->parser->on('close', $this->expectCallableNever()); - $this->input->emit('data', array("A\r\n0123456790\r\n")); + $this->input->emit('data', ["A\r\n0123456790\r\n"]); } public function testLowerCaseHexWillBeHandled() @@ -547,7 +547,7 @@ public function testLowerCaseHexWillBeHandled() $this->parser->on('end', $this->expectCallableNever()); $this->parser->on('close', $this->expectCallableNever()); - $this->input->emit('data', array("a\r\n0123456790\r\n")); + $this->input->emit('data', ["a\r\n0123456790\r\n"]); } public function testMixedUpperAndLowerCaseHexValuesInHeaderWillBeHandled() @@ -559,6 +559,6 @@ public function testMixedUpperAndLowerCaseHexValuesInHeaderWillBeHandled() $this->parser->on('end', $this->expectCallableNever()); $this->parser->on('close', $this->expectCallableNever()); - $this->input->emit('data', array("aA\r\n" . $data . "\r\n")); + $this->input->emit('data', ["aA\r\n" . $data . "\r\n"]); } } diff --git a/tests/Io/ChunkedEncoderTest.php b/tests/Io/ChunkedEncoderTest.php index 87ce44c4..96b97848 100644 --- a/tests/Io/ChunkedEncoderTest.php +++ b/tests/Io/ChunkedEncoderTest.php @@ -23,19 +23,19 @@ public function setUpChunkedStream() public function testChunked() { $this->chunkedStream->on('data', $this->expectCallableOnceWith("5\r\nhello\r\n")); - $this->input->emit('data', array('hello')); + $this->input->emit('data', ['hello']); } public function testEmptyString() { $this->chunkedStream->on('data', $this->expectCallableNever()); - $this->input->emit('data', array('')); + $this->input->emit('data', ['']); } public function testBiggerStringToCheckHexValue() { $this->chunkedStream->on('data', $this->expectCallableOnceWith("1a\r\nabcdefghijklmnopqrstuvwxyz\r\n")); - $this->input->emit('data', array('abcdefghijklmnopqrstuvwxyz')); + $this->input->emit('data', ['abcdefghijklmnopqrstuvwxyz']); } public function testHandleClose() @@ -52,7 +52,7 @@ public function testHandleError() $this->chunkedStream->on('error', $this->expectCallableOnce()); $this->chunkedStream->on('close', $this->expectCallableOnce()); - $this->input->emit('error', array(new \RuntimeException())); + $this->input->emit('error', [new \RuntimeException()]); $this->assertFalse($this->chunkedStream->isReadable()); } diff --git a/tests/Io/ClientConnectionManagerTest.php b/tests/Io/ClientConnectionManagerTest.php index 6aafa6db..88c5ff4f 100644 --- a/tests/Io/ClientConnectionManagerTest.php +++ b/tests/Io/ClientConnectionManagerTest.php @@ -7,6 +7,7 @@ use React\Promise\Promise; use React\Promise\PromiseInterface; use React\Tests\Http\TestCase; +use function React\Promise\resolve; class ClientConnectionManagerTest extends TestCase { @@ -100,51 +101,51 @@ public function testConnectReusesIdleConnectionFromPreviousKeepAliveCallWithoutU $streamHandler = null; $connectionToReuse->expects($this->exactly(3))->method('on')->withConsecutive( - array( + [ 'close', $this->callback(function ($cb) use (&$streamHandler) { $streamHandler = $cb; return true; }) - ), - array( + ], + [ 'data', $this->callback(function ($cb) use (&$streamHandler) { assert($streamHandler instanceof \Closure); return $cb === $streamHandler; }) - ), - array( + ], + [ 'error', $this->callback(function ($cb) use (&$streamHandler) { assert($streamHandler instanceof \Closure); return $cb === $streamHandler; }) - ) + ] ); $connectionToReuse->expects($this->exactly(3))->method('removeListener')->withConsecutive( - array( + [ 'close', $this->callback(function ($cb) use (&$streamHandler) { assert($streamHandler instanceof \Closure); return $cb === $streamHandler; }) - ), - array( + ], + [ 'data', $this->callback(function ($cb) use (&$streamHandler) { assert($streamHandler instanceof \Closure); return $cb === $streamHandler; }) - ), - array( + ], + [ 'error', $this->callback(function ($cb) use (&$streamHandler) { assert($streamHandler instanceof \Closure); return $cb === $streamHandler; }) - ) + ] ); $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); @@ -223,7 +224,7 @@ public function testConnectUsesConnectorForNewConnectionWhenPreviousConnectReuse $secondConnection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); - $connector->expects($this->once())->method('connect')->with('tls://reactphp.org:443')->willReturn(\React\Promise\resolve($secondConnection)); + $connector->expects($this->once())->method('connect')->with('tls://reactphp.org:443')->willReturn(resolve($secondConnection)); $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); @@ -297,7 +298,7 @@ public function testConnectUsesConnectorForNewConnectionWhenIdleConnectionFromPr $secondConnection->expects($this->never())->method('close'); $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); - $connector->expects($this->once())->method('connect')->with('tls://reactphp.org:443')->willReturn(\React\Promise\resolve($secondConnection)); + $connector->expects($this->once())->method('connect')->with('tls://reactphp.org:443')->willReturn(resolve($secondConnection)); $timerCallback = null; $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); @@ -334,34 +335,34 @@ public function testConnectUsesConnectorForNewConnectionWhenIdleConnectionFromPr $streamHandler = null; $firstConnection->expects($this->exactly(3))->method('on')->withConsecutive( - array( + [ 'close', $this->callback(function ($cb) use (&$streamHandler) { $streamHandler = $cb; return true; }) - ), - array( + ], + [ 'data', $this->callback(function ($cb) use (&$streamHandler) { assert($streamHandler instanceof \Closure); return $cb === $streamHandler; }) - ), - array( + ], + [ 'error', $this->callback(function ($cb) use (&$streamHandler) { assert($streamHandler instanceof \Closure); return $cb === $streamHandler; }) - ) + ] ); $secondConnection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); $secondConnection->expects($this->never())->method('close'); $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); - $connector->expects($this->once())->method('connect')->with('tls://reactphp.org:443')->willReturn(\React\Promise\resolve($secondConnection)); + $connector->expects($this->once())->method('connect')->with('tls://reactphp.org:443')->willReturn(resolve($secondConnection)); $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); diff --git a/tests/Io/ClientRequestStreamTest.php b/tests/Io/ClientRequestStreamTest.php index 9a5373a1..0df92961 100644 --- a/tests/Io/ClientRequestStreamTest.php +++ b/tests/Io/ClientRequestStreamTest.php @@ -12,6 +12,8 @@ use React\Stream\DuplexResourceStream; use React\Stream\ReadableStreamInterface; use React\Tests\Http\TestCase; +use function React\Promise\reject; +use function React\Promise\resolve; class ClientRequestStreamTest extends TestCase { @@ -22,25 +24,25 @@ public function testRequestShouldUseConnectionManagerWithUriFromRequestAndBindTo $uri = new Uri('http://www.example.com'); $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); - $connectionManager->expects($this->once())->method('connect')->with($uri)->willReturn(\React\Promise\resolve($connection)); + $connectionManager->expects($this->once())->method('connect')->with($uri)->willReturn(resolve($connection)); $requestData = new Request('GET', $uri); $request = new ClientRequestStream($connectionManager, $requestData); $connection->expects($this->atLeast(5))->method('on')->withConsecutive( - array('drain', $this->identicalTo(array($request, 'handleDrain'))), - array('data', $this->identicalTo(array($request, 'handleData'))), - array('end', $this->identicalTo(array($request, 'handleEnd'))), - array('error', $this->identicalTo(array($request, 'handleError'))), - array('close', $this->identicalTo(array($request, 'close'))) + ['drain', $this->identicalTo([$request, 'handleDrain'])], + ['data', $this->identicalTo([$request, 'handleData'])], + ['end', $this->identicalTo([$request, 'handleEnd'])], + ['error', $this->identicalTo([$request, 'handleError'])], + ['close', $this->identicalTo([$request, 'close'])] ); $connection->expects($this->exactly(5))->method('removeListener')->withConsecutive( - array('drain', $this->identicalTo(array($request, 'handleDrain'))), - array('data', $this->identicalTo(array($request, 'handleData'))), - array('end', $this->identicalTo(array($request, 'handleEnd'))), - array('error', $this->identicalTo(array($request, 'handleError'))), - array('close', $this->identicalTo(array($request, 'close'))) + ['drain', $this->identicalTo([$request, 'handleDrain'])], + ['data', $this->identicalTo([$request, 'handleData'])], + ['end', $this->identicalTo([$request, 'handleEnd'])], + ['error', $this->identicalTo([$request, 'handleError'])], + ['close', $this->identicalTo([$request, 'close'])] ); $request->end(); @@ -54,7 +56,7 @@ public function testRequestShouldUseConnectionManagerWithUriFromRequestAndBindTo public function requestShouldEmitErrorIfConnectionFails() { $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); - $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\reject(new \RuntimeException())); + $connectionManager->expects($this->once())->method('connect')->willReturn(reject(new \RuntimeException())); $requestData = new Request('GET', 'http://www.example.com'); $request = new ClientRequestStream($connectionManager, $requestData); @@ -71,7 +73,7 @@ public function requestShouldEmitErrorIfConnectionClosesBeforeResponseIsParsed() $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); - $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection)); + $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); $requestData = new Request('GET', 'http://www.example.com'); $request = new ClientRequestStream($connectionManager, $requestData); @@ -89,7 +91,7 @@ public function requestShouldEmitErrorIfConnectionEmitsError() $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); - $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection)); + $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); $requestData = new Request('GET', 'http://www.example.com'); $request = new ClientRequestStream($connectionManager, $requestData); @@ -105,44 +107,44 @@ public static function provideInvalidRequest() { $request = new Request('GET' , "http://localhost/"); - return array( - array( + return [ + [ $request->withMethod("INVA\r\nLID", '') - ), - array( + ], + [ $request->withRequestTarget('/inva lid') - ), - array( + ], + [ $request->withHeader('Invalid', "Yes\r\n") - ), - array( + ], + [ $request->withHeader('Invalid', "Yes\n") - ), - array( + ], + [ $request->withHeader('Invalid', "Yes\r") - ), - array( + ], + [ $request->withHeader("Inva\r\nlid", 'Yes') - ), - array( + ], + [ $request->withHeader("Inva\nlid", 'Yes') - ), - array( + ], + [ $request->withHeader("Inva\rlid", 'Yes') - ), - array( + ], + [ $request->withHeader('Inva Lid', 'Yes') - ), - array( + ], + [ $request->withHeader('Inva:Lid', 'Yes') - ), - array( + ], + [ $request->withHeader('Invalid', "Val\0ue") - ), - array( + ], + [ $request->withHeader("Inva\0lid", 'Yes') - ) - ); + ] + ]; } /** @@ -168,7 +170,7 @@ public function requestShouldEmitErrorIfRequestParserThrowsException() $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); - $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection)); + $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); $requestData = new Request('GET', 'http://www.example.com'); $request = new ClientRequestStream($connectionManager, $requestData); @@ -187,9 +189,9 @@ public function getRequestShouldSendAGetRequest() $connection->expects($this->once())->method('write')->with("GET / HTTP/1.0\r\nHost: www.example.com\r\n\r\n"); $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); - $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection)); + $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); - $requestData = new Request('GET', 'http://www.example.com', array(), '', '1.0'); + $requestData = new Request('GET', 'http://www.example.com', [], '', '1.0'); $request = new ClientRequestStream($connectionManager, $requestData); $request->end(); @@ -202,9 +204,9 @@ public function getHttp11RequestShouldSendAGetRequestWithGivenConnectionCloseHea $connection->expects($this->once())->method('write')->with("GET / HTTP/1.1\r\nHost: www.example.com\r\nConnection: close\r\n\r\n"); $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); - $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection)); + $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); - $requestData = new Request('GET', 'http://www.example.com', array('Connection' => 'close'), '', '1.1'); + $requestData = new Request('GET', 'http://www.example.com', ['Connection' => 'close'], '', '1.1'); $request = new ClientRequestStream($connectionManager, $requestData); $request->end(); @@ -217,9 +219,9 @@ public function getOptionsAsteriskShouldSendAOptionsRequestAsteriskRequestTarget $connection->expects($this->once())->method('write')->with("OPTIONS * HTTP/1.1\r\nHost: www.example.com\r\nConnection: close\r\n\r\n"); $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); - $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection)); + $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); - $requestData = new Request('OPTIONS', 'http://www.example.com', array('Connection' => 'close'), '', '1.1'); + $requestData = new Request('OPTIONS', 'http://www.example.com', ['Connection' => 'close'], '', '1.1'); $requestData = $requestData->withRequestTarget('*'); $request = new ClientRequestStream($connectionManager, $requestData); @@ -233,16 +235,15 @@ public function testStreamShouldEmitResponseWithEmptyBodyWhenResponseContainsCon $connection->expects($this->once())->method('close'); $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); - $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection)); + $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); - $requestData = new Request('GET', 'http://www.example.com', array('Connection' => 'close'), '', '1.1'); + $requestData = new Request('GET', 'http://www.example.com', ['Connection' => 'close'], '', '1.1'); $request = new ClientRequestStream($connectionManager, $requestData); - $that = $this; - $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($that) { - $body->on('data', $that->expectCallableNever()); - $body->on('end', $that->expectCallableOnce()); - $body->on('close', $that->expectCallableOnce()); + $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) { + $body->on('data', $this->expectCallableNever()); + $body->on('end', $this->expectCallableOnce()); + $body->on('close', $this->expectCallableOnce()); }); $request->on('close', $this->expectCallableOnce()); @@ -258,16 +259,15 @@ public function testStreamShouldEmitResponseWithEmptyBodyWhenResponseContainsSta $connection->expects($this->once())->method('close'); $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); - $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection)); + $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); - $requestData = new Request('GET', 'http://www.example.com', array('Connection' => 'close'), '', '1.1'); + $requestData = new Request('GET', 'http://www.example.com', ['Connection' => 'close'], '', '1.1'); $request = new ClientRequestStream($connectionManager, $requestData); - $that = $this; - $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($that) { - $body->on('data', $that->expectCallableNever()); - $body->on('end', $that->expectCallableOnce()); - $body->on('close', $that->expectCallableOnce()); + $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) { + $body->on('data', $this->expectCallableNever()); + $body->on('end', $this->expectCallableOnce()); + $body->on('close', $this->expectCallableOnce()); }); $request->on('close', $this->expectCallableOnce()); @@ -283,16 +283,15 @@ public function testStreamShouldEmitResponseWithEmptyBodyWhenResponseContainsSta $connection->expects($this->once())->method('close'); $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); - $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection)); + $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); - $requestData = new Request('GET', 'http://www.example.com', array('Connection' => 'close'), '', '1.1'); + $requestData = new Request('GET', 'http://www.example.com', ['Connection' => 'close'], '', '1.1'); $request = new ClientRequestStream($connectionManager, $requestData); - $that = $this; - $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($that) { - $body->on('data', $that->expectCallableNever()); - $body->on('end', $that->expectCallableOnce()); - $body->on('close', $that->expectCallableOnce()); + $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) { + $body->on('data', $this->expectCallableNever()); + $body->on('end', $this->expectCallableOnce()); + $body->on('close', $this->expectCallableOnce()); }); $request->on('close', $this->expectCallableOnce()); @@ -308,16 +307,15 @@ public function testStreamShouldEmitResponseWithEmptyBodyWhenRequestMethodIsHead $connection->expects($this->once())->method('close'); $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); - $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection)); + $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); - $requestData = new Request('HEAD', 'http://www.example.com', array('Connection' => 'close'), '', '1.1'); + $requestData = new Request('HEAD', 'http://www.example.com', ['Connection' => 'close'], '', '1.1'); $request = new ClientRequestStream($connectionManager, $requestData); - $that = $this; - $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($that) { - $body->on('data', $that->expectCallableNever()); - $body->on('end', $that->expectCallableOnce()); - $body->on('close', $that->expectCallableOnce()); + $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) { + $body->on('data', $this->expectCallableNever()); + $body->on('end', $this->expectCallableOnce()); + $body->on('close', $this->expectCallableOnce()); }); $request->on('close', $this->expectCallableOnce()); @@ -333,16 +331,15 @@ public function testStreamShouldEmitResponseWithStreamingBodyUntilEndWhenRespons $connection->expects($this->once())->method('close'); $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); - $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection)); + $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); - $requestData = new Request('GET', 'http://www.example.com', array('Connection' => 'close'), '', '1.1'); + $requestData = new Request('GET', 'http://www.example.com', ['Connection' => 'close'], '', '1.1'); $request = new ClientRequestStream($connectionManager, $requestData); - $that = $this; - $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($that) { - $body->on('data', $that->expectCallableOnceWith('OK')); - $body->on('end', $that->expectCallableOnce()); - $body->on('close', $that->expectCallableOnce()); + $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) { + $body->on('data', $this->expectCallableOnceWith('OK')); + $body->on('end', $this->expectCallableOnce()); + $body->on('close', $this->expectCallableOnce()); }); $request->on('close', $this->expectCallableOnce()); @@ -358,16 +355,15 @@ public function testStreamShouldEmitResponseWithStreamingBodyWithoutDataWhenResp $connection->expects($this->never())->method('close'); $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); - $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection)); + $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); - $requestData = new Request('GET', 'http://www.example.com', array('Connection' => 'close'), '', '1.1'); + $requestData = new Request('GET', 'http://www.example.com', ['Connection' => 'close'], '', '1.1'); $request = new ClientRequestStream($connectionManager, $requestData); - $that = $this; - $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($that) { - $body->on('data', $that->expectCallableNever()); - $body->on('end', $that->expectCallableNever()); - $body->on('close', $that->expectCallableNever()); + $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) { + $body->on('data', $this->expectCallableNever()); + $body->on('end', $this->expectCallableNever()); + $body->on('close', $this->expectCallableNever()); }); $request->on('close', $this->expectCallableNever()); @@ -383,16 +379,15 @@ public function testStreamShouldEmitResponseWithStreamingBodyWithDataWithoutEndW $connection->expects($this->never())->method('close'); $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); - $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection)); + $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); - $requestData = new Request('GET', 'http://www.example.com', array('Connection' => 'close'), '', '1.1'); + $requestData = new Request('GET', 'http://www.example.com', ['Connection' => 'close'], '', '1.1'); $request = new ClientRequestStream($connectionManager, $requestData); - $that = $this; - $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($that) { - $body->on('data', $that->expectCallableOnce('O')); - $body->on('end', $that->expectCallableNever()); - $body->on('close', $that->expectCallableNever()); + $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) { + $body->on('data', $this->expectCallableOnce('O')); + $body->on('end', $this->expectCallableNever()); + $body->on('close', $this->expectCallableNever()); }); $request->on('close', $this->expectCallableNever()); @@ -408,16 +403,15 @@ public function testStreamShouldEmitResponseWithStreamingBodyUntilEndWhenRespons $connection->expects($this->once())->method('close'); $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); - $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection)); + $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); - $requestData = new Request('GET', 'http://www.example.com', array('Connection' => 'close'), '', '1.1'); + $requestData = new Request('GET', 'http://www.example.com', ['Connection' => 'close'], '', '1.1'); $request = new ClientRequestStream($connectionManager, $requestData); - $that = $this; - $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($that) { - $body->on('data', $that->expectCallableOnceWith('OK')); - $body->on('end', $that->expectCallableOnce()); - $body->on('close', $that->expectCallableOnce()); + $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) { + $body->on('data', $this->expectCallableOnceWith('OK')); + $body->on('end', $this->expectCallableOnce()); + $body->on('close', $this->expectCallableOnce()); }); $request->on('close', $this->expectCallableOnce()); @@ -433,16 +427,15 @@ public function testStreamShouldEmitResponseWithStreamingBodyWithoutDataWhenResp $connection->expects($this->never())->method('close'); $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); - $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection)); + $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); - $requestData = new Request('GET', 'http://www.example.com', array('Connection' => 'close'), '', '1.1'); + $requestData = new Request('GET', 'http://www.example.com', ['Connection' => 'close'], '', '1.1'); $request = new ClientRequestStream($connectionManager, $requestData); - $that = $this; - $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($that) { - $body->on('data', $that->expectCallableNever()); - $body->on('end', $that->expectCallableNever()); - $body->on('close', $that->expectCallableNever()); + $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) { + $body->on('data', $this->expectCallableNever()); + $body->on('end', $this->expectCallableNever()); + $body->on('close', $this->expectCallableNever()); }); $request->on('close', $this->expectCallableNever()); @@ -458,16 +451,15 @@ public function testStreamShouldEmitResponseWithStreamingBodyWithDataWithoutEndW $connection->expects($this->never())->method('close'); $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); - $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection)); + $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); - $requestData = new Request('GET', 'http://www.example.com', array('Connection' => 'close'), '', '1.1'); + $requestData = new Request('GET', 'http://www.example.com', ['Connection' => 'close'], '', '1.1'); $request = new ClientRequestStream($connectionManager, $requestData); - $that = $this; - $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($that) { - $body->on('data', $that->expectCallableOnceWith('O')); - $body->on('end', $that->expectCallableNever()); - $body->on('close', $that->expectCallableNever()); + $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) { + $body->on('data', $this->expectCallableOnceWith('O')); + $body->on('end', $this->expectCallableNever()); + $body->on('close', $this->expectCallableNever()); }); $request->on('close', $this->expectCallableNever()); @@ -483,16 +475,15 @@ public function testStreamShouldEmitResponseWithStreamingBodyWithDataWithoutEndW $connection->expects($this->never())->method('close'); $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); - $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection)); + $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); - $requestData = new Request('GET', 'http://www.example.com', array('Connection' => 'close'), '', '1.1'); + $requestData = new Request('GET', 'http://www.example.com', ['Connection' => 'close'], '', '1.1'); $request = new ClientRequestStream($connectionManager, $requestData); - $that = $this; - $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($that) { - $body->on('data', $that->expectCallableOnce('O')); - $body->on('end', $that->expectCallableNever()); - $body->on('close', $that->expectCallableNever()); + $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) { + $body->on('data', $this->expectCallableOnce('O')); + $body->on('end', $this->expectCallableNever()); + $body->on('close', $this->expectCallableNever()); }); $request->on('close', $this->expectCallableNever()); @@ -520,16 +511,15 @@ public function testStreamShouldEmitResponseWithStreamingBodyUntilEndWhenRespons })); $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); - $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection)); + $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); - $requestData = new Request('GET', 'http://www.example.com', array('Connection' => 'close'), '', '1.1'); + $requestData = new Request('GET', 'http://www.example.com', ['Connection' => 'close'], '', '1.1'); $request = new ClientRequestStream($connectionManager, $requestData); - $that = $this; - $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($that) { - $body->on('data', $that->expectCallableOnce('OK')); - $body->on('end', $that->expectCallableOnce()); - $body->on('close', $that->expectCallableOnce()); + $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) { + $body->on('data', $this->expectCallableOnce('OK')); + $body->on('end', $this->expectCallableOnce()); + $body->on('close', $this->expectCallableOnce()); }); $request->on('close', $this->expectCallableOnce()); @@ -549,10 +539,10 @@ public function testStreamShouldReuseConnectionForHttp11ByDefault() $connection->expects($this->never())->method('close'); $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); - $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection)); + $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); $connectionManager->expects($this->once())->method('keepAlive')->with(new Uri('http://www.example.com'), $connection); - $requestData = new Request('GET', 'http://www.example.com', array(), '', '1.1'); + $requestData = new Request('GET', 'http://www.example.com', [], '', '1.1'); $request = new ClientRequestStream($connectionManager, $requestData); $request->on('close', $this->expectCallableOnce()); @@ -570,9 +560,9 @@ public function testStreamShouldNotReuseConnectionWhenResponseContainsConnection $connection->expects($this->once())->method('close'); $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); - $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection)); + $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); - $requestData = new Request('GET', 'http://www.example.com', array(), '', '1.1'); + $requestData = new Request('GET', 'http://www.example.com', [], '', '1.1'); $request = new ClientRequestStream($connectionManager, $requestData); $request->on('close', $this->expectCallableOnce()); @@ -590,9 +580,9 @@ public function testStreamShouldNotReuseConnectionWhenRequestContainsConnectionC $connection->expects($this->once())->method('close'); $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); - $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection)); + $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); - $requestData = new Request('GET', 'http://www.example.com', array('Connection' => 'FOO, CLOSE, BAR'), '', '1.1'); + $requestData = new Request('GET', 'http://www.example.com', ['Connection' => 'FOO, CLOSE, BAR'], '', '1.1'); $request = new ClientRequestStream($connectionManager, $requestData); $request->on('close', $this->expectCallableOnce()); @@ -610,9 +600,9 @@ public function testStreamShouldNotReuseConnectionForHttp10ByDefault() $connection->expects($this->once())->method('close'); $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); - $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection)); + $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); - $requestData = new Request('GET', 'http://www.example.com', array(), '', '1.0'); + $requestData = new Request('GET', 'http://www.example.com', [], '', '1.0'); $request = new ClientRequestStream($connectionManager, $requestData); $request->on('close', $this->expectCallableOnce()); @@ -630,10 +620,10 @@ public function testStreamShouldReuseConnectionForHttp10WhenBothRequestAndRespon $connection->expects($this->never())->method('close'); $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); - $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection)); + $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); $connectionManager->expects($this->once())->method('keepAlive')->with(new Uri('http://www.example.com'), $connection); - $requestData = new Request('GET', 'http://www.example.com', array('Connection' => 'keep-alive'), '', '1.0'); + $requestData = new Request('GET', 'http://www.example.com', ['Connection' => 'keep-alive'], '', '1.0'); $request = new ClientRequestStream($connectionManager, $requestData); $request->on('close', $this->expectCallableOnce()); @@ -651,10 +641,10 @@ public function testStreamShouldReuseConnectionForHttp10WhenBothRequestAndRespon $connection->expects($this->never())->method('close'); $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); - $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection)); + $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); $connectionManager->expects($this->once())->method('keepAlive')->with(new Uri('http://www.example.com'), $connection); - $requestData = new Request('GET', 'http://www.example.com', array('Connection' => 'FOO, KEEP-ALIVE, BAR'), '', '1.0'); + $requestData = new Request('GET', 'http://www.example.com', ['Connection' => 'FOO, KEEP-ALIVE, BAR'], '', '1.0'); $request = new ClientRequestStream($connectionManager, $requestData); $request->on('close', $this->expectCallableOnce()); @@ -684,9 +674,9 @@ public function testStreamShouldNotReuseConnectionWhenResponseContainsNoContentL })); $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); - $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection)); + $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); - $requestData = new Request('GET', 'http://www.example.com', array(), '', '1.1'); + $requestData = new Request('GET', 'http://www.example.com', [], '', '1.1'); $request = new ClientRequestStream($connectionManager, $requestData); $request->on('close', $this->expectCallableOnce()); @@ -719,9 +709,9 @@ public function testStreamShouldNotReuseConnectionWhenResponseContainsContentLen })); $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); - $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection)); + $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); - $requestData = new Request('GET', 'http://www.example.com', array(), '', '1.1'); + $requestData = new Request('GET', 'http://www.example.com', [], '', '1.1'); $request = new ClientRequestStream($connectionManager, $requestData); $request->on('close', $this->expectCallableOnce()); @@ -742,10 +732,10 @@ public function testStreamShouldReuseConnectionWhenResponseContainsTransferEncod $connection->expects($this->never())->method('close'); $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); - $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection)); + $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); $connectionManager->expects($this->once())->method('keepAlive')->with(new Uri('http://www.example.com'), $connection); - $requestData = new Request('GET', 'http://www.example.com', array(), '', '1.1'); + $requestData = new Request('GET', 'http://www.example.com', [], '', '1.1'); $request = new ClientRequestStream($connectionManager, $requestData); $request->on('close', $this->expectCallableOnce()); @@ -763,9 +753,9 @@ public function testStreamShouldNotReuseConnectionWhenResponseContainsTransferEn $connection->expects($this->once())->method('close'); $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); - $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection)); + $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); - $requestData = new Request('GET', 'http://www.example.com', array(), '', '1.1'); + $requestData = new Request('GET', 'http://www.example.com', [], '', '1.1'); $request = new ClientRequestStream($connectionManager, $requestData); $request->on('close', $this->expectCallableOnce()); @@ -782,9 +772,9 @@ public function postRequestShouldSendAPostRequest() $connection->expects($this->once())->method('write')->with($this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\n\r\nsome post data$#")); $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); - $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection)); + $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); - $requestData = new Request('POST', 'http://www.example.com', array(), '', '1.0'); + $requestData = new Request('POST', 'http://www.example.com', [], '', '1.0'); $request = new ClientRequestStream($connectionManager, $requestData); $request->end('some post data'); @@ -799,15 +789,15 @@ public function writeWithAPostRequestShouldSendToTheStream() { $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); $connection->expects($this->exactly(3))->method('write')->withConsecutive( - array($this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\n\r\nsome$#")), - array($this->identicalTo("post")), - array($this->identicalTo("data")) + [$this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\n\r\nsome$#")], + [$this->identicalTo("post")], + [$this->identicalTo("data")] ); $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); - $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection)); + $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); - $requestData = new Request('POST', 'http://www.example.com', array(), '', '1.0'); + $requestData = new Request('POST', 'http://www.example.com', [], '', '1.0'); $request = new ClientRequestStream($connectionManager, $requestData); $request->write("some"); @@ -824,8 +814,8 @@ public function writeWithAPostRequestShouldSendBodyAfterHeadersAndEmitDrainEvent { $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); $connection->expects($this->exactly(2))->method('write')->withConsecutive( - array($this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\n\r\nsomepost$#")), - array($this->identicalTo("data")) + [$this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\n\r\nsomepost$#")], + [$this->identicalTo("data")] )->willReturn( true ); @@ -834,7 +824,7 @@ public function writeWithAPostRequestShouldSendBodyAfterHeadersAndEmitDrainEvent $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); $connectionManager->expects($this->once())->method('connect')->willReturn($deferred->promise()); - $requestData = new Request('POST', 'http://www.example.com', array(), '', '1.0'); + $requestData = new Request('POST', 'http://www.example.com', [], '', '1.0'); $request = new ClientRequestStream($connectionManager, $requestData); $this->assertFalse($request->write("some")); @@ -858,12 +848,12 @@ public function writeWithAPostRequestShouldForwardDrainEventIfFirstChunkExceedsB { $connection = $this->getMockBuilder('React\Socket\Connection') ->disableOriginalConstructor() - ->setMethods(array('write')) + ->setMethods(['write']) ->getMock(); $connection->expects($this->exactly(2))->method('write')->withConsecutive( - array($this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\n\r\nsomepost$#")), - array($this->identicalTo("data")) + [$this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\n\r\nsomepost$#")], + [$this->identicalTo("data")] )->willReturn( false ); @@ -872,7 +862,7 @@ public function writeWithAPostRequestShouldForwardDrainEventIfFirstChunkExceedsB $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); $connectionManager->expects($this->once())->method('connect')->willReturn($deferred->promise()); - $requestData = new Request('POST', 'http://www.example.com', array(), '', '1.0'); + $requestData = new Request('POST', 'http://www.example.com', [], '', '1.0'); $request = new ClientRequestStream($connectionManager, $requestData); $this->assertFalse($request->write("some")); @@ -897,15 +887,15 @@ public function pipeShouldPipeDataIntoTheRequestBody() { $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); $connection->expects($this->exactly(3))->method('write')->withConsecutive( - array($this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\n\r\nsome$#")), - array($this->identicalTo("post")), - array($this->identicalTo("data")) + [$this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\n\r\nsome$#")], + [$this->identicalTo("post")], + [$this->identicalTo("data")] ); $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); - $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection)); + $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); - $requestData = new Request('POST', 'http://www.example.com', array(), '', '1.0'); + $requestData = new Request('POST', 'http://www.example.com', [], '', '1.0'); $request = new ClientRequestStream($connectionManager, $requestData); $loop = $this @@ -916,9 +906,9 @@ public function pipeShouldPipeDataIntoTheRequestBody() $stream = new DuplexResourceStream($stream, $loop); $stream->pipe($request); - $stream->emit('data', array('some')); - $stream->emit('data', array('post')); - $stream->emit('data', array('data')); + $stream->emit('data', ['some']); + $stream->emit('data', ['post']); + $stream->emit('data', ['data']); $request->handleData("HTTP/1.0 200 OK\r\n"); $request->handleData("Content-Type: text/plain\r\n"); @@ -1044,7 +1034,7 @@ public function multivalueHeader() $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); - $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection)); + $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); $requestData = new Request('GET', 'http://www.example.com'); $request = new ClientRequestStream($connectionManager, $requestData); diff --git a/tests/Io/CloseProtectionStreamTest.php b/tests/Io/CloseProtectionStreamTest.php index 8490daff..4f3d35ca 100644 --- a/tests/Io/CloseProtectionStreamTest.php +++ b/tests/Io/CloseProtectionStreamTest.php @@ -27,7 +27,7 @@ public function testErrorWontCloseStream() $protection->on('error', $this->expectCallableOnce()); $protection->on('close', $this->expectCallableNever()); - $input->emit('error', array(new \RuntimeException())); + $input->emit('error', [new \RuntimeException()]); $this->assertTrue($protection->isReadable()); $this->assertTrue($input->isReadable()); @@ -91,7 +91,7 @@ public function testStopEmittingDataAfterClose() $protection->close(); - $input->emit('data', array('hello')); + $input->emit('data', ['hello']); $this->assertFalse($protection->isReadable()); $this->assertTrue($input->isReadable()); @@ -108,7 +108,7 @@ public function testErrorIsNeverCalledAfterClose() $protection->close(); - $input->emit('error', array(new \Exception())); + $input->emit('error', [new \Exception()]); $this->assertFalse($protection->isReadable()); $this->assertTrue($input->isReadable()); @@ -124,7 +124,7 @@ public function testEndWontBeEmittedAfterClose() $protection->close(); - $input->emit('end', array()); + $input->emit('end', []); $this->assertFalse($protection->isReadable()); $this->assertTrue($input->isReadable()); diff --git a/tests/Io/EmptyBodyStreamTest.php b/tests/Io/EmptyBodyStreamTest.php index 8430239d..3633ff81 100644 --- a/tests/Io/EmptyBodyStreamTest.php +++ b/tests/Io/EmptyBodyStreamTest.php @@ -65,7 +65,7 @@ public function testCloseTwiceEmitsCloseEventAndClearsListeners() $this->bodyStream->close(); $this->bodyStream->close(); - $this->assertEquals(array(), $this->bodyStream->listeners('close')); + $this->assertEquals([], $this->bodyStream->listeners('close')); } public function testTell() @@ -104,7 +104,7 @@ public function testGetContentsReturnsEmpy() public function testGetMetaDataWithoutKeyReturnsEmptyArray() { - $this->assertSame(array(), $this->bodyStream->getMetadata()); + $this->assertSame([], $this->bodyStream->getMetadata()); } public function testGetMetaDataWithKeyReturnsNull() diff --git a/tests/Io/HttpBodyStreamTest.php b/tests/Io/HttpBodyStreamTest.php index db21dcf8..1fd269b1 100644 --- a/tests/Io/HttpBodyStreamTest.php +++ b/tests/Io/HttpBodyStreamTest.php @@ -22,8 +22,8 @@ public function setUpBodyStream() public function testDataEmit() { - $this->bodyStream->on('data', $this->expectCallableOnce(array("hello"))); - $this->input->emit('data', array("hello")); + $this->bodyStream->on('data', $this->expectCallableOnce(["hello"])); + $this->input->emit('data', ["hello"]); } public function testPauseStream() @@ -58,7 +58,7 @@ public function testHandleClose() $this->bodyStream->on('close', $this->expectCallableOnce()); $this->input->close(); - $this->input->emit('end', array()); + $this->input->emit('end', []); $this->assertFalse($this->bodyStream->isReadable()); } @@ -67,11 +67,11 @@ public function testStopDataEmittingAfterClose() { $bodyStream = new HttpBodyStream($this->input, null); $bodyStream->on('close', $this->expectCallableOnce()); - $this->bodyStream->on('data', $this->expectCallableOnce(array("hello"))); + $this->bodyStream->on('data', $this->expectCallableOnce(["hello"])); - $this->input->emit('data', array("hello")); + $this->input->emit('data', ["hello"]); $bodyStream->close(); - $this->input->emit('data', array("world")); + $this->input->emit('data', ["world"]); } public function testHandleError() @@ -79,7 +79,7 @@ public function testHandleError() $this->bodyStream->on('error', $this->expectCallableOnce()); $this->bodyStream->on('close', $this->expectCallableOnce()); - $this->input->emit('error', array(new \RuntimeException())); + $this->input->emit('error', [new \RuntimeException()]); $this->assertFalse($this->bodyStream->isReadable()); } diff --git a/tests/Io/IniUtilTest.php b/tests/Io/IniUtilTest.php index 155c6ed2..0bc9a249 100644 --- a/tests/Io/IniUtilTest.php +++ b/tests/Io/IniUtilTest.php @@ -9,40 +9,40 @@ class IniUtilTest extends TestCase { public function provideIniSizes() { - return array( - array( + return [ + [ '1', 1, - ), - array( + ], + [ '10', 10, - ), - array( + ], + [ '1024', 1024, - ), - array( + ], + [ '1K', 1024, - ), - array( + ], + [ '1.5M', 1572864, - ), - array( + ], + [ '64M', 67108864, - ), - array( + ], + [ '8G', 8589934592, - ), - array( + ], + [ '1T', 1099511627776, - ), - ); + ], + ]; } /** @@ -60,14 +60,14 @@ public function testIniSizeToBytesWithInvalidSuffixReturnsNumberWithoutSuffix() public function provideInvalidInputIniSizeToBytes() { - return array( - array('-1G'), - array('0G'), - array('foo'), - array('fooK'), - array('1ooL'), - array('1ooL'), - ); + return [ + ['-1G'], + ['0G'], + ['foo'], + ['fooK'], + ['1ooL'], + ['1ooL'], + ]; } /** diff --git a/tests/Io/LengthLimitedStreamTest.php b/tests/Io/LengthLimitedStreamTest.php index b415269c..f1761a0b 100644 --- a/tests/Io/LengthLimitedStreamTest.php +++ b/tests/Io/LengthLimitedStreamTest.php @@ -24,7 +24,7 @@ public function testSimpleChunk() $stream = new LengthLimitedStream($this->input, 5); $stream->on('data', $this->expectCallableOnceWith('hello')); $stream->on('end', $this->expectCallableOnce()); - $this->input->emit('data', array("hello world")); + $this->input->emit('data', ["hello world"]); } public function testInputStreamKeepsEmitting() @@ -33,9 +33,9 @@ public function testInputStreamKeepsEmitting() $stream->on('data', $this->expectCallableOnceWith('hello')); $stream->on('end', $this->expectCallableOnce()); - $this->input->emit('data', array("hello world")); - $this->input->emit('data', array("world")); - $this->input->emit('data', array("world")); + $this->input->emit('data', ["hello world"]); + $this->input->emit('data', ["world"]); + $this->input->emit('data', ["world"]); } public function testZeroLengthInContentLengthWillIgnoreEmittedDataEvents() @@ -43,7 +43,7 @@ public function testZeroLengthInContentLengthWillIgnoreEmittedDataEvents() $stream = new LengthLimitedStream($this->input, 0); $stream->on('data', $this->expectCallableNever()); $stream->on('end', $this->expectCallableOnce()); - $this->input->emit('data', array("hello world")); + $this->input->emit('data', ["hello world"]); } public function testHandleError() @@ -52,7 +52,7 @@ public function testHandleError() $stream->on('error', $this->expectCallableOnce()); $stream->on('close', $this->expectCallableOnce()); - $this->input->emit('error', array(new \RuntimeException())); + $this->input->emit('error', [new \RuntimeException()]); $this->assertFalse($stream->isReadable()); } @@ -92,7 +92,7 @@ public function testHandleClose() $stream->on('close', $this->expectCallableOnce()); $this->input->close(); - $this->input->emit('end', array()); + $this->input->emit('end', []); $this->assertFalse($stream->isReadable()); } diff --git a/tests/Io/MiddlewareRunnerTest.php b/tests/Io/MiddlewareRunnerTest.php index e742ef6d..e46039bf 100644 --- a/tests/Io/MiddlewareRunnerTest.php +++ b/tests/Io/MiddlewareRunnerTest.php @@ -8,17 +8,19 @@ use React\Http\Io\MiddlewareRunner; use React\Http\Message\Response; use React\Http\Message\ServerRequest; -use React\Promise; +use React\Promise\Promise; use React\Promise\PromiseInterface; use React\Tests\Http\Middleware\ProcessStack; use React\Tests\Http\TestCase; +use function React\Async\await; +use function React\Promise\reject; final class MiddlewareRunnerTest extends TestCase { public function testEmptyMiddlewareStackThrowsException() { $request = new ServerRequest('GET', 'https://example.com/'); - $middlewares = array(); + $middlewares = []; $middlewareStack = new MiddlewareRunner($middlewares); $this->setExpectedException('RuntimeException', 'No middleware to run'); @@ -28,7 +30,7 @@ public function testEmptyMiddlewareStackThrowsException() public function testMiddlewareHandlerReceivesTwoArguments() { $args = null; - $middleware = new MiddlewareRunner(array( + $middleware = new MiddlewareRunner([ function (ServerRequestInterface $request, $next) use (&$args) { $args = func_num_args(); return $next($request); @@ -36,7 +38,7 @@ function (ServerRequestInterface $request, $next) use (&$args) { function (ServerRequestInterface $request) { return null; } - )); + ]); $request = new ServerRequest('GET', 'http://example.com/'); @@ -48,12 +50,12 @@ function (ServerRequestInterface $request) { public function testFinalHandlerReceivesOneArgument() { $args = null; - $middleware = new MiddlewareRunner(array( + $middleware = new MiddlewareRunner([ function (ServerRequestInterface $request) use (&$args) { $args = func_num_args(); return null; } - )); + ]); $request = new ServerRequest('GET', 'http://example.com/'); @@ -64,11 +66,11 @@ function (ServerRequestInterface $request) use (&$args) { public function testThrowsIfHandlerThrowsException() { - $middleware = new MiddlewareRunner(array( + $middleware = new MiddlewareRunner([ function (ServerRequestInterface $request) { throw new \RuntimeException('hello'); } - )); + ]); $request = new ServerRequest('GET', 'http://example.com/'); @@ -78,11 +80,11 @@ function (ServerRequestInterface $request) { public function testThrowsIfHandlerThrowsThrowable() { - $middleware = new MiddlewareRunner(array( + $middleware = new MiddlewareRunner([ function (ServerRequestInterface $request) { throw new \Error('hello'); } - )); + ]); $request = new ServerRequest('GET', 'http://example.com/'); @@ -99,42 +101,42 @@ public function provideProcessStackMiddlewares() $responseMiddleware = function () { return new Response(200); }; - return array( - array( - array( + return [ + [ + [ $processStackA, $responseMiddleware, - ), + ], 1, - ), - array( - array( + ], + [ + [ $processStackB, $processStackB, $responseMiddleware, - ), + ], 2, - ), - array( - array( + ], + [ + [ $processStackC, $processStackC, $processStackC, $responseMiddleware, - ), + ], 3, - ), - array( - array( + ], + [ + [ $processStackD, $processStackD, $processStackD, $processStackD, $responseMiddleware, - ), + ], 4, - ), - ); + ], + ]; } /** @@ -156,7 +158,7 @@ public function testProcessStack(array $middlewares, $expectedCallCount) $response = $middlewareStack($request); $this->assertTrue($response instanceof PromiseInterface); - $response = \React\Async\await($response); + $response = await($response); $this->assertTrue($response instanceof ResponseInterface); $this->assertSame(200, $response->getStatusCode()); @@ -172,18 +174,18 @@ public function testProcessStack(array $middlewares, $expectedCallCount) public function provideErrorHandler() { - return array( - array( + return [ + [ function (\Exception $e) { throw $e; } - ), - array( + ], + [ function (\Exception $e) { - return Promise\reject($e); + return reject($e); } - ) - ); + ] + ]; } /** @@ -195,7 +197,7 @@ public function testNextCanBeRunMoreThanOnceWithoutCorruptingTheMiddlewareStack( $retryCalled = 0; $error = null; $retry = function ($request, $next) use (&$error, &$retryCalled) { - $promise = new \React\Promise\Promise(function ($resolve) use ($request, $next) { + $promise = new Promise(function ($resolve) use ($request, $next) { $resolve($next($request)); }); @@ -209,7 +211,7 @@ public function testNextCanBeRunMoreThanOnceWithoutCorruptingTheMiddlewareStack( $response = new Response(); $called = 0; - $runner = new MiddlewareRunner(array( + $runner = new MiddlewareRunner([ $retry, function () use ($errorHandler, &$called, $response, $exception) { $called++; @@ -219,11 +221,11 @@ function () use ($errorHandler, &$called, $response, $exception) { return $response; } - )); + ]); $request = new ServerRequest('GET', 'https://example.com/'); - $this->assertSame($response, \React\Async\await($runner($request))); + $this->assertSame($response, await($runner($request))); $this->assertSame(1, $retryCalled); $this->assertSame(2, $called); $this->assertSame($exception, $error); @@ -231,15 +233,15 @@ function () use ($errorHandler, &$called, $response, $exception) { public function testMultipleRunsInvokeAllMiddlewareInCorrectOrder() { - $requests = array( + $requests = [ new ServerRequest('GET', 'https://example.com/1'), new ServerRequest('GET', 'https://example.com/2'), new ServerRequest('GET', 'https://example.com/3') - ); + ]; - $receivedRequests = array(); + $receivedRequests = []; - $middlewareRunner = new MiddlewareRunner(array( + $middlewareRunner = new MiddlewareRunner([ function (ServerRequestInterface $request, $next) use (&$receivedRequests) { $receivedRequests[] = 'middleware1: ' . $request->getUri(); return $next($request); @@ -250,16 +252,16 @@ function (ServerRequestInterface $request, $next) use (&$receivedRequests) { }, function (ServerRequestInterface $request) use (&$receivedRequests) { $receivedRequests[] = 'middleware3: ' . $request->getUri(); - return new \React\Promise\Promise(function () { }); + return new Promise(function () { }); } - )); + ]); foreach ($requests as $request) { $middlewareRunner($request); } $this->assertEquals( - array( + [ 'middleware1: https://example.com/1', 'middleware2: https://example.com/1', 'middleware3: https://example.com/1', @@ -269,20 +271,20 @@ function (ServerRequestInterface $request) use (&$receivedRequests) { 'middleware1: https://example.com/3', 'middleware2: https://example.com/3', 'middleware3: https://example.com/3' - ), + ], $receivedRequests ); } public function provideUncommonMiddlewareArrayFormats() { - return array( - array( + return [ + [ function () { $sequence = ''; // Numeric index gap - return array( + return [ 0 => function (ServerRequestInterface $request, $next) use (&$sequence) { $sequence .= 'A'; @@ -294,18 +296,18 @@ function () { return $next($request); }, 3 => function () use (&$sequence) { - return new Response(200, array(), $sequence . 'C'); + return new Response(200, [], $sequence . 'C'); }, - ); + ]; }, 'ABC', - ), - array( + ], + [ function () { $sequence = ''; // Reversed numeric indexes - return array( + return [ 2 => function (ServerRequestInterface $request, $next) use (&$sequence) { $sequence .= 'A'; @@ -317,18 +319,18 @@ function () { return $next($request); }, 0 => function () use (&$sequence) { - return new Response(200, array(), $sequence . 'C'); + return new Response(200, [], $sequence . 'C'); }, - ); + ]; }, 'ABC', - ), - array( + ], + [ function () { $sequence = ''; // Associative array - return array( + return [ 'middleware1' => function (ServerRequestInterface $request, $next) use (&$sequence) { $sequence .= 'A'; @@ -340,18 +342,18 @@ function () { return $next($request); }, 'middleware3' => function () use (&$sequence) { - return new Response(200, array(), $sequence . 'C'); + return new Response(200, [], $sequence . 'C'); }, - ); + ]; }, 'ABC', - ), - array( + ], + [ function () { $sequence = ''; // Associative array with empty or trimmable string keys - return array( + return [ '' => function (ServerRequestInterface $request, $next) use (&$sequence) { $sequence .= 'A'; @@ -363,18 +365,18 @@ function () { return $next($request); }, ' ' => function () use (&$sequence) { - return new Response(200, array(), $sequence . 'C'); + return new Response(200, [], $sequence . 'C'); }, - ); + ]; }, 'ABC', - ), - array( + ], + [ function () { $sequence = ''; // Mixed array keys - return array( + return [ '' => function (ServerRequestInterface $request, $next) use (&$sequence) { $sequence .= 'A'; @@ -391,13 +393,13 @@ function () { return $next($request); }, 2 => function () use (&$sequence) { - return new Response(200, array(), $sequence . 'D'); + return new Response(200, [], $sequence . 'D'); }, - ); + ]; }, 'ABCD', - ), - ); + ], + ]; } /** @@ -417,7 +419,7 @@ public function testUncommonMiddlewareArrayFormats($middlewareFactory, $expected public function testPendingNextRequestHandlersCanBeCalledConcurrently() { $called = 0; - $middleware = new MiddlewareRunner(array( + $middleware = new MiddlewareRunner([ function (RequestInterface $request, $next) { $first = $next($request); $second = $next($request); @@ -427,9 +429,9 @@ function (RequestInterface $request, $next) { function (RequestInterface $request) use (&$called) { ++$called; - return new Promise\Promise(function () { }); + return new Promise(function () { }); } - )); + ]); $request = new ServerRequest('GET', 'http://example.com/'); @@ -442,7 +444,7 @@ function (RequestInterface $request) use (&$called) { public function testCancelPendingNextHandler() { $once = $this->expectCallableOnce(); - $middleware = new MiddlewareRunner(array( + $middleware = new MiddlewareRunner([ function (RequestInterface $request, $next) { $ret = $next($request); $ret->cancel(); @@ -450,9 +452,9 @@ function (RequestInterface $request, $next) { return $ret; }, function (RequestInterface $request) use ($once) { - return new Promise\Promise(function () { }, $once); + return new Promise(function () { }, $once); } - )); + ]); $request = new ServerRequest('GET', 'http://example.com/'); @@ -462,14 +464,14 @@ function (RequestInterface $request) use ($once) { public function testCancelResultingPromiseWillCancelPendingNextHandler() { $once = $this->expectCallableOnce(); - $middleware = new MiddlewareRunner(array( + $middleware = new MiddlewareRunner([ function (RequestInterface $request, $next) { return $next($request); }, function (RequestInterface $request) use ($once) { - return new Promise\Promise(function () { }, $once); + return new Promise(function () { }, $once); } - )); + ]); $request = new ServerRequest('GET', 'http://example.com/'); diff --git a/tests/Io/MultipartParserTest.php b/tests/Io/MultipartParserTest.php index 7f1ec667..ba439760 100644 --- a/tests/Io/MultipartParserTest.php +++ b/tests/Io/MultipartParserTest.php @@ -22,9 +22,9 @@ public function testDoesNotParseWithoutMultipartFormDataContentType() $data .= "second\r\n"; $data .= "--$boundary--\r\n"; - $request = new ServerRequest('POST', 'http://example.com/', array( + $request = new ServerRequest('POST', 'http://example.com/', [ 'Content-Type' => 'multipart/form-data', - ), $data, 1.1); + ], $data, 1.1); $parser = new MultipartParser(); $parsedRequest = $parser->parse($request); @@ -47,21 +47,21 @@ public function testPostKey() $data .= "second\r\n"; $data .= "--$boundary--\r\n"; - $request = new ServerRequest('POST', 'http://example.com/', array( + $request = new ServerRequest('POST', 'http://example.com/', [ 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, - ), $data, 1.1); + ], $data, 1.1); $parser = new MultipartParser(); $parsedRequest = $parser->parse($request); $this->assertEmpty($parsedRequest->getUploadedFiles()); $this->assertSame( - array( - 'users' => array( + [ + 'users' => [ 'one' => 'single', 'two' => 'second', - ), - ), + ], + ], $parsedRequest->getParsedBody() ); } @@ -80,21 +80,21 @@ public function testPostWithQuotationMarkEncapsulatedBoundary() $data .= "second\r\n"; $data .= "--$boundary--\r\n"; - $request = new ServerRequest('POST', 'http://example.com/', array( + $request = new ServerRequest('POST', 'http://example.com/', [ 'Content-Type' => 'multipart/form-data; boundary="' . $boundary . '"', - ), $data, 1.1); + ], $data, 1.1); $parser = new MultipartParser(); $parsedRequest = $parser->parse($request); $this->assertEmpty($parsedRequest->getUploadedFiles()); $this->assertSame( - array( - 'users' => array( + [ + 'users' => [ 'one' => 'single', 'two' => 'second', - ), - ), + ], + ], $parsedRequest->getParsedBody() ); } @@ -113,21 +113,21 @@ public function testPostFormDataNamesWithoutQuotationMark() $data .= "second\r\n"; $data .= "--$boundary--\r\n"; - $request = new ServerRequest('POST', 'http://example.com/', array( + $request = new ServerRequest('POST', 'http://example.com/', [ 'Content-Type' => 'multipart/form-data; boundary="' . $boundary . '"', - ), $data, 1.1); + ], $data, 1.1); $parser = new MultipartParser(); $parsedRequest = $parser->parse($request); $this->assertEmpty($parsedRequest->getUploadedFiles()); $this->assertSame( - array( - 'users' => array( + [ + 'users' => [ 'one' => 'single', 'two' => 'second', - ), - ), + ], + ], $parsedRequest->getParsedBody() ); } @@ -146,18 +146,18 @@ public function testPostStringOverwritesMap() $data .= "2\r\n"; $data .= "--$boundary--\r\n"; - $request = new ServerRequest('POST', 'http://example.com/', array( + $request = new ServerRequest('POST', 'http://example.com/', [ 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, - ), $data, 1.1); + ], $data, 1.1); $parser = new MultipartParser(); $parsedRequest = $parser->parse($request); $this->assertEmpty($parsedRequest->getUploadedFiles()); $this->assertSame( - array( + [ 'users' => '2' - ), + ], $parsedRequest->getParsedBody() ); } @@ -176,20 +176,20 @@ public function testPostMapOverwritesString() $data .= "2\r\n"; $data .= "--$boundary--\r\n"; - $request = new ServerRequest('POST', 'http://example.com/', array( + $request = new ServerRequest('POST', 'http://example.com/', [ 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, - ), $data, 1.1); + ], $data, 1.1); $parser = new MultipartParser(); $parsedRequest = $parser->parse($request); $this->assertEmpty($parsedRequest->getUploadedFiles()); $this->assertSame( - array( - 'users' => array( + [ + 'users' => [ 'two' => '2', - ), - ), + ], + ], $parsedRequest->getParsedBody() ); } @@ -208,20 +208,20 @@ public function testPostVectorOverwritesString() $data .= "2\r\n"; $data .= "--$boundary--\r\n"; - $request = new ServerRequest('POST', 'http://example.com/', array( + $request = new ServerRequest('POST', 'http://example.com/', [ 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, - ), $data, 1.1); + ], $data, 1.1); $parser = new MultipartParser(); $parsedRequest = $parser->parse($request); $this->assertEmpty($parsedRequest->getUploadedFiles()); $this->assertSame( - array( - 'users' => array( + [ + 'users' => [ '2', - ), - ), + ], + ], $parsedRequest->getParsedBody() ); } @@ -240,25 +240,25 @@ public function testPostDeeplyNestedArray() $data .= "2\r\n"; $data .= "--$boundary--\r\n"; - $request = new ServerRequest('POST', 'http://example.com/', array( + $request = new ServerRequest('POST', 'http://example.com/', [ 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, - ), $data, 1.1); + ], $data, 1.1); $parser = new MultipartParser(); $parsedRequest = $parser->parse($request); $this->assertEmpty($parsedRequest->getUploadedFiles()); $this->assertSame( - array( - 'users' => array( - array( + [ + 'users' => [ + [ '1' - ), - array( + ], + [ '2' - ) - ), - ), + ] + ], + ], $parsedRequest->getParsedBody() ); } @@ -273,18 +273,18 @@ public function testEmptyPostValue() $data .= "\r\n"; $data .= "--$boundary--\r\n"; - $request = new ServerRequest('POST', 'http://example.com/', array( + $request = new ServerRequest('POST', 'http://example.com/', [ 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, - ), $data, 1.1); + ], $data, 1.1); $parser = new MultipartParser(); $parsedRequest = $parser->parse($request); $this->assertEmpty($parsedRequest->getUploadedFiles()); $this->assertSame( - array( + [ 'key' => '' - ), + ], $parsedRequest->getParsedBody() ); } @@ -299,18 +299,18 @@ public function testEmptyPostKey() $data .= "value\r\n"; $data .= "--$boundary--\r\n"; - $request = new ServerRequest('POST', 'http://example.com/', array( + $request = new ServerRequest('POST', 'http://example.com/', [ 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, - ), $data, 1.1); + ], $data, 1.1); $parser = new MultipartParser(); $parsedRequest = $parser->parse($request); $this->assertEmpty($parsedRequest->getUploadedFiles()); $this->assertSame( - array( + [ '' => 'value' - ), + ], $parsedRequest->getParsedBody() ); } @@ -325,22 +325,22 @@ public function testNestedPostKeyAssoc() $data .= "value\r\n"; $data .= "--$boundary--\r\n"; - $request = new ServerRequest('POST', 'http://example.com/', array( + $request = new ServerRequest('POST', 'http://example.com/', [ 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, - ), $data, 1.1); + ], $data, 1.1); $parser = new MultipartParser(); $parsedRequest = $parser->parse($request); $this->assertEmpty($parsedRequest->getUploadedFiles()); $this->assertSame( - array( - 'a' => array( - 'b' => array( + [ + 'a' => [ + 'b' => [ 'c' => 'value' - ) - ) - ), + ] + ] + ], $parsedRequest->getParsedBody() ); } @@ -355,22 +355,22 @@ public function testNestedPostKeyVector() $data .= "value\r\n"; $data .= "--$boundary--\r\n"; - $request = new ServerRequest('POST', 'http://example.com/', array( + $request = new ServerRequest('POST', 'http://example.com/', [ 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, - ), $data, 1.1); + ], $data, 1.1); $parser = new MultipartParser(); $parsedRequest = $parser->parse($request); $this->assertEmpty($parsedRequest->getUploadedFiles()); $this->assertSame( - array( - 'a' => array( - array( + [ + 'a' => [ + [ 'value' - ) - ) - ), + ] + ] + ], $parsedRequest->getParsedBody() ); } @@ -436,25 +436,25 @@ public function testFileUpload() $data .= "\r\n"; $data .= "--$boundary--\r\n"; - $request = new ServerRequest('POST', 'http://example.com/', array( + $request = new ServerRequest('POST', 'http://example.com/', [ 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, - ), $data, 1.1); + ], $data, 1.1); $parser = new MultipartParser(); $parsedRequest = $parser->parse($request); $this->assertSame( - array( + [ 'MAX_FILE_SIZE' => '12000', - 'users' => array( + 'users' => [ 'one' => 'single', 'two' => 'second', 0 => 'first in array', 1 => 'second in array', - ), + ], 'user' => 'single', 'user2' => 'second', - ), + ], $parsedRequest->getParsedBody() ); @@ -491,18 +491,18 @@ public function testInvalidDoubleContentDispositionUsesLast() $data .= "value\r\n"; $data .= "--$boundary--\r\n"; - $request = new ServerRequest('POST', 'http://example.com/', array( + $request = new ServerRequest('POST', 'http://example.com/', [ 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, - ), $data, 1.1); + ], $data, 1.1); $parser = new MultipartParser(); $parsedRequest = $parser->parse($request); $this->assertEmpty($parsedRequest->getUploadedFiles()); $this->assertSame( - array( + [ 'key' => 'value' - ), + ], $parsedRequest->getParsedBody() ); } @@ -517,9 +517,9 @@ public function testInvalidMissingNewlineAfterValueWillBeIgnored() $data .= "value"; $data .= "--$boundary--\r\n"; - $request = new ServerRequest('POST', 'http://example.com/', array( + $request = new ServerRequest('POST', 'http://example.com/', [ 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, - ), $data, 1.1); + ], $data, 1.1); $parser = new MultipartParser(); $parsedRequest = $parser->parse($request); @@ -537,9 +537,9 @@ public function testInvalidMissingValueWillBeIgnored() $data .= "\r\n"; $data .= "--$boundary--\r\n"; - $request = new ServerRequest('POST', 'http://example.com/', array( + $request = new ServerRequest('POST', 'http://example.com/', [ 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, - ), $data, 1.1); + ], $data, 1.1); $parser = new MultipartParser(); $parsedRequest = $parser->parse($request); @@ -555,9 +555,9 @@ public function testInvalidMissingValueAndEndBoundaryWillBeIgnored() $data = "--$boundary\r\n"; $data .= "Content-Disposition: form-data; name=\"key\"\r\n"; - $request = new ServerRequest('POST', 'http://example.com/', array( + $request = new ServerRequest('POST', 'http://example.com/', [ 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, - ), $data, 1.1); + ], $data, 1.1); $parser = new MultipartParser(); $parsedRequest = $parser->parse($request); @@ -576,9 +576,9 @@ public function testInvalidContentDispositionMissingWillBeIgnored() $data .= "hello\r\n"; $data .= "--$boundary--\r\n"; - $request = new ServerRequest('POST', 'http://example.com/', array( + $request = new ServerRequest('POST', 'http://example.com/', [ 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, - ), $data, 1.1); + ], $data, 1.1); $parser = new MultipartParser(); $parsedRequest = $parser->parse($request); @@ -597,9 +597,9 @@ public function testInvalidContentDispositionMissingValueWillBeIgnored() $data .= "value\r\n"; $data .= "--$boundary--\r\n"; - $request = new ServerRequest('POST', 'http://example.com/', array( + $request = new ServerRequest('POST', 'http://example.com/', [ 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, - ), $data, 1.1); + ], $data, 1.1); $parser = new MultipartParser(); $parsedRequest = $parser->parse($request); @@ -618,9 +618,9 @@ public function testInvalidContentDispositionWithoutNameWillBeIgnored() $data .= "value\r\n"; $data .= "--$boundary--\r\n"; - $request = new ServerRequest('POST', 'http://example.com/', array( + $request = new ServerRequest('POST', 'http://example.com/', [ 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, - ), $data, 1.1); + ], $data, 1.1); $parser = new MultipartParser(); $parsedRequest = $parser->parse($request); @@ -638,9 +638,9 @@ public function testInvalidMissingEndBoundaryWillBeIgnored() $data .= "\r\n"; $data .= "value\r\n"; - $request = new ServerRequest('POST', 'http://example.com/', array( + $request = new ServerRequest('POST', 'http://example.com/', [ 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, - ), $data, 1.1); + ], $data, 1.1); $parser = new MultipartParser(); $parsedRequest = $parser->parse($request); @@ -662,9 +662,9 @@ public function testInvalidUploadFileWithoutContentTypeUsesNullValue() $data .= "world\r\n"; $data .= "--$boundary--\r\n"; - $request = new ServerRequest('POST', 'http://example.com/', array( + $request = new ServerRequest('POST', 'http://example.com/', [ 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, - ), $data, 1.1); + ], $data, 1.1); $parser = new MultipartParser(); $parsedRequest = $parser->parse($request); @@ -697,9 +697,9 @@ public function testInvalidUploadFileWithoutMultipleContentTypeUsesLastValue() $data .= "world\r\n"; $data .= "--$boundary--\r\n"; - $request = new ServerRequest('POST', 'http://example.com/', array( + $request = new ServerRequest('POST', 'http://example.com/', [ 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, - ), $data, 1.1); + ], $data, 1.1); $parser = new MultipartParser(); $parsedRequest = $parser->parse($request); @@ -731,9 +731,9 @@ public function testUploadEmptyFile() $data .= "\r\n"; $data .= "--$boundary--\r\n"; - $request = new ServerRequest('POST', 'http://example.com/', array( + $request = new ServerRequest('POST', 'http://example.com/', [ 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, - ), $data, 1.1); + ], $data, 1.1); $parser = new MultipartParser(); $parsedRequest = $parser->parse($request); @@ -765,9 +765,9 @@ public function testUploadTooLargeFile() $data .= "world\r\n"; $data .= "--$boundary--\r\n"; - $request = new ServerRequest('POST', 'http://example.com/', array( + $request = new ServerRequest('POST', 'http://example.com/', [ 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, - ), $data, 1.1); + ], $data, 1.1); $parser = new MultipartParser(4); $parsedRequest = $parser->parse($request); @@ -798,9 +798,9 @@ public function testUploadTooLargeFileWithIniLikeSize() $data .= str_repeat('world', 1024) . "\r\n"; $data .= "--$boundary--\r\n"; - $request = new ServerRequest('POST', 'http://example.com/', array( + $request = new ServerRequest('POST', 'http://example.com/', [ 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, - ), $data, 1.1); + ], $data, 1.1); $parser = new MultipartParser('1K'); $parsedRequest = $parser->parse($request); @@ -831,9 +831,9 @@ public function testUploadNoFile() $data .= "\r\n"; $data .= "--$boundary--\r\n"; - $request = new ServerRequest('POST', 'http://example.com/', array( + $request = new ServerRequest('POST', 'http://example.com/', [ 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, - ), $data, 1.1); + ], $data, 1.1); $parser = new MultipartParser(); $parsedRequest = $parser->parse($request); @@ -869,9 +869,9 @@ public function testUploadTooManyFilesReturnsTruncatedList() $data .= "world\r\n"; $data .= "--$boundary--\r\n"; - $request = new ServerRequest('POST', 'http://example.com/', array( + $request = new ServerRequest('POST', 'http://example.com/', [ 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, - ), $data, 1.1); + ], $data, 1.1); $parser = new MultipartParser(100, 1); $parsedRequest = $parser->parse($request); @@ -910,9 +910,9 @@ public function testUploadTooManyFilesIgnoresEmptyFilesAndIncludesThemDespiteTru $data .= "world\r\n"; $data .= "--$boundary--\r\n"; - $request = new ServerRequest('POST', 'http://example.com/', array( + $request = new ServerRequest('POST', 'http://example.com/', [ 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, - ), $data, 1.1); + ], $data, 1.1); $parser = new MultipartParser(100, 1); $parsedRequest = $parser->parse($request); @@ -953,9 +953,9 @@ public function testPostMaxFileSize() $data .= "\r\n"; $data .= "--$boundary--\r\n"; - $request = new ServerRequest('POST', 'http://example.com/', array( + $request = new ServerRequest('POST', 'http://example.com/', [ 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, - ), $data, 1.1); + ], $data, 1.1); $parser = new MultipartParser(); $parsedRequest = $parser->parse($request); @@ -1008,9 +1008,9 @@ public function testPostMaxFileSizeIgnoredByFilesComingBeforeIt() $data .= "\r\n"; $data .= "--$boundary--\r\n"; - $request = new ServerRequest('POST', 'http://example.com/', array( + $request = new ServerRequest('POST', 'http://example.com/', [ 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, - ), $data, 1.1); + ], $data, 1.1); $parser = new MultipartParser(); $parsedRequest = $parser->parse($request); @@ -1040,9 +1040,9 @@ public function testWeOnlyParseTheAmountOfMultiPartChunksWeConfigured() $data .= str_repeat($chunk, $chunkCount); $data .= "--$boundary--\r\n"; - $request = new ServerRequest('POST', 'http://example.com/', array( + $request = new ServerRequest('POST', 'http://example.com/', [ 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, - ), $data, 1.1); + ], $data, 1.1); $parser = new MultipartParser(); diff --git a/tests/Io/PauseBufferStreamTest.php b/tests/Io/PauseBufferStreamTest.php index a9678a78..05bf3ee3 100644 --- a/tests/Io/PauseBufferStreamTest.php +++ b/tests/Io/PauseBufferStreamTest.php @@ -156,7 +156,7 @@ public function testErrorEventWillBePassedThroughAsIs() $stream->on('error', $this->expectCallableOnce()); $stream->on('close', $this->expectCallableOnce()); - $input->emit('error', array(new \RuntimeException())); + $input->emit('error', [new \RuntimeException()]); } public function testPausedStreamWillNotPassThroughErrorEvent() @@ -167,7 +167,7 @@ public function testPausedStreamWillNotPassThroughErrorEvent() $stream->pause(); $stream->on('error', $this->expectCallableNever()); $stream->on('close', $this->expectCallableNever()); - $input->emit('error', array(new \RuntimeException())); + $input->emit('error', [new \RuntimeException()]); } public function testPausedStreamWillPassThroughErrorEventOnResume() @@ -176,7 +176,7 @@ public function testPausedStreamWillPassThroughErrorEventOnResume() $stream = new PauseBufferStream($input); $stream->pause(); - $input->emit('error', array(new \RuntimeException())); + $input->emit('error', [new \RuntimeException()]); $stream->on('error', $this->expectCallableOnce()); $stream->on('close', $this->expectCallableOnce()); @@ -191,7 +191,7 @@ public function testPausedStreamWillNotPassThroughErrorEventOnExplicitClose() $stream->pause(); $stream->on('error', $this->expectCallableNever()); $stream->on('close', $this->expectCallableOnce()); - $input->emit('error', array(new \RuntimeException())); + $input->emit('error', [new \RuntimeException()]); $stream->close(); } diff --git a/tests/Io/ReadableBodyStreamTest.php b/tests/Io/ReadableBodyStreamTest.php index d89199ff..8ece6791 100644 --- a/tests/Io/ReadableBodyStreamTest.php +++ b/tests/Io/ReadableBodyStreamTest.php @@ -250,6 +250,6 @@ public function testPointlessGetMetadataReturnsNullWhenKeyIsGiven() public function testPointlessGetMetadataReturnsEmptyArrayWhenNoKeyIsGiven() { - $this->assertEquals(array(), $this->stream->getMetadata()); + $this->assertEquals([], $this->stream->getMetadata()); } } diff --git a/tests/Io/RequestHeaderParserTest.php b/tests/Io/RequestHeaderParserTest.php index 1ed994b7..d15d4e7f 100644 --- a/tests/Io/RequestHeaderParserTest.php +++ b/tests/Io/RequestHeaderParserTest.php @@ -19,14 +19,14 @@ public function testSplitShouldHappenOnDoubleCrlf() $parser->handle($connection); - $connection->emit('data', array("GET / HTTP/1.1\r\n")); - $connection->emit('data', array("Host: example.com:80\r\n")); - $connection->emit('data', array("Connection: close\r\n")); + $connection->emit('data', ["GET / HTTP/1.1\r\n"]); + $connection->emit('data', ["Host: example.com:80\r\n"]); + $connection->emit('data', ["Connection: close\r\n"]); $parser->removeAllListeners(); $parser->on('headers', $this->expectCallableOnce()); - $connection->emit('data', array("\r\n")); + $connection->emit('data', ["\r\n"]); } public function testFeedInOneGo() @@ -40,7 +40,7 @@ public function testFeedInOneGo() $parser->handle($connection); $data = $this->createGetRequest(); - $connection->emit('data', array($data)); + $connection->emit('data', [$data]); } public function testFeedTwoRequestsOnSeparateConnections() @@ -60,8 +60,8 @@ public function testFeedTwoRequestsOnSeparateConnections() $parser->handle($connection2); $data = $this->createGetRequest(); - $connection1->emit('data', array($data)); - $connection2->emit('data', array($data)); + $connection1->emit('data', [$data]); + $connection2->emit('data', [$data]); $this->assertEquals(2, $called); } @@ -83,13 +83,13 @@ public function testHeadersEventShouldEmitRequestAndConnection() $parser->handle($connection); $data = $this->createGetRequest(); - $connection->emit('data', array($data)); + $connection->emit('data', [$data]); $this->assertInstanceOf('Psr\Http\Message\RequestInterface', $request); $this->assertSame('GET', $request->getMethod()); $this->assertEquals('http://example.com/', $request->getUri()); $this->assertSame('1.1', $request->getProtocolVersion()); - $this->assertSame(array('Host' => array('example.com'), 'Connection' => array('close')), $request->getHeaders()); + $this->assertSame(['Host' => ['example.com'], 'Connection' => ['close']], $request->getHeaders()); $this->assertSame($connection, $conn); } @@ -101,10 +101,9 @@ public function testHeadersEventShouldEmitRequestWhichShouldEmitEndForStreamingB $parser = new RequestHeaderParser($clock); $ended = false; - $that = $this; - $parser->on('headers', function (ServerRequestInterface $request) use (&$ended, $that) { + $parser->on('headers', function (ServerRequestInterface $request) use (&$ended) { $body = $request->getBody(); - $that->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); + $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); $body->on('end', function () use (&$ended) { $ended = true; @@ -115,7 +114,7 @@ public function testHeadersEventShouldEmitRequestWhichShouldEmitEndForStreamingB $parser->handle($connection); $data = "GET / HTTP/1.0\r\n\r\n"; - $connection->emit('data', array($data)); + $connection->emit('data', [$data]); $this->assertTrue($ended); } @@ -127,10 +126,9 @@ public function testHeadersEventShouldEmitRequestWhichShouldEmitStreamingBodyDat $parser = new RequestHeaderParser($clock); $buffer = ''; - $that = $this; - $parser->on('headers', function (ServerRequestInterface $request) use (&$buffer, $that) { + $parser->on('headers', function (ServerRequestInterface $request) use (&$buffer) { $body = $request->getBody(); - $that->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); + $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); $body->on('data', function ($chunk) use (&$buffer) { $buffer .= $chunk; @@ -145,7 +143,7 @@ public function testHeadersEventShouldEmitRequestWhichShouldEmitStreamingBodyDat $data = "POST / HTTP/1.0\r\nContent-Length: 11\r\n\r\n"; $data .= 'RANDOM DATA'; - $connection->emit('data', array($data)); + $connection->emit('data', [$data]); $this->assertSame('RANDOM DATA.', $buffer); } @@ -157,10 +155,9 @@ public function testHeadersEventShouldEmitRequestWhichShouldEmitStreamingBodyWit $parser = new RequestHeaderParser($clock); $buffer = ''; - $that = $this; - $parser->on('headers', function (ServerRequestInterface $request) use (&$buffer, $that) { + $parser->on('headers', function (ServerRequestInterface $request) use (&$buffer) { $body = $request->getBody(); - $that->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); + $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); $body->on('data', function ($chunk) use (&$buffer) { $buffer .= $chunk; @@ -173,7 +170,7 @@ public function testHeadersEventShouldEmitRequestWhichShouldEmitStreamingBodyWit $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)); + $connection->emit('data', [$data]); $this->assertSame($size, strlen($buffer)); } @@ -185,10 +182,9 @@ public function testHeadersEventShouldEmitRequestWhichShouldNotEmitStreamingBody $parser = new RequestHeaderParser($clock); $buffer = ''; - $that = $this; - $parser->on('headers', function (ServerRequestInterface $request) use (&$buffer, $that) { + $parser->on('headers', function (ServerRequestInterface $request) use (&$buffer) { $body = $request->getBody(); - $that->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); + $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); $body->on('data', function ($chunk) use (&$buffer) { $buffer .= $chunk; @@ -200,7 +196,7 @@ public function testHeadersEventShouldEmitRequestWhichShouldNotEmitStreamingBody $data = "POST / HTTP/1.0\r\n\r\n"; $data .= 'RANDOM DATA'; - $connection->emit('data', array($data)); + $connection->emit('data', [$data]); $this->assertSame('', $buffer); } @@ -212,10 +208,9 @@ public function testHeadersEventShouldEmitRequestWhichShouldEmitStreamingBodyDat $parser = new RequestHeaderParser($clock); $buffer = ''; - $that = $this; - $parser->on('headers', function (ServerRequestInterface $request) use (&$buffer, $that) { + $parser->on('headers', function (ServerRequestInterface $request) use (&$buffer) { $body = $request->getBody(); - $that->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); + $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); $body->on('data', function ($chunk) use (&$buffer) { $buffer .= $chunk; @@ -227,7 +222,7 @@ public function testHeadersEventShouldEmitRequestWhichShouldEmitStreamingBodyDat $data = "POST / HTTP/1.0\r\nContent-Length: 6\r\n\r\n"; $data .= 'RANDOM DATA'; - $connection->emit('data', array($data)); + $connection->emit('data', [$data]); $this->assertSame('RANDOM', $buffer); } @@ -247,17 +242,17 @@ public function testHeadersEventShouldParsePathAndQueryString() $parser->handle($connection); $data = $this->createAdvancedPostRequest(); - $connection->emit('data', array($data)); + $connection->emit('data', [$data]); $this->assertInstanceOf('Psr\Http\Message\RequestInterface', $request); $this->assertSame('POST', $request->getMethod()); $this->assertEquals('http://example.com/foo?bar=baz', $request->getUri()); $this->assertSame('1.1', $request->getProtocolVersion()); - $headers = array( - 'Host' => array('example.com'), - 'User-Agent' => array('react/alpha'), - 'Connection' => array('close'), - ); + $headers = [ + 'Host' => ['example.com'], + 'User-Agent' => ['react/alpha'], + 'Connection' => ['close'] + ]; $this->assertSame($headers, $request->getHeaders()); } @@ -272,11 +267,11 @@ public function testHeaderEventWithShouldApplyDefaultAddressFromLocalConnectionA $request = $parsedRequest; }); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('getLocalAddress'))->getMock(); + $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(['getLocalAddress'])->getMock(); $connection->expects($this->once())->method('getLocalAddress')->willReturn('tcp://127.1.1.1:8000'); $parser->handle($connection); - $connection->emit('data', array("GET /foo HTTP/1.0\r\n\r\n")); + $connection->emit('data', ["GET /foo HTTP/1.0\r\n\r\n"]); $this->assertEquals('http://127.1.1.1:8000/foo', $request->getUri()); $this->assertFalse($request->hasHeader('Host')); @@ -293,11 +288,11 @@ public function testHeaderEventViaHttpsShouldApplyHttpsSchemeFromLocalTlsConnect $request = $parsedRequest; }); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('getLocalAddress'))->getMock(); + $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(['getLocalAddress'])->getMock(); $connection->expects($this->once())->method('getLocalAddress')->willReturn('tls://127.1.1.1:8000'); $parser->handle($connection); - $connection->emit('data', array("GET /foo HTTP/1.0\r\nHost: example.com\r\n\r\n")); + $connection->emit('data', ["GET /foo HTTP/1.0\r\nHost: example.com\r\n\r\n"]); $this->assertEquals('https://example.com/foo', $request->getUri()); $this->assertEquals('example.com', $request->getHeaderLine('Host')); @@ -321,7 +316,7 @@ public function testHeaderOverflowShouldEmitError() $parser->handle($connection); $data = str_repeat('A', 8193); - $connection->emit('data', array($data)); + $connection->emit('data', [$data]); $this->assertInstanceOf('OverflowException', $error); $this->assertSame('Maximum header size of 8192 exceeded.', $error->getMessage()); @@ -343,7 +338,7 @@ public function testInvalidEmptyRequestHeadersParseException() $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); - $connection->emit('data', array("\r\n\r\n")); + $connection->emit('data', ["\r\n\r\n"]); $this->assertInstanceOf('InvalidArgumentException', $error); $this->assertSame('Unable to parse invalid request-line', $error->getMessage()); @@ -364,7 +359,7 @@ public function testInvalidMalformedRequestLineParseException() $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); - $connection->emit('data', array("GET /\r\n\r\n")); + $connection->emit('data', ["GET /\r\n\r\n"]); $this->assertInstanceOf('InvalidArgumentException', $error); $this->assertSame('Unable to parse invalid request-line', $error->getMessage()); @@ -385,7 +380,7 @@ public function testInvalidMalformedRequestHeadersThrowsParseException() $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); - $connection->emit('data', array("GET / HTTP/1.1\r\nHost : yes\r\n\r\n")); + $connection->emit('data', ["GET / HTTP/1.1\r\nHost : yes\r\n\r\n"]); $this->assertInstanceOf('InvalidArgumentException', $error); $this->assertSame('Unable to parse invalid request header fields', $error->getMessage()); @@ -406,7 +401,7 @@ public function testInvalidMalformedRequestHeadersWhitespaceThrowsParseException $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); - $connection->emit('data', array("GET / HTTP/1.1\r\nHost: yes\rFoo: bar\r\n\r\n")); + $connection->emit('data', ["GET / HTTP/1.1\r\nHost: yes\rFoo: bar\r\n\r\n"]); $this->assertInstanceOf('InvalidArgumentException', $error); $this->assertSame('Unable to parse invalid request header fields', $error->getMessage()); @@ -427,7 +422,7 @@ public function testInvalidAbsoluteFormSchemeEmitsError() $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); - $connection->emit('data', array("GET tcp://example.com:80/ HTTP/1.0\r\n\r\n")); + $connection->emit('data', ["GET tcp://example.com:80/ HTTP/1.0\r\n\r\n"]); $this->assertInstanceOf('InvalidArgumentException', $error); $this->assertSame('Invalid absolute-form request-target', $error->getMessage()); @@ -448,15 +443,15 @@ public function testOriginFormWithSchemeSeparatorInParam() $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); - $connection->emit('data', array("GET /somepath?param=http://example.com HTTP/1.1\r\nHost: localhost\r\n\r\n")); + $connection->emit('data', ["GET /somepath?param=http://example.com HTTP/1.1\r\nHost: localhost\r\n\r\n"]); $this->assertInstanceOf('Psr\Http\Message\RequestInterface', $request); $this->assertSame('GET', $request->getMethod()); $this->assertEquals('http://localhost/somepath?param=http://example.com', $request->getUri()); $this->assertSame('1.1', $request->getProtocolVersion()); - $headers = array( - 'Host' => array('localhost') - ); + $headers = [ + 'Host' => ['localhost'] + ]; $this->assertSame($headers, $request->getHeaders()); } @@ -475,7 +470,7 @@ public function testUriStartingWithColonSlashSlashFails() $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); - $connection->emit('data', array("GET ://example.com:80/ HTTP/1.0\r\n\r\n")); + $connection->emit('data', ["GET ://example.com:80/ HTTP/1.0\r\n\r\n"]); $this->assertInstanceOf('InvalidArgumentException', $error); $this->assertSame('Invalid absolute-form request-target', $error->getMessage()); @@ -496,7 +491,7 @@ public function testInvalidAbsoluteFormWithFragmentEmitsError() $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); - $connection->emit('data', array("GET http://example.com:80/#home HTTP/1.0\r\n\r\n")); + $connection->emit('data', ["GET http://example.com:80/#home HTTP/1.0\r\n\r\n"]); $this->assertInstanceOf('InvalidArgumentException', $error); $this->assertSame('Invalid absolute-form request-target', $error->getMessage()); @@ -517,7 +512,7 @@ public function testInvalidHeaderContainsFullUri() $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); - $connection->emit('data', array("GET / HTTP/1.1\r\nHost: http://user:pass@host/\r\n\r\n")); + $connection->emit('data', ["GET / HTTP/1.1\r\nHost: http://user:pass@host/\r\n\r\n"]); $this->assertInstanceOf('InvalidArgumentException', $error); $this->assertSame('Invalid Host header value', $error->getMessage()); @@ -538,7 +533,7 @@ public function testInvalidAbsoluteFormWithHostHeaderEmpty() $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); - $connection->emit('data', array("GET http://example.com/ HTTP/1.1\r\nHost: \r\n\r\n")); + $connection->emit('data', ["GET http://example.com/ HTTP/1.1\r\nHost: \r\n\r\n"]); $this->assertInstanceOf('InvalidArgumentException', $error); $this->assertSame('Invalid Host header value', $error->getMessage()); @@ -559,7 +554,7 @@ public function testInvalidConnectRequestWithNonAuthorityForm() $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); - $connection->emit('data', array("CONNECT http://example.com:8080/ HTTP/1.1\r\nHost: example.com:8080\r\n\r\n")); + $connection->emit('data', ["CONNECT http://example.com:8080/ HTTP/1.1\r\nHost: example.com:8080\r\n\r\n"]); $this->assertInstanceOf('InvalidArgumentException', $error); $this->assertSame('CONNECT method MUST use authority-form request target', $error->getMessage()); @@ -580,7 +575,7 @@ public function testInvalidHttpVersion() $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); - $connection->emit('data', array("GET / HTTP/1.2\r\n\r\n")); + $connection->emit('data', ["GET / HTTP/1.2\r\n\r\n"]); $this->assertInstanceOf('InvalidArgumentException', $error); $this->assertSame(505, $error->getCode()); @@ -602,7 +597,7 @@ public function testInvalidContentLengthRequestHeaderWillEmitError() $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); - $connection->emit('data', array("GET / HTTP/1.1\r\nHost: localhost\r\nContent-Length: foo\r\n\r\n")); + $connection->emit('data', ["GET / HTTP/1.1\r\nHost: localhost\r\nContent-Length: foo\r\n\r\n"]); $this->assertInstanceOf('InvalidArgumentException', $error); $this->assertSame(400, $error->getCode()); @@ -624,7 +619,7 @@ public function testInvalidRequestWithMultipleContentLengthRequestHeadersWillEmi $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); - $connection->emit('data', array("GET / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 4\r\nContent-Length: 5\r\n\r\n")); + $connection->emit('data', ["GET / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 4\r\nContent-Length: 5\r\n\r\n"]); $this->assertInstanceOf('InvalidArgumentException', $error); $this->assertSame(400, $error->getCode()); @@ -646,7 +641,7 @@ public function testInvalidTransferEncodingRequestHeaderWillEmitError() $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); - $connection->emit('data', array("GET / HTTP/1.1\r\nHost: localhost\r\nTransfer-Encoding: foo\r\n\r\n")); + $connection->emit('data', ["GET / HTTP/1.1\r\nHost: localhost\r\nTransfer-Encoding: foo\r\n\r\n"]); $this->assertInstanceOf('InvalidArgumentException', $error); $this->assertSame(501, $error->getCode()); @@ -668,7 +663,7 @@ public function testInvalidRequestWithBothTransferEncodingAndContentLengthWillEm $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); - $connection->emit('data', array("GET / HTTP/1.1\r\nHost: localhost\r\nTransfer-Encoding: chunked\r\nContent-Length: 0\r\n\r\n")); + $connection->emit('data', ["GET / HTTP/1.1\r\nHost: localhost\r\nTransfer-Encoding: chunked\r\nContent-Length: 0\r\n\r\n"]); $this->assertInstanceOf('InvalidArgumentException', $error); $this->assertSame(400, $error->getCode()); @@ -688,12 +683,12 @@ public function testServerParamsWillBeSetOnHttpsRequest() $request = $parsedRequest; }); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('getLocalAddress', 'getRemoteAddress'))->getMock(); + $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(['getLocalAddress', 'getRemoteAddress'])->getMock(); $connection->expects($this->once())->method('getLocalAddress')->willReturn('tls://127.1.1.1:8000'); $connection->expects($this->once())->method('getRemoteAddress')->willReturn('tls://192.168.1.1:8001'); $parser->handle($connection); - $connection->emit('data', array("GET /foo HTTP/1.0\r\nHost: example.com\r\n\r\n")); + $connection->emit('data', ["GET /foo HTTP/1.0\r\nHost: example.com\r\n\r\n"]); $serverParams = $request->getServerParams(); @@ -721,12 +716,12 @@ public function testServerParamsWillBeSetOnHttpRequest() $request = $parsedRequest; }); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('getLocalAddress', 'getRemoteAddress'))->getMock(); + $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(['getLocalAddress', 'getRemoteAddress'])->getMock(); $connection->expects($this->once())->method('getLocalAddress')->willReturn('tcp://127.1.1.1:8000'); $connection->expects($this->once())->method('getRemoteAddress')->willReturn('tcp://192.168.1.1:8001'); $parser->handle($connection); - $connection->emit('data', array("GET /foo HTTP/1.0\r\nHost: example.com\r\n\r\n")); + $connection->emit('data', ["GET /foo HTTP/1.0\r\nHost: example.com\r\n\r\n"]); $serverParams = $request->getServerParams(); @@ -754,12 +749,12 @@ public function testServerParamsWillNotSetRemoteAddressForUnixDomainSockets() $request = $parsedRequest; }); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('getLocalAddress', 'getRemoteAddress'))->getMock(); + $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(['getLocalAddress', 'getRemoteAddress'])->getMock(); $connection->expects($this->once())->method('getLocalAddress')->willReturn('unix://./server.sock'); $connection->expects($this->once())->method('getRemoteAddress')->willReturn(null); $parser->handle($connection); - $connection->emit('data', array("GET /foo HTTP/1.0\r\nHost: example.com\r\n\r\n")); + $connection->emit('data', ["GET /foo HTTP/1.0\r\nHost: example.com\r\n\r\n"]); $serverParams = $request->getServerParams(); @@ -790,7 +785,7 @@ public function testServerParamsWontBeSetOnMissingUrls() $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); - $connection->emit('data', array("GET /foo HTTP/1.0\r\nHost: example.com\r\n\r\n")); + $connection->emit('data', ["GET /foo HTTP/1.0\r\nHost: example.com\r\n\r\n"]); $serverParams = $request->getServerParams(); @@ -811,12 +806,12 @@ public function testServerParamsWillBeReusedForMultipleRequestsFromSameConnectio $parser = new RequestHeaderParser($clock); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('getLocalAddress', 'getRemoteAddress'))->getMock(); + $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(['getLocalAddress', 'getRemoteAddress'])->getMock(); $connection->expects($this->once())->method('getLocalAddress')->willReturn('tcp://127.1.1.1:8000'); $connection->expects($this->once())->method('getRemoteAddress')->willReturn('tcp://192.168.1.1:8001'); $parser->handle($connection); - $connection->emit('data', array("GET /foo HTTP/1.0\r\nHost: example.com\r\n\r\n")); + $connection->emit('data', ["GET /foo HTTP/1.0\r\nHost: example.com\r\n\r\n"]); $request = null; $parser->on('headers', function ($parsedRequest) use (&$request) { @@ -824,7 +819,7 @@ public function testServerParamsWillBeReusedForMultipleRequestsFromSameConnectio }); $parser->handle($connection); - $connection->emit('data', array("GET /foo HTTP/1.0\r\nHost: example.com\r\n\r\n")); + $connection->emit('data', ["GET /foo HTTP/1.0\r\nHost: example.com\r\n\r\n"]); assert($request instanceof ServerRequestInterface); $serverParams = $request->getServerParams(); @@ -846,10 +841,10 @@ public function testServerParamsWillBeRememberedUntilConnectionIsClosed() $parser = new RequestHeaderParser($clock); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('getLocalAddress', 'getRemoteAddress'))->getMock(); + $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(['getLocalAddress', 'getRemoteAddress'])->getMock(); $parser->handle($connection); - $connection->emit('data', array("GET /foo HTTP/1.0\r\nHost: example.com\r\n\r\n")); + $connection->emit('data', ["GET /foo HTTP/1.0\r\nHost: example.com\r\n\r\n"]); $ref = new \ReflectionProperty($parser, 'connectionParams'); $ref->setAccessible(true); @@ -857,7 +852,7 @@ public function testServerParamsWillBeRememberedUntilConnectionIsClosed() $this->assertCount(1, $ref->getValue($parser)); $connection->emit('close'); - $this->assertEquals(array(), $ref->getValue($parser)); + $this->assertEquals([], $ref->getValue($parser)); } public function testQueryParmetersWillBeSet() @@ -875,7 +870,7 @@ public function testQueryParmetersWillBeSet() $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); - $connection->emit('data', array("GET /foo.php?hello=world&test=this HTTP/1.0\r\nHost: example.com\r\n\r\n")); + $connection->emit('data', ["GET /foo.php?hello=world&test=this HTTP/1.0\r\nHost: example.com\r\n\r\n"]); $queryParams = $request->getQueryParams(); diff --git a/tests/Io/SenderTest.php b/tests/Io/SenderTest.php index 03a9b56e..59bb9719 100644 --- a/tests/Io/SenderTest.php +++ b/tests/Io/SenderTest.php @@ -9,13 +9,15 @@ use React\Http\Io\ReadableBodyStream; use React\Http\Io\Sender; use React\Http\Message\Request; -use React\Promise; +use React\Promise\Promise; use React\Stream\ThroughStream; use React\Tests\Http\TestCase; +use function React\Promise\reject; +use function React\Promise\resolve; class SenderTest extends TestCase { - /** @var \React\EventLoop\LoopInterface */ + /** @var LoopInterface */ private $loop; /** @@ -55,7 +57,7 @@ public function testSenderRejectsInvalidUri() public function testSenderConnectorRejection() { $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); - $connector->expects($this->once())->method('connect')->willReturn(Promise\reject(new \RuntimeException('Rejected'))); + $connector->expects($this->once())->method('connect')->willReturn(reject(new \RuntimeException('Rejected'))); $sender = new Sender(new HttpClient(new ClientConnectionManager($connector, $this->loop))); @@ -80,7 +82,7 @@ public function testSendPostWillAutomaticallySendContentLengthHeader() $sender = new Sender($client); - $request = new Request('POST', 'http://www.google.com/', array(), 'hello'); + $request = new Request('POST', 'http://www.google.com/', [], 'hello'); $sender->send($request); } @@ -93,7 +95,7 @@ public function testSendPostWillAutomaticallySendContentLengthZeroHeaderForEmpty $sender = new Sender($client); - $request = new Request('POST', 'http://www.google.com/', array(), ''); + $request = new Request('POST', 'http://www.google.com/', [], ''); $sender->send($request); } @@ -110,7 +112,7 @@ public function testSendPostStreamWillAutomaticallySendTransferEncodingChunked() $sender = new Sender($client); $stream = new ThroughStream(); - $request = new Request('POST', 'http://www.google.com/', array(), new ReadableBodyStream($stream)); + $request = new Request('POST', 'http://www.google.com/', [], new ReadableBodyStream($stream)); $sender->send($request); } @@ -118,7 +120,7 @@ public function testSendPostStreamWillAutomaticallyPipeChunkEncodeBodyForWriteAn { $outgoing = $this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock(); $outgoing->expects($this->once())->method('isWritable')->willReturn(true); - $outgoing->expects($this->exactly(2))->method('write')->withConsecutive(array(""), array("5\r\nhello\r\n"))->willReturn(false); + $outgoing->expects($this->exactly(2))->method('write')->withConsecutive([""], ["5\r\nhello\r\n"])->willReturn(false); $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); $client->expects($this->once())->method('request')->willReturn($outgoing); @@ -126,7 +128,7 @@ public function testSendPostStreamWillAutomaticallyPipeChunkEncodeBodyForWriteAn $sender = new Sender($client); $stream = new ThroughStream(); - $request = new Request('POST', 'http://www.google.com/', array(), new ReadableBodyStream($stream)); + $request = new Request('POST', 'http://www.google.com/', [], new ReadableBodyStream($stream)); $sender->send($request); $ret = $stream->write('hello'); @@ -137,7 +139,7 @@ public function testSendPostStreamWillAutomaticallyPipeChunkEncodeBodyForEnd() { $outgoing = $this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock(); $outgoing->expects($this->once())->method('isWritable')->willReturn(true); - $outgoing->expects($this->exactly(2))->method('write')->withConsecutive(array(""), array("0\r\n\r\n"))->willReturn(false); + $outgoing->expects($this->exactly(2))->method('write')->withConsecutive([""], ["0\r\n\r\n"])->willReturn(false); $outgoing->expects($this->once())->method('end')->with(null); $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); @@ -146,7 +148,7 @@ public function testSendPostStreamWillAutomaticallyPipeChunkEncodeBodyForEnd() $sender = new Sender($client); $stream = new ThroughStream(); - $request = new Request('POST', 'http://www.google.com/', array(), new ReadableBodyStream($stream)); + $request = new Request('POST', 'http://www.google.com/', [], new ReadableBodyStream($stream)); $sender->send($request); $stream->end(); @@ -167,10 +169,10 @@ public function testSendPostStreamWillRejectWhenRequestBodyEmitsErrorEvent() $expected = new \RuntimeException(); $stream = new ThroughStream(); - $request = new Request('POST', 'http://www.google.com/', array(), new ReadableBodyStream($stream)); + $request = new Request('POST', 'http://www.google.com/', [], new ReadableBodyStream($stream)); $promise = $sender->send($request); - $stream->emit('error', array($expected)); + $stream->emit('error', [$expected]); $exception = null; $promise->then(null, function ($e) use (&$exception) { @@ -196,7 +198,7 @@ public function testSendPostStreamWillRejectWhenRequestBodyClosesWithoutEnd() $sender = new Sender($client); $stream = new ThroughStream(); - $request = new Request('POST', 'http://www.google.com/', array(), new ReadableBodyStream($stream)); + $request = new Request('POST', 'http://www.google.com/', [], new ReadableBodyStream($stream)); $promise = $sender->send($request); $stream->close(); @@ -214,7 +216,7 @@ public function testSendPostStreamWillNotRejectWhenRequestBodyClosesAfterEnd() { $outgoing = $this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock(); $outgoing->expects($this->once())->method('isWritable')->willReturn(true); - $outgoing->expects($this->exactly(2))->method('write')->withConsecutive(array(""), array("0\r\n\r\n"))->willReturn(false); + $outgoing->expects($this->exactly(2))->method('write')->withConsecutive([""], ["0\r\n\r\n"])->willReturn(false); $outgoing->expects($this->once())->method('end'); $outgoing->expects($this->never())->method('close'); @@ -224,7 +226,7 @@ public function testSendPostStreamWillNotRejectWhenRequestBodyClosesAfterEnd() $sender = new Sender($client); $stream = new ThroughStream(); - $request = new Request('POST', 'http://www.google.com/', array(), new ReadableBodyStream($stream)); + $request = new Request('POST', 'http://www.google.com/', [], new ReadableBodyStream($stream)); $promise = $sender->send($request); $stream->end(); @@ -248,7 +250,7 @@ public function testSendPostStreamWithExplicitContentLengthWillSendHeaderAsIs() $sender = new Sender($client); $stream = new ThroughStream(); - $request = new Request('POST', 'http://www.google.com/', array('Content-Length' => '100'), new ReadableBodyStream($stream)); + $request = new Request('POST', 'http://www.google.com/', ['Content-Length' => '100'], new ReadableBodyStream($stream)); $sender->send($request); } @@ -275,7 +277,7 @@ public function testSendGetWithEmptyBodyStreamWillNotPassContentLengthOrTransfer $sender = new Sender($client); $body = new EmptyBodyStream(); - $request = new Request('GET', 'http://www.google.com/', array(), $body); + $request = new Request('GET', 'http://www.google.com/', [], $body); $sender->send($request); } @@ -302,7 +304,7 @@ public function testSendCustomMethodWithExplicitContentLengthZeroWillBePassedAsI $sender = new Sender($client); - $request = new Request('CUSTOM', 'http://www.google.com/', array('Content-Length' => '0')); + $request = new Request('CUSTOM', 'http://www.google.com/', ['Content-Length' => '0']); $sender->send($request); } @@ -330,13 +332,13 @@ public function getRequestWithUserAndPassShouldSendAGetRequestWithGivenAuthoriza $sender = new Sender($client); - $request = new Request('GET', 'http://john:dummy@www.example.com', array('Authorization' => 'bearer abc123')); + $request = new Request('GET', 'http://john:dummy@www.example.com', ['Authorization' => 'bearer abc123']); $sender->send($request); } public function testCancelRequestWillCancelConnector() { - $promise = new \React\Promise\Promise(function () { }, function () { + $promise = new Promise(function () { }, function () { throw new \RuntimeException(); }); @@ -364,7 +366,7 @@ public function testCancelRequestWillCloseConnection() $connection->expects($this->once())->method('close'); $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); - $connector->expects($this->once())->method('connect')->willReturn(Promise\resolve($connection)); + $connector->expects($this->once())->method('connect')->willReturn(resolve($connection)); $sender = new Sender(new HttpClient(new ClientConnectionManager($connector, $this->loop))); diff --git a/tests/Io/StreamingServerTest.php b/tests/Io/StreamingServerTest.php index 152fece6..3b5f28f4 100644 --- a/tests/Io/StreamingServerTest.php +++ b/tests/Io/StreamingServerTest.php @@ -12,6 +12,7 @@ use React\Stream\ThroughStream; use React\Tests\Http\SocketServerStub; use React\Tests\Http\TestCase; +use function React\Promise\resolve; class StreamingServerTest extends TestCase { @@ -37,7 +38,7 @@ private function mockConnection(array $additionalMethods = null) $connection = $this->getMockBuilder('React\Socket\Connection') ->disableOriginalConstructor() ->setMethods(array_merge( - array( + [ 'write', 'end', 'close', @@ -48,8 +49,8 @@ private function mockConnection(array $additionalMethods = null) 'getRemoteAddress', 'getLocalAddress', 'pipe' - ), - (is_array($additionalMethods) ? $additionalMethods : array()) + ], + (is_array($additionalMethods) ? $additionalMethods : []) )) ->getMock(); @@ -64,11 +65,11 @@ public function testRequestEventWillNotBeEmittedForIncompleteHeaders() $server = new StreamingServer(Loop::get(), $this->expectCallableNever()); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = ''; $data .= "GET / HTTP/1.1\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); } public function testRequestEventIsEmitted() @@ -76,22 +77,22 @@ public function testRequestEventIsEmitted() $server = new StreamingServer(Loop::get(), $this->expectCallableOnce()); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = $this->createGetRequest(); - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); } public function testRequestEventIsEmittedForArrayCallable() { $this->called = null; - $server = new StreamingServer(Loop::get(), array($this, 'helperCallableOnce')); + $server = new StreamingServer(Loop::get(), [$this, 'helperCallableOnce']); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = $this->createGetRequest(); - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertEquals(1, $this->called); } @@ -116,10 +117,10 @@ public function testRequestEvent() ->willReturn('127.0.0.1:8080'); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = $this->createGetRequest(); - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $serverParams = $requestAssertion->getServerParams(); @@ -128,7 +129,7 @@ public function testRequestEvent() $this->assertSame('GET', $requestAssertion->getMethod()); $this->assertSame('/', $requestAssertion->getRequestTarget()); $this->assertSame('/', $requestAssertion->getUri()->getPath()); - $this->assertSame(array(), $requestAssertion->getQueryParams()); + $this->assertSame([], $requestAssertion->getQueryParams()); $this->assertSame('http://example.com/', (string)$requestAssertion->getUri()); $this->assertSame('example.com', $requestAssertion->getHeaderLine('Host')); $this->assertSame('127.0.0.1', $serverParams['REMOTE_ADDR']); @@ -149,10 +150,10 @@ public function testRequestEventWithSingleRequestHandlerArray() ->willReturn('127.0.0.1:8080'); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = $this->createGetRequest(); - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $serverParams = $requestAssertion->getServerParams(); @@ -161,7 +162,7 @@ public function testRequestEventWithSingleRequestHandlerArray() $this->assertSame('GET', $requestAssertion->getMethod()); $this->assertSame('/', $requestAssertion->getRequestTarget()); $this->assertSame('/', $requestAssertion->getUri()->getPath()); - $this->assertSame(array(), $requestAssertion->getQueryParams()); + $this->assertSame([], $requestAssertion->getQueryParams()); $this->assertSame('http://example.com/', (string)$requestAssertion->getUri()); $this->assertSame('example.com', $requestAssertion->getHeaderLine('Host')); $this->assertSame('127.0.0.1', $serverParams['REMOTE_ADDR']); @@ -175,10 +176,10 @@ public function testRequestGetWithHostAndCustomPort() }); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\nHost: example.com:8080\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion); $this->assertSame('GET', $requestAssertion->getMethod()); @@ -197,10 +198,10 @@ public function testRequestGetWithHostAndHttpsPort() }); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\nHost: example.com:443\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion); $this->assertSame('GET', $requestAssertion->getMethod()); @@ -219,10 +220,10 @@ public function testRequestGetWithHostAndDefaultPortWillBeIgnored() }); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion); $this->assertSame('GET', $requestAssertion->getMethod()); @@ -241,10 +242,10 @@ public function testRequestGetHttp10WithoutHostWillBeIgnored() }); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.0\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion); $this->assertSame('GET', $requestAssertion->getMethod()); @@ -262,10 +263,10 @@ public function testRequestGetHttp11WithoutHostWillReject() $server->on('error', $this->expectCallableOnce()); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); } public function testRequestOptionsAsterisk() @@ -276,10 +277,10 @@ public function testRequestOptionsAsterisk() }); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "OPTIONS * HTTP/1.1\r\nHost: example.com\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion); $this->assertSame('OPTIONS', $requestAssertion->getMethod()); @@ -295,10 +296,10 @@ public function testRequestNonOptionsWithAsteriskRequestTargetWillReject() $server->on('error', $this->expectCallableOnce()); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET * HTTP/1.1\r\nHost: example.com\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); } public function testRequestConnectAuthorityForm() @@ -309,10 +310,10 @@ public function testRequestConnectAuthorityForm() }); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "CONNECT example.com:443 HTTP/1.1\r\nHost: example.com:443\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion); $this->assertSame('CONNECT', $requestAssertion->getMethod()); @@ -331,10 +332,10 @@ public function testRequestConnectWithoutHostWillBePassesAsIs() }); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "CONNECT example.com:443 HTTP/1.1\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion); $this->assertSame('CONNECT', $requestAssertion->getMethod()); @@ -353,10 +354,10 @@ public function testRequestConnectAuthorityFormWithDefaultPortWillBePassedAsIs() }); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "CONNECT example.com:80 HTTP/1.1\r\nHost: example.com:80\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion); $this->assertSame('CONNECT', $requestAssertion->getMethod()); @@ -375,10 +376,10 @@ public function testRequestConnectAuthorityFormNonMatchingHostWillBePassedAsIs() }); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "CONNECT example.com:80 HTTP/1.1\r\nHost: other.example.org\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion); $this->assertSame('CONNECT', $requestAssertion->getMethod()); @@ -395,10 +396,10 @@ public function testRequestConnectOriginFormRequestTargetWillReject() $server->on('error', $this->expectCallableOnce()); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "CONNECT / HTTP/1.1\r\nHost: example.com\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); } public function testRequestNonConnectWithAuthorityRequestTargetWillReject() @@ -407,10 +408,10 @@ public function testRequestNonConnectWithAuthorityRequestTargetWillReject() $server->on('error', $this->expectCallableOnce()); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET example.com:80 HTTP/1.1\r\nHost: example.com\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); } public function testRequestWithoutHostEventUsesSocketAddress() @@ -427,10 +428,10 @@ public function testRequestWithoutHostEventUsesSocketAddress() ->willReturn('127.0.0.1:80'); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET /test HTTP/1.0\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion); $this->assertSame('GET', $requestAssertion->getMethod()); @@ -448,10 +449,10 @@ public function testRequestAbsoluteEvent() }); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET http://example.com/test HTTP/1.1\r\nHost: example.com\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion); $this->assertSame('GET', $requestAssertion->getMethod()); @@ -470,10 +471,10 @@ public function testRequestAbsoluteNonMatchingHostWillBePassedAsIs() }); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET http://example.com/test HTTP/1.1\r\nHost: other.example.org\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion); $this->assertSame('GET', $requestAssertion->getMethod()); @@ -489,10 +490,10 @@ public function testRequestAbsoluteWithoutHostWillReject() $server->on('error', $this->expectCallableOnce()); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET http://example.com:8080/test HTTP/1.1\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); } public function testRequestOptionsAsteriskEvent() @@ -504,10 +505,10 @@ public function testRequestOptionsAsteriskEvent() }); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "OPTIONS * HTTP/1.1\r\nHost: example.com\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion); $this->assertSame('OPTIONS', $requestAssertion->getMethod()); @@ -526,10 +527,10 @@ public function testRequestOptionsAbsoluteEvent() }); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "OPTIONS http://example.com HTTP/1.1\r\nHost: example.com\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion); $this->assertSame('OPTIONS', $requestAssertion->getMethod()); @@ -548,7 +549,7 @@ public function testRequestPauseWillBeForwardedToConnection() $this->connection->expects($this->once())->method('pause'); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\n"; $data .= "Host: example.com\r\n"; @@ -556,7 +557,7 @@ public function testRequestPauseWillBeForwardedToConnection() $data .= "Content-Length: 5\r\n"; $data .= "\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); } public function testRequestResumeWillBeForwardedToConnection() @@ -568,7 +569,7 @@ public function testRequestResumeWillBeForwardedToConnection() $this->connection->expects($this->once())->method('resume'); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\n"; $data .= "Host: example.com\r\n"; @@ -576,7 +577,7 @@ public function testRequestResumeWillBeForwardedToConnection() $data .= "Content-Length: 5\r\n"; $data .= "\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); } public function testRequestCloseWillNotCloseConnection() @@ -588,10 +589,10 @@ public function testRequestCloseWillNotCloseConnection() $this->connection->expects($this->never())->method('close'); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = $this->createGetRequest(); - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); } public function testRequestPauseAfterCloseWillNotBeForwarded() @@ -605,10 +606,10 @@ public function testRequestPauseAfterCloseWillNotBeForwarded() $this->connection->expects($this->never())->method('pause'); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = $this->createGetRequest(); - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); } public function testRequestResumeAfterCloseWillNotBeForwarded() @@ -622,10 +623,10 @@ public function testRequestResumeAfterCloseWillNotBeForwarded() $this->connection->expects($this->never())->method('resume'); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = $this->createGetRequest(); - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); } public function testRequestEventWithoutBodyWillNotEmitData() @@ -637,10 +638,10 @@ public function testRequestEventWithoutBodyWillNotEmitData() }); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = $this->createGetRequest(); - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); } public function testRequestEventWithSecondDataEventWillEmitBodyData() @@ -652,7 +653,7 @@ public function testRequestEventWithSecondDataEventWillEmitBodyData() }); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = ''; $data .= "POST / HTTP/1.1\r\n"; @@ -660,7 +661,7 @@ public function testRequestEventWithSecondDataEventWillEmitBodyData() $data .= "Content-Length: 100\r\n"; $data .= "\r\n"; $data .= "incomplete"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); } public function testRequestEventWithPartialBodyWillEmitData() @@ -672,18 +673,18 @@ public function testRequestEventWithPartialBodyWillEmitData() }); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = ''; $data .= "POST / HTTP/1.1\r\n"; $data .= "Host: localhost\r\n"; $data .= "Content-Length: 100\r\n"; $data .= "\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $data = ''; $data .= "incomplete"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); } public function testResponseContainsServerHeader() @@ -706,10 +707,10 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = $this->createGetRequest(); - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertContainsString("\r\nServer: ReactPHP/1\r\n", $buffer); } @@ -736,10 +737,10 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = $this->createGetRequest(); - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertEquals('', $buffer); } @@ -766,10 +767,10 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = $this->createGetRequest(); - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->connection->emit('close'); $this->assertEquals('', $buffer); @@ -783,7 +784,7 @@ public function testResponseBodyStreamAlreadyClosedWillSendEmptyBodyChunkedEncod $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($stream) { return new Response( 200, - array(), + [], $stream ); }); @@ -802,10 +803,10 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertStringStartsWith("HTTP/1.1 200 OK\r\n", $buffer); $this->assertStringEndsWith("\r\n\r\n0\r\n\r\n", $buffer); @@ -818,7 +819,7 @@ public function testResponseBodyStreamEndingWillSendEmptyBodyChunkedEncoded() $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($stream) { return new Response( 200, - array(), + [], $stream ); }); @@ -837,10 +838,10 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $stream->end(); @@ -856,7 +857,7 @@ public function testResponseBodyStreamAlreadyClosedWillSendEmptyBodyPlainHttp10( $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($stream) { return new Response( 200, - array(), + [], $stream ); }); @@ -875,10 +876,10 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.0\r\nHost: localhost\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertStringStartsWith("HTTP/1.0 200 OK\r\n", $buffer); $this->assertStringEndsWith("\r\n\r\n", $buffer); @@ -892,7 +893,7 @@ public function testResponseStreamWillBeClosedIfConnectionIsAlreadyClosed() $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($stream) { return new Response( 200, - array(), + [], $stream ); }); @@ -913,7 +914,7 @@ function ($data) use (&$buffer) { $this->connection = $this->getMockBuilder('React\Socket\Connection') ->disableOriginalConstructor() ->setMethods( - array( + [ 'write', 'end', 'close', @@ -924,7 +925,7 @@ function ($data) use (&$buffer) { 'getRemoteAddress', 'getLocalAddress', 'pipe' - ) + ] ) ->getMock(); @@ -933,10 +934,10 @@ function ($data) use (&$buffer) { $this->connection->expects($this->never())->method('write'); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = $this->createGetRequest(); - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); } public function testResponseBodyStreamWillBeClosedIfConnectionEmitsCloseEvent() @@ -947,16 +948,16 @@ public function testResponseBodyStreamWillBeClosedIfConnectionEmitsCloseEvent() $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($stream) { return new Response( 200, - array(), + [], $stream ); }); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = $this->createGetRequest(); - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->connection->emit('close'); } @@ -965,11 +966,11 @@ public function testResponseUpgradeInResponseCanBeUsedToAdvertisePossibleUpgrade $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Response( 200, - array( + [ 'date' => '', 'server' => '', 'Upgrade' => 'demo' - ), + ], 'foo' ); }); @@ -988,10 +989,10 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertEquals("HTTP/1.1 200 OK\r\nUpgrade: demo\r\nContent-Length: 3\r\n\r\nfoo", $buffer); } @@ -1001,10 +1002,10 @@ public function testResponseUpgradeWishInRequestCanBeIgnoredByReturningNormalRes $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Response( 200, - array( + [ 'date' => '', 'server' => '' - ), + ], 'foo' ); }); @@ -1023,10 +1024,10 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\nHost: localhost\r\nUpgrade: demo\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertEquals("HTTP/1.1 200 OK\r\nContent-Length: 3\r\n\r\nfoo", $buffer); } @@ -1036,11 +1037,11 @@ public function testResponseUpgradeSwitchingProtocolIncludesConnectionUpgradeHea $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Response( 101, - array( + [ 'date' => '', 'server' => '', 'Upgrade' => 'demo' - ), + ], 'foo' ); }); @@ -1061,10 +1062,10 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\nHost: localhost\r\nUpgrade: demo\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertEquals("HTTP/1.1 101 Switching Protocols\r\nUpgrade: demo\r\nConnection: upgrade\r\n\r\nfoo", $buffer); } @@ -1076,11 +1077,11 @@ public function testResponseUpgradeSwitchingProtocolWithStreamWillPipeDataToConn $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($stream) { return new Response( 101, - array( + [ 'date' => '', 'server' => '', 'Upgrade' => 'demo' - ), + ], $stream ); }); @@ -1099,10 +1100,10 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\nHost: localhost\r\nUpgrade: demo\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $stream->write('hello'); $stream->write('world'); @@ -1117,7 +1118,7 @@ public function testResponseConnectMethodStreamWillPipeDataToConnection() $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($stream) { return new Response( 200, - array(), + [], $stream ); }); @@ -1136,10 +1137,10 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "CONNECT example.com:80 HTTP/1.1\r\nHost: example.com:80\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $stream->write('hello'); $stream->write('world'); @@ -1155,18 +1156,18 @@ public function testResponseConnectMethodStreamWillPipeDataFromConnection() $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($stream) { return new Response( 200, - array(), + [], $stream ); }); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $this->connection->expects($this->once())->method('pipe')->with($stream); $data = "CONNECT example.com:80 HTTP/1.1\r\nHost: example.com:80\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); } public function testResponseContainsSameRequestProtocolVersionAndChunkedBodyForHttp11() @@ -1174,7 +1175,7 @@ public function testResponseContainsSameRequestProtocolVersionAndChunkedBodyForH $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Response( 200, - array(), + [], 'bye' ); }); @@ -1193,10 +1194,10 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); $this->assertContainsString("bye", $buffer); @@ -1207,7 +1208,7 @@ public function testResponseContainsSameRequestProtocolVersionAndRawBodyForHttp1 $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Response( 200, - array(), + [], 'bye' ); }); @@ -1226,10 +1227,10 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.0\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertContainsString("HTTP/1.0 200 OK\r\n", $buffer); $this->assertContainsString("\r\n\r\n", $buffer); @@ -1241,7 +1242,7 @@ public function testResponseContainsNoResponseBodyForHeadRequest() $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Response( 200, - array(), + [], 'bye' ); }); @@ -1259,10 +1260,10 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "HEAD / HTTP/1.1\r\nHost: localhost\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); $this->assertContainsString("\r\nContent-Length: 3\r\n", $buffer); @@ -1277,7 +1278,7 @@ public function testResponseContainsNoResponseBodyForHeadRequestWithStreamingRes $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($stream) { return new Response( 200, - array('Content-Length' => '3'), + ['Content-Length' => '3'], $stream ); }); @@ -1295,10 +1296,10 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "HEAD / HTTP/1.1\r\nHost: localhost\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); $this->assertContainsString("\r\nContent-Length: 3\r\n", $buffer); @@ -1309,7 +1310,7 @@ public function testResponseContainsNoResponseBodyAndNoContentLengthForNoContent $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Response( 204, - array(), + [], 'bye' ); }); @@ -1327,10 +1328,10 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertContainsString("HTTP/1.1 204 No Content\r\n", $buffer); $this->assertNotContainsString("\r\nContent-Length: 3\r\n", $buffer); @@ -1345,7 +1346,7 @@ public function testResponseContainsNoResponseBodyAndNoContentLengthForNoContent $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($stream) { return new Response( 204, - array('Content-Length' => '3'), + ['Content-Length' => '3'], $stream ); }); @@ -1363,10 +1364,10 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "HEAD / HTTP/1.1\r\nHost: localhost\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertContainsString("HTTP/1.1 204 No Content\r\n", $buffer); $this->assertNotContainsString("\r\nContent-Length: 3\r\n", $buffer); @@ -1377,7 +1378,7 @@ public function testResponseContainsNoContentLengthHeaderForNotModifiedStatus() $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Response( 304, - array(), + [], '' ); }); @@ -1395,10 +1396,10 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertContainsString("HTTP/1.1 304 Not Modified\r\n", $buffer); $this->assertNotContainsString("\r\nContent-Length: 0\r\n", $buffer); @@ -1409,7 +1410,7 @@ public function testResponseContainsExplicitContentLengthHeaderForNotModifiedSta $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Response( 304, - array('Content-Length' => 3), + ['Content-Length' => 3], '' ); }); @@ -1427,10 +1428,10 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertContainsString("HTTP/1.1 304 Not Modified\r\n", $buffer); $this->assertContainsString("\r\nContent-Length: 3\r\n", $buffer); @@ -1441,7 +1442,7 @@ public function testResponseContainsExplicitContentLengthHeaderForHeadRequests() $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Response( 200, - array('Content-Length' => 3), + ['Content-Length' => 3], '' ); }); @@ -1459,10 +1460,10 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "HEAD / HTTP/1.1\r\nHost: localhost\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); $this->assertContainsString("\r\nContent-Length: 3\r\n", $buffer); @@ -1473,7 +1474,7 @@ public function testResponseContainsNoResponseBodyForNotModifiedStatus() $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Response( 304, - array(), + [], 'bye' ); }); @@ -1491,10 +1492,10 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertContainsString("HTTP/1.1 304 Not Modified\r\n", $buffer); $this->assertContainsString("\r\nContent-Length: 3\r\n", $buffer); @@ -1509,7 +1510,7 @@ public function testResponseContainsNoResponseBodyForNotModifiedStatusWithStream $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($stream) { return new Response( 304, - array('Content-Length' => '3'), + ['Content-Length' => '3'], $stream ); }); @@ -1527,10 +1528,10 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertContainsString("HTTP/1.1 304 Not Modified\r\n", $buffer); $this->assertContainsString("\r\nContent-Length: 3\r\n", $buffer); @@ -1558,10 +1559,10 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.2\r\nHost: localhost\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertInstanceOf('InvalidArgumentException', $error); @@ -1592,11 +1593,11 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\nX-DATA: "; $data .= str_repeat('A', 8193 - strlen($data)) . "\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertInstanceOf('OverflowException', $error); @@ -1626,10 +1627,10 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "bad request\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertInstanceOf('InvalidArgumentException', $error); @@ -1652,7 +1653,7 @@ public function testRequestContentLengthBodyDataWillEmitDataEventOnRequestStream }); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\n"; $data .= "Host: example.com\r\n"; @@ -1661,7 +1662,7 @@ public function testRequestContentLengthBodyDataWillEmitDataEventOnRequestStream $data .= "\r\n"; $data .= "hello"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); } public function testRequestChunkedTransferEncodingRequestWillEmitDecodedDataEventOnRequestStream() @@ -1681,7 +1682,7 @@ public function testRequestChunkedTransferEncodingRequestWillEmitDecodedDataEven }); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\n"; $data .= "Host: example.com\r\n"; @@ -1691,7 +1692,7 @@ public function testRequestChunkedTransferEncodingRequestWillEmitDecodedDataEven $data .= "5\r\nhello\r\n"; $data .= "0\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertEquals('chunked', $requestValidation->getHeaderLine('Transfer-Encoding')); } @@ -1711,7 +1712,7 @@ public function testRequestChunkedTransferEncodingWithAdditionalDataWontBeEmitte }); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\n"; $data .= "Host: example.com\r\n"; @@ -1722,7 +1723,7 @@ public function testRequestChunkedTransferEncodingWithAdditionalDataWontBeEmitte $data .= "0\r\n\r\n"; $data .= "2\r\nhi\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); } public function testRequestChunkedTransferEncodingEmpty() @@ -1740,7 +1741,7 @@ public function testRequestChunkedTransferEncodingEmpty() }); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\n"; $data .= "Host: example.com\r\n"; @@ -1749,7 +1750,7 @@ public function testRequestChunkedTransferEncodingEmpty() $data .= "\r\n"; $data .= "0\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); } public function testRequestChunkedTransferEncodingHeaderCanBeUpperCase() @@ -1769,7 +1770,7 @@ public function testRequestChunkedTransferEncodingHeaderCanBeUpperCase() }); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\n"; $data .= "Host: example.com\r\n"; @@ -1779,7 +1780,7 @@ public function testRequestChunkedTransferEncodingHeaderCanBeUpperCase() $data .= "5\r\nhello\r\n"; $data .= "0\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertEquals('CHUNKED', $requestValidation->getHeaderLine('Transfer-Encoding')); } @@ -1798,7 +1799,7 @@ public function testRequestChunkedTransferEncodingCanBeMixedUpperAndLowerCase() }); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\n"; $data .= "Host: example.com\r\n"; @@ -1807,7 +1808,7 @@ public function testRequestChunkedTransferEncodingCanBeMixedUpperAndLowerCase() $data .= "\r\n"; $data .= "5\r\nhello\r\n"; $data .= "0\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); } public function testRequestContentLengthWillEmitDataEventAndEndEventAndAdditionalDataWillBeIgnored() @@ -1823,11 +1824,11 @@ public function testRequestContentLengthWillEmitDataEventAndEndEventAndAdditiona $request->getBody()->on('close', $closeEvent); $request->getBody()->on('error', $errorEvent); - return \React\Promise\resolve(new Response()); + return resolve(new Response()); }); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\n"; $data .= "Host: example.com\r\n"; @@ -1837,7 +1838,7 @@ public function testRequestContentLengthWillEmitDataEventAndEndEventAndAdditiona $data .= "hello"; $data .= "world"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); } public function testRequestContentLengthWillEmitDataEventAndEndEventAndAdditionalDataWillBeIgnoredSplitted() @@ -1856,7 +1857,7 @@ public function testRequestContentLengthWillEmitDataEventAndEndEventAndAdditiona }); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\n"; $data .= "Host: example.com\r\n"; @@ -1865,11 +1866,11 @@ public function testRequestContentLengthWillEmitDataEventAndEndEventAndAdditiona $data .= "\r\n"; $data .= "hello"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $data = "world"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); } public function testRequestZeroContentLengthWillEmitEndEvent() @@ -1888,7 +1889,7 @@ public function testRequestZeroContentLengthWillEmitEndEvent() }); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\n"; $data .= "Host: example.com\r\n"; @@ -1896,7 +1897,7 @@ public function testRequestZeroContentLengthWillEmitEndEvent() $data .= "Content-Length: 0\r\n"; $data .= "\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); } public function testRequestZeroContentLengthWillEmitEndAndAdditionalDataWillBeIgnored() @@ -1914,7 +1915,7 @@ public function testRequestZeroContentLengthWillEmitEndAndAdditionalDataWillBeIg }); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\n"; $data .= "Host: example.com\r\n"; @@ -1923,7 +1924,7 @@ public function testRequestZeroContentLengthWillEmitEndAndAdditionalDataWillBeIg $data .= "\r\n"; $data .= "hello"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); } public function testRequestZeroContentLengthWillEmitEndAndAdditionalDataWillBeIgnoredSplitted() @@ -1941,7 +1942,7 @@ public function testRequestZeroContentLengthWillEmitEndAndAdditionalDataWillBeIg }); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\n"; $data .= "Host: example.com\r\n"; @@ -1949,11 +1950,11 @@ public function testRequestZeroContentLengthWillEmitEndAndAdditionalDataWillBeIg $data .= "Content-Length: 0\r\n"; $data .= "\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $data = "hello"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); } public function testRequestInvalidChunkHeaderTooLongWillEmitErrorOnRequestStream() @@ -1961,13 +1962,13 @@ public function testRequestInvalidChunkHeaderTooLongWillEmitErrorOnRequestStream $errorEvent = $this->expectCallableOnceWith($this->isInstanceOf('Exception')); $server = new StreamingServer(Loop::get(), function ($request) use ($errorEvent){ $request->getBody()->on('error', $errorEvent); - return \React\Promise\resolve(new Response()); + return resolve(new Response()); }); $this->connection->expects($this->never())->method('close'); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\n"; $data .= "Host: example.com\r\n"; @@ -1978,7 +1979,7 @@ public function testRequestInvalidChunkHeaderTooLongWillEmitErrorOnRequestStream $data .= 'a'; } - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); } public function testRequestInvalidChunkBodyTooLongWillEmitErrorOnRequestStream() @@ -1991,7 +1992,7 @@ public function testRequestInvalidChunkBodyTooLongWillEmitErrorOnRequestStream() $this->connection->expects($this->never())->method('close'); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\n"; $data .= "Host: example.com\r\n"; @@ -2000,7 +2001,7 @@ public function testRequestInvalidChunkBodyTooLongWillEmitErrorOnRequestStream() $data .= "\r\n"; $data .= "5\r\nhello world\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); } public function testRequestUnexpectedEndOfRequestWithChunkedTransferConnectionWillEmitErrorOnRequestStream() @@ -2013,7 +2014,7 @@ public function testRequestUnexpectedEndOfRequestWithChunkedTransferConnectionWi $this->connection->expects($this->never())->method('close'); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\n"; $data .= "Host: example.com\r\n"; @@ -2022,7 +2023,7 @@ public function testRequestUnexpectedEndOfRequestWithChunkedTransferConnectionWi $data .= "\r\n"; $data .= "5\r\nhello\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->connection->emit('end'); } @@ -2036,7 +2037,7 @@ public function testRequestInvalidChunkHeaderWillEmitErrorOnRequestStream() $this->connection->expects($this->never())->method('close'); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\n"; $data .= "Host: example.com\r\n"; @@ -2045,7 +2046,7 @@ public function testRequestInvalidChunkHeaderWillEmitErrorOnRequestStream() $data .= "\r\n"; $data .= "hello\r\nhello\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); } public function testRequestUnexpectedEndOfRequestWithContentLengthWillEmitErrorOnRequestStream() @@ -2058,7 +2059,7 @@ public function testRequestUnexpectedEndOfRequestWithContentLengthWillEmitErrorO $this->connection->expects($this->never())->method('close'); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\n"; $data .= "Host: example.com\r\n"; @@ -2067,7 +2068,7 @@ public function testRequestUnexpectedEndOfRequestWithContentLengthWillEmitErrorO $data .= "\r\n"; $data .= "incomplete"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->connection->emit('end'); } @@ -2088,11 +2089,11 @@ public function testRequestWithoutBodyWillEmitEndOnRequestStream() $this->connection->expects($this->never())->method('close'); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = $this->createGetRequest(); - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); } public function testRequestWithoutDefinedLengthWillIgnoreDataEvent() @@ -2110,12 +2111,12 @@ public function testRequestWithoutDefinedLengthWillIgnoreDataEvent() }); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = $this->createGetRequest(); $data .= "hello world"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); } public function testResponseWithBodyStreamWillUseChunkedTransferEncodingByDefault() @@ -2124,7 +2125,7 @@ public function testResponseWithBodyStreamWillUseChunkedTransferEncodingByDefaul $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($stream) { return new Response( 200, - array(), + [], $stream ); }); @@ -2142,12 +2143,12 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = $this->createGetRequest(); - $this->connection->emit('data', array($data)); - $stream->emit('data', array('hello')); + $this->connection->emit('data', [$data]); + $stream->emit('data', ['hello']); $this->assertContainsString("Transfer-Encoding: chunked", $buffer); $this->assertContainsString("hello", $buffer); @@ -2158,10 +2159,10 @@ public function testResponseWithBodyStringWillOverwriteExplicitContentLengthAndT $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Response( 200, - array( + [ 'Content-Length' => 1000, 'Transfer-Encoding' => 'chunked' - ), + ], 'hello' ); }); @@ -2179,11 +2180,11 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = $this->createGetRequest(); - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertNotContainsString("Transfer-Encoding: chunked", $buffer); $this->assertContainsString("Content-Length: 5", $buffer); @@ -2199,7 +2200,7 @@ public function testResponseContainsResponseBodyWithTransferEncodingChunkedForBo $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($body) { return new Response( 200, - array(), + [], $body ); }); @@ -2217,10 +2218,10 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertContainsString("Transfer-Encoding: chunked", $buffer); $this->assertNotContainsString("Content-Length:", $buffer); @@ -2236,7 +2237,7 @@ public function testResponseContainsResponseBodyWithPlainBodyWithUnknownSizeForL $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($body) { return new Response( 200, - array(), + [], $body ); }); @@ -2254,10 +2255,10 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.0\r\nHost: localhost\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertNotContainsString("Transfer-Encoding: chunked", $buffer); $this->assertNotContainsString("Content-Length:", $buffer); @@ -2270,9 +2271,9 @@ public function testResponseWithCustomTransferEncodingWillBeIgnoredAndUseChunked $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($stream) { return new Response( 200, - array( + [ 'Transfer-Encoding' => 'custom' - ), + ], $stream ); }); @@ -2290,12 +2291,12 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = $this->createGetRequest(); - $this->connection->emit('data', array($data)); - $stream->emit('data', array('hello')); + $this->connection->emit('data', [$data]); + $stream->emit('data', ['hello']); $this->assertContainsString('Transfer-Encoding: chunked', $buffer); $this->assertNotContainsString('Transfer-Encoding: custom', $buffer); @@ -2329,11 +2330,11 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = $this->createGetRequest(); - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); $this->assertContainsString("Date: Thu, 19 May 2022 14:54:51 GMT\r\n", $buffer); @@ -2345,7 +2346,7 @@ public function testResponseWithCustomDateHeaderOverwritesDefault() $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Response( 200, - array("Date" => "Tue, 15 Nov 1994 08:12:31 GMT") + ["Date" => "Tue, 15 Nov 1994 08:12:31 GMT"] ); }); @@ -2362,11 +2363,11 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = $this->createGetRequest(); - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); $this->assertContainsString("Date: Tue, 15 Nov 1994 08:12:31 GMT\r\n", $buffer); @@ -2378,7 +2379,7 @@ public function testResponseWithEmptyDateHeaderRemovesDateHeader() $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Response( 200, - array('Date' => '') + ['Date' => ''] ); }); @@ -2395,11 +2396,11 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = $this->createGetRequest(); - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); $this->assertNotContainsString("Date:", $buffer); @@ -2411,14 +2412,14 @@ public function testResponseCanContainMultipleCookieHeaders() $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Response( 200, - array( - 'Set-Cookie' => array( + [ + 'Set-Cookie' => [ 'name=test', 'session=abc' - ), + ], 'Date' => '', 'Server' => '' - ) + ] ); }); @@ -2435,11 +2436,11 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = $this->createGetRequest(); - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $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); } @@ -2463,7 +2464,7 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\n"; $data .= "Host: example.com\r\n"; @@ -2471,7 +2472,7 @@ function ($data) use (&$buffer) { $data .= "Expect: 100-continue\r\n"; $data .= "\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertContainsString("HTTP/1.1 100 Continue\r\n", $buffer); $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); } @@ -2495,13 +2496,13 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.0\r\n"; $data .= "Expect: 100-continue\r\n"; $data .= "\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertContainsString("HTTP/1.0 200 OK\r\n", $buffer); $this->assertNotContainsString("HTTP/1.1 100 Continue\r\n\r\n", $buffer); } @@ -2519,7 +2520,7 @@ public function testResponseBodyStreamWillStreamDataWithChunkedTransferEncoding( $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($input) { return new Response( 200, - array(), + [], $input ); }); @@ -2537,13 +2538,13 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = $this->createGetRequest(); - $this->connection->emit('data', array($data)); - $input->emit('data', array('1')); - $input->emit('data', array('23')); + $this->connection->emit('data', [$data]); + $input->emit('data', ['1']); + $input->emit('data', ['23']); $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); $this->assertContainsString("\r\n\r\n", $buffer); @@ -2558,7 +2559,7 @@ public function testResponseBodyStreamWithContentLengthWillStreamTillLengthWitho $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($input) { return new Response( 200, - array('Content-Length' => 5), + ['Content-Length' => 5], $input ); }); @@ -2576,13 +2577,13 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = $this->createGetRequest(); - $this->connection->emit('data', array($data)); - $input->emit('data', array('hel')); - $input->emit('data', array('lo')); + $this->connection->emit('data', [$data]); + $input->emit('data', ['hel']); + $input->emit('data', ['lo']); $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); $this->assertContainsString("Content-Length: 5\r\n", $buffer); @@ -2594,7 +2595,7 @@ function ($data) use (&$buffer) { public function testResponseWithResponsePromise() { $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { - return \React\Promise\resolve(new Response()); + return resolve(new Response()); }); $buffer = ''; @@ -2610,11 +2611,11 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = $this->createGetRequest(); - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); $this->assertContainsString("\r\n\r\n", $buffer); } @@ -2643,11 +2644,11 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = $this->createGetRequest(); - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertContainsString("HTTP/1.1 500 Internal Server Error\r\n", $buffer); $this->assertInstanceOf('RuntimeException', $exception); @@ -2656,7 +2657,7 @@ function ($data) use (&$buffer) { public function testResponseResolveWrongTypeInPromiseWillResultInError() { $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { - return \React\Promise\resolve("invalid"); + return resolve("invalid"); }); $buffer = ''; @@ -2672,11 +2673,11 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = $this->createGetRequest(); - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertContainsString("HTTP/1.1 500 Internal Server Error\r\n", $buffer); } @@ -2703,11 +2704,11 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = $this->createGetRequest(); - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertContainsString("HTTP/1.1 500 Internal Server Error\r\n", $buffer); } @@ -2734,11 +2735,11 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = $this->createGetRequest(); - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertContainsString("HTTP/1.1 500 Internal Server Error\r\n", $buffer); } @@ -2748,7 +2749,7 @@ public function testResponseWithContentLengthHeaderForStringBodyOverwritesTransf $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Response( 200, - array('Transfer-Encoding' => 'chunked'), + ['Transfer-Encoding' => 'chunked'], 'hello' ); }); @@ -2766,11 +2767,11 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = $this->createGetRequest(); - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); $this->assertContainsString("Content-Length: 5\r\n", $buffer); @@ -2798,11 +2799,11 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = $this->createGetRequest(); - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); } @@ -2831,11 +2832,11 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = $this->createGetRequest(); - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertInstanceOf('RuntimeException', $exception); $this->assertContainsString("HTTP/1.1 500 Internal Server Error\r\n", $buffer); @@ -2866,12 +2867,12 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = $this->createGetRequest(); try { - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); } catch (\Error $e) { $this->markTestSkipped( 'A \Throwable bubbled out of the request callback. ' . @@ -2911,11 +2912,11 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = $this->createGetRequest(); - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertContainsString("HTTP/1.1 500 Internal Server Error\r\n", $buffer); $this->assertInstanceOf('RuntimeException', $exception); @@ -2923,49 +2924,49 @@ function ($data) use (&$buffer) { public static function provideInvalidResponse() { - $response = new Response(200, array(), '', '1.1', 'OK'); + $response = new Response(200, [], '', '1.1', 'OK'); - return array( - array( + return [ + [ $response->withStatus(99, 'OK') - ), - array( + ], + [ $response->withStatus(1000, 'OK') - ), - array( + ], + [ $response->withStatus(200, "Invald\r\nReason: Yes") - ), - array( + ], + [ $response->withHeader('Invalid', "Yes\r\n") - ), - array( + ], + [ $response->withHeader('Invalid', "Yes\n") - ), - array( + ], + [ $response->withHeader('Invalid', "Yes\r") - ), - array( + ], + [ $response->withHeader("Inva\r\nlid", 'Yes') - ), - array( + ], + [ $response->withHeader("Inva\nlid", 'Yes') - ), - array( + ], + [ $response->withHeader("Inva\rlid", 'Yes') - ), - array( + ], + [ $response->withHeader('Inva Lid', 'Yes') - ), - array( + ], + [ $response->withHeader('Inva:Lid', 'Yes') - ), - array( + ], + [ $response->withHeader('Invalid', "Val\0ue") - ), - array( + ], + [ $response->withHeader("Inva\0lid", 'Yes') - ) - ); + ] + ]; } /** @@ -2996,11 +2997,11 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = $this->createGetRequest(); - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertContainsString("HTTP/1.1 500 Internal Server Error\r\n", $buffer); $this->assertInstanceOf('InvalidArgumentException', $exception); @@ -3024,11 +3025,11 @@ public function testRequestServerRequestParams() ->willReturn('127.0.0.1:8080'); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = $this->createGetRequest(); - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $serverParams = $requestValidation->getServerParams(); @@ -3048,11 +3049,11 @@ public function testRequestQueryParametersWillBeAddedToRequest() }); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET /foo.php?hello=world&test=bar HTTP/1.0\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $queryParams = $requestValidation->getQueryParams(); @@ -3068,7 +3069,7 @@ public function testRequestCookieWillBeAddedToServerRequest() }); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\n"; $data .= "Host: example.com\r\n"; @@ -3076,9 +3077,9 @@ public function testRequestCookieWillBeAddedToServerRequest() $data .= "Cookie: hello=world\r\n"; $data .= "\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); - $this->assertEquals(array('hello' => 'world'), $requestValidation->getCookieParams()); + $this->assertEquals(['hello' => 'world'], $requestValidation->getCookieParams()); } public function testRequestInvalidMultipleCookiesWontBeAddedToServerRequest() @@ -3089,7 +3090,7 @@ public function testRequestInvalidMultipleCookiesWontBeAddedToServerRequest() }); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\n"; $data .= "Host: example.com\r\n"; @@ -3098,8 +3099,8 @@ public function testRequestInvalidMultipleCookiesWontBeAddedToServerRequest() $data .= "Cookie: test=failed\r\n"; $data .= "\r\n"; - $this->connection->emit('data', array($data)); - $this->assertEquals(array(), $requestValidation->getCookieParams()); + $this->connection->emit('data', [$data]); + $this->assertEquals([], $requestValidation->getCookieParams()); } public function testRequestCookieWithSeparatorWillBeAddedToServerRequest() @@ -3110,7 +3111,7 @@ public function testRequestCookieWithSeparatorWillBeAddedToServerRequest() }); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\n"; $data .= "Host: example.com\r\n"; @@ -3118,8 +3119,8 @@ public function testRequestCookieWithSeparatorWillBeAddedToServerRequest() $data .= "Cookie: hello=world; test=abc\r\n"; $data .= "\r\n"; - $this->connection->emit('data', array($data)); - $this->assertEquals(array('hello' => 'world', 'test' => 'abc'), $requestValidation->getCookieParams()); + $this->connection->emit('data', [$data]); + $this->assertEquals(['hello' => 'world', 'test' => 'abc'], $requestValidation->getCookieParams()); } public function testRequestCookieWithCommaValueWillBeAddedToServerRequest() @@ -3130,7 +3131,7 @@ public function testRequestCookieWithCommaValueWillBeAddedToServerRequest() }); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\n"; $data .= "Host: example.com\r\n"; @@ -3138,8 +3139,8 @@ public function testRequestCookieWithCommaValueWillBeAddedToServerRequest() $data .= "Cookie: test=abc,def; hello=world\r\n"; $data .= "\r\n"; - $this->connection->emit('data', array($data)); - $this->assertEquals(array('test' => 'abc,def', 'hello' => 'world'), $requestValidation->getCookieParams()); + $this->connection->emit('data', [$data]); + $this->assertEquals(['test' => 'abc,def', 'hello' => 'world'], $requestValidation->getCookieParams()); } public function testNewConnectionWillInvokeParserOnce() @@ -3154,12 +3155,12 @@ public function testNewConnectionWillInvokeParserOnce() $ref->setValue($server, $parser); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); } public function testNewConnectionWillInvokeParserOnceAndInvokeRequestHandlerWhenParserIsDoneForHttp10() { - $request = new ServerRequest('GET', 'http://localhost/', array(), '', '1.0'); + $request = new ServerRequest('GET', 'http://localhost/', [], '', '1.0'); $server = new StreamingServer(Loop::get(), $this->expectCallableOnceWith($request)); @@ -3171,7 +3172,7 @@ public function testNewConnectionWillInvokeParserOnceAndInvokeRequestHandlerWhen $ref->setValue($server, $parser); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $this->connection->expects($this->once())->method('write'); $this->connection->expects($this->once())->method('end'); @@ -3182,7 +3183,7 @@ public function testNewConnectionWillInvokeParserOnceAndInvokeRequestHandlerWhen public function testNewConnectionWillInvokeParserOnceAndInvokeRequestHandlerWhenParserIsDoneForHttp11ConnectionClose() { - $request = new ServerRequest('GET', 'http://localhost/', array('Connection' => 'close')); + $request = new ServerRequest('GET', 'http://localhost/', ['Connection' => 'close']); $server = new StreamingServer(Loop::get(), $this->expectCallableOnceWith($request)); @@ -3194,7 +3195,7 @@ public function testNewConnectionWillInvokeParserOnceAndInvokeRequestHandlerWhen $ref->setValue($server, $parser); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $this->connection->expects($this->once())->method('write'); $this->connection->expects($this->once())->method('end'); @@ -3208,7 +3209,7 @@ public function testNewConnectionWillInvokeParserOnceAndInvokeRequestHandlerWhen $request = new ServerRequest('GET', 'http://localhost/'); $server = new StreamingServer(Loop::get(), function () { - return new Response(200, array('Connection' => 'close')); + return new Response(200, ['Connection' => 'close']); }); $parser = $this->getMockBuilder('React\Http\Io\RequestHeaderParser')->disableOriginalConstructor()->getMock(); @@ -3219,7 +3220,7 @@ public function testNewConnectionWillInvokeParserOnceAndInvokeRequestHandlerWhen $ref->setValue($server, $parser); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $this->connection->expects($this->once())->method('write'); $this->connection->expects($this->once())->method('end'); @@ -3244,7 +3245,7 @@ public function testNewConnectionWillInvokeParserTwiceAfterInvokingRequestHandle $ref->setValue($server, $parser); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $this->connection->expects($this->once())->method('write'); $this->connection->expects($this->never())->method('end'); @@ -3255,7 +3256,7 @@ public function testNewConnectionWillInvokeParserTwiceAfterInvokingRequestHandle public function testNewConnectionWillInvokeParserTwiceAfterInvokingRequestHandlerWhenConnectionCanBeKeptAliveForHttp10ConnectionKeepAlive() { - $request = new ServerRequest('GET', 'http://localhost/', array('Connection' => 'keep-alive'), '', '1.0'); + $request = new ServerRequest('GET', 'http://localhost/', ['Connection' => 'keep-alive'], '', '1.0'); $server = new StreamingServer(Loop::get(), function () { return new Response(); @@ -3269,7 +3270,7 @@ public function testNewConnectionWillInvokeParserTwiceAfterInvokingRequestHandle $ref->setValue($server, $parser); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $this->connection->expects($this->once())->method('write'); $this->connection->expects($this->never())->method('end'); @@ -3284,7 +3285,7 @@ public function testNewConnectionWillInvokeParserOnceAfterInvokingRequestHandler $body = new ThroughStream(); $server = new StreamingServer(Loop::get(), function () use ($body) { - return new Response(200, array(), $body); + return new Response(200, [], $body); }); $parser = $this->getMockBuilder('React\Http\Io\RequestHeaderParser')->disableOriginalConstructor()->getMock(); @@ -3295,7 +3296,7 @@ public function testNewConnectionWillInvokeParserOnceAfterInvokingRequestHandler $ref->setValue($server, $parser); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $this->connection->expects($this->once())->method('write'); $this->connection->expects($this->never())->method('end'); @@ -3310,7 +3311,7 @@ public function testNewConnectionWillInvokeParserTwiceAfterInvokingRequestHandle $body = new ThroughStream(); $server = new StreamingServer(Loop::get(), function () use ($body) { - return new Response(200, array(), $body); + return new Response(200, [], $body); }); $parser = $this->getMockBuilder('React\Http\Io\RequestHeaderParser')->disableOriginalConstructor()->getMock(); @@ -3321,7 +3322,7 @@ public function testNewConnectionWillInvokeParserTwiceAfterInvokingRequestHandle $ref->setValue($server, $parser); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $this->connection->expects($this->exactly(2))->method('write'); $this->connection->expects($this->never())->method('end'); @@ -3336,16 +3337,16 @@ public function testNewConnectionWillInvokeParserTwiceAfterInvokingRequestHandle public function testCompletingARequestWillRemoveConnectionOnCloseListener() { - $connection = $this->mockConnection(array('removeListener')); + $connection = $this->mockConnection(['removeListener']); $request = new ServerRequest('GET', 'http://localhost/'); $server = new StreamingServer(Loop::get(), function () { - return \React\Promise\resolve(new Response()); + return resolve(new Response()); }); $server->listen($this->socket); - $this->socket->emit('connection', array($connection)); + $this->socket->emit('connection', [$connection]); $connection->expects($this->once())->method('removeListener'); diff --git a/tests/Io/TransactionTest.php b/tests/Io/TransactionTest.php index 284d059f..b4825024 100644 --- a/tests/Io/TransactionTest.php +++ b/tests/Io/TransactionTest.php @@ -11,10 +11,13 @@ use React\Http\Message\Response; use React\Http\Message\ResponseException; use React\EventLoop\Loop; -use React\Promise; use React\Promise\Deferred; +use React\Promise\Promise; use React\Stream\ThroughStream; use React\Tests\Http\TestCase; +use function React\Async\await; +use function React\Promise\reject; +use function React\Promise\resolve; class TransactionTest extends TestCase { @@ -24,7 +27,7 @@ public function testWithOptionsReturnsNewInstanceWithChangedOption() $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $transaction = new Transaction($sender, $loop); - $new = $transaction->withOptions(array('followRedirects' => false)); + $new = $transaction->withOptions(['followRedirects' => false]); $this->assertInstanceOf('React\Http\Io\Transaction', $new); $this->assertNotSame($transaction, $new); @@ -41,7 +44,7 @@ public function testWithOptionsDoesNotChangeOriginalInstance() $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $transaction = new Transaction($sender, $loop); - $transaction->withOptions(array('followRedirects' => false)); + $transaction->withOptions(['followRedirects' => false]); $ref = new \ReflectionProperty($transaction, 'followRedirects'); $ref->setAccessible(true); @@ -55,8 +58,8 @@ public function testWithOptionsNullValueReturnsNewInstanceWithDefaultOption() $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $transaction = new Transaction($sender, $loop); - $transaction = $transaction->withOptions(array('followRedirects' => false)); - $transaction = $transaction->withOptions(array('followRedirects' => null)); + $transaction = $transaction->withOptions(['followRedirects' => false]); + $transaction = $transaction->withOptions(['followRedirects' => null]); $ref = new \ReflectionProperty($transaction, 'followRedirects'); $ref->setAccessible(true); @@ -74,10 +77,10 @@ public function testTimeoutExplicitOptionWillStartTimeoutTimer() $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); - $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new \React\Promise\Promise(function () { })); + $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new Promise(function () { })); $transaction = new Transaction($sender, $loop); - $transaction = $transaction->withOptions(array('timeout' => 2)); + $transaction = $transaction->withOptions(['timeout' => 2]); $promise = $transaction->send($request); $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); @@ -93,7 +96,7 @@ public function testTimeoutImplicitFromIniWillStartTimeoutTimer() $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); - $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new \React\Promise\Promise(function () { })); + $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new Promise(function () { })); $transaction = new Transaction($sender, $loop); @@ -119,10 +122,10 @@ public function testTimeoutExplicitOptionWillRejectWhenTimerFires() $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); - $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new \React\Promise\Promise(function () { })); + $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new Promise(function () { })); $transaction = new Transaction($sender, $loop); - $transaction = $transaction->withOptions(array('timeout' => 2)); + $transaction = $transaction->withOptions(['timeout' => 2]); $promise = $transaction->send($request); $this->assertNotNull($timeout); @@ -143,13 +146,13 @@ public function testTimeoutExplicitOptionWillNotStartTimeoutWhenSenderResolvesIm $loop->expects($this->never())->method('addTimer'); $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); - $response = new Response(200, array(), ''); + $response = new Response(200, [], ''); $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); - $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(Promise\resolve($response)); + $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(resolve($response)); $transaction = new Transaction($sender, $loop); - $transaction = $transaction->withOptions(array('timeout' => 0.001)); + $transaction = $transaction->withOptions(['timeout' => 0.001]); $promise = $transaction->send($request); $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); @@ -164,14 +167,14 @@ public function testTimeoutExplicitOptionWillCancelTimeoutTimerWhenSenderResolve $loop->expects($this->once())->method('cancelTimer')->with($timer); $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); - $response = new Response(200, array(), ''); + $response = new Response(200, [], ''); $deferred = new Deferred(); $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn($deferred->promise()); $transaction = new Transaction($sender, $loop); - $transaction = $transaction->withOptions(array('timeout' => 0.001)); + $transaction = $transaction->withOptions(['timeout' => 0.001]); $promise = $transaction->send($request); $deferred->resolve($response); @@ -189,10 +192,10 @@ public function testTimeoutExplicitOptionWillNotStartTimeoutWhenSenderRejectsImm $exception = new \RuntimeException(); $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); - $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(Promise\reject($exception)); + $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(reject($exception)); $transaction = new Transaction($sender, $loop); - $transaction = $transaction->withOptions(array('timeout' => 0.001)); + $transaction = $transaction->withOptions(['timeout' => 0.001]); $promise = $transaction->send($request); $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); @@ -213,7 +216,7 @@ public function testTimeoutExplicitOptionWillCancelTimeoutTimerWhenSenderRejects $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn($deferred->promise()); $transaction = new Transaction($sender, $loop); - $transaction = $transaction->withOptions(array('timeout' => 0.001)); + $transaction = $transaction->withOptions(['timeout' => 0.001]); $promise = $transaction->send($request); $exception = new \RuntimeException(); @@ -231,10 +234,10 @@ public function testTimeoutExplicitNegativeWillNotStartTimeoutTimer() $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); - $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new \React\Promise\Promise(function () { })); + $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new Promise(function () { })); $transaction = new Transaction($sender, $loop); - $transaction = $transaction->withOptions(array('timeout' => -1)); + $transaction = $transaction->withOptions(['timeout' => -1]); $promise = $transaction->send($request); $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); @@ -246,13 +249,13 @@ public function testTimeoutExplicitOptionWillNotStartTimeoutTimerWhenRequestBody $loop->expects($this->never())->method('addTimer'); $stream = new ThroughStream(); - $request = new Request('POST', 'http://example.com', array(), new ReadableBodyStream($stream)); + $request = new Request('POST', 'http://example.com', [], new ReadableBodyStream($stream)); $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); - $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new \React\Promise\Promise(function () { })); + $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new Promise(function () { })); $transaction = new Transaction($sender, $loop); - $transaction = $transaction->withOptions(array('timeout' => 2)); + $transaction = $transaction->withOptions(['timeout' => 2]); $promise = $transaction->send($request); $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); @@ -267,13 +270,13 @@ public function testTimeoutExplicitOptionWillStartTimeoutTimerWhenStreamingReque $stream = new ThroughStream(); $stream->close(); - $request = new Request('POST', 'http://example.com', array(), new ReadableBodyStream($stream)); + $request = new Request('POST', 'http://example.com', [], new ReadableBodyStream($stream)); $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); - $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new \React\Promise\Promise(function () { })); + $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new Promise(function () { })); $transaction = new Transaction($sender, $loop); - $transaction = $transaction->withOptions(array('timeout' => 2)); + $transaction = $transaction->withOptions(['timeout' => 2]); $promise = $transaction->send($request); $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); @@ -287,13 +290,13 @@ public function testTimeoutExplicitOptionWillStartTimeoutTimerWhenStreamingReque $loop->expects($this->never())->method('cancelTimer'); $stream = new ThroughStream(); - $request = new Request('POST', 'http://example.com', array(), new ReadableBodyStream($stream)); + $request = new Request('POST', 'http://example.com', [], new ReadableBodyStream($stream)); $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); - $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new \React\Promise\Promise(function () { })); + $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new Promise(function () { })); $transaction = new Transaction($sender, $loop); - $transaction = $transaction->withOptions(array('timeout' => 2)); + $transaction = $transaction->withOptions(['timeout' => 2]); $promise = $transaction->send($request); $stream->close(); @@ -307,14 +310,14 @@ public function testTimeoutExplicitOptionWillNotStartTimeoutTimerWhenStreamingRe $loop->expects($this->never())->method('addTimer'); $stream = new ThroughStream(); - $request = new Request('POST', 'http://example.com', array(), new ReadableBodyStream($stream)); + $request = new Request('POST', 'http://example.com', [], new ReadableBodyStream($stream)); $deferred = new Deferred(); $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn($deferred->promise()); $transaction = new Transaction($sender, $loop); - $transaction = $transaction->withOptions(array('timeout' => 2)); + $transaction = $transaction->withOptions(['timeout' => 2]); $promise = $transaction->send($request); $deferred->reject(new \RuntimeException('Request failed')); @@ -337,13 +340,13 @@ public function testTimeoutExplicitOptionWillRejectWhenTimerFiresAfterStreamingR $loop->expects($this->never())->method('cancelTimer'); $stream = new ThroughStream(); - $request = new Request('POST', 'http://example.com', array(), new ReadableBodyStream($stream)); + $request = new Request('POST', 'http://example.com', [], new ReadableBodyStream($stream)); $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); - $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new \React\Promise\Promise(function () { })); + $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new Promise(function () { })); $transaction = new Transaction($sender, $loop); - $transaction = $transaction->withOptions(array('timeout' => 2)); + $transaction = $transaction->withOptions(['timeout' => 2]); $promise = $transaction->send($request); $stream->close(); @@ -368,10 +371,10 @@ public function testReceivingErrorResponseWillRejectWithResponseException() // mock sender to resolve promise with the given $response in response to the given $request $sender = $this->makeSenderMock(); - $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(Promise\resolve($response)); + $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(resolve($response)); $transaction = new Transaction($sender, $loop); - $transaction = $transaction->withOptions(array('timeout' => -1)); + $transaction = $transaction->withOptions(['timeout' => -1]); $promise = $transaction->send($request); $exception = null; @@ -388,21 +391,21 @@ public function testReceivingStreamingBodyWillResolveWithBufferedResponseByDefau { $stream = new ThroughStream(); Loop::addTimer(0.001, function () use ($stream) { - $stream->emit('data', array('hello world')); + $stream->emit('data', ['hello world']); $stream->close(); }); $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); - $response = new Response(200, array(), new ReadableBodyStream($stream)); + $response = new Response(200, [], new ReadableBodyStream($stream)); // mock sender to resolve promise with the given $response in response to the given $request $sender = $this->makeSenderMock(); - $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(Promise\resolve($response)); + $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(resolve($response)); $transaction = new Transaction($sender, Loop::get()); $promise = $transaction->send($request); - $response = \React\Async\await($promise); + $response = await($promise); $this->assertEquals(200, $response->getStatusCode()); $this->assertEquals('hello world', (string)$response->getBody()); @@ -415,11 +418,11 @@ public function testReceivingStreamingBodyWithContentLengthExceedingMaximumRespo $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); - $response = new Response(200, array('Content-Length' => '100000000'), new ReadableBodyStream($stream, 100000000)); + $response = new Response(200, ['Content-Length' => '100000000'], new ReadableBodyStream($stream, 100000000)); // mock sender to resolve promise with the given $response in response to the given $request $sender = $this->makeSenderMock(); - $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(Promise\resolve($response)); + $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(resolve($response)); $transaction = new Transaction($sender, Loop::get()); @@ -446,14 +449,14 @@ public function testReceivingStreamingBodyWithContentsExceedingMaximumResponseBu $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); - $response = new Response(200, array(), new ReadableBodyStream($stream)); + $response = new Response(200, [], new ReadableBodyStream($stream)); // mock sender to resolve promise with the given $response in response to the given $request $sender = $this->makeSenderMock(); - $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(Promise\resolve($response)); + $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(resolve($response)); $transaction = new Transaction($sender, Loop::get()); - $transaction = $transaction->withOptions(array('maximumSize' => 10)); + $transaction = $transaction->withOptions(['maximumSize' => 10]); $promise = $transaction->send($request); $exception = null; @@ -479,11 +482,11 @@ public function testReceivingStreamingBodyWillRejectWhenStreamEmitsError() }); $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); - $response = new Response(200, array(), new ReadableBodyStream($stream)); + $response = new Response(200, [], new ReadableBodyStream($stream)); // mock sender to resolve promise with the given $response in response to the given $request $sender = $this->makeSenderMock(); - $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(Promise\resolve($response)); + $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(resolve($response)); $transaction = new Transaction($sender, Loop::get()); $promise = $transaction->send($request); @@ -511,7 +514,7 @@ public function testCancelBufferingResponseWillCloseStreamAndReject() $stream->expects($this->once())->method('close'); $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); - $response = new Response(200, array(), new ReadableBodyStream($stream)); + $response = new Response(200, [], new ReadableBodyStream($stream)); // mock sender to resolve promise with the given $response in response to the given $request $deferred = new Deferred(); @@ -541,14 +544,14 @@ public function testReceivingStreamingBodyWillResolveWithStreamingResponseIfStre $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); - $response = new Response(200, array(), new ReadableBodyStream($this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock())); + $response = new Response(200, [], new ReadableBodyStream($this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock())); // mock sender to resolve promise with the given $response in response to the given $request $sender = $this->makeSenderMock(); - $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(Promise\resolve($response)); + $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(resolve($response)); $transaction = new Transaction($sender, $loop); - $transaction = $transaction->withOptions(array('streaming' => true, 'timeout' => -1)); + $transaction = $transaction->withOptions(['streaming' => true, 'timeout' => -1]); $promise = $transaction->send($request); $response = null; @@ -566,13 +569,13 @@ public function testResponseCode304WithoutLocationWillResolveWithResponseAsIs() $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); // conditional GET request will respond with 304 (Not Modified - $request = new Request('GET', 'http://example.com', array('If-None-Match' => '"abc"')); - $response = new Response(304, array('ETag' => '"abc"')); + $request = new Request('GET', 'http://example.com', ['If-None-Match' => '"abc"']); + $response = new Response(304, ['ETag' => '"abc"']); $sender = $this->makeSenderMock(); - $sender->expects($this->once())->method('send')->with($request)->willReturn(Promise\resolve($response)); + $sender->expects($this->once())->method('send')->with($request)->willReturn(resolve($response)); $transaction = new Transaction($sender, $loop); - $transaction = $transaction->withOptions(array('timeout' => -1)); + $transaction = $transaction->withOptions(['timeout' => -1]); $promise = $transaction->send($request); $promise->then($this->expectCallableOnceWith($response)); @@ -584,16 +587,16 @@ public function testCustomRedirectResponseCode333WillFollowLocationHeaderAndSend // original GET request will respond with custom 333 redirect status code and follow location header $requestOriginal = new Request('GET', 'http://example.com'); - $response = new Response(333, array('Location' => 'foo')); + $response = new Response(333, ['Location' => 'foo']); $sender = $this->makeSenderMock(); $sender->expects($this->exactly(2))->method('send')->withConsecutive( - array($requestOriginal), - array($this->callback(function (RequestInterface $request) { + [$requestOriginal], + [$this->callback(function (RequestInterface $request) { return $request->getMethod() === 'GET' && (string)$request->getUri() === 'http://example.com/foo'; - })) + })] )->willReturnOnConsecutiveCalls( - Promise\resolve($response), - new \React\Promise\Promise(function () { }) + resolve($response), + new Promise(function () { }) ); $transaction = new Transaction($sender, $loop); @@ -604,27 +607,26 @@ public function testFollowingRedirectWithSpecifiedHeaders() { $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $customHeaders = array('User-Agent' => 'Chrome'); + $customHeaders = ['User-Agent' => 'Chrome']; $requestWithUserAgent = new Request('GET', 'http://example.com', $customHeaders); $sender = $this->makeSenderMock(); // mock sender to resolve promise with the given $redirectResponse in // response to the given $requestWithUserAgent - $redirectResponse = new Response(301, array('Location' => 'http://redirect.com')); + $redirectResponse = new Response(301, ['Location' => 'http://redirect.com']); // mock sender to resolve promise with the given $okResponse in // response to the given $requestWithUserAgent $okResponse = new Response(200); - $that = $this; $sender->expects($this->exactly(2))->method('send')->withConsecutive( - array($this->anything()), - array($this->callback(function (RequestInterface $request) use ($that) { - $that->assertEquals(array('Chrome'), $request->getHeader('User-Agent')); + [$this->anything()], + [$this->callback(function (RequestInterface $request) { + $this->assertEquals(['Chrome'], $request->getHeader('User-Agent')); return true; - })) + })] )->willReturnOnConsecutiveCalls( - Promise\resolve($redirectResponse), - Promise\resolve($okResponse) + resolve($redirectResponse), + resolve($okResponse) ); $transaction = new Transaction($sender, $loop); @@ -635,27 +637,26 @@ public function testRemovingAuthorizationHeaderWhenChangingHostnamesDuringRedire { $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $customHeaders = array('Authorization' => 'secret'); + $customHeaders = ['Authorization' => 'secret']; $requestWithAuthorization = new Request('GET', 'http://example.com', $customHeaders); $sender = $this->makeSenderMock(); // mock sender to resolve promise with the given $redirectResponse in // response to the given $requestWithAuthorization - $redirectResponse = new Response(301, array('Location' => 'http://redirect.com')); + $redirectResponse = new Response(301, ['Location' => 'http://redirect.com']); // mock sender to resolve promise with the given $okResponse in // response to the given $requestWithAuthorization $okResponse = new Response(200); - $that = $this; $sender->expects($this->exactly(2))->method('send')->withConsecutive( - array($this->anything()), - array($this->callback(function (RequestInterface $request) use ($that) { - $that->assertFalse($request->hasHeader('Authorization')); + [$this->anything()], + [$this->callback(function (RequestInterface $request) { + $this->assertFalse($request->hasHeader('Authorization')); return true; - })) + })] )->willReturnOnConsecutiveCalls( - Promise\resolve($redirectResponse), - Promise\resolve($okResponse) + resolve($redirectResponse), + resolve($okResponse) ); $transaction = new Transaction($sender, $loop); @@ -666,27 +667,26 @@ public function testAuthorizationHeaderIsForwardedWhenRedirectingToSameDomain() { $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $customHeaders = array('Authorization' => 'secret'); + $customHeaders = ['Authorization' => 'secret']; $requestWithAuthorization = new Request('GET', 'http://example.com', $customHeaders); $sender = $this->makeSenderMock(); // mock sender to resolve promise with the given $redirectResponse in // response to the given $requestWithAuthorization - $redirectResponse = new Response(301, array('Location' => 'http://example.com/new')); + $redirectResponse = new Response(301, ['Location' => 'http://example.com/new']); // mock sender to resolve promise with the given $okResponse in // response to the given $requestWithAuthorization $okResponse = new Response(200); - $that = $this; $sender->expects($this->exactly(2))->method('send')->withConsecutive( - array($this->anything()), - array($this->callback(function (RequestInterface $request) use ($that) { - $that->assertEquals(array('secret'), $request->getHeader('Authorization')); + [$this->anything()], + [$this->callback(function (RequestInterface $request) { + $this->assertEquals(['secret'], $request->getHeader('Authorization')); return true; - })) + })] )->willReturnOnConsecutiveCalls( - Promise\resolve($redirectResponse), - Promise\resolve($okResponse) + resolve($redirectResponse), + resolve($okResponse) ); $transaction = new Transaction($sender, $loop); @@ -702,22 +702,21 @@ public function testAuthorizationHeaderIsForwardedWhenLocationContainsAuthentica // mock sender to resolve promise with the given $redirectResponse in // response to the given $requestWithAuthorization - $redirectResponse = new Response(301, array('Location' => 'http://user:pass@example.com/new')); + $redirectResponse = new Response(301, ['Location' => 'http://user:pass@example.com/new']); // mock sender to resolve promise with the given $okResponse in // response to the given $requestWithAuthorization $okResponse = new Response(200); - $that = $this; $sender->expects($this->exactly(2))->method('send')->withConsecutive( - array($this->anything()), - array($this->callback(function (RequestInterface $request) use ($that) { - $that->assertEquals('user:pass', $request->getUri()->getUserInfo()); - $that->assertFalse($request->hasHeader('Authorization')); + [$this->anything()], + [$this->callback(function (RequestInterface $request) { + $this->assertEquals('user:pass', $request->getUri()->getUserInfo()); + $this->assertFalse($request->hasHeader('Authorization')); return true; - })) + })] )->willReturnOnConsecutiveCalls( - Promise\resolve($redirectResponse), - Promise\resolve($okResponse) + resolve($redirectResponse), + resolve($okResponse) ); $transaction = new Transaction($sender, $loop); @@ -728,32 +727,31 @@ public function testSomeRequestHeadersShouldBeRemovedWhenRedirecting() { $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $customHeaders = array( + $customHeaders = [ 'Content-Type' => 'text/html; charset=utf-8', - 'Content-Length' => '111', - ); + 'Content-Length' => '111' + ]; $requestWithCustomHeaders = new Request('GET', 'http://example.com', $customHeaders); $sender = $this->makeSenderMock(); // mock sender to resolve promise with the given $redirectResponse in // response to the given $requestWithCustomHeaders - $redirectResponse = new Response(301, array('Location' => 'http://example.com/new')); + $redirectResponse = new Response(301, ['Location' => 'http://example.com/new']); // mock sender to resolve promise with the given $okResponse in // response to the given $requestWithCustomHeaders $okResponse = new Response(200); - $that = $this; $sender->expects($this->exactly(2))->method('send')->withConsecutive( - array($this->anything()), - array($this->callback(function (RequestInterface $request) use ($that) { - $that->assertFalse($request->hasHeader('Content-Type')); - $that->assertFalse($request->hasHeader('Content-Length')); + [$this->anything()], + [$this->callback(function (RequestInterface $request) { + $this->assertFalse($request->hasHeader('Content-Type')); + $this->assertFalse($request->hasHeader('Content-Length')); return true; - })) + })] )->willReturnOnConsecutiveCalls( - Promise\resolve($redirectResponse), - Promise\resolve($okResponse) + resolve($redirectResponse), + resolve($okResponse) ); $transaction = new Transaction($sender, $loop); @@ -764,33 +762,32 @@ public function testRequestMethodShouldBeChangedWhenRedirectingWithSeeOther() { $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $customHeaders = array( + $customHeaders = [ 'Content-Type' => 'text/html; charset=utf-8', - 'Content-Length' => '111', - ); + 'Content-Length' => '111' + ]; $request = new Request('POST', 'http://example.com', $customHeaders); $sender = $this->makeSenderMock(); // mock sender to resolve promise with the given $redirectResponse in // response to the given $request - $redirectResponse = new Response(303, array('Location' => 'http://example.com/new')); + $redirectResponse = new Response(303, ['Location' => 'http://example.com/new']); // mock sender to resolve promise with the given $okResponse in // response to the given $request $okResponse = new Response(200); - $that = $this; $sender->expects($this->exactly(2))->method('send')->withConsecutive( - array($this->anything()), - array($this->callback(function (RequestInterface $request) use ($that) { - $that->assertEquals('GET', $request->getMethod()); - $that->assertFalse($request->hasHeader('Content-Type')); - $that->assertFalse($request->hasHeader('Content-Length')); + [$this->anything()], + [$this->callback(function (RequestInterface $request) { + $this->assertEquals('GET', $request->getMethod()); + $this->assertFalse($request->hasHeader('Content-Type')); + $this->assertFalse($request->hasHeader('Content-Length')); return true; - })) + })] )->willReturnOnConsecutiveCalls( - Promise\resolve($redirectResponse), - Promise\resolve($okResponse) + resolve($redirectResponse), + resolve($okResponse) ); $transaction = new Transaction($sender, $loop); @@ -801,40 +798,39 @@ public function testRequestMethodAndBodyShouldNotBeChangedWhenRedirectingWith307 { $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $customHeaders = array( + $customHeaders = [ 'Content-Type' => 'text/html; charset=utf-8', - 'Content-Length' => '111', - ); + 'Content-Length' => '111' + ]; $request = new Request('POST', 'http://example.com', $customHeaders, '{"key":"value"}'); $sender = $this->makeSenderMock(); // mock sender to resolve promise with the given $redirectResponse in // response to the given $request - $redirectResponse = new Response(307, array('Location' => 'http://example.com/new')); + $redirectResponse = new Response(307, ['Location' => 'http://example.com/new']); // mock sender to resolve promise with the given $okResponse in // response to the given $request $okResponse = new Response(200); - $that = $this; $sender->expects($this->exactly(2))->method('send')->withConsecutive( - array($this->anything()), - array($this->callback(function (RequestInterface $request) use ($that) { - $that->assertEquals('POST', $request->getMethod()); - $that->assertEquals('{"key":"value"}', (string)$request->getBody()); - $that->assertEquals( - array( - 'Content-Type' => array('text/html; charset=utf-8'), - 'Content-Length' => array('111'), - 'Host' => array('example.com') - ), + [$this->anything()], + [$this->callback(function (RequestInterface $request) { + $this->assertEquals('POST', $request->getMethod()); + $this->assertEquals('{"key":"value"}', (string)$request->getBody()); + $this->assertEquals( + [ + 'Content-Type' => ['text/html; charset=utf-8'], + 'Content-Length' => ['111'], + 'Host' => ['example.com'] + ], $request->getHeaders() ); return true; - })) + })] )->willReturnOnConsecutiveCalls( - Promise\resolve($redirectResponse), - Promise\resolve($okResponse) + resolve($redirectResponse), + resolve($okResponse) ); $transaction = new Transaction($sender, $loop); @@ -845,10 +841,10 @@ public function testRedirectingStreamingBodyWith307Or308ShouldThrowCantRedirectS { $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $customHeaders = array( + $customHeaders = [ 'Content-Type' => 'text/html; charset=utf-8', - 'Content-Length' => '111', - ); + 'Content-Length' => '111' + ]; $stream = new ThroughStream(); $request = new Request('POST', 'http://example.com', $customHeaders, new ReadableBodyStream($stream)); @@ -856,12 +852,12 @@ public function testRedirectingStreamingBodyWith307Or308ShouldThrowCantRedirectS // mock sender to resolve promise with the given $redirectResponse in // response to the given $request - $redirectResponse = new Response(307, array('Location' => 'http://example.com/new')); + $redirectResponse = new Response(307, ['Location' => 'http://example.com/new']); $sender->expects($this->once())->method('send')->withConsecutive( - array($this->anything()) + [$this->anything()] )->willReturnOnConsecutiveCalls( - Promise\resolve($redirectResponse) + resolve($redirectResponse) ); $transaction = new Transaction($sender, $loop); @@ -883,7 +879,7 @@ public function testCancelTransactionWillCancelRequest() $request = new Request('GET', 'http://example.com'); $sender = $this->makeSenderMock(); - $pending = new \React\Promise\Promise(function () { }, $this->expectCallableOnce()); + $pending = new Promise(function () { }, $this->expectCallableOnce()); // mock sender to return pending promise which should be cancelled when cancelling result $sender->expects($this->once())->method('send')->willReturn($pending); @@ -904,13 +900,13 @@ public function testCancelTransactionWillCancelTimeoutTimer() $request = new Request('GET', 'http://example.com'); $sender = $this->makeSenderMock(); - $pending = new \React\Promise\Promise(function () { }, function () { throw new \RuntimeException(); }); + $pending = new Promise(function () { }, function () { throw new \RuntimeException(); }); // mock sender to return pending promise which should be cancelled when cancelling result $sender->expects($this->once())->method('send')->willReturn($pending); $transaction = new Transaction($sender, $loop); - $transaction = $transaction->withOptions(array('timeout' => 2)); + $transaction = $transaction->withOptions(['timeout' => 2]); $promise = $transaction->send($request); $promise->cancel(); @@ -924,16 +920,16 @@ public function testCancelTransactionWillCancelRedirectedRequest() $sender = $this->makeSenderMock(); // mock sender to resolve promise with the given $redirectResponse in - $redirectResponse = new Response(301, array('Location' => 'http://example.com/new')); + $redirectResponse = new Response(301, ['Location' => 'http://example.com/new']); - $pending = new \React\Promise\Promise(function () { }, $this->expectCallableOnce()); + $pending = new Promise(function () { }, $this->expectCallableOnce()); // mock sender to return pending promise which should be cancelled when cancelling result $sender->expects($this->exactly(2))->method('send')->withConsecutive( - array($this->anything()), - array($this->anything()) + [$this->anything()], + [$this->anything()] )->willReturnOnConsecutiveCalls( - Promise\resolve($redirectResponse), + resolve($redirectResponse), $pending ); @@ -953,12 +949,12 @@ public function testCancelTransactionWillCancelRedirectedRequestAgain() // mock sender to resolve promise with the given $redirectResponse in $first = new Deferred(); - $second = new \React\Promise\Promise(function () { }, $this->expectCallableOnce()); + $second = new Promise(function () { }, $this->expectCallableOnce()); // mock sender to return pending promise which should be cancelled when cancelling result $sender->expects($this->exactly(2))->method('send')->withConsecutive( - array($this->anything()), - array($this->anything()) + [$this->anything()], + [$this->anything()] )->willReturnOnConsecutiveCalls( $first->promise(), $second @@ -968,7 +964,7 @@ public function testCancelTransactionWillCancelRedirectedRequestAgain() $promise = $transaction->send($request); // mock sender to resolve promise with the given $redirectResponse in - $first->resolve(new Response(301, array('Location' => 'http://example.com/new'))); + $first->resolve(new Response(301, ['Location' => 'http://example.com/new'])); $promise->cancel(); } @@ -990,7 +986,7 @@ public function testCancelTransactionWillCloseBufferingStream() $transaction = new Transaction($sender, $loop); $promise = $transaction->send($request); - $redirectResponse = new Response(301, array('Location' => 'http://example.com/new'), new ReadableBodyStream($body)); + $redirectResponse = new Response(301, ['Location' => 'http://example.com/new'], new ReadableBodyStream($body)); $deferred->resolve($redirectResponse); $promise->cancel(); @@ -1013,7 +1009,7 @@ public function testCancelTransactionWillCloseBufferingStreamAgain() $body->on('close', $this->expectCallableOnce()); // mock sender to resolve promise with the given $redirectResponse in - $first->resolve(new Response(301, array('Location' => 'http://example.com/new'), new ReadableBodyStream($body))); + $first->resolve(new Response(301, ['Location' => 'http://example.com/new'], new ReadableBodyStream($body))); $promise->cancel(); } @@ -1025,16 +1021,16 @@ public function testCancelTransactionShouldCancelSendingPromise() $sender = $this->makeSenderMock(); // mock sender to resolve promise with the given $redirectResponse in - $redirectResponse = new Response(301, array('Location' => 'http://example.com/new')); + $redirectResponse = new Response(301, ['Location' => 'http://example.com/new']); - $pending = new \React\Promise\Promise(function () { }, $this->expectCallableOnce()); + $pending = new Promise(function () { }, $this->expectCallableOnce()); // mock sender to return pending promise which should be cancelled when cancelling result $sender->expects($this->exactly(2))->method('send')->withConsecutive( - array($this->anything()), - array($this->anything()) + [$this->anything()], + [$this->anything()] )->willReturnOnConsecutiveCalls( - Promise\resolve($redirectResponse), + resolve($redirectResponse), $pending ); diff --git a/tests/Io/UploadedFileTest.php b/tests/Io/UploadedFileTest.php index 4e9c0dd5..adbed51c 100644 --- a/tests/Io/UploadedFileTest.php +++ b/tests/Io/UploadedFileTest.php @@ -10,12 +10,12 @@ class UploadedFileTest extends TestCase { public function failtyErrorProvider() { - return array( - array('a'), - array(null), - array(-1), - array(9), - ); + return [ + ['a'], + [null], + [-1], + [9] + ]; } /** diff --git a/tests/Message/RequestTest.php b/tests/Message/RequestTest.php index 29baf8a7..543ddb88 100644 --- a/tests/Message/RequestTest.php +++ b/tests/Message/RequestTest.php @@ -14,7 +14,7 @@ public function testConstructWithStringRequestBodyReturnsStringBodyWithAutomatic $request = new Request( 'GET', 'http://localhost', - array(), + [], 'foo' ); @@ -28,7 +28,7 @@ public function testConstructWithStreamingRequestBodyReturnsBodyWhichImplementsR $request = new Request( 'GET', 'http://localhost', - array(), + [], new ThroughStream() ); @@ -43,7 +43,7 @@ public function testConstructWithHttpBodyStreamReturnsBodyAsIs() $request = new Request( 'GET', 'http://localhost', - array(), + [], $body = new HttpBodyStream(new ThroughStream(), 100) ); @@ -56,7 +56,7 @@ public function testConstructWithNullBodyThrows() new Request( 'GET', 'http://localhost', - array(), + [], null ); } diff --git a/tests/Message/ResponseTest.php b/tests/Message/ResponseTest.php index 1c70ae3a..61acf19e 100644 --- a/tests/Message/ResponseTest.php +++ b/tests/Message/ResponseTest.php @@ -11,7 +11,7 @@ class ResponseTest extends TestCase { public function testConstructWithStringBodyWillReturnStreamInstance() { - $response = new Response(200, array(), 'hello'); + $response = new Response(200, [], 'hello'); $body = $response->getBody(); /** @var \Psr\Http\Message\StreamInterface $body */ @@ -21,7 +21,7 @@ public function testConstructWithStringBodyWillReturnStreamInstance() public function testConstructWithStreamingBodyWillReturnReadableBodyStream() { - $response = new Response(200, array(), new ThroughStream()); + $response = new Response(200, [], new ThroughStream()); $body = $response->getBody(); /** @var \Psr\Http\Message\StreamInterface $body */ @@ -35,7 +35,7 @@ public function testConstructWithHttpBodyStreamReturnsBodyAsIs() { $response = new Response( 200, - array(), + [], $body = new HttpBodyStream(new ThroughStream(), 100) ); @@ -45,13 +45,13 @@ public function testConstructWithHttpBodyStreamReturnsBodyAsIs() public function testFloatBodyWillThrow() { $this->setExpectedException('InvalidArgumentException'); - new Response(200, array(), 1.0); + new Response(200, [], 1.0); } public function testResourceBodyWillThrow() { $this->setExpectedException('InvalidArgumentException'); - new Response(200, array(), tmpfile()); + new Response(200, [], tmpfile()); } public function testWithStatusReturnsNewInstanceWhenStatusIsChanged() @@ -99,7 +99,7 @@ public function testHtmlMethodReturnsHtmlResponse() public function testJsonMethodReturnsPrettyPrintedJsonResponse() { - $response = Response::json(array('text' => 'Hello wörld!')); + $response = Response::json(['text' => 'Hello wörld!']); $this->assertEquals(200, $response->getStatusCode()); $this->assertEquals('application/json', $response->getHeaderLine('Content-Type')); @@ -155,7 +155,7 @@ public function testParseMessageWithMinimalOkResponse() $this->assertEquals('1.1', $response->getProtocolVersion()); $this->assertEquals(200, $response->getStatusCode()); $this->assertEquals('OK', $response->getReasonPhrase()); - $this->assertEquals(array(), $response->getHeaders()); + $this->assertEquals([], $response->getHeaders()); } public function testParseMessageWithSimpleOkResponse() @@ -165,7 +165,7 @@ public function testParseMessageWithSimpleOkResponse() $this->assertEquals('1.1', $response->getProtocolVersion()); $this->assertEquals(200, $response->getStatusCode()); $this->assertEquals('OK', $response->getReasonPhrase()); - $this->assertEquals(array('Server' => array('demo')), $response->getHeaders()); + $this->assertEquals(['Server' => ['demo']], $response->getHeaders()); } public function testParseMessageWithSimpleOkResponseWithCustomReasonPhrase() @@ -175,7 +175,7 @@ public function testParseMessageWithSimpleOkResponseWithCustomReasonPhrase() $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()); + $this->assertEquals(['Server' => ['demo']], $response->getHeaders()); } public function testParseMessageWithSimpleOkResponseWithEmptyReasonPhraseAppliesDefault() @@ -185,7 +185,7 @@ public function testParseMessageWithSimpleOkResponseWithEmptyReasonPhraseApplies $this->assertEquals('1.1', $response->getProtocolVersion()); $this->assertEquals(200, $response->getStatusCode()); $this->assertEquals('OK', $response->getReasonPhrase()); - $this->assertEquals(array('Server' => array('demo')), $response->getHeaders()); + $this->assertEquals(['Server' => ['demo']], $response->getHeaders()); } public function testParseMessageWithSimpleOkResponseWithoutReasonPhraseAndWhitespaceSeparatorAppliesDefault() @@ -195,7 +195,7 @@ public function testParseMessageWithSimpleOkResponseWithoutReasonPhraseAndWhites $this->assertEquals('1.1', $response->getProtocolVersion()); $this->assertEquals(200, $response->getStatusCode()); $this->assertEquals('OK', $response->getReasonPhrase()); - $this->assertEquals(array('Server' => array('demo')), $response->getHeaders()); + $this->assertEquals(['Server' => ['demo']], $response->getHeaders()); } public function testParseMessageWithHttp10SimpleOkResponse() @@ -205,7 +205,7 @@ public function testParseMessageWithHttp10SimpleOkResponse() $this->assertEquals('1.0', $response->getProtocolVersion()); $this->assertEquals(200, $response->getStatusCode()); $this->assertEquals('OK', $response->getReasonPhrase()); - $this->assertEquals(array('Server' => array('demo')), $response->getHeaders()); + $this->assertEquals(['Server' => ['demo']], $response->getHeaders()); } public function testParseMessageWithHttp10SimpleOkResponseWithLegacyNewlines() @@ -215,7 +215,7 @@ public function testParseMessageWithHttp10SimpleOkResponseWithLegacyNewlines() $this->assertEquals('1.0', $response->getProtocolVersion()); $this->assertEquals(200, $response->getStatusCode()); $this->assertEquals('OK', $response->getReasonPhrase()); - $this->assertEquals(array('Server' => array('demo')), $response->getHeaders()); + $this->assertEquals(['Server' => ['demo']], $response->getHeaders()); } public function testParseMessageWithInvalidHttpProtocolVersion12Throws() diff --git a/tests/Message/ServerRequestTest.php b/tests/Message/ServerRequestTest.php index f82d60f8..36d20bfa 100644 --- a/tests/Message/ServerRequestTest.php +++ b/tests/Message/ServerRequestTest.php @@ -21,7 +21,7 @@ public function setUpRequest() public function testGetNoAttributes() { - $this->assertEquals(array(), $this->request->getAttributes()); + $this->assertEquals([], $this->request->getAttributes()); } public function testWithAttribute() @@ -29,7 +29,7 @@ public function testWithAttribute() $request = $this->request->withAttribute('hello', 'world'); $this->assertNotSame($request, $this->request); - $this->assertEquals(array('hello' => 'world'), $request->getAttributes()); + $this->assertEquals(['hello' => 'world'], $request->getAttributes()); } public function testGetAttribute() @@ -56,61 +56,61 @@ public function testWithoutAttribute() $request = $request->withoutAttribute('hello'); $this->assertNotSame($request, $this->request); - $this->assertEquals(array('test' => 'nice'), $request->getAttributes()); + $this->assertEquals(['test' => 'nice'], $request->getAttributes()); } public function testGetQueryParamsFromConstructorUri() { $this->request = new ServerRequest('GET', 'http://localhost/?test=world'); - $this->assertEquals(array('test' => 'world'), $this->request->getQueryParams()); + $this->assertEquals(['test' => 'world'], $this->request->getQueryParams()); } public function testWithCookieParams() { - $request = $this->request->withCookieParams(array('test' => 'world')); + $request = $this->request->withCookieParams(['test' => 'world']); $this->assertNotSame($request, $this->request); - $this->assertEquals(array('test' => 'world'), $request->getCookieParams()); + $this->assertEquals(['test' => 'world'], $request->getCookieParams()); } public function testGetQueryParamsFromConstructorUriUrlencoded() { $this->request = new ServerRequest('GET', 'http://localhost/?test=hello+world%21'); - $this->assertEquals(array('test' => 'hello world!'), $this->request->getQueryParams()); + $this->assertEquals(['test' => 'hello world!'], $this->request->getQueryParams()); } public function testWithQueryParams() { - $request = $this->request->withQueryParams(array('test' => 'world')); + $request = $this->request->withQueryParams(['test' => 'world']); $this->assertNotSame($request, $this->request); - $this->assertEquals(array('test' => 'world'), $request->getQueryParams()); + $this->assertEquals(['test' => 'world'], $request->getQueryParams()); } public function testWithQueryParamsWithoutSpecialEncoding() { - $request = $this->request->withQueryParams(array('test' => 'hello world!')); + $request = $this->request->withQueryParams(['test' => 'hello world!']); $this->assertNotSame($request, $this->request); - $this->assertEquals(array('test' => 'hello world!'), $request->getQueryParams()); + $this->assertEquals(['test' => 'hello world!'], $request->getQueryParams()); } public function testWithUploadedFiles() { - $request = $this->request->withUploadedFiles(array('test' => 'world')); + $request = $this->request->withUploadedFiles(['test' => 'world']); $this->assertNotSame($request, $this->request); - $this->assertEquals(array('test' => 'world'), $request->getUploadedFiles()); + $this->assertEquals(['test' => 'world'], $request->getUploadedFiles()); } public function testWithParsedBody() { - $request = $this->request->withParsedBody(array('test' => 'world')); + $request = $this->request->withParsedBody(['test' => 'world']); $this->assertNotSame($request, $this->request); - $this->assertEquals(array('test' => 'world'), $request->getParsedBody()); + $this->assertEquals(['test' => 'world'], $request->getParsedBody()); } public function testServerRequestParameter() @@ -119,10 +119,10 @@ public function testServerRequestParameter() $request = new ServerRequest( 'POST', 'http://127.0.0.1', - array('Content-Length' => strlen($body)), + ['Content-Length' => strlen($body)], $body, '1.0', - array('SERVER_ADDR' => '127.0.0.1') + ['SERVER_ADDR' => '127.0.0.1'] ); $serverParams = $request->getServerParams(); @@ -139,11 +139,11 @@ public function testParseSingleCookieNameValuePairWillReturnValidArray() $this->request = new ServerRequest( 'GET', 'http://localhost', - array('Cookie' => 'hello=world') + ['Cookie' => 'hello=world'] ); $cookies = $this->request->getCookieParams(); - $this->assertEquals(array('hello' => 'world'), $cookies); + $this->assertEquals(['hello' => 'world'], $cookies); } public function testParseMultipleCookieNameValuePairWillReturnValidArray() @@ -151,11 +151,11 @@ public function testParseMultipleCookieNameValuePairWillReturnValidArray() $this->request = new ServerRequest( 'GET', 'http://localhost', - array('Cookie' => 'hello=world; test=abc') + ['Cookie' => 'hello=world; test=abc'] ); $cookies = $this->request->getCookieParams(); - $this->assertEquals(array('hello' => 'world', 'test' => 'abc'), $cookies); + $this->assertEquals(['hello' => 'world', 'test' => 'abc'], $cookies); } public function testParseMultipleCookieHeadersAreNotAllowedAndWillReturnEmptyArray() @@ -163,11 +163,11 @@ public function testParseMultipleCookieHeadersAreNotAllowedAndWillReturnEmptyArr $this->request = new ServerRequest( 'GET', 'http://localhost', - array('Cookie' => array('hello=world', 'test=abc')) + ['Cookie' => ['hello=world', 'test=abc']] ); $cookies = $this->request->getCookieParams(); - $this->assertEquals(array(), $cookies); + $this->assertEquals([], $cookies); } public function testMultipleCookiesWithSameNameWillReturnLastValue() @@ -175,11 +175,11 @@ public function testMultipleCookiesWithSameNameWillReturnLastValue() $this->request = new ServerRequest( 'GET', 'http://localhost', - array('Cookie' => 'hello=world; hello=abc') + ['Cookie' => 'hello=world; hello=abc'] ); $cookies = $this->request->getCookieParams(); - $this->assertEquals(array('hello' => 'abc'), $cookies); + $this->assertEquals(['hello' => 'abc'], $cookies); } public function testOtherEqualSignsWillBeAddedToValueAndWillReturnValidArray() @@ -187,11 +187,11 @@ public function testOtherEqualSignsWillBeAddedToValueAndWillReturnValidArray() $this->request = new ServerRequest( 'GET', 'http://localhost', - array('Cookie' => 'hello=world=test=php') + ['Cookie' => 'hello=world=test=php'] ); $cookies = $this->request->getCookieParams(); - $this->assertEquals(array('hello' => 'world=test=php'), $cookies); + $this->assertEquals(['hello' => 'world=test=php'], $cookies); } public function testSingleCookieValueInCookiesReturnsEmptyArray() @@ -199,11 +199,11 @@ public function testSingleCookieValueInCookiesReturnsEmptyArray() $this->request = new ServerRequest( 'GET', 'http://localhost', - array('Cookie' => 'world') + ['Cookie' => 'world'] ); $cookies = $this->request->getCookieParams(); - $this->assertEquals(array(), $cookies); + $this->assertEquals([], $cookies); } public function testSingleMutlipleCookieValuesReturnsEmptyArray() @@ -211,11 +211,11 @@ public function testSingleMutlipleCookieValuesReturnsEmptyArray() $this->request = new ServerRequest( 'GET', 'http://localhost', - array('Cookie' => 'world; test') + ['Cookie' => 'world; test'] ); $cookies = $this->request->getCookieParams(); - $this->assertEquals(array(), $cookies); + $this->assertEquals([], $cookies); } public function testSingleValueIsValidInMultipleValueCookieWillReturnValidArray() @@ -223,11 +223,11 @@ public function testSingleValueIsValidInMultipleValueCookieWillReturnValidArray( $this->request = new ServerRequest( 'GET', 'http://localhost', - array('Cookie' => 'world; test=php') + ['Cookie' => 'world; test=php'] ); $cookies = $this->request->getCookieParams(); - $this->assertEquals(array('test' => 'php'), $cookies); + $this->assertEquals(['test' => 'php'], $cookies); } public function testUrlEncodingForValueWillReturnValidArray() @@ -235,11 +235,11 @@ public function testUrlEncodingForValueWillReturnValidArray() $this->request = new ServerRequest( 'GET', 'http://localhost', - array('Cookie' => 'hello=world%21; test=100%25%20coverage') + ['Cookie' => 'hello=world%21; test=100%25%20coverage'] ); $cookies = $this->request->getCookieParams(); - $this->assertEquals(array('hello' => 'world!', 'test' => '100% coverage'), $cookies); + $this->assertEquals(['hello' => 'world!', 'test' => '100% coverage'], $cookies); } public function testUrlEncodingForKeyWillReturnValidArray() @@ -247,11 +247,11 @@ public function testUrlEncodingForKeyWillReturnValidArray() $this->request = new ServerRequest( 'GET', 'http://localhost', - array('Cookie' => 'react%3Bphp=is%20great') + ['Cookie' => 'react%3Bphp=is%20great'] ); $cookies = $this->request->getCookieParams(); - $this->assertEquals(array('react%3Bphp' => 'is great'), $cookies); + $this->assertEquals(['react%3Bphp' => 'is great'], $cookies); } public function testCookieWithoutSpaceAfterSeparatorWillBeAccepted() @@ -259,11 +259,11 @@ public function testCookieWithoutSpaceAfterSeparatorWillBeAccepted() $this->request = new ServerRequest( 'GET', 'http://localhost', - array('Cookie' => 'hello=world;react=php') + ['Cookie' => 'hello=world;react=php'] ); $cookies = $this->request->getCookieParams(); - $this->assertEquals(array('hello' => 'world', 'react' => 'php'), $cookies); + $this->assertEquals(['hello' => 'world', 'react' => 'php'], $cookies); } public function testConstructWithStringRequestBodyReturnsStringBodyWithAutomaticSize() @@ -271,7 +271,7 @@ public function testConstructWithStringRequestBodyReturnsStringBodyWithAutomatic $request = new ServerRequest( 'GET', 'http://localhost', - array(), + [], 'foo' ); @@ -285,7 +285,7 @@ public function testConstructWithHttpBodyStreamReturnsBodyAsIs() $request = new ServerRequest( 'GET', 'http://localhost', - array(), + [], $body = new HttpBodyStream(new ThroughStream(), 100) ); @@ -297,7 +297,7 @@ public function testConstructWithStreamingRequestBodyReturnsBodyWhichImplementsR $request = new ServerRequest( 'GET', 'http://localhost', - array(), + [], new ThroughStream() ); @@ -312,9 +312,9 @@ public function testConstructWithStreamingRequestBodyReturnsBodyWithSizeFromCont $request = new ServerRequest( 'GET', 'http://localhost', - array( + [ 'Content-Length' => 100 - ), + ], new ThroughStream() ); @@ -329,9 +329,9 @@ public function testConstructWithStreamingRequestBodyReturnsBodyWithSizeUnknownF $request = new ServerRequest( 'GET', 'http://localhost', - array( + [ 'Transfer-Encoding' => 'Chunked' - ), + ], new ThroughStream() ); @@ -347,7 +347,7 @@ public function testConstructWithFloatRequestBodyThrows() new ServerRequest( 'GET', 'http://localhost', - array(), + [], 1.0 ); } @@ -358,14 +358,14 @@ public function testConstructWithResourceRequestBodyThrows() new ServerRequest( 'GET', 'http://localhost', - array(), + [], tmpfile() ); } public function testParseMessageWithSimpleGetRequest() { - $request = ServerRequest::parseMessage("GET / HTTP/1.1\r\nHost: example.com\r\n", array()); + $request = ServerRequest::parseMessage("GET / HTTP/1.1\r\nHost: example.com\r\n", []); $this->assertEquals('GET', $request->getMethod()); $this->assertEquals('http://example.com/', (string) $request->getUri()); @@ -374,7 +374,7 @@ public function testParseMessageWithSimpleGetRequest() public function testParseMessageWithHttp10RequestWithoutHost() { - $request = ServerRequest::parseMessage("GET / HTTP/1.0\r\n", array()); + $request = ServerRequest::parseMessage("GET / HTTP/1.0\r\n", []); $this->assertEquals('GET', $request->getMethod()); $this->assertEquals('http://127.0.0.1/', (string) $request->getUri()); @@ -383,7 +383,7 @@ public function testParseMessageWithHttp10RequestWithoutHost() public function testParseMessageWithOptionsMethodWithAsteriskFormRequestTarget() { - $request = ServerRequest::parseMessage("OPTIONS * HTTP/1.1\r\nHost: example.com\r\n", array()); + $request = ServerRequest::parseMessage("OPTIONS * HTTP/1.1\r\nHost: example.com\r\n", []); $this->assertEquals('OPTIONS', $request->getMethod()); $this->assertEquals('*', $request->getRequestTarget()); @@ -393,7 +393,7 @@ public function testParseMessageWithOptionsMethodWithAsteriskFormRequestTarget() public function testParseMessageWithConnectMethodWithAuthorityFormRequestTarget() { - $request = ServerRequest::parseMessage("CONNECT example.com:80 HTTP/1.1\r\nHost: example.com:80\r\n", array()); + $request = ServerRequest::parseMessage("CONNECT example.com:80 HTTP/1.1\r\nHost: example.com:80\r\n", []); $this->assertEquals('CONNECT', $request->getMethod()); $this->assertEquals('example.com:80', $request->getRequestTarget()); @@ -404,84 +404,84 @@ public function testParseMessageWithConnectMethodWithAuthorityFormRequestTarget( public function testParseMessageWithInvalidHttp11RequestWithoutHostThrows() { $this->setExpectedException('InvalidArgumentException'); - ServerRequest::parseMessage("GET / HTTP/1.1\r\n", array()); + ServerRequest::parseMessage("GET / HTTP/1.1\r\n", []); } public function testParseMessageWithInvalidHttpProtocolVersionThrows() { $this->setExpectedException('InvalidArgumentException'); - ServerRequest::parseMessage("GET / HTTP/1.2\r\n", array()); + ServerRequest::parseMessage("GET / HTTP/1.2\r\n", []); } public function testParseMessageWithInvalidProtocolThrows() { $this->setExpectedException('InvalidArgumentException'); - ServerRequest::parseMessage("GET / CUSTOM/1.1\r\n", array()); + ServerRequest::parseMessage("GET / CUSTOM/1.1\r\n", []); } public function testParseMessageWithInvalidHostHeaderWithoutValueThrows() { $this->setExpectedException('InvalidArgumentException'); - ServerRequest::parseMessage("GET / HTTP/1.1\r\nHost\r\n", array()); + ServerRequest::parseMessage("GET / HTTP/1.1\r\nHost\r\n", []); } public function testParseMessageWithInvalidHostHeaderSyntaxThrows() { $this->setExpectedException('InvalidArgumentException'); - ServerRequest::parseMessage("GET / HTTP/1.1\r\nHost: ///\r\n", array()); + ServerRequest::parseMessage("GET / HTTP/1.1\r\nHost: ///\r\n", []); } public function testParseMessageWithInvalidHostHeaderWithSchemeThrows() { $this->setExpectedException('InvalidArgumentException'); - ServerRequest::parseMessage("GET / HTTP/1.1\r\nHost: http://localhost\r\n", array()); + ServerRequest::parseMessage("GET / HTTP/1.1\r\nHost: http://localhost\r\n", []); } public function testParseMessageWithInvalidHostHeaderWithQueryThrows() { $this->setExpectedException('InvalidArgumentException'); - ServerRequest::parseMessage("GET / HTTP/1.1\r\nHost: localhost?foo\r\n", array()); + ServerRequest::parseMessage("GET / HTTP/1.1\r\nHost: localhost?foo\r\n", []); } public function testParseMessageWithInvalidHostHeaderWithFragmentThrows() { $this->setExpectedException('InvalidArgumentException'); - ServerRequest::parseMessage("GET / HTTP/1.1\r\nHost: localhost#foo\r\n", array()); + ServerRequest::parseMessage("GET / HTTP/1.1\r\nHost: localhost#foo\r\n", []); } public function testParseMessageWithInvalidContentLengthHeaderThrows() { $this->setExpectedException('InvalidArgumentException'); - ServerRequest::parseMessage("GET / HTTP/1.1\r\nHost: localhost\r\nContent-Length:\r\n", array()); + ServerRequest::parseMessage("GET / HTTP/1.1\r\nHost: localhost\r\nContent-Length:\r\n", []); } public function testParseMessageWithInvalidTransferEncodingHeaderThrows() { $this->setExpectedException('InvalidArgumentException'); - ServerRequest::parseMessage("GET / HTTP/1.1\r\nHost: localhost\r\nTransfer-Encoding:\r\n", array()); + ServerRequest::parseMessage("GET / HTTP/1.1\r\nHost: localhost\r\nTransfer-Encoding:\r\n", []); } 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()); + ServerRequest::parseMessage("GET / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 0\r\nTransfer-Encoding: chunked\r\n", []); } public function testParseMessageWithInvalidEmptyHostHeaderWithAbsoluteFormRequestTargetThrows() { $this->setExpectedException('InvalidArgumentException'); - ServerRequest::parseMessage("GET http://example.com/ HTTP/1.1\r\nHost: \r\n", array()); + ServerRequest::parseMessage("GET http://example.com/ HTTP/1.1\r\nHost: \r\n", []); } public function testParseMessageWithInvalidConnectMethodNotUsingAuthorityFormThrows() { $this->setExpectedException('InvalidArgumentException'); - ServerRequest::parseMessage("CONNECT / HTTP/1.1\r\nHost: localhost\r\n", array()); + ServerRequest::parseMessage("CONNECT / HTTP/1.1\r\nHost: localhost\r\n", []); } public function testParseMessageWithInvalidRequestTargetAsteriskFormThrows() { $this->setExpectedException('InvalidArgumentException'); - ServerRequest::parseMessage("GET * HTTP/1.1\r\nHost: localhost\r\n", array()); + ServerRequest::parseMessage("GET * HTTP/1.1\r\nHost: localhost\r\n", []); } } diff --git a/tests/Message/UriTest.php b/tests/Message/UriTest.php index 10f355df..cdbc5a87 100644 --- a/tests/Message/UriTest.php +++ b/tests/Message/UriTest.php @@ -33,95 +33,95 @@ public function testCtorWithInvalidPortThrows() public static function provideValidUris() { - return array( - array( + return [ + [ 'http://localhost' - ), - array( + ], + [ 'http://localhost/' - ), - array( + ], + [ 'http://localhost:8080/' - ), - array( + ], + [ 'http://127.0.0.1/' - ), - array( + ], + [ 'http://[::1]:8080/' - ), - array( + ], + [ 'http://localhost/path' - ), - array( + ], + [ 'http://localhost/sub/path' - ), - array( + ], + [ 'http://localhost/with%20space' - ), - array( + ], + [ 'http://localhost/with%2fslash' - ), - array( + ], + [ 'http://localhost/?name=Alice' - ), - array( + ], + [ 'http://localhost/?name=John+Doe' - ), - array( + ], + [ 'http://localhost/?name=John%20Doe' - ), - array( + ], + [ 'http://localhost/?name=Alice&age=42' - ), - array( + ], + [ 'http://localhost/?name=Alice&' - ), - array( + ], + [ 'http://localhost/?choice=A%26B' - ), - array( + ], + [ 'http://localhost/?safe=Yes!?' - ), - array( + ], + [ 'http://localhost/?alias=@home' - ), - array( + ], + [ 'http://localhost/?assign:=true' - ), - array( + ], + [ 'http://localhost/?name=' - ), - array( + ], + [ 'http://localhost/?name' - ), - array( + ], + [ '' - ), - array( + ], + [ '/' - ), - array( + ], + [ '/path' - ), - array( + ], + [ 'path' - ), - array( + ], + [ 'http://user@localhost/' - ), - array( + ], + [ 'http://user:@localhost/' - ), - array( + ], + [ 'http://:pass@localhost/' - ), - array( + ], + [ 'http://user:pass@localhost/path?query#fragment' - ), - array( + ], + [ 'http://user%20name:pass%20word@localhost/path%20name?query%20name#frag%20ment' - ) - ); + ] + ]; } /** @@ -137,36 +137,36 @@ public function testToStringReturnsOriginalUriGivenToCtor($string) public static function provideValidUrisThatWillBeTransformed() { - return array( - array( + return [ + [ 'http://localhost:8080/?', 'http://localhost:8080/' - ), - array( + ], + [ 'http://localhost:8080/#', 'http://localhost:8080/' - ), - array( + ], + [ 'http://localhost:8080/?#', 'http://localhost:8080/' - ), - array( + ], + [ 'http://@localhost:8080/', 'http://localhost:8080/' - ), - array( + ], + [ 'http://localhost:8080/?percent=50%', 'http://localhost:8080/?percent=50%25' - ), - array( + ], + [ 'http://user name:pass word@localhost/path name?query name#frag ment', 'http://user%20name:pass%20word@localhost/path%20name?query%20name#frag%20ment' - ), - array( + ], + [ 'HTTP://USER:PASS@LOCALHOST:8080/PATH?QUERY#FRAGMENT', 'http://USER:PASS@localhost:8080/PATH?QUERY#FRAGMENT' - ) - ); + ] + ]; } /** @@ -576,113 +576,113 @@ public function testWithFragmentReturnsSameInstanceWhenFragmentIsUnchangedEncode public static function provideResolveUris() { - return array( - array( + return [ + [ 'http://localhost/', '', 'http://localhost/' - ), - array( + ], + [ 'http://localhost/', 'http://example.com/', 'http://example.com/' - ), - array( + ], + [ 'http://localhost/', 'path', 'http://localhost/path' - ), - array( + ], + [ 'http://localhost/', 'path/', 'http://localhost/path/' - ), - array( + ], + [ 'http://localhost/', 'path//', 'http://localhost/path/' - ), - array( + ], + [ 'http://localhost', 'path', 'http://localhost/path' - ), - array( + ], + [ 'http://localhost/a/b', '/path', 'http://localhost/path' - ), - array( + ], + [ 'http://localhost/', '/a/b/c', 'http://localhost/a/b/c' - ), - array( + ], + [ 'http://localhost/a/path', 'b/c', 'http://localhost/a/b/c' - ), - array( + ], + [ 'http://localhost/a/path', '/b/c', 'http://localhost/b/c' - ), - array( + ], + [ 'http://localhost/a/path/', 'b/c', 'http://localhost/a/path/b/c' - ), - array( + ], + [ 'http://localhost/a/path/', '../b/c', 'http://localhost/a/b/c' - ), - array( + ], + [ 'http://localhost', '../../../a/b', 'http://localhost/a/b' - ), - array( + ], + [ 'http://localhost/path', '?query', 'http://localhost/path?query' - ), - array( + ], + [ 'http://localhost/path', '#fragment', 'http://localhost/path#fragment' - ), - array( + ], + [ 'http://localhost/path', 'http://localhost', 'http://localhost' - ), - array( + ], + [ 'http://localhost/path', 'http://localhost/?query#fragment', 'http://localhost/?query#fragment' - ), - array( + ], + [ 'http://localhost/path/?a#fragment', '?b', 'http://localhost/path/?b' - ), - array( + ], + [ 'http://localhost/path', '//localhost', 'http://localhost' - ), - array( + ], + [ 'http://localhost/path', '//localhost/a?query', 'http://localhost/a?query' - ), - array( + ], + [ 'http://localhost/path', '//LOCALHOST', 'http://localhost' - ) - ); + ] + ]; } /** diff --git a/tests/Middleware/LimitConcurrentRequestsMiddlewareTest.php b/tests/Middleware/LimitConcurrentRequestsMiddlewareTest.php index 23455e6c..b79826d6 100644 --- a/tests/Middleware/LimitConcurrentRequestsMiddlewareTest.php +++ b/tests/Middleware/LimitConcurrentRequestsMiddlewareTest.php @@ -163,7 +163,7 @@ public function testStreamDoesNotPauseOrResumeWhenBelowLimit() $body->expects($this->never())->method('pause'); $body->expects($this->never())->method('resume'); $limitHandlers = new LimitConcurrentRequestsMiddleware(1); - $limitHandlers(new ServerRequest('GET', 'https://example.com/', array(), $body), function () {}); + $limitHandlers(new ServerRequest('GET', 'https://example.com/', [], $body), function () {}); } public function testStreamDoesPauseWhenAboveLimit() @@ -177,7 +177,7 @@ public function testStreamDoesPauseWhenAboveLimit() return new Promise(function () { }); }); - $limitHandlers(new ServerRequest('GET', 'https://example.com/', array(), $body), function () {}); + $limitHandlers(new ServerRequest('GET', 'https://example.com/', [], $body), function () {}); } public function testStreamDoesPauseAndThenResumeWhenDequeued() @@ -195,7 +195,7 @@ public function testStreamDoesPauseAndThenResumeWhenDequeued() assert($promise instanceof PromiseInterface); $promise->then(null, $this->expectCallableOnce()); // avoid reporting unhandled rejection - $limitHandlers(new ServerRequest('GET', 'https://example.com/', array(), $body), function () {}); + $limitHandlers(new ServerRequest('GET', 'https://example.com/', [], $body), function () {}); $deferred->reject(new \RuntimeException()); } @@ -205,7 +205,7 @@ public function testReceivesBufferedRequestSameInstance() $request = new ServerRequest( 'POST', 'http://example.com/', - array(), + [], 'hello' ); @@ -224,7 +224,7 @@ public function testReceivesStreamingBodyRequestSameInstanceWhenBelowLimit() $request = new ServerRequest( 'POST', 'http://example.com/', - array(), + [], new HttpBodyStream($stream, 5) ); @@ -246,7 +246,7 @@ public function testReceivesRequestsSequentially() $request = new ServerRequest( 'POST', 'http://example.com/', - array(), + [], 'hello' ); @@ -261,7 +261,7 @@ public function testDoesNotReceiveNextRequestIfHandlerIsPending() $request = new ServerRequest( 'POST', 'http://example.com/', - array(), + [], 'hello' ); @@ -280,7 +280,7 @@ public function testReceivesNextRequestAfterPreviousHandlerIsSettled() $request = new ServerRequest( 'POST', 'http://example.com/', - array(), + [], 'hello' ); @@ -303,7 +303,7 @@ public function testReceivesNextRequestWhichThrowsAfterPreviousHandlerIsSettled( $request = new ServerRequest( 'POST', 'http://example.com/', - array(), + [], 'hello' ); @@ -331,7 +331,7 @@ public function testPendingRequestCanBeCancelledAndForwardsCancellationToInnerPr $request = new ServerRequest( 'POST', 'http://example.com/', - array(), + [], 'hello' ); @@ -354,7 +354,7 @@ public function testQueuedRequestCanBeCancelledBeforeItStartsProcessing() $request = new ServerRequest( 'POST', 'http://example.com/', - array(), + [], 'hello' ); @@ -376,7 +376,7 @@ public function testReceivesNextRequestAfterPreviousHandlerIsCancelled() $request = new ServerRequest( 'POST', 'http://example.com/', - array(), + [], 'hello' ); @@ -400,7 +400,7 @@ public function testRejectsWhenQueuedPromiseIsCancelled() $request = new ServerRequest( 'POST', 'http://example.com/', - array(), + [], 'hello' ); @@ -422,7 +422,7 @@ public function testDoesNotInvokeNextHandlersWhenQueuedPromiseIsCancelled() $request = new ServerRequest( 'POST', 'http://example.com/', - array(), + [], 'hello' ); @@ -445,7 +445,7 @@ public function testReceivesStreamingBodyChangesInstanceWithCustomBodyButSameDat $request = new ServerRequest( 'POST', 'http://example.com/', - array(), + [], new HttpBodyStream($stream, 5) ); @@ -494,7 +494,7 @@ public function testReceivesNextStreamingBodyWithBufferedDataAfterPreviousHandle $request = new ServerRequest( 'POST', 'http://example.com/', - array(), + [], new HttpBodyStream($stream, 10) ); @@ -524,7 +524,7 @@ public function testReceivesNextStreamingBodyAndDoesNotEmitDataIfExplicitlyClose $request = new ServerRequest( 'POST', 'http://example.com/', - array(), + [], new HttpBodyStream($stream, 10) ); @@ -555,7 +555,7 @@ public function testReceivesNextStreamingBodyAndDoesNotEmitDataIfExplicitlyPause $request = new ServerRequest( 'POST', 'http://example.com/', - array(), + [], new HttpBodyStream($stream, 10) ); @@ -586,7 +586,7 @@ public function testReceivesNextStreamingBodyAndDoesEmitDataImmediatelyIfExplici $request = new ServerRequest( 'POST', 'http://example.com/', - array(), + [], new HttpBodyStream($stream, 10) ); diff --git a/tests/Middleware/ProcessStack.php b/tests/Middleware/ProcessStack.php index 69bf34a8..22904310 100644 --- a/tests/Middleware/ProcessStack.php +++ b/tests/Middleware/ProcessStack.php @@ -3,7 +3,7 @@ namespace React\Tests\Http\Middleware; use Psr\Http\Message\ServerRequestInterface; -use React\Promise; +use function React\Promise\resolve; final class ProcessStack { @@ -15,7 +15,7 @@ final class ProcessStack public function __invoke(ServerRequestInterface $request, $stack) { $this->callCount++; - return Promise\resolve($stack($request)); + return resolve($stack($request)); } /** diff --git a/tests/Middleware/RequestBodyBufferMiddlewareTest.php b/tests/Middleware/RequestBodyBufferMiddlewareTest.php index 1c3b0b33..262ad9ca 100644 --- a/tests/Middleware/RequestBodyBufferMiddlewareTest.php +++ b/tests/Middleware/RequestBodyBufferMiddlewareTest.php @@ -11,6 +11,7 @@ use React\Http\Middleware\RequestBodyBufferMiddleware; use React\Stream\ThroughStream; use React\Tests\Http\TestCase; +use function React\Async\await; final class RequestBodyBufferMiddlewareTest extends TestCase { @@ -20,7 +21,7 @@ public function testBufferingResolvesWhenStreamEnds() $serverRequest = new ServerRequest( 'GET', 'https://example.com/', - array(), + [], new HttpBodyStream($stream, 11) ); @@ -49,7 +50,7 @@ public function testAlreadyBufferedResolvesImmediately() $serverRequest = new ServerRequest( 'GET', 'https://example.com/', - array(), + [], $stream ); @@ -72,7 +73,7 @@ public function testEmptyStreamingResolvesImmediatelyWithEmptyBufferedBody() $serverRequest = new ServerRequest( 'GET', 'https://example.com/', - array(), + [], $body = new HttpBodyStream($stream, 0) ); @@ -95,7 +96,7 @@ public function testEmptyBufferedResolvesImmediatelyWithSameBody() $serverRequest = new ServerRequest( 'GET', 'https://example.com/', - array(), + [], '' ); $body = $serverRequest->getBody(); @@ -122,7 +123,7 @@ public function testClosedStreamResolvesImmediatelyWithEmptyBody() $serverRequest = new ServerRequest( 'GET', 'https://example.com/', - array(), + [], new HttpBodyStream($stream, 2) ); @@ -145,7 +146,7 @@ public function testKnownExcessiveSizedBodyIsDiscardedAndRequestIsPassedDownToTh $serverRequest = new ServerRequest( 'GET', 'https://example.com/', - array(), + [], new HttpBodyStream($stream, 2) ); @@ -154,13 +155,13 @@ public function testKnownExcessiveSizedBodyIsDiscardedAndRequestIsPassedDownToTh $promise = $buffer( $serverRequest, function (ServerRequestInterface $request) { - return new Response(200, array(), $request->getBody()->getContents()); + return new Response(200, [], $request->getBody()->getContents()); } ); $stream->end('aa'); - $response = \React\Async\await($promise); + $response = await($promise); $this->assertSame(200, $response->getStatusCode()); $this->assertSame('', $response->getBody()->getContents()); @@ -175,15 +176,15 @@ public function testKnownExcessiveSizedWithIniLikeSize() $serverRequest = new ServerRequest( 'GET', 'https://example.com/', - array(), + [], new HttpBodyStream($stream, 2048) ); $buffer = new RequestBodyBufferMiddleware('1K'); - $response = \React\Async\await($buffer( + $response = await($buffer( $serverRequest, function (ServerRequestInterface $request) { - return new Response(200, array(), $request->getBody()->getContents()); + return new Response(200, [], $request->getBody()->getContents()); } )); @@ -196,7 +197,7 @@ public function testAlreadyBufferedExceedingSizeResolvesImmediatelyWithEmptyBody $serverRequest = new ServerRequest( 'GET', 'https://example.com/', - array(), + [], 'hello' ); @@ -219,7 +220,7 @@ public function testExcessiveSizeBodyIsDiscardedAndTheRequestIsPassedDownToTheNe $serverRequest = new ServerRequest( 'GET', 'https://example.com/', - array(), + [], new HttpBodyStream($stream, null) ); @@ -227,13 +228,13 @@ public function testExcessiveSizeBodyIsDiscardedAndTheRequestIsPassedDownToTheNe $promise = $buffer( $serverRequest, function (ServerRequestInterface $request) { - return new Response(200, array(), $request->getBody()->getContents()); + return new Response(200, [], $request->getBody()->getContents()); } ); $stream->end('aa'); - $exposedResponse = \React\Async\await($promise->then( + $exposedResponse = await($promise->then( null, $this->expectCallableNever() )); @@ -249,7 +250,7 @@ public function testBufferingRejectsWhenNextHandlerThrowsWhenStreamEnds() $serverRequest = new ServerRequest( 'GET', 'https://example.com/', - array(), + [], new HttpBodyStream($stream, null) ); @@ -284,7 +285,7 @@ public function testBufferingRejectsWhenNextHandlerThrowsErrorWhenStreamEnds() $serverRequest = new ServerRequest( 'GET', 'https://example.com/', - array(), + [], new HttpBodyStream($stream, null) ); @@ -321,7 +322,7 @@ public function testBufferingRejectsWhenStreamEmitsError() $serverRequest = new ServerRequest( 'GET', 'https://example.com/', - array(), + [], new HttpBodyStream($stream, null) ); @@ -355,7 +356,7 @@ public function testFullBodyStreamedBeforeCallingNextMiddleware() $serverRequest = new ServerRequest( 'GET', 'https://example.com/', - array(), + [], new HttpBodyStream($stream, null) ); @@ -381,7 +382,7 @@ public function testCancelBufferingClosesStreamAndRejectsPromise() $serverRequest = new ServerRequest( 'GET', 'https://example.com/', - array(), + [], new HttpBodyStream($stream, 2) ); diff --git a/tests/Middleware/RequestBodyParserMiddlewareTest.php b/tests/Middleware/RequestBodyParserMiddlewareTest.php index b588bdd5..b601b478 100644 --- a/tests/Middleware/RequestBodyParserMiddlewareTest.php +++ b/tests/Middleware/RequestBodyParserMiddlewareTest.php @@ -15,9 +15,9 @@ public function testFormUrlencodedParsing() $request = new ServerRequest( 'POST', 'https://example.com/', - array( - 'Content-Type' => 'application/x-www-form-urlencoded', - ), + [ + 'Content-Type' => 'application/x-www-form-urlencoded' + ], 'hello=world' ); @@ -30,7 +30,7 @@ function (ServerRequestInterface $request) { ); $this->assertSame( - array('hello' => 'world'), + ['hello' => 'world'], $parsedRequest->getParsedBody() ); $this->assertSame('hello=world', (string)$parsedRequest->getBody()); @@ -42,9 +42,9 @@ public function testFormUrlencodedParsingIgnoresCaseForHeadersButRespectsContent $request = new ServerRequest( 'POST', 'https://example.com/', - array( - 'CONTENT-TYPE' => 'APPLICATION/X-WWW-Form-URLEncoded', - ), + [ + 'CONTENT-TYPE' => 'APPLICATION/X-WWW-Form-URLEncoded' + ], 'Hello=World' ); @@ -57,7 +57,7 @@ function (ServerRequestInterface $request) { ); $this->assertSame( - array('Hello' => 'World'), + ['Hello' => 'World'], $parsedRequest->getParsedBody() ); $this->assertSame('Hello=World', (string)$parsedRequest->getBody()); @@ -69,9 +69,9 @@ public function testFormUrlencodedParsingNestedStructure() $request = new ServerRequest( 'POST', 'https://example.com/', - array( - 'Content-Type' => 'application/x-www-form-urlencoded', - ), + [ + 'Content-Type' => 'application/x-www-form-urlencoded' + ], 'foo=bar&baz[]=cheese&bar[]=beer&bar[]=wine&market[fish]=salmon&market[meat][]=beef&market[meat][]=chicken&market[]=bazaar' ); @@ -84,24 +84,24 @@ function (ServerRequestInterface $request) { ); $this->assertSame( - array( + [ 'foo' => 'bar', - 'baz' => array( + 'baz' => [ 'cheese', - ), - 'bar' => array( + ], + 'bar' => [ 'beer', 'wine', - ), - 'market' => array( + ], + 'market' => [ 'fish' => 'salmon', - 'meat' => array( + 'meat' => [ 'beef', 'chicken', - ), + ], 0 => 'bazaar', - ), - ), + ], + ], $parsedRequest->getParsedBody() ); $this->assertSame('foo=bar&baz[]=cheese&bar[]=beer&bar[]=wine&market[fish]=salmon&market[meat][]=beef&market[meat][]=chicken&market[]=bazaar', (string)$parsedRequest->getBody()); @@ -115,9 +115,9 @@ public function testFormUrlencodedIgnoresBodyWithExcessiveNesting() $request = new ServerRequest( 'POST', 'https://example.com/', - array( - 'Content-Type' => 'application/x-www-form-urlencoded', - ), + [ + 'Content-Type' => 'application/x-www-form-urlencoded' + ], 'hello' . str_repeat('[]', $allowed + 1) . '=world' ); @@ -130,7 +130,7 @@ function (ServerRequestInterface $request) { ); $this->assertSame( - array(), + [], $parsedRequest->getParsedBody() ); } @@ -143,9 +143,9 @@ public function testFormUrlencodedTruncatesBodyWithExcessiveLength() $request = new ServerRequest( 'POST', 'https://example.com/', - array( - 'Content-Type' => 'application/x-www-form-urlencoded', - ), + [ + 'Content-Type' => 'application/x-www-form-urlencoded' + ], str_repeat('a[]=b&', $allowed + 1) ); @@ -170,9 +170,9 @@ public function testDoesNotParseJsonByDefault() $request = new ServerRequest( 'POST', 'https://example.com/', - array( - 'Content-Type' => 'application/json', - ), + [ + 'Content-Type' => 'application/json' + ], '{"hello":"world"}' ); @@ -204,9 +204,9 @@ public function testMultipartFormDataParsing() $data .= "second\r\n"; $data .= "--$boundary--\r\n"; - $request = new ServerRequest('POST', 'http://example.com/', array( - 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, - ), $data, 1.1); + $request = new ServerRequest('POST', 'http://example.com/', [ + 'Content-Type' => 'multipart/form-data; boundary=' . $boundary + ], $data, 1.1); /** @var ServerRequestInterface $parsedRequest */ $parsedRequest = $middleware( @@ -217,12 +217,12 @@ function (ServerRequestInterface $request) { ); $this->assertSame( - array( - 'users' => array( + [ + 'users' => [ 'one' => 'single', 'two' => 'second', - ), - ), + ], + ], $parsedRequest->getParsedBody() ); $this->assertSame($data, (string)$parsedRequest->getBody()); @@ -242,9 +242,9 @@ public function testMultipartFormDataIgnoresFieldWithExcessiveNesting() $data .= "world\r\n"; $data .= "--$boundary--\r\n"; - $request = new ServerRequest('POST', 'http://example.com/', array( - 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, - ), $data, 1.1); + $request = new ServerRequest('POST', 'http://example.com/', [ + 'Content-Type' => 'multipart/form-data; boundary=' . $boundary + ], $data, 1.1); /** @var ServerRequestInterface $parsedRequest */ $parsedRequest = $middleware( @@ -274,9 +274,9 @@ public function testMultipartFormDataTruncatesBodyWithExcessiveLength() } $data .= "--$boundary--\r\n"; - $request = new ServerRequest('POST', 'http://example.com/', array( - 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, - ), $data, 1.1); + $request = new ServerRequest('POST', 'http://example.com/', [ + 'Content-Type' => 'multipart/form-data; boundary=' . $boundary + ], $data, 1.1); /** @var ServerRequestInterface $parsedRequest */ $parsedRequest = $middleware( @@ -310,9 +310,9 @@ public function testMultipartFormDataTruncatesExcessiveNumberOfEmptyFileUploads( } $data .= "--$boundary--\r\n"; - $request = new ServerRequest('POST', 'http://example.com/', array( - 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, - ), $data, 1.1); + $request = new ServerRequest('POST', 'http://example.com/', [ + 'Content-Type' => 'multipart/form-data; boundary=' . $boundary + ], $data, 1.1); /** @var ServerRequestInterface $parsedRequest */ $parsedRequest = $middleware( diff --git a/tests/TestCase.php b/tests/TestCase.php index 88d8a3df..fa6fcd1c 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -6,7 +6,7 @@ class TestCase extends BaseTestCase { - public function expectCallableOnce() // protected (PHP 5.4+) + protected function expectCallableOnce() { $mock = $this->createCallableMock(); $mock @@ -16,7 +16,7 @@ public function expectCallableOnce() // protected (PHP 5.4+) return $mock; } - public function expectCallableOnceWith($value) // protected (PHP 5.4+) + protected function expectCallableOnceWith($value) { $mock = $this->createCallableMock(); $mock @@ -27,7 +27,7 @@ public function expectCallableOnceWith($value) // protected (PHP 5.4+) return $mock; } - public function expectCallableNever() // protected (PHP 5.4+) + protected function expectCallableNever() { $mock = $this->createCallableMock(); $mock @@ -41,10 +41,10 @@ protected function createCallableMock() { if (method_exists('PHPUnit\Framework\MockObject\MockBuilder', 'addMethods')) { // PHPUnit 9+ - return $this->getMockBuilder('stdClass')->addMethods(array('__invoke'))->getMock(); + return $this->getMockBuilder('stdClass')->addMethods(['__invoke'])->getMock(); } else { // legacy PHPUnit 4 - PHPUnit 8 - return $this->getMockBuilder('stdClass')->setMethods(array('__invoke'))->getMock(); + return $this->getMockBuilder('stdClass')->setMethods(['__invoke'])->getMock(); } } diff --git a/tests/benchmark-middleware-runner.php b/tests/benchmark-middleware-runner.php index 3f1dacaf..d330a1b0 100644 --- a/tests/benchmark-middleware-runner.php +++ b/tests/benchmark-middleware-runner.php @@ -13,7 +13,7 @@ $middleware = function (ServerRequestInterface $request, $next) { return $next($request); }; -$middlewareList = array(); +$middlewareList = []; for ($i = 0; $i < MIDDLEWARE_COUNT; $i++) { $middlewareList[] = $middleware; } From 012ee77f237d62431e90e9c531c8b01200025bf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 26 May 2024 20:17:32 +0200 Subject: [PATCH 452/456] Update test suite and remove legacy PHPUnit workarounds --- composer.json | 2 +- phpunit.xml.legacy | 2 +- tests/BrowserTest.php | 215 ++--- tests/FunctionalBrowserTest.php | 46 +- tests/FunctionalHttpServerTest.php | 52 +- tests/HttpServerTest.php | 50 +- tests/Io/AbstractMessageTest.php | 18 +- tests/Io/AbstractRequestTest.php | 64 +- tests/Io/BufferedBodyTest.php | 18 +- tests/Io/ChunkedDecoderTest.php | 8 +- tests/Io/ChunkedEncoderTest.php | 8 +- tests/Io/ClientConnectionManagerTest.php | 94 +- tests/Io/ClientRequestStreamTest.php | 250 +++--- tests/Io/ClockTest.php | 5 +- tests/Io/CloseProtectionStreamTest.php | 14 +- tests/Io/EmptyBodyStreamTest.php | 15 +- tests/Io/HttpBodyStreamTest.php | 20 +- tests/Io/IniUtilTest.php | 84 +- tests/Io/LengthLimitedStreamTest.php | 8 +- tests/Io/MiddlewareRunnerTest.php | 329 ++++--- tests/Io/MultipartParserTest.php | 15 +- tests/Io/PauseBufferStreamTest.php | 9 +- tests/Io/ReadableBodyStreamTest.php | 21 +- tests/Io/RequestHeaderParserTest.php | 198 +++-- tests/Io/SenderTest.php | 90 +- tests/Io/StreamingServerTest.php | 835 ++++++++---------- tests/Io/TransactionTest.php | 233 ++--- tests/Io/UploadedFileTest.php | 21 +- tests/Message/RequestTest.php | 9 +- tests/Message/ResponseTest.php | 25 +- tests/Message/ServerRequestTest.php | 46 +- tests/Message/UriTest.php | 458 +++++----- .../LimitConcurrentRequestsMiddlewareTest.php | 19 +- .../RequestBodyBufferMiddlewareTest.php | 10 +- tests/TestCase.php | 43 +- 35 files changed, 1618 insertions(+), 1716 deletions(-) diff --git a/composer.json b/composer.json index 33919186..2fe67da0 100644 --- a/composer.json +++ b/composer.json @@ -39,7 +39,7 @@ "clue/http-proxy-react": "^1.8", "clue/reactphp-ssh-proxy": "^1.4", "clue/socks-react": "^1.4", - "phpunit/phpunit": "^9.6 || ^5.7", + "phpunit/phpunit": "^9.6 || ^7.5", "react/async": "^4 || ^3", "react/promise-stream": "^1.4", "react/promise-timer": "^1.9" diff --git a/phpunit.xml.legacy b/phpunit.xml.legacy index a018d7ab..00868603 100644 --- a/phpunit.xml.legacy +++ b/phpunit.xml.legacy @@ -2,7 +2,7 @@ diff --git a/tests/BrowserTest.php b/tests/BrowserTest.php index a7188b2c..8f3e10bd 100644 --- a/tests/BrowserTest.php +++ b/tests/BrowserTest.php @@ -3,8 +3,11 @@ namespace React\Tests\Http; use Psr\Http\Message\RequestInterface; +use React\EventLoop\LoopInterface; +use React\Http\Io\Transaction; use React\Http\Browser; use React\Promise\Promise; +use React\Socket\ConnectorInterface; class BrowserTest extends TestCase { @@ -17,8 +20,8 @@ class BrowserTest extends TestCase */ public function setUpBrowser() { - $this->loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $this->sender = $this->getMockBuilder('React\Http\Io\Transaction')->disableOriginalConstructor()->getMock(); + $this->loop = $this->createMock(LoopInterface::class); + $this->sender = $this->createMock(Transaction::class); $this->browser = new Browser(null, $this->loop); $ref = new \ReflectionProperty($this->browser, 'transaction'); @@ -38,12 +41,12 @@ public function testConstructWithoutLoopAssignsLoopAutomatically() $ref->setAccessible(true); $loop = $ref->getValue($transaction); - $this->assertInstanceOf('React\EventLoop\LoopInterface', $loop); + $this->assertInstanceOf(LoopInterface::class, $loop); } public function testConstructWithConnectorAssignsGivenConnector() { - $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $connector = $this->createMock(ConnectorInterface::class); $browser = new Browser($connector); @@ -250,108 +253,106 @@ public function testWithBase() { $browser = $this->browser->withBase('http://example.com/root'); - $this->assertInstanceOf('React\Http\Browser', $browser); + $this->assertInstanceOf(Browser::class, $browser); $this->assertNotSame($this->browser, $browser); } - public function provideOtherUris() - { - return [ - 'empty returns base' => [ - 'http://example.com/base', - '', - 'http://example.com/base', - ], - 'absolute same as base returns base' => [ - 'http://example.com/base', - 'http://example.com/base', - 'http://example.com/base', - ], - 'absolute below base returns absolute' => [ - 'http://example.com/base', - 'http://example.com/base/another', - 'http://example.com/base/another', - ], - 'slash returns base without path' => [ - 'http://example.com/base', - '/', - 'http://example.com/', - ], - 'relative is added behind base' => [ - 'http://example.com/base/', - 'test', - 'http://example.com/base/test', - ], - 'relative is added behind base without path' => [ - 'http://example.com/base', - 'test', - 'http://example.com/test', - ], - 'relative level up is added behind parent path' => [ - 'http://example.com/base/foo/', - '../bar', - 'http://example.com/base/bar', - ], - 'absolute with slash is added behind base without path' => [ - 'http://example.com/base', - '/test', - 'http://example.com/test', - ], - 'query string is added behind base' => [ - 'http://example.com/base', - '?key=value', - 'http://example.com/base?key=value', - ], - 'query string is added behind base with slash' => [ - 'http://example.com/base/', - '?key=value', - 'http://example.com/base/?key=value', - ], - 'query string with slash is added behind base without path' => [ - 'http://example.com/base', - '/?key=value', - 'http://example.com/?key=value', - ], - 'absolute with query string below base is returned as-is' => [ - 'http://example.com/base', - 'http://example.com/base?test', - 'http://example.com/base?test', - ], - 'urlencoded special chars will stay as-is' => [ - 'http://example.com/%7Bversion%7D/', - '', - 'http://example.com/%7Bversion%7D/' - ], - 'special chars will be urlencoded' => [ - 'http://example.com/{version}/', - '', - 'http://example.com/%7Bversion%7D/' - ], - 'other domain' => [ - 'http://example.com/base/', - 'http://example.org/base/', - 'http://example.org/base/' - ], - 'other scheme' => [ - 'http://example.com/base/', - 'https://example.com/base/', - 'https://example.com/base/' - ], - 'other port' => [ - 'http://example.com/base/', - 'http://example.com:81/base/', - 'http://example.com:81/base/' - ], - 'other path' => [ - 'http://example.com/base/', - 'http://example.com/other/', - 'http://example.com/other/' - ], - 'other path due to missing slash' => [ - 'http://example.com/base/', - 'http://example.com/other', - 'http://example.com/other' - ], + public static function provideOtherUris() + { + yield 'empty returns base' => [ + 'http://example.com/base', + '', + 'http://example.com/base', + ]; + yield 'absolute same as base returns base' => [ + 'http://example.com/base', + 'http://example.com/base', + 'http://example.com/base', + ]; + yield 'absolute below base returns absolute' => [ + 'http://example.com/base', + 'http://example.com/base/another', + 'http://example.com/base/another', + ]; + yield 'slash returns base without path' => [ + 'http://example.com/base', + '/', + 'http://example.com/', + ]; + yield 'relative is added behind base' => [ + 'http://example.com/base/', + 'test', + 'http://example.com/base/test', + ]; + yield 'relative is added behind base without path' => [ + 'http://example.com/base', + 'test', + 'http://example.com/test', + ]; + yield 'relative level up is added behind parent path' => [ + 'http://example.com/base/foo/', + '../bar', + 'http://example.com/base/bar', + ]; + yield 'absolute with slash is added behind base without path' => [ + 'http://example.com/base', + '/test', + 'http://example.com/test', + ]; + yield 'query string is added behind base' => [ + 'http://example.com/base', + '?key=value', + 'http://example.com/base?key=value', + ]; + yield 'query string is added behind base with slash' => [ + 'http://example.com/base/', + '?key=value', + 'http://example.com/base/?key=value', + ]; + yield 'query string with slash is added behind base without path' => [ + 'http://example.com/base', + '/?key=value', + 'http://example.com/?key=value', + ]; + yield 'absolute with query string below base is returned as-is' => [ + 'http://example.com/base', + 'http://example.com/base?test', + 'http://example.com/base?test', + ]; + yield 'urlencoded special chars will stay as-is' => [ + 'http://example.com/%7Bversion%7D/', + '', + 'http://example.com/%7Bversion%7D/' + ]; + yield 'special chars will be urlencoded' => [ + 'http://example.com/{version}/', + '', + 'http://example.com/%7Bversion%7D/' + ]; + yield 'other domain' => [ + 'http://example.com/base/', + 'http://example.org/base/', + 'http://example.org/base/' + ]; + yield 'other scheme' => [ + 'http://example.com/base/', + 'https://example.com/base/', + 'https://example.com/base/' + ]; + yield 'other port' => [ + 'http://example.com/base/', + 'http://example.com:81/base/', + 'http://example.com:81/base/' + ]; + yield 'other path' => [ + 'http://example.com/base/', + 'http://example.com/other/', + 'http://example.com/other/' + ]; + yield 'other path due to missing slash' => [ + 'http://example.com/base/', + 'http://example.com/other', + 'http://example.com/other' ]; } @@ -374,13 +375,13 @@ public function testResolveUriWithBaseEndsWithoutSlash($base, $uri, $expectedAbs public function testWithBaseUrlNotAbsoluteFails() { - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); $this->browser->withBase('hello'); } public function testWithBaseUrlInvalidSchemeFails() { - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); $this->browser->withBase('ftp://example.com'); } @@ -410,7 +411,7 @@ public function testWithProtocolVersionFollowedByGetRequestSendsRequestWithProto public function testWithProtocolVersionInvalidThrows() { - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); $this->browser->withProtocolVersion('1.2'); } @@ -418,7 +419,7 @@ public function testCancelGetRequestShouldCancelUnderlyingSocketConnection() { $pending = new Promise(function () { }, $this->expectCallableOnce()); - $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $connector = $this->createMock(ConnectorInterface::class); $connector->expects($this->once())->method('connect')->with('example.com:80')->willReturn($pending); $this->browser = new Browser($connector, $this->loop); diff --git a/tests/FunctionalBrowserTest.php b/tests/FunctionalBrowserTest.php index 92c873d1..d89d92e9 100644 --- a/tests/FunctionalBrowserTest.php +++ b/tests/FunctionalBrowserTest.php @@ -176,7 +176,8 @@ public function testGetRequestWithRelativeAddressRejects() { $promise = $this->browser->get('delay'); - $this->setExpectedException('InvalidArgumentException', 'Invalid request URL given'); + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid request URL given'); await($promise); } @@ -201,7 +202,7 @@ public function testCancelGetRequestWillRejectRequest() $promise = $this->browser->get($this->base . 'get'); $promise->cancel(); - $this->setExpectedException('RuntimeException'); + $this->expectException(\RuntimeException::class); await($promise); } @@ -212,13 +213,13 @@ public function testCancelRequestWithPromiseFollowerWillRejectRequest() }); $promise->cancel(); - $this->setExpectedException('RuntimeException'); + $this->expectException(\RuntimeException::class); await($promise); } public function testRequestWithoutAuthenticationFails() { - $this->setExpectedException('RuntimeException'); + $this->expectException(\RuntimeException::class); await($this->browser->get($this->base . 'basic-auth/user/pass')); } @@ -269,7 +270,8 @@ public function testCancelRedirectedRequestShouldReject() $promise->cancel(); }); - $this->setExpectedException('RuntimeException', 'Request cancelled'); + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Request cancelled'); await($promise); } @@ -277,7 +279,8 @@ public function testTimeoutDelayedResponseShouldReject() { $promise = $this->browser->withTimeout(0.1)->get($this->base . 'delay/10'); - $this->setExpectedException('RuntimeException', 'Request timed out after 0.1 seconds'); + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Request timed out after 0.1 seconds'); await($promise); } @@ -287,7 +290,8 @@ public function testTimeoutDelayedResponseAfterStreamingRequestShouldReject() $promise = $this->browser->withTimeout(0.1)->post($this->base . 'delay/10', [], $stream); $stream->end(); - $this->setExpectedException('RuntimeException', 'Request timed out after 0.1 seconds'); + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Request timed out after 0.1 seconds'); await($promise); } @@ -329,7 +333,7 @@ public function testFollowRedirectsZeroRejectsOnRedirect() { $browser = $this->browser->withFollowRedirects(0); - $this->setExpectedException('RuntimeException'); + $this->expectException(\RuntimeException::class); await($browser->get($this->base . 'redirect-to?url=get')); } @@ -367,11 +371,9 @@ public function testGetRequestWithResponseBufferExceededRejects() { $promise = $this->browser->withResponseBuffer(4)->get($this->base . 'get'); - $this->setExpectedException( - 'OverflowException', - 'Response body size of 5 bytes exceeds maximum of 4 bytes', - defined('SOCKET_EMSGSIZE') ? SOCKET_EMSGSIZE : 0 - ); + $this->expectException(\OverflowException::class); + $this->expectExceptionMessage('Response body size of 5 bytes exceeds maximum of 4 bytes'); + $this->expectExceptionCode(defined('SOCKET_EMSGSIZE') ? SOCKET_EMSGSIZE : 0); await($promise); } @@ -379,11 +381,9 @@ public function testGetRequestWithResponseBufferExceededDuringStreamingRejects() { $promise = $this->browser->withResponseBuffer(4)->get($this->base . 'stream/1'); - $this->setExpectedException( - 'OverflowException', - 'Response body size exceeds maximum of 4 bytes', - defined('SOCKET_EMSGSIZE') ? SOCKET_EMSGSIZE : 0 - ); + $this->expectException(\OverflowException::class); + $this->expectExceptionMessage('Response body size exceeds maximum of 4 bytes'); + $this->expectExceptionCode(defined('SOCKET_EMSGSIZE') ? SOCKET_EMSGSIZE : 0); await($promise); } @@ -409,7 +409,7 @@ public function testVerifyPeerEnabledForBadSslRejects() $browser = new Browser($connector); - $this->setExpectedException('RuntimeException'); + $this->expectException(\RuntimeException::class); await($browser->get('https://self-signed.badssl.com/')); } @@ -435,7 +435,7 @@ public function testVerifyPeerDisabledForBadSslResolves() */ public function testInvalidPort() { - $this->setExpectedException('RuntimeException'); + $this->expectException(\RuntimeException::class); await($this->browser->get('http://www.google.com:443/')); } @@ -447,7 +447,7 @@ public function testErrorStatusCodeRejectsWithResponseException() } catch (ResponseException $e) { $this->assertEquals(404, $e->getCode()); - $this->assertInstanceOf('Psr\Http\Message\ResponseInterface', $e->getResponse()); + $this->assertInstanceOf(ResponseInterface::class, $e->getResponse()); $this->assertEquals(404, $e->getResponse()->getStatusCode()); } } @@ -762,7 +762,7 @@ public function testRequestStreamingGetReceivesResponseWithStreamingBodyAndKnown $body = $response->getBody(); $this->assertEquals(5, $body->getSize()); $this->assertEquals('', (string) $body); - $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); + $this->assertInstanceOf(ReadableStreamInterface::class, $body); } public function testRequestStreamingGetReceivesResponseWithStreamingBodyAndUnknownSizeFromStreamingEndpoint() @@ -773,7 +773,7 @@ public function testRequestStreamingGetReceivesResponseWithStreamingBodyAndUnkno $body = $response->getBody(); $this->assertNull($body->getSize()); $this->assertEquals('', (string) $body); - $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); + $this->assertInstanceOf(ReadableStreamInterface::class, $body); } public function testRequestStreamingGetReceivesStreamingResponseBody() diff --git a/tests/FunctionalHttpServerTest.php b/tests/FunctionalHttpServerTest.php index c0fcfe83..dc0bd276 100644 --- a/tests/FunctionalHttpServerTest.php +++ b/tests/FunctionalHttpServerTest.php @@ -43,8 +43,8 @@ public function testPlainHttpOnRandomPort() $response = await(timeout($result, 1.0)); - $this->assertContainsString("HTTP/1.0 200 OK", $response); - $this->assertContainsString('http://' . noScheme($socket->getAddress()) . '/', $response); + $this->assertStringContainsString("HTTP/1.0 200 OK", $response); + $this->assertStringContainsString('http://' . noScheme($socket->getAddress()) . '/', $response); $socket->close(); } @@ -70,7 +70,7 @@ function () { $response = await(timeout($result, 1.0)); - $this->assertContainsString("HTTP/1.0 404 Not Found", $response); + $this->assertStringContainsString("HTTP/1.0 404 Not Found", $response); $socket->close(); } @@ -94,8 +94,8 @@ public function testPlainHttpOnRandomPortWithoutHostHeaderUsesSocketUri() $response = await(timeout($result, 1.0)); - $this->assertContainsString("HTTP/1.0 200 OK", $response); - $this->assertContainsString('http://' . noScheme($socket->getAddress()) . '/', $response); + $this->assertStringContainsString("HTTP/1.0 200 OK", $response); + $this->assertStringContainsString('http://' . noScheme($socket->getAddress()) . '/', $response); $socket->close(); } @@ -119,8 +119,8 @@ public function testPlainHttpOnRandomPortWithOtherHostHeaderTakesPrecedence() $response = await(timeout($result, 1.0)); - $this->assertContainsString("HTTP/1.0 200 OK", $response); - $this->assertContainsString('http://localhost:1000/', $response); + $this->assertStringContainsString("HTTP/1.0 200 OK", $response); + $this->assertStringContainsString('http://localhost:1000/', $response); $socket->close(); } @@ -150,8 +150,8 @@ public function testSecureHttpsOnRandomPort() $response = await(timeout($result, 1.0)); - $this->assertContainsString("HTTP/1.0 200 OK", $response); - $this->assertContainsString('https://' . noScheme($socket->getAddress()) . '/', $response); + $this->assertStringContainsString("HTTP/1.0 200 OK", $response); + $this->assertStringContainsString('https://' . noScheme($socket->getAddress()) . '/', $response); $socket->close(); } @@ -181,8 +181,8 @@ public function testSecureHttpsReturnsData() $response = await(timeout($result, 1.0)); - $this->assertContainsString("HTTP/1.0 200 OK", $response); - $this->assertContainsString("\r\nContent-Length: 33000\r\n", $response); + $this->assertStringContainsString("HTTP/1.0 200 OK", $response); + $this->assertStringContainsString("\r\nContent-Length: 33000\r\n", $response); $this->assertStringEndsWith("\r\n". str_repeat('.', 33000), $response); $socket->close(); @@ -211,8 +211,8 @@ public function testSecureHttpsOnRandomPortWithoutHostHeaderUsesSocketUri() $response = await(timeout($result, 1.0)); - $this->assertContainsString("HTTP/1.0 200 OK", $response); - $this->assertContainsString('https://' . noScheme($socket->getAddress()) . '/', $response); + $this->assertStringContainsString("HTTP/1.0 200 OK", $response); + $this->assertStringContainsString('https://' . noScheme($socket->getAddress()) . '/', $response); $socket->close(); } @@ -240,8 +240,8 @@ public function testPlainHttpOnStandardPortReturnsUriWithNoPort() $response = await(timeout($result, 1.0)); - $this->assertContainsString("HTTP/1.0 200 OK", $response); - $this->assertContainsString('http://127.0.0.1/', $response); + $this->assertStringContainsString("HTTP/1.0 200 OK", $response); + $this->assertStringContainsString('http://127.0.0.1/', $response); $socket->close(); } @@ -269,8 +269,8 @@ public function testPlainHttpOnStandardPortWithoutHostHeaderReturnsUriWithNoPort $response = await(timeout($result, 1.0)); - $this->assertContainsString("HTTP/1.0 200 OK", $response); - $this->assertContainsString('http://127.0.0.1/', $response); + $this->assertStringContainsString("HTTP/1.0 200 OK", $response); + $this->assertStringContainsString('http://127.0.0.1/', $response); $socket->close(); } @@ -303,8 +303,8 @@ public function testSecureHttpsOnStandardPortReturnsUriWithNoPort() $response = await(timeout($result, 1.0)); - $this->assertContainsString("HTTP/1.0 200 OK", $response); - $this->assertContainsString('https://127.0.0.1/', $response); + $this->assertStringContainsString("HTTP/1.0 200 OK", $response); + $this->assertStringContainsString('https://127.0.0.1/', $response); $socket->close(); } @@ -337,8 +337,8 @@ public function testSecureHttpsOnStandardPortWithoutHostHeaderUsesSocketUri() $response = await(timeout($result, 1.0)); - $this->assertContainsString("HTTP/1.0 200 OK", $response); - $this->assertContainsString('https://127.0.0.1/', $response); + $this->assertStringContainsString("HTTP/1.0 200 OK", $response); + $this->assertStringContainsString('https://127.0.0.1/', $response); $socket->close(); } @@ -366,8 +366,8 @@ public function testPlainHttpOnHttpsStandardPortReturnsUriWithPort() $response = await(timeout($result, 1.0)); - $this->assertContainsString("HTTP/1.0 200 OK", $response); - $this->assertContainsString('http://127.0.0.1:443/', $response); + $this->assertStringContainsString("HTTP/1.0 200 OK", $response); + $this->assertStringContainsString('http://127.0.0.1:443/', $response); $socket->close(); } @@ -400,8 +400,8 @@ public function testSecureHttpsOnHttpStandardPortReturnsUriWithPort() $response = await(timeout($result, 1.0)); - $this->assertContainsString("HTTP/1.0 200 OK", $response); - $this->assertContainsString('https://127.0.0.1:80/', $response); + $this->assertStringContainsString("HTTP/1.0 200 OK", $response); + $this->assertStringContainsString('https://127.0.0.1:80/', $response); $socket->close(); } @@ -743,7 +743,7 @@ function (ServerRequestInterface $request) { $responses = await(timeout(all($result), 1.0)); foreach ($responses as $response) { - $this->assertContainsString("HTTP/1.0 200 OK", $response, $response); + $this->assertStringContainsString("HTTP/1.0 200 OK", $response, $response); $this->assertTrue(substr($response, -4) == 1024, $response); } diff --git a/tests/HttpServerTest.php b/tests/HttpServerTest.php index fc977d9b..a62e5fbd 100644 --- a/tests/HttpServerTest.php +++ b/tests/HttpServerTest.php @@ -4,10 +4,14 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Loop; +use React\EventLoop\LoopInterface; use React\Http\HttpServer; use React\Http\Io\IniUtil; +use React\Http\Middleware\LimitConcurrentRequestsMiddleware; +use React\Http\Middleware\RequestBodyBufferMiddleware; use React\Http\Middleware\StreamingRequestMiddleware; use React\Promise\Deferred; +use React\Socket\Connection; use React\Stream\ReadableStreamInterface; use function React\Async\await; use function React\Promise\reject; @@ -25,7 +29,7 @@ final class HttpServerTest extends TestCase */ public function setUpConnectionMockAndSocket() { - $this->connection = $this->getMockBuilder('React\Socket\Connection') + $this->connection = $this->getMockBuilder(Connection::class) ->disableOriginalConstructor() ->setMethods( [ @@ -65,12 +69,12 @@ public function testConstructWithoutLoopAssignsLoopAutomatically() $ref->setAccessible(true); $loop = $ref->getValue($clock); - $this->assertInstanceOf('React\EventLoop\LoopInterface', $loop); + $this->assertInstanceOf(LoopInterface::class, $loop); } public function testInvalidCallbackFunctionLeadsToException() { - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); new HttpServer('invalid'); } @@ -271,8 +275,8 @@ public function testForwardErrors() $data = $this->createPostFileUploadRequest(); $this->connection->emit('data', [implode('', $data)]); - $this->assertInstanceOf('RuntimeException', $capturedException); - $this->assertInstanceOf('Exception', $capturedException->getPrevious()); + $this->assertInstanceOf(\RuntimeException::class, $capturedException); + $this->assertInstanceOf(\Exception::class, $capturedException->getPrevious()); $this->assertSame($exception, $capturedException->getPrevious()); } @@ -297,24 +301,22 @@ private function createPostFileUploadRequest() return $data; } - public function provideIniSettingsForConcurrency() + public static function provideIniSettingsForConcurrency() { - return [ - 'default settings' => [ - '128M', - '64K', // 8M capped at maximum - 1024 - ], - 'unlimited memory_limit has no concurrency limit' => [ - '-1', - '8M', - null - ], - 'small post_max_size results in high concurrency' => [ - '128M', - '1k', - 65536 - ] + yield 'default settings' => [ + '128M', + '64K', // 8M capped at maximum + 1024 + ]; + yield 'unlimited memory_limit has no concurrency limit' => [ + '-1', + '8M', + null + ]; + yield 'small post_max_size results in high concurrency' => [ + '128M', + '1k', + 65536 ]; } @@ -401,7 +403,7 @@ public function testConstructServerWithUnlimitedMemoryLimitDoesNotLimitConcurren $middleware = $ref->getValue($middlewareRunner); $this->assertTrue(is_array($middleware)); - $this->assertInstanceOf('React\Http\Middleware\RequestBodyBufferMiddleware', $middleware[0]); + $this->assertInstanceOf(RequestBodyBufferMiddleware::class, $middleware[0]); } public function testConstructServerWithMemoryLimitDoesLimitConcurrency() @@ -431,7 +433,7 @@ public function testConstructServerWithMemoryLimitDoesLimitConcurrency() $middleware = $ref->getValue($middlewareRunner); $this->assertTrue(is_array($middleware)); - $this->assertInstanceOf('React\Http\Middleware\LimitConcurrentRequestsMiddleware', $middleware[0]); + $this->assertInstanceOf(LimitConcurrentRequestsMiddleware::class, $middleware[0]); } public function testConstructFiltersOutConfigurationMiddlewareBefore() diff --git a/tests/Io/AbstractMessageTest.php b/tests/Io/AbstractMessageTest.php index 59c170ec..5451281a 100644 --- a/tests/Io/AbstractMessageTest.php +++ b/tests/Io/AbstractMessageTest.php @@ -26,7 +26,7 @@ public function testWithProtocolVersionReturnsNewInstanceWhenProtocolVersionIsCh $message = new MessageMock( '1.1', [], - $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock() + $this->createMock(StreamInterface::class) ); $new = $message->withProtocolVersion('1.0'); @@ -40,7 +40,7 @@ public function testWithProtocolVersionReturnsSameInstanceWhenProtocolVersionIsU $message = new MessageMock( '1.1', [], - $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock() + $this->createMock(StreamInterface::class) ); $new = $message->withProtocolVersion('1.1'); @@ -55,7 +55,7 @@ public function testHeaderWithStringValue() [ 'Content-Type' => 'text/plain' ], - $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock() + $this->createMock(StreamInterface::class) ); $this->assertEquals(['Content-Type' => ['text/plain']], $message->getHeaders()); @@ -109,7 +109,7 @@ public function testHeaderWithMultipleValues() 'b=2' ] ], - $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock() + $this->createMock(StreamInterface::class) ); $this->assertEquals(['Set-Cookie' => ['a=1', 'b=2']], $message->getHeaders()); @@ -152,7 +152,7 @@ public function testHeaderWithEmptyValue() [ 'Content-Type' => [] ], - $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock() + $this->createMock(StreamInterface::class) ); $this->assertEquals([], $message->getHeaders()); @@ -183,7 +183,7 @@ public function testHeaderWithMultipleValuesAcrossMixedCaseNamesInConstructorMer 'set-cookie' => ['b=2'], 'set-COOKIE' => [] ], - $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock() + $this->createMock(StreamInterface::class) ); $this->assertEquals(['set-cookie' => ['a=1', 'b=2']], $message->getHeaders()); @@ -192,14 +192,14 @@ public function testHeaderWithMultipleValuesAcrossMixedCaseNamesInConstructorMer public function testWithBodyReturnsNewInstanceWhenBodyIsChanged() { - $body = $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(); + $body = $this->createMock(StreamInterface::class); $message = new MessageMock( '1.1', [], $body ); - $body2 = $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(); + $body2 = $this->createMock(StreamInterface::class); $new = $message->withBody($body2); $this->assertNotSame($message, $new); $this->assertSame($body2, $new->getBody()); @@ -208,7 +208,7 @@ public function testWithBodyReturnsNewInstanceWhenBodyIsChanged() public function testWithBodyReturnsSameInstanceWhenBodyIsUnchanged() { - $body = $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(); + $body = $this->createMock(StreamInterface::class); $message = new MessageMock( '1.1', [], diff --git a/tests/Io/AbstractRequestTest.php b/tests/Io/AbstractRequestTest.php index 24990622..5d41369b 100644 --- a/tests/Io/AbstractRequestTest.php +++ b/tests/Io/AbstractRequestTest.php @@ -32,12 +32,12 @@ class AbstractRequestTest extends TestCase { public function testCtorWithInvalidUriThrows() { - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); new RequestMock( 'GET', null, [], - $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + $this->createMock(StreamInterface::class), '1.1' ); } @@ -48,7 +48,7 @@ public function testGetHeadersReturnsHostHeaderFromUri() 'GET', 'http://example.com/', [], - $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + $this->createMock(StreamInterface::class), '1.1' ); @@ -61,7 +61,7 @@ public function testGetHeadersReturnsHostHeaderFromUriWithCustomHttpPort() 'GET', 'http://example.com:8080/', [], - $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + $this->createMock(StreamInterface::class), '1.1' ); @@ -74,7 +74,7 @@ public function testGetHeadersReturnsHostHeaderFromUriWithCustomPortHttpOnHttpsP 'GET', 'http://example.com:443/', [], - $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + $this->createMock(StreamInterface::class), '1.1' ); @@ -87,7 +87,7 @@ public function testGetHeadersReturnsHostHeaderFromUriWithCustomPortHttpsOnHttpP 'GET', 'https://example.com:80/', [], - $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + $this->createMock(StreamInterface::class), '1.1' ); @@ -100,7 +100,7 @@ public function testGetHeadersReturnsHostHeaderFromUriWithoutDefaultHttpPort() 'GET', 'http://example.com:80/', [], - $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + $this->createMock(StreamInterface::class), '1.1' ); @@ -113,7 +113,7 @@ public function testGetHeadersReturnsHostHeaderFromUriWithoutDefaultHttpsPort() 'GET', 'https://example.com:443/', [], - $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + $this->createMock(StreamInterface::class), '1.1' ); @@ -128,7 +128,7 @@ public function testGetHeadersReturnsHostHeaderFromUriBeforeOtherHeadersExplicit [ 'User-Agent' => 'demo' ], - $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + $this->createMock(StreamInterface::class), '1.1' ); @@ -143,7 +143,7 @@ public function testGetHeadersReturnsHostHeaderFromHeadersExplicitlyGiven() [ 'Host' => 'example.com:8080' ], - $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + $this->createMock(StreamInterface::class), '1.1' ); @@ -158,7 +158,7 @@ public function testGetHeadersReturnsHostHeaderFromUriWhenHeadersExplicitlyGiven [ 'Host' => [] ], - $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + $this->createMock(StreamInterface::class), '1.1' ); @@ -171,7 +171,7 @@ public function testGetRequestTargetReturnsPathAndQueryFromUri() 'GET', 'http://example.com/demo?name=Alice', [], - $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + $this->createMock(StreamInterface::class), '1.1' ); @@ -184,7 +184,7 @@ public function testGetRequestTargetReturnsSlashOnlyIfUriHasNoPathOrQuery() 'GET', 'http://example.com', [], - $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + $this->createMock(StreamInterface::class), '1.1' ); @@ -197,7 +197,7 @@ public function testGetRequestTargetReturnsRequestTargetInAbsoluteFormIfGivenExp 'GET', 'http://example.com/demo?name=Alice', [], - $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + $this->createMock(StreamInterface::class), '1.1' ); $request = $request->withRequestTarget('http://example.com/demo?name=Alice'); @@ -211,7 +211,7 @@ public function testWithRequestTargetReturnsNewInstanceWhenRequestTargetIsChange 'GET', 'http://example.com/', [], - $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + $this->createMock(StreamInterface::class), '1.1' ); @@ -227,7 +227,7 @@ public function testWithRequestTargetReturnsSameInstanceWhenRequestTargetIsUncha 'GET', 'http://example.com/', [], - $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + $this->createMock(StreamInterface::class), '1.1' ); $request = $request->withRequestTarget('/'); @@ -243,7 +243,7 @@ public function testWithMethodReturnsNewInstanceWhenMethodIsChanged() 'GET', 'http://example.com/', [], - $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + $this->createMock(StreamInterface::class), '1.1' ); @@ -259,7 +259,7 @@ public function testWithMethodReturnsSameInstanceWhenMethodIsUnchanged() 'GET', 'http://example.com/', [], - $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + $this->createMock(StreamInterface::class), '1.1' ); @@ -270,13 +270,13 @@ public function testWithMethodReturnsSameInstanceWhenMethodIsUnchanged() public function testGetUriReturnsUriInstanceGivenToCtor() { - $uri = $this->getMockBuilder('Psr\Http\Message\UriInterface')->getMock(); + $uri = $this->createMock(UriInterface::class); $request = new RequestMock( 'GET', $uri, [], - $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + $this->createMock(StreamInterface::class), '1.1' ); @@ -289,12 +289,12 @@ public function testGetUriReturnsUriInstanceForUriStringGivenToCtor() 'GET', 'http://example.com/', [], - $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + $this->createMock(StreamInterface::class), '1.1' ); $uri = $request->getUri(); - $this->assertInstanceOf('Psr\Http\Message\UriInterface', $uri); + $this->assertInstanceOf(UriInterface::class, $uri); $this->assertEquals('http://example.com/', (string) $uri); } @@ -304,11 +304,11 @@ public function testWithUriReturnsNewInstanceWhenUriIsChanged() 'GET', 'http://example.com/', [], - $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + $this->createMock(StreamInterface::class), '1.1' ); - $uri = $this->getMockBuilder('Psr\Http\Message\UriInterface')->getMock(); + $uri = $this->createMock(UriInterface::class); $new = $request->withUri($uri); $this->assertNotSame($request, $new); @@ -318,13 +318,13 @@ public function testWithUriReturnsNewInstanceWhenUriIsChanged() public function testWithUriReturnsSameInstanceWhenUriIsUnchanged() { - $uri = $this->getMockBuilder('Psr\Http\Message\UriInterface')->getMock(); + $uri = $this->createMock(UriInterface::class); $request = new RequestMock( 'GET', $uri, [], - $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + $this->createMock(StreamInterface::class), '1.1' ); @@ -339,7 +339,7 @@ public function testWithUriReturnsNewInstanceWithHostHeaderChangedIfUriContainsH 'GET', 'http://example.com/', [], - $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + $this->createMock(StreamInterface::class), '1.1' ); @@ -357,7 +357,7 @@ public function testWithUriReturnsNewInstanceWithHostHeaderChangedIfUriContainsH 'GET', 'http://example.com/', [], - $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + $this->createMock(StreamInterface::class), '1.1' ); @@ -377,7 +377,7 @@ public function testWithUriReturnsNewInstanceWithHostHeaderAddedAsFirstHeaderBef [ 'User-Agent' => 'test' ], - $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + $this->createMock(StreamInterface::class), '1.1' ); $request = $request->withoutHeader('Host'); @@ -396,7 +396,7 @@ public function testWithUriReturnsNewInstanceWithHostHeaderUnchangedIfUriContain 'GET', 'http://example.com/', [], - $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + $this->createMock(StreamInterface::class), '1.1' ); @@ -414,7 +414,7 @@ public function testWithUriReturnsNewInstanceWithHostHeaderUnchangedIfPreserveHo 'GET', 'http://example.com/', [], - $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + $this->createMock(StreamInterface::class), '1.1' ); @@ -434,7 +434,7 @@ public function testWithUriReturnsNewInstanceWithHostHeaderAddedAsFirstHeaderNoM [ 'User-Agent' => 'test' ], - $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + $this->createMock(StreamInterface::class), '1.1' ); $request = $request->withoutHeader('Host'); diff --git a/tests/Io/BufferedBodyTest.php b/tests/Io/BufferedBodyTest.php index 4f5d042a..c8534d50 100644 --- a/tests/Io/BufferedBodyTest.php +++ b/tests/Io/BufferedBodyTest.php @@ -91,7 +91,7 @@ public function testSeekBeforeStartThrows() } catch (\RuntimeException $e) { $this->assertSame(0, $stream->tell()); - $this->setExpectedException('RuntimeException'); + $this->expectException(\RuntimeException::class); throw $e; } } @@ -100,7 +100,7 @@ public function testSeekWithInvalidModeThrows() { $stream = new BufferedBody('hello'); - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); $stream->seek(1, 12345); } @@ -109,7 +109,7 @@ public function testSeekAfterCloseThrows() $stream = new BufferedBody('hello'); $stream->close(); - $this->setExpectedException('RuntimeException'); + $this->expectException(\RuntimeException::class); $stream->seek(0); } @@ -118,7 +118,7 @@ public function testTellAfterCloseThrows() $stream = new BufferedBody('hello'); $stream->close(); - $this->setExpectedException('RuntimeException'); + $this->expectException(\RuntimeException::class); $stream->tell(); } @@ -136,7 +136,7 @@ public function testRewindAfterCloseThrows() $stream = new BufferedBody('hello'); $stream->close(); - $this->setExpectedException('RuntimeException'); + $this->expectException(\RuntimeException::class); $stream->rewind(); } @@ -180,7 +180,7 @@ public function testReadZeroThrows() { $stream = new BufferedBody('hello'); - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); $stream->read(0); } @@ -189,7 +189,7 @@ public function testReadAfterCloseThrows() $stream = new BufferedBody('hello'); $stream->close(); - $this->setExpectedException('RuntimeException'); + $this->expectException(\RuntimeException::class); $stream->read(10); } @@ -218,7 +218,7 @@ public function testGetContentsAfterCloseThrows() $stream = new BufferedBody('hello'); $stream->close(); - $this->setExpectedException('RuntimeException'); + $this->expectException(\RuntimeException::class); $stream->getContents(); } @@ -280,7 +280,7 @@ public function testWriteAfterCloseThrows() $stream = new BufferedBody('hello'); $stream->close(); - $this->setExpectedException('RuntimeException'); + $this->expectException(\RuntimeException::class); $stream->write('foo'); } diff --git a/tests/Io/ChunkedDecoderTest.php b/tests/Io/ChunkedDecoderTest.php index 5168f2d0..3ae8c742 100644 --- a/tests/Io/ChunkedDecoderTest.php +++ b/tests/Io/ChunkedDecoderTest.php @@ -3,7 +3,9 @@ namespace React\Tests\Http\Io; use React\Http\Io\ChunkedDecoder; +use React\Stream\ReadableStreamInterface; use React\Stream\ThroughStream; +use React\Stream\WritableStreamInterface; use React\Tests\Http\TestCase; class ChunkedDecoderTest extends TestCase @@ -394,7 +396,7 @@ public function testHandleError() public function testPauseStream() { - $input = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $input = $this->createMock(ReadableStreamInterface::class); $input->expects($this->once())->method('pause'); $parser = new ChunkedDecoder($input); @@ -403,7 +405,7 @@ public function testPauseStream() public function testResumeStream() { - $input = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $input = $this->createMock(ReadableStreamInterface::class); $input->expects($this->once())->method('pause'); $parser = new ChunkedDecoder($input); @@ -413,7 +415,7 @@ public function testResumeStream() public function testPipeStream() { - $dest = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + $dest = $this->createMock(WritableStreamInterface::class); $ret = $this->parser->pipe($dest); diff --git a/tests/Io/ChunkedEncoderTest.php b/tests/Io/ChunkedEncoderTest.php index 96b97848..cbb3e7ad 100644 --- a/tests/Io/ChunkedEncoderTest.php +++ b/tests/Io/ChunkedEncoderTest.php @@ -3,7 +3,9 @@ namespace React\Tests\Http\Io; use React\Http\Io\ChunkedEncoder; +use React\Stream\ReadableStreamInterface; use React\Stream\ThroughStream; +use React\Stream\WritableStreamInterface; use React\Tests\Http\TestCase; class ChunkedEncoderTest extends TestCase @@ -59,7 +61,7 @@ public function testHandleError() public function testPauseStream() { - $input = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $input = $this->createMock(ReadableStreamInterface::class); $input->expects($this->once())->method('pause'); $parser = new ChunkedEncoder($input); @@ -68,7 +70,7 @@ public function testPauseStream() public function testResumeStream() { - $input = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $input = $this->createMock(ReadableStreamInterface::class); $input->expects($this->once())->method('pause'); $parser = new ChunkedEncoder($input); @@ -78,7 +80,7 @@ public function testResumeStream() public function testPipeStream() { - $dest = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + $dest = $this->createMock(WritableStreamInterface::class); $ret = $this->chunkedStream->pipe($dest); diff --git a/tests/Io/ClientConnectionManagerTest.php b/tests/Io/ClientConnectionManagerTest.php index 88c5ff4f..c4b3a07e 100644 --- a/tests/Io/ClientConnectionManagerTest.php +++ b/tests/Io/ClientConnectionManagerTest.php @@ -2,10 +2,14 @@ namespace React\Tests\Http\Io; +use React\EventLoop\LoopInterface; +use React\EventLoop\TimerInterface; use React\Http\Io\ClientConnectionManager; use React\Http\Message\Uri; use React\Promise\Promise; use React\Promise\PromiseInterface; +use React\Socket\ConnectionInterface; +use React\Socket\ConnectorInterface; use React\Tests\Http\TestCase; use function React\Promise\resolve; @@ -14,10 +18,10 @@ class ClientConnectionManagerTest extends TestCase public function testConnectWithHttpsUriShouldConnectToTlsWithDefaultPort() { $promise = new Promise(function () { }); - $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $connector = $this->createMock(ConnectorInterface::class); $connector->expects($this->once())->method('connect')->with('tls://reactphp.org:443')->willReturn($promise); - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop = $this->createMock(LoopInterface::class); $connectionManager = new ClientConnectionManager($connector, $loop); @@ -30,10 +34,10 @@ public function testConnectWithHttpsUriShouldConnectToTlsWithDefaultPort() public function testConnectWithHttpUriShouldConnectToTcpWithDefaultPort() { $promise = new Promise(function () { }); - $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $connector = $this->createMock(ConnectorInterface::class); $connector->expects($this->once())->method('connect')->with('reactphp.org:80')->willReturn($promise); - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop = $this->createMock(LoopInterface::class); $connectionManager = new ClientConnectionManager($connector, $loop); @@ -44,10 +48,10 @@ public function testConnectWithHttpUriShouldConnectToTcpWithDefaultPort() public function testConnectWithExplicitPortShouldConnectWithGivenPort() { $promise = new Promise(function () { }); - $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $connector = $this->createMock(ConnectorInterface::class); $connector->expects($this->once())->method('connect')->with('reactphp.org:8080')->willReturn($promise); - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop = $this->createMock(LoopInterface::class); $connectionManager = new ClientConnectionManager($connector, $loop); @@ -57,10 +61,10 @@ public function testConnectWithExplicitPortShouldConnectWithGivenPort() public function testConnectWithInvalidSchemeShouldRejectWithException() { - $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $connector = $this->createMock(ConnectorInterface::class); $connector->expects($this->never())->method('connect'); - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop = $this->createMock(LoopInterface::class); $connectionManager = new ClientConnectionManager($connector, $loop); @@ -71,16 +75,16 @@ public function testConnectWithInvalidSchemeShouldRejectWithException() $exception = $reason; }); - $this->assertInstanceOf('InvalidArgumentException', $exception); + $this->assertInstanceOf(\InvalidArgumentException::class, $exception); $this->assertEquals('Invalid request URL given', $exception->getMessage()); } public function testConnectWithoutSchemeShouldRejectWithException() { - $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $connector = $this->createMock(ConnectorInterface::class); $connector->expects($this->never())->method('connect'); - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop = $this->createMock(LoopInterface::class); $connectionManager = new ClientConnectionManager($connector, $loop); @@ -91,13 +95,13 @@ public function testConnectWithoutSchemeShouldRejectWithException() $exception = $reason; }); - $this->assertInstanceOf('InvalidArgumentException', $exception); + $this->assertInstanceOf(\InvalidArgumentException::class, $exception); $this->assertEquals('Invalid request URL given', $exception->getMessage()); } public function testConnectReusesIdleConnectionFromPreviousKeepAliveCallWithoutUsingConnectorAndWillAddAndRemoveStreamEventsAndAddAndCancelIdleTimer() { - $connectionToReuse = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connectionToReuse = $this->createMock(ConnectionInterface::class); $streamHandler = null; $connectionToReuse->expects($this->exactly(3))->method('on')->withConsecutive( @@ -148,11 +152,11 @@ public function testConnectReusesIdleConnectionFromPreviousKeepAliveCallWithoutU ] ); - $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $connector = $this->createMock(ConnectorInterface::class); $connector->expects($this->never())->method('connect'); - $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $timer = $this->createMock(TimerInterface::class); + $loop = $this->createMock(LoopInterface::class); $loop->expects($this->once())->method('addTimer')->willReturn($timer); $loop->expects($this->once())->method('cancelTimer')->with($timer); @@ -173,13 +177,13 @@ public function testConnectReusesIdleConnectionFromPreviousKeepAliveCallWithoutU public function testConnectReusesIdleConnectionFromPreviousKeepAliveCallWithoutUsingConnectorAlsoWhenUriPathAndQueryAndFragmentIsDifferent() { - $connectionToReuse = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connectionToReuse = $this->createMock(ConnectionInterface::class); - $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $connector = $this->createMock(ConnectorInterface::class); $connector->expects($this->never())->method('connect'); - $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $timer = $this->createMock(TimerInterface::class); + $loop = $this->createMock(LoopInterface::class); $loop->expects($this->once())->method('addTimer')->willReturn($timer); $loop->expects($this->once())->method('cancelTimer')->with($timer); @@ -200,13 +204,13 @@ public function testConnectReusesIdleConnectionFromPreviousKeepAliveCallWithoutU public function testConnectUsesConnectorWithSameUriAndReturnsPromiseForNewConnectionFromConnectorWhenPreviousKeepAliveCallUsedDifferentUri() { - $connectionToReuse = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connectionToReuse = $this->createMock(ConnectionInterface::class); $promise = new Promise(function () { }); - $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $connector = $this->createMock(ConnectorInterface::class); $connector->expects($this->once())->method('connect')->with('tls://reactphp.org:443')->willReturn($promise); - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop = $this->createMock(LoopInterface::class); $connectionManager = new ClientConnectionManager($connector, $loop); @@ -220,14 +224,14 @@ public function testConnectUsesConnectorWithSameUriAndReturnsPromiseForNewConnec public function testConnectUsesConnectorForNewConnectionWhenPreviousConnectReusedIdleConnectionFromPreviousKeepAliveCall() { - $firstConnection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); - $secondConnection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $firstConnection = $this->createMock(ConnectionInterface::class); + $secondConnection = $this->createMock(ConnectionInterface::class); - $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $connector = $this->createMock(ConnectorInterface::class); $connector->expects($this->once())->method('connect')->with('tls://reactphp.org:443')->willReturn(resolve($secondConnection)); - $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $timer = $this->createMock(TimerInterface::class); + $loop = $this->createMock(LoopInterface::class); $loop->expects($this->once())->method('addTimer')->willReturn($timer); $loop->expects($this->once())->method('cancelTimer')->with($timer); @@ -251,12 +255,12 @@ public function testConnectUsesConnectorForNewConnectionWhenPreviousConnectReuse public function testKeepAliveAddsTimerAndDoesNotCloseConnectionImmediately() { - $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection = $this->createMock(ConnectionInterface::class); $connection->expects($this->never())->method('close'); - $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $connector = $this->createMock(ConnectorInterface::class); - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop = $this->createMock(LoopInterface::class); $loop->expects($this->once())->method('addTimer')->with(0.001, $this->anything()); $connectionManager = new ClientConnectionManager($connector, $loop); @@ -266,14 +270,14 @@ public function testKeepAliveAddsTimerAndDoesNotCloseConnectionImmediately() public function testKeepAliveClosesConnectionAfterIdleTimeout() { - $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection = $this->createMock(ConnectionInterface::class); $connection->expects($this->once())->method('close'); - $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $connector = $this->createMock(ConnectorInterface::class); $timerCallback = null; - $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $timer = $this->createMock(TimerInterface::class); + $loop = $this->createMock(LoopInterface::class); $loop->expects($this->once())->method('addTimer')->with($this->anything(), $this->callback(function ($cb) use (&$timerCallback) { $timerCallback = $cb; return true; @@ -291,18 +295,18 @@ public function testKeepAliveClosesConnectionAfterIdleTimeout() public function testConnectUsesConnectorForNewConnectionWhenIdleConnectionFromPreviousKeepAliveCallHasAlreadyTimedOut() { - $firstConnection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $firstConnection = $this->createMock(ConnectionInterface::class); $firstConnection->expects($this->once())->method('close'); - $secondConnection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $secondConnection = $this->createMock(ConnectionInterface::class); $secondConnection->expects($this->never())->method('close'); - $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $connector = $this->createMock(ConnectorInterface::class); $connector->expects($this->once())->method('connect')->with('tls://reactphp.org:443')->willReturn(resolve($secondConnection)); $timerCallback = null; - $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $timer = $this->createMock(TimerInterface::class); + $loop = $this->createMock(LoopInterface::class); $loop->expects($this->once())->method('addTimer')->with($this->anything(), $this->callback(function ($cb) use (&$timerCallback) { $timerCallback = $cb; return true; @@ -330,7 +334,7 @@ public function testConnectUsesConnectorForNewConnectionWhenIdleConnectionFromPr public function testConnectUsesConnectorForNewConnectionWhenIdleConnectionFromPreviousKeepAliveCallHasAlreadyFiredUnexpectedStreamEventBeforeIdleTimeoutThatClosesConnection() { - $firstConnection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $firstConnection = $this->createMock(ConnectionInterface::class); $firstConnection->expects($this->once())->method('close'); $streamHandler = null; @@ -358,14 +362,14 @@ public function testConnectUsesConnectorForNewConnectionWhenIdleConnectionFromPr ] ); - $secondConnection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $secondConnection = $this->createMock(ConnectionInterface::class); $secondConnection->expects($this->never())->method('close'); - $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $connector = $this->createMock(ConnectorInterface::class); $connector->expects($this->once())->method('connect')->with('tls://reactphp.org:443')->willReturn(resolve($secondConnection)); - $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $timer = $this->createMock(TimerInterface::class); + $loop = $this->createMock(LoopInterface::class); $loop->expects($this->once())->method('addTimer')->willReturn($timer); $loop->expects($this->once())->method('cancelTimer')->with($timer); diff --git a/tests/Io/ClientRequestStreamTest.php b/tests/Io/ClientRequestStreamTest.php index 0df92961..a20cda61 100644 --- a/tests/Io/ClientRequestStreamTest.php +++ b/tests/Io/ClientRequestStreamTest.php @@ -4,11 +4,15 @@ use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; +use React\EventLoop\LoopInterface; +use React\Http\Io\ClientConnectionManager; use React\Http\Io\ClientRequestStream; use React\Http\Message\Request; use React\Http\Message\Uri; use React\Promise\Deferred; use React\Promise\Promise; +use React\Socket\Connection; +use React\Socket\ConnectionInterface; use React\Stream\DuplexResourceStream; use React\Stream\ReadableStreamInterface; use React\Tests\Http\TestCase; @@ -20,10 +24,10 @@ class ClientRequestStreamTest extends TestCase /** @test */ public function testRequestShouldUseConnectionManagerWithUriFromRequestAndBindToStreamEvents() { - $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection = $this->createMock(ConnectionInterface::class); $uri = new Uri('http://www.example.com'); - $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->with($uri)->willReturn(resolve($connection)); $requestData = new Request('GET', $uri); @@ -55,13 +59,13 @@ public function testRequestShouldUseConnectionManagerWithUriFromRequestAndBindTo /** @test */ public function requestShouldEmitErrorIfConnectionFails() { - $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->willReturn(reject(new \RuntimeException())); $requestData = new Request('GET', 'http://www.example.com'); $request = new ClientRequestStream($connectionManager, $requestData); - $request->on('error', $this->expectCallableOnceWith($this->isInstanceOf('RuntimeException'))); + $request->on('error', $this->expectCallableOnceWith($this->isInstanceOf(\RuntimeException::class))); $request->on('close', $this->expectCallableOnce()); $request->end(); @@ -70,15 +74,15 @@ public function requestShouldEmitErrorIfConnectionFails() /** @test */ public function requestShouldEmitErrorIfConnectionClosesBeforeResponseIsParsed() { - $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection = $this->createMock(ConnectionInterface::class); - $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); $requestData = new Request('GET', 'http://www.example.com'); $request = new ClientRequestStream($connectionManager, $requestData); - $request->on('error', $this->expectCallableOnceWith($this->isInstanceOf('RuntimeException'))); + $request->on('error', $this->expectCallableOnceWith($this->isInstanceOf(\RuntimeException::class))); $request->on('close', $this->expectCallableOnce()); $request->end(); @@ -88,15 +92,15 @@ public function requestShouldEmitErrorIfConnectionClosesBeforeResponseIsParsed() /** @test */ public function requestShouldEmitErrorIfConnectionEmitsError() { - $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection = $this->createMock(ConnectionInterface::class); - $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); $requestData = new Request('GET', 'http://www.example.com'); $request = new ClientRequestStream($connectionManager, $requestData); - $request->on('error', $this->expectCallableOnceWith($this->isInstanceOf('Exception'))); + $request->on('error', $this->expectCallableOnceWith($this->isInstanceOf(\Exception::class))); $request->on('close', $this->expectCallableOnce()); $request->end(); @@ -107,43 +111,41 @@ public static function provideInvalidRequest() { $request = new Request('GET' , "http://localhost/"); - return [ - [ - $request->withMethod("INVA\r\nLID", '') - ], - [ - $request->withRequestTarget('/inva lid') - ], - [ - $request->withHeader('Invalid', "Yes\r\n") - ], - [ - $request->withHeader('Invalid', "Yes\n") - ], - [ - $request->withHeader('Invalid', "Yes\r") - ], - [ - $request->withHeader("Inva\r\nlid", 'Yes') - ], - [ - $request->withHeader("Inva\nlid", 'Yes') - ], - [ - $request->withHeader("Inva\rlid", 'Yes') - ], - [ - $request->withHeader('Inva Lid', 'Yes') - ], - [ - $request->withHeader('Inva:Lid', 'Yes') - ], - [ - $request->withHeader('Invalid', "Val\0ue") - ], - [ - $request->withHeader("Inva\0lid", 'Yes') - ] + yield [ + $request->withMethod("INVA\r\nLID", '') + ]; + yield [ + $request->withRequestTarget('/inva lid') + ]; + yield [ + $request->withHeader('Invalid', "Yes\r\n") + ]; + yield [ + $request->withHeader('Invalid', "Yes\n") + ]; + yield [ + $request->withHeader('Invalid', "Yes\r") + ]; + yield [ + $request->withHeader("Inva\r\nlid", 'Yes') + ]; + yield [ + $request->withHeader("Inva\nlid", 'Yes') + ]; + yield [ + $request->withHeader("Inva\rlid", 'Yes') + ]; + yield [ + $request->withHeader('Inva Lid', 'Yes') + ]; + yield [ + $request->withHeader('Inva:Lid', 'Yes') + ]; + yield [ + $request->withHeader('Invalid', "Val\0ue") + ]; + yield [ + $request->withHeader("Inva\0lid", 'Yes') ]; } @@ -153,12 +155,12 @@ public static function provideInvalidRequest() */ public function testStreamShouldEmitErrorBeforeCreatingConnectionWhenRequestIsInvalid(RequestInterface $request) { - $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->never())->method('connect'); $stream = new ClientRequestStream($connectionManager, $request); - $stream->on('error', $this->expectCallableOnceWith($this->isInstanceOf('InvalidArgumentException'))); + $stream->on('error', $this->expectCallableOnceWith($this->isInstanceOf(\InvalidArgumentException::class))); $stream->on('close', $this->expectCallableOnce()); $stream->end(); @@ -167,15 +169,15 @@ public function testStreamShouldEmitErrorBeforeCreatingConnectionWhenRequestIsIn /** @test */ public function requestShouldEmitErrorIfRequestParserThrowsException() { - $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection = $this->createMock(ConnectionInterface::class); - $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); $requestData = new Request('GET', 'http://www.example.com'); $request = new ClientRequestStream($connectionManager, $requestData); - $request->on('error', $this->expectCallableOnceWith($this->isInstanceOf('InvalidArgumentException'))); + $request->on('error', $this->expectCallableOnceWith($this->isInstanceOf(\InvalidArgumentException::class))); $request->on('close', $this->expectCallableOnce()); $request->end(); @@ -185,10 +187,10 @@ public function requestShouldEmitErrorIfRequestParserThrowsException() /** @test */ public function getRequestShouldSendAGetRequest() { - $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection = $this->createMock(ConnectionInterface::class); $connection->expects($this->once())->method('write')->with("GET / HTTP/1.0\r\nHost: www.example.com\r\n\r\n"); - $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); $requestData = new Request('GET', 'http://www.example.com', [], '', '1.0'); @@ -200,10 +202,10 @@ public function getRequestShouldSendAGetRequest() /** @test */ public function getHttp11RequestShouldSendAGetRequestWithGivenConnectionCloseHeader() { - $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection = $this->createMock(ConnectionInterface::class); $connection->expects($this->once())->method('write')->with("GET / HTTP/1.1\r\nHost: www.example.com\r\nConnection: close\r\n\r\n"); - $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); $requestData = new Request('GET', 'http://www.example.com', ['Connection' => 'close'], '', '1.1'); @@ -215,10 +217,10 @@ public function getHttp11RequestShouldSendAGetRequestWithGivenConnectionCloseHea /** @test */ public function getOptionsAsteriskShouldSendAOptionsRequestAsteriskRequestTarget() { - $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection = $this->createMock(ConnectionInterface::class); $connection->expects($this->once())->method('write')->with("OPTIONS * HTTP/1.1\r\nHost: www.example.com\r\nConnection: close\r\n\r\n"); - $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); $requestData = new Request('OPTIONS', 'http://www.example.com', ['Connection' => 'close'], '', '1.1'); @@ -230,11 +232,11 @@ public function getOptionsAsteriskShouldSendAOptionsRequestAsteriskRequestTarget public function testStreamShouldEmitResponseWithEmptyBodyWhenResponseContainsContentLengthZero() { - $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection = $this->createMock(ConnectionInterface::class); $connection->expects($this->once())->method('write')->with("GET / HTTP/1.1\r\nHost: www.example.com\r\nConnection: close\r\n\r\n"); $connection->expects($this->once())->method('close'); - $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); $requestData = new Request('GET', 'http://www.example.com', ['Connection' => 'close'], '', '1.1'); @@ -254,11 +256,11 @@ public function testStreamShouldEmitResponseWithEmptyBodyWhenResponseContainsCon public function testStreamShouldEmitResponseWithEmptyBodyWhenResponseContainsStatusNoContent() { - $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection = $this->createMock(ConnectionInterface::class); $connection->expects($this->once())->method('write')->with("GET / HTTP/1.1\r\nHost: www.example.com\r\nConnection: close\r\n\r\n"); $connection->expects($this->once())->method('close'); - $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); $requestData = new Request('GET', 'http://www.example.com', ['Connection' => 'close'], '', '1.1'); @@ -278,11 +280,11 @@ public function testStreamShouldEmitResponseWithEmptyBodyWhenResponseContainsSta public function testStreamShouldEmitResponseWithEmptyBodyWhenResponseContainsStatusNotModifiedWithContentLengthGiven() { - $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection = $this->createMock(ConnectionInterface::class); $connection->expects($this->once())->method('write')->with("GET / HTTP/1.1\r\nHost: www.example.com\r\nConnection: close\r\n\r\n"); $connection->expects($this->once())->method('close'); - $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); $requestData = new Request('GET', 'http://www.example.com', ['Connection' => 'close'], '', '1.1'); @@ -302,11 +304,11 @@ public function testStreamShouldEmitResponseWithEmptyBodyWhenResponseContainsSta public function testStreamShouldEmitResponseWithEmptyBodyWhenRequestMethodIsHead() { - $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection = $this->createMock(ConnectionInterface::class); $connection->expects($this->once())->method('write')->with("HEAD / HTTP/1.1\r\nHost: www.example.com\r\nConnection: close\r\n\r\n"); $connection->expects($this->once())->method('close'); - $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); $requestData = new Request('HEAD', 'http://www.example.com', ['Connection' => 'close'], '', '1.1'); @@ -326,11 +328,11 @@ public function testStreamShouldEmitResponseWithEmptyBodyWhenRequestMethodIsHead public function testStreamShouldEmitResponseWithStreamingBodyUntilEndWhenResponseContainsContentLengthAndResponseBody() { - $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection = $this->createMock(ConnectionInterface::class); $connection->expects($this->once())->method('write')->with("GET / HTTP/1.1\r\nHost: www.example.com\r\nConnection: close\r\n\r\n"); $connection->expects($this->once())->method('close'); - $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); $requestData = new Request('GET', 'http://www.example.com', ['Connection' => 'close'], '', '1.1'); @@ -350,11 +352,11 @@ public function testStreamShouldEmitResponseWithStreamingBodyUntilEndWhenRespons public function testStreamShouldEmitResponseWithStreamingBodyWithoutDataWhenResponseContainsContentLengthWithoutResponseBody() { - $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection = $this->createMock(ConnectionInterface::class); $connection->expects($this->once())->method('write')->with("GET / HTTP/1.1\r\nHost: www.example.com\r\nConnection: close\r\n\r\n"); $connection->expects($this->never())->method('close'); - $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); $requestData = new Request('GET', 'http://www.example.com', ['Connection' => 'close'], '', '1.1'); @@ -374,11 +376,11 @@ public function testStreamShouldEmitResponseWithStreamingBodyWithoutDataWhenResp public function testStreamShouldEmitResponseWithStreamingBodyWithDataWithoutEndWhenResponseContainsContentLengthWithIncompleteResponseBody() { - $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection = $this->createMock(ConnectionInterface::class); $connection->expects($this->once())->method('write')->with("GET / HTTP/1.1\r\nHost: www.example.com\r\nConnection: close\r\n\r\n"); $connection->expects($this->never())->method('close'); - $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); $requestData = new Request('GET', 'http://www.example.com', ['Connection' => 'close'], '', '1.1'); @@ -398,11 +400,11 @@ public function testStreamShouldEmitResponseWithStreamingBodyWithDataWithoutEndW public function testStreamShouldEmitResponseWithStreamingBodyUntilEndWhenResponseContainsTransferEncodingChunkedAndResponseBody() { - $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection = $this->createMock(ConnectionInterface::class); $connection->expects($this->once())->method('write')->with("GET / HTTP/1.1\r\nHost: www.example.com\r\nConnection: close\r\n\r\n"); $connection->expects($this->once())->method('close'); - $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); $requestData = new Request('GET', 'http://www.example.com', ['Connection' => 'close'], '', '1.1'); @@ -422,11 +424,11 @@ public function testStreamShouldEmitResponseWithStreamingBodyUntilEndWhenRespons public function testStreamShouldEmitResponseWithStreamingBodyWithoutDataWhenResponseContainsTransferEncodingChunkedWithoutResponseBody() { - $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection = $this->createMock(ConnectionInterface::class); $connection->expects($this->once())->method('write')->with("GET / HTTP/1.1\r\nHost: www.example.com\r\nConnection: close\r\n\r\n"); $connection->expects($this->never())->method('close'); - $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); $requestData = new Request('GET', 'http://www.example.com', ['Connection' => 'close'], '', '1.1'); @@ -446,11 +448,11 @@ public function testStreamShouldEmitResponseWithStreamingBodyWithoutDataWhenResp public function testStreamShouldEmitResponseWithStreamingBodyWithDataWithoutEndWhenResponseContainsTransferEncodingChunkedWithIncompleteResponseBody() { - $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection = $this->createMock(ConnectionInterface::class); $connection->expects($this->once())->method('write')->with("GET / HTTP/1.1\r\nHost: www.example.com\r\nConnection: close\r\n\r\n"); $connection->expects($this->never())->method('close'); - $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); $requestData = new Request('GET', 'http://www.example.com', ['Connection' => 'close'], '', '1.1'); @@ -470,11 +472,11 @@ public function testStreamShouldEmitResponseWithStreamingBodyWithDataWithoutEndW public function testStreamShouldEmitResponseWithStreamingBodyWithDataWithoutEndWhenResponseContainsNoContentLengthAndIncompleteResponseBody() { - $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection = $this->createMock(ConnectionInterface::class); $connection->expects($this->once())->method('write')->with("GET / HTTP/1.1\r\nHost: www.example.com\r\nConnection: close\r\n\r\n"); $connection->expects($this->never())->method('close'); - $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); $requestData = new Request('GET', 'http://www.example.com', ['Connection' => 'close'], '', '1.1'); @@ -494,7 +496,7 @@ public function testStreamShouldEmitResponseWithStreamingBodyWithDataWithoutEndW public function testStreamShouldEmitResponseWithStreamingBodyUntilEndWhenResponseContainsNoContentLengthAndResponseBodyTerminatedByConnectionEndEvent() { - $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection = $this->createMock(ConnectionInterface::class); $connection->expects($this->once())->method('write')->with("GET / HTTP/1.1\r\nHost: www.example.com\r\nConnection: close\r\n\r\n"); $connection->expects($this->once())->method('close'); @@ -510,7 +512,7 @@ public function testStreamShouldEmitResponseWithStreamingBodyUntilEndWhenRespons return true; })); - $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); $requestData = new Request('GET', 'http://www.example.com', ['Connection' => 'close'], '', '1.1'); @@ -533,12 +535,12 @@ public function testStreamShouldEmitResponseWithStreamingBodyUntilEndWhenRespons public function testStreamShouldReuseConnectionForHttp11ByDefault() { - $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection = $this->createMock(ConnectionInterface::class); $connection->expects($this->once())->method('write')->with("GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n"); $connection->expects($this->once())->method('isReadable')->willReturn(true); $connection->expects($this->never())->method('close'); - $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); $connectionManager->expects($this->once())->method('keepAlive')->with(new Uri('http://www.example.com'), $connection); @@ -554,12 +556,12 @@ public function testStreamShouldReuseConnectionForHttp11ByDefault() public function testStreamShouldNotReuseConnectionWhenResponseContainsConnectionClose() { - $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection = $this->createMock(ConnectionInterface::class); $connection->expects($this->once())->method('write')->with("GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n"); $connection->expects($this->once())->method('isReadable')->willReturn(true); $connection->expects($this->once())->method('close'); - $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); $requestData = new Request('GET', 'http://www.example.com', [], '', '1.1'); @@ -574,12 +576,12 @@ public function testStreamShouldNotReuseConnectionWhenResponseContainsConnection public function testStreamShouldNotReuseConnectionWhenRequestContainsConnectionCloseWithAdditionalOptions() { - $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection = $this->createMock(ConnectionInterface::class); $connection->expects($this->once())->method('write')->with("GET / HTTP/1.1\r\nHost: www.example.com\r\nConnection: FOO, CLOSE, BAR\r\n\r\n"); $connection->expects($this->once())->method('isReadable')->willReturn(true); $connection->expects($this->once())->method('close'); - $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); $requestData = new Request('GET', 'http://www.example.com', ['Connection' => 'FOO, CLOSE, BAR'], '', '1.1'); @@ -594,12 +596,12 @@ public function testStreamShouldNotReuseConnectionWhenRequestContainsConnectionC public function testStreamShouldNotReuseConnectionForHttp10ByDefault() { - $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection = $this->createMock(ConnectionInterface::class); $connection->expects($this->once())->method('write')->with("GET / HTTP/1.0\r\nHost: www.example.com\r\n\r\n"); $connection->expects($this->once())->method('isReadable')->willReturn(true); $connection->expects($this->once())->method('close'); - $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); $requestData = new Request('GET', 'http://www.example.com', [], '', '1.0'); @@ -614,12 +616,12 @@ public function testStreamShouldNotReuseConnectionForHttp10ByDefault() public function testStreamShouldReuseConnectionForHttp10WhenBothRequestAndResponseContainConnectionKeepAlive() { - $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection = $this->createMock(ConnectionInterface::class); $connection->expects($this->once())->method('write')->with("GET / HTTP/1.0\r\nHost: www.example.com\r\nConnection: keep-alive\r\n\r\n"); $connection->expects($this->once())->method('isReadable')->willReturn(true); $connection->expects($this->never())->method('close'); - $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); $connectionManager->expects($this->once())->method('keepAlive')->with(new Uri('http://www.example.com'), $connection); @@ -635,12 +637,12 @@ public function testStreamShouldReuseConnectionForHttp10WhenBothRequestAndRespon public function testStreamShouldReuseConnectionForHttp10WhenBothRequestAndResponseContainConnectionKeepAliveWithAdditionalOptions() { - $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection = $this->createMock(ConnectionInterface::class); $connection->expects($this->once())->method('write')->with("GET / HTTP/1.0\r\nHost: www.example.com\r\nConnection: FOO, KEEP-ALIVE, BAR\r\n\r\n"); $connection->expects($this->once())->method('isReadable')->willReturn(true); $connection->expects($this->never())->method('close'); - $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); $connectionManager->expects($this->once())->method('keepAlive')->with(new Uri('http://www.example.com'), $connection); @@ -656,7 +658,7 @@ public function testStreamShouldReuseConnectionForHttp10WhenBothRequestAndRespon public function testStreamShouldNotReuseConnectionWhenResponseContainsNoContentLengthAndResponseBodyTerminatedByConnectionEndEvent() { - $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection = $this->createMock(ConnectionInterface::class); $connection->expects($this->once())->method('write')->with("GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n"); $connection->expects($this->once())->method('isReadable')->willReturn(false); $connection->expects($this->once())->method('close'); @@ -673,7 +675,7 @@ public function testStreamShouldNotReuseConnectionWhenResponseContainsNoContentL return true; })); - $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); $requestData = new Request('GET', 'http://www.example.com', [], '', '1.1'); @@ -691,7 +693,7 @@ public function testStreamShouldNotReuseConnectionWhenResponseContainsNoContentL public function testStreamShouldNotReuseConnectionWhenResponseContainsContentLengthButIsTerminatedByUnexpectedCloseEvent() { - $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection = $this->createMock(ConnectionInterface::class); $connection->expects($this->once())->method('write')->with("GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n"); $connection->expects($this->atMost(1))->method('isReadable')->willReturn(false); $connection->expects($this->once())->method('close'); @@ -708,7 +710,7 @@ public function testStreamShouldNotReuseConnectionWhenResponseContainsContentLen return true; })); - $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); $requestData = new Request('GET', 'http://www.example.com', [], '', '1.1'); @@ -726,12 +728,12 @@ public function testStreamShouldNotReuseConnectionWhenResponseContainsContentLen public function testStreamShouldReuseConnectionWhenResponseContainsTransferEncodingChunkedAndResponseBody() { - $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection = $this->createMock(ConnectionInterface::class); $connection->expects($this->once())->method('write')->with("GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n"); $connection->expects($this->once())->method('isReadable')->willReturn(true); $connection->expects($this->never())->method('close'); - $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); $connectionManager->expects($this->once())->method('keepAlive')->with(new Uri('http://www.example.com'), $connection); @@ -747,12 +749,12 @@ public function testStreamShouldReuseConnectionWhenResponseContainsTransferEncod public function testStreamShouldNotReuseConnectionWhenResponseContainsTransferEncodingChunkedAndResponseBodyContainsInvalidData() { - $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection = $this->createMock(ConnectionInterface::class); $connection->expects($this->once())->method('write')->with("GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n"); $connection->expects($this->atMost(1))->method('isReadable')->willReturn(true); $connection->expects($this->once())->method('close'); - $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); $requestData = new Request('GET', 'http://www.example.com', [], '', '1.1'); @@ -768,10 +770,10 @@ public function testStreamShouldNotReuseConnectionWhenResponseContainsTransferEn /** @test */ public function postRequestShouldSendAPostRequest() { - $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection = $this->createMock(ConnectionInterface::class); $connection->expects($this->once())->method('write')->with($this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\n\r\nsome post data$#")); - $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); $requestData = new Request('POST', 'http://www.example.com', [], '', '1.0'); @@ -787,14 +789,14 @@ public function postRequestShouldSendAPostRequest() /** @test */ public function writeWithAPostRequestShouldSendToTheStream() { - $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection = $this->createMock(ConnectionInterface::class); $connection->expects($this->exactly(3))->method('write')->withConsecutive( [$this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\n\r\nsome$#")], [$this->identicalTo("post")], [$this->identicalTo("data")] ); - $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); $requestData = new Request('POST', 'http://www.example.com', [], '', '1.0'); @@ -812,7 +814,7 @@ public function writeWithAPostRequestShouldSendToTheStream() /** @test */ public function writeWithAPostRequestShouldSendBodyAfterHeadersAndEmitDrainEvent() { - $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection = $this->createMock(ConnectionInterface::class); $connection->expects($this->exactly(2))->method('write')->withConsecutive( [$this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\n\r\nsomepost$#")], [$this->identicalTo("data")] @@ -821,7 +823,7 @@ public function writeWithAPostRequestShouldSendBodyAfterHeadersAndEmitDrainEvent ); $deferred = new Deferred(); - $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->willReturn($deferred->promise()); $requestData = new Request('POST', 'http://www.example.com', [], '', '1.0'); @@ -846,7 +848,7 @@ public function writeWithAPostRequestShouldSendBodyAfterHeadersAndEmitDrainEvent /** @test */ public function writeWithAPostRequestShouldForwardDrainEventIfFirstChunkExceedsBuffer() { - $connection = $this->getMockBuilder('React\Socket\Connection') + $connection = $this->getMockBuilder(Connection::class) ->disableOriginalConstructor() ->setMethods(['write']) ->getMock(); @@ -859,7 +861,7 @@ public function writeWithAPostRequestShouldForwardDrainEventIfFirstChunkExceedsB ); $deferred = new Deferred(); - $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->willReturn($deferred->promise()); $requestData = new Request('POST', 'http://www.example.com', [], '', '1.0'); @@ -885,22 +887,20 @@ public function writeWithAPostRequestShouldForwardDrainEventIfFirstChunkExceedsB /** @test */ public function pipeShouldPipeDataIntoTheRequestBody() { - $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection = $this->createMock(ConnectionInterface::class); $connection->expects($this->exactly(3))->method('write')->withConsecutive( [$this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\n\r\nsome$#")], [$this->identicalTo("post")], [$this->identicalTo("data")] ); - $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); $requestData = new Request('POST', 'http://www.example.com', [], '', '1.0'); $request = new ClientRequestStream($connectionManager, $requestData); - $loop = $this - ->getMockBuilder('React\EventLoop\LoopInterface') - ->getMock(); + $loop = $this->createMock(LoopInterface::class); $stream = fopen('php://memory', 'r+'); $stream = new DuplexResourceStream($stream, $loop); @@ -920,7 +920,7 @@ public function pipeShouldPipeDataIntoTheRequestBody() */ public function writeShouldStartConnecting() { - $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->willReturn(new Promise(function () { })); $requestData = new Request('POST', 'http://www.example.com'); @@ -934,7 +934,7 @@ public function writeShouldStartConnecting() */ public function endShouldStartConnectingAndChangeStreamIntoNonWritableMode() { - $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->willReturn(new Promise(function () { })); $requestData = new Request('POST', 'http://www.example.com'); @@ -950,7 +950,7 @@ public function endShouldStartConnectingAndChangeStreamIntoNonWritableMode() */ public function closeShouldEmitCloseEvent() { - $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager = $this->createMock(ClientConnectionManager::class); $requestData = new Request('POST', 'http://www.example.com'); $request = new ClientRequestStream($connectionManager, $requestData); @@ -964,7 +964,7 @@ public function closeShouldEmitCloseEvent() */ public function writeAfterCloseReturnsFalse() { - $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager = $this->createMock(ClientConnectionManager::class); $requestData = new Request('POST', 'http://www.example.com'); $request = new ClientRequestStream($connectionManager, $requestData); @@ -980,7 +980,7 @@ public function writeAfterCloseReturnsFalse() */ public function endAfterCloseIsNoOp() { - $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->never())->method('connect'); $requestData = new Request('POST', 'http://www.example.com'); @@ -998,7 +998,7 @@ public function closeShouldCancelPendingConnectionAttempt() $promise = new Promise(function () {}, function () { throw new \RuntimeException(); }); - $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->willReturn($promise); $requestData = new Request('POST', 'http://www.example.com'); @@ -1016,7 +1016,7 @@ public function closeShouldCancelPendingConnectionAttempt() /** @test */ public function requestShouldRemoveAllListenerAfterClosed() { - $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager = $this->createMock(ClientConnectionManager::class); $requestData = new Request('GET', 'http://www.example.com'); $request = new ClientRequestStream($connectionManager, $requestData); @@ -1031,9 +1031,9 @@ public function requestShouldRemoveAllListenerAfterClosed() /** @test */ public function multivalueHeader() { - $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection = $this->createMock(ConnectionInterface::class); - $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); $requestData = new Request('GET', 'http://www.example.com'); @@ -1054,7 +1054,7 @@ public function multivalueHeader() $request->handleData("\r\nbody"); /** @var \Psr\Http\Message\ResponseInterface $response */ - $this->assertInstanceOf('Psr\Http\Message\ResponseInterface', $response); + $this->assertInstanceOf(ResponseInterface::class, $response); $this->assertEquals('1.0', $response->getProtocolVersion()); $this->assertEquals(200, $response->getStatusCode()); $this->assertEquals('OK', $response->getReasonPhrase()); diff --git a/tests/Io/ClockTest.php b/tests/Io/ClockTest.php index 8f4b90fa..318fa7ef 100644 --- a/tests/Io/ClockTest.php +++ b/tests/Io/ClockTest.php @@ -3,13 +3,14 @@ namespace React\Tests\Http\Io; use PHPUnit\Framework\TestCase; +use React\EventLoop\LoopInterface; use React\Http\Io\Clock; class ClockTest extends TestCase { public function testNowReturnsSameTimestampMultipleTimesInSameTick() { - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop = $this->createMock(LoopInterface::class); $clock = new Clock($loop); @@ -21,7 +22,7 @@ public function testNowReturnsSameTimestampMultipleTimesInSameTick() public function testNowResetsMemoizedTimestampOnFutureTick() { $tick = null; - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop = $this->createMock(LoopInterface::class); $loop->expects($this->once())->method('futureTick')->with($this->callback(function ($cb) use (&$tick) { $tick = $cb; return true; diff --git a/tests/Io/CloseProtectionStreamTest.php b/tests/Io/CloseProtectionStreamTest.php index 4f3d35ca..f3aa346d 100644 --- a/tests/Io/CloseProtectionStreamTest.php +++ b/tests/Io/CloseProtectionStreamTest.php @@ -3,14 +3,16 @@ namespace React\Tests\Http\Io; use React\Http\Io\CloseProtectionStream; +use React\Stream\ReadableStreamInterface; use React\Stream\ThroughStream; +use React\Stream\WritableStreamInterface; use React\Tests\Http\TestCase; class CloseProtectionStreamTest extends TestCase { public function testCloseDoesNotCloseTheInputStream() { - $input = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->disableOriginalConstructor()->getMock(); + $input = $this->createMock(ReadableStreamInterface::class); $input->expects($this->never())->method('pause'); $input->expects($this->never())->method('resume'); $input->expects($this->never())->method('close'); @@ -35,7 +37,7 @@ public function testErrorWontCloseStream() public function testResumeStreamWillResumeInputStream() { - $input = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $input = $this->createMock(ReadableStreamInterface::class); $input->expects($this->once())->method('pause'); $input->expects($this->once())->method('resume'); @@ -46,7 +48,7 @@ public function testResumeStreamWillResumeInputStream() public function testCloseResumesInputStreamIfItWasPreviouslyPaused() { - $input = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $input = $this->createMock(ReadableStreamInterface::class); $input->expects($this->once())->method('pause'); $input->expects($this->once())->method('resume'); @@ -73,7 +75,7 @@ public function testPipeStream() $input = new ThroughStream(); $protection = new CloseProtectionStream($input); - $dest = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + $dest = $this->createMock(WritableStreamInterface::class); $ret = $protection->pipe($dest); @@ -132,7 +134,7 @@ public function testEndWontBeEmittedAfterClose() public function testPauseAfterCloseHasNoEffect() { - $input = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $input = $this->createMock(ReadableStreamInterface::class); $input->expects($this->never())->method('pause'); $input->expects($this->never())->method('resume'); @@ -146,7 +148,7 @@ public function testPauseAfterCloseHasNoEffect() public function testResumeAfterCloseHasNoEffect() { - $input = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $input = $this->createMock(ReadableStreamInterface::class); $input->expects($this->never())->method('pause'); $input->expects($this->never())->method('resume'); diff --git a/tests/Io/EmptyBodyStreamTest.php b/tests/Io/EmptyBodyStreamTest.php index 3633ff81..4ee92364 100644 --- a/tests/Io/EmptyBodyStreamTest.php +++ b/tests/Io/EmptyBodyStreamTest.php @@ -3,6 +3,7 @@ namespace React\Tests\Http\Io; use React\Http\Io\EmptyBodyStream; +use React\Stream\WritableStreamInterface; use React\Tests\Http\TestCase; class EmptyBodyStreamTest extends TestCase @@ -36,7 +37,7 @@ public function testResumeIsNoop() public function testPipeStreamReturnsDestinationStream() { - $dest = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + $dest = $this->createMock(WritableStreamInterface::class); $ret = $this->bodyStream->pipe($dest); @@ -70,13 +71,13 @@ public function testCloseTwiceEmitsCloseEventAndClearsListeners() public function testTell() { - $this->setExpectedException('BadMethodCallException'); + $this->expectException(\BadMethodCallException::class); $this->bodyStream->tell(); } public function testEof() { - $this->setExpectedException('BadMethodCallException'); + $this->expectException(\BadMethodCallException::class); $this->bodyStream->eof(); } @@ -87,13 +88,13 @@ public function testIsSeekable() public function testWrite() { - $this->setExpectedException('BadMethodCallException'); + $this->expectException(\BadMethodCallException::class); $this->bodyStream->write(''); } public function testRead() { - $this->setExpectedException('BadMethodCallException'); + $this->expectException(\BadMethodCallException::class); $this->bodyStream->read(1); } @@ -126,13 +127,13 @@ public function testIsReadableReturnsFalseWhenAlreadyClosed() public function testSeek() { - $this->setExpectedException('BadMethodCallException'); + $this->expectException(\BadMethodCallException::class); $this->bodyStream->seek(''); } public function testRewind() { - $this->setExpectedException('BadMethodCallException'); + $this->expectException(\BadMethodCallException::class); $this->bodyStream->rewind(); } diff --git a/tests/Io/HttpBodyStreamTest.php b/tests/Io/HttpBodyStreamTest.php index 1fd269b1..c246bd96 100644 --- a/tests/Io/HttpBodyStreamTest.php +++ b/tests/Io/HttpBodyStreamTest.php @@ -3,7 +3,9 @@ namespace React\Tests\Http\Io; use React\Http\Io\HttpBodyStream; +use React\Stream\ReadableStreamInterface; use React\Stream\ThroughStream; +use React\Stream\WritableStreamInterface; use React\Tests\Http\TestCase; class HttpBodyStreamTest extends TestCase @@ -28,7 +30,7 @@ public function testDataEmit() public function testPauseStream() { - $input = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $input = $this->createMock(ReadableStreamInterface::class); $input->expects($this->once())->method('pause'); $bodyStream = new HttpBodyStream($input, null); @@ -37,7 +39,7 @@ public function testPauseStream() public function testResumeStream() { - $input = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $input = $this->createMock(ReadableStreamInterface::class); $input->expects($this->once())->method('resume'); $bodyStream = new HttpBodyStream($input, null); @@ -46,7 +48,7 @@ public function testResumeStream() public function testPipeStream() { - $dest = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + $dest = $this->createMock(WritableStreamInterface::class); $ret = $this->bodyStream->pipe($dest); @@ -107,13 +109,13 @@ public function testGetSizeCustom() public function testTell() { - $this->setExpectedException('BadMethodCallException'); + $this->expectException(\BadMethodCallException::class); $this->bodyStream->tell(); } public function testEof() { - $this->setExpectedException('BadMethodCallException'); + $this->expectException(\BadMethodCallException::class); $this->bodyStream->eof(); } @@ -124,13 +126,13 @@ public function testIsSeekable() public function testWrite() { - $this->setExpectedException('BadMethodCallException'); + $this->expectException(\BadMethodCallException::class); $this->bodyStream->write(''); } public function testRead() { - $this->setExpectedException('BadMethodCallException'); + $this->expectException(\BadMethodCallException::class); $this->bodyStream->read(''); } @@ -151,13 +153,13 @@ public function testIsReadable() public function testSeek() { - $this->setExpectedException('BadMethodCallException'); + $this->expectException(\BadMethodCallException::class); $this->bodyStream->seek(''); } public function testRewind() { - $this->setExpectedException('BadMethodCallException'); + $this->expectException(\BadMethodCallException::class); $this->bodyStream->rewind(); } diff --git a/tests/Io/IniUtilTest.php b/tests/Io/IniUtilTest.php index 0bc9a249..2e9f99c9 100644 --- a/tests/Io/IniUtilTest.php +++ b/tests/Io/IniUtilTest.php @@ -7,41 +7,39 @@ class IniUtilTest extends TestCase { - public function provideIniSizes() + public static function provideIniSizes() { - return [ - [ - '1', - 1, - ], - [ - '10', - 10, - ], - [ - '1024', - 1024, - ], - [ - '1K', - 1024, - ], - [ - '1.5M', - 1572864, - ], - [ - '64M', - 67108864, - ], - [ - '8G', - 8589934592, - ], - [ - '1T', - 1099511627776, - ], + yield [ + '1', + 1, + ]; + yield [ + '10', + 10, + ]; + yield [ + '1024', + 1024, + ]; + yield [ + '1K', + 1024, + ]; + yield [ + '1.5M', + 1572864, + ]; + yield [ + '64M', + 67108864, + ]; + yield [ + '8G', + 8589934592, + ]; + yield [ + '1T', + 1099511627776, ]; } @@ -58,16 +56,14 @@ public function testIniSizeToBytesWithInvalidSuffixReturnsNumberWithoutSuffix() $this->assertEquals('2', IniUtil::iniSizeToBytes('2x')); } - public function provideInvalidInputIniSizeToBytes() + public static function provideInvalidInputIniSizeToBytes() { - return [ - ['-1G'], - ['0G'], - ['foo'], - ['fooK'], - ['1ooL'], - ['1ooL'], - ]; + yield ['-1G']; + yield ['0G']; + yield ['foo']; + yield ['fooK']; + yield ['1ooL']; + yield ['1ooL']; } /** @@ -75,7 +71,7 @@ public function provideInvalidInputIniSizeToBytes() */ public function testInvalidInputIniSizeToBytes($input) { - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); IniUtil::iniSizeToBytes($input); } } diff --git a/tests/Io/LengthLimitedStreamTest.php b/tests/Io/LengthLimitedStreamTest.php index f1761a0b..b841257a 100644 --- a/tests/Io/LengthLimitedStreamTest.php +++ b/tests/Io/LengthLimitedStreamTest.php @@ -3,7 +3,9 @@ namespace React\Tests\Http\Io; use React\Http\Io\LengthLimitedStream; +use React\Stream\ReadableStreamInterface; use React\Stream\ThroughStream; +use React\Stream\WritableStreamInterface; use React\Tests\Http\TestCase; class LengthLimitedStreamTest extends TestCase @@ -59,7 +61,7 @@ public function testHandleError() public function testPauseStream() { - $input = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $input = $this->createMock(ReadableStreamInterface::class); $input->expects($this->once())->method('pause'); $stream = new LengthLimitedStream($input, 0); @@ -68,7 +70,7 @@ public function testPauseStream() public function testResumeStream() { - $input = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $input = $this->createMock(ReadableStreamInterface::class); $input->expects($this->once())->method('pause'); $stream = new LengthLimitedStream($input, 0); @@ -79,7 +81,7 @@ public function testResumeStream() public function testPipeStream() { $stream = new LengthLimitedStream($this->input, 0); - $dest = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + $dest = $this->createMock(WritableStreamInterface::class); $ret = $stream->pipe($dest); diff --git a/tests/Io/MiddlewareRunnerTest.php b/tests/Io/MiddlewareRunnerTest.php index e46039bf..a2344d3a 100644 --- a/tests/Io/MiddlewareRunnerTest.php +++ b/tests/Io/MiddlewareRunnerTest.php @@ -23,7 +23,8 @@ public function testEmptyMiddlewareStackThrowsException() $middlewares = []; $middlewareStack = new MiddlewareRunner($middlewares); - $this->setExpectedException('RuntimeException', 'No middleware to run'); + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('No middleware to run'); $middlewareStack($request); } @@ -74,7 +75,8 @@ function (ServerRequestInterface $request) { $request = new ServerRequest('GET', 'http://example.com/'); - $this->setExpectedException('RuntimeException', 'hello'); + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('hello'); $middleware($request); } @@ -88,11 +90,12 @@ function (ServerRequestInterface $request) { $request = new ServerRequest('GET', 'http://example.com/'); - $this->setExpectedException('Throwable', 'hello'); + $this->expectException(\Throwable::class); + $this->expectExceptionMessage('hello'); $middleware($request); } - public function provideProcessStackMiddlewares() + public static function provideProcessStackMiddlewares() { $processStackA = new ProcessStack(); $processStackB = new ProcessStack(); @@ -101,41 +104,39 @@ public function provideProcessStackMiddlewares() $responseMiddleware = function () { return new Response(200); }; - return [ + yield [ [ - [ - $processStackA, - $responseMiddleware, - ], - 1, + $processStackA, + $responseMiddleware, ], + 1, + ]; + yield [ [ - [ - $processStackB, - $processStackB, - $responseMiddleware, - ], - 2, + $processStackB, + $processStackB, + $responseMiddleware, ], + 2, + ]; + yield [ [ - [ - $processStackC, - $processStackC, - $processStackC, - $responseMiddleware, - ], - 3, + $processStackC, + $processStackC, + $processStackC, + $responseMiddleware, ], + 3, + ]; + yield [ [ - [ - $processStackD, - $processStackD, - $processStackD, - $processStackD, - $responseMiddleware, - ], - 4, + $processStackD, + $processStackD, + $processStackD, + $processStackD, + $responseMiddleware, ], + 4, ]; } @@ -172,19 +173,17 @@ public function testProcessStack(array $middlewares, $expectedCallCount) } } - public function provideErrorHandler() + public static function provideErrorHandler() { - return [ - [ - function (\Exception $e) { - throw $e; - } - ], - [ - function (\Exception $e) { - return reject($e); - } - ] + yield [ + function (\Exception $e) { + throw $e; + } + ]; + yield [ + function (\Exception $e) { + return reject($e); + } ]; } @@ -193,7 +192,7 @@ function (\Exception $e) { */ public function testNextCanBeRunMoreThanOnceWithoutCorruptingTheMiddlewareStack($errorHandler) { - $exception = new \RuntimeException('exception'); + $exception = new \RuntimeException(\exception::class); $retryCalled = 0; $error = null; $retry = function ($request, $next) use (&$error, &$retryCalled) { @@ -276,129 +275,127 @@ function (ServerRequestInterface $request) use (&$receivedRequests) { ); } - public function provideUncommonMiddlewareArrayFormats() + public static function provideUncommonMiddlewareArrayFormats() { - return [ - [ - function () { - $sequence = ''; - - // Numeric index gap - return [ - 0 => function (ServerRequestInterface $request, $next) use (&$sequence) { - $sequence .= 'A'; - - return $next($request); - }, - 2 => function (ServerRequestInterface $request, $next) use (&$sequence) { - $sequence .= 'B'; - - return $next($request); - }, - 3 => function () use (&$sequence) { - return new Response(200, [], $sequence . 'C'); - }, - ]; - }, - 'ABC', - ], - [ - function () { - $sequence = ''; - - // Reversed numeric indexes - return [ - 2 => function (ServerRequestInterface $request, $next) use (&$sequence) { - $sequence .= 'A'; - - return $next($request); - }, - 1 => function (ServerRequestInterface $request, $next) use (&$sequence) { - $sequence .= 'B'; - - return $next($request); - }, - 0 => function () use (&$sequence) { - return new Response(200, [], $sequence . 'C'); - }, - ]; - }, - 'ABC', - ], - [ - function () { - $sequence = ''; - - // Associative array - return [ - 'middleware1' => function (ServerRequestInterface $request, $next) use (&$sequence) { - $sequence .= 'A'; - - return $next($request); - }, - 'middleware2' => function (ServerRequestInterface $request, $next) use (&$sequence) { - $sequence .= 'B'; - - return $next($request); - }, - 'middleware3' => function () use (&$sequence) { - return new Response(200, [], $sequence . 'C'); - }, - ]; - }, - 'ABC', - ], - [ - function () { - $sequence = ''; - - // Associative array with empty or trimmable string keys - return [ - '' => function (ServerRequestInterface $request, $next) use (&$sequence) { - $sequence .= 'A'; - - return $next($request); - }, - ' ' => function (ServerRequestInterface $request, $next) use (&$sequence) { - $sequence .= 'B'; - - return $next($request); - }, - ' ' => function () use (&$sequence) { - return new Response(200, [], $sequence . 'C'); - }, - ]; - }, - 'ABC', - ], - [ - function () { - $sequence = ''; - - // Mixed array keys - return [ - '' => function (ServerRequestInterface $request, $next) use (&$sequence) { - $sequence .= 'A'; - - return $next($request); - }, - 0 => function (ServerRequestInterface $request, $next) use (&$sequence) { - $sequence .= 'B'; - - return $next($request); - }, - 'foo' => function (ServerRequestInterface $request, $next) use (&$sequence) { - $sequence .= 'C'; - - return $next($request); - }, - 2 => function () use (&$sequence) { - return new Response(200, [], $sequence . 'D'); - }, - ]; - }, - 'ABCD', - ], + yield [ + function () { + $sequence = ''; + + // Numeric index gap + return [ + 0 => function (ServerRequestInterface $request, $next) use (&$sequence) { + $sequence .= 'A'; + + return $next($request); + }, + 2 => function (ServerRequestInterface $request, $next) use (&$sequence) { + $sequence .= 'B'; + + return $next($request); + }, + 3 => function () use (&$sequence) { + return new Response(200, [], $sequence . 'C'); + }, + ]; + }, + 'ABC', + ]; + yield [ + function () { + $sequence = ''; + + // Reversed numeric indexes + return [ + 2 => function (ServerRequestInterface $request, $next) use (&$sequence) { + $sequence .= 'A'; + + return $next($request); + }, + 1 => function (ServerRequestInterface $request, $next) use (&$sequence) { + $sequence .= 'B'; + + return $next($request); + }, + 0 => function () use (&$sequence) { + return new Response(200, [], $sequence . 'C'); + }, + ]; + }, + 'ABC', + ]; + yield [ + function () { + $sequence = ''; + + // Associative array + return [ + 'middleware1' => function (ServerRequestInterface $request, $next) use (&$sequence) { + $sequence .= 'A'; + + return $next($request); + }, + 'middleware2' => function (ServerRequestInterface $request, $next) use (&$sequence) { + $sequence .= 'B'; + + return $next($request); + }, + 'middleware3' => function () use (&$sequence) { + return new Response(200, [], $sequence . 'C'); + }, + ]; + }, + 'ABC', + ]; + yield [ + function () { + $sequence = ''; + + // Associative array with empty or trimmable string keys + return [ + '' => function (ServerRequestInterface $request, $next) use (&$sequence) { + $sequence .= 'A'; + + return $next($request); + }, + ' ' => function (ServerRequestInterface $request, $next) use (&$sequence) { + $sequence .= 'B'; + + return $next($request); + }, + ' ' => function () use (&$sequence) { + return new Response(200, [], $sequence . 'C'); + }, + ]; + }, + 'ABC', + ]; + yield [ + function () { + $sequence = ''; + + // Mixed array keys + return [ + '' => function (ServerRequestInterface $request, $next) use (&$sequence) { + $sequence .= 'A'; + + return $next($request); + }, + 0 => function (ServerRequestInterface $request, $next) use (&$sequence) { + $sequence .= 'B'; + + return $next($request); + }, + 'foo' => function (ServerRequestInterface $request, $next) use (&$sequence) { + $sequence .= 'C'; + + return $next($request); + }, + 2 => function () use (&$sequence) { + return new Response(200, [], $sequence . 'D'); + }, + ]; + }, + 'ABCD', ]; } diff --git a/tests/Io/MultipartParserTest.php b/tests/Io/MultipartParserTest.php index ba439760..ebc5972f 100644 --- a/tests/Io/MultipartParserTest.php +++ b/tests/Io/MultipartParserTest.php @@ -2,6 +2,7 @@ namespace React\Tests\Http\Io\Middleware; +use Psr\Http\Message\UploadedFileInterface; use React\Http\Io\MultipartParser; use React\Http\Message\ServerRequest; use React\Tests\Http\TestCase; @@ -673,7 +674,7 @@ public function testInvalidUploadFileWithoutContentTypeUsesNullValue() $this->assertCount(1, $files); $this->assertTrue(isset($files['file'])); - $this->assertInstanceOf('Psr\Http\Message\UploadedFileInterface', $files['file']); + $this->assertInstanceOf(UploadedFileInterface::class, $files['file']); /* @var $file \Psr\Http\Message\UploadedFileInterface */ $file = $files['file']; @@ -708,7 +709,7 @@ public function testInvalidUploadFileWithoutMultipleContentTypeUsesLastValue() $this->assertCount(1, $files); $this->assertTrue(isset($files['file'])); - $this->assertInstanceOf('Psr\Http\Message\UploadedFileInterface', $files['file']); + $this->assertInstanceOf(UploadedFileInterface::class, $files['file']); /* @var $file \Psr\Http\Message\UploadedFileInterface */ $file = $files['file']; @@ -742,7 +743,7 @@ public function testUploadEmptyFile() $this->assertCount(1, $files); $this->assertTrue(isset($files['file'])); - $this->assertInstanceOf('Psr\Http\Message\UploadedFileInterface', $files['file']); + $this->assertInstanceOf(UploadedFileInterface::class, $files['file']); /* @var $file \Psr\Http\Message\UploadedFileInterface */ $file = $files['file']; @@ -776,7 +777,7 @@ public function testUploadTooLargeFile() $this->assertCount(1, $files); $this->assertTrue(isset($files['file'])); - $this->assertInstanceOf('Psr\Http\Message\UploadedFileInterface', $files['file']); + $this->assertInstanceOf(UploadedFileInterface::class, $files['file']); /* @var $file \Psr\Http\Message\UploadedFileInterface */ $file = $files['file']; @@ -809,7 +810,7 @@ public function testUploadTooLargeFileWithIniLikeSize() $this->assertCount(1, $files); $this->assertTrue(isset($files['file'])); - $this->assertInstanceOf('Psr\Http\Message\UploadedFileInterface', $files['file']); + $this->assertInstanceOf(UploadedFileInterface::class, $files['file']); /* @var $file \Psr\Http\Message\UploadedFileInterface */ $file = $files['file']; @@ -842,7 +843,7 @@ public function testUploadNoFile() $this->assertCount(1, $files); $this->assertTrue(isset($files['file'])); - $this->assertInstanceOf('Psr\Http\Message\UploadedFileInterface', $files['file']); + $this->assertInstanceOf(UploadedFileInterface::class, $files['file']); /* @var $file \Psr\Http\Message\UploadedFileInterface */ $file = $files['file']; @@ -1046,7 +1047,7 @@ public function testWeOnlyParseTheAmountOfMultiPartChunksWeConfigured() $parser = new MultipartParser(); - $reflectecClass = new \ReflectionClass('\React\Http\Io\MultipartParser'); + $reflectecClass = new \ReflectionClass(MultipartParser::class); $requestProperty = $reflectecClass->getProperty('request'); $requestProperty->setAccessible(true); $cursorProperty = $reflectecClass->getProperty('cursor'); diff --git a/tests/Io/PauseBufferStreamTest.php b/tests/Io/PauseBufferStreamTest.php index 05bf3ee3..139db8fd 100644 --- a/tests/Io/PauseBufferStreamTest.php +++ b/tests/Io/PauseBufferStreamTest.php @@ -2,15 +2,16 @@ namespace React\Tests\Io; -use React\Tests\Http\TestCase; +use React\Stream\ReadableStreamInterface; use React\Stream\ThroughStream; +use React\Tests\Http\TestCase; use React\Http\Io\PauseBufferStream; class PauseBufferStreamTest extends TestCase { public function testPauseMethodWillBePassedThroughToInput() { - $input = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $input = $this->createMock(ReadableStreamInterface::class); $input->expects($this->once())->method('pause'); $stream = new PauseBufferStream($input); @@ -19,7 +20,7 @@ public function testPauseMethodWillBePassedThroughToInput() public function testCloseMethodWillBePassedThroughToInput() { - $input = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $input = $this->createMock(ReadableStreamInterface::class); $input->expects($this->once())->method('close'); $stream = new PauseBufferStream($input); @@ -28,7 +29,7 @@ public function testCloseMethodWillBePassedThroughToInput() public function testPauseMethodWillNotBePassedThroughToInputAfterClose() { - $input = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $input = $this->createMock(ReadableStreamInterface::class); $input->expects($this->never())->method('pause'); $stream = new PauseBufferStream($input); diff --git a/tests/Io/ReadableBodyStreamTest.php b/tests/Io/ReadableBodyStreamTest.php index 8ece6791..2409a6be 100644 --- a/tests/Io/ReadableBodyStreamTest.php +++ b/tests/Io/ReadableBodyStreamTest.php @@ -3,8 +3,9 @@ namespace React\Tests\Http\Io; use React\Http\Io\ReadableBodyStream; -use React\Tests\Http\TestCase; +use React\Stream\ReadableStreamInterface; use React\Stream\ThroughStream; +use React\Tests\Http\TestCase; class ReadableBodyStreamTest extends TestCase { @@ -16,7 +17,7 @@ class ReadableBodyStreamTest extends TestCase */ public function setUpStream() { - $this->input = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $this->input = $this->createMock(ReadableStreamInterface::class); $this->stream = new ReadableBodyStream($this->input); } @@ -102,7 +103,7 @@ public function testEndInputWillEmitErrorEventWhenDataDoesNotReachExpectedLength $this->input->write('hi'); $this->input->end(); - $this->assertInstanceOf('UnderflowException', $called); + $this->assertInstanceOf(\UnderflowException::class, $called); $this->assertSame('Unexpected end of response body after 2/5 bytes', $called->getMessage()); } @@ -188,7 +189,7 @@ public function testPointlessTostringReturnsEmptyString() public function testPointlessDetachThrows() { - $this->setExpectedException('BadMethodCallException'); + $this->expectException(\BadMethodCallException::class); $this->stream->detach(); } @@ -199,7 +200,7 @@ public function testPointlessGetSizeReturnsNull() public function testPointlessTellThrows() { - $this->setExpectedException('BadMethodCallException'); + $this->expectException(\BadMethodCallException::class); $this->stream->tell(); } @@ -210,13 +211,13 @@ public function testPointlessIsSeekableReturnsFalse() public function testPointlessSeekThrows() { - $this->setExpectedException('BadMethodCallException'); + $this->expectException(\BadMethodCallException::class); $this->stream->seek(0); } public function testPointlessRewindThrows() { - $this->setExpectedException('BadMethodCallException'); + $this->expectException(\BadMethodCallException::class); $this->stream->rewind(); } @@ -227,19 +228,19 @@ public function testPointlessIsWritableReturnsFalse() public function testPointlessWriteThrows() { - $this->setExpectedException('BadMethodCallException'); + $this->expectException(\BadMethodCallException::class); $this->stream->write(''); } public function testPointlessReadThrows() { - $this->setExpectedException('BadMethodCallException'); + $this->expectException(\BadMethodCallException::class); $this->stream->read(8192); } public function testPointlessGetContentsThrows() { - $this->setExpectedException('BadMethodCallException'); + $this->expectException(\BadMethodCallException::class); $this->stream->getContents(); } diff --git a/tests/Io/RequestHeaderParserTest.php b/tests/Io/RequestHeaderParserTest.php index d15d4e7f..568fc375 100644 --- a/tests/Io/RequestHeaderParserTest.php +++ b/tests/Io/RequestHeaderParserTest.php @@ -2,20 +2,24 @@ namespace React\Tests\Http\Io; +use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ServerRequestInterface; +use React\Http\Io\Clock; use React\Http\Io\RequestHeaderParser; +use React\Socket\Connection; +use React\Stream\ReadableStreamInterface; use React\Tests\Http\TestCase; class RequestHeaderParserTest extends TestCase { public function testSplitShouldHappenOnDoubleCrlf() { - $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + $clock = $this->createMock(Clock::class); $parser = new RequestHeaderParser($clock); $parser->on('headers', $this->expectCallableNever()); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); @@ -31,12 +35,12 @@ public function testSplitShouldHappenOnDoubleCrlf() public function testFeedInOneGo() { - $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + $clock = $this->createMock(Clock::class); $parser = new RequestHeaderParser($clock); $parser->on('headers', $this->expectCallableOnce()); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); $data = $this->createGetRequest(); @@ -45,7 +49,7 @@ public function testFeedInOneGo() public function testFeedTwoRequestsOnSeparateConnections() { - $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + $clock = $this->createMock(Clock::class); $parser = new RequestHeaderParser($clock); @@ -54,8 +58,8 @@ public function testFeedTwoRequestsOnSeparateConnections() ++$called; }); - $connection1 = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); - $connection2 = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $connection1 = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->setMethods(null)->getMock(); + $connection2 = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection1); $parser->handle($connection2); @@ -71,7 +75,7 @@ public function testHeadersEventShouldEmitRequestAndConnection() $request = null; $conn = null; - $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + $clock = $this->createMock(Clock::class); $parser = new RequestHeaderParser($clock); $parser->on('headers', function ($parsedRequest, $connection) use (&$request, &$conn) { @@ -79,13 +83,13 @@ public function testHeadersEventShouldEmitRequestAndConnection() $conn = $connection; }); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); $data = $this->createGetRequest(); $connection->emit('data', [$data]); - $this->assertInstanceOf('Psr\Http\Message\RequestInterface', $request); + $this->assertInstanceOf(RequestInterface::class, $request); $this->assertSame('GET', $request->getMethod()); $this->assertEquals('http://example.com/', $request->getUri()); $this->assertSame('1.1', $request->getProtocolVersion()); @@ -96,21 +100,21 @@ public function testHeadersEventShouldEmitRequestAndConnection() public function testHeadersEventShouldEmitRequestWhichShouldEmitEndForStreamingBodyWithoutContentLengthFromInitialRequestBody() { - $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + $clock = $this->createMock(Clock::class); $parser = new RequestHeaderParser($clock); $ended = false; $parser->on('headers', function (ServerRequestInterface $request) use (&$ended) { $body = $request->getBody(); - $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); + $this->assertInstanceOf(ReadableStreamInterface::class, $body); $body->on('end', function () use (&$ended) { $ended = true; }); }); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); $data = "GET / HTTP/1.0\r\n\r\n"; @@ -121,14 +125,14 @@ public function testHeadersEventShouldEmitRequestWhichShouldEmitEndForStreamingB public function testHeadersEventShouldEmitRequestWhichShouldEmitStreamingBodyDataFromInitialRequestBody() { - $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + $clock = $this->createMock(Clock::class); $parser = new RequestHeaderParser($clock); $buffer = ''; $parser->on('headers', function (ServerRequestInterface $request) use (&$buffer) { $body = $request->getBody(); - $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); + $this->assertInstanceOf(ReadableStreamInterface::class, $body); $body->on('data', function ($chunk) use (&$buffer) { $buffer .= $chunk; @@ -138,7 +142,7 @@ public function testHeadersEventShouldEmitRequestWhichShouldEmitStreamingBodyDat }); }); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); $data = "POST / HTTP/1.0\r\nContent-Length: 11\r\n\r\n"; @@ -150,21 +154,21 @@ public function testHeadersEventShouldEmitRequestWhichShouldEmitStreamingBodyDat public function testHeadersEventShouldEmitRequestWhichShouldEmitStreamingBodyWithPlentyOfDataFromInitialRequestBody() { - $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + $clock = $this->createMock(Clock::class); $parser = new RequestHeaderParser($clock); $buffer = ''; $parser->on('headers', function (ServerRequestInterface $request) use (&$buffer) { $body = $request->getBody(); - $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); + $this->assertInstanceOf(ReadableStreamInterface::class, $body); $body->on('data', function ($chunk) use (&$buffer) { $buffer .= $chunk; }); }); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); $size = 10000; @@ -177,21 +181,21 @@ public function testHeadersEventShouldEmitRequestWhichShouldEmitStreamingBodyWit public function testHeadersEventShouldEmitRequestWhichShouldNotEmitStreamingBodyDataWithoutContentLengthFromInitialRequestBody() { - $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + $clock = $this->createMock(Clock::class); $parser = new RequestHeaderParser($clock); $buffer = ''; $parser->on('headers', function (ServerRequestInterface $request) use (&$buffer) { $body = $request->getBody(); - $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); + $this->assertInstanceOf(ReadableStreamInterface::class, $body); $body->on('data', function ($chunk) use (&$buffer) { $buffer .= $chunk; }); }); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); $data = "POST / HTTP/1.0\r\n\r\n"; @@ -203,21 +207,21 @@ public function testHeadersEventShouldEmitRequestWhichShouldNotEmitStreamingBody public function testHeadersEventShouldEmitRequestWhichShouldEmitStreamingBodyDataUntilContentLengthBoundaryFromInitialRequestBody() { - $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + $clock = $this->createMock(Clock::class); $parser = new RequestHeaderParser($clock); $buffer = ''; $parser->on('headers', function (ServerRequestInterface $request) use (&$buffer) { $body = $request->getBody(); - $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); + $this->assertInstanceOf(ReadableStreamInterface::class, $body); $body->on('data', function ($chunk) use (&$buffer) { $buffer .= $chunk; }); }); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); $data = "POST / HTTP/1.0\r\nContent-Length: 6\r\n\r\n"; @@ -231,20 +235,20 @@ public function testHeadersEventShouldParsePathAndQueryString() { $request = null; - $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + $clock = $this->createMock(Clock::class); $parser = new RequestHeaderParser($clock); $parser->on('headers', function ($parsedRequest) use (&$request) { $request = $parsedRequest; }); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); $data = $this->createAdvancedPostRequest(); $connection->emit('data', [$data]); - $this->assertInstanceOf('Psr\Http\Message\RequestInterface', $request); + $this->assertInstanceOf(RequestInterface::class, $request); $this->assertSame('POST', $request->getMethod()); $this->assertEquals('http://example.com/foo?bar=baz', $request->getUri()); $this->assertSame('1.1', $request->getProtocolVersion()); @@ -260,14 +264,14 @@ public function testHeaderEventWithShouldApplyDefaultAddressFromLocalConnectionA { $request = null; - $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + $clock = $this->createMock(Clock::class); $parser = new RequestHeaderParser($clock); $parser->on('headers', function ($parsedRequest) use (&$request) { $request = $parsedRequest; }); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(['getLocalAddress'])->getMock(); + $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->setMethods(['getLocalAddress'])->getMock(); $connection->expects($this->once())->method('getLocalAddress')->willReturn('tcp://127.1.1.1:8000'); $parser->handle($connection); @@ -281,14 +285,14 @@ public function testHeaderEventViaHttpsShouldApplyHttpsSchemeFromLocalTlsConnect { $request = null; - $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + $clock = $this->createMock(Clock::class); $parser = new RequestHeaderParser($clock); $parser->on('headers', function ($parsedRequest) use (&$request) { $request = $parsedRequest; }); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(['getLocalAddress'])->getMock(); + $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->setMethods(['getLocalAddress'])->getMock(); $connection->expects($this->once())->method('getLocalAddress')->willReturn('tls://127.1.1.1:8000'); $parser->handle($connection); @@ -303,7 +307,7 @@ public function testHeaderOverflowShouldEmitError() $error = null; $passedConnection = null; - $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + $clock = $this->createMock(Clock::class); $parser = new RequestHeaderParser($clock); $parser->on('headers', $this->expectCallableNever()); @@ -312,13 +316,13 @@ public function testHeaderOverflowShouldEmitError() $passedConnection = $connection; }); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); $data = str_repeat('A', 8193); $connection->emit('data', [$data]); - $this->assertInstanceOf('OverflowException', $error); + $this->assertInstanceOf(\OverflowException::class, $error); $this->assertSame('Maximum header size of 8192 exceeded.', $error->getMessage()); $this->assertSame($connection, $passedConnection); } @@ -327,7 +331,7 @@ public function testInvalidEmptyRequestHeadersParseException() { $error = null; - $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + $clock = $this->createMock(Clock::class); $parser = new RequestHeaderParser($clock); $parser->on('headers', $this->expectCallableNever()); @@ -335,12 +339,12 @@ public function testInvalidEmptyRequestHeadersParseException() $error = $message; }); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); $connection->emit('data', ["\r\n\r\n"]); - $this->assertInstanceOf('InvalidArgumentException', $error); + $this->assertInstanceOf(\InvalidArgumentException::class, $error); $this->assertSame('Unable to parse invalid request-line', $error->getMessage()); } @@ -348,7 +352,7 @@ public function testInvalidMalformedRequestLineParseException() { $error = null; - $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + $clock = $this->createMock(Clock::class); $parser = new RequestHeaderParser($clock); $parser->on('headers', $this->expectCallableNever()); @@ -356,12 +360,12 @@ public function testInvalidMalformedRequestLineParseException() $error = $message; }); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); $connection->emit('data', ["GET /\r\n\r\n"]); - $this->assertInstanceOf('InvalidArgumentException', $error); + $this->assertInstanceOf(\InvalidArgumentException::class, $error); $this->assertSame('Unable to parse invalid request-line', $error->getMessage()); } @@ -369,7 +373,7 @@ public function testInvalidMalformedRequestHeadersThrowsParseException() { $error = null; - $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + $clock = $this->createMock(Clock::class); $parser = new RequestHeaderParser($clock); $parser->on('headers', $this->expectCallableNever()); @@ -377,12 +381,12 @@ public function testInvalidMalformedRequestHeadersThrowsParseException() $error = $message; }); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); $connection->emit('data', ["GET / HTTP/1.1\r\nHost : yes\r\n\r\n"]); - $this->assertInstanceOf('InvalidArgumentException', $error); + $this->assertInstanceOf(\InvalidArgumentException::class, $error); $this->assertSame('Unable to parse invalid request header fields', $error->getMessage()); } @@ -390,7 +394,7 @@ public function testInvalidMalformedRequestHeadersWhitespaceThrowsParseException { $error = null; - $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + $clock = $this->createMock(Clock::class); $parser = new RequestHeaderParser($clock); $parser->on('headers', $this->expectCallableNever()); @@ -398,12 +402,12 @@ public function testInvalidMalformedRequestHeadersWhitespaceThrowsParseException $error = $message; }); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); $connection->emit('data', ["GET / HTTP/1.1\r\nHost: yes\rFoo: bar\r\n\r\n"]); - $this->assertInstanceOf('InvalidArgumentException', $error); + $this->assertInstanceOf(\InvalidArgumentException::class, $error); $this->assertSame('Unable to parse invalid request header fields', $error->getMessage()); } @@ -411,7 +415,7 @@ public function testInvalidAbsoluteFormSchemeEmitsError() { $error = null; - $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + $clock = $this->createMock(Clock::class); $parser = new RequestHeaderParser($clock); $parser->on('headers', $this->expectCallableNever()); @@ -419,12 +423,12 @@ public function testInvalidAbsoluteFormSchemeEmitsError() $error = $message; }); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); $connection->emit('data', ["GET tcp://example.com:80/ HTTP/1.0\r\n\r\n"]); - $this->assertInstanceOf('InvalidArgumentException', $error); + $this->assertInstanceOf(\InvalidArgumentException::class, $error); $this->assertSame('Invalid absolute-form request-target', $error->getMessage()); } @@ -432,7 +436,7 @@ public function testOriginFormWithSchemeSeparatorInParam() { $request = null; - $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + $clock = $this->createMock(Clock::class); $parser = new RequestHeaderParser($clock); $parser->on('error', $this->expectCallableNever()); @@ -440,12 +444,12 @@ public function testOriginFormWithSchemeSeparatorInParam() $request = $parsedRequest; }); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); $connection->emit('data', ["GET /somepath?param=http://example.com HTTP/1.1\r\nHost: localhost\r\n\r\n"]); - $this->assertInstanceOf('Psr\Http\Message\RequestInterface', $request); + $this->assertInstanceOf(RequestInterface::class, $request); $this->assertSame('GET', $request->getMethod()); $this->assertEquals('http://localhost/somepath?param=http://example.com', $request->getUri()); $this->assertSame('1.1', $request->getProtocolVersion()); @@ -459,7 +463,7 @@ public function testUriStartingWithColonSlashSlashFails() { $error = null; - $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + $clock = $this->createMock(Clock::class); $parser = new RequestHeaderParser($clock); $parser->on('headers', $this->expectCallableNever()); @@ -467,12 +471,12 @@ public function testUriStartingWithColonSlashSlashFails() $error = $message; }); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); $connection->emit('data', ["GET ://example.com:80/ HTTP/1.0\r\n\r\n"]); - $this->assertInstanceOf('InvalidArgumentException', $error); + $this->assertInstanceOf(\InvalidArgumentException::class, $error); $this->assertSame('Invalid absolute-form request-target', $error->getMessage()); } @@ -480,7 +484,7 @@ public function testInvalidAbsoluteFormWithFragmentEmitsError() { $error = null; - $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + $clock = $this->createMock(Clock::class); $parser = new RequestHeaderParser($clock); $parser->on('headers', $this->expectCallableNever()); @@ -488,12 +492,12 @@ public function testInvalidAbsoluteFormWithFragmentEmitsError() $error = $message; }); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); $connection->emit('data', ["GET http://example.com:80/#home HTTP/1.0\r\n\r\n"]); - $this->assertInstanceOf('InvalidArgumentException', $error); + $this->assertInstanceOf(\InvalidArgumentException::class, $error); $this->assertSame('Invalid absolute-form request-target', $error->getMessage()); } @@ -501,7 +505,7 @@ public function testInvalidHeaderContainsFullUri() { $error = null; - $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + $clock = $this->createMock(Clock::class); $parser = new RequestHeaderParser($clock); $parser->on('headers', $this->expectCallableNever()); @@ -509,12 +513,12 @@ public function testInvalidHeaderContainsFullUri() $error = $message; }); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); $connection->emit('data', ["GET / HTTP/1.1\r\nHost: http://user:pass@host/\r\n\r\n"]); - $this->assertInstanceOf('InvalidArgumentException', $error); + $this->assertInstanceOf(\InvalidArgumentException::class, $error); $this->assertSame('Invalid Host header value', $error->getMessage()); } @@ -522,7 +526,7 @@ public function testInvalidAbsoluteFormWithHostHeaderEmpty() { $error = null; - $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + $clock = $this->createMock(Clock::class); $parser = new RequestHeaderParser($clock); $parser->on('headers', $this->expectCallableNever()); @@ -530,12 +534,12 @@ public function testInvalidAbsoluteFormWithHostHeaderEmpty() $error = $message; }); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); $connection->emit('data', ["GET http://example.com/ HTTP/1.1\r\nHost: \r\n\r\n"]); - $this->assertInstanceOf('InvalidArgumentException', $error); + $this->assertInstanceOf(\InvalidArgumentException::class, $error); $this->assertSame('Invalid Host header value', $error->getMessage()); } @@ -543,7 +547,7 @@ public function testInvalidConnectRequestWithNonAuthorityForm() { $error = null; - $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + $clock = $this->createMock(Clock::class); $parser = new RequestHeaderParser($clock); $parser->on('headers', $this->expectCallableNever()); @@ -551,12 +555,12 @@ public function testInvalidConnectRequestWithNonAuthorityForm() $error = $message; }); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); $connection->emit('data', ["CONNECT http://example.com:8080/ HTTP/1.1\r\nHost: example.com:8080\r\n\r\n"]); - $this->assertInstanceOf('InvalidArgumentException', $error); + $this->assertInstanceOf(\InvalidArgumentException::class, $error); $this->assertSame('CONNECT method MUST use authority-form request target', $error->getMessage()); } @@ -564,7 +568,7 @@ public function testInvalidHttpVersion() { $error = null; - $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + $clock = $this->createMock(Clock::class); $parser = new RequestHeaderParser($clock); $parser->on('headers', $this->expectCallableNever()); @@ -572,12 +576,12 @@ public function testInvalidHttpVersion() $error = $message; }); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); $connection->emit('data', ["GET / HTTP/1.2\r\n\r\n"]); - $this->assertInstanceOf('InvalidArgumentException', $error); + $this->assertInstanceOf(\InvalidArgumentException::class, $error); $this->assertSame(505, $error->getCode()); $this->assertSame('Received request with invalid protocol version', $error->getMessage()); } @@ -586,7 +590,7 @@ public function testInvalidContentLengthRequestHeaderWillEmitError() { $error = null; - $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + $clock = $this->createMock(Clock::class); $parser = new RequestHeaderParser($clock); $parser->on('headers', $this->expectCallableNever()); @@ -594,12 +598,12 @@ public function testInvalidContentLengthRequestHeaderWillEmitError() $error = $message; }); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); $connection->emit('data', ["GET / HTTP/1.1\r\nHost: localhost\r\nContent-Length: foo\r\n\r\n"]); - $this->assertInstanceOf('InvalidArgumentException', $error); + $this->assertInstanceOf(\InvalidArgumentException::class, $error); $this->assertSame(400, $error->getCode()); $this->assertSame('The value of `Content-Length` is not valid', $error->getMessage()); } @@ -608,7 +612,7 @@ public function testInvalidRequestWithMultipleContentLengthRequestHeadersWillEmi { $error = null; - $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + $clock = $this->createMock(Clock::class); $parser = new RequestHeaderParser($clock); $parser->on('headers', $this->expectCallableNever()); @@ -616,12 +620,12 @@ public function testInvalidRequestWithMultipleContentLengthRequestHeadersWillEmi $error = $message; }); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); $connection->emit('data', ["GET / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 4\r\nContent-Length: 5\r\n\r\n"]); - $this->assertInstanceOf('InvalidArgumentException', $error); + $this->assertInstanceOf(\InvalidArgumentException::class, $error); $this->assertSame(400, $error->getCode()); $this->assertSame('The value of `Content-Length` is not valid', $error->getMessage()); } @@ -630,7 +634,7 @@ public function testInvalidTransferEncodingRequestHeaderWillEmitError() { $error = null; - $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + $clock = $this->createMock(Clock::class); $parser = new RequestHeaderParser($clock); $parser->on('headers', $this->expectCallableNever()); @@ -638,12 +642,12 @@ public function testInvalidTransferEncodingRequestHeaderWillEmitError() $error = $message; }); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); $connection->emit('data', ["GET / HTTP/1.1\r\nHost: localhost\r\nTransfer-Encoding: foo\r\n\r\n"]); - $this->assertInstanceOf('InvalidArgumentException', $error); + $this->assertInstanceOf(\InvalidArgumentException::class, $error); $this->assertSame(501, $error->getCode()); $this->assertSame('Only chunked-encoding is allowed for Transfer-Encoding', $error->getMessage()); } @@ -652,7 +656,7 @@ public function testInvalidRequestWithBothTransferEncodingAndContentLengthWillEm { $error = null; - $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + $clock = $this->createMock(Clock::class); $parser = new RequestHeaderParser($clock); $parser->on('headers', $this->expectCallableNever()); @@ -660,12 +664,12 @@ public function testInvalidRequestWithBothTransferEncodingAndContentLengthWillEm $error = $message; }); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); $connection->emit('data', ["GET / HTTP/1.1\r\nHost: localhost\r\nTransfer-Encoding: chunked\r\nContent-Length: 0\r\n\r\n"]); - $this->assertInstanceOf('InvalidArgumentException', $error); + $this->assertInstanceOf(\InvalidArgumentException::class, $error); $this->assertSame(400, $error->getCode()); $this->assertSame('Using both `Transfer-Encoding: chunked` and `Content-Length` is not allowed', $error->getMessage()); } @@ -674,7 +678,7 @@ public function testServerParamsWillBeSetOnHttpsRequest() { $request = null; - $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + $clock = $this->createMock(Clock::class); $clock->expects($this->once())->method('now')->willReturn(1652972091.3958); $parser = new RequestHeaderParser($clock); @@ -683,7 +687,7 @@ public function testServerParamsWillBeSetOnHttpsRequest() $request = $parsedRequest; }); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(['getLocalAddress', 'getRemoteAddress'])->getMock(); + $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->setMethods(['getLocalAddress', 'getRemoteAddress'])->getMock(); $connection->expects($this->once())->method('getLocalAddress')->willReturn('tls://127.1.1.1:8000'); $connection->expects($this->once())->method('getRemoteAddress')->willReturn('tls://192.168.1.1:8001'); $parser->handle($connection); @@ -707,7 +711,7 @@ public function testServerParamsWillBeSetOnHttpRequest() { $request = null; - $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + $clock = $this->createMock(Clock::class); $clock->expects($this->once())->method('now')->willReturn(1652972091.3958); $parser = new RequestHeaderParser($clock); @@ -716,7 +720,7 @@ public function testServerParamsWillBeSetOnHttpRequest() $request = $parsedRequest; }); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(['getLocalAddress', 'getRemoteAddress'])->getMock(); + $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->setMethods(['getLocalAddress', 'getRemoteAddress'])->getMock(); $connection->expects($this->once())->method('getLocalAddress')->willReturn('tcp://127.1.1.1:8000'); $connection->expects($this->once())->method('getRemoteAddress')->willReturn('tcp://192.168.1.1:8001'); $parser->handle($connection); @@ -740,7 +744,7 @@ public function testServerParamsWillNotSetRemoteAddressForUnixDomainSockets() { $request = null; - $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + $clock = $this->createMock(Clock::class); $clock->expects($this->once())->method('now')->willReturn(1652972091.3958); $parser = new RequestHeaderParser($clock); @@ -749,7 +753,7 @@ public function testServerParamsWillNotSetRemoteAddressForUnixDomainSockets() $request = $parsedRequest; }); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(['getLocalAddress', 'getRemoteAddress'])->getMock(); + $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->setMethods(['getLocalAddress', 'getRemoteAddress'])->getMock(); $connection->expects($this->once())->method('getLocalAddress')->willReturn('unix://./server.sock'); $connection->expects($this->once())->method('getRemoteAddress')->willReturn(null); $parser->handle($connection); @@ -773,7 +777,7 @@ public function testServerParamsWontBeSetOnMissingUrls() { $request = null; - $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + $clock = $this->createMock(Clock::class); $clock->expects($this->once())->method('now')->willReturn(1652972091.3958); $parser = new RequestHeaderParser($clock); @@ -782,7 +786,7 @@ public function testServerParamsWontBeSetOnMissingUrls() $request = $parsedRequest; }); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); $connection->emit('data', ["GET /foo HTTP/1.0\r\nHost: example.com\r\n\r\n"]); @@ -801,12 +805,12 @@ public function testServerParamsWontBeSetOnMissingUrls() public function testServerParamsWillBeReusedForMultipleRequestsFromSameConnection() { - $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + $clock = $this->createMock(Clock::class); $clock->expects($this->exactly(2))->method('now')->willReturn(1652972091.3958); $parser = new RequestHeaderParser($clock); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(['getLocalAddress', 'getRemoteAddress'])->getMock(); + $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->setMethods(['getLocalAddress', 'getRemoteAddress'])->getMock(); $connection->expects($this->once())->method('getLocalAddress')->willReturn('tcp://127.1.1.1:8000'); $connection->expects($this->once())->method('getRemoteAddress')->willReturn('tcp://192.168.1.1:8001'); @@ -837,11 +841,11 @@ public function testServerParamsWillBeReusedForMultipleRequestsFromSameConnectio public function testServerParamsWillBeRememberedUntilConnectionIsClosed() { - $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + $clock = $this->createMock(Clock::class); $parser = new RequestHeaderParser($clock); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(['getLocalAddress', 'getRemoteAddress'])->getMock(); + $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->setMethods(['getLocalAddress', 'getRemoteAddress'])->getMock(); $parser->handle($connection); $connection->emit('data', ["GET /foo HTTP/1.0\r\nHost: example.com\r\n\r\n"]); @@ -859,7 +863,7 @@ public function testQueryParmetersWillBeSet() { $request = null; - $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + $clock = $this->createMock(Clock::class); $parser = new RequestHeaderParser($clock); @@ -867,7 +871,7 @@ public function testQueryParmetersWillBeSet() $request = $parsedRequest; }); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); $connection->emit('data', ["GET /foo.php?hello=world&test=this HTTP/1.0\r\nHost: example.com\r\n\r\n"]); diff --git a/tests/Io/SenderTest.php b/tests/Io/SenderTest.php index 59bb9719..df4c5359 100644 --- a/tests/Io/SenderTest.php +++ b/tests/Io/SenderTest.php @@ -3,13 +3,17 @@ namespace React\Tests\Http\Io; use Psr\Http\Message\RequestInterface; +use React\EventLoop\LoopInterface; use React\Http\Client\Client as HttpClient; use React\Http\Io\ClientConnectionManager; +use React\Http\Io\ClientRequestStream; use React\Http\Io\EmptyBodyStream; use React\Http\Io\ReadableBodyStream; use React\Http\Io\Sender; use React\Http\Message\Request; use React\Promise\Promise; +use React\Socket\ConnectionInterface; +use React\Socket\ConnectorInterface; use React\Stream\ThroughStream; use React\Tests\Http\TestCase; use function React\Promise\reject; @@ -25,19 +29,19 @@ class SenderTest extends TestCase */ public function setUpLoop() { - $this->loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $this->loop = $this->createMock(LoopInterface::class); } public function testCreateFromLoop() { $sender = Sender::createFromLoop($this->loop, null); - $this->assertInstanceOf('React\Http\Io\Sender', $sender); + $this->assertInstanceOf(Sender::class, $sender); } public function testSenderRejectsInvalidUri() { - $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $connector = $this->createMock(ConnectorInterface::class); $connector->expects($this->never())->method('connect'); $sender = new Sender(new HttpClient(new ClientConnectionManager($connector, $this->loop))); @@ -51,12 +55,12 @@ public function testSenderRejectsInvalidUri() $exception = $e; }); - $this->assertInstanceOf('InvalidArgumentException', $exception); + $this->assertInstanceOf(\InvalidArgumentException::class, $exception); } public function testSenderConnectorRejection() { - $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $connector = $this->createMock(ConnectorInterface::class); $connector->expects($this->once())->method('connect')->willReturn(reject(new \RuntimeException('Rejected'))); $sender = new Sender(new HttpClient(new ClientConnectionManager($connector, $this->loop))); @@ -70,15 +74,15 @@ public function testSenderConnectorRejection() $exception = $e; }); - $this->assertInstanceOf('RuntimeException', $exception); + $this->assertInstanceOf(\RuntimeException::class, $exception); } public function testSendPostWillAutomaticallySendContentLengthHeader() { - $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); + $client = $this->createMock(HttpClient::class); $client->expects($this->once())->method('request')->with($this->callback(function (RequestInterface $request) { return $request->getHeaderLine('Content-Length') === '5'; - }))->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock()); + }))->willReturn($this->createMock(ClientRequestStream::class)); $sender = new Sender($client); @@ -88,10 +92,10 @@ public function testSendPostWillAutomaticallySendContentLengthHeader() public function testSendPostWillAutomaticallySendContentLengthZeroHeaderForEmptyRequestBody() { - $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); + $client = $this->createMock(HttpClient::class); $client->expects($this->once())->method('request')->with($this->callback(function (RequestInterface $request) { return $request->getHeaderLine('Content-Length') === '0'; - }))->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock()); + }))->willReturn($this->createMock(ClientRequestStream::class)); $sender = new Sender($client); @@ -101,10 +105,10 @@ public function testSendPostWillAutomaticallySendContentLengthZeroHeaderForEmpty public function testSendPostStreamWillAutomaticallySendTransferEncodingChunked() { - $outgoing = $this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock(); + $outgoing = $this->createMock(ClientRequestStream::class); $outgoing->expects($this->once())->method('write')->with(""); - $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); + $client = $this->createMock(HttpClient::class); $client->expects($this->once())->method('request')->with($this->callback(function (RequestInterface $request) { return $request->getHeaderLine('Transfer-Encoding') === 'chunked'; }))->willReturn($outgoing); @@ -118,11 +122,11 @@ public function testSendPostStreamWillAutomaticallySendTransferEncodingChunked() public function testSendPostStreamWillAutomaticallyPipeChunkEncodeBodyForWriteAndRespectRequestThrottling() { - $outgoing = $this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock(); + $outgoing = $this->createMock(ClientRequestStream::class); $outgoing->expects($this->once())->method('isWritable')->willReturn(true); $outgoing->expects($this->exactly(2))->method('write')->withConsecutive([""], ["5\r\nhello\r\n"])->willReturn(false); - $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); + $client = $this->createMock(HttpClient::class); $client->expects($this->once())->method('request')->willReturn($outgoing); $sender = new Sender($client); @@ -137,12 +141,12 @@ public function testSendPostStreamWillAutomaticallyPipeChunkEncodeBodyForWriteAn public function testSendPostStreamWillAutomaticallyPipeChunkEncodeBodyForEnd() { - $outgoing = $this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock(); + $outgoing = $this->createMock(ClientRequestStream::class); $outgoing->expects($this->once())->method('isWritable')->willReturn(true); $outgoing->expects($this->exactly(2))->method('write')->withConsecutive([""], ["0\r\n\r\n"])->willReturn(false); $outgoing->expects($this->once())->method('end')->with(null); - $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); + $client = $this->createMock(HttpClient::class); $client->expects($this->once())->method('request')->willReturn($outgoing); $sender = new Sender($client); @@ -156,13 +160,13 @@ public function testSendPostStreamWillAutomaticallyPipeChunkEncodeBodyForEnd() public function testSendPostStreamWillRejectWhenRequestBodyEmitsErrorEvent() { - $outgoing = $this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock(); + $outgoing = $this->createMock(ClientRequestStream::class); $outgoing->expects($this->once())->method('isWritable')->willReturn(true); $outgoing->expects($this->once())->method('write')->with("")->willReturn(false); $outgoing->expects($this->never())->method('end'); $outgoing->expects($this->once())->method('close'); - $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); + $client = $this->createMock(HttpClient::class); $client->expects($this->once())->method('request')->willReturn($outgoing); $sender = new Sender($client); @@ -179,20 +183,20 @@ public function testSendPostStreamWillRejectWhenRequestBodyEmitsErrorEvent() $exception = $e; }); - $this->assertInstanceOf('RuntimeException', $exception); + $this->assertInstanceOf(\RuntimeException::class, $exception); $this->assertEquals('Request failed because request body reported an error', $exception->getMessage()); $this->assertSame($expected, $exception->getPrevious()); } public function testSendPostStreamWillRejectWhenRequestBodyClosesWithoutEnd() { - $outgoing = $this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock(); + $outgoing = $this->createMock(ClientRequestStream::class); $outgoing->expects($this->once())->method('isWritable')->willReturn(true); $outgoing->expects($this->once())->method('write')->with("")->willReturn(false); $outgoing->expects($this->never())->method('end'); $outgoing->expects($this->once())->method('close'); - $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); + $client = $this->createMock(HttpClient::class); $client->expects($this->once())->method('request')->willReturn($outgoing); $sender = new Sender($client); @@ -208,19 +212,19 @@ public function testSendPostStreamWillRejectWhenRequestBodyClosesWithoutEnd() $exception = $e; }); - $this->assertInstanceOf('RuntimeException', $exception); + $this->assertInstanceOf(\RuntimeException::class, $exception); $this->assertEquals('Request failed because request body closed unexpectedly', $exception->getMessage()); } public function testSendPostStreamWillNotRejectWhenRequestBodyClosesAfterEnd() { - $outgoing = $this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock(); + $outgoing = $this->createMock(ClientRequestStream::class); $outgoing->expects($this->once())->method('isWritable')->willReturn(true); $outgoing->expects($this->exactly(2))->method('write')->withConsecutive([""], ["0\r\n\r\n"])->willReturn(false); $outgoing->expects($this->once())->method('end'); $outgoing->expects($this->never())->method('close'); - $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); + $client = $this->createMock(HttpClient::class); $client->expects($this->once())->method('request')->willReturn($outgoing); $sender = new Sender($client); @@ -242,10 +246,10 @@ public function testSendPostStreamWillNotRejectWhenRequestBodyClosesAfterEnd() public function testSendPostStreamWithExplicitContentLengthWillSendHeaderAsIs() { - $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); + $client = $this->createMock(HttpClient::class); $client->expects($this->once())->method('request')->with($this->callback(function (RequestInterface $request) { return $request->getHeaderLine('Content-Length') === '100'; - }))->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock()); + }))->willReturn($this->createMock(ClientRequestStream::class)); $sender = new Sender($client); @@ -256,10 +260,10 @@ public function testSendPostStreamWithExplicitContentLengthWillSendHeaderAsIs() public function testSendGetWillNotPassContentLengthHeaderForEmptyRequestBody() { - $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); + $client = $this->createMock(HttpClient::class); $client->expects($this->once())->method('request')->with($this->callback(function (RequestInterface $request) { return !$request->hasHeader('Content-Length'); - }))->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock()); + }))->willReturn($this->createMock(ClientRequestStream::class)); $sender = new Sender($client); @@ -269,10 +273,10 @@ public function testSendGetWillNotPassContentLengthHeaderForEmptyRequestBody() public function testSendGetWithEmptyBodyStreamWillNotPassContentLengthOrTransferEncodingHeader() { - $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); + $client = $this->createMock(HttpClient::class); $client->expects($this->once())->method('request')->with($this->callback(function (RequestInterface $request) { return !$request->hasHeader('Content-Length') && !$request->hasHeader('Transfer-Encoding'); - }))->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock()); + }))->willReturn($this->createMock(ClientRequestStream::class)); $sender = new Sender($client); @@ -284,10 +288,10 @@ public function testSendGetWithEmptyBodyStreamWillNotPassContentLengthOrTransfer public function testSendCustomMethodWillNotPassContentLengthHeaderForEmptyRequestBody() { - $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); + $client = $this->createMock(HttpClient::class); $client->expects($this->once())->method('request')->with($this->callback(function (RequestInterface $request) { return !$request->hasHeader('Content-Length'); - }))->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock()); + }))->willReturn($this->createMock(ClientRequestStream::class)); $sender = new Sender($client); @@ -297,10 +301,10 @@ public function testSendCustomMethodWillNotPassContentLengthHeaderForEmptyReques public function testSendCustomMethodWithExplicitContentLengthZeroWillBePassedAsIs() { - $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); + $client = $this->createMock(HttpClient::class); $client->expects($this->once())->method('request')->with($this->callback(function (RequestInterface $request) { return $request->getHeaderLine('Content-Length') === '0'; - }))->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock()); + }))->willReturn($this->createMock(ClientRequestStream::class)); $sender = new Sender($client); @@ -311,10 +315,10 @@ public function testSendCustomMethodWithExplicitContentLengthZeroWillBePassedAsI /** @test */ public function getRequestWithUserAndPassShouldSendAGetRequestWithBasicAuthorizationHeader() { - $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); + $client = $this->createMock(HttpClient::class); $client->expects($this->once())->method('request')->with($this->callback(function (RequestInterface $request) { return $request->getHeaderLine('Authorization') === 'Basic am9objpkdW1teQ=='; - }))->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock()); + }))->willReturn($this->createMock(ClientRequestStream::class)); $sender = new Sender($client); @@ -325,10 +329,10 @@ public function getRequestWithUserAndPassShouldSendAGetRequestWithBasicAuthoriza /** @test */ public function getRequestWithUserAndPassShouldSendAGetRequestWithGivenAuthorizationHeaderBasicAuthorizationHeader() { - $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); + $client = $this->createMock(HttpClient::class); $client->expects($this->once())->method('request')->with($this->callback(function (RequestInterface $request) { return $request->getHeaderLine('Authorization') === 'bearer abc123'; - }))->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock()); + }))->willReturn($this->createMock(ClientRequestStream::class)); $sender = new Sender($client); @@ -342,7 +346,7 @@ public function testCancelRequestWillCancelConnector() throw new \RuntimeException(); }); - $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $connector = $this->createMock(ConnectorInterface::class); $connector->expects($this->once())->method('connect')->willReturn($promise); $sender = new Sender(new HttpClient(new ClientConnectionManager($connector, $this->loop))); @@ -357,15 +361,15 @@ public function testCancelRequestWillCancelConnector() $exception = $e; }); - $this->assertInstanceOf('RuntimeException', $exception); + $this->assertInstanceOf(\RuntimeException::class, $exception); } public function testCancelRequestWillCloseConnection() { - $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection = $this->createMock(ConnectionInterface::class); $connection->expects($this->once())->method('close'); - $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $connector = $this->createMock(ConnectorInterface::class); $connector->expects($this->once())->method('connect')->willReturn(resolve($connection)); $sender = new Sender(new HttpClient(new ClientConnectionManager($connector, $this->loop))); @@ -380,6 +384,6 @@ public function testCancelRequestWillCloseConnection() $exception = $e; }); - $this->assertInstanceOf('RuntimeException', $exception); + $this->assertInstanceOf(\RuntimeException::class, $exception); } } diff --git a/tests/Io/StreamingServerTest.php b/tests/Io/StreamingServerTest.php index 3b5f28f4..a61d1425 100644 --- a/tests/Io/StreamingServerTest.php +++ b/tests/Io/StreamingServerTest.php @@ -4,11 +4,14 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Message\StreamInterface; use React\EventLoop\Loop; +use React\Http\Io\RequestHeaderParser; use React\Http\Io\StreamingServer; use React\Http\Message\Response; use React\Http\Message\ServerRequest; use React\Promise\Promise; +use React\Socket\Connection; use React\Stream\ThroughStream; use React\Tests\Http\SocketServerStub; use React\Tests\Http\TestCase; @@ -35,7 +38,7 @@ public function setUpConnectionMockAndSocket() private function mockConnection(array $additionalMethods = null) { - $connection = $this->getMockBuilder('React\Socket\Connection') + $connection = $this->getMockBuilder(Connection::class) ->disableOriginalConstructor() ->setMethods(array_merge( [ @@ -125,7 +128,7 @@ public function testRequestEvent() $serverParams = $requestAssertion->getServerParams(); $this->assertSame(1, $i); - $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion); + $this->assertInstanceOf(ServerRequestInterface::class, $requestAssertion); $this->assertSame('GET', $requestAssertion->getMethod()); $this->assertSame('/', $requestAssertion->getRequestTarget()); $this->assertSame('/', $requestAssertion->getUri()->getPath()); @@ -158,7 +161,7 @@ public function testRequestEventWithSingleRequestHandlerArray() $serverParams = $requestAssertion->getServerParams(); $this->assertSame(1, $i); - $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion); + $this->assertInstanceOf(ServerRequestInterface::class, $requestAssertion); $this->assertSame('GET', $requestAssertion->getMethod()); $this->assertSame('/', $requestAssertion->getRequestTarget()); $this->assertSame('/', $requestAssertion->getUri()->getPath()); @@ -181,7 +184,7 @@ public function testRequestGetWithHostAndCustomPort() $data = "GET / HTTP/1.1\r\nHost: example.com:8080\r\n\r\n"; $this->connection->emit('data', [$data]); - $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion); + $this->assertInstanceOf(ServerRequestInterface::class, $requestAssertion); $this->assertSame('GET', $requestAssertion->getMethod()); $this->assertSame('/', $requestAssertion->getRequestTarget()); $this->assertSame('/', $requestAssertion->getUri()->getPath()); @@ -203,7 +206,7 @@ public function testRequestGetWithHostAndHttpsPort() $data = "GET / HTTP/1.1\r\nHost: example.com:443\r\n\r\n"; $this->connection->emit('data', [$data]); - $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion); + $this->assertInstanceOf(ServerRequestInterface::class, $requestAssertion); $this->assertSame('GET', $requestAssertion->getMethod()); $this->assertSame('/', $requestAssertion->getRequestTarget()); $this->assertSame('/', $requestAssertion->getUri()->getPath()); @@ -225,7 +228,7 @@ public function testRequestGetWithHostAndDefaultPortWillBeIgnored() $data = "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n"; $this->connection->emit('data', [$data]); - $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion); + $this->assertInstanceOf(ServerRequestInterface::class, $requestAssertion); $this->assertSame('GET', $requestAssertion->getMethod()); $this->assertSame('/', $requestAssertion->getRequestTarget()); $this->assertSame('/', $requestAssertion->getUri()->getPath()); @@ -247,7 +250,7 @@ public function testRequestGetHttp10WithoutHostWillBeIgnored() $data = "GET / HTTP/1.0\r\n\r\n"; $this->connection->emit('data', [$data]); - $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion); + $this->assertInstanceOf(ServerRequestInterface::class, $requestAssertion); $this->assertSame('GET', $requestAssertion->getMethod()); $this->assertSame('/', $requestAssertion->getRequestTarget()); $this->assertSame('/', $requestAssertion->getUri()->getPath()); @@ -282,7 +285,7 @@ public function testRequestOptionsAsterisk() $data = "OPTIONS * HTTP/1.1\r\nHost: example.com\r\n\r\n"; $this->connection->emit('data', [$data]); - $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion); + $this->assertInstanceOf(ServerRequestInterface::class, $requestAssertion); $this->assertSame('OPTIONS', $requestAssertion->getMethod()); $this->assertSame('*', $requestAssertion->getRequestTarget()); $this->assertSame('', $requestAssertion->getUri()->getPath()); @@ -315,7 +318,7 @@ public function testRequestConnectAuthorityForm() $data = "CONNECT example.com:443 HTTP/1.1\r\nHost: example.com:443\r\n\r\n"; $this->connection->emit('data', [$data]); - $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion); + $this->assertInstanceOf(ServerRequestInterface::class, $requestAssertion); $this->assertSame('CONNECT', $requestAssertion->getMethod()); $this->assertSame('example.com:443', $requestAssertion->getRequestTarget()); $this->assertSame('', $requestAssertion->getUri()->getPath()); @@ -337,7 +340,7 @@ public function testRequestConnectWithoutHostWillBePassesAsIs() $data = "CONNECT example.com:443 HTTP/1.1\r\n\r\n"; $this->connection->emit('data', [$data]); - $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion); + $this->assertInstanceOf(ServerRequestInterface::class, $requestAssertion); $this->assertSame('CONNECT', $requestAssertion->getMethod()); $this->assertSame('example.com:443', $requestAssertion->getRequestTarget()); $this->assertSame('', $requestAssertion->getUri()->getPath()); @@ -359,7 +362,7 @@ public function testRequestConnectAuthorityFormWithDefaultPortWillBePassedAsIs() $data = "CONNECT example.com:80 HTTP/1.1\r\nHost: example.com:80\r\n\r\n"; $this->connection->emit('data', [$data]); - $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion); + $this->assertInstanceOf(ServerRequestInterface::class, $requestAssertion); $this->assertSame('CONNECT', $requestAssertion->getMethod()); $this->assertSame('example.com:80', $requestAssertion->getRequestTarget()); $this->assertSame('', $requestAssertion->getUri()->getPath()); @@ -381,7 +384,7 @@ public function testRequestConnectAuthorityFormNonMatchingHostWillBePassedAsIs() $data = "CONNECT example.com:80 HTTP/1.1\r\nHost: other.example.org\r\n\r\n"; $this->connection->emit('data', [$data]); - $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion); + $this->assertInstanceOf(ServerRequestInterface::class, $requestAssertion); $this->assertSame('CONNECT', $requestAssertion->getMethod()); $this->assertSame('example.com:80', $requestAssertion->getRequestTarget()); $this->assertSame('', $requestAssertion->getUri()->getPath()); @@ -433,7 +436,7 @@ public function testRequestWithoutHostEventUsesSocketAddress() $data = "GET /test HTTP/1.0\r\n\r\n"; $this->connection->emit('data', [$data]); - $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion); + $this->assertInstanceOf(ServerRequestInterface::class, $requestAssertion); $this->assertSame('GET', $requestAssertion->getMethod()); $this->assertSame('/test', $requestAssertion->getRequestTarget()); $this->assertEquals('http://127.0.0.1/test', $requestAssertion->getUri()); @@ -454,7 +457,7 @@ public function testRequestAbsoluteEvent() $data = "GET http://example.com/test HTTP/1.1\r\nHost: example.com\r\n\r\n"; $this->connection->emit('data', [$data]); - $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion); + $this->assertInstanceOf(ServerRequestInterface::class, $requestAssertion); $this->assertSame('GET', $requestAssertion->getMethod()); $this->assertSame('http://example.com/test', $requestAssertion->getRequestTarget()); $this->assertEquals('http://example.com/test', $requestAssertion->getUri()); @@ -476,7 +479,7 @@ public function testRequestAbsoluteNonMatchingHostWillBePassedAsIs() $data = "GET http://example.com/test HTTP/1.1\r\nHost: other.example.org\r\n\r\n"; $this->connection->emit('data', [$data]); - $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion); + $this->assertInstanceOf(ServerRequestInterface::class, $requestAssertion); $this->assertSame('GET', $requestAssertion->getMethod()); $this->assertSame('http://example.com/test', $requestAssertion->getRequestTarget()); $this->assertEquals('http://example.com/test', $requestAssertion->getUri()); @@ -510,7 +513,7 @@ public function testRequestOptionsAsteriskEvent() $data = "OPTIONS * HTTP/1.1\r\nHost: example.com\r\n\r\n"; $this->connection->emit('data', [$data]); - $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion); + $this->assertInstanceOf(ServerRequestInterface::class, $requestAssertion); $this->assertSame('OPTIONS', $requestAssertion->getMethod()); $this->assertSame('*', $requestAssertion->getRequestTarget()); $this->assertEquals('http://example.com', $requestAssertion->getUri()); @@ -532,7 +535,7 @@ public function testRequestOptionsAbsoluteEvent() $data = "OPTIONS http://example.com HTTP/1.1\r\nHost: example.com\r\n\r\n"; $this->connection->emit('data', [$data]); - $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion); + $this->assertInstanceOf(ServerRequestInterface::class, $requestAssertion); $this->assertSame('OPTIONS', $requestAssertion->getMethod()); $this->assertSame('http://example.com', $requestAssertion->getRequestTarget()); $this->assertEquals('http://example.com', $requestAssertion->getUri()); @@ -698,12 +701,10 @@ public function testResponseContainsServerHeader() $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -712,7 +713,7 @@ function ($data) use (&$buffer) { $data = $this->createGetRequest(); $this->connection->emit('data', [$data]); - $this->assertContainsString("\r\nServer: ReactPHP/1\r\n", $buffer); + $this->assertStringContainsString("\r\nServer: ReactPHP/1\r\n", $buffer); } public function testResponsePendingPromiseWillNotSendAnything() @@ -728,12 +729,10 @@ public function testResponsePendingPromiseWillNotSendAnything() $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -758,12 +757,10 @@ public function testResponsePendingPromiseWillBeCancelledIfConnectionCloses() $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -794,12 +791,10 @@ public function testResponseBodyStreamAlreadyClosedWillSendEmptyBodyChunkedEncod $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -829,12 +824,10 @@ public function testResponseBodyStreamEndingWillSendEmptyBodyChunkedEncoded() $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -867,12 +860,10 @@ public function testResponseBodyStreamAlreadyClosedWillSendEmptyBodyPlainHttp10( $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -903,15 +894,13 @@ public function testResponseStreamWillBeClosedIfConnectionIsAlreadyClosed() $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); - $this->connection = $this->getMockBuilder('React\Socket\Connection') + $this->connection = $this->getMockBuilder(Connection::class) ->disableOriginalConstructor() ->setMethods( [ @@ -980,12 +969,10 @@ public function testResponseUpgradeInResponseCanBeUsedToAdvertisePossibleUpgrade $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -1015,12 +1002,10 @@ public function testResponseUpgradeWishInRequestCanBeIgnoredByReturningNormalRes $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -1053,12 +1038,10 @@ public function testResponseUpgradeSwitchingProtocolIncludesConnectionUpgradeHea $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -1091,12 +1074,10 @@ public function testResponseUpgradeSwitchingProtocolWithStreamWillPipeDataToConn $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -1128,12 +1109,10 @@ public function testResponseConnectMethodStreamWillPipeDataToConnection() $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -1185,12 +1164,10 @@ public function testResponseContainsSameRequestProtocolVersionAndChunkedBodyForH $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -1199,8 +1176,8 @@ function ($data) use (&$buffer) { $data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"; $this->connection->emit('data', [$data]); - $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); - $this->assertContainsString("bye", $buffer); + $this->assertStringContainsString("HTTP/1.1 200 OK\r\n", $buffer); + $this->assertStringContainsString("bye", $buffer); } public function testResponseContainsSameRequestProtocolVersionAndRawBodyForHttp10() @@ -1218,12 +1195,10 @@ public function testResponseContainsSameRequestProtocolVersionAndRawBodyForHttp1 $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -1232,9 +1207,9 @@ function ($data) use (&$buffer) { $data = "GET / HTTP/1.0\r\n\r\n"; $this->connection->emit('data', [$data]); - $this->assertContainsString("HTTP/1.0 200 OK\r\n", $buffer); - $this->assertContainsString("\r\n\r\n", $buffer); - $this->assertContainsString("bye", $buffer); + $this->assertStringContainsString("HTTP/1.0 200 OK\r\n", $buffer); + $this->assertStringContainsString("\r\n\r\n", $buffer); + $this->assertStringContainsString("bye", $buffer); } public function testResponseContainsNoResponseBodyForHeadRequest() @@ -1251,12 +1226,10 @@ public function testResponseContainsNoResponseBodyForHeadRequest() $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -1265,9 +1238,9 @@ function ($data) use (&$buffer) { $data = "HEAD / HTTP/1.1\r\nHost: localhost\r\n\r\n"; $this->connection->emit('data', [$data]); - $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); - $this->assertContainsString("\r\nContent-Length: 3\r\n", $buffer); - $this->assertNotContainsString("bye", $buffer); + $this->assertStringContainsString("HTTP/1.1 200 OK\r\n", $buffer); + $this->assertStringContainsString("\r\nContent-Length: 3\r\n", $buffer); + $this->assertStringNotContainsString("bye", $buffer); } public function testResponseContainsNoResponseBodyForHeadRequestWithStreamingResponse() @@ -1287,12 +1260,10 @@ public function testResponseContainsNoResponseBodyForHeadRequestWithStreamingRes $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -1301,8 +1272,8 @@ function ($data) use (&$buffer) { $data = "HEAD / HTTP/1.1\r\nHost: localhost\r\n\r\n"; $this->connection->emit('data', [$data]); - $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); - $this->assertContainsString("\r\nContent-Length: 3\r\n", $buffer); + $this->assertStringContainsString("HTTP/1.1 200 OK\r\n", $buffer); + $this->assertStringContainsString("\r\nContent-Length: 3\r\n", $buffer); } public function testResponseContainsNoResponseBodyAndNoContentLengthForNoContentStatus() @@ -1319,12 +1290,10 @@ public function testResponseContainsNoResponseBodyAndNoContentLengthForNoContent $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -1333,9 +1302,9 @@ function ($data) use (&$buffer) { $data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"; $this->connection->emit('data', [$data]); - $this->assertContainsString("HTTP/1.1 204 No Content\r\n", $buffer); - $this->assertNotContainsString("\r\nContent-Length: 3\r\n", $buffer); - $this->assertNotContainsString("bye", $buffer); + $this->assertStringContainsString("HTTP/1.1 204 No Content\r\n", $buffer); + $this->assertStringNotContainsString("\r\nContent-Length: 3\r\n", $buffer); + $this->assertStringNotContainsString("bye", $buffer); } public function testResponseContainsNoResponseBodyAndNoContentLengthForNoContentStatusResponseWithStreamingBody() @@ -1355,12 +1324,10 @@ public function testResponseContainsNoResponseBodyAndNoContentLengthForNoContent $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -1369,8 +1336,8 @@ function ($data) use (&$buffer) { $data = "HEAD / HTTP/1.1\r\nHost: localhost\r\n\r\n"; $this->connection->emit('data', [$data]); - $this->assertContainsString("HTTP/1.1 204 No Content\r\n", $buffer); - $this->assertNotContainsString("\r\nContent-Length: 3\r\n", $buffer); + $this->assertStringContainsString("HTTP/1.1 204 No Content\r\n", $buffer); + $this->assertStringNotContainsString("\r\nContent-Length: 3\r\n", $buffer); } public function testResponseContainsNoContentLengthHeaderForNotModifiedStatus() @@ -1387,12 +1354,10 @@ public function testResponseContainsNoContentLengthHeaderForNotModifiedStatus() $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -1401,8 +1366,8 @@ function ($data) use (&$buffer) { $data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"; $this->connection->emit('data', [$data]); - $this->assertContainsString("HTTP/1.1 304 Not Modified\r\n", $buffer); - $this->assertNotContainsString("\r\nContent-Length: 0\r\n", $buffer); + $this->assertStringContainsString("HTTP/1.1 304 Not Modified\r\n", $buffer); + $this->assertStringNotContainsString("\r\nContent-Length: 0\r\n", $buffer); } public function testResponseContainsExplicitContentLengthHeaderForNotModifiedStatus() @@ -1419,12 +1384,10 @@ public function testResponseContainsExplicitContentLengthHeaderForNotModifiedSta $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -1433,8 +1396,8 @@ function ($data) use (&$buffer) { $data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"; $this->connection->emit('data', [$data]); - $this->assertContainsString("HTTP/1.1 304 Not Modified\r\n", $buffer); - $this->assertContainsString("\r\nContent-Length: 3\r\n", $buffer); + $this->assertStringContainsString("HTTP/1.1 304 Not Modified\r\n", $buffer); + $this->assertStringContainsString("\r\nContent-Length: 3\r\n", $buffer); } public function testResponseContainsExplicitContentLengthHeaderForHeadRequests() @@ -1451,12 +1414,10 @@ public function testResponseContainsExplicitContentLengthHeaderForHeadRequests() $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -1465,8 +1426,8 @@ function ($data) use (&$buffer) { $data = "HEAD / HTTP/1.1\r\nHost: localhost\r\n\r\n"; $this->connection->emit('data', [$data]); - $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); - $this->assertContainsString("\r\nContent-Length: 3\r\n", $buffer); + $this->assertStringContainsString("HTTP/1.1 200 OK\r\n", $buffer); + $this->assertStringContainsString("\r\nContent-Length: 3\r\n", $buffer); } public function testResponseContainsNoResponseBodyForNotModifiedStatus() @@ -1483,12 +1444,10 @@ public function testResponseContainsNoResponseBodyForNotModifiedStatus() $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -1497,9 +1456,9 @@ function ($data) use (&$buffer) { $data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"; $this->connection->emit('data', [$data]); - $this->assertContainsString("HTTP/1.1 304 Not Modified\r\n", $buffer); - $this->assertContainsString("\r\nContent-Length: 3\r\n", $buffer); - $this->assertNotContainsString("bye", $buffer); + $this->assertStringContainsString("HTTP/1.1 304 Not Modified\r\n", $buffer); + $this->assertStringContainsString("\r\nContent-Length: 3\r\n", $buffer); + $this->assertStringNotContainsString("bye", $buffer); } public function testResponseContainsNoResponseBodyForNotModifiedStatusWithStreamingBody() @@ -1519,12 +1478,10 @@ public function testResponseContainsNoResponseBodyForNotModifiedStatusWithStream $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -1533,8 +1490,8 @@ function ($data) use (&$buffer) { $data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"; $this->connection->emit('data', [$data]); - $this->assertContainsString("HTTP/1.1 304 Not Modified\r\n", $buffer); - $this->assertContainsString("\r\nContent-Length: 3\r\n", $buffer); + $this->assertStringContainsString("HTTP/1.1 304 Not Modified\r\n", $buffer); + $this->assertStringContainsString("\r\nContent-Length: 3\r\n", $buffer); } public function testRequestInvalidHttpProtocolVersionWillEmitErrorAndSendErrorResponse() @@ -1550,12 +1507,10 @@ public function testRequestInvalidHttpProtocolVersionWillEmitErrorAndSendErrorRe $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -1564,11 +1519,11 @@ function ($data) use (&$buffer) { $data = "GET / HTTP/1.2\r\nHost: localhost\r\n\r\n"; $this->connection->emit('data', [$data]); - $this->assertInstanceOf('InvalidArgumentException', $error); + $this->assertInstanceOf(\InvalidArgumentException::class, $error); - $this->assertContainsString("HTTP/1.1 505 HTTP Version Not Supported\r\n", $buffer); - $this->assertContainsString("\r\n\r\n", $buffer); - $this->assertContainsString("Error 505: HTTP Version Not Supported", $buffer); + $this->assertStringContainsString("HTTP/1.1 505 HTTP Version Not Supported\r\n", $buffer); + $this->assertStringContainsString("\r\n\r\n", $buffer); + $this->assertStringContainsString("Error 505: HTTP Version Not Supported", $buffer); } public function testRequestOverflowWillEmitErrorAndSendErrorResponse() @@ -1584,12 +1539,10 @@ public function testRequestOverflowWillEmitErrorAndSendErrorResponse() $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -1599,10 +1552,10 @@ function ($data) use (&$buffer) { $data .= str_repeat('A', 8193 - strlen($data)) . "\r\n\r\n"; $this->connection->emit('data', [$data]); - $this->assertInstanceOf('OverflowException', $error); + $this->assertInstanceOf(\OverflowException::class, $error); - $this->assertContainsString("HTTP/1.1 431 Request Header Fields Too Large\r\n", $buffer); - $this->assertContainsString("\r\n\r\nError 431: Request Header Fields Too Large", $buffer); + $this->assertStringContainsString("HTTP/1.1 431 Request Header Fields Too Large\r\n", $buffer); + $this->assertStringContainsString("\r\n\r\nError 431: Request Header Fields Too Large", $buffer); } public function testRequestInvalidWillEmitErrorAndSendErrorResponse() @@ -1618,12 +1571,10 @@ public function testRequestInvalidWillEmitErrorAndSendErrorResponse() $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -1632,10 +1583,10 @@ function ($data) use (&$buffer) { $data = "bad request\r\n\r\n"; $this->connection->emit('data', [$data]); - $this->assertInstanceOf('InvalidArgumentException', $error); + $this->assertInstanceOf(\InvalidArgumentException::class, $error); - $this->assertContainsString("HTTP/1.1 400 Bad Request\r\n", $buffer); - $this->assertContainsString("\r\n\r\nError 400: Bad Request", $buffer); + $this->assertStringContainsString("HTTP/1.1 400 Bad Request\r\n", $buffer); + $this->assertStringContainsString("\r\n\r\nError 400: Bad Request", $buffer); } public function testRequestContentLengthBodyDataWillEmitDataEventOnRequestStream() @@ -1959,7 +1910,7 @@ public function testRequestZeroContentLengthWillEmitEndAndAdditionalDataWillBeIg public function testRequestInvalidChunkHeaderTooLongWillEmitErrorOnRequestStream() { - $errorEvent = $this->expectCallableOnceWith($this->isInstanceOf('Exception')); + $errorEvent = $this->expectCallableOnceWith($this->isInstanceOf(\Exception::class)); $server = new StreamingServer(Loop::get(), function ($request) use ($errorEvent){ $request->getBody()->on('error', $errorEvent); return resolve(new Response()); @@ -1984,7 +1935,7 @@ public function testRequestInvalidChunkHeaderTooLongWillEmitErrorOnRequestStream public function testRequestInvalidChunkBodyTooLongWillEmitErrorOnRequestStream() { - $errorEvent = $this->expectCallableOnceWith($this->isInstanceOf('Exception')); + $errorEvent = $this->expectCallableOnceWith($this->isInstanceOf(\Exception::class)); $server = new StreamingServer(Loop::get(), function ($request) use ($errorEvent){ $request->getBody()->on('error', $errorEvent); }); @@ -2006,7 +1957,7 @@ public function testRequestInvalidChunkBodyTooLongWillEmitErrorOnRequestStream() public function testRequestUnexpectedEndOfRequestWithChunkedTransferConnectionWillEmitErrorOnRequestStream() { - $errorEvent = $this->expectCallableOnceWith($this->isInstanceOf('Exception')); + $errorEvent = $this->expectCallableOnceWith($this->isInstanceOf(\Exception::class)); $server = new StreamingServer(Loop::get(), function ($request) use ($errorEvent){ $request->getBody()->on('error', $errorEvent); }); @@ -2029,7 +1980,7 @@ public function testRequestUnexpectedEndOfRequestWithChunkedTransferConnectionWi public function testRequestInvalidChunkHeaderWillEmitErrorOnRequestStream() { - $errorEvent = $this->expectCallableOnceWith($this->isInstanceOf('Exception')); + $errorEvent = $this->expectCallableOnceWith($this->isInstanceOf(\Exception::class)); $server = new StreamingServer(Loop::get(), function ($request) use ($errorEvent){ $request->getBody()->on('error', $errorEvent); }); @@ -2051,7 +2002,7 @@ public function testRequestInvalidChunkHeaderWillEmitErrorOnRequestStream() public function testRequestUnexpectedEndOfRequestWithContentLengthWillEmitErrorOnRequestStream() { - $errorEvent = $this->expectCallableOnceWith($this->isInstanceOf('Exception')); + $errorEvent = $this->expectCallableOnceWith($this->isInstanceOf(\Exception::class)); $server = new StreamingServer(Loop::get(), function ($request) use ($errorEvent){ $request->getBody()->on('error', $errorEvent); }); @@ -2134,12 +2085,10 @@ public function testResponseWithBodyStreamWillUseChunkedTransferEncodingByDefaul $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -2150,8 +2099,8 @@ function ($data) use (&$buffer) { $this->connection->emit('data', [$data]); $stream->emit('data', ['hello']); - $this->assertContainsString("Transfer-Encoding: chunked", $buffer); - $this->assertContainsString("hello", $buffer); + $this->assertStringContainsString("Transfer-Encoding: chunked", $buffer); + $this->assertStringContainsString("hello", $buffer); } public function testResponseWithBodyStringWillOverwriteExplicitContentLengthAndTransferEncoding() @@ -2171,12 +2120,10 @@ public function testResponseWithBodyStringWillOverwriteExplicitContentLengthAndT $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -2186,14 +2133,14 @@ function ($data) use (&$buffer) { $this->connection->emit('data', [$data]); - $this->assertNotContainsString("Transfer-Encoding: chunked", $buffer); - $this->assertContainsString("Content-Length: 5", $buffer); - $this->assertContainsString("hello", $buffer); + $this->assertStringNotContainsString("Transfer-Encoding: chunked", $buffer); + $this->assertStringContainsString("Content-Length: 5", $buffer); + $this->assertStringContainsString("hello", $buffer); } public function testResponseContainsResponseBodyWithTransferEncodingChunkedForBodyWithUnknownSize() { - $body = $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(); + $body = $this->createMock(StreamInterface::class); $body->expects($this->once())->method('getSize')->willReturn(null); $body->expects($this->once())->method('__toString')->willReturn('body'); @@ -2209,12 +2156,10 @@ public function testResponseContainsResponseBodyWithTransferEncodingChunkedForBo $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -2223,14 +2168,14 @@ function ($data) use (&$buffer) { $data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"; $this->connection->emit('data', [$data]); - $this->assertContainsString("Transfer-Encoding: chunked", $buffer); - $this->assertNotContainsString("Content-Length:", $buffer); - $this->assertContainsString("body", $buffer); + $this->assertStringContainsString("Transfer-Encoding: chunked", $buffer); + $this->assertStringNotContainsString("Content-Length:", $buffer); + $this->assertStringContainsString("body", $buffer); } public function testResponseContainsResponseBodyWithPlainBodyWithUnknownSizeForLegacyHttp10() { - $body = $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(); + $body = $this->createMock(StreamInterface::class); $body->expects($this->once())->method('getSize')->willReturn(null); $body->expects($this->once())->method('__toString')->willReturn('body'); @@ -2246,12 +2191,10 @@ public function testResponseContainsResponseBodyWithPlainBodyWithUnknownSizeForL $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -2260,9 +2203,9 @@ function ($data) use (&$buffer) { $data = "GET / HTTP/1.0\r\nHost: localhost\r\n\r\n"; $this->connection->emit('data', [$data]); - $this->assertNotContainsString("Transfer-Encoding: chunked", $buffer); - $this->assertNotContainsString("Content-Length:", $buffer); - $this->assertContainsString("body", $buffer); + $this->assertStringNotContainsString("Transfer-Encoding: chunked", $buffer); + $this->assertStringNotContainsString("Content-Length:", $buffer); + $this->assertStringContainsString("body", $buffer); } public function testResponseWithCustomTransferEncodingWillBeIgnoredAndUseChunkedTransferEncodingInstead() @@ -2282,12 +2225,10 @@ public function testResponseWithCustomTransferEncodingWillBeIgnoredAndUseChunked $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -2298,9 +2239,9 @@ function ($data) use (&$buffer) { $this->connection->emit('data', [$data]); $stream->emit('data', ['hello']); - $this->assertContainsString('Transfer-Encoding: chunked', $buffer); - $this->assertNotContainsString('Transfer-Encoding: custom', $buffer); - $this->assertContainsString("5\r\nhello\r\n", $buffer); + $this->assertStringContainsString('Transfer-Encoding: chunked', $buffer); + $this->assertStringNotContainsString('Transfer-Encoding: custom', $buffer); + $this->assertStringContainsString("5\r\nhello\r\n", $buffer); } public function testResponseWithoutExplicitDateHeaderWillAddCurrentDateFromClock() @@ -2321,12 +2262,10 @@ public function testResponseWithoutExplicitDateHeaderWillAddCurrentDateFromClock $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -2336,9 +2275,9 @@ function ($data) use (&$buffer) { $this->connection->emit('data', [$data]); - $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); - $this->assertContainsString("Date: Thu, 19 May 2022 14:54:51 GMT\r\n", $buffer); - $this->assertContainsString("\r\n\r\n", $buffer); + $this->assertStringContainsString("HTTP/1.1 200 OK\r\n", $buffer); + $this->assertStringContainsString("Date: Thu, 19 May 2022 14:54:51 GMT\r\n", $buffer); + $this->assertStringContainsString("\r\n\r\n", $buffer); } public function testResponseWithCustomDateHeaderOverwritesDefault() @@ -2354,12 +2293,10 @@ public function testResponseWithCustomDateHeaderOverwritesDefault() $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -2369,9 +2306,9 @@ function ($data) use (&$buffer) { $this->connection->emit('data', [$data]); - $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); - $this->assertContainsString("Date: Tue, 15 Nov 1994 08:12:31 GMT\r\n", $buffer); - $this->assertContainsString("\r\n\r\n", $buffer); + $this->assertStringContainsString("HTTP/1.1 200 OK\r\n", $buffer); + $this->assertStringContainsString("Date: Tue, 15 Nov 1994 08:12:31 GMT\r\n", $buffer); + $this->assertStringContainsString("\r\n\r\n", $buffer); } public function testResponseWithEmptyDateHeaderRemovesDateHeader() @@ -2387,12 +2324,10 @@ public function testResponseWithEmptyDateHeaderRemovesDateHeader() $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -2402,9 +2337,9 @@ function ($data) use (&$buffer) { $this->connection->emit('data', [$data]); - $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); - $this->assertNotContainsString("Date:", $buffer); - $this->assertContainsString("\r\n\r\n", $buffer); + $this->assertStringContainsString("HTTP/1.1 200 OK\r\n", $buffer); + $this->assertStringNotContainsString("Date:", $buffer); + $this->assertStringContainsString("\r\n\r\n", $buffer); } public function testResponseCanContainMultipleCookieHeaders() @@ -2427,12 +2362,10 @@ public function testResponseCanContainMultipleCookieHeaders() $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -2455,12 +2388,10 @@ public function testReponseWithExpectContinueRequestContainsContinueWithLaterRes $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -2473,8 +2404,8 @@ function ($data) use (&$buffer) { $data .= "\r\n"; $this->connection->emit('data', [$data]); - $this->assertContainsString("HTTP/1.1 100 Continue\r\n", $buffer); - $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); + $this->assertStringContainsString("HTTP/1.1 100 Continue\r\n", $buffer); + $this->assertStringContainsString("HTTP/1.1 200 OK\r\n", $buffer); } public function testResponseWithExpectContinueRequestWontSendContinueForHttp10() @@ -2487,12 +2418,10 @@ public function testResponseWithExpectContinueRequestWontSendContinueForHttp10() $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -2503,13 +2432,13 @@ function ($data) use (&$buffer) { $data .= "\r\n"; $this->connection->emit('data', [$data]); - $this->assertContainsString("HTTP/1.0 200 OK\r\n", $buffer); - $this->assertNotContainsString("HTTP/1.1 100 Continue\r\n\r\n", $buffer); + $this->assertStringContainsString("HTTP/1.0 200 OK\r\n", $buffer); + $this->assertStringNotContainsString("HTTP/1.1 100 Continue\r\n\r\n", $buffer); } public function testInvalidCallbackFunctionLeadsToException() { - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); new StreamingServer(Loop::get(), 'invalid'); } @@ -2529,12 +2458,10 @@ public function testResponseBodyStreamWillStreamDataWithChunkedTransferEncoding( $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -2546,10 +2473,10 @@ function ($data) use (&$buffer) { $input->emit('data', ['1']); $input->emit('data', ['23']); - $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); - $this->assertContainsString("\r\n\r\n", $buffer); - $this->assertContainsString("1\r\n1\r\n", $buffer); - $this->assertContainsString("2\r\n23\r\n", $buffer); + $this->assertStringContainsString("HTTP/1.1 200 OK\r\n", $buffer); + $this->assertStringContainsString("\r\n\r\n", $buffer); + $this->assertStringContainsString("1\r\n1\r\n", $buffer); + $this->assertStringContainsString("2\r\n23\r\n", $buffer); } public function testResponseBodyStreamWithContentLengthWillStreamTillLengthWithoutTransferEncoding() @@ -2568,12 +2495,10 @@ public function testResponseBodyStreamWithContentLengthWillStreamTillLengthWitho $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -2585,11 +2510,11 @@ function ($data) use (&$buffer) { $input->emit('data', ['hel']); $input->emit('data', ['lo']); - $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); - $this->assertContainsString("Content-Length: 5\r\n", $buffer); - $this->assertNotContainsString("Transfer-Encoding", $buffer); - $this->assertContainsString("\r\n\r\n", $buffer); - $this->assertContainsString("hello", $buffer); + $this->assertStringContainsString("HTTP/1.1 200 OK\r\n", $buffer); + $this->assertStringContainsString("Content-Length: 5\r\n", $buffer); + $this->assertStringNotContainsString("Transfer-Encoding", $buffer); + $this->assertStringContainsString("\r\n\r\n", $buffer); + $this->assertStringContainsString("hello", $buffer); } public function testResponseWithResponsePromise() @@ -2602,12 +2527,10 @@ public function testResponseWithResponsePromise() $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -2616,8 +2539,8 @@ function ($data) use (&$buffer) { $data = $this->createGetRequest(); $this->connection->emit('data', [$data]); - $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); - $this->assertContainsString("\r\n\r\n", $buffer); + $this->assertStringContainsString("HTTP/1.1 200 OK\r\n", $buffer); + $this->assertStringContainsString("\r\n\r\n", $buffer); } public function testResponseReturnInvalidTypeWillResultInError() @@ -2635,12 +2558,10 @@ public function testResponseReturnInvalidTypeWillResultInError() $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -2650,8 +2571,8 @@ function ($data) use (&$buffer) { $this->connection->emit('data', [$data]); - $this->assertContainsString("HTTP/1.1 500 Internal Server Error\r\n", $buffer); - $this->assertInstanceOf('RuntimeException', $exception); + $this->assertStringContainsString("HTTP/1.1 500 Internal Server Error\r\n", $buffer); + $this->assertInstanceOf(\RuntimeException::class, $exception); } public function testResponseResolveWrongTypeInPromiseWillResultInError() @@ -2664,12 +2585,10 @@ public function testResponseResolveWrongTypeInPromiseWillResultInError() $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -2679,7 +2598,7 @@ function ($data) use (&$buffer) { $this->connection->emit('data', [$data]); - $this->assertContainsString("HTTP/1.1 500 Internal Server Error\r\n", $buffer); + $this->assertStringContainsString("HTTP/1.1 500 Internal Server Error\r\n", $buffer); } public function testResponseRejectedPromiseWillResultInErrorMessage() @@ -2695,12 +2614,10 @@ public function testResponseRejectedPromiseWillResultInErrorMessage() $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -2710,7 +2627,7 @@ function ($data) use (&$buffer) { $this->connection->emit('data', [$data]); - $this->assertContainsString("HTTP/1.1 500 Internal Server Error\r\n", $buffer); + $this->assertStringContainsString("HTTP/1.1 500 Internal Server Error\r\n", $buffer); } public function testResponseExceptionInCallbackWillResultInErrorMessage() @@ -2726,12 +2643,10 @@ public function testResponseExceptionInCallbackWillResultInErrorMessage() $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -2741,7 +2656,7 @@ function ($data) use (&$buffer) { $this->connection->emit('data', [$data]); - $this->assertContainsString("HTTP/1.1 500 Internal Server Error\r\n", $buffer); + $this->assertStringContainsString("HTTP/1.1 500 Internal Server Error\r\n", $buffer); } public function testResponseWithContentLengthHeaderForStringBodyOverwritesTransferEncoding() @@ -2758,12 +2673,10 @@ public function testResponseWithContentLengthHeaderForStringBodyOverwritesTransf $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -2773,11 +2686,11 @@ function ($data) use (&$buffer) { $this->connection->emit('data', [$data]); - $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); - $this->assertContainsString("Content-Length: 5\r\n", $buffer); - $this->assertContainsString("hello", $buffer); + $this->assertStringContainsString("HTTP/1.1 200 OK\r\n", $buffer); + $this->assertStringContainsString("Content-Length: 5\r\n", $buffer); + $this->assertStringContainsString("hello", $buffer); - $this->assertNotContainsString("Transfer-Encoding", $buffer); + $this->assertStringNotContainsString("Transfer-Encoding", $buffer); } public function testResponseWillBeHandled() @@ -2790,12 +2703,10 @@ public function testResponseWillBeHandled() $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -2805,7 +2716,7 @@ function ($data) use (&$buffer) { $this->connection->emit('data', [$data]); - $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); + $this->assertStringContainsString("HTTP/1.1 200 OK\r\n", $buffer); } public function testResponseExceptionThrowInCallBackFunctionWillResultInErrorMessage() @@ -2823,12 +2734,10 @@ public function testResponseExceptionThrowInCallBackFunctionWillResultInErrorMes $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -2838,8 +2747,8 @@ function ($data) use (&$buffer) { $this->connection->emit('data', [$data]); - $this->assertInstanceOf('RuntimeException', $exception); - $this->assertContainsString("HTTP/1.1 500 Internal Server Error\r\n", $buffer); + $this->assertInstanceOf(\RuntimeException::class, $exception); + $this->assertStringContainsString("HTTP/1.1 500 Internal Server Error\r\n", $buffer); $this->assertEquals('hello', $exception->getPrevious()->getMessage()); } @@ -2858,12 +2767,10 @@ public function testResponseThrowableThrowInCallBackFunctionWillResultInErrorMes $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -2881,8 +2788,8 @@ function ($data) use (&$buffer) { ); } - $this->assertInstanceOf('RuntimeException', $exception); - $this->assertContainsString("HTTP/1.1 500 Internal Server Error\r\n", $buffer); + $this->assertInstanceOf(\RuntimeException::class, $exception); + $this->assertStringContainsString("HTTP/1.1 500 Internal Server Error\r\n", $buffer); $this->assertEquals('hello', $exception->getPrevious()->getMessage()); } @@ -2903,12 +2810,10 @@ public function testResponseRejectOfNonExceptionWillResultInErrorMessage() $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -2918,54 +2823,52 @@ function ($data) use (&$buffer) { $this->connection->emit('data', [$data]); - $this->assertContainsString("HTTP/1.1 500 Internal Server Error\r\n", $buffer); - $this->assertInstanceOf('RuntimeException', $exception); + $this->assertStringContainsString("HTTP/1.1 500 Internal Server Error\r\n", $buffer); + $this->assertInstanceOf(\RuntimeException::class, $exception); } public static function provideInvalidResponse() { $response = new Response(200, [], '', '1.1', 'OK'); - return [ - [ - $response->withStatus(99, 'OK') - ], - [ - $response->withStatus(1000, 'OK') - ], - [ - $response->withStatus(200, "Invald\r\nReason: Yes") - ], - [ - $response->withHeader('Invalid', "Yes\r\n") - ], - [ - $response->withHeader('Invalid', "Yes\n") - ], - [ - $response->withHeader('Invalid', "Yes\r") - ], - [ - $response->withHeader("Inva\r\nlid", 'Yes') - ], - [ - $response->withHeader("Inva\nlid", 'Yes') - ], - [ - $response->withHeader("Inva\rlid", 'Yes') - ], - [ - $response->withHeader('Inva Lid', 'Yes') - ], - [ - $response->withHeader('Inva:Lid', 'Yes') - ], - [ - $response->withHeader('Invalid', "Val\0ue") - ], - [ - $response->withHeader("Inva\0lid", 'Yes') - ] + yield [ + $response->withStatus(99, 'OK') + ]; + yield [ + $response->withStatus(1000, 'OK') + ]; + yield [ + $response->withStatus(200, "Invald\r\nReason: Yes") + ]; + yield [ + $response->withHeader('Invalid', "Yes\r\n") + ]; + yield [ + $response->withHeader('Invalid', "Yes\n") + ]; + yield [ + $response->withHeader('Invalid', "Yes\r") + ]; + yield [ + $response->withHeader("Inva\r\nlid", 'Yes') + ]; + yield [ + $response->withHeader("Inva\nlid", 'Yes') + ]; + yield [ + $response->withHeader("Inva\rlid", 'Yes') + ]; + yield [ + $response->withHeader('Inva Lid', 'Yes') + ]; + yield [ + $response->withHeader('Inva:Lid', 'Yes') + ]; + yield [ + $response->withHeader('Invalid', "Val\0ue") + ]; + yield [ + $response->withHeader("Inva\0lid", 'Yes') ]; } @@ -2988,12 +2891,10 @@ public function testInvalidResponseObjectWillResultInErrorMessage(ResponseInterf $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -3003,8 +2904,8 @@ function ($data) use (&$buffer) { $this->connection->emit('data', [$data]); - $this->assertContainsString("HTTP/1.1 500 Internal Server Error\r\n", $buffer); - $this->assertInstanceOf('InvalidArgumentException', $exception); + $this->assertStringContainsString("HTTP/1.1 500 Internal Server Error\r\n", $buffer); + $this->assertInstanceOf(\InvalidArgumentException::class, $exception); } public function testRequestServerRequestParams() @@ -3147,7 +3048,7 @@ public function testNewConnectionWillInvokeParserOnce() { $server = new StreamingServer(Loop::get(), $this->expectCallableNever()); - $parser = $this->getMockBuilder('React\Http\Io\RequestHeaderParser')->disableOriginalConstructor()->getMock(); + $parser = $this->createMock(RequestHeaderParser::class); $parser->expects($this->once())->method('handle'); $ref = new \ReflectionProperty($server, 'parser'); @@ -3164,7 +3065,7 @@ public function testNewConnectionWillInvokeParserOnceAndInvokeRequestHandlerWhen $server = new StreamingServer(Loop::get(), $this->expectCallableOnceWith($request)); - $parser = $this->getMockBuilder('React\Http\Io\RequestHeaderParser')->disableOriginalConstructor()->getMock(); + $parser = $this->createMock(RequestHeaderParser::class); $parser->expects($this->once())->method('handle'); $ref = new \ReflectionProperty($server, 'parser'); @@ -3187,7 +3088,7 @@ public function testNewConnectionWillInvokeParserOnceAndInvokeRequestHandlerWhen $server = new StreamingServer(Loop::get(), $this->expectCallableOnceWith($request)); - $parser = $this->getMockBuilder('React\Http\Io\RequestHeaderParser')->disableOriginalConstructor()->getMock(); + $parser = $this->createMock(RequestHeaderParser::class); $parser->expects($this->once())->method('handle'); $ref = new \ReflectionProperty($server, 'parser'); @@ -3212,7 +3113,7 @@ public function testNewConnectionWillInvokeParserOnceAndInvokeRequestHandlerWhen return new Response(200, ['Connection' => 'close']); }); - $parser = $this->getMockBuilder('React\Http\Io\RequestHeaderParser')->disableOriginalConstructor()->getMock(); + $parser = $this->createMock(RequestHeaderParser::class); $parser->expects($this->once())->method('handle'); $ref = new \ReflectionProperty($server, 'parser'); @@ -3237,7 +3138,7 @@ public function testNewConnectionWillInvokeParserTwiceAfterInvokingRequestHandle return new Response(); }); - $parser = $this->getMockBuilder('React\Http\Io\RequestHeaderParser')->disableOriginalConstructor()->getMock(); + $parser = $this->createMock(RequestHeaderParser::class); $parser->expects($this->exactly(2))->method('handle'); $ref = new \ReflectionProperty($server, 'parser'); @@ -3262,7 +3163,7 @@ public function testNewConnectionWillInvokeParserTwiceAfterInvokingRequestHandle return new Response(); }); - $parser = $this->getMockBuilder('React\Http\Io\RequestHeaderParser')->disableOriginalConstructor()->getMock(); + $parser = $this->createMock(RequestHeaderParser::class); $parser->expects($this->exactly(2))->method('handle'); $ref = new \ReflectionProperty($server, 'parser'); @@ -3288,7 +3189,7 @@ public function testNewConnectionWillInvokeParserOnceAfterInvokingRequestHandler return new Response(200, [], $body); }); - $parser = $this->getMockBuilder('React\Http\Io\RequestHeaderParser')->disableOriginalConstructor()->getMock(); + $parser = $this->createMock(RequestHeaderParser::class); $parser->expects($this->once())->method('handle'); $ref = new \ReflectionProperty($server, 'parser'); @@ -3314,7 +3215,7 @@ public function testNewConnectionWillInvokeParserTwiceAfterInvokingRequestHandle return new Response(200, [], $body); }); - $parser = $this->getMockBuilder('React\Http\Io\RequestHeaderParser')->disableOriginalConstructor()->getMock(); + $parser = $this->createMock(RequestHeaderParser::class); $parser->expects($this->exactly(2))->method('handle'); $ref = new \ReflectionProperty($server, 'parser'); diff --git a/tests/Io/TransactionTest.php b/tests/Io/TransactionTest.php index b4825024..3833cfd3 100644 --- a/tests/Io/TransactionTest.php +++ b/tests/Io/TransactionTest.php @@ -5,7 +5,10 @@ use PHPUnit\Framework\MockObject\MockObject; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; +use React\EventLoop\LoopInterface; +use React\EventLoop\TimerInterface; use React\Http\Io\ReadableBodyStream; +use React\Http\Io\Sender; use React\Http\Io\Transaction; use React\Http\Message\Request; use React\Http\Message\Response; @@ -13,6 +16,8 @@ use React\EventLoop\Loop; use React\Promise\Deferred; use React\Promise\Promise; +use React\Promise\PromiseInterface; +use React\Stream\ReadableStreamInterface; use React\Stream\ThroughStream; use React\Tests\Http\TestCase; use function React\Async\await; @@ -24,12 +29,12 @@ class TransactionTest extends TestCase public function testWithOptionsReturnsNewInstanceWithChangedOption() { $sender = $this->makeSenderMock(); - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop = $this->createMock(LoopInterface::class); $transaction = new Transaction($sender, $loop); $new = $transaction->withOptions(['followRedirects' => false]); - $this->assertInstanceOf('React\Http\Io\Transaction', $new); + $this->assertInstanceOf(Transaction::class, $new); $this->assertNotSame($transaction, $new); $ref = new \ReflectionProperty($new, 'followRedirects'); @@ -41,7 +46,7 @@ public function testWithOptionsReturnsNewInstanceWithChangedOption() public function testWithOptionsDoesNotChangeOriginalInstance() { $sender = $this->makeSenderMock(); - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop = $this->createMock(LoopInterface::class); $transaction = new Transaction($sender, $loop); $transaction->withOptions(['followRedirects' => false]); @@ -55,7 +60,7 @@ public function testWithOptionsDoesNotChangeOriginalInstance() public function testWithOptionsNullValueReturnsNewInstanceWithDefaultOption() { $sender = $this->makeSenderMock(); - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop = $this->createMock(LoopInterface::class); $transaction = new Transaction($sender, $loop); $transaction = $transaction->withOptions(['followRedirects' => false]); @@ -69,34 +74,34 @@ public function testWithOptionsNullValueReturnsNewInstanceWithDefaultOption() public function testTimeoutExplicitOptionWillStartTimeoutTimer() { - $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $timer = $this->createMock(TimerInterface::class); + $loop = $this->createMock(LoopInterface::class); $loop->expects($this->once())->method('addTimer')->with(2, $this->anything())->willReturn($timer); $loop->expects($this->never())->method('cancelTimer'); - $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); + $request = $this->createMock(RequestInterface::class); - $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); - $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new Promise(function () { })); + $sender = $this->createMock(Sender::class); + $sender->expects($this->once())->method('send')->with($request)->willReturn(new Promise(function () { })); $transaction = new Transaction($sender, $loop); $transaction = $transaction->withOptions(['timeout' => 2]); $promise = $transaction->send($request); - $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); + $this->assertInstanceOf(PromiseInterface::class, $promise); } public function testTimeoutImplicitFromIniWillStartTimeoutTimer() { - $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $timer = $this->createMock(TimerInterface::class); + $loop = $this->createMock(LoopInterface::class); $loop->expects($this->once())->method('addTimer')->with(2, $this->anything())->willReturn($timer); $loop->expects($this->never())->method('cancelTimer'); - $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); + $request = $this->createMock(RequestInterface::class); - $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); - $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new Promise(function () { })); + $sender = $this->createMock(Sender::class); + $sender->expects($this->once())->method('send')->with($request)->willReturn(new Promise(function () { })); $transaction = new Transaction($sender, $loop); @@ -105,24 +110,24 @@ public function testTimeoutImplicitFromIniWillStartTimeoutTimer() $promise = $transaction->send($request); ini_set('default_socket_timeout', $old); - $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); + $this->assertInstanceOf(PromiseInterface::class, $promise); } public function testTimeoutExplicitOptionWillRejectWhenTimerFires() { $timeout = null; - $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $timer = $this->createMock(TimerInterface::class); + $loop = $this->createMock(LoopInterface::class); $loop->expects($this->once())->method('addTimer')->with(2, $this->callback(function ($cb) use (&$timeout) { $timeout = $cb; return true; }))->willReturn($timer); $loop->expects($this->never())->method('cancelTimer'); - $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); + $request = $this->createMock(RequestInterface::class); - $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); - $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new Promise(function () { })); + $sender = $this->createMock(Sender::class); + $sender->expects($this->once())->method('send')->with($request)->willReturn(new Promise(function () { })); $transaction = new Transaction($sender, $loop); $transaction = $transaction->withOptions(['timeout' => 2]); @@ -136,42 +141,42 @@ public function testTimeoutExplicitOptionWillRejectWhenTimerFires() $exception = $e; }); - $this->assertInstanceOf('RuntimeException', $exception); + $this->assertInstanceOf(\RuntimeException::class, $exception); $this->assertEquals('Request timed out after 2 seconds', $exception->getMessage()); } public function testTimeoutExplicitOptionWillNotStartTimeoutWhenSenderResolvesImmediately() { - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop = $this->createMock(LoopInterface::class); $loop->expects($this->never())->method('addTimer'); - $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); + $request = $this->createMock(RequestInterface::class); $response = new Response(200, [], ''); - $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); - $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(resolve($response)); + $sender = $this->createMock(Sender::class); + $sender->expects($this->once())->method('send')->with($request)->willReturn(resolve($response)); $transaction = new Transaction($sender, $loop); $transaction = $transaction->withOptions(['timeout' => 0.001]); $promise = $transaction->send($request); - $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); + $this->assertInstanceOf(PromiseInterface::class, $promise); $promise->then($this->expectCallableOnceWith($response)); } public function testTimeoutExplicitOptionWillCancelTimeoutTimerWhenSenderResolvesLaterOn() { - $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $timer = $this->createMock(TimerInterface::class); + $loop = $this->createMock(LoopInterface::class); $loop->expects($this->once())->method('addTimer')->willReturn($timer); $loop->expects($this->once())->method('cancelTimer')->with($timer); - $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); + $request = $this->createMock(RequestInterface::class); $response = new Response(200, [], ''); $deferred = new Deferred(); - $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); - $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn($deferred->promise()); + $sender = $this->createMock(Sender::class); + $sender->expects($this->once())->method('send')->with($request)->willReturn($deferred->promise()); $transaction = new Transaction($sender, $loop); $transaction = $transaction->withOptions(['timeout' => 0.001]); @@ -179,41 +184,41 @@ public function testTimeoutExplicitOptionWillCancelTimeoutTimerWhenSenderResolve $deferred->resolve($response); - $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); + $this->assertInstanceOf(PromiseInterface::class, $promise); $promise->then($this->expectCallableOnceWith($response)); } public function testTimeoutExplicitOptionWillNotStartTimeoutWhenSenderRejectsImmediately() { - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop = $this->createMock(LoopInterface::class); $loop->expects($this->never())->method('addTimer'); - $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); + $request = $this->createMock(RequestInterface::class); $exception = new \RuntimeException(); - $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); - $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(reject($exception)); + $sender = $this->createMock(Sender::class); + $sender->expects($this->once())->method('send')->with($request)->willReturn(reject($exception)); $transaction = new Transaction($sender, $loop); $transaction = $transaction->withOptions(['timeout' => 0.001]); $promise = $transaction->send($request); - $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); + $this->assertInstanceOf(PromiseInterface::class, $promise); $promise->then(null, $this->expectCallableOnceWith($exception)); } public function testTimeoutExplicitOptionWillCancelTimeoutTimerWhenSenderRejectsLaterOn() { - $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $timer = $this->createMock(TimerInterface::class); + $loop = $this->createMock(LoopInterface::class); $loop->expects($this->once())->method('addTimer')->willReturn($timer); $loop->expects($this->once())->method('cancelTimer')->with($timer); - $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); + $request = $this->createMock(RequestInterface::class); $deferred = new Deferred(); - $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); - $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn($deferred->promise()); + $sender = $this->createMock(Sender::class); + $sender->expects($this->once())->method('send')->with($request)->willReturn($deferred->promise()); $transaction = new Transaction($sender, $loop); $transaction = $transaction->withOptions(['timeout' => 0.001]); @@ -222,49 +227,49 @@ public function testTimeoutExplicitOptionWillCancelTimeoutTimerWhenSenderRejects $exception = new \RuntimeException(); $deferred->reject($exception); - $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); + $this->assertInstanceOf(PromiseInterface::class, $promise); $promise->then(null, $this->expectCallableOnceWith($exception)); } public function testTimeoutExplicitNegativeWillNotStartTimeoutTimer() { - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop = $this->createMock(LoopInterface::class); $loop->expects($this->never())->method('addTimer'); - $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); + $request = $this->createMock(RequestInterface::class); - $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); - $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new Promise(function () { })); + $sender = $this->createMock(Sender::class); + $sender->expects($this->once())->method('send')->with($request)->willReturn(new Promise(function () { })); $transaction = new Transaction($sender, $loop); $transaction = $transaction->withOptions(['timeout' => -1]); $promise = $transaction->send($request); - $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); + $this->assertInstanceOf(PromiseInterface::class, $promise); } public function testTimeoutExplicitOptionWillNotStartTimeoutTimerWhenRequestBodyIsStreaming() { - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop = $this->createMock(LoopInterface::class); $loop->expects($this->never())->method('addTimer'); $stream = new ThroughStream(); $request = new Request('POST', 'http://example.com', [], new ReadableBodyStream($stream)); - $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); - $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new Promise(function () { })); + $sender = $this->createMock(Sender::class); + $sender->expects($this->once())->method('send')->with($request)->willReturn(new Promise(function () { })); $transaction = new Transaction($sender, $loop); $transaction = $transaction->withOptions(['timeout' => 2]); $promise = $transaction->send($request); - $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); + $this->assertInstanceOf(PromiseInterface::class, $promise); } public function testTimeoutExplicitOptionWillStartTimeoutTimerWhenStreamingRequestBodyIsAlreadyClosed() { - $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $timer = $this->createMock(TimerInterface::class); + $loop = $this->createMock(LoopInterface::class); $loop->expects($this->once())->method('addTimer')->with(2, $this->anything())->willReturn($timer); $loop->expects($this->never())->method('cancelTimer'); @@ -272,28 +277,28 @@ public function testTimeoutExplicitOptionWillStartTimeoutTimerWhenStreamingReque $stream->close(); $request = new Request('POST', 'http://example.com', [], new ReadableBodyStream($stream)); - $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); - $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new Promise(function () { })); + $sender = $this->createMock(Sender::class); + $sender->expects($this->once())->method('send')->with($request)->willReturn(new Promise(function () { })); $transaction = new Transaction($sender, $loop); $transaction = $transaction->withOptions(['timeout' => 2]); $promise = $transaction->send($request); - $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); + $this->assertInstanceOf(PromiseInterface::class, $promise); } public function testTimeoutExplicitOptionWillStartTimeoutTimerWhenStreamingRequestBodyClosesWhileSenderIsStillPending() { - $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $timer = $this->createMock(TimerInterface::class); + $loop = $this->createMock(LoopInterface::class); $loop->expects($this->once())->method('addTimer')->with(2, $this->anything())->willReturn($timer); $loop->expects($this->never())->method('cancelTimer'); $stream = new ThroughStream(); $request = new Request('POST', 'http://example.com', [], new ReadableBodyStream($stream)); - $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); - $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new Promise(function () { })); + $sender = $this->createMock(Sender::class); + $sender->expects($this->once())->method('send')->with($request)->willReturn(new Promise(function () { })); $transaction = new Transaction($sender, $loop); $transaction = $transaction->withOptions(['timeout' => 2]); @@ -301,20 +306,20 @@ public function testTimeoutExplicitOptionWillStartTimeoutTimerWhenStreamingReque $stream->close(); - $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); + $this->assertInstanceOf(PromiseInterface::class, $promise); } public function testTimeoutExplicitOptionWillNotStartTimeoutTimerWhenStreamingRequestBodyClosesAfterSenderRejects() { - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop = $this->createMock(LoopInterface::class); $loop->expects($this->never())->method('addTimer'); $stream = new ThroughStream(); $request = new Request('POST', 'http://example.com', [], new ReadableBodyStream($stream)); $deferred = new Deferred(); - $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); - $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn($deferred->promise()); + $sender = $this->createMock(Sender::class); + $sender->expects($this->once())->method('send')->with($request)->willReturn($deferred->promise()); $transaction = new Transaction($sender, $loop); $transaction = $transaction->withOptions(['timeout' => 2]); @@ -323,7 +328,7 @@ public function testTimeoutExplicitOptionWillNotStartTimeoutTimerWhenStreamingRe $deferred->reject(new \RuntimeException('Request failed')); $stream->close(); - $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); + $this->assertInstanceOf(PromiseInterface::class, $promise); $promise->then(null, $this->expectCallableOnce()); // avoid reporting unhandled rejection } @@ -331,8 +336,8 @@ public function testTimeoutExplicitOptionWillNotStartTimeoutTimerWhenStreamingRe public function testTimeoutExplicitOptionWillRejectWhenTimerFiresAfterStreamingRequestBodyCloses() { $timeout = null; - $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $timer = $this->createMock(TimerInterface::class); + $loop = $this->createMock(LoopInterface::class); $loop->expects($this->once())->method('addTimer')->with(2, $this->callback(function ($cb) use (&$timeout) { $timeout = $cb; return true; @@ -342,8 +347,8 @@ public function testTimeoutExplicitOptionWillRejectWhenTimerFiresAfterStreamingR $stream = new ThroughStream(); $request = new Request('POST', 'http://example.com', [], new ReadableBodyStream($stream)); - $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); - $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new Promise(function () { })); + $sender = $this->createMock(Sender::class); + $sender->expects($this->once())->method('send')->with($request)->willReturn(new Promise(function () { })); $transaction = new Transaction($sender, $loop); $transaction = $transaction->withOptions(['timeout' => 2]); @@ -359,19 +364,19 @@ public function testTimeoutExplicitOptionWillRejectWhenTimerFiresAfterStreamingR $exception = $e; }); - $this->assertInstanceOf('RuntimeException', $exception); + $this->assertInstanceOf(\RuntimeException::class, $exception); $this->assertEquals('Request timed out after 2 seconds', $exception->getMessage()); } public function testReceivingErrorResponseWillRejectWithResponseException() { - $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); + $request = $this->createMock(RequestInterface::class); $response = new Response(404); - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop = $this->createMock(LoopInterface::class); // mock sender to resolve promise with the given $response in response to the given $request $sender = $this->makeSenderMock(); - $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(resolve($response)); + $sender->expects($this->once())->method('send')->with($request)->willReturn(resolve($response)); $transaction = new Transaction($sender, $loop); $transaction = $transaction->withOptions(['timeout' => -1]); @@ -395,12 +400,12 @@ public function testReceivingStreamingBodyWillResolveWithBufferedResponseByDefau $stream->close(); }); - $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); + $request = $this->createMock(RequestInterface::class); $response = new Response(200, [], new ReadableBodyStream($stream)); // mock sender to resolve promise with the given $response in response to the given $request $sender = $this->makeSenderMock(); - $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(resolve($response)); + $sender->expects($this->once())->method('send')->with($request)->willReturn(resolve($response)); $transaction = new Transaction($sender, Loop::get()); $promise = $transaction->send($request); @@ -416,13 +421,13 @@ public function testReceivingStreamingBodyWithContentLengthExceedingMaximumRespo $stream = new ThroughStream(); $stream->on('close', $this->expectCallableOnce()); - $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); + $request = $this->createMock(RequestInterface::class); $response = new Response(200, ['Content-Length' => '100000000'], new ReadableBodyStream($stream, 100000000)); // mock sender to resolve promise with the given $response in response to the given $request $sender = $this->makeSenderMock(); - $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(resolve($response)); + $sender->expects($this->once())->method('send')->with($request)->willReturn(resolve($response)); $transaction = new Transaction($sender, Loop::get()); @@ -436,7 +441,7 @@ public function testReceivingStreamingBodyWithContentLengthExceedingMaximumRespo $this->assertFalse($stream->isWritable()); assert($exception instanceof \OverflowException); - $this->assertInstanceOf('OverflowException', $exception); + $this->assertInstanceOf(\OverflowException::class, $exception); $this->assertEquals('Response body size of 100000000 bytes exceeds maximum of 16777216 bytes', $exception->getMessage()); $this->assertEquals(defined('SOCKET_EMSGSIZE') ? \SOCKET_EMSGSIZE : 90, $exception->getCode()); $this->assertNull($exception->getPrevious()); @@ -447,13 +452,13 @@ public function testReceivingStreamingBodyWithContentsExceedingMaximumResponseBu $stream = new ThroughStream(); $stream->on('close', $this->expectCallableOnce()); - $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); + $request = $this->createMock(RequestInterface::class); $response = new Response(200, [], new ReadableBodyStream($stream)); // mock sender to resolve promise with the given $response in response to the given $request $sender = $this->makeSenderMock(); - $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(resolve($response)); + $sender->expects($this->once())->method('send')->with($request)->willReturn(resolve($response)); $transaction = new Transaction($sender, Loop::get()); $transaction = $transaction->withOptions(['maximumSize' => 10]); @@ -469,7 +474,7 @@ public function testReceivingStreamingBodyWithContentsExceedingMaximumResponseBu $this->assertFalse($stream->isWritable()); assert($exception instanceof \OverflowException); - $this->assertInstanceOf('OverflowException', $exception); + $this->assertInstanceOf(\OverflowException::class, $exception); $this->assertEquals('Response body size exceeds maximum of 10 bytes', $exception->getMessage()); $this->assertEquals(defined('SOCKET_EMSGSIZE') ? \SOCKET_EMSGSIZE : 90, $exception->getCode()); $this->assertNull($exception->getPrevious()); @@ -481,12 +486,12 @@ public function testReceivingStreamingBodyWillRejectWhenStreamEmitsError() throw new \UnexpectedValueException('Unexpected ' . $data, 42); }); - $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); + $request = $this->createMock(RequestInterface::class); $response = new Response(200, [], new ReadableBodyStream($stream)); // mock sender to resolve promise with the given $response in response to the given $request $sender = $this->makeSenderMock(); - $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(resolve($response)); + $sender->expects($this->once())->method('send')->with($request)->willReturn(resolve($response)); $transaction = new Transaction($sender, Loop::get()); $promise = $transaction->send($request); @@ -501,25 +506,25 @@ public function testReceivingStreamingBodyWillRejectWhenStreamEmitsError() $this->assertFalse($stream->isWritable()); assert($exception instanceof \RuntimeException); - $this->assertInstanceOf('RuntimeException', $exception); + $this->assertInstanceOf(\RuntimeException::class, $exception); $this->assertEquals('Error while buffering response body: Unexpected Foo', $exception->getMessage()); $this->assertEquals(42, $exception->getCode()); - $this->assertInstanceOf('UnexpectedValueException', $exception->getPrevious()); + $this->assertInstanceOf(\UnexpectedValueException::class, $exception->getPrevious()); } public function testCancelBufferingResponseWillCloseStreamAndReject() { - $stream = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $stream = $this->createMock(ReadableStreamInterface::class); $stream->expects($this->any())->method('isReadable')->willReturn(true); $stream->expects($this->once())->method('close'); - $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); + $request = $this->createMock(RequestInterface::class); $response = new Response(200, [], new ReadableBodyStream($stream)); // mock sender to resolve promise with the given $response in response to the given $request $deferred = new Deferred(); $sender = $this->makeSenderMock(); - $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn($deferred->promise()); + $sender->expects($this->once())->method('send')->with($request)->willReturn($deferred->promise()); $transaction = new Transaction($sender, Loop::get()); $promise = $transaction->send($request); @@ -533,7 +538,7 @@ public function testCancelBufferingResponseWillCloseStreamAndReject() }); assert($exception instanceof \RuntimeException); - $this->assertInstanceOf('RuntimeException', $exception); + $this->assertInstanceOf(\RuntimeException::class, $exception); $this->assertEquals('Cancelled buffering response body', $exception->getMessage()); $this->assertEquals(0, $exception->getCode()); $this->assertNull($exception->getPrevious()); @@ -541,14 +546,14 @@ public function testCancelBufferingResponseWillCloseStreamAndReject() public function testReceivingStreamingBodyWillResolveWithStreamingResponseIfStreamingIsEnabled() { - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop = $this->createMock(LoopInterface::class); - $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); - $response = new Response(200, [], new ReadableBodyStream($this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock())); + $request = $this->createMock(RequestInterface::class); + $response = new Response(200, [], new ReadableBodyStream($this->createMock(ReadableStreamInterface::class))); // mock sender to resolve promise with the given $response in response to the given $request $sender = $this->makeSenderMock(); - $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(resolve($response)); + $sender->expects($this->once())->method('send')->with($request)->willReturn(resolve($response)); $transaction = new Transaction($sender, $loop); $transaction = $transaction->withOptions(['streaming' => true, 'timeout' => -1]); @@ -566,7 +571,7 @@ public function testReceivingStreamingBodyWillResolveWithStreamingResponseIfStre public function testResponseCode304WithoutLocationWillResolveWithResponseAsIs() { - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop = $this->createMock(LoopInterface::class); // conditional GET request will respond with 304 (Not Modified $request = new Request('GET', 'http://example.com', ['If-None-Match' => '"abc"']); @@ -583,7 +588,7 @@ public function testResponseCode304WithoutLocationWillResolveWithResponseAsIs() public function testCustomRedirectResponseCode333WillFollowLocationHeaderAndSendRedirectedRequest() { - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop = $this->createMock(LoopInterface::class); // original GET request will respond with custom 333 redirect status code and follow location header $requestOriginal = new Request('GET', 'http://example.com'); @@ -605,7 +610,7 @@ public function testCustomRedirectResponseCode333WillFollowLocationHeaderAndSend public function testFollowingRedirectWithSpecifiedHeaders() { - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop = $this->createMock(LoopInterface::class); $customHeaders = ['User-Agent' => 'Chrome']; $requestWithUserAgent = new Request('GET', 'http://example.com', $customHeaders); @@ -635,7 +640,7 @@ public function testFollowingRedirectWithSpecifiedHeaders() public function testRemovingAuthorizationHeaderWhenChangingHostnamesDuringRedirect() { - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop = $this->createMock(LoopInterface::class); $customHeaders = ['Authorization' => 'secret']; $requestWithAuthorization = new Request('GET', 'http://example.com', $customHeaders); @@ -665,7 +670,7 @@ public function testRemovingAuthorizationHeaderWhenChangingHostnamesDuringRedire public function testAuthorizationHeaderIsForwardedWhenRedirectingToSameDomain() { - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop = $this->createMock(LoopInterface::class); $customHeaders = ['Authorization' => 'secret']; $requestWithAuthorization = new Request('GET', 'http://example.com', $customHeaders); @@ -695,7 +700,7 @@ public function testAuthorizationHeaderIsForwardedWhenRedirectingToSameDomain() public function testAuthorizationHeaderIsForwardedWhenLocationContainsAuthentication() { - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop = $this->createMock(LoopInterface::class); $request = new Request('GET', 'http://example.com'); $sender = $this->makeSenderMock(); @@ -725,7 +730,7 @@ public function testAuthorizationHeaderIsForwardedWhenLocationContainsAuthentica public function testSomeRequestHeadersShouldBeRemovedWhenRedirecting() { - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop = $this->createMock(LoopInterface::class); $customHeaders = [ 'Content-Type' => 'text/html; charset=utf-8', @@ -760,7 +765,7 @@ public function testSomeRequestHeadersShouldBeRemovedWhenRedirecting() public function testRequestMethodShouldBeChangedWhenRedirectingWithSeeOther() { - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop = $this->createMock(LoopInterface::class); $customHeaders = [ 'Content-Type' => 'text/html; charset=utf-8', @@ -796,7 +801,7 @@ public function testRequestMethodShouldBeChangedWhenRedirectingWithSeeOther() public function testRequestMethodAndBodyShouldNotBeChangedWhenRedirectingWith307Or308() { - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop = $this->createMock(LoopInterface::class); $customHeaders = [ 'Content-Type' => 'text/html; charset=utf-8', @@ -839,7 +844,7 @@ public function testRequestMethodAndBodyShouldNotBeChangedWhenRedirectingWith307 public function testRedirectingStreamingBodyWith307Or308ShouldThrowCantRedirectStreamException() { - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop = $this->createMock(LoopInterface::class); $customHeaders = [ 'Content-Type' => 'text/html; charset=utf-8', @@ -874,7 +879,7 @@ public function testRedirectingStreamingBodyWith307Or308ShouldThrowCantRedirectS public function testCancelTransactionWillCancelRequest() { - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop = $this->createMock(LoopInterface::class); $request = new Request('GET', 'http://example.com'); $sender = $this->makeSenderMock(); @@ -892,8 +897,8 @@ public function testCancelTransactionWillCancelRequest() public function testCancelTransactionWillCancelTimeoutTimer() { - $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $timer = $this->createMock(TimerInterface::class); + $loop = $this->createMock(LoopInterface::class); $loop->expects($this->once())->method('addTimer')->willReturn($timer); $loop->expects($this->once())->method('cancelTimer')->with($timer); @@ -914,7 +919,7 @@ public function testCancelTransactionWillCancelTimeoutTimer() public function testCancelTransactionWillCancelRedirectedRequest() { - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop = $this->createMock(LoopInterface::class); $request = new Request('GET', 'http://example.com'); $sender = $this->makeSenderMock(); @@ -941,7 +946,7 @@ public function testCancelTransactionWillCancelRedirectedRequest() public function testCancelTransactionWillCancelRedirectedRequestAgain() { - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop = $this->createMock(LoopInterface::class); $request = new Request('GET', 'http://example.com'); $sender = $this->makeSenderMock(); @@ -971,7 +976,7 @@ public function testCancelTransactionWillCancelRedirectedRequestAgain() public function testCancelTransactionWillCloseBufferingStream() { - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop = $this->createMock(LoopInterface::class); $request = new Request('GET', 'http://example.com'); $sender = $this->makeSenderMock(); @@ -994,7 +999,7 @@ public function testCancelTransactionWillCloseBufferingStream() public function testCancelTransactionWillCloseBufferingStreamAgain() { - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop = $this->createMock(LoopInterface::class); $request = new Request('GET', 'http://example.com'); $sender = $this->makeSenderMock(); @@ -1015,7 +1020,7 @@ public function testCancelTransactionWillCloseBufferingStreamAgain() public function testCancelTransactionShouldCancelSendingPromise() { - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop = $this->createMock(LoopInterface::class); $request = new Request('GET', 'http://example.com'); $sender = $this->makeSenderMock(); @@ -1045,6 +1050,6 @@ public function testCancelTransactionShouldCancelSendingPromise() */ private function makeSenderMock() { - return $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); + return $this->createMock(Sender::class); } } diff --git a/tests/Io/UploadedFileTest.php b/tests/Io/UploadedFileTest.php index adbed51c..529b75af 100644 --- a/tests/Io/UploadedFileTest.php +++ b/tests/Io/UploadedFileTest.php @@ -8,14 +8,12 @@ class UploadedFileTest extends TestCase { - public function failtyErrorProvider() + public static function failtyErrorProvider() { - return [ - ['a'], - [null], - [-1], - [9] - ]; + yield ['a']; + yield [null]; + yield [-1]; + yield [9]; } /** @@ -25,7 +23,8 @@ public function testFailtyError($error) { $stream = new BufferedBody(''); - $this->setExpectedException('InvalidArgumentException', 'Invalid error code, must be an UPLOAD_ERR_* constant'); + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid error code, must be an UPLOAD_ERR_* constant'); new UploadedFile($stream, 0, $error, 'foo.bar', 'foo/bar'); } @@ -34,7 +33,8 @@ public function testNoMoveFile() $stream = new BufferedBody(''); $uploadedFile = new UploadedFile($stream, 0, UPLOAD_ERR_OK, 'foo.bar', 'foo/bar'); - $this->setExpectedException('RuntimeException', 'Not implemented'); + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Not implemented'); $uploadedFile->moveTo('bar.foo'); } @@ -54,7 +54,8 @@ public function testGetStreamOnFailedUpload() $stream = new BufferedBody(''); $uploadedFile = new UploadedFile($stream, 0, UPLOAD_ERR_NO_FILE, 'foo.bar', 'foo/bar'); - $this->setExpectedException('RuntimeException', 'Cannot retrieve stream due to upload error'); + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Cannot retrieve stream due to upload error'); $uploadedFile->getStream(); } } diff --git a/tests/Message/RequestTest.php b/tests/Message/RequestTest.php index 543ddb88..148536a0 100644 --- a/tests/Message/RequestTest.php +++ b/tests/Message/RequestTest.php @@ -2,8 +2,10 @@ namespace React\Tests\Http\Message; +use Psr\Http\Message\StreamInterface; use React\Http\Io\HttpBodyStream; use React\Http\Message\Request; +use React\Stream\ReadableStreamInterface; use React\Stream\ThroughStream; use React\Tests\Http\TestCase; @@ -33,8 +35,8 @@ public function testConstructWithStreamingRequestBodyReturnsBodyWhichImplementsR ); $body = $request->getBody(); - $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $body); - $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); + $this->assertInstanceOf(StreamInterface::class, $body); + $this->assertInstanceOf(ReadableStreamInterface::class, $body); $this->assertNull($body->getSize()); } @@ -52,7 +54,8 @@ public function testConstructWithHttpBodyStreamReturnsBodyAsIs() public function testConstructWithNullBodyThrows() { - $this->setExpectedException('InvalidArgumentException', 'Invalid request body given'); + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid request body given'); new Request( 'GET', 'http://localhost', diff --git a/tests/Message/ResponseTest.php b/tests/Message/ResponseTest.php index 61acf19e..6d4acb73 100644 --- a/tests/Message/ResponseTest.php +++ b/tests/Message/ResponseTest.php @@ -2,8 +2,10 @@ namespace React\Tests\Http\Message; +use Psr\Http\Message\StreamInterface; use React\Http\Io\HttpBodyStream; use React\Http\Message\Response; +use React\Stream\ReadableStreamInterface; use React\Stream\ThroughStream; use React\Tests\Http\TestCase; @@ -15,7 +17,7 @@ public function testConstructWithStringBodyWillReturnStreamInstance() $body = $response->getBody(); /** @var \Psr\Http\Message\StreamInterface $body */ - $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $body); + $this->assertInstanceOf(StreamInterface::class, $body); $this->assertEquals('hello', (string) $body); } @@ -25,9 +27,9 @@ public function testConstructWithStreamingBodyWillReturnReadableBodyStream() $body = $response->getBody(); /** @var \Psr\Http\Message\StreamInterface $body */ - $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $body); - $this->assertInstanceof('React\Stream\ReadableStreamInterface', $body); - $this->assertInstanceOf('React\Http\Io\HttpBodyStream', $body); + $this->assertInstanceOf(StreamInterface::class, $body); + $this->assertInstanceof(ReadableStreamInterface::class, $body); + $this->assertInstanceOf(HttpBodyStream::class, $body); $this->assertNull($body->getSize()); } @@ -44,13 +46,13 @@ public function testConstructWithHttpBodyStreamReturnsBodyAsIs() public function testFloatBodyWillThrow() { - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); new Response(200, [], 1.0); } public function testResourceBodyWillThrow() { - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); new Response(200, [], tmpfile()); } @@ -126,7 +128,8 @@ public function testJsonMethodReturnsJsonTextForSimpleString() public function testJsonMethodThrowsForInvalidString() { - $this->setExpectedException('InvalidArgumentException', 'Unable to encode given data as JSON: Malformed UTF-8 characters, possibly incorrectly encoded'); + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Unable to encode given data as JSON: Malformed UTF-8 characters, possibly incorrectly encoded'); Response::json("Hello w\xF6rld!"); } @@ -220,25 +223,25 @@ public function testParseMessageWithHttp10SimpleOkResponseWithLegacyNewlines() public function testParseMessageWithInvalidHttpProtocolVersion12Throws() { - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); Response::parseMessage("HTTP/1.2 200 OK\r\n"); } public function testParseMessageWithInvalidHttpProtocolVersion2Throws() { - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); Response::parseMessage("HTTP/2 200 OK\r\n"); } public function testParseMessageWithInvalidStatusCodeUnderflowThrows() { - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); Response::parseMessage("HTTP/1.1 99 OK\r\n"); } public function testParseMessageWithInvalidResponseHeaderFieldThrows() { - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); Response::parseMessage("HTTP/1.1 200 OK\r\nServer\r\n"); } } diff --git a/tests/Message/ServerRequestTest.php b/tests/Message/ServerRequestTest.php index 36d20bfa..596ebf47 100644 --- a/tests/Message/ServerRequestTest.php +++ b/tests/Message/ServerRequestTest.php @@ -2,8 +2,10 @@ namespace React\Tests\Http\Message; +use Psr\Http\Message\StreamInterface; use React\Http\Io\HttpBodyStream; use React\Http\Message\ServerRequest; +use React\Stream\ReadableStreamInterface; use React\Stream\ThroughStream; use React\Tests\Http\TestCase; @@ -302,8 +304,8 @@ public function testConstructWithStreamingRequestBodyReturnsBodyWhichImplementsR ); $body = $request->getBody(); - $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $body); - $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); + $this->assertInstanceOf(StreamInterface::class, $body); + $this->assertInstanceOf(ReadableStreamInterface::class, $body); $this->assertSame(0, $body->getSize()); } @@ -319,8 +321,8 @@ public function testConstructWithStreamingRequestBodyReturnsBodyWithSizeFromCont ); $body = $request->getBody(); - $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $body); - $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); + $this->assertInstanceOf(StreamInterface::class, $body); + $this->assertInstanceOf(ReadableStreamInterface::class, $body); $this->assertSame(100, $body->getSize()); } @@ -336,14 +338,14 @@ public function testConstructWithStreamingRequestBodyReturnsBodyWithSizeUnknownF ); $body = $request->getBody(); - $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $body); - $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); + $this->assertInstanceOf(StreamInterface::class, $body); + $this->assertInstanceOf(ReadableStreamInterface::class, $body); $this->assertNull($body->getSize()); } public function testConstructWithFloatRequestBodyThrows() { - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); new ServerRequest( 'GET', 'http://localhost', @@ -354,7 +356,7 @@ public function testConstructWithFloatRequestBodyThrows() public function testConstructWithResourceRequestBodyThrows() { - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); new ServerRequest( 'GET', 'http://localhost', @@ -403,85 +405,85 @@ public function testParseMessageWithConnectMethodWithAuthorityFormRequestTarget( public function testParseMessageWithInvalidHttp11RequestWithoutHostThrows() { - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); ServerRequest::parseMessage("GET / HTTP/1.1\r\n", []); } public function testParseMessageWithInvalidHttpProtocolVersionThrows() { - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); ServerRequest::parseMessage("GET / HTTP/1.2\r\n", []); } public function testParseMessageWithInvalidProtocolThrows() { - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); ServerRequest::parseMessage("GET / CUSTOM/1.1\r\n", []); } public function testParseMessageWithInvalidHostHeaderWithoutValueThrows() { - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); ServerRequest::parseMessage("GET / HTTP/1.1\r\nHost\r\n", []); } public function testParseMessageWithInvalidHostHeaderSyntaxThrows() { - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); ServerRequest::parseMessage("GET / HTTP/1.1\r\nHost: ///\r\n", []); } public function testParseMessageWithInvalidHostHeaderWithSchemeThrows() { - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); ServerRequest::parseMessage("GET / HTTP/1.1\r\nHost: http://localhost\r\n", []); } public function testParseMessageWithInvalidHostHeaderWithQueryThrows() { - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); ServerRequest::parseMessage("GET / HTTP/1.1\r\nHost: localhost?foo\r\n", []); } public function testParseMessageWithInvalidHostHeaderWithFragmentThrows() { - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); ServerRequest::parseMessage("GET / HTTP/1.1\r\nHost: localhost#foo\r\n", []); } public function testParseMessageWithInvalidContentLengthHeaderThrows() { - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); ServerRequest::parseMessage("GET / HTTP/1.1\r\nHost: localhost\r\nContent-Length:\r\n", []); } public function testParseMessageWithInvalidTransferEncodingHeaderThrows() { - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); ServerRequest::parseMessage("GET / HTTP/1.1\r\nHost: localhost\r\nTransfer-Encoding:\r\n", []); } public function testParseMessageWithInvalidBothContentLengthHeaderAndTransferEncodingHeaderThrows() { - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); ServerRequest::parseMessage("GET / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 0\r\nTransfer-Encoding: chunked\r\n", []); } public function testParseMessageWithInvalidEmptyHostHeaderWithAbsoluteFormRequestTargetThrows() { - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); ServerRequest::parseMessage("GET http://example.com/ HTTP/1.1\r\nHost: \r\n", []); } public function testParseMessageWithInvalidConnectMethodNotUsingAuthorityFormThrows() { - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); ServerRequest::parseMessage("CONNECT / HTTP/1.1\r\nHost: localhost\r\n", []); } public function testParseMessageWithInvalidRequestTargetAsteriskFormThrows() { - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); ServerRequest::parseMessage("GET * HTTP/1.1\r\nHost: localhost\r\n", []); } } diff --git a/tests/Message/UriTest.php b/tests/Message/UriTest.php index cdbc5a87..adaee94b 100644 --- a/tests/Message/UriTest.php +++ b/tests/Message/UriTest.php @@ -9,118 +9,116 @@ class UriTest extends TestCase { public function testCtorWithInvalidSyntaxThrows() { - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); new Uri('///'); } public function testCtorWithInvalidSchemeThrows() { - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); new Uri('not+a+scheme://localhost'); } public function testCtorWithInvalidHostThrows() { - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); new Uri('http://not a host/'); } public function testCtorWithInvalidPortThrows() { - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); new Uri('http://localhost:80000/'); } public static function provideValidUris() { - return [ - [ - 'http://localhost' - ], - [ - 'http://localhost/' - ], - [ - 'http://localhost:8080/' - ], - [ - 'http://127.0.0.1/' - ], - [ - 'http://[::1]:8080/' - ], - [ - 'http://localhost/path' - ], - [ - 'http://localhost/sub/path' - ], - [ - 'http://localhost/with%20space' - ], - [ - 'http://localhost/with%2fslash' - ], - [ - 'http://localhost/?name=Alice' - ], - [ - 'http://localhost/?name=John+Doe' - ], - [ - 'http://localhost/?name=John%20Doe' - ], - [ - 'http://localhost/?name=Alice&age=42' - ], - [ - 'http://localhost/?name=Alice&' - ], - [ - 'http://localhost/?choice=A%26B' - ], - [ - 'http://localhost/?safe=Yes!?' - ], - [ - 'http://localhost/?alias=@home' - ], - [ - 'http://localhost/?assign:=true' - ], - [ - 'http://localhost/?name=' - ], - [ - 'http://localhost/?name' - ], - [ - '' - ], - [ - '/' - ], - [ - '/path' - ], - [ - 'path' - ], - [ - 'http://user@localhost/' - ], - [ - 'http://user:@localhost/' - ], - [ - 'http://:pass@localhost/' - ], - [ - 'http://user:pass@localhost/path?query#fragment' - ], - [ - 'http://user%20name:pass%20word@localhost/path%20name?query%20name#frag%20ment' - ] + yield [ + 'http://localhost' + ]; + yield [ + 'http://localhost/' + ]; + yield [ + 'http://localhost:8080/' + ]; + yield [ + 'http://127.0.0.1/' + ]; + yield [ + 'http://[::1]:8080/' + ]; + yield [ + 'http://localhost/path' + ]; + yield [ + 'http://localhost/sub/path' + ]; + yield [ + 'http://localhost/with%20space' + ]; + yield [ + 'http://localhost/with%2fslash' + ]; + yield [ + 'http://localhost/?name=Alice' + ]; + yield [ + 'http://localhost/?name=John+Doe' + ]; + yield [ + 'http://localhost/?name=John%20Doe' + ]; + yield [ + 'http://localhost/?name=Alice&age=42' + ]; + yield [ + 'http://localhost/?name=Alice&' + ]; + yield [ + 'http://localhost/?choice=A%26B' + ]; + yield [ + 'http://localhost/?safe=Yes!?' + ]; + yield [ + 'http://localhost/?alias=@home' + ]; + yield [ + 'http://localhost/?assign:=true' + ]; + yield [ + 'http://localhost/?name=' + ]; + yield [ + 'http://localhost/?name' + ]; + yield [ + '' + ]; + yield [ + '/' + ]; + yield [ + '/path' + ]; + yield [ + 'path' + ]; + yield [ + 'http://user@localhost/' + ]; + yield [ + 'http://user:@localhost/' + ]; + yield [ + 'http://:pass@localhost/' + ]; + yield [ + 'http://user:pass@localhost/path?query#fragment' + ]; + yield [ + 'http://user%20name:pass%20word@localhost/path%20name?query%20name#frag%20ment' ]; } @@ -137,35 +135,33 @@ public function testToStringReturnsOriginalUriGivenToCtor($string) public static function provideValidUrisThatWillBeTransformed() { - return [ - [ - 'http://localhost:8080/?', - 'http://localhost:8080/' - ], - [ - 'http://localhost:8080/#', - 'http://localhost:8080/' - ], - [ - 'http://localhost:8080/?#', - 'http://localhost:8080/' - ], - [ - 'http://@localhost:8080/', - 'http://localhost:8080/' - ], - [ - 'http://localhost:8080/?percent=50%', - 'http://localhost:8080/?percent=50%25' - ], - [ - 'http://user name:pass word@localhost/path name?query name#frag ment', - 'http://user%20name:pass%20word@localhost/path%20name?query%20name#frag%20ment' - ], - [ - 'HTTP://USER:PASS@LOCALHOST:8080/PATH?QUERY#FRAGMENT', - 'http://USER:PASS@localhost:8080/PATH?QUERY#FRAGMENT' - ] + yield [ + 'http://localhost:8080/?', + 'http://localhost:8080/' + ]; + yield [ + 'http://localhost:8080/#', + 'http://localhost:8080/' + ]; + yield [ + 'http://localhost:8080/?#', + 'http://localhost:8080/' + ]; + yield [ + 'http://@localhost:8080/', + 'http://localhost:8080/' + ]; + yield [ + 'http://localhost:8080/?percent=50%', + 'http://localhost:8080/?percent=50%25' + ]; + yield [ + 'http://user name:pass word@localhost/path name?query name#frag ment', + 'http://user%20name:pass%20word@localhost/path%20name?query%20name#frag%20ment' + ]; + yield [ + 'HTTP://USER:PASS@LOCALHOST:8080/PATH?QUERY#FRAGMENT', + 'http://USER:PASS@localhost:8080/PATH?QUERY#FRAGMENT' ]; } @@ -251,7 +247,7 @@ public function testWithSchemeThrowsWhenSchemeIsInvalid() { $uri = new Uri('http://localhost'); - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); $uri->withScheme('invalid+scheme'); } @@ -375,7 +371,7 @@ public function testWithHostThrowsWhenHostIsInvalidWithPlus() { $uri = new Uri('http://localhost'); - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); $uri->withHost('invalid+host'); } @@ -383,7 +379,7 @@ public function testWithHostThrowsWhenHostIsInvalidWithSpace() { $uri = new Uri('http://localhost'); - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); $uri->withHost('invalid host'); } @@ -448,7 +444,7 @@ public function testWithPortThrowsWhenPortIsInvalidUnderflow() { $uri = new Uri('http://localhost'); - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); $uri->withPort(0); } @@ -456,7 +452,7 @@ public function testWithPortThrowsWhenPortIsInvalidOverflow() { $uri = new Uri('http://localhost'); - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); $uri->withPort(65536); } @@ -576,112 +572,110 @@ public function testWithFragmentReturnsSameInstanceWhenFragmentIsUnchangedEncode public static function provideResolveUris() { - return [ - [ - 'http://localhost/', - '', - 'http://localhost/' - ], - [ - 'http://localhost/', - 'http://example.com/', - 'http://example.com/' - ], - [ - 'http://localhost/', - 'path', - 'http://localhost/path' - ], - [ - 'http://localhost/', - 'path/', - 'http://localhost/path/' - ], - [ - 'http://localhost/', - 'path//', - 'http://localhost/path/' - ], - [ - 'http://localhost', - 'path', - 'http://localhost/path' - ], - [ - 'http://localhost/a/b', - '/path', - 'http://localhost/path' - ], - [ - 'http://localhost/', - '/a/b/c', - 'http://localhost/a/b/c' - ], - [ - 'http://localhost/a/path', - 'b/c', - 'http://localhost/a/b/c' - ], - [ - 'http://localhost/a/path', - '/b/c', - 'http://localhost/b/c' - ], - [ - 'http://localhost/a/path/', - 'b/c', - 'http://localhost/a/path/b/c' - ], - [ - 'http://localhost/a/path/', - '../b/c', - 'http://localhost/a/b/c' - ], - [ - 'http://localhost', - '../../../a/b', - 'http://localhost/a/b' - ], - [ - 'http://localhost/path', - '?query', - 'http://localhost/path?query' - ], - [ - 'http://localhost/path', - '#fragment', - 'http://localhost/path#fragment' - ], - [ - 'http://localhost/path', - 'http://localhost', - 'http://localhost' - ], - [ - 'http://localhost/path', - 'http://localhost/?query#fragment', - 'http://localhost/?query#fragment' - ], - [ - 'http://localhost/path/?a#fragment', - '?b', - 'http://localhost/path/?b' - ], - [ - 'http://localhost/path', - '//localhost', - 'http://localhost' - ], - [ - 'http://localhost/path', - '//localhost/a?query', - 'http://localhost/a?query' - ], - [ - 'http://localhost/path', - '//LOCALHOST', - 'http://localhost' - ] + yield [ + 'http://localhost/', + '', + 'http://localhost/' + ]; + yield [ + 'http://localhost/', + 'http://example.com/', + 'http://example.com/' + ]; + yield [ + 'http://localhost/', + 'path', + 'http://localhost/path' + ]; + yield [ + 'http://localhost/', + 'path/', + 'http://localhost/path/' + ]; + yield [ + 'http://localhost/', + 'path//', + 'http://localhost/path/' + ]; + yield [ + 'http://localhost', + 'path', + 'http://localhost/path' + ]; + yield [ + 'http://localhost/a/b', + '/path', + 'http://localhost/path' + ]; + yield [ + 'http://localhost/', + '/a/b/c', + 'http://localhost/a/b/c' + ]; + yield [ + 'http://localhost/a/path', + 'b/c', + 'http://localhost/a/b/c' + ]; + yield [ + 'http://localhost/a/path', + '/b/c', + 'http://localhost/b/c' + ]; + yield [ + 'http://localhost/a/path/', + 'b/c', + 'http://localhost/a/path/b/c' + ]; + yield [ + 'http://localhost/a/path/', + '../b/c', + 'http://localhost/a/b/c' + ]; + yield [ + 'http://localhost', + '../../../a/b', + 'http://localhost/a/b' + ]; + yield [ + 'http://localhost/path', + '?query', + 'http://localhost/path?query' + ]; + yield [ + 'http://localhost/path', + '#fragment', + 'http://localhost/path#fragment' + ]; + yield [ + 'http://localhost/path', + 'http://localhost', + 'http://localhost' + ]; + yield [ + 'http://localhost/path', + 'http://localhost/?query#fragment', + 'http://localhost/?query#fragment' + ]; + yield [ + 'http://localhost/path/?a#fragment', + '?b', + 'http://localhost/path/?b' + ]; + yield [ + 'http://localhost/path', + '//localhost', + 'http://localhost' + ]; + yield [ + 'http://localhost/path', + '//localhost/a?query', + 'http://localhost/a?query' + ]; + yield [ + 'http://localhost/path', + '//LOCALHOST', + 'http://localhost' ]; } diff --git a/tests/Middleware/LimitConcurrentRequestsMiddlewareTest.php b/tests/Middleware/LimitConcurrentRequestsMiddlewareTest.php index b79826d6..67656d4c 100644 --- a/tests/Middleware/LimitConcurrentRequestsMiddlewareTest.php +++ b/tests/Middleware/LimitConcurrentRequestsMiddlewareTest.php @@ -10,6 +10,7 @@ use React\Promise\Deferred; use React\Promise\Promise; use React\Promise\PromiseInterface; +use React\Stream\ReadableStreamInterface; use React\Stream\ThroughStream; use React\Tests\Http\TestCase; @@ -114,7 +115,8 @@ public function testThrowsExceptionDirectlyFromMiddlewareWhenBelowLimit() { $middleware = new LimitConcurrentRequestsMiddleware(1); - $this->setExpectedException('RuntimeException', 'demo'); + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('demo'); $middleware(new ServerRequest('GET', 'https://example.com/'), function () { throw new \RuntimeException('demo'); }); @@ -124,7 +126,8 @@ public function testThrowsErrorDirectlyFromMiddlewareWhenBelowLimit() { $middleware = new LimitConcurrentRequestsMiddleware(1); - $this->setExpectedException('Error', 'demo'); + $this->expectException(\Error::class); + $this->expectExceptionMessage('demo'); $middleware(new ServerRequest('GET', 'https://example.com/'), function () { throw new \Error('demo'); }); @@ -159,7 +162,7 @@ public function testReturnsPendingPromiseFromMiddlewareWhenAboveLimit() public function testStreamDoesNotPauseOrResumeWhenBelowLimit() { - $body = $this->getMockBuilder('React\Http\Io\HttpBodyStream')->disableOriginalConstructor()->getMock(); + $body = $this->createMock(HttpBodyStream::class); $body->expects($this->never())->method('pause'); $body->expects($this->never())->method('resume'); $limitHandlers = new LimitConcurrentRequestsMiddleware(1); @@ -168,7 +171,7 @@ public function testStreamDoesNotPauseOrResumeWhenBelowLimit() public function testStreamDoesPauseWhenAboveLimit() { - $body = $this->getMockBuilder('React\Http\Io\HttpBodyStream')->disableOriginalConstructor()->getMock(); + $body = $this->createMock(HttpBodyStream::class); $body->expects($this->once())->method('pause'); $body->expects($this->never())->method('resume'); $limitHandlers = new LimitConcurrentRequestsMiddleware(1); @@ -182,7 +185,7 @@ public function testStreamDoesPauseWhenAboveLimit() public function testStreamDoesPauseAndThenResumeWhenDequeued() { - $body = $this->getMockBuilder('React\Http\Io\HttpBodyStream')->disableOriginalConstructor()->getMock(); + $body = $this->createMock(HttpBodyStream::class); $body->expects($this->once())->method('pause'); $body->expects($this->once())->method('resume'); $limitHandlers = new LimitConcurrentRequestsMiddleware(1); @@ -467,11 +470,11 @@ public function testReceivesStreamingBodyChangesInstanceWithCustomBodyButSameDat $deferred->reject(new \RuntimeException()); $this->assertNotSame($request, $req); - $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $req); + $this->assertInstanceOf(ServerRequestInterface::class, $req); $body = $req->getBody(); - $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); - /* @var $body \React\Stream\ReadableStreamInterface */ + $this->assertInstanceOf(ReadableStreamInterface::class, $body); + /* @var $body ReadableStreamInterface */ $this->assertEquals(5, $body->getSize()); diff --git a/tests/Middleware/RequestBodyBufferMiddlewareTest.php b/tests/Middleware/RequestBodyBufferMiddlewareTest.php index 262ad9ca..28866c96 100644 --- a/tests/Middleware/RequestBodyBufferMiddlewareTest.php +++ b/tests/Middleware/RequestBodyBufferMiddlewareTest.php @@ -272,7 +272,7 @@ function (ServerRequestInterface $request) { $this->assertFalse($stream->isWritable()); assert($exception instanceof \RuntimeException); - $this->assertInstanceOf('RuntimeException', $exception); + $this->assertInstanceOf(\RuntimeException::class, $exception); $this->assertEquals('Buffered 3', $exception->getMessage()); $this->assertEquals(42, $exception->getCode()); $this->assertNull($exception->getPrevious()); @@ -307,7 +307,7 @@ function (ServerRequestInterface $request) { $this->assertFalse($stream->isWritable()); assert($exception instanceof \Error); - $this->assertInstanceOf('Error', $exception); + $this->assertInstanceOf(\Error::class, $exception); $this->assertEquals('Buffered 3', $exception->getMessage()); $this->assertEquals(42, $exception->getCode()); $this->assertNull($exception->getPrevious()); @@ -342,10 +342,10 @@ public function testBufferingRejectsWhenStreamEmitsError() $this->assertFalse($stream->isWritable()); assert($exception instanceof \RuntimeException); - $this->assertInstanceOf('RuntimeException', $exception); + $this->assertInstanceOf(\RuntimeException::class, $exception); $this->assertEquals('Error while buffering request body: Unexpected Foo', $exception->getMessage()); $this->assertEquals(42, $exception->getCode()); - $this->assertInstanceOf('UnexpectedValueException', $exception->getPrevious()); + $this->assertInstanceOf(\UnexpectedValueException::class, $exception->getPrevious()); } public function testFullBodyStreamedBeforeCallingNextMiddleware() @@ -399,7 +399,7 @@ public function testCancelBufferingClosesStreamAndRejectsPromise() }); assert($exception instanceof \RuntimeException); - $this->assertInstanceOf('RuntimeException', $exception); + $this->assertInstanceOf(\RuntimeException::class, $exception); $this->assertEquals('Cancelled buffering request body', $exception->getMessage()); $this->assertEquals(0, $exception->getCode()); $this->assertNull($exception->getPrevious()); diff --git a/tests/TestCase.php b/tests/TestCase.php index fa6fcd1c..4df6087f 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -2,6 +2,7 @@ namespace React\Tests\Http; +use PHPUnit\Framework\MockObject\MockBuilder; use PHPUnit\Framework\TestCase as BaseTestCase; class TestCase extends BaseTestCase @@ -39,46 +40,12 @@ protected function expectCallableNever() protected function createCallableMock() { - if (method_exists('PHPUnit\Framework\MockObject\MockBuilder', 'addMethods')) { + if (method_exists(MockBuilder::class, 'addMethods')) { // PHPUnit 9+ - return $this->getMockBuilder('stdClass')->addMethods(['__invoke'])->getMock(); + return $this->getMockBuilder(\stdClass::class)->addMethods(['__invoke'])->getMock(); } else { - // legacy PHPUnit 4 - PHPUnit 8 - return $this->getMockBuilder('stdClass')->setMethods(['__invoke'])->getMock(); + // legacy PHPUnit + return $this->getMockBuilder(\stdClass::class)->setMethods(['__invoke'])->getMock(); } } - - public function assertContainsString($needle, $haystack) - { - if (method_exists($this, 'assertStringContainsString')) { - // PHPUnit 7.5+ - $this->assertStringContainsString($needle, $haystack); - } else { - // legacy PHPUnit 4 - PHPUnit 7.5 - $this->assertContains($needle, $haystack); - } - } - - public function assertNotContainsString($needle, $haystack) - { - if (method_exists($this, 'assertStringNotContainsString')) { - // PHPUnit 7.5+ - $this->assertStringNotContainsString($needle, $haystack); - } else { - // legacy PHPUnit 4 - PHPUnit 7.5 - $this->assertNotContains($needle, $haystack); - } - } - - public function setExpectedException($exception, $exceptionMessage = '', $exceptionCode = null) - { - $this->expectException($exception); - if ($exceptionMessage !== '') { - $this->expectExceptionMessage($exceptionMessage); - } - if ($exceptionCode !== null) { - $this->expectExceptionCode($exceptionCode); - } - } - } From 905eabfc3c41b229d0a3527e05c7f15f07f3f215 Mon Sep 17 00:00:00 2001 From: James Lucas Date: Wed, 12 Jun 2024 13:41:00 +1000 Subject: [PATCH 453/456] Fix expected error code when ext-sockets is not enabled --- tests/FunctionalBrowserTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/FunctionalBrowserTest.php b/tests/FunctionalBrowserTest.php index d89d92e9..210cfa50 100644 --- a/tests/FunctionalBrowserTest.php +++ b/tests/FunctionalBrowserTest.php @@ -373,7 +373,7 @@ public function testGetRequestWithResponseBufferExceededRejects() $this->expectException(\OverflowException::class); $this->expectExceptionMessage('Response body size of 5 bytes exceeds maximum of 4 bytes'); - $this->expectExceptionCode(defined('SOCKET_EMSGSIZE') ? SOCKET_EMSGSIZE : 0); + $this->expectExceptionCode(defined('SOCKET_EMSGSIZE') ? SOCKET_EMSGSIZE : 90); await($promise); } @@ -383,7 +383,7 @@ public function testGetRequestWithResponseBufferExceededDuringStreamingRejects() $this->expectException(\OverflowException::class); $this->expectExceptionMessage('Response body size exceeds maximum of 4 bytes'); - $this->expectExceptionCode(defined('SOCKET_EMSGSIZE') ? SOCKET_EMSGSIZE : 0); + $this->expectExceptionCode(defined('SOCKET_EMSGSIZE') ? SOCKET_EMSGSIZE : 90); await($promise); } From f1a0406b76e7dce5bc7ecb71e940a0352139fb02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 5 Jul 2024 21:49:14 +0200 Subject: [PATCH 454/456] Improve PHP 8.4+ support by avoiding implicitly nullable types --- composer.json | 10 +++++----- src/Browser.php | 5 +++-- src/Io/Sender.php | 9 ++------- tests/Io/SenderTest.php | 4 +++- tests/Io/StreamingServerTest.php | 4 ++-- 5 files changed, 15 insertions(+), 17 deletions(-) diff --git a/composer.json b/composer.json index 2fe67da0..809e81c9 100644 --- a/composer.json +++ b/composer.json @@ -31,18 +31,18 @@ "fig/http-message-util": "^1.1", "psr/http-message": "^1.0", "react/event-loop": "^1.2", - "react/promise": "^3 || ^2.3 || ^1.2.1", - "react/socket": "^1.12", - "react/stream": "^1.2" + "react/promise": "^3.2 || ^2.3 || ^1.2.1", + "react/socket": "^1.16", + "react/stream": "^1.4" }, "require-dev": { "clue/http-proxy-react": "^1.8", "clue/reactphp-ssh-proxy": "^1.4", "clue/socks-react": "^1.4", "phpunit/phpunit": "^9.6 || ^7.5", - "react/async": "^4 || ^3", + "react/async": "^4.2 || ^3", "react/promise-stream": "^1.4", - "react/promise-timer": "^1.9" + "react/promise-timer": "^1.11" }, "autoload": { "psr-4": { diff --git a/src/Browser.php b/src/Browser.php index 06e194d9..f042b799 100644 --- a/src/Browser.php +++ b/src/Browser.php @@ -10,6 +10,7 @@ use React\Http\Message\Request; use React\Http\Message\Uri; use React\Promise\PromiseInterface; +use React\Socket\Connector; use React\Socket\ConnectorInterface; use React\Stream\ReadableStreamInterface; use InvalidArgumentException; @@ -68,11 +69,11 @@ class Browser * @param ?ConnectorInterface $connector * @param ?LoopInterface $loop */ - public function __construct(ConnectorInterface $connector = null, LoopInterface $loop = null) + public function __construct(?ConnectorInterface $connector = null, ?LoopInterface $loop = null) { $loop = $loop ?? Loop::get(); $this->transaction = new Transaction( - Sender::createFromLoop($loop, $connector), + Sender::createFromLoop($loop, $connector ?? new Connector([], $loop)), $loop ); } diff --git a/src/Io/Sender.php b/src/Io/Sender.php index 8ece2ee0..ccb1e1da 100644 --- a/src/Io/Sender.php +++ b/src/Io/Sender.php @@ -8,7 +8,6 @@ use React\Http\Client\Client as HttpClient; use React\Promise\PromiseInterface; use React\Promise\Deferred; -use React\Socket\Connector; use React\Socket\ConnectorInterface; use React\Stream\ReadableStreamInterface; @@ -45,15 +44,11 @@ class Sender * ``` * * @param LoopInterface $loop - * @param ConnectorInterface|null $connector + * @param ConnectorInterface $connector * @return self */ - public static function createFromLoop(LoopInterface $loop, ConnectorInterface $connector = null) + public static function createFromLoop(LoopInterface $loop, ConnectorInterface $connector) { - if ($connector === null) { - $connector = new Connector([], $loop); - } - return new self(new HttpClient(new ClientConnectionManager($connector, $loop))); } diff --git a/tests/Io/SenderTest.php b/tests/Io/SenderTest.php index df4c5359..4154a38d 100644 --- a/tests/Io/SenderTest.php +++ b/tests/Io/SenderTest.php @@ -34,7 +34,9 @@ public function setUpLoop() public function testCreateFromLoop() { - $sender = Sender::createFromLoop($this->loop, null); + $connector = $this->createMock(ConnectorInterface::class); + + $sender = Sender::createFromLoop($this->loop, $connector); $this->assertInstanceOf(Sender::class, $sender); } diff --git a/tests/Io/StreamingServerTest.php b/tests/Io/StreamingServerTest.php index a61d1425..803ff45c 100644 --- a/tests/Io/StreamingServerTest.php +++ b/tests/Io/StreamingServerTest.php @@ -36,7 +36,7 @@ public function setUpConnectionMockAndSocket() } - private function mockConnection(array $additionalMethods = null) + private function mockConnection(array $additionalMethods = []) { $connection = $this->getMockBuilder(Connection::class) ->disableOriginalConstructor() @@ -53,7 +53,7 @@ private function mockConnection(array $additionalMethods = null) 'getLocalAddress', 'pipe' ], - (is_array($additionalMethods) ? $additionalMethods : []) + $additionalMethods )) ->getMock(); From 559c30d44811a1f048c91c7ea297c0bfa0433368 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ana=C3=AFs=20Babel?= Date: Mon, 22 Apr 2024 13:22:45 +0200 Subject: [PATCH 455/456] Allow underscore character in Uri host --- src/Message/Uri.php | 4 ++-- tests/Message/UriTest.php | 13 +++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/Message/Uri.php b/src/Message/Uri.php index 84fc38d8..661f90c4 100644 --- a/src/Message/Uri.php +++ b/src/Message/Uri.php @@ -46,7 +46,7 @@ final class Uri implements UriInterface public function __construct($uri) { $parts = \parse_url($uri); - if ($parts === false || (isset($parts['scheme']) && !\preg_match('#^[a-z]+$#i', $parts['scheme'])) || (isset($parts['host']) && \preg_match('#[\s_%+]#', $parts['host']))) { + if ($parts === false || (isset($parts['scheme']) && !\preg_match('#^[a-z]+$#i', $parts['scheme'])) || (isset($parts['host']) && \preg_match('#[\s%+]#', $parts['host']))) { throw new \InvalidArgumentException('Invalid URI given'); } @@ -164,7 +164,7 @@ public function withHost($host) return $this; } - if (\preg_match('#[\s_%+]#', $host) || ($host !== '' && \parse_url('http://' . $host, \PHP_URL_HOST) !== $host)) { + if (\preg_match('#[\s%+]#', $host) || ($host !== '' && \parse_url('http://' . $host, \PHP_URL_HOST) !== $host)) { throw new \InvalidArgumentException('Invalid URI host given'); } diff --git a/tests/Message/UriTest.php b/tests/Message/UriTest.php index adaee94b..1774953f 100644 --- a/tests/Message/UriTest.php +++ b/tests/Message/UriTest.php @@ -120,6 +120,9 @@ public static function provideValidUris() yield [ 'http://user%20name:pass%20word@localhost/path%20name?query%20name#frag%20ment' ]; + yield [ + 'http://docker_container/' + ]; } /** @@ -329,6 +332,16 @@ public function testWithHostReturnsNewInstanceWhenHostIsChanged() $this->assertEquals('localhost', $uri->getHost()); } + public function testWithHostReturnsNewInstanceWhenHostIsChangedWithUnderscore() + { + $uri = new Uri('http://localhost'); + + $new = $uri->withHost('docker_container'); + $this->assertNotSame($uri, $new); + $this->assertEquals('docker_container', $new->getHost()); + $this->assertEquals('localhost', $uri->getHost()); + } + public function testWithHostReturnsNewInstanceWithHostToLowerCaseWhenHostIsChangedWithUpperCase() { $uri = new Uri('http://localhost'); From 6074eefe76a7ada6f902453f0a5ad0147623d049 Mon Sep 17 00:00:00 2001 From: Paul Rotmann Date: Tue, 25 Mar 2025 09:30:14 +0100 Subject: [PATCH 456/456] Run tests on PHP 8.4 and update test environment --- .github/workflows/ci.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6f9cfb41..22793579 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,10 +7,11 @@ on: jobs: PHPUnit: name: PHPUnit (PHP ${{ matrix.php }}) - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 strategy: matrix: php: + - 8.4 - 8.3 - 8.2 - 8.1 @@ -24,7 +25,7 @@ jobs: - uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} - coverage: xdebug + coverage: ${{ matrix.php < 8.0 && 'xdebug' || 'pcov' }} ini-file: development - run: composer install - run: vendor/bin/phpunit --coverage-text