Skip to content
Draft
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
1 change: 1 addition & 0 deletions packages/bun-usockets/src/context.c
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,7 @@ static void us_internal_init_listen_socket(struct us_listen_socket_t *ls,
ls->on_server_name = NULL;
ls->socket_ext_size = socket_ext_size;
ls->deferred_accept = 0;
ls->keylog_enabled = 0;

/* Link into the group so close_all() / test-isolation can find it. */
ls->next = group->head_listen_sockets;
Expand Down
64 changes: 53 additions & 11 deletions packages/bun-usockets/src/crypto/openssl.c
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,12 @@ static void ssl_flush_pending_keylog(struct us_socket_t *s) {
if (!s->ssl || us_socket_is_closed(s)) {
return;
}
/* uWS HTTP server sockets have no keylog dispatch; leave the lines parked
* so the JS connection callback can pop them with us_socket_pop_keylog.
* Whatever is never drained is freed by the ex_data destructor. */
if (us_socket_kind(s) == BUN_SOCKET_KIND_UWS_HTTP_TLS) {
return;
}
struct us_ssl_pending_session_t *pending =
SSL_get_ex_data(s->ssl, us_ssl_pending_keylog_idx);
if (!pending) {
Expand Down Expand Up @@ -483,6 +489,18 @@ int us_ssl_pop_pending_keylog(SSL *ssl, unsigned char *out, int out_cap) {
return us_ssl_pop_pending(ssl, us_ssl_pending_keylog_idx, out, out_cap);
}

/* node:https server 'keylog': arm parking for every socket subsequently
* accepted by `ls` (see the keylog_enabled branch in us_internal_ssl_attach).
* The JS layer drains via us_socket_pop_keylog once the handshake completes. */
void us_listen_socket_enable_keylog(struct us_listen_socket_t *ls) {
ls->keylog_enabled = 1;
}

int us_socket_pop_keylog(struct us_socket_t *s, unsigned char *out, int out_cap) {
if (!s->ssl) return 0;
return us_ssl_pop_pending_keylog((SSL *)s->ssl, out, out_cap);
}

/* The resumable session most recently delivered via the new-session callback,
* or NULL if none has arrived. The returned pointer is borrowed from the SSL's
* ex_data and valid until the next NewSessionTicket or SSL_free. */
Expand Down Expand Up @@ -1473,7 +1491,8 @@ void us_internal_ssl_attach(struct us_socket_t *s, SSL_CTX *ctx,
* sockets lives in accept_kind and may not have been copied onto `s` yet
* when its SSL is initialized. */
if (ssl && (us_socket_kind(s) == BUN_SOCKET_KIND_BUN_SOCKET_TLS ||
(listener && listener->accept_kind == BUN_SOCKET_KIND_BUN_SOCKET_TLS))) {
(listener && (listener->accept_kind == BUN_SOCKET_KIND_BUN_SOCKET_TLS ||
listener->keylog_enabled)))) {
/* The very first TLS attach in a process can be a client connection, and
* nothing on that path has registered the ex_data indices yet - using the
* still--1 index would make CRYPTO_set_ex_data grow its slot array toward
Expand Down Expand Up @@ -1996,18 +2015,24 @@ struct us_socket_t *us_internal_ssl_on_close(struct us_socket_t *s, int code, vo
return ret;
}

/* The EOF dispatch below is scoped to uWS HTTP server sockets: their
* context's onEnd owns the EOF (premature-EOF clientError
* HPE_INVALID_EOF_STATE, CONNECT/Upgrade half-open, pipeline drain after
* FIN), and closing without dispatching silently skipped all of it for
* node:https. Every other TLS socket kind predates the dispatch and
* synthesizes its JS 'end' from the close event, so they keep the
* historical force-close (dispatching for them strands sockets whose end
* handler expects the transport to close underneath it). */
static int ssl_wants_eof_dispatch(struct us_socket_t *s) {
static int ssl_is_uws_http_tls(struct us_socket_t *s) {
return us_socket_kind(s) == BUN_SOCKET_KIND_UWS_HTTP_TLS;
}

/* EOF dispatch only for kinds whose user layer consumes 'end' + honors
* allow_half_open (uWS HTTP server, Bun.connect/listen ⇒ node:tls); all
* other TLS kinds derive EOF from close and keep the force-close. */
static int ssl_wants_eof_dispatch(struct us_socket_t *s) {
unsigned char kind = us_socket_kind(s);
if (kind == BUN_SOCKET_KIND_UWS_HTTP_TLS) {
return 1;
}
/* Mid-handshake EOF keeps the force-close so JS surfaces it as a failed
* handshake (ECONNRESET "socket hang up", like Node) not a clean 'end'. */
return kind == BUN_SOCKET_KIND_BUN_SOCKET_TLS &&
s->ssl_handshake_state == HANDSHAKE_COMPLETED;
}

/* Deliver the plaintext EOF to the user layer once, like the plain-TCP path
* (loop.c dispatches us_dispatch_end for non-SSL sockets). Both TLS EOF
* paths (peer close_notify -> ZERO_RETURN, and the raw TCP FIN that usually
Expand Down Expand Up @@ -2126,7 +2151,7 @@ struct us_socket_t *us_internal_ssl_on_writable(struct us_socket_t *s) {
* onWritable clears the teardown timeout armed at shutdown. node sockets
* still get write-completion dispatch after a half-close in either
* direction. */
if (ssl_wants_eof_dispatch(s) && us_internal_ssl_is_shut_down(s)) return s;
if (ssl_is_uws_http_tls(s) && us_internal_ssl_is_shut_down(s)) return s;

if (s->ssl_handshake_state == HANDSHAKE_COMPLETED) {
s = us_dispatch_writable(s);
Expand Down Expand Up @@ -2882,6 +2907,23 @@ struct ssl_ctx_st *us_listen_socket_find_server_name_ctx(struct us_listen_socket
return node->ctx;
}

void us_listen_socket_set_default_ssl_ctx(struct us_listen_socket_t *ls,
SSL_CTX *ctx) {
if (ls->ssl_ctx == ctx) return;
SSL_CTX_up_ref(ctx);
/* Carry over the listener-level callbacks registered on the old default. */
if (ls->sni) {
SSL_CTX_set_tlsext_servername_callback(ctx, sni_cb);
}
if (ls->on_server_name) {
SSL_CTX_set_select_certificate_cb(ctx, us_select_cert_cb);
}
if (ls->ssl_ctx) {
us_internal_ssl_ctx_unref(ls->ssl_ctx);
}
ls->ssl_ctx = ctx;
}

void us_listen_socket_on_server_name(struct us_listen_socket_t *ls,
struct ssl_ctx_st *(*cb)(struct us_listen_socket_t *, const char *, int *, struct us_socket_t *)) {
ls->on_server_name = cb;
Expand Down
3 changes: 3 additions & 0 deletions packages/bun-usockets/src/internal/internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,9 @@ struct us_listen_socket_t {
unsigned int socket_ext_size;
/* kind to stamp on accepted sockets. */
unsigned char accept_kind;
/* node:https server 'keylog': park NSS key-log lines on accepted sockets'
* SSLs so the JS layer can drain them after the handshake. */
unsigned char keylog_enabled;
/* Set when TCP_DEFER_ACCEPT/SO_ACCEPTFILTER was successfully applied. */
unsigned char deferred_accept;
};
Expand Down
9 changes: 9 additions & 0 deletions packages/bun-usockets/src/libusockets.h
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,11 @@ void *us_listen_socket_find_server_name_userdata(struct us_listen_socket_t *ls,
/* Returns an owned reference; the caller must release it. */
struct ssl_ctx_st *us_listen_socket_find_server_name_ctx(struct us_listen_socket_t *ls,
const char *hostname_pattern) nonnull_fn_decl;
/* tls.Server#setSecureContext(): swap the default SSL_CTX used for NEWLY
* accepted sockets (SNI-selected contexts are untouched). Up_refs ctx; live
* connections keep the previous context alive through their own SSL refs. */
void us_listen_socket_set_default_ssl_ctx(struct us_listen_socket_t *ls,
struct ssl_ctx_st *ctx) __attribute__((nonnull(1, 2)));
/* Parses a PKCS#12 blob into malloc'd PEM key/cert/ca strings (caller frees);
* returns 0 with a static *err_reason tag on failure. */
int us_ssl_parse_pkcs12(const char *data, size_t len, const char *pass,
Expand Down Expand Up @@ -551,6 +556,10 @@ int us_ssl_ctx_add_ca_cert(struct ssl_ctx_st *ctx, const char *content);
void us_ssl_enable_pending_events(struct ssl_st *ssl);
int us_ssl_pop_pending_session(struct ssl_st *ssl, unsigned char *out, int out_cap);
int us_ssl_pop_pending_keylog(struct ssl_st *ssl, unsigned char *out, int out_cap);
/* node:https server 'keylog': park key-log lines for sockets accepted by `ls`;
* drain per-socket with us_socket_pop_keylog after the handshake. */
void us_listen_socket_enable_keylog(struct us_listen_socket_t *ls) nonnull_fn_decl;
int us_socket_pop_keylog(us_socket_r s, unsigned char *out, int out_cap);
/* The resumable session most recently delivered via the new-session callback,
* or NULL if none. Borrowed; valid until the next NewSessionTicket or SSL_free. */
struct ssl_session_st *us_ssl_get_new_session(struct ssl_st *ssl);
Expand Down
3 changes: 3 additions & 0 deletions src/boringssl_sys/boringssl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -915,6 +915,9 @@ unsafe extern "C" {
pub fn SSL_is_init_finished(ssl: *const SSL) -> c_int;
pub fn SSL_set_verify(ssl: *mut SSL, mode: c_int, callback: SSL_verify_cb);
pub fn SSL_set0_verify_cert_store(ssl: *mut SSL, store: *mut X509_STORE) -> c_int;
pub fn X509_STORE_new() -> *mut X509_STORE;
/// Takes ownership of `store` (consumes one reference).
pub fn SSL_CTX_set_cert_store(ctx: *mut SSL_CTX, store: *mut X509_STORE);
pub fn SSL_set_renegotiate_mode(ssl: *mut SSL, mode: ssl_renegotiate_mode_t);
pub fn SSL_renegotiate(ssl: *mut SSL) -> c_int;
pub fn SSL_get_servername(ssl: *const SSL, ty: c_int) -> *const c_char;
Expand Down
43 changes: 43 additions & 0 deletions src/http/HTTPContext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -542,6 +542,49 @@
self.init_with_opts(&opts)
}

/// Swap this context's `SSL_CTX` for one trusting exactly `certs`
/// (`tls.setDefaultCACertificates()`); only new connects see it, empty ⇒
/// empty trust store. <https://github.com/nodejs/node/blob/main/lib/tls.js>
pub(crate) fn replace_ssl_ctx_with_default_ca(&mut self, certs: &[std::ffi::CString]) {
debug_assert!(SSL, "ssl only");
let ptrs: Vec<*const core::ffi::c_char> = certs.iter().map(|c| c.as_ptr()).collect();
let mut err = uws::create_bun_socket_error_t::none;
let opts = uws::SocketContext::BunSocketContextOptions {
ca: if ptrs.is_empty() {
core::ptr::null()
} else {
ptrs.as_ptr().cast()
},
ca_count: u32::try_from(ptrs.len()).expect("int cast"),
request_cert: 1,
..Default::default()
};
let Some(ctx) = opts.create_ssl_context(&mut err) else {
// PEMs were pre-validated (parseCACertificates) so this is resource
// exhaustion; keep the previous ctx rather than drop verification.
return;
};
if ptrs.is_empty() {
// ca_count == 0 seeds the shared default root store, but
// setDefaultCACertificates([]) means an explicitly empty one.
let store = unsafe { bun_boringssl_sys::X509_STORE_new() };

Check failure on line 570 in src/http/HTTPContext.rs

View workflow job for this annotation

GitHub Actions / cargo clippy

unsafe block missing a safety comment
if store.is_null() {
// SAFETY: ctx was created above and not yet published.
unsafe { bun_boringssl_sys::SSL_CTX_free(ctx) };
return;
}
// SAFETY: consumes `store`; ctx owns it from here.
unsafe { bun_boringssl_sys::SSL_CTX_set_cert_store(ctx, store) };
}
// SAFETY: ctx is the fresh SSL_CTX this context is about to own.
unsafe { ssl_ctx_setup(ctx) };
if let Some(old) = self.secure.replace(ctx) {
// SAFETY: releases the one ref this context held; sockets created
// from the old ctx keep it alive through their own SSL refs.
unsafe { bun_boringssl_sys::SSL_CTX_free(old) };
}
}

pub(crate) fn init(&mut self) {
let owner_ptr = std::ptr::from_mut::<Self>(self).cast::<c_void>();
self.group
Expand Down
19 changes: 19 additions & 0 deletions src/http/HTTPThread.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,9 @@ pub struct HttpThread {
/// `Option::take` is the once-guard (no atomics needed — `connect` is never
/// reentrant).
lazy_https_init: Option<InitOpts>,
/// Last `crate::default_ca::generation()` applied to `https_context`.
/// 0 = the override was never installed. HTTP-thread-only.
applied_default_ca_generation: u64,

pub(crate) queued_tasks: Queue,
/// Tasks popped from `queued_tasks` that couldn't start because
Expand Down Expand Up @@ -172,6 +175,7 @@ impl HttpThread {
pending_h2_connects: Vec::new(),
},
lazy_https_init: None,
applied_default_ca_generation: 0,
queued_tasks: Queue::new(),
deferred_tasks: Vec::new(),
has_pending_queued_abort: false,
Expand Down Expand Up @@ -460,6 +464,21 @@ impl HttpThread {
if let Some(opts) = self.lazy_https_init.take() {
self.init_https_context_cold(&opts);
}
// `tls.setDefaultCACertificates()` replaced the default CA set since
// the default HTTPS context was built (or since the last replacement):
// rebuild its SSL_CTX so new connects verify against the current set.
let generation = crate::default_ca::generation();
if generation != self.applied_default_ca_generation {
self.apply_default_ca_override_cold(generation);
}
}

#[cold]
fn apply_default_ca_override_cold(&mut self, generation: u64) {
self.applied_default_ca_generation = generation;
if let Some(certs) = crate::default_ca::snapshot() {
self.https_context.replace_ssl_ctx_with_default_ca(&certs);
}
}

#[cold]
Expand Down
31 changes: 31 additions & 0 deletions src/http/default_ca.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//! `tls.setDefaultCACertificates()` override for `fetch()`'s HTTP-thread TLS
//! contexts (node:tls applies it in JS instead — src/js/node/tls.ts).
//! <https://github.com/nodejs/node/blob/main/lib/tls.js>

use std::ffi::CString;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};

use bun_threading::Guarded;

static OVERRIDE: Guarded<Option<Arc<Vec<CString>>>> = Guarded::new(None);
/// 0 = never set. Bumped after the new store is published, so a reader that
/// observes generation N under Acquire sees at least the store published for
/// N when it takes the lock.
static GENERATION: AtomicU64 = AtomicU64::new(0);

/// Replaces the default CA set. An empty `certs` means an explicitly empty
/// trust store (every verification fails), matching Node's
/// `tls.setDefaultCACertificates([])`.
pub fn set(certs: Vec<CString>) {
*OVERRIDE.lock() = Some(Arc::new(certs));
GENERATION.fetch_add(1, Ordering::Release);
}

pub fn generation() -> u64 {
GENERATION.load(Ordering::Acquire)
}

pub fn snapshot() -> Option<Arc<Vec<CString>>> {
OVERRIDE.lock().clone()
}
1 change: 1 addition & 0 deletions src/http/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ pub use proxy_tunnel::ProxyTunnel;
pub use send_file::SendFile;
pub use signals::Signals;
pub use thread_safe_stream_buffer::ThreadSafeStreamBuffer;
pub mod default_ca;
#[path = "ssl_config.rs"]
pub mod ssl_config;
pub use ssl_config::SSLConfig;
Expand Down
6 changes: 6 additions & 0 deletions src/js/internal/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ const {
webRequestOrResponseHasBodyValue,
setServerCustomOptions,
setServerAppFlags,
setServerSecureContext,
enableServerKeylog,
getCompleteWebRequestOrResponseBodyValueAsArrayBuffer,
drainMicrotasks,
setServerIdleTimeout,
Expand Down Expand Up @@ -37,6 +39,8 @@ const {
insecureHTTPParser: boolean,
httpAllowHalfOpen: boolean,
) => void;
setServerSecureContext: (server: any, tls: any) => void;
enableServerKeylog: (server: any) => void;
getCompleteWebRequestOrResponseBodyValueAsArrayBuffer: (arg: any) => ArrayBuffer | undefined;
drainMicrotasks: () => void;
setServerIdleTimeout: (server: any, timeout: number) => void;
Expand Down Expand Up @@ -605,6 +609,7 @@ export {
emitCloseNTAndComplete,
emitEOFIncomingMessage,
emitErrorNextTickIfErrorListenerNT,
enableServerKeylog,
eofInProgress,
fakeSocketSymbol,
filterEnvForProxies,
Expand Down Expand Up @@ -670,6 +675,7 @@ export {
setServerAppFlags,
setServerCustomOptions,
setServerIdleTimeout,
setServerSecureContext,
statusCodeSymbol,
statusMessageSymbol,
timeoutTimerSymbol,
Expand Down
Loading
Loading