diff --git a/.github/workflows/phpunit-test.yml b/.github/workflows/phpunit-test.yml index c09b199d7..599968526 100644 --- a/.github/workflows/phpunit-test.yml +++ b/.github/workflows/phpunit-test.yml @@ -19,7 +19,7 @@ jobs: - 3306:3306 options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 redis: - image: redis:5.0 + image: redis:7.0 ports: - 6379:6379 options: >- diff --git a/app/Http/Controllers/AnnounceController.php b/app/Http/Controllers/AnnounceController.php index db340f224..0e0ddbd64 100644 --- a/app/Http/Controllers/AnnounceController.php +++ b/app/Http/Controllers/AnnounceController.php @@ -18,7 +18,6 @@ declare(strict_types=1); namespace App\Http\Controllers; use App\Exceptions\TrackerException; -use App\Helpers\Bencode; use App\Models\BlacklistClient; use App\Models\FreeleechToken; use App\Models\Group; @@ -91,7 +90,7 @@ class AnnounceController extends Controller */ public function index(Request $request, string $passkey): ?Response { - $repDict = null; + $response = null; try { // Check client. @@ -129,14 +128,14 @@ class AnnounceController extends Controller } // Generate A Response For The Torrent Client. - $repDict = $this->generateSuccessAnnounceResponse($queries, $torrent, $user); + $response = $this->generateSuccessAnnounceResponse($queries, $torrent, $user); // Process Annnounce Job. $this->processAnnounceJob($queries, $user, $group, $torrent); } catch (TrackerException $exception) { - $repDict = $this->generateFailedAnnounceResponse($exception); + $response = $this->generateFailedAnnounceResponse($exception); } finally { - return $this->sendFinalAnnounceResponse($repDict); + return $this->sendFinalAnnounceResponse($response); } } @@ -471,9 +470,9 @@ class AnnounceController extends Controller // Detect broken (namely qBittorrent) clients sending duplicate announces // and eliminate them from screwing up stats. - $duplicateAnnounceKey = 'announce-lock:'.$user->id.'-'.$torrent->id.'-'.base64_decode($queries['peer_id']).'-'.$event; + $duplicateAnnounceKey = config('cache.prefix').'announce-lock:'.$user->id.'-'.$torrent->id.'-'.base64_decode($queries['peer_id']).'-'.$event; - $lastAnnouncedAt = Redis::command('SET', [$duplicateAnnounceKey, $now, 'NX', 'GET', 'EX', '30']); + $lastAnnouncedAt = Redis::connection('announce')->command('SET', [$duplicateAnnounceKey, $now, 'NX', 'GET', 'EX', '30']); if ($lastAnnouncedAt !== null) { throw new TrackerException(162, [':elapsed' => $now - $lastAnnouncedAt]); @@ -481,16 +480,16 @@ class AnnounceController extends Controller // Block clients disrespecting the min interval - $lastAnnouncedKey = 'peer-last-announced:'.$user->id.'-'.$torrent->id.'-'.base64_decode($queries['peer_id']); + $lastAnnouncedKey = config('cache.prefix').'peer-last-announced:'.$user->id.'-'.$torrent->id.'-'.base64_decode($queries['peer_id']); $randomMinInterval = intdiv(random_int(85, 95) * self::MIN, 100); - $lastAnnouncedAt = Redis::command('SET', [$lastAnnouncedKey, $now, 'NX', 'GET', 'EX', $randomMinInterval]); + $lastAnnouncedAt = Redis::connection('announce')->command('SET', [$lastAnnouncedKey, $now, 'NX', 'GET', 'EX', $randomMinInterval]); // Delete the timer if the user paused the torrent, and it's been at // least 5 minutes since they last announced. if ($event === 'stopped' && $lastAnnouncedAt < $now - 5 * 60) { - Redis::command('DEL', [$lastAnnouncedKey]); + Redis::connection('announce')->command('DEL', [$lastAnnouncedKey]); } elseif ($lastAnnouncedAt !== null && ! \in_array($event, ['completed', 'stopped'])) { throw new TrackerException(162, [':elapsed' => $now - $lastAnnouncedAt]); } @@ -560,18 +559,21 @@ class AnnounceController extends Controller * * @throws Exception */ - private function generateSuccessAnnounceResponse($queries, $torrent, $user): array + private function generateSuccessAnnounceResponse($queries, $torrent, $user): string { // Build Response For Bittorrent Client - $repDict = [ - 'interval' => random_int(self::MIN, self::MAX), - 'min interval' => self::MIN, - 'complete' => (int) $torrent->seeders, - 'incomplete' => (int) $torrent->leechers, - 'downloaded' => (int) $torrent->times_completed, - 'peers' => '', - 'peers6' => '', - ]; + // Keys must be ordered alphabetically + $response = 'd8:completei' + .$torrent->seeders + .'e10:downloadedi' + .$torrent->times_completed + .'e10:incompletei' + .$torrent->leechers + .'e8:intervali' + .random_int(self::MIN, self::MAX) + .'e12:min intervali' + .self::MIN + .'e'; /** * For non `stopped` event only @@ -590,15 +592,29 @@ class AnnounceController extends Controller ->only(['ip', 'port']) ->toArray(); + $peersIpv4 = ''; + $peersIpv6 = ''; + foreach ($peers as $peer) { if (isset($peer['ip'], $peer['port'])) { - $peer_insert_field = filter_var($peer['ip'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) ? 'peers' : 'peers6'; - $repDict[$peer_insert_field] .= inet_pton($peer['ip']).pack('n', (int) $peer['port']); + if (filter_var($peer['ip'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { + $peersIpv4 .= inet_pton($peer['ip']).pack('n', (int) $peer['port']); + } else { + $peersIpv6 .= inet_pton($peer['ip']).pack('n', (int) $peer['port']); + } } } + + $response .= '5:peers'.\strlen($peersIpv4).':'.$peersIpv4; + + if ($peersIpv6 !== '') { + $response .= '6:peers6'.\strlen($peersIpv6).':'.$peersIpv6; + } } - return $repDict; + $response .= 'e'; + + return $response; } /** @@ -764,19 +780,18 @@ class AnnounceController extends Controller } } - protected function generateFailedAnnounceResponse(TrackerException $trackerException): array + protected function generateFailedAnnounceResponse(TrackerException $trackerException): string { - return [ - 'failure reason' => $trackerException->getMessage(), - 'min interval' => self::MIN, - ]; + $message = $trackerException->getMessage(); + + return 'd14:failure reason'.\strlen($message).':'.$message.'8:intervali'.self::MIN.'e12:min intervali'.self::MIN.'ee'; } /** * Send Final Announce Response. */ - protected function sendFinalAnnounceResponse($repDict): Response + protected function sendFinalAnnounceResponse(string $response): Response { - return response(Bencode::bencode($repDict), headers: self::HEADERS); + return response($response, headers: self::HEADERS); } } diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 09dd55ee6..ede09346a 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -2110,11 +2110,6 @@ parameters: count: 1 path: app/Http/Controllers/AnnounceController.php - - - message: "#^Method App\\\\Http\\\\Controllers\\\\AnnounceController\\:\\:generateFailedAnnounceResponse\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: app/Http/Controllers/AnnounceController.php - - message: "#^Method App\\\\Http\\\\Controllers\\\\AnnounceController\\:\\:generateSuccessAnnounceResponse\\(\\) has parameter \\$queries with no type specified\\.$#" count: 1 @@ -2130,11 +2125,6 @@ parameters: count: 1 path: app/Http/Controllers/AnnounceController.php - - - message: "#^Method App\\\\Http\\\\Controllers\\\\AnnounceController\\:\\:generateSuccessAnnounceResponse\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: app/Http/Controllers/AnnounceController.php - - message: "#^Method App\\\\Http\\\\Controllers\\\\AnnounceController\\:\\:processAnnounceJob\\(\\) has parameter \\$group with no type specified\\.$#" count: 1 @@ -2155,11 +2145,6 @@ parameters: count: 1 path: app/Http/Controllers/AnnounceController.php - - - message: "#^Method App\\\\Http\\\\Controllers\\\\AnnounceController\\:\\:sendFinalAnnounceResponse\\(\\) has parameter \\$repDict with no type specified\\.$#" - count: 1 - path: app/Http/Controllers/AnnounceController.php - - message: "#^Method App\\\\Http\\\\Controllers\\\\Auth\\\\ActivationController\\:\\:activate\\(\\) has parameter \\$token with no type specified\\.$#" count: 1 diff --git a/tests/Feature/Http/Controllers/AnnounceControllerTest.php b/tests/Feature/Http/Controllers/AnnounceControllerTest.php index 0d9ab07bd..0f7117c4c 100644 --- a/tests/Feature/Http/Controllers/AnnounceControllerTest.php +++ b/tests/Feature/Http/Controllers/AnnounceControllerTest.php @@ -35,5 +35,5 @@ test('index returns an ok response', function (): void { ])); $response ->assertOk(); - $this->assertArrayNotHasKey('failure reason', [$response->getContent()]); + $this->assertStringNotContainsString('failure reason', $response->getContent()); });