use super::{Request, Response};
use futures_util::{
stream::{SplitSink, SplitStream},
SinkExt, StreamExt,
};
use hyper::header;
use hyper_util::rt::TokioIo;
use std::{future::Future, io};
use tokio_tungstenite::{tungstenite, WebSocketStream};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct WebSocketLimits {
pub max_message_bytes: usize,
pub max_frame_bytes: usize,
}
impl Default for WebSocketLimits {
fn default() -> Self {
Self {
max_message_bytes: 64 * 1024,
max_frame_bytes: 64 * 1024,
}
}
}
impl WebSocketLimits {
fn config(self) -> io::Result<tungstenite::protocol::WebSocketConfig> {
if !(1..=64 * 1024 * 1024).contains(&self.max_message_bytes)
|| !(1..=self.max_message_bytes).contains(&self.max_frame_bytes)
{
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"invalid WebSocket message/frame limits",
));
}
Ok(tungstenite::protocol::WebSocketConfig::default()
.max_message_size(Some(self.max_message_bytes))
.max_frame_size(Some(self.max_frame_bytes)))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Message {
Text(String),
Binary(Vec<u8>),
Ping(Vec<u8>),
Pong(Vec<u8>),
Close,
}
fn from_transport(message: tungstenite::Message) -> Message {
match message {
tungstenite::Message::Text(text) => Message::Text(text.to_string()),
tungstenite::Message::Binary(data) => Message::Binary(data.to_vec()),
tungstenite::Message::Ping(data) => Message::Ping(data.to_vec()),
tungstenite::Message::Pong(data) => Message::Pong(data.to_vec()),
tungstenite::Message::Close(_) => Message::Close,
tungstenite::Message::Frame(_) => Message::Close,
}
}
fn into_transport(message: Message) -> tungstenite::Message {
match message {
Message::Text(text) => tungstenite::Message::Text(text.into()),
Message::Binary(data) => tungstenite::Message::Binary(data.into()),
Message::Ping(data) => tungstenite::Message::Ping(data.into()),
Message::Pong(data) => tungstenite::Message::Pong(data.into()),
Message::Close => tungstenite::Message::Close(None),
}
}
pub struct WebSocket {
inner: WebSocketStream<TokioIo<hyper::upgrade::Upgraded>>,
limits: WebSocketLimits,
}
pub struct WebSocketSender {
inner: SplitSink<WebSocketStream<TokioIo<hyper::upgrade::Upgraded>>, tungstenite::Message>,
limits: WebSocketLimits,
}
pub struct WebSocketReceiver {
inner: SplitStream<WebSocketStream<TokioIo<hyper::upgrade::Upgraded>>>,
}
impl WebSocket {
pub async fn receive(&mut self) -> io::Result<Option<Message>> {
match self.inner.next().await {
Some(Ok(message)) => Ok(Some(from_transport(message))),
Some(Err(error)) => Err(io::Error::other(error)),
None => Ok(None),
}
}
pub async fn send(&mut self, message: Message) -> io::Result<()> {
validate_outgoing(self.limits, &message)?;
self.inner
.send(into_transport(message))
.await
.map_err(io::Error::other)
}
pub fn split(self) -> (WebSocketSender, WebSocketReceiver) {
let (inner, receiver) = self.inner.split();
(
WebSocketSender {
inner,
limits: self.limits,
},
WebSocketReceiver { inner: receiver },
)
}
}
impl WebSocketSender {
pub async fn send(&mut self, message: Message) -> io::Result<()> {
validate_outgoing(self.limits, &message)?;
self.inner
.send(into_transport(message))
.await
.map_err(io::Error::other)
}
}
impl WebSocketReceiver {
pub async fn receive(&mut self) -> io::Result<Option<Message>> {
match self.inner.next().await {
Some(Ok(message)) => Ok(Some(from_transport(message))),
Some(Err(error)) => Err(io::Error::other(error)),
None => Ok(None),
}
}
}
fn validate_outgoing(limits: WebSocketLimits, message: &Message) -> io::Result<()> {
let bytes = match message {
Message::Text(text) => text.len(),
Message::Binary(data) | Message::Ping(data) | Message::Pong(data) => data.len(),
Message::Close => 0,
};
if bytes > limits.max_message_bytes
|| bytes > limits.max_frame_bytes
|| (matches!(message, Message::Ping(_) | Message::Pong(_)) && bytes > 125)
{
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"WebSocket outgoing message exceeds configured limits",
));
}
Ok(())
}
pub struct Upgrade {
upgrade: hyper::upgrade::OnUpgrade,
accept: String,
}
impl Upgrade {
pub(super) fn from_request(request: Request) -> io::Result<Self> {
if !request.http_1_1
|| request.method != "GET"
|| !has_token(&request.headers, header::CONNECTION, "upgrade")
|| !has_token(&request.headers, header::UPGRADE, "websocket")
|| singleton(&request.headers, "sec-websocket-version") != Some(b"13".as_slice())
{
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"request is not a WebSocket upgrade",
));
}
let key = singleton(&request.headers, "sec-websocket-key")
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "missing WebSocket key"))?;
if !valid_key(key) {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"invalid WebSocket key",
));
}
let upgrade = request
.upgrade
.ok_or_else(|| io::Error::other("WebSocket upgrade unavailable"))?;
Ok(Self {
upgrade,
accept: tungstenite::handshake::derive_accept_key(key),
})
}
pub fn on_upgrade<F, Fut>(self, limits: WebSocketLimits, callback: F) -> io::Result<Response>
where
F: FnOnce(WebSocket) -> Fut + Send + 'static,
Fut: Future<Output = ()> + Send + 'static,
{
let config = limits.config()?;
let accept = self.accept;
let task = Box::pin(async move {
if let Ok(upgraded) = self.upgrade.await {
let socket = WebSocket {
inner: WebSocketStream::from_raw_socket(
TokioIo::new(upgraded),
tungstenite::protocol::Role::Server,
Some(config),
)
.await,
limits,
};
callback(socket).await;
}
});
Ok(Response::websocket_upgrade(&accept, task))
}
}
fn has_token(headers: &hyper::HeaderMap, name: hyper::header::HeaderName, token: &str) -> bool {
headers.get_all(name).iter().any(|value| {
std::str::from_utf8(value.as_bytes()).is_ok_and(|value| {
value
.split(',')
.any(|candidate| candidate.trim().eq_ignore_ascii_case(token))
})
})
}
fn singleton<'a>(headers: &'a hyper::HeaderMap, name: &str) -> Option<&'a [u8]> {
let values = headers.get_all(name);
let mut values = values.iter();
let value = values.next()?;
if values.next().is_some() {
None
} else {
Some(value.as_bytes())
}
}
fn valid_key(key: &[u8]) -> bool {
key.len() == 24
&& key.ends_with(b"==")
&& key[..22]
.iter()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'+' | b'/'))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn limits_are_bounded_and_frame_cannot_exceed_message() {
assert!(WebSocketLimits::default().config().is_ok());
assert!(WebSocketLimits {
max_message_bytes: 0,
max_frame_bytes: 1,
}
.config()
.is_err());
assert!(WebSocketLimits {
max_message_bytes: 10,
max_frame_bytes: 11,
}
.config()
.is_err());
}
#[test]
fn upgrade_rejects_non_websocket_before_claiming_transport() {
let request = Request {
method: "GET".into(),
http_1_1: true,
target: "/terminal/ws".into(),
uri: "/terminal/ws".parse().unwrap(),
headers: hyper::HeaderMap::new(),
body: Vec::new(),
upgrade: None,
};
assert!(Upgrade::from_request(request).is_err());
}
#[test]
fn websocket_key_must_be_one_canonical_16_byte_nonce() {
assert!(valid_key(b"dGhlIHNhbXBsZSBub25jZQ=="));
assert!(!valid_key(b""));
assert!(!valid_key(b"dGhlIHNhbXBsZSBub25jZQ="));
assert!(!valid_key(b"!!!!!!!!!!!!!!!!!!!!!!=="));
}
#[test]
fn facade_messages_do_not_expose_transport_values() {
let message = Message::Binary(vec![1, 2, 3]);
assert_eq!(from_transport(into_transport(message.clone())), message);
}
#[tokio::test]
async fn server_emits_a_real_switching_protocols_response() {
use tokio::io::{AsyncReadExt, AsyncWriteExt};
let server = super::super::Server::bind(
"127.0.0.1:0".parse().unwrap(),
super::super::Limits::default(),
)
.await
.unwrap();
let address = server.local_addr().unwrap();
let serving = tokio::spawn(server.serve(|request| async move {
match request.into_websocket() {
Ok(upgrade) => upgrade
.on_upgrade(WebSocketLimits::default(), |_socket| async {})
.unwrap(),
Err(_) => Response::new(400, Vec::new()).unwrap(),
}
}));
let mut client = tokio::net::TcpStream::connect(address).await.unwrap();
client
.write_all(
b"GET /ws HTTP/1.1\r\nHost: localhost\r\nConnection: Upgrade\r\nUpgrade: websocket\r\nSec-WebSocket-Version: 13\r\nSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n\r\n",
)
.await
.unwrap();
let mut response = [0; 1024];
let count = tokio::time::timeout(
std::time::Duration::from_secs(1),
client.read(&mut response),
)
.await
.unwrap()
.unwrap();
let response = std::str::from_utf8(&response[..count]).unwrap();
assert!(response.starts_with("HTTP/1.1 101 Switching Protocols\r\n"));
assert!(response.contains("connection: Upgrade\r\n"));
assert!(response.contains("upgrade: websocket\r\n"));
assert!(response.contains("sec-websocket-accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n"));
serving.abort();
}
}