
Added a lot of logging to error, trace and message logs which should help the user to handle errors which can't be automatically resolved, like attempt to use nonexisting database.
584 lines
16 KiB
C
584 lines
16 KiB
C
/*
|
|
* This file is distributed as part of the SkySQL Gateway. It 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.
|
|
*
|
|
* 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 Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
*
|
|
* Copyright SkySQL Ab 2013
|
|
*/
|
|
|
|
/**
|
|
* @file dbusers.c - Loading MySQL users from a MySQL backend server, this needs libmysqlclient.so and header files
|
|
*
|
|
* @verbatim
|
|
* Revision History
|
|
*
|
|
* Date Who Description
|
|
* 24/06/2013 Massimiliano Pinto Initial implementation
|
|
* 08/08/2013 Massimiliano Pinto Fixed bug for invalid memory access in row[1]+1 when row[1] is ""
|
|
* 06/02/2014 Massimiliano Pinto Mysql user root selected based on configuration flag
|
|
* 26/02/2014 Massimiliano Pinto Addd: replace_mysql_users() routine may replace users' table based on a checksum
|
|
* 28/02/2014 Massimiliano Pinto Added Mysql user@host authentication
|
|
*
|
|
* @endverbatim
|
|
*/
|
|
|
|
#include <stdio.h>
|
|
#include <mysql.h>
|
|
|
|
#include <dcb.h>
|
|
#include <service.h>
|
|
#include <users.h>
|
|
#include <dbusers.h>
|
|
#include <skygw_utils.h>
|
|
#include <log_manager.h>
|
|
#include <secrets.h>
|
|
#include <mysql_client_server_protocol.h>
|
|
|
|
#define USERS_QUERY_NO_ROOT " AND user NOT IN ('root')"
|
|
#define LOAD_MYSQL_USERS_QUERY "SELECT user, host, password, concat(user,host,password) AS userdata FROM mysql.user WHERE user IS NOT NULL AND user <> ''"
|
|
#define MYSQL_USERS_COUNT "SELECT COUNT(1) AS nusers FROM mysql.user"
|
|
|
|
extern int lm_enabled_logfiles_bitmask;
|
|
|
|
static int getUsers(SERVICE *service, struct users *users);
|
|
static int uh_cmpfun( void* v1, void* v2);
|
|
static void *uh_keydup(void* key);
|
|
static void uh_keyfree( void* key);
|
|
static int uh_hfun( void* key);
|
|
char *mysql_users_fetch(USERS *users, MYSQL_USER_HOST *key);
|
|
char *mysql_format_user_entry(void *data);
|
|
|
|
/**
|
|
* Load the user/passwd form mysql.user table into the service users' hashtable
|
|
* environment.
|
|
*
|
|
* @param service The current service
|
|
* @return -1 on any error or the number of users inserted (0 means no users at all)
|
|
*/
|
|
int
|
|
load_mysql_users(SERVICE *service)
|
|
{
|
|
return getUsers(service, service->users);
|
|
}
|
|
|
|
/**
|
|
* Reload the user/passwd form mysql.user table into the service users' hashtable
|
|
* environment.
|
|
*
|
|
* @param service The current service
|
|
* @return -1 on any error or the number of users inserted (0 means no users at all)
|
|
*/
|
|
int
|
|
reload_mysql_users(SERVICE *service)
|
|
{
|
|
int i;
|
|
struct users *newusers, *oldusers;
|
|
|
|
if ((newusers = mysql_users_alloc()) == NULL)
|
|
return 0;
|
|
i = getUsers(service, newusers);
|
|
spinlock_acquire(&service->spin);
|
|
oldusers = service->users;
|
|
service->users = newusers;
|
|
spinlock_release(&service->spin);
|
|
users_free(oldusers);
|
|
|
|
return i;
|
|
}
|
|
|
|
/**
|
|
* Replace the user/passwd form mysql.user table into the service users' hashtable
|
|
* environment.
|
|
* The replacement is succesful only if the users' table checksums differ
|
|
*
|
|
* @param service The current service
|
|
* @return -1 on any error or the number of users inserted (0 means no users at all)
|
|
*/
|
|
int
|
|
replace_mysql_users(SERVICE *service)
|
|
{
|
|
int i;
|
|
struct users *newusers, *oldusers;
|
|
|
|
if ((newusers = mysql_users_alloc()) == NULL)
|
|
return -1;
|
|
|
|
i = getUsers(service, newusers);
|
|
|
|
if (i <= 0)
|
|
return i;
|
|
|
|
spinlock_acquire(&service->spin);
|
|
oldusers = service->users;
|
|
|
|
/* digest compare */
|
|
if (memcmp(oldusers->cksum, newusers->cksum, SHA_DIGEST_LENGTH) == 0) {
|
|
/* same data, nothing to do */
|
|
LOGIF(LD, (skygw_log_write_flush(
|
|
LOGFILE_DEBUG,
|
|
"%lu [replace_mysql_users] users' tables not switched, checksum is the same",
|
|
pthread_self())));
|
|
/* free the new table */
|
|
users_free(newusers);
|
|
i = 0;
|
|
} else {
|
|
/* replace the service with effective new data */
|
|
LOGIF(LD, (skygw_log_write_flush(
|
|
LOGFILE_DEBUG,
|
|
"%lu [replace_mysql_users] users' tables replaced, checksum differs",
|
|
pthread_self())));
|
|
service->users = newusers;
|
|
}
|
|
|
|
spinlock_release(&service->spin);
|
|
|
|
if (i)
|
|
users_free(oldusers);
|
|
|
|
return i;
|
|
}
|
|
|
|
/**
|
|
* Load the user/passwd form mysql.user table into the service users' hashtable
|
|
* environment.
|
|
*
|
|
* @param service The current service
|
|
* @param users The users table into which to load the users
|
|
* @return -1 on any error or the number of users inserted (0 means no users at all)
|
|
*/
|
|
static int
|
|
getUsers(SERVICE *service, struct users *users)
|
|
{
|
|
MYSQL *con = NULL;
|
|
MYSQL_ROW row;
|
|
MYSQL_RES *result = NULL;
|
|
int num_fields = 0;
|
|
char *service_user = NULL;
|
|
char *service_passwd = NULL;
|
|
char *dpwd;
|
|
int total_users = 0;
|
|
SERVER *server;
|
|
char *users_query;
|
|
unsigned char hash[SHA_DIGEST_LENGTH]="";
|
|
char *users_data = NULL;
|
|
int nusers = 0;
|
|
int users_data_row_len = MYSQL_USER_MAXLEN + MYSQL_HOST_MAXLEN + MYSQL_PASSWORD_LEN;
|
|
struct sockaddr_in serv_addr;
|
|
MYSQL_USER_HOST key;
|
|
|
|
/* enable_root for MySQL protocol module means load the root user credentials from backend databases */
|
|
if(service->enable_root) {
|
|
users_query = LOAD_MYSQL_USERS_QUERY " ORDER BY HOST DESC";
|
|
} else {
|
|
users_query = LOAD_MYSQL_USERS_QUERY USERS_QUERY_NO_ROOT " ORDER BY HOST DESC";
|
|
}
|
|
|
|
serviceGetUser(service, &service_user, &service_passwd);
|
|
if (service_user == NULL || service_passwd == NULL)
|
|
return -1;
|
|
|
|
/** multi-thread environment requires that thread init succeeds. */
|
|
if (mysql_thread_init()) {
|
|
LOGIF(LE, (skygw_log_write_flush(
|
|
LOGFILE_ERROR,
|
|
"Error : mysql_thread_init failed.")));
|
|
return -1;
|
|
}
|
|
|
|
con = mysql_init(NULL);
|
|
|
|
if (con == NULL) {
|
|
LOGIF(LE, (skygw_log_write_flush(
|
|
LOGFILE_ERROR,
|
|
"Error : mysql_init: %s",
|
|
mysql_error(con))));
|
|
return -1;
|
|
}
|
|
|
|
if (mysql_options(con, MYSQL_OPT_USE_REMOTE_CONNECTION, NULL)) {
|
|
LOGIF(LE, (skygw_log_write_flush(
|
|
LOGFILE_ERROR,
|
|
"Error : failed to set external connection. "
|
|
"It is needed for backend server connections. "
|
|
"Exiting.")));
|
|
return -1;
|
|
}
|
|
/**
|
|
* Attempt to connect to one of the databases database or until we run
|
|
* out of databases
|
|
* to try
|
|
*/
|
|
server = service->databases;
|
|
dpwd = decryptPassword(service_passwd);
|
|
while (server != NULL && (mysql_real_connect(con,
|
|
server->name,
|
|
service_user,
|
|
dpwd,
|
|
NULL,
|
|
server->port,
|
|
NULL,
|
|
0) == NULL))
|
|
{
|
|
server = server->nextdb;
|
|
}
|
|
free(dpwd);
|
|
|
|
if (server == NULL)
|
|
{
|
|
LOGIF(LE, (skygw_log_write_flush(
|
|
LOGFILE_ERROR,
|
|
"Error : Unable to get user data from backend database "
|
|
"for service %s. Missing server information.",
|
|
service->name)));
|
|
mysql_close(con);
|
|
return -1;
|
|
}
|
|
|
|
if (mysql_query(con, MYSQL_USERS_COUNT)) {
|
|
LOGIF(LE, (skygw_log_write_flush(
|
|
LOGFILE_ERROR,
|
|
"Error : Loading users for service %s encountered "
|
|
"error: %s.",
|
|
service->name,
|
|
mysql_error(con))));
|
|
mysql_close(con);
|
|
return -1;
|
|
}
|
|
result = mysql_store_result(con);
|
|
|
|
if (result == NULL) {
|
|
LOGIF(LE, (skygw_log_write_flush(
|
|
LOGFILE_ERROR,
|
|
"Error : Loading users for service %s encountered "
|
|
"error: %s.",
|
|
service->name,
|
|
mysql_error(con))));
|
|
mysql_close(con);
|
|
return -1;
|
|
}
|
|
num_fields = mysql_num_fields(result);
|
|
row = mysql_fetch_row(result);
|
|
|
|
nusers = atoi(row[0]);
|
|
|
|
mysql_free_result(result);
|
|
|
|
if (!nusers) {
|
|
LOGIF(LE, (skygw_log_write_flush(
|
|
LOGFILE_ERROR,
|
|
"Error : Counting users for service %s returned 0",
|
|
service->name)));
|
|
mysql_close(con);
|
|
return -1;
|
|
}
|
|
|
|
if (mysql_query(con, users_query)) {
|
|
LOGIF(LE, (skygw_log_write_flush(
|
|
LOGFILE_ERROR,
|
|
"Error : Loading users for service %s encountered "
|
|
"error: %s.",
|
|
service->name,
|
|
mysql_error(con))));
|
|
mysql_close(con);
|
|
return -1;
|
|
}
|
|
|
|
result = mysql_store_result(con);
|
|
|
|
if (result == NULL) {
|
|
LOGIF(LE, (skygw_log_write_flush(
|
|
LOGFILE_ERROR,
|
|
"Error : Loading users for service %s encountered "
|
|
"error: %s.",
|
|
service->name,
|
|
mysql_error(con))));
|
|
mysql_close(con);
|
|
return -1;
|
|
}
|
|
num_fields = mysql_num_fields(result);
|
|
|
|
users_data = (char *)calloc(nusers, (users_data_row_len * sizeof(char)) + 1);
|
|
|
|
if(users_data == NULL)
|
|
return -1;
|
|
|
|
while ((row = mysql_fetch_row(result))) {
|
|
/**
|
|
* Four fields should be returned.
|
|
* user and passwd+1 (escaping the first byte that is '*') are
|
|
* added to hashtable.
|
|
*/
|
|
|
|
char ret_ip[INET_ADDRSTRLEN + 1]="";
|
|
const char *rc;
|
|
|
|
/* prepare the user@host data struct */
|
|
memset(&serv_addr, 0, sizeof(serv_addr));
|
|
memset(&key, 0, sizeof(key));
|
|
|
|
/* if host == '%', 0 is passed */
|
|
if (setipaddress(&serv_addr.sin_addr, strcmp(row[1], "%") ? row[1] : "0.0.0.0")) {
|
|
|
|
key.user = strdup(row[0]);
|
|
|
|
if(key.user == NULL) {
|
|
LOGIF(LE, (skygw_log_write_flush(
|
|
LOGFILE_ERROR,
|
|
"%lu [getUsers()] strdup() failed for user %s",
|
|
pthread_self(),
|
|
row[0])));
|
|
|
|
continue;
|
|
}
|
|
|
|
memcpy(&key.ipv4, &serv_addr, sizeof(serv_addr));
|
|
|
|
rc = inet_ntop(AF_INET, &(serv_addr).sin_addr, ret_ip, INET_ADDRSTRLEN);
|
|
|
|
/* add user@host as key and passwd as value in the MySQL users hash table */
|
|
if (mysql_users_add(users, &key, strlen(row[2]) ? row[2]+1 : row[2])) {
|
|
LOGIF(LD, (skygw_log_write_flush(
|
|
LOGFILE_DEBUG,
|
|
"%lu [mysql_users_add()] Added user %s@%s(%s)",
|
|
pthread_self(),
|
|
row[0],
|
|
row[1],
|
|
rc == NULL ? "NULL" : ret_ip)));
|
|
|
|
/* Append data in the memory area for SHA1 digest */
|
|
strncat(users_data, row[3], users_data_row_len);
|
|
|
|
total_users++;
|
|
} else {
|
|
LOGIF(LE, (skygw_log_write_flush(
|
|
LOGFILE_ERROR,
|
|
"%lu [mysql_users_add()] Failed adding user %s@%s(%s)",
|
|
pthread_self(),
|
|
row[0],
|
|
row[1],
|
|
rc == NULL ? "NULL" : ret_ip)));
|
|
|
|
continue;
|
|
}
|
|
|
|
} else {
|
|
/* setipaddress() failed, skip user add and log this*/
|
|
LOGIF(LE, (skygw_log_write_flush(
|
|
LOGFILE_ERROR,
|
|
"%lu [getUsers()] setipaddress failed: user %s@%s not added",
|
|
pthread_self(),
|
|
row[0],
|
|
row[1])));
|
|
}
|
|
}
|
|
|
|
/* compute SHA1 digest for users' data */
|
|
SHA1((const unsigned char *) users_data, strlen(users_data), hash);
|
|
|
|
memcpy(users->cksum, hash, SHA_DIGEST_LENGTH);
|
|
|
|
free(users_data);
|
|
|
|
mysql_free_result(result);
|
|
mysql_close(con);
|
|
mysql_thread_end();
|
|
|
|
return total_users;
|
|
}
|
|
|
|
/**
|
|
* Allocate a new MySQL users table for mysql specific users@host as key
|
|
*
|
|
* @return The users table
|
|
*/
|
|
USERS *
|
|
mysql_users_alloc()
|
|
{
|
|
USERS *rval;
|
|
|
|
if ((rval = calloc(1, sizeof(USERS))) == NULL)
|
|
return NULL;
|
|
|
|
if ((rval->data = hashtable_alloc(USERS_HASHTABLE_DEFAULT_SIZE, uh_hfun, uh_cmpfun)) == NULL) {
|
|
free(rval);
|
|
return NULL;
|
|
}
|
|
|
|
/* set the MySQL user@host print routine for the debug interface */
|
|
rval->usersCustomUserFormat = mysql_format_user_entry;
|
|
|
|
/* the key is handled by uh_keydup/uh_keyfree.
|
|
* the value is a (char *): it's handled by strdup/free
|
|
*/
|
|
hashtable_memory_fns(rval->data, (HASHMEMORYFN)uh_keydup, (HASHMEMORYFN) strdup, (HASHMEMORYFN)uh_keyfree, (HASHMEMORYFN)free);
|
|
|
|
return rval;
|
|
}
|
|
|
|
/**
|
|
* Add a new MySQL user to the user table. The user name must be unique
|
|
*
|
|
* @param users The users table
|
|
* @param user The user name
|
|
* @param auth The authentication data
|
|
* @return The number of users added to the table
|
|
*/
|
|
int
|
|
mysql_users_add(USERS *users, MYSQL_USER_HOST *key, char *auth)
|
|
{
|
|
int add;
|
|
|
|
if (key == NULL || key->user == NULL)
|
|
return 0;
|
|
|
|
atomic_add(&users->stats.n_adds, 1);
|
|
add = hashtable_add(users->data, key, auth);
|
|
atomic_add(&users->stats.n_entries, add);
|
|
|
|
return add;
|
|
}
|
|
|
|
/**
|
|
* Fetch the authentication data for a particular user from the users table
|
|
*
|
|
* @param users The MySQL users table
|
|
* @param key The key with user@host
|
|
* @return The authentication data or NULL on error
|
|
*/
|
|
char *mysql_users_fetch(USERS *users, MYSQL_USER_HOST *key) {
|
|
if (key == NULL)
|
|
return NULL;
|
|
atomic_add(&users->stats.n_fetches, 1);
|
|
return hashtable_fetch(users->data, key);
|
|
}
|
|
|
|
/**
|
|
* The hash function we use for storing MySQL users as: users@hosts.
|
|
* Currently only IPv4 addresses are supported
|
|
*
|
|
* @param key The key value, i.e. username@host (IPv4)
|
|
* @return The hash key
|
|
*/
|
|
|
|
static int uh_hfun( void* key) {
|
|
MYSQL_USER_HOST *hu = (MYSQL_USER_HOST *) key;
|
|
|
|
if (key == NULL || hu == NULL || hu->user == NULL) {
|
|
return 0;
|
|
} else {
|
|
return (*hu->user + *(hu->user + 1) + (unsigned int) (hu->ipv4.sin_addr.s_addr & 0xFF000000 / (256 * 256 * 256)));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The compare function we use for compare MySQL users as: users@hosts.
|
|
* Currently only IPv4 addresses are supported
|
|
*
|
|
* @param key1 The key value, i.e. username@host (IPv4)
|
|
* @param key1 The key value, i.e. username@host (IPv4)
|
|
* @return The compare value
|
|
*/
|
|
|
|
static int uh_cmpfun( void* v1, void* v2) {
|
|
MYSQL_USER_HOST *hu1 = (MYSQL_USER_HOST *) v1;
|
|
MYSQL_USER_HOST *hu2 = (MYSQL_USER_HOST *) v2;
|
|
|
|
if (v1 == NULL || v2 == NULL || hu1 == NULL || hu2 == NULL || hu1->user == NULL || hu2->user == NULL)
|
|
return 0;
|
|
|
|
if (strcmp(hu1->user, hu2->user) == 0 && (hu1->ipv4.sin_addr.s_addr == hu2->ipv4.sin_addr.s_addr)) {
|
|
return 0;
|
|
} else {
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
/**
|
|
*The key dup function we use for duplicate the users@hosts.
|
|
*
|
|
* @param key The key value, i.e. username@host ip4/ip6 data
|
|
*/
|
|
|
|
static void *uh_keydup(void* key) {
|
|
MYSQL_USER_HOST *rval = (MYSQL_USER_HOST *) calloc(1, sizeof(MYSQL_USER_HOST));
|
|
MYSQL_USER_HOST *current_key = (MYSQL_USER_HOST *)key;
|
|
|
|
if (key == NULL || rval == NULL || current_key == NULL || current_key->user == NULL) {
|
|
return NULL;
|
|
}
|
|
|
|
rval->user = strdup(current_key->user);
|
|
|
|
if (rval->user == NULL)
|
|
return NULL;
|
|
|
|
memcpy(&rval->ipv4, ¤t_key->ipv4, sizeof(struct sockaddr_in));
|
|
|
|
return (void *) rval;
|
|
}
|
|
|
|
/**
|
|
* The key free function we use for freeing the users@hosts data
|
|
*
|
|
* @param key The key value, i.e. username@host ip4 data
|
|
*/
|
|
static void uh_keyfree( void* key) {
|
|
MYSQL_USER_HOST *current_key = (MYSQL_USER_HOST *)key;
|
|
|
|
if (key == NULL)
|
|
return;
|
|
|
|
if (current_key && current_key->user)
|
|
free(current_key->user);
|
|
|
|
free(key);
|
|
}
|
|
|
|
/**
|
|
* Format the mysql user as user@host
|
|
* The returned memory must be freed by the caller
|
|
*
|
|
* @param data Input data
|
|
* @return the MySQL user@host
|
|
*/
|
|
char *mysql_format_user_entry(void *data)
|
|
{
|
|
MYSQL_USER_HOST *entry;
|
|
char *mysql_user;
|
|
/* the returned user string is "USER" + "@" + "HOST" + '\0' */
|
|
int mysql_user_len = MYSQL_USER_MAXLEN + 1 + INET_ADDRSTRLEN + 1;
|
|
|
|
if (data == NULL)
|
|
return NULL;
|
|
|
|
entry = (MYSQL_USER_HOST *) data;
|
|
|
|
if (entry == NULL)
|
|
return NULL;
|
|
|
|
mysql_user = (char *) calloc(mysql_user_len, sizeof(char));
|
|
|
|
if (mysql_user == NULL)
|
|
return NULL;
|
|
|
|
if (entry->ipv4.sin_addr.s_addr == INADDR_ANY) {
|
|
snprintf(mysql_user, mysql_user_len, "%s@%%", entry->user);
|
|
} else {
|
|
strncpy(mysql_user, entry->user, MYSQL_USER_MAXLEN);
|
|
strcat(mysql_user, "@");
|
|
inet_ntop(AF_INET, &(entry->ipv4).sin_addr, mysql_user+strlen(mysql_user), INET_ADDRSTRLEN);
|
|
}
|
|
|
|
return mysql_user;
|
|
}
|