Skip to main content

webtrans_proto/
settings.rs

1//! HTTP/3 SETTINGS frame helpers for WebTransport.
2
3use std::{
4    collections::HashMap,
5    fmt::Debug,
6    ops::{Deref, DerefMut},
7    sync::Arc,
8};
9
10use bytes::{Buf, BufMut, BytesMut};
11
12use thiserror::Error;
13use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt};
14
15use super::{Frame, UniStream, VarInt, VarIntUnexpectedEnd};
16use crate::grease::is_grease_value;
17use crate::io::read_incremental;
18
19#[derive(Clone, Copy, PartialEq, Eq, Hash)]
20/// HTTP/3 SETTINGS identifier.
21pub struct Setting(pub VarInt);
22
23impl Setting {
24    /// Decode a settings identifier.
25    pub fn decode<B: Buf>(buf: &mut B) -> Result<Self, VarIntUnexpectedEnd> {
26        Ok(Setting(VarInt::decode(buf)?))
27    }
28
29    /// Encode a settings identifier.
30    pub fn encode<B: BufMut>(&self, buf: &mut B) {
31        self.0.encode(buf)
32    }
33
34    /// Return the encoded size of this identifier.
35    pub fn size(&self) -> usize {
36        self.0.size()
37    }
38
39    // Reference: https://datatracker.ietf.org/doc/html/rfc9114#section-7.2.4.1
40    /// Return `true` when the setting uses RFC 9114 GREASE spacing.
41    pub fn is_grease(&self) -> bool {
42        is_grease_value(self.0.into_inner())
43    }
44}
45
46impl Debug for Setting {
47    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48        match *self {
49            Setting::QPACK_MAX_TABLE_CAPACITY => write!(f, "QPACK_MAX_TABLE_CAPACITY"),
50            Setting::MAX_FIELD_SECTION_SIZE => write!(f, "MAX_FIELD_SECTION_SIZE"),
51            Setting::QPACK_BLOCKED_STREAMS => write!(f, "QPACK_BLOCKED_STREAMS"),
52            Setting::ENABLE_CONNECT_PROTOCOL => write!(f, "ENABLE_CONNECT_PROTOCOL"),
53            Setting::ENABLE_DATAGRAM => write!(f, "ENABLE_DATAGRAM"),
54            Setting::ENABLE_DATAGRAM_DEPRECATED => write!(f, "ENABLE_DATAGRAM_DEPRECATED"),
55            Setting::WEBTRANSPORT_ENABLE_DEPRECATED => write!(f, "WEBTRANSPORT_ENABLE_DEPRECATED"),
56            Setting::WEBTRANSPORT_MAX_SESSIONS_DEPRECATED => {
57                write!(f, "WEBTRANSPORT_MAX_SESSIONS_DEPRECATED")
58            }
59            Setting::WEBTRANSPORT_MAX_SESSIONS => write!(f, "WEBTRANSPORT_MAX_SESSIONS"),
60            x if x.is_grease() => write!(f, "GREASE SETTING [{:x?}]", x.0.into_inner()),
61            x => write!(f, "UNKNOWN_SETTING [{:x?}]", x.0.into_inner()),
62        }
63    }
64}
65
66impl Setting {
67    /// Build a settings identifier from a known `u32` value.
68    pub const fn from_u32(value: u32) -> Self {
69        Self(VarInt::from_u32(value))
70    }
71
72    // HTTP/3 settings that WebTransport ignores.
73    /// HTTP/3 QPACK dynamic table capacity setting.
74    pub const QPACK_MAX_TABLE_CAPACITY: Setting = Setting::from_u32(0x1); // Default is 0, which disables the dynamic table.
75    /// HTTP/3 maximum header field section size setting.
76    pub const MAX_FIELD_SECTION_SIZE: Setting = Setting::from_u32(0x6);
77    /// HTTP/3 QPACK blocked streams setting.
78    pub const QPACK_BLOCKED_STREAMS: Setting = Setting::from_u32(0x7);
79
80    // Both values are required for WebTransport.
81    /// HTTP/3 extended CONNECT enable flag.
82    pub const ENABLE_CONNECT_PROTOCOL: Setting = Setting::from_u32(0x8);
83    /// HTTP/3 datagram support flag (current).
84    pub const ENABLE_DATAGRAM: Setting = Setting::from_u32(0x33);
85    /// HTTP/3 datagram support flag (deprecated draft value).
86    pub const ENABLE_DATAGRAM_DEPRECATED: Setting = Setting::from_u32(0xFFD277); // Still used by some Chrome versions.
87
88    // Removed in draft-06.
89    /// Draft WebTransport enable flag (deprecated).
90    pub const WEBTRANSPORT_ENABLE_DEPRECATED: Setting = Setting::from_u32(0x2b603742);
91    /// Draft maximum WebTransport sessions setting (deprecated).
92    pub const WEBTRANSPORT_MAX_SESSIONS_DEPRECATED: Setting = Setting::from_u32(0x2b603743);
93
94    // Current way to enable WebTransport.
95    /// Current WebTransport maximum sessions setting.
96    pub const WEBTRANSPORT_MAX_SESSIONS: Setting = Setting::from_u32(0xc671706a);
97}
98
99#[derive(Error, Debug, Clone)]
100/// Errors returned while encoding or decoding SETTINGS exchanges.
101pub enum SettingsError {
102    /// Input ended before a full SETTINGS payload was available.
103    #[error("unexpected end of input")]
104    UnexpectedEnd,
105
106    /// First unidirectional stream type was not the expected control stream.
107    #[error("unexpected stream type {0:?}")]
108    UnexpectedStreamType(UniStream),
109
110    /// Frame type was not SETTINGS.
111    #[error("unexpected frame {0:?}")]
112    UnexpectedFrame(Frame),
113
114    /// Invalid varint or truncated settings pair inside a frame payload.
115    #[error("invalid size")]
116    InvalidSize,
117
118    /// I/O error while reading or writing the SETTINGS exchange.
119    #[error("io error: {0}")]
120    Io(Arc<std::io::Error>),
121}
122
123impl From<std::io::Error> for SettingsError {
124    fn from(err: std::io::Error) -> Self {
125        SettingsError::Io(Arc::new(err))
126    }
127}
128
129// A map of SETTINGS identifiers to values.
130#[derive(Default, Debug)]
131/// Parsed HTTP/3 settings map keyed by [`Setting`].
132pub struct Settings(HashMap<Setting, VarInt>);
133
134impl Settings {
135    /// Decode a control stream prefix and SETTINGS frame from an in-memory buffer.
136    pub fn decode<B: Buf>(buf: &mut B) -> Result<Self, SettingsError> {
137        let typ = UniStream::decode(buf).map_err(|_| SettingsError::UnexpectedEnd)?;
138        if typ != UniStream::CONTROL {
139            return Err(SettingsError::UnexpectedStreamType(typ));
140        }
141
142        let (typ, mut data) = Frame::read(buf).map_err(|_| SettingsError::UnexpectedEnd)?;
143        if typ != Frame::SETTINGS {
144            return Err(SettingsError::UnexpectedFrame(typ));
145        }
146
147        let mut settings = Settings::default();
148        while data.has_remaining() {
149            // Use InvalidSize because retrying will not help.
150            let id = Setting::decode(&mut data).map_err(|_| SettingsError::InvalidSize)?;
151            let value = VarInt::decode(&mut data).map_err(|_| SettingsError::InvalidSize)?;
152            // Only retain non-GREASE entries.
153            if !id.is_grease() {
154                settings.0.insert(id, value);
155            }
156        }
157
158        Ok(settings)
159    }
160
161    /// Read and decode one SETTINGS exchange from an async stream.
162    pub async fn read<S: AsyncRead + Unpin>(stream: &mut S) -> Result<Self, SettingsError> {
163        read_incremental(
164            stream,
165            |cursor| Self::decode(cursor),
166            |err| matches!(err, SettingsError::UnexpectedEnd),
167            SettingsError::UnexpectedEnd,
168        )
169        .await
170    }
171
172    /// Encode this settings map as a control stream prefix followed by a SETTINGS frame.
173    pub fn encode<B: BufMut>(&self, buf: &mut B) {
174        UniStream::CONTROL.encode(buf);
175        Frame::SETTINGS.encode(buf);
176
177        let payload_len = self.payload_len();
178        VarInt::try_from(payload_len as u64)
179            .expect("settings payload length exceeds VarInt bounds")
180            .encode(buf);
181
182        for (id, value) in &self.0 {
183            id.encode(buf);
184            value.encode(buf);
185        }
186    }
187
188    /// Encode and write this settings map to an async stream.
189    pub async fn write<S: AsyncWrite + Unpin>(&self, stream: &mut S) -> Result<(), SettingsError> {
190        let mut buf = BytesMut::with_capacity(self.encoded_len());
191        self.encode(&mut buf);
192        stream.write_all_buf(&mut buf).await?;
193        Ok(())
194    }
195
196    /// Enable WebTransport settings, including deprecated parameters for compatibility.
197    pub fn enable_webtransport(&mut self, max_sessions: u32) {
198        self.enable_webtransport_internal(max_sessions, true);
199    }
200
201    /// Enable WebTransport settings without deprecated draft parameters.
202    pub fn enable_webtransport_latest(&mut self, max_sessions: u32) {
203        self.enable_webtransport_internal(max_sessions, false);
204    }
205
206    fn enable_webtransport_internal(&mut self, max_sessions: u32, include_deprecated: bool) {
207        let max = VarInt::from_u32(max_sessions);
208
209        self.insert(Setting::ENABLE_CONNECT_PROTOCOL, VarInt::from_u32(1));
210        self.insert(Setting::ENABLE_DATAGRAM, VarInt::from_u32(1));
211        self.insert(Setting::ENABLE_DATAGRAM_DEPRECATED, VarInt::from_u32(1));
212        self.insert(Setting::WEBTRANSPORT_MAX_SESSIONS, max);
213
214        if include_deprecated {
215            self.insert(Setting::WEBTRANSPORT_MAX_SESSIONS_DEPRECATED, max);
216            self.insert(Setting::WEBTRANSPORT_ENABLE_DEPRECATED, VarInt::from_u32(1));
217        } else {
218            self.0
219                .remove(&Setting::WEBTRANSPORT_MAX_SESSIONS_DEPRECATED);
220            self.0.remove(&Setting::WEBTRANSPORT_ENABLE_DEPRECATED);
221        }
222    }
223
224    // Return the maximum number of sessions supported.
225    /// Return the peer-advertised maximum number of WebTransport sessions, or `0` when unsupported.
226    pub fn supports_webtransport(&self) -> u64 {
227        // Observed from Chrome 114.0.5735.198 (July 19, 2023).
228        // Setting(1): 65536,              // qpack_max_table_capacity
229        // Setting(6): 16384,              // max_field_section_size
230        // Setting(7): 100,                // qpack_blocked_streams
231        // Setting(51): 1,                 // enable_datagram
232        // Setting(16765559): 1            // enable_datagram_deprecated
233        // Setting(727725890): 1,          // webtransport_max_sessions_deprecated
234        // Setting(4445614305): 454654587, // grease
235
236        // NOTE: The presence of ENABLE_WEBTRANSPORT implies ENABLE_CONNECT is supported.
237
238        let datagram = self
239            .get(&Setting::ENABLE_DATAGRAM)
240            .or(self.get(&Setting::ENABLE_DATAGRAM_DEPRECATED))
241            .map(|v| v.into_inner());
242
243        if datagram != Some(1) {
244            return 0;
245        }
246
247        // Before draft-07, enabling WebTransport used two parameters: ENABLE=1 and MAX_SESSIONS=N.
248        // The modern approach uses MAX_SESSIONS alone, where non-zero means enabled.
249
250        if let Some(max) = self.get(&Setting::WEBTRANSPORT_MAX_SESSIONS) {
251            return max.into_inner();
252        }
253
254        let enabled = self
255            .get(&Setting::WEBTRANSPORT_ENABLE_DEPRECATED)
256            .map(|v| v.into_inner());
257        if enabled != Some(1) {
258            return 0;
259        }
260
261        // Only the server may set this value; default to 1 if absent.
262        self.get(&Setting::WEBTRANSPORT_MAX_SESSIONS_DEPRECATED)
263            .map(|v| v.into_inner())
264            .unwrap_or(1)
265    }
266
267    fn payload_len(&self) -> usize {
268        self.0
269            .iter()
270            .map(|(id, value)| id.size() + value.size())
271            .sum()
272    }
273
274    fn encoded_len(&self) -> usize {
275        let payload_len = self.payload_len();
276        UniStream::CONTROL.0.size()
277            + Frame::SETTINGS.0.size()
278            + VarInt::try_from(payload_len as u64)
279                .expect("settings payload length exceeds VarInt bounds")
280                .size()
281            + payload_len
282    }
283}
284
285impl Deref for Settings {
286    type Target = HashMap<Setting, VarInt>;
287
288    fn deref(&self) -> &Self::Target {
289        &self.0
290    }
291}
292
293impl DerefMut for Settings {
294    fn deref_mut(&mut self) -> &mut Self::Target {
295        &mut self.0
296    }
297}