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/log/Logger.hpp"
33#include "util/newconfig/ConfigDefinition.hpp"
34#include "web/impl/ErrorHandling.hpp"
35#include "web/interface/ConnectionBase.hpp"
36
37#include <boost/asio/spawn.hpp>
38#include <boost/beast/core/error.hpp>
39#include <boost/json/array.hpp>
40#include <boost/json/object.hpp>
41#include <boost/json/parse.hpp>
42#include <boost/json/serialize.hpp>
43#include <boost/system/system_error.hpp>
44#include <xrpl/protocol/jss.h>
45
46#include <chrono>
47#include <exception>
48#include <functional>
49#include <memory>
50#include <ratio>
51#include <stdexcept>
52#include <string>
53#include <utility>
54
55namespace web {
56
62template <typename RPCEngineType>
64 std::shared_ptr<BackendInterface const> const backend_;
65 std::shared_ptr<RPCEngineType> const rpcEngine_;
66 std::shared_ptr<etlng::ETLServiceInterface const> const etl_;
67 util::TagDecoratorFactory const tagFactory_;
68 rpc::impl::ProductionAPIVersionParser apiVersionParser_; // can be injected if needed
69
70 util::Logger log_{"RPC"};
71 util::Logger perfLog_{"Performance"};
72
73public:
84 std::shared_ptr<BackendInterface const> const& backend,
85 std::shared_ptr<RPCEngineType> const& rpcEngine,
86 std::shared_ptr<etlng::ETLServiceInterface const> const& etl
87 )
88 : backend_(backend)
89 , rpcEngine_(rpcEngine)
90 , etl_(etl)
91 , tagFactory_(config)
92 , apiVersionParser_(config.getObject("api_version"))
93 {
94 }
95
102 void
103 operator()(std::string const& request, std::shared_ptr<web::ConnectionBase> const& connection)
104 {
105 try {
106 auto req = boost::json::parse(request).as_object();
107 LOG(perfLog_.debug()) << connection->tag() << "Adding to work queue";
108
109 if (not connection->upgraded and shouldReplaceParams(req))
110 req[JS(params)] = boost::json::array({boost::json::object{}});
111
112 if (!rpcEngine_->post(
113 [this, request = std::move(req), connection](boost::asio::yield_context yield) mutable {
114 handleRequest(yield, std::move(request), connection);
115 },
116 connection->clientIp
117 )) {
118 rpcEngine_->notifyTooBusy();
119 web::impl::ErrorHelper(connection).sendTooBusyError();
120 }
121 } catch (boost::system::system_error const& ex) {
122 // system_error thrown when json parsing failed
123 rpcEngine_->notifyBadSyntax();
124 web::impl::ErrorHelper(connection).sendJsonParsingError();
125 LOG(log_.warn()) << "Error parsing JSON: " << ex.what() << ". For request: " << request;
126 } catch (std::invalid_argument const& ex) {
127 // thrown when json parses something that is not an object at top level
128 rpcEngine_->notifyBadSyntax();
129 LOG(log_.warn()) << "Invalid argument error: " << ex.what() << ". For request: " << request;
130 web::impl::ErrorHelper(connection).sendJsonParsingError();
131 } catch (std::exception const& ex) {
132 LOG(perfLog_.error()) << connection->tag() << "Caught exception: " << ex.what();
133 rpcEngine_->notifyInternalError();
134 throw;
135 }
136 }
137
138private:
139 void
140 handleRequest(
141 boost::asio::yield_context yield,
142 boost::json::object&& request,
143 std::shared_ptr<web::ConnectionBase> const& connection
144 )
145 {
146 LOG(log_.info()) << connection->tag() << (connection->upgraded ? "ws" : "http")
147 << " received request from work queue: " << util::removeSecret(request)
148 << " ip = " << connection->clientIp;
149
150 try {
151 auto const range = backend_->fetchLedgerRange();
152 if (!range) {
153 // for error that happened before the handler, we don't attach any warnings
154 rpcEngine_->notifyNotReady();
155 web::impl::ErrorHelper(connection, std::move(request)).sendNotReadyError();
156
157 return;
158 }
159
160 auto const context = [&] {
161 if (connection->upgraded) {
162 return rpc::makeWsContext(
163 yield,
164 request,
165 connection->makeSubscriptionContext(tagFactory_),
166 tagFactory_.with(connection->tag()),
167 *range,
168 connection->clientIp,
169 std::cref(apiVersionParser_),
170 connection->isAdmin()
171 );
172 }
174 yield,
175 request,
176 tagFactory_.with(connection->tag()),
177 *range,
178 connection->clientIp,
179 std::cref(apiVersionParser_),
180 connection->isAdmin()
181 );
182 }();
183
184 if (!context) {
185 auto const err = context.error();
186 LOG(perfLog_.warn()) << connection->tag() << "Could not create Web context: " << err;
187 LOG(log_.warn()) << connection->tag() << "Could not create Web context: " << err;
188
189 // we count all those as BadSyntax - as the WS path would.
190 // Although over HTTP these will yield a 400 status with a plain text response (for most).
191 rpcEngine_->notifyBadSyntax();
192 web::impl::ErrorHelper(connection, std::move(request)).sendError(err);
193
194 return;
195 }
196
197 auto [result, timeDiff] = 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 (auto const status = std::get_if<rpc::Status>(&result.response)) {
205 // note: error statuses are counted/notified in buildResponse itself
206 response = web::impl::ErrorHelper(connection, request).composeError(*status);
207 auto const responseStr = boost::json::serialize(response);
208
209 LOG(perfLog_.debug()) << context->tag() << "Encountered error: " << responseStr;
210 LOG(log_.debug()) << context->tag() << "Encountered error: " << responseStr;
211 } else {
212 // This can still technically be an error. Clio counts forwarded requests as successful.
213 rpcEngine_->notifyComplete(context->method, us);
214
215 auto& json = std::get<boost::json::object>(result.response);
216 auto const isForwarded =
217 json.contains("forwarded") && json.at("forwarded").is_bool() && json.at("forwarded").as_bool();
218
219 if (isForwarded)
220 json.erase("forwarded");
221
222 // if the result is forwarded - just use it as is
223 // if forwarded request has error, for http, error should be in "result"; for ws, error should
224 // be at top
225 if (isForwarded && (json.contains(JS(result)) || connection->upgraded)) {
226 for (auto const& [k, v] : json)
227 response.insert_or_assign(k, v);
228 } else {
229 response[JS(result)] = json;
230 }
231
232 if (isForwarded)
233 response["forwarded"] = true;
234
235 // for ws there is an additional field "status" in the response,
236 // otherwise the "status" is in the "result" field
237 if (connection->upgraded) {
238 auto const appendFieldIfExist = [&](auto const& field) {
239 if (request.contains(field) and not request.at(field).is_null())
240 response[field] = request.at(field);
241 };
242
243 appendFieldIfExist(JS(id));
244 appendFieldIfExist(JS(api_version));
245
246 if (!response.contains(JS(error)))
247 response[JS(status)] = JS(success);
248
249 response[JS(type)] = JS(response);
250 } else {
251 if (response.contains(JS(result)) && !response[JS(result)].as_object().contains(JS(error)))
252 response[JS(result)].as_object()[JS(status)] = JS(success);
253 }
254 }
255
256 boost::json::array warnings = std::move(result.warnings);
257 warnings.emplace_back(rpc::makeWarning(rpc::WarnRpcClio));
258
259 if (etl_->lastCloseAgeSeconds() >= 60)
260 warnings.emplace_back(rpc::makeWarning(rpc::WarnRpcOutdated));
261
262 response["warnings"] = warnings;
263 connection->send(boost::json::serialize(response));
264 } catch (std::exception const& ex) {
265 // note: while we are catching this in buildResponse too, this is here to make sure
266 // that any other code that may throw is outside of buildResponse is also worked around.
267 LOG(perfLog_.error()) << connection->tag() << "Caught exception: " << ex.what();
268 LOG(log_.error()) << connection->tag() << "Caught exception: " << ex.what();
269
270 rpcEngine_->notifyInternalError();
271 web::impl::ErrorHelper(connection, std::move(request)).sendInternalError();
272
273 return;
274 }
275 }
276
277 bool
278 shouldReplaceParams(boost::json::object const& req) const
279 {
280 auto const hasParams = req.contains(JS(params));
281 auto const paramsIsArray = hasParams and req.at(JS(params)).is_array();
282 auto const paramsIsEmptyString =
283 hasParams and req.at(JS(params)).is_string() and req.at(JS(params)).as_string().empty();
284 auto const paramsIsEmptyObject =
285 hasParams and req.at(JS(params)).is_object() and req.at(JS(params)).as_object().empty();
286 auto const paramsIsNull = hasParams and req.at(JS(params)).is_null();
287 auto const arrayIsEmpty = paramsIsArray and req.at(JS(params)).as_array().empty();
288 auto const arrayIsNotEmpty = paramsIsArray and not req.at(JS(params)).as_array().empty();
289 auto const firstArgIsNull = arrayIsNotEmpty and req.at(JS(params)).as_array().at(0).is_null();
290 auto const firstArgIsEmptyString = arrayIsNotEmpty and req.at(JS(params)).as_array().at(0).is_string() and
291 req.at(JS(params)).as_array().at(0).as_string().empty();
292
293 // Note: all this compatibility dance is to match `rippled` as close as possible
294 return not hasParams or paramsIsEmptyString or paramsIsNull or paramsIsEmptyObject or arrayIsEmpty or
295 firstArgIsEmptyString or firstArgIsNull;
296 }
297};
298
299} // 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:169
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:63
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)
Create a new server handler.
Definition RPCServerHandler.hpp:82
void operator()(std::string const &request, std::shared_ptr< web::ConnectionBase > const &connection)
The callback when server receives a request.
Definition RPCServerHandler.hpp:103
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:37
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:754
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