diff --git a/server/core/modutil.c b/server/core/modutil.c index 2f1646a8c..9c06d1c05 100644 --- a/server/core/modutil.c +++ b/server/core/modutil.c @@ -801,6 +801,115 @@ void* strnchr_esc(char* ptr, char c, int len) return NULL; } +/** + * Find the first occurrence of a character in a string. This function ignores + * escaped characters and all characters that are enclosed in single or double quotes. + * Also MySQL style comment blocks are ignored. + * @param ptr Pointer to area of memory to inspect + * @param c Character to search for + * @param len Size of the memory area + * @return Pointer to the first non-escaped, non-quoted occurrence of the character. + * If the character is not found, NULL is returned. + */ +void* strnchr_esc_mysql(char* ptr, char c, int len) +{ + char* p = (char*) ptr; + char* start = p; + bool quoted = false, escaped = false, backtick = false; + char qc; + + while (p < start + len) + { + if (escaped) + { + escaped = false; + } + else + { + switch (*p) + { + case '\\': + escaped = true; + break; + + case'\'': + case '"': + if (!quoted) + { + quoted = true; + qc = *p; + } + else if (*p == qc) + { + quoted = false; + } + break; + + case '`': + backtick = !backtick; + break; + + case '#': + if (!backtick) + { + return NULL; + } + break; + + case '-': + if (!backtick && p + 1 < start + len && *(p + 1) == '-') + { + return NULL; + } + break; + } + + if (*p == c && !escaped && !quoted) + { + return p; + } + p++; + } + } + return NULL; +} + +/** + * Check if the start of the token is the start of a MySQL style comment block + * @param ptr String with at least two non-null characters in it + * @return True if the token starts a comment block + */ +bool is_mysql_comment_start(const char* start, int len) +{ + const char *ptr = start; + + while (ptr - start < len && isspace(*ptr)) + { + ptr++; + } + + switch (*ptr) + { + case '-': + if (*(ptr + 1) == '-') + { + return true; + } + break; + + case '#': + return true; + + case '/': + if (*(ptr + 1) == '*') + { + return true; + } + break; + } + return false; +} + /** * Create a COM_QUERY packet from a string. * @param query Query to create. diff --git a/server/include/modutil.h b/server/include/modutil.h index 7492839a4..288adcc86 100644 --- a/server/include/modutil.h +++ b/server/include/modutil.h @@ -67,6 +67,10 @@ GWBUF* modutil_create_mysql_err_msg(int packet_number, const char *msg); int modutil_count_signal_packets(GWBUF*,int,int,int*); mxs_pcre2_result_t modutil_mysql_wildcard_match(const char* pattern, const char* string); + +/** Character and token searching functions */ void* strnchr_esc(char* ptr, char c, int len); +void* strnchr_esc_mysql(char* ptr, char c, int len); +bool is_mysql_comment_start(const char* start, int len); #endif diff --git a/server/modules/routing/readwritesplit/readwritesplit.c b/server/modules/routing/readwritesplit/readwritesplit.c index 2990e69d9..225746b32 100644 --- a/server/modules/routing/readwritesplit/readwritesplit.c +++ b/server/modules/routing/readwritesplit/readwritesplit.c @@ -308,6 +308,8 @@ static ROUTER_INSTANCE* instances; static int hashkeyfun(void* key); static int hashcmpfun (void *, void *); +static void check_for_multi_stmt(ROUTER_CLIENT_SES* rses, GWBUF *buf, + mysql_server_cmd_t packet_type); static int hashkeyfun( void* key) @@ -2156,23 +2158,7 @@ static bool route_single_stmt( /** Check for multi-statement queries. We assume here that the client * protocol is MySQLClient. * TODO: add warnings when incompatible protocols are used */ - MySQLProtocol *proto = (MySQLProtocol*)rses->client_dcb->protocol; - if (proto->client_capabilities & GW_MYSQL_CAPABILITIES_MULTI_STATEMENTS && - packet_type == MYSQL_COM_QUERY && rses->forced_node != rses->rses_master_ref) - { - for (GWBUF *buf = querybuf; buf != NULL; buf = buf->next) - { - if (strnchr_esc(GWBUF_DATA(buf), ';', GWBUF_LENGTH(buf))) - { - /** It is possible that the session state is modified inside - * the multi-statement query which would leave any slave sessions - * in an inconsistent state. Due to this, for the duration of - * this session, all queries will be sent to the master. */ - rses->forced_node = rses->rses_master_ref; - MXS_INFO("Multi-statement query, routing all future queries to master."); - } - } - } + check_for_multi_stmt(rses, querybuf, packet_type); /** * Check if the query has anything to do with temporary tables. @@ -5357,12 +5343,35 @@ static backend_ref_t* get_root_master_bref( return candidate_bref; } +/** + * @brief Detect multi-statement queries + * + * It is possible that the session state is modified inside a multi-statement + * query which would leave any slave sessions in an inconsistent state. Due to + * this, for the duration of this session, all queries will be sent to the master + * if the current query contains a multi-statement query. + * @param rses Router client session + * @param buf Buffer containing the full query + */ +static void check_for_multi_stmt(ROUTER_CLIENT_SES* rses, GWBUF *buf, + mysql_server_cmd_t packet_type) +{ + MySQLProtocol *proto = (MySQLProtocol*) rses->client_dcb->protocol; + if (proto->client_capabilities & GW_MYSQL_CAPABILITIES_MULTI_STATEMENTS && + packet_type == MYSQL_COM_QUERY && rses->forced_node != rses->rses_master_ref) + { + char *ptr, *data = GWBUF_DATA(buf) + 5; + int buflen = GWBUF_LENGTH(buf) - 5; - - - - - - - + if ((ptr = strnchr_esc_mysql(data, ';', buflen))) + { + ptr++; + if (ptr - data < buflen && !is_mysql_comment_start(ptr, ptr - data)) + { + rses->forced_node = rses->rses_master_ref; + MXS_INFO("Multi-statement query, routing all future queries to master."); + } + } + } +}