 47c69c3e96
			
		
	
	47c69c3e96
	
	
	
		
			
			Each protocol module now resides in a subdirectory of its own. The name of the subdirectory is the same as the name of the library in the module's CMakeLists.txt file.
		
			
				
	
	
		
			2719 lines
		
	
	
		
			86 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
			
		
		
	
	
			2719 lines
		
	
	
		
			86 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
| /*
 | |
|  * 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.
 | |
|  */
 | |
| 
 | |
| #include "mysql_client_server_protocol.h"
 | |
| #include <skygw_types.h>
 | |
| #include <skygw_utils.h>
 | |
| #include <log_manager.h>
 | |
| #include <modutil.h>
 | |
| #include <utils.h>
 | |
| #include <netinet/tcp.h>
 | |
| #include <gw.h>
 | |
| 
 | |
| /* 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.
 | |
|  *
 | |
|  * Revision History
 | |
|  * Date         Who                     Description
 | |
|  * 14/06/2013   Mark Riddoch            Initial version
 | |
|  * 17/06/2013   Massimiliano Pinto      Added MaxScale To Backends routines
 | |
|  * 01/07/2013   Massimiliano Pinto      Put Log Manager example code behind SS_DEBUG macros.
 | |
|  * 03/07/2013   Massimiliano Pinto      Added delayq for incoming data before mysql connection
 | |
|  * 04/07/2013   Massimiliano Pinto      Added asynchronous MySQL protocol connection to backend
 | |
|  * 05/07/2013   Massimiliano Pinto      Added closeSession if backend auth fails
 | |
|  * 12/07/2013   Massimiliano Pinto      Added Mysql Change User via dcb->func.auth()
 | |
|  * 15/07/2013   Massimiliano Pinto      Added Mysql session change via dcb->func.session()
 | |
|  * 17/07/2013   Massimiliano Pinto      Added dcb->command update from gwbuf->command for proper routing
 | |
|  *                                      server replies to client via router->clientReply
 | |
|  * 04/09/2013   Massimiliano Pinto      Added dcb->session and dcb->session->client checks for NULL
 | |
|  * 12/09/2013   Massimiliano Pinto      Added checks in gw_read_backend_event() for gw_read_backend_handshake
 | |
|  * 27/09/2013   Massimiliano Pinto      Changed in gw_read_backend_event the check for dcb_read(),
 | |
|  *                                      now is if rc less than 0
 | |
|  * 24/10/2014   Massimiliano Pinto      Added Mysql user@host @db authentication support
 | |
|  * 10/11/2014   Massimiliano Pinto      Client charset is passed to backend
 | |
|  * 19/06/2015   Martin Brampton         Persistent connection handling
 | |
|  * 07/10/2015   Martin Brampton         Remove calls to dcb_close - should be done by routers
 | |
|  * 27/10/2015   Martin Brampton         Test for RCAP_TYPE_NO_RSESSION before calling clientReply
 | |
|  * 23/05/2016   Martin Brampton         Provide for backend SSL
 | |
|  *
 | |
|  */
 | |
| #include <maxscale/alloc.h>
 | |
| #include <modinfo.h>
 | |
| #include <gw_protocol.h>
 | |
| #include <mysql_auth.h>
 | |
| 
 | |
|  /* @see function load_module in load_utils.c for explanation of the following
 | |
|   * lint directives.
 | |
|  */
 | |
| /*lint -e14 */
 | |
| MODULE_INFO info = {
 | |
|                     MODULE_API_PROTOCOL,
 | |
|                     MODULE_GA,
 | |
|                     GWPROTOCOL_VERSION,
 | |
|                     "The MySQL to backend server protocol"
 | |
| };
 | |
| /*lint +e14 */
 | |
| 
 | |
| static char *version_str = "V2.0.0";
 | |
| static int gw_create_backend_connection(DCB *backend, SERVER *server, SESSION *in_session);
 | |
| static int gw_read_backend_event(DCB* dcb);
 | |
| static int gw_write_backend_event(DCB *dcb);
 | |
| static int gw_MySQLWrite_backend(DCB *dcb, GWBUF *queue);
 | |
| static int gw_error_backend_event(DCB *dcb);
 | |
| static int gw_backend_close(DCB *dcb);
 | |
| static int gw_backend_hangup(DCB *dcb);
 | |
| static int backend_write_delayqueue(DCB *dcb);
 | |
| static void backend_set_delayqueue(DCB *dcb, GWBUF *queue);
 | |
| static int gw_change_user(DCB *backend_dcb, SERVER *server, SESSION *in_session, GWBUF *queue);
 | |
| static char *gw_backend_default_auth();
 | |
| static GWBUF* process_response_data(DCB* dcb, GWBUF* readbuf, int nbytes_to_process);
 | |
| extern char* create_auth_failed_msg(GWBUF* readbuf, char* hostaddr, uint8_t* sha1);
 | |
| static bool sescmd_response_complete(DCB* dcb);
 | |
| static int gw_read_reply_or_error(DCB *dcb, MYSQL_session local_session);
 | |
| static int gw_read_and_write(DCB *dcb, MYSQL_session local_session);
 | |
| static int gw_read_backend_handshake(MySQLProtocol *conn);
 | |
| static int gw_decode_mysql_server_handshake(MySQLProtocol *conn, uint8_t *payload);
 | |
| static int gw_receive_backend_auth(MySQLProtocol *protocol);
 | |
| static mysql_auth_state_t gw_send_authentication_to_backend(char *dbname,
 | |
|                                       char *user,
 | |
|                                       uint8_t *passwd,
 | |
|                                       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,
 | |
|                                     MySQLProtocol*  protocol);
 | |
| static int gw_send_change_user_to_backend(char          *dbname,
 | |
|                                    char          *user,
 | |
|                                    uint8_t       *passwd,
 | |
|                                    MySQLProtocol *conn);
 | |
| 
 | |
| #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 = {
 | |
|                               gw_read_backend_event, /* Read - EPOLLIN handler        */
 | |
|                               gw_MySQLWrite_backend, /* Write - data from gateway     */
 | |
|                               gw_write_backend_event, /* WriteReady - EPOLLOUT handler */
 | |
|                               gw_error_backend_event, /* Error - EPOLLERR handler      */
 | |
|                               gw_backend_hangup, /* HangUp - EPOLLHUP handler     */
 | |
|                               NULL, /* Accept                        */
 | |
|                               gw_create_backend_connection, /* Connect                       */
 | |
|                               gw_backend_close, /* Close                         */
 | |
|                               NULL, /* Listen                        */
 | |
|                               gw_change_user, /* Authentication                */
 | |
|                               NULL, /* Session                       */
 | |
|                               gw_backend_default_auth, /* Default authenticator */
 | |
|                               NULL  /**< Connection limit reached      */
 | |
| };
 | |
| 
 | |
| /*
 | |
|  * 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
 | |
|  */
 | |
| GWPROTOCOL* GetModuleObject()
 | |
| {
 | |
|     return &MyObject;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * The default authenticator name for this protocol
 | |
|  *
 | |
|  * This is not used for a backend protocol, it is for client authentication.
 | |
|  *
 | |
|  * @return name of authenticator
 | |
|  */
 | |
| static char *gw_backend_default_auth()
 | |
| {
 | |
|     return "NullBackendAuth";
 | |
| }
 | |
| /*lint +e14 */
 | |
| 
 | |
| /*******************************************************************************
 | |
|  *******************************************************************************
 | |
|  *
 | |
|  * API Entry Point - Connect
 | |
|  *
 | |
|  * This is the first entry point that will be called in the life of a backend
 | |
|  * (database) connection. It creates a protocol data structure and attempts
 | |
|  * to open a non-blocking socket to the database. If it succeeds, the
 | |
|  * protocol_auth_state will become MYSQL_CONNECTED.
 | |
|  *
 | |
|  *******************************************************************************
 | |
|  ******************************************************************************/
 | |
| 
 | |
| /*
 | |
|  * Create a new backend connection.
 | |
|  *
 | |
|  * This routine will connect to a backend server and it is called by dbc_connect
 | |
|  * in router->newSession
 | |
|  *
 | |
|  * @param backend_dcb, in, out, use - backend DCB allocated from dcb_connect
 | |
|  * @param server, in, use - server to connect to
 | |
|  * @param session, in use - current session from client DCB
 | |
|  * @return 0/1 on Success and -1 on Failure.
 | |
|  * If succesful, returns positive fd to socket which is connected to
 | |
|  *  backend server. Positive fd is copied to protocol and to dcb.
 | |
|  * If fails, fd == -1 and socket is closed.
 | |
|  */
 | |
| static int gw_create_backend_connection(DCB *backend_dcb,
 | |
|                                         SERVER *server,
 | |
|                                         SESSION *session)
 | |
| {
 | |
|     MySQLProtocol *protocol = NULL;
 | |
|     int rv = -1;
 | |
|     int fd = -1;
 | |
| 
 | |
|     protocol = mysql_protocol_init(backend_dcb, -1);
 | |
|     ss_dassert(protocol != NULL);
 | |
| 
 | |
|     if (protocol == NULL)
 | |
|     {
 | |
|         MXS_DEBUG("%lu [gw_create_backend_connection] Failed to create "
 | |
|                   "protocol object for backend connection.",
 | |
|                   pthread_self());
 | |
|         MXS_ERROR("Failed to create protocol object for backend connection.");
 | |
|         goto return_fd;
 | |
|     }
 | |
| 
 | |
|     /** Copy client flags to backend protocol */
 | |
|     if (backend_dcb->session->client_dcb->protocol)
 | |
|     {
 | |
|         /** Copy client flags to backend protocol */
 | |
|         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_dcb->protocol))->charset;
 | |
|     }
 | |
|     else
 | |
|     {
 | |
|         protocol->client_capabilities = (int)GW_MYSQL_CAPABILITIES_CLIENT;
 | |
|         protocol->charset = 0x08;
 | |
|     }
 | |
| 
 | |
|     /*< if succeed, fd > 0, -1 otherwise */
 | |
|     /* TODO: Better if function returned a protocol auth state */
 | |
|     rv = gw_do_connect_to_backend(server->name, server->port, &fd);
 | |
|     /*< Assign protocol with backend_dcb */
 | |
|     backend_dcb->protocol = protocol;
 | |
| 
 | |
|     /*< Set protocol state */
 | |
|     switch (rv)
 | |
|     {
 | |
|         case 0:
 | |
|             ss_dassert(fd > 0);
 | |
|             protocol->fd = fd;
 | |
|             protocol->protocol_auth_state = MYSQL_CONNECTED;
 | |
|             MXS_DEBUG("%lu [gw_create_backend_connection] Established "
 | |
|                       "connection to %s:%i, protocol fd %d client "
 | |
|                       "fd %d.",
 | |
|                       pthread_self(),
 | |
|                       server->name,
 | |
|                       server->port,
 | |
|                       protocol->fd,
 | |
|                       session->client_dcb->fd);
 | |
|             break;
 | |
| 
 | |
|         case 1:
 | |
|             /* The state MYSQL_PENDING_CONNECT is likely to be transitory,    */
 | |
|             /* as it means the calls have been successful but the connection  */
 | |
|             /* has not yet completed and the calls are non-blocking.          */
 | |
|             ss_dassert(fd > 0);
 | |
|             protocol->protocol_auth_state = MYSQL_PENDING_CONNECT;
 | |
|             protocol->fd = fd;
 | |
|             MXS_DEBUG("%lu [gw_create_backend_connection] Connection "
 | |
|                       "pending to %s:%i, protocol fd %d client fd %d.",
 | |
|                       pthread_self(),
 | |
|                       server->name,
 | |
|                       server->port,
 | |
|                       protocol->fd,
 | |
|                       session->client_dcb->fd);
 | |
|             break;
 | |
| 
 | |
|         default:
 | |
|             /* Failure - the state reverts to its initial value */
 | |
|             ss_dassert(fd == -1);
 | |
|             ss_dassert(protocol->protocol_auth_state == MYSQL_ALLOC);
 | |
|             MXS_DEBUG("%lu [gw_create_backend_connection] Connection "
 | |
|                       "failed to %s:%i, protocol fd %d client fd %d.",
 | |
|                       pthread_self(),
 | |
|                       server->name,
 | |
|                       server->port,
 | |
|                       protocol->fd,
 | |
|                       session->client_dcb->fd);
 | |
|             break;
 | |
|     } /*< switch */
 | |
