/*
 * ModSecurity for Apache 2.x, http://www.modsecurity.org/
 * Copyright (c) 2004-2006 Thinking Stone (http://www.thinkingstone.com)
 *
 * $Id: apache2_io.c,v 1.6 2007/01/23 16:08:15 ivanr Exp $
 *
 * You should have received a copy of the licence along with this
 * program (stored in the file "LICENSE"). If the file is missing,
 * or if you have any other questions related to the licence, please
 * write to Thinking Stone at contact@thinkingstone.com.
 *
 */
#include "modsecurity.h"
#include "apache2.h"


/* -- Input filter -- */

#if 0
static void dummy_free_func(void *data) {}
#endif

/**
 * This request filter will forward the previously stored
 * request body further down the chain (most likely to the
 * processing module).
 */
apr_status_t input_filter(ap_filter_t *f, apr_bucket_brigade *bb_out,
    ap_input_mode_t mode, apr_read_type_e block, apr_off_t nbytes)
{
    modsec_rec *msr = (modsec_rec *)f->ctx;
    msc_data_chunk *chunk = NULL;
    apr_bucket *bucket;
    apr_status_t rc;

    if (msr == NULL) {
        ap_log_error(APLOG_MARK, APLOG_ERR | APLOG_NOERRNO, 0, f->r->server,
            "ModSecurity: Internal error in input filter: msr is null.");
        ap_remove_input_filter(f);
        return APR_EGENERAL;
    }

    if ((msr->if_status == IF_STATUS_COMPLETE)||(msr->if_status == IF_STATUS_NONE)) {
        if (msr->txcfg->debuglog_level >= 4) {
            msr_log(msr, 4, "Input filter: Input forwarding already complete, skipping (f %x, r %x).", f, f->r);
        }
        ap_remove_input_filter(f);
        return ap_get_brigade(f->next, bb_out, mode, block, nbytes);
    }

    if (msr->txcfg->debuglog_level >= 4) {
        msr_log(msr, 4, "Input filter: Forwarding input: mode=%i, block=%i, nbytes=%" APR_OFF_T_FMT
            " (f %x, r %x).", mode, block, nbytes, f, f->r);
    }

    if (msr->if_started_forwarding == 0) {
        msr->if_started_forwarding = 1;
        rc = modsecurity_request_body_retrieve_start(msr);
        if (rc == -1) {
            // TODO err
            return APR_EGENERAL;
        }
    }

    rc = modsecurity_request_body_retrieve(msr, &chunk, (unsigned int)nbytes);
    if (rc == -1) {
        // TODO err
        return APR_EGENERAL;
    }

    if (chunk) {
        /* Copy the data we received in the chunk */
        bucket = apr_bucket_heap_create(chunk->data, chunk->length, NULL,
            f->r->connection->bucket_alloc);

        #if 0

        It would seem that we cannot prevent other filters in the chain
        from modifying data in-place. Hence we copy.

        if (chunk->is_permanent) {
            /* Do not make a copy of the data we received in the chunk. */
            bucket = apr_bucket_heap_create(chunk->data, chunk->length, dummy_free_func,
                f->r->connection->bucket_alloc);
        } else {
            /* Copy the data we received in the chunk. */
            bucket = apr_bucket_heap_create(chunk->data, chunk->length, NULL,
                f->r->connection->bucket_alloc);
        }

        #endif

        if (bucket == NULL) return APR_EGENERAL;
        APR_BRIGADE_INSERT_TAIL(bb_out, bucket);

        if (msr->txcfg->debuglog_level >= 4) {
            msr_log(msr, 4, "Input filter: Forwarded %lu bytes.", chunk->length);
        }
    }

    if (rc == 0) {
        modsecurity_request_body_retrieve_end(msr);

        bucket = apr_bucket_eos_create(f->r->connection->bucket_alloc);
        if (bucket == NULL) return APR_EGENERAL;
        APR_BRIGADE_INSERT_TAIL(bb_out, bucket);

        if (msr->txcfg->debuglog_level >= 4) {
            msr_log(msr, 4, "Input filter: Sent EOS.");
        }

        /* We're done */
        msr->if_status = IF_STATUS_COMPLETE;
        ap_remove_input_filter(f);

        if (msr->txcfg->debuglog_level >= 4) {
            msr_log(msr, 4, "Input filter: Input forwarding complete.");
        }
    }

    return APR_SUCCESS;
}

