rippled
Loading...
Searching...
No Matches
tests/libxrpl/net/HTTPClient.cpp
1#include <xrpl/basics/Log.h>
2#include <xrpl/net/HTTPClient.h>
3
4#include <boost/algorithm/string/predicate.hpp>
5#include <boost/asio/co_spawn.hpp>
6#include <boost/asio/detached.hpp>
7#include <boost/asio/ip/tcp.hpp>
8#include <boost/asio/use_awaitable.hpp>
9#include <boost/asio/use_future.hpp>
10#include <boost/beast/core.hpp>
11#include <boost/beast/http.hpp>
12#include <boost/beast/version.hpp>
13
14#include <gtest/gtest.h>
15#include <helpers/TestSink.h>
16
17#include <atomic>
18#include <map>
19#include <memory>
20#include <semaphore>
21#include <thread>
22
23using namespace xrpl;
24
25namespace {
26
27// Simple HTTP server using Beast for testing
28class TestHTTPServer
29{
30private:
31 boost::asio::io_context ioc_;
32 boost::asio::ip::tcp::acceptor acceptor_;
33 boost::asio::ip::tcp::endpoint endpoint_;
34 bool running_{true};
35 bool finished_{false};
36 unsigned short port_{0};
37
38 // Custom headers to return
40 std::string responseBody_;
41 unsigned int statusCode_{200};
42
44
45public:
46 TestHTTPServer() : acceptor_(ioc_), j_(TestSink::instance())
47 {
48 // Bind to any available port
49 endpoint_ = {boost::asio::ip::tcp::v4(), 0};
50 acceptor_.open(endpoint_.protocol());
51 acceptor_.set_option(boost::asio::socket_base::reuse_address(true));
52 acceptor_.bind(endpoint_);
53 acceptor_.listen();
54
55 // Get the actual port that was assigned
56 port_ = acceptor_.local_endpoint().port();
57
58 // Start the accept coroutine
59 boost::asio::co_spawn(ioc_, accept(), boost::asio::detached);
60 }
61
62 TestHTTPServer(TestHTTPServer&&) = delete;
63 TestHTTPServer&
64 operator=(TestHTTPServer&&) = delete;
65
66 ~TestHTTPServer()
67 {
68 XRPL_ASSERT(finished(), "xrpl::TestHTTPServer::~TestHTTPServer : accept future ready");
69 }
70
71 boost::asio::io_context&
72 ioc()
73 {
74 return ioc_;
75 }
76
77 unsigned short
78 port() const
79 {
80 return port_;
81 }
82
83 void
84 setHeader(std::string const& name, std::string const& value)
85 {
86 customHeaders_[name] = value;
87 }
88
89 void
90 setResponseBody(std::string const& body)
91 {
92 responseBody_ = body;
93 }
94
95 void
96 setStatusCode(unsigned int code)
97 {
98 statusCode_ = code;
99 }
100
101 void
102 stop()
103 {
104 running_ = false;
105 acceptor_.close();
106 }
107
108 bool
109 finished() const
110 {
111 return finished_;
112 }
113
114private:
115 boost::asio::awaitable<void>
116 accept()
117 {
118 while (running_)
119 {
120 try
121 {
122 auto socket = co_await acceptor_.async_accept(boost::asio::use_awaitable);
123
124 if (!running_)
125 break;
126
127 // Handle this connection
128 co_await handleConnection(std::move(socket));
129 }
130 catch (std::exception const& e)
131 {
132 // Accept or handle failed, stop accepting
133 JLOG(j_.debug()) << "Error: " << e.what();
134 break;
135 }
136 }
137
138 finished_ = true;
139 }
140
141 boost::asio::awaitable<void>
142 handleConnection(boost::asio::ip::tcp::socket socket)
143 {
144 try
145 {
146 boost::beast::flat_buffer buffer;
147 boost::beast::http::request<boost::beast::http::string_body> req;
148
149 // Read the HTTP request asynchronously
150 co_await boost::beast::http::async_read(
151 socket, buffer, req, boost::asio::use_awaitable);
152
153 // Create response
154 boost::beast::http::response<boost::beast::http::string_body> res;
155 res.version(req.version());
156 res.result(statusCode_);
157 res.set(boost::beast::http::field::server, "TestServer");
158
159 // Set body and prepare payload first
160 res.body() = responseBody_;
161 res.prepare_payload();
162
163 // Override Content-Length with custom headers after
164 // prepare_payload. This allows us to test case-insensitive
165 // header parsing.
166 for (auto const& [name, value] : customHeaders_)
167 {
168 res.set(name, value);
169 }
170
171 // Send response asynchronously
172 co_await boost::beast::http::async_write(socket, res, boost::asio::use_awaitable);
173
174 // Shutdown socket gracefully
175 boost::system::error_code shutdownEc;
176
177 // NOLINTNEXTLINE(bugprone-unused-return-value)
178 socket.shutdown(boost::asio::ip::tcp::socket::shutdown_send, shutdownEc);
179 }
180 catch (std::exception const& e)
181 {
182 // Error reading or writing, just close the connection
183 JLOG(j_.debug()) << "Connection error: " << e.what();
184 }
185 }
186};
187
188} // anonymous namespace
189
190// Test fixture that manages the SSL context lifecycle via RAII.
191// SetUp() initializes the context before each test and TearDown()
192// cleans it up afterwards, so individual tests don't need to worry
193// about resource management.
194class HTTPClientTest : public ::testing::Test
195{
196protected:
197 // Shared journal for SSL context initialization and HTTP requests.
199
200 // Initialize the global SSL context used by HTTPClient.
201 void
202 SetUp() override
203 {
205 "" /* sslVerifyDir*/, "" /*sslVerifyFile */, false /* sslVerify */, j_ /* journal */);
206 }
207
208 // Release the global SSL context to prevent memory leaks.
209 void
210 TearDown() override
211 {
213 }
214
215 // Issue an HTTP GET to the given test server and drive the
216 // io_context until a response arrives or a timeout is reached.
217 // Returns true when the completion handler was invoked.
218 bool
220 TestHTTPServer& server,
221 std::string const& path,
222 bool& completed,
223 int& resultStatus,
224 std::string& resultData,
225 boost::system::error_code& resultError)
226 {
228 false, // no SSL
229 server.ioc(),
230 "127.0.0.1",
231 server.port(),
232 path,
233 1024, // max response size
235 [&](boost::system::error_code const& ec, int status, std::string const& data) -> bool {
236 resultError = ec;
237 resultStatus = status;
238 resultData = data;
239 completed = true;
240 return false; // don't retry
241 },
242 j_);
243
244 // Run the IO context until completion
245 auto start = std::chrono::steady_clock::now();
246 while (server.ioc().run_one() != 0)
247 {
249 server.finished())
250 {
251 break;
252 }
253
254 if (completed)
255 {
256 server.stop();
257 }
258 }
259
260 // Drain any remaining handlers to ensure proper cleanup of HTTPClientImp
261 server.ioc().poll();
262
263 return completed;
264 }
265};
266
267TEST_F(HTTPClientTest, case_insensitive_content_length)
268{
269 // Test different cases of Content-Length header
270 std::vector<std::string> const headerCases = {
271 "Content-Length", // Standard case
272 "content-length", // Lowercase - this tests the regex icase fix
273 "CONTENT-LENGTH", // Uppercase
274 "Content-length", // Mixed case
275 "content-Length" // Mixed case 2
276 };
277
278 for (auto const& headerName : headerCases)
279 {
280 TestHTTPServer server;
281 std::string const testBody = "Hello World!";
282 server.setResponseBody(testBody);
283 server.setHeader(headerName, std::to_string(testBody.size()));
284
285 bool completed{false};
286 int resultStatus{0};
287 std::string resultData;
288 boost::system::error_code resultError;
289
290 bool const testCompleted =
291 runHTTPTest(server, "/test", completed, resultStatus, resultData, resultError);
292 // Verify results
293 EXPECT_TRUE(testCompleted);
294 EXPECT_FALSE(resultError);
295 EXPECT_EQ(resultStatus, 200);
296 EXPECT_EQ(resultData, testBody);
297 }
298}
299
300TEST_F(HTTPClientTest, basic_http_request)
301{
302 TestHTTPServer server;
303 std::string const testBody = "Test response body";
304 server.setResponseBody(testBody);
305 server.setHeader("Content-Type", "text/plain");
306
307 bool completed{false};
308 int resultStatus{0};
309 std::string resultData;
310 boost::system::error_code resultError;
311
312 bool const testCompleted =
313 runHTTPTest(server, "/basic", completed, resultStatus, resultData, resultError);
314
315 EXPECT_TRUE(testCompleted);
316 EXPECT_FALSE(resultError);
317 EXPECT_EQ(resultStatus, 200);
318 EXPECT_EQ(resultData, testBody);
319}
320
321TEST_F(HTTPClientTest, empty_response)
322{
323 TestHTTPServer server;
324 server.setResponseBody(""); // Empty body
325 server.setHeader("Content-Length", "0");
326
327 bool completed{false};
328 int resultStatus{0};
329 std::string resultData;
330 boost::system::error_code resultError;
331
332 bool const testCompleted =
333 runHTTPTest(server, "/empty", completed, resultStatus, resultData, resultError);
334
335 EXPECT_TRUE(testCompleted);
336 EXPECT_FALSE(resultError);
337 EXPECT_EQ(resultStatus, 200);
338 EXPECT_TRUE(resultData.empty());
339}
340
341TEST_F(HTTPClientTest, different_status_codes)
342{
343 std::vector<unsigned int> const statusCodes = {200, 404, 500};
344
345 for (auto status : statusCodes)
346 {
347 TestHTTPServer server;
348 server.setStatusCode(status);
349 server.setResponseBody("Status " + std::to_string(status));
350
351 bool completed{false};
352 int resultStatus{0};
353 std::string resultData;
354 boost::system::error_code resultError;
355
356 bool const testCompleted =
357 runHTTPTest(server, "/status", completed, resultStatus, resultData, resultError);
358
359 EXPECT_TRUE(testCompleted);
360 EXPECT_FALSE(resultError);
361 EXPECT_EQ(resultStatus, static_cast<int>(status));
362 }
363}
bool runHTTPTest(TestHTTPServer &server, std::string const &path, bool &completed, int &resultStatus, std::string &resultData, boost::system::error_code &resultError)
A generic endpoint for log messages.
Definition Journal.h:40
Stream debug() const
Definition Journal.h:301
static void initializeSSLContext(std::string const &sslVerifyDir, std::string const &sslVerifyFile, bool sslVerify, beast::Journal j)
static void get(bool bSSL, boost::asio::io_context &io_context, std::deque< std::string > deqSites, unsigned short const port, std::string const &strPath, std::size_t responseMax, std::chrono::seconds timeout, std::function< bool(boost::system::error_code const &ecResult, int iStatus, std::string const &strData)> complete, beast::Journal &j)
static void cleanupSSLContext()
Destroys the global SSL context created by initializeSSLContext().
static TestSink & instance()
Definition TestSink.h:10
T empty(T... args)
Json::Value accept(jtx::Account const &subject, jtx::Account const &issuer, std::string_view credType)
Definition creds.cpp:26
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:5
T size(T... args)
T to_string(T... args)
T what(T... args)