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