Merge branch 'MXS-544' into develop-MXS-544-merge

This commit is contained in:
Markus Makela
2016-03-03 21:39:39 +02:00
42 changed files with 3075 additions and 2294 deletions

View File

@ -1,9 +1,9 @@
add_library(MySQLClient SHARED mysql_client.c mysql_common.c)
add_library(MySQLClient SHARED mysql_client.c mysql_common.c mysql_auth.c)
target_link_libraries(MySQLClient maxscale-common)
set_target_properties(MySQLClient PROPERTIES VERSION "1.0.0")
install(TARGETS MySQLClient DESTINATION ${MAXSCALE_LIBDIR})
add_library(MySQLBackend SHARED mysql_backend.c mysql_common.c)
add_library(MySQLBackend SHARED mysql_backend.c mysql_common.c mysql_auth.c)
target_link_libraries(MySQLBackend maxscale-common)
set_target_properties(MySQLBackend PROPERTIES VERSION "2.0.0")
install(TARGETS MySQLBackend DESTINATION ${MAXSCALE_LIBDIR})

View File

@ -38,6 +38,7 @@
*/
#include <httpd.h>
#include <gw_protocol.h>
#include <gw.h>
#include <modinfo.h>
#include <log_manager.h>
@ -356,6 +357,7 @@ static int httpd_accept(DCB *dcb)
if ((client = dcb_alloc(DCB_ROLE_REQUEST_HANDLER)))
{
client->listen_ssl = dcb->listen_ssl;
client->fd = so;
client->remote = strdup(inet_ntoa(addr.sin_addr));
memcpy(&client->func, &MyObject, sizeof(GWPROTOCOL));

View File

@ -21,6 +21,7 @@
#include <string.h>
#include <dcb.h>
#include <buffer.h>
#include <gw_protocol.h>
#include <service.h>
#include <session.h>
#include <sys/ioctl.h>
@ -267,6 +268,7 @@ static int maxscaled_accept(DCB *dcb)
close(so);
return n_connect;
}
client_dcb->listen_ssl = dcb->listen_ssl;
client_dcb->fd = so;
client_dcb->remote = strdup(inet_ntoa(addr.sin_addr));
memcpy(&client_dcb->func, &MyObject, sizeof(GWPROTOCOL));

View File

