mirror of
https://github.com/oven-sh/bun
synced 2026-02-10 10:58:56 +00:00
407 lines
12 KiB
TypeScript
407 lines
12 KiB
TypeScript
import { expect, it } from "bun:test";
|
|
import { bunEnv, bunExe, tmpdirSync } from "harness";
|
|
import { join } from "node:path";
|
|
import tls from "node:tls";
|
|
|
|
type TLSOptions = {
|
|
cert: string;
|
|
key: string;
|
|
passphrase?: string;
|
|
};
|
|
|
|
import { expiredTls, invalidTls, tls as validTls } from "harness";
|
|
|
|
const CERT_LOCALHOST_IP = { ...validTls };
|
|
const CERT_EXPIRED = { ...expiredTls };
|
|
|
|
// Note: Do not use bun.sh as the example domain
|
|
// Cloudflare sometimes blocks automated requests to it.
|
|
// so it will cause flaky tests.
|
|
async function createServer(cert: TLSOptions, callback: (port: number) => Promise<any>) {
|
|
using server = Bun.serve({
|
|
port: 0,
|
|
tls: cert,
|
|
fetch() {
|
|
return new Response("Hello World");
|
|
},
|
|
});
|
|
await callback(server.port);
|
|
}
|
|
|
|
it("can handle multiple requests with non native checkServerIdentity", async () => {
|
|
async function request() {
|
|
let called = false;
|
|
const result = await fetch("https://www.example.com", {
|
|
keepalive: false,
|
|
tls: {
|
|
checkServerIdentity(hostname: string, cert: tls.PeerCertificate) {
|
|
called = true;
|
|
return tls.checkServerIdentity(hostname, cert);
|
|
},
|
|
},
|
|
}).then((res: Response) => res.blob());
|
|
expect(result?.size).toBeGreaterThan(0);
|
|
expect(called).toBe(true);
|
|
}
|
|
const promises = [];
|
|
for (let i = 0; i < 5; i++) {
|
|
promises.push(request());
|
|
}
|
|
await Promise.all(promises);
|
|
});
|
|
|
|
it("fetch with valid tls should not throw", async () => {
|
|
const promises = [`https://example.com`, `https://www.example.com`].map(async url => {
|
|
const result = await fetch(url, { keepalive: false }).then((res: Response) => res.blob());
|
|
expect(result?.size).toBeGreaterThan(0);
|
|
});
|
|
|
|
await Promise.all(promises);
|
|
});
|
|
|
|
it("fetch with valid tls and non-native checkServerIdentity should work", async () => {
|
|
for (const isBusy of [true, false]) {
|
|
let count = 0;
|
|
const promises = [`https://example.com`, `https://www.example.com`].map(async url => {
|
|
await fetch(url, {
|
|
keepalive: false,
|
|
tls: {
|
|
checkServerIdentity(hostname: string, cert: tls.PeerCertificate) {
|
|
count++;
|
|
expect(url).toContain(hostname);
|
|
return tls.checkServerIdentity(hostname, cert);
|
|
},
|
|
},
|
|
}).then((res: Response) => res.blob());
|
|
});
|
|
if (isBusy) {
|
|
const start = performance.now();
|
|
while (performance.now() - start < 500) {}
|
|
}
|
|
await Promise.all(promises);
|
|
expect(count).toBe(2);
|
|
}
|
|
});
|
|
|
|
it("fetch with valid tls and non-native checkServerIdentity should work", async () => {
|
|
let count = 0;
|
|
const promises = [`https://example.com`, `https://www.example.com`].map(async url => {
|
|
await fetch(url, {
|
|
keepalive: false,
|
|
tls: {
|
|
checkServerIdentity(hostname: string, cert: tls.PeerCertificate) {
|
|
count++;
|
|
expect(url).toContain(hostname);
|
|
throw new Error("CustomError");
|
|
},
|
|
},
|
|
});
|
|
});
|
|
const start = performance.now();
|
|
while (performance.now() - start < 1000) {}
|
|
expect((await Promise.allSettled(promises)).every(p => p.status === "rejected")).toBe(true);
|
|
expect(count).toBe(2);
|
|
});
|
|
|
|
it("fetch with rejectUnauthorized: false should not call checkServerIdentity", async () => {
|
|
let count = 0;
|
|
|
|
await fetch("https://example.com", {
|
|
keepalive: false,
|
|
tls: {
|
|
rejectUnauthorized: false,
|
|
checkServerIdentity(hostname: string, cert: tls.PeerCertificate) {
|
|
count++;
|
|
return tls.checkServerIdentity(hostname, cert);
|
|
},
|
|
},
|
|
}).then((res: Response) => res.blob());
|
|
expect(count).toBe(0);
|
|
});
|
|
|
|
it("fetch with self-sign tls should throw", async () => {
|
|
await createServer(CERT_LOCALHOST_IP, async port => {
|
|
const urls = [`https://localhost:${port}`, `https://127.0.0.1:${port}`];
|
|
await Promise.all(
|
|
urls.map(async url => {
|
|
try {
|
|
await fetch(url).then((res: Response) => res.blob());
|
|
expect.unreachable();
|
|
} catch (e: any) {
|
|
expect(e.code).toBe("DEPTH_ZERO_SELF_SIGNED_CERT");
|
|
}
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
|
|
it("fetch with invalid tls should throw", async () => {
|
|
await createServer(CERT_EXPIRED, async port => {
|
|
await Promise.all(
|
|
[`https://localhost:${port}`, `https://127.0.0.1:${port}`].map(async url => {
|
|
try {
|
|
await fetch(url).then((res: Response) => res.blob());
|
|
expect.unreachable();
|
|
} catch (e: any) {
|
|
expect(e.code).toBe("CERT_HAS_EXPIRED");
|
|
}
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
|
|
it("fetch with checkServerIdentity failing should throw", async () => {
|
|
try {
|
|
await fetch(`https://example.com`, {
|
|
keepalive: false,
|
|
tls: {
|
|
checkServerIdentity() {
|
|
return new Error("CustomError");
|
|
},
|
|
},
|
|
}).then((res: Response) => res.blob());
|
|
|
|
expect.unreachable();
|
|
} catch (e: any) {
|
|
expect(e.message).toBe("CustomError");
|
|
}
|
|
});
|
|
|
|
it("fetch with self-sign certificate tls + rejectUnauthorized: false should not throw", async () => {
|
|
await createServer(CERT_LOCALHOST_IP, async port => {
|
|
const urls = [`https://localhost:${port}`, `https://127.0.0.1:${port}`];
|
|
await Promise.all(
|
|
urls.map(async url => {
|
|
try {
|
|
const result = await fetch(url, { tls: { rejectUnauthorized: false } }).then((res: Response) => res.text());
|
|
expect(result).toBe("Hello World");
|
|
} catch {
|
|
expect.unreachable();
|
|
}
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
|
|
it("fetch with invalid tls + rejectUnauthorized: false should not throw", async () => {
|
|
await createServer(CERT_EXPIRED, async port => {
|
|
const urls = [`https://localhost:${port}`, `https://127.0.0.1:${port}`];
|
|
await Promise.all(
|
|
urls.map(async url => {
|
|
try {
|
|
const result = await fetch(url, { tls: { rejectUnauthorized: false } }).then((res: Response) => res.text());
|
|
expect(result).toBe("Hello World");
|
|
} catch (e) {
|
|
expect.unreachable();
|
|
}
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
|
|
it("fetch should respect rejectUnauthorized env", async () => {
|
|
await createServer(CERT_EXPIRED, async port => {
|
|
const url = `https://localhost:${port}`;
|
|
|
|
const promises = [];
|
|
for (let i = 0; i < 2; i++) {
|
|
const proc = Bun.spawn({
|
|
env: {
|
|
...bunEnv,
|
|
SERVER: url,
|
|
NODE_TLS_REJECT_UNAUTHORIZED: i.toString(),
|
|
},
|
|
stderr: "inherit",
|
|
stdout: "inherit",
|
|
stdin: "inherit",
|
|
cmd: [bunExe(), join(import.meta.dir, "fetch-reject-authorized-env-fixture.js")],
|
|
});
|
|
|
|
promises.push(proc.exited);
|
|
}
|
|
|
|
const [exitCode1, exitCode2] = await Promise.all(promises);
|
|
expect(exitCode1).toBe(0);
|
|
expect(exitCode2).toBe(1);
|
|
});
|
|
});
|
|
|
|
it("fetch timeout works on tls", async () => {
|
|
using server = Bun.serve({
|
|
tls: validTls,
|
|
hostname: "localhost",
|
|
port: 0,
|
|
rejectUnauthorized: false,
|
|
async fetch() {
|
|
async function* body() {
|
|
yield "Hello, ";
|
|
await Bun.sleep(700); // should only take 200ms-350ms
|
|
yield "World!";
|
|
}
|
|
return new Response(body);
|
|
},
|
|
});
|
|
const start = performance.now();
|
|
const TIMEOUT = 200;
|
|
const THRESHOLD = 150;
|
|
|
|
try {
|
|
await fetch(server.url, {
|
|
signal: AbortSignal.timeout(TIMEOUT),
|
|
tls: { ca: validTls.cert },
|
|
}).then(res => res.text());
|
|
expect.unreachable();
|
|
} catch (e) {
|
|
expect(e.name).toBe("TimeoutError");
|
|
} finally {
|
|
const total = performance.now() - start;
|
|
expect(total).toBeGreaterThanOrEqual(TIMEOUT - THRESHOLD);
|
|
expect(total).toBeLessThanOrEqual(TIMEOUT + THRESHOLD);
|
|
}
|
|
});
|
|
for (const timeout of [0, 1, 10, 20, 100, 300]) {
|
|
it(`fetch should abort as soon as possible under tls using AbortSignal.timeout(${timeout})`, async () => {
|
|
using server = Bun.serve({
|
|
port: 0,
|
|
tls: CERT_LOCALHOST_IP,
|
|
async fetch() {
|
|
await Bun.sleep(1000);
|
|
return new Response("Hello World");
|
|
},
|
|
});
|
|
const THRESHOLD = 50;
|
|
|
|
const time = performance.now();
|
|
try {
|
|
await fetch(server.url, {
|
|
//@ts-ignore
|
|
tls: { ca: CERT_LOCALHOST_IP.cert },
|
|
signal: AbortSignal.timeout(timeout),
|
|
}).then(res => res.text());
|
|
expect.unreachable();
|
|
} catch (err) {
|
|
expect((err as Error).name).toBe("TimeoutError");
|
|
} finally {
|
|
const diff = performance.now() - time;
|
|
expect(diff).toBeLessThanOrEqual(timeout + THRESHOLD);
|
|
expect(diff).toBeGreaterThanOrEqual(timeout - THRESHOLD);
|
|
}
|
|
});
|
|
}
|
|
|
|
it("fetch should use NODE_EXTRA_CA_CERTS", async () => {
|
|
using server = Bun.serve({
|
|
port: 0,
|
|
tls: validTls,
|
|
fetch() {
|
|
return new Response("OK");
|
|
},
|
|
});
|
|
const cert_path = join(tmpdirSync(), "cert.pem");
|
|
await Bun.write(cert_path, validTls.cert);
|
|
|
|
const proc = Bun.spawn({
|
|
env: {
|
|
...bunEnv,
|
|
SERVER: server.url,
|
|
NODE_EXTRA_CA_CERTS: cert_path,
|
|
},
|
|
stderr: "inherit",
|
|
stdout: "inherit",
|
|
stdin: "inherit",
|
|
cmd: [bunExe(), join(import.meta.dir, "fetch.tls.extra-cert.fixture.js")],
|
|
});
|
|
|
|
expect(await proc.exited).toBe(0);
|
|
});
|
|
|
|
it("fetch should use NODE_EXTRA_CA_CERTS even if the used CA is not first in bundle", async () => {
|
|
using server = Bun.serve({
|
|
port: 0,
|
|
tls: validTls,
|
|
fetch() {
|
|
return new Response("OK");
|
|
},
|
|
});
|
|
|
|
const bundlePath = join(tmpdirSync(), "bundle.pem");
|
|
const bundleContent = `${expiredTls.cert}\n${validTls.cert}`;
|
|
await Bun.write(bundlePath, bundleContent);
|
|
|
|
const proc = Bun.spawn({
|
|
env: {
|
|
...bunEnv,
|
|
SERVER: server.url,
|
|
NODE_EXTRA_CA_CERTS: bundlePath,
|
|
},
|
|
stderr: "inherit",
|
|
stdout: "inherit",
|
|
stdin: "inherit",
|
|
cmd: [bunExe(), join(import.meta.dir, "fetch.tls.extra-cert.fixture.js")],
|
|
});
|
|
|
|
expect(await proc.exited).toBe(0);
|
|
});
|
|
|
|
it("fetch should ignore invalid NODE_EXTRA_CA_CERTS", async () => {
|
|
using server = Bun.serve({
|
|
port: 0,
|
|
tls: validTls,
|
|
fetch() {
|
|
return new Response("OK");
|
|
},
|
|
});
|
|
|
|
for (const invalid of ["not-exist.pem", "", " "]) {
|
|
const proc = Bun.spawn({
|
|
env: {
|
|
...bunEnv,
|
|
SERVER: server.url,
|
|
NODE_EXTRA_CA_CERTS: invalid,
|
|
},
|
|
stderr: "pipe",
|
|
stdout: "inherit",
|
|
stdin: "inherit",
|
|
cmd: [bunExe(), join(import.meta.dir, "fetch.tls.extra-cert.fixture.js")],
|
|
});
|
|
|
|
expect(await proc.exited).toBe(1);
|
|
expect(await Bun.readableStreamToText(proc.stderr)).toContain("DEPTH_ZERO_SELF_SIGNED_CERT");
|
|
}
|
|
});
|
|
|
|
it("fetch should ignore NODE_EXTRA_CA_CERTS if it's contains invalid cert", async () => {
|
|
using server = Bun.serve({
|
|
port: 0,
|
|
tls: validTls,
|
|
fetch() {
|
|
return new Response("OK");
|
|
},
|
|
});
|
|
|
|
const mixedValidAndInvalidCertsBundlePath = join(tmpdirSync(), "mixed-valid-and-invalid-certs-bundle.pem");
|
|
await Bun.write(mixedValidAndInvalidCertsBundlePath, `${invalidTls.cert}\n${validTls.cert}`);
|
|
|
|
const mixedInvalidAndValidCertsBundlePath = join(tmpdirSync(), "mixed-invalid-and-valid-certs-bundle.pem");
|
|
await Bun.write(mixedInvalidAndValidCertsBundlePath, `${validTls.cert}\n${invalidTls.cert}`);
|
|
|
|
for (const invalid of [mixedValidAndInvalidCertsBundlePath, mixedInvalidAndValidCertsBundlePath]) {
|
|
const proc = Bun.spawn({
|
|
env: {
|
|
...bunEnv,
|
|
SERVER: server.url,
|
|
NODE_EXTRA_CA_CERTS: invalid,
|
|
},
|
|
stderr: "pipe",
|
|
stdout: "inherit",
|
|
stdin: "inherit",
|
|
cmd: [bunExe(), join(import.meta.dir, "fetch.tls.extra-cert.fixture.js")],
|
|
});
|
|
|
|
expect(await proc.exited).toBe(1);
|
|
const stderr = await Bun.readableStreamToText(proc.stderr);
|
|
expect(stderr).toContain("DEPTH_ZERO_SELF_SIGNED_CERT");
|
|
expect(stderr).toContain("ignoring extra certs");
|
|
}
|
|
});
|