Commit 2ce1a07c authored by kostja@bodhi.local's avatar kostja@bodhi.local

A fix and a test case for Bug#15752 "Lost connection to MySQL server

when calling a SP from C API"

The bug was caused by lack of checks for misuse in mysql_real_query. 
A stored procedure always returns at least one result, which is the 
status of execution of the procedure itself.
This result, or so-called OK packet, is similar to a result
returned by INSERT/UPDATE/CREATE operations: it contains the overall
status of execution, the number of affected rows and the number of
warnings. The client test program attached to the bug did not read this 
result and ivnoked the next query. In turn, libmysql had no check for 
such scenario and mysql_real_query was simply trying to send that query 
without reading the pending response, thus messing up the communication
protocol.

The fix is to return an error from mysql_real_query when it's called
prior to retrieval of all pending results.
parent afb31714
......@@ -36,6 +36,7 @@
/* That one is necessary for defines of OPTION_NO_FOREIGN_KEY_CHECKS etc */
#include "mysql_priv.h"
#include "log_event.h"
#include "sql_common.h"
#define BIN_LOG_HEADER_SIZE 4
#define PROBE_HEADER_LEN (EVENT_LEN_OFFSET+4)
......@@ -1077,7 +1078,7 @@ could be out of memory");
const char *error_msg;
Log_event *ev;
len = net_safe_read(mysql);
len= cli_safe_read(mysql);
if (len == packet_error)
{
fprintf(stderr, "Got error reading packet from server: %s\n",
......
......@@ -837,7 +837,6 @@ int STDCALL mysql_drop_db(MYSQL *mysql, const char *DB);
#define simple_command(mysql, command, arg, length, skip_check) \
(*(mysql)->methods->advanced_command)(mysql, command, \
NullS, 0, arg, length, skip_check)
unsigned long net_safe_read(MYSQL* mysql);
#ifdef __NETWARE__
#pragma pack(pop) /* restore alignment */
......
......@@ -140,7 +140,6 @@ enum enum_server_command
#define SERVER_STATUS_IN_TRANS 1 /* Transaction has started */
#define SERVER_STATUS_AUTOCOMMIT 2 /* Server in auto_commit mode */
#define SERVER_STATUS_MORE_RESULTS 4 /* More results on server */
#define SERVER_MORE_RESULTS_EXISTS 8 /* Multi query - next query exists */
#define SERVER_QUERY_NO_GOOD_INDEX_USED 16
#define SERVER_QUERY_NO_INDEX_USED 32
......
......@@ -35,7 +35,7 @@ my_bool
cli_advanced_command(MYSQL *mysql, enum enum_server_command command,
const char *header, ulong header_length,
const char *arg, ulong arg_length, my_bool skip_check);
unsigned long cli_safe_read(MYSQL *mysql);
void set_stmt_errmsg(MYSQL_STMT * stmt, const char *err, int errcode,
const char *sqlstate);
void set_mysql_error(MYSQL *mysql, int errcode, const char *sqlstate);
......
......@@ -645,7 +645,7 @@ int cli_read_change_user_result(MYSQL *mysql, char *buff, const char *passwd)
NET *net= &mysql->net;
ulong pkt_length;
pkt_length= net_safe_read(mysql);
pkt_length= cli_safe_read(mysql);
if (pkt_length == packet_error)
return 1;
......@@ -666,7 +666,7 @@ int cli_read_change_user_result(MYSQL *mysql, char *buff, const char *passwd)
return 1;
}
/* Read what server thinks about out new auth message report */
if (net_safe_read(mysql) == packet_error)
if (cli_safe_read(mysql) == packet_error)
return 1;
}
return 0;
......@@ -1887,7 +1887,7 @@ my_bool cli_read_prepare_result(MYSQL *mysql, MYSQL_STMT *stmt)
DBUG_ENTER("cli_read_prepare_result");
mysql= mysql->last_used_con;
if ((packet_length= net_safe_read(mysql)) == packet_error)
if ((packet_length= cli_safe_read(mysql)) == packet_error)
DBUG_RETURN(1);
mysql->warning_count= 0;
......@@ -2505,7 +2505,8 @@ int cli_stmt_execute(MYSQL_STMT *stmt)
if (stmt->param_count)
{
NET *net= &stmt->mysql->net;
MYSQL *mysql= stmt->mysql;
NET *net= &mysql->net;
MYSQL_BIND *param, *param_end;
char *param_data;
ulong length;
......@@ -2517,7 +2518,8 @@ int cli_stmt_execute(MYSQL_STMT *stmt)
set_stmt_error(stmt, CR_PARAMS_NOT_BOUND, unknown_sqlstate);
DBUG_RETURN(1);
}
if (stmt->mysql->status != MYSQL_STATUS_READY)
if (mysql->status != MYSQL_STATUS_READY ||
mysql->server_status & SERVER_MORE_RESULTS_EXISTS)
{
set_stmt_error(stmt, CR_COMMANDS_OUT_OF_SYNC, unknown_sqlstate);
DBUG_RETURN(1);
......@@ -4531,7 +4533,7 @@ static int stmt_fetch_row(MYSQL_STMT *stmt, uchar *row)
int cli_unbuffered_fetch(MYSQL *mysql, char **row)
{
if (packet_error == net_safe_read(mysql))
if (packet_error == cli_safe_read(mysql))
return 1;
*row= ((mysql->net.read_pos[0] == 254) ? NULL :
......@@ -4640,7 +4642,7 @@ int cli_read_binary_rows(MYSQL_STMT *stmt)
mysql= mysql->last_used_con;
while ((pkt_len= net_safe_read(mysql)) != packet_error)
while ((pkt_len= cli_safe_read(mysql)) != packet_error)
{
cp= net->read_pos;
if (cp[0] != 254 || pkt_len >= 8)
......
......@@ -584,7 +584,7 @@ err:
*****************************************************************************/
ulong
net_safe_read(MYSQL *mysql)
cli_safe_read(MYSQL *mysql)
{
NET *net= &mysql->net;
ulong len=0;
......@@ -627,6 +627,16 @@ net_safe_read(MYSQL *mysql)
}
else
set_mysql_error(mysql, CR_UNKNOWN_ERROR, unknown_sqlstate);
/*
Cover a protocol design error: error packet does not
contain the server status. Therefore, the client has no way
to find out whether there are more result sets of
a multiple-result-set statement pending. Luckily, in 5.0 an
error always aborts execution of a statement, wherever it is
a multi-statement or a stored procedure, so it should be
safe to unconditionally turn off the flag here.
*/
mysql->server_status&= ~SERVER_MORE_RESULTS_EXISTS;
DBUG_PRINT("error",("Got error: %d/%s (%s)",
net->last_errno, net->sqlstate, net->last_error));
......@@ -662,7 +672,8 @@ cli_advanced_command(MYSQL *mysql, enum enum_server_command command,
if (mysql_reconnect(mysql))
DBUG_RETURN(1);
}
if (mysql->status != MYSQL_STATUS_READY)
if (mysql->status != MYSQL_STATUS_READY ||
mysql->server_status & SERVER_MORE_RESULTS_EXISTS)
{
DBUG_PRINT("error",("state: %d", mysql->status));
set_mysql_error(mysql, CR_COMMANDS_OUT_OF_SYNC, unknown_sqlstate);
......@@ -701,7 +712,7 @@ cli_advanced_command(MYSQL *mysql, enum enum_server_command command,
}
result=0;
if (!skip_check)
result= ((mysql->packet_length=net_safe_read(mysql)) == packet_error ?
result= ((mysql->packet_length=cli_safe_read(mysql)) == packet_error ?
1 : 0);
end:
reset_sigpipe(mysql);
......@@ -753,7 +764,7 @@ static void cli_flush_use_result(MYSQL *mysql)
for (;;)
{
ulong pkt_len;
if ((pkt_len=net_safe_read(mysql)) == packet_error)
if ((pkt_len=cli_safe_read(mysql)) == packet_error)
break;
if (pkt_len <= 8 && mysql->net.read_pos[0] == 254)
{
......@@ -1275,7 +1286,7 @@ MYSQL_DATA *cli_read_rows(MYSQL *mysql,MYSQL_FIELD *mysql_fields,
NET *net = &mysql->net;
DBUG_ENTER("cli_read_rows");
if ((pkt_len= net_safe_read(mysql)) == packet_error)
if ((pkt_len= cli_safe_read(mysql)) == packet_error)
DBUG_RETURN(0);
if (!(result=(MYSQL_DATA*) my_malloc(sizeof(MYSQL_DATA),
MYF(MY_WME | MY_ZEROFILL))))
......@@ -1340,7 +1351,7 @@ MYSQL_DATA *cli_read_rows(MYSQL *mysql,MYSQL_FIELD *mysql_fields,
}
}
cur->data[field]=to; /* End of last field */
if ((pkt_len=net_safe_read(mysql)) == packet_error)
if ((pkt_len=cli_safe_read(mysql)) == packet_error)
{
free_rows(result);
DBUG_RETURN(0);
......@@ -1372,7 +1383,7 @@ read_one_row(MYSQL *mysql,uint fields,MYSQL_ROW row, ulong *lengths)
uchar *pos, *prev_pos, *end_pos;
NET *net= &mysql->net;
if ((pkt_len=net_safe_read(mysql)) == packet_error)
if ((pkt_len=cli_safe_read(mysql)) == packet_error)
return -1;
if (pkt_len <= 8 && net->read_pos[0] == 254)
{
......@@ -1649,23 +1660,23 @@ static MYSQL_RES *cli_use_result(MYSQL *mysql);
static MYSQL_METHODS client_methods=
{
cli_read_query_result,
cli_advanced_command,
cli_read_rows,
cli_use_result,
cli_fetch_lengths,
cli_flush_use_result
cli_read_query_result, /* read_query_result */
cli_advanced_command, /* advanced_command */
cli_read_rows, /* read_rows */
cli_use_result, /* use_result */
cli_fetch_lengths, /* fetch_lengths */
cli_flush_use_result /* flush_use_result */
#ifndef MYSQL_SERVER
,cli_list_fields,
cli_read_prepare_result,
cli_stmt_execute,
cli_read_binary_rows,
cli_unbuffered_fetch,
NULL,
cli_read_statistics,
cli_read_query_result,
cli_read_change_user_result,
cli_read_binary_rows
,cli_list_fields, /* list_fields */
cli_read_prepare_result, /* read_prepare_result */
cli_stmt_execute, /* stmt_execute */
cli_read_binary_rows, /* read_binary_rows */
cli_unbuffered_fetch, /* unbuffered_fetch */
NULL, /* free_embedded_thd */
cli_read_statistics, /* read_statistics */
cli_read_query_result, /* next_result */
cli_read_change_user_result, /* read_change_user_result */
cli_read_binary_rows /* read_rows_from_cursor */
#endif
};
......@@ -1999,7 +2010,7 @@ CLI_MYSQL_REAL_CONNECT(MYSQL *mysql,const char *host, const char *user,
Part 1: Connection established, read and parse first packet
*/
if ((pkt_length=net_safe_read(mysql)) == packet_error)
if ((pkt_length=cli_safe_read(mysql)) == packet_error)
goto error;
/* Check if version of protocol matches current one */
......@@ -2223,7 +2234,7 @@ CLI_MYSQL_REAL_CONNECT(MYSQL *mysql,const char *host, const char *user,
OK-packet, or re-request scrambled password.
*/
if ((pkt_length=net_safe_read(mysql)) == packet_error)
if ((pkt_length=cli_safe_read(mysql)) == packet_error)
goto error;
if (pkt_length == 1 && net->read_pos[0] == 254 &&
......@@ -2240,7 +2251,7 @@ CLI_MYSQL_REAL_CONNECT(MYSQL *mysql,const char *host, const char *user,
goto error;
}
/* Read what server thinks about out new auth message report */
if (net_safe_read(mysql) == packet_error)
if (cli_safe_read(mysql) == packet_error)
goto error;
}
......@@ -2564,7 +2575,7 @@ static my_bool cli_read_query_result(MYSQL *mysql)
*/
mysql = mysql->last_used_con;
if ((length = net_safe_read(mysql)) == packet_error)
if ((length = cli_safe_read(mysql)) == packet_error)
DBUG_RETURN(1);
free_old_query(mysql); /* Free old result */
#ifdef MYSQL_CLIENT /* Avoid warn of unused labels*/
......@@ -2599,7 +2610,7 @@ get_info:
if (field_count == NULL_LENGTH) /* LOAD DATA LOCAL INFILE */
{
int error=handle_local_infile(mysql,(char*) pos);
if ((length=net_safe_read(mysql)) == packet_error || error)
if ((length= cli_safe_read(mysql)) == packet_error || error)
DBUG_RETURN(1);
goto get_info; /* Get info packet */
}
......
......@@ -322,7 +322,7 @@ static char eof_buff[1]= { (char) 254 }; /* Marker for end of fields */
254 Marker (1 byte)
warning_count Stored in 2 bytes; New in 4.1 protocol
status_flag Stored in 2 bytes;
For flags like SERVER_STATUS_MORE_RESULTS
For flags like SERVER_MORE_RESULTS_EXISTS
Note that the warning count will not be sent if 'no_flush' is set as
we don't want to report the warning count until all data is sent to the
......
......@@ -3051,7 +3051,7 @@ static ulong read_event(MYSQL* mysql, MASTER_INFO *mi, bool* suppress_warnings)
return packet_error;
#endif
len = net_safe_read(mysql);
len = cli_safe_read(mysql);
if (len == packet_error || (long) len < 1)
{
if (mysql_errno(mysql) == ER_NET_READ_INTERRUPTED)
......
......@@ -50,7 +50,6 @@ static unsigned int opt_port;
static my_bool tty_password= 0, opt_silent= 0;
static MYSQL *mysql= 0;
static char query[MAX_TEST_QUERY_LENGTH];
static char current_db[]= "client_test_db";
static unsigned int test_count= 0;
static unsigned int opt_count= 0;
......@@ -269,6 +268,7 @@ mysql_simple_prepare(MYSQL *mysql, const char *query)
static void client_connect(ulong flag)
{
int rc;
static char query[MAX_TEST_QUERY_LENGTH];
myheader_r("client_connect");
if (!opt_silent)
......@@ -326,6 +326,8 @@ static void client_connect(ulong flag)
static void client_disconnect()
{
static char query[MAX_TEST_QUERY_LENGTH];
myheader_r("client_disconnect");
if (mysql)
......@@ -657,6 +659,7 @@ int my_stmt_result(const char *buff)
static void verify_col_data(const char *table, const char *col,
const char *exp_data)
{
static char query[MAX_TEST_QUERY_LENGTH];
MYSQL_RES *result;
MYSQL_ROW row;
int rc, field= 1;
......@@ -1362,6 +1365,7 @@ static void test_prepare_insert_update()
for (cur_query= testcase; *cur_query; cur_query++)
{
char query[MAX_TEST_QUERY_LENGTH];
printf("\nRunning query: %s", *cur_query);
strmov(query, *cur_query);
stmt= mysql_simple_prepare(mysql, query);
......@@ -1396,6 +1400,7 @@ static void test_prepare_simple()
{
MYSQL_STMT *stmt;
int rc;
char query[MAX_TEST_QUERY_LENGTH];
myheader("test_prepare_simple");
......@@ -1466,6 +1471,7 @@ static void test_prepare_field_result()
MYSQL_STMT *stmt;
MYSQL_RES *result;
int rc;
char query[MAX_TEST_QUERY_LENGTH];
myheader("test_prepare_field_result");
......@@ -1517,6 +1523,7 @@ static void test_prepare_syntax()
{
MYSQL_STMT *stmt;
int rc;
char query[MAX_TEST_QUERY_LENGTH];
myheader("test_prepare_syntax");
......@@ -1558,6 +1565,7 @@ static void test_prepare()
my_bool is_null[7];
char llbuf[22];
MYSQL_BIND bind[7];
char query[MAX_TEST_QUERY_LENGTH];
myheader("test_prepare");
......@@ -1731,6 +1739,7 @@ static void test_double_compare()
MYSQL_RES *result;
MYSQL_BIND bind[3];
ulong length[3];
char query[MAX_TEST_QUERY_LENGTH];
myheader("test_double_compare");
......@@ -1813,6 +1822,7 @@ static void test_null()
uint nData;
MYSQL_BIND bind[2];
my_bool is_null[2];
char query[MAX_TEST_QUERY_LENGTH];
myheader("test_null");
......@@ -1959,6 +1969,7 @@ static void test_ps_null_param()
/* Execute several queries, all returning NULL in result. */
for(cur_query= queries; *cur_query; cur_query++)
{
char query[MAX_TEST_QUERY_LENGTH];
strmov(query, *cur_query);
stmt= mysql_simple_prepare(mysql, query);
check_stmt(stmt);
......@@ -1990,6 +2001,7 @@ static void test_fetch_null()
MYSQL_BIND bind[11];
ulong length[11];
my_bool is_null[11];
char query[MAX_TEST_QUERY_LENGTH];
myheader("test_fetch_null");
......@@ -2218,6 +2230,7 @@ static void test_select()
int nData= 1;
MYSQL_BIND bind[2];
ulong length[2];
char query[MAX_TEST_QUERY_LENGTH];
myheader("test_select");
......@@ -2289,6 +2302,7 @@ static void test_ps_conj_select()
int32 int_data;
char str_data[32];
unsigned long str_length;
char query[MAX_TEST_QUERY_LENGTH];
myheader("test_ps_conj_select");
rc= mysql_query(mysql, "drop table if exists t1");
......@@ -2346,6 +2360,7 @@ static void test_bug1115()
MYSQL_BIND bind[1];
ulong length[1];
char szData[11];
char query[MAX_TEST_QUERY_LENGTH];
myheader("test_bug1115");
......@@ -2457,6 +2472,7 @@ static void test_bug1180()
MYSQL_BIND bind[1];
ulong length[1];
char szData[11];
char query[MAX_TEST_QUERY_LENGTH];
myheader("test_select_bug");
......@@ -2547,6 +2563,7 @@ static void test_bug1644()
int num;
my_bool isnull;
int rc, i;
char query[MAX_TEST_QUERY_LENGTH];
myheader("test_bug1644");
......@@ -2646,6 +2663,7 @@ static void test_select_show()
{
MYSQL_STMT *stmt;
int rc;
char query[MAX_TEST_QUERY_LENGTH];
myheader("test_select_show");
......@@ -2714,6 +2732,7 @@ static void test_simple_update()
MYSQL_RES *result;
MYSQL_BIND bind[2];
ulong length[2];
char query[MAX_TEST_QUERY_LENGTH];
myheader("test_simple_update");
......@@ -2791,6 +2810,7 @@ static void test_long_data()
char *data= NullS;
MYSQL_RES *result;
MYSQL_BIND bind[3];
char query[MAX_TEST_QUERY_LENGTH];
myheader("test_long_data");
......@@ -2877,6 +2897,7 @@ static void test_long_data_str()
MYSQL_RES *result;
MYSQL_BIND bind[2];
my_bool is_null[2];
char query[MAX_TEST_QUERY_LENGTH];
myheader("test_long_data_str");
......@@ -2969,6 +2990,7 @@ static void test_long_data_str1()
MYSQL_RES *result;
MYSQL_BIND bind[2];
MYSQL_FIELD *field;
char query[MAX_TEST_QUERY_LENGTH];
myheader("test_long_data_str1");
......@@ -3124,6 +3146,7 @@ static void test_long_data_bin()
long length;
MYSQL_RES *result;
MYSQL_BIND bind[2];
char query[MAX_TEST_QUERY_LENGTH];
myheader("test_long_data_bin");
......@@ -3203,6 +3226,7 @@ static void test_simple_delete()
MYSQL_RES *result;
MYSQL_BIND bind[2];
ulong length[2];
char query[MAX_TEST_QUERY_LENGTH];
myheader("test_simple_delete");
......@@ -3285,6 +3309,7 @@ static void test_update()
MYSQL_RES *result;
MYSQL_BIND bind[2];
ulong length[2];
char query[MAX_TEST_QUERY_LENGTH];
myheader("test_update");
......@@ -3381,6 +3406,7 @@ static void test_prepare_noparam()
MYSQL_STMT *stmt;
int rc;
MYSQL_RES *result;
char query[MAX_TEST_QUERY_LENGTH];
myheader("test_prepare_noparam");
......@@ -4237,6 +4263,7 @@ static void test_prepare_ext()
short sData= 10;
longlong bData= 20;
MYSQL_BIND bind[6];
char query[MAX_TEST_QUERY_LENGTH];
myheader("test_prepare_ext");
rc= mysql_query(mysql, "DROP TABLE IF EXISTS test_prepare_ext");
......@@ -4624,6 +4651,7 @@ static void test_stmt_close()
MYSQL_RES *result;
unsigned int count;
int rc;
char query[MAX_TEST_QUERY_LENGTH];
myheader("test_stmt_close");
......@@ -5270,6 +5298,7 @@ static void test_manual_sample()
ulonglong affected_rows;
MYSQL_BIND bind[3];
my_bool is_null;
char query[MAX_TEST_QUERY_LENGTH];
myheader("test_manual_sample");
......@@ -5624,6 +5653,7 @@ static void test_prepare_multi_statements()
{
MYSQL *mysql_local;
MYSQL_STMT *stmt;
char query[MAX_TEST_QUERY_LENGTH];
myheader("test_prepare_multi_statements");
if (!(mysql_local= mysql_init(NULL)))
......@@ -5841,6 +5871,7 @@ static void test_store_result2()
int nData;
ulong length;
MYSQL_BIND bind[1];
char query[MAX_TEST_QUERY_LENGTH];
myheader("test_store_result2");
......@@ -7120,6 +7151,7 @@ static void test_set_option()
static void test_prepare_grant()
{
int rc;
char query[MAX_TEST_QUERY_LENGTH];
myheader("test_prepare_grant");
......@@ -8592,6 +8624,7 @@ static void test_sqlmode()
MYSQL_BIND bind[2];
char c1[5], c2[5];
int rc;
char query[MAX_TEST_QUERY_LENGTH];
myheader("test_sqlmode");
......@@ -8735,6 +8768,7 @@ static void test_ts()
ulong length;
int rc, field_count;
char name;
char query[MAX_TEST_QUERY_LENGTH];
myheader("test_ts");
......@@ -15019,6 +15053,65 @@ static void test_bug20152()
}
}
/* Bug#15752 "Lost connection to MySQL server when calling a SP from C API" */
static void test_bug15752()
{
MYSQL mysql_local;
int rc, i;
const int ITERATION_COUNT= 100;
char *query= "CALL p1()";
myheader("test_bug15752");
rc= mysql_query(mysql, "drop procedure if exists p1");
myquery(rc);
rc= mysql_query(mysql, "create procedure p1() select 1");
myquery(rc);
mysql_init(&mysql_local);
if (! mysql_real_connect(&mysql_local, opt_host, opt_user,
opt_password, current_db, opt_port,
opt_unix_socket,
CLIENT_MULTI_STATEMENTS|CLIENT_MULTI_RESULTS))
{
printf("Unable connect to MySQL server: %s\n", mysql_error(&mysql_local));
DIE_UNLESS(0);
}
rc= mysql_real_query(&mysql_local, query, strlen(query));
myquery(rc);
mysql_free_result(mysql_store_result(&mysql_local));
rc= mysql_real_query(&mysql_local, query, strlen(query));
DIE_UNLESS(rc && mysql_errno(&mysql_local) == CR_COMMANDS_OUT_OF_SYNC);
if (! opt_silent)
printf("Got error (as expected): %s\n", mysql_error(&mysql_local));
/* Check some other commands too */
DIE_UNLESS(mysql_next_result(&mysql_local) == 0);
mysql_free_result(mysql_store_result(&mysql_local));
DIE_UNLESS(mysql_next_result(&mysql_local) == -1);
/* The second problem is not reproducible: add the test case */
for (i = 0; i < ITERATION_COUNT; i++)
{
if (mysql_real_query(&mysql_local, query, strlen(query)))
{
printf("\ni=%d %s failed: %s\n", i, query, mysql_error(&mysql_local));
break;
}
mysql_free_result(mysql_store_result(&mysql_local));
DIE_UNLESS(mysql_next_result(&mysql_local) == 0);
mysql_free_result(mysql_store_result(&mysql_local));
DIE_UNLESS(mysql_next_result(&mysql_local) == -1);
}
mysql_close(&mysql_local);
rc= mysql_query(mysql, "drop procedure p1");
myquery(rc);
}
/*
Read and parse arguments and MySQL options from my.cnf
......@@ -15287,7 +15380,8 @@ static struct my_tests_st my_tests[]= {
{ "test_bug20152", test_bug20152 },
{ "test_bug14169", test_bug14169 },
{ "test_bug17667", test_bug17667 },
{ "test_bug19671", test_bug19671},
{ "test_bug19671", test_bug19671 },
{ "test_bug15752", test_bug15752 },
{ 0, 0 }
};
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment