Files
MoonTVPlus/src/app/api/proxy/vod/key/route.ts
Troray 3e9fdbb7a0 fix: 强化 SSRF 防护、修复代理链路安全漏洞与代码质量问题
基于 Gemini Code Assist 审查建议,对代理链路进行全面安全加固与代码优化。

- [新增] `src/lib/server/ssrf.ts`: 使用 `dns.promises.lookup` 进行真实 IP 解析,
  替代原有的正则匹配,防御 DNS 重绑定、非十进制 IP 等绕过手段
- [新增] 为 `proxy/vod/m3u8`、`proxy/vod/key`、`video-proxy` 三个接口补齐 SSRF 校验,
  此前仅 `proxy-m3u8` 和 `proxy/vod/segment` 有防护
- [删除] `utils.ts` 中已弃用的 `isValidUrlForProxy` 函数

- 所有代理接口统一强制 SSRF 校验,不再仅限于 `source=directplay`

- 修复 `proxy/vod/segment` 中 `isCancelled` 为 `const` 导致流取消信号失效的问题
- 修复 `proxy-m3u8/route.ts` 中导入不存在的函数(`extractResolutionFromM3u8`, `filterAdsFromM3U8Default`, `resolveM3u8Links`)的构建错误
- 修复直链直连模式下 `fetchCurrentSourceVideoInfo` 使用 HLS.js (XHR) 探测视频分辨率
  触发 CORS 误报的问题,改为直接跳过探测
- 移除 `proxy/vod/segment` 中未使用的 `NextRequest` 导入

- [新增] `src/lib/server/proxy-headers.ts`: 抽取 CORS 响应头为共享工具函数,
  消除 `proxy/vod/segment`、`proxy/vod/key`、`proxy/vod/m3u8` 中重复代码
- 统一使用 `DIRECT_PLAY_SOURCE` 常量替代硬编码 `'directplay'` 字符串

- `src/lib/server/ssrf.ts`
- `src/lib/server/proxy-headers.ts`

- `/app/api/proxy-m3u8/route`
- `/app/api/proxy/vod/key/route`
- `/app/api/proxy/vod/m3u8/route`
- `/app/api/proxy/vod/segment/route`
- `/app/api/video-proxy/route`
- `/app/play/page`x
- `/lib/utils`
2026-03-11 21:11:24 +08:00

65 lines
2.0 KiB
TypeScript

/* eslint-disable no-console,@typescript-eslint/no-explicit-any */
import { NextResponse } from "next/server";
import { getConfig } from "@/lib/config";
import { validateProxyUrlServerSide } from '@/lib/server/ssrf';
import { 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');
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 });
}
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 });
}
const 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 key' }, { status: 500 });
}
const headers = buildProxyStreamHeaders(
response.headers.get('Content-Type') || 'application/octet-stream'
);
return new Response(response.body, { headers });
} catch (error) {
return NextResponse.json({ error: 'Failed to fetch key' }, { status: 500 });
}
}