1pub mod actions;
39pub mod messages;
40pub mod packstream;
41pub mod values;
42
43use crate::llm::action_helper::call_llm;
44use crate::llm::actions::protocol_trait::ActionResult;
45use crate::llm::ollama_client::OllamaClient;
46use crate::logging::emit::Log;
47use crate::protocol::Event;
48use crate::server::connection::ConnectionId;
49use crate::state::app_state::AppState;
50use anyhow::Result;
51use messages::Request;
52use packstream::{Dechunker, Value};
53use std::collections::VecDeque;
54use std::net::SocketAddr;
55use std::sync::Arc;
56use std::time::Duration;
57use tokio::io::{AsyncReadExt, AsyncWriteExt};
58use tokio::net::TcpListener;
59use tokio::sync::{mpsc, Mutex};
60
61pub use packstream::{MAX_MESSAGE_BYTES, MAX_PACKSTREAM_DEPTH};
62
63pub const DEFAULT_NEO4J_VERSION: &str = "5.26.0";
65
66pub const FIRST_BYTE_TIMEOUT: Duration = Duration::from_secs(30);
70
71pub const IDLE_TIMEOUT: Duration = Duration::from_secs(300);
75
76const MAX_CONNECTIONS: usize = crate::server::accept_bounded::DEFAULT_MAX_CONNECTIONS;
79
80const CONNECTION_CAP_REFUSAL: &[u8] = b"";
83
84const REQUEST_INVALID: &str = "Neo.ClientError.Request.Invalid";
85const DATABASE_UNAVAILABLE: &str = "Neo.TransientError.General.DatabaseUnavailable";
86const UNAVAILABLE_MESSAGE: &str = "The database is unavailable: the query could not be answered.";
89const OVERLOADED_MESSAGE: &str = "The database is at capacity; retry the query later.";
90const UNAUTHORIZED_MESSAGE: &str = "The client is unauthorized due to authentication failure.";
91
92#[derive(Debug, Clone)]
94pub struct BoltConfig {
95 pub password: Option<String>,
96 pub neo4j_version: String,
97 pub first_byte_timeout: Duration,
98 pub idle_timeout: Duration,
99}
100
101pub struct BoltServer;
102
103impl BoltServer {
104 pub async fn spawn_with_llm_actions(
105 listen_addr: SocketAddr,
106 llm_client: OllamaClient,
107 app_state: Arc<AppState>,
108 status_tx: mpsc::UnboundedSender<String>,
109 server_id: crate::state::ServerId,
110 config: BoltConfig,
111 ) -> Result<SocketAddr> {
112 let listener = TcpListener::bind(listen_addr).await?;
113 let local_addr = listener.local_addr()?;
114 Log::new(Some(&status_tx)).info(format!(
115 "Bolt server listening on {} (Neo4j/{})",
116 local_addr, config.neo4j_version
117 ));
118
119 let config = Arc::new(config);
120 let protocol = Arc::new(actions::BoltProtocol::new());
121 let task_registrar = app_state.clone();
122 let limiter = crate::server::accept_bounded::ConnectionLimiter::new(MAX_CONNECTIONS);
123 let accept_handle = tokio::spawn(async move {
124 loop {
125 match crate::server::accept_bounded::accept_bounded(
126 &listener,
127 &limiter,
128 CONNECTION_CAP_REFUSAL,
129 "Bolt",
130 Some(&status_tx),
131 )
132 .await
133 {
134 Ok((socket, peer_addr, permit)) => {
135 let connection_id =
136 ConnectionId::new(app_state.get_next_unified_id().await);
137 use crate::state::server::{
138 ConnectionState as ServerConnectionState, ConnectionStatus,
139 ProtocolConnectionInfo,
140 };
141 let now = crate::utils::clock::Instant::now();
142 app_state
143 .add_connection_to_server(
144 server_id,
145 ServerConnectionState {
146 id: connection_id,
147 remote_addr: peer_addr,
148 local_addr,
149 bytes_sent: 0,
150 bytes_received: 0,
151 packets_sent: 0,
152 packets_received: 0,
153 last_activity: now,
154 status: ConnectionStatus::Active,
155 status_changed_at: now,
156 protocol_info: ProtocolConnectionInfo::empty(),
157 },
158 )
159 .await;
160 let _ = status_tx.send("__UPDATE_UI__".to_string());
161 Log::new(Some(&status_tx))
162 .info(format!("Bolt client connected from {}", peer_addr));
163
164 let session = Session {
165 peer_addr,
166 local_addr,
167 llm_client: llm_client.clone(),
168 app_state: app_state.clone(),
169 status_tx: status_tx.clone(),
170 server_id,
171 protocol: protocol.clone(),
172 connection_id,
173 config: config.clone(),
174 };
175 let task_owner = app_state.clone();
177 task_owner
178 .spawn_server_task(server_id, async move {
179 let _permit = permit;
182 session.run(socket).await
183 })
184 .await;
185 }
186 Err(e) => {
187 Log::new(Some(&status_tx)).error(format!("Bolt accept error: {}", e));
188 break;
189 }
190 }
191 }
192 });
193
194 task_registrar
195 .register_server_task(server_id, accept_handle)
196 .await;
197 Ok(local_addr)
198 }
199}
200
201#[derive(Debug, Clone, Copy, PartialEq, Eq)]
203enum Phase {
204 Connected,
206 Authentication,
208 Ready,
209 Streaming,
211 TxReady,
212 TxStreaming,
214 Failed,
215}
216
217impl Phase {
218 fn wire_name(self) -> &'static str {
219 match self {
220 Phase::Connected => "CONNECTED",
221 Phase::Authentication => "AUTHENTICATION",
222 Phase::Ready => "READY",
223 Phase::Streaming => "STREAMING",
224 Phase::TxReady => "TX_READY",
225 Phase::TxStreaming => "TX_STREAMING",
226 Phase::Failed => "FAILED",
227 }
228 }
229}
230
231struct OpenResult {
233 qid: i64,
234 records: VecDeque<Vec<Value>>,
235 query_type: &'static str,
236 stats: Option<Value>,
237 db: String,
238}
239
240struct Transaction {
241 db: Option<String>,
242 read: bool,
243}
244
245struct Conn {
247 minor: u8,
248 phase: Phase,
249 authenticated: bool,
250 user_agent: String,
251 routing_address: Option<String>,
254 tx: Option<Transaction>,
255 results: Vec<OpenResult>,
256 next_qid: i64,
257}
258
259impl Conn {
260 fn new(minor: u8) -> Self {
261 Self {
262 minor,
263 phase: Phase::Connected,
264 authenticated: false,
265 user_agent: String::new(),
266 routing_address: None,
267 tx: None,
268 results: Vec::new(),
269 next_qid: 0,
270 }
271 }
272}
273
274enum Flow {
276 Continue,
277 Close,
278}
279
280enum QueryOutcome {
282 Answer(values::QueryAnswer),
283 Failed,
285 Close,
286}
287
288enum Login {
290 Accepted,
291 Refused,
293 Closed,
294}
295
296struct Session {
297 peer_addr: SocketAddr,
298 local_addr: SocketAddr,
299 llm_client: OllamaClient,
300 app_state: Arc<AppState>,
301 status_tx: mpsc::UnboundedSender<String>,
302 server_id: crate::state::ServerId,
303 protocol: Arc<actions::BoltProtocol>,
304 connection_id: ConnectionId,
305 config: Arc<BoltConfig>,
306}
307
308#[derive(Default)]
310struct Out {
311 bytes: Vec<u8>,
312 messages: u64,
313}
314
315impl Out {
316 fn push(&mut self, message: &Value) {
317 self.bytes.extend(packstream::message_bytes(message));
318 self.messages += 1;
319 }
320}
321
322fn is_reset(raw: &[u8]) -> bool {
323 raw.len() >= 2 && raw[0] == 0xB0 && raw[1] == messages::RESET
324}
325
326impl Session {
327 async fn run(self, socket: tokio::net::TcpStream) {
328 let (mut reader, write_half) = tokio::io::split(socket);
329 let write_half = Arc::new(Mutex::new(write_half));
330
331 let peer_rx = crate::server::peer_support::register_peer_channel(
335 &self.app_state,
336 self.server_id,
337 self.connection_id.as_u32(),
338 )
339 .await;
340 crate::server::peer_support::spawn_peer_command_task(
341 peer_rx,
342 self.protocol.clone(),
343 self.app_state.clone(),
344 self.server_id,
345 self.connection_id.as_u32(),
346 write_half.clone(),
347 self.status_tx.clone(),
348 );
349
350 self.session(&mut reader, &write_half).await;
351
352 self.app_state
353 .remove_peer_handle(self.server_id, self.connection_id.as_u32())
354 .await;
355 let _ = write_half.lock().await.shutdown().await;
356 linger(&mut reader).await;
357 self.app_state
358 .update_connection_status(
359 self.server_id,
360 self.connection_id,
361 crate::state::server::ConnectionStatus::Closed,
362 )
363 .await;
364 let _ = self.status_tx.send("__UPDATE_UI__".to_string());
365 }
366
367 fn log(&self) -> Log<'_> {
368 Log::new(Some(&self.status_tx))
369 }
370
371 async fn write<W>(&self, write_half: &Arc<Mutex<W>>, data: &[u8], messages: u64) -> bool
372 where
373 W: tokio::io::AsyncWrite + Unpin,
374 {
375 if data.is_empty() {
376 return true;
377 }
378 let ok = {
379 let mut writer = write_half.lock().await;
380 writer.write_all(data).await.is_ok() && writer.flush().await.is_ok()
381 };
382 self.app_state
383 .update_connection_stats(
384 self.server_id,
385 self.connection_id,
386 None,
387 Some(data.len() as u64),
388 None,
389 Some(messages.max(1)),
390 )
391 .await;
392 ok
393 }
394
395 async fn received(&self, bytes: usize) {
396 self.app_state
397 .update_connection_stats(
398 self.server_id,
399 self.connection_id,
400 Some(bytes as u64),
401 None,
402 Some(1),
403 None,
404 )
405 .await;
406 }
407
408 async fn session<R, W>(&self, reader: &mut R, write_half: &Arc<Mutex<W>>)
409 where
410 R: tokio::io::AsyncRead + Unpin,
411 W: tokio::io::AsyncWrite + Unpin,
412 {
413 let Some(minor) = self.handshake(reader, write_half).await else {
414 return;
415 };
416 let mut conn = Conn::new(minor);
417 let mut dechunker = Dechunker::new(MAX_MESSAGE_BYTES);
418 let mut queue: VecDeque<Vec<u8>> = VecDeque::new();
419 let mut buf = vec![0u8; 16 * 1024];
420
421 loop {
422 loop {
423 match dechunker.next_message() {
424 Ok(Some(message)) => queue.push_back(message),
425 Ok(None) => break,
426 Err(packstream::FrameError::TooLarge { limit }) => {
427 self.log().warn(format!(
428 "Bolt message from {} exceeds {} bytes \
429 decision=fail_closed_message_too_large",
430 self.peer_addr, limit
431 ));
432 let mut out = Out::default();
433 out.push(&messages::failure(
434 conn.minor,
435 REQUEST_INVALID,
436 &format!("Message exceeds the {limit}-byte limit of this server."),
437 ));
438 self.write(write_half, &out.bytes, out.messages).await;
439 return;
440 }
441 }
442 }
443
444 let Some(raw) = queue.pop_front() else {
445 let read =
446 tokio::time::timeout(self.config.idle_timeout, reader.read(&mut buf)).await;
447 match read {
448 Err(_) => {
449 self.log().info(format!(
450 "Bolt client {} sent nothing for {}s; closing",
451 self.peer_addr,
452 self.config.idle_timeout.as_secs()
453 ));
454 return;
455 }
456 Ok(Ok(0)) => {
457 self.log()
458 .info(format!("Bolt client {} disconnected", self.peer_addr));
459 return;
460 }
461 Ok(Ok(n)) => {
462 self.received(n).await;
463 dechunker.push(&buf[..n]);
464 }
465 Ok(Err(e)) => {
466 self.log()
467 .error(format!("Bolt read error from {}: {}", self.peer_addr, e));
468 return;
469 }
470 }
471 continue;
472 };
473
474 let interrupted = queue.iter().any(|m| is_reset(m));
475 let mut out = Out::default();
476 let flow = self.handle(&raw, interrupted, &mut conn, &mut out).await;
477 if !self.write(write_half, &out.bytes, out.messages).await {
478 return;
479 }
480 if matches!(flow, Flow::Close) {
481 return;
482 }
483 }
484 }
485
486 async fn handshake<R, W>(&self, reader: &mut R, write_half: &Arc<Mutex<W>>) -> Option<u8>
488 where
489 R: tokio::io::AsyncRead + Unpin,
490 W: tokio::io::AsyncWrite + Unpin,
491 {
492 let mut hs = [0u8; 20];
493 match tokio::time::timeout(self.config.first_byte_timeout, reader.read_exact(&mut hs)).await
494 {
495 Err(_) => {
496 self.log().info(format!(
497 "Bolt client {} sent no handshake within {}s; closing",
498 self.peer_addr,
499 self.config.first_byte_timeout.as_secs()
500 ));
501 return None;
502 }
503 Ok(Err(_)) => {
504 self.log().info(format!(
505 "Bolt client {} closed before completing the handshake",
506 self.peer_addr
507 ));
508 return None;
509 }
510 Ok(Ok(_)) => {}
511 }
512 self.received(hs.len()).await;
513 if hs[..4] != messages::MAGIC {
514 self.log().warn(format!(
515 "Bolt client {} did not open with the Bolt magic (got {:02X?}) \
516 decision=fail_closed_bad_magic",
517 self.peer_addr,
518 &hs[..4]
519 ));
520 return None;
521 }
522 let mut proposals = [0u8; 16];
523 proposals.copy_from_slice(&hs[4..]);
524 match messages::negotiate(&proposals) {
525 Some((major, minor)) => {
526 self.log().debug(format!(
527 "Bolt client {} proposed {:02X?}; speaking {}.{}",
528 self.peer_addr, proposals, major, minor
529 ));
530 if !self.write(write_half, &[0, 0, minor, major], 1).await {
531 return None;
532 }
533 Some(minor)
534 }
535 None => {
536 self.log().warn(format!(
537 "Bolt client {} proposed no version in 5.{}..=5.{} ({:02X?}) \
538 decision=fail_closed_no_common_version",
539 self.peer_addr,
540 messages::MIN_MINOR,
541 messages::MAX_MINOR,
542 proposals
543 ));
544 self.write(write_half, &[0, 0, 0, 0], 1).await;
545 None
546 }
547 }
548 }
549
550 fn protocol_violation(&self, conn: &Conn, out: &mut Out, what: String) -> Flow {
552 self.log().warn(format!(
553 "Bolt client {}: {} decision=fail_closed_protocol_violation",
554 self.peer_addr, what
555 ));
556 out.push(&messages::failure(conn.minor, REQUEST_INVALID, &what));
557 Flow::Close
558 }
559
560 async fn handle(&self, raw: &[u8], interrupted: bool, conn: &mut Conn, out: &mut Out) -> Flow {
561 let value = match packstream::decode(raw) {
562 Ok(v) => v,
563 Err(e) => {
564 let decision = if e == packstream::DecodeError::TooDeep {
565 "fail_closed_too_deep"
566 } else {
567 "fail_closed_malformed_message"
568 };
569 self.log().warn(format!(
570 "Bolt message from {} is not PackStream: {} decision={}",
571 self.peer_addr, e, decision
572 ));
573 out.push(&messages::failure(
574 conn.minor,
575 REQUEST_INVALID,
576 &format!("Invalid message: {e}"),
577 ));
578 return Flow::Close;
579 }
580 };
581 let request = match messages::parse_request(value) {
582 Ok(r) => r,
583 Err(e) => return self.protocol_violation(conn, out, format!("Invalid message: {e}")),
584 };
585 self.log().trace(format!(
586 "Bolt {} from {} in {}",
587 request.name(),
588 self.peer_addr,
589 conn.phase.wire_name()
590 ));
591
592 match (&request, conn.phase) {
593 (Request::Goodbye, _) => {
594 self.log()
595 .debug(format!("Bolt client {} said GOODBYE", self.peer_addr));
596 return Flow::Close;
597 }
598 (Request::Reset, Phase::Connected) => {}
599 (Request::Reset, _) => {
600 conn.tx = None;
601 conn.results.clear();
602 conn.phase = if conn.authenticated {
603 Phase::Ready
604 } else {
605 Phase::Authentication
606 };
607 out.push(&messages::success(Vec::new()));
608 return Flow::Continue;
609 }
610 (_, _) if interrupted => {
612 out.push(&messages::ignored());
613 return Flow::Continue;
614 }
615 (_, Phase::Failed) => {
616 out.push(&messages::ignored());
617 return Flow::Continue;
618 }
619 _ => {}
620 }
621
622 match (request, conn.phase) {
623 (Request::Hello { extra }, Phase::Connected) => self.hello(extra, conn, out).await,
624 (Request::Logon { auth }, Phase::Authentication) if conn.minor >= 1 => {
625 match self.authenticate(&auth, conn, out).await {
626 Login::Accepted => {
627 conn.authenticated = true;
628 conn.phase = Phase::Ready;
629 out.push(&messages::success(Vec::new()));
630 Flow::Continue
631 }
632 Login::Refused | Login::Closed => Flow::Close,
633 }
634 }
635 (Request::Logoff, Phase::Ready) if conn.minor >= 1 => {
636 conn.authenticated = false;
637 conn.phase = Phase::Authentication;
638 out.push(&messages::success(Vec::new()));
639 Flow::Continue
640 }
641 (
642 Request::Run {
643 query,
644 params,
645 extra,
646 },
647 Phase::Ready,
648 ) => self.run_query(&query, ¶ms, &extra, conn, out).await,
649 (
650 Request::Run {
651 query,
652 params,
653 extra,
654 },
655 Phase::TxReady | Phase::TxStreaming,
656 ) => self.run_query(&query, ¶ms, &extra, conn, out).await,
657 (Request::Pull { n, qid }, Phase::Streaming | Phase::TxStreaming) => {
658 self.stream(n, qid, false, conn, out)
659 }
660 (Request::Discard { n, qid }, Phase::Streaming | Phase::TxStreaming) => {
661 self.stream(n, qid, true, conn, out)
662 }
663 (Request::Begin { extra }, Phase::Ready) => {
664 conn.tx = Some(Transaction {
665 db: extra.get("db").and_then(Value::as_str).map(str::to_string),
666 read: extra.get("mode").and_then(Value::as_str) == Some("r"),
667 });
668 conn.phase = Phase::TxReady;
669 out.push(&messages::success(Vec::new()));
670 Flow::Continue
671 }
672 (Request::Commit, Phase::TxReady) => {
673 conn.tx = None;
674 conn.phase = Phase::Ready;
675 out.push(&messages::success(vec![(
676 "bookmark",
677 Value::String(self.bookmark()),
678 )]));
679 Flow::Continue
680 }
681 (Request::Rollback, Phase::TxReady) => {
682 conn.tx = None;
683 conn.phase = Phase::Ready;
684 out.push(&messages::success(Vec::new()));
685 Flow::Continue
686 }
687 (Request::Route { routing, extra }, Phase::Ready) => {
688 out.push(&self.routing_table(&routing, &extra, conn));
689 Flow::Continue
690 }
691 (Request::Telemetry, Phase::Ready) if conn.minor >= 4 => {
692 out.push(&messages::success(Vec::new()));
693 Flow::Continue
694 }
695 (request, phase) => self.protocol_violation(
696 conn,
697 out,
698 format!(
699 "Message '{}' cannot be handled by a session in the {} state.",
700 request.name(),
701 phase.wire_name()
702 ),
703 ),
704 }
705 }
706
707 fn server_agent(&self) -> String {
708 format!("Neo4j/{}", self.config.neo4j_version)
709 }
710
711 fn bookmark(&self) -> String {
712 format!(
713 "FB:netget:{}:{}",
714 self.server_id.as_u32(),
715 crate::utils::clock::SystemTime::now()
716 .duration_since(crate::utils::clock::UNIX_EPOCH)
717 .map(|d| d.as_millis())
718 .unwrap_or(0)
719 )
720 }
721
722 async fn hello(&self, extra: Value, conn: &mut Conn, out: &mut Out) -> Flow {
723 conn.user_agent = extra
724 .get("user_agent")
725 .and_then(Value::as_str)
726 .unwrap_or("")
727 .to_string();
728 conn.routing_address = extra
729 .get("routing")
730 .and_then(|r| r.get("address"))
731 .and_then(Value::as_str)
732 .map(str::to_string);
733 self.log().info(format!(
734 "Bolt 5.{} HELLO from {} ({})",
735 conn.minor, self.peer_addr, conn.user_agent
736 ));
737 let success = messages::success(vec![
738 ("server", Value::String(self.server_agent())),
739 (
740 "connection_id",
741 Value::String(format!("bolt-{}", self.connection_id.as_u32())),
742 ),
743 ("hints", Value::Map(Vec::new())),
744 ]);
745 if conn.minor == 0 {
746 match self.authenticate(&extra, conn, out).await {
748 Login::Accepted => {
749 conn.authenticated = true;
750 conn.phase = Phase::Ready;
751 out.push(&success);
752 Flow::Continue
753 }
754 Login::Refused | Login::Closed => Flow::Close,
755 }
756 } else {
757 conn.phase = Phase::Authentication;
758 out.push(&success);
759 Flow::Continue
760 }
761 }
762
763 async fn authenticate(&self, auth: &Value, conn: &Conn, out: &mut Out) -> Login {
766 let scheme = auth
767 .get("scheme")
768 .and_then(Value::as_str)
769 .unwrap_or("none")
770 .to_string();
771 let principal = auth
772 .get("principal")
773 .and_then(Value::as_str)
774 .unwrap_or("")
775 .to_string();
776 let credentials = auth.get("credentials").and_then(Value::as_str);
777
778 if let Some(expected) = &self.config.password {
779 let matches = scheme == "basic"
780 && credentials.is_some_and(|c| constant_time_eq(c.as_bytes(), expected.as_bytes()));
781 if !matches {
782 self.log().warn(format!(
783 "Bolt login {:?} ({}) from {} does not match the configured password \
784 decision=reject_bad_credentials",
785 principal, scheme, self.peer_addr
786 ));
787 out.push(&messages::failure(
788 conn.minor,
789 actions::UNAUTHORIZED,
790 UNAUTHORIZED_MESSAGE,
791 ));
792 return Login::Refused;
793 }
794 }
795
796 let event = Event::new(
797 &actions::BOLT_AUTHENTICATE_EVENT,
798 serde_json::json!({
799 "user_agent": conn.user_agent,
800 "scheme": scheme,
801 "principal": principal,
802 "credentials_present": credentials.is_some_and(|c| !c.is_empty()),
803 "password_configured": self.config.password.is_some(),
804 "answer_with": actions::login_answer_with(
805 &principal,
806 &scheme,
807 credentials.is_some_and(|c| !c.is_empty()),
808 ),
809 }),
810 );
811 let (results, refused) = match self.ask(&event, "login").await {
812 Ok(answer) => answer,
813 Err(message) => {
814 out.push(&messages::failure(
815 conn.minor,
816 DATABASE_UNAVAILABLE,
817 message,
818 ));
819 return Login::Refused;
820 }
821 };
822 for result in results {
823 match result {
824 ActionResult::CloseConnection => {
825 self.log().info(format!(
826 "Bolt login from {} decision=model_close",
827 self.peer_addr
828 ));
829 return Login::Closed;
830 }
831 ActionResult::Custom { name, data } if name == actions::LOGIN_RESULT => {
832 if data["accept"].as_bool() == Some(true) {
833 self.log().info(format!(
834 "Bolt login {:?} from {} decision=model_answer accepted",
835 principal, self.peer_addr
836 ));
837 return Login::Accepted;
838 }
839 let code = data["code"].as_str().unwrap_or(actions::UNAUTHORIZED);
840 let message = data["message"].as_str().unwrap_or(UNAUTHORIZED_MESSAGE);
841 self.log().info(format!(
842 "Bolt login {:?} from {} decision=model_reject code={}",
843 principal, self.peer_addr, code
844 ));
845 out.push(&messages::failure(conn.minor, code, message));
846 return Login::Refused;
847 }
848 ActionResult::Custom { name, .. } => {
849 self.log().warn(format!(
850 "Bolt login from {}: answered with {} decision=fail_closed_mismatched_reply",
851 self.peer_addr, name
852 ));
853 out.push(&messages::failure(
854 conn.minor,
855 DATABASE_UNAVAILABLE,
856 UNAVAILABLE_MESSAGE,
857 ));
858 return Login::Refused;
859 }
860 _ => {}
861 }
862 }
863 self.log().warn(format!(
864 "Bolt login from {}: no usable answer decision={}",
865 self.peer_addr,
866 nothing_usable(refused)
867 ));
868 out.push(&messages::failure(
869 conn.minor,
870 DATABASE_UNAVAILABLE,
871 UNAVAILABLE_MESSAGE,
872 ));
873 Login::Refused
874 }
875
876 async fn ask(
880 &self,
881 event: &Event,
882 what: &str,
883 ) -> Result<(Vec<ActionResult>, usize), &'static str> {
884 match call_llm(
885 &self.llm_client,
886 &self.app_state,
887 self.server_id,
888 Some(self.connection_id),
889 event,
890 self.protocol.as_ref(),
891 )
892 .await
893 {
894 Ok(result) => {
895 for message in &result.messages {
896 self.log().info(message);
897 }
898 for failure in &result.failures {
899 self.log()
900 .warn(format!("Bolt {} answer refused: {:?}", what, failure));
901 }
902 let mut flat = Vec::new();
903 let mut stack = result.protocol_results;
904 stack.reverse();
905 while let Some(item) = stack.pop() {
906 match item {
907 ActionResult::Multiple(items) => stack.extend(items.into_iter().rev()),
908 other => flat.push(other),
909 }
910 }
911 Ok((flat, result.failures.len()))
912 }
913 Err(e) => {
914 let (category, message) = match crate::utils::WireFailure::classify(&e) {
915 crate::utils::WireFailure::Overloaded => ("overloaded", OVERLOADED_MESSAGE),
916 crate::utils::WireFailure::Unavailable => ("unavailable", UNAVAILABLE_MESSAGE),
917 };
918 self.log().warn(format!(
919 "Bolt {} from {} decision=fail_closed_llm_error category={}",
920 what, self.peer_addr, category
921 ));
922 self.log().debug(format!("Bolt LLM call failed: {}", e));
923 Err(message)
924 }
925 }
926 }
927
928 async fn run_query(
929 &self,
930 query: &str,
931 params: &Value,
932 extra: &Value,
933 conn: &mut Conn,
934 out: &mut Out,
935 ) -> Flow {
936 let in_tx = conn.tx.is_some();
937 let db = extra
938 .get("db")
939 .and_then(Value::as_str)
940 .map(str::to_string)
941 .or_else(|| conn.tx.as_ref().and_then(|t| t.db.clone()));
942 let read = match &conn.tx {
943 Some(tx) => tx.read,
944 None => extra.get("mode").and_then(Value::as_str) == Some("r"),
945 };
946
947 let answer = if let Some(answer) = self.admin_answer(query) {
948 self.log().debug(format!(
949 "Bolt {:?} from {} answered by NetGet (admin query, no model call)",
950 query, self.peer_addr
951 ));
952 answer
953 } else {
954 let mut data = serde_json::json!({
955 "query": query,
956 "parameters": values::value_to_json(params),
957 "mode": if read { "read" } else { "write" },
958 "in_transaction": in_tx,
959 });
960 if let Some(db) = &db {
961 data["database"] = serde_json::json!(db);
962 }
963 let event = Event::new(&actions::BOLT_QUERY_EVENT, data);
964 let (results, refused) = match self.ask(&event, "query").await {
965 Ok(answer) => answer,
966 Err(message) => {
967 conn.phase = Phase::Failed;
968 out.push(&messages::failure(
969 conn.minor,
970 DATABASE_UNAVAILABLE,
971 message,
972 ));
973 return Flow::Continue;
974 }
975 };
976 match self.query_outcome(query, results, refused, conn, out) {
977 QueryOutcome::Answer(answer) => answer,
978 QueryOutcome::Failed => return Flow::Continue,
979 QueryOutcome::Close => return Flow::Close,
980 }
981 };
982
983 let qid = conn.next_qid;
984 conn.next_qid += 1;
985 let mut metadata = vec![
986 (
987 "fields",
988 Value::List(answer.fields.iter().cloned().map(Value::String).collect()),
989 ),
990 ("t_first", Value::Int(0)),
991 ];
992 if in_tx {
993 metadata.push(("qid", Value::Int(qid)));
994 }
995 out.push(&messages::success(metadata));
996 conn.results.push(OpenResult {
997 qid,
998 records: answer.records.into(),
999 query_type: answer.query_type,
1000 stats: answer.stats,
1001 db: db.unwrap_or_else(|| "neo4j".to_string()),
1002 });
1003 conn.phase = if in_tx {
1004 Phase::TxStreaming
1005 } else {
1006 Phase::Streaming
1007 };
1008 Flow::Continue
1009 }
1010
1011 fn query_outcome(
1014 &self,
1015 query: &str,
1016 results: Vec<ActionResult>,
1017 refused: usize,
1018 conn: &mut Conn,
1019 out: &mut Out,
1020 ) -> QueryOutcome {
1021 let mut decision = nothing_usable(refused);
1022 for result in results {
1023 match result {
1024 ActionResult::CloseConnection => {
1025 self.log().info(format!(
1026 "Bolt query {:?} from {} decision=model_close",
1027 query, self.peer_addr
1028 ));
1029 return QueryOutcome::Close;
1030 }
1031 ActionResult::Custom { name, data } if name == actions::RECORDS_RESULT => {
1032 match values::query_answer(&data) {
1033 Ok(answer) => {
1034 self.log().info(format!(
1035 "Bolt query {:?} from {} decision=model_answer rows={}",
1036 query,
1037 self.peer_addr,
1038 answer.records.len()
1039 ));
1040 return QueryOutcome::Answer(answer);
1041 }
1042 Err(e) => {
1043 self.log().warn(format!(
1044 "Bolt query from {}: unusable result ({})",
1045 self.peer_addr, e
1046 ));
1047 decision = "fail_closed_invalid_answer";
1048 break;
1049 }
1050 }
1051 }
1052 ActionResult::Custom { name, data } if name == actions::FAILURE_RESULT => {
1053 let code = data["code"].as_str().unwrap_or(DATABASE_UNAVAILABLE);
1054 let message = data["message"].as_str().unwrap_or(UNAVAILABLE_MESSAGE);
1055 self.log().info(format!(
1056 "Bolt query {:?} from {} decision=model_reject code={}",
1057 query, self.peer_addr, code
1058 ));
1059 conn.phase = Phase::Failed;
1060 out.push(&messages::failure(conn.minor, code, message));
1061 return QueryOutcome::Failed;
1062 }
1063 ActionResult::Custom { name, .. } => {
1064 self.log().warn(format!(
1065 "Bolt query from {}: answered with {}",
1066 self.peer_addr, name
1067 ));
1068 decision = "fail_closed_mismatched_reply";
1069 break;
1070 }
1071 _ => {}
1072 }
1073 }
1074 self.log().warn(format!(
1075 "Bolt query {:?} from {}: no usable answer decision={}",
1076 query, self.peer_addr, decision
1077 ));
1078 conn.phase = Phase::Failed;
1079 out.push(&messages::failure(
1080 conn.minor,
1081 DATABASE_UNAVAILABLE,
1082 UNAVAILABLE_MESSAGE,
1083 ));
1084 QueryOutcome::Failed
1085 }
1086
1087 fn stream(&self, n: i64, qid: i64, discard: bool, conn: &mut Conn, out: &mut Out) -> Flow {
1089 let index = if qid == -1 {
1090 conn.results.len().checked_sub(1)
1091 } else {
1092 conn.results.iter().position(|r| r.qid == qid)
1093 };
1094 let Some(index) = index else {
1095 return self.protocol_violation(conn, out, format!("No open result with qid {qid}."));
1096 };
1097 let result = &mut conn.results[index];
1098 let take = if n == -1 {
1099 result.records.len()
1100 } else {
1101 (n as usize).min(result.records.len())
1102 };
1103 for row in result.records.drain(..take) {
1104 if !discard {
1105 out.push(&messages::record(row));
1106 }
1107 }
1108 if !result.records.is_empty() {
1109 out.push(&messages::success(vec![("has_more", Value::Bool(true))]));
1110 return Flow::Continue;
1111 }
1112
1113 let result = conn.results.remove(index);
1114 let mut summary = vec![
1115 ("type", Value::string(result.query_type)),
1116 ("t_last", Value::Int(0)),
1117 ("db", Value::String(result.db)),
1118 ];
1119 if let Some(stats) = result.stats {
1120 summary.push(("stats", stats));
1121 }
1122 let autocommit = conn.tx.is_none();
1123 if autocommit && result.query_type != "r" {
1124 summary.push(("bookmark", Value::String(self.bookmark())));
1125 }
1126 out.push(&messages::success(summary));
1127 conn.phase = match (autocommit, conn.results.is_empty()) {
1128 (true, _) => Phase::Ready,
1129 (false, true) => Phase::TxReady,
1130 (false, false) => Phase::TxStreaming,
1131 };
1132 Flow::Continue
1133 }
1134
1135 fn routing_table(&self, routing: &Value, extra: &Value, conn: &Conn) -> Value {
1137 let address = routing
1138 .get("address")
1139 .and_then(Value::as_str)
1140 .map(str::to_string)
1141 .or_else(|| conn.routing_address.clone())
1142 .unwrap_or_else(|| self.local_addr.to_string());
1143 let db = extra
1144 .get("db")
1145 .and_then(Value::as_str)
1146 .unwrap_or("neo4j")
1147 .to_string();
1148 let server = |role: &str| {
1149 Value::map([
1150 (
1151 "addresses",
1152 Value::List(vec![Value::String(address.clone())]),
1153 ),
1154 ("role", Value::string(role)),
1155 ])
1156 };
1157 messages::success(vec![(
1158 "rt",
1159 Value::map([
1160 ("ttl", Value::Int(300)),
1161 ("db", Value::String(db)),
1162 (
1163 "servers",
1164 Value::List(vec![server("WRITE"), server("READ"), server("ROUTE")]),
1165 ),
1166 ]),
1167 )])
1168 }
1169
1170 fn admin_answer(&self, query: &str) -> Option<values::QueryAnswer> {
1173 let normalized = query
1174 .trim()
1175 .trim_end_matches(';')
1176 .split_whitespace()
1177 .collect::<Vec<_>>()
1178 .join(" ")
1179 .to_ascii_lowercase();
1180 let version = Value::List(vec![Value::String(self.config.neo4j_version.clone())]);
1181 let (fields, row): (&[&str], Vec<Value>) = match normalized.as_str() {
1182 "call db.ping()" => (&["success"], vec![Value::Bool(true)]),
1183 "call dbms.licenseagreementdetails()" => (
1184 &["status", "daysLeftOnTrial", "totalTrialDays"],
1185 vec![Value::string("yes"), Value::Int(0), Value::Int(0)],
1186 ),
1187 "call dbms.components()" => (
1188 &["name", "versions", "edition"],
1189 vec![
1190 Value::string("Neo4j Kernel"),
1191 version,
1192 Value::string("community"),
1193 ],
1194 ),
1195 "call dbms.components() yield versions" => (&["versions"], vec![version]),
1196 _ => return None,
1197 };
1198 Some(values::QueryAnswer {
1199 fields: fields.iter().map(|f| f.to_string()).collect(),
1200 records: vec![row],
1201 stats: None,
1202 query_type: "r",
1203 })
1204 }
1205}
1206
1207fn nothing_usable(refused: usize) -> &'static str {
1210 if refused > 0 {
1211 "fail_closed_invalid_answer"
1212 } else {
1213 "model_silent"
1214 }
1215}
1216
1217fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
1218 if a.len() != b.len() {
1220 return false;
1221 }
1222 a.iter().zip(b).fold(0u8, |acc, (x, y)| acc | (x ^ y)) == 0
1223}
1224
1225const LINGER_TIME: Duration = Duration::from_secs(2);
1229const LINGER_BYTES: usize = 64 * 1024;
1230
1231async fn linger<R: tokio::io::AsyncRead + Unpin>(reader: &mut R) {
1232 let deadline = tokio::time::Instant::now() + LINGER_TIME;
1233 let mut sink = [0u8; 4096];
1234 let mut drained = 0usize;
1235 while drained < LINGER_BYTES {
1236 match tokio::time::timeout_at(deadline, reader.read(&mut sink)).await {
1237 Ok(Ok(n)) if n > 0 => drained += n,
1238 _ => break,
1239 }
1240 }
1241}