h2ts_server/wslay.rs
1//! wslay framing (the WebSocket ⇄ byte-stream pump).
2//!
3//! wslay, driven through its event API with `no_buffering` enabled, delivers
4//! each frame's payload **incrementally** via `on_frame_recv_chunk_callback` —
5//! so we never hold a whole frame in memory, no matter how large. Control frames
6//! (ping / close) are auto-handled by wslay.
7//!
8//! wslay is a synchronous, I/O-agnostic state machine: it never touches the
9//! socket itself, only in-memory buffers via callbacks. We drive it from async
10//! Rust — feeding it bytes we read from the WebSocket and flushing bytes it
11//! produces. The HTTP upgrade is done separately in [`accept`](crate::accept),
12//! which hands us the raw upgraded byte stream (no frames have been read yet, so
13//! nothing is lost).
14//!
15//! The wslay context and the shared-state cell are reached from the two
16//! directions as `usize` addresses (which are `Send`), cast back to raw pointers
17//! only inside the synchronous `unsafe` sections. This keeps the future `Send`
18//! (so it composes like a normal async fn) while never letting a bare pointer
19//! live across an `.await`. It is sound because everything runs on one task: the
20//! two directions interleave at awaits but never execute concurrently.
21
22use std::cell::RefCell;
23use std::ffi::c_void;
24use std::future;
25use std::io;
26use std::os::raw::c_int;
27use std::ptr;
28use std::slice;
29use std::sync::Arc;
30use std::time::{Duration, Instant};
31
32use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
33use tokio::sync::{mpsc, Mutex};
34use wslay_sys::*;
35
36const READ_CHUNK: usize = 64 * 1024;
37
38/// Single-task state, shared between async Rust (between wslay calls) and the C
39/// callbacks (during wslay calls). No borrow is ever held across an `.await`,
40/// and everything runs on one task, so the accesses never overlap.
41#[derive(Default)]
42struct Shared {
43 /// Raw WS bytes read from the socket, consumed by `recv_cb`.
44 inbound: Vec<u8>,
45 inbound_pos: usize,
46 /// Bytes wslay wants sent on the WS (framed data + pong/close replies).
47 outbound: Vec<u8>,
48 /// Decoded payload of data frames, to forward to the peer.
49 to_peer: Vec<u8>,
50 /// Whether the frame currently being received is a data (non-control) frame.
51 cur_is_data: bool,
52 /// Received control frames (close/ping/pong), surfaced to the bridge's hooks.
53 control_events: Vec<ControlEvent>,
54}
55
56/// An inbound WebSocket control frame, surfaced from a wslay callback.
57enum ControlEvent {
58 Close { code: u16, reason: Vec<u8> },
59 Ping(Vec<u8>),
60 Pong(Vec<u8>),
61}
62
63#[inline]
64unsafe fn shared<'a>(user_data: *mut c_void) -> &'a RefCell<Shared> {
65 &*(user_data as *const RefCell<Shared>)
66}
67
68/// wslay wants raw bytes from the peer; feed it from `inbound`.
69unsafe extern "C" fn recv_cb(
70 ctx: wslay_event_context_ptr,
71 buf: *mut u8,
72 len: usize,
73 _flags: c_int,
74 user_data: *mut c_void,
75) -> isize {
76 let mut s = shared(user_data).borrow_mut();
77 let avail = s.inbound.len() - s.inbound_pos;
78 if avail == 0 {
79 wslay_event_set_error(ctx, wslay_error_WSLAY_ERR_WOULDBLOCK); // non-blocking: stop
80 return -1;
81 }
82 let n = len.min(avail);
83 let from = s.inbound_pos;
84 ptr::copy_nonoverlapping(s.inbound.as_ptr().add(from), buf, n);
85 s.inbound_pos += n;
86 n as isize
87}
88
89/// wslay wants to send raw bytes; buffer them for the async writer.
90unsafe extern "C" fn send_cb(
91 _ctx: wslay_event_context_ptr,
92 data: *const u8,
93 len: usize,
94 _flags: c_int,
95 user_data: *mut c_void,
96) -> isize {
97 shared(user_data)
98 .borrow_mut()
99 .outbound
100 .extend_from_slice(slice::from_raw_parts(data, len));
101 len as isize
102}
103
104/// A new frame started; remember whether it carries data (vs. a control frame).
105unsafe extern "C" fn frame_start_cb(
106 _ctx: wslay_event_context_ptr,
107 arg: *const wslay_event_on_frame_recv_start_arg,
108 user_data: *mut c_void,
109) {
110 // is_ctrl_frame(opcode) == (opcode >> 3) & 1
111 shared(user_data).borrow_mut().cur_is_data = ((*arg).opcode >> 3) & 1 == 0;
112}
113
114/// A chunk of the current frame's payload arrived — forward it if it's data.
115/// This is the incremental, never-buffer-a-whole-frame path.
116unsafe extern "C" fn frame_chunk_cb(
117 _ctx: wslay_event_context_ptr,
118 arg: *const wslay_event_on_frame_recv_chunk_arg,
119 user_data: *mut c_void,
120) {
121 let mut s = shared(user_data).borrow_mut();
122 if s.cur_is_data {
123 let chunk = slice::from_raw_parts((*arg).data, (*arg).data_length);
124 s.to_peer.extend_from_slice(chunk);
125 }
126}
127
128/// A complete message was received. Under `no_buffering`, data messages arrive
129/// here with a NULL payload (already streamed via `frame_chunk_cb`); control
130/// frames (close/ping/pong) are still delivered complete. Surface the control
131/// ones so the bridge can hand them to the caller's hooks. wslay has already
132/// auto-queued the pong (for a ping) / close echo by the time this runs.
133unsafe extern "C" fn msg_recv_cb(
134 _ctx: wslay_event_context_ptr,
135 arg: *const wslay_event_on_msg_recv_arg,
136 user_data: *mut c_void,
137) {
138 let arg = &*arg;
139 // Opcodes: 0x8 close, 0x9 ping, 0xA pong. Data frames (0x0/0x1/0x2) arrive
140 // with a NULL msg under no_buffering and are ignored here.
141 let event = match arg.opcode {
142 0x8 => {
143 // Close payload is [2-byte code][reason]; wslay pre-parsed the code.
144 let reason = if arg.msg_length >= 2 && !arg.msg.is_null() {
145 slice::from_raw_parts(arg.msg.add(2), arg.msg_length - 2).to_vec()
146 } else {
147 Vec::new()
148 };
149 Some(ControlEvent::Close {
150 code: arg.status_code,
151 reason,
152 })
153 }
154 0x9 | 0xA => {
155 let payload = if arg.msg.is_null() || arg.msg_length == 0 {
156 Vec::new()
157 } else {
158 slice::from_raw_parts(arg.msg, arg.msg_length).to_vec()
159 };
160 Some(if arg.opcode == 0x9 {
161 ControlEvent::Ping(payload)
162 } else {
163 ControlEvent::Pong(payload)
164 })
165 }
166 _ => None,
167 };
168 if let Some(ev) = event {
169 shared(user_data).borrow_mut().control_events.push(ev);
170 }
171}
172
173/// Frees the wslay context on drop (even on early return / panic). Holds the
174/// context as a `usize` address so the guard is naturally `Send` and no bare
175/// pointer is ever kept across an `.await`.
176struct CtxGuard(usize);
177impl Drop for CtxGuard {
178 fn drop(&mut self) {
179 unsafe { wslay_event_context_free(self.0 as wslay_event_context_ptr) };
180 }
181}
182
183/// A WebSocket close: status code and (UTF-8) reason. Uses RFC 6455 close codes;
184/// the reason should be at most 123 bytes.
185#[derive(Debug, Clone, PartialEq, Eq)]
186pub struct CloseFrame {
187 pub code: u16,
188 pub reason: String,
189}
190
191impl Default for CloseFrame {
192 /// 1000 (Normal Closure), empty reason.
193 fn default() -> Self {
194 Self {
195 code: wslay_status_code_WSLAY_CODE_NORMAL_CLOSURE as u16,
196 reason: String::new(),
197 }
198 }
199}
200
201/// A control frame to send into a running bridge (see [`WsControl`]).
202enum ControlCmd {
203 Ping(Vec<u8>),
204 Pong(Vec<u8>),
205 Close(CloseFrame),
206}
207
208/// Sends WebSocket control frames into a running [`bridge_with`]. Cheaply
209/// cloneable; call from any task. Frames are queued and flushed by the bridge on
210/// its next turn; a send fails only once the bridge has ended.
211#[derive(Clone)]
212pub struct WsControl {
213 tx: mpsc::UnboundedSender<ControlCmd>,
214}
215
216impl WsControl {
217 /// Send a Ping with `payload` (≤125 bytes). The peer's Pong surfaces via
218 /// [`BridgeConfig::on_pong`].
219 pub fn ping(&self, payload: impl Into<Vec<u8>>) -> io::Result<()> {
220 self.send(ControlCmd::Ping(payload.into()))
221 }
222
223 /// Send an unsolicited Pong with `payload` (≤125 bytes).
224 pub fn pong(&self, payload: impl Into<Vec<u8>>) -> io::Result<()> {
225 self.send(ControlCmd::Pong(payload.into()))
226 }
227
228 /// Queue a Close with `code` and `reason` (≤123 bytes); the bridge winds down
229 /// after the peer's close echo.
230 pub fn close(&self, code: u16, reason: impl Into<String>) -> io::Result<()> {
231 self.send(ControlCmd::Close(CloseFrame {
232 code,
233 reason: reason.into(),
234 }))
235 }
236
237 fn send(&self, cmd: ControlCmd) -> io::Result<()> {
238 self.tx
239 .send(cmd)
240 .map_err(|_| io::Error::new(io::ErrorKind::BrokenPipe, "bridge has ended"))
241 }
242}
243
244/// The receiving half of a [`control_channel`]; give it to
245/// [`BridgeConfig::control`].
246pub struct ControlReceiver(mpsc::UnboundedReceiver<ControlCmd>);
247
248/// Create a control channel. Keep the [`WsControl`] to send control frames; put
249/// the [`ControlReceiver`] in [`BridgeConfig::control`].
250pub fn control_channel() -> (WsControl, ControlReceiver) {
251 let (tx, rx) = mpsc::unbounded_channel();
252 (WsControl { tx }, ControlReceiver(rx))
253}
254
255/// Hook invoked with the close frame describing why the connection ended.
256pub type CloseHook = Box<dyn FnMut(&CloseFrame) + Send>;
257/// Hook invoked with a received Ping/Pong payload.
258pub type ControlHook = Box<dyn FnMut(&[u8]) + Send>;
259
260/// Server-initiated keepalive: send a Ping when the connection goes idle, and
261/// disconnect the peer if it doesn't respond in time.
262///
263/// This is the *proactive* half of ping/pong — wslay already auto-answers
264/// *incoming* pings, but never initiates its own. Browsers can't send pings from
265/// JavaScript, so liveness for an h2ts client must be driven from the server;
266/// the browser's platform auto-answers these pings transparently.
267#[derive(Debug, Clone)]
268pub struct KeepAlive {
269 /// Send a Ping once the connection has been idle (no frame received) this long.
270 pub interval: Duration,
271 /// If no frame arrives within this long after that Ping, close the peer.
272 pub timeout: Duration,
273 /// Close frame sent to the peer, and surfaced to [`BridgeConfig::on_close`],
274 /// when keepalive fails. Defaults to 1001 (Going Away), "keepalive timeout".
275 pub close: CloseFrame,
276}
277
278impl KeepAlive {
279 /// Keepalive with the given ping `interval` and response `timeout`, closing
280 /// with 1001 (Going Away) on failure.
281 pub fn new(interval: Duration, timeout: Duration) -> Self {
282 Self {
283 interval,
284 timeout,
285 close: CloseFrame {
286 code: wslay_status_code_WSLAY_CODE_GOING_AWAY as u16,
287 reason: "keepalive timeout".to_string(),
288 },
289 }
290 }
291}
292
293impl Default for KeepAlive {
294 /// Sensible server defaults: ping an idle connection every 30s and close it
295 /// (1001 Going Away) if no frame arrives within a further 15s. This is what
296 /// [`BridgeConfig::default`] (and thus [`bridge`] / [`serve_h2`](crate::serve_h2))
297 /// use — keepalive is **on by default**. Tune via [`KeepAlive::new`], or set
298 /// [`BridgeConfig::keepalive`] to `None` to opt out.
299 fn default() -> Self {
300 Self::new(Duration::from_secs(30), Duration::from_secs(15))
301 }
302}
303
304/// Control-frame configuration and hooks for [`bridge_with`] /
305/// [`serve_h2_with`](crate::serve_h2_with).
306///
307/// [`BridgeConfig::default`] enables **server-initiated keepalive on by default**
308/// ([`KeepAlive::default`]: 30s ping / 15s timeout / 1001) so a silently-dead peer
309/// can't leak the bridge; wslay still auto-answers incoming pings, a Normal
310/// Closure is sent on teardown, and no hooks fire. To opt out of keepalive, set
311/// [`keepalive`](BridgeConfig::keepalive) to `None`.
312pub struct BridgeConfig {
313 /// Close frame this side sends when it starts the close after the peer/upstream
314 /// reached a **clean** EOF. Defaults to 1000 Normal Closure, empty reason.
315 pub close: CloseFrame,
316 /// Close frame this side sends when it tears down because the peer/upstream
317 /// **errored** (a read/write failure — e.g. a reset — not a clean EOF).
318 /// Defaults to 1011 (Internal Error); the proxy sets 1014 (Bad Gateway) so a
319 /// failed upstream is distinguishable from a clean shutdown (à la nginx 502).
320 pub error_close: CloseFrame,
321 /// Server-initiated keepalive (ping-and-timeout). Defaults to
322 /// `Some(`[`KeepAlive::default`]`)`; set to `None` to opt out (and optionally
323 /// drive your own pings via [`control_channel`] instead).
324 pub keepalive: Option<KeepAlive>,
325 /// Receiver from [`control_channel`], to send control frames while running.
326 pub control: Option<ControlReceiver>,
327 /// Called once when the connection ends, with the close frame describing why:
328 /// the peer's Close, the keepalive-timeout close, the teardown close, or
329 /// 1006 (Abnormal) if the transport dropped without a Close.
330 pub on_close: Option<CloseHook>,
331 /// Called when a Ping is received (wslay has already auto-queued the Pong).
332 pub on_ping: Option<ControlHook>,
333 /// Called when a Pong is received.
334 pub on_pong: Option<ControlHook>,
335}
336
337impl Default for BridgeConfig {
338 fn default() -> Self {
339 Self {
340 close: CloseFrame::default(),
341 // An errored (vs cleanly-closed) peer/upstream tears down with 1011.
342 error_close: CloseFrame {
343 code: wslay_status_code_WSLAY_CODE_INTERNAL_SERVER_ERROR as u16,
344 reason: String::new(),
345 },
346 // Keepalive is on by default (opt out with `keepalive: None`) so a
347 // peer that dies without a TCP FIN can't leak the bridge task.
348 keepalive: Some(KeepAlive::default()),
349 control: None,
350 on_close: None,
351 on_ping: None,
352 on_pong: None,
353 }
354 }
355}
356
357/// Why the bridge ended — used to surface a close reason to `on_close`.
358enum EndReason {
359 /// The peer sent a Close with this code + reason.
360 PeerClose(CloseFrame),
361 /// This side closed (peer/upstream EOF, or keepalive failure).
362 LocalClose(CloseFrame),
363 /// The WS transport ended without a Close frame (abnormal).
364 Abnormal,
365}
366
367/// Queue a control/data message of `opcode` carrying `payload` (wslay copies it).
368unsafe fn queue_msg(ctx: wslay_event_context_ptr, opcode: u8, payload: &[u8]) {
369 let msg = wslay_event_msg {
370 opcode,
371 msg: payload.as_ptr(),
372 msg_length: payload.len(),
373 };
374 wslay_event_queue_msg(ctx, &msg);
375}
376
377/// Queue a close with `code` + `reason`.
378unsafe fn queue_close(ctx: wslay_event_context_ptr, code: u16, reason: &[u8]) {
379 let (ptr, len) = if reason.is_empty() {
380 (ptr::null(), 0)
381 } else {
382 (reason.as_ptr(), reason.len())
383 };
384 wslay_event_queue_close(ctx, code, ptr, len);
385}
386
387/// Pump bytes full-duplex between a WebSocket and a byte-stream peer until either
388/// side closes, using wslay for framing. Equivalent to [`bridge_with`] with a
389/// default [`BridgeConfig`].
390///
391/// `ws_io` is the raw upgraded WebSocket byte stream from [`accept`](crate::accept).
392/// This is item 3's core: `bridge(ws_io, TcpStream::connect(upstream))` is a
393/// websockify-equivalent WS→TCP proxy.
394pub async fn bridge<S, P>(ws_io: S, peer: P) -> io::Result<()>
395where
396 S: AsyncRead + AsyncWrite + Unpin + Send + 'static,
397 P: AsyncRead + AsyncWrite + Unpin + Send + 'static,
398{
399 bridge_with(ws_io, peer, BridgeConfig::default()).await
400}
401
402/// Like [`bridge`], but with control-frame configuration and hooks
403/// ([`BridgeConfig`]): send control frames via [`control_channel`], observe
404/// received close/ping/pong, and set the close sent on teardown.
405///
406/// WS message payloads flow to `peer`; `peer` bytes flow back as binary WS
407/// frames — all streamed incrementally, never buffering a whole frame.
408pub async fn bridge_with<S, P>(ws_io: S, peer: P, config: BridgeConfig) -> io::Result<()>
409where
410 S: AsyncRead + AsyncWrite + Unpin + Send + 'static,
411 P: AsyncRead + AsyncWrite + Unpin + Send + 'static,
412{
413 let BridgeConfig {
414 close,
415 error_close,
416 keepalive,
417 control,
418 mut on_close,
419 mut on_ping,
420 mut on_pong,
421 } = config;
422 // `ws_to_peer` needs its own copy of the error close (the `peer_to_ws` closure
423 // takes the original).
424 let error_close_ws = error_close.clone();
425
426 let (mut ws_read, ws_write) = tokio::io::split(ws_io);
427 let (mut peer_read, mut peer_write) = tokio::io::split(peer);
428 let ws_write = Arc::new(Mutex::new(ws_write));
429
430 // Last time a frame was received from the WS peer; read by the keepalive
431 // direction to decide when to ping and whether a pong came back. Shared and
432 // `Send`; only ever locked briefly (never across an await).
433 let last_activity = Arc::new(std::sync::Mutex::new(Instant::now()));
434
435 // Lives for the whole future; the callbacks and every direction reach it via
436 // its address (see the module note on Send-safety).
437 let shared: Box<RefCell<Shared>> = Box::new(RefCell::new(Shared::default()));
438 let sp_addr = &*shared as *const RefCell<Shared> as usize;
439
440 // Confine every bare pointer to this block so none can live across an await.
441 let (ctx_addr, _guard) = {
442 let callbacks = wslay_event_callbacks {
443 recv_callback: Some(recv_cb),
444 send_callback: Some(send_cb),
445 genmask_callback: None, // server role never masks
446 on_frame_recv_start_callback: Some(frame_start_cb),
447 on_frame_recv_chunk_callback: Some(frame_chunk_cb),
448 on_frame_recv_end_callback: None,
449 on_msg_recv_callback: Some(msg_recv_cb), // surface control frames
450 };
451 let mut ctx_raw: wslay_event_context_ptr = ptr::null_mut();
452 let rc = unsafe {
453 wslay_event_context_server_init(&mut ctx_raw, &callbacks, sp_addr as *mut c_void)
454 };
455 if rc != 0 || ctx_raw.is_null() {
456 return Err(io::Error::other("wslay_event_context_server_init failed"));
457 }
458 unsafe { wslay_event_config_set_no_buffering(ctx_raw, 1) }; // incremental frames
459 let addr = ctx_raw as usize;
460 (addr, CtxGuard(addr))
461 };
462
463 // Direction 1: WebSocket -> peer. Also fires ping/pong hooks and flushes
464 // wslay's auto-queued pong/close replies. Reports why it ends.
465 let ws_to_peer = {
466 let ws_write = ws_write.clone();
467 let last_activity = last_activity.clone();
468 async move {
469 let mut buf = vec![0u8; READ_CHUNK];
470 loop {
471 let n = ws_read.read(&mut buf).await?;
472 if n == 0 {
473 let _ = peer_write.shutdown().await;
474 return Ok(EndReason::Abnormal); // WS transport EOF, no Close
475 }
476 *last_activity.lock().unwrap() = Instant::now();
477 let (to_peer, outbound, closed, events) = unsafe {
478 let ctx = ctx_addr as wslay_event_context_ptr;
479 let sp = &*(sp_addr as *const RefCell<Shared>);
480 sp.borrow_mut().inbound.extend_from_slice(&buf[..n]);
481 let recv_rc = wslay_event_recv(ctx); // fires callbacks
482 wslay_event_send(ctx); // flush auto-queued pong/close
483 let mut s = sp.borrow_mut();
484 let pos = s.inbound_pos;
485 s.inbound.drain(..pos);
486 s.inbound_pos = 0;
487 let closed = recv_rc != 0 || wslay_event_get_close_received(ctx) != 0;
488 (
489 std::mem::take(&mut s.to_peer),
490 std::mem::take(&mut s.outbound),
491 closed,
492 std::mem::take(&mut s.control_events),
493 )
494 };
495 if !to_peer.is_empty() && peer_write.write_all(&to_peer).await.is_err() {
496 // Upstream write failed: tell the client with the error close
497 // (1011 by default, 1014 for a proxy) rather than a bare 1006.
498 let outbound = unsafe {
499 let ctx = ctx_addr as wslay_event_context_ptr;
500 queue_close(ctx, error_close_ws.code, error_close_ws.reason.as_bytes());
501 wslay_event_send(ctx);
502 std::mem::take(
503 &mut (*(sp_addr as *const RefCell<Shared>)).borrow_mut().outbound,
504 )
505 };
506 if !outbound.is_empty() {
507 let _ = ws_write.lock().await.write_all(&outbound).await;
508 }
509 return Ok(EndReason::LocalClose(error_close_ws.clone()));
510 }
511 let mut peer_close = None;
512 for event in events {
513 match event {
514 ControlEvent::Close { code, reason } => {
515 let code = if code == 0 {
516 wslay_status_code_WSLAY_CODE_NO_STATUS_RCVD as u16
517 } else {
518 code
519 };
520 peer_close = Some(CloseFrame {
521 code,
522 reason: String::from_utf8_lossy(&reason).into_owned(),
523 });
524 }
525 ControlEvent::Ping(p) => {
526 if let Some(cb) = on_ping.as_mut() {
527 cb(&p);
528 }
529 }
530 ControlEvent::Pong(p) => {
531 if let Some(cb) = on_pong.as_mut() {
532 cb(&p);
533 }
534 }
535 }
536 }
537 if !outbound.is_empty() {
538 ws_write.lock().await.write_all(&outbound).await?;
539 }
540 if closed {
541 let _ = peer_write.shutdown().await;
542 return Ok(peer_close.map_or(EndReason::Abnormal, EndReason::PeerClose));
543 }
544 }
545 }
546 };
547
548 // Direction 2: peer -> WebSocket (framed as binary messages).
549 let peer_to_ws = {
550 let ws_write = ws_write.clone();
551 async move {
552 let mut buf = vec![0u8; READ_CHUNK];
553 loop {
554 let n = match peer_read.read(&mut buf).await {
555 Ok(n) if n > 0 => n,
556 other => {
557 // Ok(0) = clean upstream EOF -> `close` (1000 by default; any
558 // h2 GOAWAY already flowed through as bytes). Err = upstream
559 // failure/reset -> `error_close` (1011, or 1014 for a proxy).
560 let cf = if other.is_ok() { &close } else { &error_close };
561 let outbound = unsafe {
562 let ctx = ctx_addr as wslay_event_context_ptr;
563 queue_close(ctx, cf.code, cf.reason.as_bytes());
564 wslay_event_send(ctx);
565 std::mem::take(
566 &mut (*(sp_addr as *const RefCell<Shared>)).borrow_mut().outbound,
567 )
568 };
569 if !outbound.is_empty() {
570 let _ = ws_write.lock().await.write_all(&outbound).await;
571 }
572 return Ok(EndReason::LocalClose(cf.clone()));
573 }
574 };
575 let outbound = unsafe {
576 let ctx = ctx_addr as wslay_event_context_ptr;
577 queue_msg(ctx, wslay_opcode_WSLAY_BINARY_FRAME as u8, &buf[..n]);
578 wslay_event_send(ctx);
579 std::mem::take(
580 &mut (*(sp_addr as *const RefCell<Shared>)).borrow_mut().outbound,
581 )
582 };
583 if !outbound.is_empty() {
584 ws_write.lock().await.write_all(&outbound).await?;
585 }
586 }
587 }
588 };
589
590 // Direction 3: application-injected control frames (ping/pong/close).
591 let control_dir = {
592 let ws_write = ws_write.clone();
593 async move {
594 let mut rx = match control {
595 Some(ControlReceiver(rx)) => rx,
596 // No control channel: park forever so this branch never ends the
597 // bridge on its own.
598 None => return future::pending::<io::Result<EndReason>>().await,
599 };
600 while let Some(cmd) = rx.recv().await {
601 let outbound = unsafe {
602 let ctx = ctx_addr as wslay_event_context_ptr;
603 match &cmd {
604 ControlCmd::Ping(p) => queue_msg(ctx, wslay_opcode_WSLAY_PING as u8, p),
605 ControlCmd::Pong(p) => queue_msg(ctx, wslay_opcode_WSLAY_PONG as u8, p),
606 ControlCmd::Close(cf) => queue_close(ctx, cf.code, cf.reason.as_bytes()),
607 }
608 wslay_event_send(ctx);
609 std::mem::take(
610 &mut (*(sp_addr as *const RefCell<Shared>)).borrow_mut().outbound,
611 )
612 };
613 if !outbound.is_empty() {
614 ws_write.lock().await.write_all(&outbound).await?;
615 }
616 }
617 // All senders dropped: park rather than tear down the live bridge.
618 future::pending::<io::Result<EndReason>>().await
619 }
620 };
621
622 // Direction 4: server-initiated keepalive (ping when idle, close on timeout).
623 let keepalive_dir = {
624 let ws_write = ws_write.clone();
625 let last_activity = last_activity.clone();
626 async move {
627 let ka = match keepalive {
628 Some(ka) => ka,
629 None => return future::pending::<io::Result<EndReason>>().await,
630 };
631 loop {
632 // Wait until the connection has been idle for a full interval.
633 let idle = last_activity.lock().unwrap().elapsed();
634 if idle < ka.interval {
635 tokio::time::sleep(ka.interval - idle).await;
636 continue;
637 }
638 // Idle: send a Ping and note when.
639 let outbound = unsafe {
640 let ctx = ctx_addr as wslay_event_context_ptr;
641 queue_msg(ctx, wslay_opcode_WSLAY_PING as u8, b"");
642 wslay_event_send(ctx);
643 std::mem::take(
644 &mut (*(sp_addr as *const RefCell<Shared>)).borrow_mut().outbound,
645 )
646 };
647 if !outbound.is_empty() {
648 ws_write.lock().await.write_all(&outbound).await?;
649 }
650 let pinged_at = Instant::now();
651 tokio::time::sleep(ka.timeout).await;
652 // No frame (pong or data) since our ping? Disconnect.
653 if *last_activity.lock().unwrap() <= pinged_at {
654 let outbound = unsafe {
655 let ctx = ctx_addr as wslay_event_context_ptr;
656 queue_close(ctx, ka.close.code, ka.close.reason.as_bytes());
657 wslay_event_send(ctx);
658 std::mem::take(
659 &mut (*(sp_addr as *const RefCell<Shared>)).borrow_mut().outbound,
660 )
661 };
662 if !outbound.is_empty() {
663 let _ = ws_write.lock().await.write_all(&outbound).await;
664 }
665 return Ok(EndReason::LocalClose(ka.close));
666 }
667 }
668 }
669 };
670
671 // Single task, cooperative: the directions interleave only at awaits, so the
672 // synchronous wslay sections never overlap. First to finish tears down.
673 let result = tokio::select! {
674 r = ws_to_peer => r,
675 r = peer_to_ws => r,
676 r = control_dir => r,
677 r = keepalive_dir => r,
678 };
679 drop(_guard); // free ctx while `shared` is still alive
680 drop(shared);
681
682 // Surface the close reason once, whatever ended the bridge.
683 let closed_with = match &result {
684 Ok(EndReason::PeerClose(cf)) | Ok(EndReason::LocalClose(cf)) => cf.clone(),
685 Ok(EndReason::Abnormal) => CloseFrame {
686 code: wslay_status_code_WSLAY_CODE_ABNORMAL_CLOSURE as u16,
687 reason: String::new(),
688 },
689 Err(e) => CloseFrame {
690 code: wslay_status_code_WSLAY_CODE_ABNORMAL_CLOSURE as u16,
691 reason: e.to_string(),
692 },
693 };
694 if let Some(cb) = on_close.as_mut() {
695 cb(&closed_with);
696 }
697 result.map(|_| ())
698}