Clio  develop
The XRP Ledger API server.
Loading...
Searching...
No Matches
RPCServerHandler.hpp
1#pragma once
2
3#include "data/BackendInterface.hpp"
4#include "etl/ETLServiceInterface.hpp"
5#include "rpc/Errors.hpp"
6#include "rpc/Factories.hpp"
7#include "rpc/JS.hpp"
8#include "rpc/RPCHelpers.hpp"
9#include "rpc/common/impl/APIVersionParser.hpp"
10#include "util/JsonUtils.hpp"
11#include "util/Profiler.hpp"
12#include "util/Taggable.hpp"
13#include "util/config/ConfigDefinition.hpp"
14#include "util/log/Logger.hpp"
15#include "web/dosguard/DOSGuardInterface.hpp"
16#include "web/impl/ErrorHandling.hpp"
17#include "web/interface/ConnectionBase.hpp"
18
19#include <boost/asio/spawn.hpp>
20#include <boost/beast/core/error.hpp>
21#include <boost/json/array.hpp>
22#include <boost/json/object.hpp>
23#include <boost/json/parse.hpp>
24#include <boost/json/serialize.hpp>
25#include <boost/system/system_error.hpp>
26#include <xrpl/protocol/jss.h>
27
28#include <chrono>
29#include <exception>
30#include <functional>
31#include <memory>
32#include <ratio>
33#include <stdexcept>
34#include <string>
35#include <utility>
36
37namespace web {
38
44template <typename RPCEngineType>
46 std::shared_ptr<BackendInterface const> const backend_;
47 std::shared_ptr<RPCEngineType> const rpcEngine_;
48 std::shared_ptr<etl::ETLServiceInterface const> const etl_;
49 util::TagDecoratorFactory const tagFactory_;
50 rpc::impl::ProductionAPIVersionParser apiVersionParser_; // can be injected if needed
51 std::reference_wrapper<web::dosguard::DOSGuardInterface> dosguard_;
52
53 util::Logger log_{"RPC"};
54 util::Logger perfLog_{"Performance"};
55
56public:
68 std::shared_ptr<BackendInterface const> const& backend,
69 std::shared_ptr<RPCEngineType> const& rpcEngine,
70 std::shared_ptr<etl::ETLServiceInterface const> const& etl,
72 )
73 : backend_(backend)
74 , rpcEngine_(rpcEngine)
75 , etl_(etl)
76 , tagFactory_(config)
77 , apiVersionParser_(config.getObject("api_version"))
78 , dosguard_(dosguard)
79 {
80 }
81
88 void
89 operator()(std::string const& request, std::shared_ptr<web::ConnectionBase> const& connection)
90 {
91 if (not dosguard_.get().isOk(connection->clientIp())) {
92 connection->sendSlowDown(request);
93 return;
94 }
95
96 try {
97 auto req = boost::json::parse(request).as_object();
98 LOG(perfLog_.debug()) << connection->tag() << "Adding to work queue";
99
100 if (not connection->upgraded and shouldReplaceParams(req))
101 req[JS(params)] = boost::json::array({boost::json::object{}});
102
103 if (not dosguard_.get().request(connection->clientIp(), req)) {
104 connection->sendSlowDown(request);
105 return;
106 }
107
108 if (!rpcEngine_->post(
109 [this, request = std::move(req), connection](
110 boost::asio::yield_context yield
111 ) mutable { handleRequest(yield, std::move(request), connection); },
112 connection->clientIp()
113 )) {
114 rpcEngine_->notifyTooBusy();
115 web::impl::ErrorHelper(connection).sendTooBusyError();
116 }
117 } catch (boost::system::system_error const& ex) {
118 // system_error thrown when json parsing failed
119 rpcEngine_->notifyBadSyntax();
120 web::impl::ErrorHelper(connection).sendJsonParsingError();
121 LOG(log_.warn()) << "Error parsing JSON: " << ex.what() << ". For request: " << request;
122 } catch (std::invalid_argument const& ex) {
123 // thrown when json parses something that is not an object at top level
124 rpcEngine_->notifyBadSyntax();
125 LOG(log_.warn()) << "Invalid argument error: " << ex.what()
126 << ". For request: " << request;
127 web::impl::ErrorHelper(connection).sendJsonParsingError();
128 } catch (std::exception const& ex) {
129 LOG(perfLog_.error()) << connection->tag() << "Caught exception: " << ex.what();
130 rpcEngine_->notifyInternalError();
131 throw;
132 }
133 }
134
135private:
136 void
137 handleRequest(
138 boost::asio::yield_context yield,
139 boost::json::object&& request,
140 std::shared_ptr<web::ConnectionBase> const& connection
141 )
142 {
143 LOG(log_.info()) << connection->tag() << (connection->upgraded ? "ws" : "http")
144 << " received request from work queue: " << util::removeSecret(request)
145 << " ip = " << connection->clientIp();
146
147 try {
148 auto const range = backend_->fetchLedgerRange();
149 if (!range) {
150 // for error that happened before the handler, we don't attach any warnings
151 rpcEngine_->notifyNotReady();
152 web::impl::ErrorHelper(connection, std::move(request)).sendNotReadyError();
153
154 return;
155 }
156
157 auto const context = [&] {
158 if (connection->upgraded) {
159 return rpc::makeWsContext(
160 yield,
161 request,
162 connection->makeSubscriptionContext(tagFactory_),
163 tagFactory_.with(connection->tag()),
164 *range,
165 connection->clientIp(),
166 std::cref(apiVersionParser_),
167 connection->isAdmin()
168 );
169 }
171 yield,
172 request,
173 tagFactory_.with(connection->tag()),
174 *range,
175 connection->clientIp(),
176 std::cref(apiVersionParser_),
177 connection->isAdmin()
178 );
179 }();
180
181 if (!context) {
182 auto const err = context.error();
183 LOG(perfLog_.warn())
184 << connection->tag() << "Could not create Web context: " << err;
185 LOG(log_.warn()) << connection->tag() << "Could not create Web context: " << err;
186
187 // we count all those as BadSyntax - as the WS path would.
188 // Although over HTTP these will yield a 400 status with a plain text response (for
189 // most).
190 rpcEngine_->notifyBadSyntax();
191 web::impl::ErrorHelper(connection, std::move(request)).sendError(err);
192
193 return;
194 }
195
196 auto [result, timeDiff] =
197 util::timed([&]() { return rpcEngine_->buildResponse(*context); });
198
199 auto const us = std::chrono::duration<int, std::milli>(timeDiff);
200 rpc::logDuration(request, context->tag(), us);
201
202 boost::json::object response;
203
204 if (!result.response.has_value()) {
205 // note: error statuses are counted/notified in buildResponse itself
206 response = web::impl::ErrorHelper(connection, request)
207 .composeError(result.response.error());
208 auto const responseStr = boost::json::serialize(response);
209
210 LOG(perfLog_.debug()) << context->tag() << "Encountered error: " << responseStr;
211 LOG(log_.debug()) << context->tag() << "Encountered error: " << responseStr;
212 } else {
213 auto& json = result.response.value();
214 auto const isForwarded = json.contains("forwarded") &&
215 json.at("forwarded").is_bool() && json.at("forwarded").as_bool();
216
217 // This can still technically be an error. Clio counts forwarded requests
218 // as successful.
219 rpcEngine_->notifyComplete(*context, us, isForwarded);
220
221 if (isForwarded)
222 json.erase("forwarded");
223
224 // if the result is forwarded - just use it as is
225 // if forwarded request has error, for http, error should be in "result"; for ws,
226 // error should be at top
227 if (isForwarded && (json.contains(JS(result)) || connection->upgraded)) {
228 for (auto const& [k, v] : json)
229 response.insert_or_assign(k, v);
230 } else {
231 response[JS(result)] = json;
232 }
233
234 if (isForwarded)
235 response["forwarded"] = true;
236
237 // for ws there is an additional field "status" in the response,
238 // otherwise the "status" is in the "result" field
239 if (connection->upgraded) {
240 auto const appendFieldIfExist = [&](auto const& field) {
241 if (request.contains(field) and not request.at(field).is_null())
242 response[field] = request.at(field);
243 };
244
245 appendFieldIfExist(JS(id));
246 appendFieldIfExist(JS(api_version));
247
248 if (!response.contains(JS(error)))
249 response[JS(status)] = JS(success);
250
251 response[JS(type)] = JS(response);
252 } else {
253 if (response.contains(JS(result)) &&
254 !response[JS(result)].as_object().contains(JS(error)))
255 response[JS(result)].as_object()[JS(status)] = JS(success);
256 }
257 }
258
259 boost::json::array warnings = std::move(result.warnings);
260 warnings.emplace_back(rpc::makeWarning(rpc::WarnRpcClio));
261
262 if (etl_->lastCloseAgeSeconds() >= 60)
263 warnings.emplace_back(rpc::makeWarning(rpc::WarnRpcOutdated));
264
265 response["warnings"] = warnings;
266 connection->send(boost::json::serialize(response));
267 } catch (std::exception const& ex) {
268 // note: while we are catching this in buildResponse too, this is here to make sure
269 // that any other code that may throw is outside of buildResponse is also worked around.
270 LOG(perfLog_.error()) << connection->tag() << "Caught exception: " << ex.what();
271 LOG(log_.error()) << connection->tag() << "Caught exception: " << ex.what();
272
273 rpcEngine_->notifyInternalError();
274 web::impl::ErrorHelper(connection, std::move(request)).sendInternalError();
275
276 return;
277 }
278 }
279
280 bool
281 shouldReplaceParams(boost::json::object const& req) const
282 {
283 auto const hasParams = req.contains(JS(params));
284 auto const paramsIsArray = hasParams and req.at(JS(params)).is_array();
285 auto const paramsIsEmptyString =
286 hasParams and req.at(JS(params)).is_string() and req.at(JS(params)).as_string().empty();
287 auto const paramsIsEmptyObject =
288 hasParams and req.at(JS(params)).is_object() and req.at(JS(params)).as_object().empty();
289 auto const paramsIsNull = hasParams and req.at(JS(params)).is_null();
290 auto const arrayIsEmpty = paramsIsArray and req.at(JS(params)).as_array().empty();
291 auto const arrayIsNotEmpty = paramsIsArray and not req.at(JS(params)).as_array().empty();
292 auto const firstArgIsNull =
293 arrayIsNotEmpty and req.at(JS(params)).as_array().at(0).is_null();
294 auto const firstArgIsEmptyString = arrayIsNotEmpty and
295 req.at(JS(params)).as_array().at(0).is_string() and
296 req.at(JS(params)).as_array().at(0).as_string().empty();
297
298 // Note: all this compatibility dance is to match `rippled` as close as possible
299 return not hasParams or paramsIsEmptyString or paramsIsNull or paramsIsEmptyObject or
300 arrayIsEmpty or firstArgIsEmptyString or firstArgIsNull;
301 }
302};
303
304} // namespace web
Definition APIVersionParser.hpp:15
A simple thread-safe logger for the channel specified in the constructor.
Definition Logger.hpp:77
Pump info(SourceLocationType const &loc=CURRENT_SRC_LOCATION) const
Interface for logging at Severity::NFO severity.
Definition Logger.cpp:488
A factory for TagDecorator instantiation.
Definition Taggable.hpp:165
TagDecoratorFactory with(ParentType parent) const noexcept
Creates a new tag decorator factory with a bound parent tag decorator.
Definition Taggable.cpp:47
All the config data will be stored and extracted from this class.
Definition ConfigDefinition.hpp:31
RPCServerHandler(util::config::ClioConfigDefinition const &config, std::shared_ptr< BackendInterface const > const &backend, std::shared_ptr< RPCEngineType > const &rpcEngine, std::shared_ptr< etl::ETLServiceInterface const > const &etl, web::dosguard::DOSGuardInterface &dosguard)
Create a new server handler.
Definition RPCServerHandler.hpp:66
void operator()(std::string const &request, std::shared_ptr< web::ConnectionBase > const &connection)
The callback when server receives a request.
Definition RPCServerHandler.hpp:89
The interface of a denial of service guard.
Definition DOSGuardInterface.hpp:27
A helper that attempts to match rippled reporting mode HTTP errors as close as possible.
Definition ErrorHandling.hpp:26
std::expected< web::Context, Status > makeWsContext(boost::asio::yield_context yc, boost::json::object const &request, web::SubscriptionContextPtr session, util::TagDecoratorFactory const &tagFactory, data::LedgerRange const &range, std::string const &clientIp, std::reference_wrapper< APIVersionParser const > apiVersionParser, bool isAdmin)
A factory function that creates a Websocket context.
Definition Factories.cpp:28
void logDuration(boost::json::object const &request, util::BaseTagDecorator const &tag, DurationType const &dur)
Log the duration of the request processing.
Definition RPCHelpers.hpp:756
boost::json::object makeWarning(WarningCode code)
Generate JSON from a rpc::WarningCode.
Definition Errors.cpp:84
std::expected< web::Context, Status > makeHttpContext(boost::asio::yield_context yc, boost::json::object const &request, util::TagDecoratorFactory const &tagFactory, data::LedgerRange const &range, std::string const &clientIp, std::reference_wrapper< APIVersionParser const > apiVersionParser, bool const isAdmin)
A factory function that creates a HTTP context.
Definition Factories.cpp:63
boost::json::object removeSecret(boost::json::object const &object)
Removes any detected secret information from a response JSON object.
Definition JsonUtils.hpp:55
auto timed(FnType &&func)
Profiler function to measure the time a function execution consumes.
Definition Profiler.hpp:21
This namespace implements the web server and related components.
Definition Types.hpp:24