772 lines
22 KiB
JavaScript
772 lines
22 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { execFile } from "node:child_process";
|
|
import { EventEmitter, once } from "node:events";
|
|
import http from "node:http";
|
|
import https from "node:https";
|
|
import net from "node:net";
|
|
import { fileURLToPath } from "node:url";
|
|
import { promisify } from "node:util";
|
|
import test from "node:test";
|
|
import {
|
|
INERT_BODY,
|
|
INVALID_SIGNATURE,
|
|
TIMEOUT_MS,
|
|
isPublicAddress,
|
|
main,
|
|
parseArgs,
|
|
probeTarget,
|
|
} from "./chat-webhook-ingress.mjs";
|
|
|
|
const script = fileURLToPath(
|
|
new URL("./chat-webhook-ingress.mjs", import.meta.url),
|
|
);
|
|
const webhookPath = "/api/chat-webhooks/fixture-public-id-secret/slack";
|
|
const publicUrl = `https://canary.example.com:8443${webhookPath}`;
|
|
const secret = "PRIVATE_RESPONSE_CREDENTIAL_CANARY";
|
|
const runFile = promisify(execFile);
|
|
|
|
async function fixture(t, handler) {
|
|
const server = http.createServer(handler);
|
|
const sockets = new Set();
|
|
server.on("connection", (socket) => {
|
|
sockets.add(socket);
|
|
socket.once("close", () => sockets.delete(socket));
|
|
});
|
|
server.listen(0, "127.0.0.1");
|
|
await once(server, "listening");
|
|
t.after(async () => {
|
|
for (const socket of sockets) socket.destroy();
|
|
await new Promise((resolve) => server.close(resolve));
|
|
});
|
|
const url = `http://127.0.0.1:${server.address().port}${webhookPath}`;
|
|
const target = parseArgs(["--url", publicUrl, "--compare-url", url])
|
|
.targets[1];
|
|
return { server, target, url };
|
|
}
|
|
|
|
function assertRedacted(value) {
|
|
const output = typeof value === "string" ? value : JSON.stringify(value);
|
|
for (const denied of [
|
|
secret,
|
|
"fixture-public-id-secret",
|
|
"canary.example.com",
|
|
"/api/chat-webhooks",
|
|
"127.0.0.1",
|
|
"http:",
|
|
"https:",
|
|
]) {
|
|
assert.equal(
|
|
output.includes(denied),
|
|
false,
|
|
`unexpected private output: ${denied}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
test("no URL, help, invalid options and missing values never invoke networking", async () => {
|
|
for (const argv of [
|
|
[],
|
|
["--help"],
|
|
["--url"],
|
|
["--compare-url", publicUrl],
|
|
["--relay-ip", "8.8.8.8"],
|
|
["--url", publicUrl, "--url", publicUrl],
|
|
[publicUrl],
|
|
["--help", "--url", publicUrl],
|
|
["--url", publicUrl, "--headers", secret],
|
|
["--url", publicUrl, "--retry", "1"],
|
|
["--url", publicUrl, "--timeout", "999"],
|
|
["--url", publicUrl, "--"],
|
|
["--url", 123],
|
|
]) {
|
|
let calls = 0;
|
|
let output = "";
|
|
const status = await main(argv, {
|
|
write: (line) => {
|
|
output += line;
|
|
},
|
|
probe: () => {
|
|
calls++;
|
|
},
|
|
});
|
|
assert.equal(calls, 0);
|
|
assert.equal(status, argv.length === 0 || argv.join() === "--help" ? 0 : 2);
|
|
assert.equal(output.includes(secret), false);
|
|
assert.equal(output.includes("fixture-public-id-secret"), false);
|
|
}
|
|
});
|
|
|
|
for (const raw of [
|
|
`http://canary.example.com${webhookPath}`,
|
|
`https://user:password@canary.example.com${webhookPath}`,
|
|
`${publicUrl}?token=${secret}`,
|
|
`${publicUrl}#${secret}`,
|
|
`${publicUrl}?`,
|
|
`${publicUrl}#`,
|
|
`${publicUrl}\r\nAuthorization: ${secret}`,
|
|
`${publicUrl}\t`,
|
|
` ${publicUrl}`,
|
|
`https://canary.example.com/%2e%2e${webhookPath}`,
|
|
`https://canary.example.com/wrong/..${webhookPath}`,
|
|
`https://canary.example.com\\evil${webhookPath}`,
|
|
`https://canary.example.com${webhookPath.replace("/slack", "/telegram")}`,
|
|
`https://canary.example.com${webhookPath}/`,
|
|
`https://127.0.0.1${webhookPath}`,
|
|
`https://[::1]${webhookPath}`,
|
|
`https://canary.local${webhookPath}`,
|
|
`https://canary.internal${webhookPath}`,
|
|
`https://canary.example.com:0${webhookPath}`,
|
|
`https://canary.example.com:999999${webhookPath}`,
|
|
]) {
|
|
test(`rejects unsafe public URL variant ${[...raw].reduce((sum, char) => sum + char.codePointAt(0), 0)}`, async () => {
|
|
let output = "";
|
|
assert.equal(
|
|
await main(["--url", raw], {
|
|
write: (line) => {
|
|
output += line;
|
|
},
|
|
probe: () => assert.fail("networking"),
|
|
}),
|
|
2,
|
|
);
|
|
assertRedacted(output);
|
|
});
|
|
}
|
|
|
|
test("comparator is exact literal loopback HTTP with exactly the same path", () => {
|
|
for (const authority of ["127.0.0.1:3104", "[::1]:3104"]) {
|
|
assert.equal(
|
|
parseArgs([
|
|
"--url",
|
|
publicUrl,
|
|
"--compare-url",
|
|
`http://${authority}${webhookPath}`,
|
|
]).targets.length,
|
|
2,
|
|
);
|
|
}
|
|
for (const value of [
|
|
`https://127.0.0.1${webhookPath}`,
|
|
`http://localhost${webhookPath}`,
|
|
`http://127.1${webhookPath}`,
|
|
`http://2130706433${webhookPath}`,
|
|
`http://127.0.0.2${webhookPath}`,
|
|
`http://10.0.0.1${webhookPath}`,
|
|
`http://127.0.0.1:3104${webhookPath.replace("fixture-public", "another-public")}`,
|
|
`http://127.0.0.1:3104${webhookPath}\r\nCookie: ${secret}`,
|
|
])
|
|
assert.throws(() =>
|
|
parseArgs(["--url", publicUrl, "--compare-url", value]),
|
|
);
|
|
});
|
|
|
|
test("only unambiguous public relay IPs are accepted, including no IPv6 tunneling", () => {
|
|
for (const address of [
|
|
"8.8.8.8",
|
|
"209.177.145.192",
|
|
"2607:f740:f::b31",
|
|
"2001:4860:4860::8888",
|
|
]) {
|
|
assert.equal(isPublicAddress(address), true);
|
|
assert.equal(
|
|
parseArgs(["--url", publicUrl, "--relay-ip", address]).targets[0].relayIp,
|
|
address,
|
|
);
|
|
}
|
|
for (const address of [
|
|
"0.0.0.0",
|
|
"10.0.0.1",
|
|
"100.64.0.1",
|
|
"100.127.255.255",
|
|
"127.0.0.1",
|
|
"169.254.169.254",
|
|
"172.16.0.1",
|
|
"192.0.0.1",
|
|
"192.0.2.1",
|
|
"192.88.99.1",
|
|
"192.168.1.1",
|
|
"198.18.0.1",
|
|
"198.51.100.1",
|
|
"203.0.113.1",
|
|
"224.0.0.1",
|
|
"255.255.255.255",
|
|
"::",
|
|
"::1",
|
|
"fc00::1",
|
|
"fe80::1",
|
|
"ff02::1",
|
|
"::ffff:127.0.0.1",
|
|
"::ffff:8.8.8.8",
|
|
"64:ff9b::808:808",
|
|
"2001::1",
|
|
"2001:db8::1",
|
|
"2002:7f00:1::1",
|
|
"3fff::1",
|
|
"4000::1",
|
|
"fe80::1%en0",
|
|
"[2607:f740:f::b31]",
|
|
"8.8.8.8:443",
|
|
"008.008.008.008",
|
|
"0x08080808",
|
|
"134744072",
|
|
"example.com",
|
|
"8.8.8.8\r\nAuthorization: secret",
|
|
" 8.8.8.8",
|
|
"8.8.8.8 ",
|
|
"",
|
|
]) {
|
|
assert.equal(isPublicAddress(address), false);
|
|
assert.throws(() => parseArgs(["--url", publicUrl, "--relay-ip", address]));
|
|
}
|
|
});
|
|
|
|
test("401 is one inert POST, with no copied auth or response content", async (t) => {
|
|
let calls = 0;
|
|
let headers;
|
|
let method;
|
|
let path;
|
|
let body = "";
|
|
const { target } = await fixture(t, (req, res) => {
|
|
calls++;
|
|
headers = req.headers;
|
|
method = req.method;
|
|
path = req.url;
|
|
req.setEncoding("utf8");
|
|
req.on("data", (chunk) => {
|
|
body += chunk;
|
|
});
|
|
req.on("end", () =>
|
|
res
|
|
.writeHead(401, { "Set-Cookie": secret, "X-Request-Id": secret })
|
|
.end(secret),
|
|
);
|
|
});
|
|
const result = await probeTarget(target);
|
|
assert.equal(calls, 1);
|
|
assert.equal(method, "POST");
|
|
assert.equal(path, webhookPath);
|
|
assert.equal(body, INERT_BODY);
|
|
assert.equal(headers.host, "canary.example.com:8443");
|
|
assert.equal(headers.authorization, undefined);
|
|
assert.equal(headers.cookie, undefined);
|
|
assert.equal(headers["proxy-authorization"], undefined);
|
|
assert.equal(headers["x-slack-signature"], INVALID_SIGNATURE);
|
|
assert.equal(/^v0=[a-f0-9]{64}$/u.test(headers["x-slack-signature"]), false);
|
|
assert.ok(
|
|
Math.abs(
|
|
Number(headers["x-slack-request-timestamp"]) * 1_000 - Date.now(),
|
|
) < 2_000,
|
|
);
|
|
assert.equal(result.outcome, "expected_rejection");
|
|
assert.equal(result.status, 401);
|
|
assert.equal(result.errorCode, null);
|
|
assert.equal(result.timingsMs.dns, null);
|
|
assert.equal(result.timingsMs.tls, null);
|
|
assert.ok(result.timingsMs.connect <= result.timingsMs.ttfb);
|
|
assert.ok(result.timingsMs.ttfb <= result.timingsMs.total);
|
|
assertRedacted(result);
|
|
});
|
|
|
|
for (const status of [
|
|
101, 200, 202, 204, 301, 302, 307, 308, 400, 403, 404, 429, 500, 502,
|
|
]) {
|
|
test(`HTTP ${status} is unexpected; no redirect, retry or body wait`, async (t) => {
|
|
let destinationCalls = 0;
|
|
let calls = 0;
|
|
const destination = await fixture(t, (_req, res) => {
|
|
destinationCalls++;
|
|
res.end();
|
|
});
|
|
const { target } = await fixture(t, (_req, res) => {
|
|
calls++;
|
|
res.writeHead(status, {
|
|
Location: `${destination.url}?token=${secret}`,
|
|
"Set-Cookie": secret,
|
|
});
|
|
res.flushHeaders(); // Deliberately never end a body: the probe must discard it.
|
|
});
|
|
const result = await probeTarget(target, { timeoutMs: 1_000 });
|
|
assert.equal(result.status, status);
|
|
assert.equal(result.outcome, "unexpected_status");
|
|
assert.equal(calls, 1);
|
|
assert.equal(destinationCalls, 0);
|
|
assertRedacted(result);
|
|
let output = "";
|
|
assert.equal(
|
|
await main(["--url", publicUrl], {
|
|
write: (line) => {
|
|
output += line;
|
|
},
|
|
probe: async () => result,
|
|
}),
|
|
1,
|
|
);
|
|
assertRedacted(output);
|
|
});
|
|
}
|
|
|
|
test("hard deadline destroys a real held HTTP connection instead of retrying", async (t) => {
|
|
let calls = 0;
|
|
let close;
|
|
const closed = new Promise((resolve) => {
|
|
close = resolve;
|
|
});
|
|
const { target } = await fixture(t, (req) => {
|
|
calls++;
|
|
req.socket.once("close", close);
|
|
});
|
|
const result = await probeTarget(target, { timeoutMs: 200 });
|
|
assert.equal(result.outcome, "timeout");
|
|
assert.equal(result.errorCode, "DEADLINE_EXCEEDED");
|
|
assert.equal(result.status, null);
|
|
assert.equal(result.timingsMs.ttfb, null);
|
|
await closed;
|
|
assert.equal(calls, 1);
|
|
assert.equal(TIMEOUT_MS, 8_000);
|
|
await assert.rejects(probeTarget(target, { timeoutMs: 8_001 }));
|
|
assertRedacted(result);
|
|
});
|
|
|
|
test("default eight-second deadline cancels original DNS and forbids late request", async (t) => {
|
|
t.mock.timers.enable({ apis: ["setTimeout"] });
|
|
let release4;
|
|
let release6;
|
|
let cancelled = 0;
|
|
let requests = 0;
|
|
const target = parseArgs(["--url", publicUrl]).targets[0];
|
|
const promise = probeTarget(target, {
|
|
createResolver: () => ({
|
|
resolve4: () =>
|
|
new Promise((resolve) => {
|
|
release4 = resolve;
|
|
}),
|
|
resolve6: () =>
|
|
new Promise((resolve) => {
|
|
release6 = resolve;
|
|
}),
|
|
cancel: () => {
|
|
cancelled++;
|
|
release4(["8.8.8.8"]);
|
|
release6([]);
|
|
},
|
|
}),
|
|
request: () => {
|
|
requests++;
|
|
assert.fail("late request");
|
|
},
|
|
});
|
|
t.mock.timers.tick(7_999);
|
|
assert.equal(cancelled, 0);
|
|
t.mock.timers.tick(1);
|
|
const result = await promise;
|
|
await Promise.resolve();
|
|
assert.equal(result.outcome, "timeout");
|
|
assert.equal(cancelled, 1);
|
|
assert.equal(requests, 0);
|
|
});
|
|
|
|
test("deadline also destroys an actual socket held during TLS handshake", async (t) => {
|
|
let close;
|
|
let connections = 0;
|
|
const closed = new Promise((resolve) => {
|
|
close = resolve;
|
|
});
|
|
const sockets = new Set();
|
|
const server = net.createServer((socket) => {
|
|
connections++;
|
|
sockets.add(socket);
|
|
socket.on("data", () => {}); // Consume the ClientHello, but never answer it.
|
|
socket.once("close", () => {
|
|
sockets.delete(socket);
|
|
close();
|
|
});
|
|
});
|
|
server.listen(0, "127.0.0.1");
|
|
await once(server, "listening");
|
|
t.after(async () => {
|
|
for (const socket of sockets) socket.destroy();
|
|
await new Promise((resolve) => server.close(resolve));
|
|
});
|
|
const target = parseArgs(["--url", publicUrl, "--relay-ip", "8.8.8.8"])
|
|
.targets[0];
|
|
const result = await probeTarget(target, {
|
|
timeoutMs: 200,
|
|
// Test-only transport routes the real TLS client to this owned TCP fixture.
|
|
// It does not disable TLS verification or expose a CLI private-relay option.
|
|
request: (options, callback) =>
|
|
https.request(
|
|
{
|
|
...options,
|
|
port: server.address().port,
|
|
lookup: (_hostname, _options, callback) =>
|
|
callback(null, "127.0.0.1", 4),
|
|
},
|
|
callback,
|
|
),
|
|
});
|
|
assert.equal(result.outcome, "timeout");
|
|
assert.equal(result.status, null);
|
|
assert.notEqual(result.timingsMs.connect, null);
|
|
assert.equal(result.timingsMs.tls, null);
|
|
await closed;
|
|
assert.equal(connections, 1);
|
|
assertRedacted(result);
|
|
});
|
|
|
|
test("DNS private or mixed answers fail closed before any connection", async () => {
|
|
for (const addresses of [
|
|
["100.64.0.1"],
|
|
["8.8.8.8", "127.0.0.1"],
|
|
["::ffff:127.0.0.1"],
|
|
]) {
|
|
const result = await probeTarget(
|
|
parseArgs(["--url", publicUrl]).targets[0],
|
|
{
|
|
createResolver: () => ({
|
|
resolve4: async () => addresses,
|
|
resolve6: async () => [],
|
|
cancel() {},
|
|
}),
|
|
request: () => assert.fail("private network connection"),
|
|
},
|
|
);
|
|
assert.equal(result.outcome, "unsafe_resolution");
|
|
assert.equal(result.errorCode, "NON_PUBLIC_DNS_ANSWER");
|
|
assertRedacted(result);
|
|
}
|
|
});
|
|
|
|
test("public resolution/override pins one address but preserves Host, TLS SNI and verification", async () => {
|
|
for (const override of [null, "2607:f740:f::b31"]) {
|
|
const argv = [
|
|
"--url",
|
|
publicUrl,
|
|
...(override ? ["--relay-ip", override] : []),
|
|
];
|
|
let requests = 0;
|
|
let dnsCalls = 0;
|
|
let destroyed = 0;
|
|
const result = await probeTarget(parseArgs(argv).targets[0], {
|
|
createResolver: () => ({
|
|
resolve4: async () => {
|
|
dnsCalls++;
|
|
return ["8.8.8.8", "8.8.4.4"];
|
|
},
|
|
resolve6: async () => {
|
|
dnsCalls++;
|
|
return [];
|
|
},
|
|
cancel() {},
|
|
}),
|
|
request: (options, callback) => {
|
|
requests++;
|
|
assert.equal(options.hostname, "canary.example.com");
|
|
assert.equal(options.servername, "canary.example.com");
|
|
assert.equal(options.headers.Host, "canary.example.com:8443");
|
|
assert.equal(options.path, webhookPath);
|
|
assert.equal(options.rejectUnauthorized, true);
|
|
assert.equal(options.auth, undefined);
|
|
assert.equal(options.autoSelectFamily, false);
|
|
assert.ok(options.agent instanceof https.Agent);
|
|
assert.deepEqual(options.agent.options.proxyEnv, {});
|
|
options.lookup(options.hostname, {}, (error, address) => {
|
|
assert.equal(error, null);
|
|
assert.equal(address, override ?? "8.8.8.8");
|
|
});
|
|
const req = new EventEmitter();
|
|
req.destroy = () => {
|
|
destroyed++;
|
|
};
|
|
req.end = (body) => {
|
|
assert.equal(body, "{}");
|
|
const socket = new EventEmitter();
|
|
req.emit("socket", socket);
|
|
socket.emit("connect");
|
|
socket.emit("secureConnect");
|
|
const response = new EventEmitter();
|
|
response.statusCode = 401;
|
|
response.on("newListener", (event) => assert.notEqual(event, "data"));
|
|
response.read = () => assert.fail("response body must never be read");
|
|
response.destroy = () => {};
|
|
callback(response);
|
|
};
|
|
return req;
|
|
},
|
|
});
|
|
assert.equal(requests, 1);
|
|
assert.equal(dnsCalls, override ? 0 : 2);
|
|
assert.equal(destroyed, 1);
|
|
assert.equal(result.outcome, "expected_rejection");
|
|
assert.ok(result.timingsMs.connect <= result.timingsMs.tls);
|
|
assert.ok(result.timingsMs.tls <= result.timingsMs.ttfb);
|
|
assertRedacted(result);
|
|
}
|
|
});
|
|
|
|
test("failed pinned family is not retried with another DNS answer; asynchronous errors are redacted", async () => {
|
|
let calls = 0;
|
|
let destroys = 0;
|
|
let cancelled = 0;
|
|
const result = await probeTarget(parseArgs(["--url", publicUrl]).targets[0], {
|
|
createResolver: () => ({
|
|
resolve4: async () => ["8.8.8.8", "8.8.4.4"],
|
|
resolve6: async () => ["2001:4860:4860::8888"],
|
|
cancel: () => {
|
|
cancelled++;
|
|
},
|
|
}),
|
|
request: (options) => {
|
|
calls++;
|
|
options.lookup(options.hostname, { all: true }, (error, addresses) => {
|
|
assert.equal(error, null);
|
|
assert.deepEqual(addresses, [{ address: "8.8.8.8", family: 4 }]);
|
|
});
|
|
const request = new EventEmitter();
|
|
request.destroy = () => {
|
|
destroys++;
|
|
};
|
|
request.end = () =>
|
|
queueMicrotask(() =>
|
|
request.emit(
|
|
"error",
|
|
Object.assign(new Error(`${publicUrl} ${secret}`), {
|
|
code: "ENETUNREACH",
|
|
}),
|
|
),
|
|
);
|
|
return request;
|
|
},
|
|
});
|
|
assert.equal(result.outcome, "network_error");
|
|
assert.equal(result.errorCode, "ENETUNREACH");
|
|
assert.equal(calls, 1);
|
|
assert.equal(destroys, 1);
|
|
assert.equal(cancelled, 1);
|
|
assertRedacted(result);
|
|
});
|
|
|
|
test("network/DNS exceptions expose only closed error codes, never messages or stdout/stderr", async () => {
|
|
for (const code of ["ECONNRESET", "ENOTFOUND", secret, undefined]) {
|
|
const error = Object.assign(new Error(`${publicUrl} ${secret}`), {
|
|
code,
|
|
stdout: secret,
|
|
stderr: secret,
|
|
});
|
|
const target = parseArgs(["--url", publicUrl, "--relay-ip", "8.8.8.8"])
|
|
.targets[0];
|
|
const result = await probeTarget(target, {
|
|
request: () => {
|
|
throw error;
|
|
},
|
|
});
|
|
assert.equal(result.outcome, "network_error");
|
|
assert.equal(
|
|
result.errorCode,
|
|
["ECONNRESET", "ENOTFOUND"].includes(code) ? code : "NETWORK_ERROR",
|
|
);
|
|
assertRedacted(result);
|
|
const dnsResult = await probeTarget(
|
|
parseArgs(["--url", publicUrl]).targets[0],
|
|
{
|
|
createResolver: () => ({
|
|
resolve4: async () => {
|
|
throw error;
|
|
},
|
|
resolve6: async () => {
|
|
throw error;
|
|
},
|
|
cancel() {},
|
|
}),
|
|
request: () => assert.fail("failed DNS must not connect"),
|
|
},
|
|
);
|
|
assert.equal(dnsResult.outcome, "network_error");
|
|
assertRedacted(dnsResult);
|
|
let output = "";
|
|
assert.equal(
|
|
await main(["--url", publicUrl], {
|
|
write: (line) => {
|
|
output += line;
|
|
},
|
|
probe: async () => {
|
|
throw error;
|
|
},
|
|
}),
|
|
1,
|
|
);
|
|
assertRedacted(output);
|
|
}
|
|
});
|
|
|
|
test("actual socket failure is redacted and not retried", async (t) => {
|
|
let calls = 0;
|
|
const { target } = await fixture(t, (req) => {
|
|
calls++;
|
|
req.socket.destroy();
|
|
});
|
|
const result = await probeTarget(target);
|
|
assert.equal(result.outcome, "network_error");
|
|
assert.equal(result.errorCode, "ECONNRESET");
|
|
assert.equal(calls, 1);
|
|
assertRedacted(result);
|
|
});
|
|
|
|
test("CLI child and real comparator ignore proxy/auth environments and redact bad argv", async (t) => {
|
|
let proxyCalls = 0;
|
|
const proxy = await fixture(t, (_req, res) => {
|
|
proxyCalls++;
|
|
res.end(secret);
|
|
});
|
|
const proxyUrl = proxy.url
|
|
.replace(webhookPath, "")
|
|
.replace("http://", `http://user:${secret}@`);
|
|
const env = {
|
|
...process.env,
|
|
NODE_USE_ENV_PROXY: "1",
|
|
HTTP_PROXY: proxyUrl,
|
|
HTTPS_PROXY: proxyUrl,
|
|
ALL_PROXY: proxyUrl,
|
|
http_proxy: proxyUrl,
|
|
https_proxy: proxyUrl,
|
|
NO_PROXY: "",
|
|
no_proxy: "",
|
|
PAPERCLIP_AUTH_HEADER: `Bearer ${secret}`,
|
|
PAPERCLIP_COOKIE: secret,
|
|
SLACK_BOT_TOKEN: secret,
|
|
};
|
|
for (const argv of [
|
|
[],
|
|
["--help"],
|
|
["--url", `${publicUrl}\r\nCookie: ${secret}`],
|
|
]) {
|
|
let child;
|
|
try {
|
|
child = await runFile(process.execPath, [script, ...argv], {
|
|
env,
|
|
timeout: 5_000,
|
|
});
|
|
} catch (error) {
|
|
assert.equal(error.code, 2);
|
|
child = error;
|
|
}
|
|
assert.equal(child.stderr, "");
|
|
assert.equal(child.stdout.includes(secret), false);
|
|
assert.equal(child.stdout.includes("fixture-public-id-secret"), false);
|
|
}
|
|
// Explicitly poison Node's GLOBAL agent as --use-env-proxy does. The canary's
|
|
// fresh agent must still connect straight to the one local fixture.
|
|
const originalAgent = http.globalAgent;
|
|
http.globalAgent = new http.Agent({ proxyEnv: env });
|
|
t.after(() => {
|
|
http.globalAgent.destroy();
|
|
http.globalAgent = originalAgent;
|
|
});
|
|
const direct = await fixture(t, (req, res) => {
|
|
assert.equal(req.headers.authorization, undefined);
|
|
assert.equal(req.headers.cookie, undefined);
|
|
res.writeHead(401).end();
|
|
});
|
|
assert.equal(
|
|
(await probeTarget(direct.target)).outcome,
|
|
"expected_rejection",
|
|
);
|
|
assert.equal(proxyCalls, 0);
|
|
});
|
|
|
|
test("CLI refuses native debug output before starting a request", async () => {
|
|
for (const debug of [
|
|
{ NODE_DEBUG: "http,net,tls" },
|
|
{ NODE_DEBUG_NATIVE: "TLS" },
|
|
{ NODE_OPTIONS: "--trace-tls" },
|
|
]) {
|
|
const env = {
|
|
...process.env,
|
|
NODE_DEBUG: "",
|
|
NODE_DEBUG_NATIVE: "",
|
|
NODE_OPTIONS: "",
|
|
...debug,
|
|
};
|
|
const child = await runFile(
|
|
process.execPath,
|
|
[script, "--url", publicUrl, "--relay-ip", "8.8.8.8"],
|
|
{ env, timeout: 5_000 },
|
|
).catch((error) => error);
|
|
assert.equal(child.code, 2);
|
|
assert.equal(
|
|
JSON.parse(child.stdout).errorCode,
|
|
"UNSAFE_DEBUG_ENVIRONMENT",
|
|
);
|
|
assertRedacted(child.stdout);
|
|
assertRedacted(child.stderr);
|
|
}
|
|
});
|
|
|
|
for (const flag of ["--trace_tls", "--trace-tls=true", "--trace_tls=true"]) {
|
|
for (const source of ["argv", "environment"]) {
|
|
test(`refuses ${source} TLS tracing alias ${flag} without networking`, async (t) => {
|
|
const previousArgv = process.execArgv;
|
|
const previousOptions = process.env.NODE_OPTIONS;
|
|
t.after(() => {
|
|
process.execArgv = previousArgv;
|
|
if (previousOptions === undefined) delete process.env.NODE_OPTIONS;
|
|
else process.env.NODE_OPTIONS = previousOptions;
|
|
});
|
|
if (source === "argv") process.execArgv = [...previousArgv, flag];
|
|
else process.env.NODE_OPTIONS = flag;
|
|
let calls = 0;
|
|
let output = "";
|
|
const status = await main(["--url", publicUrl], {
|
|
write: (line) => {
|
|
output += line;
|
|
},
|
|
probe: async () => {
|
|
calls++;
|
|
return { outcome: "expected_rejection" };
|
|
},
|
|
});
|
|
assert.equal(calls, 0);
|
|
assert.equal(status, 2);
|
|
assert.equal(JSON.parse(output).errorCode, "UNSAFE_DEBUG_ENVIRONMENT");
|
|
assertRedacted(output);
|
|
});
|
|
}
|
|
}
|
|
|
|
test("two explicit targets run once in order and any unexpected result fails the command", async () => {
|
|
for (const statuses of [
|
|
[401, 401],
|
|
[502, 401],
|
|
[401, 202],
|
|
]) {
|
|
const calls = [];
|
|
let output = "";
|
|
const exit = await main(
|
|
[
|
|
"--url",
|
|
publicUrl,
|
|
"--compare-url",
|
|
`http://127.0.0.1:3104${webhookPath}`,
|
|
],
|
|
{
|
|
write: (line) => {
|
|
output += line;
|
|
},
|
|
probe: async (target) => {
|
|
const status = statuses[calls.length];
|
|
calls.push(target.label);
|
|
return {
|
|
target: target.label,
|
|
status,
|
|
outcome:
|
|
status === 401 ? "expected_rejection" : "unexpected_status",
|
|
};
|
|
},
|
|
},
|
|
);
|
|
assert.deepEqual(calls, ["public", "comparison"]);
|
|
assert.equal(exit, statuses.every((status) => status === 401) ? 0 : 1);
|
|
assert.equal(output.trim().split("\n").length, 2);
|
|
assertRedacted(output);
|
|
}
|
|
});
|