You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

80 lines
2.9 KiB

import { resolve, join, relative } from "node:path";
import { promises as fsp } from "node:fs";
import { decodePath, withLeadingSlash, withoutTrailingSlash, parseURL } from "ufo";
import mime from "mime";
import { STATIC_DIR, STATIC_PUBLIC_PREFIX } from "../constants/upload";
if (import.meta.dev) {
console.log("plugin: 00.static");
}
const METHODS = new Set(["HEAD", "GET"]);
const SAFE_BASE_DIR = resolve(STATIC_DIR);
const CACHE_CONTROL = "public, max-age=31536000, immutable";
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook("request", async (event) => {
if (!event.path.startsWith(STATIC_PUBLIC_PREFIX)) return;
const { req } = event.node;
const method = req.method;
if (method && !METHODS.has(method)) return;
try {
const url = event.path.replace(STATIC_PUBLIC_PREFIX, "");
const pathname = decodePath(
withLeadingSlash(withoutTrailingSlash(parseURL(url).pathname))
);
const targetPath = join(SAFE_BASE_DIR, pathname);
const resolvedPath = resolve(targetPath);
// 安全校验
const rel = relative(SAFE_BASE_DIR, resolvedPath);
if (rel.startsWith("..") || rel === "..") {
event.respondWith(new Response("Forbidden", { status: 403 }));
return;
}
const stat = await fsp.stat(resolvedPath);
if (!stat.isFile()) {
event.respondWith(new Response("Not Found", { status: 404 }));
return;
}
const mtime = stat.mtime.toUTCString();
const etag = `"${stat.mtime.getTime().toString(16)}-${stat.size.toString(16)}"`;
const contentType = mime.getType(resolvedPath) || "application/octet-stream";
const headers: Record<string, string> = {
"Cache-Control": CACHE_CONTROL,
ETag: etag,
"Last-Modified": mtime,
Connection: "close",
};
// 304 协商缓存
const ifNoneMatch = req.headers["if-none-match"];
const ifModifiedSince = req.headers["if-modified-since"];
if (ifNoneMatch === etag || (ifModifiedSince && ifModifiedSince === mtime)) {
event.respondWith(new Response(null, { status: 304, headers }));
return;
}
headers["Content-Type"] = contentType;
headers["Content-Length"] = String(stat.size);
const body = method === "HEAD" ? null : await fsp.readFile(resolvedPath);
event.respondWith(new Response(body, { status: 200, headers }));
} catch (err: any) {
if (err.code === "ENOENT") {
event.respondWith(new Response("Not Found", { status: 404 }));
return;
}
event.respondWith(new Response("Server Error", { status: 500 }));
}
});
});