diff --git a/tests/http/test_05_errors.py b/tests/http/test_05_errors.py index 4e5f552412..374cbedff3 100644 --- a/tests/http/test_05_errors.py +++ b/tests/http/test_05_errors.py @@ -217,3 +217,15 @@ class TestErrors: # - CURLE_RECV_ERROR (56) - some TLS backends fail with that assert r.exit_code in [35, 56], \ f'unexpected error {r.exit_code}\n{r.dump_logs()}' + + # Get a resource many times with limited requests + @pytest.mark.skipif(condition=not Env.have_h2_curl(), reason='curl without HTTP/2') + def test_05_10_limits(self, env: Env, httpd): + proto = 'h2' + count = 6 + curl = CurlClient(env=env) + url = f'https://{env.authority_for(env.domain1, proto)}/curltest/limit?id=[0-{count-1}]' + r = curl.http_download(urls=[url], alpn_proto=proto, extra_args=[ + '--parallel', '--retry', '5' + ]) + r.check_stats(count=count, http_status=200, exitcode=0) diff --git a/tests/http/testenv/httpd.py b/tests/http/testenv/httpd.py index 06fbf25f06..64ca796d13 100644 --- a/tests/http/testenv/httpd.py +++ b/tests/http/testenv/httpd.py @@ -535,6 +535,9 @@ class Httpd: ' ', ' SetHandler curltest-echo', ' ', + ' ', + ' SetHandler curltest-limit', + ' ', ' ', ' SetHandler curltest-put', ' ', diff --git a/tests/http/testenv/mod_curltest/mod_curltest.c b/tests/http/testenv/mod_curltest/mod_curltest.c index bc1c75e5d9..5e0f6400fb 100644 --- a/tests/http/testenv/mod_curltest/mod_curltest.c +++ b/tests/http/testenv/mod_curltest/mod_curltest.c @@ -824,11 +824,136 @@ cleanup: return DECLINED; } +struct curltest_limit_rec { + int rcount; + int rlimit; + apr_time_t end; + apr_time_t duration_sec; + struct apr_thread_mutex_t *lock; +}; + +static struct curltest_limit_rec limitrec = { + 0, 5, 0, 2 +}; + +static int curltest_limit_handler(request_rec *r) +{ + conn_rec *c = r->connection; + apr_bucket_brigade *bb; + apr_bucket *b; + apr_status_t rv; + const char *request_id = NULL; + int i, denied; + apr_time_t now; + + if(strcmp(r->handler, "curltest-limit")) { + return DECLINED; + } + if(r->method_number != M_GET) { + return DECLINED; + } + + if(r->args) { + apr_array_header_t *args = apr_cstr_split(r->args, "&", 1, r->pool); + for(i = 0; i < args->nelts; ++i) { + char *s, *val, *arg = APR_ARRAY_IDX(args, i, char *); + s = strchr(arg, '='); + if(s) { + *s = '\0'; + val = s + 1; + if(!strcmp("id", arg)) { + /* just an id for repeated requests with curl's URL globbing */ + request_id = val; + continue; + } + } + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "query parameter not " + "understood: '%s' in %s", + arg, r->args); + ap_die(HTTP_BAD_REQUEST, r); + return OK; + } + } + + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, "limit: processing"); + + now = apr_time_now(); + apr_thread_mutex_lock(limitrec.lock); + if(limitrec.end && (now > limitrec.end)) { + /* reset limit */ + ap_log_rerror(APLOG_MARK, APLOG_INFO, 0, r, "limit: reset"); + limitrec.rcount = 0; + limitrec.end = 0; + } + limitrec.rcount += 1; + denied = (limitrec.rcount > limitrec.rlimit); + if(denied) { + /* extend limit duration */ + limitrec.end = now + apr_time_from_sec(limitrec.duration_sec); + ap_log_rerror(APLOG_MARK, APLOG_INFO, 0, r, "limit: denied, %d request %s", + limitrec.rcount, request_id); + } + else { + ap_log_rerror(APLOG_MARK, APLOG_INFO, 0, r, "limit: ok, %d request %s", + limitrec.rcount, request_id); + } + apr_thread_mutex_unlock(limitrec.lock); + + r->status = denied ? 429 : 200; + r->clength = -1; + r->chunked = 1; + apr_table_unset(r->headers_out, "Content-Length"); + /* Discourage content-encodings */ + apr_table_unset(r->headers_out, "Content-Encoding"); + if(request_id) + apr_table_setn(r->headers_out, "request-id", request_id); + apr_table_setn(r->subprocess_env, "no-brotli", "1"); + apr_table_setn(r->subprocess_env, "no-gzip", "1"); + + if(denied) { + char *v = apr_psprintf(r->pool, "%d", limitrec.duration_sec); + apr_table_set(r->headers_out, "Retry-After", v); + } + + ap_set_content_type(r, "text/plain"); + + bb = apr_brigade_create(r->pool, c->bucket_alloc); + + apr_brigade_puts(bb, NULL, NULL, "The resource served with limits.\n"); + + /* flush response */ + b = apr_bucket_flush_create(c->bucket_alloc); + APR_BRIGADE_INSERT_TAIL(bb, b); + rv = ap_pass_brigade(r->output_filters, bb); + if(APR_SUCCESS != rv) + goto cleanup; + + /* we are done */ + b = apr_bucket_eos_create(c->bucket_alloc); + APR_BRIGADE_INSERT_TAIL(bb, b); + rv = ap_pass_brigade(r->output_filters, bb); + +cleanup: + if(rv == APR_SUCCESS || + r->status != HTTP_OK || + c->aborted) { + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, rv, r, "limit: done"); + return OK; + } + else { + /* no way to know what type of error occurred */ + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, rv, r, "limit failed"); + return AP_FILTER_ERROR; + } + return DECLINED; +} + static int curltest_post_config(apr_pool_t *p, apr_pool_t *plog, apr_pool_t *ptemp, server_rec *s) { void *data = NULL; const char *key = "mod_curltest_init_counter"; + apr_status_t rv; (void)p; (void)plog; @@ -842,9 +967,7 @@ static int curltest_post_config(apr_pool_t *p, apr_pool_t *plog, return APR_SUCCESS; } - /* mess with the overall server here */ - - return APR_SUCCESS; + return apr_thread_mutex_create(&limitrec.lock, APR_THREAD_MUTEX_DEFAULT, p); } static void curltest_hooks(apr_pool_t *pool) @@ -861,6 +984,7 @@ static void curltest_hooks(apr_pool_t *pool) ap_hook_handler(curltest_tweak_handler, NULL, NULL, APR_HOOK_MIDDLE); ap_hook_handler(curltest_1_1_required, NULL, NULL, APR_HOOK_MIDDLE); ap_hook_handler(curltest_sslinfo_handler, NULL, NULL, APR_HOOK_MIDDLE); + ap_hook_handler(curltest_limit_handler, NULL, NULL, APR_HOOK_MIDDLE); } AP_DECLARE_MODULE(curltest) =