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
10 changes: 10 additions & 0 deletions packages/bun-usockets/src/crypto/openssl.c
Original file line number Diff line number Diff line change
Expand Up @@ -1522,6 +1522,16 @@ void us_internal_ssl_ctx_unref(SSL_CTX *p) {
if (p) SSL_CTX_free(p);
}

/* Clears the per-domain userdata (uWS HttpRouter*) stored on a SNI SSL_CTX.
* App.h::removeServerName() deletes the router while live keep-alive
* connections may still hold a ref on this SSL_CTX via SSL_set_SSL_CTX();
* without clearing, the next request on such a connection would read the
* freed router through us_socket_server_name_userdata(). After clearing,
* HttpContext.h falls back to the default router. */
void us_internal_ssl_ctx_clear_sni_userdata(struct ssl_ctx_st *p) {
if (p && us_sni_ex_idx >= 0) SSL_CTX_set_ex_data(p, us_sni_ex_idx, NULL);
}

/* ── Per-socket SSL attach/detach ────────────────────────────────────────── */

void us_internal_ssl_attach(struct us_socket_t *s, SSL_CTX *ctx,
Expand Down
1 change: 1 addition & 0 deletions packages/bun-usockets/src/libusockets.h
Original file line number Diff line number Diff line change
Expand Up @@ -554,6 +554,7 @@ struct ssl_ctx_st *us_ssl_ctx_from_options(
* (uWS App.h) that don't pull in BoringSSL headers. */
void us_internal_ssl_ctx_up_ref(struct ssl_ctx_st *ssl_ctx);
void us_internal_ssl_ctx_unref(struct ssl_ctx_st *ssl_ctx);
void us_internal_ssl_ctx_clear_sni_userdata(struct ssl_ctx_st *ssl_ctx);
long us_ssl_ctx_live_count(void);
/* Appends the certificates in the PEM `content` to `ctx`'s trust store;
* returns 0 when nothing could be added. */
Expand Down
8 changes: 8 additions & 0 deletions packages/bun-uws/src/App.h
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,14 @@ struct TemplatedApp {
});
for (auto it = pendingServerNames.begin(); it != pendingServerNames.end(); ) {
if (it->hostname == hostname_pattern) {
/* Live keep-alive connections accepted under this SNI
* still hold a ref on it->ctx (via SSL_set_SSL_CTX in
* sni_cb) and would read the freed router through
* us_socket_server_name_userdata() on their next
* request. Clear the ex_data slot so those connections
* fall back to the default router instead of
* dereferencing freed memory. */
us_internal_ssl_ctx_clear_sni_userdata(it->ctx);
us_internal_ssl_ctx_unref(it->ctx);
delete it->router;
it = pendingServerNames.erase(it);
Expand Down
2 changes: 2 additions & 0 deletions src/js/internal/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ const serverSymbol = Symbol.for("::bunternal::");
const kPendingCallbacks = Symbol("pendingCallbacks");
const kRequest = Symbol("request");
const kCloseCallback = Symbol("closeCallback");
const kSNIContexts = Symbol("sniContexts");

export const enum ClientRequestEmitState {
socket = 1,
Expand Down Expand Up @@ -597,6 +598,7 @@ export {
kRequest,
kRes,
kReusedSocket,
kSNIContexts,
kSignal,
kSocketPath,
kTimeoutTimer,
Expand Down
103 changes: 103 additions & 0 deletions src/js/internal/tls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,9 +178,112 @@ function processPfxOptions(options) {
return out;
}

// Node.js only requests a client certificate when `requestCert: true`.
// The uSockets SSL context treats `ca` alone as "verify peer", so without
// these two flags an `https.Server({ ca })` would reject every client that
// doesn't present a cert. Mirror tls.Server (net.ts): default `requestCert`
// to false and, when not requesting, force `rejectUnauthorized` to false so
// the CA is loaded into the trust store without requiring a client cert.
Comment thread
robobun marked this conversation as resolved.
function normalizeServerTls(tls) {
const requestCert = !!tls.requestCert;
tls.requestCert = requestCert;
tls.rejectUnauthorized = requestCert ? tls.rejectUnauthorized !== false : false;
return tls;
}

/**
* Turns the TLS options Node's http.Server / https.Server accept (in the
* constructor, `setSecureContext()` and `addContext()`) into the `tls` object
* handed to `Bun.serve`. Every option is validated before anything is built,
* so a throw leaves the caller's current config untouched.
*
* Returns `null` when the options carry no key material (pfx/cert/key/ca)
* unless `alwaysTls` is set: a plain http.Server only becomes a TLS server
* when given key material, while an https.Server is one regardless.
*/
Comment thread
robobun marked this conversation as resolved.
function serverTlsFromOptions(options, alwaysTls: boolean) {
let hasKeyMaterial = false;
let tlsOptions = options;
if (options.pfx) {
tlsOptions = processPfxOptions(options);
hasKeyMaterial = true;
}

const cert = tlsOptions.cert;
if (cert) {
throwOnInvalidTLSArray("options.cert", cert);
hasKeyMaterial = true;
}

const key = tlsOptions.key;
if (key) {
throwOnInvalidTLSArray("options.key", key);
hasKeyMaterial = true;
}

let ca = tlsOptions.ca;
// PKCS#12-embedded CAs extend the trust set; the server path hands raw
// {key, cert, ca} to the native config and has no addCACert hook, so fold
// them into `ca` (mirrors tls.Server.setSecureContext).
Comment thread
robobun marked this conversation as resolved.
const pfxExtraCAs = tlsOptions._pfxExtraCACerts;
if (pfxExtraCAs?.length) {
ca = ca == null ? pfxExtraCAs : $isArray(ca) ? [...ca, ...pfxExtraCAs] : [ca, ...pfxExtraCAs];
}
if (ca) {
throwOnInvalidTLSArray("options.ca", ca);
hasKeyMaterial = true;
}

const passphrase = options.passphrase;
if (passphrase && typeof passphrase !== "string") {
throw $ERR_INVALID_ARG_TYPE("options.passphrase", "string", passphrase);
}

const serverName = options.servername;
if (serverName && typeof serverName !== "string") {
throw $ERR_INVALID_ARG_TYPE("options.servername", "string", serverName);
}

const secureOptions = options.secureOptions || 0;
if (secureOptions && typeof secureOptions !== "number") {
throw $ERR_INVALID_ARG_TYPE("options.secureOptions", "number", secureOptions);
}

if (!hasKeyMaterial && !alwaysTls) return null;

// Translate minVersion/maxVersion/secureProtocol into the integer protocol
// range the native layer applies (secureProtocol wins, like Node's
// SecureContext::Init); 0 keeps the native defaults.
Comment thread
robobun marked this conversation as resolved.
validateSecureProtocol(options.secureProtocol);
let minVersion, maxVersion;
const range = secureProtocolToVersionRange(options.secureProtocol);
if (range) {
minVersion = range[0];
maxVersion = range[1];
} else {
minVersion = tlsStringToProtocolVersion(options.minVersion);
maxVersion = tlsStringToProtocolVersion(options.maxVersion);
}
return normalizeServerTls({
serverName,
key,
cert,
ca,
passphrase,
secureOptions,
minVersion,
maxVersion,
ciphers: typeof options.ciphers === "string" && options.ciphers ? options.ciphers : undefined,
requestCert: options.requestCert,
rejectUnauthorized: options.rejectUnauthorized,
});
}

export {
normalizeServerTls,
processPfxOptions,
secureProtocolToVersionRange,
serverTlsFromOptions,
throwOnInvalidTLSArray,
tlsStringToProtocolVersion,
validateSecureProtocol,
Expand Down
117 changes: 19 additions & 98 deletions src/js/node/_http_server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,15 @@ const { ConnResetException, hasObserver, startPerf, stopPerf, kInternalSendOptio
const kServerResponseStatistics = Symbol("ServerResponseStatistics");

const { isPrimary } = require("internal/cluster/isPrimary");
const {
throwOnInvalidTLSArray,
tlsStringToProtocolVersion,
secureProtocolToVersionRange,
processPfxOptions,
validateSecureProtocol,
} = require("internal/tls");
const { normalizeServerTls, serverTlsFromOptions } = require("internal/tls");
const {
kInternalSocketData,
serverSymbol,
kHandle,
kRealListen,
tlsSymbol,
optionsSymbol,
kSNIContexts,
headerStateSymbol,
NodeHTTPHeaderState,
kPendingCallbacks,
Expand Down Expand Up @@ -245,19 +240,6 @@ function emitListeningNextTick(self, hostname, port) {
}
}

// Node.js only requests a client certificate when `requestCert: true`.
// The uSockets SSL context treats `ca` alone as "verify peer", so without
// these two flags an `https.Server({ ca })` would reject every client that
// doesn't present a cert. Mirror tls.Server (net.ts): default `requestCert`
// to false and, when not requesting, force `rejectUnauthorized` to false so
// the CA is loaded into the trust store without requiring a client cert.
function normalizeServerTls(tls) {
const requestCert = !!tls.requestCert;
tls.requestCert = requestCert;
tls.rejectUnauthorized = requestCert ? tls.rejectUnauthorized !== false : false;
return tls;
}

// Node registers connectionListener on every http.Server so `server.emit("connection", socket)`
// works for foreign Duplex sockets. The native listener handles its own sockets end to end;
// this picks up the rest. https://github.com/nodejs/node/blob/main/lib/_http_server.js
Expand Down Expand Up @@ -300,84 +282,12 @@ function Server(options, callback): void {
validateObject(options, "options");
options = { ...options };

// Node's https.Server accepts PKCS#12 bundles (pfx [+ passphrase]); fold
// them into plain key/cert/ca so the native TLS config sees PEM material.
let tlsOptions = options;
if (options.pfx) {
tlsOptions = processPfxOptions(options);
this[isTlsSymbol] = true;
}

let cert = tlsOptions.cert;
if (cert) {
throwOnInvalidTLSArray("options.cert", cert);
this[isTlsSymbol] = true;
}

let key = tlsOptions.key;
if (key) {
throwOnInvalidTLSArray("options.key", key);
this[isTlsSymbol] = true;
}

let ca = tlsOptions.ca;
// PKCS#12-embedded CAs extend the trust set; the server path hands raw
// {key, cert, ca} to the native config and has no addCACert hook, so fold
// them into `ca` (mirrors tls.Server.setSecureContext).
const pfxExtraCAs = tlsOptions._pfxExtraCACerts;
if (pfxExtraCAs?.length) {
ca = ca == null ? pfxExtraCAs : $isArray(ca) ? [...ca, ...pfxExtraCAs] : [ca, ...pfxExtraCAs];
}
if (ca) {
throwOnInvalidTLSArray("options.ca", ca);
this[isTlsSymbol] = true;
}

let passphrase = options.passphrase;
if (passphrase && typeof passphrase !== "string") {
throw $ERR_INVALID_ARG_TYPE("options.passphrase", "string", passphrase);
}

let serverName = options.servername;
if (serverName && typeof serverName !== "string") {
throw $ERR_INVALID_ARG_TYPE("options.servername", "string", serverName);
}

let secureOptions = options.secureOptions || 0;
if (secureOptions && typeof secureOptions !== "number") {
throw $ERR_INVALID_ARG_TYPE("options.secureOptions", "number", secureOptions);
}

if (this[isTlsSymbol]) {
// Translate minVersion/maxVersion/secureProtocol into the integer
// protocol range the native layer applies (secureProtocol wins, like
// Node's SecureContext::Init); 0 keeps the native defaults.
validateSecureProtocol(options.secureProtocol);
let minVersion, maxVersion;
const range = secureProtocolToVersionRange(options.secureProtocol);
if (range) {
minVersion = range[0];
maxVersion = range[1];
} else {
minVersion = tlsStringToProtocolVersion(options.minVersion);
maxVersion = tlsStringToProtocolVersion(options.maxVersion);
}
this[tlsSymbol] = normalizeServerTls({
serverName,
key,
cert,
ca,
passphrase,
secureOptions,
minVersion,
maxVersion,
ciphers: typeof options.ciphers === "string" && options.ciphers ? options.ciphers : undefined,
requestCert: options.requestCert,
rejectUnauthorized: options.rejectUnauthorized,
});
} else {
this[tlsSymbol] = null;
}
// https.Server sets isTlsSymbol before calling: it is a TLS server even
// when constructed without key material (certificates can arrive later via
// setSecureContext()/addContext()). An http.Server is one only if given some.
Comment thread
robobun marked this conversation as resolved.
const tls = serverTlsFromOptions(options, this[isTlsSymbol] === true);
if (tls !== null) this[isTlsSymbol] = true;
this[tlsSymbol] = tls;
}

this[optionsSymbol] = options;
Expand Down Expand Up @@ -637,6 +547,17 @@ Server.prototype[kRealListen] = function (tls, port, host, socketPath, reusePort
if (tls) {
this.serverName = tls.serverName || host || "localhost";
}
const sniContexts = this[kSNIContexts];
const sniContextsLength = sniContexts ? sniContexts.length : 0;
if (sniContextsLength > 0) {
// tls array: [default context, ...SNI contexts]. The first entry must be
// the default so an SNI context is never promoted to it.
Comment thread
robobun marked this conversation as resolved.
const tlsArray = [tls ?? { requestCert: false, rejectUnauthorized: false }];
for (let i = 0; i < sniContextsLength; i++) {
tlsArray.push(sniContexts[i]);
}
tls = tlsArray;
}
this[serverSymbol] = Bun.serve<any>({
idleTimeout: 0, // nodejs dont have a idleTimeout by default
tls,
Expand Down
Loading
Loading