lighttpd1.4/src/mod_authn_mysql.c

515 lines
18 KiB
C

#include "first.h"
/* mod_authn_mysql
*
* KNOWN LIMITATIONS:
* - no mechanism provided to configure SSL connection to a remote MySQL db
*
* FUTURE POTENTIAL PERFORMANCE ENHANCEMENTS:
* - database response is not cached
* TODO: db response caching (for limited time) to reduce load on db
* (only cache successful logins to prevent cache bloat?)
* (or limit number of entries (size) of cache)
* (maybe have negative cache (limited size) of names not found in database)
* - database query is synchronous and blocks waiting for response
* TODO: https://mariadb.com/kb/en/mariadb/using-the-non-blocking-library/
* - opens and closes connection to MySQL db for each request (inefficient)
* (fixed) one-element cache for persistent connection open to last used db
* TODO: db connection pool (if asynchronous requests)
*/
#include <mysql.h>
#include "base.h"
#include "http_auth.h"
#include "log.h"
#include "plugin.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#if defined(HAVE_CRYPT_R) || defined(HAVE_CRYPT)
#include <unistd.h> /* crypt() */
#endif
#ifdef HAVE_CRYPT_H
#include <crypt.h>
#endif
#include "sys-crypto-md.h"
typedef struct {
int auth_mysql_port;
const char *auth_mysql_host;
const char *auth_mysql_user;
const char *auth_mysql_pass;
const char *auth_mysql_db;
const char *auth_mysql_socket;
const char *auth_mysql_users_table;
const char *auth_mysql_col_user;
const char *auth_mysql_col_pass;
const char *auth_mysql_col_realm;
log_error_st *errh;
} plugin_config;
typedef struct {
PLUGIN_DATA;
plugin_config defaults;
plugin_config conf;
MYSQL *mysql_conn;
const char *mysql_conn_host;
const char *mysql_conn_user;
const char *mysql_conn_pass;
const char *mysql_conn_db;
int mysql_conn_port;
} plugin_data;
static void mod_authn_mysql_sock_close(void *p_d) {
plugin_data * const p = p_d;
if (NULL != p->mysql_conn) {
mysql_close(p->mysql_conn);
p->mysql_conn = NULL;
}
}
static MYSQL * mod_authn_mysql_sock_connect(plugin_data *p) {
plugin_config * const pconf = &p->conf;
if (NULL != p->mysql_conn) {
/* reuse open db connection if same ptrs to host user pass db port */
if ( p->mysql_conn_host == pconf->auth_mysql_host
&& p->mysql_conn_user == pconf->auth_mysql_user
&& p->mysql_conn_pass == pconf->auth_mysql_pass
&& p->mysql_conn_db == pconf->auth_mysql_db
&& p->mysql_conn_port == pconf->auth_mysql_port) {
return p->mysql_conn;
}
mod_authn_mysql_sock_close(p);
}
/* !! mysql_init() is not thread safe !! (see MySQL doc) */
p->mysql_conn = mysql_init(NULL);
if (mysql_real_connect(p->mysql_conn,
pconf->auth_mysql_host,
pconf->auth_mysql_user,
pconf->auth_mysql_pass,
pconf->auth_mysql_db,
pconf->auth_mysql_port,
(pconf->auth_mysql_socket && *pconf->auth_mysql_socket)
? pconf->auth_mysql_socket
: NULL,
CLIENT_IGNORE_SIGPIPE)) {
/* (copy ptrs to plugin data (has lifetime until server shutdown)) */
p->mysql_conn_host = pconf->auth_mysql_host;
p->mysql_conn_user = pconf->auth_mysql_user;
p->mysql_conn_pass = pconf->auth_mysql_pass;
p->mysql_conn_db = pconf->auth_mysql_db;
p->mysql_conn_port = pconf->auth_mysql_port;
return p->mysql_conn;
}
else {
/*(note: any of these params might be NULL)*/
log_error(pconf->errh, __FILE__, __LINE__,
"opening connection to mysql: %s user: %s db: %s failed: %s",
pconf->auth_mysql_host ? pconf->auth_mysql_host : "",
pconf->auth_mysql_user ? pconf->auth_mysql_user : "",
/*"pass:",*//*(omit pass from logs)*/
/*p->conf.auth_mysql_pass ? p->conf.auth_mysql_pass : "",*/
pconf->auth_mysql_db ? pconf->auth_mysql_db : "",
mysql_error(p->mysql_conn));
mod_authn_mysql_sock_close(p);
return NULL;
}
}
static MYSQL * mod_authn_mysql_sock_acquire(plugin_data *p) {
return mod_authn_mysql_sock_connect(p);
}
static void mod_authn_mysql_sock_release(plugin_data *p) {
UNUSED(p);
/*(empty; leave db connection open)*/
/* Note: mod_authn_mysql_result() calls mod_authn_mysql_sock_error()
* on error, so take that into account if making changes here.
* Must check if (NULL == p->mysql_conn) */
}
__attribute_cold__
static void mod_authn_mysql_sock_error(plugin_data *p) {
mod_authn_mysql_sock_close(p);
}
static handler_t mod_authn_mysql_basic(request_st *r, void *p_d, const http_auth_require_t *require, const buffer *username, const char *pw);
static handler_t mod_authn_mysql_digest(request_st *r, void *p_d, http_auth_info_t *dig);
INIT_FUNC(mod_authn_mysql_init) {
static http_auth_backend_t http_auth_backend_mysql =
{ "mysql", mod_authn_mysql_basic, mod_authn_mysql_digest, NULL };
plugin_data *p = calloc(1, sizeof(*p));
/* register http_auth_backend_mysql */
http_auth_backend_mysql.p_d = p;
http_auth_backend_set(&http_auth_backend_mysql);
return p;
}
static void mod_authn_mysql_merge_config_cpv(plugin_config * const pconf, const config_plugin_value_t * const cpv) {
switch (cpv->k_id) { /* index into static config_plugin_keys_t cpk[] */
case 0: /* auth.backend.mysql.host */
pconf->auth_mysql_host = cpv->v.b->ptr;
break;
case 1: /* auth.backend.mysql.user */
pconf->auth_mysql_user = cpv->v.b->ptr;
break;
case 2: /* auth.backend.mysql.pass */
pconf->auth_mysql_pass = cpv->v.b->ptr;
break;
case 3: /* auth.backend.mysql.db */
pconf->auth_mysql_db = cpv->v.b->ptr;
break;
case 4: /* auth.backend.mysql.port */
pconf->auth_mysql_port = (int)cpv->v.shrt;
break;
case 5: /* auth.backend.mysql.socket */
pconf->auth_mysql_socket = cpv->v.b->ptr;
break;
case 6: /* auth.backend.mysql.users_table */
pconf->auth_mysql_users_table = cpv->v.b->ptr;
break;
case 7: /* auth.backend.mysql.col_user */
pconf->auth_mysql_col_user = cpv->v.b->ptr;
break;
case 8: /* auth.backend.mysql.col_pass */
pconf->auth_mysql_col_pass = cpv->v.b->ptr;
break;
case 9: /* auth.backend.mysql.col_realm */
pconf->auth_mysql_col_realm = cpv->v.b->ptr;
break;
default:/* should not happen */
return;
}
}
static void mod_authn_mysql_merge_config(plugin_config * const pconf, const config_plugin_value_t *cpv) {
do {
mod_authn_mysql_merge_config_cpv(pconf, cpv);
} while ((++cpv)->k_id != -1);
}
static void mod_authn_mysql_patch_config(request_st * const r, plugin_data * const p) {
memcpy(&p->conf, &p->defaults, sizeof(plugin_config));
for (int i = 1, used = p->nconfig; i < used; ++i) {
if (config_check_cond(r, (uint32_t)p->cvlist[i].k_id))
mod_authn_mysql_merge_config(&p->conf,
p->cvlist + p->cvlist[i].v.u2[0]);
}
}
SETDEFAULTS_FUNC(mod_authn_mysql_set_defaults) {
static const config_plugin_keys_t cpk[] = {
{ CONST_STR_LEN("auth.backend.mysql.host"),
T_CONFIG_STRING,
T_CONFIG_SCOPE_CONNECTION }
,{ CONST_STR_LEN("auth.backend.mysql.user"),
T_CONFIG_STRING,
T_CONFIG_SCOPE_CONNECTION }
,{ CONST_STR_LEN("auth.backend.mysql.pass"),
T_CONFIG_STRING,
T_CONFIG_SCOPE_CONNECTION }
,{ CONST_STR_LEN("auth.backend.mysql.db"),
T_CONFIG_STRING,
T_CONFIG_SCOPE_CONNECTION }
,{ CONST_STR_LEN("auth.backend.mysql.port"),
T_CONFIG_SHORT,
T_CONFIG_SCOPE_CONNECTION }
,{ CONST_STR_LEN("auth.backend.mysql.socket"),
T_CONFIG_STRING,
T_CONFIG_SCOPE_CONNECTION }
,{ CONST_STR_LEN("auth.backend.mysql.users_table"),
T_CONFIG_STRING,
T_CONFIG_SCOPE_CONNECTION }
,{ CONST_STR_LEN("auth.backend.mysql.col_user"),
T_CONFIG_STRING,
T_CONFIG_SCOPE_CONNECTION }
,{ CONST_STR_LEN("auth.backend.mysql.col_pass"),
T_CONFIG_STRING,
T_CONFIG_SCOPE_CONNECTION }
,{ CONST_STR_LEN("auth.backend.mysql.col_realm"),
T_CONFIG_STRING,
T_CONFIG_SCOPE_CONNECTION }
,{ NULL, 0,
T_CONFIG_UNSET,
T_CONFIG_SCOPE_UNSET }
};
plugin_data * const p = p_d;
if (!config_plugin_values_init(srv, p, cpk, "mod_authn_mysql"))
return HANDLER_ERROR;
/* process and validate config directives
* (init i to 0 if global context; to 1 to skip empty global context) */
for (int i = !p->cvlist[0].v.u2[1]; i < p->nconfig; ++i) {
config_plugin_value_t *cpv = p->cvlist + p->cvlist[i].v.u2[0];
for (; -1 != cpv->k_id; ++cpv) {
switch (cpv->k_id) {
case 0: /* auth.backend.mysql.host */
case 1: /* auth.backend.mysql.user */
case 2: /* auth.backend.mysql.pass */
case 3: /* auth.backend.mysql.db */
case 4: /* auth.backend.mysql.port */
case 5: /* auth.backend.mysql.socket */
case 6: /* auth.backend.mysql.users_table */
break;
case 7: /* auth.backend.mysql.col_user */
case 8: /* auth.backend.mysql.col_pass */
case 9: /* auth.backend.mysql.col_realm */
if (buffer_string_is_empty(cpv->v.b)) {
log_error(srv->errh, __FILE__, __LINE__,
"%s must not be blank", cpk[cpv->k_id].k);
return HANDLER_ERROR;
}
break;
default:/* should not happen */
break;
}
}
}
p->defaults.auth_mysql_col_user = "user";
p->defaults.auth_mysql_col_pass = "password";
p->defaults.auth_mysql_col_realm = "realm";
p->defaults.errh = srv->errh;
/* initialize p->defaults from global config context */
if (p->nconfig > 0 && p->cvlist->v.u2[1]) {
const config_plugin_value_t *cpv = p->cvlist + p->cvlist->v.u2[0];
if (-1 != cpv->k_id)
mod_authn_mysql_merge_config(&p->defaults, cpv);
}
return HANDLER_GO_ON;
}
static int mod_authn_mysql_password_cmp(const char *userpw, unsigned long userpwlen, const char *reqpw) {
#if defined(HAVE_CRYPT_R) || defined(HAVE_CRYPT)
if (userpwlen >= 3 && userpw[0] == '$' && userpw[2] == '$') {
/* md5 crypt()
* request by Nicola Tiling <nti@w4w.net> */
const char *saltb = userpw+3;
const char *salte = strchr(saltb, '$');
char salt[32];
size_t slen = (NULL != salte) ? (size_t)(salte - saltb) : sizeof(salt);
if (slen < sizeof(salt)) {
char *crypted;
#if defined(HAVE_CRYPT_R)
struct crypt_data crypt_tmp_data;
#ifdef _AIX
memset(&crypt_tmp_data, 0, sizeof(crypt_tmp_data));
#else
crypt_tmp_data.initialized = 0;
#endif
#endif
memcpy(salt, saltb, slen);
salt[slen] = '\0';
#if defined(HAVE_CRYPT_R)
crypted = crypt_r(reqpw, salt, &crypt_tmp_data);
#else
crypted = crypt(reqpw, salt);
#endif
if (NULL != crypted) {
return strcmp(userpw, crypted);
}
}
}
else
#endif
if (32 == userpwlen) {
/* plain md5 */
li_MD5_CTX Md5Ctx;
unsigned char HA1[16];
unsigned char md5pw[16];
li_MD5_Init(&Md5Ctx);
li_MD5_Update(&Md5Ctx, (unsigned char *)reqpw, strlen(reqpw));
li_MD5_Final(HA1, &Md5Ctx);
/*(compare 16-byte MD5 binary instead of converting to hex strings
* in order to then have to do case-insensitive hex str comparison)*/
return (0 == http_auth_digest_hex2bin(userpw, 32, md5pw, sizeof(md5pw)))
? http_auth_const_time_memeq(HA1, md5pw, sizeof(md5pw)) ? 0 : 1
: -1;
}
return -1;
}
static int mod_authn_mysql_result(plugin_data *p, http_auth_info_t *ai, const char *pw) {
MYSQL_RES *result = mysql_store_result(p->mysql_conn);
int rc = -1;
my_ulonglong num_rows;
if (NULL == result) {
/*(future: might log mysql_error() string)*/
#if 0
log_error(errh, __FILE__, __LINE__,
"mysql_store_result: %s", mysql_error(p->mysql_conn));
#endif
mod_authn_mysql_sock_error(p);
return -1;
}
num_rows = mysql_num_rows(result);
if (1 == num_rows) {
MYSQL_ROW row = mysql_fetch_row(result);
unsigned long *lengths = mysql_fetch_lengths(result);
if (NULL == lengths) {
/*(error; should not happen)*/
}
else if (pw) { /* used with HTTP Basic auth */
rc = mod_authn_mysql_password_cmp(row[0], lengths[0], pw);
}
else { /* used with HTTP Digest auth */
/*(currently supports only single row, single digest algorithm)*/
if (lengths[0] == (ai->dlen << 1)) {
rc = http_auth_digest_hex2bin(row[0], lengths[0],
ai->digest, sizeof(ai->digest));
}
}
}
else if (0 == num_rows) {
/* user,realm not found */
}
else {
/* (multiple rows returned, which should not happen) */
/* (future: might log if multiple rows returned; unexpected result) */
}
mysql_free_result(result);
return rc;
}
static handler_t mod_authn_mysql_query(request_st * const r, void *p_d, http_auth_info_t * const ai, const char * const pw) {
plugin_data *p = (plugin_data *)p_d;
int rc = -1;
mod_authn_mysql_patch_config(r, p);
p->conf.errh = r->conf.errh;
if (NULL == p->conf.auth_mysql_users_table) {
/*(auth.backend.mysql.host, auth.backend.mysql.db might be NULL; do not log)*/
log_error(r->conf.errh, __FILE__, __LINE__,
"auth config missing auth.backend.mysql.users_table for uri: %s",
r->target.ptr);
return HANDLER_ERROR;
}
do {
char q[1024], uname[512], urealm[512];
unsigned long mrc;
if (ai->ulen > sizeof(uname)/2-1)
return HANDLER_ERROR;
if (ai->rlen > sizeof(urealm)/2-1)
return HANDLER_ERROR;
if (!mod_authn_mysql_sock_acquire(p)) {
return HANDLER_ERROR;
}
#if 0
mrc = mysql_real_escape_string_quote(p->mysql_conn, uname,
ai->username, ai->ulen, '\'');
if ((unsigned long)~0 == mrc) break;
mrc = mysql_real_escape_string_quote(p->mysql_conn, urealm,
ai->realm, ai->rlen, '\'');
if ((unsigned long)~0 == mrc) break;
#else
mrc = mysql_real_escape_string(p->mysql_conn, uname,
ai->username, ai->ulen);
if ((unsigned long)~0 == mrc) break;
mrc = mysql_real_escape_string(p->mysql_conn, urealm,
ai->realm, ai->rlen);
if ((unsigned long)~0 == mrc) break;
#endif
rc = snprintf(q, sizeof(q),
"SELECT %s FROM %s WHERE %s='%s' AND %s='%s'",
p->conf.auth_mysql_col_pass,
p->conf.auth_mysql_users_table,
p->conf.auth_mysql_col_user,
uname,
p->conf.auth_mysql_col_realm,
urealm);
if (rc >= (int)sizeof(q)) {
rc = -1;
break;
}
/* for now we stay synchronous */
if (0 != mysql_query(p->mysql_conn, q)) {
/* reconnect to db and retry once if query error occurs */
mod_authn_mysql_sock_error(p);
if (!mod_authn_mysql_sock_acquire(p)) {
rc = -1;
break;
}
if (0 != mysql_query(p->mysql_conn, q)) {
/*(note: any of these params might be bufs w/ b->ptr == NULL)*/
log_error(r->conf.errh, __FILE__, __LINE__,
"mysql_query host: %s user: %s db: %s query: %s failed: %s",
p->conf.auth_mysql_host ? p->conf.auth_mysql_host : "",
p->conf.auth_mysql_user ? p->conf.auth_mysql_user : "",
/*"pass:",*//*(omit pass from logs)*/
/*p->conf.auth_mysql_pass ? p->conf.auth_mysql_pass : "",*/
p->conf.auth_mysql_db ? p->conf.auth_mysql_db : "",
q, mysql_error(p->mysql_conn));
rc = -1;
break;
}
}
rc = mod_authn_mysql_result(p, ai, pw);
} while (0);
mod_authn_mysql_sock_release(p);
return (0 == rc) ? HANDLER_GO_ON : HANDLER_ERROR;
}
static handler_t mod_authn_mysql_basic(request_st * const r, void *p_d, const http_auth_require_t * const require, const buffer * const username, const char * const pw) {
handler_t rc;
http_auth_info_t ai;
ai.dalgo = HTTP_AUTH_DIGEST_NONE;
ai.dlen = 0;
ai.username = username->ptr;
ai.ulen = buffer_string_length(username);
ai.realm = require->realm->ptr;
ai.rlen = buffer_string_length(require->realm);
rc = mod_authn_mysql_query(r, p_d, &ai, pw);
if (HANDLER_GO_ON != rc) return rc;
return http_auth_match_rules(require, username->ptr, NULL, NULL)
? HANDLER_GO_ON /* access granted */
: HANDLER_ERROR;
}
static handler_t mod_authn_mysql_digest(request_st * const r, void *p_d, http_auth_info_t * const ai) {
return mod_authn_mysql_query(r, p_d, ai, NULL);
}
int mod_authn_mysql_plugin_init(plugin *p);
int mod_authn_mysql_plugin_init(plugin *p) {
p->version = LIGHTTPD_VERSION_ID;
p->name = "authn_mysql";
p->init = mod_authn_mysql_init;
p->set_defaults= mod_authn_mysql_set_defaults;
p->cleanup = mod_authn_mysql_sock_close;
return 0;
}