基于 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`
99 lines
2.9 KiB
TypeScript
99 lines
2.9 KiB
TypeScript
import { NextResponse } from 'next/server';
|
||
import { validateProxyUrlServerSide } from '@/lib/server/ssrf';
|
||
|
||
export const runtime = 'nodejs';
|
||
|
||
// 视频代理接口,支持Range请求
|
||
export async function GET(request: Request) {
|
||
const { searchParams } = new URL(request.url);
|
||
const videoUrl = searchParams.get('url');
|
||
|
||
if (!videoUrl) {
|
||
return NextResponse.json({ error: 'Missing video URL' }, { status: 400 });
|
||
}
|
||
|
||
// 安全校验:防 SSRF,只允许合法的公网 URL
|
||
const isSafeUrl = await validateProxyUrlServerSide(videoUrl);
|
||
if (!isSafeUrl) {
|
||
return NextResponse.json({ error: 'Proxy request to local or invalid network is forbidden' }, { status: 403 });
|
||
}
|
||
|
||
try {
|
||
// 获取客户端的Range请求头
|
||
const range = request.headers.get('range');
|
||
|
||
const fetchHeaders: HeadersInit = {
|
||
'User-Agent':
|
||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36',
|
||
Accept: 'video/mp4,video/*;q=0.9,*/*;q=0.8',
|
||
Referer: 'https://movie.douban.com/',
|
||
};
|
||
|
||
// 如果客户端发送了Range请求,转发给源服务器
|
||
if (range) {
|
||
fetchHeaders['Range'] = range;
|
||
}
|
||
|
||
const videoResponse = await fetch(videoUrl, {
|
||
headers: fetchHeaders,
|
||
});
|
||
|
||
if (!videoResponse.ok) {
|
||
return NextResponse.json(
|
||
{ error: videoResponse.statusText },
|
||
{ status: videoResponse.status }
|
||
);
|
||
}
|
||
|
||
if (!videoResponse.body) {
|
||
return NextResponse.json(
|
||
{ error: 'Video response has no body' },
|
||
{ status: 500 }
|
||
);
|
||
}
|
||
|
||
// 创建响应头
|
||
const headers = new Headers();
|
||
|
||
// 复制重要的响应头
|
||
const contentType = videoResponse.headers.get('content-type');
|
||
if (contentType) {
|
||
headers.set('Content-Type', contentType);
|
||
}
|
||
|
||
const contentLength = videoResponse.headers.get('content-length');
|
||
if (contentLength) {
|
||
headers.set('Content-Length', contentLength);
|
||
}
|
||
|
||
const contentRange = videoResponse.headers.get('content-range');
|
||
if (contentRange) {
|
||
headers.set('Content-Range', contentRange);
|
||
}
|
||
|
||
const acceptRanges = videoResponse.headers.get('accept-ranges');
|
||
if (acceptRanges) {
|
||
headers.set('Accept-Ranges', acceptRanges);
|
||
}
|
||
|
||
// 设置缓存头
|
||
headers.set('Cache-Control', 'public, max-age=31536000, s-maxage=31536000'); // 缓存1年
|
||
headers.set('CDN-Cache-Control', 'public, s-maxage=31536000');
|
||
headers.set('Vercel-CDN-Cache-Control', 'public, s-maxage=31536000');
|
||
|
||
// 返回视频流,状态码根据是否有Range请求决定
|
||
const status = range && contentRange ? 206 : 200;
|
||
|
||
return new Response(videoResponse.body, {
|
||
status,
|
||
headers,
|
||
});
|
||
} catch (error) {
|
||
console.error('Error proxying video:', error);
|
||
return NextResponse.json(
|
||
{ error: 'Error fetching video' },
|
||
{ status: 500 }
|
||
);
|
||
}
|
||
}
|