starraid/star-raid/server/server.js
JOJO ab403f4cc7 后端化:账号系统+排行榜+图鉴收集同步+飞行员大厅(零依赖Node+SQLite)
- server/:node:http+node:sqlite 零依赖后端(users/sessions/user_stats/codex_unlocks/runs 五表)
- 安全:scrypt密码哈希/服务端session/全prepared statement/输入白名单/限流/CSP等安全头/静态白名单防穿越
- 前端:o-auth登陆注册 + o-home飞行员大厅(排行榜Top100三排序+图鉴展柜背景:随机3主3副2僚机+刷怪巡游)
- 图鉴改为收集点亮制(未收集=剪影+???),服务端codex_unlocks与本地并集同步
- 修复:回标题/大厅后背景卡在上场残影——render按battleUI门控+离场G=null;空闲=星空
- 结算上报:endRun/rush胜利两处submitRun,Boss击杀计数入排行榜
- playwright全流程验证通过(注册/登陆/游戏/结算/图鉴/安全注入/限流);游戏视觉未验收(用户亲自)
2026-08-06 04:20:25 +08:00

107 lines
4.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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/jsserver/、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})`);
});