#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 #include "base.h" #include "http_auth.h" #include "log.h" #include "md5.h" #include "plugin.h" #include #include #include #include #if defined(HAVE_CRYPT_R) || defined(HAVE_CRYPT) #include /* crypt() */ #endif #ifdef HAVE_CRYPT_H #include #endif typedef struct { MYSQL *mysql_conn; buffer *mysql_conn_host; buffer *mysql_conn_user; buffer *mysql_conn_pass; buffer *mysql_conn_db; int mysql_conn_port; int auth_mysql_port; buffer *auth_mysql_host; buffer *auth_mysql_user; buffer *auth_mysql_pass; buffer *auth_mysql_db; buffer *auth_mysql_socket; buffer *auth_mysql_users_table; buffer *auth_mysql_col_user; buffer *auth_mysql_col_pass; buffer *auth_mysql_col_realm; } plugin_config; typedef struct { PLUGIN_DATA; plugin_config **config_storage; plugin_config conf; } plugin_data; static void mod_authn_mysql_sock_close(plugin_config *pconf) { if (NULL != pconf->mysql_conn) { mysql_close(pconf->mysql_conn); pconf->mysql_conn = NULL; } } static MYSQL * mod_authn_mysql_sock_connect(server *srv, plugin_config *pconf) { if (NULL != pconf->mysql_conn) { /* reuse open db connection if same ptrs to host user pass db port */ if ( pconf->mysql_conn_host == pconf->auth_mysql_host && pconf->mysql_conn_user == pconf->auth_mysql_user && pconf->mysql_conn_pass == pconf->auth_mysql_pass && pconf->mysql_conn_db == pconf->auth_mysql_db && pconf->mysql_conn_port == pconf->auth_mysql_port) { return pconf->mysql_conn; } mod_authn_mysql_sock_close(pconf); } /* !! mysql_init() is not thread safe !! (see MySQL doc) */ pconf->mysql_conn = mysql_init(NULL); if (mysql_real_connect(pconf->mysql_conn, pconf->auth_mysql_host->ptr, pconf->auth_mysql_user->ptr, pconf->auth_mysql_pass->ptr, pconf->auth_mysql_db->ptr, pconf->auth_mysql_port, !buffer_string_is_empty(pconf->auth_mysql_socket) ? pconf->auth_mysql_socket->ptr : NULL, CLIENT_IGNORE_SIGPIPE)) { /* (copy ptrs to config data (has lifetime until server shutdown)) */ pconf->mysql_conn_host = pconf->auth_mysql_host; pconf->mysql_conn_user = pconf->auth_mysql_user; pconf->mysql_conn_pass = pconf->auth_mysql_pass; pconf->mysql_conn_db = pconf->auth_mysql_db; pconf->mysql_conn_port = pconf->auth_mysql_port; return pconf->mysql_conn; } else { /*(note: any of these params might be buffers with b->ptr == NULL)*/ log_error_write(srv, __FILE__, __LINE__, "sbsb"/*sb*/"sbss", "opening connection to mysql:", pconf->auth_mysql_host, "user:", pconf->auth_mysql_user, /*"pass:", pconf->auth_mysql_pass,*//*(omit from logs)*/ "db:", pconf->auth_mysql_db, "failed:", mysql_error(pconf->mysql_conn)); mod_authn_mysql_sock_close(pconf); return NULL; } } static MYSQL * mod_authn_mysql_sock_acquire(server *srv, plugin_config *pconf) { return mod_authn_mysql_sock_connect(srv, pconf); } static void mod_authn_mysql_sock_release(server *srv, plugin_config *pconf) { UNUSED(srv); UNUSED(pconf); /*(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 == pconf->mysql_conn) */ } static void mod_authn_mysql_sock_error(server *srv, plugin_config *pconf) { UNUSED(srv); mod_authn_mysql_sock_close(pconf); } static handler_t mod_authn_mysql_basic(server *srv, connection *con, void *p_d, const http_auth_require_t *require, const buffer *username, const char *pw); static handler_t mod_authn_mysql_digest(server *srv, connection *con, void *p_d, const char *username, const char *realm, unsigned char HA1[16]); 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; } FREE_FUNC(mod_authn_mysql_free) { plugin_data *p = p_d; UNUSED(srv); if (!p) return HANDLER_GO_ON; if (p->config_storage) { size_t i; for (i = 0; i < srv->config_context->used; i++) { plugin_config *s = p->config_storage[i]; if (NULL == s) continue; buffer_free(s->auth_mysql_host); buffer_free(s->auth_mysql_user); buffer_free(s->auth_mysql_pass); buffer_free(s->auth_mysql_db); buffer_free(s->auth_mysql_socket); buffer_free(s->auth_mysql_users_table); buffer_free(s->auth_mysql_col_user); buffer_free(s->auth_mysql_col_pass); buffer_free(s->auth_mysql_col_realm); if (s->mysql_conn) mod_authn_mysql_sock_close(s); free(s); } free(p->config_storage); } mod_authn_mysql_sock_close(&p->conf); free(p); return HANDLER_GO_ON; } SETDEFAULTS_FUNC(mod_authn_mysql_set_defaults) { plugin_data *p = p_d; size_t i; config_values_t cv[] = { { "auth.backend.mysql.host", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION }, { "auth.backend.mysql.user", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION }, { "auth.backend.mysql.pass", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION }, { "auth.backend.mysql.db", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION }, { "auth.backend.mysql.port", NULL, T_CONFIG_INT, T_CONFIG_SCOPE_CONNECTION }, { "auth.backend.mysql.socket", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION }, { "auth.backend.mysql.users_table", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION }, { "auth.backend.mysql.col_user", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION }, { "auth.backend.mysql.col_pass", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION }, { "auth.backend.mysql.col_realm", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION }, { NULL, NULL, T_CONFIG_UNSET, T_CONFIG_SCOPE_UNSET } }; p->config_storage = calloc(1, srv->config_context->used * sizeof(plugin_config *)); for (i = 0; i < srv->config_context->used; i++) { data_config const* config = (data_config const*)srv->config_context->data[i]; plugin_config *s; s = calloc(1, sizeof(plugin_config)); s->mysql_conn = NULL; s->auth_mysql_host = buffer_init(); s->auth_mysql_user = buffer_init(); s->auth_mysql_pass = buffer_init(); s->auth_mysql_db = buffer_init(); s->auth_mysql_socket = buffer_init(); s->auth_mysql_users_table = buffer_init(); s->auth_mysql_col_user = buffer_init(); s->auth_mysql_col_pass = buffer_init(); s->auth_mysql_col_realm = buffer_init(); cv[0].destination = s->auth_mysql_host; cv[1].destination = s->auth_mysql_user; cv[2].destination = s->auth_mysql_pass; cv[3].destination = s->auth_mysql_db; cv[4].destination = &s->auth_mysql_port; cv[5].destination = s->auth_mysql_socket; cv[6].destination = s->auth_mysql_users_table; cv[7].destination = s->auth_mysql_col_user; cv[8].destination = s->auth_mysql_col_pass; cv[9].destination = s->auth_mysql_col_realm; p->config_storage[i] = s; if (0 != config_insert_values_global(srv, config->value, cv, i == 0 ? T_CONFIG_SCOPE_SERVER : T_CONFIG_SCOPE_CONNECTION)) { return HANDLER_ERROR; } if (!buffer_is_empty(s->auth_mysql_col_user) && buffer_string_is_empty(s->auth_mysql_col_user)) { log_error_write(srv, __FILE__, __LINE__, "s", "auth.backend.mysql.col_user must not be blank"); return HANDLER_ERROR; } if (!buffer_is_empty(s->auth_mysql_col_pass) && buffer_string_is_empty(s->auth_mysql_col_pass)) { log_error_write(srv, __FILE__, __LINE__, "s", "auth.backend.mysql.col_pass must not be blank"); return HANDLER_ERROR; } if (!buffer_is_empty(s->auth_mysql_col_realm) && buffer_string_is_empty(s->auth_mysql_col_realm)) { log_error_write(srv, __FILE__, __LINE__, "s", "auth.backend.mysql.col_realm must not be blank"); return HANDLER_ERROR; } } if (p->config_storage[0]) { /*(always true)*/ plugin_config *s = p->config_storage[0]; if (buffer_is_empty(s->auth_mysql_col_user)) { buffer_copy_string_len(s->auth_mysql_col_user, CONST_STR_LEN("user")); } if (buffer_is_empty(s->auth_mysql_col_pass)) { buffer_copy_string_len(s->auth_mysql_col_pass, CONST_STR_LEN("password")); } if (buffer_is_empty(s->auth_mysql_col_realm)) { buffer_copy_string_len(s->auth_mysql_col_realm, CONST_STR_LEN("realm")); } } return HANDLER_GO_ON; } #define PATCH(x) \ p->conf.x = s->x; static int mod_authn_mysql_patch_connection(server *srv, connection *con, plugin_data *p) { size_t i, j; plugin_config *s = p->config_storage[0]; PATCH(auth_mysql_host); PATCH(auth_mysql_user); PATCH(auth_mysql_pass); PATCH(auth_mysql_db); PATCH(auth_mysql_port); PATCH(auth_mysql_socket); PATCH(auth_mysql_users_table); PATCH(auth_mysql_col_user); PATCH(auth_mysql_col_pass); PATCH(auth_mysql_col_realm); /* skip the first, the global context */ for (i = 1; i < srv->config_context->used; i++) { data_config *dc = (data_config *)srv->config_context->data[i]; s = p->config_storage[i]; /* condition didn't match */ if (!config_check_cond(srv, con, dc)) continue; /* merge config */ for (j = 0; j < dc->value->used; j++) { data_unset *du = dc->value->data[j]; if (buffer_is_equal_string(du->key, CONST_STR_LEN("auth.backend.mysql.host"))) { PATCH(auth_mysql_host); } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("auth.backend.mysql.user"))) { PATCH(auth_mysql_user); } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("auth.backend.mysql.pass"))) { PATCH(auth_mysql_pass); } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("auth.backend.mysql.db"))) { PATCH(auth_mysql_db); } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("auth.backend.mysql.port"))) { PATCH(auth_mysql_port); } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("auth.backend.mysql.socket"))) { PATCH(auth_mysql_socket); } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("auth.backend.mysql.users_table"))) { PATCH(auth_mysql_users_table); } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("auth.backend.mysql.col_user"))) { PATCH(auth_mysql_col_user); } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("auth.backend.mysql.col_pass"))) { PATCH(auth_mysql_col_pass); } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("auth.backend.mysql.col_realm"))) { PATCH(auth_mysql_col_realm); } } } return 0; } #undef PATCH 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 */ 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_md5_hex2bin(userpw, 32 /*(userpwlen)*/, md5pw)) ? memcmp(HA1, md5pw, sizeof(md5pw)) : -1; } return -1; } static int mod_authn_mysql_result(server *srv, plugin_data *p, const char *pw, unsigned char HA1[16]) { MYSQL_RES *result = mysql_store_result(p->conf.mysql_conn); int rc = -1; my_ulonglong num_rows; if (NULL == result) { /*(future: might log mysql_error() string)*/ #if 0 log_error_write(srv, __FILE__, __LINE__, "ss", "mysql_store_result:", mysql_error(p->conf.mysql_conn)); #endif mod_authn_mysql_sock_error(srv, &p->conf); 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 */ rc = http_auth_md5_hex2bin(row[0], lengths[0], HA1); } } 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(server *srv, connection *con, void *p_d, const char *username, const char *realm, const char *pw, unsigned char HA1[16]) { plugin_data *p = (plugin_data *)p_d; int rc = -1; mod_authn_mysql_patch_connection(srv, con, p); if (buffer_string_is_empty(p->conf.auth_mysql_users_table)) { /*(auth.backend.mysql.host, auth.backend.mysql.db might be NULL; do not log)*/ log_error_write(srv, __FILE__, __LINE__, "sb", "auth config missing auth.backend.mysql.users_table for uri:", con->request.uri); return HANDLER_ERROR; } do { size_t unamelen = strlen(username); size_t urealmlen = strlen(realm); char q[1024], uname[512], urealm[512]; unsigned long mrc; if (unamelen > sizeof(uname)/2-1) return HANDLER_ERROR; if (urealmlen > sizeof(urealm)/2-1) return HANDLER_ERROR; if (!mod_authn_mysql_sock_acquire(srv, &p->conf)) { return HANDLER_ERROR; } #if 0 mrc = mysql_real_escape_string_quote(p->conf.mysql_conn,uname,username, (unsigned long)unamelen, '\''); if ((unsigned long)~0 == mrc) break; mrc = mysql_real_escape_string_quote(p->conf.mysql_conn,urealm,realm, (unsigned long)urealmlen, '\''); if ((unsigned long)~0 == mrc) break; #else mrc = mysql_real_escape_string(p->conf.mysql_conn, uname, username, (unsigned long)unamelen); if ((unsigned long)~0 == mrc) break; mrc = mysql_real_escape_string(p->conf.mysql_conn, urealm, realm, (unsigned long)urealmlen); 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->ptr, p->conf.auth_mysql_users_table->ptr, p->conf.auth_mysql_col_user->ptr, uname, p->conf.auth_mysql_col_realm->ptr, urealm); if (rc >= (int)sizeof(q)) { rc = -1; break; } /* for now we stay synchronous */ if (0 != mysql_query(p->conf.mysql_conn, q)) { /* reconnect to db and retry once if query error occurs */ mod_authn_mysql_sock_error(srv, &p->conf); if (!mod_authn_mysql_sock_acquire(srv, &p->conf)) { rc = -1; break; } if (0 != mysql_query(p->conf.mysql_conn, q)) { /*(note: any of these params might be bufs w/ b->ptr == NULL)*/ log_error_write(srv, __FILE__, __LINE__, "sbsb"/*sb*/"sbssss", "mysql_query host:", p->conf.auth_mysql_host, "user:", p->conf.auth_mysql_user, /*(omit pass from logs)*/ /*"pass:", p->conf.auth_mysql_pass,*/ "db:", p->conf.auth_mysql_db, "query:", q, "failed:", mysql_error(p->conf.mysql_conn)); rc = -1; break; } } rc = mod_authn_mysql_result(srv, p, pw, HA1); } while (0); mod_authn_mysql_sock_release(srv, &p->conf); return (0 == rc) ? HANDLER_GO_ON : HANDLER_ERROR; } static handler_t mod_authn_mysql_basic(server *srv, connection *con, void *p_d, const http_auth_require_t *require, const buffer *username, const char *pw) { /*(HA1 is not written since pw passed should not be NULL; * avoid passing NULL since subroutine expects unsigned char HA1[16] arg)*/ static unsigned char HA1[16]; char *realm = require->realm->ptr; handler_t rc =mod_authn_mysql_query(srv,con,p_d,username->ptr,realm,pw,HA1); 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(server *srv, connection *con, void *p_d, const char *username, const char *realm, unsigned char HA1[16]) { return mod_authn_mysql_query(srv,con,p_d,username,realm,NULL,HA1); } int mod_authn_mysql_plugin_init(plugin *p); int mod_authn_mysql_plugin_init(plugin *p) { p->version = LIGHTTPD_VERSION_ID; p->name = buffer_init_string("authn_mysql"); p->init = mod_authn_mysql_init; p->set_defaults= mod_authn_mysql_set_defaults; p->cleanup = mod_authn_mysql_free; p->data = NULL; return 0; }