Skip to main content

webtrans_proto/
capsule.rs

1//! Capsule parsing and serialization for WebTransport over HTTP/3.
2
3use std::sync::Arc;
4
5use bytes::{Buf, BufMut, Bytes, BytesMut};
6use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
7
8use crate::grease::is_grease_value;
9use crate::{VarInt, VarIntUnexpectedEnd};
10
11// The draft (draft-ietf-webtrans-http3-06) specifies type 0x2843, which encodes as 0x68 0x43.
12// Some wire traces show 0x43 0x28 (decoded as 808), so implementations may diverge.
13// Use 0x2843 per the current specification.
14const CLOSE_WEBTRANSPORT_SESSION_TYPE: u64 = 0x2843;
15const MAX_MESSAGE_SIZE: usize = 1024;
16const MAX_CLOSE_PAYLOAD_SIZE: usize = 4 + MAX_MESSAGE_SIZE;
17
18#[derive(Debug, Clone, PartialEq, Eq)]
19/// WebTransport HTTP/3 capsule payloads.
20pub enum Capsule {
21    /// CLOSE_WEBTRANSPORT_SESSION capsule carrying application close details.
22    CloseWebTransportSession {
23        /// Application close code in WebTransport space.
24        code: u32,
25        /// UTF-8 close reason.
26        reason: String,
27    },
28    /// Any unknown capsule type preserved as raw bytes.
29    Unknown {
30        /// Unrecognized capsule type identifier.
31        typ: VarInt,
32        /// Raw payload bytes for the unknown type.
33        payload: Bytes,
34    },
35}
36
37impl Capsule {
38    /// Decode one capsule from a complete in-memory buffer.
39    pub fn decode<B: Buf>(buf: &mut B) -> Result<Self, CapsuleError> {
40        loop {
41            let typ = VarInt::decode(buf)?;
42            let length = VarInt::decode(buf)?;
43
44            let mut payload = buf.take(length.into_inner() as usize);
45            if payload.remaining() > MAX_CLOSE_PAYLOAD_SIZE {
46                return Err(CapsuleError::MessageTooLong);
47            }
48
49            if payload.remaining() < payload.limit() {
50                return Err(CapsuleError::UnexpectedEnd);
51            }
52
53            match typ.into_inner() {
54                CLOSE_WEBTRANSPORT_SESSION_TYPE => {
55                    if payload.remaining() < 4 {
56                        return Err(CapsuleError::UnexpectedEnd);
57                    }
58
59                    let error_code = payload.get_u32();
60
61                    let message_len = payload.remaining();
62                    if message_len > MAX_MESSAGE_SIZE {
63                        return Err(CapsuleError::MessageTooLong);
64                    }
65
66                    let message_bytes = payload.copy_to_bytes(message_len);
67                    let error_message = String::from_utf8(message_bytes.to_vec())
68                        .map_err(|_| CapsuleError::InvalidUtf8)?;
69
70                    return Ok(Self::CloseWebTransportSession {
71                        code: error_code,
72                        reason: error_message,
73                    });
74                }
75                t if is_grease(t) => continue,
76                _ => {
77                    let payload_bytes = payload.copy_to_bytes(payload.remaining());
78                    return Ok(Self::Unknown {
79                        typ,
80                        payload: payload_bytes,
81                    });
82                }
83            }
84        }
85    }
86
87    /// Read and decode one capsule from an async stream.
88    pub async fn read<S: AsyncRead + Unpin>(stream: &mut S) -> Result<Self, CapsuleError> {
89        loop {
90            let typ = VarInt::read(stream)
91                .await
92                .map_err(|_| CapsuleError::UnexpectedEnd)?;
93            let length = VarInt::read(stream)
94                .await
95                .map_err(|_| CapsuleError::UnexpectedEnd)?;
96            let length =
97                usize::try_from(length.into_inner()).map_err(|_| CapsuleError::MessageTooLong)?;
98            if length > MAX_CLOSE_PAYLOAD_SIZE {
99                return Err(CapsuleError::MessageTooLong);
100            }
101
102            let mut payload = vec![0; length];
103            stream.read_exact(&mut payload).await?;
104            if is_grease(typ.into_inner()) {
105                continue;
106            }
107
108            let mut capsule = Vec::with_capacity(VarInt::MAX_SIZE * 2 + length);
109            typ.encode(&mut capsule);
110            VarInt::try_from(length)
111                .map_err(|_| CapsuleError::MessageTooLong)?
112                .encode(&mut capsule);
113            capsule.extend_from_slice(&payload);
114            return Self::decode(&mut capsule.as_slice());
115        }
116    }
117
118    /// Encode this capsule into the provided buffer.
119    pub fn encode<B: BufMut>(&self, buf: &mut B) -> Result<(), CapsuleError> {
120        match self {
121            Self::CloseWebTransportSession {
122                code: error_code,
123                reason: error_message,
124            } => {
125                if error_message.len() > MAX_MESSAGE_SIZE {
126                    return Err(CapsuleError::MessageTooLong);
127                }
128
129                // Encode the capsule type.
130                VarInt::from_u64(CLOSE_WEBTRANSPORT_SESSION_TYPE)
131                    .unwrap()
132                    .encode(buf);
133
134                // Calculate and encode the payload length.
135                let length = 4 + error_message.len();
136                VarInt::from_u32(length as u32).encode(buf);
137
138                // Encode the 32-bit error code.
139                buf.put_u32(*error_code);
140
141                // Encode the UTF-8 error message.
142                buf.put_slice(error_message.as_bytes());
143            }
144            Self::Unknown { typ, payload } => {
145                if payload.len() > MAX_CLOSE_PAYLOAD_SIZE {
146                    return Err(CapsuleError::MessageTooLong);
147                }
148
149                // Encode the capsule type.
150                typ.encode(buf);
151
152                // Encode the payload length.
153                VarInt::try_from(payload.len())
154                    .map_err(|_| CapsuleError::MessageTooLong)?
155                    .encode(buf);
156
157                // Encode the payload bytes.
158                buf.put_slice(payload);
159            }
160        }
161        Ok(())
162    }
163
164    /// Encode and write this capsule to an async stream.
165    pub async fn write<S: AsyncWrite + Unpin>(&self, stream: &mut S) -> Result<(), CapsuleError> {
166        let mut buf = BytesMut::new();
167        self.encode(&mut buf)?;
168        stream.write_all_buf(&mut buf).await?;
169        Ok(())
170    }
171}
172
173fn is_grease(val: u64) -> bool {
174    is_grease_value(val)
175}
176
177#[derive(Debug, Clone, thiserror::Error)]
178/// Errors returned by capsule encoding and decoding.
179pub enum CapsuleError {
180    #[error("unexpected end of buffer")]
181    /// Input ended before the full capsule payload could be read.
182    UnexpectedEnd,
183
184    #[error("invalid UTF-8")]
185    /// CLOSE_WEBTRANSPORT_SESSION reason bytes were not valid UTF-8.
186    InvalidUtf8,
187
188    #[error("message too long")]
189    /// Capsule payload exceeded the implementation message limit.
190    MessageTooLong,
191
192    #[error("unknown capsule type: {0:?}")]
193    /// Capsule type is unsupported by this implementation.
194    UnknownType(VarInt),
195
196    #[error("varint decode error: {0:?}")]
197    /// Failed to decode a QUIC variable-length integer.
198    VarInt(#[from] VarIntUnexpectedEnd),
199
200    #[error("io error: {0}")]
201    /// I/O error while reading from or writing to a stream.
202    Io(Arc<std::io::Error>),
203}
204
205impl From<std::io::Error> for CapsuleError {
206    fn from(err: std::io::Error) -> Self {
207        CapsuleError::Io(Arc::new(err))
208    }
209}
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214    use bytes::Bytes;
215
216    #[test]
217    fn test_close_webtransport_session_decode() {
218        // Validate the spec-defined type 0x2843 (encoded as 0x68 0x43).
219        let mut data = Vec::new();
220        VarInt::from_u64(0x2843).unwrap().encode(&mut data);
221        VarInt::from_u32(8).encode(&mut data);
222        data.extend_from_slice(b"\x00\x00\x01\xa4test");
223
224        let mut buf = data.as_slice();
225        let capsule = Capsule::decode(&mut buf).unwrap();
226
227        match capsule {
228            Capsule::CloseWebTransportSession {
229                code: error_code,
230                reason: error_message,
231            } => {
232                assert_eq!(error_code, 420);
233                assert_eq!(error_message, "test");
234            }
235            _ => panic!("Expected CloseWebTransportSession"),
236        }
237
238        assert_eq!(buf.len(), 0); // All bytes should be consumed.
239    }
240
241    #[test]
242    fn test_close_webtransport_session_encode() {
243        let capsule = Capsule::CloseWebTransportSession {
244            code: 420,
245            reason: "test".to_string(),
246        };
247
248        let mut buf = Vec::new();
249        capsule.encode(&mut buf).unwrap();
250
251        // Expected format: type(0x2843 as varint = 0x68 0x43) + length(8 as varint)
252        // + error_code(420 as u32 BE) + "test".
253        assert_eq!(buf, b"\x68\x43\x08\x00\x00\x01\xa4test");
254    }
255
256    #[test]
257    fn test_close_webtransport_session_roundtrip() {
258        let original = Capsule::CloseWebTransportSession {
259            code: 12345,
260            reason: "Connection closed by application".to_string(),
261        };
262
263        let mut buf = Vec::new();
264        original.encode(&mut buf).unwrap();
265
266        let mut read_buf = buf.as_slice();
267        let decoded = Capsule::decode(&mut read_buf).unwrap();
268
269        assert_eq!(original, decoded);
270        assert_eq!(read_buf.len(), 0); // All bytes should be consumed.
271    }
272
273    #[test]
274    fn test_empty_error_message() {
275        let capsule = Capsule::CloseWebTransportSession {
276            code: 0,
277            reason: String::new(),
278        };
279
280        let mut buf = Vec::new();
281        capsule.encode(&mut buf).unwrap();
282
283        // Type(0x2843 as varint = 0x68 0x43) + Length(4) + error_code(0).
284        assert_eq!(buf, b"\x68\x43\x04\x00\x00\x00\x00");
285
286        let mut read_buf = buf.as_slice();
287        let decoded = Capsule::decode(&mut read_buf).unwrap();
288        assert_eq!(capsule, decoded);
289    }
290
291    #[test]
292    fn test_invalid_utf8() {
293        // Create a capsule with invalid UTF-8 in the message.
294        let mut data = Vec::new();
295        VarInt::from_u64(0x2843).unwrap().encode(&mut data); // type
296        VarInt::from_u32(5).encode(&mut data); // length(5)
297        data.extend_from_slice(b"\x00\x00\x00\x00"); // error_code(0)
298        data.push(0xFF); // Invalid UTF-8 byte.
299
300        let mut buf = data.as_slice();
301        let result = Capsule::decode(&mut buf);
302        assert!(matches!(result, Err(CapsuleError::InvalidUtf8)));
303    }
304
305    #[test]
306    fn test_truncated_error_code() {
307        // Capsule length indicates 3 bytes, but the error code needs 4.
308        let mut data = Vec::new();
309        VarInt::from_u64(0x2843).unwrap().encode(&mut data); // type
310        VarInt::from_u32(3).encode(&mut data); // length(3)
311        data.extend_from_slice(b"\x00\x00\x00"); // incomplete error code.
312
313        let mut buf = data.as_slice();
314        let result = Capsule::decode(&mut buf);
315        assert!(matches!(result, Err(CapsuleError::UnexpectedEnd)));
316    }
317
318    #[test]
319    fn test_unknown_capsule() {
320        // Verify handling of unknown capsule types.
321        let unknown_type = 0x1234u64;
322        let payload_data = b"unknown payload";
323
324        let mut data = Vec::new();
325        VarInt::from_u64(unknown_type).unwrap().encode(&mut data);
326        VarInt::from_u32(payload_data.len() as u32).encode(&mut data);
327        data.extend_from_slice(payload_data);
328
329        let mut buf = data.as_slice();
330        let capsule = Capsule::decode(&mut buf).unwrap();
331
332        match capsule {
333            Capsule::Unknown { typ, payload } => {
334                assert_eq!(typ.into_inner(), unknown_type);
335                assert_eq!(payload.as_ref(), payload_data);
336            }
337            _ => panic!("Expected Unknown capsule"),
338        }
339    }
340
341    #[test]
342    fn test_unknown_capsule_roundtrip() {
343        let capsule = Capsule::Unknown {
344            typ: VarInt::from_u64(0x9999).unwrap(),
345            payload: Bytes::from("test payload"),
346        };
347
348        let mut buf = Vec::new();
349        capsule.encode(&mut buf).unwrap();
350
351        let mut read_buf = buf.as_slice();
352        let decoded = Capsule::decode(&mut read_buf).unwrap();
353
354        assert_eq!(capsule, decoded);
355        assert_eq!(read_buf.len(), 0);
356    }
357
358    #[test]
359    fn test_oversized_close_reason_is_rejected_when_encoding() {
360        let capsule = Capsule::CloseWebTransportSession {
361            code: 0,
362            reason: "x".repeat(MAX_MESSAGE_SIZE + 1),
363        };
364
365        assert!(matches!(
366            capsule.encode(&mut Vec::new()),
367            Err(CapsuleError::MessageTooLong)
368        ));
369    }
370}