
/* ====================================================================
 * Copyright (c) 1995 The Apache Group.  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. 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.
 *
 * 5. 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/>.
 *
 */


/*
 * This is an EXPERIMENTAL module, which implements the TransferLog directive
 * (same as the common log module), and an additional directive, LogFormat.
 * Bugs would not surprise me.
 *
 * The argument to LogFormat is a string, which can include literal
 * characters copied into the log files, and '%' directives as follows:
 *
 * %...h:  remote host
 * %...l:  remote logname (from identd, if supplied)
 * %...u:  remote user (from auth; may be bogus if return status (%s) is 401)
 * %...t:  time, in common log format time format
 * %...r:  first line of request
 * %...s:  status.  For requests that got internally redirected, this
 *         is status of the *original* request --- %...>s for the last.
 * %...b:  bytes sent.
 * %...{Foobar}i:  The contents of Foobar: header line(s) in the request
 *                 sent to the client.
 * %...{Foobar}o:  The contents of Foobar: header line(s) in the reply.
 *
 * The '...' can be nothing at all (e.g. "%h %u %r %s %b"), or it can
 * indicate conditions for inclusion of the item (which will cause it
 * to be replaced with '-' if the condition is not met).  Note that
 * there is no escaping performed on the strings from %r, %...i and
 * %...o; some with long memories may remember that I thought this was
 * a bad idea, once upon a time, and I'm still not comfortable with
 * it, but it is difficult to see how to "do the right thing" with all
 * of '%..i', unless we URL-escape everything and break with CLF.
 *
 * The forms of condition are a list of HTTP status codes, which may
 * or may not be preceded by '!'.  Thus, '%400,501{User-agent}i' logs
 * User-agent: on 400 errors and 501 errors (Bad Request, Not
 * Implemented) only; '%!200,304,302{Referer}i' logs Referer: on all
 * requests which did *not* return some sort of normal status.
 *
 * The default LogFormat reproduces CLF; see below.
 *
 * The way this is supposed to work with virtual hosts is as follows:
 * a virtual host can have its own LogFormat, or its own TransferLog.
 * If it doesn't have its own LogFormat, it inherits from the main
 * server.  If it doesn't have its own TransferLog, it writes to the
 * same descriptor (meaning the same process for "| ...").
 *
 * That means that you can do things like:
 *
 * <VirtualHost hosta.com>
 * LogFormat "hosta ..."
 * ...
 * </VirtualHost>
 *
 * <VirtualHost hosta.com>
 * LogFormat "hostb ..."
 * ...
 * </VirtualHost>
 *
 * ... to have different virtual servers write into the same log file,
 * but have some indication which host they came from, though a %v
 * directive may well be a better way to handle this.
 *
 * --- rst
 */

#define DEFAULT_LOG_FORMAT "%h %l %u %t \"%r\" %s %b"

#include "httpd.h"
#include "http_config.h"

module config_log_module;

static int xfer_flags = ( O_WRONLY | O_APPEND | O_CREAT );
#ifdef __EMX__
/* OS/2 dosen't support users and groups */
static mode_t xfer_mode = ( S_IREAD | S_IWRITE );
#else
static mode_t xfer_mode = ( S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH );
#endif

typedef struct {
    char *fname;
    array_header *format;
    int log_fd;
} config_log_state;

/*
 * Format items...
 */

typedef char *(*item_key_func)(request_rec *, char *);

typedef struct {
    item_key_func func;
    char *arg;
    int condition_sense;
    int want_orig;
    array_header *conditions;
} log_format_item;

char *format_integer(pool *p, int i)
{
    char dummy[40];
    sprintf (dummy, "%d", i);
    return pstrdup (p, dummy);
}

static char *pfmt(pool *p, int i)
{
    if (i <= 0) return "-";
    else return format_integer (p, i);
}

char *constant_item (request_rec *dummy, char *stuff) { return stuff; }

char *log_remote_host (request_rec *r, char *a)
{ return r->connection->remote_name; }

char *log_remote_logname(request_rec *r, char *a)
{return r->connection->remote_logname;}

char *log_remote_user (request_rec *r, char *a)
{ return r->connection->user; }

char *log_request_line (request_rec *r, char *a)
{ return r->the_request; }

char *log_status (request_rec *r, char *a)
{ return pfmt(r->pool, r->status); }

char *log_bytes_sent (request_rec *r, char *a)
{ return pfmt (r->pool, r->bytes_sent); }

char *log_header_in (request_rec *r, char *a)
{ return table_get (r->headers_in, a); }

char *log_header_out (request_rec *r, char *a)
{
    char *cp = table_get (r->headers_out, a);
    if (cp) return cp;
    return table_get (r->err_headers_out, a);
}

char *log_env_var (request_rec *r, char *a)
{ return table_get (r->subprocess_env, a); }