/**
 * Reads request body from a client.
 */
apr_status_t read_request_body(modsec_rec *msr, char **error_msg) {
    request_rec *r = msr->r;
    unsigned int seen_eos;
    apr_bucket_brigade *bb_in;
    apr_bucket *bucket;

    if (error_msg == NULL) return -1;
    *error_msg = NULL;

    if (msr->reqbody_should_exist != 1) {
        if (msr->txcfg->debuglog_level >= 4) {
            msr_log(msr, 4, "Input filter: This request does not have a body.");
        }
        return 0;
    }

    if (msr->txcfg->reqbody_access != 1) {
        if (msr->txcfg->debuglog_level >= 4) {
            msr_log(msr, 4, "Input filter: Request body access not enabled.");
        }
        return 0;
    }

    if (msr->txcfg->debuglog_level >= 4) {
        msr_log(msr, 4, "Input filter: Reading request body.");
    }

    if (modsecurity_request_body_start(msr) < 0) {
        // TODO err
        return -1;
    }

    seen_eos = 0;
    bb_in = apr_brigade_create(msr->mp, r->connection->bucket_alloc);
    if (bb_in == NULL) return -1;    
    do {
        apr_status_t rc;

        rc = ap_get_brigade(r->input_filters, bb_in, AP_MODE_READBYTES, APR_BLOCK_READ, HUGE_STRING_LEN);
        if (rc != APR_SUCCESS) {
            /* NOTE Apache returns -3 here when the request is too large
             *      and APR_EGENERAL when the client disconnects.
             */
            switch(rc) {
                case APR_TIMEUP :
                    return -4;
                    break;
                case -3 :
                    *error_msg = apr_psprintf(msr->mp, "Error reading request body: HTTP Error 413 - Request entity too large. (Most likely.)");
                    rc = -3;
                    break;
                case APR_EGENERAL :
                    *error_msg = apr_psprintf(msr->mp, "Error reading request body: Client went away.");
                    rc = -2;
                    break;
                default :
                    *error_msg = apr_psprintf(msr->mp, "Error reading request body: %s", get_apr_error(msr->mp, rc));
                    rc = -1;
                    break;
            }

            if (*error_msg) msr_log(msr, 1, "%s", *error_msg);

            return rc;
        }

        /* Loop through the buckets in the brigade in order
         * to extract the size of the data available.
         */
        for(bucket = APR_BRIGADE_FIRST(bb_in);
            bucket != APR_BRIGADE_SENTINEL(bb_in);
            bucket = APR_BUCKET_NEXT(bucket))
        {
            const char *buf;
            apr_size_t buflen;

            rc = apr_bucket_read(bucket, &buf, &buflen, APR_BLOCK_READ);
            if (rc != APR_SUCCESS) {   
                msr_log(msr, 1, "Input filter: Failed reading input / bucket (%i): %s",
                    rc, get_apr_error(msr->mp, rc));
                return -1;
            }

            if (msr->txcfg->debuglog_level >= 9) {
                msr_log(msr, 9, "Input filter: Bucket type %s contains %i bytes.",
                    bucket->type->name, buflen);
            }

            /* Check request body limit (should only trigger on chunked requests). */
            if (msr->reqbody_length + buflen > (apr_size_t)msr->txcfg->reqbody_limit) {
                *error_msg = apr_psprintf(msr->mp, "Requests body is larger than the "
                    "configured limit (%lu).", msr->txcfg->reqbody_limit);
                return -5;
            }

            if (buflen != 0) {
                if (modsecurity_request_body_store(msr, buf, buflen) < 0) {
                    // TODO err
                    return -1;
                }

                msr->reqbody_length += buflen;
            }

            if (APR_BUCKET_IS_EOS(bucket)) {
                seen_eos = 1;
            }
        }

        apr_brigade_cleanup(bb_in);
    } while(!seen_eos);

    modsecurity_request_body_end(msr);

    if (msr->txcfg->debuglog_level >= 4) {
        msr_log(msr, 4, "Input filter: Completed receiving request body (length %lu).",
            msr->reqbody_length);
    }

    msr->if_status = IF_STATUS_WANTS_TO_RUN;

    return 1;
}


/* -- Output filter -- */

