1 /* 2 * mod_authn_ldap - HTTP Auth LDAP backend 3 * 4 * Fully-rewritten from original 5 * Copyright(c) 2016 Glenn Strauss gstrauss()gluelogic.com All rights reserved 6 * License: BSD 3-clause (same as lighttpd) 7 */ 8 #include "first.h" 9 10 #include <ldap.h> 11 12 #include "base.h" 13 #include "http_auth.h" 14 #include "log.h" 15 #include "plugin.h" 16 17 #include <stdlib.h> 18 #include <string.h> 19 20 typedef struct { 21 LDAP *ldap; 22 log_error_st *errh; 23 const char *auth_ldap_hostname; 24 const char *auth_ldap_binddn; 25 const char *auth_ldap_bindpw; 26 const char *auth_ldap_cafile; 27 int auth_ldap_starttls; 28 } plugin_config_ldap; 29 30 typedef struct { 31 plugin_config_ldap *ldc; 32 const char *auth_ldap_basedn; 33 const buffer *auth_ldap_filter; 34 const buffer *auth_ldap_groupmember; 35 int auth_ldap_allow_empty_pw; 36 37 int auth_ldap_starttls; 38 const char *auth_ldap_binddn; 39 const char *auth_ldap_bindpw; 40 const char *auth_ldap_cafile; 41 } plugin_config; 42 43 typedef struct { 44 PLUGIN_DATA; 45 plugin_config defaults; 46 plugin_config conf; 47 48 buffer ldap_filter; 49 } plugin_data; 50 51 static handler_t mod_authn_ldap_basic(request_st * const r, void *p_d, const http_auth_require_t *require, const buffer *username, const char *pw); 52 53 INIT_FUNC(mod_authn_ldap_init) { 54 static http_auth_backend_t http_auth_backend_ldap = 55 { "ldap", mod_authn_ldap_basic, NULL, NULL }; 56 plugin_data *p = calloc(1, sizeof(*p)); 57 58 /* register http_auth_backend_ldap */ 59 http_auth_backend_ldap.p_d = p; 60 http_auth_backend_set(&http_auth_backend_ldap); 61 62 return p; 63 } 64 65 FREE_FUNC(mod_authn_ldap_free) { 66 plugin_data * const p = p_d; 67 if (NULL == p->cvlist) return; 68 /* (init i to 0 if global context; to 1 to skip empty global context) */ 69 for (int i = !p->cvlist[0].v.u2[1], used = p->nconfig; i < used; ++i) { 70 config_plugin_value_t *cpv = p->cvlist + p->cvlist[i].v.u2[0]; 71 for (; -1 != cpv->k_id; ++cpv) { 72 switch (cpv->k_id) { 73 case 0: /* auth.backend.ldap.hostname */ 74 if (cpv->vtype == T_CONFIG_LOCAL) { 75 plugin_config_ldap *s = cpv->v.v; 76 if (NULL != s->ldap) ldap_unbind_ext_s(s->ldap, NULL, NULL); 77 free(s); 78 } 79 break; 80 default: 81 break; 82 } 83 } 84 } 85 86 free(p->ldap_filter.ptr); 87 } 88 89 static void mod_authn_ldap_merge_config_cpv(plugin_config * const pconf, const config_plugin_value_t * const cpv) { 90 switch (cpv->k_id) { /* index into static config_plugin_keys_t cpk[] */ 91 case 0: /* auth.backend.ldap.hostname */ 92 if (cpv->vtype == T_CONFIG_LOCAL) 93 pconf->ldc = cpv->v.v; 94 break; 95 case 1: /* auth.backend.ldap.base-dn */ 96 if (cpv->vtype == T_CONFIG_LOCAL) 97 pconf->auth_ldap_basedn = cpv->v.v; 98 break; 99 case 2: /* auth.backend.ldap.filter */ 100 pconf->auth_ldap_filter = cpv->v.v; 101 break; 102 case 3: /* auth.backend.ldap.ca-file */ 103 pconf->auth_ldap_cafile = cpv->v.v; 104 break; 105 case 4: /* auth.backend.ldap.starttls */ 106 pconf->auth_ldap_starttls = (int)cpv->v.u; 107 break; 108 case 5: /* auth.backend.ldap.bind-dn */ 109 pconf->auth_ldap_binddn = cpv->v.v; 110 break; 111 case 6: /* auth.backend.ldap.bind-pw */ 112 pconf->auth_ldap_bindpw = cpv->v.v; 113 break; 114 case 7: /* auth.backend.ldap.allow-empty-pw */ 115 pconf->auth_ldap_allow_empty_pw = (int)cpv->v.u; 116 break; 117 case 8: /* auth.backend.ldap.groupmember */ 118 pconf->auth_ldap_groupmember = cpv->v.b; 119 break; 120 default:/* should not happen */ 121 return; 122 } 123 } 124 125 static void mod_authn_ldap_merge_config(plugin_config * const pconf, const config_plugin_value_t *cpv) { 126 do { 127 mod_authn_ldap_merge_config_cpv(pconf, cpv); 128 } while ((++cpv)->k_id != -1); 129 } 130 131 static void mod_authn_ldap_patch_config(request_st * const r, plugin_data * const p) { 132 memcpy(&p->conf, &p->defaults, sizeof(plugin_config)); 133 for (int i = 1, used = p->nconfig; i < used; ++i) { 134 if (config_check_cond(r, (uint32_t)p->cvlist[i].k_id)) 135 mod_authn_ldap_merge_config(&p->conf, 136 p->cvlist + p->cvlist[i].v.u2[0]); 137 } 138 } 139 140 /*(copied from mod_vhostdb_ldap.c)*/ 141 static void mod_authn_add_scheme (server *srv, buffer *host) 142 { 143 if (!buffer_string_is_empty(host)) { 144 /* reformat hostname(s) as LDAP URIs (scheme://host:port) */ 145 static const char *schemes[] = { 146 "ldap://", "ldaps://", "ldapi://", "cldap://" 147 }; 148 char *b, *e = host->ptr; 149 buffer * const tb = srv->tmp_buf; 150 buffer_clear(tb); 151 while (*(b = e)) { 152 unsigned int j; 153 while (*b==' '||*b=='\t'||*b=='\r'||*b=='\n'||*b==',') ++b; 154 if (*b == '\0') break; 155 e = b; 156 while (*e!=' '&&*e!='\t'&&*e!='\r'&&*e!='\n'&&*e!=','&&*e!='\0') 157 ++e; 158 if (!buffer_string_is_empty(tb)) 159 buffer_append_string_len(tb, CONST_STR_LEN(",")); 160 for (j = 0; j < sizeof(schemes)/sizeof(char *); ++j) { 161 if (buffer_eq_icase_ssn(b, schemes[j], strlen(schemes[j]))) { 162 break; 163 } 164 } 165 if (j == sizeof(schemes)/sizeof(char *)) 166 buffer_append_string_len(tb, CONST_STR_LEN("ldap://")); 167 buffer_append_string_len(tb, b, (size_t)(e - b)); 168 } 169 buffer_copy_buffer(host, tb); 170 } 171 } 172 173 SETDEFAULTS_FUNC(mod_authn_ldap_set_defaults) { 174 static const config_plugin_keys_t cpk[] = { 175 { CONST_STR_LEN("auth.backend.ldap.hostname"), 176 T_CONFIG_STRING, 177 T_CONFIG_SCOPE_CONNECTION } 178 ,{ CONST_STR_LEN("auth.backend.ldap.base-dn"), 179 T_CONFIG_STRING, 180 T_CONFIG_SCOPE_CONNECTION } 181 ,{ CONST_STR_LEN("auth.backend.ldap.filter"), 182 T_CONFIG_STRING, 183 T_CONFIG_SCOPE_CONNECTION } 184 ,{ CONST_STR_LEN("auth.backend.ldap.ca-file"), 185 T_CONFIG_STRING, 186 T_CONFIG_SCOPE_CONNECTION } 187 ,{ CONST_STR_LEN("auth.backend.ldap.starttls"), 188 T_CONFIG_BOOL, 189 T_CONFIG_SCOPE_CONNECTION } 190 ,{ CONST_STR_LEN("auth.backend.ldap.bind-dn"), 191 T_CONFIG_STRING, 192 T_CONFIG_SCOPE_CONNECTION } 193 ,{ CONST_STR_LEN("auth.backend.ldap.bind-pw"), 194 T_CONFIG_STRING, 195 T_CONFIG_SCOPE_CONNECTION } 196 ,{ CONST_STR_LEN("auth.backend.ldap.allow-empty-pw"), 197 T_CONFIG_BOOL, 198 T_CONFIG_SCOPE_CONNECTION } 199 ,{ CONST_STR_LEN("auth.backend.ldap.groupmember"), 200 T_CONFIG_STRING, 201 T_CONFIG_SCOPE_CONNECTION } 202 ,{ NULL, 0, 203 T_CONFIG_UNSET, 204 T_CONFIG_SCOPE_UNSET } 205 }; 206 207 plugin_data * const p = p_d; 208 if (!config_plugin_values_init(srv, p, cpk, "mod_authn_ldap")) 209 return HANDLER_ERROR; 210 211 /* process and validate config directives 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]; i < p->nconfig; ++i) { 214 config_plugin_value_t *cpv = p->cvlist + p->cvlist[i].v.u2[0]; 215 plugin_config_ldap *ldc = NULL; 216 char *binddn = NULL, *bindpw = NULL, *cafile = NULL; 217 int starttls = 0; 218 for (; -1 != cpv->k_id; ++cpv) { 219 switch (cpv->k_id) { 220 case 0: /* auth.backend.ldap.hostname */ 221 if (!buffer_string_is_empty(cpv->v.b)) { 222 buffer *b; 223 *(const buffer **)&b = cpv->v.b; 224 mod_authn_add_scheme(srv, b); 225 ldc = malloc(sizeof(plugin_config_ldap)); 226 force_assert(ldc); 227 ldc->errh = srv->errh; 228 ldc->auth_ldap_hostname = b->ptr; 229 cpv->v.v = ldc; 230 } 231 else { 232 cpv->v.v = NULL; 233 } 234 cpv->vtype = T_CONFIG_LOCAL; 235 break; 236 case 1: /* auth.backend.ldap.base-dn */ 237 cpv->vtype = T_CONFIG_LOCAL; 238 cpv->v.v = !buffer_string_is_empty(cpv->v.b) 239 ? cpv->v.b->ptr 240 : NULL; 241 break; 242 case 2: /* auth.backend.ldap.filter */ 243 if (!buffer_string_is_empty(cpv->v.b)) { 244 buffer *b; 245 *(const buffer **)&b = cpv->v.b; 246 if (*b->ptr != ',') { 247 /*(translate $ to ? for consistency w/ other modules)*/ 248 char *d = b->ptr; 249 for (; NULL != (d = strchr(d, '$')); ++d) *d = '?'; 250 if (NULL == strchr(b->ptr, '?')) { 251 log_error(srv->errh, __FILE__, __LINE__, 252 "ldap: %s is missing a replace-operator '?'", 253 cpk[cpv->k_id].k); 254 return HANDLER_ERROR; 255 } 256 } 257 cpv->v.v = b; 258 } 259 else { 260 cpv->v.v = NULL; 261 } 262 cpv->vtype = T_CONFIG_LOCAL; 263 break; 264 case 3: /* auth.backend.ldap.ca-file */ 265 cafile = !buffer_string_is_empty(cpv->v.b) 266 ? cpv->v.b->ptr 267 : NULL; 268 cpv->vtype = T_CONFIG_LOCAL; 269 cpv->v.v = cafile; 270 break; 271 case 4: /* auth.backend.ldap.starttls */ 272 starttls = (int)cpv->v.u; 273 break; 274 case 5: /* auth.backend.ldap.bind-dn */ 275 binddn = !buffer_string_is_empty(cpv->v.b) 276 ? cpv->v.b->ptr 277 : NULL; 278 cpv->vtype = T_CONFIG_LOCAL; 279 cpv->v.v = binddn; 280 break; 281 case 6: /* auth.backend.ldap.bind-pw */ 282 cpv->vtype = T_CONFIG_LOCAL; 283 cpv->v.v = bindpw = cpv->v.b->ptr; 284 break; 285 case 7: /* auth.backend.ldap.allow-empty-pw */ 286 case 8: /* auth.backend.ldap.groupmember */ 287 break; 288 default:/* should not happen */ 289 break; 290 } 291 } 292 293 if (ldc) { 294 ldc->auth_ldap_binddn = binddn; 295 ldc->auth_ldap_bindpw = bindpw; 296 ldc->auth_ldap_cafile = cafile; 297 ldc->auth_ldap_starttls = starttls; 298 } 299 } 300 301 static const struct { const char *ptr; uint32_t used; uint32_t size; } 302 memberUid = { "memberUid", sizeof("memberUid"), 0 }; 303 *(const buffer **)&p->defaults.auth_ldap_groupmember = 304 (const buffer *)&memberUid; 305 306 /* initialize p->defaults from global config context */ 307 if (p->nconfig > 0 && p->cvlist->v.u2[1]) { 308 const config_plugin_value_t *cpv = p->cvlist + p->cvlist->v.u2[0]; 309 if (-1 != cpv->k_id) 310 mod_authn_ldap_merge_config(&p->defaults, cpv); 311 } 312 313 return HANDLER_GO_ON; 314 } 315 316 __attribute_cold__ 317 static void mod_authn_ldap_err(log_error_st *errh, const char *file, unsigned long line, const char *fn, int err) 318 { 319 log_error(errh, file, line, "ldap: %s: %s", fn, ldap_err2string(err)); 320 } 321 322 __attribute_cold__ 323 static void mod_authn_ldap_opt_err(log_error_st *errh, const char *file, unsigned long line, const char *fn, LDAP *ld) 324 { 325 int err; 326 ldap_get_option(ld, LDAP_OPT_ERROR_NUMBER, &err); 327 mod_authn_ldap_err(errh, file, line, fn, err); 328 } 329 330 static void mod_authn_append_ldap_dn_escape(buffer * const filter, const buffer * const raw) { 331 /* [RFC4514] 2.4 Converting an AttributeValue from ASN.1 to a String 332 * 333 * https://www.ldap.com/ldap-dns-and-rdns 334 * http://social.technet.microsoft.com/wiki/contents/articles/5312.active-directory-characters-to-escape.aspx 335 */ 336 const char * const b = raw->ptr; 337 const size_t rlen = buffer_string_length(raw); 338 if (0 == rlen) return; 339 340 if (b[0] == ' ') { /* || b[0] == '#' handled below for MS Active Directory*/ 341 /* escape leading ' ' */ 342 buffer_append_string_len(filter, CONST_STR_LEN("\\")); 343 } 344 345 for (size_t i = 0; i < rlen; ++i) { 346 size_t len = i; 347 int bs = 0; 348 do { 349 /* encode all UTF-8 chars with high bit set 350 * (instead of validating UTF-8 and escaping only invalid UTF-8) */ 351 if (((unsigned char *)b)[len] > 0x7f) 352 break; 353 switch (b[len]) { 354 default: 355 continue; 356 case '"': case '+': case ',': case ';': case '\\': 357 case '<': case '>': 358 case '=': case '#': /* (for MS Active Directory) */ 359 bs = 1; 360 break; 361 case '\0': 362 break; 363 } 364 break; 365 } while (++len < rlen); 366 len -= i; 367 368 if (len) { 369 buffer_append_string_len(filter, b+i, len); 370 if ((i += len) == rlen) break; 371 } 372 373 if (bs) { 374 buffer_append_string_len(filter, CONST_STR_LEN("\\")); 375 buffer_append_string_len(filter, b+i, 1); 376 } 377 else { 378 /* escape NUL ('\0') (and all UTF-8 chars with high bit set) */ 379 char *f; 380 buffer_string_prepare_append(filter, 3); 381 f = filter->ptr + buffer_string_length(filter); 382 f[0] = '\\'; 383 f[1] = "0123456789abcdef"[(((unsigned char *)b)[i] >> 4) & 0xf]; 384 f[2] = "0123456789abcdef"[(((unsigned char *)b)[i] ) & 0xf]; 385 buffer_commit(filter, 3); 386 } 387 } 388 389 if (rlen > 1 && b[rlen-1] == ' ') { 390 /* escape trailing ' ' */ 391 filter->ptr[buffer_string_length(filter)-1] = '\\'; 392 buffer_append_string_len(filter, CONST_STR_LEN(" ")); 393 } 394 } 395 396 static void mod_authn_append_ldap_filter_escape(buffer * const filter, const buffer * const raw) { 397 /* [RFC4515] 3. String Search Filter Definition 398 * 399 * [...] 400 * 401 * The <valueencoding> rule ensures that the entire filter string is a 402 * valid UTF-8 string and provides that the octets that represent the 403 * ASCII characters "*" (ASCII 0x2a), "(" (ASCII 0x28), ")" (ASCII 404 * 0x29), "\" (ASCII 0x5c), and NUL (ASCII 0x00) are represented as a 405 * backslash "\" (ASCII 0x5c) followed by the two hexadecimal digits 406 * representing the value of the encoded octet. 407 * 408 * [...] 409 * 410 * As indicated by the <valueencoding> rule, implementations MUST escape 411 * all octets greater than 0x7F that are not part of a valid UTF-8 412 * encoding sequence when they generate a string representation of a 413 * search filter. Implementations SHOULD accept as input strings that 414 * are not valid UTF-8 strings. This is necessary because RFC 2254 did 415 * not clearly define the term "string representation" (and in 416 * particular did not mention that the string representation of an LDAP 417 * search filter is a string of UTF-8-encoded Unicode characters). 418 * 419 * 420 * https://www.ldap.com/ldap-filters 421 * Although not required, you may escape any other characters that you want 422 * in the assertion value (or substring component) of a filter. This may be 423 * accomplished by prefixing the hexadecimal representation of each byte of 424 * the UTF-8 encoding of the character to escape with a backslash character. 425 */ 426 const char * const b = raw->ptr; 427 const size_t rlen = buffer_string_length(raw); 428 for (size_t i = 0; i < rlen; ++i) { 429 size_t len = i; 430 char *f; 431 do { 432 /* encode all UTF-8 chars with high bit set 433 * (instead of validating UTF-8 and escaping only invalid UTF-8) */ 434 if (((unsigned char *)b)[len] > 0x7f) 435 break; 436 switch (b[len]) { 437 default: 438 continue; 439 case '\0': case '(': case ')': case '*': case '\\': 440 break; 441 } 442 break; 443 } while (++len < rlen); 444 len -= i; 445 446 if (len) { 447 buffer_append_string_len(filter, b+i, len); 448 if ((i += len) == rlen) break; 449 } 450 451 /* escape * ( ) \ NUL ('\0') (and all UTF-8 chars with high bit set) */ 452 buffer_string_prepare_append(filter, 3); 453 f = filter->ptr + buffer_string_length(filter); 454 f[0] = '\\'; 455 f[1] = "0123456789abcdef"[(((unsigned char *)b)[i] >> 4) & 0xf]; 456 f[2] = "0123456789abcdef"[(((unsigned char *)b)[i] ) & 0xf]; 457 buffer_commit(filter, 3); 458 } 459 } 460 461 static LDAP * mod_authn_ldap_host_init(log_error_st *errh, plugin_config_ldap *s) { 462 LDAP *ld; 463 int ret; 464 465 if (NULL == s->auth_ldap_hostname) return NULL; 466 467 if (LDAP_SUCCESS != ldap_initialize(&ld, s->auth_ldap_hostname)) { 468 log_perror(errh, __FILE__, __LINE__, "ldap: ldap_initialize()"); 469 return NULL; 470 } 471 472 ret = LDAP_VERSION3; 473 ret = ldap_set_option(ld, LDAP_OPT_PROTOCOL_VERSION, &ret); 474 if (LDAP_OPT_SUCCESS != ret) { 475 mod_authn_ldap_err(errh, __FILE__, __LINE__, "ldap_set_option()", ret); 476 ldap_destroy(ld); 477 return NULL; 478 } 479 480 /* restart ldap functions if interrupted by a signal, e.g. SIGCHLD */ 481 ldap_set_option(ld, LDAP_OPT_RESTART, LDAP_OPT_ON); 482 483 if (s->auth_ldap_starttls) { 484 /* if no CA file is given, it is ok, as we will use encryption 485 * if the server requires a CAfile it will tell us */ 486 if (s->auth_ldap_cafile) { 487 ret = ldap_set_option(NULL, LDAP_OPT_X_TLS_CACERTFILE, 488 s->auth_ldap_cafile); 489 if (LDAP_OPT_SUCCESS != ret) { 490 mod_authn_ldap_err(errh, __FILE__, __LINE__, 491 "ldap_set_option(LDAP_OPT_X_TLS_CACERTFILE)", 492 ret); 493 ldap_destroy(ld); 494 return NULL; 495 } 496 } 497 498 ret = ldap_start_tls_s(ld, NULL, NULL); 499 if (LDAP_OPT_SUCCESS != ret) { 500 mod_authn_ldap_err(errh,__FILE__,__LINE__,"ldap_start_tls_s()",ret); 501 ldap_destroy(ld); 502 return NULL; 503 } 504 } 505 506 return ld; 507 } 508 509 static int mod_authn_ldap_bind(log_error_st *errh, LDAP *ld, const char *dn, const char *pw) { 510 struct berval creds; 511 int ret; 512 513 if (NULL != pw) { 514 *((const char **)&creds.bv_val) = pw; /*(cast away const)*/ 515 creds.bv_len = strlen(pw); 516 } else { 517 creds.bv_val = NULL; 518 creds.bv_len = 0; 519 } 520 521 /* RFE: add functionality: LDAP_SASL_EXTERNAL (or GSS-SPNEGO, etc.) */ 522 523 ret = ldap_sasl_bind_s(ld,dn,LDAP_SASL_SIMPLE,&creds,NULL,NULL,NULL); 524 if (ret != LDAP_SUCCESS) { 525 mod_authn_ldap_err(errh, __FILE__, __LINE__, "ldap_sasl_bind_s()", ret); 526 } 527 528 return ret; 529 } 530 531 static int mod_authn_ldap_rebind_proc (LDAP *ld, LDAP_CONST char *url, ber_tag_t ldap_request, ber_int_t msgid, void *params) { 532 const plugin_config_ldap *s = (const plugin_config_ldap *)params; 533 UNUSED(url); 534 UNUSED(ldap_request); 535 UNUSED(msgid); 536 return s->auth_ldap_binddn 537 ? mod_authn_ldap_bind(s->errh, ld, 538 s->auth_ldap_binddn, 539 s->auth_ldap_bindpw) 540 : mod_authn_ldap_bind(s->errh, ld, NULL, NULL); 541 } 542 543 static LDAPMessage * mod_authn_ldap_search(log_error_st *errh, plugin_config_ldap *s, const char *base, const char *filter) { 544 LDAPMessage *lm = NULL; 545 char *attrs[] = { LDAP_NO_ATTRS, NULL }; 546 int ret; 547 548 /* 549 * 1. connect anonymously (if not already connected) 550 * (ldap connection is kept open unless connection-level error occurs) 551 * 2. issue search using filter 552 */ 553 554 if (s->ldap != NULL) { 555 ret = ldap_search_ext_s(s->ldap, base, LDAP_SCOPE_SUBTREE, filter, 556 attrs, 0, NULL, NULL, NULL, 0, &lm); 557 if (LDAP_SUCCESS == ret) { 558 return lm; 559 } else if (LDAP_SERVER_DOWN != ret) { 560 /* try again (or initial request); 561 * ldap lib sometimes fails for the first call but reconnects */ 562 ret = ldap_search_ext_s(s->ldap, base, LDAP_SCOPE_SUBTREE, filter, 563 attrs, 0, NULL, NULL, NULL, 0, &lm); 564 if (LDAP_SUCCESS == ret) { 565 return lm; 566 } 567 } 568 569 ldap_unbind_ext_s(s->ldap, NULL, NULL); 570 } 571 572 s->ldap = mod_authn_ldap_host_init(errh, s); 573 if (NULL == s->ldap) { 574 return NULL; 575 } 576 577 ldap_set_rebind_proc(s->ldap, mod_authn_ldap_rebind_proc, s); 578 ret = mod_authn_ldap_rebind_proc(s->ldap, NULL, 0, 0, s); 579 if (LDAP_SUCCESS != ret) { 580 ldap_destroy(s->ldap); 581 s->ldap = NULL; 582 return NULL; 583 } 584 585 ret = ldap_search_ext_s(s->ldap, base, LDAP_SCOPE_SUBTREE, filter, 586 attrs, 0, NULL, NULL, NULL, 0, &lm); 587 if (LDAP_SUCCESS != ret) { 588 log_error(errh, __FILE__, __LINE__, 589 "ldap: %s; filter: %s", ldap_err2string(ret), filter); 590 ldap_unbind_ext_s(s->ldap, NULL, NULL); 591 s->ldap = NULL; 592 return NULL; 593 } 594 595 return lm; 596 } 597 598 static char * mod_authn_ldap_get_dn(log_error_st *errh, plugin_config_ldap *s, const char *base, const char *filter) { 599 LDAP *ld; 600 LDAPMessage *lm, *first; 601 char *dn; 602 int count; 603 604 lm = mod_authn_ldap_search(errh, s, base, filter); 605 if (NULL == lm) { 606 return NULL; 607 } 608 609 ld = s->ldap; /*(must be after mod_authn_ldap_search(); might reconnect)*/ 610 611 count = ldap_count_entries(ld, lm); 612 if (0 == count) { /*(no entries found)*/ 613 ldap_msgfree(lm); 614 return NULL; 615 } else if (count > 1) { 616 log_error(errh, __FILE__, __LINE__, 617 "ldap: more than one record returned. " 618 "you might have to refine the filter: %s", filter); 619 } 620 621 if (NULL == (first = ldap_first_entry(ld, lm))) { 622 mod_authn_ldap_opt_err(errh,__FILE__,__LINE__,"ldap_first_entry()",ld); 623 ldap_msgfree(lm); 624 return NULL; 625 } 626 627 if (NULL == (dn = ldap_get_dn(ld, first))) { 628 mod_authn_ldap_opt_err(errh,__FILE__,__LINE__,"ldap_get_dn()",ld); 629 ldap_msgfree(lm); 630 return NULL; 631 } 632 633 ldap_msgfree(lm); 634 return dn; 635 } 636 637 static handler_t mod_authn_ldap_memberOf(log_error_st *errh, plugin_config *s, const http_auth_require_t *require, const buffer *username, const char *userdn) { 638 const array *groups = &require->group; 639 buffer *filter = buffer_init(); 640 handler_t rc = HANDLER_ERROR; 641 642 buffer_copy_string_len(filter, CONST_STR_LEN("(")); 643 buffer_append_string_buffer(filter, s->auth_ldap_groupmember); 644 buffer_append_string_len(filter, CONST_STR_LEN("=")); 645 if (buffer_is_equal_string(s->auth_ldap_groupmember, 646 CONST_STR_LEN("member"))) { 647 buffer_append_string(filter, userdn); 648 } else { /*(assume "memberUid"; consider validating in SETDEFAULTS_FUNC)*/ 649 mod_authn_append_ldap_filter_escape(filter, username); 650 } 651 buffer_append_string_len(filter, CONST_STR_LEN(")")); 652 653 plugin_config_ldap * const ldc = s->ldc; 654 for (size_t i = 0; i < groups->used; ++i) { 655 const char *base = groups->data[i]->key.ptr; 656 LDAPMessage *lm = mod_authn_ldap_search(errh, ldc, base, filter->ptr); 657 if (NULL != lm) { 658 int count = ldap_count_entries(ldc->ldap, lm); 659 ldap_msgfree(lm); 660 if (count > 0) { 661 rc = HANDLER_GO_ON; 662 break; 663 } 664 } 665 } 666 667 buffer_free(filter); 668 return rc; 669 } 670 671 static handler_t mod_authn_ldap_basic(request_st * const r, void *p_d, const http_auth_require_t * const require, const buffer * const username, const char * const pw) { 672 plugin_data *p = (plugin_data *)p_d; 673 LDAP *ld; 674 char *dn; 675 676 mod_authn_ldap_patch_config(r, p); 677 678 if (pw[0] == '\0' && !p->conf.auth_ldap_allow_empty_pw) 679 return HANDLER_ERROR; 680 681 const buffer * const template = p->conf.auth_ldap_filter; 682 if (NULL == template) 683 return HANDLER_ERROR; 684 685 log_error_st * const errh = r->conf.errh; 686 687 /* build filter to get DN for uid = username */ 688 buffer * const ldap_filter = &p->ldap_filter; 689 buffer_clear(ldap_filter); 690 if (*template->ptr == ',') { 691 /* special-case filter template beginning with ',' to be explicit DN */ 692 buffer_append_string_len(ldap_filter, CONST_STR_LEN("uid=")); 693 mod_authn_append_ldap_dn_escape(ldap_filter, username); 694 buffer_append_string_buffer(ldap_filter, template); 695 dn = ldap_filter->ptr; 696 } 697 else { 698 for (const char *b = template->ptr, *d; *b; b = d+1) { 699 if (NULL != (d = strchr(b, '?'))) { 700 buffer_append_string_len(ldap_filter, b, (size_t)(d - b)); 701 mod_authn_append_ldap_filter_escape(ldap_filter, username); 702 } 703 else { 704 d = template->ptr + buffer_string_length(template); 705 buffer_append_string_len(ldap_filter, b, (size_t)(d - b)); 706 break; 707 } 708 } 709 710 /* ldap_search for DN (synchronous; blocking) */ 711 dn = mod_authn_ldap_get_dn(errh, p->conf.ldc, 712 p->conf.auth_ldap_basedn, ldap_filter->ptr); 713 if (NULL == dn) return HANDLER_ERROR; 714 } 715 716 /*(Check ldc here rather than further up to preserve historical behavior 717 * where p->conf.ldc above (was p->anon_conf above) is set of directives in 718 * same context as auth_ldap_hostname. Preference: admin intentions are 719 * clearer if directives are always together in a set in same context)*/ 720 721 plugin_config_ldap * const ldc_base = p->conf.ldc; 722 plugin_config_ldap ldc_custom; 723 724 if ( p->conf.ldc->auth_ldap_starttls != p->conf.auth_ldap_starttls 725 || p->conf.ldc->auth_ldap_binddn != p->conf.auth_ldap_binddn 726 || p->conf.ldc->auth_ldap_bindpw != p->conf.auth_ldap_bindpw 727 || p->conf.ldc->auth_ldap_cafile != p->conf.auth_ldap_cafile ) { 728 ldc_custom.ldap = NULL; 729 ldc_custom.errh = errh; 730 ldc_custom.auth_ldap_hostname = ldc_base->auth_ldap_hostname; 731 ldc_custom.auth_ldap_starttls = p->conf.auth_ldap_starttls; 732 ldc_custom.auth_ldap_binddn = p->conf.auth_ldap_binddn; 733 ldc_custom.auth_ldap_bindpw = p->conf.auth_ldap_bindpw; 734 ldc_custom.auth_ldap_cafile = p->conf.auth_ldap_cafile; 735 p->conf.ldc = &ldc_custom; 736 } 737 738 handler_t rc = HANDLER_ERROR; 739 do { 740 /* auth against LDAP server (synchronous; blocking) */ 741 742 ld = mod_authn_ldap_host_init(errh, p->conf.ldc); 743 if (NULL == ld) 744 break; 745 746 /* Disable referral tracking; target user should be in provided scope */ 747 int ret = ldap_set_option(ld, LDAP_OPT_REFERRALS, LDAP_OPT_OFF); 748 if (LDAP_OPT_SUCCESS != ret) { 749 mod_authn_ldap_err(errh,__FILE__,__LINE__,"ldap_set_option()",ret); 750 break; 751 } 752 753 if (LDAP_SUCCESS != mod_authn_ldap_bind(errh, ld, dn, pw)) 754 break; 755 756 ldap_unbind_ext_s(ld, NULL, NULL); /* disconnect */ 757 ld = NULL; 758 759 if (http_auth_match_rules(require, username->ptr, NULL, NULL)) { 760 rc = HANDLER_GO_ON; /* access granted */ 761 } 762 else if (require->group.used) { 763 /*(must not re-use ldap_filter, since it might be used for dn)*/ 764 rc = mod_authn_ldap_memberOf(errh,&p->conf,require,username,dn); 765 } 766 } while (0); 767 768 if (NULL != ld) ldap_destroy(ld); 769 if (ldc_base != p->conf.ldc && NULL != p->conf.ldc->ldap) 770 ldap_unbind_ext_s(p->conf.ldc->ldap, NULL, NULL); 771 if (dn != ldap_filter->ptr) ldap_memfree(dn); 772 return rc; 773 } 774 775 int mod_authn_ldap_plugin_init(plugin *p); 776 int mod_authn_ldap_plugin_init(plugin *p) { 777 p->version = LIGHTTPD_VERSION_ID; 778 p->name = "authn_ldap"; 779 p->init = mod_authn_ldap_init; 780 p->set_defaults = mod_authn_ldap_set_defaults; 781 p->cleanup = mod_authn_ldap_free; 782 783 return 0; 784 } 785