/* Copyright 2004 The Apache Software Foundation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/*
 * Documentation:
 *
 * mod_whatkilledus is an experimental module for Apache httpd 1.3 which
 * tracks the current request and logs a report of the active request
 * when a child process crashes.  You should verify that it works reasonably
 * on your system before putting it in production.
 *
 * mod_whatkilledus is called during request processing to save information
 * about the current request.  It also implements a fatal exception hook
 * that will be called when a child process crashes.
 *
 * Apache httpd requirements for mod_whatkilledus:
 *
 *   Apache httpd >= 1.3.30 must be built with the AP_ENABLE_EXCEPTION_HOOK
 *   symbol defined and mod_so enabled.  AP_ENABLE_EXCEPTION_HOOK is already
 *   defined in ap_config.h for some platforms, including AIX, Linux,
 *   Solaris, and HP-UX.  It can be enabled for other platforms by including
 *   -DAP_ENABLE_EXCEPTION_HOOK in CFLAGS when the configure script is
 *   invoked.
 *
 * Compiling mod_whatkilledus:
 *
 *   AIX:
 *     apxs -ci -I/path/to/apache/src/main -Wl,-bE:mod_whatkilledus.exp mod_whatkilledus.c
 *
 *   other:
 *     apxs -ci -I/path/to/apache/src/main mod_whatkilledus.c
 *
 * Activating mod_whatkilledus:
 *
 *   1. Load it like any other DSO, but the AddModule should come
 *      last so that if another module causes a crash early in
 *      request processing mod_whatkilledus will have already
 *      had a chance to save information about the request.
 *
 *        LoadModule whatkilledus_module libexec/mod_whatkilledus.so
 *        ...
 *        AddModule mod_whatkilledus.c
 *
 *   2. Enable exception hooks for modules like mod_whatkilledus:
 *        EnableExceptionHook On
 *
 *   3. Choose where the report on current activity should be written.  If
 *      you want it reported to some place other than the error log, use the
 *      WhatKilledUsLog directive to specify a fully-qualified filename for
 *      the log.  Note that the web server user id (e.g., "nobody") must
 *      be able to create or append to this log file, as the log file is
 *      not opened until a crash occurs.
 */

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

#include "test_char.h" /* an odd one since it is not installed */

/* this module is not thread-safe; it is intended for Apache 1.3 on
 * platforms that use single-threaded child processes for handling
 * client connections
 */
static char *log_fname;
static char *local_addr;
static char *remote_addr;
static char *request_plus_headers;
static char buffer[2048];

static void exception_hook(ap_exception_info_t *ei)
{
    int msg_len;
    int logfd;
    char msg_prefix[60];
    time_t now;
    char *newline;
    int using_errorlog = 1;

    time(&now);
    ap_snprintf(msg_prefix, sizeof msg_prefix,
                "[%s pid %ld mod_whatkilledus",
                asctime(localtime(&now)),
                (long)getpid());
    newline = strchr(msg_prefix, '\n'); /* dang asctime() */
    if (newline) {                      /* silly we are */
        *newline = ']';
    }

    if (log_fname) {
        logfd = open(log_fname, O_WRONLY|O_APPEND|O_CREAT, 0644);
        if (logfd == -1) {
            logfd = 2; /* unix, so fd 2 is the web server error log */
            ap_snprintf(buffer, sizeof buffer,
                        "%s error %d opening %s\n",
                        msg_prefix, errno, log_fname);
            write(logfd, buffer, strlen(buffer));
        }
        else {
            using_errorlog = 0;
        }
    }
    else {
        logfd = 2;
    }

    msg_len = ap_snprintf(buffer, sizeof buffer,
                          "%s sig %d crash\n",
                          msg_prefix, ei->sig);
    write(logfd, buffer, msg_len);

    if (local_addr) {
        msg_len = ap_snprintf(buffer, sizeof buffer,
                              "%s active connection: %s->%s\n",
                              msg_prefix, remote_addr, local_addr);
    }
    else {
        msg_len = ap_snprintf(buffer, sizeof buffer,
                              "%s no active connection at crash\n",
                              msg_prefix);
    }

    write(logfd, buffer, msg_len);

    if (request_plus_headers) {
        msg_len = ap_snprintf(buffer, sizeof buffer,
                              "%s active request:\n",
                              msg_prefix);
        write(logfd, buffer, msg_len);
        write(logfd, request_plus_headers, strlen(request_plus_headers));
    }
    else {
        msg_len = ap_snprintf(buffer, sizeof buffer,
                              "%s no request active at crash\n",
                              msg_prefix);
        write(logfd, buffer, msg_len);
    }
    msg_len = ap_snprintf(buffer, sizeof buffer,
                          "%s end of report\n",
                          msg_prefix);
    write(logfd, buffer, msg_len);
    if (!using_errorlog) {
        close(logfd);
    }
}

