🎯 Server-Side Preview Implementation

📅 Tarih: 2025-12-04 🎯 Tenant: muzibu.com (1001) 🔒 Güvenlik: Server-Side Chunk Kontrolü 👤 Hedef: 30 saniye preview manipülasyonunu engelle

📊 Mevcut Sistem vs Yeni Sistem

❌ MEVCUT: Client-Side Kontrol

  • JavaScript 30 saniye sonra durdurur
  • Tüm chunk'lar playlist'te listelenir
  • Client chunk URL'lerini görür
  • JavaScript manipüle edilebilir
  • DevTools ile chunk'lar indirilebilir

✅ YENİ: Server-Side Kontrol

  • Server sadece izin verilen chunk'ları listeler
  • Dynamic M3U8 generation (user tipine göre)
  • Her chunk isteği sunucuda kontrol edilir
  • JavaScript manipülasyonu işe yaramaz
  • Chunk URL'leri signed token içerir

🔄 Nasıl Çalışır?

1
User Şarkı İster

Frontend GET /api/muzibu/songs/{id}/stream isteği yapar

2
Server User Tipini Kontrol Eder

Guest? Normal User? Premium? → Bu bilgiye göre chunk limiti belirlenir

3
Dynamic M3U8 Playlist Oluşturulur

Guest/Normal → İlk 3 chunk (30 saniye)
Premium → Tüm chunk'lar

4
Chunk URL'leri Signed Token İle Oluşturulur

Her chunk URL'inde user_id, chunk_index, expiry bilgisi olur

5
Client Chunk İsteği Gönderir

HLS player playlist'teki chunk URL'lerini sırayla ister

6
Server Chunk İsteğini Doğrular

✓ Token geçerli mi?
✓ User bu chunk'ı isteyebilir mi?
✓ Chunk index izin verilen sınırda mı?

7
Chunk Serve Edilir veya 403 Döner

✅ İzin varsa → Chunk dosyası serve edilir
❌ İzin yoksa → 403 Forbidden

🛠️ Implementation Adımları

Adım 1: Dynamic M3U8 Generator Service Oluştur

Orijinal playlist.m3u8 dosyasını okuyup, user tipine göre modify eden bir servis.

📁 app/Services/Muzibu/ └── PlaylistGeneratorService.php (YENİ)
// PlaylistGeneratorService.php public function generatePlaylist($song, $user) { // 1. Orijinal playlist.m3u8'i oku $originalPlaylist = file_get_contents($song->hls_path); // 2. User tipine göre chunk limiti belirle $chunkLimit = $this->getChunkLimit($user); // 3. Playlist'i parse et + chunk sayısını sınırla $modifiedPlaylist = $this->limitChunks( $originalPlaylist, $chunkLimit ); // 4. Chunk URL'lerini signed token ile değiştir $signedPlaylist = $this->signChunkUrls( $modifiedPlaylist, $song->song_id, $user ); return $signedPlaylist; } private function getChunkLimit($user) { // Guest veya Normal User if (!$user || !$user->isPremium()) { return 3; // 3 chunk = 30 saniye (10 saniye/chunk) } // Premium User return null; // Sınırsız (tüm chunk'lar) } private function limitChunks($playlist, $limit) { // M3U8 format'ını parse et $lines = explode("\n", $playlist); $output = []; $chunkCount = 0; foreach ($lines as $line) { // #EXTINF satırı → chunk başlangıcı if (str_starts_with($line, '#EXTINF')) { if ($limit && $chunkCount >= $limit) { break; // Limit aşıldı, dur! } $chunkCount++; } $output[] = $line; } // #EXT-X-ENDLIST ekle (playlist sonu) $output[] = '#EXT-X-ENDLIST'; return implode("\n", $output); } private function signChunkUrls($playlist, $songId, $user) { // segment-000.ts → /api/muzibu/songs/{id}/chunk/segment-000.ts?token=xxx return preg_replace_callback( '/(segment-\d+\.ts)/', function($matches) use ($songId, $user) { $chunkName = $matches[1]; $token = $this->generateChunkToken($songId, $chunkName, $user); return "/api/muzibu/songs/{$songId}/chunk/{$chunkName}?token={$token}"; }, $playlist ); }

Adım 2: Chunk Serve Endpoint + Route Ekle

Her chunk isteğinde token doğrulama ve yetki kontrolü yapan yeni endpoint.

