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 "rpc/Errors.hpp"
23#include "util/Assert.hpp"
24#include "util/Taggable.hpp"
25#include "util/build/Build.hpp"
26#include "util/log/Logger.hpp"
27#include "util/prometheus/Http.hpp"
28#include "web/AdminVerificationStrategy.hpp"
29#include "web/SubscriptionContextInterface.hpp"
30#include "web/dosguard/DOSGuardInterface.hpp"
31#include "web/interface/Concepts.hpp"
32#include "web/interface/ConnectionBase.hpp"
33
34#include <boost/asio/error.hpp>
35#include <boost/asio/ip/tcp.hpp>
36#include <boost/asio/ssl/error.hpp>
37#include <boost/beast/core.hpp>
38#include <boost/beast/core/error.hpp>
39#include <boost/beast/core/flat_buffer.hpp>
40#include <boost/beast/http.hpp>
41#include <boost/beast/http/error.hpp>
42#include <boost/beast/http/field.hpp>
43#include <boost/beast/http/message.hpp>
44#include <boost/beast/http/status.hpp>
45#include <boost/beast/http/string_body.hpp>
46#include <boost/beast/http/verb.hpp>
47#include <boost/beast/ssl.hpp>
48#include <boost/core/ignore_unused.hpp>
49#include <boost/json.hpp>
50#include <boost/json/array.hpp>
51#include <boost/json/parse.hpp>
52#include <boost/json/serialize.hpp>
53#include <xrpl/protocol/ErrorCodes.h>
54
55#include <chrono>
56#include <cstddef>
57#include <exception>
58#include <functional>
59#include <memory>
60#include <string>
61#include <utility>
62
63namespace web::impl {
64
65static auto constexpr kHEALTH_CHECK_HTML = R"html(
66 <!DOCTYPE html>
67 <html>
68 <head><title>Test page for Clio</title></head>
69 <body><h1>Clio Test</h1><p>This page shows Clio http(s) connectivity is working.</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 we use a shared_ptr
105 // 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(&HttpBase::onWrite, self.derived().shared_from_this(), sp->need_eof())
116 );
117 }
118 };
119
120 std::shared_ptr<void> res_;
121 SendLambda sender_;
122 std::shared_ptr<AdminVerificationStrategy> adminVerification_;
123
124protected:
125 boost::beast::flat_buffer buffer_;
126 http::request<http::string_body> req_;
127 std::reference_wrapper<dosguard::DOSGuardInterface> dosGuard_;
128 std::shared_ptr<HandlerType> const handler_;
129 util::Logger log_{"WebServer"};
130 util::Logger perfLog_{"Performance"};
131
132 void
133 httpFail(boost::beast::error_code ec, char const* what)
134 {
135 // ssl::error::stream_truncated, also known as an SSL "short read",
136 // indicates the peer closed the connection without performing the
137 // required closing handshake (for example, Google does this to
138 // improve performance). Generally this can be a security issue,
139 // but if your communication protocol is self-terminated (as
140 // it is with both HTTP and WebSocket) then you may simply
141 // ignore the lack of close_notify.
142 //
143 // https://github.com/boostorg/beast/issues/38
144 //
145 // https://security.stackexchange.com/questions/91435/how-to-handle-a-malicious-ssl-tls-shutdown
146 //
147 // When a short read would cut off the end of an HTTP message,
148 // Beast returns the error boost::beast::http::error::partial_message.
149 // Therefore, if we see a short read here, it has occurred
150 // after the message has been completed, so it is safe to ignore it.
151
152 if (ec == boost::asio::ssl::error::stream_truncated)
153 return;
154
155 if (!ec_ && ec != boost::asio::error::operation_aborted) {
156 ec_ = ec;
157 LOG(perfLog_.info()) << tag() << ": " << what << ": " << ec.message();
158 boost::beast::get_lowest_layer(derived().stream()).socket().close(ec);
159 }
160 }
161
162public:
163 HttpBase(
164 std::string const& ip,
165 std::reference_wrapper<util::TagDecoratorFactory const> tagFactory,
166 std::shared_ptr<AdminVerificationStrategy> adminVerification,
167 std::reference_wrapper<dosguard::DOSGuardInterface> dosGuard,
168 std::shared_ptr<HandlerType> handler,
169 boost::beast::flat_buffer buffer
170 )
171 : ConnectionBase(tagFactory, ip)
172 , sender_(*this)
173 , adminVerification_(std::move(adminVerification))
174 , buffer_(std::move(buffer))
175 , dosGuard_(dosGuard)
176 , handler_(std::move(handler))
177 {
178 LOG(perfLog_.debug()) << tag() << "http session created";
179 dosGuard_.get().increment(ip);
180 }
181
182 ~HttpBase() override
183 {
184 LOG(perfLog_.debug()) << tag() << "http session closed";
185 if (not upgraded)
186 dosGuard_.get().decrement(this->clientIp);
187 }
188
189 void
190 doRead()
191 {
192 if (dead())
193 return;
194
195 // Make the request empty before reading, otherwise the operation behavior is undefined.
196 req_ = {};
197
198 // Set the timeout.
199 boost::beast::get_lowest_layer(derived().stream()).expires_after(std::chrono::seconds(30));
200
201 http::async_read(
202 derived().stream(),
203 buffer_,
204 req_,
205 boost::beast::bind_front_handler(&HttpBase::onRead, derived().shared_from_this())
206 );
207 }
208
209 void
210 onRead(boost::beast::error_code ec, [[maybe_unused]] std::size_t bytesTransferred)
211 {
212 if (ec == http::error::end_of_stream)
213 return derived().doClose();
214
215 if (ec)
216 return httpFail(ec, "read");
217
218 if (req_.method() == http::verb::get and req_.target() == "/health")
219 return sender_(httpResponse(http::status::ok, "text/html", kHEALTH_CHECK_HTML));
220
221 // Update isAdmin property of the connection
222 ConnectionBase::isAdmin_ = adminVerification_->isAdmin(req_, this->clientIp);
223
224 if (boost::beast::websocket::is_upgrade(req_)) {
225 if (dosGuard_.get().isOk(this->clientIp)) {
226 // Disable the timeout. The websocket::stream uses its own timeout settings.
227 boost::beast::get_lowest_layer(derived().stream()).expires_never();
228
229 upgraded = true;
230 return derived().upgrade();
231 }
232
233 return sender_(httpResponse(http::status::too_many_requests, "text/html", "Too many requests"));
234 }
235
236 if (auto response = util::prometheus::handlePrometheusRequest(req_, isAdmin()); response.has_value())
237 return sender_(std::move(response.value()));
238
239 if (req_.method() != http::verb::post) {
240 return sender_(httpResponse(http::status::bad_request, "text/html", "Expected a POST request"));
241 }
242
243 // to avoid overwhelm work queue, the request limit check should be
244 // before posting to queue the web socket creation will be guarded via
245 // connection limit
246 if (!dosGuard_.get().request(clientIp)) {
247 // TODO: this looks like it could be useful to count too in the future
248 return sender_(httpResponse(
249 http::status::service_unavailable,
250 "text/plain",
251 boost::json::serialize(rpc::makeError(rpc::RippledError::rpcSLOW_DOWN))
252 ));
253 }
254
255 LOG(log_.info()) << tag() << "Received request from ip = " << clientIp << " - posting to WorkQueue";
256
257 try {
258 (*handler_)(req_.body(), derived().shared_from_this());
259 } catch (std::exception const&) {
260 return sender_(httpResponse(
261 http::status::internal_server_error,
262 "application/json",
263 boost::json::serialize(rpc::makeError(rpc::RippledError::rpcINTERNAL))
264 ));
265 }
266 }
267
273 void
274 send(std::string&& msg, http::status status = http::status::ok) override
275 {
276 if (!dosGuard_.get().add(clientIp, msg.size())) {
277 auto jsonResponse = boost::json::parse(msg).as_object();
278 jsonResponse["warning"] = "load";
279 if (jsonResponse.contains("warnings") && jsonResponse["warnings"].is_array()) {
280 jsonResponse["warnings"].as_array().push_back(rpc::makeWarning(rpc::WarnRpcRateLimit));
281 } else {
282 jsonResponse["warnings"] = boost::json::array{rpc::makeWarning(rpc::WarnRpcRateLimit)};
283 }
284
285 // Reserialize when we need to include this warning
286 msg = boost::json::serialize(jsonResponse);
287 }
288 sender_(httpResponse(status, "application/json", std::move(msg)));
289 }
290
293 {
294 ASSERT(false, "SubscriptionContext can't be created for a HTTP connection");
295 std::unreachable();
296 }
297
298 void
299 onWrite(bool close, boost::beast::error_code ec, std::size_t bytesTransferred)
300 {
301 boost::ignore_unused(bytesTransferred);
302
303 if (ec)
304 return httpFail(ec, "write");
305
306 // This means we should close the connection, usually because
307 // the response indicated the "Connection: close" semantic.
308 if (close)
309 return derived().doClose();
310
311 res_ = nullptr;
312 doRead();
313 }
314
315private:
316 http::response<http::string_body>
317 httpResponse(http::status status, std::string contentType, std::string message) const
318 {
319 http::response<http::string_body> res{status, req_.version()};
320 res.set(http::field::server, "clio-server-" + util::build::getClioVersionString());
321 res.set(http::field::content_type, contentType);
322 res.keep_alive(req_.keep_alive());
323 res.body() = std::move(message);
324 res.prepare_payload();
325 return res;
326 };
327};
328
329} // namespace web::impl
A simple thread-safe logger for the channel specified in the constructor.
Definition Logger.hpp:110
Pump debug(SourceLocationType const &loc=CURRENT_SRC_LOCATION) const
Interface for logging at Severity::DBG severity.
Definition Logger.cpp:200
Pump info(SourceLocationType const &loc=CURRENT_SRC_LOCATION) const
Interface for logging at Severity::NFO severity.
Definition Logger.cpp:205
A factory for TagDecorator instantiation.
Definition Taggable.hpp:169
BaseTagDecorator const & tag() const
Getter for tag decorator.
Definition Taggable.hpp:267
This is the implementation class for http sessions.
Definition HttpBase.hpp:82
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:274
SubscriptionContextPtr makeSubscriptionContext(util::TagDecoratorFactory const &) override
Get the subscription context for this connection.
Definition HttpBase.hpp:292
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:120
boost::json::object makeWarning(WarningCode code)
Generate JSON from a rpc::WarningCode.
Definition Errors.cpp:65
std::shared_ptr< SubscriptionContextInterface > SubscriptionContextPtr
An alias for shared pointer to a SubscriptionContextInterface.
Definition SubscriptionContextInterface.hpp:86
Base class for all connections.
Definition ConnectionBase.hpp:44
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:111
bool dead()
Indicates whether the connection had an error and is considered dead.
Definition ConnectionBase.hpp:100