/*
* auth_mysql.c:
* Authenticate users against a MySQL database.
*
* Copyright (c) 2001 Chris Lightfoot. All rights reserved.
*
*/
#ifdef HAVE_CONFIG_H
#include "configuration.h"
#endif /* HAVE_CONFIG_H */
#ifdef AUTH_MYSQL
static const char rcsid[] = "$Id: auth_mysql.c,v 1.54 2003/07/14 23:31:20 chris Exp $";
#include <sys/types.h> /* BSD needs this here, apparently. */
#ifdef HAVE_CRYPT_H
#include <crypt.h>
#endif
#include <grp.h>
#include <pwd.h>
#include <mysql.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <syslog.h>
#include <unistd.h>
#include "auth_mysql.h"
#include "authswitch.h"
#include "config.h"
#include "md5.h"
#include "password.h"
#include "stringmap.h"
#include "util.h"
#include "tokenise.h"
/*
* Default query templates. The returned fields are:
* [0] location of mailbox
* [1] password hash
* [2] unix user
* [3] mailbox type
*/
char *user_pass_query_template =
"SELECT concat(domain.path, '/', popbox.mbox_name), popbox.password_hash, "
"domain.unix_user, 'bsd' "
"FROM popbox, domain "
"WHERE popbox.local_part = '$(local_part)' "
"AND popbox.domain_name = '$(domain)' "
"AND popbox.domain_name = domain.domain_name";
char *apop_query_template =
"SELECT concat(domain.path, '/', popbox.mbox_name), popbox.password_hash, "
"domain.unix_user, 'bsd' "
"FROM popbox, domain "
"WHERE popbox.local_part = '$(local_part)' "
"AND popbox.domain_name = '$(domain)' "
"AND popbox.domain_name = domain.domain_name";
char *onlogin_query_template = NULL;
/* GID used to access mail spool (if any). */
int use_gid;
gid_t mail_gid;
static char *substitute_query_params(const char *temp, const char *user, const char *local_part, const char *domain, const char *clienthost, const char *serverhost);
/*
* Connection to the MySQL server.
*/
MYSQL *mysql = NULL;
tokens mysql_servers;
char mysql_driver_active = 0;
/* get_mysql_server:
* If we are not currently connected to a MySQL server, or if the current MySQL
* server doesn't respond any more, try to connect to all defined MySQL
* servers. If none work, we give up. Return 0 if OK, -1 if we can't connect
* to any server. */
static int get_mysql_server(void) {
int n;
static MYSQL mysql_handle;
char *password;
unsigned int timeout;
if (mysql && mysql_ping(mysql) == 0)
/* The current server is up and running. */
return 0;
if (mysql)
/* The current server doesn't respond anymore. */
mysql_close(mysql);
mysql = mysql_init(&mysql_handle);
if (!mysql) {
log_print(LOG_ERR, _("get_mysql_server: mysql_init: failed"));
return -1;
}
if (!(password = config_get_string("auth-mysql-password")))
password = "";
for (n = 0; n < mysql_servers->num; n++) {
/* To prevent the main process from being blocked for too long, we set
* the timeout when connecting to a remote mysql server to 5 seconds. Of
* course you need to have a network fast enough to allow TCP
* connections to the mysql servers to be started in less than 5
* seconds.... */
timeout = 5;
mysql_options(mysql, MYSQL_OPT_CONNECT_TIMEOUT, (char*)&(timeout));
if (mysql_real_connect(mysql, mysql_servers->toks[n],
config_get_string("auth-mysql-username"),
password,
config_get_string("auth-mysql-database"),
0, NULL, 0) != mysql) {
log_print(LOG_WARNING, "get_mysql_server: server %s: %s", mysql_servers->toks[n], mysql_error(mysql));
continue;
}
log_print(LOG_DEBUG, _("get_mysql_server: now using server %s"), mysql_servers->toks[n]);
return 0;
}
log_print(LOG_ERR, _("get_mysql_server: can't find any working MySQL server; giving up"));
mysql = NULL;
return -1;
}
/* auth_mysql_init:
* Initialise the database connection driver. */
int auth_mysql_init() {
char *hostname = NULL, *localhost = "localhost", *s;
if (!config_get_string("auth-mysql-username")) {
log_print(LOG_ERR, _("auth_mysql_init: no auth-mysql-username directive in config"));
return 0;
}
if (!config_get_string("auth-mysql-password")) {
log_print(LOG_WARNING, _("auth_mysql_init: no auth-mysql-password directive in config; using blank password"));
}
if (!config_get_string("auth-mysql-database")) {
log_print(LOG_ERR, _("auth_mysql_init: no auth-mysql-database directive in config"));
return 0;
}
if ((s = config_get_string("auth-mysql-hostname")))
hostname = s;
else hostname = localhost;
/* Obtain query templates. The special string `none' means `don't use
* any query for this action'. */
if ((s = config_get_string("auth-mysql-pass-query")))
user_pass_query_template = s;
if (strcmp(user_pass_query_template, "none") == 0)
user_pass_query_template = NULL;
if ((s = config_get_string("auth-mysql-apop-query")))
apop_query_template = s;
if (strcmp(apop_query_template, "none") == 0)
apop_query_template = NULL;
/* This is an optional action to put a row into the database after a
* successful login, for POP-before-SMTP relaying. */
if ((s = config_get_string("auth-mysql-onlogin-query")))
onlogin_query_template = s;
/* Obtain gid to use */
if ((s = config_get_string("auth-mysql-mail-group"))) {
if (!parse_gid(s, &mail_gid)) {
log_print(LOG_ERR, _("auth_mysql_init: auth-mysql-mail-group directive `%s' does not make sense"), s);
return 0;
}
use_gid = 1;
}
mysql_servers = tokens_new(hostname, " \t");
if (get_mysql_server() == -1) {
/* No server has been found working. */
tokens_delete(mysql_servers);
log_print(LOG_ERR, _("auth_mysql_init: aborting"));
return 0;
}
mysql_driver_active = 1;
return 1;
}
extern int verbose; /* in main.c */
/* auth_mysql_new_apop:
* Attempt to authenticate a user via APOP, using the template SELECT query in
* the config file or the default defined above otherwise. */
authcontext auth_mysql_new_apop(const char *name, const char *local_part, const char *domain, const char *timestamp, const unsigned char *digest, const char *clienthost /* unused */, const char *serverhost) {
char *query = NULL;
authcontext a = NULL;
char *who;
who = username_string(name, local_part, domain);
if (!mysql_driver_active || !apop_query_template) return NULL;
if (get_mysql_server() == -1) {
log_print(LOG_ERR, _("auth_mysql_new_apop: aborting"));
return NULL;
}
/* Obtain the actual query to use. */
if (!(query = substitute_query_params(apop_query_template, name, local_part, domain, NULL, serverhost)))
goto fail;
if (verbose)
log_print(LOG_DEBUG, _("auth_mysql_new_apop: SQL query: %s"), query);
if (mysql_query(mysql, query) == 0) {
MYSQL_RES *result;
int i;
result = mysql_store_result(mysql);
if (!result) {
log_print(LOG_ERR, "auth_mysql_new_apop: mysql_store_result: %s", mysql_error(mysql));
goto fail;
}
if (mysql_field_count(mysql) != 4) {
log_print(LOG_ERR, _("auth_mysql_new_apop: %d fields returned by query, should be 4: mailbox location, password hash, unix user, mailbox type"), mysql_field_count(mysql));
goto fail;
}
switch (i = mysql_num_rows(result)) {
case 0:
break;
case 1: {
MYSQL_ROW row;
unsigned long *lengths;
struct passwd *pw;
uid_t uid;
row = mysql_fetch_row(result);
/* These are "can't happen" errors */
if (!row || !(lengths = mysql_fetch_lengths(result))) break;
/* Sanity check. Verify that user has UID and password. */
if (!row[2]) {
log_print(LOG_ERR, _("auth_mysql_new_apop: UID for user %s is NULL"), who);
goto fail;
} else if (!row[1]) {
log_print(LOG_ERR, _("auth_mysql_new_apop: password hash for user %s is NULL"), who);
goto fail;
}
/* Actually check the password. */
if (!check_password_apop(who, row[1], timestamp, digest)) {
log_print(LOG_WARNING, _("auth_mysql_new_apop: failed login for %s"), who);
break;
}
/* User was not lying (about her password) */
if (!parse_uid((const char*)row[2], &uid)) {
log_print(LOG_ERR, _("auth_mysql_new_apop: unix user `%s' for %s does not make sense"), row[3], who);
break;
}
pw = getpwuid(uid);
if (!pw) {
log_print(LOG_ERR, "auth_mysql_new_apop: getpwuid(%d): %m", (int)uid);
break;
}
a = authcontext_new(pw->pw_uid, use_gid ? mail_gid : pw->pw_gid, row[3], row[0], pw->pw_dir);
break;
}
default:
log_print(LOG_ERR, _("auth_mysql_new_apop: database inconsistency: query for %s returned %d rows, should be 0 or 1"), name, i);
break;
}
mysql_free_result(result);
} else
log_print(LOG_ERR, "auth_mysql_new_apop: mysql_query: %s", mysql_error(mysql));
fail:
if (query) xfree(query);
return a;
}
/* auth_mysql_new_user_pass:
* Attempt to authenticate a user via USER/PASS, using the template SELECT
* query in the config file or the default defined above otherwise. */
authcontext auth_mysql_new_user_pass(const char *user, const char *local_part, const char *domain, const char *pass, const char *clienthost /* unused */, const char *serverhost) {
char *query = NULL, *who;
authcontext a = NULL;
who = username_string(user, local_part, domain);
if (!mysql_driver_active || !user_pass_query_template) return NULL;
if (get_mysql_server() == -1) {
log_print(LOG_ERR, _("auth_mysql_new_user_pass: aborting"));
return NULL;
}
/* Obtain the actual query to use. */
if (!(query = substitute_query_params(user_pass_query_template, user, local_part, domain, NULL, serverhost)))
goto fail;
if (verbose)
log_print(LOG_DEBUG, _("auth_mysql_new_user_pass: SQL query: %s"), query);
if (mysql_query(mysql, query) == 0) {
MYSQL_RES *result;
int i;
result = mysql_store_result(mysql);
if (!result) {
log_print(LOG_ERR, _("auth_mysql_new_user_pass: mysql_store_result: %s"), mysql_error(mysql));
goto fail;
}
if (mysql_field_count(mysql) != 4) {
log_print(LOG_ERR, _("auth_mysql_new_user_pass: %d fields returned by query, should be 4: mailbox location, password hash, unix user, mailbox type"), mysql_field_count(mysql));
goto fail;
}
switch (i = mysql_num_rows(result)) {
case 0:
break;
case 1: {
MYSQL_ROW row;
unsigned long *lengths;
struct passwd *pw;
uid_t uid;
row = mysql_fetch_row(result);
/* These are "can't happen" errors */
if (!row || !(lengths = mysql_fetch_lengths(result))) break;
/* Sanity check. Verify that user has UID and password. */
if (!row[2]) {
log_print(LOG_ERR, _("auth_mysql_new_user_pass: UID for user %s is NULL"), who);
goto fail;
} else if (!row[1]) {
log_print(LOG_ERR, _("auth_mysql_new_user_pass: password hash for user %s is NULL"), who);
break;
}
/* Verify the password. */
if (!check_password(who, row[1], pass, "{md5}")) {
log_print(LOG_ERR, _("auth_mysql_new_user_pass: %s failed login with wrong password"), who);
break;
}
if (!parse_uid((const char*)row[2], &uid)) {
log_print(LOG_ERR, _("auth_mysql_new_user_pass: unix user `%s' for %s does not make sense"), row[3], who);
break;
}
pw = getpwuid(uid);
if (!pw) {
log_print(LOG_ERR, "auth_mysql_new_user_pass: getpwuid(%d): %m", (int)uid);
break;
}
a = authcontext_new(pw->pw_uid, use_gid ? mail_gid : pw->pw_gid, row[3], row[0], pw->pw_dir);
break;
}
default:
log_print(LOG_ERR, _("auth_mysql_new_user_pass: database inconsistency: query for %s returned %d rows, should be 0 or 1"), who, i);
break;
}
mysql_free_result(result);
} else
log_print(LOG_ERR, "auth_mysql_new_user_pass: mysql_query: %s", mysql_error(mysql));
fail:
xfree(query);
return a;
}
/* auth_mysql_onlogin:
* If specified, perform a query (action) after a successful login. The
* variables substituted in the template are $(local_part), $(domain) and
* $(clienthost), the username, domain, and connecting client host. */
void auth_mysql_onlogin(const authcontext A, const char *clienthost, const char *serverhost) {
char *query;
if (!mysql_driver_active || !onlogin_query_template) return;
if (get_mysql_server() == -1) {
log_print(LOG_ERR, _("auth_mysql_onlogin: aborting"));
return;
}
query = substitute_query_params(onlogin_query_template, A->user, A->local_part, A->domain, clienthost, serverhost);
if (!query)
return;
if (verbose)
log_print(LOG_DEBUG, _("auth_mysql_onlogin: SQL query: %s"), query);
if (mysql_query(mysql, query) == 0) {
MYSQL_RES *result;
/* It's possible that the user put a query in which returned some rows.
* This is bogus but there's not a lot we can do; to avoid leaking
* memory or confusing the database, we obtain and free a result, and
* log a warning. */
result = mysql_store_result(mysql);
if (result) {
log_print(LOG_WARNING, _("auth_mysql_onlogin: supplied SQL query returned %d rows, which is dubious"), mysql_num_rows(result));
mysql_free_result(result);
}
} else
log_print(LOG_ERR, "auth_mysql_onlogin: mysql_query: %s", mysql_error(mysql));
xfree(query);
}
/* auth_mysql_postfork:
* Post-fork cleanup. */
void auth_mysql_postfork() {
mysql = NULL;
mysql_driver_active = 0;
}
/* auth_mysql_close:
* Close the database connection. */
void auth_mysql_close() {
if (mysql) {
mysql_close(mysql);
mysql = NULL;
tokens_delete(mysql_servers);
}
}
/* substitute_query_params
* Given a query template, a localpart and a domain, return a copy of the
* template with the fields filled in. */
static char *substitute_query_params(const char *template, const char *user, const char *local_part, const char *domain, const char *clienthost, const char *serverhost) {
char *query, *u = NULL, *l = NULL, *d = NULL, *c = NULL, *s = NULL;
struct sverr err;
/* Form escaped copies of the user and domain. */
u = xmalloc(strlen(user) * 2 + 1);
mysql_escape_string(u, user, strlen(user));
if (local_part) {
l = xmalloc(strlen(local_part) * 2 + 1);
mysql_escape_string(l, local_part, strlen(local_part));
}
if (domain) {
d = xmalloc(strlen(domain) * 2 + 1);
mysql_escape_string(d, domain, strlen(domain));
}
if (clienthost) {
c = xmalloc(strlen(clienthost) * 2 + 1);
mysql_escape_string(c, clienthost, strlen(clienthost));
}
if (serverhost) {
s = xmalloc(strlen(serverhost) * 2 + 1);
mysql_escape_string(s, serverhost, strlen(serverhost));
}
/* Do the substitution. */
query = substitute_variables(template, &err, 5, "user", u, "local_part", l, "domain", d, "clienthost", c, "serverhost", s);
if (!query && err.code != sv_nullvalue)
log_print(LOG_ERR, _("substitute_query_params: %s near `%.16s'"), err.msg, template + err.offset);
xfree(s);
xfree(c);
xfree(u);
xfree(l);
xfree(d);
return query;
}
#endif /* AUTH_MYSQL */
syntax highlighted by Code2HTML, v. 0.9.1