/* Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.
 */

/*
 * Originally written @ Covalent by Jim Jagielski
 * Modified to support writing to non blocking pipes @ BBC by Graham Leggett
 * Modifications (C) 2011 British Broadcasting Corporation
 */

/*
 * mod_firehose.c:
 *  A request and response sniffer for Apache v2.x. It logs
 *  all filter data right before and after it goes out on the
 *  wire (BUT right before SSL encoded or after SSL decoded).
 *  It can produce a *huge* amount of data.
 */

#include "httpd.h"
#include "http_connection.h"
#include "http_config.h"
#include "http_core.h"
#include "http_log.h"
#include "http_request.h"
#include "util_ebcdic.h"
#include "apr_strings.h"
#include "apr_portable.h"
#include "apr_uuid.h"
#include "mod_proxy.h"

#if APR_HAVE_SYS_SYSLIMITS_H
#include <sys/syslimits.h>
#endif
#if APR_HAVE_LINUX_LIMITS_H
#include <linux/limits.h>
#endif
#if APR_HAVE_FCNTL_H
#include <fcntl.h>
#endif
#if APR_HAVE_UNISTD_H
#include <unistd.h>
#endif

module AP_MODULE_DECLARE_DATA firehose_module;

typedef enum proxy_enum
{
    FIREHOSE_PROXY, FIREHOSE_NORMAL
} proxy_enum;

typedef enum request_enum
{
    FIREHOSE_CONNECTION, FIREHOSE_REQUEST
} request_enum;

typedef enum direction_enum
{
    FIREHOSE_IN = '<', FIREHOSE_OUT = '>'
} direction_enum;

typedef struct firehose_conn_t
{
    const char *filename;
    apr_file_t *file;
    proxy_enum proxy;
    direction_enum direction;
    request_enum request;
    int suppress;
    apr_int32_t nonblock;
} firehose_conn_t;

typedef struct firehose_conf_t
{
    apr_array_header_t *firehoses;
} firehose_conf_t;

typedef struct firehose_ctx_t
{
    firehose_conf_t *conf;
    firehose_conn_t *conn;
    apr_bucket_brigade *bb;
    apr_bucket_brigade *tmp;
    char uuid[APR_UUID_FORMATTED_LENGTH + 1];
    apr_uint64_t count;
    int direction;
    conn_rec *c;
    request_rec *r;
    ap_filter_t *f;
} firehose_ctx_t;

#define HEADER_LEN (sizeof(apr_uint64_t)*6 + APR_UUID_FORMATTED_LENGTH + 7)
#define BODY_LEN (PIPE_BUF - HEADER_LEN - 2)
#define HEADER_FMT "%" APR_UINT64_T_HEX_FMT " %" APR_UINT64_T_HEX_FMT " %c %s %" APR_UINT64_T_HEX_FMT CRLF

static apr_status_t filter_output_cleanup(void *dummy)
{
    ap_filter_t *f = (ap_filter_t *) dummy;
    ap_remove_output_filter(f);
    return APR_SUCCESS;
}

static apr_status_t filter_input_cleanup(void *dummy)
{
    ap_filter_t *f = (ap_filter_t *) dummy;
    ap_remove_input_filter(f);
    return APR_SUCCESS;
}

/**
 * Add the terminating empty fragment to indicate end-of-connection.
 */
