/* 
 * mod_authenticache.c -- Apache auth caching module.
 *
 * If we cache the auth info, we make less trips to the database, slapd,
 * pdc, etc...  It also allows us to do crazy things like auth against an
 * fwtk authsrv or something.
 * 
 * ====================================================================
 * The Apache Software License, Version 1.1
 *
 * Copyright (c) 2002 Anthony D. Urso.  All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 *
 * 3. The end-user documentation included with the redistribution,
 *    if any, must include the following acknowledgment:
 *       "This product includes software developed by the
 *        Apache Software Foundation (http://www.apache.org/)."
 *    Alternately, this acknowledgment may appear in the software itself,
 *    if and wherever such third-party acknowledgments normally appear.
 *
 * 4. The names "Apache" and "Apache Software Foundation" must
 *    not be used to endorse or promote products derived from this
 *    software without prior written permission. For written
 *    permission, please contact apache@apache.org.
 *
 * 5. Products derived from this software may not be called "Apache",
 *    nor may "Apache" appear in their name, without prior written
 *    permission of the Apache Software Foundation.
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED.  IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
 * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 * ====================================================================
 *
 * This software consists of voluntary contributions made by many
 * individuals on behalf of the Apache Software Foundation.  For more
 * information on the Apache Software Foundation, please see
 * <http://www.apache.org/>.
 *
 * Portions of this software are based upon public domain software
 * originally written at the National Center for Supercomputing Applications,
 * University of Illinois, Urbana-Champaign.
 *
 * $Id: mod_authenticache.c,v 1.3 2004/03/06 01:46:59 anthonyu Exp $
 *
 */

#include <time.h> /* for time_t */

#include "httpd.h"
#include "http_config.h"
#include "http_request.h"
#include "http_log.h"

#include "apr.h"
#include "apr_base64.h"
#include "apr_optional.h"
#include "apr_strings.h"

#ifdef AUTHENTICACHE_USE_SHM
#	include "apr_shm.h"
#endif

#include "util_md5.h"

#include "defines.h"


module AP_MODULE_DECLARE_DATA authenticache_module;

#ifdef AUTHENTICACHE_USE_SHM
	static apr_shm_t *authenticache_keys = NULL;
#else
	static unsigned char *authenticache_keys;
#endif

static const command_rec authenticache_configs[] = {
	AP_INIT_FLAG("Authenticache",
		ap_set_flag_slot,
		(void *)APR_OFFSETOF(authenticache_cfg, authenticache_on),
		OR_AUTHCFG,
		"When \"on\", cache successful authentication attempts. "
			"Default: off."),

	AP_INIT_FLAG("AuthenticacheAuthoritative",
		ap_set_flag_slot,
		(void *)APR_OFFSETOF(authenticache_cfg,
					authenticache_authoritative),
		OR_AUTHCFG,
		"This should always be off. Default: off."),

	AP_INIT_TAKE1("AuthenticacheTTL",
		ap_set_int_slot,
		(void *)APR_OFFSETOF(authenticache_cfg, authenticache_ttl),
		OR_AUTHCFG,
		"Time, in seconds, before expiration of cached auth "
			"info. Default: 600."),

	AP_INIT_TAKE1("AuthenticacheMaxTTL",
		ap_set_int_slot,
		(void *)APR_OFFSETOF(authenticache_cfg, authenticache_maxttl),
		OR_AUTHCFG,
		"Maximum time, in seconds, before expiration of cached auth "
			"info. Default: 3600."),

	AP_INIT_TAKE1("AuthenticacheTicketName",
		ap_set_string_slot,
		(void *)APR_OFFSETOF(authenticache_cfg, authenticache_tname),
		OR_AUTHCFG,
		"Name of cookie to sent to browser.  Should be unique amongst "
			"cookies across the virtual host. "
				"Default: AuthentiCache"),

	AP_INIT_TAKE1("AuthenticacheTicketPath",
		ap_set_string_slot,
		(void *)APR_OFFSETOF(authenticache_cfg, authenticache_tpath),
		OR_AUTHCFG,
		"Path of cookie to sent to browser. Default: /"),

	AP_INIT_FLAG("AuthenticacheVerifyIP",
		ap_set_flag_slot,
		(void *)APR_OFFSETOF(authenticache_cfg,
				     authenticache_verify_ip),
		OR_AUTHCFG,
		"When \"on\", verify ticket is returned from the IP we sent "
		       "it to. Default: off"),

	{NULL}
};


/*
 * Here we shall create the two keys that we'll use for the lifetime of the
 * server process...  This is cheaper than makeing the two keys for each
 * connection.
 */
