/* TODO: Include attribution for ideas, and code from mod_auth_digest */ #define _XOPEN_SOURCE #include "usuals.h" #include "httpauthd.h" #include "hash.h" #include "defaults.h" #include "digest.h" #include "basic.h" #include "md5.h" #include "sha1.h" #include #include #include /* LDAP library */ #include unsigned char g_ldap_secret[DIGEST_SECRET_LEN]; /* ------------------------------------------------------------------------------- * Defaults and Constants */ #define BASIC_ESTABLISHED (void*)1 /* TODO: We need to support more password types */ #define LDAP_PW_CLEAR 0 #define LDAP_PW_CRYPT 1 #define LDAP_PW_MD5 2 #define LDAP_PW_SHA 3 #define LDAP_PW_UNKNOWN -1 typedef struct ldap_pw_type { const char* name; int type; } ldap_pw_type_t; static const ldap_pw_type_t kLDAPPWTypes[] = { { "cleartext", LDAP_PW_CLEAR }, { "crypt", LDAP_PW_CRYPT }, { "md5", LDAP_PW_MD5 }, { "sha", LDAP_PW_SHA } }; /* ------------------------------------------------------------------------------- * Structures */ /* Our hanler context */ typedef struct ldap_context { /* Settings ---------------------------------------------------------- */ const char* servers; /* Servers to authenticate against (required) */ const char* filter; /* Filter (either this or dnmap must be set) */ const char* base; /* Base for the filter */ const char* pw_attr; /* The clear password attribute */ const char* ha1_attr; /* Password for an encrypted Digest H(A1) */ const char* user; /* User to bind as */ const char* password; /* Password to bind with */ const char* dnmap; /* For mapping users to dns */ int port; /* Port to connect to LDAP server on */ int scope; /* Scope for filter */ int dobind; /* Bind to do simple authentication */ int ldap_max; /* Number of open connections allowed */ int ldap_timeout; /* Maximum amount of time to dedicate to an ldap query */ /* Context ----------------------------------------------------------- */ hsh_t* cache; /* Some cached records or basic */ LDAP** pool; /* Pool of available connections */ int pool_mark; /* Amount of connections allocated */ } ldap_context_t; /* The defaults for the context */ static const ldap_context_t ldap_defaults = { NULL, /* servers */ NULL, /* filter */ NULL, /* base */ "userPassword", /* pw_attr */ NULL, /* ha1_attr */ NULL, /* user */ NULL, /* password */ NULL, /* dnmap */ 389, /* port */ LDAP_SCOPE_SUBTREE, /* scope */ 1, /* dobind */ 10, /* ldap_max */ 30, /* ldap_timeout */ NULL, /* cache */ NULL, /* pool */ 0 /* pool_mark */ }; /* ------------------------------------------------------------------------------- * Internal Functions */ static void free_hash_object(void* arg, void* val) { if(val && val != BASIC_ESTABLISHED) free(val); } static int report_ldap(const char* msg, int code) { ASSERT(code != LDAP_SUCCESS); switch(code) { case LDAP_NO_MEMORY: ha_messagex(LOG_CRIT, "out of memory"); return HA_CRITERROR; default: if(!msg) msg = "error"; ha_messagex(LOG_ERR, "ldap: %s: %s", msg, ldap_err2string(code)); return HA_FAILED; }; } static digest_record_t* get_cached_digest(ldap_context_t* ctx, ha_context_t* c, unsigned char* nonce) { digest_record_t* rec; ASSERT(ctx && c && nonce); if(c->cache_max == 0) return NULL; ha_lock(NULL); rec = (digest_record_t*)hsh_get(ctx->cache, nonce); /* Just in case it's a basic :) */ if(rec && rec != BASIC_ESTABLISHED) hsh_rem(ctx->cache, nonce); ha_unlock(NULL); ASSERT(!rec || memcmp(nonce, rec->nonce, DIGEST_NONCE_LEN) == 0); return rec; } static int have_cached_basic(ldap_context_t* ctx, unsigned char* key) { int ret = 0; ASSERT(ctx && key); ha_lock(NULL); ret = (hsh_get(ctx->cache, key) == BASIC_ESTABLISHED); ha_unlock(NULL); return ret; } static int save_cached_digest(ldap_context_t* ctx, ha_context_t* c, digest_record_t* rec) { int r; ASSERT(ctx && rec); if(c->cache_max == 0) { free_hash_object(NULL, rec); return HA_FALSE; } ha_lock(NULL); while(hsh_count(ctx->cache) >= c->cache_max) hsh_bump(ctx->cache); r = hsh_set(ctx->cache, rec->nonce, rec); ha_unlock(NULL); if(!r) { ha_messagex(LOG_CRIT, "out of memory"); return HA_CRITERROR; } return HA_OK; } static int add_cached_basic(ldap_context_t* ctx, ha_context_t* c, unsigned char* key) { int r; ASSERT(ctx && c && key); if(c->cache_max == 0) return HA_FALSE; ha_lock(NULL); while(hsh_count(ctx->cache) >= c->cache_max) hsh_bump(ctx->cache); r = hsh_set(ctx->cache, key, BASIC_ESTABLISHED); ha_unlock(NULL); if(!r) { ha_messagex(LOG_CRIT, "out of memory"); return HA_CRITERROR; } return HA_OK; } #define LDAP_NO_ESCAPE "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.-_" #define LDAP_HEX "0123456789abcdef" static const char* escape_ldap(ha_buffer_t* buf, const char* str) { const char* t = str; size_t pos; ha_bufcpy(buf, ""); while(*t) { pos = strspn(t, LDAP_NO_ESCAPE); if(pos > 0) { ha_bufjoin(buf); ha_bufncpy(buf, t, pos); t += pos; } while(*t && !strchr(LDAP_NO_ESCAPE, *t)) { char hex[4]; hex[0] = '\\'; hex[1] = LDAP_HEX[*t >> 4 & 0xf]; hex[2] = LDAP_HEX[*t & 0xf]; hex[3] = '\0'; ha_bufjoin(buf); ha_bufcpy(buf, hex); t++; } } return ha_bufdata(buf); } static const char* substitute_params(ldap_context_t* ctx, const ha_request_t* req, const char* user, const char* str) { const char* t; ASSERT(ctx && req && user && str); /* TODO: We need to be escaping the user and realm properly */ /* This starts a new block to join */ ha_bufcpy(req->buf, ""); while(str[0]) { t = strchr(str, '%'); if(!t) { ha_bufjoin(req->buf); ha_bufcpy(req->buf, str); break; } ha_bufjoin(req->buf); ha_bufncpy(req->buf, str, t - str); t++; switch(t[0]) { case 'u': ha_bufjoin(req->buf); escape_ldap(req->buf, user); t++; break; case 'r': ha_bufjoin(req->buf); escape_ldap(req->buf, req->context->realm); t++; break; case '%': ha_bufjoin(req->buf); ha_bufcpy(req->buf, "%"); t++; break; }; str = t; } return ha_bufdata(req->buf); } static const char* make_password_md5(ha_buffer_t* buf, const char* clearpw) { md5_ctx_t md5; unsigned char digest[MD5_LEN]; ASSERT(buf && clearpw); md5_init(&md5); md5_update(&md5, clearpw, strlen(clearpw)); md5_final(digest, &md5); return ha_bufenc64(buf, digest, MD5_LEN); } static const char* make_password_sha(ha_buffer_t* buf, const char* clearpw) { sha1_ctx_t sha; unsigned char digest[SHA1_LEN]; ASSERT(buf && clearpw); sha1_init(&sha); sha1_update(&sha, clearpw, strlen(clearpw)); sha1_final(digest, &sha); return ha_bufenc64(buf, digest, SHA1_LEN); } static int parse_ldap_password(const char** password) { const char* pw; const char* scheme; int i; ASSERT(password && *password); pw = *password; /* zero length passwords are clear */ if(strlen(pw) == 0) return LDAP_PW_CLEAR; /* passwords without a scheme are clear */ if(pw[0] != '{') return LDAP_PW_CLEAR; pw++; scheme = pw; while(*pw && (isalpha(*pw) || isdigit(*pw) || *pw == '-')) pw++; /* scheme should end in a brace */ if(*pw != '}') return LDAP_PW_CLEAR; *password = pw + 1; /* find a scheme in our map */ for(i = 0; i < countof(kLDAPPWTypes); i++) { if(strncasecmp(kLDAPPWTypes[i].name, scheme, pw - scheme) == 0) return kLDAPPWTypes[i].type; } return LDAP_PW_UNKNOWN; } static const char* find_cleartext_password(ha_buffer_t* buf, const char** pws) { ASSERT(buf); for(; pws && *pws; pws++) { const char* pw = *pws; if(parse_ldap_password(&pw) == LDAP_PW_CLEAR) return pw; } return NULL; } static int parse_ldap_ha1(ha_buffer_t* buf, struct berval* bv, unsigned char* ha1) { size_t len; void* d; ASSERT(buf && bv && ha1); /* Raw binary */ if(bv->bv_len == MD5_LEN) { ha_messagex(LOG_DEBUG, "ldap: found ha1 in raw binary format"); memcpy(ha1, bv->bv_val, MD5_LEN); return HA_OK; } /* Hex encoded */ else if(bv->bv_len == (MD5_LEN * 2)) { len = MD5_LEN; d = ha_bufdechex(buf, bv->bv_val, &len); if(d && len == MD5_LEN) { ha_messagex(LOG_DEBUG, "ldap: found ha1 in hex encoded format"); memcpy(ha1, d, MD5_LEN); return HA_OK; } } /* B64 Encoded */ else { len = MD5_LEN; d = ha_bufdec64(buf, bv->bv_val, &len); if(d && len == MD5_LEN) { ha_messagex(LOG_DEBUG, "ldap: found ha1 in b64 encoded format"); memcpy(ha1, ha_bufdata(buf), MD5_LEN); return HA_OK; } } return ha_buferr(buf) ? HA_CRITERROR : HA_FALSE; } static int validate_ldap_password(ldap_context_t* ctx, LDAP* ld, LDAPMessage* entry, ha_buffer_t* buf, const char* user, const char* clearpw) { char** pws; char** t; const char* pw; const char* p; int type; int res = HA_FALSE; int unknown = 0; ASSERT(entry && ld && ctx && clearpw); ASSERT(ctx->pw_attr); pws = ldap_get_values(ld, entry, ctx->pw_attr); if(pws) { for(t = pws ; *t; t++) { pw = *t; type = parse_ldap_password(&pw); switch(type) { case LDAP_PW_CLEAR: p = clearpw; break; case LDAP_PW_MD5: p = make_password_md5(buf, clearpw); break; case LDAP_PW_CRYPT: /* Not sure if crypt is thread safe */ ha_lock(NULL); p = crypt(clearpw, pw); ha_unlock(NULL); break; case LDAP_PW_SHA: p = make_password_sha(buf, clearpw); break; case LDAP_PW_UNKNOWN: unknown = 1; continue; default: /* Not reached */ ASSERT(0); }; if(!p) { res = HA_CRITERROR; break; } if(strcmp(pw, p) == 0) { ha_messagex(LOG_DEBUG, "ldap: successful validate against password"); res = HA_OK; break; } } ldap_value_free(pws); } if(res == HA_FALSE && unknown) ha_messagex(LOG_ERR, "ldap: server does not contain any compatible passwords for user: %s", user); return res; } static int validate_ldap_ha1(ldap_context_t* ctx, LDAP* ld, LDAPMessage* entry, ha_buffer_t* buf, const char* user, const char* realm, const char* clearpw) { struct berval** ha1s; struct berval** b; unsigned char key[MD5_LEN]; unsigned char k[MD5_LEN]; int r, first = 1; int res = HA_FALSE; ASSERT(ctx && ld && entry && buf && user && clearpw); if(!ctx->ha1_attr) return HA_FALSE; ha1s = ldap_get_values_len(ld, entry, ctx->ha1_attr); if(ha1s) { digest_makeha1(key, user, realm, clearpw); for(b = ha1s ; *b; b++) { r = parse_ldap_ha1(buf, *b, k); if(r < 0) { res = r; break; } if(r == HA_FALSE) { if(first) ha_messagex(LOG_ERR, "ldap: server contains invalid HA1 digest hash for user: %s", user); first = 0; continue; } if(memcmp(key, k, MD5_LEN) == 0) { ha_messagex(LOG_DEBUG, "ldap: successful validate against ha1"); res = HA_OK; break; } } ldap_value_free_len(ha1s); } return res; } static LDAP* get_ldap_connection(ldap_context_t* ctx) { LDAP* ld; int i, r; ASSERT(ctx); for(i = 0; i < ctx->ldap_max; i++) { /* An open connection in the pool */ if(ctx->pool[i]) { ha_messagex(LOG_DEBUG, "ldap: using cached connection"); ld = ctx->pool[i]; ctx->pool[i] = NULL; return ld; } } if(ctx->pool_mark >= ctx->ldap_max) { ha_messagex(LOG_ERR, "ldap: too many open connections"); return NULL; } ld = ldap_init(ctx->servers, ctx->port); if(!ld) { ha_message(LOG_ERR, "ldap: couldn't initialize connection"); return NULL; } if(ctx->user || ctx->password) { r = ldap_simple_bind_s(ld, ctx->user ? ctx->user : "", ctx->password ? ctx->password : ""); if(r != LDAP_SUCCESS) { report_ldap("ldap: couldn't bind to LDAP server", r); ldap_unbind_s(ld); return NULL; } } ctx->pool_mark++; ha_messagex(LOG_DEBUG, "ldap: opened new connection (total %d)", ctx->pool_mark); return ld; } static void discard_ldap_connection(ldap_context_t* ctx, LDAP* ld) { ldap_unbind_s(ld); ctx->pool_mark--; ha_messagex(LOG_DEBUG, "ldap: discarding connection (total %d)", ctx->pool_mark); } static void save_ldap_connection(ldap_context_t* ctx, LDAP* ld) { int i, e; ASSERT(ctx); if(!ld) return; ldap_get_option(ld, LDAP_OPT_ERROR_NUMBER, &e); /* Make sure it's worth saving */ switch(e) { case LDAP_SERVER_DOWN: case LDAP_LOCAL_ERROR: case LDAP_NO_MEMORY: discard_ldap_connection(ctx, ld); break; default: for(i = 0; i < ctx->ldap_max; i++) { /* An open connection in the pool */ if(!ctx->pool[i]) { ha_messagex(LOG_DEBUG, "ldap: caching connection for later use"); ctx->pool[i] = ld; ld = NULL; break; } } break; }; } static int retrieve_user_entry(ldap_context_t* ctx, const ha_request_t* req, LDAP* ld, const char* user, const char** dn, LDAPMessage** entry, LDAPMessage** result) { struct timeval tv; const char* filter; const char* attrs[3]; const char* base; int scope; int r; ASSERT(ctx && req && ld && user && dn && entry && result); if(ctx->filter) { /* Filters can also have %u and %r */ filter = substitute_params(ctx, req, user, ctx->filter); if(!filter) return HA_CRITERROR; } else { filter = "(objectClass=*)"; } attrs[0] = ctx->dobind ? NULL : ctx->pw_attr; attrs[1] = ctx->dobind ? NULL : ctx->ha1_attr; attrs[2] = NULL; tv.tv_sec = ctx->ldap_timeout; tv.tv_usec = 0; base = *dn ? *dn : ctx->base; scope = *dn ? LDAP_SCOPE_BASE : ctx->scope; ha_messagex(LOG_DEBUG, "ldap: performing search: [ base: %s / scope: %d / filter: %s ]", base, scope, filter); r = ldap_search_st(ld, base, scope, filter, (char**)attrs, 0, &tv, result); if(r != LDAP_SUCCESS) { if(r == LDAP_NO_SUCH_OBJECT) { ha_messagex(LOG_WARNING, "ldap: user not found in LDAP: %s", user); return HA_FALSE; } return report_ldap("couldn't search LDAP server", r); } /* Only one result should exist */ switch(r = ldap_count_entries(ld, *result)) { case 1: *entry = ldap_first_entry(ld, *result); if(!(*dn)) { *dn = ldap_get_dn(ld, *entry); ha_messagex(LOG_DEBUG, "ldap: found entry for user: %s", *dn); } return HA_OK; case 0: ha_messagex(LOG_WARNING, "ldap: user not found in LDAP: %s", user); break; default: ha_messagex(LOG_WARNING, "ldap: more than one user found for filter: %s", filter); break; }; return HA_FALSE; } static int complete_digest_ha1(ldap_context_t* ctx, digest_record_t* rec, const ha_request_t* req, const char* user) { LDAP* ld = NULL; /* freed in finally */ LDAPMessage* results = NULL; /* freed in finally */ LDAPMessage* entry = NULL; /* no need to free */ struct berval** ha1s = NULL; /* freed manually */ char** pws; int ret = HA_FALSE; const char* dn = NULL; int r; ASSERT(ctx && req && rec && user); ld = get_ldap_connection(ctx); if(!ld) { ret = HA_FAILED; goto finally; } /* * Discover the DN of the user. If there's a DN map string * then we can do this really quickly here without querying * the LDAP tree */ if(ctx->dnmap) { /* The map can have %u and %r to denote user and realm */ dn = substitute_params(ctx, req, user, ctx->dnmap); if(!dn) { ret = HA_CRITERROR; goto finally; } ha_messagex(LOG_INFO, "ldap: mapped %s to %s", user, dn); } /* Okay now we contact the LDAP server. */ r = retrieve_user_entry(ctx, req, ld, user, &dn, &entry, &results); if(r != HA_OK) { ret = r; goto finally; } /* Figure out the users ha1 */ if(ctx->ha1_attr) ha1s = ldap_get_values_len(ld, entry, ctx->ha1_attr); if(ha1s) { if(*ha1s) { ret = parse_ldap_ha1(req->buf, *ha1s, rec->ha1); if(ret != HA_OK) { if(ret == HA_FALSE) ha_messagex(LOG_ERR, "ldap: server contains invalid HA1 for user: %s", user); } } ha_messagex(LOG_DEBUG, "ldap: using HA1 from ldap"); ldap_value_free_len(ha1s); goto finally; } /* If no ha1 set or none found, use password and make a HA1 */ pws = ldap_get_values(ld, entry, ctx->pw_attr); if(pws) { /* Find a cleartext password */ const char* t = find_cleartext_password(req->buf, (const char**)pws); if(t) { digest_makeha1(rec->ha1, user, req->context->realm, t); ret = HA_OK; } ldap_value_free(pws); if(ret == HA_OK) { ha_messagex(LOG_DEBUG, "ldap: using clear password from ldap"); goto finally; } } ha_messagex(LOG_ERR, "ldap: server contains no clear password or HA1 for user: %s", user); finally: if(ld) save_ldap_connection(ctx, ld); if(results) ldap_msgfree(results); return ret; } static int basic_ldap_response(ldap_context_t* ctx, const char* header, const ha_request_t* req, ha_response_t* resp) { basic_header_t basic; LDAP* ld = NULL; LDAPMessage* entry = NULL; LDAPMessage* results = NULL; const char* dn = NULL; int ret = HA_FALSE; int found = 0; int r; ASSERT(header && resp && req); if((r = basic_parse(header, req->buf, &basic)) < 0) return r; /* Past this point we don't return directly */ /* Check and see if this connection is in the cache */ if(have_cached_basic(ctx, basic.key)) { ha_messagex(LOG_NOTICE, "ldap: validated basic user against cache: %s", basic.user); found = 1; ret = HA_OK; goto finally; } /* If we have a user name and password */ if(!basic.user || !basic.user[0] || !basic.password || !basic.password[0]) { ha_messagex(LOG_NOTICE, "ldap: no valid basic auth info"); goto finally; } ld = get_ldap_connection(ctx); if(!ld) { ret = HA_FAILED; goto finally; } /* * Discover the DN of the user. If there's a DN map string * then we can do this really quickly here without querying * the LDAP tree */ if(ctx->dnmap) { /* The map can have %u and %r to denote user and realm */ dn = substitute_params(ctx, req, basic.user, ctx->dnmap); if(!dn) { ret = HA_CRITERROR; goto finally; } ha_messagex(LOG_INFO, "ldap: mapped %s to %s", basic.user, dn); } /** * Okay now we contact the LDAP server. There are many ways * this is used for different authentication modes: * * - If a dn has been mapped above, this can apply a * configured filter to narrow things down. * - If no dn has been mapped, then this maps out a dn * by using the single object the filter returns. * - If not in 'dobind' mode we also retrieve the password * here. * * All this results in only one query to the LDAP server, * except for the case of dobind without a dnmap. */ if(!ctx->dobind || !dn || ctx->filter) { r = retrieve_user_entry(ctx, req, ld, basic.user, &dn, &entry, &results); if(r != HA_OK) { ret = r; goto finally; } } /* Now if in bind mode we try to bind as that user */ if(ctx->dobind) { ASSERT(dn); r = ldap_simple_bind_s(ld, dn, basic.password); if(r != LDAP_SUCCESS) { if(r == LDAP_INVALID_CREDENTIALS) ha_messagex(LOG_WARNING, "ldap: basic authentication (via bind) failed for user: %s", basic.user); else report_ldap("couldn't bind to LDAP server", r); goto finally; } /* It worked! */ ha_messagex(LOG_NOTICE, "ldap: validated basic user using bind: %s", basic.user); found = 1; /* Now we have to rebind the connection back to the main user */ r = ldap_simple_bind_s(ld, ctx->user ? ctx->user : "", ctx->password ? ctx->password : ""); if(r != LDAP_SUCCESS) { report_ldap("ldap: couldn't rebind LDAP connection back to auth credentials", r); /* Discard the connection since it's useless to us */ discard_ldap_connection(ctx, ld); ld = NULL; } } /* Otherwise we compare the password attribute */ else { ret = validate_ldap_password(ctx, ld, entry, req->buf, basic.user, basic.password); if(ret == HA_FALSE) ret = validate_ldap_ha1(ctx, ld, entry, req->buf, basic.user, req->context->realm, basic.password); if(ret == HA_OK) { ha_messagex(LOG_NOTICE, "ldap: validated basic user password/ha1: %s", basic.user); found = 1; } else { ha_messagex(LOG_WARNING, "ldap: invalid, unreadable or unrecognized password for user: %s", basic.user); } } finally: if(ld) save_ldap_connection(ctx, ld); if(results) ldap_msgfree(results); if(found && ret >= 0) { resp->code = HA_SERVER_OK; resp->detail = basic.user; /* We put this connection into the successful connections */ ret = add_cached_basic(ctx, req->context, basic.key); } return ret; } static int digest_ldap_challenge(ldap_context_t* ctx, const ha_request_t* req, ha_response_t* resp, int stale) { unsigned char nonce[DIGEST_NONCE_LEN]; const char* nonce_str; const char* header; ASSERT(ctx && resp && req); #ifdef _DEBUG if(req->context->digest_debugnonce) { nonce_str = req->context->digest_debugnonce; ha_messagex(LOG_WARNING, "ldap: using debug nonce. security non-existant."); } else #endif { unsigned char nonce[DIGEST_NONCE_LEN]; digest_makenonce(nonce, g_ldap_secret, NULL); nonce_str = ha_bufenchex(req->buf, nonce, DIGEST_NONCE_LEN); if(!nonce_str) return HA_CRITERROR; } /* Now generate a message to send */ header = digest_challenge(req->buf, nonce_str, req->context->realm, req->digest_domain, stale); if(!header) return HA_CRITERROR; /* And append it nicely */ resp->code = HA_SERVER_DECLINE; ha_addheader(resp, "WWW-Authenticate", header); ha_messagex(LOG_DEBUG, "ldap: created digest challenge with nonce: %s", nonce_str); return HA_OK; } static int digest_ldap_response(ldap_context_t* ctx, const char* header, const ha_request_t* req, ha_response_t* resp) { unsigned char nonce[DIGEST_NONCE_LEN]; digest_header_t dg; digest_record_t* rec = NULL; const char* t; time_t expiry; int ret = HA_FALSE; int stale = 0; int r; ASSERT(ctx && header && req && resp); /* We use this below to send a default response */ resp->code = -1; if((r = digest_parse(header, req->buf, &dg, nonce)) < 0) return r; #ifdef _DEBUG if(req->context->digest_debugnonce) { if(dg.nonce && strcmp(dg.nonce, req->context->digest_debugnonce) != 0) { ret = HA_FALSE; ha_messagex(LOG_WARNING, "ldap: digest response contains invalid nonce"); goto finally; } /* Do a rough hash into the real nonce, for use as a key */ md5_string(nonce, req->context->digest_debugnonce); /* Debug nonce's never expire */ expiry = time(NULL); } else #endif { r = digest_checknonce(nonce, g_ldap_secret, &expiry); if(r != HA_OK) { if(r == HA_FALSE) ha_messagex(LOG_WARNING, "ldap: digest response contains invalid nonce"); goto finally; } } rec = get_cached_digest(ctx, req->context, nonce); /* Check to see if we're stale */ if((expiry + req->context->cache_timeout) <= time(NULL)) { ha_messagex(LOG_INFO, "ldap: nonce expired, sending stale challenge: %s", dg.username); stale = 1; goto finally; } if(!rec) { ha_messagex(LOG_INFO, "ldap: no record in cache, creating one: %s", dg.username); /* * If we're valid but don't have a record in the * cache then complete the record properly. */ rec = digest_makerec(nonce, dg.username); if(!rec) { ret = HA_CRITERROR; goto finally; } r = complete_digest_ha1(ctx, rec, req, dg.username); if(r != HA_OK) { ret = r; goto finally; } } /* We had a record so ... */ else { rec->nc++; } ret = digest_check(&dg, rec, req->context, req->buf, req->args[AUTH_ARG_METHOD], req->args[AUTH_ARG_URI]); if(ret == HA_BADREQ) { ret = HA_FALSE; resp->code = HA_SERVER_BADREQ; } else if(ret == HA_OK) { resp->code = HA_SERVER_OK; resp->detail = dg.username; /* Figure out if we need a new nonce */ if((expiry + (req->context->cache_timeout - (req->context->cache_timeout / 8))) < time(NULL)) { ha_messagex(LOG_INFO, "ldap: nonce almost expired, creating new one: %s", dg.username); digest_makenonce(nonce, g_ldap_secret, NULL); stale = 1; } t = digest_respond(req->buf, &dg, rec, stale ? nonce : NULL); if(!t) { ret = HA_CRITERROR; goto finally; } if(t[0]) ha_addheader(resp, "Authentication-Info", t); ha_messagex(LOG_NOTICE, "ldap: validated digest user: %s", dg.username); /* Put the connection into the cache */ if((r = save_cached_digest(ctx, req->context, rec)) < 0) ret = r; rec = NULL; } finally: /* If the record wasn't stored away then free it */ if(rec) free(rec); /* If nobody above responded then challenge the client again */ if(resp->code == -1) return digest_ldap_challenge(ctx, req, resp, stale); return ret; } /* ------------------------------------------------------------------------------- * Handler Functions */ int ldap_config(ha_context_t* context, const char* name, const char* value) { ldap_context_t* ctx = (ldap_context_t*)(context->ctx_data); ASSERT(name && value && value[0]); if(strcmp(name, "ldapservers") == 0) { ctx->servers = value; return HA_OK; } else if(strcmp(name, "ldapfilter") == 0) { ctx->filter = value; return HA_OK; } else if(strcmp(name, "ldapbase") == 0) { ctx->base = value; return HA_OK; } else if(strcmp(name, "ldappwattr") == 0) { ctx->pw_attr = value; return HA_OK; } else if(strcmp(name, "ldapha1attr") == 0) { ctx->ha1_attr = value; return HA_OK; } else if(strcmp(name, "ldapuser") == 0) { ctx->user = value; return HA_OK; } else if(strcmp(name, "ldappassword") == 0) { ctx->password = value; return HA_OK; } else if(strcmp(name, "ldapdnmap") == 0) { ctx->dnmap = value; return HA_OK; } else if(strcmp(name, "ldapscope") == 0) { if(strcmp(value, "sub") == 0 || strcmp(value, "subtree") == 0) ctx->scope = LDAP_SCOPE_SUBTREE; else if(strcmp(value, "base") == 0) ctx->scope = LDAP_SCOPE_BASE; else if(strcmp(value, "one") == 0 || strcmp(value, "onelevel") == 0) ctx->scope = LDAP_SCOPE_ONELEVEL; else { ha_messagex(LOG_ERR, "invalid value for '%s' (must be 'sub', 'base' or 'one')", name); return HA_FAILED; } return HA_OK; } else if(strcmp(name, "ldapdobind") == 0) { return ha_confbool(name, value, &(ctx->dobind)); } else if(strcmp(name, "ldapmax") == 0) { return ha_confint(name, value, 1, 256, &(ctx->ldap_max)); } else if(strcmp(name, "ldaptimeout") == 0) { return ha_confint(name, value, 0, 86400, &(ctx->ldap_timeout)); } return HA_FALSE; } int ldap_inithand(ha_context_t* context) { /* Global initialization */ if(!context) { ha_messagex(LOG_DEBUG, "ldap: generating secret"); return ha_genrandom(g_ldap_secret, DIGEST_SECRET_LEN); } /* Context specific initialization */ else { ldap_context_t* ctx = (ldap_context_t*)(context->ctx_data); hsh_table_calls_t htc; ASSERT(ctx); /* Make sure there are some types of authentication we can do */ if(!(context->allowed_types & (HA_TYPE_BASIC | HA_TYPE_DIGEST))) { ha_messagex(LOG_ERR, "ldap: module configured, but does not implement any " "configured authentication type."); return HA_FAILED; } /* Check for mandatory configuration */ if(!ctx->servers) { ha_messagex(LOG_ERR, "ldap: configuration incomplete. " "Must have LDAPServers."); return HA_FAILED; } if(!ctx->dnmap && (!ctx->filter || !ctx->base)) { ha_messagex(LOG_ERR, "ldap: configuration incomplete. " "When not using LDAPDNMap must specify LDAPBase and LDAPFilter."); return HA_FAILED; } /* The cache for digest records and basic */ if(!(ctx->cache = hsh_create(MD5_LEN))) { ha_messagex(LOG_CRIT, "out of memory"); return HA_CRITERROR; } htc.f_freeval = free_hash_object; htc.arg = NULL; hsh_set_table_calls(ctx->cache, &htc); ASSERT(!ctx->pool); ASSERT(ctx->ldap_max > 0); /* * Our connection pool. It's the size of our maximum * amount of pending connections as that's the max * we'd be able to use at a time anyway. */ ctx->pool = (LDAP**)malloc(sizeof(LDAP*) * ctx->ldap_max); if(!ctx->pool) { ha_messagex(LOG_CRIT, "out of memory"); return HA_CRITERROR; } memset(ctx->pool, 0, sizeof(LDAP*) * ctx->ldap_max); ha_messagex(LOG_INFO, "ldap: initialized handler"); } return HA_OK; } void ldap_destroy(ha_context_t* context) { ldap_context_t* ctx; int i; if(!context) return; /* Note: We don't need to be thread safe here anymore */ ctx = (ldap_context_t*)(context->ctx_data); ASSERT(ctx); if(ctx->cache) hsh_free(ctx->cache); if(ctx->pool) { /* Close any connections we have open */ for(i = 0; i < ctx->ldap_max; i++) { if(ctx->pool[i]) ldap_unbind_s(ctx->pool[i]); } /* And free the connection pool */ free(ctx->pool); } ha_messagex(LOG_INFO, "ldap: uninitialized handler"); } int ldap_process(const ha_request_t* req, ha_response_t* resp) { ldap_context_t* ctx = (ldap_context_t*)req->context->ctx_data; time_t t = time(NULL); const char* header = NULL; int ret, r; ASSERT(req && resp); ASSERT(req->args[AUTH_ARG_METHOD]); ASSERT(req->args[AUTH_ARG_URI]); ha_lock(NULL); /* Purge out stale connection stuff. */ r = hsh_purge(ctx->cache, t - req->context->cache_timeout); ha_unlock(NULL); if(r > 0) ha_messagex(LOG_DEBUG, "ldap: purged cache records: %d", r); /* We use this below to detect whether to send a default response */ resp->code = -1; /* Check the headers and see if we got a response thingy */ if(req->context->allowed_types & HA_TYPE_DIGEST) { header = ha_getheader(req, "Authorization", HA_PREFIX_DIGEST); if(header) { ha_messagex(LOG_DEBUG, "ldap: processing digest auth header"); ret = digest_ldap_response(ctx, header, req, resp); if(ret < 0) return ret; } } /* Or a basic authentication */ if(!header && req->context->allowed_types & HA_TYPE_BASIC) { header = ha_getheader(req, "Authorization", HA_PREFIX_BASIC); if(header) { ha_messagex(LOG_DEBUG, "ldap: processing basic auth header"); ret = basic_ldap_response(ctx, header, req, resp); if(ret < 0) return ret; } } /* Send a default response if that's what we need */ if(resp->code == -1) { resp->code = HA_SERVER_DECLINE; if(req->context->allowed_types & HA_TYPE_BASIC) { ha_bufmcat(req->buf, "BASIC realm=\"", req->context->realm , "\"", NULL); if(ha_buferr(req->buf)) return HA_CRITERROR; ha_addheader(resp, "WWW-Authenticate", ha_bufdata(req->buf)); ha_messagex(LOG_DEBUG, "ldap: sent basic auth request"); } if(req->context->allowed_types & HA_TYPE_DIGEST) { ret = digest_ldap_challenge(ctx, req, resp, 0); if(ret < 0) return ret; } } return ret; } /* ------------------------------------------------------------------------------- * Handler Definition */ ha_handler_t ldap_handler = { "LDAP", /* The type */ ldap_inithand, /* Initialization function */ ldap_destroy, /* Uninitialization routine */ ldap_config, /* Config routine */ ldap_process, /* Processing routine */ &ldap_defaults, /* The context defaults */ sizeof(ldap_context_t) };