/* * 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 #include #include #include #include #include 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; #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 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: if (gwbuf_copy_data(buffer, MYSQL_HEADER_LEN + 1, 1, &cmd) && cmd > 1) { rval = true; } break; } } return rval; }