返回 CodeWhale
computer_display_tests.rs
根目录 / crates / tui / src / runtime_api / computer_display_tests.rs
1 //! Tests for `/v1/computer/*` (ARCHITECTURE §6 S2 acceptance).
2 //!
3 //! The fake Xvnc below speaks the server side of RFB 3.8 on a Unix socket
4 //! and records every client byte it receives after `ClientInit`. "Watcher
5 //! input produces 0 X events" is asserted as "Xvnc received 0 input bytes":
6 //! input that never reaches the X server cannot become an X event.
7
8 use super::*;
9
10 #[test]
11 fn parser_forwards_allowed_messages_split_across_frames() {
12 let mut parser = ClientParser::default();
13 let mut out = Vec::new();
14 // FramebufferUpdateRequest (10 bytes) split 3 + 7.
15 let fur = [3u8, 1, 0, 0, 0, 0, 5, 160, 3, 132];
16 parser.feed(&fur[..3], false, &mut out).unwrap();
17 assert!(out.is_empty());
18 parser.feed(&fur[3..], false, &mut out).unwrap();
19 assert_eq!(out, fur);
20 }
21
22 #[test]
23 fn parser_drops_input_without_lease_and_forwards_with_it() {
24 let key = [4u8, 1, 0, 0, 0, 0, 0, 0x61];
25 let pointer = [5u8, 1, 0, 10, 0, 20];
26 let cut = [6u8, 0, 0, 0, 0, 0, 0, 2, b'h', b'i'];
27 let mut resize = vec![251u8, 0, 5, 0, 3, 32, 1, 0];
28 resize.extend_from_slice(&[0u8; 16]);
29 let mut all = Vec::new();
30 for m in [&key[..], &pointer[..], &cut[..], &resize[..]] {
31 all.extend_from_slice(m);
32 }
33
34 let mut out = Vec::new();
35 let stats = ClientParser::default().feed(&all, false, &mut out).unwrap();
36 assert!(out.is_empty(), "watcher input must not be forwarded");
37 assert_eq!(stats.input_dropped, 4);
38
39 let mut out = Vec::new();
40 let stats = ClientParser::default().feed(&all, true, &mut out).unwrap();
41 assert_eq!(out, all);
42 assert_eq!(stats.input_forwarded, 4);
43 }
44
45 #[test]
46 fn parser_closes_on_unknown_type_and_oversize() {
47 let mut out = Vec::new();
48 assert_eq!(
49 ClientParser::default().feed(&[248, 0, 0, 0], true, &mut out),
50 Err(ParseError::UnknownType(248))
51 );
52 // A negative (extended-clipboard) or huge cut length is refused.
53 assert!(matches!(
54 ClientParser::default().feed(&[6, 0, 0, 0, 0xff, 0xff, 0xff, 0xfc], true, &mut out),
55 Err(ParseError::TooLarge {
56 message_type: 6,
57 ..
58 })
59 ));
60 // Unknown type after a valid message still closes.
61 let mut parser = ClientParser::default();
62 let mut bytes = vec![3u8, 1, 0, 0, 0, 0, 0, 1, 0, 1];
63 bytes.push(0xff);
64 assert_eq!(
65 parser.feed(&bytes, true, &mut out),
66 Err(ParseError::UnknownType(0xff))
67 );
68 }
69
70 #[test]
71 fn parser_strips_encodings_that_start_unparsed_subprotocols() {
72 let encodings: [i32; 5] = [16, -312, 7, -258, -239];
73 let mut msg = vec![2u8, 0];
74 msg.extend_from_slice(&(encodings.len() as u16).to_be_bytes());
75 for e in encodings {
76 msg.extend_from_slice(&e.to_be_bytes());
77 }
78 let mut out = Vec::new();
79 ClientParser::default().feed(&msg, false, &mut out).unwrap();
80 let mut expected = vec![2u8, 0, 0, 3];
81 for e in [16i32, 7, -239] {
82 expected.extend_from_slice(&e.to_be_bytes());
83 }
84 assert_eq!(out, expected);
85 }
86
87 #[test]
88 fn display_socket_path_must_be_absolute_and_plain() {
89 assert_eq!(
90 validated_socket_path(" /run/cw/vnc.sock "),
91 Some(PathBuf::from("/run/cw/vnc.sock"))
92 );
93 assert_eq!(validated_socket_path(""), None);
94 assert_eq!(validated_socket_path("vnc.sock"), None);
95 assert_eq!(validated_socket_path("./vnc.sock"), None);
96 assert_eq!(validated_socket_path("/run/cw/../../etc/passwd"), None);
97 assert_eq!(validated_socket_path("/run/./cw/vnc.sock"), None);
98 assert_eq!(validated_socket_path("/run/cw/vnc\0.sock"), None);
99 assert_eq!(validated_socket_path("/"), None);
100 }
101
102 #[test]
103 fn redaction_hides_ticket_and_token_values() {
104 let redacted = redact_query_secrets(
105 "/v1/computer/display?ticket=cwdt_secret&mode=view&mobile_stream_ticket=abc&Token=x#frag",
106 );
107 assert_eq!(
108 redacted,
109 "/v1/computer/display?ticket=redacted&mode=view&mobile_stream_ticket=redacted&Token=redacted#redacted"
110 );
111 assert!(!redacted.contains("cwdt_secret"));
112 assert_eq!(redact_query_secrets("/v1/computer"), "/v1/computer");
113 }
114
115 #[tokio::test]
116 async fn parser_panic_is_contained_to_its_task() {
117 // Stand-in for an active turn running on the same runtime.
118 let turn = tokio::spawn(async {
119 tokio::time::sleep(Duration::from_millis(50)).await;
120 "turn finished"
121 });
122 let parser = tokio::spawn(async {
123 if std::hint::black_box(true) {
124 panic!("parser bug");
125 }
126 ExitReason::ClientClosed
127 });
128 assert_eq!(parser_exit(parser.await), ExitReason::ParserPanic);
129 assert_eq!(turn.await.unwrap(), "turn finished");
130 }
131
132 #[cfg(unix)]
133 mod live {
134 use super::*;
135 use tokio::net::UnixListener;
136 use tokio_tungstenite::tungstenite::client::IntoClientRequest;
137 use tokio_tungstenite::tungstenite::protocol::Message as TMessage;
138
139 const MASTER: &str = "master-token-for-tests";
140
141 struct Harness {
142 base: String,
143 ws_base: String,
144 computer: ComputerState,
145 received: Arc<parking_lot::Mutex<Vec<u8>>>,
146 _dir: tempfile::TempDir,
147 }
148
149 async fn fake_xvnc(listener: UnixListener, received: Arc<parking_lot::Mutex<Vec<u8>>>) {
150 loop {
151 let Ok((mut s, _)) = listener.accept().await else {
152 return;
153 };
154 let received = received.clone();
155 tokio::spawn(async move {
156 s.write_all(RFB_VERSION_38).await.unwrap();
157 let mut v = [0u8; 12];
158 s.read_exact(&mut v).await.unwrap();
159 s.write_all(&[1, 1]).await.unwrap();
160 let mut one = [0u8; 1];
161 s.read_exact(&mut one).await.unwrap();
162 s.write_all(&[0, 0, 0, 0]).await.unwrap();
163 s.read_exact(&mut one).await.unwrap(); // ClientInit
164 let mut init = Vec::new();
165 init.extend_from_slice(&1440u16.to_be_bytes());
166 init.extend_from_slice(&900u16.to_be_bytes());
167 init.extend_from_slice(&[32, 24, 0, 1, 0, 255, 0, 255, 0, 255, 16, 8, 0, 0, 0, 0]);
168 init.extend_from_slice(&4u32.to_be_bytes());
169 init.extend_from_slice(b"twin");
170 s.write_all(&init).await.unwrap();
171 let mut buf = [0u8; 4096];
172 loop {
173 match s.read(&mut buf).await {
174 Ok(0) | Err(_) => return,
175 Ok(n) => received.lock().extend_from_slice(&buf[..n]),
176 }
177 }
178 });
179 }
180 }
181
182 async fn harness() -> Harness {
183 // The workspace builds reqwest with `rustls-no-provider`; the binary
184 // installs ring at startup, so tests that build a Client must too.
185 let _ = rustls::crypto::ring::default_provider().install_default();
186 let dir = tempfile::tempdir().unwrap();
187 let sock = dir.path().join("vnc.sock");
188 let listener = UnixListener::bind(&sock).unwrap();
189 let received = Arc::new(parking_lot::Mutex::new(Vec::new()));
190 tokio::spawn(fake_xvnc(listener, received.clone()));
191 let computer = ComputerState::new(sock, DEFAULT_IDLE);
192 let app: Router = router(computer.clone(), Some(MASTER.to_string()));
193 let tcp = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
194 let addr = tcp.local_addr().unwrap();
195 tokio::spawn(async move { axum::serve(tcp, app).await.unwrap() });
196 Harness {
197 base: format!("http://{addr}"),
198 ws_base: format!("ws://{addr}"),
199 computer,
200 received,
201 _dir: dir,
202 }
203 }
204
205 type Ws = tokio_tungstenite::WebSocketStream<
206 tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>,
207 >;
208
209 async fn connect(h: &Harness, bearer: Option<&str>, query: &str) -> Result<Ws, u16> {
210 let mut req = format!("{}/v1/computer/display{query}", h.ws_base)
211 .into_client_request()
212 .unwrap();
213 if let Some(token) = bearer {
214 req.headers_mut()
215 .insert("authorization", format!("Bearer {token}").parse().unwrap());
216 }
217 match tokio_tungstenite::connect_async(req).await {
218 Ok((ws, _)) => Ok(ws),
219 Err(tokio_tungstenite::tungstenite::Error::Http(resp)) => Err(resp.status().as_u16()),
220 Err(other) => panic!("unexpected connect error: {other}"),
221 }
222 }
223
224 struct RfbClient {
225 ws: Ws,
226 buf: Vec<u8>,
227 }
228
229 impl RfbClient {
230 async fn read(&mut self, n: usize) -> Vec<u8> {
231 while self.buf.len() < n {
232 match tokio::time::timeout(Duration::from_secs(5), self.ws.next())
233 .await
234 .expect("frame within 5 s")
235 {
236 Some(Ok(TMessage::Binary(b))) => self.buf.extend_from_slice(&b),
237 other => panic!("expected binary frame, got {other:?}"),
238 }
239 }
240 let rest = self.buf.split_off(n);
241 std::mem::replace(&mut self.buf, rest)
242 }
243
244 async fn send(&mut self, bytes: &[u8]) {
245 self.ws
246 .send(TMessage::Binary(bytes.to_vec().into()))
247 .await
248 .unwrap();
249 }
250
251 /// Handshake and return the ServerInit width/height.
252 async fn handshake(ws: Ws) -> (Self, u16, u16) {
253 let mut c = RfbClient {
254 ws,
255 buf: Vec::new(),
256 };
257 assert_eq!(c.read(12).await, RFB_VERSION_38);
258 c.send(RFB_VERSION_38).await;
259 assert_eq!(c.read(2).await, [1, 1], "only security None offered");
260 c.send(&[1]).await;
261 assert_eq!(c.read(4).await, [0, 0, 0, 0]);
262 c.send(&[0]).await; // ClientInit
263 let init = c.read(24).await;
264 let name_len = u32::from_be_bytes([init[20], init[21], init[22], init[23]]) as usize;
265 assert_eq!(c.read(name_len).await, b"twin");
266 let w = u16::from_be_bytes([init[0], init[1]]);
267 let h = u16::from_be_bytes([init[2], init[3]]);
268 (c, w, h)
269 }
270 }
271
272 async fn wait_for_received(h: &Harness, len: usize) -> Vec<u8> {
273 for _ in 0..100 {
274 if h.received.lock().len() >= len {
275 break;
276 }
277 tokio::time::sleep(Duration::from_millis(20)).await;
278 }
279 h.received.lock().clone()
280 }
281
282 fn event_kinds(h: &Harness) -> Vec<(String, Value)> {
283 h.computer
284 .events_since(0)
285 .0
286 .into_iter()
287 .map(|e| (e.kind, e.data))
288 .collect()
289 }
290
291 const FUR: [u8; 10] = [3, 1, 0, 0, 0, 0, 5, 160, 3, 132];
292 const KEY: [u8; 8] = [4, 1, 0, 0, 0, 0, 0, 0x61];
293 const POINTER: [u8; 6] = [5, 1, 0, 10, 0, 20];
294
295 #[tokio::test]
296 async fn display_requires_a_token_or_a_single_use_ticket() {
297 let h = harness().await;
298 assert_eq!(connect(&h, None, "").await.err(), Some(401));
299 assert_eq!(connect(&h, Some("wrong"), "").await.err(), Some(401));
300 assert_eq!(
301 connect(&h, None, "?ticket=cwdt_forged").await.err(),
302 Some(401)
303 );
304
305 let http = codewhale_release::tls::reqwest_client();
306 let resp = http
307 .post(format!("{}/v1/computer/display/tickets", h.base))
308 .bearer_auth(MASTER)
309 .send()
310 .await
311 .unwrap();
312 assert_eq!(resp.status().as_u16(), 201);
313 let body: Value = resp.json().await.unwrap();
314 let ticket = body["ticket"].as_str().unwrap().to_string();
315
316 let ws = connect(&h, None, &format!("?ticket={ticket}"))
317 .await
318 .expect("ticket works once");
319 let (_client, w, hgt) = RfbClient::handshake(ws).await;
320 assert_eq!((w, hgt), (1440, 900), "valid ticket reaches ServerInit");
321 assert_eq!(
322 connect(&h, None, &format!("?ticket={ticket}")).await.err(),
323 Some(401),
324 "reused ticket is refused"
325 );
326 }
327
328 #[tokio::test]
329 async fn watcher_input_never_reaches_xvnc_and_lease_holder_input_does() {
330 let h = harness().await;
331 let ws = connect(&h, Some(MASTER), "").await.unwrap();
332 let (mut c, _, _) = RfbClient::handshake(ws).await;
333
334 // Watching: key + pointer are dropped, the update request passes.
335 let mut burst = Vec::new();
336 burst.extend_from_slice(&KEY);
337 burst.extend_from_slice(&POINTER);
338 burst.extend_from_slice(&FUR);
339 c.send(&burst).await;
340 let got = wait_for_received(&h, FUR.len()).await;
341 tokio::time::sleep(Duration::from_millis(100)).await;
342 assert_eq!(got, FUR, "watcher input produced bytes at Xvnc");
343 assert_eq!(h.received.lock().len(), FUR.len());
344
345 // Driving: after acquiring the lease the same input is forwarded.
346 let resp = codewhale_release::tls::reqwest_client()
347 .post(format!("{}/v1/computer/control/acquire", h.base))
348 .bearer_auth(MASTER)
349 .send()
350 .await
351 .unwrap();
352 assert_eq!(resp.status().as_u16(), 200);
353 c.send(&KEY).await;
354 let got = wait_for_received(&h, FUR.len() + KEY.len()).await;
355 assert_eq!(&got[FUR.len()..], KEY);
356
357 let released = codewhale_release::tls::reqwest_client()
358 .post(format!("{}/v1/computer/control/release", h.base))
359 .bearer_auth(MASTER)
360 .send()
361 .await
362 .unwrap();
363 assert_eq!(released.status().as_u16(), 200);
364 let kinds: Vec<String> = event_kinds(&h).into_iter().map(|(k, _)| k).collect();
365 assert!(kinds.contains(&"computer.display.attached".to_string()));
366 assert!(kinds.contains(&"computer.control.acquired".to_string()));
367 assert!(kinds.contains(&"computer.control.released".to_string()));
368 // Events carry counts, never key values.
369 let released = event_kinds(&h)
370 .into_iter()
371 .find(|(k, _)| k == "computer.control.released")
372 .unwrap()
373 .1;
374 assert_eq!(released["input_events"], 1);
375 }
376
377 #[tokio::test]
378 async fn unknown_client_message_closes_the_stream_with_an_event() {
379 let h = harness().await;
380 let ws = connect(&h, Some(MASTER), "").await.unwrap();
381 let (mut c, _, _) = RfbClient::handshake(ws).await;
382 c.send(&[200, 0, 0, 0]).await;
383 let close = loop {
384 match tokio::time::timeout(Duration::from_secs(5), c.ws.next())
385 .await
386 .expect("close within 5 s")
387 {
388 Some(Ok(TMessage::Close(frame))) => break frame,
389 Some(Ok(_)) => continue,
390 other => panic!("expected close, got {other:?}"),
391 }
392 };
393 assert_eq!(u16::from(close.unwrap().code), 1008);
394 for _ in 0..50 {
395 if event_kinds(&h)
396 .iter()
397 .any(|(k, _)| k == "computer.display.detached")
398 {
399 break;
400 }
401 tokio::time::sleep(Duration::from_millis(20)).await;
402 }
403 let detached = event_kinds(&h)
404 .into_iter()
405 .find(|(k, _)| k == "computer.display.detached")
406 .expect("detached event")
407 .1;
408 assert!(
409 detached["reason"]
410 .as_str()
411 .unwrap()
412 .contains("unknown client message type 200")
413 );
414 assert!(h.received.lock().is_empty());
415 }
416
417 #[tokio::test]
418 async fn client_tokens_are_owner_minted_scoped_and_revocable() {
419 let h = harness().await;
420 let http = codewhale_release::tls::reqwest_client();
421 let resp = http
422 .post(format!("{}/v1/auth/client-tokens", h.base))
423 .bearer_auth(MASTER)
424 .json(&json!({ "device_id": "mac-1", "ttl_seconds": 999999 }))
425 .send()
426 .await
427 .unwrap();
428 assert_eq!(resp.status().as_u16(), 201);
429 let body: Value = resp.json().await.unwrap();
430 let token = body["token"].as_str().unwrap().to_string();
431 let id = body["id"].as_str().unwrap().to_string();
432 let expires: DateTime<Utc> = body["expires_at"].as_str().unwrap().parse().unwrap();
433 assert!(
434 expires <= Utc::now() + chrono::Duration::seconds(3601),
435 "ttl clamps to 1 h"
436 );
437
438 // The client token works on the computer surface...
439 let status = http
440 .get(format!("{}/v1/computer", h.base))
441 .bearer_auth(&token)
442 .send()
443 .await
444 .unwrap();
445 assert_eq!(status.status().as_u16(), 200);
446 // ...and on the display, whose lease is per device.
447 let ws = connect(&h, Some(&token), "").await.unwrap();
448 let (_c, _, _) = RfbClient::handshake(ws).await;
449 let lease: Value = http
450 .post(format!("{}/v1/computer/control/acquire", h.base))
451 .bearer_auth(&token)
452 .send()
453 .await
454 .unwrap()
455 .json()
456 .await
457 .unwrap();
458 assert_eq!(lease["lease"]["holder"], "device:mac-1");
459 // The owner is refused without force, and takes over with it.
460 let conflict = http
461 .post(format!("{}/v1/computer/control/acquire", h.base))
462 .bearer_auth(MASTER)
463 .send()
464 .await
465 .unwrap();
466 assert_eq!(conflict.status().as_u16(), 409);
467
468 // A client token cannot mint or list client tokens.
469 let forbidden = http
470 .post(format!("{}/v1/auth/client-tokens", h.base))
471 .bearer_auth(&token)
472 .json(&json!({ "device_id": "evil" }))
473 .send()
474 .await
475 .unwrap();
476 assert_eq!(forbidden.status().as_u16(), 403);
477 let bad_device = http
478 .post(format!("{}/v1/auth/client-tokens", h.base))
479 .bearer_auth(MASTER)
480 .json(&json!({ "device_id": "has space" }))
481 .send()
482 .await
483 .unwrap();
484 assert_eq!(bad_device.status().as_u16(), 400);
485
486 // Revoke: the token stops working and its lease expires.
487 let revoked = http
488 .delete(format!("{}/v1/auth/client-tokens/{id}", h.base))
489 .bearer_auth(MASTER)
490 .send()
491 .await
492 .unwrap();
493 assert_eq!(revoked.status().as_u16(), 204);
494 let after = http
495 .get(format!("{}/v1/computer", h.base))
496 .bearer_auth(&token)
497 .send()
498 .await
499 .unwrap();
500 assert_eq!(after.status().as_u16(), 401);
501 let status: Value = http
502 .get(format!("{}/v1/computer", h.base))
503 .bearer_auth(MASTER)
504 .send()
505 .await
506 .unwrap()
507 .json()
508 .await
509 .unwrap();
510 assert!(status["control"]["lease"].is_null());
511 assert!(
512 event_kinds(&h)
513 .iter()
514 .any(|(k, _)| k == "computer.control.expired")
515 );
516 }
517
518 #[tokio::test]
519 async fn missing_display_socket_is_503_not_a_hang() {
520 let dir = tempfile::tempdir().unwrap();
521 let computer = ComputerState::new(dir.path().join("absent.sock"), DEFAULT_IDLE);
522 let app: Router = router(computer, Some(MASTER.to_string()));
523 let tcp = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
524 let addr = tcp.local_addr().unwrap();
525 tokio::spawn(async move { axum::serve(tcp, app).await.unwrap() });
526 let mut req = format!("ws://{addr}/v1/computer/display")
527 .into_client_request()
528 .unwrap();
529 req.headers_mut()
530 .insert("authorization", format!("Bearer {MASTER}").parse().unwrap());
531 match tokio_tungstenite::connect_async(req).await {
532 Err(tokio_tungstenite::tungstenite::Error::Http(resp)) => {
533 assert_eq!(resp.status().as_u16(), 503)
534 }
535 other => panic!("expected 503, got {other:?}"),
536 }
537 }
538 }
539
539 lines RUST