static apr_status_t pumpit_cleanup(void *dummy)
{
    firehose_ctx_t *ctx = (firehose_ctx_t *) dummy;
    apr_status_t rv;
    apr_size_t hdr_len;
    char header[HEADER_LEN + 1];

    if (!ctx->count) {
        return APR_SUCCESS;
    }

    hdr_len = apr_snprintf(header, sizeof(header), HEADER_FMT,
            (apr_uint64_t) 0, (apr_uint64_t) apr_time_now(), ctx->direction,
            ctx->uuid, ctx->count);
    ap_xlate_proto_to_ascii(header, hdr_len);

    rv = apr_file_write_full(ctx->conn->file, header, hdr_len, NULL);
    if (APR_SUCCESS != rv) {
        if (ctx->conn->suppress) {
            /* ignore the error */
        }
        else if (ctx->r) {
            ap_log_rerror(APLOG_MARK, APLOG_WARNING, rv, ctx->r, APLOGNO(03183)
                    "mod_firehose: could not write %" APR_UINT64_T_FMT " bytes to '%s' for '%c' connection '%s' and count '%0" APR_UINT64_T_HEX_FMT "', bytes dropped (further errors will be suppressed)",
                    (apr_uint64_t)(hdr_len), ctx->conn->filename, ctx->conn->direction, ctx->uuid, ctx->count);
        }
        else {
            ap_log_cerror(APLOG_MARK, APLOG_WARNING, rv, ctx->c, APLOGNO(03184)
                    "mod_firehose: could not write %" APR_UINT64_T_FMT " bytes to '%s' for '%c' connection '%s' and count '%0" APR_UINT64_T_HEX_FMT "', bytes dropped (further errors will be suppressed)",
                    (apr_uint64_t)(hdr_len), ctx->conn->filename, ctx->conn->direction, ctx->uuid, ctx->count);
        }
        ctx->conn->suppress = 1;
    }
    else {
        ctx->conn->suppress = 0;
    }

    ctx->count = 0;

    return APR_SUCCESS;
}

/*
 * Pump the bucket contents to the pipe.
 *
 * Writes smaller than PIPE_BUF are guaranteed to be atomic when written to
 * pipes. As a result, we break the buckets into packets smaller than PIPE_BUF and
 * send each one in turn.
 *
 * Each packet is marked with the UUID of the connection so that the process that
 * reassembles the packets can put the right packets in the right order.
 *
 * Each packet is numbered with an incrementing counter. If a packet cannot be
 * written we drop the packet on the floor, and the counter will enable dropped
 * packets to be detected.
 */
static apr_status_t pumpit(ap_filter_t *f, apr_bucket *b, firehose_ctx_t *ctx)
{
    apr_status_t rv = APR_SUCCESS;

    if (!(APR_BUCKET_IS_METADATA(b))) {
        const char *buf;
        apr_size_t nbytes, offset = 0;

        rv = apr_bucket_read(b, &buf, &nbytes, APR_BLOCK_READ);

        if (rv == APR_SUCCESS) {
            while (nbytes > 0) {
                char header[HEADER_LEN + 1];
                apr_size_t hdr_len;
                apr_size_t body_len = nbytes < BODY_LEN ? nbytes : BODY_LEN;
                apr_size_t bytes;
                struct iovec vec[3];

                /*
                 * Insert the chunk header, specifying the number of bytes in
                 * the chunk.
                 */
                hdr_len = apr_snprintf(header, sizeof(header), HEADER_FMT,
                        (apr_uint64_t) body_len, (apr_uint64_t) apr_time_now(),
                        ctx->direction, ctx->uuid, ctx->count);
                ap_xlate_proto_to_ascii(header, hdr_len);

                vec[0].iov_base = header;
                vec[0].iov_len = hdr_len;
                vec[1].iov_base = (void *) (buf + offset);
                vec[1].iov_len = body_len;
                vec[2].iov_base = CRLF;
                vec[2].iov_len = 2;

                rv = apr_file_writev_full(ctx->conn->file, vec, 3, &bytes);
                if (APR_SUCCESS != rv) {
                    if (ctx->conn->suppress) {
                        /* ignore the error */
                    }
                    else if (ctx->r) {
                        ap_log_rerror(APLOG_MARK, APLOG_WARNING, rv, ctx->r, APLOGNO(03185)
                                "mod_firehose: could not write %" APR_UINT64_T_FMT " bytes to '%s' for '%c' connection '%s' and count '%0" APR_UINT64_T_HEX_FMT "', bytes dropped (further errors will be suppressed)",
                                (apr_uint64_t)(vec[0].iov_len + vec[1].iov_len + vec[2].iov_len), ctx->conn->filename, ctx->conn->direction, ctx->uuid, ctx->count);
                    }
                    else {
                        ap_log_cerror(APLOG_MARK, APLOG_WARNING, rv, ctx->c, APLOGNO(03186)
                                "mod_firehose: could not write %" APR_UINT64_T_FMT " bytes to '%s' for '%c' connection '%s' and count '%0" APR_UINT64_T_HEX_FMT "', bytes dropped (further errors will be suppressed)",
                                (apr_uint64_t)(vec[0].iov_len + vec[1].iov_len + vec[2].iov_len), ctx->conn->filename, ctx->conn->direction, ctx->uuid, ctx->count);
                    }
                    ctx->conn->suppress = 1;
                    rv = APR_SUCCESS;
                }
                else {
                    ctx->conn->suppress = 0;
                }

                ctx->count++;
                nbytes -= vec[1].iov_len;
                offset += vec[1].iov_len;
            }
        }

    }
    return rv;
}

