1pub mod actions;
26pub mod wire;
27
28use crate::llm::action_helper::call_llm;
29use crate::llm::actions::protocol_trait::ActionResult;
30use crate::llm::ollama_client::OllamaClient;
31use crate::logging::emit::Log;
32use crate::protocol::{Event, EventType};
33use crate::server::connection::ConnectionId;
34use crate::state::app_state::AppState;
35use anyhow::Result;
36use std::net::SocketAddr;
37use std::sync::Arc;
38use std::time::Duration;
39use tokio::io::{AsyncReadExt, AsyncWriteExt};
40use tokio::net::TcpListener;
41use tokio::sync::{mpsc, Mutex};
42
43pub use wire::MAX_LINE_BYTES;
44
45const FIRST_COMMAND_TIMEOUT: Duration = Duration::from_secs(300);
53
54const IDLE_TIMEOUT: Duration = Duration::from_secs(300);
60
61const MAX_CONNECTIONS: usize = crate::server::accept_bounded::DEFAULT_MAX_CONNECTIONS;
65
66const CONNECTION_CAP_REFUSAL: &[u8] = b"420 Server temporarily unavailable\r\n";
69
70const UNAVAILABLE_REPLY: &[u8] = b"420 Server temporarily unavailable\r\n";
74const OVERLOADED_REPLY: &[u8] = b"420 Server temporarily unavailable, backend at capacity\r\n";
75
76const HELP_LINES: &[&str] = &[
78 "DEFINE database word -- look up word in database",
79 "MATCH database strategy word -- match word in database using strategy",
80 "SHOW DB -- list all accessible databases",
81 "SHOW DATABASES -- list all accessible databases",
82 "SHOW STRAT -- list available matching strategies",
83 "SHOW STRATEGIES -- list available matching strategies",
84 "SHOW INFO database -- provide information about the database",
85 "SHOW SERVER -- provide site-specific information",
86 "OPTION MIME -- use MIME headers",
87 "CLIENT info -- identify client to server",
88 "STATUS -- display timing information",
89 "HELP -- display this help information",
90 "QUIT -- terminate connection",
91];
92
93pub struct DictServer;
94
95impl DictServer {
96 pub async fn spawn_with_llm_actions(
97 listen_addr: SocketAddr,
98 llm_client: OllamaClient,
99 app_state: Arc<AppState>,
100 status_tx: mpsc::UnboundedSender<String>,
101 server_id: crate::state::ServerId,
102 first_byte_timeout_secs: Option<u64>,
103 idle_timeout_secs: Option<u64>,
104 ) -> Result<SocketAddr> {
105 let first_timeout = first_byte_timeout_secs
106 .map(Duration::from_secs)
107 .unwrap_or(FIRST_COMMAND_TIMEOUT);
108 let idle_timeout = idle_timeout_secs
109 .map(Duration::from_secs)
110 .unwrap_or(IDLE_TIMEOUT);
111 let listener = TcpListener::bind(listen_addr).await?;
112 let local_addr = listener.local_addr()?;
113
114 Log::new(Some(&status_tx)).info(format!("DICT server listening on {}", local_addr));
115
116 let protocol = Arc::new(actions::DictProtocol::new());
117 let task_registrar = app_state.clone();
118 let limiter = crate::server::accept_bounded::ConnectionLimiter::new(MAX_CONNECTIONS);
119 let accept_handle = tokio::spawn(async move {
120 loop {
121 match crate::server::accept_bounded::accept_bounded(
122 &listener,
123 &limiter,
124 CONNECTION_CAP_REFUSAL,
125 "DICT",
126 Some(&status_tx),
127 )
128 .await
129 {
130 Ok((socket, peer_addr, permit)) => {
131 let connection_id =
132 ConnectionId::new(app_state.get_next_unified_id().await);
133 use crate::state::server::{
134 ConnectionState as ServerConnectionState, ConnectionStatus,
135 ProtocolConnectionInfo,
136 };
137 let now = crate::utils::clock::Instant::now();
138 app_state
139 .add_connection_to_server(
140 server_id,
141 ServerConnectionState {
142 id: connection_id,
143 remote_addr: peer_addr,
144 local_addr,
145 bytes_sent: 0,
146 bytes_received: 0,
147 packets_sent: 0,
148 packets_received: 0,
149 last_activity: now,
150 status: ConnectionStatus::Active,
151 status_changed_at: now,
152 protocol_info: ProtocolConnectionInfo::empty(),
153 },
154 )
155 .await;
156 let _ = status_tx.send("__UPDATE_UI__".to_string());
157 Log::new(Some(&status_tx))
158 .info(format!("DICT client connected from {}", peer_addr));
159
160 let session = Session {
161 peer_addr,
162 llm_client: llm_client.clone(),
163 app_state: app_state.clone(),
164 status_tx: status_tx.clone(),
165 server_id,
166 protocol: protocol.clone(),
167 connection_id,
168 first_timeout,
169 idle_timeout,
170 };
171 let task_owner = app_state.clone();
173 task_owner
174 .spawn_server_task(server_id, async move {
175 let _permit = permit;
178 session.run(socket).await
179 })
180 .await;
181 }
182 Err(e) => {
183 Log::new(Some(&status_tx)).error(format!("DICT accept error: {}", e));
184 break;
185 }
186 }
187 }
188 });
189
190 task_registrar
191 .register_server_task(server_id, accept_handle)
192 .await;
193 Ok(local_addr)
194 }
195}
196
197#[derive(Debug, Clone, PartialEq, Eq)]
199pub enum Command {
200 Define {
201 database: String,
202 word: String,
203 },
204 Match {
205 database: String,
206 strategy: String,
207 word: String,
208 },
209 ShowDatabases,
210 ShowStrategies,
211 ShowInfo {
212 database: String,
213 },
214 ShowServer,
215 Client,
216 Status,
217 Help,
218 OptionMime,
219 OptionOther,
221 Quit,
222 NotImplemented,
224 BadParameters,
226 Unknown,
228}
229
230pub fn parse_command(line: &str) -> Command {
232 let Ok(args) = wire::split_args(line) else {
233 return Command::BadParameters;
234 };
235 let Some(verb) = args.first() else {
236 return Command::Unknown;
237 };
238 let rest = &args[1..];
239 match verb.to_ascii_uppercase().as_str() {
240 "DEFINE" => match rest {
241 [database, word] => Command::Define {
242 database: database.clone(),
243 word: word.clone(),
244 },
245 _ => Command::BadParameters,
246 },
247 "MATCH" => match rest {
248 [database, strategy, word] => Command::Match {
249 database: database.clone(),
250 strategy: strategy.clone(),
251 word: word.clone(),
252 },
253 _ => Command::BadParameters,
254 },
255 "SHOW" => {
256 let sub = rest.first().map(|s| s.to_ascii_uppercase());
257 match (sub.as_deref(), rest.len()) {
258 (Some("DB") | Some("DATABASES"), 1) => Command::ShowDatabases,
259 (Some("STRAT") | Some("STRATEGIES"), 1) => Command::ShowStrategies,
260 (Some("INFO"), 2) => Command::ShowInfo {
261 database: rest[1].clone(),
262 },
263 (Some("SERVER"), 1) => Command::ShowServer,
264 _ => Command::BadParameters,
265 }
266 }
267 "CLIENT" => Command::Client,
268 "STATUS" => Command::Status,
269 "HELP" => Command::Help,
270 "OPTION" => match rest {
271 [opt] if opt.eq_ignore_ascii_case("MIME") => Command::OptionMime,
272 [] => Command::BadParameters,
273 _ => Command::OptionOther,
274 },
275 "QUIT" => Command::Quit,
276 "AUTH" | "SASLAUTH" | "SASLRESP" => Command::NotImplemented,
277 _ => Command::Unknown,
278 }
279}
280
281impl Command {
282 fn event(&self) -> Option<(&'static EventType, serde_json::Value)> {
284 use actions::{DICT_DEFINE_EVENT, DICT_MATCH_EVENT, DICT_SHOW_EVENT};
285 let (event, data) = match self {
286 Command::Define { database, word } => (
287 &*DICT_DEFINE_EVENT,
288 serde_json::json!({"database": database, "word": word}),
289 ),
290 Command::Match {
291 database,
292 strategy,
293 word,
294 } => (
295 &*DICT_MATCH_EVENT,
296 serde_json::json!({"database": database, "strategy": strategy, "word": word}),
297 ),
298 Command::ShowDatabases => (&*DICT_SHOW_EVENT, serde_json::json!({"what": "databases"})),
299 Command::ShowStrategies => {
300 (&*DICT_SHOW_EVENT, serde_json::json!({"what": "strategies"}))
301 }
302 Command::ShowInfo { database } => (
303 &*DICT_SHOW_EVENT,
304 serde_json::json!({"what": "info", "database": database}),
305 ),
306 Command::ShowServer => (&*DICT_SHOW_EVENT, serde_json::json!({"what": "server"})),
307 _ => return None,
308 };
309 Some((event, data))
310 }
311
312 fn success_code(&self) -> Option<u16> {
314 Some(match self {
315 Command::Define { .. } => 150,
316 Command::Match { .. } => 152,
317 Command::ShowDatabases => 110,
318 Command::ShowStrategies => 111,
319 Command::ShowInfo { .. } => 112,
320 Command::ShowServer => 114,
321 _ => return None,
322 })
323 }
324
325 fn fixed_reply(&self) -> Option<(String, bool)> {
327 let reply = match self {
328 Command::Client | Command::OptionMime => "250 ok\r\n".to_string(),
329 Command::Status => "210 status [netget]\r\n".to_string(),
330 Command::Help => {
331 let lines: Vec<String> = HELP_LINES.iter().map(|s| s.to_string()).collect();
332 let mut out = wire::text_block("113 help text follows", &lines);
333 out.push_str("250 ok\r\n");
334 out
335 }
336 Command::Quit => return Some(("221 Closing Connection\r\n".to_string(), true)),
337 Command::OptionOther => "503 Command parameter not implemented\r\n".to_string(),
338 Command::NotImplemented => "502 Command not implemented\r\n".to_string(),
339 Command::BadParameters => "501 Syntax error, illegal parameters\r\n".to_string(),
340 Command::Unknown => "500 Syntax error, command not recognized\r\n".to_string(),
341 _ => return None,
342 };
343 Some((reply, false))
344 }
345}
346
347struct Session {
348 peer_addr: SocketAddr,
349 llm_client: OllamaClient,
350 app_state: Arc<AppState>,
351 status_tx: mpsc::UnboundedSender<String>,
352 server_id: crate::state::ServerId,
353 protocol: Arc<actions::DictProtocol>,
354 connection_id: ConnectionId,
355 first_timeout: Duration,
356 idle_timeout: Duration,
357}
358
359enum Next {
361 Continue,
362 Close,
363}
364
365impl Session {
366 async fn run(self, socket: tokio::net::TcpStream) {
367 let (mut reader, write_half) = tokio::io::split(socket);
368 let write_half = Arc::new(Mutex::new(write_half));
369
370 let peer_rx = crate::server::peer_support::register_peer_channel(
373 &self.app_state,
374 self.server_id,
375 self.connection_id.as_u32(),
376 )
377 .await;
378 crate::server::peer_support::spawn_peer_command_task(
379 peer_rx,
380 self.protocol.clone(),
381 self.app_state.clone(),
382 self.server_id,
383 self.connection_id.as_u32(),
384 write_half.clone(),
385 self.status_tx.clone(),
386 );
387
388 self.session(&mut reader, &write_half).await;
389
390 self.app_state
391 .remove_peer_handle(self.server_id, self.connection_id.as_u32())
392 .await;
393 let _ = write_half.lock().await.shutdown().await;
394 linger(&mut reader).await;
395 self.app_state
396 .update_connection_status(
397 self.server_id,
398 self.connection_id,
399 crate::state::server::ConnectionStatus::Closed,
400 )
401 .await;
402 let _ = self.status_tx.send("__UPDATE_UI__".to_string());
403 }
404
405 async fn write<W>(&self, write_half: &Arc<Mutex<W>>, data: &[u8]) -> std::io::Result<()>
406 where
407 W: tokio::io::AsyncWrite + Unpin,
408 {
409 {
410 let mut writer = write_half.lock().await;
411 writer.write_all(data).await?;
412 writer.flush().await?;
413 }
414 self.app_state
415 .update_connection_stats(
416 self.server_id,
417 self.connection_id,
418 None,
419 Some(data.len() as u64),
420 None,
421 Some(1),
422 )
423 .await;
424 Ok(())
425 }
426
427 async fn session<R, W>(&self, reader: &mut R, write_half: &Arc<Mutex<W>>)
428 where
429 R: tokio::io::AsyncRead + Unpin,
430 W: tokio::io::AsyncWrite + Unpin,
431 {
432 let log = Log::new(Some(&self.status_tx));
433 let banner = format!(
434 "220 netget DICT server <mime> <{}.{}.{}@netget>\r\n",
435 self.connection_id.as_u32(),
436 crate::utils::clock::process_id(),
437 crate::utils::clock::SystemTime::now()
438 .duration_since(crate::utils::clock::UNIX_EPOCH)
439 .map(|d| d.as_secs())
440 .unwrap_or(0)
441 );
442 if self.write(write_half, banner.as_bytes()).await.is_err() {
443 return;
444 }
445
446 let mut lines = LineReader::new(reader);
447 let mut mime = false;
448 let mut answered_one = false;
449
450 loop {
451 let timeout = if answered_one {
452 self.idle_timeout
453 } else {
454 self.first_timeout
455 };
456 let (line, n) = match lines.next_line(timeout).await {
457 LineRead::Line(line, n) => (line, n),
458 LineRead::Eof => {
459 log.info(format!("DICT client {} disconnected", self.peer_addr));
460 return;
461 }
462 LineRead::TimedOut => {
463 log.info(format!(
464 "DICT client {} sent no command within {}s; closing",
465 self.peer_addr,
466 timeout.as_secs()
467 ));
468 return;
469 }
470 LineRead::TooLong => {
471 log.warn(format!(
472 "DICT command from {} exceeded {} bytes decision=fail_closed_line_too_long",
473 self.peer_addr, MAX_LINE_BYTES
474 ));
475 let _ = self
476 .write(write_half, b"500 Syntax error, command line too long\r\n")
477 .await;
478 return;
479 }
480 LineRead::Failed(e) => {
481 log.error(format!("DICT read error from {}: {}", self.peer_addr, e));
482 return;
483 }
484 };
485 self.app_state
486 .update_connection_stats(
487 self.server_id,
488 self.connection_id,
489 Some(n as u64),
490 None,
491 Some(1),
492 None,
493 )
494 .await;
495 log.trace(format!("DICT command from {}: {:?}", self.peer_addr, line));
496
497 if line.trim().is_empty() {
498 continue;
499 }
500 answered_one = true;
501 let command = parse_command(&line);
502
503 if let Some((reply, close)) = command.fixed_reply() {
504 if matches!(command, Command::OptionMime) {
505 mime = true;
506 }
507 if matches!(command, Command::Client) {
508 log.debug(format!(
509 "DICT client {} identified: {}",
510 self.peer_addr, line
511 ));
512 }
513 if self.write(write_half, reply.as_bytes()).await.is_err() || close {
514 return;
515 }
516 continue;
517 }
518
519 match self.answer_with_model(&command, write_half, mime).await {
520 Next::Continue => {}
521 Next::Close => return,
522 }
523 }
524 }
525
526 async fn answer_with_model<W>(
528 &self,
529 command: &Command,
530 write_half: &Arc<Mutex<W>>,
531 mime: bool,
532 ) -> Next
533 where
534 W: tokio::io::AsyncWrite + Unpin,
535 {
536 let log = Log::new(Some(&self.status_tx));
537 let (Some((event_type, data)), Some(success)) = (command.event(), command.success_code())
538 else {
539 return Next::Continue;
540 };
541 let event = Event::new(event_type, data);
542
543 let result = match call_llm(
544 &self.llm_client,
545 &self.app_state,
546 self.server_id,
547 Some(self.connection_id),
548 &event,
549 self.protocol.as_ref(),
550 )
551 .await
552 {
553 Ok(result) => result,
554 Err(e) => {
555 let (category, reply) = match crate::utils::WireFailure::classify(&e) {
556 crate::utils::WireFailure::Overloaded => ("overloaded", OVERLOADED_REPLY),
557 crate::utils::WireFailure::Unavailable => ("unavailable", UNAVAILABLE_REPLY),
558 };
559 log.warn(format!(
560 "DICT {:?} from {} decision=fail_closed_llm_error category={}",
561 command, self.peer_addr, category
562 ));
563 log.debug(format!("DICT LLM call failed: {}", e));
564 let _ = self.write(write_half, reply).await;
565 return Next::Close;
566 }
567 };
568 for message in &result.messages {
569 log.info(message);
570 }
571
572 let mut replies: Vec<Vec<u8>> = Vec::new();
573 let mut close = false;
574 let mut stack = result.protocol_results;
575 stack.reverse();
576 while let Some(item) = stack.pop() {
577 match item {
578 ActionResult::Output(bytes) => replies.push(bytes),
579 ActionResult::CloseConnection => close = true,
580 ActionResult::Multiple(items) => stack.extend(items.into_iter().rev()),
581 _ => {}
582 }
583 }
584
585 let Some(reply) = replies.first() else {
586 log.warn(format!(
587 "DICT {:?} from {} decision=model_silent ({} failed action(s)); answering 420",
588 command,
589 self.peer_addr,
590 result.failures.len()
591 ));
592 let _ = self.write(write_half, UNAVAILABLE_REPLY).await;
593 return Next::Close;
594 };
595
596 let code = wire::leading_code(reply);
597 let fits = matches!(code, Some(c) if c == success || (500..600).contains(&c));
598 if !fits {
599 log.warn(format!(
600 "DICT {:?} from {} decision=fail_closed_mismatched_reply code={:?} (expected {} \
601 or 5xx); answering 420",
602 command, self.peer_addr, code, success
603 ));
604 let _ = self.write(write_half, UNAVAILABLE_REPLY).await;
605 return Next::Close;
606 }
607 if replies.len() > 1 {
608 log.warn(format!(
609 "DICT {:?}: the model produced {} replies to one command; sending the first",
610 command,
611 replies.len()
612 ));
613 }
614
615 let decision = if code == Some(success) {
616 "model_answer"
617 } else {
618 "model_reject"
619 };
620 log.info(format!(
621 "DICT {:?} from {} decision={} code={}",
622 command,
623 self.peer_addr,
624 decision,
625 code.unwrap_or(0)
626 ));
627
628 let bytes = if mime {
629 wire::apply_mime(reply)
630 } else {
631 reply.clone()
632 };
633 if self.write(write_half, &bytes).await.is_err() || close {
634 return Next::Close;
635 }
636 Next::Continue
637 }
638}
639
640const LINGER_TIME: Duration = Duration::from_secs(2);
649const LINGER_BYTES: usize = 64 * 1024;
650
651async fn linger<R: tokio::io::AsyncRead + Unpin>(reader: &mut R) {
652 let deadline = tokio::time::Instant::now() + LINGER_TIME;
653 let mut sink = [0u8; 4096];
654 let mut drained = 0usize;
655 while drained < LINGER_BYTES {
656 match tokio::time::timeout_at(deadline, reader.read(&mut sink)).await {
657 Ok(Ok(n)) if n > 0 => drained += n,
658 _ => break,
659 }
660 }
661}
662
663enum LineRead {
664 Line(String, usize),
666 Eof,
667 TimedOut,
668 TooLong,
670 Failed(std::io::Error),
671}
672
673struct LineReader<'a, R> {
676 reader: &'a mut R,
677 pending: Vec<u8>,
678 chunk: Vec<u8>,
679}
680
681impl<'a, R: tokio::io::AsyncRead + Unpin> LineReader<'a, R> {
682 fn new(reader: &'a mut R) -> Self {
683 Self {
684 reader,
685 pending: Vec::new(),
686 chunk: vec![0u8; 1024],
687 }
688 }
689
690 async fn next_line(&mut self, timeout: Duration) -> LineRead {
692 loop {
693 if let Some(idx) = self.pending.iter().position(|b| *b == b'\n') {
694 let consumed = idx + 1;
695 if consumed > MAX_LINE_BYTES {
696 return LineRead::TooLong;
697 }
698 let line: Vec<u8> = self.pending.drain(..consumed).collect();
699 let text = String::from_utf8_lossy(&line[..idx]);
700 return LineRead::Line(text.trim_end_matches('\r').to_string(), consumed);
701 }
702 if self.pending.len() >= MAX_LINE_BYTES {
705 return LineRead::TooLong;
706 }
707 let read = match tokio::time::timeout(timeout, self.reader.read(&mut self.chunk)).await
708 {
709 Err(_) => return LineRead::TimedOut,
710 Ok(read) => read,
711 };
712 match read {
713 Ok(0) => {
714 if self.pending.is_empty() {
715 return LineRead::Eof;
716 }
717 let line = std::mem::take(&mut self.pending);
718 let consumed = line.len();
719 let text = String::from_utf8_lossy(&line);
720 return LineRead::Line(text.trim_end_matches('\r').to_string(), consumed);
721 }
722 Ok(n) => self.pending.extend_from_slice(&self.chunk[..n]),
723 Err(e) => return LineRead::Failed(e),
724 }
725 }
726 }
727}