gatewayd: ws, the WebSocket error type and the opening handshake
Implemented-By: OpenCode session (model recorded in docs/implementer-log.md)
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
//! The opening handshake (RFC 6455, section 4.1) and the base64 it needs.
|
||||
|
||||
use std::io::{Read, Write};
|
||||
|
||||
use crate::http::{Head, read_head};
|
||||
use crate::ws::WsError;
|
||||
|
||||
/// RFC 6455's magic string, appended to the key before hashing.
|
||||
pub const GUID: &str = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
||||
|
||||
const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
|
||||
/// Standard base64 with padding (RFC 4648, section 4).
|
||||
pub fn base64(bytes: &[u8]) -> String {
|
||||
// Standard alphabet (ALPHABET), "=" padding: each 3 bytes become 4 characters; a last group of
|
||||
// 1 or 2 bytes becomes 2 or 3 characters and 2 or 1 "=". No indexing that can go out of bounds.
|
||||
let mut out = String::new();
|
||||
for chunk in bytes.chunks(3) {
|
||||
let b0 = chunk.first().copied().unwrap_or(0);
|
||||
let b1 = chunk.get(1).copied().unwrap_or(0);
|
||||
let b2 = chunk.get(2).copied().unwrap_or(0);
|
||||
let n = ((b0 as u32) << 16) | ((b1 as u32) << 8) | (b2 as u32);
|
||||
out.push(ALPHABET[((n >> 18) & 0x3f) as usize] as char);
|
||||
out.push(ALPHABET[((n >> 12) & 0x3f) as usize] as char);
|
||||
out.push(if chunk.len() > 1 {
|
||||
ALPHABET[((n >> 6) & 0x3f) as usize] as char
|
||||
} else {
|
||||
'='
|
||||
});
|
||||
out.push(if chunk.len() > 2 {
|
||||
ALPHABET[(n & 0x3f) as usize] as char
|
||||
} else {
|
||||
'='
|
||||
});
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// The `Sec-WebSocket-Accept` a server must send for `key`.
|
||||
pub fn accept_for(key: &str) -> String {
|
||||
// base64(sha1(key + GUID)), with `proto::sha1::sha1`.
|
||||
let mut data = Vec::with_capacity(key.len() + GUID.len());
|
||||
data.extend_from_slice(key.as_bytes());
|
||||
data.extend_from_slice(GUID.as_bytes());
|
||||
base64(&proto::sha1::sha1(&data))
|
||||
}
|
||||
|
||||
/// A fresh key: 16 bytes from `random` (in `gatewayd`, `/dev/urandom`), in base64.
|
||||
pub fn new_key(random: &mut dyn Read) -> std::io::Result<String> {
|
||||
// 16 bytes read from `random` with read_exact, then base64.
|
||||
let mut key = [0u8; 16];
|
||||
random.read_exact(&mut key)?;
|
||||
Ok(base64(&key))
|
||||
}
|
||||
|
||||
/// The request, exactly.
|
||||
pub fn request_text(host: &str, path: &str, key: &str, token: &str) -> String {
|
||||
format!(
|
||||
"GET {path} HTTP/1.1\r\nHost: {host}\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n\
|
||||
Sec-WebSocket-Key: {key}\r\nSec-WebSocket-Version: 13\r\nAuthorization: Bearer {token}\r\n\r\n"
|
||||
)
|
||||
}
|
||||
|
||||
/// Is `head` a server's acceptance of `key`? Status 101, `Upgrade: websocket`, a `Connection`
|
||||
/// holding the token `upgrade`, and the right `Sec-WebSocket-Accept` (case matters there).
|
||||
pub fn check_response(head: &Head, key: &str) -> Result<(), WsError> {
|
||||
// In this order, each a Handshake error: status is not 101 ("status <n>"); no Upgrade header
|
||||
// equal to "websocket" ignoring case; no Connection header with a comma-separated token equal
|
||||
// to "upgrade" ignoring case; Sec-WebSocket-Accept missing, or not exactly `accept_for(key)`.
|
||||
if head.status != 101 {
|
||||
return Err(WsError::Handshake(format!("status {}", head.status)));
|
||||
}
|
||||
if !head
|
||||
.header("Upgrade")
|
||||
.is_some_and(|v| v.eq_ignore_ascii_case("websocket"))
|
||||
{
|
||||
return Err(WsError::Handshake(
|
||||
"the Upgrade header is not websocket".to_string(),
|
||||
));
|
||||
}
|
||||
let upgraded = head.header("Connection").is_some_and(|v| {
|
||||
v.split(',')
|
||||
.any(|tok| tok.trim().eq_ignore_ascii_case("upgrade"))
|
||||
});
|
||||
if !upgraded {
|
||||
return Err(WsError::Handshake(
|
||||
"the Connection header has no upgrade token".to_string(),
|
||||
));
|
||||
}
|
||||
match head.header("Sec-WebSocket-Accept") {
|
||||
Some(got) if got == accept_for(key) => Ok(()),
|
||||
_ => Err(WsError::Handshake(
|
||||
"the Sec-WebSocket-Accept is missing or wrong".to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// The whole handshake on `stream`. Nothing after the server's head is read.
|
||||
pub fn handshake(
|
||||
stream: &mut (impl Read + Write),
|
||||
host: &str,
|
||||
path: &str,
|
||||
token: &str,
|
||||
random: &mut dyn Read,
|
||||
) -> Result<(), WsError> {
|
||||
// A new key; write `request_text` and flush; `read_head` (its error is a Handshake error); then
|
||||
// `check_response`. Read nothing after the head.
|
||||
let key = new_key(random)?;
|
||||
stream.write_all(request_text(host, path, &key, token).as_bytes())?;
|
||||
stream.flush()?;
|
||||
let head = read_head(stream).map_err(|e| WsError::Handshake(e.to_string()))?;
|
||||
check_response(&head, &key)
|
||||
}
|
||||
Reference in New Issue
Block a user