mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-08-29 10:21:34 +00:00
193 lines
5.6 KiB
TypeScript
193 lines
5.6 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import type { Request } from "express";
|
|
import type { UserRecord } from "../../../database/repositories/user-repository.js";
|
|
import {
|
|
isLoopbackRequest,
|
|
extractBearerOrCookieToken,
|
|
isNativeTokenExportRequest,
|
|
resolveDesktopAutoSessionUser,
|
|
} from "../../../database/routes/desktop-auto-session.js";
|
|
|
|
function makeUser(overrides: Partial<UserRecord> = {}): UserRecord {
|
|
return {
|
|
id: "user-1",
|
|
username: "local",
|
|
passwordHash: "",
|
|
isOidc: false,
|
|
totpEnabled: false,
|
|
isAdmin: false,
|
|
registeredAt: "2026-01-01T00:00:00.000Z",
|
|
...overrides,
|
|
} as UserRecord;
|
|
}
|
|
|
|
describe("isLoopbackRequest", () => {
|
|
it.each(["127.0.0.1", "::1", "::ffff:127.0.0.1"])(
|
|
"accepts %s as loopback",
|
|
(ip) => {
|
|
expect(
|
|
isLoopbackRequest({
|
|
ip,
|
|
headers: {},
|
|
socket: { remoteAddress: ip },
|
|
} as unknown as Request),
|
|
).toBe(true);
|
|
},
|
|
);
|
|
|
|
it("accepts an IPv4-mapped loopback suffix", () => {
|
|
expect(
|
|
isLoopbackRequest({
|
|
ip: "::ffff:127.0.0.1",
|
|
headers: {},
|
|
socket: { remoteAddress: "::ffff:127.0.0.1" },
|
|
} as unknown as Request),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("rejects a non-loopback TCP peer address", () => {
|
|
expect(
|
|
isLoopbackRequest({
|
|
ip: "192.168.1.50",
|
|
headers: {},
|
|
socket: { remoteAddress: "192.168.1.50" },
|
|
} as unknown as Request),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("ignores a spoofed X-Forwarded-For value", () => {
|
|
expect(
|
|
isLoopbackRequest({
|
|
ip: "127.0.0.1",
|
|
headers: { "x-forwarded-for": "127.0.0.1" },
|
|
socket: { remoteAddress: "203.0.113.10" },
|
|
} as unknown as Request),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("rejects requests that traversed the reverse proxy (X-Real-IP set)", () => {
|
|
expect(
|
|
isLoopbackRequest({
|
|
ip: "127.0.0.1",
|
|
headers: { "x-real-ip": "203.0.113.10" },
|
|
socket: { remoteAddress: "127.0.0.1" },
|
|
} as unknown as Request),
|
|
).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("extractBearerOrCookieToken", () => {
|
|
it("prefers the jwt cookie over the Authorization header", () => {
|
|
const req = {
|
|
cookies: { jwt: "cookie-token" },
|
|
headers: { authorization: "Bearer header-token" },
|
|
} as unknown as Request;
|
|
expect(extractBearerOrCookieToken(req)).toBe("cookie-token");
|
|
});
|
|
|
|
it("falls back to a Bearer Authorization header", () => {
|
|
const req = {
|
|
cookies: {},
|
|
headers: { authorization: "Bearer header-token" },
|
|
} as unknown as Request;
|
|
expect(extractBearerOrCookieToken(req)).toBe("header-token");
|
|
});
|
|
|
|
it("returns undefined when neither is present", () => {
|
|
const req = { cookies: {}, headers: {} } as unknown as Request;
|
|
expect(extractBearerOrCookieToken(req)).toBeUndefined();
|
|
});
|
|
|
|
it("ignores a non-Bearer Authorization header", () => {
|
|
const req = {
|
|
cookies: {},
|
|
headers: { authorization: "Basic abc123" },
|
|
} as unknown as Request;
|
|
expect(extractBearerOrCookieToken(req)).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("isNativeTokenExportRequest", () => {
|
|
function requestWithUserAgent(userAgent: string): Request {
|
|
return {
|
|
get: (name: string) =>
|
|
name.toLowerCase() === "user-agent" ? userAgent : undefined,
|
|
} as unknown as Request;
|
|
}
|
|
|
|
it.each([
|
|
"Termix-Mobile/iOS",
|
|
"Termix-Mobile/Android",
|
|
"Termix-Desktop/2.8.0 (win32; Electron/43)",
|
|
])("allows native user agent %s", (userAgent) => {
|
|
expect(isNativeTokenExportRequest(requestWithUserAgent(userAgent))).toBe(
|
|
true,
|
|
);
|
|
});
|
|
|
|
it.each(["Mozilla/5.0", "Electron/43.0.0", "", "Termix-MobileFake/iOS"])(
|
|
"rejects non-native user agent %s",
|
|
(userAgent) => {
|
|
expect(isNativeTokenExportRequest(requestWithUserAgent(userAgent))).toBe(
|
|
false,
|
|
);
|
|
},
|
|
);
|
|
});
|
|
|
|
describe("resolveDesktopAutoSessionUser", () => {
|
|
it("returns the sole local user regardless of having a real password", () => {
|
|
const user = makeUser({ passwordHash: "$2a$10$realbcryptvaluehere" });
|
|
expect(resolveDesktopAutoSessionUser([user])).toBe(user);
|
|
});
|
|
|
|
it("returns the sole local user even when OIDC-enabled", () => {
|
|
const user = makeUser({ isOidc: true });
|
|
expect(resolveDesktopAutoSessionUser([user])).toBe(user);
|
|
});
|
|
|
|
it("returns the sole local user even when TOTP-enabled", () => {
|
|
const user = makeUser({ totpEnabled: true });
|
|
expect(resolveDesktopAutoSessionUser([user])).toBe(user);
|
|
});
|
|
|
|
it("returns the auto-provisioned passwordless placeholder", () => {
|
|
const user = makeUser({ passwordHash: "" });
|
|
expect(resolveDesktopAutoSessionUser([user])).toBe(user);
|
|
});
|
|
|
|
it("declines when zero users exist", () => {
|
|
expect(resolveDesktopAutoSessionUser([])).toBeNull();
|
|
});
|
|
|
|
it("never declines for a multi-user local database -- prefers the admin account", () => {
|
|
const admin = makeUser({
|
|
id: "user-2",
|
|
isAdmin: true,
|
|
registeredAt: "2026-02-01T00:00:00.000Z",
|
|
});
|
|
const result = resolveDesktopAutoSessionUser([
|
|
makeUser({
|
|
id: "user-1",
|
|
isAdmin: false,
|
|
registeredAt: "2026-01-01T00:00:00.000Z",
|
|
}),
|
|
admin,
|
|
]);
|
|
expect(result).toBe(admin);
|
|
});
|
|
|
|
it("falls back to the earliest-registered account when no admin exists", () => {
|
|
const earliest = makeUser({
|
|
id: "user-1",
|
|
registeredAt: "2026-01-01T00:00:00.000Z",
|
|
});
|
|
const result = resolveDesktopAutoSessionUser([
|
|
makeUser({ id: "user-2", registeredAt: "2026-03-01T00:00:00.000Z" }),
|
|
earliest,
|
|
makeUser({ id: "user-3", registeredAt: "2026-02-01T00:00:00.000Z" }),
|
|
]);
|
|
expect(result).toBe(earliest);
|
|
});
|
|
});
|