fix(stream,bot-detector): prevent false-positive 403 bans and memory exhaustion on direct video streams
This commit is contained in:
@@ -61,20 +61,37 @@ class BotDetector
|
|||||||
'postman', 'insomnia', 'httpie',
|
'postman', 'insomnia', 'httpie',
|
||||||
];
|
];
|
||||||
|
|
||||||
// Bu path'ler için sadece log tut, engelleme yapma
|
// Bu path'ler için sadece log tut, engelleme ve genel rate limit yapma
|
||||||
private const SKIP_PATHS = [
|
private const SKIP_PATHS = [
|
||||||
'/up', '/api/', '/sitemap',
|
'/up',
|
||||||
|
'/api/',
|
||||||
|
'/sitemap',
|
||||||
|
'/stream/',
|
||||||
|
'/vtt-proxy',
|
||||||
|
'/track/',
|
||||||
|
'/calls/',
|
||||||
|
'/episode/',
|
||||||
|
'/comments',
|
||||||
|
'/media/',
|
||||||
|
'/assets/',
|
||||||
|
'/build/',
|
||||||
|
'/vendor/',
|
||||||
];
|
];
|
||||||
|
|
||||||
public function handle(Request $request, Closure $next)
|
public function handle(Request $request, Closure $next)
|
||||||
{
|
{
|
||||||
$ip = $request->ip();
|
$ip = $request->ip();
|
||||||
$ua = strtolower($request->userAgent() ?? '');
|
$ua = strtolower($request->userAgent() ?? '');
|
||||||
$path = $request->path();
|
$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);
|
||||||
|
}
|
||||||
|
|
||||||
// Skip paths
|
// Skip paths
|
||||||
foreach (self::SKIP_PATHS as $skip) {
|
foreach (self::SKIP_PATHS as $skip) {
|
||||||
if (str_starts_with('/' . $path, $skip)) {
|
if (str_starts_with($path, $skip)) {
|
||||||
return $next($request);
|
return $next($request);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -86,9 +103,9 @@ class BotDetector
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Manuel engelli IP kontrolü
|
// Manuel engelli IP kontrolü (Sadece yöneticinin manuel engellediği IP'ler)
|
||||||
if ($this->isBlockedIp($ip)) {
|
if ($this->isBlockedIp($ip)) {
|
||||||
$this->logBot($ip, $request->userAgent(), '/' . $path, $request->method(), 'ip_blocked', 'blocked_ip');
|
$this->logBot($ip, $request->userAgent(), $path, $request->method(), 'ip_blocked', 'blocked_ip');
|
||||||
return response('Erişim engellendi.', 403);
|
return response('Erişim engellendi.', 403);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,14 +113,14 @@ class BotDetector
|
|||||||
if (empty($ua)) {
|
if (empty($ua)) {
|
||||||
$request->attributes->set('is_bot', true);
|
$request->attributes->set('is_bot', true);
|
||||||
$request->attributes->set('bot_type', 'noua');
|
$request->attributes->set('bot_type', 'noua');
|
||||||
$this->logBot($ip, '', '/' . $path, $request->method(), 'allowed', 'no_ua');
|
$this->logBot($ip, '', $path, $request->method(), 'allowed', 'no_ua');
|
||||||
return $next($request);
|
return $next($request);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Kötü bot mu?
|
// Kötü bot mu?
|
||||||
foreach (self::BAD_BOTS as $pattern) {
|
foreach (self::BAD_BOTS as $pattern) {
|
||||||
if (str_contains($ua, $pattern)) {
|
if (str_contains($ua, $pattern)) {
|
||||||
$this->logBot($ip, $request->userAgent(), '/' . $path, $request->method(), 'blocked', $pattern);
|
$this->logBot($ip, $request->userAgent(), $path, $request->method(), 'blocked', $pattern);
|
||||||
return response('', 403);
|
return response('', 403);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -113,8 +130,8 @@ class BotDetector
|
|||||||
if (str_contains($ua, $pattern)) {
|
if (str_contains($ua, $pattern)) {
|
||||||
$request->attributes->set('is_bot', true);
|
$request->attributes->set('is_bot', true);
|
||||||
$request->attributes->set('bot_type', 'good');
|
$request->attributes->set('bot_type', 'good');
|
||||||
// İyi botlar için çok agresif rate limit (dakikada 60)
|
// İyi botlar için rate limit (dakikada 120)
|
||||||
if ($this->isRateLimited($ip, 60, 'good_bot')) {
|
if ($this->isRateLimited($ip, 120, 'good_bot')) {
|
||||||
return response('', 429);
|
return response('', 429);
|
||||||
}
|
}
|
||||||
return $next($request);
|
return $next($request);
|
||||||
@@ -126,28 +143,24 @@ class BotDetector
|
|||||||
if (str_contains($ua, $pattern)) {
|
if (str_contains($ua, $pattern)) {
|
||||||
$request->attributes->set('is_bot', true);
|
$request->attributes->set('is_bot', true);
|
||||||
$request->attributes->set('bot_type', 'generic');
|
$request->attributes->set('bot_type', 'generic');
|
||||||
if ($this->isRateLimited($ip, 10, 'generic')) {
|
if ($this->isRateLimited($ip, 20, 'generic')) {
|
||||||
$this->logBot($ip, $request->userAgent(), '/' . $path, $request->method(), 'rate_limited', $pattern);
|
$this->logBot($ip, $request->userAgent(), $path, $request->method(), 'rate_limited', $pattern);
|
||||||
// 30+ istek → otomatik engelle
|
// 50+ istek → otomatik engelle
|
||||||
$count = Cache::get("bot_count_{$ip}", 0);
|
$count = Cache::get("bot_count_{$ip}", 0);
|
||||||
if ($count > 30) {
|
if ($count > 50) {
|
||||||
$this->autoBlock($ip, 'Otomatik: dakikada 30+ generic bot isteği');
|
$this->autoBlock($ip, 'Otomatik: dakikada 50+ generic bot isteği');
|
||||||
}
|
}
|
||||||
return response('', 429);
|
return response('', 429);
|
||||||
}
|
}
|
||||||
$this->logBot($ip, $request->userAgent(), '/' . $path, $request->method(), 'allowed', $pattern);
|
$this->logBot($ip, $request->userAgent(), $path, $request->method(), 'allowed', $pattern);
|
||||||
return $next($request);
|
return $next($request);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normal kullanıcı — genel rate limit (dakikada 120 istek)
|
// Normal kullanıcı — genel sayfa istek limiti (dakikada 300 sayfa isteği)
|
||||||
if ($this->isRateLimited($ip, 120, 'human')) {
|
if ($this->isRateLimited($ip, 300, 'human')) {
|
||||||
$this->logBot($ip, $request->userAgent(), '/' . $path, $request->method(), 'rate_limited', 'human_flood');
|
$this->logBot($ip, $request->userAgent(), $path, $request->method(), 'rate_limited', 'human_flood');
|
||||||
$count = Cache::get("bot_count_{$ip}", 0);
|
return response('Çok fazla istek gönderdiniz. Lütfen biraz bekleyin.', 429);
|
||||||
if ($count > 200) {
|
|
||||||
$this->autoBlock($ip, 'Otomatik: dakikada 200+ istek flood');
|
|
||||||
}
|
|
||||||
return response('', 429);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$request->attributes->set('is_bot', false);
|
$request->attributes->set('is_bot', false);
|
||||||
@@ -156,10 +169,11 @@ class BotDetector
|
|||||||
|
|
||||||
private function isBlockedIp(string $ip): bool
|
private function isBlockedIp(string $ip): bool
|
||||||
{
|
{
|
||||||
return Cache::remember("blocked_ip_{$ip}", 300, function () use ($ip) {
|
return Cache::remember("blocked_ip_{$ip}", 30, function () use ($ip) {
|
||||||
try {
|
try {
|
||||||
return DB::table('blocked_ips')
|
return DB::table('blocked_ips')
|
||||||
->where('ip', $ip)
|
->where('ip', $ip)
|
||||||
|
->where('auto_blocked', 0)
|
||||||
->where(function ($q) {
|
->where(function ($q) {
|
||||||
$q->whereNull('expires_at')->orWhere('expires_at', '>', now());
|
$q->whereNull('expires_at')->orWhere('expires_at', '>', now());
|
||||||
})
|
})
|
||||||
@@ -216,3 +230,4 @@ class BotDetector
|
|||||||
} catch (\Exception) {}
|
} catch (\Exception) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+46
-3
@@ -510,6 +510,52 @@ Route::get("/stream/proxy", function (\Illuminate\Http\Request $request) {
|
|||||||
$customRef = base64_encode($referer);
|
$customRef = base64_encode($referer);
|
||||||
}
|
}
|
||||||
$origin = rtrim($referer, '/');
|
$origin = rtrim($referer, '/');
|
||||||
|
$path = strtolower(parse_url($url, PHP_URL_PATH) ?? "");
|
||||||
|
$UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36";
|
||||||
|
|
||||||
|
// Doğrudan MP4 / video dosyası istendiyse belleğe yüklemeden stream et (PHP memory_limit çökmesini önler)
|
||||||
|
$isDirectMedia = str_ends_with($path, ".mp4")
|
||||||
|
|| str_ends_with($path, ".m4v")
|
||||||
|
|| str_ends_with($path, ".webm")
|
||||||
|
|| str_ends_with($path, ".mkv");
|
||||||
|
|
||||||
|
if ($isDirectMedia) {
|
||||||
|
$streamHeaders = [
|
||||||
|
"Referer: {$referer}",
|
||||||
|
"User-Agent: {$UA}",
|
||||||
|
"Accept: */*",
|
||||||
|
];
|
||||||
|
if ($range = $request->header('Range')) {
|
||||||
|
$streamHeaders[] = "Range: {$range}";
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->stream(function () use ($url, $streamHeaders) {
|
||||||
|
$ch = curl_init($url);
|
||||||
|
curl_setopt_array($ch, [
|
||||||
|
CURLOPT_FOLLOWLOCATION => true,
|
||||||
|
CURLOPT_MAXREDIRS => 5,
|
||||||
|
CURLOPT_TIMEOUT => 300,
|
||||||
|
CURLOPT_CONNECTTIMEOUT => 10,
|
||||||
|
CURLOPT_SSL_VERIFYPEER => false,
|
||||||
|
CURLOPT_SSL_VERIFYHOST => false,
|
||||||
|
CURLOPT_HTTPHEADER => $streamHeaders,
|
||||||
|
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) {
|
||||||
|
echo $chunk;
|
||||||
|
if (ob_get_level() > 0) {
|
||||||
|
ob_flush();
|
||||||
|
}
|
||||||
|
flush();
|
||||||
|
return strlen($chunk);
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
curl_exec($ch);
|
||||||
|
curl_close($ch);
|
||||||
|
}, 200, [
|
||||||
|
"Content-Type" => "video/mp4",
|
||||||
|
"Access-Control-Allow-Origin" => "*",
|
||||||
|
"Cache-Control" => "public, max-age=86400",
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
// Tek istek yürüten yardımcı — farklı header setleriyle tekrar denemek için.
|
// Tek istek yürüten yardımcı — farklı header setleriyle tekrar denemek için.
|
||||||
// Redirect'ler elle takip edilir ki her adım SSRF kontrolünden geçsin.
|
// Redirect'ler elle takip edilir ki her adım SSRF kontrolünden geçsin.
|
||||||
@@ -546,8 +592,6 @@ Route::get("/stream/proxy", function (\Illuminate\Http\Request $request) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
$UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36";
|
|
||||||
|
|
||||||
// 1) Referer-only (gerçek oynatıcı davranışı — Origin göndermez, hotlink koruması daha toleranslı)
|
// 1) Referer-only (gerçek oynatıcı davranışı — Origin göndermez, hotlink koruması daha toleranslı)
|
||||||
[$content, $httpCode, $contentType, $curlErrStr] = $fetchUpstream($url, [
|
[$content, $httpCode, $contentType, $curlErrStr] = $fetchUpstream($url, [
|
||||||
"Referer: {$referer}",
|
"Referer: {$referer}",
|
||||||
@@ -578,7 +622,6 @@ Route::get("/stream/proxy", function (\Illuminate\Http\Request $request) {
|
|||||||
abort(502);
|
abort(502);
|
||||||
}
|
}
|
||||||
|
|
||||||
$path = strtolower(parse_url($url, PHP_URL_PATH) ?? "");
|
|
||||||
$isM3u8 = str_ends_with($path, ".m3u8")
|
$isM3u8 = str_ends_with($path, ".m3u8")
|
||||||
|| str_contains($contentType, "mpegurl")
|
|| str_contains($contentType, "mpegurl")
|
||||||
|| str_starts_with(trim((string)$content), "#EXTM3U");
|
|| str_starts_with(trim((string)$content), "#EXTM3U");
|
||||||
|
|||||||
Reference in New Issue
Block a user