use crate::call::Call;
use crate::error::{Error, Result};
use crate::extract::FromCallParts;
use crate::response::Response;
use async_trait::async_trait;
use futures_util::{SinkExt, StreamExt};
use http::header::{
CONNECTION, SEC_WEBSOCKET_ACCEPT, SEC_WEBSOCKET_KEY, SEC_WEBSOCKET_PROTOCOL,
SEC_WEBSOCKET_VERSION, UPGRADE,
};
use http::{HeaderMap, HeaderValue, StatusCode};
use hyper::upgrade::OnUpgrade;
use hyper_util::rt::TokioIo;
use std::future::Future;
use std::sync::{Arc, Mutex};
use tokio_tungstenite::tungstenite::protocol::{Role, WebSocketConfig};
use tokio_tungstenite::WebSocketStream;
#[derive(Clone)]
pub struct OnUpgradeHandle(Arc<Mutex<Option<OnUpgrade>>>);
#[derive(Debug, Clone, Copy)]
pub struct WsLimits {
pub max_frame_bytes: usize,
pub max_message_bytes: usize,
}
impl OnUpgradeHandle {
pub fn new(on_upgrade: OnUpgrade) -> Self {
Self(Arc::new(Mutex::new(Some(on_upgrade))))
}
pub(crate) fn take(&self) -> Option<OnUpgrade> {
self.0.lock().ok().and_then(|mut guard| guard.take())
}
}
impl std::fmt::Debug for OnUpgradeHandle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("OnUpgradeHandle")
}
}
pub(crate) fn is_upgrade_request(headers: &HeaderMap) -> bool {
let connection_upgrade = headers
.get(CONNECTION)
.and_then(|v| v.to_str().ok())
.map(|v| {
v.to_ascii_lowercase()
.split(',')
.any(|p| p.trim() == "upgrade")
})
.unwrap_or(false);
let upgrade_websocket = headers
.get(UPGRADE)
.and_then(|v| v.to_str().ok())
.map(|v| v.eq_ignore_ascii_case("websocket"))
.unwrap_or(false);
connection_upgrade && upgrade_websocket
}
use tokio_tungstenite::tungstenite::Message as TMessage;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Message {
Text(String),
Binary(Vec<u8>),
Ping(Vec<u8>),
Pong(Vec<u8>),
Close,
}
impl From<Message> for TMessage {
fn from(m: Message) -> Self {
match m {
Message::Text(s) => TMessage::Text(s.into()),
Message::Binary(b) => TMessage::Binary(b.into()),
Message::Ping(b) => TMessage::Ping(b.into()),
Message::Pong(b) => TMessage::Pong(b.into()),
Message::Close => TMessage::Close(None),
}
}
}
impl From<TMessage> for Message {
fn from(m: TMessage) -> Self {
match m {
TMessage::Text(s) => Message::Text(s.to_string()),
TMessage::Binary(b) => Message::Binary(b.to_vec()),
TMessage::Ping(b) => Message::Ping(b.to_vec()),
TMessage::Pong(b) => Message::Pong(b.to_vec()),
TMessage::Close(_) => Message::Close,
_ => Message::Close,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct WsIdleTimeout(pub u64);
pub struct WebSocket {
inner: WebSocketStream<TokioIo<hyper::upgrade::Upgraded>>,
activity: std::sync::Arc<WsActivity>,
}
pub(crate) struct WsActivity {
last_ms: std::sync::atomic::AtomicU64,
origin: tokio::time::Instant,
}
impl WsActivity {
pub(crate) fn new() -> Self {
Self {
last_ms: std::sync::atomic::AtomicU64::new(0),
origin: tokio::time::Instant::now(),
}
}
fn touch(&self) {
self.last_ms.store(
self.origin.elapsed().as_millis() as u64,
std::sync::atomic::Ordering::Relaxed,
);
}
pub(crate) fn idle_for(&self, idle_ms: u64) -> Option<std::time::Duration> {
let last = self.last_ms.load(std::sync::atomic::Ordering::Relaxed);
let quiet = (self.origin.elapsed().as_millis() as u64).saturating_sub(last);
match idle_ms.checked_sub(quiet) {
Some(0) | None => None,
Some(remaining) => Some(std::time::Duration::from_millis(remaining)),
}
}
}
impl WebSocket {
pub async fn recv(&mut self) -> Option<Result<Message>> {
let got = self.inner.next().await;
self.activity.touch();
match got {
Some(Ok(msg)) => Some(Ok(msg.into())),
Some(Err(e)) => Some(Err(Error::internal(format!("websocket recv: {e}")))),
None => None,
}
}
pub async fn send(&mut self, msg: Message) -> Result<()> {
let out = self
.inner
.send(msg.into())
.await
.map_err(|e| Error::internal(format!("websocket send: {e}")));
self.activity.touch();
out
}
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, bytes: impl Into<Vec<u8>>) -> Result<()> {
self.send(Message::Binary(bytes.into())).await
}
pub async fn close(&mut self) -> Result<()> {
self.inner
.close(None)
.await
.map_err(|e| Error::internal(format!("websocket close: {e}")))
}
}
pub struct WebSocketUpgrade {
on_upgrade: OnUpgrade,
accept_key: HeaderValue,
protocol: Option<HeaderValue>,
limits: WsLimits,
conn_guard: Option<crate::engine::ConnGuard>,
idle_timeout_ms: u64,
}
#[async_trait]
impl FromCallParts for WebSocketUpgrade {
async fn from_call_parts(call: &mut Call) -> Result<Self> {
let version_ok = call
.header(SEC_WEBSOCKET_VERSION.as_str())
.map(|v| v == "13")
.unwrap_or(false);
if !is_upgrade_request(call.headers()) || !version_ok {
return Err(Error::new(
StatusCode::UPGRADE_REQUIRED,
"expected a WebSocket upgrade request",
)
.with_response_header(UPGRADE, HeaderValue::from_static("websocket")));
}
let key = call
.header(SEC_WEBSOCKET_KEY.as_str())
.ok_or_else(|| Error::bad_request("missing Sec-WebSocket-Key"))?;
let accept = tokio_tungstenite::tungstenite::handshake::derive_accept_key(key.as_bytes());
let accept_key =
HeaderValue::from_str(&accept).map_err(|_| Error::internal("invalid accept key"))?;
let protocol = call
.header(SEC_WEBSOCKET_PROTOCOL.as_str())
.and_then(|p| p.split(',').next())
.and_then(|p| HeaderValue::from_str(p.trim()).ok());
let handle = call.get::<OnUpgradeHandle>().ok_or_else(|| {
Error::new(
StatusCode::UPGRADE_REQUIRED,
"WebSocket upgrade unavailable (no pending connection upgrade)",
)
})?;
let on_upgrade = handle
.take()
.ok_or_else(|| Error::internal("WebSocket upgrade already consumed"))?;
let limits = call.get::<WsLimits>().unwrap_or(WsLimits {
max_frame_bytes: 1 << 20,
max_message_bytes: 4 << 20,
});
let conn_guard = call.get::<crate::engine::ConnGuard>();
let idle_timeout_ms = call
.get::<WsIdleTimeout>()
.map(|WsIdleTimeout(ms)| ms)
.unwrap_or(300_000);
Ok(WebSocketUpgrade {
on_upgrade,
limits,
accept_key,
protocol,
conn_guard,
idle_timeout_ms,
})
}
}
impl WebSocketUpgrade {
pub fn on_upgrade<F, Fut>(self, callback: F) -> Response
where
F: FnOnce(WebSocket) -> Fut + Send + 'static,
Fut: Future<Output = ()> + Send + 'static,
{
let WebSocketUpgrade {
on_upgrade,
accept_key,
protocol,
limits,
conn_guard,
idle_timeout_ms,
} = self;
tokio::spawn(async move {
let _conn_guard = conn_guard;
if let Ok(upgraded) = on_upgrade.await {
let mut ws_config = WebSocketConfig::default();
ws_config.max_frame_size = Some(limits.max_frame_bytes);
ws_config.max_message_size = Some(limits.max_message_bytes);
let stream = WebSocketStream::from_raw_socket(
TokioIo::new(upgraded),
Role::Server,
Some(ws_config),
)
.await;
let activity = std::sync::Arc::new(WsActivity::new());
let socket = WebSocket {
inner: stream,
activity: activity.clone(),
};
if idle_timeout_ms == 0 {
callback(socket).await;
} else {
let work = callback(socket);
tokio::pin!(work);
let reaper =
tokio::time::sleep(std::time::Duration::from_millis(idle_timeout_ms));
tokio::pin!(reaper);
loop {
tokio::select! {
_ = &mut work => break,
_ = reaper.as_mut() => match activity.idle_for(idle_timeout_ms) {
None => {
tracing::debug!("closing an idle WebSocket");
break;
}
Some(remaining) => reaper
.as_mut()
.reset(tokio::time::Instant::now() + remaining),
},
}
}
}
}
});
let mut res = Response::new(StatusCode::SWITCHING_PROTOCOLS);
res.headers
.insert(UPGRADE, HeaderValue::from_static("websocket"));
res.headers
.insert(CONNECTION, HeaderValue::from_static("upgrade"));
res.headers.insert(SEC_WEBSOCKET_ACCEPT, accept_key);
if let Some(p) = protocol {
res.headers.insert(SEC_WEBSOCKET_PROTOCOL, p);
}
res
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Churust, TestClient};
use tokio_tungstenite::tungstenite::Message as TMessage;
#[tokio::test]
async fn plain_get_to_ws_route_is_426() {
let app = Churust::server()
.routing(|r| {
r.get("/ws", |ws: WebSocketUpgrade| async move {
ws.on_upgrade(|_sock| async {})
});
})
.build();
let res = TestClient::new(app).get("/ws").send().await;
assert_eq!(res.status(), http::StatusCode::UPGRADE_REQUIRED);
}
#[test]
fn message_round_trips_through_tungstenite() {
let cases = [
Message::Text("hi".into()),
Message::Binary(vec![1, 2, 3]),
Message::Ping(vec![9]),
Message::Pong(vec![8]),
Message::Close,
];
for m in cases {
let t: TMessage = m.clone().into();
let back: Message = t.into();
assert_eq!(m, back);
}
}
#[test]
fn accept_key_matches_rfc6455_example() {
let accept = tokio_tungstenite::tungstenite::handshake::derive_accept_key(
b"dGhlIHNhbXBsZSBub25jZQ==",
);
assert_eq!(accept, "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=");
}
}