mirror of https://github.com/sysown/proxysql
parent
6c5bd0971c
commit
44db1dfb40
@ -0,0 +1,408 @@
|
||||
/**
|
||||
* @file test_sqlite3_pass_exts-t.cpp
|
||||
* @brief Tests the SQLite3 extensions in the Admin interface and it's MySQL compatibility.
|
||||
* @details The test perform the following operations:
|
||||
* 1. Create MySQL users with random pass and check pass reproduction for:
|
||||
* - 'mysql_native_password'
|
||||
* - 'caching_sha2_password'
|
||||
* 2. Stress password creation, ensure that start and length matches expected.
|
||||
*/
|
||||
|
||||
#include <cassert>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <utility>
|
||||
|
||||
#include "mysql.h"
|
||||
|
||||
#include "command_line.h"
|
||||
#include "tap.h"
|
||||
#include "utils.h"
|
||||
|
||||
using std::string;
|
||||
using std::vector;
|
||||
using std::pair;
|
||||
|
||||
struct user_def_t {
|
||||
string name;
|
||||
string auth;
|
||||
string pass;
|
||||
string hash;
|
||||
string salt;
|
||||
};
|
||||
|
||||
#define MYSQL_QUERY_T_(mysql, query) \
|
||||
do { \
|
||||
const std::string time { get_formatted_time() }; \
|
||||
fprintf(stderr, "# %s: Issuing query `%s` to ('%s':%d)\n", time.c_str(), query, mysql->host, mysql->port); \
|
||||
if (mysql_query(mysql, query)) { \
|
||||
fprintf(stderr, "File %s, line %d, Error: %s\n", __FILE__, __LINE__, mysql_error(mysql)); \
|
||||
return { EXIT_FAILURE, user_def_t {} }; \
|
||||
} \
|
||||
} while(0)
|
||||
|
||||
pair<int,user_def_t> create_mysql_user_rnd_creds(MYSQL* mysql, const string& name, const string& auth) {
|
||||
diag("Creating user with random pass user:'%s'", name.c_str());
|
||||
|
||||
const string CREATE_USER {
|
||||
"CREATE USER '" + name + "'@'%' IDENTIFIED WITH '" + auth + "' BY RANDOM PASSWORD"
|
||||
};
|
||||
const string EXT_NATIVE_AUTH {
|
||||
"SELECT authentication_string FROM mysql.user WHERE user='" + name + "'"
|
||||
};
|
||||
const string EXT_SHA2_AUTH {
|
||||
"SELECT HEX(authentication_string), HEX(SUBSTR(authentication_string, 8, 20)) AS salt"
|
||||
" FROM mysql.user WHERE user='" + name + "'"
|
||||
};
|
||||
const string DROP_USER { "DROP USER IF EXISTS '" + name + "'"};
|
||||
string pass {};
|
||||
string hash {};
|
||||
string salt {};
|
||||
|
||||
// DROP/CREATE and extract new password
|
||||
{
|
||||
MYSQL_QUERY_T_(mysql, DROP_USER.c_str());
|
||||
MYSQL_QUERY_T_(mysql, CREATE_USER.c_str());
|
||||
|
||||
MYSQL_RES* myres = mysql_store_result(mysql);
|
||||
MYSQL_ROW myrow = mysql_fetch_row(myres);
|
||||
|
||||
if (myrow && myrow[2]) {
|
||||
pass = string { myrow[2] };
|
||||
}
|
||||
|
||||
mysql_free_result(myres);
|
||||
}
|
||||
|
||||
// Extract 'authentication_string' and 'salt'
|
||||
if (auth == "mysql_native_password") {
|
||||
MYSQL_QUERY_T_(mysql, EXT_NATIVE_AUTH.c_str());
|
||||
MYSQL_RES* myres = mysql_store_result(mysql);
|
||||
MYSQL_ROW myrow = mysql_fetch_row(myres);
|
||||
|
||||
if (myrow && myrow[0]) {
|
||||
hash = string { myrow[0] };
|
||||
} else {
|
||||
assert(!"Received malformed result");
|
||||
}
|
||||
|
||||
mysql_free_result(myres);
|
||||
} else if (auth == "caching_sha2_password") {
|
||||
MYSQL_QUERY_T_(mysql, EXT_SHA2_AUTH.c_str());
|
||||
|
||||
MYSQL_RES* myres = mysql_store_result(mysql);
|
||||
MYSQL_ROW myrow = mysql_fetch_row(myres);
|
||||
|
||||
if (myrow && myrow[0] && myrow[1]) {
|
||||
hash = string { myrow[0] };
|
||||
salt = string { myrow[1] };
|
||||
} else {
|
||||
assert(!"Received malformed result");
|
||||
}
|
||||
|
||||
mysql_free_result(myres);
|
||||
} else {
|
||||
assert(!"Invalid auth method");
|
||||
}
|
||||
|
||||
diag(
|
||||
"Created user user:'%s', pass:'%s', hash: '%s', salt:'%s'",
|
||||
name.c_str(), pass.c_str(), hash.c_str(), salt.c_str()
|
||||
);
|
||||
|
||||
return { EXIT_SUCCESS, user_def_t { name, auth, pass, hash, salt } };
|
||||
}
|
||||
|
||||
int test_pass_match(MYSQL* admin, const user_def_t& def) {
|
||||
diag(
|
||||
"Test MySQL/Admin pass match user:'%s', auth:'%s', pass:'%s', hash: '%s', salt:'%s'",
|
||||
def.name.c_str(), def.auth.c_str(), def.pass.c_str(), def.hash.c_str(), def.salt.c_str()
|
||||
);
|
||||
|
||||
if (def.auth == "mysql_native_password") {
|
||||
const string GEN_NATIVE_PASS {
|
||||
"SELECT MYSQL_NATIVE_PASSWORD('" + def.pass + "')"
|
||||
};
|
||||
MYSQL_QUERY_T(admin, GEN_NATIVE_PASS.c_str());
|
||||
|
||||
MYSQL_RES* myres = mysql_store_result(admin);
|
||||
MYSQL_ROW myrow = mysql_fetch_row(myres);
|
||||
|
||||
string admin_hash { myrow[0] };
|
||||
|
||||
ok(
|
||||
def.hash == admin_hash,
|
||||
"MySQL hash should match ProxySQL generated mysql:'%s', admin:'%s'",
|
||||
def.hash.c_str(), admin_hash.c_str()
|
||||
);
|
||||
|
||||
mysql_free_result(myres);
|
||||
} else if (def.auth == "caching_sha2_password") {
|
||||
const string GEN_SHA2_PASS {
|
||||
"SELECT HEX(CACHING_SHA2_PASSWORD('" + def.pass + "', UNHEX('" + def.salt + "')))"
|
||||
};
|
||||
MYSQL_QUERY_T(admin, GEN_SHA2_PASS.c_str());
|
||||
|
||||
MYSQL_RES* myres = mysql_store_result(admin);
|
||||
MYSQL_ROW myrow = mysql_fetch_row(myres);
|
||||
|
||||
string admin_hash { myrow[0] };
|
||||
|
||||
ok(
|
||||
def.hash == admin_hash,
|
||||
"MySQL hash should match ProxySQL generated mysql:'%s', admin:'%s'",
|
||||
def.hash.c_str(), admin_hash.c_str()
|
||||
);
|
||||
|
||||
mysql_free_result(myres);
|
||||
}
|
||||
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
int test_pass_gen(MYSQL* admin, const string& auth, const string& pass, const string& salt) {
|
||||
diag("Test Admin pass hash gen auth:'%s',pass:'%s'", auth.c_str(), pass.c_str());
|
||||
|
||||
if (auth == "mysql_native_password") {
|
||||
const string GEN_NATIVE_PASS { "SELECT MYSQL_NATIVE_PASSWORD('" + pass + "')" };
|
||||
MYSQL_QUERY_T(admin, GEN_NATIVE_PASS.c_str());
|
||||
|
||||
MYSQL_RES* myres = mysql_store_result(admin);
|
||||
MYSQL_ROW myrow = mysql_fetch_row(myres);
|
||||
|
||||
if (pass.size() > 0) {
|
||||
const string admin_hash { myrow[0] };
|
||||
bool valid_hash = admin_hash.size() == 41 && admin_hash[0] == '*';
|
||||
|
||||
ok(
|
||||
valid_hash,
|
||||
"Gen hash should be wellformed size:'%lu', hash:'%s'",
|
||||
admin_hash.size(), admin_hash.c_str()
|
||||
);
|
||||
} else {
|
||||
const string act_msg { myrow[0] };
|
||||
const string exp_msg { "Invalid argument size" };
|
||||
|
||||
ok(
|
||||
exp_msg == act_msg,
|
||||
"Args verf should have failed exp_msg:'%s', act_msg:'%s'",
|
||||
exp_msg.c_str(), act_msg.c_str()
|
||||
);
|
||||
}
|
||||
|
||||
mysql_free_result(myres);
|
||||
} else if (auth == "caching_sha2_password") {
|
||||
const string GEN_SHA2_PASS { "SELECT CACHING_SHA2_PASSWORD('" + pass + "', '" + salt + "')" };
|
||||
MYSQL_QUERY_T(admin, GEN_SHA2_PASS.c_str());
|
||||
|
||||
MYSQL_RES* myres = mysql_store_result(admin);
|
||||
MYSQL_ROW myrow = mysql_fetch_row(myres);
|
||||
|
||||
if (pass.size() > 0 && salt.size() > 0 && salt.size() <= 20) {
|
||||
const string admin_hash { myrow[0] };
|
||||
const string hash_start { "$A$005$" + salt };
|
||||
|
||||
bool valid_hash =
|
||||
(admin_hash.size() == 50 + salt.size()) &&
|
||||
admin_hash.rfind(hash_start, 0) == 0;
|
||||
|
||||
ok(
|
||||
valid_hash,
|
||||
"Gen hash should be wellformed size:'%lu', salt_size:'%lu', hash:'%s'",
|
||||
admin_hash.size(), salt.size(), admin_hash.c_str()
|
||||
);
|
||||
} else {
|
||||
const string act_msg { myrow[0] };
|
||||
const string exp_msg { "Invalid argument size" };
|
||||
|
||||
ok(
|
||||
exp_msg == act_msg,
|
||||
"Args verf should have failed exp_msg:'%s', act_msg:'%s'",
|
||||
exp_msg.c_str(), act_msg.c_str()
|
||||
);
|
||||
}
|
||||
|
||||
mysql_free_result(myres);
|
||||
}
|
||||
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
pair<int,string> get_query_res(MYSQL* mysql, const string& query) {
|
||||
string res {};
|
||||
|
||||
int rc = mysql_query_t(mysql, query.c_str());
|
||||
|
||||
if (rc) {
|
||||
return { rc, mysql_error(mysql) };
|
||||
} else {
|
||||
MYSQL_RES* myres = mysql_store_result(mysql);
|
||||
MYSQL_ROW myrow = mysql_fetch_row(myres);
|
||||
|
||||
if (myrow && myrow[0]) {
|
||||
res = myrow[0];
|
||||
} else {
|
||||
rc = 1;
|
||||
res = "Invalid resultset received";
|
||||
}
|
||||
|
||||
mysql_free_result(myres);
|
||||
}
|
||||
|
||||
return { rc, res };
|
||||
}
|
||||
|
||||
struct inv_input_t {
|
||||
string query;
|
||||
int err;
|
||||
string msg;
|
||||
};
|
||||
|
||||
// NOTE: If modified, set even numbers
|
||||
const uint32_t USER_GEN_COUNT = 100;
|
||||
const uint32_t PASS_GEN_COUNT = 1000;
|
||||
|
||||
const vector<inv_input_t> INV_INPUTS {
|
||||
{
|
||||
"SELECT MYSQL_NATIVE_PASSWORD()", 1,
|
||||
"ProxySQL Admin Error: wrong number of arguments to function MYSQL_NATIVE_PASSWORD()"
|
||||
},
|
||||
{
|
||||
"SELECT MYSQL_NATIVE_PASSWORD('00', '00')", 1,
|
||||
"ProxySQL Admin Error: wrong number of arguments to function MYSQL_NATIVE_PASSWORD()"
|
||||
},
|
||||
{ "SELECT MYSQL_NATIVE_PASSWORD('')", 0, "Invalid argument size" },
|
||||
{ "SELECT MYSQL_NATIVE_PASSWORD(2)", 0, "Invalid argument type" },
|
||||
|
||||
{
|
||||
"SELECT CACHING_SHA2_PASSWORD()", 1,
|
||||
"ProxySQL Admin Error: wrong number of arguments to function CACHING_SHA2_PASSWORD()"
|
||||
},
|
||||
{
|
||||
"SELECT CACHING_SHA2_PASSWORD('00', '00', '00')", 1,
|
||||
"ProxySQL Admin Error: wrong number of arguments to function CACHING_SHA2_PASSWORD()"
|
||||
},
|
||||
{ "SELECT CACHING_SHA2_PASSWORD('', '')", 0, "Invalid argument size" },
|
||||
{ "SELECT CACHING_SHA2_PASSWORD('', '000000000000000000000')", 0, "Invalid argument size" },
|
||||
{ "SELECT CACHING_SHA2_PASSWORD(2, '00')", 0, "Invalid argument type" },
|
||||
{ "SELECT CACHING_SHA2_PASSWORD('00', 2)", 0, "Invalid argument type" },
|
||||
};
|
||||
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
CommandLine cl;
|
||||
|
||||
plan(
|
||||
INV_INPUTS.size() +
|
||||
USER_GEN_COUNT +
|
||||
PASS_GEN_COUNT * 2 +
|
||||
2 // EXTRA: Two extra correctness tests; forcing randomness
|
||||
);
|
||||
|
||||
if (cl.getEnv()) {
|
||||
diag("Failed to get the required environmental variables.");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
MYSQL* mysql = mysql_init(NULL);
|
||||
|
||||
if (!mysql_real_connect(mysql, cl.host, cl.mysql_username, cl.mysql_password, NULL, cl.mysql_port, NULL, 0)) {
|
||||
fprintf(stderr, "File %s, line %d, Error: %s\n", __FILE__, __LINE__, mysql_error(mysql));
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
|
||||
MYSQL* admin = mysql_init(NULL);
|
||||
|
||||
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_FAILURE;
|
||||
}
|
||||
|
||||
// Tests functions input verification
|
||||
{
|
||||
for (const inv_input_t& inv_input : INV_INPUTS) {
|
||||
const pair<int,string> res { get_query_res(admin, inv_input.query) };
|
||||
|
||||
if (res.first && inv_input.err == 0) {
|
||||
diag("Query on Admin unexpectedly failed rc:'%d', err:'%s'", res.first, res.second.c_str());
|
||||
goto cleanup;
|
||||
} else {
|
||||
ok(
|
||||
inv_input.msg == res.second,
|
||||
"Expected failure should match actual exp:'%s', act:'%s'",
|
||||
inv_input.msg.c_str(), res.second.c_str()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tests MySQL/Admin hashes compatibility
|
||||
{
|
||||
vector<user_def_t> users {};
|
||||
|
||||
for (size_t i = 0; i < USER_GEN_COUNT/2; i++) {
|
||||
const string name { "rndextuser" + std::to_string(i) };
|
||||
pair<int,user_def_t> user_def {
|
||||
create_mysql_user_rnd_creds(mysql, name, "mysql_native_password")
|
||||
};
|
||||
|
||||
if (user_def.first) {
|
||||
diag("User creation failed user:'%s'", name.c_str());
|
||||
goto cleanup;
|
||||
} else {
|
||||
users.push_back(user_def.second);
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t i = 50; i < USER_GEN_COUNT; i++) {
|
||||
const string name { "rndextuser" + std::to_string(i) };
|
||||
pair<int,user_def_t> user_def {
|
||||
create_mysql_user_rnd_creds(mysql, name, "caching_sha2_password")
|
||||
};
|
||||
|
||||
if (user_def.first) {
|
||||
diag("User creation failed user:'%s'", name.c_str());
|
||||
goto cleanup;
|
||||
} else {
|
||||
users.push_back(user_def.second);
|
||||
}
|
||||
}
|
||||
|
||||
for (const user_def_t& def : users) {
|
||||
test_pass_match(admin, def);
|
||||
}
|
||||
}
|
||||
|
||||
// Tests correctness of randomly generated hashes
|
||||
{
|
||||
std::srand(static_cast<unsigned int>(std::time(nullptr)));
|
||||
|
||||
// EXTRA: Two extra correctness tests; forcing randomness
|
||||
test_pass_gen(admin, "mysql_native_password", "randpass0", "");
|
||||
test_pass_gen(admin, "caching_sha2_password", "randpass0", "00000000000000000000");
|
||||
|
||||
for (size_t i = 0; i < PASS_GEN_COUNT; i++) {
|
||||
const uint32_t pass_len = rand() % 150;
|
||||
const string pass { random_string(pass_len) };
|
||||
|
||||
test_pass_gen(admin, "mysql_native_password", pass, "");
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < PASS_GEN_COUNT; i++) {
|
||||
const uint32_t pass_len = rand() % 150;
|
||||
const uint32_t salt_len = rand() % 20;
|
||||
const string pass { random_string(pass_len) };
|
||||
const string salt { random_string(pass_len) };
|
||||
|
||||
test_pass_gen(admin, "caching_sha2_password", pass, salt);
|
||||
}
|
||||
}
|
||||
|
||||
cleanup:
|
||||
|
||||
mysql_close(mysql);
|
||||
mysql_close(admin);
|
||||
|
||||
return exit_status();
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
TAP_MYSQLUSERNAME=root
|
||||
TAP_MYSQLPASSWORD=root
|
||||
TAP_MYSQLPORT=14806
|
||||
Loading…
Reference in new issue