const WS_GUID: &str = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
pub fn accept_key(client_key: &str) -> String {
let digest = sha1(format!("{client_key}{WS_GUID}").as_bytes());
base64_encode(&digest)
}
pub fn text_frame(payload: &str) -> Vec<u8> {
let bytes = payload.as_bytes();
let mut frame = Vec::with_capacity(bytes.len() + 10);
frame.push(0x81); let len = bytes.len();
if len < 126 {
frame.push(len as u8);
} else if len <= u16::MAX as usize {
frame.push(126);
frame.extend_from_slice(&(len as u16).to_be_bytes());
} else {
frame.push(127);
frame.extend_from_slice(&(len as u64).to_be_bytes());
}
frame.extend_from_slice(bytes);
frame
}
pub fn websocket_key(request: &str) -> Option<String> {
let is_upgrade = request
.lines()
.any(|l| l.to_ascii_lowercase().starts_with("upgrade:") && l.to_ascii_lowercase().contains("websocket"));
if !is_upgrade {
return None;
}
request.lines().find_map(|l| {
let lower = l.to_ascii_lowercase();
if lower.starts_with("sec-websocket-key:") {
Some(l[l.find(':')? + 1..].trim().to_string())
} else {
None
}
})
}
pub fn handshake_response(client_key: &str) -> String {
format!(
"HTTP/1.1 101 Switching Protocols\r\n\
Upgrade: websocket\r\n\
Connection: Upgrade\r\n\
Sec-WebSocket-Accept: {}\r\n\r\n",
accept_key(client_key)
)
}
fn sha1(message: &[u8]) -> [u8; 20] {
let mut h: [u32; 5] = [0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476, 0xC3D2E1F0];
let bit_len = (message.len() as u64) * 8;
let mut data = message.to_vec();
data.push(0x80);
while data.len() % 64 != 56 {
data.push(0);
}
data.extend_from_slice(&bit_len.to_be_bytes());
for chunk in data.chunks_exact(64) {
let mut w = [0u32; 80];
for (i, word) in chunk.chunks_exact(4).enumerate() {
w[i] = u32::from_be_bytes([word[0], word[1], word[2], word[3]]);
}
for i in 16..80 {
w[i] = (w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16]).rotate_left(1);
}
let (mut a, mut b, mut c, mut d, mut e) = (h[0], h[1], h[2], h[3], h[4]);
for (i, &wi) in w.iter().enumerate() {
let (f, k) = match i {
0..=19 => ((b & c) | ((!b) & d), 0x5A827999u32),
20..=39 => (b ^ c ^ d, 0x6ED9EBA1),
40..=59 => ((b & c) | (b & d) | (c & d), 0x8F1BBCDC),
_ => (b ^ c ^ d, 0xCA62C1D6),
};
let tmp = a
.rotate_left(5)
.wrapping_add(f)
.wrapping_add(e)
.wrapping_add(k)
.wrapping_add(wi);
e = d;
d = c;
c = b.rotate_left(30);
b = a;
a = tmp;
}
h[0] = h[0].wrapping_add(a);
h[1] = h[1].wrapping_add(b);
h[2] = h[2].wrapping_add(c);
h[3] = h[3].wrapping_add(d);
h[4] = h[4].wrapping_add(e);
}
let mut out = [0u8; 20];
for (i, word) in h.iter().enumerate() {
out[i * 4..i * 4 + 4].copy_from_slice(&word.to_be_bytes());
}
out
}
fn base64_encode(data: &[u8]) -> String {
const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut out = String::with_capacity(data.len().div_ceil(3) * 4);
for chunk in data.chunks(3) {
let b0 = chunk[0] as u32;
let b1 = *chunk.get(1).unwrap_or(&0) as u32;
let b2 = *chunk.get(2).unwrap_or(&0) as u32;
let n = (b0 << 16) | (b1 << 8) | b2;
out.push(ALPHABET[(n >> 18 & 63) as usize] as char);
out.push(ALPHABET[(n >> 12 & 63) as usize] as char);
out.push(if chunk.len() > 1 { ALPHABET[(n >> 6 & 63) as usize] as char } else { '=' });
out.push(if chunk.len() > 2 { ALPHABET[(n & 63) as usize] as char } else { '=' });
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sha1_known_vectors() {
assert_eq!(hex(&sha1(b"abc")), "a9993e364706816aba3e25717850c26c9cd0d89d");
assert_eq!(hex(&sha1(b"")), "da39a3ee5e6b4b0d3255bfef95601890afd80709");
}
#[test]
fn base64_known_vectors() {
assert_eq!(base64_encode(b"Man"), "TWFu");
assert_eq!(base64_encode(b"Ma"), "TWE=");
assert_eq!(base64_encode(b"M"), "TQ==");
}
#[test]
fn accept_key_matches_rfc6455_example() {
assert_eq!(accept_key("dGhlIHNhbXBsZSBub25jZQ=="), "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=");
}
#[test]
fn text_frame_small_payload_is_fin_text_unmasked() {
let f = text_frame("hi");
assert_eq!(f[0], 0x81); assert_eq!(f[1], 2); assert_eq!(&f[2..], b"hi");
}
#[test]
fn text_frame_uses_16bit_length_over_125() {
let payload = "x".repeat(200);
let f = text_frame(&payload);
assert_eq!(f[0], 0x81);
assert_eq!(f[1], 126);
assert_eq!(u16::from_be_bytes([f[2], f[3]]), 200);
assert_eq!(f.len(), 4 + 200);
}
#[test]
fn websocket_key_extracted_only_from_an_upgrade_request() {
let req = "GET /__rosace_hot HTTP/1.1\r\nUpgrade: websocket\r\nSec-WebSocket-Key: abc123==\r\n\r\n";
assert_eq!(websocket_key(req).as_deref(), Some("abc123=="));
let plain = "GET /index.html HTTP/1.1\r\nHost: localhost\r\n\r\n";
assert_eq!(websocket_key(plain), None);
}
fn hex(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{b:02x}")).collect()
}
}