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