use std::ffi::CString;
use std::io::{self, Read, Write};
use std::net::TcpStream;
use std::pin::Pin;
use std::task::{Context, Poll};
use bao_boringssl_bridge::connection::{TlsConnection, TlsError, TlsState};
use bao_boringssl_bridge::{TlsClient, session_cache};
const BUF_SIZE: usize = 16_384;
#[derive(Clone, Debug, Default)]
pub struct WsTlsOptions {
pub sigalg_list: Option<String>,
pub alpn_wire: Option<Vec<u8>>,
pub curves_list: Option<String>,
pub ignore_certificate_errors: bool,
}
pub fn stealth_pc_salt(
sigalg_list: Option<&str>,
alpn_wire: Option<&[u8]>,
curves_list: Option<&str>,
) -> u64 {
use std::hash::{Hash, Hasher};
let mut h = std::collections::hash_map::DefaultHasher::new();
sigalg_list.hash(&mut h);
alpn_wire.hash(&mut h);
curves_list.hash(&mut h);
h.finish()
}
pub struct WsTlsStream {
tcp: tokio::net::TcpStream,
tls: TlsConnection,
outgoing: Vec<u8>,
}
impl WsTlsStream {
pub fn new(
tcp: tokio::net::TcpStream,
tls_client: &TlsClient,
host: &str,
port: u16,
opts: &WsTlsOptions,
) -> Result<Self, TlsError> {
let mut tls = TlsConnection::new_client(tls_client, host)?;
let ssl = tls.ssl_ptr();
unsafe {
if let Some(ref sigalg_str) = opts.sigalg_list {
let sigalg_c = CString::new(sigalg_str.as_str()).map_err(|_| TlsError::BoringSSL("interior NUL in WS TLS sigalg list"))?;
if bun_boringssl::c::SSL_set1_sigalgs_list(ssl, sigalg_c.as_ptr()) == 0 {
return Err(TlsError::BoringSSL("WS TLS: SSL_set1_sigalgs_list failed"));
}
}
if let Some(ref alpn_wire) = opts.alpn_wire {
if bun_boringssl::c::SSL_set_alpn_protos(ssl, alpn_wire.as_ptr(), alpn_wire.len())
!= 0
{
return Err(TlsError::BoringSSL("WS TLS: SSL_set_alpn_protos failed"));
}
}
}
if let Some(ref curves_str) = opts.curves_list {
let curves_c = CString::new(curves_str.as_str()).map_err(|_| TlsError::BoringSSL("interior NUL in WS TLS curves list"))?;
if tls.set_curves_list(curves_c.as_ptr()) == 0 {
return Err(TlsError::BoringSSL("WS TLS: SSL_set1_curves_list failed"));
}
}
if opts.ignore_certificate_errors {
tls.set_verify_off();
}
let profile_salt = stealth_pc_salt(
opts.sigalg_list.as_deref(),
opts.alpn_wire.as_deref(),
opts.curves_list.as_deref(),
);
session_cache::offer_session(ssl, host, port, profile_salt);
Ok(Self {
tcp,
tls,
outgoing: Vec::new(),
})
}
pub async fn handshake(&mut self) -> io::Result<()> {
loop {
match self.tls.process() {
Ok(result) => {
self.flush_outgoing().await?;
match result.state {
TlsState::Active => return Ok(()),
TlsState::PeerClosed | TlsState::Closed => {
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
"TLS peer closed during handshake",
));
},
TlsState::Handshaking => {
self.read_from_tcp().await?;
},
TlsState::PendingCertificate => {
return Err(io::Error::new(
io::ErrorKind::Other,
"TLS certificate selection pending without a resolver",
));
},
}
},
Err(TlsError::BoringSSL(msg)) => {
return Err(io::Error::new(io::ErrorKind::Other, msg));
},
Err(e) => {
return Err(io::Error::new(io::ErrorKind::Other, e.to_string()));
},
}
}
}
async fn read_from_tcp(&mut self) -> io::Result<()> {
let mut buf = [0u8; BUF_SIZE];
self.tcp.readable().await?;
match self.tcp.try_read(&mut buf) {
Ok(0) => {
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
"TCP connection closed",
));
},
Ok(n) => {
self.tls.feed(&buf[..n]);
},
Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => {},
Err(e) => return Err(e),
}
Ok(())
}
async fn flush_outgoing(&mut self) -> io::Result<()> {
if self.outgoing.is_empty() {
self.outgoing = self.tls.take_outgoing();
}
while !self.outgoing.is_empty() {
self.tcp.writable().await?;
match self.tcp.try_write(&self.outgoing) {
Ok(n) => {
self.outgoing = self.outgoing[n..].to_vec();
},
Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => {
break;
},
Err(e) => return Err(e),
}
}
Ok(())
}
pub fn alpn_protocol(&self) -> Option<&[u8]> {
self.tls.alpn_protocol()
}
}
impl tokio::io::AsyncRead for WsTlsStream {
fn poll_read(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut tokio::io::ReadBuf<'_>,
) -> Poll<io::Result<()>> {
let this = self.get_mut();
loop {
match this.tls.process() {
Ok(result) => {
if result.outgoing_bytes > 0 {
let outgoing = this.tls.take_outgoing();
this.outgoing.extend_from_slice(&outgoing);
}
if !result.plaintext.is_empty() {
for chunk in &result.plaintext {
let remaining = buf.remaining();
let to_copy = remaining.min(chunk.len());
if to_copy > 0 {
buf.put_slice(&chunk[..to_copy]);
}
}
return Poll::Ready(Ok(()));
}
match result.state {
TlsState::Active | TlsState::Handshaking => {
break;
},
TlsState::PeerClosed | TlsState::Closed => {
return Poll::Ready(Ok(())); },
TlsState::PendingCertificate => {
return Poll::Ready(Err(io::Error::new(
io::ErrorKind::Other,
"TLS certificate selection pending without a resolver",
)));
},
}
},
Err(TlsError::NotReady) => break,
Err(TlsError::BoringSSL(msg)) => {
return Poll::Ready(Err(io::Error::new(io::ErrorKind::Other, msg)));
},
Err(e) => {
return Poll::Ready(Err(io::Error::new(
io::ErrorKind::Other,
e.to_string(),
)));
},
}
}
let mut tcp_buf = [0u8; BUF_SIZE];
let mut read_buf = tokio::io::ReadBuf::new(&mut tcp_buf);
match Pin::new(&mut this.tcp).poll_read(cx, &mut read_buf) {
Poll::Ready(Ok(())) => {
let n = read_buf.filled().len();
if n == 0 {
return Poll::Ready(Ok(()));
}
this.tls.feed(read_buf.filled());
match this.tls.process() {
Ok(result) => {
if result.outgoing_bytes > 0 {
let outgoing = this.tls.take_outgoing();
this.outgoing.extend_from_slice(&outgoing);
}
if !result.plaintext.is_empty() {
for chunk in &result.plaintext {
let remaining = buf.remaining();
let to_copy = remaining.min(chunk.len());
if to_copy > 0 {
buf.put_slice(&chunk[..to_copy]);
}
}
return Poll::Ready(Ok(()));
}
Poll::Pending
},
Err(TlsError::NotReady) => Poll::Pending,
Err(e) => Poll::Ready(Err(io::Error::new(
io::ErrorKind::Other,
e.to_string(),
))),
}
},
Poll::Ready(Err(e)) => Poll::Ready(Err(e)),
Poll::Pending => Poll::Pending,
}
}
}
impl tokio::io::AsyncWrite for WsTlsStream {
fn poll_write(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<Result<usize, io::Error>> {
let this = self.get_mut();
match this.tls.write(buf) {
Ok(n) => {
let outgoing = this.tls.take_outgoing();
this.outgoing.extend_from_slice(&outgoing);
while !this.outgoing.is_empty() {
match Pin::new(&mut this.tcp).poll_write(cx, &this.outgoing) {
Poll::Ready(Ok(0)) => {
return Poll::Ready(Err(io::Error::new(
io::ErrorKind::WriteZero,
"TCP write returned zero",
)));
},
Poll::Ready(Ok(written)) => {
this.outgoing = this.outgoing[written..].to_vec();
},
Poll::Ready(Err(e)) => return Poll::Ready(Err(e)),
Poll::Pending => return Poll::Pending,
}
}
Poll::Ready(Ok(n))
},
Err(TlsError::NotReady) => Poll::Pending,
Err(e) => Poll::Ready(Err(io::Error::new(io::ErrorKind::Other, e.to_string()))),
}
}
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), io::Error>> {
let this = self.get_mut();
while !this.outgoing.is_empty() {
match Pin::new(&mut this.tcp).poll_write(cx, &this.outgoing) {
Poll::Ready(Ok(0)) => {
return Poll::Ready(Err(io::Error::new(
io::ErrorKind::WriteZero,
"TCP write returned zero during flush",
)));
},
Poll::Ready(Ok(written)) => {
this.outgoing = this.outgoing[written..].to_vec();
},
Poll::Ready(Err(e)) => return Poll::Ready(Err(e)),
Poll::Pending => return Poll::Pending,
}
}
Pin::new(&mut this.tcp).poll_flush(cx)
}
fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), io::Error>> {
let this = self.get_mut();
let _ = this.tls.queue_close_notify();
let outgoing = this.tls.take_outgoing();
if !outgoing.is_empty() {
this.outgoing.extend_from_slice(&outgoing);
}
while !this.outgoing.is_empty() {
match Pin::new(&mut this.tcp).poll_write(cx, &this.outgoing) {
Poll::Ready(Ok(0)) => break,
Poll::Ready(Ok(written)) => {
this.outgoing = this.outgoing[written..].to_vec();
},
Poll::Ready(Err(_)) => break,
Poll::Pending => return Poll::Pending,
}
}
Pin::new(&mut this.tcp).poll_shutdown(cx)
}
}
pub struct TlsIoStream {
tcp: TcpStream,
tls: TlsConnection,
pending_plain: Vec<u8>,
pending_off: usize,
}
impl TlsIoStream {
pub fn new(tcp: TcpStream, tls: TlsConnection) -> Self {
Self {
tcp,
tls,
pending_plain: Vec::new(),
pending_off: 0,
}
}
fn pump_inbound(&mut self) -> io::Result<Vec<u8>> {
loop {
self.flush_outgoing()?;
let res = self
.tls
.process()
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?;
if !res.plaintext.is_empty() {
let mut joined = Vec::new();
for chunk in res.plaintext {
joined.extend_from_slice(&chunk);
}
return Ok(joined);
}
let mut buf = [0u8; BUF_SIZE];
match self.tcp.read(&mut buf) {
Ok(0) => {
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
"tls peer closed",
));
},
Ok(n) => self.tls.feed(&buf[..n]),
Err(ref e)
if e.kind() == io::ErrorKind::WouldBlock
|| e.kind() == io::ErrorKind::TimedOut =>
{
return Err(io::Error::from(io::ErrorKind::WouldBlock));
},
Err(e) => return Err(e),
}
}
}
fn flush_outgoing(&mut self) -> io::Result<()> {
let outgoing = self.tls.take_outgoing();
if outgoing.is_empty() {
return Ok(());
}
self.tcp.write_all(&outgoing)
}
pub fn drive_handshake(&mut self) -> io::Result<()> {
loop {
let res = self
.tls
.process()
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?;
loop {
let outgoing = self.tls.take_outgoing();
if outgoing.is_empty() {
break;
}
self.tcp.write_all(&outgoing)?;
}
match res.state {
TlsState::Active | TlsState::PeerClosed => return Ok(()),
TlsState::Handshaking => {
let mut buf = [0u8; BUF_SIZE];
match self.tcp.read(&mut buf) {
Ok(n) if n > 0 => self.tls.feed(&buf[..n]),
_ => {
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
"tls handshake stalled",
));
},
}
},
TlsState::Closed => {
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
"tls closed during handshake",
));
},
TlsState::PendingCertificate => {
return Err(io::Error::new(
io::ErrorKind::Other,
"tls certificate selection pending without a resolver",
));
},
}
}
}
pub fn alpn_protocol(&self) -> Option<&[u8]> {
self.tls.alpn_protocol()
}
}
impl io::Read for TlsIoStream {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
if self.pending_off >= self.pending_plain.len() {
self.pending_plain = self.pump_inbound()?;
self.pending_off = 0;
}
let avail = &self.pending_plain[self.pending_off..];
let n = avail.len().min(buf.len());
buf[..n].copy_from_slice(&avail[..n]);
self.pending_off += n;
Ok(n)
}
}
impl io::Write for TlsIoStream {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
let written = self
.tls
.write(buf)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?;
self.flush_outgoing()?;
Ok(written)
}
fn flush(&mut self) -> io::Result<()> {
self.tcp.flush()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[unsafe(no_mangle)]
extern "Rust" fn __bun_run_file_poll(_poll: *mut bun_io::FilePoll, _size_or_offset: i64) {}
#[unsafe(no_mangle)]
extern "Rust" fn __bun_crash_handler_out_of_memory() -> ! {
eprintln!("bun: out of memory");
std::process::abort()
}
#[test]
fn stealth_pc_salt_is_deterministic_and_sensitive() {
let a = stealth_pc_salt(Some("a:b"), Some(&[8, b'h']), Some("X25519"));
let b = stealth_pc_salt(Some("a:b"), Some(&[8, b'h']), Some("X25519"));
assert_eq!(a, b, "same parameter set must salt identically");
assert_ne!(a, stealth_pc_salt(None, Some(&[8, b'h']), Some("X25519")));
assert_ne!(a, stealth_pc_salt(Some("a:b"), None, Some("X25519")));
assert_ne!(a, stealth_pc_salt(Some("a:b"), Some(&[8, b'h']), None));
assert_ne!(
stealth_pc_salt(None, Some(&[2, b'h', b'2', 8, b'h']), None),
stealth_pc_salt(None, Some(&[8, b'h']), None)
);
}
#[test]
fn ws_tls_stream_async_roundtrip() {
use std::net::TcpListener;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
bao_native_stubs::force_link();
let (cert_pem, key_pem) =
bao_boringssl_bridge::generate_self_signed_pem("localhost", 1)
.expect("generate self-signed cert");
let tls_server =
bao_boringssl_bridge::TlsServer::new(&cert_pem, &key_pem).expect("TlsServer::new");
let listener = TcpListener::bind("127.0.0.1:0").expect("bind");
let port = listener.local_addr().unwrap().port();
let server = std::thread::spawn(move || {
let (tcp, _) = listener.accept().expect("accept");
let tls_conn = tls_server.accept().expect("tls accept");
let mut io = TlsIoStream::new(tcp, tls_conn);
io.drive_handshake().expect("server tls handshake");
bun_uws::ws_handshake::server_handshake(&mut io).expect("server ws handshake");
let mut decoder = bun_uws::ws_codec::FrameDecoder::new();
let mut encoder = bun_uws::ws_codec::FrameEncoder::new();
let header = decoder.decode_frame(&mut io).expect("decode").expect("frame");
let payload = if header.mask {
let key = decoder.take_mask();
let mut p = decoder.take_payload(&header);
bun_uws::ws_codec::apply_mask(&mut p, &key);
p
} else {
decoder.take_payload(&header)
};
let reply = encoder
.encode_text(&String::from_utf8_lossy(&payload))
.to_vec();
io.write_all(&reply).expect("server write");
});
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("tokio runtime")
.block_on(async move {
let tcp = tokio::net::TcpStream::connect(("127.0.0.1", port))
.await
.expect("tcp connect");
let opts = WsTlsOptions {
alpn_wire: Some(vec![8, b'h', b't', b't', b'p', b'/', b'1', b'.', b'1']),
..Default::default()
};
let tls_client = TlsClient::new().expect("TlsClient");
let mut stream = WsTlsStream::new(tcp, &tls_client, "127.0.0.1", port, &opts)
.expect("WsTlsStream::new");
stream.handshake().await.expect("client tls handshake");
let key = bun_uws::ws_handshake::generate_sec_websocket_key();
let request = format!(
"GET / HTTP/1.1\r\nHost: 127.0.0.1\r\nUpgrade: websocket\r\n\
Connection: Upgrade\r\nSec-WebSocket-Key: {}\r\n\
Sec-WebSocket-Version: 13\r\n\r\n",
key
);
stream.write_all(request.as_bytes()).await.expect("ws upgrade send");
let mut buf = Vec::new();
let mut chunk = [0u8; 512];
while !buf.windows(4).any(|w| w == b"\r\n\r\n") {
let n = stream.read(&mut chunk).await.expect("read 101");
assert!(n > 0, "EOF during upgrade response");
buf.extend_from_slice(&chunk[..n]);
}
let headers = String::from_utf8_lossy(&buf).to_string();
assert!(
headers.starts_with("HTTP/1.1 101"),
"unexpected upgrade response: {}",
headers
);
let expected_accept = bun_uws::ws_handshake::compute_accept(&key);
assert!(
headers.contains(&expected_accept),
"missing Sec-WebSocket-Accept in: {}",
headers
);
let mut encoder = bun_uws::ws_codec::FrameEncoder::new();
let frame = encoder
.encode_frame(
bun_uws::ws_codec::Opcode::Text,
b"ping",
Some(bun_uws::ws_codec::gen_mask_key()),
)
.to_vec();
stream.write_all(&frame).await.expect("frame send");
let mut reply = Vec::new();
while reply.len() < 2 + 4 {
let n = stream.read(&mut chunk).await.expect("read echo");
assert!(n > 0, "EOF waiting for echo");
reply.extend_from_slice(&chunk[..n]);
}
assert_eq!(&reply[..2], &[0x81, 0x04], "echo frame header");
assert_eq!(&reply[2..6], b"ping");
});
server.join().expect("server thread");
}
}