Skip to main content

codetics_ocpp_ws/
handshake.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 Kevin Vavelin, Codetics (https://codetics.fr)
3
4//! Opening handshake helpers (RFC 6455 §4).
5
6use crate::{base64, sha1};
7
8const GUID: &str = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
9
10/// Computes `Sec-WebSocket-Accept` for a `Sec-WebSocket-Key`.
11pub fn accept_key(key: &str) -> String {
12    let mut input = key.trim().as_bytes().to_vec();
13    input.extend_from_slice(GUID.as_bytes());
14    base64::encode(&sha1::sha1(&input))
15}
16
17/// `true` if a comma-separated header value contains `token` (case-insensitive).
18pub fn header_has_token(value: &str, token: &str) -> bool {
19    value
20        .split(',')
21        .any(|t| t.trim().eq_ignore_ascii_case(token))
22}
23
24/// Validates a 16-byte base64 `Sec-WebSocket-Key`.
25pub fn valid_client_key(key: &str) -> bool {
26    base64::decode(key).is_some_and(|k| k.len() == 16)
27}
28
29#[cfg(test)]
30mod tests {
31    use super::*;
32
33    #[test]
34    fn rfc_example() {
35        // RFC 6455 §1.3 and the OCPP-J examples.
36        assert_eq!(
37            accept_key("dGhlIHNhbXBsZSBub25jZQ=="),
38            "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="
39        );
40        assert_eq!(
41            accept_key("x3JJHMbDL1EzLkh9GBhXDw=="),
42            "HSmrc0sMlYUkAGmm5OPpG2HaGWk="
43        );
44        assert!(valid_client_key("dGhlIHNhbXBsZSBub25jZQ=="));
45        assert!(!valid_client_key("abc"));
46        assert!(header_has_token("keep-alive, Upgrade", "upgrade"));
47    }
48}