use std::fmt;
use std::future::Future;
use base64::Engine as _;
use bytes::Bytes;
use http::header::{
HeaderName, HeaderValue, CONNECTION, ORIGIN, SEC_WEBSOCKET_ACCEPT, SEC_WEBSOCKET_KEY,
SEC_WEBSOCKET_PROTOCOL, UPGRADE,
};
use http_body_util::Empty;
use hyper::upgrade::Upgraded;
use hyper::{Request, Response, StatusCode};
use hyper_util::rt::TokioIo;
use sha1::{Digest, Sha1};
const WS_GUID: &[u8] = b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
pub const DEFAULT_SUBPROTOCOL: &str = "h2ts";
#[derive(Debug, Clone, Default)]
pub struct AcceptOptions {
pub allow_implicit_codec: bool,
pub allowed_origins: Option<Vec<String>>,
}
pub type UpgradedIo = TokioIo<Upgraded>;
#[derive(Debug)]
pub enum WebSocketError {
NotUpgradeRequest,
UnsupportedSubprotocol,
ForbiddenOrigin,
Upgrade(hyper::Error),
}
impl WebSocketError {
pub fn rejection_response(&self) -> Response<Empty<Bytes>> {
let status = match self {
WebSocketError::NotUpgradeRequest => StatusCode::UPGRADE_REQUIRED,
WebSocketError::UnsupportedSubprotocol => StatusCode::BAD_REQUEST,
WebSocketError::ForbiddenOrigin => StatusCode::FORBIDDEN,
WebSocketError::Upgrade(_) => StatusCode::INTERNAL_SERVER_ERROR,
};
Response::builder()
.status(status)
.body(Empty::new())
.expect("static rejection response is well-formed")
}
}
impl fmt::Display for WebSocketError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
WebSocketError::NotUpgradeRequest => f.write_str("not a WebSocket upgrade request"),
WebSocketError::UnsupportedSubprotocol => {
f.write_str("client offered no supported subprotocol")
}
WebSocketError::ForbiddenOrigin => f.write_str("origin not allowed"),
WebSocketError::Upgrade(e) => write!(f, "WebSocket upgrade failed: {e}"),
}
}
}
impl std::error::Error for WebSocketError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
WebSocketError::Upgrade(e) => Some(e),
WebSocketError::NotUpgradeRequest
| WebSocketError::UnsupportedSubprotocol
| WebSocketError::ForbiddenOrigin => None,
}
}
}
fn header_lists<B>(request: &Request<B>, header: HeaderName, needle: &str) -> bool {
request
.headers()
.get(header)
.and_then(|v| v.to_str().ok())
.map(|list| {
list.split(',')
.any(|t| t.trim().eq_ignore_ascii_case(needle))
})
.unwrap_or(false)
}
pub fn is_upgrade_request<B>(request: &Request<B>) -> bool {
header_lists(request, UPGRADE, "websocket")
&& header_lists(request, CONNECTION, "upgrade")
&& request.headers().contains_key(SEC_WEBSOCKET_KEY)
}
pub fn offered_protocols<B>(request: &Request<B>) -> Vec<&str> {
request
.headers()
.get(SEC_WEBSOCKET_PROTOCOL)
.and_then(|v| v.to_str().ok())
.map(|list| {
list.split(',')
.map(str::trim)
.filter(|s| !s.is_empty())
.collect()
})
.unwrap_or_default()
}
#[derive(Debug, PartialEq, Eq)]
enum Fallback {
Accept(Option<String>),
Reject,
}
fn fallback_subprotocol(offered: &[&str], allow_implicit_codec: bool) -> Fallback {
if let Some(p) = offered
.iter()
.find(|p| p.eq_ignore_ascii_case(DEFAULT_SUBPROTOCOL))
{
return Fallback::Accept(Some(p.to_string()));
}
if allow_implicit_codec {
return Fallback::Accept(offered.first().map(|p| p.to_string()));
}
Fallback::Reject
}
fn accept_key(key: &[u8]) -> String {
let mut hasher = Sha1::new();
hasher.update(key);
hasher.update(WS_GUID);
base64::engine::general_purpose::STANDARD.encode(hasher.finalize())
}
#[allow(clippy::type_complexity)]
pub fn accept_with_options<B, F>(
request: &mut Request<B>,
select: F,
options: AcceptOptions,
) -> Result<
(
Response<Empty<Bytes>>,
impl Future<Output = Result<UpgradedIo, WebSocketError>>,
),
WebSocketError,
>
where
F: FnOnce(&[&str]) -> Option<String>,
{
if !is_upgrade_request(request) {
return Err(WebSocketError::NotUpgradeRequest);
}
if let Some(allowed) = &options.allowed_origins {
let origin = request.headers().get(ORIGIN).and_then(|v| v.to_str().ok());
let ok = origin.is_some_and(|o| allowed.iter().any(|a| a.eq_ignore_ascii_case(o)));
if !ok {
return Err(WebSocketError::ForbiddenOrigin);
}
}
let key = request
.headers()
.get(SEC_WEBSOCKET_KEY)
.ok_or(WebSocketError::NotUpgradeRequest)?;
let accept_value = accept_key(key.as_bytes());
let offered = offered_protocols(request);
let decision = match select(&offered) {
Some(proto) if offered.iter().any(|o| o.eq_ignore_ascii_case(&proto)) => {
Fallback::Accept(Some(proto))
}
_ => fallback_subprotocol(&offered, options.allow_implicit_codec),
};
drop(offered); let chosen = match decision {
Fallback::Accept(proto) => proto,
Fallback::Reject => return Err(WebSocketError::UnsupportedSubprotocol),
};
let accept_header = HeaderValue::from_str(&accept_value).expect("base64 is valid header ASCII");
let mut response = Response::builder()
.status(StatusCode::SWITCHING_PROTOCOLS)
.header(CONNECTION, HeaderValue::from_static("Upgrade"))
.header(UPGRADE, HeaderValue::from_static("websocket"))
.header(SEC_WEBSOCKET_ACCEPT, accept_header)
.body(Empty::<Bytes>::new())
.expect("static 101 response is well-formed");
if let Some(proto) = chosen {
if let Ok(value) = HeaderValue::from_str(&proto) {
response.headers_mut().insert(SEC_WEBSOCKET_PROTOCOL, value);
}
}
let on_upgrade = hyper::upgrade::on(&mut *request);
let fut = async move {
let upgraded = on_upgrade.await.map_err(WebSocketError::Upgrade)?;
Ok(TokioIo::new(upgraded))
};
Ok((response, fut))
}
#[allow(clippy::type_complexity)]
pub fn accept_with<B, F>(
request: &mut Request<B>,
select: F,
) -> Result<
(
Response<Empty<Bytes>>,
impl Future<Output = Result<UpgradedIo, WebSocketError>>,
),
WebSocketError,
>
where
F: FnOnce(&[&str]) -> Option<String>,
{
accept_with_options(request, select, AcceptOptions::default())
}
#[allow(clippy::type_complexity)]
pub fn accept<B>(
request: &mut Request<B>,
) -> Result<
(
Response<Empty<Bytes>>,
impl Future<Output = Result<UpgradedIo, WebSocketError>>,
),
WebSocketError,
> {
accept_with(request, |_offered| None)
}