

/*=======================================================================
 * Copyright (c) 2000 MPL Communications Inc.        Author: Justin Wells
 * http://www.carlsononline.com                       justin@semiotek.com
 * ======================================================================
 *
 * Module Name: mod_ticket.c 
 *
 * Version:     1.0 ($Id: mod_ticket.c,v 1.2 2000/01/25 20:17:42 justin Exp $)
 *
 * Contributor: MPL Communications Inc. (http://www.carlsononline.com)
 *
 * Author:      Justin Wells             (justin@semiotek.com)
 *
 * Description: Check for a a digitally signed ticket in the URI, and
 *              if found make it available in the variable $TICKET 
 *
 * Motivation:  Allow passing authenticated sessions from one domain 
 *              to another in a secure fashion by way of a shared secret;
 *              track an http session through a site without using cookies
 *              in a manner which survives relative URL links.
 *
 * URI Format:  http://servername/$ticketname$ticketvalue$md5sum/URI
 *              where md5sum == md5sum(strcat(secret,ticketvalue,remote_ip))
 *
 * Directives : TicketKey NAME SECRET      define a ticketname 
 *              TicketDelim CHAR           use CHAR instead of $ 
 *              TicketSumLength NUMBER     minimum length of md5sum
 *              TicketCryptIP on|off       include remote IP in md5sum?
 *
 * Environment: $TICKET                    the value of the ticket
 *              $TICKET_NAME               the name of the ticket
 *              $TICKET_ERROR              reason for ignoring ticket
 *              $TICKET_SUM                the string used to generate md5sum
 * 
 * mod_ticket will only set the environment variables $TICKET and 
 * $TICKET_NAME if the md5sum in the ticket is valid. If a ticket is
 * rejected an explanation will appear in the variable $TICKET_ERROR.
 *
 * The TicketKey directive is used to specify ticket names. Each ticket
 * name has a secret phrase. When checking a request, the ticket value 
 * is appended to the ticket secret and an md5sum is computed. The 
 * computed sum must match the md5sum stored in the ticket.
 *
 * The TicketDelim directive allows you to specify a different delimiter
 * other than the default $. Any URI beginning with this delimiter will 
 * be inspected to see if it begins with a ticket.
 *
 * The TicketCryptIP directive allows you to turn on and off inclusion 
 * of the remote IP in the generated md5sum. Including the IP results 
 * in tickets that are valid for only one client.
 *
 * The TicketSumLength directive allows you to specify only a subset 
 * of the actual md5sum in the URL. This shortens the URL, and reduces
 * the cryptographic security of the scheme. If you specify a value of
 * 12, for example, then you only need the 12 rightmost digits of the
 * md5sum in the ticket (though you are allowed to use more). Setting
 * TicketSumLength to zero allows tickets with no authentication, 
 * though if a sum is specified it must still be valid. 
 *
 * =======================================================================
 * Copyright (c) 2000 MPL Communications Inc. All rights reserved. 
 * This software was written for MPL Communications Inc. by Justin Wells
 * and is hereby contributed to the Apache Software Foundation for 
 * distribution under the Apache license, as follows.
 *
 * 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. All advertising materials mentioning features or use of this
 *    software must display the following acknowledgment:
 *    "This product includes software developed by the Apache Group
 *    for use in the Apache HTTP server project (http://www.apache.org/)."
 *
 * 4. The names "Apache Server" and "Apache Group" 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 names without prior written
 *    permission of the Apache Group.
 *
 * 6. Redistributions of any form whatsoever must retain the following
 *    acknowledgment:
 *    "This product includes software developed by the Apache Group
 *    for use in the Apache HTTP server project (http://www.apache.org/)."
 *
 * THIS SOFTWARE IS PROVIDED BY THE APACHE GROUP ``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 GROUP 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 Group and was originally based
 * on public domain software written at the National Center for
 * Supercomputing Applications, University of Illinois, Urbana-Champaign.
 * For more information on the Apache Group and the Apache HTTP server
 * project, please see <http://www.apache.org/>.
 *
 */

#include "httpd.h"
#include "http_config.h"
#include "http_core.h"
#include "http_log.h"
#include "http_main.h"
#include "http_protocol.h"
#include "util_script.h"
#include "util_md5.h"

