diff --git a/app/Http/Middleware/BlockAbusiveServices.php b/app/Http/Middleware/BlockAbusiveServices.php index 937aef180..548b8b4c2 100644 --- a/app/Http/Middleware/BlockAbusiveServices.php +++ b/app/Http/Middleware/BlockAbusiveServices.php @@ -17,7 +17,11 @@ use Symfony\Component\HttpFoundation\Response; * Blocks: * - AIOStreams (User-Agent based) * - UsenetStreamer (User-Agent based) - * - Configured indexer apps on public indexer API/RSS endpoints (User-Agent based, opt-in) + * - Configured indexer apps proxying NZB downloads (User-Agent based, opt-in). + * Only NZB download/grab requests are blocked, so proxied searches and RSS + * feeds keep working. Redirected grabs (Prowlarr/NZBHydra2 "Redirect" setting) + * arrive directly from the download client, whose User-Agent does not match + * the configured proxy patterns, and are therefore allowed. * - Oracle Cloud (ASN: AS31898) * - Cloudflare WARP (ASN: AS13335) */ @@ -83,13 +87,13 @@ class BlockAbusiveServices } if ($this->shouldBlockProxyIndexerApp($request, $userAgent)) { - Log::warning('Blocked proxy indexer app request', [ + Log::warning('Blocked proxied NZB download from indexer app', [ 'ip' => $ip, 'user_agent' => $userAgent, 'uri' => $this->safeRequestUri($request), ]); - return $this->blockedResponse('Access denied: Proxy indexer app access is not allowed.'); + return $this->blockedResponse('Access denied: Proxying NZB downloads through indexer apps is not allowed.'); } // Check for blocked ASNs @@ -137,7 +141,7 @@ class BlockAbusiveServices return false; } - if (! $this->isPublicIndexerEndpoint($request)) { + if (! $this->isNzbDownloadRequest($request)) { return false; } @@ -148,15 +152,25 @@ class BlockAbusiveServices } /** - * Detect public indexer API/RSS endpoints while leaving unrelated requests alone. + * Detect NZB download/grab requests so only proxied downloads are blocked. + * + * Searches, caps and RSS feeds are intentionally out of scope so proxied + * searches keep working. Redirected grabs arrive directly from the download + * client, whose User-Agent does not match the configured proxy patterns. */ - protected function isPublicIndexerEndpoint(Request $request): bool + protected function isNzbDownloadRequest(Request $request): bool { - if ($request->is('api/v1/api') || $request->is('api/v2/*')) { + if ($request->is('api/v2/getnzb')) { return true; } - return $request->is('rss/*') && ! $request->is('rss/health'); + if (! $request->is('api/v1/api')) { + return false; + } + + $type = strtolower(trim((string) $request->input('t', ''))); + + return in_array($type, ['get', 'g'], true); } /** diff --git a/tests/Feature/Http/Middleware/BlockAbusiveServicesTest.php b/tests/Feature/Http/Middleware/BlockAbusiveServicesTest.php index 0af0c0452..211ccd2ae 100644 --- a/tests/Feature/Http/Middleware/BlockAbusiveServicesTest.php +++ b/tests/Feature/Http/Middleware/BlockAbusiveServicesTest.php @@ -320,4 +320,62 @@ final class BlockAbusiveServicesTest extends TestCase $this->assertArrayHasKey('message', $json); $this->assertTrue($json['error']); } + + /** + * Test that a proxied NZB download is blocked when the block is enabled. + */ + public function test_blocks_proxied_download_from_indexer_app(): void + { + config()->set('nntmux.block_proxy_indexer_apps', true); + config()->set('nntmux.block_proxy_indexer_app_user_agents', 'Prowlarr/,NZBHydra2'); + + $request = Request::create('/api/v1/api?t=get&id=release-guid', 'GET'); + $request->headers->set('User-Agent', 'Prowlarr/2.0.0'); + $request->server->set('REMOTE_ADDR', '127.0.0.1'); + + $response = $this->middleware->handle($request, function ($req) { + return new Response('OK', 200); + }); + + $this->assertEquals(403, $response->getStatusCode()); + $this->assertStringContainsString('Proxying NZB downloads through indexer apps is not allowed', $response->getContent()); + } + + /** + * Test that a proxied search is allowed even when the block is enabled. + */ + public function test_allows_proxied_search_from_indexer_app(): void + { + config()->set('nntmux.block_proxy_indexer_apps', true); + config()->set('nntmux.block_proxy_indexer_app_user_agents', 'Prowlarr/,NZBHydra2'); + + $request = Request::create('/api/v1/api?t=search&q=linux', 'GET'); + $request->headers->set('User-Agent', 'Prowlarr/2.0.0'); + $request->server->set('REMOTE_ADDR', '127.0.0.1'); + + $response = $this->middleware->handle($request, function ($req) { + return new Response('OK', 200); + }); + + $this->assertEquals(200, $response->getStatusCode()); + } + + /** + * Test that a redirected download (download-client UA) is allowed. + */ + public function test_allows_redirected_download_from_download_client(): void + { + config()->set('nntmux.block_proxy_indexer_apps', true); + config()->set('nntmux.block_proxy_indexer_app_user_agents', 'Prowlarr/,NZBHydra2'); + + $request = Request::create('/api/v1/api?t=get&id=release-guid', 'GET'); + $request->headers->set('User-Agent', 'SABnzbd/4.3.3'); + $request->server->set('REMOTE_ADDR', '127.0.0.1'); + + $response = $this->middleware->handle($request, function ($req) { + return new Response('OK', 200); + }); + + $this->assertEquals(200, $response->getStatusCode()); + } } diff --git a/tests/Unit/BlockAbusiveServicesTest.php b/tests/Unit/BlockAbusiveServicesTest.php index 593f2951c..fc221f49b 100644 --- a/tests/Unit/BlockAbusiveServicesTest.php +++ b/tests/Unit/BlockAbusiveServicesTest.php @@ -76,40 +76,42 @@ class BlockAbusiveServicesTest extends TestCase $this->assertSame(Response::HTTP_OK, $response->getStatusCode()); } - public function test_enabled_proxy_indexer_app_block_denies_configured_user_agent_on_newznab_endpoints(): void + public function test_enabled_proxy_indexer_app_block_denies_proxied_downloads(): void { config()->set('nntmux.block_proxy_indexer_apps', true); config()->set('nntmux.block_proxy_indexer_app_user_agents', 'Prowlarr/,NZBHydra2'); foreach ([ - '/api/v1/api?t=caps', - '/api/v1/api?t=search&q=linux', - '/api/v1/api?t=get&id=release-guid', - ] as $uri) { - $response = $this->handleRequest($uri, 'Prowlarr/2.0.0'); + '/api/v1/api?t=get&id=release-guid' => 'Prowlarr/2.0.0', + '/api/v1/api?t=g&id=release-guid' => 'Prowlarr/2.0.0', + '/api/v2/getnzb?id=release-guid' => 'NZBHydra2 8.3.0', + ] as $uri => $userAgent) { + $response = $this->handleRequest($uri, $userAgent); $this->assertSame(Response::HTTP_FORBIDDEN, $response->getStatusCode(), $uri); - $this->assertStringContainsString('Proxy indexer app access is not allowed', (string) $response->getContent()); + $this->assertStringContainsString('Proxying NZB downloads through indexer apps is not allowed', (string) $response->getContent()); } } - public function test_enabled_proxy_indexer_app_block_denies_configured_user_agent_on_v2_endpoints(): void + public function test_enabled_proxy_indexer_app_block_allows_proxied_searches(): void { config()->set('nntmux.block_proxy_indexer_apps', true); config()->set('nntmux.block_proxy_indexer_app_user_agents', 'Prowlarr/,NZBHydra2'); foreach ([ - '/api/v2/capabilities', - '/api/v2/search?q=linux', - '/api/v2/getnzb?id=release-guid', - ] as $uri) { - $response = $this->handleRequest($uri, 'NZBHydra2 8.3.0'); + '/api/v1/api?t=caps' => 'Prowlarr/2.0.0', + '/api/v1/api?t=search&q=linux' => 'Prowlarr/2.0.0', + '/api/v1/api?t=tvsearch&q=linux' => 'Prowlarr/2.0.0', + '/api/v2/capabilities' => 'NZBHydra2 8.3.0', + '/api/v2/search?q=linux' => 'NZBHydra2 8.3.0', + ] as $uri => $userAgent) { + $response = $this->handleRequest($uri, $userAgent); - $this->assertSame(Response::HTTP_FORBIDDEN, $response->getStatusCode(), $uri); + $this->assertSame(Response::HTTP_OK, $response->getStatusCode(), $uri); } } - public function test_enabled_proxy_indexer_app_block_denies_configured_user_agent_on_rss_feed_endpoints(): void + public function test_enabled_proxy_indexer_app_block_allows_proxied_rss_feeds(): void { config()->set('nntmux.block_proxy_indexer_apps', true); config()->set('nntmux.block_proxy_indexer_app_user_agents', 'Prowlarr/,NZBHydra2'); @@ -120,7 +122,7 @@ class BlockAbusiveServicesTest extends TestCase ] as $uri) { $response = $this->handleRequest($uri, 'Prowlarr/2.0.0'); - $this->assertSame(Response::HTTP_FORBIDDEN, $response->getStatusCode(), $uri); + $this->assertSame(Response::HTTP_OK, $response->getStatusCode(), $uri); } } @@ -167,13 +169,13 @@ class BlockAbusiveServicesTest extends TestCase config()->set('nntmux.block_proxy_indexer_app_user_agents', 'Prowlarr/,NZBHydra2'); $response = $this->handleRequest( - '/api/v1/api?t=search&apikey=secret-api-key&api_token=secret-token&passkey=secret-passkey&q=linux', + '/api/v1/api?t=get&id=release-guid&apikey=secret-api-key&api_token=secret-token&passkey=secret-passkey', 'Prowlarr/2.0.0' ); $this->assertSame(Response::HTTP_FORBIDDEN, $response->getStatusCode()); Log::shouldHaveReceived('warning') - ->with('Blocked proxy indexer app request', \Mockery::on(function (mixed $context): bool { + ->with('Blocked proxied NZB download from indexer app', \Mockery::on(function (mixed $context): bool { if (! is_array($context)) { return false; } @@ -183,7 +185,7 @@ class BlockAbusiveServicesTest extends TestCase return str_contains($uri, 'apikey=%5Bredacted%5D') && str_contains($uri, 'api_token=%5Bredacted%5D') && str_contains($uri, 'passkey=%5Bredacted%5D') - && str_contains($uri, 'q=linux') + && str_contains($uri, 'id=release-guid') && ! str_contains($uri, 'secret-api-key') && ! str_contains($uri, 'secret-token') && ! str_contains($uri, 'secret-passkey');