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