use crate::http::{Http1Builder, Http1Response, encode_request};
use base64::Engine as _;
use base64::engine::general_purpose::STANDARD as B64;
use sha1::{Digest, Sha1};
use thiserror::Error;
const WS_GUID: &str = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
#[derive(Debug, Error)]
pub enum HandshakeError {
#[error("RNG failed to produce client key")]
RngFailure,
#[error("server returned status {0}, expected 101")]
BadStatus(u16),
#[error("missing required header: {0}")]
MissingHeader(&'static str),
#[error("Upgrade header is not 'websocket'")]
BadUpgrade,
#[error("Connection header does not contain 'Upgrade'")]
BadConnection,
#[error("Sec-WebSocket-Accept verification failed")]
BadAccept,
#[error("server returned unexpected Sec-WebSocket-Protocol: {0}")]
BadSubprotocol(String),
#[error("server returned unexpected Sec-WebSocket-Extensions: {0}")]
BadExtension(String),
}
pub fn generate_key() -> Result<String, HandshakeError> {
use ring::rand::SecureRandom;
let mut raw = [0_u8; 16];
let rng = ring::rand::SystemRandom::new();
rng.fill(&mut raw).map_err(|_| HandshakeError::RngFailure)?;
Ok(B64.encode(raw))
}
#[must_use]
pub fn compute_accept(client_key: &str) -> String {
let mut hasher = Sha1::new();
hasher.update(client_key.as_bytes());
hasher.update(WS_GUID.as_bytes());
let digest = hasher.finalize();
B64.encode(digest)
}
#[must_use]
pub fn verify_accept(client_key: &str, server_accept: &str) -> bool {
compute_accept(client_key) == server_accept
}
#[derive(Debug)]
pub struct UpgradeRequest<'a> {
pub host: &'a str,
pub path: &'a str,
pub key: &'a str,
pub subprotocols: &'a [&'a str],
pub origin: Option<&'a str>,
}
pub fn encode_upgrade_request(buf: &mut Vec<u8>, req: &UpgradeRequest<'_>) {
let subproto_joined: String = req.subprotocols.join(", ");
let mut headers: Vec<(&str, &str)> = vec![
("Upgrade", "websocket"),
("Connection", "Upgrade"),
("Sec-WebSocket-Key", req.key),
("Sec-WebSocket-Version", "13"),
];
if !req.subprotocols.is_empty() {
headers.push(("Sec-WebSocket-Protocol", subproto_joined.as_str()));
}
if let Some(o) = req.origin {
headers.push(("Origin", o));
}
let b = Http1Builder {
method: "GET",
path: req.path,
host: req.host,
headers,
};
encode_request(buf, &b);
}
pub fn verify_upgrade_response(
response: &Http1Response<'_>,
client_key: &str,
offered_subprotocols: &[&str],
) -> Result<(), HandshakeError> {
if response.status != 101 {
return Err(HandshakeError::BadStatus(response.status));
}
if !response.header_values("Upgrade").any(|value| {
value
.split(',')
.any(|s| s.trim().eq_ignore_ascii_case("websocket"))
}) {
if response.header("Upgrade").is_none() {
return Err(HandshakeError::MissingHeader("Upgrade"));
}
return Err(HandshakeError::BadUpgrade);
}
if !response.header_values("Connection").any(|value| {
value
.split(',')
.any(|s| s.trim().eq_ignore_ascii_case("Upgrade"))
}) {
if response.header("Connection").is_none() {
return Err(HandshakeError::MissingHeader("Connection"));
}
return Err(HandshakeError::BadConnection);
}
let accept = response
.header("Sec-WebSocket-Accept")
.ok_or(HandshakeError::MissingHeader("Sec-WebSocket-Accept"))?;
if !verify_accept(client_key, accept) {
return Err(HandshakeError::BadAccept);
}
if let Some(ext) = response.header("Sec-WebSocket-Extensions") {
return Err(HandshakeError::BadExtension(ext.to_owned()));
}
if let Some(server_proto) = response.header("Sec-WebSocket-Protocol") {
let server_proto = server_proto.trim();
if offered_subprotocols.is_empty() {
return Err(HandshakeError::BadSubprotocol(server_proto.to_owned()));
}
if !offered_subprotocols
.iter()
.any(|o| o.eq_ignore_ascii_case(server_proto))
{
return Err(HandshakeError::BadSubprotocol(server_proto.to_owned()));
}
}
Ok(())
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use crate::http::parse_response;
#[test]
fn rfc_sample_accept() {
let key = "dGhlIHNhbXBsZSBub25jZQ==";
let expected = "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=";
assert_eq!(compute_accept(key), expected);
assert!(verify_accept(key, expected));
}
#[test]
fn generate_key_is_24_chars() {
let k = generate_key().unwrap();
assert_eq!(k.len(), 24);
let raw = B64.decode(&k).unwrap();
assert_eq!(raw.len(), 16);
}
#[test]
fn upgrade_request_well_formed() {
let mut buf = Vec::new();
let req = UpgradeRequest {
host: "www.deribit.com",
path: "/ws/api/v2",
key: "dGhlIHNhbXBsZSBub25jZQ==",
subprotocols: &[],
origin: None,
};
encode_upgrade_request(&mut buf, &req);
let s = std::str::from_utf8(&buf).unwrap();
assert!(s.starts_with("GET /ws/api/v2 HTTP/1.1\r\n"));
assert!(s.contains("Host: www.deribit.com\r\n"));
assert!(s.contains("Upgrade: websocket\r\n"));
assert!(s.contains("Connection: Upgrade\r\n"));
assert!(s.contains("Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n"));
assert!(s.contains("Sec-WebSocket-Version: 13\r\n"));
}
#[test]
fn verify_101_ok() {
let resp = b"HTTP/1.1 101 Switching Protocols\r\n\
Upgrade: websocket\r\n\
Connection: Upgrade\r\n\
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\
\r\n";
let (r, _) = parse_response(resp).unwrap().unwrap();
verify_upgrade_response(&r, "dGhlIHNhbXBsZSBub25jZQ==", &[]).unwrap();
}
#[test]
fn verify_rejects_bad_accept() {
let resp = b"HTTP/1.1 101 OK\r\n\
Upgrade: websocket\r\n\
Connection: Upgrade\r\n\
Sec-WebSocket-Accept: AAAAAAAAAAAAAAAAAAAAAAAAAAA=\r\n\
\r\n";
let (r, _) = parse_response(resp).unwrap().unwrap();
let err = verify_upgrade_response(&r, "dGhlIHNhbXBsZSBub25jZQ==", &[]).unwrap_err();
assert!(matches!(err, HandshakeError::BadAccept));
}
#[test]
fn verify_rejects_non_101() {
let resp = b"HTTP/1.1 400 Bad Request\r\n\r\n";
let (r, _) = parse_response(resp).unwrap().unwrap();
let err = verify_upgrade_response(&r, "any", &[]).unwrap_err();
assert!(matches!(err, HandshakeError::BadStatus(400)));
}
#[test]
fn verify_rejects_unsolicited_subprotocol() {
let resp = b"HTTP/1.1 101 OK\r\n\
Upgrade: websocket\r\n\
Connection: Upgrade\r\n\
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\
Sec-WebSocket-Protocol: chat\r\n\
\r\n";
let (r, _) = parse_response(resp).unwrap().unwrap();
let err = verify_upgrade_response(&r, "dGhlIHNhbXBsZSBub25jZQ==", &[]).unwrap_err();
assert!(matches!(err, HandshakeError::BadSubprotocol(_)));
}
#[test]
fn verify_rejects_subprotocol_not_in_offered() {
let resp = b"HTTP/1.1 101 OK\r\n\
Upgrade: websocket\r\n\
Connection: Upgrade\r\n\
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\
Sec-WebSocket-Protocol: notice\r\n\
\r\n";
let (r, _) = parse_response(resp).unwrap().unwrap();
let err =
verify_upgrade_response(&r, "dGhlIHNhbXBsZSBub25jZQ==", &["chat", "echo"]).unwrap_err();
assert!(matches!(err, HandshakeError::BadSubprotocol(_)));
}
#[test]
fn verify_accepts_subprotocol_in_offered() {
let resp = b"HTTP/1.1 101 OK\r\n\
Upgrade: websocket\r\n\
Connection: Upgrade\r\n\
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\
Sec-WebSocket-Protocol: chat\r\n\
\r\n";
let (r, _) = parse_response(resp).unwrap().unwrap();
verify_upgrade_response(&r, "dGhlIHNhbXBsZSBub25jZQ==", &["chat", "echo"]).unwrap();
}
#[test]
fn verify_connection_multi_value() {
let resp = b"HTTP/1.1 101 OK\r\n\
Upgrade: websocket\r\n\
Connection: keep-alive, Upgrade\r\n\
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\
\r\n";
let (r, _) = parse_response(resp).unwrap().unwrap();
verify_upgrade_response(&r, "dGhlIHNhbXBsZSBub25jZQ==", &[]).unwrap();
}
#[test]
fn verify_connection_split_headers() {
let resp = b"HTTP/1.1 101 OK\r\n\
Upgrade: websocket\r\n\
Connection: keep-alive\r\n\
Connection: Upgrade\r\n\
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\
\r\n";
let (r, _) = parse_response(resp).unwrap().unwrap();
verify_upgrade_response(&r, "dGhlIHNhbXBsZSBub25jZQ==", &[]).unwrap();
}
#[test]
fn verify_rejects_unsolicited_extension() {
let resp = b"HTTP/1.1 101 OK\r\n\
Upgrade: websocket\r\n\
Connection: Upgrade\r\n\
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\
Sec-WebSocket-Extensions: permessage-deflate\r\n\
\r\n";
let (r, _) = parse_response(resp).unwrap().unwrap();
let err = verify_upgrade_response(&r, "dGhlIHNhbXBsZSBub25jZQ==", &[]).unwrap_err();
assert!(matches!(err, HandshakeError::BadExtension(_)));
}
}