[mod_cgi] fix pipe_cloexec() when no O_CLOEXEC
[lighttpd.git] / src / mod_authn_mysql.c
blobf014a7bf5e338bf5bca1615c18996c2b477aad43
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 "plugin.h"
23 #ifdef HAVE_MYSQL
25 #include <mysql.h>
27 #include "server.h"
28 #include "http_auth.h"
29 #include "log.h"
30 #include "md5.h"
32 #include <ctype.h>
33 #include <errno.h>
34 #include <string.h>
36 #ifdef HAVE_CRYPT_H
37 #include <crypt.h>
38 #endif
40 typedef struct {
41 MYSQL *mysql_conn;
42 buffer *mysql_conn_host;
43 buffer *mysql_conn_user;
44 buffer *mysql_conn_pass;
45 buffer *mysql_conn_db;
46 int mysql_conn_port;
47 int auth_mysql_port;
48 buffer *auth_mysql_host;
49 buffer *auth_mysql_user;
50 buffer *auth_mysql_pass;
51 buffer *auth_mysql_db;
52 buffer *auth_mysql_socket;
53 buffer *auth_mysql_users_table;
54 buffer *auth_mysql_col_user;
55 buffer *auth_mysql_col_pass;
56 buffer *auth_mysql_col_realm;
57 } plugin_config;
59 typedef struct {
60 PLUGIN_DATA;
61 plugin_config **config_storage;
62 plugin_config conf;
63 } plugin_data;
65 static void mod_authn_mysql_sock_close(plugin_config *pconf) {
66 if (NULL != pconf->mysql_conn) {
67 mysql_close(pconf->mysql_conn);
68 pconf->mysql_conn = NULL;
72 static MYSQL * mod_authn_mysql_sock_connect(server *srv, plugin_config *pconf) {
73 if (NULL != pconf->mysql_conn) {
74 /* reuse open db connection if same ptrs to host user pass db port */
75 if ( pconf->mysql_conn_host == pconf->auth_mysql_host
76 && pconf->mysql_conn_user == pconf->auth_mysql_user
77 && pconf->mysql_conn_pass == pconf->auth_mysql_pass
78 && pconf->mysql_conn_db == pconf->auth_mysql_db
79 && pconf->mysql_conn_port == pconf->auth_mysql_port) {
80 return pconf->mysql_conn;
82 mod_authn_mysql_sock_close(pconf);
85 /* !! mysql_init() is not thread safe !! (see MySQL doc) */
86 pconf->mysql_conn = mysql_init(NULL);
87 if (mysql_real_connect(pconf->mysql_conn,
88 pconf->auth_mysql_host->ptr,
89 pconf->auth_mysql_user->ptr,
90 pconf->auth_mysql_pass->ptr,
91 pconf->auth_mysql_db->ptr,
92 pconf->auth_mysql_port,
93 !buffer_string_is_empty(pconf->auth_mysql_socket)
94 ? pconf->auth_mysql_socket->ptr
95 : NULL,
96 CLIENT_IGNORE_SIGPIPE)) {
97 /* (copy ptrs to config data (has lifetime until server shutdown)) */
98 pconf->mysql_conn_host = pconf->auth_mysql_host;
99 pconf->mysql_conn_user = pconf->auth_mysql_user;
100 pconf->mysql_conn_pass = pconf->auth_mysql_pass;
101 pconf->mysql_conn_db = pconf->auth_mysql_db;
102 pconf->mysql_conn_port = pconf->auth_mysql_port;
103 return pconf->mysql_conn;
105 else {
106 /*(note: any of these params might be buffers with b->ptr == NULL)*/
107 log_error_write(srv, __FILE__, __LINE__, "sbsb"/*sb*/"sbss",
108 "opening connection to mysql:", pconf->auth_mysql_host,
109 "user:", pconf->auth_mysql_user,
110 /*"pass:", pconf->auth_mysql_pass,*//*(omit from logs)*/
111 "db:", pconf->auth_mysql_db,
112 "failed:", mysql_error(pconf->mysql_conn));
113 mod_authn_mysql_sock_close(pconf);
114 return NULL;
118 static MYSQL * mod_authn_mysql_sock_acquire(server *srv, plugin_config *pconf) {
119 return mod_authn_mysql_sock_connect(srv, pconf);
122 static void mod_authn_mysql_sock_release(server *srv, plugin_config *pconf) {
123 UNUSED(srv);
124 UNUSED(pconf);
125 /*(empty; leave db connection open)*/
126 /* Note: mod_authn_mysql_result() calls mod_authn_mysql_sock_error()
127 * on error, so take that into account if making changes here.
128 * Must check if (NULL == pconf->mysql_conn) */
131 static void mod_authn_mysql_sock_error(server *srv, plugin_config *pconf) {
132 UNUSED(srv);
133 mod_authn_mysql_sock_close(pconf);
136 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);
137 static handler_t mod_authn_mysql_digest(server *srv, connection *con, void *p_d, const char *username, const char *realm, unsigned char HA1[16]);
139 INIT_FUNC(mod_authn_mysql_init) {
140 static http_auth_backend_t http_auth_backend_mysql =
141 { "mysql", mod_authn_mysql_basic, mod_authn_mysql_digest, NULL };
142 plugin_data *p = calloc(1, sizeof(*p));
144 /* register http_auth_backend_mysql */
145 http_auth_backend_mysql.p_d = p;
146 http_auth_backend_set(&http_auth_backend_mysql);
148 return p;
151 FREE_FUNC(mod_authn_mysql_free) {
152 plugin_data *p = p_d;
154 UNUSED(srv);
156 if (!p) return HANDLER_GO_ON;
158 if (p->config_storage) {
159 size_t i;
160 for (i = 0; i < srv->config_context->used; i++) {
161 plugin_config *s = p->config_storage[i];
163 if (NULL == s) continue;
165 buffer_free(s->auth_mysql_host);
166 buffer_free(s->auth_mysql_user);
167 buffer_free(s->auth_mysql_pass);
168 buffer_free(s->auth_mysql_db);
169 buffer_free(s->auth_mysql_socket);
170 buffer_free(s->auth_mysql_users_table);
171 buffer_free(s->auth_mysql_col_user);
172 buffer_free(s->auth_mysql_col_pass);
173 buffer_free(s->auth_mysql_col_realm);
175 if (s->mysql_conn) mod_authn_mysql_sock_close(s);
177 free(s);
179 free(p->config_storage);
182 free(p);
184 return HANDLER_GO_ON;
187 SETDEFAULTS_FUNC(mod_authn_mysql_set_defaults) {
188 plugin_data *p = p_d;
189 size_t i;
190 config_values_t cv[] = {
191 { "auth.backend.mysql.host", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION },
192 { "auth.backend.mysql.user", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION },
193 { "auth.backend.mysql.pass", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION },
194 { "auth.backend.mysql.db", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION },
195 { "auth.backend.mysql.port", NULL, T_CONFIG_INT, T_CONFIG_SCOPE_CONNECTION },
196 { "auth.backend.mysql.socket", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION },
197 { "auth.backend.mysql.users_table", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION },
198 { "auth.backend.mysql.col_user", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION },
199 { "auth.backend.mysql.col_pass", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION },
200 { "auth.backend.mysql.col_realm", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION },
201 { NULL, NULL, T_CONFIG_UNSET, T_CONFIG_SCOPE_UNSET }
204 p->config_storage = calloc(1, srv->config_context->used * sizeof(plugin_config *));
206 for (i = 0; i < srv->config_context->used; i++) {
207 data_config const* config = (data_config const*)srv->config_context->data[i];
208 plugin_config *s;
210 s = calloc(1, sizeof(plugin_config));
212 s->mysql_conn = NULL;
213 s->auth_mysql_host = buffer_init();
214 s->auth_mysql_user = buffer_init();
215 s->auth_mysql_pass = buffer_init();
216 s->auth_mysql_db = buffer_init();
217 s->auth_mysql_socket = buffer_init();
218 s->auth_mysql_users_table = buffer_init();
219 s->auth_mysql_col_user = buffer_init();
220 s->auth_mysql_col_pass = buffer_init();
221 s->auth_mysql_col_realm = buffer_init();
223 cv[0].destination = s->auth_mysql_host;
224 cv[1].destination = s->auth_mysql_user;
225 cv[2].destination = s->auth_mysql_pass;
226 cv[3].destination = s->auth_mysql_db;
227 cv[4].destination = &s->auth_mysql_port;
228 cv[5].destination = s->auth_mysql_socket;
229 cv[6].destination = s->auth_mysql_users_table;
230 cv[7].destination = s->auth_mysql_col_user;
231 cv[8].destination = s->auth_mysql_col_pass;
232 cv[9].destination = s->auth_mysql_col_realm;
234 p->config_storage[i] = s;
236 if (0 != config_insert_values_global(srv, config->value, cv, i == 0 ? T_CONFIG_SCOPE_SERVER : T_CONFIG_SCOPE_CONNECTION)) {
237 return HANDLER_ERROR;
240 if (!buffer_is_empty(s->auth_mysql_col_user)
241 && buffer_string_is_empty(s->auth_mysql_col_user)) {
242 log_error_write(srv, __FILE__, __LINE__, "s",
243 "auth.backend.mysql.col_user must not be blank");
244 return HANDLER_ERROR;
246 if (!buffer_is_empty(s->auth_mysql_col_pass)
247 && buffer_string_is_empty(s->auth_mysql_col_pass)) {
248 log_error_write(srv, __FILE__, __LINE__, "s",
249 "auth.backend.mysql.col_pass must not be blank");
250 return HANDLER_ERROR;
252 if (!buffer_is_empty(s->auth_mysql_col_realm)
253 && buffer_string_is_empty(s->auth_mysql_col_realm)) {
254 log_error_write(srv, __FILE__, __LINE__, "s",
255 "auth.backend.mysql.col_realm must not be blank");
256 return HANDLER_ERROR;
261 plugin_config *s = p->config_storage[0];
262 if (buffer_is_empty(s->auth_mysql_col_user)) {
263 s->auth_mysql_col_user = buffer_init_string("user");
265 if (buffer_is_empty(s->auth_mysql_col_pass)) {
266 s->auth_mysql_col_pass = buffer_init_string("password");
268 if (buffer_is_empty(s->auth_mysql_col_realm)) {
269 s->auth_mysql_col_realm = buffer_init_string("realm");
273 return HANDLER_GO_ON;
276 #define PATCH(x) \
277 p->conf.x = s->x;
278 static int mod_authn_mysql_patch_connection(server *srv, connection *con, plugin_data *p) {
279 size_t i, j;
280 plugin_config *s = p->config_storage[0];
282 PATCH(auth_mysql_host);
283 PATCH(auth_mysql_user);
284 PATCH(auth_mysql_pass);
285 PATCH(auth_mysql_db);
286 PATCH(auth_mysql_port);
287 PATCH(auth_mysql_socket);
288 PATCH(auth_mysql_users_table);
289 PATCH(auth_mysql_col_user);
290 PATCH(auth_mysql_col_pass);
291 PATCH(auth_mysql_col_realm);
293 /* skip the first, the global context */
294 for (i = 1; i < srv->config_context->used; i++) {
295 data_config *dc = (data_config *)srv->config_context->data[i];
296 s = p->config_storage[i];
298 /* condition didn't match */
299 if (!config_check_cond(srv, con, dc)) continue;
301 /* merge config */
302 for (j = 0; j < dc->value->used; j++) {
303 data_unset *du = dc->value->data[j];
305 if (buffer_is_equal_string(du->key, CONST_STR_LEN("auth.backend.mysql.host"))) {
306 PATCH(auth_mysql_host);
307 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("auth.backend.mysql.user"))) {
308 PATCH(auth_mysql_user);
309 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("auth.backend.mysql.pass"))) {
310 PATCH(auth_mysql_pass);
311 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("auth.backend.mysql.db"))) {
312 PATCH(auth_mysql_db);
313 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("auth.backend.mysql.port"))) {
314 PATCH(auth_mysql_port);
315 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("auth.backend.mysql.socket"))) {
316 PATCH(auth_mysql_socket);
317 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("auth.backend.mysql.users_table"))) {
318 PATCH(auth_mysql_users_table);
319 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("auth.backend.mysql.col_user"))) {
320 PATCH(auth_mysql_col_user);
321 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("auth.backend.mysql.col_pass"))) {
322 PATCH(auth_mysql_col_pass);
323 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("auth.backend.mysql.col_realm"))) {
324 PATCH(auth_mysql_col_realm);
329 return 0;
331 #undef PATCH
333 static int mod_authn_mysql_password_cmp(const char *userpw, unsigned long userpwlen, const char *reqpw) {
334 #if defined(HAVE_CRYPT_R) || defined(HAVE_CRYPT)
335 if (userpwlen >= 3 && userpw[0] == '$' && userpw[2] == '$') {
336 /* md5 crypt()
337 * request by Nicola Tiling <nti@w4w.net> */
338 const char *saltb = userpw+3;
339 const char *salte = strchr(saltb, '$');
340 char salt[32];
341 size_t slen = (NULL != salte) ? (size_t)(salte - saltb) : sizeof(salt);
343 if (slen < sizeof(salt)) {
344 char *crypted;
345 #if defined(HAVE_CRYPT_R)
346 struct crypt_data crypt_tmp_data;
347 crypt_tmp_data.initialized = 0;
348 #endif
349 memcpy(salt, saltb, slen);
350 salt[slen] = '\0';
352 #if defined(HAVE_CRYPT_R)
353 crypted = crypt_r(reqpw, salt, &crypt_tmp_data);
354 #else
355 crypted = crypt(reqpw, salt);
356 #endif
357 if (NULL != crypted) {
358 return strcmp(userpw, crypted);
362 else
363 #endif
364 if (32 == userpwlen) {
365 /* plain md5 */
366 li_MD5_CTX Md5Ctx;
367 unsigned char HA1[16];
368 unsigned char md5pw[16];
370 li_MD5_Init(&Md5Ctx);
371 li_MD5_Update(&Md5Ctx, (unsigned char *)reqpw, strlen(reqpw));
372 li_MD5_Final(HA1, &Md5Ctx);
374 /*(compare 16-byte MD5 binary instead of converting to hex strings
375 * in order to then have to do case-insensitive hex str comparison)*/
376 return (0 == http_auth_md5_hex2bin(userpw, 32 /*(userpwlen)*/, md5pw))
377 ? memcmp(HA1, md5pw, sizeof(md5pw))
378 : -1;
381 return -1;
384 static int mod_authn_mysql_result(server *srv, plugin_data *p, const char *pw, unsigned char HA1[16]) {
385 MYSQL_RES *result = mysql_store_result(p->conf.mysql_conn);
386 int rc = -1;
387 my_ulonglong num_rows;
389 if (NULL == result) {
390 /*(future: might log mysql_error() string)*/
391 #if 0
392 log_error_write(srv, __FILE__, __LINE__, "ss", "mysql_store_result:",
393 mysql_error(p->conf.mysql_conn));
394 #endif
395 mod_authn_mysql_sock_error(srv, &p->conf);
396 return -1;
399 num_rows = mysql_num_rows(result);
400 if (1 == num_rows) {
401 MYSQL_ROW row = mysql_fetch_row(result);
402 unsigned long *lengths = mysql_fetch_lengths(result);
403 if (NULL == lengths) {
404 /*(error; should not happen)*/
406 else if (pw) { /* used with HTTP Basic auth */
407 rc = mod_authn_mysql_password_cmp(row[0], lengths[0], pw);
409 else { /* used with HTTP Digest auth */
410 rc = http_auth_md5_hex2bin(row[0], lengths[0], HA1);
413 else if (0 == num_rows) {
414 /* user,realm not found */
416 else {
417 /* (multiple rows returned, which should not happen) */
418 /* (future: might log if multiple rows returned; unexpected result) */
420 mysql_free_result(result);
421 return rc;
424 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]) {
425 plugin_data *p = (plugin_data *)p_d;
426 int rc = -1;
428 mod_authn_mysql_patch_connection(srv, con, p);
430 if (buffer_string_is_empty(p->conf.auth_mysql_users_table)) {
431 /*(auth.backend.mysql.host, auth.backend.mysql.db might be NULL; do not log)*/
432 log_error_write(srv, __FILE__, __LINE__, "sb",
433 "auth config missing auth.backend.mysql.users_table for uri:",
434 con->request.uri);
435 return HANDLER_ERROR;
438 do {
439 size_t unamelen = strlen(username);
440 size_t urealmlen = strlen(realm);
441 char q[1024], uname[512], urealm[512];
442 unsigned long mrc;
444 if (unamelen > sizeof(uname)/2-1)
445 return HANDLER_ERROR;
446 if (urealmlen > sizeof(urealm)/2-1)
447 return HANDLER_ERROR;
449 if (!mod_authn_mysql_sock_acquire(srv, &p->conf)) {
450 return HANDLER_ERROR;
453 #if 0
454 mrc = mysql_real_escape_string_quote(p->conf.mysql_conn,uname,username,
455 (unsigned long)unamelen, '\'');
456 if ((unsigned long)~0 == mrc) break;
458 mrc = mysql_real_escape_string_quote(p->conf.mysql_conn,urealm,realm,
459 (unsigned long)urealmlen, '\'');
460 if ((unsigned long)~0 == mrc) break;
461 #else
462 mrc = mysql_real_escape_string(p->conf.mysql_conn, uname,
463 username, (unsigned long)unamelen);
464 if ((unsigned long)~0 == mrc) break;
466 mrc = mysql_real_escape_string(p->conf.mysql_conn, urealm,
467 realm, (unsigned long)urealmlen);
468 if ((unsigned long)~0 == mrc) break;
469 #endif
471 rc = snprintf(q, sizeof(q),
472 "SELECT %s FROM %s WHERE %s='%s' AND %s='%s'",
473 p->conf.auth_mysql_col_pass->ptr,
474 p->conf.auth_mysql_users_table->ptr,
475 p->conf.auth_mysql_col_user->ptr,
476 uname,
477 p->conf.auth_mysql_col_realm->ptr,
478 urealm);
480 if (rc >= (int)sizeof(q)) {
481 rc = -1;
482 break;
485 /* for now we stay synchronous */
486 if (0 != mysql_query(p->conf.mysql_conn, q)) {
487 /* reconnect to db and retry once if query error occurs */
488 mod_authn_mysql_sock_error(srv, &p->conf);
489 if (!mod_authn_mysql_sock_acquire(srv, &p->conf)) {
490 rc = -1;
491 break;
493 if (0 != mysql_query(p->conf.mysql_conn, q)) {
494 /*(note: any of these params might be bufs w/ b->ptr == NULL)*/
495 log_error_write(srv, __FILE__, __LINE__, "sbsb"/*sb*/"sbssss",
496 "mysql_query host:", p->conf.auth_mysql_host,
497 "user:", p->conf.auth_mysql_user,
498 /*(omit pass from logs)*/
499 /*"pass:", p->conf.auth_mysql_pass,*/
500 "db:", p->conf.auth_mysql_db,
501 "query:", q,
502 "failed:", mysql_error(p->conf.mysql_conn));
503 rc = -1;
504 break;
508 rc = mod_authn_mysql_result(srv, p, pw, HA1);
510 } while (0);
512 mod_authn_mysql_sock_release(srv, &p->conf);
514 return (0 == rc) ? HANDLER_GO_ON : HANDLER_ERROR;
517 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) {
518 /*(HA1 is not written since pw passed should not be NULL;
519 * avoid passing NULL since subroutine expects unsigned char HA1[16] arg)*/
520 static unsigned char HA1[16];
521 char *realm = require->realm->ptr;
522 handler_t rc =mod_authn_mysql_query(srv,con,p_d,username->ptr,realm,pw,HA1);
523 if (HANDLER_GO_ON != rc) return rc;
524 return http_auth_match_rules(require, username->ptr, NULL, NULL)
525 ? HANDLER_GO_ON /* access granted */
526 : HANDLER_ERROR;
529 static handler_t mod_authn_mysql_digest(server *srv, connection *con, void *p_d, const char *username, const char *realm, unsigned char HA1[16]) {
530 return mod_authn_mysql_query(srv,con,p_d,username,realm,NULL,HA1);
533 int mod_authn_mysql_plugin_init(plugin *p);
534 int mod_authn_mysql_plugin_init(plugin *p) {
535 p->version = LIGHTTPD_VERSION_ID;
536 p->name = buffer_init_string("authn_mysql");
537 p->init = mod_authn_mysql_init;
538 p->set_defaults= mod_authn_mysql_set_defaults;
539 p->cleanup = mod_authn_mysql_free;
541 p->data = NULL;
543 return 0;
546 #else
548 int mod_authn_mysql_plugin_init(plugin *p);
549 int mod_authn_mysql_plugin_init(plugin *p) {
550 UNUSED(p);
551 return -1;
554 #endif