mirror of
https://github.com/XRPLF/clio.git
synced 2025-11-04 11:55:51 +00:00
@@ -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)
|
||||||
|
|||||||
20
README.md
20
README.md
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
59
src/rpc/common/APIVersion.h
Normal file
59
src/rpc/common/APIVersion.h
Normal 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
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 =
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|||||||
61
src/rpc/common/impl/APIVersionParser.cpp
Normal file
61
src/rpc/common/impl/APIVersionParser.cpp
Normal 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
|
||||||
79
src/rpc/common/impl/APIVersionParser.h
Normal file
79
src/rpc/common/impl/APIVersionParser.h
Normal 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
|
||||||
@@ -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_;
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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},
|
||||||
|
|||||||
@@ -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},
|
||||||
|
|||||||
@@ -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},
|
||||||
|
|||||||
@@ -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},
|
||||||
|
|||||||
@@ -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},
|
||||||
|
|||||||
@@ -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},
|
||||||
|
|||||||
@@ -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},
|
||||||
|
|||||||
@@ -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},
|
||||||
|
|||||||
@@ -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},
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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}},
|
||||||
|
|||||||
@@ -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>{}},
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()};
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
Result
|
Result
|
||||||
process() const;
|
process(Context const& ctx) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
friend void
|
friend void
|
||||||
|
|||||||
@@ -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},
|
||||||
|
|||||||
@@ -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},
|
||||||
|
|||||||
@@ -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},
|
||||||
|
|||||||
@@ -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},
|
||||||
|
|||||||
@@ -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{};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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},
|
||||||
|
|||||||
@@ -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},
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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); });
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
137
unittests/rpc/APIVersionTests.cpp
Normal file
137
unittests/rpc/APIVersionTests.cpp
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"({})"));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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");
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user