mirror of
https://github.com/XRPLF/clio.git
synced 2025-11-04 11:55:51 +00:00
Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3555b7d998 | ||
|
|
8a418bfe00 | ||
|
|
dc1b146729 | ||
|
|
47c0a6a297 | ||
|
|
44df7bf966 | ||
|
|
8a5a984d51 | ||
|
|
9ca4c7afd3 | ||
|
|
40be25a68c | ||
|
|
9fc8846f6a | ||
|
|
d001e35427 | ||
|
|
592af70f03 | ||
|
|
33cf336964 | ||
|
|
16e07b90db | ||
|
|
39419c8b58 | ||
|
|
e38658a0d6 | ||
|
|
fb98a6a394 | ||
|
|
b8a8248c42 | ||
|
|
a092b7ae08 | ||
|
|
07438a2e02 | ||
|
|
09aa688de4 | ||
|
|
a7bff26fd6 | ||
|
|
081adf1cae | ||
|
|
ffc9deb0f8 | ||
|
|
717a29ecdf | ||
|
|
e8db74456a | ||
|
|
4947a83696 | ||
|
|
164387cab0 | ||
|
|
b8f1deb90f | ||
|
|
5c77e59374 | ||
|
|
6d070132c7 | ||
|
|
d2dda69448 | ||
|
|
e2aeaa0956 | ||
|
|
2951b4aaa0 | ||
|
|
6c3c761dd1 | ||
|
|
527020680a | ||
|
|
401448f771 | ||
|
|
0f12a6d7f2 | ||
|
|
5c8fc939f2 | ||
|
|
b1be848098 | ||
|
|
41aabbfcce | ||
|
|
c00d25aa6b | ||
|
|
8d5c588e35 |
@@ -26,12 +26,12 @@ sources="src tests"
|
||||
formatter="clang-format -i"
|
||||
version=$($formatter --version | grep -o '[0-9\.]*')
|
||||
|
||||
if [[ "18.0.0" > "$version" ]]; then
|
||||
if [[ "19.0.0" > "$version" ]]; then
|
||||
cat <<EOF
|
||||
|
||||
ERROR
|
||||
-----------------------------------------------------------------------------
|
||||
A minimum of version 18 of `which clang-format` is required.
|
||||
A minimum of version 19 of `which clang-format` is required.
|
||||
Your version is $version.
|
||||
Please fix paths and run again.
|
||||
-----------------------------------------------------------------------------
|
||||
|
||||
16
.github/workflows/build.yml
vendored
16
.github/workflows/build.yml
vendored
@@ -74,7 +74,7 @@ jobs:
|
||||
conan_profile: clang
|
||||
code_coverage: false
|
||||
static: true
|
||||
- os: macos14
|
||||
- os: macos15
|
||||
build_type: Release
|
||||
code_coverage: false
|
||||
static: false
|
||||
@@ -149,13 +149,6 @@ jobs:
|
||||
name: clio_tests_${{ runner.os }}_${{ matrix.build_type }}_${{ steps.conan.outputs.conan_profile }}
|
||||
path: build/clio_*tests
|
||||
|
||||
- name: Upload test data
|
||||
if: ${{ !matrix.code_coverage }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: clio_test_data_${{ runner.os }}_${{ matrix.build_type }}_${{ steps.conan.outputs.conan_profile }}
|
||||
path: build/tests/unit/test_data
|
||||
|
||||
- name: Save cache
|
||||
uses: ./.github/actions/save_cache
|
||||
with:
|
||||
@@ -204,7 +197,7 @@ jobs:
|
||||
image: rippleci/clio_ci:latest
|
||||
conan_profile: clang
|
||||
build_type: Debug
|
||||
- os: macos14
|
||||
- os: macos15
|
||||
conan_profile: apple_clang_15
|
||||
build_type: Release
|
||||
runs-on: [self-hosted, "${{ matrix.os }}"]
|
||||
@@ -219,11 +212,6 @@ jobs:
|
||||
with:
|
||||
name: clio_tests_${{ runner.os }}_${{ matrix.build_type }}_${{ matrix.conan_profile }}
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: clio_test_data_${{ runner.os }}_${{ matrix.build_type }}_${{ matrix.conan_profile }}
|
||||
path: tests/unit/test_data
|
||||
|
||||
- name: Run clio_tests
|
||||
run: |
|
||||
chmod +x ./clio_tests
|
||||
|
||||
2
.github/workflows/check_pr_title.yml
vendored
2
.github/workflows/check_pr_title.yml
vendored
@@ -10,7 +10,7 @@ jobs:
|
||||
# permissions:
|
||||
# pull-requests: write
|
||||
steps:
|
||||
- uses: ytanikin/PRConventionalCommits@1.2.0
|
||||
- uses: ytanikin/PRConventionalCommits@1.3.0
|
||||
with:
|
||||
task_types: '["build","feat","fix","docs","test","ci","style","refactor","perf","chore"]'
|
||||
add_label: false
|
||||
|
||||
2
.github/workflows/clang-tidy.yml
vendored
2
.github/workflows/clang-tidy.yml
vendored
@@ -60,7 +60,7 @@ jobs:
|
||||
shell: bash
|
||||
id: run_clang_tidy
|
||||
run: |
|
||||
run-clang-tidy-18 -p build -j ${{ steps.number_of_threads.outputs.threads_number }} -fix -quiet 1>output.txt
|
||||
run-clang-tidy-19 -p build -j ${{ steps.number_of_threads.outputs.threads_number }} -fix -quiet 1>output.txt
|
||||
|
||||
- name: Check format
|
||||
if: ${{ steps.run_clang_tidy.outcome != 'success' }}
|
||||
|
||||
11
.github/workflows/nightly.yml
vendored
11
.github/workflows/nightly.yml
vendored
@@ -71,12 +71,6 @@ jobs:
|
||||
name: clio_tests_${{ runner.os }}_${{ matrix.build_type }}
|
||||
path: build/clio_*tests
|
||||
|
||||
- name: Upload test data
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: clio_test_data_${{ runner.os }}_${{ matrix.build_type }}
|
||||
path: build/tests/unit/test_data
|
||||
|
||||
- name: Compress clio_server
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -130,11 +124,6 @@ jobs:
|
||||
with:
|
||||
name: clio_tests_${{ runner.os }}_${{ matrix.build_type }}
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: clio_test_data_${{ runner.os }}_${{ matrix.build_type }}
|
||||
path: tests/unit/test_data
|
||||
|
||||
- name: Run clio_tests
|
||||
run: |
|
||||
chmod +x ./clio_tests
|
||||
|
||||
2
.github/workflows/upload_coverage_report.yml
vendored
2
.github/workflows/upload_coverage_report.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
|
||||
- name: Upload coverage report
|
||||
if: ${{ hashFiles('build/coverage_report.xml') != '' }}
|
||||
uses: wandalen/wretry.action@v3.5.0
|
||||
uses: wandalen/wretry.action@v3.7.2
|
||||
with:
|
||||
action: codecov/codecov-action@v4
|
||||
with: |
|
||||
|
||||
@@ -21,7 +21,7 @@ git config --local core.hooksPath .githooks
|
||||
```
|
||||
|
||||
## Git hooks dependencies
|
||||
The pre-commit hook requires `clang-format >= 18.0.0` and `cmake-format` to be installed on your machine.
|
||||
The pre-commit hook requires `clang-format >= 19.0.0` and `cmake-format` to be installed on your machine.
|
||||
`clang-format` can be installed using `brew` on macOS and default package manager on Linux.
|
||||
`cmake-format` can be installed using `pip`.
|
||||
The hook will also attempt to automatically use `doxygen` to verify that everything public in the codebase is covered by doc comments. If `doxygen` is not installed, the hook will raise a warning suggesting to install `doxygen` for future commits.
|
||||
@@ -105,7 +105,7 @@ The button for that is near the bottom of the PR's page on GitHub.
|
||||
This is a non-exhaustive list of recommended style guidelines. These are not always strictly enforced and serve as a way to keep the codebase coherent.
|
||||
|
||||
## Formatting
|
||||
Code must conform to `clang-format` version 18, unless the result would be unreasonably difficult to read or maintain.
|
||||
Code must conform to `clang-format` version 19, unless the result would be unreasonably difficult to read or maintain.
|
||||
In most cases the pre-commit hook will take care of formatting and will fix any issues automatically.
|
||||
To manually format your code, use `clang-format -i <your changed files>` for C++ files and `cmake-format -i <your changed files>` for CMake files.
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ if (lint)
|
||||
endif ()
|
||||
message(STATUS "Using clang-tidy from CLIO_CLANG_TIDY_BIN")
|
||||
else ()
|
||||
find_program(_CLANG_TIDY_BIN NAMES "clang-tidy-18" "clang-tidy" REQUIRED)
|
||||
find_program(_CLANG_TIDY_BIN NAMES "clang-tidy-19" "clang-tidy" REQUIRED)
|
||||
endif ()
|
||||
|
||||
if (NOT _CLANG_TIDY_BIN)
|
||||
|
||||
@@ -28,7 +28,8 @@ class Clio(ConanFile):
|
||||
'protobuf/3.21.9',
|
||||
'grpc/1.50.1',
|
||||
'openssl/1.1.1u',
|
||||
'xrpl/2.3.0-b4',
|
||||
'xrpl/2.3.1',
|
||||
'zlib/1.3.1',
|
||||
'libbacktrace/cci.20210118'
|
||||
]
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ USER root
|
||||
WORKDIR /root
|
||||
|
||||
ENV CCACHE_VERSION=4.10.2 \
|
||||
LLVM_TOOLS_VERSION=18 \
|
||||
LLVM_TOOLS_VERSION=19 \
|
||||
GH_VERSION=2.40.0 \
|
||||
DOXYGEN_VERSION=1.12.0
|
||||
|
||||
|
||||
@@ -39,6 +39,9 @@
|
||||
"cache_timeout": 0.250, // in seconds, could be 0, which means no cache
|
||||
"request_timeout": 10.0 // time for Clio to wait for rippled to reply on a forwarded request (default is 10 seconds)
|
||||
},
|
||||
"rpc": {
|
||||
"cache_timeout": 0.5 // in seconds, could be 0, which means no cache for rpc
|
||||
},
|
||||
"dos_guard": {
|
||||
// Comma-separated list of IPs to exclude from rate limiting
|
||||
"whitelist": [
|
||||
@@ -67,7 +70,14 @@
|
||||
"admin_password": "xrp",
|
||||
// If local_admin is true, Clio will consider requests come from 127.0.0.1 as admin requests
|
||||
// It's true by default unless admin_password is set,'local_admin' : true and 'admin_password' can not be set at the same time
|
||||
"local_admin": false
|
||||
"local_admin": false,
|
||||
"processing_policy": "parallel", // Could be "sequent" or "parallel".
|
||||
// For sequent policy request from one client connection will be processed one by one and the next one will not be read before
|
||||
// the previous one is processed. For parallel policy Clio will take all requests and process them in parallel and
|
||||
// send a reply for each request whenever it is ready.
|
||||
"parallel_requests_limit": 10, // Optional parameter, used only if "processing_strategy" is "parallel". It limits the number of requests for one client connection processed in parallel. Infinite if not specified.
|
||||
// Max number of responses to queue up before sent successfully. If a client's waiting queue is too long, the server will close the connection.
|
||||
"ws_max_sending_queue_size": 1500
|
||||
},
|
||||
// Time in seconds for graceful shutdown. Defaults to 10 seconds. Not fully implemented yet.
|
||||
"graceful_period": 10.0,
|
||||
|
||||
@@ -14,7 +14,7 @@ You can find an example docker-compose file, with Prometheus and Grafana configs
|
||||
|
||||
## Using `clang-tidy` for static analysis
|
||||
|
||||
The minimum [clang-tidy](https://clang.llvm.org/extra/clang-tidy/) version required is 17.0.
|
||||
The minimum [clang-tidy](https://clang.llvm.org/extra/clang-tidy/) version required is 19.0.
|
||||
|
||||
Clang-tidy can be run by Cmake when building the project. To achieve this, you just need to provide the option `-o lint=True` for the `conan install` command:
|
||||
|
||||
@@ -26,5 +26,5 @@ By default Cmake will try to find `clang-tidy` automatically in your system.
|
||||
To force Cmake to use your desired binary, set the `CLIO_CLANG_TIDY_BIN` environment variable to the path of the `clang-tidy` binary. For example:
|
||||
|
||||
```sh
|
||||
export CLIO_CLANG_TIDY_BIN=/opt/homebrew/opt/llvm@17/bin/clang-tidy
|
||||
export CLIO_CLANG_TIDY_BIN=/opt/homebrew/opt/llvm@19/bin/clang-tidy
|
||||
```
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
add_subdirectory(util)
|
||||
add_subdirectory(data)
|
||||
add_subdirectory(etl)
|
||||
add_subdirectory(etlng)
|
||||
add_subdirectory(feed)
|
||||
add_subdirectory(rpc)
|
||||
add_subdirectory(web)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
add_library(clio_app)
|
||||
target_sources(clio_app PRIVATE CliArgs.cpp ClioApplication.cpp)
|
||||
|
||||
target_link_libraries(clio_app PUBLIC clio_etl clio_feed clio_web clio_rpc)
|
||||
target_link_libraries(clio_app PUBLIC clio_etl clio_etlng clio_feed clio_web clio_rpc)
|
||||
|
||||
@@ -121,12 +121,14 @@ ClioApplication::run()
|
||||
auto const handlerProvider = std::make_shared<rpc::impl::ProductionHandlerProvider const>(
|
||||
config_, backend, subscriptions, balancer, etl, amendmentCenter, counters
|
||||
);
|
||||
|
||||
using RPCEngineType = rpc::RPCEngine<etl::LoadBalancer, rpc::Counters>;
|
||||
auto const rpcEngine =
|
||||
rpc::RPCEngine::make_RPCEngine(backend, balancer, dosGuard, workQueue, counters, handlerProvider);
|
||||
RPCEngineType::make_RPCEngine(config_, backend, balancer, dosGuard, workQueue, counters, handlerProvider);
|
||||
|
||||
// Init the web server
|
||||
auto handler =
|
||||
std::make_shared<web::RPCServerHandler<rpc::RPCEngine, etl::ETLService>>(config_, backend, rpcEngine, etl);
|
||||
std::make_shared<web::RPCServerHandler<RPCEngineType, etl::ETLService>>(config_, backend, rpcEngine, etl);
|
||||
auto const httpServer = web::make_HttpServer(config_, ioc, dosGuard, handler);
|
||||
|
||||
// Blocks until stopped.
|
||||
|
||||
@@ -127,6 +127,10 @@ struct Amendments {
|
||||
REGISTER(fixInnerObjTemplate2);
|
||||
REGISTER(fixNFTokenPageLinks);
|
||||
REGISTER(InvariantsV1_1);
|
||||
REGISTER(MPTokensV1);
|
||||
REGISTER(fixAMMv1_2);
|
||||
REGISTER(AMMClawback);
|
||||
REGISTER(Credentials);
|
||||
|
||||
// Obsolete but supported by libxrpl
|
||||
REGISTER(CryptoConditionsSuite);
|
||||
|
||||
@@ -364,6 +364,25 @@ public:
|
||||
boost::asio::yield_context yield
|
||||
) const = 0;
|
||||
|
||||
/**
|
||||
* @brief Fetches all holders' balances for a MPTIssuanceID
|
||||
*
|
||||
* @param mptID MPTIssuanceID you wish you query.
|
||||
* @param limit Paging limit.
|
||||
* @param cursorIn Optional cursor to allow us to pick up from where we last left off.
|
||||
* @param ledgerSequence The ledger sequence to fetch for
|
||||
* @param yield Currently executing coroutine.
|
||||
* @return std::vector<Blob> of MPToken balances and an optional marker
|
||||
*/
|
||||
virtual MPTHoldersAndCursor
|
||||
fetchMPTHolders(
|
||||
ripple::uint192 const& mptID,
|
||||
std::uint32_t const limit,
|
||||
std::optional<ripple::AccountID> const& cursorIn,
|
||||
std::uint32_t const ledgerSequence,
|
||||
boost::asio::yield_context yield
|
||||
) const = 0;
|
||||
|
||||
/**
|
||||
* @brief Fetches a specific ledger object.
|
||||
*
|
||||
@@ -617,6 +636,14 @@ public:
|
||||
virtual void
|
||||
writeNFTTransactions(std::vector<NFTTransactionsData> const& data) = 0;
|
||||
|
||||
/**
|
||||
* @brief Write accounts that started holding onto a MPT.
|
||||
*
|
||||
* @param data A vector of MPT ID and account pairs
|
||||
*/
|
||||
virtual void
|
||||
writeMPTHolders(std::vector<MPTHolderData> const& data) = 0;
|
||||
|
||||
/**
|
||||
* @brief Write a new successor.
|
||||
*
|
||||
|
||||
@@ -547,6 +547,45 @@ public:
|
||||
return ret;
|
||||
}
|
||||
|
||||
MPTHoldersAndCursor
|
||||
fetchMPTHolders(
|
||||
ripple::uint192 const& mptID,
|
||||
std::uint32_t const limit,
|
||||
std::optional<ripple::AccountID> const& cursorIn,
|
||||
std::uint32_t const ledgerSequence,
|
||||
boost::asio::yield_context yield
|
||||
) const override
|
||||
{
|
||||
auto const holderEntries = executor_.read(
|
||||
yield, schema_->selectMPTHolders, mptID, cursorIn.value_or(ripple::AccountID(0)), Limit{limit}
|
||||
);
|
||||
|
||||
auto const& holderResults = holderEntries.value();
|
||||
if (not holderResults.hasRows()) {
|
||||
LOG(log_.debug()) << "No rows returned";
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<ripple::uint256> mptKeys;
|
||||
std::optional<ripple::AccountID> cursor;
|
||||
for (auto const [holder] : extract<ripple::AccountID>(holderResults)) {
|
||||
mptKeys.push_back(ripple::keylet::mptoken(mptID, holder).key);
|
||||
cursor = holder;
|
||||
}
|
||||
|
||||
auto mptObjects = doFetchLedgerObjects(mptKeys, ledgerSequence, yield);
|
||||
|
||||
auto it = std::remove_if(mptObjects.begin(), mptObjects.end(), [](Blob const& mpt) { return mpt.empty(); });
|
||||
|
||||
mptObjects.erase(it, mptObjects.end());
|
||||
|
||||
ASSERT(mptKeys.size() <= limit, "Number of keys can't exceed the limit");
|
||||
if (mptKeys.size() == limit)
|
||||
return {mptObjects, cursor};
|
||||
|
||||
return {mptObjects, {}};
|
||||
}
|
||||
|
||||
std::optional<Blob>
|
||||
doFetchLedgerObject(ripple::uint256 const& key, std::uint32_t const sequence, boost::asio::yield_context yield)
|
||||
const override
|
||||
@@ -905,6 +944,17 @@ public:
|
||||
executor_.write(std::move(statements));
|
||||
}
|
||||
|
||||
void
|
||||
writeMPTHolders(std::vector<MPTHolderData> const& data) override
|
||||
{
|
||||
std::vector<Statement> statements;
|
||||
statements.reserve(data.size());
|
||||
for (auto [mptId, holder] : data)
|
||||
statements.push_back(schema_->insertMPTHolder.bind(std::move(mptId), std::move(holder)));
|
||||
|
||||
executor_.write(std::move(statements));
|
||||
}
|
||||
|
||||
void
|
||||
startWrites() const override
|
||||
{
|
||||
|
||||
@@ -172,6 +172,14 @@ struct NFTsData {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Represents an MPT and holder pair
|
||||
*/
|
||||
struct MPTHolderData {
|
||||
ripple::uint192 mptID;
|
||||
ripple::AccountID holder;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Check whether the supplied object is an offer.
|
||||
*
|
||||
|
||||
@@ -233,6 +233,14 @@ struct NFTsAndCursor {
|
||||
std::optional<ripple::uint256> cursor;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Represents an array of MPTokens
|
||||
*/
|
||||
struct MPTHoldersAndCursor {
|
||||
std::vector<Blob> mptokens;
|
||||
std::optional<ripple::AccountID> cursor;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Stores a range of sequences as a min and max pair.
|
||||
*/
|
||||
|
||||
@@ -257,6 +257,19 @@ public:
|
||||
qualifiedTableName(settingsProvider_.get(), "nf_token_transactions")
|
||||
));
|
||||
|
||||
statements.emplace_back(fmt::format(
|
||||
R"(
|
||||
CREATE TABLE IF NOT EXISTS {}
|
||||
(
|
||||
mpt_id blob,
|
||||
holder blob,
|
||||
PRIMARY KEY (mpt_id, holder)
|
||||
)
|
||||
WITH CLUSTERING ORDER BY (holder ASC)
|
||||
)",
|
||||
qualifiedTableName(settingsProvider_.get(), "mp_token_holders")
|
||||
));
|
||||
|
||||
return statements;
|
||||
}();
|
||||
|
||||
@@ -393,6 +406,17 @@ public:
|
||||
));
|
||||
}();
|
||||
|
||||
PreparedStatement insertMPTHolder = [this]() {
|
||||
return handle_.get().prepare(fmt::format(
|
||||
R"(
|
||||
INSERT INTO {}
|
||||
(mpt_id, holder)
|
||||
VALUES (?, ?)
|
||||
)",
|
||||
qualifiedTableName(settingsProvider_.get(), "mp_token_holders")
|
||||
));
|
||||
}();
|
||||
|
||||
PreparedStatement insertLedgerHeader = [this]() {
|
||||
return handle_.get().prepare(fmt::format(
|
||||
R"(
|
||||
@@ -687,6 +711,20 @@ public:
|
||||
));
|
||||
}();
|
||||
|
||||
PreparedStatement selectMPTHolders = [this]() {
|
||||
return handle_.get().prepare(fmt::format(
|
||||
R"(
|
||||
SELECT holder
|
||||
FROM {}
|
||||
WHERE mpt_id = ?
|
||||
AND holder > ?
|
||||
ORDER BY holder ASC
|
||||
LIMIT ?
|
||||
)",
|
||||
qualifiedTableName(settingsProvider_.get(), "mp_token_holders")
|
||||
));
|
||||
}();
|
||||
|
||||
PreparedStatement selectLedgerByHash = [this]() {
|
||||
return handle_.get().prepare(fmt::format(
|
||||
R"(
|
||||
|
||||
@@ -106,9 +106,9 @@ public:
|
||||
using UintByteTupleType = std::tuple<uint32_t, ripple::uint256>;
|
||||
using ByteVectorType = std::vector<ripple::uint256>;
|
||||
|
||||
if constexpr (std::is_same_v<DecayedType, ripple::uint256>) {
|
||||
if constexpr (std::is_same_v<DecayedType, ripple::uint256> || std::is_same_v<DecayedType, ripple::uint192>) {
|
||||
auto const rc = bindBytes(value.data(), value.size());
|
||||
throwErrorIfNeeded(rc, "Bind ripple::uint256");
|
||||
throwErrorIfNeeded(rc, "Bind ripple::base_uint");
|
||||
} else if constexpr (std::is_same_v<DecayedType, ripple::AccountID>) {
|
||||
auto const rc = bindBytes(value.data(), value.size());
|
||||
throwErrorIfNeeded(rc, "Bind ripple::AccountID");
|
||||
|
||||
@@ -10,8 +10,8 @@ target_sources(
|
||||
NetworkValidatedLedgers.cpp
|
||||
NFTHelpers.cpp
|
||||
Source.cpp
|
||||
MPTHelpers.cpp
|
||||
impl/AmendmentBlockHandler.cpp
|
||||
impl/ForwardingCache.cpp
|
||||
impl/ForwardingSource.cpp
|
||||
impl/GrpcSource.cpp
|
||||
impl/SubscriptionSource.cpp
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
#include "rpc/Errors.hpp"
|
||||
#include "util/Assert.hpp"
|
||||
#include "util/Random.hpp"
|
||||
#include "util/ResponseExpirationCache.hpp"
|
||||
#include "util/log/Logger.hpp"
|
||||
|
||||
#include <boost/asio/io_context.hpp>
|
||||
@@ -34,6 +35,7 @@
|
||||
#include <boost/json/array.hpp>
|
||||
#include <boost/json/object.hpp>
|
||||
#include <boost/json/value.hpp>
|
||||
#include <boost/json/value_to.hpp>
|
||||
#include <fmt/core.h>
|
||||
|
||||
#include <algorithm>
|
||||
@@ -79,7 +81,10 @@ LoadBalancer::LoadBalancer(
|
||||
{
|
||||
auto const forwardingCacheTimeout = config.valueOr<float>("forwarding.cache_timeout", 0.f);
|
||||
if (forwardingCacheTimeout > 0.f) {
|
||||
forwardingCache_ = impl::ForwardingCache{Config::toMilliseconds(forwardingCacheTimeout)};
|
||||
forwardingCache_ = util::ResponseExpirationCache{
|
||||
Config::toMilliseconds(forwardingCacheTimeout),
|
||||
{"server_info", "server_state", "server_definitions", "fee", "ledger_closed"}
|
||||
};
|
||||
}
|
||||
|
||||
static constexpr std::uint32_t MAX_DOWNLOAD = 256;
|
||||
@@ -224,8 +229,12 @@ LoadBalancer::forwardToRippled(
|
||||
boost::asio::yield_context yield
|
||||
)
|
||||
{
|
||||
if (not request.contains("command"))
|
||||
return std::unexpected{rpc::ClioError::rpcCOMMAND_IS_MISSING};
|
||||
|
||||
auto const cmd = boost::json::value_to<std::string>(request.at("command"));
|
||||
if (forwardingCache_) {
|
||||
if (auto cachedResponse = forwardingCache_->get(request); cachedResponse) {
|
||||
if (auto cachedResponse = forwardingCache_->get(cmd); cachedResponse) {
|
||||
return std::move(cachedResponse).value();
|
||||
}
|
||||
}
|
||||
@@ -253,7 +262,7 @@ LoadBalancer::forwardToRippled(
|
||||
|
||||
if (response) {
|
||||
if (forwardingCache_ and not response->contains("error"))
|
||||
forwardingCache_->put(request, *response);
|
||||
forwardingCache_->put(cmd, *response);
|
||||
return std::move(response).value();
|
||||
}
|
||||
|
||||
|
||||
@@ -23,9 +23,10 @@
|
||||
#include "etl/ETLState.hpp"
|
||||
#include "etl/NetworkValidatedLedgersInterface.hpp"
|
||||
#include "etl/Source.hpp"
|
||||
#include "etl/impl/ForwardingCache.hpp"
|
||||
#include "feed/SubscriptionManagerInterface.hpp"
|
||||
#include "rpc/Errors.hpp"
|
||||
#include "util/Mutex.hpp"
|
||||
#include "util/ResponseExpirationCache.hpp"
|
||||
#include "util/config/Config.hpp"
|
||||
#include "util/log/Logger.hpp"
|
||||
|
||||
@@ -68,7 +69,7 @@ private:
|
||||
|
||||
util::Logger log_{"ETL"};
|
||||
// Forwarding cache must be destroyed after sources because sources have a callback to invalidate cache
|
||||
std::optional<impl::ForwardingCache> forwardingCache_;
|
||||
std::optional<util::ResponseExpirationCache> forwardingCache_;
|
||||
std::optional<std::string> forwardingXUserValue_;
|
||||
|
||||
std::vector<SourcePtr> sources_;
|
||||
|
||||
83
src/etl/MPTHelpers.cpp
Normal file
83
src/etl/MPTHelpers.cpp
Normal file
@@ -0,0 +1,83 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2024, 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 "data/DBHelpers.hpp"
|
||||
|
||||
#include <ripple/protocol/STBase.h>
|
||||
#include <ripple/protocol/STTx.h>
|
||||
#include <ripple/protocol/TxMeta.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/STObject.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFormats.h>
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
namespace etl {
|
||||
|
||||
/**
|
||||
* @brief Get the MPToken created from a transaction
|
||||
*
|
||||
* @param txMeta Transaction metadata
|
||||
* @return MPT and holder account pair
|
||||
*/
|
||||
static std::optional<MPTHolderData>
|
||||
getMPTokenAuthorize(ripple::TxMeta const& txMeta)
|
||||
{
|
||||
for (ripple::STObject const& node : txMeta.getNodes()) {
|
||||
if (node.getFieldU16(ripple::sfLedgerEntryType) != ripple::ltMPTOKEN)
|
||||
continue;
|
||||
|
||||
if (node.getFName() == ripple::sfCreatedNode) {
|
||||
auto const& newMPT = node.peekAtField(ripple::sfNewFields).downcast<ripple::STObject>();
|
||||
return MPTHolderData{newMPT[ripple::sfMPTokenIssuanceID], newMPT.getAccountID(ripple::sfAccount)};
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
std::optional<MPTHolderData>
|
||||
getMPTHolderFromTx(ripple::TxMeta const& txMeta, ripple::STTx const& sttx)
|
||||
{
|
||||
if (txMeta.getResultTER() != ripple::tesSUCCESS || sttx.getTxnType() != ripple::TxType::ttMPTOKEN_AUTHORIZE)
|
||||
return {};
|
||||
|
||||
return getMPTokenAuthorize(txMeta);
|
||||
}
|
||||
|
||||
std::optional<MPTHolderData>
|
||||
getMPTHolderFromObj(std::string const& key, std::string const& blob)
|
||||
{
|
||||
ripple::STLedgerEntry const sle =
|
||||
ripple::STLedgerEntry(ripple::SerialIter{blob.data(), blob.size()}, ripple::uint256::fromVoid(key.data()));
|
||||
|
||||
if (sle.getFieldU16(ripple::sfLedgerEntryType) != ripple::ltMPTOKEN)
|
||||
return {};
|
||||
|
||||
auto const mptIssuanceID = sle[ripple::sfMPTokenIssuanceID];
|
||||
auto const holder = sle.getAccountID(ripple::sfAccount);
|
||||
|
||||
return MPTHolderData{mptIssuanceID, holder};
|
||||
}
|
||||
|
||||
} // namespace etl
|
||||
50
src/etl/MPTHelpers.hpp
Normal file
50
src/etl/MPTHelpers.hpp
Normal file
@@ -0,0 +1,50 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2024, 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.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
/** @file */
|
||||
#pragma once
|
||||
|
||||
#include "data/DBHelpers.hpp"
|
||||
|
||||
#include <ripple/protocol/STTx.h>
|
||||
#include <ripple/protocol/TxMeta.h>
|
||||
|
||||
namespace etl {
|
||||
|
||||
/**
|
||||
* @brief Pull MPT data from TX via ETLService.
|
||||
*
|
||||
* @param txMeta Transaction metadata
|
||||
* @param sttx The transaction
|
||||
* @return The MPTIssuanceID and holder pair as a optional
|
||||
*/
|
||||
std::optional<MPTHolderData>
|
||||
getMPTHolderFromTx(ripple::TxMeta const& txMeta, ripple::STTx const& sttx);
|
||||
|
||||
/**
|
||||
* @brief Pull MPT data from ledger object via loadInitialLedger.
|
||||
*
|
||||
* @param key The owner key
|
||||
* @param blob Object data as blob
|
||||
* @return The MPTIssuanceID and holder pair as a optional
|
||||
*/
|
||||
std::optional<MPTHolderData>
|
||||
getMPTHolderFromObj(std::string const& key, std::string const& blob);
|
||||
|
||||
} // namespace etl
|
||||
@@ -22,6 +22,7 @@
|
||||
#include "data/BackendInterface.hpp"
|
||||
#include "data/Types.hpp"
|
||||
#include "etl/ETLHelpers.hpp"
|
||||
#include "etl/MPTHelpers.hpp"
|
||||
#include "etl/NFTHelpers.hpp"
|
||||
#include "util/Assert.hpp"
|
||||
#include "util/log/Logger.hpp"
|
||||
@@ -154,6 +155,11 @@ public:
|
||||
backend.writeSuccessor(std::move(lastKey_), request_.ledger().sequence(), std::string{obj.key()});
|
||||
lastKey_ = obj.key();
|
||||
backend.writeNFTs(getNFTDataFromObj(request_.ledger().sequence(), obj.key(), obj.data()));
|
||||
|
||||
auto const maybeMPTHolder = getMPTHolderFromObj(obj.key(), obj.data());
|
||||
if (maybeMPTHolder)
|
||||
backend.writeMPTHolders({*maybeMPTHolder});
|
||||
|
||||
backend.writeLedgerObject(
|
||||
std::move(*obj.mutable_key()), request_.ledger().sequence(), std::move(*obj.mutable_data())
|
||||
);
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include "data/BackendInterface.hpp"
|
||||
#include "data/DBHelpers.hpp"
|
||||
#include "data/Types.hpp"
|
||||
#include "etl/MPTHelpers.hpp"
|
||||
#include "etl/NFTHelpers.hpp"
|
||||
#include "etl/SystemState.hpp"
|
||||
#include "etl/impl/LedgerFetcher.hpp"
|
||||
@@ -55,6 +56,7 @@ struct FormattedTransactionsData {
|
||||
std::vector<AccountTransactionsData> accountTxData;
|
||||
std::vector<NFTTransactionsData> nfTokenTxData;
|
||||
std::vector<NFTsData> nfTokensData;
|
||||
std::vector<MPTHolderData> mptHoldersData;
|
||||
};
|
||||
|
||||
namespace etl::impl {
|
||||
@@ -124,6 +126,10 @@ public:
|
||||
if (maybeNFT)
|
||||
result.nfTokensData.push_back(*maybeNFT);
|
||||
|
||||
auto const maybeMPTHolder = getMPTHolderFromTx(txMeta, sttx);
|
||||
if (maybeMPTHolder)
|
||||
result.mptHoldersData.push_back(*maybeMPTHolder);
|
||||
|
||||
result.accountTxData.emplace_back(txMeta, sttx.getTransactionID());
|
||||
static constexpr std::size_t KEY_SIZE = 32;
|
||||
std::string keyStr{reinterpret_cast<char const*>(sttx.getTransactionID().data()), KEY_SIZE};
|
||||
@@ -240,6 +246,7 @@ public:
|
||||
backend_->writeAccountTransactions(std::move(insertTxResult.accountTxData));
|
||||
backend_->writeNFTs(insertTxResult.nfTokensData);
|
||||
backend_->writeNFTTransactions(insertTxResult.nfTokenTxData);
|
||||
backend_->writeMPTHolders(insertTxResult.mptHoldersData);
|
||||
}
|
||||
|
||||
backend_->finishWrites(sequence);
|
||||
|
||||
@@ -213,6 +213,7 @@ private:
|
||||
backend_->writeAccountTransactions(std::move(insertTxResultOp->accountTxData));
|
||||
backend_->writeNFTs(insertTxResultOp->nfTokensData);
|
||||
backend_->writeNFTTransactions(insertTxResultOp->nfTokenTxData);
|
||||
backend_->writeMPTHolders(insertTxResultOp->mptHoldersData);
|
||||
|
||||
auto [success, duration] =
|
||||
::util::timed<std::chrono::duration<double>>([&]() { return backend_->finishWrites(lgrInfo.seq); });
|
||||
|
||||
5
src/etlng/CMakeLists.txt
Normal file
5
src/etlng/CMakeLists.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
add_library(clio_etlng INTERFACE)
|
||||
|
||||
# target_sources(clio_etlng PRIVATE )
|
||||
|
||||
target_link_libraries(clio_etlng INTERFACE clio_data)
|
||||
129
src/etlng/Models.hpp
Normal file
129
src/etlng/Models.hpp
Normal file
@@ -0,0 +1,129 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2024, 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 "util/Concepts.hpp"
|
||||
|
||||
#include <boost/json/object.hpp>
|
||||
#include <fmt/core.h>
|
||||
#include <xrpl/basics/Blob.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/proto/org/xrpl/rpc/v1/get_ledger.pb.h>
|
||||
#include <xrpl/proto/org/xrpl/rpc/v1/ledger.pb.h>
|
||||
#include <xrpl/protocol/LedgerHeader.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TxFormats.h>
|
||||
#include <xrpl/protocol/TxMeta.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace etlng::model {
|
||||
|
||||
/**
|
||||
* @brief A specification for the Registry.
|
||||
*
|
||||
* This specification simply defines the transaction types that are to be filtered out from the incoming transactions by
|
||||
* the Registry for its `onTransaction` and `onInitialTransaction` hooks.
|
||||
* It's a compilation error to list the same transaction type more than once.
|
||||
*/
|
||||
template <ripple::TxType... Types>
|
||||
requires(util::hasNoDuplicates(Types...))
|
||||
struct Spec {
|
||||
static constexpr bool SpecTag = true;
|
||||
|
||||
/**
|
||||
* @brief Checks if the transaction type was requested.
|
||||
*
|
||||
* @param type The transaction type
|
||||
* @return true if the transaction was requested; false otherwise
|
||||
*/
|
||||
[[nodiscard]] constexpr static bool
|
||||
wants(ripple::TxType type) noexcept
|
||||
{
|
||||
return ((Types == type) || ...);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Represents a single transaction on the ledger.
|
||||
*/
|
||||
struct Transaction {
|
||||
std::string raw; // raw binary blob
|
||||
std::string metaRaw;
|
||||
|
||||
// unpacked blob and meta
|
||||
ripple::STTx sttx;
|
||||
ripple::TxMeta meta;
|
||||
|
||||
// commonly used stuff
|
||||
ripple::uint256 id;
|
||||
std::string key; // key is the above id as a string of 32 characters
|
||||
ripple::TxType type;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Represents a single object on the ledger.
|
||||
*/
|
||||
struct Object {
|
||||
/**
|
||||
* @brief Modification type for the object.
|
||||
*/
|
||||
enum class ModType : int {
|
||||
Unspecified = 0,
|
||||
Created = 1,
|
||||
Modified = 2,
|
||||
Deleted = 3,
|
||||
};
|
||||
|
||||
ripple::uint256 key;
|
||||
std::string keyRaw;
|
||||
ripple::Blob data;
|
||||
std::string dataRaw;
|
||||
std::string successor;
|
||||
std::string predecessor;
|
||||
|
||||
ModType type;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Represents a book successor.
|
||||
*/
|
||||
struct BookSuccessor {
|
||||
std::string firstBook;
|
||||
std::string bookBase;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Represents an entire ledger diff worth of transactions and objects.
|
||||
*/
|
||||
struct LedgerData {
|
||||
std::vector<Transaction> transactions;
|
||||
std::vector<Object> objects;
|
||||
std::optional<std::vector<BookSuccessor>> successors;
|
||||
|
||||
ripple::LedgerHeader header;
|
||||
std::string rawHeader;
|
||||
uint32_t seq;
|
||||
};
|
||||
|
||||
} // namespace etlng::model
|
||||
108
src/etlng/RegistryInterface.hpp
Normal file
108
src/etlng/RegistryInterface.hpp
Normal file
@@ -0,0 +1,108 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2024, 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 "etlng/Models.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace etlng {
|
||||
|
||||
/**
|
||||
* @brief The interface for a registry that can dispatch transactions and objects to extensions.
|
||||
*
|
||||
* This class defines the interface for dispatching data through to extensions.
|
||||
*
|
||||
* @note
|
||||
* The registry itself consists of Extensions.
|
||||
* Each extension must define at least one valid hook:
|
||||
* - for ongoing ETL dispatch:
|
||||
* - void onLedgerData(etlng::model::LedgerData const&)
|
||||
* - void onTransaction(uint32_t, etlng::model::Transaction const&)
|
||||
* - void onObject(uint32_t, etlng::model::Object const&)
|
||||
* - for initial ledger load
|
||||
* - void onInitialData(etlng::model::LedgerData const&)
|
||||
* - void onInitialTransaction(uint32_t, etlng::model::Transaction const&)
|
||||
* - for initial objects (called for each downloaded batch)
|
||||
* - void onInitialObjects(uint32_t, std::vector<etlng::model::Object> const&, std::string)
|
||||
* - void onInitialObject(uint32_t, etlng::model::Object const&)
|
||||
*
|
||||
* When the registry dispatches (initial)data or objects, each of the above hooks will be called in order on each
|
||||
* registered extension.
|
||||
* This means that the order of execution is from left to right (hooks) and top to bottom (registered extensions).
|
||||
*
|
||||
* If either `onTransaction` or `onInitialTransaction` are defined, the extension will have to additionally define a
|
||||
* Specification. The specification lists transaction types to filter from the incoming data such that `onTransaction`
|
||||
* and `onInitialTransaction` are only called for the transactions that are of interest for the given extension.
|
||||
*
|
||||
* The specification is setup like so:
|
||||
* @code{.cpp}
|
||||
* struct Ext {
|
||||
* using spec = etlng::model::Spec<
|
||||
* ripple::TxType::ttNFTOKEN_BURN,
|
||||
* ripple::TxType::ttNFTOKEN_ACCEPT_OFFER,
|
||||
* ripple::TxType::ttNFTOKEN_CREATE_OFFER,
|
||||
* ripple::TxType::ttNFTOKEN_CANCEL_OFFER,
|
||||
* ripple::TxType::ttNFTOKEN_MINT>;
|
||||
*
|
||||
* static void
|
||||
* onInitialTransaction(uint32_t, etlng::model::Transaction const&);
|
||||
* };
|
||||
* @endcode
|
||||
*/
|
||||
struct RegistryInterface {
|
||||
virtual ~RegistryInterface() = default;
|
||||
|
||||
/**
|
||||
* @brief Dispatch initial objects.
|
||||
*
|
||||
* These objects are received during initial ledger load.
|
||||
*
|
||||
* @param seq The sequence
|
||||
* @param data The objects to dispatch
|
||||
* @param lastKey The predcessor of the first object in data if known; an empty string otherwise
|
||||
*/
|
||||
virtual void
|
||||
dispatchInitialObjects(uint32_t seq, std::vector<model::Object> const& data, std::string lastKey) = 0;
|
||||
|
||||
/**
|
||||
* @brief Dispatch initial ledger data.
|
||||
*
|
||||
* The transactions, header and edge keys are received during initial ledger load.
|
||||
*
|
||||
* @param data The data to dispatch
|
||||
*/
|
||||
virtual void
|
||||
dispatchInitialData(model::LedgerData const& data) = 0;
|
||||
|
||||
/**
|
||||
* @brief Dispatch an entire ledger diff.
|
||||
*
|
||||
* This is used to dispatch incoming diffs through the extensions.
|
||||
*
|
||||
* @param data The data to dispatch
|
||||
*/
|
||||
virtual void
|
||||
dispatch(model::LedgerData const& data) = 0;
|
||||
};
|
||||
|
||||
} // namespace etlng
|
||||
219
src/etlng/impl/Registry.hpp
Normal file
219
src/etlng/impl/Registry.hpp
Normal file
@@ -0,0 +1,219 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2024, 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 "etlng/Models.hpp"
|
||||
#include "etlng/RegistryInterface.hpp"
|
||||
|
||||
#include <xrpl/protocol/TxFormats.h>
|
||||
|
||||
#include <concepts>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <tuple>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace etlng::impl {
|
||||
|
||||
template <typename T>
|
||||
concept HasLedgerDataHook = requires(T p) {
|
||||
{ p.onLedgerData(std::declval<etlng::model::LedgerData>()) } -> std::same_as<void>;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
concept HasInitialDataHook = requires(T p) {
|
||||
{ p.onInitialData(std::declval<etlng::model::LedgerData>()) } -> std::same_as<void>;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
concept HasTransactionHook = requires(T p) {
|
||||
{ p.onTransaction(uint32_t{}, std::declval<etlng::model::Transaction>()) } -> std::same_as<void>;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
concept HasObjectHook = requires(T p) {
|
||||
{ p.onObject(uint32_t{}, std::declval<etlng::model::Object>()) } -> std::same_as<void>;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
concept HasInitialTransactionHook = requires(T p) {
|
||||
{ p.onInitialTransaction(uint32_t{}, std::declval<etlng::model::Transaction>()) } -> std::same_as<void>;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
concept HasInitialObjectsHook = requires(T p) {
|
||||
{
|
||||
p.onInitialObjects(uint32_t{}, std::declval<std::vector<etlng::model::Object>>(), std::string{})
|
||||
} -> std::same_as<void>;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
concept HasInitialObjectHook = requires(T p) {
|
||||
{ p.onInitialObject(uint32_t{}, std::declval<etlng::model::Object>()) } -> std::same_as<void>;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
concept ContainsSpec = std::decay_t<T>::spec::SpecTag;
|
||||
|
||||
template <typename T>
|
||||
concept ContainsValidHook = HasLedgerDataHook<T> or HasInitialDataHook<T> or
|
||||
(HasTransactionHook<T> and ContainsSpec<T>) or (HasInitialTransactionHook<T> and ContainsSpec<T>) or
|
||||
HasObjectHook<T> or HasInitialObjectsHook<T> or HasInitialObjectHook<T>;
|
||||
|
||||
template <typename T>
|
||||
concept NoTwoOfKind = not(HasLedgerDataHook<T> and HasTransactionHook<T>) and
|
||||
not(HasInitialDataHook<T> and HasInitialTransactionHook<T>) and not(HasInitialDataHook<T> and HasObjectHook<T>) and
|
||||
not(HasInitialObjectsHook<T> and HasInitialObjectHook<T>);
|
||||
|
||||
template <typename T>
|
||||
concept SomeExtension = NoTwoOfKind<T> and ContainsValidHook<T>;
|
||||
|
||||
template <SomeExtension... Ps>
|
||||
class Registry : public RegistryInterface {
|
||||
std::tuple<Ps...> store_;
|
||||
|
||||
static_assert(
|
||||
(((not HasTransactionHook<std::decay_t<Ps>>) or ContainsSpec<std::decay_t<Ps>>) and ...),
|
||||
"Spec must be specified when 'onTransaction' function exists."
|
||||
);
|
||||
|
||||
static_assert(
|
||||
(((not HasInitialTransactionHook<std::decay_t<Ps>>) or ContainsSpec<std::decay_t<Ps>>) and ...),
|
||||
"Spec must be specified when 'onInitialTransaction' function exists."
|
||||
);
|
||||
|
||||
public:
|
||||
explicit constexpr Registry(SomeExtension auto&&... exts)
|
||||
requires(std::is_same_v<std::decay_t<decltype(exts)>, std::decay_t<Ps>> and ...)
|
||||
: store_(std::forward<Ps>(exts)...)
|
||||
{
|
||||
}
|
||||
|
||||
~Registry() override = default;
|
||||
Registry(Registry const&) = delete;
|
||||
Registry(Registry&&) = default;
|
||||
Registry&
|
||||
operator=(Registry const&) = delete;
|
||||
Registry&
|
||||
operator=(Registry&&) = default;
|
||||
|
||||
constexpr void
|
||||
dispatch(model::LedgerData const& data) override
|
||||
{
|
||||
// send entire batch of data at once
|
||||
{
|
||||
auto const expand = [&](auto& p) {
|
||||
if constexpr (requires { p.onLedgerData(data); }) {
|
||||
p.onLedgerData(data);
|
||||
}
|
||||
};
|
||||
|
||||
std::apply([&expand](auto&&... xs) { (expand(xs), ...); }, store_);
|
||||
}
|
||||
|
||||
// send filtered transactions
|
||||
{
|
||||
auto const expand = [&]<typename P>(P& p, model::Transaction const& t) {
|
||||
if constexpr (requires { p.onTransaction(data.seq, t); }) {
|
||||
if (std::decay_t<P>::spec::wants(t.type))
|
||||
p.onTransaction(data.seq, t);
|
||||
}
|
||||
};
|
||||
|
||||
for (auto const& t : data.transactions) {
|
||||
std::apply([&expand, &t](auto&&... xs) { (expand(xs, t), ...); }, store_);
|
||||
}
|
||||
}
|
||||
|
||||
// send per object path
|
||||
{
|
||||
auto const expand = [&]<typename P>(P&& p, model::Object const& o) {
|
||||
if constexpr (requires { p.onObject(data.seq, o); }) {
|
||||
p.onObject(data.seq, o);
|
||||
}
|
||||
};
|
||||
|
||||
for (auto const& obj : data.objects) {
|
||||
std::apply([&expand, &obj](auto&&... xs) { (expand(xs, obj), ...); }, store_);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
constexpr void
|
||||
dispatchInitialObjects(uint32_t seq, std::vector<model::Object> const& data, std::string lastKey) override
|
||||
{
|
||||
// send entire vector path
|
||||
{
|
||||
auto const expand = [&](auto&& p) {
|
||||
if constexpr (requires { p.onInitialObjects(seq, data, lastKey); }) {
|
||||
p.onInitialObjects(seq, data, lastKey);
|
||||
}
|
||||
};
|
||||
|
||||
std::apply([&expand](auto&&... xs) { (expand(xs), ...); }, store_);
|
||||
}
|
||||
|
||||
// send per object path
|
||||
{
|
||||
auto const expand = [&]<typename P>(P&& p, model::Object const& o) {
|
||||
if constexpr (requires { p.onInitialObject(seq, o); }) {
|
||||
p.onInitialObject(seq, o);
|
||||
}
|
||||
};
|
||||
|
||||
for (auto const& obj : data) {
|
||||
std::apply([&expand, &obj](auto&&... xs) { (expand(xs, obj), ...); }, store_);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
constexpr void
|
||||
dispatchInitialData(model::LedgerData const& data) override
|
||||
{
|
||||
// send entire batch path
|
||||
{
|
||||
auto const expand = [&](auto&& p) {
|
||||
if constexpr (requires { p.onInitialData(data); }) {
|
||||
p.onInitialData(data);
|
||||
}
|
||||
};
|
||||
|
||||
std::apply([&expand](auto&&... xs) { (expand(xs), ...); }, store_);
|
||||
}
|
||||
|
||||
// send per tx path
|
||||
{
|
||||
auto const expand = [&]<typename P>(P&& p, model::Transaction const& tx) {
|
||||
if constexpr (requires { p.onInitialTransaction(data.seq, tx); }) {
|
||||
if (std::decay_t<P>::spec::wants(tx.type))
|
||||
p.onInitialTransaction(data.seq, tx);
|
||||
}
|
||||
};
|
||||
|
||||
for (auto const& tx : data.transactions) {
|
||||
std::apply([&expand, &tx](auto&&... xs) { (expand(xs, tx), ...); }, store_);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace etlng::impl
|
||||
@@ -203,6 +203,7 @@ TransactionFeed::pub(
|
||||
pubObj[JS(meta)] = rpc::toJson(*meta);
|
||||
rpc::insertDeliveredAmount(pubObj[JS(meta)].as_object(), tx, meta, txMeta.date);
|
||||
rpc::insertDeliverMaxAlias(pubObj[txKey].as_object(), version);
|
||||
rpc::insertMPTIssuanceID(pubObj[JS(meta)].as_object(), tx, meta);
|
||||
|
||||
pubObj[JS(type)] = "transaction";
|
||||
pubObj[JS(validated)] = true;
|
||||
|
||||
@@ -6,6 +6,7 @@ target_sources(
|
||||
Factories.cpp
|
||||
AMMHelpers.cpp
|
||||
RPCHelpers.cpp
|
||||
CredentialHelpers.cpp
|
||||
Counters.cpp
|
||||
WorkQueue.cpp
|
||||
common/Specs.cpp
|
||||
@@ -33,6 +34,7 @@ target_sources(
|
||||
handlers/LedgerEntry.cpp
|
||||
handlers/LedgerIndex.cpp
|
||||
handlers/LedgerRange.cpp
|
||||
handlers/MPTHolders.cpp
|
||||
handlers/NFTsByIssuer.cpp
|
||||
handlers/NFTBuyOffers.cpp
|
||||
handlers/NFTHistory.cpp
|
||||
|
||||
161
src/rpc/CredentialHelpers.cpp
Normal file
161
src/rpc/CredentialHelpers.cpp
Normal file
@@ -0,0 +1,161 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2024, 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 "data/BackendInterface.hpp"
|
||||
#include "rpc/Errors.hpp"
|
||||
#include "rpc/JS.hpp"
|
||||
#include "rpc/common/Types.hpp"
|
||||
#include "util/Assert.hpp"
|
||||
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/json/array.hpp>
|
||||
#include <boost/json/value_to.hpp>
|
||||
#include <xrpl/basics/Slice.h>
|
||||
#include <xrpl/basics/StringUtilities.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/basics/chrono.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/LedgerHeader.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STArray.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/STObject.h>
|
||||
#include <xrpl/protocol/Serializer.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
|
||||
namespace rpc::credentials {
|
||||
|
||||
bool
|
||||
checkExpired(ripple::SLE const& sleCred, ripple::LedgerHeader const& ledger)
|
||||
{
|
||||
if (sleCred.isFieldPresent(ripple::sfExpiration)) {
|
||||
std::uint32_t const exp = sleCred.getFieldU32(ripple::sfExpiration);
|
||||
std::uint32_t const now = ledger.parentCloseTime.time_since_epoch().count();
|
||||
return now > exp;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::set<std::pair<ripple::AccountID, ripple::Slice>>
|
||||
createAuthCredentials(ripple::STArray const& in)
|
||||
{
|
||||
std::set<std::pair<ripple::AccountID, ripple::Slice>> out;
|
||||
for (auto const& cred : in)
|
||||
out.insert({cred[ripple::sfIssuer], cred[ripple::sfCredentialType]});
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
ripple::STArray
|
||||
parseAuthorizeCredentials(boost::json::array const& jv)
|
||||
{
|
||||
ripple::STArray arr;
|
||||
for (auto const& jo : jv) {
|
||||
ASSERT(
|
||||
jo.at(JS(issuer)).is_string(),
|
||||
"issuer must be string, should already be checked in AuthorizeCredentialValidator"
|
||||
);
|
||||
auto const issuer =
|
||||
ripple::parseBase58<ripple::AccountID>(static_cast<std::string>(jo.at(JS(issuer)).as_string()));
|
||||
ASSERT(
|
||||
issuer.has_value(), "issuer must be present, should already be checked in AuthorizeCredentialValidator."
|
||||
);
|
||||
|
||||
ASSERT(
|
||||
jo.at(JS(credential_type)).is_string(),
|
||||
"credential_type must be string, should already be checked in AuthorizeCredentialValidator"
|
||||
);
|
||||
auto const credentialType = ripple::strUnHex(static_cast<std::string>(jo.at(JS(credential_type)).as_string()));
|
||||
ASSERT(
|
||||
credentialType.has_value(),
|
||||
"credential_type must be present, should already be checked in AuthorizeCredentialValidator."
|
||||
);
|
||||
|
||||
auto credential = ripple::STObject::makeInnerObject(ripple::sfCredential);
|
||||
credential.setAccountID(ripple::sfIssuer, *issuer);
|
||||
credential.setFieldVL(ripple::sfCredentialType, *credentialType);
|
||||
arr.push_back(std::move(credential));
|
||||
}
|
||||
|
||||
return arr;
|
||||
}
|
||||
|
||||
std::expected<ripple::STArray, Status>
|
||||
fetchCredentialArray(
|
||||
std::optional<boost::json::array> const& credID,
|
||||
ripple::AccountID const& srcAcc,
|
||||
BackendInterface const& backend,
|
||||
ripple::LedgerHeader const& info,
|
||||
boost::asio::yield_context const& yield
|
||||
)
|
||||
{
|
||||
ripple::STArray authCreds;
|
||||
std::unordered_set<std::string_view> elems;
|
||||
for (auto const& elem : credID.value()) {
|
||||
ASSERT(elem.is_string(), "should already be checked in validators.hpp that elem is a string.");
|
||||
|
||||
if (elems.contains(elem.as_string()))
|
||||
return Error{Status{RippledError::rpcBAD_CREDENTIALS, "duplicates in credentials."}};
|
||||
elems.insert(elem.as_string());
|
||||
|
||||
ripple::uint256 credHash;
|
||||
ASSERT(
|
||||
credHash.parseHex(boost::json::value_to<std::string>(elem)),
|
||||
"should already be checked in validators.hpp that elem is a uint256 hex"
|
||||
);
|
||||
|
||||
auto const credKeylet = ripple::keylet::credential(credHash).key;
|
||||
auto const credLedgerObject = backend.fetchLedgerObject(credKeylet, info.seq, yield);
|
||||
if (!credLedgerObject)
|
||||
return Error{Status{RippledError::rpcBAD_CREDENTIALS, "credentials don't exist."}};
|
||||
|
||||
auto credIt = ripple::SerialIter{credLedgerObject->data(), credLedgerObject->size()};
|
||||
auto const sleCred = ripple::SLE{credIt, credKeylet};
|
||||
|
||||
if ((sleCred.getType() != ripple::ltCREDENTIAL) ||
|
||||
((sleCred.getFieldU32(ripple::sfFlags) & ripple::lsfAccepted) == 0u))
|
||||
return Error{Status{RippledError::rpcBAD_CREDENTIALS, "credentials aren't accepted"}};
|
||||
|
||||
if (credentials::checkExpired(sleCred, info))
|
||||
return Error{Status{RippledError::rpcBAD_CREDENTIALS, "credentials are expired"}};
|
||||
|
||||
if (sleCred.getAccountID(ripple::sfSubject) != srcAcc)
|
||||
return Error{Status{RippledError::rpcBAD_CREDENTIALS, "credentials don't belong to the root account"}};
|
||||
|
||||
auto credential = ripple::STObject::makeInnerObject(ripple::sfCredential);
|
||||
credential.setAccountID(ripple::sfIssuer, sleCred.getAccountID(ripple::sfIssuer));
|
||||
credential.setFieldVL(ripple::sfCredentialType, sleCred.getFieldVL(ripple::sfCredentialType));
|
||||
authCreds.push_back(std::move(credential));
|
||||
}
|
||||
|
||||
return authCreds;
|
||||
}
|
||||
|
||||
} // namespace rpc::credentials
|
||||
89
src/rpc/CredentialHelpers.hpp
Normal file
89
src/rpc/CredentialHelpers.hpp
Normal file
@@ -0,0 +1,89 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2024, 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 "data/BackendInterface.hpp"
|
||||
#include "rpc/Errors.hpp"
|
||||
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/json/array.hpp>
|
||||
#include <xrpl/basics/Slice.h>
|
||||
#include <xrpl/basics/chrono.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/LedgerHeader.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/STObject.h>
|
||||
|
||||
#include <expected>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <utility>
|
||||
|
||||
namespace rpc::credentials {
|
||||
|
||||
/**
|
||||
* @brief Check if credential is expired
|
||||
*
|
||||
* @param sleCred The credential to check
|
||||
* @param ledger The ledger to check the closed time of
|
||||
* @return true if credential not expired, false otherwise
|
||||
*/
|
||||
bool
|
||||
checkExpired(ripple::SLE const& sleCred, ripple::LedgerHeader const& ledger);
|
||||
|
||||
/**
|
||||
* @brief Creates authentication credential field (which is a set of pairs of AccountID and Credential ID)
|
||||
*
|
||||
* @param in The array of Credential objects to check
|
||||
* @return Auth Credential array
|
||||
*/
|
||||
std::set<std::pair<ripple::AccountID, ripple::Slice>>
|
||||
createAuthCredentials(ripple::STArray const& in);
|
||||
|
||||
/**
|
||||
* @brief Parses each credential object and makes sure the credential type and values are correct
|
||||
*
|
||||
* @param jv The boost json array of credentials to parse
|
||||
* @return Array of credentials after parsing
|
||||
*/
|
||||
ripple::STArray
|
||||
parseAuthorizeCredentials(boost::json::array const& jv);
|
||||
|
||||
/**
|
||||
* @brief Get Array of Credential objects
|
||||
*
|
||||
* @param credID Array of CredentialID's to parse
|
||||
* @param srcAcc The Source Account
|
||||
* @param backend backend interface
|
||||
* @param info The ledger header
|
||||
* @param yield The coroutine context
|
||||
* @return Array of credential objects, error if failed otherwise
|
||||
*/
|
||||
std::expected<ripple::STArray, Status>
|
||||
fetchCredentialArray(
|
||||
std::optional<boost::json::array> const& credID,
|
||||
ripple::AccountID const& srcAcc,
|
||||
BackendInterface const& backend,
|
||||
ripple::LedgerHeader const& info,
|
||||
boost::asio::yield_context const& yield
|
||||
);
|
||||
|
||||
} // namespace rpc::credentials
|
||||
@@ -79,10 +79,12 @@ getErrorInfo(ClioError code)
|
||||
{ClioError::rpcMALFORMED_REQUEST, "malformedRequest", "Malformed request."},
|
||||
{ClioError::rpcMALFORMED_OWNER, "malformedOwner", "Malformed owner."},
|
||||
{ClioError::rpcMALFORMED_ADDRESS, "malformedAddress", "Malformed address."},
|
||||
{ClioError::rpcINVALID_HOT_WALLET, "invalidHotWallet", "Invalid hot wallet."},
|
||||
{ClioError::rpcUNKNOWN_OPTION, "unknownOption", "Unknown option."},
|
||||
{ClioError::rpcFIELD_NOT_FOUND_TRANSACTION, "fieldNotFoundTransaction", "Missing field."},
|
||||
{ClioError::rpcMALFORMED_ORACLE_DOCUMENT_ID, "malformedDocumentID", "Malformed oracle_document_id."},
|
||||
{ClioError::rpcMALFORMED_AUTHORIZED_CREDENTIALS,
|
||||
"malformedAuthorizedCredentials",
|
||||
"Malformed authorized credentials."},
|
||||
// special system errors
|
||||
{ClioError::rpcINVALID_API_VERSION, JS(invalid_API_version), "Invalid API version."},
|
||||
{ClioError::rpcCOMMAND_IS_MISSING, JS(missingCommand), "Method is not specified or is not a string."},
|
||||
|
||||
@@ -39,10 +39,10 @@ enum class ClioError {
|
||||
rpcMALFORMED_REQUEST = 5001,
|
||||
rpcMALFORMED_OWNER = 5002,
|
||||
rpcMALFORMED_ADDRESS = 5003,
|
||||
rpcINVALID_HOT_WALLET = 5004,
|
||||
rpcUNKNOWN_OPTION = 5005,
|
||||
rpcFIELD_NOT_FOUND_TRANSACTION = 5006,
|
||||
rpcMALFORMED_ORACLE_DOCUMENT_ID = 5007,
|
||||
rpcMALFORMED_AUTHORIZED_CREDENTIALS = 5008,
|
||||
|
||||
// special system errors start with 6000
|
||||
rpcINVALID_API_VERSION = 6000,
|
||||
|
||||
@@ -20,20 +20,22 @@
|
||||
#pragma once
|
||||
|
||||
#include "data/BackendInterface.hpp"
|
||||
#include "rpc/Counters.hpp"
|
||||
#include "rpc/Errors.hpp"
|
||||
#include "rpc/RPCHelpers.hpp"
|
||||
#include "rpc/WorkQueue.hpp"
|
||||
#include "rpc/common/HandlerProvider.hpp"
|
||||
#include "rpc/common/Types.hpp"
|
||||
#include "rpc/common/impl/ForwardingProxy.hpp"
|
||||
#include "util/ResponseExpirationCache.hpp"
|
||||
#include "util/log/Logger.hpp"
|
||||
#include "web/Context.hpp"
|
||||
#include "web/dosguard/DOSGuardInterface.hpp"
|
||||
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/iterator/transform_iterator.hpp>
|
||||
#include <boost/json.hpp>
|
||||
#include <fmt/core.h>
|
||||
#include <fmt/format.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
|
||||
#include <chrono>
|
||||
@@ -42,14 +44,9 @@
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
|
||||
// forward declarations
|
||||
namespace etl {
|
||||
class LoadBalancer;
|
||||
class ETLService;
|
||||
} // namespace etl
|
||||
|
||||
/**
|
||||
* @brief This namespace contains all the RPC logic and handlers.
|
||||
*/
|
||||
@@ -58,6 +55,7 @@ namespace rpc {
|
||||
/**
|
||||
* @brief The RPC engine that ties all RPC-related functionality together.
|
||||
*/
|
||||
template <typename LoadBalancerType, typename CountersType>
|
||||
class RPCEngine {
|
||||
util::Logger perfLog_{"Performance"};
|
||||
util::Logger log_{"RPC"};
|
||||
@@ -65,16 +63,19 @@ class RPCEngine {
|
||||
std::shared_ptr<BackendInterface> backend_;
|
||||
std::reference_wrapper<web::dosguard::DOSGuardInterface const> dosGuard_;
|
||||
std::reference_wrapper<WorkQueue> workQueue_;
|
||||
std::reference_wrapper<Counters> counters_;
|
||||
std::reference_wrapper<CountersType> counters_;
|
||||
|
||||
std::shared_ptr<HandlerProvider const> handlerProvider_;
|
||||
|
||||
impl::ForwardingProxy<etl::LoadBalancer, Counters, HandlerProvider> forwardingProxy_;
|
||||
impl::ForwardingProxy<LoadBalancerType, CountersType, HandlerProvider> forwardingProxy_;
|
||||
|
||||
std::optional<util::ResponseExpirationCache> responseCache_;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Construct a new RPCEngine object
|
||||
*
|
||||
* @param config The config to use
|
||||
* @param backend The backend to use
|
||||
* @param balancer The load balancer to use
|
||||
* @param dosGuard The DOS guard to use
|
||||
@@ -83,11 +84,12 @@ public:
|
||||
* @param handlerProvider The handler provider to use
|
||||
*/
|
||||
RPCEngine(
|
||||
util::Config const& config,
|
||||
std::shared_ptr<BackendInterface> const& backend,
|
||||
std::shared_ptr<etl::LoadBalancer> const& balancer,
|
||||
std::shared_ptr<LoadBalancerType> const& balancer,
|
||||
web::dosguard::DOSGuardInterface const& dosGuard,
|
||||
WorkQueue& workQueue,
|
||||
Counters& counters,
|
||||
CountersType& counters,
|
||||
std::shared_ptr<HandlerProvider const> const& handlerProvider
|
||||
)
|
||||
: backend_{backend}
|
||||
@@ -97,11 +99,22 @@ public:
|
||||
, handlerProvider_{handlerProvider}
|
||||
, forwardingProxy_{balancer, counters, handlerProvider}
|
||||
{
|
||||
// Let main thread catch the exception if config type is wrong
|
||||
auto const cacheTimeout = config.valueOr<float>("rpc.cache_timeout", 0.f);
|
||||
|
||||
if (cacheTimeout > 0.f) {
|
||||
LOG(log_.info()) << fmt::format("Init RPC Cache, timeout: {} seconds", cacheTimeout);
|
||||
|
||||
responseCache_.emplace(
|
||||
util::Config::toMilliseconds(cacheTimeout), std::unordered_set<std::string>{"server_info"}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Factory function to create a new instance of the RPC engine.
|
||||
*
|
||||
* @param config The config to use
|
||||
* @param backend The backend to use
|
||||
* @param balancer The load balancer to use
|
||||
* @param dosGuard The DOS guard to use
|
||||
@@ -112,15 +125,16 @@ public:
|
||||
*/
|
||||
static std::shared_ptr<RPCEngine>
|
||||
make_RPCEngine(
|
||||
util::Config const& config,
|
||||
std::shared_ptr<BackendInterface> const& backend,
|
||||
std::shared_ptr<etl::LoadBalancer> const& balancer,
|
||||
std::shared_ptr<LoadBalancerType> const& balancer,
|
||||
web::dosguard::DOSGuardInterface const& dosGuard,
|
||||
WorkQueue& workQueue,
|
||||
Counters& counters,
|
||||
CountersType& counters,
|
||||
std::shared_ptr<HandlerProvider const> const& handlerProvider
|
||||
)
|
||||
{
|
||||
return std::make_shared<RPCEngine>(backend, balancer, dosGuard, workQueue, counters, handlerProvider);
|
||||
return std::make_shared<RPCEngine>(config, backend, balancer, dosGuard, workQueue, counters, handlerProvider);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -140,6 +154,11 @@ public:
|
||||
return forwardingProxy_.forward(ctx);
|
||||
}
|
||||
|
||||
if (not ctx.isAdmin and responseCache_) {
|
||||
if (auto res = responseCache_->get(ctx.method); res.has_value())
|
||||
return Result{std::move(res).value()};
|
||||
}
|
||||
|
||||
if (backend_->isTooBusy()) {
|
||||
LOG(log_.error()) << "Database is too busy. Rejecting request";
|
||||
notifyTooBusy(); // TODO: should we add ctx.method if we have it?
|
||||
@@ -160,8 +179,11 @@ public:
|
||||
|
||||
LOG(perfLog_.debug()) << ctx.tag() << " finish executing rpc `" << ctx.method << '`';
|
||||
|
||||
if (not v)
|
||||
if (not v) {
|
||||
notifyErrored(ctx.method);
|
||||
} else if (not ctx.isAdmin and responseCache_) {
|
||||
responseCache_->put(ctx.method, v.result->as_object());
|
||||
}
|
||||
|
||||
return Result{std::move(v)};
|
||||
} catch (data::DatabaseTimeout const& t) {
|
||||
|
||||
@@ -81,6 +81,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cassert>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
@@ -259,6 +260,7 @@ toExpandedJson(
|
||||
auto metaJson = toJson(*meta);
|
||||
insertDeliveredAmount(metaJson, txn, meta, blobs.date);
|
||||
insertDeliverMaxAlias(txnJson, apiVersion);
|
||||
insertMPTIssuanceID(metaJson, txn, meta);
|
||||
|
||||
if (nftEnabled == NFTokenjson::ENABLE) {
|
||||
Json::Value nftJson;
|
||||
@@ -314,6 +316,67 @@ insertDeliveredAmount(
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the delivered amount
|
||||
*
|
||||
* @param meta The metadata
|
||||
* @return The mpt_issuance_id or std::nullopt if not available
|
||||
*/
|
||||
static std::optional<ripple::uint192>
|
||||
getMPTIssuanceID(std::shared_ptr<ripple::TxMeta const> const& meta)
|
||||
{
|
||||
ripple::TxMeta const& transactionMeta = *meta;
|
||||
|
||||
for (ripple::STObject const& node : transactionMeta.getNodes()) {
|
||||
if (node.getFieldU16(ripple::sfLedgerEntryType) != ripple::ltMPTOKEN_ISSUANCE ||
|
||||
node.getFName() != ripple::sfCreatedNode)
|
||||
continue;
|
||||
|
||||
auto const& mptNode = node.peekAtField(ripple::sfNewFields).downcast<ripple::STObject>();
|
||||
return ripple::makeMptID(mptNode[ripple::sfSequence], mptNode[ripple::sfIssuer]);
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if transaction has a new MPToken created
|
||||
*
|
||||
* @param txn The transaction
|
||||
* @param meta The metadata
|
||||
* @return true if the transaction can have a mpt_issuance_id
|
||||
*/
|
||||
static bool
|
||||
canHaveMPTIssuanceID(std::shared_ptr<ripple::STTx const> const& txn, std::shared_ptr<ripple::TxMeta const> const& meta)
|
||||
{
|
||||
if (txn->getTxnType() != ripple::ttMPTOKEN_ISSUANCE_CREATE)
|
||||
return false;
|
||||
|
||||
if (meta->getResultTER() != ripple::tesSUCCESS)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
insertMPTIssuanceID(
|
||||
boost::json::object& metaJson,
|
||||
std::shared_ptr<ripple::STTx const> const& txn,
|
||||
std::shared_ptr<ripple::TxMeta const> const& meta
|
||||
)
|
||||
{
|
||||
if (!canHaveMPTIssuanceID(txn, meta))
|
||||
return false;
|
||||
|
||||
if (auto const id = getMPTIssuanceID(meta)) {
|
||||
metaJson[JS(mpt_issuance_id)] = ripple::to_string(*id);
|
||||
return true;
|
||||
}
|
||||
|
||||
assert(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
void
|
||||
insertDeliverMaxAlias(boost::json::object& txJson, std::uint32_t const apiVersion)
|
||||
{
|
||||
@@ -430,8 +493,9 @@ ledgerHeaderFromRequest(std::shared_ptr<data::BackendInterface const> const& bac
|
||||
} else {
|
||||
ledgerSequence = parseStringAsUInt(stringIndex);
|
||||
}
|
||||
} else if (indexValue.is_int64())
|
||||
} else if (indexValue.is_int64()) {
|
||||
ledgerSequence = indexValue.as_int64();
|
||||
}
|
||||
} else {
|
||||
ledgerSequence = ctx.range.maxSequence;
|
||||
}
|
||||
@@ -949,7 +1013,8 @@ accountHolds(
|
||||
auto const blob = backend.fetchLedgerObject(key, sequence, yield);
|
||||
|
||||
if (!blob) {
|
||||
amount.clear({currency, issuer});
|
||||
amount.setIssue(ripple::Issue(currency, issuer));
|
||||
amount.clear();
|
||||
return amount;
|
||||
}
|
||||
|
||||
@@ -957,7 +1022,8 @@ accountHolds(
|
||||
ripple::SLE const sle{it, key};
|
||||
|
||||
if (zeroIfFrozen && isFrozen(backend, sequence, account, currency, issuer, yield)) {
|
||||
amount.clear(ripple::Issue(currency, issuer));
|
||||
amount.setIssue(ripple::Issue(currency, issuer));
|
||||
amount.clear();
|
||||
} else {
|
||||
amount = sle.getFieldAmount(ripple::sfBalance);
|
||||
if (account > issuer) {
|
||||
|
||||
@@ -191,6 +191,21 @@ insertDeliveredAmount(
|
||||
uint32_t date
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Add "mpt_issuance_id" into MPTokenIssuanceCreate transaction json.
|
||||
*
|
||||
* @param metaJson The metadata json object to add "MPTokenIssuanceID"
|
||||
* @param txn The transaction object
|
||||
* @param meta The metadata object
|
||||
* @return true if the "mpt_issuance_id" is added to the metadata json object
|
||||
*/
|
||||
bool
|
||||
insertMPTIssuanceID(
|
||||
boost::json::object& metaJson,
|
||||
std::shared_ptr<ripple::STTx const> const& txn,
|
||||
std::shared_ptr<ripple::TxMeta const> const& meta
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Convert STBase object to JSON
|
||||
*
|
||||
|
||||
@@ -34,16 +34,13 @@ static constexpr uint32_t API_VERSION_DEFAULT = 1u;
|
||||
|
||||
/**
|
||||
* @brief Minimum API version supported by this build
|
||||
*
|
||||
* Note: Clio does not natively support v1 and only supports v2 or newer.
|
||||
* However, Clio will forward all v1 requests to rippled for backward compatibility.
|
||||
*/
|
||||
static constexpr uint32_t API_VERSION_MIN = 1u;
|
||||
|
||||
/**
|
||||
* @brief Maximum API version supported by this build
|
||||
*/
|
||||
static constexpr uint32_t API_VERSION_MAX = 2u;
|
||||
static constexpr uint32_t API_VERSION_MAX = 3u;
|
||||
|
||||
/**
|
||||
* @brief A baseclass for API version helper
|
||||
|
||||
@@ -29,8 +29,10 @@
|
||||
#include <boost/json/value.hpp>
|
||||
#include <boost/json/value_to.hpp>
|
||||
#include <fmt/core.h>
|
||||
#include <xrpl/basics/StringUtilities.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/UintTypes.h>
|
||||
|
||||
#include <charconv>
|
||||
@@ -89,16 +91,19 @@ checkIsU32Numeric(std::string_view sv)
|
||||
return ec == std::errc();
|
||||
}
|
||||
|
||||
CustomValidator CustomValidators::Uint160HexStringValidator =
|
||||
CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError {
|
||||
return makeHexStringValidator<ripple::uint160>(value, key);
|
||||
}};
|
||||
|
||||
CustomValidator CustomValidators::Uint192HexStringValidator =
|
||||
CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError {
|
||||
return makeHexStringValidator<ripple::uint192>(value, key);
|
||||
}};
|
||||
|
||||
CustomValidator CustomValidators::Uint256HexStringValidator =
|
||||
CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError {
|
||||
if (!value.is_string())
|
||||
return Error{Status{RippledError::rpcINVALID_PARAMS, std::string(key) + "NotString"}};
|
||||
|
||||
ripple::uint256 ledgerHash;
|
||||
if (!ledgerHash.parseHex(boost::json::value_to<std::string>(value)))
|
||||
return Error{Status{RippledError::rpcINVALID_PARAMS, std::string(key) + "Malformed"}};
|
||||
|
||||
return MaybeError{};
|
||||
return makeHexStringValidator<ripple::uint256>(value, key);
|
||||
}};
|
||||
|
||||
CustomValidator CustomValidators::LedgerIndexValidator =
|
||||
@@ -250,4 +255,79 @@ CustomValidator CustomValidators::CurrencyIssueValidator =
|
||||
return MaybeError{};
|
||||
}};
|
||||
|
||||
CustomValidator CustomValidators::CredentialTypeValidator =
|
||||
CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError {
|
||||
if (not value.is_string())
|
||||
return Error{Status{ClioError::rpcMALFORMED_AUTHORIZED_CREDENTIALS, std::string(key) + " NotString"}};
|
||||
|
||||
auto const& credTypeHex = ripple::strViewUnHex(value.as_string());
|
||||
if (!credTypeHex.has_value())
|
||||
return Error{Status{ClioError::rpcMALFORMED_AUTHORIZED_CREDENTIALS, std::string(key) + " NotHexString"}};
|
||||
|
||||
if (credTypeHex->empty())
|
||||
return Error{Status{ClioError::rpcMALFORMED_AUTHORIZED_CREDENTIALS, std::string(key) + " is empty"}};
|
||||
|
||||
if (credTypeHex->size() > ripple::maxCredentialTypeLength) {
|
||||
return Error{
|
||||
Status{ClioError::rpcMALFORMED_AUTHORIZED_CREDENTIALS, std::string(key) + " greater than max length"}
|
||||
};
|
||||
}
|
||||
|
||||
return MaybeError{};
|
||||
}};
|
||||
|
||||
CustomValidator CustomValidators::AuthorizeCredentialValidator =
|
||||
CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError {
|
||||
if (not value.is_array())
|
||||
return Error{Status{ClioError::rpcMALFORMED_REQUEST, std::string(key) + " not array"}};
|
||||
|
||||
auto const& authCred = value.as_array();
|
||||
if (authCred.empty()) {
|
||||
return Error{Status{
|
||||
ClioError::rpcMALFORMED_AUTHORIZED_CREDENTIALS,
|
||||
fmt::format("Requires at least one element in authorized_credentials array.")
|
||||
}};
|
||||
}
|
||||
|
||||
if (authCred.size() > ripple::maxCredentialsArraySize) {
|
||||
return Error{Status{
|
||||
ClioError::rpcMALFORMED_AUTHORIZED_CREDENTIALS,
|
||||
fmt::format(
|
||||
"Max {} number of credentials in authorized_credentials array", ripple::maxCredentialsArraySize
|
||||
)
|
||||
}};
|
||||
}
|
||||
|
||||
for (auto const& credObj : value.as_array()) {
|
||||
if (!credObj.is_object()) {
|
||||
return Error{Status{
|
||||
ClioError::rpcMALFORMED_AUTHORIZED_CREDENTIALS,
|
||||
"authorized_credentials elements in array are not objects."
|
||||
}};
|
||||
}
|
||||
auto const& obj = credObj.as_object();
|
||||
|
||||
if (!obj.contains("issuer")) {
|
||||
return Error{
|
||||
Status{ClioError::rpcMALFORMED_AUTHORIZED_CREDENTIALS, "Field 'Issuer' is required but missing."}
|
||||
};
|
||||
}
|
||||
|
||||
// don't want to change issuer error message to be about credentials
|
||||
if (!IssuerValidator.verify(credObj, "issuer"))
|
||||
return Error{Status{ClioError::rpcMALFORMED_AUTHORIZED_CREDENTIALS, "issuer NotString"}};
|
||||
|
||||
if (!obj.contains("credential_type")) {
|
||||
return Error{Status{
|
||||
ClioError::rpcMALFORMED_AUTHORIZED_CREDENTIALS, "Field 'CredentialType' is required but missing."
|
||||
}};
|
||||
}
|
||||
|
||||
if (auto const err = CredentialTypeValidator.verify(credObj, "credential_type"); !err)
|
||||
return err;
|
||||
}
|
||||
|
||||
return MaybeError{};
|
||||
}};
|
||||
|
||||
} // namespace rpc::validation
|
||||
|
||||
@@ -27,15 +27,13 @@
|
||||
#include <boost/json/object.hpp>
|
||||
#include <boost/json/value.hpp>
|
||||
#include <fmt/core.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
|
||||
#include <concepts>
|
||||
#include <cstdint>
|
||||
#include <ctime>
|
||||
#include <functional>
|
||||
#include <initializer_list>
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
@@ -153,7 +151,7 @@ struct Type final {
|
||||
verify(boost::json::value const& value, std::string_view key) const
|
||||
{
|
||||
if (not value.is_object() or not value.as_object().contains(key.data()))
|
||||
return {}; // ignore. field does not exist, let 'required' fail instead
|
||||
return {}; // ignore. If field is supposed to exist, let 'required' fail instead
|
||||
|
||||
auto const& res = value.as_object().at(key.data());
|
||||
auto const convertible = (checkType<Types>(res) || ...);
|
||||
@@ -458,6 +456,21 @@ public:
|
||||
[[nodiscard]] bool
|
||||
checkIsU32Numeric(std::string_view sv);
|
||||
|
||||
template <class HexType>
|
||||
requires(std::is_same_v<HexType, ripple::uint160> || std::is_same_v<HexType, ripple::uint192> || std::is_same_v<HexType, ripple::uint256>)
|
||||
MaybeError
|
||||
makeHexStringValidator(boost::json::value const& value, std::string_view key)
|
||||
{
|
||||
if (!value.is_string())
|
||||
return Error{Status{RippledError::rpcINVALID_PARAMS, std::string(key) + "NotString"}};
|
||||
|
||||
HexType parsedInt;
|
||||
if (!parsedInt.parseHex(value.as_string().c_str()))
|
||||
return Error{Status{RippledError::rpcINVALID_PARAMS, std::string(key) + "Malformed"}};
|
||||
|
||||
return MaybeError{};
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief A group of custom validation functions
|
||||
*/
|
||||
@@ -492,6 +505,22 @@ struct CustomValidators final {
|
||||
*/
|
||||
static CustomValidator AccountMarkerValidator;
|
||||
|
||||
/**
|
||||
* @brief Provides a commonly used validator for uint160(AccountID) hex string.
|
||||
*
|
||||
* It must be a string and also a decodable hex.
|
||||
* AccountID uses this validator.
|
||||
*/
|
||||
static CustomValidator Uint160HexStringValidator;
|
||||
|
||||
/**
|
||||
* @brief Provides a commonly used validator for uint192 hex string.
|
||||
*
|
||||
* It must be a string and also a decodable hex.
|
||||
* MPTIssuanceID uses this validator.
|
||||
*/
|
||||
static CustomValidator Uint192HexStringValidator;
|
||||
|
||||
/**
|
||||
* @brief Provides a commonly used validator for uint256 hex string.
|
||||
*
|
||||
@@ -528,6 +557,51 @@ struct CustomValidators final {
|
||||
* Used by amm_info.
|
||||
*/
|
||||
static CustomValidator CurrencyIssueValidator;
|
||||
|
||||
/**
|
||||
* @brief Provides a validator for validating authorized_credentials json array.
|
||||
*
|
||||
* Used by deposit_preauth.
|
||||
*/
|
||||
static CustomValidator AuthorizeCredentialValidator;
|
||||
|
||||
/**
|
||||
* @brief Provides a validator for validating credential_type.
|
||||
*
|
||||
* Used by AuthorizeCredentialValidator in deposit_preauth.
|
||||
*/
|
||||
static CustomValidator CredentialTypeValidator;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Validates that the elements of the array is of type Hex256 uint
|
||||
*/
|
||||
struct Hex256ItemType final {
|
||||
/**
|
||||
* @brief Validates given the prerequisite that the type of the json value is an array,
|
||||
* verifies all values within the array is of uint256 hash
|
||||
*
|
||||
* @param value the value to verify
|
||||
* @param key The key used to retrieve the tested value from the outer object
|
||||
* @return `RippledError::rpcINVALID_PARAMS` if validation failed; otherwise no error is returned
|
||||
*/
|
||||
[[nodiscard]] static MaybeError
|
||||
verify(boost::json::value const& value, std::string_view key)
|
||||
{
|
||||
if (not value.is_object() or not value.as_object().contains(key.data()))
|
||||
return {}; // ignore. If field is supposed to exist, let 'required' fail instead
|
||||
|
||||
auto const& res = value.as_object().at(key.data());
|
||||
|
||||
// loop through each item in the array and make sure it is uint256 hex string
|
||||
for (auto const& elem : res.as_array()) {
|
||||
ripple::uint256 num;
|
||||
if (!elem.is_string() || !num.parseHex(elem.as_string())) {
|
||||
return Error{Status{RippledError::rpcINVALID_PARAMS, "Item is not a valid uint256 type."}};
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace rpc::validation
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
#include "rpc/handlers/LedgerEntry.hpp"
|
||||
#include "rpc/handlers/LedgerIndex.hpp"
|
||||
#include "rpc/handlers/LedgerRange.hpp"
|
||||
#include "rpc/handlers/MPTHolders.hpp"
|
||||
#include "rpc/handlers/NFTBuyOffers.hpp"
|
||||
#include "rpc/handlers/NFTHistory.hpp"
|
||||
#include "rpc/handlers/NFTInfo.hpp"
|
||||
@@ -97,6 +98,7 @@ ProductionHandlerProvider::ProductionHandlerProvider(
|
||||
{"ledger_entry", {LedgerEntryHandler{backend}}},
|
||||
{"ledger_index", {LedgerIndexHandler{backend}, true}}, // clio only
|
||||
{"ledger_range", {LedgerRangeHandler{backend}}},
|
||||
{"mpt_holders", {MPTHoldersHandler{backend}, true}}, // clio only
|
||||
{"nfts_by_issuer", {NFTsByIssuerHandler{backend}, true}}, // clio only
|
||||
{"nft_history", {NFTHistoryHandler{backend}, true}}, // clio only
|
||||
{"nft_buy_offers", {NFTBuyOffersHandler{backend}}},
|
||||
|
||||
@@ -55,10 +55,6 @@ class AccountObjectsHandler {
|
||||
// dependencies
|
||||
std::shared_ptr<BackendInterface> sharedPtrBackend_;
|
||||
|
||||
// constants
|
||||
static std::unordered_map<std::string, ripple::LedgerEntryType> const TYPES_MAP;
|
||||
static std::unordered_set<std::string> const TYPES_KEYS;
|
||||
|
||||
public:
|
||||
static auto constexpr LIMIT_MIN = 10;
|
||||
static auto constexpr LIMIT_MAX = 400;
|
||||
|
||||
@@ -39,7 +39,6 @@
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
@@ -57,8 +56,8 @@ class AccountTxHandler {
|
||||
std::shared_ptr<BackendInterface> sharedPtrBackend_;
|
||||
|
||||
public:
|
||||
// no max limit
|
||||
static auto constexpr LIMIT_MIN = 1;
|
||||
static auto constexpr LIMIT_MAX = 1000;
|
||||
static auto constexpr LIMIT_DEFAULT = 200;
|
||||
|
||||
/**
|
||||
@@ -133,7 +132,7 @@ public:
|
||||
{JS(limit),
|
||||
validation::Type<uint32_t>{},
|
||||
validation::Min(1u),
|
||||
modifiers::Clamp<int32_t>{LIMIT_MIN, std::numeric_limits<int32_t>::max()}},
|
||||
modifiers::Clamp<int32_t>{LIMIT_MIN, LIMIT_MAX}},
|
||||
{JS(marker),
|
||||
meta::WithCustomError{
|
||||
validation::Type<boost::json::object>{},
|
||||
|
||||
@@ -19,25 +19,32 @@
|
||||
|
||||
#include "rpc/handlers/DepositAuthorized.hpp"
|
||||
|
||||
#include "rpc/CredentialHelpers.hpp"
|
||||
#include "rpc/Errors.hpp"
|
||||
#include "rpc/JS.hpp"
|
||||
#include "rpc/RPCHelpers.hpp"
|
||||
#include "rpc/common/Types.hpp"
|
||||
#include "util/Assert.hpp"
|
||||
|
||||
#include <boost/json/array.hpp>
|
||||
#include <boost/json/conversion.hpp>
|
||||
#include <boost/json/object.hpp>
|
||||
#include <boost/json/value.hpp>
|
||||
#include <boost/json/value_to.hpp>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/basics/strHex.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/LedgerHeader.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/STObject.h>
|
||||
#include <xrpl/protocol/Serializer.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <variant>
|
||||
|
||||
namespace rpc {
|
||||
@@ -71,26 +78,55 @@ DepositAuthorizedHandler::process(DepositAuthorizedHandler::Input input, Context
|
||||
|
||||
Output response;
|
||||
|
||||
auto it = ripple::SerialIter{dstAccountLedgerObject->data(), dstAccountLedgerObject->size()};
|
||||
auto const sleDest = ripple::SLE{it, dstKeylet};
|
||||
bool const reqAuth = sleDest.isFlag(ripple::lsfDepositAuth) && (sourceAccountID != destinationAccountID);
|
||||
auto const& creds = input.credentials;
|
||||
bool const credentialsPresent = creds.has_value();
|
||||
|
||||
ripple::STArray authCreds;
|
||||
if (credentialsPresent) {
|
||||
if (creds.value().empty()) {
|
||||
return Error{Status{RippledError::rpcINVALID_PARAMS, "credential array has no elements."}};
|
||||
}
|
||||
if (creds.value().size() > ripple::maxCredentialsArraySize) {
|
||||
return Error{Status{RippledError::rpcINVALID_PARAMS, "credential array too long."}};
|
||||
}
|
||||
auto const credArray = credentials::fetchCredentialArray(
|
||||
input.credentials, *sourceAccountID, *sharedPtrBackend_, lgrInfo, ctx.yield
|
||||
);
|
||||
if (!credArray.has_value())
|
||||
return Error{std::move(credArray).error()};
|
||||
authCreds = std::move(credArray).value();
|
||||
}
|
||||
|
||||
// If the two accounts are the same OR if that flag is
|
||||
// not set, then the deposit should be fine.
|
||||
bool depositAuthorized = true;
|
||||
|
||||
if (reqAuth) {
|
||||
ripple::uint256 hashKey;
|
||||
if (credentialsPresent) {
|
||||
auto const sortedAuthCreds = credentials::createAuthCredentials(authCreds);
|
||||
ASSERT(
|
||||
sortedAuthCreds.size() == authCreds.size(), "should already be checked above that there is no duplicate"
|
||||
);
|
||||
|
||||
hashKey = ripple::keylet::depositPreauth(*destinationAccountID, sortedAuthCreds).key;
|
||||
} else {
|
||||
hashKey = ripple::keylet::depositPreauth(*destinationAccountID, *sourceAccountID).key;
|
||||
}
|
||||
|
||||
depositAuthorized = sharedPtrBackend_->fetchLedgerObject(hashKey, lgrInfo.seq, ctx.yield).has_value();
|
||||
}
|
||||
|
||||
response.sourceAccount = input.sourceAccount;
|
||||
response.destinationAccount = input.destinationAccount;
|
||||
response.ledgerHash = ripple::strHex(lgrInfo.hash);
|
||||
response.ledgerIndex = lgrInfo.seq;
|
||||
|
||||
// If the two accounts are the same, then the deposit should be fine.
|
||||
if (sourceAccountID != destinationAccountID) {
|
||||
auto it = ripple::SerialIter{dstAccountLedgerObject->data(), dstAccountLedgerObject->size()};
|
||||
auto sle = ripple::SLE{it, dstKeylet};
|
||||
|
||||
// Check destination for the DepositAuth flag.
|
||||
// If that flag is not set then a deposit should be just fine.
|
||||
if ((sle.getFieldU32(ripple::sfFlags) & ripple::lsfDepositAuth) != 0u) {
|
||||
// See if a preauthorization entry is in the ledger.
|
||||
auto const depositPreauthKeylet = ripple::keylet::depositPreauth(*destinationAccountID, *sourceAccountID);
|
||||
auto const sleDepositAuth =
|
||||
sharedPtrBackend_->fetchLedgerObject(depositPreauthKeylet.key, lgrInfo.seq, ctx.yield);
|
||||
response.depositAuthorized = static_cast<bool>(sleDepositAuth);
|
||||
}
|
||||
}
|
||||
response.depositAuthorized = depositAuthorized;
|
||||
if (credentialsPresent)
|
||||
response.credentials = input.credentials.value();
|
||||
|
||||
return response;
|
||||
}
|
||||
@@ -115,6 +151,10 @@ tag_invoke(boost::json::value_to_tag<DepositAuthorizedHandler::Input>, boost::js
|
||||
}
|
||||
}
|
||||
|
||||
if (jsonObject.contains(JS(credentials))) {
|
||||
input.credentials = boost::json::value_to<boost::json::array>(jv.at(JS(credentials)));
|
||||
}
|
||||
|
||||
return input;
|
||||
}
|
||||
|
||||
@@ -127,8 +167,10 @@ tag_invoke(boost::json::value_from_tag, boost::json::value& jv, DepositAuthorize
|
||||
{JS(destination_account), output.destinationAccount},
|
||||
{JS(ledger_hash), output.ledgerHash},
|
||||
{JS(ledger_index), output.ledgerIndex},
|
||||
{JS(validated), output.validated},
|
||||
{JS(validated), output.validated}
|
||||
};
|
||||
if (output.credentials)
|
||||
jv.as_object()[JS(credentials)] = *output.credentials;
|
||||
}
|
||||
|
||||
} // namespace rpc
|
||||
|
||||
@@ -25,8 +25,10 @@
|
||||
#include "rpc/common/Types.hpp"
|
||||
#include "rpc/common/Validators.hpp"
|
||||
|
||||
#include <boost/json/array.hpp>
|
||||
#include <boost/json/conversion.hpp>
|
||||
#include <boost/json/value.hpp>
|
||||
#include <xrpl/protocol/STArray.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
#include <cstdint>
|
||||
@@ -59,6 +61,8 @@ public:
|
||||
std::string destinationAccount;
|
||||
std::string ledgerHash;
|
||||
uint32_t ledgerIndex{};
|
||||
std::optional<boost::json::array> credentials;
|
||||
|
||||
// validated should be sent via framework
|
||||
bool validated = true;
|
||||
};
|
||||
@@ -71,6 +75,7 @@ public:
|
||||
std::string destinationAccount;
|
||||
std::optional<std::string> ledgerHash;
|
||||
std::optional<uint32_t> ledgerIndex;
|
||||
std::optional<boost::json::array> credentials;
|
||||
};
|
||||
|
||||
using Result = HandlerReturnType<Output>;
|
||||
@@ -99,6 +104,7 @@ public:
|
||||
{JS(destination_account), validation::Required{}, validation::CustomValidators::AccountValidator},
|
||||
{JS(ledger_hash), validation::CustomValidators::Uint256HexStringValidator},
|
||||
{JS(ledger_index), validation::CustomValidators::LedgerIndexValidator},
|
||||
{JS(credentials), validation::Type<boost::json::array>{}, validation::Hex256ItemType()}
|
||||
};
|
||||
|
||||
return rpcSpec;
|
||||
|
||||
@@ -142,10 +142,6 @@ GatewayBalancesHandler::process(GatewayBalancesHandler::Input input, Context con
|
||||
if (auto status = std::get_if<Status>(&ret))
|
||||
return Error{*status};
|
||||
|
||||
auto inHotbalances = [&](auto const& hw) { return output.hotBalances.contains(hw); };
|
||||
if (not std::all_of(input.hotWallets.begin(), input.hotWallets.end(), inHotbalances))
|
||||
return Error{Status{ClioError::rpcINVALID_HOT_WALLET}};
|
||||
|
||||
output.accountID = input.account;
|
||||
output.ledgerHash = ripple::strHex(lgrInfo.hash);
|
||||
output.ledgerIndex = lgrInfo.seq;
|
||||
|
||||
@@ -108,44 +108,51 @@ public:
|
||||
static RpcSpecConstRef
|
||||
spec([[maybe_unused]] uint32_t apiVersion)
|
||||
{
|
||||
static auto const hotWalletValidator =
|
||||
validation::CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError {
|
||||
if (!value.is_string() && !value.is_array())
|
||||
return Error{Status{RippledError::rpcINVALID_PARAMS, std::string(key) + "NotStringOrArray"}};
|
||||
auto const getHotWalletValidator = [](RippledError errCode) {
|
||||
return validation::CustomValidator{
|
||||
[errCode](boost::json::value const& value, std::string_view key) -> MaybeError {
|
||||
if (!value.is_string() && !value.is_array())
|
||||
return Error{Status{errCode, std::string(key) + "NotStringOrArray"}};
|
||||
|
||||
// wallet needs to be an valid accountID or public key
|
||||
auto const wallets = value.is_array() ? value.as_array() : boost::json::array{value};
|
||||
auto const getAccountID = [](auto const& j) -> std::optional<ripple::AccountID> {
|
||||
if (j.is_string()) {
|
||||
auto const pk = util::parseBase58Wrapper<ripple::PublicKey>(
|
||||
ripple::TokenType::AccountPublic, boost::json::value_to<std::string>(j)
|
||||
);
|
||||
// wallet needs to be an valid accountID or public key
|
||||
auto const wallets = value.is_array() ? value.as_array() : boost::json::array{value};
|
||||
auto const getAccountID = [](auto const& j) -> std::optional<ripple::AccountID> {
|
||||
if (j.is_string()) {
|
||||
auto const pk = util::parseBase58Wrapper<ripple::PublicKey>(
|
||||
ripple::TokenType::AccountPublic, boost::json::value_to<std::string>(j)
|
||||
);
|
||||
|
||||
if (pk)
|
||||
return ripple::calcAccountID(*pk);
|
||||
if (pk)
|
||||
return ripple::calcAccountID(*pk);
|
||||
|
||||
return util::parseBase58Wrapper<ripple::AccountID>(boost::json::value_to<std::string>(j));
|
||||
return util::parseBase58Wrapper<ripple::AccountID>(boost::json::value_to<std::string>(j));
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
||||
|
||||
for (auto const& wallet : wallets) {
|
||||
if (!getAccountID(wallet))
|
||||
return Error{Status{errCode, std::string(key) + "Malformed"}};
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
||||
|
||||
for (auto const& wallet : wallets) {
|
||||
if (!getAccountID(wallet))
|
||||
return Error{Status{RippledError::rpcINVALID_PARAMS, std::string(key) + "Malformed"}};
|
||||
return MaybeError{};
|
||||
}
|
||||
|
||||
return MaybeError{};
|
||||
}};
|
||||
|
||||
static auto const rpcSpec = RpcSpec{
|
||||
{JS(account), validation::Required{}, validation::CustomValidators::AccountValidator},
|
||||
{JS(ledger_hash), validation::CustomValidators::Uint256HexStringValidator},
|
||||
{JS(ledger_index), validation::CustomValidators::LedgerIndexValidator},
|
||||
{JS(hotwallet), hotWalletValidator}
|
||||
};
|
||||
};
|
||||
|
||||
return rpcSpec;
|
||||
static auto const kSPEC_COMMON = RpcSpec{
|
||||
{JS(account), validation::Required{}, validation::CustomValidators::AccountValidator},
|
||||
{JS(ledger_hash), validation::CustomValidators::Uint256HexStringValidator},
|
||||
{JS(ledger_index), validation::CustomValidators::LedgerIndexValidator}
|
||||
};
|
||||
|
||||
auto static const kSPEC_V1 =
|
||||
RpcSpec{kSPEC_COMMON, {{JS(hotwallet), getHotWalletValidator(ripple::rpcINVALID_HOTWALLET)}}};
|
||||
auto static const kSPEC_V2 =
|
||||
RpcSpec{kSPEC_COMMON, {{JS(hotwallet), getHotWalletValidator(ripple::rpcINVALID_PARAMS)}}};
|
||||
|
||||
return apiVersion == 1 ? kSPEC_V1 : kSPEC_V2;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -43,7 +43,6 @@
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace rpc {
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
#include "rpc/handlers/LedgerEntry.hpp"
|
||||
|
||||
#include "rpc/CredentialHelpers.hpp"
|
||||
#include "rpc/Errors.hpp"
|
||||
#include "rpc/JS.hpp"
|
||||
#include "rpc/RPCHelpers.hpp"
|
||||
@@ -30,6 +31,8 @@
|
||||
#include <boost/json/object.hpp>
|
||||
#include <boost/json/value.hpp>
|
||||
#include <boost/json/value_to.hpp>
|
||||
#include <xrpl/basics/Slice.h>
|
||||
#include <xrpl/basics/StringUtilities.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/basics/strHex.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
@@ -97,11 +100,30 @@ LedgerEntryHandler::process(LedgerEntryHandler::Input input, Context const& ctx)
|
||||
auto const owner = util::parseBase58Wrapper<ripple::AccountID>(
|
||||
boost::json::value_to<std::string>(input.depositPreauth->at(JS(owner)))
|
||||
);
|
||||
auto const authorized = util::parseBase58Wrapper<ripple::AccountID>(
|
||||
boost::json::value_to<std::string>(input.depositPreauth->at(JS(authorized)))
|
||||
);
|
||||
// Only one of authorize or authorized_credentials MUST exist;
|
||||
if (input.depositPreauth->contains(JS(authorized)) ==
|
||||
input.depositPreauth->contains(JS(authorized_credentials))) {
|
||||
return Error{
|
||||
Status{ClioError::rpcMALFORMED_REQUEST, "Must have one of authorized or authorized_credentials."}
|
||||
};
|
||||
}
|
||||
|
||||
key = ripple::keylet::depositPreauth(*owner, *authorized).key;
|
||||
if (input.depositPreauth->contains(JS(authorized))) {
|
||||
auto const authorized = util::parseBase58Wrapper<ripple::AccountID>(
|
||||
boost::json::value_to<std::string>(input.depositPreauth->at(JS(authorized)))
|
||||
);
|
||||
key = ripple::keylet::depositPreauth(*owner, *authorized).key;
|
||||
} else {
|
||||
auto const authorizedCredentials = rpc::credentials::parseAuthorizeCredentials(
|
||||
input.depositPreauth->at(JS(authorized_credentials)).as_array()
|
||||
);
|
||||
|
||||
auto const authCreds = credentials::createAuthCredentials(authorizedCredentials);
|
||||
if (authCreds.size() != authorizedCredentials.size())
|
||||
return Error{Status{ClioError::rpcMALFORMED_AUTHORIZED_CREDENTIALS, "duplicates in credentials."}};
|
||||
|
||||
key = ripple::keylet::depositPreauth(owner.value(), authCreds).key;
|
||||
}
|
||||
} else if (input.ticket) {
|
||||
auto const id =
|
||||
util::parseBase58Wrapper<ripple::AccountID>(boost::json::value_to<std::string>(input.ticket->at(JS(account))
|
||||
@@ -145,6 +167,18 @@ LedgerEntryHandler::process(LedgerEntryHandler::Input input, Context const& ctx)
|
||||
}
|
||||
} else if (input.oracleNode) {
|
||||
key = input.oracleNode.value();
|
||||
} else if (input.credential) {
|
||||
key = input.credential.value();
|
||||
} else if (input.mptIssuance) {
|
||||
auto const mptIssuanceID = ripple::uint192{std::string_view(*(input.mptIssuance))};
|
||||
key = ripple::keylet::mptIssuance(mptIssuanceID).key;
|
||||
} else if (input.mptoken) {
|
||||
auto const holder =
|
||||
ripple::parseBase58<ripple::AccountID>(boost::json::value_to<std::string>(input.mptoken->at(JS(account))));
|
||||
auto const mptIssuanceID =
|
||||
ripple::uint192{std::string_view(boost::json::value_to<std::string>(input.mptoken->at(JS(mpt_issuance_id))))
|
||||
};
|
||||
key = ripple::keylet::mptoken(mptIssuanceID, *holder).key;
|
||||
} else {
|
||||
// Must specify 1 of the following fields to indicate what type
|
||||
if (ctx.apiVersion == 1)
|
||||
@@ -277,6 +311,8 @@ tag_invoke(boost::json::value_to_tag<LedgerEntryHandler::Input>, boost::json::va
|
||||
{JS(xchain_owned_create_account_claim_id), ripple::ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID},
|
||||
{JS(xchain_owned_claim_id), ripple::ltXCHAIN_OWNED_CLAIM_ID},
|
||||
{JS(oracle), ripple::ltORACLE},
|
||||
{JS(credential), ripple::ltCREDENTIAL},
|
||||
{JS(mptoken), ripple::ltMPTOKEN},
|
||||
};
|
||||
|
||||
auto const parseBridgeFromJson = [](boost::json::value const& bridgeJson) {
|
||||
@@ -302,6 +338,16 @@ tag_invoke(boost::json::value_to_tag<LedgerEntryHandler::Input>, boost::json::va
|
||||
return ripple::keylet::oracle(*account, documentId).key;
|
||||
};
|
||||
|
||||
auto const parseCredentialFromJson = [](boost::json::value const& json) {
|
||||
auto const subject =
|
||||
util::parseBase58Wrapper<ripple::AccountID>(boost::json::value_to<std::string>(json.at(JS(subject))));
|
||||
auto const issuer =
|
||||
util::parseBase58Wrapper<ripple::AccountID>(boost::json::value_to<std::string>(json.at(JS(issuer))));
|
||||
auto const credType = ripple::strUnHex(boost::json::value_to<std::string>(json.at(JS(credential_type))));
|
||||
|
||||
return ripple::keylet::credential(*subject, *issuer, ripple::Slice(credType->data(), credType->size())).key;
|
||||
};
|
||||
|
||||
auto const indexFieldType =
|
||||
std::find_if(indexFieldTypeMap.begin(), indexFieldTypeMap.end(), [&jsonObject](auto const& pair) {
|
||||
auto const& [field, _] = pair;
|
||||
@@ -317,6 +363,8 @@ tag_invoke(boost::json::value_to_tag<LedgerEntryHandler::Input>, boost::json::va
|
||||
input.accountRoot = boost::json::value_to<std::string>(jv.at(JS(account_root)));
|
||||
} else if (jsonObject.contains(JS(did))) {
|
||||
input.did = boost::json::value_to<std::string>(jv.at(JS(did)));
|
||||
} else if (jsonObject.contains(JS(mpt_issuance))) {
|
||||
input.mptIssuance = boost::json::value_to<std::string>(jv.at(JS(mpt_issuance)));
|
||||
}
|
||||
// no need to check if_object again, validator only allows string or object
|
||||
else if (jsonObject.contains(JS(directory))) {
|
||||
@@ -348,6 +396,10 @@ tag_invoke(boost::json::value_to_tag<LedgerEntryHandler::Input>, boost::json::va
|
||||
);
|
||||
} else if (jsonObject.contains(JS(oracle))) {
|
||||
input.oracleNode = parseOracleFromJson(jv.at(JS(oracle)));
|
||||
} else if (jsonObject.contains(JS(credential))) {
|
||||
input.credential = parseCredentialFromJson(jv.at(JS(credential)));
|
||||
} else if (jsonObject.contains(JS(mptoken))) {
|
||||
input.mptoken = jv.at(JS(mptoken)).as_object();
|
||||
}
|
||||
|
||||
if (jsonObject.contains("include_deleted"))
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
#include "rpc/common/Validators.hpp"
|
||||
#include "util/AccountUtils.hpp"
|
||||
|
||||
#include <boost/json/array.hpp>
|
||||
#include <boost/json/conversion.hpp>
|
||||
#include <boost/json/object.hpp>
|
||||
#include <boost/json/value.hpp>
|
||||
@@ -91,6 +92,8 @@ public:
|
||||
std::optional<std::string> accountRoot;
|
||||
// account id to address did object
|
||||
std::optional<std::string> did;
|
||||
// mpt issuance id to address mptIssuance object
|
||||
std::optional<std::string> mptIssuance;
|
||||
// TODO: extract into custom objects, remove json from Input
|
||||
std::optional<boost::json::object> directory;
|
||||
std::optional<boost::json::object> offer;
|
||||
@@ -99,11 +102,13 @@ public:
|
||||
std::optional<boost::json::object> depositPreauth;
|
||||
std::optional<boost::json::object> ticket;
|
||||
std::optional<boost::json::object> amm;
|
||||
std::optional<boost::json::object> mptoken;
|
||||
std::optional<ripple::STXChainBridge> bridge;
|
||||
std::optional<std::string> bridgeAccount;
|
||||
std::optional<uint32_t> chainClaimId;
|
||||
std::optional<uint32_t> createAccountClaimId;
|
||||
std::optional<ripple::uint256> oracleNode;
|
||||
std::optional<ripple::uint256> credential;
|
||||
bool includeDeleted = false;
|
||||
};
|
||||
|
||||
@@ -194,7 +199,8 @@ public:
|
||||
meta::WithCustomError{
|
||||
validation::CustomValidators::AccountBase58Validator, Status(ClioError::rpcMALFORMED_OWNER)
|
||||
}},
|
||||
{JS(authorized), validation::Required{}, validation::CustomValidators::AccountBase58Validator},
|
||||
{JS(authorized), validation::CustomValidators::AccountBase58Validator},
|
||||
{JS(authorized_credentials), validation::CustomValidators::AuthorizeCredentialValidator}
|
||||
},
|
||||
}},
|
||||
{JS(directory),
|
||||
@@ -315,6 +321,59 @@ public:
|
||||
},
|
||||
meta::WithCustomError{modifiers::ToNumber{}, Status(ClioError::rpcMALFORMED_ORACLE_DOCUMENT_ID)}},
|
||||
}}},
|
||||
{JS(credential),
|
||||
meta::WithCustomError{
|
||||
validation::Type<std::string, boost::json::object>{}, Status(ClioError::rpcMALFORMED_REQUEST)
|
||||
},
|
||||
meta::IfType<std::string>{
|
||||
meta::WithCustomError{malformedRequestHexStringValidator, Status(ClioError::rpcMALFORMED_ADDRESS)}
|
||||
},
|
||||
meta::IfType<boost::json::object>{meta::Section{
|
||||
{JS(subject),
|
||||
meta::WithCustomError{validation::Required{}, Status(ClioError::rpcMALFORMED_REQUEST)},
|
||||
meta::WithCustomError{
|
||||
validation::CustomValidators::AccountBase58Validator, Status(ClioError::rpcMALFORMED_ADDRESS)
|
||||
}},
|
||||
{JS(issuer),
|
||||
meta::WithCustomError{validation::Required{}, Status(ClioError::rpcMALFORMED_REQUEST)},
|
||||
meta::WithCustomError{
|
||||
validation::CustomValidators::AccountBase58Validator, Status(ClioError::rpcMALFORMED_ADDRESS)
|
||||
}},
|
||||
{
|
||||
JS(credential_type),
|
||||
meta::WithCustomError{validation::Required{}, Status(ClioError::rpcMALFORMED_REQUEST)},
|
||||
meta::WithCustomError{validation::Type<std::string>{}, Status(ClioError::rpcMALFORMED_REQUEST)},
|
||||
},
|
||||
}}},
|
||||
{JS(mpt_issuance),
|
||||
meta::WithCustomError{
|
||||
validation::CustomValidators::Uint192HexStringValidator, Status(ClioError::rpcMALFORMED_REQUEST)
|
||||
}},
|
||||
{JS(mptoken),
|
||||
meta::WithCustomError{
|
||||
validation::Type<std::string, boost::json::object>{}, Status(ClioError::rpcMALFORMED_REQUEST)
|
||||
},
|
||||
meta::IfType<std::string>{malformedRequestHexStringValidator},
|
||||
meta::IfType<boost::json::object>{
|
||||
meta::Section{
|
||||
{
|
||||
JS(account),
|
||||
meta::WithCustomError{validation::Required{}, Status(ClioError::rpcMALFORMED_REQUEST)},
|
||||
meta::WithCustomError{
|
||||
validation::CustomValidators::AccountBase58Validator,
|
||||
Status(ClioError::rpcMALFORMED_ADDRESS)
|
||||
},
|
||||
},
|
||||
{
|
||||
JS(mpt_issuance_id),
|
||||
meta::WithCustomError{validation::Required{}, Status(ClioError::rpcMALFORMED_REQUEST)},
|
||||
meta::WithCustomError{
|
||||
validation::CustomValidators::Uint192HexStringValidator,
|
||||
Status(ClioError::rpcMALFORMED_REQUEST)
|
||||
},
|
||||
},
|
||||
},
|
||||
}},
|
||||
{JS(ledger), check::Deprecated{}},
|
||||
{"include_deleted", validation::Type<bool>{}},
|
||||
};
|
||||
|
||||
139
src/rpc/handlers/MPTHolders.cpp
Normal file
139
src/rpc/handlers/MPTHolders.cpp
Normal file
@@ -0,0 +1,139 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2024, 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 "rpc/handlers/MPTHolders.hpp"
|
||||
|
||||
#include "rpc/Errors.hpp"
|
||||
#include "rpc/JS.hpp"
|
||||
#include "rpc/RPCHelpers.hpp"
|
||||
#include "rpc/common/Types.hpp"
|
||||
|
||||
#include <boost/json/array.hpp>
|
||||
#include <boost/json/conversion.hpp>
|
||||
#include <boost/json/object.hpp>
|
||||
#include <boost/json/value.hpp>
|
||||
#include <ripple/basics/base_uint.h>
|
||||
#include <ripple/basics/strHex.h>
|
||||
#include <ripple/protocol/AccountID.h>
|
||||
#include <ripple/protocol/Indexes.h>
|
||||
#include <ripple/protocol/LedgerHeader.h>
|
||||
#include <ripple/protocol/jss.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <variant>
|
||||
|
||||
using namespace ripple;
|
||||
|
||||
namespace rpc {
|
||||
|
||||
MPTHoldersHandler::Result
|
||||
MPTHoldersHandler::process(MPTHoldersHandler::Input input, Context const& ctx) const
|
||||
{
|
||||
auto const range = sharedPtrBackend_->fetchLedgerRange();
|
||||
auto const lgrInfoOrStatus = getLedgerHeaderFromHashOrSeq(
|
||||
*sharedPtrBackend_, ctx.yield, input.ledgerHash, input.ledgerIndex, range->maxSequence
|
||||
);
|
||||
if (auto const status = std::get_if<Status>(&lgrInfoOrStatus))
|
||||
return Error{*status};
|
||||
|
||||
auto const lgrInfo = std::get<LedgerInfo>(lgrInfoOrStatus);
|
||||
auto const limit = input.limit.value_or(MPTHoldersHandler::LIMIT_DEFAULT);
|
||||
auto const mptID = ripple::uint192{input.mptID.c_str()};
|
||||
|
||||
auto const issuanceLedgerObject =
|
||||
sharedPtrBackend_->fetchLedgerObject(ripple::keylet::mptIssuance(mptID).key, lgrInfo.seq, ctx.yield);
|
||||
if (!issuanceLedgerObject)
|
||||
return Error{Status{RippledError::rpcOBJECT_NOT_FOUND, "objectNotFound"}};
|
||||
|
||||
std::optional<ripple::AccountID> cursor;
|
||||
if (input.marker)
|
||||
cursor = ripple::AccountID{input.marker->c_str()};
|
||||
|
||||
auto const dbResponse = sharedPtrBackend_->fetchMPTHolders(mptID, limit, cursor, lgrInfo.seq, ctx.yield);
|
||||
auto output = MPTHoldersHandler::Output{};
|
||||
output.mptID = to_string(mptID);
|
||||
output.limit = limit;
|
||||
output.ledgerIndex = lgrInfo.seq;
|
||||
|
||||
boost::json::array const mpts;
|
||||
for (auto const& mpt : dbResponse.mptokens) {
|
||||
ripple::STLedgerEntry const sle{ripple::SerialIter{mpt.data(), mpt.size()}, keylet::mptIssuance(mptID).key};
|
||||
boost::json::object mptJson;
|
||||
|
||||
mptJson[JS(account)] = toBase58(sle[ripple::sfAccount]);
|
||||
mptJson[JS(flags)] = sle.getFlags();
|
||||
mptJson["mpt_amount"] =
|
||||
toBoostJson(ripple::STUInt64{ripple::sfMPTAmount, sle[ripple::sfMPTAmount]}.getJson(JsonOptions::none));
|
||||
mptJson["mptoken_index"] = ripple::to_string(ripple::keylet::mptoken(mptID, sle[ripple::sfAccount]).key);
|
||||
|
||||
output.mpts.push_back(mptJson);
|
||||
}
|
||||
|
||||
if (dbResponse.cursor.has_value())
|
||||
output.marker = strHex(*dbResponse.cursor);
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
void
|
||||
tag_invoke(boost::json::value_from_tag, boost::json::value& jv, MPTHoldersHandler::Output const& output)
|
||||
{
|
||||
jv = {
|
||||
{JS(mpt_issuance_id), output.mptID},
|
||||
{JS(limit), output.limit},
|
||||
{JS(ledger_index), output.ledgerIndex},
|
||||
{"mptokens", output.mpts},
|
||||
{JS(validated), output.validated},
|
||||
};
|
||||
|
||||
if (output.marker.has_value())
|
||||
jv.as_object()[JS(marker)] = *(output.marker);
|
||||
}
|
||||
|
||||
MPTHoldersHandler::Input
|
||||
tag_invoke(boost::json::value_to_tag<MPTHoldersHandler::Input>, boost::json::value const& jv)
|
||||
{
|
||||
auto const& jsonObject = jv.as_object();
|
||||
MPTHoldersHandler::Input input;
|
||||
|
||||
input.mptID = jsonObject.at(JS(mpt_issuance_id)).as_string().c_str();
|
||||
|
||||
if (jsonObject.contains(JS(ledger_hash)))
|
||||
input.ledgerHash = jsonObject.at(JS(ledger_hash)).as_string().c_str();
|
||||
|
||||
if (jsonObject.contains(JS(ledger_index))) {
|
||||
if (!jsonObject.at(JS(ledger_index)).is_string()) {
|
||||
input.ledgerIndex = jsonObject.at(JS(ledger_index)).as_int64();
|
||||
} else if (jsonObject.at(JS(ledger_index)).as_string() != "validated") {
|
||||
input.ledgerIndex = std::stoi(jsonObject.at(JS(ledger_index)).as_string().c_str());
|
||||
}
|
||||
}
|
||||
|
||||
if (jsonObject.contains(JS(limit)))
|
||||
input.limit = jsonObject.at(JS(limit)).as_int64();
|
||||
|
||||
if (jsonObject.contains(JS(marker)))
|
||||
input.marker = jsonObject.at(JS(marker)).as_string().c_str();
|
||||
|
||||
return input;
|
||||
}
|
||||
} // namespace rpc
|
||||
128
src/rpc/handlers/MPTHolders.hpp
Normal file
128
src/rpc/handlers/MPTHolders.hpp
Normal file
@@ -0,0 +1,128 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2024, 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 "data/BackendInterface.hpp"
|
||||
#include "rpc/JS.hpp"
|
||||
#include "rpc/common/Modifiers.hpp"
|
||||
#include "rpc/common/Specs.hpp"
|
||||
#include "rpc/common/Types.hpp"
|
||||
#include "rpc/common/Validators.hpp"
|
||||
|
||||
namespace rpc {
|
||||
|
||||
/**
|
||||
* @brief The mpt_holders command asks the Clio server for all holders of a particular MPTokenIssuance.
|
||||
*/
|
||||
class MPTHoldersHandler {
|
||||
std::shared_ptr<BackendInterface> sharedPtrBackend_;
|
||||
|
||||
public:
|
||||
static auto constexpr LIMIT_MIN = 1;
|
||||
static auto constexpr LIMIT_MAX = 100;
|
||||
static auto constexpr LIMIT_DEFAULT = 50;
|
||||
|
||||
/**
|
||||
* @brief A struct to hold the output data of the command
|
||||
*/
|
||||
struct Output {
|
||||
boost::json::array mpts;
|
||||
uint32_t ledgerIndex;
|
||||
std::string mptID;
|
||||
bool validated = true;
|
||||
uint32_t limit;
|
||||
std::optional<std::string> marker;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief A struct to hold the input data for the command
|
||||
*/
|
||||
struct Input {
|
||||
std::string mptID;
|
||||
std::optional<std::string> ledgerHash;
|
||||
std::optional<uint32_t> ledgerIndex;
|
||||
std::optional<std::string> marker;
|
||||
std::optional<uint32_t> limit;
|
||||
};
|
||||
|
||||
using Result = HandlerReturnType<Output>;
|
||||
|
||||
/**
|
||||
* @brief Construct a new MPTHoldersHandler object
|
||||
*
|
||||
* @param sharedPtrBackend The backend to use
|
||||
*/
|
||||
MPTHoldersHandler(std::shared_ptr<BackendInterface> const& sharedPtrBackend) : sharedPtrBackend_(sharedPtrBackend)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Returns the API specification for the command
|
||||
*
|
||||
* @param apiVersion The api version to return the spec for
|
||||
* @return The spec for the given apiVersion
|
||||
*/
|
||||
static RpcSpecConstRef
|
||||
spec([[maybe_unused]] uint32_t apiVersion)
|
||||
{
|
||||
static auto const rpcSpec = RpcSpec{
|
||||
{JS(mpt_issuance_id), validation::Required{}, validation::CustomValidators::Uint192HexStringValidator},
|
||||
{JS(ledger_hash), validation::CustomValidators::Uint256HexStringValidator},
|
||||
{JS(ledger_index), validation::CustomValidators::LedgerIndexValidator},
|
||||
{JS(limit),
|
||||
validation::Type<uint32_t>{},
|
||||
validation::Min(1u),
|
||||
modifiers::Clamp<int32_t>{LIMIT_MIN, LIMIT_MAX}},
|
||||
{JS(marker), validation::CustomValidators::Uint160HexStringValidator},
|
||||
};
|
||||
|
||||
return rpcSpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Process the MPTHolders command
|
||||
*
|
||||
* @param input The input data for the command
|
||||
* @param ctx The context of the request
|
||||
* @return The result of the operation
|
||||
*/
|
||||
Result
|
||||
process(Input input, Context const& ctx) const;
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief Convert the Output to a JSON object
|
||||
*
|
||||
* @param [out] jv The JSON object to convert to
|
||||
* @param output The output to convert
|
||||
*/
|
||||
friend void
|
||||
tag_invoke(boost::json::value_from_tag, boost::json::value& jv, Output const& output);
|
||||
|
||||
/**
|
||||
* @brief Convert a JSON object to Input type
|
||||
*
|
||||
* @param jv The JSON object to convert
|
||||
* @return Input parsed from the JSON object
|
||||
*/
|
||||
friend Input
|
||||
tag_invoke(boost::json::value_to_tag<Input>, boost::json::value const& jv);
|
||||
};
|
||||
} // namespace rpc
|
||||
@@ -19,6 +19,8 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/basics/StringUtilities.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/tokens.h>
|
||||
|
||||
#include <cctype>
|
||||
|
||||
@@ -4,6 +4,7 @@ target_sources(
|
||||
clio_util
|
||||
PRIVATE build/Build.cpp
|
||||
config/Config.cpp
|
||||
CoroutineGroup.cpp
|
||||
log/Logger.cpp
|
||||
prometheus/Http.cpp
|
||||
prometheus/Label.cpp
|
||||
@@ -19,6 +20,7 @@ target_sources(
|
||||
requests/Types.cpp
|
||||
requests/WsConnection.cpp
|
||||
requests/impl/SslContext.cpp
|
||||
ResponseExpirationCache.cpp
|
||||
SignalsHandler.cpp
|
||||
Taggable.cpp
|
||||
TerminationHandler.cpp
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <type_traits>
|
||||
|
||||
namespace util {
|
||||
@@ -29,4 +31,19 @@ namespace util {
|
||||
template <typename T>
|
||||
concept SomeNumberType = std::is_arithmetic_v<T> && !std::is_same_v<T, bool> && !std::is_const_v<T>;
|
||||
|
||||
/**
|
||||
* @brief Checks that the list of given values contains no duplicates
|
||||
*
|
||||
* @param values The list of values to check
|
||||
* @returns true if no duplicates exist; false otherwise
|
||||
*/
|
||||
static consteval auto
|
||||
hasNoDuplicates(auto&&... values)
|
||||
{
|
||||
auto store = std::array{values...};
|
||||
auto end = store.end();
|
||||
std::ranges::sort(store);
|
||||
return (std::unique(std::begin(store), end) == end);
|
||||
}
|
||||
|
||||
} // namespace util
|
||||
|
||||
82
src/util/CoroutineGroup.cpp
Normal file
82
src/util/CoroutineGroup.cpp
Normal file
@@ -0,0 +1,82 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2024, 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/CoroutineGroup.hpp"
|
||||
|
||||
#include "util/Assert.hpp"
|
||||
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/asio/steady_timer.hpp>
|
||||
|
||||
#include <cstddef>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <utility>
|
||||
|
||||
namespace util {
|
||||
|
||||
CoroutineGroup::CoroutineGroup(boost::asio::yield_context yield, std::optional<int> maxChildren)
|
||||
: timer_{yield.get_executor(), boost::asio::steady_timer::duration::max()}, maxChildren_{maxChildren}
|
||||
{
|
||||
}
|
||||
|
||||
CoroutineGroup::~CoroutineGroup()
|
||||
{
|
||||
ASSERT(childrenCounter_ == 0, "CoroutineGroup is destroyed without waiting for child coroutines to finish");
|
||||
}
|
||||
|
||||
bool
|
||||
CoroutineGroup::canSpawn() const
|
||||
{
|
||||
return not maxChildren_.has_value() or childrenCounter_ < *maxChildren_;
|
||||
}
|
||||
|
||||
bool
|
||||
CoroutineGroup::spawn(boost::asio::yield_context yield, std::function<void(boost::asio::yield_context)> fn)
|
||||
{
|
||||
if (not canSpawn())
|
||||
return false;
|
||||
|
||||
++childrenCounter_;
|
||||
boost::asio::spawn(yield, [this, fn = std::move(fn)](boost::asio::yield_context yield) {
|
||||
fn(yield);
|
||||
--childrenCounter_;
|
||||
if (childrenCounter_ == 0)
|
||||
timer_.cancel();
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
void
|
||||
CoroutineGroup::asyncWait(boost::asio::yield_context yield)
|
||||
{
|
||||
if (childrenCounter_ == 0)
|
||||
return;
|
||||
|
||||
boost::system::error_code error;
|
||||
timer_.async_wait(yield[error]);
|
||||
}
|
||||
|
||||
size_t
|
||||
CoroutineGroup::size() const
|
||||
{
|
||||
return childrenCounter_;
|
||||
}
|
||||
|
||||
} // namespace util
|
||||
96
src/util/CoroutineGroup.hpp
Normal file
96
src/util/CoroutineGroup.hpp
Normal file
@@ -0,0 +1,96 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2024, 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 <boost/asio/spawn.hpp>
|
||||
#include <boost/asio/steady_timer.hpp>
|
||||
|
||||
#include <cstddef>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
|
||||
namespace util {
|
||||
|
||||
/**
|
||||
* @brief CoroutineGroup is a helper class to manage a group of coroutines. It allows to spawn multiple coroutines and
|
||||
* wait for all of them to finish.
|
||||
*/
|
||||
class CoroutineGroup {
|
||||
boost::asio::steady_timer timer_;
|
||||
std::optional<int> maxChildren_;
|
||||
int childrenCounter_{0};
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Construct a new Coroutine Group object
|
||||
*
|
||||
* @param yield The yield context to use for the internal timer
|
||||
* @param maxChildren The maximum number of coroutines that can be spawned at the same time. If not provided, there
|
||||
* is no limit
|
||||
*/
|
||||
CoroutineGroup(boost::asio::yield_context yield, std::optional<int> maxChildren = std::nullopt);
|
||||
|
||||
/**
|
||||
* @brief Destroy the Coroutine Group object
|
||||
*
|
||||
* @note asyncWait() must be called before the object is destroyed
|
||||
*/
|
||||
~CoroutineGroup();
|
||||
|
||||
/**
|
||||
* @brief Check if a new coroutine can be spawned (i.e. there is space for a new coroutine in the group)
|
||||
*
|
||||
* @return true If a new coroutine can be spawned. false if the maximum number of coroutines has been reached
|
||||
*/
|
||||
bool
|
||||
canSpawn() const;
|
||||
|
||||
/**
|
||||
* @brief Spawn a new coroutine in the group
|
||||
*
|
||||
* @param yield The yield context to use for the coroutine (it should be the same as the one used in the
|
||||
* constructor)
|
||||
* @param fn The function to execute
|
||||
* @return true If the coroutine was spawned successfully. false if the maximum number of coroutines has been
|
||||
* reached
|
||||
*/
|
||||
bool
|
||||
spawn(boost::asio::yield_context yield, std::function<void(boost::asio::yield_context)> fn);
|
||||
|
||||
/**
|
||||
* @brief Wait for all the coroutines in the group to finish
|
||||
*
|
||||
* @note This method must be called before the object is destroyed
|
||||
*
|
||||
* @param yield The yield context to use for the internal timer
|
||||
*/
|
||||
void
|
||||
asyncWait(boost::asio::yield_context yield);
|
||||
|
||||
/**
|
||||
* @brief Get the number of coroutines in the group
|
||||
*
|
||||
* @return size_t The number of coroutines in the group
|
||||
*/
|
||||
size_t
|
||||
size() const;
|
||||
};
|
||||
|
||||
} // namespace util
|
||||
@@ -112,7 +112,10 @@ class LedgerTypes {
|
||||
),
|
||||
LedgerTypeAttribute::AccountOwnedLedgerType(JS(did), ripple::ltDID),
|
||||
LedgerTypeAttribute::AccountOwnedLedgerType(JS(oracle), ripple::ltORACLE),
|
||||
LedgerTypeAttribute::AccountOwnedLedgerType(JS(credential), ripple::ltCREDENTIAL),
|
||||
LedgerTypeAttribute::ChainLedgerType(JS(nunl), ripple::ltNEGATIVE_UNL),
|
||||
LedgerTypeAttribute::DeletionBlockerLedgerType(JS(mpt_issuance), ripple::ltMPTOKEN_ISSUANCE),
|
||||
LedgerTypeAttribute::DeletionBlockerLedgerType(JS(mptoken), ripple::ltMPTOKEN),
|
||||
};
|
||||
|
||||
public:
|
||||
|
||||
@@ -17,88 +17,56 @@
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include "etl/impl/ForwardingCache.hpp"
|
||||
#include "util/ResponseExpirationCache.hpp"
|
||||
|
||||
#include "util/Assert.hpp"
|
||||
|
||||
#include <boost/json/object.hpp>
|
||||
#include <boost/json/value_to.hpp>
|
||||
|
||||
#include <chrono>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <shared_mutex>
|
||||
#include <string>
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
|
||||
namespace etl::impl {
|
||||
|
||||
namespace {
|
||||
|
||||
std::optional<std::string>
|
||||
getCommand(boost::json::object const& request)
|
||||
{
|
||||
if (not request.contains("command")) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return boost::json::value_to<std::string>(request.at("command"));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
namespace util {
|
||||
|
||||
void
|
||||
CacheEntry::put(boost::json::object response)
|
||||
ResponseExpirationCache::Entry::put(boost::json::object response)
|
||||
{
|
||||
response_ = std::move(response);
|
||||
lastUpdated_ = std::chrono::steady_clock::now();
|
||||
}
|
||||
|
||||
std::optional<boost::json::object>
|
||||
CacheEntry::get() const
|
||||
ResponseExpirationCache::Entry::get() const
|
||||
{
|
||||
return response_;
|
||||
}
|
||||
|
||||
std::chrono::steady_clock::time_point
|
||||
CacheEntry::lastUpdated() const
|
||||
ResponseExpirationCache::Entry::lastUpdated() const
|
||||
{
|
||||
return lastUpdated_;
|
||||
}
|
||||
|
||||
void
|
||||
CacheEntry::invalidate()
|
||||
ResponseExpirationCache::Entry::invalidate()
|
||||
{
|
||||
response_.reset();
|
||||
}
|
||||
|
||||
std::unordered_set<std::string> const
|
||||
ForwardingCache::CACHEABLE_COMMANDS{"server_info", "server_state", "server_definitions", "fee", "ledger_closed"};
|
||||
|
||||
ForwardingCache::ForwardingCache(std::chrono::steady_clock::duration const cacheTimeout) : cacheTimeout_{cacheTimeout}
|
||||
{
|
||||
for (auto const& command : CACHEABLE_COMMANDS) {
|
||||
cache_.emplace(command, CacheEntry{});
|
||||
}
|
||||
}
|
||||
|
||||
bool
|
||||
ForwardingCache::shouldCache(boost::json::object const& request)
|
||||
ResponseExpirationCache::shouldCache(std::string const& cmd)
|
||||
{
|
||||
auto const command = getCommand(request);
|
||||
return command.has_value() and CACHEABLE_COMMANDS.contains(*command);
|
||||
return cache_.contains(cmd);
|
||||
}
|
||||
|
||||
std::optional<boost::json::object>
|
||||
ForwardingCache::get(boost::json::object const& request) const
|
||||
ResponseExpirationCache::get(std::string const& cmd) const
|
||||
{
|
||||
auto const command = getCommand(request);
|
||||
|
||||
if (not command.has_value()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
auto it = cache_.find(*command);
|
||||
auto it = cache_.find(cmd);
|
||||
if (it == cache_.end())
|
||||
return std::nullopt;
|
||||
|
||||
@@ -110,20 +78,19 @@ ForwardingCache::get(boost::json::object const& request) const
|
||||
}
|
||||
|
||||
void
|
||||
ForwardingCache::put(boost::json::object const& request, boost::json::object const& response)
|
||||
ResponseExpirationCache::put(std::string const& cmd, boost::json::object const& response)
|
||||
{
|
||||
auto const command = getCommand(request);
|
||||
if (not command.has_value() or not shouldCache(request))
|
||||
if (not shouldCache(cmd))
|
||||
return;
|
||||
|
||||
ASSERT(cache_.contains(*command), "Command is not in the cache: {}", *command);
|
||||
ASSERT(cache_.contains(cmd), "Command is not in the cache: {}", cmd);
|
||||
|
||||
auto entry = cache_[*command].lock<std::unique_lock>();
|
||||
auto entry = cache_[cmd].lock<std::unique_lock>();
|
||||
entry->put(response);
|
||||
}
|
||||
|
||||
void
|
||||
ForwardingCache::invalidate()
|
||||
ResponseExpirationCache::invalidate()
|
||||
{
|
||||
for (auto& [_, entry] : cache_) {
|
||||
auto entryLock = entry.lock<std::unique_lock>();
|
||||
@@ -131,4 +98,4 @@ ForwardingCache::invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace etl::impl
|
||||
} // namespace util
|
||||
@@ -30,90 +30,92 @@
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace etl::impl {
|
||||
namespace util {
|
||||
|
||||
/**
|
||||
* @brief A class to store a cache entry.
|
||||
* @brief Cache of requests' responses with TTL support and configurable cachable commands
|
||||
*/
|
||||
class CacheEntry {
|
||||
std::chrono::steady_clock::time_point lastUpdated_;
|
||||
std::optional<boost::json::object> response_;
|
||||
|
||||
public:
|
||||
class ResponseExpirationCache {
|
||||
/**
|
||||
* @brief Put a response into the cache
|
||||
*
|
||||
* @param response The response to store
|
||||
* @brief A class to store a cache entry.
|
||||
*/
|
||||
void
|
||||
put(boost::json::object response);
|
||||
class Entry {
|
||||
std::chrono::steady_clock::time_point lastUpdated_;
|
||||
std::optional<boost::json::object> response_;
|
||||
|
||||
/**
|
||||
* @brief Get the response from the cache
|
||||
*
|
||||
* @return The response
|
||||
*/
|
||||
std::optional<boost::json::object>
|
||||
get() const;
|
||||
public:
|
||||
/**
|
||||
* @brief Put a response into the cache
|
||||
*
|
||||
* @param response The response to store
|
||||
*/
|
||||
void
|
||||
put(boost::json::object response);
|
||||
|
||||
/**
|
||||
* @brief Get the last time the cache was updated
|
||||
*
|
||||
* @return The last time the cache was updated
|
||||
*/
|
||||
std::chrono::steady_clock::time_point
|
||||
lastUpdated() const;
|
||||
/**
|
||||
* @brief Get the response from the cache
|
||||
*
|
||||
* @return The response
|
||||
*/
|
||||
std::optional<boost::json::object>
|
||||
get() const;
|
||||
|
||||
/**
|
||||
* @brief Invalidate the cache entry
|
||||
*/
|
||||
void
|
||||
invalidate();
|
||||
};
|
||||
/**
|
||||
* @brief Get the last time the cache was updated
|
||||
*
|
||||
* @return The last time the cache was updated
|
||||
*/
|
||||
std::chrono::steady_clock::time_point
|
||||
lastUpdated() const;
|
||||
|
||||
/**
|
||||
* @brief Invalidate the cache entry
|
||||
*/
|
||||
void
|
||||
invalidate();
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief A class to store a cache of forwarding responses
|
||||
*/
|
||||
class ForwardingCache {
|
||||
std::chrono::steady_clock::duration cacheTimeout_;
|
||||
std::unordered_map<std::string, util::Mutex<CacheEntry, std::shared_mutex>> cache_;
|
||||
std::unordered_map<std::string, util::Mutex<Entry, std::shared_mutex>> cache_;
|
||||
|
||||
bool
|
||||
shouldCache(std::string const& cmd);
|
||||
|
||||
public:
|
||||
static std::unordered_set<std::string> const CACHEABLE_COMMANDS;
|
||||
|
||||
/**
|
||||
* @brief Construct a new Forwarding Cache object
|
||||
* @brief Construct a new Cache object
|
||||
*
|
||||
* @param cacheTimeout The time for cache entries to expire
|
||||
* @param cmds The commands that should be cached
|
||||
*/
|
||||
ForwardingCache(std::chrono::steady_clock::duration cacheTimeout);
|
||||
|
||||
/**
|
||||
* @brief Check if a request should be cached
|
||||
*
|
||||
* @param request The request to check
|
||||
* @return true if the request should be cached and false otherwise
|
||||
*/
|
||||
[[nodiscard]] static bool
|
||||
shouldCache(boost::json::object const& request);
|
||||
ResponseExpirationCache(
|
||||
std::chrono::steady_clock::duration cacheTimeout,
|
||||
std::unordered_set<std::string> const& cmds
|
||||
)
|
||||
: cacheTimeout_(cacheTimeout)
|
||||
{
|
||||
for (auto const& command : cmds) {
|
||||
cache_.emplace(command, Entry{});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get a response from the cache
|
||||
*
|
||||
* @param request The request to get the response for
|
||||
* @param cmd The command to get the response for
|
||||
* @return The response if it exists or std::nullopt otherwise
|
||||
*/
|
||||
[[nodiscard]] std::optional<boost::json::object>
|
||||
get(boost::json::object const& request) const;
|
||||
get(std::string const& cmd) const;
|
||||
|
||||
/**
|
||||
* @brief Put a response into the cache if the request should be cached
|
||||
*
|
||||
* @param request The request to store the response for
|
||||
* @param cmd The command to store the response for
|
||||
* @param response The response to store
|
||||
*/
|
||||
void
|
||||
put(boost::json::object const& request, boost::json::object const& response);
|
||||
put(std::string const& cmd, boost::json::object const& response);
|
||||
|
||||
/**
|
||||
* @brief Invalidate all entries in the cache
|
||||
@@ -121,5 +123,4 @@ public:
|
||||
void
|
||||
invalidate();
|
||||
};
|
||||
|
||||
} // namespace etl::impl
|
||||
} // namespace util
|
||||
@@ -57,10 +57,16 @@ Retry::Retry(RetryStrategyPtr strategy, boost::asio::strand<boost::asio::io_cont
|
||||
{
|
||||
}
|
||||
|
||||
Retry::~Retry()
|
||||
{
|
||||
*canceled_ = true;
|
||||
}
|
||||
|
||||
void
|
||||
Retry::cancel()
|
||||
{
|
||||
timer_.cancel();
|
||||
*canceled_ = true;
|
||||
}
|
||||
|
||||
size_t
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#include <boost/asio/steady_timer.hpp>
|
||||
#include <boost/asio/strand.hpp>
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <memory>
|
||||
@@ -80,6 +81,7 @@ class Retry {
|
||||
RetryStrategyPtr strategy_;
|
||||
boost::asio::steady_timer timer_;
|
||||
size_t attemptNumber_ = 0;
|
||||
std::shared_ptr<std::atomic_bool> canceled_{std::make_shared<std::atomic_bool>(false)};
|
||||
|
||||
public:
|
||||
/**
|
||||
@@ -90,6 +92,11 @@ public:
|
||||
*/
|
||||
Retry(RetryStrategyPtr strategy, boost::asio::strand<boost::asio::io_context::executor_type> strand);
|
||||
|
||||
/**
|
||||
* @brief Destroy the Retry object
|
||||
*/
|
||||
~Retry();
|
||||
|
||||
/**
|
||||
* @brief Schedule a retry
|
||||
*
|
||||
@@ -100,15 +107,18 @@ public:
|
||||
void
|
||||
retry(Fn&& func)
|
||||
{
|
||||
*canceled_ = false;
|
||||
timer_.expires_after(strategy_->getDelay());
|
||||
strategy_->increaseDelay();
|
||||
timer_.async_wait([this, func = std::forward<Fn>(func)](boost::system::error_code const& ec) {
|
||||
if (ec == boost::asio::error::operation_aborted) {
|
||||
return;
|
||||
timer_.async_wait(
|
||||
[this, canceled = canceled_, func = std::forward<Fn>(func)](boost::system::error_code const& ec) {
|
||||
if (ec == boost::asio::error::operation_aborted or *canceled) {
|
||||
return;
|
||||
}
|
||||
++attemptNumber_;
|
||||
func();
|
||||
}
|
||||
++attemptNumber_;
|
||||
func();
|
||||
});
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
71
src/util/WithTimeout.hpp
Normal file
71
src/util/WithTimeout.hpp
Normal file
@@ -0,0 +1,71 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2024, 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 <boost/asio/associated_executor.hpp>
|
||||
#include <boost/asio/bind_cancellation_slot.hpp>
|
||||
#include <boost/asio/cancellation_signal.hpp>
|
||||
#include <boost/asio/cancellation_type.hpp>
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/asio/steady_timer.hpp>
|
||||
#include <boost/system/detail/error_code.hpp>
|
||||
#include <boost/system/errc.hpp>
|
||||
|
||||
#include <chrono>
|
||||
#include <ctime>
|
||||
#include <memory>
|
||||
|
||||
namespace util {
|
||||
|
||||
/**
|
||||
* @brief Perform a coroutine operation with a timeout.
|
||||
*
|
||||
* @tparam Operation The operation type to perform. Must be a callable accepting yield context with bound cancellation
|
||||
* token.
|
||||
* @param operation The operation to perform.
|
||||
* @param yield The yield context.
|
||||
* @param timeout The timeout duration.
|
||||
* @return The error code of the operation.
|
||||
*/
|
||||
template <typename Operation>
|
||||
boost::system::error_code
|
||||
withTimeout(Operation&& operation, boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout)
|
||||
{
|
||||
boost::system::error_code error;
|
||||
auto operationCompleted = std::make_shared<bool>(false);
|
||||
boost::asio::cancellation_signal cancellationSignal;
|
||||
auto cyield = boost::asio::bind_cancellation_slot(cancellationSignal.slot(), yield[error]);
|
||||
|
||||
boost::asio::steady_timer timer{boost::asio::get_associated_executor(cyield), timeout};
|
||||
timer.async_wait([&cancellationSignal, operationCompleted](boost::system::error_code errorCode) {
|
||||
if (!errorCode and !*operationCompleted)
|
||||
cancellationSignal.emit(boost::asio::cancellation_type::terminal);
|
||||
});
|
||||
operation(cyield);
|
||||
*operationCompleted = true;
|
||||
|
||||
// Map error code to timeout
|
||||
if (error == boost::system::errc::operation_canceled) {
|
||||
return boost::system::errc::make_error_code(boost::system::errc::timed_out);
|
||||
}
|
||||
return error;
|
||||
}
|
||||
|
||||
} // namespace util
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "util/WithTimeout.hpp"
|
||||
#include "util/requests/Types.hpp"
|
||||
#include "util/requests/WsConnection.hpp"
|
||||
|
||||
@@ -67,15 +68,13 @@ public:
|
||||
|
||||
auto operation = [&](auto&& token) { ws_.async_read(buffer, token); };
|
||||
if (timeout) {
|
||||
withTimeout(operation, yield[errorCode], *timeout);
|
||||
errorCode = util::withTimeout(operation, yield[errorCode], *timeout);
|
||||
} else {
|
||||
operation(yield[errorCode]);
|
||||
}
|
||||
|
||||
if (errorCode) {
|
||||
errorCode = mapError(errorCode);
|
||||
if (errorCode)
|
||||
return std::unexpected{RequestError{"Read error", errorCode}};
|
||||
}
|
||||
|
||||
return boost::beast::buffers_to_string(std::move(buffer).data());
|
||||
}
|
||||
@@ -90,15 +89,13 @@ public:
|
||||
boost::beast::error_code errorCode;
|
||||
auto operation = [&](auto&& token) { ws_.async_write(boost::asio::buffer(message), token); };
|
||||
if (timeout) {
|
||||
withTimeout(operation, yield[errorCode], *timeout);
|
||||
errorCode = util::withTimeout(operation, yield, *timeout);
|
||||
} else {
|
||||
operation(yield[errorCode]);
|
||||
}
|
||||
|
||||
if (errorCode) {
|
||||
errorCode = mapError(errorCode);
|
||||
if (errorCode)
|
||||
return RequestError{"Write error", errorCode};
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
@@ -119,36 +116,6 @@ public:
|
||||
return RequestError{"Close error", errorCode};
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
private:
|
||||
template <typename Operation>
|
||||
static void
|
||||
withTimeout(Operation&& operation, boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout)
|
||||
{
|
||||
auto isCompleted = std::make_shared<bool>(false);
|
||||
boost::asio::cancellation_signal cancellationSignal;
|
||||
auto cyield = boost::asio::bind_cancellation_slot(cancellationSignal.slot(), yield);
|
||||
|
||||
boost::asio::steady_timer timer{boost::asio::get_associated_executor(cyield), timeout};
|
||||
|
||||
// The timer below can be called with no error code even if the operation is completed before the timeout, so we
|
||||
// need an additional flag here
|
||||
timer.async_wait([&cancellationSignal, isCompleted](boost::system::error_code errorCode) {
|
||||
if (!errorCode and not *isCompleted)
|
||||
cancellationSignal.emit(boost::asio::cancellation_type::terminal);
|
||||
});
|
||||
operation(cyield);
|
||||
*isCompleted = true;
|
||||
}
|
||||
|
||||
static boost::system::error_code
|
||||
mapError(boost::system::error_code const ec)
|
||||
{
|
||||
if (ec == boost::system::errc::operation_canceled) {
|
||||
return boost::system::errc::make_error_code(boost::system::errc::timed_out);
|
||||
}
|
||||
return ec;
|
||||
}
|
||||
};
|
||||
|
||||
using PlainWsConnection = WsConnectionImpl<boost::beast::websocket::stream<boost::beast::tcp_stream>>;
|
||||
|
||||
@@ -3,13 +3,17 @@ add_library(clio_web)
|
||||
target_sources(
|
||||
clio_web
|
||||
PRIVATE Resolver.cpp
|
||||
Server.cpp
|
||||
dosguard/DOSGuard.cpp
|
||||
dosguard/IntervalSweepHandler.cpp
|
||||
dosguard/WhitelistHandler.cpp
|
||||
impl/AdminVerificationStrategy.cpp
|
||||
impl/ServerSslContext.cpp
|
||||
ng/Connection.cpp
|
||||
ng/impl/ConnectionHandler.cpp
|
||||
ng/impl/ServerSslContext.cpp
|
||||
ng/impl/WsConnection.cpp
|
||||
ng/Server.cpp
|
||||
ng/Request.cpp
|
||||
ng/Response.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(clio_web PUBLIC clio_util)
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
#include <boost/beast/core/flat_buffer.hpp>
|
||||
#include <boost/beast/core/tcp_stream.hpp>
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
@@ -52,6 +53,7 @@ class HttpSession : public impl::HttpBase<HttpSession, HandlerType>,
|
||||
public std::enable_shared_from_this<HttpSession<HandlerType>> {
|
||||
boost::beast::tcp_stream stream_;
|
||||
std::reference_wrapper<util::TagDecoratorFactory const> tagFactory_;
|
||||
std::uint32_t maxWsSendingQueueSize_;
|
||||
|
||||
public:
|
||||
/**
|
||||
@@ -64,6 +66,7 @@ public:
|
||||
* @param dosGuard The denial of service guard to use
|
||||
* @param handler The server handler to use
|
||||
* @param buffer Buffer with initial data received from the peer
|
||||
* @param maxWsSendingQueueSize The maximum size of the sending queue for websocket
|
||||
*/
|
||||
explicit HttpSession(
|
||||
tcp::socket&& socket,
|
||||
@@ -72,7 +75,8 @@ public:
|
||||
std::reference_wrapper<util::TagDecoratorFactory const> tagFactory,
|
||||
std::reference_wrapper<dosguard::DOSGuardInterface> dosGuard,
|
||||
std::shared_ptr<HandlerType> const& handler,
|
||||
boost::beast::flat_buffer buffer
|
||||
boost::beast::flat_buffer buffer,
|
||||
std::uint32_t maxWsSendingQueueSize
|
||||
)
|
||||
: impl::HttpBase<HttpSession, HandlerType>(
|
||||
ip,
|
||||
@@ -84,6 +88,7 @@ public:
|
||||
)
|
||||
, stream_(std::move(socket))
|
||||
, tagFactory_(tagFactory)
|
||||
, maxWsSendingQueueSize_(maxWsSendingQueueSize)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -128,7 +133,8 @@ public:
|
||||
this->handler_,
|
||||
std::move(this->buffer_),
|
||||
std::move(this->req_),
|
||||
ConnectionBase::isAdmin()
|
||||
ConnectionBase::isAdmin(),
|
||||
maxWsSendingQueueSize_
|
||||
)
|
||||
->run();
|
||||
}
|
||||
|
||||
@@ -62,7 +62,8 @@ public:
|
||||
* @param dosGuard The denial of service guard to use
|
||||
* @param handler The server handler to use
|
||||
* @param buffer Buffer with initial data received from the peer
|
||||
* @param isAdmin Whether the connection has admin privileges
|
||||
* @param isAdmin Whether the connection has admin privileges,
|
||||
* @param maxSendingQueueSize The maximum size of the sending queue for websocket
|
||||
*/
|
||||
explicit PlainWsSession(
|
||||
boost::asio::ip::tcp::socket&& socket,
|
||||
@@ -71,9 +72,17 @@ public:
|
||||
std::reference_wrapper<dosguard::DOSGuardInterface> dosGuard,
|
||||
std::shared_ptr<HandlerType> const& handler,
|
||||
boost::beast::flat_buffer&& buffer,
|
||||
bool isAdmin
|
||||
bool isAdmin,
|
||||
std::uint32_t maxSendingQueueSize
|
||||
)
|
||||
: impl::WsBase<PlainWsSession, HandlerType>(ip, tagFactory, dosGuard, handler, std::move(buffer))
|
||||
: impl::WsBase<PlainWsSession, HandlerType>(
|
||||
ip,
|
||||
tagFactory,
|
||||
dosGuard,
|
||||
handler,
|
||||
std::move(buffer),
|
||||
maxSendingQueueSize
|
||||
)
|
||||
, ws_(std::move(socket))
|
||||
{
|
||||
ConnectionBase::isAdmin_ = isAdmin; // NOLINT(cppcoreguidelines-prefer-member-initializer)
|
||||
@@ -107,6 +116,7 @@ class WsUpgrader : public std::enable_shared_from_this<WsUpgrader<HandlerType>>
|
||||
std::string ip_;
|
||||
std::shared_ptr<HandlerType> const handler_;
|
||||
bool isAdmin_;
|
||||
std::uint32_t maxWsSendingQueueSize_;
|
||||
|
||||
public:
|
||||
/**
|
||||
@@ -120,6 +130,7 @@ public:
|
||||
* @param buffer Buffer with initial data received from the peer. Ownership is transferred
|
||||
* @param request The request. Ownership is transferred
|
||||
* @param isAdmin Whether the connection has admin privileges
|
||||
* @param maxWsSendingQueueSize The maximum size of the sending queue for websocket
|
||||
*/
|
||||
WsUpgrader(
|
||||
boost::beast::tcp_stream&& stream,
|
||||
@@ -129,7 +140,8 @@ public:
|
||||
std::shared_ptr<HandlerType> const& handler,
|
||||
boost::beast::flat_buffer&& buffer,
|
||||
http::request<http::string_body> request,
|
||||
bool isAdmin
|
||||
bool isAdmin,
|
||||
std::uint32_t maxWsSendingQueueSize
|
||||
)
|
||||
: http_(std::move(stream))
|
||||
, buffer_(std::move(buffer))
|
||||
@@ -139,6 +151,7 @@ public:
|
||||
, ip_(std::move(ip))
|
||||
, handler_(handler)
|
||||
, isAdmin_(isAdmin)
|
||||
, maxWsSendingQueueSize_(maxWsSendingQueueSize)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -175,7 +188,14 @@ private:
|
||||
boost::beast::get_lowest_layer(http_).expires_never();
|
||||
|
||||
std::make_shared<PlainWsSession<HandlerType>>(
|
||||
http_.release_socket(), ip_, tagFactory_, dosGuard_, handler_, std::move(buffer_), isAdmin_
|
||||
http_.release_socket(),
|
||||
ip_,
|
||||
tagFactory_,
|
||||
dosGuard_,
|
||||
handler_,
|
||||
std::move(buffer_),
|
||||
isAdmin_,
|
||||
maxWsSendingQueueSize_
|
||||
)
|
||||
->run(std::move(req_));
|
||||
}
|
||||
|
||||
@@ -24,8 +24,8 @@
|
||||
#include "web/HttpSession.hpp"
|
||||
#include "web/SslHttpSession.hpp"
|
||||
#include "web/dosguard/DOSGuardInterface.hpp"
|
||||
#include "web/impl/ServerSslContext.hpp"
|
||||
#include "web/interface/Concepts.hpp"
|
||||
#include "web/ng/impl/ServerSslContext.hpp"
|
||||
|
||||
#include <boost/asio/io_context.hpp>
|
||||
#include <boost/asio/ip/address.hpp>
|
||||
@@ -41,6 +41,7 @@
|
||||
#include <fmt/core.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <exception>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
@@ -59,15 +60,6 @@
|
||||
*/
|
||||
namespace web {
|
||||
|
||||
/**
|
||||
* @brief A helper function to create a server SSL context.
|
||||
*
|
||||
* @param config The config to create the context
|
||||
* @return Optional SSL context or error message if any
|
||||
*/
|
||||
std::expected<std::optional<boost::asio::ssl::context>, std::string>
|
||||
makeServerSslContext(util::Config const& config);
|
||||
|
||||
/**
|
||||
* @brief The Detector class to detect if the connection is a ssl or not.
|
||||
*
|
||||
@@ -79,10 +71,8 @@ makeServerSslContext(util::Config const& config);
|
||||
* @tparam HandlerType The executor to handle the requests
|
||||
*/
|
||||
template <
|
||||
template <typename>
|
||||
class PlainSessionType,
|
||||
template <typename>
|
||||
class SslSessionType,
|
||||
template <typename> class PlainSessionType,
|
||||
template <typename> class SslSessionType,
|
||||
SomeServerHandler HandlerType>
|
||||
class Detector : public std::enable_shared_from_this<Detector<PlainSessionType, SslSessionType, HandlerType>> {
|
||||
using std::enable_shared_from_this<Detector<PlainSessionType, SslSessionType, HandlerType>>::shared_from_this;
|
||||
@@ -95,6 +85,7 @@ class Detector : public std::enable_shared_from_this<Detector<PlainSessionType,
|
||||
std::shared_ptr<HandlerType> const handler_;
|
||||
boost::beast::flat_buffer buffer_;
|
||||
std::shared_ptr<impl::AdminVerificationStrategy> const adminVerification_;
|
||||
std::uint32_t maxWsSendingQueueSize_;
|
||||
|
||||
public:
|
||||
/**
|
||||
@@ -106,6 +97,7 @@ public:
|
||||
* @param dosGuard The denial of service guard to use
|
||||
* @param handler The server handler to use
|
||||
* @param adminVerification The admin verification strategy to use
|
||||
* @param maxWsSendingQueueSize The maximum size of the sending queue for websocket
|
||||
*/
|
||||
Detector(
|
||||
tcp::socket&& socket,
|
||||
@@ -113,7 +105,8 @@ public:
|
||||
std::reference_wrapper<util::TagDecoratorFactory const> tagFactory,
|
||||
std::reference_wrapper<dosguard::DOSGuardInterface> dosGuard,
|
||||
std::shared_ptr<HandlerType> handler,
|
||||
std::shared_ptr<impl::AdminVerificationStrategy> adminVerification
|
||||
std::shared_ptr<impl::AdminVerificationStrategy> adminVerification,
|
||||
std::uint32_t maxWsSendingQueueSize
|
||||
)
|
||||
: stream_(std::move(socket))
|
||||
, ctx_(ctx)
|
||||
@@ -121,6 +114,7 @@ public:
|
||||
, dosGuard_(dosGuard)
|
||||
, handler_(std::move(handler))
|
||||
, adminVerification_(std::move(adminVerification))
|
||||
, maxWsSendingQueueSize_(maxWsSendingQueueSize)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -178,14 +172,22 @@ public:
|
||||
tagFactory_,
|
||||
dosGuard_,
|
||||
handler_,
|
||||
std::move(buffer_)
|
||||
std::move(buffer_),
|
||||
maxWsSendingQueueSize_
|
||||
)
|
||||
->run();
|
||||
return;
|
||||
}
|
||||
|
||||
std::make_shared<PlainSessionType<HandlerType>>(
|
||||
stream_.release_socket(), ip, adminVerification_, tagFactory_, dosGuard_, handler_, std::move(buffer_)
|
||||
stream_.release_socket(),
|
||||
ip,
|
||||
adminVerification_,
|
||||
tagFactory_,
|
||||
dosGuard_,
|
||||
handler_,
|
||||
std::move(buffer_),
|
||||
maxWsSendingQueueSize_
|
||||
)
|
||||
->run();
|
||||
}
|
||||
@@ -201,10 +203,8 @@ public:
|
||||
* @tparam HandlerType The handler to process the request and return response.
|
||||
*/
|
||||
template <
|
||||
template <typename>
|
||||
class PlainSessionType,
|
||||
template <typename>
|
||||
class SslSessionType,
|
||||
template <typename> class PlainSessionType,
|
||||
template <typename> class SslSessionType,
|
||||
SomeServerHandler HandlerType>
|
||||
class Server : public std::enable_shared_from_this<Server<PlainSessionType, SslSessionType, HandlerType>> {
|
||||
using std::enable_shared_from_this<Server<PlainSessionType, SslSessionType, HandlerType>>::shared_from_this;
|
||||
@@ -217,6 +217,7 @@ class Server : public std::enable_shared_from_this<Server<PlainSessionType, SslS
|
||||
std::shared_ptr<HandlerType> handler_;
|
||||
tcp::acceptor acceptor_;
|
||||
std::shared_ptr<impl::AdminVerificationStrategy> adminVerification_;
|
||||
std::uint32_t maxWsSendingQueueSize_;
|
||||
|
||||
public:
|
||||
/**
|
||||
@@ -229,6 +230,7 @@ public:
|
||||
* @param dosGuard The denial of service guard to use
|
||||
* @param handler The server handler to use
|
||||
* @param adminPassword The optional password to verify admin role in requests
|
||||
* @param maxWsSendingQueueSize The maximum size of the sending queue for websocket
|
||||
*/
|
||||
Server(
|
||||
boost::asio::io_context& ioc,
|
||||
@@ -237,7 +239,8 @@ public:
|
||||
util::TagDecoratorFactory tagFactory,
|
||||
dosguard::DOSGuardInterface& dosGuard,
|
||||
std::shared_ptr<HandlerType> handler,
|
||||
std::optional<std::string> adminPassword
|
||||
std::optional<std::string> adminPassword,
|
||||
std::uint32_t maxWsSendingQueueSize
|
||||
)
|
||||
: ioc_(std::ref(ioc))
|
||||
, ctx_(std::move(ctx))
|
||||
@@ -246,6 +249,7 @@ public:
|
||||
, handler_(std::move(handler))
|
||||
, acceptor_(boost::asio::make_strand(ioc))
|
||||
, adminVerification_(impl::make_AdminVerificationStrategy(std::move(adminPassword)))
|
||||
, maxWsSendingQueueSize_(maxWsSendingQueueSize)
|
||||
{
|
||||
boost::beast::error_code ec;
|
||||
|
||||
@@ -299,7 +303,13 @@ private:
|
||||
ctx_ ? std::optional<std::reference_wrapper<boost::asio::ssl::context>>{ctx_.value()} : std::nullopt;
|
||||
|
||||
std::make_shared<Detector<PlainSessionType, SslSessionType, HandlerType>>(
|
||||
std::move(socket), ctxRef, std::cref(tagFactory_), dosGuard_, handler_, adminVerification_
|
||||
std::move(socket),
|
||||
ctxRef,
|
||||
std::cref(tagFactory_),
|
||||
dosGuard_,
|
||||
handler_,
|
||||
adminVerification_,
|
||||
maxWsSendingQueueSize_
|
||||
)
|
||||
->run();
|
||||
}
|
||||
@@ -333,7 +343,7 @@ make_HttpServer(
|
||||
{
|
||||
static util::Logger const log{"WebServer"};
|
||||
|
||||
auto expectedSslContext = makeServerSslContext(config);
|
||||
auto expectedSslContext = ng::impl::makeServerSslContext(config);
|
||||
if (not expectedSslContext) {
|
||||
LOG(log.error()) << "Failed to create SSL context: " << expectedSslContext.error();
|
||||
return nullptr;
|
||||
@@ -361,6 +371,10 @@ make_HttpServer(
|
||||
throw std::logic_error("Admin config error, one method must be specified to authorize admin.");
|
||||
}
|
||||
|
||||
// If the transactions number is 200 per ledger, A client which subscribes everything will send 400+ feeds for
|
||||
// each ledger. we allow user delay 3 ledgers by default
|
||||
auto const maxWsSendingQueueSize = serverConfig.valueOr("ws_max_sending_queue_size", 1500);
|
||||
|
||||
auto server = std::make_shared<HttpServer<HandlerType>>(
|
||||
ioc,
|
||||
std::move(expectedSslContext).value(),
|
||||
@@ -368,7 +382,8 @@ make_HttpServer(
|
||||
util::TagDecoratorFactory(config),
|
||||
dosGuard,
|
||||
handler,
|
||||
std::move(adminPassword)
|
||||
std::move(adminPassword),
|
||||
maxWsSendingQueueSize
|
||||
);
|
||||
|
||||
server->run();
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
@@ -59,6 +60,7 @@ class SslHttpSession : public impl::HttpBase<SslHttpSession, HandlerType>,
|
||||
public std::enable_shared_from_this<SslHttpSession<HandlerType>> {
|
||||
boost::beast::ssl_stream<boost::beast::tcp_stream> stream_;
|
||||
std::reference_wrapper<util::TagDecoratorFactory const> tagFactory_;
|
||||
std::uint32_t maxWsSendingQueueSize_;
|
||||
|
||||
public:
|
||||
/**
|
||||
@@ -72,6 +74,7 @@ public:
|
||||
* @param dosGuard The denial of service guard to use
|
||||
* @param handler The server handler to use
|
||||
* @param buffer Buffer with initial data received from the peer
|
||||
* @param maxWsSendingQueueSize The maximum size of the sending queue for websocket
|
||||
*/
|
||||
explicit SslHttpSession(
|
||||
tcp::socket&& socket,
|
||||
@@ -81,7 +84,8 @@ public:
|
||||
std::reference_wrapper<util::TagDecoratorFactory const> tagFactory,
|
||||
std::reference_wrapper<dosguard::DOSGuardInterface> dosGuard,
|
||||
std::shared_ptr<HandlerType> const& handler,
|
||||
boost::beast::flat_buffer buffer
|
||||
boost::beast::flat_buffer buffer,
|
||||
std::uint32_t maxWsSendingQueueSize
|
||||
)
|
||||
: impl::HttpBase<SslHttpSession, HandlerType>(
|
||||
ip,
|
||||
@@ -93,6 +97,7 @@ public:
|
||||
)
|
||||
, stream_(std::move(socket), ctx)
|
||||
, tagFactory_(tagFactory)
|
||||
, maxWsSendingQueueSize_(maxWsSendingQueueSize)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -173,7 +178,8 @@ public:
|
||||
this->handler_,
|
||||
std::move(this->buffer_),
|
||||
std::move(this->req_),
|
||||
ConnectionBase::isAdmin()
|
||||
ConnectionBase::isAdmin(),
|
||||
maxWsSendingQueueSize_
|
||||
)
|
||||
->run();
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
#include <boost/optional/optional.hpp>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
@@ -64,6 +65,7 @@ public:
|
||||
* @param handler The server handler to use
|
||||
* @param buffer Buffer with initial data received from the peer
|
||||
* @param isAdmin Whether the connection has admin privileges
|
||||
* @param maxWsSendingQueueSize The maximum size of the sending queue for websocket
|
||||
*/
|
||||
explicit SslWsSession(
|
||||
boost::beast::ssl_stream<boost::beast::tcp_stream>&& stream,
|
||||
@@ -72,9 +74,17 @@ public:
|
||||
std::reference_wrapper<dosguard::DOSGuardInterface> dosGuard,
|
||||
std::shared_ptr<HandlerType> const& handler,
|
||||
boost::beast::flat_buffer&& buffer,
|
||||
bool isAdmin
|
||||
bool isAdmin,
|
||||
std::uint32_t maxWsSendingQueueSize
|
||||
)
|
||||
: impl::WsBase<SslWsSession, HandlerType>(ip, tagFactory, dosGuard, handler, std::move(buffer))
|
||||
: impl::WsBase<SslWsSession, HandlerType>(
|
||||
ip,
|
||||
tagFactory,
|
||||
dosGuard,
|
||||
handler,
|
||||
std::move(buffer),
|
||||
maxWsSendingQueueSize
|
||||
)
|
||||
, ws_(std::move(stream))
|
||||
{
|
||||
ConnectionBase::isAdmin_ = isAdmin; // NOLINT(cppcoreguidelines-prefer-member-initializer)
|
||||
@@ -106,6 +116,7 @@ class SslWsUpgrader : public std::enable_shared_from_this<SslWsUpgrader<HandlerT
|
||||
std::shared_ptr<HandlerType> const handler_;
|
||||
http::request<http::string_body> req_;
|
||||
bool isAdmin_;
|
||||
std::uint32_t maxWsSendingQueueSize_;
|
||||
|
||||
public:
|
||||
/**
|
||||
@@ -119,6 +130,7 @@ public:
|
||||
* @param buffer Buffer with initial data received from the peer. Ownership is transferred
|
||||
* @param request The request. Ownership is transferred
|
||||
* @param isAdmin Whether the connection has admin privileges
|
||||
* @param maxWsSendingQueueSize The maximum size of the sending queue for websocket
|
||||
*/
|
||||
SslWsUpgrader(
|
||||
boost::beast::ssl_stream<boost::beast::tcp_stream> stream,
|
||||
@@ -128,7 +140,8 @@ public:
|
||||
std::shared_ptr<HandlerType> handler,
|
||||
boost::beast::flat_buffer&& buffer,
|
||||
http::request<http::string_body> request,
|
||||
bool isAdmin
|
||||
bool isAdmin,
|
||||
std::uint32_t maxWsSendingQueueSize
|
||||
)
|
||||
: https_(std::move(stream))
|
||||
, buffer_(std::move(buffer))
|
||||
@@ -138,6 +151,7 @@ public:
|
||||
, handler_(std::move(handler))
|
||||
, req_(std::move(request))
|
||||
, isAdmin_(isAdmin)
|
||||
, maxWsSendingQueueSize_(maxWsSendingQueueSize)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -179,7 +193,14 @@ private:
|
||||
boost::beast::get_lowest_layer(https_).expires_never();
|
||||
|
||||
std::make_shared<SslWsSession<HandlerType>>(
|
||||
std::move(https_), ip_, tagFactory_, dosGuard_, handler_, std::move(buffer_), isAdmin_
|
||||
std::move(https_),
|
||||
ip_,
|
||||
tagFactory_,
|
||||
dosGuard_,
|
||||
handler_,
|
||||
std::move(buffer_),
|
||||
isAdmin_,
|
||||
maxWsSendingQueueSize_
|
||||
)
|
||||
->run(std::move(req_));
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include "web/impl/AdminVerificationStrategy.hpp"
|
||||
|
||||
#include "util/JsonUtils.hpp"
|
||||
#include "util/config/Config.hpp"
|
||||
|
||||
#include <boost/beast/http/field.hpp>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
@@ -79,4 +80,20 @@ make_AdminVerificationStrategy(std::optional<std::string> password)
|
||||
return std::make_shared<IPAdminVerificationStrategy>();
|
||||
}
|
||||
|
||||
std::expected<std::shared_ptr<AdminVerificationStrategy>, std::string>
|
||||
make_AdminVerificationStrategy(util::Config const& serverConfig)
|
||||
{
|
||||
auto adminPassword = serverConfig.maybeValue<std::string>("admin_password");
|
||||
auto const localAdmin = serverConfig.maybeValue<bool>("local_admin");
|
||||
bool const localAdminEnabled = localAdmin && localAdmin.value();
|
||||
|
||||
if (localAdminEnabled == adminPassword.has_value()) {
|
||||
if (adminPassword.has_value())
|
||||
return std::unexpected{"Admin config error, local_admin and admin_password can not be set together."};
|
||||
return std::unexpected{"Admin config error, either local_admin and admin_password must be specified."};
|
||||
}
|
||||
|
||||
return make_AdminVerificationStrategy(std::move(adminPassword));
|
||||
}
|
||||
|
||||
} // namespace web::impl
|
||||
|
||||
@@ -19,10 +19,13 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "util/config/Config.hpp"
|
||||
|
||||
#include <boost/beast/http.hpp>
|
||||
#include <boost/beast/http/message.hpp>
|
||||
#include <boost/beast/http/string_body.hpp>
|
||||
|
||||
#include <expected>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
@@ -82,4 +85,7 @@ public:
|
||||
std::shared_ptr<AdminVerificationStrategy>
|
||||
make_AdminVerificationStrategy(std::optional<std::string> password);
|
||||
|
||||
std::expected<std::shared_ptr<AdminVerificationStrategy>, std::string>
|
||||
make_AdminVerificationStrategy(util::Config const& serverConfig);
|
||||
|
||||
} // namespace web::impl
|
||||
|
||||
@@ -87,9 +87,9 @@ public:
|
||||
case rpc::ClioError::rpcMALFORMED_REQUEST:
|
||||
case rpc::ClioError::rpcMALFORMED_OWNER:
|
||||
case rpc::ClioError::rpcMALFORMED_ADDRESS:
|
||||
case rpc::ClioError::rpcINVALID_HOT_WALLET:
|
||||
case rpc::ClioError::rpcFIELD_NOT_FOUND_TRANSACTION:
|
||||
case rpc::ClioError::rpcMALFORMED_ORACLE_DOCUMENT_ID:
|
||||
case rpc::ClioError::rpcMALFORMED_AUTHORIZED_CREDENTIALS:
|
||||
case rpc::ClioError::etlCONNECTION_ERROR:
|
||||
case rpc::ClioError::etlREQUEST_ERROR:
|
||||
case rpc::ClioError::etlREQUEST_TIMEOUT:
|
||||
|
||||
@@ -60,6 +60,14 @@
|
||||
|
||||
namespace web::impl {
|
||||
|
||||
static auto constexpr HealthCheckHTML = R"html(
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head><title>Test page for Clio</title></head>
|
||||
<body><h1>Clio Test</h1><p>This page shows Clio http(s) connectivity is working.</p></body>
|
||||
</html>
|
||||
)html";
|
||||
|
||||
using tcp = boost::asio::ip::tcp;
|
||||
|
||||
/**
|
||||
@@ -205,6 +213,9 @@ public:
|
||||
if (ec)
|
||||
return httpFail(ec, "read");
|
||||
|
||||
if (req_.method() == http::verb::get and req_.target() == "/health")
|
||||
return sender_(httpResponse(http::status::ok, "text/html", HealthCheckHTML));
|
||||
|
||||
// Update isAdmin property of the connection
|
||||
ConnectionBase::isAdmin_ = adminVerification_->isAdmin(req_, this->clientIp);
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
#include <boost/beast/http/status.hpp>
|
||||
#include <boost/beast/http/string_body.hpp>
|
||||
#include <boost/beast/version.hpp>
|
||||
#include <boost/beast/websocket/error.hpp>
|
||||
#include <boost/beast/websocket/rfc6455.hpp>
|
||||
#include <boost/beast/websocket/stream_base.hpp>
|
||||
#include <boost/core/ignore_unused.hpp>
|
||||
@@ -47,6 +48,7 @@
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <exception>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
@@ -77,6 +79,8 @@ class WsBase : public ConnectionBase, public std::enable_shared_from_this<WsBase
|
||||
std::queue<std::shared_ptr<std::string>> messages_;
|
||||
std::shared_ptr<HandlerType> const handler_;
|
||||
|
||||
std::uint32_t maxSendingQueueSize_;
|
||||
|
||||
protected:
|
||||
util::Logger log_{"WebServer"};
|
||||
util::Logger perfLog_{"Performance"};
|
||||
@@ -85,9 +89,8 @@ protected:
|
||||
wsFail(boost::beast::error_code ec, char const* what)
|
||||
{
|
||||
// Don't log if the WebSocket stream was gracefully closed at both endpoints
|
||||
if (ec != boost::beast::websocket::error::closed) {
|
||||
LOG(perfLog_.error()) << tag() << ": " << what << ": " << ec.message() << ": " << ec.value();
|
||||
}
|
||||
if (ec != boost::beast::websocket::error::closed)
|
||||
LOG(log_.error()) << tag() << ": " << what << ": " << ec.message() << ": " << ec.value();
|
||||
|
||||
if (!ec_ && ec != boost::asio::error::operation_aborted) {
|
||||
ec_ = ec;
|
||||
@@ -101,11 +104,17 @@ public:
|
||||
std::reference_wrapper<util::TagDecoratorFactory const> tagFactory,
|
||||
std::reference_wrapper<dosguard::DOSGuardInterface> dosGuard,
|
||||
std::shared_ptr<HandlerType> const& handler,
|
||||
boost::beast::flat_buffer&& buffer
|
||||
boost::beast::flat_buffer&& buffer,
|
||||
std::uint32_t maxSendingQueueSize
|
||||
)
|
||||
: ConnectionBase(tagFactory, ip), buffer_(std::move(buffer)), dosGuard_(dosGuard), handler_(handler)
|
||||
: ConnectionBase(tagFactory, ip)
|
||||
, buffer_(std::move(buffer))
|
||||
, dosGuard_(dosGuard)
|
||||
, handler_(handler)
|
||||
, maxSendingQueueSize_(maxSendingQueueSize)
|
||||
{
|
||||
upgraded = true; // NOLINT (cppcoreguidelines-pro-type-member-init)
|
||||
|
||||
LOG(perfLog_.debug()) << tag() << "session created";
|
||||
}
|
||||
|
||||
@@ -164,6 +173,11 @@ public:
|
||||
boost::asio::dispatch(
|
||||
derived().ws().get_executor(),
|
||||
[this, self = derived().shared_from_this(), msg = std::move(msg)]() {
|
||||
if (messages_.size() > maxSendingQueueSize_) {
|
||||
wsFail(boost::asio::error::timed_out, "Client is too slow");
|
||||
return;
|
||||
}
|
||||
|
||||
messages_.push(msg);
|
||||
maybeSendNext();
|
||||
}
|
||||
|
||||
@@ -17,32 +17,41 @@
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include "web/impl/ServerSslContext.hpp"
|
||||
#include "web/ng/Connection.hpp"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include "util/Taggable.hpp"
|
||||
|
||||
using namespace web::impl;
|
||||
#include <boost/beast/core/flat_buffer.hpp>
|
||||
|
||||
TEST(ServerSslContext, makeServerSslContext)
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace web::ng {
|
||||
|
||||
Connection::Connection(
|
||||
std::string ip,
|
||||
boost::beast::flat_buffer buffer,
|
||||
util::TagDecoratorFactory const& tagDecoratorFactory
|
||||
)
|
||||
: util::Taggable(tagDecoratorFactory), ip_{std::move(ip)}, buffer_{std::move(buffer)}
|
||||
{
|
||||
auto const sslContext = makeServerSslContext(TEST_DATA_SSL_CERT_PATH, TEST_DATA_SSL_KEY_PATH);
|
||||
ASSERT_TRUE(sslContext);
|
||||
}
|
||||
|
||||
TEST(ServerSslContext, makeServerSslContext_WrongCertPath)
|
||||
ConnectionContext
|
||||
Connection::context() const
|
||||
{
|
||||
auto const sslContext = makeServerSslContext("wrong_path", TEST_DATA_SSL_KEY_PATH);
|
||||
ASSERT_FALSE(sslContext);
|
||||
return ConnectionContext{*this};
|
||||
}
|
||||
|
||||
TEST(ServerSslContext, makeServerSslContext_WrongKeyPath)
|
||||
std::string const&
|
||||
Connection::ip() const
|
||||
{
|
||||
auto const sslContext = makeServerSslContext(TEST_DATA_SSL_CERT_PATH, "wrong_path");
|
||||
ASSERT_FALSE(sslContext);
|
||||
return ip_;
|
||||
}
|
||||
|
||||
TEST(ServerSslContext, makeServerSslContext_CertKeyMismatch)
|
||||
ConnectionContext::ConnectionContext(Connection const& connection) : connection_{connection}
|
||||
{
|
||||
auto const sslContext = makeServerSslContext(TEST_DATA_SSL_KEY_PATH, TEST_DATA_SSL_CERT_PATH);
|
||||
ASSERT_FALSE(sslContext);
|
||||
}
|
||||
|
||||
} // namespace web::ng
|
||||
148
src/web/ng/Connection.hpp
Normal file
148
src/web/ng/Connection.hpp
Normal file
@@ -0,0 +1,148 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2024, 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 "util/Taggable.hpp"
|
||||
#include "web/ng/Error.hpp"
|
||||
#include "web/ng/Request.hpp"
|
||||
#include "web/ng/Response.hpp"
|
||||
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/beast/core/flat_buffer.hpp>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <expected>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
namespace web::ng {
|
||||
|
||||
/**
|
||||
* @brief A forward declaration of ConnectionContext.
|
||||
*/
|
||||
class ConnectionContext;
|
||||
|
||||
/**
|
||||
*@brief A class representing a connection to a client.
|
||||
*/
|
||||
class Connection : public util::Taggable {
|
||||
protected:
|
||||
std::string ip_; // client ip
|
||||
boost::beast::flat_buffer buffer_;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief The default timeout for send, receive, and close operations.
|
||||
*/
|
||||
static constexpr std::chrono::steady_clock::duration DEFAULT_TIMEOUT = std::chrono::seconds{30};
|
||||
|
||||
/**
|
||||
* @brief Construct a new Connection object
|
||||
*
|
||||
* @param ip The client ip.
|
||||
* @param buffer The buffer to use for reading and writing.
|
||||
* @param tagDecoratorFactory The factory for creating tag decorators.
|
||||
*/
|
||||
Connection(std::string ip, boost::beast::flat_buffer buffer, util::TagDecoratorFactory const& tagDecoratorFactory);
|
||||
|
||||
/**
|
||||
* @brief Whether the connection was upgraded. Upgraded connections are websocket connections.
|
||||
*
|
||||
* @return true if the connection was upgraded.
|
||||
*/
|
||||
virtual bool
|
||||
wasUpgraded() const = 0;
|
||||
|
||||
/**
|
||||
* @brief Send a response to the client.
|
||||
*
|
||||
* @param response The response to send.
|
||||
* @param yield The yield context.
|
||||
* @param timeout The timeout for the operation.
|
||||
* @return An error if the operation failed or nullopt if it succeeded.
|
||||
*/
|
||||
|
||||
virtual std::optional<Error>
|
||||
send(
|
||||
Response response,
|
||||
boost::asio::yield_context yield,
|
||||
std::chrono::steady_clock::duration timeout = DEFAULT_TIMEOUT
|
||||
) = 0;
|
||||
|
||||
/**
|
||||
* @brief Receive a request from the client.
|
||||
*
|
||||
* @param yield The yield context.
|
||||
* @param timeout The timeout for the operation.
|
||||
* @return The request if it was received or an error if the operation failed.
|
||||
*/
|
||||
virtual std::expected<Request, Error>
|
||||
receive(boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout = DEFAULT_TIMEOUT) = 0;
|
||||
|
||||
/**
|
||||
* @brief Gracefully close the connection.
|
||||
*
|
||||
* @param yield The yield context.
|
||||
* @param timeout The timeout for the operation.
|
||||
*/
|
||||
virtual void
|
||||
close(boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout = DEFAULT_TIMEOUT) = 0;
|
||||
|
||||
/**
|
||||
* @brief Get the connection context.
|
||||
*
|
||||
* @return The connection context.
|
||||
*/
|
||||
ConnectionContext
|
||||
context() const;
|
||||
|
||||
/**
|
||||
* @brief Get the ip of the client.
|
||||
*
|
||||
* @return The ip of the client.
|
||||
*/
|
||||
std::string const&
|
||||
ip() const;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief A pointer to a connection.
|
||||
*/
|
||||
using ConnectionPtr = std::unique_ptr<Connection>;
|
||||
|
||||
/**
|
||||
* @brief A class representing the context of a connection.
|
||||
*/
|
||||
class ConnectionContext {
|
||||
std::reference_wrapper<Connection const> connection_;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Construct a new ConnectionContext object.
|
||||
*
|
||||
* @param connection The connection.
|
||||
*/
|
||||
explicit ConnectionContext(Connection const& connection);
|
||||
};
|
||||
|
||||
} // namespace web::ng
|
||||
31
src/web/ng/Error.hpp
Normal file
31
src/web/ng/Error.hpp
Normal file
@@ -0,0 +1,31 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2024, 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 <boost/system/detail/error_code.hpp>
|
||||
|
||||
namespace web::ng {
|
||||
|
||||
/**
|
||||
* @brief Error of any async operation.
|
||||
*/
|
||||
using Error = boost::system::error_code;
|
||||
|
||||
} // namespace web::ng
|
||||
37
src/web/ng/MessageHandler.hpp
Normal file
37
src/web/ng/MessageHandler.hpp
Normal file
@@ -0,0 +1,37 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2024, 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 "web/ng/Connection.hpp"
|
||||
#include "web/ng/Request.hpp"
|
||||
#include "web/ng/Response.hpp"
|
||||
|
||||
#include <boost/asio/spawn.hpp>
|
||||
|
||||
#include <functional>
|
||||
|
||||
namespace web::ng {
|
||||
|
||||
/**
|
||||
* @brief Handler for messages.
|
||||
*/
|
||||
using MessageHandler = std::function<Response(Request const&, ConnectionContext, boost::asio::yield_context)>;
|
||||
|
||||
} // namespace web::ng
|
||||
131
src/web/ng/Request.cpp
Normal file
131
src/web/ng/Request.cpp
Normal file
@@ -0,0 +1,131 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2024, 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 "web/ng/Request.hpp"
|
||||
|
||||
#include <boost/beast/http/field.hpp>
|
||||
#include <boost/beast/http/message.hpp>
|
||||
#include <boost/beast/http/string_body.hpp>
|
||||
#include <boost/beast/http/verb.hpp>
|
||||
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <variant>
|
||||
|
||||
namespace web::ng {
|
||||
|
||||
namespace {
|
||||
|
||||
template <typename HeadersType, typename HeaderNameType>
|
||||
std::optional<std::string_view>
|
||||
getHeaderValue(HeadersType const& headers, HeaderNameType const& headerName)
|
||||
{
|
||||
auto const it = headers.find(headerName);
|
||||
if (it == headers.end())
|
||||
return std::nullopt;
|
||||
return it->value();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Request::Request(boost::beast::http::request<boost::beast::http::string_body> request) : data_{std::move(request)}
|
||||
{
|
||||
}
|
||||
|
||||
Request::Request(std::string request, HttpHeaders const& headers)
|
||||
: data_{WsData{.request = std::move(request), .headers = headers}}
|
||||
{
|
||||
}
|
||||
|
||||
Request::Method
|
||||
Request::method() const
|
||||
{
|
||||
if (not isHttp())
|
||||
return Method::Websocket;
|
||||
|
||||
switch (httpRequest().method()) {
|
||||
case boost::beast::http::verb::get:
|
||||
return Method::Get;
|
||||
case boost::beast::http::verb::post:
|
||||
return Method::Post;
|
||||
default:
|
||||
return Method::Unsupported;
|
||||
}
|
||||
}
|
||||
|
||||
bool
|
||||
Request::isHttp() const
|
||||
{
|
||||
return std::holds_alternative<HttpRequest>(data_);
|
||||
}
|
||||
|
||||
std::optional<std::reference_wrapper<boost::beast::http::request<boost::beast::http::string_body> const>>
|
||||
Request::asHttpRequest() const
|
||||
{
|
||||
if (not isHttp())
|
||||
return std::nullopt;
|
||||
|
||||
return httpRequest();
|
||||
}
|
||||
|
||||
std::string_view
|
||||
Request::message() const
|
||||
{
|
||||
if (not isHttp())
|
||||
return std::get<WsData>(data_).request;
|
||||
return httpRequest().body();
|
||||
}
|
||||
|
||||
std::optional<std::string_view>
|
||||
Request::target() const
|
||||
{
|
||||
if (not isHttp())
|
||||
return std::nullopt;
|
||||
|
||||
return httpRequest().target();
|
||||
}
|
||||
|
||||
std::optional<std::string_view>
|
||||
Request::headerValue(boost::beast::http::field headerName) const
|
||||
{
|
||||
if (not isHttp())
|
||||
return getHeaderValue(std::get<WsData>(data_).headers.get(), headerName);
|
||||
|
||||
return getHeaderValue(httpRequest(), headerName);
|
||||
}
|
||||
|
||||
std::optional<std::string_view>
|
||||
Request::headerValue(std::string const& headerName) const
|
||||
{
|
||||
if (not isHttp())
|
||||
return getHeaderValue(std::get<WsData>(data_).headers.get(), headerName);
|
||||
|
||||
return getHeaderValue(httpRequest(), headerName);
|
||||
}
|
||||
|
||||
Request::HttpRequest const&
|
||||
Request::httpRequest() const
|
||||
{
|
||||
return std::get<HttpRequest>(data_);
|
||||
}
|
||||
|
||||
} // namespace web::ng
|
||||
145
src/web/ng/Request.hpp
Normal file
145
src/web/ng/Request.hpp
Normal file
@@ -0,0 +1,145 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2024, 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 <boost/beast/http/field.hpp>
|
||||
#include <boost/beast/http/message.hpp>
|
||||
#include <boost/beast/http/string_body.hpp>
|
||||
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <variant>
|
||||
|
||||
namespace web::ng {
|
||||
|
||||
/**
|
||||
* @brief Represents an HTTP or WebSocket request.
|
||||
*/
|
||||
class Request {
|
||||
public:
|
||||
/**
|
||||
* @brief The headers of an HTTP request.
|
||||
*/
|
||||
using HttpHeaders = boost::beast::http::request<boost::beast::http::string_body>::header_type;
|
||||
|
||||
private:
|
||||
struct WsData {
|
||||
std::string request;
|
||||
std::reference_wrapper<HttpHeaders const> headers;
|
||||
};
|
||||
|
||||
using HttpRequest = boost::beast::http::request<boost::beast::http::string_body>;
|
||||
std::variant<HttpRequest, WsData> data_;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Construct from an HTTP request.
|
||||
*
|
||||
* @param request The HTTP request.
|
||||
*/
|
||||
explicit Request(boost::beast::http::request<boost::beast::http::string_body> request);
|
||||
|
||||
/**
|
||||
* @brief Construct from a WebSocket request.
|
||||
*
|
||||
* @param request The WebSocket request.
|
||||
* @param headers The headers of the HTTP request initiated the WebSocket connection
|
||||
*/
|
||||
Request(std::string request, HttpHeaders const& headers);
|
||||
|
||||
/**
|
||||
* @brief Method of the request.
|
||||
* @note Websocket is not a real method, it is used to distinguish WebSocket requests from HTTP requests.
|
||||
*/
|
||||
enum class Method { Get, Post, Websocket, Unsupported };
|
||||
|
||||
/**
|
||||
* @brief Get the method of the request.
|
||||
*
|
||||
* @return The method of the request.
|
||||
*/
|
||||
Method
|
||||
method() const;
|
||||
|
||||
/**
|
||||
* @brief Check if the request is an HTTP request.
|
||||
*
|
||||
* @return true if the request is an HTTP request, false otherwise.
|
||||
*/
|
||||
bool
|
||||
isHttp() const;
|
||||
|
||||
/**
|
||||
* @brief Get the HTTP request.
|
||||
*
|
||||
* @return The HTTP request or std::nullopt if the request is a WebSocket request.
|
||||
*/
|
||||
std::optional<std::reference_wrapper<boost::beast::http::request<boost::beast::http::string_body> const>>
|
||||
asHttpRequest() const;
|
||||
|
||||
/**
|
||||
* @brief Get the body (in case of an HTTP request) or the message (in case of a WebSocket request).
|
||||
*
|
||||
* @return The message of the request.
|
||||
*/
|
||||
std::string_view
|
||||
message() const;
|
||||
|
||||
/**
|
||||
* @brief Get the target of the request.
|
||||
*
|
||||
* @return The target of the request or std::nullopt if the request is a WebSocket request.
|
||||
*/
|
||||
std::optional<std::string_view>
|
||||
target() const;
|
||||
|
||||
/**
|
||||
* @brief Get the value of a header.
|
||||
*
|
||||
* @param headerName The name of the header.
|
||||
* @return The value of the header or std::nullopt if the header does not exist.
|
||||
*/
|
||||
std::optional<std::string_view>
|
||||
headerValue(boost::beast::http::field headerName) const;
|
||||
|
||||
/**
|
||||
* @brief Get the value of a header.
|
||||
*
|
||||
* @param headerName The name of the header.
|
||||
* @return The value of the header or std::nullopt if the header does not exist.
|
||||
*/
|
||||
std::optional<std::string_view>
|
||||
headerValue(std::string const& headerName) const;
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief Get the HTTP request.
|
||||
* @note This function assumes that the request is an HTTP request. So if data_ is not an HTTP request,
|
||||
* the behavior is undefined.
|
||||
*
|
||||
* @return The HTTP request.
|
||||
*/
|
||||
HttpRequest const&
|
||||
httpRequest() const;
|
||||
};
|
||||
|
||||
} // namespace web::ng
|
||||
116
src/web/ng/Response.cpp
Normal file
116
src/web/ng/Response.cpp
Normal file
@@ -0,0 +1,116 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2024, 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 "web/ng/Response.hpp"
|
||||
|
||||
#include "util/Assert.hpp"
|
||||
#include "util/build/Build.hpp"
|
||||
#include "web/ng/Request.hpp"
|
||||
|
||||
#include <boost/asio/buffer.hpp>
|
||||
#include <boost/beast/http/field.hpp>
|
||||
#include <boost/beast/http/message.hpp>
|
||||
#include <boost/beast/http/status.hpp>
|
||||
#include <boost/beast/http/string_body.hpp>
|
||||
#include <boost/json/object.hpp>
|
||||
#include <boost/json/serialize.hpp>
|
||||
#include <fmt/core.h>
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
|
||||
namespace http = boost::beast::http;
|
||||
namespace web::ng {
|
||||
|
||||
namespace {
|
||||
|
||||
std::string_view
|
||||
asString(Response::HttpData::ContentType type)
|
||||
{
|
||||
switch (type) {
|
||||
case Response::HttpData::ContentType::TextHtml:
|
||||
return "text/html";
|
||||
case Response::HttpData::ContentType::ApplicationJson:
|
||||
return "application/json";
|
||||
}
|
||||
ASSERT(false, "Unknown content type");
|
||||
std::unreachable();
|
||||
}
|
||||
|
||||
template <typename MessageType>
|
||||
std::optional<Response::HttpData>
|
||||
makeHttpData(http::status status, Request const& request)
|
||||
{
|
||||
if (request.isHttp()) {
|
||||
auto const& httpRequest = request.asHttpRequest()->get();
|
||||
auto constexpr contentType = std::is_same_v<std::remove_cvref_t<MessageType>, std::string>
|
||||
? Response::HttpData::ContentType::TextHtml
|
||||
: Response::HttpData::ContentType::ApplicationJson;
|
||||
return Response::HttpData{
|
||||
.status = status,
|
||||
.contentType = contentType,
|
||||
.keepAlive = httpRequest.keep_alive(),
|
||||
.version = httpRequest.version()
|
||||
};
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Response::Response(boost::beast::http::status status, std::string message, Request const& request)
|
||||
: message_(std::move(message)), httpData_{makeHttpData<decltype(message)>(status, request)}
|
||||
{
|
||||
}
|
||||
|
||||
Response::Response(boost::beast::http::status status, boost::json::object const& message, Request const& request)
|
||||
: message_(boost::json::serialize(message)), httpData_{makeHttpData<decltype(message)>(status, request)}
|
||||
{
|
||||
}
|
||||
|
||||
std::string const&
|
||||
Response::message() const
|
||||
{
|
||||
return message_;
|
||||
}
|
||||
|
||||
http::response<http::string_body>
|
||||
Response::intoHttpResponse() &&
|
||||
{
|
||||
ASSERT(httpData_.has_value(), "Response must have http data to be converted into http response");
|
||||
|
||||
http::response<http::string_body> result{httpData_->status, httpData_->version};
|
||||
result.set(http::field::server, fmt::format("clio-server-{}", util::build::getClioVersionString()));
|
||||
result.set(http::field::content_type, asString(httpData_->contentType));
|
||||
result.keep_alive(httpData_->keepAlive);
|
||||
result.body() = std::move(message_);
|
||||
result.prepare_payload();
|
||||
return result;
|
||||
}
|
||||
|
||||
boost::asio::const_buffer
|
||||
Response::asConstBuffer() const&
|
||||
{
|
||||
ASSERT(not httpData_.has_value(), "Losing existing http data");
|
||||
return boost::asio::buffer(message_.data(), message_.size());
|
||||
}
|
||||
|
||||
} // namespace web::ng
|
||||
106
src/web/ng/Response.hpp
Normal file
106
src/web/ng/Response.hpp
Normal file
@@ -0,0 +1,106 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2024, 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 "web/ng/Request.hpp"
|
||||
|
||||
#include <boost/asio/buffer.hpp>
|
||||
#include <boost/beast/http/message.hpp>
|
||||
#include <boost/beast/http/status.hpp>
|
||||
#include <boost/beast/http/string_body.hpp>
|
||||
#include <boost/json/object.hpp>
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
namespace web::ng {
|
||||
|
||||
/**
|
||||
* @brief Represents an HTTP or Websocket response.
|
||||
*/
|
||||
class Response {
|
||||
public:
|
||||
/**
|
||||
* @brief The data for an HTTP response.
|
||||
*/
|
||||
struct HttpData {
|
||||
/**
|
||||
* @brief The content type of the response.
|
||||
*/
|
||||
enum class ContentType { ApplicationJson, TextHtml };
|
||||
|
||||
boost::beast::http::status status; ///< The HTTP status.
|
||||
ContentType contentType; ///< The content type.
|
||||
bool keepAlive; ///< Whether the connection should be kept alive.
|
||||
unsigned int version; ///< The HTTP version.
|
||||
};
|
||||
|
||||
private:
|
||||
std::string message_;
|
||||
std::optional<HttpData> httpData_;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Construct a Response from string. Content type will be text/html.
|
||||
*
|
||||
* @param status The HTTP status.
|
||||
* @param message The message to send.
|
||||
* @param request The request that triggered this response. Used to determine whether the response should contain
|
||||
* HTTP or WebSocket data.
|
||||
*/
|
||||
Response(boost::beast::http::status status, std::string message, Request const& request);
|
||||
|
||||
/**
|
||||
* @brief Construct a Response from JSON object. Content type will be application/json.
|
||||
*
|
||||
* @param status The HTTP status.
|
||||
* @param message The message to send.
|
||||
* @param request The request that triggered this response. Used to determine whether the response should contain
|
||||
* HTTP or WebSocket
|
||||
*/
|
||||
Response(boost::beast::http::status status, boost::json::object const& message, Request const& request);
|
||||
|
||||
/**
|
||||
* @brief Get the message of the response.
|
||||
*
|
||||
* @return The message of the response.
|
||||
*/
|
||||
std::string const&
|
||||
message() const;
|
||||
|
||||
/**
|
||||
* @brief Convert the Response to an HTTP response.
|
||||
* @note The Response must be constructed with an HTTP request.
|
||||
*
|
||||
* @return The HTTP response.
|
||||
*/
|
||||
boost::beast::http::response<boost::beast::http::string_body>
|
||||
intoHttpResponse() &&;
|
||||
|
||||
/**
|
||||
* @brief Get the message of the response as a const buffer.
|
||||
* @note The response must be constructed with a WebSocket request.
|
||||
*
|
||||
* @return The message of the response as a const buffer.
|
||||
*/
|
||||
boost::asio::const_buffer
|
||||
asConstBuffer() const&;
|
||||
};
|
||||
|
||||
} // namespace web::ng
|
||||
@@ -0,0 +1,321 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2024, 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 "web/ng/Server.hpp"
|
||||
|
||||
#include "util/Assert.hpp"
|
||||
#include "util/Taggable.hpp"
|
||||
#include "util/config/Config.hpp"
|
||||
#include "util/log/Logger.hpp"
|
||||
#include "web/ng/Connection.hpp"
|
||||
#include "web/ng/MessageHandler.hpp"
|
||||
#include "web/ng/impl/HttpConnection.hpp"
|
||||
#include "web/ng/impl/ServerSslContext.hpp"
|
||||
|
||||
#include <boost/asio/detached.hpp>
|
||||
#include <boost/asio/io_context.hpp>
|
||||
#include <boost/asio/ip/address.hpp>
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
#include <boost/asio/socket_base.hpp>
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/asio/ssl/context.hpp>
|
||||
#include <boost/asio/ssl/error.hpp>
|
||||
#include <boost/beast/core/detect_ssl.hpp>
|
||||
#include <boost/beast/core/error.hpp>
|
||||
#include <boost/beast/core/flat_buffer.hpp>
|
||||
#include <boost/beast/core/tcp_stream.hpp>
|
||||
#include <boost/system/system_error.hpp>
|
||||
#include <fmt/core.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace web::ng {
|
||||
|
||||
namespace {
|
||||
|
||||
std::expected<boost::asio::ip::tcp::endpoint, std::string>
|
||||
makeEndpoint(util::Config const& serverConfig)
|
||||
{
|
||||
auto const ip = serverConfig.maybeValue<std::string>("ip");
|
||||
if (not ip.has_value())
|
||||
return std::unexpected{"Missing 'ip` in server config."};
|
||||
|
||||
boost::system::error_code error;
|
||||
auto const address = boost::asio::ip::make_address(*ip, error);
|
||||
if (error)
|
||||
return std::unexpected{fmt::format("Error parsing provided IP: {}", error.message())};
|
||||
|
||||
auto const port = serverConfig.maybeValue<unsigned short>("port");
|
||||
if (not port.has_value())
|
||||
return std::unexpected{"Missing 'port` in server config."};
|
||||
|
||||
return boost::asio::ip::tcp::endpoint{address, *port};
|
||||
}
|
||||
|
||||
std::expected<boost::asio::ip::tcp::acceptor, std::string>
|
||||
makeAcceptor(boost::asio::io_context& context, boost::asio::ip::tcp::endpoint const& endpoint)
|
||||
{
|
||||
boost::asio::ip::tcp::acceptor acceptor{context};
|
||||
try {
|
||||
acceptor.open(endpoint.protocol());
|
||||
acceptor.set_option(boost::asio::socket_base::reuse_address(true));
|
||||
acceptor.bind(endpoint);
|
||||
acceptor.listen(boost::asio::socket_base::max_listen_connections);
|
||||
} catch (boost::system::system_error const& error) {
|
||||
return std::unexpected{fmt::format("Error creating TCP acceptor: {}", error.what())};
|
||||
}
|
||||
return acceptor;
|
||||
}
|
||||
|
||||
std::expected<std::string, boost::system::system_error>
|
||||
extractIp(boost::asio::ip::tcp::socket const& socket)
|
||||
{
|
||||
std::string ip;
|
||||
try {
|
||||
ip = socket.remote_endpoint().address().to_string();
|
||||
} catch (boost::system::system_error const& error) {
|
||||
return std::unexpected{error};
|
||||
}
|
||||
return ip;
|
||||
}
|
||||
|
||||
struct SslDetectionResult {
|
||||
boost::asio::ip::tcp::socket socket;
|
||||
bool isSsl;
|
||||
boost::beast::flat_buffer buffer;
|
||||
};
|
||||
|
||||
std::expected<std::optional<SslDetectionResult>, std::string>
|
||||
detectSsl(boost::asio::ip::tcp::socket socket, boost::asio::yield_context yield)
|
||||
{
|
||||
boost::beast::tcp_stream tcpStream{std::move(socket)};
|
||||
boost::beast::flat_buffer buffer;
|
||||
boost::beast::error_code errorCode;
|
||||
bool const isSsl = boost::beast::async_detect_ssl(tcpStream, buffer, yield[errorCode]);
|
||||
|
||||
if (errorCode == boost::asio::ssl::error::stream_truncated)
|
||||
return std::nullopt;
|
||||
|
||||
if (errorCode)
|
||||
return std::unexpected{fmt::format("Detector failed (detect): {}", errorCode.message())};
|
||||
|
||||
return SslDetectionResult{.socket = tcpStream.release_socket(), .isSsl = isSsl, .buffer = std::move(buffer)};
|
||||
}
|
||||
|
||||
std::expected<ConnectionPtr, std::string>
|
||||
makeConnection(
|
||||
SslDetectionResult sslDetectionResult,
|
||||
std::optional<boost::asio::ssl::context>& sslContext,
|
||||
std::string ip,
|
||||
util::TagDecoratorFactory& tagDecoratorFactory,
|
||||
boost::asio::yield_context yield
|
||||
)
|
||||
{
|
||||
impl::UpgradableConnectionPtr connection;
|
||||
if (sslDetectionResult.isSsl) {
|
||||
if (not sslContext.has_value())
|
||||
return std::unexpected{"SSL is not supported by this server"};
|
||||
|
||||
connection = std::make_unique<impl::SslHttpConnection>(
|
||||
std::move(sslDetectionResult.socket),
|
||||
std::move(ip),
|
||||
std::move(sslDetectionResult.buffer),
|
||||
*sslContext,
|
||||
tagDecoratorFactory
|
||||
);
|
||||
} else {
|
||||
connection = std::make_unique<impl::PlainHttpConnection>(
|
||||
std::move(sslDetectionResult.socket),
|
||||
std::move(ip),
|
||||
std::move(sslDetectionResult.buffer),
|
||||
tagDecoratorFactory
|
||||
);
|
||||
}
|
||||
|
||||
auto const expectedIsUpgrade = connection->isUpgradeRequested(yield);
|
||||
if (not expectedIsUpgrade.has_value()) {
|
||||
return std::unexpected{
|
||||
fmt::format("Error checking whether upgrade requested: {}", expectedIsUpgrade.error().message())
|
||||
};
|
||||
}
|
||||
|
||||
if (*expectedIsUpgrade) {
|
||||
auto expectedUpgradedConnection = connection->upgrade(sslContext, tagDecoratorFactory, yield);
|
||||
if (expectedUpgradedConnection.has_value())
|
||||
return std::move(expectedUpgradedConnection).value();
|
||||
|
||||
return std::unexpected{fmt::format("Error upgrading connection: {}", expectedUpgradedConnection.error().what())
|
||||
};
|
||||
}
|
||||
|
||||
return connection;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Server::Server(
|
||||
boost::asio::io_context& ctx,
|
||||
boost::asio::ip::tcp::endpoint endpoint,
|
||||
std::optional<boost::asio::ssl::context> sslContext,
|
||||
impl::ConnectionHandler connectionHandler,
|
||||
util::TagDecoratorFactory tagDecoratorFactory
|
||||
)
|
||||
: ctx_{ctx}
|
||||
, sslContext_{std::move(sslContext)}
|
||||
, connectionHandler_{std::move(connectionHandler)}
|
||||
, endpoint_{std::move(endpoint)}
|
||||
, tagDecoratorFactory_{tagDecoratorFactory}
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
Server::onGet(std::string const& target, MessageHandler handler)
|
||||
{
|
||||
ASSERT(not running_, "Adding a GET handler is not allowed when Server is running.");
|
||||
connectionHandler_.onGet(target, std::move(handler));
|
||||
}
|
||||
|
||||
void
|
||||
Server::onPost(std::string const& target, MessageHandler handler)
|
||||
{
|
||||
ASSERT(not running_, "Adding a POST handler is not allowed when Server is running.");
|
||||
connectionHandler_.onPost(target, std::move(handler));
|
||||
}
|
||||
|
||||
void
|
||||
Server::onWs(MessageHandler handler)
|
||||
{
|
||||
ASSERT(not running_, "Adding a Websocket handler is not allowed when Server is running.");
|
||||
connectionHandler_.onWs(std::move(handler));
|
||||
}
|
||||
|
||||
std::optional<std::string>
|
||||
Server::run()
|
||||
{
|
||||
auto acceptor = makeAcceptor(ctx_.get(), endpoint_);
|
||||
if (not acceptor.has_value())
|
||||
return std::move(acceptor).error();
|
||||
|
||||
running_ = true;
|
||||
boost::asio::spawn(
|
||||
ctx_.get(),
|
||||
[this, acceptor = std::move(acceptor).value()](boost::asio::yield_context yield) mutable {
|
||||
while (true) {
|
||||
boost::beast::error_code errorCode;
|
||||
boost::asio::ip::tcp::socket socket{ctx_.get().get_executor()};
|
||||
|
||||
acceptor.async_accept(socket, yield[errorCode]);
|
||||
if (errorCode) {
|
||||
LOG(log_.debug()) << "Error accepting a connection: " << errorCode.what();
|
||||
continue;
|
||||
}
|
||||
boost::asio::spawn(
|
||||
ctx_.get(),
|
||||
[this, socket = std::move(socket)](boost::asio::yield_context yield) mutable {
|
||||
handleConnection(std::move(socket), yield);
|
||||
},
|
||||
boost::asio::detached
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
void
|
||||
Server::stop()
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
Server::handleConnection(boost::asio::ip::tcp::socket socket, boost::asio::yield_context yield)
|
||||
{
|
||||
auto sslDetectionResultExpected = detectSsl(std::move(socket), yield);
|
||||
if (not sslDetectionResultExpected) {
|
||||
LOG(log_.info()) << sslDetectionResultExpected.error();
|
||||
return;
|
||||
}
|
||||
auto sslDetectionResult = std::move(sslDetectionResultExpected).value();
|
||||
if (not sslDetectionResult)
|
||||
return; // stream truncated, probably user disconnected
|
||||
|
||||
auto ip = extractIp(sslDetectionResult->socket);
|
||||
if (not ip.has_value()) {
|
||||
LOG(log_.info()) << "Cannot get remote endpoint: " << ip.error().what();
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO(kuznetsss): check ip with dosguard here
|
||||
|
||||
auto connectionExpected = makeConnection(
|
||||
std::move(sslDetectionResult).value(), sslContext_, std::move(ip).value(), tagDecoratorFactory_, yield
|
||||
);
|
||||
if (not connectionExpected.has_value()) {
|
||||
LOG(log_.info()) << "Error creating a connection: " << connectionExpected.error();
|
||||
return;
|
||||
}
|
||||
|
||||
boost::asio::spawn(
|
||||
ctx_.get(),
|
||||
[this, connection = std::move(connectionExpected).value()](boost::asio::yield_context yield) mutable {
|
||||
connectionHandler_.processConnection(std::move(connection), yield);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
std::expected<Server, std::string>
|
||||
make_Server(util::Config const& config, boost::asio::io_context& context)
|
||||
{
|
||||
auto const serverConfig = config.section("server");
|
||||
|
||||
auto endpoint = makeEndpoint(serverConfig);
|
||||
if (not endpoint.has_value())
|
||||
return std::unexpected{std::move(endpoint).error()};
|
||||
|
||||
auto expectedSslContext = impl::makeServerSslContext(config);
|
||||
if (not expectedSslContext)
|
||||
return std::unexpected{std::move(expectedSslContext).error()};
|
||||
|
||||
impl::ConnectionHandler::ProcessingPolicy processingPolicy{impl::ConnectionHandler::ProcessingPolicy::Parallel};
|
||||
std::optional<size_t> parallelRequestLimit;
|
||||
|
||||
auto const processingStrategyStr = serverConfig.valueOr<std::string>("processing_policy", "parallel");
|
||||
if (processingStrategyStr == "sequent") {
|
||||
processingPolicy = impl::ConnectionHandler::ProcessingPolicy::Sequential;
|
||||
} else if (processingStrategyStr == "parallel") {
|
||||
parallelRequestLimit = serverConfig.maybeValue<size_t>("parallel_requests_limit");
|
||||
} else {
|
||||
return std::unexpected{fmt::format("Invalid 'server.processing_strategy': {}", processingStrategyStr)};
|
||||
}
|
||||
|
||||
return Server{
|
||||
context,
|
||||
std::move(endpoint).value(),
|
||||
std::move(expectedSslContext).value(),
|
||||
impl::ConnectionHandler{processingPolicy, parallelRequestLimit},
|
||||
util::TagDecoratorFactory(config)
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace web::ng
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2024, 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 "util/Taggable.hpp"
|
||||
#include "util/config/Config.hpp"
|
||||
#include "util/log/Logger.hpp"
|
||||
#include "web/impl/AdminVerificationStrategy.hpp"
|
||||
#include "web/ng/MessageHandler.hpp"
|
||||
#include "web/ng/impl/ConnectionHandler.hpp"
|
||||
|
||||
#include <boost/asio/io_context.hpp>
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/asio/ssl/context.hpp>
|
||||
|
||||
#include <cstddef>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
namespace web::ng {
|
||||
|
||||
/**
|
||||
* @brief Web server class.
|
||||
*/
|
||||
class Server {
|
||||
util::Logger log_{"WebServer"};
|
||||
util::Logger perfLog_{"Performance"};
|
||||
std::reference_wrapper<boost::asio::io_context> ctx_;
|
||||
|
||||
std::optional<boost::asio::ssl::context> sslContext_;
|
||||
|
||||
impl::ConnectionHandler connectionHandler_;
|
||||
|
||||
boost::asio::ip::tcp::endpoint endpoint_;
|
||||
|
||||
util::TagDecoratorFactory tagDecoratorFactory_;
|
||||
|
||||
bool running_{false};
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Construct a new Server object.
|
||||
*
|
||||
* @param ctx The boost::asio::io_context to use.
|
||||
* @param endpoint The endpoint to listen on.
|
||||
* @param sslContext The SSL context to use (optional).
|
||||
* @param connectionHandler The connection handler.
|
||||
* @param tagDecoratorFactory The tag decorator factory.
|
||||
*/
|
||||
Server(
|
||||
boost::asio::io_context& ctx,
|
||||
boost::asio::ip::tcp::endpoint endpoint,
|
||||
std::optional<boost::asio::ssl::context> sslContext,
|
||||
impl::ConnectionHandler connectionHandler,
|
||||
util::TagDecoratorFactory tagDecoratorFactory
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Copy constructor is deleted. The Server couldn't be copied.
|
||||
*/
|
||||
Server(Server const&) = delete;
|
||||
|
||||
/**
|
||||
* @brief Move constructor is defaulted.
|
||||
*/
|
||||
Server(Server&&) = default;
|
||||
|
||||
/**
|
||||
* @brief Set handler for GET requests.
|
||||
* @note This method can't be called after run() is called.
|
||||
*
|
||||
* @param target The target of the request.
|
||||
* @param handler The handler to set.
|
||||
*/
|
||||
void
|
||||
onGet(std::string const& target, MessageHandler handler);
|
||||
|
||||
/**
|
||||
* @brief Set handler for POST requests.
|
||||
* @note This method can't be called after run() is called.
|
||||
*
|
||||
* @param target The target of the request.
|
||||
* @param handler The handler to set.
|
||||
*/
|
||||
void
|
||||
onPost(std::string const& target, MessageHandler handler);
|
||||
|
||||
/**
|
||||
* @brief Set handler for WebSocket requests.
|
||||
* @note This method can't be called after run() is called.
|
||||
*
|
||||
* @param handler The handler to set.
|
||||
*/
|
||||
void
|
||||
onWs(MessageHandler handler);
|
||||
|
||||
/**
|
||||
* @brief Run the server.
|
||||
*
|
||||
* @return std::nullopt if the server started successfully, otherwise an error message.
|
||||
*/
|
||||
std::optional<std::string>
|
||||
run();
|
||||
|
||||
/**
|
||||
* @brief Stop the server.
|
||||
** @note Stopping the server cause graceful shutdown of all connections. And rejecting new connections.
|
||||
*/
|
||||
void
|
||||
stop();
|
||||
|
||||
private:
|
||||
void
|
||||
handleConnection(boost::asio::ip::tcp::socket socket, boost::asio::yield_context yield);
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Create a new Server.
|
||||
*
|
||||
* @param config The configuration.
|
||||
* @param context The boost::asio::io_context to use.
|
||||
*
|
||||
* @return The Server or an error message.
|
||||
*/
|
||||
std::expected<Server, std::string>
|
||||
make_Server(util::Config const& config, boost::asio::io_context& context);
|
||||
|
||||
} // namespace web::ng
|
||||
|
||||
35
src/web/ng/impl/Concepts.hpp
Normal file
35
src/web/ng/impl/Concepts.hpp
Normal file
@@ -0,0 +1,35 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2024, 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 <boost/beast/core/basic_stream.hpp>
|
||||
#include <boost/beast/core/tcp_stream.hpp>
|
||||
|
||||
#include <type_traits>
|
||||
|
||||
namespace web::ng::impl {
|
||||
|
||||
template <typename T>
|
||||
concept IsTcpStream = std::is_same_v<std::decay_t<T>, boost::beast::tcp_stream>;
|
||||
|
||||
template <typename T>
|
||||
concept IsSslTcpStream = std::is_same_v<std::decay_t<T>, boost::asio::ssl::stream<boost::beast::tcp_stream>>;
|
||||
|
||||
} // namespace web::ng::impl
|
||||
281
src/web/ng/impl/ConnectionHandler.cpp
Normal file
281
src/web/ng/impl/ConnectionHandler.cpp
Normal file
@@ -0,0 +1,281 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2024, 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 "web/ng/impl/ConnectionHandler.hpp"
|
||||
|
||||
#include "util/Assert.hpp"
|
||||
#include "util/CoroutineGroup.hpp"
|
||||
#include "util/log/Logger.hpp"
|
||||
#include "web/ng/Connection.hpp"
|
||||
#include "web/ng/Error.hpp"
|
||||
#include "web/ng/MessageHandler.hpp"
|
||||
#include "web/ng/Request.hpp"
|
||||
#include "web/ng/Response.hpp"
|
||||
|
||||
#include <boost/asio/error.hpp>
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/asio/ssl/error.hpp>
|
||||
#include <boost/beast/http/error.hpp>
|
||||
#include <boost/beast/http/status.hpp>
|
||||
#include <boost/beast/websocket/error.hpp>
|
||||
|
||||
#include <cstddef>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
namespace web::ng::impl {
|
||||
|
||||
namespace {
|
||||
|
||||
Response
|
||||
handleHttpRequest(
|
||||
ConnectionContext const& connectionContext,
|
||||
ConnectionHandler::TargetToHandlerMap const& handlers,
|
||||
Request const& request,
|
||||
boost::asio::yield_context yield
|
||||
)
|
||||
{
|
||||
ASSERT(request.target().has_value(), "Got not a HTTP request");
|
||||
auto it = handlers.find(*request.target());
|
||||
if (it == handlers.end()) {
|
||||
return Response{boost::beast::http::status::bad_request, "Bad target", request};
|
||||
}
|
||||
return it->second(request, connectionContext, yield);
|
||||
}
|
||||
|
||||
Response
|
||||
handleWsRequest(
|
||||
ConnectionContext connectionContext,
|
||||
std::optional<MessageHandler> const& handler,
|
||||
Request const& request,
|
||||
boost::asio::yield_context yield
|
||||
)
|
||||
{
|
||||
if (not handler.has_value()) {
|
||||
return Response{boost::beast::http::status::bad_request, "WebSocket is not supported by this server", request};
|
||||
}
|
||||
return handler->operator()(request, connectionContext, yield);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
size_t
|
||||
ConnectionHandler::StringHash::operator()(char const* str) const
|
||||
{
|
||||
return hash_type{}(str);
|
||||
}
|
||||
|
||||
size_t
|
||||
ConnectionHandler::StringHash::operator()(std::string_view str) const
|
||||
{
|
||||
return hash_type{}(str);
|
||||
}
|
||||
|
||||
size_t
|
||||
ConnectionHandler::StringHash::operator()(std::string const& str) const
|
||||
{
|
||||
return hash_type{}(str);
|
||||
}
|
||||
|
||||
ConnectionHandler::ConnectionHandler(ProcessingPolicy processingPolicy, std::optional<size_t> maxParallelRequests)
|
||||
: processingPolicy_{processingPolicy}, maxParallelRequests_{maxParallelRequests}
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
ConnectionHandler::onGet(std::string const& target, MessageHandler handler)
|
||||
{
|
||||
getHandlers_[target] = std::move(handler);
|
||||
}
|
||||
|
||||
void
|
||||
ConnectionHandler::onPost(std::string const& target, MessageHandler handler)
|
||||
{
|
||||
postHandlers_[target] = std::move(handler);
|
||||
}
|
||||
|
||||
void
|
||||
ConnectionHandler::onWs(MessageHandler handler)
|
||||
{
|
||||
wsHandler_ = std::move(handler);
|
||||
}
|
||||
|
||||
void
|
||||
ConnectionHandler::processConnection(ConnectionPtr connectionPtr, boost::asio::yield_context yield)
|
||||
{
|
||||
auto& connectionRef = *connectionPtr;
|
||||
auto signalConnection = onStop_.connect([&connectionRef, yield]() { connectionRef.close(yield); });
|
||||
|
||||
bool shouldCloseGracefully = false;
|
||||
|
||||
switch (processingPolicy_) {
|
||||
case ProcessingPolicy::Sequential:
|
||||
shouldCloseGracefully = sequentRequestResponseLoop(connectionRef, yield);
|
||||
break;
|
||||
case ProcessingPolicy::Parallel:
|
||||
shouldCloseGracefully = parallelRequestResponseLoop(connectionRef, yield);
|
||||
break;
|
||||
}
|
||||
if (shouldCloseGracefully)
|
||||
connectionRef.close(yield);
|
||||
|
||||
signalConnection.disconnect();
|
||||
}
|
||||
|
||||
void
|
||||
ConnectionHandler::stop()
|
||||
{
|
||||
onStop_();
|
||||
}
|
||||
|
||||
bool
|
||||
ConnectionHandler::handleError(Error const& error, Connection const& connection) const
|
||||
{
|
||||
// ssl::error::stream_truncated, also known as an SSL "short read",
|
||||
// indicates the peer closed the connection without performing the
|
||||
// required closing handshake (for example, Google does this to
|
||||
// improve performance). Generally this can be a security issue,
|
||||
// but if your communication protocol is self-terminated (as
|
||||
// it is with both HTTP and WebSocket) then you may simply
|
||||
// ignore the lack of close_notify.
|
||||
//
|
||||
// https://github.com/boostorg/beast/issues/38
|
||||
//
|
||||
// https://security.stackexchange.com/questions/91435/how-to-handle-a-malicious-ssl-tls-shutdown
|
||||
//
|
||||
// When a short read would cut off the end of an HTTP message,
|
||||
// Beast returns the error boost::beast::http::error::partial_message.
|
||||
// Therefore, if we see a short read here, it has occurred
|
||||
// after the message has been completed, so it is safe to ignore it.
|
||||
if (error == boost::beast::http::error::end_of_stream || error == boost::asio::ssl::error::stream_truncated)
|
||||
return false;
|
||||
|
||||
// WebSocket connection was gracefully closed
|
||||
if (error == boost::beast::websocket::error::closed)
|
||||
return false;
|
||||
|
||||
if (error != boost::asio::error::operation_aborted) {
|
||||
LOG(log_.error()) << connection.tag() << ": " << error.message() << ": " << error.value();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
ConnectionHandler::sequentRequestResponseLoop(Connection& connection, boost::asio::yield_context yield)
|
||||
{
|
||||
// The loop here is infinite because:
|
||||
// - For websocket connection is persistent so Clio will try to read and respond infinite unless client
|
||||
// disconnected.
|
||||
// - When client disconnected connection.send() or connection.receive() will return an error.
|
||||
// - For http it is still a loop to reuse the connection if keep alive is set. Otherwise client will disconnect and
|
||||
// an error appears.
|
||||
// - When server is shutting down it will cancel all operations on the connection so an error appears.
|
||||
|
||||
while (true) {
|
||||
auto expectedRequest = connection.receive(yield);
|
||||
if (not expectedRequest)
|
||||
return handleError(expectedRequest.error(), connection);
|
||||
|
||||
LOG(log_.info()) << connection.tag() << "Received request from ip = " << connection.ip();
|
||||
|
||||
auto maybeReturnValue = processRequest(connection, std::move(expectedRequest).value(), yield);
|
||||
if (maybeReturnValue.has_value())
|
||||
return maybeReturnValue.value();
|
||||
}
|
||||
}
|
||||
|
||||
bool
|
||||
ConnectionHandler::parallelRequestResponseLoop(Connection& connection, boost::asio::yield_context yield)
|
||||
{
|
||||
// atomic_bool is not needed here because everything happening on coroutine's strand
|
||||
bool stop = false;
|
||||
bool closeConnectionGracefully = true;
|
||||
util::CoroutineGroup tasksGroup{yield, maxParallelRequests_};
|
||||
|
||||
while (not stop) {
|
||||
auto expectedRequest = connection.receive(yield);
|
||||
if (not expectedRequest) {
|
||||
auto const closeGracefully = handleError(expectedRequest.error(), connection);
|
||||
stop = true;
|
||||
closeConnectionGracefully &= closeGracefully;
|
||||
break;
|
||||
}
|
||||
if (tasksGroup.canSpawn()) {
|
||||
bool const spawnSuccess = tasksGroup.spawn(
|
||||
yield, // spawn on the same strand
|
||||
[this, &stop, &closeConnectionGracefully, &connection, request = std::move(expectedRequest).value()](
|
||||
boost::asio::yield_context innerYield
|
||||
) mutable {
|
||||
auto maybeCloseConnectionGracefully = processRequest(connection, request, innerYield);
|
||||
if (maybeCloseConnectionGracefully.has_value()) {
|
||||
stop = true;
|
||||
closeConnectionGracefully &= maybeCloseConnectionGracefully.value();
|
||||
}
|
||||
}
|
||||
);
|
||||
ASSERT(spawnSuccess, "The coroutine was expected to be spawned");
|
||||
} else {
|
||||
connection.send(
|
||||
Response{
|
||||
boost::beast::http::status::too_many_requests,
|
||||
"Too many requests for one connection",
|
||||
expectedRequest.value()
|
||||
},
|
||||
yield
|
||||
);
|
||||
}
|
||||
}
|
||||
tasksGroup.asyncWait(yield);
|
||||
return closeConnectionGracefully;
|
||||
}
|
||||
|
||||
std::optional<bool>
|
||||
ConnectionHandler::processRequest(Connection& connection, Request const& request, boost::asio::yield_context yield)
|
||||
{
|
||||
auto response = handleRequest(connection.context(), request, yield);
|
||||
|
||||
auto const maybeError = connection.send(std::move(response), yield);
|
||||
if (maybeError.has_value()) {
|
||||
return handleError(maybeError.value(), connection);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
Response
|
||||
ConnectionHandler::handleRequest(
|
||||
ConnectionContext const& connectionContext,
|
||||
Request const& request,
|
||||
boost::asio::yield_context yield
|
||||
)
|
||||
{
|
||||
switch (request.method()) {
|
||||
case Request::Method::Get:
|
||||
return handleHttpRequest(connectionContext, getHandlers_, request, yield);
|
||||
case Request::Method::Post:
|
||||
return handleHttpRequest(connectionContext, postHandlers_, request, yield);
|
||||
case Request::Method::Websocket:
|
||||
return handleWsRequest(connectionContext, wsHandler_, request, yield);
|
||||
default:
|
||||
return Response{boost::beast::http::status::bad_request, "Unsupported http method", request};
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace web::ng::impl
|
||||
130
src/web/ng/impl/ConnectionHandler.hpp
Normal file
130
src/web/ng/impl/ConnectionHandler.hpp
Normal file
@@ -0,0 +1,130 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2024, 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 "util/log/Logger.hpp"
|
||||
#include "web/ng/Connection.hpp"
|
||||
#include "web/ng/Error.hpp"
|
||||
#include "web/ng/MessageHandler.hpp"
|
||||
#include "web/ng/Request.hpp"
|
||||
#include "web/ng/Response.hpp"
|
||||
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/signals2/signal.hpp>
|
||||
#include <boost/signals2/variadic_signal.hpp>
|
||||
|
||||
#include <cstddef>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace web::ng::impl {
|
||||
|
||||
class ConnectionHandler {
|
||||
public:
|
||||
enum class ProcessingPolicy { Sequential, Parallel };
|
||||
|
||||
struct StringHash {
|
||||
using hash_type = std::hash<std::string_view>;
|
||||
using is_transparent = void;
|
||||
|
||||
std::size_t
|
||||
operator()(char const* str) const;
|
||||
std::size_t
|
||||
operator()(std::string_view str) const;
|
||||
std::size_t
|
||||
operator()(std::string const& str) const;
|
||||
};
|
||||
|
||||
using TargetToHandlerMap = std::unordered_map<std::string, MessageHandler, StringHash, std::equal_to<>>;
|
||||
|
||||
private:
|
||||
util::Logger log_{"WebServer"};
|
||||
util::Logger perfLog_{"Performance"};
|
||||
|
||||
ProcessingPolicy processingPolicy_;
|
||||
std::optional<size_t> maxParallelRequests_;
|
||||
|
||||
TargetToHandlerMap getHandlers_;
|
||||
TargetToHandlerMap postHandlers_;
|
||||
std::optional<MessageHandler> wsHandler_;
|
||||
|
||||
boost::signals2::signal<void()> onStop_;
|
||||
|
||||
public:
|
||||
ConnectionHandler(ProcessingPolicy processingPolicy, std::optional<size_t> maxParallelRequests);
|
||||
|
||||
void
|
||||
onGet(std::string const& target, MessageHandler handler);
|
||||
|
||||
void
|
||||
onPost(std::string const& target, MessageHandler handler);
|
||||
|
||||
void
|
||||
onWs(MessageHandler handler);
|
||||
|
||||
void
|
||||
processConnection(ConnectionPtr connection, boost::asio::yield_context yield);
|
||||
|
||||
void
|
||||
stop();
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief Handle an error.
|
||||
*
|
||||
* @param error The error to handle.
|
||||
* @param connection The connection that caused the error.
|
||||
* @return True if the connection should be gracefully closed, false otherwise.
|
||||
*/
|
||||
bool
|
||||
handleError(Error const& error, Connection const& connection) const;
|
||||
|
||||
/**
|
||||
* @brief The request-response loop.
|
||||
*
|
||||
* @param connection The connection to handle.
|
||||
* @param yield The yield context.
|
||||
* @return True if the connection should be gracefully closed, false otherwise.
|
||||
*/
|
||||
bool
|
||||
sequentRequestResponseLoop(Connection& connection, boost::asio::yield_context yield);
|
||||
|
||||
bool
|
||||
parallelRequestResponseLoop(Connection& connection, boost::asio::yield_context yield);
|
||||
|
||||
std::optional<bool>
|
||||
processRequest(Connection& connection, Request const& request, boost::asio::yield_context yield);
|
||||
|
||||
/**
|
||||
* @brief Handle a request.
|
||||
*
|
||||
* @param connectionContext The connection context.
|
||||
* @param request The request to handle.
|
||||
* @param yield The yield context.
|
||||
* @return The response to send.
|
||||
*/
|
||||
Response
|
||||
handleRequest(ConnectionContext const& connectionContext, Request const& request, boost::asio::yield_context yield);
|
||||
};
|
||||
|
||||
} // namespace web::ng::impl
|
||||
219
src/web/ng/impl/HttpConnection.hpp
Normal file
219
src/web/ng/impl/HttpConnection.hpp
Normal file
@@ -0,0 +1,219 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2024, 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 "util/Assert.hpp"
|
||||
#include "util/Taggable.hpp"
|
||||
#include "web/ng/Connection.hpp"
|
||||
#include "web/ng/Error.hpp"
|
||||
#include "web/ng/Request.hpp"
|
||||
#include "web/ng/Response.hpp"
|
||||
#include "web/ng/impl/Concepts.hpp"
|
||||
#include "web/ng/impl/WsConnection.hpp"
|
||||
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/asio/ssl/context.hpp>
|
||||
#include <boost/asio/ssl/stream.hpp>
|
||||
#include <boost/beast/core/basic_stream.hpp>
|
||||
#include <boost/beast/core/error.hpp>
|
||||
#include <boost/beast/core/flat_buffer.hpp>
|
||||
#include <boost/beast/core/tcp_stream.hpp>
|
||||
#include <boost/beast/http.hpp>
|
||||
#include <boost/beast/http/message.hpp>
|
||||
#include <boost/beast/http/string_body.hpp>
|
||||
#include <boost/beast/websocket.hpp>
|
||||
|
||||
#include <chrono>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace web::ng::impl {
|
||||
|
||||
class UpgradableConnection : public Connection {
|
||||
public:
|
||||
using Connection::Connection;
|
||||
|
||||
virtual std::expected<bool, Error>
|
||||
isUpgradeRequested(
|
||||
boost::asio::yield_context yield,
|
||||
std::chrono::steady_clock::duration timeout = DEFAULT_TIMEOUT
|
||||
) = 0;
|
||||
|
||||
virtual std::expected<ConnectionPtr, Error>
|
||||
upgrade(
|
||||
std::optional<boost::asio::ssl::context>& sslContext,
|
||||
util::TagDecoratorFactory const& tagDecoratorFactory,
|
||||
boost::asio::yield_context yield
|
||||
) = 0;
|
||||
};
|
||||
|
||||
using UpgradableConnectionPtr = std::unique_ptr<UpgradableConnection>;
|
||||
|
||||
template <typename StreamType>
|
||||
class HttpConnection : public UpgradableConnection {
|
||||
StreamType stream_;
|
||||
std::optional<boost::beast::http::request<boost::beast::http::string_body>> request_;
|
||||
|
||||
public:
|
||||
HttpConnection(
|
||||
boost::asio::ip::tcp::socket socket,
|
||||
std::string ip,
|
||||
boost::beast::flat_buffer buffer,
|
||||
util::TagDecoratorFactory const& tagDecoratorFactory
|
||||
)
|
||||
requires IsTcpStream<StreamType>
|
||||
: UpgradableConnection(std::move(ip), std::move(buffer), tagDecoratorFactory), stream_{std::move(socket)}
|
||||
{
|
||||
}
|
||||
|
||||
HttpConnection(
|
||||
boost::asio::ip::tcp::socket socket,
|
||||
std::string ip,
|
||||
boost::beast::flat_buffer buffer,
|
||||
boost::asio::ssl::context& sslCtx,
|
||||
util::TagDecoratorFactory const& tagDecoratorFactory
|
||||
)
|
||||
requires IsSslTcpStream<StreamType>
|
||||
: UpgradableConnection(std::move(ip), std::move(buffer), tagDecoratorFactory)
|
||||
, stream_{std::move(socket), sslCtx}
|
||||
{
|
||||
}
|
||||
|
||||
bool
|
||||
wasUpgraded() const override
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
std::optional<Error>
|
||||
send(
|
||||
Response response,
|
||||
boost::asio::yield_context yield,
|
||||
std::chrono::steady_clock::duration timeout = DEFAULT_TIMEOUT
|
||||
) override
|
||||
{
|
||||
auto const httpResponse = std::move(response).intoHttpResponse();
|
||||
boost::system::error_code error;
|
||||
boost::beast::get_lowest_layer(stream_).expires_after(timeout);
|
||||
boost::beast::http::async_write(stream_, httpResponse, yield[error]);
|
||||
if (error)
|
||||
return error;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::expected<Request, Error>
|
||||
receive(boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout = DEFAULT_TIMEOUT) override
|
||||
{
|
||||
if (request_.has_value()) {
|
||||
Request result{std::move(request_).value()};
|
||||
request_.reset();
|
||||
return result;
|
||||
}
|
||||
auto expectedRequest = fetch(yield, timeout);
|
||||
if (expectedRequest.has_value())
|
||||
return Request{std::move(expectedRequest).value()};
|
||||
|
||||
return std::unexpected{std::move(expectedRequest).error()};
|
||||
}
|
||||
|
||||
void
|
||||
close(boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout = DEFAULT_TIMEOUT) override
|
||||
{
|
||||
[[maybe_unused]] boost::system::error_code error;
|
||||
if constexpr (IsSslTcpStream<StreamType>) {
|
||||
boost::beast::get_lowest_layer(stream_).expires_after(timeout);
|
||||
stream_.async_shutdown(yield[error]);
|
||||
}
|
||||
if constexpr (IsTcpStream<StreamType>) {
|
||||
stream_.socket().shutdown(boost::asio::ip::tcp::socket::shutdown_type::shutdown_both, error);
|
||||
} else {
|
||||
boost::beast::get_lowest_layer(stream_).socket().shutdown(
|
||||
boost::asio::ip::tcp::socket::shutdown_type::shutdown_both, error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
std::expected<bool, Error>
|
||||
isUpgradeRequested(boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout = DEFAULT_TIMEOUT)
|
||||
override
|
||||
{
|
||||
auto expectedRequest = fetch(yield, timeout);
|
||||
if (not expectedRequest.has_value())
|
||||
return std::unexpected{std::move(expectedRequest).error()};
|
||||
|
||||
request_ = std::move(expectedRequest).value();
|
||||
|
||||
return boost::beast::websocket::is_upgrade(request_.value());
|
||||
}
|
||||
|
||||
std::expected<ConnectionPtr, Error>
|
||||
upgrade(
|
||||
[[maybe_unused]] std::optional<boost::asio::ssl::context>& sslContext,
|
||||
util::TagDecoratorFactory const& tagDecoratorFactory,
|
||||
boost::asio::yield_context yield
|
||||
) override
|
||||
{
|
||||
ASSERT(request_.has_value(), "Request must be present to upgrade the connection");
|
||||
|
||||
if constexpr (IsSslTcpStream<StreamType>) {
|
||||
ASSERT(sslContext.has_value(), "SSL context must be present to upgrade the connection");
|
||||
return make_SslWsConnection(
|
||||
boost::beast::get_lowest_layer(stream_).release_socket(),
|
||||
std::move(ip_),
|
||||
std::move(buffer_),
|
||||
std::move(request_).value(),
|
||||
sslContext.value(),
|
||||
tagDecoratorFactory,
|
||||
yield
|
||||
);
|
||||
} else {
|
||||
return make_PlainWsConnection(
|
||||
stream_.release_socket(),
|
||||
std::move(ip_),
|
||||
std::move(buffer_),
|
||||
std::move(request_).value(),
|
||||
tagDecoratorFactory,
|
||||
yield
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
std::expected<boost::beast::http::request<boost::beast::http::string_body>, Error>
|
||||
fetch(boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout)
|
||||
{
|
||||
boost::beast::http::request<boost::beast::http::string_body> request{};
|
||||
boost::system::error_code error;
|
||||
boost::beast::get_lowest_layer(stream_).expires_after(timeout);
|
||||
boost::beast::http::async_read(stream_, buffer_, request, yield[error]);
|
||||
if (error)
|
||||
return std::unexpected{error};
|
||||
return request;
|
||||
}
|
||||
};
|
||||
|
||||
using PlainHttpConnection = HttpConnection<boost::beast::tcp_stream>;
|
||||
|
||||
using SslHttpConnection = HttpConnection<boost::asio::ssl::stream<boost::beast::tcp_stream>>;
|
||||
|
||||
} // namespace web::ng::impl
|
||||
@@ -17,7 +17,9 @@
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include "web/impl/ServerSslContext.hpp"
|
||||
#include "web/ng/impl/ServerSslContext.hpp"
|
||||
|
||||
#include "util/config/Config.hpp"
|
||||
|
||||
#include <boost/asio/buffer.hpp>
|
||||
#include <boost/asio/ssl/context.hpp>
|
||||
@@ -31,7 +33,7 @@
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace web::impl {
|
||||
namespace web::ng::impl {
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -49,32 +51,47 @@ readFile(std::string const& path)
|
||||
|
||||
} // namespace
|
||||
|
||||
std::expected<boost::asio::ssl::context, std::string>
|
||||
makeServerSslContext(std::string const& certFilePath, std::string const& keyFilePath)
|
||||
std::expected<std::optional<boost::asio::ssl::context>, std::string>
|
||||
makeServerSslContext(util::Config const& config)
|
||||
{
|
||||
auto const certContent = readFile(certFilePath);
|
||||
bool const configHasCertFile = config.contains("ssl_cert_file");
|
||||
bool const configHasKeyFile = config.contains("ssl_key_file");
|
||||
|
||||
if (configHasCertFile != configHasKeyFile)
|
||||
return std::unexpected{"Config entries 'ssl_cert_file' and 'ssl_key_file' must be set or unset together."};
|
||||
|
||||
if (not configHasCertFile)
|
||||
return std::nullopt;
|
||||
|
||||
auto const certFilename = config.value<std::string>("ssl_cert_file");
|
||||
auto const certContent = readFile(certFilename);
|
||||
if (!certContent)
|
||||
return std::unexpected{"Can't read SSL certificate: " + certFilePath};
|
||||
return std::unexpected{"Can't read SSL certificate: " + certFilename};
|
||||
|
||||
auto const keyContent = readFile(keyFilePath);
|
||||
auto const keyFilename = config.value<std::string>("ssl_key_file");
|
||||
auto const keyContent = readFile(keyFilename);
|
||||
if (!keyContent)
|
||||
return std::unexpected{"Can't read SSL key: " + keyFilePath};
|
||||
return std::unexpected{"Can't read SSL key: " + keyFilename};
|
||||
|
||||
return impl::makeServerSslContext(*certContent, *keyContent);
|
||||
}
|
||||
|
||||
std::expected<boost::asio::ssl::context, std::string>
|
||||
makeServerSslContext(std::string const& certData, std::string const& keyData)
|
||||
{
|
||||
using namespace boost::asio;
|
||||
|
||||
ssl::context ctx{ssl::context::tls_server};
|
||||
ctx.set_options(ssl::context::default_workarounds | ssl::context::no_sslv2);
|
||||
|
||||
try {
|
||||
ctx.use_certificate_chain(buffer(certContent->data(), certContent->size()));
|
||||
ctx.use_private_key(buffer(keyContent->data(), keyContent->size()), ssl::context::file_format::pem);
|
||||
ctx.use_certificate_chain(buffer(certData.data(), certData.size()));
|
||||
ctx.use_private_key(buffer(keyData.data(), keyData.size()), ssl::context::file_format::pem);
|
||||
} catch (...) {
|
||||
return std::unexpected{
|
||||
fmt::format("Error loading SSL certificate ({}) or SSL key ({}).", certFilePath, keyFilePath)
|
||||
};
|
||||
return std::unexpected{fmt::format("Error loading SSL certificate or SSL key.")};
|
||||
}
|
||||
|
||||
return ctx;
|
||||
}
|
||||
|
||||
} // namespace web::impl
|
||||
} // namespace web::ng::impl
|
||||
@@ -19,14 +19,20 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "util/config/Config.hpp"
|
||||
|
||||
#include <boost/asio/ssl/context.hpp>
|
||||
|
||||
#include <expected>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
namespace web::impl {
|
||||
namespace web::ng::impl {
|
||||
|
||||
std::expected<std::optional<boost::asio::ssl::context>, std::string>
|
||||
makeServerSslContext(util::Config const& config);
|
||||
|
||||
std::expected<boost::asio::ssl::context, std::string>
|
||||
makeServerSslContext(std::string const& certFilePath, std::string const& keyFilePath);
|
||||
makeServerSslContext(std::string const& certData, std::string const& keyData);
|
||||
|
||||
} // namespace web::impl
|
||||
} // namespace web::ng::impl
|
||||
77
src/web/ng/impl/WsConnection.cpp
Normal file
77
src/web/ng/impl/WsConnection.cpp
Normal file
@@ -0,0 +1,77 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2024, 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 "web/ng/impl/WsConnection.hpp"
|
||||
|
||||
#include "util/Taggable.hpp"
|
||||
#include "web/ng/Error.hpp"
|
||||
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/asio/ssl/context.hpp>
|
||||
#include <boost/beast/core/flat_buffer.hpp>
|
||||
#include <boost/beast/http/message.hpp>
|
||||
#include <boost/beast/http/string_body.hpp>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace web::ng::impl {
|
||||
|
||||
std::expected<std::unique_ptr<PlainWsConnection>, Error>
|
||||
make_PlainWsConnection(
|
||||
boost::asio::ip::tcp::socket socket,
|
||||
std::string ip,
|
||||
boost::beast::flat_buffer buffer,
|
||||
boost::beast::http::request<boost::beast::http::string_body> request,
|
||||
util::TagDecoratorFactory const& tagDecoratorFactory,
|
||||
boost::asio::yield_context yield
|
||||
)
|
||||
{
|
||||
auto connection = std::make_unique<PlainWsConnection>(
|
||||
std::move(socket), std::move(ip), std::move(buffer), std::move(request), tagDecoratorFactory
|
||||
);
|
||||
auto maybeError = connection->performHandshake(yield);
|
||||
if (maybeError.has_value())
|
||||
return std::unexpected{maybeError.value()};
|
||||
return connection;
|
||||
}
|
||||
|
||||
std::expected<std::unique_ptr<SslWsConnection>, Error>
|
||||
make_SslWsConnection(
|
||||
boost::asio::ip::tcp::socket socket,
|
||||
std::string ip,
|
||||
boost::beast::flat_buffer buffer,
|
||||
boost::beast::http::request<boost::beast::http::string_body> request,
|
||||
boost::asio::ssl::context& sslContext,
|
||||
util::TagDecoratorFactory const& tagDecoratorFactory,
|
||||
boost::asio::yield_context yield
|
||||
)
|
||||
{
|
||||
auto connection = std::make_unique<SslWsConnection>(
|
||||
std::move(socket), std::move(ip), std::move(buffer), sslContext, std::move(request), tagDecoratorFactory
|
||||
);
|
||||
auto maybeError = connection->performHandshake(yield);
|
||||
if (maybeError.has_value())
|
||||
return std::unexpected{maybeError.value()};
|
||||
return connection;
|
||||
}
|
||||
|
||||
} // namespace web::ng::impl
|
||||
178
src/web/ng/impl/WsConnection.hpp
Normal file
178
src/web/ng/impl/WsConnection.hpp
Normal file
@@ -0,0 +1,178 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2024, 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 "util/Taggable.hpp"
|
||||
#include "util/WithTimeout.hpp"
|
||||
#include "util/build/Build.hpp"
|
||||
#include "web/ng/Connection.hpp"
|
||||
#include "web/ng/Error.hpp"
|
||||
#include "web/ng/Request.hpp"
|
||||
#include "web/ng/Response.hpp"
|
||||
#include "web/ng/impl/Concepts.hpp"
|
||||
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/asio/ssl/context.hpp>
|
||||
#include <boost/asio/ssl/stream.hpp>
|
||||
#include <boost/beast/core/buffers_to_string.hpp>
|
||||
#include <boost/beast/core/flat_buffer.hpp>
|
||||
#include <boost/beast/core/role.hpp>
|
||||
#include <boost/beast/core/tcp_stream.hpp>
|
||||
#include <boost/beast/http/field.hpp>
|
||||
#include <boost/beast/http/message.hpp>
|
||||
#include <boost/beast/http/string_body.hpp>
|
||||
#include <boost/beast/ssl.hpp>
|
||||
#include <boost/beast/websocket/rfc6455.hpp>
|
||||
#include <boost/beast/websocket/stream.hpp>
|
||||
#include <boost/beast/websocket/stream_base.hpp>
|
||||
|
||||
#include <chrono>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace web::ng::impl {
|
||||
|
||||
template <typename StreamType>
|
||||
class WsConnection : public Connection {
|
||||
boost::beast::websocket::stream<StreamType> stream_;
|
||||
boost::beast::http::request<boost::beast::http::string_body> initialRequest_;
|
||||
|
||||
public:
|
||||
WsConnection(
|
||||
boost::asio::ip::tcp::socket socket,
|
||||
std::string ip,
|
||||
boost::beast::flat_buffer buffer,
|
||||
boost::beast::http::request<boost::beast::http::string_body> initialRequest,
|
||||
util::TagDecoratorFactory const& tagDecoratorFactory
|
||||
)
|
||||
requires IsTcpStream<StreamType>
|
||||
: Connection(std::move(ip), std::move(buffer), tagDecoratorFactory)
|
||||
, stream_(std::move(socket))
|
||||
, initialRequest_(std::move(initialRequest))
|
||||
{
|
||||
}
|
||||
|
||||
WsConnection(
|
||||
boost::asio::ip::tcp::socket socket,
|
||||
std::string ip,
|
||||
boost::beast::flat_buffer buffer,
|
||||
boost::asio::ssl::context& sslContext,
|
||||
boost::beast::http::request<boost::beast::http::string_body> initialRequest,
|
||||
util::TagDecoratorFactory const& tagDecoratorFactory
|
||||
)
|
||||
requires IsSslTcpStream<StreamType>
|
||||
: Connection(std::move(ip), std::move(buffer), tagDecoratorFactory)
|
||||
, stream_(std::move(socket), sslContext)
|
||||
, initialRequest_(std::move(initialRequest))
|
||||
{
|
||||
// Disable the timeout. The websocket::stream uses its own timeout settings.
|
||||
boost::beast::get_lowest_layer(stream_).expires_never();
|
||||
stream_.set_option(boost::beast::websocket::stream_base::timeout::suggested(boost::beast::role_type::server));
|
||||
stream_.set_option(
|
||||
boost::beast::websocket::stream_base::decorator([](boost::beast::websocket::response_type& res) {
|
||||
res.set(boost::beast::http::field::server, util::build::getClioFullVersionString());
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
std::optional<Error>
|
||||
performHandshake(boost::asio::yield_context yield)
|
||||
{
|
||||
Error error;
|
||||
stream_.async_accept(initialRequest_, yield[error]);
|
||||
if (error)
|
||||
return error;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
bool
|
||||
wasUpgraded() const override
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
std::optional<Error>
|
||||
send(
|
||||
Response response,
|
||||
boost::asio::yield_context yield,
|
||||
std::chrono::steady_clock::duration timeout = DEFAULT_TIMEOUT
|
||||
) override
|
||||
{
|
||||
auto error = util::withTimeout(
|
||||
[this, &response](auto&& yield) { stream_.async_write(response.asConstBuffer(), yield); }, yield, timeout
|
||||
);
|
||||
if (error)
|
||||
return error;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::expected<Request, Error>
|
||||
receive(boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout = DEFAULT_TIMEOUT) override
|
||||
{
|
||||
auto error = util::withTimeout([this](auto&& yield) { stream_.async_read(buffer_, yield); }, yield, timeout);
|
||||
if (error)
|
||||
return std::unexpected{error};
|
||||
|
||||
auto request = boost::beast::buffers_to_string(buffer_.data());
|
||||
buffer_.consume(buffer_.size());
|
||||
|
||||
return Request{std::move(request), initialRequest_};
|
||||
}
|
||||
|
||||
void
|
||||
close(boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout = DEFAULT_TIMEOUT) override
|
||||
{
|
||||
boost::beast::websocket::stream_base::timeout wsTimeout{};
|
||||
stream_.get_option(wsTimeout);
|
||||
wsTimeout.handshake_timeout = timeout;
|
||||
stream_.set_option(wsTimeout);
|
||||
|
||||
stream_.async_close(boost::beast::websocket::close_code::normal, yield);
|
||||
}
|
||||
};
|
||||
|
||||
using PlainWsConnection = WsConnection<boost::beast::tcp_stream>;
|
||||
using SslWsConnection = WsConnection<boost::asio::ssl::stream<boost::beast::tcp_stream>>;
|
||||
|
||||
std::expected<std::unique_ptr<PlainWsConnection>, Error>
|
||||
make_PlainWsConnection(
|
||||
boost::asio::ip::tcp::socket socket,
|
||||
std::string ip,
|
||||
boost::beast::flat_buffer buffer,
|
||||
boost::beast::http::request<boost::beast::http::string_body> request,
|
||||
util::TagDecoratorFactory const& tagDecoratorFactory,
|
||||
boost::asio::yield_context yield
|
||||
);
|
||||
|
||||
std::expected<std::unique_ptr<SslWsConnection>, Error>
|
||||
make_SslWsConnection(
|
||||
boost::asio::ip::tcp::socket socket,
|
||||
std::string ip,
|
||||
boost::beast::flat_buffer buffer,
|
||||
boost::beast::http::request<boost::beast::http::string_body> request,
|
||||
boost::asio::ssl::context& sslContext,
|
||||
util::TagDecoratorFactory const& tagDecoratorFactory,
|
||||
boost::asio::yield_context yield
|
||||
);
|
||||
|
||||
} // namespace web::ng::impl
|
||||
@@ -1,8 +1,15 @@
|
||||
add_library(clio_testing_common)
|
||||
|
||||
target_sources(
|
||||
clio_testing_common PRIVATE util/StringUtils.cpp util/TestHttpServer.cpp util/TestWsServer.cpp util/TestObject.cpp
|
||||
util/AssignRandomPort.cpp util/WithTimeout.cpp
|
||||
clio_testing_common
|
||||
PRIVATE util/AssignRandomPort.cpp
|
||||
util/CallWithTimeout.cpp
|
||||
util/StringUtils.cpp
|
||||
util/TestHttpClient.cpp
|
||||
util/TestHttpServer.cpp
|
||||
util/TestObject.cpp
|
||||
util/TestWebSocketClient.cpp
|
||||
util/TestWsServer.cpp
|
||||
)
|
||||
|
||||
include(deps/gtest)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user