Support api_version (#695)

Fixes #64
This commit is contained in:
Alex Kremer
2023-06-16 12:14:30 +01:00
committed by GitHub
parent 871d43c85f
commit a960471ef4
58 changed files with 734 additions and 299 deletions

View File

@@ -68,6 +68,7 @@ target_sources(clio PRIVATE
src/rpc/WorkQueue.cpp src/rpc/WorkQueue.cpp
src/rpc/common/Specs.cpp src/rpc/common/Specs.cpp
src/rpc/common/Validators.cpp src/rpc/common/Validators.cpp
src/rpc/common/impl/APIVersionParser.cpp
# RPC impl # RPC impl
src/rpc/common/impl/HandlerProvider.cpp src/rpc/common/impl/HandlerProvider.cpp
## RPC handler ## RPC handler
@@ -125,6 +126,7 @@ if(BUILD_TESTS)
unittests/rpc/RPCHelpersTest.cpp unittests/rpc/RPCHelpersTest.cpp
unittests/rpc/CountersTest.cpp unittests/rpc/CountersTest.cpp
unittests/rpc/AdminVerificationTest.cpp unittests/rpc/AdminVerificationTest.cpp
unittests/rpc/APIVersionTests.cpp
## RPC handlers ## RPC handlers
unittests/rpc/handlers/DefaultProcessorTests.cpp unittests/rpc/handlers/DefaultProcessorTests.cpp
unittests/rpc/handlers/TestHandlerTests.cpp unittests/rpc/handlers/TestHandlerTests.cpp
@@ -167,9 +169,11 @@ if(BUILD_TESTS)
unittests/webserver/RPCExecutorTest.cpp) unittests/webserver/RPCExecutorTest.cpp)
include(CMake/deps/gtest.cmake) include(CMake/deps/gtest.cmake)
# test for dwarf5 bug on ci # fix for dwarf5 bug on ci
target_compile_options(clio PUBLIC -gdwarf-4) target_compile_options(clio PUBLIC -gdwarf-4)
target_compile_definitions(${TEST_TARGET} PUBLIC UNITTEST_BUILD)
# if CODE_COVERAGE enable, add clio_test-ccov # if CODE_COVERAGE enable, add clio_test-ccov
if(CODE_COVERAGE) if(CODE_COVERAGE)
include(CMake/coverage.cmake) include(CMake/coverage.cmake)

View File

@@ -96,12 +96,12 @@ The parameters `ssl_cert_file` and `ssl_key_file` can also be added to the top l
An example of how to specify `ssl_cert_file` and `ssl_key_file` in the config: An example of how to specify `ssl_cert_file` and `ssl_key_file` in the config:
```json ```json
"server":{ "server": {
"ip": "0.0.0.0", "ip": "0.0.0.0",
"port": 51233 "port": 51233
}, },
"ssl_cert_file" : "/full/path/to/cert.file", "ssl_cert_file": "/full/path/to/cert.file",
"ssl_key_file" : "/full/path/to/key.file" "ssl_key_file": "/full/path/to/key.file"
``` ```
Once your config files are ready, start rippled and Clio. It doesn't matter which you Once your config files are ready, start rippled and Clio. It doesn't matter which you
@@ -172,6 +172,20 @@ which can cause high latencies. A possible alternative to this is to just deploy
a database in each region, and the Clio nodes in each region use their region's database. a database in each region, and the Clio nodes in each region use their region's database.
This is effectively two systems. This is effectively two systems.
Clio supports API versioning as [described here](https://xrpl.org/request-formatting.html#api-versioning).
It's possible to configure `minimum`, `maximum` and `default` version like so:
```json
"api_version": {
"min": 1,
"max": 2,
"default": 2
}
```
All of the above are optional.
Clio will fallback to hardcoded defaults when not specified in the config file or configured values are outside
of the minimum and maximum supported versions hardcoded in `src/rpc/common/APIVersion.h`.
> **Note:** See `example-config.json` for more details.
## Developing against `rippled` in standalone mode ## Developing against `rippled` in standalone mode
If you wish you develop against a `rippled` instance running in standalone If you wish you develop against a `rippled` instance running in standalone

View File

@@ -90,4 +90,9 @@
//"finish_sequence": [integer] the ledger index to finish at, //"finish_sequence": [integer] the ledger index to finish at,
//"ssl_cert_file" : "/full/path/to/cert.file", //"ssl_cert_file" : "/full/path/to/cert.file",
//"ssl_key_file" : "/full/path/to/key.file" //"ssl_key_file" : "/full/path/to/key.file"
"api_version": {
"min": 2,
"max": 2,
"default": 2 // Clio only supports API v2 and newer
}
} }

View File

@@ -142,6 +142,15 @@ Config::section(key_type key) const
throw std::logic_error("No section found at '" + key + "'"); throw std::logic_error("No section found at '" + key + "'");
} }
Config
Config::sectionOr(key_type key, boost::json::object fallback) const
{
auto maybe_element = lookup(key);
if (maybe_element && maybe_element->is_object())
return Config{std::move(*maybe_element)};
return Config{std::move(fallback)};
}
Config::array_type Config::array_type
Config::array() const Config::array() const
{ {

View File

@@ -266,6 +266,20 @@ public:
[[nodiscard]] Config [[nodiscard]] Config
section(key_type key) const; section(key_type key) const;
/**
* @brief Interface for fetching a sub section by key with a fallback object.
*
* Will attempt to fetch an entire section under the desired key and return
* it as a Config instance. If the section does not exist or another type is
* stored under the desired key - fallback object is used instead.
*
* @param key The key to check
* @param fallabkc The fallback object
* @return Config Section represented as a separate instance of Config
*/
[[nodiscard]] Config
sectionOr(key_type key, boost::json::object fallback) const;
// //
// Direct self-value access // Direct self-value access
// //

View File

@@ -18,6 +18,7 @@
//============================================================================== //==============================================================================
#include <rpc/Errors.h> #include <rpc/Errors.h>
#include <rpc/JS.h>
#include <algorithm> #include <algorithm>
@@ -77,7 +78,8 @@ getErrorInfo(ClioError code)
{ClioError::rpcMALFORMED_REQUEST, "malformedRequest", "Malformed request."}, {ClioError::rpcMALFORMED_REQUEST, "malformedRequest", "Malformed request."},
{ClioError::rpcMALFORMED_OWNER, "malformedOwner", "Malformed owner."}, {ClioError::rpcMALFORMED_OWNER, "malformedOwner", "Malformed owner."},
{ClioError::rpcMALFORMED_ADDRESS, "malformedAddress", "Malformed address."}, {ClioError::rpcMALFORMED_ADDRESS, "malformedAddress", "Malformed address."},
{ClioError::rpcINVALID_HOT_WALLET, "invalidHotWallet", "Invalid hot wallet."}}; {ClioError::rpcINVALID_HOT_WALLET, "invalidHotWallet", "Invalid hot wallet."},
{ClioError::rpcINVALID_API_VERSION, JS(invalid_API_version), "Invalid API version."}};
auto matchByCode = [code](auto const& info) { return info.code == code; }; auto matchByCode = [code](auto const& info) { return info.code == code; };
if (auto it = find_if(begin(infos), end(infos), matchByCode); it != end(infos)) if (auto it = find_if(begin(infos), end(infos), matchByCode); it != end(infos))

View File

