mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-08-29 18:31:33 +00:00
fix SSH login alert delivery (#1100)
This commit is contained in:
@@ -857,6 +857,36 @@ app.use((_req, res, next) => {
|
||||
next();
|
||||
});
|
||||
|
||||
// Internal endpoint — only accepts calls from localhost.
|
||||
// Used by the main backend to notify the metrics service of SSH login events.
|
||||
app.post("/internal/login-alert", async (req, res) => {
|
||||
const remoteIp = req.socket.remoteAddress;
|
||||
if (
|
||||
remoteIp !== "127.0.0.1" &&
|
||||
remoteIp !== "::1" &&
|
||||
remoteIp !== "::ffff:127.0.0.1"
|
||||
) {
|
||||
return res.status(403).json({ error: "Forbidden" });
|
||||
}
|
||||
const systemCrypto = (await import("../../utils/system-crypto.js"))
|
||||
.SystemCrypto;
|
||||
const expectedToken = await systemCrypto.getInstance().getInternalAuthToken();
|
||||
const token = req.headers["x-internal-auth"];
|
||||
if (!token || token !== expectedToken) {
|
||||
return res.status(403).json({ error: "Forbidden" });
|
||||
}
|
||||
const { hostId, userId, sshUser, fromIp } = req.body as {
|
||||
hostId: number;
|
||||
userId: string;
|
||||
sshUser: string;
|
||||
fromIp: string;
|
||||
};
|
||||
AlertEngine.getInstance()
|
||||
.evaluateUserLogin(hostId, userId, sshUser, fromIp)
|
||||
.catch(() => {});
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
app.use(authManager.createAuthMiddleware());
|
||||
const requireAdmin = authManager.createAdminMiddleware();
|
||||
|
||||
@@ -2803,36 +2833,6 @@ registerManagerRoutes(app, {
|
||||
},
|
||||
});
|
||||
|
||||
// Internal endpoint — only accepts calls from localhost.
|
||||
// Used by the main backend to notify the metrics service of SSH login events.
|
||||
app.post("/internal/login-alert", async (req, res) => {
|
||||
const remoteIp = req.socket.remoteAddress;
|
||||
if (
|
||||
remoteIp !== "127.0.0.1" &&
|
||||
remoteIp !== "::1" &&
|
||||
remoteIp !== "::ffff:127.0.0.1"
|
||||
) {
|
||||
return res.status(403).json({ error: "Forbidden" });
|
||||
}
|
||||
const systemCrypto = (await import("../../utils/system-crypto.js"))
|
||||
.SystemCrypto;
|
||||
const expectedToken = await systemCrypto.getInstance().getInternalAuthToken();
|
||||
const token = req.headers["x-internal-auth"];
|
||||
if (!token || token !== expectedToken) {
|
||||
return res.status(403).json({ error: "Forbidden" });
|
||||
}
|
||||
const { hostId, userId, sshUser, fromIp } = req.body as {
|
||||
hostId: number;
|
||||
userId: string;
|
||||
sshUser: string;
|
||||
fromIp: string;
|
||||
};
|
||||
AlertEngine.getInstance()
|
||||
.evaluateUserLogin(hostId, userId, sshUser, fromIp)
|
||||
.catch(() => {});
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
process.on("SIGINT", () => {
|
||||
pollingManager.destroy();
|
||||
connectionPool.destroy();
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { triggerLoginAlert } from "../../utils/alert-trigger.js";
|
||||
import { SystemCrypto } from "../../utils/system-crypto.js";
|
||||
import { sshLogger } from "../../utils/logger.js";
|
||||
|
||||
describe("triggerLoginAlert", () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("reports a rejected metrics-service request", async () => {
|
||||
vi.spyOn(SystemCrypto, "getInstance").mockReturnValue({
|
||||
getInternalAuthToken: vi.fn().mockResolvedValue("internal-token"),
|
||||
} as never);
|
||||
vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
||||
new Response('{"error":"Missing authentication token"}', {
|
||||
status: 401,
|
||||
}),
|
||||
);
|
||||
const warn = vi.spyOn(sshLogger, "warn").mockImplementation(() => {});
|
||||
|
||||
await triggerLoginAlert(7, "user-1", "root", "192.0.2.1");
|
||||
|
||||
expect(warn).toHaveBeenCalledWith(
|
||||
"Failed to trigger login alert",
|
||||
expect.objectContaining({
|
||||
operation: "login_alert_trigger_error",
|
||||
hostId: 7,
|
||||
error:
|
||||
'Metrics service returned 401: {"error":"Missing authentication token"}',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not log a warning when the metrics service accepts the event", async () => {
|
||||
vi.spyOn(SystemCrypto, "getInstance").mockReturnValue({
|
||||
getInternalAuthToken: vi.fn().mockResolvedValue("internal-token"),
|
||||
} as never);
|
||||
vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
||||
new Response('{"ok":true}', { status: 200 }),
|
||||
);
|
||||
const warn = vi.spyOn(sshLogger, "warn").mockImplementation(() => {});
|
||||
|
||||
await triggerLoginAlert(7, "user-1", "root", "192.0.2.1");
|
||||
|
||||
expect(warn).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -11,14 +11,23 @@ export async function triggerLoginAlert(
|
||||
): Promise<void> {
|
||||
try {
|
||||
const token = await SystemCrypto.getInstance().getInternalAuthToken();
|
||||
await fetch(`${METRICS_SERVICE_URL}/internal/login-alert`, {
|
||||
const response = await fetch(
|
||||
`${METRICS_SERVICE_URL}/internal/login-alert`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-internal-auth": token,
|
||||
},
|
||||
body: JSON.stringify({ hostId, userId, sshUser, fromIp }),
|
||||
});
|
||||
},
|
||||
);
|
||||
if (!response.ok) {
|
||||
const details = await response.text();
|
||||
throw new Error(
|
||||
`Metrics service returned ${response.status}${details ? `: ${details}` : ""}`,
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
sshLogger.warn("Failed to trigger login alert", {
|
||||
operation: "login_alert_trigger_error",
|
||||
|
||||
Reference in New Issue
Block a user