Reject an EOF without close_notify in the Mbed TLS and wolfSSL read (#2649)

Mbed TLS and wolfSSL read a TLS EOF without close_notify as a clean end, so a response delimited by the connection close could be cut short and still come back complete. Both reads now end cleanly only on a close_notify, as on OpenSSL.
This commit is contained in:
metsw24-max authored and GitHub committed 2026-10-27 18:31:55 -04:00
1 parent 578e4815b7
commit 25ba40f6fe
2 files changed
+132 -10

No files matched your search

+120
View File
@@ -72,6 +72,17 @@ namespace tls {
ca_store_t create_ca_store(const char *pem, size_t len);
bool set_verify_callback(ctx_t ctx, VerifyCallback callback);
std::vector<std::string> get_ca_names(ctx_t ctx);
ctx_t create_server_context();
void free_context(ctx_t ctx);
bool set_server_cert_file(ctx_t ctx, const char *cert_path,
const char *key_path, const char *password);
session_t create_session(ctx_t ctx, socket_t sock);
void free_session(session_t session);
bool accept_nonblocking(session_t session, socket_t sock, time_t timeout_sec,
time_t timeout_usec, TlsError *err);
ssize_t read(session_t session, void *buf, size_t len, TlsError &err);
ssize_t write(session_t session, const void *buf, size_t len, TlsError &err);
void shutdown(session_t session, bool graceful);
#ifdef CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN
namespace impl {
SecTrustSettingsResult
@@ -24067,6 +24078,115 @@ TEST(StreamBodyEndTest, BodyEndingAtConnectionClose) {
});
}
#ifdef CPPHTTPLIB_SSL_ENABLED
// Serves `response` to one request, then closes with or without a close_notify
template <typename Fn>
static void with_single_tls_response(const std::string &response,
bool close_notify, Fn fn) {
#ifndef _WIN32
signal(SIGPIPE, SIG_IGN);
#endif
std::promise<int> port_promise;
auto port_future = port_promise.get_future();
auto server_thread = std::thread([&port_promise, response, close_notify] {
auto srv = ::socket(AF_INET, SOCK_STREAM, 0);
default_socket_options(srv);
sockaddr_in addr{};
addr.sin_family = AF_INET;
addr.sin_port = htons(0);
::inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);
if (::bind(srv, reinterpret_cast<sockaddr *>(&addr), sizeof(addr)) != 0 ||
::listen(srv, 1) != 0) {
port_promise.set_value(-1);
detail::close_socket(srv);
return;
}
socklen_t addr_len = sizeof(addr);
::getsockname(srv, reinterpret_cast<sockaddr *>(&addr), &addr_len);
port_promise.set_value(static_cast<int>(ntohs(addr.sin_port)));
auto cli = ::accept(srv, nullptr, nullptr);
if (cli != INVALID_SOCKET) {
auto ctx = tls::create_server_context();
tls::set_server_cert_file(ctx, SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE,
nullptr);
auto session = tls::create_session(ctx, cli);
tls::TlsError err;
if (tls::accept_nonblocking(session, cli, 5, 0, &err)) {
// The request head first, so that closing the socket sends a FIN
// rather than an RST for the unread bytes.
std::string req;
char buf[256];
while (req.find("\r\n\r\n") == std::string::npos) {
auto n = tls::read(session, buf, sizeof(buf), err);
if (n <= 0) { break; }
req.append(buf, static_cast<size_t>(n));
}
size_t sent = 0;
while (sent < response.size()) {
auto n = tls::write(session, response.data() + sent,
response.size() - sent, err);
if (n <= 0) { break; }
sent += static_cast<size_t>(n);
}
if (close_notify) { tls::shutdown(session, true); }
}
tls::free_session(session);
detail::close_socket(cli);
tls::free_context(ctx);
}
detail::close_socket(srv);
});
auto se = detail::scope_exit([&] { server_thread.join(); });
auto port = port_future.get();
ASSERT_GT(port, 0);
SSLClient cli("127.0.0.1", port);
cli.enable_server_certificate_verification(false);
fn(cli);
}
// RFC 8446 6.1: a close-delimited body is complete only after a close_notify
TEST(SSLClientTest, UnframedBodyEndsOnlyAtCloseNotify) {
const std::string close_delimited = "HTTP/1.1 200 OK\r\n"
"Connection: close\r\n"
"\r\n"
"hello";
with_single_tls_response(close_delimited, true, [](SSLClient &cli) {
auto res = cli.Get("/");
ASSERT_TRUE(res) << to_string(res.error());
EXPECT_EQ("hello", res->body);
});
with_single_tls_response(close_delimited, false, [](SSLClient &cli) {
auto res = cli.Get("/");
ASSERT_FALSE(res);
EXPECT_EQ(Error::Read, res.error());
});
with_single_tls_response(close_delimited, false, [](SSLClient &cli) {
auto handle = cli.open_stream("GET", "/");
ASSERT_TRUE(handle.is_valid()) << to_string(handle.error);
read_all(handle);
EXPECT_TRUE(handle.has_read_error());
});
// A framed body ends where its framing says, with or without the alert
const std::string framed = "HTTP/1.1 200 OK\r\n"
"Content-Length: 5\r\n"
"Connection: close\r\n"
"\r\n"
"hello";
with_single_tls_response(framed, false, [](SSLClient &cli) {
auto res = cli.Get("/");
ASSERT_TRUE(res) << to_string(res.error());
EXPECT_EQ("hello", res->body);
});
}
#endif
TEST(InterimResponseTest, SkippedWithTheirHeaderFields) {
const char *responses[] = {
"HTTP/1.1 103 Early Hints\r\n"