mirror of
https://github.com/Vateron-Media/XC_VM.git
synced 2026-10-25 03:03:12 +01:00
In Redis mode live.php took ConnectionTracker::getLineConnections() as connection rows, but it answers the LINE# set's members: connection keys (uuid strings). array_column() over them gave [], and array_multisort([], SORT_ASC, $rConnections) threw "ValueError: Array sizes are inconsistent" on PHP 8 as soon as the line had one active connection, including the same viewer's next playlist refresh. The request died with a 500 instead of being limited. For an HMAC token the line id is null, so passing it to getLineConnections(int) was a TypeError before any connection was counted. Add ConnectionTracker::getLineConnectionRows(), which reads and unserializes the rows behind a line's LINE# keys (an unreachable Redis, a failed call or an unreadable payload reads as no connection), and ConnectionTracker::oldestConnectionIP(), which picks the user_ip with the earliest date_start. live.php now accepts that IP, as the MySQL branch does with the first lines_live row. HMAC identities skip the check, as they already do in MySQL mode where `user_id = NULL` matches nothing. getLineConnections() and ConnectionLimiter are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TQHeypQpfAXTdCysmi7b6a
110 lines
4.3 KiB
PHP
110 lines
4.3 KiB
PHP
<?php
|
|
|
|
use PHPUnit\Framework\TestCase;
|
|
use XcVm\Domain\Stream\ConnectionTracker;
|
|
|
|
/**
|
|
* disallow_2nd_ip_con in Redis mode — live.php accepts only the IP of the
|
|
* line's oldest connection. The LINE# set holds connection keys, not the
|
|
* connections, so the rows are read behind them before the oldest is picked;
|
|
* sorting the keys by date_start was a ValueError on PHP 8.
|
|
*/
|
|
final class LiveSecondIpConnectionTest extends TestCase {
|
|
|
|
public function testOldestConnectionIpPicksTheEarliestStartWhateverTheOrder(): void {
|
|
$rRows = [
|
|
['user_ip' => '3.3.3.3', 'date_start' => 300],
|
|
['user_ip' => '1.1.1.1', 'date_start' => 100],
|
|
['user_ip' => '2.2.2.2', 'date_start' => 200],
|
|
];
|
|
|
|
$this->assertSame('1.1.1.1', ConnectionTracker::oldestConnectionIP($rRows));
|
|
$this->assertSame('1.1.1.1', ConnectionTracker::oldestConnectionIP(array_reverse($rRows)));
|
|
}
|
|
|
|
public function testOldestConnectionIpSkipsUnreadableRowsAndRowsWithoutAnIp(): void {
|
|
$rRows = [
|
|
false,
|
|
['date_start' => 50],
|
|
['user_ip' => '', 'date_start' => 60],
|
|
['user_ip' => '2.2.2.2', 'date_start' => 200],
|
|
['user_ip' => '1.1.1.1', 'date_start' => 100],
|
|
];
|
|
|
|
$this->assertSame('1.1.1.1', ConnectionTracker::oldestConnectionIP($rRows));
|
|
}
|
|
|
|
public function testOldestConnectionIpSortsUndatedRowsLastAndKeepsTheFirstOnATie(): void {
|
|
$rRows = [
|
|
['user_ip' => '9.9.9.9'],
|
|
['user_ip' => '1.1.1.1', 'date_start' => 100],
|
|
['user_ip' => '2.2.2.2', 'date_start' => 100],
|
|
];
|
|
|
|
$this->assertSame('1.1.1.1', ConnectionTracker::oldestConnectionIP($rRows));
|
|
$this->assertSame('9.9.9.9', ConnectionTracker::oldestConnectionIP([['user_ip' => '9.9.9.9']]));
|
|
}
|
|
|
|
public function testNoConnectionsMeansNoAcceptedIp(): void {
|
|
$this->assertNull(ConnectionTracker::oldestConnectionIP([]));
|
|
}
|
|
|
|
public function testConnectionKeysAloneYieldNoIpInsteadOfAValueError(): void {
|
|
// The shape live.php used to sort: the LINE# members, uuid strings.
|
|
$this->assertNull(ConnectionTracker::oldestConnectionIP(['uuid-a', 'uuid-b']));
|
|
}
|
|
|
|
public function testLineConnectionRowsReadsTheRowsBehindTheLineKeys(): void {
|
|
if (!extension_loaded('redis') || !extension_loaded('igbinary')) {
|
|
$this->markTestSkipped('phpredis and igbinary are needed for the \Redis double and the payloads.');
|
|
}
|
|
|
|
$rRedis = $this->createMock(\Redis::class);
|
|
$rRedis->expects($this->once())->method('zRangeByScore')->with('LINE#7', '-inf', '+inf')->willReturn(['a', 'b']);
|
|
$rRedis->expects($this->once())->method('mGet')->with(['a', 'b'])->willReturn([igbinary_serialize(['user_ip' => '1.1.1.1', 'date_start' => 5]), false]);
|
|
|
|
$this->assertSame([['user_ip' => '1.1.1.1', 'date_start' => 5]], ConnectionTracker::getLineConnectionRows($rRedis, 7, true));
|
|
}
|
|
|
|
public function testDroppedConnectionReadsAsNoRows(): void {
|
|
if (!extension_loaded('redis')) {
|
|
$this->markTestSkipped('phpredis is not loaded; the mGet branch needs a \Redis double.');
|
|
}
|
|
|
|
$rRedis = $this->createMock(\Redis::class);
|
|
$rRedis->method('zRangeByScore')->willReturn(['a']);
|
|
$rRedis->method('mGet')->willReturn(false);
|
|
|
|
$this->assertSame([], ConnectionTracker::getLineConnectionRows($rRedis, 7, true));
|
|
}
|
|
|
|
public function testUnreachableRedisReadsAsNoRows(): void {
|
|
$this->assertSame([], ConnectionTracker::getLineConnectionRows(null, 7, true));
|
|
}
|
|
|
|
public function testFailedKeyLookupReadsAsNoRows(): void {
|
|
if (!extension_loaded('redis')) {
|
|
$this->markTestSkipped('phpredis is not loaded; the zRangeByScore branch needs a \Redis double.');
|
|
}
|
|
|
|
$rRedis = $this->createMock(\Redis::class);
|
|
$rRedis->method('zRangeByScore')->willReturn(false);
|
|
$rRedis->expects($this->never())->method('mGet');
|
|
|
|
$this->assertSame([], ConnectionTracker::getLineConnectionRows($rRedis, 7, true));
|
|
}
|
|
|
|
public function testLiveEndpointNoLongerSortsTheLineKeys(): void {
|
|
$rPath = MAIN_HOME . 'Public/stream/live.php';
|
|
if (!is_file($rPath)) {
|
|
$this->markTestSkipped('live.php is not part of this layout');
|
|
}
|
|
$rSource = (string) file_get_contents($rPath);
|
|
|
|
// getLineConnections() answers the LINE# members (keys); sorting those by
|
|
// date_start threw "Array sizes are inconsistent" for any live line.
|
|
$this->assertSame(0, substr_count($rSource, 'array_multisort'), 'live.php must not sort connection keys');
|
|
$this->assertSame(0, substr_count($rSource, 'ConnectionTracker::getLineConnections('), 'live.php must read the connection rows, not the keys');
|
|
}
|
|
}
|