static apr_status_t firehose_input_filter(ap_filter_t *f,
                                          apr_bucket_brigade *bb,
                                          ap_input_mode_t mode,
                                          apr_read_type_e block,
                                          apr_off_t readbytes)
{
    apr_bucket *b;
    apr_status_t rv;
    firehose_ctx_t *ctx = f->ctx;

    /* just get out of the way of things we don't want. */
    if (mode != AP_MODE_READBYTES && mode != AP_MODE_GETLINE) {
        return ap_get_brigade(f->next, bb, mode, block, readbytes);
    }

    rv = ap_get_brigade(f->next, bb, mode, block, readbytes);

    /* if an error was received, bail out now. If the error is
     * EAGAIN and we have not yet seen an EOS, we will definitely
     * be called again, at which point we will send our buffered
     * data. Instead of sending EAGAIN, some filters return an
     * empty brigade instead when data is not yet available. In
     * this case, pass through the APR_SUCCESS and emulate the
     * underlying filter.
     */
    if (rv != APR_SUCCESS || APR_BRIGADE_EMPTY(bb)) {
        return rv;
    }

    for (b = APR_BRIGADE_FIRST(bb); b != APR_BRIGADE_SENTINEL(bb); b
            = APR_BUCKET_NEXT(b)) {
        rv = pumpit(f, b, ctx);
        if (APR_SUCCESS != rv) {
            return rv;
        }
    }

    return APR_SUCCESS;
}

static apr_status_t firehose_output_filter(ap_filter_t *f,
                                           apr_bucket_brigade *bb)
{
    apr_bucket *b;
    apr_status_t rv = APR_SUCCESS;
    firehose_ctx_t *ctx = f->ctx;

    while (APR_SUCCESS == rv && !APR_BRIGADE_EMPTY(bb)) {

        b = APR_BRIGADE_FIRST(bb);

        rv = pumpit(f, b, ctx);
        if (APR_SUCCESS != rv) {
            return rv;
        }

        /* pass each bucket down */
        APR_BUCKET_REMOVE(b);
        APR_BRIGADE_INSERT_TAIL(ctx->bb, b);

        /*
         * If we ever see an EOS, make sure to FLUSH.
         */
        if (APR_BUCKET_IS_EOS(b)) {
            apr_bucket *flush = apr_bucket_flush_create(f->c->bucket_alloc);
            APR_BUCKET_INSERT_BEFORE(b, flush);
        }

        rv = ap_pass_brigade(f->next, ctx->bb);

    }

    return rv;
}

/**
 * Create a firehose for each main request.
 */
static int firehose_create_request(request_rec *r)
{
    firehose_ctx_t *ctx;
    apr_uuid_t uuid;
    int set = 0;
    ap_filter_t *f;

    if (r->main) {
        return DECLINED;
    }

    f = r->connection->input_filters;
    while (f) {
        if (f->frec->filter_func.in_func == &firehose_input_filter) {
            ctx = (firehose_ctx_t *) f->ctx;
            if (ctx->conn->request == FIREHOSE_REQUEST) {
                pumpit_cleanup(ctx);
                if (!set) {
                    apr_uuid_get(&uuid);
                    set = 1;
                }
                apr_uuid_format(ctx->uuid, &uuid);
            }
        }
        f = f->next;
    }

    f = r->connection->output_filters;
    while (f) {
        if (f->frec->filter_func.out_func == &firehose_output_filter) {
            ctx = (firehose_ctx_t *) f->ctx;
            if (ctx->conn->request == FIREHOSE_REQUEST) {
                pumpit_cleanup(ctx);
                if (!set) {
                    apr_uuid_get(&uuid);
                    set = 1;
                }
                apr_uuid_format(ctx->uuid, &uuid);
            }
        }
        f = f->next;
    }

    return OK;
}

