1pub mod actions;
13
14use crate::llm::action_helper::call_llm;
15use crate::llm::ollama_client::OllamaClient;
16use crate::logging::emit::Log;
17use crate::protocol::Event;
18use crate::server::connection::ConnectionId;
19use crate::state::app_state::AppState;
20use crate::utils::WireFailure;
21use actions::{FingerQuery, FINGER_QUERY_EVENT, MAX_QUERY_BYTES};
22use anyhow::Result;
23use std::net::SocketAddr;
24use std::sync::Arc;
25use std::time::Duration;
26use tokio::io::{AsyncReadExt, AsyncWriteExt};
27use tokio::net::TcpListener;
28use tokio::sync::{mpsc, Mutex};
29
30const FORWARD_DENIED: &[u8] = b"Finger forwarding service denied.\r\n";
32
33const QUERY_TOO_LONG: &[u8] = b"finger: query too long\r\n";
35
36const NO_INFORMATION: &[u8] = b"finger: no information available\r\n";
38
39const QUERY_READ_TIMEOUT: Duration = Duration::from_secs(30);
48
49const MAX_CONNECTIONS: usize = crate::server::accept_bounded::DEFAULT_MAX_CONNECTIONS;
56
57const CONNECTION_CAP_REFUSAL: &[u8] = b"finger: too many connections, try again later\r\n";
65
66pub struct FingerServer;
67
68impl FingerServer {
69 pub async fn spawn_with_llm_actions(
72 listen_addr: SocketAddr,
73 llm_client: OllamaClient,
74 app_state: Arc<AppState>,
75 status_tx: mpsc::UnboundedSender<String>,
76 server_id: crate::state::ServerId,
77 answer_forward_queries: bool,
78 ) -> Result<SocketAddr> {
79 let listener = TcpListener::bind(listen_addr).await?;
80 let local_addr = listener.local_addr()?;
81
82 let log = Log::new(Some(&status_tx));
83 log.info(format!("FINGER server listening on {}", local_addr));
84 if answer_forward_queries {
85 log.warn(
86 "FINGER answer_forward_queries=true: user@host queries will be answered locally \
87 with invented information. Nothing is proxied to the named host.",
88 );
89 } else {
90 log.debug(
91 "FINGER forwarding refused (RFC 1288 3.2.1); set answer_forward_queries=true to \
92 answer user@host locally instead",
93 );
94 }
95
96 let protocol = Arc::new(actions::FingerProtocol::new());
97
98 let task_registrar = app_state.clone();
99 let limiter = crate::server::accept_bounded::ConnectionLimiter::new(MAX_CONNECTIONS);
100 let accept_handle = tokio::spawn(async move {
101 loop {
102 match crate::server::accept_bounded::accept_bounded(
103 &listener,
104 &limiter,
105 CONNECTION_CAP_REFUSAL,
106 "FINGER",
107 Some(&status_tx),
108 )
109 .await
110 {
111 Ok((socket, peer_addr, permit)) => {
112 let connection_id =
113 ConnectionId::new(app_state.get_next_unified_id().await);
114
115 use crate::state::server::{
116 ConnectionState as ServerConnectionState, ConnectionStatus,
117 ProtocolConnectionInfo,
118 };
119 let now = crate::utils::clock::Instant::now();
120 let conn_state = ServerConnectionState {
121 id: connection_id,
122 remote_addr: peer_addr,
123 local_addr,
124 bytes_sent: 0,
125 bytes_received: 0,
126 packets_sent: 0,
127 packets_received: 0,
128 last_activity: now,
129 status: ConnectionStatus::Active,
130 status_changed_at: now,
131 protocol_info: ProtocolConnectionInfo::empty(),
132 };
133 app_state
134 .add_connection_to_server(server_id, conn_state)
135 .await;
136 let _ = status_tx.send("__UPDATE_UI__".to_string());
137
138 Log::new(Some(&status_tx))
139 .info(format!("FINGER client connected from {}", peer_addr));
140
141 let llm_clone = llm_client.clone();
142 let state_clone = app_state.clone();
143 let status_clone = status_tx.clone();
144 let protocol_clone = protocol.clone();
145 let registrar = app_state.clone();
146
147 let conn_handle = tokio::spawn(async move {
148 let _permit = permit;
152 handle_finger_connection(
153 socket,
154 peer_addr,
155 llm_clone,
156 state_clone,
157 status_clone,
158 server_id,
159 protocol_clone,
160 connection_id,
161 answer_forward_queries,
162 )
163 .await
164 });
165
166 registrar.register_server_task(server_id, conn_handle).await;
170 }
171 Err(e) => {
172 Log::new(Some(&status_tx)).error(format!("FINGER accept error: {}", e));
173 break;
174 }
175 }
176 }
177 });
178
179 task_registrar
180 .register_server_task(server_id, accept_handle)
181 .await;
182
183 Ok(local_addr)
184 }
185}
186
187async fn write_counted<W>(
190 write_half: &Arc<Mutex<W>>,
191 data: &[u8],
192 app_state: &AppState,
193 server_id: crate::state::ServerId,
194 connection_id: ConnectionId,
195) -> std::io::Result<()>
196where
197 W: tokio::io::AsyncWrite + Unpin,
198{
199 {
200 let mut writer = write_half.lock().await;
201 writer.write_all(data).await?;
202 writer.flush().await?;
203 }
204 app_state
205 .update_connection_stats(
206 server_id,
207 connection_id,
208 None,
209 Some(data.len() as u64),
210 None,
211 Some(1),
212 )
213 .await;
214 Ok(())
215}
216
217#[allow(clippy::too_many_arguments)]
218async fn handle_finger_connection(
219 socket: tokio::net::TcpStream,
220 peer_addr: SocketAddr,
221 llm_client: OllamaClient,
222 app_state: Arc<AppState>,
223 status_tx: mpsc::UnboundedSender<String>,
224 server_id: crate::state::ServerId,
225 protocol: Arc<actions::FingerProtocol>,
226 connection_id: ConnectionId,
227 answer_forward_queries: bool,
228) {
229 let (reader, write_half) = tokio::io::split(socket);
230 let write_half = Arc::new(Mutex::new(write_half));
231
232 let peer_rx = crate::server::peer_support::register_peer_channel(
236 &app_state,
237 server_id,
238 connection_id.as_u32(),
239 )
240 .await;
241 crate::server::peer_support::spawn_peer_command_task(
242 peer_rx,
243 protocol.clone(),
244 app_state.clone(),
245 server_id,
246 connection_id.as_u32(),
247 write_half.clone(),
248 status_tx.clone(),
249 );
250
251 run_finger_session(
252 reader,
253 &write_half,
254 peer_addr,
255 &llm_client,
256 &app_state,
257 &status_tx,
258 server_id,
259 &protocol,
260 connection_id,
261 answer_forward_queries,
262 )
263 .await;
264
265 app_state
269 .remove_peer_handle(server_id, connection_id.as_u32())
270 .await;
271 let _ = write_half.lock().await.shutdown().await;
272
273 use crate::state::server::ConnectionStatus;
274 app_state
275 .update_connection_status(server_id, connection_id, ConnectionStatus::Closed)
276 .await;
277 let _ = status_tx.send("__UPDATE_UI__".to_string());
278}
279
280enum QueryRead {
282 Line(String, usize),
284 Eof,
286 TooLong,
288 TimedOut,
290 Failed(std::io::Error),
292}
293
294async fn read_query_line<R>(reader: &mut R) -> QueryRead
303where
304 R: tokio::io::AsyncRead + Unpin,
305{
306 let mut accumulated: Vec<u8> = Vec::with_capacity(64);
307 let mut chunk = [0u8; 256];
308
309 loop {
310 let read = match tokio::time::timeout(QUERY_READ_TIMEOUT, reader.read(&mut chunk)).await {
311 Err(_) => return QueryRead::TimedOut,
312 Ok(read) => read,
313 };
314
315 match read {
316 Ok(0) => {
317 return if accumulated.is_empty() {
318 QueryRead::Eof
319 } else {
320 let n = accumulated.len();
321 QueryRead::Line(
322 String::from_utf8_lossy(&accumulated).trim_end().to_string(),
323 n,
324 )
325 };
326 }
327 Ok(n) => {
328 accumulated.extend_from_slice(&chunk[..n]);
329 if let Some(idx) = accumulated.iter().position(|b| *b == b'\n') {
330 let consumed = accumulated.len();
331 let line = String::from_utf8_lossy(&accumulated[..idx])
332 .trim_end_matches('\r')
333 .to_string();
334 return QueryRead::Line(line, consumed);
335 }
336 if accumulated.len() > MAX_QUERY_BYTES {
337 return QueryRead::TooLong;
338 }
339 }
340 Err(e) => return QueryRead::Failed(e),
341 }
342 }
343}
344
345#[allow(clippy::too_many_arguments)]
346async fn run_finger_session<R, W>(
347 mut reader: R,
348 write_half: &Arc<Mutex<W>>,
349 peer_addr: SocketAddr,
350 llm_client: &OllamaClient,
351 app_state: &Arc<AppState>,
352 status_tx: &mpsc::UnboundedSender<String>,
353 server_id: crate::state::ServerId,
354 protocol: &Arc<actions::FingerProtocol>,
355 connection_id: ConnectionId,
356 answer_forward_queries: bool,
357) where
358 R: tokio::io::AsyncRead + Unpin,
359 W: tokio::io::AsyncWrite + Unpin,
360{
361 let log = Log::new(Some(status_tx));
362
363 let (line, consumed) = match read_query_line(&mut reader).await {
364 QueryRead::Line(line, consumed) => (line, consumed),
365 QueryRead::Eof => {
366 log.info(format!(
367 "FINGER client {} disconnected without sending a query",
368 peer_addr
369 ));
370 return;
371 }
372 QueryRead::TooLong => {
373 log.warn(format!(
374 "FINGER query from {} exceeded {} bytes with no terminator; refusing",
375 peer_addr, MAX_QUERY_BYTES
376 ));
377 let _ = write_counted(
378 write_half,
379 QUERY_TOO_LONG,
380 app_state,
381 server_id,
382 connection_id,
383 )
384 .await;
385 return;
386 }
387 QueryRead::TimedOut => {
388 log.info(format!(
391 "FINGER client {} sent no query within {}s; closing",
392 peer_addr,
393 QUERY_READ_TIMEOUT.as_secs()
394 ));
395 return;
396 }
397 QueryRead::Failed(e) => {
398 log.error(format!("FINGER read error from {}: {}", peer_addr, e));
399 return;
400 }
401 };
402
403 app_state
404 .update_connection_stats(
405 server_id,
406 connection_id,
407 Some(consumed as u64),
408 None,
409 Some(1),
410 None,
411 )
412 .await;
413
414 log.debug(format!(
415 "FINGER received {} bytes from {}",
416 consumed, peer_addr
417 ));
418 log.trace(format!("FINGER query line: {}", line));
419
420 let query = FingerQuery::parse(&line);
421
422 if let Some(host) = query.forward_host.as_deref() {
426 if !answer_forward_queries {
427 log.warn(format!(
428 "FINGER decision=forward_refused peer={} user={:?} forward_host={}",
429 peer_addr, query.username, host
430 ));
431 let _ = write_counted(
432 write_half,
433 FORWARD_DENIED,
434 app_state,
435 server_id,
436 connection_id,
437 )
438 .await;
439 return;
440 }
441 log.warn(format!(
442 "FINGER decision=forward_answered_locally peer={} user={:?} forward_host={} \
443 (nothing is contacted; the model invents the answer)",
444 peer_addr, query.username, host
445 ));
446 }
447
448 let event = Event::new(&FINGER_QUERY_EVENT, query.to_event_data());
449
450 log.debug(format!("FINGER calling LLM for query from {}", peer_addr));
451
452 match call_llm(
453 llm_client,
454 app_state,
455 server_id,
456 Some(connection_id),
457 &event,
458 protocol.as_ref(),
459 )
460 .await
461 {
462 Ok(execution_result) => {
463 for message in &execution_result.messages {
464 log.info(message);
465 }
466
467 let mut wrote_output = false;
468 for protocol_result in execution_result.protocol_results {
469 match protocol_result {
470 crate::llm::actions::protocol_trait::ActionResult::Output(output_data) => {
471 if let Err(e) = write_counted(
472 write_half,
473 &output_data,
474 app_state,
475 server_id,
476 connection_id,
477 )
478 .await
479 {
480 log.error(format!("FINGER write error: {}", e));
481 return;
482 }
483 wrote_output = true;
484 log.debug(format!(
485 "FINGER sent {} bytes to {}",
486 output_data.len(),
487 peer_addr
488 ));
489 log.trace(format!(
490 "FINGER response: {}",
491 String::from_utf8_lossy(&output_data)
492 ));
493 log.info(format!(
494 "FINGER response to {} ({} bytes)",
495 peer_addr,
496 output_data.len()
497 ));
498 }
499 crate::llm::actions::protocol_trait::ActionResult::CloseConnection => {
500 log.debug("FINGER closing connection per LLM request");
501 break;
504 }
505 _ => {}
506 }
507 }
508
509 if !wrote_output {
514 log.warn(format!(
515 "FINGER produced no response for {} ({} failed action(s)); answering with a \
516 notice instead of closing silently",
517 peer_addr,
518 execution_result.failures.len()
519 ));
520 let _ = write_counted(
521 write_half,
522 NO_INFORMATION,
523 app_state,
524 server_id,
525 connection_id,
526 )
527 .await;
528 }
529 }
530 Err(e) => {
531 log.warn(format!("FINGER LLM call failed for {}: {}", peer_addr, e));
534 let notice: &[u8] = match WireFailure::classify(&e) {
535 WireFailure::Overloaded => b"finger: backend at capacity, retry later\r\n",
536 WireFailure::Unavailable => b"finger: request could not be processed\r\n",
537 };
538 let _ = write_counted(write_half, notice, app_state, server_id, connection_id).await;
539 }
540 }
541
542 }