[mod_cgi] skip local-redir handling if to self (fixes #2779, #2108)
[lighttpd.git] / src / mod_authn_mysql.c
blob10f905869dc227354561d5babc5ba0ab47bc3263
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 "server.h"
24 #include "http_auth.h"
25 #include "log.h"
26 #include "md5.h"
27 #include "plugin.h"
29 #include <ctype.h>
30 #include <errno.h>
31 #include <string.h>
33 #ifdef HAVE_CRYPT_H
34 #include <crypt.h>
35 #endif
37 typedef struct {
38 MYSQL *mysql_conn;
39 buffer *mysql_conn_host;
40 buffer *mysql_conn_user;
41 buffer *mysql_conn_pass;
42 buffer *mysql_conn_db;
43 int mysql_conn_port;
44 int auth_mysql_port;
45 buffer *auth_mysql_host;
46 buffer *auth_mysql_user;
47 buffer *auth_mysql_pass;
48 buffer *auth_mysql_db;
49 buffer *auth_mysql_socket;
50 buffer *auth_mysql_users_table;
51 buffer *auth_mysql_col_user;
52 buffer *auth_mysql_col_pass;
53 buffer *auth_mysql_col_realm;
54 } plugin_config;
56 typedef struct {
57 PLUGIN_DATA;
58 plugin_config **config_storage;
59 plugin_config conf;
60 } plugin_data;
62 static void mod_authn_mysql_sock_close(plugin_config *pconf) {
63 if (NULL != pconf->mysql_conn) {
64 mysql_close(pconf->mysql_conn);
65 pconf->mysql_conn = NULL;
69 static MYSQL * mod_authn_mysql_sock_connect(server *srv, plugin_config *pconf) {
70 if (NULL != pconf->mysql_conn) {
71 /* reuse open db connection if same ptrs to host user pass db port */
72 if ( pconf->mysql_conn_host == pconf->auth_mysql_host
73 && pconf->mysql_conn_user == pconf->auth_mysql_user
74 && pconf->mysql_conn_pass == pconf->auth_mysql_pass
75 && pconf->mysql_conn_db == pconf->auth_mysql_db
76 && pconf->mysql_conn_port == pconf->auth_mysql_port) {
77 return pconf->mysql_conn;
79 mod_authn_mysql_sock_close(pconf);
82 /* !! mysql_init() is not thread safe !! (see MySQL doc) */
83 pconf->mysql_conn = mysql_init(NULL);
84 if (mysql_real_connect(pconf->mysql_conn,
85 pconf->auth_mysql_host->ptr,
86 pconf->auth_mysql_user->ptr,
87 pconf->auth_mysql_pass->ptr,
88 pconf->auth_mysql_db->ptr,
89 pconf->auth_mysql_port,
90 !buffer_string_is_empty(pconf->auth_mysql_socket)
91 ? pconf->auth_mysql_socket->ptr
92 : NULL,
93 CLIENT_IGNORE_SIGPIPE)) {
94 /* (copy ptrs to config data (has lifetime until server shutdown)) */
95 pconf->mysql_conn_host = pconf->auth_mysql_host;
96 pconf->mysql_conn_user = pconf->auth_mysql_user;
97 pconf->mysql_conn_pass = pconf->auth_mysql_pass;
98 pconf->mysql_conn_db = pconf->auth_mysql_db;
99 pconf->mysql_conn_port = pconf->auth_mysql_port;
100 return pconf->mysql_conn;
102 else {
103 /*(note: any of these params might be buffers with b->ptr == NULL)*/
104 log_error_write(srv, __FILE__, __LINE__, "sbsb"/*sb*/"sbss",
105 "opening connection to mysql:", pconf->auth_mysql_host,
106 "user:", pconf->auth_mysql_user,
107 /*"pass:", pconf->auth_mysql_pass,*//*(omit from logs)*/
108 "db:", pconf->auth_mysql_db,
109 "failed:", mysql_error(pconf->mysql_conn));
110 mod_authn_mysql_sock_close(pconf);
111 return NULL;
115 static MYSQL * mod_authn_mysql_sock_acquire(server *srv, plugin_config *pconf) {
116 return mod_authn_mysql_sock_connect(srv, pconf);
119 static void mod_authn_mysql_sock_release(server *srv, plugin_config *pconf) {
120 UNUSED(srv);
121 UNUSED(pconf);
122 /*(empty; leave db connection open)*/
123 /* Note: mod_authn_mysql_result() calls mod_authn_mysql_sock_error()
124 * on error, so take that into account if making changes here.
125 * Must check if (NULL == pconf->mysql_conn) */
128 static void mod_authn_mysql_sock_error(server *srv, plugin_config *pconf) {
129 UNUSED(srv);
130 mod_authn_mysql_sock_close(pconf);
133 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);
134 static handler_t mod_authn_mysql_digest(server *srv, connection *con, void *p_d, const char *username, const char *realm, unsigned char HA1[16]);
136 INIT_FUNC(mod_authn_mysql_init) {
137 static http_auth_backend_t http_auth_backend_mysql =
138 { "mysql", mod_authn_mysql_basic, mod_authn_mysql_digest, NULL };
139 plugin_data *p = calloc(1, sizeof(*p));
141 /* register http_auth_backend_mysql */
142 http_auth_backend_mysql.p_d = p;
143 http_auth_backend_set(&http_auth_backend_mysql);
145 return p;
148 FREE_FUNC(mod_authn_mysql_free) {
149 plugin_data *p = p_d;
151 UNUSED(srv);
153 if (!p) return HANDLER_GO_ON;
155 if (p->config_storage) {
156 size_t i;
157 for (i = 0; i < srv->config_context->used; i++) {
158 plugin_config *s = p->config_storage[i];
160 if (NULL == s) continue;
162 buffer_free(s->auth_mysql_host);
163 buffer_free(s->auth_mysql_user);
164 buffer_free(s->auth_mysql_pass);
165 buffer_free(s->auth_mysql_db);
166 buffer_free(s->auth_mysql_socket);
167 buffer_free(s->auth_mysql_users_table);
168 buffer_free(s->auth_mysql_col_user);
169 buffer_free(s->auth_mysql_col_pass);
170 buffer_free(s->auth_mysql_col_realm);
172 if (s->mysql_conn) mod_authn_mysql_sock_close(s);
174 free(s);
176 free(p->config_storage);
178 mod_authn_mysql_sock_close(&p->conf);
180 free(p);
182 return HANDLER_GO_ON;
185 SETDEFAULTS_FUNC(mod_authn_mysql_set_defaults) {
186 plugin_data *p = p_d;
187 size_t i;
188 config_values_t cv[] = {
189 { "auth.backend.mysql.host", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION },
190 { "auth.backend.mysql.user", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION },
191 { "auth.backend.mysql.pass", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION },
192 { "auth.backend.mysql.db", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION },
193 { "auth.backend.mysql.port", NULL, T_CONFIG_INT, T_CONFIG_SCOPE_CONNECTION },
194 { "auth.backend.mysql.socket", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION },
195 { "auth.backend.mysql.users_table", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION },
196 { "auth.backend.mysql.col_user", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION },
197 { "auth.backend.mysql.col_pass", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION },
198 { "auth.backend.mysql.col_realm", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION },
199 { NULL, NULL, T_CONFIG_UNSET, T_CONFIG_SCOPE_UNSET }
202 p->config_storage = calloc(1, srv->config_context->used * sizeof(plugin_config *));
204 for (i = 0; i < srv->config_context->used; i++) {
205 data_config const* config = (data_config const*)srv->config_context->data[i];
206 plugin_config *s;
208 s = calloc(1, sizeof(plugin_config));
210 s->mysql_conn = NULL;
211 s->auth_mysql_host = buffer_init();
212 s->auth_mysql_user = buffer_init();
213 s->auth_mysql_pass = buffer_init();
214 s->auth_mysql_db = buffer_init();
215 s->auth_mysql_socket = buffer_init();
216 s->auth_mysql_users_table = buffer_init();
217 s->auth_mysql_col_user = buffer_init();
218 s->auth_mysql_col_pass = buffer_init();
219 s->auth_mysql_col_realm = buffer_init();
221 cv[0].destination = s->auth_mysql_host;
222 cv[1].destination = s->auth_mysql_user;
223 cv[2].destination = s->auth_mysql_pass;
224 cv[3].destination = s->auth_mysql_db;
225 cv[4].destination = &s->auth_mysql_port;
226 cv[5].destination = s->auth_mysql_socket;
227 cv[6].destination = s->auth_mysql_users_table;
228 cv[7].destination = s->auth_mysql_col_user;
229 cv[8].destination = s->auth_mysql_col_pass;
230 cv[9].destination = s->auth_mysql_col_realm;
232 p->config_storage[i] = s;
234 if (0 != config_insert_values_global(srv, config->value, cv, i == 0 ? T_CONFIG_SCOPE_SERVER : T_CONFIG_SCOPE_CONNECTION)) {
235 return HANDLER_ERROR;
238 if (!buffer_is_empty(s->auth_mysql_col_user)
239 && buffer_string_is_empty(s->auth_mysql_col_user)) {
240 log_error_write(srv, __FILE__, __LINE__, "s",
241 "auth.backend.mysql.col_user must not be blank");
242 return HANDLER_ERROR;
244 if (!buffer_is_empty(s->auth_mysql_col_pass)
245 && buffer_string_is_empty(s->auth_mysql_col_pass)) {
246 log_error_write(srv, __FILE__, __LINE__, "s",
247 "auth.backend.mysql.col_pass must not be blank");
248 return HANDLER_ERROR;
250 if (!buffer_is_empty(s->auth_mysql_col_realm)
251 && buffer_string_is_empty(s->auth_mysql_col_realm)) {
252 log_error_write(srv, __FILE__, __LINE__, "s",
253 "auth.backend.mysql.col_realm must not be blank");
254 return HANDLER_ERROR;
258 if (p->config_storage[0]) { /*(always true)*/
259 plugin_config *s = p->config_storage[0];
260 if (buffer_is_empty(s->auth_mysql_col_user)) {
261 s->auth_mysql_col_user = buffer_init_string("user");
263 if (buffer_is_empty(s->auth_mysql_col_pass)) {
264 s->auth_mysql_col_pass = buffer_init_string("password");
266 if (buffer_is_empty(s->auth_mysql_col_realm)) {
267 s->auth_mysql_col_realm = buffer_init_string("realm");
271 return HANDLER_GO_ON;
274 #define PATCH(x) \
275 p->conf.x = s->x;
276 static int mod_authn_mysql_patch_connection(server *srv, connection *con, plugin_data *p) {
277 size_t i, j;
278 plugin_config *s = p->config_storage[0];
280 PATCH(auth_mysql_host);
281 PATCH(auth_mysql_user);
282 PATCH(auth_mysql_pass);
283 PATCH(auth_mysql_db);
284 PATCH(auth_mysql_port);
285 PATCH(auth_mysql_socket);
286 PATCH(auth_mysql_users_table);
287 PATCH(auth_mysql_col_user);
288 PATCH(auth_mysql_col_pass);
289 PATCH(auth_mysql_col_realm);
291 /* skip the first, the global context */
292 for (i = 1; i < srv->config_context->used; i++) {
293 data_config *dc = (data_config *)srv->config_context->data[i];
294 s = p->config_storage[i];
296 /* condition didn't match */
297 if (!config_check_cond(srv, con, dc)) continue;
299 /* merge config */
300 for (j = 0; j < dc->value->used; j++) {
301 data_unset *du = dc->value->data[j];
303 if (buffer_is_equal_string(du->key, CONST_STR_LEN("auth.backend.mysql.host"))) {
304 PATCH(auth_mysql_host);
305 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("auth.backend.mysql.user"))) {
306 PATCH(auth_mysql_user);
307 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("auth.backend.mysql.pass"))) {
308 PATCH(auth_mysql_pass);
309 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("auth.backend.mysql.db"))) {
310 PATCH(auth_mysql_db);
311 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("auth.backend.mysql.port"))) {
312 PATCH(auth_mysql_port);
313 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("auth.backend.mysql.socket"))) {
314 PATCH(auth_mysql_socket);
315 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("auth.backend.mysql.users_table"))) {
316 PATCH(auth_mysql_users_table);
317 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("auth.backend.mysql.col_user"))) {
318 PATCH(auth_mysql_col_user);
319 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("auth.backend.mysql.col_pass"))) {
320 PATCH(auth_mysql_col_pass);
321 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("auth.backend.mysql.col_realm"))) {
322 PATCH(auth_mysql_col_realm);
327 return 0;
329 #undef PATCH
331 static int mod_authn_mysql_password_cmp(const char *userpw, unsigned long userpwlen, const char *reqpw) {
332 #if defined(HAVE_CRYPT_R) || defined(HAVE_CRYPT)
333 if (userpwlen >= 3 && userpw[0] == '$' && userpw[2] == '$') {
334 /* md5 crypt()
335 * request by Nicola Tiling <nti@w4w.net> */
336 const char *saltb = userpw+3;
337 const char *salte = strchr(saltb, '$');
338 char salt[32];
339 size_t slen = (NULL != salte) ? (size_t)(salte - saltb) : sizeof(salt);
341 if (slen < sizeof(salt)) {
342 char *crypted;
343 #if defined(HAVE_CRYPT_R)
344 struct crypt_data crypt_tmp_data;
345 #ifdef _AIX
346 memset(&crypt_tmp_data, 0, sizeof(crypt_tmp_data));
347 #else
348 crypt_tmp_data.initialized = 0;
349 #endif
350 #endif
351 memcpy(salt, saltb, slen);
352 salt[slen] = '\0';
354 #if defined(HAVE_CRYPT_R)
355 crypted = crypt_r(reqpw, salt, &crypt_tmp_data);
356 #else
357 crypted = crypt(reqpw, salt);
358 #endif
359 if (NULL != crypted) {
360 return strcmp(userpw, crypted);
364 else
365 #endif
366 if (32 == userpwlen) {
367 /* plain md5 */
368 li_MD5_CTX Md5Ctx;
369 unsigned char HA1[16];
370 unsigned char md5pw[16];
372 li_MD5_Init(&Md5Ctx);
373 li_MD5_Update(&Md5Ctx, (unsigned char *)reqpw, strlen(reqpw));
374 li_MD5_Final(HA1, &Md5Ctx);
376 /*(compare 16-byte MD5 binary instead of converting to hex strings
377 * in order to then have to do case-insensitive hex str comparison)*/
378 return (0 == http_auth_md5_hex2bin(userpw, 32 /*(userpwlen)*/, md5pw))
379 ? memcmp(HA1, md5pw, sizeof(md5pw))
380 : -1;
383 return -1;
386 static int mod_authn_mysql_result(server *srv, plugin_data *p, const char *pw, unsigned char HA1[16]) {
387 MYSQL_RES *result = mysql_store_result(p->conf.mysql_conn);
388 int rc = -1;
389 my_ulonglong num_rows;
391 if (NULL == result) {
392 /*(future: might log mysql_error() string)*/
393 #if 0
394 log_error_write(srv, __FILE__, __LINE__, "ss", "mysql_store_result:",
395 mysql_error(p->conf.mysql_conn));
396 #endif
397 mod_authn_mysql_sock_error(srv, &p->conf);
398 return -1;
401 num_rows = mysql_num_rows(result);
402 if (1 == num_rows) {
403 MYSQL_ROW row = mysql_fetch_row(result);
404 unsigned long *lengths = mysql_fetch_lengths(result);
405 if (NULL == lengths) {
406 /*(error; should not happen)*/
408 else if (pw) { /* used with HTTP Basic auth */
409 rc = mod_authn_mysql_password_cmp(row[0], lengths[0], pw);
411 else { /* used with HTTP Digest auth */
412 rc = http_auth_md5_hex2bin(row[0], lengths[0], HA1);
415 else if (0 == num_rows) {
416 /* user,realm not found */
418 else {
419 /* (multiple rows returned, which should not happen) */
420 /* (future: might log if multiple rows returned; unexpected result) */
422 mysql_free_result(result);
423 return rc;
426 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]) {
427 plugin_data *p = (plugin_data *)p_d;
428 int rc = -1;
430 mod_authn_mysql_patch_connection(srv, con, p);
432 if (buffer_string_is_empty(p->conf.auth_mysql_users_table)) {
433 /*(auth.backend.mysql.host, auth.backend.mysql.db might be NULL; do not log)*/
434 log_error_write(srv, __FILE__, __LINE__, "sb",
435 "auth config missing auth.backend.mysql.users_table for uri:",
436 con->request.uri);
437 return HANDLER_ERROR;
440 do {
441 size_t unamelen = strlen(username);
442 size_t urealmlen = strlen(realm);
443 char q[1024], uname[512], urealm[512];
444 unsigned long mrc;
446 if (unamelen > sizeof(uname)/2-1)
447 return HANDLER_ERROR;
448 if (urealmlen > sizeof(urealm)/2-1)
449 return HANDLER_ERROR;
451 if (!mod_authn_mysql_sock_acquire(srv, &p->conf)) {
452 return HANDLER_ERROR;
455 #if 0
456 mrc = mysql_real_escape_string_quote(p->conf.mysql_conn,uname,username,
457 (unsigned long)unamelen, '\'');
458 if ((unsigned long)~0 == mrc) break;
460 mrc = mysql_real_escape_string_quote(p->conf.mysql_conn,urealm,realm,
461 (unsigned long)urealmlen, '\'');
462 if ((unsigned long)~0 == mrc) break;
463 #else
464 mrc = mysql_real_escape_string(p->conf.mysql_conn, uname,
465 username, (unsigned long)unamelen);
466 if ((unsigned long)~0 == mrc) break;
468 mrc = mysql_real_escape_string(p->conf.mysql_conn, urealm,
469 realm, (unsigned long)urealmlen);
470 if ((unsigned long)~0 == mrc) break;
471 #endif
473 rc = snprintf(q, sizeof(q),
474 "SELECT %s FROM %s WHERE %s='%s' AND %s='%s'",
475 p->conf.auth_mysql_col_pass->ptr,
476 p->conf.auth_mysql_users_table->ptr,
477 p->conf.auth_mysql_col_user->ptr,
478 uname,
479 p->conf.auth_mysql_col_realm->ptr,
480 urealm);
482 if (rc >= (int)sizeof(q)) {
483 rc = -1;
484 break;
487 /* for now we stay synchronous */
488 if (0 != mysql_query(p->conf.mysql_conn, q)) {
489 /* reconnect to db and retry once if query error occurs */
490 mod_authn_mysql_sock_error(srv, &p->conf);
491 if (!mod_authn_mysql_sock_acquire(srv, &p->conf)) {
492 rc = -1;
493 break;
495 if (0 != mysql_query(p->conf.mysql_conn, q)) {
496 /*(note: any of these params might be bufs w/ b->ptr == NULL)*/
497 log_error_write(srv, __FILE__, __LINE__, "sbsb"/*sb*/"sbssss",
498 "mysql_query host:", p->conf.auth_mysql_host,
499 "user:", p->conf.auth_mysql_user,
500 /*(omit pass from logs)*/
501 /*"pass:", p->conf.auth_mysql_pass,*/
502 "db:", p->conf.auth_mysql_db,
503 "query:", q,
504 "failed:", mysql_error(p->conf.mysql_conn));
505 rc = -1;
506 break;
510 rc = mod_authn_mysql_result(srv, p, pw, HA1);
512 } while (0);
514 mod_authn_mysql_sock_release(srv, &p->conf);
516 return (0 == rc) ? HANDLER_GO_ON : HANDLER_ERROR;
519 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) {
520 /*(HA1 is not written since pw passed should not be NULL;
521 * avoid passing NULL since subroutine expects unsigned char HA1[16] arg)*/
522 static unsigned char HA1[16];
523 char *realm = require->realm->ptr;
524 handler_t rc =mod_authn_mysql_query(srv,con,p_d,username->ptr,realm,pw,HA1);
525 if (HANDLER_GO_ON != rc) return rc;
526 return http_auth_match_rules(require, username->ptr, NULL, NULL)
527 ? HANDLER_GO_ON /* access granted */
528 : HANDLER_ERROR;
531 static handler_t mod_authn_mysql_digest(server *srv, connection *con, void *p_d, const char *username, const char *realm, unsigned char HA1[16]) {
532 return mod_authn_mysql_query(srv,con,p_d,username,realm,NULL,HA1);
535 int mod_authn_mysql_plugin_init(plugin *p);
536 int mod_authn_mysql_plugin_init(plugin *p) {
537 p->version = LIGHTTPD_VERSION_ID;
538 p->name = buffer_init_string("authn_mysql");
539 p->init = mod_authn_mysql_init;
540 p->set_defaults= mod_authn_mysql_set_defaults;
541 p->cleanup = mod_authn_mysql_free;
543 p->data = NULL;
545 return 0;