Skip to main content

netget/server/gemini/
mod.rs

1//! Gemini server (gemini://, TLS, port 1965) — the model writes the capsule.
2//!
3//! One connection carries one exchange: TLS handshake, one request line (an absolute URL and
4//! CRLF), one response header, a body only after a 2x, then `close_notify` and close. That
5//! shape decides the whole loop:
6//!
7//! 1. **NetGet refuses what is not a request before the model sees it.** A URL over 1024 bytes,
8//!    a relative or malformed one, a BOM, userinfo or a fragment get `59`; another scheme gets
9//!    `53` (this server is not a proxy). No model call is spent on any of them.
10//! 2. **Every response closes the connection**, so there is no idle phase: the two deadlines
11//!    are the handshake and the request line, and both wrap reads only — a request parked for a
12//!    human under a `manual` rule is not closed by either.
13//! 3. **The model cannot write a header.** Its actions are rendered by [`wire`]; the loop sends
14//!    the first rendered response and nothing after it.
15//! 4. **Failure is a status, never silence.** Backend overload → `41` (the spec's "server
16//!    unavailable due to overload"), any other backend failure → `40` (the generic temporary
17//!    failure), a model that produced no response → `40`. Meta texts are fixed literals.
18pub mod actions;
19pub mod wire;
20
21use crate::llm::action_helper::call_llm;
22use crate::llm::actions::protocol_trait::ActionResult;
23use crate::llm::ollama_client::OllamaClient;
24use crate::logging::emit::Log;
25use crate::protocol::Event;
26use crate::server::connection::ConnectionId;
27use crate::state::app_state::AppState;
28use anyhow::Result;
29use std::net::SocketAddr;
30use std::sync::Arc;
31use std::time::Duration;
32use tokio::io::{AsyncReadExt, AsyncWriteExt};
33use tokio::net::TcpListener;
34use tokio::sync::{mpsc, Mutex};
35use tokio_rustls::TlsAcceptor;
36
37pub use wire::{MAX_REQUEST_BYTES, MAX_URL_BYTES};
38
39/// How long a connected peer has to complete the TLS handshake. Real clients send ClientHello
40/// at once and finish in a round trip; nothing in this phase involves the model.
41const HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(60);
42
43/// How long a peer that completed the handshake has to send its request line.
44///
45/// A real Gemini client sends it in the same flight as its Finished. 300 seconds is the window a
46/// `manual` rule gives a human — the peer may be NetGet's own TLS client, which completes the
47/// handshake and then waits for its operator to type the request.
48const FIRST_BYTE_TIMEOUT: Duration = Duration::from_secs(300);
49
50/// Concurrent connections admitted before new ones are refused — the house default.
51const MAX_CONNECTIONS: usize = crate::server::accept_bounded::DEFAULT_MAX_CONNECTIONS;
52
53/// What a peer over [`MAX_CONNECTIONS`] gets: nothing, then a close. Every Gemini response is
54/// sent inside TLS, and a refused peer is refused precisely so that it does not get a handshake
55/// — plaintext `41` bytes here would be a malformed TLS record, not a refusal. `dot` and `doh`
56/// refuse the same way for the same reason.
57const CONNECTION_CAP_REFUSAL: &[u8] = b"";
58
59/// Responses NetGet writes itself when the model cannot answer. Fixed literals: nothing
60/// derived from an error can reach the peer.
61const UNAVAILABLE_RESPONSE: &[u8] = b"40 request could not be processed\r\n";
62const OVERLOADED_RESPONSE: &[u8] = b"41 backend at capacity, retry later\r\n";
63const TOO_LONG_RESPONSE: &[u8] = b"59 Request too long\r\n";
64
65/// After the last response, how long and how much the server keeps reading before dropping
66/// the socket. Closing over unread input sends RST, which can overtake the response the client
67/// has not read yet — a `59` for an over-long request, typically.
68const LINGER_TIME: Duration = Duration::from_secs(2);
69const LINGER_BYTES: usize = 64 * 1024;
70
71/// The two read deadlines, resolved from startup parameters.
72#[derive(Debug, Clone, Copy)]
73pub struct Deadlines {
74    pub handshake: Duration,
75    pub first_byte: Duration,
76}
77
78impl Deadlines {
79    pub fn new(handshake_secs: Option<u64>, first_byte_secs: Option<u64>) -> Self {
80        Self {
81            handshake: handshake_secs
82                .map(Duration::from_secs)
83                .unwrap_or(HANDSHAKE_TIMEOUT),
84            first_byte: first_byte_secs
85                .map(Duration::from_secs)
86                .unwrap_or(FIRST_BYTE_TIMEOUT),
87        }
88    }
89}
90
91pub struct GeminiServer;
92
93impl GeminiServer {
94    pub async fn spawn_with_llm_actions(
95        listen_addr: SocketAddr,
96        llm_client: OllamaClient,
97        app_state: Arc<AppState>,
98        status_tx: mpsc::UnboundedSender<String>,
99        server_id: crate::state::ServerId,
100        tls_config: Arc<rustls::ServerConfig>,
101        deadlines: Deadlines,
102    ) -> Result<SocketAddr> {
103        let listener = TcpListener::bind(listen_addr).await?;
104        let local_addr = listener.local_addr()?;
105        Log::new(Some(&status_tx)).info(format!("Gemini server listening on {}", local_addr));
106
107        let protocol = Arc::new(actions::GeminiProtocol::new());
108        let acceptor = TlsAcceptor::from(tls_config);
109        let task_registrar = app_state.clone();
110        let limiter = crate::server::accept_bounded::ConnectionLimiter::new(MAX_CONNECTIONS);
111        let accept_handle = tokio::spawn(async move {
112            loop {
113                match crate::server::accept_bounded::accept_bounded(
114                    &listener,
115                    &limiter,
116                    CONNECTION_CAP_REFUSAL,
117                    "Gemini",
118                    Some(&status_tx),
119                )
120                .await
121                {
122                    Ok((socket, peer_addr, permit)) => {
123                        let connection_id =
124                            ConnectionId::new(app_state.get_next_unified_id().await);
125                        use crate::state::server::{
126                            ConnectionState as ServerConnectionState, ConnectionStatus,
127                            ProtocolConnectionInfo,
128                        };
129                        let now = crate::utils::clock::Instant::now();
130                        app_state
131                            .add_connection_to_server(
132                                server_id,
133                                ServerConnectionState {
134                                    id: connection_id,
135                                    remote_addr: peer_addr,
136                                    local_addr,
137                                    bytes_sent: 0,
138                                    bytes_received: 0,
139                                    packets_sent: 0,
140                                    packets_received: 0,
141                                    last_activity: now,
142                                    status: ConnectionStatus::Active,
143                                    status_changed_at: now,
144                                    protocol_info: ProtocolConnectionInfo::empty(),
145                                },
146                            )
147                            .await;
148                        let _ = status_tx.send("__UPDATE_UI__".to_string());
149
150                        let session = Session {
151                            peer_addr,
152                            llm_client: llm_client.clone(),
153                            app_state: app_state.clone(),
154                            status_tx: status_tx.clone(),
155                            server_id,
156                            protocol: protocol.clone(),
157                            connection_id,
158                            deadlines,
159                        };
160                        let acceptor = acceptor.clone();
161                        // Tracked, not detached: stop_server must abort this task too.
162                        let task_owner = app_state.clone();
163                        task_owner
164                            .spawn_server_task(server_id, async move {
165                                // Released when the connection ends; this task is the whole
166                                // connection, so MAX_CONNECTIONS caps live connections.
167                                let _permit = permit;
168                                session.run(acceptor, socket).await
169                            })
170                            .await;
171                    }
172                    Err(e) => {
173                        Log::new(Some(&status_tx)).error(format!("Gemini accept error: {}", e));
174                        break;
175                    }
176                }
177            }
178        });
179
180        task_registrar
181            .register_server_task(server_id, accept_handle)
182            .await;
183        Ok(local_addr)
184    }
185}
186
187struct Session {
188    peer_addr: SocketAddr,
189    llm_client: OllamaClient,
190    app_state: Arc<AppState>,
191    status_tx: mpsc::UnboundedSender<String>,
192    server_id: crate::state::ServerId,
193    protocol: Arc<actions::GeminiProtocol>,
194    connection_id: ConnectionId,
195    deadlines: Deadlines,
196}
197
198/// What reading the request line produced.
199enum RequestRead {
200    Line(String, usize),
201    /// The line exceeded [`MAX_REQUEST_BYTES`] including its CRLF.
202    TooLong,
203    TimedOut,
204    Closed,
205}
206
207impl Session {
208    async fn run(self, acceptor: TlsAcceptor, socket: tokio::net::TcpStream) {
209        let log = Log::new(Some(&self.status_tx));
210        let tls =
211            match tokio::time::timeout(self.deadlines.handshake, acceptor.accept(socket)).await {
212                Err(_) => {
213                    log.info(format!(
214                        "Gemini peer {} did not complete the TLS handshake within {}s; closing",
215                        self.peer_addr,
216                        self.deadlines.handshake.as_secs()
217                    ));
218                    self.finish().await;
219                    return;
220                }
221                Ok(Err(e)) => {
222                    log.warn(format!(
223                        "Gemini TLS handshake with {} failed: {}",
224                        self.peer_addr, e
225                    ));
226                    self.finish().await;
227                    return;
228                }
229                Ok(Ok(tls)) => tls,
230            };
231        log.info(format!("Gemini client connected from {}", self.peer_addr));
232
233        let (mut reader, write_half) = tokio::io::split(tls);
234        let write_half = Arc::new(Mutex::new(write_half));
235
236        // Registered once the handshake is done — before it there is no channel a reply could
237        // travel on — and before the request is read, so the operator can answer a peer whose
238        // request is parked for them.
239        let peer_rx = crate::server::peer_support::register_peer_channel(
240            &self.app_state,
241            self.server_id,
242            self.connection_id.as_u32(),
243        )
244        .await;
245        crate::server::peer_support::spawn_peer_command_task(
246            peer_rx,
247            self.protocol.clone(),
248            self.app_state.clone(),
249            self.server_id,
250            self.connection_id.as_u32(),
251            write_half.clone(),
252            self.status_tx.clone(),
253        );
254
255        self.exchange(&mut reader, &write_half).await;
256
257        self.app_state
258            .remove_peer_handle(self.server_id, self.connection_id.as_u32())
259            .await;
260        // close_notify, then drain what the peer already sent so the TCP close is a FIN.
261        let _ = write_half.lock().await.shutdown().await;
262        let deadline = tokio::time::Instant::now() + LINGER_TIME;
263        let mut sink = [0u8; 4096];
264        let mut drained = 0usize;
265        while drained < LINGER_BYTES {
266            match tokio::time::timeout_at(deadline, reader.read(&mut sink)).await {
267                Ok(Ok(n)) if n > 0 => drained += n,
268                _ => break,
269            }
270        }
271        self.finish().await;
272    }
273
274    async fn finish(&self) {
275        self.app_state
276            .update_connection_status(
277                self.server_id,
278                self.connection_id,
279                crate::state::server::ConnectionStatus::Closed,
280            )
281            .await;
282        let _ = self.status_tx.send("__UPDATE_UI__".to_string());
283    }
284
285    async fn write<W>(&self, write_half: &Arc<Mutex<W>>, data: &[u8]) -> std::io::Result<()>
286    where
287        W: tokio::io::AsyncWrite + Unpin,
288    {
289        {
290            let mut writer = write_half.lock().await;
291            writer.write_all(data).await?;
292            writer.flush().await?;
293        }
294        self.app_state
295            .update_connection_stats(
296                self.server_id,
297                self.connection_id,
298                None,
299                Some(data.len() as u64),
300                None,
301                Some(1),
302            )
303            .await;
304        Ok(())
305    }
306
307    /// Read one request line: up to CRLF (or a bare LF), bounded in size and in time.
308    async fn read_request<R: tokio::io::AsyncRead + Unpin>(&self, reader: &mut R) -> RequestRead {
309        let mut pending: Vec<u8> = Vec::new();
310        let mut chunk = [0u8; 1024];
311        loop {
312            if let Some(idx) = pending.iter().position(|b| *b == b'\n') {
313                if idx + 1 > MAX_REQUEST_BYTES {
314                    return RequestRead::TooLong;
315                }
316                let line = String::from_utf8_lossy(&pending[..idx]);
317                return RequestRead::Line(line.trim_end_matches('\r').to_string(), idx + 1);
318            }
319            if pending.len() >= MAX_REQUEST_BYTES {
320                return RequestRead::TooLong;
321            }
322            match tokio::time::timeout(self.deadlines.first_byte, reader.read(&mut chunk)).await {
323                Err(_) => return RequestRead::TimedOut,
324                Ok(Ok(0)) | Ok(Err(_)) => return RequestRead::Closed,
325                Ok(Ok(n)) => pending.extend_from_slice(&chunk[..n]),
326            }
327        }
328    }
329
330    async fn exchange<R, W>(&self, reader: &mut R, write_half: &Arc<Mutex<W>>)
331    where
332        R: tokio::io::AsyncRead + Unpin,
333        W: tokio::io::AsyncWrite + Unpin,
334    {
335        let log = Log::new(Some(&self.status_tx));
336        let (line, n) = match self.read_request(reader).await {
337            RequestRead::Line(line, n) => (line, n),
338            RequestRead::TooLong => {
339                log.warn(format!(
340                    "Gemini request from {} exceeded {} bytes decision=fail_closed_request_too_long",
341                    self.peer_addr, MAX_REQUEST_BYTES
342                ));
343                let _ = self.write(write_half, TOO_LONG_RESPONSE).await;
344                return;
345            }
346            RequestRead::TimedOut => {
347                log.info(format!(
348                    "Gemini peer {} sent no request within {}s; closing",
349                    self.peer_addr,
350                    self.deadlines.first_byte.as_secs()
351                ));
352                return;
353            }
354            RequestRead::Closed => {
355                log.info(format!(
356                    "Gemini peer {} closed without a request",
357                    self.peer_addr
358                ));
359                return;
360            }
361        };
362        self.app_state
363            .update_connection_stats(
364                self.server_id,
365                self.connection_id,
366                Some(n as u64),
367                None,
368                Some(1),
369                None,
370            )
371            .await;
372
373        let request = match wire::parse_request(&line) {
374            Ok(request) => request,
375            Err(refusal) => {
376                log.info(format!(
377                    "Gemini request from {} refused decision={}",
378                    self.peer_addr,
379                    refusal.decision()
380                ));
381                let _ = self.write(write_half, refusal.response().as_bytes()).await;
382                return;
383            }
384        };
385
386        let event = Event::new(
387            &actions::GEMINI_REQUEST_EVENT,
388            serde_json::json!({
389                "url": request.url,
390                "host": request.host,
391                "path": request.path,
392                "query": request.query,
393                "answer_with": actions::answer_with_for_request(
394                    &request.path,
395                    request.query.as_deref(),
396                ),
397            }),
398        );
399        let result = match call_llm(
400            &self.llm_client,
401            &self.app_state,
402            self.server_id,
403            Some(self.connection_id),
404            &event,
405            self.protocol.as_ref(),
406        )
407        .await
408        {
409            Ok(result) => result,
410            Err(e) => {
411                let (category, response) = match crate::utils::WireFailure::classify(&e) {
412                    crate::utils::WireFailure::Overloaded => ("overloaded", OVERLOADED_RESPONSE),
413                    crate::utils::WireFailure::Unavailable => ("unavailable", UNAVAILABLE_RESPONSE),
414                };
415                log.warn(format!(
416                    "Gemini {} from {} decision=fail_closed_llm_error category={}",
417                    request.url, self.peer_addr, category
418                ));
419                log.debug(format!("Gemini LLM call failed: {}", e));
420                let _ = self.write(write_half, response).await;
421                return;
422            }
423        };
424        for message in &result.messages {
425            log.info(message);
426        }
427
428        let mut responses: Vec<Vec<u8>> = Vec::new();
429        let mut stack = result.protocol_results;
430        stack.reverse();
431        while let Some(item) = stack.pop() {
432            match item {
433                ActionResult::Output(bytes) => responses.push(bytes),
434                ActionResult::Multiple(items) => stack.extend(items.into_iter().rev()),
435                _ => {}
436            }
437        }
438
439        let Some(response) = responses.first() else {
440            log.warn(format!(
441                "Gemini {} from {} decision=model_silent ({} failed action(s)); answering 40",
442                request.url,
443                self.peer_addr,
444                result.failures.len()
445            ));
446            let _ = self.write(write_half, UNAVAILABLE_RESPONSE).await;
447            return;
448        };
449        if responses.len() > 1 {
450            log.warn(format!(
451                "Gemini {}: the model produced {} responses to one request; sending the first",
452                request.url,
453                responses.len()
454            ));
455        }
456        let status = wire::leading_status(response).unwrap_or(0);
457        let decision = if (10..40).contains(&status) {
458            "model_answer"
459        } else {
460            "model_reject"
461        };
462        log.info(format!(
463            "Gemini {} from {} decision={} status={}",
464            request.url, self.peer_addr, decision, status
465        ));
466        if let Err(e) = self.write(write_half, response).await {
467            log.debug(format!("Gemini write to {} failed: {}", self.peer_addr, e));
468        }
469    }
470}