#include <stdio.h>

/*
 * Declare ourselves so the configuration routines can find and know us.
 * We'll fill it in at the end of the module.
 */
module ticket_module;

/*--------------------------------------------------------------------------*/
/*                                                                          */
/* CONFIGURATION HANDLING--build up the key_node list                       */
/*                                                                          */
/*--------------------------------------------------------------------------*/


#define TICKET_DELIM '$'
#define SUM_LENGTH 32

/*
 * Configuration for this module consists of a list of ticket_records. Each
 * record supplies the data for one passphrase, along with a pointer to 
 * the next ticket record. This is a linked list. The first node in the 
 * list is a dummy record. The subsequent nodes are the config data. 
 * New data is added to the start of the list. Data is merged from a parent
 * config by sharing the tail of a list, and then appending the override 
 * data to the start of the list.
 *
 * Our main hook is URI rewriting, which is called before per-directory 
 * configuration steps are performed, therefore we can only use server conf.
 */

typedef struct key_node
{
    char *name;
    char *phrase;
    struct key_node *next;
}
key_node;


typedef struct ticket_conf
{
    key_node *head;
    char delimiter;
    int md5length;
    int cryptip;
}
ticket_conf;


/**
  * Internal method--create a new key node
  */
key_node *new_key_node(pool * p, char *name, char *phrase)
{
    key_node *new = (key_node *) ap_palloc(p, sizeof(key_node));;
    new->name = name;
    new->phrase = phrase;
    new->next = NULL;
    return new;
}

/**
  * Internal method--insert a key node into a key node list
  */
void insert_key_node(key_node * head, key_node * new)
{
    new->next = head->next;
    head->next = new;
}



/**
  * Create a new config structure, being a list of key_node. This
  * list begins with a dummy node. This is the server config.
  */
static void *ticket_create_sconfig(pool * p, server_rec * s)
{
    ticket_conf *conf = ap_palloc(p, sizeof(ticket_conf));
    conf->delimiter = TICKET_DELIM;
    conf->md5length = SUM_LENGTH;
    conf->head = new_key_node(p, NULL, NULL);
    conf->cryptip = 1;
    return conf;
}


/**
  * Merge the config structure for this location with its parents. This 
  * means attaching the parents list to the end of the childs list. 
  */
static void *ticket_merge_sconfig(pool * p, void *parent_conf, void *sub_conf)
{

    ticket_conf *par = (ticket_conf *) parent_conf;
    ticket_conf *sub = (ticket_conf *) sub_conf;
    ticket_conf *conf = ap_palloc(p, sizeof(ticket_conf));
    key_node *phead = ((ticket_conf *) parent_conf)->head;
    key_node *shead = ((ticket_conf *) sub_conf)->head;
    key_node *head = new_key_node(p, NULL, NULL);
    key_node *n = NULL;

    conf->delimiter = sub->delimiter;
    conf->md5length = sub->md5length;
    conf->cryptip = sub->cryptip;

    /* parent list at end, unmodified, sub list inserted ahead of that */
    conf->head->next = phead->next;
    while (shead->next != NULL) {
	shead = shead->next;
	n = new_key_node(p, shead->name, shead->phrase);
	insert_key_node(conf->head, n);
    }
    return conf;
}


/**
  * Add a new key to the list of ticket records, at the beginning, after
  * the initial dummy node.
  */
static const char *ticket_handle_key(cmd_parms * cmd, void *mconfig,
				     char *keyname, char *keyphrase)
{

    server_rec *s = cmd->server;
    ticket_conf *conf =
	(ticket_conf *) ap_get_module_config(s->module_config,
					     &ticket_module);

    key_node *n = new_key_node(cmd->pool, keyname, keyphrase);

    if (!keyname || !keyphrase) {
	return "You must specify both a keyname and a keyphrase";
    }
    insert_key_node(conf->head, n);
    return NULL;
}

/**
  * Set the delimiter character used to separate the tickets. By default
  * this is the $ character, but you can set this to another character
  * if you prefer something different. Do not use / or ~ or something 
  * meaningful to another module or in a URL.
  */
