http2: detect session being closed on ingress handling

nghttp2 will on its own send GOAWAY frames, closing the connection, when
internal processing of frames runs into errors. This may not become
visible in a direct error code from a call to nghttp2.

Check for session being closed on ingress processing (on sending, we
already did that) and report an error if so. In addition, monitor
outgoing GOAWAY not initiated by us so that the user will get a fail
message when that happens.

Add some more long response header tests.

Closes #16544
This commit is contained in:
Stefan Eissing 2025-03-04 12:31:32 +01:00 committed by Daniel Stenberg
parent aa042210d9
commit e542fd9da1
No known key found for this signature in database
GPG key ID: 5CC908FDB71E12C2
2 changed files with 55 additions and 5 deletions

View file

@ -648,10 +648,8 @@ static int h2_process_pending_input(struct Curl_cfilter *cf,
rv = nghttp2_session_mem_recv(ctx->h2, (const uint8_t *)buf, blen);
if(rv < 0) {
failf(data,
"process_pending_input: nghttp2_session_mem_recv() returned "
"%zd:%s", rv, nghttp2_strerror((int)rv));
*err = CURLE_RECV_ERROR;
failf(data, "nghttp2 error %zd: %s", rv, nghttp2_strerror((int)rv));
*err = CURLE_HTTP2;
return -1;
}
Curl_bufq_skip(&ctx->inbufq, (size_t)rv);
@ -1292,6 +1290,7 @@ static int on_frame_send(nghttp2_session *session, const nghttp2_frame *frame,
void *userp)
{
struct Curl_cfilter *cf = userp;
struct cf_h2_ctx *ctx = cf->ctx;
struct Curl_easy *data = CF_DATA_CURRENT(cf);
(void)session;
@ -1303,6 +1302,13 @@ static int on_frame_send(nghttp2_session *session, const nghttp2_frame *frame,
buffer[len] = 0;
CURL_TRC_CF(data, cf, "[%d] -> %s", frame->hd.stream_id, buffer);
}
if((frame->hd.type == NGHTTP2_GOAWAY) && !ctx->sent_goaway) {
/* A GOAWAY not initiated by us, but by nghttp2 itself on detecting
* a protocol error on the connection */
failf(data, "nghttp2 shuts down connection with error %d: %s",
frame->goaway.error_code,
nghttp2_http2_strerror(frame->goaway.error_code));
}
return 0;
}
#endif /* !CURL_DISABLE_VERBOSE_STRINGS */
@ -2036,6 +2042,11 @@ static CURLcode h2_progress_ingress(struct Curl_cfilter *cf,
CURLcode result = CURLE_OK;
ssize_t nread;
if(should_close_session(ctx)) {
CURL_TRC_CF(data, cf, "progress ingress, session is closed");
return CURLE_HTTP2;
}
/* Process network input buffer fist */
if(!Curl_bufq_is_empty(&ctx->inbufq)) {
CURL_TRC_CF(data, cf, "Process %zu bytes in connection buffer",
@ -2610,6 +2621,7 @@ static CURLcode cf_h2_shutdown(struct Curl_cfilter *cf,
CF_DATA_SAVE(save, cf, data);
if(!ctx->sent_goaway) {
ctx->sent_goaway = TRUE;
rv = nghttp2_submit_goaway(ctx->h2, NGHTTP2_FLAG_NONE,
ctx->local_max_sid, 0,
(const uint8_t *)"shutdown",
@ -2620,7 +2632,6 @@ static CURLcode cf_h2_shutdown(struct Curl_cfilter *cf,
result = CURLE_SEND_ERROR;
goto out;
}
ctx->sent_goaway = TRUE;
}
/* GOAWAY submitted, process egress and ingress until nghttp2 is done. */
result = CURLE_OK;

View file

@ -203,3 +203,42 @@ class TestBasic:
r.check_exit_code(16) # CURLE_HTTP2
else:
r.check_exit_code(100) # CURLE_TOO_LARGE
# http: several response headers, together > 256 KB
# nghttp2 error -905: Too many CONTINUATION frames following a HEADER frame
@pytest.mark.skipif(condition=not Env.httpd_is_at_least('2.4.64'),
reason='httpd must be at least 2.4.64')
@pytest.mark.parametrize("proto", ['http/1.1', 'h2'])
def test_01_14_gigalarge_resp_headers(self, env: Env, httpd, proto):
httpd.set_extra_config('base', [
'LogLevel http2:trace2',
f'H2MaxHeaderBlockLen {1024 * 1024}',
])
httpd.reload()
curl = CurlClient(env=env)
url = f'https://{env.authority_for(env.domain1, proto)}' \
f'/curltest/tweak?x-hd={256 * 1024}'
r = curl.http_get(url=url, alpn_proto=proto, extra_args=[])
if proto == 'h2':
r.check_exit_code(16) # CURLE_HTTP2
else:
r.check_exit_code(0) # 1.1 can do
# http: one response header > 256 KB
@pytest.mark.skipif(condition=not Env.httpd_is_at_least('2.4.64'),
reason='httpd must be at least 2.4.64')
@pytest.mark.parametrize("proto", ['http/1.1', 'h2'])
def test_01_15_gigalarge_resp_headers(self, env: Env, httpd, proto):
httpd.set_extra_config('base', [
'LogLevel http2:trace2',
f'H2MaxHeaderBlockLen {1024 * 1024}',
])
httpd.reload()
curl = CurlClient(env=env)
url = f'https://{env.authority_for(env.domain1, proto)}' \
f'/curltest/tweak?x-hd1={256 * 1024}'
r = curl.http_get(url=url, alpn_proto=proto, extra_args=[])
if proto == 'h2':
r.check_exit_code(16) # CURLE_HTTP2
else:
r.check_exit_code(100) # CURLE_TOO_LARGE