"use strict"; /* ================= Star Raid 服务端入口 ================= 零依赖:node:http + node:sqlite。静态资源白名单 + /api 路由 + 安全头 */ const http = require("node:http"); const fs = require("node:fs"); const path = require("node:path"); const db = require("./db"); const auth = require("./auth"); const { ROUTES } = require("./api"); const { readJson, parseCookies, clientIp } = require("./util"); const PORT = Number(process.env.PORT || 8643); const HOST = process.env.HOST || "0.0.0.0"; const SECURE = process.env.COOKIE_SECURE === "1"; // HTTPS 反代部署时置 1 const ROOT = path.join(__dirname, ".."); // star-raid/ /* 静态资源白名单:只允许入口页与 css/js(server/、data/、demo/ 一律不放行) */ const STATIC_PREFIXES = ["/css/", "/js/"]; const MIME = { ".html": "text/html; charset=utf-8", ".css": "text/css; charset=utf-8", ".js": "application/javascript; charset=utf-8", ".json": "application/json; charset=utf-8", ".png": "image/png", ".jpg": "image/jpeg", ".svg": "image/svg+xml", ".ico": "image/x-icon" }; function secHeaders(res) { res.setHeader("X-Content-Type-Options", "nosniff"); res.setHeader("X-Frame-Options", "DENY"); res.setHeader("Referrer-Policy", "no-referrer"); res.setHeader("Content-Security-Policy", "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src https://fonts.gstatic.com; img-src 'self' data:; connect-src 'self'; base-uri 'none'; frame-ancestors 'none'"); } function sendJson(res, status, obj, setCookie) { const body = JSON.stringify(obj); res.writeHead(status, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store", ...(setCookie ? { "Set-Cookie": setCookie } : {}), }); res.end(body); } /* 请求上下文:统一成功/失败出口 */ function makeCtx(req, res) { return { ip: clientIp(req), secure: SECURE, token: parseCookies(req)[auth.COOKIE_NAME], user: null, setCookie: null, ok(data) { sendJson(res, 200, data, this.setCookie); return true; }, fail(status, error) { sendJson(res, status, { error }, this.setCookie); return false; }, }; } async function handleApi(req, res, pathname, query) { const ctx = makeCtx(req, res); const route = ROUTES[req.method + " " + pathname]; if (!route) return ctx.fail(404, "not found"); if (route.auth) { const u = auth.getSessionUser(db, ctx.token); if (!u) return ctx.fail(401, "未登录或会话已过期"); ctx.user = u; } try { let body = null; /* logout 等无 body 的 POST 不强制 JSON;有 body 的必须 application/json */ if (req.method === "POST" && Number(req.headers["content-length"] || 0) > 0) body = await readJson(req); return route.fn(req, res, ctx, body, query); } catch (e) { const status = e && e.status && Number.isInteger(e.status) ? e.status : 500; if (status === 500) console.error("[api]", pathname, e); return ctx.fail(status, status === 500 ? "服务器开小差了" : e.message); } } function handleStatic(req, res, pathname) { if (req.method !== "GET" && req.method !== "HEAD") { res.writeHead(405); return res.end(); } let p = pathname; if (p === "/") p = "/index.html"; const allowed = p === "/index.html" || STATIC_PREFIXES.some((pre) => p.startsWith(pre)); if (!allowed || p.includes("..")) { res.writeHead(404); return res.end("not found"); } const full = path.normalize(path.join(ROOT, p)); if (!full.startsWith(ROOT + path.sep)) { res.writeHead(404); return res.end("not found"); } const ext = path.extname(full).toLowerCase(); const mime = MIME[ext]; if (!mime) { res.writeHead(404); return res.end("not found"); } fs.readFile(full, (err, data) => { if (err) { res.writeHead(404); return res.end("not found"); } res.writeHead(200, { "Content-Type": mime, "Cache-Control": "no-cache" }); if (req.method === "HEAD") return res.end(); res.end(data); }); } const server = http.createServer((req, res) => { secHeaders(res); let url; try { url = new URL(req.url, "http://x"); } catch (e) { res.writeHead(400); return res.end(); } const pathname = url.pathname; if (pathname.startsWith("/api/")) { if (pathname === "/api/health" && req.method === "GET") return sendJson(res, 200, { ok: true, t: Date.now() }); handleApi(req, res, pathname, url.searchParams); } else { handleStatic(req, res, pathname); } }); server.listen(PORT, HOST, () => { console.log(`[star-raid] server listening on http://${HOST}:${PORT} (secure-cookie=${SECURE})`); });