Reject all requests if HEALTH_TOKEN env var is undefined instead of allowing unauthenticated access (fail-open → fail-closed). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
111 lines
3.1 KiB
JavaScript
111 lines
3.1 KiB
JavaScript
const http = require("node:http");
|
|
const os = require("node:os");
|
|
const { execSync } = require("node:child_process");
|
|
|
|
const PORT = parseInt(process.env.PORT || "3001", 10);
|
|
const TOKEN = process.env.HEALTH_TOKEN;
|
|
|
|
if (!TOKEN) {
|
|
console.warn("WARNING: HEALTH_TOKEN is not set. All requests will be rejected (fail-closed).");
|
|
}
|
|
|
|
function readProcStat() {
|
|
try {
|
|
const line = execSync("head -1 /proc/stat", { encoding: "utf-8" }).trim();
|
|
const parts = line.split(/\s+/).slice(1).map(Number);
|
|
const idle = parts[3] + parts[4];
|
|
const total = parts.reduce((a, b) => a + b, 0);
|
|
return { idle, total };
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function getCpuPercent() {
|
|
const t1 = readProcStat();
|
|
if (!t1) return 0;
|
|
const { idle: idle1, total: total1 } = t1;
|
|
|
|
// Sample over 500ms
|
|
execSync("sleep 0.5");
|
|
|
|
const t2 = readProcStat();
|
|
if (!t2) return 0;
|
|
const dIdle = t2.idle - idle1;
|
|
const dTotal = t2.total - total1;
|
|
if (dTotal === 0) return 0;
|
|
return Math.round((1 - dIdle / dTotal) * 100);
|
|
}
|
|
|
|
function getDisk() {
|
|
try {
|
|
// Alpine df doesn't support --output, use standard POSIX format
|
|
const out = execSync("df -k /", { encoding: "utf-8" });
|
|
const parts = out.trim().split("\n")[1].trim().split(/\s+/);
|
|
// df -k columns: Filesystem, 1K-blocks, Used, Available, Use%, Mounted
|
|
const totalGB = +(parseInt(parts[1], 10) / 1e6).toFixed(1);
|
|
const usedGB = +(parseInt(parts[2], 10) / 1e6).toFixed(1);
|
|
const freeGB = +(parseInt(parts[3], 10) / 1e6).toFixed(1);
|
|
const usagePercent = totalGB > 0 ? Math.round((usedGB / totalGB) * 100) : 0;
|
|
return { totalGB, usedGB, freeGB, usagePercent };
|
|
} catch {
|
|
return { totalGB: 0, usedGB: 0, freeGB: 0, usagePercent: 0 };
|
|
}
|
|
}
|
|
|
|
function getHealth() {
|
|
const cpus = os.cpus();
|
|
const totalMem = os.totalmem();
|
|
const freeMem = os.freemem();
|
|
const usedMem = totalMem - freeMem;
|
|
|
|
return {
|
|
timestamp: new Date().toISOString(),
|
|
hostname: os.hostname(),
|
|
uptime: Math.floor(os.uptime()),
|
|
cpu: {
|
|
model: cpus[0]?.model?.trim() || "unknown",
|
|
cores: cpus.length,
|
|
loadAvg: os.loadavg().map((l) => +l.toFixed(2)),
|
|
usagePercent: getCpuPercent(),
|
|
},
|
|
memory: {
|
|
totalGB: +(totalMem / 1e9).toFixed(1),
|
|
usedGB: +(usedMem / 1e9).toFixed(1),
|
|
freeGB: +(freeMem / 1e9).toFixed(1),
|
|
usagePercent: Math.round((usedMem / totalMem) * 100),
|
|
},
|
|
disk: getDisk(),
|
|
};
|
|
}
|
|
|
|
const server = http.createServer((req, res) => {
|
|
res.setHeader("Content-Type", "application/json");
|
|
|
|
if (req.url !== "/health" || req.method !== "GET") {
|
|
res.writeHead(404);
|
|
res.end(JSON.stringify({ error: "Not found" }));
|
|
return;
|
|
}
|
|
|
|
if (!TOKEN) {
|
|
res.writeHead(401);
|
|
res.end(JSON.stringify({ error: "HEALTH_TOKEN not configured" }));
|
|
return;
|
|
}
|
|
|
|
const auth = req.headers["authorization"];
|
|
if (auth !== `Bearer ${TOKEN}`) {
|
|
res.writeHead(401);
|
|
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
return;
|
|
}
|
|
|
|
const data = getHealth();
|
|
res.writeHead(200);
|
|
res.end(JSON.stringify(data));
|
|
});
|
|
|
|
server.listen(PORT, () => {
|
|
console.log(`vps-health-api listening on :${PORT}`);
|
|
});
|