feat: add secure on-demand HLS playback plugin
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Plugin\AzuraCastOnDemandHls\Tests\Security;
|
||||
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use Plugin\AzuraCastOnDemandHls\Security\ResourcePath;
|
||||
use Plugin\AzuraCastOnDemandHls\Tests\Support\TemporaryDirectoryTestCase;
|
||||
|
||||
#[CoversClass(ResourcePath::class)]
|
||||
final class ResourcePathTest extends TemporaryDirectoryTestCase
|
||||
{
|
||||
public function testOnlyExpectedHlsNamesAreAllowed(): void
|
||||
{
|
||||
self::assertTrue(ResourcePath::isAllowed('master.m3u8'));
|
||||
self::assertTrue(ResourcePath::isAllowed('media.m3u8'));
|
||||
self::assertTrue(ResourcePath::isAllowed('segment00000.ts'));
|
||||
self::assertTrue(ResourcePath::isAllowed('segment99999.ts'));
|
||||
}
|
||||
|
||||
#[DataProvider('traversalProvider')]
|
||||
public function testTraversalAndUnexpectedPathsAreRejected(string $path): void
|
||||
{
|
||||
self::assertFalse(ResourcePath::isAllowed($path));
|
||||
}
|
||||
|
||||
/** @return iterable<string, array{string}> */
|
||||
public static function traversalProvider(): iterable
|
||||
{
|
||||
yield 'dot-dot slash' => ['../master.m3u8'];
|
||||
yield 'dot-dot backslash' => ['..\\master.m3u8'];
|
||||
yield 'absolute Unix' => ['/etc/passwd'];
|
||||
yield 'absolute Windows' => ['C:\\Windows\\win.ini'];
|
||||
yield 'URL encoded' => ['%2e%2e%2fmaster.m3u8'];
|
||||
yield 'double encoded' => ['%252e%252e%252fmaster.m3u8'];
|
||||
yield 'null byte' => ["master.m3u8\0.ts"];
|
||||
yield 'nested segment' => ['folder/segment00000.ts'];
|
||||
yield 'bad extension' => ['segment00000.php'];
|
||||
yield 'unbounded digits' => ['segment1.ts'];
|
||||
yield 'query text' => ['media.m3u8?x=1'];
|
||||
}
|
||||
|
||||
public function testLongMediaSegmentNumbersRemainValid(): void
|
||||
{
|
||||
self::assertTrue(ResourcePath::isAllowed('segment100000.ts'));
|
||||
self::assertTrue(ResourcePath::isAllowed('segment999999999.ts'));
|
||||
self::assertFalse(ResourcePath::isAllowed('segment1000000000.ts'));
|
||||
}
|
||||
|
||||
public function testResolveRejectsSymlinksAndMissingFiles(): void
|
||||
{
|
||||
file_put_contents($this->temporaryDirectory . '/master.m3u8', '#EXTM3U');
|
||||
self::assertSame(
|
||||
realpath($this->temporaryDirectory . '/master.m3u8'),
|
||||
ResourcePath::resolve($this->temporaryDirectory, 'master.m3u8')
|
||||
);
|
||||
self::assertNull(ResourcePath::resolve($this->temporaryDirectory, 'media.m3u8'));
|
||||
|
||||
file_put_contents($this->temporaryDirectory . '/outside', 'secret');
|
||||
symlink($this->temporaryDirectory . '/outside', $this->temporaryDirectory . '/media.m3u8');
|
||||
self::assertNull(ResourcePath::resolve($this->temporaryDirectory, 'media.m3u8'));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user