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