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)
24 #include "http_auth.h"
34 #if defined(HAVE_CRYPT_R) || defined(HAVE_CRYPT)
35 #include <unistd.h> /* crypt() */
43 buffer
*mysql_conn_host
;
44 buffer
*mysql_conn_user
;
45 buffer
*mysql_conn_pass
;
46 buffer
*mysql_conn_db
;
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
;
62 plugin_config
**config_storage
;
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
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
;
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
);
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
) {
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
) {
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
);
152 FREE_FUNC(mod_authn_mysql_free
) {
153 plugin_data
*p
= p_d
;
157 if (!p
) return HANDLER_GO_ON
;
159 if (p
->config_storage
) {
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
);
180 free(p
->config_storage
);
182 mod_authn_mysql_sock_close(&p
->conf
);
186 return HANDLER_GO_ON
;
189 SETDEFAULTS_FUNC(mod_authn_mysql_set_defaults
) {
190 plugin_data
*p
= p_d
;
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
];
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
;
280 static int mod_authn_mysql_patch_connection(server
*srv
, connection
*con
, plugin_data
*p
) {
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;
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
);
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] == '$') {
339 * request by Nicola Tiling <nti@w4w.net> */
340 const char *saltb
= userpw
+3;
341 const char *salte
= strchr(saltb
, '$');
343 size_t slen
= (NULL
!= salte
) ? (size_t)(salte
- saltb
) : sizeof(salt
);
345 if (slen
< sizeof(salt
)) {
347 #if defined(HAVE_CRYPT_R)
348 struct crypt_data crypt_tmp_data
;
350 memset(&crypt_tmp_data
, 0, sizeof(crypt_tmp_data
));
352 crypt_tmp_data
.initialized
= 0;
355 memcpy(salt
, saltb
, slen
);
358 #if defined(HAVE_CRYPT_R)
359 crypted
= crypt_r(reqpw
, salt
, &crypt_tmp_data
);
361 crypted
= crypt(reqpw
, salt
);
363 if (NULL
!= crypted
) {
364 return strcmp(userpw
, crypted
);
370 if (32 == userpwlen
) {
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
))
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
);
393 my_ulonglong num_rows
;
395 if (NULL
== result
) {
396 /*(future: might log mysql_error() string)*/
398 log_error_write(srv
, __FILE__
, __LINE__
, "ss", "mysql_store_result:",
399 mysql_error(p
->conf
.mysql_conn
));
401 mod_authn_mysql_sock_error(srv
, &p
->conf
);
405 num_rows
= mysql_num_rows(result
);
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 */
427 /* (multiple rows returned, which should not happen) */
428 /* (future: might log if multiple rows returned; unexpected result) */
430 mysql_free_result(result
);
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
;
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:",
445 return HANDLER_ERROR
;
449 char q
[1024], uname
[512], urealm
[512];
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
;
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;
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;
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
,
485 p
->conf
.auth_mysql_col_realm
->ptr
,
488 if (rc
>= (int)sizeof(q
)) {
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
)) {
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
,
510 "failed:", mysql_error(p
->conf
.mysql_conn
));
516 rc
= mod_authn_mysql_result(srv
, p
, ai
, pw
);
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
) {
528 ai
.dalgo
= HTTP_AUTH_DIGEST_NONE
;
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 */
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
;