// routes/api.php (Muzibu) Route::get('{id}/chunk/{chunkName}', [SongController::class, 'serveChunk']) ->name('api.muzibu.songs.serve-chunk') ->middleware(['throttle.user:stream']); // Rate limiting
// SongController.php public function serveChunk($id, $chunkName, Request $request) { // 1. Token doğrula $token = $request->input('token'); $tokenData = $this->validateChunkToken($token); if (!$tokenData) { abort(403, 'Invalid or expired token'); } // 2. Token song_id'si ile URL song_id'si eşleşiyor mu? if ($tokenData['song_id'] != $id) { abort(403, 'Token mismatch'); } // 3. Chunk name eşleşiyor mu? if ($tokenData['chunk_name'] != $chunkName) { abort(403, 'Chunk mismatch'); } // 4. User bu chunk'ı isteyebilir mi? (Chunk index kontrolü) $user = auth()->user(); $chunkIndex = $this->extractChunkIndex($chunkName); // segment-005.ts → 5 $maxChunkIndex = $this->getMaxChunkIndex($user); // Guest/Normal: 2 (0,1,2 = 3 chunk) if ($maxChunkIndex !== null && $chunkIndex > $maxChunkIndex) { Log::warning('Chunk access denied', [ 'user_id' => $user?->id, 'song_id' => $id, 'chunk_index' => $chunkIndex, 'max_allowed' => $maxChunkIndex ]); abort(403, 'Preview limit exceeded'); } // 5. Chunk dosyasını serve et $song = Song::findOrFail($id); $chunkPath = storage_path( 'app/public/muzibu/hls/' . $song->song_id . '/'.span> . $chunkName ); if (!file_exists($chunkPath)) { abort(404, 'Chunk not found'); } return response()->file($chunkPath, [ 'Content-Type' => 'video/mp2t', // MPEG-TS format 'Cache-Control' => 'private, max-age=3600', ]); } private function getMaxChunkIndex($user) { // Guest veya Normal User if (!$user || !$user->isPremium()) { return 2; // Index 0, 1, 2 → 3 chunk } // Premium User return null; // Sınırsız }

Adım 3: SongStreamController'ı Güncelle

Mevcut stream() method'unda, HLS serve ederken dynamic playlist kullan.

// SongStreamController.php - stream() method // Eski kod (statik playlist): $streamUrl = $this->signedUrlService->generateHlsUrl($songId, 60); // Yeni kod (dynamic playlist): $streamUrl = route('api.muzibu.songs.dynamic-playlist', [ 'id' => $songId, 'token' => $this->generatePlaylistToken($songId, $user) ]);
// Yeni route ekle: Route::get('{id}/playlist', [SongController::class, 'dynamicPlaylist']) ->name('api.muzibu.songs.dynamic-playlist');
// SongController.php - dynamicPlaylist() method public function dynamicPlaylist($id, Request $request) { // 1. Token doğrula $token = $request->input('token'); // ... token validation ... // 2. Song'u al $song = Song::findOrFail($id); // 3. Dynamic playlist oluştur $playlistGenerator = app(PlaylistGeneratorService::class); $playlist = $playlistGenerator->generatePlaylist( $song, auth()->user() ); // 4. M3U8 response döndür return response($playlist, 200, [ 'Content-Type' => 'application/vnd.apple.mpegurl', 'Cache-Control' => 'no-cache, no-store, must-revalidate', ]); }

Adım 4: Chunk Token Generation + Validation

Her chunk için unique token oluştur ve doğrula.

// SignedUrlService.php (veya yeni ChunkTokenService.php) public function generateChunkToken( int $songId, string $chunkName, $user, int $expiryMinutes = 10 ): string { $payload = [ 'song_id' => $songId, 'chunk_name' => $chunkName, 'user_id' => $user?->id, 'expires_at' => now()->addMinutes($expiryMinutes)->timestamp, ]; // HMAC-SHA256 ile imzala $signature = hash_hmac( 'sha256', json_encode($payload), config('app.key') ); return base64_encode( json_encode($payload) . '|' . $signature ); } public function validateChunkToken(string $token): ?array { // 1. Token decode $decoded = base64_decode($token); [$payloadJson, $signature] = explode('|', $decoded, 2); // 2. İmza doğrulama $expectedSignature = hash_hmac( 'sha256', $payloadJson, config('app.key') ); if (!hash_equals($expectedSignature, $signature)) { return null; // Invalid signature } // 3. Payload parse $payload = json_decode($payloadJson, true); // 4. Expiry check if ($payload['expires_at'] < time()) { return null; // Expired } return $payload; }

📄 Örnek M3U8 Playlist Farkı

Guest/Normal User İçin (İlk 3 Chunk)

#EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:10 #EXT-X-MEDIA-SEQUENCE:0 # Chunk 0 (0-10 saniye) #EXTINF:10.0, /api/muzibu/songs/123/chunk/segment-000.ts?token=eyJ... # Chunk 1 (10-20 saniye) #EXTINF:10.0, /api/muzibu/songs/123/chunk/segment-001.ts?token=eyJ... # Chunk 2 (20-30 saniye) #EXTINF:10.0, /api/muzibu/songs/123/chunk/segment-002.ts?token=eyJ... # Playlist sonu (30 saniye) #EXT-X-ENDLIST

Premium User İçin (Tüm Chunk'lar)

#EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:10 #EXT-X-MEDIA-SEQUENCE:0 # Chunk 0-30 (0-300 saniye = 5 dakikalık şarkı örneği) #EXTINF:10.0, /api/muzibu/songs/123/chunk/segment-000.ts?token=eyJ... #EXTINF:10.0, /api/muzibu/songs/123/chunk/segment-001.ts?token=eyJ... ... (28 chunk daha) #EXTINF:10.0, /api/muzibu/songs/123/chunk/segment-030.ts?token=eyJ... #EXT-X-ENDLIST

🧪 Güvenlik Test Senaryoları

Test Senaryosu Beklenen Sonuç Durum
Guest user 4. chunk'ı isterse 403 Forbidden Engellenir
JavaScript ile 30 saniye limiti kaldırılırsa Server yine 3 chunk döner Etkisiz
Token expire olduktan sonra chunk isterse 403 Forbidden (Token expired) Engellenir
Chunk token'ı farklı şarkı için kullanılırsa 403 Forbidden (Song ID mismatch) Engellenir
DevTools ile chunk URL'lerini kopyalarsa Token 10 dakika sonra geçersiz Geçici risk
Premium user'a downgrade olursa Yeni istek 3 chunk ile sınırlanır Dinamik kontrol

⚡ Performans Notları

1. Playlist Cache

Dynamic playlist generation her istekte yapılmamalı. User tipine göre cache'le:

// Cache key format $cacheKey = "playlist:{$songId}:user_type:{$userType}"; // Cache TTL: 10 dakika (token expiry ile aynı) Cache::remember($cacheKey, 600, function() { return $playlistGenerator->generatePlaylist($song, $user); });

2. Rate Limiting

Chunk endpoint'ine sıkı rate limiting uygula:

  • Guest: 30 chunk/dakika (maksimum 3 şarkı)
  • Normal User: 120 chunk/dakika (maksimum 12 şarkı)
  • Premium: 300 chunk/dakika (sınırsız)

3. CDN Optimization

Chunk dosyaları CDN'e cache'lenebilir ama URL'lerde token var, bu CDN'yi bypass eder.
Çözüm: Token'ı query string yerine custom header'da gönder.

🛡️ Ek Güvenlik Katmanları (Opsiyonel)

1. IP-Based Rate Limiting

Aynı IP'den çok fazla chunk isteği gelirse (örneğin bot), IP'yi geçici olarak engelle.

2. User Agent Validation

Chunk isteklerinde User Agent kontrolü yap, şüpheli bot trafiğini filtrele.

3. Referer Check

Chunk istekleri sadece kendi domain'den gelmeli, hotlinking engellenir.

4. Fingerprint Tracking

Her chunk isteğinde user fingerprint'i (IP + User Agent + Device ID) logla, anormal aktivite tespiti için analiz et.

5. Dynamic Chunk Duration

Guest user için chunk süresini 10 saniye yerine 5 saniye yap (daha hassas kontrol). Bu durumda 30 saniye = 6 chunk olur.

🎉 Beklenen Sonuçlar

✅ Server-Side Preview Uygulandıktan Sonra

1. JavaScript Manipülasyonu İşe Yaramaz: Client-side kod değişse bile, server sadece izin verilen chunk'ları serve eder.

2. DevTools ile İndirme Engellenir: Chunk URL'lerinde token var, 10 dakika sonra geçersiz olur.

3. URL Paylaşımı İşe Yaramaz: Token user_id ve chunk_name içerir, başkası kullanamaz.

4. Hassas Kontrol: Chunk index ile saniye-seviye kontrol (10 saniye/chunk).

5. Dinamik User Tipine Göre Servis: Premium'a yükseltme anında etkili olur.

⏱️ Implementation Süresi Tahmini

Adım Süre Zorluk
1. PlaylistGeneratorService 2-3 saat Orta
2. Chunk Serve Endpoint + Route 1-2 saat Kolay
3. SongStreamController Güncelleme 1 saat Kolay
4. Chunk Token System 2 saat Orta
5. Test + Debug 2-3 saat Orta
TOPLAM 8-11 saat 1-1.5 gün