use crate::error::{Error, Result};
use crate::handler::Handler;
use crate::request::Request;
use crate::response::Response;
use async_trait::async_trait;
use futures_util::{SinkExt, StreamExt};
use hyper::upgrade::{OnUpgrade, Upgraded};
use hyper_util::rt::TokioIo;
use std::future::Future;
use std::sync::Arc;
use tokio_tungstenite::tungstenite::handshake::derive_accept_key;
use tokio_tungstenite::tungstenite::protocol::{Role, WebSocketConfig};
use tokio_tungstenite::tungstenite::{self, Message as WsMessage};
use tokio_tungstenite::WebSocketStream;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Message {
Text(String),
Binary(Vec<u8>),
Ping(Vec<u8>),
Pong(Vec<u8>),
Close,
}
#[derive(Debug, Clone)]
pub struct WsOptions {
pub max_message_size: usize,
pub allowed_origins: Vec<String>,
pub ping_interval: Option<std::time::Duration>,
pub idle_timeout: Option<std::time::Duration>,
}
impl Default for WsOptions {
fn default() -> Self {
Self {
max_message_size: 1024 * 1024,
allowed_origins: Vec::new(),
ping_interval: Some(std::time::Duration::from_secs(30)),
idle_timeout: Some(std::time::Duration::from_secs(90)),
}
}
}
pub struct WebSocket {
inner: WebSocketStream<TokioIo<Upgraded>>,
ping: Option<tokio::time::Interval>,
idle_timeout: Option<std::time::Duration>,
last_seen: std::time::Instant,
}
impl WebSocket {
fn new(inner: WebSocketStream<TokioIo<Upgraded>>, options: &WsOptions) -> Self {
let ping = options.ping_interval.map(|every| {
let mut i = tokio::time::interval_at(tokio::time::Instant::now() + every, every);
i.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
i
});
Self {
inner,
ping,
idle_timeout: options.idle_timeout,
last_seen: std::time::Instant::now(),
}
}
pub async fn recv(&mut self) -> Option<Result<Message>> {
loop {
let Self {
inner,
ping,
idle_timeout,
last_seen,
} = self;
let tick = async {
match ping {
Some(interval) => {
interval.tick().await;
}
None => std::future::pending::<()>().await,
}
};
tokio::select! {
incoming = inner.next() => {
*last_seen = std::time::Instant::now();
return match incoming? {
Ok(WsMessage::Text(t)) => Some(Ok(Message::Text(t.as_str().to_owned()))),
Ok(WsMessage::Binary(b)) => Some(Ok(Message::Binary(b.to_vec()))),
Ok(WsMessage::Ping(b)) => Some(Ok(Message::Ping(b.to_vec()))),
Ok(WsMessage::Pong(b)) => Some(Ok(Message::Pong(b.to_vec()))),
Ok(WsMessage::Close(_)) => Some(Ok(Message::Close)),
Ok(WsMessage::Frame(_)) => continue,
Err(tungstenite::Error::ConnectionClosed | tungstenite::Error::AlreadyClosed) => None,
Err(e) => Some(Err(Error::bad_request(format!("WebSocket: {}", e)))),
};
}
_ = tick => {
if idle_timeout.is_some_and(|limit| last_seen.elapsed() >= limit) {
let _ = inner.close(None).await;
return None;
}
if inner.send(WsMessage::Ping(Vec::new().into())).await.is_err() {
return None;
}
}
}
}
}
pub async fn send_json<T: serde::Serialize>(&mut self, value: &T) -> Result<()> {
let text = serde_json::to_string(value)
.map_err(|e| Error::internal(format!("WebSocket JSON: {}", e)))?;
self.send_text(text).await
}
pub async fn close_with(&mut self, code: u16, reason: &str) -> Result<()> {
use tungstenite::protocol::frame::{coding::CloseCode, CloseFrame};
self.inner
.close(Some(CloseFrame {
code: CloseCode::from(code),
reason: reason.to_string().into(),
}))
.await
.map_err(|e| Error::internal(format!("WebSocket close: {}", e)))
}
pub async fn send(&mut self, message: Message) -> Result<()> {
let msg = match message {
Message::Text(t) => WsMessage::text(t),
Message::Binary(b) => WsMessage::binary(b),
Message::Ping(b) => WsMessage::Ping(b.into()),
Message::Pong(b) => WsMessage::Pong(b.into()),
Message::Close => WsMessage::Close(None),
};
self.inner
.send(msg)
.await
.map_err(|e| Error::internal(format!("WebSocket send: {}", e)))
}
pub async fn send_text(&mut self, text: impl Into<String>) -> Result<()> {
self.send(Message::Text(text.into())).await
}
pub async fn send_binary(&mut self, data: impl Into<Vec<u8>>) -> Result<()> {
self.send(Message::Binary(data.into())).await
}
pub async fn close(&mut self) -> Result<()> {
self.inner
.close(None)
.await
.map_err(|e| Error::internal(format!("WebSocket close: {}", e)))
}
}
pub(crate) struct WsRoute<F> {
handler: Arc<F>,
options: WsOptions,
}
impl<F> WsRoute<F> {
pub(crate) fn new(handler: F, options: WsOptions) -> Self {
Self {
handler: Arc::new(handler),
options,
}
}
}
fn header<'a>(req: &'a Request, name: &str) -> Option<&'a str> {
req.headers.get(name).and_then(|v| v.to_str().ok())
}
fn has_token(req: &Request, name: &str, token: &str) -> bool {
req.headers
.get_all(name)
.iter()
.filter_map(|v| v.to_str().ok())
.any(|v| v.split(',').any(|t| t.trim().eq_ignore_ascii_case(token)))
}
fn origin_allowed(req: &Request, options: &WsOptions) -> bool {
let Some(origin) = header(req, "origin") else {
return true;
};
if options.allowed_origins.iter().any(|o| o == origin) {
return true;
}
let authority = origin
.split_once("://")
.map(|(_, rest)| rest)
.unwrap_or(origin);
header(req, "host")
.map(|h| h.eq_ignore_ascii_case(authority))
.unwrap_or(false)
}
#[async_trait]
impl<F, Fut> Handler for WsRoute<F>
where
F: Fn(Request, WebSocket) -> Fut + Send + Sync + 'static,
Fut: Future<Output = ()> + Send + 'static,
{
async fn call(&self, mut req: Request) -> Result<Response> {
if !has_token(&req, "connection", "upgrade")
|| !header(&req, "upgrade").is_some_and(|v| v.eq_ignore_ascii_case("websocket"))
{
return Err(Error::bad_request("Expected a WebSocket upgrade request"));
}
if header(&req, "sec-websocket-version") != Some("13") {
return Ok(
Response::from_error(Error::custom(426, "Unsupported WebSocket version"))
.with_header("sec-websocket-version", "13"),
);
}
let Some(key) = header(&req, "sec-websocket-key").filter(|k| !k.is_empty()) else {
return Err(Error::bad_request("Missing Sec-WebSocket-Key"));
};
let accept = derive_accept_key(key.as_bytes());
if !origin_allowed(&req, &self.options) {
return Err(Error::forbidden("Origin not allowed"));
}
let Some(on_upgrade) = req.extensions.remove::<OnUpgrade>() else {
return Err(Error::internal("Connection cannot be upgraded"));
};
let handler = self.handler.clone();
let options = self.options.clone();
let config = WebSocketConfig::default()
.max_message_size(Some(self.options.max_message_size))
.max_frame_size(Some(self.options.max_message_size));
tokio::spawn(async move {
let Ok(upgraded) = on_upgrade.await else {
return;
};
let inner = WebSocketStream::from_raw_socket(
TokioIo::new(upgraded),
Role::Server,
Some(config),
)
.await;
handler(req, WebSocket::new(inner, &options)).await;
});
let mut res = Response::new(http::StatusCode::SWITCHING_PROTOCOLS)
.with_header("upgrade", "websocket")
.with_header("connection", "Upgrade")
.with_header("sec-websocket-accept", accept);
res.body = bytes::Bytes::new();
Ok(res)
}
}