Compare commits

...

42 Commits

Author SHA1 Message Date
Sergey Kuznetsov
3555b7d998 fix: gateway_balance discrepancy (#1839) (#1874)
Port of #1839 into 2.3.1.
Fixes #1832.

rippled code:


https://github.com/XRPLF/rippled/blob/develop/src/xrpld/rpc/handlers/GatewayBalances.cpp#L129
2025-02-04 17:10:12 +00:00
Sergey Kuznetsov
8a418bfe00 Merge branch 'release/2.3.1' into Port_gateway_balance_discrepancy 2025-02-04 16:39:18 +00:00
cyan317
dc1b146729 fix: gateway_balance discrepancy (#1839)
Fix https://github.com/XRPLF/clio/issues/1832

rippled code:

https://github.com/XRPLF/rippled/blob/develop/src/xrpld/rpc/handlers/GatewayBalances.cpp#L129
2025-02-04 16:36:09 +00:00
Sergey Kuznetsov
47c0a6a297 fix: Remove InvalidHotWallet Error from gateway_balances RPC handler … (#1873)
Port of #1830 into release/2.3.1.

Fixes #1825 by removing the check in the gateway_balances RPC handler
that returns the RpcInvalidHotWallet error code if one of the addresses
supplied in the request's `hotwallet` array does not have a trustline
with the `account` from the request.

As stated in the original ticket, this change fixes a discrepancy in
behavior between Clio and rippled, as rippled does not check for
trustline existence when handling gateway_balances RPCs
2025-02-04 15:37:49 +00:00
Sergey Kuznetsov
44df7bf966 Merge branch 'release/2.3.1' into Commits_for_2.3.1 2025-02-04 13:32:08 +00:00
Sergey Kuznetsov
8a5a984d51 chore: Update libxrpl to 2.3.1 (#1866) 2025-02-04 13:31:34 +00:00
nkramer44
9ca4c7afd3 fix: Remove InvalidHotWallet Error from gateway_balances RPC handler (#1830)
Fixes #1825 by removing the check in the gateway_balances RPC handler
that returns the RpcInvalidHotWallet error code if one of the addresses
supplied in the request's `hotwallet` array does not have a trustline
with the `account` from the request.

As stated in the original ticket, this change fixes a discrepancy in
behavior between Clio and rippled, as rippled does not check for
trustline existence when handling gateway_balances RPCs

Co-authored-by: Sergey Kuznetsov <skuznetsov@ripple.com>
2025-02-04 10:37:50 +00:00
Alex Kremer
40be25a68c fix: Add upper bound to limit 2024-12-11 23:51:44 +00:00
Alex Kremer
9fc8846f6a chore: Add relevant changes from develop (#1762) 2024-11-27 19:16:38 +00:00
Peter Chen
d001e35427 fix: authorized_credential elements in array not objects bug (#1744) (#1747)
fixes: #1743
2024-11-21 12:05:15 -05:00
Peter Chen
592af70f03 fix: Credential error message (#1738)
fixes #1737
2024-11-18 16:15:11 +00:00
Alex Kremer
33cf336964 feat: Upgrade to libxrpl 2.3.0-rc2 (#1736) 2024-11-18 16:13:59 +00:00
github-actions[bot]
16e07b90db style: clang-tidy auto fixes (#1735)
Fixes #1734. Please review and commit clang-tidy fixes.

Co-authored-by: kuznetsss <15742918+kuznetsss@users.noreply.github.com>
2024-11-18 16:13:47 +00:00
Peter Chen
39419c8b58 feat: Add Support Credentials for Clio (#1712)
Rippled PR: [here](https://github.com/XRPLF/rippled/pull/5103)
2024-11-18 16:13:09 +00:00
github-actions[bot]
e38658a0d6 style: clang-tidy auto fixes (#1730)
Fixes #1729. Please review and commit clang-tidy fixes.

Co-authored-by: kuznetsss <15742918+kuznetsss@users.noreply.github.com>
2024-11-18 16:12:40 +00:00
Shawn Xie
fb98a6a394 feat: Implement MPT changes (#1147)
Implements https://github.com/XRPLF/XRPL-Standards/tree/master/XLS-0033d-multi-purpose-tokens
2024-11-11 17:27:04 +00:00
dependabot[bot]
b8a8248c42 ci: Bump wandalen/wretry.action from 3.7.0 to 3.7.2 (#1723)
Bumps
[wandalen/wretry.action](https://github.com/wandalen/wretry.action) from
3.7.0 to 3.7.2.
<details>
<summary>Commits</summary>
<ul>
<li><a
href="8ceaefd717"><code>8ceaefd</code></a>
version 3.7.2</li>
<li><a
href="ce976ac9e7"><code>ce976ac</code></a>
version 3.7.1</li>
<li><a
href="7a8f8d4bf2"><code>7a8f8d4</code></a>
Merge pull request <a
href="https://redirect.github.com/wandalen/wretry.action/issues/174">#174</a>
from dmvict/master</li>
<li><a
href="2103bce855"><code>2103bce</code></a>
Fix action, add option <code>pre_retry_command</code> to call of
subaction</li>
<li>See full diff in <a
href="https://github.com/wandalen/wretry.action/compare/v3.7.0...v3.7.2">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=wandalen/wretry.action&package-manager=github_actions&previous-version=3.7.0&new-version=3.7.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Alex Kremer <akremer@ripple.com>
2024-11-11 17:27:04 +00:00
Alex Kremer
a092b7ae08 feat: Upgrade to libxrpl 2.3.0-rc1 (#1718)
Fixes #1717
2024-11-11 17:27:04 +00:00
github-actions[bot]
07438a2e02 style: clang-tidy auto fixes (#1720)
Fixes #1719. Please review and commit clang-tidy fixes.

Co-authored-by: kuznetsss <15742918+kuznetsss@users.noreply.github.com>
2024-11-11 17:27:04 +00:00
Alex Kremer
09aa688de4 feat: ETLng Registry (#1713)
For #1597
2024-11-11 17:27:03 +00:00
dependabot[bot]
a7bff26fd6 ci: Bump wandalen/wretry.action from 3.5.0 to 3.7.0 (#1714)
Bumps
[wandalen/wretry.action](https://github.com/wandalen/wretry.action) from
3.5.0 to 3.7.0.
<details>
<summary>Commits</summary>
<ul>
<li><a
href="f8754f7974"><code>f8754f7</code></a>
version 3.7.0</li>
<li><a
href="03db9837ed"><code>03db983</code></a>
Merge pull request <a
href="https://redirect.github.com/wandalen/wretry.action/issues/171">#171</a>
from dmvict/docker_readme</li>
<li><a
href="d80901cd5c"><code>d80901c</code></a>
Sync readme for new feature</li>
<li><a
href="e00d406ade"><code>e00d406</code></a>
version 3.6.0</li>
<li><a
href="e00deaa9ba"><code>e00deaa</code></a>
Merge pull request <a
href="https://redirect.github.com/wandalen/wretry.action/issues/170">#170</a>
from dmvict/pre_retry_action</li>
<li><a
href="8b50f3152e"><code>8b50f31</code></a>
Update action, add option <code>pre_retry_command</code> to run command
between retries</li>
<li><a
href="990f16983d"><code>990f169</code></a>
Merge pull request <a
href="https://redirect.github.com/wandalen/wretry.action/issues/167">#167</a>
from Vampire/add-typing</li>
<li><a
href="aeb34f4d13"><code>aeb34f4</code></a>
Add action typing</li>
<li>See full diff in <a
href="https://github.com/wandalen/wretry.action/compare/v3.5.0...v3.7.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=wandalen/wretry.action&package-manager=github_actions&previous-version=3.5.0&new-version=3.7.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Alex Kremer <akremer@ripple.com>
2024-11-11 17:27:03 +00:00
Peter Chen
081adf1cae fix: Support Delete NFT (#1695)
Fixes #1677
2024-11-11 17:27:03 +00:00
cyan317
ffc9deb0f8 fix: Add queue size limit for websocket (#1701)
For slow clients, we will disconnect with it if the message queue is too
long.

---------

Co-authored-by: Sergey Kuznetsov <skuznetsov@ripple.com>
2024-11-11 17:27:02 +00:00
github-actions[bot]
717a29ecdf style: clang-tidy auto fixes (#1711)
Fixes #1710.

---------

Co-authored-by: kuznetsss <15742918+kuznetsss@users.noreply.github.com>
Co-authored-by: Sergey Kuznetsov <skuznetsov@ripple.com>
2024-11-11 17:27:02 +00:00
Sergey Kuznetsov
e8db74456a ci: Fix nightly build (#1709)
Fixes #1703.
2024-11-11 17:27:02 +00:00
Sergey Kuznetsov
4947a83696 fix: Fix issues clang-tidy found (#1708)
Fixes #1706.
2024-11-11 17:27:01 +00:00
github-actions[bot]
164387cab0 style: clang-tidy auto fixes (#1705)
Fixes #1704. Please review and commit clang-tidy fixes.

Co-authored-by: kuznetsss <15742918+kuznetsss@users.noreply.github.com>
2024-11-11 17:27:01 +00:00
Sergey Kuznetsov
b8f1deb90f refactor: Coroutine based webserver (#1699)
Code of new coroutine-based web server. The new server is not connected
to Clio and not ready to use yet.
For #919.
2024-11-11 17:27:01 +00:00
Sergey Kuznetsov
5c77e59374 fix: Fix timer spurious calls (#1700)
Fixes #1634.
I also checked other timers and they don't have the issue.
2024-11-11 17:27:00 +00:00
Peter Chen
6d070132c7 fix: example config syntax (#1696) 2024-11-11 17:27:00 +00:00
cyan317
d2dda69448 fix: Remove log (#1694) 2024-11-11 17:27:00 +00:00
cyan317
e2aeaa0956 chore: Add counter for total messages waiting to be sent (#1691) 2024-11-11 17:27:00 +00:00
Sergey Kuznetsov
2951b4aaa0 style: Fix include (#1687)
Fixes #1686
2024-11-11 17:26:59 +00:00
cyan317
6c3c761dd1 chore: Remove unused static variables (#1683) 2024-11-11 17:26:59 +00:00
github-actions[bot]
527020680a style: clang-tidy auto fixes (#1685)
Fixes #1684. Please review and commit clang-tidy fixes.

Co-authored-by: kuznetsss <15742918+kuznetsss@users.noreply.github.com>
2024-11-11 17:26:59 +00:00
Alex Kremer
401448f771 style: Update code formatting (#1682)
For #1664
2024-11-11 17:26:58 +00:00
Alex Kremer
0f12a6d7f2 chore: Upgrade to llvm 19 tooling (#1681)
For #1664
2024-11-11 17:26:58 +00:00
Peter Chen
5c8fc939f2 fix: deletion script will not OOM (#1679)
fixes #1676 and #1678
2024-11-11 17:26:58 +00:00
github-actions[bot]
b1be848098 style: clang-tidy auto fixes (#1674)
Fixes #1673. Please review and commit clang-tidy fixes.

Co-authored-by: kuznetsss <15742918+kuznetsss@users.noreply.github.com>
2024-11-11 17:26:58 +00:00
cyan317
41aabbfcce feat: server info cache (#1671)
fix: #1181
2024-11-11 17:26:57 +00:00
dependabot[bot]
c00d25aa6b chore: Bump ytanikin/PRConventionalCommits from 1.2.0 to 1.3.0 (#1670)
Bumps
[ytanikin/PRConventionalCommits](https://github.com/ytanikin/prconventionalcommits)
from 1.2.0 to 1.3.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/ytanikin/prconventionalcommits/releases">ytanikin/PRConventionalCommits's
releases</a>.</em></p>
<blockquote>
<h2>1.3.0</h2>
<h2>What's Changed</h2>
<ul>
<li>fix: Set breaking changes regex by <a
href="https://github.com/alexangas"><code>@​alexangas</code></a> in <a
href="https://redirect.github.com/ytanikin/PRConventionalCommits/pull/24">ytanikin/PRConventionalCommits#24</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/alexangas"><code>@​alexangas</code></a>
made their first contribution in <a
href="https://redirect.github.com/ytanikin/PRConventionalCommits/pull/24">ytanikin/PRConventionalCommits#24</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/ytanikin/PRConventionalCommits/compare/1.2.0...1.3.0">https://github.com/ytanikin/PRConventionalCommits/compare/1.2.0...1.3.0</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="b628c5a234"><code>b628c5a</code></a>
test: enable &quot;breaking change&quot; test</li>
<li><a
href="e1b5683aa4"><code>e1b5683</code></a>
fix: Set breaking changes regex (<a
href="https://redirect.github.com/ytanikin/prconventionalcommits/issues/24">#24</a>)</li>
<li><a
href="92a7ab7dc6"><code>92a7ab7</code></a>
fix: upgrade dependencies (<a
href="https://redirect.github.com/ytanikin/prconventionalcommits/issues/26">#26</a>)</li>
<li><a
href="cc6cc0dddb"><code>cc6cc0d</code></a>
test: fix tests (<a
href="https://redirect.github.com/ytanikin/prconventionalcommits/issues/25">#25</a>)</li>
<li>See full diff in <a
href="https://github.com/ytanikin/prconventionalcommits/compare/1.2.0...1.3.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=ytanikin/PRConventionalCommits&package-manager=github_actions&previous-version=1.2.0&new-version=1.3.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-11 17:26:57 +00:00
Alex Kremer
8d5c588e35 chore: Apply commits for 2.3.0-b4 (#1725) 2024-11-11 14:37:31 +00:00
153 changed files with 11802 additions and 1289 deletions

View File

@@ -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.
-----------------------------------------------------------------------------

View File

@@ -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

View File

@@ -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

View File

@@ -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' }}

View File

@@ -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

View File

@@ -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: |

View File

@@ -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.

View File

@@ -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)

View File

@@ -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'
]

View File

@@ -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

View File

@@ -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,

View File

@@ -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
```

View File

@@ -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)

View File

@@ -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)

View File

@@ -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.

View File

@@ -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);

View File

@@ -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.
*

View File

@@ -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
{

View File

@@ -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.
*

View File

@@ -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.
*/

View File

@@ -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"(

View File

@@ -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");

View File

@@ -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

View File

@@ -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();
}

View File

@@ -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
View 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
View 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

View File

@@ -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())
);

View File

@@ -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);

View File

@@ -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
View 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
View 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

View 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
View 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

View File

@@ -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;

View File

@@ -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

View 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

View 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

View File

@@ -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."},

View File

@@ -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,

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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
*

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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}}},

View File

@@ -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;

View File

@@ -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>{},

View File

@@ -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

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
}
/**

View File

@@ -43,7 +43,6 @@
#include <memory>
#include <optional>
#include <string>
#include <unordered_set>
namespace rpc {

View File

@@ -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"))

View File

@@ -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>{}},
};

View 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

View 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

View File

@@ -19,6 +19,8 @@
#pragma once
#include <xrpl/basics/StringUtilities.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/tokens.h>
#include <cctype>

View File

@@ -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

View File

@@ -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

View 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

View 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

View File

@@ -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:

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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
View 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

View File

@@ -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>>;

View File

@@ -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)

View File

@@ -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();
}

View File

@@ -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_));
}

View File

@@ -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();

View File

@@ -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();
}

View File

@@ -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_));
}

View File

@@ -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

View File

@@ -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

View File

@@ -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:

View File

@@ -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);

View File

@@ -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();
}

View File

@@ -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
View 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
View 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

View 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
View 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
View 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
View 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
View 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

View File

@@ -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

View File

@@ -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

View 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

View 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

View 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

View 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

View File

@@ -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

View File

@@ -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

View 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

View 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

View File

@@ -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