mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-31 15:45:23 +00:00
fix(homepage): check allowlisted private service reachability (#1479)
* fix(homepage): check allowlisted private service reachability * test(css): exercise the configured PostCSS plugin chain
This commit is contained in:
1 parent
3b9a1bacf0
commit
2824b49ba9
11 files changed
+232
-10
No files matched your search
@@ -1,4 +1,5 @@
|
||||
import http from "node:http";
|
||||
import type { PluginFetchInit } from "@termix/plugin-sdk/backend";
|
||||
import type { AddressInfo } from "node:net";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import express, { type Router } from "express";
|
||||
@@ -36,7 +37,7 @@ export async function startServer(
|
||||
options: {
|
||||
permissions?: string[];
|
||||
users?: string[];
|
||||
fetch?: (url: string, init?: { method?: string }) => Promise<Response>;
|
||||
fetch?: (url: string, init?: PluginFetchInit) => Promise<Response>;
|
||||
} = {},
|
||||
): Promise<TestServer> {
|
||||
const db = await createTestDb(pluginDir);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import type { PluginFetch } from "@termix/plugin-sdk/backend";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { startServer, type TestServer } from "./helpers";
|
||||
|
||||
let server: TestServer;
|
||||
@@ -94,3 +95,101 @@ describe("outbound routes go through ctx.fetch", () => {
|
||||
expect((await server.request("GET", "/favicon")).status).toBe(400);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ping reachability policy", () => {
|
||||
it.each([200, 302, 307, 401, 403, 404, 503])(
|
||||
"classifies HTTP %s without following redirects",
|
||||
async (status) => {
|
||||
const fetch = vi.fn<PluginFetch>(
|
||||
async () => new Response(null, { status }),
|
||||
);
|
||||
server = await startServer({ fetch });
|
||||
const result = await server.request(
|
||||
"GET",
|
||||
"/ping?url=https://example.com",
|
||||
);
|
||||
expect(result.body).toMatchObject({
|
||||
ok: status < 400 || status === 401 || status === 403,
|
||||
statusCode: status,
|
||||
});
|
||||
expect(fetch).toHaveBeenCalledWith(
|
||||
"https://example.com",
|
||||
expect.objectContaining({ redirect: "manual", allowPrivateHosts: [] }),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it("retries HEAD-not-supported as GET with the same policy", async () => {
|
||||
const fetch = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce(new Response(null, { status: 405 }))
|
||||
.mockResolvedValueOnce(new Response(null, { status: 401 }));
|
||||
server = await startServer({ fetch });
|
||||
const result = await server.request("GET", "/ping?url=https://example.com");
|
||||
expect(result.body.ok).toBe(true);
|
||||
expect(fetch.mock.calls.map((call) => call[1].method)).toEqual([
|
||||
"HEAD",
|
||||
"GET",
|
||||
]);
|
||||
expect(
|
||||
fetch.mock.calls.every((call) => call[1].redirect === "manual"),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it.each(["ping", "proxy"])(
|
||||
"%s uses only the admin allowlist and invalidates cached results on revocation",
|
||||
async (route) => {
|
||||
const fetch = vi.fn<PluginFetch>(async () =>
|
||||
jsonResponse({ alive: true }),
|
||||
);
|
||||
server = await startServer({ fetch });
|
||||
await server.mock.ctx.settings.set(
|
||||
"privateEndpoints",
|
||||
"SERVICE.LAN,127.0.0.1\nservice.lan\nhttps://invalid.example",
|
||||
);
|
||||
const path = `/${route}?url=http://service.lan`;
|
||||
await server.request("GET", path);
|
||||
expect(fetch).toHaveBeenLastCalledWith(
|
||||
"http://service.lan",
|
||||
expect.objectContaining({
|
||||
allowPrivateHosts: ["service.lan", "127.0.0.1"],
|
||||
}),
|
||||
);
|
||||
await server.request("GET", path);
|
||||
expect(fetch).toHaveBeenCalledTimes(1);
|
||||
await server.mock.ctx.settings.set("privateEndpoints", "");
|
||||
await server.request("GET", path);
|
||||
expect(fetch).toHaveBeenCalledTimes(2);
|
||||
expect(fetch).toHaveBeenLastCalledWith(
|
||||
"http://service.lan",
|
||||
expect.objectContaining({ allowPrivateHosts: [] }),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it("uses custom CA trust only for allowlisted hosts and refreshes cached results on CA change", async () => {
|
||||
const fetch = vi.fn<PluginFetch>(async () => jsonResponse({ alive: true }));
|
||||
server = await startServer({ fetch });
|
||||
await server.mock.ctx.settings.set("privateEndpoints", "service.lan");
|
||||
await server.mock.ctx.settings.set(
|
||||
"privateCertificateAuthority",
|
||||
"PEM ONE",
|
||||
);
|
||||
await server.request("GET", "/ping?url=https://service.lan");
|
||||
expect(fetch).toHaveBeenLastCalledWith(
|
||||
"https://service.lan",
|
||||
expect.objectContaining({ tls: { ca: "PEM ONE" } }),
|
||||
);
|
||||
await server.mock.ctx.settings.set(
|
||||
"privateCertificateAuthority",
|
||||
"PEM TWO",
|
||||
);
|
||||
await server.request("GET", "/ping?url=https://service.lan");
|
||||
expect(fetch).toHaveBeenLastCalledWith(
|
||||
"https://service.lan",
|
||||
expect.objectContaining({ tls: { ca: "PEM TWO" } }),
|
||||
);
|
||||
await server.request("GET", "/ping?url=https://example.com");
|
||||
expect(fetch.mock.calls.at(-1)?.[1]).not.toHaveProperty("tls");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user