mirror of
https://github.com/tdurieux/anonymous_github.git
synced 2026-10-18 21:57:20 +02:00
362 lines
12 KiB
JavaScript
362 lines
12 KiB
JavaScript
const { expect } = require("chai");
|
|
const https = require("https");
|
|
const fs = require("fs");
|
|
const os = require("os");
|
|
const path = require("path");
|
|
const process = require("process");
|
|
const { fork, execFileSync } = require("child_process");
|
|
const {
|
|
setTimeout,
|
|
clearTimeout,
|
|
setInterval,
|
|
clearInterval,
|
|
} = require("timers");
|
|
const clientId = "a".repeat(32),
|
|
intentId = "c".repeat(32);
|
|
const at = (offset) =>
|
|
new Date(Math.floor((Date.now() + offset) / 1000) * 1000)
|
|
.toISOString()
|
|
.replace(".000Z", "Z");
|
|
const input = () => ({
|
|
contract: "4open.artifacts/1",
|
|
clientId,
|
|
intentId,
|
|
token: "d".repeat(64),
|
|
requestId: "e".repeat(32),
|
|
});
|
|
const intent = () => ({
|
|
contract: "4open.artifacts/1",
|
|
clientId,
|
|
intentId,
|
|
submissionRef: "f".repeat(32),
|
|
callbackId: "1".repeat(32),
|
|
entitlementId: "2".repeat(32),
|
|
expiresAt: at(120000),
|
|
policy: {
|
|
version: 1,
|
|
access: "restricted-review",
|
|
retainUntil: at(86400000),
|
|
},
|
|
});
|
|
|
|
describe("review intent HTTPS client", function () {
|
|
this.timeout(20000);
|
|
let server,
|
|
child,
|
|
folder,
|
|
origin,
|
|
handler,
|
|
calls,
|
|
serial = 0;
|
|
const pending = new Map();
|
|
const config = () => ({
|
|
origin,
|
|
clientId,
|
|
keyId: "rotation-1",
|
|
token: "b".repeat(64),
|
|
notBefore: at(-60000),
|
|
notAfter: at(3600000),
|
|
});
|
|
function call(options = {}) {
|
|
const id = ++serial;
|
|
return new Promise((resolve, reject) => {
|
|
const timer = setTimeout(() => {
|
|
pending.delete(id);
|
|
reject(new Error("child test timed out"));
|
|
}, 18000);
|
|
pending.set(id, (value) => {
|
|
clearTimeout(timer);
|
|
resolve(value);
|
|
});
|
|
child.send({ id, config: config(), input: input(), ...options });
|
|
});
|
|
}
|
|
before(async function () {
|
|
folder = fs.mkdtempSync(path.join(os.tmpdir(), "review-intent-tls-"));
|
|
execFileSync(
|
|
"openssl",
|
|
[
|
|
"req",
|
|
"-x509",
|
|
"-newkey",
|
|
"rsa:2048",
|
|
"-nodes",
|
|
"-days",
|
|
"1",
|
|
"-subj",
|
|
"/CN=localhost",
|
|
"-addext",
|
|
"subjectAltName=IP:127.0.0.1,DNS:localhost",
|
|
"-keyout",
|
|
path.join(folder, "key"),
|
|
"-out",
|
|
path.join(folder, "cert"),
|
|
],
|
|
{ stdio: "ignore" },
|
|
);
|
|
server = https.createServer(
|
|
{
|
|
key: fs.readFileSync(path.join(folder, "key")),
|
|
cert: fs.readFileSync(path.join(folder, "cert")),
|
|
},
|
|
(req, res) => {
|
|
const chunks = [];
|
|
req.on("data", (chunk) => chunks.push(chunk));
|
|
req.on("end", () => {
|
|
calls.push({
|
|
method: req.method,
|
|
url: req.url,
|
|
headers: req.headers,
|
|
body: JSON.parse(Buffer.concat(chunks).toString()),
|
|
});
|
|
handler(req, res);
|
|
});
|
|
},
|
|
);
|
|
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
origin = "https://127.0.0.1:" + server.address().port;
|
|
child = fork(path.join(__dirname, "fixtures/review-intent-client.js"), {
|
|
env: { ...process.env, NODE_EXTRA_CA_CERTS: path.join(folder, "cert") },
|
|
stdio: ["ignore", "ignore", "pipe", "ipc"],
|
|
});
|
|
child.on("message", (message) => {
|
|
const done = pending.get(message.id);
|
|
pending.delete(message.id);
|
|
if (done) done(message);
|
|
});
|
|
});
|
|
after(async function () {
|
|
child?.kill();
|
|
server?.closeAllConnections();
|
|
if (server) await new Promise((resolve) => server.close(resolve));
|
|
if (folder) fs.rmSync(folder, { recursive: true, force: true });
|
|
});
|
|
beforeEach(() => {
|
|
calls = [];
|
|
handler = (_req, res) => {
|
|
res.setHeader("Content-Type", "application/json");
|
|
res.end(JSON.stringify(intent()));
|
|
};
|
|
});
|
|
it("uses authenticated TLS, the saved policy, and the caller's retry ID", async () => {
|
|
const result = await call({ mode: "mutate" });
|
|
expect(result.result.policy.access).to.equal("restricted-review");
|
|
expect(result.frozen).to.equal(true);
|
|
expect(result.serialized).to.equal("{}");
|
|
expect(calls).to.have.length(1);
|
|
expect(calls[0].body).to.deep.equal(input());
|
|
expect(calls[0].method).to.equal("POST");
|
|
expect(calls[0].url).to.equal("/service/v1/artifact-intents/consume");
|
|
expect(calls[0].headers.authorization).to.equal("Bearer " + "b".repeat(64));
|
|
expect(calls[0].headers["x-4open-artifact-client-id"]).to.equal(clientId);
|
|
expect(calls[0].headers["x-4open-artifact-service-key-id"]).to.equal(
|
|
"rotation-1",
|
|
);
|
|
for (const key of ["cookie", "origin", "referer"])
|
|
expect(calls[0].headers[key]).to.equal(undefined);
|
|
await call();
|
|
expect(calls[1].body.requestId).to.equal(calls[0].body.requestId);
|
|
});
|
|
it("rejects invalid config and caller data before making a request", async () => {
|
|
for (const patch of [
|
|
{ origin: origin + "/" },
|
|
{ origin: "http://127.0.0.1" },
|
|
{ origin: origin + "/path" },
|
|
{ token: "bad" },
|
|
{ notAfter: at(-1000) },
|
|
{ notBefore: at(30000) },
|
|
{ clientId: "bad" },
|
|
{ extra: "ignored?" },
|
|
])
|
|
expect((await call({ config: { ...config(), ...patch } })).kind).to.equal(
|
|
"invalid",
|
|
);
|
|
for (const patch of [
|
|
{ clientId: "0".repeat(32) },
|
|
{ token: "bad" },
|
|
{ requestId: "bad" },
|
|
{ policy: {} },
|
|
{ contract: "other" },
|
|
])
|
|
expect((await call({ input: { ...input(), ...patch } })).kind).to.equal(
|
|
"invalid",
|
|
);
|
|
expect((await call({ mode: "preabort" })).kind).to.equal("unavailable");
|
|
expect(calls).to.have.length(0);
|
|
});
|
|
it("classifies failures without following redirects, retrying, or exposing response bodies", async () => {
|
|
for (const [status, kind] of [
|
|
[301, "protocol"],
|
|
[400, "rejected"],
|
|
[401, "rejected"],
|
|
[403, "rejected"],
|
|
[404, "not-found"],
|
|
[409, "conflict"],
|
|
[410, "expired"],
|
|
[422, "rejected"],
|
|
[429, "unavailable"],
|
|
[503, "unavailable"],
|
|
]) {
|
|
handler = (_req, res) => {
|
|
res.writeHead(status, {
|
|
Location: origin + "/leak",
|
|
"Content-Length": "10000000",
|
|
});
|
|
res.write("private response and token");
|
|
};
|
|
const result = await call();
|
|
expect(result.kind).to.equal(kind);
|
|
expect(result.message).to.equal("Review intent consumption " + kind);
|
|
}
|
|
expect(calls).to.have.length(10);
|
|
});
|
|
it("rejects mismatched identity, policy, expiry, unknown fields and malformed JSON", async () => {
|
|
const good = intent();
|
|
const values = [
|
|
{ ...good, clientId: "0".repeat(32) },
|
|
{ ...good, intentId: "0".repeat(32) },
|
|
{ ...good, callbackId: "bad" },
|
|
{ ...good, entitlementId: "bad" },
|
|
{ ...good, expiresAt: at(-1000) },
|
|
{ ...good, expiresAt: at(700000) },
|
|
{ ...good, policy: { ...good.policy, access: "public" } },
|
|
{ ...good, policy: { ...good.policy, version: 1.5 } },
|
|
{ ...good, policy: { ...good.policy, retainUntil: at(-1000) } },
|
|
{ ...good, owner: "browser" },
|
|
];
|
|
const raw = [
|
|
...values.map((v) => JSON.stringify(v)),
|
|
"null",
|
|
"[]",
|
|
"{",
|
|
JSON.stringify(good).replace(
|
|
'"clientId":',
|
|
'"clientId":"' + clientId + '","cl\\u0069entId":',
|
|
),
|
|
JSON.stringify(good).replace('"version":1', '"version":2,"version":1'),
|
|
Buffer.from([0xff]),
|
|
" ".repeat(65537),
|
|
];
|
|
for (const body of raw) {
|
|
handler = (_req, res) => {
|
|
res.setHeader("Content-Type", "application/json");
|
|
res.end(body);
|
|
};
|
|
expect((await call()).kind).to.equal("protocol");
|
|
}
|
|
});
|
|
it("rejects cookies, encoding, ambiguous content types and oversized declared bodies", async () => {
|
|
for (const headers of [
|
|
{ "Set-Cookie": "identity=secret" },
|
|
{ "Content-Encoding": "gzip" },
|
|
{ "Content-Type": "text/html" },
|
|
{ "Content-Type": ["application/json", "application/json"] },
|
|
{ "Content-Length": "65537" },
|
|
]) {
|
|
handler = (_req, res) => {
|
|
res.writeHead(200, { "Content-Type": "application/json", ...headers });
|
|
res.write(JSON.stringify(intent()));
|
|
};
|
|
expect((await call()).kind).to.equal("protocol");
|
|
}
|
|
});
|
|
it("bounds concurrent attempts and releases capacity after completion", async () => {
|
|
handler = (_req, res) =>
|
|
setTimeout(() => {
|
|
res.setHeader("Content-Type", "application/json");
|
|
res.end(JSON.stringify(intent()));
|
|
}, 150);
|
|
const result = await call({ mode: "parallel" });
|
|
expect(result.results).to.deep.equal(["ok", "ok", "ok", "ok", "busy"]);
|
|
expect(result.followup.intentId).to.equal(intentId);
|
|
expect(calls).to.have.length(5);
|
|
});
|
|
it("cancels an unfinished response without retrying", async () => {
|
|
handler = (_req, res) => {
|
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
res.write("{");
|
|
};
|
|
expect((await call({ mode: "abort" })).kind).to.equal("unavailable");
|
|
expect(calls).to.have.length(1);
|
|
});
|
|
it("times out an inactive response within its request deadline", async () => {
|
|
handler = (_req, res) => {
|
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
res.write("{");
|
|
};
|
|
const start = Date.now();
|
|
expect((await call()).kind).to.equal("unavailable");
|
|
expect(Date.now() - start).to.be.within(4500, 12000);
|
|
expect(calls).to.have.length(1);
|
|
});
|
|
it("rejects an untrusted TLS certificate before sending credentials", async () => {
|
|
const env = {
|
|
...process.env,
|
|
NODE_EXTRA_CA_CERTS: "",
|
|
NODE_TLS_REJECT_UNAUTHORIZED: "1",
|
|
};
|
|
delete env.NODE_OPTIONS;
|
|
const untrusted = fork(
|
|
path.join(__dirname, "fixtures/review-intent-client.js"),
|
|
{ env, stdio: ["ignore", "ignore", "pipe", "ipc"] },
|
|
);
|
|
try {
|
|
const response = new Promise((resolve) =>
|
|
untrusted.once("message", resolve),
|
|
);
|
|
untrusted.send({ id: 1, config: config(), input: input() });
|
|
expect((await response).kind).to.equal("unavailable");
|
|
expect(calls).to.have.length(0);
|
|
} finally {
|
|
untrusted.kill();
|
|
}
|
|
});
|
|
it("rejects a response received after the credential expires", async () => {
|
|
handler = (_req, res) =>
|
|
setTimeout(() => {
|
|
res.setHeader("Content-Type", "application/json");
|
|
res.end(JSON.stringify(intent()));
|
|
}, 2100);
|
|
expect(
|
|
(await call({ config: { ...config(), notAfter: at(2000) } })).kind,
|
|
).to.equal("unavailable");
|
|
expect(calls).to.have.length(1);
|
|
});
|
|
it("enforces the total deadline even while the peer streams data", async () => {
|
|
handler = (_req, res) => {
|
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
res.write(" ");
|
|
const timer = setInterval(() => res.write(" "), 200);
|
|
res.once("close", () => clearInterval(timer));
|
|
};
|
|
const start = Date.now();
|
|
expect((await call()).kind).to.equal("unavailable");
|
|
expect(Date.now() - start).to.be.within(9500, 15000);
|
|
expect(calls).to.have.length(1);
|
|
});
|
|
it("measures repeated fresh TLS connections when explicitly requested", async function () {
|
|
if (!process.env.TEST_REVIEW_INTENT_PERF) this.skip();
|
|
const times = [];
|
|
for (let n = 0; n < 100; n++) {
|
|
const result = await call();
|
|
expect(result.result.intentId).to.equal(intentId);
|
|
times.push(result.elapsedMs);
|
|
}
|
|
times.sort((a, b) => a - b);
|
|
const receipt = {
|
|
calls: calls.length,
|
|
transport: "fresh loopback HTTPS connections",
|
|
synthetic: true,
|
|
meanMs: times.reduce((a, b) => a + b, 0) / times.length,
|
|
medianMs: times[50],
|
|
p95Ms: times[94],
|
|
};
|
|
expect(calls).to.have.length(100);
|
|
if (process.env.TEST_REVIEW_INTENT_PERF_REPORT)
|
|
fs.writeFileSync(
|
|
process.env.TEST_REVIEW_INTENT_PERF_REPORT,
|
|
JSON.stringify(receipt, null, 2) + "\n",
|
|
);
|
|
});
|
|
});
|