feat: complete TASK-WP0-03 browser gate
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { execFileSync, spawn } from "node:child_process";
|
||||
import { once } from "node:events";
|
||||
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { request } from "node:http";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
const host = "127.0.0.1";
|
||||
const port = 43121;
|
||||
|
||||
function sha256(value) {
|
||||
return createHash("sha256").update(value).digest("hex").toUpperCase();
|
||||
}
|
||||
|
||||
function firewallSnapshot() {
|
||||
return execFileSync(
|
||||
"netsh",
|
||||
["advfirewall", "firewall", "show", "rule", "name=all", "dir=in"],
|
||||
{ encoding: "utf8", windowsHide: true },
|
||||
);
|
||||
}
|
||||
|
||||
function httpRequest(headers = {}) {
|
||||
return new Promise((resolveRequest, reject) => {
|
||||
const outgoing = request({ headers, host, method: "GET", path: "/", port }, (response) => {
|
||||
response.resume();
|
||||
response.once("end", () => resolveRequest({ headers: response.headers, status: response.statusCode }));
|
||||
});
|
||||
outgoing.once("error", reject);
|
||||
outgoing.end();
|
||||
});
|
||||
}
|
||||
|
||||
async function waitUntilReady() {
|
||||
for (let attempt = 0; attempt < 40; attempt += 1) {
|
||||
try {
|
||||
const response = await httpRequest();
|
||||
if (response.status === 200) return response;
|
||||
} catch {
|
||||
// The fixed listener may still be starting.
|
||||
}
|
||||
await new Promise((resolveWait) => setTimeout(resolveWait, 125));
|
||||
}
|
||||
throw new Error("The fixed loopback listener did not become ready.");
|
||||
}
|
||||
|
||||
const firewallBefore = firewallSnapshot();
|
||||
const child = spawn(process.execPath, ["apps/api/dist/main.js"], {
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
windowsHide: true,
|
||||
});
|
||||
let stderr = "";
|
||||
child.stderr.on("data", (chunk) => {
|
||||
stderr += chunk.toString();
|
||||
});
|
||||
|
||||
let rootResponse;
|
||||
let rejectedResponse;
|
||||
let listeners;
|
||||
try {
|
||||
rootResponse = await waitUntilReady();
|
||||
rejectedResponse = await httpRequest({ host: "192.168.1.10:43121" });
|
||||
const netstat = execFileSync("netstat", ["-ano", "-p", "tcp"], { encoding: "utf8", windowsHide: true });
|
||||
const matchingListeners = netstat
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trim().split(/\s+/))
|
||||
.filter((tokens) => tokens[0]?.toUpperCase() === "TCP" && tokens.at(-1) === String(child.pid) && tokens[1]?.endsWith(`:${port}`))
|
||||
.map((tokens) => ({ local_address: tokens[1], pid: child.pid }));
|
||||
listeners = [...new Map(matchingListeners.map((listener) => [listener.local_address, listener])).values()];
|
||||
} finally {
|
||||
child.kill();
|
||||
await Promise.race([
|
||||
once(child, "exit"),
|
||||
new Promise((_, reject) => setTimeout(() => reject(new Error("API process did not stop.")), 5000)),
|
||||
]);
|
||||
}
|
||||
|
||||
if (stderr.trim()) throw new Error("The API wrote to stderr during loopback smoke.");
|
||||
const firewallAfter = firewallSnapshot();
|
||||
const expectedListener = `${host}:${port}`;
|
||||
const socketResult = {
|
||||
expected: expectedListener,
|
||||
listeners,
|
||||
status: listeners.length > 0 && listeners.every(({ local_address }) => local_address === expectedListener)
|
||||
? "passed"
|
||||
: "failed",
|
||||
};
|
||||
const responseResult = {
|
||||
invalid_host: rejectedResponse,
|
||||
localhost: rootResponse,
|
||||
status: rootResponse.status === 200 && rejectedResponse.status === 426 ? "passed" : "failed",
|
||||
};
|
||||
const firewallResult = {
|
||||
after_sha256: sha256(firewallAfter),
|
||||
before_sha256: sha256(firewallBefore),
|
||||
status: firewallAfter === firewallBefore ? "passed" : "failed",
|
||||
unchanged: firewallAfter === firewallBefore,
|
||||
};
|
||||
|
||||
const evidenceDirectory = process.env.DADA_EVIDENCE_DIR_BND;
|
||||
if (evidenceDirectory) {
|
||||
mkdirSync(evidenceDirectory, { recursive: true });
|
||||
writeFileSync(resolve(evidenceDirectory, "socket-listeners.json"), `${JSON.stringify(socketResult, null, 2)}\n`);
|
||||
writeFileSync(resolve(evidenceDirectory, "response.json"), `${JSON.stringify(responseResult, null, 2)}\n`);
|
||||
writeFileSync(resolve(evidenceDirectory, "firewall-diff.json"), `${JSON.stringify(firewallResult, null, 2)}\n`);
|
||||
}
|
||||
|
||||
const result = {
|
||||
firewall: firewallResult.status,
|
||||
responses: responseResult.status,
|
||||
sockets: socketResult.status,
|
||||
status: [firewallResult.status, responseResult.status, socketResult.status].every((status) => status === "passed")
|
||||
? "passed"
|
||||
: "failed",
|
||||
};
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
if (result.status !== "passed") process.exit(1);
|
||||
Reference in New Issue
Block a user