/**
 * Sends a brigade with an error bucket down the filter chain.
 */
static apr_status_t send_error_bucket(ap_filter_t *f, int status) {
    apr_bucket_brigade *brigade = NULL;
    apr_bucket *bucket = NULL;

    brigade = apr_brigade_create(f->r->pool, f->r->connection->bucket_alloc);
    if (brigade == NULL) return APR_EGENERAL;
    bucket = ap_bucket_error_create(status, NULL, f->r->pool, f->r->connection->bucket_alloc);
    if (bucket == NULL) return APR_EGENERAL;
    APR_BRIGADE_INSERT_TAIL(brigade, bucket);
    bucket = apr_bucket_eos_create(f->r->connection->bucket_alloc);
    if (bucket == NULL) return APR_EGENERAL;
    APR_BRIGADE_INSERT_TAIL(brigade, bucket);

    return ap_pass_brigade(f->next, brigade);
}

/**
 * Examines the configuration and the response MIME type
 * in order to determine whether output buffering should
 * run or not.
 */
static int output_filter_should_run(modsec_rec *msr, request_rec *r) {
    char *content_type = NULL;

    /* Check configuration. */
    if (msr->txcfg->resbody_access != 1) {
        if (msr->txcfg->debuglog_level >= 4) {
            msr_log(msr, 4, "Output filter: Response body buffering is not enabled.");
        }
        return 0;
    }

    /* Check MIME type. */

    if ((msr->txcfg->of_mime_types == NULL)||(msr->txcfg->of_mime_types == NOT_SET_P)) {
        msr_log(msr, 1, "Output filter: MIME type structures are corrupted (internal error).");
        return -1;
    }

    if (r->content_type != NULL) {
        char *p = NULL;

        content_type = apr_pstrdup(msr->mp, r->content_type);
        if (content_type == NULL) return -1;

        /* Hide the character encoding information
         * if present. Sometimes the content type header
         * looks like this "text/html; charset=xyz" ...
         */
        p = strstr(content_type, ";");
        if (p != NULL) {
            *p = '\0';
        }

        strtolower_inplace((unsigned char *)content_type);

        if (strcmp(content_type, "text/html") == 0) {
            /* Useful information to have should we later
             * decide to do something with the HTML output.
             */
            msr->resbody_contains_html = 1;
        }
    } else {
        content_type = "null";
    }

    if (apr_table_get(msr->txcfg->of_mime_types, content_type) != NULL) return 1;

    return 0;
}

/**
 * Initialises the output filter.
 */
static apr_status_t output_filter_init(modsec_rec *msr, ap_filter_t *f,
    apr_bucket_brigade *bb_in)
{
    request_rec *r = f->r;
    const char *s_content_length = NULL;
    apr_status_t rc;

    msr->of_brigade = apr_brigade_create(msr->mp, f->c->bucket_alloc);
    if (msr->of_brigade == NULL) return -1;
    msr->of_status = OF_STATUS_IN_PROGRESS;
    
    rc = output_filter_should_run(msr, r);
    if (rc < 0) return -1;
    if (rc == 0) return 0;

    /* Look up the Content-Length header to see if we know
     * the amount of data coming our way. If we do and if
     * it's too much we might want to stop processing right here.
     */
    s_content_length = apr_table_get(r->headers_out, "Content-Length");
    if (s_content_length == NULL) {
        /* Try this too, mod_cgi seems to put headers there */
        s_content_length = apr_table_get(r->err_headers_out, "Content-Length");
    }

    if (s_content_length != NULL) {
        long int len;

        len = strtol(s_content_length, NULL, 10);
        if ((len == LONG_MIN)||(len == LONG_MAX)||(len < 0)||(len >= 1073741824)) {
            msr_log(msr, 1, "Output filter: Invalid Content-Length: %s", log_escape_nq(r->pool, (char *)s_content_length));
            return -1;
        }

        if (len == 0) {
            if (msr->txcfg->debuglog_level >= 4) {
                msr_log(msr, 4, "Output filter: Skipping response since Content-Length is zero.");
            }
            return 0;
        }

        if (len > msr->txcfg->of_limit) {
            msr_log(msr, 1, "Output filter: Content-Length (%s) over the limit (%lu).", log_escape_nq(r->pool, (char *)s_content_length), msr->txcfg->of_limit);
            return -2;
        }
    }

    return 1;
}

