summaryrefslogtreecommitdiffstats
path: root/src/auth/db-ldap.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/auth/db-ldap.c')
-rw-r--r--src/auth/db-ldap.c2035
1 files changed, 2035 insertions, 0 deletions
diff --git a/src/auth/db-ldap.c b/src/auth/db-ldap.c
new file mode 100644
index 0000000..8083108
--- /dev/null
+++ b/src/auth/db-ldap.c
@@ -0,0 +1,2035 @@
+/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */
+
+#include "auth-common.h"
+
+#if defined(BUILTIN_LDAP) || defined(PLUGIN_BUILD)
+
+#include "net.h"
+#include "ioloop.h"
+#include "array.h"
+#include "hash.h"
+#include "aqueue.h"
+#include "str.h"
+#include "time-util.h"
+#include "env-util.h"
+#include "var-expand.h"
+#include "settings.h"
+#include "userdb.h"
+#include "db-ldap.h"
+
+#include <stddef.h>
+#include <unistd.h>
+
+#define HAVE_LDAP_SASL
+#ifdef HAVE_SASL_SASL_H
+# include <sasl/sasl.h>
+#elif defined (HAVE_SASL_H)
+# include <sasl.h>
+#else
+# undef HAVE_LDAP_SASL
+#endif
+#ifdef LDAP_OPT_X_TLS
+# define OPENLDAP_TLS_OPTIONS
+#endif
+#if !defined(SASL_VERSION_MAJOR) || SASL_VERSION_MAJOR < 2
+# undef HAVE_LDAP_SASL
+#endif
+
+#ifndef LDAP_SASL_QUIET
+# define LDAP_SASL_QUIET 0 /* Doesn't exist in Solaris LDAP */
+#endif
+
+/* Older versions may require calling ldap_result() twice */
+#if LDAP_VENDOR_VERSION <= 20112
+# define OPENLDAP_ASYNC_WORKAROUND
+#endif
+
+/* Solaris LDAP library doesn't have LDAP_OPT_SUCCESS */
+#ifndef LDAP_OPT_SUCCESS
+# define LDAP_OPT_SUCCESS LDAP_SUCCESS
+#endif
+
+#define DB_LDAP_REQUEST_MAX_ATTEMPT_COUNT 3
+
+static const char *LDAP_ESCAPE_CHARS = "*,\\#+<>;\"()= ";
+
+struct db_ldap_result {
+ int refcount;
+ LDAPMessage *msg;
+};
+
+struct db_ldap_value {
+ const char **values;
+ bool used;
+};
+
+struct db_ldap_result_iterate_context {
+ pool_t pool;
+
+ struct ldap_request *ldap_request;
+ const ARRAY_TYPE(ldap_field) *attr_map;
+ unsigned int attr_idx;
+
+ /* attribute name => value */
+ HASH_TABLE(char *, struct db_ldap_value *) ldap_attrs;
+
+ const char *val_1_arr[2];
+ string_t *var, *debug;
+
+ bool skip_null_values;
+ bool iter_dn_values;
+ LDAPMessage *ldap_msg;
+ LDAP *ld;
+};
+
+struct db_ldap_sasl_bind_context {
+ const char *authcid;
+ const char *passwd;
+ const char *realm;
+ const char *authzid;
+};
+
+#define DEF_STR(name) DEF_STRUCT_STR(name, ldap_settings)
+#define DEF_INT(name) DEF_STRUCT_INT(name, ldap_settings)
+#define DEF_BOOL(name) DEF_STRUCT_BOOL(name, ldap_settings)
+
+static struct setting_def setting_defs[] = {
+ DEF_STR(hosts),
+ DEF_STR(uris),
+ DEF_STR(dn),
+ DEF_STR(dnpass),
+ DEF_BOOL(auth_bind),
+ DEF_STR(auth_bind_userdn),
+ DEF_BOOL(tls),
+ DEF_BOOL(sasl_bind),
+ DEF_STR(sasl_mech),
+ DEF_STR(sasl_realm),
+ DEF_STR(sasl_authz_id),
+ DEF_STR(tls_ca_cert_file),
+ DEF_STR(tls_ca_cert_dir),
+ DEF_STR(tls_cert_file),
+ DEF_STR(tls_key_file),
+ DEF_STR(tls_cipher_suite),
+ DEF_STR(tls_require_cert),
+ DEF_STR(deref),
+ DEF_STR(scope),
+ DEF_STR(base),
+ DEF_INT(ldap_version),
+ DEF_STR(debug_level),
+ DEF_STR(ldaprc_path),
+ DEF_STR(user_attrs),
+ DEF_STR(user_filter),
+ DEF_STR(pass_attrs),
+ DEF_STR(pass_filter),
+ DEF_STR(iterate_attrs),
+ DEF_STR(iterate_filter),
+ DEF_STR(default_pass_scheme),
+ DEF_BOOL(userdb_warning_disable),
+ DEF_BOOL(blocking),
+
+ { 0, NULL, 0 }
+};
+
+static struct ldap_settings default_ldap_settings = {
+ .hosts = NULL,
+ .uris = NULL,
+ .dn = NULL,
+ .dnpass = NULL,
+ .auth_bind = FALSE,
+ .auth_bind_userdn = NULL,
+ .tls = FALSE,
+ .sasl_bind = FALSE,
+ .sasl_mech = NULL,
+ .sasl_realm = NULL,
+ .sasl_authz_id = NULL,
+ .tls_ca_cert_file = NULL,
+ .tls_ca_cert_dir = NULL,
+ .tls_cert_file = NULL,
+ .tls_key_file = NULL,
+ .tls_cipher_suite = NULL,
+ .tls_require_cert = NULL,
+ .deref = "never",
+ .scope = "subtree",
+ .base = NULL,
+ .ldap_version = 3,
+ .debug_level = "0",
+ .ldaprc_path = "",
+ .user_attrs = "homeDirectory=home,uidNumber=uid,gidNumber=gid",
+ .user_filter = "(&(objectClass=posixAccount)(uid=%u))",
+ .pass_attrs = "uid=user,userPassword=password",
+ .pass_filter = "(&(objectClass=posixAccount)(uid=%u))",
+ .iterate_attrs = "uid=user",
+ .iterate_filter = "(objectClass=posixAccount)",
+ .default_pass_scheme = "crypt",
+ .userdb_warning_disable = FALSE,
+ .blocking = FALSE
+};
+
+static struct ldap_connection *ldap_connections = NULL;
+
+static int db_ldap_bind(struct ldap_connection *conn);
+static void db_ldap_conn_close(struct ldap_connection *conn);
+struct db_ldap_result_iterate_context *
+db_ldap_result_iterate_init_full(struct ldap_connection *conn,
+ struct ldap_request_search *ldap_request,
+ LDAPMessage *res, bool skip_null_values,
+ bool iter_dn_values);
+static bool db_ldap_abort_requests(struct ldap_connection *conn,
+ unsigned int max_count,
+ unsigned int timeout_secs,
+ bool error, const char *reason);
+static void db_ldap_request_free(struct ldap_request *request);
+
+static int deref2str(const char *str, int *ref_r)
+{
+ if (strcasecmp(str, "never") == 0)
+ *ref_r = LDAP_DEREF_NEVER;
+ else if (strcasecmp(str, "searching") == 0)
+ *ref_r = LDAP_DEREF_SEARCHING;
+ else if (strcasecmp(str, "finding") == 0)
+ *ref_r = LDAP_DEREF_FINDING;
+ else if (strcasecmp(str, "always") == 0)
+ *ref_r = LDAP_DEREF_ALWAYS;
+ else
+ return -1;
+ return 0;
+}
+
+static int scope2str(const char *str, int *scope_r)
+{
+ if (strcasecmp(str, "base") == 0)
+ *scope_r = LDAP_SCOPE_BASE;
+ else if (strcasecmp(str, "onelevel") == 0)
+ *scope_r = LDAP_SCOPE_ONELEVEL;
+ else if (strcasecmp(str, "subtree") == 0)
+ *scope_r = LDAP_SCOPE_SUBTREE;
+ else
+ return -1;
+ return 0;
+}
+
+#ifdef OPENLDAP_TLS_OPTIONS
+static int tls_require_cert2str(const char *str, int *value_r)
+{
+ if (strcasecmp(str, "never") == 0)
+ *value_r = LDAP_OPT_X_TLS_NEVER;
+ else if (strcasecmp(str, "hard") == 0)
+ *value_r = LDAP_OPT_X_TLS_HARD;
+ else if (strcasecmp(str, "demand") == 0)
+ *value_r = LDAP_OPT_X_TLS_DEMAND;
+ else if (strcasecmp(str, "allow") == 0)
+ *value_r = LDAP_OPT_X_TLS_ALLOW;
+ else if (strcasecmp(str, "try") == 0)
+ *value_r = LDAP_OPT_X_TLS_TRY;
+ else
+ return -1;
+ return 0;
+}
+#endif
+
+static int ldap_get_errno(struct ldap_connection *conn)
+{
+ int ret, err;
+
+ ret = ldap_get_option(conn->ld, LDAP_OPT_ERROR_NUMBER, (void *) &err);
+ if (ret != LDAP_SUCCESS) {
+ e_error(conn->event, "Can't get error number: %s",
+ ldap_err2string(ret));
+ return LDAP_UNAVAILABLE;
+ }
+
+ return err;
+}
+
+const char *ldap_get_error(struct ldap_connection *conn)
+{
+ const char *ret;
+ char *str = NULL;
+
+ ret = ldap_err2string(ldap_get_errno(conn));
+
+ ldap_get_option(conn->ld, LDAP_OPT_ERROR_STRING, (void *)&str);
+ if (str != NULL) {
+ ret = t_strconcat(ret, ", ", str, NULL);
+ ldap_memfree(str);
+ }
+ ldap_set_option(conn->ld, LDAP_OPT_ERROR_STRING, NULL);
+ return ret;
+}
+
+static void ldap_conn_reconnect(struct ldap_connection *conn)
+{
+ db_ldap_conn_close(conn);
+ if (db_ldap_connect(conn) < 0)
+ db_ldap_conn_close(conn);
+}
+
+static int ldap_handle_error(struct ldap_connection *conn)
+{
+ int err = ldap_get_errno(conn);
+
+ switch (err) {
+ case LDAP_SUCCESS:
+ i_unreached();
+ case LDAP_SIZELIMIT_EXCEEDED:
+ case LDAP_TIMELIMIT_EXCEEDED:
+ case LDAP_NO_SUCH_ATTRIBUTE:
+ case LDAP_UNDEFINED_TYPE:
+ case LDAP_INAPPROPRIATE_MATCHING:
+ case LDAP_CONSTRAINT_VIOLATION:
+ case LDAP_TYPE_OR_VALUE_EXISTS:
+ case LDAP_INVALID_SYNTAX:
+ case LDAP_NO_SUCH_OBJECT:
+ case LDAP_ALIAS_PROBLEM:
+ case LDAP_INVALID_DN_SYNTAX:
+ case LDAP_IS_LEAF:
+ case LDAP_ALIAS_DEREF_PROBLEM:
+ case LDAP_FILTER_ERROR:
+ /* invalid input */
+ return -1;
+ case LDAP_SERVER_DOWN:
+ case LDAP_TIMEOUT:
+ case LDAP_UNAVAILABLE:
+ case LDAP_BUSY:
+#ifdef LDAP_CONNECT_ERROR
+ case LDAP_CONNECT_ERROR:
+#endif
+ case LDAP_LOCAL_ERROR:
+ case LDAP_INVALID_CREDENTIALS:
+ case LDAP_OPERATIONS_ERROR:
+ default:
+ /* connection problems */
+ ldap_conn_reconnect(conn);
+ return 0;
+ }
+}
+
+static int db_ldap_request_bind(struct ldap_connection *conn,
+ struct ldap_request *request)
+{
+ struct ldap_request_bind *brequest =
+ (struct ldap_request_bind *)request;
+
+ i_assert(request->type == LDAP_REQUEST_TYPE_BIND);
+ i_assert(request->msgid == -1);
+ i_assert(conn->conn_state == LDAP_CONN_STATE_BOUND_AUTH ||
+ conn->conn_state == LDAP_CONN_STATE_BOUND_DEFAULT);
+ i_assert(conn->pending_count == 0);
+
+ request->msgid = ldap_bind(conn->ld, brequest->dn,
+ request->auth_request->mech_password,
+ LDAP_AUTH_SIMPLE);
+ if (request->msgid == -1) {
+ e_error(authdb_event(request->auth_request),
+ "ldap_bind(%s) failed: %s",
+ brequest->dn, ldap_get_error(conn));
+ if (ldap_handle_error(conn) < 0) {
+ /* broken request, remove it */
+ return 0;
+ }
+ return -1;
+ }
+ conn->conn_state = LDAP_CONN_STATE_BINDING;
+ return 1;
+}
+
+static int db_ldap_request_search(struct ldap_connection *conn,
+ struct ldap_request *request)
+{
+ struct ldap_request_search *srequest =
+ (struct ldap_request_search *)request;
+
+ i_assert(conn->conn_state == LDAP_CONN_STATE_BOUND_DEFAULT);
+ i_assert(request->msgid == -1);
+
+ request->msgid =
+ ldap_search(conn->ld, *srequest->base == '\0' ? NULL :
+ srequest->base, conn->set.ldap_scope,
+ srequest->filter, srequest->attributes, 0);
+ if (request->msgid == -1) {
+ e_error(authdb_event(request->auth_request),
+ "ldap_search(%s) parsing failed: %s",
+ srequest->filter, ldap_get_error(conn));
+ if (ldap_handle_error(conn) < 0) {
+ /* broken request, remove it */
+ return 0;
+ }
+ return -1;
+ }
+ return 1;
+}
+
+static bool db_ldap_request_queue_next(struct ldap_connection *conn)
+{
+ struct ldap_request *request;
+ int ret = -1;
+
+ /* connecting may call db_ldap_connect_finish(), which gets us back
+ here. so do the connection before checking the request queue. */
+ if (db_ldap_connect(conn) < 0)
+ return FALSE;
+
+ if (conn->pending_count == aqueue_count(conn->request_queue)) {
+ /* no non-pending requests */
+ return FALSE;
+ }
+ if (conn->pending_count > DB_LDAP_MAX_PENDING_REQUESTS) {
+ /* wait until server has replied to some requests */
+ return FALSE;
+ }
+
+ request = array_idx_elem(&conn->request_array,
+ aqueue_idx(conn->request_queue,
+ conn->pending_count));
+
+ if (conn->pending_count > 0 &&
+ request->type == LDAP_REQUEST_TYPE_BIND) {
+ /* we can't do binds until all existing requests are finished */
+ return FALSE;
+ }
+
+ switch (conn->conn_state) {
+ case LDAP_CONN_STATE_DISCONNECTED:
+ case LDAP_CONN_STATE_BINDING:
+ /* wait until we're in bound state */
+ return FALSE;
+ case LDAP_CONN_STATE_BOUND_AUTH:
+ if (request->type == LDAP_REQUEST_TYPE_BIND)
+ break;
+
+ /* bind to default dn first */
+ i_assert(conn->pending_count == 0);
+ (void)db_ldap_bind(conn);
+ return FALSE;
+ case LDAP_CONN_STATE_BOUND_DEFAULT:
+ /* we can do anything in this state */
+ break;
+ }
+
+ if (request->send_count >= DB_LDAP_REQUEST_MAX_ATTEMPT_COUNT) {
+ /* Enough many times retried. Server just keeps disconnecting
+ whenever attempting to send the request. */
+ ret = 0;
+ } else {
+ /* clear away any partial results saved before reconnecting */
+ db_ldap_request_free(request);
+
+ switch (request->type) {
+ case LDAP_REQUEST_TYPE_BIND:
+ ret = db_ldap_request_bind(conn, request);
+ break;
+ case LDAP_REQUEST_TYPE_SEARCH:
+ ret = db_ldap_request_search(conn, request);
+ break;
+ }
+ }
+
+ if (ret > 0) {
+ /* success */
+ i_assert(request->msgid != -1);
+ request->send_count++;
+ conn->pending_count++;
+ return TRUE;
+ } else if (ret < 0) {
+ /* disconnected */
+ return FALSE;
+ } else {
+ /* broken request, remove from queue */
+ aqueue_delete_tail(conn->request_queue);
+ request->callback(conn, request, NULL);
+ return TRUE;
+ }
+}
+
+static void
+db_ldap_check_hanging(struct ldap_connection *conn)
+{
+ struct ldap_request *first_request;
+ unsigned int count;
+ time_t secs_diff;
+
+ count = aqueue_count(conn->request_queue);
+ if (count == 0)
+ return;
+
+ first_request = array_idx_elem(&conn->request_array,
+ aqueue_idx(conn->request_queue, 0));
+ secs_diff = ioloop_time - first_request->create_time;
+ if (secs_diff > DB_LDAP_REQUEST_LOST_TIMEOUT_SECS) {
+ db_ldap_abort_requests(conn, UINT_MAX, 0, TRUE,
+ "LDAP connection appears to be hanging");
+ ldap_conn_reconnect(conn);
+ }
+}
+
+void db_ldap_request(struct ldap_connection *conn,
+ struct ldap_request *request)
+{
+ i_assert(request->auth_request != NULL);
+
+ request->msgid = -1;
+ request->create_time = ioloop_time;
+
+ db_ldap_check_hanging(conn);
+
+ aqueue_append(conn->request_queue, &request);
+ (void)db_ldap_request_queue_next(conn);
+}
+
+static int db_ldap_connect_finish(struct ldap_connection *conn, int ret)
+{
+ if (ret == LDAP_SERVER_DOWN) {
+ e_error(conn->event, "Can't connect to server: %s",
+ conn->set.uris != NULL ?
+ conn->set.uris : conn->set.hosts);
+ return -1;
+ }
+ if (ret != LDAP_SUCCESS) {
+ e_error(conn->event, "binding failed (dn %s): %s",
+ conn->set.dn == NULL ? "(none)" : conn->set.dn,
+ ldap_get_error(conn));
+ return -1;
+ }
+
+ timeout_remove(&conn->to);
+ conn->conn_state = LDAP_CONN_STATE_BOUND_DEFAULT;
+ while (db_ldap_request_queue_next(conn))
+ ;
+ return 0;
+}
+
+static void db_ldap_default_bind_finished(struct ldap_connection *conn,
+ struct db_ldap_result *res)
+{
+ int ret;
+
+ i_assert(conn->pending_count == 0);
+ conn->default_bind_msgid = -1;
+
+ ret = ldap_result2error(conn->ld, res->msg, FALSE);
+ if (db_ldap_connect_finish(conn, ret) < 0) {
+ /* lost connection, close it */
+ db_ldap_conn_close(conn);
+ }
+}
+
+static bool db_ldap_abort_requests(struct ldap_connection *conn,
+ unsigned int max_count,
+ unsigned int timeout_secs,
+ bool error, const char *reason)
+{
+ struct ldap_request *request;
+ time_t diff;
+ bool aborts = FALSE;
+
+ while (aqueue_count(conn->request_queue) > 0 && max_count > 0) {
+ request = array_idx_elem(&conn->request_array,
+ aqueue_idx(conn->request_queue, 0));
+
+ diff = ioloop_time - request->create_time;
+ if (diff < (time_t)timeout_secs)
+ break;
+
+ /* timed out, abort */
+ aqueue_delete_tail(conn->request_queue);
+
+ if (request->msgid != -1) {
+ i_assert(conn->pending_count > 0);
+ conn->pending_count--;
+ }
+ if (error) {
+ e_error(authdb_event(request->auth_request),
+ "%s", reason);
+ } else {
+ e_info(authdb_event(request->auth_request),
+ "%s", reason);
+ }
+ request->callback(conn, request, NULL);
+ max_count--;
+ aborts = TRUE;
+ }
+ return aborts;
+}
+
+static struct ldap_request *
+db_ldap_find_request(struct ldap_connection *conn, int msgid,
+ unsigned int *idx_r)
+{
+ struct ldap_request *const *requests, *request = NULL;
+ unsigned int i, count;
+
+ count = aqueue_count(conn->request_queue);
+ if (count == 0)
+ return NULL;
+
+ requests = array_front(&conn->request_array);
+ for (i = 0; i < count; i++) {
+ request = requests[aqueue_idx(conn->request_queue, i)];
+ if (request->msgid == msgid) {
+ *idx_r = i;
+ return request;
+ }
+ if (request->msgid == -1)
+ break;
+ }
+ return NULL;
+}
+
+static int db_ldap_fields_get_dn(struct ldap_connection *conn,
+ struct ldap_request_search *request,
+ struct db_ldap_result *res)
+{
+ struct auth_request *auth_request = request->request.auth_request;
+ struct ldap_request_named_result *named_res;
+ struct db_ldap_result_iterate_context *ldap_iter;
+ const char *name, *const *values;
+
+ ldap_iter = db_ldap_result_iterate_init_full(conn, request, res->msg,
+ TRUE, TRUE);
+ while (db_ldap_result_iterate_next(ldap_iter, &name, &values)) {
+ if (values[1] != NULL) {
+ e_warning(authdb_event(auth_request),
+ "Multiple values found for '%s', "
+ "using value '%s'", name, values[0]);
+ }
+ array_foreach_modifiable(&request->named_results, named_res) {
+ if (strcmp(named_res->field->name, name) != 0)
+ continue;
+ /* In future we could also support LDAP URLs here */
+ named_res->dn = p_strdup(auth_request->pool,
+ values[0]);
+ }
+ }
+ db_ldap_result_iterate_deinit(&ldap_iter);
+ return 0;
+}
+
+struct ldap_field_find_subquery_context {
+ ARRAY_TYPE(string) attr_names;
+ const char *name;
+};
+
+static int
+db_ldap_field_subquery_find(const char *data, void *context,
+ const char **value_r,
+ const char **error_r ATTR_UNUSED)
+{
+ struct ldap_field_find_subquery_context *ctx = context;
+ char *ldap_attr;
+ const char *p;
+
+ if (*data != '\0') {
+ data = t_strcut(data, ':');
+ p = strchr(data, '@');
+ if (p != NULL && strcmp(p+1, ctx->name) == 0) {
+ ldap_attr = p_strdup_until(unsafe_data_stack_pool,
+ data, p);
+ array_push_back(&ctx->attr_names, &ldap_attr);
+ }
+ }
+ *value_r = NULL;
+ return 1;
+}
+
+static int
+ldap_request_send_subquery(struct ldap_connection *conn,
+ struct ldap_request_search *request,
+ struct ldap_request_named_result *named_res)
+{
+ const struct ldap_field *field;
+ const char *p, *error;
+ char *name;
+ struct auth_request *auth_request = request->request.auth_request;
+ struct ldap_field_find_subquery_context ctx;
+ const struct var_expand_table *table =
+ auth_request_get_var_expand_table(auth_request, NULL);
+ const struct var_expand_func_table *ptr;
+ struct var_expand_func_table *ftable;
+ string_t *tmp_str = t_str_new(64);
+ ARRAY(struct var_expand_func_table) var_funcs_table;
+ t_array_init(&var_funcs_table, 8);
+
+ for(ptr = auth_request_var_funcs_table; ptr->key != NULL; ptr++) {
+ array_push_back(&var_funcs_table, ptr);
+ }
+ ftable = array_append_space(&var_funcs_table);
+ ftable->key = "ldap";
+ ftable->func = db_ldap_field_subquery_find;
+ ftable = array_append_space(&var_funcs_table);
+ ftable->key = "ldap_ptr";
+ ftable->func = db_ldap_field_subquery_find;
+ array_append_zero(&var_funcs_table);
+
+ i_zero(&ctx);
+ t_array_init(&ctx.attr_names, 8);
+ ctx.name = named_res->field->name;
+
+ /* get the attributes names into array (ldapAttr@name -> ldapAttr) */
+ array_foreach(request->attr_map, field) {
+ if (field->ldap_attr_name[0] == '\0') {
+ str_truncate(tmp_str, 0);
+ if (var_expand_with_funcs(tmp_str, field->value, table,
+ array_front(&var_funcs_table), &ctx, &error) <= 0) {
+ e_error(authdb_event(auth_request),
+ "Failed to expand subquery %s: %s",
+ field->value, error);
+ return -1;
+ }
+ } else {
+ p = strchr(field->ldap_attr_name, '@');
+ if (p != NULL &&
+ strcmp(p+1, named_res->field->name) == 0) {
+ name = p_strdup_until(unsafe_data_stack_pool,
+ field->ldap_attr_name, p);
+ array_push_back(&ctx.attr_names, &name);
+ }
+ }
+ }
+ array_append_zero(&ctx.attr_names);
+
+ request->request.msgid =
+ ldap_search(conn->ld, named_res->dn, LDAP_SCOPE_BASE,
+ NULL, array_front_modifiable(&ctx.attr_names), 0);
+ if (request->request.msgid == -1) {
+ e_error(authdb_event(auth_request),
+ "ldap_search(dn=%s) failed: %s",
+ named_res->dn, ldap_get_error(conn));
+ return -1;
+ }
+ return 0;
+}
+
+static int db_ldap_search_save_result(struct ldap_request_search *request,
+ struct db_ldap_result *res)
+{
+ struct ldap_request_named_result *named_res;
+
+ if (!array_is_created(&request->named_results)) {
+ if (request->result != NULL)
+ return -1;
+ request->result = res;
+ } else {
+ named_res = array_idx_modifiable(&request->named_results,
+ request->name_idx);
+ if (named_res->result != NULL)
+ return -1;
+ named_res->result = res;
+ }
+ res->refcount++;
+ return 0;
+}
+
+static int db_ldap_search_next_subsearch(struct ldap_connection *conn,
+ struct ldap_request_search *request,
+ struct db_ldap_result *res)
+{
+ struct ldap_request_named_result *named_res;
+ const struct ldap_field *field;
+
+ if (request->result != NULL)
+ res = request->result;
+
+ if (!array_is_created(&request->named_results)) {
+ /* see if we need to do more LDAP queries */
+ p_array_init(&request->named_results,
+ request->request.auth_request->pool, 2);
+ array_foreach(request->attr_map, field) {
+ if (!field->value_is_dn)
+ continue;
+ named_res = array_append_space(&request->named_results);
+ named_res->field = field;
+ }
+ if (db_ldap_fields_get_dn(conn, request, res) < 0)
+ return -1;
+ } else {
+ request->name_idx++;
+ }
+ while (request->name_idx < array_count(&request->named_results)) {
+ /* send the next LDAP query */
+ named_res = array_idx_modifiable(&request->named_results,
+ request->name_idx);
+ if (named_res->dn != NULL) {
+ if (ldap_request_send_subquery(conn, request,
+ named_res) < 0)
+ return -1;
+ return 1;
+ }
+ /* dn field wasn't returned, skip this */
+ request->name_idx++;
+ }
+ return 0;
+}
+
+static bool
+db_ldap_handle_request_result(struct ldap_connection *conn,
+ struct ldap_request *request, unsigned int idx,
+ struct db_ldap_result *res)
+{
+ struct ldap_request_search *srequest = NULL;
+ const struct ldap_request_named_result *named_res;
+ int ret;
+ bool final_result;
+
+ i_assert(conn->pending_count > 0);
+
+ if (request->type == LDAP_REQUEST_TYPE_BIND) {
+ i_assert(conn->conn_state == LDAP_CONN_STATE_BINDING);
+ i_assert(conn->pending_count == 1);
+ conn->conn_state = LDAP_CONN_STATE_BOUND_AUTH;
+ } else {
+ srequest = (struct ldap_request_search *)request;
+ switch (ldap_msgtype(res->msg)) {
+ case LDAP_RES_SEARCH_ENTRY:
+ case LDAP_RES_SEARCH_RESULT:
+ break;
+ case LDAP_RES_SEARCH_REFERENCE:
+ /* we're going to ignore this */
+ return FALSE;
+ default:
+ e_error(conn->event, "Reply with unexpected type %d",
+ ldap_msgtype(res->msg));
+ return TRUE;
+ }
+ }
+ if (ldap_msgtype(res->msg) == LDAP_RES_SEARCH_ENTRY) {
+ ret = LDAP_SUCCESS;
+ final_result = FALSE;
+ } else {
+ final_result = TRUE;
+ ret = ldap_result2error(conn->ld, res->msg, 0);
+ }
+ /* LDAP_NO_SUCH_OBJECT is returned for nonexistent base */
+ if (ret != LDAP_SUCCESS && ret != LDAP_NO_SUCH_OBJECT &&
+ request->type == LDAP_REQUEST_TYPE_SEARCH) {
+ /* handle search failures here */
+ struct ldap_request_search *srequest =
+ (struct ldap_request_search *)request;
+
+ if (!array_is_created(&srequest->named_results)) {
+ e_error(authdb_event(request->auth_request),
+ "ldap_search(base=%s filter=%s) failed: %s",
+ srequest->base, srequest->filter,
+ ldap_err2string(ret));
+ } else {
+ named_res = array_idx(&srequest->named_results,
+ srequest->name_idx);
+ e_error(authdb_event(request->auth_request),
+ "ldap_search(base=%s) failed: %s",
+ named_res->dn, ldap_err2string(ret));
+ }
+ res = NULL;
+ }
+ if (ret == LDAP_SUCCESS && srequest != NULL && !srequest->multi_entry) {
+ /* expand any @results */
+ if (!final_result) {
+ if (db_ldap_search_save_result(srequest, res) < 0) {
+ e_error(authdb_event(request->auth_request),
+ "LDAP search returned multiple entries");
+ res = NULL;
+ } else {
+ /* wait for finish */
+ return FALSE;
+ }
+ } else {
+ ret = db_ldap_search_next_subsearch(conn, srequest, res);
+ if (ret > 0) {
+ /* more LDAP queries left */
+ return FALSE;
+ }
+ if (ret < 0)
+ res = NULL;
+ }
+ }
+ if (res == NULL && !final_result) {
+ /* wait for the final reply */
+ request->failed = TRUE;
+ return TRUE;
+ }
+ if (request->failed)
+ res = NULL;
+ if (final_result) {
+ conn->pending_count--;
+ aqueue_delete(conn->request_queue, idx);
+ }
+
+ T_BEGIN {
+ if (res != NULL && srequest != NULL && srequest->result != NULL)
+ request->callback(conn, request, srequest->result->msg);
+
+ request->callback(conn, request, res == NULL ? NULL : res->msg);
+ } T_END;
+
+ if (idx > 0) {
+ /* see if there are timed out requests */
+ if (db_ldap_abort_requests(conn, idx,
+ DB_LDAP_REQUEST_LOST_TIMEOUT_SECS,
+ TRUE, "Request lost"))
+ ldap_conn_reconnect(conn);
+ }
+ return TRUE;
+}
+
+static void db_ldap_result_unref(struct db_ldap_result **_res)
+{
+ struct db_ldap_result *res = *_res;
+
+ *_res = NULL;
+ i_assert(res->refcount > 0);
+ if (--res->refcount == 0) {
+ ldap_msgfree(res->msg);
+ i_free(res);
+ }
+}
+
+static void
+db_ldap_request_free(struct ldap_request *request)
+{
+ if (request->type == LDAP_REQUEST_TYPE_SEARCH) {
+ struct ldap_request_search *srequest =
+ (struct ldap_request_search *)request;
+ struct ldap_request_named_result *named_res;
+
+ if (srequest->result != NULL)
+ db_ldap_result_unref(&srequest->result);
+
+ if (array_is_created(&srequest->named_results)) {
+ array_foreach_modifiable(&srequest->named_results, named_res) {
+ if (named_res->result != NULL)
+ db_ldap_result_unref(&named_res->result);
+ }
+ array_free(&srequest->named_results);
+ srequest->name_idx = 0;
+ }
+ }
+}
+
+static void
+db_ldap_handle_result(struct ldap_connection *conn, struct db_ldap_result *res)
+{
+ struct auth_request *auth_request;
+ struct ldap_request *request;
+ unsigned int idx;
+ int msgid;
+
+ msgid = ldap_msgid(res->msg);
+ if (msgid == conn->default_bind_msgid) {
+ db_ldap_default_bind_finished(conn, res);
+ return;
+ }
+
+ request = db_ldap_find_request(conn, msgid, &idx);
+ if (request == NULL) {
+ e_error(conn->event, "Reply with unknown msgid %d", msgid);
+ ldap_conn_reconnect(conn);
+ return;
+ }
+ /* request is allocated from auth_request's pool */
+ auth_request = request->auth_request;
+ auth_request_ref(auth_request);
+ if (db_ldap_handle_request_result(conn, request, idx, res))
+ db_ldap_request_free(request);
+ auth_request_unref(&auth_request);
+}
+
+static void ldap_input(struct ldap_connection *conn)
+{
+ struct timeval timeout;
+ struct db_ldap_result *res;
+ LDAPMessage *msg;
+ time_t prev_reply_diff;
+ int ret;
+
+ do {
+ if (conn->ld == NULL)
+ return;
+
+ i_zero(&timeout);
+ ret = ldap_result(conn->ld, LDAP_RES_ANY, 0, &timeout, &msg);
+#ifdef OPENLDAP_ASYNC_WORKAROUND
+ if (ret == 0) {
+ /* try again, there may be another in buffer */
+ ret = ldap_result(conn->ld, LDAP_RES_ANY, 0,
+ &timeout, &msg);
+ }
+#endif
+ if (ret <= 0)
+ break;
+
+ res = i_new(struct db_ldap_result, 1);
+ res->refcount = 1;
+ res->msg = msg;
+ db_ldap_handle_result(conn, res);
+ db_ldap_result_unref(&res);
+ } while (conn->io != NULL);
+
+ prev_reply_diff = ioloop_time - conn->last_reply_stamp;
+ conn->last_reply_stamp = ioloop_time;
+
+ if (ret > 0) {
+ /* input disabled, continue once it's enabled */
+ i_assert(conn->io == NULL);
+ } else if (ret == 0) {
+ /* send more requests */
+ while (db_ldap_request_queue_next(conn))
+ ;
+ } else if (ldap_get_errno(conn) != LDAP_SERVER_DOWN) {
+ e_error(conn->event, "ldap_result() failed: %s", ldap_get_error(conn));
+ ldap_conn_reconnect(conn);
+ } else if (aqueue_count(conn->request_queue) > 0 ||
+ prev_reply_diff < DB_LDAP_IDLE_RECONNECT_SECS) {
+ e_error(conn->event, "Connection lost to LDAP server, reconnecting");
+ ldap_conn_reconnect(conn);
+ } else {
+ /* server probably disconnected an idle connection. don't
+ reconnect until the next request comes. */
+ db_ldap_conn_close(conn);
+ }
+}
+
+#ifdef HAVE_LDAP_SASL
+static int
+sasl_interact(LDAP *ld ATTR_UNUSED, unsigned flags ATTR_UNUSED,
+ void *defaults, void *interact)
+{
+ struct db_ldap_sasl_bind_context *context = defaults;
+ sasl_interact_t *in;
+ const char *str;
+
+ for (in = interact; in->id != SASL_CB_LIST_END; in++) {
+ switch (in->id) {
+ case SASL_CB_GETREALM:
+ str = context->realm;
+ break;
+ case SASL_CB_AUTHNAME:
+ str = context->authcid;
+ break;
+ case SASL_CB_USER:
+ str = context->authzid;
+ break;
+ case SASL_CB_PASS:
+ str = context->passwd;
+ break;
+ default:
+ str = NULL;
+ break;
+ }
+ if (str != NULL) {
+ in->len = strlen(str);
+ in->result = str;
+ }
+
+ }
+ return LDAP_SUCCESS;
+}
+#endif
+
+static void ldap_connection_timeout(struct ldap_connection *conn)
+{
+ i_assert(conn->conn_state == LDAP_CONN_STATE_BINDING);
+
+ e_error(conn->event, "Initial binding to LDAP server timed out");
+ db_ldap_conn_close(conn);
+}
+
+#ifdef HAVE_LDAP_SASL
+static int db_ldap_bind_sasl(struct ldap_connection *conn)
+{
+ struct db_ldap_sasl_bind_context context;
+ int ret;
+
+ i_zero(&context);
+ context.authcid = conn->set.dn;
+ context.passwd = conn->set.dnpass;
+ context.realm = conn->set.sasl_realm;
+ context.authzid = conn->set.sasl_authz_id;
+
+ /* There doesn't seem to be a way to do SASL binding
+ asynchronously.. */
+ ret = ldap_sasl_interactive_bind_s(conn->ld, NULL,
+ conn->set.sasl_mech,
+ NULL, NULL, LDAP_SASL_QUIET,
+ sasl_interact, &context);
+ if (db_ldap_connect_finish(conn, ret) < 0)
+ return -1;
+
+ conn->conn_state = LDAP_CONN_STATE_BOUND_DEFAULT;
+
+ return 0;
+}
+#else
+static int db_ldap_bind_sasl(struct ldap_connection *conn ATTR_UNUSED)
+{
+ i_unreached(); /* already checked at init */
+
+ return -1;
+}
+#endif
+
+static int db_ldap_bind_simple(struct ldap_connection *conn)
+{
+ int msgid;
+
+ i_assert(conn->conn_state != LDAP_CONN_STATE_BINDING);
+ i_assert(conn->default_bind_msgid == -1);
+ i_assert(conn->pending_count == 0);
+
+ msgid = ldap_bind(conn->ld, conn->set.dn, conn->set.dnpass,
+ LDAP_AUTH_SIMPLE);
+ if (msgid == -1) {
+ i_assert(ldap_get_errno(conn) != LDAP_SUCCESS);
+ if (db_ldap_connect_finish(conn, ldap_get_errno(conn)) < 0) {
+ /* lost connection, close it */
+ db_ldap_conn_close(conn);
+ }
+ return -1;
+ }
+
+ conn->conn_state = LDAP_CONN_STATE_BINDING;
+ conn->default_bind_msgid = msgid;
+
+ timeout_remove(&conn->to);
+ conn->to = timeout_add(DB_LDAP_REQUEST_LOST_TIMEOUT_SECS*1000,
+ ldap_connection_timeout, conn);
+ return 0;
+}
+
+static int db_ldap_bind(struct ldap_connection *conn)
+{
+ if (conn->set.sasl_bind) {
+ if (db_ldap_bind_sasl(conn) < 0)
+ return -1;
+ } else {
+ if (db_ldap_bind_simple(conn) < 0)
+ return -1;
+ }
+
+ return 0;
+}
+
+static void db_ldap_get_fd(struct ldap_connection *conn)
+{
+ int ret;
+
+ /* get the connection's fd */
+ ret = ldap_get_option(conn->ld, LDAP_OPT_DESC, (void *)&conn->fd);
+ if (ret != LDAP_SUCCESS) {
+ i_fatal("LDAP %s: Can't get connection fd: %s",
+ conn->config_path, ldap_err2string(ret));
+ }
+ if (conn->fd <= STDERR_FILENO) {
+ /* Solaris LDAP library seems to be broken */
+ i_fatal("LDAP %s: Buggy LDAP library returned wrong fd: %d",
+ conn->config_path, conn->fd);
+ }
+ i_assert(conn->fd != -1);
+ net_set_nonblock(conn->fd, TRUE);
+}
+
+static void ATTR_NULL(1)
+db_ldap_set_opt(struct ldap_connection *conn, LDAP *ld, int opt,
+ const void *value, const char *optname, const char *value_str)
+{
+ int ret;
+
+ ret = ldap_set_option(ld, opt, value);
+ if (ret != LDAP_SUCCESS) {
+ i_fatal("LDAP %s: Can't set option %s to %s: %s",
+ conn->config_path, optname, value_str, ldap_err2string(ret));
+ }
+}
+
+static void ATTR_NULL(1)
+db_ldap_set_opt_str(struct ldap_connection *conn, LDAP *ld, int opt,
+ const char *value, const char *optname)
+{
+ if (value != NULL)
+ db_ldap_set_opt(conn, ld, opt, value, optname, value);
+}
+
+static void db_ldap_set_tls_options(struct ldap_connection *conn)
+{
+#ifdef OPENLDAP_TLS_OPTIONS
+ db_ldap_set_opt_str(conn, NULL, LDAP_OPT_X_TLS_CACERTFILE,
+ conn->set.tls_ca_cert_file, "tls_ca_cert_file");
+ db_ldap_set_opt_str(conn, NULL, LDAP_OPT_X_TLS_CACERTDIR,
+ conn->set.tls_ca_cert_dir, "tls_ca_cert_dir");
+ db_ldap_set_opt_str(conn, NULL, LDAP_OPT_X_TLS_CERTFILE,
+ conn->set.tls_cert_file, "tls_cert_file");
+ db_ldap_set_opt_str(conn, NULL, LDAP_OPT_X_TLS_KEYFILE,
+ conn->set.tls_key_file, "tls_key_file");
+ db_ldap_set_opt_str(conn, NULL, LDAP_OPT_X_TLS_CIPHER_SUITE,
+ conn->set.tls_cipher_suite, "tls_cipher_suite");
+ if (conn->set.tls_require_cert != NULL) {
+ db_ldap_set_opt(conn, NULL, LDAP_OPT_X_TLS_REQUIRE_CERT, &conn->set.ldap_tls_require_cert_parsed,
+ "tls_require_cert", conn->set.tls_require_cert);
+ }
+#else
+ if (conn->set.tls_ca_cert_file != NULL ||
+ conn->set.tls_ca_cert_dir != NULL ||
+ conn->set.tls_cert_file != NULL ||
+ conn->set.tls_key_file != NULL ||
+ conn->set.tls_cipher_suite != NULL) {
+ i_fatal("LDAP %s: tls_* settings aren't supported by your LDAP library - they must not be set",
+ conn->config_path);
+ }
+#endif
+}
+
+static void db_ldap_set_options(struct ldap_connection *conn)
+{
+ unsigned int ldap_version;
+ int value;
+
+#ifdef LDAP_OPT_NETWORK_TIMEOUT
+ struct timeval tv;
+ int ret;
+
+ tv.tv_sec = DB_LDAP_CONNECT_TIMEOUT_SECS; tv.tv_usec = 0;
+ ret = ldap_set_option(conn->ld, LDAP_OPT_NETWORK_TIMEOUT, &tv);
+ if (ret != LDAP_SUCCESS) {
+ i_fatal("LDAP %s: Can't set network-timeout: %s",
+ conn->config_path, ldap_err2string(ret));
+ }
+#endif
+
+ db_ldap_set_opt(conn, conn->ld, LDAP_OPT_DEREF, &conn->set.ldap_deref,
+ "deref", conn->set.deref);
+#ifdef LDAP_OPT_DEBUG_LEVEL
+ if (str_to_int(conn->set.debug_level, &value) >= 0 && value != 0) {
+ db_ldap_set_opt(conn, NULL, LDAP_OPT_DEBUG_LEVEL, &value,
+ "debug_level", conn->set.debug_level);
+ event_set_forced_debug(conn->event, TRUE);
+ }
+#endif
+
+ ldap_version = conn->set.ldap_version;
+ db_ldap_set_opt(conn, conn->ld, LDAP_OPT_PROTOCOL_VERSION, &ldap_version,
+ "protocol_version", dec2str(ldap_version));
+ db_ldap_set_tls_options(conn);
+}
+
+static void db_ldap_init_ld(struct ldap_connection *conn)
+{
+ int ret;
+
+ if (conn->set.uris != NULL) {
+#ifdef LDAP_HAVE_INITIALIZE
+ ret = ldap_initialize(&conn->ld, conn->set.uris);
+ if (ret != LDAP_SUCCESS) {
+ i_fatal("LDAP %s: ldap_initialize() failed with uris %s: %s",
+ conn->config_path, conn->set.uris,
+ ldap_err2string(ret));
+ }
+#else
+ i_unreached(); /* already checked at init */
+#endif
+ } else {
+ conn->ld = ldap_init(conn->set.hosts, LDAP_PORT);
+ if (conn->ld == NULL) {
+ i_fatal("LDAP %s: ldap_init() failed with hosts: %s",
+ conn->config_path, conn->set.hosts);
+ }
+ }
+ db_ldap_set_options(conn);
+}
+
+int db_ldap_connect(struct ldap_connection *conn)
+{
+ struct timeval start, end;
+ int ret;
+
+ if (conn->conn_state != LDAP_CONN_STATE_DISCONNECTED)
+ return 0;
+
+ i_gettimeofday(&start);
+ i_assert(conn->pending_count == 0);
+
+ if (conn->delayed_connect) {
+ conn->delayed_connect = FALSE;
+ timeout_remove(&conn->to);
+ }
+ if (conn->ld == NULL)
+ db_ldap_init_ld(conn);
+
+ if (conn->set.tls) {
+#ifdef LDAP_HAVE_START_TLS_S
+ ret = ldap_start_tls_s(conn->ld, NULL, NULL);
+ if (ret != LDAP_SUCCESS) {
+ if (ret == LDAP_OPERATIONS_ERROR &&
+ conn->set.uris != NULL &&
+ str_begins(conn->set.uris, "ldaps:")) {
+ i_fatal("LDAP %s: Don't use both tls=yes "
+ "and ldaps URI", conn->config_path);
+ }
+ e_error(conn->event, "ldap_start_tls_s() failed: %s",
+ ldap_err2string(ret));
+ return -1;
+ }
+#else
+ i_unreached(); /* already checked at init */
+#endif
+ }
+
+ if (db_ldap_bind(conn) < 0)
+ return -1;
+
+ i_gettimeofday(&end);
+ int msecs = timeval_diff_msecs(&end, &start);
+ e_debug(conn->event, "LDAP initialization took %d msecs", msecs);
+
+ db_ldap_get_fd(conn);
+ conn->io = io_add(conn->fd, IO_READ, ldap_input, conn);
+ return 0;
+}
+
+static void db_ldap_connect_callback(struct ldap_connection *conn)
+{
+ i_assert(conn->conn_state == LDAP_CONN_STATE_DISCONNECTED);
+ (void)db_ldap_connect(conn);
+}
+
+void db_ldap_connect_delayed(struct ldap_connection *conn)
+{
+ if (conn->delayed_connect)
+ return;
+ conn->delayed_connect = TRUE;
+
+ i_assert(conn->to == NULL);
+ conn->to = timeout_add_short(0, db_ldap_connect_callback, conn);
+}
+
+void db_ldap_enable_input(struct ldap_connection *conn, bool enable)
+{
+ if (!enable) {
+ io_remove(&conn->io);
+ } else {
+ if (conn->io == NULL && conn->fd != -1) {
+ conn->io = io_add(conn->fd, IO_READ, ldap_input, conn);
+ ldap_input(conn);
+ }
+ }
+}
+
+static void db_ldap_disconnect_timeout(struct ldap_connection *conn)
+{
+ db_ldap_abort_requests(conn, UINT_MAX,
+ DB_LDAP_REQUEST_DISCONNECT_TIMEOUT_SECS, FALSE,
+ "Aborting (timeout), we're not connected to LDAP server");
+
+ if (aqueue_count(conn->request_queue) == 0) {
+ /* no requests left, remove this timeout handler */
+ timeout_remove(&conn->to);
+ }
+}
+
+static void db_ldap_conn_close(struct ldap_connection *conn)
+{
+ struct ldap_request *const *requests, *request;
+ unsigned int i;
+
+ conn->conn_state = LDAP_CONN_STATE_DISCONNECTED;
+ conn->delayed_connect = FALSE;
+ conn->default_bind_msgid = -1;
+
+ timeout_remove(&conn->to);
+
+ if (conn->pending_count != 0) {
+ requests = array_front(&conn->request_array);
+ for (i = 0; i < conn->pending_count; i++) {
+ request = requests[aqueue_idx(conn->request_queue, i)];
+
+ i_assert(request->msgid != -1);
+ request->msgid = -1;
+ }
+ conn->pending_count = 0;
+ }
+
+ if (conn->ld != NULL) {
+ ldap_unbind(conn->ld);
+ conn->ld = NULL;
+ }
+ conn->fd = -1;
+
+ /* the fd may have already been closed before ldap_unbind(),
+ so we'll have to use io_remove_closed(). */
+ io_remove_closed(&conn->io);
+
+ if (aqueue_count(conn->request_queue) > 0) {
+ conn->to = timeout_add(DB_LDAP_REQUEST_DISCONNECT_TIMEOUT_SECS *
+ 1000/2, db_ldap_disconnect_timeout, conn);
+ }
+}
+
+struct ldap_field_find_context {
+ ARRAY_TYPE(string) attr_names;
+ pool_t pool;
+};
+
+static int
+db_ldap_field_find(const char *data, void *context,
+ const char **value_r,
+ const char **error_r ATTR_UNUSED)
+{
+ struct ldap_field_find_context *ctx = context;
+ char *ldap_attr;
+
+ if (*data != '\0') {
+ ldap_attr = p_strdup(ctx->pool, t_strcut(data, ':'));
+ if (strchr(ldap_attr, '@') == NULL)
+ array_push_back(&ctx->attr_names, &ldap_attr);
+ }
+ *value_r = NULL;
+ return 1;
+}
+
+void db_ldap_set_attrs(struct ldap_connection *conn, const char *attrlist,
+ char ***attr_names_r, ARRAY_TYPE(ldap_field) *attr_map,
+ const char *skip_attr)
+{
+ static struct var_expand_func_table var_funcs_table[] = {
+ { "ldap", db_ldap_field_find },
+ { "ldap_ptr", db_ldap_field_find },
+ { NULL, NULL }
+ };
+ struct ldap_field_find_context ctx;
+ struct ldap_field *field;
+ string_t *tmp_str;
+ const char *const *attr, *attr_data, *p, *error;
+ char *ldap_attr, *name, *templ;
+ unsigned int i;
+
+ if (*attrlist == '\0')
+ return;
+
+ attr = t_strsplit_spaces(attrlist, ",");
+
+ tmp_str = t_str_new(128);
+ ctx.pool = conn->pool;
+ p_array_init(&ctx.attr_names, conn->pool, 16);
+ for (i = 0; attr[i] != NULL; i++) {
+ /* allow spaces here so "foo=1, bar=2" works */
+ attr_data = attr[i];
+ while (*attr_data == ' ') attr_data++;
+
+ p = strchr(attr_data, '=');
+ if (p == NULL)
+ ldap_attr = name = p_strdup(conn->pool, attr_data);
+ else if (attr_data[0] == '@') {
+ ldap_attr = "";
+ name = p_strdup(conn->pool, attr_data);
+ } else {
+ ldap_attr = p_strdup_until(conn->pool, attr_data, p);
+ name = p_strdup(conn->pool, p + 1);
+ }
+
+ templ = strchr(name, '=');
+ if (templ == NULL) {
+ if (*ldap_attr == '\0') {
+ /* =foo static value */
+ templ = "";
+ }
+ } else {
+ *templ++ = '\0';
+ str_truncate(tmp_str, 0);
+ if (var_expand_with_funcs(tmp_str, templ, NULL,
+ var_funcs_table, &ctx, &error) <= 0) {
+ /* This var_expand_with_funcs call fills the
+ * ldap_field_find_context in ctx, but the
+ * resulting string_t is not used, and the
+ * return value or error_r is not checked since
+ * it gives errors for non-ldap variable
+ * expansions. */
+ }
+ if (strchr(templ, '%') == NULL) {
+ /* backwards compatibility:
+ attr=name=prefix means same as
+ attr=name=prefix%$ when %vars are missing */
+ templ = p_strconcat(conn->pool, templ,
+ "%$", NULL);
+ }
+ }
+
+ if (*name == '\0')
+ e_error(conn->event, "Invalid attrs entry: %s", attr_data);
+ else if (skip_attr == NULL || strcmp(skip_attr, name) != 0) {
+ field = array_append_space(attr_map);
+ if (name[0] == '@') {
+ /* @name=ldapField */
+ name++;
+ field->value_is_dn = TRUE;
+ } else if (name[0] == '!' && name == ldap_attr) {
+ /* !ldapAttr */
+ name = "";
+ i_assert(ldap_attr[0] == '!');
+ ldap_attr++;
+ field->skip = TRUE;
+ }
+ field->name = name;
+ field->value = templ;
+ field->ldap_attr_name = ldap_attr;
+ if (*ldap_attr != '\0' &&
+ strchr(ldap_attr, '@') == NULL) {
+ /* root request's attribute */
+ array_push_back(&ctx.attr_names, &ldap_attr);
+ }
+ }
+ }
+ array_append_zero(&ctx.attr_names);
+ *attr_names_r = array_front_modifiable(&ctx.attr_names);
+}
+
+static const struct var_expand_table *
+db_ldap_value_get_var_expand_table(struct auth_request *auth_request,
+ const char *ldap_value)
+{
+ struct var_expand_table *table;
+ unsigned int count = 1;
+
+ table = auth_request_get_var_expand_table_full(auth_request,
+ auth_request->fields.user, NULL, &count);
+ table[0].key = '$';
+ table[0].value = ldap_value;
+ return table;
+}
+
+#define IS_LDAP_ESCAPED_CHAR(c) \
+ ((((unsigned char)(c)) & 0x80) != 0 || strchr(LDAP_ESCAPE_CHARS, (c)) != NULL)
+
+const char *ldap_escape(const char *str,
+ const struct auth_request *auth_request ATTR_UNUSED)
+{
+ string_t *ret = NULL;
+
+ for (const char *p = str; *p != '\0'; p++) {
+ if (IS_LDAP_ESCAPED_CHAR(*p)) {
+ if (ret == NULL) {
+ ret = t_str_new((size_t) (p - str) + 64);
+ str_append_data(ret, str, (size_t) (p - str));
+ }
+ str_printfa(ret, "\\%02X", (unsigned char)*p);
+ } else if (ret != NULL)
+ str_append_c(ret, *p);
+ }
+
+ return ret == NULL ? str : str_c(ret);
+}
+
+static bool
+ldap_field_hide_password(struct db_ldap_result_iterate_context *ctx,
+ const char *attr)
+{
+ const struct ldap_field *field;
+
+ if (ctx->ldap_request->auth_request->set->debug_passwords)
+ return FALSE;
+
+ array_foreach(ctx->attr_map, field) {
+ if (strcmp(field->ldap_attr_name, attr) == 0) {
+ if (strcmp(field->name, "password") == 0 ||
+ strcmp(field->name, "password_noscheme") == 0)
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+static void
+get_ldap_fields(struct db_ldap_result_iterate_context *ctx,
+ struct ldap_connection *conn, LDAPMessage *entry,
+ const char *suffix)
+{
+ struct db_ldap_value *ldap_value;
+ char *attr, **vals;
+ unsigned int i, count;
+ BerElement *ber;
+
+ attr = ldap_first_attribute(conn->ld, entry, &ber);
+ while (attr != NULL) {
+ vals = ldap_get_values(conn->ld, entry, attr);
+
+ ldap_value = p_new(ctx->pool, struct db_ldap_value, 1);
+ if (vals == NULL) {
+ ldap_value->values = p_new(ctx->pool, const char *, 1);
+ count = 0;
+ } else {
+ for (count = 0; vals[count] != NULL; count++) ;
+ }
+
+ ldap_value->values = p_new(ctx->pool, const char *, count + 1);
+ for (i = 0; i < count; i++)
+ ldap_value->values[i] = p_strdup(ctx->pool, vals[i]);
+
+ if (ctx->debug != NULL) {
+ str_printfa(ctx->debug, " %s%s=", attr, suffix);
+ if (count == 0)
+ str_append(ctx->debug, "<no values>");
+ else if (ldap_field_hide_password(ctx, attr))
+ str_append(ctx->debug, PASSWORD_HIDDEN_STR);
+ else {
+ str_append(ctx->debug, ldap_value->values[0]);
+ for (i = 1; i < count; i++) {
+ str_printfa(ctx->debug, ",%s",
+ ldap_value->values[0]);
+ }
+ }
+ }
+ hash_table_insert(ctx->ldap_attrs,
+ p_strconcat(ctx->pool, attr, suffix, NULL),
+ ldap_value);
+
+ ldap_value_free(vals);
+ ldap_memfree(attr);
+ attr = ldap_next_attribute(conn->ld, entry, ber);
+ }
+ ber_free(ber, 0);
+}
+
+struct db_ldap_result_iterate_context *
+db_ldap_result_iterate_init_full(struct ldap_connection *conn,
+ struct ldap_request_search *ldap_request,
+ LDAPMessage *res, bool skip_null_values,
+ bool iter_dn_values)
+{
+ struct db_ldap_result_iterate_context *ctx;
+ const struct ldap_request_named_result *named_res;
+ const char *suffix;
+ pool_t pool;
+
+ pool = pool_alloconly_create(MEMPOOL_GROWING"ldap result iter", 1024);
+ ctx = p_new(pool, struct db_ldap_result_iterate_context, 1);
+ ctx->pool = pool;
+ ctx->ldap_request = &ldap_request->request;
+ ctx->attr_map = ldap_request->attr_map;
+ ctx->skip_null_values = skip_null_values;
+ ctx->iter_dn_values = iter_dn_values;
+ hash_table_create(&ctx->ldap_attrs, pool, 0, strcase_hash, strcasecmp);
+ ctx->var = str_new(ctx->pool, 256);
+ if (event_want_debug(ctx->ldap_request->auth_request->event))
+ ctx->debug = t_str_new(256);
+ ctx->ldap_msg = res;
+ ctx->ld = conn->ld;
+
+ get_ldap_fields(ctx, conn, res, "");
+ if (array_is_created(&ldap_request->named_results)) {
+ array_foreach(&ldap_request->named_results, named_res) {
+ suffix = t_strdup_printf("@%s", named_res->field->name);
+ if (named_res->result != NULL) {
+ get_ldap_fields(ctx, conn,
+ named_res->result->msg, suffix);
+ }
+ }
+ }
+ return ctx;
+}
+
+struct db_ldap_result_iterate_context *
+db_ldap_result_iterate_init(struct ldap_connection *conn,
+ struct ldap_request_search *ldap_request,
+ LDAPMessage *res, bool skip_null_values)
+{
+ return db_ldap_result_iterate_init_full(conn, ldap_request, res,
+ skip_null_values, FALSE);
+}
+
+static const char *db_ldap_field_get_default(const char *data)
+{
+ const char *p;
+
+ p = i_strchr_to_next(data, ':');
+ if (p == NULL)
+ return "";
+ else {
+ /* default value given */
+ return p;
+ }
+}
+
+static int
+db_ldap_field_expand(const char *data, void *context,
+ const char **value_r, const char **error_r ATTR_UNUSED)
+{
+ struct db_ldap_result_iterate_context *ctx = context;
+ struct db_ldap_value *ldap_value;
+ const char *field_name = t_strcut(data, ':');
+
+ ldap_value = hash_table_lookup(ctx->ldap_attrs, field_name);
+ if (ldap_value == NULL) {
+ /* requested ldap attribute wasn't returned at all */
+ if (ctx->debug != NULL)
+ str_printfa(ctx->debug, "; %s missing", field_name);
+ *value_r = db_ldap_field_get_default(data);
+ return 1;
+ }
+ ldap_value->used = TRUE;
+
+ if (ldap_value->values[0] == NULL) {
+ /* no value for ldap attribute */
+ *value_r = db_ldap_field_get_default(data);
+ return 1;
+ }
+ if (ldap_value->values[1] != NULL) {
+ e_warning(authdb_event(ctx->ldap_request->auth_request),
+ "Multiple values found for '%s', using value '%s'",
+ field_name, ldap_value->values[0]);
+ }
+ *value_r = ldap_value->values[0];
+ return 1;
+}
+
+static int
+db_ldap_field_ptr_expand(const char *data, void *context,
+ const char **value_r, const char **error_r)
+{
+ struct db_ldap_result_iterate_context *ctx = context;
+ const char *field_name, *suffix;
+
+ suffix = strchr(t_strcut(data, ':'), '@');
+ if (db_ldap_field_expand(data, ctx, &field_name, error_r) <= 0)
+ i_unreached();
+ if (field_name[0] == '\0') {
+ *value_r = "";
+ return 1;
+ }
+ field_name = t_strconcat(field_name, suffix, NULL);
+ return db_ldap_field_expand(field_name, ctx, value_r, error_r);
+}
+
+static int
+db_ldap_field_dn_expand(const char *data ATTR_UNUSED, void *context ATTR_UNUSED,
+ const char **value_r, const char **error_r ATTR_UNUSED)
+{
+ struct db_ldap_result_iterate_context *ctx = context;
+ char *dn = ldap_get_dn(ctx->ld, ctx->ldap_msg);
+ *value_r = t_strdup(dn);
+ ldap_memfree(dn);
+ return 1;
+}
+
+static struct var_expand_func_table ldap_var_funcs_table[] = {
+ { "ldap", db_ldap_field_expand },
+ { "ldap_ptr", db_ldap_field_ptr_expand },
+ { "ldap_dn", db_ldap_field_dn_expand },
+ { NULL, NULL }
+};
+
+static const char *const *
+db_ldap_result_return_value(struct db_ldap_result_iterate_context *ctx,
+ const struct ldap_field *field,
+ struct db_ldap_value *ldap_value)
+{
+ const struct var_expand_table *var_table;
+ const char *const *values, *error;
+
+ if (ldap_value != NULL)
+ values = ldap_value->values;
+ else {
+ /* LDAP attribute doesn't exist */
+ ctx->val_1_arr[0] = NULL;
+ values = ctx->val_1_arr;
+ }
+
+ if (field->value == NULL) {
+ /* use the LDAP attribute's value */
+ } else {
+ /* template */
+ if (values[0] == NULL && *field->ldap_attr_name != '\0') {
+ /* ldapAttr=key=template%$, but ldapAttr doesn't
+ exist. */
+ return values;
+ }
+ if (values[0] != NULL && values[1] != NULL) {
+ e_warning(authdb_event(ctx->ldap_request->auth_request),
+ "Multiple values found for '%s', "
+ "using value '%s'",
+ field->name, values[0]);
+ }
+
+ /* do this lookup separately for each expansion, because:
+ 1) the values are allocated from data stack
+ 2) if "user" field is updated, we want %u/%n/%d updated
+ (and less importantly the same for other variables) */
+ var_table = db_ldap_value_get_var_expand_table(
+ ctx->ldap_request->auth_request, values[0]);
+ if (var_expand_with_funcs(ctx->var, field->value, var_table,
+ ldap_var_funcs_table, ctx, &error) <= 0) {
+ e_warning(authdb_event(ctx->ldap_request->auth_request),
+ "Failed to expand template %s: %s",
+ field->value, error);
+ }
+ ctx->val_1_arr[0] = str_c(ctx->var);
+ values = ctx->val_1_arr;
+ }
+ return values;
+}
+
+bool db_ldap_result_iterate_next(struct db_ldap_result_iterate_context *ctx,
+ const char **name_r,
+ const char *const **values_r)
+{
+ const struct var_expand_table *tab;
+ const struct ldap_field *field;
+ struct db_ldap_value *ldap_value;
+ unsigned int pos;
+ const char *error;
+
+ do {
+ if (ctx->attr_idx == array_count(ctx->attr_map))
+ return FALSE;
+ field = array_idx(ctx->attr_map, ctx->attr_idx++);
+ } while (field->value_is_dn != ctx->iter_dn_values ||
+ field->skip);
+
+ ldap_value = *field->ldap_attr_name == '\0' ? NULL :
+ hash_table_lookup(ctx->ldap_attrs, field->ldap_attr_name);
+ if (ldap_value != NULL)
+ ldap_value->used = TRUE;
+ else if (ctx->debug != NULL && *field->ldap_attr_name != '\0')
+ str_printfa(ctx->debug, "; %s missing", field->ldap_attr_name);
+
+ str_truncate(ctx->var, 0);
+ *values_r = db_ldap_result_return_value(ctx, field, ldap_value);
+
+ if (strchr(field->name, '%') == NULL)
+ *name_r = field->name;
+ else {
+ /* expand %variables also for LDAP name fields. we'll use the
+ same ctx->var, which may already contain the value. */
+ str_append_c(ctx->var, '\0');
+ pos = str_len(ctx->var);
+
+ tab = auth_request_get_var_expand_table(
+ ctx->ldap_request->auth_request, NULL);
+ if (var_expand_with_funcs(ctx->var, field->name, tab,
+ ldap_var_funcs_table, ctx, &error) <= 0) {
+ e_warning(authdb_event(ctx->ldap_request->auth_request),
+ "Failed to expand %s: %s", field->name, error);
+ }
+ *name_r = str_c(ctx->var) + pos;
+ }
+
+ if (ctx->skip_null_values && (*values_r)[0] == NULL) {
+ /* no values. don't confuse the caller with this reply. */
+ return db_ldap_result_iterate_next(ctx, name_r, values_r);
+ }
+ return TRUE;
+}
+
+static void
+db_ldap_result_finish_debug(struct db_ldap_result_iterate_context *ctx)
+{
+ struct hash_iterate_context *iter;
+ char *name;
+ struct db_ldap_value *value;
+ unsigned int unused_count = 0;
+ size_t orig_len;
+
+ if (ctx->ldap_request->result_logged)
+ return;
+
+ orig_len = str_len(ctx->debug);
+ if (orig_len == 0) {
+ e_debug(authdb_event(ctx->ldap_request->auth_request),
+ "no fields returned by the server");
+ return;
+ }
+
+ str_append(ctx->debug, "; ");
+
+ iter = hash_table_iterate_init(ctx->ldap_attrs);
+ while (hash_table_iterate(iter, ctx->ldap_attrs, &name, &value)) {
+ if (!value->used) {
+ str_printfa(ctx->debug, "%s,", name);
+ unused_count++;
+ }
+ }
+ hash_table_iterate_deinit(&iter);
+
+ if (unused_count == 0)
+ str_truncate(ctx->debug, orig_len);
+ else {
+ str_truncate(ctx->debug, str_len(ctx->debug)-1);
+ str_append(ctx->debug, " unused");
+ }
+ e_debug(authdb_event(ctx->ldap_request->auth_request),
+ "result: %s", str_c(ctx->debug) + 1);
+
+ ctx->ldap_request->result_logged = TRUE;
+}
+
+void db_ldap_result_iterate_deinit(struct db_ldap_result_iterate_context **_ctx)
+{
+ struct db_ldap_result_iterate_context *ctx = *_ctx;
+
+ *_ctx = NULL;
+
+ if (ctx->debug != NULL)
+ db_ldap_result_finish_debug(ctx);
+ hash_table_destroy(&ctx->ldap_attrs);
+ pool_unref(&ctx->pool);
+}
+
+static const char *parse_setting(const char *key, const char *value,
+ struct ldap_connection *conn)
+{
+ return parse_setting_from_defs(conn->pool, setting_defs,
+ &conn->set, key, value);
+}
+
+static struct ldap_connection *ldap_conn_find(const char *config_path)
+{
+ struct ldap_connection *conn;
+
+ for (conn = ldap_connections; conn != NULL; conn = conn->next) {
+ if (strcmp(conn->config_path, config_path) == 0)
+ return conn;
+ }
+
+ return NULL;
+}
+
+struct ldap_connection *db_ldap_init(const char *config_path, bool userdb)
+{
+ struct ldap_connection *conn;
+ const char *str, *error;
+ pool_t pool;
+
+ /* see if it already exists */
+ conn = ldap_conn_find(config_path);
+ if (conn != NULL) {
+ if (userdb)
+ conn->userdb_used = TRUE;
+ conn->refcount++;
+ return conn;
+ }
+
+ if (*config_path == '\0')
+ i_fatal("LDAP: Configuration file path not given");
+
+ pool = pool_alloconly_create("ldap_connection", 1024);
+ conn = p_new(pool, struct ldap_connection, 1);
+ conn->pool = pool;
+ conn->refcount = 1;
+
+ conn->userdb_used = userdb;
+ conn->conn_state = LDAP_CONN_STATE_DISCONNECTED;
+ conn->default_bind_msgid = -1;
+ conn->fd = -1;
+ conn->config_path = p_strdup(pool, config_path);
+ conn->set = default_ldap_settings;
+ if (!settings_read_nosection(config_path, parse_setting, conn, &error))
+ i_fatal("ldap %s: %s", config_path, error);
+
+ if (conn->set.base == NULL)
+ i_fatal("LDAP %s: No base given", config_path);
+
+ if (conn->set.uris == NULL && conn->set.hosts == NULL)
+ i_fatal("LDAP %s: No uris or hosts set", config_path);
+#ifndef LDAP_HAVE_INITIALIZE
+ if (conn->set.uris != NULL) {
+ i_fatal("LDAP %s: uris set, but Dovecot compiled without support for LDAP uris "
+ "(ldap_initialize() not supported by LDAP library)", config_path);
+ }
+#endif
+#ifndef LDAP_HAVE_START_TLS_S
+ if (conn->set.tls)
+ i_fatal("LDAP %s: tls=yes, but your LDAP library doesn't support TLS", config_path);
+#endif
+#ifndef HAVE_LDAP_SASL
+ if (conn->set.sasl_bind)
+ i_fatal("LDAP %s: sasl_bind=yes but no SASL support compiled in", conn->config_path);
+#endif
+ if (conn->set.ldap_version < 3) {
+ if (conn->set.sasl_bind)
+ i_fatal("LDAP %s: sasl_bind=yes requires ldap_version=3", config_path);
+ if (conn->set.tls)
+ i_fatal("LDAP %s: tls=yes requires ldap_version=3", config_path);
+ }
+#ifdef OPENLDAP_TLS_OPTIONS
+ if (conn->set.tls_require_cert != NULL) {
+ if (tls_require_cert2str(conn->set.tls_require_cert,
+ &conn->set.ldap_tls_require_cert_parsed) < 0)
+ i_fatal("LDAP %s: Unknown tls_require_cert value '%s'",
+ config_path, conn->set.tls_require_cert);
+ }
+#endif
+
+ if (*conn->set.ldaprc_path != '\0') {
+ str = getenv("LDAPRC");
+ if (str != NULL && strcmp(str, conn->set.ldaprc_path) != 0) {
+ i_fatal("LDAP %s: Multiple different ldaprc_path "
+ "settings not allowed (%s and %s)",
+ config_path, str, conn->set.ldaprc_path);
+ }
+ env_put("LDAPRC", conn->set.ldaprc_path);
+ }
+
+ if (deref2str(conn->set.deref, &conn->set.ldap_deref) < 0)
+ i_fatal("LDAP %s: Unknown deref option '%s'", config_path, conn->set.deref);
+ if (scope2str(conn->set.scope, &conn->set.ldap_scope) < 0)
+ i_fatal("LDAP %s: Unknown scope option '%s'", config_path, conn->set.scope);
+
+ conn->event = event_create(auth_event);
+ event_set_append_log_prefix(conn->event, t_strdup_printf(
+ "ldap(%s): ", conn->config_path));
+
+ i_array_init(&conn->request_array, 512);
+ conn->request_queue = aqueue_init(&conn->request_array.arr);
+
+ conn->next = ldap_connections;
+ ldap_connections = conn;
+
+ db_ldap_init_ld(conn);
+ return conn;
+}
+
+void db_ldap_unref(struct ldap_connection **_conn)
+{
+ struct ldap_connection *conn = *_conn;
+ struct ldap_connection **p;
+
+ *_conn = NULL;
+ i_assert(conn->refcount >= 0);
+ if (--conn->refcount > 0)
+ return;
+
+ for (p = &ldap_connections; *p != NULL; p = &(*p)->next) {
+ if (*p == conn) {
+ *p = conn->next;
+ break;
+ }
+ }
+
+ db_ldap_abort_requests(conn, UINT_MAX, 0, FALSE, "Shutting down");
+ i_assert(conn->pending_count == 0);
+ db_ldap_conn_close(conn);
+ i_assert(conn->to == NULL);
+
+ array_free(&conn->request_array);
+ aqueue_deinit(&conn->request_queue);
+
+ event_unref(&conn->event);
+ pool_unref(&conn->pool);
+}
+
+#ifndef BUILTIN_LDAP
+/* Building a plugin */
+extern struct passdb_module_interface passdb_ldap_plugin;
+extern struct userdb_module_interface userdb_ldap_plugin;
+
+void authdb_ldap_init(void);
+void authdb_ldap_deinit(void);
+
+void authdb_ldap_init(void)
+{
+ passdb_register_module(&passdb_ldap_plugin);
+ userdb_register_module(&userdb_ldap_plugin);
+
+}
+void authdb_ldap_deinit(void)
+{
+ passdb_unregister_module(&passdb_ldap_plugin);
+ userdb_unregister_module(&userdb_ldap_plugin);
+}
+#endif
+
+#endif