You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
proxysql/plugins/mysqlx/include/mysqlx_session.h

615 lines
30 KiB

#ifndef __MYSQLX_SESSION_H
#define __MYSQLX_SESSION_H
#include "mysqlx_data_stream.h"
#include "mysqlx_connection.h"
#include "mysqlx_config_store.h"
#include <cstdint>
#include <string>
#include <vector>
#include <functional>
#include <optional>
class Mysqlx_Thread;
// Opaque forward decl for the zstd streaming decompression context.
// Defined as ZSTD_CCtx in zstd.h, but we keep that header out of the
// public session header — the typedef is anonymous to the consumer of
// this header and only the .cpp instantiates it.
struct ZSTD_DCtx_s;
typedef struct ZSTD_DCtx_s ZSTD_DCtx;
struct ZSTD_CCtx_s;
typedef struct ZSTD_CCtx_s ZSTD_CCtx;
using MysqlxIdentityLookup =
std::function<std::optional<MysqlxResolvedIdentity>(const std::string& username)>;
// Resolved per-session backend TLS decision for a given combination of
// runtime mode (mysqlx_tls_backend_mode), per-endpoint operator override
// (mysqlx_backend_endpoints.use_ssl), and frontend TLS state
// (client_ds_.is_encrypted()). Held by handler_connecting_server() across
// the cache lookup and the fresh-connection setup so both sides see the
// same posture.
struct MysqlxBackendTlsDecision {
bool require_tls { false }; // ask the backend for TLS via CapabilitiesSet
bool fallback_allowed { false }; // on Mysqlx::Error, downgrade to plaintext
};
// Pure function: computes the per-session backend TLS decision from
// the four inputs. Lifted out of MysqlxSession so the 8 (mode x
// frontend_tls) combinations called out in issue #5693 can be unit-
// tested without driving the full session state machine. Production
// callsite: handler_connecting_server.
MysqlxBackendTlsDecision mysqlx_resolve_backend_tls_decision(
MysqlxBackendTlsMode mode,
bool endpoint_use_ssl_override,
bool frontend_is_encrypted);
// Per-message response state for the X-Protocol response sequence the
// proxy is currently waiting on. The X protocol defines distinct frame
// allow-sets and terminal markers per request type; this enum splits
// PREPARE and CURSOR into their per-request sub-shapes so each per-state
// contract can be expressed cleanly in is_frame_allowed / is_terminal_frame.
//
// Background: the previous coarse three-value model (PREPARE / CURSOR
// each lumped together) over-accepted on PREPARE_PREPARE — which the
// spec terminates with a bare Mysqlx.Ok — by also allowing the wider
// SQL_STMT_EXECUTE_OK / FETCH_DONE_* terminators that only PREPARE_EXECUTE
// can legitimately emit. Likewise CURSOR_OPEN's response always carries
// ColumnMetaData while CURSOR_FETCH's never does (the metadata is sent
// once at Open). Splitting the state lets the validation hook reject
// out-of-shape backend frames precisely.
enum MysqlxResponseState {
RESP_IDLE = 0,
RESP_WAITING_STMT_EXECUTE,
RESP_WAITING_CRUD,
RESP_WAITING_PREPARE_PREPARE, // Prepare::Prepare — terminator: Ok
RESP_WAITING_PREPARE_EXECUTE, // Prepare::Execute — terminators: Ok / SQL_STMT_EXECUTE_OK / FETCH_DONE / FETCH_SUSPENDED
RESP_WAITING_PREPARE_DEALLOCATE, // Prepare::Deallocate — terminator: Ok
RESP_WAITING_CURSOR_OPEN, // Cursor::Open — terminators: FETCH_DONE / FETCH_SUSPENDED; carries ColumnMetaData
RESP_WAITING_CURSOR_FETCH, // Cursor::Fetch — terminators: FETCH_DONE / FETCH_SUSPENDED; rows-only (metadata was at Open)
RESP_WAITING_CURSOR_CLOSE, // Cursor::Close — terminator: Ok
RESP_WAITING_EXPECT,
RESP_WAITING_SESS_RESET
};
// Per-session TLS posture. Today the runtime distinguishes:
// - TLS_OFF: this session is not configured for TLS at all.
// - TLS_TERMINATE: the proxy terminates TLS itself (the default).
// - TLS_PASSTHROUGH: after the X-Protocol CapabilitiesSet(tls=true)
// handshake, the proxy splices raw bytes between the client and
// the backend without decrypting them. Driven by the per-route
// `tls_mode='passthrough'` column on `mysqlx_routes` (issue #5692).
// The proxy stops parsing X-Protocol frames once the session
// enters this mode — backend connection is non-reusable, no per-
// query routing, no multiplexing.
//
// Historical note: an earlier prototype carried a TLS_PASSTHROUGH value
// without an actual implementation; that branch was deleted in the
// asymmetric-TLS series and is now reintroduced as a real feature.
enum MysqlxTlsMode {
TLS_OFF = 0,
TLS_TERMINATE,
TLS_PASSTHROUGH
};
// X Protocol compression algorithm negotiated via Mysqlx.Connection.Capabilities.
// NONE means no Compression message is expected on either direction. Anything
// else is a value the client supplied via CapabilitiesSet that we accepted.
// The set of values we are willing to accept is defined by send_capabilities()
// and validated in handler_capabilities_set().
enum MysqlxCompressionAlgo {
MYSQLX_COMPR_NONE = 0,
MYSQLX_COMPR_ZSTD_STREAM,
MYSQLX_COMPR_LZ4_MESSAGE
};
class MysqlxSession {
public:
enum Status {
NONE = 0,
CONNECTING_CLIENT,
X_CAPABILITIES_GET,
X_CAPABILITIES_SET,
X_AUTH_START,
X_AUTH_CHALLENGE_SENT,
X_AUTH_OK_SENT,
X_AUTH_FAILED,
WAITING_CLIENT_XMSG,
PROCESSING_X_QUERY,
CONNECTING_SERVER,
WAITING_SERVER_XMSG,
X_TLS_ACCEPT_INIT,
X_TLS_ACCEPT_CONT,
X_TLS_ACCEPT_DONE,
X_TLS_CONNECT_INIT,
X_TLS_CONNECT_CONT,
X_TLS_CONNECT_DONE,
X_SESSION_CLOSING,
X_SESSION_CLOSED,
X_SESSION_RESET_WAITING,
// Raw-byte splice between client and backend. Reached after a
// CapabilitiesSet(tls=true) on a route configured with
// tls_mode='passthrough'; once entered, the session never
// returns to X-Protocol parsing (the bytes past this point are
// the client's TLS handshake + application data, opaque to the
// proxy). The session is permanently bound to one backend
// connection (no pooling, no multiplexing).
X_PASSTHROUGH_FORWARD,
// Transient state on the way to X_PASSTHROUGH_FORWARD. Reached
// when the client sends CapabilitiesSet(tls=true) on a route
// whose tls_mode='passthrough'. The proxy is mid-flight: it
// has already started a non-blocking TCP connect to the
// backend resolved from the route's destination_hostgroup, and
// is buffering the client's CapabilitiesSet frame so that
// once connect completes the bytes can be forwarded verbatim
// to the backend. The state machine reads the backend's
// single CONN_CAPABILITIES_OK / Mysqlx::Error response (this
// is the LAST X-Protocol frame the proxy parses) and, on Ok,
// forwards it to the client and transitions to
// X_PASSTHROUGH_FORWARD. On Error, the error is propagated to
// the client and the session closes.
X_PASSTHROUGH_BACKEND_CONNECTING
};
MysqlxSession();
~MysqlxSession();
void init(int fd, Mysqlx_Thread* thread_ptr);
// Listener-aware overload: records the logical route name the
// client connected through so per-route policies (e.g.
// tls_mode='passthrough') can be looked up before any X-Protocol
// auth message has arrived. The base init(fd, thread_ptr)
// overload calls this with an empty route, preserving prior
// behaviour for callers that have no listener context (notably
// the unit-test harness, which constructs sessions directly).
void init(int fd, Mysqlx_Thread* thread_ptr, const std::string& listener_route);
void reset();
int handler();
Status get_status() const { return status_; }
void set_status(Status s) { status_ = s; }
bool is_healthy() const { return healthy; }
int get_fd() const { return client_ds_.get_fd(); }
MysqlxDataStream& client_ds() { return client_ds_; }
// Session-level accessor for the backend data stream. When a backend
// connection is attached, this proxies to MysqlxConnection::backend_ds()
// so the SSL* established during the optional backend TLS handshake is
// preserved for the rest of the session. Falls back to an uninitialized
// placeholder (fd == -1) when no backend is attached so that pollers and
// tests can safely query get_fd()/get_status() without crashing.
MysqlxDataStream& server_ds() {
return backend_conn_ ? backend_conn_->backend_ds() : server_ds_placeholder_;
}
MysqlxConnection*& backend_conn() { return backend_conn_; }
void set_identity_lookup(MysqlxIdentityLookup lookup) { identity_lookup_ = std::move(lookup); }
void set_tls_mode(MysqlxTlsMode mode) { tls_mode_ = mode; }
MysqlxTlsMode get_tls_mode() const { return tls_mode_; }
uint64_t get_start_time() const { return start_time_; }
uint64_t get_last_active_time() const { return last_active_time_; }
void set_last_active_time(uint64_t t) { last_active_time_ = t; }
// Best-effort graceful shutdown notification: send a Mysqlx::Error
// with code 1053 (server shutting down) to the client and drain any
// pending writes (and run SSL_shutdown if a TLS session is up). Used
// by Mysqlx_Thread::run() on the way out so connected clients see a
// clean X-Protocol error frame instead of an unannounced TCP RST or
// a half-finished TLS record. Safe to call regardless of session
// status_; idempotent in the sense that subsequent calls just no-op
// against an already-drained data stream.
void shutdown_notify_client();
// --- Test-only accessors ---
// These exist so unit tests can drive resolve_backend_target() in
// isolation from the full auth state machine. They are not called by
// production code. Tests that want full control over the resolved
// identity call inject_identity_for_test(MysqlxResolvedIdentity); the
// string overload is a convenience wrapper that fetches the identity
// from the thread's configured MysqlxConfigStore, mimicking what the
// auth handler does when a real client connects.
//
// inject_identity_for_test and resolve_backend_target_for_test are
// gated behind MYSQLX_TEST_BUILD because they are forgery vectors:
// inject_identity_for_test bypasses the full auth flow (no credential
// check, no cap negotiation), and resolve_backend_target_for_test
// drives a private routing helper without an authenticated identity.
// The test Makefile defines MYSQLX_TEST_BUILD; the production .so
// build does not, so these methods do not exist in shipped binaries.
// The remaining target_*_for_test getters are read-only state
// observers and are left available unconditionally (they leak no
// state a debugger could not also observe and cannot mutate the
// session).
#ifdef MYSQLX_TEST_BUILD
void inject_identity_for_test(const MysqlxResolvedIdentity& id) { identity_ = id; }
void inject_identity_for_test(const std::string& username);
int resolve_backend_target_for_test() { return resolve_backend_target(); }
// Drives the session straight into X_PASSTHROUGH_FORWARD with a
// caller-supplied backend fd, bypassing CapabilitiesSet / auth /
// resolve_backend_target. The fixture wraps the fd in a stub
// MysqlxConnection (owned by the session — destructor frees it)
// so the standard server_ds()/return_backend_to_pool plumbing is
// reusable. Must NOT exist in production (test/forgery hazard);
// gated behind MYSQLX_TEST_BUILD same as inject_identity_for_test.
void enter_passthrough_for_test(int backend_fd);
void set_listener_route_name_for_test(const std::string& route_name) {
listener_route_name_ = route_name;
}
const std::string& listener_route_name_for_test() const { return listener_route_name_; }
// Drives one pass of the splice loop without going through the
// outer handler() (which would also try to read X-Protocol frames
// from client_ds_ before dispatching). Used by the passthrough
// unit tests to assert byte forwarding in isolation.
void run_passthrough_pump_for_test() { handler_passthrough_forward(); }
// Pre-loads the c2b passthrough backlog with `n` zero bytes so
// the next splice tick is operating against a near-cap backlog
// state. The cap-overflow branch is otherwise organically
// unreachable (BURST_BYTES is smaller than the cap and the
// drain-first contract limits how many bytes a single pump can
// add). This test-only seeder makes the cap branch reachable
// without lowering the cap at runtime. NOT exposed in the
// production build.
void seed_passthrough_c2b_backlog_for_test(size_t n) {
passthrough_c2b_backlog_.assign(n, 0);
}
size_t passthrough_c2b_backlog_size_for_test() const {
return passthrough_c2b_backlog_.size();
}
// Test-only direct invocation of the cap-check append path on
// the c2b backlog. Bypasses the full splice loop; intended for
// the unit test that asserts the cap fires when an append would
// take the backlog past PASSTHROUGH_BACKLOG_CAP. Returns true if
// the append succeeded, false if the cap fired (in which case
// healthy=false and status_=X_SESSION_CLOSING are set, mirroring
// the production path).
bool try_append_to_passthrough_c2b_backlog_for_test(size_t n) {
std::vector<uint8_t> dummy(n, 0);
if (passthrough_c2b_backlog_.size() + n > PASSTHROUGH_BACKLOG_CAP) {
healthy = false;
status_ = X_SESSION_CLOSING;
return false;
}
passthrough_c2b_backlog_.insert(passthrough_c2b_backlog_.end(),
dummy.begin(), dummy.end());
return true;
}
#endif /* MYSQLX_TEST_BUILD */
int target_hostgroup_for_test() const { return target_hostgroup_; }
const std::string& target_address_for_test() const { return target_address_; }
int target_port_for_test() const { return target_port_; }
// Read-only state observers used by the stats_mysqlx_processlist
// projection (Mysqlx_Thread::snapshot_sessions_for_stats walks
// sessions_ and reads these under sessions_mutex_). Borrowed
// references — caller must copy if it needs the value past the
// snapshot scope. The identity_ accessor returns the optional by
// value because the projection wants the resolved fields, not the
// optional itself; this only happens once per session per admin
// SELECT against stats_mysqlx_processlist, so the copy cost is
// negligible.
const std::string& username_for_stats() const { return username_; }
const std::string& route_name_for_stats() const { return route_name_; }
std::optional<MysqlxResolvedIdentity> identity_for_stats() const { return identity_; }
uint64_t start_time_for_stats() const { return start_time_; }
bool to_process;
private:
void handler_connecting_client();
void handler_capabilities_get();
void handler_capabilities_set();
void handler_auth_start();
void handler_auth_challenge_response();
void handler_waiting_client_msg();
void handler_waiting_server_msg();
void handler_session_closing();
void handler_connecting_server();
void handler_session_reset_waiting();
void handler_tls_accept_init();
// Raw-byte splice loop. Pumps available bytes both directions
// (client -> backend and backend -> client) using read(2)/write(2).
// EAGAIN/EWOULDBLOCK is the normal "no more data right now" exit;
// any other read/write error or 0-byte read (peer EOF) marks the
// session unhealthy and transitions to X_SESSION_CLOSING. No
// X-Protocol parsing — once this state is reached the bytes are
// opaque (TLS handshake + application data).
void handler_passthrough_forward();
// Transient state ahead of X_PASSTHROUGH_FORWARD. Wired by
// handler_capabilities_set when a client sends
// CapabilitiesSet(tls=true) on a route with tls_mode='passthrough':
//
// 1. resolve the backend endpoint from the route's
// destination_hostgroup (no identity yet — passthrough is
// authenticated end-to-end between the client and the
// backend, the proxy never sees plaintext past this);
// 2. start a non-blocking TCP connect (or pick up an in-progress
// one);
// 3. once connected, forward the buffered CapabilitiesSet bytes
// verbatim to the backend;
// 4. read exactly one frame back from the backend; if it's an
// Ok, forward it to the client and transition the session to
// X_PASSTHROUGH_FORWARD; if it's an Error, propagate to the
// client and close.
//
// This is the last point the proxy parses an X-Protocol frame in
// a passthrough session — past step 4 the bytes are opaque
// (TLS ClientHello + application data) and handler_passthrough_
// forward() splices them blindly.
void handler_passthrough_backend_connecting();
// Pre-auth route-keyed backend endpoint resolution. Used by the
// passthrough entry path (no identity_, no default_route — the
// route is the listener_route_name_ the client connected through).
// Populates target_hostgroup_, target_address_, target_port_ from
// the route's destination_hostgroup. Returns 0 on success; nonzero
// error code on failure (already emitted an X-Protocol Error
// frame and marked the session unhealthy / closing).
int resolve_passthrough_backend_target();
// Effective per-route TLS posture for the listener this session
// was accepted on. Reads listener_route_name_ + thread's config
// store; returns MysqlxRouteTlsMode::inherit when either is
// unavailable or the route is unknown — same fail-safe semantics
// route_tls_mode() returns for unknown routes. Used by
// send_capabilities() (advertise gating) and
// handler_capabilities_set() (passthrough entry).
MysqlxRouteTlsMode effective_route_tls_mode() const;
void handle_auth_mysql41(const std::string& auth_data);
void handle_auth_plain(const std::string& auth_data);
// After identity_ is resolved, validate that the user is allowed to
// authenticate with the negotiated mechanism over the current
// transport. Sends the appropriate X-Protocol error frame and
// returns false if any per-identity policy is violated:
// - identity_->require_tls=1 and the frontend connection is
// not encrypted
// - identity_->allowed_auth_methods is set and does not contain
// auth_method_ (a comma-separated list; empty means
// "any wired method allowed", matching the historical default)
// Caller is expected to set `healthy = false` and return on a
// false return value (the helper itself does not change session
// state beyond emitting the error frame).
bool enforce_identity_policy();
void forward_frame_to_client(uint8_t msg_type, const MysqlxFrame& frame);
int dispatch_client_message(uint8_t msg_type);
void forward_to_backend();
void return_backend_to_pool();
void send_error(int code, const char* msg, bool fatal = false);
void send_ok(const char* msg = "");
void send_auth_continue(const std::string& auth_data);
void send_auth_ok();
void send_capabilities();
uint8_t extract_msg_type_from_frame(const MysqlxFrame& frame);
// Per-state validation contract for backend frames. is_frame_allowed
// returns true iff a frame of msg_type is acceptable in the current
// response_state_ (NOTICE and ERROR are universal). is_terminal_frame
// returns true iff msg_type closes the current response sequence so
// the dispatch loop can transition back to RESP_IDLE.
//
// Both are pure queries — no mutation of response_state_ or session
// state. Replaces the older is_terminal_for_state() which conflated
// "valid mid-result frame" and "terminal frame" by deferring to a
// generic terminal table for any state outside the explicit cases.
bool is_frame_allowed(uint8_t msg_type) const;
bool is_terminal_frame(uint8_t msg_type) const;
// Validate that a NOTICE frame's outer Mysqlx::Notice::Frame::type
// field is a known enum value (1..5 in the X-Protocol spec — WARNING,
// SESSION_VARIABLE_CHANGED, SESSION_STATE_CHANGED,
// GROUP_REPLICATION_STATE_CHANGED, SERVER_HELLO). Returns true iff
// the protobuf parses cleanly AND the type is in the known set.
//
// Rationale (issue #5695): the X-Protocol allows backends to emit
// NOTICE frames in essentially any state, and prior to this hook the
// proxy forwarded them uncritically. A buggy or hostile backend
// (or MITM that bypassed TLS) could ship a NOTICE with an unknown
// type field — clients written to a strict spec interpretation may
// crash or misbehave. We drop unknown types with a log line. Empty
// payload is treated as malformed (a notice with no fields is
// structurally invalid per the proto).
//
// Pure query — no session-state mutation. Caller decides the action
// (forward vs drop) based on the return value.
bool is_notice_frame_valid(const uint8_t* body, size_t body_len) const;
// Resolve identity_->default_route to concrete target_hostgroup_,
// target_address_, target_port_ via the thread's MysqlxConfigStore.
// Returns 0 on success; on failure returns a nonzero error code
// (4000 = empty default_route, 4001 = route name not in store,
// 4002 = route has no endpoints or prerequisites missing) and has
// already emitted an X-Protocol Error frame, recorded the failure
// via mysqlx_stats().record_conn_err(), and marked the session
// unhealthy. Called from the auth handlers (handle_auth_plain,
// handler_auth_challenge_response) immediately before send_auth_ok()
// so any routing failure surfaces as an X-Protocol Error frame
// instead of leaving the client in a phantom "authenticated" state
// with no backend target.
int resolve_backend_target();
MysqlxDataStream client_ds_;
// Placeholder stream returned by server_ds() when no backend connection
// is attached. Intentionally never init()'d during the data-plane phase:
// the real backend stream lives on MysqlxConnection::backend_ds_, which
// owns the SSL* from the optional backend TLS handshake. Rewrapping the
// raw fd here after auth would discard that SSL* and silently regress
// TLS-wrapped sessions to cleartext I/O.
MysqlxDataStream server_ds_placeholder_;
MysqlxConnection* backend_conn_;
Mysqlx_Thread* thread_ptr_;
Status status_;
bool healthy;
std::string username_;
std::string schema_;
std::string auth_method_;
std::vector<uint8_t> auth_challenge_;
int target_hostgroup_;
std::string target_address_;
int target_port_;
// Per-endpoint TLS posture, copied from the resolved
// MysqlxBackendEndpoint at resolve_backend_target time. Drives
// backend TLS independently of frontend TLS state — operator
// sets mysqlx_backend_endpoints.use_ssl=1 to force backend TLS
// even when the client connected in plaintext.
bool target_use_ssl_;
// Cached identity_->default_route, captured in
// resolve_backend_target() so the data-plane bytes-counter sites
// (forward_to_backend / handler_waiting_server_msg) don't have
// to dereference identity_ on every frame. Empty until resolve
// succeeds; cleared by reset().
std::string route_name_;
// Logical route name the client connected through, captured at
// accept time from the listener's parallel route_name vector.
// Drives per-route policies that need to fire before any
// X-Protocol message has arrived (tls_mode='passthrough' is the
// only one today). Empty for sessions constructed without a
// listener (e.g. unit tests); the lookup paths fall back on the
// global default in that case.
std::string listener_route_name_;
MysqlxIdentityLookup identity_lookup_;
std::optional<MysqlxResolvedIdentity> identity_;
uint64_t start_time_;
uint64_t last_active_time_;
MysqlxResponseState response_state_;
// Sub-state flag for the response_state_ matrix: gates whether
// RESULTSET_ROW frames are currently allowed in states where the
// X-Protocol requires ColumnMetaData to precede any Row. Set when
// COLUMN_META_DATA is forwarded from the backend in
// handler_waiting_server_msg(); cleared on each transition into a
// state that begins a new column-metadata sequence (STMT_EXECUTE,
// CRUD, PREPARE_EXECUTE, CURSOR_OPEN), at terminal-frame flush, and
// at init() / reset(). NOT cleared on entry to CURSOR_FETCH — per
// the X-Protocol spec, ColumnMetaData is sent at Cursor::Open and
// not re-sent at Cursor::Fetch, so the flag must carry across.
bool seen_column_metadata_;
MysqlxTlsMode tls_mode_;
// Per-direction write backlogs for the passthrough splice loop
// (issue #5710 follow-up). When the destination fd's send buffer
// is full, write(2) returns short / EAGAIN; we used to abort the
// session, which kills any slow-consumer scenario (slow client,
// chatty backend, kernel buffer drained slower than we read). The
// backlogs hold the unwritten bytes between scheduler ticks; on
// every entry to handler_passthrough_forward() we drain the
// backlog first, then resume reading. Capped at
// PASSTHROUGH_BACKLOG_CAP (1 MiB) per direction to bound the
// proxy's memory exposure to a single slow-consumer DoS — beyond
// the cap the session is killed.
//
// c2b = client -> backend (bytes read from client_fd, awaiting
// write to backend_fd); b2c is the reverse. Cleared on reset()
// and on the destructor's return-to-pool path.
std::vector<uint8_t> passthrough_c2b_backlog_;
std::vector<uint8_t> passthrough_b2c_backlog_;
// Buffered CapabilitiesSet(tls=true) frame the client sent on a
// passthrough-mode route. Forwarded verbatim to the backend once
// the X_PASSTHROUGH_BACKEND_CONNECTING state's TCP connect
// completes. Cleared by reset() / init() / on transition to
// X_PASSTHROUGH_FORWARD.
std::vector<uint8_t> passthrough_pending_capset_frame_;
public:
static constexpr size_t PASSTHROUGH_BACKLOG_CAP = 1 * 1024 * 1024;
private:
// Compression negotiation state. compression_algo_ is set by
// handler_capabilities_set() once the client successfully sets the
// `compression` capability. The two combine_* fields are stored but
// not yet acted on; Phase 2 (decompression) and Phase 3 (compression
// on output) consume them to coalesce frames as the spec allows.
MysqlxCompressionAlgo compression_algo_;
bool compression_combine_mixed_messages_;
uint32_t compression_max_combine_messages_;
// Streaming decompression context, lazily allocated when the first
// Compression message arrives. Only used for ZSTD_STREAM (lz4_message
// is one-shot per frame and needs no persistent state). Freed in
// reset_compression_state(); zero-init for sessions that never
// negotiate compression.
ZSTD_DCtx* zstd_dctx_;
// Streaming compression context for outbound frames (Phase 3).
ZSTD_CCtx* zstd_cctx_;
// Phase 2 / Phase 3 plumbing — kept private to the .cpp.
int handle_compression_message();
void reset_compression_state();
// Phase 3: outbound compression. send_to_client_compressed() is the
// chokepoint that data-plane senders go through: it decides whether
// to wrap a body in a Mysqlx.Connection.Compression message based on
// the negotiated algorithm + body size threshold + combine flags.
// When combine_mixed_messages is in effect the body may be buffered
// in compress_batch_framed_ rather than emitted immediately; callers
// must invoke flush_compression_batch() at the end of a draining
// round to drain the buffer before waiting on more I/O.
void send_to_client_compressed(uint8_t msg_type, const uint8_t* body, size_t body_len);
void flush_compression_batch();
// Internal helpers — the single-message variant emits a Compression
// frame with `server_messages` set; the batched variant emits one
// with neither client_messages nor server_messages set, carrying a
// sequence of fully-framed messages in the payload.
bool emit_single_compressed(uint8_t msg_type, const uint8_t* body, size_t body_len);
bool emit_batched_compressed();
// Pending-batch state for combine_mixed_messages mode. The buffer
// holds zero or more fully-framed X messages (each prefixed with
// its 5-byte X header) waiting to be coalesced into one Compression
// message; pending_count is the number of messages currently in the
// buffer (not the byte count). Both reset to zero after each flush.
std::vector<uint8_t> compress_batch_framed_;
uint32_t compress_batch_count_;
// Pre-auth capability-message replay counter. A hostile or buggy
// client can replay CapabilitiesGet / CapabilitiesSet arbitrarily
// many times before authenticating; each one runs through protobuf
// parsing and a small allocation. Bound the count per session so
// the path is not a free CPU/memory amplifier. Reset to zero on
// successful auth; bumps on every CapabilitiesGet/Set seen while
// status_ is still pre-auth (CONNECTING_CLIENT / X_CAPABILITIES_*).
uint32_t pre_auth_cap_msgs_;
static constexpr uint32_t MAX_PRE_AUTH_CAP_MSGS = 64;
public:
// Test-only accessors for compression negotiation outcome.
MysqlxCompressionAlgo compression_algo_for_test() const { return compression_algo_; }
bool compression_combine_mixed_for_test() const { return compression_combine_mixed_messages_; }
uint32_t compression_max_combine_for_test() const { return compression_max_combine_messages_; }
// Test-only access to the outbound batch state (Phase 3).
uint32_t compression_batch_count_for_test() const { return compress_batch_count_; }
void flush_compression_batch_for_test() { flush_compression_batch(); }
// Test-only chokepoint: drive send_to_client_compressed() directly
// so unit tests can exercise the outbound compression path without
// having to wire a fake backend that emits frames over the data plane.
void send_to_client_compressed_for_test(uint8_t msg_type, const uint8_t* body, size_t body_len) {
send_to_client_compressed(msg_type, body, body_len);
}
const std::vector<uint8_t>& client_write_buffer_for_test() const {
return client_ds_.write_buffer_raw();
}
#ifdef MYSQLX_TEST_BUILD
// Test-only observers for the per-message response state machine,
// gated behind MYSQLX_TEST_BUILD because they expose private state
// the production code never reads back. Used by the validation
// tests in mysqlx_message_dispatch_unit-t to assert state
// transitions after dispatching synthetic backend frames.
MysqlxResponseState response_state_for_test() const { return response_state_; }
bool seen_column_metadata_for_test() const { return seen_column_metadata_; }
void set_response_state_for_test(MysqlxResponseState s) { response_state_ = s; }
// Test-only access to the NOTICE validation predicate so tests can
// drive the parser directly with synthetic bodies (well-formed,
// unknown-type, malformed protobuf, empty). Pure query — no
// session-state mutation, safe to call from any test.
bool is_notice_frame_valid_for_test(const uint8_t* body, size_t body_len) const {
return is_notice_frame_valid(body, body_len);
}
#endif /* MYSQLX_TEST_BUILD */
};
#endif