rippled
Loading...
Searching...
No Matches
tests/libxrpl/net/HTTPClient.cpp
1//------------------------------------------------------------------------------
2/*
3 This file is part of rippled: https://github.com/ripple/rippled
4 Copyright (c) 2024 Ripple Labs Inc.
5
6 Permission to use, copy, modify, and/or distribute this software for any
7 purpose with or without fee is hereby granted, provided that the above
8 copyright notice and this permission notice appear in all copies.
9
10 THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11 WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12 MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13 ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14 WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15 ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16 OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17*/
18//==============================================================================
19
20#include <xrpl/basics/Log.h>
21#include <xrpl/net/HTTPClient.h>
22
23#include <boost/algorithm/string/predicate.hpp>
24#include <boost/asio/ip/tcp.hpp>
25#include <boost/beast/core.hpp>
26#include <boost/beast/http.hpp>
27#include <boost/beast/version.hpp>
28
29#include <doctest/doctest.h>
30
31#include <atomic>
32#include <map>
33#include <thread>
34
35using namespace ripple;
36
37namespace {
38
39// Simple HTTP server using Beast for testing
40class TestHTTPServer
41{
42private:
43 boost::asio::io_context ioc_;
44 boost::asio::ip::tcp::acceptor acceptor_;
45 boost::asio::ip::tcp::endpoint endpoint_;
46 std::atomic<bool> running_{true};
47 unsigned short port_;
48
49 // Custom headers to return
51 std::string response_body_;
52 unsigned int status_code_{200};
53
54public:
55 TestHTTPServer() : acceptor_(ioc_), port_(0)
56 {
57 // Bind to any available port
58 endpoint_ = {boost::asio::ip::tcp::v4(), 0};
59 acceptor_.open(endpoint_.protocol());
60 acceptor_.set_option(boost::asio::socket_base::reuse_address(true));
61 acceptor_.bind(endpoint_);
62 acceptor_.listen();
63
64 // Get the actual port that was assigned
65 port_ = acceptor_.local_endpoint().port();
66
67 accept();
68 }
69
70 ~TestHTTPServer()
71 {
72 stop();
73 }
74
75 boost::asio::io_context&
76 ioc()
77 {
78 return ioc_;
79 }
80
81 unsigned short
82 port() const
83 {
84 return port_;
85 }
86
87 void
88 setHeader(std::string const& name, std::string const& value)
89 {
90 custom_headers_[name] = value;
91 }
92
93 void
94 setResponseBody(std::string const& body)
95 {
96 response_body_ = body;
97 }
98
99 void
100 setStatusCode(unsigned int code)
101 {
102 status_code_ = code;
103 }
104
105private:
106 void
107 stop()
108 {
109 running_ = false;
110 acceptor_.close();
111 }
112
113 void
114 accept()
115 {
116 if (!running_)
117 return;
118
119 acceptor_.async_accept(
120 ioc_,
121 endpoint_,
122 [&](boost::system::error_code const& error,
123 boost::asio::ip::tcp::socket peer) {
124 if (!running_)
125 return;
126
127 if (!error)
128 {
129 handleConnection(std::move(peer));
130 }
131 });
132 }
133
134 void
135 handleConnection(boost::asio::ip::tcp::socket socket)
136 {
137 try
138 {
139 // Read the HTTP request
140 boost::beast::flat_buffer buffer;
141 boost::beast::http::request<boost::beast::http::string_body> req;
142 boost::beast::http::read(socket, buffer, req);
143
144 // Create response
145 boost::beast::http::response<boost::beast::http::string_body> res;
146 res.version(req.version());
147 res.result(status_code_);
148 res.set(boost::beast::http::field::server, "TestServer");
149
150 // Add custom headers
151 for (auto const& [name, value] : custom_headers_)
152 {
153 res.set(name, value);
154 }
155
156 // Set body and prepare payload first
157 res.body() = response_body_;
158 res.prepare_payload();
159
160 // Override Content-Length with custom headers after prepare_payload
161 // This allows us to test case-insensitive header parsing
162 for (auto const& [name, value] : custom_headers_)
163 {
164 if (boost::iequals(name, "Content-Length"))
165 {
166 res.erase(boost::beast::http::field::content_length);
167 res.set(name, value);
168 }
169 }
170
171 // Send response
172 boost::beast::http::write(socket, res);
173
174 // Shutdown socket gracefully
175 boost::system::error_code ec;
176 socket.shutdown(boost::asio::ip::tcp::socket::shutdown_send, ec);
177 }
178 catch (std::exception const&)
179 {
180 // Connection handling errors are expected
181 }
182
183 if (running_)
184 accept();
185 }
186};
187
188// Helper function to run HTTP client test
189bool
190runHTTPTest(
191 TestHTTPServer& server,
192 std::string const& path,
193 std::atomic<bool>& completed,
194 std::atomic<int>& result_status,
195 std::string& result_data,
196 boost::system::error_code& result_error)
197{
198 // Create a null journal for testing
200
201 // Initialize HTTPClient SSL context
202 HTTPClient::initializeSSLContext("", "", false, j);
203
205 false, // no SSL
206 server.ioc(),
207 "127.0.0.1",
208 server.port(),
209 path,
210 1024, // max response size
212 [&](boost::system::error_code const& ec,
213 int status,
214 std::string const& data) -> bool {
215 result_error = ec;
216 result_status = status;
217 result_data = data;
218 completed = true;
219 return false; // don't retry
220 },
221 j);
222
223 // Run the IO context until completion
224 auto start = std::chrono::steady_clock::now();
225 while (!completed &&
227 {
228 if (server.ioc().run_one() == 0)
229 {
230 break;
231 }
232 }
233
234 return completed;
235}
236
237} // anonymous namespace
238
239TEST_CASE("HTTPClient case insensitive Content-Length")
240{
241 // Test different cases of Content-Length header
242 std::vector<std::string> header_cases = {
243 "Content-Length", // Standard case
244 "content-length", // Lowercase - this tests the regex icase fix
245 "CONTENT-LENGTH", // Uppercase
246 "Content-length", // Mixed case
247 "content-Length" // Mixed case 2
248 };
249
250 for (auto const& header_name : header_cases)
251 {
252 TestHTTPServer server;
253 std::string test_body = "Hello World!";
254 server.setResponseBody(test_body);
255 server.setHeader(header_name, std::to_string(test_body.size()));
256
257 std::atomic<bool> completed{false};
258 std::atomic<int> result_status{0};
259 std::string result_data;
260 boost::system::error_code result_error;
261
262 bool test_completed = runHTTPTest(
263 server,
264 "/test",
265 completed,
266 result_status,
267 result_data,
268 result_error);
269
270 // Verify results
271 CHECK(test_completed);
272 CHECK(!result_error);
273 CHECK(result_status == 200);
274 CHECK(result_data == test_body);
275 }
276}
277
278TEST_CASE("HTTPClient basic HTTP request")
279{
280 TestHTTPServer server;
281 std::string test_body = "Test response body";
282 server.setResponseBody(test_body);
283 server.setHeader("Content-Type", "text/plain");
284
285 std::atomic<bool> completed{false};
286 std::atomic<int> result_status{0};
287 std::string result_data;
288 boost::system::error_code result_error;
289
290 bool test_completed = runHTTPTest(
291 server, "/basic", completed, result_status, result_data, result_error);
292
293 CHECK(test_completed);
294 CHECK(!result_error);
295 CHECK(result_status == 200);
296 CHECK(result_data == test_body);
297}
298
299TEST_CASE("HTTPClient empty response")
300{
301 TestHTTPServer server;
302 server.setResponseBody(""); // Empty body
303 server.setHeader("Content-Length", "0");
304
305 std::atomic<bool> completed{false};
306 std::atomic<int> result_status{0};
307 std::string result_data;
308 boost::system::error_code result_error;
309
310 bool test_completed = runHTTPTest(
311 server, "/empty", completed, result_status, result_data, result_error);
312
313 CHECK(test_completed);
314 CHECK(!result_error);
315 CHECK(result_status == 200);
316 CHECK(result_data.empty());
317}
318
319TEST_CASE("HTTPClient different status codes")
320{
321 std::vector<unsigned int> status_codes = {200, 404, 500};
322
323 for (auto status : status_codes)
324 {
325 TestHTTPServer server;
326 server.setStatusCode(status);
327 server.setResponseBody("Status " + std::to_string(status));
328
329 std::atomic<bool> completed{false};
330 std::atomic<int> result_status{0};
331 std::string result_data;
332 boost::system::error_code result_error;
333
334 bool test_completed = runHTTPTest(
335 server,
336 "/status",
337 completed,
338 result_status,
339 result_data,
340 result_error);
341
342 CHECK(test_completed);
343 CHECK(!result_error);
344 CHECK(result_status == static_cast<int>(status));
345 }
346}
A generic endpoint for log messages.
Definition Journal.h:60
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:48
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:25
TEST_CASE("construct and compare Json::StaticString")
Definition Value.cpp:37
T size(T... args)
T to_string(T... args)