Skip to main content

zerodds_websocket_bridge/
negotiation.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! Generic Extension- und Subprotocol-Negotiation nach RFC 6455 §9.
5//!
6//! Neben permessage-deflate (RFC 7692, siehe `permessage_deflate.rs`)
7//! braucht der Handshake-Layer Hilfen fuer:
8//!
9//! - **Sec-WebSocket-Extensions** generisches Parsing einer
10//!   Extension-Liste mit Parametern (z.B. `foo; bar=baz, qux`).
11//! - **Sec-WebSocket-Protocol** Subprotocol-Negotiation (Server
12//!   waehlt eines der vom Client angebotenen Subprotokolle).
13
14use alloc::string::{String, ToString};
15use alloc::vec::Vec;
16
17/// Eine geparste Extension mit optionalen Parametern.
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct ExtensionOffer {
20    /// Extension-Name (token, case-insensitive).
21    pub name: String,
22    /// Liste der `name=value`-Parameter; `value=None` entspricht
23    /// einem Boolean-Parameter (nur Name).
24    pub params: Vec<(String, Option<String>)>,
25}
26
27/// Parst den Wert eines `Sec-WebSocket-Extensions`-Headers in eine
28/// Liste von `ExtensionOffer`s.
29///
30/// Beispiel: `permessage-deflate; client_max_window_bits, foo`
31/// → 2 Eintraege, der erste mit Parameter `client_max_window_bits`
32/// (kein Wert), der zweite ohne Parameter.
33#[must_use]
34pub fn parse_extensions(header: &str) -> Vec<ExtensionOffer> {
35    let mut offers = Vec::new();
36    for raw in header.split(',') {
37        let trimmed = raw.trim();
38        if trimmed.is_empty() {
39            continue;
40        }
41        let mut parts = trimmed.split(';').map(str::trim);
42        let Some(name) = parts.next() else {
43            continue;
44        };
45        if name.is_empty() {
46            continue;
47        }
48        let mut params = Vec::new();
49        for p in parts {
50            if p.is_empty() {
51                continue;
52            }
53            if let Some(eq_pos) = p.find('=') {
54                let k = p[..eq_pos].trim().to_string();
55                let v = p[eq_pos + 1..].trim().trim_matches('"').to_string();
56                params.push((k, Some(v)));
57            } else {
58                params.push((p.to_string(), None));
59            }
60        }
61        offers.push(ExtensionOffer {
62            name: name.to_string(),
63            params,
64        });
65    }
66    offers
67}
68
69/// Parst den Wert eines `Sec-WebSocket-Protocol`-Headers in eine
70/// Liste von Subprotocol-Tokens.
71#[must_use]
72pub fn parse_subprotocols(header: &str) -> Vec<String> {
73    header
74        .split(',')
75        .map(|s| s.trim().to_string())
76        .filter(|s| !s.is_empty())
77        .collect()
78}
79
80/// Server-Pfad: waehlt das erste vom Client angebotene Subprotocol,
81/// das in der Server-Preferenzliste vorkommt. Liefert `None` wenn
82/// keine Schnittmenge.
83#[must_use]
84pub fn select_subprotocol(client_offered: &[String], server_preferred: &[&str]) -> Option<String> {
85    for offer in client_offered {
86        for pref in server_preferred {
87            if offer.eq_ignore_ascii_case(pref) {
88                return Some(offer.clone());
89            }
90        }
91    }
92    None
93}
94
95/// Spec §4.2.2 — Spec-konformer Default-Header-Name fuer Subprotocol.
96pub const SUBPROTOCOL_HEADER: &str = "Sec-WebSocket-Protocol";
97
98/// Spec §4.2.1 — Spec-konformer Default-Header-Name fuer Extensions.
99pub const EXTENSIONS_HEADER: &str = "Sec-WebSocket-Extensions";
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    #[test]
106    fn parse_extensions_single() {
107        let offers = parse_extensions("permessage-deflate");
108        assert_eq!(offers.len(), 1);
109        assert_eq!(offers[0].name, "permessage-deflate");
110        assert!(offers[0].params.is_empty());
111    }
112
113    #[test]
114    fn parse_extensions_with_param() {
115        let offers = parse_extensions("permessage-deflate; client_max_window_bits");
116        assert_eq!(offers.len(), 1);
117        assert_eq!(offers[0].params.len(), 1);
118        assert_eq!(offers[0].params[0], ("client_max_window_bits".into(), None));
119    }
120
121    #[test]
122    fn parse_extensions_with_value_param() {
123        let offers = parse_extensions("foo; bar=baz");
124        assert_eq!(offers[0].params[0].0, "bar");
125        assert_eq!(offers[0].params[0].1.as_deref(), Some("baz"));
126    }
127
128    #[test]
129    fn parse_extensions_strips_quoted_value() {
130        let offers = parse_extensions("foo; bar=\"baz\"");
131        assert_eq!(offers[0].params[0].1.as_deref(), Some("baz"));
132    }
133
134    #[test]
135    fn parse_extensions_multiple_offers() {
136        let offers = parse_extensions("foo, bar; x=1, baz");
137        assert_eq!(offers.len(), 3);
138        assert_eq!(offers[0].name, "foo");
139        assert_eq!(offers[1].name, "bar");
140        assert_eq!(offers[2].name, "baz");
141    }
142
143    #[test]
144    fn parse_extensions_empty_returns_empty() {
145        assert!(parse_extensions("").is_empty());
146        assert!(parse_extensions(" , ").is_empty());
147    }
148
149    #[test]
150    fn parse_subprotocols_basic() {
151        assert_eq!(
152            parse_subprotocols("chat, soap, mqtt"),
153            vec!["chat".to_string(), "soap".into(), "mqtt".into()]
154        );
155    }
156
157    #[test]
158    fn parse_subprotocols_empty_returns_empty() {
159        assert!(parse_subprotocols("").is_empty());
160    }
161
162    #[test]
163    fn select_subprotocol_picks_first_match() {
164        let client = vec!["soap".to_string(), "chat".into()];
165        let server = ["mqtt", "chat", "soap"];
166        assert_eq!(
167            select_subprotocol(&client, &server).as_deref(),
168            Some("soap")
169        );
170    }
171
172    #[test]
173    fn select_subprotocol_returns_none_when_no_match() {
174        let client = vec!["xmpp".to_string()];
175        let server = ["chat", "mqtt"];
176        assert!(select_subprotocol(&client, &server).is_none());
177    }
178
179    #[test]
180    fn select_subprotocol_is_case_insensitive() {
181        let client = vec!["CHAT".to_string()];
182        let server = ["chat"];
183        assert_eq!(
184            select_subprotocol(&client, &server).as_deref(),
185            Some("CHAT")
186        );
187    }
188
189    #[test]
190    fn header_constants_match_spec() {
191        assert_eq!(SUBPROTOCOL_HEADER, "Sec-WebSocket-Protocol");
192        assert_eq!(EXTENSIONS_HEADER, "Sec-WebSocket-Extensions");
193    }
194}