/* Ideas for extension:
 *
 * TODO: An idea for configuration. Let the filename directives be per-directory,
 * with a global hashtable of filename to filehandle mappings. As each directive
 * is parsed, a file is opened at server start. By default, all input is buffered
 * until the header_parser hook, at which point we check if we should be buffering
 * at all. If not, we dump the buffer and remove the filter. If so, we start
 * attempting to write the buffer to the file.
 *
 * TODO: Implement a buffer to allow firehose fragment writes to back up to some
 * threshold before packets are dropped. Flush the buffer on cleanup, waiting a
 * suitable amount of time for the downstream to catch up.
 *
 * TODO: For the request firehose, have an option to set aside request buckets
 * until we decide whether we're going to record this request or not. Allows for
 * targeted firehose by URL space.
 *
 * TODO: Potentially decide on firehose sending based on a variable in the notes
 * table or subprocess_env. Use standard httpd SetEnvIf and friends to decide on
 * whether to include the request or not. Using this, we can react to the value
 * of a flagpole. Run this check in the header_parser hook.
 */

static int firehose_pre_conn(conn_rec *c, void *csd)
{
    firehose_conf_t *conf;
    firehose_ctx_t *ctx;
    apr_uuid_t uuid;
    int i;
    firehose_conn_t *conn;

    conf = ap_get_module_config(c->base_server->module_config,
            &firehose_module);

    if (conf->firehoses->nelts) {
        apr_uuid_get(&uuid);
    }

    conn = (firehose_conn_t *) conf->firehoses->elts;
    for (i = 0; i < conf->firehoses->nelts; i++) {

        if (!conn->file || (conn->proxy == FIREHOSE_NORMAL
                && !c->sbh) || (conn->proxy == FIREHOSE_PROXY && c->sbh)) {
            conn++;
            continue;
        }

        ctx = apr_pcalloc(c->pool, sizeof(firehose_ctx_t));
        apr_uuid_format(ctx->uuid, &uuid);
        ctx->conf = conf;
        ctx->conn = conn;
        ctx->bb = apr_brigade_create(c->pool, c->bucket_alloc);
        ctx->c = c;
        apr_pool_cleanup_register(c->pool, ctx, pumpit_cleanup, pumpit_cleanup);
        if (conn->direction == FIREHOSE_IN) {
            ctx->direction = conn->proxy == FIREHOSE_PROXY ? '>' : '<';
            ctx->f = ap_add_input_filter("FIREHOSE_IN", ctx, NULL, c);
            apr_pool_cleanup_register(c->pool, ctx->f, filter_input_cleanup,
                    filter_input_cleanup);
        }
        if (conn->direction == FIREHOSE_OUT) {
            ctx->direction = conn->proxy == FIREHOSE_PROXY ? '<' : '>';
            ctx->f = ap_add_output_filter("FIREHOSE_OUT", ctx, NULL, c);
            apr_pool_cleanup_register(c->pool, ctx->f, filter_output_cleanup,
                    filter_output_cleanup);
        }

        conn++;
    }

    return OK;
}

