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/ProxyIpResolver.hpp"
30#include "web/SubscriptionContextInterface.hpp"
31#include "web/dosguard/DOSGuardInterface.hpp"
32#include "web/interface/Concepts.hpp"
33#include "web/interface/ConnectionBase.hpp"
34
35#include <boost/asio/error.hpp>
36#include <boost/asio/ip/tcp.hpp>
37#include <boost/asio/ssl/error.hpp>
38#include <boost/beast/core.hpp>
39#include <boost/beast/core/error.hpp>
40#include <boost/beast/core/flat_buffer.hpp>
41#include <boost/beast/http.hpp>
42#include <boost/beast/http/error.hpp>
43#include <boost/beast/http/field.hpp>
44#include <boost/beast/http/message.hpp>
45#include <boost/beast/http/status.hpp>
46#include <boost/beast/http/string_body.hpp>
47#include <boost/beast/http/verb.hpp>
48#include <boost/beast/ssl.hpp>
49#include <boost/core/ignore_unused.hpp>
50#include <boost/json.hpp>
51#include <boost/json/array.hpp>
52#include <boost/json/parse.hpp>
53#include <boost/json/serialize.hpp>
54#include <xrpl/protocol/ErrorCodes.h>
55
56#include <chrono>
57#include <cstddef>
58#include <exception>
59#include <functional>
60#include <memory>
61#include <string>
62#include <utility>
63
64namespace web::impl {
65
66static constexpr auto kHEALTH_CHECK_HTML = R"html(
67 <!DOCTYPE html>
68 <html>
69 <head><title>Test page for Clio</title></head>
70 <body><h1>Clio Test</h1><p>This page shows Clio http(s) connectivity is working.</p></body>
71 </html>
72)html";
73
74using tcp = boost::asio::ip::tcp;
75
82template <template <typename> typename Derived, SomeServerHandler HandlerType>
83class HttpBase : public ConnectionBase {
84 Derived<HandlerType>&
85 derived()
86 {
87 return static_cast<Derived<HandlerType>&>(*this);
88 }
89
90 // TODO: this should be rewritten using http::message_generator instead
91 struct SendLambda {
92 HttpBase& self;
93
94 explicit SendLambda(HttpBase& self) : self(self)
95 {
96 }
97
98 template <bool IsRequest, typename Body, typename Fields>
99 void
100 operator()(http::message<IsRequest, Body, Fields>&& msg) const
101 {
102 if (self.dead())
103 return;
104
105 // The lifetime of the message has to extend for the duration of the async operation so we use a shared_ptr
106 // to manage it.
107 auto sp = std::make_shared<http::message<IsRequest, Body, Fields>>(std::move(msg));
108
109 // Store a type-erased version of the shared pointer in the class to keep it alive.
110 self.res_ = sp;
111
112 // Write the response
113 http::async_write(
114 self.derived().stream(),
115 *sp,
116 boost::beast::bind_front_handler(&HttpBase::onWrite, self.derived().shared_from_this(), sp->need_eof())
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 util::Logger log_{"WebServer"};
132 util::Logger perfLog_{"Performance"};
133
134 void
135 httpFail(boost::beast::error_code ec, char const* what)
136 {
137 // ssl::error::stream_truncated, also known as an SSL "short read",
138 // indicates the peer closed the connection without performing the
139 // required closing handshake (for example, Google does this to
140 // improve performance). Generally this can be a security issue,
141 // but if your communication protocol is self-terminated (as
142 // it is with both HTTP and WebSocket) then you may simply
143 // ignore the lack of close_notify.
144 //
145 // https://github.com/boostorg/beast/issues/38
146 //
147 // https://security.stackexchange.com/questions/91435/how-to-handle-a-malicious-ssl-tls-shutdown
148 //
149 // When a short read would cut off the end of an HTTP message,
150 // Beast returns the error boost::beast::http::error::partial_message.
151 // Therefore, if we see a short read here, it has occurred
152 // after the message has been completed, so it is safe to ignore it.
153
154 if (ec == boost::asio::ssl::error::stream_truncated)
155 return;
156
157 if (!ec_ && ec != boost::asio::error::operation_aborted) {
158 ec_ = ec;
159 LOG(perfLog_.info()) << tag() << ": " << what << ": " << ec.message();
160 boost::beast::get_lowest_layer(derived().stream()).socket().close(ec);
161 }
162 }
163
164public:
165 HttpBase(
166 std::string const& ip,
167 std::reference_wrapper<util::TagDecoratorFactory const> tagFactory,
168 std::shared_ptr<AdminVerificationStrategy> adminVerification,
169 std::shared_ptr<ProxyIpResolver> proxyIpResolver,
170 std::reference_wrapper<dosguard::DOSGuardInterface> dosGuard,
171 std::shared_ptr<HandlerType> handler,
172 boost::beast::flat_buffer buffer
173 )
174 : ConnectionBase(tagFactory, ip)
175 , sender_(*this)
176 , adminVerification_(std::move(adminVerification))
177 , proxyIpResolver_(std::move(proxyIpResolver))
178 , buffer_(std::move(buffer))
179 , dosGuard_(dosGuard)
180 , handler_(std::move(handler))
181 {
182 LOG(perfLog_.debug()) << tag() << "http session created";
183 dosGuard_.get().increment(ip);
184 }
185
186 ~HttpBase() override
187 {
188 LOG(perfLog_.debug()) << tag() << "http session closed";
189 if (not upgraded)
190 dosGuard_.get().decrement(clientIp_);
191 }
192
193 void
194 doRead()
195 {
196 if (dead())
197 return;
198
199 // Make the request empty before reading, otherwise the operation behavior is undefined.
200 req_ = {};
201
202 // Set the timeout.
203 boost::beast::get_lowest_layer(derived().stream()).expires_after(std::chrono::seconds(30));
204
205 http::async_read(
206 derived().stream(),
207 buffer_,
208 req_,
209 boost::beast::bind_front_handler(&HttpBase::onRead, derived().shared_from_this())
210 );
211 }
212
213 void
214 onRead(boost::beast::error_code ec, [[maybe_unused]] std::size_t bytesTransferred)
215 {
216 if (ec == http::error::end_of_stream)
217 return derived().doClose();
218
219 if (ec)
220 return httpFail(ec, "read");
221
222 if (req_.method() == http::verb::get and req_.target() == "/health")
223 return sender_(httpResponse(http::status::ok, "text/html", kHEALTH_CHECK_HTML));
224
225 if (auto resolvedIp = proxyIpResolver_->resolveClientIp(clientIp_, req_); resolvedIp != clientIp_) {
226 LOG(log_.info()) << tag() << "Detected a forwarded request from proxy. Proxy ip: " << clientIp_
227 << ". Resolved client ip: " << resolvedIp;
228 dosGuard_.get().decrement(clientIp_);
229 clientIp_ = std::move(resolvedIp);
230 dosGuard_.get().increment(clientIp_);
231 }
232
233 // Update isAdmin property of the connection
234 ConnectionBase::isAdmin_ = adminVerification_->isAdmin(req_, clientIp_);
235
236 if (boost::beast::websocket::is_upgrade(req_)) {
237 if (dosGuard_.get().isOk(clientIp_)) {
238 // Disable the timeout. The websocket::stream uses its own timeout settings.
239 boost::beast::get_lowest_layer(derived().stream()).expires_never();
240
241 upgraded = true;
242 return derived().upgrade();
243 }
244
245 return sender_(httpResponse(http::status::too_many_requests, "text/html", "Too many requests"));
246 }
247
248 if (auto response = util::prometheus::handlePrometheusRequest(req_, isAdmin()); response.has_value())
249 return sender_(std::move(response.value()));
250
251 if (req_.method() != http::verb::post) {
252 return sender_(httpResponse(http::status::bad_request, "text/html", "Expected a POST request"));
253 }
254
255 LOG(log_.info()) << tag() << "Received request from ip = " << clientIp_;
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
268 void
269 sendSlowDown(std::string const&) override
270 {
271 sender_(httpResponse(
272 http::status::service_unavailable,
273 "text/plain",
274 boost::json::serialize(rpc::makeError(rpc::RippledError::rpcSLOW_DOWN))
275 ));
276 }
277
283 void
284 send(std::string&& msg, http::status status = http::status::ok) override
285 {
286 if (!dosGuard_.get().add(clientIp_, msg.size())) {
287 auto jsonResponse = boost::json::parse(msg).as_object();
288 jsonResponse["warning"] = "load";
289 if (jsonResponse.contains("warnings") && jsonResponse["warnings"].is_array()) {
290 jsonResponse["warnings"].as_array().push_back(rpc::makeWarning(rpc::WarnRpcRateLimit));
291 } else {
292 jsonResponse["warnings"] = boost::json::array{rpc::makeWarning(rpc::WarnRpcRateLimit)};
293 }
294
295 // Reserialize when we need to include this warning
296 msg = boost::json::serialize(jsonResponse);
297 }
298 sender_(httpResponse(status, "application/json", std::move(msg)));
299 }
300
303 {
304 ASSERT(false, "SubscriptionContext can't be created for a HTTP connection");
305 std::unreachable();
306 }
307
308 void
309 onWrite(bool close, boost::beast::error_code ec, std::size_t bytesTransferred)
310 {
311 boost::ignore_unused(bytesTransferred);
312
313 if (ec)
314 return httpFail(ec, "write");
315
316 // This means we should close the connection, usually because
317 // the response indicated the "Connection: close" semantic.
318 if (close)
319 return derived().doClose();
320
321 res_ = nullptr;
322 doRead();
323 }
324
325private:
326 http::response<http::string_body>
327 httpResponse(http::status status, std::string contentType, std::string message) const
328 {
329 http::response<http::string_body> res{status, req_.version()};
330 res.set(http::field::server, "clio-server-" + util::build::getClioVersionString());
331 res.set(http::field::content_type, contentType);
332 res.keep_alive(req_.keep_alive());
333 res.body() = std::move(message);
334 res.prepare_payload();
335 return res;
336 };
337};
338
339} // namespace web::impl
A simple thread-safe logger for the channel specified in the constructor.
Definition Logger.hpp:87
Pump debug(SourceLocationType const &loc=CURRENT_SRC_LOCATION) const
Interface for logging at Severity::DBG severity.
Definition Logger.cpp:399
Pump info(SourceLocationType const &loc=CURRENT_SRC_LOCATION) const
Interface for logging at Severity::NFO severity.
Definition Logger.cpp:404
A factory for TagDecorator instantiation.
Definition Taggable.hpp:182
BaseTagDecorator const & tag() const
Getter for tag decorator.
Definition Taggable.hpp:280
This is the implementation class for http sessions.
Definition HttpBase.hpp:83
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:284
void sendSlowDown(std::string const &) override
Send a "slow down" error response to the client.
Definition HttpBase.hpp:269
SubscriptionContextPtr makeSubscriptionContext(util::TagDecoratorFactory const &) override
Get the subscription context for this connection.
Definition HttpBase.hpp:302
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:83
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:118
bool dead()
Indicates whether the connection had an error and is considered dead.
Definition ConnectionBase.hpp:107