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