1use gwk_domain::ids::Seq;
18use gwk_domain::protocol::{
19 CapabilityName, ClientControl, FrameKind, HELLO_DEADLINE_SECS, HELLO_MAX_BYTES,
20 KernelErrorCode, MAX_CAPABILITIES, PROTOCOL_MINOR, ProtocolVersion, ServerControl,
21};
22use tokio::io::{AsyncRead, AsyncWrite};
23
24use super::frame::{Budget, Incoming, read_frame, write_frame};
25use super::{WireError, strict};
26
27pub const OFFERED_CAPABILITIES: &[&str] = &[];
35
36#[derive(Debug, Clone, PartialEq, Eq)]
38pub struct Session {
39 pub minor: u32,
40 pub capabilities: Vec<CapabilityName>,
41 pub client: Option<String>,
42}
43
44#[derive(Debug, Clone, Copy)]
46pub struct Readiness {
47 pub sealed: bool,
48 pub watermark: Option<Seq>,
49}
50
51pub async fn negotiate<R, W>(
58 reader: &mut R,
59 writer: &mut W,
60 budget: &mut Budget,
61 readiness: Readiness,
62) -> Result<Session, WireError>
63where
64 R: AsyncRead + Unpin,
65 W: AsyncWrite + Unpin,
66{
67 let outcome = read_hello(reader, budget).await;
68 match outcome {
69 Ok(session) => {
70 let ack = ServerControl::HelloAck {
71 protocol_major: ProtocolVersion::V1,
72 protocol_minor: session.minor,
73 capabilities: session.capabilities.clone(),
74 sealed: readiness.sealed,
75 watermark: readiness.watermark,
76 };
77 send(writer, budget, &ack).await?;
78 Ok(session)
79 }
80 Err(refusal) => {
81 let told = ServerControl::HelloRefusal {
85 code: refusal.code,
86 message: refusal.message.clone(),
87 };
88 let _ = send(writer, budget, &told).await;
89 Err(refusal)
90 }
91 }
92}
93
94async fn read_hello<R>(reader: &mut R, budget: &mut Budget) -> Result<Session, WireError>
95where
96 R: AsyncRead + Unpin,
97{
98 let deadline = std::time::Duration::from_secs(HELLO_DEADLINE_SECS);
102 let incoming = tokio::time::timeout(deadline, read_frame(reader, HELLO_MAX_BYTES, budget))
103 .await
104 .map_err(|_| {
105 WireError::new(
106 KernelErrorCode::Handshake,
107 format!("no hello within {HELLO_DEADLINE_SECS}s"),
108 )
109 })??;
110
111 let frame = match incoming {
112 Incoming::Frame(frame) => frame,
113 Incoming::Closed => {
114 return Err(WireError::new(
115 KernelErrorCode::Handshake,
116 "the connection closed before its hello",
117 ));
118 }
119 };
120 if frame.kind != FrameKind::Json {
121 return Err(WireError::new(
122 KernelErrorCode::Handshake,
123 format!("the first frame must be JSON, not kind {:?}", frame.kind),
124 ));
125 }
126
127 let control: ClientControl = strict::decode(&frame.body)?;
128 let (protocol_major, protocol_minor, capabilities, client) = match control {
129 ClientControl::Hello {
130 protocol_major,
131 protocol_minor,
132 capabilities,
133 client,
134 } => (protocol_major, protocol_minor, capabilities, client),
135 ClientControl::Request { .. } => {
139 return Err(WireError::new(
140 KernelErrorCode::Handshake,
141 "the first frame must be a hello, not a request",
142 ));
143 }
144 };
145
146 if protocol_major != ProtocolVersion::V1 {
152 return Err(WireError::new(
153 KernelErrorCode::UnsupportedVersion,
154 format!(
155 "protocol major {protocol_major} is not {}",
156 ProtocolVersion::V1
157 ),
158 ));
159 }
160
161 if capabilities.len() > MAX_CAPABILITIES {
162 return Err(WireError::new(
163 KernelErrorCode::Capability,
164 format!(
165 "{} capabilities exceeds the {MAX_CAPABILITIES} maximum",
166 capabilities.len()
167 ),
168 ));
169 }
170
171 Ok(Session {
172 #[allow(clippy::unnecessary_min_or_max)]
181 minor: protocol_minor.min(PROTOCOL_MINOR),
182 capabilities: intersect(&capabilities),
183 client,
184 })
185}
186
187fn intersect(asked: &[CapabilityName]) -> Vec<CapabilityName> {
190 let mut out: Vec<CapabilityName> = Vec::new();
191 for name in asked {
192 if OFFERED_CAPABILITIES.contains(&name.as_str()) && !out.contains(name) {
193 out.push(name.clone());
194 }
195 }
196 out
197}
198
199async fn send<W>(
200 writer: &mut W,
201 budget: &mut Budget,
202 control: &ServerControl,
203) -> Result<(), WireError>
204where
205 W: AsyncWrite + Unpin,
206{
207 let body = serde_json::to_vec(control).map_err(|e| {
208 WireError::new(
209 KernelErrorCode::Storage,
210 format!("serialize server control: {e}"),
211 )
212 })?;
213 write_frame(writer, FrameKind::Json, &body, budget).await
214}
215
216#[cfg(test)]
217mod tests {
218 use gwk_domain::protocol::{
219 CONNECTION_EGRESS_BYTES_PER_WINDOW, CONNECTION_INGRESS_BYTES_PER_WINDOW,
220 FRAME_BODY_MAX_BYTES,
221 };
222
223 use super::*;
224
225 fn budget() -> Budget {
226 Budget::new(
227 CONNECTION_INGRESS_BYTES_PER_WINDOW,
228 CONNECTION_EGRESS_BYTES_PER_WINDOW,
229 )
230 }
231
232 fn ready() -> Readiness {
233 Readiness {
234 sealed: true,
235 watermark: Some(Seq::new(1)),
236 }
237 }
238
239 async fn handshake(raw: &str) -> (Result<Session, WireError>, ServerControl) {
242 let mut client_bytes = Vec::new();
243 write_frame(
244 &mut client_bytes,
245 FrameKind::Json,
246 raw.as_bytes(),
247 &mut budget(),
248 )
249 .await
250 .expect("frame the hello");
251
252 let mut reader = std::io::Cursor::new(client_bytes);
253 let mut written = Vec::new();
254 let mut b = budget();
255 let outcome = negotiate(&mut reader, &mut written, &mut b, ready()).await;
256
257 let mut back = std::io::Cursor::new(written);
258 let answer = match read_frame(&mut back, FRAME_BODY_MAX_BYTES, &mut budget())
259 .await
260 .expect("the kernel answered")
261 {
262 Incoming::Frame(frame) => {
263 strict::decode::<ServerControl>(&frame.body).expect("decode the answer")
264 }
265 Incoming::Closed => panic!("the kernel said nothing"),
266 };
267 (outcome, answer)
268 }
269
270 #[tokio::test]
271 async fn a_matching_hello_is_acked_with_the_kernels_state() {
272 let (session, answer) = handshake(
273 r#"{"type":"hello","protocol_major":1,"protocol_minor":0,"capabilities":[],"client":"gw/0.0.1"}"#,
274 )
275 .await;
276 let session = session.expect("negotiate");
277 assert_eq!(session.minor, 0);
278 assert_eq!(session.client.as_deref(), Some("gw/0.0.1"));
279 match answer {
280 ServerControl::HelloAck {
281 protocol_major,
282 protocol_minor,
283 capabilities,
284 sealed,
285 watermark,
286 } => {
287 assert_eq!(protocol_major, ProtocolVersion::V1);
288 assert_eq!(protocol_minor, 0);
289 assert!(capabilities.is_empty());
290 assert!(sealed);
293 assert_eq!(watermark, Some(Seq::new(1)));
294 }
295 other => panic!("{other:?}"),
296 }
297 }
298
299 #[tokio::test]
300 async fn an_unknown_major_is_refused_in_words_the_client_can_read() {
301 let (outcome, answer) = handshake(
304 r#"{"type":"hello","protocol_major":2,"protocol_minor":0,"capabilities":[]}"#,
305 )
306 .await;
307 let error = outcome.expect_err("major 2 accepted");
308 assert!(matches!(
309 error.code,
310 KernelErrorCode::UnsupportedVersion | KernelErrorCode::Validation
311 ));
312 match answer {
313 ServerControl::HelloRefusal { message, .. } => {
314 assert!(message.contains('2'), "{message}");
315 }
316 other => panic!("{other:?}"),
317 }
318 }
319
320 #[tokio::test]
321 async fn a_newer_minor_negotiates_down_instead_of_failing() {
322 let (session, answer) = handshake(
323 r#"{"type":"hello","protocol_major":1,"protocol_minor":99,"capabilities":[]}"#,
324 )
325 .await;
326 assert_eq!(session.expect("negotiate").minor, PROTOCOL_MINOR);
327 assert!(matches!(
328 answer,
329 ServerControl::HelloAck { protocol_minor, .. } if protocol_minor == PROTOCOL_MINOR
330 ));
331 }
332
333 #[tokio::test]
334 async fn capabilities_intersect_and_this_kernel_offers_none() {
335 let (session, _) = handshake(
339 r#"{"type":"hello","protocol_major":1,"protocol_minor":0,"capabilities":["event_subscribe","blob_transfer"]}"#,
340 )
341 .await;
342 assert!(session.expect("negotiate").capabilities.is_empty());
343 }
344
345 #[tokio::test]
346 async fn too_many_capabilities_is_a_typed_refusal() {
347 let names: Vec<String> = (0..=MAX_CAPABILITIES)
348 .map(|i| format!("\"cap_{i}\""))
349 .collect();
350 let (outcome, _) = handshake(&format!(
351 r#"{{"type":"hello","protocol_major":1,"protocol_minor":0,"capabilities":[{}]}}"#,
352 names.join(",")
353 ))
354 .await;
355 let error = outcome.expect_err("65 capabilities accepted");
356 assert_eq!(error.code, KernelErrorCode::Capability);
357 }
358
359 #[tokio::test]
360 async fn a_request_before_a_hello_is_refused() {
361 let (outcome, _) =
362 handshake(r#"{"type":"request","request_id":"r-1","request":{"type":"health"}}"#).await;
363 let error = outcome.expect_err("a request served before the handshake");
364 assert_eq!(error.code, KernelErrorCode::Handshake);
365 }
366
367 #[tokio::test]
368 async fn silence_is_closed_on_the_deadline() {
369 tokio::time::pause();
372 let (client, mut server) = tokio::io::duplex(64);
373 let mut b = budget();
374 let mut written = Vec::new();
375 let handshaking = tokio::spawn(async move {
376 negotiate(&mut server, &mut written, &mut b, ready())
377 .await
378 .map(|_| ())
379 });
380 tokio::time::advance(std::time::Duration::from_secs(HELLO_DEADLINE_SECS + 1)).await;
381 let error = handshaking
382 .await
383 .expect("join")
384 .expect_err("silence accepted");
385 assert_eq!(error.code, KernelErrorCode::Handshake);
386 assert!(error.message.contains("hello"), "{error}");
387 drop(client);
388 }
389
390 #[tokio::test]
391 async fn an_oversized_hello_is_refused_at_its_own_lower_cap() {
392 let mut raw = (HELLO_MAX_BYTES + 1).to_be_bytes().to_vec();
395 raw.push(FrameKind::Json.as_u8());
396 let mut reader = std::io::Cursor::new(raw);
397 let mut written = Vec::new();
398 let error = negotiate(&mut reader, &mut written, &mut budget(), ready())
399 .await
400 .expect_err("oversized hello accepted");
401 assert_eq!(error.code, KernelErrorCode::FrameSize);
402 }
403}