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