@@ -34,11 +34,15 @@ namespace RPC {
* @brief Custom clio RPC Errors. * @brief Custom clio RPC Errors.
*/ */
enum class ClioError { enum class ClioError {
// normal clio errors start with 5000
rpcMALFORMED_CURRENCY = 5000, rpcMALFORMED_CURRENCY = 5000,
rpcMALFORMED_REQUEST = 5001, rpcMALFORMED_REQUEST = 5001,
rpcMALFORMED_OWNER = 5002, rpcMALFORMED_OWNER = 5002,
rpcMALFORMED_ADDRESS = 5003, rpcMALFORMED_ADDRESS = 5003,
rpcINVALID_HOT_WALLET = 5004, rpcINVALID_HOT_WALLET = 5004,
// special system errors start with 6000
rpcINVALID_API_VERSION = 6000,
}; };
/** /**

View File

@@ -18,21 +18,25 @@
//============================================================================== //==============================================================================
#include <rpc/Factories.h> #include <rpc/Factories.h>
#include <rpc/common/Types.h>
using namespace std; using namespace std;
using namespace clio; using namespace clio;
namespace RPC { namespace RPC {
optional<Web::Context> util::Expected<Web::Context, Status>
make_WsContext( make_WsContext(
boost::asio::yield_context& yc, boost::asio::yield_context& yc,
boost::json::object const& request, boost::json::object const& request,
shared_ptr<Server::ConnectionBase> const& session, shared_ptr<Server::ConnectionBase> const& session,
util::TagDecoratorFactory const& tagFactory, util::TagDecoratorFactory const& tagFactory,
Backend::LedgerRange const& range, Backend::LedgerRange const& range,
string const& clientIp) string const& clientIp,
std::reference_wrapper<APIVersionParser const> apiVersionParser)
{ {
using Error = util::Unexpected<Status>;
boost::json::value commandValue = nullptr; boost::json::value commandValue = nullptr;
if (!request.contains("command") && request.contains("method")) if (!request.contains("command") && request.contains("method"))
commandValue = request.at("method"); commandValue = request.at("method");
@@ -40,40 +44,48 @@ make_WsContext(
commandValue = request.at("command"); commandValue = request.at("command");
if (!commandValue.is_string()) if (!commandValue.is_string())
return {}; return Error{{RippledError::rpcBAD_SYNTAX, "Method/Command is not specified or is not a string."}};
auto const apiVersion = apiVersionParser.get().parse(request);
if (!apiVersion)
return Error{{ClioError::rpcINVALID_API_VERSION, apiVersion.error()}};
string command = commandValue.as_string().c_str(); string command = commandValue.as_string().c_str();
return make_optional<Web::Context>(yc, command, 1, request, session, tagFactory, range, clientIp); return Web::Context(yc, command, *apiVersion, request, session, tagFactory, range, clientIp);
} }
optional<Web::Context> util::Expected<Web::Context, Status>
make_HttpContext( make_HttpContext(
boost::asio::yield_context& yc, boost::asio::yield_context& yc,
boost::json::object const& request, boost::json::object const& request,
util::TagDecoratorFactory const& tagFactory, util::TagDecoratorFactory const& tagFactory,
Backend::LedgerRange const& range, Backend::LedgerRange const& range,
string const& clientIp) string const& clientIp,
std::reference_wrapper<APIVersionParser const> apiVersionParser)
{ {
using Error = util::Unexpected<Status>;
if (!request.contains("method") || !request.at("method").is_string()) if (!request.contains("method") || !request.at("method").is_string())
return {}; return Error{{RippledError::rpcBAD_SYNTAX, "Method is not specified or is not a string."}};
string const& command = request.at("method").as_string().c_str(); string const& command = request.at("method").as_string().c_str();
if (command == "subscribe" || command == "unsubscribe") if (command == "subscribe" || command == "unsubscribe")
return {}; return Error{{RippledError::rpcBAD_SYNTAX, "Subscribe and unsubscribe are only allowed or websocket."}};
if (!request.at("params").is_array()) if (!request.at("params").is_array())
return {}; return Error{{RippledError::rpcBAD_SYNTAX, "Missing params array."}};
boost::json::array const& array = request.at("params").as_array(); boost::json::array const& array = request.at("params").as_array();
if (array.size() != 1) if (array.size() != 1 || !array.at(0).is_object())
return {}; return Error{{RippledError::rpcBAD_SYNTAX, "Params must be an array holding exactly one object."}};
if (!array.at(0).is_object()) auto const apiVersion = apiVersionParser.get().parse(request.at("params").as_array().at(0).as_object());
return {}; if (!apiVersion)
return Error{{ClioError::rpcINVALID_API_VERSION, apiVersion.error()}};
return make_optional<Web::Context>(yc, command, 1, array.at(0).as_object(), nullptr, tagFactory, range, clientIp); return Web::Context(yc, command, *apiVersion, array.at(0).as_object(), nullptr, tagFactory, range, clientIp);
} }
} // namespace RPC } // namespace RPC

View File

@@ -20,6 +20,9 @@
#pragma once #pragma once
#include <backend/BackendInterface.h> #include <backend/BackendInterface.h>
#include <rpc/Errors.h>
#include <rpc/common/APIVersion.h>
#include <util/Expected.h>
#include <webserver/Context.h> #include <webserver/Context.h>
#include <webserver/interface/ConnectionBase.h> #include <webserver/interface/ConnectionBase.h>
@@ -38,21 +41,23 @@
*/ */
namespace RPC { namespace RPC {
std::optional<Web::Context> util::Expected<Web::Context, Status>
make_WsContext( make_WsContext(
boost::asio::yield_context& yc, boost::asio::yield_context& yc,
boost::json::object const& request, boost::json::object const& request,
std::shared_ptr<Server::ConnectionBase> const& session, std::shared_ptr<Server::ConnectionBase> const& session,
util::TagDecoratorFactory const& tagFactory, util::TagDecoratorFactory const& tagFactory,
Backend::LedgerRange const& range, Backend::LedgerRange const& range,
std::string const& clientIp); std::string const& clientIp,
std::reference_wrapper<APIVersionParser const> apiVersionParser);
std::optional<Web::Context> util::Expected<Web::Context, Status>
make_HttpContext( make_HttpContext(
boost::asio::yield_context& yc, boost::asio::yield_context& yc,
boost::json::object const& request, boost::json::object const& request,
util::TagDecoratorFactory const& tagFactory, util::TagDecoratorFactory const& tagFactory,
Backend::LedgerRange const& range, Backend::LedgerRange const& range,
std::string const& clientIp); std::string const& clientIp,
std::reference_wrapper<APIVersionParser const> apiVersionParser);
} // namespace RPC } // namespace RPC

View File

@@ -24,6 +24,6 @@ Handlers need to fulfil the requirements specified by the **`Handler`** concept
- Expose types: - Expose types:
* `Input` - The POD struct which acts as input for the handler * `Input` - The POD struct which acts as input for the handler
* `Output` - The POD struct which acts as output of a valid handler invocation * `Output` - The POD struct which acts as output of a valid handler invocation
- Have a `spec()` member function returning a const reference to an **`RpcSpec`** describing the JSON input. - Have a `spec(uint32_t)` member function returning a const reference to an **`RpcSpec`** describing the JSON input.
- Have a `process(Input)` member function that operates on `Input` POD and returns `HandlerReturnType<Output>` - Have a `process(Input)` member function that operates on `Input` POD and returns `HandlerReturnType<Output>`
- Implement `value_from` and `value_to` support using `tag_invoke` as per `boost::json` documentation for these functions. - Implement `value_from` and `value_to` support using `tag_invoke` as per `boost::json` documentation for these functions.

View File

@@ -148,7 +148,7 @@ public:
perfLog_.debug() << ctx.tag() << " start executing rpc `" << ctx.method << '`'; perfLog_.debug() << ctx.tag() << " start executing rpc `" << ctx.method << '`';
auto const isAdmin = adminVerifier_.isAdmin(ctx.clientIp); auto const isAdmin = adminVerifier_.isAdmin(ctx.clientIp);
auto const context = Context{ctx.yield, ctx.session, isAdmin, ctx.clientIp}; auto const context = Context{ctx.yield, ctx.session, isAdmin, ctx.clientIp, ctx.apiVersion};
auto const v = (*method).process(ctx.params, context); auto const v = (*method).process(ctx.params, context);
perfLog_.debug() << ctx.tag() << " finish executing rpc `" << ctx.method << '`'; perfLog_.debug() << ctx.tag() << " finish executing rpc `" << ctx.method << '`';
@@ -303,7 +303,7 @@ private:
bool bool
shouldForwardToRippled(Web::Context const& ctx) const shouldForwardToRippled(Web::Context const& ctx) const
{ {
auto request = ctx.params; auto const& request = ctx.params;
if (isClioOnly(ctx.method)) if (isClioOnly(ctx.method))
return false; return false;

View File

@@ -0,0 +1,59 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2023, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#pragma once
#include <rpc/common/Types.h>
#include <util/Expected.h>
#include <boost/json.hpp>
#include <string>
namespace RPC {
/**
* @brief Default API version to use if no version is specified by clients
*/
static constexpr uint32_t API_VERSION_DEFAULT = 2u;
/**
* @brief Minimum API version supported by this build
*
* Note: Clio does not support v1 and only supports v2 and newer.
*/
static constexpr uint32_t API_VERSION_MIN = 2u;
/**
* @brief Maximum API version supported by this build
*/
static constexpr uint32_t API_VERSION_MAX = 2u;
/**
* @brief A baseclass for API version helper
*/
class APIVersionParser
{
public:
virtual ~APIVersionParser() = default;
util::Expected<uint32_t, std::string> virtual parse(boost::json::object const& request) const = 0;
};
} // namespace RPC

View File

@@ -69,18 +69,7 @@ public:
* @brief Process incoming JSON by the stored handler * @brief Process incoming JSON by the stored handler
* *
* @param value The JSON to process * @param value The JSON to process
* @return JSON result or @ref Status on error * @param ctx Request context
*/
[[nodiscard]] ReturnType
process(boost::json::value const& value) const
{
return pimpl_->process(value);
}
/**
* @brief Process incoming JSON by the stored handler in a provided coroutine
*
* @param value The JSON to process
* @return JSON result or @ref Status on error * @return JSON result or @ref Status on error
*/ */
[[nodiscard]] ReturnType [[nodiscard]] ReturnType
@@ -97,9 +86,6 @@ private:
[[nodiscard]] virtual ReturnType [[nodiscard]] virtual ReturnType
process(boost::json::value const& value, Context const& ctx) const = 0; process(boost::json::value const& value, Context const& ctx) const = 0;
[[nodiscard]] virtual ReturnType
process(boost::json::value const& value) const = 0;
[[nodiscard]] virtual std::unique_ptr<Concept> [[nodiscard]] virtual std::unique_ptr<Concept>
clone() const = 0; clone() const = 0;
}; };
@@ -114,16 +100,10 @@ private:
{ {
} }
[[nodiscard]] ReturnType
process(boost::json::value const& value) const override
{
return processor(handler, value);
}
[[nodiscard]] ReturnType [[nodiscard]] ReturnType
process(boost::json::value const& value, Context const& ctx) const override process(boost::json::value const& value, Context const& ctx) const override
{ {
return processor(handler, value, &ctx); return processor(handler, value, ctx);
} }
[[nodiscard]] std::unique_ptr<Concept> [[nodiscard]] std::unique_ptr<Concept>

View File

@@ -59,22 +59,14 @@ concept ContextProcessWithoutInput = requires(T a, typename T::Output out, Conte
}; };
template <typename T> template <typename T>
concept NonContextProcess = requires(T a, typename T::Input in, typename T::Output out) { concept HandlerWithInput = requires(T a, uint32_t version) {
{ a.process(in) } -> std::same_as<HandlerReturnType<decltype(out)>>; { a.spec(version) } -> std::same_as<RpcSpecConstRef>;
};
template <typename T>
concept HandlerWithInput = requires(T a) {
{ a.spec() } -> std::same_as<RpcSpecConstRef>;
} }
and (ContextProcessWithInput<T> or NonContextProcess<T>) and ContextProcessWithInput<T>
and boost::json::has_value_to<typename T::Input>::value; and boost::json::has_value_to<typename T::Input>::value;
template <typename T> template <typename T>
concept HandlerWithoutInput = requires(T a, typename T::Output out) { concept HandlerWithoutInput = ContextProcessWithoutInput<T>;
{ a.process() } -> std::same_as<HandlerReturnType<decltype(out)>>;
}
or ContextProcessWithoutInput<T>;
template <typename T> template <typename T>
concept Handler = concept Handler =

View File

@@ -73,6 +73,7 @@ struct Context
std::shared_ptr<Server::ConnectionBase> session; std::shared_ptr<Server::ConnectionBase> session;
bool isAdmin = false; bool isAdmin = false;
std::string clientIp; std::string clientIp;
uint32_t apiVersion = 0u; // invalid by default
}; };
using Result = std::variant<Status, boost::json::object>; using Result = std::variant<Status, boost::json::object>;

