[autobuild] allow sendfile() in cross-compile (fixes #2836)
[lighttpd.git] / src / mod_authn_mysql.c
blob4a2871173659d7b97aca166e0242525fd2c2eddb
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 <errno.h>
30 #include <stdio.h>
31 #include <string.h>
33 #if defined(HAVE_CRYPT_R) || defined(HAVE_CRYPT)
34 #include <unistd.h> /* crypt() */
35 #endif
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);
181 mod_authn_mysql_sock_close(&p->conf);
183 free(p);
185 return HANDLER_GO_ON;
188 SETDEFAULTS_FUNC(mod_authn_mysql_set_defaults) {
189 plugin_data *p = p_d;
190 size_t i;
191 config_values_t cv[] = {
192 { "auth.backend.mysql.host", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION },
193 { "auth.backend.mysql.user", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION },
194 { "auth.backend.mysql.pass", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION },
195 { "auth.backend.mysql.db", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION },
196 { "auth.backend.mysql.port", NULL, T_CONFIG_INT, T_CONFIG_SCOPE_CONNECTION },
197 { "auth.backend.mysql.socket", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION },
198 { "auth.backend.mysql.users_table", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION },
199 { "auth.backend.mysql.col_user", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION },
200 { "auth.backend.mysql.col_pass", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION },
201 { "auth.backend.mysql.col_realm", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION },
202 { NULL, NULL, T_CONFIG_UNSET, T_CONFIG_SCOPE_UNSET }
205 p->config_storage = calloc(1, srv->config_context->used * sizeof(plugin_config *));
207 for (i = 0; i < srv->config_context->used; i++) {
208 data_config const* config = (data_config const*)srv->config_context->data[i];
209 plugin_config *s;
211 s = calloc(1, sizeof(plugin_config));
213 s->mysql_conn = NULL;
214 s->auth_mysql_host = buffer_init();
215 s->auth_mysql_user = buffer_init();
216 s->auth_mysql_pass = buffer_init();
217 s->auth_mysql_db = buffer_init();
218 s->auth_mysql_socket = buffer_init();
219 s->auth_mysql_users_table = buffer_init();
220 s->auth_mysql_col_user = buffer_init();
221 s->auth_mysql_col_pass = buffer_init();
222 s->auth_mysql_col_realm = buffer_init();
224 cv[0].destination = s->auth_mysql_host;
225 cv[1].destination = s->auth_mysql_user;
226 cv[2].destination = s->auth_mysql_pass;
227 cv[3].destination = s->auth_mysql_db;
228 cv[4].destination = &s->auth_mysql_port;
229 cv[5].destination = s->auth_mysql_socket;
230 cv[6].destination = s->auth_mysql_users_table;
231 cv[7].destination = s->auth_mysql_col_user;
232 cv[8].destination = s->auth_mysql_col_pass;
233 cv[9].destination = s->auth_mysql_col_realm;
235 p->config_storage[i] = s;
237 if (0 != config_insert_values_global(srv, config->value, cv, i == 0 ? T_CONFIG_SCOPE_SERVER : T_CONFIG_SCOPE_CONNECTION)) {
238 return HANDLER_ERROR;
241 if (!buffer_is_empty(s->auth_mysql_col_user)
242 && buffer_string_is_empty(s->auth_mysql_col_user)) {
243 log_error_write(srv, __FILE__, __LINE__, "s",
244 "auth.backend.mysql.col_user must not be blank");
245 return HANDLER_ERROR;
247 if (!buffer_is_empty(s->auth_mysql_col_pass)
248 && buffer_string_is_empty(s->auth_mysql_col_pass)) {
249 log_error_write(srv, __FILE__, __LINE__, "s",
250 "auth.backend.mysql.col_pass must not be blank");
251 return HANDLER_ERROR;
253 if (!buffer_is_empty(s->auth_mysql_col_realm)
254 && buffer_string_is_empty(s->auth_mysql_col_realm)) {
255 log_error_write(srv, __FILE__, __LINE__, "s",
256 "auth.backend.mysql.col_realm must not be blank");
257 return HANDLER_ERROR;
261 if (p->config_storage[0]) { /*(always true)*/
262 plugin_config *s = p->config_storage[0];
263 if (buffer_is_empty(s->auth_mysql_col_user)) {
264 buffer_copy_string_len(s->auth_mysql_col_user, CONST_STR_LEN("user"));
266 if (buffer_is_empty(s->auth_mysql_col_pass)) {
267 buffer_copy_string_len(s->auth_mysql_col_pass, CONST_STR_LEN("password"));
269 if (buffer_is_empty(s->auth_mysql_col_realm)) {
270 buffer_copy_string_len(s->auth_mysql_col_realm, CONST_STR_LEN("realm"));
274 return HANDLER_GO_ON;
277 #define PATCH(x) \
278 p->conf.x = s->x;
279 static int mod_authn_mysql_patch_connection(server *srv, connection *con, plugin_data *p) {
280 size_t i, j;
281 plugin_config *s = p->config_storage[0];
283 PATCH(auth_mysql_host);
284 PATCH(auth_mysql_user);
285 PATCH(auth_mysql_pass);
286 PATCH(auth_mysql_db);
287 PATCH(auth_mysql_port);
288 PATCH(auth_mysql_socket);
289 PATCH(auth_mysql_users_table);
290 PATCH(auth_mysql_col_user);
291 PATCH(auth_mysql_col_pass);
292 PATCH(auth_mysql_col_realm);
294 /* skip the first, the global context */
295 for (i = 1; i < srv->config_context->used; i++) {
296 data_config *dc = (data_config *)srv->config_context->data[i];
297 s = p->config_storage[i];
299 /* condition didn't match */
300 if (!config_check_cond(srv, con, dc)) continue;
302 /* merge config */
303 for (j = 0; j < dc->value->used; j++) {
304 data_unset *du = dc->value->data[j];
306 if (buffer_is_equal_string(du->key, CONST_STR_LEN("auth.backend.mysql.host"))) {
307 PATCH(auth_mysql_host);
308 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("auth.backend.mysql.user"))) {
309 PATCH(auth_mysql_user);
310 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("auth.backend.mysql.pass"))) {
311 PATCH(auth_mysql_pass);
312 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("auth.backend.mysql.db"))) {
313 PATCH(auth_mysql_db);
314 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("auth.backend.mysql.port"))) {
315 PATCH(auth_mysql_port);
316 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("auth.backend.mysql.socket"))) {
317 PATCH(auth_mysql_socket);
318 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("auth.backend.mysql.users_table"))) {
319 PATCH(auth_mysql_users_table);
320 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("auth.backend.mysql.col_user"))) {
321 PATCH(auth_mysql_col_user);
322 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("auth.backend.mysql.col_pass"))) {
323 PATCH(auth_mysql_col_pass);
324 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("auth.backend.mysql.col_realm"))) {
325 PATCH(auth_mysql_col_realm);
330 return 0;
332 #undef PATCH
334 static int mod_authn_mysql_password_cmp(const char *userpw, unsigned long userpwlen, const char *reqpw) {
335 #if defined(HAVE_CRYPT_R) || defined(HAVE_CRYPT)
336 if (userpwlen >= 3 && userpw[0] == '$' && userpw[2] == '$') {
337 /* md5 crypt()
338 * request by Nicola Tiling <nti@w4w.net> */
339 const char *saltb = userpw+3;
340 const char *salte = strchr(saltb, '$');
341 char salt[32];
342 size_t slen = (NULL != salte) ? (size_t)(salte - saltb) : sizeof(salt);
344 if (slen < sizeof(salt)) {
345 char *crypted;
346 #if defined(HAVE_CRYPT_R)
347 struct crypt_data crypt_tmp_data;
348 #ifdef _AIX
349 memset(&crypt_tmp_data, 0, sizeof(crypt_tmp_data));
350 #else
351 crypt_tmp_data.initialized = 0;
352 #endif
353 #endif
354 memcpy(salt, saltb, slen);
355 salt[slen] = '\0';
357 #if defined(HAVE_CRYPT_R)
358 crypted = crypt_r(reqpw, salt, &crypt_tmp_data);
359 #else
360 crypted = crypt(reqpw, salt);
361 #endif
362 if (NULL != crypted) {
363 return strcmp(userpw, crypted);
367 else
368 #endif
369 if (32 == userpwlen) {
370 /* plain md5 */
371 li_MD5_CTX Md5Ctx;
372 unsigned char HA1[16];
373 unsigned char md5pw[16];
375 li_MD5_Init(&Md5Ctx);
376 li_MD5_Update(&Md5Ctx, (unsigned char *)reqpw, strlen(reqpw));
377 li_MD5_Final(HA1, &Md5Ctx);
379 /*(compare 16-byte MD5 binary instead of converting to hex strings
380 * in order to then have to do case-insensitive hex str comparison)*/
381 return (0 == http_auth_md5_hex2bin(userpw, 32 /*(userpwlen)*/, md5pw))
382 ? memcmp(HA1, md5pw, sizeof(md5pw))
383 : -1;
386 return -1;
389 static int mod_authn_mysql_result(server *srv, plugin_data *p, const char *pw, unsigned char HA1[16]) {
390 MYSQL_RES *result = mysql_store_result(p->conf.mysql_conn);
391 int rc = -1;
392 my_ulonglong num_rows;
394 if (NULL == result) {
395 /*(future: might log mysql_error() string)*/
396 #if 0
397 log_error_write(srv, __FILE__, __LINE__, "ss", "mysql_store_result:",
398 mysql_error(p->conf.mysql_conn));
399 #endif
400 mod_authn_mysql_sock_error(srv, &p->conf);
401 return -1;
404 num_rows = mysql_num_rows(result);
405 if (1 == num_rows) {
406 MYSQL_ROW row = mysql_fetch_row(result);
407 unsigned long *lengths = mysql_fetch_lengths(result);
408 if (NULL == lengths) {
409 /*(error; should not happen)*/
411 else if (pw) { /* used with HTTP Basic auth */
412 rc = mod_authn_mysql_password_cmp(row[0], lengths[0], pw);
414 else { /* used with HTTP Digest auth */
415 rc = http_auth_md5_hex2bin(row[0], lengths[0], HA1);
418 else if (0 == num_rows) {
419 /* user,realm not found */
421 else {
422 /* (multiple rows returned, which should not happen) */
423 /* (future: might log if multiple rows returned; unexpected result) */
425 mysql_free_result(result);
426 return rc;
429 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]) {
430 plugin_data *p = (plugin_data *)p_d;
431 int rc = -1;
433 mod_authn_mysql_patch_connection(srv, con, p);
435 if (buffer_string_is_empty(p->conf.auth_mysql_users_table)) {
436 /*(auth.backend.mysql.host, auth.backend.mysql.db might be NULL; do not log)*/
437 log_error_write(srv, __FILE__, __LINE__, "sb",
438 "auth config missing auth.backend.mysql.users_table for uri:",
439 con->request.uri);
440 return HANDLER_ERROR;
443 do {
444 size_t unamelen = strlen(username);
445 size_t urealmlen = strlen(realm);
446 char q[1024], uname[512], urealm[512];
447 unsigned long mrc;
449 if (unamelen > sizeof(uname)/2-1)
450 return HANDLER_ERROR;
451 if (urealmlen > sizeof(urealm)/2-1)
452 return HANDLER_ERROR;
454 if (!mod_authn_mysql_sock_acquire(srv, &p->conf)) {
455 return HANDLER_ERROR;
458 #if 0
459 mrc = mysql_real_escape_string_quote(p->conf.mysql_conn,uname,username,
460 (unsigned long)unamelen, '\'');
461 if ((unsigned long)~0 == mrc) break;
463 mrc = mysql_real_escape_string_quote(p->conf.mysql_conn,urealm,realm,
464 (unsigned long)urealmlen, '\'');
465 if ((unsigned long)~0 == mrc) break;
466 #else
467 mrc = mysql_real_escape_string(p->conf.mysql_conn, uname,
468 username, (unsigned long)unamelen);
469 if ((unsigned long)~0 == mrc) break;
471 mrc = mysql_real_escape_string(p->conf.mysql_conn, urealm,
472 realm, (unsigned long)urealmlen);
473 if ((unsigned long)~0 == mrc) break;
474 #endif
476 rc = snprintf(q, sizeof(q),
477 "SELECT %s FROM %s WHERE %s='%s' AND %s='%s'",
478 p->conf.auth_mysql_col_pass->ptr,
479 p->conf.auth_mysql_users_table->ptr,
480 p->conf.auth_mysql_col_user->ptr,
481 uname,
482 p->conf.auth_mysql_col_realm->ptr,
483 urealm);
485 if (rc >= (int)sizeof(q)) {
486 rc = -1;
487 break;
490 /* for now we stay synchronous */
491 if (0 != mysql_query(p->conf.mysql_conn, q)) {
492 /* reconnect to db and retry once if query error occurs */
493 mod_authn_mysql_sock_error(srv, &p->conf);
494 if (!mod_authn_mysql_sock_acquire(srv, &p->conf)) {
495 rc = -1;
496 break;
498 if (0 != mysql_query(p->conf.mysql_conn, q)) {
499 /*(note: any of these params might be bufs w/ b->ptr == NULL)*/
500 log_error_write(srv, __FILE__, __LINE__, "sbsb"/*sb*/"sbssss",
501 "mysql_query host:", p->conf.auth_mysql_host,
502 "user:", p->conf.auth_mysql_user,
503 /*(omit pass from logs)*/
504 /*"pass:", p->conf.auth_mysql_pass,*/
505 "db:", p->conf.auth_mysql_db,
506 "query:", q,
507 "failed:", mysql_error(p->conf.mysql_conn));
508 rc = -1;
509 break;
513 rc = mod_authn_mysql_result(srv, p, pw, HA1);
515 } while (0);
517 mod_authn_mysql_sock_release(srv, &p->conf);
519 return (0 == rc) ? HANDLER_GO_ON : HANDLER_ERROR;
522 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) {
523 /*(HA1 is not written since pw passed should not be NULL;
524 * avoid passing NULL since subroutine expects unsigned char HA1[16] arg)*/
525 static unsigned char HA1[16];
526 char *realm = require->realm->ptr;
527 handler_t rc =mod_authn_mysql_query(srv,con,p_d,username->ptr,realm,pw,HA1);
528 if (HANDLER_GO_ON != rc) return rc;
529 return http_auth_match_rules(require, username->ptr, NULL, NULL)
530 ? HANDLER_GO_ON /* access granted */
531 : HANDLER_ERROR;
534 static handler_t mod_authn_mysql_digest(server *srv, connection *con, void *p_d, const char *username, const char *realm, unsigned char HA1[16]) {
535 return mod_authn_mysql_query(srv,con,p_d,username,realm,NULL,HA1);
538 int mod_authn_mysql_plugin_init(plugin *p);
539 int mod_authn_mysql_plugin_init(plugin *p) {
540 p->version = LIGHTTPD_VERSION_ID;
541 p->name = buffer_init_string("authn_mysql");
542 p->init = mod_authn_mysql_init;
543 p->set_defaults= mod_authn_mysql_set_defaults;
544 p->cleanup = mod_authn_mysql_free;
546 p->data = NULL;
548 return 0;