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 "etl/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<etl::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<etl::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](
129 boost::asio::yield_context yield
130 ) mutable { handleRequest(yield, std::move(request), connection); },
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()
145 << ". For request: " << request;
146 web::impl::ErrorHelper(connection).sendJsonParsingError();
147 } catch (std::exception const& ex) {
148 LOG(perfLog_.error()) << connection->tag() << "Caught exception: " << ex.what();
149 rpcEngine_->notifyInternalError();
150 throw;
151 }
152 }
153
154private:
155 void
156 handleRequest(
157 boost::asio::yield_context yield,
158 boost::json::object&& request,
159 std::shared_ptr<web::ConnectionBase> const& connection
160 )
161 {
162 LOG(log_.info()) << connection->tag() << (connection->upgraded ? "ws" : "http")
163 << " received request from work queue: " << util::removeSecret(request)
164 << " ip = " << connection->clientIp();
165
166 try {
167 auto const range = backend_->fetchLedgerRange();
168 if (!range) {
169 // for error that happened before the handler, we don't attach any warnings
170 rpcEngine_->notifyNotReady();
171 web::impl::ErrorHelper(connection, std::move(request)).sendNotReadyError();
172
173 return;
174 }
175
176 auto const context = [&] {
177 if (connection->upgraded) {
178 return rpc::makeWsContext(
179 yield,
180 request,
181 connection->makeSubscriptionContext(tagFactory_),
182 tagFactory_.with(connection->tag()),
183 *range,
184 connection->clientIp(),
185 std::cref(apiVersionParser_),
186 connection->isAdmin()
187 );
188 }
190 yield,
191 request,
192 tagFactory_.with(connection->tag()),
193 *range,
194 connection->clientIp(),
195 std::cref(apiVersionParser_),
196 connection->isAdmin()
197 );
198 }();
199
200 if (!context) {
201 auto const err = context.error();
202 LOG(perfLog_.warn())
203 << connection->tag() << "Could not create Web context: " << err;
204 LOG(log_.warn()) << connection->tag() << "Could not create Web context: " << err;
205
206 // we count all those as BadSyntax - as the WS path would.
207 // Although over HTTP these will yield a 400 status with a plain text response (for
208 // most).
209 rpcEngine_->notifyBadSyntax();
210 web::impl::ErrorHelper(connection, std::move(request)).sendError(err);
211
212 return;
213 }
214
215 auto [result, timeDiff] =
216 util::timed([&]() { return rpcEngine_->buildResponse(*context); });
217
218 auto const us = std::chrono::duration<int, std::milli>(timeDiff);
219 rpc::logDuration(request, context->tag(), us);
220
221 boost::json::object response;
222
223 if (!result.response.has_value()) {
224 // note: error statuses are counted/notified in buildResponse itself
225 response = web::impl::ErrorHelper(connection, request)
226 .composeError(result.response.error());
227 auto const responseStr = boost::json::serialize(response);
228
229 LOG(perfLog_.debug()) << context->tag() << "Encountered error: " << responseStr;
230 LOG(log_.debug()) << context->tag() << "Encountered error: " << responseStr;
231 } else {
232 auto& json = result.response.value();
233 auto const isForwarded = json.contains("forwarded") &&
234 json.at("forwarded").is_bool() && json.at("forwarded").as_bool();
235
236 // This can still technically be an error. Clio counts forwarded requests
237 // as successful.
238 rpcEngine_->notifyComplete(*context, us, isForwarded);
239
240 if (isForwarded)
241 json.erase("forwarded");
242
243 // if the result is forwarded - just use it as is
244 // if forwarded request has error, for http, error should be in "result"; for ws,
245 // error should be at top
246 if (isForwarded && (json.contains(JS(result)) || connection->upgraded)) {
247 for (auto const& [k, v] : json)
248 response.insert_or_assign(k, v);
249 } else {
250 response[JS(result)] = json;
251 }
252
253 if (isForwarded)
254 response["forwarded"] = true;
255
256 // for ws there is an additional field "status" in the response,
257 // otherwise the "status" is in the "result" field
258 if (connection->upgraded) {
259 auto const appendFieldIfExist = [&](auto const& field) {
260 if (request.contains(field) and not request.at(field).is_null())
261 response[field] = request.at(field);
262 };
263
264 appendFieldIfExist(JS(id));
265 appendFieldIfExist(JS(api_version));
266
267 if (!response.contains(JS(error)))
268 response[JS(status)] = JS(success);
269
270 response[JS(type)] = JS(response);
271 } else {
272 if (response.contains(JS(result)) &&
273 !response[JS(result)].as_object().contains(JS(error)))
274 response[JS(result)].as_object()[JS(status)] = JS(success);
275 }
276 }
277
278 boost::json::array warnings = std::move(result.warnings);
279 warnings.emplace_back(rpc::makeWarning(rpc::WarnRpcClio));
280
281 if (etl_->lastCloseAgeSeconds() >= 60)
282 warnings.emplace_back(rpc::makeWarning(rpc::WarnRpcOutdated));
283
284 response["warnings"] = warnings;
285 connection->send(boost::json::serialize(response));
286 } catch (std::exception const& ex) {
287 // note: while we are catching this in buildResponse too, this is here to make sure
288 // that any other code that may throw is outside of buildResponse is also worked around.
289 LOG(perfLog_.error()) << connection->tag() << "Caught exception: " << ex.what();
290 LOG(log_.error()) << connection->tag() << "Caught exception: " << ex.what();
291
292 rpcEngine_->notifyInternalError();
293 web::impl::ErrorHelper(connection, std::move(request)).sendInternalError();
294
295 return;
296 }
297 }
298
299 bool
300 shouldReplaceParams(boost::json::object const& req) const
301 {
302 auto const hasParams = req.contains(JS(params));
303 auto const paramsIsArray = hasParams and req.at(JS(params)).is_array();
304 auto const paramsIsEmptyString =
305 hasParams and req.at(JS(params)).is_string() and req.at(JS(params)).as_string().empty();
306 auto const paramsIsEmptyObject =
307 hasParams and req.at(JS(params)).is_object() and req.at(JS(params)).as_object().empty();
308 auto const paramsIsNull = hasParams and req.at(JS(params)).is_null();
309 auto const arrayIsEmpty = paramsIsArray and req.at(JS(params)).as_array().empty();
310 auto const arrayIsNotEmpty = paramsIsArray and not req.at(JS(params)).as_array().empty();
311 auto const firstArgIsNull =
312 arrayIsNotEmpty and req.at(JS(params)).as_array().at(0).is_null();
313 auto const firstArgIsEmptyString = arrayIsNotEmpty and
314 req.at(JS(params)).as_array().at(0).is_string() and
315 req.at(JS(params)).as_array().at(0).as_string().empty();
316
317 // Note: all this compatibility dance is to match `rippled` as close as possible
318 return not hasParams or paramsIsEmptyString or paramsIsNull or paramsIsEmptyObject or
319 arrayIsEmpty or firstArgIsEmptyString or firstArgIsNull;
320 }
321};
322
323} // namespace web
Definition APIVersionParser.hpp:34
A simple thread-safe logger for the channel specified in the constructor.
Definition Logger.hpp:96
Pump info(SourceLocationType const &loc=CURRENT_SRC_LOCATION) const
Interface for logging at Severity::NFO severity.
Definition Logger.cpp:507
A factory for TagDecorator instantiation.
Definition Taggable.hpp:184
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:50
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: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
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:775
boost::json::object makeWarning(WarningCode code)
Generate JSON from a rpc::WarningCode.
Definition Errors.cpp:103
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:81
boost::json::object removeSecret(boost::json::object const &object)
Removes any detected secret information from a response JSON object.
Definition JsonUtils.hpp:74
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