diff --git a/httplib.h b/httplib.h index 1be95c20..375ca9bb 100644 --- a/httplib.h +++ b/httplib.h @@ -8658,6 +8658,13 @@ inline bool is_chunked_transfer_encoding(const Headers &headers) { return case_ignore::equal(last_coding, "chunked"); } +// RFC 9112 6.1: a message framed by Transfer-Encoding next to a Content-Length, +// or in HTTP/1.0, leaves the connection unfit to carry another one. +template inline bool has_faulty_transfer_encoding(const T &msg) { + return msg.has_header("Transfer-Encoding") && + (msg.has_header("Content-Length") || msg.version == "HTTP/1.0"); +} + inline bool has_conflicting_content_length(const Headers &headers) { // RFC 9112 §6.3: a message carrying both Transfer-Encoding and a non-zero // Content-Length is framed ambiguously. The body readers here delimit it by @@ -14913,10 +14920,7 @@ Server::process_request(Stream &strm, const std::string &remote_addr, // RFC 9112 §6.1: a Transfer-Encoding alongside a Content-Length, or in an // HTTP/1.0 request, is honoured, but the connection closes after the // response. - if (req.has_header("Transfer-Encoding") && - (req.has_header("Content-Length") || req.version == "HTTP/1.0")) { - connection_closed = true; - } + if (detail::has_faulty_transfer_encoding(req)) { connection_closed = true; } // RFC 9112 §3.2: Reject more than one Host field line, an invalid Host // value, or none in HTTP/1.1, so that every recipient of the request sees @@ -16760,11 +16764,16 @@ inline bool ClientImpl::process_request(Stream &strm, Request &req, // whose final transfer coding is not chunked is not ambiguous: its body // runs until the server closes the connection, so it is not rejected. // HEAD/204 are excluded above and a 304 carries no body. - if (res.status != StatusCode::NotModified_304 && - detail::has_conflicting_content_length(res.headers)) { - error = Error::Read; - output_error_log(error, &req); - return false; + if (res.status != StatusCode::NotModified_304) { + if (detail::has_conflicting_content_length(res.headers)) { + error = Error::Read; + output_error_log(error, &req); + return false; + } + if (detail::has_faulty_transfer_encoding(res)) { + std::lock_guard guard(socket_mutex_); + socket_should_be_closed_when_request_is_done_ = true; + } } auto redirect = 300 < res.status && res.status < 400 && diff --git a/test/test.cc b/test/test.cc index 04f5a394..43abcd68 100644 --- a/test/test.cc +++ b/test/test.cc @@ -22664,6 +22664,29 @@ TEST(ClientResponseSmugglingTest, BodylessResponseNotRejected) { } } +// RFC 9112 6.1: Transfer-Encoding next to a Content-Length, or in an HTTP/1.0 +// response, is faulty framing, so the connection is not reused after it. +TEST(ClientResponseSmugglingTest, FaultyTransferEncodingClosesConnection) { + for (const char *response : {"HTTP/1.1 200 OK\r\n" + "Content-Length: 0\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "5\r\nhello\r\n0\r\n\r\n", + "HTTP/1.0 200 Connection established\r\n" + "Connection: keep-alive\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "5\r\nhello\r\n0\r\n\r\n"}) { + with_single_response(response, [&](Client &cli) { + cli.set_keep_alive(true); + auto res = cli.Get("/"); + ASSERT_TRUE(res) << response; + EXPECT_EQ("hello", res->body); + EXPECT_FALSE(cli.is_socket_open()) << response; + }); + } +} + // A payload max length of 0 means no limit, however the body is framed. TEST(PayloadMaxLengthZeroTest, ClientReadsChunkedAndUnframedResponse) { for (const char *response : {"HTTP/1.1 200 OK\r\n"