[stat_cache] FAM: ignore event with no valid match
[lighttpd.git] / src / mod_authn_mysql.c
blobc1f881f55aca05577fe1648281a1bdef9d9dd032
1 #include "first.h"
3 /* mod_authn_mysql
4 *
5 * KNOWN LIMITATIONS:
6 * - no mechanism provided to configure SSL connection to a remote MySQL db
8 * FUTURE POTENTIAL PERFORMANCE ENHANCEMENTS:
9 * - database response is not cached
10 * TODO: db response caching (for limited time) to reduce load on db
11 * (only cache successful logins to prevent cache bloat?)
12 * (or limit number of entries (size) of cache)
13 * (maybe have negative cache (limited size) of names not found in database)
14 * - database query is synchronous and blocks waiting for response
15 * TODO: https://mariadb.com/kb/en/mariadb/using-the-non-blocking-library/
16 * - opens and closes connection to MySQL db for each request (inefficient)
17 * (fixed) one-element cache for persistent connection open to last used db
18 * TODO: db connection pool (if asynchronous requests)
21 #include <mysql.h>
23 #include "base.h"
24 #include "http_auth.h"
25 #include "log.h"
26 #include "md5.h"
27 #include "plugin.h"
29 #include <errno.h>
30 #include <stdio.h>
31 #include <stdlib.h>
32 #include <string.h>
34 #if defined(HAVE_CRYPT_R) || defined(HAVE_CRYPT)
35 #include <unistd.h> /* crypt() */
36 #endif
37 #ifdef HAVE_CRYPT_H
38 #include <crypt.h>
39 #endif
41 typedef struct {
42 MYSQL *mysql_conn;
43 buffer *mysql_conn_host;
44 buffer *mysql_conn_user;
45 buffer *mysql_conn_pass;
46 buffer *mysql_conn_db;
47 int mysql_conn_port;
48 int auth_mysql_port;
49 buffer *auth_mysql_host;
50 buffer *auth_mysql_user;
51 buffer *auth_mysql_pass;
52 buffer *auth_mysql_db;
53 buffer *auth_mysql_socket;
54 buffer *auth_mysql_users_table;
55 buffer *auth_mysql_col_user;
56 buffer *auth_mysql_col_pass;
57 buffer *auth_mysql_col_realm;
58 } plugin_config;
60 typedef struct {
61 PLUGIN_DATA;
62 plugin_config **config_storage;
63 plugin_config conf;
64 } plugin_data;
66 static void mod_authn_mysql_sock_close(plugin_config *pconf) {
67 if (NULL != pconf->mysql_conn) {
68 mysql_close(pconf->mysql_conn);
69 pconf->mysql_conn = NULL;
73 static MYSQL * mod_authn_mysql_sock_connect(server *srv, plugin_config *pconf) {
74 if (NULL != pconf->mysql_conn) {
75 /* reuse open db connection if same ptrs to host user pass db port */
76 if ( pconf->mysql_conn_host == pconf->auth_mysql_host
77 && pconf->mysql_conn_user == pconf->auth_mysql_user
78 && pconf->mysql_conn_pass == pconf->auth_mysql_pass
79 && pconf->mysql_conn_db == pconf->auth_mysql_db
80 && pconf->mysql_conn_port == pconf->auth_mysql_port) {
81 return pconf->mysql_conn;
83 mod_authn_mysql_sock_close(pconf);
86 /* !! mysql_init() is not thread safe !! (see MySQL doc) */
87 pconf->mysql_conn = mysql_init(NULL);
88 if (mysql_real_connect(pconf->mysql_conn,
89 pconf->auth_mysql_host->ptr,
90 pconf->auth_mysql_user->ptr,
91 pconf->auth_mysql_pass->ptr,
92 pconf->auth_mysql_db->ptr,
93 pconf->auth_mysql_port,
94 !buffer_string_is_empty(pconf->auth_mysql_socket)
95 ? pconf->auth_mysql_socket->ptr
96 : NULL,
97 CLIENT_IGNORE_SIGPIPE)) {
98 /* (copy ptrs to config data (has lifetime until server shutdown)) */
99 pconf->mysql_conn_host = pconf->auth_mysql_host;
100 pconf->mysql_conn_user = pconf->auth_mysql_user;
101 pconf->mysql_conn_pass = pconf->auth_mysql_pass;
102 pconf->mysql_conn_db = pconf->auth_mysql_db;
103 pconf->mysql_conn_port = pconf->auth_mysql_port;
104 return pconf->mysql_conn;
106 else {
107 /*(note: any of these params might be buffers with b->ptr == NULL)*/
108 log_error_write(srv, __FILE__, __LINE__, "sbsb"/*sb*/"sbss",
109 "opening connection to mysql:", pconf->auth_mysql_host,
110 "user:", pconf->auth_mysql_user,
111 /*"pass:", pconf->auth_mysql_pass,*//*(omit from logs)*/
112 "db:", pconf->auth_mysql_db,
113 "failed:", mysql_error(pconf->mysql_conn));
114 mod_authn_mysql_sock_close(pconf);
115 return NULL;
119 static MYSQL * mod_authn_mysql_sock_acquire(server *srv, plugin_config *pconf) {
120 return mod_authn_mysql_sock_connect(srv, pconf);
123 static void mod_authn_mysql_sock_release(server *srv, plugin_config *pconf) {
124 UNUSED(srv);
125 UNUSED(pconf);
126 /*(empty; leave db connection open)*/
127 /* Note: mod_authn_mysql_result() calls mod_authn_mysql_sock_error()
128 * on error, so take that into account if making changes here.
129 * Must check if (NULL == pconf->mysql_conn) */
132 static void mod_authn_mysql_sock_error(server *srv, plugin_config *pconf) {
133 UNUSED(srv);
134 mod_authn_mysql_sock_close(pconf);
137 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);
138 static handler_t mod_authn_mysql_digest(server *srv, connection *con, void *p_d, http_auth_info_t *dig);
140 INIT_FUNC(mod_authn_mysql_init) {
141 static http_auth_backend_t http_auth_backend_mysql =
142 { "mysql", mod_authn_mysql_basic, mod_authn_mysql_digest, NULL };
143 plugin_data *p = calloc(1, sizeof(*p));
145 /* register http_auth_backend_mysql */
146 http_auth_backend_mysql.p_d = p;
147 http_auth_backend_set(&http_auth_backend_mysql);
149 return p;
152 FREE_FUNC(mod_authn_mysql_free) {
153 plugin_data *p = p_d;
155 UNUSED(srv);
157 if (!p) return HANDLER_GO_ON;
159 if (p->config_storage) {
160 size_t i;
161 for (i = 0; i < srv->config_context->used; i++) {
162 plugin_config *s = p->config_storage[i];
164 if (NULL == s) continue;
166 buffer_free(s->auth_mysql_host);
167 buffer_free(s->auth_mysql_user);
168 buffer_free(s->auth_mysql_pass);
169 buffer_free(s->auth_mysql_db);
170 buffer_free(s->auth_mysql_socket);
171 buffer_free(s->auth_mysql_users_table);
172 buffer_free(s->auth_mysql_col_user);
173 buffer_free(s->auth_mysql_col_pass);
174 buffer_free(s->auth_mysql_col_realm);
176 if (s->mysql_conn) mod_authn_mysql_sock_close(s);
178 free(s);
180 free(p->config_storage);
182 mod_authn_mysql_sock_close(&p->conf);
184 free(p);
186 return HANDLER_GO_ON;
189 SETDEFAULTS_FUNC(mod_authn_mysql_set_defaults) {
190 plugin_data *p = p_d;
191 size_t i;
192 config_values_t cv[] = {
193 { "auth.backend.mysql.host", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION },
194 { "auth.backend.mysql.user", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION },
195 { "auth.backend.mysql.pass", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION },
196 { "auth.backend.mysql.db", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION },
197 { "auth.backend.mysql.port", NULL, T_CONFIG_INT, T_CONFIG_SCOPE_CONNECTION },
198 { "auth.backend.mysql.socket", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION },
199 { "auth.backend.mysql.users_table", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION },
200 { "auth.backend.mysql.col_user", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION },
201 { "auth.backend.mysql.col_pass", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION },
202 { "auth.backend.mysql.col_realm", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION },
203 { NULL, NULL, T_CONFIG_UNSET, T_CONFIG_SCOPE_UNSET }
206 p->config_storage = calloc(srv->config_context->used, sizeof(plugin_config *));
208 for (i = 0; i < srv->config_context->used; i++) {
209 data_config const* config = (data_config const*)srv->config_context->data[i];
210 plugin_config *s;
212 s = calloc(1, sizeof(plugin_config));
214 s->mysql_conn = NULL;
215 s->auth_mysql_host = buffer_init();
216 s->auth_mysql_user = buffer_init();
217 s->auth_mysql_pass = buffer_init();
218 s->auth_mysql_db = buffer_init();
219 s->auth_mysql_socket = buffer_init();
220 s->auth_mysql_users_table = buffer_init();
221 s->auth_mysql_col_user = buffer_init();
222 s->auth_mysql_col_pass = buffer_init();
223 s->auth_mysql_col_realm = buffer_init();
225 cv[0].destination = s->auth_mysql_host;
226 cv[1].destination = s->auth_mysql_user;
227 cv[2].destination = s->auth_mysql_pass;
228 cv[3].destination = s->auth_mysql_db;
229 cv[4].destination = &s->auth_mysql_port;
230 cv[5].destination = s->auth_mysql_socket;
231 cv[6].destination = s->auth_mysql_users_table;
232 cv[7].destination = s->auth_mysql_col_user;
233 cv[8].destination = s->auth_mysql_col_pass;
234 cv[9].destination = s->auth_mysql_col_realm;
236 p->config_storage[i] = s;
238 if (0 != config_insert_values_global(srv, config->value, cv, i == 0 ? T_CONFIG_SCOPE_SERVER : T_CONFIG_SCOPE_CONNECTION)) {
239 return HANDLER_ERROR;
242 if (!buffer_is_empty(s->auth_mysql_col_user)
243 && buffer_string_is_empty(s->auth_mysql_col_user)) {
244 log_error_write(srv, __FILE__, __LINE__, "s",
245 "auth.backend.mysql.col_user must not be blank");
246 return HANDLER_ERROR;
248 if (!buffer_is_empty(s->auth_mysql_col_pass)
249 && buffer_string_is_empty(s->auth_mysql_col_pass)) {
250 log_error_write(srv, __FILE__, __LINE__, "s",
251 "auth.backend.mysql.col_pass must not be blank");
252 return HANDLER_ERROR;
254 if (!buffer_is_empty(s->auth_mysql_col_realm)
255 && buffer_string_is_empty(s->auth_mysql_col_realm)) {
256 log_error_write(srv, __FILE__, __LINE__, "s",
257 "auth.backend.mysql.col_realm must not be blank");
258 return HANDLER_ERROR;
262 if (p->config_storage[0]) { /*(always true)*/
263 plugin_config *s = p->config_storage[0];
264 if (buffer_is_empty(s->auth_mysql_col_user)) {
265 buffer_copy_string_len(s->auth_mysql_col_user, CONST_STR_LEN("user"));
267 if (buffer_is_empty(s->auth_mysql_col_pass)) {
268 buffer_copy_string_len(s->auth_mysql_col_pass, CONST_STR_LEN("password"));
270 if (buffer_is_empty(s->auth_mysql_col_realm)) {
271 buffer_copy_string_len(s->auth_mysql_col_realm, CONST_STR_LEN("realm"));
275 return HANDLER_GO_ON;
278 #define PATCH(x) \
279 p->conf.x = s->x;
280 static int mod_authn_mysql_patch_connection(server *srv, connection *con, plugin_data *p) {
281 size_t i, j;
282 plugin_config *s = p->config_storage[0];
284 PATCH(auth_mysql_host);
285 PATCH(auth_mysql_user);
286 PATCH(auth_mysql_pass);
287 PATCH(auth_mysql_db);
288 PATCH(auth_mysql_port);
289 PATCH(auth_mysql_socket);
290 PATCH(auth_mysql_users_table);
291 PATCH(auth_mysql_col_user);
292 PATCH(auth_mysql_col_pass);
293 PATCH(auth_mysql_col_realm);
295 /* skip the first, the global context */
296 for (i = 1; i < srv->config_context->used; i++) {
297 data_config *dc = (data_config *)srv->config_context->data[i];
298 s = p->config_storage[i];
300 /* condition didn't match */
301 if (!config_check_cond(srv, con, dc)) continue;
303 /* merge config */
304 for (j = 0; j < dc->value->used; j++) {
305 data_unset *du = dc->value->data[j];
307 if (buffer_is_equal_string(du->key, CONST_STR_LEN("auth.backend.mysql.host"))) {
308 PATCH(auth_mysql_host);
309 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("auth.backend.mysql.user"))) {
310 PATCH(auth_mysql_user);
311 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("auth.backend.mysql.pass"))) {
312 PATCH(auth_mysql_pass);
313 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("auth.backend.mysql.db"))) {
314 PATCH(auth_mysql_db);
315 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("auth.backend.mysql.port"))) {
316 PATCH(auth_mysql_port);
317 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("auth.backend.mysql.socket"))) {
318 PATCH(auth_mysql_socket);
319 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("auth.backend.mysql.users_table"))) {
320 PATCH(auth_mysql_users_table);
321 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("auth.backend.mysql.col_user"))) {
322 PATCH(auth_mysql_col_user);
323 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("auth.backend.mysql.col_pass"))) {
324 PATCH(auth_mysql_col_pass);
325 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("auth.backend.mysql.col_realm"))) {
326 PATCH(auth_mysql_col_realm);
331 return 0;
333 #undef PATCH
335 static int mod_authn_mysql_password_cmp(const char *userpw, unsigned long userpwlen, const char *reqpw) {
336 #if defined(HAVE_CRYPT_R) || defined(HAVE_CRYPT)
337 if (userpwlen >= 3 && userpw[0] == '$' && userpw[2] == '$') {
338 /* md5 crypt()
339 * request by Nicola Tiling <nti@w4w.net> */
340 const char *saltb = userpw+3;
341 const char *salte = strchr(saltb, '$');
342 char salt[32];
343 size_t slen = (NULL != salte) ? (size_t)(salte - saltb) : sizeof(salt);
345 if (slen < sizeof(salt)) {
346 char *crypted;
347 #if defined(HAVE_CRYPT_R)
348 struct crypt_data crypt_tmp_data;
349 #ifdef _AIX
350 memset(&crypt_tmp_data, 0, sizeof(crypt_tmp_data));
351 #else
352 crypt_tmp_data.initialized = 0;
353 #endif
354 #endif
355 memcpy(salt, saltb, slen);
356 salt[slen] = '\0';
358 #if defined(HAVE_CRYPT_R)
359 crypted = crypt_r(reqpw, salt, &crypt_tmp_data);
360 #else
361 crypted = crypt(reqpw, salt);
362 #endif
363 if (NULL != crypted) {
364 return strcmp(userpw, crypted);
368 else
369 #endif
370 if (32 == userpwlen) {
371 /* plain md5 */
372 li_MD5_CTX Md5Ctx;
373 unsigned char HA1[16];
374 unsigned char md5pw[16];
376 li_MD5_Init(&Md5Ctx);
377 li_MD5_Update(&Md5Ctx, (unsigned char *)reqpw, strlen(reqpw));
378 li_MD5_Final(HA1, &Md5Ctx);
380 /*(compare 16-byte MD5 binary instead of converting to hex strings
381 * in order to then have to do case-insensitive hex str comparison)*/
382 return (0 == http_auth_digest_hex2bin(userpw, 32, md5pw, sizeof(md5pw)))
383 ? memcmp(HA1, md5pw, sizeof(md5pw))
384 : -1;
387 return -1;
390 static int mod_authn_mysql_result(server *srv, plugin_data *p, http_auth_info_t *ai, const char *pw) {
391 MYSQL_RES *result = mysql_store_result(p->conf.mysql_conn);
392 int rc = -1;
393 my_ulonglong num_rows;
395 if (NULL == result) {
396 /*(future: might log mysql_error() string)*/
397 #if 0
398 log_error_write(srv, __FILE__, __LINE__, "ss", "mysql_store_result:",
399 mysql_error(p->conf.mysql_conn));
400 #endif
401 mod_authn_mysql_sock_error(srv, &p->conf);
402 return -1;
405 num_rows = mysql_num_rows(result);
406 if (1 == num_rows) {
407 MYSQL_ROW row = mysql_fetch_row(result);
408 unsigned long *lengths = mysql_fetch_lengths(result);
409 if (NULL == lengths) {
410 /*(error; should not happen)*/
412 else if (pw) { /* used with HTTP Basic auth */
413 rc = mod_authn_mysql_password_cmp(row[0], lengths[0], pw);
415 else { /* used with HTTP Digest auth */
416 /*(currently supports only single row, single digest algorithm)*/
417 if (lengths[0] == (ai->dlen << 1)) {
418 rc = http_auth_digest_hex2bin(row[0], lengths[0],
419 ai->digest, sizeof(ai->digest));
423 else if (0 == num_rows) {
424 /* user,realm not found */
426 else {
427 /* (multiple rows returned, which should not happen) */
428 /* (future: might log if multiple rows returned; unexpected result) */
430 mysql_free_result(result);
431 return rc;
434 static handler_t mod_authn_mysql_query(server *srv, connection *con, void *p_d, http_auth_info_t *ai, const char *pw) {
435 plugin_data *p = (plugin_data *)p_d;
436 int rc = -1;
438 mod_authn_mysql_patch_connection(srv, con, p);
440 if (buffer_string_is_empty(p->conf.auth_mysql_users_table)) {
441 /*(auth.backend.mysql.host, auth.backend.mysql.db might be NULL; do not log)*/
442 log_error_write(srv, __FILE__, __LINE__, "sb",
443 "auth config missing auth.backend.mysql.users_table for uri:",
444 con->request.uri);
445 return HANDLER_ERROR;
448 do {
449 char q[1024], uname[512], urealm[512];
450 unsigned long mrc;
452 if (ai->ulen > sizeof(uname)/2-1)
453 return HANDLER_ERROR;
454 if (ai->rlen > sizeof(urealm)/2-1)
455 return HANDLER_ERROR;
457 if (!mod_authn_mysql_sock_acquire(srv, &p->conf)) {
458 return HANDLER_ERROR;
461 #if 0
462 mrc = mysql_real_escape_string_quote(p->conf.mysql_conn, uname,
463 ai->username, ai->ulen, '\'');
464 if ((unsigned long)~0 == mrc) break;
466 mrc = mysql_real_escape_string_quote(p->conf.mysql_conn, urealm,
467 ai->realm, ai->rlen, '\'');
468 if ((unsigned long)~0 == mrc) break;
469 #else
470 mrc = mysql_real_escape_string(p->conf.mysql_conn, uname,
471 ai->username, ai->ulen);
472 if ((unsigned long)~0 == mrc) break;
474 mrc = mysql_real_escape_string(p->conf.mysql_conn, urealm,
475 ai->realm, ai->rlen);
476 if ((unsigned long)~0 == mrc) break;
477 #endif
479 rc = snprintf(q, sizeof(q),
480 "SELECT %s FROM %s WHERE %s='%s' AND %s='%s'",
481 p->conf.auth_mysql_col_pass->ptr,
482 p->conf.auth_mysql_users_table->ptr,
483 p->conf.auth_mysql_col_user->ptr,
484 uname,
485 p->conf.auth_mysql_col_realm->ptr,
486 urealm);
488 if (rc >= (int)sizeof(q)) {
489 rc = -1;
490 break;
493 /* for now we stay synchronous */
494 if (0 != mysql_query(p->conf.mysql_conn, q)) {
495 /* reconnect to db and retry once if query error occurs */
496 mod_authn_mysql_sock_error(srv, &p->conf);
497 if (!mod_authn_mysql_sock_acquire(srv, &p->conf)) {
498 rc = -1;
499 break;
501 if (0 != mysql_query(p->conf.mysql_conn, q)) {
502 /*(note: any of these params might be bufs w/ b->ptr == NULL)*/
503 log_error_write(srv, __FILE__, __LINE__, "sbsb"/*sb*/"sbssss",
504 "mysql_query host:", p->conf.auth_mysql_host,
505 "user:", p->conf.auth_mysql_user,
506 /*(omit pass from logs)*/
507 /*"pass:", p->conf.auth_mysql_pass,*/
508 "db:", p->conf.auth_mysql_db,
509 "query:", q,
510 "failed:", mysql_error(p->conf.mysql_conn));
511 rc = -1;
512 break;
516 rc = mod_authn_mysql_result(srv, p, ai, pw);
518 } while (0);
520 mod_authn_mysql_sock_release(srv, &p->conf);
522 return (0 == rc) ? HANDLER_GO_ON : HANDLER_ERROR;
525 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) {
526 handler_t rc;
527 http_auth_info_t ai;
528 ai.dalgo = HTTP_AUTH_DIGEST_NONE;
529 ai.dlen = 0;
530 ai.username = username->ptr;
531 ai.ulen = buffer_string_length(username);
532 ai.realm = require->realm->ptr;
533 ai.rlen = buffer_string_length(require->realm);
534 rc = mod_authn_mysql_query(srv, con, p_d, &ai, pw);
535 if (HANDLER_GO_ON != rc) return rc;
536 return http_auth_match_rules(require, username->ptr, NULL, NULL)
537 ? HANDLER_GO_ON /* access granted */
538 : HANDLER_ERROR;
541 static handler_t mod_authn_mysql_digest(server *srv, connection *con, void *p_d, http_auth_info_t *ai) {
542 return mod_authn_mysql_query(srv, con, p_d, ai, NULL);
545 int mod_authn_mysql_plugin_init(plugin *p);
546 int mod_authn_mysql_plugin_init(plugin *p) {
547 p->version = LIGHTTPD_VERSION_ID;
548 p->name = buffer_init_string("authn_mysql");
549 p->init = mod_authn_mysql_init;
550 p->set_defaults= mod_authn_mysql_set_defaults;
551 p->cleanup = mod_authn_mysql_free;
553 p->data = NULL;
555 return 0;