diff --git a/.env.example b/.env.example index fe504eec8..bd61f8d25 100644 --- a/.env.example +++ b/.env.example @@ -109,6 +109,10 @@ CLOUDFLARE_IPS_RETRY_SLEEP_MS=250 CLOUDFLARE_IPS_STORAGE_PATH=storage/app/cloudflare/trusted-proxies.json CLOUDFLARE_TRUST_REMOTE_ADDR_FALLBACK=true +# Block configured indexer apps only when they proxy NZB downloads instead of redirecting to the downloader. +BLOCK_PROXY_INDEXER_APPS=false +BLOCK_PROXY_INDEXER_APP_USER_AGENTS=Prowlarr/,NZBHydra2 + APP_LOCALE=en APP_FALLBACK_LOCALE=en APP_FAKER_LOCALE=en_US diff --git a/app/Http/Middleware/BlockAbusiveServices.php b/app/Http/Middleware/BlockAbusiveServices.php index 254e8ab4f..551756dd6 100644 --- a/app/Http/Middleware/BlockAbusiveServices.php +++ b/app/Http/Middleware/BlockAbusiveServices.php @@ -17,6 +17,7 @@ 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) * - Oracle Cloud (ASN: AS31898) * - Cloudflare WARP (ASN: AS13335) */ @@ -71,6 +72,16 @@ class BlockAbusiveServices return $this->blockedResponse('Access denied: Streaming services are not allowed.'); } + if ($this->shouldBlockProxyIndexerApp($request, $userAgent)) { + Log::warning('Blocked proxy indexer app request', [ + 'ip' => $ip, + 'user_agent' => $userAgent, + 'uri' => $request->getRequestUri(), + ]); + + return $this->blockedResponse('Access denied: Proxy indexer app access is not allowed.'); + } + // Check for blocked ASNs $asnInfo = $this->getAsnInfo($ip); if ($asnInfo !== null && isset($this->blockedAsns[$asnInfo['asn']])) { @@ -107,6 +118,78 @@ class BlockAbusiveServices return false; } + /** + * Check whether an opt-in proxy indexer app block should apply. + */ + protected function shouldBlockProxyIndexerApp(Request $request, string $userAgent): bool + { + if (! (bool) config('nntmux.block_proxy_indexer_apps', false)) { + return false; + } + + if (! $this->isPublicIndexerEndpoint($request)) { + return false; + } + + return $this->matchesAnyUserAgentPattern( + $userAgent, + $this->configuredProxyIndexerAppUserAgents() + ); + } + + /** + * Detect public indexer API/RSS endpoints while leaving unrelated requests alone. + */ + protected function isPublicIndexerEndpoint(Request $request): bool + { + if ($request->is('api/v1/api') || $request->is('api/v2/*')) { + return true; + } + + return $request->is('rss/*') && ! $request->is('rss/health'); + } + + /** + * @return array + */ + protected function configuredProxyIndexerAppUserAgents(): array + { + $configured = config('nntmux.block_proxy_indexer_app_user_agents', []); + + if (is_string($configured)) { + $configured = preg_split('/[\r\n,]+/', $configured) ?: []; + } + + if (! is_array($configured)) { + Log::warning('Ignoring invalid proxy indexer app User-Agent configuration.', [ + 'configured_type' => get_debug_type($configured), + ]); + + return []; + } + + return array_values(array_filter(array_map( + static fn (mixed $pattern): string => is_string($pattern) ? trim($pattern) : '', + $configured, + ))); + } + + /** + * @param array $patterns + */ + protected function matchesAnyUserAgentPattern(string $userAgent, array $patterns): bool + { + $lowerUserAgent = strtolower($userAgent); + + foreach ($patterns as $pattern) { + if (str_contains($lowerUserAgent, strtolower($pattern))) { + return true; + } + } + + return false; + } + /** * Get ASN information for an IP address. * Uses ip-api.com with caching to minimize API calls. diff --git a/config/nntmux.php b/config/nntmux.php index 19f67a000..d4eef5c59 100644 --- a/config/nntmux.php +++ b/config/nntmux.php @@ -21,6 +21,8 @@ return [ 'purge_inactive_users' => env('PURGE_INACTIVE_USERS', false), 'purge_inactive_users_days' => env('PURGE_INACTIVE_USERS_DAYS', 180), 'mysql_search_fallback' => env('MYSQL_SEARCH_FALLBACK', false), // Disable MySQL LIKE fallback when Manticore/Elasticsearch return no results + 'block_proxy_indexer_apps' => (bool) env('BLOCK_PROXY_INDEXER_APPS', false), + 'block_proxy_indexer_app_user_agents' => env('BLOCK_PROXY_INDEXER_APP_USER_AGENTS', 'Prowlarr/,NZBHydra2'), /* |--------------------------------------------------------------------------