diff --git a/server/core/test/CMakeLists.txt b/server/core/test/CMakeLists.txt index c35a887c7..b16fd2542 100644 --- a/server/core/test/CMakeLists.txt +++ b/server/core/test/CMakeLists.txt @@ -30,7 +30,7 @@ target_link_libraries(test_log maxscale-common) target_link_libraries(test_logorder maxscale-common) target_link_libraries(test_logthrottling maxscale-common) target_link_libraries(test_modutil maxscale-common) -target_link_libraries(test_mysql_users MySQLClient maxscale-common) +target_link_libraries(test_mysql_users MySQLAuth MySQLCommon maxscale-common) target_link_libraries(test_poll maxscale-common) target_link_libraries(test_queuemanager maxscale-common) target_link_libraries(test_server maxscale-common) diff --git a/server/modules/authenticator/CMakeLists.txt b/server/modules/authenticator/CMakeLists.txt index bba4feb42..c8928b726 100644 --- a/server/modules/authenticator/CMakeLists.txt +++ b/server/modules/authenticator/CMakeLists.txt @@ -1,21 +1,20 @@ add_library(MySQLAuth SHARED mysql_auth.c) -target_link_libraries(MySQLAuth maxscale-common) +target_link_libraries(MySQLAuth maxscale-common MySQLCommon) 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) +target_link_libraries(MySQLBackendAuth maxscale-common MySQLCommon) set_target_properties(MySQLBackendAuth PROPERTIES VERSION "1.0.0") install_module(MySQLBackendAuth core) add_library(GSSAPIAuth SHARED gssapi_auth.c gssapi_auth_common.c) -target_link_libraries(GSSAPIAuth maxscale-common gssapi_krb5) +target_link_libraries(GSSAPIAuth maxscale-common gssapi_krb5 MySQLCommon) set_target_properties(GSSAPIAuth PROPERTIES VERSION "1.0.0") install_module(GSSAPIAuth core) add_library(GSSAPIBackendAuth SHARED gssapi_backend_auth.c gssapi_auth_common.c) -target_link_libraries(GSSAPIBackendAuth maxscale-common gssapi_krb5 - MySQLBackend) # Needed for gw_send_backend_auth +target_link_libraries(GSSAPIBackendAuth maxscale-common gssapi_krb5 MySQLCommon) set_target_properties(GSSAPIBackendAuth PROPERTIES VERSION "1.0.0") install_module(GSSAPIBackendAuth core) diff --git a/server/modules/authenticator/gssapi_auth.c b/server/modules/authenticator/gssapi_auth.c index ac13f6b9c..6c165f683 100644 --- a/server/modules/authenticator/gssapi_auth.c +++ b/server/modules/authenticator/gssapi_auth.c @@ -281,26 +281,10 @@ int gssapi_auth_authenticate(DCB *dcb) MYSQL_session *ses = (MYSQL_session*)dcb->data; - if (validate_gssapi_token(ses->auth_token, ses->auth_token_len)) + if (validate_gssapi_token(ses->auth_token, ses->auth_token_len) && + mxs_mysql_send_ok(dcb, 4, 0, NULL)) { - /** Auth token is valid, send the OK packet - * @see https://dev.mysql.com/doc/internals/en/packet-OK_Packet.html */ - uint8_t ok_packet[] = - { - 0x07, 0x00, 0x00, 0x04, // Header - 0x00, // OK byte - 0x00, // Affected rows - 0x00, // Last insert id - 0x02, 0x00, // Status flags - 0x00, 0x00 // Warnings - }; - - GWBUF *buffer = gwbuf_alloc_and_load(sizeof(ok_packet), ok_packet); - - if (buffer && dcb->func.write(dcb, buffer)) - { - rval = MXS_AUTH_SUCCEEDED; - } + rval = MXS_AUTH_SUCCEEDED; } } diff --git a/server/modules/authenticator/mysql_auth.c b/server/modules/authenticator/mysql_auth.c index 874640f0d..838e64346 100644 --- a/server/modules/authenticator/mysql_auth.c +++ b/server/modules/authenticator/mysql_auth.c @@ -192,6 +192,8 @@ mysql_auth_authenticate(DCB *dcb) if (MXS_AUTH_SUCCEEDED == auth_ret) { dcb->user = MXS_STRDUP_A(client_data->user); + /** Send an OK packet to the client */ + mxs_mysql_send_ok(dcb, ssl_required_by_dcb(dcb) ? 3 : 2, 0, NULL); } else if (dcb->service->log_auth_warnings) { diff --git a/server/modules/include/mysql_client_server_protocol.h b/server/modules/include/mysql_client_server_protocol.h index c19292f9d..08c9853ba 100644 --- a/server/modules/include/mysql_client_server_protocol.h +++ b/server/modules/include/mysql_client_server_protocol.h @@ -380,4 +380,7 @@ 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); +/** Write an OK packet to a DCB */ +int mxs_mysql_send_ok(DCB *dcb, int sequence, int affected_rows, const char* message); + #endif /** _MYSQL_PROTOCOL_H */ diff --git a/server/modules/protocol/CMakeLists.txt b/server/modules/protocol/CMakeLists.txt index a6f3e6a46..3972c1509 100644 --- a/server/modules/protocol/CMakeLists.txt +++ b/server/modules/protocol/CMakeLists.txt @@ -5,8 +5,7 @@ endif() add_subdirectory(HTTPD) add_subdirectory(maxscaled) -add_subdirectory(MySQLBackend) -add_subdirectory(MySQLClient) +add_subdirectory(MySQL) add_subdirectory(telnetd) if(BUILD_TESTS) diff --git a/server/modules/protocol/MySQL/CMakeLists.txt b/server/modules/protocol/MySQL/CMakeLists.txt new file mode 100644 index 000000000..c8e967bf3 --- /dev/null +++ b/server/modules/protocol/MySQL/CMakeLists.txt @@ -0,0 +1,7 @@ +add_library(MySQLCommon SHARED mysql_common.c) +target_link_libraries(MySQLCommon maxscale-common) +set_target_properties(MySQLCommon PROPERTIES VERSION "2.0.0") +install_module(MySQLCommon core) + +add_subdirectory(MySQLBackend) +add_subdirectory(MySQLClient) diff --git a/server/modules/protocol/MySQL/MySQLBackend/CMakeLists.txt b/server/modules/protocol/MySQL/MySQLBackend/CMakeLists.txt new file mode 100644 index 000000000..3dc29a6e7 --- /dev/null +++ b/server/modules/protocol/MySQL/MySQLBackend/CMakeLists.txt @@ -0,0 +1,7 @@ +add_library(MySQLBackend SHARED mysql_backend.c) +# TODO: Refactor MySQLBackend so that COM_CHANGE_USER processing is +# transparent to the protocol module. After this change, we don't need to +# link against MySQLAuth. +target_link_libraries(MySQLBackend maxscale-common MySQLCommon MySQLAuth) +set_target_properties(MySQLBackend PROPERTIES VERSION "2.0.0") +install_module(MySQLBackend core) diff --git a/server/modules/protocol/MySQLBackend/mysql_backend.c b/server/modules/protocol/MySQL/MySQLBackend/mysql_backend.c similarity index 84% rename from server/modules/protocol/MySQLBackend/mysql_backend.c rename to server/modules/protocol/MySQL/MySQLBackend/mysql_backend.c index dfaec2b07..4442b8add 100644 --- a/server/modules/protocol/MySQLBackend/mysql_backend.c +++ b/server/modules/protocol/MySQL/MySQLBackend/mysql_backend.c @@ -661,290 +661,6 @@ 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); - - /** - * 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, 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[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; -} - -/** - * 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 * @@ -1649,6 +1365,8 @@ static int backend_write_delayqueue(DCB *dcb, GWBUF *buffer) /** * This routine handles the COM_CHANGE_USER command * + * TODO: Move this into the authenticators + * * @param dcb The current backend DCB * @param server The backend server pointer * @param in_session The current session data (MYSQL_session) @@ -2028,105 +1746,6 @@ static bool sescmd_response_complete(DCB* dcb) return succp; } -/** - * gw_decode_mysql_server_handshake - * - * 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; -} - static void inline close_socket(int sock) { diff --git a/server/modules/protocol/MySQL/MySQLClient/CMakeLists.txt b/server/modules/protocol/MySQL/MySQLClient/CMakeLists.txt new file mode 100644 index 000000000..31f7c937f --- /dev/null +++ b/server/modules/protocol/MySQL/MySQLClient/CMakeLists.txt @@ -0,0 +1,4 @@ +add_library(MySQLClient SHARED mysql_client.c) +target_link_libraries(MySQLClient maxscale-common MySQLCommon) +set_target_properties(MySQLClient PROPERTIES VERSION "1.0.0") +install_module(MySQLClient core) diff --git a/server/modules/protocol/MySQLClient/mysql_client.c b/server/modules/protocol/MySQL/MySQLClient/mysql_client.c similarity index 93% rename from server/modules/protocol/MySQLClient/mysql_client.c rename to server/modules/protocol/MySQL/MySQLClient/mysql_client.c index 2c021a4f4..2ac029271 100644 --- a/server/modules/protocol/MySQLClient/mysql_client.c +++ b/server/modules/protocol/MySQL/MySQLClient/mysql_client.c @@ -86,7 +86,6 @@ static int gw_client_close(DCB *dcb); static int gw_client_hangup_event(DCB *dcb); static char *gw_default_auth(); static int gw_connection_limit(DCB *dcb, int limit); -static int mysql_send_ok(DCB *dcb, int packet_number, int in_affected_rows, const char* mysql_message); static int MySQLSendHandshake(DCB* dcb); static int route_by_statement(SESSION *, GWBUF **); static void mysql_client_auth_error_handling(DCB *dcb, int auth_val); @@ -162,92 +161,6 @@ static char *gw_default_auth() { return "MySQLAuth"; } -/** - * mysql_send_ok - * - * Send a MySQL protocol OK message to the dcb (client) - * - * @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_ok(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 affected_rows = 0; - uint8_t insert_id = 0; - uint8_t mysql_server_status[2]; - uint8_t mysql_warning_counter[2]; - GWBUF *buf; - - affected_rows = in_affected_rows; - - mysql_payload_size = - sizeof(field_count) + - sizeof(affected_rows) + - sizeof(insert_id) + - sizeof(mysql_server_status) + - sizeof(mysql_warning_counter); - - if (mysql_message != NULL) - { - mysql_payload_size += strlen(mysql_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] = packet_number; - - // 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 (mysql_message != NULL) - { - memcpy(mysql_payload, mysql_message, strlen(mysql_message)); - } - - // writing data in the Client buffer queue - dcb->func.write(dcb, buf); - - return sizeof(mysql_packet_header) + mysql_payload_size; -} /** * MySQLSendHandshake @@ -625,18 +538,10 @@ gw_read_do_authentication(DCB *dcb, GWBUF *read_buffer, int nbytes_read) if (session != NULL) { - int packet_number = ssl_required_by_dcb(dcb) ? 3 : 2; - CHK_SESSION(session); ss_dassert(session->state != SESSION_STATE_ALLOC && - session->state != SESSION_STATE_DUMMY); - + session->state != SESSION_STATE_DUMMY); protocol->protocol_auth_state = MXS_AUTH_STATE_COMPLETE; - /** - * Send an AUTH_OK packet to the client, - * packet sequence is # packet_number - */ - mysql_send_ok(dcb, packet_number, 0, NULL); } else { diff --git a/server/modules/protocol/mysql_common.c b/server/modules/protocol/MySQL/mysql_common.c similarity index 68% rename from server/modules/protocol/mysql_common.c rename to server/modules/protocol/MySQL/mysql_common.c index c0c416b67..fbc6fc0b3 100644 --- a/server/modules/protocol/mysql_common.c +++ b/server/modules/protocol/MySQL/mysql_common.c @@ -1078,3 +1078,465 @@ bool gw_get_shared_session_auth_info(DCB* dcb, MYSQL_session* session) spinlock_release(&dcb->session->ses_lock); 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, int 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 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); + + /** + * 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, 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[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; +} diff --git a/server/modules/protocol/MySQLBackend/CMakeLists.txt b/server/modules/protocol/MySQLBackend/CMakeLists.txt deleted file mode 100644 index f23cb071b..000000000 --- a/server/modules/protocol/MySQLBackend/CMakeLists.txt +++ /dev/null @@ -1,4 +0,0 @@ -add_library(MySQLBackend SHARED mysql_backend.c ../mysql_common.c) -target_link_libraries(MySQLBackend maxscale-common MySQLAuth) -set_target_properties(MySQLBackend PROPERTIES VERSION "2.0.0") -install_module(MySQLBackend core) diff --git a/server/modules/protocol/MySQLClient/CMakeLists.txt b/server/modules/protocol/MySQLClient/CMakeLists.txt deleted file mode 100644 index 6a250cea0..000000000 --- a/server/modules/protocol/MySQLClient/CMakeLists.txt +++ /dev/null @@ -1,4 +0,0 @@ -add_library(MySQLClient SHARED mysql_client.c ../mysql_common.c) -target_link_libraries(MySQLClient maxscale-common MySQLAuth) -set_target_properties(MySQLClient PROPERTIES VERSION "1.0.0") -install_module(MySQLClient core)