Skip to content
Closed
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
57 changes: 48 additions & 9 deletions packages/bun-usockets/src/crypto/openssl.c
Original file line number Diff line number Diff line change
Expand Up @@ -191,11 +191,16 @@ static pthread_once_t us_ex_idx_once = PTHREAD_ONCE_INIT;
/* Async SNICallback suspension state, hung off the SSL via ex_data.
* Allocated the first time a dynamic resolver answers "pending"; freed with
* the SSL. The resolved ctx carries one reference owned by this struct until
* select_cert_cb consumes it (SSL_set_SSL_CTX takes its own). */
* select_cert_cb consumes it (SSL_set_SSL_CTX takes its own).
* `owner` is an opaque handle a resolver attaches so it learns, via
* `owner_free`, that this handshake's SSL is gone and a late resolution must
* not touch the socket. */
struct us_ssl_sni_pending_t {
/* 0 = none, 1 = waiting for the JS resolution, 2 = resolved, 3 = error */
int state;
struct ssl_ctx_st *resolved_ctx;
void *owner;
void (*owner_free)(void *owner);
};

static void us_ssl_sni_pending_free(void *parent, void *ptr, CRYPTO_EX_DATA *ad,
Expand All @@ -204,9 +209,25 @@ static void us_ssl_sni_pending_free(void *parent, void *ptr, CRYPTO_EX_DATA *ad,
struct us_ssl_sni_pending_t *st = ptr;
if (!st) return;
if (st->resolved_ctx) SSL_CTX_free(st->resolved_ctx);
if (st->owner_free) st->owner_free(st->owner);
us_free(st);
}

/* Get (creating if needed) the suspension state for this SSL. NULL when the
* state could not be attached. */
static struct us_ssl_sni_pending_t *us_ssl_sni_pending_get(SSL *ssl) {
if (us_ssl_sni_pending_idx < 0) return NULL;
struct us_ssl_sni_pending_t *pending = SSL_get_ex_data(ssl, us_ssl_sni_pending_idx);
if (!pending) {
pending = us_calloc(1, sizeof(*pending));
if (!SSL_set_ex_data(ssl, us_ssl_sni_pending_idx, pending)) {
us_free(pending);
return NULL;
}
}
return pending;
}

struct us_ssl_reneg_state_t {
uint64_t window_start_ms;
uint32_t count;
Expand Down Expand Up @@ -2218,6 +2239,27 @@ void us_socket_sni_resolve(struct us_socket_t *s, struct ssl_ctx_st *ctx, int er
ssl_update_handshake(s);
}

/* Hand an opaque resume handle to this handshake's suspension state. The
* handle's `owner_free` runs exactly once, when the SSL is freed - which
* always precedes the socket's own free (us_internal_ssl_detach) - so a
* resolver that nulls its socket pointer there can never resume a dangling
* socket. Called by a dynamic SNI resolver right before it answers "pending". */
void us_socket_sni_attach_resume(struct us_socket_t *s, void *owner,
void (*owner_free)(void *owner)) {
if (!s || us_socket_is_closed(s) || !s->ssl || !s_ssl(s)) {
if (owner_free) owner_free(owner);
return;
}
struct us_ssl_sni_pending_t *pending = us_ssl_sni_pending_get(s_ssl(s));
if (!pending) {
if (owner_free) owner_free(owner);
return;
}
if (pending->owner_free) pending->owner_free(pending->owner);
pending->owner = owner;
pending->owner_free = owner_free;
}

void us_internal_ssl_handshake_abort(struct us_socket_t *s) {
s->ssl_fatal_error = 1;
ssl_close(s, 0, NULL);
Expand Down Expand Up @@ -2399,14 +2441,11 @@ static enum ssl_select_cert_result_t us_select_cert_cb(const SSL_CLIENT_HELLO *h
return ssl_select_cert_error;
}
if (abort_handshake == 2) {
/* The JS resolver answered "pending": suspend until us_socket_sni_resolve. */
if (us_ssl_sni_pending_idx >= 0) {
if (!pending) {
pending = us_calloc(1, sizeof(*pending));
SSL_set_ex_data(ssl, us_ssl_sni_pending_idx, pending);
}
pending->state = 1;
}
/* The JS resolver answered "pending": suspend until us_socket_sni_resolve.
* Re-fetch rather than reuse the `pending` read at entry: the resolver may
* have just created the state to attach its resume handle to it. */
pending = us_ssl_sni_pending_get(ssl);
if (pending) pending->state = 1;
return ssl_select_cert_retry;
}
if (dyn) {
Expand Down
5 changes: 5 additions & 0 deletions packages/bun-usockets/src/libusockets.h
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,11 @@ void us_listen_socket_on_server_name(struct us_listen_socket_t *ls,
* call consumes the reference. `error` != 0 aborts the handshake. Safe to call
* after the socket closed (no-op). */
void us_socket_sni_resolve(us_socket_r s, struct ssl_ctx_st *ctx, int error);
/* Attach an opaque resume handle to the in-flight handshake. `owner_free` runs
* exactly once, when the handshake's SSL is freed (which always precedes the
* socket's free), so the resolver can invalidate a pending JS resolution. */
void us_socket_sni_attach_resume(us_socket_r s, void *owner,
void (*owner_free)(void *owner));
void *us_socket_server_name_userdata(us_socket_r s);

/* ── Connect ──────────────────────────────────────────────────────────────
Expand Down
45 changes: 37 additions & 8 deletions packages/bun-uws/src/App.h
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,21 @@ struct TemplatedApp {
if (!constructorFailed()) {
httpContext->getSocketContextData()->missingServerNameHandler = std::move(handler);
forEachListenSocket([&](us_listen_socket_t *ls) {
us_listen_socket_on_server_name(ls, &onMissingServerName);
us_listen_socket_on_server_name(ls, &onServerNameDispatch);
});
}
return std::move(*this);
}

/* Per-handshake certificate selector (node:https SNICallback). Takes
* precedence over the static SNI tree and the missingServerName handler. */
TemplatedApp &&serverNameResolver(typename HttpContextData<SSL>::ServerNameResolver resolver, void *userData) {
if (!constructorFailed()) {
auto *data = httpContext->getSocketContextData();
data->serverNameResolver = resolver;
data->serverNameResolverUserData = userData;
forEachListenSocket([&](us_listen_socket_t *ls) {
us_listen_socket_on_server_name(ls, &onServerNameDispatch);
});
}
return std::move(*this);
Expand Down Expand Up @@ -308,13 +322,27 @@ struct TemplatedApp {
TemplatedApp(TemplatedApp &&other) = delete;

private:
static struct ssl_ctx_st *onMissingServerName(struct us_listen_socket_t *ls, const char *hostname, int *abort_handshake, struct us_socket_t *socket) {
static struct ssl_ctx_st *onServerNameDispatch(struct us_listen_socket_t *ls, const char *hostname, int *abort_handshake, struct us_socket_t *socket) {
auto *httpContext = (HttpContext<SSL> *) us_socket_group_ext(us_listen_socket_group(ls));
auto *data = httpContext->getSocketContextData();

if (data->serverNameResolver) {
struct ssl_ctx_st *ctx = data->serverNameResolver(data->serverNameResolverUserData, hostname, abort_handshake, socket);
/* A selection, an abort or a suspension all end the dispatch; only
* "selected nothing, handshake still live" falls through. */
if (ctx || *abort_handshake) {
return ctx;
}
}

if (!data->missingServerNameHandler) {
/* us_select_cert_cb falls back to the static SNI tree on nullptr. */
return nullptr;
}

/* Bun.serve's missingServerName handler registers a context or lets the
* default serve the request - it never aborts or suspends the handshake. */
(void) abort_handshake;
(void) socket;
auto *httpContext = (HttpContext<SSL> *) us_socket_group_ext(us_listen_socket_group(ls));
httpContext->getSocketContextData()->missingServerNameHandler(hostname);
data->missingServerNameHandler(hostname);
/* The handler is expected to have registered the name via
* addServerName(); hand the newly-registered context back so the
* in-flight handshake uses it (the resolver no longer re-checks the
Expand Down Expand Up @@ -687,8 +715,9 @@ struct TemplatedApp {
for (auto &p : pendingServerNames) {
us_listen_socket_add_server_name(ls, p.hostname.c_str(), p.ctx, p.router);
}
if (httpContext->getSocketContextData()->missingServerNameHandler) {
us_listen_socket_on_server_name(ls, &onMissingServerName);
auto *data = httpContext->getSocketContextData();
if (data->missingServerNameHandler || data->serverNameResolver) {
us_listen_socket_on_server_name(ls, &onServerNameDispatch);
}
}
return ls;
Expand Down
13 changes: 13 additions & 0 deletions packages/bun-uws/src/HttpContextData.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@
#include <vector>
#include "MoveOnlyFunction.h"
#include "HttpParser.h"

/* Global scope, not uWS: an elaborated-type-specifier first seen inside the
* namespace would declare a distinct uWS::ssl_ctx_st. */
struct ssl_ctx_st;
struct us_socket_t;

namespace uWS {
template<bool> struct HttpResponse;
struct HttpRequest;
Expand Down Expand Up @@ -51,6 +57,13 @@ struct alignas(16) HttpContextData {

MoveOnlyFunction<void(const char *hostname)> missingServerNameHandler;

/* Dynamic per-handshake certificate selector (node:https SNICallback).
* Runs before the static SNI tree and may abort (*abort_handshake = 1) or
* suspend (= 2) the handshake. Returns an owned SSL_CTX* or nullptr. */
using ServerNameResolver = struct ssl_ctx_st *(*)(void *userData, const char *hostname, int *abort_handshake, struct us_socket_t *socket);
ServerNameResolver serverNameResolver = nullptr;
void *serverNameResolverUserData = nullptr;

struct RouterData {
HttpResponse<SSL> *httpResponse;
HttpRequest *httpRequest;
Expand Down
4 changes: 4 additions & 0 deletions src/js/internal/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const {
getCompleteWebRequestOrResponseBodyValueAsArrayBuffer,
drainMicrotasks,
setServerIdleTimeout,
resumeServerSNI,
} = $cpp("NodeHTTP.cpp", "createNodeHTTPInternalBinding") as {
getHeader: (headers: Headers, name: string) => string | undefined;
setHeader: (headers: Headers, name: string, value: string) => void;
Expand All @@ -30,6 +31,8 @@ const {
getCompleteWebRequestOrResponseBodyValueAsArrayBuffer: (arg: any) => ArrayBuffer | undefined;
drainMicrotasks: () => void;
setServerIdleTimeout: (server: any, timeout: number) => void;
/** Completes a handshake an asynchronous `SNICallback` left suspended. */
resumeServerSNI: (token: number, context: any, isError: boolean) => void;
};

const getRawKeys = $newCppFunction("JSFetchHeaders.cpp", "jsFetchHeaders_getRawKeys", 0);
Expand Down Expand Up @@ -579,6 +582,7 @@ export {
parseProxyConfigFromEnv,
parseProxyUrl,
reqSymbol,
resumeServerSNI,
runSymbol,
serverSymbol,
setHeader,
Expand Down
73 changes: 73 additions & 0 deletions src/js/node/_http_server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ const {
eofInProgress,
runSymbol,
drainMicrotasks,
resumeServerSNI,
setServerIdleTimeout,
setServerCustomOptions,
getMaxHTTPHeaderSize,
Expand Down Expand Up @@ -247,6 +248,66 @@ function normalizeServerTls(tls) {
return tls;
}

// ─── SNICallback dispatch (https.Server) ─────────────────────────────────────
// The native dispatch (Bun.serve's `onServerName`) runs from inside the
// ClientHello and expects back: the selected native SecureContext, `undefined`
// to fall through to the default context, `true` to suspend the handshake until
// `resumeServerSNI(token, ...)`, or an Error to refuse the connection.
// Node's SNICallback is `(servername, cb)` and may answer either way, so the
// resolution has to be recognised as synchronous or asynchronous after the fact.

// Node assigns `sni_context = context.context || context`: both the
// tls.createSecureContext() wrapper and a raw native context are accepted, and
// null/undefined falls through to the default context. The native side rejects
// anything that is not a context, so no validation is needed here.
function unwrapSNIContext(context) {
if (context == null) return undefined;
return (typeof context === "object" && context.context) || context;
}

// Non-Error rejections (cb(true), cb("reason"), throw true) are normalized: the
// native dispatch reads an Error return as the abort signal, and a literal
// `true` would collide with the handshake-suspension sentinel.
function toSNIError(err) {
return err instanceof Error ? err : Object.assign(new Error("SNI callback error"), { reason: err });
}

// The user SNICallback's completion callback, bound to the per-handshake state.
// A synchronous resolution is carried by dispatchServerSNI's return value; an
// asynchronous one completes the parked handshake through resumeServerSNI.
function onSNIResolution(state, err, context) {
if (state.settled) return; // an SNICallback must resolve exactly once
state.settled = true;
if (err) {
state.failed = toSNIError(err);
} else {
state.selected = unwrapSNIContext(context);
}
if (!state.suspended) return; // synchronous - the return value carries it
if (state.failed !== undefined) {
resumeServerSNI(state.token, undefined, true);
} else {
resumeServerSNI(state.token, state.selected, false);
}
}

function dispatchServerSNI(this: any, servername, token) {
const cb = this._SNICallback;
if (typeof cb !== "function" || !servername) return undefined;
const state = { token, selected: undefined, failed: undefined, settled: false, suspended: false };
try {
cb.$call(this, servername, onSNIResolution.bind(null, state));
} catch (err) {
state.settled = true;
state.failed = toSNIError(err);
}
if (!state.settled) {
state.suspended = true;
return true;
}
return state.failed !== undefined ? state.failed : state.selected;
}

function Server(options, callback): void {
if (!(this instanceof Server)) return new Server(options, callback);
EventEmitter.$call(this);
Expand Down Expand Up @@ -302,6 +363,13 @@ function Server(options, callback): void {
throw $ERR_INVALID_ARG_TYPE("options.secureOptions", "number", secureOptions);
}

// https.Server accepts tls.Server's per-handshake certificate selector.
const sniCallback = options.SNICallback;
if (sniCallback != null) {
validateFunction(sniCallback, "options.SNICallback");
this._SNICallback = sniCallback;
}

if (this[isTlsSymbol]) {
this[tlsSymbol] = normalizeServerTls({
serverName,
Expand Down Expand Up @@ -330,6 +398,9 @@ Server.prototype[kIncomingMessage] = undefined;

Server.prototype[kServerResponse] = undefined;

// tls.Server's per-handshake certificate selector, honored by https.Server.
Server.prototype._SNICallback = undefined;

Server.prototype[kConnectionsCheckingInterval] = undefined;

function rethrowUncaught(err) {
Expand Down Expand Up @@ -562,6 +633,8 @@ Server.prototype[kRealListen] = function (tls, port, host, socketPath, reusePort
this[serverSymbol] = Bun.serve<any>({
idleTimeout: 0, // nodejs dont have a idleTimeout by default
tls,
// A plain server never pays the JS round-trip from inside the handshake.
onServerName: tls && server._SNICallback ? dispatchServerSNI.bind(server) : undefined,
port,
hostname: host,
unix: socketPath,
Expand Down
23 changes: 23 additions & 0 deletions src/jsc/bindings/NodeHTTP.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ extern "C" void Request__setInternalEventCallback(void*, EncodedJSValue, JSC::JS
extern "C" void Request__setTimeout(void*, EncodedJSValue, JSC::JSGlobalObject*);
extern "C" bool NodeHTTPResponse__setTimeout(void*, EncodedJSValue, JSC::JSGlobalObject*);
extern "C" void Server__setIdleTimeout(EncodedJSValue, EncodedJSValue, JSC::JSGlobalObject*);
extern "C" void Bun__resumeServerSNI(double token, EncodedJSValue context, bool isError);
extern "C" EncodedJSValue Server__setAppFlags(JSC::JSGlobalObject*, EncodedJSValue, bool require_host_header, bool use_strict_method_validation);
extern "C" EncodedJSValue Server__setOnClientError(JSC::JSGlobalObject*, EncodedJSValue, EncodedJSValue);
extern "C" EncodedJSValue Server__setMaxHTTPHeaderSize(JSC::JSGlobalObject*, EncodedJSValue, uint64_t);
Expand Down Expand Up @@ -984,6 +985,24 @@ JSC_DEFINE_HOST_FUNCTION(jsHTTPSetServerIdleTimeout, (JSGlobalObject * globalObj
return JSValue::encode(jsUndefined());
}

/* resumeServerSNI(token, secureContextOrUndefined, isError) — completes a
* handshake that an asynchronous node:https SNICallback left suspended.
* Internal binding: `token` is the number the native SNI dispatch handed the
* callback, so an unknown (stale, already-resolved) one is a no-op. */
JSC_DEFINE_HOST_FUNCTION(jsHTTPResumeServerSNI, (JSGlobalObject * globalObject, CallFrame* callFrame))
{
ASSERT(callFrame->argumentCount() == 3);

JSValue tokenValue = callFrame->uncheckedArgument(0);
if (!tokenValue.isNumber()) {
return JSValue::encode(jsUndefined());
}

Bun__resumeServerSNI(tokenValue.asNumber(), JSValue::encode(callFrame->uncheckedArgument(1)), callFrame->uncheckedArgument(2).isTrue());

return JSValue::encode(jsUndefined());
}

JSC_DEFINE_HOST_FUNCTION(jsHTTPSetCustomOptions, (JSGlobalObject * globalObject, CallFrame* callFrame))
{
auto& vm = JSC::getVM(globalObject);
Expand Down Expand Up @@ -1163,6 +1182,10 @@ JSValue createNodeHTTPInternalBinding(Zig::GlobalObject* globalObject)
vm, JSC::PropertyName(JSC::Identifier::fromString(vm, "setServerIdleTimeout"_s)),
JSC::JSFunction::create(vm, globalObject, 2, "setServerIdleTimeout"_s, jsHTTPSetServerIdleTimeout, ImplementationVisibility::Public), 0);

obj->putDirect(
vm, JSC::PropertyName(JSC::Identifier::fromString(vm, "resumeServerSNI"_s)),
JSC::JSFunction::create(vm, globalObject, 3, "resumeServerSNI"_s, jsHTTPResumeServerSNI, ImplementationVisibility::Public), 0);

obj->putDirect(
vm, JSC::PropertyName(JSC::Identifier::fromString(vm, "setServerCustomOptions"_s)),
JSC::JSFunction::create(vm, globalObject, 2, "setServerCustomOptions"_s, jsHTTPSetCustomOptions, ImplementationVisibility::Public), 0);
Expand Down
Loading
Loading