use futures::{SinkExt, StreamExt};
use tokio_tungstenite::tungstenite::client::IntoClientRequest;
use tokio_tungstenite::tungstenite::Message;
use tokio_tungstenite::WebSocketStream;
use super::WsError;
pub struct WsSender {
inner: Box<
dyn futures::Sink<Message, Error = tokio_tungstenite::tungstenite::Error> + Unpin + Send,
>,
}
impl std::fmt::Debug for WsSender {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("WsSender").finish_non_exhaustive()
}
}
pub struct WsReceiver {
inner: Box<
dyn futures::Stream<Item = Result<Message, tokio_tungstenite::tungstenite::Error>>
+ Unpin
+ Send,
>,
}
impl std::fmt::Debug for WsReceiver {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("WsReceiver").finish_non_exhaustive()
}
}
impl WsSender {
pub async fn send(&mut self, text: String) -> Result<(), WsError> {
self.inner
.send(Message::Text(text.into()))
.await
.map_err(WsError::from_tungstenite)
}
pub async fn close(&mut self) -> Result<(), WsError> {
self.inner
.send(Message::Close(None))
.await
.map_err(WsError::from_tungstenite)
}
}
impl WsReceiver {
pub async fn recv(&mut self) -> Option<Result<String, WsError>> {
loop {
match self.inner.next().await {
Some(Ok(Message::Text(text))) => return Some(Ok(text.to_string())),
Some(Ok(Message::Close(_))) | None => return None,
Some(Ok(_)) => continue, Some(Err(e)) => return Some(Err(WsError::from_tungstenite(e))),
}
}
}
}
pub fn from_websocket_stream<S>(ws_stream: WebSocketStream<S>) -> (WsSender, WsReceiver)
where
WebSocketStream<S>: futures::Sink<Message, Error = tokio_tungstenite::tungstenite::Error>
+ futures::Stream<Item = Result<Message, tokio_tungstenite::tungstenite::Error>>
+ Unpin
+ Send
+ 'static,
{
let (sink, stream) = ws_stream.split();
(
WsSender {
inner: Box::new(sink),
},
WsReceiver {
inner: Box::new(stream),
},
)
}
pub async fn connect(
url: &str,
headers: &[(&str, &str)],
) -> Result<(WsSender, WsReceiver), WsError> {
let mut request = url
.into_client_request()
.map_err(|e| WsError::Terminal(e.to_string()))?;
for &(name, value) in headers {
let header_name = name
.parse::<tokio_tungstenite::tungstenite::http::header::HeaderName>()
.map_err(|error| {
WsError::Terminal(format!("invalid WebSocket header name `{name}`: {error}"))
})?;
let header_value = value
.parse::<tokio_tungstenite::tungstenite::http::header::HeaderValue>()
.map_err(|error| {
WsError::Terminal(format!(
"invalid value for WebSocket header `{name}`: {error}"
))
})?;
request.headers_mut().insert(header_name, header_value);
}
let (ws_stream, _) = tokio_tungstenite::connect_async(request)
.await
.map_err(WsError::from_tungstenite)?;
Ok(from_websocket_stream(ws_stream))
}
#[cfg(feature = "tor")]
pub(crate) async fn connect_tor(
tor_client: arti_client::TorClient<tor_rtcompat::PreferredRuntime>,
url: &str,
headers: &[(&str, &str)],
) -> Result<(WsSender, WsReceiver), WsError> {
let parsed_url =
url::Url::parse(url).map_err(|e| WsError::Terminal(format!("Invalid URL: {e}")))?;
let host = parsed_url
.host_str()
.ok_or_else(|| WsError::Terminal("WebSocket URL must include a host".to_string()))?;
let port = parsed_url
.port_or_known_default()
.ok_or_else(|| WsError::Terminal("WebSocket URL must include a port".to_string()))?;
let mut request = url
.into_client_request()
.map_err(|e| WsError::Terminal(e.to_string()))?;
for &(name, value) in headers {
let header_name = name
.parse::<tokio_tungstenite::tungstenite::http::header::HeaderName>()
.map_err(|error| {
WsError::Terminal(format!("invalid WebSocket header name `{name}`: {error}"))
})?;
let header_value = value
.parse::<tokio_tungstenite::tungstenite::http::header::HeaderValue>()
.map_err(|error| {
WsError::Terminal(format!(
"invalid value for WebSocket header `{name}`: {error}"
))
})?;
request.headers_mut().insert(header_name, header_value);
}
let stream = tor_client
.connect((host, port))
.await
.map_err(|e| WsError::Transient(e.to_string()))?;
let (ws_stream, _) =
tokio_tungstenite::client_async_tls_with_config(request, stream, None, None)
.await
.map_err(WsError::from_tungstenite)?;
Ok(from_websocket_stream(ws_stream))
}