修复SSRF/Host Header注入与Null报错的问题

This commit is contained in:
Troray
2026-03-10 20:58:56 +08:00
parent b4320b7028
commit 802bf80bef
3 changed files with 134 additions and 61 deletions

View File

@@ -155,7 +155,7 @@ export function processVideoUrl(originalUrl: string): string {
*/
export async function getVideoResolutionFromM3u8(
m3u8Url: string,
timeoutMs = 4000
timeoutMs = 6000
): Promise<{
quality: string; // 如720p、1080p等
loadSpeed: string; // 自动转换为KB/s或MB/s
@@ -185,11 +185,55 @@ export async function getVideoResolutionFromM3u8(
// 固定使用hls.js加载
const hls = new Hls();
// 设置超时处理 - 使用传入的超时时间
const timeout = setTimeout(() => {
let actualLoadSpeed = '未知';
let hasSpeedCalculated = false;
let hasMetadataLoaded = false;
let estimatedBitrate = 0; // 估算的码率bps
// 提取核心返回逻辑供 resolve 和 timeout 共同调用
const resolveCurrentState = () => {
const width = video.videoWidth;
const quality =
width >= 3840
? '4K'
: width >= 2560
? '2K'
: width >= 1920
? '1080p'
: width >= 1280
? '720p'
: width >= 854
? '480p'
: width > 0
? 'SD'
: '未知';
const bitrateStr = estimatedBitrate > 0
? estimatedBitrate >= 1000000
? `${(estimatedBitrate / 1000000).toFixed(1)} Mbps`
: `${Math.round(estimatedBitrate / 1000)} Kbps`
: '未知';
hls.destroy();
video.remove();
reject(new Error('Timeout loading video metadata'));
resolve({
quality,
loadSpeed: actualLoadSpeed,
pingTime: Math.round(pingTime),
bitrate: bitrateStr,
});
};
// 设置超时处理 - 如果部分数据已拿到,则宽容返回
const timeout = setTimeout(() => {
if (hasMetadataLoaded || hasSpeedCalculated) {
resolveCurrentState();
} else {
hls.destroy();
video.remove();
reject(new Error('Timeout loading video metadata'));
}
}, timeoutMs);
video.onerror = () => {
@@ -199,67 +243,16 @@ export async function getVideoResolutionFromM3u8(
reject(new Error('Failed to load video metadata'));
};
let actualLoadSpeed = '未知';
let hasSpeedCalculated = false;
let hasMetadataLoaded = false;
let estimatedBitrate = 0; // 估算的码率bps
let fragmentStartTime = 0;
// 检查是否可以返回结果
// 检查是否可以相互满足要求
const checkAndResolve = () => {
if (
hasMetadataLoaded &&
(hasSpeedCalculated || actualLoadSpeed !== '未知')
) {
clearTimeout(timeout);
const width = video.videoWidth;
if (width && width > 0) {
hls.destroy();
video.remove();
// 根据视频宽度判断视频质量等级,使用经典分辨率的宽度作为分割点
const quality =
width >= 3840
? '4K' // 4K: 3840x2160
: width >= 2560
? '2K' // 2K: 2560x1440
: width >= 1920
? '1080p' // 1080p: 1920x1080
: width >= 1280
? '720p' // 720p: 1280x720
: width >= 854
? '480p'
: 'SD'; // 480p: 854x480
// 格式化码率
const bitrateStr = estimatedBitrate > 0
? estimatedBitrate >= 1000000
? `${(estimatedBitrate / 1000000).toFixed(1)} Mbps`
: `${Math.round(estimatedBitrate / 1000)} Kbps`
: '未知';
resolve({
quality,
loadSpeed: actualLoadSpeed,
pingTime: Math.round(pingTime),
bitrate: bitrateStr,
});
} else {
// webkit 无法获取尺寸,直接返回
const bitrateStr = estimatedBitrate > 0
? estimatedBitrate >= 1000000
? `${(estimatedBitrate / 1000000).toFixed(1)} Mbps`
: `${Math.round(estimatedBitrate / 1000)} Kbps`
: '未知';
resolve({
quality: '未知',
loadSpeed: actualLoadSpeed,
pingTime: Math.round(pingTime),
bitrate: bitrateStr,
});
}
resolveCurrentState();
}
};
@@ -309,7 +302,7 @@ export async function getVideoResolutionFromM3u8(
});
// 为分片请求添加时间戳参数破除浏览器缓存
hls.config.xhrSetup = function(xhr: XMLHttpRequest, url: string) {
hls.config.xhrSetup = function (xhr: XMLHttpRequest, url: string) {
const urlWithTimestamp = url.includes('?')
? `${url}&_t=${Date.now()}`
: `${url}?_t=${Date.now()}`;
@@ -397,3 +390,48 @@ export function base58Decode(encoded: string): string {
// 在 Node.js 环境中使用 Buffer
return Buffer.from(bytes).toString('utf-8');
}
/**
* 校验提供给代理层的外部 URL 是否安全
* 防止 SSRF (服务端请求伪造) 访问内网私有地址
*/
export function isValidUrlForProxy(urlStr: string): boolean {
if (!urlStr) return false;
try {
const parsed = new URL(urlStr);
// 仅允许 http 和 https
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
return false;
}
const { hostname } = parsed;
// 拦截明显的本地或特定地址
if (
hostname === 'localhost' ||
hostname === '127.0.0.1' ||
hostname === '0.0.0.0' ||
hostname === '[::1]'
) {
return false;
}
// 通过正则拦截内网 IPv4 (10.x, 172.16-31.x, 192.168.x, 169.254.x)
const ipv4Regex = /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/;
const match = hostname.match(ipv4Regex);
if (match) {
const parts = match.slice(1).map(Number);
if (parts[0] === 10) return false;
if (parts[0] === 127) return false;
if (parts[0] === 192 && parts[1] === 168) return false;
if (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) return false;
if (parts[0] === 169 && parts[1] === 254) return false;
}
return true;
} catch {
// 解析失败直接判定为不安全
return false;
}
}