Clio  develop
The XRP Ledger API server.
Loading...
Searching...
No Matches
HttpBase.hpp
1#pragma once
2
3#include "data/LedgerCacheInterface.hpp"
4#include "rpc/Errors.hpp"
5#include "util/Assert.hpp"
6#include "util/Taggable.hpp"
7#include "util/build/Build.hpp"
8#include "util/log/Logger.hpp"
9#include "util/prometheus/Http.hpp"
10#include "web/AdminVerificationStrategy.hpp"
11#include "web/ProxyIpResolver.hpp"
12#include "web/SubscriptionContextInterface.hpp"
13#include "web/dosguard/DOSGuardInterface.hpp"
14#include "web/interface/Concepts.hpp"
15#include "web/interface/ConnectionBase.hpp"
16
17#include <boost/asio/error.hpp>
18#include <boost/asio/ip/tcp.hpp>
19#include <boost/asio/ssl/error.hpp>
20#include <boost/beast/core.hpp>
21#include <boost/beast/core/error.hpp>
22#include <boost/beast/core/flat_buffer.hpp>
23#include <boost/beast/http.hpp>
24#include <boost/beast/http/error.hpp>
25#include <boost/beast/http/field.hpp>
26#include <boost/beast/http/message.hpp>
27#include <boost/beast/http/status.hpp>
28#include <boost/beast/http/string_body.hpp>
29#include <boost/beast/http/verb.hpp>
30#include <boost/beast/ssl.hpp>
31#include <boost/core/ignore_unused.hpp>
32#include <boost/json.hpp>
33#include <boost/json/array.hpp>
34#include <boost/json/parse.hpp>
35#include <boost/json/serialize.hpp>
36#include <xrpl/protocol/ErrorCodes.h>
37
38#include <chrono>
39#include <cstddef>
40#include <exception>
41#include <functional>
42#include <memory>
43#include <string>
44#include <utility>
45
46namespace web::impl {
47
48static constexpr auto kHealthCheckHtml = R"html(
49 <!DOCTYPE html>
50 <html>
51 <head><title>Test page for Clio</title></head>
52 <body><h1>Clio Test</h1><p>This page shows Clio http(s) connectivity is working.</p></body>
53 </html>
54)html";
55
56static constexpr auto kCacheCheckLoadedHtml = R"html(
57 <!DOCTYPE html>
58 <html>
59 <head><title>Cache state</title></head>
60 <body><h1>Cache state</h1><p>Cache is fully loaded</p></body>
61 </html>
62)html";
63
64static constexpr auto kCacheCheckNotLoadedHtml = R"html(
65 <!DOCTYPE html>
66 <html>
67 <head><title>Cache state</title></head>
68 <body><h1>Cache state</h1><p>Cache is not yet loaded</p></body>
69 </html>
70)html";
71
72using tcp = boost::asio::ip::tcp;
73
80template <template <typename> typename Derived, SomeServerHandler HandlerType>
81class HttpBase : public ConnectionBase {
82 Derived<HandlerType>&
83 derived()
84 {
85 return static_cast<Derived<HandlerType>&>(*this);
86 }
87
88 // TODO: this should be rewritten using http::message_generator instead
89 struct SendLambda {
90 HttpBase& self;
91
92 explicit SendLambda(HttpBase& self) : self(self)
93 {
94 }
95
96 template <bool IsRequest, typename Body, typename Fields>
97 void
98 operator()(http::message<IsRequest, Body, Fields>&& msg) const
99 {
100 if (self.dead())
101 return;
102
103 // The lifetime of the message has to extend for the duration of the async operation so
104 // we use a shared_ptr to manage it.
105 auto sp = std::make_shared<http::message<IsRequest, Body, Fields>>(std::move(msg));
106
107 // Store a type-erased version of the shared pointer in the class to keep it alive.
108 self.res_ = sp;
109
110 // Write the response
111 http::async_write(
112 self.derived().stream(),
113 *sp,
114 boost::beast::bind_front_handler(
115 &HttpBase::onWrite, self.derived().shared_from_this(), sp->need_eof()
116 )
117 );
118 }
119 };
120
121 std::shared_ptr<void> res_;
122 SendLambda sender_;
123 std::shared_ptr<AdminVerificationStrategy> adminVerification_;
124 std::shared_ptr<ProxyIpResolver> proxyIpResolver_;
125 bool isProxyConnection_ = false;
126
127protected:
128 boost::beast::flat_buffer buffer_;
129 http::request<http::string_body> req_;
130 std::reference_wrapper<dosguard::DOSGuardInterface> dosGuard_;
131 std::shared_ptr<HandlerType> const handler_;
132 std::reference_wrapper<data::LedgerCacheInterface const> cache_;
133 util::Logger log_{"WebServer"};
134 util::Logger perfLog_{"Performance"};
135
136 void
137 httpFail(boost::beast::error_code ec, char const* what)
138 {
139 // ssl::error::stream_truncated, also known as an SSL "short read",
140 // indicates the peer closed the connection without performing the
141 // required closing handshake (for example, Google does this to
142 // improve performance). Generally this can be a security issue,
143 // but if your communication protocol is self-terminated (as
144 // it is with both HTTP and WebSocket) then you may simply
145 // ignore the lack of close_notify.
146 //
147 // https://github.com/boostorg/beast/issues/38
148 //
149 // https://security.stackexchange.com/questions/91435/how-to-handle-a-malicious-ssl-tls-shutdown
150 //
151 // When a short read would cut off the end of an HTTP message,
152 // Beast returns the error boost::beast::http::error::partial_message.
153 // Therefore, if we see a short read here, it has occurred
154 // after the message has been completed, so it is safe to ignore it.
155
156 if (ec == boost::asio::ssl::error::stream_truncated)
157 return;
158
159 if (!ec_ && ec != boost::asio::error::operation_aborted) {
160 ec_ = ec;
161 LOG(perfLog_.info()) << tag() << ": " << what << ": " << ec.message();
162 boost::beast::get_lowest_layer(derived().stream()).socket().close(ec);
163 }
164 }
165
166public:
167 HttpBase(
168 std::string const& ip,
169 std::reference_wrapper<util::TagDecoratorFactory const> tagFactory,
170 std::shared_ptr<AdminVerificationStrategy> adminVerification,
171 std::shared_ptr<ProxyIpResolver> proxyIpResolver,
172 std::reference_wrapper<dosguard::DOSGuardInterface> dosGuard,
173 std::shared_ptr<HandlerType> handler,
174 std::reference_wrapper<data::LedgerCacheInterface const> cache,
175 boost::beast::flat_buffer buffer
176 )
177 : ConnectionBase(tagFactory, ip)
178 , sender_(*this)
179 , adminVerification_(std::move(adminVerification))
180 , proxyIpResolver_(std::move(proxyIpResolver))
181 , buffer_(std::move(buffer))
182 , dosGuard_(dosGuard)
183 , handler_(std::move(handler))
184 , cache_(cache)
185 {
186 LOG(perfLog_.debug()) << tag() << "http session created";
187 dosGuard_.get().increment(ip);
188 }
189
190 ~HttpBase() override
191 {
192 LOG(perfLog_.debug()) << tag() << "http session closed";
193 if (not upgraded)
194 dosGuard_.get().decrement(clientIp_);
195 }
196
197 void
198 doRead()
199 {
200 if (dead())
201 return;
202
203 // Make the request empty before reading, otherwise the operation behavior is undefined.
204 req_ = {};
205
206 // Set the timeout.
207 boost::beast::get_lowest_layer(derived().stream()).expires_after(std::chrono::seconds(30));
208
209 http::async_read(
210 derived().stream(),
211 buffer_,
212 req_,
213 boost::beast::bind_front_handler(&HttpBase::onRead, derived().shared_from_this())
214 );
215 }
216
217 void
218 onRead(boost::beast::error_code ec, [[maybe_unused]] std::size_t bytesTransferred)
219 {
220 if (ec == http::error::end_of_stream)
221 return derived().doClose();
222
223 if (ec)
224 return httpFail(ec, "read");
225
226 auto const updateClientIp = [&](std::string newIp) {
227 if (newIp == clientIp_)
228 return;
229 LOG(log_.info()) << tag()
230 << "Detected a forwarded request from proxy. Resolved client ip: "
231 << newIp;
232 dosGuard_.get().decrement(clientIp_);
233 clientIp_ = std::move(newIp);
234 dosGuard_.get().increment(clientIp_);
235 };
236
237 if (isProxyConnection_) {
238 if (auto resolvedIp = ProxyIpResolver::extractClientIp(req_); resolvedIp.has_value())
239 updateClientIp(std::move(*resolvedIp));
240 } else if (
241 auto resolvedIp = proxyIpResolver_->resolveClientIp(clientIp_, req_);
242 resolvedIp.has_value()
243 ) {
244 updateClientIp(std::move(*resolvedIp));
245 isProxyConnection_ = true;
246 }
247
248 if (req_.method() == http::verb::get and req_.target() == "/health")
249 return sender_(httpResponse(http::status::ok, "text/html", kHealthCheckHtml));
250
251 if (req_.method() == http::verb::get and req_.target() == "/cache_state") {
252 if (cache_.get().isFull()) {
253 return sender_(httpResponse(http::status::ok, "text/html", kCacheCheckLoadedHtml));
254 }
255
256 return sender_(httpResponse(
257 http::status::service_unavailable, "text/html", kCacheCheckNotLoadedHtml
258 ));
259 }
260
261 // Update isAdmin property of the connection
262 ConnectionBase::isAdmin_ = adminVerification_->isAdmin(req_, clientIp_);
263
264 if (boost::beast::websocket::is_upgrade(req_)) {
265 if (dosGuard_.get().isOk(clientIp_)) {
266 // Disable the timeout. The websocket::stream uses its own timeout settings.
267 boost::beast::get_lowest_layer(derived().stream()).expires_never();
268
269 upgraded = true;
270 return derived().upgrade();
271 }
272
273 return sender_(
274 httpResponse(http::status::too_many_requests, "text/html", "Too many requests")
275 );
276 }
277
278 if (auto response = util::prometheus::handlePrometheusRequest(req_, isAdmin());
279 response.has_value())
280 return sender_(std::move(response.value()));
281
282 if (req_.method() != http::verb::post) {
283 return sender_(
284 httpResponse(http::status::bad_request, "text/html", "Expected a POST request")
285 );
286 }
287
288 LOG(log_.info()) << tag() << "Received request from ip = " << clientIp_;
289
290 try {
291 (*handler_)(req_.body(), derived().shared_from_this());
292 } catch (std::exception const&) {
293 return sender_(httpResponse(
294 http::status::internal_server_error,
295 "application/json",
296 boost::json::serialize(rpc::makeError(rpc::RippledError::rpcINTERNAL))
297 ));
298 }
299 }
300
301 void
302 sendSlowDown(std::string const&) override
303 {
304 sender_(httpResponse(
305 http::status::service_unavailable,
306 "text/plain",
307 boost::json::serialize(rpc::makeError(rpc::RippledError::rpcSLOW_DOWN))
308 ));
309 }
310
316 void
317 send(std::string&& msg, http::status status = http::status::ok) override
318 {
319 if (!dosGuard_.get().add(clientIp_, msg.size())) {
320 auto jsonResponse = boost::json::parse(msg).as_object();
321 jsonResponse["warning"] = "load";
322 if (jsonResponse.contains("warnings") && jsonResponse["warnings"].is_array()) {
323 jsonResponse["warnings"].as_array().push_back(
324 rpc::makeWarning(rpc::WarningCode::WarnRpcRateLimit)
325 );
326 } else {
327 jsonResponse["warnings"] =
328 boost::json::array{rpc::makeWarning(rpc::WarningCode::WarnRpcRateLimit)};
329 }
330
331 // Reserialize when we need to include this warning
332 msg = boost::json::serialize(jsonResponse);
333 }
334 sender_(httpResponse(status, "application/json", std::move(msg)));
335 }
336
339 {
340 ASSERT(false, "SubscriptionContext can't be created for a HTTP connection");
341 std::unreachable();
342 }
343
344 void
345 onWrite(bool close, boost::beast::error_code ec, std::size_t bytesTransferred)
346 {
347 boost::ignore_unused(bytesTransferred);
348
349 if (ec)
350 return httpFail(ec, "write");
351
352 // This means we should close the connection, usually because
353 // the response indicated the "Connection: close" semantic.
354 if (close)
355 return derived().doClose();
356
357 res_ = nullptr;
358 doRead();
359 }
360
361private:
362 [[nodiscard]] http::response<http::string_body>
363 httpResponse(http::status status, std::string contentType, std::string message) const
364 {
365 http::response<http::string_body> res{status, req_.version()};
366 res.set(http::field::server, "clio-server-" + util::build::getClioVersionString());
367 res.set(http::field::content_type, contentType);
368 res.keep_alive(req_.keep_alive());
369 res.body() = std::move(message);
370 res.prepare_payload();
371 return res;
372 };
373};
374
375} // namespace web::impl
A simple thread-safe logger for the channel specified in the constructor.
Definition Logger.hpp:78
A factory for TagDecorator instantiation.
Definition Taggable.hpp:165
BaseTagDecorator const & tag() const
Getter for tag decorator.
Definition Taggable.hpp:264
static std::optional< std::string > extractClientIp(HttpHeaders const &headers)
Extracts the client IP from the Forwarded HTTP header.
Definition ProxyIpResolver.cpp:70
void send(std::string &&msg, http::status status=http::status::ok) override
Send a response to the client The message length will be added to the DOSGuard, if the limit is reach...
Definition HttpBase.hpp:317
void sendSlowDown(std::string const &) override
Send a "slow down" error response to the client.
Definition HttpBase.hpp:302
SubscriptionContextPtr makeSubscriptionContext(util::TagDecoratorFactory const &) override
Get the subscription context for this connection.
Definition HttpBase.hpp:338
boost::json::object makeError(RippledError err, std::optional< std::string_view > customError, std::optional< std::string_view > customMessage)
Generate JSON from a rpc::RippledError.
Definition Errors.cpp:160
boost::json::object makeWarning(WarningCode code)
Generate JSON from a rpc::WarningCode.
Definition Errors.cpp:84
std::shared_ptr< SubscriptionContextInterface > SubscriptionContextPtr
An alias for shared pointer to a SubscriptionContextInterface.
Definition SubscriptionContextInterface.hpp:64
ConnectionBase(util::TagDecoratorFactory const &tagFactory, std::string ip)
Create a new connection base.
Definition ConnectionBase.hpp:40
bool isAdmin() const
Indicates whether the connection has admin privileges.
Definition ConnectionBase.hpp:99
bool dead()
Indicates whether the connection had an error and is considered dead.
Definition ConnectionBase.hpp:88