Skip to main content

powdb_server/
handler.rs

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
16/// Tracks per-IP authentication failure counts for rate limiting.
17pub type AuthRateLimiter = Arc<Mutex<HashMap<IpAddr, (u32, Instant)>>>;
18
19/// Maximum query text length accepted from the wire (1 MB).
20const MAX_QUERY_LENGTH: usize = 1024 * 1024;
21
22/// Timeout for writing a response to the client. Prevents slow-drain
23/// clients from blocking the handler indefinitely.
24const WRITE_TIMEOUT: Duration = Duration::from_secs(30);
25
26/// Maximum number of auth failures per IP within the rate-limit window.
27const MAX_AUTH_FAILURES: u32 = 5;
28
29/// Window during which auth failures are counted (60 seconds).
30const AUTH_FAILURE_WINDOW: Duration = Duration::from_secs(60);
31
32/// Create a new shared rate limiter.
33pub fn new_rate_limiter() -> AuthRateLimiter {
34    Arc::new(Mutex::new(HashMap::new()))
35}
36
37/// Check whether an IP is rate-limited and record a failure if requested.
38/// Returns `true` if the IP should be rejected.
39fn is_rate_limited(limiter: &AuthRateLimiter, ip: IpAddr) -> bool {
40    let mut map = limiter.lock().unwrap_or_else(|e| e.into_inner());
41    // Clean up stale entries while we have the lock.
42    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
52/// Record an auth failure for the given IP.
53fn 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    // Reset counter if the window has elapsed.
58    if now.duration_since(entry.1) >= AUTH_FAILURE_WINDOW {
59        *entry = (1, now);
60    } else {
61        entry.0 += 1;
62    }
63}
64
65/// Clear the failure counter on successful auth.
66fn 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
71/// Constant-time password comparison. Hashes both inputs to fixed-size
72/// SHA-256 digests so neither length nor content leaks through timing.
73fn 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/// An authenticated connection's identity, carried alongside the session for
85/// later per-operation RBAC enforcement (Slice 3). For now it is bound at
86/// connect time and logged; it is not yet consulted per query.
87#[derive(Debug, Clone, PartialEq, Eq)]
88pub struct Principal {
89    pub name: String,
90    pub role: String,
91}
92
93/// Result of the connect-time authentication decision.
94#[derive(Debug, Clone, PartialEq, Eq)]
95pub enum AuthOutcome {
96    /// Authenticated. `principal` is `Some` when a named user authenticated via
97    /// the UserStore, and `None` for the legacy shared-password / open paths
98    /// where there is no per-user identity.
99    Authenticated { principal: Option<Principal> },
100    /// Rejected. The caller sends a generic "authentication failed" error and
101    /// records a rate-limit failure — it must not reveal which check failed.
102    Rejected,
103}
104
105/// Pure, exhaustively-testable authentication decision for a CONNECT handshake.
106///
107/// Policy:
108/// - If `users` has at least one user, multi-user auth is in force: a
109///   `username` is required and `users.authenticate(username, password)` must
110///   succeed. Unknown user, wrong password, or a missing username all reject
111///   with an indistinguishable `Rejected` (no user-vs-password leak).
112/// - If `users` is empty, fall back verbatim to the legacy behavior: when
113///   `expected_password` is `Some`, the candidate must match it (constant time);
114///   when `None`, no auth is required (open). The `username` is ignored here so
115///   that a new client talking to a shared-password server still connects.
116pub 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        // Multi-user mode: a username is mandatory.
124        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        // Legacy shared-password fallback (byte-identical to prior behavior).
141        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
154/// Error messages that are safe to forward to the client verbatim.
155const 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
174/// Sanitize an error message before sending it to the client.
175/// Known safe errors are passed through; everything else is replaced
176/// with a generic message to avoid leaking internal details.
177fn 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
187/// Write a message to the client with a timeout. Returns false if the
188/// write failed or timed out (caller should close the connection).
189async 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
201/// Options for a single connection, bundled to keep `handle_connection`'s
202/// argument list short.
203pub struct ConnOpts<'a> {
204    pub engine: Arc<RwLock<Engine>>,
205    /// Expected client password. Wrapped in `Zeroizing` so the secret is wiped
206    /// from memory on drop (defends against leaking via a core dump).
207    pub expected_password: Option<Zeroizing<String>>,
208    /// Multi-user store loaded from the data dir at startup. When it has users,
209    /// the handshake authenticates `(username, password)` against it; when empty
210    /// the server falls back to `expected_password`. Shared across connections.
211    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
219/// Execute a query against the engine under the RwLock. Read-only
220/// statements acquire `.read()` so concurrent SELECTs can scan in
221/// parallel; mutations acquire `.write()`.
222fn 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                // Escalate: fall through to the write path below.
237            }
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    // Wait for Connect message (with idle timeout).
273    // Accept Ping messages before authentication so load balancers can
274    // health-check without completing a full CONNECT handshake.
275    // Uses the smaller pre-auth payload limit (4 KB) to prevent memory abuse.
276    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    // The authenticated identity for this connection. Bound at connect time and
302    // carried for later per-operation RBAC enforcement (Slice 3).
303    let _principal: Option<Principal>;
304    match connect_msg {
305        Message::Connect {
306            db_name,
307            password,
308            username,
309        } => {
310            // Check rate limiting before verifying credentials.
311            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                    // Record the failure for rate limiting.
333                    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                    // Auth succeeded — clear any prior failure count.
344                    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    // Main query loop with idle timeout and shutdown awareness.
377    loop {
378        let msg = tokio::select! {
379            // Read next message with idle timeout.
380            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            // If server is shutting down, notify client and close.
397            _ = 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    // ---- Empty store: legacy shared-password fallback ----
514
515    #[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        // Even a stray username/password is accepted (legacy open behavior).
523        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        // A new client may send a username even against a shared-password
559        // server; the username is ignored and the password still governs.
560        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    // ---- Populated store: multi-user auth ----
568
569    #[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        // With users present, the shared password is irrelevant: supplying it as
622        // the password without a valid user must NOT authenticate.
623        let s = store_with_alice();
624        assert_eq!(
625            authenticate_connect(&s, Some("shared"), None, Some("shared")),
626            AuthOutcome::Rejected
627        );
628    }
629}