118 lines
4.1 KiB
JavaScript
118 lines
4.1 KiB
JavaScript
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);
|