HTTPS restreint aux hotes autorises dans config.json */ // ── Charger la whitelist depuis config.json ──────────────────────────────────── $config_path = __DIR__ . '/config.json'; $allowed_hosts = []; if (file_exists($config_path)) { $config = json_decode(file_get_contents($config_path), true); foreach ($config['epg_sources'] ?? [] as $src) { foreach (['epg_url', 'm3u_url'] as $key) { if (!empty($src[$key])) { $host = parse_url($src[$key], PHP_URL_HOST); if ($host) $allowed_hosts[] = strtolower($host); } } } } // ── Valider l'URL demandee ───────────────────────────────────────────────────── $url = $_GET['url'] ?? ''; if (empty($url) || !preg_match('#^https?://#i', $url)) { http_response_code(400); die('Invalid URL'); } $parsed = parse_url($url); $host = strtolower($parsed['host'] ?? ''); // Bloquer si hote absent de la whitelist if (empty($allowed_hosts) || !in_array($host, $allowed_hosts, true)) { http_response_code(403); die('Host not allowed'); } // Bloquer les IPs privees, loopback, metadata cloud function is_private_host(string $host): bool { // Loopback / localhost if ($host === 'localhost' || $host === '::1') return true; // Metadata AWS/GCP/Azure if ($host === '169.254.169.254' || $host === 'metadata.google.internal') return true; // Resoudre et verifier si IP privee $ip = filter_var($host, FILTER_VALIDATE_IP) ? $host : gethostbyname($host); if (!filter_var($ip, FILTER_VALIDATE_IP)) return true; // echec resolution return !filter_var($ip, FILTER_VALIDATE_IP, [ 'flags' => FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ]); } // Note : on autorise les IPs privees si elles sont explicitement dans config.json // (cas Tunarr/Jellyfin sur le reseau local) — on bloque seulement les hotes // qui ne sont PAS dans la whitelist, ce qui couvre deja le SSRF. // ── Proxy ───────────────────────────────────────────────────────────────────── $base = preg_replace('#[^/]*(\?.*)?$#', '', $url); $origin = $parsed['scheme'] . '://' . $parsed['host']; $port = $parsed['port'] ?? null; if ($port) $origin .= ':' . $port; $path = $parsed['path'] ?? ''; $is_segment = preg_match('#\.(ts|aac|mp4|m4s|fmp4)(\?|$)#i', $path); header('Access-Control-Allow-Origin: *'); header('Cache-Control: no-cache'); $ua = $_SERVER['HTTP_USER_AGENT'] ?? 'Mozilla/5.0'; if ($is_segment) { header('Content-Type: video/MP2T'); header('X-Content-Type-Options: nosniff'); if (ob_get_level()) ob_end_clean(); $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_FOLLOWLOCATION => true, CURLOPT_TIMEOUT => 30, CURLOPT_USERAGENT => $ua, CURLOPT_HTTPHEADER => ['Accept: */*'], CURLOPT_RETURNTRANSFER => false, CURLOPT_WRITEFUNCTION => function($ch, $data) { echo $data; flush(); return strlen($data); }, CURLOPT_HEADERFUNCTION => function($ch, $header) { $h = trim($header); if (preg_match('/^Content-Type:/i', $h)) header($h); return strlen($header); }, ]); $ok = curl_exec($ch); $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); if (!$ok || $code >= 400) http_response_code($code ?: 502); curl_close($ch); } else { $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_FOLLOWLOCATION => true, CURLOPT_TIMEOUT => 15, CURLOPT_USERAGENT => $ua, CURLOPT_HTTPHEADER => ['Accept: */*'], CURLOPT_RETURNTRANSFER => true, ]); $body = curl_exec($ch); $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($body === false || $code >= 400) { http_response_code($code ?: 502); die("Upstream error $code"); } header('Content-Type: application/vnd.apple.mpegurl'); $proxy_base = (isset($_SERVER['HTTPS']) ? 'https' : 'http') . '://' . $_SERVER['HTTP_HOST'] . strtok($_SERVER['REQUEST_URI'], '?') . '?url='; $out = []; foreach (explode("\n", $body) as $line) { $line = rtrim($line); if ($line === '' || $line[0] === '#') { $line = preg_replace_callback('/URI="([^"]+)"/', function($m) use ($base, $origin, $proxy_base) { $seg = strpos($m[1], 'http') === 0 ? $m[1] : ($m[1][0] === '/' ? $origin . $m[1] : $base . $m[1]); return 'URI="' . $proxy_base . urlencode($seg) . '"'; }, $line); $out[] = $line; } else { $seg = strpos($line, 'http') === 0 ? $line : ($line[0] === '/' ? $origin . $line : $base . $line); $out[] = $proxy_base . urlencode($seg); } } echo implode("\n", $out); }