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