/* 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. */ /* * mod_request.c --- HTTP routines to set aside or process request bodies. */ #include "apr.h" #include "apr_strings.h" #include "apr_buckets.h" #include "apr_lib.h" #include "ap_config.h" #include "httpd.h" #include "http_config.h" #include "http_protocol.h" #include "http_log.h" /* For errors detected in basic auth common * support code... */ #include "http_request.h" #include "mod_request.h" /* Handles for core filters */ static ap_filter_rec_t *keep_body_input_filter_handle; static ap_filter_rec_t *kept_body_input_filter_handle; static apr_status_t bail_out_on_error(apr_bucket_brigade *bb, ap_filter_t *f, int http_error) { apr_bucket *e; apr_brigade_cleanup(bb); e = ap_bucket_error_create(http_error, NULL, f->r->pool, f->c->bucket_alloc); APR_BRIGADE_INSERT_TAIL(bb, e); e = apr_bucket_eos_create(f->c->bucket_alloc); APR_BRIGADE_INSERT_TAIL(bb, e); return ap_pass_brigade(f->r->output_filters, bb); } typedef struct keep_body_filter_ctx { apr_off_t remaining; apr_off_t keep_body; } keep_body_ctx_t; /** * This is the KEEP_BODY_INPUT filter for HTTP requests, for times when the * body should be set aside for future use by other modules. */ static apr_status_t keep_body_filter(ap_filter_t *f, apr_bucket_brigade *b, ap_input_mode_t mode, apr_read_type_e block, apr_off_t readbytes) { apr_bucket *e; keep_body_ctx_t *ctx = f->ctx; apr_status_t rv; apr_bucket *bucket; apr_off_t len = 0; if (!ctx) { const char *lenp; char *endstr = NULL; request_dir_conf *dconf = ap_get_module_config(f->r->per_dir_config, &request_module); /* must we step out of the way? */ if (!dconf->keep_body || f->r->kept_body) { ap_remove_input_filter(f); return ap_get_brigade(f->next, b, mode, block, readbytes); } f->ctx = ctx = apr_pcalloc(f->r->pool, sizeof(*ctx)); /* fail fast if the content length exceeds keep body */ lenp = apr_table_get(f->r->headers_in, "Content-Length"); if (lenp) { /* Protects against over/underflow, non-digit chars in the * string (excluding leading space) (the endstr checks) * and a negative number. */ if (apr_strtoff(&ctx->remaining, lenp, &endstr, 10) || endstr == lenp || *endstr || ctx->remaining < 0) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, f->r, APLOGNO(01411) "Invalid Content-Length"); ap_remove_input_filter(f); return bail_out_on_error(b, f, HTTP_REQUEST_ENTITY_TOO_LARGE); } /* If we have a limit in effect and we know the C-L ahead of * time, stop it here if it is invalid. */ if (dconf->keep_body < ctx->remaining) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, f->r, APLOGNO(01412) "Requested content-length of %" APR_OFF_T_FMT " is larger than the configured limit" " of %" APR_OFF_T_FMT, ctx->remaining, dconf->keep_body); ap_remove_input_filter(f); return bail_out_on_error(b, f, HTTP_REQUEST_ENTITY_TOO_LARGE); } } f->r->kept_body = apr_brigade_create(f->r->pool, f->r->connection->bucket_alloc); ctx->remaining = dconf->keep_body; } /* get the brigade from upstream, and read it in to get its length */ rv = ap_get_brigade(f->next, b, mode, block, readbytes); if (rv == APR_SUCCESS) { rv = apr_brigade_length(b, 1, &len); } /* does the length take us over the limit? */ if (APR_SUCCESS == rv && len > ctx->remaining) { if (f->r->kept_body) { apr_brigade_cleanup(f->r->kept_body); f->r->kept_body = NULL; } ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, f->r, APLOGNO(01413) "Requested content-length of %" APR_OFF_T_FMT " is larger than the configured limit" " of %" APR_OFF_T_FMT, len, ctx->keep_body); return bail_out_on_error(b, f, HTTP_REQUEST_ENTITY_TOO_LARGE); } ctx->remaining -= len; /* pass any errors downstream */ if (rv != APR_SUCCESS) { if (f->r->kept_body) { apr_brigade_cleanup(f->r->kept_body); f->r->kept_body = NULL; } return rv; } /* all is well, set aside the buckets */ for (bucket = APR_BRIGADE_FIRST(b); bucket != APR_BRIGADE_SENTINEL(b); bucket = APR_BUCKET_NEXT(bucket)) { apr_bucket_copy(bucket, &e); APR_BRIGADE_INSERT_TAIL(f->r->kept_body, e); } return APR_SUCCESS; } typedef struct kept_body_filter_ctx { apr_off_t offset; apr_off_t remaining; } kept_body_ctx_t; /** * Initialisation of filter to handle a kept body on subrequests. * * If a body is to be reinserted into a subrequest, any chunking will have * been removed from the body during storage. We need to change the request * from Transfer-Encoding: chunked to an explicit Content-Length. */ static int kept_body_filter_init(ap_filter_t *f) { apr_off_t length = 0; request_rec *r = f->r; apr_bucket_brigade *kept_body = r->kept_body; if (kept_body) { apr_table_unset(r->headers_in, "Transfer-Encoding"); apr_brigade_length(kept_body, 1, &length); apr_table_setn(r->headers_in, "Content-Length", apr_off_t_toa(r->pool, length)); } return OK; } /** * Filter to handle a kept body on subrequests. * * If a body has been previously kept by the request, and if a subrequest wants * to re-insert the body into the request, this input filter makes it happen. */ static apr_status_t kept_body_filter(ap_filter_t *f, apr_bucket_brigade *b, ap_input_mode_t mode, apr_read_type_e block, apr_off_t readbytes) { request_rec *r = f->r; apr_bucket_brigade *kept_body = r->kept_body; kept_body_ctx_t *ctx = f->ctx; apr_bucket *ec, *e2; apr_status_t rv; /* just get out of the way of things we don't want. */ if (!kept_body || (mode != AP_MODE_READBYTES && mode != AP_MODE_GETLINE)) { return ap_get_brigade(f->next, b, mode, block, readbytes); } /* set up the context if it does not already exist */ if (!ctx) { f->ctx = ctx = apr_palloc(f->r->pool, sizeof(*ctx)); ctx->offset = 0; apr_brigade_length(kept_body, 1, &ctx->remaining); } /* kept_body is finished, send next filter */ if (ctx->remaining <= 0) { return ap_get_brigade(f->next, b, mode, block, readbytes); } /* send all of the kept_body, but no more */ if (readbytes > ctx->remaining) { readbytes = ctx->remaining; } /* send part of the kept_body */ if ((rv = apr_brigade_partition(kept_body, ctx->offset, &ec)) != APR_SUCCESS) { ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(01414) "apr_brigade_partition() failed on kept_body at %" APR_OFF_T_FMT, ctx->offset); return rv; } if ((rv = apr_brigade_partition(kept_body, ctx->offset + readbytes, &e2)) != APR_SUCCESS) { ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(01415) "apr_brigade_partition() failed on kept_body at %" APR_OFF_T_FMT, ctx->offset + readbytes); return rv; } do { apr_bucket *foo; const char *str; apr_size_t len; if (apr_bucket_copy(ec, &foo) != APR_SUCCESS) { /* As above; this should not fail since the bucket has * a known length, but just to be sure, this takes * care of uncopyable buckets that do somehow manage * to slip through. */ /* XXX: check for failure? */ apr_bucket_read(ec, &str, &len, APR_BLOCK_READ); apr_bucket_copy(ec, &foo); } APR_BRIGADE_INSERT_TAIL(b, foo); ec = APR_BUCKET_NEXT(ec); } while (ec != e2); ctx->remaining -= readbytes; ctx->offset += readbytes; return APR_SUCCESS; } /** * Check whether this filter is not already present. */ static int request_is_filter_present(request_rec * r, ap_filter_rec_t *fn) { ap_filter_t * f = r->input_filters; while (f) { if (f->frec == fn) { return 1; } f = f->next; } return 0; } /** * Insert filter hook. * * Add the KEEP_BODY filter to the request, if the admin wants to keep * the body using the KeptBodySize directive. * * As a precaution, any pre-existing instances of either the kept_body or * keep_body filters will be removed before the filter is added. * * @param r The request */ static void ap_request_insert_filter(request_rec * r) { request_dir_conf *conf = ap_get_module_config(r->per_dir_config, &request_module); if (r->kept_body) { if (!request_is_filter_present(r, kept_body_input_filter_handle)) { ap_add_input_filter_handle(kept_body_input_filter_handle, NULL, r, r->connection); } } else if (conf->keep_body) { if (!request_is_filter_present(r, kept_body_input_filter_handle)) { ap_add_input_filter_handle(keep_body_input_filter_handle, NULL, r, r->connection); } } } /** * Remove the kept_body and keep body filters from this specific request. */ static void ap_request_remove_filter(request_rec * r) { ap_filter_t * f = r->input_filters; while (f) { if (f->frec->filter_func.in_func == kept_body_filter || f->frec->filter_func.in_func == keep_body_filter) { ap_remove_input_filter(f); } f = f->next; } } static void *create_request_dir_config(apr_pool_t *p, char *dummy) { request_dir_conf *new = (request_dir_conf *) apr_pcalloc(p, sizeof(request_dir_conf)); new->keep_body_set = 0; /* unset */ new->keep_body = 0; /* don't by default */ return (void *) new; } static void *merge_request_dir_config(apr_pool_t *p, void *basev, void *addv) { request_dir_conf *new = (request_dir_conf *) apr_pcalloc(p, sizeof(request_dir_conf)); request_dir_conf *add = (request_dir_conf *) addv; request_dir_conf *base = (request_dir_conf *) basev; new->keep_body = (add->keep_body_set == 0) ? base->keep_body : add->keep_body; new->keep_body_set = add->keep_body_set || base->keep_body_set; return new; } static const char *set_kept_body_size(cmd_parms *cmd, void *dconf, const char *arg) { request_dir_conf *conf = dconf; char *end = NULL; if (APR_SUCCESS != apr_strtoff(&(conf->keep_body), arg, &end, 10) || conf->keep_body < 0 || *end) { return "KeptBodySize must be a valid size in bytes, or zero."; } conf->keep_body_set = 1; return NULL; } static const command_rec request_cmds[] = { AP_INIT_TAKE1("KeptBodySize", set_kept_body_size, NULL, ACCESS_CONF, "Maximum size of request bodies kept aside for use by filters"), { NULL } }; static void register_hooks(apr_pool_t *p) { keep_body_input_filter_handle = ap_register_input_filter(KEEP_BODY_FILTER, keep_body_filter, NULL, AP_FTYPE_RESOURCE); kept_body_input_filter_handle = ap_register_input_filter(KEPT_BODY_FILTER, kept_body_filter, kept_body_filter_init, AP_FTYPE_RESOURCE); ap_hook_insert_filter(ap_request_insert_filter, NULL, NULL, APR_HOOK_LAST); APR_REGISTER_OPTIONAL_FN(ap_request_insert_filter); APR_REGISTER_OPTIONAL_FN(ap_request_remove_filter); } AP_DECLARE_MODULE(request) = { STANDARD20_MODULE_STUFF, create_request_dir_config, /* create per-directory config structure */ merge_request_dir_config, /* merge per-directory config structures */ NULL, /* create per-server config structure */ NULL, /* merge per-server config structures */ request_cmds, /* command apr_table_t */ register_hooks /* register hooks */ };