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