static int authenticache_init(apr_pool_t *p, apr_pool_t *plog,
		apr_pool_t *ptemp, server_rec *srec)
{

	unsigned char *key1, *key2;
#ifdef DEV_RANDOM
	apr_size_t size;
	apr_file_t *fp;
#endif
	int i;


	ap_add_version_component(p, "AuthentiCache/" VERSION);

#ifdef AUTHENTICACHE_USE_SHM
	if (apr_shm_create(&authenticache_keys, SIZE * 2, NULL, p)) {
		ap_log_error(APLOG_MARK, APLOG_EMERG, 0, srec,
			"mod_authenticache: unable to create shm segment");
		return DECLINED;
	}

	key1 = (unsigned char *)apr_shm_baseaddr_get(authenticache_keys);
#else
	authenticache_keys = (unsigned char *)apr_palloc(p, SIZE * 2);

	key1 = authenticache_keys;
#endif /* AUTHENTICACHE_USE_SHM */

	key2 = key1 + SIZE;

#if APR_HAS_RANDOM
	if (apr_generate_random_bytes(key1, SIZE)) {
		ap_log_error(APLOG_MARK, APLOG_EMERG, 0, srec,
			"mod_authenticache: unable to generate random keys");
		return DECLINED;
	}
#elif defined(DEV_RANDOM)
#	warning "/dev/random code is untested"
	size = SIZE;

	apr_file_open(&fp, DEV_RANDOM, APR_READ, 0600, p);
	apr_file_read(fp, key1, &size);
	apr_file_close(fp);
#else
	for (i = 0; i < SIZE; i++)
		key1[i] = randbyte();
#endif

	/* Magic bytes from RFC 2107... */
	for (i = 0; i < SIZE; i++) {
		key2[i] = key1[i] ^ 0x5c;
		key1[i] ^= 0x36;
	}

	return OK;

}


/*
 * Here we create the default config for those entries not specified in an
 * .htaccess file.
 */
static void *authenticache_defaults(apr_pool_t *p, char *dir)
{

	authenticache_cfg *c;


	c = (authenticache_cfg *)apr_palloc(p, sizeof(authenticache_cfg));

	c->authenticache_on = 0;
	c->authenticache_authoritative = 0;
	c->authenticache_ttl = 600;
	c->authenticache_maxttl = 3600;
	c->authenticache_tname = "AuthentiCache";
	c->authenticache_tpath = "/";
	c->authenticache_verify_ip = 0;

	return (void *)c;

}


static apr_status_t authenticache_auth(request_rec *r)
{

	authenticache_cfg *c;
	apr_table_t *ttab;


	c = (authenticache_cfg *)ap_get_module_config(r->per_dir_config,
			&authenticache_module);

	if (!c->authenticache_on)
		return DECLINED;

	ttab = apr_table_make(r->pool, 5);

	/* Parse out the cookie for me... */
	if (!authenticache_parse_cookie(r, c, ttab))
		return DECLINED;

	/* Test the cookie... If it is old or invalid, delete it and return
	 * an error condition. */
	if (!authenticache_valid_ticket(r, c, ttab)) {
		return (c->authenticache_authoritative) ?
			HTTP_UNAUTHORIZED : DECLINED;
	}

	/* This ticket passed all checks, set the connection user to the
	 * ticket's username and log that fact. */
	r->user = (char *)apr_table_get(ttab, "user");
	ap_log_rerror(APLOG_MARK, APLOG_INFO|APLOG_NOERRNO, 0, r,
			"mod_authenticache: valid ticket from %s for %s",
			r->user, r->uri);

	/* Renew the ticket. */
	authenticache_set_cookie(r, c->authenticache_tname,
			c->authenticache_tpath, c->authenticache_ttl,
			(char *)apr_table_get(ttab, "first"));

	return OK;

}


/*
 * We are in the fixup stage of the transaction.  We only use this
 * opportunity to set a cookie since we know the user has already been
 * authenticated by another module. 
 */
static int authenticache_cache(request_rec *r)
{

	authenticache_cfg *c;
	const char *hdr;


	c = (authenticache_cfg *)ap_get_module_config(r->per_dir_config,
			&authenticache_module);

	if (!c->authenticache_on)
		return DECLINED;

	if (!ap_is_initial_req(r))
		return DECLINED;

	if ((!r->user) || !strlen(r->user))
		return DECLINED;

	if ((hdr = apr_table_get(r->headers_out, "Set-Cookie")) != NULL)
		if (strstr(hdr, c->authenticache_tname))
			return DECLINED;

	/* The fact that r->connection->user is defined means that some
	 * other module authenticated this user.  Let's give browser
	 * a cookie for future connections. */
	authenticache_set_cookie(r, c->authenticache_tname,
			c->authenticache_tpath, c->authenticache_ttl, NULL);

	return DECLINED;

}


