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"
39 buffer
*mysql_conn_host
;
40 buffer
*mysql_conn_user
;
41 buffer
*mysql_conn_pass
;
42 buffer
*mysql_conn_db
;
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
;
58 plugin_config
**config_storage
;
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
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
;
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
);
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
) {
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
) {
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
);
148 FREE_FUNC(mod_authn_mysql_free
) {
149 plugin_data
*p
= p_d
;
153 if (!p
) return HANDLER_GO_ON
;
155 if (p
->config_storage
) {
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
);
176 free(p
->config_storage
);
181 return HANDLER_GO_ON
;
184 SETDEFAULTS_FUNC(mod_authn_mysql_set_defaults
) {
185 plugin_data
*p
= p_d
;
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
];
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
;
275 static int mod_authn_mysql_patch_connection(server
*srv
, connection
*con
, plugin_data
*p
) {
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;
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
);
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] == '$') {
334 * request by Nicola Tiling <nti@w4w.net> */
335 const char *saltb
= userpw
+3;
336 const char *salte
= strchr(saltb
, '$');
338 size_t slen
= (NULL
!= salte
) ? (size_t)(salte
- saltb
) : sizeof(salt
);
340 if (slen
< sizeof(salt
)) {
342 #if defined(HAVE_CRYPT_R)
343 struct crypt_data crypt_tmp_data
;
344 crypt_tmp_data
.initialized
= 0;
346 memcpy(salt
, saltb
, slen
);
349 #if defined(HAVE_CRYPT_R)
350 crypted
= crypt_r(reqpw
, salt
, &crypt_tmp_data
);
352 crypted
= crypt(reqpw
, salt
);
354 if (NULL
!= crypted
) {
355 return strcmp(userpw
, crypted
);
361 if (32 == userpwlen
) {
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
))
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
);
384 my_ulonglong num_rows
;
386 if (NULL
== result
) {
387 /*(future: might log mysql_error() string)*/
389 log_error_write(srv
, __FILE__
, __LINE__
, "ss", "mysql_store_result:",
390 mysql_error(p
->conf
.mysql_conn
));
392 mod_authn_mysql_sock_error(srv
, &p
->conf
);
396 num_rows
= mysql_num_rows(result
);
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 */
414 /* (multiple rows returned, which should not happen) */
415 /* (future: might log if multiple rows returned; unexpected result) */
417 mysql_free_result(result
);
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
;
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:",
432 return HANDLER_ERROR
;
436 size_t unamelen
= strlen(username
);
437 size_t urealmlen
= strlen(realm
);
438 char q
[1024], uname
[512], urealm
[512];
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
;
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;
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;
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
,
474 p
->conf
.auth_mysql_col_realm
->ptr
,
477 if (rc
>= (int)sizeof(q
)) {
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
)) {
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
,
499 "failed:", mysql_error(p
->conf
.mysql_conn
));
505 rc
= mod_authn_mysql_result(srv
, p
, pw
, HA1
);
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 */
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
;