diff --git a/src/download.ts b/src/download.ts index 9889bfb..6efad7f 100644 --- a/src/download.ts +++ b/src/download.ts @@ -19,6 +19,7 @@ export type DownloadConfig = { httpAgent: http.Agent, httpsAgent: https.Agent, proxy?: boolean; + signatureKey?: string; } export const defaultDownloadConfig = { diff --git a/src/index.ts b/src/index.ts index 2b463c5..bd0e0b3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,6 +15,7 @@ import { StatusError } from './status-error.js'; import { DownloadConfig, defaultDownloadConfig, downloadUrl } from './download.js'; import { getAgents } from './http.js'; import _contentDisposition from 'content-disposition'; +import { verifySignedProxyURL } from "@/sign-proxy-url.js"; const _filename = fileURLToPath(import.meta.url); const _dirname = dirname(_filename); @@ -28,6 +29,7 @@ export type MediaProxyOptions = { userAgent?: string; allowedPrivateNetworks?: string[]; maxSize?: number; + signatureKey?: string; } & ({ proxy?: string; } | { @@ -54,6 +56,7 @@ export function setMediaProxyConfig(setting?: MediaProxyOptions | null) { userAgent: setting.userAgent ?? defaultDownloadConfig.userAgent, allowedPrivateNetworks: setting.allowedPrivateNetworks ?? defaultDownloadConfig.allowedPrivateNetworks, maxSize: setting.maxSize ?? defaultDownloadConfig.maxSize, + signatureKey: setting.signatureKey ?? undefined, ...('proxy' in setting ? { ...getAgents(setting.proxy), proxy: !!setting.proxy } : 'httpAgent' in setting ? { @@ -124,6 +127,12 @@ async function proxyHandler(request: FastifyRequest<{ Params: { url: string; }; return; } + // 检查签名是否符合 + if (!!config.signatureKey && !verifySignedProxyURL(request.url, config.signatureKey)) { + reply.code(401); + return; + } + // Create temp file const file = await downloadAndDetectTypeFromUrl(url); diff --git a/src/sign-proxy-url.ts b/src/sign-proxy-url.ts new file mode 100644 index 0000000..b30beae --- /dev/null +++ b/src/sign-proxy-url.ts @@ -0,0 +1,49 @@ +import { createHmac, timingSafeEqual } from 'node:crypto'; + +export function verifySignedProxyURL(signedURLString: string, signatureKey: string): boolean { + const workingURL = new URL(signedURLString); + + // 提取签名参数 + const sig = workingURL.searchParams.get('sig'); + if (sig === null) { + // 缺失签名 + return false; + } + + // 去掉签名参数 + workingURL.searchParams.delete('sig'); + + // 提取过期时间 + const exp = workingURL.searchParams.get('exp'); + if (exp === null) { + // 缺失过期时间 + return false; + } + // 检查是否已过期 + const expEpochSec = parseInt(exp); + if (expEpochSec > Date.now() / 1000) { + // 无效的时间,或者已经超时 + return false; + } + + // 检查是否有 static 参数:因为前端可能会追加这个参数,为避免参数影响,要把它删除掉。 + if (workingURL.searchParams.has('static')) { + workingURL.searchParams.delete('static'); + } + + // 排序查询字符串 + workingURL.searchParams.sort(); + + // 生成正确的签名用来对照 + const sigCorrect = createHmac('sha256', signatureKey). + update(workingURL.toString()).digest('hex'); + + // 检查签名是否匹配 + if (!timingSafeEqual(Buffer.from(sigCorrect), Buffer.from(sig))) { + // 不匹配 + return false; + } + + // 验证通过 + return true; +}