@ -0,0 +1,552 @@
/*
* This file is distributed as part of the MariaDB Corporation MaxScale. It is free
* software: you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation,
* version 2.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License along with
* this program; if not, write to the Free Software Foundation, Inc., 51
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Copyright MariaDB Corporation Ab 2013-2014
*/
/**
* @file mysql_auth.c
*
* MySQL Authentication module for handling the checking of clients credentials
* in the MySQL protocol.
*
* @verbatim
* Revision History
* Date Who Description
* 02/02/2016 Martin Brampton Initial version
*
* @endverbatim
*/
#include <mysql_auth.h>
#include <mysql_client_server_protocol.h>
static int combined_auth_check(
DCB *dcb,
uint8_t *auth_token,
size_t auth_token_len,
MySQLProtocol *protocol,
char *username,
uint8_t *stage1_hash,
char *database
);
static int mysql_auth_set_client_data(
MYSQL_session *client_data,
MySQLProtocol *protocol,
uint8_t *client_auth_packet,
int client_auth_packet_size);
/**
* @brief Authenticates a MySQL user who is a client to MaxScale.
*
* First call the SSL authentication function, passing the DCB and a boolean
* indicating whether the client is SSL capable. If SSL authentication is
* successful, check whether connection is complete. Fail if we do not have a
* user name. Call other functions to validate the user, reloading the user
* data if the first attempt fails.
*
* @param dcb Request handler DCB connected to the client
* @param buffer Pointer to pointer to buffer containing data from client
* @return Authentication status
* @note Authentication status codes are defined in mysql_client_server_protocol.h
*/
int
mysql_auth_authenticate(DCB *dcb, GWBUF **buffer)
{
MySQLProtocol *protocol = DCB_PROTOCOL(dcb, MySQLProtocol);
MYSQL_session *client_data = (MYSQL_session *)dcb->data;
int auth_ret, ssl_ret;
if (0 != (ssl_ret = ssl_authenticate_client(dcb, client_data->user, mysql_auth_is_client_ssl_capable(dcb))))
{
auth_ret = (SSL_ERROR_CLIENT_NOT_SSL == ssl_ret) ? MYSQL_FAILED_AUTH_SSL : MYSQL_FAILED_AUTH;
}
else if (!ssl_is_connection_healthy(dcb))
{
auth_ret = MYSQL_AUTH_SSL_INCOMPLETE;
}
else if (0 == strlen(client_data->user))
{
auth_ret = MYSQL_FAILED_AUTH;
}
else
{
MXS_DEBUG("Receiving connection from '%s' to database '%s'.",
client_data->user, client_data->db);
auth_ret = combined_auth_check(dcb, client_data->auth_token, client_data->auth_token_len,
protocol, client_data->user, client_data->client_sha1, client_data->db);
/* On failed authentication try to load user table from backend database */
/* Success for service_refresh_users returns 0 */
if (MYSQL_AUTH_SUCCEEDED != auth_ret && 0 == service_refresh_users(dcb->service))
{
auth_ret = combined_auth_check(dcb, client_data->auth_token, client_data->auth_token_len, protocol,
client_data->user, client_data->client_sha1, client_data->db);
}
/* on successful authentication, set user into dcb field */
if (MYSQL_AUTH_SUCCEEDED == auth_ret)
{
dcb->user = strdup(client_data->user);
}
else if (dcb->service->log_auth_warnings)
{
MXS_NOTICE("%s: login attempt for user '%s', authentication failed.",
dcb->service->name, client_data->user);
if (dcb->ipv4.sin_addr.s_addr == 0x0100007F &&
!dcb->service->localhost_match_wildcard_host)
{
MXS_NOTICE("If you have a wildcard grant that covers"
" this address, try adding "
"'localhost_match_wildcard_host=true' for "
"service '%s'. ", dcb->service->name);
}
}
/* let's free the auth_token now */
if (client_data->auth_token)
{
free(client_data->auth_token);
client_data->auth_token = NULL;
}
}
return auth_ret;
}
/**
* @brief Transfer data from the authentication request to the DCB.
*
* The request handler DCB has a field called data that contains protocol
* specific information. This function examines a buffer containing MySQL
* authentication data and puts it into a structure that is referred to
* by the DCB. If the information in the buffer is invalid, then a failure
* code is returned. A call to mysql_auth_set_client_data does the
* detailed work.
*
* @param dcb Request handler DCB connected to the client
* @param buffer Pointer to pointer to buffer containing data from client
* @return Authentication status
* @note Authentication status codes are defined in mysql_client_server_protocol.h
* @see https://dev.mysql.com/doc/internals/en/client-server-protocol.html
*/
int
mysql_auth_set_protocol_data(DCB *dcb, GWBUF *buf)
{
uint8_t *client_auth_packet = GWBUF_DATA(buf);
MySQLProtocol *protocol = NULL;
MYSQL_session *client_data = NULL;
int client_auth_packet_size = 0;
protocol = DCB_PROTOCOL(dcb, MySQLProtocol);
CHK_PROTOCOL(protocol);
if (dcb->data == NULL)
{
if (NULL == (client_data = (MYSQL_session *)calloc(1, sizeof(MYSQL_session))))
{
return MYSQL_FAILED_AUTH;
}
#if defined(SS_DEBUG)
client_data->myses_chk_top = CHK_NUM_MYSQLSES;
client_data->myses_chk_tail = CHK_NUM_MYSQLSES;
#endif
dcb->data = client_data;
}
else
{
client_data = (MYSQL_session *)dcb->data;
}
client_auth_packet_size = gwbuf_length(buf);
/* For clients supporting CLIENT_PROTOCOL_41
* the Handshake Response Packet is:
*
* 4 bytes mysql protocol heade
* 4 bytes capability flags
* 4 max-packet size
* 1 byte character set
* string[23] reserved (all [0])
* ...
* ...
* Note that the fixed elements add up to 36
*/
/* Detect now if there are enough bytes to continue */
if (client_auth_packet_size < (4 + 4 + 4 + 1 + 23))
{
/* Packet is not big enough */
return MYSQL_FAILED_AUTH;
}
return mysql_auth_set_client_data(client_data, protocol, client_auth_packet,
client_auth_packet_size);
}
/**
* @brief Transfer detailed data from the authentication request to the DCB.
*
* The caller has created the data structure pointed to by the DCB, and this
* function fills in the details. If problems are found with the data, the
* return code indicates failure.
*
* @param client_data The data structure for the DCB
* @param protocol The protocol structure for this connection
* @param client_auth_packet The data from the buffer received from client
* @param client_auth_packet size An integer giving the size of the data
* @return Authentication status
* @note Authentication status codes are defined in mysql_client_server_protocol.h
* @see https://dev.mysql.com/doc/internals/en/client-server-protocol.html
*/
static int
mysql_auth_set_client_data(
MYSQL_session *client_data,
MySQLProtocol *protocol,
uint8_t *client_auth_packet,
int client_auth_packet_size)
{
/* The numbers are the fixed elements in the client handshake packet */
int auth_packet_base_size = 4 + 4 + 4 + 1 + 23;
int packet_length_used = 0;
/* Take data from fixed locations first */
memcpy(&protocol->client_capabilities, client_auth_packet + 4, 4);
protocol->charset = 0;
memcpy(&protocol->charset, client_auth_packet + 4 + 4 + 4, 1);
/* Make username and database a null string in case none is provided */
client_data->user[0] = 0;
client_data->db[0] = 0;
/* Make authentication token length 0 and token null in case none is provided */
client_data->auth_token_len = 0;
client_data->auth_token = NULL;
if (client_auth_packet_size > auth_packet_base_size)
{
/* Should have a username */
char *first_letter_of_username = (char *)(client_auth_packet + auth_packet_base_size);
int user_length = strlen(first_letter_of_username);
if (client_auth_packet_size > (auth_packet_base_size + user_length)
&& user_length <= MYSQL_USER_MAXLEN)
{
strcpy(client_data->user, first_letter_of_username);
}
else
{
/* Packet has incomplete or too long username */
return MYSQL_FAILED_AUTH;
}
if (client_auth_packet_size > (auth_packet_base_size + user_length + 1))
{
/* Extra 1 is for the terminating null after user name */
packet_length_used = auth_packet_base_size + user_length + 1;
/* We should find an authentication token next */
/* One byte of packet is the length of authentication token */
memcpy(&client_data->auth_token_len,
client_auth_packet + packet_length_used,
1);
if (client_auth_packet_size >
(packet_length_used + client_data->auth_token_len))
{
/* Packet is large enough for authentication token */
if (NULL != (client_data->auth_token = (uint8_t *)malloc(client_data->auth_token_len)))
{
/* The extra 1 is for the token length byte, just extracted*/
memcpy(client_data->auth_token,
client_auth_packet + auth_packet_base_size + user_length + 1 +1,
client_data->auth_token_len);
}
else
{
/* Failed to allocate space for authentication token string */
return MYSQL_FAILED_AUTH;
}
}
else
{
/* Packet was too small to contain authentication token */
return MYSQL_FAILED_AUTH;
}
packet_length_used += 1 + client_data->auth_token_len;
/*
* Note: some clients may pass empty database, CONNECT_WITH_DB !=0 but database =""
*/
if (GW_MYSQL_CAPABILITIES_CONNECT_WITH_DB &
gw_mysql_get_byte4((uint32_t *)&protocol->client_capabilities)
&& client_auth_packet_size > packet_length_used)
{
char *database = (char *)(client_auth_packet + packet_length_used);
int database_length = strlen(database);
if (client_auth_packet_size >
(packet_length_used + database_length)
&& strlen(database) <= MYSQL_DATABASE_MAXLEN)
{
strcpy(client_data->db, database);
}
else
{
/* Packet is too short to contain database string */
/* or database string in packet is too long */
return MYSQL_FAILED_AUTH;
}
}
}
}
return MYSQL_AUTH_SUCCEEDED;
}
/**
* @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
*/
bool
mysql_auth_is_client_ssl_capable(DCB *dcb)
{
MySQLProtocol *protocol;
protocol = DCB_PROTOCOL(dcb, MySQLProtocol);
return (protocol->client_capabilities & GW_MYSQL_CAPABILITIES_SSL) ? true : false;
}
/**
*
* @brief Check authentication token received against stage1_hash and scramble
*
* @param dcb The current dcb
* @param token The token sent by the client in the authentication request
* @param token_len The token size in bytes
* @param scramble The scramble data sent by the server during handshake
* @param scramble_len The scramble size in bytes
* @param username The current username in the authentication request
* @param stage1_hash The SHA1(candidate_password) decoded by this routine
* @return Authentication status
* @note Authentication status codes are defined in mysql_client_server_protocol.h
*
*/
int
gw_check_mysql_scramble_data(DCB *dcb,
uint8_t *token,
unsigned int token_len,
uint8_t *scramble,
unsigned int scramble_len,
char *username,
uint8_t *stage1_hash)
{
uint8_t step1[GW_MYSQL_SCRAMBLE_SIZE]="";
uint8_t step2[GW_MYSQL_SCRAMBLE_SIZE +1]="";
uint8_t check_hash[GW_MYSQL_SCRAMBLE_SIZE]="";
char hex_double_sha1[2 * GW_MYSQL_SCRAMBLE_SIZE + 1]="";
uint8_t password[GW_MYSQL_SCRAMBLE_SIZE]="";
/* The following can be compared using memcmp to detect a null password */
uint8_t null_client_sha1[MYSQL_SCRAMBLE_LEN]="";
if ((username == NULL) || (scramble == NULL) || (stage1_hash == NULL))
{
return MYSQL_FAILED_AUTH;
}
/*<
* get the user's password from repository in SHA1(SHA1(real_password));
* please note 'real_password' is unknown!
*/
if (gw_find_mysql_user_password_sha1(username, password, dcb))
{
/* if password was sent, fill stage1_hash with at least 1 byte in order
* to create right error message: (using password: YES|NO)
*/
if (token_len)
memcpy(stage1_hash, (char *)"_", 1);
return MYSQL_FAILED_AUTH;
}
if (token && token_len)
{
/*<
* convert in hex format: this is the content of mysql.user table.
* The field password is without the '*' prefix and it is 40 bytes long
*/
gw_bin2hex(hex_double_sha1, password, SHA_DIGEST_LENGTH);
}
else
{
/* check if the password is not set in the user table */
return memcmp(password, null_client_sha1, MYSQL_SCRAMBLE_LEN) ?
MYSQL_FAILED_AUTH : MYSQL_AUTH_SUCCEEDED;
}
/*<
* Auth check in 3 steps
*
* Note: token = XOR (SHA1(real_password), SHA1(CONCAT(scramble, SHA1(SHA1(real_password)))))
* the client sends token
*
* Now, server side:
*
*
* step 1: compute the STEP1 = SHA1(CONCAT(scramble, gateway_password))
* the result in step1 is SHA_DIGEST_LENGTH long
*/
gw_sha1_2_str(scramble, scramble_len, password, SHA_DIGEST_LENGTH, step1);
/*<
* step2: STEP2 = XOR(token, STEP1)
*
* token is transmitted form client and it's based on the handshake scramble and SHA1(real_passowrd)
* step1 has been computed in the previous step
* the result STEP2 is SHA1(the_password_to_check) and is SHA_DIGEST_LENGTH long
*/
gw_str_xor(step2, token, step1, token_len);
/*<
* copy the stage1_hash back to the caller
* stage1_hash will be used for backend authentication
*/
memcpy(stage1_hash, step2, SHA_DIGEST_LENGTH);
/*<
* step 3: prepare the check_hash
*
* compute the SHA1(STEP2) that is SHA1(SHA1(the_password_to_check)), and is SHA_DIGEST_LENGTH long
*/
gw_sha1_str(step2, SHA_DIGEST_LENGTH, check_hash);
#ifdef GW_DEBUG_CLIENT_AUTH
{
char inpass[128]="";
gw_bin2hex(inpass, check_hash, SHA_DIGEST_LENGTH);
fprintf(stderr, "The CLIENT hex(SHA1(SHA1(password))) for \"%s\" is [%s]", username, inpass);
}
#endif
/* now compare SHA1(SHA1(gateway_password)) and check_hash: return 0 is MYSQL_AUTH_OK */
return (0 == memcmp(password, check_hash, SHA_DIGEST_LENGTH)) ?
MYSQL_AUTH_SUCCEEDED : MYSQL_FAILED_AUTH;
}
/**
* @brief If the client connection specifies a database, check existence
*
* The client can specify a default database, but if so, it must be one
* that exists. This function is chained from the previous one, and will
* amend the given return code if it is previously showing success.
*
* @param dcb Request handler DCB connected to the client
* @param database A string containing the database name
* @param auth_ret The authentication status prior to calling this function.
* @return Authentication status
* @note Authentication status codes are defined in mysql_client_server_protocol.h
*/
int
check_db_name_after_auth(DCB *dcb, char *database, int auth_ret)
{
int db_exists = -1;
/* check for database name and possible match in resource hashtable */
if (database && strlen(database))
{
/* if database names are loaded we can check if db name exists */
if (dcb->service->resources != NULL)
{
if (hashtable_fetch(dcb->service->resources, database))
{
db_exists = 1;
}
else
{
db_exists = 0;
}
}
else
{
/* if database names are not loaded we don't allow connection with db name*/
db_exists = -1;
}
if (db_exists == 0 && auth_ret == MYSQL_AUTH_SUCCEEDED)
{
auth_ret = MYSQL_FAILED_AUTH_DB;
}
if (db_exists < 0 && auth_ret == MYSQL_AUTH_SUCCEEDED)
{
auth_ret = MYSQL_FAILED_AUTH;
}
}
return auth_ret;
}
/**
* @brief Function to easily call authentication and database checks.
*
* The two functions are called one after the other, with the return from
* the first passed to the second. For convenience and clarity this function
* combines the calls.
*
* @param dcb Request handler DCB connected to the client
* @param auth_token A string of bytes containing the authentication token
* @param auth_token_len An integer, the length of the preceding parameter
* @param protocol The protocol structure for the connection
* @param username String containing username
* @param stage1_hash A password hash for authentication
* @param database A string containing the name for the default database
* @return Authentication status
* @note Authentication status codes are defined in mysql_client_server_protocol.h
*/
static int combined_auth_check(
DCB *dcb,
uint8_t *auth_token,
size_t auth_token_len,
MySQLProtocol *protocol,
char *username,
uint8_t *stage1_hash,
char *database
)
{
int auth_ret;
auth_ret = gw_check_mysql_scramble_data(dcb,
auth_token,
auth_token_len,
protocol->scramble,
sizeof(protocol->scramble),
username,
stage1_hash);
/* check for database name match in resource hashtable */
auth_ret = check_db_name_after_auth(dcb, database, auth_ret);
return auth_ret;
}

