pytest: check 429 handling
No binary (build failed)
—
(log yok)
| @@ -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) |
| @@ -535,6 +535,9 @@ class Httpd: | |
| ' <Location /curltest/echo>', | |
| ' SetHandler curltest-echo', | |
| ' </Location>', | |
| + | ' <Location /curltest/limit>', |
| + | ' SetHandler curltest-limit', |
| + | ' </Location>', |
| ' <Location /curltest/put>', | |
| ' SetHandler curltest-put', | |
| ' </Location>', | |
| @@ -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) = | |