char *log_request_time (request_rec *r, char *a)
{
    long timz;
    struct tm *t;
    char tstr[MAX_STRING_LEN],sign;
    
    t = get_gmtoff(&timz);
    sign = (timz < 0 ? '-' : '+');
    if(timz < 0) 
        timz = -timz;

    strftime(tstr,MAX_STRING_LEN,"[%d/%b/%Y:%H:%M:%S ",t);

    sprintf (tstr + strlen(tstr), "%c%02ld%02ld]",
	     sign, timz/3600, timz%3600);

    return pstrdup (r->pool, tstr);
}

/*****************************************************************
 *
 * Parsing the log format string
 */

struct log_item_list {
    char ch;
    item_key_func func;
    int want_orig_default;
} log_item_keys[] = {
    { 'h', log_remote_host, 0 },
    { 'l', log_remote_logname, 0 },
    { 'u', log_remote_user, 0 },
    { 't', log_request_time, 0 },
    { 'r', log_request_line, 1 },
    { 's', log_status, 1 },
    { 'b', log_bytes_sent, 0 },
    { 'i', log_header_in, 0 },
    { 'o', log_header_out, 0 },
    { 'e', log_env_var, 0 },
    { '\0' }
};

struct log_item_list  *find_log_func (char k)
{
    int i;

    for (i = 0; log_item_keys[i].ch; ++i)
	if (k == log_item_keys[i].ch)
	    return &log_item_keys[i];

    return NULL;
}

char *log_format_substring (pool *p, char *start, char *end)
{
    char *res = palloc (p, end - start + 1);
    strncpy (res, start, end - start);
    res[end - start] = '\0';
    return res;
}

char *parse_log_misc_string (pool *p, log_format_item *it, char **sa)
{
    char *s = *sa;
    
    it->func = constant_item;
    it->conditions = NULL;

    while (*s && *s != '%') ++s;
    it->arg = log_format_substring (p, *sa, s);
    *sa = s;
    
    return NULL;
}

char *parse_log_item (pool *p, log_format_item *it, char **sa)
{
    char *s = *sa;
    if (*s != '%') return parse_log_misc_string (p, it, sa);

    ++s;
    it->condition_sense = 0;
    it->conditions = NULL;
    it->want_orig = -1;
    it->arg = "";		/* For safety's sake... */

    while (*s) {
	int i;
	struct log_item_list *l;
	
	switch (*s) {
	case '!':
	    ++s;
	    it->condition_sense = !it->condition_sense;
	    break;

	case '<':
	    ++s;
	    it->want_orig = 1;
	    break;

	case '>':
	    ++s;
	    it->want_orig = 0;
	    break;
	    
	case ',':
	    ++s;
	    break;

	case '{':
	    ++s;
	    it->arg = getword (p, &s, '}');
	    break;
	    
	case '0': case '1': case '2': case '3': case '4': 
	case '5': case '6': case '7': case '8': case '9':
	    i = *s - '0';
	    while (isdigit (*++s)) i = i * 10 + (*s) - '0';
	    if (!it->conditions)
		it->conditions = make_array (p, 4, sizeof(int));
	    *(int *)push_array(it->conditions) = i;
	    break;

	default:
	    l = find_log_func (*s++);
	    if (!l) {
		char dummy[] = { '\0', '\0'};
		dummy[0] = s[-1];
		return pstrcat (p, "Unrecognized LogFormat directive %",
				dummy, NULL);
	    }
	    it->func = l->func;
	    if (it->want_orig == -1) it->want_orig = l->want_orig_default;
	    *sa = s;
	    return NULL;
	}
    }

    return "Ran off end of LogFormat parsing args to some directive";
}

array_header *parse_log_string (pool *p, char *s, char **err)
{
    array_header *a = make_array (p, 30, sizeof (log_format_item));
    char *res;

    while (*s) {
	if ((res = parse_log_item (p, (log_format_item *)push_array(a), &s))) {
	    *err = res;
	    return NULL;
	}
    }

    s = "\n";
    parse_log_item (p, (log_format_item *)push_array(a), &s);
    return a;
}

/*****************************************************************
 *
 * Actually logging.
 */

char *process_item(request_rec *r, request_rec *orig, log_format_item *item)
{
    char *cp;
    
    /* First, see if we need to process this thing at all... */

    if (item->conditions && item->conditions->nelts != 0) {
	int i;
	int *conds = (int *)item->conditions->elts;
	int in_list = 0;

	for (i = 0; i < item->conditions->nelts; ++i)
	    if (r->status == conds[i]) {
		in_list = 1;
		break;
	    }

	if ((item->condition_sense && in_list)
	    || (!item->condition_sense && !in_list))
	{
	    return "-";
	}
    }

    /* We do.  Do it... */

    cp = (*item->func)(item->want_orig ? orig : r, item->arg);
    return cp ? cp : "-";
}