static const char *ticket_set_delim(cmd_parms * cmd, void *mconfig,
				    char *delimiter)
{

    server_rec *s = cmd->server;
    char delim;
    ticket_conf *conf =
	(ticket_conf *) ap_get_module_config(s->module_config,
					     &ticket_module);

    if (delimiter) {
	delim = delimiter[0];
	switch (delim) {
	case '\0':
	case '/':
	case '\\':
	case ':':
	case ' ':
	case '	':
	case '#':
	    return "Illegal character specified as ticket delimiter";
	}
	conf->delimiter = delim;
    }
    else {
	"Ticket delimiter directive incorrectly specified";
    }
    return NULL;
}

/**
  * Set the length of the MD5SUM used to generate tickets. A full MD5SUM
  * will be generated, but you may wish to reduce the length of the 
  * resulting URLs (and thus reduce the security of the scheme) by 
  * using only part of the computed sum. mod_ticket will only use this
  * many characters from the __end__ of the md5sum. By default it is
  * a 12 character checksum. It can range from 0 to 32. Setting it 
  * to zero turns off authentication checking. 
  */
static const char *ticket_set_sumlength(cmd_parms * cmd, void *mconfig,
					char *length)
{
    int len = atoi(length);

    server_rec *s = cmd->server;
    ticket_conf *conf =
	(ticket_conf *) ap_get_module_config(s->module_config,
					     &ticket_module);
    if ((len >= 0) && (len <= 32)) {
	conf->md5length = len;
    }
    else {
	return "Ticket MD5 length must be between 0 and 32";
    }
    return NULL;
}


/**
 * Set whether or not the IP number is crypted into the md5sum or not.
 * The default is that it is.
 */
static const char *ticket_set_cryptip(cmd_parms * cmd, void *mconfig,
				      int bool)
{
    server_rec *s = cmd->server;
    ticket_conf *conf =
	(ticket_conf *) ap_get_module_config(s->module_config,
					     &ticket_module);
    conf->cryptip = bool;
    return NULL;
}

/*--------------------------------------------------------------------------*/
/*                                                                          */
/* REQUEST PROCESSING -- Handle requests and extract our ticket from the    */
/*                       URL if it is there                                 */
/*                                                                          */
/*--------------------------------------------------------------------------*/


/*
 * This routine gives our module an opportunity to translate the URI into an
 * actual filename.  If we don't do anything special, the server's default
 * rules (Alias directives and the like) will continue to be followed.
 *
 * The return value is OK, DECLINED, or HTTP_mumble.  If we return OK, no
 * further modules are called for this phase.
 */
static int ticket_translate_handler(request_rec * r)
{

    void *sconf = r->server->module_config;
    char *ticket, *ticket_value, *ticket_name, *ticket_sum;
    char *sum, *md5string;
    ticket_conf *conf =
	(ticket_conf *) ap_get_module_config(sconf, &ticket_module);
    key_node *keynode = conf->head;

    char *name = r->uri;
    const char *filename;
    char *newfilename;
    int len = 0;

    /*
     * If the name doesn't match our pattern, we ignore it
     */
    if ((keynode == NULL) || (keynode->next == NULL) ||
	(name[0] != '/') || (name[1] != conf->delimiter)) {
	return DECLINED;
    }

    filename = name + 2;

    /*
     * Advance filename so that the / following our ticket, if there was one 
     * is at the head of filename. 
     */
    ticket = ap_getword(r->pool, &filename, '/');
    if (filename[-1] == '/') {
	--filename;
    }

    /*
     * Check whether we got a ticket
     */
    if (ticket[0] == '\0') {
	return DECLINED;
    }

   /**
     * Fixup the URI and filename so that the ticket was never there
     */
    newfilename = ap_pstrdup(r->pool, filename);
    r->filename = newfilename;
    r->uri = ap_pstrdup(r->pool, newfilename);

    ticket_name = ticket;

    ticket_value = strchr(ticket, conf->delimiter);
    if (!ticket_value) {
	ap_table_setn(r->subprocess_env, "TICKET_ERROR",
		      "Supplied ticket does not have a value!");
	return DECLINED;
    }
    ticket_value[0] = '\0';
    ticket_value++;

    /* If we need an md5sum (conf length > 0) make sure we got one */
    ticket_sum = strchr(ticket_value, conf->delimiter);
    if (ticket_sum) {
	ticket_sum[0] = '\0';
	ticket_sum++;
	len = strlen(ticket_sum);
	if (len < conf->md5length) {
	    ap_table_setn(r->subprocess_env, "TICKET_ERROR",
			  "Supplied md5sum was not long enough");
	    return DECLINED;
	}
	if (len > 32) {
	    len = 32;
	}
    }
    else if (conf->md5length) {
	ap_table_setn(r->subprocess_env, "TICKET_ERROR",
		      "Supplied ticket did not have an md5sum");
	return DECLINED;
    }

    while (keynode->next) {
	keynode = keynode->next;
	if (keynode->name && (strcmp(ticket_name, keynode->name) == 0)) {
	    if (len > 0) {
		md5string = ap_pstrcat(r->pool, keynode->phrase, ticket_value,
				       (conf->cryptip ? r->connection->
					remote_ip : NULL), NULL);
		ap_table_setn(r->subprocess_env, "TICKET_SUM", md5string);

		sum = ap_md5(r->pool, md5string);

		sum = sum + (32 - len);

		if (ticket_sum && strcmp(sum, ticket_sum) != 0) {
		    ap_table_setn(r->subprocess_env, "TICKET_ERROR",
				  "Ticket failed md5sum check");
		    return DECLINED;
		}
	    }
	    ap_table_setn(r->subprocess_env, "TICKET_NAME", ticket_name);
	    ap_table_setn(r->subprocess_env, "TICKET", ticket_value);
	    return DECLINED;
	}
    }

    ap_table_setn(r->subprocess_env, "TICKET_ERROR",
		  "Ticket NAME did not match any of the available keys");
    return DECLINED;
}



