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"
33 #if defined(HAVE_CRYPT_R) || defined(HAVE_CRYPT)
34 #include <unistd.h> /* crypt() */
42 buffer
*mysql_conn_host
;
43 buffer
*mysql_conn_user
;
44 buffer
*mysql_conn_pass
;
45 buffer
*mysql_conn_db
;
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
;
61 plugin_config
**config_storage
;
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
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
;
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
);
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
) {
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
) {
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
);
151 FREE_FUNC(mod_authn_mysql_free
) {
152 plugin_data
*p
= p_d
;
156 if (!p
) return HANDLER_GO_ON
;
158 if (p
->config_storage
) {
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
);
179 free(p
->config_storage
);
181 mod_authn_mysql_sock_close(&p
->conf
);
185 return HANDLER_GO_ON
;
188 SETDEFAULTS_FUNC(mod_authn_mysql_set_defaults
) {
189 plugin_data
*p
= p_d
;
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
];
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
;
279 static int mod_authn_mysql_patch_connection(server
*srv
, connection
*con
, plugin_data
*p
) {
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;
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
);
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] == '$') {
338 * request by Nicola Tiling <nti@w4w.net> */
339 const char *saltb
= userpw
+3;
340 const char *salte
= strchr(saltb
, '$');
342 size_t slen
= (NULL
!= salte
) ? (size_t)(salte
- saltb
) : sizeof(salt
);
344 if (slen
< sizeof(salt
)) {
346 #if defined(HAVE_CRYPT_R)
347 struct crypt_data crypt_tmp_data
;
349 memset(&crypt_tmp_data
, 0, sizeof(crypt_tmp_data
));
351 crypt_tmp_data
.initialized
= 0;
354 memcpy(salt
, saltb
, slen
);
357 #if defined(HAVE_CRYPT_R)
358 crypted
= crypt_r(reqpw
, salt
, &crypt_tmp_data
);
360 crypted
= crypt(reqpw
, salt
);
362 if (NULL
!= crypted
) {
363 return strcmp(userpw
, crypted
);
369 if (32 == userpwlen
) {
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
))
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
);
392 my_ulonglong num_rows
;
394 if (NULL
== result
) {
395 /*(future: might log mysql_error() string)*/
397 log_error_write(srv
, __FILE__
, __LINE__
, "ss", "mysql_store_result:",
398 mysql_error(p
->conf
.mysql_conn
));
400 mod_authn_mysql_sock_error(srv
, &p
->conf
);
404 num_rows
= mysql_num_rows(result
);
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 */
422 /* (multiple rows returned, which should not happen) */
423 /* (future: might log if multiple rows returned; unexpected result) */
425 mysql_free_result(result
);
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
;
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:",
440 return HANDLER_ERROR
;
444 size_t unamelen
= strlen(username
);
445 size_t urealmlen
= strlen(realm
);
446 char q
[1024], uname
[512], urealm
[512];
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
;
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;
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;
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
,
482 p
->conf
.auth_mysql_col_realm
->ptr
,
485 if (rc
>= (int)sizeof(q
)) {
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
)) {
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
,
507 "failed:", mysql_error(p
->conf
.mysql_conn
));
513 rc
= mod_authn_mysql_result(srv
, p
, pw
, HA1
);
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 */
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
;