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::logging::emit::Log;
use crate::protocol::Event;
use crate::server::connection::ConnectionId;
use crate::state::app_state::AppState;
use crate::state::server::{ConnectionState, ConnectionStatus, ProtocolConnectionInfo};
use crate::utils::clock::Instant;
use actions::*;
use anyhow::{Context, Result};
use bytes::{Buf, BytesMut};
use cassandra_protocol::compression::Compression;
use cassandra_protocol::frame::{Direction, Envelope, Flags, Opcode, Version};
use serde_json::json;
use std::collections::HashMap;
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, trace, warn};
pub const MAX_FRAME_BODY_BYTES: usize = 256 * 1024 * 1024;
const FIRST_FRAME_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
const IDLE_BETWEEN_FRAMES_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(900);
const MAX_CONNECTIONS: usize = crate::server::accept_bounded::DEFAULT_MAX_CONNECTIONS;
const CONNECTION_CAP_REFUSAL: &[u8] = b"";
const MAX_PREPARED_STATEMENTS: usize = 1024;
const CASSANDRA_ERROR_SERVER_ERROR: u32 = 0x0000;
const CASSANDRA_ERROR_OVERLOADED: u32 = 0x1001;
const CASSANDRA_ERROR_PROTOCOL: u32 = 0x000A;
const CASSANDRA_ERROR_UNPREPARED: u32 = 0x2500;
fn consistency_name(code: u16) -> &'static str {
match code {
0x0000 => "ANY",
0x0001 => "ONE",
0x0002 => "TWO",
0x0003 => "THREE",
0x0004 => "QUORUM",
0x0005 => "ALL",
0x0006 => "LOCAL_QUORUM",
0x0007 => "EACH_QUORUM",
0x0008 => "SERIAL",
0x0009 => "LOCAL_SERIAL",
0x000A => "LOCAL_ONE",
_ => "UNKNOWN",
}
}
pub struct CassandraServer {
llm_client: OllamaClient,
app_state: Arc<AppState>,
_status_tx: mpsc::UnboundedSender<String>,
server_id: Option<crate::state::ServerId>,
}
struct CassandraConnectionState {
ready: bool,
protocol_version: u8,
prepared_statements: HashMap<Vec<u8>, (String, usize)>,
authenticated: bool,
username: Option<String>,
}
struct ConnWrite {
write_half: Arc<Mutex<tokio::io::WriteHalf<TcpStream>>>,
app_state: Arc<AppState>,
server_id: Option<crate::state::ServerId>,
connection_id: ConnectionId,
}
impl ConnWrite {
async fn write(&self, data: &[u8]) -> Result<()> {
{
let mut writer = self.write_half.lock().await;
writer.write_all(data).await?;
writer.flush().await?;
}
if let Some(server_id) = self.server_id {
self.app_state
.update_connection_stats(
server_id,
self.connection_id,
None,
Some(data.len() as u64),
None,
Some(1),
)
.await;
}
Ok(())
}
}
impl CassandraServer {
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()?;
Log::new(Some(&status_tx))
.info(format!("Cassandra/CQL server listening on {}", actual_addr));
let server = Arc::new(CassandraServer::new(
llm_client,
app_state.clone(),
status_tx.clone(),
Some(server_id),
));
let limiter = crate::server::accept_bounded::ConnectionLimiter::new(MAX_CONNECTIONS);
let loop_state = app_state.clone();
let accept_handle = tokio::spawn(async move {
loop {
match crate::server::accept_bounded::accept_bounded(
&listener,
&limiter,
CONNECTION_CAP_REFUSAL,
"Cassandra",
Some(&status_tx),
)
.await
{
Ok((stream, addr, permit)) => {
Log::new(Some(&status_tx))
.debug(format!("Cassandra connection from {}", addr));
let server_clone = server.clone();
let status_tx_clone = status_tx.clone();
let task_owner = loop_state.clone();
task_owner
.spawn_server_task(server_id, async move {
let _permit = permit;
if let Err(e) = server_clone
.handle_connection(stream, addr, status_tx_clone)
.await
{
error!("Cassandra connection error: {}", e);
}
})
.await;
}
Err(e) => {
Log::new(Some(&status_tx))
.error(format!("Cassandra accept failed, listener stopped: {}", e));
break;
}
}
}
});
app_state
.register_server_task(server_id, accept_handle)
.await;
Ok(actual_addr)
}
async fn handle_connection(
&self,
stream: TcpStream,
addr: SocketAddr,
status_tx: mpsc::UnboundedSender<String>,
) -> Result<()> {
let connection_id = ConnectionId::new(self.app_state.get_next_unified_id().await);
let local_addr = stream.local_addr().unwrap_or(addr);
if let Some(server_id) = self.server_id {
let now = Instant::now();
let conn_state = ConnectionState {
id: connection_id,
remote_addr: addr,
local_addr,
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(),
};
self.app_state
.add_connection_to_server(server_id, conn_state)
.await;
}
let (reader, write_half) = tokio::io::split(stream);
let write_half = Arc::new(Mutex::new(write_half));
let out = ConnWrite {
write_half: write_half.clone(),
app_state: self.app_state.clone(),
server_id: self.server_id,
connection_id,
};
if let Some(server_id) = self.server_id {
let peer_rx = crate::server::peer_support::register_peer_channel(
&self.app_state,
server_id,
connection_id.as_u32(),
)
.await;
let protocol = Arc::new(CassandraProtocol::new(
connection_id,
self.app_state.clone(),
status_tx.clone(),
));
crate::server::peer_support::spawn_peer_command_task(
peer_rx,
protocol,
self.app_state.clone(),
server_id,
connection_id.as_u32(),
write_half.clone(),
status_tx.clone(),
);
}
let result = self
.run_session(reader, &out, addr, connection_id, &status_tx)
.await;
if let Some(server_id) = self.server_id {
self.app_state
.remove_peer_handle(server_id, connection_id.as_u32())
.await;
}
let _ = write_half.lock().await.shutdown().await;
if let Some(server_id) = self.server_id {
self.app_state
.close_connection_on_server(server_id, connection_id)
.await;
}
result
}
async fn run_session(
&self,
mut reader: tokio::io::ReadHalf<TcpStream>,
out: &ConnWrite,
addr: SocketAddr,
connection_id: ConnectionId,
status_tx: &mpsc::UnboundedSender<String>,
) -> Result<()> {
let mut conn_state = CassandraConnectionState {
ready: false,
protocol_version: 4,
prepared_statements: HashMap::new(),
authenticated: false,
username: None,
};
let mut buffer = BytesMut::with_capacity(4096);
let mut closing = false;
loop {
let read_timeout = if conn_state.ready {
IDLE_BETWEEN_FRAMES_TIMEOUT
} else {
FIRST_FRAME_READ_TIMEOUT
};
let read = match tokio::time::timeout(read_timeout, reader.read_buf(&mut buffer)).await
{
Ok(read) => read,
Err(_) => {
Log::new(Some(status_tx)).debug(format!(
"Cassandra client {} sent nothing for {}s; closing idle connection",
addr,
read_timeout.as_secs()
));
break;
}
};
let n = match read {
Ok(0) => {
Log::new(Some(status_tx))
.debug(format!("Cassandra client {} disconnected", addr));
break;
}
Ok(n) => n,
Err(e) => {
error!("Read error from {}: {}", addr, e);
break;
}
};
if let Some(server_id) = self.server_id {
self.app_state
.update_connection_stats(
server_id,
connection_id,
Some(n as u64),
None,
Some(1),
None,
)
.await;
}
trace!("Read {} bytes from Cassandra client {}", n, addr);
while buffer.remaining() >= 9 {
let frame_start = buffer.as_ref();
if frame_start.len() < 9 {
break;
}
let length = u32::from_be_bytes([
frame_start[5],
frame_start[6],
frame_start[7],
frame_start[8],
]) as usize;
if length > MAX_FRAME_BODY_BYTES {
Log::new(Some(status_tx)).error(format!(
"Cassandra frame too large ({} bytes, limit {}), closing {}",
length, MAX_FRAME_BODY_BYTES, addr
));
closing = true;
break;
}
if buffer.remaining() < 9 + length {
trace!(
"Waiting for complete frame: have {}, need {}",
buffer.remaining(),
9 + length
);
break;
}
let frame_bytes = buffer.split_to(9 + length);
match self
.handle_frame(&frame_bytes, &mut conn_state, out, connection_id, status_tx)
.await
{
Ok(should_continue) => {
if !should_continue {
debug!("Closing Cassandra connection to {}", addr);
closing = true;
break;
}
}
Err(e) => {
Log::new(Some(status_tx)).error(format!(
"Cassandra connection {} decision=fail_closed_protocol_error: {}",
connection_id, e
));
let stream_id = i16::from_be_bytes([frame_bytes[2], frame_bytes[3]]);
if let Err(send_err) = self
.send_error(
stream_id,
CASSANDRA_ERROR_PROTOCOL,
"netget: malformed CQL frame",
out,
status_tx,
)
.await
{
debug!(
"Cassandra could not report the protocol error to {}: {}",
addr, send_err
);
}
closing = true;
break;
}
}
}
if closing {
break;
}
}
Ok(())
}
async fn handle_frame(
&self,
frame_bytes: &[u8],
conn_state: &mut CassandraConnectionState,
out: &ConnWrite,
connection_id: ConnectionId,
status_tx: &mpsc::UnboundedSender<String>,
) -> Result<bool> {
let parsed = Envelope::from_buffer(frame_bytes, Compression::None)
.context("Failed to parse Cassandra frame")?;
let frame = parsed.envelope;
Log::new(Some(status_tx)).trace(format!(
"Cassandra ← {:?} stream={}",
frame.opcode, frame.stream_id
));
match frame.opcode {
Opcode::Startup => {
self.handle_startup(frame, conn_state, out, connection_id, status_tx)
.await
}
Opcode::Options => {
self.handle_options(frame, out, connection_id, status_tx)
.await
}
Opcode::Query => {
self.handle_query(frame, out, connection_id, status_tx)
.await
}
Opcode::Prepare => {
self.handle_prepare(frame, conn_state, out, connection_id, status_tx)
.await
}
Opcode::Execute => {
self.handle_execute(frame, conn_state, out, connection_id, status_tx)
.await
}
Opcode::AuthResponse => {
self.handle_auth_response(frame, conn_state, out, connection_id, status_tx)
.await
}
Opcode::Register => {
Log::new(Some(status_tx))
.debug("Cassandra: Client registered for events (not supported, no-op)");
self.send_ready(frame.stream_id, out, status_tx).await?;
Ok(true)
}
_ => {
Log::new(Some(status_tx))
.warn(format!("Unsupported Cassandra opcode: {:?}", frame.opcode));
self.send_error(
frame.stream_id,
0x000A,
"Unsupported operation",
out,
status_tx,
)
.await?;
Ok(true)
}
}
}
async fn handle_startup(
&self,
frame: Envelope,
conn_state: &mut CassandraConnectionState,
out: &ConnWrite,
connection_id: ConnectionId,
status_tx: &mpsc::UnboundedSender<String>,
) -> Result<bool> {
debug!("Handling STARTUP from connection {}", connection_id);
let protocol =
CassandraProtocol::new(connection_id, self.app_state.clone(), status_tx.clone());
let event = Event {
event_type: &CASSANDRA_STARTUP_EVENT,
data: json!({
"protocol_version": conn_state.protocol_version,
"options": {"CQL_VERSION": "3.0.0"}
}),
};
let server_id = self.server_id.context("Server ID not set")?;
let execution_result = match call_llm(
&self.llm_client,
&self.app_state,
server_id,
Some(connection_id),
&event,
&protocol,
)
.await
{
Ok(result) => result,
Err(e) => {
self.send_llm_failure_error(
frame.stream_id,
"STARTUP",
&e,
out,
connection_id,
status_tx,
)
.await?;
return Ok(true);
}
};
for message in &execution_result.messages {
Log::new(Some(status_tx)).info(format!("{}", message));
}
for action_result in execution_result.protocol_results {
match action_result {
ActionResult::Custom { name, data } => match name.as_str() {
"cassandra_ready" => {
conn_state.ready = true;
self.send_ready(frame.stream_id, out, status_tx).await?;
return Ok(true);
}
"cassandra_authenticate" => {
let authenticator = data
.get("authenticator")
.and_then(|v| v.as_str())
.unwrap_or("org.apache.cassandra.auth.PasswordAuthenticator");
self.send_authenticate(frame.stream_id, authenticator, out, status_tx)
.await?;
return Ok(true);
}
"cassandra_error" => {
let error_code = data
.get("error_code")
.and_then(|v| v.as_u64())
.unwrap_or(0x0000) as u32;
let message = data
.get("message")
.and_then(|v| v.as_str())
.unwrap_or("Unknown error");
self.send_model_error(
frame.stream_id,
"STARTUP",
error_code,
message,
out,
connection_id,
status_tx,
)
.await?;
return Ok(true);
}
_ => {}
},
ActionResult::CloseConnection => {
return Ok(false);
}
_ => {
warn!("Unexpected action result for STARTUP");
}
}
}
self.send_no_answer_error(frame.stream_id, "STARTUP", out, connection_id, status_tx)
.await?;
Ok(false)
}
async fn handle_options(
&self,
frame: Envelope,
out: &ConnWrite,
connection_id: ConnectionId,
status_tx: &mpsc::UnboundedSender<String>,
) -> Result<bool> {
debug!("Handling OPTIONS from connection {}", connection_id);
let protocol =
CassandraProtocol::new(connection_id, self.app_state.clone(), status_tx.clone());
let event = Event {
event_type: &CASSANDRA_OPTIONS_EVENT,
data: json!({}),
};
let server_id = self.server_id.context("Server ID not set")?;
let execution_result = match call_llm(
&self.llm_client,
&self.app_state,
server_id,
Some(connection_id),
&event,
&protocol,
)
.await
{
Ok(result) => result,
Err(e) => {
self.send_llm_failure_error(
frame.stream_id,
"OPTIONS",
&e,
out,
connection_id,
status_tx,
)
.await?;
return Ok(true);
}
};
for message in &execution_result.messages {
Log::new(Some(status_tx)).info(format!("{}", message));
}
for action_result in execution_result.protocol_results {
match action_result {
ActionResult::Custom { name, data } => match name.as_str() {
"cassandra_supported" => {
let options = data
.get("options")
.and_then(|v| v.as_object())
.cloned()
.unwrap_or_default();
self.send_supported(frame.stream_id, options, out, status_tx)
.await?;
return Ok(true);
}
"cassandra_error" => {
let error_code = data
.get("error_code")
.and_then(|v| v.as_u64())
.unwrap_or(0x0000) as u32;
let message = data
.get("message")
.and_then(|v| v.as_str())
.unwrap_or("Unknown error");
self.send_model_error(
frame.stream_id,
"OPTIONS",
error_code,
message,
out,
connection_id,
status_tx,
)
.await?;
return Ok(true);
}
_ => {}
},
ActionResult::CloseConnection => {
return Ok(false);
}
_ => {
warn!("Unexpected action result for OPTIONS");
}
}
}
self.send_supported(frame.stream_id, serde_json::Map::new(), out, status_tx)
.await?;
Ok(true)
}
async fn handle_query(
&self,
frame: Envelope,
out: &ConnWrite,
connection_id: ConnectionId,
status_tx: &mpsc::UnboundedSender<String>,
) -> Result<bool> {
let (query_str, consistency) = self.parse_query(&frame)?;
Log::new(Some(status_tx)).debug(format!(
"Cassandra ← Query from connection {}: {}",
connection_id, query_str
));
let protocol =
CassandraProtocol::new(connection_id, self.app_state.clone(), status_tx.clone());
let event = Event {
event_type: &CASSANDRA_QUERY_EVENT,
data: json!({
"query": query_str,
"consistency": consistency
}),
};
let server_id = self.server_id.context("Server ID not set")?;
let execution_result = match call_llm(
&self.llm_client,
&self.app_state,
server_id,
Some(connection_id),
&event,
&protocol,
)
.await
{
Ok(result) => result,
Err(e) => {
self.send_llm_failure_error(
frame.stream_id,
"QUERY",
&e,
out,
connection_id,
status_tx,
)
.await?;
return Ok(true);
}
};
for message in &execution_result.messages {
Log::new(Some(status_tx)).info(format!("{}", message));
}
for action_result in execution_result.protocol_results {
match action_result {
ActionResult::Custom { name, data } => match name.as_str() {
"cassandra_result_rows" => {
let columns = data
.get("columns")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default();
let rows = data
.get("rows")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default();
self.send_result_rows(frame.stream_id, columns, rows, out, status_tx)
.await?;
return Ok(true);
}
"cassandra_error" => {
let error_code = data
.get("error_code")
.and_then(|v| v.as_u64())
.unwrap_or(0x0000) as u32;
let message = data
.get("message")
.and_then(|v| v.as_str())
.unwrap_or("Unknown error");
self.send_model_error(
frame.stream_id,
"QUERY",
error_code,
message,
out,
connection_id,
status_tx,
)
.await?;
return Ok(true);
}
_ => {}
},
ActionResult::CloseConnection => {
return Ok(false);
}
_ => {
warn!("Unexpected action result for QUERY");
}
}
}
self.send_no_answer_error(frame.stream_id, "QUERY", out, connection_id, status_tx)
.await?;
Ok(true)
}
fn parse_query(&self, frame: &Envelope) -> Result<(String, &'static str)> {
let body = &frame.body;
if body.len() < 4 {
return Err(anyhow::anyhow!("Query frame too short"));
}
let query_len = u32::from_be_bytes([body[0], body[1], body[2], body[3]]) as usize;
if body.len().saturating_sub(4) < query_len {
return Err(anyhow::anyhow!("Query frame truncated"));
}
let query_bytes = &body[4..4 + query_len];
let query_str = String::from_utf8_lossy(query_bytes).to_string();
let consistency = body
.get(4 + query_len..6 + query_len)
.map(|b| consistency_name(u16::from_be_bytes([b[0], b[1]])))
.unwrap_or("UNKNOWN");
Ok((query_str, consistency))
}
async fn send_ready(
&self,
stream_id: i16,
out: &ConnWrite,
status_tx: &mpsc::UnboundedSender<String>,
) -> Result<()> {
let response = Envelope {
version: Version::V4,
direction: Direction::Response,
flags: Flags::empty(),
stream_id: stream_id,
opcode: Opcode::Ready,
body: vec![],
tracing_id: None,
warnings: vec![],
};
let bytes = response.encode_with(Compression::None)?;
out.write(&bytes).await?;
Log::new(Some(status_tx)).trace(format!("Cassandra → READY ({} bytes)", bytes.len()));
Ok(())
}
async fn send_supported(
&self,
stream_id: i16,
options: serde_json::Map<String, serde_json::Value>,
out: &ConnWrite,
status_tx: &mpsc::UnboundedSender<String>,
) -> Result<()> {
let mut body = Vec::new();
body.extend_from_slice(&(options.len() as u16).to_be_bytes());
for (key, value) in options.iter() {
let key_bytes = key.as_bytes();
body.extend_from_slice(&(key_bytes.len() as u16).to_be_bytes());
body.extend_from_slice(key_bytes);
if let Some(arr) = value.as_array() {
body.extend_from_slice(&(arr.len() as u16).to_be_bytes());
for item in arr {
if let Some(s) = item.as_str() {
let s_bytes = s.as_bytes();
body.extend_from_slice(&(s_bytes.len() as u16).to_be_bytes());
body.extend_from_slice(s_bytes);
}
}
} else {
body.extend_from_slice(&0u16.to_be_bytes());
}
}
let response = Envelope {
version: Version::V4,
direction: Direction::Response,
flags: Flags::empty(),
stream_id: stream_id,
opcode: Opcode::Supported,
body,
tracing_id: None,
warnings: vec![],
};
let bytes = response.encode_with(Compression::None)?;
out.write(&bytes).await?;
Log::new(Some(status_tx)).trace(format!("Cassandra → SUPPORTED ({} bytes)", bytes.len()));
Ok(())
}
async fn send_result_rows(
&self,
stream_id: i16,
columns: Vec<serde_json::Value>,
rows: Vec<serde_json::Value>,
out: &ConnWrite,
status_tx: &mpsc::UnboundedSender<String>,
) -> Result<()> {
let mut body = Vec::new();
body.extend_from_slice(&0x00000002u32.to_be_bytes());
body.extend_from_slice(&0x00000001u32.to_be_bytes());
body.extend_from_slice(&(columns.len() as u32).to_be_bytes());
let keyspace = b"system";
let table = b"local";
body.extend_from_slice(&(keyspace.len() as u16).to_be_bytes());
body.extend_from_slice(keyspace);
body.extend_from_slice(&(table.len() as u16).to_be_bytes());
body.extend_from_slice(table);
for col in &columns {
let name = col.get("name").and_then(|v| v.as_str()).unwrap_or("col");
let col_type = col
.get("type")
.and_then(|v| v.as_str())
.unwrap_or("varchar");
body.extend_from_slice(&(name.len() as u16).to_be_bytes());
body.extend_from_slice(name.as_bytes());
let type_code: u16 = match col_type {
"int" => 0x0009,
"boolean" => 0x0004,
_ => 0x000D, };
body.extend_from_slice(&type_code.to_be_bytes());
}
body.extend_from_slice(&(rows.len() as u32).to_be_bytes());
for row in &rows {
let empty = Vec::new();
let row_arr = row.as_array().unwrap_or(&empty);
if row_arr.len() != columns.len() {
warn!(
"Cassandra row has {} cell(s) but {} column(s) were declared; padding/truncating",
row_arr.len(),
columns.len()
);
}
for i in 0..columns.len() {
let col_type = columns[i].get("type").and_then(|v| v.as_str());
let cell_bytes = row_arr
.get(i)
.and_then(|cell| self.serialize_cell_value(cell, col_type));
if let Some(bytes) = cell_bytes {
body.extend_from_slice(&(bytes.len() as i32).to_be_bytes());
body.extend_from_slice(&bytes);
} else {
body.extend_from_slice(&(-1i32).to_be_bytes());
}
}
}
let response = Envelope {
version: Version::V4,
direction: Direction::Response,
flags: Flags::empty(),
stream_id: stream_id,
opcode: Opcode::Result,
body,
tracing_id: None,
warnings: vec![],
};
let bytes = response.encode_with(Compression::None)?;
out.write(&bytes).await?;
Log::new(Some(status_tx)).trace(format!(
"Cassandra → RESULT ({} rows, {} bytes)",
rows.len(),
bytes.len()
));
Ok(())
}
fn serialize_cell_value(
&self,
value: &serde_json::Value,
col_type: Option<&str>,
) -> Option<Vec<u8>> {
if value.is_null() {
return None;
}
match col_type.unwrap_or("varchar") {
"int" => {
let n = match value {
serde_json::Value::Number(n) => n.as_i64(),
serde_json::Value::String(s) => s.trim().parse::<i64>().ok(),
serde_json::Value::Bool(b) => Some(if *b { 1 } else { 0 }),
_ => None,
};
match n {
Some(n) if n >= i32::MIN as i64 && n <= i32::MAX as i64 => {
Some((n as i32).to_be_bytes().to_vec())
}
_ => {
warn!("Cassandra int column got {:?}, sending NULL", value);
None
}
}
}
"boolean" => {
let b = match value {
serde_json::Value::Bool(b) => Some(*b),
serde_json::Value::Number(n) => n.as_i64().map(|n| n != 0),
serde_json::Value::String(s) => match s.trim().to_ascii_lowercase().as_str() {
"true" | "1" | "yes" => Some(true),
"false" | "0" | "no" => Some(false),
_ => None,
},
_ => None,
};
match b {
Some(b) => Some(vec![u8::from(b)]),
None => {
warn!("Cassandra boolean column got {:?}, sending NULL", value);
None
}
}
}
_ => match value {
serde_json::Value::String(s) => Some(s.as_bytes().to_vec()),
other => Some(other.to_string().into_bytes()),
},
}
}
async fn handle_prepare(
&self,
frame: Envelope,
conn_state: &mut CassandraConnectionState,
out: &ConnWrite,
connection_id: ConnectionId,
status_tx: &mpsc::UnboundedSender<String>,
) -> Result<bool> {
debug!("Handling PREPARE from connection {}", connection_id);
let (query, _consistency) = self.parse_query(&frame)?;
trace!("PREPARE query: {}", query);
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
query.hash(&mut hasher);
let hash = hasher.finish();
let statement_id = hash.to_be_bytes().to_vec();
let param_count = query.matches('?').count();
if conn_state.prepared_statements.len() >= MAX_PREPARED_STATEMENTS
&& !conn_state.prepared_statements.contains_key(&statement_id)
{
warn!(
"Cassandra connection {} reached {} prepared statements; rejecting PREPARE",
connection_id, MAX_PREPARED_STATEMENTS
);
self.send_error(
frame.stream_id,
0x2200,
"Too many prepared statements on this connection",
out,
status_tx,
)
.await?;
return Ok(true);
}
conn_state
.prepared_statements
.insert(statement_id.clone(), (query.clone(), param_count));
debug!(
"Prepared statement ID {:?} with {} params",
statement_id, param_count
);
let protocol =
CassandraProtocol::new(connection_id, self.app_state.clone(), status_tx.clone());
let event = Event {
event_type: &CASSANDRA_PREPARE_EVENT,
data: json!({
"query": query,
"statement_id": hex::encode(&statement_id),
"param_count": param_count,
}),
};
let server_id = self.server_id.context("Server ID not set")?;
let execution_result = match call_llm(
&self.llm_client,
&self.app_state,
server_id,
Some(connection_id),
&event,
&protocol,
)
.await
{
Ok(result) => result,
Err(e) => {
self.send_llm_failure_error(
frame.stream_id,
"PREPARE",
&e,
out,
connection_id,
status_tx,
)
.await?;
return Ok(true);
}
};
for message in &execution_result.messages {
Log::new(Some(status_tx)).info(format!("{}", message));
}
for action_result in execution_result.protocol_results {
match action_result {
ActionResult::Custom { name, data } => match name.as_str() {
"cassandra_prepared" => {
let columns = data
.get("columns")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default();
let params = data.get("params").and_then(|v| v.as_array()).cloned();
self.send_prepared(
frame.stream_id,
statement_id,
columns,
params,
param_count,
out,
status_tx,
)
.await?;
return Ok(true);
}
"cassandra_error" => {
let error_code = data
.get("error_code")
.and_then(|v| v.as_u64())
.unwrap_or(0x0000) as u32;
let message = data
.get("message")
.and_then(|v| v.as_str())
.unwrap_or("Unknown error");
self.send_model_error(
frame.stream_id,
"PREPARE",
error_code,
message,
out,
connection_id,
status_tx,
)
.await?;
return Ok(true);
}
_ => {}
},
ActionResult::CloseConnection => {
return Ok(false);
}
_ => {
warn!("Unexpected action result for PREPARE");
}
}
}
self.send_no_answer_error(frame.stream_id, "PREPARE", out, connection_id, status_tx)
.await?;
Ok(true)
}
async fn handle_execute(
&self,
frame: Envelope,
conn_state: &mut CassandraConnectionState,
out: &ConnWrite,
connection_id: ConnectionId,
status_tx: &mpsc::UnboundedSender<String>,
) -> Result<bool> {
debug!("Handling EXECUTE from connection {}", connection_id);
let (statement_id, params, consistency) = self.parse_execute(&frame)?;
let Some((query, expected_param_count)) = conn_state
.prepared_statements
.get(&statement_id)
.map(|(q, n)| (q.clone(), *n))
else {
Log::new(Some(status_tx)).warn(format!(
"Cassandra connection {} decision=unprepared stage=EXECUTE: statement id {} is \
not prepared on this connection; answering ERROR 0x{:04X} so the driver \
re-prepares",
connection_id,
hex::encode(&statement_id),
CASSANDRA_ERROR_UNPREPARED
));
self.send_unprepared_error(frame.stream_id, &statement_id, out, status_tx)
.await?;
return Ok(true);
};
trace!("EXECUTE statement: {} with {} params", query, params.len());
if params.len() != expected_param_count {
let err_msg = format!(
"Expected {} parameters, got {}",
expected_param_count,
params.len()
);
self.send_error(frame.stream_id, 0x2200, &err_msg, out, status_tx)
.await?;
return Ok(true);
}
let protocol =
CassandraProtocol::new(connection_id, self.app_state.clone(), status_tx.clone());
let event = Event {
event_type: &CASSANDRA_EXECUTE_EVENT,
data: json!({
"query": query,
"statement_id": hex::encode(&statement_id),
"parameters": params,
"consistency": consistency,
}),
};
let server_id = self.server_id.context("Server ID not set")?;
let execution_result = match call_llm(
&self.llm_client,
&self.app_state,
server_id,
Some(connection_id),
&event,
&protocol,
)
.await
{
Ok(result) => result,
Err(e) => {
self.send_llm_failure_error(
frame.stream_id,
"EXECUTE",
&e,
out,
connection_id,
status_tx,
)
.await?;
return Ok(true);
}
};
for message in &execution_result.messages {
Log::new(Some(status_tx)).info(format!("{}", message));
}
for action_result in execution_result.protocol_results {
match action_result {
ActionResult::Custom { name, data } => match name.as_str() {
"cassandra_result_rows" => {
let columns = data
.get("columns")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default();
let rows = data
.get("rows")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default();
self.send_result_rows(frame.stream_id, columns, rows, out, status_tx)
.await?;
return Ok(true);
}
"cassandra_error" => {
let error_code = data
.get("error_code")
.and_then(|v| v.as_u64())
.unwrap_or(0x0000) as u32;
let message = data
.get("message")
.and_then(|v| v.as_str())
.unwrap_or("Unknown error");
self.send_model_error(
frame.stream_id,
"EXECUTE",
error_code,
message,
out,
connection_id,
status_tx,
)
.await?;
return Ok(true);
}
_ => {}
},
ActionResult::CloseConnection => {
return Ok(false);
}
_ => {
warn!("Unexpected action result for EXECUTE");
}
}
}
self.send_no_answer_error(frame.stream_id, "EXECUTE", out, connection_id, status_tx)
.await?;
Ok(true)
}
fn parse_execute(
&self,
frame: &Envelope,
) -> Result<(Vec<u8>, Vec<serde_json::Value>, &'static str)> {
let body = &frame.body;
if body.len() < 2 {
return Err(anyhow::anyhow!("EXECUTE frame too short"));
}
let id_len = u16::from_be_bytes([body[0], body[1]]) as usize;
if body.len() < 2 + id_len {
return Err(anyhow::anyhow!("EXECUTE frame truncated (statement ID)"));
}
let statement_id = body[2..2 + id_len].to_vec();
let mut offset = 2 + id_len;
if body.len() < offset + 2 {
return Err(anyhow::anyhow!("EXECUTE frame truncated (consistency)"));
}
let consistency = consistency_name(u16::from_be_bytes([body[offset], body[offset + 1]]));
offset += 2;
if body.len() < offset + 1 {
return Err(anyhow::anyhow!("EXECUTE frame truncated (flags)"));
}
let flags = body[offset];
offset += 1;
let mut params = Vec::new();
if flags & 0x01 != 0 {
if body.len() < offset + 2 {
return Err(anyhow::anyhow!("EXECUTE frame truncated (param count)"));
}
let param_count = u16::from_be_bytes([body[offset], body[offset + 1]]) as usize;
offset += 2;
for _ in 0..param_count {
if body.len() < offset + 4 {
return Err(anyhow::anyhow!("EXECUTE frame truncated (param length)"));
}
let param_len = i32::from_be_bytes([
body[offset],
body[offset + 1],
body[offset + 2],
body[offset + 3],
]);
offset += 4;
if param_len < 0 {
params.push(serde_json::Value::Null);
} else {
let param_len = param_len as usize;
if body.len() < offset + param_len {
return Err(anyhow::anyhow!("EXECUTE frame truncated (param data)"));
}
let param_bytes = &body[offset..offset + param_len];
let param_str = String::from_utf8_lossy(param_bytes).to_string();
params.push(json!(param_str));
offset += param_len;
}
}
}
Ok((statement_id, params, consistency))
}
async fn send_prepared(
&self,
stream_id: i16,
statement_id: Vec<u8>,
columns: Vec<serde_json::Value>,
params: Option<Vec<serde_json::Value>>,
param_count: usize,
out: &ConnWrite,
status_tx: &mpsc::UnboundedSender<String>,
) -> Result<()> {
let mut body = Vec::new();
body.extend_from_slice(&0x00000004u32.to_be_bytes());
body.extend_from_slice(&(statement_id.len() as u16).to_be_bytes());
body.extend_from_slice(&statement_id);
let keyspace = b"netget";
let table = b"data";
body.extend_from_slice(&0x00000001u32.to_be_bytes());
body.extend_from_slice(&(param_count as u32).to_be_bytes());
body.extend_from_slice(&0u32.to_be_bytes());
body.extend_from_slice(&(keyspace.len() as u16).to_be_bytes());
body.extend_from_slice(keyspace);
body.extend_from_slice(&(table.len() as u16).to_be_bytes());
body.extend_from_slice(table);
for i in 0..param_count {
let param_name = format!("param{}", i);
let param_bytes = param_name.as_bytes();
body.extend_from_slice(&(param_bytes.len() as u16).to_be_bytes());
body.extend_from_slice(param_bytes);
let param_type = params
.as_ref()
.and_then(|p| p.get(i))
.and_then(|param| param.get("type"))
.and_then(|v| v.as_str())
.unwrap_or("varchar");
let type_code: u16 = match param_type {
"int" => 0x0009,
"varchar" | "text" => 0x000D,
"boolean" => 0x0004,
_ => 0x000D, };
body.extend_from_slice(&type_code.to_be_bytes());
}
body.extend_from_slice(&0x00000001u32.to_be_bytes());
body.extend_from_slice(&(columns.len() as u32).to_be_bytes());
body.extend_from_slice(&(keyspace.len() as u16).to_be_bytes());
body.extend_from_slice(keyspace);
body.extend_from_slice(&(table.len() as u16).to_be_bytes());
body.extend_from_slice(table);
for col in columns {
let name = col.get("name").and_then(|v| v.as_str()).unwrap_or("col");
let col_type = col
.get("type")
.and_then(|v| v.as_str())
.unwrap_or("varchar");
let name_bytes = name.as_bytes();
body.extend_from_slice(&(name_bytes.len() as u16).to_be_bytes());
body.extend_from_slice(name_bytes);
let type_code: u16 = match col_type {
"int" => 0x0009,
"varchar" | "text" => 0x000D,
"boolean" => 0x0004,
_ => 0x000D, };
body.extend_from_slice(&type_code.to_be_bytes());
}
trace!(
"PREPARED response body hex (first 200 bytes): {}",
hex::encode(&body[..std::cmp::min(200, body.len())])
);
let response = Envelope {
version: Version::V4,
direction: Direction::Response,
flags: Flags::empty(),
stream_id: stream_id,
opcode: Opcode::Result,
body,
tracing_id: None,
warnings: vec![],
};
let bytes = response.encode_with(Compression::None)?;
out.write(&bytes).await?;
Log::new(Some(status_tx)).trace(format!(
"Cassandra → RESULT (Prepared: {} params, {} bytes)",
param_count,
bytes.len()
));
Ok(())
}
async fn send_llm_failure_error(
&self,
stream_id: i16,
stage: &str,
err: &anyhow::Error,
out: &ConnWrite,
connection_id: ConnectionId,
status_tx: &mpsc::UnboundedSender<String>,
) -> Result<()> {
let overloaded = crate::llm::is_overload_error(err);
let (code, label) = if overloaded {
(CASSANDRA_ERROR_OVERLOADED, "Overloaded")
} else {
(CASSANDRA_ERROR_SERVER_ERROR, "Server error")
};
let message = crate::utils::WireFailure::classify(err).prefixed_text();
Log::new(Some(status_tx)).warn(format!(
"Cassandra connection {} answering {} with ERROR 0x{:04X} ({}): {}",
connection_id, stage, code, label, message
));
self.send_error(stream_id, code, &message, out, status_tx)
.await
}
async fn send_no_answer_error(
&self,
stream_id: i16,
stage: &str,
out: &ConnWrite,
connection_id: ConnectionId,
status_tx: &mpsc::UnboundedSender<String>,
) -> Result<()> {
Log::new(Some(status_tx)).warn(format!(
"Cassandra connection {} decision=fail_closed_no_answer stage={}: handler produced \
no recognised action, answering ERROR 0x{:04X} (Server error)",
connection_id, stage, CASSANDRA_ERROR_SERVER_ERROR
));
self.send_error(
stream_id,
CASSANDRA_ERROR_SERVER_ERROR,
"netget: no handler answer for this request",
out,
status_tx,
)
.await
}
async fn send_model_error(
&self,
stream_id: i16,
stage: &str,
error_code: u32,
message: &str,
out: &ConnWrite,
connection_id: ConnectionId,
status_tx: &mpsc::UnboundedSender<String>,
) -> Result<()> {
Log::new(Some(status_tx)).info(format!(
"Cassandra connection {} decision=model_reject stage={}: ERROR 0x{:04X}",
connection_id, stage, error_code
));
self.send_error(stream_id, error_code, message, out, status_tx)
.await
}
async fn send_unprepared_error(
&self,
stream_id: i16,
statement_id: &[u8],
out: &ConnWrite,
status_tx: &mpsc::UnboundedSender<String>,
) -> Result<()> {
let message = "netget: statement not prepared on this connection";
let mut body = Vec::new();
body.extend_from_slice(&CASSANDRA_ERROR_UNPREPARED.to_be_bytes());
body.extend_from_slice(&(message.len() as u16).to_be_bytes());
body.extend_from_slice(message.as_bytes());
let id_len = u16::try_from(statement_id.len()).unwrap_or(u16::MAX) as usize;
body.extend_from_slice(&(id_len as u16).to_be_bytes());
body.extend_from_slice(&statement_id[..id_len]);
let response = Envelope {
version: Version::V4,
direction: Direction::Response,
flags: Flags::empty(),
stream_id,
opcode: Opcode::Error,
body,
tracing_id: None,
warnings: vec![],
};
let bytes = response.encode_with(Compression::None)?;
out.write(&bytes).await?;
Log::new(Some(status_tx)).trace(format!(
"Cassandra → ERROR 0x{:04X} UNPREPARED id={}",
CASSANDRA_ERROR_UNPREPARED,
hex::encode(statement_id)
));
Ok(())
}
async fn send_error(
&self,
stream_id: i16,
error_code: u32,
message: &str,
out: &ConnWrite,
status_tx: &mpsc::UnboundedSender<String>,
) -> Result<()> {
let mut body = Vec::new();
body.extend_from_slice(&error_code.to_be_bytes());
let msg_bytes = message.as_bytes();
body.extend_from_slice(&(msg_bytes.len() as u16).to_be_bytes());
body.extend_from_slice(msg_bytes);
let response = Envelope {
version: Version::V4,
direction: Direction::Response,
flags: Flags::empty(),
stream_id: stream_id,
opcode: Opcode::Error,
body,
tracing_id: None,
warnings: vec![],
};
let bytes = response.encode_with(Compression::None)?;
out.write(&bytes).await?;
Log::new(Some(status_tx)).trace(format!(
"Cassandra → ERROR 0x{:04X} - {}",
error_code, message
));
Ok(())
}
async fn handle_auth_response(
&self,
frame: Envelope,
conn_state: &mut CassandraConnectionState,
out: &ConnWrite,
connection_id: ConnectionId,
status_tx: &mpsc::UnboundedSender<String>,
) -> Result<bool> {
debug!("Handling AUTH_RESPONSE from connection {}", connection_id);
let body = &frame.body;
let (username, password) = self.parse_sasl_plain(body)?;
trace!("AUTH_RESPONSE: username={}", username);
let protocol =
CassandraProtocol::new(connection_id, self.app_state.clone(), status_tx.clone());
let event = Event {
event_type: &CASSANDRA_AUTH_EVENT,
data: json!({
"username": username,
"password": password,
}),
};
let server_id = self.server_id.context("Server ID not set")?;
let execution_result = match call_llm(
&self.llm_client,
&self.app_state,
server_id,
Some(connection_id),
&event,
&protocol,
)
.await
{
Ok(result) => result,
Err(e) => {
self.send_llm_failure_error(
frame.stream_id,
"AUTH_RESPONSE",
&e,
out,
connection_id,
status_tx,
)
.await?;
return Ok(true);
}
};
for message in &execution_result.messages {
Log::new(Some(status_tx)).info(format!("{}", message));
}
for action_result in execution_result.protocol_results {
match action_result {
ActionResult::Custom { name, data } => {
match name.as_str() {
"cassandra_auth_success" => {
conn_state.authenticated = true;
conn_state.username = Some(username);
self.send_auth_success(frame.stream_id, out, status_tx)
.await?;
return Ok(true);
}
"cassandra_error" => {
let error_code =
data.get("error_code")
.and_then(|v| v.as_u64())
.unwrap_or(0x0000) as u32;
let message = data
.get("message")
.and_then(|v| v.as_str())
.unwrap_or("Unknown error");
self.send_model_error(
frame.stream_id,
"AUTH_RESPONSE",
error_code,
message,
out,
connection_id,
status_tx,
)
.await?;
return Ok(false); }
_ => {}
}
}
ActionResult::CloseConnection => {
return Ok(false);
}
_ => {
warn!("Unexpected action result for AUTH_RESPONSE");
}
}
}
self.send_error(
frame.stream_id,
0x0100,
"Authentication failed",
out,
status_tx,
)
.await?;
Ok(false)
}
fn parse_sasl_plain(&self, body: &[u8]) -> Result<(String, String)> {
if body.is_empty() {
return Err(anyhow::anyhow!("Empty AUTH_RESPONSE body"));
}
let mut idx = 0;
while idx < body.len() && body[idx] != 0 {
idx += 1;
}
idx += 1;
if idx >= body.len() {
return Err(anyhow::anyhow!("Invalid SASL PLAIN format"));
}
let username_start = idx;
while idx < body.len() && body[idx] != 0 {
idx += 1;
}
let username = String::from_utf8_lossy(&body[username_start..idx]).to_string();
idx += 1;
if idx >= body.len() {
return Err(anyhow::anyhow!(
"Invalid SASL PLAIN format - missing password"
));
}
let password = String::from_utf8_lossy(&body[idx..]).to_string();
Ok((username, password))
}
async fn send_authenticate(
&self,
stream_id: i16,
authenticator: &str,
out: &ConnWrite,
status_tx: &mpsc::UnboundedSender<String>,
) -> Result<()> {
let mut body = Vec::new();
let auth_bytes = authenticator.as_bytes();
body.extend_from_slice(&(auth_bytes.len() as u16).to_be_bytes());
body.extend_from_slice(auth_bytes);
let response = Envelope {
version: Version::V4,
direction: Direction::Response,
flags: Flags::empty(),
stream_id: stream_id,
opcode: Opcode::Authenticate,
body,
tracing_id: None,
warnings: vec![],
};
let bytes = response.encode_with(Compression::None)?;
out.write(&bytes).await?;
Log::new(Some(status_tx)).trace(format!("Cassandra → AUTHENTICATE ({})", authenticator));
Ok(())
}
async fn send_auth_success(
&self,
stream_id: i16,
out: &ConnWrite,
status_tx: &mpsc::UnboundedSender<String>,
) -> Result<()> {
let body = vec![];
let response = Envelope {
version: Version::V4,
direction: Direction::Response,
flags: Flags::empty(),
stream_id: stream_id,
opcode: Opcode::AuthSuccess,
body,
tracing_id: None,
warnings: vec![],
};
let bytes = response.encode_with(Compression::None)?;
out.write(&bytes).await?;
Log::new(Some(status_tx)).trace("Cassandra → AUTH_SUCCESS");
Ok(())
}
}