Files
MoonTVPlus/src/app/api/proxy/vod/segment/route.ts
T

167 lines
4.6 KiB
TypeScript

/* 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<Uint8Array> | 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<Uint8Array>).releaseLock();
} catch (e) {
// 忽略错误
}
}
if (response?.body) {
try {
response.body.cancel();
} catch (e) {
// 忽略错误
}
}
return NextResponse.json({ error: 'Failed to fetch segment' }, { status: 500 });
}
}