/* * HttpAuth * * Copyright (C) 2004 Stefan Walter * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. * You should have received a copy of the GNU General Public License * along with this program; if not, write to the * Free Software Foundation, Inc., * 59 Temple Place, Suite 330, * Boston, MA 02111-1307, USA. */ /* Ideas and Guidance from Apache 2.0's mod_auth_digest */ #include "usuals.h" #include "httpauthd.h" #include "hash.h" #include "defaults.h" #include "digest.h" #include "basic.h" #include "md5.h" #include "bd.h" static unsigned char g_digest_secret[DIGEST_SECRET_LEN]; /* ------------------------------------------------------------------------------- * Defaults and Constants */ #define BASIC_ESTABLISHED (void*)1 /* Kept by the us for validating the client */ typedef struct digest_record { unsigned char nonce[DIGEST_NONCE_LEN]; unsigned char userhash[MD5_LEN]; unsigned char ha1[MD5_LEN]; unsigned int nc; } digest_record_t; /* ------------------------------------------------------------------------------- * Internal Functions */ static void free_hash_object(void* arg, void* val) { if(val && val != BASIC_ESTABLISHED) free(val); } static digest_record_t* get_cached_digest(bd_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(bd_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(bd_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_memerr(NULL); return HA_CRITERROR; } return HA_OK; } static int add_cached_basic(bd_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_memerr(NULL); return HA_CRITERROR; } return HA_OK; } digest_record_t* make_digest_rec(unsigned char* nonce, const char* user) { digest_record_t* rec = (digest_record_t*)malloc(sizeof(*rec)); ASSERT(nonce && user); if(!rec) { ha_memerr(NULL); return NULL; } memset(rec, 0, sizeof(*rec)); memcpy(rec->nonce, nonce, DIGEST_NONCE_LEN); md5_string(rec->userhash, user); return rec; } static int do_basic_response(ha_request_t* rq, bd_context_t* ctx, const char* header) { basic_header_t basic; int ret = HA_FALSE; ASSERT(header && rq); if((ret = basic_parse(header, rq->buf, &basic)) < 0) return ret; /* 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(rq, LOG_NOTICE, "validated basic user against cache: %s", basic.user); RETURN(HA_OK); } /* If we have a user name and password */ if(!basic.user || !basic.user[0] || !basic.password || !basic.password[0]) { ha_messagex(rq, LOG_NOTICE, "no valid basic auth info"); RETURN(HA_FALSE); } ASSERT(ctx->f_validate_basic); ret = ctx->f_validate_basic(rq, basic.user, basic.password); finally: if(ret == HA_OK) { rq->resp_code = HA_SERVER_OK; rq->resp_detail = basic.user; /* We put this connection into the successful connections */ ret = add_cached_basic(ctx, rq->context, basic.key); } return ret; } static int do_digest_challenge(ha_request_t* rq, bd_context_t* ctx, int stale) { unsigned char nonce[DIGEST_NONCE_LEN]; const char* nonce_str; const char* header; ASSERT(ctx && rq); #ifdef _DEBUG if(rq->context->digest_debugnonce) { nonce_str = rq->context->digest_debugnonce; ha_messagex(rq, LOG_WARNING, "using debug nonce. security non-existant."); } else #endif { unsigned char nonce[DIGEST_NONCE_LEN]; digest_makenonce(nonce, g_digest_secret, NULL); nonce_str = ha_bufenchex(rq->buf, nonce, DIGEST_NONCE_LEN); if(!nonce_str) return HA_CRITERROR; } /* Now generate a message to send */ header = digest_challenge(rq->buf, nonce_str, rq->context->realm, rq->digest_domain, stale); if(!header) return HA_CRITERROR; /* And append it nicely */ rq->resp_code = HA_SERVER_DECLINE; ha_addheader(rq, "WWW-Authenticate", header); ha_messagex(rq, LOG_DEBUG, "created digest challenge with nonce: %s", nonce_str); return HA_OK; } static int do_digest_response(ha_request_t* rq, bd_context_t* ctx, const char* header) { unsigned char nonce[DIGEST_NONCE_LEN]; digest_context_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 && rq); /* We use this below to send a default response */ rq->resp_code = -1; if((r = digest_parse(header, rq->buf, &(dg.client))) < 0) return r; if(!dg.client.username) { ha_messagex(rq, LOG_WARNING, "digest response contains no user name"); RETURN(HA_FALSE); } #ifdef _DEBUG if(rq->context->digest_debugnonce) { if(dg.client.nonce && strcmp(dg.client.nonce, rq->context->digest_debugnonce) != 0) { ha_messagex(rq, LOG_WARNING, "digest response contains invalid nonce"); RETURN(HA_FALSE); } /* Do a rough hash into the real nonce, for use as a key */ md5_string(nonce, rq->context->digest_debugnonce); /* Debug nonce's never expire */ expiry = time(NULL); } else #endif { /* Parse out the nonce from that header */ memset(nonce, 0, DIGEST_NONCE_LEN); if(dg.client.nonce) { size_t len = DIGEST_NONCE_LEN; void* d = ha_bufdechex(rq->buf, dg.client.nonce, &len); if(d && len == DIGEST_NONCE_LEN) memcpy(nonce, d, DIGEST_NONCE_LEN); } r = digest_checknonce(nonce, g_digest_secret, &expiry); if(r != HA_OK) { if(r == HA_FALSE) ha_messagex(rq, LOG_WARNING, "digest response contains invalid nonce"); RETURN(r); } /* Check to see if we're stale */ if((expiry + rq->context->cache_timeout) <= time(NULL)) { ha_messagex(rq, LOG_INFO, "nonce expired, sending stale challenge: %s", dg.client.username); stale = 1; RETURN(HA_FALSE); } } /* See if we have this one from before. */ rec = get_cached_digest(ctx, rq->context, nonce); /* * Fill in the required fields. */ dg.server_nc = rec ? ++(rec->nc) : 0; /* Note bumping up nc */ dg.server_uri = rq->req_args[AUTH_ARG_URI]; dg.server_method = rq->req_args[AUTH_ARG_METHOD]; /* Check the majority of the fields */ ret = digest_pre_check(&dg, rq->context, rq->buf); if(ret != HA_OK) { if(ret == HA_BADREQ) { ret = HA_FALSE; rq->resp_code = HA_SERVER_BADREQ; } RETURN(ret); } /* * If this is the first instance then we pass off to our derived * handler for validation and completion of the ha1. This completes * the authentication, and leaves us the ha1 caching. */ if(!rec) { ha_messagex(rq, LOG_INFO, "no record in cache, creating one: %s", dg.client.username); /* * If we're valid but don't have a record in the * cache then complete the record properly. */ rec = make_digest_rec(nonce, dg.client.username); if(!rec) RETURN(HA_CRITERROR); ASSERT(ctx->f_validate_digest); r = ctx->f_validate_digest(rq, dg.client.username, &dg); if(r != HA_OK) RETURN(r); /* Save away pertinent information when successful*/ memcpy(rec->ha1, dg.ha1, MD5_LEN); } /* We had a record so ... */ else { /* Bump up the ncount */ rec->nc++; /* Check the user name */ if(md5_strcmp(rec->userhash, dg.client.username) != 0) { ha_messagex(NULL, LOG_ERR, "digest response contains invalid username"); RETURN(HA_FALSE); } /* And do the validation ourselves */ memcpy(dg.ha1, rec->ha1, MD5_LEN); ret = digest_complete_check(&dg, rq->context, rq->buf); if(ret != HA_OK) { if(ret == HA_BADREQ) { ret = HA_FALSE; rq->resp_code = HA_SERVER_BADREQ; } if(ret == HA_FALSE) ha_messagex(NULL, LOG_WARNING, "digest re-authentication failed for user: %s", dg.client.username); RETURN(ret); } } rq->resp_code = HA_SERVER_OK; rq->resp_detail = dg.client.username; /* Figure out if we need a new nonce */ if((expiry + (rq->context->cache_timeout - (rq->context->cache_timeout / 8))) < time(NULL)) { ha_messagex(rq, LOG_INFO, "nonce almost expired, creating new one: %s", dg.client.username); digest_makenonce(nonce, g_digest_secret, NULL); stale = 1; } t = digest_respond(&dg, rq->buf, stale ? nonce : NULL); if(!t) RETURN(HA_CRITERROR); if(t[0]) ha_addheader(rq, "Authentication-Info", t); ha_messagex(rq, LOG_NOTICE, "validated digest user: %s", dg.client.username); /* Put the connection into the cache */ if((ret = save_cached_digest(ctx, rq->context, rec)) == HA_OK) 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(ret == HA_FALSE && rq->resp_code == -1) return do_digest_challenge(rq, ctx, stale); return ret; } const char* bd_substitute(const ha_request_t* rq, const char* user, const char* str) { bd_context_t* ctx = (bd_context_t*)rq->context->ctx_data; const char* t; ASSERT(rq && user && str); ASSERT(ctx->f_escape_value); /* This starts a new block to join */ ha_bufcpy(rq->buf, ""); while(str[0]) { t = strchr(str, '%'); if(!t) { ha_bufjoin(rq->buf); ha_bufcpy(rq->buf, str); break; } ha_bufjoin(rq->buf); ha_bufncpy(rq->buf, str, t - str); t++; switch(t[0]) { case 'u': ha_bufjoin(rq->buf); (ctx->f_escape_value)(rq, rq->buf, user); t++; break; case 'r': ha_bufjoin(rq->buf); (ctx->f_escape_value)(rq, rq->buf, rq->context->realm); t++; break; case '%': ha_bufjoin(rq->buf); ha_bufcpy(rq->buf, "%"); t++; break; }; str = t; } return ha_bufdata(rq->buf); } /* ------------------------------------------------------------------------------- * Handler Functions */ int bd_init(ha_context_t* context) { /* Global initialization */ if(!context) { ha_messagex(NULL, LOG_DEBUG, "generating secret"); return ha_genrandom(g_digest_secret, DIGEST_SECRET_LEN); } /* Context specific initialization */ else { bd_context_t* ctx = (bd_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(NULL, LOG_ERR, "module configured, but does not implement any " "configured authentication type."); return HA_FAILED; } /* The cache for digest records and basic */ if(!(ctx->cache = hsh_create(MD5_LEN))) { ha_memerr(NULL); return HA_CRITERROR; } htc.f_freeval = free_hash_object; htc.arg = NULL; hsh_set_table_calls(ctx->cache, &htc); } return HA_OK; } void bd_destroy(ha_context_t* context) { bd_context_t* ctx; int i; if(!context) return; /* Note: We don't need to be thread safe here anymore */ ctx = (bd_context_t*)(context->ctx_data); ASSERT(ctx); if(ctx->cache) hsh_free(ctx->cache); ha_messagex(NULL, LOG_INFO, "uninitialized handler"); } int bd_process(ha_request_t* rq) { bd_context_t* ctx = (bd_context_t*)rq->context->ctx_data; time_t t = time(NULL); const char* header = NULL; int ret, r; ASSERT(rq); ASSERT(rq->req_args[AUTH_ARG_METHOD]); ASSERT(rq->req_args[AUTH_ARG_URI]); ha_lock(NULL); /* Purge out stale connection stuff. */ r = hsh_purge(ctx->cache, t - rq->context->cache_timeout); ha_unlock(NULL); if(r > 0) ha_messagex(rq, LOG_DEBUG, "purged cache records: %d", r); /* We use this below to detect whether to send a default response */ rq->resp_code = -1; /* Check the headers and see if we got a response thingy */ if(rq->context->allowed_types & HA_TYPE_DIGEST) { header = ha_getheader(rq, "Authorization", HA_PREFIX_DIGEST); if(header) { ha_messagex(rq, LOG_DEBUG, "processing digest auth header"); ret = do_digest_response(rq, ctx, header); if(ret < 0) return ret; } } /* Or a basic authentication */ if(!header && rq->context->allowed_types & HA_TYPE_BASIC) { header = ha_getheader(rq, "Authorization", HA_PREFIX_BASIC); if(header) { ha_messagex(rq, LOG_DEBUG, "processing basic auth header"); ret = do_basic_response(rq, ctx, header); if(ret < 0) return ret; } } /* Send a default response if that's what we need */ if(rq->resp_code == -1) { rq->resp_code = HA_SERVER_DECLINE; if(rq->context->allowed_types & HA_TYPE_BASIC) { ha_bufmcat(rq->buf, "BASIC realm=\"", rq->context->realm , "\"", NULL); if(CHECK_RBUF(rq)) return HA_CRITERROR; ha_addheader(rq, "WWW-Authenticate", ha_bufdata(rq->buf)); ha_messagex(rq, LOG_DEBUG, "sent basic auth request"); ret = HA_OK; } if(rq->context->allowed_types & HA_TYPE_DIGEST) { ret = do_digest_challenge(rq, ctx, 0); if(ret < 0) return ret; } } return ret; }