use core::ffi::{CStr, c_char};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Weak};
use bun_uws as uws;
use bun_threading::Guarded as Mutex;
use bun_wyhash::Wyhash;
type CStrPtr = *const c_char;
type CStrSlice = Option<Box<[CStrPtr]>>;
pub struct SSLConfig {
pub server_name: CStrPtr,
pub key_file_name: CStrPtr,
pub cert_file_name: CStrPtr,
pub ca_file_name: CStrPtr,
pub dh_params_file_name: CStrPtr,
pub passphrase: CStrPtr,
pub key: CStrSlice,
pub cert: CStrSlice,
pub ca: CStrSlice,
pub secure_options: u32,
pub request_cert: i32,
pub reject_unauthorized: i32,
pub ssl_ciphers: CStrPtr,
pub protos: CStrPtr,
pub client_renegotiation_limit: u32,
pub client_renegotiation_window: u32,
pub requires_custom_request_ctx: bool,
pub is_using_default_ciphers: bool,
pub low_memory_mode: bool,
pub tls12_cipher_list: CStrPtr,
pub tls13_cipher_suites: CStrPtr,
pub tls_curves_list: CStrPtr,
pub tls_sigalgs_list: CStrPtr,
pub h2_settings_payload: Option<Box<[u8]>>,
pub h2_initial_window_size: u32,
pub h2_pseudo_header_order: Option<Box<[Box<str>]>>,
pub h2_priority_frames: Option<Box<[H2PriorityFrame]>>,
pub ca_certs_der: Option<Box<[Box<[u8]>]>>,
pub cached_hash: AtomicU64,
}
pub type SslConfig = SSLConfig;
pub use crate::h2_frame_parser::H2PriorityFrame;
#[derive(Clone)]
#[repr(transparent)]
pub struct SharedPtr(Arc<SSLConfig>);
pub type WeakPtr = Weak<SSLConfig>;
impl SharedPtr {
#[inline]
pub fn new(config: SSLConfig) -> Self {
Self(Arc::new(config))
}
#[inline]
pub fn get(&self) -> &SSLConfig {
&self.0
}
#[inline]
pub fn clone_weak(&self) -> WeakPtr {
Arc::downgrade(&self.0)
}
#[inline]
pub fn as_arc(&self) -> &Arc<SSLConfig> {
&self.0
}
}
impl core::ops::Deref for SharedPtr {
type Target = SSLConfig;
#[inline]
fn deref(&self) -> &SSLConfig {
&self.0
}
}
impl From<Arc<SSLConfig>> for SharedPtr {
#[inline]
fn from(a: Arc<SSLConfig>) -> Self {
Self(a)
}
}
impl SSLConfig {
pub const ZERO: SSLConfig = SSLConfig {
server_name: core::ptr::null(),
key_file_name: core::ptr::null(),
cert_file_name: core::ptr::null(),
ca_file_name: core::ptr::null(),
dh_params_file_name: core::ptr::null(),
passphrase: core::ptr::null(),
key: None,
cert: None,
ca: None,
secure_options: 0,
request_cert: 0,
reject_unauthorized: 0,
ssl_ciphers: core::ptr::null(),
protos: core::ptr::null(),
client_renegotiation_limit: 0,
client_renegotiation_window: 0,
requires_custom_request_ctx: false,
is_using_default_ciphers: true,
low_memory_mode: false,
tls12_cipher_list: core::ptr::null(),
tls13_cipher_suites: core::ptr::null(),
tls_curves_list: core::ptr::null(),
tls_sigalgs_list: core::ptr::null(),
h2_settings_payload: None,
h2_initial_window_size: 0,
h2_pseudo_header_order: None,
h2_priority_frames: None,
ca_certs_der: None,
cached_hash: AtomicU64::new(0),
};
#[inline]
pub fn zero() -> Self {
Self::default()
}
#[inline]
pub fn server_name_cstr(&self) -> Option<&CStr> {
if self.server_name.is_null() {
None
} else {
Some(unsafe { CStr::from_ptr(self.server_name) })
}
}
#[inline]
pub fn server_name_bytes(&self) -> Option<&[u8]> {
if self.server_name.is_null() {
None
} else {
Some(cstr_bytes(self.server_name))
}
}
#[inline]
pub fn protos_bytes(&self) -> Option<&[u8]> {
if self.protos.is_null() {
None
} else {
Some(cstr_bytes(self.protos))
}
}
#[inline]
pub fn raw_ptr<D>(maybe_shared: Option<&D>) -> Option<*const SSLConfig>
where
D: core::ops::Deref<Target = SSLConfig>,
{
maybe_shared.map(|s| &raw const **s)
}
pub fn as_usockets(&self) -> uws::socket_context::BunSocketContextOptions {
let mut ctx_opts = uws::socket_context::BunSocketContextOptions::default();
if !self.key_file_name.is_null() {
ctx_opts.key_file_name = self.key_file_name;
}
if !self.cert_file_name.is_null() {
ctx_opts.cert_file_name = self.cert_file_name;
}
if !self.ca_file_name.is_null() {
ctx_opts.ca_file_name = self.ca_file_name;
}
if !self.dh_params_file_name.is_null() {
ctx_opts.dh_params_file_name = self.dh_params_file_name;
}
if !self.passphrase.is_null() {
ctx_opts.passphrase = self.passphrase;
}
ctx_opts.ssl_prefer_low_memory_usage = i32::from(self.low_memory_mode);
if let Some(key) = &self.key {
ctx_opts.key = key.as_ptr();
ctx_opts.key_count = key.len() as u32;
}
if let Some(cert) = &self.cert {
ctx_opts.cert = cert.as_ptr();
ctx_opts.cert_count = cert.len() as u32;
}
if let Some(ca) = &self.ca {
ctx_opts.ca = ca.as_ptr();
ctx_opts.ca_count = ca.len() as u32;
}
if !self.ssl_ciphers.is_null() {
ctx_opts.ssl_ciphers = self.ssl_ciphers;
}
ctx_opts.request_cert = self.request_cert;
ctx_opts.reject_unauthorized = self.reject_unauthorized;
ctx_opts
}
pub fn as_usockets_for_client_verification(
&self,
) -> uws::socket_context::BunSocketContextOptions {
let mut opts = self.as_usockets();
opts.request_cert = 1;
opts.reject_unauthorized = 0;
opts
}
pub fn for_client_verification(&self) -> SSLConfig {
let mut copy = self.clone();
copy.request_cert = 1;
copy.reject_unauthorized = 0;
copy
}
pub fn is_same(&self, other: &SSLConfig) -> bool {
macro_rules! eq_cstr {
($f:ident) => {
if !cstr_eq(self.$f, other.$f) {
return false;
}
};
}
macro_rules! eq_slice {
($f:ident) => {
match (&self.$f, &other.$f) {
(Some(a), Some(b)) => {
if a.len() != b.len() {
return false;
}
for (x, y) in a.iter().zip(b.iter()) {
if !cstr_eq(*x, *y) {
return false;
}
}
}
(None, None) => {}
_ => return false,
}
};
}
eq_cstr!(server_name);
eq_cstr!(key_file_name);
eq_cstr!(cert_file_name);
eq_cstr!(ca_file_name);
eq_cstr!(dh_params_file_name);
eq_cstr!(passphrase);
eq_slice!(key);
eq_slice!(cert);
eq_slice!(ca);
if self.secure_options != other.secure_options {
return false;
}
if self.request_cert != other.request_cert {
return false;
}
if self.reject_unauthorized != other.reject_unauthorized {
return false;
}
eq_cstr!(ssl_ciphers);
eq_cstr!(protos);
if self.client_renegotiation_limit != other.client_renegotiation_limit {
return false;
}
if self.client_renegotiation_window != other.client_renegotiation_window {
return false;
}
if self.requires_custom_request_ctx != other.requires_custom_request_ctx {
return false;
}
if self.is_using_default_ciphers != other.is_using_default_ciphers {
return false;
}
if self.low_memory_mode != other.low_memory_mode {
return false;
}
eq_cstr!(tls12_cipher_list);
eq_cstr!(tls13_cipher_suites);
eq_cstr!(tls_curves_list);
eq_cstr!(tls_sigalgs_list);
match (&self.h2_settings_payload, &other.h2_settings_payload) {
(Some(a), Some(b)) => {
if a.len() != b.len() {
return false;
}
if a != b {
return false;
}
}
(None, None) => {}
_ => return false,
}
if self.h2_initial_window_size != other.h2_initial_window_size {
return false;
}
match (&self.h2_pseudo_header_order, &other.h2_pseudo_header_order) {
(Some(a), Some(b)) => {
if a.len() != b.len() || a.iter().zip(b.iter()).any(|(x, y)| x != y) {
return false;
}
}
(None, None) => {}
_ => return false,
}
match (&self.h2_priority_frames, &other.h2_priority_frames) {
(Some(a), Some(b)) => {
if a.len() != b.len() || a.iter().zip(b.iter()).any(|(x, y)| x != y) {
return false;
}
}
(None, None) => {}
_ => return false,
}
match (&self.ca_certs_der, &other.ca_certs_der) {
(Some(a), Some(b)) => {
if a.len() != b.len() || a.iter().zip(b.iter()).any(|(x, y)| x != y) {
return false;
}
}
(None, None) => {}
_ => return false,
}
true
}
pub fn content_hash(&self) -> u64 {
let cached = self.cached_hash.load(Ordering::Relaxed);
if cached != 0 {
return cached;
}
let mut hasher = Wyhash::init(0);
macro_rules! hash_cstr {
($f:ident) => {
if !self.$f.is_null() {
hasher.update(cstr_bytes(self.$f));
}
hasher.update(&[0]);
};
}
macro_rules! hash_slice {
($f:ident) => {
if let Some(slice) = &self.$f {
for s in slice.iter() {
hasher.update(cstr_bytes(*s));
hasher.update(&[0]);
}
}
hasher.update(&[0]);
};
}
hash_cstr!(server_name);
hash_cstr!(key_file_name);
hash_cstr!(cert_file_name);
hash_cstr!(ca_file_name);
hash_cstr!(dh_params_file_name);
hash_cstr!(passphrase);
hash_slice!(key);
hash_slice!(cert);
hash_slice!(ca);
hasher.update(&self.secure_options.to_ne_bytes());
hasher.update(&self.request_cert.to_ne_bytes());
hasher.update(&self.reject_unauthorized.to_ne_bytes());
hash_cstr!(ssl_ciphers);
hash_cstr!(protos);
hasher.update(&self.client_renegotiation_limit.to_ne_bytes());
hasher.update(&self.client_renegotiation_window.to_ne_bytes());
hasher.update(&[u8::from(self.requires_custom_request_ctx)]);
hasher.update(&[u8::from(self.is_using_default_ciphers)]);
hasher.update(&[u8::from(self.low_memory_mode)]);
hash_cstr!(tls12_cipher_list);
hash_cstr!(tls13_cipher_suites);
hash_cstr!(tls_curves_list);
hash_cstr!(tls_sigalgs_list);
if let Some(ref payload) = self.h2_settings_payload {
hasher.update(payload);
}
hasher.update(&self.h2_initial_window_size.to_ne_bytes());
if let Some(ref order) = self.h2_pseudo_header_order {
for name in order.iter() {
hasher.update(name.as_bytes());
hasher.update(&[0]);
}
}
hasher.update(&[0]);
if let Some(ref frames) = self.h2_priority_frames {
for f in frames.iter() {
hasher.update(&f.stream_id.to_ne_bytes());
hasher.update(&f.stream_dependency.to_ne_bytes());
hasher.update(&[u8::from(f.exclusive), f.weight]);
}
}
if let Some(ref ders) = self.ca_certs_der {
for der in ders.iter() {
hasher.update(&der);
hasher.update(&[0]);
}
}
let hash = hasher.final_();
let hash = if hash == 0 { 1 } else { hash };
self.cached_hash.store(hash, Ordering::Relaxed);
hash
}
pub fn deinit(&mut self) {
global_registry::remove(self);
free_string(&mut self.server_name);
free_string(&mut self.key_file_name);
free_string(&mut self.cert_file_name);
free_string(&mut self.ca_file_name);
free_string(&mut self.dh_params_file_name);
free_string(&mut self.passphrase);
free_strings(&mut self.key);
free_strings(&mut self.cert);
free_strings(&mut self.ca);
free_string(&mut self.ssl_ciphers);
free_string(&mut self.protos);
free_string(&mut self.tls12_cipher_list);
free_string(&mut self.tls13_cipher_suites);
free_string(&mut self.tls_curves_list);
free_string(&mut self.tls_sigalgs_list);
self.h2_settings_payload = None;
}
pub fn take_protos(&mut self) -> Option<Box<[u8]>> {
if self.protos.is_null() {
return None;
}
let p = core::mem::replace(&mut self.protos, core::ptr::null());
let bytes = cstr_bytes(p);
let owned = bytes.to_vec().into_boxed_slice();
unsafe { bun_core::free_sensitive(p) };
Some(owned)
}
pub fn take_server_name(&mut self) -> Option<Box<[u8]>> {
if self.server_name.is_null() {
return None;
}
let p = core::mem::replace(&mut self.server_name, core::ptr::null());
let bytes = cstr_bytes(p);
let owned = bytes.to_vec().into_boxed_slice();
unsafe { bun_core::free_sensitive(p) };
Some(owned)
}
}
impl Default for SSLConfig {
fn default() -> Self {
Self::ZERO
}
}
impl Clone for SSLConfig {
fn clone(&self) -> Self {
Self {
server_name: clone_string(self.server_name),
key_file_name: clone_string(self.key_file_name),
cert_file_name: clone_string(self.cert_file_name),
ca_file_name: clone_string(self.ca_file_name),
dh_params_file_name: clone_string(self.dh_params_file_name),
passphrase: clone_string(self.passphrase),
key: clone_strings(&self.key),
cert: clone_strings(&self.cert),
ca: clone_strings(&self.ca),
secure_options: self.secure_options,
request_cert: self.request_cert,
reject_unauthorized: self.reject_unauthorized,
ssl_ciphers: clone_string(self.ssl_ciphers),
protos: clone_string(self.protos),
client_renegotiation_limit: self.client_renegotiation_limit,
client_renegotiation_window: self.client_renegotiation_window,
requires_custom_request_ctx: self.requires_custom_request_ctx,
is_using_default_ciphers: self.is_using_default_ciphers,
low_memory_mode: self.low_memory_mode,
tls12_cipher_list: clone_string(self.tls12_cipher_list),
tls13_cipher_suites: clone_string(self.tls13_cipher_suites),
tls_curves_list: clone_string(self.tls_curves_list),
tls_sigalgs_list: clone_string(self.tls_sigalgs_list),
h2_settings_payload: self.h2_settings_payload.clone(),
h2_initial_window_size: self.h2_initial_window_size,
h2_pseudo_header_order: self.h2_pseudo_header_order.clone(),
h2_priority_frames: self.h2_priority_frames.clone(),
ca_certs_der: self.ca_certs_der.clone(),
cached_hash: AtomicU64::new(0),
}
}
}
impl Drop for SSLConfig {
fn drop(&mut self) {
self.deinit();
}
}
unsafe impl Send for SSLConfig {}
unsafe impl Sync for SSLConfig {}
#[inline]
fn cstr_bytes<'a>(p: CStrPtr) -> &'a [u8] {
debug_assert!(!p.is_null());
unsafe { bun_core::ffi::cstr(p) }.to_bytes()
}
fn cstr_eq(a: CStrPtr, b: CStrPtr) -> bool {
match (a.is_null(), b.is_null()) {
(true, true) => true,
(false, false) => bun_core::strings::eql_long(cstr_bytes(a), cstr_bytes(b), true),
_ => false,
}
}
fn free_strings(slice: &mut CStrSlice) {
if let Some(inner) = slice.take() {
for s in inner.iter() {
unsafe { bun_core::free_sensitive(*s) };
}
}
}
fn free_string(s: &mut CStrPtr) {
if s.is_null() {
return;
}
unsafe { bun_core::free_sensitive(core::mem::replace(s, core::ptr::null())) };
}
fn clone_strings(slice: &CStrSlice) -> CStrSlice {
let inner = slice.as_ref()?;
let mut out = Vec::with_capacity(inner.len());
for s in inner.iter() {
out.push(clone_string(*s));
}
Some(out.into_boxed_slice())
}
fn clone_string(s: CStrPtr) -> CStrPtr {
if s.is_null() {
return core::ptr::null();
}
bun_core::dupe_z(cstr_bytes(s))
}
pub mod global_registry {
use super::*;
static REGISTRY: Mutex<Vec<(u64, WeakPtr)>> = Mutex::new(Vec::new());
pub fn intern(config: SSLConfig) -> SharedPtr {
let hash = config.content_hash();
let new_shared = SharedPtr::new(config);
let mut dispose_new: Option<SharedPtr> = None;
let mut dispose_old_weak: Option<WeakPtr> = None;
let result = {
let mut configs = REGISTRY.lock();
let mut found_idx: Option<usize> = None;
for (i, (h, weak)) in configs.iter().enumerate() {
if *h != hash {
continue;
}
if let Some(existing_shared) = weak.upgrade() {
if existing_shared.is_same(&new_shared) {
dispose_new = Some(new_shared);
drop(configs);
drop(dispose_new);
drop(dispose_old_weak);
return SharedPtr(existing_shared);
}
} else {
found_idx = Some(i);
break;
}
}
if let Some(idx) = found_idx {
dispose_old_weak = Some(core::mem::replace(
&mut configs[idx].1,
new_shared.clone_weak(),
));
configs[idx].0 = hash;
} else {
configs.push((hash, new_shared.clone_weak()));
}
new_shared
};
drop(dispose_new);
drop(dispose_old_weak);
result
}
pub(super) fn remove(config: &SSLConfig) {
let hash = config.cached_hash.load(Ordering::Relaxed);
let self_ptr: *const SSLConfig = config;
let mut configs = REGISTRY.lock();
if configs.is_empty() {
return;
}
let Some(idx) = configs.iter().position(|(h, weak)| {
(hash == 0 || *h == hash) && Weak::as_ptr(weak) == self_ptr
}) else {
return;
};
let (_, weak) = configs.swap_remove(idx);
drop(configs);
drop(weak);
}
}
pub use global_registry as GlobalRegistry;