use core::cell::Cell;
use core::ffi::{c_int, c_void};
use core::ptr::NonNull;
use crate::http_thread::InitOpts as HTTPThreadInitOpts;
use crate::{
self as http, AlpnOffer, HTTPCertError, HTTPClient, InitError, get_cert_error_from_no, h2,
};
use bun_boringssl::ssl_ctx_setup;
use bun_boringssl_sys::SSL_CTX;
use bun_collections::{HiveArray, TaggedPtrUnion};
use bun_core::{self, Error, FeatureFlags};
use crate::ssl_config::{self, SSLConfig};
use bun_core::strings;
use bun_uws as uws;
bun_core::declare_scope!(HTTPContext, hidden);
const POOL_SIZE: usize = 64;
const MAX_KEEPALIVE_HOSTNAME: usize = 128;
#[derive(bun_ptr::CellRefCounted)]
pub struct HTTPContext<const SSL: bool> {
pub ref_count: Cell<u32>,
pub pending_sockets: PooledSocketHiveAllocator<SSL>,
pub group: uws::SocketGroup,
pub secure: Option<*mut SSL_CTX>,
pub active_h2_sessions: Vec<*mut h2::ClientSession>,
#[expect(clippy::vec_box)]
pub pending_h2_connects: Vec<Box<h2::PendingConnect>>,
}
pub(crate) type HTTPContextRc<const SSL: bool> = bun_ptr::IntrusiveRc<HTTPContext<SSL>>;
pub(crate) type PooledSocketHiveAllocator<const SSL: bool> =
HiveArray<PooledSocket<SSL>, POOL_SIZE>;
pub type HTTPSocket<const SSL: bool> = uws::SocketHandler<SSL>;
pub(crate) type ActiveSocket<const SSL: bool> = TaggedPtrUnion<ActiveSocketTypes<SSL>>;
pub(crate) struct ActiveSocketTypes<const SSL: bool>;
impl<const SSL: bool> bun_ptr::tagged_pointer::TypeList for ActiveSocketTypes<SSL> {
const LEN: usize = 4;
const MIN_TAG: bun_ptr::tagged_pointer::TagType = 1024 - 3;
fn type_name_from_tag(tag: bun_ptr::tagged_pointer::TagType) -> Option<&'static str> {
match tag {
1024 => Some("DeadSocket"),
1023 => Some("HTTPClient"),
1022 => Some("PooledSocket"),
1021 => Some("H2.ClientSession"),
_ => None,
}
}
}
impl<const SSL: bool> bun_ptr::tagged_pointer::UnionMember<ActiveSocketTypes<SSL>> for DeadSocket {
const TAG: bun_ptr::tagged_pointer::TagType = 1024;
const NAME: &'static str = "DeadSocket";
}
impl<const SSL: bool> bun_ptr::tagged_pointer::UnionMember<ActiveSocketTypes<SSL>>
for HTTPClient<'static>
{
const TAG: bun_ptr::tagged_pointer::TagType = 1023;
const NAME: &'static str = "HTTPClient";
}
impl<const SSL: bool> bun_ptr::tagged_pointer::UnionMember<ActiveSocketTypes<SSL>>
for PooledSocket<SSL>
{
const TAG: bun_ptr::tagged_pointer::TagType = 1022;
const NAME: &'static str = "PooledSocket";
}
impl<const SSL: bool> bun_ptr::tagged_pointer::UnionMember<ActiveSocketTypes<SSL>>
for h2::ClientSession
{
const TAG: bun_ptr::tagged_pointer::TagType = 1021;
const NAME: &'static str = "H2.ClientSession";
}
pub(crate) trait ActiveSocketExt<const SSL: bool>: Copy {
fn client_mut<'a>(self) -> Option<&'a mut HTTPClient<'static>>;
fn session(self) -> Option<h2::SessionPtr>;
fn pooled_mut<'a>(self) -> Option<&'a mut PooledSocket<SSL>>;
}
#[inline(always)]
fn active_socket_get_mut<'a, const SSL: bool, T>(tagged: ActiveSocket<SSL>) -> Option<&'a mut T>
where
T: bun_ptr::tagged_pointer::UnionMember<ActiveSocketTypes<SSL>>,
{
tagged.get::<T>().map(|p| unsafe { &mut *p })
}
impl<const SSL: bool> ActiveSocketExt<SSL> for ActiveSocket<SSL> {
#[inline]
fn client_mut<'a>(self) -> Option<&'a mut HTTPClient<'static>> {
active_socket_get_mut(self)
}
#[inline]
fn session(self) -> Option<h2::SessionPtr> {
self.get::<h2::ClientSession>()
.and_then(NonNull::new)
.map(h2::ClientSession::this_ptr)
}
#[inline]
fn pooled_mut<'a>(self) -> Option<&'a mut PooledSocket<SSL>> {
active_socket_get_mut(self)
}
}
pub struct PooledSocket<const SSL: bool> {
pub http_socket: HTTPSocket<SSL>,
pub hostname_buf: [u8; MAX_KEEPALIVE_HOSTNAME],
pub hostname_len: u8,
pub port: u16,
pub did_have_handshaking_error_while_reject_unauthorized_is_false: bool,
pub established_with_reject_unauthorized: bool,
pub ssl_config: Option<ssl_config::SharedPtr>,
pub owner: *mut HTTPContext<SSL>,
pub proxy_tunnel: Option<crate::proxy_tunnel::RefPtr>,
pub target_hostname: Box<[u8]>,
pub target_port: u16,
pub proxy_auth_hash: u64,
pub h2_session: Option<NonNull<h2::ClientSession>>,
}
#[inline]
fn h2_session_as_mut<'a>(
s: Option<NonNull<h2::ClientSession>>,
) -> Option<&'a mut h2::ClientSession> {
s.map(|mut s| unsafe { s.as_mut() })
}
#[inline]
fn pooled_socket_mut<'a, const SSL: bool>(p: *mut PooledSocket<SSL>) -> &'a mut PooledSocket<SSL> {
unsafe { &mut *p }
}
impl<const SSL: bool> PooledSocket<SSL> {
#[inline]
pub(crate) fn h2_session_mut(&mut self) -> Option<&mut h2::ClientSession> {
h2_session_as_mut(self.h2_session)
}
fn release_parked_refs(&mut self) {
self.ssl_config = None;
self.target_hostname = Box::default();
if let Some(rp) = self.proxy_tunnel.take() {
rp.deref();
}
if let Some(s) = self.h2_session.take() {
unsafe { h2::ClientSession::deref(s.as_ptr()) };
}
}
}
struct ExistingSocket<const SSL: bool> {
socket: HTTPSocket<SSL>,
tunnel: Option<crate::proxy_tunnel::RefPtr>,
h2_session: Option<NonNull<h2::ClientSession>>,
}
impl<const SSL: bool> ExistingSocket<SSL> {
#[inline]
fn h2_session_mut(&mut self) -> Option<&mut h2::ClientSession> {
h2_session_as_mut(self.h2_session)
}
}
pub type ActiveSocketHandler<const SSL: bool> = Handler<SSL>;
impl<const SSL: bool> HTTPContext<SSL> {
pub(crate) const KIND: uws::SocketKind = if SSL {
uws::SocketKind::HttpClientTls
} else {
uws::SocketKind::HttpClient
};
pub(crate) fn mark_tagged_socket_as_dead(socket: HTTPSocket<SSL>, tagged: ActiveSocket<SSL>) {
if tagged.is::<PooledSocket<SSL>>() {
unsafe {
Handler::<SSL>::add_memory_back_to_pool(tagged.as_unchecked::<PooledSocket<SSL>>());
}
}
Self::set_socket_ext(socket, ActiveSocket::<SSL>::init(dead_socket()));
}
pub(crate) fn mark_socket_as_dead(socket: HTTPSocket<SSL>) {
Self::mark_tagged_socket_as_dead(socket, Self::get_tagged_from_socket(socket));
}
pub(crate) fn terminate_socket(socket: HTTPSocket<SSL>) {
Self::mark_socket_as_dead(socket);
socket.close(uws::CloseKind::Failure);
}
pub(crate) fn close_socket(socket: HTTPSocket<SSL>) {
Self::mark_socket_as_dead(socket);
socket.close(uws::CloseKind::Normal);
}
fn get_tagged(ptr: *mut c_void) -> ActiveSocket<SSL> {
ActiveSocket::<SSL>::from(Some(ptr))
}
pub(crate) fn get_tagged_from_socket(socket: HTTPSocket<SSL>) -> ActiveSocket<SSL> {
if let Some(slot) = socket.ext::<*mut c_void>() {
return Self::get_tagged(unsafe { *slot });
}
ActiveSocket::<SSL>::init(dead_socket())
}
fn ext_tagged_ptr(socket: HTTPSocket<SSL>) -> *mut c_void {
match socket.ext::<*mut c_void>() {
Some(slot) => unsafe { *slot },
None => core::ptr::null_mut(),
}
}
#[inline]
pub(crate) fn set_socket_ext(socket: HTTPSocket<SSL>, tagged: ActiveSocket<SSL>) {
if let Some(slot) = socket.ext::<*mut c_void>() {
unsafe { *slot = tagged.ptr() };
}
}
#[inline]
fn h2_session_ref(session: *const h2::ClientSession) -> bun_ptr::ParentRef<h2::ClientSession> {
bun_ptr::ParentRef::from(
NonNull::new(session.cast_mut()).expect("h2 registry session is non-null"),
)
}
fn h2_swap_remove_and_deref(
list: &mut Vec<*mut h2::ClientSession>,
idx: u32,
session: *const h2::ClientSession,
) {
debug_assert!(
(idx as usize) < list.len() && core::ptr::eq(list[idx as usize].cast_const(), session)
);
let entry = list.swap_remove(idx as usize);
if (idx as usize) < list.len() {
Self::h2_session_ref(list[idx as usize]).set_registry_index(idx);
}
unsafe { h2::ClientSession::deref(entry) };
}
pub(crate) fn register_h2(&mut self, session: *mut h2::ClientSession) {
if !SSL {
return;
}
let s = Self::h2_session_ref(session);
if s.registry_index() != u32::MAX {
return;
}
s.ref_();
s.set_registry_index(u32::try_from(self.active_h2_sessions.len()).expect("int cast"));
self.active_h2_sessions.push(session);
}
pub(crate) fn abort_pending_h2_waiter(&mut self, async_http_id: u32) -> bool {
if !SSL {
return false;
}
for pc in &mut self.pending_h2_connects {
let pos = pc
.waiters
.iter()
.position(|w| bun_ptr::BackRef::from(*w).async_http_id == async_http_id);
if let Some(i) = pos {
let waiter = pc.waiters.swap_remove(i);
h2::PendingConnect::waiter_mut(waiter).fail_from_h2(bun_core::err!("Aborted"));
return true;
}
}
false
}
pub(crate) unsafe fn unregister_h2_raw(ctx: *mut Self, session: *const h2::ClientSession) {
if !SSL {
return;
}
let s = Self::h2_session_ref(session);
let idx = s.registry_index();
if idx == u32::MAX {
return;
}
s.set_registry_index(u32::MAX);
let list = unsafe { &mut (*ctx).active_h2_sessions };
Self::h2_swap_remove_and_deref(list, idx, session);
}
pub(crate) fn tag_as_h2(socket: HTTPSocket<SSL>, session: *const h2::ClientSession) {
Self::set_socket_ext(socket, ActiveSocket::<SSL>::init(session));
}
pub(crate) fn ssl_ctx(&self) -> *mut SSL_CTX {
if !SSL {
unreachable!();
}
self.secure.unwrap()
}
pub(crate) fn init_with_client_config(
&mut self,
client: &mut HTTPClient,
) -> Result<(), InitError> {
debug_assert!(SSL, "ssl only");
let opts = client
.tls_props
.as_ref()
.unwrap()
.get()
.as_usockets_for_client_verification();
self.init_with_opts(&opts)
}
fn init_with_opts(
&mut self,
opts: &uws::SocketContext::BunSocketContextOptions,
) -> Result<(), InitError> {
debug_assert!(SSL, "ssl only");
let mut err = uws::create_bun_socket_error_t::none;
self.secure = match opts.create_ssl_context(&mut err) {
Some(ctx) => Some(ctx),
None => {
return Err(match err {
uws::create_bun_socket_error_t::load_ca_file => InitError::LoadCAFile,
uws::create_bun_socket_error_t::invalid_ca_file => InitError::InvalidCAFile,
uws::create_bun_socket_error_t::invalid_ca => InitError::InvalidCA,
_ => InitError::FailedToOpenSocket,
});
}
};
unsafe { ssl_ctx_setup(self.ssl_ctx()) };
bao_boringssl_bridge::session_cache::enable_client(self.ssl_ctx());
let owner_ptr = std::ptr::from_mut::<Self>(self).cast::<c_void>();
self.group
.init(http::http_thread().uws_loop(), Some(&HTTPS_VTABLE), owner_ptr);
Ok(())
}
pub(crate) fn init_with_thread_opts(
&mut self,
init_opts: &HTTPThreadInitOpts,
) -> Result<(), InitError> {
debug_assert!(SSL, "ssl only");
let opts = uws::SocketContext::BunSocketContextOptions {
ca: if !init_opts.ca.is_empty() {
init_opts.ca.as_ptr().cast()
} else {
core::ptr::null()
},
ca_count: u32::try_from(init_opts.ca.len()).expect("int cast"),
ca_file_name: if !init_opts.abs_ca_file_name.is_empty() {
init_opts.abs_ca_file_name.as_ptr().cast()
} else {
core::ptr::null()
},
request_cert: 1,
..Default::default()
};
self.init_with_opts(&opts)
}
pub(crate) fn init(&mut self) {
let owner_ptr = std::ptr::from_mut::<Self>(self).cast::<c_void>();
let vt: &'static uws::SocketGroupVTable = if SSL { &HTTPS_VTABLE } else { &HTTP_VTABLE };
self.group
.init(http::http_thread().uws_loop(), Some(vt), owner_ptr);
if SSL {
let mut err = uws::create_bun_socket_error_t::none;
self.secure = Some(
uws::SocketContext::BunSocketContextOptions {
request_cert: 1,
reject_unauthorized: 0,
..Default::default()
}
.create_ssl_context(&mut err)
.unwrap(),
);
unsafe { ssl_ctx_setup(self.ssl_ctx()) };
bao_boringssl_bridge::session_cache::enable_client(self.ssl_ctx());
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn release_socket(
&mut self,
socket: HTTPSocket<SSL>,
did_have_handshaking_error_while_reject_unauthorized_is_false: bool,
established_with_reject_unauthorized: bool,
hostname: &[u8],
port: u16,
ssl_config: Option<&ssl_config::SharedPtr>,
tunnel: Option<crate::proxy_tunnel::RefPtr>,
target_hostname: &[u8],
target_port: u16,
proxy_auth_hash: u64,
h2_session: Option<NonNull<h2::ClientSession>>,
) {
if cfg!(debug_assertions) {
debug_assert!(!socket.is_closed());
debug_assert!(!socket.is_shutdown());
debug_assert!(socket.is_established());
}
debug_assert!(!hostname.is_empty());
debug_assert!(port > 0);
if hostname.len() <= MAX_KEEPALIVE_HOSTNAME
&& !(socket.is_closed() || socket.is_shutdown() || socket.get_error() != 0)
&& socket.is_established()
{
let owner: *mut Self = self;
if let Some(slot) = self.pending_sockets.claim() {
let pending_addr = slot.addr();
Self::set_socket_ext(
socket,
ActiveSocket::<SSL>::init(pending_addr.as_ptr().cast_const()),
);
socket.flush();
socket.timeout(0);
socket.set_timeout_minutes(5);
let had_tunnel = tunnel.is_some();
let mut hostname_buf = [0u8; MAX_KEEPALIVE_HOSTNAME];
hostname_buf[..hostname.len()].copy_from_slice(hostname);
slot.write(PooledSocket {
http_socket: socket,
hostname_buf,
hostname_len: hostname.len() as u8, port,
did_have_handshaking_error_while_reject_unauthorized_is_false,
established_with_reject_unauthorized,
ssl_config: ssl_config.cloned(),
owner,
proxy_tunnel: tunnel,
target_hostname: if had_tunnel && !target_hostname.is_empty() {
Box::<[u8]>::from(target_hostname)
} else {
Box::default()
},
target_port,
proxy_auth_hash,
h2_session,
});
bun_core::scoped_log!(
HTTPContext,
"Keep-Alive release {}:{} tunnel={} target={}:{}",
bstr::BStr::new(hostname),
port,
had_tunnel,
bstr::BStr::new(target_hostname),
target_port,
);
return;
}
}
bun_core::scoped_log!(HTTPContext, "close socket");
if let Some(t) = tunnel {
crate::proxy_tunnel::raw_as_mut(t.as_ptr()).shutdown();
crate::proxy_tunnel::raw_as_mut(t.as_ptr()).detach_socket();
t.deref();
}
if let Some(s) = h2_session {
unsafe { h2::ClientSession::deref(s.as_ptr()) };
}
Self::close_socket(socket);
}
#[allow(clippy::too_many_arguments)]
fn existing_socket(
&mut self,
reject_unauthorized: bool,
hostname: &[u8],
port: u16,
ssl_config: Option<*const SSLConfig>,
want_tunnel: bool,
target_hostname: &[u8],
target_port: u16,
proxy_auth_hash: u64,
want_h2: AlpnOffer,
) -> Option<ExistingSocket<SSL>> {
if hostname.len() > MAX_KEEPALIVE_HOSTNAME {
return None;
}
let mut iter = self.pending_sockets.used.iterator::<true, true>();
while let Some(pending_socket_index) = iter.next() {
let socket_ptr = self
.pending_sockets
.at(u16::try_from(pending_socket_index).expect("int cast"));
let socket = pooled_socket_mut(socket_ptr);
if socket.port != port {
continue;
}
if SSLConfig::raw_ptr(socket.ssl_config.as_ref()) != ssl_config {
continue;
}
if socket.did_have_handshaking_error_while_reject_unauthorized_is_false
&& reject_unauthorized
{
continue;
}
if socket.h2_session.is_some() {
if want_h2 == AlpnOffer::H1 {
continue;
}
} else if want_h2 == AlpnOffer::H2Only {
continue;
}
if want_tunnel != socket.proxy_tunnel.is_some() {
continue;
}
if socket.proxy_auth_hash != proxy_auth_hash {
continue;
}
if want_tunnel {
if socket.target_port != target_port {
continue;
}
if !strings::eql_long(&socket.target_hostname, target_hostname, true) {
continue;
}
if reject_unauthorized
&& !socket
.proxy_tunnel
.as_ref()
.unwrap()
.established_with_reject_unauthorized
{
continue;
}
} else if SSL
&& reject_unauthorized
&& !socket.established_with_reject_unauthorized
{
continue;
}
if strings::eql_long(
&socket.hostname_buf[..socket.hostname_len as usize],
hostname,
true,
) {
let http_socket = socket.http_socket;
if http_socket.is_closed() {
Self::mark_socket_as_dead(http_socket);
continue;
}
if http_socket.is_shutdown() || http_socket.get_error() != 0 {
Self::terminate_socket(http_socket);
continue;
}
socket.ssl_config = None;
let tunnel: Option<crate::proxy_tunnel::RefPtr> = socket.proxy_tunnel.take();
socket.target_hostname = Box::default();
let h2_session = socket.h2_session.take();
let ok = unsafe { self.pending_sockets.put(socket_ptr) };
debug_assert!(ok);
bun_core::scoped_log!(
HTTPContext,
"+ Keep-Alive reuse {}:{}{}",
bstr::BStr::new(hostname),
port,
if tunnel.is_some() {
" (with tunnel)"
} else {
""
}
);
return Some(ExistingSocket {
socket: http_socket,
tunnel,
h2_session,
});
}
}
None
}
pub(crate) fn connect_socket(
&mut self,
client: &mut HTTPClient,
socket_path: &[u8],
) -> Result<Option<HTTPSocket<SSL>>, Error> {
client.connected_url = client
.http_proxy
.clone()
.unwrap_or_else(|| client.url.clone());
let socket = HTTPSocket::<SSL>::connect_unix_group(
&mut self.group,
Self::KIND,
if SSL { self.secure } else { None },
socket_path,
ActiveSocket::<SSL>::init(
client
.as_erased_ptr()
.as_ptr()
.cast::<HTTPClient<'static>>(),
)
.ptr(),
false, )?;
client.allow_retry = false;
Ok(Some(socket))
}
pub(crate) fn connect(
&mut self,
client: &mut HTTPClient,
hostname_: &[u8],
port: u16,
) -> Result<Option<HTTPSocket<SSL>>, Error> {
let hostname: &[u8] =
if FeatureFlags::HARDCODE_LOCALHOST_TO_127_0_0_1 && hostname_ == b"localhost" {
b"127.0.0.1"
} else {
hostname_
};
client.connected_url = client
.http_proxy
.clone()
.unwrap_or_else(|| client.url.clone());
client.connected_url.hostname =
unsafe { bun_ptr::detach_lifetime(hostname) };
if SSL {
if client.can_offer_h2() {
let cfg = SSLConfig::raw_ptr(client.tls_props.as_ref());
let host_header_hash = client.proxy_auth_hash();
let reusable = self
.active_h2_sessions
.iter()
.map(|&session| {
h2::ClientSession::this_ptr(
NonNull::new(session).expect("h2 registry entries are non-null"),
)
})
.find(|s| {
s.has_headroom()
&& s.matches(hostname, port, cfg, host_header_hash)
&& (!client.flags.reject_unauthorized
|| s.established_with_reject_unauthorized)
});
if let Some(session) = reusable {
h2::ClientSession::adopt(session, client);
return Ok(None);
}
let cfg_nn = cfg.and_then(|p| NonNull::new(p.cast_mut()));
for pc in &mut self.pending_h2_connects {
if pc.matches(hostname, port, cfg_nn, host_header_hash)
&& (!client.flags.reject_unauthorized || pc.reject_unauthorized)
{
pc.waiters.push(client.as_erased_ptr());
return Ok(None);
}
}
}
}
if client.is_keep_alive_possible() {
let want_tunnel = client.http_proxy.is_some() && client.url.is_https();
let target_hostname: &[u8] = if want_tunnel {
client.url.hostname
} else {
b""
};
let target_port: u16 = if want_tunnel {
client.url.get_port_auto()
} else {
0
};
let proxy_auth_hash: u64 = if want_tunnel || (SSL && client.http_proxy.is_none()) {
client.proxy_auth_hash()
} else {
0
};
if let Some(mut found) = self.existing_socket(
client.flags.reject_unauthorized,
hostname,
port,
SSLConfig::raw_ptr(client.tls_props.as_ref()),
want_tunnel,
target_hostname,
target_port,
proxy_auth_hash,
if SSL {
client.alpn_offer()
} else {
AlpnOffer::H1
},
) {
let sock = found.socket;
Self::set_socket_ext(
sock,
ActiveSocket::<SSL>::init(
client
.as_erased_ptr()
.as_ptr()
.cast::<HTTPClient<'static>>(),
),
);
client.allow_retry = true;
if let Some(session) = found.h2_session {
if SSL {
found.h2_session_mut().unwrap().socket = sock.assume_ssl();
Self::tag_as_h2(sock, session.as_ptr());
self.register_h2(session.as_ptr());
h2::ClientSession::adopt(h2::ClientSession::this_ptr(session), client);
} else {
unreachable!();
}
return Ok(None);
}
if let Some(tunnel) = found.tunnel {
crate::proxy_tunnel::ProxyTunnel::adopt::<SSL>(tunnel, client, sock);
client.on_open::<SSL>(sock)?;
client.on_writable::<true, SSL>(sock);
} else {
client.on_open::<SSL>(sock)?;
if SSL {
client.first_call::<SSL>(sock);
}
}
return Ok(Some(sock));
}
}
let socket = HTTPSocket::<SSL>::connect_group(
&mut self.group,
Self::KIND,
if SSL { self.secure } else { None },
hostname,
port as c_int,
ActiveSocket::<SSL>::init(
client
.as_erased_ptr()
.as_ptr()
.cast::<HTTPClient<'static>>(),
)
.ptr(),
false,
)?;
client.allow_retry = false;
if SSL {
if client.can_offer_h2() {
let cfg = SSLConfig::raw_ptr(client.tls_props.as_ref())
.and_then(|p| NonNull::new(p.cast_mut()));
let mut pc = h2::PendingConnect::new(h2::PendingConnect {
hostname: Box::<[u8]>::from(hostname),
port,
ssl_config: cfg,
reject_unauthorized: client.flags.reject_unauthorized,
host_header_hash: client.proxy_auth_hash(),
..Default::default()
});
client.pending_h2 = Some(NonNull::from(&mut *pc));
self.pending_h2_connects.push(pc);
}
}
Ok(Some(socket))
}
}
impl<const SSL: bool> Drop for HTTPContext<SSL> {
fn drop(&mut self) {
{
let mut iter = self.pending_sockets.used.iterator::<true, true>();
while let Some(idx) = iter.next() {
let pooled_ptr = self
.pending_sockets
.at(u16::try_from(idx).expect("int cast"));
let pooled = pooled_socket_mut(pooled_ptr);
pooled.release_parked_refs();
pooled.http_socket.close(uws::CloseKind::Failure);
}
}
if !self.group.loop_.is_null() {
self.group.close_all();
unsafe { uws::SocketGroup::destroy(&raw mut self.group) };
}
if SSL {
if let Some(c) = self.secure {
unsafe { bun_boringssl_sys::SSL_CTX_free(c) };
}
}
}
}
pub struct Handler<const SSL: bool>;
impl<const SSL: bool> Handler<SSL> {
pub fn on_open(ptr: *mut c_void, socket: HTTPSocket<SSL>) {
let active = HTTPContext::<SSL>::get_tagged(ptr);
if let Some(client) = active.client_mut() {
match client.on_open::<SSL>(socket) {
Ok(_) => return,
Err(_) => {
bun_core::scoped_log!(HTTPContext, "Unable to open socket");
HTTPContext::<SSL>::terminate_socket(socket);
return;
}
}
}
bun_core::scoped_log!(HTTPContext, "Unexpected open on unknown socket");
HTTPContext::<SSL>::terminate_socket(socket);
}
pub fn on_handshake(
ptr: *mut c_void,
socket: HTTPSocket<SSL>,
success: i32,
ssl_error: uws::us_bun_verify_error_t,
) {
let handshake_success = success == 1;
let handshake_error = HTTPCertError::from_verify_error(ssl_error);
let active = HTTPContext::<SSL>::get_tagged(ptr);
if let Some(client) = active.client_mut() {
client.flags.did_have_handshaking_error = handshake_error.error_no != 0;
if handshake_success {
if client.flags.reject_unauthorized {
if client.flags.did_have_handshaking_error {
client.close_and_fail::<SSL>(
get_cert_error_from_no(handshake_error.error_no),
socket,
);
return;
}
let ssl = unsafe {
&mut *socket
.get_native_handle()
.expect("TLS socket has native handle after handshake")
.cast::<bun_boringssl_sys::SSL>()
};
if !client.check_server_identity::<SSL>(socket, handshake_error, ssl, true) {
return;
}
}
return client.first_call::<SSL>(socket);
} else {
if client.flags.did_have_handshaking_error {
client.close_and_fail::<SSL>(
get_cert_error_from_no(handshake_error.error_no),
socket,
);
return;
}
client.close_and_fail::<SSL>(bun_core::err!("ConnectionRefused"), socket);
return;
}
}
if socket.is_closed() {
HTTPContext::<SSL>::mark_socket_as_dead(socket);
return;
}
if handshake_success {
if active.is::<PooledSocket<SSL>>() {
socket.set_timeout(0);
socket.set_timeout_minutes(5);
return;
}
}
HTTPContext::<SSL>::terminate_socket(socket);
}
pub fn on_close(ptr: *mut c_void, socket: HTTPSocket<SSL>, _: c_int, _: Option<*mut c_void>) {
let tagged = HTTPContext::<SSL>::get_tagged(ptr);
HTTPContext::<SSL>::mark_socket_as_dead(socket);
if let Some(client) = tagged.client_mut() {
return client.on_close::<SSL>(socket);
}
if let Some(session) = tagged.session() {
return h2::ClientSession::on_close(session, bun_core::err!("ConnectionClosed"));
}
}
unsafe fn add_memory_back_to_pool(pooled_ptr: *mut PooledSocket<SSL>) {
let owner = unsafe {
let slot = &mut *pooled_ptr;
slot.release_parked_refs();
slot.owner
};
let ok = unsafe { (*owner).pending_sockets.put(pooled_ptr) };
debug_assert!(ok);
}
pub fn on_data(ptr: *mut c_void, socket: HTTPSocket<SSL>, buf: &[u8]) {
let tagged = HTTPContext::<SSL>::get_tagged(ptr);
if let Some(client) = tagged.client_mut() {
return client.on_data::<SSL>(buf, client.get_ssl_ctx::<SSL>(), socket);
} else if let Some(session) = tagged.session() {
return h2::ClientSession::on_data(session, buf);
} else if let Some(pooled) = tagged.pooled_mut() {
if pooled.proxy_tunnel.is_some() {
bun_core::scoped_log!(HTTPContext, "Data on idle pooled tunnel — evicting");
HTTPContext::<SSL>::terminate_socket(socket);
return;
}
if let Some(session) = pooled.h2_session_mut() {
session.on_idle_data(buf);
if !session.can_pool() {
HTTPContext::<SSL>::terminate_socket(socket);
}
return;
}
if buf == http::END_OF_CHUNKED_HTTP1_1_ENCODING_RESPONSE_BODY {
return;
}
bun_core::scoped_log!(HTTPContext, "Unexpected data on socket");
HTTPContext::<SSL>::terminate_socket(socket);
return;
}
bun_core::scoped_log!(HTTPContext, "Unexpected data on unknown socket");
HTTPContext::<SSL>::terminate_socket(socket);
}
pub fn on_writable(ptr: *mut c_void, socket: HTTPSocket<SSL>) {
let tagged = HTTPContext::<SSL>::get_tagged(ptr);
if let Some(client) = tagged.client_mut() {
return client.on_writable::<false, SSL>(socket);
} else if let Some(session) = tagged.session() {
return h2::ClientSession::on_writable(session);
} else if tagged.is::<PooledSocket<SSL>>() {
} else {
bun_core::scoped_log!(HTTPContext, "Unexpected writable on socket");
HTTPContext::<SSL>::terminate_socket(socket);
}
}
pub fn on_long_timeout(ptr: *mut c_void, socket: HTTPSocket<SSL>) {
let tagged = HTTPContext::<SSL>::get_tagged(ptr);
if let Some(client) = tagged.client_mut() {
return client.on_timeout::<SSL>(socket);
}
if let Some(session) = tagged.session() {
HTTPContext::<SSL>::mark_socket_as_dead(socket);
h2::ClientSession::on_close(session, bun_core::err!("Timeout"));
}
HTTPContext::<SSL>::terminate_socket(socket);
}
pub fn on_timeout(ptr: *mut c_void, socket: HTTPSocket<SSL>) {
Self::on_long_timeout(ptr, socket);
}
pub fn on_connect_error(ptr: *mut c_void, socket: HTTPSocket<SSL>, _: c_int) {
let tagged = HTTPContext::<SSL>::get_tagged(ptr);
HTTPContext::<SSL>::mark_tagged_socket_as_dead(socket, tagged);
if let Some(client) = tagged.client_mut() {
client.on_connect_error();
}
HTTPContext::<SSL>::terminate_socket(socket);
}
pub fn on_end(ptr: *mut c_void, socket: HTTPSocket<SSL>) {
let tagged = HTTPContext::<SSL>::get_tagged(ptr);
HTTPContext::<SSL>::mark_tagged_socket_as_dead(socket, tagged);
socket.close(uws::CloseKind::Failure);
if let Some(client) = tagged.client_mut() {
client.on_close::<SSL>(socket);
return;
}
if let Some(session) = tagged.session() {
h2::ClientSession::on_close(session, bun_core::err!("ConnectionClosed"));
return;
}
}
}
unsafe extern "C" fn http_vt_on_open<const SSL: bool>(
s: *mut uws::Socket,
_is_client: c_int,
_ip: *mut u8,
_ip_len: c_int,
) -> *mut uws::Socket {
let socket = HTTPSocket::<SSL>::from(s);
let ptr = HTTPContext::<SSL>::ext_tagged_ptr(socket);
Handler::<SSL>::on_open(ptr, socket);
s
}
unsafe extern "C" fn http_vt_on_data<const SSL: bool>(
s: *mut uws::Socket,
data: *mut u8,
len: c_int,
) -> *mut uws::Socket {
let buf: &[u8] = if data.is_null() || len <= 0 {
&[]
} else {
unsafe { core::slice::from_raw_parts(data, usize::try_from(len).expect("int cast")) }
};
let socket = HTTPSocket::<SSL>::from(s);
let ptr = HTTPContext::<SSL>::ext_tagged_ptr(socket);
Handler::<SSL>::on_data(ptr, socket, buf);
s
}
unsafe extern "C" fn http_vt_on_writable<const SSL: bool>(
s: *mut uws::Socket,
) -> *mut uws::Socket {
let socket = HTTPSocket::<SSL>::from(s);
let ptr = HTTPContext::<SSL>::ext_tagged_ptr(socket);
Handler::<SSL>::on_writable(ptr, socket);
s
}
unsafe extern "C" fn http_vt_on_close<const SSL: bool>(
s: *mut uws::Socket,
code: c_int,
reason: *mut c_void,
) -> *mut uws::Socket {
let reason = if reason.is_null() { None } else { Some(reason) };
let socket = HTTPSocket::<SSL>::from(s);
let ptr = HTTPContext::<SSL>::ext_tagged_ptr(socket);
Handler::<SSL>::on_close(ptr, socket, code, reason);
s
}
unsafe extern "C" fn http_vt_on_timeout<const SSL: bool>(
s: *mut uws::Socket,
) -> *mut uws::Socket {
let socket = HTTPSocket::<SSL>::from(s);
let ptr = HTTPContext::<SSL>::ext_tagged_ptr(socket);
Handler::<SSL>::on_timeout(ptr, socket);
s
}
unsafe extern "C" fn http_vt_on_long_timeout<const SSL: bool>(
s: *mut uws::Socket,
) -> *mut uws::Socket {
let socket = HTTPSocket::<SSL>::from(s);
let ptr = HTTPContext::<SSL>::ext_tagged_ptr(socket);
Handler::<SSL>::on_long_timeout(ptr, socket);
s
}
unsafe extern "C" fn http_vt_on_end<const SSL: bool>(
s: *mut uws::Socket,
) -> *mut uws::Socket {
let socket = HTTPSocket::<SSL>::from(s);
let ptr = HTTPContext::<SSL>::ext_tagged_ptr(socket);
Handler::<SSL>::on_end(ptr, socket);
s
}
unsafe extern "C" fn http_vt_on_connect_error<const SSL: bool>(
s: *mut uws::Socket,
code: c_int,
) -> *mut uws::Socket {
let socket = HTTPSocket::<SSL>::from(s);
let ptr = HTTPContext::<SSL>::ext_tagged_ptr(socket);
Handler::<SSL>::on_connect_error(ptr, socket, code);
s
}
unsafe extern "C" fn http_vt_on_connecting_error<const SSL: bool>(
cs: *mut uws::ConnectingSocket,
_code: c_int,
) -> *mut uws::ConnectingSocket {
if cs.is_null() {
return cs;
}
let ext_slot: Option<NonNull<core::ffi::c_void>> =
*unsafe { &mut *cs }.ext::<Option<NonNull<core::ffi::c_void>>>();
let Some(tagged_word) = ext_slot else {
return cs;
};
let tagged = HTTPContext::<SSL>::get_tagged(tagged_word.as_ptr());
if let Some(client) = tagged.client_mut() {
client.on_connect_error();
}
cs
}
unsafe extern "C" fn http_vt_on_handshake<const SSL: bool>(
s: *mut uws::Socket,
success: c_int,
err: uws::us_bun_verify_error_t,
_custom: *mut c_void,
) {
let socket = HTTPSocket::<SSL>::from(s);
let ptr = HTTPContext::<SSL>::ext_tagged_ptr(socket);
Handler::<SSL>::on_handshake(ptr, socket, success, err);
}
static HTTP_VTABLE: uws::SocketGroupVTable = uws::SocketGroupVTable {
on_open: Some(http_vt_on_open::<false>),
on_data: Some(http_vt_on_data::<false>),
on_fd: None,
on_writable: Some(http_vt_on_writable::<false>),
on_close: Some(http_vt_on_close::<false>),
on_timeout: Some(http_vt_on_timeout::<false>),
on_long_timeout: Some(http_vt_on_long_timeout::<false>),
on_end: Some(http_vt_on_end::<false>),
on_connect_error: Some(http_vt_on_connect_error::<false>),
on_connecting_error: Some(http_vt_on_connecting_error::<false>),
on_handshake: None,
};
static HTTPS_VTABLE: uws::SocketGroupVTable = uws::SocketGroupVTable {
on_open: Some(http_vt_on_open::<true>),
on_data: Some(http_vt_on_data::<true>),
on_fd: None,
on_writable: Some(http_vt_on_writable::<true>),
on_close: Some(http_vt_on_close::<true>),
on_timeout: Some(http_vt_on_timeout::<true>),
on_long_timeout: Some(http_vt_on_long_timeout::<true>),
on_end: Some(http_vt_on_end::<true>),
on_connect_error: Some(http_vt_on_connect_error::<true>),
on_connecting_error: Some(http_vt_on_connecting_error::<true>),
on_handshake: Some(http_vt_on_handshake::<true>),
};
#[repr(C, align(8))]
pub(crate) struct DeadSocket {
garbage: u8,
}
static DEAD_SOCKET: DeadSocket = DeadSocket { garbage: 0 };
#[inline]
fn dead_socket() -> *const DeadSocket {
&raw const DEAD_SOCKET
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn http_vtable_dispatch_slots_populated() {
assert!(HTTP_VTABLE.on_open.is_some(), "on_open must reach Handler::on_open");
assert!(
HTTP_VTABLE.on_writable.is_some(),
"on_writable must reach Handler::on_writable"
);
assert!(HTTP_VTABLE.on_data.is_some(), "on_data must reach Handler::on_data");
assert!(HTTP_VTABLE.on_close.is_some(), "on_close must reach Handler::on_close");
assert!(HTTP_VTABLE.on_end.is_some(), "on_end must reach Handler::on_end");
assert!(
HTTP_VTABLE.on_connect_error.is_some(),
"on_connect_error must reach Handler::on_connect_error"
);
assert!(
HTTP_VTABLE.on_timeout.is_some(),
"on_timeout must reach Handler::on_timeout"
);
assert!(
HTTP_VTABLE.on_long_timeout.is_some(),
"on_long_timeout must reach Handler::on_long_timeout"
);
assert!(
HTTP_VTABLE.on_connecting_error.is_some(),
"on_connecting_error must fail the HTTPClient (DNS-error fetch hang)"
);
}
#[test]
fn https_vtable_handshake_slot_populated() {
for (label, slot) in [
("on_open", HTTPS_VTABLE.on_open.is_some()),
("on_writable", HTTPS_VTABLE.on_writable.is_some()),
("on_data", HTTPS_VTABLE.on_data.is_some()),
("on_close", HTTPS_VTABLE.on_close.is_some()),
("on_handshake", HTTPS_VTABLE.on_handshake.is_some()),
("on_connect_error", HTTPS_VTABLE.on_connect_error.is_some()),
("on_connecting_error", HTTPS_VTABLE.on_connecting_error.is_some()),
] {
assert!(slot, "HTTPS_VTABLE.{label} must reach Handler");
}
}
#[test]
fn http_and_https_vtables_bind_distinct_trampolines() {
let http_open = HTTP_VTABLE.on_open.unwrap() as *const ();
let https_open = HTTPS_VTABLE.on_open.unwrap() as *const ();
assert_ne!(
http_open, https_open,
"HTTP and HTTPS vtables must bind distinct per-SSL trampolines"
);
}
}