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/test/tap/tests/test_tls_stats-t.cpp

345 lines
14 KiB

/**
* @file test_tls_stats-t.cpp
* @brief TAP test for stats_tls_certificates and stats_proxysql_global TLS metrics.
*
* @details This test verifies:
* 1. stats_proxysql_global table exists and is queryable from the stats schema.
* 2. stats_proxysql_global contains the expected TLS tracking variables:
* - TLS_Load_Count
* - TLS_Last_Load_Timestamp
* - TLS_Last_Load_Result
* - TLS_Server_Cert_File
* - TLS_CA_Cert_File
* - TLS_Key_File
* 3. TLS_Load_Count >= 1 (ProxySQL always loads certs at startup).
* 4. TLS_Last_Load_Timestamp is a positive Unix timestamp.
* 5. TLS_Last_Load_Result is one of "NONE", "SUCCESS", or "FAILED".
* 6. TLS_Server_Cert_File and TLS_CA_Cert_File are non-empty strings.
* 7. stats_tls_certificates table exists and is queryable.
* 8. stats_tls_certificates has exactly two rows (cert_type 'server' and 'ca').
* 9. Both rows have non-empty file_path, subject_cn, issuer_cn, serial_number,
* not_before, not_after, sha256_fingerprint.
* 10. days_until_expiry is a reasonable value (> -36500 and < 36500).
* 11. loaded_at is a positive Unix timestamp.
* 12. After PROXYSQL RELOAD TLS:
* - TLS_Load_Count in stats_proxysql_global increments by 1.
* - stats_tls_certificates rows are still present and non-empty.
* 13. TLS_Last_Load_Timestamp in stats_proxysql_global increases after PROXYSQL RELOAD TLS.
* 14. TLS-related variables are NOT present in stats_mysql_global.
*/
#include <cstdlib>
#include <cstdio>
#include <cstring>
#include <unistd.h>
#include <map>
#include <set>
#include <string>
#include <vector>
#include "mysql.h"
#include "tap.h"
#include "command_line.h"
#include "utils.h"
using std::map;
using std::set;
using std::string;
using std::vector;
/** @brief Maximum plausible lifetime of a certificate in days (approx. 100 years). */
static const int MAX_CERT_DAYS = 36500;
/**
* @brief Executes a query and returns all rows as a map keyed by the first column.
* @param conn An open MySQL connection.
* @param query The SQL query to run (must return exactly 2 columns).
* @return A map<string, string> where key=col[0] and value=col[1].
*/
static map<string,string> query_key_value(MYSQL* conn, const char* query) {
map<string,string> result;
if (mysql_query(conn, query)) {
diag("Query failed: '%s' err='%s'", query, mysql_error(conn));
return result;
}
MYSQL_RES* res = mysql_store_result(conn);
if (!res) return result;
MYSQL_ROW row;
while ((row = mysql_fetch_row(res))) {
if (row[0] && row[1])
result[string(row[0])] = string(row[1]);
}
mysql_free_result(res);
return result;
}
/**
* @brief Executes a query and returns all rows as a vector of key-value maps.
* @param conn An open MySQL connection.
* @param query The SQL query to run.
* @param col_names Column names to use as map keys (in order).
* @return A vector of maps, one per result row.
*/
static vector<map<string,string>> query_rows(MYSQL* conn, const char* query, const vector<string>& col_names) {
vector<map<string,string>> rows;
if (mysql_query(conn, query)) {
diag("Query failed: '%s' err='%s'", query, mysql_error(conn));
return rows;
}
MYSQL_RES* res = mysql_store_result(conn);
if (!res) return rows;
MYSQL_ROW row;
while ((row = mysql_fetch_row(res))) {
map<string,string> r;
for (size_t i = 0; i < col_names.size(); i++) {
r[col_names[i]] = row[i] ? string(row[i]) : "";
}
rows.push_back(r);
}
mysql_free_result(res);
return rows;
}
int main(int argc, char** argv) {
CommandLine cl;
diag("TAP test for SSL/TLS Certificate Statistics Table");
diag("This test verifies that ProxySQL correctly reports TLS certificate information,");
diag("tracks TLS load operations in stats_proxysql_global, and updates these stats after a reload.");
if (cl.getEnv()) {
diag("Failed to get the required environmental variables.");
return exit_status();
}
// Tests:
// 1-5: stats_proxysql_global TLS variables present
// 6-9: stats_proxysql_global TLS variable values
// 10-11: TLS_Last_Load_Result valid value
// 12-16: stats_tls_certificates structure
// 17-24: stats_tls_certificates row data for 'server' and 'ca'
// 23-26: PROXYSQL RELOAD TLS increments counter and updates timestamp
// 27: TLS vars NOT in stats_mysql_global
plan(27);
diag("Connecting to ProxySQL Admin interface on %s:%d", cl.host, cl.admin_port);
MYSQL* admin = mysql_init(NULL);
if (!admin) {
fprintf(stderr, "File %s, line %d, Error: %s\n", __FILE__, __LINE__, mysql_error(admin));
return exit_status();
}
if (!mysql_real_connect(admin, cl.host, cl.admin_username, cl.admin_password, NULL, cl.admin_port, NULL, 0)) {
fprintf(stderr, "File %s, line %d, Error: %s\n", __FILE__, __LINE__, mysql_error(admin));
return exit_status();
}
// -----------------------------------------------------------------------
// Part 1: stats_proxysql_global - TLS tracking variables
// -----------------------------------------------------------------------
diag("Step 1: Verifying TLS tracking variables in stats.stats_proxysql_global");
diag("--- Querying stats.stats_proxysql_global ---");
auto global_stats = query_key_value(admin,
"SELECT Variable_Name, Variable_Value FROM stats.stats_proxysql_global");
const vector<string> expected_tls_vars = {
"TLS_Load_Count",
"TLS_Last_Load_Timestamp",
"TLS_Last_Load_Result",
"TLS_Server_Cert_File",
"TLS_CA_Cert_File",
"TLS_Key_File",
};
for (const auto& var : expected_tls_vars) {
ok(global_stats.count(var) > 0,
"stats_proxysql_global: Variable '%s' is present", var.c_str());
}
diag("Verifying values of TLS tracking variables...");
// TLS_Load_Count >= 1
int tls_load_count = 0;
if (global_stats.count("TLS_Load_Count"))
tls_load_count = std::stoi(global_stats["TLS_Load_Count"]);
ok(tls_load_count >= 1,
"stats_proxysql_global: TLS_Load_Count is >= 1 (was %d)", tls_load_count);
// TLS_Last_Load_Timestamp is a positive Unix timestamp (> 0)
long long tls_ts = 0;
if (global_stats.count("TLS_Last_Load_Timestamp"))
tls_ts = std::stoll(global_stats["TLS_Last_Load_Timestamp"]);
ok(tls_ts > 0,
"stats_proxysql_global: TLS_Last_Load_Timestamp is positive (was %lld)", tls_ts);
// TLS_Last_Load_Result must be one of NONE, SUCCESS, FAILED
string tls_result = global_stats.count("TLS_Last_Load_Result") ?
global_stats["TLS_Last_Load_Result"] : "";
ok(tls_result == "SUCCESS" || tls_result == "NONE" || tls_result == "FAILED",
"stats_proxysql_global: TLS_Last_Load_Result='%s' is valid (NONE/SUCCESS/FAILED)",
tls_result.c_str());
// TLS_Server_Cert_File should be a non-empty path
string cert_file = global_stats.count("TLS_Server_Cert_File") ?
global_stats["TLS_Server_Cert_File"] : "";
ok(!cert_file.empty(),
"stats_proxysql_global: TLS_Server_Cert_File is non-empty ('%s')", cert_file.c_str());
// TLS_CA_Cert_File should be a non-empty path
string ca_file = global_stats.count("TLS_CA_Cert_File") ?
global_stats["TLS_CA_Cert_File"] : "";
ok(!ca_file.empty(),
"stats_proxysql_global: TLS_CA_Cert_File is non-empty ('%s')", ca_file.c_str());
// -----------------------------------------------------------------------
// Part 2: stats_tls_certificates - table structure and row content
// -----------------------------------------------------------------------
diag("Step 2: Verifying content of stats.stats_tls_certificates");
diag("--- Querying stats.stats_tls_certificates ---");
const vector<string> cert_cols = {
"cert_type", "file_path", "subject_cn", "issuer_cn",
"serial_number", "not_before", "not_after",
"days_until_expiry", "sha256_fingerprint", "loaded_at"
};
auto cert_rows = query_rows(admin,
"SELECT cert_type, file_path, subject_cn, issuer_cn, serial_number, "
"not_before, not_after, days_until_expiry, sha256_fingerprint, loaded_at "
"FROM stats.stats_tls_certificates",
cert_cols);
// Should have exactly 2 rows: 'server' and 'ca'
ok(cert_rows.size() == 2,
"stats_tls_certificates: has exactly 2 rows (got %zu)", cert_rows.size());
// Find the 'server' and 'ca' rows
map<string,string> server_row, ca_row;
for (const auto& r : cert_rows) {
if (r.at("cert_type") == "server") server_row = r;
if (r.at("cert_type") == "ca") ca_row = r;
}
diag("Validating cert_type 'server' and 'ca' rows exist...");
ok(!server_row.empty(), "stats_tls_certificates: row with cert_type='server' exists");
ok(!ca_row.empty(), "stats_tls_certificates: row with cert_type='ca' exists");
// Check non-empty required fields for server cert
diag("Validating server certificate row data...");
if (!server_row.empty()) {
ok(!server_row["file_path"].empty(),
"stats_tls_certificates: server file_path is non-empty ('%s')",
server_row["file_path"].c_str());
ok(!server_row["sha256_fingerprint"].empty(),
"stats_tls_certificates: server sha256_fingerprint is non-empty");
int server_days = std::stoi(server_row["days_until_expiry"]);
ok(server_days > -MAX_CERT_DAYS && server_days < MAX_CERT_DAYS,
"stats_tls_certificates: server days_until_expiry=%d is reasonable",
server_days);
long long server_loaded_at = std::stoll(server_row["loaded_at"]);
ok(server_loaded_at > 0,
"stats_tls_certificates: server loaded_at=%lld is positive",
server_loaded_at);
} else {
ok(false, "stats_tls_certificates: server file_path is non-empty (row missing)");
ok(false, "stats_tls_certificates: server sha256_fingerprint is non-empty (row missing)");
ok(false, "stats_tls_certificates: server days_until_expiry is reasonable (row missing)");
ok(false, "stats_tls_certificates: server loaded_at is positive (row missing)");
}
// Check non-empty required fields for CA cert
diag("Validating CA certificate row data...");
if (!ca_row.empty()) {
ok(!ca_row["file_path"].empty(),
"stats_tls_certificates: ca file_path is non-empty ('%s')",
ca_row["file_path"].c_str());
ok(!ca_row["sha256_fingerprint"].empty(),
"stats_tls_certificates: ca sha256_fingerprint is non-empty");
int ca_days = std::stoi(ca_row["days_until_expiry"]);
ok(ca_days > -MAX_CERT_DAYS && ca_days < MAX_CERT_DAYS,
"stats_tls_certificates: ca days_until_expiry=%d is reasonable",
ca_days);
long long ca_loaded_at = std::stoll(ca_row["loaded_at"]);
ok(ca_loaded_at > 0,
"stats_tls_certificates: ca loaded_at=%lld is positive",
ca_loaded_at);
} else {
ok(false, "stats_tls_certificates: ca file_path is non-empty (row missing)");
ok(false, "stats_tls_certificates: ca sha256_fingerprint is non-empty (row missing)");
ok(false, "stats_tls_certificates: ca days_until_expiry is reasonable (row missing)");
ok(false, "stats_tls_certificates: ca loaded_at is positive (row missing)");
}
// -----------------------------------------------------------------------
// Part 3: PROXYSQL RELOAD TLS increments TLS_Load_Count and updates timestamp
// -----------------------------------------------------------------------
diag("Step 3: Verifying PROXYSQL RELOAD TLS updates stats");
diag("--- Executing PROXYSQL RELOAD TLS ---");
// Sleep 1 second to guarantee timestamp changes on fast systems
sleep(1);
if (mysql_query(admin, "PROXYSQL RELOAD TLS")) {
diag("PROXYSQL RELOAD TLS failed: %s", mysql_error(admin));
}
mysql_free_result(mysql_store_result(admin));
diag("Verifying updated stats in stats.stats_proxysql_global after reload...");
auto global_stats_after = query_key_value(admin,
"SELECT Variable_Name, Variable_Value FROM stats.stats_proxysql_global");
int tls_load_count_after = 0;
if (global_stats_after.count("TLS_Load_Count"))
tls_load_count_after = std::stoi(global_stats_after["TLS_Load_Count"]);
ok(tls_load_count_after == tls_load_count + 1,
"stats_proxysql_global: TLS_Load_Count incremented after RELOAD TLS (%d -> %d)",
tls_load_count, tls_load_count_after);
long long tls_ts_after = 0;
if (global_stats_after.count("TLS_Last_Load_Timestamp"))
tls_ts_after = std::stoll(global_stats_after["TLS_Last_Load_Timestamp"]);
ok(tls_ts_after > tls_ts,
"stats_proxysql_global: TLS_Last_Load_Timestamp increased after RELOAD TLS (%lld -> %lld)",
tls_ts, tls_ts_after);
string tls_result_after = global_stats_after.count("TLS_Last_Load_Result") ?
global_stats_after["TLS_Last_Load_Result"] : "";
ok(tls_result_after == "SUCCESS",
"stats_proxysql_global: TLS_Last_Load_Result='SUCCESS' after RELOAD TLS (got '%s')",
tls_result_after.c_str());
diag("Verifying stats.stats_tls_certificates rows after reload...");
// stats_tls_certificates rows still present after reload
auto cert_rows_after = query_rows(admin,
"SELECT cert_type, file_path, sha256_fingerprint FROM stats.stats_tls_certificates",
{"cert_type", "file_path", "sha256_fingerprint"});
ok(cert_rows_after.size() == 2,
"stats_tls_certificates: still has 2 rows after RELOAD TLS (got %zu)",
cert_rows_after.size());
// -----------------------------------------------------------------------
// Part 4: TLS variables must NOT appear in stats_mysql_global
// -----------------------------------------------------------------------
diag("Step 4: Verifying TLS variables are absent from stats.stats_mysql_global");
diag("--- Querying stats.stats_mysql_global ---");
auto mysql_global_stats = query_key_value(admin,
"SELECT Variable_Name, Variable_Value FROM stats.stats_mysql_global");
bool tls_vars_in_mysql_global = false;
for (const auto& var : expected_tls_vars) {
if (mysql_global_stats.count(var)) {
diag("UNEXPECTED: TLS variable '%s' found in stats_mysql_global", var.c_str());
tls_vars_in_mysql_global = true;
}
}
ok(!tls_vars_in_mysql_global,
"TLS variables are NOT present in stats_mysql_global");
diag("Test completed successfully, closing connection.");
mysql_close(admin);
return exit_status();
}