Added support for partial response body processing.

This commit is contained in:
ivanr
2007-09-21 23:23:11 +00:00
parent 59333a6a81
commit 9ed3cf9e5a
4 changed files with 245 additions and 91 deletions

View File

@@ -300,13 +300,14 @@ static int output_filter_should_run(modsec_rec *msr, request_rec *r) {
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).");
msr_log(msr, 1, "Output filter: MIME type structures corrupted (internal error).");
return -1;
}
@@ -362,13 +363,21 @@ static apr_status_t output_filter_init(modsec_rec *msr, ap_filter_t *f,
if (rc < 0) return -1;
if (rc == 0) return 0;
/* Do not check the output limit if we are willing to
* process partial response bodies.
*/
if (msr->txcfg->of_limit_action == RESPONSE_BODY_LIMIT_ACTION_PARTIAL) {
return 1;
}
/* 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 */
/* Try this too, mod_cgi seems to put headers there. */
s_content_length = apr_table_get(r->err_headers_out, "Content-Length");
}
@@ -377,26 +386,111 @@ static apr_status_t output_filter_init(modsec_rec *msr, ap_filter_t *f,
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;
msr_log(msr, 1, "Output filter: Invalid Content-Length: %s", log_escape_nq(r->pool,
(char *)s_content_length));
return -1; /* Invalid. */
}
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;
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; /* Over the limit. */
}
}
return 1;
}
/**
* Send the accumulated content down the filter stream
* and to the client.
*/
static apr_status_t send_of_brigade(modsec_rec *msr, ap_filter_t *f) {
apr_status_t rc;
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;
}
if (msr->txcfg->debuglog_level >= log_level) {
msr_log(msr, log_level, "Output filter: Error while forwarding response data (%i): %s",
rc, get_apr_error(msr->mp, rc));
}
return rc;
}
return APR_SUCCESS;
}
/**
*
*/
static void prepend_content_to_of_brigade(modsec_rec *msr, ap_filter_t *f) {
if ((msr->txcfg->content_injection_enabled) && (msr->content_prepend) && (!msr->of_skipping)) {
apr_bucket *bucket_ci = NULL;
bucket_ci = apr_bucket_heap_create(msr->content_prepend,
msr->content_prepend_len, NULL, f->r->connection->bucket_alloc);
APR_BRIGADE_INSERT_HEAD(msr->of_brigade, bucket_ci);
if (msr->txcfg->debuglog_level >= 9) {
msr_log(msr, 9, "Content Injection (b): Added content to top: %s",
log_escape_nq_ex(msr->mp, msr->content_prepend, msr->content_prepend_len));
}
}
}
/**
*
*/
static int flatten_response_body(modsec_rec *msr) {
apr_status_t rc;
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 -1;
}
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 -1;
}
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(msr->mp, rc));
return -1;
}
msr->resbody_data[msr->resbody_length] = '\0';
msr->resbody_status = RESBODY_STATUS_READ;
return 1;
}
/**
* Output filter.
*/
@@ -405,7 +499,9 @@ apr_status_t output_filter(ap_filter_t *f, apr_bucket_brigade *bb_in) {
modsec_rec *msr = (modsec_rec *)f->ctx;
apr_bucket *bucket = NULL, *eos_bucket = NULL;
apr_status_t rc;
int start_skipping = 0;
/* Do we have the context? */
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.");
@@ -490,7 +586,7 @@ apr_status_t output_filter(ap_filter_t *f, apr_bucket_brigade *bb_in) {
}
/* Content injection (prepend & non-buffering). */
if (msr->txcfg->content_injection_enabled && msr->content_prepend && msr->of_skipping) {
if ((msr->txcfg->content_injection_enabled) && (msr->content_prepend) && (msr->of_skipping)) {
apr_bucket *bucket_ci = apr_bucket_heap_create(msr->content_prepend,
msr->content_prepend_len, NULL, f->r->connection->bucket_alloc);
APR_BRIGADE_INSERT_HEAD(bb_in, bucket_ci);
@@ -517,13 +613,19 @@ apr_status_t output_filter(ap_filter_t *f, apr_bucket_brigade *bb_in) {
const char *buf;
apr_size_t buflen;
if (msr->of_skipping == 0) {
/* Look into response data if configured to do so,
* unless we've already processed a partial response.
*/
if ((msr->of_skipping == 0)&&(!msr->of_partial)) { /* Observe the response data. */
/* Retrieve data from the bucket. */
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);
}
@@ -533,23 +635,45 @@ apr_status_t output_filter(ap_filter_t *f, apr_bucket_brigade *bb_in) {
bucket->type->name, buflen);
}
/* Check the response size. */
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);
}
/* The size of the response is larger than what we're
* ready to accept. We need to decide what we want to do
* about it.
*/
if (msr->txcfg->of_limit_action == RESPONSE_BODY_LIMIT_ACTION_REJECT) {
/* Reject response. */
msr_log(msr, 1, "Output filter: Response body too large (over limit of %lu, "
"total length not known).", msr->txcfg->of_limit);
msr->resbody_length += buflen;
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);
} else {
/* Process partial response. */
start_skipping = 1;
msr->resbody_length = msr->txcfg->of_limit;
if (msr->txcfg->debuglog_level >= 4) {
msr_log(msr, 4, "Output filter: Processing partial response body (limit %lu)",
msr->txcfg->of_limit);
}
}
} else {
msr->resbody_length += buflen;
}
}
/* Have we reached the end of the response? */
if (APR_BUCKET_IS_EOS(bucket)) {
eos_bucket = bucket;
/* Inject content (append & non-buffering). */
if (msr->txcfg->content_injection_enabled && msr->content_append && msr->of_skipping) {
if ((msr->txcfg->content_injection_enabled) && (msr->content_append)
&& (msr->of_skipping || msr->of_partial || start_skipping))
{
apr_bucket *bucket_ci = NULL;
bucket_ci = apr_bucket_heap_create(msr->content_append,
@@ -570,91 +694,94 @@ apr_status_t output_filter(ap_filter_t *f, apr_bucket_brigade *bb_in) {
* we have in the context, but only if we actually
* want to keep the response body.
*/
if (msr->of_skipping == 0) {
if ((msr->of_skipping == 0)&&(msr->of_partial == 0)) {
ap_save_brigade(f, &msr->of_brigade, &bb_in, msr->mp);
/* Do we need to process a partial response? */
if (start_skipping) {
if (flatten_response_body(msr) < 0) {
return send_error_bucket(f, HTTP_INTERNAL_SERVER_ERROR);
}
/* 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);
}
}
/* Prepend content as necessary. */
prepend_content_to_of_brigade(msr, f);
if ((rc = send_of_brigade(msr, f)) != APR_SUCCESS) {
return rc;
}
msr->of_partial = 1;
}
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);
msr_log(msr, 4, "Output filter: Completed receiving response body (buffered %s - %lu bytes).",
(msr->of_partial ? "partial" : "full"), msr->resbody_length);
}
} else {
} else { /* Not looking at response data. */
if (msr->of_done_reading == 0) {
if (msr->txcfg->debuglog_level >= 9) {
msr_log(msr, 9, "Output filter: Sending input brigade directly.");
}
return ap_pass_brigade(f->next, bb_in);
}
if (msr->txcfg->debuglog_level >= 4) {
msr_log(msr, 4, "Output filter: Completed receiving response.");
msr_log(msr, 4, "Output filter: Completed receiving response body (non-buffering).");
}
}
/* We're not coming back here. */
/* We've done our thing; remove us from the filter list. */
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);
/* Process phase RESPONSE_BODY, but
* only if it hasn't been processed already.
*/
if (msr->phase < PHASE_RESPONSE_BODY) {
if (flatten_response_body(msr) < 0) {
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);
rc = modsecurity_process_phase(msr, PHASE_RESPONSE_BODY);
if (rc < 0) {
return send_error_bucket(f, HTTP_INTERNAL_SERVER_ERROR);
}
// TODO Why does the function below take pointer to length? Will it modify it?
// BR: Yes - The maximum length of the char array. On return, it is the actual length of the char array.
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);
/* Inject content into response (prepend & buffering). */
if (msr->txcfg->content_injection_enabled && msr->content_prepend && (!msr->of_skipping)) {
apr_bucket *bucket_ci = NULL;
bucket_ci = apr_bucket_heap_create(msr->content_prepend,
msr->content_prepend_len, NULL, f->r->connection->bucket_alloc);
APR_BRIGADE_INSERT_HEAD(msr->of_brigade, bucket_ci);
if (msr->txcfg->debuglog_level >= 9) {
msr_log(msr, 9, "Content Injection (b): Added content to top: %s",
log_escape_nq_ex(msr->mp, msr->content_prepend, msr->content_prepend_len));
if (rc > 0) {
int status = perform_interception(msr);
if (status != DECLINED) { /* DECLINED means we allow-ed the request. */
return send_error_bucket(f, status);
}
}
}
/* Now send data down the filter stream
* (full-buffering only).
*/
if ((msr->of_skipping == 0)&&(!msr->of_partial)) {
record_time_checkpoint(msr, 3);
prepend_content_to_of_brigade(msr, f);
/* Inject content into response (append & buffering). */
if (msr->txcfg->content_injection_enabled && msr->content_append && (!msr->of_skipping)) {
if ((msr->txcfg->content_injection_enabled) && (msr->content_append)) {
apr_bucket *bucket_ci = NULL;
bucket_ci = apr_bucket_heap_create(msr->content_append,
@@ -667,22 +794,8 @@ apr_status_t output_filter(ap_filter_t *f, apr_bucket_brigade *bb_in) {
}
}
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));
/* Send data down the filter stream. */
if ((rc = send_of_brigade(msr, f)) != APR_SUCCESS) {
return rc;
}
}
@@ -692,9 +805,13 @@ apr_status_t output_filter(ap_filter_t *f, apr_bucket_brigade *bb_in) {
msr_log(msr, 4, "Output filter: Output forwarding complete.");
}
if (msr->of_skipping == 0) {
if ((msr->of_skipping == 0)&&(msr->of_partial == 0)) {
return APR_SUCCESS;
} else {
if (msr->txcfg->debuglog_level >= 9) {
msr_log(msr, 9, "Output filter: Sending input brigade directly.");
}
return ap_pass_brigade(f->next, bb_in);
}
}