static void init(server_rec *s, pool *p)
{
    int rc = ap_add_fatal_exception_hook(exception_hook);

    if (rc) {
        ap_log_error(APLOG_MARK, APLOG_ALERT|APLOG_NOERRNO, s,
                     "fatal exception hooks are not enabled; please "
                     "enable them with the EnableExceptionHook directive "
                     "or disable mod_whatkilledus");
    }
}

static void clear_conn_info(void *ignored)
{
    local_addr = remote_addr = NULL;
}

static void save_conn_info(request_rec *r)
{
    conn_rec *c = r->connection;
    local_addr =  ap_psprintf(c->pool, "%pI", &c->local_addr);
    remote_addr = ap_psprintf(c->pool, "%pI", &c->remote_addr);

    ap_register_cleanup(c->pool, NULL, clear_conn_info, ap_null_cleanup);
}

static void clear_req_info(void *ignored)
{
    request_plus_headers = NULL;
}

#define FIELD_SEPARATOR   "|"
#define KEYVAL_SEPARATOR  ":"

/* graciously lifted from mod_log_forensic */

static int count_string(const char *p)
{
    int n;

    for (n = 0; *p; ++p, ++n) {
        if (test_char_table[*(unsigned char *)p] & T_ESCAPE_FORENSIC) {
            n += 2;
        }
    }
    return n;
}

static int count_headers(void *in_len, const char *key, const char *value)
{
    int *len = in_len;

    *len += strlen(FIELD_SEPARATOR);
    *len += count_string(key);
    *len += strlen(KEYVAL_SEPARATOR);
    *len += count_string(value);

    return 1;
}

static char *copy_and_escape(char *loc, const char *str) {
    /* mod_log_forensic will SIGABRT here if it messed up the count
     * and overflowed; mod_whatkilledus will segfault here or will
     * SIGABRT back in the caller if that happens
     */
    for ( ; *str; ++str) {
        if (test_char_table[*(unsigned char *)str] & T_ESCAPE_FORENSIC) {
            *loc++ = '%';
            sprintf(loc, "%02x", *(unsigned char *)str);
            loc += 2;
        }
        else {
            *loc++ = *str;
        }
    }
    *loc = '\0';
    return loc;
}

static int copy_headers(void *in_ch, const char *key, const char *value)
{
    char **ch = in_ch;

    strcpy(*ch, FIELD_SEPARATOR);
    *ch += strlen(FIELD_SEPARATOR);

    *ch = copy_and_escape(*ch, key);

    strcpy(*ch, KEYVAL_SEPARATOR);
    *ch += strlen(KEYVAL_SEPARATOR);

    *ch = copy_and_escape(*ch, value);

    return 1;
}

static void save_req_info(request_rec *r)
{
    /* to save for the request:
     * r->the_request + 
     * foreach header:
     *   '|' + header field
     */
    int len = strlen(r->the_request);
    char *ch;
    ap_table_do(count_headers, &len, r->headers_in, NULL);

    request_plus_headers = ap_palloc(r->pool, len + 2 /* 2 for the '\n' + '\0' at end */);
    ch = request_plus_headers;
    strcpy(ch, r->the_request);
    ch += strlen(ch);

    ap_table_do(copy_headers, &ch, r->headers_in, NULL);
    *ch = '\n';
    *(ch + 1) = '\0';

    ap_assert(ch == request_plus_headers + len);

    ap_register_cleanup(r->pool, NULL, clear_req_info, ap_null_cleanup);
}

static int post_read(request_rec *r)
{
    if (r->prev) { /* we were already called for this internal redirect */
        return DECLINED;
    }

    /* save whatever info, like client, which vhost, which port
     * (to know SSL or not), etc.
     */
    if (!local_addr) { /* first request on this connection */
        save_conn_info(r);
    }

    save_req_info(r);

    return DECLINED;
}

static const char *cmd_file(cmd_parms *cmd, void *dconf, char *fname)
{
    log_fname = ap_pstrdup(cmd->pool, fname);
    return NULL;
}

static const command_rec command_table[] = {
    {
        "WhatKilledUsLog", cmd_file, NULL, RSRC_CONF, TAKE1, "the fully-qualified filename of the mod_whatkilledus logfile"
    }
    ,
    {
        NULL
    }
};

module MODULE_VAR_EXPORT whatkilledus_module = {
    STANDARD_MODULE_STUFF,
    init,                       /* initializer */
    NULL,                       /* create per-dir config */
    NULL,                       /* merge per-dir config */
    NULL,                       /* server config */
    NULL,                       /* merge server config */
    command_table,              /* command table */
    NULL,                       /* handlers */
    NULL,                       /* filename translation */
    NULL,                       /* check_user_id */
    NULL,                       /* check auth */
    NULL,                       /* check access */
    NULL,                       /* type_checker */
    NULL,                       /* fixups */
    NULL,                       /* logger */
    NULL,                       /* header parser */
    NULL,                       /* child_init */
    NULL,                       /* child_exit */
    post_read                   /* post read-request */
};
