1 /* 2 * mod_authn_dbi - authn backend employing DBI interface 3 * 4 * Copyright(c) 2020 Glenn Strauss gstrauss()gluelogic.com All rights reserved 5 * License: BSD 3-clause (same as lighttpd) 6 */ 7 /* 8 * authn backend employing DBI 9 * 10 * e.g. 11 * auth.backend.dbi = ( 12 * "sql" => "SELECT passwd FROM users WHERE user='?' AND realm='?'" 13 * "dbtype" => "sqlite3", 14 * "dbname" => "mydb.sqlite", 15 * "sqlite3_dbdir" => "/path/to/sqlite/dbs/" 16 * ) 17 * 18 * SQL samples (change table and column names for your database schema): 19 * "sql" => "SELECT passwd FROM users WHERE user='?'" 20 * "sql" => "SELECT passwd FROM users WHERE user='?' AND realm='?'" 21 * "sql" => "SELECT passwd FROM users WHERE user='?' AND realm='?' AND algorithm='?'" 22 * "sql" => "SELECT passwd FROM users WHERE user='?' AND realm='?' AND algorithm='?' AND group IN ('groupA','groupB','groupC')" 23 */ 24 #include "first.h" 25 26 #if defined(HAVE_CRYPT_R) || defined(HAVE_CRYPT) 27 #ifndef _XOPEN_SOURCE 28 #define _XOPEN_SOURCE 700 29 #endif 30 #ifndef _XOPEN_CRYPT 31 #define _XOPEN_CRYPT 1 32 #endif 33 #include <unistd.h> /* crypt() */ 34 #endif 35 #ifdef HAVE_CRYPT_H 36 #include <crypt.h> 37 #endif 38 39 #include <string.h> 40 #include <stdlib.h> 41 42 #include <dbi/dbi.h> 43 44 #include "mod_auth_api.h" 45 #include "sys-crypto-md.h" 46 #include "base.h" 47 #include "ck.h" 48 #include "fdevent.h" 49 #include "log.h" 50 #include "plugin.h" 51 52 typedef struct { 53 dbi_conn dbconn; 54 dbi_inst dbinst; 55 const buffer *sqlquery; 56 const buffer *sqluserhash; 57 log_error_st *errh; 58 short reconnect_count; 59 } dbi_config; 60 61 typedef struct { 62 void *vdata; 63 } plugin_config; 64 65 typedef struct { 66 PLUGIN_DATA; 67 plugin_config defaults; 68 plugin_config conf; 69 } plugin_data; 70 71 72 /* used to reconnect to the database when we get disconnected */ 73 static void 74 mod_authn_dbi_error_callback (dbi_conn dbconn, void *vdata) 75 { 76 dbi_config *dbconf = (dbi_config *)vdata; 77 const char *errormsg = NULL; 78 /*assert(dbconf->dbconn == dbconn);*/ 79 80 while (++dbconf->reconnect_count <= 3) { /* retry */ 81 if (0 == dbi_conn_connect(dbconn)) { 82 fdevent_setfd_cloexec(dbi_conn_get_socket(dbconn)); 83 return; 84 } 85 } 86 87 dbi_conn_error(dbconn, &errormsg); 88 log_error(dbconf->errh,__FILE__,__LINE__,"dbi_conn_connect(): %s",errormsg); 89 } 90 91 92 static void 93 mod_authn_dbi_dbconf_free (void *vdata) 94 { 95 dbi_config *dbconf = (dbi_config *)vdata; 96 if (!dbconf) return; 97 dbi_conn_close(dbconf->dbconn); 98 dbi_shutdown_r(dbconf->dbinst); 99 free(dbconf); 100 } 101 102 103 static int 104 mod_authn_dbi_dbconf_setup (server *srv, const array *opts, void **vdata) 105 { 106 const buffer *sqlquery = NULL; 107 const buffer *sqluserhash = NULL; 108 const buffer *dbtype=NULL, *dbname=NULL; 109 110 for (size_t i = 0; i < opts->used; ++i) { 111 const data_string *ds = (data_string *)opts->data[i]; 112 if (ds->type == TYPE_STRING) { 113 if (buffer_eq_icase_slen(&ds->key, CONST_STR_LEN("sql"))) 114 sqlquery = &ds->value; 115 else if (buffer_eq_icase_slen(&ds->key, CONST_STR_LEN("dbname"))) 116 dbname = &ds->value; 117 else if (buffer_eq_icase_slen(&ds->key, CONST_STR_LEN("dbtype"))) 118 dbtype = &ds->value; 119 else if (buffer_eq_icase_slen(&ds->key, 120 CONST_STR_LEN("sql-userhash"))) 121 sqluserhash = &ds->value; 122 } 123 } 124 125 /* required: 126 * - sql (sql query) 127 * - dbtype 128 * - dbname 129 * 130 * optional: 131 * - username, some databases don't require this (sqlite) 132 * - password, default: empty 133 * - socket, default: database type default 134 * - hostname, if set overrides socket 135 * - port, default: database default 136 * - encoding, default: database default 137 */ 138 139 if (sqlquery && !buffer_is_blank(sqlquery) && dbname && dbtype) { 140 /* create/initialise database */ 141 dbi_config *dbconf; 142 dbi_inst dbinst = NULL; 143 dbi_conn dbconn; 144 if (dbi_initialize_r(NULL, &dbinst) < 1) { 145 log_error(srv->errh, __FILE__, __LINE__, 146 "dbi_initialize_r() failed. " 147 "Do you have the DBD for this db type installed?"); 148 return -1; 149 } 150 dbconn = dbi_conn_new_r(dbtype->ptr, dbinst); 151 if (NULL == dbconn) { 152 log_error(srv->errh, __FILE__, __LINE__, 153 "dbi_conn_new_r() failed. " 154 "Do you have the DBD for this db type installed?"); 155 dbi_shutdown_r(dbinst); 156 return -1; 157 } 158 159 /* set options */ 160 for (size_t j = 0; j < opts->used; ++j) { 161 data_unset *du = opts->data[j]; 162 const buffer *opt = &du->key; 163 if (!buffer_is_blank(opt)) { 164 if (du->type == TYPE_INTEGER) { 165 data_integer *di = (data_integer *)du; 166 dbi_conn_set_option_numeric(dbconn, opt->ptr, di->value); 167 } 168 else if (du->type == TYPE_STRING) { 169 data_string *ds = (data_string *)du; 170 if (&ds->value != sqlquery && &ds->value != dbtype 171 && &ds->value != sqluserhash) { 172 dbi_conn_set_option(dbconn, opt->ptr, ds->value.ptr); 173 } 174 } 175 } 176 } 177 178 dbconf = (dbi_config *)ck_calloc(1, sizeof(*dbconf)); 179 dbconf->dbinst = dbinst; 180 dbconf->dbconn = dbconn; 181 dbconf->sqlquery = sqlquery; 182 dbconf->sqluserhash = sqluserhash; 183 dbconf->errh = srv->errh; 184 dbconf->reconnect_count = 0; 185 *vdata = dbconf; 186 187 /* used to automatically reconnect to the database */ 188 dbi_conn_error_handler(dbconn, mod_authn_dbi_error_callback, dbconf); 189 190 /* connect to database */ 191 mod_authn_dbi_error_callback(dbconn, dbconf); 192 if (dbconf->reconnect_count >= 3) { 193 mod_authn_dbi_dbconf_free(dbconf); 194 return -1; 195 } 196 } 197 198 return 0; 199 } 200 201 202 static handler_t mod_authn_dbi_basic(request_st *r, void *p_d, const http_auth_require_t *require, const buffer *username, const char *pw); 203 static handler_t mod_authn_dbi_digest(request_st *r, void *p_d, http_auth_info_t *dig); 204 205 INIT_FUNC(mod_authn_dbi_init) { 206 static http_auth_backend_t http_auth_backend_dbi = 207 { "dbi", mod_authn_dbi_basic, mod_authn_dbi_digest, NULL }; 208 plugin_data *p = ck_calloc(1, sizeof(*p)); 209 210 /* register http_auth_backend_dbi */ 211 http_auth_backend_dbi.p_d = p; 212 http_auth_backend_set(&http_auth_backend_dbi); 213 214 return p; 215 } 216 217 218 FREE_FUNC(mod_authn_dbi_cleanup) { 219 plugin_data * const p = p_d; 220 if (NULL == p->cvlist) return; 221 /* (init i to 0 if global context; to 1 to skip empty global context) */ 222 for (int i = !p->cvlist[0].v.u2[1], used = p->nconfig; i < used; ++i) { 223 config_plugin_value_t *cpv = p->cvlist + p->cvlist[i].v.u2[0]; 224 for (; -1 != cpv->k_id; ++cpv) { 225 if (cpv->vtype != T_CONFIG_LOCAL || NULL == cpv->v.v) continue; 226 switch (cpv->k_id) { 227 case 0: /* auth.backend.dbi */ 228 mod_authn_dbi_dbconf_free(cpv->v.v); 229 break; 230 default: 231 break; 232 } 233 } 234 } 235 } 236 237 238 static void 239 mod_authn_dbi_merge_config_cpv (plugin_config * const pconf, const config_plugin_value_t * const cpv) 240 { 241 switch (cpv->k_id) { /* index into static config_plugin_keys_t cpk[] */ 242 case 0: /* auth.backend.dbi */ 243 if (cpv->vtype == T_CONFIG_LOCAL) 244 pconf->vdata = cpv->v.v; 245 break; 246 default:/* should not happen */ 247 return; 248 } 249 } 250 251 252 static void 253 mod_authn_dbi_merge_config (plugin_config * const pconf, const config_plugin_value_t *cpv) 254 { 255 do { 256 mod_authn_dbi_merge_config_cpv(pconf, cpv); 257 } while ((++cpv)->k_id != -1); 258 } 259 260 261 static void 262 mod_authn_dbi_patch_config(request_st * const r, plugin_data * const p) 263 { 264 p->conf = p->defaults; /* copy small struct instead of memcpy() */ 265 /*memcpy(&p->conf, &p->defaults, sizeof(plugin_config));*/ 266 for (int i = 1, used = p->nconfig; i < used; ++i) { 267 if (config_check_cond(r, (uint32_t)p->cvlist[i].k_id)) 268 mod_authn_dbi_merge_config(&p->conf, 269 p->cvlist + p->cvlist[i].v.u2[0]); 270 } 271 } 272 273 274 SETDEFAULTS_FUNC(mod_authn_dbi_set_defaults) { 275 static const config_plugin_keys_t cpk[] = { 276 { CONST_STR_LEN("auth.backend.dbi"), 277 T_CONFIG_ARRAY_KVANY, 278 T_CONFIG_SCOPE_CONNECTION } 279 ,{ NULL, 0, 280 T_CONFIG_UNSET, 281 T_CONFIG_SCOPE_UNSET } 282 }; 283 284 plugin_data * const p = p_d; 285 if (!config_plugin_values_init(srv, p, cpk, "mod_authn_dbi")) 286 return HANDLER_ERROR; 287 288 /* process and validate config directives 289 * (init i to 0 if global context; to 1 to skip empty global context) */ 290 for (int i = !p->cvlist[0].v.u2[1]; i < p->nconfig; ++i) { 291 config_plugin_value_t *cpv = p->cvlist + p->cvlist[i].v.u2[0]; 292 for (; -1 != cpv->k_id; ++cpv) { 293 switch (cpv->k_id) { 294 case 0: /* auth.backend.dbi */ 295 if (cpv->v.a->used) { 296 if (0 != mod_authn_dbi_dbconf_setup(srv,cpv->v.a,&cpv->v.v)) 297 return HANDLER_ERROR; 298 if (NULL != cpv->v.v) 299 cpv->vtype = T_CONFIG_LOCAL; 300 } 301 break; 302 default:/* should not happen */ 303 break; 304 } 305 } 306 } 307 308 /* initialize p->defaults from global config context */ 309 if (p->nconfig > 0 && p->cvlist->v.u2[1]) { 310 const config_plugin_value_t *cpv = p->cvlist + p->cvlist->v.u2[0]; 311 if (-1 != cpv->k_id) 312 mod_authn_dbi_merge_config(&p->defaults, cpv); 313 } 314 315 return HANDLER_GO_ON; 316 } 317 318 319 /* improved and diverged from mod_authn_mysql_password_cmp() 320 * If Basic Auth and storing MD5 or SHA-256, the hash is of user:realm:passwd 321 * with unique user:realm: combination acting as salt, rather than salt-less 322 * hash of passwd as was implemented in (deprecated) mod_authn_mysql. 323 * Conveniently, hash digest of user:realm:passwd is the same value used by 324 * Digest Auth, so transition to Digest Auth is simple change in lighttpd.conf 325 * future: might move to mod_auth.c and have only mod_auth.so depend on -lcrypt 326 * references: 327 * https://en.wikipedia.org/wiki/Crypt_(C) 328 * Modular Crypt Format (MCF) 329 * https://passlib.readthedocs.io/en/stable/modular_crypt_format.html 330 * PHC string format 331 * https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md 332 * Note: (struct crypt_data) is large and might not fit on the stack 333 * On some systems it is 32k, on others 128k or more. 334 * Therefore, since lighttpd is single-threaded, simply use crypt(), 335 * which is not thread-safe but fine for single-threaded lighttpd. 336 * If crypt_r() is preferred, then safest method is to allocate from heap 337 * rather than using (struct crypt_data) on stack. 338 */ 339 #if defined(HAVE_CRYPT_R) || defined(HAVE_CRYPT) 340 static int 341 mod_authn_crypt_cmp (const char *reqpw, const char *userpw, unsigned long userpwlen) 342 { 343 #if 1 344 345 char *crypted = crypt(reqpw, userpw); 346 size_t crypwlen = (NULL != crypted) ? strlen(crypted) : 0; 347 int rc = (crypwlen == userpwlen) ? memcmp(crypted, userpw, crypwlen) : -1; 348 if (crypwlen >= 13) ck_memzero(crypted, crypwlen); 349 return rc; 350 351 #else 352 353 #if defined(HAVE_CRYPT_R) 354 #if 1 /* (must free() before returning if allocated here) */ 355 struct crypt_data *crypt_tmp_data = ck_malloc(sizeof(struct crypt_data)); 356 #else /* safe if sizeof(struct crypt_data) <= 32768 */ 357 struct crypt_data crypt_tmp_data_stack; 358 struct crypt_data *crypt_tmp_data = &crypt_tmp_data_stack; 359 #endif 360 #ifdef _AIX 361 memset(&crypt_tmp_data_stack, 0, sizeof(struct crypt_data)); 362 #else 363 crypt_tmp_data_stack.initialized = 0; 364 #endif 365 #endif 366 367 #if defined(HAVE_CRYPT_R) 368 char *crypted = crypt_r(reqpw, userpw, crypt_tmp_data); 369 #else 370 char *crypted = crypt(reqpw, userpw); 371 #endif 372 size_t crypwlen = (NULL != crypted) ? strlen(crypted) : 0; 373 int rc = (crypwlen == userpwlen) ? memcmp(crypted, userpw, crypwlen) : -1; 374 375 if (crypwlen >= 13) ck_memzero(crypted, crypwlen); 376 #if defined(HAVE_CRYPT_R) 377 #if 1 /* (must free() if allocated above) */ 378 free(crypt_tmp_data); 379 #else /* safe if sizeof(struct crypt_data) <= 32768 */ 380 #endif 381 #endif 382 return rc; 383 384 #endif 385 } 386 #endif 387 388 389 static int 390 mod_authn_dbi_password_cmp (const char *userpw, unsigned long userpwlen, http_auth_info_t * const ai, const char *reqpw) 391 { 392 #if defined(HAVE_CRYPT_R) || defined(HAVE_CRYPT) 393 if (userpwlen >= 3 && userpw[0] == '$') 394 return mod_authn_crypt_cmp(reqpw, userpw, userpwlen); 395 #endif 396 397 const struct const_iovec iov[] = { 398 { ai->username, ai->ulen } 399 ,{ ":", 1 } 400 ,{ ai->realm, ai->rlen } 401 ,{ ":", 1 } 402 ,{ reqpw, strlen(reqpw) } 403 }; 404 405 unsigned char HA1[MD_DIGEST_LENGTH_MAX]; 406 unsigned char pwbin[MD_DIGEST_LENGTH_MAX]; 407 408 if (32 == userpwlen) 409 MD5_iov(HA1, iov, sizeof(iov)/sizeof(*iov)); 410 #ifdef USE_LIB_CRYPTO 411 else if (64 == userpwlen) 412 SHA256_iov(HA1, iov, sizeof(iov)/sizeof(*iov)); 413 #endif 414 else 415 return -1; 416 417 /*(compare 32-byte binary digest instead of converting to hex strings 418 * in order to then have to do case-insensitive hex str comparison)*/ 419 return (0 == li_hex2bin(pwbin, sizeof(pwbin), userpw, userpwlen)) 420 ? ck_memeq_const_time_fixed_len(HA1, pwbin, userpwlen>>1) ? 0 : 1 421 : -1; 422 } 423 424 425 static buffer * 426 mod_authn_dbi_query_build (buffer * const sqlquery, dbi_config * const dbconf, http_auth_info_t * const ai) 427 { 428 char buf[1024]; 429 buffer_clear(sqlquery); 430 int qcount = 0; 431 const buffer * const sqlb = (ai->userhash) 432 ? dbconf->sqluserhash 433 : dbconf->sqlquery; 434 if (NULL == sqlb) 435 return NULL; 436 for (char *b = sqlb->ptr, *d; *b; b = d+1) { 437 if (NULL != (d = strchr(b, '?'))) { 438 /* Substitute for up to three question marks (?) 439 * substitute username for first question mark 440 * substitute realm for second question mark 441 * substitute digest algorithm for third question mark */ 442 const char *v; 443 switch (++qcount) { 444 case 1: 445 if (ai->ulen < sizeof(buf)) { 446 memcpy(buf, ai->username, ai->ulen); 447 buf[ai->ulen] = '\0'; 448 v = buf; 449 } 450 else 451 return NULL; 452 break; 453 case 2: 454 if (ai->rlen < sizeof(buf)) { 455 memcpy(buf, ai->realm, ai->rlen); 456 buf[ai->rlen] = '\0'; 457 v = buf; 458 } 459 else 460 return NULL; 461 break; 462 case 3: 463 if (ai->dalgo & HTTP_AUTH_DIGEST_SHA256) 464 v = "SHA-256"; 465 else if (ai->dalgo & HTTP_AUTH_DIGEST_MD5) 466 v = "MD5"; 467 else if (ai->dalgo == HTTP_AUTH_DIGEST_NONE) 468 v = "NONE"; 469 else 470 return NULL; 471 break; 472 default: 473 return NULL; 474 } 475 /* escape the value */ 476 char *esc = NULL; 477 size_t elen = 478 dbi_conn_escape_string_copy(dbconf->dbconn, v, &esc); 479 if (0 == elen) return NULL; /*('esc' must not be freed if error)*/ 480 buffer_append_string_len(sqlquery, b, (size_t)(d - b)); 481 buffer_append_string_len(sqlquery, esc, elen); 482 free(esc); 483 } 484 else { 485 d = sqlb->ptr + buffer_clen(sqlb); 486 buffer_append_string_len(sqlquery, b, (size_t)(d - b)); 487 break; 488 } 489 } 490 491 return sqlquery; 492 } 493 494 495 static handler_t 496 mod_authn_dbi_query (request_st * const r, void *p_d, http_auth_info_t * const ai, const char * const pw) 497 { 498 plugin_data * const p = (plugin_data *)p_d; 499 mod_authn_dbi_patch_config(r, p); 500 if (NULL == p->conf.vdata) return HANDLER_ERROR; /*(should not happen)*/ 501 dbi_config * const dbconf = (dbi_config *)p->conf.vdata; 502 503 buffer * const sqlquery = mod_authn_dbi_query_build(r->tmp_buf, dbconf, ai); 504 if (NULL == sqlquery) 505 return HANDLER_ERROR; 506 507 /* reset our reconnect-attempt counter, this is a new query. */ 508 dbconf->reconnect_count = 0; 509 510 dbi_result result; 511 int retry_count = 0; 512 do { 513 result = dbi_conn_query(dbconf->dbconn, sqlquery->ptr); 514 } while (!result && ++retry_count < 2); 515 516 if (!result) { 517 const char *errmsg; 518 dbi_conn_error(dbconf->dbconn, &errmsg); 519 log_error(r->conf.errh, __FILE__, __LINE__, "%s", errmsg); 520 return HANDLER_ERROR; 521 } 522 523 handler_t rc = HANDLER_ERROR; 524 unsigned long long nrows = dbi_result_get_numrows(result); 525 if (nrows && nrows != DBI_ROW_ERROR && dbi_result_next_row(result)) { 526 size_t len = dbi_result_get_field_length_idx(result, 1); 527 const char *rpw = dbi_result_get_string_idx(result, 1); 528 if (len != DBI_LENGTH_ERROR && rpw 529 && (len != 5 /*(rpw might be "ERROR" if len == 5)*/ 530 || dbi_conn_error(dbconf->dbconn, NULL) == DBI_ERROR_NONE)) { 531 if (pw) { /* used with HTTP Basic auth */ 532 if (0 == mod_authn_dbi_password_cmp(rpw, len, ai, pw)) 533 rc = HANDLER_GO_ON; 534 } 535 else { /* used with HTTP Digest auth */ 536 /*(currently supports only single row, single digest algo)*/ 537 if (len == (ai->dlen << 1) 538 && 0 == li_hex2bin(ai->digest,sizeof(ai->digest),rpw,len)) 539 rc = HANDLER_GO_ON; 540 } 541 } 542 if (ai->userhash) { 543 len = dbi_result_get_field_length_idx(result, 2); 544 rpw = dbi_result_get_string_idx(result, 2); 545 ai->username = ai->userbuf; 546 if (len != DBI_LENGTH_ERROR && rpw && len <= sizeof(ai->userbuf) 547 && (len != 5 /*(rpw might be "ERROR" if len == 5)*/ 548 || dbi_conn_error(dbconf->dbconn, NULL) == DBI_ERROR_NONE)) 549 memcpy(ai->userbuf, rpw, (ai->ulen = len)); 550 else { 551 ai->ulen = 1; 552 ai->userbuf[0] = '\0'; /* invalid username "\0" */ 553 } 554 } 555 } /* else not found */ 556 557 dbi_result_free(result); 558 return rc; 559 } 560 561 562 static handler_t 563 mod_authn_dbi_basic (request_st * const r, void *p_d, const http_auth_require_t * const require, const buffer * const username, const char * const pw) 564 { 565 handler_t rc; 566 http_auth_info_t ai; 567 ai.dalgo = HTTP_AUTH_DIGEST_NONE; 568 ai.dlen = 0; 569 ai.username = username->ptr; 570 ai.ulen = buffer_clen(username); 571 ai.realm = require->realm->ptr; 572 ai.rlen = buffer_clen(require->realm); 573 ai.userhash = 0; 574 rc = mod_authn_dbi_query(r, p_d, &ai, pw); 575 if (HANDLER_GO_ON != rc) return rc; 576 return http_auth_match_rules(require, username->ptr, NULL, NULL) 577 ? HANDLER_GO_ON /* access granted */ 578 : HANDLER_ERROR; 579 } 580 581 582 static handler_t 583 mod_authn_dbi_digest (request_st * const r, void *p_d, http_auth_info_t * const ai) 584 { 585 return mod_authn_dbi_query(r, p_d, ai, NULL); 586 } 587 588 589 __attribute_cold__ 590 int mod_authn_dbi_plugin_init (plugin *p); 591 int mod_authn_dbi_plugin_init (plugin *p) 592 { 593 p->version = LIGHTTPD_VERSION_ID; 594 p->name = "authn_dbi"; 595 p->init = mod_authn_dbi_init; 596 p->cleanup = mod_authn_dbi_cleanup; 597 p->set_defaults = mod_authn_dbi_set_defaults; 598 599 return 0; 600 } 601