/*
 * This funtion requests that a cookie be parsed, then validates the
 * contents.
 */
static int authenticache_valid_ticket(request_rec *r, authenticache_cfg *c,
					apr_table_t *ttab)
{

	unsigned char *key1, *key2;
	char *ticket, *hmac;
	time_t timelst, time1st, timenow;


	/* then put it back the way it was. */
	ticket = apr_pstrcat(r->pool, apr_table_get(ttab, "user"), "|",
			apr_table_get(ttab, "last"), "|",
			apr_table_get(ttab, "first"), "|",
			apr_table_get(ttab, "address"), NULL);

#ifdef AUTHENTICACHE_USE_SHM
	key1 = (unsigned char *)apr_shm_baseaddr_get(authenticache_keys);
#else
	key1 = authenticache_keys;
#endif /* AUTHENTICACHE_USE_SHM */

	key2 = key1 + SIZE;

	/* Do an HMAC of the sent values... */
	hmac = ap_hmac_md5(r->pool, ticket, key1, key2);

	/* and make sure it matches the sent signature. */
	if (strcmp(hmac, apr_table_get(ttab, "hmac")))
		return 0;

	/* Get the sent time, configured TTL and now. */
	timelst = (time_t)strtol(apr_table_get(ttab, "last"),
			(char **)NULL, 10);
	time1st = (time_t)strtol(apr_table_get(ttab, "first"),
			(char **)NULL, 10);
	timenow = time(NULL);

	/* Drop any stale cookies, or ones that were sent in the future. */
	if ((timenow > (time1st + c->authenticache_maxttl)) ||
			(timenow > (timelst + c->authenticache_ttl)) ||
			(timenow < time1st) || (timenow < timelst)) 
		return 0;

	/* If we're asked to check the IP, let's do so. If this cookie
	 * were sent over a clear channel, it could be stolen.  Then again,
	 * it would be easier to sniff the Basic auth info and
	 * authenticate using that... */
	if ((c->authenticache_verify_ip) &&
			(strcmp(apr_table_get(ttab, "address"),
					r->connection->remote_ip)))
		return 0;

	return 1;

}


static char *authenticache_hmac(apr_pool_t *p, char *text)
{

	unsigned char *key1, *key2;


#ifdef AUTHENTICACHE_USE_SHM
	key1 = (unsigned char *)apr_shm_baseaddr_get(authenticache_keys);
#else
	key1 = authenticache_keys;
#endif /* AUTHENTICACHE_USE_SHM */

	key2 = key1 + SIZE;

	return ap_hmac_md5(p, text, key1, key2);

}


static char *ap_hmac_md5(apr_pool_t *p, char *text,
		unsigned char *k0, unsigned char *k1)
{

	apr_md5_ctx_t ctx;
	unsigned char *digest;


	if (!k0)
		return k0;

	if (!k1) {
		unsigned int i;

		if ((k1 = apr_palloc(p, SIZE)) == NULL)
			return k1;
		
		memcpy(k1, k0, SIZE);

		/* Magic bytes from RFC 2104... */
		for (i = 0; i < SIZE; i++) {
			k0[i] ^= 0x36;
			k1[i] ^= 0x5c;
		}
	}

	if ((digest = apr_palloc(p, MD5_DIGESTSIZE)) == NULL)
		return digest;

	/* perform inner MD5 */
	apr_md5_init(&ctx);
	apr_md5_update(&ctx, k0, SIZE);
	apr_md5_update(&ctx, text, strlen(text));
	apr_md5_final(digest, &ctx);

	/* perform outer MD5 */
	apr_md5_init(&ctx);
	apr_md5_update(&ctx, k1, SIZE);
	apr_md5_update(&ctx, digest, MD5_DIGESTSIZE);

	return ap_md5contextTo64(p, &ctx);

}


/*
 * This function retrieves the authenticache cookie, parses out the fields,
 * and returns the info in a table provided by the caller.
 */
