/* $%BEGINLICENSE%$
Copyright (c) 2007, 2009, Oracle and/or its affiliates. All rights reserved.
This program 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 of the
License.
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 St, Fifth Floor, Boston, MA
02110-1301 USA
$%ENDLICENSE%$ */
/**
* @page page-plugin-admin Administration plugin
*
* The admin plugin exposes the internals of the MySQL Proxy on a SQL interface
* to the outside world.
*
* @section plugin-admin-options Configuration
*
* @li @c --admin-address defaults to @c :4041
* @li @c --admin-lua-script specifies the lua script to load that exposes handles the SQL statements
* @li @c --admin-username username
* @li @c --admin-password password
*
* @section plugin-admin-implementation Implementation
*
* The admin plugin handles two SQL queries by default that are used by the mysql commandline client when
* it logins to expose the version string and username. All other queries are returned with an error if they
* are not handled by the Lua script (@c --admin-lua-script).
*
* The script provides a @c read_query() function which returns a result-set in the same way as the proxy
* module does:
*
* @include lib/admin.lua
*
* @section plugin-admin-missing To fix before 1.0
*
* Before MySQL Proxy 1.0 we have to cleanup the admin plugin to:
*
* @li replace the hard-coded username, password by a real credential store @see network_mysqld_admin_plugin_apply_config()
* @li provide a full fleged admin script that exposes all the internal stats @see lib/admin.lua
*
* @section plugin-admin-backends Backends
*
* @b TODO The admin plugin should also be able to change and add the information about the backends
* while the MySQL Proxy is running. It is stored in the @c proxy.global.backends table can be mapped to SQL commands.
*
* @li support for @c SHOW @c CREATE @c TABLE should return @code
* CREATE TABLE backends {
* id INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
* address VARCHAR(...) NOT NULL,
* port INT,
* is_enabled INT NOT NULL, -- 0 or 1, a bool
* }
* @endcode
* @li getting all backends @code
* SELECT * FROM backends;
* SELECT * FROM backends WHERE id = 1;
* @endcode
* @li disable backends (a flag needs to be added to the backend code) @code
* UPDATE backends SET is_enabled = 0;
* @endcode
* @li adding and removing backends like @code
* INSERT INTO backends ( address, port ) VALUES ( "10.0.0.20", 3306 );
* DELETE backends WHERE id = 1;
* @endcode
*
* In a similar way the @c config section of @c proxy.global should be exposed allowing the admin plugin to change the
* configuration at runtime. @see lib/proxy/auto-config.lua
*/
#include
#include
#include
#include
#include "network-mysqld.h"
#include "network-mysqld-proto.h"
#include "network-mysqld-packet.h"
#include "network-mysqld-lua.h"
#include "sys-pedantic.h"
#include "glib-ext.h"
#include "lua-env.h"
#include
#define C(x) x, sizeof(x) -1
#define S(x) x->str, x->len
struct chassis_plugin_config {
gchar *address; /**< listening address of the admin interface */
gchar *lua_script; /**< script to load at the start the connection */
gchar *admin_username; /**< login username */
gchar *admin_password; /**< login password */
network_mysqld_con *listen_con;
};
int network_mysqld_con_handle_stmt(chassis G_GNUC_UNUSED *chas, network_mysqld_con *con, GString *s) {
gsize i, j;
GPtrArray *fields;
GPtrArray *rows;
GPtrArray *row;
switch(s->str[NET_HEADER_SIZE]) {
case COM_QUERY:
fields = NULL;
rows = NULL;
row = NULL;
if (0 == g_ascii_strncasecmp(s->str + NET_HEADER_SIZE + 1, C("select @@version_comment limit 1"))) {
MYSQL_FIELD *field;
fields = network_mysqld_proto_fielddefs_new();
field = network_mysqld_proto_fielddef_new();
field->name = g_strdup("@@version_comment");
field->type = FIELD_TYPE_VAR_STRING;
g_ptr_array_add(fields, field);
rows = g_ptr_array_new();
row = g_ptr_array_new();
g_ptr_array_add(row, g_strdup("MySQL Enterprise Agent"));
g_ptr_array_add(rows, row);
network_mysqld_con_send_resultset(con->client, fields, rows);
} else if (0 == g_ascii_strncasecmp(s->str + NET_HEADER_SIZE + 1, C("select USER()"))) {
MYSQL_FIELD *field;
fields = network_mysqld_proto_fielddefs_new();
field = network_mysqld_proto_fielddef_new();
field->name = g_strdup("USER()");
field->type = FIELD_TYPE_VAR_STRING;
g_ptr_array_add(fields, field);
rows = g_ptr_array_new();
row = g_ptr_array_new();
g_ptr_array_add(row, g_strdup("root"));
g_ptr_array_add(rows, row);
network_mysqld_con_send_resultset(con->client, fields, rows);
} else {
network_mysqld_con_send_error(con->client, C("(admin-server) query not known"));
}
/* clean up */
if (fields) {
network_mysqld_proto_fielddefs_free(fields);
fields = NULL;
}
if (rows) {
for (i = 0; i < rows->len; i++) {
row = rows->pdata[i];
for (j = 0; j < row->len; j++) {
g_free(row->pdata[j]);
}
g_ptr_array_free(row, TRUE);
}
g_ptr_array_free(rows, TRUE);
rows = NULL;
}
break;
case COM_QUIT:
break;
case COM_INIT_DB:
network_mysqld_con_send_ok(con->client);
break;
default:
network_mysqld_con_send_error(con->client, C("unknown COM_*"));
break;
}
return 0;
}
NETWORK_MYSQLD_PLUGIN_PROTO(server_con_init) {
network_mysqld_auth_challenge *challenge;
GString *packet;
challenge = network_mysqld_auth_challenge_new();
challenge->server_version_str = g_strdup("5.0.99-agent-admin");
challenge->server_version = 50099;
challenge->charset = 0x08; /* latin1 */
challenge->capabilities = CLIENT_PROTOCOL_41 | CLIENT_SECURE_CONNECTION | CLIENT_LONG_PASSWORD;
challenge->server_status = SERVER_STATUS_AUTOCOMMIT;
challenge->thread_id = 1;
network_mysqld_auth_challenge_set_challenge(challenge); /* generate a random challenge */
packet = g_string_new(NULL);
network_mysqld_proto_append_auth_challenge(packet, challenge);
con->client->challenge = challenge;
network_mysqld_queue_append(con->client, con->client->send_queue, S(packet));
g_string_free(packet, TRUE);
con->state = CON_STATE_SEND_HANDSHAKE;
g_assert(con->plugin_con_state == NULL);
con->plugin_con_state = network_mysqld_con_lua_new();
return NETWORK_SOCKET_SUCCESS;
}
NETWORK_MYSQLD_PLUGIN_PROTO(server_read_auth) {
network_packet packet;
network_socket *recv_sock, *send_sock;
network_mysqld_auth_response *auth;
GString *excepted_response;
GString *hashed_password;
recv_sock = con->client;
send_sock = con->client;
packet.data = g_queue_peek_head(recv_sock->recv_queue->chunks);
packet.offset = 0;
/* decode the packet */
network_mysqld_proto_skip_network_header(&packet);
auth = network_mysqld_auth_response_new();
if (network_mysqld_proto_get_auth_response(&packet, auth)) {
network_mysqld_auth_response_free(auth);
return NETWORK_SOCKET_ERROR;
}
if (!(auth->capabilities & CLIENT_PROTOCOL_41)) {
/* should use packet-id 0 */
network_mysqld_queue_append(con->client, con->client->send_queue, C("\xff\xd7\x07" "4.0 protocol is not supported"));
network_mysqld_auth_response_free(auth);
return NETWORK_SOCKET_ERROR;
}
con->client->response = auth;
/* check if the password matches */
excepted_response = g_string_new(NULL);
hashed_password = g_string_new(NULL);
if (!strleq(S(con->client->response->username), con->config->admin_username, strlen(con->config->admin_username))) {
network_mysqld_con_send_error_full(send_sock, C("unknown user"), 1045, "28000");
con->state = CON_STATE_SEND_ERROR; /* close the connection after we have sent this packet */
} else if (network_mysqld_proto_password_hash(hashed_password, con->config->admin_password, strlen(con->config->admin_password))) {
} else if (network_mysqld_proto_password_scramble(excepted_response,
S(recv_sock->challenge->challenge),
S(hashed_password))) {
network_mysqld_con_send_error_full(send_sock, C("scrambling failed"), 1045, "28000");
con->state = CON_STATE_SEND_ERROR; /* close the connection after we have sent this packet */
} else if (!g_string_equal(excepted_response, auth->response)) {
network_mysqld_con_send_error_full(send_sock, C("password doesn't match"), 1045, "28000");
con->state = CON_STATE_SEND_ERROR; /* close the connection after we have sent this packet */
} else {
network_mysqld_con_send_ok(send_sock);
con->state = CON_STATE_SEND_AUTH_RESULT;
}
g_string_free(hashed_password, TRUE);
g_string_free(excepted_response, TRUE);
g_string_free(g_queue_pop_tail(recv_sock->recv_queue->chunks), TRUE);
return NETWORK_SOCKET_SUCCESS;
}
static network_mysqld_lua_stmt_ret admin_lua_read_query(network_mysqld_con *con) {
network_mysqld_con_lua_t *st = con->plugin_con_state;
char command = -1;
network_socket *recv_sock = con->client;
GList *chunk = recv_sock->recv_queue->chunks->head;
GString *packet = chunk->data;
if (packet->len < NET_HEADER_SIZE) return PROXY_SEND_QUERY; /* packet too short */
command = packet->str[NET_HEADER_SIZE + 0];
if (COM_QUERY == command) {
/* we need some more data after the COM_QUERY */
if (packet->len < NET_HEADER_SIZE + 2) return PROXY_SEND_QUERY;
/* LOAD DATA INFILE is nasty */
if (packet->len - NET_HEADER_SIZE - 1 >= sizeof("LOAD ") - 1 &&
0 == g_ascii_strncasecmp(packet->str + NET_HEADER_SIZE + 1, C("LOAD "))) return PROXY_SEND_QUERY;
}
network_injection_queue_reset(st->injected.queries);
/* ok, here we go */
#ifdef HAVE_LUA_H
switch(network_mysqld_con_lua_register_callback(con, con->config->lua_script)) {
case REGISTER_CALLBACK_SUCCESS:
break;
case REGISTER_CALLBACK_LOAD_FAILED:
network_mysqld_con_send_error(con->client, C("MySQL Proxy Lua script failed to load. Check the error log."));
con->state = CON_STATE_SEND_ERROR;
return PROXY_SEND_RESULT;
case REGISTER_CALLBACK_EXECUTE_FAILED:
network_mysqld_con_send_error(con->client, C("MySQL Proxy Lua script failed to execute. Check the error log."));
con->state = CON_STATE_SEND_ERROR;
return PROXY_SEND_RESULT;
}
if (st->L) {
lua_State *L = st->L;
network_mysqld_lua_stmt_ret ret = PROXY_NO_DECISION;
g_assert(lua_isfunction(L, -1));
lua_getfenv(L, -1);
g_assert(lua_istable(L, -1));
/**
* reset proxy.response to a empty table
*/
lua_getfield(L, -1, "proxy");
g_assert(lua_istable(L, -1));
lua_newtable(L);
lua_setfield(L, -2, "response");
lua_pop(L, 1);
/**
* get the call back
*/
lua_getfield_literal(L, -1, C("read_query"));
if (lua_isfunction(L, -1)) {
/* pass the packet as parameter */
lua_pushlstring(L, packet->str + NET_HEADER_SIZE, packet->len - NET_HEADER_SIZE);
if (lua_pcall(L, 1, 1, 0) != 0) {
/* hmm, the query failed */
g_critical("(read_query) %s", lua_tostring(L, -1));
lua_pop(L, 2); /* fenv + errmsg */
/* perhaps we should clean up ?*/
return PROXY_SEND_QUERY;
} else {
if (lua_isnumber(L, -1)) {
ret = lua_tonumber(L, -1);
}
lua_pop(L, 1);
}
switch (ret) {
case PROXY_SEND_RESULT:
/* check the proxy.response table for content,
*
*/
if (network_mysqld_con_lua_handle_proxy_response(con, con->config->lua_script)) {
/**
* handling proxy.response failed
*
* send a ERR packet
*/
network_mysqld_con_send_error(con->client, C("(lua) handling proxy.response failed, check error-log"));
}
break;
case PROXY_NO_DECISION:
/**
* PROXY_NO_DECISION and PROXY_SEND_QUERY may pick another backend
*/
break;
case PROXY_SEND_QUERY:
/* send the injected queries
*
* injection_new(..., query);
*
* */
if (st->injected.queries->length) {
ret = PROXY_SEND_INJECTION;
}
break;
default:
break;
}
lua_pop(L, 1); /* fenv */
} else {
lua_pop(L, 2); /* fenv + nil */
}
g_assert(lua_isfunction(L, -1));
if (ret != PROXY_NO_DECISION) {
return ret;
}
}
#endif
return PROXY_NO_DECISION;
}
/**
* gets called after a query has been read
*
* - calls the lua script via network_mysqld_con_handle_proxy_stmt()
*
* @see network_mysqld_con_handle_proxy_stmt
*/
NETWORK_MYSQLD_PLUGIN_PROTO(server_read_query) {
GString *packet;
GList *chunk;
network_socket *recv_sock, *send_sock;
network_mysqld_con_lua_t *st = con->plugin_con_state;
network_mysqld_lua_stmt_ret ret;
send_sock = NULL;
recv_sock = con->client;
st->injected.sent_resultset = 0;
chunk = recv_sock->recv_queue->chunks->head;
if (recv_sock->recv_queue->chunks->length != 1) {
g_message("%s.%d: client-recv-queue-len = %d", __FILE__, __LINE__, recv_sock->recv_queue->chunks->length);
}
packet = chunk->data;
ret = admin_lua_read_query(con);
switch (ret) {
case PROXY_NO_DECISION:
network_mysqld_con_send_error(con->client, C("need a resultset + proxy.PROXY_SEND_RESULT"));
con->state = CON_STATE_SEND_ERROR;
break;
case PROXY_SEND_RESULT:
con->state = CON_STATE_SEND_QUERY_RESULT;
break;
default:
network_mysqld_con_send_error(con->client, C("need a resultset + proxy.PROXY_SEND_RESULT ... got something else"));
con->state = CON_STATE_SEND_ERROR;
break;
}
g_string_free(g_queue_pop_tail(recv_sock->recv_queue->chunks), TRUE);
return NETWORK_SOCKET_SUCCESS;
}
/**
* cleanup the admin specific data on the current connection
*
* @return NETWORK_SOCKET_SUCCESS
*/
NETWORK_MYSQLD_PLUGIN_PROTO(admin_disconnect_client) {
network_mysqld_con_lua_t *st = con->plugin_con_state;
lua_scope *sc = con->srv->sc;
if (st == NULL) return NETWORK_SOCKET_SUCCESS;
#ifdef HAVE_LUA_H
/* remove this cached script from registry */
if (st->L_ref > 0) {
luaL_unref(sc->L, LUA_REGISTRYINDEX, st->L_ref);
}
#endif
network_mysqld_con_lua_free(st);
con->plugin_con_state = NULL;
return NETWORK_SOCKET_SUCCESS;
}
static int network_mysqld_server_connection_init(network_mysqld_con *con) {
con->plugins.con_init = server_con_init;
con->plugins.con_read_auth = server_read_auth;
con->plugins.con_read_query = server_read_query;
con->plugins.con_cleanup = admin_disconnect_client;
return 0;
}
static chassis_plugin_config *network_mysqld_admin_plugin_new(void) {
chassis_plugin_config *config;
config = g_new0(chassis_plugin_config, 1);
return config;
}
static void network_mysqld_admin_plugin_free(chassis_plugin_config *config) {
if (config->listen_con) {
/* the socket will be freed by network_mysqld_free() */
}
if (config->address) {
g_free(config->address);
}
if (config->admin_username) g_free(config->admin_username);
if (config->admin_password) g_free(config->admin_password);
if (config->lua_script) g_free(config->lua_script);
g_free(config);
}
/**
* add the proxy specific options to the cmdline interface
*/
static GOptionEntry * network_mysqld_admin_plugin_get_options(chassis_plugin_config *config) {
guint i;
static GOptionEntry config_entries[] =
{
{ "admin-address", 0, 0, G_OPTION_ARG_STRING, NULL, "listening address:port of the admin-server (default: :4041)", "" },
{ "admin-username", 0, 0, G_OPTION_ARG_STRING, NULL, "username to allow to log in", "" },
{ "admin-password", 0, 0, G_OPTION_ARG_STRING, NULL, "password to allow to log in", "" },
{ NULL, 0, 0, G_OPTION_ARG_NONE, NULL, NULL, NULL }
};
i = 0;
config_entries[i++].arg_data = &(config->address);
config_entries[i++].arg_data = &(config->admin_username);
config_entries[i++].arg_data = &(config->admin_password);
return config_entries;
}
/**
* init the plugin with the parsed config
*/
static int network_mysqld_admin_plugin_apply_config(chassis *chas, chassis_plugin_config *config) {
network_mysqld_con *con;
network_socket *listen_sock;
//if (!config->address) config->address = g_strdup(":4041");
if (!config->address) {
g_critical("%s: Failed to get bind address, please set by --admin-address=",
G_STRLOC);
return -1;
}
if (!config->admin_username) {
g_critical("%s: --admin-username needs to be set",
G_STRLOC);
return -1;
}
if (!config->admin_password) {
g_critical("%s: --admin-password needs to be set",
G_STRLOC);
return -1;
}
if (!config->lua_script) {
config->lua_script = g_strdup_printf("%s/lib/mysql-proxy/lua/admin.lua", chas->base_dir);
}
/**
* create a connection handle for the listen socket
*/
con = network_mysqld_con_new();
network_mysqld_add_connection(chas, con);
con->config = config;
config->listen_con = con;
listen_sock = network_socket_new();
con->server = listen_sock;
/* set the plugin hooks as we want to apply them to the new connections too later */
network_mysqld_server_connection_init(con);
/* FIXME: network_socket_set_address() */
if (0 != network_address_set_address(listen_sock->dst, config->address)) {
return -1;
}
/* FIXME: network_socket_bind() */
if (0 != network_socket_bind(listen_sock)) {
return -1;
}
/**
* call network_mysqld_con_accept() with this connection when we are done
*/
event_set(&(listen_sock->event), listen_sock->fd, EV_READ|EV_PERSIST, network_mysqld_admin_con_accept, con);
event_base_set(chas->event_base, &(listen_sock->event));
event_add(&(listen_sock->event), NULL);
return 0;
}
G_MODULE_EXPORT int plugin_init(chassis_plugin *p) {
p->magic = CHASSIS_PLUGIN_MAGIC;
p->name = g_strdup("admin");
p->version = g_strdup(PACKAGE_VERSION);
p->init = network_mysqld_admin_plugin_new;
p->get_options = network_mysqld_admin_plugin_get_options;
p->apply_config = network_mysqld_admin_plugin_apply_config;
p->destroy = network_mysqld_admin_plugin_free;
return 0;
}