1pub mod actions;
35pub mod deflate;
36
37use anyhow::{Context, Result};
38use futures::stream::StreamExt;
39use futures::SinkExt;
40use std::collections::VecDeque;
41use std::net::SocketAddr;
42use std::sync::Arc;
43use tokio::io::{AsyncReadExt, AsyncWriteExt};
44use tokio::net::TcpStream;
45use tokio::sync::{mpsc, Mutex};
46use tokio::task::JoinSet;
47use tokio_tungstenite::tungstenite::protocol::frame::coding::CloseCode;
48use tokio_tungstenite::tungstenite::protocol::{CloseFrame, Role, WebSocketConfig};
49use tokio_tungstenite::tungstenite::Message;
50use tokio_tungstenite::WebSocketStream;
51use tracing::{debug, trace};
52
53use crate::llm::action_helper::call_llm;
54use crate::llm::actions::protocol_trait::ActionResult;
55use crate::llm::ollama_client::OllamaClient;
56use crate::logging::emit::Log;
57use crate::protocol::Event;
58use crate::server::connection::ConnectionId;
59use crate::state::app_state::AppState;
60use crate::state::server::{ConnectionState, ConnectionStatus, ProtocolConnectionInfo};
61use crate::state::ServerId;
62use actions::{
63 encode_inbound_payload, WebSocketProtocol, WsOut, WEBSOCKET_BINARY_MESSAGE_EVENT,
64 WEBSOCKET_CLOSE_EVENT, WEBSOCKET_CONNECTION_OPENED_EVENT, WEBSOCKET_HANDSHAKE_EVENT,
65 WEBSOCKET_PING_EVENT, WEBSOCKET_TEXT_MESSAGE_EVENT,
66};
67
68const MAX_REQUEST_HEAD: usize = 16 * 1024;
70
71const HANDSHAKE_TIMEOUT_SECS: u64 = 15;
73
74const DEFAULT_MAX_MESSAGE_SIZE: usize = 1024 * 1024;
75const DEFAULT_MAX_FRAME_SIZE: usize = 1024 * 1024;
76pub const MAX_SIZE_LIMIT: usize = 64 * 1024 * 1024;
77
78pub const IDLE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(600);
103
104pub const DEFAULT_PERMESSAGE_DEFLATE: bool = true;
108
109const MAX_CONNECTIONS: usize = crate::server::accept_bounded::DEFAULT_MAX_CONNECTIONS;
123
124const CONNECTION_CAP_REASON: &str = "Too many connections, try again later";
126
127static CONNECTION_CAP_REFUSAL: std::sync::LazyLock<Vec<u8>> = std::sync::LazyLock::new(|| {
144 build_error_response(
145 503,
146 CONNECTION_CAP_REASON,
147 &[("Retry-After".to_string(), "30".to_string())],
148 )
149});
150
151#[derive(Debug, Clone, PartialEq, Eq)]
158pub struct RequestHead {
159 pub method: String,
160 pub target: String,
162 pub version: String,
163 pub headers: Vec<(String, String)>,
165}
166
167impl RequestHead {
168 pub fn header(&self, name: &str) -> Option<&str> {
170 let lower = name.to_ascii_lowercase();
171 self.headers
172 .iter()
173 .find(|(k, _)| *k == lower)
174 .map(|(_, v)| v.as_str())
175 }
176
177 pub fn header_all(&self, name: &str) -> Vec<&str> {
179 let lower = name.to_ascii_lowercase();
180 self.headers
181 .iter()
182 .filter(|(k, _)| *k == lower)
183 .map(|(_, v)| v.as_str())
184 .collect()
185 }
186
187 pub fn path(&self) -> &str {
189 match self.target.split_once('?') {
190 Some((p, _)) => p,
191 None => &self.target,
192 }
193 }
194
195 pub fn query(&self) -> &str {
197 match self.target.split_once('?') {
198 Some((_, q)) => q,
199 None => "",
200 }
201 }
202
203 pub fn headers_json(&self) -> serde_json::Value {
205 let mut map = serde_json::Map::new();
206 for (k, v) in &self.headers {
207 match map.get_mut(k) {
208 Some(serde_json::Value::String(existing)) => {
209 existing.push_str(", ");
210 existing.push_str(v);
211 }
212 _ => {
213 map.insert(k.clone(), serde_json::Value::String(v.clone()));
214 }
215 }
216 }
217 serde_json::Value::Object(map)
218 }
219
220 pub fn offered_subprotocols(&self) -> Vec<String> {
225 self.header_all("sec-websocket-protocol")
226 .into_iter()
227 .flat_map(|v| v.split(','))
228 .map(|s| s.trim().to_string())
229 .filter(|s| !s.is_empty())
230 .collect()
231 }
232}
233
234pub fn parse_request_head(bytes: &[u8]) -> std::result::Result<RequestHead, String> {
240 let text =
241 std::str::from_utf8(bytes).map_err(|_| "request head is not valid UTF-8".to_string())?;
242
243 let mut lines = text.split("\r\n");
244 let request_line = lines.next().unwrap_or("");
245 if request_line.is_empty() {
246 return Err("empty request line".to_string());
247 }
248
249 let mut parts = request_line.split(' ');
250 let method = parts.next().unwrap_or("").to_string();
251 let target = parts.next().unwrap_or("").to_string();
252 let version = parts.next().unwrap_or("").to_string();
253 if method.is_empty() || target.is_empty() || version.is_empty() {
254 return Err(format!("malformed request line: {request_line:?}"));
255 }
256 if !version.starts_with("HTTP/") {
257 return Err(format!("unrecognised HTTP version: {version:?}"));
258 }
259
260 let mut headers = Vec::new();
261 for line in lines {
262 if line.is_empty() {
263 break;
264 }
265 if line.starts_with(' ') || line.starts_with('\t') {
268 return Err("obsolete header line folding is not supported".to_string());
269 }
270 let Some((name, value)) = line.split_once(':') else {
271 return Err(format!("malformed header line: {line:?}"));
272 };
273 if name.is_empty() || name.ends_with(' ') {
274 return Err(format!("malformed header name: {name:?}"));
275 }
276 headers.push((name.to_ascii_lowercase(), value.trim().to_string()));
277 }
278
279 Ok(RequestHead {
280 method,
281 target,
282 version,
283 headers,
284 })
285}
286
287#[derive(Debug, Clone, PartialEq, Eq)]
289pub struct UpgradeRejection {
290 pub status: u16,
291 pub reason: String,
292 pub extra_headers: Vec<(String, String)>,
294}
295
296pub fn validate_upgrade(head: &RequestHead) -> std::result::Result<String, UpgradeRejection> {
302 let reject = |status: u16, reason: &str| UpgradeRejection {
303 status,
304 reason: reason.to_string(),
305 extra_headers: Vec::new(),
306 };
307
308 if !head.method.eq_ignore_ascii_case("GET") {
309 return Err(reject(
310 405,
311 "WebSocket upgrade requires the GET method (RFC 6455 section 4.1)",
312 ));
313 }
314 if head.version != "HTTP/1.1" {
315 return Err(reject(
316 505,
317 "WebSocket upgrade requires HTTP/1.1 (RFC 6455 section 4.1)",
318 ));
319 }
320
321 let upgrade_ok = head.header_all("upgrade").iter().any(|v| {
322 v.split(',')
323 .any(|t| t.trim().eq_ignore_ascii_case("websocket"))
324 });
325 if !upgrade_ok {
326 return Err(reject(400, "missing or invalid Upgrade: websocket header"));
327 }
328
329 let connection_ok = head.header_all("connection").iter().any(|v| {
330 v.split(',')
331 .any(|t| t.trim().eq_ignore_ascii_case("upgrade"))
332 });
333 if !connection_ok {
334 return Err(reject(400, "missing or invalid Connection: Upgrade header"));
335 }
336
337 match head.header("sec-websocket-version") {
338 Some("13") => {}
339 Some(other) => {
340 return Err(UpgradeRejection {
341 status: 426,
342 reason: format!(
343 "unsupported Sec-WebSocket-Version {other:?}; this server speaks version 13"
344 ),
345 extra_headers: vec![("Sec-WebSocket-Version".into(), "13".into())],
347 });
348 }
349 None => {
350 return Err(UpgradeRejection {
351 status: 426,
352 reason: "missing Sec-WebSocket-Version header".to_string(),
353 extra_headers: vec![("Sec-WebSocket-Version".into(), "13".into())],
354 })
355 }
356 }
357
358 let key = head
359 .header("sec-websocket-key")
360 .ok_or_else(|| reject(400, "missing Sec-WebSocket-Key header"))?;
361
362 use base64::Engine as _;
364 match base64::engine::general_purpose::STANDARD.decode(key) {
365 Ok(raw) if raw.len() == 16 => {}
366 _ => {
367 return Err(reject(
368 400,
369 "Sec-WebSocket-Key is not a base64-encoded 16-byte value",
370 ))
371 }
372 }
373
374 Ok(key.to_string())
375}
376
377pub fn build_accept_response(
383 key: &str,
384 subprotocol: Option<&str>,
385 extensions: Option<&str>,
386) -> String {
387 let accept = tokio_tungstenite::tungstenite::handshake::derive_accept_key(key.as_bytes());
388 let mut response = String::from("HTTP/1.1 101 Switching Protocols\r\n");
389 response.push_str("Upgrade: websocket\r\n");
390 response.push_str("Connection: Upgrade\r\n");
391 response.push_str(&format!("Sec-WebSocket-Accept: {accept}\r\n"));
392 if let Some(sub) = subprotocol {
393 response.push_str(&format!("Sec-WebSocket-Protocol: {sub}\r\n"));
394 }
395 if let Some(ext) = extensions {
396 response.push_str(&format!("Sec-WebSocket-Extensions: {ext}\r\n"));
397 }
398 response.push_str("\r\n");
399 response
400}
401
402pub fn build_error_response(
404 status: u16,
405 reason: &str,
406 extra_headers: &[(String, String)],
407) -> Vec<u8> {
408 let phrase = match status {
409 400 => "Bad Request",
410 401 => "Unauthorized",
411 403 => "Forbidden",
412 404 => "Not Found",
413 405 => "Method Not Allowed",
414 426 => "Upgrade Required",
415 429 => "Too Many Requests",
416 500 => "Internal Server Error",
417 503 => "Service Unavailable",
418 505 => "HTTP Version Not Supported",
419 _ => "Error",
420 };
421 let body = format!("{reason}\n");
422 let mut response = format!("HTTP/1.1 {status} {phrase}\r\n");
423 for (name, value) in extra_headers {
424 response.push_str(&format!("{name}: {value}\r\n"));
425 }
426 response.push_str("Connection: close\r\n");
427 response.push_str("Content-Type: text/plain; charset=utf-8\r\n");
428 response.push_str(&format!("Content-Length: {}\r\n\r\n", body.len()));
429 response.push_str(&body);
430 response.into_bytes()
431}
432
433#[derive(Debug, Clone, Copy, PartialEq, Eq)]
438enum HandlerState {
439 Idle,
440 Processing,
441 Accumulating,
442}
443
444#[derive(Debug, Clone)]
446enum Inbound {
447 Text(String),
448 Binary(Vec<u8>),
449 Ping(Vec<u8>),
450}
451
452impl Inbound {
453 fn merge(self, next: Inbound) -> std::result::Result<Inbound, (Inbound, Inbound)> {
455 match (self, next) {
456 (Inbound::Text(mut a), Inbound::Text(b)) => {
457 a.push_str(&b);
458 Ok(Inbound::Text(a))
459 }
460 (Inbound::Binary(mut a), Inbound::Binary(b)) => {
461 a.extend_from_slice(&b);
462 Ok(Inbound::Binary(a))
463 }
464 (a, b) => Err((a, b)),
465 }
466 }
467}
468
469struct ConnData {
470 state: HandlerState,
471 queued: VecDeque<Inbound>,
473 pending: Option<Inbound>,
475}
476
477struct ConnCtx {
479 server_id: ServerId,
480 connection_id: ConnectionId,
481 llm_client: OllamaClient,
482 app_state: Arc<AppState>,
483 status_tx: mpsc::UnboundedSender<String>,
484 out_tx: mpsc::UnboundedSender<WsOut>,
485 protocol: Arc<WebSocketProtocol>,
486 data: Arc<Mutex<ConnData>>,
487 subprotocol: Option<String>,
488}
489
490pub struct WebSocketServer;
495
496impl WebSocketServer {
497 pub async fn spawn_with_llm_actions(
500 listen_addr: SocketAddr,
501 llm_client: OllamaClient,
502 app_state: Arc<AppState>,
503 status_tx: mpsc::UnboundedSender<String>,
504 server_id: ServerId,
505 startup_params: Option<crate::protocol::StartupParams>,
506 ) -> Result<SocketAddr> {
507 let permessage_deflate = match startup_params.as_ref() {
510 Some(params) => params
511 .get_optional_bool("permessage_deflate")
512 .map_err(|e| anyhow::anyhow!("WebSocket startup parameter error: {e}"))?
513 .unwrap_or(DEFAULT_PERMESSAGE_DEFLATE),
514 None => DEFAULT_PERMESSAGE_DEFLATE,
515 };
516 let (path_filter, max_message_size, max_frame_size, idle_timeout) =
517 match startup_params.as_ref() {
518 Some(params) => {
519 let path = params
520 .get_optional_string("path")
521 .map_err(|e| anyhow::anyhow!("WebSocket startup parameter error: {e}"))?
522 .map(|p| {
523 if p.starts_with('/') {
524 p
525 } else {
526 format!("/{p}")
527 }
528 });
529 let msg = params
530 .get_optional_u64("max_message_size")
531 .map_err(|e| anyhow::anyhow!("WebSocket startup parameter error: {e}"))?
532 .map(|v| (v as usize).clamp(125, MAX_SIZE_LIMIT))
533 .unwrap_or(DEFAULT_MAX_MESSAGE_SIZE);
534 let frame = params
535 .get_optional_u64("max_frame_size")
536 .map_err(|e| anyhow::anyhow!("WebSocket startup parameter error: {e}"))?
537 .map(|v| (v as usize).clamp(125, MAX_SIZE_LIMIT))
538 .unwrap_or(DEFAULT_MAX_FRAME_SIZE);
539 let idle = params
540 .get_optional_u64("idle_timeout_secs")
541 .map_err(|e| anyhow::anyhow!("WebSocket startup parameter error: {e}"))?
542 .map(std::time::Duration::from_secs)
543 .unwrap_or(IDLE_TIMEOUT);
544 (path, msg, frame, idle)
545 }
546 None => (
547 None,
548 DEFAULT_MAX_MESSAGE_SIZE,
549 DEFAULT_MAX_FRAME_SIZE,
550 IDLE_TIMEOUT,
551 ),
552 };
553
554 let ws_config = WebSocketConfig {
555 max_message_size: Some(max_message_size),
556 max_frame_size: Some(max_frame_size),
557 accept_unmasked_frames: false,
560 ..Default::default()
561 };
562
563 let listener =
564 crate::server::socket_helpers::create_reusable_tcp_listener(listen_addr).await?;
565 let local_addr = listener.local_addr()?;
566
567 Log::new(Some(&status_tx)).info(format!("WebSocket server listening on {}", local_addr));
568 if let Some(p) = &path_filter {
569 Log::new(Some(&status_tx)).info(format!("WebSocket server only upgrades path {}", p));
570 }
571
572 let accept_state = app_state.clone();
573 let accept_status_tx = status_tx.clone();
574
575 let limiter = crate::server::accept_bounded::ConnectionLimiter::new(MAX_CONNECTIONS);
576 let accept_handle = tokio::spawn(async move {
577 loop {
578 match crate::server::accept_bounded::accept_bounded(
579 &listener,
580 &limiter,
581 &CONNECTION_CAP_REFUSAL,
582 "WEBSOCKET",
583 Some(&accept_status_tx),
584 )
585 .await
586 {
587 Ok((socket, peer_addr, permit)) => {
588 let llm_client = llm_client.clone();
589 let app_state = accept_state.clone();
590 let status_tx = accept_status_tx.clone();
591 let path_filter = path_filter.clone();
592 let task_owner = app_state.clone();
594 task_owner
595 .spawn_server_task(server_id, async move {
596 let permit = Arc::new(permit);
604 if let Err(e) = Self::handle_connection(
605 socket,
606 peer_addr,
607 local_addr,
608 llm_client,
609 app_state,
610 status_tx,
611 server_id,
612 path_filter,
613 ws_config,
614 permit,
615 idle_timeout,
616 permessage_deflate,
617 )
618 .await
619 {
620 debug!("WebSocket connection from {} ended: {}", peer_addr, e);
621 }
622 })
623 .await;
624 }
625 Err(e) => {
626 Log::new(Some(&accept_status_tx))
627 .error(format!("WebSocket accept error: {}", e));
628 break;
629 }
630 }
631 }
632 });
633
634 app_state
636 .register_server_task(server_id, accept_handle)
637 .await;
638
639 let _ = status_tx.send("__UPDATE_UI__".to_string());
640 Ok(local_addr)
641 }
642
643 #[allow(clippy::too_many_arguments)]
645 async fn handle_connection(
646 mut socket: TcpStream,
647 peer_addr: SocketAddr,
648 local_addr: SocketAddr,
649 llm_client: OllamaClient,
650 app_state: Arc<AppState>,
651 status_tx: mpsc::UnboundedSender<String>,
652 server_id: ServerId,
653 path_filter: Option<String>,
654 ws_config: WebSocketConfig,
655 permit: Arc<crate::server::accept_bounded::ConnectionPermit>,
656 idle_timeout: std::time::Duration,
657 permessage_deflate: bool,
658 ) -> Result<()> {
659 let (head_bytes, leftover) = match tokio::time::timeout(
661 std::time::Duration::from_secs(HANDSHAKE_TIMEOUT_SECS),
662 read_request_head(&mut socket),
663 )
664 .await
665 {
666 Ok(Ok(v)) => v,
667 Ok(Err(e)) => {
668 debug!("WebSocket handshake read failed from {}: {}", peer_addr, e);
669 return Ok(());
670 }
671 Err(_) => {
672 debug!("WebSocket handshake timed out for {}", peer_addr);
673 let _ = socket
674 .write_all(&build_error_response(
675 408,
676 "Timed out waiting for the HTTP request",
677 &[],
678 ))
679 .await;
680 return Ok(());
681 }
682 };
683
684 let head = match parse_request_head(&head_bytes) {
685 Ok(h) => h,
686 Err(e) => {
687 Log::new(Some(&status_tx)).warn(format!(
688 "WebSocket malformed request from {}: {}",
689 peer_addr, e
690 ));
691 let _ = socket.write_all(&build_error_response(400, &e, &[])).await;
692 return Ok(());
693 }
694 };
695
696 let key = match validate_upgrade(&head) {
698 Ok(k) => k,
699 Err(rej) => {
700 Log::new(Some(&status_tx)).debug(format!(
701 "WebSocket upgrade refused for {} ({}): {}",
702 peer_addr, rej.status, rej.reason
703 ));
704 let _ = socket
705 .write_all(&build_error_response(
706 rej.status,
707 &rej.reason,
708 &rej.extra_headers,
709 ))
710 .await;
711 return Ok(());
712 }
713 };
714
715 if let Some(want) = &path_filter {
716 if head.path() != want {
717 Log::new(Some(&status_tx)).debug(format!(
718 "WebSocket 404 for path {} (server serves {})",
719 head.path(),
720 want
721 ));
722 let _ = socket
723 .write_all(&build_error_response(
724 404,
725 &format!("This server only serves WebSocket at {want}"),
726 &[],
727 ))
728 .await;
729 return Ok(());
730 }
731 }
732
733 let connection_id = ConnectionId::new(app_state.get_next_unified_id().await);
735 let offered = head.offered_subprotocols();
736 let path = head.path().to_string();
737
738 let (out_tx, out_rx) = mpsc::unbounded_channel::<WsOut>();
739 actions::register_connection(
740 server_id,
741 connection_id,
742 peer_addr.to_string(),
743 path.clone(),
744 out_tx.clone(),
745 );
746
747 let now = crate::utils::clock::Instant::now();
748 app_state
749 .add_connection_to_server(
750 server_id,
751 ConnectionState {
752 id: connection_id,
753 remote_addr: peer_addr,
754 local_addr,
755 bytes_sent: 0,
756 bytes_received: 0,
757 packets_sent: 0,
758 packets_received: 0,
759 last_activity: now,
760 status: ConnectionStatus::Active,
761 status_changed_at: now,
762 protocol_info: ProtocolConnectionInfo::new(serde_json::json!({
763 "path": path,
764 "subprotocol": serde_json::Value::Null,
765 "state": "Handshake",
766 })),
767 },
768 )
769 .await;
770 let _ = status_tx.send("__UPDATE_UI__".to_string());
771
772 let protocol = Arc::new(WebSocketProtocol::for_connection(
773 server_id,
774 connection_id,
775 out_tx.clone(),
776 status_tx.clone(),
777 offered.clone(),
778 ));
779
780 let decision = Self::run_handshake_decision(
782 &head,
783 peer_addr,
784 &llm_client,
785 &app_state,
786 &status_tx,
787 server_id,
788 connection_id,
789 protocol.as_ref(),
790 )
791 .await;
792
793 let subprotocol = match decision {
794 HandshakeDecision::Accept { subprotocol } => subprotocol,
795 HandshakeDecision::Reject { status, reason } => {
796 Log::new(Some(&status_tx)).info(format!(
800 "WebSocket upgrade from {} rejected by handler ({}): {}",
801 peer_addr, status, reason
802 ));
803 let _ = socket
804 .write_all(&build_error_response(status, &reason, &[]))
805 .await;
806 Self::cleanup(&app_state, server_id, connection_id, &status_tx).await;
807 return Ok(());
808 }
809 };
810
811 let deflate = if permessage_deflate {
815 deflate::negotiate(head.header_all("sec-websocket-extensions"))
816 } else {
817 None
818 };
819 let response = build_accept_response(
820 &key,
821 subprotocol.as_deref(),
822 deflate.as_ref().map(|(_, value)| value.as_str()),
823 );
824 let deflate = deflate.map(|(params, _)| params);
825 trace!("WebSocket 101 response to {}:\n{}", peer_addr, response);
826 if let Err(e) = socket.write_all(response.as_bytes()).await {
827 debug!("WebSocket failed to write 101 to {}: {}", peer_addr, e);
828 Self::cleanup(&app_state, server_id, connection_id, &status_tx).await;
829 return Ok(());
830 }
831 if let Err(e) = socket.flush().await {
832 debug!("WebSocket failed to flush 101 to {}: {}", peer_addr, e);
833 Self::cleanup(&app_state, server_id, connection_id, &status_tx).await;
834 return Ok(());
835 }
836
837 actions::set_connection_subprotocol(connection_id, subprotocol.clone());
838 app_state
839 .with_server_mut(server_id, |server| {
840 if let Some(conn) = server.connections.get_mut(&connection_id) {
841 conn.protocol_info = ProtocolConnectionInfo::new(serde_json::json!({
842 "path": path,
843 "subprotocol": subprotocol,
844 "permessage_deflate": deflate.is_some(),
845 "state": "Open",
846 }));
847 }
848 })
849 .await;
850
851 Log::new(Some(&status_tx)).info(format!(
852 "WebSocket {} open from {} ({}{}{})",
853 connection_id,
854 peer_addr,
855 path,
856 subprotocol
857 .as_ref()
858 .map(|s| format!(", subprotocol {s}"))
859 .unwrap_or_default(),
860 if deflate.is_some() {
861 ", permessage-deflate"
862 } else {
863 ""
864 }
865 ));
866
867 let max_message = ws_config
875 .max_message_size
876 .unwrap_or(DEFAULT_MAX_MESSAGE_SIZE);
877 let max_frame = ws_config.max_frame_size.unwrap_or(DEFAULT_MAX_FRAME_SIZE);
878 let ws_stream = WebSocketStream::from_partially_read(
879 deflate::DeflateStream::server(socket, deflate, max_message, max_frame, leftover),
880 Vec::new(),
881 Role::Server,
882 Some(ws_config),
883 )
884 .await;
885
886 Self::run_connection(
887 ws_stream,
888 out_rx,
889 permit,
890 ConnCtx {
891 server_id,
892 connection_id,
893 llm_client,
894 app_state: app_state.clone(),
895 status_tx: status_tx.clone(),
896 out_tx,
897 protocol,
898 data: Arc::new(Mutex::new(ConnData {
899 state: HandlerState::Idle,
900 queued: VecDeque::new(),
901 pending: None,
902 })),
903 subprotocol,
904 },
905 path,
906 peer_addr,
907 idle_timeout,
908 deflate,
909 )
910 .await;
911
912 Self::cleanup(&app_state, server_id, connection_id, &status_tx).await;
913 Ok(())
914 }
915
916 #[allow(clippy::too_many_arguments)]
922 async fn run_handshake_decision(
923 head: &RequestHead,
924 peer_addr: SocketAddr,
925 llm_client: &OllamaClient,
926 app_state: &Arc<AppState>,
927 status_tx: &mpsc::UnboundedSender<String>,
928 server_id: ServerId,
929 connection_id: ConnectionId,
930 protocol: &WebSocketProtocol,
931 ) -> HandshakeDecision {
932 let event = Event::new(
933 &WEBSOCKET_HANDSHAKE_EVENT,
934 serde_json::json!({
935 "path": head.path(),
936 "query": head.query(),
937 "subprotocols": head.offered_subprotocols(),
938 "answer_with": actions::handshake_answer_with(&head.offered_subprotocols()),
939 "origin": head.header("origin").unwrap_or(""),
940 "headers": head.headers_json(),
941 "client_ip": peer_addr.ip().to_string(),
942 "client_port": peer_addr.port(),
943 }),
944 );
945
946 let result = match call_llm(
947 llm_client,
948 app_state,
949 server_id,
950 Some(connection_id),
951 &event,
952 protocol,
953 )
954 .await
955 {
956 Ok(r) => r,
957 Err(e) => {
958 let tag = match crate::utils::wire_failure::WireFailure::classify(&e) {
962 crate::utils::wire_failure::WireFailure::Overloaded => {
963 "fail_closed_llm_overloaded"
964 }
965 crate::utils::wire_failure::WireFailure::Unavailable => "fail_closed_llm_error",
966 };
967 Log::new(Some(status_tx)).error(format!(
968 "WebSocket upgrade from {} refused decision={}: handler error ({})",
969 peer_addr, tag, e
970 ));
971 return HandshakeDecision::Reject {
972 status: 503,
973 reason: "The upgrade handler failed, so the connection was not opened"
974 .to_string(),
975 };
976 }
977 };
978
979 for msg in result.messages {
980 let _ = status_tx.send(msg);
981 }
982
983 for r in &result.protocol_results {
984 if let ActionResult::Custom { name, data } = r {
985 match name.as_str() {
986 "accept_websocket" => {
987 let subprotocol = data
988 .get("subprotocol")
989 .and_then(|v| v.as_str())
990 .map(|s| s.to_string());
991 Log::new(Some(status_tx)).info(format!(
992 "WebSocket upgrade from {} accepted decision=model_answer{}",
993 peer_addr,
994 subprotocol
995 .as_ref()
996 .map(|s| format!(" (subprotocol {s})"))
997 .unwrap_or_default()
998 ));
999 return HandshakeDecision::Accept { subprotocol };
1000 }
1001 "reject_websocket" => {
1002 let status = data
1003 .get("status_code")
1004 .and_then(|v| v.as_u64())
1005 .unwrap_or(403) as u16;
1006 Log::new(Some(status_tx)).info(format!(
1007 "WebSocket upgrade from {} refused decision=model_reject ({})",
1008 peer_addr, status
1009 ));
1010 return HandshakeDecision::Reject {
1011 status,
1012 reason: data
1013 .get("reason")
1014 .and_then(|v| v.as_str())
1015 .unwrap_or("Upgrade refused")
1016 .to_string(),
1017 };
1018 }
1019 _ => {}
1020 }
1021 }
1022 }
1023
1024 let tag = if result.raw_actions.is_empty() {
1030 "fail_closed_no_action"
1031 } else {
1032 "fail_closed_bad_action"
1033 };
1034 Log::new(Some(status_tx)).error(format!(
1035 "WebSocket upgrade from {} refused decision={}: handler returned neither \
1036 accept_websocket nor reject_websocket",
1037 peer_addr, tag
1038 ));
1039 HandshakeDecision::Reject {
1040 status: 503,
1041 reason: "The upgrade handler did not accept the connection".to_string(),
1042 }
1043 }
1044
1045 #[allow(clippy::too_many_arguments)]
1047 async fn run_connection(
1048 ws_stream: WebSocketStream<deflate::DeflateStream<TcpStream>>,
1049 mut out_rx: mpsc::UnboundedReceiver<WsOut>,
1050 permit: Arc<crate::server::accept_bounded::ConnectionPermit>,
1051 ctx: ConnCtx,
1052 path: String,
1053 peer_addr: SocketAddr,
1054 idle_timeout: std::time::Duration,
1055 deflate: Option<deflate::DeflateParams>,
1056 ) {
1057 let (mut sink, mut stream) = ws_stream.split();
1058
1059 let writer_status_tx = ctx.status_tx.clone();
1063 let writer_conn = ctx.connection_id;
1064 let writer_permit = Arc::clone(&permit);
1068 let writer_handle = tokio::spawn(async move {
1069 let _permit = writer_permit;
1070 while let Some(out) = out_rx.recv().await {
1071 let closing = matches!(out, WsOut::Close { .. });
1072 let message = match out {
1073 WsOut::Text(t) => Message::Text(t),
1074 WsOut::Binary(b) => Message::Binary(b),
1075 WsOut::Ping(p) => Message::Ping(p),
1076 WsOut::Close { code, reason } => Message::Close(Some(CloseFrame {
1077 code: CloseCode::from(code),
1078 reason: reason.into(),
1079 })),
1080 };
1081 if let Err(e) = sink.send(message).await {
1082 Log::new(Some(&writer_status_tx))
1083 .debug(format!("WebSocket write failed on {}: {}", writer_conn, e));
1084 break;
1085 }
1086 if closing {
1087 let _ = sink.flush().await;
1090 break;
1091 }
1092 }
1093 let _ = sink.flush().await;
1094 });
1095
1096 let opened_event = Event::new(
1098 &WEBSOCKET_CONNECTION_OPENED_EVENT,
1099 serde_json::json!({
1100 "path": path,
1101 "answer_with": actions::OPENED_ANSWER_WITH,
1102 "subprotocol": ctx.subprotocol.clone().unwrap_or_default(),
1103 "permessage_deflate": deflate.map(|p| p.to_json()),
1104 "client_ip": peer_addr.ip().to_string(),
1105 "client_port": peer_addr.port(),
1106 }),
1107 );
1108 ctx.protocol.set_speaking_first(true);
1109 let opened_answer = call_llm(
1110 &ctx.llm_client,
1111 &ctx.app_state,
1112 ctx.server_id,
1113 Some(ctx.connection_id),
1114 &opened_event,
1115 ctx.protocol.as_ref(),
1116 )
1117 .await;
1118 ctx.protocol.set_speaking_first(false);
1119 match opened_answer {
1120 Ok(result) => {
1121 for msg in result.messages {
1122 let _ = ctx.status_tx.send(msg);
1123 }
1124 }
1125 Err(e) => {
1126 Log::new(Some(&ctx.status_tx)).warn(format!(
1135 "WebSocket connection-opened handler failed on {} \
1136 decision=llm_error_notice_only: {}",
1137 ctx.connection_id, e
1138 ));
1139 }
1140 }
1141
1142 let mut handlers: JoinSet<()> = JoinSet::new();
1145 let ctx = Arc::new(ctx);
1146
1147 let activity = Arc::new(crate::server::accept_bounded::ConnectionActivity::new());
1150
1151 loop {
1152 let keepalive_tx = ctx.out_tx.clone();
1153 let message = tokio::select! {
1154 message = stream.next() => message,
1155 _ = crate::server::accept_bounded::watch_idle_with_probe(
1156 Arc::clone(&activity),
1157 idle_timeout,
1158 move || {
1159 let _ = keepalive_tx.send(WsOut::Ping(
1160 crate::server::accept_bounded::KEEPALIVE_PING_PAYLOAD.to_vec(),
1161 ));
1162 },
1163 ) => {
1164 Log::new(Some(&ctx.status_tx)).info(format!(
1165 "WebSocket {} sent no frame for {}s, not even a Pong; closing \
1166 decision=idle_timeout",
1167 ctx.connection_id,
1168 idle_timeout.as_secs()
1169 ));
1170 let _ = ctx.out_tx.send(WsOut::Close {
1171 code: 1001,
1172 reason: "idle timeout".to_string(),
1173 });
1174 break;
1175 }
1176 };
1177 let Some(message) = message else { break };
1178 activity.touch();
1179 match message {
1180 Ok(Message::Text(text)) => {
1181 trace!("WebSocket {} <- text {:?}", ctx.connection_id, text);
1182 let ctx = ctx.clone();
1183 let busy = activity.busy();
1184 handlers.spawn(async move {
1185 let _busy = busy;
1186 Self::handle_inbound(ctx, Inbound::Text(text)).await;
1187 });
1188 }
1189 Ok(Message::Binary(bytes)) => {
1190 trace!(
1191 "WebSocket {} <- binary {} bytes: {}",
1192 ctx.connection_id,
1193 bytes.len(),
1194 hex::encode(&bytes)
1195 );
1196 let ctx = ctx.clone();
1197 let busy = activity.busy();
1198 handlers.spawn(async move {
1199 let _busy = busy;
1200 Self::handle_inbound(ctx, Inbound::Binary(bytes)).await;
1201 });
1202 }
1203 Ok(Message::Ping(payload)) => {
1204 let ctx = ctx.clone();
1206 let busy = activity.busy();
1207 handlers.spawn(async move {
1208 let _busy = busy;
1209 Self::handle_inbound(ctx, Inbound::Ping(payload)).await;
1210 });
1211 }
1212 Ok(Message::Pong(_)) => {
1213 debug!("WebSocket {} <- pong", ctx.connection_id);
1214 }
1215 Ok(Message::Close(frame)) => {
1216 let (code, reason) = match &frame {
1217 Some(f) => (u16::from(f.code), f.reason.to_string()),
1218 None => (1005u16, String::new()),
1220 };
1221 Log::new(Some(&ctx.status_tx)).info(format!(
1222 "WebSocket {} closed by client (code {} reason {:?})",
1223 ctx.connection_id, code, reason
1224 ));
1225 Self::handle_close(ctx.clone(), code, reason).await;
1227 break;
1228 }
1229 Ok(Message::Frame(_)) => {}
1230 Err(e) => {
1231 let text = e.to_string();
1232 if text.contains("permessage-deflate") {
1233 Log::new(Some(&ctx.status_tx)).warn(format!(
1237 "WebSocket {} closed: {} decision=permessage_deflate_refused",
1238 ctx.connection_id, text
1239 ));
1240 let _ = ctx.out_tx.send(WsOut::Close {
1241 code: 1009,
1242 reason: "message too big".to_string(),
1243 });
1244 } else {
1245 Log::new(Some(&ctx.status_tx)).debug(format!(
1246 "WebSocket {} read error: {}",
1247 ctx.connection_id, text
1248 ));
1249 }
1250 break;
1251 }
1252 }
1253 }
1254
1255 while handlers.join_next().await.is_some() {}
1258
1259 drop(ctx);
1261 let _ = tokio::time::timeout(std::time::Duration::from_secs(5), writer_handle).await;
1262 drop(permit);
1266 }
1267
1268 async fn handle_inbound(ctx: Arc<ConnCtx>, frame: Inbound) {
1270 {
1271 let mut data = ctx.data.lock().await;
1272 if data.state == HandlerState::Processing {
1273 data.queued.push_back(frame);
1274 Log::new(Some(&ctx.status_tx))
1275 .debug(format!("Queued a message for {}", ctx.connection_id));
1276 return;
1277 }
1278 data.state = HandlerState::Processing;
1279
1280 match data.pending.take() {
1282 Some(pending) => match pending.merge(frame) {
1283 Ok(merged) => data.queued.push_front(merged),
1284 Err((held, new)) => {
1285 data.queued.push_front(new);
1287 data.queued.push_front(held);
1288 }
1289 },
1290 None => data.queued.push_front(frame),
1291 }
1292 }
1293
1294 loop {
1295 let next = {
1296 let mut data = ctx.data.lock().await;
1297 match data.queued.pop_front() {
1298 Some(f) => f,
1299 None => {
1300 data.state = HandlerState::Idle;
1301 return;
1302 }
1303 }
1304 };
1305
1306 let sub = ctx.subprotocol.clone().unwrap_or_default();
1307 let event = match &next {
1308 Inbound::Text(text) => Event::new(
1309 &WEBSOCKET_TEXT_MESSAGE_EVENT,
1310 serde_json::json!({
1311 "text": text,
1312 "message_bytes": text.len(),
1313 "subprotocol": sub,
1314 "answer_with": actions::TEXT_MESSAGE_ANSWER_WITH,
1315 }),
1316 ),
1317 Inbound::Binary(bytes) => {
1318 let (data, encoding) = encode_inbound_payload(bytes);
1319 Event::new(
1320 &WEBSOCKET_BINARY_MESSAGE_EVENT,
1321 serde_json::json!({
1322 "data": data,
1323 "encoding": encoding,
1324 "message_bytes": bytes.len(),
1325 "subprotocol": sub,
1326 }),
1327 )
1328 }
1329 Inbound::Ping(payload) => {
1330 let (data, encoding) = encode_inbound_payload(payload);
1331 Event::new(
1332 &WEBSOCKET_PING_EVENT,
1333 serde_json::json!({ "payload": data, "encoding": encoding }),
1334 )
1335 }
1336 };
1337
1338 match call_llm(
1339 &ctx.llm_client,
1340 &ctx.app_state,
1341 ctx.server_id,
1342 Some(ctx.connection_id),
1343 &event,
1344 ctx.protocol.as_ref(),
1345 )
1346 .await
1347 {
1348 Ok(result) => {
1349 for msg in result.messages {
1350 let _ = ctx.status_tx.send(msg);
1351 }
1352
1353 let mut should_close = false;
1354 let mut should_wait = false;
1355 for r in &result.protocol_results {
1356 match r {
1357 ActionResult::CloseConnection => should_close = true,
1358 ActionResult::WaitForMore => should_wait = true,
1359 _ => {}
1360 }
1361 }
1362
1363 let log = Log::new(Some(&ctx.status_tx));
1368 let subject =
1369 format!("WebSocket {} {}", ctx.connection_id, event.event_type.id);
1370 if result.raw_actions.is_empty() {
1371 log.warn(format!(
1372 "{} decision=model_silent: no action, so no frame was sent",
1373 subject
1374 ));
1375 } else if !result.failures.is_empty() && result.protocol_results.is_empty() {
1376 log.error(format!(
1377 "{} decision=fail_closed_bad_action: {}",
1378 subject,
1379 result
1380 .failures
1381 .iter()
1382 .map(|f| format!("{}: {}", f.action, f.error))
1383 .collect::<Vec<_>>()
1384 .join("; ")
1385 ));
1386 } else if should_close {
1387 log.info(format!(
1388 "{} decision=model_reject: the handler closed the connection",
1389 subject
1390 ));
1391 } else {
1392 log.info(format!("{} decision=model_answer", subject));
1393 }
1394
1395 if should_wait {
1396 let mut data = ctx.data.lock().await;
1397 data.pending = Some(next);
1398 data.state = HandlerState::Accumulating;
1399 Log::new(Some(&ctx.status_tx)).debug(format!(
1400 "Holding a message from {} until more arrives",
1401 ctx.connection_id
1402 ));
1403 return;
1404 }
1405 if should_close {
1406 let mut data = ctx.data.lock().await;
1407 data.state = HandlerState::Idle;
1408 data.queued.clear();
1409 return;
1410 }
1411 }
1412 Err(e) => {
1413 let overloaded = crate::llm::is_overload_error(&e);
1421 let (code, tag) = if overloaded {
1422 (1013u16, "fail_closed_llm_overloaded")
1423 } else {
1424 (1011u16, "fail_closed_llm_error")
1425 };
1426 Log::new(Some(&ctx.status_tx)).error(format!(
1427 "WebSocket {} {} decision={}: handler failed, closing with {} ({})",
1428 ctx.connection_id, event.event_type.id, tag, code, e
1429 ));
1430 let _ = ctx.out_tx.send(WsOut::Close {
1431 code,
1432 reason: "handler failed".to_string(),
1433 });
1434 let mut data = ctx.data.lock().await;
1435 data.state = HandlerState::Idle;
1436 data.queued.clear();
1437 return;
1438 }
1439 }
1440 }
1441 }
1442
1443 async fn handle_close(ctx: Arc<ConnCtx>, code: u16, reason: String) {
1445 let event = Event::new(
1446 &WEBSOCKET_CLOSE_EVENT,
1447 serde_json::json!({ "code": code, "reason": reason }),
1448 );
1449 match call_llm(
1450 &ctx.llm_client,
1451 &ctx.app_state,
1452 ctx.server_id,
1453 Some(ctx.connection_id),
1454 &event,
1455 ctx.protocol.as_ref(),
1456 )
1457 .await
1458 {
1459 Ok(result) => {
1460 for msg in result.messages {
1461 let _ = ctx.status_tx.send(msg);
1462 }
1463 }
1464 Err(e) => {
1465 Log::new(Some(&ctx.status_tx)).warn(format!(
1470 "WebSocket close handler failed on {} decision=llm_error_notice_only: {}",
1471 ctx.connection_id, e
1472 ));
1473 }
1474 }
1475 }
1476
1477 async fn cleanup(
1478 app_state: &Arc<AppState>,
1479 server_id: ServerId,
1480 connection_id: ConnectionId,
1481 status_tx: &mpsc::UnboundedSender<String>,
1482 ) {
1483 actions::unregister_connection(connection_id);
1484 app_state
1485 .close_connection_on_server(server_id, connection_id)
1486 .await;
1487 let _ = status_tx.send("__UPDATE_UI__".to_string());
1488 }
1489}
1490
1491enum HandshakeDecision {
1493 Accept { subprotocol: Option<String> },
1494 Reject { status: u16, reason: String },
1495}
1496
1497async fn read_request_head(socket: &mut TcpStream) -> Result<(Vec<u8>, Vec<u8>)> {
1502 let mut buf: Vec<u8> = Vec::with_capacity(1024);
1503 let mut chunk = [0u8; 1024];
1504
1505 loop {
1506 if let Some(pos) = find_head_end(&buf) {
1507 let leftover = buf.split_off(pos + 4);
1508 buf.truncate(pos);
1509 return Ok((buf, leftover));
1510 }
1511 if buf.len() > MAX_REQUEST_HEAD {
1512 anyhow::bail!("HTTP request head exceeded {MAX_REQUEST_HEAD} bytes");
1513 }
1514 let n = socket
1515 .read(&mut chunk)
1516 .await
1517 .context("read failed while waiting for the HTTP request head")?;
1518 if n == 0 {
1519 anyhow::bail!("peer closed before sending a complete HTTP request head");
1520 }
1521 buf.extend_from_slice(&chunk[..n]);
1522 }
1523}
1524
1525fn find_head_end(buf: &[u8]) -> Option<usize> {
1527 buf.windows(4).position(|w| w == b"\r\n\r\n")
1528}