From 5fab3896256a49622f3cd1386967145e1b52b7c0 Mon Sep 17 00:00:00 2001 From: Aleksander Machniak Date: Sat, 5 Apr 2025 13:31:04 +0200 Subject: [PATCH] Add (incomplete) tests for static.php and installer.php --- composer.json | 3 +- public_html/static.php | 4 +- tests/Public/InstallerTest.php | 24 +++++++++ tests/Public/StaticTest.php | 90 ++++++++++++++++++++++++++++++++++ tests/ServerTestCase.php | 49 ++++++++++++++++++ 5 files changed, 167 insertions(+), 3 deletions(-) create mode 100644 tests/Public/InstallerTest.php create mode 100644 tests/Public/StaticTest.php create mode 100644 tests/ServerTestCase.php diff --git a/composer.json b/composer.json index c7ef10d04..b2cb03235 100644 --- a/composer.json +++ b/composer.json @@ -60,7 +60,8 @@ "roundcube/vcard_attachments": "*", "roundcube/virtuser_file": "*", "roundcube/virtuser_query": "*", - "roundcube/zipdownload": "*" + "roundcube/zipdownload": "*", + "symfony/process": "^7.0" }, "suggest": { "bjeavons/zxcvbn-php": "^1.0 required for Zxcvbn password strength driver", diff --git a/public_html/static.php b/public_html/static.php index edfed2013..bb2bc1247 100644 --- a/public_html/static.php +++ b/public_html/static.php @@ -98,7 +98,7 @@ function validateStaticFile(string $path): ?string $found = false; foreach (ALLOWED_PATHS as $prefix) { - if (strpos($path, $prefix) === 0 && !preg_match('~skins/.+/templates/~', $path)) { + if (str_starts_with($path, $prefix) && !preg_match('~skins/.+/templates/~', $path)) { $found = true; break; } @@ -138,7 +138,7 @@ function serveStaticFile($path): void if (isset($_SERVER['HTTP_RANGE'])) { // $valid = preg_match('^bytes=\d*-\d*(,\d*-\d*)*$', $_SERVER['HTTP_RANGE']); - if (substr($_SERVER['HTTP_RANGE'], 0, 6) != 'bytes=') { + if (!str_starts_with($_SERVER['HTTP_RANGE'], 'bytes=')) { http_response_code(416); // "Range Not Satisfiable" header('Content-Range: bytes */' . $size); // Required in 416. return; diff --git a/tests/Public/InstallerTest.php b/tests/Public/InstallerTest.php new file mode 100644 index 000000000..8accb8341 --- /dev/null +++ b/tests/Public/InstallerTest.php @@ -0,0 +1,24 @@ +request('GET', 'installer.php/'); + + $body = (string) $response->getBody(); + + $this->assertSame(200, $response->getStatusCode()); + $this->assertTrue(str_starts_with($body, '')); + } +} diff --git a/tests/Public/StaticTest.php b/tests/Public/StaticTest.php new file mode 100644 index 000000000..aca860903 --- /dev/null +++ b/tests/Public/StaticTest.php @@ -0,0 +1,90 @@ +request('GET', 'static.php/' . $path); + + $file = file_get_contents(INSTALL_PATH . preg_replace('/\?.*$/', '', $path)); + + $this->assertSame(200, $response->getStatusCode()); + $this->assertSame($file, (string) $response->getBody()); + $this->assertSame(['public, max-age=604800'], $response->getHeader('Cache-Control')); + $this->assertSame(['bytes'], $response->getHeader('Accept-Ranges')); + $this->assertSame([(string) strlen($file)], $response->getHeader('Content-Length')); + $this->assertStringContainsString($ctype, $response->getHeader('Content-Type')[0]); + // TODO: Expires header + } + + /** + * Dataset for testForbiddenResources() + */ + public static function provideForbiddenResources(): iterable + { + return [ + [''], + ['CHANGELOG.md'], + ['LICENSE'], + ['skins/../passwd'], + ['skins/elastic/templates/about.html'], + ['plugins/acl/composer.json'], + ['program/include/iniset.php'], + ['program/localization/index.inc'], + ['public_html/.htaccess'], + ['vendor/friendsofphp/php-cs-fixer/logo.png'], + ]; + } + + /** + * Test forbidden resources + */ + #[DataProvider('provideForbiddenResources')] + public function testForbiddenResources($path): void + { + $response = $this->request('GET', 'static.php/' . $path); + + $this->assertSame(404, $response->getStatusCode()); + $this->assertSame('', (string) $response->getBody()); + } + + /** + * Test handling of Modified-Since header + */ + public function testModifiedSinceHeader(): void + { + $this->markTestIncomplete(); + } + + /** + * Test handling of Range header + */ + public function testRangeHeader(): void + { + $this->markTestIncomplete(); + } +} diff --git a/tests/ServerTestCase.php b/tests/ServerTestCase.php new file mode 100644 index 000000000..ea4210c99 --- /dev/null +++ b/tests/ServerTestCase.php @@ -0,0 +1,49 @@ +setWorkingDirectory($path); + static::$phpProcess->start(); + usleep(50 * 1000); // give the server some time before we start testing + } + + #[\Override] + public static function tearDownAfterClass(): void + { + static::$phpProcess->stop(); + } + + /** + * HTTP client request + */ + protected function request($method, $path, $options = []) + { + $config = [ + 'base_uri' => 'http://localhost:8000', + 'http_errors' => false, // no exceptions for HTTP error codes + 'handler' => null, // reset Mock state from other tests + ]; + + $client = \rcmail::get_instance()->get_http_client($config); + + return $client->request($method, $path, $options); + } +}