46 lines
2.2 KiB
JavaScript
46 lines
2.2 KiB
JavaScript
"use strict";
|
|
const crypto = require("node:crypto");
|
|
const fs = require("node:fs");
|
|
const path = require("node:path");
|
|
|
|
class MapGate {
|
|
constructor({ mapsDir = "/etc/guanghu/navigation-maps", stateFile = "" } = {}) {
|
|
this.mapsDir = mapsDir;
|
|
this.stateFile = stateFile;
|
|
this.acks = new Map();
|
|
this.load();
|
|
}
|
|
read(target) {
|
|
if (!/^[A-Z0-9-]+$/.test(target)) throw new Error("invalid_target");
|
|
const data = JSON.parse(fs.readFileSync(path.join(this.mapsDir, `${target}.json`), "utf8"));
|
|
const canonical = JSON.stringify(data);
|
|
return { data, hash: crypto.createHash("sha256").update(canonical).digest("hex") };
|
|
}
|
|
ack(sessionToken, target, mapHash, now = Date.now() / 1000, ttl = 3600) {
|
|
const current = this.read(target);
|
|
if (!safeEqual(current.hash, mapHash)) return { ok: false, reason: "map_hash_mismatch" };
|
|
this.acks.set(hash(sessionToken), { target, mapHash, expiresAt: now + ttl });
|
|
this.persist();
|
|
return { ok: true, expiresAt: now + ttl };
|
|
}
|
|
verify(sessionToken, target, mapHash, now = Date.now() / 1000) {
|
|
const ack = this.acks.get(hash(sessionToken));
|
|
if (!ack) return { ok: false, reason: "map_not_acknowledged" };
|
|
if (now > ack.expiresAt) return { ok: false, reason: "map_ack_expired" };
|
|
if (ack.target !== target) return { ok: false, reason: "map_target_mismatch" };
|
|
if (!safeEqual(ack.mapHash, mapHash)) return { ok: false, reason: "map_changed" };
|
|
return { ok: true };
|
|
}
|
|
load(now = Date.now() / 1000) {
|
|
if (!this.stateFile || !fs.existsSync(this.stateFile)) return;
|
|
try { for (const [key, value] of Object.entries(JSON.parse(fs.readFileSync(this.stateFile, "utf8")).acks || {})) if (value.expiresAt >= now) this.acks.set(key, value); } catch { this.acks.clear(); }
|
|
}
|
|
persist() {
|
|
if (!this.stateFile) return;
|
|
fs.writeFileSync(this.stateFile, JSON.stringify({ version: 1, acks: Object.fromEntries(this.acks) }), { mode: 0o600 });
|
|
}
|
|
}
|
|
function hash(value) { return crypto.createHash("sha256").update(String(value)).digest("hex"); }
|
|
function safeEqual(left, right) { const a = Buffer.from(String(left)); const b = Buffer.from(String(right)); return a.length === b.length && crypto.timingSafeEqual(a, b); }
|
|
module.exports = { MapGate };
|