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