mirror of
https://github.com/XRPLF/clio.git
synced 2026-04-29 15:37:53 +00:00
161
src/web/impl/ErrorHandling.h
Normal file
161
src/web/impl/ErrorHandling.h
Normal file
@@ -0,0 +1,161 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2023, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <rpc/Errors.h>
|
||||
#include <web/interface/ConnectionBase.h>
|
||||
|
||||
#include <boost/beast/http.hpp>
|
||||
#include <boost/json.hpp>
|
||||
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
namespace web::detail {
|
||||
|
||||
/**
|
||||
* @brief A helper that attempts to match rippled reporting mode HTTP errors as close as possible.
|
||||
*/
|
||||
class ErrorHelper
|
||||
{
|
||||
std::shared_ptr<web::ConnectionBase> connection_;
|
||||
std::optional<boost::json::object> request_;
|
||||
|
||||
public:
|
||||
ErrorHelper(
|
||||
std::shared_ptr<web::ConnectionBase> const& connection,
|
||||
std::optional<boost::json::object> request = std::nullopt)
|
||||
: connection_{connection}, request_{std::move(request)}
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
sendError(RPC::Status const& err) const
|
||||
{
|
||||
if (connection_->upgraded)
|
||||
{
|
||||
connection_->send(boost::json::serialize(composeError(err)));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Note: a collection of crutches to match rippled output follows
|
||||
if (auto const clioCode = std::get_if<RPC::ClioError>(&err.code))
|
||||
{
|
||||
switch (*clioCode)
|
||||
{
|
||||
case RPC::ClioError::rpcINVALID_API_VERSION:
|
||||
connection_->send(
|
||||
std::string{RPC::getErrorInfo(*clioCode).error}, boost::beast::http::status::bad_request);
|
||||
break;
|
||||
case RPC::ClioError::rpcCOMMAND_IS_MISSING:
|
||||
connection_->send("Null method", boost::beast::http::status::bad_request);
|
||||
break;
|
||||
case RPC::ClioError::rpcCOMMAND_IS_EMPTY:
|
||||
connection_->send("method is empty", boost::beast::http::status::bad_request);
|
||||
break;
|
||||
case RPC::ClioError::rpcCOMMAND_NOT_STRING:
|
||||
connection_->send("method is not string", boost::beast::http::status::bad_request);
|
||||
break;
|
||||
case RPC::ClioError::rpcPARAMS_UNPARSEABLE:
|
||||
connection_->send("params unparseable", boost::beast::http::status::bad_request);
|
||||
break;
|
||||
|
||||
// others are not applicable but we want a compilation error next time we add one
|
||||
case RPC::ClioError::rpcUNKNOWN_OPTION:
|
||||
case RPC::ClioError::rpcMALFORMED_CURRENCY:
|
||||
case RPC::ClioError::rpcMALFORMED_REQUEST:
|
||||
case RPC::ClioError::rpcMALFORMED_OWNER:
|
||||
case RPC::ClioError::rpcMALFORMED_ADDRESS:
|
||||
case RPC::ClioError::rpcINVALID_HOT_WALLET:
|
||||
case RPC::ClioError::rpcFIELD_NOT_FOUND_TRANSACTION:
|
||||
assert(false); // this should never happen
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
connection_->send(boost::json::serialize(composeError(err)), boost::beast::http::status::bad_request);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
sendInternalError() const
|
||||
{
|
||||
connection_->send(
|
||||
boost::json::serialize(composeError(RPC::RippledError::rpcINTERNAL)),
|
||||
boost::beast::http::status::internal_server_error);
|
||||
}
|
||||
|
||||
void
|
||||
sendNotReadyError() const
|
||||
{
|
||||
connection_->send(
|
||||
boost::json::serialize(composeError(RPC::RippledError::rpcNOT_READY)), boost::beast::http::status::ok);
|
||||
}
|
||||
|
||||
void
|
||||
sendTooBusyError() const
|
||||
{
|
||||
if (connection_->upgraded)
|
||||
connection_->send(
|
||||
boost::json::serialize(RPC::makeError(RPC::RippledError::rpcTOO_BUSY)), boost::beast::http::status::ok);
|
||||
else
|
||||
connection_->send(
|
||||
boost::json::serialize(RPC::makeError(RPC::RippledError::rpcTOO_BUSY)),
|
||||
boost::beast::http::status::service_unavailable);
|
||||
}
|
||||
|
||||
void
|
||||
sendJsonParsingError(std::string_view reason) const
|
||||
{
|
||||
if (connection_->upgraded)
|
||||
connection_->send(
|
||||
boost::json::serialize(RPC::makeError(RPC::RippledError::rpcBAD_SYNTAX)),
|
||||
boost::beast::http::status::ok);
|
||||
else
|
||||
connection_->send(
|
||||
fmt::format("Unable to parse request: {}", reason), boost::beast::http::status::bad_request);
|
||||
}
|
||||
|
||||
boost::json::object
|
||||
composeError(auto const& error) const
|
||||
{
|
||||
auto e = RPC::makeError(error);
|
||||
|
||||
if (request_)
|
||||
{
|
||||
auto const& req = request_.value();
|
||||
auto const id = req.contains("id") ? req.at("id") : nullptr;
|
||||
if (not id.is_null())
|
||||
e["id"] = id;
|
||||
|
||||
e["request"] = req;
|
||||
}
|
||||
|
||||
if (connection_->upgraded)
|
||||
return e;
|
||||
else
|
||||
return {{"result", e}};
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace web::detail
|
||||
276
src/web/impl/HttpBase.h
Normal file
276
src/web/impl/HttpBase.h
Normal file
@@ -0,0 +1,276 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2023, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <main/Build.h>
|
||||
#include <util/log/Logger.h>
|
||||
#include <web/DOSGuard.h>
|
||||
#include <web/interface/Concepts.h>
|
||||
#include <web/interface/ConnectionBase.h>
|
||||
|
||||
#include <boost/beast/core.hpp>
|
||||
#include <boost/beast/http.hpp>
|
||||
#include <boost/beast/ssl.hpp>
|
||||
#include <boost/json.hpp>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
namespace web::detail {
|
||||
|
||||
using tcp = boost::asio::ip::tcp;
|
||||
|
||||
/**
|
||||
* @brief This is the implementation class for http sessions
|
||||
*
|
||||
* @tparam Derived The derived class
|
||||
* @tparam Handler The handler class, will be called when a request is received.
|
||||
*/
|
||||
template <template <class> class Derived, ServerHandler Handler>
|
||||
class HttpBase : public ConnectionBase
|
||||
{
|
||||
Derived<Handler>&
|
||||
derived()
|
||||
{
|
||||
return static_cast<Derived<Handler>&>(*this);
|
||||
}
|
||||
|
||||
// TODO: this should be rewritten using http::message_generator instead
|
||||
struct SendLambda
|
||||
{
|
||||
HttpBase& self_;
|
||||
|
||||
explicit SendLambda(HttpBase& self) : self_(self)
|
||||
{
|
||||
}
|
||||
|
||||
template <bool isRequest, class Body, class Fields>
|
||||
void
|
||||
operator()(http::message<isRequest, Body, Fields>&& msg) const
|
||||
{
|
||||
if (self_.dead())
|
||||
return;
|
||||
|
||||
// The lifetime of the message has to extend for the duration of the async operation so we use a shared_ptr
|
||||
// to manage it.
|
||||
auto sp = std::make_shared<http::message<isRequest, Body, Fields>>(std::move(msg));
|
||||
|
||||
// Store a type-erased version of the shared pointer in the class to keep it alive.
|
||||
self_.res_ = sp;
|
||||
|
||||
// Write the response
|
||||
http::async_write(
|
||||
self_.derived().stream(),
|
||||
*sp,
|
||||
boost::beast::bind_front_handler(
|
||||
&HttpBase::onWrite, self_.derived().shared_from_this(), sp->need_eof()));
|
||||
}
|
||||
};
|
||||
|
||||
std::shared_ptr<void> res_;
|
||||
SendLambda sender_;
|
||||
|
||||
protected:
|
||||
boost::beast::flat_buffer buffer_;
|
||||
http::request<http::string_body> req_;
|
||||
std::reference_wrapper<web::DOSGuard> dosGuard_;
|
||||
std::shared_ptr<Handler> const handler_;
|
||||
util::Logger log_{"WebServer"};
|
||||
util::Logger perfLog_{"Performance"};
|
||||
|
||||
inline void
|
||||
httpFail(boost::beast::error_code ec, char const* what)
|
||||
{
|
||||
// ssl::error::stream_truncated, also known as an SSL "short read",
|
||||
// indicates the peer closed the connection without performing the
|
||||
// required closing handshake (for example, Google does this to
|
||||
// improve performance). Generally this can be a security issue,
|
||||
// but if your communication protocol is self-terminated (as
|
||||
// it is with both HTTP and WebSocket) then you may simply
|
||||
// ignore the lack of close_notify.
|
||||
//
|
||||
// https://github.com/boostorg/beast/issues/38
|
||||
//
|
||||
// https://security.stackexchange.com/questions/91435/how-to-handle-a-malicious-ssl-tls-shutdown
|
||||
//
|
||||
// When a short read would cut off the end of an HTTP message,
|
||||
// Beast returns the error boost::beast::http::error::partial_message.
|
||||
// Therefore, if we see a short read here, it has occurred
|
||||
// after the message has been completed, so it is safe to ignore it.
|
||||
|
||||
if (ec == boost::asio::ssl::error::stream_truncated)
|
||||
return;
|
||||
|
||||
if (!ec_ && ec != boost::asio::error::operation_aborted)
|
||||
{
|
||||
ec_ = ec;
|
||||
perfLog_.info() << tag() << ": " << what << ": " << ec.message();
|
||||
boost::beast::get_lowest_layer(derived().stream()).socket().close(ec);
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
HttpBase(
|
||||
std::string const& ip,
|
||||
std::reference_wrapper<util::TagDecoratorFactory const> tagFactory,
|
||||
std::reference_wrapper<web::DOSGuard> dosGuard,
|
||||
std::shared_ptr<Handler> const& handler,
|
||||
boost::beast::flat_buffer buffer)
|
||||
: ConnectionBase(tagFactory, ip)
|
||||
, sender_(*this)
|
||||
, buffer_(std::move(buffer))
|
||||
, dosGuard_(dosGuard)
|
||||
, handler_(handler)
|
||||
{
|
||||
perfLog_.debug() << tag() << "http session created";
|
||||
dosGuard_.get().increment(ip);
|
||||
}
|
||||
|
||||
virtual ~HttpBase()
|
||||
{
|
||||
perfLog_.debug() << tag() << "http session closed";
|
||||
if (not upgraded)
|
||||
dosGuard_.get().decrement(this->clientIp);
|
||||
}
|
||||
|
||||
void
|
||||
doRead()
|
||||
{
|
||||
if (dead())
|
||||
return;
|
||||
|
||||
// Make the request empty before reading, otherwise the operation behavior is undefined.
|
||||
req_ = {};
|
||||
|
||||
// Set the timeout.
|
||||
boost::beast::get_lowest_layer(derived().stream()).expires_after(std::chrono::seconds(30));
|
||||
|
||||
http::async_read(
|
||||
derived().stream(),
|
||||
buffer_,
|
||||
req_,
|
||||
boost::beast::bind_front_handler(&HttpBase::onRead, derived().shared_from_this()));
|
||||
}
|
||||
|
||||
void
|
||||
onRead(boost::beast::error_code ec, [[maybe_unused]] std::size_t bytes_transferred)
|
||||
{
|
||||
if (ec == http::error::end_of_stream)
|
||||
return derived().doClose();
|
||||
|
||||
if (ec)
|
||||
return httpFail(ec, "read");
|
||||
|
||||
if (boost::beast::websocket::is_upgrade(req_))
|
||||
{
|
||||
upgraded = true;
|
||||
// Disable the timeout.
|
||||
// The websocket::stream uses its own timeout settings.
|
||||
boost::beast::get_lowest_layer(derived().stream()).expires_never();
|
||||
return derived().upgrade();
|
||||
}
|
||||
|
||||
if (req_.method() != http::verb::post)
|
||||
{
|
||||
return sender_(httpResponse(http::status::bad_request, "text/html", "Expected a POST request"));
|
||||
}
|
||||
|
||||
// to avoid overwhelm work queue, the request limit check should be
|
||||
// before posting to queue the web socket creation will be guarded via
|
||||
// connection limit
|
||||
if (!dosGuard_.get().request(clientIp))
|
||||
{
|
||||
// TODO: this looks like it could be useful to count too in the future
|
||||
return sender_(httpResponse(
|
||||
http::status::service_unavailable,
|
||||
"text/plain",
|
||||
boost::json::serialize(RPC::makeError(RPC::RippledError::rpcSLOW_DOWN))));
|
||||
}
|
||||
|
||||
log_.info() << tag() << "Received request from ip = " << clientIp << " - posting to WorkQueue";
|
||||
|
||||
try
|
||||
{
|
||||
(*handler_)(req_.body(), derived().shared_from_this());
|
||||
}
|
||||
catch (std::exception const&)
|
||||
{
|
||||
return sender_(httpResponse(
|
||||
http::status::internal_server_error,
|
||||
"application/json",
|
||||
boost::json::serialize(RPC::makeError(RPC::RippledError::rpcINTERNAL))));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Send a response to the client
|
||||
* The message length will be added to the DOSGuard, if the limit is reached, a warning will be added to the
|
||||
* response
|
||||
*/
|
||||
void
|
||||
send(std::string&& msg, http::status status = http::status::ok) override
|
||||
{
|
||||
if (!dosGuard_.get().add(clientIp, msg.size()))
|
||||
{
|
||||
auto jsonResponse = boost::json::parse(msg).as_object();
|
||||
jsonResponse["warning"] = "load";
|
||||
if (jsonResponse.contains("warnings") && jsonResponse["warnings"].is_array())
|
||||
jsonResponse["warnings"].as_array().push_back(RPC::makeWarning(RPC::warnRPC_RATE_LIMIT));
|
||||
else
|
||||
jsonResponse["warnings"] = boost::json::array{RPC::makeWarning(RPC::warnRPC_RATE_LIMIT)};
|
||||
|
||||
// Reserialize when we need to include this warning
|
||||
msg = boost::json::serialize(jsonResponse);
|
||||
}
|
||||
sender_(httpResponse(status, "application/json", std::move(msg)));
|
||||
}
|
||||
|
||||
void
|
||||
onWrite(bool close, boost::beast::error_code ec, std::size_t bytes_transferred)
|
||||
{
|
||||
boost::ignore_unused(bytes_transferred);
|
||||
|
||||
if (ec)
|
||||
return httpFail(ec, "write");
|
||||
|
||||
// This means we should close the connection, usually because
|
||||
// the response indicated the "Connection: close" semantic.
|
||||
if (close)
|
||||
return derived().doClose();
|
||||
|
||||
res_ = nullptr;
|
||||
doRead();
|
||||
}
|
||||
|
||||
private:
|
||||
http::response<http::string_body>
|
||||
httpResponse(http::status status, std::string content_type, std::string message) const
|
||||
{
|
||||
http::response<http::string_body> res{status, req_.version()};
|
||||
res.set(http::field::server, "clio-server-" + Build::getClioVersionString());
|
||||
res.set(http::field::content_type, content_type);
|
||||
res.keep_alive(req_.keep_alive());
|
||||
res.body() = std::move(message);
|
||||
res.prepare_payload();
|
||||
return res;
|
||||
};
|
||||
};
|
||||
|
||||
} // namespace web::detail
|
||||
93
src/web/impl/IntervalSweepHandler.h
Normal file
93
src/web/impl/IntervalSweepHandler.h
Normal file
@@ -0,0 +1,93 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2022, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <boost/asio.hpp>
|
||||
#include <boost/iterator/transform_iterator.hpp>
|
||||
#include <boost/system/error_code.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <ctime>
|
||||
|
||||
namespace web::detail {
|
||||
|
||||
/**
|
||||
* @brief Sweep handler using a steady_timer and boost::asio::io_context.
|
||||
*/
|
||||
class IntervalSweepHandler
|
||||
{
|
||||
std::chrono::milliseconds sweepInterval_;
|
||||
std::reference_wrapper<boost::asio::io_context> ctx_;
|
||||
boost::asio::steady_timer timer_;
|
||||
|
||||
web::BaseDOSGuard* dosGuard_ = nullptr;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Construct a new interval-based sweep handler
|
||||
*
|
||||
* @param config Clio config
|
||||
* @param ctx The boost::asio::io_context
|
||||
*/
|
||||
IntervalSweepHandler(util::Config const& config, boost::asio::io_context& ctx)
|
||||
: sweepInterval_{std::max(1u, static_cast<uint32_t>(config.valueOr("dos_guard.sweep_interval", 1.0) * 1000.0))}
|
||||
, ctx_{std::ref(ctx)}
|
||||
, timer_{ctx.get_executor()}
|
||||
{
|
||||
}
|
||||
|
||||
~IntervalSweepHandler()
|
||||
{
|
||||
timer_.cancel();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief This setup member function is called by @ref BasicDOSGuard during
|
||||
* its initialization.
|
||||
*
|
||||
* @param guard Pointer to the dos guard
|
||||
*/
|
||||
void
|
||||
setup(web::BaseDOSGuard* guard)
|
||||
{
|
||||
assert(dosGuard_ == nullptr);
|
||||
dosGuard_ = guard;
|
||||
assert(dosGuard_ != nullptr);
|
||||
|
||||
createTimer();
|
||||
}
|
||||
|
||||
private:
|
||||
void
|
||||
createTimer()
|
||||
{
|
||||
timer_.expires_after(sweepInterval_);
|
||||
timer_.async_wait([this](boost::system::error_code const& error) {
|
||||
if (error == boost::asio::error::operation_aborted)
|
||||
return;
|
||||
|
||||
dosGuard_->clear();
|
||||
boost::asio::post(ctx_.get().get_executor(), [this] { createTimer(); });
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace web::detail
|
||||
264
src/web/impl/WsBase.h
Normal file
264
src/web/impl/WsBase.h
Normal file
@@ -0,0 +1,264 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2023, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <rpc/common/Types.h>
|
||||
#include <util/log/Logger.h>
|
||||
#include <web/DOSGuard.h>
|
||||
#include <web/interface/Concepts.h>
|
||||
#include <web/interface/ConnectionBase.h>
|
||||
|
||||
#include <boost/beast/core.hpp>
|
||||
#include <boost/beast/websocket.hpp>
|
||||
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
|
||||
namespace web::detail {
|
||||
|
||||
/**
|
||||
* @brief Web socket implementation. This class is the base class of the web socket session, it will handle the read and
|
||||
* write operations.
|
||||
* The write operation is via a queue, each write operation of this session will be sent in order.
|
||||
* The write operation also supports shared_ptr of string, so the caller can keep the string alive until it is sent. It
|
||||
* is useful when we have multiple sessions sending the same content
|
||||
* @tparam Derived The derived class
|
||||
* @tparam Handler The handler type, will be called when a request is received.
|
||||
*/
|
||||
template <template <class> class Derived, ServerHandler Handler>
|
||||
class WsBase : public ConnectionBase, public std::enable_shared_from_this<WsBase<Derived, Handler>>
|
||||
{
|
||||
using std::enable_shared_from_this<WsBase<Derived, Handler>>::shared_from_this;
|
||||
|
||||
boost::beast::flat_buffer buffer_;
|
||||
std::reference_wrapper<web::DOSGuard> dosGuard_;
|
||||
bool sending_ = false;
|
||||
std::queue<std::shared_ptr<std::string>> messages_;
|
||||
std::shared_ptr<Handler> const handler_;
|
||||
|
||||
protected:
|
||||
util::Logger log_{"WebServer"};
|
||||
util::Logger perfLog_{"Performance"};
|
||||
|
||||
void
|
||||
wsFail(boost::beast::error_code ec, char const* what)
|
||||
{
|
||||
if (!ec_ && ec != boost::asio::error::operation_aborted)
|
||||
{
|
||||
ec_ = ec;
|
||||
perfLog_.info() << tag() << ": " << what << ": " << ec.message();
|
||||
boost::beast::get_lowest_layer(derived().ws()).socket().close(ec);
|
||||
(*handler_)(ec, derived().shared_from_this());
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
explicit WsBase(
|
||||
std::string ip,
|
||||
std::reference_wrapper<util::TagDecoratorFactory const> tagFactory,
|
||||
std::reference_wrapper<web::DOSGuard> dosGuard,
|
||||
std::shared_ptr<Handler> const& handler,
|
||||
boost::beast::flat_buffer&& buffer)
|
||||
: ConnectionBase(tagFactory, ip), buffer_(std::move(buffer)), dosGuard_(dosGuard), handler_(handler)
|
||||
{
|
||||
upgraded = true;
|
||||
perfLog_.debug() << tag() << "session created";
|
||||
}
|
||||
|
||||
virtual ~WsBase()
|
||||
{
|
||||
perfLog_.debug() << tag() << "session closed";
|
||||
dosGuard_.get().decrement(clientIp);
|
||||
}
|
||||
|
||||
Derived<Handler>&
|
||||
derived()
|
||||
{
|
||||
return static_cast<Derived<Handler>&>(*this);
|
||||
}
|
||||
|
||||
void
|
||||
doWrite()
|
||||
{
|
||||
sending_ = true;
|
||||
derived().ws().async_write(
|
||||
boost::asio::buffer(messages_.front()->data(), messages_.front()->size()),
|
||||
boost::beast::bind_front_handler(&WsBase::onWrite, derived().shared_from_this()));
|
||||
}
|
||||
|
||||
void
|
||||
onWrite(boost::system::error_code ec, std::size_t)
|
||||
{
|
||||
if (ec)
|
||||
{
|
||||
wsFail(ec, "Failed to write");
|
||||
}
|
||||
else
|
||||
{
|
||||
messages_.pop();
|
||||
sending_ = false;
|
||||
maybeSendNext();
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
maybeSendNext()
|
||||
{
|
||||
if (ec_ || sending_ || messages_.empty())
|
||||
return;
|
||||
|
||||
doWrite();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Send a message to the client
|
||||
* @param msg The message to send, it will keep the string alive until it is sent. It is useful when we have
|
||||
* multiple session sending the same content.
|
||||
* Be aware that the message length will not be added to the DOSGuard from this function.
|
||||
*/
|
||||
void
|
||||
send(std::shared_ptr<std::string> msg) override
|
||||
{
|
||||
boost::asio::dispatch(
|
||||
derived().ws().get_executor(), [this, self = derived().shared_from_this(), msg = std::move(msg)]() {
|
||||
messages_.push(std::move(msg));
|
||||
maybeSendNext();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Send a message to the client
|
||||
* @param msg The message to send
|
||||
* Send this message to the client. The message length will be added to the DOSGuard
|
||||
* If the DOSGuard is triggered, the message will be modified to include a warning
|
||||
*/
|
||||
void
|
||||
send(std::string&& msg, http::status _ = http::status::ok) override
|
||||
{
|
||||
if (!dosGuard_.get().add(clientIp, msg.size()))
|
||||
{
|
||||
auto jsonResponse = boost::json::parse(msg).as_object();
|
||||
jsonResponse["warning"] = "load";
|
||||
|
||||
if (jsonResponse.contains("warnings") && jsonResponse["warnings"].is_array())
|
||||
jsonResponse["warnings"].as_array().push_back(RPC::makeWarning(RPC::warnRPC_RATE_LIMIT));
|
||||
else
|
||||
jsonResponse["warnings"] = boost::json::array{RPC::makeWarning(RPC::warnRPC_RATE_LIMIT)};
|
||||
|
||||
// Reserialize when we need to include this warning
|
||||
msg = boost::json::serialize(jsonResponse);
|
||||
}
|
||||
auto sharedMsg = std::make_shared<std::string>(std::move(msg));
|
||||
send(std::move(sharedMsg));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Accept the session asynchroniously
|
||||
*/
|
||||
void
|
||||
run(http::request<http::string_body> req)
|
||||
{
|
||||
using namespace boost::beast;
|
||||
|
||||
derived().ws().set_option(websocket::stream_base::timeout::suggested(role_type::server));
|
||||
|
||||
// Set a decorator to change the Server of the handshake
|
||||
derived().ws().set_option(websocket::stream_base::decorator([](websocket::response_type& res) {
|
||||
res.set(http::field::server, std::string(BOOST_BEAST_VERSION_STRING) + " websocket-server-async");
|
||||
}));
|
||||
|
||||
derived().ws().async_accept(req, bind_front_handler(&WsBase::onAccept, this->shared_from_this()));
|
||||
}
|
||||
|
||||
void
|
||||
onAccept(boost::beast::error_code ec)
|
||||
{
|
||||
if (ec)
|
||||
return wsFail(ec, "accept");
|
||||
|
||||
perfLog_.info() << tag() << "accepting new connection";
|
||||
|
||||
doRead();
|
||||
}
|
||||
|
||||
void
|
||||
doRead()
|
||||
{
|
||||
if (dead())
|
||||
return;
|
||||
|
||||
// Clear the buffer
|
||||
buffer_.consume(buffer_.size());
|
||||
|
||||
derived().ws().async_read(buffer_, boost::beast::bind_front_handler(&WsBase::onRead, this->shared_from_this()));
|
||||
}
|
||||
|
||||
void
|
||||
onRead(boost::beast::error_code ec, std::size_t bytes_transferred)
|
||||
{
|
||||
boost::ignore_unused(bytes_transferred);
|
||||
|
||||
if (ec)
|
||||
return wsFail(ec, "read");
|
||||
|
||||
perfLog_.info() << tag() << "Received request from ip = " << this->clientIp;
|
||||
|
||||
auto sendError = [this](auto error, std::string&& requestStr) {
|
||||
auto e = RPC::makeError(error);
|
||||
|
||||
try
|
||||
{
|
||||
auto request = boost::json::parse(requestStr);
|
||||
if (request.is_object() && request.as_object().contains("id"))
|
||||
e["id"] = request.as_object().at("id");
|
||||
e["request"] = std::move(request);
|
||||
}
|
||||
catch (std::exception const&)
|
||||
{
|
||||
e["request"] = std::move(requestStr);
|
||||
}
|
||||
|
||||
this->send(std::make_shared<std::string>(boost::json::serialize(e)));
|
||||
};
|
||||
|
||||
std::string requestStr{static_cast<char const*>(buffer_.data().data()), buffer_.size()};
|
||||
|
||||
// dosGuard served request++ and check ip address
|
||||
if (!dosGuard_.get().request(clientIp))
|
||||
{
|
||||
// TODO: could be useful to count in counters in the future too
|
||||
sendError(RPC::RippledError::rpcSLOW_DOWN, std::move(requestStr));
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
(*handler_)(requestStr, shared_from_this());
|
||||
}
|
||||
catch (std::exception const&)
|
||||
{
|
||||
sendError(RPC::RippledError::rpcINTERNAL, std::move(requestStr));
|
||||
}
|
||||
}
|
||||
|
||||
doRead();
|
||||
}
|
||||
};
|
||||
} // namespace web::detail
|
||||
Reference in New Issue
Block a user