use crate::protocol::Message;
use powdb_query::executor::{is_read_only_statement, Engine};
use powdb_query::parser;
use powdb_query::result::{QueryError, QueryResult};
use powdb_storage::types::Value;
use std::collections::HashMap;
use std::net::IpAddr;
use std::sync::{Arc, Mutex, RwLock};
use std::time::{Duration, Instant};
use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt, BufReader, BufWriter};
use tokio::sync::watch;
use tracing::{debug, error, info, warn};
use zeroize::Zeroizing;
pub type AuthRateLimiter = Arc<Mutex<HashMap<IpAddr, (u32, Instant)>>>;
const MAX_QUERY_LENGTH: usize = 1024 * 1024;
const WRITE_TIMEOUT: Duration = Duration::from_secs(30);
const MAX_AUTH_FAILURES: u32 = 5;
const AUTH_FAILURE_WINDOW: Duration = Duration::from_secs(60);
pub fn new_rate_limiter() -> AuthRateLimiter {
Arc::new(Mutex::new(HashMap::new()))
}
fn is_rate_limited(limiter: &AuthRateLimiter, ip: IpAddr) -> bool {
let mut map = limiter.lock().unwrap_or_else(|e| e.into_inner());
let now = Instant::now();
map.retain(|_, (_, ts)| now.duration_since(*ts) < AUTH_FAILURE_WINDOW);
if let Some((count, _)) = map.get(&ip) {
*count >= MAX_AUTH_FAILURES
} else {
false
}
}
fn record_auth_failure(limiter: &AuthRateLimiter, ip: IpAddr) {
let mut map = limiter.lock().unwrap_or_else(|e| e.into_inner());
let now = Instant::now();
let entry = map.entry(ip).or_insert((0, now));
if now.duration_since(entry.1) >= AUTH_FAILURE_WINDOW {
*entry = (1, now);
} else {
entry.0 += 1;
}
}
fn clear_auth_failures(limiter: &AuthRateLimiter, ip: IpAddr) {
let mut map = limiter.lock().unwrap_or_else(|e| e.into_inner());
map.remove(&ip);
}
fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
use sha2::{Digest, Sha256};
let ha = Sha256::digest(a);
let hb = Sha256::digest(b);
let mut diff = 0u8;
for (x, y) in ha.iter().zip(hb.iter()) {
diff |= x ^ y;
}
diff == 0
}
const SAFE_ERROR_PREFIXES: &[&str] = &[
"table not found",
"column not found",
"parse error",
"type mismatch",
"unknown table",
"unknown column",
"unknown function",
"syntax error",
"expected",
"unexpected",
"missing",
"duplicate",
"invalid",
"cannot",
"no such",
"already exists",
];
fn sanitize_error(e: &str) -> String {
let lower = e.to_lowercase();
for prefix in SAFE_ERROR_PREFIXES {
if lower.starts_with(prefix) {
return e.to_string();
}
}
"query execution error".into()
}
async fn write_msg<W: AsyncWrite + Unpin>(writer: &mut BufWriter<W>, msg: &Message) -> bool {
let write_fut = async {
if msg.write_to(writer).await.is_err() {
return false;
}
writer.flush().await.is_ok()
};
tokio::time::timeout(WRITE_TIMEOUT, write_fut)
.await
.unwrap_or_default()
}
pub struct ConnOpts<'a> {
pub engine: Arc<RwLock<Engine>>,
pub expected_password: Option<Zeroizing<String>>,
pub shutdown_rx: &'a mut watch::Receiver<bool>,
pub idle_timeout: Duration,
pub query_timeout: Duration,
pub rate_limiter: Option<&'a AuthRateLimiter>,
pub peer_addr: Option<std::net::SocketAddr>,
}
fn dispatch_query(engine: &Arc<RwLock<Engine>>, query: &str) -> Result<QueryResult, QueryError> {
let stmt_result = parser::parse(query).map_err(|e| e.to_string());
let can_try_read = matches!(&stmt_result, Ok(s) if is_read_only_statement(s));
if can_try_read {
let res = {
let eng = engine
.read()
.map_err(|e| QueryError::Execution(format!("lock poisoned: {e}")))?;
eng.execute_powql_readonly(query)
};
match res {
Ok(r) => return Ok(r),
Err(QueryError::ReadonlyNeedsWrite) => {
}
Err(e) => return Err(e),
}
}
let mut eng = engine
.write()
.map_err(|e| QueryError::Execution(format!("lock poisoned: {e}")))?;
eng.execute_powql(query)
}
pub async fn handle_connection<S>(stream: S, opts: ConnOpts<'_>)
where
S: AsyncRead + AsyncWrite + Unpin,
{
let ConnOpts {
engine,
expected_password,
shutdown_rx,
idle_timeout,
query_timeout,
rate_limiter,
peer_addr,
} = opts;
let peer = peer_addr
.map(|a| a.to_string())
.unwrap_or_else(|| "unknown".into());
let peer_ip = peer_addr.map(|a| a.ip());
let (reader, writer) = tokio::io::split(stream);
let mut reader = BufReader::new(reader);
let mut writer = BufWriter::new(writer);
let connect_msg = loop {
match tokio::time::timeout(idle_timeout, Message::read_from_preauth(&mut reader)).await {
Ok(Ok(Some(Message::Ping))) => {
debug!(peer = %peer, "pre-auth ping");
if !write_msg(&mut writer, &Message::Pong).await {
return;
}
continue;
}
Ok(Ok(Some(msg))) => break msg,
Ok(Ok(None)) => {
debug!(peer = %peer, "client closed before CONNECT");
return;
}
Ok(Err(e)) => {
error!(peer = %peer, error = %e, "error reading CONNECT");
return;
}
Err(_) => {
warn!(peer = %peer, "idle timeout waiting for CONNECT");
return;
}
}
};
match connect_msg {
Message::Connect { db_name, password } => {
if let (Some(limiter), Some(ip)) = (rate_limiter, peer_ip) {
if is_rate_limited(limiter, ip) {
warn!(peer = %peer, "rate limited: too many auth failures");
let err = Message::Error {
message: "too many auth failures, try again later".into(),
};
write_msg(&mut writer, &err).await;
return;
}
}
if let Some(expected) = &expected_password {
if !password
.as_deref()
.is_some_and(|p| constant_time_eq(p.as_bytes(), expected.as_bytes()))
{
warn!(peer = %peer, db = %db_name, "auth rejected: bad password");
if let (Some(limiter), Some(ip)) = (rate_limiter, peer_ip) {
record_auth_failure(limiter, ip);
}
let err = Message::Error {
message: "authentication failed".into(),
};
write_msg(&mut writer, &err).await;
return;
}
}
if let (Some(limiter), Some(ip)) = (rate_limiter, peer_ip) {
clear_auth_failures(limiter, ip);
}
info!(peer = %peer, db = %db_name, "client connected");
let ok = Message::ConnectOk {
version: env!("CARGO_PKG_VERSION").into(),
};
if !write_msg(&mut writer, &ok).await {
return;
}
}
_ => {
warn!(peer = %peer, "first message was not CONNECT");
let err = Message::Error {
message: "expected CONNECT".into(),
};
write_msg(&mut writer, &err).await;
return;
}
}
loop {
let msg = tokio::select! {
result = tokio::time::timeout(idle_timeout, Message::read_from(&mut reader)) => {
match result {
Ok(Ok(Some(msg))) => msg,
Ok(Ok(None)) => break,
Ok(Err(e)) => {
error!(peer = %peer, error = %e, "read error");
break;
}
Err(_) => {
info!(peer = %peer, "idle timeout, closing connection");
let err = Message::Error { message: "idle timeout".into() };
write_msg(&mut writer, &err).await;
break;
}
}
}
_ = shutdown_rx.changed() => {
if *shutdown_rx.borrow() {
info!(peer = %peer, "server shutting down, closing connection");
let err = Message::Error { message: "server shutting down".into() };
write_msg(&mut writer, &err).await;
break;
}
continue;
}
};
let response = match msg {
Message::Ping => {
debug!(peer = %peer, "ping");
Message::Pong
}
Message::Query { query } => {
if query.len() > MAX_QUERY_LENGTH {
Message::Error {
message: format!(
"query too large: {} bytes (max {})",
query.len(),
MAX_QUERY_LENGTH
),
}
} else {
debug!(peer = %peer, query = %query, "received query");
let handle = tokio::task::spawn_blocking({
let engine = engine.clone();
let query = query.clone();
move || dispatch_query(&engine, &query)
});
let abort_handle = handle.abort_handle();
match tokio::time::timeout(query_timeout, handle).await {
Ok(Ok(Ok(result))) => query_result_to_message(result),
Ok(Ok(Err(e))) => Message::Error {
message: sanitize_error(&e.to_string()),
},
Ok(Err(e)) => Message::Error {
message: format!("internal error: {e}"),
},
Err(_) => {
abort_handle.abort();
warn!(peer = %peer, query = %query, "query timeout exceeded");
Message::Error {
message: "query timeout exceeded".into(),
}
}
}
}
}
Message::Disconnect => {
debug!(peer = %peer, "received DISCONNECT");
break;
}
_ => Message::Error {
message: "unexpected message type".into(),
},
};
if !write_msg(&mut writer, &response).await {
break;
}
}
info!(peer = %peer, "client disconnected");
}
fn query_result_to_message(result: QueryResult) -> Message {
match result {
QueryResult::Rows { columns, rows } => {
let str_rows: Vec<Vec<String>> = rows
.iter()
.map(|row| row.iter().map(value_to_display).collect())
.collect();
Message::ResultRows {
columns,
rows: str_rows,
}
}
QueryResult::Scalar(val) => Message::ResultScalar {
value: value_to_display(&val),
},
QueryResult::Modified(n) => Message::ResultOk { affected: n },
QueryResult::Created(name) => Message::ResultMessage {
message: format!("type {name} created"),
},
QueryResult::Executed { message } => Message::ResultMessage { message },
}
}
fn value_to_display(v: &Value) -> String {
match v {
Value::Int(n) => n.to_string(),
Value::Float(n) => format!("{n}"),
Value::Bool(b) => b.to_string(),
Value::Str(s) => s.clone(),
Value::DateTime(t) => format!("{t}"),
Value::Uuid(u) => format!("{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
u[0], u[1], u[2], u[3], u[4], u[5], u[6], u[7],
u[8], u[9], u[10], u[11], u[12], u[13], u[14], u[15]),
Value::Bytes(b) => format!("<{} bytes>", b.len()),
Value::Empty => "{}".into(),
}
}