Files

257 lines
9.6 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
class BotDetector
{
// Tamamen engelle — 403
private const BAD_BOTS = [
'scrapy', 'httrack', 'webcopier', 'webzip', 'teleportpro', 'webstripper',
'offline explorer', 'larbin', 'libwww-perl',
'masscan', 'nikto', 'sqlmap', 'nmap', 'zgrab', 'nuclei',
'dirsearch', 'gobuster', 'feroxbuster', 'ffuf',
'ahrefsbot', 'semrushbot', 'dotbot', 'mj12bot', 'blexbot',
'bytespider', 'gptbot', 'claudebot', 'chatgpt-user',
'ccbot', 'anthropic-ai', 'cohere-ai', 'omgili', 'omgilibot',
'pinterestbot', 'petalbot', 'proximic', 'mediatoolkitbot',
'dataforseobot', 'sitechecker', 'seokicks', 'linkfluence',
'serpstatbot', 'serendeputy', 'riddler', 'netcraftsurveyagent',
'netsystemsresearch', 'ioncrawl', 'brandverity',
'wp_is_mobile', 'wordpress', 'wpbot',
];
// Bu IP prefix'leri için tüm kontroller atlanır (güvenilir crawler'lar)
private const TRUSTED_IP_PREFIXES = [
'66.249.', // Googlebot IPv4
'64.233.', // Google
'74.125.', // Google
'209.85.', // Google
'216.239.', // Google
'34.68.', // Google Cloud us-central1
'34.64.', // Google Cloud
'35.187.', // Google Cloud
'35.190.', // Google Cloud
'2001:4860:', // Googlebot IPv6
'2a00:1450:', // Google IPv6
];
// İzin ver ama bot olarak işaretle (SEO crawler'lar)
private const GOOD_BOTS = [
'googlebot', 'google-inspectiontool', 'google-structured-data-testing-tool',
'adsbot-google', 'adsbot-google-mobile', 'mediapartners-google',
'apis-google', 'feedfetcher-google', 'google-adwords-instant',
'bingbot', 'msnbot', 'adidxbot', 'bingpreview',
'duckduckbot', 'baiduspider', 'yandexbot', 'yandexmobilebot',
'slurp', 'teoma', 'ia_archiver',
'facebot', 'facebookexternalhit',
'twitterbot', 'linkedinbot', 'whatsapp',
'applebot', 'discordbot', 'telegrambot',
'slackbot', 'pingdom',
];
// Rate-limit uygula (dakikada 10'dan fazla → uyarı, 30'dan fazla → otomatik engel)
private const GENERIC_BOTS = [
'curl/', 'wget/', 'python-requests', 'python-urllib',
'go-http-client', 'java/', 'okhttp/', 'libcurl',
'node-fetch', 'node.js', 'axios', 'got/',
'apache-httpclient', 'php/', 'perl/', 'ruby',
'postman', 'insomnia', 'httpie',
];
// Bu path'ler için sadece log tut, engelleme ve genel rate limit yapma
private const SKIP_PATHS = [
'/up',
'/api/',
'/sitemap',
'/stream/',
'/vtt-proxy',
'/track/',
'/calls/',
'/episode/',
'/comments',
'/media/',
'/assets/',
'/build/',
'/vendor/',
];
public function handle(Request $request, Closure $next)
{
// Cloudflare / proxy arkasındaki gerçek istemci IP'sini tespit et
$cfIp = $request->header('CF-Connecting-IP')
?: ($request->header('True-Client-IP')
?: ($request->header('X-Forwarded-For') ? trim(explode(',', $request->header('X-Forwarded-For'))[0]) : null));
$ip = $cfIp ?: $request->ip();
$ua = strtolower($request->userAgent() ?? '');
$path = '/' . ltrim($request->path(), '/');
// index.php önekini normalize et (/index.php/stream/seg -> /stream/seg)
if (str_starts_with($path, '/index.php/')) {
$path = '/' . substr($path, 11);
}
// Botların video parçalarını ve m3u8 playlistlerini boşuna taramasını engelle (CPU / FastCGI koruması)
$isBotCandidate = false;
if (!empty($ua)) {
foreach (array_merge(self::BAD_BOTS, self::GOOD_BOTS, self::GENERIC_BOTS) as $botPattern) {
if (str_contains($ua, $botPattern)) {
$isBotCandidate = true;
break;
}
}
}
if ($isBotCandidate && (str_starts_with($path, '/stream/') || str_starts_with($path, '/vtt-proxy'))) {
return response('No bot indexing allowed on media streams.', 403, [
'X-Robots-Tag' => 'noindex, nofollow, noarchive',
]);
}
// Skip paths (medya, tracking vb. insan trafiği için rate limit muafiyeti)
foreach (self::SKIP_PATHS as $skip) {
if (str_starts_with($path, $skip)) {
return $next($request);
}
}
// Güvenilir IP (Google vb.) — tüm kontrolleri atla
foreach (self::TRUSTED_IP_PREFIXES as $prefix) {
if (str_starts_with($ip, $prefix)) {
return $next($request);
}
}
// Manuel engelli IP kontrolü (Sadece yöneticinin manuel engellediği IP'ler)
if ($this->isBlockedIp($ip)) {
$this->logBot($ip, $request->userAgent(), $path, $request->method(), 'ip_blocked', 'blocked_ip');
return response('Erişim engellendi.', 403);
}
// UA boşsa bot olarak işaretle
if (empty($ua)) {
$request->attributes->set('is_bot', true);
$request->attributes->set('bot_type', 'noua');
$this->logBot($ip, '', $path, $request->method(), 'allowed', 'no_ua');
return $next($request);
}
// Kötü bot mu?
foreach (self::BAD_BOTS as $pattern) {
if (str_contains($ua, $pattern)) {
$this->logBot($ip, $request->userAgent(), $path, $request->method(), 'blocked', $pattern);
return response('', 403);
}
}
// İyi bot mu?
foreach (self::GOOD_BOTS as $pattern) {
if (str_contains($ua, $pattern)) {
$request->attributes->set('is_bot', true);
$request->attributes->set('bot_type', 'good');
// İyi botlar için rate limit (dakikada 120)
if ($this->isRateLimited($ip, 120, 'good_bot')) {
return response('', 429);
}
return $next($request);
}
}
// Generic araç mı?
foreach (self::GENERIC_BOTS as $pattern) {
if (str_contains($ua, $pattern)) {
$request->attributes->set('is_bot', true);
$request->attributes->set('bot_type', 'generic');
if ($this->isRateLimited($ip, 20, 'generic')) {
$this->logBot($ip, $request->userAgent(), $path, $request->method(), 'rate_limited', $pattern);
// 50+ istek → otomatik engelle
$count = Cache::get("bot_count_{$ip}", 0);
if ($count > 50) {
$this->autoBlock($ip, 'Otomatik: dakikada 50+ generic bot isteği');
}
return response('', 429);
}
$this->logBot($ip, $request->userAgent(), $path, $request->method(), 'allowed', $pattern);
return $next($request);
}
}
// Normal kullanıcı — genel sayfa istek limiti (dakikada 300 sayfa isteği)
if ($this->isRateLimited($ip, 300, 'human')) {
$this->logBot($ip, $request->userAgent(), $path, $request->method(), 'rate_limited', 'human_flood');
return response('Çok fazla istek gönderdiniz. Lütfen biraz bekleyin.', 429);
}
$request->attributes->set('is_bot', false);
return $next($request);
}
private function isBlockedIp(string $ip): bool
{
return Cache::remember("blocked_ip_{$ip}", 30, function () use ($ip) {
try {
return DB::table('blocked_ips')
->where('ip', $ip)
->where('auto_blocked', 0)
->where(function ($q) {
$q->whereNull('expires_at')->orWhere('expires_at', '>', now());
})
->exists();
} catch (\Exception) {
return false;
}
});
}
private function isRateLimited(string $ip, int $maxPerMinute, string $type): bool
{
$key = "rl_{$type}_{$ip}";
$count = Cache::get($key, 0);
if ($count === 0) {
Cache::put($key, 1, 60);
} else {
Cache::increment($key);
}
// Bot count ayrı izle
Cache::put("bot_count_{$ip}", Cache::get("bot_count_{$ip}", 0) + 1, 60);
return $count >= $maxPerMinute;
}
private function autoBlock(string $ip, string $reason): void
{
try {
DB::table('blocked_ips')->insertOrIgnore([
'ip' => $ip,
'reason' => $reason,
'auto_blocked' => 1,
'blocked_at' => now(),
'expires_at' => now()->addHours(24),
]);
Cache::forget("blocked_ip_{$ip}");
} catch (\Exception) {}
}
private function logBot(string $ip, ?string $ua, string $path, string $method, string $action, string $botName): void
{
try {
DB::table('analytics_bot_logs')->insert([
'ip' => $ip,
'user_agent' => mb_substr($ua ?? '', 0, 500),
'path' => mb_substr($path, 0, 500),
'method' => $method,
'action' => $action,
'bot_name' => mb_substr($botName, 0, 100),
'created_at' => now(),
]);
} catch (\Exception) {}
}
}