[core] consolidate duplicated read-to-close code
[lighttpd.git] / src / mod_authn_ldap.c
bloba75c2a40f8f8e193484d8a0503c3148182753d9e
1 #include "first.h"
3 #define USE_LDAP
4 #include <ldap.h>
6 #include "server.h"
7 #include "http_auth.h"
8 #include "log.h"
9 #include "plugin.h"
11 #include <ctype.h>
12 #include <errno.h>
13 #include <string.h>
15 typedef struct {
16 LDAP *ldap;
18 buffer *auth_ldap_hostname;
19 buffer *auth_ldap_basedn;
20 buffer *auth_ldap_binddn;
21 buffer *auth_ldap_bindpw;
22 buffer *auth_ldap_filter;
23 buffer *auth_ldap_cafile;
24 unsigned short auth_ldap_starttls;
25 unsigned short auth_ldap_allow_empty_pw;
26 } plugin_config;
28 typedef struct {
29 PLUGIN_DATA;
30 plugin_config **config_storage;
31 plugin_config conf, *anon_conf; /* this is only used as long as no handler_ctx is setup */
33 buffer *ldap_filter;
34 } plugin_data;
36 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);
38 INIT_FUNC(mod_authn_ldap_init) {
39 static http_auth_backend_t http_auth_backend_ldap =
40 { "ldap", mod_authn_ldap_basic, NULL, NULL };
41 plugin_data *p = calloc(1, sizeof(*p));
42 p->ldap_filter = buffer_init();
44 /* register http_auth_backend_ldap */
45 http_auth_backend_ldap.p_d = p;
46 http_auth_backend_set(&http_auth_backend_ldap);
48 return p;
51 FREE_FUNC(mod_authn_ldap_free) {
52 plugin_data *p = p_d;
54 UNUSED(srv);
56 if (!p) return HANDLER_GO_ON;
58 buffer_free(p->ldap_filter);
60 if (p->config_storage) {
61 size_t i;
62 for (i = 0; i < srv->config_context->used; i++) {
63 plugin_config *s = p->config_storage[i];
65 if (NULL == s) continue;
67 buffer_free(s->auth_ldap_hostname);
68 buffer_free(s->auth_ldap_basedn);
69 buffer_free(s->auth_ldap_binddn);
70 buffer_free(s->auth_ldap_bindpw);
71 buffer_free(s->auth_ldap_filter);
72 buffer_free(s->auth_ldap_cafile);
74 if (NULL != s->ldap) ldap_unbind_ext_s(s->ldap, NULL, NULL);
75 free(s);
77 free(p->config_storage);
80 free(p);
82 return HANDLER_GO_ON;
85 SETDEFAULTS_FUNC(mod_authn_ldap_set_defaults) {
86 plugin_data *p = p_d;
87 size_t i;
88 config_values_t cv[] = {
89 { "auth.backend.ldap.hostname", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION }, /* 0 */
90 { "auth.backend.ldap.base-dn", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION }, /* 1 */
91 { "auth.backend.ldap.filter", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION }, /* 2 */
92 { "auth.backend.ldap.ca-file", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION }, /* 3 */
93 { "auth.backend.ldap.starttls", NULL, T_CONFIG_BOOLEAN, T_CONFIG_SCOPE_CONNECTION }, /* 4 */
94 { "auth.backend.ldap.bind-dn", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION }, /* 5 */
95 { "auth.backend.ldap.bind-pw", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION }, /* 6 */
96 { "auth.backend.ldap.allow-empty-pw", NULL, T_CONFIG_BOOLEAN, T_CONFIG_SCOPE_CONNECTION }, /* 7 */
97 { NULL, NULL, T_CONFIG_UNSET, T_CONFIG_SCOPE_UNSET }
100 p->config_storage = calloc(1, srv->config_context->used * sizeof(plugin_config *));
102 for (i = 0; i < srv->config_context->used; i++) {
103 data_config const* config = (data_config const*)srv->config_context->data[i];
104 plugin_config *s;
106 s = calloc(1, sizeof(plugin_config));
108 s->auth_ldap_hostname = buffer_init();
109 s->auth_ldap_basedn = buffer_init();
110 s->auth_ldap_binddn = buffer_init();
111 s->auth_ldap_bindpw = buffer_init();
112 s->auth_ldap_filter = buffer_init();
113 s->auth_ldap_cafile = buffer_init();
114 s->auth_ldap_starttls = 0;
115 s->ldap = NULL;
117 cv[0].destination = s->auth_ldap_hostname;
118 cv[1].destination = s->auth_ldap_basedn;
119 cv[2].destination = s->auth_ldap_filter;
120 cv[3].destination = s->auth_ldap_cafile;
121 cv[4].destination = &(s->auth_ldap_starttls);
122 cv[5].destination = s->auth_ldap_binddn;
123 cv[6].destination = s->auth_ldap_bindpw;
124 cv[7].destination = &(s->auth_ldap_allow_empty_pw);
126 p->config_storage[i] = s;
128 if (0 != config_insert_values_global(srv, config->value, cv, i == 0 ? T_CONFIG_SCOPE_SERVER : T_CONFIG_SCOPE_CONNECTION)) {
129 return HANDLER_ERROR;
132 if (!buffer_string_is_empty(s->auth_ldap_filter)) {
133 if (*s->auth_ldap_filter->ptr != ','
134 && NULL == strchr(s->auth_ldap_filter->ptr, '$')) {
135 log_error_write(srv, __FILE__, __LINE__, "s", "ldap: auth.backend.ldap.filter is missing a replace-operator '$'");
137 return HANDLER_ERROR;
142 return HANDLER_GO_ON;
145 #define PATCH(x) \
146 p->conf.x = s->x;
147 static int mod_authn_ldap_patch_connection(server *srv, connection *con, plugin_data *p) {
148 size_t i, j;
149 plugin_config *s = p->config_storage[0];
151 PATCH(auth_ldap_hostname);
152 PATCH(auth_ldap_basedn);
153 PATCH(auth_ldap_binddn);
154 PATCH(auth_ldap_bindpw);
155 PATCH(auth_ldap_filter);
156 PATCH(auth_ldap_cafile);
157 PATCH(auth_ldap_starttls);
158 PATCH(auth_ldap_allow_empty_pw);
159 p->anon_conf = s;
161 /* skip the first, the global context */
162 for (i = 1; i < srv->config_context->used; i++) {
163 data_config *dc = (data_config *)srv->config_context->data[i];
164 s = p->config_storage[i];
166 /* condition didn't match */
167 if (!config_check_cond(srv, con, dc)) continue;
169 /* merge config */
170 for (j = 0; j < dc->value->used; j++) {
171 data_unset *du = dc->value->data[j];
173 if (buffer_is_equal_string(du->key, CONST_STR_LEN("auth.backend.ldap.hostname"))) {
174 PATCH(auth_ldap_hostname);
175 p->anon_conf = s;
176 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("auth.backend.ldap.base-dn"))) {
177 PATCH(auth_ldap_basedn);
178 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("auth.backend.ldap.filter"))) {
179 PATCH(auth_ldap_filter);
180 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("auth.backend.ldap.ca-file"))) {
181 PATCH(auth_ldap_cafile);
182 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("auth.backend.ldap.starttls"))) {
183 PATCH(auth_ldap_starttls);
184 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("auth.backend.ldap.bind-dn"))) {
185 PATCH(auth_ldap_binddn);
186 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("auth.backend.ldap.bind-pw"))) {
187 PATCH(auth_ldap_bindpw);
188 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("auth.backend.ldap.allow-empty-pw"))) {
189 PATCH(auth_ldap_allow_empty_pw);
194 return 0;
196 #undef PATCH
198 static void mod_authn_ldap_err(server *srv, const char *file, unsigned long line, const char *fn, int err)
200 log_error_write(srv,file,line,"sSss","ldap:",fn,":",ldap_err2string(err));
203 static void mod_authn_ldap_opt_err(server *srv, const char *file, unsigned long line, const char *fn, LDAP *ld)
205 int err;
206 ldap_get_option(ld, LDAP_OPT_ERROR_NUMBER, &err);
207 mod_authn_ldap_err(srv, file, line, fn, err);
210 static LDAP * mod_authn_ldap_host_init(server *srv, plugin_config *s) {
211 LDAP *ld;
212 int ret;
214 if (buffer_string_is_empty(s->auth_ldap_hostname)) return NULL;
216 ld = ldap_init(s->auth_ldap_hostname->ptr, LDAP_PORT);
217 if (NULL == ld) {
218 log_error_write(srv, __FILE__, __LINE__, "sss", "ldap:", "ldap_init():",
219 strerror(errno));
220 return NULL;
223 ret = LDAP_VERSION3;
224 ret = ldap_set_option(ld, LDAP_OPT_PROTOCOL_VERSION, &ret);
225 if (LDAP_OPT_SUCCESS != ret) {
226 mod_authn_ldap_err(srv, __FILE__, __LINE__, "ldap_set_options()", ret);
227 ldap_memfree(ld);
228 return NULL;
231 if (s->auth_ldap_starttls) {
232 /* if no CA file is given, it is ok, as we will use encryption
233 * if the server requires a CAfile it will tell us */
234 if (!buffer_string_is_empty(s->auth_ldap_cafile)) {
235 ret = ldap_set_option(NULL, LDAP_OPT_X_TLS_CACERTFILE,
236 s->auth_ldap_cafile->ptr);
237 if (LDAP_OPT_SUCCESS != ret) {
238 mod_authn_ldap_err(srv, __FILE__, __LINE__,
239 "ldap_set_option(LDAP_OPT_X_TLS_CACERTFILE)",
240 ret);
241 ldap_memfree(ld);
242 return NULL;
246 ret = ldap_start_tls_s(ld, NULL, NULL);
247 if (LDAP_OPT_SUCCESS != ret) {
248 mod_authn_ldap_err(srv,__FILE__,__LINE__,"ldap_start_tls_s()",ret);
249 ldap_memfree(ld);
250 return NULL;
254 return ld;
257 static int mod_authn_ldap_bind(server *srv, LDAP *ld, const char *dn, const char *pw) {
258 #if 0
259 struct berval creds;
260 int ret;
262 if (NULL != pw) {
263 *((const char **)&creds.bv_val) = pw; /*(cast away const)*/
264 creds.bv_len = strlen(pw);
265 } else {
266 creds.bv_val = NULL;
267 creds.bv_len = 0;
270 /* RFE: add functionality: LDAP_SASL_EXTERNAL (or GSS-SPNEGO, etc.) */
272 ret = ldap_sasl_bind_s(ld,dn,LDAP_SASL_SIMPLE,&creds,NULL,NULL,NULL);
273 if (ret != LDAP_SUCCESS) {
274 mod_authn_ldap_err(srv, __FILE__, __LINE__, "ldap_sasl_bind_s()", ret);
276 #else
277 int ret = ldap_simple_bind_s(ld, dn, pw);
278 if (ret != LDAP_SUCCESS) {
279 mod_authn_ldap_err(srv, __FILE__, __LINE__, "ldap_simple_bind_s()",ret);
281 #endif
283 return ret;
286 static LDAPMessage * mod_authn_ldap_search(server *srv, plugin_config *s, char *base, char *filter) {
287 LDAPMessage *lm = NULL;
288 char *attrs[] = { LDAP_NO_ATTRS, NULL };
289 int ret;
292 * 1. connect anonymously (if not already connected)
293 * (ldap connection is kept open unless connection-level error occurs)
294 * 2. issue search using filter
297 if (s->ldap != NULL) {
298 ret = ldap_search_ext_s(s->ldap, base, LDAP_SCOPE_SUBTREE, filter,
299 attrs, 0, NULL, NULL, NULL, 0, &lm);
300 if (LDAP_SUCCESS == ret) {
301 return lm;
302 } else if (LDAP_SERVER_DOWN != ret) {
303 /* try again (or initial request);
304 * ldap lib sometimes fails for the first call but reconnects */
305 ret = ldap_search_ext_s(s->ldap, base, LDAP_SCOPE_SUBTREE, filter,
306 attrs, 0, NULL, NULL, NULL, 0, &lm);
307 if (LDAP_SUCCESS == ret) {
308 return lm;
312 ldap_unbind_ext_s(s->ldap, NULL, NULL);
315 s->ldap = mod_authn_ldap_host_init(srv, s);
316 if (NULL == s->ldap) {
317 return NULL;
320 ret = !buffer_string_is_empty(s->auth_ldap_binddn)
321 ? mod_authn_ldap_bind(srv, s->ldap,
322 s->auth_ldap_binddn->ptr,
323 s->auth_ldap_bindpw->ptr)
324 : mod_authn_ldap_bind(srv, s->ldap, NULL, NULL);
325 if (LDAP_SUCCESS != ret) {
326 ldap_memfree(s->ldap);
327 s->ldap = NULL;
328 return NULL;
331 ret = ldap_search_ext_s(s->ldap, base, LDAP_SCOPE_SUBTREE, filter,
332 attrs, 0, NULL, NULL, NULL, 0, &lm);
333 if (LDAP_SUCCESS != ret) {
334 log_error_write(srv, __FILE__, __LINE__, "sSss",
335 "ldap:", ldap_err2string(ret), "; filter:", filter);
336 ldap_unbind_ext_s(s->ldap, NULL, NULL);
337 s->ldap = NULL;
338 return NULL;
341 return lm;
344 static char * mod_authn_ldap_get_dn(server *srv, plugin_config *s, char *base, char *filter) {
345 LDAP *ld;
346 LDAPMessage *lm, *first;
347 char *dn;
348 int count;
350 lm = mod_authn_ldap_search(srv, s, base, filter);
351 if (NULL == lm) {
352 return NULL;
355 ld = s->ldap; /*(must be after mod_authn_ldap_search(); might reconnect)*/
357 count = ldap_count_entries(ld, lm);
358 if (0 == count) { /*(no entires found)*/
359 ldap_msgfree(lm);
360 return NULL;
361 } else if (count > 1) {
362 log_error_write(srv, __FILE__, __LINE__, "sss",
363 "ldap:", "more than one record returned. "
364 "you might have to refine the filter:", filter);
367 if (NULL == (first = ldap_first_entry(ld, lm))) {
368 mod_authn_ldap_opt_err(srv,__FILE__,__LINE__,"ldap_first_entry()",ld);
369 ldap_msgfree(lm);
370 return NULL;
373 if (NULL == (dn = ldap_get_dn(ld, first))) {
374 mod_authn_ldap_opt_err(srv,__FILE__,__LINE__,"ldap_get_dn()",ld);
375 ldap_msgfree(lm);
376 return NULL;
379 ldap_msgfree(lm);
380 return dn;
383 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) {
384 plugin_data *p = (plugin_data *)p_d;
385 LDAP *ld;
386 char *dn;
387 buffer *template;
389 mod_authn_ldap_patch_connection(srv, con, p);
391 if (pw[0] == '\0' && !p->conf.auth_ldap_allow_empty_pw)
392 return HANDLER_ERROR;
394 /* check username
396 * we have to protect againt username which modifies our filter in
397 * an unpleasant way
400 for (size_t i = 0, len = buffer_string_length(username); i < len; i++) {
401 char c = username->ptr[i];
403 if (!isalpha(c) &&
404 !isdigit(c) &&
405 (c != ' ') &&
406 (c != '@') &&
407 (c != '-') &&
408 (c != '_') &&
409 (c != '.') ) {
411 log_error_write(srv, __FILE__, __LINE__, "sbd",
412 "ldap: invalid character (- _.@a-zA-Z0-9 allowed) "
413 "in username:", username, i);
415 con->http_status = 400; /* Bad Request */
416 con->mode = DIRECT;
417 return HANDLER_FINISHED;
421 template = p->conf.auth_ldap_filter;
422 if (buffer_string_is_empty(template)) {
423 return HANDLER_ERROR;
426 /* build filter to get DN for uid = username */
427 buffer_string_set_length(p->ldap_filter, 0);
428 if (*template->ptr == ',') {
429 /* special-case filter template beginning with ',' to be explicit DN */
430 buffer_append_string_len(p->ldap_filter, CONST_STR_LEN("uid="));
431 buffer_append_string_buffer(p->ldap_filter, username);
432 buffer_append_string_buffer(p->ldap_filter, template);
433 dn = p->ldap_filter->ptr;
434 } else {
435 for (char *b = template->ptr, *d; *b; b = d+1) {
436 if (NULL != (d = strchr(b, '$'))) {
437 buffer_append_string_len(p->ldap_filter, b, (size_t)(d - b));
438 buffer_append_string_buffer(p->ldap_filter, username);
439 } else {
440 d = template->ptr + buffer_string_length(template);
441 buffer_append_string_len(p->ldap_filter, b, (size_t)(d - b));
442 break;
446 /* ldap_search for DN (synchronous; blocking) */
447 dn = mod_authn_ldap_get_dn(srv, p->anon_conf,
448 p->conf.auth_ldap_basedn->ptr,
449 p->ldap_filter->ptr);
450 if (NULL == dn) {
451 return HANDLER_ERROR;
455 /* auth against LDAP server (synchronous; blocking) */
457 ld = mod_authn_ldap_host_init(srv, &p->conf);
458 if (NULL == ld) {
459 if (dn != p->ldap_filter->ptr) ldap_memfree(dn);
460 return HANDLER_ERROR;
463 if (LDAP_SUCCESS != mod_authn_ldap_bind(srv, ld, dn, pw)) {
464 ldap_memfree(ld);
465 if (dn != p->ldap_filter->ptr) ldap_memfree(dn);
466 return HANDLER_ERROR;
469 ldap_unbind_ext_s(ld, NULL, NULL); /* disconnect */
470 if (dn != p->ldap_filter->ptr) ldap_memfree(dn);
471 return http_auth_match_rules(require, username->ptr, NULL, NULL)
472 ? HANDLER_GO_ON /* access granted */
473 : HANDLER_ERROR;
476 int mod_authn_ldap_plugin_init(plugin *p);
477 int mod_authn_ldap_plugin_init(plugin *p) {
478 p->version = LIGHTTPD_VERSION_ID;
479 p->name = buffer_init_string("authn_ldap");
480 p->init = mod_authn_ldap_init;
481 p->set_defaults = mod_authn_ldap_set_defaults;
482 p->cleanup = mod_authn_ldap_free;
484 p->data = NULL;
486 return 0;