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