| /* 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. |
| */ |
| |
| #include "httpd.h" |
| #include "http_config.h" |
| #include "http_log.h" |
| #include "util_filter.h" |
| |
| #include "mod_ratelimit.h" |
| |
| #define RATE_LIMIT_FILTER_NAME "RATE_LIMIT" |
| #define RATE_INTERVAL_MS (200) |
| |
| typedef enum rl_state_e |
| { |
| RATE_LIMIT, |
| RATE_FULLSPEED |
| } rl_state_e; |
| |
| typedef struct rl_ctx_t |
| { |
| int speed; |
| int chunk_size; |
| int burst; |
| int do_sleep; |
| rl_state_e state; |
| apr_bucket_brigade *tmpbb; |
| apr_bucket_brigade *holdingbb; |
| } rl_ctx_t; |
| |
| #if defined(RLFDEBUG) |
| static void brigade_dump(request_rec *r, apr_bucket_brigade *bb) |
| { |
| apr_bucket *e; |
| int i = 0; |
| |
| for (e = APR_BRIGADE_FIRST(bb); |
| e != APR_BRIGADE_SENTINEL(bb); e = APR_BUCKET_NEXT(e), i++) { |
| ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(03193) |
| "brigade: [%d] %s", i, e->type->name); |
| |
| } |
| } |
| #endif /* RLFDEBUG */ |
| |
| static apr_status_t |
| rate_limit_filter(ap_filter_t *f, apr_bucket_brigade *bb) |
| { |
| apr_status_t rv = APR_SUCCESS; |
| rl_ctx_t *ctx = f->ctx; |
| apr_bucket_alloc_t *ba = f->r->connection->bucket_alloc; |
| |
| /* Set up our rl_ctx_t on first use */ |
| if (ctx == NULL) { |
| const char *rl = NULL; |
| int ratelimit; |
| int burst = 0; |
| |
| /* no subrequests. */ |
| if (f->r->main != NULL) { |
| ap_remove_output_filter(f); |
| return ap_pass_brigade(f->next, bb); |
| } |
| |
| /* Configuration: rate limit */ |
| rl = apr_table_get(f->r->subprocess_env, "rate-limit"); |
| |
| if (rl == NULL) { |
| ap_remove_output_filter(f); |
| return ap_pass_brigade(f->next, bb); |
| } |
| |
| /* rl is in kilo bytes / second */ |
| ratelimit = atoi(rl) * 1024; |
| if (ratelimit <= 0) { |
| /* remove ourselves */ |
| ap_log_rerror(APLOG_MARK, APLOG_INFO, 0, f->r, |
| APLOGNO(03488) "rl: disabling: rate-limit = %s (too high?)", rl); |
| ap_remove_output_filter(f); |
| return ap_pass_brigade(f->next, bb); |
| } |
| |
| /* Configuration: optional initial burst */ |
| rl = apr_table_get(f->r->subprocess_env, "rate-initial-burst"); |
| if (rl != NULL) { |
| burst = atoi(rl) * 1024; |
| if (burst <= 0) { |
| ap_log_rerror(APLOG_MARK, APLOG_INFO, 0, f->r, |
| APLOGNO(03489) "rl: disabling burst: rate-initial-burst = %s (too high?)", rl); |
| burst = 0; |
| } |
| } |
| |
| /* Set up our context */ |
| ctx = apr_palloc(f->r->pool, sizeof(rl_ctx_t)); |
| f->ctx = ctx; |
| ctx->state = RATE_LIMIT; |
| ctx->speed = ratelimit; |
| ctx->burst = burst; |
| ctx->do_sleep = 0; |
| |
| /* calculate how many bytes / interval we want to send */ |
| /* speed is bytes / second, so, how many (speed / 1000 % interval) */ |
| ctx->chunk_size = (ctx->speed / (1000 / RATE_INTERVAL_MS)); |
| ctx->tmpbb = apr_brigade_create(f->r->pool, ba); |
| ctx->holdingbb = apr_brigade_create(f->r->pool, ba); |
| } |
| else { |
| APR_BRIGADE_PREPEND(bb, ctx->holdingbb); |
| } |
| |
| while (!APR_BRIGADE_EMPTY(bb)) { |
| apr_bucket *e; |
| |
| if (ctx->state == RATE_FULLSPEED) { |
| /* Find where we 'stop' going full speed. */ |
| for (e = APR_BRIGADE_FIRST(bb); |
| e != APR_BRIGADE_SENTINEL(bb); e = APR_BUCKET_NEXT(e)) { |
| if (AP_RL_BUCKET_IS_END(e)) { |
| apr_brigade_split_ex(bb, e, ctx->holdingbb); |
| ctx->state = RATE_LIMIT; |
| break; |
| } |
| } |
| |
| e = apr_bucket_flush_create(ba); |
| APR_BRIGADE_INSERT_TAIL(bb, e); |
| rv = ap_pass_brigade(f->next, bb); |
| apr_brigade_cleanup(bb); |
| |
| if (rv != APR_SUCCESS) { |
| ap_log_rerror(APLOG_MARK, APLOG_TRACE1, rv, f->r, APLOGNO(01455) |
| "rl: full speed brigade pass failed."); |
| return rv; |
| } |
| } |
| else { |
| for (e = APR_BRIGADE_FIRST(bb); |
| e != APR_BRIGADE_SENTINEL(bb); e = APR_BUCKET_NEXT(e)) { |
| if (AP_RL_BUCKET_IS_START(e)) { |
| apr_brigade_split_ex(bb, e, ctx->holdingbb); |
| ctx->state = RATE_FULLSPEED; |
| break; |
| } |
| } |
| |
| while (!APR_BRIGADE_EMPTY(bb)) { |
| apr_off_t len = ctx->chunk_size + ctx->burst; |
| |
| APR_BRIGADE_CONCAT(ctx->tmpbb, bb); |
| |
| /* |
| * Pull next chunk of data; the initial amount is our |
| * burst allotment (if any) plus a chunk. All subsequent |
| * iterations are just chunks with whatever remaining |
| * burst amounts we have left (in case not done in the |
| * first bucket). |
| */ |
| rv = apr_brigade_partition(ctx->tmpbb, len, &e); |
| if (rv != APR_SUCCESS && rv != APR_INCOMPLETE) { |
| ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, f->r, APLOGNO(01456) |
| "rl: partition failed."); |
| return rv; |
| } |
| /* Send next metadata now if any */ |
| while (e != APR_BRIGADE_SENTINEL(ctx->tmpbb) |
| && APR_BUCKET_IS_METADATA(e)) { |
| e = APR_BUCKET_NEXT(e); |
| } |
| if (e != APR_BRIGADE_SENTINEL(ctx->tmpbb)) { |
| apr_brigade_split_ex(ctx->tmpbb, e, bb); |
| } |
| else { |
| apr_brigade_length(ctx->tmpbb, 1, &len); |
| } |
| |
| /* |
| * Adjust the burst amount depending on how much |
| * we've done up to now. |
| */ |
| if (ctx->burst) { |
| ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, f->r, |
| APLOGNO(03485) "rl: burst %d; len %"APR_OFF_T_FMT, ctx->burst, len); |
| if (len < ctx->burst) { |
| ctx->burst -= len; |
| } |
| else { |
| ctx->burst = 0; |
| } |
| } |
| |
| e = APR_BRIGADE_LAST(ctx->tmpbb); |
| if (APR_BUCKET_IS_EOS(e)) { |
| ap_remove_output_filter(f); |
| } |
| else if (!APR_BUCKET_IS_FLUSH(e)) { |
| if (APR_BRIGADE_EMPTY(bb)) { |
| /* Wait for more (or next call) */ |
| break; |
| } |
| e = apr_bucket_flush_create(ba); |
| APR_BRIGADE_INSERT_TAIL(ctx->tmpbb, e); |
| } |
| |
| #if defined(RLFDEBUG) |
| brigade_dump(f->r, ctx->tmpbb); |
| brigade_dump(f->r, bb); |
| #endif /* RLFDEBUG */ |
| |
| if (ctx->do_sleep) { |
| apr_sleep(RATE_INTERVAL_MS * 1000); |
| } |
| else { |
| ctx->do_sleep = 1; |
| } |
| |
| rv = ap_pass_brigade(f->next, ctx->tmpbb); |
| apr_brigade_cleanup(ctx->tmpbb); |
| |
| if (rv != APR_SUCCESS) { |
| /* Most often, user disconnects from stream */ |
| ap_log_rerror(APLOG_MARK, APLOG_TRACE1, rv, f->r, APLOGNO(01457) |
| "rl: brigade pass failed."); |
| return rv; |
| } |
| } |
| } |
| |
| if (!APR_BRIGADE_EMPTY(ctx->holdingbb)) { |
| /* Any rate-limited data in tmpbb is sent unlimited along |
| * with the rest. |
| */ |
| APR_BRIGADE_CONCAT(bb, ctx->tmpbb); |
| APR_BRIGADE_CONCAT(bb, ctx->holdingbb); |
| } |
| } |
| |
| #if defined(RLFDEBUG) |
| brigade_dump(f->r, ctx->tmpbb); |
| #endif /* RLFDEBUG */ |
| |
| /* Save remaining tmpbb with the correct lifetime for the next call */ |
| return ap_save_brigade(f, &ctx->holdingbb, &ctx->tmpbb, f->r->pool); |
| } |
| |
| |
| static apr_status_t |
| rl_bucket_read(apr_bucket *b, const char **str, |
| apr_size_t *len, apr_read_type_e block) |
| { |
| *str = NULL; |
| *len = 0; |
| return APR_SUCCESS; |
| } |
| |
| AP_RL_DECLARE(apr_bucket *) |
| ap_rl_end_create(apr_bucket_alloc_t *list) |
| { |
| apr_bucket *b = apr_bucket_alloc(sizeof(*b), list); |
| |
| APR_BUCKET_INIT(b); |
| b->free = apr_bucket_free; |
| b->list = list; |
| b->length = 0; |
| b->start = 0; |
| b->data = NULL; |
| b->type = &ap_rl_bucket_type_end; |
| |
| return b; |
| } |
| |
| AP_RL_DECLARE(apr_bucket *) |
| ap_rl_start_create(apr_bucket_alloc_t *list) |
| { |
| apr_bucket *b = apr_bucket_alloc(sizeof(*b), list); |
| |
| APR_BUCKET_INIT(b); |
| b->free = apr_bucket_free; |
| b->list = list; |
| b->length = 0; |
| b->start = 0; |
| b->data = NULL; |
| b->type = &ap_rl_bucket_type_start; |
| |
| return b; |
| } |
| |
| |
| |
| AP_RL_DECLARE_DATA const apr_bucket_type_t ap_rl_bucket_type_end = { |
| "RL_END", 5, APR_BUCKET_METADATA, |
| apr_bucket_destroy_noop, |
| rl_bucket_read, |
| apr_bucket_setaside_noop, |
| apr_bucket_split_notimpl, |
| apr_bucket_simple_copy |
| }; |
| |
| |
| AP_RL_DECLARE_DATA const apr_bucket_type_t ap_rl_bucket_type_start = { |
| "RL_START", 5, APR_BUCKET_METADATA, |
| apr_bucket_destroy_noop, |
| rl_bucket_read, |
| apr_bucket_setaside_noop, |
| apr_bucket_split_notimpl, |
| apr_bucket_simple_copy |
| }; |
| |
| |
| |
| |
| static void register_hooks(apr_pool_t *p) |
| { |
| /* run after mod_deflate etc etc, but not at connection level, ie, mod_ssl. */ |
| ap_register_output_filter(RATE_LIMIT_FILTER_NAME, rate_limit_filter, |
| NULL, AP_FTYPE_CONNECTION - 1); |
| } |
| |
| AP_DECLARE_MODULE(ratelimit) = { |
| STANDARD20_MODULE_STUFF, |
| NULL, /* create per-directory config structure */ |
| NULL, /* merge per-directory config structures */ |
| NULL, /* create per-server config structure */ |
| NULL, /* merge per-server config structures */ |
| NULL, /* command apr_table_t */ |
| register_hooks |
| }; |