/* * This file is distributed as part of the MariaDB Corporation MaxScale. It is free * software: you can redistribute it and/or modify it under the terms of the * GNU General Public License as published by the Free Software Foundation, * version 2. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License along with * this program; if not, write to the Free Software Foundation, Inc., 51 * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. * * Copyright MariaDB Corporation Ab 2013-2014 */ /* * MySQL Protocol common routines for client to gateway and gateway to backend * * Revision History * Date Who Description * 17/06/2013 Massimiliano Pinto Common MySQL protocol routines * 02/06/2013 Massimiliano Pinto MySQL connect asynchronous phases * 04/09/2013 Massimiliano Pinto Added dcb NULL assert in mysql_send_custom_error * 12/09/2013 Massimiliano Pinto Added checks in gw_decode_mysql_server_handshake and gw_read_backend_handshake * 10/02/2014 Massimiliano Pinto Added MySQL Authentication with user@host * 10/09/2014 Massimiliano Pinto Added MySQL Authentication option enabling localhost match with any host (wildcard %) * Backend server configuration may differ so default is 0, don't match and an explicit * localhost entry should be added for the selected user in the backends. * Setting to 1 allow localhost (127.0.0.1 or socket) to match the any host grant via * user@% * 29/09/2014 Massimiliano Pinto Added Mysql user@host authentication with wildcard in IPv4 hosts: * x.y.z.%, x.y.%.%, x.%.%.% * 03/10/2014 Massimiliano Pinto Added netmask for wildcard in IPv4 hosts. * 24/10/2014 Massimiliano Pinto Added Mysql user@host @db authentication support * 10/11/2014 Massimiliano Pinto Charset at connect is passed to backend during authentication * 07/07/15 Martin Brampton Fix problem recognising null password * */ #include #include "mysql_client_server_protocol.h" #include #include #include #include /* The following can be compared using memcmp to detect a null password */ uint8_t null_client_sha1[MYSQL_SCRAMBLE_LEN]=""; /** Defined in log_manager.cc */ extern int lm_enabled_logfiles_bitmask; extern size_t log_ses_count[]; extern __thread log_info_t tls_log_info; extern int gw_read_backend_event(DCB* dcb); extern int gw_write_backend_event(DCB *dcb); extern int gw_MySQLWrite_backend(DCB *dcb, GWBUF *queue); extern int gw_error_backend_event(DCB *dcb); char* get_username_from_auth(char* ptr, uint8_t* data); static server_command_t* server_command_init(server_command_t* srvcmd, mysql_server_cmd_t cmd); /** * Creates MySQL protocol structure * * @param dcb * Must be non-NULL. * @param fd * * @return * * * @details Protocol structure does not have fd because dcb is not * connected yet. * */ MySQLProtocol* mysql_protocol_init( DCB* dcb, int fd) { MySQLProtocol* p; p = (MySQLProtocol *) calloc(1, sizeof(MySQLProtocol)); ss_dassert(p != NULL); if (p == NULL) { int eno = errno; errno = 0; LOGIF(LE, (skygw_log_write_flush( LOGFILE_ERROR, "%lu [mysql_init_protocol] MySQL protocol init failed : " "memory allocation due error %d, %s.", pthread_self(), eno, strerror(eno)))); goto return_p; } p->protocol_state = MYSQL_PROTOCOL_ALLOC; p->protocol_auth_state = MYSQL_ALLOC; p->protocol_command.scom_cmd = MYSQL_COM_UNDEFINED; p->protocol_command.scom_nresponse_packets = 0; p->protocol_command.scom_nbytes_to_read = 0; #if defined(SS_DEBUG) p->protocol_chk_top = CHK_NUM_PROTOCOL; p->protocol_chk_tail = CHK_NUM_PROTOCOL; #endif /*< Assign fd with protocol */ p->fd = fd; p->owner_dcb = dcb; p->protocol_state = MYSQL_PROTOCOL_ACTIVE; CHK_PROTOCOL(p); return_p: return p; } /** * mysql_protocol_done * * free protocol allocations. * * @param dcb owner DCB * */ void mysql_protocol_done ( DCB* dcb) { MySQLProtocol* p; server_command_t* scmd; server_command_t* scmd2; p = (MySQLProtocol *)dcb->protocol; spinlock_acquire(&p->protocol_lock); if (p->protocol_state != MYSQL_PROTOCOL_ACTIVE) { goto retblock; } scmd = p->protocol_cmd_history; while (scmd != NULL) { scmd2 = scmd->scom_next; free(scmd); scmd = scmd2; } p->protocol_state = MYSQL_PROTOCOL_DONE; retblock: spinlock_release(&p->protocol_lock); } /** * Read the backend server MySQL handshake * * @param conn MySQL protocol structure * @return 0 on success, 1 on failure */ int gw_read_backend_handshake( MySQLProtocol *conn) { GWBUF *head = NULL; DCB *dcb = conn->owner_dcb; int n = -1; uint8_t *payload = NULL; int h_len = 0; int success = 0; int packet_len = 0; if ((n = 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; LOGIF(LD, (skygw_log_write( LOGFILE_DEBUG, "%lu [gw_read_backend_handshake] after " "dcb_read, fd %d, " "state = MYSQL_HANDSHAKE_FAILED.", dcb->fd, pthread_self()))); 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; LOGIF(LD, (skygw_log_write( LOGFILE_DEBUG, "%lu [gw_receive_backend_auth] Invalid " "authentication message from backend dcb %p " "fd %d, ptr[4] = %p, error code %d, msg %s.", pthread_self(), dcb, dcb->fd, payload[4], errcode, bufstr))); LOGIF(LE, (skygw_log_write_flush( LOGFILE_ERROR, "Error : Invalid authentication message " "from backend. Error code: %d, Msg : %s", 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) { LOGIF(LE, (skygw_log_write_flush( LOGFILE_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); } 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; LOGIF(LD, (skygw_log_write( LOGFILE_DEBUG, "%lu [gw_read_backend_handshake] after " "gw_mysql_get_byte3, fd %d, " "state = MYSQL_HANDSHAKE_FAILED.", pthread_self(), dcb->fd, pthread_self()))); 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; LOGIF(LD, (skygw_log_write( LOGFILE_DEBUG, "%lu [gw_read_backend_handshake] after " "gw_decode_mysql_server_handshake, fd %d, " "state = MYSQL_HANDSHAKE_FAILED.", pthread_self(), conn->owner_dcb->fd, pthread_self()))); while((head = gwbuf_consume(head, GWBUF_LENGTH(head)))); return 1; } conn->protocol_auth_state = MYSQL_AUTH_SENT; // consume all the data here head = gwbuf_consume(head, GWBUF_LENGTH(head)); return 0; } } // Nothing done here, log error this return 1; } /** * 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 * */ 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 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(scramble, scramble_data_1, GW_SCRAMBLE_LENGTH_323); memcpy(scramble + GW_SCRAMBLE_LENGTH_323, scramble_data_2, scramble_len - GW_SCRAMBLE_LENGTH_323); // full 20 bytes scramble is ready memcpy(conn->scramble, 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. */ 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); LOGIF(LD, (skygw_log_write( LOGFILE_DEBUG, "%lu [gw_receive_backend_auth] Invalid " "authentication message from backend dcb %p " "fd %d, ptr[4] = %p, error %s, msg %s.", pthread_self(), dcb, dcb->fd, ptr[4], err, bufstr))); LOGIF(LE, (skygw_log_write_flush( LOGFILE_ERROR, "Error : Invalid authentication message " "from backend. Error : %s, Msg : %s", err, bufstr))); free(bufstr); free(err); rc = -1; } else { LOGIF(LD, (skygw_log_write( LOGFILE_DEBUG, "%lu [gw_receive_backend_auth] Invalid " "authentication message from backend dcb %p " "fd %d, ptr[4] = %p", pthread_self(), dcb, dcb->fd, ptr[4]))); LOGIF(LE, (skygw_log_write_flush( LOGFILE_ERROR, "Error : Invalid authentication message " "from backend. Packet type : %p", 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; LOGIF(LD, (skygw_log_write( LOGFILE_DEBUG, "%lu [gw_receive_backend_auth] Read zero bytes from " "backend dcb %p fd %d in state %s. n %d, head %p, len %d", 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; LOGIF(LD, (skygw_log_write_flush( LOGFILE_DEBUG, "%lu [gw_receive_backend_auth] Reading from backend dcb %p " "fd %d in state %s failed. n %d, head %p, len %d", pthread_self(), dcb, dcb->fd, STRDCBSTATE(dcb->state), n, head, (head == NULL) ? 0 : GWBUF_LENGTH(head)))); } return rc; } /** * 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 0 on success, 1 on failure */ int gw_send_authentication_to_backend( char *dbname, char *user, uint8_t *passwd, MySQLProtocol *conn) { int compress = 0; int rv; uint8_t *payload = NULL; uint8_t *payload_start = NULL; long bytes; uint8_t client_scramble[GW_MYSQL_SCRAMBLE_SIZE]; uint8_t client_capabilities[4]; uint32_t server_capabilities = 0; uint32_t final_capabilities = 0; char dbpass[MYSQL_USER_MAXLEN + 1]=""; GWBUF *buffer; DCB *dcb; char *curr_db = NULL; uint8_t *curr_passwd = NULL; unsigned int charset; /** * 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 1; } if (strlen(dbname)) curr_db = dbname; if (memcmp(passwd, null_client_sha1, MYSQL_SCRAMBLE_LEN)) curr_passwd = passwd; dcb = conn->owner_dcb; final_capabilities = gw_mysql_get_byte4((uint8_t *)&server_capabilities); /** Copy client's flags to backend but with the known capabilities mask */ final_capabilities |= (conn->client_capabilities & GW_MYSQL_CAPABILITIES_CLIENT); /* get charset the client sent and use it for connection auth */ charset = conn->charset; if (compress) { final_capabilities |= GW_MYSQL_CAPABILITIES_COMPRESS; #ifdef DEBUG_MYSQL_CONN fprintf(stderr, ">>>> Backend Connection with compression\n"); #endif } 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]=""; // 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); // 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(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); } if (curr_db == NULL) { // without db final_capabilities &= ~GW_MYSQL_CAPABILITIES_CONNECT_WITH_DB; } else { final_capabilities |= GW_MYSQL_CAPABILITIES_CONNECT_WITH_DB; } final_capabilities |= GW_MYSQL_CAPABILITIES_PLUGIN_AUTH; gw_mysql_set_byte4(client_capabilities, final_capabilities); // 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; bytes += strlen(user); // the NULL bytes++; // next will be + 1 (scramble_len) + 20 (fixed_scramble) + 1 (user NULL term) + 1 (db NULL term) if (curr_passwd != NULL) { bytes++; bytes += GW_MYSQL_SCRAMBLE_SIZE; } else { bytes++; } if (curr_db != NULL) { bytes += strlen(curr_db); bytes++; } bytes +=strlen("mysql_native_password"); bytes++; // the packet header bytes += 4; // allocating the GWBUF buffer = gwbuf_alloc(bytes); payload = GWBUF_DATA(buffer); // clearing data memset(payload, '\0', bytes); // save the start pointer payload_start = payload; // set packet # = 1 payload[3] = '\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 = charset; payload++; // 23 bytes of 0 payload += 23; // 4 + 4 + 4 + 1 + 23 = 36, this includes the 4 bytes packet header memcpy(payload, user, strlen(user)); payload += strlen(user); payload++; if (curr_passwd != NULL) { // 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 write a 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++; } memcpy(payload, "mysql_native_password", strlen("mysql_native_password")); payload += strlen("mysql_native_password"); payload++; // put here the paylod size: bytes to write - 4 bytes packet header gw_mysql_set_byte3(payload_start, (bytes-4)); rv = dcb_write(dcb, buffer); if (rv < 0) { return rv; } else { return 0; } } /** * 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 succesful, fd has file descriptor to socket which is connected to * backend server. In failure, fd == -1 and socket is closed. * */ 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 = AF_INET; so = socket(AF_INET,SOCK_STREAM,0); if (so < 0) { LOGIF(LE, (skygw_log_write_flush( LOGFILE_ERROR, "Error: Establishing connection to backend server " "%s:%d failed.\n\t\t Socket creation failed " "due %d, %s.", host, port, errno, strerror(errno)))); 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) { LOGIF(LE, (skygw_log_write_flush( LOGFILE_ERROR, "Error: Failed to set socket options " "%s:%d failed.\n\t\t Socket configuration failed " "due %d, %s.", host, port, errno, strerror(errno)))); rv = -1; /** Close socket */ goto close_so; } bufsize = GW_BACKEND_SO_RCVBUF; if(setsockopt(so, SOL_SOCKET, SO_RCVBUF, &bufsize, sizeof(bufsize)) != 0) { LOGIF(LE, (skygw_log_write_flush( LOGFILE_ERROR, "Error: Failed to set socket options " "%s:%d failed.\n\t\t Socket configuration failed " "due %d, %s.", host, port, errno, strerror(errno)))); rv = -1; /** Close socket */ goto close_so; } int one = 1; if(setsockopt(so, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one)) != 0) { LOGIF(LE, (skygw_log_write_flush( LOGFILE_ERROR, "Error: Failed to set socket options " "%s:%d failed.\n\t\t Socket configuration failed " "due %d, %s.", host, port, errno, strerror(errno)))); rv = -1; /** Close socket */ goto close_so; } /* 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 { LOGIF(LE, (skygw_log_write_flush( LOGFILE_ERROR, "Error: Failed to connect backend server %s:%d, " "due %d, %s.", host, port, errno, strerror(errno)))); /** Close socket */ goto close_so; } } *fd = so; LOGIF(LD, (skygw_log_write_flush( LOGFILE_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; close_so: /*< Close newly created socket. */ if (close(so) != 0) { LOGIF(LE, (skygw_log_write_flush( LOGFILE_ERROR, "Error: Failed to " "close socket %d due %d, %s.", so, errno, strerror(errno)))); } goto return_rv; } /** * Return a string representation of a MySQL protocol state. * * @param state The protocol state * @return String representation of the state * */ const char * gw_mysql_protocol_state2string (int state) { switch(state) { case MYSQL_ALLOC: return "MySQL Protocl struct allocated"; case MYSQL_PENDING_CONNECT: return "MySQL Backend socket PENDING connect"; case MYSQL_CONNECTED: return "MySQL Backend socket CONNECTED"; case MYSQL_AUTH_SENT: return "MySQL Authentication handshake has been sent"; case MYSQL_AUTH_RECV: return "MySQL Received user, password, db and capabilities"; case MYSQL_AUTH_FAILED: return "MySQL Authentication failed"; case MYSQL_IDLE: return "MySQL authentication is succesfully done."; case MYSQL_AUTH_SSL_REQ: return "MYSQL_AUTH_SSL_REQ"; case MYSQL_AUTH_SSL_HANDSHAKE_DONE: return "MYSQL_AUTH_SSL_HANDSHAKE_DONE"; case MYSQL_AUTH_SSL_HANDSHAKE_FAILED: return "MYSQL_AUTH_SSL_HANDSHAKE_FAILED"; case MYSQL_AUTH_SSL_HANDSHAKE_ONGOING: return "MYSQL_AUTH_SSL_HANDSHAKE_ONGOING"; default: return "MySQL (unknown protocol state)"; } } GWBUF* mysql_create_com_quit( GWBUF* bufparam, int packet_number) { uint8_t* data; GWBUF* buf; if (bufparam == NULL) { buf = gwbuf_alloc(COM_QUIT_PACKET_SIZE); } else { buf = bufparam; } if (buf == NULL) { return 0; } ss_dassert(GWBUF_LENGTH(buf) == COM_QUIT_PACKET_SIZE); data = GWBUF_DATA(buf); *data++ = 0x1; *data++ = 0x0; *data++ = 0x0; *data++ = packet_number; *data = 0x1; return buf; } int mysql_send_com_quit( DCB* dcb, int packet_number, GWBUF* bufparam) { GWBUF *buf; int nbytes = 0; CHK_DCB(dcb); ss_dassert(packet_number <= 255); if (dcb == NULL || dcb->state == DCB_STATE_ZOMBIE) { return 0; } if (bufparam == NULL) { buf = mysql_create_com_quit(NULL, packet_number); } else { buf = bufparam; } if (buf == NULL) { return 0; } nbytes = dcb->func.write(dcb, buf); return nbytes; } GWBUF* mysql_create_custom_error( int packet_number, int affected_rows, const char* msg) { uint8_t* outbuf = NULL; uint32_t mysql_payload_size = 0; uint8_t mysql_packet_header[4]; uint8_t* mysql_payload = NULL; uint8_t field_count = 0; uint8_t mysql_err[2]; uint8_t mysql_statemsg[6]; unsigned int mysql_errno = 0; const char* mysql_error_msg = NULL; const char* mysql_state = NULL; GWBUF* errbuf = NULL; mysql_errno = 2003; mysql_error_msg = "An errorr occurred ..."; mysql_state = "HY000"; field_count = 0xff; gw_mysql_set_byte2(mysql_err, mysql_errno); mysql_statemsg[0]='#'; memcpy(mysql_statemsg+1, mysql_state, 5); if (msg != NULL) { mysql_error_msg = msg; } mysql_payload_size = sizeof(field_count) + sizeof(mysql_err) + sizeof(mysql_statemsg) + strlen(mysql_error_msg); /** allocate memory for packet header + payload */ errbuf = gwbuf_alloc(sizeof(mysql_packet_header) + mysql_payload_size); ss_dassert(errbuf != NULL); if (errbuf == NULL) { return 0; } outbuf = GWBUF_DATA(errbuf); /** write packet header and packet number */ gw_mysql_set_byte3(mysql_packet_header, mysql_payload_size); mysql_packet_header[3] = packet_number; /** write header */ memcpy(outbuf, mysql_packet_header, sizeof(mysql_packet_header)); mysql_payload = outbuf + sizeof(mysql_packet_header); /** write field */ memcpy(mysql_payload, &field_count, sizeof(field_count)); mysql_payload = mysql_payload + sizeof(field_count); /** write errno */ memcpy(mysql_payload, mysql_err, sizeof(mysql_err)); mysql_payload = mysql_payload + sizeof(mysql_err); /** write sqlstate */ memcpy(mysql_payload, mysql_statemsg, sizeof(mysql_statemsg)); mysql_payload = mysql_payload + sizeof(mysql_statemsg); /** write error message */ memcpy(mysql_payload, mysql_error_msg, strlen(mysql_error_msg)); return errbuf; } /** * mysql_send_custom_error * * Send a MySQL protocol Generic ERR message, to the dcb * Note the errno and state are still fixed now * * @param dcb Owner_Dcb Control Block for the connection to which the OK is sent * @param packet_number * @param in_affected_rows * @param mysql_message * @return 1 Non-zero if data was sent * */ int mysql_send_custom_error ( DCB *dcb, int packet_number, int in_affected_rows, const char *mysql_message) { GWBUF* buf; buf = mysql_create_custom_error(packet_number, in_affected_rows, mysql_message); return dcb->func.write(dcb, buf); } /** * 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 */ 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; uint8_t client_scramble[GW_MYSQL_SCRAMBLE_SIZE]; uint32_t server_capabilities = 0; uint32_t final_capabilities = 0; 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; } final_capabilities = gw_mysql_get_byte4((uint8_t *)&server_capabilities); /** Copy client's flags to backend */ final_capabilities |= protocol->client_capabilities; /* get charset the client sent and use it for connection auth */ charset = protocol->charset; if (compress) { final_capabilities |= GW_MYSQL_CAPABILITIES_COMPRESS; #ifdef DEBUG_MYSQL_CONN fprintf(stderr, ">>>> Backend Connection with compression\n"); #endif } 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]=""; /** 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); } if (curr_db == NULL) { final_capabilities &= ~GW_MYSQL_CAPABILITIES_CONNECT_WITH_DB; } else { final_capabilities |= GW_MYSQL_CAPABILITIES_CONNECT_WITH_DB; } final_capabilities |= GW_MYSQL_CAPABILITIES_PLUGIN_AUTH; /** * 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) { /** 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 write a 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")); payload += strlen("mysql_native_password"); payload++; /** 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 */ 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->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; } /** * gw_check_mysql_scramble_data * * Check authentication token received against stage1_hash and scramble * * @param dcb The current dcb * @param token The token sent by the client in the authentication request * @param token_len The token size in bytes * @param scramble The scramble data sent by the server during handshake * @param scramble_len The scrable size in bytes * @param username The current username in the authentication request * @param stage1_hash The SHA1(candidate_password) decoded by this routine * @return 0 on succesful check or 1 on failure * */ int gw_check_mysql_scramble_data(DCB *dcb, uint8_t *token, unsigned int token_len, uint8_t *scramble, unsigned int scramble_len, char *username, uint8_t *stage1_hash) { uint8_t step1[GW_MYSQL_SCRAMBLE_SIZE]=""; uint8_t step2[GW_MYSQL_SCRAMBLE_SIZE +1]=""; uint8_t check_hash[GW_MYSQL_SCRAMBLE_SIZE]=""; char hex_double_sha1[2 * GW_MYSQL_SCRAMBLE_SIZE + 1]=""; uint8_t password[GW_MYSQL_SCRAMBLE_SIZE]=""; int ret_val = 1; if ((username == NULL) || (scramble == NULL) || (stage1_hash == NULL)) { return 1; } /*< * get the user's password from repository in SHA1(SHA1(real_password)); * please note 'real_password' is unknown! */ ret_val = gw_find_mysql_user_password_sha1(username, password, dcb); if (ret_val) { /* if password was sent, fill stage1_hash with at least 1 byte in order * to create right error message: (using password: YES|NO) */ if (token_len) memcpy(stage1_hash, (char *)"_", 1); return 1; } if (token && token_len) { /*< * convert in hex format: this is the content of mysql.user table. * The field password is without the '*' prefix and it is 40 bytes long */ gw_bin2hex(hex_double_sha1, password, SHA_DIGEST_LENGTH); } else { /* check if the password is not set in the user table */ return memcmp(password, null_client_sha1, MYSQL_SCRAMBLE_LEN) ? 1 : 0; } /*< * Auth check in 3 steps * * Note: token = XOR (SHA1(real_password), SHA1(CONCAT(scramble, SHA1(SHA1(real_password))))) * the client sends token * * Now, server side: * * * step 1: compute the STEP1 = SHA1(CONCAT(scramble, gateway_password)) * the result in step1 is SHA_DIGEST_LENGTH long */ gw_sha1_2_str(scramble, scramble_len, password, SHA_DIGEST_LENGTH, step1); /*< * step2: STEP2 = XOR(token, STEP1) * * token is transmitted form client and it's based on the handshake scramble and SHA1(real_passowrd) * step1 has been computed in the previous step * the result STEP2 is SHA1(the_password_to_check) and is SHA_DIGEST_LENGTH long */ gw_str_xor(step2, token, step1, token_len); /*< * copy the stage1_hash back to the caller * stage1_hash will be used for backend authentication */ memcpy(stage1_hash, step2, SHA_DIGEST_LENGTH); /*< * step 3: prepare the check_hash * * compute the SHA1(STEP2) that is SHA1(SHA1(the_password_to_check)), and is SHA_DIGEST_LENGTH long */ gw_sha1_str(step2, SHA_DIGEST_LENGTH, check_hash); #ifdef GW_DEBUG_CLIENT_AUTH { char inpass[128]=""; gw_bin2hex(inpass, check_hash, SHA_DIGEST_LENGTH); fprintf(stderr, "The CLIENT hex(SHA1(SHA1(password))) for \"%s\" is [%s]", username, inpass); } #endif /* now compare SHA1(SHA1(gateway_password)) and check_hash: return 0 is MYSQL_AUTH_OK */ ret_val = memcmp(password, check_hash, SHA_DIGEST_LENGTH); if (ret_val != 0) return 1; else return 0; } /** * gw_find_mysql_user_password_sha1 * * The routine fetches an user from the MaxScale users' table * The users' table is dcb->service->users or a different one specified with void *repository * The user lookup uses username,host and db name (if passed in connection or change user) * * If found the HEX password, representing sha1(sha1(password)), is converted in binary data and * copied into gateway_password * * @param username The user to look for * @param gateway_password The related SHA1(SHA1(password)), the pointer must be preallocated * @param dcb Current DCB * @return 1 if user is not found or 0 if the user exists * */ int gw_find_mysql_user_password_sha1(char *username, uint8_t *gateway_password, DCB *dcb) { SERVICE *service = NULL; struct sockaddr_in *client; char *user_password = NULL; MYSQL_USER_HOST key; MYSQL_session *client_data = NULL; client_data = (MYSQL_session *) dcb->data; service = (SERVICE *) dcb->service; client = (struct sockaddr_in *) &dcb->ipv4; key.user = username; memcpy(&key.ipv4, client, sizeof(struct sockaddr_in)); key.netmask = 32; key.resource = client_data->db; LOGIF(LD, (skygw_log_write_flush( LOGFILE_DEBUG, "%lu [MySQL Client Auth], checking user [%s@%s]%s%s", pthread_self(), key.user, dcb->remote, key.resource != NULL ?" db: " :"", key.resource != NULL ?key.resource :""))); /* look for user@current_ipv4 now */ user_password = mysql_users_fetch(service->users, &key); if (!user_password) { /* The user is not authenticated @ current IPv4 */ while (1) { /* * (1) Check for localhost first: 127.0.0.1 (IPv4 only) */ if ((key.ipv4.sin_addr.s_addr == 0x0100007F) && !dcb->service->localhost_match_wildcard_host) { /* Skip the wildcard check and return 1 */ LOGIF(LE, (skygw_log_write_flush( LOGFILE_ERROR, "Error : user %s@%s not found, try set " "'localhost_match_wildcard_host=1' in " "service definition of the configuration " "file.", key.user, dcb->remote))); break; } /* * (2) check for possible IPv4 class C,B,A networks */ /* Class C check */ key.ipv4.sin_addr.s_addr &= 0x00FFFFFF; key.netmask -= 8; user_password = mysql_users_fetch(service->users, &key); if (user_password) { break; } /* Class B check */ key.ipv4.sin_addr.s_addr &= 0x0000FFFF; key.netmask -= 8; user_password = mysql_users_fetch(service->users, &key); if (user_password) { break; } /* Class A check */ key.ipv4.sin_addr.s_addr &= 0x000000FF; key.netmask -= 8; user_password = mysql_users_fetch(service->users, &key); if (user_password) { break; } /* * (3) Continue check for wildcard host, user@% */ memset(&key.ipv4, 0, sizeof(struct sockaddr_in)); key.netmask = 0; LOGIF(LD, (skygw_log_write_flush( LOGFILE_DEBUG, "%lu [MySQL Client Auth], checking user [%s@%s] with wildcard host [%%]", pthread_self(), key.user, dcb->remote))); user_password = mysql_users_fetch(service->users, &key); if (user_password) { break; } if (!user_password) { /* * user@% not found. */ LOGIF(LD, (skygw_log_write_flush( LOGFILE_DEBUG, "%lu [MySQL Client Auth], user [%s@%s] not existent", pthread_self(), key.user, dcb->remote))); LOGIF(LT,skygw_log_write_flush( LOGFILE_ERROR, "Authentication Failed: user [%s@%s] not found.", key.user, dcb->remote)); break; } } } /* If user@host has been found we get the the password in binary format*/ if (user_password) { /* * Convert the hex data (40 bytes) to binary (20 bytes). * The gateway_password represents the SHA1(SHA1(real_password)). * Please note: the real_password is unknown and SHA1(real_password) is unknown as well */ int passwd_len=strlen(user_password); if (passwd_len) { passwd_len = (passwd_len <= (SHA_DIGEST_LENGTH * 2)) ? passwd_len : (SHA_DIGEST_LENGTH * 2); gw_hex2bin(gateway_password, user_password, passwd_len); } return 0; } else { return 1; } } /** * mysql_send_auth_error * * Send a MySQL protocol ERR message, for gateway authentication error to the dcb * * @param dcb descriptor Control Block for the connection to which the OK is sent * @param packet_number * @param in_affected_rows * @param mysql_message * @return packet length * */ int mysql_send_auth_error ( DCB *dcb, int packet_number, int in_affected_rows, const char *mysql_message) { uint8_t *outbuf = NULL; uint32_t mysql_payload_size = 0; uint8_t mysql_packet_header[4]; uint8_t *mysql_payload = NULL; uint8_t field_count = 0; uint8_t mysql_err[2]; uint8_t mysql_statemsg[6]; unsigned int mysql_errno = 0; const char *mysql_error_msg = NULL; const char *mysql_state = NULL; GWBUF *buf; if (dcb->state != DCB_STATE_POLLING) { LOGIF(LD, (skygw_log_write( LOGFILE_DEBUG, "%lu [mysql_send_auth_error] dcb %p is in a state %s, " "and it is not in epoll set anymore. Skip error sending.", pthread_self(), dcb, STRDCBSTATE(dcb->state)))); return 0; } mysql_errno = 1045; mysql_error_msg = "Access denied!"; mysql_state = "28000"; field_count = 0xff; gw_mysql_set_byte2(mysql_err, mysql_errno); mysql_statemsg[0]='#'; memcpy(mysql_statemsg+1, mysql_state, 5); if (mysql_message != NULL) { mysql_error_msg = mysql_message; } mysql_payload_size = sizeof(field_count) + sizeof(mysql_err) + sizeof(mysql_statemsg) + strlen(mysql_error_msg); // allocate memory for packet header + payload if ((buf = gwbuf_alloc(sizeof(mysql_packet_header) + mysql_payload_size)) == NULL) { return 0; } outbuf = GWBUF_DATA(buf); // write packet header with packet number gw_mysql_set_byte3(mysql_packet_header, mysql_payload_size); mysql_packet_header[3] = packet_number; // write header memcpy(outbuf, mysql_packet_header, sizeof(mysql_packet_header)); mysql_payload = outbuf + sizeof(mysql_packet_header); // write field memcpy(mysql_payload, &field_count, sizeof(field_count)); mysql_payload = mysql_payload + sizeof(field_count); // write errno memcpy(mysql_payload, mysql_err, sizeof(mysql_err)); mysql_payload = mysql_payload + sizeof(mysql_err); // write sqlstate memcpy(mysql_payload, mysql_statemsg, sizeof(mysql_statemsg)); mysql_payload = mysql_payload + sizeof(mysql_statemsg); // write err messg memcpy(mysql_payload, mysql_error_msg, strlen(mysql_error_msg)); // writing data in the Client buffer queue dcb->func.write(dcb, buf); return sizeof(mysql_packet_header) + mysql_payload_size; } /** * Buffer contains at least one of the following: * complete [complete] [partial] mysql packet * * @param p_readbuf Address of read buffer pointer * * @return pointer to gwbuf containing a complete packet or * NULL if no complete packet was found. */ GWBUF* gw_MySQL_get_next_packet( GWBUF** p_readbuf) { GWBUF* packetbuf; GWBUF* readbuf; size_t buflen; size_t packetlen; size_t totalbuflen; uint8_t* data; size_t nbytes_copied = 0; uint8_t* target; readbuf = *p_readbuf; if (readbuf == NULL) { packetbuf = NULL; goto return_packetbuf; } CHK_GWBUF(readbuf); if (GWBUF_EMPTY(readbuf)) { packetbuf = NULL; goto return_packetbuf; } totalbuflen = gwbuf_length(readbuf); data = (uint8_t *)GWBUF_DATA((readbuf)); packetlen = MYSQL_GET_PACKET_LEN(data)+4; /** packet is incomplete */ if (packetlen > totalbuflen) { packetbuf = NULL; goto return_packetbuf; } packetbuf = gwbuf_alloc(packetlen); target = GWBUF_DATA(packetbuf); packetbuf->gwbuf_type = readbuf->gwbuf_type; /*< Copy the type too */ /** * Copy first MySQL packet to packetbuf and leave posible other * packets to read buffer. */ while (nbytes_copied < packetlen && totalbuflen > 0) { uint8_t* src = GWBUF_DATA((*p_readbuf)); size_t bytestocopy; buflen = GWBUF_LENGTH((*p_readbuf)); bytestocopy = MIN(buflen,packetlen-nbytes_copied); memcpy(target+nbytes_copied, src, bytestocopy); *p_readbuf = gwbuf_consume((*p_readbuf), bytestocopy); totalbuflen = gwbuf_length((*p_readbuf)); nbytes_copied += bytestocopy; } ss_dassert(buflen == 0 || nbytes_copied == packetlen); return_packetbuf: return packetbuf; } /** * Move from buffer pointed to by <*p_readbuf>. */ GWBUF* gw_MySQL_get_packets( GWBUF** p_srcbuf, int* npackets) { GWBUF* packetbuf; GWBUF* targetbuf = NULL; while (*npackets > 0 && (packetbuf = gw_MySQL_get_next_packet(p_srcbuf)) != NULL) { targetbuf = gwbuf_append(targetbuf, packetbuf); *npackets -= 1; } ss_dassert(*npackets < 128); ss_dassert(*npackets >= 0); return targetbuf; } static server_command_t* server_command_init( server_command_t* srvcmd, mysql_server_cmd_t cmd) { server_command_t* c; if (srvcmd != NULL) { c = srvcmd; } else { c = (server_command_t *)malloc(sizeof(server_command_t)); } c->scom_cmd = cmd; c->scom_nresponse_packets = -1; c->scom_nbytes_to_read = 0; c->scom_next = NULL; return c; } static server_command_t* server_command_copy( server_command_t* srvcmd) { server_command_t* c = (server_command_t *)malloc(sizeof(server_command_t)); *c = *srvcmd; return c; } #define MAX_CMD_HISTORY 10 void protocol_archive_srv_command( MySQLProtocol* p) { server_command_t* s1; server_command_t* h1; int len = 0; CHK_PROTOCOL(p); spinlock_acquire(&p->protocol_lock); if (p->protocol_state != MYSQL_PROTOCOL_ACTIVE) { goto retblock; } s1 = &p->protocol_command; #if defined(EXTRA_SS_DEBUG) LOGIF(LT, (skygw_log_write( LOGFILE_TRACE, "Move command %s from fd %d to command history.", STRPACKETTYPE(s1->scom_cmd), p->owner_dcb->fd))); #endif /** Copy to history list */ if ((h1 = p->protocol_cmd_history) == NULL) { p->protocol_cmd_history = server_command_copy(s1); } else /*< scan and count history commands */ { len = 1; while (h1->scom_next != NULL) { h1 = h1->scom_next; len += 1; } h1->scom_next = server_command_copy(s1); } /** Keep history limits, remove oldest */ if (len > MAX_CMD_HISTORY) { server_command_t* c = p->protocol_cmd_history; p->protocol_cmd_history = p->protocol_cmd_history->scom_next; free(c); } /** Remove from command list */ if (s1->scom_next == NULL) { p->protocol_command.scom_cmd = MYSQL_COM_UNDEFINED; } else { p->protocol_command = *(s1->scom_next); free(s1->scom_next); } retblock: spinlock_release(&p->protocol_lock); CHK_PROTOCOL(p); } /** * If router expects to get separate, complete statements, add MySQL command * to MySQLProtocol structure. It is removed when response has arrived. */ void protocol_add_srv_command( MySQLProtocol* p, mysql_server_cmd_t cmd) { #if defined(EXTRA_SS_DEBUG) server_command_t* c; #endif spinlock_acquire(&p->protocol_lock); if (p->protocol_state != MYSQL_PROTOCOL_ACTIVE) { goto retblock; } /** this is the only server command in protocol */ if (p->protocol_command.scom_cmd == MYSQL_COM_UNDEFINED) { /** write into structure */ server_command_init(&p->protocol_command, cmd); } else { /** add to the end of list */ p->protocol_command.scom_next = server_command_init(NULL, cmd); } #if defined(EXTRA_SS_DEBUG) LOGIF(LT, (skygw_log_write( LOGFILE_TRACE, "Added command %s to fd %d.", STRPACKETTYPE(cmd), p->owner_dcb->fd))); c = &p->protocol_command; while (c != NULL && c->scom_cmd != MYSQL_COM_UNDEFINED) { LOGIF(LT, (skygw_log_write( LOGFILE_TRACE, "fd %d : %d %s", p->owner_dcb->fd, c->scom_cmd, STRPACKETTYPE(c->scom_cmd)))); c = c->scom_next; } #endif retblock: spinlock_release(&p->protocol_lock); } /** * If router processes separate statements, every stmt has corresponding MySQL * command stored in MySQLProtocol structure. * * Remove current (=oldest) command. */ void protocol_remove_srv_command( MySQLProtocol* p) { server_command_t* s; spinlock_acquire(&p->protocol_lock); s = &p->protocol_command; #if defined(EXTRA_SS_DEBUG) LOGIF(LT, (skygw_log_write( LOGFILE_TRACE, "Removed command %s from fd %d.", STRPACKETTYPE(s->scom_cmd), p->owner_dcb->fd))); #endif if (s->scom_next == NULL) { p->protocol_command.scom_cmd = MYSQL_COM_UNDEFINED; } else { p->protocol_command = *(s->scom_next); free(s->scom_next); } spinlock_release(&p->protocol_lock); } mysql_server_cmd_t protocol_get_srv_command( MySQLProtocol* p, bool removep) { mysql_server_cmd_t cmd; cmd = p->protocol_command.scom_cmd; if (removep) { protocol_remove_srv_command(p); } LOGIF(LD, (skygw_log_write( LOGFILE_DEBUG, "%lu [protocol_get_srv_command] Read command %s for fd %d.", pthread_self(), STRPACKETTYPE(cmd), p->owner_dcb->fd))); return cmd; } /** * 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. */ void init_response_status ( GWBUF* buf, mysql_server_cmd_t cmd, int* npackets, ssize_t* nbytes_left) { uint8_t* packet; int nparam; int nattr; uint8_t* data; ss_dassert(gwbuf_length(buf) >= 3); data = (uint8_t *)buf->start; if (data[4] == 0xff) /*< error */ { *npackets = 1; } else { switch (cmd) { case MYSQL_COM_STMT_PREPARE: packet = (uint8_t *)GWBUF_DATA(buf); /** ok + nparam + eof + nattr + eof */ nparam = MYSQL_GET_STMTOK_NPARAM(packet); nattr = MYSQL_GET_STMTOK_NATTR(packet); *npackets = 1 + nparam + MIN(1, nparam) + nattr + MIN(nattr, 1); break; case MYSQL_COM_QUIT: case MYSQL_COM_STMT_SEND_LONG_DATA: case MYSQL_COM_STMT_CLOSE: *npackets = 0; /*< these don't reply anything */ break; default: /** * assume that other session commands respond * OK or ERR */ *npackets = 1; break; } } *nbytes_left = MYSQL_GET_PACKET_LEN(data) + MYSQL_HEADER_LEN; /** * There is at least one complete packet in the buffer so buffer is bigger * than packet */ ss_dassert(*nbytes_left > 0); ss_dassert(*npackets > 0); } /** * Read how many packets are left from current response and how many bytes there * is still to be read from the current packet. */ bool protocol_get_response_status ( MySQLProtocol* p, int* npackets, ssize_t* nbytes) { bool succp; CHK_PROTOCOL(p); spinlock_acquire(&p->protocol_lock); *npackets = p->protocol_command.scom_nresponse_packets; *nbytes = (ssize_t)p->protocol_command.scom_nbytes_to_read; spinlock_release(&p->protocol_lock); if (*npackets < 0 && *nbytes == 0) { succp = false; } else { succp = true; } return succp; } void protocol_set_response_status ( MySQLProtocol* p, int npackets_left, ssize_t nbytes) { CHK_PROTOCOL(p); spinlock_acquire(&p->protocol_lock); p->protocol_command.scom_nbytes_to_read = nbytes; ss_dassert(p->protocol_command.scom_nbytes_to_read >= 0); p->protocol_command.scom_nresponse_packets = npackets_left; spinlock_release(&p->protocol_lock); } char* create_auth_failed_msg( GWBUF* readbuf, char* hostaddr, uint8_t* sha1) { char* errstr; char* uname=(char *)GWBUF_DATA(readbuf) + 5; const char* ferrstr = "Access denied for user '%s'@'%s' (using password: %s)"; /** -4 comes from 2X'%s' minus terminating char */ errstr = (char *)malloc(strlen(uname)+strlen(ferrstr)+strlen(hostaddr)+strlen("YES")-6 + 1); if (errstr != NULL) { sprintf(errstr, ferrstr, uname, hostaddr, (*sha1 == '\0' ? "NO" : "YES")); } return errstr; } /** * Read username from MySQL authentication packet. * * Only for client to server packet, COM_CHANGE_USER packet has different format. * * @param ptr address where to write the result or NULL if memory * is allocated here. * @param data Address of MySQL packet. * * @return Pointer to a copy of the username. NULL if memory allocation * failed or if username was empty. */ char* get_username_from_auth( char* ptr, uint8_t* data) { char* first_letter; char* rval; first_letter = (char *)(data + 4 + 4 + 4 + 1 + 23); if (*first_letter == '\0') { rval = NULL; goto retblock; } if (ptr == NULL) { if ((rval = (char *)malloc(MYSQL_USER_MAXLEN+1)) == NULL) { goto retblock; } } else { rval = ptr; } snprintf(rval, MYSQL_USER_MAXLEN+1, "%s", first_letter); retblock: return rval; } int check_db_name_after_auth(DCB *dcb, char *database, int auth_ret) { int db_exists = -1; /* check for dabase name and possible match in resource hashtable */ if (database && strlen(database)) { /* if database names are loaded we can check if db name exists */ if (dcb->service->resources != NULL) { if (hashtable_fetch(dcb->service->resources, database)) { db_exists = 1; } else { db_exists = 0; } } else { /* if database names are not loaded we don't allow connection with db name*/ db_exists = -1; } if (db_exists == 0 && auth_ret == 0) { auth_ret = 2; } if (db_exists < 0 && auth_ret == 0) { auth_ret = 1; } } return auth_ret; } /** * Create a message error string to send via MySQL ERR packet. * * @param username the MySQL user * @param hostaddr the client IP * @param sha1 authentication scramble data * @param db the MySQL db to connect to * * @return Pointer to the allocated string or NULL on failure */ char *create_auth_fail_str( char *username, char *hostaddr, char *sha1, char *db, int errcode) { char* errstr; const char* ferrstr; int db_len; if (db != NULL) db_len = strlen(db); else db_len = 0; if (db_len > 0) { ferrstr = "Access denied for user '%s'@'%s' (using password: %s) to database '%s'"; } else if(errcode == MYSQL_FAILED_AUTH_SSL) { ferrstr = "Access without SSL denied"; } else { ferrstr = "Access denied for user '%s'@'%s' (using password: %s)"; } errstr = (char *)malloc(strlen(username)+strlen(ferrstr)+strlen(hostaddr)+strlen("YES")-6 + db_len + ((db_len > 0) ? (strlen(" to database ") +2) : 0) + 1); if (errstr == NULL) { LOGIF(LE, (skygw_log_write_flush( LOGFILE_ERROR, "Error : Memory allocation failed due to %s.", strerror(errno)))); goto retblock; } if (db_len > 0) { sprintf(errstr, ferrstr, username, hostaddr, (*sha1 == '\0' ? "NO" : "YES"), db); } else if(errcode == MYSQL_FAILED_AUTH_SSL) { sprintf(errstr, ferrstr); } else { sprintf(errstr, ferrstr, username, hostaddr, (*sha1 == '\0' ? "NO" : "YES")); } retblock: return errstr; }