View File

@@ -0,0 +1,61 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2023, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <log/Logger.h>
#include <rpc/common/impl/APIVersionParser.h>
#include <fmt/core.h>
using namespace std;
namespace RPC::detail {
ProductionAPIVersionParser::ProductionAPIVersionParser(clio::Config const& config)
: ProductionAPIVersionParser(
config.valueOr("default", API_VERSION_DEFAULT),
config.valueOr("min", API_VERSION_MIN),
config.valueOr("max", API_VERSION_MAX))
{
}
util::Expected<uint32_t, std::string>
ProductionAPIVersionParser::parse(boost::json::object const& request) const
{
using Error = util::Unexpected<std::string>;
if (request.contains("api_version"))
{
if (!request.at("api_version").is_int64())
return Error{"API version must be an integer"};
auto const version = request.at("api_version").as_int64();
if (version > maxVersion_)
return Error{fmt::format("Requested API version is higher than maximum supported ({})", maxVersion_)};
if (version < minVersion_)
return Error{fmt::format("Requested API version is lower than minimum supported ({})", minVersion_)};
return version;
}
return defaultVersion_;
}
} // namespace RPC::detail

View File

@@ -0,0 +1,79 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2023, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#pragma once
#include <config/Config.h>
#include <log/Logger.h>
#include <rpc/common/APIVersion.h>
#include <util/Expected.h>
#include <algorithm>
#include <string>
namespace RPC::detail {
class ProductionAPIVersionParser : public APIVersionParser
{
clio::Logger log_{"RPC"};
uint32_t defaultVersion_;
uint32_t minVersion_;
uint32_t maxVersion_;
public:
// Note: this constructor must remain in the header because of UNITTEST_BUILD definition below
ProductionAPIVersionParser(
uint32_t defaultVersion = API_VERSION_DEFAULT,
uint32_t minVersion = API_VERSION_MIN,
uint32_t maxVersion = API_VERSION_MAX)
: defaultVersion_{defaultVersion}, minVersion_{minVersion}, maxVersion_{maxVersion}
{
#ifndef UNITTEST_BUILD
// in production, we don't want the ability to misconfigure clio with bogus versions
// that are not actually supported by the code itself. for testing it is desired however.
auto checkRange = [this](uint32_t version, std::string label) {
if (std::clamp(version, API_VERSION_MIN, API_VERSION_MAX) != version)
{
log_.error() << "API version settings issue detected: " << label << " version with value " << version
<< " is outside of supported range " << API_VERSION_MIN << "-" << API_VERSION_MAX
<< "; Falling back to hardcoded values.";
defaultVersion_ = API_VERSION_DEFAULT;
minVersion_ = API_VERSION_MIN;
maxVersion_ = API_VERSION_MAX;
}
};
checkRange(defaultVersion, "default");
checkRange(minVersion, "minimum");
checkRange(maxVersion, "maximum");
#endif
log_.info() << "API version settings: [min = " << minVersion_ << "; max = " << maxVersion_
<< "; default = " << defaultVersion_ << "]";
}
ProductionAPIVersionParser(clio::Config const& config);
util::Expected<uint32_t, std::string>
parse(boost::json::object const& request) const override;
};
} // namespace RPC::detail

View File

@@ -43,11 +43,7 @@ class ProductionHandlerProvider final : public HandlerProvider
struct Handler struct Handler
{ {
AnyHandler handler; AnyHandler handler;
bool isClioOnly; bool isClioOnly = false;
/* implicit */ Handler(AnyHandler handler, bool clioOnly = false) : handler{handler}, isClioOnly{clioOnly}
{
}
}; };
std::unordered_map<std::string, Handler> handlerMap_; std::unordered_map<std::string, Handler> handlerMap_;

View File

