ai-chatbot/lib/video-canvas/local-image-input.ts

65 lines
1.9 KiB
TypeScript

import { readFile, realpath } from "node:fs/promises";
import { resolve, sep } from "node:path";
const LOCAL_UPLOAD_URL_PREFIX = "/uploads/local/";
function isLoopbackHostname(hostname: string) {
const normalized = hostname.toLowerCase();
return normalized === "localhost" || normalized === "127.0.0.1" || normalized === "::1";
}
export function resolveTrustedLocalImageCandidate(
imageInput: string,
workspaceRoot = process.cwd(),
) {
const raw = String(imageInput || "").trim();
if (!raw) return null;
let pathname = "";
if (raw.startsWith("/")) {
pathname = new URL(raw, "http://localhost").pathname;
} else {
let url: URL;
try {
url = new URL(raw);
} catch {
return null;
}
if (!["http:", "https:"].includes(url.protocol) || !isLoopbackHostname(url.hostname)) {
return null;
}
pathname = url.pathname;
}
let decodedPathname: string;
try {
decodedPathname = decodeURIComponent(pathname);
} catch {
return null;
}
if (!decodedPathname.startsWith(LOCAL_UPLOAD_URL_PREFIX)) return null;
const uploadRoot = resolve(workspaceRoot, "public", "uploads", "local");
const relativePath = decodedPathname.slice(LOCAL_UPLOAD_URL_PREFIX.length);
const candidatePath = resolve(uploadRoot, relativePath);
if (!relativePath || !candidatePath.startsWith(`${uploadRoot}${sep}`)) return null;
return { candidatePath, uploadRoot };
}
export async function readTrustedLocalImage(
imageInput: string,
workspaceRoot = process.cwd(),
) {
const candidate = resolveTrustedLocalImageCandidate(imageInput, workspaceRoot);
if (!candidate) return null;
const [realUploadRoot, realCandidatePath] = await Promise.all([
realpath(candidate.uploadRoot),
realpath(candidate.candidatePath),
]);
if (!realCandidatePath.startsWith(`${realUploadRoot}${sep}`)) {
throw new Error("本地图片路径不允许访问");
}
return readFile(realCandidatePath);
}