1 #include "first.h" 2 3 #include <ldap.h> 4 5 #include <string.h> 6 #include <stdlib.h> 7 8 #include "base.h" 9 #include "http_vhostdb.h" 10 #include "log.h" 11 #include "plugin.h" 12 13 /* 14 * virtual host plugin using LDAP for domain to directory lookups 15 */ 16 17 typedef struct { 18 LDAP *ldap; 19 const buffer *filter; 20 log_error_st *errh; 21 22 const char *attr; 23 const char *host; 24 const char *basedn; 25 const char *binddn; 26 const char *bindpw; 27 const char *cafile; 28 unsigned short starttls; 29 } vhostdb_config; 30 31 typedef struct { 32 void *vdata; 33 } plugin_config; 34 35 typedef struct { 36 PLUGIN_DATA; 37 plugin_config defaults; 38 plugin_config conf; 39 } plugin_data; 40 41 static void mod_vhostdb_dbconf_free (void *vdata) 42 { 43 vhostdb_config *dbconf = (vhostdb_config *)vdata; 44 if (!dbconf) return; 45 if (NULL != dbconf->ldap) ldap_unbind_ext_s(dbconf->ldap, NULL, NULL); 46 free(dbconf); 47 } 48 49 /*(copied from mod_authn_ldap.c)*/ 50 static void mod_vhostdb_dbconf_add_scheme (server *srv, buffer *host) 51 { 52 if (!buffer_string_is_empty(host)) { 53 /* reformat hostname(s) as LDAP URIs (scheme://host:port) */ 54 static const char *schemes[] = { 55 "ldap://", "ldaps://", "ldapi://", "cldap://" 56 }; 57 char *b, *e = host->ptr; 58 buffer * const tb = srv->tmp_buf; 59 buffer_clear(tb); 60 while (*(b = e)) { 61 unsigned int j; 62 while (*b==' '||*b=='\t'||*b=='\r'||*b=='\n'||*b==',') ++b; 63 if (*b == '\0') break; 64 e = b; 65 while (*e!=' '&&*e!='\t'&&*e!='\r'&&*e!='\n'&&*e!=','&&*e!='\0') 66 ++e; 67 if (!buffer_string_is_empty(tb)) 68 buffer_append_string_len(tb, CONST_STR_LEN(",")); 69 for (j = 0; j < sizeof(schemes)/sizeof(char *); ++j) { 70 if (buffer_eq_icase_ssn(b, schemes[j], strlen(schemes[j]))) { 71 break; 72 } 73 } 74 if (j == sizeof(schemes)/sizeof(char *)) 75 buffer_append_string_len(tb, CONST_STR_LEN("ldap://")); 76 buffer_append_string_len(tb, b, (size_t)(e - b)); 77 } 78 buffer_copy_buffer(host, tb); 79 } 80 } 81 82 static int mod_vhostdb_dbconf_setup (server *srv, const array *opts, void **vdata) 83 { 84 const buffer *filter = NULL; 85 const char *attr = "documentRoot"; 86 const char *basedn=NULL,*binddn=NULL,*bindpw=NULL,*host=NULL,*cafile=NULL; 87 unsigned short starttls = 0; 88 89 for (size_t i = 0; i < opts->used; ++i) { 90 data_string *ds = (data_string *)opts->data[i]; 91 if (ds->type == TYPE_STRING) { 92 if (buffer_is_equal_caseless_string(&ds->key, CONST_STR_LEN("filter"))) { 93 filter = &ds->value; 94 } else if (buffer_is_equal_caseless_string(&ds->key, CONST_STR_LEN("attr"))) { 95 if (!buffer_string_is_empty(&ds->value)) attr = ds->value.ptr; 96 } else if (buffer_is_equal_caseless_string(&ds->key, CONST_STR_LEN("host"))) { 97 mod_vhostdb_dbconf_add_scheme(srv, &ds->value); 98 host = ds->value.ptr; 99 } else if (buffer_is_equal_caseless_string(&ds->key, CONST_STR_LEN("base-dn"))) { 100 if (!buffer_string_is_empty(&ds->value)) basedn = ds->value.ptr; 101 } else if (buffer_is_equal_caseless_string(&ds->key, CONST_STR_LEN("bind-dn"))) { 102 if (!buffer_string_is_empty(&ds->value)) binddn = ds->value.ptr; 103 } else if (buffer_is_equal_caseless_string(&ds->key, CONST_STR_LEN("bind-pw"))) { 104 bindpw = ds->value.ptr; 105 } else if (buffer_is_equal_caseless_string(&ds->key, CONST_STR_LEN("ca-file"))) { 106 if (!buffer_string_is_empty(&ds->value)) cafile = ds->value.ptr; 107 } else if (buffer_is_equal_caseless_string(&ds->key, CONST_STR_LEN("starttls"))) { 108 starttls = !buffer_is_equal_string(&ds->value, CONST_STR_LEN("disable")) 109 && !buffer_is_equal_string(&ds->value, CONST_STR_LEN("0")); 110 } 111 } 112 } 113 114 /* required: 115 * - host 116 * - filter (LDAP query) 117 * - base-dn 118 * 119 * optional: 120 * - attr (LDAP attribute with docroot; default "documentRoot") 121 * - bind-dn 122 * - bind-pw 123 * - ca-file 124 * - starttls 125 */ 126 127 if (!buffer_string_is_empty(filter) && NULL != host && NULL != basedn) { 128 vhostdb_config *dbconf; 129 130 if (NULL == strchr(filter->ptr, '?')) { 131 log_error(srv->errh, __FILE__, __LINE__, 132 "ldap: filter is missing a replace-operator '?'"); 133 return -1; 134 } 135 136 /* openldap sets FD_CLOEXEC on database socket descriptors 137 * (still race between creation of socket and fcntl FD_CLOEXEC) 138 * (YMMV with other LDAP client libraries) */ 139 140 dbconf = (vhostdb_config *)calloc(1, sizeof(*dbconf)); 141 dbconf->ldap = NULL; 142 dbconf->filter = filter; 143 dbconf->attr = attr; 144 dbconf->host = host; 145 dbconf->basedn = basedn; 146 dbconf->binddn = binddn; 147 dbconf->bindpw = bindpw; 148 dbconf->cafile = cafile; 149 dbconf->starttls = starttls; 150 *vdata = dbconf; 151 } 152 153 return 0; 154 } 155 156 /* 157 * Note: a large portion of the LDAP code is copied verbatim from mod_authn_ldap 158 * with only changes being use of vhostdb_config instead of plugin_config struct 159 * and (const char *) strings in vhostdb_config instead of (buffer *). 160 */ 161 162 __attribute_cold__ 163 static void mod_authn_ldap_err(log_error_st *errh, const char *file, unsigned long line, const char *fn, int err) 164 { 165 log_error(errh, file, line, "ldap: %s: %s", fn, ldap_err2string(err)); 166 } 167 168 __attribute_cold__ 169 static void mod_authn_ldap_opt_err(log_error_st *errh, const char *file, unsigned long line, const char *fn, LDAP *ld) 170 { 171 int err; 172 ldap_get_option(ld, LDAP_OPT_ERROR_NUMBER, &err); 173 mod_authn_ldap_err(errh, file, line, fn, err); 174 } 175 176 static void mod_authn_append_ldap_filter_escape(buffer * const filter, const buffer * const raw) { 177 /* [RFC4515] 3. String Search Filter Definition 178 * 179 * [...] 180 * 181 * The <valueencoding> rule ensures that the entire filter string is a 182 * valid UTF-8 string and provides that the octets that represent the 183 * ASCII characters "*" (ASCII 0x2a), "(" (ASCII 0x28), ")" (ASCII 184 * 0x29), "\" (ASCII 0x5c), and NUL (ASCII 0x00) are represented as a 185 * backslash "\" (ASCII 0x5c) followed by the two hexadecimal digits 186 * representing the value of the encoded octet. 187 * 188 * [...] 189 * 190 * As indicated by the <valueencoding> rule, implementations MUST escape 191 * all octets greater than 0x7F that are not part of a valid UTF-8 192 * encoding sequence when they generate a string representation of a 193 * search filter. Implementations SHOULD accept as input strings that 194 * are not valid UTF-8 strings. This is necessary because RFC 2254 did 195 * not clearly define the term "string representation" (and in 196 * particular did not mention that the string representation of an LDAP 197 * search filter is a string of UTF-8-encoded Unicode characters). 198 * 199 * 200 * https://www.ldap.com/ldap-filters 201 * Although not required, you may escape any other characters that you want 202 * in the assertion value (or substring component) of a filter. This may be 203 * accomplished by prefixing the hexadecimal representation of each byte of 204 * the UTF-8 encoding of the character to escape with a backslash character. 205 */ 206 const char * const b = raw->ptr; 207 const size_t rlen = buffer_string_length(raw); 208 for (size_t i = 0; i < rlen; ++i) { 209 size_t len = i; 210 char *f; 211 do { 212 /* encode all UTF-8 chars with high bit set 213 * (instead of validating UTF-8 and escaping only invalid UTF-8) */ 214 if (((unsigned char *)b)[len] > 0x7f) 215 break; 216 switch (b[len]) { 217 default: 218 continue; 219 case '\0': case '(': case ')': case '*': case '\\': 220 break; 221 } 222 break; 223 } while (++len < rlen); 224 len -= i; 225 226 if (len) { 227 buffer_append_string_len(filter, b+i, len); 228 if ((i += len) == rlen) break; 229 } 230 231 /* escape * ( ) \ NUL ('\0') (and all UTF-8 chars with high bit set) */ 232 buffer_string_prepare_append(filter, 3); 233 f = filter->ptr + buffer_string_length(filter); 234 f[0] = '\\'; 235 f[1] = "0123456789abcdef"[(((unsigned char *)b)[i] >> 4) & 0xf]; 236 f[2] = "0123456789abcdef"[(((unsigned char *)b)[i] ) & 0xf]; 237 buffer_commit(filter, 3); 238 } 239 } 240 241 static LDAP * mod_authn_ldap_host_init(log_error_st *errh, vhostdb_config *s) { 242 LDAP *ld; 243 int ret; 244 245 ret = ldap_initialize(&ld, s->host); 246 if (LDAP_SUCCESS != ret) { 247 log_perror(errh, __FILE__, __LINE__, "ldap: ldap_initialize()"); 248 return NULL; 249 } 250 251 ret = LDAP_VERSION3; 252 ret = ldap_set_option(ld, LDAP_OPT_PROTOCOL_VERSION, &ret); 253 if (LDAP_OPT_SUCCESS != ret) { 254 mod_authn_ldap_err(errh, __FILE__, __LINE__, "ldap_set_options()", ret); 255 ldap_destroy(ld); 256 return NULL; 257 } 258 259 /* restart ldap functions if interrupted by a signal, e.g. SIGCHLD */ 260 ldap_set_option(ld, LDAP_OPT_RESTART, LDAP_OPT_ON); 261 262 if (s->starttls) { 263 /* if no CA file is given, it is ok, as we will use encryption 264 * if the server requires a CAfile it will tell us */ 265 if (s->cafile) { 266 ret = ldap_set_option(NULL, LDAP_OPT_X_TLS_CACERTFILE, s->cafile); 267 if (LDAP_OPT_SUCCESS != ret) { 268 mod_authn_ldap_err(errh, __FILE__, __LINE__, 269 "ldap_set_option(LDAP_OPT_X_TLS_CACERTFILE)", ret); 270 ldap_destroy(ld); 271 return NULL; 272 } 273 } 274 275 ret = ldap_start_tls_s(ld, NULL, NULL); 276 if (LDAP_OPT_SUCCESS != ret) { 277 mod_authn_ldap_err(errh,__FILE__,__LINE__,"ldap_start_tls_s()",ret); 278 ldap_destroy(ld); 279 return NULL; 280 } 281 } 282 283 return ld; 284 } 285 286 static int mod_authn_ldap_bind(log_error_st *errh, LDAP *ld, const char *dn, const char *pw) { 287 struct berval creds; 288 int ret; 289 290 if (NULL != pw) { 291 *((const char **)&creds.bv_val) = pw; /*(cast away const)*/ 292 creds.bv_len = strlen(pw); 293 } else { 294 creds.bv_val = NULL; 295 creds.bv_len = 0; 296 } 297 298 /* RFE: add functionality: LDAP_SASL_EXTERNAL (or GSS-SPNEGO, etc.) */ 299 300 ret = ldap_sasl_bind_s(ld,dn,LDAP_SASL_SIMPLE,&creds,NULL,NULL,NULL); 301 if (ret != LDAP_SUCCESS) { 302 mod_authn_ldap_err(errh, __FILE__, __LINE__, "ldap_sasl_bind_s()", ret); 303 } 304 305 return ret; 306 } 307 308 static int mod_authn_ldap_rebind_proc (LDAP *ld, LDAP_CONST char *url, ber_tag_t ldap_request, ber_int_t msgid, void *params) { 309 vhostdb_config *s = (vhostdb_config *)params; 310 UNUSED(url); 311 UNUSED(ldap_request); 312 UNUSED(msgid); 313 return mod_authn_ldap_bind(s->errh, ld, s->binddn, s->bindpw); 314 } 315 316 static LDAPMessage * mod_authn_ldap_search(log_error_st *errh, vhostdb_config *s, char *base, char *filter) { 317 LDAPMessage *lm = NULL; 318 char *attrs[] = { LDAP_NO_ATTRS, NULL }; 319 int ret; 320 321 /* 322 * 1. connect anonymously (if not already connected) 323 * (ldap connection is kept open unless connection-level error occurs) 324 * 2. issue search using filter 325 */ 326 327 if (s->ldap != NULL) { 328 ret = ldap_search_ext_s(s->ldap, base, LDAP_SCOPE_SUBTREE, filter, 329 attrs, 0, NULL, NULL, NULL, 0, &lm); 330 if (LDAP_SUCCESS == ret) { 331 return lm; 332 } else if (LDAP_SERVER_DOWN != ret) { 333 /* try again (or initial request); 334 * ldap lib sometimes fails for the first call but reconnects */ 335 ret = ldap_search_ext_s(s->ldap, base, LDAP_SCOPE_SUBTREE, filter, 336 attrs, 0, NULL, NULL, NULL, 0, &lm); 337 if (LDAP_SUCCESS == ret) { 338 return lm; 339 } 340 } 341 342 ldap_unbind_ext_s(s->ldap, NULL, NULL); 343 } 344 345 s->ldap = mod_authn_ldap_host_init(errh, s); 346 if (NULL == s->ldap) { 347 return NULL; 348 } 349 350 ldap_set_rebind_proc(s->ldap, mod_authn_ldap_rebind_proc, s); 351 ret = mod_authn_ldap_bind(errh, s->ldap, s->binddn, s->bindpw); 352 if (LDAP_SUCCESS != ret) { 353 ldap_destroy(s->ldap); 354 s->ldap = NULL; 355 return NULL; 356 } 357 358 ret = ldap_search_ext_s(s->ldap, base, LDAP_SCOPE_SUBTREE, filter, 359 attrs, 0, NULL, NULL, NULL, 0, &lm); 360 if (LDAP_SUCCESS != ret) { 361 log_error(errh, __FILE__, __LINE__, 362 "ldap: %s; filter: %s", ldap_err2string(ret), filter); 363 ldap_unbind_ext_s(s->ldap, NULL, NULL); 364 s->ldap = NULL; 365 return NULL; 366 } 367 368 return lm; 369 } 370 371 static void mod_vhostdb_patch_config (request_st * const r, plugin_data * const p); 372 373 static int mod_vhostdb_ldap_query(request_st * const r, void *p_d, buffer *docroot) 374 { 375 plugin_data *p = (plugin_data *)p_d; 376 vhostdb_config *dbconf; 377 LDAP *ld; 378 LDAPMessage *lm, *first; 379 struct berval **vals; 380 int count; 381 char *basedn; 382 const buffer *template; 383 384 /*(reuse buffer for ldap query before generating docroot result)*/ 385 buffer *filter = docroot; 386 buffer_clear(filter); /*(also resets docroot (alias))*/ 387 388 mod_vhostdb_patch_config(r, p); 389 if (NULL == p->conf.vdata) return 0; /*(after resetting docroot)*/ 390 dbconf = (vhostdb_config *)p->conf.vdata; 391 log_error_st * const errh = r->conf.errh; 392 dbconf->errh = errh; 393 394 template = dbconf->filter; 395 for (char *b = template->ptr, *d; *b; b = d+1) { 396 if (NULL != (d = strchr(b, '?'))) { 397 buffer_append_string_len(filter, b, (size_t)(d - b)); 398 mod_authn_append_ldap_filter_escape(filter, &r->uri.authority); 399 } else { 400 d = template->ptr + buffer_string_length(template); 401 buffer_append_string_len(filter, b, (size_t)(d - b)); 402 break; 403 } 404 } 405 406 /* (cast away const for poor LDAP ldap_search_ext_s() prototype) */ 407 *(const char **)&basedn = dbconf->basedn; 408 409 /* ldap_search (synchronous; blocking) */ 410 lm = mod_authn_ldap_search(errh, dbconf, basedn, filter->ptr); 411 if (NULL == lm) { 412 return -1; 413 } 414 415 /*(must be after mod_authn_ldap_search(); might reconnect)*/ 416 ld = dbconf->ldap; 417 418 count = ldap_count_entries(ld, lm); 419 if (count > 1) { 420 log_error(errh, __FILE__, __LINE__, 421 "ldap: more than one record returned. " 422 "you might have to refine the filter: %s", filter->ptr); 423 } 424 425 buffer_clear(docroot); /*(reset buffer to store result)*/ 426 427 if (0 == count) { /*(no entries found)*/ 428 ldap_msgfree(lm); 429 return 0; 430 } 431 432 if (NULL == (first = ldap_first_entry(ld, lm))) { 433 mod_authn_ldap_opt_err(errh,__FILE__,__LINE__,"ldap_first_entry()",ld); 434 ldap_msgfree(lm); 435 return -1; 436 } 437 438 if (NULL != (vals = ldap_get_values_len(ld, first, dbconf->attr))) { 439 buffer_copy_string_len(docroot, vals[0]->bv_val, vals[0]->bv_len); 440 ldap_value_free_len(vals); 441 } 442 443 ldap_msgfree(lm); 444 return 0; 445 } 446 447 448 449 450 INIT_FUNC(mod_vhostdb_init) { 451 static http_vhostdb_backend_t http_vhostdb_backend_ldap = 452 { "ldap", mod_vhostdb_ldap_query, NULL }; 453 plugin_data *p = calloc(1, sizeof(*p)); 454 455 /* register http_vhostdb_backend_ldap */ 456 http_vhostdb_backend_ldap.p_d = p; 457 http_vhostdb_backend_set(&http_vhostdb_backend_ldap); 458 459 return p; 460 } 461 462 FREE_FUNC(mod_vhostdb_cleanup) { 463 plugin_data * const p = p_d; 464 if (NULL == p->cvlist) return; 465 /* (init i to 0 if global context; to 1 to skip empty global context) */ 466 for (int i = !p->cvlist[0].v.u2[1], used = p->nconfig; i < used; ++i) { 467 config_plugin_value_t *cpv = p->cvlist + p->cvlist[i].v.u2[0]; 468 for (; -1 != cpv->k_id; ++cpv) { 469 if (cpv->vtype != T_CONFIG_LOCAL || NULL == cpv->v.v) continue; 470 switch (cpv->k_id) { 471 case 0: /* vhostdb.<db> */ 472 mod_vhostdb_dbconf_free(cpv->v.v); 473 break; 474 default: 475 break; 476 } 477 } 478 } 479 } 480 481 static void mod_vhostdb_merge_config_cpv(plugin_config * const pconf, const config_plugin_value_t * const cpv) { 482 switch (cpv->k_id) { /* index into static config_plugin_keys_t cpk[] */ 483 case 0: /* vhostdb.<db> */ 484 if (cpv->vtype == T_CONFIG_LOCAL) 485 pconf->vdata = cpv->v.v; 486 break; 487 default:/* should not happen */ 488 return; 489 } 490 } 491 492 static void mod_vhostdb_merge_config(plugin_config * const pconf, const config_plugin_value_t *cpv) { 493 do { 494 mod_vhostdb_merge_config_cpv(pconf, cpv); 495 } while ((++cpv)->k_id != -1); 496 } 497 498 static void mod_vhostdb_patch_config(request_st * const r, plugin_data * const p) { 499 p->conf = p->defaults; /* copy small struct instead of memcpy() */ 500 /*memcpy(&p->conf, &p->defaults, sizeof(plugin_config));*/ 501 for (int i = 1, used = p->nconfig; i < used; ++i) { 502 if (config_check_cond(r, (uint32_t)p->cvlist[i].k_id)) 503 mod_vhostdb_merge_config(&p->conf,p->cvlist + p->cvlist[i].v.u2[0]); 504 } 505 } 506 507 SETDEFAULTS_FUNC(mod_vhostdb_set_defaults) { 508 static const config_plugin_keys_t cpk[] = { 509 { CONST_STR_LEN("vhostdb.ldap"), 510 T_CONFIG_ARRAY_KVSTRING, 511 T_CONFIG_SCOPE_CONNECTION } 512 ,{ NULL, 0, 513 T_CONFIG_UNSET, 514 T_CONFIG_SCOPE_UNSET } 515 }; 516 517 plugin_data * const p = p_d; 518 if (!config_plugin_values_init(srv, p, cpk, "mod_vhostdb_ldap")) 519 return HANDLER_ERROR; 520 521 /* process and validate config directives 522 * (init i to 0 if global context; to 1 to skip empty global context) */ 523 for (int i = !p->cvlist[0].v.u2[1]; i < p->nconfig; ++i) { 524 config_plugin_value_t *cpv = p->cvlist + p->cvlist[i].v.u2[0]; 525 for (; -1 != cpv->k_id; ++cpv) { 526 switch (cpv->k_id) { 527 case 0: /* vhostdb.<db> */ 528 if (cpv->v.a->used) { 529 if (0 != mod_vhostdb_dbconf_setup(srv, cpv->v.a, &cpv->v.v)) 530 return HANDLER_ERROR; 531 if (NULL != cpv->v.v) 532 cpv->vtype = T_CONFIG_LOCAL; 533 } 534 break; 535 default:/* should not happen */ 536 break; 537 } 538 } 539 } 540 541 /* initialize p->defaults from global config context */ 542 if (p->nconfig > 0 && p->cvlist->v.u2[1]) { 543 const config_plugin_value_t *cpv = p->cvlist + p->cvlist->v.u2[0]; 544 if (-1 != cpv->k_id) 545 mod_vhostdb_merge_config(&p->defaults, cpv); 546 } 547 548 return HANDLER_GO_ON; 549 } 550 551 552 int mod_vhostdb_ldap_plugin_init (plugin *p); 553 int mod_vhostdb_ldap_plugin_init (plugin *p) 554 { 555 p->version = LIGHTTPD_VERSION_ID; 556 p->name = "vhostdb_ldap"; 557 558 p->init = mod_vhostdb_init; 559 p->cleanup = mod_vhostdb_cleanup; 560 p->set_defaults = mod_vhostdb_set_defaults; 561 562 return 0; 563 } 564