/*++ /* NAME /* dict_ldap 3 /* SUMMARY /* dictionary manager interface to LDAP maps /* SYNOPSIS /* #include /* /* 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 #include #include #include #include #include #include #include #include #include #ifdef STRCASECMP_IN_STRINGS_H #include #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 #include #include #include #include #include #include /* 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 #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