[mod_accesslog] %{ratio}n logs compression ratio (fixes #2133)
[lighttpd.git] / src / mod_authn_mysql.c
blobce1b79013f6b3bad1a4a73ecef039aa0e47000e0
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);
179 free(p);
181 return HANDLER_GO_ON;
184 SETDEFAULTS_FUNC(mod_authn_mysql_set_defaults) {
185 plugin_data *p = p_d;
186 size_t i;
187 config_values_t cv[] = {
188 { "auth.backend.mysql.host", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION },
189 { "auth.backend.mysql.user", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION },
190 { "auth.backend.mysql.pass", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION },
191 { "auth.backend.mysql.db", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION },
192 { "auth.backend.mysql.port", NULL, T_CONFIG_INT, T_CONFIG_SCOPE_CONNECTION },
193 { "auth.backend.mysql.socket", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION },
194 { "auth.backend.mysql.users_table", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION },
195 { "auth.backend.mysql.col_user", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION },
196 { "auth.backend.mysql.col_pass", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION },
197 { "auth.backend.mysql.col_realm", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION },
198 { NULL, NULL, T_CONFIG_UNSET, T_CONFIG_SCOPE_UNSET }
201 p->config_storage = calloc(1, srv->config_context->used * sizeof(plugin_config *));
203 for (i = 0; i < srv->config_context->used; i++) {
204 data_config const* config = (data_config const*)srv->config_context->data[i];
205 plugin_config *s;
207 s = calloc(1, sizeof(plugin_config));
209 s->mysql_conn = NULL;
210 s->auth_mysql_host = buffer_init();
211 s->auth_mysql_user = buffer_init();
212 s->auth_mysql_pass = buffer_init();
213 s->auth_mysql_db = buffer_init();
214 s->auth_mysql_socket = buffer_init();
215 s->auth_mysql_users_table = buffer_init();
216 s->auth_mysql_col_user = buffer_init();
217 s->auth_mysql_col_pass = buffer_init();
218 s->auth_mysql_col_realm = buffer_init();
220 cv[0].destination = s->auth_mysql_host;
221 cv[1].destination = s->auth_mysql_user;
222 cv[2].destination = s->auth_mysql_pass;
223 cv[3].destination = s->auth_mysql_db;
224 cv[4].destination = &s->auth_mysql_port;
225 cv[5].destination = s->auth_mysql_socket;
226 cv[6].destination = s->auth_mysql_users_table;
227 cv[7].destination = s->auth_mysql_col_user;
228 cv[8].destination = s->auth_mysql_col_pass;
229 cv[9].destination = s->auth_mysql_col_realm;
231 p->config_storage[i] = s;
233 if (0 != config_insert_values_global(srv, config->value, cv, i == 0 ? T_CONFIG_SCOPE_SERVER : T_CONFIG_SCOPE_CONNECTION)) {
234 return HANDLER_ERROR;
237 if (!buffer_is_empty(s->auth_mysql_col_user)
238 && buffer_string_is_empty(s->auth_mysql_col_user)) {
239 log_error_write(srv, __FILE__, __LINE__, "s",
240 "auth.backend.mysql.col_user must not be blank");
241 return HANDLER_ERROR;
243 if (!buffer_is_empty(s->auth_mysql_col_pass)
244 && buffer_string_is_empty(s->auth_mysql_col_pass)) {
245 log_error_write(srv, __FILE__, __LINE__, "s",
246 "auth.backend.mysql.col_pass must not be blank");
247 return HANDLER_ERROR;
249 if (!buffer_is_empty(s->auth_mysql_col_realm)
250 && buffer_string_is_empty(s->auth_mysql_col_realm)) {
251 log_error_write(srv, __FILE__, __LINE__, "s",
252 "auth.backend.mysql.col_realm must not be blank");
253 return HANDLER_ERROR;
257 if (p->config_storage[0]) { /*(always true)*/
258 plugin_config *s = p->config_storage[0];
259 if (buffer_is_empty(s->auth_mysql_col_user)) {
260 s->auth_mysql_col_user = buffer_init_string("user");
262 if (buffer_is_empty(s->auth_mysql_col_pass)) {
263 s->auth_mysql_col_pass = buffer_init_string("password");
265 if (buffer_is_empty(s->auth_mysql_col_realm)) {
266 s->auth_mysql_col_realm = buffer_init_string("realm");
270 return HANDLER_GO_ON;
273 #define PATCH(x) \
274 p->conf.x = s->x;
275 static int mod_authn_mysql_patch_connection(server *srv, connection *con, plugin_data *p) {
276 size_t i, j;
277 plugin_config *s = p->config_storage[0];
279 PATCH(auth_mysql_host);
280 PATCH(auth_mysql_user);
281 PATCH(auth_mysql_pass);
282 PATCH(auth_mysql_db);
283 PATCH(auth_mysql_port);
284 PATCH(auth_mysql_socket);
285 PATCH(auth_mysql_users_table);
286 PATCH(auth_mysql_col_user);
287 PATCH(auth_mysql_col_pass);
288 PATCH(auth_mysql_col_realm);
290 /* skip the first, the global context */
291 for (i = 1; i < srv->config_context->used; i++) {
292 data_config *dc = (data_config *)srv->config_context->data[i];
293 s = p->config_storage[i];
295 /* condition didn't match */
296 if (!config_check_cond(srv, con, dc)) continue;
298 /* merge config */
299 for (j = 0; j < dc->value->used; j++) {
300 data_unset *du = dc->value->data[j];
302 if (buffer_is_equal_string(du->key, CONST_STR_LEN("auth.backend.mysql.host"))) {
303 PATCH(auth_mysql_host);
304 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("auth.backend.mysql.user"))) {
305 PATCH(auth_mysql_user);
306 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("auth.backend.mysql.pass"))) {
307 PATCH(auth_mysql_pass);
308 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("auth.backend.mysql.db"))) {
309 PATCH(auth_mysql_db);
310 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("auth.backend.mysql.port"))) {
311 PATCH(auth_mysql_port);
312 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("auth.backend.mysql.socket"))) {
313 PATCH(auth_mysql_socket);
314 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("auth.backend.mysql.users_table"))) {
315 PATCH(auth_mysql_users_table);
316 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("auth.backend.mysql.col_user"))) {
317 PATCH(auth_mysql_col_user);
318 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("auth.backend.mysql.col_pass"))) {
319 PATCH(auth_mysql_col_pass);
320 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("auth.backend.mysql.col_realm"))) {
321 PATCH(auth_mysql_col_realm);
326 return 0;
328 #undef PATCH
330 static int mod_authn_mysql_password_cmp(const char *userpw, unsigned long userpwlen, const char *reqpw) {
331 #if defined(HAVE_CRYPT_R) || defined(HAVE_CRYPT)
332 if (userpwlen >= 3 && userpw[0] == '$' && userpw[2] == '$') {
333 /* md5 crypt()
334 * request by Nicola Tiling <nti@w4w.net> */
335 const char *saltb = userpw+3;
336 const char *salte = strchr(saltb, '$');
337 char salt[32];
338 size_t slen = (NULL != salte) ? (size_t)(salte - saltb) : sizeof(salt);
340 if (slen < sizeof(salt)) {
341 char *crypted;
342 #if defined(HAVE_CRYPT_R)
343 struct crypt_data crypt_tmp_data;
344 crypt_tmp_data.initialized = 0;
345 #endif
346 memcpy(salt, saltb, slen);
347 salt[slen] = '\0';
349 #if defined(HAVE_CRYPT_R)
350 crypted = crypt_r(reqpw, salt, &crypt_tmp_data);
351 #else
352 crypted = crypt(reqpw, salt);
353 #endif
354 if (NULL != crypted) {
355 return strcmp(userpw, crypted);
359 else
360 #endif
361 if (32 == userpwlen) {
362 /* plain md5 */
363 li_MD5_CTX Md5Ctx;
364 unsigned char HA1[16];
365 unsigned char md5pw[16];
367 li_MD5_Init(&Md5Ctx);
368 li_MD5_Update(&Md5Ctx, (unsigned char *)reqpw, strlen(reqpw));
369 li_MD5_Final(HA1, &Md5Ctx);
371 /*(compare 16-byte MD5 binary instead of converting to hex strings
372 * in order to then have to do case-insensitive hex str comparison)*/
373 return (0 == http_auth_md5_hex2bin(userpw, 32 /*(userpwlen)*/, md5pw))
374 ? memcmp(HA1, md5pw, sizeof(md5pw))
375 : -1;
378 return -1;
381 static int mod_authn_mysql_result(server *srv, plugin_data *p, const char *pw, unsigned char HA1[16]) {
382 MYSQL_RES *result = mysql_store_result(p->conf.mysql_conn);
383 int rc = -1;
384 my_ulonglong num_rows;
386 if (NULL == result) {
387 /*(future: might log mysql_error() string)*/
388 #if 0
389 log_error_write(srv, __FILE__, __LINE__, "ss", "mysql_store_result:",
390 mysql_error(p->conf.mysql_conn));
391 #endif
392 mod_authn_mysql_sock_error(srv, &p->conf);
393 return -1;
396 num_rows = mysql_num_rows(result);
397 if (1 == num_rows) {
398 MYSQL_ROW row = mysql_fetch_row(result);
399 unsigned long *lengths = mysql_fetch_lengths(result);
400 if (NULL == lengths) {
401 /*(error; should not happen)*/
403 else if (pw) { /* used with HTTP Basic auth */
404 rc = mod_authn_mysql_password_cmp(row[0], lengths[0], pw);
406 else { /* used with HTTP Digest auth */
407 rc = http_auth_md5_hex2bin(row[0], lengths[0], HA1);
410 else if (0 == num_rows) {
411 /* user,realm not found */
413 else {
414 /* (multiple rows returned, which should not happen) */
415 /* (future: might log if multiple rows returned; unexpected result) */
417 mysql_free_result(result);
418 return rc;
421 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]) {
422 plugin_data *p = (plugin_data *)p_d;
423 int rc = -1;
425 mod_authn_mysql_patch_connection(srv, con, p);
427 if (buffer_string_is_empty(p->conf.auth_mysql_users_table)) {
428 /*(auth.backend.mysql.host, auth.backend.mysql.db might be NULL; do not log)*/
429 log_error_write(srv, __FILE__, __LINE__, "sb",
430 "auth config missing auth.backend.mysql.users_table for uri:",
431 con->request.uri);
432 return HANDLER_ERROR;
435 do {
436 size_t unamelen = strlen(username);
437 size_t urealmlen = strlen(realm);
438 char q[1024], uname[512], urealm[512];
439 unsigned long mrc;
441 if (unamelen > sizeof(uname)/2-1)
442 return HANDLER_ERROR;
443 if (urealmlen > sizeof(urealm)/2-1)
444 return HANDLER_ERROR;
446 if (!mod_authn_mysql_sock_acquire(srv, &p->conf)) {
447 return HANDLER_ERROR;
450 #if 0
451 mrc = mysql_real_escape_string_quote(p->conf.mysql_conn,uname,username,
452 (unsigned long)unamelen, '\'');
453 if ((unsigned long)~0 == mrc) break;
455 mrc = mysql_real_escape_string_quote(p->conf.mysql_conn,urealm,realm,
456 (unsigned long)urealmlen, '\'');
457 if ((unsigned long)~0 == mrc) break;
458 #else
459 mrc = mysql_real_escape_string(p->conf.mysql_conn, uname,
460 username, (unsigned long)unamelen);
461 if ((unsigned long)~0 == mrc) break;
463 mrc = mysql_real_escape_string(p->conf.mysql_conn, urealm,
464 realm, (unsigned long)urealmlen);
465 if ((unsigned long)~0 == mrc) break;
466 #endif
468 rc = snprintf(q, sizeof(q),
469 "SELECT %s FROM %s WHERE %s='%s' AND %s='%s'",
470 p->conf.auth_mysql_col_pass->ptr,
471 p->conf.auth_mysql_users_table->ptr,
472 p->conf.auth_mysql_col_user->ptr,
473 uname,
474 p->conf.auth_mysql_col_realm->ptr,
475 urealm);
477 if (rc >= (int)sizeof(q)) {
478 rc = -1;
479 break;
482 /* for now we stay synchronous */
483 if (0 != mysql_query(p->conf.mysql_conn, q)) {
484 /* reconnect to db and retry once if query error occurs */
485 mod_authn_mysql_sock_error(srv, &p->conf);
486 if (!mod_authn_mysql_sock_acquire(srv, &p->conf)) {
487 rc = -1;
488 break;
490 if (0 != mysql_query(p->conf.mysql_conn, q)) {
491 /*(note: any of these params might be bufs w/ b->ptr == NULL)*/
492 log_error_write(srv, __FILE__, __LINE__, "sbsb"/*sb*/"sbssss",
493 "mysql_query host:", p->conf.auth_mysql_host,
494 "user:", p->conf.auth_mysql_user,
495 /*(omit pass from logs)*/
496 /*"pass:", p->conf.auth_mysql_pass,*/
497 "db:", p->conf.auth_mysql_db,
498 "query:", q,
499 "failed:", mysql_error(p->conf.mysql_conn));
500 rc = -1;
501 break;
505 rc = mod_authn_mysql_result(srv, p, pw, HA1);
507 } while (0);
509 mod_authn_mysql_sock_release(srv, &p->conf);
511 return (0 == rc) ? HANDLER_GO_ON : HANDLER_ERROR;
514 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) {
515 /*(HA1 is not written since pw passed should not be NULL;
516 * avoid passing NULL since subroutine expects unsigned char HA1[16] arg)*/
517 static unsigned char HA1[16];
518 char *realm = require->realm->ptr;
519 handler_t rc =mod_authn_mysql_query(srv,con,p_d,username->ptr,realm,pw,HA1);
520 if (HANDLER_GO_ON != rc) return rc;
521 return http_auth_match_rules(require, username->ptr, NULL, NULL)
522 ? HANDLER_GO_ON /* access granted */
523 : HANDLER_ERROR;
526 static handler_t mod_authn_mysql_digest(server *srv, connection *con, void *p_d, const char *username, const char *realm, unsigned char HA1[16]) {
527 return mod_authn_mysql_query(srv,con,p_d,username,realm,NULL,HA1);
530 int mod_authn_mysql_plugin_init(plugin *p);
531 int mod_authn_mysql_plugin_init(plugin *p) {
532 p->version = LIGHTTPD_VERSION_ID;
533 p->name = buffer_init_string("authn_mysql");
534 p->init = mod_authn_mysql_init;
535 p->set_defaults= mod_authn_mysql_set_defaults;
536 p->cleanup = mod_authn_mysql_free;
538 p->data = NULL;
540 return 0;