1pub mod actions;
3
4pub use actions::VncClientProtocol;
5
6use anyhow::{anyhow, bail, Context, Result};
7use std::net::SocketAddr;
8use std::sync::Arc;
9use tokio::io::{AsyncReadExt, AsyncWriteExt};
10use tokio::net::TcpStream;
11use tokio::sync::{mpsc, Mutex};
12use tracing::{debug, error, info, trace, warn};
13
14use crate::client::llm_budget::call_llm_for_client;
15use crate::client::vnc::actions::{
16 VNC_CLIENT_CONNECTED_EVENT, VNC_CLIENT_FRAMEBUFFER_UPDATE_EVENT,
17 VNC_CLIENT_SERVER_CUT_TEXT_EVENT,
18};
19use crate::llm::actions::client_trait::ClientActionResult;
20use crate::llm::ollama_client::OllamaClient;
21use crate::llm::ClientLlmResult;
22use crate::protocol::{Event, StartupParams};
23use crate::state::app_state::AppState;
24use crate::state::{ClientId, ClientStatus};
25use serde_json::Value as JsonValue;
26
27pub const MAX_TEXT_BYTES: usize = 1024 * 1024;
29
30pub const MAX_RAW_RECTANGLE_BYTES: u64 = 256 * 1024 * 1024;
32
33pub async fn read_server_text<R: AsyncReadExt + Unpin>(
35 reader: &mut R,
36 length: u32,
37) -> Result<Vec<u8>> {
38 if u64::from(length) > MAX_TEXT_BYTES as u64 {
39 bail!("VNC text length {length} exceeds the {MAX_TEXT_BYTES}-byte cap");
40 }
41 let mut text = vec![0; length as usize];
42 reader.read_exact(&mut text).await?;
43 Ok(text)
44}
45
46pub async fn discard_raw_rectangle<R: AsyncReadExt + Unpin>(
49 reader: &mut R,
50 width: u16,
51 height: u16,
52) -> Result<()> {
53 let length = u64::from(width) * u64::from(height) * 4;
54 if length > MAX_RAW_RECTANGLE_BYTES {
55 bail!("VNC raw rectangle is {length} bytes, over the {MAX_RAW_RECTANGLE_BYTES}-byte cap");
56 }
57 let consumed = tokio::io::copy(&mut reader.take(length), &mut tokio::io::sink()).await?;
58 if consumed != length {
59 bail!("truncated VNC raw rectangle: expected {length} bytes, received {consumed}");
60 }
61 Ok(())
62}
63
64pub fn resolve_keysym(value: &serde_json::Value) -> Option<u32> {
76 if let Some(n) = value.as_u64() {
77 return u32::try_from(n).ok();
78 }
79 let name = value.as_str()?;
80
81 let mut chars = name.chars();
83 if let (Some(c), None) = (chars.next(), chars.next()) {
84 if c.is_ascii_graphic() || c == ' ' {
85 return Some(c as u32);
86 }
87 }
88
89 Some(match name.to_ascii_lowercase().as_str() {
90 "enter" | "return" => 0xff0d,
91 "backspace" => 0xff08,
92 "tab" => 0xff09,
93 "escape" | "esc" => 0xff1b,
94 "space" => 0x0020,
95 "delete" | "del" => 0xffff,
96 "home" => 0xff50,
97 "left" => 0xff51,
98 "up" => 0xff52,
99 "right" => 0xff53,
100 "down" => 0xff54,
101 "pageup" => 0xff55,
102 "pagedown" => 0xff56,
103 "end" => 0xff57,
104 "insert" => 0xff63,
105 "shift" => 0xffe1,
106 "control" | "ctrl" => 0xffe3,
107 "alt" => 0xffe9,
108 "meta" | "super" | "cmd" => 0xffeb,
109 other => {
110 let n: u32 = other.strip_prefix('f')?.parse().ok()?;
112 if (1..=35).contains(&n) {
113 0xffbe + (n - 1)
114 } else {
115 return None;
116 }
117 }
118 })
119}
120
121#[derive(Debug, Clone, PartialEq)]
123enum ConnectionState {
124 Idle,
125 Processing,
126 #[allow(dead_code)]
127 Accumulating,
128}
129
130struct ClientData {
132 state: ConnectionState,
133 memory: String,
134 fb_width: u16,
135 fb_height: u16,
136}
137
138pub fn vnc_auth_response(password: &[u8], mut challenge: [u8; 16]) -> [u8; 16] {
142 use des::cipher::{Block, BlockEncrypt, KeyInit};
143 let mut key = [0u8; 8];
144 for (target, source) in key.iter_mut().zip(password.iter()) {
145 *target = source.reverse_bits();
146 }
147 let cipher = des::Des::new_from_slice(&key).expect("DES uses an eight-byte key");
148 for block in challenge.chunks_exact_mut(8) {
149 let mut encrypted = Block::<des::Des>::default();
150 encrypted.copy_from_slice(block);
151 cipher.encrypt_block(&mut encrypted);
152 block.copy_from_slice(&encrypted);
153 }
154 challenge
155}
156
157pub struct VncClient;
158
159impl VncClient {
160 pub async fn connect_with_llm_actions(
162 remote_addr: String,
163 llm_client: OllamaClient,
164 app_state: Arc<AppState>,
165 status_tx: mpsc::UnboundedSender<String>,
166 client_id: ClientId,
167 startup_params: Option<StartupParams>,
168 ) -> Result<SocketAddr> {
169 let mut stream = TcpStream::connect(&remote_addr)
171 .await
172 .context(format!("Failed to connect to {}", remote_addr))?;
173
174 let local_addr = stream.local_addr()?;
175 let remote_sock_addr = stream.peer_addr()?;
176
177 info!(
178 "VNC client {} connecting to {} (local: {})",
179 client_id, remote_sock_addr, local_addr
180 );
181
182 let password = startup_params
184 .as_ref()
185 .map(|p| p.get_optional_string("password"))
186 .transpose()?
187 .flatten();
188
189 let (fb_width, fb_height, server_name) = tokio::time::timeout(
191 crate::client::response_reader::RESPONSE_DEADLINE,
192 Self::perform_handshake(&mut stream, password.as_deref()),
193 )
194 .await
195 .context("VNC handshake deadline exceeded")??;
196
197 info!(
198 "VNC client {} connected: {}x{} ({})",
199 client_id, fb_width, fb_height, server_name
200 );
201
202 app_state
204 .update_client_status(client_id, ClientStatus::Connected)
205 .await;
206 let _ = status_tx.send(format!("[CLIENT] VNC client {} connected", client_id));
207 let _ = status_tx.send("__UPDATE_UI__".to_string());
208
209 let protocol = Arc::new(VncClientProtocol::new());
210
211 let (read_half, write_half) = tokio::io::split(stream);
214 let mut read_half = crate::client::response_reader::FrameReader::new(read_half);
215 let write_half_arc = Arc::new(Mutex::new(write_half));
216
217 let command_rx =
223 crate::client::command_support::register_command_channel(&app_state, client_id).await;
224 let cmd_protocol = protocol.clone();
225 let cmd_write = write_half_arc.clone();
226 let cmd_state = app_state.clone();
227 let cmd_tx = status_tx.clone();
228 let cmd_task = tokio::spawn(async move {
229 Self::command_loop(
230 command_rx,
231 cmd_protocol,
232 cmd_write,
233 client_id,
234 fb_width,
235 fb_height,
236 cmd_state,
237 cmd_tx,
238 )
239 .await;
240 });
241 app_state.register_client_task(client_id, cmd_task).await;
242
243 if let Some(instruction) = app_state.get_instruction_for_client(client_id).await {
245 let event = Event::new(
246 &VNC_CLIENT_CONNECTED_EVENT,
247 serde_json::json!({
248 "remote_addr": remote_addr,
249 "width": fb_width,
250 "height": fb_height,
251 "server_name": server_name,
252 }),
253 );
254
255 match call_llm_for_client(
257 &llm_client,
258 &app_state,
259 client_id.to_string(),
260 &instruction,
261 "",
262 Some(&event),
263 protocol.as_ref(),
264 &status_tx,
265 )
266 .await
267 {
268 Ok(ClientLlmResult {
269 actions,
270 memory_updates: _,
271 }) => {
272 for action in actions {
274 let mut guard = write_half_arc.lock().await;
275 if let Err(e) = Self::execute_vnc_action_with_writer(
276 &mut *guard,
277 &protocol,
278 action,
279 fb_width,
280 fb_height,
281 )
282 .await
283 {
284 error!("Failed to execute VNC action: {}", e);
285 }
286 }
287 }
288 Err(e) => {
289 error!("LLM error for VNC client {}: {}", client_id, e);
290 }
291 }
292 }
293
294 let client_data = Arc::new(Mutex::new(ClientData {
296 state: ConnectionState::Idle,
297 memory: String::new(),
298 fb_width,
299 fb_height,
300 }));
301
302 let task_registrar = app_state.clone();
306 let task_handle = tokio::spawn(async move {
307 loop {
308 read_half.end_frame();
309 let mut msg_type_buf = [0u8; 1];
311 match read_half.read_exact(&mut msg_type_buf).await {
312 Ok(_) => {
313 read_half.start_frame(crate::client::response_reader::RESPONSE_DEADLINE);
314 let msg_type = msg_type_buf[0];
315 trace!(
316 "VNC client {} received message type: {}",
317 client_id,
318 msg_type
319 );
320
321 match msg_type {
322 0 => {
323 if let Err(e) = Self::handle_framebuffer_update(
325 &mut read_half,
326 &llm_client,
327 &app_state,
328 &status_tx,
329 client_id,
330 &client_data,
331 &protocol,
332 &write_half_arc,
333 )
334 .await
335 {
336 error!("Failed to handle framebuffer update: {}", e);
337 break;
338 }
339 }
340 1 => {
341 if let Err(e) =
343 Self::handle_set_colour_map_entries(&mut read_half).await
344 {
345 error!("Failed to handle SetColourMapEntries: {}", e);
346 break;
347 }
348 }
349 2 => {
350 debug!("VNC client {}: Bell received", client_id);
352 }
353 3 => {
354 if let Err(e) = Self::handle_server_cut_text(
356 &mut read_half,
357 &llm_client,
358 &app_state,
359 &status_tx,
360 client_id,
361 &client_data,
362 &protocol,
363 &write_half_arc,
364 )
365 .await
366 {
367 error!("Failed to handle server cut text: {}", e);
368 break;
369 }
370 }
371 _ => {
372 warn!(
373 "VNC client {}: Unknown message type: {}",
374 client_id, msg_type
375 );
376 break;
377 }
378 }
379 }
380 Err(e) => {
381 info!("VNC client {} disconnected: {}", client_id, e);
382 break;
383 }
384 }
385 }
386 app_state
389 .update_client_status(client_id, ClientStatus::Disconnected)
390 .await;
391 app_state.remove_client_handle(client_id).await;
392 let _ = status_tx.send(format!("[CLIENT] VNC client {} disconnected", client_id));
393 let _ = status_tx.send("__UPDATE_UI__".to_string());
394 });
395 task_registrar
396 .register_client_task(client_id, task_handle)
397 .await;
398
399 Ok(local_addr)
400 }
401
402 async fn perform_handshake(
404 stream: &mut TcpStream,
405 password: Option<&str>,
406 ) -> Result<(u16, u16, String)> {
407 let mut version_buf = [0u8; 12];
409 stream.read_exact(&mut version_buf).await?;
410 let version_str = std::str::from_utf8(&version_buf)?;
411
412 if !version_str.starts_with("RFB ") {
413 bail!("Invalid VNC protocol version: {}", version_str);
414 }
415
416 debug!("VNC server version: {}", version_str.trim());
417
418 stream.write_all(b"RFB 003.008\n").await?;
420
421 let mut num_security_types = [0u8; 1];
423 stream.read_exact(&mut num_security_types).await?;
424
425 if num_security_types[0] == 0 {
426 let mut reason_len = [0u8; 4];
428 stream.read_exact(&mut reason_len).await?;
429 let len = u32::from_be_bytes(reason_len);
430 let reason = read_server_text(stream, len).await?;
431 bail!(
432 "VNC connection failed: {}",
433 String::from_utf8_lossy(&reason)
434 );
435 }
436
437 let mut security_types = vec![0u8; num_security_types[0] as usize];
438 stream.read_exact(&mut security_types).await?;
439
440 debug!("VNC security types: {:?}", security_types);
441
442 let chosen_security = if password.is_some() && security_types.contains(&2) {
444 2
445 } else if security_types.contains(&1) {
446 1 } else if security_types.contains(&2) {
448 if password.is_none() {
449 bail!("VNC server requires password authentication but no password provided");
450 }
451 2 } else {
453 bail!(
454 "No supported security type (server offers: {:?})",
455 security_types
456 );
457 };
458
459 stream.write_all(&[chosen_security]).await?;
460
461 if chosen_security == 2 {
463 let password = password.ok_or_else(|| anyhow!("Password required but not provided"))?;
464 Self::perform_vnc_auth(stream, password).await?;
465 }
466
467 let mut security_result = [0u8; 4];
469 stream.read_exact(&mut security_result).await?;
470 let result = u32::from_be_bytes(security_result);
471
472 if result != 0 {
473 let mut reason_len = [0u8; 4];
475 stream.read_exact(&mut reason_len).await?;
476 let len = u32::from_be_bytes(reason_len);
477 let reason = read_server_text(stream, len).await?;
478 bail!(
479 "VNC authentication failed: {}",
480 String::from_utf8_lossy(&reason)
481 );
482 }
483
484 stream.write_all(&[1]).await?;
486
487 let mut server_init = [0u8; 24];
489 stream.read_exact(&mut server_init).await?;
490
491 let fb_width = u16::from_be_bytes([server_init[0], server_init[1]]);
492 let fb_height = u16::from_be_bytes([server_init[2], server_init[3]]);
493
494 let name_length = u32::from_be_bytes([
496 server_init[20],
497 server_init[21],
498 server_init[22],
499 server_init[23],
500 ]);
501
502 let name_bytes = read_server_text(stream, name_length).await?;
503 let server_name = String::from_utf8_lossy(&name_bytes).to_string();
504
505 stream
509 .write_all(&[
510 0, 0, 0, 0, 32, 24, 0, 1, 0, 255, 0, 255, 0, 255, 16, 8, 0, 0, 0, 0, ])
515 .await?;
516
517 let set_encodings = [
519 2u8, 0, 0, 1, 0, 0, 0, 0, ];
524 stream.write_all(&set_encodings).await?;
525
526 Ok((fb_width, fb_height, server_name))
527 }
528
529 async fn perform_vnc_auth(stream: &mut TcpStream, password: &str) -> Result<()> {
531 let mut challenge = [0u8; 16];
533 stream.read_exact(&mut challenge).await?;
534
535 stream
536 .write_all(&vnc_auth_response(password.as_bytes(), challenge))
537 .await?;
538
539 Ok(())
540 }
541
542 async fn handle_set_colour_map_entries<R>(read_half: &mut R) -> Result<()>
544 where
545 R: AsyncReadExt + Unpin,
546 {
547 let mut header = [0u8; 5];
549 read_half.read_exact(&mut header).await?;
550
551 let first_color = u16::from_be_bytes([header[1], header[2]]);
552 let num_colors = u16::from_be_bytes([header[3], header[4]]);
553
554 trace!(
555 "SetColourMapEntries: first={}, count={}",
556 first_color,
557 num_colors
558 );
559
560 let color_data_size = (num_colors as usize) * 6;
562 let mut color_data = vec![0u8; color_data_size];
563 read_half.read_exact(&mut color_data).await?;
564
565 Ok(())
568 }
569
570 async fn handle_framebuffer_update<R, W>(
572 read_half: &mut R,
573 llm_client: &OllamaClient,
574 app_state: &Arc<AppState>,
575 status_tx: &mpsc::UnboundedSender<String>,
576 client_id: ClientId,
577 client_data: &Arc<Mutex<ClientData>>,
578 protocol: &Arc<VncClientProtocol>,
579 write_half: &Arc<Mutex<W>>,
580 ) -> Result<()>
581 where
582 R: AsyncReadExt + Unpin,
583 W: AsyncWriteExt + Unpin,
584 {
585 let mut header = [0u8; 3];
587 read_half.read_exact(&mut header).await?;
588
589 let num_rects = u16::from_be_bytes([header[1], header[2]]);
590 trace!("FramebufferUpdate: {} rectangles", num_rects);
591
592 for _ in 0..num_rects {
595 let mut rect_header = [0u8; 12];
597 read_half.read_exact(&mut rect_header).await?;
598
599 let x = u16::from_be_bytes([rect_header[0], rect_header[1]]);
600 let y = u16::from_be_bytes([rect_header[2], rect_header[3]]);
601 let width = u16::from_be_bytes([rect_header[4], rect_header[5]]);
602 let height = u16::from_be_bytes([rect_header[6], rect_header[7]]);
603 let encoding = i32::from_be_bytes([
604 rect_header[8],
605 rect_header[9],
606 rect_header[10],
607 rect_header[11],
608 ]);
609
610 trace!(
611 "Rectangle: {}x{} at ({}, {}), encoding={}",
612 width,
613 height,
614 x,
615 y,
616 encoding
617 );
618
619 if encoding != 0 {
620 bail!("unsupported VNC rectangle encoding {encoding}; only Raw was negotiated");
621 }
622 discard_raw_rectangle(read_half, width, height).await?;
623 }
624
625 let mut client_data_lock = client_data.lock().await;
626
627 if client_data_lock.state != ConnectionState::Idle {
628 return Ok(());
630 }
631
632 client_data_lock.state = ConnectionState::Processing;
633 drop(client_data_lock);
634
635 if let Some(instruction) = app_state.get_instruction_for_client(client_id).await {
637 let event = Event::new(
638 &VNC_CLIENT_FRAMEBUFFER_UPDATE_EVENT,
639 serde_json::json!({
640 "rectangles": num_rects,
641 "update_summary": format!("{} rectangle(s) updated", num_rects),
642 }),
643 );
644
645 match call_llm_for_client(
646 llm_client,
647 app_state,
648 client_id.to_string(),
649 &instruction,
650 &client_data.lock().await.memory,
651 Some(&event),
652 protocol.as_ref(),
653 status_tx,
654 )
655 .await
656 {
657 Ok(ClientLlmResult {
658 actions,
659 memory_updates,
660 }) => {
661 if let Some(mem) = memory_updates {
663 client_data.lock().await.memory = mem;
664 }
665
666 let fb_width = client_data.lock().await.fb_width;
668 let fb_height = client_data.lock().await.fb_height;
669
670 for action in actions {
671 let mut write_lock = write_half.lock().await;
672 if let Err(e) = Self::execute_vnc_action_with_writer(
673 &mut *write_lock,
674 protocol,
675 action,
676 fb_width,
677 fb_height,
678 )
679 .await
680 {
681 error!("Failed to execute VNC action: {}", e);
682 }
683 }
684 }
685 Err(e) => {
686 error!("LLM error for VNC client {}: {}", client_id, e);
687 }
688 }
689 }
690
691 client_data.lock().await.state = ConnectionState::Idle;
692 Ok(())
693 }
694
695 async fn handle_server_cut_text<R, W>(
697 read_half: &mut R,
698 llm_client: &OllamaClient,
699 app_state: &Arc<AppState>,
700 status_tx: &mpsc::UnboundedSender<String>,
701 client_id: ClientId,
702 client_data: &Arc<Mutex<ClientData>>,
703 protocol: &Arc<VncClientProtocol>,
704 write_half: &Arc<Mutex<W>>,
705 ) -> Result<()>
706 where
707 R: AsyncReadExt + Unpin,
708 W: AsyncWriteExt + Unpin,
709 {
710 let mut header = [0u8; 7];
712 read_half.read_exact(&mut header).await?;
713
714 let text_length = u32::from_be_bytes([header[3], header[4], header[5], header[6]]);
715
716 let text_bytes = read_server_text(read_half, text_length).await?;
717
718 let text = String::from_utf8_lossy(&text_bytes).to_string();
719 debug!("VNC ServerCutText: {}", text);
720
721 if let Some(instruction) = app_state.get_instruction_for_client(client_id).await {
723 let event = Event::new(
724 &VNC_CLIENT_SERVER_CUT_TEXT_EVENT,
725 serde_json::json!({
726 "text": text,
727 }),
728 );
729
730 if let Ok(ClientLlmResult {
731 actions,
732 memory_updates,
733 }) = call_llm_for_client(
734 llm_client,
735 app_state,
736 client_id.to_string(),
737 &instruction,
738 &client_data.lock().await.memory,
739 Some(&event),
740 protocol.as_ref(),
741 status_tx,
742 )
743 .await
744 {
745 if let Some(mem) = memory_updates {
746 client_data.lock().await.memory = mem;
747 }
748
749 let fb_width = client_data.lock().await.fb_width;
750 let fb_height = client_data.lock().await.fb_height;
751
752 for action in actions {
753 let mut write_lock = write_half.lock().await;
754 if let Err(e) = Self::execute_vnc_action_with_writer(
755 &mut *write_lock,
756 protocol,
757 action,
758 fb_width,
759 fb_height,
760 )
761 .await
762 {
763 error!("Failed to execute VNC action: {}", e);
764 }
765 }
766 }
767 }
768
769 Ok(())
770 }
771
772 #[allow(clippy::too_many_arguments)]
781 async fn command_loop<W>(
782 mut command_rx: mpsc::Receiver<crate::state::client_handles::ClientCommand>,
783 protocol: Arc<VncClientProtocol>,
784 write_half: Arc<Mutex<W>>,
785 client_id: ClientId,
786 fb_width: u16,
787 fb_height: u16,
788 app_state: Arc<AppState>,
789 status_tx: mpsc::UnboundedSender<String>,
790 ) where
791 W: AsyncWriteExt + Unpin + Send + 'static,
792 {
793 use crate::llm::actions::protocol_trait::Protocol;
794 use crate::state::client_handles::ClientSendOutcome;
795 use crate::state::AccessLogOwner;
796
797 while let Some(command) = command_rx.recv().await {
798 let action = command.action.clone();
799 let outcome = Self::apply_injected_action(
800 action.clone(),
801 &protocol,
802 &write_half,
803 fb_width,
804 fb_height,
805 )
806 .await;
807
808 let outcome_json = match &outcome {
809 Ok(outcome) => serde_json::to_value(outcome).unwrap_or(serde_json::Value::Null),
810 Err(e) => serde_json::json!({"error": e.to_string()}),
811 };
812 app_state
813 .record_access_log(
814 AccessLogOwner::Client(client_id.as_u32()),
815 protocol.as_ref().protocol_name(),
816 None,
817 "injected_action",
818 action,
819 vec![outcome_json],
820 )
821 .await;
822
823 let disconnect = matches!(outcome, Ok(ClientSendOutcome::Disconnected));
824 if let Err(e) = &outcome {
825 error!("VNC client {} injected action failed: {}", client_id, e);
826 let _ = status_tx.send(format!(
827 "[WARN] Client {} injected action failed: {}",
828 client_id, e
829 ));
830 }
831 let _ = status_tx.send("__UPDATE_UI__".to_string());
832 crate::client::command_support::reply(command, outcome);
833
834 if disconnect {
835 let _ = write_half.lock().await.shutdown().await;
838 break;
839 }
840 }
841 }
842
843 async fn apply_injected_action<W>(
849 action: JsonValue,
850 protocol: &Arc<VncClientProtocol>,
851 write_half: &Arc<Mutex<W>>,
852 fb_width: u16,
853 fb_height: u16,
854 ) -> Result<crate::state::client_handles::ClientSendOutcome>
855 where
856 W: AsyncWriteExt + Unpin,
857 {
858 use crate::llm::actions::client_trait::Client;
859 use crate::state::client_handles::ClientSendOutcome;
860
861 match protocol.as_ref().execute_action(action) {
862 Err(e) => Ok(ClientSendOutcome::Rejected {
863 error: e.to_string(),
864 }),
865 Ok(ClientActionResult::Custom { name, data }) => {
866 let mut buf: Vec<u8> = Vec::new();
869 Self::send_vnc_message_with_writer(&mut buf, &name, &data, fb_width, fb_height)
870 .await?;
871 let bytes_sent = buf.len();
872 {
873 let mut guard = write_half.lock().await;
874 guard.write_all(&buf).await?;
875 guard.flush().await?;
876 }
877 Ok(ClientSendOutcome::Sent { bytes_sent })
878 }
879 Ok(ClientActionResult::Disconnect) => Ok(ClientSendOutcome::Disconnected),
880 Ok(_) => Ok(ClientSendOutcome::Executed {
882 detail: "executed (nothing to write)".to_string(),
883 }),
884 }
885 }
886
887 async fn execute_vnc_action_with_writer<W>(
889 writer: &mut W,
890 protocol: &Arc<VncClientProtocol>,
891 action: JsonValue,
892 fb_width: u16,
893 fb_height: u16,
894 ) -> Result<()>
895 where
896 W: AsyncWriteExt + Unpin,
897 {
898 use crate::llm::actions::client_trait::Client;
899
900 match protocol.as_ref().execute_action(action)? {
901 ClientActionResult::Custom { name, data } => {
902 Self::send_vnc_message_with_writer(writer, &name, &data, fb_width, fb_height)
903 .await?;
904 }
905 ClientActionResult::Disconnect => {
906 return Err(anyhow!("Disconnect requested"));
907 }
908 _ => {}
909 }
910
911 Ok(())
912 }
913
914 pub async fn send_vnc_message_with_writer<W>(
920 writer: &mut W,
921 action_name: &str,
922 data: &JsonValue,
923 fb_width: u16,
924 fb_height: u16,
925 ) -> Result<()>
926 where
927 W: AsyncWriteExt + Unpin,
928 {
929 match action_name {
930 "request_framebuffer_update" => {
931 let incremental = data["incremental"].as_bool().unwrap_or(true);
932 let x = rfb_u16(data, "x", 0)?;
933 let y = rfb_u16(data, "y", 0)?;
934 let width = rfb_u16(data, "width", fb_width)?;
935 let height = rfb_u16(data, "height", fb_height)?;
936
937 let msg = [
938 3u8, if incremental { 1 } else { 0 },
940 (x >> 8) as u8,
941 (x & 0xff) as u8,
942 (y >> 8) as u8,
943 (y & 0xff) as u8,
944 (width >> 8) as u8,
945 (width & 0xff) as u8,
946 (height >> 8) as u8,
947 (height & 0xff) as u8,
948 ];
949 writer.write_all(&msg).await?;
950 }
951 "send_pointer_event" => {
952 let x = rfb_u16(data, "x", 0)?;
953 let y = rfb_u16(data, "y", 0)?;
954 let button_mask = rfb_u8(data, "button_mask", 0)?;
955
956 let msg = [
957 5u8, button_mask,
959 (x >> 8) as u8,
960 (x & 0xff) as u8,
961 (y >> 8) as u8,
962 (y & 0xff) as u8,
963 ];
964 writer.write_all(&msg).await?;
965 }
966 "send_key_event" => {
967 let Some(key) = resolve_keysym(&data["key"]) else {
968 anyhow::bail!(
969 "send_key_event: unrecognised 'key' {}. Use a keysym number, a single \
970 character like \"a\", or a name like \"Enter\"/\"Escape\"/\"F1\"",
971 data["key"]
972 );
973 };
974 let down = data["down"].as_bool().unwrap_or(false);
975
976 let msg = [
977 4u8, if down { 1 } else { 0 },
979 0,
980 0, (key >> 24) as u8,
982 (key >> 16) as u8,
983 (key >> 8) as u8,
984 (key & 0xff) as u8,
985 ];
986 writer.write_all(&msg).await?;
987 }
988 "send_client_cut_text" => {
989 let text = data["text"].as_str().unwrap_or("");
990 let text_bytes = text.as_bytes();
991 if text_bytes.len() > MAX_TEXT_BYTES {
992 anyhow::bail!("VNC clipboard exceeds byte cap");
993 }
994 let length =
995 u32::try_from(text_bytes.len()).context("VNC clipboard length exceeds u32")?;
996
997 let mut msg = vec![
998 6u8, 0,
1000 0,
1001 0, (length >> 24) as u8,
1003 (length >> 16) as u8,
1004 (length >> 8) as u8,
1005 (length & 0xff) as u8,
1006 ];
1007 msg.extend_from_slice(text_bytes);
1008 writer.write_all(&msg).await?;
1009 }
1010 _ => {
1011 warn!("Unknown VNC action: {}", action_name);
1012 }
1013 }
1014
1015 Ok(())
1016 }
1017}
1018
1019fn rfb_u16(data: &JsonValue, key: &str, default: u16) -> Result<u16> {
1030 match data.get(key) {
1031 None | Some(JsonValue::Null) => Ok(default),
1032 Some(value) => {
1033 let raw = value.as_u64().ok_or_else(|| {
1034 anyhow::anyhow!("'{key}' must be a non-negative whole number, got {value}")
1035 })?;
1036 u16::try_from(raw).map_err(|_| {
1037 anyhow::anyhow!(
1038 "'{key}' is {raw}; RFB carries coordinates and extents in two bytes, so \
1039 0-65535 is the whole of what this field can say"
1040 )
1041 })
1042 }
1043 }
1044}
1045
1046fn rfb_u8(data: &JsonValue, key: &str, default: u8) -> Result<u8> {
1051 match data.get(key) {
1052 None | Some(JsonValue::Null) => Ok(default),
1053 Some(value) => {
1054 let raw = value.as_u64().ok_or_else(|| {
1055 anyhow::anyhow!("'{key}' must be a non-negative whole number, got {value}")
1056 })?;
1057 u8::try_from(raw).map_err(|_| {
1058 anyhow::anyhow!(
1059 "'{key}' is {raw}; the RFB button mask is one byte (0-255), one bit per \
1060 button"
1061 )
1062 })
1063 }
1064 }
1065}