mirror of https://github.com/sysown/proxysql
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.
227 lines
9.6 KiB
227 lines
9.6 KiB
// Step 2 ABI extension test: pre-execution query hook.
|
|
//
|
|
// Exercises the manager-level register/dispatch pair AND the global
|
|
// dispatcher (proxysql_dispatch_configured_plugin_query_hook) that the
|
|
// MySQL_Session / PgSQL_Session hot path will call once Commit 2 lands.
|
|
// Hot-path call sites themselves are tested separately.
|
|
|
|
#include "ProxySQL_PluginManager.h"
|
|
#include "ProxySQL_Plugin.h"
|
|
#include "tap.h"
|
|
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <memory>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#ifndef PROXYSQL_FAKE_PLUGIN_PATH
|
|
#error "PROXYSQL_FAKE_PLUGIN_PATH must be defined"
|
|
#endif
|
|
|
|
namespace {
|
|
|
|
char g_fake_admin_db = '\0';
|
|
char g_fake_config_db = '\0';
|
|
char g_fake_stats_db = '\0';
|
|
|
|
ProxySQL_PluginQueryHookResult always_allow_hook(const ProxySQL_PluginQueryHookPayload&) {
|
|
return {ProxySQL_PluginQueryHookAction::allow, ""};
|
|
}
|
|
|
|
ProxySQL_PluginQueryHookResult always_deny_hook(const ProxySQL_PluginQueryHookPayload&) {
|
|
return {ProxySQL_PluginQueryHookAction::deny, "blocked"};
|
|
}
|
|
|
|
ProxySQL_PluginQueryHookResult echo_hook(const ProxySQL_PluginQueryHookPayload& p) {
|
|
std::string body(p.query_text, p.query_len);
|
|
return {ProxySQL_PluginQueryHookAction::allow,
|
|
std::string(p.user) + "/" + p.client_ip + "/" + p.schema + ":" + body};
|
|
}
|
|
|
|
ProxySQL_PluginQueryHookPayload payload_for(const char* user, const char* ip,
|
|
const char* schema, const char* query) {
|
|
return ProxySQL_PluginQueryHookPayload {
|
|
user, ip, schema, query, static_cast<uint32_t>(std::strlen(query))
|
|
};
|
|
}
|
|
|
|
} // namespace
|
|
|
|
SQLite3DB* proxysql_plugin_get_admindb() { return reinterpret_cast<SQLite3DB*>(&g_fake_admin_db); }
|
|
SQLite3DB* proxysql_plugin_get_configdb() { return reinterpret_cast<SQLite3DB*>(&g_fake_config_db); }
|
|
SQLite3DB* proxysql_plugin_get_statsdb() { return reinterpret_cast<SQLite3DB*>(&g_fake_stats_db); }
|
|
|
|
static void test_unregistered_protocols_have_no_hook() {
|
|
ProxySQL_PluginManager mgr;
|
|
ok(!mgr.has_query_hook(ProxySQL_PluginProtocol::mysql),
|
|
"fresh manager has no MySQL hook");
|
|
ok(!mgr.has_query_hook(ProxySQL_PluginProtocol::pgsql),
|
|
"fresh manager has no PgSQL hook");
|
|
ProxySQL_PluginQueryHookResult r {ProxySQL_PluginQueryHookAction::deny, "x"};
|
|
auto p = payload_for("u", "ip", "s", "select 1");
|
|
ok(!mgr.dispatch_query_hook(ProxySQL_PluginProtocol::mysql, p, r),
|
|
"dispatch returns false on protocol with no hook");
|
|
ok(r.action == ProxySQL_PluginQueryHookAction::deny && r.message == "x",
|
|
"dispatch leaves caller's result struct untouched on no-hook miss");
|
|
}
|
|
|
|
static void test_register_and_dispatch_allow() {
|
|
ProxySQL_PluginManager mgr;
|
|
ok(mgr.register_query_hook(ProxySQL_PluginProtocol::mysql, &always_allow_hook),
|
|
"register MySQL allow hook");
|
|
ok(mgr.has_query_hook(ProxySQL_PluginProtocol::mysql),
|
|
"MySQL hook now present");
|
|
|
|
ProxySQL_PluginQueryHookResult r {ProxySQL_PluginQueryHookAction::deny, "init"};
|
|
auto p = payload_for("alice", "10.0.0.1", "test", "select 1");
|
|
ok(mgr.dispatch_query_hook(ProxySQL_PluginProtocol::mysql, p, r),
|
|
"dispatch returns true (hook fired)");
|
|
ok(r.action == ProxySQL_PluginQueryHookAction::allow,
|
|
"hook returned ALLOW");
|
|
ok(r.message.empty(),
|
|
"ALLOW carries empty message");
|
|
}
|
|
|
|
static void test_register_and_dispatch_deny() {
|
|
ProxySQL_PluginManager mgr;
|
|
ok(mgr.register_query_hook(ProxySQL_PluginProtocol::mysql, &always_deny_hook),
|
|
"register MySQL deny hook");
|
|
ProxySQL_PluginQueryHookResult r {ProxySQL_PluginQueryHookAction::allow, ""};
|
|
auto p = payload_for("eve", "10.0.0.99", "prod", "drop table users");
|
|
ok(mgr.dispatch_query_hook(ProxySQL_PluginProtocol::mysql, p, r),
|
|
"dispatch returns true");
|
|
ok(r.action == ProxySQL_PluginQueryHookAction::deny,
|
|
"hook returned DENY");
|
|
ok(r.message == "blocked",
|
|
"DENY message propagated to caller");
|
|
}
|
|
|
|
static void test_register_null_callback_rejected() {
|
|
ProxySQL_PluginManager mgr;
|
|
ok(!mgr.register_query_hook(ProxySQL_PluginProtocol::mysql, nullptr),
|
|
"null callback rejected for MySQL");
|
|
ok(!mgr.register_query_hook(ProxySQL_PluginProtocol::pgsql, nullptr),
|
|
"null callback rejected for PgSQL");
|
|
ok(!mgr.has_query_hook(ProxySQL_PluginProtocol::mysql),
|
|
"no hook stored after rejection (MySQL)");
|
|
ok(!mgr.has_query_hook(ProxySQL_PluginProtocol::pgsql),
|
|
"no hook stored after rejection (PgSQL)");
|
|
}
|
|
|
|
static void test_duplicate_hook_rejected() {
|
|
ProxySQL_PluginManager mgr;
|
|
ok(mgr.register_query_hook(ProxySQL_PluginProtocol::mysql, &always_allow_hook),
|
|
"first MySQL registration succeeds");
|
|
ok(!mgr.register_query_hook(ProxySQL_PluginProtocol::mysql, &always_deny_hook),
|
|
"second MySQL registration rejected (one hook per protocol per manager)");
|
|
// Verify the first hook is still in effect
|
|
ProxySQL_PluginQueryHookResult r {ProxySQL_PluginQueryHookAction::deny, "x"};
|
|
auto p = payload_for("u", "ip", "s", "q");
|
|
mgr.dispatch_query_hook(ProxySQL_PluginProtocol::mysql, p, r);
|
|
ok(r.action == ProxySQL_PluginQueryHookAction::allow,
|
|
"original hook still in effect after rejected duplicate");
|
|
}
|
|
|
|
static void test_protocols_independent() {
|
|
ProxySQL_PluginManager mgr;
|
|
ok(mgr.register_query_hook(ProxySQL_PluginProtocol::mysql, &always_allow_hook),
|
|
"MySQL hook registers");
|
|
ok(mgr.register_query_hook(ProxySQL_PluginProtocol::pgsql, &always_deny_hook),
|
|
"PgSQL hook registers independently");
|
|
|
|
ProxySQL_PluginQueryHookResult r {ProxySQL_PluginQueryHookAction::deny, ""};
|
|
auto p = payload_for("u", "ip", "s", "q");
|
|
mgr.dispatch_query_hook(ProxySQL_PluginProtocol::mysql, p, r);
|
|
ok(r.action == ProxySQL_PluginQueryHookAction::allow,
|
|
"MySQL dispatch routes to MySQL hook (allow)");
|
|
mgr.dispatch_query_hook(ProxySQL_PluginProtocol::pgsql, p, r);
|
|
ok(r.action == ProxySQL_PluginQueryHookAction::deny,
|
|
"PgSQL dispatch routes to PgSQL hook (deny)");
|
|
}
|
|
|
|
static void test_payload_threaded_through() {
|
|
ProxySQL_PluginManager mgr;
|
|
mgr.register_query_hook(ProxySQL_PluginProtocol::mysql, &echo_hook);
|
|
ProxySQL_PluginQueryHookResult r {ProxySQL_PluginQueryHookAction::deny, ""};
|
|
auto p = payload_for("alice", "10.1.2.3", "shop", "SELECT id FROM orders");
|
|
ok(mgr.dispatch_query_hook(ProxySQL_PluginProtocol::mysql, p, r),
|
|
"dispatch fires echo hook");
|
|
ok(r.message == "alice/10.1.2.3/shop:SELECT id FROM orders",
|
|
"all payload fields reach the hook intact (got: '%s')", r.message.c_str());
|
|
}
|
|
|
|
static void test_global_dispatcher_no_active_manager() {
|
|
// Make sure no manager is active (defensive — earlier tests may have
|
|
// left state behind, but we never set the active pointer in this test).
|
|
ok(proxysql_get_plugin_manager() == nullptr,
|
|
"global manager is null at start");
|
|
ok(!proxysql_has_configured_plugin_query_hook(ProxySQL_PluginProtocol::mysql),
|
|
"has_hook helper returns false with no active manager");
|
|
ProxySQL_PluginQueryHookResult r {ProxySQL_PluginQueryHookAction::deny, "untouched"};
|
|
auto p = payload_for("u", "ip", "s", "q");
|
|
ok(!proxysql_dispatch_configured_plugin_query_hook(ProxySQL_PluginProtocol::mysql, p, r),
|
|
"dispatcher returns false with no active manager");
|
|
ok(r.message == "untouched",
|
|
"dispatcher leaves caller's result untouched on miss");
|
|
}
|
|
|
|
static void test_global_dispatcher_with_active_manager() {
|
|
setenv("PROXYSQL_FAKE_PLUGIN_REGISTER_QUERY_HOOK", "1", 1);
|
|
std::unique_ptr<ProxySQL_PluginManager> mgr;
|
|
std::vector<std::string> paths { PROXYSQL_FAKE_PLUGIN_PATH };
|
|
std::string err;
|
|
ok(proxysql_load_configured_plugins(mgr, paths, err),
|
|
"load fake plugin (which registers a MySQL query hook)");
|
|
ok(proxysql_has_configured_plugin_query_hook(ProxySQL_PluginProtocol::mysql),
|
|
"has_hook helper reports a MySQL hook is now active");
|
|
ok(!proxysql_has_configured_plugin_query_hook(ProxySQL_PluginProtocol::pgsql),
|
|
"has_hook helper reports no PgSQL hook (fake plugin only registered MySQL)");
|
|
|
|
ProxySQL_PluginQueryHookResult r {ProxySQL_PluginQueryHookAction::deny, ""};
|
|
auto p = payload_for("u", "ip", "s", "select 42");
|
|
ok(proxysql_dispatch_configured_plugin_query_hook(ProxySQL_PluginProtocol::mysql, p, r),
|
|
"global dispatcher routes to fake hook");
|
|
ok(r.action == ProxySQL_PluginQueryHookAction::allow,
|
|
"hook returned ALLOW (no DENY env set)");
|
|
ok(r.message == "select 42",
|
|
"fake hook echoed the SQL through the result message (got: '%s')", r.message.c_str());
|
|
|
|
// Now flip to DENY and re-dispatch
|
|
setenv("PROXYSQL_FAKE_PLUGIN_HOOK_DENY", "1", 1);
|
|
ProxySQL_PluginQueryHookResult r2 {ProxySQL_PluginQueryHookAction::allow, ""};
|
|
ok(proxysql_dispatch_configured_plugin_query_hook(ProxySQL_PluginProtocol::mysql, p, r2),
|
|
"second dispatch with DENY env still fires");
|
|
ok(r2.action == ProxySQL_PluginQueryHookAction::deny,
|
|
"hook now returns DENY");
|
|
ok(r2.message == "denied: select 42",
|
|
"DENY message includes the offending SQL (got: '%s')", r2.message.c_str());
|
|
|
|
(void)proxysql_stop_configured_plugins(mgr, err);
|
|
unsetenv("PROXYSQL_FAKE_PLUGIN_REGISTER_QUERY_HOOK");
|
|
unsetenv("PROXYSQL_FAKE_PLUGIN_HOOK_DENY");
|
|
|
|
// After stop, neither helper should report a hook.
|
|
ok(!proxysql_has_configured_plugin_query_hook(ProxySQL_PluginProtocol::mysql),
|
|
"has_hook helper returns false after stop");
|
|
ProxySQL_PluginQueryHookResult r3 {ProxySQL_PluginQueryHookAction::deny, "still-here"};
|
|
ok(!proxysql_dispatch_configured_plugin_query_hook(ProxySQL_PluginProtocol::mysql, p, r3),
|
|
"dispatcher returns false after stop");
|
|
}
|
|
|
|
int main() {
|
|
plan(41);
|
|
|
|
test_unregistered_protocols_have_no_hook();
|
|
test_register_and_dispatch_allow();
|
|
test_register_and_dispatch_deny();
|
|
test_register_null_callback_rejected();
|
|
test_duplicate_hook_rejected();
|
|
test_protocols_independent();
|
|
test_payload_threaded_through();
|
|
test_global_dispatcher_no_active_manager();
|
|
test_global_dispatcher_with_active_manager();
|
|
|
|
return exit_status();
|
|
}
|