@@ -19,6 +19,7 @@
#pragma once #pragma once
#include <rpc/common/APIVersion.h>
#include <rpc/common/Concepts.h> #include <rpc/common/Concepts.h>
#include <rpc/common/Types.h> #include <rpc/common/Types.h>
@@ -31,56 +32,35 @@ template <Handler HandlerType>
struct DefaultProcessor final struct DefaultProcessor final
{ {
[[nodiscard]] ReturnType [[nodiscard]] ReturnType
operator()(HandlerType const& handler, boost::json::value const& value, Context const* ctx = nullptr) const operator()(HandlerType const& handler, boost::json::value const& value, Context const& ctx) const
{ {
using boost::json::value_from; using boost::json::value_from;
using boost::json::value_to; using boost::json::value_to;
if constexpr (HandlerWithInput<HandlerType>) if constexpr (HandlerWithInput<HandlerType>)
{ {
// first we run validation // first we run validation against specified API version
auto const spec = handler.spec(); auto const spec = handler.spec(ctx.apiVersion);
if (auto const ret = spec.validate(value); not ret) if (auto const ret = spec.validate(value); not ret)
return Error{ret.error()}; // forward Status return Error{ret.error()}; // forward Status
auto const inData = value_to<typename HandlerType::Input>(value); auto const inData = value_to<typename HandlerType::Input>(value);
if constexpr (NonContextProcess<HandlerType>) auto const ret = handler.process(inData, ctx);
{
auto const ret = handler.process(inData); // real handler is given expected Input, not json
// real handler is given expected Input, not json if (!ret)
if (!ret) return Error{ret.error()}; // forward Status
return Error{ret.error()}; // forward Status
else
return value_from(ret.value());
}
else else
{ return value_from(ret.value());
auto const ret = handler.process(inData, *ctx);
// real handler is given expected Input, not json
if (!ret)
return Error{ret.error()}; // forward Status
else
return value_from(ret.value());
}
} }
else if constexpr (HandlerWithoutInput<HandlerType>) else if constexpr (HandlerWithoutInput<HandlerType>)
{ {
using OutType = HandlerReturnType<typename HandlerType::Output>; using OutType = HandlerReturnType<typename HandlerType::Output>;
// no input to pass, ignore the value // no input to pass, ignore the value
if constexpr (ContextProcessWithoutInput<HandlerType>) if (auto const ret = handler.process(ctx); not ret)
{ return Error{ret.error()}; // forward Status
if (auto const ret = handler.process(*ctx); not ret)
return Error{ret.error()}; // forward Status
else
return value_from(ret.value());
}
else else
{ return value_from(ret.value());
if (auto const ret = handler.process(); not ret)
return Error{ret.error()}; // forward Status
else
return value_from(ret.value());
}
} }
else else
{ {

View File

@@ -88,7 +88,7 @@ public:
} }
RpcSpecConstRef RpcSpecConstRef
spec() const spec([[maybe_unused]] uint32_t apiVersion) const
{ {
static auto const rpcSpec = RpcSpec{ static auto const rpcSpec = RpcSpec{
{JS(account), validation::Required{}, validation::AccountValidator}, {JS(account), validation::Required{}, validation::AccountValidator},

View File

@@ -66,7 +66,7 @@ public:
} }
RpcSpecConstRef RpcSpecConstRef
spec() const spec([[maybe_unused]] uint32_t apiVersion) const
{ {
static auto const rpcSpec = RpcSpec{ static auto const rpcSpec = RpcSpec{
{JS(account), validation::Required{}, validation::AccountValidator}, {JS(account), validation::Required{}, validation::AccountValidator},

View File

@@ -81,7 +81,7 @@ public:
} }
RpcSpecConstRef RpcSpecConstRef
spec() const spec([[maybe_unused]] uint32_t apiVersion) const
{ {
static auto const rpcSpec = RpcSpec{ static auto const rpcSpec = RpcSpec{
{JS(account), validation::AccountValidator}, {JS(account), validation::AccountValidator},

View File

@@ -87,7 +87,7 @@ public:
} }
RpcSpecConstRef RpcSpecConstRef
spec() const spec([[maybe_unused]] uint32_t apiVersion) const
{ {
static auto const rpcSpec = RpcSpec{ static auto const rpcSpec = RpcSpec{
{JS(account), validation::Required{}, validation::AccountValidator}, {JS(account), validation::Required{}, validation::AccountValidator},

View File

@@ -64,7 +64,7 @@ public:
} }
RpcSpecConstRef RpcSpecConstRef
spec() const spec([[maybe_unused]] uint32_t apiVersion) const
{ {
static auto const rpcSpec = RpcSpec{ static auto const rpcSpec = RpcSpec{
{JS(account), validation::Required{}, validation::AccountValidator}, {JS(account), validation::Required{}, validation::AccountValidator},

View File

@@ -75,7 +75,7 @@ public:
} }
RpcSpecConstRef RpcSpecConstRef
spec() const spec([[maybe_unused]] uint32_t apiVersion) const
{ {
static auto const rpcSpec = RpcSpec{ static auto const rpcSpec = RpcSpec{
{JS(account), validation::Required{}, validation::AccountValidator}, {JS(account), validation::Required{}, validation::AccountValidator},

View File

@@ -75,7 +75,7 @@ public:
} }
RpcSpecConstRef RpcSpecConstRef
spec() const spec([[maybe_unused]] uint32_t apiVersion) const
{ {
static auto const rpcSpec = RpcSpec{ static auto const rpcSpec = RpcSpec{
{JS(account), validation::Required{}, validation::AccountValidator}, {JS(account), validation::Required{}, validation::AccountValidator},

View File

@@ -81,7 +81,7 @@ public:
} }
RpcSpecConstRef RpcSpecConstRef
spec() const spec([[maybe_unused]] uint32_t apiVersion) const
{ {
static auto const rpcSpec = RpcSpec{ static auto const rpcSpec = RpcSpec{
{JS(account), validation::Required{}, validation::AccountValidator}, {JS(account), validation::Required{}, validation::AccountValidator},

View File

@@ -60,7 +60,7 @@ public:
} }
RpcSpecConstRef RpcSpecConstRef
spec() const spec([[maybe_unused]] uint32_t apiVersion) const
{ {
static auto const rpcSpec = RpcSpec{ static auto const rpcSpec = RpcSpec{
{JS(ledger_hash), validation::Uint256HexStringValidator}, {JS(ledger_hash), validation::Uint256HexStringValidator},

View File

@@ -65,7 +65,7 @@ public:
} }
RpcSpecConstRef RpcSpecConstRef
spec() const spec([[maybe_unused]] uint32_t apiVersion) const
{ {
static auto const rpcSpec = RpcSpec{ static auto const rpcSpec = RpcSpec{
{JS(taker_gets), {JS(taker_gets),

View File

@@ -68,7 +68,7 @@ public:
} }
RpcSpecConstRef RpcSpecConstRef
spec() const spec([[maybe_unused]] uint32_t apiVersion) const
{ {
static auto const hotWalletValidator = static auto const hotWalletValidator =
validation::CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError { validation::CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError {

View File

@@ -65,7 +65,7 @@ public:
} }
RpcSpecConstRef RpcSpecConstRef
spec() const spec([[maybe_unused]] uint32_t apiVersion) const
{ {
static auto const rpcSpec = RpcSpec{ static auto const rpcSpec = RpcSpec{
{JS(full), validation::Type<bool>{}, validation::NotSupported{true}}, {JS(full), validation::Type<bool>{}, validation::NotSupported{true}},

View File

@@ -76,7 +76,7 @@ public:
} }
RpcSpecConstRef RpcSpecConstRef
spec() const spec([[maybe_unused]] uint32_t apiVersion) const
{ {
static const auto rpcSpec = RpcSpec{ static const auto rpcSpec = RpcSpec{
{JS(binary), validation::Type<bool>{}}, {JS(binary), validation::Type<bool>{}},

View File

@@ -74,7 +74,7 @@ public:
} }
RpcSpecConstRef RpcSpecConstRef
spec() const spec([[maybe_unused]] uint32_t apiVersion) const
{ {
// Validator only works in this handler // Validator only works in this handler
// The accounts array must have two different elements // The accounts array must have two different elements

View File

@@ -25,7 +25,7 @@
namespace RPC { namespace RPC {
LedgerRangeHandler::Result LedgerRangeHandler::Result
LedgerRangeHandler::process() const LedgerRangeHandler::process([[maybe_unused]] Context const& ctx) const
{ {
// note: we can't get here if range is not available so it's safe // note: we can't get here if range is not available so it's safe
return Output{sharedPtrBackend_->fetchLedgerRange().value()}; return Output{sharedPtrBackend_->fetchLedgerRange().value()};

View File

@@ -50,7 +50,7 @@ public:
} }
Result Result
process() const; process(Context const& ctx) const;
private: private:
friend void friend void

View File

@@ -81,7 +81,7 @@ public:
} }
RpcSpecConstRef RpcSpecConstRef
spec() const spec([[maybe_unused]] uint32_t apiVersion) const
{ {
static auto const rpcSpec = RpcSpec{ static auto const rpcSpec = RpcSpec{
{JS(nft_id), validation::Required{}, validation::Uint256HexStringValidator}, {JS(nft_id), validation::Required{}, validation::Uint256HexStringValidator},

View File

@@ -68,7 +68,7 @@ public:
} }
RpcSpecConstRef RpcSpecConstRef
spec() const spec([[maybe_unused]] uint32_t apiVersion) const
{ {
static auto const rpcSpec = RpcSpec{ static auto const rpcSpec = RpcSpec{
{JS(nft_id), validation::Required{}, validation::Uint256HexStringValidator}, {JS(nft_id), validation::Required{}, validation::Uint256HexStringValidator},

View File

@@ -59,7 +59,7 @@ public:
} }
RpcSpecConstRef RpcSpecConstRef
spec() const spec([[maybe_unused]] uint32_t apiVersion) const
{ {
static auto const rpcSpec = RpcSpec{ static auto const rpcSpec = RpcSpec{
{JS(nft_id), validation::Required{}, validation::Uint256HexStringValidator}, {JS(nft_id), validation::Required{}, validation::Uint256HexStringValidator},

View File

@@ -67,7 +67,7 @@ public:
} }
RpcSpecConstRef RpcSpecConstRef
spec() const spec([[maybe_unused]] uint32_t apiVersion) const
{ {
static auto const rpcSpec = RpcSpec{ static auto const rpcSpec = RpcSpec{
{JS(account), validation::Required{}, validation::AccountValidator}, {JS(account), validation::Required{}, validation::AccountValidator},

View File

@@ -35,7 +35,7 @@ public:
using Result = HandlerReturnType<Output>; using Result = HandlerReturnType<Output>;
Result Result
process() const process([[maybe_unused]] Context const& ctx) const
{ {
return Output{}; return Output{};
} }

View File

@@ -26,7 +26,7 @@
namespace RPC { namespace RPC {
RandomHandler::Result RandomHandler::Result
RandomHandler::process() const RandomHandler::process([[maybe_unused]] Context const& ctx) const
{ {
ripple::uint256 rand; ripple::uint256 rand;
beast::rngfill(rand.begin(), rand.size(), ripple::crypto_prng()); beast::rngfill(rand.begin(), rand.size(), ripple::crypto_prng());

View File

@@ -44,7 +44,7 @@ public:
using Result = HandlerReturnType<Output>; using Result = HandlerReturnType<Output>;
Result Result
process() const; process(Context const& ctx) const;
private: private:
friend void friend void

View File

@@ -72,7 +72,7 @@ public:
} }
RpcSpecConstRef RpcSpecConstRef
spec() const spec([[maybe_unused]] uint32_t apiVersion) const
{ {
static auto const booksValidator = static auto const booksValidator =
validation::CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError { validation::CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError {

View File

@@ -62,7 +62,7 @@ public:
} }
RpcSpecConstRef RpcSpecConstRef
spec() const spec([[maybe_unused]] uint32_t apiVersion) const
{ {
static auto const rpcSpec = RpcSpec{ static auto const rpcSpec = RpcSpec{
{JS(tx_hash), validation::Required{}, validation::Uint256HexStringValidator}, {JS(tx_hash), validation::Required{}, validation::Uint256HexStringValidator},

View File

@@ -64,7 +64,7 @@ public:
} }
RpcSpecConstRef RpcSpecConstRef
spec() const spec([[maybe_unused]] uint32_t apiVersion) const
{ {
static const RpcSpec rpcSpec = { static const RpcSpec rpcSpec = {
{JS(transaction), validation::Required{}, validation::Uint256HexStringValidator}, {JS(transaction), validation::Required{}, validation::Uint256HexStringValidator},

View File

@@ -58,7 +58,7 @@ public:
} }
RpcSpecConstRef RpcSpecConstRef
spec() const spec([[maybe_unused]] uint32_t apiVersion) const
{ {
static auto const booksValidator = static auto const booksValidator =
validation::CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError { validation::CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError {

View File

@@ -259,6 +259,9 @@ protected:
public: public:
virtual ~Taggable() = default; virtual ~Taggable() = default;
Taggable(Taggable&&) = default;
Taggable&
operator=(Taggable&&) = default;
/** /**
* @brief Getter for tag decorator. * @brief Getter for tag decorator.

View File

@@ -32,37 +32,41 @@
namespace Web { namespace Web {
struct Context : public util::Taggable struct Context : util::Taggable
{ {
clio::Logger perfLog_{"Performance"}; std::reference_wrapper<boost::asio::yield_context> yield;
boost::asio::yield_context& yield;
std::string method; std::string method;
std::uint32_t version; std::uint32_t apiVersion;
boost::json::object const& params; boost::json::object params;
std::shared_ptr<Server::ConnectionBase> session; std::shared_ptr<Server::ConnectionBase> session;
Backend::LedgerRange const& range; Backend::LedgerRange range;
std::string clientIp; std::string clientIp;
Context( Context(
boost::asio::yield_context& yield_, boost::asio::yield_context& yield,
std::string const& command_, std::string const& command,
std::uint32_t version_, std::uint32_t apiVersion,
boost::json::object const& params_, boost::json::object params,
std::shared_ptr<Server::ConnectionBase> const& session_, std::shared_ptr<Server::ConnectionBase> const& session,
util::TagDecoratorFactory const& tagFactory_, util::TagDecoratorFactory const& tagFactory,
Backend::LedgerRange const& range_, Backend::LedgerRange const& range,
std::string const& clientIp_) std::string const& clientIp)
: Taggable(tagFactory_) : Taggable(tagFactory)
, yield(yield_) , yield(std::ref(yield))
, method(command_) , method(command)
, version(version_) , apiVersion(apiVersion)
, params(params_) , params(std::move(params))
, session(session_) , session(session)
, range(range_) , range(range)
, clientIp(clientIp_) , clientIp(clientIp)
{ {
perfLog_.debug() << tag() << "new Context created"; static clio::Logger perfLog{"Performance"};
perfLog.debug() << tag() << "new Context created";
} }
Context(Context&&) = default;
Context&
operator=(Context&&) = default;
}; };
} // namespace Web } // namespace Web

View File

@@ -19,8 +19,10 @@
#pragma once #pragma once
#include <rpc/Errors.h>
#include <rpc/Factories.h> #include <rpc/Factories.h>
#include <rpc/RPCHelpers.h> #include <rpc/RPCHelpers.h>
#include <rpc/common/impl/APIVersionParser.h>
#include <util/Profiler.h> #include <util/Profiler.h>
#include <boost/json/parse.hpp> #include <boost/json/parse.hpp>
@@ -37,6 +39,7 @@ class RPCExecutor
// subscription manager holds the shared_ptr of this class // subscription manager holds the shared_ptr of this class
std::weak_ptr<SubscriptionManager> const subscriptions_; std::weak_ptr<SubscriptionManager> const subscriptions_;
util::TagDecoratorFactory const tagFactory_; util::TagDecoratorFactory const tagFactory_;
RPC::detail::ProductionAPIVersionParser apiVersionParser_; // can be injected if needed
clio::Logger log_{"RPC"}; clio::Logger log_{"RPC"};
clio::Logger perfLog_{"Performance"}; clio::Logger perfLog_{"Performance"};
@@ -48,7 +51,12 @@ public:
std::shared_ptr<Engine> const& rpcEngine, std::shared_ptr<Engine> const& rpcEngine,
std::shared_ptr<ETL const> const& etl, std::shared_ptr<ETL const> const& etl,
std::shared_ptr<SubscriptionManager> const& subscriptions) std::shared_ptr<SubscriptionManager> const& subscriptions)
: backend_(backend), rpcEngine_(rpcEngine), etl_(etl), subscriptions_(subscriptions), tagFactory_(config) : backend_(backend)
, rpcEngine_(rpcEngine)
, etl_(etl)
, subscriptions_(subscriptions)
, tagFactory_(config)
, apiVersionParser_(config.sectionOr("api_version", {}))
{ {
} }
@@ -136,29 +144,39 @@ private:
try try
{ {
auto const range = backend_->fetchLedgerRange(); auto const range = backend_->fetchLedgerRange();
// for the error happened before the handler, we don't attach the clio warning
if (!range) if (!range)
{ {
// for error that happened before the handler, we don't attach any warnings
rpcEngine_->notifyNotReady(); rpcEngine_->notifyNotReady();
return connection->send( return connection->send(
boost::json::serialize(composeError(RPC::RippledError::rpcNOT_READY)), boost::json::serialize(composeError(RPC::RippledError::rpcNOT_READY)),
boost::beast::http::status::ok); boost::beast::http::status::ok);
} }
auto context = connection->upgraded auto context = connection->upgraded ? RPC::make_WsContext(
? RPC::make_WsContext( yc,
yc, request, connection, tagFactory_.with(connection->tag()), *range, connection->clientIp) request,
: RPC::make_HttpContext(yc, request, tagFactory_.with(connection->tag()), *range, connection->clientIp); connection,
tagFactory_.with(connection->tag()),
*range,
connection->clientIp,
std::cref(apiVersionParser_))
: RPC::make_HttpContext(
yc,
request,
tagFactory_.with(connection->tag()),
*range,
connection->clientIp,
std::cref(apiVersionParser_));
if (!context) if (!context)
{ {
perfLog_.warn() << connection->tag() << "Could not create RPC context"; auto const err = context.error();
log_.warn() << connection->tag() << "Could not create RPC context"; perfLog_.warn() << connection->tag() << "Could not create Web context: " << err;
log_.warn() << connection->tag() << "Could not create Web context: " << err;
rpcEngine_->notifyBadSyntax(); rpcEngine_->notifyBadSyntax();
return connection->send( return connection->send(boost::json::serialize(composeError(err)), boost::beast::http::status::ok);
boost::json::serialize(composeError(RPC::RippledError::rpcBAD_SYNTAX)),
boost::beast::http::status::ok);
} }
auto [v, timeDiff] = util::timed([&]() { return rpcEngine_->buildResponse(*context); }); auto [v, timeDiff] = util::timed([&]() { return rpcEngine_->buildResponse(*context); });

View File

@@ -153,6 +153,21 @@ TEST_F(ConfigTest, Section)
ASSERT_EQ(sub.value<bool>("bool"), true); ASSERT_EQ(sub.value<bool>("bool"), true);
} }
TEST_F(ConfigTest, SectionOr)
{
{
auto sub = cfg.sectionOr("section.test", {}); // exists
ASSERT_EQ(sub.value<string>("str"), "hello");
ASSERT_EQ(sub.value<int64_t>("int"), 9042);
ASSERT_EQ(sub.value<bool>("bool"), true);
}
{
auto sub = cfg.sectionOr("section.doesnotexist", {{"int", 9043}}); // does not exist
ASSERT_EQ(sub.value<int64_t>("int"), 9043); // default from fallback
}
}
TEST_F(ConfigTest, Array) TEST_F(ConfigTest, Array)
{ {
auto arr = cfg.array("arr"); auto arr = cfg.array("arr");

View File

@@ -0,0 +1,137 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2023, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <util/Fixtures.h>
#include <rpc/common/impl/APIVersionParser.h>
#include <boost/json/parse.hpp>
#include <fmt/core.h>
#include <gtest/gtest.h>
constexpr static auto DEFAULT_API_VERSION = 5u;
constexpr static auto MIN_API_VERSION = 2u;
constexpr static auto MAX_API_VERSION = 10u;
using namespace RPC::detail;
namespace json = boost::json;
class RPCAPIVersionTest : public NoLoggerFixture
{
protected:
ProductionAPIVersionParser parser{DEFAULT_API_VERSION, MIN_API_VERSION, MAX_API_VERSION};
};
TEST_F(RPCAPIVersionTest, ReturnsDefaultVersionIfNotSpecified)
{
auto ver = parser.parse(json::parse("{}").as_object());
EXPECT_TRUE(ver);
EXPECT_EQ(ver.value(), DEFAULT_API_VERSION);
}
TEST_F(RPCAPIVersionTest, ReturnsErrorIfVersionHigherThanMaxSupported)
{
auto ver = parser.parse(json::parse(R"({"api_version": 11})").as_object());
EXPECT_FALSE(ver);
}
TEST_F(RPCAPIVersionTest, ReturnsErrorIfVersionLowerThanMinSupported)
{
auto ver = parser.parse(json::parse(R"({"api_version": 1})").as_object());
EXPECT_FALSE(ver);
}
TEST_F(RPCAPIVersionTest, ReturnsErrorOnWrongType)
{
{
auto ver = parser.parse(json::parse(R"({"api_version": null})").as_object());
EXPECT_FALSE(ver);
}
{
auto ver = parser.parse(json::parse(R"({"api_version": "5"})").as_object());
EXPECT_FALSE(ver);
}
{
auto ver = parser.parse(json::parse(R"({"api_version": "wrong"})").as_object());
EXPECT_FALSE(ver);
}
}
TEST_F(RPCAPIVersionTest, ReturnsParsedVersionIfAllPreconditionsAreMet)
{
{
auto ver = parser.parse(json::parse(R"({"api_version": 2})").as_object());
EXPECT_TRUE(ver);
EXPECT_EQ(ver.value(), 2u);
}
{
auto ver = parser.parse(json::parse(R"({"api_version": 10})").as_object());
EXPECT_TRUE(ver);
EXPECT_EQ(ver.value(), 10u);
}
{
auto ver = parser.parse(json::parse(R"({"api_version": 5})").as_object());
EXPECT_TRUE(ver);
EXPECT_EQ(ver.value(), 5u);
}
}
TEST_F(RPCAPIVersionTest, GetsValuesFromConfigCorrectly)
{
clio::Config cfg{json::parse(fmt::format(
R"({{
"min": {},
"max": {},
"default": {}
}})",
MIN_API_VERSION,
MAX_API_VERSION,
DEFAULT_API_VERSION))};
ProductionAPIVersionParser configuredParser{cfg};
{
auto ver = configuredParser.parse(json::parse(R"({"api_version": 2})").as_object());
EXPECT_TRUE(ver);
EXPECT_EQ(ver.value(), 2u);
}
{
auto ver = configuredParser.parse(json::parse(R"({"api_version": 10})").as_object());
EXPECT_TRUE(ver);
EXPECT_EQ(ver.value(), 10u);
}
{
auto ver = configuredParser.parse(json::parse(R"({"api_version": 5})").as_object());
EXPECT_TRUE(ver);
EXPECT_EQ(ver.value(), 5u);
}
{
auto ver = configuredParser.parse(json::parse(R"({})").as_object());
EXPECT_TRUE(ver);
EXPECT_EQ(ver.value(), DEFAULT_API_VERSION);
}
{
auto ver = configuredParser.parse(json::parse(R"({"api_version": 11})").as_object());
EXPECT_FALSE(ver);
}
{
auto ver = configuredParser.parse(json::parse(R"({"api_version": 1})").as_object());
EXPECT_FALSE(ver);
}
}

View File

@@ -33,47 +33,53 @@ using namespace unittests::detail;
namespace json = boost::json; namespace json = boost::json;
class RPCDefaultProcessorTest : public NoLoggerFixture class RPCDefaultProcessorTest : public HandlerBaseTest
{ {
}; };
TEST_F(RPCDefaultProcessorTest, ValidInput) TEST_F(RPCDefaultProcessorTest, ValidInput)
{ {
HandlerMock handler; runSpawn([](auto& yield) {
RPC::detail::DefaultProcessor<HandlerMock> processor; HandlerMock handler;
RPC::detail::DefaultProcessor<HandlerMock> processor;
auto const input = json::parse(R"({ "something": "works" })"); auto const input = json::parse(R"({ "something": "works" })");
auto const spec = RpcSpec{{"something", Required{}}}; auto const spec = RpcSpec{{"something", Required{}}};
auto const data = InOutFake{"works"}; auto const data = InOutFake{"works"};
EXPECT_CALL(handler, spec()).WillOnce(ReturnRef(spec)); EXPECT_CALL(handler, spec(_)).WillOnce(ReturnRef(spec));
EXPECT_CALL(handler, process(Eq(data))).WillOnce(Return(data)); EXPECT_CALL(handler, process(Eq(data), _)).WillOnce(Return(data));
auto const ret = processor(handler, input); auto const ret = processor(handler, input, Context{std::ref(yield)});
ASSERT_TRUE(ret); // no error ASSERT_TRUE(ret); // no error
});
} }
TEST_F(RPCDefaultProcessorTest, NoInputVaildCall) TEST_F(RPCDefaultProcessorTest, NoInputVaildCall)
{ {
HandlerWithoutInputMock handler; runSpawn([](auto& yield) {
RPC::detail::DefaultProcessor<HandlerWithoutInputMock> processor; HandlerWithoutInputMock handler;
RPC::detail::DefaultProcessor<HandlerWithoutInputMock> processor;
auto const data = InOutFake{"works"}; auto const data = InOutFake{"works"};
auto const input = json::parse(R"({})"); auto const input = json::parse(R"({})");
EXPECT_CALL(handler, process()).WillOnce(Return(data)); EXPECT_CALL(handler, process(_)).WillOnce(Return(data));
auto const ret = processor(handler, input); auto const ret = processor(handler, input, Context{std::ref(yield)});
ASSERT_TRUE(ret); // no error ASSERT_TRUE(ret); // no error
});
} }
TEST_F(RPCDefaultProcessorTest, InvalidInput) TEST_F(RPCDefaultProcessorTest, InvalidInput)
{ {
HandlerMock handler; runSpawn([](auto& yield) {
RPC::detail::DefaultProcessor<HandlerMock> processor; HandlerMock handler;
RPC::detail::DefaultProcessor<HandlerMock> processor;
auto const input = json::parse(R"({ "other": "nope" })"); auto const input = json::parse(R"({ "other": "nope" })");
auto const spec = RpcSpec{{"something", Required{}}}; auto const spec = RpcSpec{{"something", Required{}}};
EXPECT_CALL(handler, spec()).WillOnce(ReturnRef(spec)); EXPECT_CALL(handler, spec(_)).WillOnce(ReturnRef(spec));
auto const ret = processor(handler, input); auto const ret = processor(handler, input, Context{std::ref(yield)});
ASSERT_FALSE(ret); // returns error ASSERT_FALSE(ret); // returns error
});
} }

View File

@@ -36,25 +36,29 @@ class RPCLedgerRangeTest : public HandlerBaseTest
TEST_F(RPCLedgerRangeTest, LedgerRangeMinMaxSame) TEST_F(RPCLedgerRangeTest, LedgerRangeMinMaxSame)
{ {
mockBackendPtr->updateRange(RANGEMIN); runSpawn([this](auto& yield) {
auto const handler = AnyHandler{LedgerRangeHandler{mockBackendPtr}}; mockBackendPtr->updateRange(RANGEMIN);
auto const req = json::parse("{}"); auto const handler = AnyHandler{LedgerRangeHandler{mockBackendPtr}};
auto const output = handler.process(req); auto const req = json::parse("{}");
ASSERT_TRUE(output); auto const output = handler.process(req, Context{std::ref(yield)});
auto const json = output.value(); ASSERT_TRUE(output);
EXPECT_EQ(json.at("ledger_index_min").as_uint64(), RANGEMIN); auto const json = output.value();
EXPECT_EQ(json.at("ledger_index_max").as_uint64(), RANGEMIN); EXPECT_EQ(json.at("ledger_index_min").as_uint64(), RANGEMIN);
EXPECT_EQ(json.at("ledger_index_max").as_uint64(), RANGEMIN);
});
} }
TEST_F(RPCLedgerRangeTest, LedgerRangeFullySet) TEST_F(RPCLedgerRangeTest, LedgerRangeFullySet)
{ {
mockBackendPtr->updateRange(RANGEMIN); runSpawn([this](auto& yield) {
mockBackendPtr->updateRange(RANGEMAX); mockBackendPtr->updateRange(RANGEMIN);
auto const handler = AnyHandler{LedgerRangeHandler{mockBackendPtr}}; mockBackendPtr->updateRange(RANGEMAX);
auto const req = json::parse("{}"); auto const handler = AnyHandler{LedgerRangeHandler{mockBackendPtr}};
auto const output = handler.process(req); auto const req = json::parse("{}");
ASSERT_TRUE(output); auto const output = handler.process(req, Context{std::ref(yield)});
auto const json = output.value(); ASSERT_TRUE(output);
EXPECT_EQ(json.at("ledger_index_min").as_uint64(), RANGEMIN); auto const json = output.value();
EXPECT_EQ(json.at("ledger_index_max").as_uint64(), RANGEMAX); EXPECT_EQ(json.at("ledger_index_min").as_uint64(), RANGEMIN);
EXPECT_EQ(json.at("ledger_index_max").as_uint64(), RANGEMAX);
});
} }

View File

@@ -23,15 +23,17 @@
using namespace RPC; using namespace RPC;
class RPCPingHandlerTest : public NoLoggerFixture class RPCPingHandlerTest : public HandlerBaseTest
{ {
}; };
// example handler tests // example handler tests
TEST_F(RPCPingHandlerTest, Default) TEST_F(RPCPingHandlerTest, Default)
{ {
auto const handler = AnyHandler{PingHandler{}}; runSpawn([](auto& yield) {
auto const output = handler.process(boost::json::parse(R"({})")); auto const handler = AnyHandler{PingHandler{}};
ASSERT_TRUE(output); auto const output = handler.process(boost::json::parse(R"({})"), Context{std::ref(yield)});
EXPECT_EQ(output.value(), boost::json::parse(R"({})")); ASSERT_TRUE(output);
EXPECT_EQ(output.value(), boost::json::parse(R"({})"));
});
} }

View File

@@ -24,15 +24,17 @@
using namespace RPC; using namespace RPC;
class RPCRandomHandlerTest : public NoLoggerFixture class RPCRandomHandlerTest : public HandlerBaseTest
{ {
}; };
TEST_F(RPCRandomHandlerTest, Default) TEST_F(RPCRandomHandlerTest, Default)
{ {
auto const handler = AnyHandler{RandomHandler{}}; runSpawn([](auto& yield) {
auto const output = handler.process(boost::json::parse(R"({})")); auto const handler = AnyHandler{RandomHandler{}};
ASSERT_TRUE(output); auto const output = handler.process(boost::json::parse(R"({})"), Context{std::ref(yield)});
EXPECT_TRUE(output->as_object().contains(JS(random))); ASSERT_TRUE(output);
EXPECT_EQ(output->as_object().at(JS(random)).as_string().size(), 64u); EXPECT_TRUE(output->as_object().contains(JS(random)));
EXPECT_EQ(output->as_object().at(JS(random)).as_string().size(), 64u);
});
} }

View File

@@ -31,83 +31,73 @@ using namespace unittests::detail;
namespace json = boost::json; namespace json = boost::json;
class RPCTestHandlerTest : public NoLoggerFixture class RPCTestHandlerTest : public HandlerBaseTest
{ {
}; };
// example handler tests // example handler tests
TEST_F(RPCTestHandlerTest, HandlerSuccess) TEST_F(RPCTestHandlerTest, HandlerSuccess)
{ {
auto const handler = AnyHandler{HandlerFake{}}; runSpawn([](auto& yield) {
auto const input = json::parse(R"({ auto const handler = AnyHandler{HandlerFake{}};
"hello": "world", auto const input = json::parse(R"({
"limit": 10 "hello": "world",
})"); "limit": 10
})");
auto const output = handler.process(input);
ASSERT_TRUE(output);
auto const val = output.value();
EXPECT_EQ(val.as_object().at("computed").as_string(), "world_10");
}
TEST_F(RPCTestHandlerTest, CoroutineHandlerSuccess)
{
auto const handler = AnyHandler{CoroutineHandlerFake{}};
auto const input = json::parse(R"({
"hello": "world",
"limit": 10
})");
boost::asio::io_context ctx;
boost::asio::spawn(ctx, [&](boost::asio::yield_context yield) {
auto const output = handler.process(input, Context{std::ref(yield)}); auto const output = handler.process(input, Context{std::ref(yield)});
ASSERT_TRUE(output); ASSERT_TRUE(output);
auto const val = output.value(); auto const val = output.value();
EXPECT_EQ(val.as_object().at("computed").as_string(), "world_10"); EXPECT_EQ(val.as_object().at("computed").as_string(), "world_10");
}); });
ctx.run();
} }
TEST_F(RPCTestHandlerTest, NoInputHandlerSuccess) TEST_F(RPCTestHandlerTest, NoInputHandlerSuccess)
{ {
auto const handler = AnyHandler{NoInputHandlerFake{}}; runSpawn([](auto& yield) {
auto const output = handler.process(json::parse(R"({})")); auto const handler = AnyHandler{NoInputHandlerFake{}};
ASSERT_TRUE(output); auto const output = handler.process(json::parse(R"({})"), Context{std::ref(yield)});
ASSERT_TRUE(output);
auto const val = output.value(); auto const val = output.value();
EXPECT_EQ(val.as_object().at("computed").as_string(), "test"); EXPECT_EQ(val.as_object().at("computed").as_string(), "test");
});
} }
TEST_F(RPCTestHandlerTest, HandlerErrorHandling) TEST_F(RPCTestHandlerTest, HandlerErrorHandling)
{ {
auto const handler = AnyHandler{HandlerFake{}}; runSpawn([](auto& yield) {
auto const input = json::parse(R"({ auto const handler = AnyHandler{HandlerFake{}};
"hello": "not world", auto const input = json::parse(R"({
"limit": 10 "hello": "not world",
})"); "limit": 10
})");
auto const output = handler.process(input); auto const output = handler.process(input, Context{std::ref(yield)});
ASSERT_FALSE(output); ASSERT_FALSE(output);
auto const err = RPC::makeError(output.error()); auto const err = RPC::makeError(output.error());
EXPECT_EQ(err.at("error").as_string(), "invalidParams"); EXPECT_EQ(err.at("error").as_string(), "invalidParams");
EXPECT_EQ(err.at("error_message").as_string(), "Invalid parameters."); EXPECT_EQ(err.at("error_message").as_string(), "Invalid parameters.");
EXPECT_EQ(err.at("error_code").as_uint64(), 31); EXPECT_EQ(err.at("error_code").as_uint64(), 31);
});
} }
TEST_F(RPCTestHandlerTest, HandlerInnerErrorHandling) TEST_F(RPCTestHandlerTest, HandlerInnerErrorHandling)
{ {
auto const handler = AnyHandler{FailingHandlerFake{}}; runSpawn([](auto& yield) {
auto const input = json::parse(R"({ auto const handler = AnyHandler{FailingHandlerFake{}};
"hello": "world", auto const input = json::parse(R"({
"limit": 10 "hello": "world",
})"); "limit": 10
})");
// validation succeeds but handler itself returns error // validation succeeds but handler itself returns error
auto const output = handler.process(input); auto const output = handler.process(input, Context{std::ref(yield)});
ASSERT_FALSE(output); ASSERT_FALSE(output);
auto const err = RPC::makeError(output.error()); auto const err = RPC::makeError(output.error());
EXPECT_EQ(err.at("error").as_string(), "Very custom error"); EXPECT_EQ(err.at("error").as_string(), "Very custom error");
});
} }

View File

@@ -73,7 +73,7 @@ public:
using Result = RPC::HandlerReturnType<Output>; using Result = RPC::HandlerReturnType<Output>;
RPC::RpcSpecConstRef RPC::RpcSpecConstRef
spec() const spec([[maybe_unused]] uint32_t apiVersion) const
{ {
using namespace RPC::validation; using namespace RPC::validation;
@@ -86,35 +86,7 @@ public:
} }
Result Result
process(Input input) const process(Input input, [[maybe_unused]] RPC::Context const& ctx) const
{
return Output{input.hello + '_' + std::to_string(input.limit.value_or(0))};
}
};
// example handler
class CoroutineHandlerFake
{
public:
using Input = TestInput;
using Output = TestOutput;
using Result = RPC::HandlerReturnType<Output>;
RPC::RpcSpecConstRef
spec() const
{
using namespace RPC::validation;
static const auto rpcSpec = RPC::RpcSpec{
{"hello", Required{}, Type<std::string>{}, EqualTo{"world"}},
{"limit", Type<uint32_t>{}, Between<uint32_t>{0, 100}}, // optional field
};
return rpcSpec;
}
Result
process(Input input, RPC::Context const& ctx) const
{ {
return Output{input.hello + '_' + std::to_string(input.limit.value_or(0))}; return Output{input.hello + '_' + std::to_string(input.limit.value_or(0))};
} }
@@ -127,7 +99,7 @@ public:
using Result = RPC::HandlerReturnType<Output>; using Result = RPC::HandlerReturnType<Output>;
Result Result
process() const process([[maybe_unused]] RPC::Context const& ctx) const
{ {
return Output{"test"}; return Output{"test"};
} }
@@ -142,7 +114,7 @@ public:
using Result = RPC::HandlerReturnType<Output>; using Result = RPC::HandlerReturnType<Output>;
RPC::RpcSpecConstRef RPC::RpcSpecConstRef
spec() const spec([[maybe_unused]] uint32_t apiVersion) const
{ {
using namespace RPC::validation; using namespace RPC::validation;
@@ -155,7 +127,7 @@ public:
} }
Result Result
process([[maybe_unused]] Input input) const process([[maybe_unused]] Input input, [[maybe_unused]] RPC::Context const& ctx) const
{ {
// always fail // always fail
return RPC::Error{RPC::Status{"Very custom error"}}; return RPC::Error{RPC::Status{"Very custom error"}};
@@ -191,8 +163,8 @@ struct HandlerMock
using Output = InOutFake; using Output = InOutFake;
using Result = RPC::HandlerReturnType<Output>; using Result = RPC::HandlerReturnType<Output>;
MOCK_METHOD(RPC::RpcSpecConstRef, spec, (), (const)); MOCK_METHOD(RPC::RpcSpecConstRef, spec, (uint32_t), (const));
MOCK_METHOD(Result, process, (Input), (const)); MOCK_METHOD(Result, process, (Input, RPC::Context const&), (const));
}; };
struct HandlerWithoutInputMock struct HandlerWithoutInputMock
@@ -200,7 +172,7 @@ struct HandlerWithoutInputMock
using Output = InOutFake; using Output = InOutFake;
using Result = RPC::HandlerReturnType<Output>; using Result = RPC::HandlerReturnType<Output>;
MOCK_METHOD(Result, process, (), (const)); MOCK_METHOD(Result, process, (RPC::Context const&), (const));
}; };
} // namespace unittests::detail } // namespace unittests::detail

View File

@@ -378,6 +378,71 @@ TEST_F(WebRPCExecutorTest, WsNotReady)
EXPECT_EQ(boost::json::parse(session->message), boost::json::parse(response)); EXPECT_EQ(boost::json::parse(session->message), boost::json::parse(response));
} }
TEST_F(WebRPCExecutorTest, HTTPInvalidAPIVersion)
{
static auto constexpr request = R"({
"method": "server_info",
"params": [{
"api_version": null
}]
})";
mockBackendPtr->updateRange(MINSEQ); // min
mockBackendPtr->updateRange(MAXSEQ); // max
static auto constexpr response = R"({
"result": {
"error": "invalid_API_version",
"error_code": 6000,
"error_message": "API version must be an integer",
"status": "error",
"type": "response",
"request": {
"method": "server_info",
"params": [{
"api_version": null
}]
}
}
})";
EXPECT_CALL(*rpcEngine, notifyBadSyntax).Times(1);
(*rpcExecutor)(std::move(request), session);
std::this_thread::sleep_for(200ms);
EXPECT_EQ(boost::json::parse(session->message), boost::json::parse(response));
}
TEST_F(WebRPCExecutorTest, WSInvalidAPIVersion)
{
session->upgraded = true;
static auto constexpr request = R"({
"method": "server_info",
"api_version": null
})";
mockBackendPtr->updateRange(MINSEQ); // min
mockBackendPtr->updateRange(MAXSEQ); // max
static auto constexpr response = R"({
"error": "invalid_API_version",
"error_code": 6000,
"error_message": "API version must be an integer",
"status": "error",
"type": "response",
"request": {
"method": "server_info",
"api_version": null
}
})";
EXPECT_CALL(*rpcEngine, notifyBadSyntax).Times(1);
(*rpcExecutor)(std::move(request), session);
std::this_thread::sleep_for(200ms);
EXPECT_EQ(boost::json::parse(session->message), boost::json::parse(response));
}
TEST_F(WebRPCExecutorTest, HTTPBadSyntax) TEST_F(WebRPCExecutorTest, HTTPBadSyntax)
{ {
static auto constexpr request = R"({"method2": "server_info"})"; static auto constexpr request = R"({"method2": "server_info"})";
@@ -389,7 +454,7 @@ TEST_F(WebRPCExecutorTest, HTTPBadSyntax)
"result":{ "result":{
"error": "badSyntax", "error": "badSyntax",
"error_code": 1, "error_code": 1,
"error_message": "Syntax error.", "error_message": "Method is not specified or is not a string.",
"status": "error", "status": "error",
"type": "response", "type": "response",
"request": { "request": {
@@ -417,7 +482,7 @@ TEST_F(WebRPCExecutorTest, HTTPBadSyntaxWhenRequestSubscribe)
"result": { "result": {
"error": "badSyntax", "error": "badSyntax",
"error_code": 1, "error_code": 1,
"error_message": "Syntax error.", "error_message": "Subscribe and unsubscribe are only allowed or websocket.",
"status": "error", "status": "error",
"type": "response", "type": "response",
"request": { "request": {
@@ -448,7 +513,7 @@ TEST_F(WebRPCExecutorTest, WsBadSyntax)
static auto constexpr response = R"({ static auto constexpr response = R"({
"error": "badSyntax", "error": "badSyntax",
"error_code": 1, "error_code": 1,
"error_message": "Syntax error.", "error_message": "Method/Command is not specified or is not a string.",
"status": "error", "status": "error",
"type": "response", "type": "response",
"id": 99, "id": 99,