diff --git a/include/mysql_com.h b/include/mysql_com.h index 1b796ce402490..42b378677dcb7 100644 --- a/include/mysql_com.h +++ b/include/mysql_com.h @@ -488,6 +488,8 @@ typedef struct st_net { unsigned char compress; my_bool pkt_nr_can_be_reset; my_bool using_proxy_protocol; + /* proxy protocol: real client address has connect errors to reset on login */ + my_bool have_proxy_protocol_connect_errors; /* Pointer to query object in query cache, do not equal NULL (0) for queries in cache that have not stored its results yet diff --git a/mysql-test/main/mysql_client_test.result b/mysql-test/main/mysql_client_test.result index 4c7b20314c05d..45b654560a680 100644 --- a/mysql-test/main/mysql_client_test.result +++ b/mysql-test/main/mysql_client_test.result @@ -8,6 +8,8 @@ SET @old_general_log= @@global.general_log; SET @old_slow_query_log= @@global.slow_query_log; call mtr.add_suppression(" Error reading file './client_test_db/test_frm_bug.frm'"); call mtr.add_suppression(" IP address .* could not be resolved"); +call mtr.add_suppression("Aborted connection .* host: '192.0.2.50'"); +call mtr.add_suppression("Aborted connection .* host: 'santa.claus.ipv4.example.com'"); ok # cat MYSQL_TMP_DIR/test_wl4435.out.log diff --git a/mysql-test/main/mysql_client_test.test b/mysql-test/main/mysql_client_test.test index f99aecace80d3..64a5411995bb3 100644 --- a/mysql-test/main/mysql_client_test.test +++ b/mysql-test/main/mysql_client_test.test @@ -15,6 +15,8 @@ SET @old_slow_query_log= @@global.slow_query_log; call mtr.add_suppression(" Error reading file './client_test_db/test_frm_bug.frm'"); call mtr.add_suppression(" IP address .* could not be resolved"); +call mtr.add_suppression("Aborted connection .* host: '192.0.2.50'"); +call mtr.add_suppression("Aborted connection .* host: 'santa.claus.ipv4.example.com'"); # We run with different binaries for normal and --embedded-server # diff --git a/mysql-test/suite/json/r/json_table.result b/mysql-test/suite/json/r/json_table.result index 9f874d6264e8c..e406087751981 100644 --- a/mysql-test/suite/json/r/json_table.result +++ b/mysql-test/suite/json/r/json_table.result @@ -269,7 +269,7 @@ from t1, json_table(t1.json, '$' columns (value varchar(32) PATH '$.value')) T; show create table tj1; Table Create Table tj1 CREATE TABLE `tj1` ( - `value` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL + `value` varchar(32) DEFAULT NULL ) ENGINE=MyISAM DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci drop table t1; drop table tj1; @@ -1100,7 +1100,7 @@ name VARCHAR(10) PATH '$.name' ) ) AS jt; collation(name) -utf8mb4_general_ci +latin1_swedish_ci SELECT collation(name) FROM json_table('[{"name":"Jeans"}]', '$[*]' COLUMNS( @@ -1108,7 +1108,7 @@ name VARCHAR(10) COLLATE DEFAULT PATH '$.name' ) ) AS jt; collation(name) -utf8mb4_general_ci +latin1_swedish_ci SELECT collation(name) FROM json_table('[{"name":"Jeans"}]', '$[*]' COLUMNS( @@ -1116,7 +1116,7 @@ name VARCHAR(10) BINARY PATH '$.name' ) ) AS jt; collation(name) -utf8mb4_bin +latin1_bin CREATE VIEW v1 AS SELECT * FROM json_table('[{"name":"Jeans"}]', '$[*]' @@ -1129,7 +1129,7 @@ View Create View character_set_client collation_connection v1 CREATE ALGORITHM=UNDEFINED DEFINER=`root`@`localhost` SQL SECURITY DEFINER VIEW `v1` AS select `jt`.`name` AS `name` from JSON_TABLE('[{"name":"Jeans"}]', '$[*]' COLUMNS (`name` varchar(10) PATH '$.name')) `jt` latin1 latin1_swedish_ci SELECT collation(name) FROM v1; collation(name) -utf8mb4_general_ci +latin1_swedish_ci DROP VIEW v1; CREATE VIEW v1 AS SELECT * @@ -1143,7 +1143,7 @@ View Create View character_set_client collation_connection v1 CREATE ALGORITHM=UNDEFINED DEFINER=`root`@`localhost` SQL SECURITY DEFINER VIEW `v1` AS select `jt`.`name` AS `name` from JSON_TABLE('[{"name":"Jeans"}]', '$[*]' COLUMNS (`name` varchar(10) PATH '$.name')) `jt` latin1 latin1_swedish_ci SELECT collation(name) FROM v1; collation(name) -utf8mb4_general_ci +latin1_swedish_ci DROP VIEW v1; CREATE VIEW v1 AS SELECT * @@ -1280,4 +1280,22 @@ Second Element { "value": 2 } DROP VIEW test_view; +# +# MDEV-36764: Unexpected collation when using json_table +# +CREATE DATABASE collation_testing CHARACTER SET='utf8mb4' COLLATE='utf8mb4_uca1400_ai_ci'; +USE collation_testing; +CREATE TABLE table_with_longtext_column (column_with_json LONGTEXT); +INSERT INTO table_with_longtext_column VALUES ('[{"property": "value"}]'); +SELECT COLLATION(column_with_json) FROM table_with_longtext_column; +COLLATION(column_with_json) +utf8mb4_uca1400_ai_ci +SELECT COLLATION(JSON_VALUE(column_with_json, "$.value")) FROM table_with_longtext_column; +COLLATION(JSON_VALUE(column_with_json, "$.value")) +utf8mb4_uca1400_ai_ci +SELECT COLLATION(virtual_table_from_json.property) FROM JSON_TABLE((SELECT column_with_json FROM table_with_longtext_column),'$[*]' COLUMNS (property varchar(100) path '$.property')) AS virtual_table_from_json; +COLLATION(virtual_table_from_json.property) +utf8mb4_uca1400_ai_ci +DROP TABLE table_with_longtext_column; +DROP DATABASE collation_testing; # End of 10.11 test diff --git a/mysql-test/suite/json/r/json_table_mysql.result b/mysql-test/suite/json/r/json_table_mysql.result index 8380ba0772065..566180f22fd31 100644 --- a/mysql-test/suite/json/r/json_table_mysql.result +++ b/mysql-test/suite/json/r/json_table_mysql.result @@ -1123,7 +1123,7 @@ SELECT col_varchar_key FROM JSON_TABLE( (col_varchar_key VARCHAR(10) PATH "$.col_key")) AS innr1); id select_type table type possible_keys key key_len ref rows Extra 1 PRIMARY t1 ALL NULL NULL NULL NULL 2 Using where -2 DEPENDENT SUBQUERY innr1 ALL NULL NULL NULL NULL 40 Table function: json_table; Using where +2 MATERIALIZED innr1 ALL NULL NULL NULL NULL 40 Table function: json_table SELECT * FROM t1 WHERE col_varchar_key IN ( SELECT col_varchar_key FROM JSON_TABLE( '[{"col_key": 1},{"col_key": 2}]', "$[*]" COLUMNS @@ -1136,7 +1136,8 @@ SELECT col_varchar_key FROM JSON_TABLE( (col_varchar_key VARCHAR(10) PATH "$.col_key")) AS innr1); id select_type table type possible_keys key key_len ref rows Extra 1 PRIMARY t1 ALL NULL NULL NULL NULL 2 -1 PRIMARY innr1 ALL NULL NULL NULL NULL 40 Table function: json_table; Using where; FirstMatch(t1); Using join buffer (flat, BNL join) +1 PRIMARY eq_ref distinct_key distinct_key 13 func 1 Using where +2 MATERIALIZED innr1 ALL NULL NULL NULL NULL 40 Table function: json_table DROP TABLE t1; # # Bug#26711551: WL8867:CONDITIONAL JUMP IN JSON_TABLE_COLUMN::CLEANUP @@ -1467,7 +1468,7 @@ FROM JSON_TABLE('"test"', '$' COLUMNS(col VARCHAR(10) PATH '$')) AS a; SHOW CREATE TABLE t1; Table Create Table t1 CREATE TABLE `t1` ( - `col` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL + `col` varchar(10) DEFAULT NULL ) ENGINE=MyISAM DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci SET @@SESSION.collation_connection = latin1_bin; CREATE TABLE t2 SELECT a.col @@ -1475,7 +1476,7 @@ FROM JSON_TABLE('"test"', '$' COLUMNS(col VARCHAR(10) PATH '$')) AS a; SHOW CREATE TABLE t2; Table Create Table t2 CREATE TABLE `t2` ( - `col` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL + `col` varchar(10) DEFAULT NULL ) ENGINE=MyISAM DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci DROP TABLE t1, t2; SET @@SESSION.character_set_connection = DEFAULT; diff --git a/mysql-test/suite/json/t/json_table.test b/mysql-test/suite/json/t/json_table.test index aac9005cbd6b3..bab6d4b777f4d 100644 --- a/mysql-test/suite/json/t/json_table.test +++ b/mysql-test/suite/json/t/json_table.test @@ -1103,4 +1103,22 @@ SELECT * FROM JSON_TABLE(' DROP VIEW test_view; + +--echo # +--echo # MDEV-36764: Unexpected collation when using json_table +--echo # + +CREATE DATABASE collation_testing CHARACTER SET='utf8mb4' COLLATE='utf8mb4_uca1400_ai_ci'; +USE collation_testing; + +CREATE TABLE table_with_longtext_column (column_with_json LONGTEXT); +INSERT INTO table_with_longtext_column VALUES ('[{"property": "value"}]'); + +SELECT COLLATION(column_with_json) FROM table_with_longtext_column; +SELECT COLLATION(JSON_VALUE(column_with_json, "$.value")) FROM table_with_longtext_column; +SELECT COLLATION(virtual_table_from_json.property) FROM JSON_TABLE((SELECT column_with_json FROM table_with_longtext_column),'$[*]' COLUMNS (property varchar(100) path '$.property')) AS virtual_table_from_json; + +DROP TABLE table_with_longtext_column; +DROP DATABASE collation_testing; + --echo # End of 10.11 test diff --git a/sql/json_table.cc b/sql/json_table.cc index 35a02eb2bbe8d..e5ac60085bf19 100644 --- a/sql/json_table.cc +++ b/sql/json_table.cc @@ -707,6 +707,7 @@ TABLE *Create_json_table::start(THD *thd, TABLE_SHARE *share; DBUG_ENTER("Create_json_table::start"); + param->table_charset= thd->variables.collation_database; param->tmp_name= "json"; if (!(table= Create_tmp_table::start(thd, param, table_alias))) DBUG_RETURN(0); @@ -758,7 +759,7 @@ bool Create_json_table::add_json_table_fields(THD *thd, TABLE *table, uint fieldnr= 0; MEM_ROOT *mem_root_save= thd->mem_root; List_iterator_fast jc_i(jt->m_columns); - Column_derived_attributes da(&my_charset_utf8mb4_general_ci); + Column_derived_attributes da(share->table_charset); DBUG_ENTER("add_json_table_fields"); thd->mem_root= &table->mem_root; diff --git a/sql/net_serv.cc b/sql/net_serv.cc index 35b2a24a2d2c9..38a4c0269ce61 100644 --- a/sql/net_serv.cc +++ b/sql/net_serv.cc @@ -164,6 +164,7 @@ my_bool my_net_init(NET *net, Vio *vio, void *thd, uint my_flags) net->last_errno=0; net->pkt_nr_can_be_reset= 0; net->using_proxy_protocol= 0; + net->have_proxy_protocol_connect_errors= 0; net->thread_specific_malloc= MY_TEST(my_flags & MY_THREAD_SPECIFIC); net->thd= 0; #ifdef MYSQL_SERVER @@ -219,6 +220,7 @@ void net_end(NET *net) my_free(net->buff); net->buff=0; net->using_proxy_protocol= 0; + net->have_proxy_protocol_connect_errors= 0; DBUG_VOID_RETURN; } @@ -958,11 +960,15 @@ static handle_proxy_header_result handle_proxy_header(NET *net) /* proxy header indicates LOCAL connection, no action necessary */ return RETRY; /* Change peer address in THD and ACL structures.*/ - uint host_errors; + uint host_errors= 0; net->using_proxy_protocol= 1; - return (handle_proxy_header_result)thd_set_peer_addr(thd, - &(peer_info.peer_addr), NULL, peer_info.port, - false, &host_errors); + handle_proxy_header_result res= + (handle_proxy_header_result) thd_set_peer_addr(thd, &(peer_info.peer_addr), + NULL, peer_info.port, false, &host_errors); + /* Record the real client's connect errors for the login-success reset. */ + if (host_errors) + net->have_proxy_protocol_connect_errors= 1; + return res; #endif } diff --git a/sql/sql_connect.cc b/sql/sql_connect.cc index f7712b9abc1dc..ed866ab0b3921 100644 --- a/sql/sql_connect.cc +++ b/sql/sql_connect.cc @@ -1024,6 +1024,9 @@ static int check_connection(THD *thd) uint connect_errors= 0; int auth_rc; NET *net= &thd->net; + /* Socket peer IP address, the proxy host under PROXY protocol */ + char ip[NI_MAXHOST]; + ip[0]= 0; DBUG_PRINT("info", ("New connection received on %s", vio_description(net->vio))); @@ -1035,7 +1038,6 @@ static int check_connection(THD *thd) if (!thd->main_security_ctx.host) // If TCP/IP connection { my_bool peer_rc; - char ip[NI_MAXHOST]; uint16 peer_port; peer_rc= vio_peer_addr(net->vio, ip, &peer_port, NI_MAXHOST); @@ -1153,14 +1155,16 @@ static int check_connection(THD *thd) } auth_rc= acl_authenticate(thd, 0); - if (auth_rc == 0 && connect_errors != 0) + if (auth_rc == 0) { /* - A client connection from this IP was successful, - after some previous failures. - Reset the connection error counter. + Successful login resets connect errors, for both the socket peer and, + under PROXY protocol, the real client - each by its own error count. */ - reset_host_connect_errors(thd->main_security_ctx.ip); + if (connect_errors) + reset_host_connect_errors(ip); + if (net->have_proxy_protocol_connect_errors) + reset_host_connect_errors(thd->main_security_ctx.ip); } return auth_rc; diff --git a/tests/mysql_client_test.c b/tests/mysql_client_test.c index dba0b4c63e703..bea479e86f1cc 100644 --- a/tests/mysql_client_test.c +++ b/tests/mysql_client_test.c @@ -38,6 +38,9 @@ #include "mysql_client_fw.c" #ifndef _WIN32 #include +#include +#include +#include #endif #include "my_valgrind.h" @@ -20855,6 +20858,226 @@ static void test_proxy_header_dbug_remote_connection() } +/* + Open a raw TCP connection and let the handshake fail (server reads EOF) to + add one max_connect_errors error. With a header it counts against 'client_ip'; + with NULL no header is sent, so against the socket peer (the proxy host). +*/ +static void proxy_send_handshake_error(const char *client_ip) +{ + struct addrinfo hints, *ai= NULL, *p; + char portbuf[20], header[128]; + int connected= 0; + my_socket s= INVALID_SOCKET; + + snprintf(portbuf, sizeof(portbuf), "%u", opt_port); + memset(&hints, 0, sizeof(hints)); + hints.ai_family= AF_UNSPEC; + hints.ai_socktype= SOCK_STREAM; + if (getaddrinfo(opt_host ? opt_host : "localhost", portbuf, &hints, &ai)) + return; + + for (p= ai; p; p= p->ai_next) + { + s= socket(p->ai_family, p->ai_socktype, p->ai_protocol); + if (s == INVALID_SOCKET) + continue; + if (connect(s, p->ai_addr, (int) p->ai_addrlen) == 0) + { + connected= 1; + break; + } + closesocket(s); + s= INVALID_SOCKET; + } + freeaddrinfo(ai); + if (!connected) + return; + + if (client_ip) + { + int hdr_len= snprintf(header, sizeof(header), + "PROXY TCP4 %s 127.0.0.1 12345 %u\r\n", + client_ip, opt_port); + send(s, header, hdr_len, 0); + } + /* Half-close our write side so the server's handshake read hits EOF. */ + shutdown(s, IF_WIN(SD_SEND,SHUT_WR)); + /* Drain until the server closes, ensuring the error has been accounted. */ + { + char buf[256]; + while (recv(s, buf, sizeof(buf), 0) > 0) + ; + } + closesocket(s); +} + +/* + Connect through PROXY protocol, advertising 'client_ip' as the client. + On success returns the connection (caller closes it); on failure returns NULL + and stores the client error code in *out_errno. +*/ +static MYSQL *proxy_connect_as(const char *client_ip, const char *user, + const char *passwd, unsigned int *out_errno) +{ + MYSQL *m= mysql_client_init(NULL); + char header[128]; + int proto= MYSQL_PROTOCOL_TCP; + DIE_UNLESS(m != NULL); + snprintf(header, sizeof(header), "PROXY TCP4 %s 127.0.0.1 12345 %u\r\n", + client_ip, opt_port); + mysql_optionsv(m, MARIADB_OPT_PROXY_HEADER, header, strlen(header)); + mysql_optionsv(m, MYSQL_OPT_PROTOCOL, &proto); + if (!mysql_real_connect(m, opt_host, user, passwd, NULL, opt_port, NULL, 0)) + { + if (out_errno) + *out_errno= mysql_errno(m); + mysql_close(m); + return NULL; + } + if (out_errno) + *out_errno= 0; + return m; +} + +/* + MDEV-25817: with PROXY protocol a successful login must reset the proxied + client's connect-error counter; the reset used to be gated on the proxy + host's count, so it never fired. +*/ +static void test_proxy_header_connect_errors_reset() +{ + const char *client_ip= "192.0.2.50"; + char query[256]; + unsigned int conn_errno= 0; + int rc, i; + MYSQL *m; + + myheader("test_proxy_header_connect_errors_reset"); + + rc= mysql_query(mysql, + "SET @saved_max_connect_errors= @@global.max_connect_errors"); + myquery(rc); + rc= mysql_query(mysql, "SET @@global.max_connect_errors=3"); + myquery(rc); + rc= mysql_query(mysql, "FLUSH HOSTS"); + myquery(rc); + + snprintf(query, sizeof(query), + "CREATE USER 'u'@'%s' IDENTIFIED BY 'password'", client_ip); + rc= mysql_query(mysql, query); + myquery(rc); + + /* max_connect_errors handshake errors block the host. */ + for (i= 0; i < 3; i++) + proxy_send_handshake_error(client_ip); + m= proxy_connect_as(client_ip, "u", "password", &conn_errno); + DIE_UNLESS(m == NULL && conn_errno == ER_HOST_IS_BLOCKED); + + rc= mysql_query(mysql, "FLUSH HOSTS"); + myquery(rc); + + /* Two handshake errors, still below max_connect_errors (3). */ + proxy_send_handshake_error(client_ip); + proxy_send_handshake_error(client_ip); + + /* Below the limit: must connect, and this success must reset the counter. */ + m= proxy_connect_as(client_ip, "u", "password", &conn_errno); + DIE_UNLESS(m != NULL); + mysql_close(m); + + /* Two more: with the reset count is 2 (<3, connectable); without it 4 + (>=3, blocked). */ + proxy_send_handshake_error(client_ip); + proxy_send_handshake_error(client_ip); + + m= proxy_connect_as(client_ip, "u", "password", &conn_errno); + DIE_UNLESS(m != NULL); /* ER_HOST_IS_BLOCKED with unfixed MDEV-25817 */ + mysql_close(m); + + snprintf(query, sizeof(query), "DROP USER 'u'@'%s'", client_ip); + rc= mysql_query(mysql, query); + myquery(rc); + rc= mysql_query(mysql, + "SET global max_connect_errors=@saved_max_connect_errors"); + myquery(rc); + rc= mysql_query(mysql, "FLUSH HOSTS"); + myquery(rc); +} + +/* + A successful proxied login must also reset the proxy host (socket peer), not + only the proxied client - else connections that never finish the handshake + (e.g. port checks) could block the proxy and lock out everyone behind it. + Debug injection fakes the socket peer to a non-loopback address (192.0.2.4) + and name resolution so it is accounted. +*/ +static void test_proxy_header_proxy_host_connect_errors_reset() +{ +#ifndef DBUG_OFF + const char *proxied_client= "192.0.2.5"; /* in getaddrinfo_fake_good_ipv4 */ + unsigned int conn_errno= 0; + int rc, i; + MYSQL *m; + + myheader("test_proxy_header_proxy_host_connect_errors_reset"); + + rc= mysql_query(mysql, "SET @save_dbug= @@global.debug_dbug"); + myquery(rc); + rc= mysql_query(mysql, "SET GLOBAL debug_dbug='+d,vio_peer_addr_fake_ipv4," + "getnameinfo_fake_ipv4,getaddrinfo_fake_good_ipv4'"); + myquery(rc); + rc= mysql_query(mysql, + "SET @save_max_connect_errors= @@global.max_connect_errors"); + myquery(rc); + rc= mysql_query(mysql, "SET @@global.max_connect_errors=3"); + myquery(rc); + rc= mysql_query(mysql, "FLUSH HOSTS"); + myquery(rc); + + rc= mysql_query(mysql, + "CREATE USER 'u'@'santa.claus.ipv4.example.com' IDENTIFIED BY 'password'"); + myquery(rc); + + /* Errors with no header count against the socket peer (proxy host); + max_connect_errors of them block it. */ + for (i= 0; i < 3; i++) + proxy_send_handshake_error(NULL); + m= proxy_connect_as(proxied_client, "u", "password", &conn_errno); + DIE_UNLESS(m == NULL && conn_errno == ER_HOST_IS_BLOCKED); + + rc= mysql_query(mysql, "FLUSH HOSTS"); + myquery(rc); + + /* Two proxy-host handshake errors, below max_connect_errors (3). */ + proxy_send_handshake_error(NULL); + proxy_send_handshake_error(NULL); + + /* Success: client has no errors, so this must reset the proxy host's two. */ + m= proxy_connect_as(proxied_client, "u", "password", &conn_errno); + DIE_UNLESS(m != NULL); + mysql_close(m); + + /* Two more: with the reset proxy host is at 2 (<3); without it 4 (>=3). */ + proxy_send_handshake_error(NULL); + proxy_send_handshake_error(NULL); + + m= proxy_connect_as(proxied_client, "u", "password", &conn_errno); + DIE_UNLESS(m != NULL); /* ER_HOST_IS_BLOCKED if proxy host not reset */ + mysql_close(m); + + rc= mysql_query(mysql, "DROP USER 'u'@'santa.claus.ipv4.example.com'"); + myquery(rc); + rc= mysql_query(mysql, + "SET GLOBAL max_connect_errors= @save_max_connect_errors"); + myquery(rc); + rc= mysql_query(mysql, "SET GLOBAL debug_dbug= @save_dbug"); + myquery(rc); + rc= mysql_query(mysql, "FLUSH HOSTS"); + myquery(rc); +#endif /* !DBUG_OFF */ +} + static void test_proxy_header() { myheader("test_proxy_header"); @@ -20865,6 +21088,8 @@ static void test_proxy_header() test_proxy_header_ignore(); test_proxy_header_limits(); test_proxy_header_dbug_remote_connection(); + test_proxy_header_connect_errors_reset(); + test_proxy_header_proxy_host_connect_errors_reset(); }