use crate::base64;
use rustlavel_http::{IntoResponse, Method, Request, Response, Status};
use sha1::{Digest, Sha1};
pub const ACCEPT_GUID: &str = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
pub const VERSION: &str = "13";
pub fn accept_key(key: &str) -> String {
let mut hasher = Sha1::new();
hasher.update(key.as_bytes());
hasher.update(ACCEPT_GUID.as_bytes());
base64::encode(&hasher.finalize())
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Handshake {
pub key: String,
pub accept: String,
pub protocol: Option<String>,
pub offered: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum HandshakeError {
NotAGet(Method),
MissingUpgrade,
NotWebSocket(String),
MissingConnection,
ConnectionNotUpgrade(String),
MissingVersion,
UnsupportedVersion(String),
MissingKey,
MalformedKey(String),
}
impl HandshakeError {
pub fn message(&self) -> String {
match self {
HandshakeError::NotAGet(method) => {
format!("a WebSocket handshake must be a GET, not a {method}")
}
HandshakeError::MissingUpgrade => {
"missing the `Upgrade: websocket` header".to_string()
}
HandshakeError::NotWebSocket(value) => {
format!("`Upgrade` was `{value}`, expected `websocket`")
}
HandshakeError::MissingConnection => {
"missing the `Connection: Upgrade` header".to_string()
}
HandshakeError::ConnectionNotUpgrade(value) => {
format!("`Connection` was `{value}` and does not list `Upgrade`")
}
HandshakeError::MissingVersion => {
format!("missing the `Sec-WebSocket-Version: {VERSION}` header")
}
HandshakeError::UnsupportedVersion(value) => format!(
"`Sec-WebSocket-Version` was `{value}`; this server speaks version {VERSION}"
),
HandshakeError::MissingKey => "missing the `Sec-WebSocket-Key` header".to_string(),
HandshakeError::MalformedKey(value) => format!(
"`Sec-WebSocket-Key` was `{value}`; it must be 16 random bytes, base64-encoded"
),
}
}
}
impl std::fmt::Display for HandshakeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.message())
}
}
impl std::error::Error for HandshakeError {}
impl IntoResponse for HandshakeError {
fn into_response(self) -> Response {
Response::new(Status::BAD_REQUEST)
.with_header("sec-websocket-version", VERSION)
.with_text(format!("WebSocket handshake failed: {}", self.message()))
}
}
pub fn negotiate(request: &Request, supported: &[String]) -> Result<Handshake, HandshakeError> {
if request.method() != Method::Get {
return Err(HandshakeError::NotAGet(request.method()));
}
match request.header("upgrade") {
None => return Err(HandshakeError::MissingUpgrade),
Some(value) if !value.eq_ignore_ascii_case("websocket") => {
return Err(HandshakeError::NotWebSocket(value.to_string()));
}
Some(_) => {}
}
match request.header("connection") {
None => return Err(HandshakeError::MissingConnection),
Some(value) if !lists_token(value, "upgrade") => {
return Err(HandshakeError::ConnectionNotUpgrade(value.to_string()));
}
Some(_) => {}
}
match request.header("sec-websocket-version") {
None => return Err(HandshakeError::MissingVersion),
Some(value) if value.trim() != VERSION => {
return Err(HandshakeError::UnsupportedVersion(value.to_string()));
}
Some(_) => {}
}
let key = request.header("sec-websocket-key").ok_or(HandshakeError::MissingKey)?.trim();
match base64::decode(key) {
Some(bytes) if bytes.len() == 16 => {}
_ => return Err(HandshakeError::MalformedKey(key.to_string())),
}
let offered = offered_protocols(request);
let protocol = supported.iter().find(|wanted| offered.iter().any(|o| o == *wanted)).cloned();
Ok(Handshake { key: key.to_string(), accept: accept_key(key), protocol, offered })
}
pub fn response(handshake: &Handshake) -> Response {
let mut response = Response::new(Status(101))
.with_header("upgrade", "websocket")
.with_header("connection", "Upgrade")
.with_header("sec-websocket-accept", handshake.accept.clone());
if let Some(protocol) = &handshake.protocol {
response = response.with_header("sec-websocket-protocol", protocol.clone());
}
response
}
fn offered_protocols(request: &Request) -> Vec<String> {
request
.headers()
.get_all("sec-websocket-protocol")
.iter()
.flat_map(|value| value.split(','))
.map(|token| token.trim().to_string())
.filter(|token| !token.is_empty())
.collect()
}
fn lists_token(value: &str, token: &str) -> bool {
value.split(',').any(|part| part.trim().eq_ignore_ascii_case(token))
}
#[cfg(test)]
mod tests {
use super::*;
fn request() -> Request {
Request::new(Method::Get, "/ws")
.with_header("upgrade", "websocket")
.with_header("connection", "Upgrade")
.with_header("sec-websocket-version", "13")
.with_header("sec-websocket-key", "dGhlIHNhbXBsZSBub25jZQ==")
}
#[test]
fn computes_the_accept_key_from_the_rfc_6455_example() {
assert_eq!(accept_key("dGhlIHNhbXBsZSBub25jZQ=="), "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=");
}
#[test]
fn accepts_a_well_formed_handshake() {
let handshake = negotiate(&request(), &[]).unwrap();
assert_eq!(handshake.key, "dGhlIHNhbXBsZSBub25jZQ==");
assert_eq!(handshake.accept, "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=");
assert_eq!(handshake.protocol, None);
let response = response(&handshake);
assert_eq!(response.status, Status(101));
assert_eq!(response.headers.get("upgrade"), Some("websocket"));
assert_eq!(response.headers.get("connection"), Some("Upgrade"));
assert_eq!(
response.headers.get("sec-websocket-accept"),
Some("s3pPLMBiTxaQ9kYGzzhZRbK+xOo=")
);
assert_eq!(response.headers.get("sec-websocket-protocol"), None);
}
#[test]
fn header_names_and_values_are_matched_case_insensitively() {
let request = Request::new(Method::Get, "/ws")
.with_header("Upgrade", "WebSocket")
.with_header("Connection", "keep-alive, Upgrade")
.with_header("Sec-WebSocket-Version", "13")
.with_header("Sec-WebSocket-Key", "dGhlIHNhbXBsZSBub25jZQ==");
assert!(negotiate(&request, &[]).is_ok());
}
#[test]
fn a_missing_upgrade_header_is_refused_by_name() {
let mut request = request();
request.headers_mut().remove("upgrade");
let error = negotiate(&request, &[]).unwrap_err();
assert_eq!(error, HandshakeError::MissingUpgrade);
assert!(error.message().contains("Upgrade: websocket"), "{error}");
}
#[test]
fn an_upgrade_to_something_else_is_refused() {
let request = request().with_header("upgrade", "h2c");
let error = negotiate(&request, &[]).unwrap_err();
assert_eq!(error, HandshakeError::NotWebSocket("h2c".to_string()));
}
#[test]
fn a_connection_header_that_does_not_list_upgrade_is_refused() {
let mut missing = request();
missing.headers_mut().remove("connection");
assert_eq!(negotiate(&missing, &[]).unwrap_err(), HandshakeError::MissingConnection);
let wrong = request().with_header("connection", "keep-alive");
assert_eq!(
negotiate(&wrong, &[]).unwrap_err(),
HandshakeError::ConnectionNotUpgrade("keep-alive".to_string())
);
}
#[test]
fn only_version_thirteen_is_supported() {
let mut missing = request();
missing.headers_mut().remove("sec-websocket-version");
assert_eq!(negotiate(&missing, &[]).unwrap_err(), HandshakeError::MissingVersion);
let old = request().with_header("sec-websocket-version", "8");
let error = negotiate(&old, &[]).unwrap_err();
assert_eq!(error, HandshakeError::UnsupportedVersion("8".to_string()));
assert!(error.message().contains("version 13"), "{error}");
}
#[test]
fn a_key_that_is_not_sixteen_base64_bytes_is_refused() {
let mut missing = request();
missing.headers_mut().remove("sec-websocket-key");
assert_eq!(negotiate(&missing, &[]).unwrap_err(), HandshakeError::MissingKey);
let short = request().with_header("sec-websocket-key", "Zm9v");
assert!(matches!(negotiate(&short, &[]), Err(HandshakeError::MalformedKey(_))));
let junk = request().with_header("sec-websocket-key", "not a key!");
assert!(matches!(negotiate(&junk, &[]), Err(HandshakeError::MalformedKey(_))));
}
#[test]
fn a_post_cannot_open_a_socket() {
let request = Request::new(Method::Post, "/ws")
.with_header("upgrade", "websocket")
.with_header("connection", "Upgrade")
.with_header("sec-websocket-version", "13")
.with_header("sec-websocket-key", "dGhlIHNhbXBsZSBub25jZQ==");
assert_eq!(negotiate(&request, &[]).unwrap_err(), HandshakeError::NotAGet(Method::Post));
}
#[test]
fn a_refused_handshake_answers_400_and_says_what_was_wrong() {
let response = HandshakeError::MissingKey.into_response();
assert_eq!(response.status, Status::BAD_REQUEST);
assert!(response.body_string().contains("Sec-WebSocket-Key"), "{}", response.body_string());
assert_eq!(response.headers.get("sec-websocket-version"), Some("13"));
}
#[test]
fn subprotocol_negotiation_follows_the_servers_preference() {
let request = request().with_header("sec-websocket-protocol", "soap, chat, superchat");
let supported = vec!["superchat".to_string(), "chat".to_string()];
let handshake = negotiate(&request, &supported).unwrap();
assert_eq!(handshake.offered, ["soap", "chat", "superchat"]);
assert_eq!(handshake.protocol.as_deref(), Some("superchat"));
assert_eq!(
response(&handshake).headers.get("sec-websocket-protocol"),
Some("superchat")
);
}
#[test]
fn no_shared_subprotocol_still_opens_a_plain_socket() {
let request = request().with_header("sec-websocket-protocol", "soap");
let handshake = negotiate(&request, &["chat".to_string()]).unwrap();
assert_eq!(handshake.protocol, None);
assert_eq!(response(&handshake).headers.get("sec-websocket-protocol"), None);
}
}