430 lines
9.5 KiB
C++
430 lines
9.5 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: 2022-01-01
|
|
*
|
|
* 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 <maxscale/alloc.h>
|
|
#include <maxscale/config.h>
|
|
#include <maxscale/debug.h>
|
|
#include <maxscale/log_manager.h>
|
|
|
|
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
|
|
{
|
|
ss_dassert(*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);
|
|
|
|
if (mysql)
|
|
{
|
|
/** Copy the server charset */
|
|
MY_CHARSET_INFO cs_info;
|
|
mysql_get_character_set_info(mysql, &cs_info);
|
|
server->charset = cs_info.number;
|
|
|
|
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(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(MYSQL* conn, const char* query)
|
|
{
|
|
MXS_CONFIG* cnf = config_get_global_options();
|
|
time_t start = time(NULL);
|
|
int rc = mysql_query(conn, query);
|
|
|
|
for (int n = 0; rc != 0 && n < cnf->query_retries &&
|
|
mxs_mysql_is_net_error(mysql_errno(conn)) &&
|
|
time(NULL) - start < cnf->query_retry_timeout; n++)
|
|
{
|
|
rc = mysql_query(conn, query);
|
|
}
|
|
|
|
if (this_unit.log_statements)
|
|
{
|
|
const char* host;
|
|
if (mariadb_get_info(conn, MARIADB_CONNECTION_HOST, &host) != 0)
|
|
{
|
|
// No idea about the host, but let's use something that looks like
|
|
// an IP-address as a placeholder.
|
|
host = "0.0.0.0";
|
|
}
|
|
|
|
MXS_NOTICE("SQL(%s): %d, \"%s\"", host, rc, query);
|
|
}
|
|
|
|
return rc;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
ss_dassert(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_set_server_version(MYSQL* mysql, SERVER* server)
|
|
{
|
|
const char* version_string = mysql_get_server_info(mysql);
|
|
|
|
if (version_string)
|
|
{
|
|
unsigned long version = mysql_get_server_version(mysql);
|
|
|
|
server_set_version(server, version_string, version);
|
|
|
|
if (strcasestr(version_string, "mariadb") != NULL)
|
|
{
|
|
server->server_type = SERVER_TYPE_MARIADB;
|
|
}
|
|
else
|
|
{
|
|
server->server_type = SERVER_TYPE_MYSQL;
|
|
}
|
|
}
|
|
}
|
|
|
|
void mxs_mysql_set_log_statements(bool enable)
|
|
{
|
|
this_unit.log_statements = enable;
|
|
}
|
|
|
|
bool mxs_mysql_get_log_statements()
|
|
{
|
|
return this_unit.log_statements;
|
|
}
|