moqtap_client/draft10/session/setup.rs
1use moqtap_codec::draft10::message::{ClientSetup, ServerSetup};
2use moqtap_codec::kvp::{KeyValuePair, KvpValue};
3use moqtap_codec::varint::VarInt;
4
5/// The PATH setup parameter. It is the one setup parameter whose sender this
6/// draft restricts, and the restriction runs one way: PATH is the client's.
7const PATH: u64 = 0x01;
8
9/// The version number this module speaks, and the only one it can.
10///
11/// Version numbers for IETF drafts are the draft number added to 0xff000000.
12const DRAFT_VERSION: u64 = 0xff00000a;
13
14/// Errors from setup message validation or version negotiation.
15#[derive(Debug, thiserror::Error, PartialEq, Eq)]
16pub enum SetupError {
17 /// Client and server share no supported protocol version.
18 #[error("no common version between client and server")]
19 NoCommonVersion,
20 /// A required setup parameter is missing.
21 #[error("missing required parameter: {0}")]
22 MissingParameter(
23 /// Name of the missing parameter.
24 &'static str,
25 ),
26 /// A setup parameter was sent by the endpoint that may not send it.
27 #[error("setup parameter {0:#x} may not be sent by this endpoint")]
28 WrongParameterRole(
29 /// Key of the offending parameter.
30 u64,
31 ),
32 /// A PATH parameter was offered on a session that is not native QUIC.
33 #[error("PATH may not be used when WebTransport is used")]
34 PathOverWebTransport,
35 /// The supported versions list is empty.
36 #[error("no versions offered")]
37 EmptyVersionList,
38 /// The selected version is not the one this module implements.
39 #[error("selected version {0:#x} is not this draft's")]
40 WrongDraftVersion(
41 /// The version the server selected.
42 u64,
43 ),
44}
45
46/// Read a varint out of a setup parameter value, whichever shape it arrived in.
47///
48/// This draft frames every setup parameter as {Type, Length, Value}, and the
49/// decoder has no per-key table telling it which values are numbers, so a
50/// parameter that came off the wire always arrives as bytes. A caller that
51/// matches on [`KvpValue::Varint`] alone therefore never sees anything a peer
52/// sent - only something this process built itself. Both shapes are read here,
53/// and a value with bytes left over after the varint is refused rather than
54/// truncated.
55pub fn setup_varint(value: &KvpValue) -> Option<u64> {
56 match value {
57 KvpValue::Varint(v) => Some(v.into_inner()),
58 KvpValue::Bytes(bytes) => {
59 let mut cursor = &bytes[..];
60 let parsed = VarInt::decode(&mut cursor).ok()?;
61 cursor.is_empty().then(|| parsed.into_inner())
62 }
63 }
64}
65
66/// Validate a CLIENT_SETUP message.
67///
68/// Section 8.2.2.2 puts no role restriction on MAX_SUBSCRIBE_ID: it
69/// "communicates an initial value for the Maximum Subscribe ID to the receiving
70/// subscriber", which is something either endpoint may do, and a client that
71/// grants the server a subscribe budget during setup is doing exactly that.
72/// PATH is the restricted parameter, and it belongs to the client, so a
73/// CLIENT_SETUP is where it is legal.
74///
75/// # Errors
76///
77/// [`SetupError::EmptyVersionList`] if no version is offered.
78pub fn validate_client_setup(msg: &ClientSetup) -> Result<(), SetupError> {
79 if msg.supported_versions.is_empty() {
80 return Err(SetupError::EmptyVersionList);
81 }
82 Ok(())
83}
84
85/// Validate a SERVER_SETUP message.
86///
87/// Section 8.2.2.1 on PATH: "It MUST NOT be used by the server, or when
88/// WebTransport is used. If the peer receives a PATH parameter from the server,
89/// or when WebTransport is used, it MUST close the connection." Nothing else in
90/// a SERVER_SETUP is restricted by sender.
91///
92/// # Errors
93///
94/// [`SetupError::WrongParameterRole`] if the server sent a PATH.
95pub fn validate_server_setup(msg: &ServerSetup) -> Result<(), SetupError> {
96 if has_path(&msg.parameters) {
97 return Err(SetupError::WrongParameterRole(PATH));
98 }
99 Ok(())
100}
101
102/// Refuse a PATH parameter on a session that is not native QUIC.
103///
104/// The same sentence in Section 8.2.2.1 forbids PATH "when WebTransport is
105/// used" and closes the connection on one seen there. Which transport carries
106/// the session is known to the connection and not to the endpoint, so this is a
107/// separate call rather than part of [`validate_client_setup`].
108///
109/// # Errors
110///
111/// [`SetupError::PathOverWebTransport`] if a PATH is offered over WebTransport.
112pub fn validate_client_path_transport(
113 parameters: &[KeyValuePair],
114 over_webtransport: bool,
115) -> Result<(), SetupError> {
116 if over_webtransport && has_path(parameters) {
117 return Err(SetupError::PathOverWebTransport);
118 }
119 Ok(())
120}
121
122fn has_path(parameters: &[KeyValuePair]) -> bool {
123 let path = VarInt::from_u64(PATH).unwrap();
124 parameters.iter().any(|p| p.key == path)
125}
126
127/// Negotiate a version from the client's offered list and the server's selected
128/// version.
129///
130/// A client may offer other drafts' versions beside this one, but this module
131/// encodes exactly one draft: a session that settled on another version would
132/// have every frame after the handshake written in the wrong format, and the
133/// first symptom would be the peer closing it. So a selected version that is
134/// not this draft's is refused here rather than carried.
135///
136/// # Errors
137///
138/// [`SetupError::WrongDraftVersion`] if the server selected a version this
139/// module does not implement, and [`SetupError::NoCommonVersion`] if it
140/// selected one the client did not offer.
141pub fn negotiate_version(
142 client_versions: &[VarInt],
143 server_version: VarInt,
144) -> Result<VarInt, SetupError> {
145 if server_version.into_inner() != DRAFT_VERSION {
146 return Err(SetupError::WrongDraftVersion(server_version.into_inner()));
147 }
148 if client_versions.contains(&server_version) {
149 Ok(server_version)
150 } else {
151 Err(SetupError::NoCommonVersion)
152 }
153}