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