89 lines
4.4 KiB
TypeScript
89 lines
4.4 KiB
TypeScript
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_HOST", "TALLYNOTE_PORT", "TALLYNOTE_PUBLIC_ORIGIN", "TALLYNOTE_COOKIE_SECURE", "TALLYNOTE_ALLOW_INSECURE_HTTP", "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_ALLOW_INSECURE_HTTP = "true";
|
|
expect(loadConfig().allowInsecureHttp).toBe(true);
|
|
process.env.TALLYNOTE_COOKIE_SECURE = "true";
|
|
expect(() => loadConfig()).toThrow(/安全 Cookie/);
|
|
process.env.TALLYNOTE_PUBLIC_ORIGIN = "https://example.test";
|
|
process.env.TALLYNOTE_COOKIE_SECURE = "false";
|
|
expect(() => loadConfig()).toThrow(/安全 Cookie/);
|
|
});
|
|
|
|
it("允许显式配置服务器 IP 的直连 HTTP,并拒绝通配 Origin", () => {
|
|
process.env.TALLYNOTE_HOST = "0.0.0.0";
|
|
process.env.TALLYNOTE_PORT = "3000";
|
|
process.env.TALLYNOTE_PUBLIC_ORIGIN = "http://192.0.2.10:3000";
|
|
process.env.TALLYNOTE_ALLOW_INSECURE_HTTP = "true";
|
|
process.env.TALLYNOTE_COOKIE_SECURE = "false";
|
|
expect(loadConfig()).toMatchObject({ host: "0.0.0.0", port: 3000, publicOrigin: "http://192.0.2.10:3000", allowInsecureHttp: true });
|
|
process.env.TALLYNOTE_PUBLIC_ORIGIN = "http://0.0.0.0:3000";
|
|
expect(() => loadConfig()).toThrow(/通配监听地址/);
|
|
});
|
|
|
|
it("为 IPv6 监听地址生成合法的默认 Origin", () => {
|
|
process.env.TALLYNOTE_HOST = "::1";
|
|
process.env.TALLYNOTE_PORT = "3000";
|
|
expect(loadConfig().publicOrigin).toBe("http://[::1]:3000");
|
|
});
|
|
|
|
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(false);
|
|
process.env.TALLYNOTE_UPDATE_REQUIRE_SIGNATURE = "true";
|
|
expect(loadConfig().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 });
|
|
}
|
|
});
|
|
});
|