| 
 | |
| return_fd:
 | |
|     return fd;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * gw_do_connect_to_backend
 | |
|  *
 | |
|  * This routine creates socket and connects to a backend server.
 | |
|  * Connect it non-blocking operation. If connect fails, socket is closed.
 | |
|  *
 | |
|  * @param host The host to connect to
 | |
|  * @param port The host TCP/IP port
 | |
|  * @param *fd where connected fd is copied
 | |
|  * @return 0/1 on success and -1 on failure
 | |
|  * If successful, fd has file descriptor to socket which is connected to
 | |
|  * backend server. In failure, fd == -1 and socket is closed.
 | |
|  *
 | |
|  */
 | |
| static int
 | |
| gw_do_connect_to_backend(char *host, int port, int *fd)
 | |
| {
 | |
|     struct sockaddr_in serv_addr;
 | |
|     int rv;
 | |
|     int so = 0;
 | |
|     int bufsize;
 | |
| 
 | |
|     memset(&serv_addr, 0, sizeof serv_addr);
 | |
|     serv_addr.sin_family = (int)AF_INET;
 | |
|     so = socket((int)AF_INET, (int)SOCK_STREAM, 0);
 | |
| 
 | |
|     if (so < 0)
 | |
|     {
 | |
|         char errbuf[STRERROR_BUFLEN];
 | |
|         MXS_ERROR("Establishing connection to backend server "
 | |
|                   "%s:%d failed.\n\t\t             Socket creation failed "
 | |
|                   "due %d, %s.",
 | |
|                   host,
 | |
|                   port,
 | |
|                   errno,
 | |
|                   strerror_r(errno, errbuf, sizeof(errbuf)));
 | |
|         rv = -1;
 | |
|         goto return_rv;
 | |
|     }
 | |
|     /* prepare for connect */
 | |
|     setipaddress(&serv_addr.sin_addr, host);
 | |
|     serv_addr.sin_port = htons(port);
 | |
|     bufsize = GW_BACKEND_SO_SNDBUF;
 | |
| 
 | |
|     if (setsockopt(so, SOL_SOCKET, SO_SNDBUF, &bufsize, sizeof(bufsize)) != 0)
 | |
|     {
 | |
|         char errbuf[STRERROR_BUFLEN];
 | |
|         MXS_ERROR("Failed to set socket options "
 | |
|                   "%s:%d failed.\n\t\t             Socket configuration failed "
 | |
|                   "due %d, %s.",
 | |
|                   host,
 | |
|                   port,
 | |
|                   errno,
 | |
|                   strerror_r(errno, errbuf, sizeof(errbuf)));
 | |
|         rv = -1;
 | |
|         /** Close socket */
 | |
|         close_socket(so);
 | |
|         goto return_rv;
 | |
|     }
 | |
|     bufsize = GW_BACKEND_SO_RCVBUF;
 | |
| 
 | |
|     if (setsockopt(so, SOL_SOCKET, SO_RCVBUF, &bufsize, sizeof(bufsize)) != 0)
 | |
|     {
 | |
|         char errbuf[STRERROR_BUFLEN];
 | |
|         MXS_ERROR("Failed to set socket options "
 | |
|                   "%s:%d failed.\n\t\t             Socket configuration failed "
 | |
|                   "due %d, %s.",
 | |
|                   host,
 | |
|                   port,
 | |
|                   errno,
 | |
|                   strerror_r(errno, errbuf, sizeof(errbuf)));
 | |
|         rv = -1;
 | |
|         /** Close socket */
 | |
|         close_socket(so);
 | |
|         goto return_rv;
 | |
|     }
 | |
| 
 | |
|     int one = 1;
 | |
|     if (setsockopt(so, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one)) != 0)
 | |
|     {
 | |
|         char errbuf[STRERROR_BUFLEN];
 | |
|         MXS_ERROR("Failed to set socket options "
 | |
|                   "%s:%d failed.\n\t\t             Socket configuration failed "
 | |
|                   "due %d, %s.",
 | |
|                   host,
 | |
|                   port,
 | |
|                   errno,
 | |
|                   strerror_r(errno, errbuf, sizeof(errbuf)));
 | |
|         rv = -1;
 | |
|         /** Close socket */
 | |
|         close_socket(so);
 | |
|         goto return_rv;
 | |
|     }
 | |
| 
 | |
|     /* set socket to as non-blocking here */
 | |
|     setnonblocking(so);
 | |
|     rv = connect(so, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
 | |
| 
 | |
|     if (rv != 0)
 | |
|     {
 | |
|         if (errno == EINPROGRESS)
 | |
|         {
 | |
|             rv = 1;
 | |
|         }
 | |
|         else
 | |
|         {
 | |
|             char errbuf[STRERROR_BUFLEN];
 | |
|             MXS_ERROR("Failed to connect backend server %s:%d, "
 | |
|                       "due %d, %s.",
 | |
|                       host,
 | |
|                       port,
 | |
|                       errno,
 | |
|                       strerror_r(errno, errbuf, sizeof(errbuf)));
 | |
|             /** Close socket */
 | |
|             close_socket(so);
 | |
|             goto return_rv;
 | |
|         }
 | |
|     }
 | |
|     *fd = so;
 | |
|     MXS_DEBUG("%lu [gw_do_connect_to_backend] Connected to backend server "
 | |
|               "%s:%d, fd %d.",
 | |
|               pthread_self(), host, port, so);
 | |
| #if defined(FAKE_CODE)
 | |
|     conn_open[so] = true;
 | |
| #endif /* FAKE_CODE */
 | |
| 
 | |
| return_rv:
 | |
|     return rv;
 | |
| 
 | |
| }
 | |
| 
 | |
| /*******************************************************************************
 | |
|  *******************************************************************************
 | |
|  *
 | |
|  * API Entry Point - Read
 | |
|  *
 | |
|  * When the polling mechanism finds that new incoming data is available for
 | |
|  * a backend connection, it will call this entry point, passing the relevant
 | |
|  * DCB.
 | |
|  *
 | |
|  * The first time through, it is expected that protocol_auth_state will be
 | |
|  * MYSQL_CONNECTED and an attempt will be made to send authentication data
 | |
|  * to the backend server. The state may progress to MYSQL_AUTH_REC although
 | |
|  * for an SSL connection this will not happen straight away, and the state
 | |
|  * will remain MYSQL_CONNECTED.
 | |
|  *
 | |
|  * When the connection is fully established, it is expected that the state
 | |
|  * will be MYSQL_IDLE and the information read from the backend will be
 | |
|  * transferred to the client (front end).
 | |
|  *
 | |
|  *******************************************************************************
 | |
|  ******************************************************************************/
 | |
| 
 | |
| /**
 | |
|  * Backend Read Event for EPOLLIN on the MySQL backend protocol module
 | |
|  * @param dcb   The backend Descriptor Control Block
 | |
|  * @return 1 on operation, 0 for no action
 | |
|  */
 | |
| static int
 | |
| gw_read_backend_event(DCB *dcb)
 | |
