Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions docs/runtime/http/server.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -705,6 +705,9 @@ interface WebSocketHandler<T = undefined> {
}

interface TLSOptions {
/** Accept an intermediate certificate listed in `ca` as a trust anchor */
allowPartialTrustChain?: boolean;

/** Certificate authority chain */
ca?: string | Buffer | BunFile | Array<string | Buffer | BunFile>;

Expand All @@ -714,6 +717,9 @@ interface TLSOptions {
/** Path to DH parameters file */
dhParamsFile?: string;

/** Key agreement group, or colon-separated list of groups (e.g. "X25519:P-256") */
ecdhCurve?: string;

/** Private key */
key?: string | Buffer | BunFile | Array<string | Buffer | BunFile>;

Expand All @@ -728,5 +734,11 @@ interface TLSOptions {

/** Server name for SNI */
serverName?: string;

/** Seconds a TLS 1.2 session stays resumable (0 = BoringSSL's default) */
sessionTimeout?: number;

/** Colon-separated signature algorithms (e.g. "rsa_pss_rsae_sha256:ecdsa_secp256r1_sha256") */
sigalgs?: string;
}
```
50 changes: 50 additions & 0 deletions packages/bun-types/bun.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4259,6 +4259,56 @@ declare module "bun" {
clientRenegotiationLimit?: number;

clientRenegotiationWindow?: number;

/**
* Treat the intermediate (non self-signed) certificates in `ca` as trust
* anchors, so a peer certificate whose chain ends at one of them verifies
* even though the root that issued that intermediate is not in `ca`.
* Without it such a chain fails with `UNABLE_TO_GET_ISSUER_CERT`.
*
* Applies to whichever certificate this side verifies: the server's
* certificate when connecting, or the client's certificate when a server
* sets `requestCert`.
*
* @default false
*/
allowPartialTrustChain?: boolean | undefined;

/**
* Lifetime in seconds of the TLS 1.2 sessions created with these options,
* i.e. how long a peer can resume one of them. Must be an integer; `0`
* keeps BoringSSL's default of two hours. TLS 1.3 session tickets are not
* affected by this option.
*
* @default 0
*/
sessionTimeout?: number | undefined;

/**
* Colon-separated list of the signature algorithms this side signs with
* and accepts from the peer, replacing BoringSSL's default list. Entries
* are TLS 1.3 scheme names such as `"rsa_pss_rsae_sha256"` or
* `"ecdsa_secp256r1_sha256"`, or OpenSSL `"<signature>+<digest>"` pairs
* such as `"RSA-PSS+SHA256"`.
*
* The handshake fails when the peer, or the key behind this side's own
* `cert`, supports none of the listed algorithms. A list containing an
* unknown name is rejected when the options are applied.
*/
sigalgs?: string | undefined;

/**
* Named group, or colon-separated list of groups in order of preference,
* to use for the handshake's key agreement, such as `"P-256"` or
* `"X25519:P-256:P-384"`. OpenSSL aliases like `"prime256v1"` and
* `"secp384r1"` are accepted as well. `"auto"` (the `node:tls` default)
* keeps BoringSSL's default list.
*
* The handshake fails when the peer supports none of the listed groups. A
* list containing an unknown name is rejected when the options are
* applied.
*/
ecdhCurve?: string | undefined;
}

interface SocketAddress {
Expand Down
157 changes: 130 additions & 27 deletions test/integration/bun-types/bun-types.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -367,36 +367,139 @@ describe("@types/bun integration test", () => {
});
});

// Runs on debug builds too: spawning tsc over a single file is cheap,
// unlike the in-process LanguageService runs above.
describe("Bun.mmap", () => {
test("MMapOptions accepts offset and size", async () => {
const checkDir = join(TEMP_DIR, "mmap-options-check");
const tsconfig = structuredClone(sourceTsconfig);
tsconfig.include = ["mmap-options.ts"];
tsconfig.compilerOptions.typeRoots = [join(BASE_FIXTURE_DIR, "node_modules", "@types")];
await mkdir(checkDir, { recursive: true });
await makeTree(checkDir, {
"tsconfig.json": JSON.stringify(tsconfig, null, 2),
"mmap-options.ts": `const view = Bun.mmap("./data.bin", { shared: true, sync: false, offset: 4096, size: 1024 });
view satisfies Uint8Array<ArrayBuffer>;
Bun.mmap("./data.bin", { offset: 4096 }) satisfies Uint8Array<ArrayBuffer>;
Bun.mmap("./data.bin", { size: 1024 }) satisfies Uint8Array<ArrayBuffer>;`,
});
// The tests below run on debug builds too: spawning tsc over a single file
// is cheap, unlike the in-process LanguageService runs above.
async function expectSingleFileToTypeCheck(name: string, source: string) {
const checkDir = join(TEMP_DIR, `${name}-check`);
const fileName = `${name}.ts`;
const tsconfig = structuredClone(sourceTsconfig);
tsconfig.include = [fileName];
tsconfig.compilerOptions.typeRoots = [join(BASE_FIXTURE_DIR, "node_modules", "@types")];
await mkdir(checkDir, { recursive: true });
await makeTree(checkDir, {
"tsconfig.json": JSON.stringify(tsconfig, null, 2),
[fileName]: source,
});

await using proc = Bun.spawn({
cmd: [bunExe(), join(BASE_FIXTURE_DIR, "node_modules", "typescript", "bin", "tsc"), "-p", "."],
env: bunEnv,
cwd: checkDir,
stdout: "pipe",
stderr: "pipe",
});
await using proc = Bun.spawn({
cmd: [bunExe(), join(BASE_FIXTURE_DIR, "node_modules", "typescript", "bin", "tsc"), "-p", "."],
env: bunEnv,
cwd: checkDir,
stdout: "pipe",
stderr: "pipe",
});

const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);

expect(stderr.trim()).toBe("");
expect(stdout.trim()).toBe("");
expect(exitCode).toBe(0);
expect(stderr.trim()).toBe("");
expect(stdout.trim()).toBe("");
expect(exitCode).toBe(0);
}

describe("Bun.mmap", () => {
test("MMapOptions accepts offset and size", async () => {
await expectSingleFileToTypeCheck(
"mmap-options",
`const view = Bun.mmap("./data.bin", { shared: true, sync: false, offset: 4096, size: 1024 });
view satisfies Uint8Array<ArrayBuffer>;
Bun.mmap("./data.bin", { offset: 4096 }) satisfies Uint8Array<ArrayBuffer>;
Bun.mmap("./data.bin", { size: 1024 }) satisfies Uint8Array<ArrayBuffer>;`,
);
});
});

describe("Bun.TLSOptions", () => {
test("declares allowPartialTrustChain, sessionTimeout, sigalgs and ecdhCurve on every API that takes a tls object", async () => {
await expectSingleFileToTypeCheck(
"tls-options-context-options",
`({ allowPartialTrustChain: true }) satisfies Bun.TLSOptions;
({ allowPartialTrustChain: undefined }) satisfies Bun.TLSOptions;
({ sessionTimeout: 300 }) satisfies Bun.TLSOptions;
({ sessionTimeout: undefined }) satisfies Bun.TLSOptions;
({ sigalgs: "rsa_pss_rsae_sha256:ecdsa_secp256r1_sha256" }) satisfies Bun.TLSOptions;
({ sigalgs: undefined }) satisfies Bun.TLSOptions;
({ ecdhCurve: "X25519:P-256" }) satisfies Bun.TLSOptions;
({ ecdhCurve: "auto" }) satisfies Bun.TLSOptions;
({ ecdhCurve: undefined }) satisfies Bun.TLSOptions;

// The runtime converter is strict about each value type (a truthy
// non-boolean, a numeric string, ... are rejected), so the types are too.
// @ts-expect-error allowPartialTrustChain is a boolean
({ allowPartialTrustChain: 1 }) satisfies Bun.TLSOptions;
// @ts-expect-error sessionTimeout is a number of seconds
({ sessionTimeout: "300" }) satisfies Bun.TLSOptions;
// @ts-expect-error sigalgs is a colon-separated string, not an array
({ sigalgs: ["rsa_pss_rsae_sha256"] }) satisfies Bun.TLSOptions;
// @ts-expect-error ecdhCurve is a colon-separated string, not an array
({ ecdhCurve: ["P-256"] }) satisfies Bun.TLSOptions;

// Written out per call rather than spread from a shared object: excess
// property checks, which is what catches an undeclared option, only
// apply to properties written inline in the literal.
Bun.serve({
fetch: () => new Response(),
tls: {
key: "key",
cert: "cert",
ca: "intermediate",
requestCert: true,
allowPartialTrustChain: true,
sessionTimeout: 300,
sigalgs: "rsa_pss_rsae_sha256",
ecdhCurve: "P-256",
},
});
Bun.serve({
fetch: () => new Response(),
tls: [
{ key: "key", cert: "cert", sessionTimeout: 60, sigalgs: "rsa_pss_rsae_sha256" },
{ serverName: "a.example.com", key: "key", cert: "cert", ecdhCurve: "P-384", allowPartialTrustChain: true },
],
});
Bun.listen({
hostname: "localhost",
port: 0,
socket: { data() {} },
tls: {
key: "key",
cert: "cert",
allowPartialTrustChain: true,
sessionTimeout: 300,
sigalgs: "rsa_pss_rsae_sha256",
ecdhCurve: "P-256",
},
});
Bun.connect({
hostname: "localhost",
port: 0,
socket: { data() {} },
tls: {
ca: "intermediate",
allowPartialTrustChain: true,
sessionTimeout: 300,
sigalgs: "rsa_pss_rsae_sha256",
ecdhCurve: "P-256",
},
});
fetch("https://localhost", {
tls: {
ca: "intermediate",
allowPartialTrustChain: true,
sessionTimeout: 300,
sigalgs: "rsa_pss_rsae_sha256",
ecdhCurve: "P-256",
},
});
new WebSocket("wss://localhost", {
tls: {
ca: "intermediate",
allowPartialTrustChain: true,
sessionTimeout: 300,
sigalgs: "rsa_pss_rsae_sha256",
ecdhCurve: "P-256",
},
});`,
);
});
});

Expand Down
28 changes: 28 additions & 0 deletions test/integration/bun-types/fixture/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -332,3 +332,31 @@ if (typeof process !== "undefined") {
// @ts-expect-error - Proxy must be string or object, not array
fetch("https://example.com", { proxy: ["http://proxy.example.com"] });
}

{
// TLS context options shared with Bun.serve / Bun.listen / Bun.connect
fetch("https://example.com", { tls: { ca: "intermediate", allowPartialTrustChain: true } });
fetch("https://example.com", { tls: { sessionTimeout: 300 } });
fetch("https://example.com", { tls: { sigalgs: "rsa_pss_rsae_sha256:ecdsa_secp256r1_sha256" } });
fetch("https://example.com", { tls: { ecdhCurve: "X25519:P-256" } });
}

{
// @ts-expect-error - allowPartialTrustChain is a boolean
fetch("https://example.com", { tls: { allowPartialTrustChain: "yes" } });
}

{
// @ts-expect-error - sessionTimeout is a number of seconds
fetch("https://example.com", { tls: { sessionTimeout: "300" } });
}

{
// @ts-expect-error - sigalgs is a colon-separated string, not an array
fetch("https://example.com", { tls: { sigalgs: ["rsa_pss_rsae_sha256"] } });
}

{
// @ts-expect-error - ecdhCurve is a colon-separated string, not an array
fetch("https://example.com", { tls: { ecdhCurve: ["X25519", "P-256"] } });
}
42 changes: 42 additions & 0 deletions test/integration/bun-types/fixture/serve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,45 @@ const s4 = Bun.serve({
},
},
});

Bun.serve({
fetch: () => new Response("hello"),
tls: {
key: Bun.file("key.pem"),
cert: Bun.file("cert.pem"),
ca: Bun.file("intermediate-ca.pem"),
requestCert: true,
allowPartialTrustChain: true,
sessionTimeout: 300,
sigalgs: "rsa_pss_rsae_sha256:ecdsa_secp256r1_sha256",
ecdhCurve: "X25519:P-256",
},
});

Bun.serve({
fetch: () => new Response("hello"),
tls: [
{ key: "key", cert: "cert", sessionTimeout: 60, sigalgs: "rsa_pss_rsae_sha256" },
{ serverName: "p384.example.com", key: "key", cert: "cert", ecdhCurve: "P-384", allowPartialTrustChain: true },
],
});

Bun.serve({
fetch: () => new Response("hello"),
// @ts-expect-error - allowPartialTrustChain is a boolean
tls: {
key: "key",
cert: "cert",
allowPartialTrustChain: "yes",
},
});

Bun.serve({
fetch: () => new Response("hello"),
// @ts-expect-error - sessionTimeout is a number of seconds
tls: {
key: "key",
cert: "cert",
sessionTimeout: "300",
},
});
42 changes: 42 additions & 0 deletions test/integration/bun-types/fixture/tcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,48 @@ Bun.listen({
},
});

Bun.listen({
data: { arg: "asdf" },
socket: {
data(socket) {
socket.data.arg.toLowerCase();
},
open() {
console.log("asdf");
},
},
hostname: "adsf",
port: 324,
tls: {
cert: "asdf",
key: Bun.file("adsf"),
ca: Buffer.from("asdf"),
requestCert: true,
allowPartialTrustChain: true,
sessionTimeout: 300,
sigalgs: "rsa_pss_rsae_sha256:ecdsa_secp256r1_sha256",
ecdhCurve: "X25519:P-256",
},
});

await Bun.connect({
data: { arg: "asdf" },
socket: {
data(socket) {
socket.data.arg.toLowerCase();
},
},
hostname: "adsf",
port: 324,
tls: {
ca: Bun.file("asdf"),
allowPartialTrustChain: true,
sessionTimeout: 300,
sigalgs: "rsa_pss_rsae_sha256",
ecdhCurve: "auto",
},
});

Bun.listen({
data: { arg: "asdf" },
socket: {
Expand Down