/*--------------------------------------------------------------------------*/
/*                                                                          */
/* MODULE DEFINITION -- these tables define the content of the module       */
/*                                                                          */
/*--------------------------------------------------------------------------*/

static const command_rec ticket_cmds[] = {
    {
     "TicketKey",		/* directive name */
     ticket_handle_key,		/* config action routine */
     NULL,			/* argument to include in call */
     RSRC_CONF,			/* in .htaccess if AllowOverride Options */
     TAKE2,			/* arguments */
     "Define a key for decrypting URL tickets. Two arguments: name password"
     /* directive description */
     },
    {
     "TicketDelim",		/* directive name */
     ticket_set_delim,		/* config action routine */
     NULL,			/* argument to include in call */
     RSRC_CONF,			/* in .htaccess if AllowOverride Options */
     TAKE1,			/* arguments */
     "Define the delimiter character (eg: $) used to delimit tickets\n"
     /* directive description */
     },
    {
     "TicketSumLength",		/* directive name */
     ticket_set_sumlength,	/* config action routine */
     NULL,			/* argument to include in call */
     RSRC_CONF,			/* in .htaccess if AllowOverride Options */
     TAKE1,			/* arguments */
     "The length of the md5-based checksum used in a ticket (up to 32)\n"
     /* directive description */
     },
    {
     "TicketCryptIP",		/* directive name */
     ticket_set_cryptip,	/* config action routine */
     NULL,			/* argument to include in call */
     RSRC_CONF,			/* in .htaccess if AllowOverride Options */
     FLAG,			/* arguments */
     "Does the md5sum also hash the IP number of the request?\n"
     /* directive description */
     },
    {NULL}
};


module ticket_module = {
    STANDARD_MODULE_STUFF,
    NULL,			/* module initializer */
    NULL,			/* per-directory config creator */
    NULL,			/* dir config merger */
    ticket_create_sconfig,	/* server config creator */
    ticket_merge_sconfig,	/* server config merger */
    ticket_cmds,		/* command table */
    NULL,			/* [7] list of handlers */
    ticket_translate_handler,	/* [2] filename-to-URI translation */
    NULL,			/* [5] check/validate user_id */
    NULL,			/* [6] check user_id is valid *here* */
    NULL,			/* [4] check access by host address */
    NULL,			/* [7] MIME type checker/setter */
    NULL,			/* [10] logger */
    NULL,			/* [3] header parser */
    NULL,			/* process initializer */
    NULL,			/* process exit/cleanup */
    NULL,			/* [1] post read_request handling */
};
