diff options
Diffstat (limited to 'src/global/dict_ldap.c')
-rw-r--r-- | src/global/dict_ldap.c | 1994 |
1 files changed, 1994 insertions, 0 deletions
diff --git a/src/global/dict_ldap.c b/src/global/dict_ldap.c new file mode 100644 index 0000000..a4a4c26 --- /dev/null +++ b/src/global/dict_ldap.c @@ -0,0 +1,1994 @@ +/*++ +/* NAME +/* dict_ldap 3 +/* SUMMARY +/* dictionary manager interface to LDAP maps +/* SYNOPSIS +/* #include <dict_ldap.h> +/* +/* DICT *dict_ldap_open(attribute, dummy, dict_flags) +/* const char *ldapsource; +/* int dummy; +/* int dict_flags; +/* DESCRIPTION +/* dict_ldap_open() makes LDAP user information accessible via +/* the generic dictionary operations described in dict_open(3). +/* +/* Arguments: +/* .IP ldapsource +/* Either the path to the LDAP configuration file (if it starts +/* with '/' or '.'), or the prefix which will be used to obtain +/* configuration parameters for this search. +/* +/* In the first case, the configuration variables below are +/* specified in the file as \fBname\fR=\fBvalue\fR pairs. +/* +/* In the second case, the configuration variables are prefixed +/* with the value of \fIldapsource\fR and an underscore, +/* and they are specified in main.cf. For example, if this +/* value is \fBldapone\fR, the variables would look like +/* \fBldapone_server_host\fR, \fBldapone_search_base\fR, and so on. +/* .IP dummy +/* Not used; this argument exists only for compatibility with +/* the dict_open(3) interface. +/* .PP +/* Configuration parameters: +/* .IP server_host +/* List of hosts at which all LDAP queries are directed. +/* The host names can also be LDAP URLs if the LDAP client library used +/* is OpenLDAP. +/* .IP server_port +/* The port the LDAP server listens on. +/* .IP search_base +/* The LDAP search base, for example: \fIO=organization name, C=country\fR. +/* .IP domain +/* If specified, only lookups ending in this value will be queried. +/* This can significantly reduce the query load on the LDAP server. +/* .IP timeout +/* Deadline for LDAP open() and LDAP search() . +/* .IP query_filter +/* The search filter template used to search for directory entries, +/* for example \fI(mailacceptinggeneralid=%s)\fR. See ldap_table(5) +/* for details. +/* .IP result_format +/* The result template used to expand results from queries. Default +/* is \fI%s\fR. See ldap_table(5) for details. Also supported under +/* the name \fIresult_filter\fR for compatibility with older releases. +/* .IP result_attribute +/* The attribute(s) returned by the search, in which to find +/* RFC822 addresses, for example \fImaildrop\fR. +/* .IP special_result_attribute +/* The attribute(s) of directory entries that can contain DNs or URLs. +/* If found, a recursive subsequent search is done using their values. +/* .IP leaf_result_attribute +/* These are only returned for "leaf" LDAP entries, i.e. those that are +/* not "terminal" and have no values for any of the "special" result +/* attributes. +/* .IP terminal_result_attribute +/* If found, the LDAP entry is considered a terminal LDAP object, not +/* subject to further direct or recursive expansion. Only the terminal +/* result attributes are returned. +/* .IP scope +/* LDAP search scope: sub, base, or one. +/* .IP bind +/* Whether or not to bind to the server -- LDAP v3 implementations don't +/* require it, which saves some overhead. +/* .IP bind_dn +/* If you must bind to the server, do it with this distinguished name ... +/* .IP bind_pw +/* \&... and this password. +/* .IP cache (no longer supported) +/* Whether or not to turn on client-side caching. +/* .IP cache_expiry (no longer supported) +/* If you do cache results, expire them after this many seconds. +/* .IP cache_size (no longer supported) +/* The cache size in bytes. Does nothing if the cache is off, of course. +/* .IP recursion_limit +/* Maximum recursion depth when expanding DN or URL references. +/* Queries which exceed the recursion limit fail with +/* dict->error = DICT_ERR_RETRY. +/* .IP expansion_limit +/* Limit (if any) on the total number of lookup result values. Lookups which +/* exceed the limit fail with dict->error=DICT_ERR_RETRY. Note that +/* each value of a multivalued result attribute counts as one result. +/* .IP size_limit +/* Limit on the number of entries returned by individual LDAP queries. +/* Queries which exceed the limit fail with dict->error=DICT_ERR_RETRY. +/* This is an *entry* count, for any single query performed during the +/* possibly recursive lookup. +/* .IP chase_referrals +/* Controls whether LDAP referrals are obeyed. +/* .IP dereference +/* How to handle LDAP aliases. See ldap.h or ldap_open(3) man page. +/* .IP version +/* Specifies the LDAP protocol version to use. Default is version +/* \fI2\fR. +/* .IP "\fBsasl_mechs (empty)\fR" +/* Specifies a space-separated list of LDAP SASL Mechanisms. +/* .IP "\fBsasl_realm (empty)\fR" +/* The realm to use for SASL binds. +/* .IP "\fBsasl_authz_id (empty)\fR" +/* The SASL Authorization Identity to assert. +/* .IP "\fBsasl_minssf (0)\fR" +/* The minimum SASL SSF to allow. +/* .IP start_tls +/* Whether or not to issue STARTTLS upon connection to the server. +/* At this time, STARTTLS and LDAP SSL are only available if the +/* LDAP client library used is OpenLDAP. Default is \fIno\fR. +/* .IP tls_ca_cert_file +/* File containing certificates for all of the X509 Certification +/* Authorities the client will recognize. Takes precedence over +/* tls_ca_cert_dir. +/* .IP tls_ca_cert_dir +/* Directory containing X509 Certification Authority certificates +/* in separate individual files. +/* .IP tls_cert +/* File containing client's X509 certificate. +/* .IP tls_key +/* File containing the private key corresponding to +/* tls_cert. +/* .IP tls_require_cert +/* Whether or not to request server's X509 certificate and check its +/* validity. The value "no" means don't check the cert trust chain +/* and (OpenLDAP 2.1+) don't check the peername. The value "yes" means +/* check both the trust chain and the peername (with OpenLDAP <= 2.0.11, +/* the peername checks use the reverse hostname from the LDAP servers's +/* IP address, not the user supplied servername). +/* .IP tls_random_file +/* Path of a file to obtain random bits from when /dev/[u]random is +/* not available. Generally set to the name of the EGD/PRNGD socket. +/* .IP tls_cipher_suite +/* Cipher suite to use in SSL/TLS negotiations. +/* .IP debuglevel +/* Debug level. See 'loglevel' option in slapd.conf(5) man page. +/* Currently only in openldap libraries (and derivatives). +/* SEE ALSO +/* dict(3) generic dictionary manager +/* AUTHOR(S) +/* Prabhat K Singh +/* VSNL, Bombay, India. +/* prabhat@giasbm01.vsnl.net.in +/* +/* Wietse Venema +/* IBM T.J. Watson Research +/* P.O. Box 704 +/* Yorktown Heights, NY 10598, USA +/* +/* Wietse Venema +/* Google, Inc. +/* 111 8th Avenue +/* New York, NY 10011, USA +/* +/* John Hensley +/* john@sunislelodge.com +/* +/* Current maintainers: +/* +/* LaMont Jones +/* lamont@debian.org +/* +/* Victor Duchovni +/* Morgan Stanley +/* New York, USA +/* +/* Liviu Daia +/* Institute of Mathematics of the Romanian Academy +/* P.O. BOX 1-764 +/* RO-014700 Bucharest, ROMANIA +/*--*/ + +/* System library. */ + +#include "sys_defs.h" + +#ifdef HAS_LDAP + +#include <sys/time.h> +#include <stdio.h> +#include <signal.h> +#include <setjmp.h> +#include <stdlib.h> +#include <lber.h> +#include <ldap.h> +#include <string.h> +#include <ctype.h> +#include <unistd.h> + +#ifdef STRCASECMP_IN_STRINGS_H +#include <strings.h> +#endif + + /* + * Older APIs have weird memory freeing behavior. + */ +#if !defined(LDAP_API_VERSION) || (LDAP_API_VERSION < 2000) +#error "Your LDAP version is too old" +#endif + +/* Handle differences between LDAP SDK's constant definitions */ +#ifndef LDAP_CONST +#define LDAP_CONST const +#endif +#ifndef LDAP_OPT_SUCCESS +#define LDAP_OPT_SUCCESS 0 +#endif + +/* Utility library. */ + +#include <msg.h> +#include <mymalloc.h> +#include <vstring.h> +#include <dict.h> +#include <stringops.h> +#include <binhash.h> +#include <name_code.h> + +/* Global library. */ + +#include "cfg_parser.h" +#include "db_common.h" +#include "mail_conf.h" + +#if defined(USE_LDAP_SASL) && defined(LDAP_API_FEATURE_X_OPENLDAP) + + /* + * SASL headers, for sasl_interact_t. Either SASL v1 or v2 should be fine. + */ +#include <sasl.h> +#endif + +/* Application-specific. */ + +#include "dict_ldap.h" + +#define DICT_LDAP_BIND_NONE 0 +#define DICT_LDAP_BIND_SIMPLE 1 +#define DICT_LDAP_BIND_SASL 2 +#define DICT_LDAP_DO_BIND(d) ((d)->bind != DICT_LDAP_BIND_NONE) +#define DICT_LDAP_DO_SASL(d) ((d)->bind == DICT_LDAP_BIND_SASL) + +static const NAME_CODE bindopt_table[] = { + CONFIG_BOOL_NO, DICT_LDAP_BIND_NONE, + "none", DICT_LDAP_BIND_NONE, + CONFIG_BOOL_YES, DICT_LDAP_BIND_SIMPLE, + "simple", DICT_LDAP_BIND_SIMPLE, +#ifdef LDAP_API_FEATURE_X_OPENLDAP +#if defined(USE_LDAP_SASL) + "sasl", DICT_LDAP_BIND_SASL, +#endif +#endif + 0, -1, +}; + +typedef struct { + LDAP *conn_ld; + int conn_refcount; +} LDAP_CONN; + +/* + * Structure containing all the configuration parameters for a given + * LDAP source, plus its connection handle. + */ +typedef struct { + DICT dict; /* generic member */ + CFG_PARSER *parser; /* common parameter parser */ + char *query; /* db_common_expand() query */ + char *result_format; /* db_common_expand() result_format */ + void *ctx; /* db_common_parse() context */ + int dynamic_base; /* Search base has substitutions? */ + int expansion_limit; + char *server_host; + int server_port; + int scope; + char *search_base; + ARGV *result_attributes; + int num_terminal; /* Number of terminal attributes. */ + int num_leaf; /* Number of leaf attributes */ + int num_attributes; /* Combined # of non-special attrs */ + int bind; + char *bind_dn; + char *bind_pw; + int timeout; + int dereference; + long recursion_limit; + long size_limit; + int chase_referrals; + int debuglevel; + int version; +#ifdef LDAP_API_FEATURE_X_OPENLDAP +#if defined(USE_LDAP_SASL) + int sasl; + char *sasl_mechs; + char *sasl_realm; + char *sasl_authz; + int sasl_minssf; +#endif + int ldap_ssl; + int start_tls; + int tls_require_cert; + char *tls_ca_cert_file; + char *tls_ca_cert_dir; + char *tls_cert; + char *tls_key; + char *tls_random_file; + char *tls_cipher_suite; +#endif + BINHASH_INFO *ht; /* hash entry for LDAP connection */ + LDAP *ld; /* duplicated from conn->conn_ld */ +} DICT_LDAP; + +#define DICT_LDAP_CONN(d) ((LDAP_CONN *)((d)->ht->value)) + +#define DICT_LDAP_UNBIND_RETURN(__ld, __err, __ret) do { \ + dict_ldap_unbind(__ld); \ + (__ld) = 0; \ + dict_ldap->dict.error = (__err); \ + return ((__ret)); \ + } while (0) + + /* + * Bitrot: LDAP_API 3000 and up (OpenLDAP 2.2.x) deprecated ldap_unbind() + */ +#if LDAP_API_VERSION >= 3000 +#define dict_ldap_unbind(ld) ldap_unbind_ext((ld), 0, 0) +#define dict_ldap_abandon(ld, msg) ldap_abandon_ext((ld), (msg), 0, 0) +#else +#define dict_ldap_unbind(ld) ldap_unbind(ld) +#define dict_ldap_abandon(ld, msg) ldap_abandon((ld), (msg)) +#endif + +static int dict_ldap_vendor_version(void) +{ + const char *myname = "dict_ldap_api_info"; + LDAPAPIInfo api; + + /* + * We tell the library our version, and it tells us its version and/or + * may return an error code if the versions are not the same. + */ + api.ldapai_info_version = LDAP_API_INFO_VERSION; + if (ldap_get_option(0, LDAP_OPT_API_INFO, &api) != LDAP_SUCCESS + || api.ldapai_info_version != LDAP_API_INFO_VERSION) { + if (api.ldapai_info_version != LDAP_API_INFO_VERSION) + msg_fatal("%s: run-time API_INFO version: %d, compiled with: %d", + myname, api.ldapai_info_version, LDAP_API_INFO_VERSION); + else + msg_fatal("%s: ldap_get_option(API_INFO) failed", myname); + } + if (strcmp(api.ldapai_vendor_name, LDAP_VENDOR_NAME) != 0) + msg_fatal("%s: run-time API vendor: %s, compiled with: %s", + myname, api.ldapai_vendor_name, LDAP_VENDOR_NAME); + + return (api.ldapai_vendor_version); +} + +/* + * Quoting rules. + */ + +/* rfc2253_quote - Quote input key for safe inclusion in the search base */ + +static void rfc2253_quote(DICT *unused, const char *name, VSTRING *result) +{ + const char *sub = name; + size_t len; + + /* + * The RFC only requires quoting of a leading or trailing space, but it + * is harmless to quote whitespace everywhere. Similarly, we quote all + * '#' characters, even though only the leading '#' character requires + * quoting per the RFC. + */ + while (*sub) + if ((len = strcspn(sub, " \t\"#+,;<>\\")) > 0) { + vstring_strncat(result, sub, len); + sub += len; + } else + vstring_sprintf_append(result, "\\%02X", + *((const unsigned char *) sub++)); +} + +/* rfc2254_quote - Quote input key for safe inclusion in the query filter */ + +static void rfc2254_quote(DICT *unused, const char *name, VSTRING *result) +{ + const char *sub = name; + size_t len; + + /* + * If any characters in the supplied address should be escaped per RFC + * 2254, do so. Thanks to Keith Stevenson and Wietse. And thanks to + * Samuel Tardieu for spotting that wildcard searches were being done in + * the first place, which prompted the ill-conceived lookup_wildcards + * parameter and then this more comprehensive mechanism. + */ + while (*sub) + if ((len = strcspn(sub, "*()\\")) > 0) { + vstring_strncat(result, sub, len); + sub += len; + } else + vstring_sprintf_append(result, "\\%02X", + *((const unsigned char *) sub++)); +} + +static BINHASH *conn_hash = 0; + +#if defined(LDAP_API_FEATURE_X_OPENLDAP) || !defined(LDAP_OPT_NETWORK_TIMEOUT) +/* + * LDAP connection timeout support. + */ +static jmp_buf env; + +static void dict_ldap_timeout(int unused_sig) +{ + longjmp(env, 1); +} + +#endif + +static void dict_ldap_logprint(LDAP_CONST char *data) +{ + const char *myname = "dict_ldap_debug"; + char *buf, *p; + + buf = mystrdup(data); + if (*buf) { + p = buf + strlen(buf) - 1; + while (p - buf >= 0 && ISSPACE(*p)) + *p-- = 0; + } + msg_info("%s: %s", myname, buf); + myfree(buf); +} + +static int dict_ldap_get_errno(LDAP *ld) +{ + int rc; + + if (ldap_get_option(ld, LDAP_OPT_ERROR_NUMBER, &rc) != LDAP_OPT_SUCCESS) + rc = LDAP_OTHER; + return rc; +} + +static int dict_ldap_set_errno(LDAP *ld, int rc) +{ + (void) ldap_set_option(ld, LDAP_OPT_ERROR_NUMBER, &rc); + return rc; +} + +#if defined(USE_LDAP_SASL) && defined(LDAP_API_FEATURE_X_OPENLDAP) + + /* + * Context structure for SASL property callback. + */ +typedef struct bind_props { + char *authcid; + char *passwd; + char *realm; + char *authzid; +} bind_props; + +static int ldap_b2_interact(LDAP *ld, unsigned flags, void *props, void *inter) +{ + + sasl_interact_t *in; + bind_props *ctx = (bind_props *) props; + + for (in = inter; in->id != SASL_CB_LIST_END; in++) { + in->result = NULL; + switch (in->id) { + case SASL_CB_GETREALM: + in->result = ctx->realm; + break; + case SASL_CB_AUTHNAME: + in->result = ctx->authcid; + break; + case SASL_CB_USER: + in->result = ctx->authzid; + break; + case SASL_CB_PASS: + in->result = ctx->passwd; + break; + } + if (in->result) + in->len = strlen(in->result); + } + return LDAP_SUCCESS; +} + +#endif + +/* dict_ldap_result - Read and parse LDAP result */ + +static int dict_ldap_result(LDAP *ld, int msgid, int timeout, LDAPMessage **res) +{ + struct timeval mytimeval; + int err; + + mytimeval.tv_sec = timeout; + mytimeval.tv_usec = 0; + +#define GET_ALL 1 + if (ldap_result(ld, msgid, GET_ALL, &mytimeval, res) == -1) + return (dict_ldap_get_errno(ld)); + + if ((err = dict_ldap_get_errno(ld)) != LDAP_SUCCESS) { + if (err == LDAP_TIMEOUT) { + (void) dict_ldap_abandon(ld, msgid); + return (dict_ldap_set_errno(ld, LDAP_TIMEOUT)); + } + return err; + } + return LDAP_SUCCESS; +} + +#if defined(USE_LDAP_SASL) && defined(LDAP_API_FEATURE_X_OPENLDAP) + +/* Asynchronous SASL auth if SASL is enabled */ + +static int dict_ldap_bind_sasl(DICT_LDAP *dict_ldap) +{ + int rc; + bind_props props; + static VSTRING *minssf = 0; + + if (minssf == 0) + minssf = vstring_alloc(12); + + vstring_sprintf(minssf, "minssf=%d", dict_ldap->sasl_minssf); + + if ((rc = ldap_set_option(dict_ldap->ld, LDAP_OPT_X_SASL_SECPROPS, + (char *) minssf)) != LDAP_OPT_SUCCESS) + return (rc); + + props.authcid = dict_ldap->bind_dn; + props.passwd = dict_ldap->bind_pw; + props.realm = dict_ldap->sasl_realm; + props.authzid = dict_ldap->sasl_authz; + + if ((rc = ldap_sasl_interactive_bind_s(dict_ldap->ld, NULL, + dict_ldap->sasl_mechs, NULL, NULL, + LDAP_SASL_QUIET, ldap_b2_interact, + &props)) != LDAP_SUCCESS) + return (rc); + + return (LDAP_SUCCESS); +} + +#endif + +/* dict_ldap_bind_st - Synchronous simple auth with timeout */ + +static int dict_ldap_bind_st(DICT_LDAP *dict_ldap) +{ + int rc; + int err = LDAP_SUCCESS; + int msgid; + LDAPMessage *res; + struct berval cred; + + cred.bv_val = dict_ldap->bind_pw; + cred.bv_len = strlen(cred.bv_val); + if ((rc = ldap_sasl_bind(dict_ldap->ld, dict_ldap->bind_dn, + LDAP_SASL_SIMPLE, &cred, + 0, 0, &msgid)) != LDAP_SUCCESS) + return (rc); + if ((rc = dict_ldap_result(dict_ldap->ld, msgid, dict_ldap->timeout, + &res)) != LDAP_SUCCESS) + return (rc); + +#define FREE_RESULT 1 + rc = ldap_parse_result(dict_ldap->ld, res, &err, 0, 0, 0, 0, FREE_RESULT); + return (rc == LDAP_SUCCESS ? err : rc); +} + +/* search_st - Synchronous search with timeout */ + +static int search_st(LDAP *ld, char *base, int scope, char *query, + char **attrs, int timeout, LDAPMessage **res) +{ + struct timeval mytimeval; + int msgid; + int rc; + int err; + + mytimeval.tv_sec = timeout; + mytimeval.tv_usec = 0; + +#define WANTVALS 0 +#define USE_SIZE_LIM_OPT -1 /* Any negative value will do */ + + if ((rc = ldap_search_ext(ld, base, scope, query, attrs, WANTVALS, 0, 0, + &mytimeval, USE_SIZE_LIM_OPT, + &msgid)) != LDAP_SUCCESS) + return rc; + + if ((rc = dict_ldap_result(ld, msgid, timeout, res)) != LDAP_SUCCESS) + return (rc); + +#define DONT_FREE_RESULT 0 + rc = ldap_parse_result(ld, *res, &err, 0, 0, 0, 0, DONT_FREE_RESULT); + return (err != LDAP_SUCCESS ? err : rc); +} + +#ifdef LDAP_API_FEATURE_X_OPENLDAP +static int dict_ldap_set_tls_options(DICT_LDAP *dict_ldap) +{ + const char *myname = "dict_ldap_set_tls_options"; + int rc; + +#ifdef LDAP_OPT_X_TLS_NEWCTX + int am_server = 0; + LDAP *ld = dict_ldap->ld; + +#else + LDAP *ld = 0; + +#endif + + if (dict_ldap->start_tls || dict_ldap->ldap_ssl) { + if (*dict_ldap->tls_random_file) { + if ((rc = ldap_set_option(ld, LDAP_OPT_X_TLS_RANDOM_FILE, + dict_ldap->tls_random_file)) != LDAP_SUCCESS) { + msg_warn("%s: Unable to set tls_random_file to %s: %d: %s", + myname, dict_ldap->tls_random_file, + rc, ldap_err2string(rc)); + return (-1); + } + } + if (*dict_ldap->tls_ca_cert_file) { + if ((rc = ldap_set_option(ld, LDAP_OPT_X_TLS_CACERTFILE, + dict_ldap->tls_ca_cert_file)) != LDAP_SUCCESS) { + msg_warn("%s: Unable to set tls_ca_cert_file to %s: %d: %s", + myname, dict_ldap->tls_ca_cert_file, + rc, ldap_err2string(rc)); + return (-1); + } + } + if (*dict_ldap->tls_ca_cert_dir) { + if ((rc = ldap_set_option(ld, LDAP_OPT_X_TLS_CACERTDIR, + dict_ldap->tls_ca_cert_dir)) != LDAP_SUCCESS) { + msg_warn("%s: Unable to set tls_ca_cert_dir to %s: %d: %s", + myname, dict_ldap->tls_ca_cert_dir, + rc, ldap_err2string(rc)); + return (-1); + } + } + if (*dict_ldap->tls_cert) { + if ((rc = ldap_set_option(ld, LDAP_OPT_X_TLS_CERTFILE, + dict_ldap->tls_cert)) != LDAP_SUCCESS) { + msg_warn("%s: Unable to set tls_cert to %s: %d: %s", + myname, dict_ldap->tls_cert, + rc, ldap_err2string(rc)); + return (-1); + } + } + if (*dict_ldap->tls_key) { + if ((rc = ldap_set_option(ld, LDAP_OPT_X_TLS_KEYFILE, + dict_ldap->tls_key)) != LDAP_SUCCESS) { + msg_warn("%s: Unable to set tls_key to %s: %d: %s", + myname, dict_ldap->tls_key, + rc, ldap_err2string(rc)); + return (-1); + } + } + if (*dict_ldap->tls_cipher_suite) { + if ((rc = ldap_set_option(ld, LDAP_OPT_X_TLS_CIPHER_SUITE, + dict_ldap->tls_cipher_suite)) != LDAP_SUCCESS) { + msg_warn("%s: Unable to set tls_cipher_suite to %s: %d: %s", + myname, dict_ldap->tls_cipher_suite, + rc, ldap_err2string(rc)); + return (-1); + } + } + if ((rc = ldap_set_option(ld, LDAP_OPT_X_TLS_REQUIRE_CERT, + &(dict_ldap->tls_require_cert))) != LDAP_SUCCESS) { + msg_warn("%s: Unable to set tls_require_cert to %d: %d: %s", + myname, dict_ldap->tls_require_cert, + rc, ldap_err2string(rc)); + return (-1); + } +#ifdef LDAP_OPT_X_TLS_NEWCTX + if ((rc = ldap_set_option(ld, LDAP_OPT_X_TLS_NEWCTX, &am_server)) + != LDAP_SUCCESS) { + msg_warn("%s: Unable to allocate new TLS context %d: %s", + myname, rc, ldap_err2string(rc)); + return (-1); + } +#endif + } + return (0); +} + +#endif + +/* Establish a connection to the LDAP server. */ +static int dict_ldap_connect(DICT_LDAP *dict_ldap) +{ + const char *myname = "dict_ldap_connect"; + int rc = 0; + +#ifdef LDAP_OPT_NETWORK_TIMEOUT + struct timeval mytimeval; + +#endif + +#if defined(LDAP_API_FEATURE_X_OPENLDAP) || !defined(LDAP_OPT_NETWORK_TIMEOUT) + void (*saved_alarm) (int); + +#endif + +#if defined(LDAP_OPT_DEBUG_LEVEL) && defined(LBER_OPT_LOG_PRINT_FN) + if (dict_ldap->debuglevel > 0 && + ber_set_option(NULL, LBER_OPT_LOG_PRINT_FN, + (LDAP_CONST void *) dict_ldap_logprint) != LBER_OPT_SUCCESS) + msg_warn("%s: Unable to set ber logprint function.", myname); +#if defined(LBER_OPT_DEBUG_LEVEL) + if (ber_set_option(NULL, LBER_OPT_DEBUG_LEVEL, + &(dict_ldap->debuglevel)) != LBER_OPT_SUCCESS) + msg_warn("%s: Unable to set BER debug level.", myname); +#endif + if (ldap_set_option(NULL, LDAP_OPT_DEBUG_LEVEL, + &(dict_ldap->debuglevel)) != LDAP_OPT_SUCCESS) + msg_warn("%s: Unable to set LDAP debug level.", myname); +#endif + + dict_ldap->dict.error = 0; + + if (msg_verbose) + msg_info("%s: Connecting to server %s", myname, + dict_ldap->server_host); + +#ifdef LDAP_OPT_NETWORK_TIMEOUT +#ifdef LDAP_API_FEATURE_X_OPENLDAP + ldap_initialize(&(dict_ldap->ld), dict_ldap->server_host); +#else + dict_ldap->ld = ldap_init(dict_ldap->server_host, + (int) dict_ldap->server_port); +#endif + if (dict_ldap->ld == NULL) { + msg_warn("%s: Unable to init LDAP server %s", + myname, dict_ldap->server_host); + dict_ldap->dict.error = DICT_ERR_RETRY; + return (-1); + } + mytimeval.tv_sec = dict_ldap->timeout; + mytimeval.tv_usec = 0; + if (ldap_set_option(dict_ldap->ld, LDAP_OPT_NETWORK_TIMEOUT, &mytimeval) != + LDAP_OPT_SUCCESS) { + msg_warn("%s: Unable to set network timeout.", myname); + DICT_LDAP_UNBIND_RETURN(dict_ldap->ld, DICT_ERR_RETRY, -1); + } +#else + if ((saved_alarm = signal(SIGALRM, dict_ldap_timeout)) == SIG_ERR) { + msg_warn("%s: Error setting signal handler for open timeout: %m", + myname); + dict_ldap->dict.error = DICT_ERR_RETRY; + return (-1); + } + alarm(dict_ldap->timeout); + if (setjmp(env) == 0) + dict_ldap->ld = ldap_open(dict_ldap->server_host, + (int) dict_ldap->server_port); + else + dict_ldap->ld = 0; + alarm(0); + + if (signal(SIGALRM, saved_alarm) == SIG_ERR) { + msg_warn("%s: Error resetting signal handler after open: %m", + myname); + dict_ldap->dict.error = DICT_ERR_RETRY; + return (-1); + } + if (dict_ldap->ld == NULL) { + msg_warn("%s: Unable to connect to LDAP server %s", + myname, dict_ldap->server_host); + dict_ldap->dict.error = DICT_ERR_RETRY; + return (-1); + } +#endif + + /* + * v3 support is needed for referral chasing. Thanks to Sami Haahtinen + * for the patch. + */ +#ifdef LDAP_OPT_PROTOCOL_VERSION + if (ldap_set_option(dict_ldap->ld, LDAP_OPT_PROTOCOL_VERSION, + &dict_ldap->version) != LDAP_OPT_SUCCESS) { + msg_warn("%s: Unable to set LDAP protocol version", myname); + DICT_LDAP_UNBIND_RETURN(dict_ldap->ld, DICT_ERR_RETRY, -1); + } + if (msg_verbose) { + if (ldap_get_option(dict_ldap->ld, + LDAP_OPT_PROTOCOL_VERSION, + &dict_ldap->version) != LDAP_OPT_SUCCESS) + msg_warn("%s: Unable to get LDAP protocol version", myname); + else + msg_info("%s: Actual Protocol version used is %d.", + myname, dict_ldap->version); + } +#endif + + /* + * Limit the number of entries returned by each query. + */ + if (dict_ldap->size_limit) { + if (ldap_set_option(dict_ldap->ld, LDAP_OPT_SIZELIMIT, + &dict_ldap->size_limit) != LDAP_OPT_SUCCESS) { + msg_warn("%s: %s: Unable to set query result size limit to %ld.", + myname, dict_ldap->parser->name, dict_ldap->size_limit); + DICT_LDAP_UNBIND_RETURN(dict_ldap->ld, DICT_ERR_RETRY, -1); + } + } + + /* + * Configure alias dereferencing for this connection. Thanks to Mike + * Mattice for this, and to Hery Rakotoarisoa for the v3 update. + */ + if (ldap_set_option(dict_ldap->ld, LDAP_OPT_DEREF, + &(dict_ldap->dereference)) != LDAP_OPT_SUCCESS) + msg_warn("%s: Unable to set dereference option.", myname); + + /* Chase referrals. */ + +#ifdef LDAP_OPT_REFERRALS + if (ldap_set_option(dict_ldap->ld, LDAP_OPT_REFERRALS, + dict_ldap->chase_referrals ? LDAP_OPT_ON : LDAP_OPT_OFF) + != LDAP_OPT_SUCCESS) { + msg_warn("%s: Unable to set Referral chasing.", myname); + DICT_LDAP_UNBIND_RETURN(dict_ldap->ld, DICT_ERR_RETRY, -1); + } +#else + if (dict_ldap->chase_referrals) { + msg_warn("%s: Unable to set Referral chasing.", myname); + } +#endif + +#ifdef LDAP_API_FEATURE_X_OPENLDAP + if (dict_ldap_set_tls_options(dict_ldap) != 0) + DICT_LDAP_UNBIND_RETURN(dict_ldap->ld, DICT_ERR_RETRY, -1); + if (dict_ldap->start_tls) { + if ((saved_alarm = signal(SIGALRM, dict_ldap_timeout)) == SIG_ERR) { + msg_warn("%s: Error setting signal handler for STARTTLS timeout: %m", + myname); + DICT_LDAP_UNBIND_RETURN(dict_ldap->ld, DICT_ERR_RETRY, -1); + } + alarm(dict_ldap->timeout); + if (setjmp(env) == 0) + rc = ldap_start_tls_s(dict_ldap->ld, NULL, NULL); + else { + rc = LDAP_TIMEOUT; + dict_ldap->ld = 0; /* Unknown state after + * longjmp() */ + } + alarm(0); + + if (signal(SIGALRM, saved_alarm) == SIG_ERR) { + msg_warn("%s: Error resetting signal handler after STARTTLS: %m", + myname); + dict_ldap->dict.error = DICT_ERR_RETRY; + return (-1); + } + if (rc != LDAP_SUCCESS) { + msg_error("%s: Unable to set STARTTLS: %d: %s", myname, + rc, ldap_err2string(rc)); + dict_ldap->dict.error = DICT_ERR_RETRY; + return (-1); + } + } +#endif + +#define DN_LOG_VAL(dict_ldap) \ + ((dict_ldap)->bind_dn[0] ? (dict_ldap)->bind_dn : "empty or implicit") + + /* + * If this server requires a bind, do so. Thanks to Sam Tardieu for + * noticing that the original bind call was broken. + */ + if (DICT_LDAP_DO_BIND(dict_ldap)) { + if (msg_verbose) + msg_info("%s: Binding to server %s with dn %s", + myname, dict_ldap->server_host, DN_LOG_VAL(dict_ldap)); + +#if defined(USE_LDAP_SASL) && defined(LDAP_API_FEATURE_X_OPENLDAP) + if (DICT_LDAP_DO_SASL(dict_ldap)) { + rc = dict_ldap_bind_sasl(dict_ldap); + } else { + rc = dict_ldap_bind_st(dict_ldap); + } +#else + rc = dict_ldap_bind_st(dict_ldap); +#endif + + if (rc != LDAP_SUCCESS) { + msg_warn("%s: Unable to bind to server %s with dn %s: %d (%s)", + myname, dict_ldap->server_host, DN_LOG_VAL(dict_ldap), + rc, ldap_err2string(rc)); + DICT_LDAP_UNBIND_RETURN(dict_ldap->ld, DICT_ERR_RETRY, -1); + } + if (msg_verbose) + msg_info("%s: Successful bind to server %s with dn %s", + myname, dict_ldap->server_host, DN_LOG_VAL(dict_ldap)); + } + /* Save connection handle in shared container */ + DICT_LDAP_CONN(dict_ldap)->conn_ld = dict_ldap->ld; + + if (msg_verbose) + msg_info("%s: Cached connection handle for LDAP source %s", + myname, dict_ldap->parser->name); + + return (0); +} + +/* + * Locate or allocate connection cache entry. + */ +static void dict_ldap_conn_find(DICT_LDAP *dict_ldap) +{ + VSTRING *keybuf = vstring_alloc(10); + char *key; + int len; + +#ifdef LDAP_API_FEATURE_X_OPENLDAP + int sslon = dict_ldap->start_tls || dict_ldap->ldap_ssl; + +#endif + LDAP_CONN *conn; + + /* + * Join key fields with null characters. + */ +#define ADDSTR(vp, s) vstring_memcat((vp), (s), strlen((s))+1) +#define ADDINT(vp, i) vstring_sprintf_append((vp), "%lu%c", (unsigned long)(i), 0) + + ADDSTR(keybuf, dict_ldap->server_host); + ADDINT(keybuf, dict_ldap->server_port); + ADDINT(keybuf, dict_ldap->bind); + ADDSTR(keybuf, DICT_LDAP_DO_BIND(dict_ldap) ? dict_ldap->bind_dn : ""); + ADDSTR(keybuf, DICT_LDAP_DO_BIND(dict_ldap) ? dict_ldap->bind_pw : ""); + ADDINT(keybuf, dict_ldap->dereference); + ADDINT(keybuf, dict_ldap->chase_referrals); + ADDINT(keybuf, dict_ldap->debuglevel); + ADDINT(keybuf, dict_ldap->version); +#ifdef LDAP_API_FEATURE_X_OPENLDAP +#if defined(USE_LDAP_SASL) + ADDSTR(keybuf, DICT_LDAP_DO_SASL(dict_ldap) ? dict_ldap->sasl_mechs : ""); + ADDSTR(keybuf, DICT_LDAP_DO_SASL(dict_ldap) ? dict_ldap->sasl_realm : ""); + ADDSTR(keybuf, DICT_LDAP_DO_SASL(dict_ldap) ? dict_ldap->sasl_authz : ""); + ADDINT(keybuf, DICT_LDAP_DO_SASL(dict_ldap) ? dict_ldap->sasl_minssf : 0); +#endif + ADDINT(keybuf, dict_ldap->ldap_ssl); + ADDINT(keybuf, dict_ldap->start_tls); + ADDINT(keybuf, sslon ? dict_ldap->tls_require_cert : 0); + ADDSTR(keybuf, sslon ? dict_ldap->tls_ca_cert_file : ""); + ADDSTR(keybuf, sslon ? dict_ldap->tls_ca_cert_dir : ""); + ADDSTR(keybuf, sslon ? dict_ldap->tls_cert : ""); + ADDSTR(keybuf, sslon ? dict_ldap->tls_key : ""); + ADDSTR(keybuf, sslon ? dict_ldap->tls_random_file : ""); + ADDSTR(keybuf, sslon ? dict_ldap->tls_cipher_suite : ""); +#endif + + key = vstring_str(keybuf); + len = VSTRING_LEN(keybuf); + + if (conn_hash == 0) + conn_hash = binhash_create(0); + + if ((dict_ldap->ht = binhash_locate(conn_hash, key, len)) == 0) { + conn = (LDAP_CONN *) mymalloc(sizeof(LDAP_CONN)); + conn->conn_ld = 0; + conn->conn_refcount = 0; + dict_ldap->ht = binhash_enter(conn_hash, key, len, (void *) conn); + } + ++DICT_LDAP_CONN(dict_ldap)->conn_refcount; + + vstring_free(keybuf); +} + +/* attr_sub_type - Is one of two attributes a sub-type of another */ + +static int attrdesc_subtype(const char *a1, const char *a2) +{ + + /* + * RFC 2251 section 4.1.4: LDAP attribute names are case insensitive + */ + while (*a1 && TOLOWER(*a1) == TOLOWER(*a2)) + ++a1, ++a2; + + /* + * Names equal to end of a1, is a2 equal or a subtype? + */ + if (*a1 == 0 && (*a2 == 0 || *a2 == ';')) + return (1); + + /* + * Names equal to end of a2, is a1 a subtype? + */ + if (*a2 == 0 && *a1 == ';') + return (-1); + + /* + * Distinct attributes + */ + return (0); +} + +/* url_attrs - attributes we want from LDAP URL */ + +static char **url_attrs(DICT_LDAP *dict_ldap, LDAPURLDesc * url) +{ + static ARGV *attrs; + char **a1; + char **a2; + int arel; + + /* + * If the LDAP URI specified no attributes, all entry attributes are + * returned, leading to unnecessarily large LDAP results, particularly + * since dynamic groups are most useful for large groups. + * + * Since we only make use of the various mumble_results attributes, we ask + * only for these, thus making large queries much faster. + * + * In one test case, a query returning 75K users took 16 minutes when all + * attributes are returned, and just under 3 minutes with only the + * desired result attribute. + */ + if (url->lud_attrs == 0 || *url->lud_attrs == 0) + return (dict_ldap->result_attributes->argv); + + /* + * When the LDAP URI explicitly specifies a set of attributes, we use the + * interaction of the URI attributes and our result attributes. This way + * LDAP URIs can hide certain attributes that should not be part of the + * query. There is no point in retrieving attributes not listed in our + * result set, we won't make any use of those. + */ + if (attrs) + argv_truncate(attrs, 0); + else + attrs = argv_alloc(2); + + /* + * Retrieve only those attributes that are of interest to us. + * + * If the URL attribute and the attribute we want differ only in the + * "options" part of the attribute descriptor, select the more specific + * attribute descriptor. + */ + for (a1 = url->lud_attrs; *a1; ++a1) { + for (a2 = dict_ldap->result_attributes->argv; *a2; ++a2) { + arel = attrdesc_subtype(*a1, *a2); + if (arel > 0) + argv_add(attrs, *a2, ARGV_END); + else if (arel < 0) + argv_add(attrs, *a1, ARGV_END); + } + } + + return ((attrs->argc > 0) ? attrs->argv : 0); +} + +/* + * dict_ldap_get_values: for each entry returned by a search, get the values + * of all its attributes. Recurses to resolve any DN or URL values found. + * + * This and the rest of the handling of multiple attributes, DNs and URLs + * are thanks to LaMont Jones. + */ +static void dict_ldap_get_values(DICT_LDAP *dict_ldap, LDAPMessage *res, + VSTRING *result, const char *name) +{ + static int recursion = 0; + static int expansion; + long entries = 0; + long i = 0; + int rc = 0; + LDAPMessage *resloop = 0; + LDAPMessage *entry = 0; + BerElement *ber; + char *attr; + char **attrs; + struct berval **vals; + int valcount; + LDAPURLDesc *url; + const char *myname = "dict_ldap_get_values"; + int is_leaf = 1; /* No recursion via this entry */ + int is_terminal = 0; /* No expansion via this entry */ + + if (++recursion == 1) + expansion = 0; + + if (msg_verbose) + msg_info("%s[%d]: Search found %d match(es)", myname, recursion, + ldap_count_entries(dict_ldap->ld, res)); + + for (entry = ldap_first_entry(dict_ldap->ld, res); entry != NULL; + entry = ldap_next_entry(dict_ldap->ld, entry)) { + ber = NULL; + + /* + * LDAP should not, but may produce more than the requested maximum + * number of entries. + */ + if (dict_ldap->dict.error == 0 + && dict_ldap->size_limit + && ++entries > dict_ldap->size_limit) { + msg_warn("%s[%d]: %s: Query size limit (%ld) exceeded", + myname, recursion, dict_ldap->parser->name, + dict_ldap->size_limit); + dict_ldap->dict.error = DICT_ERR_RETRY; + } + + /* + * Check for terminal attributes, these preclude expansion of all + * other attributes, and DN/URI recursion. Any terminal attributes + * are listed first in the attribute array. + */ + if (dict_ldap->num_terminal > 0) { + for (i = 0; i < dict_ldap->num_terminal; ++i) { + attr = dict_ldap->result_attributes->argv[i]; + if (!(vals = ldap_get_values_len(dict_ldap->ld, entry, attr))) + continue; + is_terminal = (ldap_count_values_len(vals) > 0); + ldap_value_free_len(vals); + if (is_terminal) + break; + } + } + + /* + * Check for special attributes, these preclude expansion of + * "leaf-only" attributes, and are at the end of the attribute array + * after the terminal, leaf and regular attributes. + */ + if (is_terminal == 0 && dict_ldap->num_leaf > 0) { + for (i = dict_ldap->num_attributes; + dict_ldap->result_attributes->argv[i]; ++i) { + attr = dict_ldap->result_attributes->argv[i]; + if (!(vals = ldap_get_values_len(dict_ldap->ld, entry, attr))) + continue; + is_leaf = (ldap_count_values_len(vals) == 0); + ldap_value_free_len(vals); + if (!is_leaf) + break; + } + } + for (attr = ldap_first_attribute(dict_ldap->ld, entry, &ber); + attr != NULL; ldap_memfree(attr), + attr = ldap_next_attribute(dict_ldap->ld, entry, ber)) { + + vals = ldap_get_values_len(dict_ldap->ld, entry, attr); + if (vals == NULL) { + if (msg_verbose) + msg_info("%s[%d]: Entry doesn't have any values for %s", + myname, recursion, attr); + continue; + } + valcount = ldap_count_values_len(vals); + + /* + * If we previously encountered an error, we still continue + * through the loop, to avoid memory leaks, but we don't waste + * time accumulating any further results. + * + * XXX: There may be a more efficient way to exit the loop with no + * leaks, but it will likely be more fragile and not worth the + * extra code. + */ + if (dict_ldap->dict.error != 0 || valcount == 0) { + ldap_value_free_len(vals); + continue; + } + + /* + * The "result_attributes" list enumerates all the requested + * attributes, first the ordinary result attributes and then the + * special result attributes that hold DN or LDAP URL values. + * + * The number of ordinary attributes is "num_attributes". + * + * We compute the attribute type (ordinary or special) from its + * index on the "result_attributes" list. + */ + for (i = 0; dict_ldap->result_attributes->argv[i]; i++) + if (attrdesc_subtype(dict_ldap->result_attributes->argv[i], + attr) > 0) + break; + + /* + * Append each returned address to the result list, possibly + * recursing (for dn or url attributes of non-terminal entries) + */ + if (i < dict_ldap->num_attributes || is_terminal) { + if ((is_terminal && i >= dict_ldap->num_terminal) + || (!is_leaf && + i < dict_ldap->num_terminal + dict_ldap->num_leaf)) { + if (msg_verbose) + msg_info("%s[%d]: skipping %d value(s) of %s " + "attribute %s", myname, recursion, valcount, + is_terminal ? "non-terminal" : "leaf-only", + attr); + } else { + /* Ordinary result attribute */ + for (i = 0; i < valcount; i++) { + if (db_common_expand(dict_ldap->ctx, + dict_ldap->result_format, + vals[i]->bv_val, + name, result, 0) + && dict_ldap->expansion_limit > 0 + && ++expansion > dict_ldap->expansion_limit) { + msg_warn("%s[%d]: %s: Expansion limit exceeded " + "for key: '%s'", myname, recursion, + dict_ldap->parser->name, name); + dict_ldap->dict.error = DICT_ERR_RETRY; + break; + } + } + if (dict_ldap->dict.error != 0) + continue; + if (msg_verbose) + msg_info("%s[%d]: search returned %d value(s) for" + " requested result attribute %s", + myname, recursion, valcount, attr); + } + } else if (recursion < dict_ldap->recursion_limit + && dict_ldap->result_attributes->argv[i]) { + /* Special result attribute */ + for (i = 0; i < valcount; i++) { + if (ldap_is_ldap_url(vals[i]->bv_val)) { + rc = ldap_url_parse(vals[i]->bv_val, &url); + if (rc == 0) { + if ((attrs = url_attrs(dict_ldap, url)) != 0) { + if (msg_verbose) + msg_info("%s[%d]: looking up URL %s", + myname, recursion, + vals[i]->bv_val); + rc = search_st(dict_ldap->ld, url->lud_dn, + url->lud_scope, + url->lud_filter, + attrs, dict_ldap->timeout, + &resloop); + } + ldap_free_urldesc(url); + if (attrs == 0) { + if (msg_verbose) + msg_info("%s[%d]: skipping URL %s: no " + "pertinent attributes", myname, + recursion, vals[i]->bv_val); + continue; + } + } else { + msg_warn("%s[%d]: malformed URL %s: %s(%d)", + myname, recursion, vals[i]->bv_val, + ldap_err2string(rc), rc); + dict_ldap->dict.error = DICT_ERR_RETRY; + break; + } + } else { + if (msg_verbose) + msg_info("%s[%d]: looking up DN %s", + myname, recursion, vals[i]->bv_val); + rc = search_st(dict_ldap->ld, vals[i]->bv_val, + LDAP_SCOPE_BASE, "objectclass=*", + dict_ldap->result_attributes->argv, + dict_ldap->timeout, &resloop); + } + switch (rc) { + case LDAP_SUCCESS: + dict_ldap_get_values(dict_ldap, resloop, result, name); + break; + case LDAP_NO_SUCH_OBJECT: + + /* + * Go ahead and treat this as though the DN existed + * and just didn't have any result attributes. + */ + msg_warn("%s[%d]: DN %s not found, skipping ", myname, + recursion, vals[i]->bv_val); + break; + default: + msg_warn("%s[%d]: search error %d: %s ", myname, + recursion, rc, ldap_err2string(rc)); + dict_ldap->dict.error = DICT_ERR_RETRY; + break; + } + + if (resloop != 0) + ldap_msgfree(resloop); + + if (dict_ldap->dict.error != 0) + break; + } + if (msg_verbose && dict_ldap->dict.error == 0) + msg_info("%s[%d]: search returned %d value(s) for" + " special result attribute %s", + myname, recursion, valcount, attr); + } else if (recursion >= dict_ldap->recursion_limit + && dict_ldap->result_attributes->argv[i]) { + msg_warn("%s[%d]: %s: Recursion limit exceeded" + " for special attribute %s=%s", myname, recursion, + dict_ldap->parser->name, attr, vals[0]->bv_val); + dict_ldap->dict.error = DICT_ERR_RETRY; + } + ldap_value_free_len(vals); + } + if (ber) + ber_free(ber, 0); + } + + if (msg_verbose) + msg_info("%s[%d]: Leaving %s", myname, recursion, myname); + --recursion; +} + +/* dict_ldap_lookup - find database entry */ + +static const char *dict_ldap_lookup(DICT *dict, const char *name) +{ + const char *myname = "dict_ldap_lookup"; + DICT_LDAP *dict_ldap = (DICT_LDAP *) dict; + LDAPMessage *res = 0; + static VSTRING *base; + static VSTRING *query; + static VSTRING *result; + int rc = 0; + int sizelimit; + int domain_rc; + + dict_ldap->dict.error = 0; + + if (msg_verbose) + msg_info("%s: In dict_ldap_lookup", myname); + + /* + * Don't frustrate future attempts to make Postfix UTF-8 transparent. + */ + if ((dict->flags & DICT_FLAG_UTF8_ACTIVE) == 0 + && !valid_utf8_string(name, strlen(name))) { + if (msg_verbose) + msg_info("%s: %s: Skipping lookup of non-UTF-8 key '%s'", + myname, dict_ldap->parser->name, name); + return (0); + } + + /* + * Optionally fold the key. + */ + if (dict->flags & DICT_FLAG_FOLD_FIX) { + if (dict->fold_buf == 0) + dict->fold_buf = vstring_alloc(10); + vstring_strcpy(dict->fold_buf, name); + name = lowercase(vstring_str(dict->fold_buf)); + } + + /* + * If they specified a domain list for this map, then only search for + * addresses in domains on the list. This can significantly reduce the + * load on the LDAP server. + */ + if ((domain_rc = db_common_check_domain(dict_ldap->ctx, name)) == 0) { + if (msg_verbose) + msg_info("%s: %s: Skipping lookup of key '%s': domain mismatch", + myname, dict_ldap->parser->name, name); + return (0); + } + if (domain_rc < 0) + DICT_ERR_VAL_RETURN(dict, domain_rc, (char *) 0); + +#define INIT_VSTR(buf, len) do { \ + if (buf == 0) \ + buf = vstring_alloc(len); \ + VSTRING_RESET(buf); \ + VSTRING_TERMINATE(buf); \ + } while (0) + + INIT_VSTR(base, 10); + INIT_VSTR(query, 10); + INIT_VSTR(result, 10); + + /* + * Because the connection may be shared and invalidated via queries for + * another map, update private copy of "ld" from shared connection + * container. + */ + dict_ldap->ld = DICT_LDAP_CONN(dict_ldap)->conn_ld; + + /* + * Connect to the LDAP server, if necessary. + */ + if (dict_ldap->ld == NULL) { + if (msg_verbose) + msg_info + ("%s: No existing connection for LDAP source %s, reopening", + myname, dict_ldap->parser->name); + + dict_ldap_connect(dict_ldap); + + /* + * if dict_ldap_connect() set dict_ldap->dict.error, abort. + */ + if (dict_ldap->dict.error) + return (0); + } else if (msg_verbose) + msg_info("%s: Using existing connection for LDAP source %s", + myname, dict_ldap->parser->name); + + /* + * Connection caching, means that the connection handle may have the + * wrong size limit. Re-adjust before each query. This is cheap, just + * sets a field in the ldap connection handle. We also do this in the + * connect code, because we sometimes reconnect (below) in the middle of + * a query. + */ + sizelimit = dict_ldap->size_limit ? dict_ldap->size_limit : LDAP_NO_LIMIT; + if (ldap_set_option(dict_ldap->ld, LDAP_OPT_SIZELIMIT, &sizelimit) + != LDAP_OPT_SUCCESS) { + msg_warn("%s: %s: Unable to set query result size limit to %ld.", + myname, dict_ldap->parser->name, dict_ldap->size_limit); + dict_ldap->dict.error = DICT_ERR_RETRY; + return (0); + } + + /* + * Expand the search base and query. Skip lookup when the input key lacks + * sufficient domain components to satisfy all the requested + * %-substitutions. + * + * When the search base is not static, LDAP_NO_SUCH_OBJECT is expected and + * is therefore treated as a non-error: the lookup returns no results + * rather than a soft error. + */ + if (!db_common_expand(dict_ldap->ctx, dict_ldap->search_base, + name, 0, base, rfc2253_quote)) { + if (msg_verbose > 1) + msg_info("%s: %s: Empty expansion for %s", myname, + dict_ldap->parser->name, dict_ldap->search_base); + return (0); + } + if (!db_common_expand(dict_ldap->ctx, dict_ldap->query, + name, 0, query, rfc2254_quote)) { + if (msg_verbose > 1) + msg_info("%s: %s: Empty expansion for %s", myname, + dict_ldap->parser->name, dict_ldap->query); + return (0); + } + + /* + * On to the search. + */ + if (msg_verbose) + msg_info("%s: %s: Searching with filter %s", myname, + dict_ldap->parser->name, vstring_str(query)); + + rc = search_st(dict_ldap->ld, vstring_str(base), dict_ldap->scope, + vstring_str(query), dict_ldap->result_attributes->argv, + dict_ldap->timeout, &res); + + if (rc == LDAP_SERVER_DOWN) { + if (msg_verbose) + msg_info("%s: Lost connection for LDAP source %s, reopening", + myname, dict_ldap->parser->name); + + dict_ldap_unbind(dict_ldap->ld); + dict_ldap->ld = DICT_LDAP_CONN(dict_ldap)->conn_ld = 0; + dict_ldap_connect(dict_ldap); + + /* + * if dict_ldap_connect() set dict_ldap->dict.error, abort. + */ + if (dict_ldap->dict.error) + return (0); + + rc = search_st(dict_ldap->ld, vstring_str(base), dict_ldap->scope, + vstring_str(query), dict_ldap->result_attributes->argv, + dict_ldap->timeout, &res); + + } + switch (rc) { + + case LDAP_SUCCESS: + + /* + * Search worked; extract the requested result_attribute. + */ + + dict_ldap_get_values(dict_ldap, res, result, name); + + /* + * OpenLDAP's ldap_next_attribute returns a bogus + * LDAP_DECODING_ERROR; I'm ignoring that for now. + */ + + rc = dict_ldap_get_errno(dict_ldap->ld); + if (rc != LDAP_SUCCESS && rc != LDAP_DECODING_ERROR) + msg_warn + ("%s: Had some trouble with entries returned by search: %s", + myname, ldap_err2string(rc)); + + if (msg_verbose) + msg_info("%s: Search returned %s", myname, + VSTRING_LEN(result) > + 0 ? vstring_str(result) : "nothing"); + break; + + case LDAP_NO_SUCH_OBJECT: + + /* + * If the search base is input key dependent, then not finding it, is + * equivalent to not finding the input key. Sadly, we cannot detect + * misconfiguration in this case. + */ + if (dict_ldap->dynamic_base) + break; + + msg_warn("%s: %s: Search base '%s' not found: %d: %s", + myname, dict_ldap->parser->name, + vstring_str(base), rc, ldap_err2string(rc)); + dict_ldap->dict.error = DICT_ERR_RETRY; + break; + + default: + + /* + * Rats. The search didn't work. + */ + msg_warn("%s: Search error %d: %s ", myname, rc, + ldap_err2string(rc)); + + /* + * Tear down the connection so it gets set up from scratch on the + * next lookup. + */ + dict_ldap_unbind(dict_ldap->ld); + dict_ldap->ld = DICT_LDAP_CONN(dict_ldap)->conn_ld = 0; + + /* + * And tell the caller to try again later. + */ + dict_ldap->dict.error = DICT_ERR_RETRY; + break; + } + + /* + * Cleanup. + */ + if (res != 0) + ldap_msgfree(res); + + /* + * If we had an error, return nothing, Otherwise, return the result, if + * any. + */ + return (VSTRING_LEN(result) > 0 && !dict_ldap->dict.error ? vstring_str(result) : 0); +} + +/* dict_ldap_close - disassociate from data base */ + +static void dict_ldap_close(DICT *dict) +{ + const char *myname = "dict_ldap_close"; + DICT_LDAP *dict_ldap = (DICT_LDAP *) dict; + LDAP_CONN *conn = DICT_LDAP_CONN(dict_ldap); + BINHASH_INFO *ht = dict_ldap->ht; + + if (--conn->conn_refcount == 0) { + if (conn->conn_ld) { + if (msg_verbose) + msg_info("%s: Closed connection handle for LDAP source %s", + myname, dict_ldap->parser->name); + dict_ldap_unbind(conn->conn_ld); + } + binhash_delete(conn_hash, ht->key, ht->key_len, myfree); + } + cfg_parser_free(dict_ldap->parser); + myfree(dict_ldap->server_host); + myfree(dict_ldap->search_base); + myfree(dict_ldap->query); + if (dict_ldap->result_format) + myfree(dict_ldap->result_format); + argv_free(dict_ldap->result_attributes); + myfree(dict_ldap->bind_dn); + myfree(dict_ldap->bind_pw); + if (dict_ldap->ctx) + db_common_free_ctx(dict_ldap->ctx); +#ifdef LDAP_API_FEATURE_X_OPENLDAP +#if defined(USE_LDAP_SASL) + if (DICT_LDAP_DO_SASL(dict_ldap)) { + myfree(dict_ldap->sasl_mechs); + myfree(dict_ldap->sasl_realm); + myfree(dict_ldap->sasl_authz); + } +#endif + myfree(dict_ldap->tls_ca_cert_file); + myfree(dict_ldap->tls_ca_cert_dir); + myfree(dict_ldap->tls_cert); + myfree(dict_ldap->tls_key); + myfree(dict_ldap->tls_random_file); + myfree(dict_ldap->tls_cipher_suite); +#endif + if (dict->fold_buf) + vstring_free(dict->fold_buf); + dict_free(dict); +} + +/* dict_ldap_open - create association with data base */ + +DICT *dict_ldap_open(const char *ldapsource, int open_flags, int dict_flags) +{ + const char *myname = "dict_ldap_open"; + DICT_LDAP *dict_ldap; + VSTRING *url_list; + char *s; + char *h; + char *server_host; + char *scope; + char *attr; + char *bindopt; + int tmp; + int vendor_version = dict_ldap_vendor_version(); + CFG_PARSER *parser; + + if (msg_verbose) + msg_info("%s: Using LDAP source %s", myname, ldapsource); + + /* + * Sanity check. + */ + if (open_flags != O_RDONLY) + return (dict_surrogate(DICT_TYPE_LDAP, ldapsource, open_flags, dict_flags, + "%s:%s map requires O_RDONLY access mode", + DICT_TYPE_LDAP, ldapsource)); + + /* + * Open the configuration file. + */ + if ((parser = cfg_parser_alloc(ldapsource)) == 0) + return (dict_surrogate(DICT_TYPE_LDAP, ldapsource, open_flags, dict_flags, + "open %s: %m", ldapsource)); + + dict_ldap = (DICT_LDAP *) dict_alloc(DICT_TYPE_LDAP, ldapsource, + sizeof(*dict_ldap)); + dict_ldap->dict.lookup = dict_ldap_lookup; + dict_ldap->dict.close = dict_ldap_close; + dict_ldap->dict.flags = dict_flags; + + dict_ldap->ld = NULL; + dict_ldap->parser = parser; + + server_host = cfg_get_str(dict_ldap->parser, "server_host", + "localhost", 1, 0); + + /* + * get configured value of "server_port"; default to LDAP_PORT (389) + */ + dict_ldap->server_port = + cfg_get_int(dict_ldap->parser, "server_port", LDAP_PORT, 0, 0); + + /* + * Define LDAP Protocol Version. + */ + dict_ldap->version = cfg_get_int(dict_ldap->parser, "version", 2, 2, 0); + switch (dict_ldap->version) { + case 2: + dict_ldap->version = LDAP_VERSION2; + break; + case 3: + dict_ldap->version = LDAP_VERSION3; + break; + default: + msg_warn("%s: %s Unknown version %d, using 2.", myname, ldapsource, + dict_ldap->version); + dict_ldap->version = LDAP_VERSION2; + } + +#if defined(LDAP_API_FEATURE_X_OPENLDAP) + dict_ldap->ldap_ssl = 0; +#endif + + url_list = vstring_alloc(32); + s = server_host; + while ((h = mystrtok(&s, CHARS_COMMA_SP)) != NULL) { +#if defined(LDAP_API_FEATURE_X_OPENLDAP) + + /* + * Convert (host, port) pairs to LDAP URLs + */ + if (ldap_is_ldap_url(h)) { + LDAPURLDesc *url_desc; + int rc; + + if ((rc = ldap_url_parse(h, &url_desc)) != 0) { + msg_error("%s: error parsing URL %s: %d: %s; skipping", myname, + h, rc, ldap_err2string(rc)); + continue; + } + if (strcasecmp(url_desc->lud_scheme, "ldap") != 0 && + dict_ldap->version != LDAP_VERSION3) { + msg_warn("%s: URL scheme %s requires protocol version 3", myname, + url_desc->lud_scheme); + dict_ldap->version = LDAP_VERSION3; + } + if (strcasecmp(url_desc->lud_scheme, "ldaps") == 0) + dict_ldap->ldap_ssl = 1; + ldap_free_urldesc(url_desc); + if (VSTRING_LEN(url_list) > 0) + VSTRING_ADDCH(url_list, ' '); + vstring_strcat(url_list, h); + } else { + if (VSTRING_LEN(url_list) > 0) + VSTRING_ADDCH(url_list, ' '); + if (strrchr(h, ':')) + vstring_sprintf_append(url_list, "ldap://%s", h); + else + vstring_sprintf_append(url_list, "ldap://%s:%d", h, + dict_ldap->server_port); + } +#else + if (VSTRING_LEN(url_list) > 0) + VSTRING_ADDCH(url_list, ' '); + vstring_strcat(url_list, h); +#endif + } + VSTRING_TERMINATE(url_list); + dict_ldap->server_host = vstring_export(url_list); + +#if defined(LDAP_API_FEATURE_X_OPENLDAP) + + /* + * With URL scheme, clear port to normalize connection cache key + */ + dict_ldap->server_port = LDAP_PORT; + if (msg_verbose) + msg_info("%s: %s server_host URL is %s", myname, ldapsource, + dict_ldap->server_host); +#endif + myfree(server_host); + + /* + * Scope handling thanks to Carsten Hoeger of SuSE. + */ + scope = cfg_get_str(dict_ldap->parser, "scope", "sub", 1, 0); + + if (strcasecmp(scope, "one") == 0) { + dict_ldap->scope = LDAP_SCOPE_ONELEVEL; + } else if (strcasecmp(scope, "base") == 0) { + dict_ldap->scope = LDAP_SCOPE_BASE; + } else if (strcasecmp(scope, "sub") == 0) { + dict_ldap->scope = LDAP_SCOPE_SUBTREE; + } else { + msg_warn("%s: %s: Unrecognized value %s specified for scope; using sub", + myname, ldapsource, scope); + dict_ldap->scope = LDAP_SCOPE_SUBTREE; + } + + myfree(scope); + + dict_ldap->search_base = cfg_get_str(dict_ldap->parser, "search_base", + "", 0, 0); + + /* + * get configured value of "timeout"; default to 10 seconds + * + * Thanks to Manuel Guesdon for spotting that this wasn't really getting + * set. + */ + dict_ldap->timeout = cfg_get_int(dict_ldap->parser, "timeout", 10, 0, 0); + dict_ldap->query = + cfg_get_str(dict_ldap->parser, "query_filter", + "(mailacceptinggeneralid=%s)", 0, 0); + if ((dict_ldap->result_format = + cfg_get_str(dict_ldap->parser, "result_format", 0, 0, 0)) == 0) + dict_ldap->result_format = + cfg_get_str(dict_ldap->parser, "result_filter", "%s", 1, 0); + + /* + * Must parse all templates before we can use db_common_expand() If data + * dependent substitutions are found in the search base, treat + * NO_SUCH_OBJECT search errors as a non-matching key, rather than a + * fatal run-time error. + */ + dict_ldap->ctx = 0; + dict_ldap->dynamic_base = + db_common_parse(&dict_ldap->dict, &dict_ldap->ctx, + dict_ldap->search_base, 1); + if (!db_common_parse(0, &dict_ldap->ctx, dict_ldap->query, 1)) { + msg_warn("%s: %s: Fixed query_filter %s is probably useless", + myname, ldapsource, dict_ldap->query); + } + (void) db_common_parse(0, &dict_ldap->ctx, dict_ldap->result_format, 0); + db_common_parse_domain(dict_ldap->parser, dict_ldap->ctx); + + /* + * Maps that use substring keys should only be used with the full input + * key. + */ + if (db_common_dict_partial(dict_ldap->ctx)) + dict_ldap->dict.flags |= DICT_FLAG_PATTERN; + else + dict_ldap->dict.flags |= DICT_FLAG_FIXED; + if (dict_flags & DICT_FLAG_FOLD_FIX) + dict_ldap->dict.fold_buf = vstring_alloc(10); + + /* Order matters, first the terminal attributes: */ + attr = cfg_get_str(dict_ldap->parser, "terminal_result_attribute", "", 0, 0); + dict_ldap->result_attributes = argv_split(attr, CHARS_COMMA_SP); + dict_ldap->num_terminal = dict_ldap->result_attributes->argc; + myfree(attr); + + /* Order matters, next the leaf-only attributes: */ + attr = cfg_get_str(dict_ldap->parser, "leaf_result_attribute", "", 0, 0); + if (*attr) + argv_split_append(dict_ldap->result_attributes, attr, CHARS_COMMA_SP); + dict_ldap->num_leaf = + dict_ldap->result_attributes->argc - dict_ldap->num_terminal; + myfree(attr); + + /* Order matters, next the regular attributes: */ + attr = cfg_get_str(dict_ldap->parser, "result_attribute", "maildrop", 0, 0); + if (*attr) + argv_split_append(dict_ldap->result_attributes, attr, CHARS_COMMA_SP); + dict_ldap->num_attributes = dict_ldap->result_attributes->argc; + myfree(attr); + + /* Order matters, finally the special attributes: */ + attr = cfg_get_str(dict_ldap->parser, "special_result_attribute", "", 0, 0); + if (*attr) + argv_split_append(dict_ldap->result_attributes, attr, CHARS_COMMA_SP); + myfree(attr); + + /* + * get configured value of "bind"; default to simple bind + */ + bindopt = cfg_get_str(dict_ldap->parser, "bind", CONFIG_BOOL_YES, 1, 0); + dict_ldap->bind = name_code(bindopt_table, NAME_CODE_FLAG_NONE, bindopt); + if (dict_ldap->bind < 0) + msg_fatal("%s: unsupported parameter value: %s = %s", + dict_ldap->parser->name, "bind", bindopt); + myfree(bindopt); + + /* + * get configured value of "bind_dn"; default to "" + */ + dict_ldap->bind_dn = cfg_get_str(dict_ldap->parser, "bind_dn", "", 0, 0); + + /* + * get configured value of "bind_pw"; default to "" + */ + dict_ldap->bind_pw = cfg_get_str(dict_ldap->parser, "bind_pw", "", 0, 0); + + /* + * LDAP message caching never worked and is no longer supported. + */ + tmp = cfg_get_bool(dict_ldap->parser, "cache", 0); + if (tmp) + msg_warn("%s: %s ignoring cache", myname, ldapsource); + + tmp = cfg_get_int(dict_ldap->parser, "cache_expiry", -1, 0, 0); + if (tmp >= 0) + msg_warn("%s: %s ignoring cache_expiry", myname, ldapsource); + + tmp = cfg_get_int(dict_ldap->parser, "cache_size", -1, 0, 0); + if (tmp >= 0) + msg_warn("%s: %s ignoring cache_size", myname, ldapsource); + + dict_ldap->recursion_limit = cfg_get_int(dict_ldap->parser, + "recursion_limit", 1000, 1, 0); + + /* + * XXX: The default should be non-zero for safety, but that is not + * backwards compatible. + */ + dict_ldap->expansion_limit = cfg_get_int(dict_ldap->parser, + "expansion_limit", 0, 0, 0); + + dict_ldap->size_limit = cfg_get_int(dict_ldap->parser, "size_limit", + dict_ldap->expansion_limit, 0, 0); + + /* + * Alias dereferencing suggested by Mike Mattice. + */ + dict_ldap->dereference = cfg_get_int(dict_ldap->parser, "dereference", + 0, 0, 0); + if (dict_ldap->dereference < 0 || dict_ldap->dereference > 3) { + msg_warn("%s: %s Unrecognized value %d specified for dereference; using 0", + myname, ldapsource, dict_ldap->dereference); + dict_ldap->dereference = 0; + } + /* Referral chasing */ + dict_ldap->chase_referrals = cfg_get_bool(dict_ldap->parser, + "chase_referrals", 0); + +#ifdef LDAP_API_FEATURE_X_OPENLDAP +#if defined(USE_LDAP_SASL) + + /* + * SASL options + */ + if (DICT_LDAP_DO_SASL(dict_ldap)) { + dict_ldap->sasl_mechs = + cfg_get_str(dict_ldap->parser, "sasl_mechs", "", 0, 0); + dict_ldap->sasl_realm = + cfg_get_str(dict_ldap->parser, "sasl_realm", "", 0, 0); + dict_ldap->sasl_authz = + cfg_get_str(dict_ldap->parser, "sasl_authz_id", "", 0, 0); + dict_ldap->sasl_minssf = + cfg_get_int(dict_ldap->parser, "sasl_minssf", 0, 0, 4096); + } else { + dict_ldap->sasl_mechs = 0; + dict_ldap->sasl_realm = 0; + dict_ldap->sasl_authz = 0; + } +#endif + + /* + * TLS options + */ + /* get configured value of "start_tls"; default to no */ + dict_ldap->start_tls = cfg_get_bool(dict_ldap->parser, "start_tls", 0); + if (dict_ldap->start_tls) { + if (dict_ldap->version < LDAP_VERSION3) { + msg_warn("%s: %s start_tls requires protocol version 3", + myname, ldapsource); + dict_ldap->version = LDAP_VERSION3; + } + /* Binary incompatibility in the OpenLDAP API from 2.0.11 to 2.0.12 */ + if (((LDAP_VENDOR_VERSION <= 20011) && !(vendor_version <= 20011)) + || (!(LDAP_VENDOR_VERSION <= 20011) && (vendor_version <= 20011))) + msg_fatal("%s: incompatible TLS support: " + "compile-time OpenLDAP version %d, " + "run-time OpenLDAP version %d", + myname, LDAP_VENDOR_VERSION, vendor_version); + } + /* get configured value of "tls_require_cert"; default to no */ + dict_ldap->tls_require_cert = + cfg_get_bool(dict_ldap->parser, "tls_require_cert", 0) ? + LDAP_OPT_X_TLS_DEMAND : LDAP_OPT_X_TLS_NEVER; + + /* get configured value of "tls_ca_cert_file"; default "" */ + dict_ldap->tls_ca_cert_file = cfg_get_str(dict_ldap->parser, + "tls_ca_cert_file", "", 0, 0); + + /* get configured value of "tls_ca_cert_dir"; default "" */ + dict_ldap->tls_ca_cert_dir = cfg_get_str(dict_ldap->parser, + "tls_ca_cert_dir", "", 0, 0); + + /* get configured value of "tls_cert"; default "" */ + dict_ldap->tls_cert = cfg_get_str(dict_ldap->parser, "tls_cert", + "", 0, 0); + + /* get configured value of "tls_key"; default "" */ + dict_ldap->tls_key = cfg_get_str(dict_ldap->parser, "tls_key", + "", 0, 0); + + /* get configured value of "tls_random_file"; default "" */ + dict_ldap->tls_random_file = cfg_get_str(dict_ldap->parser, + "tls_random_file", "", 0, 0); + + /* get configured value of "tls_cipher_suite"; default "" */ + dict_ldap->tls_cipher_suite = cfg_get_str(dict_ldap->parser, + "tls_cipher_suite", "", 0, 0); +#endif + + /* + * Debug level. + */ +#if defined(LDAP_OPT_DEBUG_LEVEL) && defined(LBER_OPT_LOG_PRINT_FN) + dict_ldap->debuglevel = cfg_get_int(dict_ldap->parser, "debuglevel", + 0, 0, 0); +#endif + + /* + * Find or allocate shared LDAP connection container. + */ + dict_ldap_conn_find(dict_ldap); + + /* + * Return the new dict_ldap structure. + */ + dict_ldap->dict.owner = cfg_get_owner(dict_ldap->parser); + return (DICT_DEBUG (&dict_ldap->dict)); +} + +#endif |