use std::time::Duration;
use tokio::io::{AsyncRead, AsyncWrite};
use tokio_tungstenite::tungstenite;
use crate::protocol::validate_protocol;
use crate::transport::WsTransport;
use crate::{alpn, Config, Error, Session, Version};
#[derive(Debug, Clone, Copy)]
pub struct KeepAlive {
pub interval: Duration,
pub timeout: Duration,
}
impl KeepAlive {
pub fn new(interval: Duration, timeout: Duration) -> Self {
Self { interval, timeout }
}
}
impl Default for KeepAlive {
fn default() -> Self {
Self {
interval: Duration::from_secs(5),
timeout: Duration::from_secs(30),
}
}
}
pub struct Upgraded<T> {
ws: T,
alpn: Option<String>,
keep_alive: Option<KeepAlive>,
}
impl<T> Upgraded<T>
where
T: futures::Stream<Item = Result<tungstenite::Message, tungstenite::Error>>
+ futures::Sink<tungstenite::Message, Error = tungstenite::Error>
+ Unpin
+ Send
+ 'static,
{
pub fn new(ws: T) -> Self {
Self {
ws,
alpn: None,
keep_alive: None,
}
}
pub fn with_alpn(mut self, alpn: &str) -> Self {
self.alpn = Some(alpn.to_string());
self
}
pub fn with_keep_alive(mut self, keep_alive: KeepAlive) -> Self {
self.keep_alive = Some(keep_alive);
self
}
pub fn connect(self) -> Session {
let (version, protocol) = alpn::parse(self.alpn.as_deref());
Session::connect(self.into_transport(), Config::negotiated(version, protocol))
}
pub fn accept(self) -> Session {
let (version, protocol) = alpn::parse(self.alpn.as_deref());
Session::accept(self.into_transport(), Config::negotiated(version, protocol))
}
fn into_transport(self) -> WsTransport<T> {
let transport = WsTransport::new(self.ws);
match self.keep_alive {
Some(ka) => transport.with_keep_alive(ka),
None => transport,
}
}
}
#[derive(Default, Clone)]
pub struct Client {
protocols: Vec<(String, Vec<Version>)>,
require_protocol: bool,
config: Option<tungstenite::protocol::WebSocketConfig>,
keep_alive: Option<KeepAlive>,
#[cfg(feature = "wss")]
connector: Option<tokio_tungstenite::Connector>,
}
impl Client {
pub fn new() -> Self {
Self::default()
}
pub fn with_protocol(mut self, alpn: &str, versions: &[Version]) -> Self {
self.protocols.push((alpn.to_string(), versions.to_vec()));
self
}
pub fn with_protocols<'a>(
mut self,
entries: impl IntoIterator<Item = (&'a str, &'a [Version])>,
) -> Self {
self.protocols.extend(
entries
.into_iter()
.map(|(a, vs)| (a.to_string(), vs.to_vec())),
);
self
}
pub fn require_protocol(mut self) -> Self {
self.require_protocol = true;
self
}
pub fn with_config(mut self, config: tungstenite::protocol::WebSocketConfig) -> Self {
self.config = Some(config);
self
}
pub fn with_keep_alive(mut self, keep_alive: KeepAlive) -> Self {
self.keep_alive = Some(keep_alive);
self
}
#[cfg(feature = "wss")]
pub fn with_connector(mut self, connector: tokio_tungstenite::Connector) -> Self {
self.connector = Some(connector);
self
}
pub async fn connect(&self, url: &str) -> Result<Session, Error> {
use tungstenite::{client::IntoClientRequest, http};
for (a, _) in &self.protocols {
validate_protocol(a)?;
}
let mut request = url.into_client_request().map_err(Error::from)?;
let entries = self
.protocols
.iter()
.map(|(a, vs)| (a.as_str(), vs.as_slice()));
let protocol_value = alpn::build(entries, self.require_protocol).join(", ");
request.headers_mut().insert(
http::header::SEC_WEBSOCKET_PROTOCOL,
http::HeaderValue::from_str(&protocol_value)
.map_err(|_| Error::InvalidProtocol(protocol_value))?,
);
#[cfg(feature = "wss")]
let (ws_stream, response) = {
tokio_tungstenite::connect_async_tls_with_config(
request,
self.config,
false,
self.connector.clone(),
)
.await
.map_err(Error::from)?
};
#[cfg(not(feature = "wss"))]
let (ws_stream, response) =
tokio_tungstenite::connect_async_with_config(request, self.config, false)
.await
.map_err(Error::from)?;
let negotiated = response
.headers()
.get(http::header::SEC_WEBSOCKET_PROTOCOL)
.and_then(|h| h.to_str().ok());
let (version, protocol) = alpn::parse(negotiated);
if self.require_protocol && protocol.is_none() {
return Err(Error::InvalidProtocol(
negotiated.unwrap_or("<none>").to_string(),
));
}
let transport = match self.keep_alive {
Some(ka) => WsTransport::new(ws_stream).with_keep_alive(ka),
None => WsTransport::new(ws_stream),
};
Ok(Session::connect(
transport,
Config::negotiated(version, protocol),
))
}
}
#[derive(Default, Clone)]
pub struct Server {
protocols: Vec<(String, Vec<Version>)>,
require_protocol: bool,
keep_alive: Option<KeepAlive>,
}
impl Server {
pub fn new() -> Self {
Self::default()
}
pub fn with_protocol(mut self, alpn: &str, versions: &[Version]) -> Self {
self.protocols.push((alpn.to_string(), versions.to_vec()));
self
}
pub fn with_protocols<'a>(
mut self,
entries: impl IntoIterator<Item = (&'a str, &'a [Version])>,
) -> Self {
self.protocols.extend(
entries
.into_iter()
.map(|(a, vs)| (a.to_string(), vs.to_vec())),
);
self
}
pub fn require_protocol(mut self) -> Self {
self.require_protocol = true;
self
}
pub fn with_keep_alive(mut self, keep_alive: KeepAlive) -> Self {
self.keep_alive = Some(keep_alive);
self
}
pub async fn accept<T: AsyncRead + AsyncWrite + Unpin + Send + 'static>(
&self,
socket: T,
) -> Result<Session, Error> {
use std::sync::{Arc, Mutex};
use tungstenite::{handshake::server, http};
for (a, _) in &self.protocols {
validate_protocol(a)?;
}
let negotiated = Arc::new(Mutex::new(None::<(Version, Option<String>)>));
let negotiated_clone = negotiated.clone();
let supported = self.protocols.clone();
let require_protocol = self.require_protocol;
#[allow(clippy::result_large_err)]
let callback = move |req: &server::Request,
mut response: server::Response|
-> Result<server::Response, server::ErrorResponse> {
let header_protocols: Vec<&str> = req
.headers()
.get_all(http::header::SEC_WEBSOCKET_PROTOCOL)
.iter()
.filter_map(|v| v.to_str().ok())
.flat_map(|h| h.split(','))
.map(|p| p.trim())
.filter(|p| !p.is_empty())
.collect();
for (alpn, versions) in &supported {
for &version in alpn::expand_versions(versions) {
let wire = format!("{}{}", version.prefix(), alpn);
if header_protocols.iter().any(|p| *p == wire) {
response.headers_mut().insert(
http::header::SEC_WEBSOCKET_PROTOCOL,
http::HeaderValue::from_str(&wire).unwrap(),
);
*negotiated_clone.lock().unwrap() = Some((version, Some(alpn.clone())));
return Ok(response);
}
}
}
if !require_protocol {
for &version in alpn::BARE_ALPNS {
let bare = version.alpn();
if header_protocols.contains(&bare) {
response.headers_mut().insert(
http::header::SEC_WEBSOCKET_PROTOCOL,
http::HeaderValue::from_str(bare).unwrap(),
);
*negotiated_clone.lock().unwrap() = Some((version, None));
return Ok(response);
}
}
}
Err(http::Response::builder()
.status(http::StatusCode::BAD_REQUEST)
.body(Some("no supported protocol".to_string()))
.unwrap())
};
let ws = tokio_tungstenite::accept_hdr_async_with_config(socket, callback, None).await?;
let (version, protocol) = negotiated
.lock()
.unwrap()
.take()
.expect("negotiated must be set after successful handshake");
let transport = match self.keep_alive {
Some(ka) => WsTransport::new(ws).with_keep_alive(ka),
None => WsTransport::new(ws),
};
Ok(Session::accept(
transport,
Config::negotiated(version, protocol),
))
}
}