Skip to main content

powdb_server/
handler.rs

1use crate::protocol::{Message, WireParam};
2use powdb_auth::{Permission, Role, UserStore};
3use powdb_query::executor::{is_read_only_statement, Engine};
4use powdb_query::parser;
5use powdb_query::result::{QueryError, QueryResult};
6use powdb_query::sql;
7use powdb_storage::types::Value;
8use std::collections::HashMap;
9use std::net::IpAddr;
10use std::sync::{Arc, Mutex, RwLock};
11use std::time::{Duration, Instant};
12use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt, BufReader, BufWriter};
13use tokio::sync::{watch, OwnedSemaphorePermit, Semaphore};
14use tracing::{debug, error, info, warn};
15use zeroize::Zeroizing;
16
17/// Tracks per-IP authentication failure counts for rate limiting.
18pub type AuthRateLimiter = Arc<Mutex<HashMap<IpAddr, (u32, Instant)>>>;
19
20/// Gate that serializes wire-protocol statements while an explicit
21/// transaction is open on any connection. The connection that runs `begin`
22/// keeps an owned permit until `commit`, `rollback`, disconnect, or timeout,
23/// preventing other connections from observing or joining uncommitted state.
24pub type TxGate = Arc<Semaphore>;
25
26/// Create a transaction gate for a shared engine.
27pub fn new_tx_gate() -> TxGate {
28    Arc::new(Semaphore::new(1))
29}
30
31/// Maximum query text length accepted from the wire (1 MB).
32const MAX_QUERY_LENGTH: usize = 1024 * 1024;
33
34/// Timeout for writing a response to the client. Prevents slow-drain
35/// clients from blocking the handler indefinitely.
36const WRITE_TIMEOUT: Duration = Duration::from_secs(30);
37
38/// Maximum number of auth failures per IP within the rate-limit window.
39const MAX_AUTH_FAILURES: u32 = 5;
40
41/// Window during which auth failures are counted (60 seconds).
42const AUTH_FAILURE_WINDOW: Duration = Duration::from_secs(60);
43
44/// Create a new shared rate limiter.
45pub fn new_rate_limiter() -> AuthRateLimiter {
46    Arc::new(Mutex::new(HashMap::new()))
47}
48
49/// Check whether an IP is rate-limited and record a failure if requested.
50/// Returns `true` if the IP should be rejected.
51fn is_rate_limited(limiter: &AuthRateLimiter, ip: IpAddr) -> bool {
52    let mut map = limiter.lock().unwrap_or_else(|e| e.into_inner());
53    // Clean up stale entries while we have the lock.
54    let now = Instant::now();
55    map.retain(|_, (_, ts)| now.duration_since(*ts) < AUTH_FAILURE_WINDOW);
56
57    if let Some((count, _)) = map.get(&ip) {
58        *count >= MAX_AUTH_FAILURES
59    } else {
60        false
61    }
62}
63
64/// Record an auth failure for the given IP.
65fn record_auth_failure(limiter: &AuthRateLimiter, ip: IpAddr) {
66    let mut map = limiter.lock().unwrap_or_else(|e| e.into_inner());
67    let now = Instant::now();
68    let entry = map.entry(ip).or_insert((0, now));
69    // Reset counter if the window has elapsed.
70    if now.duration_since(entry.1) >= AUTH_FAILURE_WINDOW {
71        *entry = (1, now);
72    } else {
73        entry.0 += 1;
74    }
75}
76
77/// Clear the failure counter on successful auth.
78fn clear_auth_failures(limiter: &AuthRateLimiter, ip: IpAddr) {
79    let mut map = limiter.lock().unwrap_or_else(|e| e.into_inner());
80    map.remove(&ip);
81}
82
83/// Constant-time password comparison. Hashes both inputs to fixed-size
84/// SHA-256 digests so neither length nor content leaks through timing.
85fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
86    use sha2::{Digest, Sha256};
87    let ha = Sha256::digest(a);
88    let hb = Sha256::digest(b);
89    let mut diff = 0u8;
90    for (x, y) in ha.iter().zip(hb.iter()) {
91        diff |= x ^ y;
92    }
93    diff == 0
94}
95
96/// An authenticated connection's identity. Bound at connect time and consulted
97/// on every query by [`dispatch_query`] to enforce the user's role: a
98/// `readonly` principal may only execute read statements.
99#[derive(Debug, Clone, PartialEq, Eq)]
100pub struct Principal {
101    pub name: String,
102    pub role: String,
103}
104
105/// Whether a parsed statement is data-definition (schema) work: creating,
106/// altering, or dropping a type or view. `explain <ddl>` is classified by its
107/// inner statement so `explain drop User` needs the same permission as
108/// `drop User`. Mutations that change *rows* (insert/update/delete/upsert/
109/// refresh) and transaction control are NOT DDL — they fall under `Write`.
110fn is_ddl_statement(stmt: &powdb_query::ast::Statement) -> bool {
111    use powdb_query::ast::Statement;
112    let inner = match stmt {
113        Statement::Explain(inner) => inner.as_ref(),
114        other => other,
115    };
116    matches!(
117        inner,
118        Statement::CreateType(_)
119            | Statement::AlterTable(_)
120            | Statement::DropTable(_)
121            | Statement::CreateView(_)
122            | Statement::DropView(_)
123    )
124}
125
126/// The capability a parsed statement requires under the RBAC lattice
127/// (`crates/auth/src/role.rs`). Reads need [`Permission::Read`]; schema
128/// definition needs [`Permission::Ddl`]; every other mutation needs
129/// [`Permission::Write`]. [`Permission::Admin`] is reserved for user/role
130/// management, which is CLI-only today and never reaches this wire path.
131fn required_permission(stmt: &powdb_query::ast::Statement) -> Permission {
132    if is_read_only_statement(stmt) {
133        Permission::Read
134    } else if is_ddl_statement(stmt) {
135        Permission::Ddl
136    } else {
137        Permission::Write
138    }
139}
140
141/// Enforce the principal's role against a parsed statement using the full
142/// permission lattice. Reads are always permitted (any authenticated role can
143/// read — unknown role names still read but fail closed on any mutation).
144/// Mutations require the specific capability the statement maps to: row
145/// mutations need `Write`, schema changes need `Ddl`. Unknown role names
146/// resolve to no builtin and therefore grant nothing beyond reads.
147///
148/// Classification uses the parsed AST via
149/// [`powdb_query::executor::is_read_only_statement`] — the exact same
150/// classifier the RwLock read/write split relies on — so the permission
151/// boundary and the concurrency boundary can never disagree.
152fn check_statement_permitted(
153    principal: Option<&Principal>,
154    stmt: &powdb_query::ast::Statement,
155) -> Result<(), QueryError> {
156    let Some(p) = principal else {
157        // No per-user identity (shared-password or open mode): full access,
158        // byte-identical to the pre-RBAC behavior.
159        return Ok(());
160    };
161    // Reads are permitted for every authenticated principal (preserves the
162    // pre-lattice contract that any connected role may run read-only queries).
163    if is_read_only_statement(stmt) {
164        return Ok(());
165    }
166    let needed = required_permission(stmt);
167    if Role::builtin(&p.role).is_some_and(|r| r.allows(needed)) {
168        return Ok(());
169    }
170    let kind = if needed == Permission::Ddl {
171        "schema-definition"
172    } else {
173        "write"
174    };
175    Err(QueryError::Execution(format!(
176        "permission denied: role '{}' cannot execute {kind} statements",
177        p.role
178    )))
179}
180
181/// Result of the connect-time authentication decision.
182#[derive(Debug, Clone, PartialEq, Eq)]
183pub enum AuthOutcome {
184    /// Authenticated. `principal` is `Some` when a named user authenticated via
185    /// the UserStore, and `None` for the legacy shared-password / open paths
186    /// where there is no per-user identity.
187    Authenticated { principal: Option<Principal> },
188    /// Rejected. The caller sends a generic "authentication failed" error and
189    /// records a rate-limit failure — it must not reveal which check failed.
190    Rejected,
191}
192
193/// Pure, exhaustively-testable authentication decision for a CONNECT handshake.
194///
195/// Policy:
196/// - If `users` has at least one user, multi-user auth is in force: a
197///   `username` is required and `users.authenticate(username, password)` must
198///   succeed. Unknown user, wrong password, or a missing username all reject
199///   with an indistinguishable `Rejected` (no user-vs-password leak).
200/// - If `users` is empty, fall back verbatim to the legacy behavior: when
201///   `expected_password` is `Some`, the candidate must match it (constant time);
202///   when `None`, no auth is required (open). The `username` is ignored here so
203///   that a new client talking to a shared-password server still connects.
204pub fn authenticate_connect(
205    users: &UserStore,
206    expected_password: Option<&str>,
207    username: Option<&str>,
208    password: Option<&str>,
209) -> AuthOutcome {
210    if !users.is_empty() {
211        // Multi-user mode: a username is mandatory.
212        let Some(name) = username else {
213            return AuthOutcome::Rejected;
214        };
215        let Some(candidate) = password else {
216            return AuthOutcome::Rejected;
217        };
218        match users.authenticate(name, candidate) {
219            Some(user) => AuthOutcome::Authenticated {
220                principal: Some(Principal {
221                    name: user.name.clone(),
222                    role: user.role.clone(),
223                }),
224            },
225            None => AuthOutcome::Rejected,
226        }
227    } else {
228        // Legacy shared-password fallback (byte-identical to prior behavior).
229        match expected_password {
230            Some(expected) => {
231                if password.is_some_and(|p| constant_time_eq(p.as_bytes(), expected.as_bytes())) {
232                    AuthOutcome::Authenticated { principal: None }
233                } else {
234                    AuthOutcome::Rejected
235                }
236            }
237            None => AuthOutcome::Authenticated { principal: None },
238        }
239    }
240}
241
242/// Error messages that are safe to forward to the client verbatim.
243const SAFE_ERROR_PREFIXES: &[&str] = &[
244    "table not found",
245    "column not found",
246    "parse error",
247    "type mismatch",
248    "unknown table",
249    "unknown column",
250    "unknown function",
251    "syntax error",
252    "expected",
253    "unexpected",
254    "missing",
255    "duplicate",
256    "invalid",
257    "cannot",
258    "no such",
259    "already exists",
260    "permission denied",
261    "row too large",
262    "unique constraint violation",
263    // Resource-limit errors carry actionable guidance (e.g. "add a LIMIT
264    // clause") and leak no internal state, so surface them verbatim instead
265    // of masking them to the generic message. See QueryError::{SortLimit,
266    // JoinLimit,MemoryLimit}Exceeded in crates/query/src/result.rs.
267    "sort input exceeds",
268    "join result exceeds",
269    "query exceeded memory budget",
270];
271
272/// Sanitize an error message before sending it to the client.
273/// Known safe errors are passed through; everything else is replaced
274/// with a generic message to avoid leaking internal details.
275fn sanitize_error(e: &str) -> String {
276    let lower = e.to_lowercase();
277    for prefix in SAFE_ERROR_PREFIXES {
278        if lower.starts_with(prefix) {
279            return e.to_string();
280        }
281    }
282    "query execution error".into()
283}
284
285/// Write a message to the client with a timeout. Returns false if the
286/// write failed or timed out (caller should close the connection).
287async fn write_msg<W: AsyncWrite + Unpin>(writer: &mut BufWriter<W>, msg: &Message) -> bool {
288    let write_fut = async {
289        if msg.write_to(writer).await.is_err() {
290            return false;
291        }
292        writer.flush().await.is_ok()
293    };
294    tokio::time::timeout(WRITE_TIMEOUT, write_fut)
295        .await
296        .unwrap_or_default()
297}
298
299/// Options for a single connection, bundled to keep `handle_connection`'s
300/// argument list short.
301pub struct ConnOpts<'a> {
302    pub engine: Arc<RwLock<Engine>>,
303    pub tx_gate: TxGate,
304    /// Expected client password. Wrapped in `Zeroizing` so the secret is wiped
305    /// from memory on drop (defends against leaking via a core dump).
306    pub expected_password: Option<Zeroizing<String>>,
307    /// Multi-user store loaded from the data dir at startup. When it has users,
308    /// the handshake authenticates `(username, password)` against it; when empty
309    /// the server falls back to `expected_password`. Shared across connections.
310    pub users: Arc<UserStore>,
311    pub shutdown_rx: &'a mut watch::Receiver<bool>,
312    pub idle_timeout: Duration,
313    pub query_timeout: Duration,
314    pub rate_limiter: Option<&'a AuthRateLimiter>,
315    pub peer_addr: Option<std::net::SocketAddr>,
316}
317
318/// Execute a query against the engine under the RwLock. Read-only
319/// statements acquire `.read()` so concurrent SELECTs can scan in
320/// parallel; mutations acquire `.write()`.
321///
322/// When `principal` is `Some`, the user's role is enforced first: a role
323/// without the `Write` permission (i.e. `readonly`) gets a clean
324/// "permission denied" error for any non-read statement, before any lock
325/// is taken or any engine state is touched.
326fn dispatch_query(
327    engine: &Arc<RwLock<Engine>>,
328    query: &str,
329    principal: Option<&Principal>,
330) -> Result<QueryResult, QueryError> {
331    let stmt_result = parser::parse(query).map_err(|e| e.to_string());
332
333    // Role enforcement happens on the parsed AST. Statements that fail to
334    // parse fall through — the engine returns the parse error itself and
335    // can never execute anything for them.
336    if let Ok(stmt) = &stmt_result {
337        check_statement_permitted(principal, stmt)?;
338    }
339
340    let can_try_read = matches!(&stmt_result, Ok(s) if is_read_only_statement(s));
341    if can_try_read {
342        let res = {
343            let eng = engine
344                .read()
345                .map_err(|e| QueryError::Execution(format!("lock poisoned: {e}")))?;
346            eng.execute_powql_readonly(query)
347        };
348        match res {
349            Ok(r) => return Ok(r),
350            Err(QueryError::ReadonlyNeedsWrite) => {
351                // Escalate: fall through to the write path below.
352            }
353            Err(e) => return Err(e),
354        }
355    }
356
357    let mut eng = engine
358        .write()
359        .map_err(|e| QueryError::Execution(format!("lock poisoned: {e}")))?;
360    eng.execute_powql(query)
361}
362
363fn dispatch_sql_query(
364    engine: &Arc<RwLock<Engine>>,
365    query: &str,
366    principal: Option<&Principal>,
367) -> Result<QueryResult, QueryError> {
368    let stmt_result = sql::parse_sql(query).map_err(|e| e.to_string());
369
370    if let Ok(stmt) = &stmt_result {
371        check_statement_permitted(principal, stmt)?;
372    }
373
374    let can_try_read = matches!(&stmt_result, Ok(s) if is_read_only_statement(s));
375    if can_try_read {
376        let res = {
377            let eng = engine
378                .read()
379                .map_err(|e| QueryError::Execution(format!("lock poisoned: {e}")))?;
380            eng.execute_sql_readonly(query)
381        };
382        match res {
383            Ok(r) => return Ok(r),
384            Err(QueryError::ReadonlyNeedsWrite) => {}
385            Err(e) => return Err(e),
386        }
387    }
388
389    let mut eng = engine
390        .write()
391        .map_err(|e| QueryError::Execution(format!("lock poisoned: {e}")))?;
392    eng.execute_sql(query)
393}
394
395/// Convert a wire parameter into the query-crate [`ParamValue`] used for
396/// token-level binding.
397fn wire_param_to_value(p: &WireParam) -> powdb_query::ast::ParamValue {
398    use powdb_query::ast::ParamValue;
399    match p {
400        WireParam::Null => ParamValue::Null,
401        WireParam::Int(v) => ParamValue::Int(*v),
402        WireParam::Float(v) => ParamValue::Float(*v),
403        WireParam::Bool(v) => ParamValue::Bool(*v),
404        WireParam::Str(s) => ParamValue::Str(s.clone()),
405    }
406}
407
408/// Parameterized counterpart of [`dispatch_query`]. Routes through the exact
409/// same role-enforcement and read/write escalation logic, but binds the
410/// `$N` placeholders at the token level via the query crate's
411/// `parse_with_params` path. A string parameter can never change the query's
412/// shape — it is substituted as a literal token, not interpolated text.
413#[derive(Debug, Clone, Copy, PartialEq, Eq)]
414enum TransactionControl {
415    Begin,
416    Commit,
417    Rollback,
418}
419
420fn transaction_control(stmt: &powdb_query::ast::Statement) -> Option<TransactionControl> {
421    use powdb_query::ast::Statement;
422    match stmt {
423        Statement::Begin => Some(TransactionControl::Begin),
424        Statement::Commit => Some(TransactionControl::Commit),
425        Statement::Rollback => Some(TransactionControl::Rollback),
426        _ => None,
427    }
428}
429
430fn classify_query_transaction_control(query: &str) -> Option<TransactionControl> {
431    parser::parse(query)
432        .ok()
433        .and_then(|stmt| transaction_control(&stmt))
434}
435
436fn classify_sql_transaction_control(query: &str) -> Option<TransactionControl> {
437    sql::parse_sql(query)
438        .ok()
439        .and_then(|stmt| transaction_control(&stmt))
440}
441
442fn classify_params_transaction_control(
443    query: &str,
444    params: &[WireParam],
445) -> Option<TransactionControl> {
446    let bound: Vec<powdb_query::ast::ParamValue> = params.iter().map(wire_param_to_value).collect();
447    parser::parse_with_params(query, &bound)
448        .ok()
449        .and_then(|stmt| transaction_control(&stmt))
450}
451
452fn dispatch_query_with_params(
453    engine: &Arc<RwLock<Engine>>,
454    query: &str,
455    params: &[WireParam],
456    principal: Option<&Principal>,
457) -> Result<QueryResult, QueryError> {
458    let bound: Vec<powdb_query::ast::ParamValue> = params.iter().map(wire_param_to_value).collect();
459
460    // Parse once (with params bound) so role enforcement and read/write
461    // classification see exactly the statement that will execute.
462    let stmt_result = parser::parse_with_params(query, &bound).map_err(|e| e.to_string());
463
464    if let Ok(stmt) = &stmt_result {
465        check_statement_permitted(principal, stmt)?;
466    }
467
468    let can_try_read = matches!(&stmt_result, Ok(s) if is_read_only_statement(s));
469    if can_try_read {
470        let res = {
471            let eng = engine
472                .read()
473                .map_err(|e| QueryError::Execution(format!("lock poisoned: {e}")))?;
474            eng.execute_powql_readonly_with_params(query, &bound)
475        };
476        match res {
477            Ok(r) => return Ok(r),
478            Err(QueryError::ReadonlyNeedsWrite) => {
479                // Escalate to the write path below.
480            }
481            Err(e) => return Err(e),
482        }
483    }
484
485    let mut eng = engine
486        .write()
487        .map_err(|e| QueryError::Execution(format!("lock poisoned: {e}")))?;
488    eng.execute_powql_with_params(query, &bound)
489}
490
491async fn execute_wire_query(
492    engine: Arc<RwLock<Engine>>,
493    tx_gate: TxGate,
494    tx_permit: &mut Option<OwnedSemaphorePermit>,
495    query: String,
496    principal: Option<Principal>,
497    query_timeout: Duration,
498) -> Message {
499    match classify_query_transaction_control(&query) {
500        Some(TransactionControl::Begin) => {
501            if tx_permit.is_some() {
502                return Message::Error {
503                    message: sanitize_error("transaction already active"),
504                };
505            }
506            let permit = match tx_gate.acquire_owned().await {
507                Ok(permit) => permit,
508                Err(_) => {
509                    return Message::Error {
510                        message: "query execution error".into(),
511                    }
512                }
513            };
514            let response = run_blocking_query(
515                engine,
516                query,
517                principal,
518                query_timeout,
519                |engine, query, principal| dispatch_query(&engine, &query, principal.as_ref()),
520            )
521            .await;
522            if is_success_response(&response) {
523                *tx_permit = Some(permit);
524            }
525            response
526        }
527        Some(TransactionControl::Commit | TransactionControl::Rollback) => {
528            let response = run_blocking_query(
529                engine,
530                query,
531                principal,
532                query_timeout,
533                |engine, query, principal| dispatch_query(&engine, &query, principal.as_ref()),
534            )
535            .await;
536            if is_success_response(&response) {
537                tx_permit.take();
538            }
539            response
540        }
541        None if tx_permit.is_some() => {
542            run_blocking_query(
543                engine,
544                query,
545                principal,
546                query_timeout,
547                |engine, query, principal| dispatch_query(&engine, &query, principal.as_ref()),
548            )
549            .await
550        }
551        None => {
552            let permit = match tx_gate.acquire_owned().await {
553                Ok(permit) => permit,
554                Err(_) => {
555                    return Message::Error {
556                        message: "query execution error".into(),
557                    }
558                }
559            };
560            let response = run_blocking_query(
561                engine,
562                query,
563                principal,
564                query_timeout,
565                |engine, query, principal| dispatch_query(&engine, &query, principal.as_ref()),
566            )
567            .await;
568            drop(permit);
569            response
570        }
571    }
572}
573
574async fn execute_wire_query_sql(
575    engine: Arc<RwLock<Engine>>,
576    tx_gate: TxGate,
577    tx_permit: &mut Option<OwnedSemaphorePermit>,
578    query: String,
579    principal: Option<Principal>,
580    query_timeout: Duration,
581) -> Message {
582    match classify_sql_transaction_control(&query) {
583        Some(TransactionControl::Begin) => {
584            if tx_permit.is_some() {
585                return Message::Error {
586                    message: sanitize_error("transaction already active"),
587                };
588            }
589            let permit = match tx_gate.acquire_owned().await {
590                Ok(permit) => permit,
591                Err(_) => {
592                    return Message::Error {
593                        message: "query execution error".into(),
594                    }
595                }
596            };
597            let response = run_blocking_query(
598                engine,
599                query,
600                principal,
601                query_timeout,
602                |engine, query, principal| dispatch_sql_query(&engine, &query, principal.as_ref()),
603            )
604            .await;
605            if is_success_response(&response) {
606                *tx_permit = Some(permit);
607            }
608            response
609        }
610        Some(TransactionControl::Commit | TransactionControl::Rollback) => {
611            let response = run_blocking_query(
612                engine,
613                query,
614                principal,
615                query_timeout,
616                |engine, query, principal| dispatch_sql_query(&engine, &query, principal.as_ref()),
617            )
618            .await;
619            if is_success_response(&response) {
620                tx_permit.take();
621            }
622            response
623        }
624        None if tx_permit.is_some() => {
625            run_blocking_query(
626                engine,
627                query,
628                principal,
629                query_timeout,
630                |engine, query, principal| dispatch_sql_query(&engine, &query, principal.as_ref()),
631            )
632            .await
633        }
634        None => {
635            let permit = match tx_gate.acquire_owned().await {
636                Ok(permit) => permit,
637                Err(_) => {
638                    return Message::Error {
639                        message: "query execution error".into(),
640                    }
641                }
642            };
643            let response = run_blocking_query(
644                engine,
645                query,
646                principal,
647                query_timeout,
648                |engine, query, principal| dispatch_sql_query(&engine, &query, principal.as_ref()),
649            )
650            .await;
651            drop(permit);
652            response
653        }
654    }
655}
656
657async fn execute_wire_query_with_params(
658    engine: Arc<RwLock<Engine>>,
659    tx_gate: TxGate,
660    tx_permit: &mut Option<OwnedSemaphorePermit>,
661    query: String,
662    params: Vec<WireParam>,
663    principal: Option<Principal>,
664    query_timeout: Duration,
665) -> Message {
666    match classify_params_transaction_control(&query, &params) {
667        Some(TransactionControl::Begin) => {
668            if tx_permit.is_some() {
669                return Message::Error {
670                    message: sanitize_error("transaction already active"),
671                };
672            }
673            let permit = match tx_gate.acquire_owned().await {
674                Ok(permit) => permit,
675                Err(_) => {
676                    return Message::Error {
677                        message: "query execution error".into(),
678                    }
679                }
680            };
681            let response = run_blocking_query(
682                engine,
683                (query, params),
684                principal,
685                query_timeout,
686                |engine, (query, params), principal| {
687                    dispatch_query_with_params(&engine, &query, &params, principal.as_ref())
688                },
689            )
690            .await;
691            if is_success_response(&response) {
692                *tx_permit = Some(permit);
693            }
694            response
695        }
696        Some(TransactionControl::Commit | TransactionControl::Rollback) => {
697            let response = run_blocking_query(
698                engine,
699                (query, params),
700                principal,
701                query_timeout,
702                |engine, (query, params), principal| {
703                    dispatch_query_with_params(&engine, &query, &params, principal.as_ref())
704                },
705            )
706            .await;
707            if is_success_response(&response) {
708                tx_permit.take();
709            }
710            response
711        }
712        None if tx_permit.is_some() => {
713            run_blocking_query(
714                engine,
715                (query, params),
716                principal,
717                query_timeout,
718                |engine, (query, params), principal| {
719                    dispatch_query_with_params(&engine, &query, &params, principal.as_ref())
720                },
721            )
722            .await
723        }
724        None => {
725            let permit = match tx_gate.acquire_owned().await {
726                Ok(permit) => permit,
727                Err(_) => {
728                    return Message::Error {
729                        message: "query execution error".into(),
730                    }
731                }
732            };
733            let response = run_blocking_query(
734                engine,
735                (query, params),
736                principal,
737                query_timeout,
738                |engine, (query, params), principal| {
739                    dispatch_query_with_params(&engine, &query, &params, principal.as_ref())
740                },
741            )
742            .await;
743            drop(permit);
744            response
745        }
746    }
747}
748
749async fn run_blocking_query<T, F>(
750    engine: Arc<RwLock<Engine>>,
751    input: T,
752    principal: Option<Principal>,
753    query_timeout: Duration,
754    f: F,
755) -> Message
756where
757    T: Send + 'static,
758    F: FnOnce(Arc<RwLock<Engine>>, T, Option<Principal>) -> Result<QueryResult, QueryError>
759        + Send
760        + 'static,
761{
762    let handle = tokio::task::spawn_blocking(move || f(engine, input, principal));
763    let abort_handle = handle.abort_handle();
764    match tokio::time::timeout(query_timeout, handle).await {
765        Ok(Ok(Ok(result))) => query_result_to_message(result),
766        Ok(Ok(Err(e))) => Message::Error {
767            message: sanitize_error(&e.to_string()),
768        },
769        Ok(Err(e)) => Message::Error {
770            message: format!("internal error: {e}"),
771        },
772        Err(_) => {
773            abort_handle.abort();
774            Message::Error {
775                message: "query timeout exceeded".into(),
776            }
777        }
778    }
779}
780
781fn is_success_response(msg: &Message) -> bool {
782    matches!(
783        msg,
784        Message::ResultRows { .. }
785            | Message::ResultScalar { .. }
786            | Message::ResultOk { .. }
787            | Message::ResultMessage { .. }
788    )
789}
790
791fn rollback_open_transaction(engine: Arc<RwLock<Engine>>, principal: Option<Principal>) {
792    let _ = dispatch_query(&engine, "rollback", principal.as_ref());
793}
794
795pub async fn handle_connection<S>(stream: S, opts: ConnOpts<'_>)
796where
797    S: AsyncRead + AsyncWrite + Unpin,
798{
799    let ConnOpts {
800        engine,
801        tx_gate,
802        expected_password,
803        users,
804        shutdown_rx,
805        idle_timeout,
806        query_timeout,
807        rate_limiter,
808        peer_addr,
809    } = opts;
810
811    let peer = peer_addr
812        .map(|a| a.to_string())
813        .unwrap_or_else(|| "unknown".into());
814    let peer_ip = peer_addr.map(|a| a.ip());
815
816    let (reader, writer) = tokio::io::split(stream);
817    let mut reader = BufReader::new(reader);
818    let mut writer = BufWriter::new(writer);
819
820    // Wait for Connect message (with idle timeout).
821    // Accept Ping messages before authentication so load balancers can
822    // health-check without completing a full CONNECT handshake.
823    // Uses the smaller pre-auth payload limit (4 KB) to prevent memory abuse.
824    let connect_msg = loop {
825        match tokio::time::timeout(idle_timeout, Message::read_from_preauth(&mut reader)).await {
826            Ok(Ok(Some(Message::Ping))) => {
827                debug!(peer = %peer, "pre-auth ping");
828                if !write_msg(&mut writer, &Message::Pong).await {
829                    return;
830                }
831                continue;
832            }
833            Ok(Ok(Some(msg))) => break msg,
834            Ok(Ok(None)) => {
835                debug!(peer = %peer, "client closed before CONNECT");
836                return;
837            }
838            Ok(Err(e)) => {
839                error!(peer = %peer, error = %e, "error reading CONNECT");
840                return;
841            }
842            Err(_) => {
843                warn!(peer = %peer, "idle timeout waiting for CONNECT");
844                return;
845            }
846        }
847    };
848
849    // The authenticated identity for this connection. Bound at connect time
850    // and enforced on every query by `dispatch_query`.
851    let principal: Option<Principal>;
852    match connect_msg {
853        Message::Connect {
854            db_name,
855            password,
856            username,
857        } => {
858            // Check rate limiting before verifying credentials.
859            if let (Some(limiter), Some(ip)) = (rate_limiter, peer_ip) {
860                if is_rate_limited(limiter, ip) {
861                    warn!(peer = %peer, "rate limited: too many auth failures");
862                    let err = Message::Error {
863                        message: "too many auth failures, try again later".into(),
864                    };
865                    write_msg(&mut writer, &err).await;
866                    return;
867                }
868            }
869
870            let outcome = authenticate_connect(
871                &users,
872                expected_password.as_ref().map(|p| p.as_str()),
873                username.as_deref(),
874                password.as_ref().map(|p| p.as_str()),
875            );
876
877            match outcome {
878                AuthOutcome::Rejected => {
879                    warn!(peer = %peer, db = %db_name, "auth rejected");
880                    // Record the failure for rate limiting.
881                    if let (Some(limiter), Some(ip)) = (rate_limiter, peer_ip) {
882                        record_auth_failure(limiter, ip);
883                    }
884                    let err = Message::Error {
885                        message: "authentication failed".into(),
886                    };
887                    write_msg(&mut writer, &err).await;
888                    return;
889                }
890                AuthOutcome::Authenticated {
891                    principal: auth_principal,
892                } => {
893                    // Auth succeeded — clear any prior failure count.
894                    if let (Some(limiter), Some(ip)) = (rate_limiter, peer_ip) {
895                        clear_auth_failures(limiter, ip);
896                    }
897                    match &auth_principal {
898                        Some(p) => {
899                            info!(peer = %peer, db = %db_name, user = %p.name, role = %p.role, "authenticated");
900                        }
901                        None => {
902                            info!(peer = %peer, db = %db_name, "client connected");
903                        }
904                    }
905                    principal = auth_principal;
906                }
907            }
908
909            let ok = Message::ConnectOk {
910                version: env!("CARGO_PKG_VERSION").into(),
911            };
912            if !write_msg(&mut writer, &ok).await {
913                return;
914            }
915        }
916        _ => {
917            warn!(peer = %peer, "first message was not CONNECT");
918            let err = Message::Error {
919                message: "expected CONNECT".into(),
920            };
921            write_msg(&mut writer, &err).await;
922            return;
923        }
924    }
925
926    let mut tx_permit: Option<OwnedSemaphorePermit> = None;
927
928    // Main query loop with idle timeout and shutdown awareness.
929    loop {
930        let msg = tokio::select! {
931            // Read next message with idle timeout.
932            result = tokio::time::timeout(idle_timeout, Message::read_from(&mut reader)) => {
933                match result {
934                    Ok(Ok(Some(msg))) => msg,
935                    Ok(Ok(None)) => break,
936                    Ok(Err(e)) => {
937                        error!(peer = %peer, error = %e, "read error");
938                        break;
939                    }
940                    Err(_) => {
941                        info!(peer = %peer, "idle timeout, closing connection");
942                        let err = Message::Error { message: "idle timeout".into() };
943                        write_msg(&mut writer, &err).await;
944                        break;
945                    }
946                }
947            }
948            // If server is shutting down, notify client and close.
949            _ = shutdown_rx.changed() => {
950                if *shutdown_rx.borrow() {
951                    info!(peer = %peer, "server shutting down, closing connection");
952                    let err = Message::Error { message: "server shutting down".into() };
953                    write_msg(&mut writer, &err).await;
954                    break;
955                }
956                continue;
957            }
958        };
959
960        let response = match msg {
961            Message::Ping => {
962                debug!(peer = %peer, "ping");
963                Message::Pong
964            }
965            Message::Query { query } => {
966                if query.len() > MAX_QUERY_LENGTH {
967                    Message::Error {
968                        message: format!(
969                            "query too large: {} bytes (max {})",
970                            query.len(),
971                            MAX_QUERY_LENGTH
972                        ),
973                    }
974                } else {
975                    debug!(peer = %peer, query = %query, "received query");
976                    let response = execute_wire_query(
977                        engine.clone(),
978                        tx_gate.clone(),
979                        &mut tx_permit,
980                        query.clone(),
981                        principal.clone(),
982                        query_timeout,
983                    )
984                    .await;
985                    if matches!(&response, Message::Error { message } if message == "query timeout exceeded")
986                    {
987                        warn!(peer = %peer, query = %query, "query timeout exceeded");
988                        if tx_permit.is_some() {
989                            let engine = engine.clone();
990                            let principal = principal.clone();
991                            let _ = tokio::task::spawn_blocking(move || {
992                                rollback_open_transaction(engine, principal)
993                            })
994                            .await;
995                        }
996                        tx_permit.take();
997                    }
998                    response
999                }
1000            }
1001            Message::QuerySql { query } => {
1002                if query.len() > MAX_QUERY_LENGTH {
1003                    Message::Error {
1004                        message: format!(
1005                            "query too large: {} bytes (max {})",
1006                            query.len(),
1007                            MAX_QUERY_LENGTH
1008                        ),
1009                    }
1010                } else {
1011                    debug!(peer = %peer, query = %query, "received SQL query");
1012                    let response = execute_wire_query_sql(
1013                        engine.clone(),
1014                        tx_gate.clone(),
1015                        &mut tx_permit,
1016                        query.clone(),
1017                        principal.clone(),
1018                        query_timeout,
1019                    )
1020                    .await;
1021                    if matches!(&response, Message::Error { message } if message == "query timeout exceeded")
1022                    {
1023                        warn!(peer = %peer, query = %query, "SQL query timeout exceeded");
1024                        if tx_permit.is_some() {
1025                            let engine = engine.clone();
1026                            let principal = principal.clone();
1027                            let _ = tokio::task::spawn_blocking(move || {
1028                                rollback_open_transaction(engine, principal)
1029                            })
1030                            .await;
1031                        }
1032                        tx_permit.take();
1033                    }
1034                    response
1035                }
1036            }
1037            Message::QueryWithParams { query, params } => {
1038                if query.len() > MAX_QUERY_LENGTH {
1039                    Message::Error {
1040                        message: format!(
1041                            "query too large: {} bytes (max {})",
1042                            query.len(),
1043                            MAX_QUERY_LENGTH
1044                        ),
1045                    }
1046                } else {
1047                    debug!(peer = %peer, query = %query, n_params = params.len(), "received parameterized query");
1048                    let response = execute_wire_query_with_params(
1049                        engine.clone(),
1050                        tx_gate.clone(),
1051                        &mut tx_permit,
1052                        query.clone(),
1053                        params.clone(),
1054                        principal.clone(),
1055                        query_timeout,
1056                    )
1057                    .await;
1058                    if matches!(&response, Message::Error { message } if message == "query timeout exceeded")
1059                    {
1060                        warn!(peer = %peer, query = %query, "query timeout exceeded");
1061                        if tx_permit.is_some() {
1062                            let engine = engine.clone();
1063                            let principal = principal.clone();
1064                            let _ = tokio::task::spawn_blocking(move || {
1065                                rollback_open_transaction(engine, principal)
1066                            })
1067                            .await;
1068                        }
1069                        tx_permit.take();
1070                    }
1071                    response
1072                }
1073            }
1074            Message::Disconnect => {
1075                debug!(peer = %peer, "received DISCONNECT");
1076                break;
1077            }
1078            _ => Message::Error {
1079                message: "unexpected message type".into(),
1080            },
1081        };
1082
1083        if !write_msg(&mut writer, &response).await {
1084            break;
1085        }
1086    }
1087
1088    // Roll back any open transaction the client left behind on disconnect.
1089    // The permit must stay alive in `tx_permit` for the duration of the awaited
1090    // rollback and be released only afterwards — mirroring the query-timeout
1091    // path above. Using `tx_permit.take().is_some()` here would drop the permit
1092    // (freeing the TxGate) *before* the rollback runs, letting another
1093    // connection BEGIN a transaction that this stale rollback would then clobber.
1094    if tx_permit.is_some() {
1095        let engine = engine.clone();
1096        let principal = principal.clone();
1097        let _ =
1098            tokio::task::spawn_blocking(move || rollback_open_transaction(engine, principal)).await;
1099    }
1100    tx_permit.take();
1101
1102    info!(peer = %peer, "client disconnected");
1103}
1104
1105fn query_result_to_message(result: QueryResult) -> Message {
1106    match result {
1107        QueryResult::Rows { columns, rows } => {
1108            let str_rows: Vec<Vec<String>> = rows
1109                .iter()
1110                .map(|row| row.iter().map(value_to_display).collect())
1111                .collect();
1112            Message::ResultRows {
1113                columns,
1114                rows: str_rows,
1115            }
1116        }
1117        QueryResult::Scalar(val) => Message::ResultScalar {
1118            value: value_to_display(&val),
1119        },
1120        QueryResult::Modified(n) => Message::ResultOk { affected: n },
1121        QueryResult::Created(name) => Message::ResultMessage {
1122            message: format!("type {name} created"),
1123        },
1124        QueryResult::Executed { message } => Message::ResultMessage { message },
1125    }
1126}
1127
1128fn value_to_display(v: &Value) -> String {
1129    match v {
1130        Value::Int(n)      => n.to_string(),
1131        Value::Float(n)    => format!("{n}"),
1132        Value::Bool(b)     => b.to_string(),
1133        Value::Str(s)      => s.clone(),
1134        Value::DateTime(t) => format!("{t}"),
1135        Value::Uuid(u)     => format!("{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
1136            u[0], u[1], u[2], u[3], u[4], u[5], u[6], u[7],
1137            u[8], u[9], u[10], u[11], u[12], u[13], u[14], u[15]),
1138        Value::Bytes(b)    => format!("<{} bytes>", b.len()),
1139        // NULL is serialized as the bareword "null" on the wire. This is the
1140        // sentinel the TypeScript client's typed-row decoder already
1141        // documents and matches (`coerceValue` treats the exact token
1142        // "null" as NULL for non-str columns); the previous "{}" rendering
1143        // was a bug that neither the TS client nor the CLI recognized.
1144        Value::Empty       => "null".into(),
1145    }
1146}
1147
1148#[cfg(test)]
1149mod tests {
1150    use super::*;
1151
1152    // ---- Wire NULL rendering (Fix: remote protocol rendered NULL as `{}`) ----
1153
1154    #[test]
1155    fn null_serializes_as_null_bareword_on_wire() {
1156        assert_eq!(value_to_display(&Value::Empty), "null");
1157    }
1158
1159    // ---- Error sanitization allowlist ----
1160
1161    #[test]
1162    fn unique_violation_error_surfaces_to_remote_clients() {
1163        // The storage layer reports the actionable message; the server must
1164        // not replace it with the generic "query execution error".
1165        assert_eq!(
1166            sanitize_error("unique constraint violation on User.email"),
1167            "unique constraint violation on User.email"
1168        );
1169    }
1170
1171    #[test]
1172    fn internal_errors_stay_generic() {
1173        assert_eq!(
1174            sanitize_error("some internal io panic detail"),
1175            "query execution error"
1176        );
1177    }
1178
1179    #[test]
1180    fn resource_limit_errors_surface_actionable_hints() {
1181        // These carry user-actionable guidance and leak no internal state, so
1182        // they must reach the client verbatim — not be masked to the generic
1183        // message. The exact strings come from QueryError's Display impl
1184        // (crates/query/src/result.rs).
1185        for msg in [
1186            "sort input exceeds row limit — add a LIMIT clause",
1187            "join result exceeds row limit",
1188            "query exceeded memory budget: requested 100 bytes, limit 50 bytes",
1189        ] {
1190            assert_eq!(sanitize_error(msg), msg, "should pass through verbatim");
1191        }
1192    }
1193
1194    // ---- Role enforcement (Fix: readonly role was not enforced) ----
1195
1196    fn parsed(q: &str) -> powdb_query::ast::Statement {
1197        parser::parse(q).unwrap()
1198    }
1199
1200    fn principal(role: &str) -> Option<Principal> {
1201        Some(Principal {
1202            name: "u".into(),
1203            role: role.into(),
1204        })
1205    }
1206
1207    #[test]
1208    fn readonly_can_read_but_not_write() {
1209        let p = principal("readonly");
1210        // Reads pass.
1211        assert!(check_statement_permitted(p.as_ref(), &parsed("User")).is_ok());
1212        assert!(check_statement_permitted(p.as_ref(), &parsed("count(User)")).is_ok());
1213        assert!(check_statement_permitted(p.as_ref(), &parsed("explain User")).is_ok());
1214        // Writes, DDL, and transaction control are denied.
1215        for q in [
1216            r#"insert User { name := "x" }"#,
1217            "User filter .id = 1 update { age := 2 }",
1218            "User filter .id = 1 delete",
1219            "drop User",
1220            "alter User add column c: str",
1221            "type T { required id: int }",
1222            "begin",
1223            "commit",
1224            "rollback",
1225        ] {
1226            let err = check_statement_permitted(p.as_ref(), &parsed(q))
1227                .expect_err(&format!("must deny: {q}"));
1228            assert!(
1229                err.to_string().contains("permission denied"),
1230                "unexpected error for {q}: {err}"
1231            );
1232        }
1233    }
1234
1235    #[test]
1236    fn readwrite_and_admin_have_full_query_access() {
1237        for role in ["readwrite", "admin"] {
1238            let p = principal(role);
1239            assert!(check_statement_permitted(p.as_ref(), &parsed("User")).is_ok());
1240            assert!(check_statement_permitted(
1241                p.as_ref(),
1242                &parsed(r#"insert User { name := "x" }"#)
1243            )
1244            .is_ok());
1245            assert!(check_statement_permitted(p.as_ref(), &parsed("drop User")).is_ok());
1246        }
1247    }
1248
1249    #[test]
1250    fn unknown_role_fails_closed_for_writes() {
1251        let p = principal("mystery");
1252        assert!(check_statement_permitted(p.as_ref(), &parsed("User")).is_ok());
1253        assert!(
1254            check_statement_permitted(p.as_ref(), &parsed(r#"insert User { name := "x" }"#))
1255                .is_err()
1256        );
1257    }
1258
1259    #[test]
1260    fn no_principal_means_full_access() {
1261        // Shared-password / open mode: no per-user identity, no restriction.
1262        assert!(check_statement_permitted(None, &parsed("drop User")).is_ok());
1263        assert!(check_statement_permitted(None, &parsed(r#"insert User { name := "x" }"#)).is_ok());
1264    }
1265
1266    fn store_with_alice() -> UserStore {
1267        let mut s = UserStore::new();
1268        s.create_user("alice", "pw", "readwrite").unwrap();
1269        s
1270    }
1271
1272    // ---- Empty store: legacy shared-password fallback ----
1273
1274    #[test]
1275    fn empty_store_no_password_is_open() {
1276        let s = UserStore::new();
1277        assert_eq!(
1278            authenticate_connect(&s, None, None, None),
1279            AuthOutcome::Authenticated { principal: None }
1280        );
1281        // Even a stray username/password is accepted (legacy open behavior).
1282        assert_eq!(
1283            authenticate_connect(&s, None, Some("x"), Some("y")),
1284            AuthOutcome::Authenticated { principal: None }
1285        );
1286    }
1287
1288    #[test]
1289    fn empty_store_correct_shared_password_succeeds() {
1290        let s = UserStore::new();
1291        assert_eq!(
1292            authenticate_connect(&s, Some("pw"), None, Some("pw")),
1293            AuthOutcome::Authenticated { principal: None }
1294        );
1295    }
1296
1297    #[test]
1298    fn empty_store_wrong_shared_password_rejected() {
1299        let s = UserStore::new();
1300        assert_eq!(
1301            authenticate_connect(&s, Some("pw"), None, Some("bad")),
1302            AuthOutcome::Rejected
1303        );
1304    }
1305
1306    #[test]
1307    fn empty_store_missing_password_rejected_when_expected() {
1308        let s = UserStore::new();
1309        assert_eq!(
1310            authenticate_connect(&s, Some("pw"), None, None),
1311            AuthOutcome::Rejected
1312        );
1313    }
1314
1315    #[test]
1316    fn empty_store_ignores_username_for_shared_password() {
1317        // A new client may send a username even against a shared-password
1318        // server; the username is ignored and the password still governs.
1319        let s = UserStore::new();
1320        assert_eq!(
1321            authenticate_connect(&s, Some("pw"), Some("whoever"), Some("pw")),
1322            AuthOutcome::Authenticated { principal: None }
1323        );
1324    }
1325
1326    // ---- Populated store: multi-user auth ----
1327
1328    #[test]
1329    fn user_auth_success_binds_principal() {
1330        let s = store_with_alice();
1331        assert_eq!(
1332            authenticate_connect(&s, None, Some("alice"), Some("pw")),
1333            AuthOutcome::Authenticated {
1334                principal: Some(Principal {
1335                    name: "alice".into(),
1336                    role: "readwrite".into(),
1337                })
1338            }
1339        );
1340    }
1341
1342    #[test]
1343    fn user_auth_wrong_password_rejected() {
1344        let s = store_with_alice();
1345        assert_eq!(
1346            authenticate_connect(&s, None, Some("alice"), Some("bad")),
1347            AuthOutcome::Rejected
1348        );
1349    }
1350
1351    #[test]
1352    fn user_auth_unknown_user_rejected() {
1353        let s = store_with_alice();
1354        assert_eq!(
1355            authenticate_connect(&s, None, Some("mallory"), Some("pw")),
1356            AuthOutcome::Rejected
1357        );
1358    }
1359
1360    #[test]
1361    fn user_auth_missing_username_rejected() {
1362        let s = store_with_alice();
1363        assert_eq!(
1364            authenticate_connect(&s, None, None, Some("pw")),
1365            AuthOutcome::Rejected
1366        );
1367    }
1368
1369    #[test]
1370    fn user_auth_missing_password_rejected() {
1371        let s = store_with_alice();
1372        assert_eq!(
1373            authenticate_connect(&s, Some("pw"), Some("alice"), None),
1374            AuthOutcome::Rejected
1375        );
1376    }
1377
1378    #[test]
1379    fn user_auth_ignores_shared_password_when_users_present() {
1380        // With users present, the shared password is irrelevant: supplying it as
1381        // the password without a valid user must NOT authenticate.
1382        let s = store_with_alice();
1383        assert_eq!(
1384            authenticate_connect(&s, Some("shared"), None, Some("shared")),
1385            AuthOutcome::Rejected
1386        );
1387    }
1388}