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 kHEALTH_CHECK_HTML = 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 kCACHE_CHECK_LOADED_HTML = 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 kCACHE_CHECK_NOT_LOADED_HTML = 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
126protected:
127 boost::beast::flat_buffer buffer_;
128 http::request<http::string_body> req_;
129 std::reference_wrapper<dosguard::DOSGuardInterface> dosGuard_;
130 std::shared_ptr<HandlerType> const handler_;
131 std::reference_wrapper<data::LedgerCacheInterface const> cache_;
132 util::Logger log_{"WebServer"};
133 util::Logger perfLog_{"Performance"};
134
135 void
136 httpFail(boost::beast::error_code ec, char const* what)
137 {
138 // ssl::error::stream_truncated, also known as an SSL "short read",
139 // indicates the peer closed the connection without performing the
140 // required closing handshake (for example, Google does this to
141 // improve performance). Generally this can be a security issue,
142 // but if your communication protocol is self-terminated (as
143 // it is with both HTTP and WebSocket) then you may simply
144 // ignore the lack of close_notify.
145 //
146 // https://github.com/boostorg/beast/issues/38
147 //
148 // https://security.stackexchange.com/questions/91435/how-to-handle-a-malicious-ssl-tls-shutdown
149 //
150 // When a short read would cut off the end of an HTTP message,
151 // Beast returns the error boost::beast::http::error::partial_message.
152 // Therefore, if we see a short read here, it has occurred
153 // after the message has been completed, so it is safe to ignore it.
154
155 if (ec == boost::asio::ssl::error::stream_truncated)
156 return;
157
158 if (!ec_ && ec != boost::asio::error::operation_aborted) {
159 ec_ = ec;
160 LOG(perfLog_.info()) << tag() << ": " << what << ": " << ec.message();
161 boost::beast::get_lowest_layer(derived().stream()).socket().close(ec);
162 }
163 }
164
165public:
166 HttpBase(
167 std::string const& ip,
168 std::reference_wrapper<util::TagDecoratorFactory const> tagFactory,
169 std::shared_ptr<AdminVerificationStrategy> adminVerification,
170 std::shared_ptr<ProxyIpResolver> proxyIpResolver,
171 std::reference_wrapper<dosguard::DOSGuardInterface> dosGuard,
172 std::shared_ptr<HandlerType> handler,
173 std::reference_wrapper<data::LedgerCacheInterface const> cache,
174 boost::beast::flat_buffer buffer
175 )
176 : ConnectionBase(tagFactory, ip)
177 , sender_(*this)
178 , adminVerification_(std::move(adminVerification))
179 , proxyIpResolver_(std::move(proxyIpResolver))
180 , buffer_(std::move(buffer))
181 , dosGuard_(dosGuard)
182 , handler_(std::move(handler))
183 , cache_(cache)
184 {
185 LOG(perfLog_.debug()) << tag() << "http session created";
186 dosGuard_.get().increment(ip);
187 }
188
189 ~HttpBase() override
190 {
191 LOG(perfLog_.debug()) << tag() << "http session closed";
192 if (not upgraded)
193 dosGuard_.get().decrement(clientIp_);
194 }
195
196 void
197 doRead()
198 {
199 if (dead())
200 return;
201
202 // Make the request empty before reading, otherwise the operation behavior is undefined.
203 req_ = {};
204
205 // Set the timeout.
206 boost::beast::get_lowest_layer(derived().stream()).expires_after(std::chrono::seconds(30));
207
208 http::async_read(
209 derived().stream(),
210 buffer_,
211 req_,
212 boost::beast::bind_front_handler(&HttpBase::onRead, derived().shared_from_this())
213 );
214 }
215
216 void
217 onRead(boost::beast::error_code ec, [[maybe_unused]] std::size_t bytesTransferred)
218 {
219 if (ec == http::error::end_of_stream)
220 return derived().doClose();
221
222 if (ec)
223 return httpFail(ec, "read");
224
225 if (auto resolvedIp = proxyIpResolver_->resolveClientIp(clientIp_, req_);
226 resolvedIp != clientIp_) {
227 LOG(log_.info()) << tag()
228 << "Detected a forwarded request from proxy. Proxy ip: " << clientIp_
229 << ". Resolved client ip: " << resolvedIp;
230 dosGuard_.get().decrement(clientIp_);
231 clientIp_ = std::move(resolvedIp);
232 dosGuard_.get().increment(clientIp_);
233 }
234
235 if (req_.method() == http::verb::get and req_.target() == "/health")
236 return sender_(httpResponse(http::status::ok, "text/html", kHEALTH_CHECK_HTML));
237
238 if (req_.method() == http::verb::get and req_.target() == "/cache_state") {
239 if (cache_.get().isFull()) {
240 return sender_(
241 httpResponse(http::status::ok, "text/html", kCACHE_CHECK_LOADED_HTML)
242 );
243 }
244
245 return sender_(httpResponse(
246 http::status::service_unavailable, "text/html", kCACHE_CHECK_NOT_LOADED_HTML
247 ));
248 }
249
250 // Update isAdmin property of the connection
251 ConnectionBase::isAdmin_ = adminVerification_->isAdmin(req_, clientIp_);
252
253 if (boost::beast::websocket::is_upgrade(req_)) {
254 if (dosGuard_.get().isOk(clientIp_)) {
255 // Disable the timeout. The websocket::stream uses its own timeout settings.
256 boost::beast::get_lowest_layer(derived().stream()).expires_never();
257
258 upgraded = true;
259 return derived().upgrade();
260 }
261
262 return sender_(
263 httpResponse(http::status::too_many_requests, "text/html", "Too many requests")
264 );
265 }
266
267 if (auto response = util::prometheus::handlePrometheusRequest(req_, isAdmin());
268 response.has_value())
269 return sender_(std::move(response.value()));
270
271 if (req_.method() != http::verb::post) {
272 return sender_(
273 httpResponse(http::status::bad_request, "text/html", "Expected a POST request")
274 );
275 }
276
277 LOG(log_.info()) << tag() << "Received request from ip = " << clientIp_;
278
279 try {
280 (*handler_)(req_.body(), derived().shared_from_this());
281 } catch (std::exception const&) {
282 return sender_(httpResponse(
283 http::status::internal_server_error,
284 "application/json",
285 boost::json::serialize(rpc::makeError(rpc::RippledError::rpcINTERNAL))
286 ));
287 }
288 }
289
290 void
291 sendSlowDown(std::string const&) override
292 {
293 sender_(httpResponse(
294 http::status::service_unavailable,
295 "text/plain",
296 boost::json::serialize(rpc::makeError(rpc::RippledError::rpcSLOW_DOWN))
297 ));
298 }
299
305 void
306 send(std::string&& msg, http::status status = http::status::ok) override
307 {
308 if (!dosGuard_.get().add(clientIp_, msg.size())) {
309 auto jsonResponse = boost::json::parse(msg).as_object();
310 jsonResponse["warning"] = "load";
311 if (jsonResponse.contains("warnings") && jsonResponse["warnings"].is_array()) {
312 jsonResponse["warnings"].as_array().push_back(
313 rpc::makeWarning(rpc::WarnRpcRateLimit)
314 );
315 } else {
316 jsonResponse["warnings"] =
317 boost::json::array{rpc::makeWarning(rpc::WarnRpcRateLimit)};
318 }
319
320 // Reserialize when we need to include this warning
321 msg = boost::json::serialize(jsonResponse);
322 }
323 sender_(httpResponse(status, "application/json", std::move(msg)));
324 }
325
328 {
329 ASSERT(false, "SubscriptionContext can't be created for a HTTP connection");
330 std::unreachable();
331 }
332
333 void
334 onWrite(bool close, boost::beast::error_code ec, std::size_t bytesTransferred)
335 {
336 boost::ignore_unused(bytesTransferred);
337
338 if (ec)
339 return httpFail(ec, "write");
340
341 // This means we should close the connection, usually because
342 // the response indicated the "Connection: close" semantic.
343 if (close)
344 return derived().doClose();
345
346 res_ = nullptr;
347 doRead();
348 }
349
350private:
351 http::response<http::string_body>
352 httpResponse(http::status status, std::string contentType, std::string message) const
353 {
354 http::response<http::string_body> res{status, req_.version()};
355 res.set(http::field::server, "clio-server-" + util::build::getClioVersionString());
356 res.set(http::field::content_type, contentType);
357 res.keep_alive(req_.keep_alive());
358 res.body() = std::move(message);
359 res.prepare_payload();
360 return res;
361 };
362};
363
364} // namespace web::impl
A simple thread-safe logger for the channel specified in the constructor.
Definition Logger.hpp:77
A factory for TagDecorator instantiation.
Definition Taggable.hpp:165
BaseTagDecorator const & tag() const
Getter for tag decorator.
Definition Taggable.hpp:264
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:306
void sendSlowDown(std::string const &) override
Send a "slow down" error response to the client.
Definition HttpBase.hpp:291
SubscriptionContextPtr makeSubscriptionContext(util::TagDecoratorFactory const &) override
Get the subscription context for this connection.
Definition HttpBase.hpp:327
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