mirror of
https://github.com/morluto/rea.git
synced 2026-11-03 05:16:16 +00:00
161 lines
5.2 KiB
JavaScript
161 lines
5.2 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { once } from "node:events";
|
|
import { createServer, request } from "node:http";
|
|
import { networkInterfaces } from "node:os";
|
|
import {
|
|
closeVerifierResources,
|
|
withVerifierCleanup,
|
|
} from "../../scripts/lib/verifier-cleanup.mjs";
|
|
|
|
const kind = process.argv[2];
|
|
const owned = [];
|
|
let alternateRequests = 0;
|
|
let site;
|
|
|
|
const upstream = async (name) => {
|
|
const server = createServer((incoming, response) => {
|
|
if (name === "alternate") alternateRequests += 1;
|
|
response.setHeader("Content-Type", "application/json");
|
|
response.end(JSON.stringify({ name, target: incoming.url }));
|
|
});
|
|
owned.push(server);
|
|
server.listen(0, "127.0.0.1");
|
|
await once(server, "listening");
|
|
return `http://127.0.0.1:${server.address().port}`;
|
|
};
|
|
|
|
const send = (origin, target) =>
|
|
new Promise((resolve, reject) => {
|
|
const selected = new URL(origin);
|
|
const pending = request(
|
|
{
|
|
hostname: selected.hostname,
|
|
port: selected.port,
|
|
path: target,
|
|
timeout: 2_000,
|
|
},
|
|
(response) => {
|
|
const chunks = [];
|
|
response.on("data", (chunk) => chunks.push(chunk));
|
|
response.on("error", reject);
|
|
response.on("end", () =>
|
|
resolve({
|
|
status: response.statusCode,
|
|
headers: response.headers,
|
|
body: Buffer.concat(chunks).toString("utf8"),
|
|
}),
|
|
);
|
|
},
|
|
);
|
|
pending.on("error", reject);
|
|
pending.on("timeout", () =>
|
|
pending.destroy(new Error("Fixture HTTP request timed out")),
|
|
);
|
|
pending.end();
|
|
});
|
|
|
|
await withVerifierCleanup(async () => {
|
|
let origin;
|
|
let ordinary;
|
|
let expected;
|
|
if (kind === "proxy") {
|
|
const configured = await upstream("configured");
|
|
const alternate = await upstream("alternate");
|
|
const { startProxy } =
|
|
await import("../../scripts/lib/browser-scenario-service-worker-setup-e2e.mjs");
|
|
site = await startProxy(configured, "http-discovery");
|
|
origin = site.endpoint;
|
|
ordinary = "/probe?x=1";
|
|
expected = JSON.stringify({ name: "configured", target: ordinary });
|
|
for (const [target, path] of [
|
|
["/probe?x=a%20b&x=2", "/probe?x=a%20b&x=2"],
|
|
[`${alternate}/probe?absolute=1`, "/probe?absolute=1"],
|
|
[
|
|
`${alternate.replace("http:", "")}/probe?x=1`,
|
|
`${alternate.replace("http:", "")}/probe?x=1`,
|
|
],
|
|
["https://unused.invalid/probe?escaped=%2f", "/probe?escaped=%2f"],
|
|
["/%40scope%2fmodule?x=%252f", "/%40scope%2fmodule?x=%252f"],
|
|
]) {
|
|
const reply = await send(origin, target);
|
|
assert.equal(reply.status, 200);
|
|
assert.deepEqual(JSON.parse(reply.body), {
|
|
name: "configured",
|
|
target: path,
|
|
});
|
|
}
|
|
assert.equal(alternateRequests, 0);
|
|
} else if (kind === "workers") {
|
|
const { startSite } =
|
|
await import("../../scripts/lib/browser-scenario-service-workers-e2e.mjs");
|
|
site = await startSite();
|
|
origin = site.origin;
|
|
ordinary = "/probe";
|
|
expected = "network";
|
|
} else {
|
|
assert.equal(kind, "storage");
|
|
const { startStorageSite } =
|
|
await import("../../scripts/lib/browser-scenario-storage-e2e.mjs");
|
|
site = await startStorageSite();
|
|
origin = site.origin;
|
|
ordinary = "/storage-main";
|
|
}
|
|
const checkOrdinary = async () => {
|
|
const reply = await send(origin, ordinary);
|
|
assert.equal(reply.status, 200);
|
|
if (expected !== undefined) assert.equal(reply.body, expected);
|
|
else assert(reply.body.includes('id="main-report"'));
|
|
};
|
|
await checkOrdinary();
|
|
if (process.argv[3] === "listener-scope") {
|
|
assert.equal(kind, "storage");
|
|
const named = new URL(origin);
|
|
named.hostname = "localhost";
|
|
const localhost = await send(named.href, ordinary);
|
|
assert.equal(localhost.status, 200);
|
|
assert(localhost.body.includes('id="main-report"'));
|
|
const address = Object.values(networkInterfaces())
|
|
.flatMap((entries) => entries ?? [])
|
|
.find((entry) => entry.family === "IPv4" && !entry.internal);
|
|
assert(
|
|
address,
|
|
"Listener scope probe requires an owned non-loopback IPv4 interface",
|
|
);
|
|
const nonLoopback = new URL(origin);
|
|
nonLoopback.hostname = address.address;
|
|
await assert.rejects(send(nonLoopback.href, ordinary), {
|
|
code: "ECONNREFUSED",
|
|
});
|
|
console.log("storage: localhost served; non-loopback connection refused");
|
|
}
|
|
for (const target of [
|
|
"http://[",
|
|
"ftp://127.0.0.1/probe",
|
|
"ws://127.0.0.1/probe",
|
|
]) {
|
|
const reply = await send(origin, target);
|
|
assert.equal(reply.status, 400);
|
|
assert.equal(reply.headers["content-type"], "text/plain; charset=utf-8");
|
|
assert.equal(reply.headers["x-content-type-options"], "nosniff");
|
|
assert.equal(reply.body, "Invalid HTTP request target");
|
|
await checkOrdinary();
|
|
}
|
|
await site.close();
|
|
site = undefined;
|
|
await assert.rejects(send(origin, ordinary));
|
|
console.log(
|
|
`${kind}: target routing, invalid-target recovery and physical close passed`,
|
|
);
|
|
}, [
|
|
() => site?.close(),
|
|
() =>
|
|
closeVerifierResources(
|
|
owned.map((server) => async () => {
|
|
server.closeAllConnections();
|
|
await new Promise((resolve, reject) =>
|
|
server.close((cause) => (cause ? reject(cause) : resolve())),
|
|
);
|
|
}),
|
|
),
|
|
]);
|