Files
Termix/src/backend/hosts/metrics/managers/logs.ts
T

93 lines
3.1 KiB
TypeScript

import type { Express } from "express";
import { execCommand } from "../widgets/common-utils.js";
import { execElevated } from "./exec-elevated.js";
import { managerHandler, ManagerInputError } from "./route-helpers.js";
import { shellSingleQuote } from "./exec-elevated.js";
import { isAllowedPath, isValidSystemdUnit } from "./validation.js";
import type { ManagerRoutesDeps } from "./types.js";
/** Directories from which arbitrary log files may be tailed. */
export const LOG_PATH_ALLOWLIST = ["/var/log"];
const COMMON_LOGS = [
"/var/log/syslog",
"/var/log/messages",
"/var/log/auth.log",
"/var/log/secure",
"/var/log/kern.log",
"/var/log/dpkg.log",
"/var/log/nginx/access.log",
"/var/log/nginx/error.log",
];
const LIST_LOGS_CMD = `ls -1 ${LOG_PATH_ALLOWLIST.map(shellSingleQuote).join(" ")} 2>/dev/null`;
export function clampLines(n: unknown): number {
const v = typeof n === "string" ? Number(n) : n;
if (typeof v !== "number" || !Number.isFinite(v)) return 200;
return Math.min(2000, Math.max(1, Math.round(v)));
}
export function buildTailCommand(path: string, lines: number): string {
// Keep stderr intact so execElevated can detect a permission error and
// escalate; suppressing it (2>/dev/null) would hide the denial and return an
// empty log with no chance to retry under sudo.
return `tail -n ${lines} ${shellSingleQuote(path)}`;
}
export function buildJournalCommand(unit: string, lines: number): string {
return `journalctl -u ${shellSingleQuote(unit)} -n ${lines} --no-pager`;
}
export function registerLogRoutes(
app: Express,
{ validateHostId, runOnHost }: ManagerRoutesDeps,
): void {
app.get(
"/host-metrics/managers/logs/:id/files",
validateHostId,
managerHandler(runOnHost, "connect", "logs_list", async (client) => {
const { stdout } = await execCommand(client, LIST_LOGS_CMD, 10000);
const found = stdout
.split("\n")
.map((l) => l.trim())
.filter(Boolean)
.map((name) => `/var/log/${name}`);
return { common: COMMON_LOGS, files: found };
}),
);
app.get(
"/host-metrics/managers/logs/:id",
validateHostId,
managerHandler(
runOnHost,
"connect",
"logs_tail",
async (client, host, req) => {
const path = req.query.path as string | undefined;
const unit = req.query.unit as string | undefined;
const lines = clampLines(req.query.lines);
let cmd: string;
if (unit) {
if (!isValidSystemdUnit(unit))
throw new ManagerInputError("Invalid unit");
cmd = buildJournalCommand(unit, lines);
} else if (path) {
if (!isAllowedPath(path, LOG_PATH_ALLOWLIST)) {
throw new ManagerInputError("Path not allowed");
}
cmd = buildTailCommand(path, lines);
} else {
throw new ManagerInputError("Provide a path or unit");
}
// Try unprivileged; many logs need root (auth.log, etc.).
const result = await execElevated(client, cmd, host.sudoPassword);
return { content: result.stdout, lines };
},
),
);
}