static int authenticache_parse_cookie(request_rec *r, authenticache_cfg *c,
		apr_table_t *ttab)
{

	const char *cookie;
	char *name, *info, *buf, *end;
	size_t infolen;

	char reject[] = { ' ', '\t', '\r', '\n', '\0' };


	if ((cookie = apr_table_get(r->headers_in, "Cookie")) == NULL)
		return 0;
	
	/* Is our cookie even in there? Point to it. */
	if ((name = strstr(cookie, c->authenticache_tname)) == NULL)
		return 0;

	/* Find the start of the cookie data. */
	if ((info = strchr(name, '=')) == NULL)
		return 0;

	/* Get the length of the coookie data. */
	if ((infolen = strcspn(++info, reject)) < 4) /* min size of base64 */
		return 0;

	/* Terminate the string. */
	info[infolen] = 0;

	if ((buf = apr_pcalloc(r->pool, apr_base64_decode_len(info))) == NULL)
		return 0;

	/* Base64 decode the ticket data. */
	if (apr_base64_decode(buf, info) < 26) /* smallest ticket */
		return 0;

	/* These stanzas populate the table with ticket bits. */
	if ((end = strchr(buf, '|')) != NULL) {
		*end = 0;
		apr_table_setn(ttab, "user", buf);
		buf = ++end;
	} else
		return 0;

	if ((end = strchr(buf, '|')) != NULL) {
		*end = 0;
		apr_table_setn(ttab, "last", buf);
		buf = ++end;
	} else
		return 0;

	if ((end = strchr(buf, '|')) != NULL) {
		*end = 0;
		apr_table_setn(ttab, "first", buf);
		buf = ++end;
	} else
		return 0;

	if ((end = strchr(buf, '|')) != NULL) {
		*end = 0;
		apr_table_setn(ttab, "address", buf);
		buf = ++end;
	} else 
		return 0;

	if (strlen(buf)) 
		apr_table_setn(ttab, "hmac", buf);
	else
		return 0;

	return 1;

}


/*
 * Given that this user already authenticated, lets give them a cookie to
 * show for it.
 */
static int authenticache_set_cookie(request_rec *r, char *name, char *path,
		unsigned int ttl, char *first)
{

	char *timenow, *ticket, *hmac, *infobuf, *info, *date, *cookie;
	unsigned char *key1, *key2;


	/* Get the current time as a string. */
	if ((timenow = apr_psprintf(r->pool, "%ld", (long)time(NULL))) == NULL)
		return 0;

	if (first == NULL) 
		first = timenow;

	/* Make the ticket part. Looks like "username|now|first|ipaddr." */
	if ((ticket = apr_pstrcat(r->pool, r->user, "|", timenow, "|",
			first, "|",  r->connection->remote_ip, NULL)) == NULL)
		return 0;

#ifdef AUTHENTICACHE_USE_SHM
	key1 = (unsigned char *)apr_shm_baseaddr_get(authenticache_keys);
#else
	key1 = authenticache_keys;
#endif /* AUTHENTICACHE_USE_SHM */

	key2 = key1 + SIZE;


	/* Make the signature. */
	if ((hmac = ap_hmac_md5(r->pool, ticket, key1, key2)) == NULL)
		return 0;

	/* Append the signature to the ticket and Base64 encode it. */
	if ((infobuf = apr_pstrcat(r->pool, ticket, "|", hmac, NULL)) == NULL)
		return 0;
	if ((info = apr_pcalloc(r->pool,
			apr_base64_encode_len(strlen(infobuf)) + 1)) == NULL)
		return 0;
	apr_base64_encode(info, infobuf, strlen(infobuf));

	date = (char *)apr_palloc(r->pool, APR_RFC822_DATE_LEN);
	apr_rfc822_date(date, apr_time_now() + (ttl * APR_USEC_PER_SEC));

	/* This is awful, but it works... */
	date[7] = '-';
	date[11] = '-';

	/* Make it look like a cookie. */
	if ((cookie = apr_pstrcat(r->pool, name, "=", info, "; path=", path,
					"; expires=", date, NULL)) == NULL)
		return 0;

	/* And set the Cookie: header. */
	apr_table_setn(r->headers_out, "Set-Cookie", cookie);

	return 1;

}

/* Register hooks. :) */
static void authenticache_register_hooks(apr_pool_t *p)
{

	APR_REGISTER_OPTIONAL_FN(authenticache_hmac);
	APR_REGISTER_OPTIONAL_FN(authenticache_set_cookie);

	ap_hook_post_config(authenticache_init, NULL, NULL, APR_HOOK_MIDDLE);
	ap_hook_check_user_id(authenticache_auth, NULL, NULL,
							APR_HOOK_REALLY_FIRST);
	ap_hook_fixups(authenticache_cache, NULL, NULL, APR_HOOK_MIDDLE);

}

/* Dispatch list for API hooks */ 
module AP_MODULE_DECLARE_DATA authenticache_module = {
	STANDARD20_MODULE_STUFF, 
	authenticache_defaults,
	NULL,
	NULL,
	NULL,
	authenticache_configs,
	authenticache_register_hooks
};
