Clio  develop
The XRP Ledger API server.
Loading...
Searching...
No Matches
WsBase.hpp
1#pragma once
2
3#include "rpc/Errors.hpp"
4#include "rpc/common/Types.hpp"
5#include "util/Taggable.hpp"
6#include "util/log/Logger.hpp"
7#include "web/LoadWarning.hpp"
8#include "web/SubscriptionContext.hpp"
9#include "web/SubscriptionContextInterface.hpp"
10#include "web/dosguard/DOSGuardInterface.hpp"
11#include "web/interface/Concepts.hpp"
12#include "web/interface/ConnectionBase.hpp"
13
14#include <boost/asio/buffer.hpp>
15#include <boost/asio/error.hpp>
16#include <boost/beast/core.hpp>
17#include <boost/beast/core/error.hpp>
18#include <boost/beast/core/flat_buffer.hpp>
19#include <boost/beast/core/role.hpp>
20#include <boost/beast/http/field.hpp>
21#include <boost/beast/http/message.hpp>
22#include <boost/beast/http/status.hpp>
23#include <boost/beast/http/string_body.hpp>
24#include <boost/beast/version.hpp>
25#include <boost/beast/websocket/error.hpp>
26#include <boost/beast/websocket/rfc6455.hpp>
27#include <boost/beast/websocket/stream_base.hpp>
28#include <boost/core/ignore_unused.hpp>
29#include <boost/json/array.hpp>
30#include <boost/json/parse.hpp>
31#include <boost/json/serialize.hpp>
32#include <xrpl/protocol/ErrorCodes.h>
33
34#include <cstddef>
35#include <cstdint>
36#include <exception>
37#include <functional>
38#include <memory>
39#include <queue>
40#include <string>
41#include <utility>
42
43namespace web::impl {
44
56template <template <typename> typename Derived, SomeServerHandler HandlerType>
57class WsBase : public ConnectionBase,
58 public std::enable_shared_from_this<WsBase<Derived, HandlerType>> {
59 using std::enable_shared_from_this<WsBase<Derived, HandlerType>>::shared_from_this;
60
61 boost::beast::flat_buffer buffer_;
62 std::reference_wrapper<dosguard::DOSGuardInterface> dosGuard_;
63 bool sending_ = false;
64 std::queue<std::shared_ptr<std::string>> messages_;
65 std::shared_ptr<HandlerType> const handler_;
66
67 SubscriptionContextPtr subscriptionContext_;
68 std::uint32_t maxSendingQueueSize_;
69
70protected:
71 util::Logger log_{"WebServer"};
72 util::Logger perfLog_{"Performance"};
73
74 void
75 wsFail(boost::beast::error_code ec, char const* what)
76 {
77 // Don't log if the WebSocket stream was gracefully closed at both endpoints
78 if (ec != boost::beast::websocket::error::closed) {
79 LOG(log_.error()) << tag() << ": " << what << ": " << ec.message() << ": "
80 << ec.value();
81 }
82
83 if (!ec_ && ec != boost::asio::error::operation_aborted) {
84 ec_ = ec;
85 boost::beast::get_lowest_layer(derived().ws()).socket().close(ec);
86 }
87 }
88
89public:
90 explicit WsBase(
91 std::string ip,
92 std::reference_wrapper<util::TagDecoratorFactory const> tagFactory,
93 std::reference_wrapper<dosguard::DOSGuardInterface> dosGuard,
94 std::shared_ptr<HandlerType> const& handler,
95 boost::beast::flat_buffer&& buffer,
96 std::uint32_t maxSendingQueueSize
97 )
98 : ConnectionBase(tagFactory, ip)
99 , buffer_(std::move(buffer))
100 , dosGuard_(dosGuard)
101 , handler_(handler)
102 , maxSendingQueueSize_(maxSendingQueueSize)
103 {
104 upgraded = true; // NOLINT (cppcoreguidelines-pro-type-member-init)
105
106 LOG(perfLog_.debug()) << tag() << "session created";
107 }
108
109 ~WsBase() override
110 {
111 LOG(perfLog_.debug()) << tag() << "session closed";
112 dosGuard_.get().decrement(clientIp_);
113 }
114
115 Derived<HandlerType>&
116 derived()
117 {
118 return static_cast<Derived<HandlerType>&>(*this);
119 }
120
121 void
122 doWrite()
123 {
124 sending_ = true;
125 derived().ws().async_write(
126 boost::asio::buffer(messages_.front()->data(), messages_.front()->size()),
127 boost::beast::bind_front_handler(&WsBase::onWrite, derived().shared_from_this())
128 );
129 }
130
131 void
132 onWrite(boost::system::error_code ec, std::size_t)
133 {
134 messages_.pop();
135 sending_ = false;
136 if (ec) {
137 wsFail(ec, "Failed to write");
138 } else {
139 maybeSendNext();
140 }
141 }
142
143 void
144 maybeSendNext()
145 {
146 if (ec_ || sending_ || messages_.empty())
147 return;
148
149 doWrite();
150 }
151
152 void
153 sendSlowDown(std::string const& request) override
154 {
155 sendError(rpc::XrpldError::RpcSlowDown, request);
156 }
157
164 void
165 send(std::shared_ptr<std::string> msg) override
166 {
167 // Note: post used instead of dispatch to guarantee async behavior of wsFail and
168 // maybeSendNext
169 boost::asio::post(
170 derived().ws().get_executor(),
171 [this, self = derived().shared_from_this(), msg = std::move(msg)]() {
172 if (messages_.size() > maxSendingQueueSize_) {
173 wsFail(boost::asio::error::timed_out, "Client is too slow");
174 return;
175 }
176
177 messages_.push(msg);
178 maybeSendNext();
179 }
180 );
181 }
182
191 {
192 if (subscriptionContext_ == nullptr) {
193 subscriptionContext_ =
194 std::make_shared<SubscriptionContext>(factory, shared_from_this());
195 }
196 return subscriptionContext_;
197 }
198
206 void
207 send(std::string&& msg, http::status) override
208 {
209 if (!dosGuard_.get().add(clientIp_, msg.size())) {
210 if (auto const warned = withLoadWarning(msg); warned.has_value()) {
211 // Reserialize when we need to include this warning
212 msg = boost::json::serialize(*warned);
213 }
214 }
215 auto sharedMsg = std::make_shared<std::string>(std::move(msg));
216 send(std::move(sharedMsg));
217 }
218
222 void
223 run(http::request<http::string_body> req)
224 {
225 using namespace boost::beast;
226
227 derived().ws().set_option(websocket::stream_base::timeout::suggested(role_type::server));
228
229 // Set a decorator to change the Server of the handshake
230 derived().ws().set_option(
231 websocket::stream_base::decorator([](websocket::response_type& res) {
232 res.set(
233 http::field::server,
234 std::string(BOOST_BEAST_VERSION_STRING) + " websocket-server-async"
235 );
236 })
237 );
238
239 derived().ws().async_accept(
240 req, bind_front_handler(&WsBase::onAccept, this->shared_from_this())
241 );
242 }
243
244 void
245 onAccept(boost::beast::error_code ec)
246 {
247 if (ec)
248 return wsFail(ec, "accept");
249
250 LOG(perfLog_.info()) << tag() << "accepting new connection";
251
252 doRead();
253 }
254
255 void
256 doRead()
257 {
258 if (dead())
259 return;
260
261 // Note: use entirely new buffer so previously used, potentially large, capacity is
262 // deallocated
263 buffer_ = boost::beast::flat_buffer{};
264
265 derived().ws().async_read(
266 buffer_, boost::beast::bind_front_handler(&WsBase::onRead, this->shared_from_this())
267 );
268 }
269
270 void
271 onRead(boost::beast::error_code ec, std::size_t bytesTransferred)
272 {
273 boost::ignore_unused(bytesTransferred);
274
275 if (ec)
276 return wsFail(ec, "read");
277
278 LOG(perfLog_.info()) << tag() << "Received request from ip = " << clientIp_;
279
280 std::string requestStr{static_cast<char const*>(buffer_.data().data()), buffer_.size()};
281
282 try {
283 (*handler_)(requestStr, shared_from_this());
284 } catch (std::exception const&) {
285 sendError(rpc::XrpldError::RpcInternal, std::move(requestStr));
286 }
287
288 doRead();
289 }
290
291private:
292 void
293 sendError(rpc::XrpldError error, std::string requestStr)
294 {
295 auto e = rpc::makeError(error);
296
297 try {
298 auto request = boost::json::parse(requestStr);
299 if (request.is_object() && request.as_object().contains("id"))
300 e["id"] = request.as_object().at("id");
301 e["request"] = std::move(request);
302 } catch (std::exception const&) {
303 e["request"] = requestStr;
304 }
305
306 this->send(std::make_shared<std::string>(boost::json::serialize(e)));
307 }
308};
309} // namespace web::impl
A simple thread-safe logger for the channel specified in the constructor.
Definition Logger.hpp:78
Pump info(std::source_location const &loc=std::source_location::current()) const
Interface for logging at Severity::NFO severity.
Definition Logger.cpp:502
A factory for TagDecorator instantiation.
Definition Taggable.hpp:165
BaseTagDecorator const & tag() const
Getter for tag decorator.
Definition Taggable.hpp:264
void sendSlowDown(std::string const &request) override
Send a "slow down" error response to the client.
Definition WsBase.hpp:153
SubscriptionContextPtr makeSubscriptionContext(util::TagDecoratorFactory const &factory) override
Get the subscription context for this connection.
Definition WsBase.hpp:190
void run(http::request< http::string_body > req)
Accept the session asynchronously.
Definition WsBase.hpp:223
void send(std::shared_ptr< std::string > msg) override
Send a message to the client.
Definition WsBase.hpp:165
void send(std::string &&msg, http::status) override
Send the response to the client.
Definition WsBase.hpp:207
boost::json::object makeError(XrpldError err, std::optional< std::string_view > customError, std::optional< std::string_view > customMessage)
Generate JSON from a rpc::XrpldError.
Definition Errors.cpp:185
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:33
ConnectionBase(util::TagDecoratorFactory const &tagFactory, std::string ip)
Create a new connection base.
Definition ConnectionBase.hpp:40
bool dead()
Indicates whether the connection had an error and is considered dead.
Definition ConnectionBase.hpp:88