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/ip/tcp.hpp>
6#include <boost/beast/core.hpp>
7#include <boost/beast/http.hpp>
8#include <boost/beast/version.hpp>
9
10#include <doctest/doctest.h>
11
12#include <atomic>
13#include <map>
14#include <thread>
15
16using namespace ripple;
17
18namespace {
19
20// Simple HTTP server using Beast for testing
21class TestHTTPServer
22{
23private:
24 boost::asio::io_context ioc_;
25 boost::asio::ip::tcp::acceptor acceptor_;
26 boost::asio::ip::tcp::endpoint endpoint_;
27 std::atomic<bool> running_{true};
28 unsigned short port_;
29
30 // Custom headers to return
32 std::string response_body_;
33 unsigned int status_code_{200};
34
35public:
36 TestHTTPServer() : acceptor_(ioc_), port_(0)
37 {
38 // Bind to any available port
39 endpoint_ = {boost::asio::ip::tcp::v4(), 0};
40 acceptor_.open(endpoint_.protocol());
41 acceptor_.set_option(boost::asio::socket_base::reuse_address(true));
42 acceptor_.bind(endpoint_);
43 acceptor_.listen();
44
45 // Get the actual port that was assigned
46 port_ = acceptor_.local_endpoint().port();
47
48 accept();
49 }
50
51 ~TestHTTPServer()
52 {
53 stop();
54 }
55
56 boost::asio::io_context&
57 ioc()
58 {
59 return ioc_;
60 }
61
62 unsigned short
63 port() const
64 {
65 return port_;
66 }
67
68 void
69 setHeader(std::string const& name, std::string const& value)
70 {
71 custom_headers_[name] = value;
72 }
73
74 void
75 setResponseBody(std::string const& body)
76 {
77 response_body_ = body;
78 }
79
80 void
81 setStatusCode(unsigned int code)
82 {
83 status_code_ = code;
84 }
85
86private:
87 void
88 stop()
89 {
90 running_ = false;
91 acceptor_.close();
92 }
93
94 void
95 accept()
96 {
97 if (!running_)
98 return;
99
100 acceptor_.async_accept(
101 ioc_,
102 endpoint_,
103 [&](boost::system::error_code const& error,
104 boost::asio::ip::tcp::socket peer) {
105 if (!running_)
106 return;
107
108 if (!error)
109 {
110 handleConnection(std::move(peer));
111 }
112 });
113 }
114
115 void
116 handleConnection(boost::asio::ip::tcp::socket socket)
117 {
118 try
119 {
120 // Read the HTTP request
121 boost::beast::flat_buffer buffer;
122 boost::beast::http::request<boost::beast::http::string_body> req;
123 boost::beast::http::read(socket, buffer, req);
124
125 // Create response
126 boost::beast::http::response<boost::beast::http::string_body> res;
127 res.version(req.version());
128 res.result(status_code_);
129 res.set(boost::beast::http::field::server, "TestServer");
130
131 // Add custom headers
132 for (auto const& [name, value] : custom_headers_)
133 {
134 res.set(name, value);
135 }
136
137 // Set body and prepare payload first
138 res.body() = response_body_;
139 res.prepare_payload();
140
141 // Override Content-Length with custom headers after prepare_payload
142 // This allows us to test case-insensitive header parsing
143 for (auto const& [name, value] : custom_headers_)
144 {
145 if (boost::iequals(name, "Content-Length"))
146 {
147 res.erase(boost::beast::http::field::content_length);
148 res.set(name, value);
149 }
150 }
151
152 // Send response
153 boost::beast::http::write(socket, res);
154
155 // Shutdown socket gracefully
156 boost::system::error_code ec;
157 socket.shutdown(boost::asio::ip::tcp::socket::shutdown_send, ec);
158 }
159 catch (std::exception const&)
160 {
161 // Connection handling errors are expected
162 }
163
164 if (running_)
165 accept();
166 }
167};
168
169// Helper function to run HTTP client test
170bool
171runHTTPTest(
172 TestHTTPServer& server,
173 std::string const& path,
174 std::atomic<bool>& completed,
175 std::atomic<int>& result_status,
176 std::string& result_data,
177 boost::system::error_code& result_error)
178{
179 // Create a null journal for testing
181
182 // Initialize HTTPClient SSL context
183 HTTPClient::initializeSSLContext("", "", false, j);
184
186 false, // no SSL
187 server.ioc(),
188 "127.0.0.1",
189 server.port(),
190 path,
191 1024, // max response size
193 [&](boost::system::error_code const& ec,
194 int status,
195 std::string const& data) -> bool {
196 result_error = ec;
197 result_status = status;
198 result_data = data;
199 completed = true;
200 return false; // don't retry
201 },
202 j);
203
204 // Run the IO context until completion
205 auto start = std::chrono::steady_clock::now();
206 while (!completed &&
208 {
209 if (server.ioc().run_one() == 0)
210 {
211 break;
212 }
213 }
214
215 return completed;
216}
217
218} // anonymous namespace
219
220TEST_CASE("HTTPClient case insensitive Content-Length")
221{
222 // Test different cases of Content-Length header
223 std::vector<std::string> header_cases = {
224 "Content-Length", // Standard case
225 "content-length", // Lowercase - this tests the regex icase fix
226 "CONTENT-LENGTH", // Uppercase
227 "Content-length", // Mixed case
228 "content-Length" // Mixed case 2
229 };
230
231 for (auto const& header_name : header_cases)
232 {
233 TestHTTPServer server;
234 std::string test_body = "Hello World!";
235 server.setResponseBody(test_body);
236 server.setHeader(header_name, std::to_string(test_body.size()));
237
238 std::atomic<bool> completed{false};
239 std::atomic<int> result_status{0};
240 std::string result_data;
241 boost::system::error_code result_error;
242
243 bool test_completed = runHTTPTest(
244 server,
245 "/test",
246 completed,
247 result_status,
248 result_data,
249 result_error);
250
251 // Verify results
252 CHECK(test_completed);
253 CHECK(!result_error);
254 CHECK(result_status == 200);
255 CHECK(result_data == test_body);
256 }
257}
258
259TEST_CASE("HTTPClient basic HTTP request")
260{
261 TestHTTPServer server;
262 std::string test_body = "Test response body";
263 server.setResponseBody(test_body);
264 server.setHeader("Content-Type", "text/plain");
265
266 std::atomic<bool> completed{false};
267 std::atomic<int> result_status{0};
268 std::string result_data;
269 boost::system::error_code result_error;
270
271 bool test_completed = runHTTPTest(
272 server, "/basic", completed, result_status, result_data, result_error);
273
274 CHECK(test_completed);
275 CHECK(!result_error);
276 CHECK(result_status == 200);
277 CHECK(result_data == test_body);
278}
279
280TEST_CASE("HTTPClient empty response")
281{
282 TestHTTPServer server;
283 server.setResponseBody(""); // Empty body
284 server.setHeader("Content-Length", "0");
285
286 std::atomic<bool> completed{false};
287 std::atomic<int> result_status{0};
288 std::string result_data;
289 boost::system::error_code result_error;
290
291 bool test_completed = runHTTPTest(
292 server, "/empty", completed, result_status, result_data, result_error);
293
294 CHECK(test_completed);
295 CHECK(!result_error);
296 CHECK(result_status == 200);
297 CHECK(result_data.empty());
298}
299
300TEST_CASE("HTTPClient different status codes")
301{
302 std::vector<unsigned int> status_codes = {200, 404, 500};
303
304 for (auto status : status_codes)
305 {
306 TestHTTPServer server;
307 server.setStatusCode(status);
308 server.setResponseBody("Status " + std::to_string(status));
309
310 std::atomic<bool> completed{false};
311 std::atomic<int> result_status{0};
312 std::string result_data;
313 boost::system::error_code result_error;
314
315 bool test_completed = runHTTPTest(
316 server,
317 "/status",
318 completed,
319 result_status,
320 result_data,
321 result_error);
322
323 CHECK(test_completed);
324 CHECK(!result_error);
325 CHECK(result_status == static_cast<int>(status));
326 }
327}
A generic endpoint for log messages.
Definition Journal.h:41
static Sink & getNullSink()
Returns a Sink which does nothing.
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)
T empty(T... args)
T erase(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:6
TEST_CASE("construct and compare Json::StaticString")
Definition Value.cpp:18
T size(T... args)
T to_string(T... args)