Skip to content

Commit 6137446

Browse files
committed
Add SSL support for open_stream_direct() and corresponding tests
1 parent f07bffe commit 6137446

File tree

2 files changed

+141
-2
lines changed

2 files changed

+141
-2
lines changed

httplib.h

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1402,7 +1402,25 @@ struct ClientConnection {
14021402

14031403
// Move-only semantics
14041404
ClientConnection() = default;
1405-
~ClientConnection() = default;
1405+
1406+
~ClientConnection() {
1407+
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
1408+
if (ssl) {
1409+
// Non-graceful shutdown to avoid waiting for peer's close_notify
1410+
// This is acceptable since we're closing the connection anyway
1411+
SSL_free(ssl);
1412+
ssl = nullptr;
1413+
}
1414+
#endif
1415+
if (sock != INVALID_SOCKET) {
1416+
#ifdef _WIN32
1417+
closesocket(sock);
1418+
#else
1419+
close(sock);
1420+
#endif
1421+
sock = INVALID_SOCKET;
1422+
}
1423+
}
14061424

14071425
ClientConnection(const ClientConnection &) = delete;
14081426
ClientConnection &operator=(const ClientConnection &) = delete;
@@ -1485,6 +1503,21 @@ class ClientImpl {
14851503
Stream *stream_ = nullptr; // Stream for reading
14861504
detail::BodyReader body_reader_; // Body reading state
14871505

1506+
// Default constructor
1507+
StreamHandle() = default;
1508+
1509+
// Move-only semantics (non-copyable due to unique_ptr members)
1510+
StreamHandle(const StreamHandle &) = delete;
1511+
StreamHandle &operator=(const StreamHandle &) = delete;
1512+
StreamHandle(StreamHandle &&) = default;
1513+
StreamHandle &operator=(StreamHandle &&) = default;
1514+
1515+
// Destructor: Cleans up socket connection.
1516+
// In socket direct mode, if the body was not fully read, remaining data
1517+
// is discarded and the connection is closed (cannot be reused).
1518+
// This is safe but may leave unread data in the socket buffer.
1519+
~StreamHandle() = default;
1520+
14881521
bool is_valid() const {
14891522
return response != nullptr && error == Error::Success;
14901523
}
@@ -9235,6 +9268,13 @@ ClientImpl::open_stream_direct(const std::string &path,
92359268
auto is_alive = false;
92369269
if (socket_.is_open()) {
92379270
is_alive = detail::is_socket_alive(socket_.sock);
9271+
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
9272+
if (is_alive && is_ssl()) {
9273+
if (detail::is_ssl_peer_could_be_closed(socket_.ssl, socket_.sock)) {
9274+
is_alive = false;
9275+
}
9276+
}
9277+
#endif
92389278
if (!is_alive) {
92399279
shutdown_ssl(socket_, false);
92409280
shutdown_socket(socket_);
@@ -9247,6 +9287,17 @@ ClientImpl::open_stream_direct(const std::string &path,
92479287
handle.response.reset();
92489288
return handle;
92499289
}
9290+
9291+
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
9292+
// Initialize SSL for SSL clients
9293+
if (is_ssl()) {
9294+
auto &scli = static_cast<SSLClient &>(*this);
9295+
if (!scli.initialize_ssl(socket_, handle.error)) {
9296+
handle.response.reset();
9297+
return handle;
9298+
}
9299+
}
9300+
#endif
92509301
}
92519302

92529303
// Transfer socket ownership to StreamHandle
@@ -9258,10 +9309,22 @@ ClientImpl::open_stream_direct(const std::string &path,
92589309
socket_.sock = INVALID_SOCKET;
92599310
}
92609311

9261-
// Create SocketStream for the transferred socket
9312+
// Create appropriate stream for the transferred socket
9313+
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
9314+
if (is_ssl() && handle.connection_->ssl) {
9315+
handle.socket_stream_ = detail::make_unique<detail::SSLSocketStream>(
9316+
handle.connection_->sock, handle.connection_->ssl, read_timeout_sec_,
9317+
read_timeout_usec_, write_timeout_sec_, write_timeout_usec_);
9318+
} else {
9319+
handle.socket_stream_ = detail::make_unique<detail::SocketStream>(
9320+
handle.connection_->sock, read_timeout_sec_, read_timeout_usec_,
9321+
write_timeout_sec_, write_timeout_usec_);
9322+
}
9323+
#else
92629324
handle.socket_stream_ = detail::make_unique<detail::SocketStream>(
92639325
handle.connection_->sock, read_timeout_sec_, read_timeout_usec_,
92649326
write_timeout_sec_, write_timeout_usec_);
9327+
#endif
92659328
handle.stream_ = handle.socket_stream_.get();
92669329

92679330
// Build and send request

test/test20.cc

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -530,6 +530,9 @@ TEST(ClientConnectionTest, IsOpenReturnsTrueWhenSocketValid) {
530530
conn.sock = 1; // Fake valid socket
531531

532532
EXPECT_TRUE(conn.is_open());
533+
534+
// Reset to avoid closing invalid socket in destructor
535+
conn.sock = INVALID_SOCKET;
533536
}
534537

535538
TEST(ClientConnectionTest, MoveConstructor) {
@@ -540,6 +543,9 @@ TEST(ClientConnectionTest, MoveConstructor) {
540543

541544
EXPECT_EQ(42, conn2.sock);
542545
EXPECT_EQ(INVALID_SOCKET, conn1.sock); // Moved-from state
546+
547+
// Reset to avoid closing invalid socket in destructor
548+
conn2.sock = INVALID_SOCKET;
543549
}
544550

545551
TEST(ClientConnectionTest, MoveAssignment) {
@@ -551,6 +557,9 @@ TEST(ClientConnectionTest, MoveAssignment) {
551557

552558
EXPECT_EQ(42, conn2.sock);
553559
EXPECT_EQ(INVALID_SOCKET, conn1.sock); // Moved-from state
560+
561+
// Reset to avoid closing invalid socket in destructor
562+
conn2.sock = INVALID_SOCKET;
554563
}
555564

556565
//------------------------------------------------------------------------------
@@ -926,3 +935,70 @@ TEST_F(OpenStreamDirectTest, ChunkedResponseInPieces) {
926935

927936
EXPECT_EQ("chunkchunkchunk", result);
928937
}
938+
939+
// =============================================================================
940+
// Phase 2.7: SSL Support Tests
941+
// =============================================================================
942+
943+
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
944+
class SSLOpenStreamDirectTest : public ::testing::Test {
945+
protected:
946+
SSLOpenStreamDirectTest() : svr_("cert.pem", "key.pem") {}
947+
948+
void SetUp() override {
949+
svr_.Get("/hello", [](const httplib::Request &, httplib::Response &res) {
950+
res.set_content("Hello SSL World!", "text/plain");
951+
});
952+
953+
svr_.Get("/chunked", [](const httplib::Request &, httplib::Response &res) {
954+
res.set_chunked_content_provider(
955+
"text/plain", [](size_t offset, httplib::DataSink &sink) {
956+
if (offset < 15) {
957+
sink.write("chunk", 5);
958+
return true;
959+
}
960+
sink.done();
961+
return true;
962+
});
963+
});
964+
965+
thread_ = std::thread([this]() { svr_.listen("127.0.0.1", 8788); });
966+
svr_.wait_until_ready();
967+
}
968+
969+
void TearDown() override {
970+
svr_.stop();
971+
if (thread_.joinable()) { thread_.join(); }
972+
}
973+
974+
httplib::SSLServer svr_;
975+
std::thread thread_;
976+
};
977+
978+
TEST_F(SSLOpenStreamDirectTest, BasicSSLStream) {
979+
httplib::SSLClient cli("127.0.0.1", 8788);
980+
cli.enable_server_certificate_verification(false);
981+
982+
auto handle = cli.open_stream_direct("/hello");
983+
984+
ASSERT_TRUE(handle.is_valid());
985+
EXPECT_EQ(200, handle.response->status);
986+
EXPECT_TRUE(handle.is_socket_direct_mode());
987+
988+
auto body = handle.read_all();
989+
EXPECT_EQ("Hello SSL World!", body);
990+
}
991+
992+
TEST_F(SSLOpenStreamDirectTest, SSLChunkedResponse) {
993+
httplib::SSLClient cli("127.0.0.1", 8788);
994+
cli.enable_server_certificate_verification(false);
995+
996+
auto handle = cli.open_stream_direct("/chunked");
997+
998+
ASSERT_TRUE(handle.is_valid());
999+
EXPECT_TRUE(handle.body_reader_.chunked);
1000+
1001+
auto body = handle.read_all();
1002+
EXPECT_EQ("chunkchunkchunk", body);
1003+
}
1004+
#endif

0 commit comments

Comments
 (0)