/**
 * Output filter.
 */
apr_status_t output_filter(ap_filter_t *f, apr_bucket_brigade *bb_in) {
    request_rec *r = f->r;
    modsec_rec *msr = (modsec_rec *)f->ctx;
    apr_bucket *bucket;
    apr_status_t rc;

    if (msr == NULL) {
        ap_log_error(APLOG_MARK, APLOG_ERR | APLOG_NOERRNO, 0, f->r->server,
            "ModSecurity: Internal Error: msr is null in output filter.");
        ap_remove_output_filter(f);
        return send_error_bucket(f, HTTP_INTERNAL_SERVER_ERROR);
    }

    if (msr->txcfg->debuglog_level >= 4) {
        msr_log(msr, 4, "Output filter: Receiving output (f %x, r %x).", f, f->r);
    }
    
    /* Initialise on first invocation */
    if (msr->of_status == OF_STATUS_NOT_STARTED) {
        /* Update our context from the request structure. */
        msr->r = r;
        msr->response_status = r->status;
        msr->status_line = ((r->status_line != NULL)    
            ? r->status_line : ap_get_status_line(r->status));
        msr->response_protocol = get_response_protocol(r);
        msr->response_headers = apr_table_overlay(msr->mp, r->err_headers_out, r->headers_out);

        /* Process phase RESPONSE_HEADERS */        
        rc = modsecurity_process_phase(msr, PHASE_RESPONSE_HEADERS);
        if (rc < 0) { /* error */
            ap_remove_output_filter(f);
            return send_error_bucket(f, HTTP_INTERNAL_SERVER_ERROR);
        }
        if (rc > 0) { /* transaction needs to be interrupted */
            int status = perform_interception(msr);
            if (status != DECLINED) { /* DECLINED means we allow-ed the request. */
                ap_remove_output_filter(f);
                return send_error_bucket(f, status);
            }
        }

        /* Decide whether to observe the response body. */
        rc = output_filter_init(msr, f, bb_in);
        switch(rc) {
            case -2 : /* response too large */
            case -1 : /* error */
                /* there's something wrong with this response */
                ap_remove_output_filter(f);
                msr->of_status = OF_STATUS_COMPLETE;
                msr->resbody_status = RESBODY_STATUS_ERROR;
                return send_error_bucket(f, HTTP_INTERNAL_SERVER_ERROR);
            case 0 :
                /* We do not want to observe this response body
                 * but we need to remain attached to observe
                 * when it is completed so that we can run
                 * the RESPONSE_BODY phase.
                 */
                msr->of_skipping = 1;
                msr->resbody_status = RESBODY_STATUS_NOT_READ;
                break;
            default :
                /* Continue (observe the response body). */
                break;
        }
    } else
    if (msr->of_status == OF_STATUS_COMPLETE) {
        msr_log(msr, 1, "Output filter: Internal error: output filtering complete yet filter was invoked.");
        ap_remove_output_filter(f);
        return APR_EGENERAL;
    }

    /* Loop through the buckets in the brigade in order
     * to extract the size of the data available.
     */
    for(bucket = APR_BRIGADE_FIRST(bb_in);
      bucket != APR_BRIGADE_SENTINEL(bb_in);
      bucket = APR_BUCKET_NEXT(bucket)) {
        const char *buf;
        apr_size_t buflen;

        if (msr->of_skipping == 0) {
            rc = apr_bucket_read(bucket, &buf, &buflen, APR_BLOCK_READ);
            if (rc != APR_SUCCESS) {   
                msr->of_status = OF_STATUS_COMPLETE;
                msr->resbody_status = RESBODY_STATUS_ERROR;
                msr_log(msr, 1, "Output filter: Failed to read bucket (rc %i): %s",
                    rc, get_apr_error(r->pool, rc));
                ap_remove_output_filter(f);
                return send_error_bucket(f, HTTP_INTERNAL_SERVER_ERROR);
            }

            if (msr->txcfg->debuglog_level >= 9) {
                msr_log(msr, 9, "Output filter: Bucket type %s contains %i bytes.",
                    bucket->type->name, buflen);
            }

            if (msr->resbody_length > (apr_size_t)msr->txcfg->of_limit) {
                msr_log(msr, 1, "Output filter: Response body too large (over limit of %lu, total length not known).",
                    msr->txcfg->of_limit);
                msr->of_status = OF_STATUS_COMPLETE;
                msr->resbody_status = RESBODY_STATUS_PARTIAL;
                ap_remove_output_filter(f);
                return send_error_bucket(f, HTTP_INTERNAL_SERVER_ERROR);
            }

            msr->resbody_length += buflen;
        }

        if (APR_BUCKET_IS_EOS(bucket)) {
            msr->of_done_reading = 1;
        }
    }

    /* Add buckets in this brigade to the brigade
     * we have in the context, but only if we actually
     * want to keep the response body.
     */
    if (msr->of_skipping == 0) {
        ap_save_brigade(f, &msr->of_brigade, &bb_in, msr->mp);

        if (msr->of_done_reading == 0) {
            /* We are done for now. We will be called again with more data. */        
            return APR_SUCCESS;
        }

        if (msr->txcfg->debuglog_level >= 4) {
            msr_log(msr, 4, "Output filter: Completed receiving response (length %lu).",
                msr->resbody_length);
        }
    } else {
        if (msr->of_done_reading == 0) {
            return ap_pass_brigade(f->next, bb_in);
        }

        if (msr->txcfg->debuglog_level >= 4) {
            msr_log(msr, 4, "Output filter: Completed receiving response.");
        }
    }

    /* We're not coming back here. */
    msr->of_status = OF_STATUS_COMPLETE;
    ap_remove_output_filter(f);

    if (msr->of_skipping == 0) {
        /* We've done with reading, it's time to inspect the data. */
        msr->resbody_status = RESBODY_STATUS_READ_BRIGADE;

        if (msr->resbody_length + 1 <= 0) {
            msr_log(msr, 1, "Output filter: Invalid response length: %lu", msr->resbody_length);
            return send_error_bucket(f, HTTP_INTERNAL_SERVER_ERROR);
        }

        msr->resbody_data = apr_palloc(msr->mp, msr->resbody_length + 1); 
        if (msr->resbody_data == NULL) {
            msr_log(msr, 1, "Output filter: Response body data memory allocation failed. Asked for: %li",
                msr->resbody_length + 1);
            return send_error_bucket(f, HTTP_INTERNAL_SERVER_ERROR);
        }

        // TODO Why does the function below take pointer to length? Will it modify it?

        rc = apr_brigade_flatten(msr->of_brigade, msr->resbody_data, &msr->resbody_length);
        if (rc != APR_SUCCESS) {
            msr_log(msr, 1, "Output filter: Failed to flatten brigade (%i): %s", rc,
                get_apr_error(r->pool, rc));
            return send_error_bucket(f, HTTP_INTERNAL_SERVER_ERROR);
        }
        msr->resbody_data[msr->resbody_length] = '\0';
        msr->resbody_status = RESBODY_STATUS_READ;
    }

    /* Process phase RESPONSE_BODY */
    rc = modsecurity_process_phase(msr, PHASE_RESPONSE_BODY);
    if (rc < 0) {
        return send_error_bucket(f, HTTP_INTERNAL_SERVER_ERROR);
    }
    if (rc > 0) {
        int status = perform_interception(msr);
        if (status != DECLINED) { /* DECLINED means we allow-ed the request. */
            return send_error_bucket(f, status);
        }
    }

    if (msr->of_skipping == 0) {
        record_time_checkpoint(msr, 3);

        rc = ap_pass_brigade(f->next, msr->of_brigade);
        if (rc != APR_SUCCESS) {
            int log_level = 1;

            if (APR_STATUS_IS_ECONNRESET(rc)) {
                /* Message "Connection reset by peer" is common and not a sign
                 * of something unusual. Hence we don't want to make a big deal
                 * about it, logging at NOTICE level. Everything else we log
                 * at ERROR level.
                 */
                log_level = 3;
            }            

            msr_log(msr, log_level, "Output filter: Error while forwarding response data (%i): %s",
                    rc, get_apr_error(msr->mp, rc));

            return rc;
        }
    }
    
    /* Another job well done! */
    if (msr->txcfg->debuglog_level >= 4) {
        msr_log(msr, 4, "Output filter: Output forwarding complete.");
    }

    if (msr->of_skipping == 0) {
        return APR_SUCCESS;
    } else {
        return ap_pass_brigade(f->next, bb_in);
    }
}
