From cd50a39b2896f46924b5479aedbb6954e1d12fa3 Mon Sep 17 00:00:00 2001 From: Zachary He Date: Fri, 4 Dec 2020 18:57:49 +1100 Subject: [PATCH 1/4] Add Gzip support on chunk --- src/Server/Manager.php | 4 +-- src/Transformers/Response.php | 54 +++++++++++++++++++++++++++-- tests/Transformers/ResponseTest.php | 7 ++-- 3 files changed, 57 insertions(+), 8 deletions(-) diff --git a/src/Server/Manager.php b/src/Server/Manager.php index 0495aba1..26fe2f69 100644 --- a/src/Server/Manager.php +++ b/src/Server/Manager.php @@ -225,7 +225,7 @@ public function onRequest($swooleRequest, $swooleResponse) $illuminateResponse = $sandbox->run($illuminateRequest); // send response - Response::make($illuminateResponse, $swooleResponse)->send(); + Response::make($illuminateResponse, $swooleResponse, $swooleRequest)->send(); } catch (Throwable $e) { try { $exceptionResponse = $this->app @@ -234,7 +234,7 @@ public function onRequest($swooleRequest, $swooleResponse) $illuminateRequest, $this->normalizeException($e) ); - Response::make($exceptionResponse, $swooleResponse)->send(); + Response::make($exceptionResponse, $swooleResponse, $swooleRequest)->send(); } catch (Throwable $e) { $this->logServerError($e); } diff --git a/src/Transformers/Response.php b/src/Transformers/Response.php index 416a26fa..da9a0228 100644 --- a/src/Transformers/Response.php +++ b/src/Transformers/Response.php @@ -4,6 +4,7 @@ use Illuminate\Http\Response as IlluminateResponse; use Swoole\Http\Response as SwooleResponse; +use Swoole\Http\Request as SwooleRequest; use Symfony\Component\HttpFoundation\BinaryFileResponse; use Symfony\Component\HttpFoundation\Response as SymfonyResponse; use Symfony\Component\HttpFoundation\StreamedResponse; @@ -17,6 +18,11 @@ class Response */ protected $swooleResponse; + /** + * @var \Swoole\Http\Request + */ + protected $swooleRequest; + /** * @var \Illuminate\Http\Response */ @@ -27,12 +33,13 @@ class Response * * @param $illuminateResponse * @param \Swoole\Http\Response $swooleResponse + * @param \Swoole\Http\Request $swooleRequest * * @return \SwooleTW\Http\Transformers\Response */ - public static function make($illuminateResponse, SwooleResponse $swooleResponse) + public static function make($illuminateResponse, SwooleResponse $swooleResponse, SwooleRequest $swooleRequest) { - return new static($illuminateResponse, $swooleResponse); + return new static($illuminateResponse, $swooleResponse, $swooleRequest); } /** @@ -40,11 +47,13 @@ public static function make($illuminateResponse, SwooleResponse $swooleResponse) * * @param mixed $illuminateResponse * @param \Swoole\Http\Response $swooleResponse + * @param \Swoole\Http\Request $swooleRequest */ - public function __construct($illuminateResponse, SwooleResponse $swooleResponse) + public function __construct($illuminateResponse, SwooleResponse $swooleResponse, SwooleRequest $swooleRequest) { $this->setIlluminateResponse($illuminateResponse); $this->setSwooleResponse($swooleResponse); + $this->setSwooleRequest($swooleRequest); } /** @@ -133,6 +142,12 @@ protected function sendInChunk($content) return; } + // Swoole Chunk mode does not support compress by default, this patch only supports gzip + if ($chunkGzip) { + $this->swooleResponse->header('Content-Encoding', 'gzip'); + $content = gzencode($content, config('swoole_http.server.options.http_compression_level', 3)); + } + foreach (str_split($content, static::CHUNK_SIZE) as $chunk) { $this->swooleResponse->write($chunk); } @@ -184,4 +199,37 @@ public function getIlluminateResponse() { return $this->illuminateResponse; } + + /** + * @param \Swoole\Http\Request $swooleRequest + * + * @return \SwooleTW\Http\Transformers\Response + */ + protected function setSwooleRequest(SwooleRequest $swooleRequest) + { + $this->swooleRequest = $swooleRequest; + + return $this; + } + + /** + * @return \Swoole\Http\Request + */ + public function getSwooleRequest() + { + return $this->swooleRequest; + } + + /** + * @param string $responseContentEncoding + * @return bool + */ + protected function canGzipContent($responseContentEncoding) + { + return empty($responseContentEncoding) && + config('swoole_http.server.options.http_compression', true) && + !empty($this->swooleRequest->header['accept-encoding']) && + strpos($this->swooleRequest->header['accept-encoding'], 'gzip') !== false && + function_exists('gzencode'); + } } diff --git a/tests/Transformers/ResponseTest.php b/tests/Transformers/ResponseTest.php index 41df5999..6b24d72c 100644 --- a/tests/Transformers/ResponseTest.php +++ b/tests/Transformers/ResponseTest.php @@ -4,6 +4,7 @@ use Illuminate\Http\Response as IlluminateResponse; use Swoole\Http\Response as SwooleResponse; +use Swoole\Http\Request as SwooleRequest; use SwooleTW\Http\Tests\TestCase; use SwooleTW\Http\Transformers\Response; @@ -11,14 +12,14 @@ class ResponseTest extends TestCase { public function testMake() { - $response = Response::make(new IlluminateResponse, new SwooleResponse); + $response = Response::make(new IlluminateResponse, new SwooleResponse, new SwooleRequest); $this->assertInstanceOf(Response::class, $response); } public function testGetIlluminateResponse() { - $response = Response::make(new IlluminateResponse, new SwooleResponse); + $response = Response::make(new IlluminateResponse, new SwooleResponse, new SwooleRequest); $illuminateResponse = $response->getIlluminateResponse(); $this->assertInstanceOf(IlluminateResponse::class, $illuminateResponse); @@ -26,7 +27,7 @@ public function testGetIlluminateResponse() public function testGetSwooleResponse() { - $response = Response::make(new IlluminateResponse, new SwooleResponse); + $response = Response::make(new IlluminateResponse, new SwooleResponse, new SwooleRequest); $swooleResponse = $response->getSwooleResponse(); $this->assertInstanceOf(SwooleResponse::class, $swooleResponse); From c350158453dfea21815d84ad9d732296fff0fc43 Mon Sep 17 00:00:00 2001 From: Zachary He Date: Fri, 4 Dec 2020 19:07:52 +1100 Subject: [PATCH 2/4] add condition in --- src/Transformers/Response.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Transformers/Response.php b/src/Transformers/Response.php index da9a0228..6a73856f 100644 --- a/src/Transformers/Response.php +++ b/src/Transformers/Response.php @@ -126,6 +126,7 @@ protected function sendContent() } elseif ($illuminateResponse instanceof BinaryFileResponse) { $this->swooleResponse->sendfile($illuminateResponse->getFile()->getPathname()); } else { + $chunkGzip = $this->canGzipContent($illuminateResponse->headers->get('Content-Encoding')); $this->sendInChunk($illuminateResponse->getContent()); } } From 2911def74d9f9f9e138c53748f769cc352b3a2a3 Mon Sep 17 00:00:00 2001 From: Zachary He Date: Fri, 4 Dec 2020 19:12:01 +1100 Subject: [PATCH 3/4] forgot to update method --- src/Transformers/Response.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Transformers/Response.php b/src/Transformers/Response.php index 6a73856f..157d6f33 100644 --- a/src/Transformers/Response.php +++ b/src/Transformers/Response.php @@ -127,7 +127,7 @@ protected function sendContent() $this->swooleResponse->sendfile($illuminateResponse->getFile()->getPathname()); } else { $chunkGzip = $this->canGzipContent($illuminateResponse->headers->get('Content-Encoding')); - $this->sendInChunk($illuminateResponse->getContent()); + $this->sendInChunk($illuminateResponse->getContent(), $chunkGzip); } } @@ -135,8 +135,9 @@ protected function sendContent() * Send content in chunk * * @param string $content + * @param bool $chunkGzip */ - protected function sendInChunk($content) + protected function sendInChunk($content, $chunkGzip) { if (strlen($content) <= static::CHUNK_SIZE) { $this->swooleResponse->end($content); From b2da17030c05710112777e1a992b1ff2d768b820 Mon Sep 17 00:00:00 2001 From: Zachary He Date: Thu, 17 Dec 2020 12:48:50 +1100 Subject: [PATCH 4/4] Add test case for Response --- tests/Transformers/ResponseTest.php | 276 ++++++++++++++++++++++++++++ 1 file changed, 276 insertions(+) diff --git a/tests/Transformers/ResponseTest.php b/tests/Transformers/ResponseTest.php index 6b24d72c..03122a10 100644 --- a/tests/Transformers/ResponseTest.php +++ b/tests/Transformers/ResponseTest.php @@ -2,11 +2,18 @@ namespace SwooleTW\Http\Tests\Transformers; +use Symfony\Component\HttpFoundation\ResponseHeaderBag; +use Symfony\Component\HttpFoundation\StreamedResponse; +use Symfony\Component\HttpFoundation\BinaryFileResponse; +use Symfony\Component\HttpFoundation\Cookie; +use Symfony\Component\HttpFoundation\File\File; use Illuminate\Http\Response as IlluminateResponse; +use Illuminate\Support\Str; use Swoole\Http\Response as SwooleResponse; use Swoole\Http\Request as SwooleRequest; use SwooleTW\Http\Tests\TestCase; use SwooleTW\Http\Transformers\Response; +use Mockery; class ResponseTest extends TestCase { @@ -32,4 +39,273 @@ public function testGetSwooleResponse() $this->assertInstanceOf(SwooleResponse::class, $swooleResponse); } + + public function testGetSwooleRequest() + { + $response = Response::make(new IlluminateResponse, new SwooleResponse, new SwooleRequest); + $swooleRequest = $response->getSwooleRequest(); + + $this->assertInstanceOf(SwooleRequest::class, $swooleRequest); + } + + public function testSendHeaders() + { + $headers = ['test' => ['123', '456']]; + $status = 200; + + // rawcookie + $cookie1 = Mockery::mock(Cookie::class); + $cookie1->shouldReceive('isRaw')->once()->andReturn(true); + $cookie1->shouldReceive('getName')->once()->andReturn('Cookie_1_getName'); + $cookie1->shouldReceive('getValue')->once()->andReturn('Cookie_1_getValue'); + $cookie1->shouldReceive('getExpiresTime')->once()->andReturn('Cookie_1_getExpiresTime'); + $cookie1->shouldReceive('getPath')->once()->andReturn('Cookie_1_getPath'); + $cookie1->shouldReceive('getDomain')->once()->andReturn('Cookie_1_getDomain'); + $cookie1->shouldReceive('isSecure')->once()->andReturn('Cookie_1_isSecure'); + $cookie1->shouldReceive('isHttpOnly')->once()->andReturn('Cookie_1_isHttpOnly'); + + // cookie + $cookie2 = Mockery::mock(Cookie::class); + $cookie2->shouldReceive('isRaw')->once()->andReturn(false); + $cookie2->shouldReceive('getName')->once()->andReturn('Cookie_2_getName'); + $cookie2->shouldReceive('getValue')->once()->andReturn('Cookie_2_getValue'); + $cookie2->shouldReceive('getExpiresTime')->once()->andReturn('Cookie_2_getExpiresTime'); + $cookie2->shouldReceive('getPath')->once()->andReturn('Cookie_2_getPath'); + $cookie2->shouldReceive('getDomain')->once()->andReturn('Cookie_2_getDomain'); + $cookie2->shouldReceive('isSecure')->once()->andReturn('Cookie_2_isSecure'); + $cookie2->shouldReceive('isHttpOnly')->once()->andReturn('Cookie_2_isHttpOnly'); + + $illuminateResponse = Mockery::mock(IlluminateResponse::class); + $illuminateResponse->headers = Mockery::mock(ResponseHeaderBag::class); + + $illuminateResponse->headers + ->shouldReceive('has') + ->once() + ->with('Date') + ->andReturn(false); + + $illuminateResponse->headers + ->shouldReceive('getCookies') + ->once() + ->andReturn([$cookie1, $cookie2] ); + + $illuminateResponse->headers + ->shouldReceive('allPreserveCase') + ->once() + ->andReturn([ + 'Set-Cookie' => uniqid(), + ] + $headers); + + $illuminateResponse->shouldReceive('setDate') + ->once() + ->withArgs(function(\DateTime $dateTime) { + $timestamp = $dateTime->getTimestamp(); + return $timestamp <= time() && $timestamp + 2 > time(); + }); + + $illuminateResponse->shouldReceive('getStatusCode') + ->once() + ->andReturn($status); + + $swooleResponse = Mockery::mock(SwooleResponse::class); + $swooleResponse->shouldReceive('header') + ->times(2) + ->withArgs(function ($name, $value) use (&$headers) { + $header = array_shift($headers['test']); + return $name === 'test' && $header === $value; + }); + $swooleResponse->shouldReceive('status') + ->once() + ->withArgs([$status]); + $swooleResponse->shouldReceive('rawcookie') + ->once() + ->withArgs([ + 'Cookie_1_getName', + 'Cookie_1_getValue', + 'Cookie_1_getExpiresTime', + 'Cookie_1_getPath', + 'Cookie_1_getDomain', + 'Cookie_1_isSecure', + 'Cookie_1_isHttpOnly' + ]); + $swooleResponse->shouldReceive('cookie') + ->once() + ->withArgs([ + 'Cookie_2_getName', + 'Cookie_2_getValue', + 'Cookie_2_getExpiresTime', + 'Cookie_2_getPath', + 'Cookie_2_getDomain', + 'Cookie_2_isSecure', + 'Cookie_2_isHttpOnly' + ]); + + $swooleRequest = Mockery::mock(SwooleRequest::class); + + $response = Response::make($illuminateResponse, $swooleResponse, $swooleRequest); + + /** + * use Closure::call to bypass protect and private method + * url: https://www.php.net/manual/en/closure.call.php + */ + $callback = function() { + $this->sendHeaders(); + }; + $callback->call($response); + } + + public function testSendStreamedResponseContent() + { + $illuminateResponse = Mockery::mock(StreamedResponse::class); + $illuminateResponse->output = uniqid(); + + $swooleResponse = Mockery::mock(SwooleResponse::class); + $swooleResponse->shouldReceive('end') + ->once() + ->withArgs([$illuminateResponse->output]); + + $swooleRequest = Mockery::mock(SwooleRequest::class); + + $response = Response::make($illuminateResponse, $swooleResponse, $swooleRequest); + + /** + * use Closure::call to bypass protect and private method + * url: https://www.php.net/manual/en/closure.call.php + */ + $callback = function() { + $this->sendContent(); + }; + $callback->call($response); + } + + public function testSendBinaryFileResponseContent() + { + $path = uniqid(); + $file = Mockery::mock(File::class); + $file->shouldReceive('getPathname') + ->once() + ->andReturn($path); + + $illuminateResponse = Mockery::mock(BinaryFileResponse::class); + $illuminateResponse->shouldReceive('getFile') + ->once() + ->andReturn($file); + + $swooleResponse = Mockery::mock(SwooleResponse::class); + $swooleResponse->shouldReceive('sendfile') + ->once() + ->withArgs([$path]); + + $swooleRequest = Mockery::mock(SwooleRequest::class); + + $response = Response::make($illuminateResponse, $swooleResponse, $swooleRequest); + + /** + * use Closure::call to bypass protect and private method + * url: https://www.php.net/manual/en/closure.call.php + */ + $callback = function() { + $this->sendContent(); + }; + $callback->call($response); + } + + public function testSendChunkedContent() + { + $http_compression_level = 5; + $content = Str::random(Response::CHUNK_SIZE * 3); + $compressedContent = gzencode($content, $http_compression_level); + $times = (int)ceil(strlen($compressedContent) / Response::CHUNK_SIZE); + + $chunks = []; + foreach (str_split($compressedContent, Response::CHUNK_SIZE) as $chunk) { + $chunks[] = $chunk; + } + + app()->instance('config', new \Illuminate\Config\Repository([ + 'swoole_http' => [ + 'server' => [ + 'options' => [ + 'http_compression' => true, + 'http_compression_level' => $http_compression_level + ] + ], + ], + ])); + + $illuminateResponse = Mockery::mock(IlluminateResponse::class); + $illuminateResponse->headers = Mockery::mock(ResponseHeaderBag::class); + + $illuminateResponse->headers + ->shouldReceive('get') + ->once() + ->withArgs(['Content-Encoding']) + ->andReturn(null); + + $illuminateResponse->shouldReceive('getContent') + ->andReturn($content); + + $swooleResponse = Mockery::mock(SwooleResponse::class); + $swooleResponse->shouldReceive('header') + ->once() + ->withArgs(['Content-Encoding', 'gzip']); + + $swooleResponse->shouldReceive('write') + ->times($times) + ->withArgs(function ($chunk) use (&$chunks) { + $expectChunk = array_shift($chunks); + return $chunk === $expectChunk; + }); + $swooleResponse->shouldReceive('end') + ->once(); + + $swooleRequest = Mockery::mock(SwooleRequest::class); + $swooleRequest->header = ['accept-encoding' => 'gzip, deflate, br']; + + $response = Response::make($illuminateResponse, $swooleResponse, $swooleRequest); + + /** + * use Closure::call to bypass protect and private method + * url: https://www.php.net/manual/en/closure.call.php + */ + $callback = function() { + $this->sendContent(); + }; + $callback->call($response); + } + + public function testSend_() + { + $status = 200; + $content = 'test'; + + app()->instance('config', new \Illuminate\Config\Repository([ + 'swoole_http' => [ + 'server' => [ + 'options' => [ + 'http_compression' => false, + ] + ], + ], + ])); + + $swooleResponse = Mockery::mock(SwooleResponse::class); + $swooleResponse->shouldReceive('header') + ->twice() + ->withArgs(function ($name, $value) { + return in_array($name, ['Date', 'Cache-Control'], true); + }); + $swooleResponse->shouldReceive('status') + ->once() + ->with(200); + $swooleResponse->shouldReceive('end') + ->once() + ->withArgs([$content]); + + $swooleRequest = Mockery::mock(SwooleRequest::class); + $swooleRequest->header = ['accept-encoding' => 'gzip, deflate, br']; + + $response = Response::make($content, $swooleResponse, $swooleRequest); + $response->send(); + } }