/* eslint-disable no-console,@typescript-eslint/no-explicit-any */ import { NextResponse } from "next/server"; import { getConfig } from "@/lib/config"; import { isValidUrlForProxy } from "@/lib/utils"; 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'); if (!url) { return NextResponse.json({ error: 'Missing url' }, { status: 400 }); } if (!source) { return NextResponse.json({ error: 'Missing source' }, { status: 400 }); } // 定义直链播放模式常量 const DIRECT_PLAY_SOURCE = 'directplay'; // 直链播放模式:跳过源站配置检查,直接代理 if (source !== DIRECT_PLAY_SOURCE) { // 检查该视频源是否启用了代理模式 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 reader: ReadableStreamDefaultReader | null = null; try { const decodedUrl = decodeURIComponent(url); // 安全校验:防 SSRF 拦截请求内网或非法 URL if (source === DIRECT_PLAY_SOURCE && !isValidUrlForProxy(decodedUrl)) { return NextResponse.json({ error: 'Proxy request to local or invalid network is forbidden' }, { status: 403 }); } response = await fetch(decodedUrl, { 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 segment' }, { status: 500 }); } const headers = new Headers(); headers.set('Content-Type', response.headers.get('Content-Type') || 'video/mp2t'); headers.set('Access-Control-Allow-Origin', '*'); headers.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); headers.set('Access-Control-Allow-Headers', 'Content-Type, Range, Origin, Accept'); headers.set('Accept-Ranges', 'bytes'); headers.set('Access-Control-Expose-Headers', 'Content-Length, Content-Range'); const contentLength = response.headers.get('content-length'); if (contentLength) { headers.set('Content-Length', contentLength); } // 使用流式传输,避免占用内存 const stream = new ReadableStream({ start(controller) { if (!response?.body) { controller.close(); return; } reader = response.body.getReader(); const isCancelled = false; function pump() { if (isCancelled || !reader) { return; } reader.read().then(({ done, value }) => { if (isCancelled) { return; } if (done) { controller.close(); cleanup(); return; } controller.enqueue(value); pump(); }).catch((error) => { if (!isCancelled) { controller.error(error); cleanup(); } }); } function cleanup() { if (reader) { try { reader.releaseLock(); } catch (e) { // reader 可能已经被释放,忽略错误 } reader = null; } } pump(); }, cancel() { // 当流被取消时,确保释放所有资源 if (reader) { try { reader.releaseLock(); } catch (e) { // reader 可能已经被释放,忽略错误 } reader = null; } if (response?.body) { try { response.body.cancel(); } catch (e) { // 忽略取消时的错误 } } } }); return new Response(stream, { headers }); } catch (error) { // 确保在错误情况下也释放资源 if (reader) { try { (reader as ReadableStreamDefaultReader).releaseLock(); } catch (e) { // 忽略错误 } } if (response?.body) { try { response.body.cancel(); } catch (e) { // 忽略错误 } } return NextResponse.json({ error: 'Failed to fetch segment' }, { status: 500 }); } }