pub mod actions;
use crate::llm::action_helper::call_llm;
use crate::llm::actions::protocol_trait::ActionResult;
use crate::llm::ollama_client::OllamaClient;
use crate::protocol::Event;
use crate::server::connection::ConnectionId;
use crate::state::app_state::AppState;
use crate::{console_debug, console_error};
use actions::{MongodbProtocol, MONGODB_COMMAND_EVENT, MONGODB_DISCONNECTED_EVENT};
use anyhow::{Context, Result};
use std::net::SocketAddr;
use std::sync::Arc;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpListener, TcpStream};
use tokio::sync::{mpsc, Mutex};
use tracing::{debug, error, info, trace, warn};
use crate::utils::bson_depth::{
scan_bson_document, scan_bson_document_with_limit, BsonScan, MAX_BSON_DEPTH,
};
use bson::{doc, Bson, Document};
pub const MAX_MESSAGE_SIZE: i32 = 48 * 1024 * 1024;
const OP_MSG: i32 = 2013;
const BODY_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
const FIRST_HEADER_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
const IDLE_BETWEEN_MESSAGES_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(600);
const MAX_CONNECTIONS: usize = crate::server::accept_bounded::DEFAULT_MAX_CONNECTIONS;
const CONNECTION_CAP_REFUSAL: &[u8] = b"";
type SharedWrite = Arc<Mutex<tokio::io::WriteHalf<TcpStream>>>;
pub struct MongodbServer {
llm_client: OllamaClient,
app_state: Arc<AppState>,
_status_tx: mpsc::UnboundedSender<String>,
server_id: Option<crate::state::ServerId>,
}
impl MongodbServer {
pub fn new(
llm_client: OllamaClient,
app_state: Arc<AppState>,
status_tx: mpsc::UnboundedSender<String>,
server_id: Option<crate::state::ServerId>,
) -> Self {
Self {
llm_client,
app_state,
_status_tx: status_tx,
server_id,
}
}
pub async fn spawn_with_llm_actions(
listen_addr: SocketAddr,
llm_client: OllamaClient,
app_state: Arc<AppState>,
status_tx: mpsc::UnboundedSender<String>,
server_id: crate::state::ServerId,
) -> Result<SocketAddr> {
let listener = TcpListener::bind(listen_addr).await?;
let actual_addr = listener.local_addr()?;
info!("MongoDB server starting on {}", actual_addr);
let _ = status_tx.send(format!(
"[INFO] MongoDB server listening on {}",
actual_addr
));
let server = Arc::new(MongodbServer::new(
llm_client,
app_state.clone(),
status_tx.clone(),
Some(server_id),
));
let status_tx_clone = status_tx.clone();
let task_registrar = app_state.clone();
let limiter = crate::server::accept_bounded::ConnectionLimiter::new(MAX_CONNECTIONS);
let accept_handle = tokio::spawn(async move {
loop {
match crate::server::accept_bounded::accept_bounded(
&listener,
&limiter,
CONNECTION_CAP_REFUSAL,
"MONGODB",
Some(&status_tx),
)
.await
{
Ok((stream, addr, permit)) => {
console_debug!(status_tx, "MongoDB connection from {}", addr);
let connection_id =
ConnectionId::new(app_state.get_next_unified_id().await);
let local_addr_conn = stream.local_addr().unwrap_or(actual_addr);
if let Some(server_id) = server.server_id {
use crate::state::server::{
ConnectionState as ServerConnectionState, ConnectionStatus,
ProtocolConnectionInfo,
};
let now = crate::utils::clock::Instant::now();
let conn_state = ServerConnectionState {
id: connection_id,
remote_addr: addr,
local_addr: local_addr_conn,
bytes_sent: 0,
bytes_received: 0,
packets_sent: 0,
packets_received: 0,
last_activity: now,
status: ConnectionStatus::Active,
status_changed_at: now,
protocol_info: ProtocolConnectionInfo::empty(),
};
server
.app_state
.add_connection_to_server(server_id, conn_state)
.await;
}
let handler = MongodbHandler::new(
connection_id,
server.llm_client.clone(),
server.app_state.clone(),
status_tx.clone(),
server.server_id,
addr,
);
let task_owner = app_state.clone();
task_owner
.spawn_server_task(server_id, async move {
let _permit = permit;
if let Err(e) = handler.handle_connection(stream).await {
error!("MongoDB connection error: {:?}", e);
}
})
.await;
}
Err(e) => {
console_error!(status_tx, "MongoDB accept error: {}", e);
}
}
}
});
task_registrar
.register_server_task(server_id, accept_handle)
.await;
let _ = status_tx_clone.send("__UPDATE_UI__".to_string());
Ok(actual_addr)
}
}
pub struct MongodbHandler {
connection_id: ConnectionId,
llm_client: OllamaClient,
app_state: Arc<AppState>,
#[allow(dead_code)]
status_tx: mpsc::UnboundedSender<String>,
#[allow(dead_code)]
server_id: Option<crate::state::ServerId>,
#[allow(dead_code)]
remote_addr: SocketAddr,
protocol: Arc<MongodbProtocol>,
}
impl MongodbHandler {
pub fn new(
connection_id: ConnectionId,
llm_client: OllamaClient,
app_state: Arc<AppState>,
status_tx: mpsc::UnboundedSender<String>,
server_id: Option<crate::state::ServerId>,
remote_addr: SocketAddr,
) -> Self {
let protocol = Arc::new(MongodbProtocol::new(
connection_id,
app_state.clone(),
status_tx.clone(),
));
Self {
connection_id,
llm_client,
app_state,
status_tx,
server_id,
remote_addr,
protocol,
}
}
async fn handle_connection(self, stream: TcpStream) -> Result<()> {
let server_id = self.server_id;
let connection_id = self.connection_id;
let app_state = self.app_state.clone();
let (reader, write_half) = tokio::io::split(stream);
let write_half: SharedWrite = Arc::new(Mutex::new(write_half));
if let Some(server_id) = server_id {
let peer_rx = crate::server::peer_support::register_peer_channel(
&app_state,
server_id,
connection_id.as_u32(),
)
.await;
crate::server::peer_support::spawn_peer_command_task(
peer_rx,
self.protocol.clone(),
app_state.clone(),
server_id,
connection_id.as_u32(),
write_half.clone(),
self.status_tx.clone(),
);
}
let outcome = self.run_session(reader, &write_half).await;
if let Some(server_id) = server_id {
app_state
.remove_peer_handle(server_id, connection_id.as_u32())
.await;
}
let _ = write_half.lock().await.shutdown().await;
if let Some(server_id) = server_id {
app_state
.close_connection_on_server(server_id, connection_id)
.await;
}
match outcome {
Ok(disconnect_reason) => {
let event = Event::new(
&MONGODB_DISCONNECTED_EVENT,
serde_json::json!({"reason": disconnect_reason}),
);
let server_id = server_id.unwrap_or_else(|| crate::state::ServerId::new(0));
let _ = call_llm(
&self.llm_client,
&self.app_state,
server_id,
Some(self.connection_id),
&event,
self.protocol.as_ref(),
)
.await;
Ok(())
}
Err(e) => Err(e),
}
}
async fn write_response(&self, write_half: &SharedWrite, bytes: &[u8]) -> Result<()> {
{
let mut writer = write_half.lock().await;
writer.write_all(bytes).await?;
writer.flush().await?;
}
self.record_sent(bytes.len() as u64).await;
Ok(())
}
async fn record_received(&self, bytes: u64) {
if let Some(server_id) = self.server_id {
self.app_state
.update_connection_stats(
server_id,
self.connection_id,
Some(bytes),
None,
Some(1),
None,
)
.await;
}
}
async fn record_sent(&self, bytes: u64) {
if let Some(server_id) = self.server_id {
self.app_state
.update_connection_stats(
server_id,
self.connection_id,
None,
Some(bytes),
None,
Some(1),
)
.await;
}
}
async fn run_session(
&self,
mut reader: tokio::io::ReadHalf<TcpStream>,
write_half: &SharedWrite,
) -> Result<&'static str> {
debug!(
"MongoDB handler starting for connection {}",
self.connection_id
);
let mut disconnect_reason = "client_disconnect";
let mut answered_one = false;
loop {
let header_timeout = if answered_one {
IDLE_BETWEEN_MESSAGES_TIMEOUT
} else {
FIRST_HEADER_READ_TIMEOUT
};
let mut header = [0u8; 16];
let header_read =
match tokio::time::timeout(header_timeout, reader.read_exact(&mut header)).await {
Ok(read) => read,
Err(_) => {
debug!(
"MongoDB: {} sent no message header for {:?}; closing idle connection",
self.remote_addr, header_timeout
);
let _ = self.status_tx.send(format!(
"[INFO] MongoDB: {} sent nothing for {:?}, closing idle connection",
self.remote_addr, header_timeout
));
disconnect_reason = "idle_timeout";
break;
}
};
match header_read {
Ok(_) => {}
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
debug!("MongoDB client disconnected");
break;
}
Err(e) => {
return Err(e.into());
}
}
let message_length = i32::from_le_bytes([header[0], header[1], header[2], header[3]]);
let request_id = i32::from_le_bytes([header[4], header[5], header[6], header[7]]);
let _response_to = i32::from_le_bytes([header[8], header[9], header[10], header[11]]);
let op_code = i32::from_le_bytes([header[12], header[13], header[14], header[15]]);
trace!(
"MongoDB message: length={}, requestID={}, opCode={}",
message_length,
request_id,
op_code
);
if !(16..=MAX_MESSAGE_SIZE).contains(&message_length) {
error!(
"MongoDB: rejecting message with out-of-range length {} from {}",
message_length, self.remote_addr
);
let _ = self.status_tx.send(format!(
"[ERROR] MongoDB: invalid message length {} from {}, closing connection",
message_length, self.remote_addr
));
disconnect_reason = "invalid_message_length";
break;
}
let body_length = (message_length - 16) as usize;
let mut body = vec![0u8; body_length];
match tokio::time::timeout(BODY_READ_TIMEOUT, reader.read_exact(&mut body)).await {
Ok(r) => {
r?;
}
Err(_) => {
error!(
"MongoDB: {} did not finish a {}-byte message within {:?}, closing",
self.remote_addr, message_length, BODY_READ_TIMEOUT
);
let _ = self.status_tx.send(format!(
"[ERROR] MongoDB: incomplete message body from {} after {:?}, \
closing connection",
self.remote_addr, BODY_READ_TIMEOUT
));
disconnect_reason = "incomplete_message_body";
break;
}
}
self.record_received(message_length as u64).await;
answered_one = true;
if op_code != OP_MSG {
error!(
"MongoDB: unsupported opCode {} from {}, closing connection",
op_code, self.remote_addr
);
let _ = self.status_tx.send(format!(
"[ERROR] MongoDB: unsupported opCode {} (only OP_MSG {} is implemented)",
op_code, OP_MSG
));
disconnect_reason = "unsupported_opcode";
break;
}
let command_doc = match self.parse_op_msg(&body) {
Ok(ParsedOpMsg::Command(doc)) => doc,
Ok(ParsedOpMsg::TooDeep { limit }) => {
warn!(
"MongoDB connection {} decision=fail_closed_bson_too_deep: command \
document nests deeper than {} levels; refused without decoding",
self.connection_id, limit
);
let _ = self.status_tx.send(format!(
"[WARN] MongoDB connection {} decision=fail_closed_bson_too_deep",
self.connection_id
));
let doc = mongodb_error_doc(MONGODB_OVERFLOW, BSON_TOO_DEEP_ERRMSG);
let response_bytes = self.encode_op_msg_response(request_id, doc)?;
self.write_response(write_half, &response_bytes).await?;
continue;
}
Err(e) => {
error!("MongoDB: malformed OP_MSG from {}: {}", self.remote_addr, e);
let _ = self
.status_tx
.send(format!("[ERROR] MongoDB: malformed OP_MSG: {}", e));
disconnect_reason = "malformed_op_msg";
break;
}
};
trace!("MongoDB command document: {:?}", command_doc);
let command_name = command_doc
.keys()
.next()
.map(|k| k.as_str())
.unwrap_or("unknown")
.to_string();
let collection = command_doc
.get(&command_name)
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let database = command_doc.get_str("$db").unwrap_or("admin").to_string();
if command_name.eq_ignore_ascii_case("hello")
|| command_name.eq_ignore_ascii_case("ismaster")
{
debug!(
"MongoDB handshake ({}) from {}",
command_name, self.remote_addr
);
let response_bytes = self.encode_op_msg_response(
request_id,
hello_response(self.connection_id.as_u32()),
)?;
self.write_response(write_half, &response_bytes).await?;
continue;
}
let event_data = serde_json::json!({
"command": command_name,
"database": database,
"collection": collection,
"filter": self.bson_to_json(command_doc.get("filter")),
"document": self.bson_to_json(command_doc.get("documents").or_else(|| command_doc.get("document"))),
"updates": self.bson_to_json(command_doc.get("updates")),
"deletes": self.bson_to_json(command_doc.get("deletes")),
});
let event = Event::new(&MONGODB_COMMAND_EVENT, event_data);
let server_id = self
.server_id
.unwrap_or_else(|| crate::state::ServerId::new(0));
let execution_result = match call_llm(
&self.llm_client,
&self.app_state,
server_id,
Some(self.connection_id),
&event,
self.protocol.as_ref(),
)
.await
{
Ok(result) => result,
Err(e) => {
let failure = crate::utils::WireFailure::classify(&e);
let code = match failure {
crate::utils::WireFailure::Overloaded => MONGODB_TEMPORARILY_UNAVAILABLE,
crate::utils::WireFailure::Unavailable => MONGODB_INTERNAL_ERROR,
};
error!(
"MongoDB command '{}' on connection {}: decision=fail_closed_llm_error \
category={:?} code={} error={}",
command_name, self.connection_id, failure, code, e
);
let message = failure.prefixed_text();
let _ = self.status_tx.send(format!(
"[ERROR] MongoDB connection {} decision=fail_closed_llm_error code={}: {}",
self.connection_id, code, message
));
let doc = mongodb_error_doc(code, message);
let response_bytes = self.encode_op_msg_response(request_id, doc)?;
self.write_response(write_half, &response_bytes).await?;
continue;
}
};
let namespace = format!("{}.{}", database, collection.as_deref().unwrap_or("$cmd"));
let mut responded = false;
let mut close_requested = false;
for protocol_result in execution_result.protocol_results {
match protocol_result {
ActionResult::Custom { name, data } => {
if name == "mongodb_response" {
let response_doc = match self.json_to_bson_doc(&data, &namespace) {
Ok(d) => {
if data.get("type").and_then(|v| v.as_str())
== Some("error_response")
{
debug!(
"MongoDB command '{}' on connection {}: \
decision=model_reject",
command_name, self.connection_id
);
}
d
}
Err(e) => {
error!(
"MongoDB command '{}' on connection {}: \
decision=fail_closed_unusable_answer error={}",
command_name, self.connection_id, e
);
let _ = self.status_tx.send(format!(
"[ERROR] MongoDB connection {} \
decision=fail_closed_unusable_answer for command '{}'",
self.connection_id, command_name
));
mongodb_error_doc(
MONGODB_INTERNAL_ERROR,
crate::utils::WireFailure::Unavailable.prefixed_text(),
)
}
};
let response_bytes =
self.encode_op_msg_response(request_id, response_doc)?;
self.write_response(write_half, &response_bytes).await?;
responded = true;
} else {
warn!(
"MongoDB: no wire encoding for action result '{}', ignoring",
name
);
}
}
ActionResult::CloseConnection => {
debug!("Closing MongoDB connection");
close_requested = true;
}
ActionResult::NoAction => {}
_ => {
debug!("Unhandled action result");
}
}
}
if !responded && !close_requested {
warn!(
"MongoDB command '{}' on connection {}: decision=fail_closed_no_answer",
command_name, self.connection_id
);
let _ = self.status_tx.send(format!(
"[WARN] MongoDB connection {} decision=fail_closed_no_answer for command '{}'",
self.connection_id, command_name
));
let doc = mongodb_error_doc(
59,
&format!(
"netget: no response produced for command '{}'",
command_name
),
);
let response_bytes = self.encode_op_msg_response(request_id, doc)?;
self.write_response(write_half, &response_bytes).await?;
}
if close_requested {
disconnect_reason = "close_this_connection";
break;
}
}
Ok(disconnect_reason)
}
fn parse_op_msg(&self, body: &[u8]) -> Result<ParsedOpMsg> {
parse_op_msg_sections(body)
}
fn encode_op_msg_response(&self, request_id: i32, doc: Document) -> Result<Vec<u8>> {
let mut body = vec![0u8; 5]; body[4] = 0;
let mut doc_bytes = Vec::new();
doc.to_writer(&mut doc_bytes)?;
body.extend_from_slice(&doc_bytes);
let message_length = (16 + body.len()) as i32;
let response_to = request_id;
let op_code = 2013i32;
let mut message = Vec::new();
message.extend_from_slice(&message_length.to_le_bytes());
message.extend_from_slice(&0i32.to_le_bytes()); message.extend_from_slice(&response_to.to_le_bytes());
message.extend_from_slice(&op_code.to_le_bytes());
message.extend_from_slice(&body);
Ok(message)
}
fn bson_to_json(&self, bson_opt: Option<&Bson>) -> serde_json::Value {
match bson_opt {
Some(bson) => bson.clone().into_relaxed_extjson(),
None => serde_json::Value::Null,
}
}
fn json_to_bson_doc(&self, json: &serde_json::Value, namespace: &str) -> Result<Document> {
let action_type = json
.get("type")
.and_then(|v| v.as_str())
.context("Missing action type")?;
match action_type {
"find_response" => {
let documents = json
.get("documents")
.and_then(|v| v.as_array())
.context("Missing documents")?;
let cursor_docs: Vec<Bson> = documents
.iter()
.filter_map(|d| d.clone().try_into().ok())
.collect();
Ok(doc! {
"ok": 1,
"cursor": {
"id": 0i64,
"ns": namespace,
"firstBatch": cursor_docs
}
})
}
"insert_response" => {
let n = json
.get("inserted_count")
.and_then(|v| v.as_u64())
.unwrap_or(1) as i32;
Ok(doc! { "ok": 1, "n": n })
}
"update_response" => {
let matched = json
.get("matched_count")
.and_then(|v| v.as_u64())
.unwrap_or(0) as i32;
let modified = json
.get("modified_count")
.and_then(|v| v.as_u64())
.unwrap_or(0) as i32;
Ok(doc! { "ok": 1, "n": matched, "nModified": modified })
}
"delete_response" => {
let n = json
.get("deleted_count")
.and_then(|v| v.as_u64())
.unwrap_or(0) as i32;
Ok(doc! { "ok": 1, "n": n })
}
"error_response" => {
let code = json.get("code").and_then(|v| v.as_i64()).unwrap_or(0) as i32;
let message = json
.get("message")
.and_then(|v| v.as_str())
.unwrap_or("Unknown error");
Ok(mongodb_error_doc(code, message))
}
other => Err(anyhow::anyhow!(
"MongoDB: unknown response action type '{}'",
other
)),
}
}
}
const MONGODB_INTERNAL_ERROR: i32 = 1;
const MONGODB_TEMPORARILY_UNAVAILABLE: i32 = 365;
const MONGODB_OVERFLOW: i32 = 15;
const BSON_TOO_DEEP_ERRMSG: &str = "BSONObj exceeded maximum nested object depth";
const OP_MSG_CHECKSUM_PRESENT: u32 = 1;
const OP_MSG_UNKNOWN_REQUIRED_FLAGS: u32 = 0xFFFE;
fn parse_op_msg_sections(body: &[u8]) -> Result<ParsedOpMsg> {
if body.len() < 5 {
return Err(anyhow::anyhow!("OP_MSG body too short"));
}
let flag_bits = u32::from_le_bytes([body[0], body[1], body[2], body[3]]);
if flag_bits & OP_MSG_UNKNOWN_REQUIRED_FLAGS != 0 {
return Err(anyhow::anyhow!(
"OP_MSG sets required flag bits this server does not understand: {:#06x}",
flag_bits & OP_MSG_UNKNOWN_REQUIRED_FLAGS
));
}
let mut sections = &body[4..];
if flag_bits & OP_MSG_CHECKSUM_PRESENT != 0 {
if sections.len() < 4 {
return Err(anyhow::anyhow!(
"OP_MSG declares a checksum it does not carry"
));
}
sections = §ions[..sections.len() - 4];
}
let mut command: Option<Document> = None;
let mut sequences: Vec<(String, Vec<Bson>)> = Vec::new();
while let Some((&kind, rest)) = sections.split_first() {
match kind {
0 => {
if command.is_some() {
return Err(anyhow::anyhow!(
"OP_MSG carries more than one kind-0 section"
));
}
match scan_bson_document(rest) {
BsonScan::Complete { consumed } => {
command = Some(Document::from_reader(&rest[..consumed])?);
sections = &rest[consumed..];
}
BsonScan::TooDeep { limit } => return Ok(ParsedOpMsg::TooDeep { limit }),
BsonScan::Malformed => {
return Err(anyhow::anyhow!("OP_MSG body is not a BSON document"))
}
}
}
1 => {
let size_bytes: [u8; 4] = rest
.get(..4)
.and_then(|b| b.try_into().ok())
.context("OP_MSG document sequence truncated before its size")?;
let size = i32::from_le_bytes(size_bytes);
if size < 5 || size as usize > rest.len() {
return Err(anyhow::anyhow!(
"OP_MSG document sequence declares {} bytes with {} present",
size,
rest.len()
));
}
let sequence = &rest[4..size as usize];
let nul = sequence
.iter()
.position(|&b| b == 0)
.context("OP_MSG document sequence identifier is not terminated")?;
let identifier = std::str::from_utf8(&sequence[..nul])
.context("OP_MSG document sequence identifier is not UTF-8")?
.to_string();
let mut documents = Vec::new();
let mut remaining = &sequence[nul + 1..];
while !remaining.is_empty() {
match scan_bson_document_with_limit(remaining, MAX_BSON_DEPTH - 2) {
BsonScan::Complete { consumed } => {
documents.push(Bson::Document(Document::from_reader(
&remaining[..consumed],
)?));
remaining = &remaining[consumed..];
}
BsonScan::TooDeep { .. } => {
return Ok(ParsedOpMsg::TooDeep {
limit: MAX_BSON_DEPTH,
})
}
BsonScan::Malformed => {
return Err(anyhow::anyhow!(
"OP_MSG document sequence '{}' holds something that is not a \
BSON document",
identifier
))
}
}
}
sequences.push((identifier, documents));
sections = &rest[size as usize..];
}
other => {
return Err(anyhow::anyhow!(
"Unsupported OP_MSG section kind: {}",
other
));
}
}
}
let mut command = command.context("OP_MSG has no kind-0 body section")?;
for (identifier, documents) in sequences {
if command.contains_key(&identifier) {
return Err(anyhow::anyhow!(
"OP_MSG supplies '{}' both in its body and as a document sequence",
identifier
));
}
command.insert(identifier, Bson::Array(documents));
}
Ok(ParsedOpMsg::Command(command))
}
enum ParsedOpMsg {
Command(Document),
TooDeep { limit: usize },
}
fn mongodb_error_doc(code: i32, message: &str) -> Document {
doc! { "ok": 0, "code": code, "errmsg": message }
}
fn hello_response(connection_id: u32) -> Document {
doc! {
"ok": 1,
"isWritablePrimary": true,
"ismaster": true,
"helloOk": true,
"readOnly": false,
"minWireVersion": 0i32,
"maxWireVersion": 17i32,
"maxBsonObjectSize": 16 * 1024 * 1024i32,
"maxMessageSizeBytes": MAX_MESSAGE_SIZE,
"maxWriteBatchSize": 100_000i32,
"logicalSessionTimeoutMinutes": 30i32,
"connectionId": connection_id as i32,
"localTime": bson::DateTime::now(),
}
}