[mod_cgi] fix pipe_cloexec() when no O_CLOEXEC
[lighttpd.git] / src / mod_authn_ldap.c
blob14153d37ee0d25d664f0f88f72415e9a67e369b0
1 #include "first.h"
3 #include "plugin.h"
5 #if defined(HAVE_LDAP_H) && defined(HAVE_LBER_H) && defined(HAVE_LIBLDAP) && defined(HAVE_LIBLBER)
7 #define USE_LDAP
8 #include <ldap.h>
10 #include "server.h"
11 #include "http_auth.h"
12 #include "log.h"
14 #include <ctype.h>
15 #include <errno.h>
16 #include <string.h>
18 typedef struct {
19 LDAP *ldap;
21 buffer *auth_ldap_hostname;
22 buffer *auth_ldap_basedn;
23 buffer *auth_ldap_binddn;
24 buffer *auth_ldap_bindpw;
25 buffer *auth_ldap_filter;
26 buffer *auth_ldap_cafile;
27 unsigned short auth_ldap_starttls;
28 unsigned short auth_ldap_allow_empty_pw;
29 } plugin_config;
31 typedef struct {
32 PLUGIN_DATA;
33 plugin_config **config_storage;
34 plugin_config conf, *anon_conf; /* this is only used as long as no handler_ctx is setup */
36 buffer *ldap_filter;
37 } plugin_data;
39 static handler_t mod_authn_ldap_basic(server *srv, connection *con, void *p_d, const http_auth_require_t *require, const buffer *username, const char *pw);
41 INIT_FUNC(mod_authn_ldap_init) {
42 static http_auth_backend_t http_auth_backend_ldap =
43 { "ldap", mod_authn_ldap_basic, NULL, NULL };
44 plugin_data *p = calloc(1, sizeof(*p));
45 p->ldap_filter = buffer_init();
47 /* register http_auth_backend_ldap */
48 http_auth_backend_ldap.p_d = p;
49 http_auth_backend_set(&http_auth_backend_ldap);
51 return p;
54 FREE_FUNC(mod_authn_ldap_free) {
55 plugin_data *p = p_d;
57 UNUSED(srv);
59 if (!p) return HANDLER_GO_ON;
61 buffer_free(p->ldap_filter);
63 if (p->config_storage) {
64 size_t i;
65 for (i = 0; i < srv->config_context->used; i++) {
66 plugin_config *s = p->config_storage[i];
68 if (NULL == s) continue;
70 buffer_free(s->auth_ldap_hostname);
71 buffer_free(s->auth_ldap_basedn);
72 buffer_free(s->auth_ldap_binddn);
73 buffer_free(s->auth_ldap_bindpw);
74 buffer_free(s->auth_ldap_filter);
75 buffer_free(s->auth_ldap_cafile);
77 if (NULL != s->ldap) ldap_unbind_ext_s(s->ldap, NULL, NULL);
78 free(s);
80 free(p->config_storage);
83 free(p);
85 return HANDLER_GO_ON;
88 SETDEFAULTS_FUNC(mod_authn_ldap_set_defaults) {
89 plugin_data *p = p_d;
90 size_t i;
91 config_values_t cv[] = {
92 { "auth.backend.ldap.hostname", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION }, /* 0 */
93 { "auth.backend.ldap.base-dn", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION }, /* 1 */
94 { "auth.backend.ldap.filter", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION }, /* 2 */
95 { "auth.backend.ldap.ca-file", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION }, /* 3 */
96 { "auth.backend.ldap.starttls", NULL, T_CONFIG_BOOLEAN, T_CONFIG_SCOPE_CONNECTION }, /* 4 */
97 { "auth.backend.ldap.bind-dn", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION }, /* 5 */
98 { "auth.backend.ldap.bind-pw", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION }, /* 6 */
99 { "auth.backend.ldap.allow-empty-pw", NULL, T_CONFIG_BOOLEAN, T_CONFIG_SCOPE_CONNECTION }, /* 7 */
100 { NULL, NULL, T_CONFIG_UNSET, T_CONFIG_SCOPE_UNSET }
103 p->config_storage = calloc(1, srv->config_context->used * sizeof(plugin_config *));
105 for (i = 0; i < srv->config_context->used; i++) {
106 data_config const* config = (data_config const*)srv->config_context->data[i];
107 plugin_config *s;
109 s = calloc(1, sizeof(plugin_config));
111 s->auth_ldap_hostname = buffer_init();
112 s->auth_ldap_basedn = buffer_init();
113 s->auth_ldap_binddn = buffer_init();
114 s->auth_ldap_bindpw = buffer_init();
115 s->auth_ldap_filter = buffer_init();
116 s->auth_ldap_cafile = buffer_init();
117 s->auth_ldap_starttls = 0;
118 s->ldap = NULL;
120 cv[0].destination = s->auth_ldap_hostname;
121 cv[1].destination = s->auth_ldap_basedn;
122 cv[2].destination = s->auth_ldap_filter;
123 cv[3].destination = s->auth_ldap_cafile;
124 cv[4].destination = &(s->auth_ldap_starttls);
125 cv[5].destination = s->auth_ldap_binddn;
126 cv[6].destination = s->auth_ldap_bindpw;
127 cv[7].destination = &(s->auth_ldap_allow_empty_pw);
129 p->config_storage[i] = s;
131 if (0 != config_insert_values_global(srv, config->value, cv, i == 0 ? T_CONFIG_SCOPE_SERVER : T_CONFIG_SCOPE_CONNECTION)) {
132 return HANDLER_ERROR;
135 if (!buffer_string_is_empty(s->auth_ldap_filter)) {
136 if (*s->auth_ldap_filter->ptr != ','
137 && NULL == strchr(s->auth_ldap_filter->ptr, '$')) {
138 log_error_write(srv, __FILE__, __LINE__, "s", "ldap: auth.backend.ldap.filter is missing a replace-operator '$'");
140 return HANDLER_ERROR;
145 return HANDLER_GO_ON;
148 #define PATCH(x) \
149 p->conf.x = s->x;
150 static int mod_authn_ldap_patch_connection(server *srv, connection *con, plugin_data *p) {
151 size_t i, j;
152 plugin_config *s = p->config_storage[0];
154 PATCH(auth_ldap_hostname);
155 PATCH(auth_ldap_basedn);
156 PATCH(auth_ldap_binddn);
157 PATCH(auth_ldap_bindpw);
158 PATCH(auth_ldap_filter);
159 PATCH(auth_ldap_cafile);
160 PATCH(auth_ldap_starttls);
161 PATCH(auth_ldap_allow_empty_pw);
162 p->anon_conf = s;
164 /* skip the first, the global context */
165 for (i = 1; i < srv->config_context->used; i++) {
166 data_config *dc = (data_config *)srv->config_context->data[i];
167 s = p->config_storage[i];
169 /* condition didn't match */
170 if (!config_check_cond(srv, con, dc)) continue;
172 /* merge config */
173 for (j = 0; j < dc->value->used; j++) {
174 data_unset *du = dc->value->data[j];
176 if (buffer_is_equal_string(du->key, CONST_STR_LEN("auth.backend.ldap.hostname"))) {
177 PATCH(auth_ldap_hostname);
178 p->anon_conf = s;
179 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("auth.backend.ldap.base-dn"))) {
180 PATCH(auth_ldap_basedn);
181 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("auth.backend.ldap.filter"))) {
182 PATCH(auth_ldap_filter);
183 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("auth.backend.ldap.ca-file"))) {
184 PATCH(auth_ldap_cafile);
185 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("auth.backend.ldap.starttls"))) {
186 PATCH(auth_ldap_starttls);
187 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("auth.backend.ldap.bind-dn"))) {
188 PATCH(auth_ldap_binddn);
189 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("auth.backend.ldap.bind-pw"))) {
190 PATCH(auth_ldap_bindpw);
191 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("auth.backend.ldap.allow-empty-pw"))) {
192 PATCH(auth_ldap_allow_empty_pw);
197 return 0;
199 #undef PATCH
201 static void mod_authn_ldap_err(server *srv, const char *file, unsigned long line, const char *fn, int err)
203 log_error_write(srv,file,line,"sSss","ldap:",fn,":",ldap_err2string(err));
206 static void mod_authn_ldap_opt_err(server *srv, const char *file, unsigned long line, const char *fn, LDAP *ld)
208 int err;
209 ldap_get_option(ld, LDAP_OPT_ERROR_NUMBER, &err);
210 mod_authn_ldap_err(srv, file, line, fn, err);
213 static LDAP * mod_authn_ldap_host_init(server *srv, plugin_config *s) {
214 LDAP *ld;
215 int ret;
217 if (buffer_string_is_empty(s->auth_ldap_hostname)) return NULL;
219 ld = ldap_init(s->auth_ldap_hostname->ptr, LDAP_PORT);
220 if (NULL == ld) {
221 log_error_write(srv, __FILE__, __LINE__, "sss", "ldap:", "ldap_init():",
222 strerror(errno));
223 return NULL;
226 ret = LDAP_VERSION3;
227 ret = ldap_set_option(ld, LDAP_OPT_PROTOCOL_VERSION, &ret);
228 if (LDAP_OPT_SUCCESS != ret) {
229 mod_authn_ldap_err(srv, __FILE__, __LINE__, "ldap_set_options()", ret);
230 ldap_memfree(ld);
231 return NULL;
234 if (s->auth_ldap_starttls) {
235 /* if no CA file is given, it is ok, as we will use encryption
236 * if the server requires a CAfile it will tell us */
237 if (!buffer_string_is_empty(s->auth_ldap_cafile)) {
238 ret = ldap_set_option(NULL, LDAP_OPT_X_TLS_CACERTFILE,
239 s->auth_ldap_cafile->ptr);
240 if (LDAP_OPT_SUCCESS != ret) {
241 mod_authn_ldap_err(srv, __FILE__, __LINE__,
242 "ldap_set_option(LDAP_OPT_X_TLS_CACERTFILE)",
243 ret);
244 ldap_memfree(ld);
245 return NULL;
249 ret = ldap_start_tls_s(ld, NULL, NULL);
250 if (LDAP_OPT_SUCCESS != ret) {
251 mod_authn_ldap_err(srv,__FILE__,__LINE__,"ldap_start_tls_s()",ret);
252 ldap_memfree(ld);
253 return NULL;
257 return ld;
260 static int mod_authn_ldap_bind(server *srv, LDAP *ld, const char *dn, const char *pw) {
261 #if 0
262 struct berval creds;
263 int ret;
265 if (NULL != pw) {
266 *((const char **)&creds.bv_val) = pw; /*(cast away const)*/
267 creds.bv_len = strlen(pw);
268 } else {
269 creds.bv_val = NULL;
270 creds.bv_len = 0;
273 /* RFE: add functionality: LDAP_SASL_EXTERNAL (or GSS-SPNEGO, etc.) */
275 ret = ldap_sasl_bind_s(ld,dn,LDAP_SASL_SIMPLE,&creds,NULL,NULL,NULL);
276 if (ret != LDAP_SUCCESS) {
277 mod_authn_ldap_err(srv, __FILE__, __LINE__, "ldap_sasl_bind_s()", ret);
279 #else
280 int ret = ldap_simple_bind_s(ld, dn, pw);
281 if (ret != LDAP_SUCCESS) {
282 mod_authn_ldap_err(srv, __FILE__, __LINE__, "ldap_simple_bind_s()",ret);
284 #endif
286 return ret;
289 static LDAPMessage * mod_authn_ldap_search(server *srv, plugin_config *s, char *base, char *filter) {
290 LDAPMessage *lm = NULL;
291 char *attrs[] = { LDAP_NO_ATTRS, NULL };
292 int ret;
295 * 1. connect anonymously (if not already connected)
296 * (ldap connection is kept open unless connection-level error occurs)
297 * 2. issue search using filter
300 if (s->ldap != NULL) {
301 ret = ldap_search_ext_s(s->ldap, base, LDAP_SCOPE_SUBTREE, filter,
302 attrs, 0, NULL, NULL, NULL, 0, &lm);
303 if (LDAP_SUCCESS == ret) {
304 return lm;
305 } else if (LDAP_SERVER_DOWN != ret) {
306 /* try again (or initial request);
307 * ldap lib sometimes fails for the first call but reconnects */
308 ret = ldap_search_ext_s(s->ldap, base, LDAP_SCOPE_SUBTREE, filter,
309 attrs, 0, NULL, NULL, NULL, 0, &lm);
310 if (LDAP_SUCCESS == ret) {
311 return lm;
315 ldap_unbind_ext_s(s->ldap, NULL, NULL);
318 s->ldap = mod_authn_ldap_host_init(srv, s);
319 if (NULL == s->ldap) {
320 return NULL;
323 ret = !buffer_string_is_empty(s->auth_ldap_binddn)
324 ? mod_authn_ldap_bind(srv, s->ldap,
325 s->auth_ldap_binddn->ptr,
326 s->auth_ldap_bindpw->ptr)
327 : mod_authn_ldap_bind(srv, s->ldap, NULL, NULL);
328 if (LDAP_SUCCESS != ret) {
329 ldap_memfree(s->ldap);
330 s->ldap = NULL;
331 return NULL;
334 ret = ldap_search_ext_s(s->ldap, base, LDAP_SCOPE_SUBTREE, filter,
335 attrs, 0, NULL, NULL, NULL, 0, &lm);
336 if (LDAP_SUCCESS != ret) {
337 log_error_write(srv, __FILE__, __LINE__, "sSss",
338 "ldap:", ldap_err2string(ret), "; filter:", filter);
339 ldap_unbind_ext_s(s->ldap, NULL, NULL);
340 s->ldap = NULL;
341 return NULL;
344 return lm;
347 static char * mod_authn_ldap_get_dn(server *srv, plugin_config *s, char *base, char *filter) {
348 LDAP *ld;
349 LDAPMessage *lm, *first;
350 char *dn;
351 int count;
353 lm = mod_authn_ldap_search(srv, s, base, filter);
354 if (NULL == lm) {
355 return NULL;
358 ld = s->ldap; /*(must be after mod_authn_ldap_search(); might reconnect)*/
360 count = ldap_count_entries(ld, lm);
361 if (0 == count) { /*(no entires found)*/
362 ldap_msgfree(lm);
363 return NULL;
364 } else if (count > 1) {
365 log_error_write(srv, __FILE__, __LINE__, "sss",
366 "ldap:", "more than one record returned. "
367 "you might have to refine the filter:", filter);
370 if (NULL == (first = ldap_first_entry(ld, lm))) {
371 mod_authn_ldap_opt_err(srv,__FILE__,__LINE__,"ldap_first_entry()",ld);
372 ldap_msgfree(lm);
373 return NULL;
376 if (NULL == (dn = ldap_get_dn(ld, first))) {
377 mod_authn_ldap_opt_err(srv,__FILE__,__LINE__,"ldap_get_dn()",ld);
378 ldap_msgfree(lm);
379 return NULL;
382 ldap_msgfree(lm);
383 return dn;
386 static handler_t mod_authn_ldap_basic(server *srv, connection *con, void *p_d, const http_auth_require_t *require, const buffer *username, const char *pw) {
387 plugin_data *p = (plugin_data *)p_d;
388 LDAP *ld;
389 char *dn;
390 buffer *template;
392 mod_authn_ldap_patch_connection(srv, con, p);
394 if (pw[0] == '\0' && !p->conf.auth_ldap_allow_empty_pw)
395 return HANDLER_ERROR;
397 /* check username
399 * we have to protect againt username which modifies our filter in
400 * an unpleasant way
403 for (size_t i = 0, len = buffer_string_length(username); i < len; i++) {
404 char c = username->ptr[i];
406 if (!isalpha(c) &&
407 !isdigit(c) &&
408 (c != ' ') &&
409 (c != '@') &&
410 (c != '-') &&
411 (c != '_') &&
412 (c != '.') ) {
414 log_error_write(srv, __FILE__, __LINE__, "sbd",
415 "ldap: invalid character (- _.@a-zA-Z0-9 allowed) "
416 "in username:", username, i);
418 con->http_status = 400; /* Bad Request */
419 con->mode = DIRECT;
420 return HANDLER_FINISHED;
424 template = p->conf.auth_ldap_filter;
425 if (buffer_string_is_empty(template)) {
426 return HANDLER_ERROR;
429 /* build filter to get DN for uid = username */
430 buffer_string_set_length(p->ldap_filter, 0);
431 if (*template->ptr == ',') {
432 /* special-case filter template beginning with ',' to be explicit DN */
433 buffer_append_string_len(p->ldap_filter, CONST_STR_LEN("uid="));
434 buffer_append_string_buffer(p->ldap_filter, username);
435 buffer_append_string_buffer(p->ldap_filter, template);
436 dn = p->ldap_filter->ptr;
437 } else {
438 for (char *b = template->ptr, *d; *b; b = d+1) {
439 if (NULL != (d = strchr(b, '$'))) {
440 buffer_append_string_len(p->ldap_filter, b, (size_t)(d - b));
441 buffer_append_string_buffer(p->ldap_filter, username);
442 } else {
443 d = template->ptr + buffer_string_length(template);
444 buffer_append_string_len(p->ldap_filter, b, (size_t)(d - b));
445 break;
449 /* ldap_search for DN (synchronous; blocking) */
450 dn = mod_authn_ldap_get_dn(srv, p->anon_conf,
451 p->conf.auth_ldap_basedn->ptr,
452 p->ldap_filter->ptr);
453 if (NULL == dn) {
454 return HANDLER_ERROR;
458 /* auth against LDAP server (synchronous; blocking) */
460 ld = mod_authn_ldap_host_init(srv, &p->conf);
461 if (NULL == ld) {
462 if (dn != p->ldap_filter->ptr) ldap_memfree(dn);
463 return HANDLER_ERROR;
466 if (LDAP_SUCCESS != mod_authn_ldap_bind(srv, ld, dn, pw)) {
467 ldap_memfree(ld);
468 if (dn != p->ldap_filter->ptr) ldap_memfree(dn);
469 return HANDLER_ERROR;
472 ldap_unbind_ext_s(ld, NULL, NULL); /* disconnect */
473 if (dn != p->ldap_filter->ptr) ldap_memfree(dn);
474 return http_auth_match_rules(require, username->ptr, NULL, NULL)
475 ? HANDLER_GO_ON /* access granted */
476 : HANDLER_ERROR;
479 int mod_authn_ldap_plugin_init(plugin *p);
480 int mod_authn_ldap_plugin_init(plugin *p) {
481 p->version = LIGHTTPD_VERSION_ID;
482 p->name = buffer_init_string("authn_ldap");
483 p->init = mod_authn_ldap_init;
484 p->set_defaults = mod_authn_ldap_set_defaults;
485 p->cleanup = mod_authn_ldap_free;
487 p->data = NULL;
489 return 0;
492 #else
494 int mod_authn_ldap_plugin_init(plugin *p);
495 int mod_authn_ldap_plugin_init(plugin *p) {
496 UNUSED(p);
497 return -1;
500 #endif