Skip to main content

tachyon_web/ws/
socket.rs

1//! The raw-frame WebSocket engine.
2//!
3//! `tungstenite`'s own high-level `protocol::WebSocket` normalizes frames into `Message`s before
4//! we'd ever see them, discarding the RSV1 bit — which is exactly what `permessage-deflate`
5//! (RFC 7692) needs to tell a compressed message from a plain one. So instead of driving that
6//! high-level type, this engine talks directly to `tungstenite::protocol::frame::FrameSocket`
7//! (which hands us raw [`Frame`]s, header included) over the [`compat::AllowStd`] async/sync
8//! bridge, and reimplements the bits of RFC 6455 that the high-level type would otherwise give us
9//! for free: fragment reassembly, ping/pong/close handling, and server-side unmasking.
10//!
11//! Compression, when negotiated, is applied to a full reassembled message rather than per-frame:
12//! RSV1 is only meaningful on the first frame of a message (continuation frames never set it).
13
14use super::compat::{AllowStd, Direction};
15use super::deflate::PerMessageDeflate;
16use crate::http::error::Error;
17use bytes::{Bytes, BytesMut};
18use futures_util::{Sink, SinkExt, Stream};
19use hyper::header::HeaderValue;
20use hyper_util::rt::TokioIo;
21use std::collections::VecDeque;
22use std::future::poll_fn;
23use std::pin::Pin;
24use std::task::{Context, Poll};
25use tokio_util::sync::PollSender;
26use tungstenite::Error as WsError;
27use tungstenite::protocol::WebSocketConfig;
28use tungstenite::protocol::frame::coding::{Control, Data as OpData, OpCode};
29use tungstenite::protocol::frame::{CloseFrame, Frame, FrameSocket, Utf8Bytes};
30
31pub use tungstenite::Message;
32
33type Io = AllowStd<TokioIo<hyper::upgrade::Upgraded>>;
34
35/// Bound on the channels [`WebSocket::split`] bridges through to its background task — enough to
36/// smooth out scheduling jitter without letting a stalled peer or slow consumer queue unbounded
37/// messages in memory.
38const SPLIT_CHANNEL_CAPACITY: usize = 32;
39
40/// An established WebSocket connection.
41///
42/// See the [module docs](super) for an example.
43pub struct WebSocket {
44    frames: FrameSocket<Io>,
45    protocol: Option<HeaderValue>,
46    config: WebSocketConfig,
47    deflate: Option<PerMessageDeflate>,
48    fragment: Option<Fragment>,
49    outgoing: VecDeque<Frame>,
50    /// Set whenever a frame is handed to the socket's own internal write buffer; cleared once a
51    /// `flush` actually completes. Lets [`WebSocket::poll_drain_outgoing`] skip the flush syscall
52    /// path entirely on the (common) poll where there's nothing new to push out.
53    flush_needed: bool,
54    sent_close: bool,
55    closed: bool,
56}
57
58struct Fragment {
59    opcode: OpData,
60    compressed: bool,
61    buffer: BytesMut,
62}
63
64impl std::fmt::Debug for WebSocket {
65    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66        f.debug_struct("WebSocket").finish_non_exhaustive()
67    }
68}
69
70fn protocol_error(err: &WsError) -> Error {
71    Error::Internal(err.to_string())
72}
73
74/// XORs `data` in place against the 4-byte rolling `mask`, per RFC 6455 §5.3 — processed 8 (then
75/// 4, then 1) bytes at a time rather than byte-by-byte, since every single client frame passes
76/// through here and the mask pattern repeats every 4 bytes regardless of chunk width.
77fn unmask(data: &mut [u8], mask: [u8; 4]) {
78    let mask8 = u64::from_ne_bytes([
79        mask[0], mask[1], mask[2], mask[3], mask[0], mask[1], mask[2], mask[3],
80    ]);
81    let mut chunks8 = data.chunks_exact_mut(8);
82    for chunk in &mut chunks8 {
83        let word =
84            u64::from_ne_bytes([
85                chunk[0], chunk[1], chunk[2], chunk[3], chunk[4], chunk[5], chunk[6], chunk[7],
86            ]) ^ mask8;
87        chunk.copy_from_slice(&word.to_ne_bytes());
88    }
89
90    let mask4 = u32::from_ne_bytes(mask);
91    let mut chunks4 = chunks8.into_remainder().chunks_exact_mut(4);
92    for chunk in &mut chunks4 {
93        let word = u32::from_ne_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]) ^ mask4;
94        chunk.copy_from_slice(&word.to_ne_bytes());
95    }
96
97    for (i, byte) in chunks4.into_remainder().iter_mut().enumerate() {
98        *byte ^= mask[i % 4];
99    }
100}
101
102fn parse_close(payload: &[u8]) -> Result<Option<CloseFrame>, Error> {
103    match payload.len() {
104        0 => Ok(None),
105        1 => Err(Error::Internal("invalid WebSocket close frame".to_string())),
106        _ => {
107            let code = u16::from_be_bytes([payload[0], payload[1]]);
108            let reason = std::str::from_utf8(&payload[2..])
109                .map_err(|_| Error::Internal("WebSocket close reason is not UTF-8".to_string()))?;
110            Ok(Some(CloseFrame { code: code.into(), reason: reason.to_string().into() }))
111        }
112    }
113}
114
115/// Runs one blocking `tungstenite` frame-socket call, translating `WouldBlock` into `Pending`
116/// after registering `cx`'s waker for `direction`.
117fn poll_io<T>(
118    frames: &mut FrameSocket<Io>,
119    direction: &Direction,
120    cx: &Context<'_>,
121    f: impl FnOnce(&mut FrameSocket<Io>) -> Result<T, WsError>,
122) -> Poll<Result<T, WsError>> {
123    frames.get_mut().register(direction, cx);
124    match f(frames) {
125        Ok(value) => Poll::Ready(Ok(value)),
126        Err(WsError::Io(err)) if err.kind() == std::io::ErrorKind::WouldBlock => Poll::Pending,
127        Err(err) => Poll::Ready(Err(err)),
128    }
129}
130
131impl WebSocket {
132    pub(super) fn new(
133        io: TokioIo<hyper::upgrade::Upgraded>,
134        protocol: Option<HeaderValue>,
135        config: WebSocketConfig,
136        deflate: Option<PerMessageDeflate>,
137    ) -> Self {
138        Self {
139            frames: FrameSocket::new(AllowStd::new(io)),
140            protocol,
141            config,
142            deflate,
143            fragment: None,
144            outgoing: VecDeque::new(),
145            flush_needed: false,
146            sent_close: false,
147            closed: false,
148        }
149    }
150
151    /// Pushes any frames queued for output (auto Pong replies, close echoes, user-sent messages)
152    /// out to the wire, retrying on `WouldBlock` until either it's all flushed or an error occurs.
153    /// Skips the flush call entirely when nothing has been written since the last one.
154    fn poll_drain_outgoing(&mut self, cx: &Context<'_>) -> Poll<Result<(), WsError>> {
155        while let Some(frame) = self.outgoing.pop_front() {
156            match poll_io(&mut self.frames, &Direction::Write, cx, |fs| fs.write(frame)) {
157                Poll::Ready(Ok(())) => self.flush_needed = true,
158                other => return other,
159            }
160        }
161        if !self.flush_needed {
162            return Poll::Ready(Ok(()));
163        }
164        match poll_io(&mut self.frames, &Direction::Write, cx, FrameSocket::flush) {
165            Poll::Ready(Ok(())) => {
166                self.flush_needed = false;
167                Poll::Ready(Ok(()))
168            }
169            other => other,
170        }
171    }
172
173    fn finish_message(
174        &mut self,
175        opcode: OpData,
176        compressed: bool,
177        payload: Bytes,
178    ) -> Result<Option<Message>, Error> {
179        let bytes = if compressed {
180            let deflate = self.deflate.as_mut().ok_or_else(|| {
181                Error::Internal("RSV1 set but permessage-deflate was not negotiated".to_string())
182            })?;
183            Bytes::from(deflate.decompress(&payload, self.config.max_message_size)?)
184        } else {
185            payload
186        };
187        match opcode {
188            OpData::Text => {
189                let text = Utf8Bytes::try_from(bytes)
190                    .map_err(|e| Error::Internal(format!("invalid UTF-8 in text message: {e}")))?;
191                Ok(Some(Message::Text(text)))
192            }
193            OpData::Binary => Ok(Some(Message::Binary(bytes))),
194            OpData::Continue | OpData::Reserved(_) => unreachable!("caller only passes Text/Binary"),
195        }
196    }
197
198    fn handle_frame(&mut self, frame: Frame) -> Result<Option<Message>, Error> {
199        let header = frame.header().clone();
200        let raw = frame.into_payload();
201        let payload = if let Some(mask) = header.mask {
202            // `raw` is uniquely owned at this point, so this reclaims its buffer instead of
203            // copying — falls back to a copy only if some other clone of the `Bytes` is
204            // (unexpectedly) still alive.
205            let mut buf = raw.try_into_mut().unwrap_or_else(|shared| BytesMut::from(&shared[..]));
206            unmask(&mut buf, mask);
207            buf.freeze()
208        } else if self.config.accept_unmasked_frames {
209            raw
210        } else {
211            return Err(Error::Internal(
212                "received an unmasked frame from the client".to_string(),
213            ));
214        };
215
216        match header.opcode {
217            OpCode::Control(Control::Ping) => {
218                self.outgoing.push_back(Frame::pong(payload.clone()));
219                Ok(Some(Message::Ping(payload)))
220            }
221            OpCode::Control(Control::Pong) => Ok(Some(Message::Pong(payload))),
222            OpCode::Control(Control::Close) => {
223                let close_frame = parse_close(&payload)?;
224                if !self.sent_close {
225                    self.outgoing.push_back(Frame::close(close_frame.clone()));
226                    self.sent_close = true;
227                }
228                self.closed = true;
229                Ok(Some(Message::Close(close_frame)))
230            }
231            OpCode::Control(Control::Reserved(code)) => Err(Error::Internal(format!(
232                "received reserved WebSocket control opcode {code}"
233            ))),
234            OpCode::Data(data @ (OpData::Text | OpData::Binary)) => {
235                if self.fragment.is_some() {
236                    return Err(Error::Internal(
237                        "received a new data frame while a fragmented message was in progress"
238                            .to_string(),
239                    ));
240                }
241                if header.is_final {
242                    self.finish_message(data, header.rsv1, payload)
243                } else {
244                    let buffer =
245                        payload.try_into_mut().unwrap_or_else(|shared| BytesMut::from(&shared[..]));
246                    self.fragment = Some(Fragment { opcode: data, compressed: header.rsv1, buffer });
247                    Ok(None)
248                }
249            }
250            OpCode::Data(OpData::Continue) => {
251                let fragment = self.fragment.as_mut().ok_or_else(|| {
252                    Error::Internal("received a continuation frame with no message in progress".to_string())
253                })?;
254                fragment.buffer.extend_from_slice(&payload);
255                if let Some(max) = self.config.max_message_size
256                    && fragment.buffer.len() > max
257                {
258                    return Err(Error::Internal(
259                        "WebSocket message exceeds the configured maximum size".to_string(),
260                    ));
261                }
262                if header.is_final {
263                    let Fragment { opcode, compressed, buffer } =
264                        self.fragment.take().unwrap_or_else(|| unreachable!());
265                    self.finish_message(opcode, compressed, buffer.freeze())
266                } else {
267                    Ok(None)
268                }
269            }
270            OpCode::Data(OpData::Reserved(code)) => {
271                Err(Error::Internal(format!("received reserved WebSocket data opcode {code}")))
272            }
273        }
274    }
275
276    fn poll_recv(&mut self, cx: &Context<'_>) -> Poll<Option<Result<Message, Error>>> {
277        loop {
278            match self.poll_drain_outgoing(cx) {
279                Poll::Ready(Ok(())) => {}
280                Poll::Ready(Err(err)) => {
281                    self.closed = true;
282                    return Poll::Ready(Some(Err(protocol_error(&err))));
283                }
284                Poll::Pending => return Poll::Pending,
285            }
286            if self.closed {
287                return Poll::Ready(None);
288            }
289
290            let max_frame_size = self.config.max_frame_size;
291            let frame = match poll_io(&mut self.frames, &Direction::Read, cx, |fs| {
292                fs.read(max_frame_size)
293            }) {
294                Poll::Ready(Ok(Some(frame))) => frame,
295                Poll::Ready(Ok(None)) => {
296                    self.closed = true;
297                    return Poll::Ready(None);
298                }
299                Poll::Ready(Err(err)) => {
300                    self.closed = true;
301                    return Poll::Ready(Some(Err(protocol_error(&err))));
302                }
303                Poll::Pending => return Poll::Pending,
304            };
305
306            match self.handle_frame(frame) {
307                Ok(Some(msg)) => return Poll::Ready(Some(Ok(msg))),
308                Ok(None) => {}
309                Err(err) => {
310                    self.closed = true;
311                    return Poll::Ready(Some(Err(err)));
312                }
313            }
314        }
315    }
316
317    fn queue_data(&mut self, opcode: OpData, payload: Bytes) -> Result<(), Error> {
318        let compressed = match &mut self.deflate {
319            Some(deflate) => deflate.compress_if_smaller(&payload)?,
320            None => None,
321        };
322        let (rsv1, bytes) =
323            compressed.map_or_else(move || (false, payload), |c| (true, Bytes::from(c)));
324        let mut frame = Frame::message(bytes, OpCode::Data(opcode), true);
325        frame.header_mut().rsv1 = rsv1;
326        self.outgoing.push_back(frame);
327        Ok(())
328    }
329
330    fn queue_message(&mut self, msg: Message) -> Result<(), Error> {
331        match msg {
332            Message::Text(text) => self.queue_data(OpData::Text, Bytes::from(text)),
333            Message::Binary(data) => self.queue_data(OpData::Binary, data),
334            Message::Ping(data) => {
335                self.outgoing.push_back(Frame::ping(data));
336                Ok(())
337            }
338            Message::Pong(data) => {
339                self.outgoing.push_back(Frame::pong(data));
340                Ok(())
341            }
342            Message::Close(frame) => {
343                self.outgoing.push_back(Frame::close(frame));
344                self.sent_close = true;
345                Ok(())
346            }
347            Message::Frame(frame) => {
348                self.outgoing.push_back(frame);
349                Ok(())
350            }
351        }
352    }
353
354    /// Receive the next message. Returns `None` once the stream has closed.
355    pub async fn recv(&mut self) -> Option<Result<Message, Error>> {
356        poll_fn(|cx| self.poll_recv(cx)).await
357    }
358
359    /// Send a message.
360    ///
361    /// # Errors
362    ///
363    /// Returns an error if the underlying connection has been closed or a protocol
364    /// error occurs while writing.
365    pub async fn send(&mut self, msg: Message) -> Result<(), Error> {
366        self.queue_message(msg)?;
367        poll_fn(|cx| self.poll_drain_outgoing(cx)).await.map_err(|e| protocol_error(&e))
368    }
369
370    /// Flush any buffered outgoing messages.
371    ///
372    /// # Errors
373    ///
374    /// Returns an error if the underlying connection has been closed or a protocol
375    /// error occurs while flushing.
376    pub async fn flush(&mut self) -> Result<(), Error> {
377        poll_fn(|cx| self.poll_drain_outgoing(cx)).await.map_err(|e| protocol_error(&e))
378    }
379
380    /// Gracefully close the connection, consuming it.
381    ///
382    /// # Errors
383    ///
384    /// Returns an error if a protocol error occurs while closing.
385    pub async fn close(mut self) -> Result<(), Error> {
386        if !self.sent_close {
387            self.outgoing.push_back(Frame::close(None));
388            self.sent_close = true;
389        }
390        poll_fn(|cx| self.poll_drain_outgoing(cx)).await.map_err(|e| protocol_error(&e))
391    }
392
393    /// The selected WebSocket subprotocol, if one was negotiated.
394    #[must_use]
395    pub const fn protocol(&self) -> Option<&HeaderValue> {
396        self.protocol.as_ref()
397    }
398
399    /// Split into independent sink and stream halves, for concurrent read/write tasks.
400    ///
401    /// Internally this hands the connection off to a background task (since the raw frame
402    /// socket, like a plain TCP stream, isn't safe to drive concurrently from two tasks at once)
403    /// and bridges to it over a pair of bounded channels — bounded so a stalled peer or a
404    /// consumer that stops polling the stream applies real backpressure instead of letting
405    /// queued messages grow without limit.
406    pub fn split(
407        self,
408    ) -> (
409        impl Sink<Message, Error = Error> + Send,
410        impl Stream<Item = Result<Message, Error>> + Send,
411    ) {
412        let (out_tx, mut out_rx) = tokio::sync::mpsc::channel::<Message>(SPLIT_CHANNEL_CAPACITY);
413        let (in_tx, in_rx) =
414            tokio::sync::mpsc::channel::<Result<Message, Error>>(SPLIT_CHANNEL_CAPACITY);
415
416        tokio::spawn(async move {
417            let mut socket = self;
418            loop {
419                tokio::select! {
420                    incoming = socket.recv() => {
421                        match incoming {
422                            Some(msg) => {
423                                if in_tx.send(msg).await.is_err() {
424                                    break;
425                                }
426                            }
427                            None => break,
428                        }
429                    }
430                    outgoing = out_rx.recv() => {
431                        match outgoing {
432                            Some(msg) => {
433                                if socket.send(msg).await.is_err() {
434                                    break;
435                                }
436                            }
437                            None => break,
438                        }
439                    }
440                }
441            }
442        });
443
444        (PollSender::new(out_tx).sink_map_err(map_poll_sender_err), SplitStream { rx: in_rx })
445    }
446}
447
448fn map_poll_sender_err<T>(_: tokio_util::sync::PollSendError<T>) -> Error {
449    Error::Internal("WebSocket connection closed".to_string())
450}
451
452struct SplitStream {
453    rx: tokio::sync::mpsc::Receiver<Result<Message, Error>>,
454}
455
456impl Stream for SplitStream {
457    type Item = Result<Message, Error>;
458
459    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
460        self.rx.poll_recv(cx)
461    }
462}