use std::sync::Arc;
use hyper::header::{self, HeaderMap, HeaderValue};
use hyper::{Request, Response, StatusCode};
use hyper_util::rt::TokioIo;
use tokio_tungstenite::WebSocketStream;
use tokio_tungstenite::tungstenite::handshake::derive_accept_key;
use tokio_tungstenite::tungstenite::protocol::{Role, WebSocketConfig};
use super::Hub;
use crate::http::{Body, status};
const MAX_MESSAGE: usize = 16 * 1024;
const MAX_SID: usize = 32;
pub fn accept(
mut request: Request<hyper::body::Incoming>,
hub: Arc<Hub>,
device: String,
) -> Response<Body> {
let headers = request.headers();
let upgrade = headers
.get(header::UPGRADE)
.and_then(|v| v.to_str().ok())
.is_some_and(|v| v.eq_ignore_ascii_case("websocket"));
let key = headers.get(header::SEC_WEBSOCKET_KEY).cloned();
let sid = request.uri().query().and_then(sid);
let (true, Some(key), Some(sid), true) = (upgrade, key, sid, same_origin(headers)) else {
return status(StatusCode::BAD_REQUEST);
};
let accept = derive_accept_key(key.as_bytes());
let upgraded = hyper::upgrade::on(&mut request);
tokio::spawn(async move {
let Ok(upgraded) = upgraded.await else {
return;
};
let config = WebSocketConfig::default()
.max_message_size(Some(MAX_MESSAGE))
.max_frame_size(Some(MAX_MESSAGE));
let socket =
WebSocketStream::from_raw_socket(TokioIo::new(upgraded), Role::Server, Some(config))
.await;
hub.serve(sid, device, socket).await;
});
let mut response = status(StatusCode::SWITCHING_PROTOCOLS);
let headers = response.headers_mut();
headers.insert(header::UPGRADE, HeaderValue::from_static("websocket"));
headers.insert(header::CONNECTION, HeaderValue::from_static("Upgrade"));
if let Ok(value) = HeaderValue::from_str(&accept) {
headers.insert(header::SEC_WEBSOCKET_ACCEPT, value);
}
response
}
fn sid(query: &str) -> Option<String> {
query
.split('&')
.find_map(|pair| pair.strip_prefix("sid="))
.filter(|sid| valid_sid(sid))
.map(str::to_string)
}
pub fn absolute(path: &str) -> String {
let path = path.trim();
if path.starts_with('/') {
path.to_string()
} else {
format!("/{path}")
}
}
pub fn valid_sid(sid: &str) -> bool {
!sid.is_empty() && sid.len() <= MAX_SID && sid.chars().all(|c| c.is_ascii_alphanumeric())
}
fn same_origin(headers: &HeaderMap) -> bool {
let value = |name| {
headers
.get(name)
.and_then(|v: &HeaderValue| v.to_str().ok())
};
match (value(header::ORIGIN), value(header::HOST)) {
(Some(origin), Some(host)) => origin
.split_once("://")
.is_some_and(|(_, rest)| rest == host),
(None, _) => true,
_ => false,
}
}