Skip to main content

netget/server/finger/
mod.rs

1//! Finger (RFC 1288) server.
2//!
3//! One TCP connection carries exactly one query line and one free-text answer, and then the
4//! server closes — which is what RFC 1288 §2.3 specifies and what every real client relies on,
5//! because a finger client reads until EOF. This server does **not** keep reading after the
6//! answer; see `src/server/finger/CLAUDE.md` for why that differs from the WHOIS
7//! implementation next door.
8//!
9//! Nothing local is ever consulted. There is no `passwd` lookup, no `utmp`, no `.plan` file,
10//! and no filesystem access of any kind: a real finger daemon leaks real accounts, and the
11//! whole point of this one is that the model invents every user.
12pub 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
30/// RFC 1288 §3.2.1's own recommended wording for a refused forwarding query.
31const FORWARD_DENIED: &[u8] = b"Finger forwarding service denied.\r\n";
32
33/// The query line exceeded [`MAX_QUERY_BYTES`] with no terminator in sight.
34const QUERY_TOO_LONG: &[u8] = b"finger: query too long\r\n";
35
36/// The model (or a handler) answered without producing anything for the wire.
37const NO_INFORMATION: &[u8] = b"finger: no information available\r\n";
38
39/// How long to wait for the query line.
40///
41/// [`MAX_QUERY_BYTES`] caps what a peer can make the server *hold*; it does nothing about a
42/// peer that connects and stays quiet, or sends a byte a minute. Either way a task, a socket
43/// and a connection row are held indefinitely by something that has not authenticated — and
44/// RFC 1288 is one line in, one answer out, so a client with nothing to say has nothing to
45/// wait for. Only the read is bounded: once the query has arrived, a manual handler may park
46/// the event for as long as its own timeout allows.
47const QUERY_READ_TIMEOUT: Duration = Duration::from_secs(30);
48
49/// Concurrent connections this server admits before it starts refusing.
50///
51/// [`crate::server::accept_bounded::DEFAULT_MAX_CONNECTIONS`]. [`QUERY_READ_TIMEOUT`] bounds
52/// how long *one* peer can hold a slot doing nothing; only a cap bounds how many such peers
53/// there can be at once, and a finger connection is cheap enough that nothing here argues for
54/// a number other than the house default.
55const MAX_CONNECTIONS: usize = crate::server::accept_bounded::DEFAULT_MAX_CONNECTIONS;
56
57/// What a peer over [`MAX_CONNECTIONS`] is told before the socket closes.
58///
59/// A finger reply is free text read until EOF (RFC 1288 §2.3) — there is no status code to
60/// get wrong and no framing a client can choke on, which is why this server already answers
61/// its other refusals the same way ([`FORWARD_DENIED`], [`QUERY_TOO_LONG`]). The peer has not
62/// sent its query yet, so this arrives as the whole of the answer rather than after one; a
63/// finger client prints it and exits, which is exactly what should happen.
64const CONNECTION_CAP_REFUSAL: &[u8] = b"finger: too many connections, try again later\r\n";
65
66pub struct FingerServer;
67
68impl FingerServer {
69    /// Bind, then serve. Binding is awaited here so a failure is returned rather than lost in
70    /// a detached task — `server_startup` turns the `Err` into `ServerStatus::Error`.
71    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                            // The permit is released when this task ends, and this task is the
149                            // whole of the connection — finger spawns nothing else per peer —
150                            // so `MAX_CONNECTIONS` caps live connections rather than accepts.
151                            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                        // Register every task we spawn, not just the accept loop: aborting a
167                        // task does not abort tasks it spawned, so an unregistered connection
168                        // task keeps its socket alive after stop_server.
169                        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
187/// Write one reply and count it. The guard is dropped before the stats update so nothing
188/// awaits an `AppState` lock while holding the write half.
189async 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    // Registered before the first read: a finger server says nothing until the client speaks,
233    // and a manual `*` rule parks the query for as long as the operator takes to answer it.
234    // The dashboard must be able to reach the connection while it waits.
235    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    // Every exit path lands here. Dropping the handle ends the peer command task, which
266    // releases its clone of the write half; the explicit shutdown makes the FIN immediate,
267    // and a finger client is blocked on exactly that EOF.
268    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
280/// What reading the client's one query line produced.
281enum QueryRead {
282    /// A complete line (terminator stripped) and the number of bytes consumed.
283    Line(String, usize),
284    /// The peer closed without sending anything.
285    Eof,
286    /// More than [`MAX_QUERY_BYTES`] arrived with no terminator.
287    TooLong,
288    /// Nothing arrived within [`QUERY_READ_TIMEOUT`].
289    TimedOut,
290    /// The socket errored.
291    Failed(std::io::Error),
292}
293
294/// Read one CRLF-terminated query.
295///
296/// Lenient about the terminator: a bare LF is accepted, and so is EOF after a partial line —
297/// RFC 1288 requires CRLF and every real client sends it, but a hand-typed `nc` session or a
298/// script using `printf` without `\r` is not worth refusing.
299///
300/// The timeout bounds the wait for *more bytes*, not the whole line, so a client on a slow
301/// link keeps its connection for as long as it is still sending.
302async 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            // Nothing to answer: no query arrived, so there is no request. Returning closes
389            // the socket, which is all the peer was owed.
390            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    // Forwarding is refused *before* the model is consulted, and the refusal is the same
423    // every time. RFC 1288 3.2.1 calls forwarding a security risk; NetGet has no outbound
424    // code path at all, so "refuse" here means "say so and hang up", never "proxy quietly".
425    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                        // The session ends after this response regardless; an explicit close
502                        // just means "and write nothing more".
503                        break;
504                    }
505                    _ => {}
506                }
507            }
508
509            // Nothing reached the wire: the model answered with only a close, or every action
510            // failed. Closing in silence is indistinguishable from a broken server, and the
511            // client is blocked on EOF either way, so say what happened. "No information
512            // available" asserts nothing about any user.
513            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            // The peer gets a category; the log gets the error. Fixed byte literals rather
532            // than a format string, so there is no placeholder an error could ever reach.
533            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    // Return, and the caller shuts the socket down. RFC 1288: one query, one answer, close.
543}