import { afterEach, describe, expect, it } from "vitest"; import { chmodSync, mkdirSync, symlinkSync, writeFileSync, statSync } from "node:fs"; import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; import { loadConfig, prepareDataDirectories } from "../server/config.js"; const keys = ["TALLYNOTE_DATA_DIR", "TALLYNOTE_PUBLIC_ORIGIN", "TALLYNOTE_COOKIE_SECURE", "TALLYNOTE_TRUST_PROXY", "NODE_ENV", "TALLYNOTE_ENV", "TALLYNOTE_UPDATE_STRATEGY", "TALLYNOTE_UPDATE_ALLOWED_HOSTS", "TALLYNOTE_UPDATE_REQUIRE_SIGNATURE", "TALLYNOTE_UPDATE_PUBLIC_KEY", "TALLYNOTE_UPDATE_PUBLIC_KEY_FILE"]; afterEach(() => { for (const key of keys) delete process.env[key]; }); describe("部署安全配置", () => { it("公网 HTTP 或 HTTPS 非安全 Cookie 一律拒绝", () => { process.env.TALLYNOTE_PUBLIC_ORIGIN = "http://example.test"; expect(() => loadConfig()).toThrow(/HTTPS/); process.env.TALLYNOTE_PUBLIC_ORIGIN = "https://example.test"; process.env.TALLYNOTE_COOKIE_SECURE = "false"; expect(() => loadConfig()).toThrow(/安全 Cookie/); }); it("生产环境不接受任意 trust proxy", () => { process.env.NODE_ENV = "production"; process.env.TALLYNOTE_TRUST_PROXY = "true"; expect(() => loadConfig()).toThrow(/代理跳数/); process.env.TALLYNOTE_TRUST_PROXY = "1"; process.env.TALLYNOTE_PUBLIC_ORIGIN = "https://example.test"; expect(loadConfig().trustProxy).toBe(1); }); it("systemd 更新必须绑定主机白名单并默认要求签名", () => { process.env.TALLYNOTE_UPDATE_STRATEGY = "systemd"; process.env.TALLYNOTE_PUBLIC_ORIGIN = "https://example.test"; process.env.TALLYNOTE_COOKIE_SECURE = "true"; expect(() => loadConfig()).toThrow(/ALLOWED_HOSTS/); process.env.TALLYNOTE_UPDATE_ALLOWED_HOSTS = "updates.example"; const config = loadConfig(); expect(config.updateRequireSignature).toBe(true); }); it("收紧已有数据目录和数据库文件权限,并拒绝符号链接", () => { const dataDir = mkdtempSync(path.join(tmpdir(), "tallynote-permissions-")); try { process.env.TALLYNOTE_DATA_DIR = dataDir; process.env.TALLYNOTE_PUBLIC_ORIGIN = "http://127.0.0.1:3994"; process.env.TALLYNOTE_COOKIE_SECURE = "false"; const config = loadConfig(); mkdirSync(config.filesDir, { recursive: true }); mkdirSync(config.stagingDir, { recursive: true }); mkdirSync(config.exportsDir, { recursive: true }); writeFileSync(config.dbPath, "placeholder"); chmodSync(config.dataDir, 0o777); chmodSync(config.filesDir, 0o777); chmodSync(config.dbPath, 0o666); prepareDataDirectories(config); expect(statSync(config.dataDir).mode & 0o777).toBe(0o700); expect(statSync(config.filesDir).mode & 0o777).toBe(0o700); expect(statSync(config.dbPath).mode & 0o777).toBe(0o600); const linked = path.join(dataDir, "linked"); symlinkSync(config.filesDir, linked); process.env.TALLYNOTE_DATA_DIR = linked; expect(() => prepareDataDirectories(loadConfig())).toThrow(/符号链接/); } finally { rmSync(dataDir, { recursive: true, force: true }); } }); });