From 429e07bba242e99799300fa61b517af7cac24267 Mon Sep 17 00:00:00 2001 From: Andrey Hristov Date: Wed, 12 Jan 2011 21:40:05 +0000 Subject: [PATCH] grok the MySQL 5.5 extended handshake. Move the authentication routines, the native ones, to separate file and encapsulate them in a plugin. Depending on the server version and what the server requests (or doesn't in old versions) load the authentication plugin to handle it. Currently only the 4.1+ authentication is supported. More to come --- ext/mysqlnd/config.w32 | 1 + ext/mysqlnd/config9.m4 | 2 +- ext/mysqlnd/mysqlnd.c | 264 +++++++++++----------------- ext/mysqlnd/mysqlnd_auth.c | 271 +++++++++++++++++++++++++++++ ext/mysqlnd/mysqlnd_enum_n_def.h | 9 +- ext/mysqlnd/mysqlnd_priv.h | 2 +- ext/mysqlnd/mysqlnd_structs.h | 19 +- ext/mysqlnd/mysqlnd_wireprotocol.c | 65 ++++++- ext/mysqlnd/mysqlnd_wireprotocol.h | 10 +- 9 files changed, 471 insertions(+), 172 deletions(-) create mode 100644 ext/mysqlnd/mysqlnd_auth.c diff --git a/ext/mysqlnd/config.w32 b/ext/mysqlnd/config.w32 index 6246ce11e4..59e0b0d03e 100644 --- a/ext/mysqlnd/config.w32 +++ b/ext/mysqlnd/config.w32 @@ -7,6 +7,7 @@ if (PHP_MYSQLND != "no") { if (CHECK_LIB("ws2_32.lib", "mysqlnd")) { mysqlnd_source = "mysqlnd.c " + + "mysqlnd_auth.c" + "mysqlnd_block_alloc.c " + "mysqlnd_charset.c " + "mysqlnd_debug.c " + diff --git a/ext/mysqlnd/config9.m4 b/ext/mysqlnd/config9.m4 index 470f215a66..9104a85889 100644 --- a/ext/mysqlnd/config9.m4 +++ b/ext/mysqlnd/config9.m4 @@ -16,7 +16,7 @@ dnl If some extension uses mysqlnd it will get compiled in PHP core if test "$PHP_MYSQLND_ENABLED" = "yes"; then mysqlnd_sources="mysqlnd.c mysqlnd_charset.c mysqlnd_wireprotocol.c \ mysqlnd_ps.c mysqlnd_loaddata.c mysqlnd_net.c \ - mysqlnd_ps_codec.c mysqlnd_statistics.c \ + mysqlnd_ps_codec.c mysqlnd_statistics.c mysqlnd_auth.c \ mysqlnd_result.c mysqlnd_result_meta.c mysqlnd_debug.c\ mysqlnd_block_alloc.c mysqlnd_plugin.c php_mysqlnd.c" diff --git a/ext/mysqlnd/mysqlnd.c b/ext/mysqlnd/mysqlnd.c index 6ee994c097..94cd5b89a8 100644 --- a/ext/mysqlnd/mysqlnd.c +++ b/ext/mysqlnd/mysqlnd.c @@ -92,6 +92,10 @@ MYSQLND_METHOD(mysqlnd_conn, free_options)(MYSQLND * conn TSRMLS_DC) mnd_pefree(conn->options.charset_name, pers); conn->options.charset_name = NULL; } + if (conn->options.auth_protocol) { + mnd_pefree(conn->options.auth_protocol, pers); + conn->options.auth_protocol = NULL; + } if (conn->options.num_commands) { unsigned int i; for (i = 0; i < conn->options.num_commands; i++) { @@ -427,9 +431,8 @@ MYSQLND_METHOD(mysqlnd_conn, end_psession)(MYSQLND * conn TSRMLS_DC) /* }}} */ -#define MYSQLND_ASSEMBLED_PACKET_MAX_SIZE 3UL*1024UL*1024UL*1024UL /* {{{ mysqlnd_switch_to_ssl_if_needed */ -static MYSQLND_PACKET_AUTH * +static enum_func_status mysqlnd_switch_to_ssl_if_needed( MYSQLND * conn, const MYSQLND_PACKET_GREET * const greet_packet, @@ -438,13 +441,15 @@ mysqlnd_switch_to_ssl_if_needed( TSRMLS_DC ) { - const MYSQLND_CHARSET * charset = NULL; - MYSQLND_PACKET_AUTH * auth_packet = conn->protocol->m.get_auth_packet(conn->protocol, FALSE TSRMLS_CC); + enum_func_status ret = FAIL; + const MYSQLND_CHARSET * charset; + MYSQLND_PACKET_AUTH * auth_packet; DBG_ENTER("mysqlnd_switch_to_ssl_if_needed"); + auth_packet = conn->protocol->m.get_auth_packet(conn->protocol, FALSE TSRMLS_CC); if (!auth_packet) { SET_OOM_ERROR(conn->error_info); - goto err; + goto end; } auth_packet->client_flags = mysql_flags; auth_packet->max_packet_size = MYSQLND_ASSEMBLED_PACKET_MAX_SIZE; @@ -466,20 +471,20 @@ mysqlnd_switch_to_ssl_if_needed( if (!PACKET_WRITE(auth_packet, conn)) { CONN_SET_STATE(conn, CONN_QUIT_SENT); SET_CLIENT_ERROR(conn->error_info, CR_SERVER_GONE_ERROR, UNKNOWN_SQLSTATE, mysqlnd_server_gone); - goto err; + goto end; } conn->net->m.set_client_option(conn->net, MYSQL_OPT_SSL_VERIFY_SERVER_CERT, (const char *) &verify TSRMLS_CC); if (FAIL == conn->net->m.enable_ssl(conn->net TSRMLS_CC)) { - goto err; + goto end; } } #endif - DBG_RETURN(auth_packet); -err: + ret = PASS; +end: PACKET_FREE(auth_packet); - DBG_RETURN(NULL); + DBG_RETURN(ret); } /* }}} */ @@ -498,76 +503,52 @@ mysqlnd_connect_run_authentication( TSRMLS_DC) { enum_func_status ret = FAIL; - MYSQLND_PACKET_AUTH * auth_packet = NULL; - MYSQLND_PACKET_OK * ok_packet = conn->protocol->m.get_ok_packet(conn->protocol, FALSE TSRMLS_CC); - DBG_ENTER("mysqlnd_connect_run_authentication"); - if (!ok_packet) { - SET_OOM_ERROR(conn->error_info); - goto err; - } - - auth_packet = mysqlnd_switch_to_ssl_if_needed(conn, greet_packet, options, mysql_flags TSRMLS_CC); + ret = mysqlnd_switch_to_ssl_if_needed(conn, greet_packet, options, mysql_flags TSRMLS_CC); + if (PASS == ret) { + char * switch_to_auth_protocol = NULL; + char * requested_protocol = NULL; + + do { + struct st_mysqlnd_authentication_plugin * auth_plugin; + char * plugin_name = NULL; + requested_protocol = switch_to_auth_protocol? switch_to_auth_protocol: + (greet_packet->auth_protocol? + greet_packet->auth_protocol: + "mysql_native_password" + ); + spprintf(&plugin_name, 0, "auth_plugin_%s", requested_protocol); + DBG_INF_FMT("looking for %s auth plugin", plugin_name); + auth_plugin = mysqlnd_plugin_find(plugin_name); + efree(plugin_name); + if (!auth_plugin) { + php_error_docref(NULL TSRMLS_CC, E_WARNING, "Server requested authentication method uknown to the client [%s]", requested_protocol); + SET_CLIENT_ERROR(conn->error_info, CR_NOT_IMPLEMENTED, UNKNOWN_SQLSTATE, "Server requested authentication method uknown to the client"); + break; + } - if (!auth_packet) { - goto err; - } + DBG_INF("plugin found"); - auth_packet->send_auth_data = TRUE; - auth_packet->user = user; - auth_packet->password = passwd; - auth_packet->db = db; - auth_packet->db_len = db_len; + ret = auth_plugin->methods.auth_handshake(conn, user, passwd, db, db_len, greet_packet, options, mysql_flags, + &switch_to_auth_protocol TSRMLS_CC); + DBG_INF_FMT("switch_to_auth_protocol=%s", switch_to_auth_protocol? switch_to_auth_protocol:"n/a"); + } while (ret == FAIL && switch_to_auth_protocol != NULL); - conn->scramble = auth_packet->server_scramble_buf = mnd_pemalloc(SCRAMBLE_LENGTH, conn->persistent); - if (!conn->scramble) { - SET_OOM_ERROR(conn->error_info); - goto err; - } - memcpy(auth_packet->server_scramble_buf, greet_packet->scramble_buf, SCRAMBLE_LENGTH); - - if (!PACKET_WRITE(auth_packet, conn)) { - CONN_SET_STATE(conn, CONN_QUIT_SENT); - SET_CLIENT_ERROR(conn->error_info, CR_SERVER_GONE_ERROR, UNKNOWN_SQLSTATE, mysqlnd_server_gone); - goto err; - } + if (ret == PASS) { + conn->m->set_client_option(conn, MYSQLND_OPT_AUTH_PROTOCOL, requested_protocol TSRMLS_CC); + } - if (FAIL == PACKET_READ(ok_packet, conn) || ok_packet->field_count >= 0xFE) { - if (ok_packet->field_count == 0xFE) { - /* old authentication with new server !*/ - DBG_ERR(mysqlnd_old_passwd); - SET_CLIENT_ERROR(conn->error_info, CR_UNKNOWN_ERROR, UNKNOWN_SQLSTATE, mysqlnd_old_passwd); - } else if (ok_packet->field_count == 0xFF) { - if (ok_packet->sqlstate[0]) { - strlcpy(conn->error_info.sqlstate, ok_packet->sqlstate, sizeof(conn->error_info.sqlstate)); - DBG_ERR_FMT("ERROR:%u [SQLSTATE:%s] %s", ok_packet->error_no, ok_packet->sqlstate, ok_packet->error); - } - conn->error_info.error_no = ok_packet->error_no; - strlcpy(conn->error_info.error, ok_packet->error, sizeof(conn->error_info.error)); + if (switch_to_auth_protocol) { + mnd_efree(switch_to_auth_protocol); + switch_to_auth_protocol = NULL; } - goto err; } - - SET_NEW_MESSAGE(conn->last_message, conn->last_message_len, - ok_packet->message, ok_packet->message_len, - conn->persistent); - conn->charset = mysqlnd_find_charset_nr(auth_packet->charset_no); - ret = PASS; -err: - PACKET_FREE(auth_packet); - PACKET_FREE(ok_packet); DBG_RETURN(ret); } /* }}} */ -#define MYSQLND_CAPABILITIES (CLIENT_LONG_PASSWORD | CLIENT_LONG_FLAG | CLIENT_TRANSACTIONS | \ - CLIENT_PROTOCOL_41 | CLIENT_SECURE_CONNECTION | \ - CLIENT_MULTI_RESULTS) - - - /* {{{ mysqlnd_conn::connect */ static enum_func_status MYSQLND_METHOD(mysqlnd_conn, connect)(MYSQLND * conn, @@ -594,7 +575,7 @@ MYSQLND_METHOD(mysqlnd_conn, connect)(MYSQLND * conn, host?host:"", user?user:"", db?db:"", port, mysql_flags, conn? conn->persistent:0, conn? CONN_GET_STATE(conn):-1); - if (conn && CONN_GET_STATE(conn) > CONN_ALLOCED && CONN_GET_STATE(conn) ) { + if (CONN_GET_STATE(conn) > CONN_ALLOCED && CONN_GET_STATE(conn) ) { DBG_INF("Connecting on a connected handle."); if (CONN_GET_STATE(conn) < CONN_QUIT_SENT) { @@ -618,6 +599,9 @@ MYSQLND_METHOD(mysqlnd_conn, connect)(MYSQLND * conn, saved_compression = TRUE; conn->net->compressed = FALSE; } + } else { + unsigned int max_allowed_size = MYSQLND_ASSEMBLED_PACKET_MAX_SIZE; + conn->m->set_client_option(conn, MYSQLND_OPT_MAX_ALLOWED_PACKET, (char *)&max_allowed_size TSRMLS_CC); } if (!host || !host[0]) { @@ -714,7 +698,6 @@ MYSQLND_METHOD(mysqlnd_conn, connect)(MYSQLND * conn, conn->greet_charset = mysqlnd_find_charset_nr(greet_packet->charset_no); /* we allow load data local infile by default */ - mysql_flags |= CLIENT_LOCAL_FILES | CLIENT_PS_MULTI_RESULTS; mysql_flags |= MYSQLND_CAPABILITIES; if (db) { @@ -1936,15 +1919,7 @@ MYSQLND_METHOD(mysqlnd_conn, change_user)(MYSQLND * const conn, Stack space is not that expensive, so use a bit more to be protected against buffer overflows. */ - size_t user_len, db_len; enum_func_status ret = FAIL; - MYSQLND_PACKET_CHG_USER_RESPONSE * chg_user_resp = NULL; - char buffer[MYSQLND_MAX_ALLOWED_USER_LEN + 1 + SCRAMBLE_LENGTH + MYSQLND_MAX_ALLOWED_DB_LEN + 1 + 2 /* charset*/ ]; - char *p = buffer; - const MYSQLND_CHARSET * old_cs = conn->charset; - - MYSQLND_PACKET_AUTH * auth_packet = conn->protocol->m.get_auth_packet(conn->protocol, FALSE TSRMLS_CC); - DBG_ENTER("mysqlnd_conn::change_user"); DBG_INF_FMT("conn=%llu user=%s passwd=%s db=%s silent=%u", @@ -1952,11 +1927,6 @@ MYSQLND_METHOD(mysqlnd_conn, change_user)(MYSQLND * const conn, SET_ERROR_AFF_ROWS(conn); - if (!auth_packet) { - SET_OOM_ERROR(conn->error_info); - goto end; - } - if (!user) { user = ""; } @@ -1966,86 +1936,43 @@ MYSQLND_METHOD(mysqlnd_conn, change_user)(MYSQLND * const conn, if (!db) { db = ""; } - user_len = strlen(user); - db_len = strlen(db); - - auth_packet->is_change_user_packet = TRUE; - auth_packet->user = user; - auth_packet->password = passwd; - auth_packet->db = db; - auth_packet->db_len = db_len; - auth_packet->server_scramble_buf = conn->scramble; - auth_packet->silent = silent; - if (mysqlnd_get_server_version(conn) >= 50123) { - auth_packet->charset_no = conn->charset->nr; - p+=2; - } - - if (!PACKET_WRITE(auth_packet, conn)) { - CONN_SET_STATE(conn, CONN_QUIT_SENT); - SET_CLIENT_ERROR(conn->error_info, CR_SERVER_GONE_ERROR, UNKNOWN_SQLSTATE, mysqlnd_server_gone); - goto end; - } - - chg_user_resp = conn->protocol->m.get_change_user_response_packet(conn->protocol, FALSE TSRMLS_CC); - if (!chg_user_resp) { - SET_OOM_ERROR(conn->error_info); - goto end; - } - ret = PACKET_READ(chg_user_resp, conn); - conn->error_info = chg_user_resp->error_info; - - if (conn->error_info.error_no) { - ret = FAIL; - /* - COM_CHANGE_USER is broken in 5.1. At least in 5.1.15 and 5.1.14, 5.1.11 is immune. - bug#25371 mysql_change_user() triggers "packets out of sync" - When it gets fixed, there should be one more check here - */ - if (mysqlnd_get_server_version(conn) > 50113L && mysqlnd_get_server_version(conn) < 50118L) { - MYSQLND_PACKET_OK * redundant_error_packet = conn->protocol->m.get_ok_packet(conn->protocol, FALSE TSRMLS_CC); - if (redundant_error_packet) { - PACKET_READ(redundant_error_packet, conn); - PACKET_FREE(redundant_error_packet); - DBG_INF_FMT("Server is %u, buggy, sends two ERR messages", mysqlnd_get_server_version(conn)); - } else { - SET_OOM_ERROR(conn->error_info); - } - } - } - if (ret == PASS) { - char * tmp = NULL; - /* if we get conn->user as parameter and then we first free it, then estrndup it, we will crash */ - tmp = mnd_pestrndup(user, user_len, conn->persistent); - if (conn->user) { - mnd_pefree(conn->user, conn->persistent); - } - conn->user = tmp; - - tmp = mnd_pestrdup(passwd, conn->persistent); - if (conn->passwd) { - mnd_pefree(conn->passwd, conn->persistent); - } - conn->passwd = tmp; - if (conn->last_message) { - mnd_pefree(conn->last_message, conn->persistent); - conn->last_message = NULL; + { + char * switch_to_auth_protocol = NULL; + const char * requested_protocol = NULL; + + do { + struct st_mysqlnd_authentication_plugin * auth_plugin; + char * plugin_name = NULL; + requested_protocol = switch_to_auth_protocol? switch_to_auth_protocol: + ((conn->server_capabilities & CLIENT_PLUGIN_AUTH)? + conn->options.auth_protocol: + "mysql_native_password" + ); + + spprintf(&plugin_name, 0, "auth_plugin_%s", requested_protocol); + DBG_INF_FMT("looking for %s auth plugin", plugin_name); + auth_plugin = mysqlnd_plugin_find(plugin_name); + efree(plugin_name); + if (!auth_plugin) { + if (!silent) { + php_error_docref(NULL TSRMLS_CC, E_WARNING, "Server requested authentication method uknown to the client [%s]", requested_protocol); + } + SET_CLIENT_ERROR(conn->error_info, CR_NOT_IMPLEMENTED, UNKNOWN_SQLSTATE, "Server requested authentication method uknown to the client"); + break; + } + DBG_INF("plugin found"); + ret = auth_plugin->methods.auth_change_user(conn, user, strlen(user), passwd, db, strlen(db), silent, &switch_to_auth_protocol TSRMLS_CC); + DBG_INF_FMT("switch_to_auth_protocol=%s", switch_to_auth_protocol? switch_to_auth_protocol:"n/a"); + } while (ret == FAIL && switch_to_auth_protocol != NULL); + if (ret == PASS) { + conn->m->set_client_option(conn, MYSQLND_OPT_AUTH_PROTOCOL, requested_protocol TSRMLS_CC); } - memset(&conn->upsert_status, 0, sizeof(conn->upsert_status)); - /* set charset for old servers */ - if (mysqlnd_get_server_version(conn) < 50123) { - ret = conn->m->set_charset(conn, old_cs->name TSRMLS_CC); + if (switch_to_auth_protocol) { + mnd_efree(switch_to_auth_protocol); + switch_to_auth_protocol = NULL; } - } else if (ret == FAIL && chg_user_resp->server_asked_323_auth == TRUE) { - /* old authentication with new server !*/ - DBG_ERR(mysqlnd_old_passwd); - SET_CLIENT_ERROR(conn->error_info, CR_UNKNOWN_ERROR, UNKNOWN_SQLSTATE, mysqlnd_old_passwd); } -end: - PACKET_FREE(auth_packet); - PACKET_FREE(chg_user_resp); - /* Here we should close all statements. Unbuffered queries should not be a problem as we won't allow sending COM_CHANGE_USER. @@ -2165,7 +2092,25 @@ MYSQLND_METHOD(mysqlnd_conn, set_client_option)(MYSQLND * const conn, /* todo: throw an error, we don't support embedded */ break; #endif - + case MYSQLND_OPT_MAX_ALLOWED_PACKET: + if (*(unsigned int*) value > (1<<16)) { + conn->options.max_allowed_packet = *(unsigned int*) value; + } + break; + case MYSQLND_OPT_AUTH_PROTOCOL: + { + char * new_auth_protocol = mnd_pestrdup(value, conn->persistent); + DBG_INF("MYSQLND_OPT_AUTH_PROTOCOL"); + if (!new_auth_protocol) { + goto oom; + } + if (conn->options.auth_protocol) { + mnd_pefree(conn->options.auth_protocol, conn->persistent); + } + conn->options.auth_protocol = new_auth_protocol; + DBG_INF_FMT("auth_protocol=%s", conn->options.auth_protocol); + break; + } #ifdef WHEN_SUPPORTED_BY_MYSQLI case MYSQL_SHARED_MEMORY_BASE_NAME: case MYSQL_OPT_USE_RESULT: @@ -2403,6 +2348,7 @@ PHPAPI void mysqlnd_library_init(TSRMLS_D) } mysqlnd_example_plugin_register(TSRMLS_C); mysqlnd_debug_trace_plugin_register(TSRMLS_C); + mysqlnd_native_authentication_plugin_register(TSRMLS_C); } } /* }}} */ diff --git a/ext/mysqlnd/mysqlnd_auth.c b/ext/mysqlnd/mysqlnd_auth.c new file mode 100644 index 0000000000..cc360cb6de --- /dev/null +++ b/ext/mysqlnd/mysqlnd_auth.c @@ -0,0 +1,271 @@ +/* + +----------------------------------------------------------------------+ + | PHP Version 5 | + +----------------------------------------------------------------------+ + | Copyright (c) 2006-2011 The PHP Group | + +----------------------------------------------------------------------+ + | This source file is subject to version 3.01 of the PHP license, | + | that is bundled with this package in the file LICENSE, and is | + | available through the world-wide-web at the following url: | + | http://www.php.net/license/3_01.txt | + | If you did not receive a copy of the PHP license and are unable to | + | obtain it through the world-wide-web, please send a note to | + | license@php.net so we can mail you a copy immediately. | + +----------------------------------------------------------------------+ + | Authors: Georg Richter | + | Andrey Hristov | + | Ulf Wendel | + +----------------------------------------------------------------------+ +*/ + +/* $Id: mysqlnd.c 307377 2011-01-11 13:02:57Z andrey $ */ +#include "php.h" +#include "mysqlnd.h" +#include "mysqlnd_structs.h" +#include "mysqlnd_wireprotocol.h" +#include "mysqlnd_priv.h" +#include "mysqlnd_result.h" +#include "mysqlnd_charset.h" +#include "mysqlnd_debug.h" + + +/* {{{ mysqlnd_native_auth_handshake */ +static enum_func_status +mysqlnd_native_auth_handshake(MYSQLND * conn, + const char * const user, + const char * const passwd, + const char * const db, + const size_t db_len, + const MYSQLND_PACKET_GREET * const greet_packet, + const MYSQLND_OPTIONS * const options, + unsigned long mysql_flags, + char ** switch_to_auth_protocol + TSRMLS_DC) +{ + enum_func_status ret = FAIL; + const MYSQLND_CHARSET * charset = NULL; + MYSQLND_PACKET_OK * ok_packet = NULL; + MYSQLND_PACKET_AUTH * auth_packet = NULL; + + DBG_ENTER("mysqlnd_native_auth_handshake"); + + /* 5.5.x reports 21 as scramble length because it needs to show the length of the data before the plugin name */ + if (greet_packet->scramble_buf_len != SCRAMBLE_LENGTH && (greet_packet->scramble_buf_len != 21)) { + /* mysql_native_password only works with SCRAMBLE_LENGTH scramble */ + SET_CLIENT_ERROR(conn->error_info, CR_MALFORMED_PACKET, UNKNOWN_SQLSTATE, "The server sent wrong length for scramble"); + DBG_ERR_FMT("The server sent wrong length for scramble %u. Expected %u", greet_packet->scramble_buf_len, SCRAMBLE_LENGTH); + goto end; + } + + ok_packet = conn->protocol->m.get_ok_packet(conn->protocol, FALSE TSRMLS_CC); + auth_packet = conn->protocol->m.get_auth_packet(conn->protocol, FALSE TSRMLS_CC); + + if (!ok_packet || !auth_packet) { + SET_OOM_ERROR(conn->error_info); + goto end; + } + + auth_packet->client_flags = mysql_flags; + auth_packet->max_packet_size = options->max_allowed_packet; + if (options->charset_name && (charset = mysqlnd_find_charset_name(options->charset_name))) { + auth_packet->charset_no = charset->nr; + } else { +#if MYSQLND_UNICODE + auth_packet->charset_no = 200;/* utf8 - swedish collation, check mysqlnd_charset.c */ +#else + auth_packet->charset_no = greet_packet->charset_no; +#endif + } + + auth_packet->send_auth_data = TRUE; + auth_packet->user = user; + auth_packet->password = passwd; + auth_packet->db = db; + auth_packet->db_len = db_len; + + auth_packet->server_scramble_buf_len = greet_packet->scramble_buf_len; + conn->scramble = auth_packet->server_scramble_buf = mnd_pemalloc(auth_packet->server_scramble_buf_len, conn->persistent); + if (!conn->scramble) { + SET_OOM_ERROR(conn->error_info); + goto end; + } + memcpy(auth_packet->server_scramble_buf, greet_packet->scramble_buf, greet_packet->scramble_buf_len); + + if (!PACKET_WRITE(auth_packet, conn)) { + CONN_SET_STATE(conn, CONN_QUIT_SENT); + SET_CLIENT_ERROR(conn->error_info, CR_SERVER_GONE_ERROR, UNKNOWN_SQLSTATE, mysqlnd_server_gone); + goto end; + } + + if (FAIL == PACKET_READ(ok_packet, conn) || ok_packet->field_count >= 0xFE) { + if (ok_packet->field_count == 0xFE) { + /* old authentication with new server !*/ + DBG_ERR(mysqlnd_old_passwd); + SET_CLIENT_ERROR(conn->error_info, CR_UNKNOWN_ERROR, UNKNOWN_SQLSTATE, mysqlnd_old_passwd); + } else if (ok_packet->field_count == 0xFF) { + if (ok_packet->sqlstate[0]) { + strlcpy(conn->error_info.sqlstate, ok_packet->sqlstate, sizeof(conn->error_info.sqlstate)); + DBG_ERR_FMT("ERROR:%u [SQLSTATE:%s] %s", ok_packet->error_no, ok_packet->sqlstate, ok_packet->error); + } + conn->error_info.error_no = ok_packet->error_no; + strlcpy(conn->error_info.error, ok_packet->error, sizeof(conn->error_info.error)); + } + goto end; + } + + SET_NEW_MESSAGE(conn->last_message, conn->last_message_len, ok_packet->message, ok_packet->message_len, conn->persistent); + conn->charset = mysqlnd_find_charset_nr(auth_packet->charset_no); + ret = PASS; +end: + PACKET_FREE(auth_packet); + PACKET_FREE(ok_packet); + DBG_RETURN(ret); +} +/* }}} */ + + +/* {{{ mysqlnd_native_auth_change_user */ +static enum_func_status +mysqlnd_native_auth_change_user(MYSQLND * const conn, + const char * const user, + const size_t user_len, + const char * const passwd, + const char * const db, + const size_t db_len, + const zend_bool silent, + char ** switch_to_auth_protocol + TSRMLS_DC) +{ + /* + User could be max 16 * 3 (utf8), pass is 20 usually, db is up to 64*3 + Stack space is not that expensive, so use a bit more to be protected against + buffer overflows. + */ + enum_func_status ret = FAIL; + char buffer[MYSQLND_MAX_ALLOWED_USER_LEN + 1 + SCRAMBLE_LENGTH + MYSQLND_MAX_ALLOWED_DB_LEN + 1 + 2 /* charset*/ ]; + char *p = buffer; + const MYSQLND_CHARSET * old_cs = conn->charset; + MYSQLND_PACKET_AUTH * auth_packet = conn->protocol->m.get_auth_packet(conn->protocol, FALSE TSRMLS_CC); + MYSQLND_PACKET_CHG_USER_RESPONSE * chg_user_resp = conn->protocol->m.get_change_user_response_packet(conn->protocol, FALSE TSRMLS_CC); + + DBG_ENTER("mysqlnd_native_auth_change_user"); + + if (!auth_packet || !chg_user_resp) { + SET_OOM_ERROR(conn->error_info); + goto end; + } + auth_packet->is_change_user_packet = TRUE; + auth_packet->user = user; + auth_packet->password = passwd; + auth_packet->db = db; + auth_packet->db_len = db_len; + auth_packet->server_scramble_buf = conn->scramble; + auth_packet->silent = silent; + if (mysqlnd_get_server_version(conn) >= 50123) { + auth_packet->charset_no = conn->charset->nr; + p+=2; + } + + if (!PACKET_WRITE(auth_packet, conn)) { + CONN_SET_STATE(conn, CONN_QUIT_SENT); + SET_CLIENT_ERROR(conn->error_info, CR_SERVER_GONE_ERROR, UNKNOWN_SQLSTATE, mysqlnd_server_gone); + goto end; + } + + ret = PACKET_READ(chg_user_resp, conn); + conn->error_info = chg_user_resp->error_info; + + if (conn->error_info.error_no) { + ret = FAIL; + /* + COM_CHANGE_USER is broken in 5.1. At least in 5.1.15 and 5.1.14, 5.1.11 is immune. + bug#25371 mysql_change_user() triggers "packets out of sync" + When it gets fixed, there should be one more check here + */ + if (mysqlnd_get_server_version(conn) > 50113L && mysqlnd_get_server_version(conn) < 50118L) { + MYSQLND_PACKET_OK * redundant_error_packet = conn->protocol->m.get_ok_packet(conn->protocol, FALSE TSRMLS_CC); + if (redundant_error_packet) { + PACKET_READ(redundant_error_packet, conn); + PACKET_FREE(redundant_error_packet); + DBG_INF_FMT("Server is %u, buggy, sends two ERR messages", mysqlnd_get_server_version(conn)); + } else { + SET_OOM_ERROR(conn->error_info); + } + } + } + if (ret == PASS) { + char * tmp = NULL; + /* if we get conn->user as parameter and then we first free it, then estrndup it, we will crash */ + tmp = mnd_pestrndup(user, user_len, conn->persistent); + if (conn->user) { + mnd_pefree(conn->user, conn->persistent); + } + conn->user = tmp; + + tmp = mnd_pestrdup(passwd, conn->persistent); + if (conn->passwd) { + mnd_pefree(conn->passwd, conn->persistent); + } + conn->passwd = tmp; + + if (conn->last_message) { + mnd_pefree(conn->last_message, conn->persistent); + conn->last_message = NULL; + } + memset(&conn->upsert_status, 0, sizeof(conn->upsert_status)); + /* set charset for old servers */ + if (mysqlnd_get_server_version(conn) < 50123) { + ret = conn->m->set_charset(conn, old_cs->name TSRMLS_CC); + } + } else if (ret == FAIL && chg_user_resp->server_asked_323_auth == TRUE) { + /* old authentication with new server !*/ + DBG_ERR(mysqlnd_old_passwd); + SET_CLIENT_ERROR(conn->error_info, CR_UNKNOWN_ERROR, UNKNOWN_SQLSTATE, mysqlnd_old_passwd); + } +end: + PACKET_FREE(auth_packet); + PACKET_FREE(chg_user_resp); + DBG_RETURN(ret); +} +/* }}} */ + +static struct st_mysqlnd_authentication_plugin mysqlnd_native_auth_plugin = +{ + { + MYSQLND_PLUGIN_API_VERSION, + "auth_plugin_mysql_native_password", + MYSQLND_VERSION_ID, + MYSQLND_VERSION, + "PHP License 3.01", + "Andrey Hristov , Ulf Wendel , Georg Richter ", + { + NULL, /* no statistics , will be filled later if there are some */ + NULL, /* no statistics */ + }, + { + NULL /* plugin shutdown */ + } + }, + {/* methods */ + mysqlnd_native_auth_handshake, + mysqlnd_native_auth_change_user + } +}; + +/* {{{ mysqlnd_native_authentication_plugin_register */ +void +mysqlnd_native_authentication_plugin_register(TSRMLS_D) +{ + mysqlnd_plugin_register_ex((struct st_mysqlnd_plugin_header *) &mysqlnd_native_auth_plugin TSRMLS_CC); +} +/* }}} */ + + +/* + * Local variables: + * tab-width: 4 + * c-basic-offset: 4 + * End: + * vim600: noet sw=4 ts=4 fdm=marker + * vim<600: noet sw=4 ts=4 + */ diff --git a/ext/mysqlnd/mysqlnd_enum_n_def.h b/ext/mysqlnd/mysqlnd_enum_n_def.h index 07f85d3c78..79fee34176 100644 --- a/ext/mysqlnd/mysqlnd_enum_n_def.h +++ b/ext/mysqlnd/mysqlnd_enum_n_def.h @@ -35,6 +35,8 @@ #define MYSQLND_MAX_PACKET_SIZE (256L*256L*256L-1) +#define MYSQLND_ASSEMBLED_PACKET_MAX_SIZE 3UL*1024UL*1024UL*1024UL + #define MYSQLND_ERRMSG_SIZE 512 #define MYSQLND_SQLSTATE_LENGTH 5 #define MYSQLND_SQLSTATE_NULL "00000" @@ -96,6 +98,9 @@ #define CLIENT_SSL_VERIFY_SERVER_CERT (1UL << 30) +#define MYSQLND_CAPABILITIES (CLIENT_LONG_PASSWORD | CLIENT_LONG_FLAG | CLIENT_TRANSACTIONS | \ + CLIENT_PROTOCOL_41 | CLIENT_SECURE_CONNECTION | \ + CLIENT_MULTI_RESULTS | CLIENT_PS_MULTI_RESULTS | CLIENT_LOCAL_FILES) #define MYSQLND_NET_FLAG_USE_COMPRESSION 1 @@ -170,7 +175,9 @@ typedef enum mysqlnd_option MYSQLND_OPT_SSL_CA = 206, MYSQLND_OPT_SSL_CAPATH = 207, MYSQLND_OPT_SSL_CIPHER = 208, - MYSQLND_OPT_SSL_PASSPHRASE = 209 + MYSQLND_OPT_SSL_PASSPHRASE = 209, + MYSQLND_OPT_MAX_ALLOWED_PACKET = 210, + MYSQLND_OPT_AUTH_PROTOCOL = 211 } enum_mysqlnd_option; typedef enum mysqlnd_protocol_type diff --git a/ext/mysqlnd/mysqlnd_priv.h b/ext/mysqlnd/mysqlnd_priv.h index 968e0b11c8..f35862087d 100644 --- a/ext/mysqlnd/mysqlnd_priv.h +++ b/ext/mysqlnd/mysqlnd_priv.h @@ -192,7 +192,7 @@ void ps_fetch_from_1_to_8_bytes(zval *zv, const MYSQLND_FIELD * const field, void mysqlnd_plugin_subsystem_init(TSRMLS_D); void mysqlnd_plugin_subsystem_end(TSRMLS_D); - +void mysqlnd_native_authentication_plugin_register(TSRMLS_D); void mysqlnd_example_plugin_register(TSRMLS_D); diff --git a/ext/mysqlnd/mysqlnd_structs.h b/ext/mysqlnd/mysqlnd_structs.h index 4a28f17ae2..5ba2f4c68f 100644 --- a/ext/mysqlnd/mysqlnd_structs.h +++ b/ext/mysqlnd/mysqlnd_structs.h @@ -151,12 +151,12 @@ typedef struct st_mysqlnd_options char *cfg_file; char *cfg_section; + char *auth_protocol; /* We need to keep these because otherwise st_mysqlnd_conn will be changed. The ABI will be broken and the methods structure will be somewhere else in the memory which can crash external code. Feel free to reuse these. */ - char * unused1; char * unused2; char * unused3; char * unused4; @@ -963,4 +963,21 @@ struct st_mysqlnd_typeii_plugin_example unsigned int counter; }; +struct st_mysqlnd_packet_greet; + +struct st_mysqlnd_authentication_plugin +{ + struct st_mysqlnd_plugin_header plugin_header; + struct { + enum_func_status (*auth_handshake)(MYSQLND * conn, const char * const user, const char * const passwd, const char * const db, + const size_t db_len, const struct st_mysqlnd_packet_greet * const greet_packet, + const MYSQLND_OPTIONS * const options, unsigned long mysql_flags, + char ** switch_to_auth_protocol TSRMLS_DC); + + enum_func_status (*auth_change_user)(MYSQLND * const conn, const char * const user, const size_t user_len, const char * const passwd, + const char * const db, const size_t db_len, const zend_bool silent, + char ** switch_to_auth_protocol TSRMLS_DC); + } methods; +}; + #endif /* MYSQLND_STRUCTS_H */ diff --git a/ext/mysqlnd/mysqlnd_wireprotocol.c b/ext/mysqlnd/mysqlnd_wireprotocol.c index f3a4ba4c1e..2ee894f639 100644 --- a/ext/mysqlnd/mysqlnd_wireprotocol.c +++ b/ext/mysqlnd/mysqlnd_wireprotocol.c @@ -306,6 +306,7 @@ php_mysqlnd_greet_read(void *_packet, MYSQLND *conn TSRMLS_DC) zend_uchar buf[2048]; zend_uchar *p = buf; zend_uchar *begin = buf; + zend_uchar *pad_start = NULL; MYSQLND_PACKET_GREET *packet= (MYSQLND_PACKET_GREET *) _packet; DBG_ENTER("php_mysqlnd_greet_read"); @@ -313,6 +314,17 @@ php_mysqlnd_greet_read(void *_packet, MYSQLND *conn TSRMLS_DC) PACKET_READ_HEADER_AND_BODY(packet, conn, buf, sizeof(buf), "greeting", PROT_GREET_PACKET); BAIL_IF_NO_MORE_DATA; + packet->scramble_buf = &packet->intern_scramble_buf; + packet->scramble_buf_len = sizeof(packet->intern_scramble_buf); + + if (packet->header.size < sizeof(buf)) { + /* + Null-terminate the string, so strdup can work even if the packets have a string at the end, + which is not ASCIIZ + */ + buf[packet->header.size] = '\0'; + } + packet->protocol_version = uint1korr(p); p++; BAIL_IF_NO_MORE_DATA; @@ -342,7 +354,7 @@ php_mysqlnd_greet_read(void *_packet, MYSQLND *conn TSRMLS_DC) BAIL_IF_NO_MORE_DATA; memcpy(packet->scramble_buf, p, SCRAMBLE_LENGTH_323); - p+= 8; + p+= SCRAMBLE_LENGTH_323; BAIL_IF_NO_MORE_DATA; /* pad1 */ @@ -362,22 +374,56 @@ php_mysqlnd_greet_read(void *_packet, MYSQLND *conn TSRMLS_DC) BAIL_IF_NO_MORE_DATA; /* pad2 */ + pad_start = p; p+= 13; BAIL_IF_NO_MORE_DATA; if ((size_t) (p - buf) < packet->header.size) { /* scramble_buf is split into two parts */ - memcpy(packet->scramble_buf + SCRAMBLE_LENGTH_323, - p, SCRAMBLE_LENGTH - SCRAMBLE_LENGTH_323); + memcpy(packet->scramble_buf + SCRAMBLE_LENGTH_323, p, SCRAMBLE_LENGTH - SCRAMBLE_LENGTH_323); + p+= SCRAMBLE_LENGTH - SCRAMBLE_LENGTH_323; + p++; /* 0x0 at the end of the scramble and thus last byte in the packet in 5.1 and previous */ } else { packet->pre41 = TRUE; } + /* Is this a 5.5+ server ? */ + if ((size_t) (p - buf) < packet->header.size) { + /* backtrack one byte, the 0x0 at the end of the scramble in 5.1 and previous */ + p--; + + /* Additional 16 bits for server capabilities */ + packet->server_capabilities |= uint2korr(pad_start) << 16; + /* And a length of the server scramble in one byte */ + packet->scramble_buf_len = uint1korr(pad_start + 2); + if (packet->scramble_buf_len > SCRAMBLE_LENGTH) { + /* more data*/ + char * new_scramble_buf = emalloc(packet->scramble_buf_len); + if (!new_scramble_buf) { + goto premature_end; + } + /* copy what we already have */ + memcpy(new_scramble_buf, packet->scramble_buf, SCRAMBLE_LENGTH); + /* add additional scramble data 5.5+ sent us */ + memcpy(new_scramble_buf + SCRAMBLE_LENGTH, p, packet->scramble_buf_len - SCRAMBLE_LENGTH); + p+= (packet->scramble_buf_len - SCRAMBLE_LENGTH); + packet->scramble_buf = new_scramble_buf; + } + } + + if (packet->server_capabilities & CLIENT_PLUGIN_AUTH) { + BAIL_IF_NO_MORE_DATA; + /* The server is 5.5.x and supports authentication plugins */ + packet->auth_protocol = estrdup((char *)p); + p+= strlen(packet->auth_protocol) + 1; /* eat the '\0' */ + } + DBG_INF_FMT("proto=%u server=%s thread_id=%u", packet->protocol_version, packet->server_version, packet->thread_id); - DBG_INF_FMT("server_capabilities=%u charset_no=%u server_status=%i", - packet->server_capabilities, packet->charset_no, packet->server_status); + DBG_INF_FMT("server_capabilities=%u charset_no=%u server_status=%i auth_protocol=%s scramble_length=%u", + packet->server_capabilities, packet->charset_no, packet->server_status, + packet->auth_protocol? packet->auth_protocol:"n/a", packet->scramble_buf_len); DBG_RETURN(PASS); premature_end: @@ -398,6 +444,14 @@ void php_mysqlnd_greet_free_mem(void *_packet, zend_bool stack_allocation TSRMLS efree(p->server_version); p->server_version = NULL; } + if (p->scramble_buf && p->scramble_buf != &p->intern_scramble_buf) { + efree(p->scramble_buf); + p->scramble_buf = NULL; + } + if (p->auth_protocol) { + efree(p->auth_protocol); + p->auth_protocol = NULL; + } if (!stack_allocation) { mnd_pefree(p, p->header.persistent); } @@ -424,7 +478,6 @@ void php_mysqlnd_scramble(zend_uchar * const buffer, const zend_uchar * const sc zend_uchar sha1[SHA1_MAX_LENGTH]; zend_uchar sha2[SHA1_MAX_LENGTH]; - /* Phase 1: hash password */ PHP_SHA1Init(&context); PHP_SHA1Update(&context, password, strlen((char *)password)); diff --git a/ext/mysqlnd/mysqlnd_wireprotocol.h b/ext/mysqlnd/mysqlnd_wireprotocol.h index 39645cb08c..0f3f73e788 100644 --- a/ext/mysqlnd/mysqlnd_wireprotocol.h +++ b/ext/mysqlnd/mysqlnd_wireprotocol.h @@ -70,17 +70,20 @@ typedef struct st_mysqlnd_packet_greet { uint8_t protocol_version; char *server_version; uint32_t thread_id; - zend_uchar scramble_buf[SCRAMBLE_LENGTH]; + zend_uchar intern_scramble_buf[SCRAMBLE_LENGTH]; + zend_uchar * scramble_buf; + size_t scramble_buf_len; /* 1 byte pad */ - uint16_t server_capabilities; + uint32_t server_capabilities; uint8_t charset_no; uint16_t server_status; - /* 13 byte pad*/ + /* 13 byte pad, in 5.5 first 2 bytes are more capabilities followed by 1 byte scramble_length */ zend_bool pre41; /* If error packet, we use these */ char error[MYSQLND_ERRMSG_SIZE+1]; char sqlstate[MYSQLND_SQLSTATE_LENGTH + 1]; unsigned int error_no; + char *auth_protocol; } MYSQLND_PACKET_GREET; @@ -100,6 +103,7 @@ typedef struct st_mysqlnd_packet_auth { const char *password; /* +1 for \0 because of scramble() */ unsigned char *server_scramble_buf; + size_t server_scramble_buf_len; size_t db_len; zend_bool send_auth_data; zend_bool is_change_user_packet; -- 2.40.0