From 9ceb23dd65b02733aadbb2aacc3de4e61933bb9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20M=C3=A4kel=C3=A4?= Date: Thu, 7 Sep 2017 17:19:16 +0300 Subject: [PATCH] MXS-1396: Fix persistent connection hangs When a COM_CHANGE_USER was executed, it is possible that the server responds with a AuthSwitchRequest packet instead of an OK packet. In this case, the server sends a new scramble which must be used to create the 20 byte hash that is expected as the response. --- include/maxscale/protocol/mysql.h | 3 + .../MySQL/MySQLBackend/mysql_backend.c | 50 +++++++++++---- server/modules/protocol/MySQL/mysql_common.c | 61 +++++++++++++------ 3 files changed, 85 insertions(+), 29 deletions(-) diff --git a/include/maxscale/protocol/mysql.h b/include/maxscale/protocol/mysql.h index f9d206615..b67876630 100644 --- a/include/maxscale/protocol/mysql.h +++ b/include/maxscale/protocol/mysql.h @@ -425,6 +425,9 @@ bool gw_read_backend_handshake(DCB *dcb, GWBUF *buffer); /** Send the server handshake response packet to the backend server */ mxs_auth_state_t gw_send_backend_auth(DCB *dcb); +/** Sends a response for an AuthSwitchRequest to the default auth plugin */ +int send_mysql_native_password_response(DCB* dcb); + /** Write an OK packet to a DCB */ int mxs_mysql_send_ok(DCB *dcb, int sequence, uint8_t affected_rows, const char* message); diff --git a/server/modules/protocol/MySQL/MySQLBackend/mysql_backend.c b/server/modules/protocol/MySQL/MySQLBackend/mysql_backend.c index c187f91df..5999c4052 100644 --- a/server/modules/protocol/MySQL/MySQLBackend/mysql_backend.c +++ b/server/modules/protocol/MySQL/MySQLBackend/mysql_backend.c @@ -766,6 +766,43 @@ gw_read_and_write(DCB *dcb) MXS_INFO("Response to COM_CHANGE_USER is OK, writing stored query"); rval = query ? dcb->func.write(dcb, query) : 1; } + else if (result == MYSQL_REPLY_AUTHSWITCHREQUEST && + gwbuf_length(reply) > MYSQL_EOF_PACKET_LEN) + { + /** + * The server requested a change of authentication methods. + * If we're changing the authentication method to the same one we + * are using now, it means that the server is simply generating + * a new scramble for the re-authentication process. + */ + if (strcmp((char*)GWBUF_DATA(reply) + 5, DEFAULT_MYSQL_AUTH_PLUGIN) == 0) + { + /** Load the new scramble into the protocol... */ + gwbuf_copy_data(reply, 5 + strlen(DEFAULT_MYSQL_AUTH_PLUGIN) + 1, + GW_MYSQL_SCRAMBLE_SIZE, proto->scramble); + + /** ... and use it to send the encrypted password to the server */ + rval = send_mysql_native_password_response(dcb); + + /** Store the query until we know the result of the authentication + * method switch. */ + proto->stored_query = query; + proto->ignore_reply = true; + return rval; + } + else + { + /** The server requested a change to something other than + * the default auth plugin */ + gwbuf_free(query); + poll_fake_hangup_event(dcb); + + // TODO: Use the authenticators to handle COM_CHANGE_USER responses + MXS_ERROR("Received AuthSwitchRequest to '%s' when '%s' was expected", + (char*)GWBUF_DATA(reply) + 5, DEFAULT_MYSQL_AUTH_PLUGIN); + + } + } else { if (result == MYSQL_REPLY_ERR) @@ -774,22 +811,13 @@ gw_read_and_write(DCB *dcb) * close the DCB and send an error to the client. */ log_error_response(dcb, reply); } - else if (result == MYSQL_REPLY_AUTHSWITCHREQUEST && - gwbuf_length(reply) > MYSQL_EOF_PACKET_LEN) - { - MXS_ERROR("Received AuthSwitchRequest to '%s' when '%s' was expected", - (char*)GWBUF_DATA(reply) + 5, DEFAULT_MYSQL_AUTH_PLUGIN); - } else { /** This should never happen */ MXS_ERROR("Unknown response to COM_CHANGE_USER (0x%02hhx), " - "ignoring and waiting for correct result", result); - gwbuf_free(reply); - proto->stored_query = query; - proto->ignore_reply = true; - return 1; + "closing connection", result); } + gwbuf_free(query); poll_fake_hangup_event(dcb); } diff --git a/server/modules/protocol/MySQL/mysql_common.c b/server/modules/protocol/MySQL/mysql_common.c index 5433f0678..67654bee7 100644 --- a/server/modules/protocol/MySQL/mysql_common.c +++ b/server/modules/protocol/MySQL/mysql_common.c @@ -1205,19 +1205,19 @@ response_length(MySQLProtocol *conn, char *user, uint8_t *passwd, char *dbname, } /** - * @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 + * Calculates the a hash from a scramble and a password + * + * The algorithm used is: `SHA1(scramble + SHA1(SHA1(password))) ^ SHA1(password)` + * + * @param scramble The 20 byte scramble sent by the server + * @param passwd The SHA1(password) sent by the client + * @param output Pointer where the resulting 20 byte hash is stored */ -static uint8_t * -load_hashed_password(uint8_t *scramble, uint8_t *payload, uint8_t *passwd) +static void calculate_hash(uint8_t *scramble, uint8_t *passwd, uint8_t *output) { 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); @@ -1229,17 +1229,24 @@ load_hashed_password(uint8_t *scramble, uint8_t *payload, uint8_t *passwd) 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); + gw_str_xor(output, 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 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) +{ + *payload++ = GW_MYSQL_SCRAMBLE_SIZE; + calculate_hash(scramble, passwd, payload); + return payload + GW_MYSQL_SCRAMBLE_SIZE; } /** @@ -1411,6 +1418,24 @@ mxs_auth_state_t gw_send_backend_auth(DCB *dcb) return dcb_write(dcb, buffer) ? MXS_AUTH_STATE_RESPONSE_SENT : MXS_AUTH_STATE_FAILED; } +int send_mysql_native_password_response(DCB* dcb) +{ + MySQLProtocol* proto = (MySQLProtocol*) dcb->protocol; + MYSQL_session local_session; + gw_get_shared_session_auth_info(dcb, &local_session); + + uint8_t *curr_passwd = memcmp(local_session.client_sha1, null_client_sha1, MYSQL_SCRAMBLE_LEN) ? + local_session.client_sha1 : null_client_sha1; + + GWBUF* buffer = gwbuf_alloc(MYSQL_HEADER_LEN + GW_MYSQL_SCRAMBLE_SIZE); + uint8_t* data = GWBUF_DATA(buffer); + gw_mysql_set_byte3(data, GW_MYSQL_SCRAMBLE_SIZE); + data[3] = 2; // This is the third packet after the COM_CHANGE_USER + calculate_hash(proto->scramble, curr_passwd, data + MYSQL_HEADER_LEN); + + return dcb_write(dcb, buffer); +} + /** * Decode mysql server handshake *