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 *)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 = 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 = malloc(sizeof(struct crypt_data)); 356 force_assert(crypt_tmp_data); 357 #else /* safe if sizeof(struct crypt_data) <= 32768 */ 358 struct crypt_data crypt_tmp_data_stack; 359 struct crypt_data *crypt_tmp_data = &crypt_tmp_data_stack; 360 #endif 361 #ifdef _AIX 362 memset(&crypt_tmp_data_stack, 0, sizeof(struct crypt_data)); 363 #else 364 crypt_tmp_data_stack.initialized = 0; 365 #endif 366 #endif 367 368 #if defined(HAVE_CRYPT_R) 369 char *crypted = crypt_r(reqpw, userpw, crypt_tmp_data); 370 #else 371 char *crypted = crypt(reqpw, userpw); 372 #endif 373 size_t crypwlen = (NULL != crypted) ? strlen(crypted) : 0; 374 int rc = (crypwlen == userpwlen) ? memcmp(crypted, userpw, crypwlen) : -1; 375 376 if (crypwlen >= 13) ck_memzero(crypted, crypwlen); 377 #if defined(HAVE_CRYPT_R) 378 #if 1 /* (must free() if allocated above) */ 379 free(crypt_tmp_data); 380 #else /* safe if sizeof(struct crypt_data) <= 32768 */ 381 #endif 382 #endif 383 return rc; 384 385 #endif 386 } 387 #endif 388 389 390 static int 391 mod_authn_dbi_password_cmp (const char *userpw, unsigned long userpwlen, http_auth_info_t * const ai, const char *reqpw) 392 { 393 #if defined(HAVE_CRYPT_R) || defined(HAVE_CRYPT) 394 if (userpwlen >= 3 && userpw[0] == '$') 395 return mod_authn_crypt_cmp(reqpw, userpw, userpwlen); 396 #endif 397 398 const struct const_iovec iov[] = { 399 { ai->username, ai->ulen } 400 ,{ ":", 1 } 401 ,{ ai->realm, ai->rlen } 402 ,{ ":", 1 } 403 ,{ reqpw, strlen(reqpw) } 404 }; 405 406 unsigned char HA1[MD_DIGEST_LENGTH_MAX]; 407 unsigned char pwbin[MD_DIGEST_LENGTH_MAX]; 408 409 if (32 == userpwlen) 410 MD5_iov(HA1, iov, sizeof(iov)/sizeof(*iov)); 411 #ifdef USE_LIB_CRYPTO 412 else if (64 == userpwlen) 413 SHA256_iov(HA1, iov, sizeof(iov)/sizeof(*iov)); 414 #endif 415 else 416 return -1; 417 418 /*(compare 32-byte binary digest instead of converting to hex strings 419 * in order to then have to do case-insensitive hex str comparison)*/ 420 return (0 == li_hex2bin(pwbin, sizeof(pwbin), userpw, userpwlen)) 421 ? ck_memeq_const_time_fixed_len(HA1, pwbin, userpwlen>>1) ? 0 : 1 422 : -1; 423 } 424 425 426 static buffer * 427 mod_authn_dbi_query_build (buffer * const sqlquery, dbi_config * const dbconf, http_auth_info_t * const ai) 428 { 429 char buf[1024]; 430 buffer_clear(sqlquery); 431 int qcount = 0; 432 const buffer * const sqlb = (ai->userhash) 433 ? dbconf->sqluserhash 434 : dbconf->sqlquery; 435 if (NULL == sqlb) 436 return NULL; 437 for (char *b = sqlb->ptr, *d; *b; b = d+1) { 438 if (NULL != (d = strchr(b, '?'))) { 439 /* Substitute for up to three question marks (?) 440 * substitute username for first question mark 441 * substitute realm for second question mark 442 * substitute digest algorithm for third question mark */ 443 const char *v; 444 switch (++qcount) { 445 case 1: 446 if (ai->ulen < sizeof(buf)) { 447 memcpy(buf, ai->username, ai->ulen); 448 buf[ai->ulen] = '\0'; 449 v = buf; 450 } 451 else 452 return NULL; 453 break; 454 case 2: 455 if (ai->rlen < sizeof(buf)) { 456 memcpy(buf, ai->realm, ai->rlen); 457 buf[ai->rlen] = '\0'; 458 v = buf; 459 } 460 else 461 return NULL; 462 break; 463 case 3: 464 if (ai->dalgo & HTTP_AUTH_DIGEST_SHA256) 465 v = "SHA-256"; 466 else if (ai->dalgo & HTTP_AUTH_DIGEST_MD5) 467 v = "MD5"; 468 else if (ai->dalgo == HTTP_AUTH_DIGEST_NONE) 469 v = "NONE"; 470 else 471 return NULL; 472 break; 473 default: 474 return NULL; 475 } 476 /* escape the value */ 477 char *esc = NULL; 478 size_t elen = 479 dbi_conn_escape_string_copy(dbconf->dbconn, v, &esc); 480 if (0 == elen) return NULL; /*('esc' must not be freed if error)*/ 481 buffer_append_string_len(sqlquery, b, (size_t)(d - b)); 482 buffer_append_string_len(sqlquery, esc, elen); 483 free(esc); 484 } 485 else { 486 d = sqlb->ptr + buffer_clen(sqlb); 487 buffer_append_string_len(sqlquery, b, (size_t)(d - b)); 488 break; 489 } 490 } 491 492 return sqlquery; 493 } 494 495 496 static handler_t 497 mod_authn_dbi_query (request_st * const r, void *p_d, http_auth_info_t * const ai, const char * const pw) 498 { 499 plugin_data * const p = (plugin_data *)p_d; 500 mod_authn_dbi_patch_config(r, p); 501 if (NULL == p->conf.vdata) return HANDLER_ERROR; /*(should not happen)*/ 502 dbi_config * const dbconf = (dbi_config *)p->conf.vdata; 503 504 buffer * const sqlquery = mod_authn_dbi_query_build(r->tmp_buf, dbconf, ai); 505 if (NULL == sqlquery) 506 return HANDLER_ERROR; 507 508 /* reset our reconnect-attempt counter, this is a new query. */ 509 dbconf->reconnect_count = 0; 510 511 dbi_result result; 512 int retry_count = 0; 513 do { 514 result = dbi_conn_query(dbconf->dbconn, sqlquery->ptr); 515 } while (!result && ++retry_count < 2); 516 517 if (!result) { 518 const char *errmsg; 519 dbi_conn_error(dbconf->dbconn, &errmsg); 520 log_error(r->conf.errh, __FILE__, __LINE__, "%s", errmsg); 521 return HANDLER_ERROR; 522 } 523 524 handler_t rc = HANDLER_ERROR; 525 unsigned long long nrows = dbi_result_get_numrows(result); 526 if (nrows && nrows != DBI_ROW_ERROR && dbi_result_next_row(result)) { 527 size_t len = dbi_result_get_field_length_idx(result, 1); 528 const char *rpw = dbi_result_get_string_idx(result, 1); 529 if (len != DBI_LENGTH_ERROR && rpw 530 && (len != 5 /*(rpw might be "ERROR" if len == 5)*/ 531 || dbi_conn_error(dbconf->dbconn, NULL) == DBI_ERROR_NONE)) { 532 if (pw) { /* used with HTTP Basic auth */ 533 if (0 == mod_authn_dbi_password_cmp(rpw, len, ai, pw)) 534 rc = HANDLER_GO_ON; 535 } 536 else { /* used with HTTP Digest auth */ 537 /*(currently supports only single row, single digest algo)*/ 538 if (len == (ai->dlen << 1) 539 && 0 == li_hex2bin(ai->digest,sizeof(ai->digest),rpw,len)) 540 rc = HANDLER_GO_ON; 541 } 542 } 543 if (ai->userhash) { 544 len = dbi_result_get_field_length_idx(result, 2); 545 rpw = dbi_result_get_string_idx(result, 2); 546 ai->username = ai->userbuf; 547 if (len != DBI_LENGTH_ERROR && rpw && len <= sizeof(ai->userbuf) 548 && (len != 5 /*(rpw might be "ERROR" if len == 5)*/ 549 || dbi_conn_error(dbconf->dbconn, NULL) == DBI_ERROR_NONE)) 550 memcpy(ai->userbuf, rpw, (ai->ulen = len)); 551 else { 552 ai->ulen = 1; 553 ai->userbuf[0] = '\0'; /* invalid username "\0" */ 554 } 555 } 556 } /* else not found */ 557 558 dbi_result_free(result); 559 return rc; 560 } 561 562 563 static handler_t 564 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) 565 { 566 handler_t rc; 567 http_auth_info_t ai; 568 ai.dalgo = HTTP_AUTH_DIGEST_NONE; 569 ai.dlen = 0; 570 ai.username = username->ptr; 571 ai.ulen = buffer_clen(username); 572 ai.realm = require->realm->ptr; 573 ai.rlen = buffer_clen(require->realm); 574 ai.userhash = 0; 575 rc = mod_authn_dbi_query(r, p_d, &ai, pw); 576 if (HANDLER_GO_ON != rc) return rc; 577 return http_auth_match_rules(require, username->ptr, NULL, NULL) 578 ? HANDLER_GO_ON /* access granted */ 579 : HANDLER_ERROR; 580 } 581 582 583 static handler_t 584 mod_authn_dbi_digest (request_st * const r, void *p_d, http_auth_info_t * const ai) 585 { 586 return mod_authn_dbi_query(r, p_d, ai, NULL); 587 } 588 589 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