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_;
37
38 // Custom headers to return
40 std::string responseBody_;
41 unsigned int statusCode_{200};
42
44
45public:
46 TestHTTPServer() : acceptor_(ioc_), port_(0), 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(socket, buffer, req, boost::asio::use_awaitable);
151
152 // Create response
153 boost::beast::http::response<boost::beast::http::string_body> res;
154 res.version(req.version());
155 res.result(statusCode_);
156 res.set(boost::beast::http::field::server, "TestServer");
157
158 // Set body and prepare payload first
159 res.body() = responseBody_;
160 res.prepare_payload();
161
162 // Override Content-Length with custom headers after
163 // prepare_payload. This allows us to test case-insensitive
164 // header parsing.
165 for (auto const& [name, value] : customHeaders_)
166 {
167 res.set(name, value);
168 }
169
170 // Send response asynchronously
171 co_await boost::beast::http::async_write(socket, res, boost::asio::use_awaitable);
172
173 // Shutdown socket gracefully
174 boost::system::error_code shutdownEc;
175 socket.shutdown(boost::asio::ip::tcp::socket::shutdown_send, shutdownEc);
176 }
177 catch (std::exception const& e)
178 {
179 // Error reading or writing, just close the connection
180 JLOG(j_.debug()) << "Connection error: " << e.what();
181 }
182 }
183};
184
185// Helper function to run HTTP client test
186bool
187runHTTPTest(
188 TestHTTPServer& server,
189 std::string const& path,
190 bool& completed,
191 int& resultStatus,
192 std::string& resultData,
193 boost::system::error_code& resultError)
194{
195 // Create a null journal for testing
197
198 // Initialize HTTPClient SSL context
199 HTTPClient::initializeSSLContext("", "", false, j);
200
202 false, // no SSL
203 server.ioc(),
204 "127.0.0.1",
205 server.port(),
206 path,
207 1024, // max response size
209 [&](boost::system::error_code const& ec, int status, std::string const& data) -> bool {
210 resultError = ec;
211 resultStatus = status;
212 resultData = data;
213 completed = true;
214 return false; // don't retry
215 },
216 j);
217
218 // Run the IO context until completion
219 auto start = std::chrono::steady_clock::now();
220 while (server.ioc().run_one() != 0)
221 {
222 if (std::chrono::steady_clock::now() - start >= std::chrono::seconds(10) || server.finished())
223 {
224 break;
225 }
226
227 if (completed)
228 {
229 server.stop();
230 }
231 }
232
233 return completed;
234}
235
236} // anonymous namespace
237
238TEST(HTTPClient, case_insensitive_content_length)
239{
240 // Test different cases of Content-Length header
241 std::vector<std::string> headerCases = {
242 "Content-Length", // Standard case
243 "content-length", // Lowercase - this tests the regex icase fix
244 "CONTENT-LENGTH", // Uppercase
245 "Content-length", // Mixed case
246 "content-Length" // Mixed case 2
247 };
248
249 for (auto const& headerName : headerCases)
250 {
251 TestHTTPServer server;
252 std::string testBody = "Hello World!";
253 server.setResponseBody(testBody);
254 server.setHeader(headerName, std::to_string(testBody.size()));
255
256 bool completed{false};
257 int resultStatus{0};
258 std::string resultData;
259 boost::system::error_code resultError;
260
261 bool testCompleted = runHTTPTest(server, "/test", completed, resultStatus, resultData, resultError);
262
263 // Verify results
264 EXPECT_TRUE(testCompleted);
265 EXPECT_FALSE(resultError);
266 EXPECT_EQ(resultStatus, 200);
267 EXPECT_EQ(resultData, testBody);
268 }
269}
270
271TEST(HTTPClient, basic_http_request)
272{
273 TestHTTPServer server;
274 std::string testBody = "Test response body";
275 server.setResponseBody(testBody);
276 server.setHeader("Content-Type", "text/plain");
277
278 bool completed{false};
279 int resultStatus{0};
280 std::string resultData;
281 boost::system::error_code resultError;
282
283 bool testCompleted = runHTTPTest(server, "/basic", completed, resultStatus, resultData, resultError);
284
285 EXPECT_TRUE(testCompleted);
286 EXPECT_FALSE(resultError);
287 EXPECT_EQ(resultStatus, 200);
288 EXPECT_EQ(resultData, testBody);
289}
290
291TEST(HTTPClient, empty_response)
292{
293 TestHTTPServer server;
294 server.setResponseBody(""); // Empty body
295 server.setHeader("Content-Length", "0");
296
297 bool completed{false};
298 int resultStatus{0};
299 std::string resultData;
300 boost::system::error_code resultError;
301
302 bool testCompleted = runHTTPTest(server, "/empty", completed, resultStatus, resultData, resultError);
303
304 EXPECT_TRUE(testCompleted);
305 EXPECT_FALSE(resultError);
306 EXPECT_EQ(resultStatus, 200);
307 EXPECT_TRUE(resultData.empty());
308}
309
310TEST(HTTPClient, different_status_codes)
311{
312 std::vector<unsigned int> statusCodes = {200, 404, 500};
313
314 for (auto status : statusCodes)
315 {
316 TestHTTPServer server;
317 server.setStatusCode(status);
318 server.setResponseBody("Status " + std::to_string(status));
319
320 bool completed{false};
321 int resultStatus{0};
322 std::string resultData;
323 boost::system::error_code resultError;
324
325 bool testCompleted = runHTTPTest(server, "/status", completed, resultStatus, resultData, resultError);
326
327 EXPECT_TRUE(testCompleted);
328 EXPECT_FALSE(resultError);
329 EXPECT_EQ(resultStatus, static_cast<int>(status));
330 }
331}
A generic endpoint for log messages.
Definition Journal.h:41
Stream debug() const
Definition Journal.h:301
Provides an asynchronous HTTP client implementation with optional SSL.
Definition HTTPClient.h:20
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 TestSink & instance()
Definition TestSink.h:11
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:6
TEST(json_value, limits)
Definition Value.cpp:17
T size(T... args)
T to_string(T... args)
T what(T... args)