mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 06:35:24 +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
@@ -173,6 +173,7 @@ export function createPluginFetch(deps: Deps): PluginFetch {
|
||||
url,
|
||||
{
|
||||
method: init.method ?? "GET",
|
||||
redirect: init.redirect,
|
||||
headers: init.headers,
|
||||
body: init.body,
|
||||
signal: controller.signal,
|
||||
|
||||
@@ -26,6 +26,7 @@ const state = vi.hoisted(() => ({
|
||||
fetchCalls: [] as Array<{ url: string; allowlist: readonly string[] }>,
|
||||
lastTls: null as Record<string, unknown> | null,
|
||||
lastSignal: null as AbortSignal | null,
|
||||
lastRedirect: undefined as RequestRedirect | undefined,
|
||||
}));
|
||||
|
||||
vi.mock("../../database/repositories/factory.js", () => ({
|
||||
@@ -66,6 +67,7 @@ vi.mock("../../utils/safe-outbound-fetch.js", () => ({
|
||||
tls: Record<string, unknown> = {},
|
||||
) => {
|
||||
state.lastTls = tls;
|
||||
state.lastRedirect = init.redirect;
|
||||
state.lastSignal = init.signal ?? null;
|
||||
state.fetchCalls.push({ url, allowlist });
|
||||
return new Response("ok", { status: 200 });
|
||||
@@ -256,6 +258,14 @@ describe("ctx.notify", () => {
|
||||
});
|
||||
|
||||
describe("ctx.fetch", () => {
|
||||
it("forwards manual redirect handling without enabling redirect following", async () => {
|
||||
const ctx = contextFor(["network:outbound"]);
|
||||
await ctx.fetch("https://example.com", { redirect: "manual" });
|
||||
expect(state.lastRedirect).toBe("manual");
|
||||
await ctx.fetch("https://example.com");
|
||||
expect(state.lastRedirect).toBeUndefined();
|
||||
});
|
||||
|
||||
it("refuses without network:outbound", async () => {
|
||||
const ctx = contextFor([]);
|
||||
await expect(ctx.fetch("https://example.com")).rejects.toThrow(
|
||||
|
||||
@@ -281,3 +281,38 @@ describe("safeOutboundFetch", () => {
|
||||
}
|
||||
}, 10_000);
|
||||
});
|
||||
|
||||
describe("redirect handling", () => {
|
||||
it.each([undefined, "error", "follow", "manual"] as const)(
|
||||
"never follows redirects with mode %s",
|
||||
async (redirect) => {
|
||||
const paths: string[] = [];
|
||||
const server = createServer((req, res) => {
|
||||
paths.push(req.url!);
|
||||
res.writeHead(302, { Location: "/private-target" });
|
||||
res.end();
|
||||
});
|
||||
await new Promise<void>((resolve) =>
|
||||
server.listen(0, "127.0.0.1", resolve),
|
||||
);
|
||||
const { port } = server.address() as AddressInfo;
|
||||
try {
|
||||
const response = safeOutboundFetch(
|
||||
`http://127.0.0.1:${port}/`,
|
||||
{ redirect },
|
||||
["127.0.0.1"],
|
||||
);
|
||||
if (redirect === "manual") {
|
||||
const result = await response;
|
||||
expect(result.status).toBe(302);
|
||||
await result.body?.cancel();
|
||||
} else {
|
||||
await expect(response).rejects.toThrow();
|
||||
}
|
||||
expect(paths).toEqual(["/"]);
|
||||
} finally {
|
||||
await new Promise<void>((resolve) => server.close(() => resolve()));
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -222,7 +222,7 @@ export async function safeOutboundFetch(
|
||||
return (await undiciFetch(url.toString(), {
|
||||
...options,
|
||||
dispatcher: proxy as unknown as Dispatcher,
|
||||
redirect: "error",
|
||||
redirect: options.redirect === "manual" ? "manual" : "error",
|
||||
} as never)) as unknown as Response;
|
||||
}
|
||||
|
||||
@@ -240,7 +240,7 @@ export async function safeOutboundFetch(
|
||||
const response = await undiciFetch(url.toString(), {
|
||||
...options,
|
||||
dispatcher,
|
||||
redirect: "error",
|
||||
redirect: options.redirect === "manual" ? "manual" : "error",
|
||||
});
|
||||
// close() waits for the body, which the caller reads after we return.
|
||||
dispatcher.close().catch(() => {});
|
||||
|
||||
Reference in new issue
Block a user