From: Stefan Eissing Date: Thu, 12 Sep 2024 08:03:33 +0000 (+0200) Subject: http2: when uploading data from stdin, fix eos forwarding X-Git-Tag: curl-8_10_1~39 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=70d3a9b6aa69ffdd9435e168463cfabd21e17cd1;p=thirdparty%2Fcurl.git http2: when uploading data from stdin, fix eos forwarding When uploading data from stdin ('-T -'), and the EOS was only detected on a 0-length read, the EOS was not forwarded to the filters. This led HTTP/2 to hang on not forwarding this to the server. Added test_07_14 to reproduce and verify. Fixes #14870 Reported-by: nekopsykose on github Closes #14877 --- diff --git a/lib/http2.c b/lib/http2.c index bc50a2f7a7..df3e6f0df3 100644 --- a/lib/http2.c +++ b/lib/http2.c @@ -1679,12 +1679,11 @@ static ssize_t req_body_read_callback(nghttp2_session *session, CURL_TRC_CF(data_s, cf, "[%d] req_body_read(len=%zu) eos=%d -> %zd, %d", stream_id, length, stream->body_eos, nread, result); - if(nread == 0) - return NGHTTP2_ERR_DEFERRED; - if(stream->body_eos && Curl_bufq_is_empty(&stream->sendbuf)) + if(stream->body_eos && Curl_bufq_is_empty(&stream->sendbuf)) { *data_flags = NGHTTP2_DATA_FLAG_EOF; - - return nread; + return nread; + } + return (nread == 0)? NGHTTP2_ERR_DEFERRED : nread; } #if !defined(CURL_DISABLE_VERBOSE_STRINGS) diff --git a/lib/request.c b/lib/request.c index fb75e5577c..6b2784c3ff 100644 --- a/lib/request.c +++ b/lib/request.c @@ -214,15 +214,19 @@ static CURLcode xfer_send(struct Curl_easy *data, eos = TRUE; } result = Curl_xfer_send(data, buf, blen, eos, pnwritten); - if(!result && *pnwritten) { - if(hds_len) - Curl_debug(data, CURLINFO_HEADER_OUT, (char *)buf, - CURLMIN(hds_len, *pnwritten)); - if(*pnwritten > hds_len) { - size_t body_len = *pnwritten - hds_len; - Curl_debug(data, CURLINFO_DATA_OUT, (char *)buf + hds_len, body_len); - data->req.writebytecount += body_len; - Curl_pgrsSetUploadCounter(data, data->req.writebytecount); + if(!result) { + if(eos && (blen == *pnwritten)) + data->req.eos_sent = TRUE; + if(*pnwritten) { + if(hds_len) + Curl_debug(data, CURLINFO_HEADER_OUT, (char *)buf, + CURLMIN(hds_len, *pnwritten)); + if(*pnwritten > hds_len) { + size_t body_len = *pnwritten - hds_len; + Curl_debug(data, CURLINFO_DATA_OUT, (char *)buf + hds_len, body_len); + data->req.writebytecount += body_len; + Curl_pgrsSetUploadCounter(data, data->req.writebytecount); + } } } return result; @@ -304,8 +308,17 @@ static CURLcode req_flush(struct Curl_easy *data) return Curl_xfer_flush(data); } - if(!data->req.upload_done && data->req.eos_read && - Curl_bufq_is_empty(&data->req.sendbuf)) { + if(data->req.eos_read && !data->req.eos_sent) { + char tmp; + size_t nwritten; + result = xfer_send(data, &tmp, 0, 0, &nwritten); + if(result) + return result; + DEBUGASSERT(data->req.eos_sent); + } + + if(!data->req.upload_done && data->req.eos_read && data->req.eos_sent) { + DEBUGASSERT(Curl_bufq_is_empty(&data->req.sendbuf)); if(data->req.shutdown) { bool done; result = Curl_xfer_send_shutdown(data, &done); diff --git a/lib/request.h b/lib/request.h index fb3f9f1168..c53c3eb5ae 100644 --- a/lib/request.h +++ b/lib/request.h @@ -130,6 +130,7 @@ struct SingleRequest { BIT(download_done); /* set to TRUE when download is complete */ BIT(eos_written); /* iff EOS has been written to client */ BIT(eos_read); /* iff EOS has been read from the client */ + BIT(eos_sent); /* iff EOS has been sent to the server */ BIT(rewind_read); /* iff reader needs rewind at next start */ BIT(upload_done); /* set to TRUE when all request data has been sent */ BIT(upload_aborted); /* set to TRUE when upload was aborted. Will also diff --git a/lib/transfer.c b/lib/transfer.c index 0f42b3f2b4..ab8fd72431 100644 --- a/lib/transfer.c +++ b/lib/transfer.c @@ -1253,8 +1253,8 @@ CURLcode Curl_xfer_send(struct Curl_easy *data, else if(!result && *pnwritten) data->info.request_size += *pnwritten; - DEBUGF(infof(data, "Curl_xfer_send(len=%zu) -> %d, %zu", - blen, result, *pnwritten)); + DEBUGF(infof(data, "Curl_xfer_send(len=%zu, eos=%d) -> %d, %zu", + blen, eos, result, *pnwritten)); return result; } diff --git a/tests/http/test_07_upload.py b/tests/http/test_07_upload.py index 8dc06d45da..70cc5918d6 100644 --- a/tests/http/test_07_upload.py +++ b/tests/http/test_07_upload.py @@ -155,6 +155,25 @@ class TestUpload: respdata = open(curl.response_file(i)).readlines() assert respdata == indata + # upload from stdin, issue #14870 + @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3']) + @pytest.mark.parametrize("indata", [ + '', '1', '123\n456andsomething\n\n' + ]) + def test_07_14_upload_stdin(self, env: Env, httpd, nghttpx, proto, indata): + if proto == 'h3' and not env.have_h3(): + pytest.skip("h3 not supported") + if proto == 'h3' and env.curl_uses_lib('msh3'): + pytest.skip("msh3 stalls here") + count = 1 + curl = CurlClient(env=env) + url = f'https://{env.authority_for(env.domain1, proto)}/curltest/put?id=[0-{count-1}]' + r = curl.http_put(urls=[url], data=indata, alpn_proto=proto) + r.check_stats(count=count, http_status=200, exitcode=0) + for i in range(count): + respdata = open(curl.response_file(i)).readlines() + assert respdata == [f'{len(indata)}'] + # upload data parallel, check that they were echoed @pytest.mark.parametrize("proto", ['h2', 'h3']) def test_07_20_upload_parallel(self, env: Env, httpd, nghttpx, repeat, proto): @@ -581,3 +600,21 @@ class TestUpload: '--expect100-timeout', f'{read_delay-1}' ]) r.check_stats(count=1, http_status=200, exitcode=0) + + # speed limited on echo handler + @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3']) + def test_07_51_echo_speed_limit(self, env: Env, httpd, nghttpx, proto, repeat): + if proto == 'h3' and not env.have_h3(): + pytest.skip("h3 not supported") + count = 1 + fdata = os.path.join(env.gen_dir, 'data-100k') + speed_limit = 50 * 1024 + curl = CurlClient(env=env) + url = f'https://{env.authority_for(env.domain1, proto)}/curltest/echo?id=[0-0]' + r = curl.http_upload(urls=[url], data=f'@{fdata}', alpn_proto=proto, + with_headers=True, extra_args=[ + '--limit-rate', f'{speed_limit}' + ]) + r.check_response(count=count, http_status=200) + up_speed = r.stats[0]['speed_upload'] + assert (speed_limit * 0.5) <= up_speed <= (speed_limit * 1.5), f'{r.stats[0]}'