/* * 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" #include "stringx.h" static unsigned char g_digest_secret[DIGEST_SECRET_LEN]; /* ------------------------------------------------------------------------------- * Defaults and Constants */ enum { RECORD_TYPE_BASIC, RECORD_TYPE_DIGEST }; /* Kept by the us for validating the client */ typedef struct bd_record { int type; /* Used for Digest */ unsigned char nonce[DIGEST_NONCE_LEN]; unsigned char userhash[MD5_LEN]; unsigned char ha1[MD5_LEN]; unsigned int nc; /* Used for both */ char **groups; } bd_record_t; /* ------------------------------------------------------------------------------- * Internal Functions */ static void free_hash_object (void *arg, void *val) { bd_record_t *rec = val; if (!rec) return; str_array_free (rec->groups); free (rec); } static int have_cached_basic (bd_context_t* ctx, unsigned char* key) { bd_record_t *rec; ASSERT (ctx && key); ha_lock(NULL); rec = (bd_record_t*)hsh_get (ctx->cache, key); if (rec && rec->type != RECORD_TYPE_BASIC) rec = NULL; else hsh_touch (ctx->cache, key); ha_unlock(NULL); return rec != NULL; } static int add_cached_basic (bd_context_t *ctx, unsigned char *key, char **groups) { bd_record_t *rec; int r; ASSERT (ctx && key); rec = (bd_record_t*)malloc (sizeof (*rec)); if (!rec) { str_array_free (groups); ha_memerr (NULL); return HA_CRITERROR; } memset (rec, 0, sizeof (*rec)); rec->type = RECORD_TYPE_BASIC; rec->groups = groups; ha_lock (NULL); while (hsh_count (ctx->cache) >= ctx->cache_max) hsh_bump (ctx->cache); r = hsh_set (ctx->cache, key, rec); ha_unlock (NULL); if (!r) { free_hash_object (NULL, rec); ha_memerr (NULL); return HA_CRITERROR; } return HA_OK; } static int prepare_digest_from_cached (bd_context_t *ctx, digest_context_t *dg, ha_request_t *rq, unsigned char *nonce) { bd_record_t *rec; int ret; ASSERT (dg && rq && nonce); ha_lock (NULL); rec = (bd_record_t*)hsh_get (ctx->cache, nonce); if (rec && rec->type == RECORD_TYPE_DIGEST) { ASSERT (memcmp (nonce, rec->nonce, DIGEST_NONCE_LEN) == 0); memcpy (dg->server_userhash, rec->userhash, sizeof (dg->server_userhash)); memcpy (dg->server_ha1, rec->ha1, sizeof (dg->server_ha1)); dg->server_nc = ++(rec->nc); hsh_touch (ctx->cache, nonce); ret = HA_OK; } else { memset (dg->server_userhash, 0, sizeof (dg->server_userhash)); memset (dg->server_ha1, 0, sizeof (dg->server_ha1)); dg->server_nc = 1; ret = HA_FALSE; } ha_unlock (NULL); dg->server_uri = rq->req_args[AUTH_ARG_URI]; dg->server_method = rq->req_args[AUTH_ARG_METHOD]; return ret; } static int add_digest_rec (bd_context_t *ctx, unsigned char *nonce, const char *user, digest_context_t *dg, char **groups) { bd_record_t* rec = (bd_record_t*)malloc (sizeof (*rec)); int r; ASSERT (nonce && user); if(!rec) { str_array_free (groups); ha_memerr(NULL); return HA_CRITERROR; } memset (rec, 0, sizeof (*rec)); rec->type = RECORD_TYPE_DIGEST; memcpy (rec->nonce, nonce, DIGEST_NONCE_LEN); memcpy (rec->ha1, dg->server_ha1, MD5_LEN); /* We take ownership of groups */ rec->groups = groups; rec->nc = dg->server_nc; md5_string (rec->userhash, user); ha_lock (NULL); while (hsh_count (ctx->cache) >= ctx->cache_max) hsh_bump (ctx->cache); r = hsh_set (ctx->cache, nonce, rec); ha_unlock (NULL); if (!r) { free_hash_object (NULL, rec); ha_memerr (NULL); return HA_CRITERROR; } return HA_OK; } static int include_group_headers (bd_context_t *ctx, ha_request_t *rq, unsigned char *key) { bd_record_t *rec; int have, all = 0; char *header; char **ug; char **rg; if (!rq->requested_groups || !rq->requested_groups[0]) return HA_OK; ha_lock (NULL); /* This starts a new block to join */ ha_bufcpy (rq->buf, ""); rec = hsh_get (ctx->cache, key); if (rec) { for (ug = rec->groups; ug && *ug; ++ug) { have = all; for (rg = rq->requested_groups; rg && *rg && !have; ++rg) { if (strcasecmp (*rg, "*") == 0) { have = all = 1; break; } else if (strcasecmp (*rg, *ug) == 0) { have = 1; break; } } if (have) { ha_bufjoin (rq->buf); ha_bufcpy (rq->buf, "\""); ha_bufjoin (rq->buf); digest_escape (rq->buf, *ug); ha_bufjoin (rq->buf); ha_bufcpy (rq->buf, "\" "); } } } ha_unlock (NULL); header = ha_bufdata (rq->buf); if (!header) return HA_CRITERROR; ha_addheader (rq, "X-HttpAuth-Groups", header); return HA_OK; } static int do_basic_response (ha_request_t* rq, bd_context_t* ctx, const char* header) { basic_header_t basic; char **groups = NULL; 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, &groups); /* * We put this connection into the successful connections. * This takes ownership of groups. */ if (ret == HA_OK) ret = add_cached_basic (ctx, basic.key, groups); finally: if (ret == HA_OK) { rq->resp_code = HA_SERVER_OK; rq->resp_detail = basic.user; include_group_headers (ctx, rq, basic.key); } return ret; } static int do_digest_challenge(ha_request_t* rq, bd_context_t* ctx, int stale) { 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; const char *t; char **groups = NULL; time_t expiry; int ret = HA_FALSE; int stale = 0; int r, cached; 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"); stale = 1; 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); } } /* * Fill in all the required fields from any cached response, * and check the user name if cached. Otherwise initializes * to default values. */ cached = (prepare_digest_from_cached (ctx, &dg, rq, nonce) == HA_OK); /* 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 (!cached) { 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. */ ASSERT (ctx->f_validate_digest); r = ctx->f_validate_digest (rq, dg.client.username, &dg, &groups); if (r != HA_OK) RETURN (r); /* Add the digest record to the cache */ r = add_digest_rec (ctx, nonce, dg.client.username, &dg, groups); if (r != HA_OK) RETURN (r); /* We had a record so ... */ } else { /* Check the user name */ if (md5_strcmp (dg.server_userhash, dg.client.username) != 0) { ha_messagex(NULL, LOG_ERR, "digest response contains invalid username"); RETURN(HA_FALSE); } /* And do the validation ourselves */ 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; include_group_headers (ctx, rq, nonce); /* 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); finally: /* 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); ctx->cache_max = context->cache_max; } return HA_OK; } void bd_destroy(ha_context_t* context) { bd_context_t* ctx; 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; }