
When an unexpected response to a COM_CHANGE_USER is received, it is now processes and discarded instead of treated as an error. This will allow further analysis of the situation in addition to possibly solving some of the problems that the persistent connections have. Added extra info level logging to relevant parts of the code that deal with the COM_CHANGE_USER reply processing. This information should allow tracking of the response state for debugging purposes.
1575 lines
43 KiB
C
1575 lines
43 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: 2019-07-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.
|
|
*/
|
|
|
|
/*
|
|
* MySQL Protocol common routines for client to gateway and gateway to backend
|
|
*
|
|
* Revision History
|
|
* Date Who Description
|
|
* 17/06/2013 Massimiliano Pinto Common MySQL protocol routines
|
|
* 02/06/2013 Massimiliano Pinto MySQL connect asynchronous phases
|
|
* 04/09/2013 Massimiliano Pinto Added dcb NULL assert in mysql_send_custom_error
|
|
* 12/09/2013 Massimiliano Pinto Added checks in gw_decode_mysql_server_handshake and
|
|
* gw_read_backend_handshake
|
|
* 10/02/2014 Massimiliano Pinto Added MySQL Authentication with user@host
|
|
* 10/09/2014 Massimiliano Pinto Added MySQL Authentication option enabling localhost
|
|
* match with any host (wildcard %)
|
|
* Backend server configuration may differ so default is 0,
|
|
* don't match and an explicit
|
|
* localhost entry should be added for the selected user
|
|
* in the backends.
|
|
* Setting to 1 allow localhost (127.0.0.1 or socket) to
|
|
* match the any host grant via
|
|
* user@%
|
|
* 29/09/2014 Massimiliano Pinto Added Mysql user@host authentication with wildcard in IPv4 hosts:
|
|
* x.y.z.%, x.y.%.%, x.%.%.%
|
|
* 03/10/2014 Massimiliano Pinto Added netmask for wildcard in IPv4 hosts.
|
|
* 24/10/2014 Massimiliano Pinto Added Mysql user@host @db authentication support
|
|
* 10/11/2014 Massimiliano Pinto Charset at connect is passed to backend during authentication
|
|
* 07/07/2015 Martin Brampton Fix problem recognising null password
|
|
* 07/02/2016 Martin Brampton Remove authentication functions to mysql_auth.c
|
|
* 31/05/2016 Martin Brampton Add mysql_create_standard_error function
|
|
*
|
|
*/
|
|
|
|
#include <maxscale/utils.h>
|
|
#include <maxscale/protocol/mysql.h>
|
|
#include <maxscale/alloc.h>
|
|
#include <maxscale/log_manager.h>
|
|
#include <netinet/tcp.h>
|
|
#include <maxscale/modutil.h>
|
|
|
|
uint8_t null_client_sha1[MYSQL_SCRAMBLE_LEN] = "";
|
|
|
|
static server_command_t* server_command_init(server_command_t* srvcmd, mysql_server_cmd_t cmd);
|
|
|
|
/**
|
|
* @brief Allocate a new MySQL_session
|
|
* @return New MySQL_session or NULL if memory allocation failed
|
|
*/
|
|
MYSQL_session* mysql_session_alloc()
|
|
{
|
|
MYSQL_session *ses = MXS_CALLOC(1, sizeof(MYSQL_session));
|
|
|
|
if (ses)
|
|
{
|
|
#ifdef SS_DEBUG
|
|
ses->myses_chk_top = CHK_NUM_MYSQLSES;
|
|
ses->myses_chk_tail = CHK_NUM_MYSQLSES;
|
|
#endif
|
|
}
|
|
|
|
return ses;
|
|
}
|
|
|
|
/**
|
|
* Creates MySQL protocol structure
|
|
*
|
|
* @param dcb * Must be non-NULL.
|
|
* @param fd
|
|
*
|
|
* @return
|
|
*
|
|
*
|
|
* @details Protocol structure does not have fd because dcb is not
|
|
* connected yet.
|
|
*
|
|
*/
|
|
MySQLProtocol* mysql_protocol_init(DCB* dcb, int fd)
|
|
{
|
|
MySQLProtocol* p;
|
|
|
|
p = (MySQLProtocol *) MXS_CALLOC(1, sizeof(MySQLProtocol));
|
|
ss_dassert(p != NULL);
|
|
|
|
if (p == NULL)
|
|
{
|
|
goto return_p;
|
|
}
|
|
p->protocol_state = MYSQL_PROTOCOL_ALLOC;
|
|
p->protocol_auth_state = MXS_AUTH_STATE_INIT;
|
|
p->current_command = MYSQL_COM_UNDEFINED;
|
|
p->protocol_command.scom_cmd = MYSQL_COM_UNDEFINED;
|
|
p->protocol_command.scom_nresponse_packets = 0;
|
|
p->protocol_command.scom_nbytes_to_read = 0;
|
|
p->stored_query = NULL;
|
|
p->extra_capabilities = 0;
|
|
p->ignore_reply = false;
|
|
#if defined(SS_DEBUG)
|
|
p->protocol_chk_top = CHK_NUM_PROTOCOL;
|
|
p->protocol_chk_tail = CHK_NUM_PROTOCOL;
|
|
#endif
|
|
/*< Assign fd with protocol */
|
|
p->fd = fd;
|
|
p->owner_dcb = dcb;
|
|
p->protocol_state = MYSQL_PROTOCOL_ACTIVE;
|
|
CHK_PROTOCOL(p);
|
|
return_p:
|
|
return p;
|
|
}
|
|
|
|
/**
|
|
* mysql_protocol_done
|
|
*
|
|
* free protocol allocations.
|
|
*
|
|
* @param dcb owner DCB
|
|
*
|
|
*/
|
|
void mysql_protocol_done(DCB* dcb)
|
|
{
|
|
MySQLProtocol* p;
|
|
server_command_t* scmd;
|
|
server_command_t* scmd2;
|
|
|
|
p = (MySQLProtocol *)dcb->protocol;
|
|
|
|
if (p->protocol_state == MYSQL_PROTOCOL_ACTIVE)
|
|
{
|
|
scmd = p->protocol_cmd_history;
|
|
|
|
while (scmd != NULL)
|
|
{
|
|
scmd2 = scmd->scom_next;
|
|
MXS_FREE(scmd);
|
|
scmd = scmd2;
|
|
}
|
|
|
|
gwbuf_free(p->stored_query);
|
|
|
|
p->protocol_state = MYSQL_PROTOCOL_DONE;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return a string representation of a MySQL protocol state.
|
|
*
|
|
* @param state The protocol state
|
|
* @return String representation of the state
|
|
*
|
|
*/
|
|
const char* gw_mysql_protocol_state2string (int state)
|
|
{
|
|
switch (state)
|
|
{
|
|
case MXS_AUTH_STATE_INIT:
|
|
return "Authentication initialized";
|
|
case MXS_AUTH_STATE_PENDING_CONNECT:
|
|
return "Network connection pending";
|
|
case MXS_AUTH_STATE_CONNECTED:
|
|
return "Network connection created";
|
|
case MXS_AUTH_STATE_MESSAGE_READ:
|
|
return "Read server handshake";
|
|
case MXS_AUTH_STATE_RESPONSE_SENT:
|
|
return "Response to handshake sent";
|
|
case MXS_AUTH_STATE_FAILED:
|
|
return "Authentication failed";
|
|
case MXS_AUTH_STATE_COMPLETE:
|
|
return "Authentication is complete.";
|
|
default:
|
|
return "MySQL (unknown protocol state)";
|
|
}
|
|
}
|
|
|
|
GWBUF* mysql_create_com_quit(GWBUF* bufparam,
|
|
int packet_number)
|
|
{
|
|
uint8_t* data;
|
|
GWBUF* buf;
|
|
|
|
if (bufparam == NULL)
|
|
{
|
|
buf = gwbuf_alloc(COM_QUIT_PACKET_SIZE);
|
|
}
|
|
else
|
|
{
|
|
buf = bufparam;
|
|
}
|
|
|
|
if (buf == NULL)
|
|
{
|
|
return 0;
|
|
}
|
|
ss_dassert(GWBUF_LENGTH(buf) == COM_QUIT_PACKET_SIZE);
|
|
|
|
data = GWBUF_DATA(buf);
|
|
|
|
*data++ = 0x1;
|
|
*data++ = 0x0;
|
|
*data++ = 0x0;
|
|
*data++ = packet_number;
|
|
*data = 0x1;
|
|
|
|
return buf;
|
|
}
|
|
|
|
int mysql_send_com_quit(DCB* dcb,
|
|
int packet_number,
|
|
GWBUF* bufparam)
|
|
{
|
|
GWBUF *buf;
|
|
int nbytes = 0;
|
|
|
|
CHK_DCB(dcb);
|
|
ss_dassert(packet_number <= 255);
|
|
|
|
if (dcb == NULL || dcb->state == DCB_STATE_ZOMBIE)
|
|
{
|
|
return 0;
|
|
}
|
|
if (bufparam == NULL)
|
|
{
|
|
buf = mysql_create_com_quit(NULL, packet_number);
|
|
}
|
|
else
|
|
{
|
|
buf = bufparam;
|
|
}
|
|
|
|
if (buf == NULL)
|
|
{
|
|
return 0;
|
|
}
|
|
nbytes = dcb->func.write(dcb, buf);
|
|
|
|
return nbytes;
|
|
}
|
|
|
|
|
|
GWBUF* mysql_create_custom_error(int packet_number,
|
|
int affected_rows,
|
|
const char* msg)
|
|
{
|
|
uint8_t* outbuf = NULL;
|
|
uint32_t mysql_payload_size = 0;
|
|
uint8_t mysql_packet_header[4];
|
|
uint8_t* mysql_payload = NULL;
|
|
uint8_t field_count = 0;
|
|
uint8_t mysql_err[2];
|
|
uint8_t mysql_statemsg[6];
|
|
const char* mysql_error_msg = NULL;
|
|
const char* mysql_state = NULL;
|
|
|
|
GWBUF* errbuf = NULL;
|
|
|
|
mysql_error_msg = "An errorr occurred ...";
|
|
mysql_state = "HY000";
|
|
|
|
field_count = 0xff;
|
|
gw_mysql_set_byte2(mysql_err, /* mysql_errno */ 2003);
|
|
mysql_statemsg[0] = '#';
|
|
memcpy(mysql_statemsg + 1, mysql_state, 5);
|
|
|
|
if (msg != NULL)
|
|
{
|
|
mysql_error_msg = msg;
|
|
}
|
|
|
|
mysql_payload_size =
|
|
sizeof(field_count) +
|
|
sizeof(mysql_err) +
|
|
sizeof(mysql_statemsg) +
|
|
strlen(mysql_error_msg);
|
|
|
|
/** allocate memory for packet header + payload */
|
|
errbuf = gwbuf_alloc(sizeof(mysql_packet_header) + mysql_payload_size);
|
|
ss_dassert(errbuf != NULL);
|
|
|
|
if (errbuf == NULL)
|
|
{
|
|
return 0;
|
|
}
|
|
outbuf = GWBUF_DATA(errbuf);
|
|
|
|
/** write packet header and packet number */
|
|
gw_mysql_set_byte3(mysql_packet_header, mysql_payload_size);
|
|
mysql_packet_header[3] = packet_number;
|
|
|
|
/** write header */
|
|
memcpy(outbuf, mysql_packet_header, sizeof(mysql_packet_header));
|
|
|
|
mysql_payload = outbuf + sizeof(mysql_packet_header);
|
|
|
|
/** write field */
|
|
memcpy(mysql_payload, &field_count, sizeof(field_count));
|
|
mysql_payload = mysql_payload + sizeof(field_count);
|
|
|
|
/** write errno */
|
|
memcpy(mysql_payload, mysql_err, sizeof(mysql_err));
|
|
mysql_payload = mysql_payload + sizeof(mysql_err);
|
|
|
|
/** write sqlstate */
|
|
memcpy(mysql_payload, mysql_statemsg, sizeof(mysql_statemsg));
|
|
mysql_payload = mysql_payload + sizeof(mysql_statemsg);
|
|
|
|
/** write error message */
|
|
memcpy(mysql_payload, mysql_error_msg, strlen(mysql_error_msg));
|
|
|
|
return errbuf;
|
|
}
|
|
|
|
/**
|
|
* @brief Create a standard MariaDB error message, emulating real server
|
|
*
|
|
* Supports the sending to a client of a standard database error, for
|
|
* circumstances where the error is generated within MaxScale but should
|
|
* appear like a backend server error. First introduced to support connection
|
|
* throttling, to send "Too many connections" error.
|
|
*
|
|
* @param packet_number Packet number for header
|
|
* @param error_number Standard error number as for MariaDB
|
|
* @param error_message Text message to be included
|
|
* @return GWBUF A buffer containing the error message, ready to send
|
|
*/
|
|
GWBUF *
|
|
mysql_create_standard_error(int packet_number,
|
|
int error_number,
|
|
const char *error_message)
|
|
{
|
|
uint8_t *outbuf = NULL;
|
|
uint32_t mysql_payload_size = 0;
|
|
uint8_t mysql_packet_header[4];
|
|
uint8_t mysql_error_number[2];
|
|
uint8_t *mysql_handshake_payload = NULL;
|
|
GWBUF *buf;
|
|
|
|
mysql_payload_size = 1 + sizeof(mysql_error_number) + strlen(error_message);
|
|
|
|
// allocate memory for packet header + payload
|
|
if ((buf = gwbuf_alloc(sizeof(mysql_packet_header) + mysql_payload_size)) == NULL)
|
|
{
|
|
return NULL;
|
|
}
|
|
outbuf = GWBUF_DATA(buf);
|
|
|
|
// write packet header with mysql_payload_size
|
|
gw_mysql_set_byte3(mysql_packet_header, mysql_payload_size);
|
|
|
|
// write packet number, now is 0
|
|
mysql_packet_header[3] = 0;
|
|
memcpy(outbuf, mysql_packet_header, sizeof(mysql_packet_header));
|
|
|
|
// current buffer pointer
|
|
mysql_handshake_payload = outbuf + sizeof(mysql_packet_header);
|
|
|
|
// write 0xff which is the error indicator
|
|
*mysql_handshake_payload = 0xff;
|
|
mysql_handshake_payload++;
|
|
|
|
// write error number
|
|
gw_mysql_set_byte2(mysql_handshake_payload, error_number);
|
|
mysql_handshake_payload += 2;
|
|
|
|
// write error message
|
|
memcpy(mysql_handshake_payload, error_message, strlen(error_message));
|
|
|
|
return buf;
|
|
}
|
|
|
|
/**
|
|
* @brief Send a standard MariaDB error message, emulating real server
|
|
*
|
|
* Supports the sending to a client of a standard database error, for
|
|
* circumstances where the error is generated within MaxScale but should
|
|
* appear like a backend server error. First introduced to support connection
|
|
* throttling, to send "Too many connections" error.
|
|
*
|
|
* @param dcb The client DCB to which error is to be sent
|
|
* @param packet_number Packet number for header
|
|
* @param error_number Standard error number as for MariaDB
|
|
* @param error_message Text message to be included
|
|
* @return 0 on failure, 1 on success
|
|
*/
|
|
int
|
|
mysql_send_standard_error(DCB *dcb,
|
|
int packet_number,
|
|
int error_number,
|
|
const char *error_message)
|
|
{
|
|
GWBUF *buf;
|
|
buf = mysql_create_standard_error(packet_number, error_number, error_message);
|
|
return buf ? dcb->func.write(dcb, buf) : 0;
|
|
}
|
|
|
|
/**
|
|
* mysql_send_custom_error
|
|
*
|
|
* Send a MySQL protocol Generic ERR message, to the dcb
|
|
* Note the errno and state are still fixed now
|
|
*
|
|
* @param dcb Owner_Dcb Control Block for the connection to which the OK is sent
|
|
* @param packet_number
|
|
* @param in_affected_rows
|
|
* @param mysql_message
|
|
* @return 1 Non-zero if data was sent
|
|
*
|
|
*/
|
|
int mysql_send_custom_error(DCB *dcb,
|
|
int packet_number,
|
|
int in_affected_rows,
|
|
const char *mysql_message)
|
|
{
|
|
GWBUF* buf;
|
|
|
|
buf = mysql_create_custom_error(packet_number, in_affected_rows, mysql_message);
|
|
|
|
return dcb->func.write(dcb, buf);
|
|
}
|
|
|
|
/**
|
|
* mysql_send_auth_error
|
|
*
|
|
* Send a MySQL protocol ERR message, for gateway authentication error to the dcb
|
|
*
|
|
* @param dcb descriptor Control Block for the connection to which the OK is sent
|
|
* @param packet_number
|
|
* @param in_affected_rows
|
|
* @param mysql_message
|
|
* @return packet length
|
|
*
|
|
*/
|
|
int mysql_send_auth_error(DCB *dcb,
|
|
int packet_number,
|
|
int in_affected_rows,
|
|
const char *mysql_message)
|
|
{
|
|
uint8_t *outbuf = NULL;
|
|
uint32_t mysql_payload_size = 0;
|
|
uint8_t mysql_packet_header[4];
|
|
uint8_t *mysql_payload = NULL;
|
|
uint8_t field_count = 0;
|
|
uint8_t mysql_err[2];
|
|
uint8_t mysql_statemsg[6];
|
|
const char *mysql_error_msg = NULL;
|
|
const char *mysql_state = NULL;
|
|
|
|
GWBUF *buf;
|
|
|
|
if (dcb->state != DCB_STATE_POLLING)
|
|
{
|
|
MXS_DEBUG("%lu [mysql_send_auth_error] dcb %p is in a state %s, "
|
|
"and it is not in epoll set anymore. Skip error sending.",
|
|
pthread_self(),
|
|
dcb,
|
|
STRDCBSTATE(dcb->state));
|
|
return 0;
|
|
}
|
|
mysql_error_msg = "Access denied!";
|
|
mysql_state = "28000";
|
|
|
|
field_count = 0xff;
|
|
gw_mysql_set_byte2(mysql_err, /*mysql_errno */ 1045);
|
|
mysql_statemsg[0] = '#';
|
|
memcpy(mysql_statemsg + 1, mysql_state, 5);
|
|
|
|
if (mysql_message != NULL)
|
|
{
|
|
mysql_error_msg = mysql_message;
|
|
}
|
|
|
|
mysql_payload_size =
|
|
sizeof(field_count) + sizeof(mysql_err) + sizeof(mysql_statemsg) + strlen(mysql_error_msg);
|
|
|
|
// allocate memory for packet header + payload
|
|
if ((buf = gwbuf_alloc(sizeof(mysql_packet_header) + mysql_payload_size)) == NULL)
|
|
{
|
|
return 0;
|
|
}
|
|
outbuf = GWBUF_DATA(buf);
|
|
|
|
// write packet header with packet number
|
|
gw_mysql_set_byte3(mysql_packet_header, mysql_payload_size);
|
|
mysql_packet_header[3] = packet_number;
|
|
|
|
// write header
|
|
memcpy(outbuf, mysql_packet_header, sizeof(mysql_packet_header));
|
|
|
|
mysql_payload = outbuf + sizeof(mysql_packet_header);
|
|
|
|
// write field
|
|
memcpy(mysql_payload, &field_count, sizeof(field_count));
|
|
mysql_payload = mysql_payload + sizeof(field_count);
|
|
|
|
// write errno
|
|
memcpy(mysql_payload, mysql_err, sizeof(mysql_err));
|
|
mysql_payload = mysql_payload + sizeof(mysql_err);
|
|
|
|
// write sqlstate
|
|
memcpy(mysql_payload, mysql_statemsg, sizeof(mysql_statemsg));
|
|
mysql_payload = mysql_payload + sizeof(mysql_statemsg);
|
|
|
|
// write err messg
|
|
memcpy(mysql_payload, mysql_error_msg, strlen(mysql_error_msg));
|
|
|
|
// writing data in the Client buffer queue
|
|
dcb->func.write(dcb, buf);
|
|
|
|
return sizeof(mysql_packet_header) + mysql_payload_size;
|
|
}
|
|
|
|
|
|
/**
|
|
* Buffer contains at least one of the following:
|
|
* complete [complete] [partial] mysql packet
|
|
*
|
|
* @param p_readbuf Address of read buffer pointer
|
|
*
|
|
* @return pointer to gwbuf containing a complete packet or
|
|
* NULL if no complete packet was found.
|
|
*/
|
|
GWBUF* gw_MySQL_get_next_packet(GWBUF** p_readbuf)
|
|
{
|
|
GWBUF* packetbuf;
|
|
GWBUF* readbuf;
|
|
size_t buflen;
|
|
size_t packetlen;
|
|
size_t totalbuflen;
|
|
uint8_t* data;
|
|
size_t nbytes_copied = 0;
|
|
uint8_t* target;
|
|
|
|
readbuf = *p_readbuf;
|
|
|
|
if (readbuf == NULL)
|
|
{
|
|
packetbuf = NULL;
|
|
goto return_packetbuf;
|
|
}
|
|
CHK_GWBUF(readbuf);
|
|
|
|
if (GWBUF_EMPTY(readbuf))
|
|
{
|
|
packetbuf = NULL;
|
|
goto return_packetbuf;
|
|
}
|
|
totalbuflen = gwbuf_length(readbuf);
|
|
data = (uint8_t *)GWBUF_DATA((readbuf));
|
|
packetlen = MYSQL_GET_PAYLOAD_LEN(data) + 4;
|
|
|
|
/** packet is incomplete */
|
|
if (packetlen > totalbuflen)
|
|
{
|
|
packetbuf = NULL;
|
|
goto return_packetbuf;
|
|
}
|
|
|
|
packetbuf = gwbuf_alloc(packetlen);
|
|
target = GWBUF_DATA(packetbuf);
|
|
packetbuf->gwbuf_type = readbuf->gwbuf_type; /*< Copy the type too */
|
|
/**
|
|
* Copy first MySQL packet to packetbuf and leave posible other
|
|
* packets to read buffer.
|
|
*/
|
|
while (nbytes_copied < packetlen && totalbuflen > 0)
|
|
{
|
|
uint8_t* src = GWBUF_DATA((*p_readbuf));
|
|
size_t bytestocopy;
|
|
|
|
buflen = GWBUF_LENGTH((*p_readbuf));
|
|
bytestocopy = buflen < (packetlen - nbytes_copied) ? buflen : packetlen - nbytes_copied;
|
|
|
|
memcpy(target + nbytes_copied, src, bytestocopy);
|
|
*p_readbuf = gwbuf_consume((*p_readbuf), bytestocopy);
|
|
totalbuflen = gwbuf_length((*p_readbuf));
|
|
nbytes_copied += bytestocopy;
|
|
}
|
|
ss_dassert(buflen == 0 || nbytes_copied == packetlen);
|
|
|
|
return_packetbuf:
|
|
return packetbuf;
|
|
}
|
|
|
|
/**
|
|
* Move <npackets> from buffer pointed to by <*p_readbuf>.
|
|
* Appears to be unused 11 May 2016 (Martin)
|
|
*/
|
|
GWBUF* gw_MySQL_get_packets(GWBUF** p_srcbuf,
|
|
int* npackets)
|
|
{
|
|
GWBUF* packetbuf;
|
|
GWBUF* targetbuf = NULL;
|
|
|
|
while (*npackets > 0 && (packetbuf = gw_MySQL_get_next_packet(p_srcbuf)) != NULL)
|
|
{
|
|
targetbuf = gwbuf_append(targetbuf, packetbuf);
|
|
*npackets -= 1;
|
|
}
|
|
ss_dassert(*npackets < 128);
|
|
ss_dassert(*npackets >= 0);
|
|
|
|
return targetbuf;
|
|
}
|
|
|
|
|
|
static server_command_t* server_command_init(server_command_t* srvcmd,
|
|
mysql_server_cmd_t cmd)
|
|
{
|
|
server_command_t* c;
|
|
|
|
if (srvcmd != NULL)
|
|
{
|
|
c = srvcmd;
|
|
}
|
|
else
|
|
{
|
|
c = (server_command_t *)MXS_MALLOC(sizeof(server_command_t));
|
|
}
|
|
if (c != NULL)
|
|
{
|
|
c->scom_cmd = cmd;
|
|
c->scom_nresponse_packets = -1;
|
|
c->scom_nbytes_to_read = 0;
|
|
c->scom_next = NULL;
|
|
}
|
|
|
|
return c;
|
|
}
|
|
|
|
static server_command_t* server_command_copy(server_command_t* srvcmd)
|
|
{
|
|
server_command_t* c = (server_command_t *)MXS_MALLOC(sizeof(server_command_t));
|
|
if (c)
|
|
{
|
|
*c = *srvcmd;
|
|
}
|
|
|
|
return c;
|
|
}
|
|
|
|
#define MAX_CMD_HISTORY 10
|
|
|
|
void protocol_archive_srv_command(MySQLProtocol* p)
|
|
{
|
|
server_command_t* s1;
|
|
server_command_t* h1;
|
|
int len = 0;
|
|
|
|
CHK_PROTOCOL(p);
|
|
|
|
if (p->protocol_state != MYSQL_PROTOCOL_ACTIVE)
|
|
{
|
|
goto retblock;
|
|
}
|
|
|
|
s1 = &p->protocol_command;
|
|
#if defined(EXTRA_SS_DEBUG)
|
|
MXS_INFO("Move command %s from fd %d to command history.",
|
|
STRPACKETTYPE(s1->scom_cmd),
|
|
p->owner_dcb->fd);
|
|
#endif
|
|
/** Copy to history list */
|
|
if ((h1 = p->protocol_cmd_history) == NULL)
|
|
{
|
|
p->protocol_cmd_history = server_command_copy(s1);
|
|
}
|
|
else /*< scan and count history commands */
|
|
{
|
|
len = 1;
|
|
|
|
while (h1->scom_next != NULL)
|
|
{
|
|
h1 = h1->scom_next;
|
|
len += 1;
|
|
}
|
|
h1->scom_next = server_command_copy(s1);
|
|
}
|
|
|
|
/** Keep history limits, remove oldest */
|
|
if (len > MAX_CMD_HISTORY)
|
|
{
|
|
server_command_t* c = p->protocol_cmd_history;
|
|
p->protocol_cmd_history = p->protocol_cmd_history->scom_next;
|
|
MXS_FREE(c);
|
|
}
|
|
|
|
/** Remove from command list */
|
|
if (s1->scom_next == NULL)
|
|
{
|
|
p->protocol_command.scom_cmd = MYSQL_COM_UNDEFINED;
|
|
}
|
|
else
|
|
{
|
|
p->protocol_command = *(s1->scom_next);
|
|
MXS_FREE(s1->scom_next);
|
|
}
|
|
|
|
retblock:
|
|
CHK_PROTOCOL(p);
|
|
}
|
|
|
|
|
|
/**
|
|
* If router expects to get separate, complete statements, add MySQL command
|
|
* to MySQLProtocol structure. It is removed when response has arrived.
|
|
*/
|
|
void protocol_add_srv_command(MySQLProtocol* p,
|
|
mysql_server_cmd_t cmd)
|
|
{
|
|
#if defined(EXTRA_SS_DEBUG)
|
|
server_command_t* c;
|
|
#endif
|
|
|
|
if (p->protocol_state != MYSQL_PROTOCOL_ACTIVE)
|
|
{
|
|
return;
|
|
}
|
|
/** this is the only server command in protocol */
|
|
if (p->protocol_command.scom_cmd == MYSQL_COM_UNDEFINED)
|
|
{
|
|
/** write into structure */
|
|
server_command_init(&p->protocol_command, cmd);
|
|
}
|
|
else
|
|
{
|
|
/** add to the end of list */
|
|
p->protocol_command.scom_next = server_command_init(NULL, cmd);
|
|
}
|
|
#if defined(EXTRA_SS_DEBUG)
|
|
MXS_INFO("Added command %s to fd %d.",
|
|
STRPACKETTYPE(cmd),
|
|
p->owner_dcb->fd);
|
|
|
|
c = &p->protocol_command;
|
|
|
|
while (c != NULL && c->scom_cmd != MYSQL_COM_UNDEFINED)
|
|
{
|
|
MXS_INFO("fd %d : %d %s",
|
|
p->owner_dcb->fd,
|
|
c->scom_cmd,
|
|
STRPACKETTYPE(c->scom_cmd));
|
|
c = c->scom_next;
|
|
}
|
|
#endif
|
|
}
|
|
|
|
|
|
/**
|
|
* If router processes separate statements, every stmt has corresponding MySQL
|
|
* command stored in MySQLProtocol structure.
|
|
*
|
|
* Remove current (=oldest) command.
|
|
*/
|
|
void protocol_remove_srv_command(MySQLProtocol* p)
|
|
{
|
|
server_command_t* s;
|
|
|
|
s = &p->protocol_command;
|
|
#if defined(EXTRA_SS_DEBUG)
|
|
MXS_INFO("Removed command %s from fd %d.",
|
|
STRPACKETTYPE(s->scom_cmd),
|
|
p->owner_dcb->fd);
|
|
#endif
|
|
if (s->scom_next == NULL)
|
|
{
|
|
p->protocol_command.scom_cmd = MYSQL_COM_UNDEFINED;
|
|
}
|
|
else
|
|
{
|
|
p->protocol_command = *(s->scom_next);
|
|
MXS_FREE(s->scom_next);
|
|
}
|
|
}
|
|
|
|
mysql_server_cmd_t protocol_get_srv_command(MySQLProtocol* p,
|
|
bool removep)
|
|
{
|
|
mysql_server_cmd_t cmd;
|
|
|
|
cmd = p->protocol_command.scom_cmd;
|
|
|
|
if (removep)
|
|
{
|
|
protocol_remove_srv_command(p);
|
|
}
|
|
MXS_DEBUG("%lu [protocol_get_srv_command] Read command %s for fd %d.",
|
|
pthread_self(),
|
|
STRPACKETTYPE(cmd),
|
|
p->owner_dcb->fd);
|
|
return cmd;
|
|
}
|
|
|
|
|
|
/**
|
|
* Examine command type and the readbuf. Conclude response
|
|
* packet count from the command type or from the first packet
|
|
* content.
|
|
* Fails if read buffer doesn't include enough data to read the
|
|
* packet length.
|
|
*/
|
|
void init_response_status(GWBUF* buf,
|
|
mysql_server_cmd_t cmd,
|
|
int* npackets,
|
|
ssize_t* nbytes_left)
|
|
{
|
|
uint8_t readbuf[3];
|
|
int nparam = 0;
|
|
int nattr = 0;
|
|
uint8_t* data;
|
|
|
|
ss_dassert(gwbuf_length(buf) >= 3);
|
|
|
|
/** Read command byte */
|
|
gwbuf_copy_data(buf, 4, 1, readbuf);
|
|
|
|
if (readbuf[0] == 0xff) /*< error */
|
|
{
|
|
*npackets = 1;
|
|
}
|
|
else
|
|
{
|
|
switch (cmd)
|
|
{
|
|
case MYSQL_COM_STMT_PREPARE:
|
|
gwbuf_copy_data(buf, 9, 2, readbuf);
|
|
nparam = gw_mysql_get_byte2(readbuf);
|
|
gwbuf_copy_data(buf, 11, 2, readbuf);
|
|
nattr = gw_mysql_get_byte2(readbuf);
|
|
*npackets = 1 + nparam + MXS_MIN(1, nparam) + nattr + MXS_MIN(nattr, 1);
|
|
break;
|
|
|
|
case MYSQL_COM_QUIT:
|
|
case MYSQL_COM_STMT_SEND_LONG_DATA:
|
|
case MYSQL_COM_STMT_CLOSE:
|
|
*npackets = 0; /*< these don't reply anything */
|
|
break;
|
|
|
|
default:
|
|
/**
|
|
* assume that other session commands respond
|
|
* OK or ERR
|
|
*/
|
|
*npackets = 1;
|
|
break;
|
|
}
|
|
}
|
|
|
|
gwbuf_copy_data(buf, 0, 3, readbuf);
|
|
*nbytes_left = gw_mysql_get_byte3(readbuf) + MYSQL_HEADER_LEN;
|
|
/**
|
|
* There is at least one complete packet in the buffer so buffer is bigger
|
|
* than packet
|
|
*/
|
|
ss_dassert(*nbytes_left > 0);
|
|
ss_dassert(*npackets > 0);
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Read how many packets are left from current response and how many bytes there
|
|
* is still to be read from the current packet.
|
|
*/
|
|
bool protocol_get_response_status(MySQLProtocol* p,
|
|
int* npackets,
|
|
ssize_t* nbytes)
|
|
{
|
|
bool succp;
|
|
|
|
CHK_PROTOCOL(p);
|
|
|
|
*npackets = p->protocol_command.scom_nresponse_packets;
|
|
*nbytes = (ssize_t)p->protocol_command.scom_nbytes_to_read;
|
|
|
|
if (*npackets < 0 && *nbytes == 0)
|
|
{
|
|
succp = false;
|
|
}
|
|
else
|
|
{
|
|
succp = true;
|
|
}
|
|
|
|
return succp;
|
|
}
|
|
|
|
void protocol_set_response_status(MySQLProtocol* p,
|
|
int npackets_left,
|
|
ssize_t nbytes)
|
|
{
|
|
CHK_PROTOCOL(p);
|
|
|
|
p->protocol_command.scom_nbytes_to_read = nbytes;
|
|
ss_dassert(p->protocol_command.scom_nbytes_to_read >= 0);
|
|
|
|
p->protocol_command.scom_nresponse_packets = npackets_left;
|
|
}
|
|
|
|
char* create_auth_failed_msg(GWBUF*readbuf,
|
|
char* hostaddr,
|
|
uint8_t* sha1)
|
|
{
|
|
char* errstr;
|
|
char* uname = (char *)GWBUF_DATA(readbuf) + 5;
|
|
const char* ferrstr = "Access denied for user '%s'@'%s' (using password: %s)";
|
|
|
|
/** -4 comes from 2X'%s' minus terminating char */
|
|
errstr = (char *)MXS_MALLOC(strlen(uname) + strlen(ferrstr) + strlen(hostaddr) + strlen("YES") - 6 + 1);
|
|
|
|
if (errstr != NULL)
|
|
{
|
|
sprintf(errstr, ferrstr, uname, hostaddr, (*sha1 == '\0' ? "NO" : "YES"));
|
|
}
|
|
|
|
return errstr;
|
|
}
|
|
|
|
/**
|
|
* Create a message error string to send via MySQL ERR packet.
|
|
*
|
|
* @param username The MySQL user
|
|
* @param hostaddr The client IP
|
|
* @param password If client provided a password
|
|
* @param db The default database the client requested
|
|
* @param errcode Authentication error code
|
|
*
|
|
* @return Pointer to the allocated string or NULL on failure
|
|
*/
|
|
char *create_auth_fail_str(char *username,
|
|
char *hostaddr,
|
|
bool password,
|
|
char *db,
|
|
int errcode)
|
|
{
|
|
char* errstr;
|
|
const char* ferrstr;
|
|
int db_len;
|
|
|
|
if (db != NULL)
|
|
{
|
|
db_len = strlen(db);
|
|
}
|
|
else
|
|
{
|
|
db_len = 0;
|
|
}
|
|
|
|
if (db_len > 0)
|
|
{
|
|
ferrstr = "Access denied for user '%s'@'%s' (using password: %s) to database '%s'";
|
|
}
|
|
else if (errcode == MXS_AUTH_FAILED_SSL)
|
|
{
|
|
ferrstr = "Access without SSL denied";
|
|
}
|
|
else
|
|
{
|
|
ferrstr = "Access denied for user '%s'@'%s' (using password: %s)";
|
|
}
|
|
errstr = (char *)MXS_MALLOC(strlen(username) + strlen(ferrstr) +
|
|
strlen(hostaddr) + strlen("YES") - 6 +
|
|
db_len + ((db_len > 0) ? (strlen(" to database ") + 2) : 0) + 1);
|
|
|
|
if (errstr == NULL)
|
|
{
|
|
goto retblock;
|
|
}
|
|
|
|
if (db_len > 0)
|
|
{
|
|
sprintf(errstr, ferrstr, username, hostaddr, password ? "YES": "NO", db);
|
|
}
|
|
else if (errcode == MXS_AUTH_FAILED_SSL)
|
|
{
|
|
sprintf(errstr, "%s", ferrstr);
|
|
}
|
|
else
|
|
{
|
|
sprintf(errstr, ferrstr, username, hostaddr, password ? "YES" : "NO");
|
|
}
|
|
|
|
retblock:
|
|
return errstr;
|
|
}
|
|
|
|
/**
|
|
* @brief Read a complete packet from a DCB
|
|
*
|
|
* Read a complete packet from a connected DCB. If data was read, @c readbuf
|
|
* will point to the head of the read data. If no data was read, @c readbuf will
|
|
* be set to NULL.
|
|
*
|
|
* @param dcb DCB to read from
|
|
* @param readbuf Pointer to a buffer where the data is stored
|
|
* @return True on success, false if an error occurred while data was being read
|
|
*/
|
|
bool read_complete_packet(DCB *dcb, GWBUF **readbuf)
|
|
{
|
|
bool rval = false;
|
|
GWBUF *localbuf = NULL;
|
|
|
|
if (dcb_read(dcb, &localbuf, 0) >= 0)
|
|
{
|
|
rval = true;
|
|
dcb->last_read = hkheartbeat;
|
|
GWBUF *packets = modutil_get_complete_packets(&localbuf);
|
|
|
|
if (packets)
|
|
{
|
|
/** A complete packet was read */
|
|
*readbuf = packets;
|
|
}
|
|
|
|
if (localbuf)
|
|
{
|
|
/** Store any extra data in the DCB's readqueue */
|
|
|
|
dcb->dcb_readqueue = gwbuf_append(dcb->dcb_readqueue, localbuf);
|
|
|
|
}
|
|
}
|
|
|
|
return rval;
|
|
}
|
|
|
|
/**
|
|
* Copy shared session authentication info
|
|
*
|
|
* @param dcb A backend DCB
|
|
* @param session Destination where authentication data is copied
|
|
* @return bool true = success, false = fail
|
|
*/
|
|
bool gw_get_shared_session_auth_info(DCB* dcb, MYSQL_session* session)
|
|
{
|
|
bool rval = true;
|
|
CHK_DCB(dcb);
|
|
CHK_SESSION(dcb->session);
|
|
|
|
|
|
if (dcb->session->state != SESSION_STATE_ALLOC &&
|
|
dcb->session->state != SESSION_STATE_DUMMY)
|
|
{
|
|
memcpy(session, dcb->session->client_dcb->data, sizeof(MYSQL_session));
|
|
}
|
|
else
|
|
{
|
|
ss_dassert(false);
|
|
MXS_ERROR("%lu [gw_get_shared_session_auth_info] Couldn't get "
|
|
"session authentication info. Session in a wrong state %d.",
|
|
pthread_self(), dcb->session->state);
|
|
rval = false;
|
|
}
|
|
|
|
return rval;
|
|
}
|
|
|
|
/**
|
|
* @brief Send a MySQL protocol OK message to the dcb (client)
|
|
*
|
|
* @param dcb DCB where packet is written
|
|
* @param sequence Packet sequence number
|
|
* @param affected_rows Number of affected rows
|
|
* @param message SQL message
|
|
* @return 1 on success, 0 on error
|
|
*
|
|
*/
|
|
int mxs_mysql_send_ok(DCB *dcb, int sequence, uint8_t affected_rows, const char* message)
|
|
{
|
|
uint8_t *outbuf = NULL;
|
|
uint32_t mysql_payload_size = 0;
|
|
uint8_t mysql_packet_header[4];
|
|
uint8_t *mysql_payload = NULL;
|
|
uint8_t field_count = 0;
|
|
uint8_t insert_id = 0;
|
|
uint8_t mysql_server_status[2];
|
|
uint8_t mysql_warning_counter[2];
|
|
GWBUF *buf;
|
|
|
|
|
|
mysql_payload_size =
|
|
sizeof(field_count) +
|
|
sizeof(affected_rows) +
|
|
sizeof(insert_id) +
|
|
sizeof(mysql_server_status) +
|
|
sizeof(mysql_warning_counter);
|
|
|
|
if (message != NULL)
|
|
{
|
|
mysql_payload_size += strlen(message);
|
|
}
|
|
|
|
// allocate memory for packet header + payload
|
|
if ((buf = gwbuf_alloc(sizeof(mysql_packet_header) + mysql_payload_size)) == NULL)
|
|
{
|
|
return 0;
|
|
}
|
|
outbuf = GWBUF_DATA(buf);
|
|
|
|
// write packet header with packet number
|
|
gw_mysql_set_byte3(mysql_packet_header, mysql_payload_size);
|
|
mysql_packet_header[3] = sequence;
|
|
|
|
// write header
|
|
memcpy(outbuf, mysql_packet_header, sizeof(mysql_packet_header));
|
|
|
|
mysql_payload = outbuf + sizeof(mysql_packet_header);
|
|
|
|
mysql_server_status[0] = 2;
|
|
mysql_server_status[1] = 0;
|
|
mysql_warning_counter[0] = 0;
|
|
mysql_warning_counter[1] = 0;
|
|
|
|
// write data
|
|
memcpy(mysql_payload, &field_count, sizeof(field_count));
|
|
mysql_payload = mysql_payload + sizeof(field_count);
|
|
|
|
memcpy(mysql_payload, &affected_rows, sizeof(affected_rows));
|
|
mysql_payload = mysql_payload + sizeof(affected_rows);
|
|
|
|
memcpy(mysql_payload, &insert_id, sizeof(insert_id));
|
|
mysql_payload = mysql_payload + sizeof(insert_id);
|
|
|
|
memcpy(mysql_payload, mysql_server_status, sizeof(mysql_server_status));
|
|
mysql_payload = mysql_payload + sizeof(mysql_server_status);
|
|
|
|
memcpy(mysql_payload, mysql_warning_counter, sizeof(mysql_warning_counter));
|
|
mysql_payload = mysql_payload + sizeof(mysql_warning_counter);
|
|
|
|
if (message != NULL)
|
|
{
|
|
memcpy(mysql_payload, message, strlen(message));
|
|
}
|
|
|
|
// writing data in the Client buffer queue
|
|
return dcb->func.write(dcb, buf);
|
|
}
|
|
|
|
/**
|
|
* @brief Computes the size of the response to the DB initial handshake
|
|
*
|
|
* When the connection is to be SSL, but an SSL connection has not yet been
|
|
* established, only a basic 36 byte response is sent, including the SSL
|
|
* capability flag.
|
|
*
|
|
* Otherwise, the packet size is computed, based on the minimum size and
|
|
* increased by the optional or variable elements.
|
|
*
|
|
* @param conn The MySQLProtocol structure for the connection
|
|
* @param user Name of the user seeking to connect
|
|
* @param passwd Password for the user seeking to connect
|
|
* @param dbname Name of the database to be made default, if any
|
|
* @return The length of the response packet
|
|
*/
|
|
static int
|
|
response_length(MySQLProtocol *conn, char *user, uint8_t *passwd, char *dbname, const char *auth_module)
|
|
{
|
|
long bytes;
|
|
|
|
if (conn->owner_dcb->server->server_ssl && conn->owner_dcb->ssl_state != SSL_ESTABLISHED)
|
|
{
|
|
return MYSQL_AUTH_PACKET_BASE_SIZE;
|
|
}
|
|
|
|
// Protocol MySQL HandshakeResponse for CLIENT_PROTOCOL_41
|
|
// 4 bytes capabilities + 4 bytes max packet size + 1 byte charset + 23 '\0' bytes
|
|
// 4 + 4 + 1 + 23 = 32
|
|
bytes = 32;
|
|
|
|
if (user)
|
|
{
|
|
bytes += strlen(user);
|
|
}
|
|
// the NULL
|
|
bytes++;
|
|
|
|
// next will be + 1 (scramble_len) + 20 (fixed_scramble) + 1 (user NULL term) + 1 (db NULL term)
|
|
|
|
if (passwd)
|
|
{
|
|
bytes += GW_MYSQL_SCRAMBLE_SIZE;
|
|
}
|
|
bytes++;
|
|
|
|
if (dbname && strlen(dbname))
|
|
{
|
|
bytes += strlen(dbname);
|
|
bytes++;
|
|
}
|
|
|
|
bytes += strlen(auth_module);
|
|
bytes++;
|
|
|
|
// the packet header
|
|
bytes += 4;
|
|
|
|
return bytes;
|
|
}
|
|
|
|
/**
|
|
* @brief Helper function to load hashed password
|
|
* @param conn DCB Protocol object
|
|
* @param payload Destination where hashed password is written
|
|
* @param passwd Client's double SHA1 password
|
|
* @return Address of the next byte after the end of the stored password
|
|
*/
|
|
static uint8_t *
|
|
load_hashed_password(uint8_t *scramble, uint8_t *payload, uint8_t *passwd)
|
|
{
|
|
uint8_t hash1[GW_MYSQL_SCRAMBLE_SIZE] = "";
|
|
uint8_t hash2[GW_MYSQL_SCRAMBLE_SIZE] = "";
|
|
uint8_t new_sha[GW_MYSQL_SCRAMBLE_SIZE] = "";
|
|
uint8_t client_scramble[GW_MYSQL_SCRAMBLE_SIZE];
|
|
|
|
// hash1 is the function input, SHA1(real_password)
|
|
memcpy(hash1, passwd, GW_MYSQL_SCRAMBLE_SIZE);
|
|
|
|
// hash2 is the SHA1(input data), where input_data = SHA1(real_password)
|
|
gw_sha1_str(hash1, GW_MYSQL_SCRAMBLE_SIZE, hash2);
|
|
|
|
// new_sha is the SHA1(CONCAT(scramble, hash2)
|
|
gw_sha1_2_str(scramble, GW_MYSQL_SCRAMBLE_SIZE, hash2, GW_MYSQL_SCRAMBLE_SIZE, new_sha);
|
|
|
|
// compute the xor in client_scramble
|
|
gw_str_xor(client_scramble, new_sha, hash1, GW_MYSQL_SCRAMBLE_SIZE);
|
|
|
|
// set the auth-length
|
|
*payload = GW_MYSQL_SCRAMBLE_SIZE;
|
|
payload++;
|
|
|
|
//copy the 20 bytes scramble data after packet_buffer + 36 + user + NULL + 1 (byte of auth-length)
|
|
memcpy(payload, client_scramble, GW_MYSQL_SCRAMBLE_SIZE);
|
|
|
|
payload += GW_MYSQL_SCRAMBLE_SIZE;
|
|
return payload;
|
|
}
|
|
|
|
/**
|
|
* @brief Computes the capabilities bit mask for connecting to backend DB
|
|
*
|
|
* We start by taking the default bitmask and removing any bits not set in
|
|
* the bitmask contained in the connection structure. Then add SSL flag if
|
|
* the connection requires SSL (set from the MaxScale configuration). The
|
|
* compression flag may be set, although compression is NOT SUPPORTED. If a
|
|
* database name has been specified in the function call, the relevant flag
|
|
* is set.
|
|
*
|
|
* @param conn The MySQLProtocol structure for the connection
|
|
* @param db_specified Whether the connection request specified a database
|
|
* @param compress Whether compression is requested - NOT SUPPORTED
|
|
* @return Bit mask (32 bits)
|
|
* @note Capability bits are defined in maxscale/protocol/mysql.h
|
|
*/
|
|
static uint32_t
|
|
create_capabilities(MySQLProtocol *conn, bool db_specified, bool compress)
|
|
{
|
|
uint32_t final_capabilities;
|
|
|
|
/** Copy client's flags to backend but with the known capabilities mask */
|
|
final_capabilities = (conn->client_capabilities & (uint32_t)GW_MYSQL_CAPABILITIES_CLIENT);
|
|
|
|
if (conn->owner_dcb->server->server_ssl)
|
|
{
|
|
final_capabilities |= (uint32_t)GW_MYSQL_CAPABILITIES_SSL;
|
|
/* Unclear whether we should include this */
|
|
/* Maybe it should depend on whether CA certificate is provided */
|
|
/* final_capabilities |= (uint32_t)GW_MYSQL_CAPABILITIES_SSL_VERIFY_SERVER_CERT; */
|
|
}
|
|
|
|
/* Compression is not currently supported */
|
|
if (compress)
|
|
{
|
|
final_capabilities |= (uint32_t)GW_MYSQL_CAPABILITIES_COMPRESS;
|
|
#ifdef DEBUG_MYSQL_CONN
|
|
fprintf(stderr, ">>>> Backend Connection with compression\n");
|
|
#endif
|
|
}
|
|
|
|
if (db_specified)
|
|
{
|
|
/* With database specified */
|
|
final_capabilities |= (int)GW_MYSQL_CAPABILITIES_CONNECT_WITH_DB;
|
|
}
|
|
else
|
|
{
|
|
/* Without database specified */
|
|
final_capabilities &= ~(int)GW_MYSQL_CAPABILITIES_CONNECT_WITH_DB;
|
|
}
|
|
|
|
final_capabilities |= (int)GW_MYSQL_CAPABILITIES_PLUGIN_AUTH;
|
|
|
|
return final_capabilities;
|
|
}
|
|
|
|
/**
|
|
* Write MySQL authentication packet to backend server
|
|
*
|
|
* @param dcb Backend DCB
|
|
* @return True on success, false on failure
|
|
*/
|
|
mxs_auth_state_t gw_send_backend_auth(DCB *dcb)
|
|
{
|
|
MYSQL_session local_session;
|
|
gw_get_shared_session_auth_info(dcb, &local_session);
|
|
|
|
uint8_t client_capabilities[4] = {0, 0, 0, 0};
|
|
uint8_t *curr_passwd = memcmp(local_session.client_sha1, null_client_sha1, MYSQL_SCRAMBLE_LEN) ?
|
|
local_session.client_sha1 : NULL;
|
|
|
|
/**
|
|
* If session is stopping or has failed return with error.
|
|
*/
|
|
if (dcb->session == NULL ||
|
|
(dcb->session->state != SESSION_STATE_READY &&
|
|
dcb->session->state != SESSION_STATE_ROUTER_READY) ||
|
|
(dcb->server->server_ssl &&
|
|
dcb->ssl_state == SSL_HANDSHAKE_FAILED))
|
|
{
|
|
return MXS_AUTH_STATE_FAILED;
|
|
}
|
|
|
|
MySQLProtocol *conn = (MySQLProtocol*)dcb->protocol;
|
|
uint32_t capabilities = create_capabilities(conn, (local_session.db && strlen(local_session.db)), false);
|
|
gw_mysql_set_byte4(client_capabilities, capabilities);
|
|
|
|
/**
|
|
* Use the default authentication plugin name. If the server is using a
|
|
* different authentication mechanism, it will send an AuthSwitchRequest
|
|
* packet.
|
|
*/
|
|
const char* auth_plugin_name = DEFAULT_MYSQL_AUTH_PLUGIN;
|
|
|
|
long bytes = response_length(conn, local_session.user, curr_passwd,
|
|
local_session.db, auth_plugin_name);
|
|
|
|
// allocating the GWBUF
|
|
GWBUF *buffer = gwbuf_alloc(bytes);
|
|
uint8_t *payload = GWBUF_DATA(buffer);
|
|
|
|
// clearing data
|
|
memset(payload, '\0', bytes);
|
|
|
|
// put here the paylod size: bytes to write - 4 bytes packet header
|
|
gw_mysql_set_byte3(payload, (bytes - 4));
|
|
|
|
// set packet # = 1
|
|
payload[3] = (SSL_ESTABLISHED == dcb->ssl_state) ? '\x02' : '\x01';
|
|
payload += 4;
|
|
|
|
// set client capabilities
|
|
memcpy(payload, client_capabilities, 4);
|
|
|
|
// set now the max-packet size
|
|
payload += 4;
|
|
gw_mysql_set_byte4(payload, 16777216);
|
|
|
|
// set the charset
|
|
payload += 4;
|
|
*payload = conn->charset;
|
|
|
|
payload++;
|
|
|
|
// 19 filler bytes of 0
|
|
payload += 19;
|
|
|
|
// Either MariaDB 10.2 extra capabilities or 4 bytes filler
|
|
memcpy(payload, &conn->extra_capabilities, sizeof(conn->extra_capabilities));
|
|
payload += 4;
|
|
|
|
if (dcb->server->server_ssl && dcb->ssl_state != SSL_ESTABLISHED)
|
|
{
|
|
if (dcb_write(dcb, buffer) && dcb_connect_SSL(dcb) >= 0)
|
|
{
|
|
return MXS_AUTH_STATE_CONNECTED;
|
|
}
|
|
|
|
return MXS_AUTH_STATE_FAILED;
|
|
}
|
|
|
|
// 4 + 4 + 4 + 1 + 23 = 36, this includes the 4 bytes packet header
|
|
memcpy(payload, local_session.user, strlen(local_session.user));
|
|
payload += strlen(local_session.user);
|
|
payload++;
|
|
|
|
if (curr_passwd != NULL)
|
|
{
|
|
payload = load_hashed_password(conn->scramble, payload, curr_passwd);
|
|
}
|
|
else
|
|
{
|
|
payload++;
|
|
}
|
|
|
|
// if the db is not NULL append it
|
|
if (local_session.db[0])
|
|
{
|
|
memcpy(payload, local_session.db, strlen(local_session.db));
|
|
payload += strlen(local_session.db);
|
|
payload++;
|
|
}
|
|
|
|
memcpy(payload, auth_plugin_name, strlen(auth_plugin_name));
|
|
|
|
return dcb_write(dcb, buffer) ? MXS_AUTH_STATE_RESPONSE_SENT : MXS_AUTH_STATE_FAILED;
|
|
}
|
|
|
|
/**
|
|
* Decode mysql server handshake
|
|
*
|
|
* @param conn The MySQLProtocol structure
|
|
* @param payload The bytes just read from the net
|
|
* @return 0 on success, < 0 on failure
|
|
*
|
|
*/
|
|
static int
|
|
gw_decode_mysql_server_handshake(MySQLProtocol *conn, uint8_t *payload)
|
|
{
|
|
uint8_t *server_version_end = NULL;
|
|
uint16_t mysql_server_capabilities_one = 0;
|
|
uint16_t mysql_server_capabilities_two = 0;
|
|
unsigned long tid = 0;
|
|
uint8_t scramble_data_1[GW_SCRAMBLE_LENGTH_323] = "";
|
|
uint8_t scramble_data_2[GW_MYSQL_SCRAMBLE_SIZE - GW_SCRAMBLE_LENGTH_323] = "";
|
|
uint8_t capab_ptr[4] = "";
|
|
int scramble_len = 0;
|
|
uint8_t mxs_scramble[GW_MYSQL_SCRAMBLE_SIZE] = "";
|
|
int protocol_version = 0;
|
|
|
|
protocol_version = payload[0];
|
|
|
|
if (protocol_version != GW_MYSQL_PROTOCOL_VERSION)
|
|
{
|
|
return -1;
|
|
}
|
|
|
|
payload++;
|
|
|
|
// Get server version (string)
|
|
server_version_end = (uint8_t *) gw_strend((char*) payload);
|
|
|
|
payload = server_version_end + 1;
|
|
|
|
// get ThreadID: 4 bytes
|
|
tid = gw_mysql_get_byte4(payload);
|
|
memcpy(&conn->tid, &tid, 4);
|
|
|
|
payload += 4;
|
|
|
|
// scramble_part 1
|
|
memcpy(scramble_data_1, payload, GW_SCRAMBLE_LENGTH_323);
|
|
payload += GW_SCRAMBLE_LENGTH_323;
|
|
|
|
// 1 filler
|
|
payload++;
|
|
|
|
mysql_server_capabilities_one = gw_mysql_get_byte2(payload);
|
|
|
|
//Get capabilities_part 1 (2 bytes) + 1 language + 2 server_status
|
|
payload += 5;
|
|
|
|
mysql_server_capabilities_two = gw_mysql_get_byte2(payload);
|
|
|
|
memcpy(capab_ptr, &mysql_server_capabilities_one, 2);
|
|
|
|
// get capabilities part 2 (2 bytes)
|
|
memcpy(&capab_ptr[2], &mysql_server_capabilities_two, 2);
|
|
|
|
// 2 bytes shift
|
|
payload += 2;
|
|
|
|
// get scramble len
|
|
if (payload[0] > 0)
|
|
{
|
|
scramble_len = payload[0] - 1;
|
|
ss_dassert(scramble_len > GW_SCRAMBLE_LENGTH_323);
|
|
ss_dassert(scramble_len <= GW_MYSQL_SCRAMBLE_SIZE);
|
|
|
|
if ((scramble_len < GW_SCRAMBLE_LENGTH_323) ||
|
|
scramble_len > GW_MYSQL_SCRAMBLE_SIZE)
|
|
{
|
|
/* log this */
|
|
return -2;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
scramble_len = GW_MYSQL_SCRAMBLE_SIZE;
|
|
}
|
|
// skip 10 zero bytes
|
|
payload += 11;
|
|
|
|
// copy the second part of the scramble
|
|
memcpy(scramble_data_2, payload, scramble_len - GW_SCRAMBLE_LENGTH_323);
|
|
|
|
memcpy(mxs_scramble, scramble_data_1, GW_SCRAMBLE_LENGTH_323);
|
|
memcpy(mxs_scramble + GW_SCRAMBLE_LENGTH_323, scramble_data_2, scramble_len - GW_SCRAMBLE_LENGTH_323);
|
|
|
|
// full 20 bytes scramble is ready
|
|
memcpy(conn->scramble, mxs_scramble, GW_MYSQL_SCRAMBLE_SIZE);
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Read the backend server MySQL handshake
|
|
*
|
|
* @param dcb Backend DCB
|
|
* @return true on success, false on failure
|
|
*/
|
|
bool gw_read_backend_handshake(DCB *dcb, GWBUF *buffer)
|
|
{
|
|
MySQLProtocol *proto = (MySQLProtocol *)dcb->protocol;
|
|
bool rval = false;
|
|
uint8_t *payload = GWBUF_DATA(buffer) + 4;
|
|
|
|
if (gw_decode_mysql_server_handshake(proto, payload) >= 0)
|
|
{
|
|
rval = true;
|
|
}
|
|
|
|
return rval;
|
|
}
|
|
|
|
/**
|
|
* @brief Check if the buffer contains an OK packet
|
|
*
|
|
* @param buffer Buffer containing a complete MySQL packet
|
|
* @return True if the buffer contains an OK packet
|
|
*/
|
|
bool mxs_mysql_is_ok_packet(GWBUF *buffer)
|
|
{
|
|
bool rval = false;
|
|
uint8_t cmd;
|
|
|
|
if (gwbuf_copy_data(buffer, MYSQL_HEADER_LEN, 1, &cmd) && cmd == MYSQL_REPLY_OK)
|
|
{
|
|
rval = true;
|
|
}
|
|
|
|
return rval;
|
|
}
|
|
|
|
bool mxs_mysql_is_result_set(GWBUF *buffer)
|
|
{
|
|
bool rval = false;
|
|
uint8_t cmd;
|
|
|
|
if (gwbuf_copy_data(buffer, MYSQL_HEADER_LEN, 1, &cmd))
|
|
{
|
|
switch (cmd)
|
|
{
|
|
|
|
case MYSQL_REPLY_OK:
|
|
case MYSQL_REPLY_ERR:
|
|
case MYSQL_REPLY_LOCAL_INFILE:
|
|
case MYSQL_REPLY_EOF:
|
|
/** Not a result set */
|
|
break;
|
|
|
|
default:
|
|
rval = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return rval;
|
|
}
|