use crate::error::{Error, Result, StreamError};
use tokio::net::TcpStream;
#[cfg(any(feature = "rustls", feature = "native-tls"))]
use tokio_tungstenite::{Connector, connect_async_tls_with_config};
use tokio_tungstenite::{MaybeTlsStream, WebSocketStream};
use tracing::{debug, info};
use url::Url;
pub type WsStream = WebSocketStream<MaybeTlsStream<TcpStream>>;
#[derive(Clone, Debug)]
pub struct WebSocketTransport {
url: Url,
}
impl WebSocketTransport {
pub fn new(url: impl Into<String>) -> Result<Self> {
let url = Url::parse(&url.into())
.map_err(|e| Error::Config(format!("Invalid WebSocket URL: {}", e)))?;
if !url.scheme().starts_with("ws") {
return Err(Error::Config(
"WebSocket URL must use ws:// or wss:// scheme".to_string(),
));
}
Ok(Self { url })
}
pub async fn connect(&self) -> Result<WsStream> {
debug!("Connecting to WebSocket at {}", self.url);
let (ws_stream, response) = handshake(&self.url)
.await?
.map_err(|e| Error::Stream(StreamError::ConnectionFailed(e.to_string())))?;
info!("WebSocket connected with status: {}", response.status());
Ok(ws_stream)
}
}
type Handshake = std::result::Result<
(
WsStream,
tokio_tungstenite::tungstenite::handshake::client::Response,
),
tokio_tungstenite::tungstenite::Error,
>;
#[cfg(any(feature = "rustls", feature = "native-tls"))]
async fn handshake(url: &Url) -> Result<Handshake> {
let connector = if url.scheme() == "wss" {
tls_connector()?
} else {
Some(Connector::Plain)
};
Ok(connect_async_tls_with_config(url.as_str(), Some(limits()), true, connector).await)
}
#[cfg(not(any(feature = "rustls", feature = "native-tls")))]
async fn handshake(url: &Url) -> Result<Handshake> {
if url.scheme() == "wss" {
return Err(Error::Config(
"wss:// needs the `rustls` or `native-tls` feature of rustyschwab".into(),
));
}
Ok(tokio_tungstenite::connect_async_with_config(url.as_str(), Some(limits()), true).await)
}
const MAX_MESSAGE_BYTES: usize = 16 * 1024 * 1024;
fn limits() -> tokio_tungstenite::tungstenite::protocol::WebSocketConfig {
tokio_tungstenite::tungstenite::protocol::WebSocketConfig::default()
.max_message_size(Some(MAX_MESSAGE_BYTES))
.max_frame_size(Some(MAX_MESSAGE_BYTES))
}
#[cfg(all(feature = "rustls", not(feature = "native-tls")))]
fn tls_connector() -> Result<Option<Connector>> {
use rustls_platform_verifier::BuilderVerifierExt;
let provider = std::sync::Arc::new(rustls::crypto::ring::default_provider());
let config = rustls::ClientConfig::builder_with_provider(provider)
.with_safe_default_protocol_versions()
.and_then(|builder| builder.with_platform_verifier())
.map_err(|e| Error::Config(format!("WebSocket TLS configuration: {e}")))?
.with_no_client_auth();
Ok(Some(Connector::Rustls(std::sync::Arc::new(config))))
}
#[cfg(feature = "native-tls")]
fn tls_connector() -> Result<Option<Connector>> {
Ok(None)
}