static int firehose_open_logs(apr_pool_t *p, apr_pool_t *plog,
        apr_pool_t *ptemp, server_rec *s)
{
    firehose_conf_t *conf;
    apr_status_t rv;
    void *data;
    int i;
    firehose_conn_t *conn;

    /* make sure we only open the files on the second pass for config */
    apr_pool_userdata_get(&data, "mod_firehose", s->process->pool);
    if (!data) {
        apr_pool_userdata_set((const void *) 1, "mod_firehose",
                apr_pool_cleanup_null, s->process->pool);
        return OK;
    }

    while (s) {

        conf = ap_get_module_config(s->module_config,
                &firehose_module);

        conn = (firehose_conn_t *) conf->firehoses->elts;
        for (i = 0; i < conf->firehoses->nelts; i++) {
            if (APR_SUCCESS != (rv = apr_file_open(&conn->file, conn->filename,
                    APR_FOPEN_WRITE | APR_FOPEN_CREATE | APR_FOPEN_APPEND
                            | conn->nonblock, APR_OS_DEFAULT, plog))) {
                ap_log_error(APLOG_MARK, APLOG_WARNING, rv, s, APLOGNO(02990)
                        "mod_firehose: could not open '%s' for write, disabling firehose %s%s %s filter",
                        conn->filename, conn->proxy == FIREHOSE_PROXY ? "proxy " : "",
                        conn->request == FIREHOSE_REQUEST ? " request" : "connection",
                        conn->direction == FIREHOSE_IN ? "input" : "output");
            }
            conn++;
        }

        s = s->next;
    }

    return OK;
}

static void firehose_register_hooks(apr_pool_t *p)
{
    /*
     * We know that SSL is CONNECTION + 5
     */
    ap_register_output_filter("FIREHOSE_OUT", firehose_output_filter, NULL,
            AP_FTYPE_CONNECTION + 3);

    ap_register_input_filter("FIREHOSE_IN", firehose_input_filter, NULL,
            AP_FTYPE_CONNECTION + 3);

    ap_hook_open_logs(firehose_open_logs, NULL, NULL, APR_HOOK_LAST);
    ap_hook_pre_connection(firehose_pre_conn, NULL, NULL, APR_HOOK_MIDDLE);
    ap_hook_create_request(firehose_create_request, NULL, NULL,
            APR_HOOK_REALLY_LAST + 1);
}

static void *firehose_create_sconfig(apr_pool_t *p, server_rec *s)
{
    firehose_conf_t *ptr = apr_pcalloc(p, sizeof(firehose_conf_t));

    ptr->firehoses = apr_array_make(p, 2, sizeof(firehose_conn_t));

    return ptr;
}

static void *firehose_merge_sconfig(apr_pool_t *p, void *basev,
        void *overridesv)
{
    firehose_conf_t *cconf = apr_pcalloc(p, sizeof(firehose_conf_t));
    firehose_conf_t *base = (firehose_conf_t *) basev;
    firehose_conf_t *overrides = (firehose_conf_t *) overridesv;

    cconf->firehoses = apr_array_append(p, overrides->firehoses,
            base->firehoses);

    return cconf;
}

static const char *firehose_enable_connection(cmd_parms *cmd, const char *arg1,
        const char *arg2, proxy_enum proxy, direction_enum direction,
        request_enum request)
{
    const char *name = arg2 ? arg2 : arg1;

    firehose_conn_t *firehose;
    firehose_conf_t
            *ptr =
                    (firehose_conf_t *) ap_get_module_config(cmd->server->module_config,
                            &firehose_module);

    firehose = apr_array_push(ptr->firehoses);

    firehose->filename = name;
    firehose->proxy = proxy;
    firehose->direction = direction;
    firehose->request = request;

    if (arg2) {
        if (!strcmp(arg1, "nonblock")) {
#ifdef APR_FOPEN_NONBLOCK
            firehose->nonblock = APR_FOPEN_NONBLOCK;
#else
            return "The parameter 'nonblock' is not supported by APR on this platform";
#endif
        }
        else if (!strcmp(arg1, "block")) {
            firehose->nonblock = 0;
        }
        else {
            return apr_psprintf(cmd->pool,
                    "The parameter '%s' should be 'block' or 'nonblock'", arg1);
        }
    }
    else {
        firehose->nonblock = 0;
    }

    return NULL;
}

static const char *firehose_enable_connection_input(cmd_parms *cmd,
        void *dummy, const char *arg1, const char *arg2)
{

    const char *err = ap_check_cmd_context(cmd, GLOBAL_ONLY
            | NOT_IN_LIMIT);
    if (err != NULL) {
        return err;
    }

    return firehose_enable_connection(cmd, arg1, arg2, FIREHOSE_NORMAL,
            FIREHOSE_IN, FIREHOSE_CONNECTION);

}