| {
 | |
|     MySQLProtocol *backend_protocol;
 | |
|     MYSQL_session local_session;
 | |
| 
 | |
|     CHK_DCB(dcb);
 | |
|     if (dcb->persistentstart)
 | |
|     {
 | |
|         dcb->dcb_errhandle_called = true;
 | |
|         return 0;
 | |
|     }
 | |
| 
 | |
|     if (dcb->dcb_is_zombie || dcb->session == NULL)
 | |
|     {
 | |
|         return 0;
 | |
|     }
 | |
| 
 | |
|     CHK_SESSION(dcb->session);
 | |
| 
 | |
|     /*< return only with complete session */
 | |
|     if (!gw_get_shared_session_auth_info(dcb, &local_session))
 | |
|     {
 | |
|         return 0;
 | |
|     }
 | |
| 
 | |
|     backend_protocol = (MySQLProtocol *) dcb->protocol;
 | |
|     CHK_PROTOCOL(backend_protocol);
 | |
| 
 | |
|     MXS_DEBUG("%lu [gw_read_backend_event] Read dcb %p fd %d protocol "
 | |
|         "state %d, %s.",
 | |
|         pthread_self(),
 | |
|         dcb,
 | |
|         dcb->fd,
 | |
|         backend_protocol->protocol_auth_state,
 | |
|         STRPROTOCOLSTATE(backend_protocol->protocol_auth_state));
 | |
| 
 | |
|     /* backend is connected:
 | |
|      *
 | |
|      * 1. read server handshake
 | |
|      * 2. if (success) write auth request
 | |
|      * 3.  and return
 | |
|      */
 | |
| 
 | |
|     /*<
 | |
|      * If starting to auhenticate with backend server, lock dcb
 | |
|      * to prevent overlapping processing of auth messages.
 | |
|      */
 | |
|     if (backend_protocol->protocol_auth_state == MYSQL_CONNECTED)
 | |
|     {
 | |
|         spinlock_acquire(&dcb->authlock);
 | |
|         if (backend_protocol->protocol_auth_state == MYSQL_CONNECTED)
 | |
|         {
 | |
|             /** Read cached backend handshake */
 | |
|             if (gw_read_backend_handshake(backend_protocol) != 0)
 | |
|             {
 | |
|                 backend_protocol->protocol_auth_state = MYSQL_HANDSHAKE_FAILED;
 | |
| 
 | |
|                 MXS_DEBUG("%lu [gw_read_backend_event] after "
 | |
|                           "gw_read_backend_handshake, fd %d, "
 | |
|                           "state = MYSQL_HANDSHAKE_FAILED.",
 | |
|                           pthread_self(),
 | |
|                           backend_protocol->owner_dcb->fd);
 | |
|             }
 | |
|             else
 | |
|             {
 | |
|                 /**
 | |
|                  * Decode password and send the auth credentials
 | |
|                  * to backend.
 | |
|                  */
 | |
|                 backend_protocol->protocol_auth_state =
 | |
|                     gw_send_authentication_to_backend(
 | |
|                     local_session.db,
 | |
|                     local_session.user,
 | |
|                     local_session.client_sha1,
 | |
|                     backend_protocol);
 | |
|             }
 | |
|         }
 | |
|         spinlock_release(&dcb->authlock);
 | |
|     } /*< backend_protocol->protocol_auth_state == MYSQL_CONNECTED */
 | |
|     /*
 | |
|      * Now:
 | |
|      *  -- check the authentication reply from backend
 | |
|      * OR
 | |
|      * -- handle a previous handshake error
 | |
|      */
 | |
|     if (backend_protocol->protocol_auth_state != MYSQL_IDLE)
 | |
|     {
 | |
|         spinlock_acquire(&dcb->authlock);
 | |
| 
 | |
|         if (backend_protocol->protocol_auth_state != MYSQL_IDLE)
 | |
|         {
 | |
|             if (backend_protocol->protocol_auth_state == MYSQL_CONNECTED)
 | |
|             {
 | |
|                 spinlock_release(&dcb->authlock);
 | |
|                 return 0;
 | |
|             }
 | |
|             /* Function gw_read_reply_or_error will release dcb->authlock */
 | |
|             int return_code = gw_read_reply_or_error(dcb, local_session);
 | |
|             /* Make decision whether to exit */
 | |
|             if (return_code < 2)
 | |
|             {
 | |
|                 return return_code;
 | |
|             }
 | |
|         }
 | |
|         else
 | |
|         {
 | |
|             spinlock_release(&dcb->authlock);
 | |
|         }
 | |
|     } /* MYSQL_AUTH_RECV || MYSQL_AUTH_FAILED || MYSQL_HANDSHAKE_FAILED */
 | |
| 
 | |
|     /* Reading MySQL command output from backend and writing to the client */
 | |
|     return gw_read_and_write(dcb, local_session);
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Read the backend server MySQL handshake
 | |
|  *
 | |
|  * @param conn  MySQL protocol structure
 | |
|  * @return 0 on success, 1 on failure
 | |
|  */
 | |
| static int
 | |
| gw_read_backend_handshake(MySQLProtocol *conn)
 | |
| {
 | |
|     GWBUF *head = NULL;
 | |
|     DCB *dcb = conn->owner_dcb;
 | |
|     uint8_t *payload = NULL;
 | |
|     int h_len = 0;
 | |
|     int  success = 0;
 | |
|     int packet_len = 0;
 | |
| 
 | |
|     if (dcb_read(dcb, &head, 0) != -1)
 | |
|     {
 | |
|         dcb->last_read = hkheartbeat;
 | |
| 
 | |
|         if (head)
 | |
|         {
 | |
|             payload = GWBUF_DATA(head);
 | |
|             h_len = gwbuf_length(head);
 | |
| 
 | |
|             /**
 | |
|              * The mysql packets content starts at byte fifth
 | |
|              * just return with less bytes
 | |
|              */
 | |
| 
 | |
|             if (h_len <= 4)
 | |
|             {
 | |
|                 /* log error this exit point */
 | |
|                 conn->protocol_auth_state = MYSQL_HANDSHAKE_FAILED;
 | |
|                 MXS_DEBUG("%lu [gw_read_backend_handshake] after "
 | |
|                           "dcb_read, fd %d, "
 | |
|                           "state = MYSQL_HANDSHAKE_FAILED.",
 | |
|                           pthread_self(),
 | |
|                           dcb->fd);
 | |
| 
 | |
|                 return 1;
 | |
|             }
 | |
| 
 | |
|             if (payload[4] == 0xff)
 | |
|             {
 | |
|                 size_t len = MYSQL_GET_PACKET_LEN(payload);
 | |
|                 uint16_t errcode = MYSQL_GET_ERRCODE(payload);
 | |
|                 char* bufstr = strndup(&((char *)payload)[7], len - 3);
 | |
| 
 | |
|                 conn->protocol_auth_state = MYSQL_HANDSHAKE_FAILED;
 | |
| 
 | |
|                 MXS_DEBUG("%lu [gw_receive_backend_auth] Invalid "
 | |
|                           "authentication message from backend dcb %p "
 | |
|                           "fd %d, ptr[4] = %d, error code %d, msg %s.",
 | |
|                           pthread_self(),
 | |
|                           dcb,
 | |
|                           dcb->fd,
 | |
|                           payload[4],
 | |
|                           errcode,
 | |
|                           bufstr);
 | |
| 
 | |
|                 MXS_ERROR("Invalid authentication message "
 | |
|                           "from backend '%s'. Error code: %d, Msg : %s",
 | |
|                           dcb->server->unique_name,
 | |
|                           errcode,
 | |
|                           bufstr);
 | |
| 
 | |
|                 /**
 | |
|                  * If ER_HOST_IS_BLOCKED is found
 | |
|                  * the related server is put in maintenace mode
 | |
|                  * This will avoid filling the error log.
 | |
|                  */
 | |
| 
 | |
|                 if (errcode == 1129)
 | |
|                 {
 | |
|                     MXS_ERROR("Server %s has been put into maintenance mode due "
 | |
|                               "to the server blocking connections from MaxScale. "
 | |
|                               "Run 'mysqladmin -h %s -P %d flush-hosts' on this "
 | |
|                               "server before taking this server out of maintenance "
 | |
|                               "mode.",
 | |
|                               dcb->server->unique_name,
 | |
|                               dcb->server->name,
 | |
|                               dcb->server->port);
 | |
| 
 | |
|                     server_set_status(dcb->server, SERVER_MAINT);
 | |
|                 }
 | |
| 
 | |
|                 MXS_FREE(bufstr);
 | |
|             }
 | |
|             //get mysql packet size, 3 bytes
 | |
|             packet_len = gw_mysql_get_byte3(payload);
 | |
| 
 | |
|             if (h_len < (packet_len + 4))
 | |
|             {
 | |
|                 /*
 | |
|                  * data in buffer less than expected in the
 | |
|                  * packet. Log error this exit point
 | |
|                  */
 | |
| 
 | |
|                 conn->protocol_auth_state = MYSQL_HANDSHAKE_FAILED;
 | |
| 
 | |
|                 MXS_DEBUG("%lu [gw_read_backend_handshake] after "
 | |
|                           "gw_mysql_get_byte3, fd %d, "
 | |
|                           "state = MYSQL_HANDSHAKE_FAILED.",
 | |
|                           pthread_self(),
 | |
|                           dcb->fd);
 | |
| 
 | |
|                 return 1;
 | |
|             }
 | |
| 
 | |
|             // skip the 4 bytes header
 | |
|             payload += 4;
 | |
| 
 | |
|             //Now decode mysql handshake
 | |
|             success = gw_decode_mysql_server_handshake(conn, payload);
 | |
| 
 | |
|             if (success < 0)
 | |
|             {
 | |
|                 /* MySQL handshake has not been properly decoded
 | |
|                  * we cannot continue
 | |
|                  * log error this exit point
 | |
|                  */
 | |
|                 conn->protocol_auth_state = MYSQL_HANDSHAKE_FAILED;
 | |
| 
 | |
|                 MXS_DEBUG("%lu [gw_read_backend_handshake] after "
 | |
|                           "gw_decode_mysql_server_handshake, fd %d, "
 | |
|                           "state = MYSQL_HANDSHAKE_FAILED.",
 | |
|                           pthread_self(),
 | |
|                           conn->owner_dcb->fd);
 | |
|                 gwbuf_free(head);
 | |
|                 return 1;
 | |
|             }
 | |
| 
 | |
|             conn->protocol_auth_state = MYSQL_AUTH_SENT;
 | |
| 
 | |
|             // consume all the data here
 | |
|             gwbuf_free(head);
 | |
| 
 | |
|             return 0;
 | |
|         }
 | |
|         else if (SSL_ESTABLISHED == dcb->ssl_state)
 | |
|         {
 | |
|             return 0;
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     // Nothing done here, log error this
 | |
|     return 1;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * 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 mysql_auth_state_t
 | |
| gw_send_authentication_to_backend(char *dbname,
 | |
|                                       char *user,
 | |
|                                       uint8_t *passwd,
 | |
|                                       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(passwd, null_client_sha1, MYSQL_SCRAMBLE_LEN) ? passwd : NULL;
 | |
| 
 | |
|     /**
 | |
|      * If session is stopping 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))
 | |
|     {
 | |
|         return MYSQL_AUTH_FAILED;
 | |
|     }
 | |
| 
 | |
|     capabilities = create_capabilities(conn, (dbname && strlen(dbname)), false);
 | |
|     gw_mysql_set_byte4(client_capabilities, capabilities);
 | |
| 
 | |
|     bytes = response_length(conn, user, passwd, dbname);
 | |
| 
 | |
|     // 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;
 | |
| 
 | |
|     // 4 + 4 + 4 + 1 + 23 = 36, this includes the 4 bytes packet header
 | |
|     if (conn->owner_dcb->server->server_ssl && conn->owner_dcb->ssl_state != SSL_ESTABLISHED)
 | |
|     {
 | |
|         if (dcb_write(conn->owner_dcb, buffer))
 | |
|         {
 | |
|             switch (dcb_connect_SSL(conn->owner_dcb))
 | |
|             {
 | |
|                 case 1:
 | |
|                     return MYSQL_CONNECTED;
 | |
|                 case 0:
 | |
|                     return MYSQL_CONNECTED;
 | |
|                 default:
 | |
|                     break;
 | |
|             }
 | |
|         }
 | |
|         return MYSQL_AUTH_FAILED;
 | |
|     }
 | |
| 
 | |
|     memcpy(payload, user, strlen(user));
 | |
|     payload += strlen(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 (dbname && strlen(dbname))
 | |
|     {
 | |
|         memcpy(payload, dbname, strlen(dbname));
 | |
|         payload += strlen(dbname);
 | |
|         payload++;
 | |
|     }
 | |
| 
 | |
|     memcpy(payload,
 | |
|            "mysql_native_password",
 | |
|            strlen("mysql_native_password"));
 | |
|     /* Following needed if payload is used again */
 | |
|     /* payload += strlen("mysql_native_password"); */
 | |
| 
 | |
|     return dcb_write(conn->owner_dcb, buffer) ? MYSQL_AUTH_RECV : MYSQL_AUTH_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
 | |
|     {
 | |
|         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
 | |
|  *
 | |
|  * @param dcb               Descriptor control block for backend server
 | |
|  * @param local_session     The current MySQL session data structure
 | |
|  * @return 0 = fail, 1 = success, 2 = success and data to be transferred
 | |
|  */
 | |
| static int
 | |
| gw_read_reply_or_error(DCB *dcb, MYSQL_session local_session)
 | |
| {
 | |
|     int return_code = 0;
 | |
|     SESSION *session = dcb->session;
 | |
|     MySQLProtocol *backend_protocol = (MySQLProtocol *)dcb->protocol;
 | |
|     CHK_PROTOCOL(backend_protocol);
 | |
| 
 | |
|         if (SESSION_STATE_DUMMY == session->state)
 | |
|         {
 | |
|             spinlock_release(&dcb->authlock);
 | |
|             return 0;
 | |
|         }
 | |
|         CHK_SESSION(session);
 | |
| 
 | |
|         if (backend_protocol->protocol_auth_state == MYSQL_AUTH_RECV)
 | |
|         {
 | |
|             /**
 | |
|              * Read backend's reply to authentication message
 | |
|              */
 | |
|             int receive_rc = gw_receive_backend_auth(backend_protocol);
 | |
| 
 | |
|             switch (receive_rc)
 | |
|             {
 | |
|                 case -1:
 | |
|                     backend_protocol->protocol_auth_state = MYSQL_AUTH_FAILED;
 | |
|                     MXS_ERROR("Backend server didn't "
 | |
|                           "accept authentication for user "
 | |
|                           "%s.",
 | |
|                           local_session.user);
 | |
|                     break;
 | |
|                 case 1:
 | |
|                     backend_protocol->protocol_auth_state = MYSQL_IDLE;
 | |
|                     MXS_DEBUG("%lu [gw_read_backend_event] "
 | |
|                           "gw_receive_backend_auth succeed. "
 | |
|                           "dcb %p fd %d, user %s.",
 | |
|                           pthread_self(),
 | |
|                           dcb,
 | |
|                           dcb->fd,
 | |
|                           local_session.user);
 | |
|                     break;
 | |
|                 default:
 | |
|                     ss_dassert(receive_rc == 0);
 | |
|                     MXS_DEBUG("%lu [gw_read_backend_event] "
 | |
|                           "gw_receive_backend_auth read "
 | |
|                           "successfully "
 | |
|                           "nothing. dcb %p fd %d, user %s.",
 | |
|                           pthread_self(),
 | |
|                           dcb,
 | |
|                           dcb->fd,
 | |
|                           local_session.user);
 | |
|                     spinlock_release(&dcb->authlock);
 | |
|                     return 0;
 | |
|             } /* switch */
 | |
|         }
 | |
| 
 | |
|         if (backend_protocol->protocol_auth_state == MYSQL_AUTH_FAILED ||
 | |
|             backend_protocol->protocol_auth_state == MYSQL_HANDSHAKE_FAILED)
 | |
|         {
 | |
|             GWBUF* errbuf;
 | |
|             bool succp;
 | |
|             /**
 | |
|              * protocol state won't change anymore, lock can be freed.
 | |
|              * First free delay queue - which is only ever processed while
 | |
|              * authlock is held.
 | |
|              */
 | |
|             gwbuf_free(dcb->delayq);
 | |
|             dcb->delayq = NULL;
 | |
|             spinlock_release(&dcb->authlock);
 | |
| 
 | |
|             /* Only reload the users table if authentication failed and the
 | |
|              * client session is not stopping. It is possible that authentication
 | |
|              * fails because the client has closed the connection before all
 | |
|              * backends have done authentication. */
 | |
|             if (backend_protocol->protocol_auth_state == MYSQL_AUTH_FAILED &&
 | |
|                 dcb->session->state != SESSION_STATE_STOPPING)
 | |
|             {
 | |
|                 service_refresh_users(dcb->session->service);
 | |
|             }
 | |
| #if defined(SS_DEBUG)
 | |
|             MXS_DEBUG("%lu [gw_read_backend_event] "
 | |
|                   "calling handleError. Backend "
 | |
|                   "DCB %p, session %p",
 | |
|                   pthread_self(),
 | |
|                   dcb,
 | |
|                   dcb->session);
 | |
| #endif
 | |
|             errbuf = mysql_create_custom_error(1,
 | |
|                                    0,
 | |
|                                    "Authentication with backend failed. "
 | |
|                                    "Session will be closed.");
 | |
| 
 | |
|             if (session->router_session)
 | |
|             {
 | |
|                 session->service->router->handleError(
 | |
|                     session->service->router_instance,
 | |
|                     session->router_session,
 | |
|                     errbuf,
 | |
|                     dcb,
 | |
|                     ERRACT_REPLY_CLIENT,
 | |
|                     &succp);
 | |
|             spinlock_acquire(&session->ses_lock);
 | |
|             session->state = SESSION_STATE_STOPPING;
 | |
|             spinlock_release(&session->ses_lock);
 | |
|             ss_dassert(dcb->dcb_errhandle_called);
 | |
|             }
 | |
|             else
 | |
|             {
 | |
|                 dcb->dcb_errhandle_called = true;
 | |
|                 /*
 | |
|                  * I'm pretty certain this is best removed and
 | |
|                  * causes trouble if present, but have left it
 | |
|                  * here just for now as a comment. Martin
 | |
|                  */
 | |
|                 /* dcb_close(dcb); */
 | |
|             }
 | |
|             gwbuf_free(errbuf);
 | |
|             return 1;
 | |
|         }
 | |
|         else
 | |
|         {
 | |
|             MXS_DEBUG("%lu [gw_read_backend_event] "
 | |
|                   "gw_receive_backend_auth succeed. Fd %d, "
 | |
|                   "user %s.",
 | |
|                   pthread_self(),
 | |
|                   dcb->fd,
 | |
|                   local_session.user);
 | |
| 
 | |
|             /* check the delay queue and flush the data */
 | |
|             if (dcb->delayq)
 | |
|             {
 | |
|                 return_code = backend_write_delayqueue(dcb);
 | |
|                 spinlock_release(&dcb->authlock);
 | |
|                 return return_code;
 | |
|             }
 | |
|         }
 | |
|     spinlock_release(&dcb->authlock);
 | |
|     return 2;
 | |
| } /* MYSQL_AUTH_RECV || MYSQL_AUTH_FAILED */
 | |
| 
 | |
| /**
 | |
|  * @brief With authentication completed, read new data and write to backend
 | |
|  *
 | |
|  * @param dcb           Descriptor control block for backend server
 | |
|  * @param local_session Current MySQL session data structure
 | |
|  * @return 0 is fail, 1 is success
 | |
|  */
 | |
| static int
 | |
| gw_read_and_write(DCB *dcb, MYSQL_session local_session)
 | |
| {
 | |
|         GWBUF *read_buffer = NULL;
 | |
|         SESSION *session = dcb->session;
 | |
|         int nbytes_read;
 | |
|         int return_code;
 | |
| 
 | |
|         CHK_SESSION(session);
 | |
| 
 | |
|         /* read available backend data */
 | |
|         return_code = dcb_read(dcb, &read_buffer, 0);
 | |
| 
 | |
|         if (return_code < 0)
 | |
|         {
 | |
|             GWBUF* errbuf;
 | |
|             bool succp;
 | |
| #if defined(SS_DEBUG)
 | |
|             MXS_ERROR("Backend read error handling #2.");
 | |
| #endif
 | |
|             errbuf = mysql_create_custom_error(1,
 | |
|                                                0,
 | |
|                                                "Read from backend failed");
 | |
| 
 | |
|             session->service->router->handleError(
 | |
|                 session->service->router_instance,
 | |
|                                 session->router_session,
 | |
|                                 errbuf,
 | |
|                                 dcb,
 | |
|                                 ERRACT_NEW_CONNECTION,
 | |
|                                 &succp);
 | |
|             gwbuf_free(errbuf);
 | |
| 
 | |
|             if (!succp)
 | |
|             {
 | |
|                 spinlock_acquire(&session->ses_lock);
 | |
|                 session->state = SESSION_STATE_STOPPING;
 | |
|                 spinlock_release(&session->ses_lock);
 | |
|             }
 | |
|             return_code = 0;
 | |
|             goto return_rc;
 | |
|         }
 | |
| 
 | |
|         nbytes_read = gwbuf_length(read_buffer);
 | |
|         if (nbytes_read == 0)
 | |
|         {
 | |
|             ss_dassert(read_buffer == NULL);
 | |
|             goto return_rc;
 | |
|         }
 | |
|         else
 | |
|         {
 | |
|             ss_dassert(read_buffer != NULL);
 | |
|         }
 | |
| 
 | |
|         if (nbytes_read < 3)
 | |
|         {
 | |
|             dcb->dcb_readqueue = read_buffer;
 | |
|             return_code = 0;
 | |
|             goto return_rc;
 | |
|         }
 | |
| 
 | |
|         {
 | |
|             GWBUF *tmp = modutil_get_complete_packets(&read_buffer);
 | |
|             /* Put any residue into the read queue */
 | |
|             spinlock_acquire(&dcb->authlock);
 | |
|             dcb->dcb_readqueue = read_buffer;
 | |
|             spinlock_release(&dcb->authlock);
 | |
|             if (tmp == NULL)
 | |
|             {
 | |
|                 /** No complete packets */
 | |
|                 return_code = 0;
 | |
|                 goto return_rc;
 | |
|             }
 | |
|             else
 | |
|             {
 | |
|                 read_buffer = tmp;
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         /**
 | |
|          * If protocol has session command set, concatenate whole
 | |
|          * response into one buffer.
 | |
|          */
 | |
|         if (protocol_get_srv_command((MySQLProtocol *) dcb->protocol, false) != MYSQL_COM_UNDEFINED)
 | |
|         {
 | |
|             read_buffer = process_response_data(dcb, read_buffer, gwbuf_length(read_buffer));
 | |
|             /**
 | |
|              * Received incomplete response to session command.
 | |
|              * Store it to readqueue and return.
 | |
|              */
 | |
|             if (!sescmd_response_complete(dcb))
 | |
|             {
 | |
|                 return_code = 0;
 | |
|                 goto return_rc;
 | |
|             }
 | |
| 
 | |
|             if (!read_buffer)
 | |
|             {
 | |
|                 MXS_NOTICE("%lu [gw_read_backend_event] "
 | |
|                            "Read buffer unexpectedly null, even though response "
 | |
|                            "not marked as complete. User: %s",
 | |
|                            pthread_self(),
 | |
|                            local_session.user);
 | |
|                 return_code = 0;
 | |
|                 goto return_rc;
 | |
|             }
 | |
|         }
 | |
|         /**
 | |
|          * Check that session is operable, and that client DCB is
 | |
|          * still listening the socket for replies.
 | |
|          */
 | |
|         if (dcb->session->state == SESSION_STATE_ROUTER_READY &&
 | |
|             dcb->session->client_dcb != NULL &&
 | |
|             dcb->session->client_dcb->state == DCB_STATE_POLLING &&
 | |
|             (session->router_session ||
 | |
|             session->service->router->getCapabilities() & (int)RCAP_TYPE_NO_RSESSION))
 | |
|         {
 | |
|             MySQLProtocol *client_protocol = (MySQLProtocol *)dcb->session->client_dcb->protocol;
 | |
|             if (client_protocol != NULL)
 | |
|             {
 | |
|                 CHK_PROTOCOL(client_protocol);
 | |
| 
 | |
|                 if (client_protocol->protocol_auth_state == MYSQL_IDLE)
 | |
|                 {
 | |
|                     gwbuf_set_type(read_buffer, GWBUF_TYPE_MYSQL);
 | |
| 
 | |
|                     session->service->router->clientReply(
 | |
|                         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);
 | |
|                 return_code = 1;
 | |
|             }
 | |
|         }
 | |
|         else /*< session is closing; replying to client isn't possible */
 | |
|         {
 | |
|             gwbuf_free(read_buffer);
 | |
|         }
 | |
| 
 | |
| return_rc:
 | |
|     return return_code;
 | |
| }
 | |
| 
 | |
| /*
 | |
|  * EPOLLOUT handler for the MySQL Backend protocol module.
 | |
|  *
 | |
|  * @param dcb   The descriptor control block
 | |
|  * @return      1 in success, 0 in case of failure,
 | |
|  */
 | |
| static int gw_write_backend_event(DCB *dcb)
 | |
| {
 | |
|     int rc = 0;
 | |
|     MySQLProtocol *backend_protocol = dcb->protocol;
 | |
| 
 | |
|     /*<
 | |
|      * Don't write to backend if backend_dcb is not in poll set anymore.
 | |
|      */
 | |
|     if (dcb->state != DCB_STATE_POLLING)
 | |
|     {
 | |
|         uint8_t* data;
 | |
| 
 | |
|         if (dcb->writeq != NULL)
 | |
|         {
 | |
|             data = (uint8_t *) GWBUF_DATA(dcb->writeq);
 | |
| 
 | |
|             if (dcb->session->client_dcb == NULL)
 | |
|             {
 | |
|                 rc = 0;
 | |
|             }
 | |
|             else if (!(MYSQL_IS_COM_QUIT(data)))
 | |
|             {
 | |
|                 /*< vraa : errorHandle */
 | |
|                 mysql_send_custom_error(dcb->session->client_dcb,
 | |
|                                         1,
 | |
|                                         0,
 | |
|                                         "Writing to backend failed due invalid Maxscale "
 | |
|                                         "state.");
 | |
|                 MXS_DEBUG("%lu [gw_write_backend_event] Write to backend "
 | |
|                           "dcb %p fd %d "
 | |
|                           "failed due invalid state %s.",
 | |
|                           pthread_self(),
 | |
|                           dcb,
 | |
|                           dcb->fd,
 | |
|                           STRDCBSTATE(dcb->state));
 | |
| 
 | |
|                 MXS_ERROR("Attempt to write buffered data to backend "
 | |
|                           "failed "
 | |
|                           "due internal inconsistent state.");
 | |
| 
 | |
|                 rc = 0;
 | |
|             }
 | |
|         }
 | |
|         else
 | |
|         {
 | |
|             MXS_DEBUG("%lu [gw_write_backend_event] Dcb %p in state %s "
 | |
|                 "but there's nothing to write either.",
 | |
|                 pthread_self(),
 | |
|                 dcb,
 | |
|                 STRDCBSTATE(dcb->state));
 | |
|             rc = 1;
 | |
|         }
 | |
|         goto return_rc;
 | |
|     }
 | |
| 
 | |
|     if (backend_protocol->protocol_auth_state == MYSQL_PENDING_CONNECT)
 | |
|     {
 | |
|         backend_protocol->protocol_auth_state = MYSQL_CONNECTED;
 | |
|         rc = 1;
 | |
|         goto return_rc;
 | |
|     }
 | |
|     dcb_drain_writeq(dcb);
 | |
|     rc = 1;
 | |
| return_rc:
 | |
|     MXS_DEBUG("%lu [gw_write_backend_event] "
 | |
|               "wrote to dcb %p fd %d, return %d",
 | |
|               pthread_self(),
 | |
|               dcb,
 | |
|               dcb->fd,
 | |
|               rc);
 | |
| 
 | |
|     return rc;
 | |
| }
 | |
| 
 | |
| /*
 | |
|  * Write function for backend DCB. Store command to protocol.
 | |
|  *
 | |
|  * @param dcb   The DCB of the backend
 | |
|  * @param queue Queue of buffers to write
 | |
|  * @return      0 on failure, 1 on success
 | |
|  */
 | |
| static int gw_MySQLWrite_backend(DCB *dcb, GWBUF *queue)
 | |
| {
 | |
|     MySQLProtocol *backend_protocol = dcb->protocol;
 | |
|     int rc = 0;
 | |
| 
 | |
|     CHK_DCB(dcb);
 | |
|     spinlock_acquire(&dcb->authlock);
 | |
|     /**
 | |
|      * Pick action according to state of protocol.
 | |
|      * If auth failed, return value is 0, write and buffered write
 | |
|      * return 1.
 | |
|      */
 | |
|     switch (backend_protocol->protocol_auth_state)
 | |
|     {
 | |
|         case MYSQL_HANDSHAKE_FAILED:
 | |
|         case MYSQL_AUTH_FAILED:
 | |
|             if (dcb->session->state != SESSION_STATE_STOPPING)
 | |
|             {
 | |
|                 MXS_ERROR("Unable to write to backend '%s' due to "
 | |
|                           "%s failure. Server in state %s.",
 | |
|                           dcb->server->unique_name,
 | |
|                           backend_protocol->protocol_auth_state == MYSQL_HANDSHAKE_FAILED ?
 | |
|                           "handshake" : "authentication",
 | |
|                           STRSRVSTATUS(dcb->server));
 | |
|             }
 | |
|             /** Consume query buffer */
 | |
|             while ((queue = gwbuf_consume(
 | |
|                                           queue,
 | |
|                                           GWBUF_LENGTH(queue))) != NULL)
 | |
|             {
 | |
|                 ;
 | |
|             }
 | |
|             rc = 0;
 | |
|             spinlock_release(&dcb->authlock);
 | |
|             break;
 | |
| 
 | |
|         case MYSQL_IDLE:
 | |
|         {
 | |
|             uint8_t* ptr = GWBUF_DATA(queue);
 | |
|             mysql_server_cmd_t cmd = MYSQL_GET_COMMAND(ptr);
 | |
| 
 | |
|             MXS_DEBUG("%lu [gw_MySQLWrite_backend] write to dcb %p "
 | |
|                       "fd %d protocol state %s.",
 | |
|                       pthread_self(),
 | |
|                       dcb,
 | |
|                       dcb->fd,
 | |
|                       STRPROTOCOLSTATE(backend_protocol->protocol_auth_state));
 | |
| 
 | |
|             spinlock_release(&dcb->authlock);
 | |
|             /**
 | |
|              * Statement type is used in readwrite split router.
 | |
|              * Command is *not* set for readconn router.
 | |
|              *
 | |
|              * Server commands are stored to MySQLProtocol structure
 | |
|              * if buffer always includes a single statement.
 | |
|              */
 | |
|             if (GWBUF_IS_TYPE_SINGLE_STMT(queue) &&
 | |
|                 GWBUF_IS_TYPE_SESCMD(queue))
 | |
|             {
 | |
|                 /** Record the command to backend's protocol */
 | |
|                 protocol_add_srv_command(backend_protocol, cmd);
 | |
|             }
 | |
|             /** Write to backend */
 | |
|             rc = dcb_write(dcb, queue);
 | |
|         }
 | |
|         break;
 | |
| 
 | |
|         default:
 | |
|         {
 | |
|             MXS_DEBUG("%lu [gw_MySQLWrite_backend] delayed write to "
 | |
|                       "dcb %p fd %d protocol state %s.",
 | |
|                       pthread_self(),
 | |
|                       dcb,
 | |
|                       dcb->fd,
 | |
|                       STRPROTOCOLSTATE(backend_protocol->protocol_auth_state));
 | |
|             /**
 | |
|              * In case of session commands, store command to DCB's
 | |
|              * protocol struct.
 | |
|              */
 | |
|             if (GWBUF_IS_TYPE_SINGLE_STMT(queue) &&
 | |
|                 GWBUF_IS_TYPE_SESCMD(queue))
 | |
|             {
 | |
|                 uint8_t* ptr = GWBUF_DATA(queue);
 | |
|                 mysql_server_cmd_t cmd = MYSQL_GET_COMMAND(ptr);
 | |
| 
 | |
|                 /** Record the command to backend's protocol */
 | |
|                 protocol_add_srv_command(backend_protocol, cmd);
 | |
|             }
 | |
|             /*<
 | |
|              * Now put the incoming data to the delay queue unless backend is
 | |
|              * connected with auth ok
 | |
|              */
 | |
|             backend_set_delayqueue(dcb, queue);
 | |
|             spinlock_release(&dcb->authlock);
 | |
|             rc = 1;
 | |
|         }
 | |
|         break;
 | |
|     }
 | |
|     return rc;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Error event handler.
 | |
|  * Create error message, pass it to router's error handler and if error
 | |
|  * handler fails in providing enough backend servers, mark session being
 | |
|  * closed and call DCB close function which triggers closing router session
 | |
|  * and related backends (if any exists.
 | |
|  */
 | |
| static int gw_error_backend_event(DCB *dcb)
 | |
| {
 | |
|     SESSION* session;
 | |
|     void* rsession;
 | |
|     ROUTER_OBJECT* router;
 | |
|     ROUTER* router_instance;
 | |
|     GWBUF* errbuf;
 | |
|     bool succp;
 | |
|     session_state_t ses_state;
 | |
| 
 | |
|     CHK_DCB(dcb);
 | |
|     session = dcb->session;
 | |
|     CHK_SESSION(session);
 | |
|     if (SESSION_STATE_DUMMY == session->state)
 | |
|     {
 | |
|         dcb_close(dcb);
 | |
|         return 1;
 | |
|     }
 | |
|     rsession = session->router_session;
 | |
|     router = session->service->router;
 | |
|     router_instance = session->service->router_instance;
 | |
| 
 | |
|     /**
 | |
|      * Avoid running redundant error handling procedure.
 | |
|      * dcb_close is already called for the DCB. Thus, either connection is
 | |
|      * closed by router and COM_QUIT sent or there was an error which
 | |
|      * have already been handled.
 | |
|      */
 | |
|     if (dcb->state != DCB_STATE_POLLING)
 | |
|     {
 | |
|         int error, len;
 | |
| 
 | |
|         len = sizeof(error);
 | |
| 
 | |
|         if (getsockopt(dcb->fd, SOL_SOCKET, SO_ERROR, &error, (socklen_t *) & len) == 0)
 | |
|         {
 | |
|             if (error != 0)
 | |
|             {
 | |
|                 char errstring[STRERROR_BUFLEN];
 | |
|                 MXS_ERROR("DCB in state %s got error '%s'.",
 | |
|                           STRDCBSTATE(dcb->state),
 | |
|                           strerror_r(error, errstring, sizeof(errstring)));
 | |
|             }
 | |
|         }
 | |
|         return 1;
 | |
|     }
 | |
|     errbuf = mysql_create_custom_error(1,
 | |
|                                        0,
 | |
|                                        "Lost connection to backend server.");
 | |
| 
 | |
|     spinlock_acquire(&session->ses_lock);
 | |
|     ses_state = session->state;
 | |
|     spinlock_release(&session->ses_lock);
 | |
| 
 | |
|     /**
 | |
|      * Session might be initialized when DCB already is in the poll set.
 | |
|      * Thus hangup can occur in the middle of session initialization.
 | |
|      * Only complete and successfully initialized sessions allow for
 | |
|      * calling error handler.
 | |
|      */
 | |
|     while (ses_state == SESSION_STATE_READY)
 | |
|     {
 | |
|         spinlock_acquire(&session->ses_lock);
 | |
|         ses_state = session->state;
 | |
|         spinlock_release(&session->ses_lock);
 | |
|     }
 | |
| 
 | |
|     if (ses_state != SESSION_STATE_ROUTER_READY)
 | |
|     {
 | |
|         int error, len;
 | |
| 
 | |
|         len = sizeof(error);
 | |
|         if (getsockopt(dcb->fd, SOL_SOCKET, SO_ERROR, &error, (socklen_t *) & len) == 0)
 | |
|         {
 | |
|             if (error != 0)
 | |
|             {
 | |
|                 char errstring[STRERROR_BUFLEN];
 | |
|                 MXS_ERROR("Error '%s' in session that is not ready for routing.",
 | |
|                           strerror_r(error, errstring, sizeof(errstring)));
 | |
|             }
 | |
|         }
 | |
|         gwbuf_free(errbuf);
 | |
|         goto retblock;
 | |
|     }
 | |
| 
 | |
| #if defined(SS_DEBUG)
 | |
|     MXS_INFO("Backend error event handling.");
 | |
| #endif
 | |
|     router->handleError(router_instance,
 | |
|                         rsession,
 | |
|                         errbuf,
 | |
|                         dcb,
 | |
|                         ERRACT_NEW_CONNECTION,
 | |
|                         &succp);
 | |
|     gwbuf_free(errbuf);
 | |
| 
 | |
|     /**
 | |
|      * If error handler fails it means that routing session can't continue
 | |
|      * and it must be closed. In success, only this DCB is closed.
 | |
|      */
 | |
|     if (!succp)
 | |
|     {
 | |
|         spinlock_acquire(&session->ses_lock);
 | |
|         session->state = SESSION_STATE_STOPPING;
 | |
|         spinlock_release(&session->ses_lock);
 | |
|     }
 | |
| 
 | |
| retblock:
 | |
|     return 1;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Error event handler.
 | |
|  * Create error message, pass it to router's error handler and if error
 | |
|  * handler fails in providing enough backend servers, mark session being
 | |
|  * closed and call DCB close function which triggers closing router session
 | |
|  * and related backends (if any exists.
 | |
|  *
 | |
|  * @param dcb The current Backend DCB
 | |
|  * @return 1 always
 | |
|  */
 | |
| static int gw_backend_hangup(DCB *dcb)
 | |
| {
 | |
|     SESSION* session;
 | |
|     void* rsession;
 | |
|     ROUTER_OBJECT* router;
 | |
|     ROUTER* router_instance;
 | |
|     bool succp;
 | |
|     GWBUF* errbuf;
 | |
|     session_state_t ses_state;
 | |
| 
 | |
|     CHK_DCB(dcb);
 | |
|     if (dcb->persistentstart)
 | |
|     {
 | |
|         dcb->dcb_errhandle_called = true;
 | |
|         goto retblock;
 | |
|     }
 | |
|     session = dcb->session;
 | |
| 
 | |
|     if (session == NULL)
 | |
|     {
 | |
|         goto retblock;
 | |
|     }
 | |
| 
 | |
|     CHK_SESSION(session);
 | |
| 
 | |
|     rsession = session->router_session;
 | |
|     router = session->service->router;
 | |
|     router_instance = session->service->router_instance;
 | |
| 
 | |
|     errbuf = mysql_create_custom_error(1,
 | |
|                                        0,
 | |
|                                        "Lost connection to backend server.");
 | |
| 
 | |
|     spinlock_acquire(&session->ses_lock);
 | |
|     ses_state = session->state;
 | |
|     spinlock_release(&session->ses_lock);
 | |
| 
 | |
|     /**
 | |
|      * Session might be initialized when DCB already is in the poll set.
 | |
|      * Thus hangup can occur in the middle of session initialization.
 | |
|      * Only complete and successfully initialized sessions allow for
 | |
|      * calling error handler.
 | |
|      */
 | |
|     while (ses_state == SESSION_STATE_READY)
 | |
|     {
 | |
|         spinlock_acquire(&session->ses_lock);
 | |
|         ses_state = session->state;
 | |
|         spinlock_release(&session->ses_lock);
 | |
|     }
 | |
| 
 | |
|     if (ses_state != SESSION_STATE_ROUTER_READY)
 | |
|     {
 | |
|         int error, len;
 | |
| 
 | |
|         len = sizeof(error);
 | |
|         if (getsockopt(dcb->fd, SOL_SOCKET, SO_ERROR, &error, (socklen_t *) & len) == 0)
 | |
|         {
 | |
|             if (error != 0 && ses_state != SESSION_STATE_STOPPING)
 | |
|             {
 | |
|                 char errstring[STRERROR_BUFLEN];
 | |
|                 MXS_ERROR("Hangup in session that is not ready for routing, "
 | |
|                           "Error reported is '%s'.",
 | |
|                           strerror_r(error, errstring, sizeof(errstring)));
 | |
|             }
 | |
|         }
 | |
|         gwbuf_free(errbuf);
 | |
|         /*
 | |
|          * I'm pretty certain this is best removed and
 | |
|          * causes trouble if present, but have left it
 | |
|          * here just for now as a comment. Martin
 | |
|          */
 | |
|         /* dcb_close(dcb); */
 | |
|         goto retblock;
 | |
|     }
 | |
| #if defined(SS_DEBUG)
 | |
|     if (ses_state != SESSION_STATE_STOPPING)
 | |
|     {
 | |
|         MXS_ERROR("Backend hangup error handling.");
 | |
|     }
 | |
| #endif
 | |
| 
 | |
|     router->handleError(router_instance,
 | |
|                         rsession,
 | |
|                         errbuf,
 | |
|                         dcb,
 | |
|                         ERRACT_NEW_CONNECTION,
 | |
|                         &succp);
 | |
| 
 | |
|     gwbuf_free(errbuf);
 | |
|     /** There are no required backends available, close session. */
 | |
|     if (!succp)
 | |
|     {
 | |
| #if defined(SS_DEBUG)
 | |
|         MXS_ERROR("Backend hangup -> closing session.");
 | |
| #endif
 | |
|         spinlock_acquire(&session->ses_lock);
 | |
|         session->state = SESSION_STATE_STOPPING;
 | |
|         spinlock_release(&session->ses_lock);
 | |
|     }
 | |
| 
 | |
| retblock:
 | |
|     return 1;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Send COM_QUIT to backend so that it can be closed.
 | |
|  * @param dcb The current Backend DCB
 | |
|  * @return 1 always
 | |
|  */
 | |
| static int gw_backend_close(DCB *dcb)
 | |
| {
 | |
|     SESSION* session;
 | |
|     GWBUF* quitbuf;
 | |
| 
 | |
|     CHK_DCB(dcb);
 | |
|     session = dcb->session;
 | |
| 
 | |
|     MXS_DEBUG("%lu [gw_backend_close]", pthread_self());
 | |
| 
 | |
|     quitbuf = mysql_create_com_quit(NULL, 0);
 | |
|     gwbuf_set_type(quitbuf, GWBUF_TYPE_MYSQL);
 | |
| 
 | |
|     /** Send COM_QUIT to the backend being closed */
 | |
|     mysql_send_com_quit(dcb, 0, quitbuf);
 | |
| 
 | |
|     mysql_protocol_done(dcb);
 | |
| 
 | |
|     if (session)
 | |
|     {
 | |
|         CHK_SESSION(session);
 | |
|         /**
 | |
|          * The lock is needed only to protect the read of session->state and
 | |
|          * 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.
 | |
|          */
 | |
|         spinlock_acquire(&session->ses_lock);
 | |
|         /**
 | |
|          * If session->state is STOPPING, start closing client session.
 | |
|          * Otherwise only this backend connection is closed.
 | |
|          */
 | |
|         if (session->state == SESSION_STATE_STOPPING &&
 | |
|             session->client_dcb != NULL)
 | |
|         {
 | |
|             if (session->client_dcb->state == DCB_STATE_POLLING)
 | |
|             {
 | |
|                 spinlock_release(&session->ses_lock);
 | |
| 
 | |
|                 /** Close client DCB */
 | |
|                 dcb_close(session->client_dcb);
 | |
|             }
 | |
|             else
 | |
|             {
 | |
|                 spinlock_release(&session->ses_lock);
 | |
|             }
 | |
|         }
 | |
|         else
 | |
|         {
 | |
|             spinlock_release(&session->ses_lock);
 | |
|         }
 | |
|     }
 | |
|     return 1;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * This routine put into the delay queue the input queue
 | |
|  * The input is what backend DCB is receiving
 | |
|  * The routine is called from func.write() when mysql backend connection
 | |
|  * is not yet complete buu there are inout data from client
 | |
|  *
 | |
|  * @param dcb   The current backend DCB
 | |
|  * @param queue Input data in the GWBUF struct
 | |
|  */
 | |
| static void backend_set_delayqueue(DCB *dcb, GWBUF *queue)
 | |
| {
 | |
|     /* Append data */
 | |
|     dcb->delayq = gwbuf_append(dcb->delayq, queue);
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * This routine writes the delayq via dcb_write
 | |
|  * The dcb->delayq contains data received from the client before
 | |
|  * mysql backend authentication succeded
 | |
|  *
 | |
|  * @param dcb The current backend DCB
 | |
|  * @return The dcb_write status
 | |
|  */
 | |
| static int backend_write_delayqueue(DCB *dcb)
 | |
| {
 | |
|     GWBUF *localq = NULL;
 | |
|     int rc;
 | |
| 
 | |
|     if (dcb->delayq == NULL)
 | |
|     {
 | |
|         rc = 1;
 | |
|     }
 | |
|     else
 | |
|     {
 | |
|         localq = dcb->delayq;
 | |
|         dcb->delayq = NULL;
 | |
| 
 | |
|         if (MYSQL_IS_CHANGE_USER(((uint8_t *)GWBUF_DATA(localq))))
 | |
|         {
 | |
|             MYSQL_session mses;
 | |
|             GWBUF* new_packet;
 | |
| 
 | |
|             gw_get_shared_session_auth_info(dcb, &mses);
 | |
|             new_packet = gw_create_change_user_packet(&mses, dcb->protocol);
 | |
|             /**
 | |
|              * Remove previous packet which lacks scramble
 | |
|              * and append the new.
 | |
|              */
 | |
|             localq = gwbuf_consume(localq, GWBUF_LENGTH(localq));
 | |
|             localq = gwbuf_append(localq, new_packet);
 | |
|         }
 | |
|         rc = dcb_write(dcb, localq);
 | |
|     }
 | |
| 
 | |
|     if (rc == 0)
 | |
|     {
 | |
|         GWBUF* errbuf;
 | |
|         bool succp;
 | |
|         ROUTER_OBJECT *router = NULL;
 | |
|         ROUTER *router_instance = NULL;
 | |
|         void *rsession = NULL;
 | |
|         SESSION *session = dcb->session;
 | |
| 
 | |
|         CHK_SESSION(session);
 | |
| 
 | |
|         if (session != NULL)
 | |
|         {
 | |
|             router = session->service->router;
 | |
|             router_instance = session->service->router_instance;
 | |
|             rsession = session->router_session;
 | |
| #if defined(SS_DEBUG)
 | |
|             MXS_INFO("Backend write delayqueue error handling.");
 | |
| #endif
 | |
|             errbuf = mysql_create_custom_error(1,
 | |
|                                                0,
 | |
|                                                "Failed to write buffered data to back-end server. "
 | |
|                                                "Buffer was empty or back-end was disconnected during "
 | |
|                                                "operation. Attempting to find a new backend.");
 | |
| 
 | |
|             router->handleError(router_instance,
 | |
|                                 rsession,
 | |
|                                 errbuf,
 | |
|                                 dcb,
 | |
|                                 ERRACT_NEW_CONNECTION,
 | |
|                                 &succp);
 | |
|             gwbuf_free(errbuf);
 | |
| 
 | |
|             if (!succp)
 | |
|             {
 | |
|                 spinlock_acquire(&session->ses_lock);
 | |
|                 session->state = SESSION_STATE_STOPPING;
 | |
|                 spinlock_release(&session->ses_lock);
 | |
|             }
 | |
|         }
 | |
|     }
 | |
|     return rc;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * This routine handles the COM_CHANGE_USER command
 | |
|  *
 | |
|  * @param dcb           The current backend DCB
 | |
|  * @param server        The backend server pointer
 | |
|  * @param in_session    The current session data (MYSQL_session)
 | |
|  * @param queue         The GWBUF containing the COM_CHANGE_USER receveid
 | |
|  * @return 1 on success and 0 on failure
 | |
|  */
 | |
| static int gw_change_user(DCB *backend,
 | |
|                           SERVER *server,
 | |
|                           SESSION *in_session,
 | |
|                           GWBUF *queue)
 | |
| {
 | |
|     MYSQL_session *current_session = NULL;
 | |
|     MySQLProtocol *backend_protocol = NULL;
 | |
|     MySQLProtocol *client_protocol = NULL;
 | |
|     char username[MYSQL_USER_MAXLEN + 1] = "";
 | |
|     char database[MYSQL_DATABASE_MAXLEN + 1] = "";
 | |
|     char current_database[MYSQL_DATABASE_MAXLEN + 1] = "";
 | |
|     uint8_t client_sha1[MYSQL_SCRAMBLE_LEN] = "";
 | |
|     uint8_t *client_auth_packet = GWBUF_DATA(queue);
 | |
|     unsigned int auth_token_len = 0;
 | |
|     uint8_t *auth_token = NULL;
 | |
|     int rv = -1;
 | |
|     int auth_ret = 1;
 | |
| 
 | |
|     current_session = (MYSQL_session *)in_session->client_dcb->data;
 | |
|     backend_protocol = backend->protocol;
 | |
|     client_protocol = in_session->client_dcb->protocol;
 | |
| 
 | |
|     /* now get the user, after 4 bytes header and 1 byte command */
 | |
|     client_auth_packet += 5;
 | |
|     size_t len = strlen((char *)client_auth_packet);
 | |
|     if (len > MYSQL_USER_MAXLEN)
 | |
|     {
 | |
|         MXS_ERROR("Client sent user name \"%s\",which is %lu characters long, "
 | |
|                   "while a maximum length of %d is allowed. Cutting trailing "
 | |
|                   "characters.", (char*)client_auth_packet, len, MYSQL_USER_MAXLEN);
 | |
|     }
 | |
|     strncpy(username, (char *)client_auth_packet, MYSQL_USER_MAXLEN);
 | |
|     username[MYSQL_USER_MAXLEN] = 0;
 | |
| 
 | |
|     client_auth_packet += (len + 1);
 | |
| 
 | |
|     /* get the auth token len */
 | |
|     memcpy(&auth_token_len, client_auth_packet, 1);
 | |
| 
 | |
|     client_auth_packet++;
 | |
| 
 | |
|     /* allocate memory for token only if auth_token_len > 0 */
 | |
|     if (auth_token_len > 0)
 | |
|     {
 | |
|         auth_token = (uint8_t *)MXS_MALLOC(auth_token_len);
 | |
|         ss_dassert(auth_token != NULL);
 | |
| 
 | |
|         if (auth_token == NULL)
 | |
|         {
 | |
|             return rv;
 | |
|         }
 | |
|         memcpy(auth_token, client_auth_packet, auth_token_len);
 | |
|         client_auth_packet += auth_token_len;
 | |
|     }
 | |
| 
 | |
|     /* get new database name */
 | |
|     len = strlen((char *)client_auth_packet);
 | |
|     if (len > MYSQL_DATABASE_MAXLEN)
 | |
|     {
 | |
|         MXS_ERROR("Client sent database name \"%s\", which is %lu characters long, "
 | |
|                   "while a maximum length of %d is allowed. Cutting trailing "
 | |
|                   "characters.", (char*)client_auth_packet, len, MYSQL_DATABASE_MAXLEN);
 | |
|     }
 | |
|     strncpy(database, (char *)client_auth_packet, MYSQL_DATABASE_MAXLEN);
 | |
|     database[MYSQL_DATABASE_MAXLEN] = 0;
 | |
| 
 | |
|     client_auth_packet += (len + 1);
 | |
| 
 | |
|     if (*client_auth_packet)
 | |
|     {
 | |
|         memcpy(&backend_protocol->charset, client_auth_packet, sizeof(int));
 | |
|     }
 | |
| 
 | |
|     spinlock_acquire(&in_session->ses_lock);
 | |
| 
 | |
|     /* save current_database name */
 | |
|     strcpy(current_database, current_session->db);
 | |
| 
 | |
|     /*
 | |
|      * Now clear database name in dcb as we don't do local authentication on db name for change user.
 | |
|      * Local authentication only for user@host and if successful the database name change is sent to backend.
 | |
|      */
 | |
|     *current_session->db = 0;
 | |
| 
 | |
|     /*
 | |
|      * 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_dcb,
 | |
|                                             auth_token, auth_token_len,
 | |
|                                             client_protocol->scramble,
 | |
|                                             sizeof(client_protocol->scramble),
 | |
|                                             username, client_sha1);
 | |
|     strcpy(current_session->db, current_database);
 | |
|     spinlock_release(&in_session->ses_lock);
 | |
| 
 | |
|     if (auth_ret != 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);
 | |
|             *current_session->db = 0;
 | |
|             auth_ret = gw_check_mysql_scramble_data(
 | |
|                                                     backend->session->client_dcb,
 | |
|                                                     auth_token, auth_token_len,
 | |
|                                                     client_protocol->scramble,
 | |
|                                                     sizeof(client_protocol->scramble),
 | |
|                                                     username, client_sha1);
 | |
|             strcpy(current_session->db, current_database);
 | |
|             spinlock_release(&in_session->ses_lock);
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /* let's free the auth_token now */
 | |
|     if (auth_token)
 | |
|     {
 | |
|         MXS_FREE(auth_token);
 | |
|     }
 | |
| 
 | |
|     if (auth_ret != 0)
 | |
|     {
 | |
|         char *password_set = NULL;
 | |
|         char *message = NULL;
 | |
| 
 | |
|         if (auth_token_len > 0)
 | |
|         {
 | |
|             password_set = (char *)client_sha1;
 | |
|         }
 | |
|         else
 | |
|         {
 | |
|             password_set = "";
 | |
|         }
 | |
| 
 | |
|         /**
 | |
|          * Create an error message and make it look like legit reply
 | |
|          * from backend server. Then make it look like an incoming event
 | |
|          * so that thread gets new task of it, calls clientReply
 | |
|          * which filters out duplicate errors from same cause and forward
 | |
|          * reply to the client.
 | |
|          */
 | |
|         message = create_auth_fail_str(username,
 | |
|                                        backend->session->client_dcb->remote,
 | |
|                                        password_set,
 | |
|                                        "",
 | |
|                                        auth_ret);
 | |
|         if (message == NULL)
 | |
|         {
 | |
|             MXS_ERROR("Creating error message failed.");
 | |
|             rv = 0;
 | |
|             goto retblock;
 | |
|         }
 | |
|         /**
 | |
|          * Add command to backend's protocol, create artificial reply
 | |
|          * packet and add it to client's read buffer.
 | |
|          */
 | |
|         protocol_add_srv_command((MySQLProtocol*)backend->protocol,
 | |
|                                  MYSQL_COM_CHANGE_USER);
 | |
|         modutil_reply_auth_error(backend, message, 0);
 | |
|         rv = 1;
 | |
|     }
 | |
|     else
 | |
|     {
 | |
|         rv = gw_send_change_user_to_backend(database, username, client_sha1, backend_protocol);
 | |
|         /*
 | |
|          * Now copy new data into user session
 | |
|          */
 | |
|         strcpy(current_session->user, username);
 | |
|         strcpy(current_session->db, database);
 | |
|         memcpy(current_session->client_sha1, client_sha1, sizeof(current_session->client_sha1));
 | |
|     }
 | |
| 
 | |
| retblock:
 | |
|     gwbuf_free(queue);
 | |
| 
 | |
|     return rv;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Move packets or parts of packets from readbuf to outbuf as the packet headers
 | |
|  * and lengths have been noticed and counted.
 | |
|  * Session commands need to be marked so that they can be handled properly in
 | |
|  * the router's clientReply.
 | |
|  *
 | |
|  * @param dcb                   Backend's DCB where data was read from
 | |
|  * @param readbuf               GWBUF where data was read to
 | |
|  * @param nbytes_to_process     Number of bytes that has been read and need to be processed
 | |
|  *
 | |
|  * @return GWBUF which includes complete MySQL packet
 | |
|  */
 | |
| static GWBUF* process_response_data(DCB* dcb,
 | |
|                                     GWBUF* readbuf,
 | |
|                                     int nbytes_to_process)
 | |
| {
 | |
|     int npackets_left = 0; /*< response's packet count */
 | |
|     ssize_t nbytes_left = 0; /*< nbytes to be read for the packet */
 | |
|     MySQLProtocol* p;
 | |
|     GWBUF* outbuf = NULL;
 | |
|     int initial_packets = npackets_left;
 | |
|     ssize_t initial_bytes = nbytes_left;
 | |
| 
 | |
|     /** Get command which was stored in gw_MySQLWrite_backend */
 | |
|     p = DCB_PROTOCOL(dcb, MySQLProtocol);
 | |
|     if (!DCB_IS_CLONE(dcb))
 | |
|     {
 | |
|         CHK_PROTOCOL(p);
 | |
|     }
 | |
| 
 | |
|     /** All buffers processed here are sescmd responses */
 | |
|     gwbuf_set_type(readbuf, GWBUF_TYPE_SESCMD_RESPONSE);
 | |
| 
 | |
|     /**
 | |
|      * Now it is known how many packets there should be and how much
 | |
|      * is read earlier.
 | |
|      */
 | |
|     while (nbytes_to_process != 0)
 | |
|     {
 | |
|         mysql_server_cmd_t srvcmd;
 | |
|         bool succp;
 | |
| 
 | |
|         srvcmd = protocol_get_srv_command(p, false);
 | |
| 
 | |
|         MXS_DEBUG("%lu [process_response_data] Read command %s for DCB %p fd %d.",
 | |
|                   pthread_self(),
 | |
|                   STRPACKETTYPE(srvcmd),
 | |
|                   dcb,
 | |
|                   dcb->fd);
 | |
|         /**
 | |
|          * Read values from protocol structure, fails if values are
 | |
|          * uninitialized.
 | |
|          */
 | |
|         if (npackets_left == 0)
 | |
|         {
 | |
|             succp = protocol_get_response_status(p, &npackets_left, &nbytes_left);
 | |
| 
 | |
|             if (!succp || npackets_left == 0)
 | |
|             {
 | |
|                 /**
 | |
|                  * Examine command type and the readbuf. Conclude response
 | |
|                  * packet count from the command type or from the first
 | |
|                  * packet content. Fails if read buffer doesn't include
 | |
|                  * enough data to read the packet length.
 | |
|                  */
 | |
|                 init_response_status(readbuf, srvcmd, &npackets_left, &nbytes_left);
 | |
|             }
 | |
| 
 | |
|             initial_packets = npackets_left;
 | |
|             initial_bytes = nbytes_left;
 | |
|         }
 | |
|         /** Only session commands with responses should be processed */
 | |
|         ss_dassert(npackets_left > 0);
 | |
| 
 | |
|         /** Read incomplete packet. */
 | |
|         if (nbytes_left > nbytes_to_process)
 | |
|         {
 | |
|             /** Includes length info so it can be processed */
 | |
|             if (nbytes_to_process >= 5)
 | |
|             {
 | |
|                 /** discard source buffer */
 | |
|                 readbuf = gwbuf_consume(readbuf, GWBUF_LENGTH(readbuf));
 | |
|                 nbytes_left -= nbytes_to_process;
 | |
|             }
 | |
|             nbytes_to_process = 0;
 | |
|         }
 | |
|             /** Packet was read. All bytes belonged to the last packet. */
 | |
|         else if (nbytes_left == nbytes_to_process)
 | |
|         {
 | |
|             nbytes_left = 0;
 | |
|             nbytes_to_process = 0;
 | |
|             ss_dassert(npackets_left > 0);
 | |
|             npackets_left -= 1;
 | |
|             outbuf = gwbuf_append(outbuf, readbuf);
 | |
|             readbuf = NULL;
 | |
|         }
 | |
|             /**
 | |
|              * Packet was read. There should be more since bytes were
 | |
|              * left over.
 | |
|              * Move the next packet to its own buffer and add that next
 | |
|              * to the prev packet's buffer.
 | |
|              */
 | |
|         else /*< nbytes_left < nbytes_to_process */
 | |
|         {
 | |
|             ss_dassert(nbytes_left >= 0);
 | |
|             nbytes_to_process -= nbytes_left;
 | |
| 
 | |
|             /** Move the prefix of the buffer to outbuf from redbuf */
 | |
|             outbuf = gwbuf_append(outbuf,
 | |
|                                   gwbuf_clone_portion(readbuf, 0, (size_t) nbytes_left));
 | |
|             readbuf = gwbuf_consume(readbuf, (size_t) nbytes_left);
 | |
|             ss_dassert(npackets_left > 0);
 | |
|             npackets_left -= 1;
 | |
|             nbytes_left = 0;
 | |
|         }
 | |
| 
 | |
|         /** Store new status to protocol structure */
 | |
|         protocol_set_response_status(p, npackets_left, nbytes_left);
 | |
| 
 | |
|         /** A complete packet was read */
 | |
|         if (nbytes_left == 0)
 | |
|         {
 | |
|             /** No more packets in this response */
 | |
|             if (npackets_left == 0 && outbuf != NULL)
 | |
|             {
 | |
|                 GWBUF* b = outbuf;
 | |
| 
 | |
|                 while (b->next != NULL)
 | |
|                 {
 | |
|                     b = b->next;
 | |
|                 }
 | |
|                 /** Mark last as end of response */
 | |
|                 gwbuf_set_type(b, GWBUF_TYPE_RESPONSE_END);
 | |
| 
 | |
|                 /** Archive the command */
 | |
|                 protocol_archive_srv_command(p);
 | |
|             }
 | |
|                 /** Read next packet */
 | |
|             else
 | |
|             {
 | |
|                 uint8_t* data;
 | |
| 
 | |
|                 /** Read next packet length if there is at least
 | |
|                  * three bytes left. If there is less than three
 | |
|                  * bytes in the buffer or it is NULL, we need to
 | |
|                  wait for more data from the backend server.*/
 | |
|                 if (readbuf == NULL || gwbuf_length(readbuf) < 3)
 | |
|                 {
 | |
|                     MXS_DEBUG("%lu [%s] Read %d packets. Waiting for %d more "
 | |
|                               "packets for a total of %d packets.",
 | |
|                               pthread_self(), __FUNCTION__,
 | |
|                               initial_packets - npackets_left,
 | |
|                               npackets_left, initial_packets);
 | |
| 
 | |
|                     /** Store the already read data into the readqueue of the DCB
 | |
|                      * and restore the response status to the initial number of packets */
 | |
|                     spinlock_acquire(&dcb->authlock);
 | |
|                     dcb->dcb_readqueue = gwbuf_append(outbuf, dcb->dcb_readqueue);
 | |
|                     spinlock_release(&dcb->authlock);
 | |
|                     protocol_set_response_status(p, initial_packets, initial_bytes);
 | |
|                     return NULL;
 | |
|                 }
 | |
|                 uint8_t packet_len[3];
 | |
|                 gwbuf_copy_data(readbuf, 0, 3, packet_len);
 | |
|                 nbytes_left = gw_mysql_get_byte3(packet_len) + MYSQL_HEADER_LEN;
 | |
|                 /** Store new status to protocol structure */
 | |
|                 protocol_set_response_status(p, npackets_left, nbytes_left);
 | |
|             }
 | |
|         }
 | |
|     }
 | |
|     return outbuf;
 | |
| }
 | |
| 
 | |
| static bool sescmd_response_complete(DCB* dcb)
 | |
| {
 | |
|     int npackets_left;
 | |
|     ssize_t nbytes_left;
 | |
|     MySQLProtocol* p;
 | |
|     bool succp;
 | |
| 
 | |
|     p = DCB_PROTOCOL(dcb, MySQLProtocol);
 | |
|     if (!DCB_IS_CLONE(dcb))
 | |
|     {
 | |
|         CHK_PROTOCOL(p);
 | |
|     }
 | |
| 
 | |
|     protocol_get_response_status(p, &npackets_left, &nbytes_left);
 | |
| 
 | |
|     if (npackets_left == 0)
 | |
|     {
 | |
|         succp = true;
 | |
|     }
 | |
|     else
 | |
|     {
 | |
|         succp = false;
 | |
|     }
 | |
|     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;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Receive the MySQL authentication packet from backend, packet # is 2
 | |
|  *
 | |
|  * @param protocol The MySQL protocol structure
 | |
|  * @return -1 in case of failure, 0 if there was nothing to read, 1 if read
 | |
|  * was successful.
 | |
|  */
 | |
| static int
 | |
| gw_receive_backend_auth(MySQLProtocol *protocol)
 | |
| {
 | |
|     int n = -1;
 | |
|     GWBUF *head = NULL;
 | |
|     DCB *dcb = protocol->owner_dcb;
 | |
|     uint8_t *ptr = NULL;
 | |
|     int rc = 0;
 | |
| 
 | |
|     n = dcb_read(dcb, &head, 0);
 | |
| 
 | |
|     dcb->last_read = hkheartbeat;
 | |
| 
 | |
|     /*<
 | |
|      * Read didn't fail and there is enough data for mysql packet.
 | |
|      */
 | |
|     if (n != -1 &&
 | |
|         head != NULL &&
 | |
|         GWBUF_LENGTH(head) >= 5)
 | |
|     {
 | |
|         ptr = GWBUF_DATA(head);
 | |
|         /*<
 | |
|          * 5th byte is 0x0 if successful.
 | |
|          */
 | |
|         if (ptr[4] == 0x00)
 | |
|         {
 | |
|             rc = 1;
 | |
|         }
 | |
|         else if (ptr[4] == 0xff)
 | |
|         {
 | |
|             size_t len = MYSQL_GET_PACKET_LEN(ptr);
 | |
|             char* err = strndup(&((char *)ptr)[8], 5);
 | |
|             char* bufstr = strndup(&((char *)ptr)[13], len - 4 - 5);
 | |
| 
 | |
|             MXS_DEBUG("%lu [gw_receive_backend_auth] Invalid "
 | |
|                       "authentication message from backend dcb %p "
 | |
|                       "fd %d, ptr[4] = %d, error %s, msg %s.",
 | |
|                       pthread_self(),
 | |
|                       dcb,
 | |
|                       dcb->fd,
 | |
|                       ptr[4],
 | |
|                       err,
 | |
|                       bufstr);
 | |
| 
 | |
|             MXS_ERROR("Invalid authentication message "
 | |
|                       "from backend. Error : %s, Msg : %s",
 | |
|                       err,
 | |
|                       bufstr);
 | |
| 
 | |
|             MXS_FREE(bufstr);
 | |
|             MXS_FREE(err);
 | |
|             rc = -1;
 | |
|         }
 | |
|         else
 | |
|         {
 | |
|             MXS_DEBUG("%lu [gw_receive_backend_auth] Invalid "
 | |
|                       "authentication message from backend dcb %p "
 | |
|                       "fd %d, ptr[4] = %d",
 | |
|                       pthread_self(),
 | |
|                       dcb,
 | |
|                       dcb->fd,
 | |
|                       ptr[4]);
 | |
| 
 | |
|             MXS_ERROR("Invalid authentication message "
 | |
|                       "from backend. Packet type : %d",
 | |
|                       ptr[4]);
 | |
|         }
 | |
|         /*<
 | |
|          * Remove data from buffer.
 | |
|          */
 | |
|         while ((head = gwbuf_consume(head, GWBUF_LENGTH(head))) != NULL)
 | |
|         {
 | |
|             ;
 | |
|         }
 | |
|     }
 | |
|     else if (n == 0)
 | |
|     {
 | |
|         /*<
 | |
|          * This is considered as success because call didn't fail,
 | |
|          * although no bytes was read.
 | |
|          */
 | |
|         rc = 0;
 | |
|         MXS_DEBUG("%lu [gw_receive_backend_auth] Read zero bytes from "
 | |
|                   "backend dcb %p fd %d in state %s. n %d, head %p, len %ld",
 | |
|                   pthread_self(),
 | |
|                   dcb,
 | |
|                   dcb->fd,
 | |
|                   STRDCBSTATE(dcb->state),
 | |
|                   n,
 | |
|                   head,
 | |
|                   (head == NULL) ? 0 : GWBUF_LENGTH(head));
 | |
|     }
 | |
|     else
 | |
|     {
 | |
|         ss_dassert(n < 0 && head == NULL);
 | |
|         rc = -1;
 | |
|         MXS_DEBUG("%lu [gw_receive_backend_auth] Reading from backend dcb %p "
 | |
|                   "fd %d in state %s failed. n %d, head %p, len %ld",
 | |
|                   pthread_self(),
 | |
|                   dcb,
 | |
|                   dcb->fd,
 | |
|                   STRDCBSTATE(dcb->state),
 | |
|                   n,
 | |
|                   head,
 | |
|                   (head == NULL) ? 0 : GWBUF_LENGTH(head));
 | |
|     }
 | |
| 
 | |
|     return rc;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * @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)
 | |
| {
 | |
|     /*< Close newly created socket. */
 | |
|     if (close(sock) != 0)
 | |
|     {
 | |
|         char errbuf[STRERROR_BUFLEN];
 | |
|         MXS_ERROR("Failed to close socket %d due %d, %s.",
 | |
|             sock,
 | |
|             errno,
 | |
|             strerror_r(errno, errbuf, sizeof(errbuf)));
 | |
|     }
 | |
| 
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Create COM_CHANGE_USER packet and store it to GWBUF
 | |
|  *
 | |
|  * @param mses          MySQL session
 | |
|  * @param protocol      protocol structure of the backend
 | |
|  *
 | |
|  * @return GWBUF buffer consisting of COM_CHANGE_USER packet
 | |
|  *
 | |
|  * @note the function doesn't fail
 | |
|  */
 | |
| static GWBUF *
 | |
| gw_create_change_user_packet(MYSQL_session*  mses,
 | |
|                                     MySQLProtocol*  protocol)
 | |
| {
 | |
|     char* db;
 | |
|     char* user;
 | |
|     uint8_t* pwd;
 | |
|     GWBUF* buffer;
 | |
|     int compress = 0;
 | |
|     uint8_t* payload = NULL;
 | |
|     uint8_t* payload_start = NULL;
 | |
|     long bytes;
 | |
|     char dbpass[MYSQL_USER_MAXLEN + 1]="";
 | |
|     char* curr_db = NULL;
 | |
|     uint8_t* curr_passwd = NULL;
 | |
|     unsigned int charset;
 | |
| 
 | |
|     db = mses->db;
 | |
|     user = mses->user;
 | |
|     pwd = mses->client_sha1;
 | |
| 
 | |
|     if (strlen(db) > 0)
 | |
|     {
 | |
|         curr_db = db;
 | |
|     }
 | |
| 
 | |
|     if (memcmp(pwd, null_client_sha1, MYSQL_SCRAMBLE_LEN))
 | |
|     {
 | |
|         curr_passwd = pwd;
 | |
|     }
 | |
| 
 | |
|     /* get charset the client sent and use it for connection auth */
 | |
|     charset = protocol->charset;
 | |
| 
 | |
|     if (compress)
 | |
|     {
 | |
| #ifdef DEBUG_MYSQL_CONN
 | |
|         fprintf(stderr, ">>>> Backend Connection with compression\n");
 | |
| #endif
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Protocol MySQL COM_CHANGE_USER for CLIENT_PROTOCOL_41
 | |
|      * 1 byte COMMAND
 | |
|      */
 | |
|     bytes = 1;
 | |
| 
 | |
|     /** add the user and a terminating char */
 | |
|     bytes += strlen(user);
 | |
|     bytes++;
 | |
|     /**
 | |
|      * next will be + 1 (scramble_len) + 20 (fixed_scramble) +
 | |
|      * (db + NULL term) + 2 bytes charset
 | |
|      */
 | |
|     if (curr_passwd != NULL)
 | |
|     {
 | |
|         bytes += GW_MYSQL_SCRAMBLE_SIZE;
 | |
|     }
 | |
|     /** 1 byte for scramble_len */
 | |
|     bytes++;
 | |
|     /** db name and terminating char */
 | |
|     if (curr_db != NULL)
 | |
|     {
 | |
|         bytes += strlen(curr_db);
 | |
|     }
 | |
|     bytes++;
 | |
| 
 | |
|     /** the charset */
 | |
|     bytes += 2;
 | |
|     bytes += strlen("mysql_native_password");
 | |
|     bytes++;
 | |
| 
 | |
|     /** the packet header */
 | |
|     bytes += 4;
 | |
| 
 | |
|     buffer = gwbuf_alloc(bytes);
 | |
|     /**
 | |
|      * Set correct type to GWBUF so that it will be handled like session
 | |
|      * commands
 | |
|      */
 | |
|     buffer->gwbuf_type = GWBUF_TYPE_MYSQL|GWBUF_TYPE_SINGLE_STMT|GWBUF_TYPE_SESCMD;
 | |
|     payload = GWBUF_DATA(buffer);
 | |
|     memset(payload, '\0', bytes);
 | |
|     payload_start = payload;
 | |
| 
 | |
|     /** set packet number to 0 */
 | |
|     payload[3] = 0x00;
 | |
|     payload += 4;
 | |
| 
 | |
|     /** set the command COM_CHANGE_USER 0x11 */
 | |
|     payload[0] = 0x11;
 | |
|     payload++;
 | |
|     memcpy(payload, user, strlen(user));
 | |
|     payload += strlen(user);
 | |
|     payload++;
 | |
| 
 | |
|     if (curr_passwd != NULL)
 | |
|     {
 | |
|         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, pwd, 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);
 | |
| 
 | |
|         /** dbpass is the HEX form of SHA1(SHA1(real_password)) */
 | |
|         gw_bin2hex(dbpass, hash2, GW_MYSQL_SCRAMBLE_SIZE);
 | |
| 
 | |
|         /** new_sha is the SHA1(CONCAT(scramble, hash2) */
 | |
|         gw_sha1_2_str(protocol->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;
 | |
|     }
 | |
|     else
 | |
|     {
 | |
|         /** skip the auth-length and leave the byte as NULL */
 | |
|         payload++;
 | |
|     }
 | |
|     /** if the db is not NULL append it */
 | |
|     if (curr_db != NULL)
 | |
|     {
 | |
|         memcpy(payload, curr_db, strlen(curr_db));
 | |
|         payload += strlen(curr_db);
 | |
|     }
 | |
|     payload++;
 | |
|     /** set the charset, 2 bytes */
 | |
|     *payload = charset;
 | |
|     payload++;
 | |
|     *payload = '\x00';
 | |
|     payload++;
 | |
|     memcpy(payload, "mysql_native_password", strlen("mysql_native_password"));
 | |
|     /* Following needed if more to be added */
 | |
|     /* payload += strlen("mysql_native_password"); */
 | |
|     /** put here the paylod size: bytes to write - 4 bytes packet header */
 | |
|     gw_mysql_set_byte3(payload_start, (bytes-4));
 | |
| 
 | |
|     return buffer;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Write a MySQL CHANGE_USER 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)
 | |
|  * @return 1 on success, 0 on failure
 | |
|  */
 | |
| static int
 | |
| gw_send_change_user_to_backend(char          *dbname,
 | |
|                                    char          *user,
 | |
|                                    uint8_t       *passwd,
 | |
|                                    MySQLProtocol *conn)
 | |
| {
 | |
|     GWBUF *buffer;
 | |
|     int rc;
 | |
|     MYSQL_session*  mses;
 | |
| 
 | |
|     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);
 | |
| 
 | |
|     if (rc != 0)
 | |
|     {
 | |
|         rc = 1;
 | |
|     }
 | |
|     return rc;
 | |
| }
 |