The Connector-C was changed to always return only the client's charset, not the actual charset that the connection ends up using. To cope with this, the code has to use SQL to join the default character set name to the default collation for it which can be used to extract the numeric ID of the charset.
464 lines
11 KiB
C++
464 lines
11 KiB
C++
/*
|
|
* Copyright (c) 2016 MariaDB Corporation Ab
|
|
*
|
|
* Use of this software is governed by the Business Source License included
|
|
* in the LICENSE.TXT file and at www.mariadb.com/bsl11.
|
|
*
|
|
* Change Date: 2024-01-15
|
|
*
|
|
* On the date above, in accordance with the Business Source License, use
|
|
* of this software will be governed by version 2 or later of the General
|
|
* Public License.
|
|
*/
|
|
|
|
|
|
/**
|
|
* @file mysql_utils.c - Binary MySQL data processing utilities
|
|
*
|
|
* This file contains functions that are used when processing binary format
|
|
* information. The MySQL protocol uses the binary format in result sets and
|
|
* row based replication.
|
|
*/
|
|
|
|
#include <maxscale/mysql_utils.h>
|
|
|
|
#include <string.h>
|
|
#include <strings.h>
|
|
#include <stdbool.h>
|
|
#include <errmsg.h>
|
|
#include <thread>
|
|
#include <chrono>
|
|
|
|
#include <maxscale/alloc.h>
|
|
#include <maxscale/config.h>
|
|
#include <maxscale/log.h>
|
|
#include <maxbase/atomic.hh>
|
|
|
|
namespace
|
|
{
|
|
|
|
struct THIS_UNIT
|
|
{
|
|
bool log_statements; // Should all statements sent to server be logged?
|
|
};
|
|
|
|
static THIS_UNIT this_unit =
|
|
{
|
|
false
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @brief Calculate the length of a length-encoded integer in bytes
|
|
*
|
|
* @param ptr Start of the length encoded value
|
|
* @return Number of bytes before the actual value
|
|
*/
|
|
size_t mxs_leint_bytes(const uint8_t* ptr)
|
|
{
|
|
uint8_t val = *ptr;
|
|
if (val < 0xfb)
|
|
{
|
|
return 1;
|
|
}
|
|
else if (val == 0xfc)
|
|
{
|
|
return 3;
|
|
}
|
|
else if (val == 0xfd)
|
|
{
|
|
return 4;
|
|
}
|
|
else
|
|
{
|
|
return 9;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Converts a length-encoded integer to @c uint64_t
|
|
*
|
|
* @see https://dev.mysql.com/doc/internals/en/integer.html
|
|
* @param c Pointer to the first byte of a length-encoded integer
|
|
* @return The value converted to a standard unsigned integer
|
|
*/
|
|
uint64_t mxs_leint_value(const uint8_t* c)
|
|
{
|
|
uint64_t sz = 0;
|
|
|
|
if (*c < 0xfb)
|
|
{
|
|
sz = *c;
|
|
}
|
|
else if (*c == 0xfc)
|
|
{
|
|
memcpy(&sz, c + 1, 2);
|
|
}
|
|
else if (*c == 0xfd)
|
|
{
|
|
memcpy(&sz, c + 1, 3);
|
|
}
|
|
else if (*c == 0xfe)
|
|
{
|
|
memcpy(&sz, c + 1, 8);
|
|
}
|
|
else
|
|
{
|
|
mxb_assert(*c == 0xff);
|
|
MXS_ERROR("Unexpected length encoding '%x' encountered when reading "
|
|
"length-encoded integer.",
|
|
*c);
|
|
}
|
|
|
|
return sz;
|
|
}
|
|
|
|
/**
|
|
* Converts a length-encoded integer into a standard unsigned integer
|
|
* and advances the pointer to the next unrelated byte.
|
|
*
|
|
* @param c Pointer to the first byte of a length-encoded integer
|
|
*/
|
|
uint64_t mxs_leint_consume(uint8_t** c)
|
|
{
|
|
uint64_t rval = mxs_leint_value(*c);
|
|
*c += mxs_leint_bytes(*c);
|
|
return rval;
|
|
}
|
|
|
|
/**
|
|
* @brief Consume and duplicate a length-encoded string
|
|
*
|
|
* Converts a length-encoded string to a C string and advances the pointer to
|
|
* the first byte after the string. The caller is responsible for freeing
|
|
* the returned string.
|
|
* @param c Pointer to the first byte of a valid packet.
|
|
* @return The newly allocated string or NULL if memory allocation failed
|
|
*/
|
|
char* mxs_lestr_consume_dup(uint8_t** c)
|
|
{
|
|
uint64_t slen = mxs_leint_consume(c);
|
|
char* str = (char*)MXS_MALLOC((slen + 1) * sizeof(char));
|
|
|
|
if (str)
|
|
{
|
|
memcpy(str, *c, slen);
|
|
str[slen] = '\0';
|
|
*c += slen;
|
|
}
|
|
|
|
return str;
|
|
}
|
|
|
|
/**
|
|
* @brief Consume a length-encoded string
|
|
*
|
|
* Converts length-encoded strings to character strings and advanced
|
|
* the pointer to the next unrelated byte.
|
|
* @param c Pointer to the start of the length-encoded string
|
|
* @param size Pointer to a variable where the size of the string is stored
|
|
* @return Pointer to the start of the string
|
|
*/
|
|
char* mxs_lestr_consume(uint8_t** c, size_t* size)
|
|
{
|
|
uint64_t slen = mxs_leint_consume(c);
|
|
*size = slen;
|
|
char* start = (char*) *c;
|
|
*c += slen;
|
|
return start;
|
|
}
|
|
|
|
MYSQL* mxs_mysql_real_connect(MYSQL* con, SERVER* server, const char* user, const char* passwd)
|
|
{
|
|
SSL_LISTENER* listener = server->server_ssl;
|
|
|
|
if (listener)
|
|
{
|
|
mysql_ssl_set(con, listener->ssl_key, listener->ssl_cert, listener->ssl_ca_cert, NULL, NULL);
|
|
}
|
|
|
|
char yes = 1;
|
|
mysql_optionsv(con, MYSQL_OPT_RECONNECT, &yes);
|
|
mysql_optionsv(con, MYSQL_INIT_COMMAND, "SET SQL_MODE=''");
|
|
|
|
MXS_CONFIG* config = config_get_global_options();
|
|
|
|
if (config->local_address)
|
|
{
|
|
if (mysql_optionsv(con, MYSQL_OPT_BIND, config->local_address) != 0)
|
|
{
|
|
MXS_ERROR("'local_address' specified in configuration file, but could not "
|
|
"configure MYSQL handle. MaxScale will try to connect using default "
|
|
"address.");
|
|
}
|
|
}
|
|
|
|
MYSQL* mysql = mysql_real_connect(con, server->address, user, passwd, NULL, server->port, NULL, 0);
|
|
auto extra_port = mxb::atomic::load(&server->extra_port, mxb::atomic::RELAXED);
|
|
|
|
if (!mysql && extra_port)
|
|
{
|
|
mysql = mysql_real_connect(con, server->address, user, passwd, NULL, extra_port, NULL, 0);
|
|
MXS_WARNING("Could not connect with normal port to server '%s', using extra_port", server->name);
|
|
}
|
|
|
|
if (mysql)
|
|
{
|
|
/** Copy the server charset */
|
|
server->charset = mxs_mysql_get_character_set(mysql);
|
|
|
|
if (listener && mysql_get_ssl_cipher(con) == NULL)
|
|
{
|
|
if (server->warn_ssl_not_enabled)
|
|
{
|
|
server->warn_ssl_not_enabled = false;
|
|
MXS_ERROR("An encrypted connection to '%s' could not be created, "
|
|
"ensure that TLS is enabled on the target server.",
|
|
server->name);
|
|
}
|
|
// Don't close the connection as it is closed elsewhere, just set to NULL
|
|
mysql = NULL;
|
|
}
|
|
}
|
|
|
|
return mysql;
|
|
}
|
|
|
|
bool mxs_mysql_is_net_error(unsigned int errcode)
|
|
{
|
|
switch (errcode)
|
|
{
|
|
case CR_SOCKET_CREATE_ERROR:
|
|
case CR_CONNECTION_ERROR:
|
|
case CR_CONN_HOST_ERROR:
|
|
case CR_IPSOCK_ERROR:
|
|
case CR_SERVER_GONE_ERROR:
|
|
case CR_TCP_CONNECTION:
|
|
case CR_SERVER_LOST:
|
|
return true;
|
|
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
int mxs_mysql_query_ex(MYSQL* conn, const char* query, int query_retries, time_t query_retry_timeout)
|
|
{
|
|
time_t start = time(NULL);
|
|
int rc = mysql_query(conn, query);
|
|
|
|
for (int n = 0; rc != 0 && n < query_retries
|
|
&& mxs_mysql_is_net_error(mysql_errno(conn))
|
|
&& time(NULL) - start < query_retry_timeout; n++)
|
|
{
|
|
if (n > 0)
|
|
{
|
|
// The first reconnection didn't work, wait for one second before attempting again. This
|
|
// should reduce the likelihood of transient problems causing state changes due to too many
|
|
// reconnection attemps in a short period of time.
|
|
std::this_thread::sleep_for(std::chrono::seconds(1));
|
|
}
|
|
|
|
rc = mysql_query(conn, query);
|
|
}
|
|
|
|
if (this_unit.log_statements)
|
|
{
|
|
const char* host = "0.0.0.0";
|
|
unsigned int port = 0;
|
|
MXB_AT_DEBUG(int rc1 = ) mariadb_get_info(conn, MARIADB_CONNECTION_HOST, &host);
|
|
MXB_AT_DEBUG(int rc2 = ) mariadb_get_info(conn, MARIADB_CONNECTION_PORT, &port);
|
|
mxb_assert(!rc1 && !rc2);
|
|
MXS_NOTICE("SQL([%s]:%u): %d, \"%s\"", host, port, rc, query);
|
|
}
|
|
|
|
return rc;
|
|
}
|
|
|
|
int mxs_mysql_query(MYSQL* conn, const char* query)
|
|
{
|
|
MXS_CONFIG* cnf = config_get_global_options();
|
|
return mxs_mysql_query_ex(conn, query, cnf->query_retries, cnf->query_retry_timeout);
|
|
}
|
|
|
|
const char* mxs_mysql_get_value(MYSQL_RES* result, MYSQL_ROW row, const char* key)
|
|
{
|
|
MYSQL_FIELD* f = mysql_fetch_fields(result);
|
|
int nfields = mysql_num_fields(result);
|
|
|
|
for (int i = 0; i < nfields; i++)
|
|
{
|
|
if (strcasecmp(f[i].name, key) == 0)
|
|
{
|
|
return row[i];
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
bool mxs_mysql_trim_quotes(char* s)
|
|
{
|
|
bool dequoted = true;
|
|
|
|
char* i = s;
|
|
char* end = s + strlen(s);
|
|
|
|
// Remove space from the beginning
|
|
while (*i && isspace(*i))
|
|
{
|
|
++i;
|
|
}
|
|
|
|
if (*i)
|
|
{
|
|
// Remove space from the end
|
|
while (isspace(*(end - 1)))
|
|
{
|
|
*(end - 1) = 0;
|
|
--end;
|
|
}
|
|
|
|
mxb_assert(end > i);
|
|
|
|
char quote;
|
|
|
|
switch (*i)
|
|
{
|
|
case '\'':
|
|
case '"':
|
|
case '`':
|
|
quote = *i;
|
|
++i;
|
|
break;
|
|
|
|
default:
|
|
quote = 0;
|
|
}
|
|
|
|
if (quote)
|
|
{
|
|
--end;
|
|
|
|
if (*end == quote)
|
|
{
|
|
*end = 0;
|
|
|
|
memmove(s, i, end - i + 1);
|
|
}
|
|
else
|
|
{
|
|
dequoted = false;
|
|
}
|
|
}
|
|
else if (i != s)
|
|
{
|
|
memmove(s, i, end - i + 1);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
*s = 0;
|
|
}
|
|
|
|
return dequoted;
|
|
}
|
|
|
|
|
|
mxs_mysql_name_kind_t mxs_mysql_name_to_pcre(char* pcre,
|
|
const char* mysql,
|
|
mxs_pcre_quote_approach_t approach)
|
|
{
|
|
mxs_mysql_name_kind_t rv = MXS_MYSQL_NAME_WITHOUT_WILDCARD;
|
|
|
|
while (*mysql)
|
|
{
|
|
switch (*mysql)
|
|
{
|
|
case '%':
|
|
if (approach == MXS_PCRE_QUOTE_WILDCARD)
|
|
{
|
|
*pcre = '.';
|
|
pcre++;
|
|
*pcre = '*';
|
|
}
|
|
rv = MXS_MYSQL_NAME_WITH_WILDCARD;
|
|
break;
|
|
|
|
case '\'':
|
|
case '^':
|
|
case '.':
|
|
case '$':
|
|
case '|':
|
|
case '(':
|
|
case ')':
|
|
case '[':
|
|
case ']':
|
|
case '*':
|
|
case '+':
|
|
case '?':
|
|
case '{':
|
|
case '}':
|
|
*pcre++ = '\\';
|
|
|
|
// Flowthrough
|
|
default:
|
|
*pcre = *mysql;
|
|
}
|
|
|
|
++pcre;
|
|
++mysql;
|
|
}
|
|
|
|
*pcre = 0;
|
|
|
|
return rv;
|
|
}
|
|
|
|
void mxs_mysql_update_server_version(MYSQL* mysql, SERVER* server)
|
|
{
|
|
// This function should only be called for a live connection.
|
|
const char* version_string = mysql_get_server_info(mysql);
|
|
unsigned long version_num = mysql_get_server_version(mysql);
|
|
mxb_assert(version_string != NULL && version_num != 0);
|
|
server_set_version(server, version_string, version_num);
|
|
}
|
|
|
|
void mxs_mysql_set_log_statements(bool enable)
|
|
{
|
|
this_unit.log_statements = enable;
|
|
}
|
|
|
|
bool mxs_mysql_get_log_statements()
|
|
{
|
|
return this_unit.log_statements;
|
|
}
|
|
|
|
uint8_t mxs_mysql_get_character_set(MYSQL* mysql)
|
|
{
|
|
uint8_t charset = 8; // Default is latin1 with the ID 8
|
|
const char* CHARSET_QUERY =
|
|
"SELECT co.id FROM information_schema.collations AS co "
|
|
"JOIN information_schema.character_sets AS cs "
|
|
"ON (co.collation_name = cs.default_collate_name) "
|
|
"WHERE cs.character_set_name=@@global.character_set_server;";
|
|
|
|
if (mysql_query(mysql, CHARSET_QUERY) == 0)
|
|
{
|
|
if (auto res = mysql_use_result(mysql))
|
|
{
|
|
if (auto row = mysql_fetch_row(res))
|
|
{
|
|
if (row[0])
|
|
{
|
|
charset = atoi(row[0]);
|
|
}
|
|
}
|
|
|
|
mysql_free_result(res);
|
|
}
|
|
}
|
|
|
|
return charset;
|
|
}
|