1 /* mod_authn_mysql
2  *
3  * KNOWN LIMITATIONS:
4  * - no mechanism provided to configure SSL connection to a remote MySQL db
5  *
6  * FUTURE POTENTIAL PERFORMANCE ENHANCEMENTS:
7  * - database response is not cached
8  *   TODO: db response caching (for limited time) to reduce load on db
9  *     (only cache successful logins to prevent cache bloat?)
10  *     (or limit number of entries (size) of cache)
11  *     (maybe have negative cache (limited size) of names not found in database)
12  * - database query is synchronous and blocks waiting for response
13  *   TODO: https://mariadb.com/kb/en/mariadb/using-the-non-blocking-library/
14  * - opens and closes connection to MySQL db for each request (inefficient)
15  *   (fixed) one-element cache for persistent connection open to last used db
16  *   TODO: db connection pool (if asynchronous requests)
17  */
18 #include "first.h"
19 
20 #if defined(HAVE_CRYPT_R) || defined(HAVE_CRYPT)
21 #ifndef _XOPEN_CRYPT
22 #define _XOPEN_CRYPT 1
23 #endif
24 #include <unistd.h>     /* crypt() */
25 #endif
26 #ifdef HAVE_CRYPT_H
27 #include <crypt.h>
28 #endif
29 
30 #include <stdlib.h>
31 #include <string.h>
32 
33 #include <mysql.h>
34 
35 #include "mod_auth_api.h"
36 #include "sys-crypto-md.h"
37 
38 #include "base.h"
39 #include "ck.h"
40 #include "log.h"
41 #include "plugin.h"
42 
43 typedef struct {
44     int auth_mysql_port;
45     const char *auth_mysql_host;
46     const char *auth_mysql_user;
47     const char *auth_mysql_pass;
48     const char *auth_mysql_db;
49     const char *auth_mysql_socket;
50     const char *auth_mysql_users_table;
51     const char *auth_mysql_col_user;
52     const char *auth_mysql_col_pass;
53     const char *auth_mysql_col_realm;
54     log_error_st *errh;
55 } plugin_config;
56 
57 typedef struct {
58     PLUGIN_DATA;
59     plugin_config defaults;
60     plugin_config conf;
61 
62     MYSQL *mysql_conn;
63     const char *mysql_conn_host;
64     const char *mysql_conn_user;
65     const char *mysql_conn_pass;
66     const char *mysql_conn_db;
67     int mysql_conn_port;
68 } plugin_data;
69 
mod_authn_mysql_sock_close(void * p_d)70 static void mod_authn_mysql_sock_close(void *p_d) {
71     plugin_data * const p = p_d;
72     if (NULL != p->mysql_conn) {
73         mysql_close(p->mysql_conn);
74         p->mysql_conn = NULL;
75     }
76 }
77 
mod_authn_mysql_sock_connect(plugin_data * p)78 static MYSQL * mod_authn_mysql_sock_connect(plugin_data *p) {
79     plugin_config * const pconf = &p->conf;
80     if (NULL != p->mysql_conn) {
81         /* reuse open db connection if same ptrs to host user pass db port */
82         if (   p->mysql_conn_host == pconf->auth_mysql_host
83             && p->mysql_conn_user == pconf->auth_mysql_user
84             && p->mysql_conn_pass == pconf->auth_mysql_pass
85             && p->mysql_conn_db   == pconf->auth_mysql_db
86             && p->mysql_conn_port == pconf->auth_mysql_port) {
87             return p->mysql_conn;
88         }
89         mod_authn_mysql_sock_close(p);
90     }
91 
92     /* !! mysql_init() is not thread safe !! (see MySQL doc) */
93     p->mysql_conn = mysql_init(NULL);
94     if (mysql_real_connect(p->mysql_conn,
95                            pconf->auth_mysql_host,
96                            pconf->auth_mysql_user,
97                            pconf->auth_mysql_pass,
98                            pconf->auth_mysql_db,
99                            pconf->auth_mysql_port,
100                            (pconf->auth_mysql_socket && *pconf->auth_mysql_socket)
101                              ? pconf->auth_mysql_socket
102                              : NULL,
103                            CLIENT_IGNORE_SIGPIPE)) {
104         /* (copy ptrs to plugin data (has lifetime until server shutdown)) */
105         p->mysql_conn_host = pconf->auth_mysql_host;
106         p->mysql_conn_user = pconf->auth_mysql_user;
107         p->mysql_conn_pass = pconf->auth_mysql_pass;
108         p->mysql_conn_db   = pconf->auth_mysql_db;
109         p->mysql_conn_port = pconf->auth_mysql_port;
110         return p->mysql_conn;
111     }
112     else {
113         /*(note: any of these params might be NULL)*/
114         log_error(pconf->errh, __FILE__, __LINE__,
115           "opening connection to mysql: %s user: %s db: %s failed: %s",
116           pconf->auth_mysql_host ? pconf->auth_mysql_host : "",
117           pconf->auth_mysql_user ? pconf->auth_mysql_user : "",
118           /*"pass:",*//*(omit pass from logs)*/
119           /*p->conf.auth_mysql_pass ? p->conf.auth_mysql_pass : "",*/
120           pconf->auth_mysql_db ? pconf->auth_mysql_db : "",
121           mysql_error(p->mysql_conn));
122         mod_authn_mysql_sock_close(p);
123         return NULL;
124     }
125 }
126 
mod_authn_mysql_sock_acquire(plugin_data * p)127 static MYSQL * mod_authn_mysql_sock_acquire(plugin_data *p) {
128     return mod_authn_mysql_sock_connect(p);
129 }
130 
mod_authn_mysql_sock_release(plugin_data * p)131 static void mod_authn_mysql_sock_release(plugin_data *p) {
132     UNUSED(p);
133     /*(empty; leave db connection open)*/
134     /* Note: mod_authn_mysql_result() calls mod_authn_mysql_sock_error()
135      *       on error, so take that into account if making changes here.
136      *       Must check if (NULL == p->mysql_conn) */
137 }
138 
139 __attribute_cold__
mod_authn_mysql_sock_error(plugin_data * p)140 static void mod_authn_mysql_sock_error(plugin_data *p) {
141     mod_authn_mysql_sock_close(p);
142 }
143 
144 static handler_t mod_authn_mysql_basic(request_st *r, void *p_d, const http_auth_require_t *require, const buffer *username, const char *pw);
145 static handler_t mod_authn_mysql_digest(request_st *r, void *p_d, http_auth_info_t *dig);
146 
INIT_FUNC(mod_authn_mysql_init)147 INIT_FUNC(mod_authn_mysql_init) {
148     static http_auth_backend_t http_auth_backend_mysql =
149       { "mysql", mod_authn_mysql_basic, mod_authn_mysql_digest, NULL };
150     plugin_data *p = calloc(1, sizeof(*p));
151 
152     /* register http_auth_backend_mysql */
153     http_auth_backend_mysql.p_d = p;
154     http_auth_backend_set(&http_auth_backend_mysql);
155 
156     return p;
157 }
158 
mod_authn_mysql_merge_config_cpv(plugin_config * const pconf,const config_plugin_value_t * const cpv)159 static void mod_authn_mysql_merge_config_cpv(plugin_config * const pconf, const config_plugin_value_t * const cpv) {
160     switch (cpv->k_id) { /* index into static config_plugin_keys_t cpk[] */
161       case 0: /* auth.backend.mysql.host */
162         pconf->auth_mysql_host = cpv->v.b->ptr;
163         break;
164       case 1: /* auth.backend.mysql.user */
165         pconf->auth_mysql_user = cpv->v.b->ptr;
166         break;
167       case 2: /* auth.backend.mysql.pass */
168         pconf->auth_mysql_pass = cpv->v.b->ptr;
169         break;
170       case 3: /* auth.backend.mysql.db */
171         pconf->auth_mysql_db = cpv->v.b->ptr;
172         break;
173       case 4: /* auth.backend.mysql.port */
174         pconf->auth_mysql_port = (int)cpv->v.shrt;
175         break;
176       case 5: /* auth.backend.mysql.socket */
177         pconf->auth_mysql_socket = cpv->v.b->ptr;
178         break;
179       case 6: /* auth.backend.mysql.users_table */
180         pconf->auth_mysql_users_table = cpv->v.b->ptr;
181         break;
182       case 7: /* auth.backend.mysql.col_user */
183         pconf->auth_mysql_col_user = cpv->v.b->ptr;
184         break;
185       case 8: /* auth.backend.mysql.col_pass */
186         pconf->auth_mysql_col_pass = cpv->v.b->ptr;
187         break;
188       case 9: /* auth.backend.mysql.col_realm */
189         pconf->auth_mysql_col_realm = cpv->v.b->ptr;
190         break;
191       default:/* should not happen */
192         return;
193     }
194 }
195 
mod_authn_mysql_merge_config(plugin_config * const pconf,const config_plugin_value_t * cpv)196 static void mod_authn_mysql_merge_config(plugin_config * const pconf, const config_plugin_value_t *cpv) {
197     do {
198         mod_authn_mysql_merge_config_cpv(pconf, cpv);
199     } while ((++cpv)->k_id != -1);
200 }
201 
mod_authn_mysql_patch_config(request_st * const r,plugin_data * const p)202 static void mod_authn_mysql_patch_config(request_st * const r, plugin_data * const p) {
203     memcpy(&p->conf, &p->defaults, sizeof(plugin_config));
204     for (int i = 1, used = p->nconfig; i < used; ++i) {
205         if (config_check_cond(r, (uint32_t)p->cvlist[i].k_id))
206             mod_authn_mysql_merge_config(&p->conf,
207                                         p->cvlist + p->cvlist[i].v.u2[0]);
208     }
209 }
210 
SETDEFAULTS_FUNC(mod_authn_mysql_set_defaults)211 SETDEFAULTS_FUNC(mod_authn_mysql_set_defaults) {
212     static const config_plugin_keys_t cpk[] = {
213       { CONST_STR_LEN("auth.backend.mysql.host"),
214         T_CONFIG_STRING,
215         T_CONFIG_SCOPE_CONNECTION }
216      ,{ CONST_STR_LEN("auth.backend.mysql.user"),
217         T_CONFIG_STRING,
218         T_CONFIG_SCOPE_CONNECTION }
219      ,{ CONST_STR_LEN("auth.backend.mysql.pass"),
220         T_CONFIG_STRING,
221         T_CONFIG_SCOPE_CONNECTION }
222      ,{ CONST_STR_LEN("auth.backend.mysql.db"),
223         T_CONFIG_STRING,
224         T_CONFIG_SCOPE_CONNECTION }
225      ,{ CONST_STR_LEN("auth.backend.mysql.port"),
226         T_CONFIG_SHORT,
227         T_CONFIG_SCOPE_CONNECTION }
228      ,{ CONST_STR_LEN("auth.backend.mysql.socket"),
229         T_CONFIG_STRING,
230         T_CONFIG_SCOPE_CONNECTION }
231      ,{ CONST_STR_LEN("auth.backend.mysql.users_table"),
232         T_CONFIG_STRING,
233         T_CONFIG_SCOPE_CONNECTION }
234      ,{ CONST_STR_LEN("auth.backend.mysql.col_user"),
235         T_CONFIG_STRING,
236         T_CONFIG_SCOPE_CONNECTION }
237      ,{ CONST_STR_LEN("auth.backend.mysql.col_pass"),
238         T_CONFIG_STRING,
239         T_CONFIG_SCOPE_CONNECTION }
240      ,{ CONST_STR_LEN("auth.backend.mysql.col_realm"),
241         T_CONFIG_STRING,
242         T_CONFIG_SCOPE_CONNECTION }
243      ,{ NULL, 0,
244         T_CONFIG_UNSET,
245         T_CONFIG_SCOPE_UNSET }
246     };
247 
248     plugin_data * const p = p_d;
249     if (!config_plugin_values_init(srv, p, cpk, "mod_authn_mysql"))
250         return HANDLER_ERROR;
251 
252     /* process and validate config directives
253      * (init i to 0 if global context; to 1 to skip empty global context) */
254     for (int i = !p->cvlist[0].v.u2[1]; i < p->nconfig; ++i) {
255         config_plugin_value_t *cpv = p->cvlist + p->cvlist[i].v.u2[0];
256         for (; -1 != cpv->k_id; ++cpv) {
257             switch (cpv->k_id) {
258               case 0: /* auth.backend.mysql.host */
259               case 1: /* auth.backend.mysql.user */
260               case 2: /* auth.backend.mysql.pass */
261               case 3: /* auth.backend.mysql.db */
262               case 4: /* auth.backend.mysql.port */
263               case 5: /* auth.backend.mysql.socket */
264               case 6: /* auth.backend.mysql.users_table */
265                 break;
266               case 7: /* auth.backend.mysql.col_user */
267               case 8: /* auth.backend.mysql.col_pass */
268               case 9: /* auth.backend.mysql.col_realm */
269                 if (buffer_is_blank(cpv->v.b)) {
270                     log_error(srv->errh, __FILE__, __LINE__,
271                       "%s must not be blank", cpk[cpv->k_id].k);
272                     return HANDLER_ERROR;
273                 }
274                 break;
275               default:/* should not happen */
276                 break;
277             }
278         }
279     }
280 
281     p->defaults.auth_mysql_col_user = "user";
282     p->defaults.auth_mysql_col_pass = "password";
283     p->defaults.auth_mysql_col_realm = "realm";
284     p->defaults.errh = srv->errh;
285 
286     /* initialize p->defaults from global config context */
287     if (p->nconfig > 0 && p->cvlist->v.u2[1]) {
288         const config_plugin_value_t *cpv = p->cvlist + p->cvlist->v.u2[0];
289         if (-1 != cpv->k_id)
290             mod_authn_mysql_merge_config(&p->defaults, cpv);
291     }
292 
293     log_error(srv->errh, __FILE__, __LINE__,
294       "Warning: mod_%s is deprecated "
295       "and will be removed from a future lighttpd release in early 2022. "
296       "https://wiki.lighttpd.net/Docs_ModAuth#mysql-mod_authn_mysql-since-lighttpd-1442",
297       p->self->name);
298 
299     return HANDLER_GO_ON;
300 }
301 
mod_authn_mysql_password_cmp(const char * userpw,unsigned long userpwlen,const char * reqpw)302 static int mod_authn_mysql_password_cmp(const char *userpw, unsigned long userpwlen, const char *reqpw) {
303   #if defined(HAVE_CRYPT_R) || defined(HAVE_CRYPT)
304     if (userpwlen >= 3 && userpw[0] == '$') {
305         char *crypted = crypt(reqpw, userpw);
306         size_t crypwlen = (NULL != crypted) ? strlen(crypted) : 0;
307         int rc = (crypwlen == userpwlen) ? memcmp(crypted, userpw, crypwlen) : -1;
308         if (crypwlen >= 13) ck_memzero(crypted, crypwlen);
309         return rc;
310     }
311     else
312   #endif
313     if (32 == userpwlen) {
314         /* plain md5 */
315         unsigned char HA1[MD5_DIGEST_LENGTH];
316         unsigned char md5pw[MD5_DIGEST_LENGTH];
317         MD5_once(HA1, reqpw, strlen(reqpw));
318 
319         /*(compare 16-byte MD5 binary instead of converting to hex strings
320          * in order to then have to do case-insensitive hex str comparison)*/
321         return (0 == li_hex2bin(md5pw, sizeof(md5pw), userpw, 32))
322           ? ck_memeq_const_time_fixed_len(HA1, md5pw, sizeof(md5pw)) ? 0 : 1
323           : -1;
324     }
325 
326     return -1;
327 }
328 
mod_authn_mysql_result(plugin_data * p,http_auth_info_t * ai,const char * pw)329 static int mod_authn_mysql_result(plugin_data *p, http_auth_info_t *ai, const char *pw) {
330     MYSQL_RES *result = mysql_store_result(p->mysql_conn);
331     int rc = -1;
332     my_ulonglong num_rows;
333 
334     if (NULL == result) {
335         /*(future: might log mysql_error() string)*/
336       #if 0
337         log_error(errh, __FILE__, __LINE__,
338           "mysql_store_result: %s", mysql_error(p->mysql_conn));
339       #endif
340         mod_authn_mysql_sock_error(p);
341         return -1;
342     }
343 
344     num_rows = mysql_num_rows(result);
345     if (1 == num_rows) {
346         MYSQL_ROW row = mysql_fetch_row(result);
347         unsigned long *lengths = mysql_fetch_lengths(result);
348         if (NULL == lengths) {
349             /*(error; should not happen)*/
350         }
351         else if (pw) {  /* used with HTTP Basic auth */
352             rc = mod_authn_mysql_password_cmp(row[0], lengths[0], pw);
353         }
354         else {          /* used with HTTP Digest auth */
355             /*(currently supports only single row, single digest algorithm)*/
356             if (lengths[0] == (ai->dlen << 1)) {
357                 rc = li_hex2bin(ai->digest, sizeof(ai->digest),
358                                 row[0], lengths[0]);
359             }
360         }
361     }
362     else if (0 == num_rows) {
363         /* user,realm not found */
364     }
365     else {
366         /* (multiple rows returned, which should not happen) */
367         /* (future: might log if multiple rows returned; unexpected result) */
368     }
369     mysql_free_result(result);
370     return rc;
371 }
372 
mod_authn_mysql_query(request_st * const r,void * p_d,http_auth_info_t * const ai,const char * const pw)373 static handler_t mod_authn_mysql_query(request_st * const r, void *p_d, http_auth_info_t * const ai, const char * const pw) {
374     plugin_data *p = (plugin_data *)p_d;
375     int rc = -1;
376 
377     mod_authn_mysql_patch_config(r, p);
378     p->conf.errh = r->conf.errh;
379 
380     if (NULL == p->conf.auth_mysql_users_table) {
381         /*(auth.backend.mysql.host, auth.backend.mysql.db might be NULL; do not log)*/
382         log_error(r->conf.errh, __FILE__, __LINE__,
383           "auth config missing auth.backend.mysql.users_table for uri: %s",
384           r->target.ptr);
385         return HANDLER_ERROR;
386     }
387 
388     do {
389         char uname[512], urealm[512];
390         unsigned long mrc;
391 
392         if (ai->ulen > sizeof(uname)/2-1)
393             return HANDLER_ERROR;
394         if (ai->rlen > sizeof(urealm)/2-1)
395             return HANDLER_ERROR;
396 
397         if (!mod_authn_mysql_sock_acquire(p)) {
398             return HANDLER_ERROR;
399         }
400 
401       #if 0
402         mrc = mysql_real_escape_string_quote(p->mysql_conn, uname,
403                                              ai->username, ai->ulen, '\'');
404         if ((unsigned long)~0 == mrc) break;
405 
406         mrc = mysql_real_escape_string_quote(p->mysql_conn, urealm,
407                                              ai->realm, ai->rlen, '\'');
408         if ((unsigned long)~0 == mrc) break;
409       #else
410         mrc = mysql_real_escape_string(p->mysql_conn, uname,
411                                        ai->username, ai->ulen);
412         if ((unsigned long)~0 == mrc) break;
413 
414         mrc = mysql_real_escape_string(p->mysql_conn, urealm,
415                                        ai->realm, ai->rlen);
416         if ((unsigned long)~0 == mrc) break;
417       #endif
418 
419         buffer * const tb = r->tmp_buf;
420         buffer_clear(tb);
421         struct const_iovec iov[] = {
422           { CONST_STR_LEN("SELECT ") }
423          ,{ p->conf.auth_mysql_col_pass, strlen(p->conf.auth_mysql_col_pass) }
424          ,{ CONST_STR_LEN(" FROM ") }
425          ,{ p->conf.auth_mysql_users_table, strlen(p->conf.auth_mysql_users_table) }
426          ,{ CONST_STR_LEN(" WHERE ") }
427          ,{ p->conf.auth_mysql_col_user, strlen(p->conf.auth_mysql_col_user) }
428          ,{ CONST_STR_LEN("='") }
429          ,{ uname, strlen(uname) }
430          ,{ CONST_STR_LEN("' AND ") }
431          ,{ p->conf.auth_mysql_col_realm, strlen(p->conf.auth_mysql_col_realm) }
432          ,{ CONST_STR_LEN("='") }
433          ,{ urealm, strlen(urealm) }
434          ,{ CONST_STR_LEN("'") }
435         };
436         buffer_append_iovec(tb, iov, sizeof(iov)/sizeof(*iov));
437         char * const q = tb->ptr;
438 
439         /* for now we stay synchronous */
440         if (0 != mysql_query(p->mysql_conn, q)) {
441             /* reconnect to db and retry once if query error occurs */
442             mod_authn_mysql_sock_error(p);
443             if (!mod_authn_mysql_sock_acquire(p)) {
444                 rc = -1;
445                 break;
446             }
447             if (0 != mysql_query(p->mysql_conn, q)) {
448                 /*(note: any of these params might be bufs w/ b->ptr == NULL)*/
449                 log_error(r->conf.errh, __FILE__, __LINE__,
450                   "mysql_query host: %s user: %s db: %s query: %s failed: %s",
451                   p->conf.auth_mysql_host ? p->conf.auth_mysql_host : "",
452                   p->conf.auth_mysql_user ? p->conf.auth_mysql_user : "",
453                   /*"pass:",*//*(omit pass from logs)*/
454                   /*p->conf.auth_mysql_pass ? p->conf.auth_mysql_pass : "",*/
455                   p->conf.auth_mysql_db ? p->conf.auth_mysql_db : "",
456                   q, mysql_error(p->mysql_conn));
457                 rc = -1;
458                 break;
459             }
460         }
461 
462         rc = mod_authn_mysql_result(p, ai, pw);
463 
464     } while (0);
465 
466     mod_authn_mysql_sock_release(p);
467 
468     return (0 == rc) ? HANDLER_GO_ON : HANDLER_ERROR;
469 }
470 
mod_authn_mysql_basic(request_st * const r,void * p_d,const http_auth_require_t * const require,const buffer * const username,const char * const pw)471 static handler_t mod_authn_mysql_basic(request_st * const r, void *p_d, const http_auth_require_t * const require, const buffer * const username, const char * const pw) {
472     handler_t rc;
473     http_auth_info_t ai;
474     ai.dalgo    = HTTP_AUTH_DIGEST_NONE;
475     ai.dlen     = 0;
476     ai.username = username->ptr;
477     ai.ulen     = buffer_clen(username);
478     ai.realm    = require->realm->ptr;
479     ai.rlen     = buffer_clen(require->realm);
480     ai.userhash = 0;
481     rc = mod_authn_mysql_query(r, p_d, &ai, pw);
482     if (HANDLER_GO_ON != rc) return rc;
483     return http_auth_match_rules(require, username->ptr, NULL, NULL)
484       ? HANDLER_GO_ON  /* access granted */
485       : HANDLER_ERROR;
486 }
487 
mod_authn_mysql_digest(request_st * const r,void * p_d,http_auth_info_t * const ai)488 static handler_t mod_authn_mysql_digest(request_st * const r, void *p_d, http_auth_info_t * const ai) {
489     return mod_authn_mysql_query(r, p_d, ai, NULL);
490 }
491 
492 int mod_authn_mysql_plugin_init(plugin *p);
mod_authn_mysql_plugin_init(plugin * p)493 int mod_authn_mysql_plugin_init(plugin *p) {
494     p->version     = LIGHTTPD_VERSION_ID;
495     p->name        = "authn_mysql";
496     p->init        = mod_authn_mysql_init;
497     p->set_defaults= mod_authn_mysql_set_defaults;
498     p->cleanup     = mod_authn_mysql_sock_close;
499 
500     return 0;
501 }
502