View File

@ -50,6 +50,8 @@
*
*/
#include <modinfo.h>
#include <gw_protocol.h>
#include <mysql_auth.h>
MODULE_INFO info = {
MODULE_API_PROTOCOL,
@ -142,7 +144,7 @@ static bool gw_get_shared_session_auth_info(DCB* dcb, MYSQL_session* session)
if (dcb->session->state != SESSION_STATE_ALLOC &&
dcb->session->state != SESSION_STATE_DUMMY)
{
memcpy(session, dcb->session->data, sizeof(MYSQL_session));
memcpy(session, dcb->session->client_dcb->data, sizeof(MYSQL_session));
}
else
{
@ -174,7 +176,7 @@ static int gw_read_backend_event(DCB *dcb)
goto return_rc;
}
if (dcb->session == NULL)
if (dcb->dcb_is_zombie || dcb->session == NULL)
{
goto return_rc;
}
@ -554,8 +556,8 @@ static int gw_read_backend_event(DCB *dcb)
* still listening the socket for replies.
*/
if (dcb->session->state == SESSION_STATE_ROUTER_READY &&
dcb->session->client != NULL &&
dcb->session->client->state == DCB_STATE_POLLING &&
dcb->session->client_dcb != NULL &&
dcb->session->client_dcb->state == DCB_STATE_POLLING &&
(session->router_session || router->getCapabilities() & RCAP_TYPE_NO_RSESSION))
{
client_protocol = SESSION_PROTOCOL(dcb->session,
@ -576,7 +578,7 @@ static int gw_read_backend_event(DCB *dcb)
}
goto return_rc;
}
else if (dcb->session->client->dcb_role == DCB_ROLE_INTERNAL)
else if (dcb->session->client_dcb->dcb_role == DCB_ROLE_INTERNAL)
{
gwbuf_set_type(read_buffer, GWBUF_TYPE_MYSQL);
router->clientReply(router_instance, session->router_session, read_buffer, dcb);
@ -619,14 +621,14 @@ static int gw_write_backend_event(DCB *dcb)
{
data = (uint8_t *) GWBUF_DATA(dcb->writeq);
if (dcb->session->client == NULL)
if (dcb->session->client_dcb == NULL)
{
rc = 0;
}
else if (!(MYSQL_IS_COM_QUIT(data)))
{
/*< vraa : errorHandle */
mysql_send_custom_error(dcb->session->client,
mysql_send_custom_error(dcb->session->client_dcb,
1,
0,
"Writing to backend failed due invalid Maxscale "
@ -942,14 +944,14 @@ static int gw_create_backend_connection(DCB *backend_dcb,
}
/** Copy client flags to backend protocol */
if (backend_dcb->session->client->protocol)
if (backend_dcb->session->client_dcb->protocol)
{
/** Copy client flags to backend protocol */
protocol->client_capabilities =
((MySQLProtocol *) (backend_dcb->session->client->protocol))->client_capabilities;
((MySQLProtocol *)(backend_dcb->session->client_dcb->protocol))->client_capabilities;
/** Copy client charset to backend protocol */
protocol->charset =
((MySQLProtocol *) (backend_dcb->session->client->protocol))->charset;
((MySQLProtocol *)(backend_dcb->session->client_dcb->protocol))->charset;
}
else
{
@ -976,7 +978,7 @@ static int gw_create_backend_connection(DCB *backend_dcb,
server->name,
server->port,
protocol->fd,
session->client->fd);
session->client_dcb->fd);
break;
case 1:
@ -989,7 +991,7 @@ static int gw_create_backend_connection(DCB *backend_dcb,
server->name,
server->port,
protocol->fd,
session->client->fd);
session->client_dcb->fd);
break;
default:
@ -1001,7 +1003,7 @@ static int gw_create_backend_connection(DCB *backend_dcb,
server->name,
server->port,
protocol->fd,
session->client->fd);
session->client_dcb->fd);
break;
} /*< switch */
@ -1152,7 +1154,7 @@ static int gw_backend_close(DCB *dcb)
CHK_SESSION(session);
/**
* The lock is needed only to protect the read of session->state and
* session->client values. Client's state may change by other thread
* session->client_dcb values. Client's state may change by other thread
* but client's close and adding client's DCB to zombies list is executed
* only if client's DCB's state does _not_ change in parallel.
*/
@ -1162,17 +1164,15 @@ static int gw_backend_close(DCB *dcb)
* Otherwise only this backend connection is closed.
*/
if (session->state == SESSION_STATE_STOPPING &&
session->client != NULL)
session->client_dcb != NULL)
{
if (session->client->state == DCB_STATE_POLLING)
if (session->client_dcb->state == DCB_STATE_POLLING)
{
DCB *temp;
spinlock_release(&session->ses_lock);
/** Close client DCB */
temp = session->client;
session->client = NULL;
dcb_close(temp);
dcb_close(session->client_dcb);
}
else
{
@ -1330,9 +1330,9 @@ static int gw_change_user(DCB *backend,
int rv = -1;
int auth_ret = 1;
current_session = (MYSQL_session *)in_session->client->data;
current_session = (MYSQL_session *)in_session->client_dcb->data;
backend_protocol = backend->protocol;
client_protocol = in_session->client->protocol;
client_protocol = in_session->client_dcb->protocol;
/* now get the user, after 4 bytes header and 1 byte command */
client_auth_packet += 5;
@ -1391,7 +1391,7 @@ static int gw_change_user(DCB *backend,
* Decode the token and check the password.
* Note: if auth_token_len == 0 && auth_token == NULL, user is without password
*/
auth_ret = gw_check_mysql_scramble_data(backend->session->client,
auth_ret = gw_check_mysql_scramble_data(backend->session->client_dcb,
auth_token, auth_token_len,
client_protocol->scramble,
sizeof(client_protocol->scramble),
@ -1401,14 +1401,14 @@ static int gw_change_user(DCB *backend,
if (auth_ret != 0)
{
if (service_refresh_users(backend->session->client->service) == 0)
if (service_refresh_users(backend->session->client_dcb->service) == 0)
{
/* Try authentication again with new repository data */
/* Note: if no auth client authentication will fail */
spinlock_acquire(&in_session->ses_lock);
strncpy(current_session->db, "", MYSQL_DATABASE_MAXLEN);
auth_ret = gw_check_mysql_scramble_data(
backend->session->client,
backend->session->client_dcb,
auth_token, auth_token_len,
client_protocol->scramble,
sizeof(client_protocol->scramble),
@ -1447,7 +1447,7 @@ static int gw_change_user(DCB *backend,
* reply to the client.
*/
message = create_auth_fail_str(username,
backend->session->client->remote,
backend->session->client_dcb->remote,
password_set,
"",
auth_ret);

View File

@ -46,10 +46,14 @@
* 09/09/2015 Martin Brampton Modify error handler calls
* 11/01/2016 Martin Brampton Remove SSL write code, now handled at lower level;
* replace gwbuf_consume by gwbuf_free (multiple).
* 07/02/2016 Martin Brampton Split off authentication and SSL.
*/
#include <gw_protocol.h>
#include <skygw_utils.h>
#include <log_manager.h>
#include <mysql_client_server_protocol.h>
#include <mysql_auth.h>
#include <gw_ssl.h>
#include <maxscale/poll.h>
#include <gw.h>
#include <modinfo.h>
@ -75,17 +79,12 @@ static int gw_MySQLWrite_client(DCB *dcb, GWBUF *queue);
static int gw_error_client_event(DCB *dcb);
static int gw_client_close(DCB *dcb);
static int gw_client_hangup_event(DCB *dcb);
int gw_read_client_event_SSL(DCB* dcb);
int mysql_send_ok(DCB *dcb, int packet_number, int in_affected_rows, const char* mysql_message);
int MySQLSendHandshake(DCB* dcb);
static int gw_mysql_do_authentication(DCB *dcb, GWBUF **queue);
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 **);
extern char* get_username_from_auth(char* ptr, uint8_t* data);
extern int check_db_name_after_auth(DCB *, char *, int);
static void mysql_client_auth_error_handling(DCB *dcb, int auth_val);
extern char* create_auth_fail_str(char *username, char *hostaddr, char *sha1, char *db,int);
int do_ssl_accept(MySQLProtocol* protocol);
/*
* The "module object" for the mysqld client protocol module.
*/
@ -336,14 +335,10 @@ int MySQLSendHandshake(DCB* dcb)
mysql_server_capabilities_one[0] &= ~GW_MYSQL_CAPABILITIES_COMPRESS;
if (dcb->service->ssl_mode != SSL_DISABLED)
if (ssl_required_by_dcb(dcb))
{
mysql_server_capabilities_one[1] |= GW_MYSQL_CAPABILITIES_SSL >> 8;
}
else
{
mysql_server_capabilities_one[0] &= ~GW_MYSQL_CAPABILITIES_SSL;
}
memcpy(mysql_handshake_payload, mysql_server_capabilities_one, sizeof(mysql_server_capabilities_one));
mysql_handshake_payload = mysql_handshake_payload + sizeof(mysql_server_capabilities_one);
@ -396,246 +391,6 @@ int MySQLSendHandshake(DCB* dcb)
return sizeof(mysql_packet_header) + mysql_payload_size;
}
/**
* gw_mysql_do_authentication
*
* Performs the MySQL protocol 4.1 authentication, using data in GWBUF **queue.
*
* (MYSQL_session*)client_data including: user, db, client_sha1 are copied into
* the dcb->data and later to dcb->session->data. client_capabilities are copied
* into the dcb->protocol.
*
* If SSL is enabled for the service, the SSL handshake will be done before the
* MySQL authentication.
*
* @param dcb Descriptor Control Block of the client
* @param queue Pointer to the location of the GWBUF with data from client
* @return 0 If succeed, otherwise non-zero value
*
* @note in case of failure, dcb->data is freed before returning. If succeed,
* dcb->data is freed in session.c:session_free.
*/
static int gw_mysql_do_authentication(DCB *dcb, GWBUF **buf)
{
GWBUF* queue = *buf;
MySQLProtocol *protocol = NULL;
/* int compress = -1; */
int connect_with_db = -1;
uint8_t *client_auth_packet = GWBUF_DATA(queue);
int client_auth_packet_size = 0;
char *username = NULL;
char *database = NULL;
unsigned int auth_token_len = 0;
uint8_t *auth_token = NULL;
uint8_t *stage1_hash = NULL;
int auth_ret = -1;
MYSQL_session *client_data = NULL;
int ssl = 0;
CHK_DCB(dcb);
protocol = DCB_PROTOCOL(dcb, MySQLProtocol);
CHK_PROTOCOL(protocol);
if (dcb->data == NULL)
{
client_data = (MYSQL_session *)calloc(1, sizeof(MYSQL_session));
#if defined(SS_DEBUG)
client_data->myses_chk_top = CHK_NUM_MYSQLSES;
client_data->myses_chk_tail = CHK_NUM_MYSQLSES;
#endif
dcb->data = client_data;
}
else
{
client_data = (MYSQL_session *)dcb->data;
}
stage1_hash = client_data->client_sha1;
username = client_data->user;
client_auth_packet_size = gwbuf_length(queue);
/* For clients supporting CLIENT_PROTOCOL_41
* the Handshake Response Packet is:
*
* 4 bytes mysql protocol heade
* 4 bytes capability flags
* 4 max-packet size
* 1 byte character set
* string[23] reserved (all [0])
* ...
* ...
*/
/* Detect now if there are enough bytes to continue */
if (client_auth_packet_size < (4 + 4 + 4 + 1 + 23))
{
return MYSQL_FAILED_AUTH;
}
memcpy(&protocol->client_capabilities, client_auth_packet + 4, 4);
connect_with_db =
GW_MYSQL_CAPABILITIES_CONNECT_WITH_DB &
gw_mysql_get_byte4((uint32_t *)&protocol->client_capabilities);
/*
compress =
GW_MYSQL_CAPABILITIES_COMPRESS & gw_mysql_get_byte4(
&protocol->client_capabilities);
*/
/** Skip this if the SSL handshake is already done.
* If not, start the SSL handshake. */
if (protocol->protocol_auth_state != MYSQL_AUTH_SSL_HANDSHAKE_DONE)
{
ssl = protocol->client_capabilities & GW_MYSQL_CAPABILITIES_SSL;
/** Client didn't requested SSL when SSL mode was required*/
if (!ssl && protocol->owner_dcb->service->ssl_mode == SSL_REQUIRED)
{
MXS_INFO("User %s@%s connected to service '%s' without SSL when SSL was required.",
protocol->owner_dcb->user,
protocol->owner_dcb->remote,
protocol->owner_dcb->service->name);
return MYSQL_FAILED_AUTH_SSL;
}
if (MXS_LOG_PRIORITY_IS_ENABLED(LOG_INFO) && ssl)
{
MXS_INFO("User %s@%s connected to service '%s' with SSL.",
protocol->owner_dcb->user,
protocol->owner_dcb->remote,
protocol->owner_dcb->service->name);
}
/** Do the SSL Handshake */
if (ssl && protocol->owner_dcb->service->ssl_mode != SSL_DISABLED)
{
protocol->protocol_auth_state = MYSQL_AUTH_SSL_REQ;
if (do_ssl_accept(protocol) < 0)
{
return MYSQL_FAILED_AUTH;
}
else
{
return 0;
}
}
else if (dcb->service->ssl_mode == SSL_ENABLED)
{
/** This is a non-SSL connection to a SSL enabled service.
* We have only read enough of the packet to know that the client
* is not requesting SSL and the rest of the auth packet is still
* waiting in the socket. We need to read the data from the socket
* to find out the username of the connecting client. */
int bytes = dcb_read(dcb,&queue, 0);
queue = gwbuf_make_contiguous(queue);
client_auth_packet = GWBUF_DATA(queue);
client_auth_packet_size = gwbuf_length(queue);
*buf = queue;
MXS_DEBUG("%lu Read %d bytes from fd %d",pthread_self(),bytes,dcb->fd);
}
}
username = get_username_from_auth(username, client_auth_packet);
if (username == NULL)
{
return MYSQL_FAILED_AUTH;
}
/* get charset */
memcpy(&protocol->charset, client_auth_packet + 4 + 4 + 4, sizeof (int));
/* get the auth token len */
memcpy(&auth_token_len,
client_auth_packet + 4 + 4 + 4 + 1 + 23 + strlen(username) + 1,
1);
/*
* Note: some clients may pass empty database, connect_with_db !=0 but database =""
*/
if (connect_with_db)
{
database = client_data->db;
strncpy(database,
(char *)(client_auth_packet + 4 + 4 + 4 + 1 + 23 + strlen(username) +
1 + 1 + auth_token_len), MYSQL_DATABASE_MAXLEN);
}
/* allocate memory for token only if auth_token_len > 0 */
if (auth_token_len)
{
auth_token = (uint8_t *)malloc(auth_token_len);
memcpy(auth_token,
client_auth_packet + 4 + 4 + 4 + 1 + 23 + strlen(username) + 1 + 1,
auth_token_len);
}
/*
* Decode the token and check the password
* Note: if auth_token_len == 0 && auth_token == NULL, user is without password
*/
MXS_DEBUG("Receiving connection from '%s' to database '%s'.",username,database);
auth_ret = gw_check_mysql_scramble_data(dcb,
auth_token,
auth_token_len,
protocol->scramble,
sizeof(protocol->scramble),
username,
stage1_hash);
/* check for database name match in resource hashtable */
auth_ret = check_db_name_after_auth(dcb, database, auth_ret);
/* On failed auth try to load users' table from backend database */
if (auth_ret != 0)
{
if (!service_refresh_users(dcb->service))
{
/* Try authentication again with new repository data */
/* Note: if no auth client authentication will fail */
auth_ret = gw_check_mysql_scramble_data(dcb,
auth_token,
auth_token_len,
protocol->scramble,
sizeof(protocol->scramble),
username,
stage1_hash);
/* Do again the database check */
auth_ret = check_db_name_after_auth(dcb, database, auth_ret);
}
}
/* on succesful auth set user into dcb field */
if (auth_ret == 0)
{
dcb->user = strdup(client_data->user);
}
else if (dcb->service->log_auth_warnings)
{
MXS_NOTICE("%s: login attempt for user '%s', authentication failed.",
dcb->service->name, username);
if (dcb->ipv4.sin_addr.s_addr == 0x0100007F &&
!dcb->service->localhost_match_wildcard_host)
{
MXS_NOTICE("If you have a wildcard grant that covers"
" this address, try adding "
"'localhost_match_wildcard_host=true' for "
"service '%s'. ", dcb->service->name);
}
}
/* let's free the auth_token now */
if (auth_token)
{
free(auth_token);
}
return auth_ret;
}
/**
* Write function for client DCB: writes data from MaxScale to Client
*
@ -648,23 +403,18 @@ int gw_MySQLWrite_client(DCB *dcb, GWBUF *queue)
}
/**
* Client read event triggered by EPOLLIN
* @brief Client read event triggered by EPOLLIN
*
* @param dcb Descriptor control block
* @return 0 if succeed, 1 otherwise
*/
int gw_read_client_event(DCB* dcb)
{
SESSION *session = NULL;
ROUTER_OBJECT *router = NULL;
ROUTER *router_instance = NULL;
void *rsession = NULL;
MySQLProtocol *protocol = NULL;
GWBUF *read_buffer = NULL;
int rc = 0;
int nbytes_read = 0;
uint8_t cap = 0;
bool stmt_input = false; /*< router input type */
int max_bytes = 0;
CHK_DCB(dcb);
protocol = DCB_PROTOCOL(dcb, MySQLProtocol);
@ -676,73 +426,149 @@ int gw_read_client_event(DCB* dcb)
#endif
/** SSL authentication is still going on, we need to call do_ssl_accept
* until it return 1 for success or -1 for error */
if (protocol->protocol_auth_state == MYSQL_AUTH_SSL_HANDSHAKE_ONGOING ||
protocol->protocol_auth_state == MYSQL_AUTH_SSL_REQ)
/**
* The use of max_bytes seems like a hack, but no better option is available
* at the time of writing. When a MySQL server receives a new connection
* request, it sends an Initial Handshake Packet. Where the client wants to
* use SSL, it responds with an SSL Request Packet (in place of a Handshake
* Response Packet). The SSL Request Packet contains only the basic header,
* and not the user credentials. It is 36 bytes long. The server then
* initiates the SSL handshake (via calls to OpenSSL).
*
* In many cases, this is what happens. But occasionally, the client seems
* to send a packet much larger than 36 bytes (in tests it was 333 bytes).
* If the whole of the packet is read, it is then lost to the SSL handshake
* process. Why this happens is presently unknown. Reading just 36 bytes
* when the server requires SSL and SSL has not yet been negotiated seems
* to solve the problem.
*
* If a neater solution can be found, so much the better.
*/
if (ssl_required_but_not_negotiated(dcb))
{
switch(do_ssl_accept(protocol))
{
case 0:
return 0;
break;
case 1:
{
int b = 0;
ioctl(dcb->fd,FIONREAD,&b);
if (b == 0)
{
MXS_DEBUG("[gw_read_client_event] No data in socket after SSL auth");
return 0;
}
}
break;
case -1:
return 1;
break;
default:
return 1;
break;
}
max_bytes = 36;
}
if (protocol->use_ssl)
{
/** SSL handshake is done, communication is now encrypted with SSL */
rc = dcb_read_SSL(dcb, &read_buffer);
}
else if (dcb->service->ssl_mode != SSL_DISABLED &&
protocol->protocol_auth_state == MYSQL_AUTH_SENT)
{
/** The service allows both SSL and non-SSL connections.
* read only enough of the auth packet to know if the client is
* requesting SSL. If the client is not requesting SSL the rest of
the auth packet will be read later. */
rc = dcb_read(dcb, &read_buffer,(4 + 4 + 4 + 1 + 23));
}
else
{
/** Normal non-SSL connection */
rc = dcb_read(dcb, &read_buffer, 0);
}
rc = dcb_read(dcb, &read_buffer, max_bytes);
if (rc < 0)
{
dcb_close(dcb);
}
nbytes_read = gwbuf_length(read_buffer);
if (nbytes_read == 0)
if (0 == (nbytes_read = gwbuf_length(read_buffer)))
{
goto return_rc;
}
session = dcb->session;
switch (protocol->protocol_auth_state)
{
/**
*
* When a listener receives a new connection request, it creates a
* request handler DCB to for the client connection. The listener also
* sends the initial authentication request to the client. The first
* time this function is called from the poll loop, the client reply
* to the authentication request should be available.
*
* If the authentication is successful the protocol authentication state
* will be changed to MYSQL_IDLE (see below).
*
*/
case MYSQL_AUTH_SENT:
{
/* int compress = -1; */
int auth_val, packet_number;
MySQLProtocol *protocol = DCB_PROTOCOL(dcb, MySQLProtocol);
if (protocol->protocol_auth_state == MYSQL_IDLE && session != NULL &&
SESSION_STATE_DUMMY != session->state)
packet_number = ssl_required_by_dcb(dcb) ? 3 : 2;
/**
* The first step in the authentication process is to extract the
* relevant information from the buffer supplied and place it
* into a data structure pointed to by the DCB. The "success"
* result is not final, it implies only that the process is so
* far successful, not that authentication has completed. If the
* data extraction succeeds, then a call is made to
* mysql_auth_authenticate to carry out the actual user checks.
*/
if (MYSQL_AUTH_SUCCEEDED == (
auth_val = mysql_auth_set_protocol_data(dcb, read_buffer)))
{
/*
compress =
GW_MYSQL_CAPABILITIES_COMPRESS & gw_mysql_get_byte4(
&protocol->client_capabilities);
*/
auth_val = mysql_auth_authenticate(dcb, &read_buffer);
}
/**
* At this point, if the auth_val return code indicates success
* the user authentication has been successfully completed.
* But in order to have a working connection, a session has to
* be created. Provided that is successful (indicated by a
* non-null session) then the whole process has succeeded. In all
* other cases an error return is made.
*/
if (MYSQL_AUTH_SUCCEEDED == auth_val)
{
SESSION *session;
protocol->protocol_auth_state = MYSQL_AUTH_RECV;
/**
* Create session, and a router session for it.
* If successful, there will be backend connection(s)
* after this point.
*/
session = session_alloc(dcb->service, dcb);
if (session != NULL)
{
CHK_SESSION(session);
ss_dassert(session->state != SESSION_STATE_ALLOC &&
session->state != SESSION_STATE_DUMMY);
protocol->protocol_auth_state = MYSQL_IDLE;
/**
* Send an AUTH_OK packet to the client,
* packet sequence is # packet_number
*/
mysql_send_ok(dcb, packet_number, 0, NULL);
}
else
{
auth_val = MYSQL_AUTH_NO_SESSION;
}
}
if (MYSQL_AUTH_SUCCEEDED != auth_val && MYSQL_AUTH_SSL_INCOMPLETE != auth_val)
{
protocol->protocol_auth_state = MYSQL_AUTH_FAILED;
mysql_client_auth_error_handling(dcb, auth_val);
/**
* Close DCB and which will release MYSQL_session
*/
dcb_close(dcb);
}
/* One way or another, the buffer is now fully processed */
gwbuf_free(read_buffer);
read_buffer = NULL;
}
break;
/**
*
* Once a client connection is authenticated, the protocol authentication
* state will be MYSQL_IDLE and so every event of data received will
* result in a call that comes to this section of code.
*
*/
case MYSQL_IDLE:
{
ROUTER_OBJECT *router = NULL;
ROUTER *router_instance = NULL;
void *rsession = NULL;
uint8_t cap = 0;
bool stmt_input = false; /*< router input type */
SESSION *session = dcb->session;
if (session != NULL && SESSION_STATE_DUMMY != session->state)
{
CHK_SESSION(session);
router = session->service->router;
@ -831,224 +657,6 @@ int gw_read_client_event(DCB* dcb)
/**
* Now there should be at least one complete mysql packet in read_buffer.
*/
switch (protocol->protocol_auth_state)
{
case MYSQL_AUTH_SENT:
{
read_buffer = gwbuf_make_contiguous(read_buffer);
int auth_val = gw_mysql_do_authentication(dcb, &read_buffer);
if (protocol->protocol_auth_state == MYSQL_AUTH_SSL_REQ ||
protocol->protocol_auth_state == MYSQL_AUTH_SSL_HANDSHAKE_ONGOING ||
protocol->protocol_auth_state == MYSQL_AUTH_SSL_HANDSHAKE_DONE ||
protocol->protocol_auth_state == MYSQL_AUTH_SSL_HANDSHAKE_FAILED)
{
/** SSL was requested and the handshake is either done or
* still ongoing. After the handshake is done, the client
* will send another auth packet. */
gwbuf_free(read_buffer);
read_buffer = NULL;
break;
}
if (auth_val == 0)
{
SESSION *session;
protocol->protocol_auth_state = MYSQL_AUTH_RECV;
/**
* Create session, and a router session for it.
* If successful, there will be backend connection(s)
* after this point.
*/
session = session_alloc(dcb->service, dcb);
if (session != NULL)
{
CHK_SESSION(session);
ss_dassert(session->state != SESSION_STATE_ALLOC &&
session->state != SESSION_STATE_DUMMY);
protocol->protocol_auth_state = MYSQL_IDLE;
/**
* Send an AUTH_OK packet to the client,
* packet sequence is # 2
*/
mysql_send_ok(dcb, 2, 0, NULL);
}
else
{
protocol->protocol_auth_state = MYSQL_AUTH_FAILED;
MXS_DEBUG("%lu [gw_read_client_event] session "
"creation failed. fd %d, "
"state = MYSQL_AUTH_FAILED.",
pthread_self(),
protocol->owner_dcb->fd);
/** Send ERR 1045 to client */
mysql_send_auth_error(dcb,
2,
0,
"failed to create new session");
dcb_close(dcb);
}
}
else
{
char* fail_str = NULL;
protocol->protocol_auth_state = MYSQL_AUTH_FAILED;
if (auth_val == 2)
{
/** Send error 1049 to client */
int message_len = 25 + MYSQL_DATABASE_MAXLEN;
fail_str = calloc(1, message_len+1);
snprintf(fail_str, message_len, "Unknown database '%s'",
(char*)((MYSQL_session *)dcb->data)->db);
modutil_send_mysql_err_packet(dcb, 2, 0, 1049, "42000", fail_str);
}
else
{
/** Send error 1045 to client */
fail_str = create_auth_fail_str((char *)((MYSQL_session *)dcb->data)->user,
dcb->remote,
(char*)((MYSQL_session *)dcb->data)->client_sha1,
(char*)((MYSQL_session *)dcb->data)->db,auth_val);
modutil_send_mysql_err_packet(dcb, 2, 0, 1045, "28000", fail_str);
}
if (fail_str)
{
free(fail_str);
}
MXS_DEBUG("%lu [gw_read_client_event] after "
"gw_mysql_do_authentication, fd %d, "
"state = MYSQL_AUTH_FAILED.",
pthread_self(),
protocol->owner_dcb->fd);
/**
* Release MYSQL_session since it is not used anymore.
*/
if (!DCB_IS_CLONE(dcb))
{
free(dcb->data);
}
dcb->data = NULL;
dcb_close(dcb);
}
gwbuf_free(read_buffer);
read_buffer = NULL;
}
break;
case MYSQL_AUTH_SSL_HANDSHAKE_DONE:
{
int auth_val;
auth_val = gw_mysql_do_authentication(dcb, &read_buffer);
if (auth_val == 0)
{
SESSION *session;
protocol->protocol_auth_state = MYSQL_AUTH_RECV;
/**
* Create session, and a router session for it.
* If successful, there will be backend connection(s)
* after this point.
*/
session = session_alloc(dcb->service, dcb);
if (session != NULL)
{
CHK_SESSION(session);
ss_dassert(session->state != SESSION_STATE_ALLOC &&
session->state != SESSION_STATE_DUMMY);
protocol->protocol_auth_state = MYSQL_IDLE;
/**
* Send an AUTH_OK packet to the client,
* packet sequence is # 2
*/
mysql_send_ok(dcb, 3, 0, NULL);
}
else
{
protocol->protocol_auth_state = MYSQL_AUTH_FAILED;
MXS_DEBUG("%lu [gw_read_client_event] session "
"creation failed. fd %d, "
"state = MYSQL_AUTH_FAILED.",
pthread_self(),
protocol->owner_dcb->fd);
/** Send ERR 1045 to client */
mysql_send_auth_error(dcb,
3,
0,
"failed to create new session");
dcb_close(dcb);
}
}
else
{
char* fail_str = NULL;
protocol->protocol_auth_state = MYSQL_AUTH_FAILED;
if (auth_val == 2)
{
/** Send error 1049 to client */
int message_len = 25 + MYSQL_DATABASE_MAXLEN;
fail_str = calloc(1, message_len+1);
snprintf(fail_str, message_len, "Unknown database '%s'",
(char*)((MYSQL_session *)dcb->data)->db);
modutil_send_mysql_err_packet(dcb, 3, 0, 1049, "42000", fail_str);
}
else
{
/** Send error 1045 to client */
fail_str = create_auth_fail_str((char *)((MYSQL_session *)dcb->data)->user,
dcb->remote,
(char*)((MYSQL_session *)dcb->data)->client_sha1,
(char*)((MYSQL_session *)dcb->data)->db,auth_val);
modutil_send_mysql_err_packet(dcb, 3, 0, 1045, "28000", fail_str);
}
if (fail_str)
{
free(fail_str);
}
MXS_DEBUG("%lu [gw_read_client_event] after "
"gw_mysql_do_authentication, fd %d, "
"state = MYSQL_AUTH_FAILED.",
pthread_self(),
protocol->owner_dcb->fd);
/**
* Release MYSQL_session since it is not used anymore.
*/
if (!DCB_IS_CLONE(dcb))
{
free(dcb->data);
}
dcb->data = NULL;
dcb_close(dcb);
}
gwbuf_free(read_buffer);
read_buffer = NULL;
}
break;
case MYSQL_IDLE:
{
uint8_t* payload = NULL;
session_state_t ses_state;
@ -1189,6 +797,98 @@ return_rc:
return rc;
}
/**
* @brief Analyse authentication errors and write appropriate log messages
*
* @param dcb Request handler DCB connected to the client
* @param auth_val The type of authentication failure
* @note Authentication status codes are defined in mysql_client_server_protocol.h
*/
static void
mysql_client_auth_error_handling(DCB *dcb, int auth_val)
{
int packet_number, message_len;
char *fail_str = NULL;
packet_number = ssl_required_by_dcb(dcb) ? 3 : 2;
switch (auth_val)
{
case MYSQL_AUTH_NO_SESSION:
MXS_DEBUG("%lu [gw_read_client_event] session "
"creation failed. fd %d, "
"state = MYSQL_AUTH_NO_SESSION.",
pthread_self(),
dcb->fd);
/** Send ERR 1045 to client */
mysql_send_auth_error(dcb,
packet_number,
0,
"failed to create new session");
case MYSQL_FAILED_AUTH_DB:
MXS_DEBUG("%lu [gw_read_client_event] database "
"specified was not valid. fd %d, "
"state = MYSQL_FAILED_AUTH_DB.",
pthread_self(),
dcb->fd);
/** Send error 1049 to client */
message_len = 25 + MYSQL_DATABASE_MAXLEN;
fail_str = calloc(1, message_len+1);
snprintf(fail_str, message_len, "Unknown database '%s'",
(char*)((MYSQL_session *)dcb->data)->db);
modutil_send_mysql_err_packet(dcb, packet_number, 0, 1049, "42000", fail_str);
case MYSQL_FAILED_AUTH_SSL:
MXS_DEBUG("%lu [gw_read_client_event] client is "
"not SSL capable for SSL listener. fd %d, "
"state = MYSQL_FAILED_AUTH_SSL.",
pthread_self(),
dcb->fd);
/** Send ERR 1045 to client */
mysql_send_auth_error(dcb,
packet_number,
0,
"failed to complete SSL authentication");
case MYSQL_AUTH_SSL_INCOMPLETE:
MXS_DEBUG("%lu [gw_read_client_event] unable to "
"complete SSL authentication. fd %d, "
"state = MYSQL_AUTH_SSL_INCOMPLETE.",
pthread_self(),
dcb->fd);
/** Send ERR 1045 to client */
mysql_send_auth_error(dcb,
packet_number,
0,
"failed to complete SSL authentication");
case MYSQL_FAILED_AUTH:
MXS_DEBUG("%lu [gw_read_client_event] authentication failed. fd %d, "
"state = MYSQL_FAILED_AUTH.",
pthread_self(),
dcb->fd);
/** Send error 1045 to client */
fail_str = create_auth_fail_str((char *)((MYSQL_session *)dcb->data)->user,
dcb->remote,
(char*)((MYSQL_session *)dcb->data)->client_sha1,
(char*)((MYSQL_session *)dcb->data)->db, auth_val);
modutil_send_mysql_err_packet(dcb, packet_number, 0, 1045, "28000", fail_str);
default:
MXS_DEBUG("%lu [gw_read_client_event] authentication failed. fd %d, "
"state unrecognized.",
pthread_self(),
dcb->fd);
/** Send error 1045 to client */
fail_str = create_auth_fail_str((char *)((MYSQL_session *)dcb->data)->user,
dcb->remote,
(char*)((MYSQL_session *)dcb->data)->client_sha1,
(char*)((MYSQL_session *)dcb->data)->db, auth_val);
modutil_send_mysql_err_packet(dcb, packet_number, 0, 1045, "28000", fail_str);
}
free(fail_str);
}
///////////////////////////////////////////////
// client write event to Client triggered by EPOLLOUT
@ -1588,6 +1288,7 @@ int gw_MySQLAccept(DCB *listener)
goto return_rc;
}
client_dcb->listen_ssl = listener->listen_ssl;
client_dcb->service = listener->session->service;
client_dcb->session = session_set_dummy(client_dcb);
client_dcb->fd = c_sock;
@ -1871,71 +1572,3 @@ static int route_by_statement(SESSION* session, GWBUF** p_readbuf)
return_rc:
return rc;
}
/**
* Do the SSL authentication handshake.
* This creates the DCB SSL structure if one has not been created and starts the
* SSL handshake handling.
* @param protocol Protocol to connect with SSL
* @return 1 on success, 0 when the handshake is ongoing or -1 on error
*/
int do_ssl_accept(MySQLProtocol* protocol)
{
int rval,errnum;
char errbuf[2014];
DCB* dcb = protocol->owner_dcb;
if (dcb->ssl == NULL)
{
if (dcb_create_SSL(dcb) != 0)
{
return -1;
}
}
rval = dcb_accept_SSL(dcb);
switch(rval)
{
case 0:
/** Not all of the data has been read. Go back to the poll
queue and wait for more.*/
rval = 0;
MXS_INFO("SSL_accept ongoing for %s@%s",
protocol->owner_dcb->user,
protocol->owner_dcb->remote);
return 0;
break;
case 1:
spinlock_acquire(&protocol->protocol_lock);
protocol->protocol_auth_state = MYSQL_AUTH_SSL_HANDSHAKE_DONE;
protocol->use_ssl = true;
spinlock_release(&protocol->protocol_lock);
rval = 1;
MXS_INFO("SSL_accept done for %s@%s",
protocol->owner_dcb->user,
protocol->owner_dcb->remote);
break;
case -1:
spinlock_acquire(&protocol->protocol_lock);
protocol->protocol_auth_state = MYSQL_AUTH_SSL_HANDSHAKE_FAILED;
spinlock_release(&protocol->protocol_lock);
rval = -1;
MXS_ERROR("Fatal error in SSL_accept for %s",
protocol->owner_dcb->remote);
break;
default:
MXS_ERROR("Fatal error in SSL_accept, returned value was %d.", rval);
break;
}
#ifdef SS_DEBUG
MXS_DEBUG("[do_ssl_accept] Protocol state: %s",
gw_mysql_protocol_state2string(protocol->protocol_auth_state));
#endif
return rval;
}

View File

@ -41,7 +41,8 @@
* 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/15 Martin Brampton Fix problem recognising null password
* 07/07/2015 Martin Brampton Fix problem recognising null password
* 07/02/2016 Martin Brampton Remove authentication functions to mysql_auth.c
*
*/
@ -59,7 +60,6 @@ extern int gw_read_backend_event(DCB* dcb);
extern int gw_write_backend_event(DCB *dcb);
extern int gw_MySQLWrite_backend(DCB *dcb, GWBUF *queue);
extern int gw_error_backend_event(DCB *dcb);
char* get_username_from_auth(char* ptr, uint8_t* data);
static server_command_t* server_command_init(server_command_t* srvcmd, mysql_server_cmd_t cmd);
@ -1282,7 +1282,7 @@ int gw_send_change_user_to_backend(char *dbname,
int rc;
MYSQL_session* mses;
mses = (MYSQL_session*)conn->owner_dcb->session->client->data;
mses = (MYSQL_session*)conn->owner_dcb->session->client_dcb->data;
buffer = gw_create_change_user_packet(mses, conn);
rc = conn->owner_dcb->func.write(conn->owner_dcb, buffer);
@ -1293,132 +1293,6 @@ int gw_send_change_user_to_backend(char *dbname,
return rc;
}
/**
* gw_check_mysql_scramble_data
*
* Check authentication token received against stage1_hash and scramble
*
* @param dcb The current dcb
* @param token The token sent by the client in the authentication request
* @param token_len The token size in bytes
* @param scramble The scramble data sent by the server during handshake
* @param scramble_len The scrable size in bytes
* @param username The current username in the authentication request
* @param stage1_hash The SHA1(candidate_password) decoded by this routine
* @return 0 on succesful check or 1 on failure
*
*/
int gw_check_mysql_scramble_data(DCB *dcb,
uint8_t *token,
unsigned int token_len,
uint8_t *scramble,
unsigned int scramble_len,
char *username,
uint8_t *stage1_hash)
{
uint8_t step1[GW_MYSQL_SCRAMBLE_SIZE]="";
uint8_t step2[GW_MYSQL_SCRAMBLE_SIZE +1]="";
uint8_t check_hash[GW_MYSQL_SCRAMBLE_SIZE]="";
char hex_double_sha1[2 * GW_MYSQL_SCRAMBLE_SIZE + 1]="";
uint8_t password[GW_MYSQL_SCRAMBLE_SIZE]="";
int ret_val = 1;
if ((username == NULL) || (scramble == NULL) || (stage1_hash == NULL))
{
return 1;
}
/*<
* get the user's password from repository in SHA1(SHA1(real_password));
* please note 'real_password' is unknown!
*/
ret_val = gw_find_mysql_user_password_sha1(username, password, dcb);
if (ret_val)
{
/* if password was sent, fill stage1_hash with at least 1 byte in order
* to create right error message: (using password: YES|NO)
*/
if (token_len)
memcpy(stage1_hash, (char *)"_", 1);
return 1;
}
if (token && token_len)
{
/*<
* convert in hex format: this is the content of mysql.user table.
* The field password is without the '*' prefix and it is 40 bytes long
*/
gw_bin2hex(hex_double_sha1, password, SHA_DIGEST_LENGTH);
}
else
{
/* check if the password is not set in the user table */
return memcmp(password, null_client_sha1, MYSQL_SCRAMBLE_LEN) ? 1 : 0;
}
/*<
* Auth check in 3 steps
*
* Note: token = XOR (SHA1(real_password), SHA1(CONCAT(scramble, SHA1(SHA1(real_password)))))
* the client sends token
*
* Now, server side:
*
*
* step 1: compute the STEP1 = SHA1(CONCAT(scramble, gateway_password))
* the result in step1 is SHA_DIGEST_LENGTH long
*/
gw_sha1_2_str(scramble, scramble_len, password, SHA_DIGEST_LENGTH, step1);
/*<
* step2: STEP2 = XOR(token, STEP1)
*
* token is transmitted form client and it's based on the handshake scramble and SHA1(real_passowrd)
* step1 has been computed in the previous step
* the result STEP2 is SHA1(the_password_to_check) and is SHA_DIGEST_LENGTH long
*/
gw_str_xor(step2, token, step1, token_len);
/*<
* copy the stage1_hash back to the caller
* stage1_hash will be used for backend authentication
*/
memcpy(stage1_hash, step2, SHA_DIGEST_LENGTH);
/*<
* step 3: prepare the check_hash
*
* compute the SHA1(STEP2) that is SHA1(SHA1(the_password_to_check)), and is SHA_DIGEST_LENGTH long
*/
gw_sha1_str(step2, SHA_DIGEST_LENGTH, check_hash);
#ifdef GW_DEBUG_CLIENT_AUTH
{
char inpass[128]="";
gw_bin2hex(inpass, check_hash, SHA_DIGEST_LENGTH);
fprintf(stderr, "The CLIENT hex(SHA1(SHA1(password))) for \"%s\" is [%s]", username, inpass);
}
#endif
/* now compare SHA1(SHA1(gateway_password)) and check_hash: return 0 is MYSQL_AUTH_OK */
ret_val = memcmp(password, check_hash, SHA_DIGEST_LENGTH);
if (ret_val != 0)
return 1;
else
return 0;
}
/**
* gw_find_mysql_user_password_sha1
@ -2088,89 +1962,6 @@ char* create_auth_failed_msg(GWBUF*readbuf,
return errstr;
}
/**
* Read username from MySQL authentication packet.
*
* Only for client to server packet, COM_CHANGE_USER packet has different format.
*
* @param ptr address where to write the result or NULL if memory
* is allocated here.
* @param data Address of MySQL packet.
*
* @return Pointer to a copy of the username. NULL if memory allocation
* failed or if username was empty.
*/
char* get_username_from_auth(char* ptr,
uint8_t* data)
{
char* first_letter;
char* rval;
first_letter = (char *)(data + 4 + 4 + 4 + 1 + 23);
if (*first_letter == '\0')
{
rval = NULL;
goto retblock;
}
if (ptr == NULL)
{
if ((rval = (char *)malloc(MYSQL_USER_MAXLEN + 1)) == NULL)
{
goto retblock;
}
}
else
{
rval = ptr;
}
snprintf(rval, MYSQL_USER_MAXLEN + 1, "%s", first_letter);
retblock:
return rval;
}
int check_db_name_after_auth(DCB *dcb, char *database, int auth_ret)
{
int db_exists = -1;
/* check for dabase name and possible match in resource hashtable */
if (database && strlen(database))
{
/* if database names are loaded we can check if db name exists */
if (dcb->service->resources != NULL)
{
if (hashtable_fetch(dcb->service->resources, database))
{
db_exists = 1;
}
else
{
db_exists = 0;
}
}
else
{
/* if database names are not loaded we don't allow connection with db name*/
db_exists = -1;
}
if (db_exists == 0 && auth_ret == 0)
{
auth_ret = 2;
}
if (db_exists < 0 && auth_ret == 0)
{
auth_ret = 1;
}
}
return auth_ret;
}
/**
* Create a message error string to send via MySQL ERR packet.
*

View File

@ -23,6 +23,7 @@
#include <buffer.h>
#include <service.h>
#include <session.h>
#include <gw_protocol.h>
#include <sys/ioctl.h>
#include <errno.h>
#include <sys/socket.h>
@ -296,6 +297,7 @@ static int telnetd_accept(DCB *dcb)
close(so);
return n_connect;
}
client_dcb->listen_ssl = dcb->listen_ssl;
client_dcb->fd = so;
client_dcb->remote = strdup(inet_ntoa(addr.sin_addr));
memcpy(&client_dcb->func, &MyObject, sizeof(GWPROTOCOL));

View File

@ -33,6 +33,7 @@
#include <modinfo.h>
#include <dcb.h>
#include <buffer.h>
#include <gw_protocol.h>
MODULE_INFO info =
{