1use crate::protocol::Message;
2use powdb_auth::UserStore;
3use powdb_query::executor::{is_read_only_statement, Engine};
4use powdb_query::parser;
5use powdb_query::result::{QueryError, QueryResult};
6use powdb_storage::types::Value;
7use std::collections::HashMap;
8use std::net::IpAddr;
9use std::sync::{Arc, Mutex, RwLock};
10use std::time::{Duration, Instant};
11use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt, BufReader, BufWriter};
12use tokio::sync::watch;
13use tracing::{debug, error, info, warn};
14use zeroize::Zeroizing;
15
16pub type AuthRateLimiter = Arc<Mutex<HashMap<IpAddr, (u32, Instant)>>>;
18
19const MAX_QUERY_LENGTH: usize = 1024 * 1024;
21
22const WRITE_TIMEOUT: Duration = Duration::from_secs(30);
25
26const MAX_AUTH_FAILURES: u32 = 5;
28
29const AUTH_FAILURE_WINDOW: Duration = Duration::from_secs(60);
31
32pub fn new_rate_limiter() -> AuthRateLimiter {
34 Arc::new(Mutex::new(HashMap::new()))
35}
36
37fn is_rate_limited(limiter: &AuthRateLimiter, ip: IpAddr) -> bool {
40 let mut map = limiter.lock().unwrap_or_else(|e| e.into_inner());
41 let now = Instant::now();
43 map.retain(|_, (_, ts)| now.duration_since(*ts) < AUTH_FAILURE_WINDOW);
44
45 if let Some((count, _)) = map.get(&ip) {
46 *count >= MAX_AUTH_FAILURES
47 } else {
48 false
49 }
50}
51
52fn record_auth_failure(limiter: &AuthRateLimiter, ip: IpAddr) {
54 let mut map = limiter.lock().unwrap_or_else(|e| e.into_inner());
55 let now = Instant::now();
56 let entry = map.entry(ip).or_insert((0, now));
57 if now.duration_since(entry.1) >= AUTH_FAILURE_WINDOW {
59 *entry = (1, now);
60 } else {
61 entry.0 += 1;
62 }
63}
64
65fn clear_auth_failures(limiter: &AuthRateLimiter, ip: IpAddr) {
67 let mut map = limiter.lock().unwrap_or_else(|e| e.into_inner());
68 map.remove(&ip);
69}
70
71fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
74 use sha2::{Digest, Sha256};
75 let ha = Sha256::digest(a);
76 let hb = Sha256::digest(b);
77 let mut diff = 0u8;
78 for (x, y) in ha.iter().zip(hb.iter()) {
79 diff |= x ^ y;
80 }
81 diff == 0
82}
83
84#[derive(Debug, Clone, PartialEq, Eq)]
88pub struct Principal {
89 pub name: String,
90 pub role: String,
91}
92
93#[derive(Debug, Clone, PartialEq, Eq)]
95pub enum AuthOutcome {
96 Authenticated { principal: Option<Principal> },
100 Rejected,
103}
104
105pub fn authenticate_connect(
117 users: &UserStore,
118 expected_password: Option<&str>,
119 username: Option<&str>,
120 password: Option<&str>,
121) -> AuthOutcome {
122 if !users.is_empty() {
123 let Some(name) = username else {
125 return AuthOutcome::Rejected;
126 };
127 let Some(candidate) = password else {
128 return AuthOutcome::Rejected;
129 };
130 match users.authenticate(name, candidate) {
131 Some(user) => AuthOutcome::Authenticated {
132 principal: Some(Principal {
133 name: user.name.clone(),
134 role: user.role.clone(),
135 }),
136 },
137 None => AuthOutcome::Rejected,
138 }
139 } else {
140 match expected_password {
142 Some(expected) => {
143 if password.is_some_and(|p| constant_time_eq(p.as_bytes(), expected.as_bytes())) {
144 AuthOutcome::Authenticated { principal: None }
145 } else {
146 AuthOutcome::Rejected
147 }
148 }
149 None => AuthOutcome::Authenticated { principal: None },
150 }
151 }
152}
153
154const SAFE_ERROR_PREFIXES: &[&str] = &[
156 "table not found",
157 "column not found",
158 "parse error",
159 "type mismatch",
160 "unknown table",
161 "unknown column",
162 "unknown function",
163 "syntax error",
164 "expected",
165 "unexpected",
166 "missing",
167 "duplicate",
168 "invalid",
169 "cannot",
170 "no such",
171 "already exists",
172];
173
174fn sanitize_error(e: &str) -> String {
178 let lower = e.to_lowercase();
179 for prefix in SAFE_ERROR_PREFIXES {
180 if lower.starts_with(prefix) {
181 return e.to_string();
182 }
183 }
184 "query execution error".into()
185}
186
187async fn write_msg<W: AsyncWrite + Unpin>(writer: &mut BufWriter<W>, msg: &Message) -> bool {
190 let write_fut = async {
191 if msg.write_to(writer).await.is_err() {
192 return false;
193 }
194 writer.flush().await.is_ok()
195 };
196 tokio::time::timeout(WRITE_TIMEOUT, write_fut)
197 .await
198 .unwrap_or_default()
199}
200
201pub struct ConnOpts<'a> {
204 pub engine: Arc<RwLock<Engine>>,
205 pub expected_password: Option<Zeroizing<String>>,
208 pub users: Arc<UserStore>,
212 pub shutdown_rx: &'a mut watch::Receiver<bool>,
213 pub idle_timeout: Duration,
214 pub query_timeout: Duration,
215 pub rate_limiter: Option<&'a AuthRateLimiter>,
216 pub peer_addr: Option<std::net::SocketAddr>,
217}
218
219fn dispatch_query(engine: &Arc<RwLock<Engine>>, query: &str) -> Result<QueryResult, QueryError> {
223 let stmt_result = parser::parse(query).map_err(|e| e.to_string());
224
225 let can_try_read = matches!(&stmt_result, Ok(s) if is_read_only_statement(s));
226 if can_try_read {
227 let res = {
228 let eng = engine
229 .read()
230 .map_err(|e| QueryError::Execution(format!("lock poisoned: {e}")))?;
231 eng.execute_powql_readonly(query)
232 };
233 match res {
234 Ok(r) => return Ok(r),
235 Err(QueryError::ReadonlyNeedsWrite) => {
236 }
238 Err(e) => return Err(e),
239 }
240 }
241
242 let mut eng = engine
243 .write()
244 .map_err(|e| QueryError::Execution(format!("lock poisoned: {e}")))?;
245 eng.execute_powql(query)
246}
247
248pub async fn handle_connection<S>(stream: S, opts: ConnOpts<'_>)
249where
250 S: AsyncRead + AsyncWrite + Unpin,
251{
252 let ConnOpts {
253 engine,
254 expected_password,
255 users,
256 shutdown_rx,
257 idle_timeout,
258 query_timeout,
259 rate_limiter,
260 peer_addr,
261 } = opts;
262
263 let peer = peer_addr
264 .map(|a| a.to_string())
265 .unwrap_or_else(|| "unknown".into());
266 let peer_ip = peer_addr.map(|a| a.ip());
267
268 let (reader, writer) = tokio::io::split(stream);
269 let mut reader = BufReader::new(reader);
270 let mut writer = BufWriter::new(writer);
271
272 let connect_msg = loop {
277 match tokio::time::timeout(idle_timeout, Message::read_from_preauth(&mut reader)).await {
278 Ok(Ok(Some(Message::Ping))) => {
279 debug!(peer = %peer, "pre-auth ping");
280 if !write_msg(&mut writer, &Message::Pong).await {
281 return;
282 }
283 continue;
284 }
285 Ok(Ok(Some(msg))) => break msg,
286 Ok(Ok(None)) => {
287 debug!(peer = %peer, "client closed before CONNECT");
288 return;
289 }
290 Ok(Err(e)) => {
291 error!(peer = %peer, error = %e, "error reading CONNECT");
292 return;
293 }
294 Err(_) => {
295 warn!(peer = %peer, "idle timeout waiting for CONNECT");
296 return;
297 }
298 }
299 };
300
301 let _principal: Option<Principal>;
304 match connect_msg {
305 Message::Connect {
306 db_name,
307 password,
308 username,
309 } => {
310 if let (Some(limiter), Some(ip)) = (rate_limiter, peer_ip) {
312 if is_rate_limited(limiter, ip) {
313 warn!(peer = %peer, "rate limited: too many auth failures");
314 let err = Message::Error {
315 message: "too many auth failures, try again later".into(),
316 };
317 write_msg(&mut writer, &err).await;
318 return;
319 }
320 }
321
322 let outcome = authenticate_connect(
323 &users,
324 expected_password.as_ref().map(|p| p.as_str()),
325 username.as_deref(),
326 password.as_ref().map(|p| p.as_str()),
327 );
328
329 match outcome {
330 AuthOutcome::Rejected => {
331 warn!(peer = %peer, db = %db_name, "auth rejected");
332 if let (Some(limiter), Some(ip)) = (rate_limiter, peer_ip) {
334 record_auth_failure(limiter, ip);
335 }
336 let err = Message::Error {
337 message: "authentication failed".into(),
338 };
339 write_msg(&mut writer, &err).await;
340 return;
341 }
342 AuthOutcome::Authenticated { principal } => {
343 if let (Some(limiter), Some(ip)) = (rate_limiter, peer_ip) {
345 clear_auth_failures(limiter, ip);
346 }
347 match &principal {
348 Some(p) => {
349 info!(peer = %peer, db = %db_name, user = %p.name, role = %p.role, "authenticated");
350 }
351 None => {
352 info!(peer = %peer, db = %db_name, "client connected");
353 }
354 }
355 _principal = principal;
356 }
357 }
358
359 let ok = Message::ConnectOk {
360 version: env!("CARGO_PKG_VERSION").into(),
361 };
362 if !write_msg(&mut writer, &ok).await {
363 return;
364 }
365 }
366 _ => {
367 warn!(peer = %peer, "first message was not CONNECT");
368 let err = Message::Error {
369 message: "expected CONNECT".into(),
370 };
371 write_msg(&mut writer, &err).await;
372 return;
373 }
374 }
375
376 loop {
378 let msg = tokio::select! {
379 result = tokio::time::timeout(idle_timeout, Message::read_from(&mut reader)) => {
381 match result {
382 Ok(Ok(Some(msg))) => msg,
383 Ok(Ok(None)) => break,
384 Ok(Err(e)) => {
385 error!(peer = %peer, error = %e, "read error");
386 break;
387 }
388 Err(_) => {
389 info!(peer = %peer, "idle timeout, closing connection");
390 let err = Message::Error { message: "idle timeout".into() };
391 write_msg(&mut writer, &err).await;
392 break;
393 }
394 }
395 }
396 _ = shutdown_rx.changed() => {
398 if *shutdown_rx.borrow() {
399 info!(peer = %peer, "server shutting down, closing connection");
400 let err = Message::Error { message: "server shutting down".into() };
401 write_msg(&mut writer, &err).await;
402 break;
403 }
404 continue;
405 }
406 };
407
408 let response = match msg {
409 Message::Ping => {
410 debug!(peer = %peer, "ping");
411 Message::Pong
412 }
413 Message::Query { query } => {
414 if query.len() > MAX_QUERY_LENGTH {
415 Message::Error {
416 message: format!(
417 "query too large: {} bytes (max {})",
418 query.len(),
419 MAX_QUERY_LENGTH
420 ),
421 }
422 } else {
423 debug!(peer = %peer, query = %query, "received query");
424 let handle = tokio::task::spawn_blocking({
425 let engine = engine.clone();
426 let query = query.clone();
427 move || dispatch_query(&engine, &query)
428 });
429 let abort_handle = handle.abort_handle();
430 match tokio::time::timeout(query_timeout, handle).await {
431 Ok(Ok(Ok(result))) => query_result_to_message(result),
432 Ok(Ok(Err(e))) => Message::Error {
433 message: sanitize_error(&e.to_string()),
434 },
435 Ok(Err(e)) => Message::Error {
436 message: format!("internal error: {e}"),
437 },
438 Err(_) => {
439 abort_handle.abort();
440 warn!(peer = %peer, query = %query, "query timeout exceeded");
441 Message::Error {
442 message: "query timeout exceeded".into(),
443 }
444 }
445 }
446 }
447 }
448 Message::Disconnect => {
449 debug!(peer = %peer, "received DISCONNECT");
450 break;
451 }
452 _ => Message::Error {
453 message: "unexpected message type".into(),
454 },
455 };
456
457 if !write_msg(&mut writer, &response).await {
458 break;
459 }
460 }
461
462 info!(peer = %peer, "client disconnected");
463}
464
465fn query_result_to_message(result: QueryResult) -> Message {
466 match result {
467 QueryResult::Rows { columns, rows } => {
468 let str_rows: Vec<Vec<String>> = rows
469 .iter()
470 .map(|row| row.iter().map(value_to_display).collect())
471 .collect();
472 Message::ResultRows {
473 columns,
474 rows: str_rows,
475 }
476 }
477 QueryResult::Scalar(val) => Message::ResultScalar {
478 value: value_to_display(&val),
479 },
480 QueryResult::Modified(n) => Message::ResultOk { affected: n },
481 QueryResult::Created(name) => Message::ResultMessage {
482 message: format!("type {name} created"),
483 },
484 QueryResult::Executed { message } => Message::ResultMessage { message },
485 }
486}
487
488fn value_to_display(v: &Value) -> String {
489 match v {
490 Value::Int(n) => n.to_string(),
491 Value::Float(n) => format!("{n}"),
492 Value::Bool(b) => b.to_string(),
493 Value::Str(s) => s.clone(),
494 Value::DateTime(t) => format!("{t}"),
495 Value::Uuid(u) => format!("{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
496 u[0], u[1], u[2], u[3], u[4], u[5], u[6], u[7],
497 u[8], u[9], u[10], u[11], u[12], u[13], u[14], u[15]),
498 Value::Bytes(b) => format!("<{} bytes>", b.len()),
499 Value::Empty => "{}".into(),
500 }
501}
502
503#[cfg(test)]
504mod tests {
505 use super::*;
506
507 fn store_with_alice() -> UserStore {
508 let mut s = UserStore::new();
509 s.create_user("alice", "pw", "readwrite").unwrap();
510 s
511 }
512
513 #[test]
516 fn empty_store_no_password_is_open() {
517 let s = UserStore::new();
518 assert_eq!(
519 authenticate_connect(&s, None, None, None),
520 AuthOutcome::Authenticated { principal: None }
521 );
522 assert_eq!(
524 authenticate_connect(&s, None, Some("x"), Some("y")),
525 AuthOutcome::Authenticated { principal: None }
526 );
527 }
528
529 #[test]
530 fn empty_store_correct_shared_password_succeeds() {
531 let s = UserStore::new();
532 assert_eq!(
533 authenticate_connect(&s, Some("pw"), None, Some("pw")),
534 AuthOutcome::Authenticated { principal: None }
535 );
536 }
537
538 #[test]
539 fn empty_store_wrong_shared_password_rejected() {
540 let s = UserStore::new();
541 assert_eq!(
542 authenticate_connect(&s, Some("pw"), None, Some("bad")),
543 AuthOutcome::Rejected
544 );
545 }
546
547 #[test]
548 fn empty_store_missing_password_rejected_when_expected() {
549 let s = UserStore::new();
550 assert_eq!(
551 authenticate_connect(&s, Some("pw"), None, None),
552 AuthOutcome::Rejected
553 );
554 }
555
556 #[test]
557 fn empty_store_ignores_username_for_shared_password() {
558 let s = UserStore::new();
561 assert_eq!(
562 authenticate_connect(&s, Some("pw"), Some("whoever"), Some("pw")),
563 AuthOutcome::Authenticated { principal: None }
564 );
565 }
566
567 #[test]
570 fn user_auth_success_binds_principal() {
571 let s = store_with_alice();
572 assert_eq!(
573 authenticate_connect(&s, None, Some("alice"), Some("pw")),
574 AuthOutcome::Authenticated {
575 principal: Some(Principal {
576 name: "alice".into(),
577 role: "readwrite".into(),
578 })
579 }
580 );
581 }
582
583 #[test]
584 fn user_auth_wrong_password_rejected() {
585 let s = store_with_alice();
586 assert_eq!(
587 authenticate_connect(&s, None, Some("alice"), Some("bad")),
588 AuthOutcome::Rejected
589 );
590 }
591
592 #[test]
593 fn user_auth_unknown_user_rejected() {
594 let s = store_with_alice();
595 assert_eq!(
596 authenticate_connect(&s, None, Some("mallory"), Some("pw")),
597 AuthOutcome::Rejected
598 );
599 }
600
601 #[test]
602 fn user_auth_missing_username_rejected() {
603 let s = store_with_alice();
604 assert_eq!(
605 authenticate_connect(&s, None, None, Some("pw")),
606 AuthOutcome::Rejected
607 );
608 }
609
610 #[test]
611 fn user_auth_missing_password_rejected() {
612 let s = store_with_alice();
613 assert_eq!(
614 authenticate_connect(&s, Some("pw"), Some("alice"), None),
615 AuthOutcome::Rejected
616 );
617 }
618
619 #[test]
620 fn user_auth_ignores_shared_password_when_users_present() {
621 let s = store_with_alice();
624 assert_eq!(
625 authenticate_connect(&s, Some("shared"), None, Some("shared")),
626 AuthOutcome::Rejected
627 );
628 }
629}