diff --git a/CHANGELOG.md b/CHANGELOG.md index 2dddce4..97b5615 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ ChangeLog ========= +5.1.7 (2023-06-26) +------------------ + +* #98 and #176 Add more tests (@peter279k) +* #207 fix: handle client disconnect properly with ignore_user_abort true (@kesselb) + 5.1.6 (2022-07-15) ------------------ diff --git a/lib/Sapi.php b/lib/Sapi.php index f8e8397..55a4c46 100644 --- a/lib/Sapi.php +++ b/lib/Sapi.php @@ -115,6 +115,12 @@ public static function sendResponse(ResponseInterface $response) if ($copied <= 0) { break; } + // Abort on client disconnect. + // With ignore_user_abort(true), the script is not aborted on client disconnect. + // To avoid reading the entire stream and dismissing the data afterward, check between the chunks if the client is still there. + if (1 === ignore_user_abort() && 1 === connection_aborted()) { + break; + } $left -= $copied; } } else { diff --git a/lib/Version.php b/lib/Version.php index 47582f2..1c9190c 100644 --- a/lib/Version.php +++ b/lib/Version.php @@ -16,5 +16,5 @@ class Version /** * Full version number. */ - const VERSION = '5.1.6'; + const VERSION = '5.1.7'; } diff --git a/tests/HTTP/Auth/AWSTest.php b/tests/HTTP/Auth/AWSTest.php index f3b36cf..243da4b 100644 --- a/tests/HTTP/Auth/AWSTest.php +++ b/tests/HTTP/Auth/AWSTest.php @@ -42,6 +42,14 @@ public function testNoHeader() $this->assertEquals(AWS::ERR_NOAWSHEADER, $this->auth->errorCode); } + public function testInvalidAuthorizationHeader() + { + $this->request->setMethod('GET'); + $this->request->setHeader('Authorization', 'Invalid Auth Header'); + + $this->assertFalse($this->auth->init(), 'The Invalid AWS authorization header'); + } + public function testIncorrectContentMD5() { $accessKey = 'accessKey'; diff --git a/tests/HTTP/FunctionsTest.php b/tests/HTTP/FunctionsTest.php index f5e2e6e..0078873 100644 --- a/tests/HTTP/FunctionsTest.php +++ b/tests/HTTP/FunctionsTest.php @@ -6,6 +6,30 @@ class FunctionsTest extends \PHPUnit\Framework\TestCase { + /** + * @dataProvider getHeaderValuesDataOnValues2 + */ + public function testGetHeaderValuesOnValues2($result, $values1, $values2) + { + $this->assertEquals($result, getHeaderValues($values1, $values2)); + } + + public function getHeaderValuesDataOnValues2() + { + return [ + [ + ['a', 'b'], + ['a'], + ['b'], + ], + [ + ['a', 'b', 'c', 'd', 'e'], + ['a', 'b', 'c'], + ['d', 'e'], + ], + ]; + } + /** * @dataProvider getHeaderValuesData */ @@ -174,4 +198,12 @@ public function testToHTTPDate() toDate($dt) ); } + + public function testParseMimeTypeOnInvalidMimeType() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Not a valid mime-type: invalid_mime_type'); + + parseMimeType('invalid_mime_type'); + } } diff --git a/tests/HTTP/SapiTest.php b/tests/HTTP/SapiTest.php index 3c8e7f5..3b0a42b 100644 --- a/tests/HTTP/SapiTest.php +++ b/tests/HTTP/SapiTest.php @@ -31,6 +31,34 @@ public function testConstructFromServerArray() $this->assertNull($request->getRawServerValue('FOO')); } + public function testConstructFromServerArrayOnNullUrl() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The _SERVER array must have a REQUEST_URI key'); + + $request = Sapi::createFromServerArray([ + 'REQUEST_METHOD' => 'GET', + 'HTTP_USER_AGENT' => 'Evert', + 'CONTENT_TYPE' => 'text/xml', + 'CONTENT_LENGTH' => '400', + 'SERVER_PROTOCOL' => 'HTTP/1.0', + ]); + } + + public function testConstructFromServerArrayOnNullMethod() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The _SERVER array must have a REQUEST_METHOD key'); + + $request = Sapi::createFromServerArray([ + 'REQUEST_URI' => '/foo', + 'HTTP_USER_AGENT' => 'Evert', + 'CONTENT_TYPE' => 'text/xml', + 'CONTENT_LENGTH' => '400', + 'SERVER_PROTOCOL' => 'HTTP/1.0', + ]); + } + public function testConstructPHPAuth() { $request = Sapi::createFromServerArray([ @@ -259,4 +287,35 @@ public function testSendWorksWithCallbackAsBody() $this->assertEquals('foo', $result); } + + public function testSendConnectionAborted(): void + { + $baseUrl = getenv('BASEURL'); + if (!$baseUrl) { + $this->markTestSkipped('Set an environment value BASEURL to continue'); + } + + $url = rtrim($baseUrl, '/').'/connection_aborted.php'; + $chunk_size = 4 * 1024 * 1024; + $fetch_size = 6 * 1024 * 1024; + + $stream = fopen($url, 'r'); + $size = 0; + + while ($size <= $fetch_size) { + $temp = fread($stream, 8192); + if (false === $temp) { + break; + } + $size += strlen($temp); + } + + fclose($stream); + + sleep(5); + + $bytes_read = file_get_contents(sys_get_temp_dir().'/dummy_stream_read_counter'); + $this->assertEquals($chunk_size * 2, $bytes_read); + $this->assertGreaterThanOrEqual($fetch_size, $bytes_read); + } } diff --git a/tests/www/connection_aborted.php b/tests/www/connection_aborted.php new file mode 100644 index 0000000..724ad2d --- /dev/null +++ b/tests/www/connection_aborted.php @@ -0,0 +1,69 @@ +position = 0; + + return true; + } + + public function stream_read(int $count): string + { + $this->position += $count; + + return random_bytes($count); + } + + public function stream_tell(): int + { + return $this->position; + } + + public function stream_eof(): bool + { + return $this->position > 25 * 1024 * 1024; + } + + public function stream_close(): void + { + file_put_contents(sys_get_temp_dir().'/dummy_stream_read_counter', $this->position); + } +} + +/* + * The DummyStream wrapper has two functions: + * - Provide dummy data. + * - Count how many bytes have been read. + */ +stream_wrapper_register('dummy', DummyStream::class); + +/* + * Overwrite default connection handling. + * The default behaviour is however for your script to be aborted when the remote client disconnects. + * + * Nextcloud/ownCloud set ignore_user_abort(true) on purpose to work around + * some edge cases where the default behavior would end a script too early. + * + * https://github.com/owncloud/core/issues/22370 + * https://github.com/owncloud/core/pull/26775 + */ +ignore_user_abort(true); + +$body = fopen('dummy://hello', 'r'); + +$response = new HTTP\Response(); +$response->setStatus(200); +$response->addHeader('Content-Length', 25 * 1024 * 1024); +$response->setBody($body); + +HTTP\Sapi::sendResponse($response);