"Fossies" - the Fresh Open Source Software archive

Member "httpauth-0.9.5/daemon/bd.c" of archive httpauth-0.9.5.tar.gz:


/*
 * 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, &stale);
	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;
}