diff options
Diffstat (limited to 'pigeonhole/src/lib-sieve/storage/ldap/sieve-ldap-db.c')
-rw-r--r-- | pigeonhole/src/lib-sieve/storage/ldap/sieve-ldap-db.c | 1378 |
1 files changed, 1378 insertions, 0 deletions
diff --git a/pigeonhole/src/lib-sieve/storage/ldap/sieve-ldap-db.c b/pigeonhole/src/lib-sieve/storage/ldap/sieve-ldap-db.c new file mode 100644 index 0000000..0a7b440 --- /dev/null +++ b/pigeonhole/src/lib-sieve/storage/ldap/sieve-ldap-db.c @@ -0,0 +1,1378 @@ +/* Copyright (c) 2002-2018 Pigeonhole authors, see the included COPYING file + */ + +#include "lib.h" + +#include "sieve-common.h" + +#include "sieve-ldap-storage.h" + +/* FIXME: Imported this from Dovecot auth for now. We're working on a proper + lib-ldap, but, until then, some code is duplicated here. */ + +#if defined(SIEVE_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 "istream.h" + +#include <stddef.h> +#include <unistd.h> + +struct db_ldap_result { + int refcount; + LDAPMessage *msg; +}; + +struct db_ldap_result_iterate_context { + pool_t pool; + + struct auth_request *auth_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; +}; + +struct db_ldap_sasl_bind_context { + const char *authcid; + const char *passwd; + const char *realm; + const char *authzid; +}; + +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); + +int ldap_deref_from_str(const char *str, int *deref_r) +{ + if (strcasecmp(str, "never") == 0) + *deref_r = LDAP_DEREF_NEVER; + else if (strcasecmp(str, "searching") == 0) + *deref_r = LDAP_DEREF_SEARCHING; + else if (strcasecmp(str, "finding") == 0) + *deref_r = LDAP_DEREF_FINDING; + else if (strcasecmp(str, "always") == 0) + *deref_r = LDAP_DEREF_ALWAYS; + else + return -1; + return 0; +} + +int ldap_scope_from_str(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 +int ldap_tls_require_cert_from_str(const char *str, int *opt_x_tls_r) +{ + if (strcasecmp(str, "never") == 0) + *opt_x_tls_r = LDAP_OPT_X_TLS_NEVER; + else if (strcasecmp(str, "hard") == 0) + *opt_x_tls_r = LDAP_OPT_X_TLS_HARD; + else if (strcasecmp(str, "demand") == 0) + *opt_x_tls_r = LDAP_OPT_X_TLS_DEMAND; + else if (strcasecmp(str, "allow") == 0) + *opt_x_tls_r = LDAP_OPT_X_TLS_ALLOW; + else if (strcasecmp(str, "try") == 0) + *opt_x_tls_r = LDAP_OPT_X_TLS_TRY; + else + return -1; + return 0; +} +#endif + + +static int ldap_get_errno(struct ldap_connection *conn) +{ + struct sieve_storage *storage = &conn->lstorage->storage; + int ret, err; + + ret = ldap_get_option(conn->ld, LDAP_OPT_ERROR_NUMBER, (void *) &err); + if (ret != LDAP_SUCCESS) { + e_error(storage->event, "db: " + "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 (sieve_ldap_db_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_search(struct ldap_connection *conn, + struct ldap_request *request) +{ + struct sieve_storage *storage = &conn->lstorage->storage; + + i_assert(conn->conn_state == LDAP_CONN_STATE_BOUND); + i_assert(request->msgid == -1); + + request->msgid = + ldap_search(conn->ld, *request->base == '\0' ? NULL : + request->base, request->scope, + request->filter, request->attributes, 0); + if (request->msgid == -1) { + e_error(storage->event, "db: " + "ldap_search(%s) parsing failed: %s", + request->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 *const *requestp, *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 (sieve_ldap_db_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; + } + + requestp = array_idx(&conn->request_array, + aqueue_idx(conn->request_queue, + conn->pending_count)); + request = *requestp; + + 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: + /* we can do anything in this state */ + break; + } + + ret = db_ldap_request_search(conn, request); + if (ret > 0) { + /* success */ + i_assert(request->msgid != -1); + 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 bool +db_ldap_check_limits(struct ldap_connection *conn) +{ + struct sieve_storage *storage = &conn->lstorage->storage; + struct ldap_request *const *first_requestp; + unsigned int count; + time_t secs_diff; + + count = aqueue_count(conn->request_queue); + if (count == 0) + return TRUE; + + first_requestp = array_idx(&conn->request_array, + aqueue_idx(conn->request_queue, 0)); + secs_diff = ioloop_time - (*first_requestp)->create_time; + if (secs_diff > DB_LDAP_REQUEST_LOST_TIMEOUT_SECS) { + e_error(storage->event, "db: " + "Connection appears to be hanging, reconnecting"); + ldap_conn_reconnect(conn); + return TRUE; + } + return TRUE; +} + +void db_ldap_request(struct ldap_connection *conn, + struct ldap_request *request) +{ + request->msgid = -1; + request->create_time = ioloop_time; + + if (!db_ldap_check_limits(conn)) { + request->callback(conn, request, NULL); + return; + } + + 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) +{ + struct sieve_storage *storage = &conn->lstorage->storage; + const struct sieve_ldap_storage_settings *set = &conn->lstorage->set; + + if (ret == LDAP_SERVER_DOWN) { + e_error(storage->event, "db: " + "Can't connect to server: %s", + set->uris != NULL ? + set->uris : set->hosts); + return -1; + } + if (ret != LDAP_SUCCESS) { + e_error(storage->event, "db: " + "binding failed (dn %s): %s", + set->dn == NULL ? "(none)" : set->dn, + ldap_get_error(conn)); + return -1; + } + + timeout_remove(&conn->to); + conn->conn_state = LDAP_CONN_STATE_BOUND; + e_debug(storage->event, "db: " + "Successfully bound (dn %s)", + set->dn == NULL ? "(none)" : set->dn); + 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 void db_ldap_abort_requests(struct ldap_connection *conn, + unsigned int max_count, + unsigned int timeout_secs, + bool error, const char *reason) +{ + struct sieve_storage *storage = &conn->lstorage->storage; + struct ldap_request *const *requestp, *request; + time_t diff; + + while (aqueue_count(conn->request_queue) > 0 && max_count > 0) { + requestp = array_idx(&conn->request_array, + aqueue_idx(conn->request_queue, 0)); + request = *requestp; + + 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(storage->event, "db: %s", reason); + else + e_debug(storage->event, "db: %s", reason); + request->callback(conn, request, NULL); + max_count--; + } +} + +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_idx(&conn->request_array, 0); + 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 bool +db_ldap_handle_request_result(struct ldap_connection *conn, + struct ldap_request *request, unsigned int idx, + struct db_ldap_result *res) +{ + struct sieve_storage *storage = &conn->lstorage->storage; + int ret; + bool final_result; + + i_assert(conn->pending_count > 0); + + 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(storage->event, "db: 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); + } + if (ret != LDAP_SUCCESS) { + /* handle search failures here */ + e_error(storage->event, "db: " + "ldap_search(base=%s filter=%s) failed: %s", + request->base, request->filter, + ldap_err2string(ret)); + res = NULL; + } else { + if (!final_result && storage->svinst->debug) { + e_debug(storage->event, + "db: ldap_search(base=%s filter=%s) returned entry: %s", + request->base, request->filter, + ldap_get_dn(conn->ld, res->msg)); + } + } + 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 { + request->callback(conn, request, res == NULL ? NULL : res->msg); + } T_END; + + if (idx > 0) { + /* see if there are timed out requests */ + db_ldap_abort_requests(conn, idx, + DB_LDAP_REQUEST_LOST_TIMEOUT_SECS, + TRUE, "Request lost"); + } + 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->result != NULL) + db_ldap_result_unref(&request->result); +} + +static void +db_ldap_handle_result(struct ldap_connection *conn, struct db_ldap_result *res) +{ + struct sieve_storage *storage = &conn->lstorage->storage; + 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(storage->event, + "db: Reply with unknown msgid %d", msgid); + return; + } + + if (db_ldap_handle_request_result(conn, request, idx, res)) + db_ldap_request_free(request); +} + +static void ldap_input(struct ldap_connection *conn) +{ + struct sieve_storage *storage = &conn->lstorage->storage; + 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(storage->event, "db: 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(storage->event, + "db: 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) +{ + struct sieve_storage *storage = &conn->lstorage->storage; + i_assert(conn->conn_state == LDAP_CONN_STATE_BINDING); + + e_error(storage->event, "db: Initial binding to LDAP server timed out"); + db_ldap_conn_close(conn); +} + +static int db_ldap_bind(struct ldap_connection *conn) +{ + const struct sieve_ldap_storage_settings *set = &conn->lstorage->set; + 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, set->dn, 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_get_fd(struct ldap_connection *conn) +{ + struct sieve_storage *storage = &conn->lstorage->storage; + int ret; + + /* get the connection's fd */ + ret = ldap_get_option(conn->ld, LDAP_OPT_DESC, (void *)&conn->fd); + if (ret != LDAP_SUCCESS) { + e_error(storage->event, "db: Can't get connection fd: %s", + ldap_err2string(ret)); + return -1; + } + if (conn->fd <= STDERR_FILENO) { + /* Solaris LDAP library seems to be broken */ + e_error(storage->event, + "db: Buggy LDAP library returned wrong fd: %d", + conn->fd); + return -1; + } + i_assert(conn->fd != -1); + net_set_nonblock(conn->fd, TRUE); + return 0; +} + +static int +db_ldap_set_opt(struct ldap_connection *conn, int opt, const void *value, + const char *optname, const char *value_str) +{ + struct sieve_storage *storage = &conn->lstorage->storage; + int ret; + + ret = ldap_set_option(conn->ld, opt, value); + if (ret != LDAP_SUCCESS) { + e_error(storage->event, "db: Can't set option %s to %s: %s", + optname, value_str, ldap_err2string(ret)); + return -1; + } + return 0; +} + +static int +db_ldap_set_opt_str(struct ldap_connection *conn, int opt, const char *value, + const char *optname) +{ + if (value != NULL) + return db_ldap_set_opt(conn, opt, value, optname, value); + return 0; +} + +static int db_ldap_set_tls_options(struct ldap_connection *conn) +{ + const struct sieve_ldap_storage_settings *set = &conn->lstorage->set; + + if (!set->tls) + return 0; + +#ifdef OPENLDAP_TLS_OPTIONS + if (db_ldap_set_opt_str(conn, LDAP_OPT_X_TLS_CACERTFILE, + set->tls_ca_cert_file, "tls_ca_cert_file") < 0) + return -1; + if (db_ldap_set_opt_str(conn, LDAP_OPT_X_TLS_CACERTDIR, + set->tls_ca_cert_dir, "tls_ca_cert_dir") < 0) + return -1; + if (db_ldap_set_opt_str(conn, LDAP_OPT_X_TLS_CERTFILE, + set->tls_cert_file, "tls_cert_file") < 0) + return -1; + if (db_ldap_set_opt_str(conn, LDAP_OPT_X_TLS_KEYFILE, + set->tls_key_file, "tls_key_file") < 0) + return -1; + if (db_ldap_set_opt_str(conn, LDAP_OPT_X_TLS_CIPHER_SUITE, + set->tls_cipher_suite, "tls_cipher_suite") < 0) + return -1; + if (set->tls_require_cert != NULL) { + if (db_ldap_set_opt(conn, LDAP_OPT_X_TLS_REQUIRE_CERT, + &set->ldap_tls_require_cert, + "tls_require_cert", set->tls_require_cert) < 0) + return -1; + } +#else + if (set->tls_ca_cert_file != NULL || + set->tls_ca_cert_dir != NULL || + set->tls_cert_file != NULL || + set->tls_key_file != NULL || + set->tls_cipher_suite != NULL) { + e_warning(&conn->lstorage->storage, "db: " + "tls_* settings ignored, " + "your LDAP library doesn't seem to support them"); + } +#endif + return 0; +} + +static int db_ldap_set_options(struct ldap_connection *conn) +{ + const struct sieve_ldap_storage_settings *set = &conn->lstorage->set; + struct sieve_storage *storage = &conn->lstorage->storage; + unsigned int ldap_version; + int value; + + if (db_ldap_set_opt(conn, LDAP_OPT_DEREF, &set->ldap_deref, + "deref", set->deref) < 0) + return -1; +#ifdef LDAP_OPT_DEBUG_LEVEL + if (str_to_int(set->debug_level, &value) >= 0 && value != 0) { + if (db_ldap_set_opt(conn, LDAP_OPT_DEBUG_LEVEL, &value, + "debug_level", set->debug_level) < 0) + return -1; + } +#endif + + if (set->ldap_version < 3) { + if (set->sasl_bind) { + e_error(storage->event, + "db: sasl_bind=yes requires ldap_version=3"); + return -1; + } + if (set->tls) { + e_error(storage->event, + "db: tls=yes requires ldap_version=3"); + return -1; + } + } + + ldap_version = set->ldap_version; + if (db_ldap_set_opt(conn, LDAP_OPT_PROTOCOL_VERSION, &ldap_version, + "protocol_version", dec2str(ldap_version)) < 0) + return -1; + if (db_ldap_set_tls_options(conn) < 0) + return -1; + return 0; +} + +int sieve_ldap_db_connect(struct ldap_connection *conn) +{ + const struct sieve_ldap_storage_settings *set = &conn->lstorage->set; + struct sieve_storage *storage = &conn->lstorage->storage; + struct timeval start, end; + int debug_level; + bool debug; +#if defined(HAVE_LDAP_SASL) || defined(LDAP_HAVE_START_TLS_S) + int ret; +#endif + + if (conn->conn_state != LDAP_CONN_STATE_DISCONNECTED) + return 0; + + debug = FALSE; + if (str_to_int(set->debug_level, &debug_level) >= 0) + debug = debug_level > 0; + + if (debug) + i_gettimeofday(&start); + i_assert(conn->pending_count == 0); + if (conn->ld == NULL) { + if (set->uris != NULL) { +#ifdef LDAP_HAVE_INITIALIZE + if (ldap_initialize(&conn->ld, set->uris) != LDAP_SUCCESS) + conn->ld = NULL; +#else + e_error(storage->event, "db: " + "Your LDAP library doesn't support " + "'uris' setting, use 'hosts' instead."); + return -1; +#endif + } else + conn->ld = ldap_init(set->hosts, LDAP_PORT); + + if (conn->ld == NULL) { + e_error(storage->event, "db: " + "ldap_init() failed with hosts: %s", set->hosts); + return -1; + } + + if (db_ldap_set_options(conn) < 0) + return -1; + } + + if (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 && + set->uris != NULL && + str_begins(set->uris, "ldaps:")) { + e_error(storage->event, "db: " + "Don't use both tls=yes and ldaps URI"); + } + e_error(storage->event, "db: " + "ldap_start_tls_s() failed: %s", + ldap_err2string(ret)); + return -1; + } +#else + e_error(storage->event, "db: " + "Your LDAP library doesn't support TLS"); + return -1; +#endif + } + + if (set->sasl_bind) { +#ifdef HAVE_LDAP_SASL + struct db_ldap_sasl_bind_context context; + + i_zero(&context); + context.authcid = set->dn; + context.passwd = set->dnpass; + context.realm = set->sasl_realm; + context.authzid = 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, + set->sasl_mech, + NULL, NULL, LDAP_SASL_QUIET, + sasl_interact, &context); + if (db_ldap_connect_finish(conn, ret) < 0) + return -1; +#else + e_error(storage->event, "db: " + "sasl_bind=yes but no SASL support compiled in"); + return -1; +#endif + conn->conn_state = LDAP_CONN_STATE_BOUND; + } else { + if (db_ldap_bind(conn) < 0) + return -1; + } + if (debug) { + i_gettimeofday(&end); + int msecs = timeval_diff_msecs(&end, &start); + e_debug(storage->event, "db: " + "Initialization took %d msecs", msecs); + } + + if (db_ldap_get_fd(conn) < 0) + return -1; + conn->io = io_add(conn->fd, IO_READ, ldap_input, conn); + return 0; +} + +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->default_bind_msgid = -1; + + timeout_remove(&conn->to); + + if (conn->pending_count != 0) { + requests = array_idx(&conn->request_array, 0); + 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; +}; + +#define IS_LDAP_ESCAPED_CHAR(c) \ + ((c) == '*' || (c) == '(' || (c) == ')' || (c) == '\\') + +const char *ldap_escape(const char *str) +{ + const char *p; + string_t *ret; + + for (p = str; *p != '\0'; p++) { + if (IS_LDAP_ESCAPED_CHAR(*p)) + break; + } + + if (*p == '\0') + return str; + + ret = t_str_new((size_t) (p - str) + 64); + str_append_data(ret, str, (size_t) (p - str)); + + for (; *p != '\0'; p++) { + if (IS_LDAP_ESCAPED_CHAR(*p)) + str_append_c(ret, '\\'); + str_append_c(ret, *p); + } + return str_c(ret); +} + +struct ldap_connection * +sieve_ldap_db_init(struct sieve_ldap_storage *lstorage) +{ + struct ldap_connection *conn; + pool_t pool; + + pool = pool_alloconly_create("ldap_connection", 1024); + conn = p_new(pool, struct ldap_connection, 1); + conn->pool = pool; + conn->refcount = 1; + conn->lstorage = lstorage; + + conn->conn_state = LDAP_CONN_STATE_DISCONNECTED; + conn->default_bind_msgid = -1; + conn->fd = -1; + + i_array_init(&conn->request_array, 512); + conn->request_queue = aqueue_init(&conn->request_array.arr); + + conn->next = ldap_connections; + ldap_connections = conn; + return conn; +} + +void sieve_ldap_db_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); + + pool_unref(&conn->pool); +} + +static void db_ldap_switch_ioloop(struct ldap_connection *conn) +{ + if (conn->to != NULL) + conn->to = io_loop_move_timeout(&conn->to); + if (conn->io != NULL) + conn->io = io_loop_move_io(&conn->io); +} + +static void db_ldap_wait(struct ldap_connection *conn) +{ + struct sieve_storage *storage = &conn->lstorage->storage; + struct ioloop *prev_ioloop = current_ioloop; + + i_assert(conn->ioloop == NULL); + + if (aqueue_count(conn->request_queue) == 0) + return; + + conn->ioloop = io_loop_create(); + db_ldap_switch_ioloop(conn); + /* either we're waiting for network I/O or we're getting out of a + callback using timeout_add_short(0) */ + i_assert(io_loop_have_ios(conn->ioloop) || + io_loop_have_immediate_timeouts(conn->ioloop)); + + do { + e_debug(storage->event, "db: " + "Waiting for %d requests to finish", + aqueue_count(conn->request_queue) ); + io_loop_run(conn->ioloop); + } while (aqueue_count(conn->request_queue) > 0); + + e_debug(storage->event, "db: All requests finished"); + + current_ioloop = prev_ioloop; + db_ldap_switch_ioloop(conn); + current_ioloop = conn->ioloop; + io_loop_destroy(&conn->ioloop); +} + +static void sieve_ldap_db_script_free(unsigned char *script) +{ + i_free(script); +} + +static int +sieve_ldap_db_get_script_modattr(struct ldap_connection *conn, + LDAPMessage *entry, pool_t pool, const char **modattr_r) +{ + const struct sieve_ldap_storage_settings *set = &conn->lstorage->set; + struct sieve_storage *storage = &conn->lstorage->storage; + char *attr, **vals; + BerElement *ber; + + *modattr_r = NULL; + + attr = ldap_first_attribute(conn->ld, entry, &ber); + while (attr != NULL) { + if (strcmp(attr, set->sieve_ldap_mod_attr) == 0) { + vals = ldap_get_values(conn->ld, entry, attr); + if (vals == NULL || vals[0] == NULL) + return 0; + + if (vals[1] != NULL) { + e_warning(storage->event, "db: " + "Search returned more than one Sieve modified attribute `%s'; " + "using only the first one.", set->sieve_ldap_mod_attr); + } + + *modattr_r = p_strdup(pool, vals[0]); + + ldap_value_free(vals); + ldap_memfree(attr); + return 1; + } + ldap_memfree(attr); + attr = ldap_next_attribute(conn->ld, entry, ber); + } + ber_free(ber, 0); + + return 0; +} + +static int +sieve_ldap_db_get_script(struct ldap_connection *conn, + LDAPMessage *entry, struct istream **script_r) +{ + const struct sieve_ldap_storage_settings *set = &conn->lstorage->set; + struct sieve_storage *storage = &conn->lstorage->storage; + char *attr; + unsigned char *data; + size_t size; + struct berval **vals; + BerElement *ber; + + attr = ldap_first_attribute(conn->ld, entry, &ber); + while (attr != NULL) { + if (strcmp(attr, set->sieve_ldap_script_attr) == 0) { + vals = ldap_get_values_len(conn->ld, entry, attr); + if (vals == NULL || vals[0] == NULL) + return 0; + + if (vals[1] != NULL) { + e_warning(storage->event, "db: " + "Search returned more than one Sieve script attribute `%s'; " + "using only the first one.", set->sieve_ldap_script_attr); + } + + size = vals[0]->bv_len; + data = i_malloc(size); + + e_debug(storage->event, "db: " + "Found script with length %zu", size); + + memcpy(data, vals[0]->bv_val, size); + + ldap_value_free_len(vals); + ldap_memfree(attr); + + *script_r = i_stream_create_from_data(data, size); + i_stream_add_destroy_callback + (*script_r, sieve_ldap_db_script_free, data); + return 1; + } + ldap_memfree(attr); + attr = ldap_next_attribute(conn->ld, entry, ber); + } + ber_free(ber, 0); + + return 0; +} + +const struct var_expand_table +auth_request_var_expand_static_tab[] = { + { 'u', NULL, "user" }, + { 'n', NULL, "username" }, + { 'd', NULL, "domain" }, + { 'h', NULL, "home" }, + { '\0', NULL, "name" }, + { '\0', NULL, NULL } +}; + +static const struct var_expand_table * +db_ldap_get_var_expand_table(struct ldap_connection *conn, + const char *name) +{ + struct sieve_ldap_storage *lstorage = conn->lstorage; + struct sieve_instance *svinst = lstorage->storage.svinst; + const unsigned int auth_count = + N_ELEMENTS(auth_request_var_expand_static_tab); + struct var_expand_table *tab; + + /* keep the extra fields at the beginning. the last static_tab field + contains the ending NULL-fields. */ + tab = t_malloc_no0((auth_count) * sizeof(*tab)); + + memcpy(tab, auth_request_var_expand_static_tab, + auth_count * sizeof(*tab)); + + tab[0].value = ldap_escape(lstorage->username); + tab[1].value = ldap_escape(t_strcut(lstorage->username, '@')); + tab[2].value = strchr(lstorage->username, '@'); + if (tab[2].value != NULL) + tab[2].value = ldap_escape(tab[2].value+1); + tab[3].value = ldap_escape(svinst->home_dir); + tab[4].value = ldap_escape(name); + return tab; +} + +struct sieve_ldap_script_lookup_request { + struct ldap_request request; + + unsigned int entries; + const char *result_dn; + const char *result_modattr; +}; + +static void +sieve_ldap_lookup_script_callback(struct ldap_connection *conn, + struct ldap_request *request, LDAPMessage *res) +{ + struct sieve_storage *storage = &conn->lstorage->storage; + struct sieve_ldap_script_lookup_request *srequest = + (struct sieve_ldap_script_lookup_request *)request; + + if (res == NULL) { + io_loop_stop(conn->ioloop); + return; + } + + if (ldap_msgtype(res) != LDAP_RES_SEARCH_RESULT) { + if (srequest->result_dn == NULL) { + srequest->result_dn = p_strdup + (request->pool, ldap_get_dn(conn->ld, res)); + (void)sieve_ldap_db_get_script_modattr + (conn, res, request->pool, &srequest->result_modattr); + } else if (srequest->entries++ == 0) { + e_warning(storage->event, "db: " + "Search returned more than one entry for Sieve script; " + "using only the first one."); + } + } else { + io_loop_stop(conn->ioloop); + return; + } +} + +int sieve_ldap_db_lookup_script(struct ldap_connection *conn, + const char *name, const char **dn_r, const char **modattr_r) +{ + struct sieve_ldap_storage *lstorage = conn->lstorage; + struct sieve_storage *storage = &lstorage->storage; + const struct sieve_ldap_storage_settings *set = &lstorage->set; + struct sieve_ldap_script_lookup_request *request; + const struct var_expand_table *tab; + char **attr_names; + const char *error; + string_t *str; + + pool_t pool = pool_alloconly_create + ("sieve_ldap_script_lookup_request", 512); + request = p_new(pool, struct sieve_ldap_script_lookup_request, 1); + request->request.pool = pool; + + tab = db_ldap_get_var_expand_table(conn, name); + + str = t_str_new(512); + if (var_expand(str, set->base, tab, &error) <= 0) { + e_error(storage->event, "db: " + "Failed to expand base=%s: %s", + set->base, error); + return -1; + } + request->request.base = p_strdup(pool, str_c(str)); + + attr_names = p_new(pool, char *, 3); + attr_names[0] = p_strdup(pool, set->sieve_ldap_mod_attr); + + str_truncate(str, 0); + if (var_expand(str, set->sieve_ldap_filter, tab, &error) <= 0) { + e_error(storage->event, "db: " + "Failed to expand sieve_ldap_filter=%s: %s", + set->sieve_ldap_filter, error); + return -1; + } + + request->request.scope = lstorage->set.ldap_scope; + request->request.filter = p_strdup(pool, str_c(str)); + request->request.attributes = attr_names; + + e_debug(storage->event, "base=%s scope=%s filter=%s fields=%s", + request->request.base, lstorage->set.scope, + request->request.filter, + t_strarray_join((const char **)attr_names, ",")); + + request->request.callback = sieve_ldap_lookup_script_callback; + db_ldap_request(conn, &request->request); + db_ldap_wait(conn); + + *dn_r = t_strdup(request->result_dn); + *modattr_r = t_strdup(request->result_modattr); + pool_unref(&request->request.pool); + return (*dn_r == NULL ? 0 : 1); +} + +struct sieve_ldap_script_read_request { + struct ldap_request request; + + unsigned int entries; + struct istream *result; +}; + +static void +sieve_ldap_read_script_callback(struct ldap_connection *conn, + struct ldap_request *request, LDAPMessage *res) +{ + struct sieve_storage *storage = &conn->lstorage->storage; + struct sieve_ldap_script_read_request *srequest = + (struct sieve_ldap_script_read_request *)request; + + if (res == NULL) { + io_loop_stop(conn->ioloop); + return; + } + + if (ldap_msgtype(res) != LDAP_RES_SEARCH_RESULT) { + + if (srequest->result == NULL) { + (void)sieve_ldap_db_get_script(conn, res, &srequest->result); + } else { + e_error(storage->event, "db: " + "Search returned more than one entry for Sieve script DN"); + i_stream_unref(&srequest->result); + } + + } else { + io_loop_stop(conn->ioloop); + return; + } +} + +int sieve_ldap_db_read_script(struct ldap_connection *conn, + const char *dn, struct istream **script_r) +{ + struct sieve_ldap_storage *lstorage = conn->lstorage; + struct sieve_storage *storage = &lstorage->storage; + const struct sieve_ldap_storage_settings *set = &lstorage->set; + struct sieve_ldap_script_read_request *request; + char **attr_names; + + pool_t pool = pool_alloconly_create + ("sieve_ldap_script_read_request", 512); + request = p_new(pool, struct sieve_ldap_script_read_request, 1); + request->request.pool = pool; + request->request.base = p_strdup(pool, dn); + + attr_names = p_new(pool, char *, 3); + attr_names[0] = p_strdup(pool, set->sieve_ldap_script_attr); + + request->request.scope = LDAP_SCOPE_BASE; + request->request.filter = "(objectClass=*)"; + request->request.attributes = attr_names; + + e_debug(storage->event, "base=%s scope=base filter=%s fields=%s", + request->request.base, request->request.filter, + t_strarray_join((const char **)attr_names, ",")); + + request->request.callback = sieve_ldap_read_script_callback; + db_ldap_request(conn, &request->request); + db_ldap_wait(conn); + + *script_r = request->result; + pool_unref(&request->request.pool); + return (*script_r == NULL ? 0 : 1); +} + + +#endif |