From f2a16359561ce75a8f045bfab35b3351312e86bb Mon Sep 17 00:00:00 2001 From: Daniel Kesselberg Date: Wed, 24 May 2023 16:02:12 +0200 Subject: [PATCH 1/2] fix: handle client disconnect properly with ignore_user_abort true ignore_user_abort(true) instruct php to not abort the script on client disconnect. To avoid reading the entire stream and dismissing the data afterward, check between the chunks if the client is still there. Signed-off-by: Daniel Kesselberg --- lib/Sapi.php | 6 +++ tests/HTTP/SapiTest.php | 31 ++++++++++++++ tests/www/connection_aborted.php | 69 ++++++++++++++++++++++++++++++++ 3 files changed, 106 insertions(+) create mode 100644 tests/www/connection_aborted.php diff --git a/lib/Sapi.php b/lib/Sapi.php index 4a9520a..f078e3a 100644 --- a/lib/Sapi.php +++ b/lib/Sapi.php @@ -113,6 +113,12 @@ public static function sendResponse(ResponseInterface $response): void 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/tests/HTTP/SapiTest.php b/tests/HTTP/SapiTest.php index c0c820a..1deb941 100644 --- a/tests/HTTP/SapiTest.php +++ b/tests/HTTP/SapiTest.php @@ -300,4 +300,35 @@ public function testSendWorksWithCallbackAsBody(): void self::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..0774954 --- /dev/null +++ b/tests/www/connection_aborted.php @@ -0,0 +1,69 @@ +position = 0; + + return true; + } + + public function stream_read(int $count) + { + $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); From c57492314744327788b6291003c36aaa0f403217 Mon Sep 17 00:00:00 2001 From: Phil Davis Date: Mon, 26 Jun 2023 14:50:42 +0545 Subject: [PATCH 2/2] Declare return type of stream_read in test --- tests/www/connection_aborted.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/www/connection_aborted.php b/tests/www/connection_aborted.php index 0774954..e9f62ab 100644 --- a/tests/www/connection_aborted.php +++ b/tests/www/connection_aborted.php @@ -17,7 +17,7 @@ public function stream_open(string $path, string $mode, int $options, ?string &$ return true; } - public function stream_read(int $count) + public function stream_read(int $count): string { $this->position += $count;