int config_log_transaction(request_rec *r)
{
    config_log_state *cls = get_module_config (r->server->module_config,
					       &config_log_module);
  
    array_header *strsa= make_array(r->pool, cls->format->nelts,sizeof(char*));
    log_format_item *items = (log_format_item *)cls->format->elts;
    char *str, **strs, *s;
    request_rec *orig;
    int i;
    int len = 0;

    orig = r;
    while (orig->prev) orig = orig->prev;
    while (r->next) r = r->next;

    for (i = 0; i < cls->format->nelts; ++i)
	*((char**)push_array (strsa)) = process_item (r, orig, &items[i]);

    strs = (char **)strsa->elts;
    
    for (i = 0; i < cls->format->nelts; ++i)
	len += strlen (strs[i]);

    str = palloc (r->pool, len + 1);

    for (i = 0, s = str; i < cls->format->nelts; ++i) {
	strcpy (s, strs[i]);
	s += strlen (strs[i]);
    }
    
    write(cls->log_fd, str, strlen(str));

    return OK;
}

/*****************************************************************
 *
 * Module glue...
 */

void *make_config_log_state (pool *p, server_rec *s)
{
    config_log_state *cls =
      (config_log_state *)palloc (p, sizeof (config_log_state));

    cls->fname = NULL;
    cls->format = NULL;
    cls->log_fd = -1;

    return (void *)cls;
}

char *set_config_log (cmd_parms *parms, void *dummy, char *arg)
{
    config_log_state *cls = get_module_config (parms->server->module_config,
					       &config_log_module);
  
    cls->fname = arg;
    return NULL;
}

char *log_format (cmd_parms *cmd, void *dummy, char *arg)
{
    char *err_string = NULL;
    config_log_state *cls = get_module_config (cmd->server->module_config,
					       &config_log_module);
  
    cls->format = parse_log_string (cmd->pool, arg, &err_string);
    return err_string;
}

command_rec config_log_cmds[] = {
{ "TransferLog", set_config_log, NULL, RSRC_CONF, TAKE1,
    "the filename of the access log" },
{ "LogFormat", log_format, NULL, RSRC_CONF, TAKE1,
      "a log format string (see docs)" },
{ NULL }
};

void config_log_child (void *cmd)
{
    /* Child process code for 'TransferLog "|..."';
     * may want a common framework for this, since I expect it will
     * be common for other foo-loggers to want this sort of thing...
     */
    
    cleanup_for_exec();
    signal (SIGHUP, SIG_IGN);
    execl (SHELL_PATH, SHELL_PATH, "-c", (char *)cmd, NULL);
    fprintf (stderr, "Exec of shell for logging failed!!!\n");
    exit (1);
}

config_log_state *open_config_log (server_rec *s, pool *p,
				   config_log_state *defaults)
{
    config_log_state *cls = get_module_config (s->module_config,
					       &config_log_module);
  
    if (cls->log_fd > 0) return cls; /* virtual config shared w/main server */
    
    if (cls->format == NULL) {
	char *dummy;
	
	if (defaults) cls->format = defaults->format;
	else cls->format = parse_log_string (p, DEFAULT_LOG_FORMAT, &dummy);
    }

    if (cls->fname == NULL) {
	if (defaults) {
	    cls->log_fd = defaults->log_fd;
	    return cls;
	}
	else cls->fname = DEFAULT_XFERLOG;
    }
    
    if (*cls->fname == '|') {
	FILE *dummy;
	
	spawn_child(p, config_log_child, (void *)(cls->fname+1),
		    kill_after_timeout, &dummy, NULL);

	if (dummy == NULL) {
	    fprintf (stderr, "Couldn't fork child for TransferLog process\n");
	    exit (1);
	}

	cls->log_fd = fileno (dummy);
    }
    else {
	char *fname = server_root_relative (p, cls->fname);
	if((cls->log_fd = popenf(p, fname, xfer_flags, xfer_mode)) < 0) {
	    fprintf (stderr,
		     "httpd: could not open transfer log file %s.\n", fname);
	    perror("open");
	    exit(1);
	}
    }

    return cls;
}

void init_config_log (server_rec *s, pool *p)
{
    /* First, do "physical" server, which gets default log fd and format
     * for the virtual servers, if they don't override...
     */
    
    config_log_state *default_conf = open_config_log (s, p, NULL);
    
    /* Then, virtual servers */
    
    for (s = s->next; s; s = s->next) open_config_log (s, p, default_conf);
}

module config_log_module = {
   STANDARD_MODULE_STUFF,
   init_config_log,		/* initializer */
   NULL,			/* create per-dir config */
   NULL,			/* merge per-dir config */
   make_config_log_state,	/* server config */
   NULL,			/* merge server config */
   config_log_cmds,		/* command table */
   NULL,			/* handlers */
   NULL,			/* filename translation */
   NULL,			/* check_user_id */
   NULL,			/* check auth */
   NULL,			/* check access */
   NULL,			/* type_checker */
   NULL,			/* fixups */
   config_log_transaction	/* logger */
};
