use std::io;
use std::time::Duration;
use async_trait::async_trait;
use futures_util::stream::{SplitSink, SplitStream};
use futures_util::{SinkExt, StreamExt};
use http::HeaderValue;
use http::header::{SEC_WEBSOCKET_EXTENSIONS, SEC_WEBSOCKET_PROTOCOL, USER_AGENT};
use tokio::io::{AsyncRead, AsyncWrite};
use tokio::sync::watch;
use tokio_tungstenite::WebSocketStream;
use tokio_tungstenite::tungstenite::error::{CapacityError, Error as TungsteniteError};
use tokio_tungstenite::tungstenite::protocol::WebSocketConfig;
use tokio_tungstenite::tungstenite::protocol::frame::CloseFrame;
use tokio_tungstenite::tungstenite::protocol::frame::coding::CloseCode;
use tokio_tungstenite::tungstenite::{Message as Frame, client::IntoClientRequest};
use crate::error::Error;
use crate::transport::{Conn, DialOptions, Transport};
const USER_AGENT_VALUE: &str = concat!("actioncable/", env!("CARGO_PKG_VERSION"));
const CLOSE_TIMEOUT: Duration = Duration::from_secs(1);
const CLOSE_NO_STATUS: u16 = 1005;
const MAX_CLOSE_REASON_BYTES: usize = 123;
#[derive(Debug, Clone)]
pub struct WebSocketTransport {
handshake_timeout: Duration,
write_timeout: Duration,
max_message_size: usize,
}
impl WebSocketTransport {
pub fn new() -> WebSocketTransport {
WebSocketTransport {
handshake_timeout: Duration::from_secs(10),
write_timeout: Duration::from_secs(10),
max_message_size: 8 << 20,
}
}
pub fn handshake_timeout(mut self, timeout: Duration) -> WebSocketTransport {
self.handshake_timeout = timeout;
self
}
pub fn write_timeout(mut self, timeout: Duration) -> WebSocketTransport {
self.write_timeout = timeout;
self
}
pub fn max_message_size(mut self, bytes: usize) -> WebSocketTransport {
self.max_message_size = bytes;
self
}
}
impl Default for WebSocketTransport {
fn default() -> WebSocketTransport {
WebSocketTransport::new()
}
}
#[async_trait]
impl Transport for WebSocketTransport {
async fn dial(&self, url: &str, options: DialOptions) -> Result<Box<dyn Conn>, Error> {
let request = upgrade_request(url, options)?;
let config = WebSocketConfig::default()
.max_message_size(Some(self.max_message_size))
.max_frame_size(Some(self.max_message_size));
let connecting = tokio_tungstenite::connect_async_with_config(request, Some(config), false);
let (stream, response) =
match tokio::time::timeout(self.handshake_timeout, connecting).await {
Ok(connected) => connected.map_err(failure),
Err(elapsed) => Err(Error::transport(elapsed)),
}?;
let subprotocol = response
.headers()
.get(SEC_WEBSOCKET_PROTOCOL)
.map(|value| String::from_utf8_lossy(value.as_bytes()).into_owned());
Ok(Box::new(WebSocketConn::new(
stream,
subprotocol,
self.write_timeout,
)))
}
}
fn upgrade_request(url: &str, options: DialOptions) -> Result<http::Request<()>, Error> {
let mut request = url.into_client_request().map_err(Error::transport)?;
let required = request.headers().clone();
let headers = request.headers_mut();
headers.extend(options.headers);
if !headers.contains_key(USER_AGENT) {
headers.insert(USER_AGENT, HeaderValue::from_static(USER_AGENT_VALUE));
}
headers.remove(SEC_WEBSOCKET_EXTENSIONS);
headers.remove(SEC_WEBSOCKET_PROTOCOL);
if !options.subprotocols.is_empty() {
let offered = options.subprotocols.join(", ");
headers.insert(
SEC_WEBSOCKET_PROTOCOL,
HeaderValue::from_str(&offered).map_err(Error::transport)?,
);
}
headers.extend(required);
Ok(request)
}
struct WebSocketConn<S> {
subprotocol: Option<String>,
write_timeout: Duration,
sink: tokio::sync::Mutex<SplitSink<WebSocketStream<S>, Frame>>,
stream: tokio::sync::Mutex<SplitStream<WebSocketStream<S>>>,
closed: watch::Sender<bool>,
}
impl<S> WebSocketConn<S>
where
S: AsyncRead + AsyncWrite + Unpin + Send + 'static,
{
fn new(
socket: WebSocketStream<S>,
subprotocol: Option<String>,
write_timeout: Duration,
) -> WebSocketConn<S> {
let (sink, stream) = socket.split();
WebSocketConn {
subprotocol,
write_timeout,
sink: tokio::sync::Mutex::new(sink),
stream: tokio::sync::Mutex::new(stream),
closed: watch::Sender::new(false),
}
}
async fn hang_up(&self, frame: Option<CloseFrame>) {
if self.closed.send_replace(true) {
return;
}
let closing = async {
let mut sink = self.sink.lock().await;
let _sent = sink.send(Frame::Close(frame)).await;
sink.close().await
};
if let Err(error) = tokio::time::timeout(CLOSE_TIMEOUT, closing).await {
tracing::debug!(%error, "the close frame did not go out");
}
}
}
#[async_trait]
impl<S> Conn for WebSocketConn<S>
where
S: AsyncRead + AsyncWrite + Unpin + Send + 'static,
{
fn subprotocol(&self) -> Option<&str> {
self.subprotocol.as_deref()
}
async fn read(&self) -> Result<Vec<u8>, Error> {
let mut closed = self.closed.subscribe();
let mut stream = self.stream.lock().await;
loop {
let next = tokio::select! {
biased;
_ = closed.wait_for(|closed| *closed) => None,
next = stream.next() => next,
};
match next {
Some(Ok(Frame::Text(text))) => break Ok(Vec::from(text.as_str())),
Some(Ok(Frame::Binary(bytes))) => break Ok(bytes.to_vec()),
Some(Ok(Frame::Ping(_) | Frame::Pong(_) | Frame::Frame(_))) => {}
Some(Ok(Frame::Close(frame))) => break Err(server_closed(frame.as_ref())),
Some(Err(error)) => break Err(failure(error)),
None => {
break Err(Error::transport(io::Error::new(
io::ErrorKind::NotConnected,
"the connection is closed",
)));
}
}
}
}
async fn write(&self, payload: &[u8]) -> Result<(), Error> {
let text = String::from_utf8(payload.to_vec()).map_err(Error::transport)?;
let sending = async { self.sink.lock().await.send(Frame::text(text)).await };
match tokio::time::timeout(self.write_timeout, sending).await {
Ok(sent) => sent.map_err(failure),
Err(elapsed) => Err(Error::transport(elapsed)),
}
}
async fn close(&self) {
self.hang_up(Some(CloseFrame {
code: CloseCode::Normal,
reason: String::new().into(),
}))
.await;
}
async fn close_with_status(&self, code: u16, reason: &str) {
self.hang_up(Some(CloseFrame {
code: CloseCode::from(code),
reason: truncate(reason, MAX_CLOSE_REASON_BYTES).into(),
}))
.await;
}
}
fn server_closed(frame: Option<&CloseFrame>) -> Error {
match frame {
Some(frame) => Error::Close {
code: u16::from(frame.code),
reason: frame.reason.to_string(),
},
None => Error::Close {
code: CLOSE_NO_STATUS,
reason: String::new(),
},
}
}
fn failure(error: TungsteniteError) -> Error {
match error {
TungsteniteError::Http(response) => Error::Handshake {
status: response.status(),
},
TungsteniteError::Capacity(CapacityError::MessageTooLong { size, max_size }) => {
Error::MessageTooBig {
size,
limit: max_size,
}
}
other => Error::transport(other),
}
}
fn truncate(text: &str, bytes: usize) -> &str {
if text.len() <= bytes {
text
} else {
let mut end = bytes;
while !text.is_char_boundary(end) {
end -= 1;
}
&text[..end]
}
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
reason = "a test that can't have what it asked for has nothing left to assert"
)]
mod tests {
use std::time::Instant;
use tokio_tungstenite::tungstenite::protocol::Role;
use super::*;
#[tokio::test]
async fn a_write_the_peer_never_reads_times_out() {
let (ours, _theirs) = tokio::io::duplex(64);
let socket = WebSocketStream::from_raw_socket(ours, Role::Client, None).await;
let conn = WebSocketConn::new(socket, None, Duration::from_millis(100));
let started = Instant::now();
let outcome = conn.write("cable".repeat(1_000).as_bytes()).await;
assert!(matches!(outcome, Err(Error::Transport(_))), "{outcome:?}");
assert!(started.elapsed() >= Duration::from_millis(100));
assert!(
started.elapsed() < Duration::from_secs(2),
"the write hung past its timeout"
);
}
#[test]
fn a_close_reason_is_cut_to_what_the_frame_holds() {
assert_eq!("done here", truncate("done here", MAX_CLOSE_REASON_BYTES));
assert_eq!(
MAX_CLOSE_REASON_BYTES,
truncate(&"r".repeat(200), MAX_CLOSE_REASON_BYTES).len()
);
assert_eq!("ab", truncate("abçd", 3), "a cut lands on a character");
}
}