static const char *firehose_enable_connection_output(cmd_parms *cmd,
        void *dummy, const char *arg1, const char *arg2)
{

    const char *err = ap_check_cmd_context(cmd, GLOBAL_ONLY
            | NOT_IN_LIMIT);
    if (err != NULL) {
        return err;
    }

    return firehose_enable_connection(cmd, arg1, arg2, FIREHOSE_NORMAL,
            FIREHOSE_OUT, FIREHOSE_CONNECTION);

}

static const char *firehose_enable_request_input(cmd_parms *cmd, void *dummy,
        const char *arg1, const char *arg2)
{

    const char *err = ap_check_cmd_context(cmd, GLOBAL_ONLY
            | NOT_IN_LIMIT);
    if (err != NULL) {
        return err;
    }

    return firehose_enable_connection(cmd, arg1, arg2, FIREHOSE_NORMAL,
            FIREHOSE_IN, FIREHOSE_REQUEST);

}

static const char *firehose_enable_request_output(cmd_parms *cmd, void *dummy,
        const char *arg1, const char *arg2)
{

    const char *err = ap_check_cmd_context(cmd, GLOBAL_ONLY
            | NOT_IN_LIMIT);
    if (err != NULL) {
        return err;
    }

    return firehose_enable_connection(cmd, arg1, arg2, FIREHOSE_NORMAL,
            FIREHOSE_OUT, FIREHOSE_REQUEST);

}

static const char *firehose_enable_proxy_connection_input(cmd_parms *cmd,
        void *dummy, const char *arg1, const char *arg2)
{

    const char *err = ap_check_cmd_context(cmd, GLOBAL_ONLY
            | NOT_IN_LIMIT);
    if (err != NULL) {
        return err;
    }

    return firehose_enable_connection(cmd, arg1, arg2, FIREHOSE_PROXY,
            FIREHOSE_IN, FIREHOSE_CONNECTION);

}

static const char *firehose_enable_proxy_connection_output(cmd_parms *cmd,
        void *dummy, const char *arg1, const char *arg2)
{

    const char *err = ap_check_cmd_context(cmd, GLOBAL_ONLY
            | NOT_IN_LIMIT);
    if (err != NULL) {
        return err;
    }

    return firehose_enable_connection(cmd, arg1, arg2, FIREHOSE_PROXY,
            FIREHOSE_OUT, FIREHOSE_CONNECTION);

}

static const command_rec firehose_cmds[] =
{
        AP_INIT_TAKE12("FirehoseConnectionInput", firehose_enable_connection_input, NULL,
                RSRC_CONF, "Enable firehose on connection input data written to the given file/pipe"),
        AP_INIT_TAKE12("FirehoseConnectionOutput", firehose_enable_connection_output, NULL,
                RSRC_CONF, "Enable firehose on connection output data written to the given file/pipe"),
        AP_INIT_TAKE12("FirehoseRequestInput", firehose_enable_request_input, NULL,
                RSRC_CONF, "Enable firehose on request input data written to the given file/pipe"),
        AP_INIT_TAKE12("FirehoseRequestOutput", firehose_enable_request_output, NULL,
                RSRC_CONF, "Enable firehose on request output data written to the given file/pipe"),
        AP_INIT_TAKE12("FirehoseProxyConnectionInput", firehose_enable_proxy_connection_input, NULL,
                RSRC_CONF, "Enable firehose on proxied connection input data written to the given file/pipe"),
        AP_INIT_TAKE12("FirehoseProxyConnectionOutput", firehose_enable_proxy_connection_output, NULL,
                RSRC_CONF, "Enable firehose on proxied connection output data written to the given file/pipe"),
        { NULL }
};

AP_DECLARE_MODULE(firehose) =
{
        STANDARD20_MODULE_STUFF,
        NULL,
        NULL,
        firehose_create_sconfig,
        firehose_merge_sconfig,
        firehose_cmds,
        firehose_register_hooks
};
