/* TODO: Include attribution for ideas, and code from mod_auth_digest */ #include "usuals.h" #include "httpauthd.h" #include "hash.h" #include "defaults.h" #include /* LDAP library */ #include /* ------------------------------------------------------------------------------- * Defaults and Constants */ /* This needs to be the same as an MD5 hash length */ #define LDAP_HASH_KEY_LEN 16 #define LDAP_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* realm; /* The realm to use in authentication */ 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 pending_max; /* Maximum number of connections at once */ int pending_timeout; /* Timeout for authentication (in seconds) */ int ldap_timeout; /* Timeout for LDAP operations */ /* Context ----------------------------------------------------------- */ hash_t* pending; /* Pending connections */ hash_t* established; /* Established connections */ LDAP** pool; /* Pool of available connections */ int pool_mark; /* Amount of connections allocated */ } ldap_context_t; /* The defaults for the context */ static const ldap_defaults = {XXXXX NULL, NULL, "", "userPassword", NULL, NULL, NULL, "", NULL LDAP_SCOPE_DEFAULT, 1, DEFAULT_PENDING_MAX, DEFAULT_PENDING_TIMEOUT, 30, NULL, NULL, NULL }; /* ------------------------------------------------------------------------------- * Internal Functions */ static void make_digest_ha1(unsigned char* digest, const char* user, const char* realm, const char* password) { struct MD5Context md5; MD5_Init(&md5); MD5_Update(&md5, user, strlen(user)); MD5_Update(&md5, ":", 1); MD5_Update(&md5, realm, strlen(realm)); MD5_Update(&md5, ":", 1); MD5_Update(&md5, password, strlen(pasword)); MD5_Final(digest, &md5); } static const char* make_password_md5(ha_buffer_t* buf, const char* clearpw) { struct MD5Context md5; unsigned char digest[MD5_LEN]; MD5_Init(&md5); MD5_Update(&md5, clearpw, strlen(clearpw)); MD5_Final(digest, &md5); ha_bufnext(buf); ha_bufenc64(buf, digest, MD5_LEN); return ha_bufdata(buf); } static const char* make_password_sha(ha_buffer_t* buf, const char* clearpw) { struct SHA1Context sha; unsigned char digest[SHA1_LEN]; SHA1_Init(&sha); SHA1_Update(&sha, clearpw, strlen(clearpw)); SHA1_Final(digest, &sha); ha_bufnext(buf); ha_bufenc64(buf, digest, SHA1_LEN); return ha_bufdata(buf); } 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(kLDAPSchemes[i].name, scheme, pw - scheme)) return kLDAPSchemes[i].type; } return LDAP_PW_UNKNOWN; } static const char* find_cleartext_password(ha_buffer_t* buf, const char** pws) { ha_bufnext(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) { /* Raw binary */ if(bv->bv_len == MD5_LEN) { memcpy(ha1, bv->bv_len, MD5_LEN); return HA_OK; } /* Hex encoded */ else if(bv->bv_len == (MD5_LEN * 2)) { ha_bufnext(buf); ha_bufdechex(buf, bv->bv_val, MD5_LEN * 2); if(!ha_bufdata(buf)) return HA_ERROR; if(ha_buflen(buf) == MD5_LEN) { memcpy(rec->ha1, ha_bufdata(buf), MD5_LEN); return HA_OK; } } /* B64 Encoded */ else { ha_bufnext(buf); ha_bufdec64(buf, (*pws)->bv_val, (*pws)->bv_len); if(!ha_bufdata(buf)) return HA_ERROR; if(ha_buflen(buf) == MD5_LEN) { memcpy(rec->ha1, ha_bufdata(buf), MD5_LEN); return HA_OK; } } return 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) { const char** pws; 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( ; *pws; pws++) { pw = *pws; 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_ERROR; break; } if(strcmp(pw, p) == 0) { res = HA_OK; break; } } ldap_free_values(pws); } if(res == HA_FALSE && unknown) ha_messagex(LOG_ERR, "LDAP 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* clearpw) { struct berval** ha1s; unsigned char key[MD5_LEN]; unsigned char k[MD5_LEN]; int r, first = 1; int res = HA_FALSE; if(!ctx->ha1_attr) return HA_FALSE; ha1s = ldap_get_values_len(ld, entry, ctx->ha1_attr); if(ha1s) { make_digest_ha1(key, user, ctx->realm, clearpw); for( ; *ha1s; ha1s++) { r = parse_ldap_h1(buf, *ha1s, k); if(r == HA_ERROR) { res = r; break; } if(r == HA_FALSE) { if(first) ha_messagex(LOG_ERROR, "LDAP contains invalid HA1 digest hash for user: %s", user); first = 0; continue; } if(memcmp(key, k, MD5_LEN) == 0) { res = HA_OK; break; } } ldap_free_values_len(ha1s); } return res; } static LDAP* get_ldap_connection(ldap_context_t* ctx) { LDAP* ld; int i, r; for(i = 0; i < ctx->pending_max; i++) { /* An open connection in the pool */ if(ctx->pool[i]) { ld = ctx->pool[i]; ctx->pool[i]; return ld; } } if(ctx->pool_mark >= ctx->pending_max) { ha_messagex("too many open connections to LDAP"); return NULL; } ld = ldap_init(ctx->servers, ctx->port); if(!ld) { ha_message("couldn't initialize ldap 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(r, NULL); ldap_unbind_s(ld); return NULL; } } return ld; } static void save_ldap_connection(ldap_context_t* ctx, LDAP* ld) { int i; if(!ld) return; /* Make sure it's worth saving */ switch(ld_errno(ld)) { case LDAP_SERVER_DOWN: case LDAP_LOCAL_ERROR: case LDAP_NO_MEMORY: break; default: for(i = 0; i < ctx->pending_max; i++) { /* An open connection in the pool */ if(!ctx->pool[i]) { ctx->pool[i] = ld; ld = NULL; break; } } break; }; if(ld != NULL) ldap_unbind_s(ld); } static int complete_digest_ha1(ldap_context_t* ctx, ha_digest_rec_t* rec, const char* user) { LDAP* ld = NULL; /* freed in finally */ LDAPMessage* results = NULL; /* freed in finally */ LDAPMessage* entry = NULL; /* no need to free */ struct berval** pws; /* freed manually */ int ret = HA_FALSE; /* Hash in the user name */ ha_md5string(user, rec->userhash); ld = get_ldap_connection(ctx); if(!ld) 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, buf, user, ctx->dnmap); if(!dn) { ret = HA_ERROR; goto finally; } } /* Okay now we contact the LDAP server. */ r = retrieve_user_entry(ctx, buf, user, &dn, &entry, &results); if(r != HA_OK) { ret = r; goto finally; } /* Figure out the users ha1 */ if(ctx->ha1_attr) pws = ldap_get_values_len(ld, entry, ctx->ha1_attr); if(pws) { if(*pws) { r = parse_ldap_ha1(buf, *pws, rec->ha1); if(r != HA_OK) { ret = r if(ret != HA_FALSE) ha_messagex(LOG_ERROR, "LDAP contains invalid HA1 digest hash for user: %s", user); } } ldap_free_values_len(pws); goto finally; } /* If no ha1 set or none found, use password and make a HA1 */ pws = ldap_get_values_len(ld, entry, ctx->pw_attr); if(pws) { /* Find a cleartext password */ const char* t = find_cleartext_password(buf, pws); ldap_free_values_len(pws); if(t) { make_digest_ha1(rec->ha1, user, ctx->realm, t); ret = HA_OK; goto finally; } } ha_messagex(LOG_ERROR, "LDAP contains no cleartext password for user: %s", user); finally: if(ld) save_ldap_connection(ctx, ld); if(results) ldap_msgfree(results); return ret; } static int retrieve_user_entry(ldap_context_t* ctx, buffer_t* buf, LDAP* ld, const char* user, const char** dn, LDAPMessage** entry, LDAPMessage** result) { timeval tv; const char* filter; const char* attrs[3]; if(ctx->filter) { /* Filters can also have %u and %r */ filter = substitute_params(ctx, buf, user, ctx->filter); if(!filter) return HA_ERROR; } 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; r = ldap_search_st(ld, *dn ? *dn : ctx->base, *dn ? LDAP_SCOPE_BASE : ctx->scope, filter, attrs, 0, &tv, result); if(r != LDAP_SUCCESS) return report_ldap(r, resp, &ret); /* 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); return HA_OK; case 0: ha_messagex(LOG_WARNING, "user not found in LDAP: %s", basic.user); break; default: ha_messagex(LOG_WARNING, "more than one user found for filter: %s", filter); break; }; ldap_msg_free(*result); return HA_FALSE; } static int basic_ldap_response(ldap_context_t* ctx, const char* header, ha_response_t* resp, ha_buffer_t* buf) { ha_basic_header_t basic; LDAP* ld = NULL; LDAPMessage* entry = NULL; LDAPMessage* results = NULL; const char* dn; int ret = HA_FALSE; int found = 0; int r; ASSERT(buf && header && resp && buf); if(ha_parsebasic(header, buf, &basic) == HA_ERROR) return HA_ERROR; /* Past this point we don't return directly */ /* Check and see if this connection is in the cache */ ha_lock(NULL); if(hash_get(ctx->established, key) == BASIC_ESTABLISHED) { found = 1; ret = HA_OK; goto finally: } ha_unlock(NULL); /* If we have a user name and password */ if(!basic.user || !basic.user[0] || !basic.password || !basic.password[0]) goto finally; ld = get_ldap_connection(); if(!ld) { resp->code = HA_SERVER_ERROR; 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, buf, basic.user, ctx->dnmap); if(!dn) { ret = HA_ERROR; goto finally; } } /** * 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, buf, 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, "invalid login for: %s", basic.user); else report_ldap(r, resp, &ret); goto finally; } /* It worked! */ resp->code = HA_SERVER_ACCEPT; } /* Otherwise we compare the password attribute */ else { ret = validate_ldap_password(ctx, ld, entry, buf, basic.user, basic.password); if(ret == HA_FALSE) ret = validate_ldap_ha1(ctx, ld, entry, buf, basic.user, basic.password); if(ret == HA_OK) resp->code = HA_SERVER_ACCEPT; else ha_messagex(LOG_WARNING, "invalid or unrecognized password for user: %s", basic.user); } finally: if(ld) save_ldap_connection(ctx, ld); if(results) ldap_msgfree(results); if(resp->code == HA_SERVER_ACCEPT) { resp->details = basic.user; /* We put this connection into the successful connections */ if(!hash_set(ctx->established, basic.key, LDAP_ESTABLISHED)) { ha_messagex(LOG_CRIT, "out of memory"); return HA_ERROR; } } return ret; } static int digest_ldap_response(ldap_context_t* ctx, const char* header, const char* method, const char* uri, ha_response_t* resp, ha_buffer_t* buf) { ha_digest_header_t dg; digest_rec_t* rec = NULL; int ret = HA_FALSE; int stale = 0; int pending = 0; /* We use this below to send a default response */ resp->code = -1; if(ha_parsedigest(header, buf, &rec) == HA_ERROR) return HA_ERROR; /* Lookup our digest context based on the nonce */ if(!dg.nonce || strlen(dg.nonce) != DIGEST_NONCE_LEN) { ha_messagex(LOG_WARNING, "digest response contains invalid nonce"); goto finally; } ha_lock(NULL); rec = (digest_rec_t*)hash_get(ctx->pending, dg.nonce) if(rec) { pending = 1; hash_rem(ctx->pending, dg.nonce); } else { rec = (digest_rec_t*)hash_get(ctx->established, dg.nonce); } ha_unlock(NULL); /* * If nothing was found for this nonce, then it might * be a stale nonce. In any case prompt the client * to reauthenticate. */ if(!rec) { stale = 1; goto finally; } /* * If we got a response from the pending table, then * we need to lookup the user name and figure out * who the dude is. */ if(pending) { ASSERT(rec); r = complete_digest_ha1(ctx, rec, dg->username); if(r != HA_OK) { ret = r; goto finally; } } /* Increment our nonce count */ rec->nc++; ret = ha_digestcheck(ctx->realm, method, uri, buf, &dg, rec); if(ret == HA_OK) { resp->code = HA_SERVER_ACCEPT; resp->details = dg->username; /* Put the connection back into established */ ha_lock(NULL); if(hash_set(ctx->established, dg.nonce, rec)) { rec = NULL; } else { ha_messagex(LOG_CRIT, "out of memory"); ret = HA_ERROR; } ha_unlock(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, resp, buf, 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.data); 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, "realm") == 0) { ctx->realm = 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 { messagex(LOG_ERR, "invalid value for '%s' (must be 'sub', 'base' or 'one')", name); return HA_ERROR; } return HA_OK; } else if(strcmp(name, "ldapdobind") == 0) { return ha_confbool(name, value, &(ctx->dobind)); } else if(strcmp(name, "pendingmax") == 0) { return ha_confint(name, value, 1, 256, &(ctx->pending_max)); } else if(strcmp(name, "pendingtimeout") == 0) { return ha_confint(name, value, 0, 86400, &(ctx->pending_timeout)); } else if(strcmp(name, "ldaptimeout") == 0) { return ha_confint(name, value, 0, 86400, &(ctx->ldap_timeout)); } return HA_FALSE; } int ldap_initialize(ha_context_t* context) { /* No global initialization */ if(!context) return HA_OK; ldap_context_t* ctx = (ldap_context_t*)(context.data); /* Make sure there are some types of authentication we can do */ if(!(context->types & (HA_TYPE_BASIC | HA_TYPE_DIGEST))) { ha_messagex(LOG_ERR, "Digest module configured, but does not implement any " "configured authentication type."); return HA_ERROR; } /* Check for mandatory configuration */ if(!ctx->servers || (!ctx->dnmap || !ctx->filter)) { ha_messagex(LOG_ERR, "Digest LDAP configuration incomplete. " "Must have LDAPServers and either LDAPFilter or LDAPDNMap."); return HA_ERROR; } /* The hash tables */ if(!(ctx->pending = hash_create(LDAP_HASH_KEY_LEN)) || !(ctx->established = hash_create(LDAP_HASH_KEY_LEN))) { ha_messagex(LOG_CRIT, "out of memory"); return HA_ERROR; } /* * 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. */ XXXXX ctx->pool = (LDAP**)malloc(sizeof(LDAP*) * ctx->pending_max); if(!ctx->pool) { ha_messagex(LOG_CRIT, "out of memory"); return HA_ERROR; } memset(ctx->pool, 0, sizeof(LDAP*) * ctx->pending_max); return HA_OK; } void ldap_destroy(ha_context_t* context) { int i; if(!context) return HA_OK; ldap_context_t* ctx = (digest_ldap_context_t*)(context.data); /* Note: We don't need to be thread safe here anymore */ hash_free(ctx->pending); hash_free(ctx->established); XXXXX /* Close any connections we have open */ for(i = 0; i < ctx->pending_max; i++) { if(ctx->pool[i]) ldap_unbind_s(ctx->pool[i]); } /* And free the connection pool */ free(ctx->pool); } int ldap_process(ha_context_t* context, ha_request_t* req, ha_response_t* resp, ha_buffer_t* buf) { ldap_context_t* ctx = (ldap_context_t*)context; time_t t = time(NULL); const char* header = NULL; int ret; ha_lock(NULL); XXXXXX /* * Purge out stale connection stuff. This includes * authenticated connections which have expired as * well as half open connections which expire. */ hash_purge(ctx->pending, t - ctx->pending_timeout); hash_purge(ctx->established, t - ctx->timeout); ha_unlock(NULL); /* 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(ctx->types & HA_TYPE_DIGEST) { header = ha_getheader(req, "Authorization", HA_PREFIX_DIGEST); if(header) { ret = digest_ldap_response(ctx, header, resp, buf); if(ret == HA_ERROR) return ret; } } /* Or a basic authentication */ if(!header && ctx->types & HA_TYPE_BASIC) { header = ha_getheader(req, "Authorization", HA_PREFIX_BASIC); if(header) { ret = basic_ldap_response(ctx, header, resp, buf); if(ret == HA_ERROR) return ret; } } /* Send a default response if that's what we need */ if(resp->code == -1) { resp->code = HA_SERVER_DECLINE; if(ctx->types & HA_TYPE_DIGEST) { ret = digest_ldap_challenge(ctx, resp, buf, 0); if(ret == HA_ERROR) return ret; } if(ctx->types & HA_TYPE_BASIC) { ha_bufnext(buf); ha_bufcat(buf, "BASIC realm=\"", ctx->realm , "\"", NULL); ha_addheader(resp, "WWW-Authenticate", ha_bufdata(buf)); } } return ret; } /* ------------------------------------------------------------------------------- * Handler Definition */ ha_handler_t digest_ldap_handler = { "LDAP", /* The type */ ldap_initialize, /* Initialization function */ ldap_destroy, /* Uninitialization routine */ ldap_config, /* Config routine */ ldap_process, /* Processing routine */ &ldap_defaults, /* The context defaults */ sizeof(ldap_context_t) };