1use std::io;
2use std::time::Duration;
3
4use async_trait::async_trait;
5use futures_util::stream::{SplitSink, SplitStream};
6use futures_util::{SinkExt, StreamExt};
7use http::HeaderValue;
8use http::header::{SEC_WEBSOCKET_EXTENSIONS, SEC_WEBSOCKET_PROTOCOL, USER_AGENT};
9use tokio::io::{AsyncRead, AsyncWrite};
10use tokio::sync::watch;
11use tokio_tungstenite::WebSocketStream;
12use tokio_tungstenite::tungstenite::error::{CapacityError, Error as TungsteniteError};
13use tokio_tungstenite::tungstenite::protocol::WebSocketConfig;
14use tokio_tungstenite::tungstenite::protocol::frame::CloseFrame;
15use tokio_tungstenite::tungstenite::protocol::frame::coding::CloseCode;
16use tokio_tungstenite::tungstenite::{Message as Frame, client::IntoClientRequest};
17
18use crate::error::Error;
19use crate::transport::{Conn, DialOptions, Transport};
20
21const USER_AGENT_VALUE: &str = concat!("actioncable/", env!("CARGO_PKG_VERSION"));
22const CLOSE_TIMEOUT: Duration = Duration::from_secs(1);
23
24const CLOSE_NO_STATUS: u16 = 1005;
26
27const MAX_CLOSE_REASON_BYTES: usize = 123;
30
31#[derive(Debug, Clone)]
36pub struct WebSocketTransport {
37 handshake_timeout: Duration,
38 write_timeout: Duration,
39 max_message_size: usize,
40}
41
42impl WebSocketTransport {
43 pub fn new() -> WebSocketTransport {
46 WebSocketTransport {
47 handshake_timeout: Duration::from_secs(10),
48 write_timeout: Duration::from_secs(10),
49 max_message_size: 8 << 20,
50 }
51 }
52
53 pub fn handshake_timeout(mut self, timeout: Duration) -> WebSocketTransport {
56 self.handshake_timeout = timeout;
57 self
58 }
59
60 pub fn write_timeout(mut self, timeout: Duration) -> WebSocketTransport {
64 self.write_timeout = timeout;
65 self
66 }
67
68 pub fn max_message_size(mut self, bytes: usize) -> WebSocketTransport {
70 self.max_message_size = bytes;
71 self
72 }
73}
74
75impl Default for WebSocketTransport {
76 fn default() -> WebSocketTransport {
77 WebSocketTransport::new()
78 }
79}
80
81#[async_trait]
82impl Transport for WebSocketTransport {
83 async fn dial(&self, url: &str, options: DialOptions) -> Result<Box<dyn Conn>, Error> {
84 let request = upgrade_request(url, options)?;
85 let config = WebSocketConfig::default()
88 .max_message_size(Some(self.max_message_size))
89 .max_frame_size(Some(self.max_message_size));
90 let connecting = tokio_tungstenite::connect_async_with_config(request, Some(config), false);
91
92 let (stream, response) =
93 match tokio::time::timeout(self.handshake_timeout, connecting).await {
94 Ok(connected) => connected.map_err(failure),
95 Err(elapsed) => Err(Error::transport(elapsed)),
96 }?;
97
98 let subprotocol = response
99 .headers()
100 .get(SEC_WEBSOCKET_PROTOCOL)
101 .map(|value| String::from_utf8_lossy(value.as_bytes()).into_owned());
102
103 Ok(Box::new(WebSocketConn::new(
104 stream,
105 subprotocol,
106 self.write_timeout,
107 )))
108 }
109}
110
111fn upgrade_request(url: &str, options: DialOptions) -> Result<http::Request<()>, Error> {
114 let mut request = url.into_client_request().map_err(Error::transport)?;
115 let required = request.headers().clone();
116
117 let headers = request.headers_mut();
118 headers.extend(options.headers);
119 if !headers.contains_key(USER_AGENT) {
120 headers.insert(USER_AGENT, HeaderValue::from_static(USER_AGENT_VALUE));
121 }
122 headers.remove(SEC_WEBSOCKET_EXTENSIONS);
123 headers.remove(SEC_WEBSOCKET_PROTOCOL);
124 if !options.subprotocols.is_empty() {
125 let offered = options.subprotocols.join(", ");
126 headers.insert(
127 SEC_WEBSOCKET_PROTOCOL,
128 HeaderValue::from_str(&offered).map_err(Error::transport)?,
129 );
130 }
131 headers.extend(required);
132
133 Ok(request)
134}
135
136struct WebSocketConn<S> {
140 subprotocol: Option<String>,
141 write_timeout: Duration,
142 sink: tokio::sync::Mutex<SplitSink<WebSocketStream<S>, Frame>>,
143 stream: tokio::sync::Mutex<SplitStream<WebSocketStream<S>>>,
144 closed: watch::Sender<bool>,
145}
146
147impl<S> WebSocketConn<S>
148where
149 S: AsyncRead + AsyncWrite + Unpin + Send + 'static,
150{
151 fn new(
152 socket: WebSocketStream<S>,
153 subprotocol: Option<String>,
154 write_timeout: Duration,
155 ) -> WebSocketConn<S> {
156 let (sink, stream) = socket.split();
157 WebSocketConn {
158 subprotocol,
159 write_timeout,
160 sink: tokio::sync::Mutex::new(sink),
161 stream: tokio::sync::Mutex::new(stream),
162 closed: watch::Sender::new(false),
163 }
164 }
165
166 async fn hang_up(&self, frame: Option<CloseFrame>) {
170 if self.closed.send_replace(true) {
171 return;
172 }
173
174 let closing = async {
175 let mut sink = self.sink.lock().await;
176 let _sent = sink.send(Frame::Close(frame)).await;
177 sink.close().await
178 };
179 if let Err(error) = tokio::time::timeout(CLOSE_TIMEOUT, closing).await {
180 tracing::debug!(%error, "the close frame did not go out");
181 }
182 }
183}
184
185#[async_trait]
186impl<S> Conn for WebSocketConn<S>
187where
188 S: AsyncRead + AsyncWrite + Unpin + Send + 'static,
189{
190 fn subprotocol(&self) -> Option<&str> {
191 self.subprotocol.as_deref()
192 }
193
194 async fn read(&self) -> Result<Vec<u8>, Error> {
196 let mut closed = self.closed.subscribe();
197 let mut stream = self.stream.lock().await;
198
199 loop {
200 let next = tokio::select! {
201 biased;
202 _ = closed.wait_for(|closed| *closed) => None,
203 next = stream.next() => next,
204 };
205
206 match next {
207 Some(Ok(Frame::Text(text))) => break Ok(Vec::from(text.as_str())),
208 Some(Ok(Frame::Binary(bytes))) => break Ok(bytes.to_vec()),
209 Some(Ok(Frame::Ping(_) | Frame::Pong(_) | Frame::Frame(_))) => {}
210 Some(Ok(Frame::Close(frame))) => break Err(server_closed(frame.as_ref())),
211 Some(Err(error)) => break Err(failure(error)),
212 None => {
213 break Err(Error::transport(io::Error::new(
214 io::ErrorKind::NotConnected,
215 "the connection is closed",
216 )));
217 }
218 }
219 }
220 }
221
222 async fn write(&self, payload: &[u8]) -> Result<(), Error> {
225 let text = String::from_utf8(payload.to_vec()).map_err(Error::transport)?;
226 let sending = async { self.sink.lock().await.send(Frame::text(text)).await };
227 match tokio::time::timeout(self.write_timeout, sending).await {
228 Ok(sent) => sent.map_err(failure),
229 Err(elapsed) => Err(Error::transport(elapsed)),
230 }
231 }
232
233 async fn close(&self) {
235 self.hang_up(Some(CloseFrame {
236 code: CloseCode::Normal,
237 reason: String::new().into(),
238 }))
239 .await;
240 }
241
242 async fn close_with_status(&self, code: u16, reason: &str) {
245 self.hang_up(Some(CloseFrame {
246 code: CloseCode::from(code),
247 reason: truncate(reason, MAX_CLOSE_REASON_BYTES).into(),
248 }))
249 .await;
250 }
251}
252
253fn server_closed(frame: Option<&CloseFrame>) -> Error {
256 match frame {
257 Some(frame) => Error::Close {
258 code: u16::from(frame.code),
259 reason: frame.reason.to_string(),
260 },
261 None => Error::Close {
262 code: CLOSE_NO_STATUS,
263 reason: String::new(),
264 },
265 }
266}
267
268fn failure(error: TungsteniteError) -> Error {
272 match error {
273 TungsteniteError::Http(response) => Error::Handshake {
274 status: response.status(),
275 },
276 TungsteniteError::Capacity(CapacityError::MessageTooLong { size, max_size }) => {
277 Error::MessageTooBig {
278 size,
279 limit: max_size,
280 }
281 }
282 other => Error::transport(other),
283 }
284}
285
286fn truncate(text: &str, bytes: usize) -> &str {
288 if text.len() <= bytes {
289 text
290 } else {
291 let mut end = bytes;
292 while !text.is_char_boundary(end) {
293 end -= 1;
294 }
295 &text[..end]
296 }
297}
298
299#[cfg(test)]
303#[allow(
304 clippy::unwrap_used,
305 clippy::expect_used,
306 reason = "a test that can't have what it asked for has nothing left to assert"
307)]
308mod tests {
309 use std::time::Instant;
310
311 use tokio_tungstenite::tungstenite::protocol::Role;
312
313 use super::*;
314
315 #[tokio::test]
316 async fn a_write_the_peer_never_reads_times_out() {
317 let (ours, _theirs) = tokio::io::duplex(64);
318 let socket = WebSocketStream::from_raw_socket(ours, Role::Client, None).await;
319 let conn = WebSocketConn::new(socket, None, Duration::from_millis(100));
320 let started = Instant::now();
321
322 let outcome = conn.write("cable".repeat(1_000).as_bytes()).await;
323
324 assert!(matches!(outcome, Err(Error::Transport(_))), "{outcome:?}");
325 assert!(started.elapsed() >= Duration::from_millis(100));
326 assert!(
327 started.elapsed() < Duration::from_secs(2),
328 "the write hung past its timeout"
329 );
330 }
331
332 #[test]
333 fn a_close_reason_is_cut_to_what_the_frame_holds() {
334 assert_eq!("done here", truncate("done here", MAX_CLOSE_REASON_BYTES));
335 assert_eq!(
336 MAX_CLOSE_REASON_BYTES,
337 truncate(&"r".repeat(200), MAX_CLOSE_REASON_BYTES).len()
338 );
339 assert_eq!("ab", truncate("abçd", 3), "a cut lands on a character");
340 }
341}