diff --git a/server/core/dcb.c b/server/core/dcb.c index 7e1384b33..c0f568f25 100644 --- a/server/core/dcb.c +++ b/server/core/dcb.c @@ -397,6 +397,11 @@ dcb_free_all_memory(DCB *dcb) dcb->authfunc.free(dcb); dcb->data = NULL; } + if (dcb->backend_data && dcb->authfunc.free && dcb->dcb_role == DCB_ROLE_BACKEND_HANDLER) + { + dcb->authfunc.free(dcb); + dcb->backend_data = NULL; + } if (dcb->protoname) { MXS_FREE(dcb->protoname); diff --git a/server/include/dcb.h b/server/include/dcb.h index 6da199e85..29c934829 100644 --- a/server/include/dcb.h +++ b/server/include/dcb.h @@ -253,6 +253,7 @@ typedef struct dcb time_t persistentstart; /**< Time when DCB placed in persistent pool */ struct service *service; /**< The related service */ void *data; /**< Specific client data */ + void *backend_data; /**< Specific backend data */ DCBMM memdata; /**< The data related to DCB memory management */ SPINLOCK cb_lock; /**< The lock for the callbacks linked list */ DCB_CALLBACK *callbacks; /**< The list of callbacks for the DCB */ @@ -282,7 +283,8 @@ typedef struct dcb .authlock = SPINLOCK_INIT, .stats = {0}, .memdata = DCBMM_INIT, \ .cb_lock = SPINLOCK_INIT, .pollinlock = SPINLOCK_INIT, \ .fd = DCBFD_CLOSED, .stats = DCBSTATS_INIT, .ssl_state = SSL_HANDSHAKE_UNKNOWN, \ - .state = DCB_STATE_ALLOC, .polloutlock = SPINLOCK_INIT, .dcb_chk_tail = CHK_NUM_DCB} + .state = DCB_STATE_ALLOC, .polloutlock = SPINLOCK_INIT, .dcb_chk_tail = CHK_NUM_DCB, \ + .backend_data = NULL} /** * The DCB usage filer used for returning DCB's in use for a certain reason diff --git a/server/include/gw_authenticator.h b/server/include/gw_authenticator.h index 5f5ab04e4..ee66c1764 100644 --- a/server/include/gw_authenticator.h +++ b/server/include/gw_authenticator.h @@ -67,8 +67,8 @@ typedef struct gw_authenticator /** Return values for extract and authenticate entry points */ #define MXS_AUTH_SUCCEEDED 0 /**< Authentication was successful */ #define MXS_AUTH_FAILED 1 /**< Authentication failed */ -#define MXS_AUTH_FAILED_DB 2 -#define MXS_AUTH_FAILED_SSL 3 +#define MXS_AUTH_FAILED_DB 2 /**< Authentication failed, database not found */ +#define MXS_AUTH_FAILED_SSL 3 /**< SSL authentication failed */ #define MXS_AUTH_INCOMPLETE 4 /**< Authentication is not yet complete */ #define MXS_AUTH_SSL_INCOMPLETE 5 /**< SSL connection is not yet complete */ #define MXS_AUTH_NO_SESSION 6 diff --git a/server/modules/authenticator/CMakeLists.txt b/server/modules/authenticator/CMakeLists.txt index abe32bec3..d15bd8310 100644 --- a/server/modules/authenticator/CMakeLists.txt +++ b/server/modules/authenticator/CMakeLists.txt @@ -3,6 +3,11 @@ target_link_libraries(MySQLAuth maxscale-common) set_target_properties(MySQLAuth PROPERTIES VERSION "1.0.0") install_module(MySQLAuth core) +add_library(MySQLBackendAuth SHARED mysql_backend_auth.c) +target_link_libraries(MySQLBackendAuth maxscale-common MySQLBackend) +set_target_properties(MySQLBackendAuth PROPERTIES VERSION "1.0.0") +install_module(MySQLBackendAuth core) + add_library(NullAuthAllow SHARED null_auth_allow.c) target_link_libraries(NullAuthAllow maxscale-common) set_target_properties(NullAuthAllow PROPERTIES VERSION "1.0.0") diff --git a/server/modules/authenticator/mysql_backend_auth.c b/server/modules/authenticator/mysql_backend_auth.c new file mode 100644 index 000000000..7b8b7111f --- /dev/null +++ b/server/modules/authenticator/mysql_backend_auth.c @@ -0,0 +1,275 @@ +/* + * 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/bsl. + * + * 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. + */ + +/** + * @file mysql_backend_auth.c - MySQL backend authenticator + * + * Backend authentication module for the MySQL protocol. Implements the + * client side of the 'mysql_native_password' authentication plugin. + * + * @verbatim + * Revision History + * Date Who Description + * 27/09/2016 Markus Makela Initial version + * + * @endverbatim + */ + +#include +#include +#include +#include + +/** Authentication states */ +enum mba_state +{ + MBA_NEED_HANDSHAKE, /**< Waiting for server's handshake packet */ + MBA_SEND_RESPONSE, /**< A response to the server's handshake has been sent */ + MBA_NEED_OK, /**< Waiting for server's OK packet */ + MBA_AUTH_OK, /**< Authentication completed successfully */ + MBA_AUTH_FAILED /**< Authentication failed */ +}; + +/** Structure representing the authentication state */ +typedef struct mysql_backend_auth +{ + enum mba_state state; /**< Authentication state */ +} mysql_backend_auth_t; + +/** + * @brief Allocate a new mysql_backend_auth object + * @return Allocated object or NULL if memory allocation failed + */ +mysql_backend_auth_t* mba_alloc() +{ + mysql_backend_auth_t* mba = MXS_MALLOC(sizeof(*mba)); + + if (mba) + { + mba->state = MBA_NEED_HANDSHAKE; + } + + return mba; +} + +/** + * Receive the MySQL authentication packet from backend, packet # is 2 + * + * @param protocol The MySQL protocol structure + * @return False in case of failure, true if authentication was successful. + */ +static bool gw_read_auth_response(DCB *dcb, 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; +} + +/** + * @brief Extract backend response + * + * @param dcb Request handler DCB connected to the client + * @param buffer Buffer containing data from client + * @return Authentication status + * @see gw_quthenticator.h + * @see https://dev.mysql.com/doc/internals/en/client-server-protocol.html + */ +static int +auth_backend_extract(DCB *dcb, GWBUF *buf) +{ + int rval = MXS_AUTH_FAILED; + + if (dcb->backend_data || (dcb->backend_data = mba_alloc())) + { + mysql_backend_auth_t *mba = (mysql_backend_auth_t*)dcb->backend_data; + + switch (mba->state) + { + case MBA_NEED_HANDSHAKE: + if (gw_read_backend_handshake(dcb, buf)) + { + rval = MXS_AUTH_INCOMPLETE; + mba->state = MBA_SEND_RESPONSE; + } + else + { + mba->state = MBA_AUTH_FAILED; + } + break; + + case MBA_NEED_OK: + if (gw_read_auth_response(dcb, buf)) + { + rval = MXS_AUTH_SUCCEEDED; + mba->state = MBA_AUTH_OK; + } + else + { + mba->state = MBA_AUTH_FAILED; + } + break; + + default: + MXS_ERROR("Unexpected call to MySQLBackendAuth::extract"); + ss_dassert(false); + break; + } + } + + return rval; +} + +/** + * @brief Authenticates as a MySQL user + * + * @param dcb Backend DCB + * @return Authentication status + * @see gw_authenticator.h + */ +static int +auth_backend_authenticate(DCB *dcb) +{ + int rval = MXS_AUTH_FAILED; + mysql_backend_auth_t *mba = (mysql_backend_auth_t*)dcb->backend_data; + + if (mba->state == MBA_SEND_RESPONSE) + { + /** First message read, decode password and send the auth credentials to backend */ + switch (gw_send_backend_auth(dcb)) + { + case MXS_AUTH_STATE_CONNECTED: + rval = MXS_AUTH_SSL_INCOMPLETE; + break; + + case MXS_AUTH_STATE_RESPONSE_SENT: + mba->state = MBA_NEED_OK; + rval = MXS_AUTH_INCOMPLETE; + break; + + default: + /** Authentication failed */ + break; + } + } + else if (mba->state == MBA_AUTH_OK) + { + /** Authentication completed successfully */ + rval = MXS_AUTH_SUCCEEDED; + } + + return rval; +} + +/** + * @brief Determine whether the client is SSL capable + * + * The authentication request from the client will indicate whether the client + * is expecting to make an SSL connection. The information has been extracted + * in the previous functions. + * + * @param dcb Request handler DCB connected to the client + * @return Boolean indicating whether client is SSL capable + */ +static bool +auth_backend_ssl(DCB *dcb) +{ + return dcb->server->server_ssl != NULL; +} + +/** + * @brief Dummy function for the free entry point + */ +static void +auth_backend_free(DCB *dcb) +{ + MXS_FREE(dcb->backend_data); + dcb->backend_data = NULL; +} + +/** + * @brief Dummy function for the loadusers entry point + */ +static int auth_backend_load_users(SERV_LISTENER *port) +{ + return MXS_AUTH_LOADUSERS_OK; +} + +/* @see function load_module in load_utils.c for explanation of the following + * lint directives. +*/ +/*lint -e14 */ +MODULE_INFO info = +{ + MODULE_API_AUTHENTICATOR, + MODULE_GA, + GWAUTHENTICATOR_VERSION, + "The MySQL MaxScale to backend server authenticator" +}; +/*lint +e14 */ + +static char *version_str = "V1.0.0"; + +/* + * The "module object" for mysql client authenticator module. + */ +static GWAUTHENTICATOR MyObject = +{ + auth_backend_extract, /* Extract data into structure */ + auth_backend_ssl, /* Check if client supports SSL */ + auth_backend_authenticate, /* Authenticate user credentials */ + auth_backend_free, /* Free the client data held in DCB */ + auth_backend_load_users, /* Load users from backend databases */ + DEFAULT_MYSQL_AUTH_PLUGIN +}; + +/** + * Implementation of the mandatory version entry point + * + * @return version string of the module + * + * @see function load_module in load_utils.c for explanation of the following + * lint directives. + */ +/*lint -e14 */ +char* version() +{ + return version_str; +} + +/** + * The module initialisation routine, called when the module + * is first loaded. + */ +void ModuleInit() +{ +} + +/** + * The module entry point routine. It is this routine that + * must populate the structure that is referred to as the + * "module object", this is a structure with the set of + * external entry points for this module. + * + * @return The module object + */ +GWAUTHENTICATOR* GetModuleObject() +{ + return &MyObject; +} +/*lint +e14 */ diff --git a/server/modules/include/mysql_client_server_protocol.h b/server/modules/include/mysql_client_server_protocol.h index 857650c87..41c8ca05c 100644 --- a/server/modules/include/mysql_client_server_protocol.h +++ b/server/modules/include/mysql_client_server_protocol.h @@ -294,6 +294,8 @@ typedef struct #define MYSQL_IS_CHANGE_USER(payload) (MYSQL_GET_COMMAND(payload)==MYSQL_COM_CHANGE_USER) #define MYSQL_GET_NATTR(payload) ((int)payload[4]) +/* The following can be compared using memcmp to detect a null password */ +extern uint8_t null_client_sha1[MYSQL_SCRAMBLE_LEN]; MySQLProtocol* mysql_protocol_init(DCB* dcb, int fd); void mysql_protocol_done (DCB* dcb); @@ -365,5 +367,12 @@ void init_response_status ( int* npackets, ssize_t* nbytes); bool read_complete_packet(DCB *dcb, GWBUF **readbuf); +bool gw_get_shared_session_auth_info(DCB* dcb, MYSQL_session* session); + +/** Read the backend server's handshake */ +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); #endif /** _MYSQL_PROTOCOL_H */ diff --git a/server/modules/protocol/MySQLBackend/mysql_backend.c b/server/modules/protocol/MySQLBackend/mysql_backend.c index e4616503b..08c7db9d5 100644 --- a/server/modules/protocol/MySQLBackend/mysql_backend.c +++ b/server/modules/protocol/MySQLBackend/mysql_backend.c @@ -21,9 +21,6 @@ #include #include -/* The following can be compared using memcmp to detect a null password */ -uint8_t null_client_sha1[MYSQL_SCRAMBLE_LEN] = ""; - /* * MySQL Protocol module for handling the protocol between the gateway * and the backend MySQL database. @@ -87,13 +84,7 @@ extern char* create_auth_failed_msg(GWBUF* readbuf, char* hostaddr, uint8_t* sha static bool sescmd_response_complete(DCB* dcb); static void gw_reply_on_error(DCB *dcb, mxs_auth_state_t state); static int gw_read_and_write(DCB *dcb); -static mxs_auth_state_t gw_read_backend_handshake(MySQLProtocol *conn, GWBUF *buffer); static int gw_decode_mysql_server_handshake(MySQLProtocol *conn, uint8_t *payload); -static mxs_auth_state_t gw_read_auth_response(DCB *dcb, GWBUF *buffer); -static mxs_auth_state_t gw_send_auth(MYSQL_session *ses, MySQLProtocol *conn); -static uint32_t create_capabilities(MySQLProtocol *conn, bool db_specified, bool compress); -static int response_length(MySQLProtocol *conn, char *user, uint8_t *passwd, char *dbname); -static uint8_t *load_hashed_password(MySQLProtocol *conn, uint8_t *payload, uint8_t *passwd); static int gw_do_connect_to_backend(char *host, int port, int *fd); static void inline close_socket(int socket); static GWBUF *gw_create_change_user_packet(MYSQL_session* mses, @@ -106,7 +97,6 @@ static int gw_send_change_user_to_backend(char *dbname, #if defined(NOT_USED) static int gw_session(DCB *backend_dcb, void *data); #endif -static bool gw_get_shared_session_auth_info(DCB* dcb, MYSQL_session* session); static GWPROTOCOL MyObject = { @@ -169,7 +159,7 @@ GWPROTOCOL* GetModuleObject() */ static char *gw_backend_default_auth() { - return "NullAuthAllow"; + return "MySQLBackendAuth"; } /*lint +e14 */ @@ -470,29 +460,41 @@ void log_error_response(DCB *dcb, GWBUF *buffer) } /** - * @brief Handle the server's handshake packet + * @brief Handle the server's response packet * - * This function reads the server's handshake packet and does the first step of + * This function reads the server's response packet and does the final step of * the authentication. * * @param dcb Backend DCB * @param buffer Buffer containing the server's complete handshake * @return MXS_AUTH_STATE_HANDSHAKE_FAILED on failure. */ -mxs_auth_state_t handle_server_handshake(DCB *dcb, GWBUF *buffer) +mxs_auth_state_t handle_server_response(DCB *dcb, GWBUF *buffer) { - MySQLProtocol *proto = (MySQLProtocol *)dcb->protocol; - mxs_auth_state_t rval = gw_read_backend_handshake(proto, buffer); + MySQLProtocol *proto = (MySQLProtocol*)dcb->protocol; + mxs_auth_state_t rval = proto->protocol_auth_state == MXS_AUTH_STATE_CONNECTED ? + MXS_AUTH_STATE_HANDSHAKE_FAILED : MXS_AUTH_STATE_FAILED; - if (rval != MXS_AUTH_STATE_HANDSHAKE_FAILED) + int rc = dcb->authfunc.extract(dcb, buffer); + + if (rc == MXS_AUTH_SUCCEEDED || rc == MXS_AUTH_INCOMPLETE) { - MYSQL_session local_session; - gw_get_shared_session_auth_info(dcb, &local_session); + switch (dcb->authfunc.authenticate(dcb)) + { + case MXS_AUTH_INCOMPLETE: + case MXS_AUTH_SSL_INCOMPLETE: + rval = MXS_AUTH_STATE_RESPONSE_SENT; + break; - /** First message read, decode password and send the auth credentials to backend */ - rval = gw_send_auth(&local_session, proto); + case MXS_AUTH_SUCCEEDED: + rval = MXS_AUTH_STATE_COMPLETE; + + default: + break; + } } + gwbuf_free(buffer); return rval; } @@ -578,45 +580,29 @@ gw_read_backend_event(DCB *dcb) log_error_response(dcb, readbuf); } - if (proto->protocol_auth_state == MXS_AUTH_STATE_CONNECTED) + if (proto->protocol_auth_state == MXS_AUTH_STATE_CONNECTED || + proto->protocol_auth_state == MXS_AUTH_STATE_RESPONSE_SENT) { - /** TODO: Convert this to a call to dcb->authfunc.authenticate */ /** Read the first message from the server */ - proto->protocol_auth_state = handle_server_handshake(dcb, readbuf); - readbuf = NULL; + proto->protocol_auth_state = handle_server_response(dcb, readbuf); } - else + + if (proto->protocol_auth_state == MXS_AUTH_STATE_COMPLETE) { - /** - * Authentication process is ongoing. The default authentication - * plugin, mysql_native_password, sends two messages: the initial - * handshake and then either an OK or an ERR packet which signals - * whether authentication was successful - */ - if (proto->protocol_auth_state == MXS_AUTH_STATE_RESPONSE_SENT) - { - /** TODO: Convert this to a call to dcb->authfunc.authenticate */ - proto->protocol_auth_state = gw_read_auth_response(dcb, readbuf); - } + /** Authentication completed successfully */ + spinlock_acquire(&dcb->authlock); + GWBUF *localq = dcb->delayq; + dcb->delayq = NULL; + spinlock_release(&dcb->authlock); - if (proto->protocol_auth_state == MXS_AUTH_STATE_COMPLETE) + if (localq) { - /** Authentication completed successfully */ - spinlock_acquire(&dcb->authlock); - GWBUF *localq = dcb->delayq; - dcb->delayq = NULL; - spinlock_release(&dcb->authlock); - - if (localq) - { - /** Send the queued commands to the backend */ - rc = backend_write_delayqueue(dcb, localq); - } + /** Send the queued commands to the backend */ + rc = backend_write_delayqueue(dcb, localq); } } - - if (proto->protocol_auth_state == MXS_AUTH_STATE_FAILED || - proto->protocol_auth_state == MXS_AUTH_STATE_HANDSHAKE_FAILED) + else if (proto->protocol_auth_state == MXS_AUTH_STATE_FAILED || + proto->protocol_auth_state == MXS_AUTH_STATE_HANDSHAKE_FAILED) { /** Authentication failed */ gw_reply_on_error(dcb, proto->protocol_auth_state); @@ -627,168 +613,6 @@ gw_read_backend_event(DCB *dcb) return rc; } -/** - * Read the backend server MySQL handshake - * - * TODO: Move this function inside a module - * - * @param conn MySQL protocol structure - * @return 0 on success, 1 on failure - */ -static mxs_auth_state_t -gw_read_backend_handshake(MySQLProtocol *conn, GWBUF *buffer) -{ - mxs_auth_state_t rval = MXS_AUTH_STATE_HANDSHAKE_FAILED; - uint8_t *payload = GWBUF_DATA(buffer) + 4; - - if (gw_decode_mysql_server_handshake(conn, payload) >= 0) - { - rval = MXS_AUTH_STATE_MESSAGE_READ; - } - - gwbuf_free(buffer); - return rval; -} - -/** - * Write MySQL authentication packet to backend server - * - * @param conn MySQL protocol structure - * @param dbname The selected database - * @param user The selected user - * @param passwd The SHA1(real_password): Note real_password is unknown - * @return MySQL authorisation state after operation - */ -static mxs_auth_state_t -gw_send_auth(MYSQL_session *ses, MySQLProtocol *conn) -{ - uint8_t *payload; - long bytes; - uint32_t capabilities; - uint8_t client_capabilities[4] = {0, 0, 0, 0}; - GWBUF *buffer; - uint8_t *curr_passwd = memcmp(ses->client_sha1, null_client_sha1, MYSQL_SCRAMBLE_LEN) ? - ses->client_sha1 : NULL; - - /** - * If session is stopping or has failed return with error. - */ - if (conn->owner_dcb->session == NULL || - (conn->owner_dcb->session->state != SESSION_STATE_READY && - conn->owner_dcb->session->state != SESSION_STATE_ROUTER_READY) || - (conn->owner_dcb->server->server_ssl && - conn->owner_dcb->ssl_state != SSL_HANDSHAKE_FAILED)) - { - return MXS_AUTH_STATE_FAILED; - } - - capabilities = create_capabilities(conn, (ses->db && strlen(ses->db)), false); - gw_mysql_set_byte4(client_capabilities, capabilities); - - bytes = response_length(conn, ses->user, ses->client_sha1, ses->db); - - // allocating the GWBUF - buffer = gwbuf_alloc(bytes); - 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 == conn->owner_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++; - - // 23 bytes of 0 - payload += 23; - - if (conn->owner_dcb->server->server_ssl && conn->owner_dcb->ssl_state != SSL_ESTABLISHED) - { - if (dcb_write(conn->owner_dcb, buffer) && dcb_connect_SSL(conn->owner_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, ses->user, strlen(ses->user)); - payload += strlen(ses->user); - payload++; - - if (curr_passwd != NULL) - { - payload = load_hashed_password(conn, payload, curr_passwd); - } - else - { - payload++; - } - - // if the db is not NULL append it - if (ses->db && strlen(ses->db)) - { - memcpy(payload, ses->db, strlen(ses->db)); - payload += strlen(ses->db); - payload++; - } - - const char* auth_plugin_name = conn->owner_dcb->authfunc.plugin_name ? - conn->owner_dcb->authfunc.plugin_name : DEFAULT_MYSQL_AUTH_PLUGIN; - - memcpy(payload, auth_plugin_name, strlen(auth_plugin_name)); - - return dcb_write(conn->owner_dcb, buffer) ? MXS_AUTH_STATE_RESPONSE_SENT : MXS_AUTH_STATE_FAILED; -} - -/** - * Copy shared session authentication info - * - * @param dcb A backend DCB - * @param session Destination where authentication data is copied - * @return bool true = success, false = fail - */ -static bool gw_get_shared_session_auth_info(DCB* dcb, MYSQL_session* session) -{ - bool rval = true; - CHK_DCB(dcb); - CHK_SESSION(dcb->session); - - spinlock_acquire(&dcb->session->ses_lock); - - 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; - } - spinlock_release(&dcb->session->ses_lock); - return rval; -} - /** * @brief Authentication of backend - read the reply, or handle an error * @@ -829,7 +653,7 @@ gw_reply_on_error(DCB *dcb, mxs_auth_state_t state) } else { - /** A NULL router_session can valid for a router if it declared the + /** A NULL router_session is valid if a router declares the * RCAP_TYPE_NO_RSESSION capability flag */ dcb->dcb_errhandle_called = true; } @@ -837,6 +661,286 @@ gw_reply_on_error(DCB *dcb, mxs_auth_state_t state) gwbuf_free(errbuf); } +/** + * @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 36; + } + + // 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 mysql_client_server_protocol.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); + + const char* auth_plugin_name = dcb->authfunc.plugin_name ? + dcb->authfunc.plugin_name : DEFAULT_MYSQL_AUTH_PLUGIN; + + long bytes = response_length(conn, local_session.user, local_session.client_sha1, + 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++; + + // 23 bytes of 0 + payload += 23; + + 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 && strlen(local_session.db)) + { + 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; +} + +/** + * 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 With authentication completed, read new data and write to backend * @@ -869,12 +973,12 @@ gw_read_and_write(DCB *dcb) "Read from backend failed"); session->service->router->handleError( - session->service->router_instance, - session->router_session, - errbuf, - dcb, - ERRACT_NEW_CONNECTION, - &succp); + session->service->router_instance, + session->router_session, + errbuf, + dcb, + ERRACT_NEW_CONNECTION, + &succp); gwbuf_free(errbuf); if (!succp) @@ -970,22 +1074,21 @@ gw_read_and_write(DCB *dcb) gwbuf_set_type(read_buffer, GWBUF_TYPE_MYSQL); session->service->router->clientReply( - session->service->router_instance, - session->router_session, - read_buffer, - dcb); + session->service->router_instance, + session->router_session, + read_buffer, + dcb); return_code = 1; } } else if (dcb->session->client_dcb->dcb_role == DCB_ROLE_INTERNAL) { gwbuf_set_type(read_buffer, GWBUF_TYPE_MYSQL); - session->service->router->clientReply( - session->service->router_instance, - session->router_session, - read_buffer, - dcb); + session->service->router_instance, + session->router_session, + read_buffer, + dcb); return_code = 1; } } @@ -1999,175 +2102,6 @@ gw_decode_mysql_server_handshake(MySQLProtocol *conn, uint8_t *payload) return 0; } -/** - * Receive the MySQL authentication packet from backend, packet # is 2 - * - * @param protocol The MySQL protocol structure - * @return -1 in case of failure, 1 if authentication was successful. - */ -static mxs_auth_state_t -gw_read_auth_response(DCB *dcb, GWBUF *buffer) -{ - mxs_auth_state_t rval = MXS_AUTH_STATE_FAILED; - uint8_t cmd; - - if (gwbuf_copy_data(buffer, MYSQL_HEADER_LEN, 1, &cmd) && cmd == MYSQL_REPLY_OK) - { - rval = MXS_AUTH_STATE_COMPLETE; - } - - return rval; -} - -/** - * @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 mysql_client_server_protocol.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; -} - -/** - * @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) -{ - long bytes; - - if (conn->owner_dcb->server->server_ssl && conn->owner_dcb->ssl_state != SSL_ESTABLISHED) - { - return 36; - } - - // 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("mysql_native_password"); - bytes++; - - // the packet header - bytes += 4; - - return bytes; -} - -static uint8_t * -load_hashed_password(MySQLProtocol *conn, 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(conn->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; -} - static void inline close_socket(int sock) { diff --git a/server/modules/protocol/MySQLClient/mysql_client.c b/server/modules/protocol/MySQLClient/mysql_client.c index ed4f3f6d3..492c84b1f 100644 --- a/server/modules/protocol/MySQLClient/mysql_client.c +++ b/server/modules/protocol/MySQLClient/mysql_client.c @@ -562,12 +562,6 @@ int gw_read_client_event(DCB* dcb) static int gw_read_do_authentication(DCB *dcb, GWBUF *read_buffer, int nbytes_read) { - MySQLProtocol *protocol; - int auth_val; - - protocol = (MySQLProtocol *)dcb->protocol; - /* int compress = -1; */ - /** * The first step in the authentication process is to extract the * relevant information from the buffer supplied and place it @@ -577,18 +571,15 @@ gw_read_do_authentication(DCB *dcb, GWBUF *read_buffer, int nbytes_read) * data extraction succeeds, then a call is made to the actual * authenticate function to carry out the user checks. */ - if (MXS_AUTH_SUCCEEDED == ( - auth_val = dcb->authfunc.extract(dcb, read_buffer))) + int auth_val = dcb->authfunc.extract(dcb, read_buffer); + + if (MXS_AUTH_SUCCEEDED == auth_val) { - /* - * Maybe this comment will be useful some day: - compress = - GW_MYSQL_CAPABILITIES_COMPRESS & gw_mysql_get_byte4( - &protocol->client_capabilities); - */ auth_val = dcb->authfunc.authenticate(dcb); } + MySQLProtocol *protocol = (MySQLProtocol *)dcb->protocol; + /** * At this point, if the auth_val return code indicates success * the user authentication has been successfully completed. diff --git a/server/modules/protocol/mysql_common.c b/server/modules/protocol/mysql_common.c index 5ddd6b665..1e6989af9 100644 --- a/server/modules/protocol/mysql_common.c +++ b/server/modules/protocol/mysql_common.c @@ -52,6 +52,8 @@ #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); /** @@ -1025,3 +1027,35 @@ bool read_complete_packet(DCB *dcb, GWBUF **readbuf) 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); + + spinlock_acquire(&dcb->session->ses_lock); + + 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; + } + spinlock_release(&dcb->session->ses_lock); + return rval; +}