/* eslint-disable no-console,@typescript-eslint/no-explicit-any */ import { NextResponse } from "next/server"; import { getConfig } from "@/lib/config"; import { getBaseUrl, resolveUrl } from "@/lib/live"; import { validateProxyUrlServerSide } from '@/lib/server/ssrf'; import { buildProxyM3u8Headers, buildProxyStreamHeaders } from '@/lib/server/proxy-headers'; export const runtime = 'nodejs'; export async function GET(request: Request) { const { searchParams } = new URL(request.url); const url = searchParams.get('url'); const source = searchParams.get('source'); // 视频源key if (!url) { return NextResponse.json({ error: 'Missing url' }, { status: 400 }); } if (!source) { return NextResponse.json({ error: 'Missing source' }, { status: 400 }); } // 检查该视频源是否启用了代理模式 const config = await getConfig(); const videoSource = config.SourceConfig?.find((s: any) => s.key === source); if (!videoSource) { return NextResponse.json({ error: 'Source not found' }, { status: 404 }); } if (!videoSource.proxyMode) { return NextResponse.json({ error: 'Proxy mode not enabled for this source' }, { status: 403 }); } let response: Response | null = null; let responseUsed = false; try { const decodedUrl = decodeURIComponent(url); // 安全校验:防 SSRF 拦截请求内网或非法 URL const isSafeUrl = await validateProxyUrlServerSide(decodedUrl); if (!isSafeUrl) { return NextResponse.json({ error: 'Proxy request to local or invalid network is forbidden' }, { status: 403 }); } response = await fetch(decodedUrl, { cache: 'no-cache', redirect: 'follow', credentials: 'same-origin', headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', 'Referer': decodedUrl, }, }); if (!response.ok) { return NextResponse.json({ error: 'Failed to fetch m3u8' }, { status: 500 }); } const contentType = response.headers.get('Content-Type') || ''; // rewrite m3u8 if (contentType.toLowerCase().includes('mpegurl') || contentType.toLowerCase().includes('octet-stream') || decodedUrl.includes('.m3u8')) { // 获取最终的响应URL(处理重定向后的URL) const finalUrl = response.url; const m3u8Content = await response.text(); responseUsed = true; // 标记 response 已被使用 // 使用最终的响应URL作为baseUrl,而不是原始的请求URL const baseUrl = getBaseUrl(finalUrl); // 重写 M3U8 内容 const modifiedContent = rewriteM3U8Content(m3u8Content, baseUrl, request, source); const headers = buildProxyM3u8Headers(contentType || undefined); return new Response(modifiedContent, { headers }); } // just proxy const headers = buildProxyStreamHeaders( response.headers.get('Content-Type') || 'application/vnd.apple.mpegurl' ); headers.set('Cache-Control', 'no-cache'); // 直接返回视频流 return new Response(response.body, { status: 200, headers, }); } catch (error) { return NextResponse.json({ error: 'Failed to fetch m3u8' }, { status: 500 }); } finally { // 确保 response 被正确关闭以释放资源 if (response && !responseUsed) { try { response.body?.cancel(); } catch (error) { // 忽略关闭时的错误 console.warn('Failed to close response body:', error); } } } } function rewriteM3U8Content(content: string, baseUrl: string, req: Request, source: string) { // 从 referer 头提取协议信息 const referer = req.headers.get('referer'); let protocol = 'http'; if (referer) { try { const refererUrl = new URL(referer); protocol = refererUrl.protocol.replace(':', ''); } catch (error) { // ignore } } const host = req.headers.get('host'); const proxyBase = `${protocol}://${host}/api/proxy/vod`; const lines = content.split('\n'); const rewrittenLines: string[] = []; for (let i = 0; i < lines.length; i++) { let line = lines[i].trim(); // 处理 TS 片段 URL 和其他媒体文件 if (line && !line.startsWith('#')) { const resolvedUrl = resolveUrl(baseUrl, line); const proxyUrl = `${proxyBase}/segment?url=${encodeURIComponent(resolvedUrl)}&source=${source}`; rewrittenLines.push(proxyUrl); continue; } // 处理 EXT-X-MAP 标签中的 URI if (line.startsWith('#EXT-X-MAP:')) { line = rewriteMapUri(line, baseUrl, proxyBase, source); } // 处理 EXT-X-KEY 标签中的 URI if (line.startsWith('#EXT-X-KEY:')) { line = rewriteKeyUri(line, baseUrl, proxyBase, source); } // 处理嵌套的 M3U8 文件 (EXT-X-STREAM-INF) if (line.startsWith('#EXT-X-STREAM-INF:')) { rewrittenLines.push(line); // 下一行通常是 M3U8 URL if (i + 1 < lines.length) { i++; const nextLine = lines[i].trim(); if (nextLine && !nextLine.startsWith('#')) { const resolvedUrl = resolveUrl(baseUrl, nextLine); const proxyUrl = `${proxyBase}/m3u8?url=${encodeURIComponent(resolvedUrl)}&source=${source}`; rewrittenLines.push(proxyUrl); } else { rewrittenLines.push(nextLine); } } continue; } rewrittenLines.push(line); } return rewrittenLines.join('\n'); } function rewriteMapUri(line: string, baseUrl: string, proxyBase: string, source: string) { const uriMatch = line.match(/URI="([^"]+)"/); if (uriMatch) { const originalUri = uriMatch[1]; const resolvedUrl = resolveUrl(baseUrl, originalUri); const proxyUrl = `${proxyBase}/segment?url=${encodeURIComponent(resolvedUrl)}&source=${source}`; return line.replace(uriMatch[0], `URI="${proxyUrl}"`); } return line; } function rewriteKeyUri(line: string, baseUrl: string, proxyBase: string, source: string) { const uriMatch = line.match(/URI="([^"]+)"/); if (uriMatch) { const originalUri = uriMatch[1]; const resolvedUrl = resolveUrl(baseUrl, originalUri); const proxyUrl = `${proxyBase}/key?url=${encodeURIComponent(resolvedUrl)}&source=${source}`; return line.replace(uriMatch[0], `URI="${proxyUrl}"`); } return line; }