mirror of
https://github.com/savoirfairelinux/opendht.git
synced 2026-11-08 12:28:42 +00:00
Compare commits
156
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4783c410bc | ||
|
|
83778d84fe | ||
|
|
b61ee620e4 | ||
|
|
0d120eb5d4 | ||
|
|
9b9056c0c8 | ||
|
|
bf8e3ea251 | ||
|
|
34eb86a119 | ||
|
|
75c789ceb2 | ||
|
|
da438d1bd2 | ||
|
|
44e67e1389 | ||
|
|
7f9126b3b0 | ||
|
|
574674d3a2 | ||
|
|
c60703bfc3 | ||
|
|
a0220827cd | ||
|
|
fb90211157 | ||
|
|
9501465293 | ||
|
|
95bd26c3be | ||
|
|
87463c2d6c | ||
|
|
baf413dd0d | ||
|
|
6bf01f3b6d | ||
|
|
852403fbeb | ||
|
|
5f6d5c769e | ||
|
|
42e4419b75 | ||
|
|
4e94d123b2 | ||
|
|
7f3f1b4ec4 | ||
|
|
6d462c0dbf | ||
|
|
0285acc766 | ||
|
|
0626e82f4a | ||
|
|
aa01f68cb5 | ||
|
|
af3fd618bf | ||
|
|
b96bdfbf04 | ||
|
|
60ca85f782 | ||
|
|
e4966aa457 | ||
|
|
aa9d23870b | ||
|
|
ac68db84eb | ||
|
|
bbbd9122af | ||
|
|
cf58d66282 | ||
|
|
2ddf1e5ec5 | ||
|
|
e48bfd578a | ||
|
|
1c3fc22c31 | ||
|
|
b6cf2fd246 | ||
|
|
91c3f62c0c | ||
|
|
2703cf129d | ||
|
|
51c2aadd4e | ||
|
|
8b20e912d6 | ||
|
|
b66ff83fe2 | ||
|
|
552813e1b9 | ||
|
|
601a8fbf80 | ||
|
|
26488d18a6 | ||
|
|
53c509c54f | ||
|
|
def6a28d96 | ||
|
|
dbd5953b17 | ||
|
|
2893823a77 | ||
|
|
3ec03ccc0b | ||
|
|
20fa751da0 | ||
|
|
0d572f0dab | ||
|
|
d9c401e1e2 | ||
|
|
dbdbe65a5d | ||
|
|
6f5bec3af6 | ||
|
|
d07a9e6583 | ||
|
|
fd43a329e7 | ||
|
|
93b2e8d5b9 | ||
|
|
d66713d97e | ||
|
|
7af9868c47 | ||
|
|
05a524b038 | ||
|
|
57ed00579e | ||
|
|
9ffccb3956 | ||
|
|
d99324ebd4 | ||
|
|
933f6cbca1 | ||
|
|
d5d7d6483f | ||
|
|
cc09a39ade | ||
|
|
621b44c8e8 | ||
|
|
e1c088f2e4 | ||
|
|
a6e0e441ac | ||
|
|
dcc2ba3492 | ||
|
|
ccf00d6b09 | ||
|
|
d8250574fc | ||
|
|
0d698c76e8 | ||
|
|
e4e3b4e73b | ||
|
|
b26d622f5d | ||
|
|
78694751bd | ||
|
|
f9b90c5c1c | ||
|
|
56b848bf47 | ||
|
|
dd875230c3 | ||
|
|
de1d428e63 | ||
|
|
55e3a6514f | ||
|
|
f4bbef210c | ||
|
|
2c1470cef6 | ||
|
|
adb6aa57e6 | ||
|
|
acd0abdb70 | ||
|
|
b9c8f5e0fd | ||
|
|
18fedf431f | ||
|
|
fc6edb4d20 | ||
|
|
d1093c77a3 | ||
|
|
b543f1e3cb | ||
|
|
be210e27da | ||
|
|
f7243fca37 | ||
|
|
1f6efd5736 | ||
|
|
54fbad3a7e | ||
|
|
1315489a7b | ||
|
|
c189fa0e82 | ||
|
|
3eae03289e | ||
|
|
e5e5a45b91 | ||
|
|
7a71beb892 | ||
|
|
74a894229f | ||
|
|
c81c1f70c8 | ||
|
|
3a383ac873 | ||
|
|
3149fe60c4 | ||
|
|
d5956e0778 | ||
|
|
8baf53d078 | ||
|
|
82ab21dc3f | ||
|
|
d676aef027 | ||
|
|
a2d9fdf334 | ||
|
|
0cec4135c0 | ||
|
|
6f76686a3e | ||
|
|
c211b3c376 | ||
|
|
0179b6ecc7 | ||
|
|
44e1ad839e | ||
|
|
dddd97daff | ||
|
|
632bec459e | ||
|
|
910ca10a5f | ||
|
|
0826822a4d | ||
|
|
8aa6b009c3 | ||
|
|
2caa21d65f | ||
|
|
cf64fa454c | ||
|
|
ef9ef12195 | ||
|
|
74a1918a4d | ||
|
|
56a60fb557 | ||
|
|
f98df1d615 | ||
|
|
8f779f2efb | ||
|
|
699073acd8 | ||
|
|
02d49b9f6a | ||
|
|
85b0bc52c8 | ||
|
|
af02c3a117 | ||
|
|
74b99453ce | ||
|
|
cba3d0e18f | ||
|
|
f9332985f5 | ||
|
|
513d0eb803 | ||
|
|
139852a531 | ||
|
|
3a73ad3aca | ||
|
|
9031cf06c4 | ||
|
|
7b1369931e | ||
|
|
64364602d7 | ||
|
|
2dbec3df97 | ||
|
|
507e2de3c4 | ||
|
|
c37abed715 | ||
|
|
0eb1c33ba5 | ||
|
|
45918186a2 | ||
|
|
29cb909228 | ||
|
|
9f6a03bf3d | ||
|
|
732da0e478 | ||
|
|
9a60329f66 | ||
|
|
0bb393ec36 | ||
|
|
947610003b | ||
|
|
dcbe158554 | ||
|
|
38e9a7fd61 |
No files matched your search
@@ -0,0 +1,12 @@
|
||||
.git
|
||||
.vscode
|
||||
.DS_Store
|
||||
build
|
||||
build-*
|
||||
build_*
|
||||
install
|
||||
vcpkg_installed
|
||||
rust/target
|
||||
**/__pycache__
|
||||
python/dist
|
||||
python/*.egg-info
|
||||
+10
-10
@@ -2,12 +2,12 @@ name: C/C++ CI
|
||||
|
||||
env:
|
||||
LLHTTP_VERSION: "v9.2.1"
|
||||
RESTINIO_VERSION: "0.7.8.1"
|
||||
RESTINIO_VERSION: "0.7.10"
|
||||
EXPECTED_LITE_URL: "https://raw.githubusercontent.com/martinmoene/expected-lite/master/include/nonstd/expected.hpp"
|
||||
UBUNTU_FULL_DEPS: "libncurses5-dev libreadline-dev nettle-dev libasio-dev libgnutls28-dev libuv1-dev python3-pip python3-dev python3-setuptools libcppunit-dev libjsoncpp-dev libfmt-dev libmsgpack-dev libargon2-dev"
|
||||
UBUNTU_MESON_DEPS: "ninja-build libncurses5-dev libreadline-dev nettle-dev libasio-dev libgnutls28-dev libuv1-dev python3-dev python3-setuptools python3-pip libcppunit-dev libjsoncpp-dev libfmt-dev libmsgpack-dev libargon2-dev"
|
||||
UBUNTU_MINIMAL_DEPS: "libncurses5-dev libreadline-dev nettle-dev libfmt-dev libgnutls28-dev libcppunit-dev libmsgpack-dev libargon2-dev"
|
||||
DEBIAN12_DEPS: "git libtool pkg-config wget cmake libncurses5-dev libreadline-dev nettle-dev libasio-dev libgnutls28-dev libuv1-dev python3-pip python3-dev python3-setuptools libcppunit-dev libjsoncpp-dev libfmt-dev libmsgpack-dev libargon2-dev"
|
||||
DEBIAN13_DEPS: "git libtool pkg-config wget cmake libncurses5-dev libreadline-dev nettle-dev libasio-dev libgnutls28-dev libuv1-dev python3-pip python3-dev python3-setuptools libcppunit-dev libjsoncpp-dev libfmt-dev libmsgpack-cxx-dev libargon2-dev"
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
name: Ubuntu/GCC Meson build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
matrix:
|
||||
cxx_standard: [17, 20, 23]
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Install Minimal Dependencies
|
||||
run: |
|
||||
@@ -65,12 +65,12 @@ jobs:
|
||||
- name: Run Unit tests
|
||||
run: ctest --output-on-failure --test-dir build
|
||||
|
||||
build-debian12:
|
||||
name: Debian 12/GCC build
|
||||
build-debian13:
|
||||
name: Debian 13/GCC build
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
container:
|
||||
image: debian:12
|
||||
image: debian:13
|
||||
volumes:
|
||||
- .:/workspace
|
||||
options: --workdir /workspace
|
||||
@@ -78,12 +78,12 @@ jobs:
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install -y $DEBIAN12_DEPS
|
||||
apt-get install -y $DEBIAN13_DEPS
|
||||
|
||||
- name: Build llhttp dependency
|
||||
run: |
|
||||
@@ -131,7 +131,7 @@ jobs:
|
||||
matrix:
|
||||
cxx_standard: [17, 20, 23]
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Install Homebrew Dependencies
|
||||
run: brew install msgpack-cxx asio gnutls nettle readline fmt jsoncpp argon2 openssl cppunit
|
||||
|
||||
@@ -6,7 +6,7 @@ jobs:
|
||||
name: Clang Static Analysis
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- name: deps
|
||||
run: |
|
||||
sudo apt update && \
|
||||
@@ -23,11 +23,13 @@ jobs:
|
||||
-O /usr/include/nonstd/expected.hpp
|
||||
|
||||
- name: restinio
|
||||
env:
|
||||
RESTINIO_VERSION: "0.7.10"
|
||||
run: |
|
||||
mkdir restinio && cd restinio
|
||||
wget https://github.com/Stiffstream/restinio/releases/download/v0.7.8.1/restinio-0.7.8.1.tar.bz2
|
||||
ls -l && tar -xjf restinio-0.7.8.1.tar.bz2
|
||||
cd restinio-0.7.8.1/dev
|
||||
wget "https://github.com/Stiffstream/restinio/releases/download/v${RESTINIO_VERSION}/restinio-${RESTINIO_VERSION}.tar.bz2"
|
||||
ls -l && tar -xjf "restinio-${RESTINIO_VERSION}.tar.bz2"
|
||||
cd "restinio-${RESTINIO_VERSION}/dev"
|
||||
cmake -DCMAKE_INSTALL_PREFIX=/usr -DRESTINIO_TEST=Off -DRESTINIO_SAMPLE=Off -DRESTINIO_BENCHMARK=Off \
|
||||
-DRESTINIO_WITH_SOBJECTIZER=Off -DRESTINIO_DEP_STANDALONE_ASIO=system -DRESTINIO_DEP_LLHTTP=system \
|
||||
-DRESTINIO_DEP_FMT=system -DRESTINIO_DEP_EXPECTED_LITE=system .
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v4
|
||||
|
||||
@@ -16,14 +16,14 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, ubuntu-24.04-arm, macos-latest]
|
||||
os: [ubuntu-latest, ubuntu-24.04-arm, macos-15]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
# Run locally with: pipx run cibuildwheel --platform linux python
|
||||
- name: Build wheels
|
||||
uses: pypa/cibuildwheel@v3.4.1
|
||||
uses: pypa/cibuildwheel@v4.3.4
|
||||
with:
|
||||
package-dir: python
|
||||
|
||||
@@ -36,36 +36,28 @@ jobs:
|
||||
name: Build wheels on Windows
|
||||
runs-on: windows-latest
|
||||
env:
|
||||
VCPKG_VERSION: "2025.07.25"
|
||||
VCPKG_INSTALLATION_ROOT: "C:\\vcpkg"
|
||||
VCPKG_DISABLE_METRICS: "1"
|
||||
VCPKG_DEFAULT_TRIPLET: x64-windows-release
|
||||
VCPKG_INSTALLED_DIR: ${{ github.workspace }}\vcpkg_installed
|
||||
VCPKG_DEFAULT_BINARY_CACHE: ${{ github.workspace }}\vcpkg_binary_cache
|
||||
VCPKG_BINARY_SOURCES: clear;files,${{ github.workspace }}/vcpkg-binary-cache,readwrite
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Cache vcpkg installed packages
|
||||
id: cache-vcpkg
|
||||
uses: actions/cache@v5
|
||||
- name: Set up vcpkg
|
||||
uses: lukka/run-vcpkg@v11
|
||||
with:
|
||||
path: |
|
||||
${{ env.VCPKG_INSTALLED_DIR }}
|
||||
${{ env.VCPKG_DEFAULT_BINARY_CACHE }}
|
||||
key: ${{ runner.os }}-vcpkg-${{ env.VCPKG_VERSION }}-${{ env.VCPKG_DEFAULT_TRIPLET }}-${{ hashFiles('**/vcpkg.json', '**/vcpkg-configuration.json') }}-${{ hashFiles('.github/workflows/windows-build.yml') }}
|
||||
vcpkgJsonGlob: vcpkg.json
|
||||
vcpkgConfigurationJsonGlob: vcpkg-configuration.json
|
||||
|
||||
- name: Setup vcpkg and install dependencies
|
||||
if: steps.cache-vcpkg.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
New-Item -Path "${{ env.VCPKG_INSTALLED_DIR }}" -ItemType Directory -Force
|
||||
New-Item -Path "${{ env.VCPKG_DEFAULT_BINARY_CACHE }}" -ItemType Directory -Force
|
||||
vcpkg install `
|
||||
--recurse `
|
||||
--clean-after-build `
|
||||
--triplet=${{ env.VCPKG_DEFAULT_TRIPLET }} `
|
||||
--x-install-root=${{ env.VCPKG_INSTALLED_DIR }}
|
||||
shell: pwsh
|
||||
- name: Cache vcpkg binary packages
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: ${{ github.workspace }}/vcpkg-binary-cache
|
||||
key: ${{ runner.os }}-vcpkg-binary-${{ env.VCPKG_DEFAULT_TRIPLET }}-${{ hashFiles('vcpkg.json', 'vcpkg-configuration.json', 'ports/**') }}-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-vcpkg-binary-${{ env.VCPKG_DEFAULT_TRIPLET }}-${{ hashFiles('vcpkg.json', 'vcpkg-configuration.json', 'ports/**') }}-
|
||||
${{ runner.os }}-vcpkg-binary-${{ env.VCPKG_DEFAULT_TRIPLET }}-
|
||||
|
||||
- name: Build wheels
|
||||
run: |
|
||||
@@ -73,7 +65,7 @@ jobs:
|
||||
cmake -S . -B build `
|
||||
-DCMAKE_BUILD_TYPE=Release `
|
||||
-DVCPKG_TARGET_TRIPLET=${{ env.VCPKG_DEFAULT_TRIPLET }} `
|
||||
-DCMAKE_TOOLCHAIN_FILE=${{ env.VCPKG_INSTALLATION_ROOT }}\scripts\buildsystems\vcpkg.cmake `
|
||||
"-DCMAKE_TOOLCHAIN_FILE=$env:VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake" `
|
||||
-DVCPKG_INSTALLED_DIR=${{ env.VCPKG_INSTALLED_DIR }} `
|
||||
-DBUILD_TESTING=OFF `
|
||||
-DOPENDHT_C=OFF `
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v4
|
||||
@@ -66,7 +66,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v4
|
||||
@@ -107,7 +107,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v4
|
||||
@@ -148,7 +148,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v4
|
||||
@@ -190,7 +190,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v4
|
||||
@@ -231,7 +231,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v4
|
||||
@@ -273,7 +273,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v4
|
||||
@@ -309,20 +309,24 @@ jobs:
|
||||
name: Build Windows MSI Installer
|
||||
runs-on: windows-latest
|
||||
env:
|
||||
VCPKG_VERSION: "2025.07.25"
|
||||
VCPKG_INSTALLATION_ROOT: "C:\\vcpkg"
|
||||
VCPKG_DISABLE_METRICS: "1"
|
||||
VCPKG_DEFAULT_TRIPLET: x64-windows
|
||||
VCPKG_INSTALLED_DIR: ${{ github.workspace }}\vcpkg_installed
|
||||
VCPKG_DEFAULT_BINARY_CACHE: ${{ github.workspace }}\vcpkg_binary_cache
|
||||
VCPKG_BINARY_SOURCES: clear;files,${{ github.workspace }}/vcpkg-binary-cache,readwrite
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Set up vcpkg
|
||||
uses: lukka/run-vcpkg@v11
|
||||
with:
|
||||
vcpkgJsonGlob: vcpkg.json
|
||||
vcpkgConfigurationJsonGlob: vcpkg-configuration.json
|
||||
|
||||
- name: Set up MSVC developer environment
|
||||
uses: microsoft/setup-msbuild@v3
|
||||
|
||||
@@ -332,27 +336,14 @@ jobs:
|
||||
wix extension add --global WixToolset.UI.wixext/6.0.1
|
||||
shell: pwsh
|
||||
|
||||
- name: Cache vcpkg installed packages
|
||||
id: cache-vcpkg
|
||||
uses: actions/cache@v5
|
||||
- name: Cache vcpkg binary packages
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: |
|
||||
${{ env.VCPKG_INSTALLED_DIR }}
|
||||
${{ env.VCPKG_DEFAULT_BINARY_CACHE }}
|
||||
key: ${{ runner.os }}-vcpkg-${{ env.VCPKG_VERSION }}-x64-shared-${{ hashFiles('**/vcpkg.json', '**/vcpkg-configuration.json') }}-${{ hashFiles('.github/workflows/windows-build.yml') }}
|
||||
|
||||
- name: Setup vcpkg and install dependencies
|
||||
if: steps.cache-vcpkg.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
New-Item -Path "${{ env.VCPKG_INSTALLED_DIR }}" -ItemType Directory -Force
|
||||
New-Item -Path "${{ env.VCPKG_DEFAULT_BINARY_CACHE }}" -ItemType Directory -Force
|
||||
vcpkg install `
|
||||
--recurse `
|
||||
--clean-after-build `
|
||||
--triplet=x64-windows `
|
||||
--x-install-root=${{ env.VCPKG_INSTALLED_DIR }} `
|
||||
--x-feature=test
|
||||
shell: pwsh
|
||||
path: ${{ github.workspace }}/vcpkg-binary-cache
|
||||
key: ${{ runner.os }}-vcpkg-binary-${{ env.VCPKG_DEFAULT_TRIPLET }}-${{ hashFiles('vcpkg.json', 'vcpkg-configuration.json', 'ports/**') }}-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-vcpkg-binary-${{ env.VCPKG_DEFAULT_TRIPLET }}-${{ hashFiles('vcpkg.json', 'vcpkg-configuration.json', 'ports/**') }}-
|
||||
${{ runner.os }}-vcpkg-binary-${{ env.VCPKG_DEFAULT_TRIPLET }}-
|
||||
|
||||
- name: Configure CMake for MSI
|
||||
shell: pwsh
|
||||
@@ -360,7 +351,7 @@ jobs:
|
||||
mkdir build -ErrorAction SilentlyContinue
|
||||
cmake -B build `
|
||||
-S . `
|
||||
-DCMAKE_TOOLCHAIN_FILE=${{ env.VCPKG_INSTALLATION_ROOT }}/scripts/buildsystems/vcpkg.cmake `
|
||||
"-DCMAKE_TOOLCHAIN_FILE=$env:VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake" `
|
||||
-DVCPKG_TARGET_TRIPLET=x64-windows `
|
||||
-DVCPKG_INSTALLED_DIR=${{ env.VCPKG_INSTALLED_DIR }} `
|
||||
-DVCPKG_MANIFEST_FEATURES=test `
|
||||
@@ -413,47 +404,35 @@ jobs:
|
||||
name: Build Windows dhtnode
|
||||
runs-on: windows-latest
|
||||
env:
|
||||
VCPKG_VERSION: "2025.07.25"
|
||||
VCPKG_INSTALLATION_ROOT: "C:\\vcpkg"
|
||||
VCPKG_DISABLE_METRICS: "1"
|
||||
VCPKG_DEFAULT_TRIPLET: x64-windows-static-release
|
||||
VCPKG_INSTALLED_DIR: ${{ github.workspace }}\vcpkg_installed
|
||||
VCPKG_DEFAULT_BINARY_CACHE: ${{ github.workspace }}\vcpkg_binary_cache
|
||||
VCPKG_BINARY_SOURCES: clear;files,${{ github.workspace }}/vcpkg-binary-cache,readwrite
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Set up vcpkg
|
||||
uses: lukka/run-vcpkg@v11
|
||||
with:
|
||||
vcpkgJsonGlob: vcpkg.json
|
||||
vcpkgConfigurationJsonGlob: vcpkg-configuration.json
|
||||
|
||||
- name: Set up MSVC developer environment
|
||||
uses: microsoft/setup-msbuild@v3
|
||||
|
||||
- name: Cache vcpkg static installed packages
|
||||
id: cache-vcpkg-static
|
||||
uses: actions/cache@v5
|
||||
- name: Cache vcpkg binary packages
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: |
|
||||
${{ env.VCPKG_INSTALLED_DIR }}
|
||||
${{ env.VCPKG_DEFAULT_BINARY_CACHE }}
|
||||
key: ${{ runner.os }}-vcpkg-${{ env.VCPKG_VERSION }}-x64-static-${{ hashFiles('**/vcpkg.json', '**/vcpkg-configuration.json') }}-${{ hashFiles('.github/workflows/windows-build.yml') }}
|
||||
path: ${{ github.workspace }}/vcpkg-binary-cache
|
||||
key: ${{ runner.os }}-vcpkg-binary-${{ env.VCPKG_DEFAULT_TRIPLET }}-${{ hashFiles('vcpkg.json', 'vcpkg-configuration.json', 'ports/**') }}-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-vcpkg-${{ env.VCPKG_VERSION }}-x64-static-${{ hashFiles('**/vcpkg.json', '**/vcpkg-configuration.json') }}
|
||||
${{ runner.os }}-vcpkg-${{ env.VCPKG_VERSION }}-x64-static-
|
||||
|
||||
- name: Setup vcpkg and install static dependencies
|
||||
if: steps.cache-vcpkg-static.outputs.cache-hit != 'true'
|
||||
shell: pwsh
|
||||
run: |
|
||||
New-Item -Path "${{ env.VCPKG_INSTALLED_DIR }}" -ItemType Directory -Force
|
||||
New-Item -Path "${{ env.VCPKG_DEFAULT_BINARY_CACHE }}" -ItemType Directory -Force
|
||||
vcpkg install `
|
||||
--recurse `
|
||||
--clean-after-build `
|
||||
--triplet=x64-windows-static-release `
|
||||
--x-install-root=${{ env.VCPKG_INSTALLED_DIR }} `
|
||||
--x-feature=test
|
||||
${{ runner.os }}-vcpkg-binary-${{ env.VCPKG_DEFAULT_TRIPLET }}-${{ hashFiles('vcpkg.json', 'vcpkg-configuration.json', 'ports/**') }}-
|
||||
${{ runner.os }}-vcpkg-binary-${{ env.VCPKG_DEFAULT_TRIPLET }}-
|
||||
|
||||
- name: Configure CMake for Static Build
|
||||
shell: pwsh
|
||||
@@ -461,7 +440,7 @@ jobs:
|
||||
mkdir build_static -ErrorAction SilentlyContinue
|
||||
cmake -B build_static `
|
||||
-S . `
|
||||
-DCMAKE_TOOLCHAIN_FILE=${{ env.VCPKG_INSTALLATION_ROOT }}/scripts/buildsystems/vcpkg.cmake `
|
||||
"-DCMAKE_TOOLCHAIN_FILE=$env:VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake" `
|
||||
-DVCPKG_TARGET_TRIPLET=x64-windows-static-release `
|
||||
-DVCPKG_INSTALLED_DIR=${{ env.VCPKG_INSTALLED_DIR }} `
|
||||
-DVCPKG_MANIFEST_FEATURES=test `
|
||||
@@ -499,7 +478,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Download Windows MSI artifact
|
||||
uses: actions/download-artifact@v8
|
||||
|
||||
@@ -9,113 +9,89 @@ on:
|
||||
branches: [ main, master ]
|
||||
|
||||
env:
|
||||
VCPKG_VERSION: "2026.01.16"
|
||||
VCPKG_INSTALLATION_ROOT: "C:\\vcpkg"
|
||||
VCPKG_DISABLE_METRICS: "1"
|
||||
VCPKG_INSTALLED_DIR: ${{ github.workspace }}\vcpkg_installed
|
||||
VCPKG_DEFAULT_BINARY_CACHE: ${{ github.workspace }}\vcpkg_binary_cache
|
||||
VCPKG_BINARY_SOURCES: clear;files,${{ github.workspace }}/vcpkg-binary-cache,readwrite
|
||||
|
||||
jobs:
|
||||
prepare-dependencies:
|
||||
name: Prepare Windows Dependencies
|
||||
runs-on: windows-latest
|
||||
outputs:
|
||||
vcpkg_cache_key: ${{ steps.generate_cache_key.outputs.key }}
|
||||
env:
|
||||
VCPKG_DEFAULT_TRIPLET: ${{ matrix.arch }}-windows
|
||||
strategy:
|
||||
matrix:
|
||||
arch: [x64]
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Set up MSVC
|
||||
uses: microsoft/setup-msbuild@v3
|
||||
|
||||
- name: Generate Cache Key
|
||||
id: generate_cache_key
|
||||
shell: pwsh
|
||||
run: |
|
||||
$key_value = "${{ runner.os }}-vcpkg-${{ env.VCPKG_VERSION }}-${{ matrix.arch }}-shared-${{ hashFiles('**/vcpkg.json', '**/vcpkg-configuration.json') }}-${{ hashFiles('.github/workflows/windows-build.yml') }}"
|
||||
echo "key=$key_value" >> $env:GITHUB_OUTPUT
|
||||
|
||||
- name: Cache vcpkg installed packages
|
||||
id: cache-vcpkg
|
||||
uses: actions/cache@v5
|
||||
- name: Cache vcpkg binary packages
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: |
|
||||
${{ env.VCPKG_INSTALLED_DIR }}
|
||||
${{ env.VCPKG_DEFAULT_BINARY_CACHE }}
|
||||
key: ${{ steps.generate_cache_key.outputs.key }}
|
||||
path: ${{ github.workspace }}/vcpkg-binary-cache
|
||||
key: ${{ runner.os }}-vcpkg-binary-${{ env.VCPKG_DEFAULT_TRIPLET }}-${{ hashFiles('vcpkg.json', 'vcpkg-configuration.json', 'ports/**') }}-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-vcpkg-${{ env.VCPKG_VERSION }}-${{ matrix.arch }}-shared-${{ hashFiles('**/vcpkg.json', '**/vcpkg-configuration.json') }}
|
||||
${{ runner.os }}-vcpkg-${{ env.VCPKG_VERSION }}-${{ matrix.arch }}-shared-
|
||||
${{ runner.os }}-vcpkg-binary-${{ env.VCPKG_DEFAULT_TRIPLET }}-${{ hashFiles('vcpkg.json', 'vcpkg-configuration.json', 'ports/**') }}-
|
||||
${{ runner.os }}-vcpkg-binary-${{ env.VCPKG_DEFAULT_TRIPLET }}-
|
||||
|
||||
- name: Setup vcpkg and install dependencies
|
||||
if: steps.cache-vcpkg.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
New-Item -Path "${{ env.VCPKG_INSTALLED_DIR }}" -ItemType Directory -Force
|
||||
New-Item -Path "${{ env.VCPKG_DEFAULT_BINARY_CACHE }}" -ItemType Directory -Force
|
||||
vcpkg install `
|
||||
--recurse `
|
||||
--clean-after-build `
|
||||
--triplet=${{ matrix.arch }}-windows `
|
||||
--x-install-root=${{ env.VCPKG_INSTALLED_DIR }} `
|
||||
--x-feature=test
|
||||
shell: pwsh
|
||||
- name: Set up vcpkg and install dependencies
|
||||
uses: lukka/run-vcpkg@v11
|
||||
with:
|
||||
vcpkgJsonGlob: vcpkg.json
|
||||
vcpkgConfigurationJsonGlob: vcpkg-configuration.json
|
||||
runVcpkgInstall: true
|
||||
runVcpkgFormatString: >-
|
||||
[`install`, `--clean-after-build`, `--x-feature=test`,
|
||||
`--triplet`, `$[env.VCPKG_DEFAULT_TRIPLET]`,
|
||||
`--x-install-root`, `$[env.VCPKG_INSTALLED_DIR]`]
|
||||
|
||||
prepare-static-dependencies:
|
||||
name: Prepare Windows Static Dependencies
|
||||
runs-on: windows-latest
|
||||
outputs:
|
||||
vcpkg_static_cache_key: ${{ steps.generate_static_cache_key.outputs.key }}
|
||||
env:
|
||||
VCPKG_DEFAULT_TRIPLET: ${{ matrix.arch }}-windows-static-release
|
||||
strategy:
|
||||
matrix:
|
||||
arch: [x64]
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Set up MSVC
|
||||
uses: microsoft/setup-msbuild@v3
|
||||
|
||||
- name: Generate Cache Key for Static Libs
|
||||
id: generate_static_cache_key
|
||||
shell: pwsh
|
||||
run: |
|
||||
$key_value = "${{ runner.os }}-vcpkg-${{ env.VCPKG_VERSION }}-${{ matrix.arch }}-static-${{ hashFiles('**/vcpkg.json', '**/vcpkg-configuration.json') }}-${{ hashFiles('.github/workflows/windows-build.yml') }}"
|
||||
echo "key=$key_value" >> $env:GITHUB_OUTPUT
|
||||
|
||||
- name: Cache vcpkg static installed packages
|
||||
id: cache-vcpkg-static
|
||||
uses: actions/cache@v5
|
||||
- name: Cache vcpkg binary packages
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: |
|
||||
${{ env.VCPKG_INSTALLED_DIR }}
|
||||
${{ env.VCPKG_DEFAULT_BINARY_CACHE }}
|
||||
key: ${{ steps.generate_static_cache_key.outputs.key }}
|
||||
path: ${{ github.workspace }}/vcpkg-binary-cache
|
||||
key: ${{ runner.os }}-vcpkg-binary-${{ env.VCPKG_DEFAULT_TRIPLET }}-${{ hashFiles('vcpkg.json', 'vcpkg-configuration.json', 'ports/**') }}-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-vcpkg-${{ env.VCPKG_VERSION }}-${{ matrix.arch }}-static-${{ hashFiles('**/vcpkg.json', '**/vcpkg-configuration.json') }}
|
||||
${{ runner.os }}-vcpkg-${{ env.VCPKG_VERSION }}-${{ matrix.arch }}-static-
|
||||
${{ runner.os }}-vcpkg-binary-${{ env.VCPKG_DEFAULT_TRIPLET }}-${{ hashFiles('vcpkg.json', 'vcpkg-configuration.json', 'ports/**') }}-
|
||||
${{ runner.os }}-vcpkg-binary-${{ env.VCPKG_DEFAULT_TRIPLET }}-
|
||||
|
||||
- name: Setup vcpkg and install static dependencies
|
||||
if: steps.cache-vcpkg-static.outputs.cache-hit != 'true'
|
||||
shell: pwsh
|
||||
run: |
|
||||
New-Item -Path "${{ env.VCPKG_INSTALLED_DIR }}" -ItemType Directory -Force
|
||||
New-Item -Path "${{ env.VCPKG_DEFAULT_BINARY_CACHE }}" -ItemType Directory -Force
|
||||
vcpkg install `
|
||||
--recurse `
|
||||
--clean-after-build `
|
||||
--triplet=${{ matrix.arch }}-windows-static-release `
|
||||
--x-install-root=${{ env.VCPKG_INSTALLED_DIR }} `
|
||||
--x-feature=test
|
||||
- name: Set up vcpkg and install dependencies
|
||||
uses: lukka/run-vcpkg@v11
|
||||
with:
|
||||
vcpkgJsonGlob: vcpkg.json
|
||||
vcpkgConfigurationJsonGlob: vcpkg-configuration.json
|
||||
runVcpkgInstall: true
|
||||
runVcpkgFormatString: >-
|
||||
[`install`, `--clean-after-build`, `--x-feature=test`,
|
||||
`--triplet`, `$[env.VCPKG_DEFAULT_TRIPLET]`,
|
||||
`--x-install-root`, `$[env.VCPKG_INSTALLED_DIR]`]
|
||||
|
||||
build-windows:
|
||||
name: Windows MSVC Build
|
||||
runs-on: windows-latest
|
||||
needs: prepare-dependencies
|
||||
env:
|
||||
VCPKG_DEFAULT_TRIPLET: ${{ matrix.arch }}-windows
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -125,22 +101,25 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Set up vcpkg
|
||||
uses: lukka/run-vcpkg@v11
|
||||
with:
|
||||
vcpkgJsonGlob: vcpkg.json
|
||||
vcpkgConfigurationJsonGlob: vcpkg-configuration.json
|
||||
|
||||
- name: Set up MSVC
|
||||
uses: microsoft/setup-msbuild@v3
|
||||
|
||||
- name: Restore vcpkg installed packages
|
||||
id: cache-vcpkg-restore
|
||||
uses: actions/cache/restore@v5
|
||||
- name: Restore vcpkg binary packages
|
||||
uses: actions/cache/restore@v6
|
||||
with:
|
||||
path: |
|
||||
${{ env.VCPKG_INSTALLED_DIR }}
|
||||
${{ env.VCPKG_DEFAULT_BINARY_CACHE }}
|
||||
key: ${{ needs.prepare-dependencies.outputs.vcpkg_cache_key }}
|
||||
path: ${{ github.workspace }}/vcpkg-binary-cache
|
||||
key: ${{ runner.os }}-vcpkg-binary-${{ env.VCPKG_DEFAULT_TRIPLET }}-${{ hashFiles('vcpkg.json', 'vcpkg-configuration.json', 'ports/**') }}-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-vcpkg-${{ env.VCPKG_VERSION }}-${{ matrix.arch }}-shared-${{ hashFiles('**/vcpkg.json', '**/vcpkg-configuration.json') }}
|
||||
${{ runner.os }}-vcpkg-${{ env.VCPKG_VERSION }}-${{ matrix.arch }}-shared-
|
||||
${{ runner.os }}-vcpkg-binary-${{ env.VCPKG_DEFAULT_TRIPLET }}-${{ hashFiles('vcpkg.json', 'vcpkg-configuration.json', 'ports/**') }}-
|
||||
${{ runner.os }}-vcpkg-binary-${{ env.VCPKG_DEFAULT_TRIPLET }}-
|
||||
|
||||
- name: Configure CMake
|
||||
shell: pwsh
|
||||
@@ -148,7 +127,7 @@ jobs:
|
||||
mkdir build -ErrorAction SilentlyContinue
|
||||
cmake -B build `
|
||||
-S . `
|
||||
-DCMAKE_TOOLCHAIN_FILE=${{ env.VCPKG_INSTALLATION_ROOT }}/scripts/buildsystems/vcpkg.cmake `
|
||||
"-DCMAKE_TOOLCHAIN_FILE=$env:VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake" `
|
||||
-DVCPKG_TARGET_TRIPLET=${{ matrix.arch }}-windows `
|
||||
-DVCPKG_INSTALLED_DIR=${{ env.VCPKG_INSTALLED_DIR }} `
|
||||
-DVCPKG_MANIFEST_FEATURES=test `
|
||||
@@ -194,6 +173,8 @@ jobs:
|
||||
name: Windows Static MSVC Build
|
||||
runs-on: windows-latest
|
||||
needs: prepare-static-dependencies
|
||||
env:
|
||||
VCPKG_DEFAULT_TRIPLET: ${{ matrix.arch }}-windows-static-release
|
||||
strategy:
|
||||
matrix:
|
||||
build_type: [Release]
|
||||
@@ -202,22 +183,25 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Set up vcpkg
|
||||
uses: lukka/run-vcpkg@v11
|
||||
with:
|
||||
vcpkgJsonGlob: vcpkg.json
|
||||
vcpkgConfigurationJsonGlob: vcpkg-configuration.json
|
||||
|
||||
- name: Set up MSVC
|
||||
uses: microsoft/setup-msbuild@v3
|
||||
|
||||
- name: Restore vcpkg static installed packages
|
||||
id: cache-vcpkg-static-restore
|
||||
uses: actions/cache/restore@v5
|
||||
- name: Restore vcpkg binary packages
|
||||
uses: actions/cache/restore@v6
|
||||
with:
|
||||
path: |
|
||||
${{ env.VCPKG_INSTALLED_DIR }}
|
||||
${{ env.VCPKG_DEFAULT_BINARY_CACHE }}
|
||||
key: ${{ needs.prepare-static-dependencies.outputs.vcpkg_static_cache_key }}
|
||||
path: ${{ github.workspace }}/vcpkg-binary-cache
|
||||
key: ${{ runner.os }}-vcpkg-binary-${{ env.VCPKG_DEFAULT_TRIPLET }}-${{ hashFiles('vcpkg.json', 'vcpkg-configuration.json', 'ports/**') }}-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-vcpkg-${{ env.VCPKG_VERSION }}-${{ matrix.arch }}-static-${{ hashFiles('**/vcpkg.json', '**/vcpkg-configuration.json') }}
|
||||
${{ runner.os }}-vcpkg-${{ env.VCPKG_VERSION }}-${{ matrix.arch }}-static-
|
||||
${{ runner.os }}-vcpkg-binary-${{ env.VCPKG_DEFAULT_TRIPLET }}-${{ hashFiles('vcpkg.json', 'vcpkg-configuration.json', 'ports/**') }}-
|
||||
${{ runner.os }}-vcpkg-binary-${{ env.VCPKG_DEFAULT_TRIPLET }}-
|
||||
|
||||
- name: Configure CMake for Static Build
|
||||
shell: pwsh
|
||||
@@ -225,7 +209,7 @@ jobs:
|
||||
mkdir build -ErrorAction SilentlyContinue
|
||||
cmake -B build `
|
||||
-S . `
|
||||
-DCMAKE_TOOLCHAIN_FILE=${{ env.VCPKG_INSTALLATION_ROOT }}/scripts/buildsystems/vcpkg.cmake `
|
||||
"-DCMAKE_TOOLCHAIN_FILE=$env:VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake" `
|
||||
-DVCPKG_TARGET_TRIPLET=${{ matrix.arch }}-windows-static-release `
|
||||
-DVCPKG_INSTALLED_DIR=${{ env.VCPKG_INSTALLED_DIR }} `
|
||||
-DVCPKG_MANIFEST_FEATURES=test `
|
||||
@@ -275,7 +259,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Setup MinGW
|
||||
uses: msys2/setup-msys2@v2
|
||||
@@ -304,7 +288,7 @@ jobs:
|
||||
shell: msys2 {0}
|
||||
env:
|
||||
LLHTTP_VERSION: "v9.2.1"
|
||||
RESTINIO_VERSION: "0.7.6"
|
||||
RESTINIO_VERSION: "0.7.10"
|
||||
EXPECTED_LITE_URL: "https://raw.githubusercontent.com/martinmoene/expected-lite/master/include/nonstd/expected.hpp"
|
||||
run: |
|
||||
MSYS2_SYSROOT=$(cygpath -u "$MSYSTEM_PREFIX")
|
||||
|
||||
@@ -115,16 +115,47 @@ cmake -DOPENDHT_PYTHON=ON \
|
||||
| `OPENDHT_PUSH_NOTIFICATIONS` | OFF | Push notification support |
|
||||
| `OPENDHT_SANITIZE` | OFF | AddressSanitizer + stack protector |
|
||||
|
||||
## C and Rust Bindings
|
||||
|
||||
Enable `OPENDHT_C=ON` to build the C ABI. With `BUILD_TESTING=ON`, CTest runs
|
||||
`test_c`; Meson registers the same test as `CBindings` when `c` and `tests` are
|
||||
enabled. Rust regression and callback-lifetime tests run with Cargo; see
|
||||
[the Rust binding guide](rust/README.md) for build and library-path settings.
|
||||
|
||||
C handles returned by constructors, imports, and reference functions are owned
|
||||
and require their matching delete/unref function. Data views, string getters,
|
||||
query fields, and callback arguments are borrowed: copy them or take a reference
|
||||
before returning from the callback. `dht_sockaddr_array_delete` releases both the
|
||||
public-address array and its elements.
|
||||
|
||||
Operation completion callbacks run once, including failure when pending work is
|
||||
discarded on runner teardown. Listener release callbacks run after the native
|
||||
registration is destroyed; deleting an operation token alone does not cancel its
|
||||
listener. Cancel first, then delete the consumed token. Startup-context release
|
||||
callbacks run after all native status/store callback captures are released, also
|
||||
on startup failure. Certificate-store callbacks must invoke the supplied emitter
|
||||
synchronously with borrowed certificates. Query-result views are callback-scoped.
|
||||
|
||||
`dht_runner_loop` drives non-threaded nodes. `dht_runner_join` waits for callbacks
|
||||
and allows restart; never call it from a DHT callback. For persisted shutdown,
|
||||
wait for the shutdown callback before joining. Push tokens can be polled without
|
||||
blocking and must be deleted separately. `dht_push_token_poll` returns `EAGAIN`
|
||||
while pending, 0 with the native outcome when ready, and an error code otherwise;
|
||||
consume a successful result only once. Native push outcomes are 0 through 3 for
|
||||
refresh/value events, 128 through 131 for ignored events, and 132 for errors.
|
||||
|
||||
## Windows/MSVC
|
||||
|
||||
Building on Windows with MSVC is supported with VCPKG and CMake.
|
||||
|
||||
### Build with VCPKG in manifest mode
|
||||
|
||||
1. Install [VCPKG](https://github.com/microsoft/vcpkg) and required dependencies:
|
||||
1. Install [VCPKG](https://github.com/microsoft/vcpkg) 2026.10.16 and required dependencies:
|
||||
```ps1
|
||||
git clone https://github.com/microsoft/vcpkg.git
|
||||
cd vcpkg; .\bootstrap-vcpkg.bat
|
||||
cd vcpkg
|
||||
git checkout 9eec002df1cd109f8f104cd966a763daf9751489
|
||||
.\bootstrap-vcpkg.bat
|
||||
```
|
||||
|
||||
2. Use CMake preset for vcpkg:
|
||||
@@ -133,3 +164,19 @@ cd opendht
|
||||
cmake --preset=vcpkg_manifest .
|
||||
cmake --build .
|
||||
```
|
||||
|
||||
### CI
|
||||
|
||||
Windows CI uses [lukka/run-vcpkg](https://github.com/lukka/run-vcpkg) to acquire
|
||||
and bootstrap the revision pinned by `default-registry.baseline` in
|
||||
`vcpkg-configuration.json`. CMake uses the action's `VCPKG_ROOT` and installs
|
||||
manifest dependencies on every run. Dependency-preparation jobs warm the binary
|
||||
cache before the Windows build matrix starts.
|
||||
|
||||
Only ABI-keyed binary package archives are cached, not installed trees. The
|
||||
supported [files provider](https://learn.microsoft.com/en-us/vcpkg/reference/binarycaching#files-provider)
|
||||
is persisted with `actions/cache`; it does not use the removed `x-gha` provider
|
||||
or require package-feed credentials. Cache keys include the target triplet,
|
||||
manifests, overlay ports, run ID, and attempt, with restore prefixes for reuse.
|
||||
Each run can save newly built archives without overwriting an immutable cache;
|
||||
vcpkg validates package ABIs before reuse.
|
||||
+18
-2
@@ -1,4 +1,4 @@
|
||||
cmake_minimum_required (VERSION 3.21...3.31)
|
||||
cmake_minimum_required (VERSION 3.21...4.4)
|
||||
if(POLICY CMP0091)
|
||||
cmake_policy(SET CMP0091 NEW)
|
||||
endif()
|
||||
@@ -10,7 +10,7 @@ if(POLICY CMP0167)
|
||||
endif()
|
||||
|
||||
set (opendht_VERSION_MAJOR 4)
|
||||
set (opendht_VERSION_MINOR 0.0)
|
||||
set (opendht_VERSION_MINOR 4.2)
|
||||
set (opendht_VERSION ${opendht_VERSION_MAJOR}.${opendht_VERSION_MINOR})
|
||||
set (PACKAGE_VERSION ${opendht_VERSION})
|
||||
set (VERSION "${opendht_VERSION}")
|
||||
@@ -845,6 +845,12 @@ endif()
|
||||
|
||||
# Unit tests
|
||||
if (BUILD_TESTING)
|
||||
if (OPENDHT_C)
|
||||
add_executable(test_c tests/test_c.c)
|
||||
target_include_directories(test_c PRIVATE c include)
|
||||
target_link_libraries(test_c PRIVATE opendht-c)
|
||||
add_test(NAME test_c COMMAND test_c)
|
||||
endif()
|
||||
if (NOT MSVC)
|
||||
pkg_search_module(Cppunit REQUIRED IMPORTED_TARGET cppunit)
|
||||
set(cppunit_LIBRARIES PkgConfig::Cppunit)
|
||||
@@ -870,7 +876,17 @@ if (BUILD_TESTING)
|
||||
tests/test_parsedmessage.cpp
|
||||
tests/test_networkengine.h
|
||||
tests/test_networkengine.cpp
|
||||
tests/test_opcache.h
|
||||
tests/test_opcache.cpp
|
||||
tests/test_valuecache.h
|
||||
tests/test_valuecache.cpp
|
||||
)
|
||||
if (OPENDHT_INDEX)
|
||||
list (APPEND test_FILES
|
||||
tests/test_pht.h
|
||||
tests/test_pht.cpp
|
||||
)
|
||||
endif()
|
||||
if (OPENDHT_PROXY_CLIENT)
|
||||
list (APPEND test_FILES
|
||||
tests/test_dhtproxy_client.h
|
||||
|
||||
+568
-99
@@ -5,12 +5,30 @@
|
||||
|
||||
#include <opendht.h>
|
||||
#include <opendht/log.h>
|
||||
#include <atomic>
|
||||
|
||||
using ValueSp = std::shared_ptr<dht::Value>;
|
||||
using PrivkeySp = std::shared_ptr<dht::crypto::PrivateKey>;
|
||||
using PubkeySp = std::shared_ptr<const dht::crypto::PublicKey>;
|
||||
using CertSp = std::shared_ptr<dht::crypto::Certificate>;
|
||||
|
||||
struct CompletionGuard
|
||||
{
|
||||
CompletionGuard(dht_done_cb callback, void* data)
|
||||
: callback(callback)
|
||||
, data(data)
|
||||
{}
|
||||
~CompletionGuard() { finish(false); }
|
||||
void finish(bool ok)
|
||||
{
|
||||
if (!finished.exchange(true) && callback)
|
||||
callback(ok, data);
|
||||
}
|
||||
dht_done_cb callback;
|
||||
void* data;
|
||||
std::atomic_bool finished {false};
|
||||
};
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
@@ -156,6 +174,47 @@ dht_value_set_user_type(dht_value* data, const char* user_type)
|
||||
(*reinterpret_cast<ValueSp*>(data))->user_type = user_type;
|
||||
}
|
||||
|
||||
uint16_t
|
||||
dht_value_get_type(const dht_value* value)
|
||||
{
|
||||
return (*reinterpret_cast<const ValueSp*>(value))->type;
|
||||
}
|
||||
void
|
||||
dht_value_set_type(dht_value* value, uint16_t type)
|
||||
{
|
||||
(*reinterpret_cast<ValueSp*>(value))->type = type;
|
||||
}
|
||||
uint16_t
|
||||
dht_value_get_seq(const dht_value* value)
|
||||
{
|
||||
return (*reinterpret_cast<const ValueSp*>(value))->seq;
|
||||
}
|
||||
void
|
||||
dht_value_set_seq(dht_value* value, uint16_t seq)
|
||||
{
|
||||
(*reinterpret_cast<ValueSp*>(value))->seq = seq;
|
||||
}
|
||||
unsigned
|
||||
dht_value_get_priority(const dht_value* value)
|
||||
{
|
||||
return (*reinterpret_cast<const ValueSp*>(value))->priority;
|
||||
}
|
||||
void
|
||||
dht_value_set_priority(dht_value* value, unsigned priority)
|
||||
{
|
||||
(*reinterpret_cast<ValueSp*>(value))->priority = priority;
|
||||
}
|
||||
const char*
|
||||
dht_value_get_push_type(const dht_value* value)
|
||||
{
|
||||
return (*reinterpret_cast<const ValueSp*>(value))->pushType.c_str();
|
||||
}
|
||||
void
|
||||
dht_value_set_push_type(dht_value* value, const char* push_type)
|
||||
{
|
||||
(*reinterpret_cast<ValueSp*>(value))->pushType = push_type;
|
||||
}
|
||||
|
||||
dht_value*
|
||||
dht_value_with_id_new(const uint8_t* data, size_t size, uint64_t value_id)
|
||||
{
|
||||
@@ -183,6 +242,29 @@ dht_value_ref(const dht_value* v)
|
||||
return reinterpret_cast<dht_value*>(new ValueSp(*reinterpret_cast<const ValueSp*>(v)));
|
||||
}
|
||||
|
||||
dht_value*
|
||||
dht_value_copy(const dht_value* value)
|
||||
{
|
||||
try {
|
||||
const auto& source = **reinterpret_cast<const ValueSp*>(value);
|
||||
auto copy = std::make_shared<dht::Value>();
|
||||
copy->id = source.id;
|
||||
copy->owner = source.owner;
|
||||
copy->recipient = source.recipient;
|
||||
copy->type = source.type;
|
||||
copy->data = source.data;
|
||||
copy->user_type = source.user_type;
|
||||
copy->seq = source.seq;
|
||||
copy->signature = source.signature;
|
||||
copy->cypher = source.cypher;
|
||||
copy->priority = source.priority;
|
||||
copy->pushType = source.pushType;
|
||||
return reinterpret_cast<dht_value*>(new ValueSp(std::move(copy)));
|
||||
} catch (...) {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
dht_value_unref(dht_value* v)
|
||||
{
|
||||
@@ -196,7 +278,7 @@ dht_publickey_import(const uint8_t* dat, size_t dat_size)
|
||||
try {
|
||||
return reinterpret_cast<dht_publickey*>(
|
||||
new PubkeySp(std::make_shared<const dht::crypto::PublicKey>(dat, dat_size)));
|
||||
} catch (const dht::crypto::CryptoException& e) {
|
||||
} catch (...) {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
@@ -214,6 +296,29 @@ dht_publickey_export(const dht_publickey* pk, char* out, size_t* outlen)
|
||||
return pkey->pack((uint8_t*) out, outlen);
|
||||
}
|
||||
|
||||
dht_blob*
|
||||
dht_publickey_export_blob(const dht_publickey* pk)
|
||||
{
|
||||
try {
|
||||
auto data = std::make_unique<dht::Blob>();
|
||||
(*reinterpret_cast<const PubkeySp*>(pk))->pack(*data);
|
||||
return reinterpret_cast<dht_blob*>(data.release());
|
||||
} catch (...) {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
int
|
||||
dht_publickey_unpack(dht_publickey* pk, const uint8_t* data, size_t size)
|
||||
{
|
||||
try {
|
||||
*reinterpret_cast<PubkeySp*>(pk) = std::make_shared<const dht::crypto::PublicKey>(data, size);
|
||||
return 0;
|
||||
} catch (...) {
|
||||
return EINVAL;
|
||||
}
|
||||
}
|
||||
|
||||
dht_infohash
|
||||
dht_publickey_get_id(const dht_publickey* pk)
|
||||
{
|
||||
@@ -255,8 +360,12 @@ dht_privatekey_generate(unsigned key_length_bits)
|
||||
{
|
||||
if (key_length_bits == 0)
|
||||
key_length_bits = 4096;
|
||||
return reinterpret_cast<dht_privatekey*>(
|
||||
new PrivkeySp(std::make_shared<dht::crypto::PrivateKey>(dht::crypto::PrivateKey::generate(key_length_bits))));
|
||||
try {
|
||||
return reinterpret_cast<dht_privatekey*>(new PrivkeySp(
|
||||
std::make_shared<dht::crypto::PrivateKey>(dht::crypto::PrivateKey::generate(key_length_bits))));
|
||||
} catch (...) {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
dht_privatekey*
|
||||
@@ -265,7 +374,7 @@ dht_privatekey_import(const uint8_t* dat, size_t dat_size, const char* password)
|
||||
try {
|
||||
return reinterpret_cast<dht_privatekey*>(
|
||||
new PrivkeySp(std::make_shared<dht::crypto::PrivateKey>(dat, dat_size, password)));
|
||||
} catch (const dht::crypto::CryptoException& e) {
|
||||
} catch (...) {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
@@ -276,7 +385,43 @@ dht_privatekey_export(const dht_privatekey* k, char* out, size_t* out_size, cons
|
||||
if (!out or !out_size or !*out_size)
|
||||
return -1;
|
||||
const auto& key = *reinterpret_cast<const PrivkeySp*>(k);
|
||||
return key->serialize((uint8_t*) out, out_size, password);
|
||||
try {
|
||||
return key->serialize((uint8_t*) out, out_size, password ? password : "");
|
||||
} catch (...) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
dht_privatekey*
|
||||
dht_privatekey_generate_ec(void)
|
||||
{
|
||||
try {
|
||||
return reinterpret_cast<dht_privatekey*>(
|
||||
new PrivkeySp(std::make_shared<dht::crypto::PrivateKey>(dht::crypto::PrivateKey::generateEC())));
|
||||
} catch (...) {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
dht_blob*
|
||||
dht_privatekey_export_blob(const dht_privatekey* key, const char* password)
|
||||
{
|
||||
try {
|
||||
return reinterpret_cast<dht_blob*>(
|
||||
new dht::Blob((*reinterpret_cast<const PrivkeySp*>(key))->serialize(password ? password : "")));
|
||||
} catch (...) {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
dht_blob*
|
||||
dht_privatekey_sign(const dht_privatekey* key, const uint8_t* data, size_t size)
|
||||
{
|
||||
try {
|
||||
return reinterpret_cast<dht_blob*>(new dht::Blob((*reinterpret_cast<const PrivkeySp*>(key))->sign(data, size)));
|
||||
} catch (...) {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
dht_publickey*
|
||||
@@ -311,11 +456,28 @@ dht_certificate_import(const uint8_t* dat, size_t dat_size)
|
||||
{
|
||||
try {
|
||||
return reinterpret_cast<dht_certificate*>(new CertSp(std::make_shared<dht::crypto::Certificate>(dat, dat_size)));
|
||||
} catch (const dht::crypto::CryptoException& e) {
|
||||
} catch (...) {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
dht_blob*
|
||||
dht_certificate_export(const dht_certificate* certificate)
|
||||
{
|
||||
try {
|
||||
return reinterpret_cast<dht_blob*>(new dht::Blob((*reinterpret_cast<const CertSp*>(certificate))->getPacked()));
|
||||
} catch (...) {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
dht_certificate*
|
||||
dht_certificate_ref(const dht_certificate* certificate)
|
||||
{
|
||||
return certificate ? reinterpret_cast<dht_certificate*>(new CertSp(*reinterpret_cast<const CertSp*>(certificate)))
|
||||
: nullptr;
|
||||
}
|
||||
|
||||
void
|
||||
dht_certificate_delete(dht_certificate* c)
|
||||
{
|
||||
@@ -364,8 +526,10 @@ inline dht_identity
|
||||
dht_identity_to_c(const dht::crypto::Identity& id)
|
||||
{
|
||||
dht_identity cid {};
|
||||
cid.privatekey = id.first ? reinterpret_cast<dht_privatekey*>(new PrivkeySp(id.first)) : NULL;
|
||||
cid.certificate = id.second ? reinterpret_cast<dht_certificate*>(new CertSp(id.second)) : NULL;
|
||||
auto key = id.first ? std::make_unique<PrivkeySp>(id.first) : nullptr;
|
||||
auto certificate = id.second ? std::make_unique<CertSp>(id.second) : nullptr;
|
||||
cid.privatekey = reinterpret_cast<dht_privatekey*>(key.release());
|
||||
cid.certificate = reinterpret_cast<dht_certificate*>(certificate.release());
|
||||
return cid;
|
||||
}
|
||||
|
||||
@@ -376,7 +540,12 @@ extern "C" {
|
||||
OPENDHT_C_PUBLIC dht_identity
|
||||
dht_identity_generate(const char* common_name, const dht_identity* ca)
|
||||
{
|
||||
return dht_identity_to_c(dht::crypto::generateIdentity(common_name, dht_identity_from_c(ca)));
|
||||
try {
|
||||
return dht_identity_to_c(
|
||||
dht::crypto::generateIdentity(common_name ? common_name : "dhtnode", dht_identity_from_c(ca)));
|
||||
} catch (...) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
OPENDHT_C_PUBLIC void
|
||||
@@ -392,6 +561,17 @@ dht_identity_delete(dht_identity* id)
|
||||
}
|
||||
}
|
||||
|
||||
dht_identity
|
||||
dht_identity_generate_ec(const char* common_name, const dht_identity* ca)
|
||||
{
|
||||
try {
|
||||
return dht_identity_to_c(
|
||||
dht::crypto::generateEcIdentity(common_name ? common_name : "dhtnode", dht_identity_from_c(ca)));
|
||||
} catch (...) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
// config
|
||||
void
|
||||
dht_runner_config_default(dht_runner_config* config)
|
||||
@@ -413,6 +593,28 @@ dht_runner_delete(dht_runner* runner)
|
||||
delete reinterpret_cast<dht::DhtRunner*>(runner);
|
||||
}
|
||||
|
||||
int
|
||||
dht_runner_loop(dht_runner* runner)
|
||||
{
|
||||
try {
|
||||
reinterpret_cast<dht::DhtRunner*>(runner)->loop();
|
||||
return 0;
|
||||
} catch (...) {
|
||||
return EINVAL;
|
||||
}
|
||||
}
|
||||
|
||||
int
|
||||
dht_runner_join(dht_runner* runner)
|
||||
{
|
||||
try {
|
||||
reinterpret_cast<dht::DhtRunner*>(runner)->join();
|
||||
return 0;
|
||||
} catch (...) {
|
||||
return EDEADLK;
|
||||
}
|
||||
}
|
||||
|
||||
int
|
||||
dht_runner_run(dht_runner* r, in_port_t port)
|
||||
{
|
||||
@@ -424,61 +626,163 @@ dht_runner_run(dht_runner* r, in_port_t port)
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static dht::DhtRunner::Config
|
||||
config_from_c(const dht_runner_config* conf)
|
||||
{
|
||||
dht::DhtRunner::Config config;
|
||||
config.dht_config.node_config.is_bootstrap = conf->dht_config.node_config.is_bootstrap;
|
||||
config.dht_config.node_config.maintain_storage = conf->dht_config.node_config.maintain_storage;
|
||||
config.dht_config.node_config.node_id = *reinterpret_cast<const dht::InfoHash*>(
|
||||
&conf->dht_config.node_config.node_id);
|
||||
config.dht_config.node_config.network = conf->dht_config.node_config.network;
|
||||
config.dht_config.node_config.persist_path = conf->dht_config.node_config.persist_path
|
||||
? std::string(conf->dht_config.node_config.persist_path)
|
||||
: std::string {};
|
||||
|
||||
if (conf->dht_config.id.privatekey)
|
||||
config.dht_config.id.first = *reinterpret_cast<const PrivkeySp*>(conf->dht_config.id.privatekey);
|
||||
|
||||
if (conf->dht_config.id.certificate)
|
||||
config.dht_config.id.second = *reinterpret_cast<const CertSp*>(conf->dht_config.id.certificate);
|
||||
|
||||
config.threaded = conf->threaded;
|
||||
config.proxy_server = conf->proxy_server ? std::string(conf->proxy_server) : std::string {};
|
||||
config.push_node_id = conf->push_node_id ? std::string(conf->push_node_id) : std::string {};
|
||||
config.push_token = conf->push_token ? std::string(conf->push_token) : std::string {};
|
||||
config.push_topic = conf->push_topic ? std::string(conf->push_topic) : std::string {};
|
||||
config.push_platform = conf->push_platform ? std::string(conf->push_platform) : std::string {};
|
||||
config.proxy_user_agent = conf->proxy_user_agent ? std::string(conf->proxy_user_agent) : std::string {};
|
||||
config.peer_discovery = conf->peer_discovery;
|
||||
config.peer_publish = conf->peer_publish;
|
||||
if (conf->server_ca)
|
||||
config.server_ca = *reinterpret_cast<const CertSp*>(conf->server_ca);
|
||||
config.client_identity = dht_identity_from_c(&conf->client_identity);
|
||||
return config;
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
|
||||
int
|
||||
dht_runner_run_config(dht_runner* r, in_port_t port, const dht_runner_config* conf)
|
||||
dht_runner_run_config(dht_runner* runner, in_port_t port, const dht_runner_config* config)
|
||||
{
|
||||
return dht_runner_run_bind(runner, nullptr, nullptr, port, config, nullptr);
|
||||
}
|
||||
|
||||
int
|
||||
dht_runner_run_bind(dht_runner* runner,
|
||||
const char* ipv4,
|
||||
const char* ipv6,
|
||||
in_port_t port,
|
||||
const dht_runner_config* conf,
|
||||
const dht_runner_context* callbacks)
|
||||
{
|
||||
auto runner = reinterpret_cast<dht::DhtRunner*>(r);
|
||||
try {
|
||||
dht::DhtRunner::Config config;
|
||||
config.dht_config.node_config.is_bootstrap = conf->dht_config.node_config.is_bootstrap;
|
||||
config.dht_config.node_config.maintain_storage = conf->dht_config.node_config.maintain_storage;
|
||||
config.dht_config.node_config.node_id = *reinterpret_cast<const dht::InfoHash*>(
|
||||
&conf->dht_config.node_config.node_id);
|
||||
config.dht_config.node_config.network = conf->dht_config.node_config.network;
|
||||
config.dht_config.node_config.persist_path = conf->dht_config.node_config.persist_path
|
||||
? std::string(conf->dht_config.node_config.persist_path)
|
||||
: std::string {};
|
||||
|
||||
if (conf->dht_config.id.privatekey)
|
||||
config.dht_config.id.first = *reinterpret_cast<const PrivkeySp*>(conf->dht_config.id.privatekey);
|
||||
|
||||
if (conf->dht_config.id.certificate)
|
||||
config.dht_config.id.second = *reinterpret_cast<const CertSp*>(conf->dht_config.id.certificate);
|
||||
|
||||
config.threaded = conf->threaded;
|
||||
config.proxy_server = conf->proxy_server ? std::string(conf->proxy_server) : std::string {};
|
||||
config.push_node_id = conf->push_node_id ? std::string(conf->push_node_id) : std::string {};
|
||||
config.push_token = conf->push_token ? std::string(conf->push_token) : std::string {};
|
||||
config.push_topic = conf->push_topic ? std::string(conf->push_topic) : std::string {};
|
||||
config.push_platform = conf->push_platform ? std::string(conf->push_platform) : std::string {};
|
||||
config.proxy_user_agent = conf->proxy_user_agent ? std::string(conf->proxy_user_agent) : std::string {};
|
||||
config.peer_discovery = conf->peer_discovery;
|
||||
config.peer_publish = conf->peer_publish;
|
||||
if (conf->server_ca)
|
||||
config.server_ca = *reinterpret_cast<const CertSp*>(conf->server_ca);
|
||||
config.client_identity = dht_identity_from_c(&conf->client_identity);
|
||||
|
||||
auto lifetime = callbacks ? std::shared_ptr<void>(callbacks->user_data,
|
||||
[release = callbacks->released_cb](void* data) {
|
||||
if (release)
|
||||
release(data);
|
||||
})
|
||||
: std::shared_ptr<void> {};
|
||||
auto config = config_from_c(conf);
|
||||
dht::DhtRunner::Context context;
|
||||
if (conf->log) {
|
||||
context.logger = dht::log::getStdLogger();
|
||||
}
|
||||
runner->run(port, config, std::move(context));
|
||||
if (callbacks) {
|
||||
if (callbacks->status_cb) {
|
||||
context.statusChangedCallback = [lifetime, cb = callbacks->status_cb](dht::NodeStatus ipv4,
|
||||
dht::NodeStatus ipv6) {
|
||||
cb(static_cast<int>(ipv4), static_cast<int>(ipv6), lifetime.get());
|
||||
};
|
||||
}
|
||||
if (callbacks->certificate_store_cb) {
|
||||
context.certificateStore = [lifetime, cb = callbacks->certificate_store_cb](const dht::PkId& id) {
|
||||
std::vector<std::shared_ptr<dht::crypto::Certificate>> found;
|
||||
auto emit = [](const dht_certificate* certificate, void* data) {
|
||||
if (certificate)
|
||||
static_cast<decltype(found)*>(data)->push_back(
|
||||
*reinterpret_cast<const CertSp*>(certificate));
|
||||
};
|
||||
auto hash = dht_pkid_to_c(id);
|
||||
cb(&hash, emit, &found, lifetime.get());
|
||||
return found;
|
||||
};
|
||||
}
|
||||
}
|
||||
auto service = std::to_string(port);
|
||||
reinterpret_cast<dht::DhtRunner*>(runner)
|
||||
->run(ipv4 ? ipv4 : "0.0.0.0", ipv6 ? ipv6 : "::", service.c_str(), config, std::move(context));
|
||||
} catch (...) {
|
||||
return ENOTCONN;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
dht_runner_connectivity_changed(dht_runner* runner)
|
||||
{
|
||||
try {
|
||||
reinterpret_cast<dht::DhtRunner*>(runner)->connectivityChanged();
|
||||
return 0;
|
||||
} catch (...) {
|
||||
return EINVAL;
|
||||
}
|
||||
}
|
||||
|
||||
int
|
||||
dht_runner_register_type(dht_runner* runner, uint16_t id, const char* name, uint64_t expiration_seconds, bool insecure)
|
||||
{
|
||||
if (!dht_runner_is_running(runner) || !name
|
||||
|| expiration_seconds
|
||||
> static_cast<uint64_t>(std::chrono::duration_cast<std::chrono::seconds>(dht::duration::max()).count()))
|
||||
return EINVAL;
|
||||
try {
|
||||
dht::ValueType type(id, name, std::chrono::seconds(expiration_seconds));
|
||||
if (insecure)
|
||||
reinterpret_cast<dht::DhtRunner*>(runner)->registerInsecureType(type);
|
||||
else
|
||||
reinterpret_cast<dht::DhtRunner*>(runner)->registerType(type);
|
||||
return 0;
|
||||
} catch (...) {
|
||||
return EINVAL;
|
||||
}
|
||||
}
|
||||
|
||||
dht_publickey*
|
||||
dht_runner_get_publickey(const dht_runner* runner)
|
||||
{
|
||||
try {
|
||||
auto key = reinterpret_cast<const dht::DhtRunner*>(runner)->getPublicKey();
|
||||
return key ? reinterpret_cast<dht_publickey*>(new PubkeySp(std::move(key))) : nullptr;
|
||||
} catch (...) {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
int
|
||||
dht_runner_register_certificate(dht_runner* runner, const dht_certificate* certificate)
|
||||
{
|
||||
if (!dht_runner_is_running(runner))
|
||||
return ENOTCONN;
|
||||
try {
|
||||
reinterpret_cast<dht::DhtRunner*>(runner)->registerCertificate(*reinterpret_cast<const CertSp*>(certificate));
|
||||
return 0;
|
||||
} catch (...) {
|
||||
return EINVAL;
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
dht_runner_ping(dht_runner* r, struct sockaddr* addr, socklen_t addr_len, dht_done_cb done_cb, void* cb_user_data)
|
||||
{
|
||||
auto runner = reinterpret_cast<dht::DhtRunner*>(r);
|
||||
if (done_cb) {
|
||||
runner->bootstrap(dht::SockAddr(addr, addr_len),
|
||||
[done_cb, cb_user_data](bool ok) { done_cb(ok, cb_user_data); });
|
||||
} else {
|
||||
runner->bootstrap(dht::SockAddr(addr, addr_len));
|
||||
auto guard = std::make_shared<CompletionGuard>(done_cb, cb_user_data);
|
||||
try {
|
||||
runner->bootstrap(dht::SockAddr(addr, addr_len), [guard](bool ok) { guard->finish(ok); });
|
||||
} catch (...) {
|
||||
guard->finish(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -494,18 +798,29 @@ dht_runner_bootstrap(dht_runner* r, const char* host, const char* service)
|
||||
|
||||
void
|
||||
dht_runner_get(dht_runner* r, const dht_infohash* h, dht_get_cb cb, dht_done_cb done_cb, void* cb_user_data)
|
||||
{
|
||||
dht_runner_get_filtered(r, h, nullptr, cb, done_cb, cb_user_data);
|
||||
}
|
||||
|
||||
void
|
||||
dht_runner_get_filtered(
|
||||
dht_runner* r, const dht_infohash* h, const char* where, dht_get_cb cb, dht_done_cb done_cb, void* cb_user_data)
|
||||
{
|
||||
auto runner = reinterpret_cast<dht::DhtRunner*>(r);
|
||||
auto hash = reinterpret_cast<const dht::InfoHash*>(h);
|
||||
runner->get(
|
||||
*hash,
|
||||
[cb, cb_user_data](std::shared_ptr<dht::Value> value) {
|
||||
return cb(reinterpret_cast<const dht_value*>(&value), cb_user_data);
|
||||
},
|
||||
[done_cb, cb_user_data](bool ok) {
|
||||
if (done_cb)
|
||||
done_cb(ok, cb_user_data);
|
||||
});
|
||||
auto guard = std::make_shared<CompletionGuard>(done_cb, cb_user_data);
|
||||
try {
|
||||
runner->get(
|
||||
*hash,
|
||||
[cb, cb_user_data, guard](std::shared_ptr<dht::Value> value) {
|
||||
return cb ? cb(reinterpret_cast<const dht_value*>(&value), cb_user_data) : true;
|
||||
},
|
||||
[guard](bool ok) { guard->finish(ok); },
|
||||
{},
|
||||
where ? dht::Where(where) : dht::Where {});
|
||||
} catch (...) {
|
||||
guard->finish(false);
|
||||
}
|
||||
}
|
||||
|
||||
struct ScopeGuardCb
|
||||
@@ -528,23 +843,38 @@ private:
|
||||
|
||||
dht_op_token*
|
||||
dht_runner_listen(dht_runner* r, const dht_infohash* h, dht_value_cb cb, dht_shutdown_cb done_cb, void* cb_user_data)
|
||||
{
|
||||
return dht_runner_listen_filtered(r, h, nullptr, cb, done_cb, cb_user_data);
|
||||
}
|
||||
|
||||
dht_op_token*
|
||||
dht_runner_listen_filtered(dht_runner* r,
|
||||
const dht_infohash* h,
|
||||
const char* where,
|
||||
dht_value_cb cb,
|
||||
dht_shutdown_cb done_cb,
|
||||
void* cb_user_data)
|
||||
{
|
||||
auto runner = reinterpret_cast<dht::DhtRunner*>(r);
|
||||
auto hash = reinterpret_cast<const dht::InfoHash*>(h);
|
||||
auto fret = new std::future<size_t>;
|
||||
*fret = runner->listen(*hash,
|
||||
[cb,
|
||||
cb_user_data,
|
||||
guard = done_cb ? std::make_shared<ScopeGuardCb>(done_cb, cb_user_data)
|
||||
: std::shared_ptr<ScopeGuardCb> {}](
|
||||
const std::vector<std::shared_ptr<dht::Value>>& values, bool expired) {
|
||||
for (const auto& value : values) {
|
||||
if (not cb(reinterpret_cast<const dht_value*>(&value), expired, cb_user_data))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
return (dht_op_token*) fret;
|
||||
auto guard = std::make_shared<ScopeGuardCb>(done_cb, cb_user_data);
|
||||
try {
|
||||
auto fret = std::make_unique<std::future<size_t>>();
|
||||
*fret = runner->listen(
|
||||
*hash,
|
||||
[cb, cb_user_data, guard](const std::vector<std::shared_ptr<dht::Value>>& values, bool expired) {
|
||||
for (const auto& value : values) {
|
||||
if (cb && not cb(reinterpret_cast<const dht_value*>(&value), expired, cb_user_data))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{},
|
||||
where ? dht::Where(where) : dht::Where {});
|
||||
return reinterpret_cast<dht_op_token*>(fret.release());
|
||||
} catch (...) {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
@@ -562,6 +892,73 @@ dht_op_token_delete(dht_op_token* token)
|
||||
delete reinterpret_cast<std::future<size_t>*>(token);
|
||||
}
|
||||
|
||||
void
|
||||
dht_runner_query(
|
||||
dht_runner* r, const dht_infohash* h, const char* query, dht_query_cb cb, dht_done_cb done_cb, void* user_data)
|
||||
{
|
||||
auto guard = std::make_shared<CompletionGuard>(done_cb, user_data);
|
||||
try {
|
||||
reinterpret_cast<dht::DhtRunner*>(r)->query(
|
||||
*reinterpret_cast<const dht::InfoHash*>(h),
|
||||
[guard, cb, user_data](const std::vector<std::shared_ptr<dht::FieldValueIndex>>& results) {
|
||||
for (const auto& index : results) {
|
||||
dht_query_result result {};
|
||||
dht::Blob user_type;
|
||||
for (const auto& entry : index->index) {
|
||||
switch (entry.first) {
|
||||
case dht::Value::Field::Id:
|
||||
result.fields |= DHT_FIELD_ID;
|
||||
result.id = entry.second.getInt();
|
||||
break;
|
||||
case dht::Value::Field::ValueType:
|
||||
result.fields |= DHT_FIELD_TYPE;
|
||||
result.value_type = entry.second.getInt();
|
||||
break;
|
||||
case dht::Value::Field::SeqNum:
|
||||
result.fields |= DHT_FIELD_SEQ;
|
||||
result.seq = entry.second.getInt();
|
||||
break;
|
||||
case dht::Value::Field::OwnerPk:
|
||||
result.fields |= DHT_FIELD_OWNER;
|
||||
result.owner = dht_infohash_to_c(entry.second.getHash());
|
||||
break;
|
||||
case dht::Value::Field::UserType:
|
||||
result.fields |= DHT_FIELD_USER_TYPE;
|
||||
user_type = entry.second.getBlob();
|
||||
result.user_type = {user_type.data(), user_type.size()};
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (cb && !cb(&result, user_data))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
[guard](bool ok) { guard->finish(ok); },
|
||||
dht::Query(query ? query : ""));
|
||||
} catch (...) {
|
||||
guard->finish(false);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
dht_runner_find_certificate(
|
||||
dht_runner* runner, const dht_pkid* id, dht_certificate_cb callback, dht_shutdown_cb released_cb, void* user_data)
|
||||
{
|
||||
auto guard = std::make_shared<ScopeGuardCb>(released_cb, user_data);
|
||||
try {
|
||||
reinterpret_cast<dht::DhtRunner*>(runner)->findCertificate(
|
||||
*reinterpret_cast<const dht::PkId*>(id),
|
||||
[guard, callback, user_data](const std::shared_ptr<dht::crypto::Certificate>& certificate) {
|
||||
if (callback)
|
||||
callback(certificate ? reinterpret_cast<const dht_certificate*>(&certificate) : nullptr, user_data);
|
||||
});
|
||||
} catch (...) {
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
dht_runner_put(
|
||||
dht_runner* r, const dht_infohash* h, const dht_value* v, dht_done_cb done_cb, void* cb_user_data, bool permanent)
|
||||
@@ -569,15 +966,12 @@ dht_runner_put(
|
||||
auto runner = reinterpret_cast<dht::DhtRunner*>(r);
|
||||
auto hash = reinterpret_cast<const dht::InfoHash*>(h);
|
||||
auto value = reinterpret_cast<const ValueSp*>(v);
|
||||
runner->put(
|
||||
*hash,
|
||||
*value,
|
||||
[done_cb, cb_user_data](bool ok) {
|
||||
if (done_cb)
|
||||
done_cb(ok, cb_user_data);
|
||||
},
|
||||
dht::time_point::max(),
|
||||
permanent);
|
||||
auto guard = std::make_shared<CompletionGuard>(done_cb, cb_user_data);
|
||||
try {
|
||||
runner->put(*hash, *value, [guard](bool ok) { guard->finish(ok); }, dht::time_point::max(), permanent);
|
||||
} catch (...) {
|
||||
guard->finish(false);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
@@ -587,14 +981,12 @@ dht_runner_put_signed(
|
||||
auto runner = reinterpret_cast<dht::DhtRunner*>(r);
|
||||
auto hash = reinterpret_cast<const dht::InfoHash*>(h);
|
||||
auto value = reinterpret_cast<const ValueSp*>(v);
|
||||
runner->putSigned(
|
||||
*hash,
|
||||
*value,
|
||||
[done_cb, cb_user_data](bool ok) {
|
||||
if (done_cb)
|
||||
done_cb(ok, cb_user_data);
|
||||
},
|
||||
permanent);
|
||||
auto guard = std::make_shared<CompletionGuard>(done_cb, cb_user_data);
|
||||
try {
|
||||
runner->putSigned(*hash, *value, [guard](bool ok) { guard->finish(ok); }, permanent);
|
||||
} catch (...) {
|
||||
guard->finish(false);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
@@ -610,15 +1002,12 @@ dht_runner_put_encrypted(dht_runner* r,
|
||||
auto hash = reinterpret_cast<const dht::InfoHash*>(h);
|
||||
auto toHash = reinterpret_cast<const dht::PkId*>(to);
|
||||
auto value = reinterpret_cast<const ValueSp*>(v);
|
||||
runner->putEncrypted(
|
||||
*hash,
|
||||
*toHash,
|
||||
*value,
|
||||
[done_cb, cb_user_data](bool ok) {
|
||||
if (done_cb)
|
||||
done_cb(ok, cb_user_data);
|
||||
},
|
||||
permanent);
|
||||
auto guard = std::make_shared<CompletionGuard>(done_cb, cb_user_data);
|
||||
try {
|
||||
runner->putEncrypted(*hash, *toHash, *value, [guard](bool ok) { guard->finish(ok); }, permanent);
|
||||
} catch (...) {
|
||||
guard->finish(false);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
@@ -633,10 +1022,8 @@ void
|
||||
dht_runner_shutdown(dht_runner* r, dht_shutdown_cb done_cb, void* cb_user_data)
|
||||
{
|
||||
auto runner = reinterpret_cast<dht::DhtRunner*>(r);
|
||||
runner->shutdown([done_cb, cb_user_data]() {
|
||||
if (done_cb)
|
||||
done_cb(cb_user_data);
|
||||
});
|
||||
auto guard = std::make_shared<ScopeGuardCb>(done_cb, cb_user_data);
|
||||
runner->shutdown([guard]() {});
|
||||
}
|
||||
|
||||
bool
|
||||
@@ -681,12 +1068,94 @@ dht_runner_get_public_address(const dht_runner* r)
|
||||
if (addrs.empty())
|
||||
return nullptr;
|
||||
auto ret = (struct sockaddr**) malloc(sizeof(struct sockaddr*) * (addrs.size() + 1));
|
||||
if (!ret)
|
||||
return nullptr;
|
||||
for (size_t i = 0; i < addrs.size(); i++)
|
||||
ret[i] = addrs[i].release();
|
||||
ret[addrs.size()] = nullptr;
|
||||
return ret;
|
||||
}
|
||||
|
||||
void
|
||||
dht_sockaddr_array_delete(struct sockaddr** addresses)
|
||||
{
|
||||
if (addresses) {
|
||||
for (auto current = addresses; *current; ++current)
|
||||
free(*current);
|
||||
free(addresses);
|
||||
}
|
||||
}
|
||||
|
||||
int
|
||||
dht_runner_set_push_notification_token(dht_runner* runner, const char* value)
|
||||
{
|
||||
try {
|
||||
reinterpret_cast<dht::DhtRunner*>(runner)->setPushNotificationToken(value ? value : "");
|
||||
return 0;
|
||||
} catch (...) {
|
||||
return EINVAL;
|
||||
}
|
||||
}
|
||||
int
|
||||
dht_runner_set_push_notification_topic(dht_runner* runner, const char* value)
|
||||
{
|
||||
try {
|
||||
reinterpret_cast<dht::DhtRunner*>(runner)->setPushNotificationTopic(value ? value : "");
|
||||
return 0;
|
||||
} catch (...) {
|
||||
return EINVAL;
|
||||
}
|
||||
}
|
||||
int
|
||||
dht_runner_set_push_notification_platform(dht_runner* runner, const char* value)
|
||||
{
|
||||
try {
|
||||
reinterpret_cast<dht::DhtRunner*>(runner)->setPushNotificationPlatform(value ? value : "");
|
||||
return 0;
|
||||
} catch (...) {
|
||||
return EINVAL;
|
||||
}
|
||||
}
|
||||
|
||||
dht_push_token*
|
||||
dht_runner_push_notification_received(dht_runner* runner,
|
||||
const char* const* keys,
|
||||
const char* const* values,
|
||||
size_t count)
|
||||
{
|
||||
try {
|
||||
std::map<std::string, std::string> data;
|
||||
for (size_t index = 0; index < count; ++index)
|
||||
data.emplace(keys[index], values[index]);
|
||||
return reinterpret_cast<dht_push_token*>(new std::future<dht::PushNotificationResult>(
|
||||
reinterpret_cast<dht::DhtRunner*>(runner)->pushNotificationReceived(data)));
|
||||
} catch (...) {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
int
|
||||
dht_push_token_poll(dht_push_token* token, int* result)
|
||||
{
|
||||
if (!token || !result)
|
||||
return EINVAL;
|
||||
try {
|
||||
auto future = reinterpret_cast<std::future<dht::PushNotificationResult>*>(token);
|
||||
if (future->wait_for(std::chrono::seconds(0)) != std::future_status::ready)
|
||||
return EAGAIN;
|
||||
*result = static_cast<int>(future->get());
|
||||
return 0;
|
||||
} catch (...) {
|
||||
return EIO;
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
dht_push_token_delete(dht_push_token* token)
|
||||
{
|
||||
delete reinterpret_cast<std::future<dht::PushNotificationResult>*>(token);
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -67,6 +67,8 @@ typedef struct dht_publickey dht_publickey;
|
||||
OPENDHT_C_PUBLIC dht_publickey* dht_publickey_import(const uint8_t* dat, size_t dat_size);
|
||||
OPENDHT_C_PUBLIC void dht_publickey_delete(dht_publickey* pk);
|
||||
OPENDHT_C_PUBLIC int dht_publickey_export(const dht_publickey* pk, char* out, size_t* out_size);
|
||||
OPENDHT_C_PUBLIC dht_blob* dht_publickey_export_blob(const dht_publickey* pk);
|
||||
OPENDHT_C_PUBLIC int dht_publickey_unpack(dht_publickey* pk, const uint8_t* data, size_t size);
|
||||
OPENDHT_C_PUBLIC dht_infohash dht_publickey_get_id(const dht_publickey* pk);
|
||||
OPENDHT_C_PUBLIC dht_pkid dht_publickey_get_long_id(const dht_publickey* pk);
|
||||
OPENDHT_C_PUBLIC bool dht_publickey_check_signature(
|
||||
@@ -77,8 +79,11 @@ OPENDHT_C_PUBLIC dht_blob* dht_publickey_encrypt(const dht_publickey* pk, const
|
||||
struct OPENDHT_C_PUBLIC dht_privatekey;
|
||||
typedef struct dht_privatekey dht_privatekey;
|
||||
OPENDHT_C_PUBLIC dht_privatekey* dht_privatekey_generate(unsigned key_length_bits);
|
||||
OPENDHT_C_PUBLIC dht_privatekey* dht_privatekey_generate_ec(void);
|
||||
OPENDHT_C_PUBLIC dht_privatekey* dht_privatekey_import(const uint8_t* dat, size_t dat_size, const char* password);
|
||||
OPENDHT_C_PUBLIC int dht_privatekey_export(const dht_privatekey*, char* out, size_t* out_size, const char* password);
|
||||
OPENDHT_C_PUBLIC dht_blob* dht_privatekey_export_blob(const dht_privatekey*, const char* password);
|
||||
OPENDHT_C_PUBLIC dht_blob* dht_privatekey_sign(const dht_privatekey*, const uint8_t* data, size_t size);
|
||||
OPENDHT_C_PUBLIC dht_publickey* dht_privatekey_get_publickey(const dht_privatekey*);
|
||||
OPENDHT_C_PUBLIC dht_blob* dht_privatekey_decrypt(const dht_privatekey*, const char* data, size_t data_size);
|
||||
OPENDHT_C_PUBLIC void dht_privatekey_delete(dht_privatekey*);
|
||||
@@ -87,6 +92,8 @@ OPENDHT_C_PUBLIC void dht_privatekey_delete(dht_privatekey*);
|
||||
struct OPENDHT_C_PUBLIC dht_certificate;
|
||||
typedef struct dht_certificate dht_certificate;
|
||||
OPENDHT_C_PUBLIC dht_certificate* dht_certificate_import(const uint8_t* dat, size_t dat_size);
|
||||
OPENDHT_C_PUBLIC dht_blob* dht_certificate_export(const dht_certificate* certificate);
|
||||
OPENDHT_C_PUBLIC dht_certificate* dht_certificate_ref(const dht_certificate* certificate);
|
||||
OPENDHT_C_PUBLIC dht_infohash dht_certificate_get_id(const dht_certificate*);
|
||||
OPENDHT_C_PUBLIC dht_pkid dht_certificate_get_long_id(const dht_certificate*);
|
||||
OPENDHT_C_PUBLIC dht_publickey* dht_certificate_get_publickey(const dht_certificate*);
|
||||
@@ -99,6 +106,7 @@ struct OPENDHT_C_PUBLIC dht_identity
|
||||
};
|
||||
typedef struct dht_identity dht_identity;
|
||||
OPENDHT_C_PUBLIC dht_identity dht_identity_generate(const char* common_name, const dht_identity* ca);
|
||||
OPENDHT_C_PUBLIC dht_identity dht_identity_generate_ec(const char* common_name, const dht_identity* ca);
|
||||
OPENDHT_C_PUBLIC void dht_identity_delete(dht_identity*);
|
||||
|
||||
// dht::Value
|
||||
@@ -109,6 +117,7 @@ OPENDHT_C_PUBLIC dht_value* dht_value_new(const uint8_t* data, size_t size);
|
||||
OPENDHT_C_PUBLIC dht_value* dht_value_with_id_new(const uint8_t* data, size_t size, uint64_t value_id);
|
||||
OPENDHT_C_PUBLIC dht_value* dht_value_new_from_string(const char* str);
|
||||
OPENDHT_C_PUBLIC dht_value* dht_value_ref(const dht_value*);
|
||||
OPENDHT_C_PUBLIC dht_value* dht_value_copy(const dht_value*);
|
||||
OPENDHT_C_PUBLIC void dht_value_unref(dht_value*);
|
||||
OPENDHT_C_PUBLIC dht_data_view dht_value_get_data(const dht_value* data);
|
||||
OPENDHT_C_PUBLIC dht_value_id dht_value_get_id(const dht_value* data);
|
||||
@@ -116,12 +125,53 @@ OPENDHT_C_PUBLIC dht_publickey* dht_value_get_owner(const dht_value* data);
|
||||
OPENDHT_C_PUBLIC dht_infohash dht_value_get_recipient(const dht_value* data);
|
||||
OPENDHT_C_PUBLIC const char* dht_value_get_user_type(const dht_value* data);
|
||||
OPENDHT_C_PUBLIC void dht_value_set_user_type(dht_value* data, const char* user_type);
|
||||
OPENDHT_C_PUBLIC uint16_t dht_value_get_type(const dht_value* value);
|
||||
OPENDHT_C_PUBLIC void dht_value_set_type(dht_value* value, uint16_t type);
|
||||
OPENDHT_C_PUBLIC uint16_t dht_value_get_seq(const dht_value* value);
|
||||
OPENDHT_C_PUBLIC void dht_value_set_seq(dht_value* value, uint16_t seq);
|
||||
OPENDHT_C_PUBLIC unsigned dht_value_get_priority(const dht_value* value);
|
||||
OPENDHT_C_PUBLIC void dht_value_set_priority(dht_value* value, unsigned priority);
|
||||
OPENDHT_C_PUBLIC const char* dht_value_get_push_type(const dht_value* value);
|
||||
OPENDHT_C_PUBLIC void dht_value_set_push_type(dht_value* value, const char* push_type);
|
||||
|
||||
// callbacks
|
||||
typedef bool (*dht_get_cb)(const dht_value* value, void* user_data);
|
||||
typedef bool (*dht_value_cb)(const dht_value* value, bool expired, void* user_data);
|
||||
typedef void (*dht_done_cb)(bool ok, void* user_data);
|
||||
typedef void (*dht_shutdown_cb)(void* user_data);
|
||||
typedef void (*dht_status_cb)(int ipv4_status, int ipv6_status, void* user_data);
|
||||
typedef void (*dht_certificate_cb)(const dht_certificate* certificate, void* user_data);
|
||||
typedef void (*dht_certificate_store_cb)(const dht_pkid* id, dht_certificate_cb emit, void* emit_data, void* user_data);
|
||||
|
||||
typedef struct dht_runner_context
|
||||
{
|
||||
dht_status_cb status_cb;
|
||||
dht_certificate_store_cb certificate_store_cb;
|
||||
dht_shutdown_cb released_cb;
|
||||
void* user_data;
|
||||
} dht_runner_context;
|
||||
|
||||
enum dht_query_field {
|
||||
DHT_FIELD_ID = 1,
|
||||
DHT_FIELD_TYPE = 2,
|
||||
DHT_FIELD_SEQ = 4,
|
||||
DHT_FIELD_OWNER = 8,
|
||||
DHT_FIELD_USER_TYPE = 16
|
||||
};
|
||||
|
||||
typedef struct dht_query_result
|
||||
{
|
||||
uint32_t fields;
|
||||
uint64_t id;
|
||||
uint16_t value_type;
|
||||
uint16_t seq;
|
||||
dht_infohash owner;
|
||||
dht_data_view user_type;
|
||||
} dht_query_result;
|
||||
typedef bool (*dht_query_cb)(const dht_query_result* result, void* user_data);
|
||||
|
||||
struct dht_push_token;
|
||||
typedef struct dht_push_token dht_push_token;
|
||||
|
||||
struct OPENDHT_C_PUBLIC dht_op_token;
|
||||
typedef struct dht_op_token dht_op_token;
|
||||
@@ -169,17 +219,50 @@ struct OPENDHT_C_PUBLIC dht_runner;
|
||||
typedef struct dht_runner dht_runner;
|
||||
OPENDHT_C_PUBLIC dht_runner* dht_runner_new(void);
|
||||
OPENDHT_C_PUBLIC void dht_runner_delete(dht_runner* runner);
|
||||
OPENDHT_C_PUBLIC int dht_runner_loop(dht_runner* runner);
|
||||
OPENDHT_C_PUBLIC int dht_runner_join(dht_runner* runner);
|
||||
/* Returns 0 on success, standard error code on failure */
|
||||
OPENDHT_C_PUBLIC int dht_runner_run(dht_runner* runner, in_port_t port);
|
||||
/* Returns 0 on success, standard error code on failure */
|
||||
OPENDHT_C_PUBLIC int dht_runner_run_config(dht_runner* runner, in_port_t port, const dht_runner_config* config);
|
||||
OPENDHT_C_PUBLIC int dht_runner_run_bind(dht_runner* runner,
|
||||
const char* ipv4,
|
||||
const char* ipv6,
|
||||
in_port_t port,
|
||||
const dht_runner_config* config,
|
||||
const dht_runner_context* context);
|
||||
OPENDHT_C_PUBLIC int dht_runner_connectivity_changed(dht_runner* runner);
|
||||
OPENDHT_C_PUBLIC int dht_runner_register_type(
|
||||
dht_runner* runner, uint16_t id, const char* name, uint64_t expiration_seconds, bool insecure);
|
||||
OPENDHT_C_PUBLIC dht_publickey* dht_runner_get_publickey(const dht_runner* runner);
|
||||
OPENDHT_C_PUBLIC void dht_runner_find_certificate(
|
||||
dht_runner* runner, const dht_pkid* id, dht_certificate_cb callback, dht_shutdown_cb released_cb, void* user_data);
|
||||
OPENDHT_C_PUBLIC int dht_runner_register_certificate(dht_runner* runner, const dht_certificate* certificate);
|
||||
OPENDHT_C_PUBLIC void dht_runner_ping(
|
||||
dht_runner* runner, struct sockaddr* addr, socklen_t addr_len, dht_done_cb done_cb, void* cb_user_data);
|
||||
OPENDHT_C_PUBLIC void dht_runner_bootstrap(dht_runner* runner, const char* host, const char* service);
|
||||
OPENDHT_C_PUBLIC void dht_runner_get(
|
||||
dht_runner* runner, const dht_infohash* hash, dht_get_cb cb, dht_done_cb done_cb, void* cb_user_data);
|
||||
OPENDHT_C_PUBLIC void dht_runner_get_filtered(dht_runner* runner,
|
||||
const dht_infohash* hash,
|
||||
const char* where,
|
||||
dht_get_cb cb,
|
||||
dht_done_cb done_cb,
|
||||
void* user_data);
|
||||
OPENDHT_C_PUBLIC void dht_runner_query(dht_runner* runner,
|
||||
const dht_infohash* hash,
|
||||
const char* query,
|
||||
dht_query_cb cb,
|
||||
dht_done_cb done_cb,
|
||||
void* user_data);
|
||||
OPENDHT_C_PUBLIC dht_op_token* dht_runner_listen(
|
||||
dht_runner* runner, const dht_infohash* hash, dht_value_cb cb, dht_shutdown_cb done_cb, void* cb_user_data);
|
||||
OPENDHT_C_PUBLIC dht_op_token* dht_runner_listen_filtered(dht_runner* runner,
|
||||
const dht_infohash* hash,
|
||||
const char* where,
|
||||
dht_value_cb cb,
|
||||
dht_shutdown_cb released_cb,
|
||||
void* user_data);
|
||||
OPENDHT_C_PUBLIC void dht_runner_cancel_listen(dht_runner* runner, const dht_infohash* hash, dht_op_token* token);
|
||||
OPENDHT_C_PUBLIC void dht_runner_put(dht_runner* runner,
|
||||
const dht_infohash* hash,
|
||||
@@ -208,6 +291,16 @@ OPENDHT_C_PUBLIC bool dht_runner_is_running(const dht_runner* runner);
|
||||
OPENDHT_C_PUBLIC in_port_t dht_runner_get_bound_port(const dht_runner* runner, sa_family_t af);
|
||||
/** Returns null-terminated array that must be freed after use as well as each element */
|
||||
OPENDHT_C_PUBLIC struct sockaddr** dht_runner_get_public_address(const dht_runner* runner);
|
||||
OPENDHT_C_PUBLIC void dht_sockaddr_array_delete(struct sockaddr** addresses);
|
||||
OPENDHT_C_PUBLIC int dht_runner_set_push_notification_token(dht_runner* runner, const char* value);
|
||||
OPENDHT_C_PUBLIC int dht_runner_set_push_notification_topic(dht_runner* runner, const char* value);
|
||||
OPENDHT_C_PUBLIC int dht_runner_set_push_notification_platform(dht_runner* runner, const char* value);
|
||||
OPENDHT_C_PUBLIC dht_push_token* dht_runner_push_notification_received(dht_runner* runner,
|
||||
const char* const* keys,
|
||||
const char* const* values,
|
||||
size_t count);
|
||||
OPENDHT_C_PUBLIC int dht_push_token_poll(dht_push_token* token, int* result);
|
||||
OPENDHT_C_PUBLIC void dht_push_token_delete(dht_push_token* token);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ RUN cd opendht && mkdir build && cd build \
|
||||
-DOPENDHT_SYSTEMD=On \
|
||||
&& make -j8 && make install
|
||||
|
||||
FROM alpine:3.23 AS install
|
||||
FROM alpine:3.24 AS install
|
||||
COPY --from=build /install /
|
||||
RUN apk add --no-cache \
|
||||
libstdc++ \
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM ubuntu:24.04
|
||||
FROM ubuntu:26.04
|
||||
LABEL maintainer="Adrien Béraud <adrien.beraud@savoirfairelinux.com>"
|
||||
LABEL org.opencontainers.image.source=https://github.com/savoirfairelinux/opendht
|
||||
|
||||
@@ -13,18 +13,20 @@ RUN apt-get update && apt-get install -y \
|
||||
python3-dev python3-setuptools python3-build python3-virtualenv cython3 \
|
||||
libncurses5-dev libreadline-dev nettle-dev libcppunit-dev \
|
||||
libgnutls28-dev libuv1-dev libjsoncpp-dev libargon2-dev \
|
||||
libssl-dev libfmt-dev libasio-dev libmsgpack-dev \
|
||||
libssl-dev libfmt-dev libasio-dev libmsgpack-cxx-dev \
|
||||
libllhttp-dev libsimdutf-dev \
|
||||
&& apt-get clean && rm -rf /var/lib/apt/lists/* /var/cache/apt/*
|
||||
|
||||
RUN mkdir /usr/include/nonstd \
|
||||
&& wget https://raw.githubusercontent.com/martinmoene/expected-lite/master/include/nonstd/expected.hpp \
|
||||
-O /usr/include/nonstd/expected.hpp
|
||||
|
||||
ARG RESTINIO_VERSION=0.7.10
|
||||
RUN echo "*** Downloading RESTinio ***" \
|
||||
&& mkdir restinio && cd restinio \
|
||||
&& wget https://github.com/Stiffstream/restinio/releases/download/v0.7.8.1/restinio-0.7.8.1.tar.bz2 \
|
||||
&& ls -l && tar -xjf restinio-0.7.8.1.tar.bz2 \
|
||||
&& cd restinio-0.7.8.1/dev \
|
||||
&& wget "https://github.com/Stiffstream/restinio/releases/download/v${RESTINIO_VERSION}/restinio-${RESTINIO_VERSION}.tar.bz2" \
|
||||
&& ls -l && tar -xjf "restinio-${RESTINIO_VERSION}.tar.bz2" \
|
||||
&& cd "restinio-${RESTINIO_VERSION}/dev" \
|
||||
&& cmake -DCMAKE_INSTALL_PREFIX=/usr -DRESTINIO_TEST=Off -DRESTINIO_SAMPLE=Off -DRESTINIO_BENCHMARK=Off \
|
||||
-DRESTINIO_WITH_SOBJECTIZER=Off -DRESTINIO_DEP_STANDALONE_ASIO=system -DRESTINIO_DEP_LLHTTP=system \
|
||||
-DRESTINIO_DEP_FMT=system -DRESTINIO_DEP_EXPECTED_LITE=system . \
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM alpine:3.23
|
||||
FROM alpine:3.24
|
||||
LABEL maintainer="Adrien Béraud <adrien.beraud@savoirfairelinux.com>"
|
||||
LABEL org.opencontainers.image.source=https://github.com/savoirfairelinux/opendht
|
||||
|
||||
@@ -15,11 +15,12 @@ RUN mkdir /usr/include/nonstd \
|
||||
&& wget https://raw.githubusercontent.com/martinmoene/expected-lite/master/include/nonstd/expected.hpp \
|
||||
-O /usr/include/nonstd/expected.hpp
|
||||
|
||||
ARG RESTINIO_VERSION=0.7.10
|
||||
RUN echo "*** Downloading RESTinio ***" \
|
||||
&& mkdir restinio && cd restinio \
|
||||
&& wget https://github.com/Stiffstream/restinio/releases/download/v0.7.8.1/restinio-0.7.8.1.tar.bz2 \
|
||||
&& ls -l && tar -xjf restinio-0.7.8.1.tar.bz2 \
|
||||
&& cd restinio-0.7.8.1/dev \
|
||||
&& wget "https://github.com/Stiffstream/restinio/releases/download/v${RESTINIO_VERSION}/restinio-${RESTINIO_VERSION}.tar.bz2" \
|
||||
&& ls -l && tar -xjf "restinio-${RESTINIO_VERSION}.tar.bz2" \
|
||||
&& cd "restinio-${RESTINIO_VERSION}/dev" \
|
||||
&& cmake -DCMAKE_INSTALL_PREFIX=/usr -DRESTINIO_TEST=Off -DRESTINIO_SAMPLE=Off -DRESTINIO_BENCHMARK=Off \
|
||||
-DRESTINIO_WITH_SOBJECTIZER=Off -DRESTINIO_DEP_STANDALONE_ASIO=system -DRESTINIO_DEP_LLHTTP=system \
|
||||
-DRESTINIO_DEP_FMT=system -DRESTINIO_DEP_EXPECTED_LITE=system . \
|
||||
|
||||
@@ -15,11 +15,12 @@ RUN mkdir /usr/include/nonstd \
|
||||
&& wget https://raw.githubusercontent.com/martinmoene/expected-lite/master/include/nonstd/expected.hpp \
|
||||
-O /usr/include/nonstd/expected.hpp
|
||||
|
||||
ARG RESTINIO_VERSION=0.7.10
|
||||
RUN echo "*** Downloading RESTinio ***" \
|
||||
&& mkdir restinio && cd restinio \
|
||||
&& wget https://github.com/Stiffstream/restinio/releases/download/v0.7.8.1/restinio-0.7.8.1.tar.bz2 \
|
||||
&& ls -l && tar -xjf restinio-0.7.8.1.tar.bz2 \
|
||||
&& cd restinio-0.7.8.1/dev \
|
||||
&& wget "https://github.com/Stiffstream/restinio/releases/download/v${RESTINIO_VERSION}/restinio-${RESTINIO_VERSION}.tar.bz2" \
|
||||
&& ls -l && tar -xjf "restinio-${RESTINIO_VERSION}.tar.bz2" \
|
||||
&& cd "restinio-${RESTINIO_VERSION}/dev" \
|
||||
&& cmake -GNinja -DCMAKE_INSTALL_PREFIX=/usr -DRESTINIO_TEST=Off -DRESTINIO_SAMPLE=Off -DRESTINIO_BENCHMARK=Off \
|
||||
-DRESTINIO_WITH_SOBJECTIZER=Off -DRESTINIO_DEP_STANDALONE_ASIO=system -DRESTINIO_DEP_LLHTTP=system \
|
||||
-DRESTINIO_DEP_FMT=system -DRESTINIO_DEP_EXPECTED_LITE=system . \
|
||||
|
||||
@@ -22,11 +22,12 @@ RUN mkdir /usr/include/nonstd \
|
||||
&& wget https://raw.githubusercontent.com/martinmoene/expected-lite/master/include/nonstd/expected.hpp \
|
||||
-O /usr/include/nonstd/expected.hpp
|
||||
|
||||
ARG RESTINIO_VERSION=0.7.10
|
||||
RUN echo "*** Downloading RESTinio ***" \
|
||||
&& mkdir restinio && cd restinio \
|
||||
&& wget https://github.com/Stiffstream/restinio/releases/download/v.0.7.3/restinio-0.7.3.tar.bz2 \
|
||||
&& ls -l && tar -xjf restinio-0.7.3.tar.bz2 \
|
||||
&& cd restinio-0.7.3/dev \
|
||||
&& wget "https://github.com/Stiffstream/restinio/releases/download/v${RESTINIO_VERSION}/restinio-${RESTINIO_VERSION}.tar.bz2" \
|
||||
&& ls -l && tar -xjf "restinio-${RESTINIO_VERSION}.tar.bz2" \
|
||||
&& cd "restinio-${RESTINIO_VERSION}/dev" \
|
||||
&& cmake -DCMAKE_INSTALL_PREFIX=/usr -DRESTINIO_TEST=Off -DRESTINIO_SAMPLE=Off -DRESTINIO_BENCHMARK=Off \
|
||||
-DRESTINIO_WITH_SOBJECTIZER=Off -DRESTINIO_DEP_STANDALONE_ASIO=system -DRESTINIO_DEP_LLHTTP=system \
|
||||
-DRESTINIO_DEP_FMT=system -DRESTINIO_DEP_EXPECTED_LITE=system . \
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM ubuntu:24.04
|
||||
FROM ubuntu:26.04
|
||||
LABEL maintainer="Adrien Béraud <adrien.beraud@savoirfairelinux.com>"
|
||||
LABEL org.opencontainers.image.source=https://github.com/savoirfairelinux/opendht
|
||||
|
||||
@@ -10,9 +10,9 @@ RUN apt-get update && apt-get install -y \
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y llvm llvm-dev lldb clang gdb make cmake meson pkg-config \
|
||||
libtool git wget libncurses5-dev libreadline-dev \
|
||||
nettle-dev libgnutls28-dev libuv1-dev libmsgpack-dev libjsoncpp-dev python3-dev \
|
||||
nettle-dev libgnutls28-dev libuv1-dev libmsgpack-cxx-dev libjsoncpp-dev python3-dev \
|
||||
python3-setuptools libcppunit-dev python3-pip python3-build python3-virtualenv \
|
||||
libssl-dev libargon2-dev \
|
||||
libssl-dev libargon2-dev libllhttp-dev libsimdutf-dev \
|
||||
libfmt-dev libasio-dev \
|
||||
&& apt-get remove -y gcc g++ && apt-get autoremove -y \
|
||||
&& apt-get clean && rm -rf /var/lib/apt/lists/* /var/cache/apt/*
|
||||
@@ -26,11 +26,12 @@ RUN mkdir /usr/include/nonstd \
|
||||
&& wget https://raw.githubusercontent.com/martinmoene/expected-lite/master/include/nonstd/expected.hpp \
|
||||
-O /usr/include/nonstd/expected.hpp
|
||||
|
||||
ARG RESTINIO_VERSION=0.7.10
|
||||
RUN echo "*** Downloading RESTinio ***" \
|
||||
&& mkdir restinio && cd restinio \
|
||||
&& wget https://github.com/Stiffstream/restinio/releases/download/v0.7.8.1/restinio-0.7.8.1.tar.bz2 \
|
||||
&& ls -l && tar -xjf restinio-0.7.8.1.tar.bz2 \
|
||||
&& cd restinio-0.7.8.1/dev \
|
||||
&& wget "https://github.com/Stiffstream/restinio/releases/download/v${RESTINIO_VERSION}/restinio-${RESTINIO_VERSION}.tar.bz2" \
|
||||
&& ls -l && tar -xjf "restinio-${RESTINIO_VERSION}.tar.bz2" \
|
||||
&& cd "restinio-${RESTINIO_VERSION}/dev" \
|
||||
&& cmake -DCMAKE_INSTALL_PREFIX=/usr -DRESTINIO_TEST=Off -DRESTINIO_SAMPLE=Off -DRESTINIO_BENCHMARK=Off \
|
||||
-DRESTINIO_WITH_SOBJECTIZER=Off -DRESTINIO_DEP_STANDALONE_ASIO=system -DRESTINIO_DEP_LLHTTP=system \
|
||||
-DRESTINIO_DEP_FMT=system -DRESTINIO_DEP_EXPECTED_LITE=system . \
|
||||
|
||||
@@ -204,7 +204,13 @@ struct OPENDHT_PUBLIC PrivateKey
|
||||
* @param algo : the public key algorithm to use, GNUTLS_PK_RSA or GNUTLS_PK_RSA_OAEP
|
||||
*/
|
||||
static PrivateKey generate(unsigned key_length = 4096, gnutls_pk_algorithm_t algo = GNUTLS_PK_RSA);
|
||||
static PrivateKey generateEC();
|
||||
|
||||
/**
|
||||
* Generate a new EC key pair
|
||||
* @param curve : the elliptic curve to use. If GNUTLS_ECC_CURVE_INVALID is provided, the default curve will be used
|
||||
* (currently GNUTLS_ECC_CURVE_SECP384R1).
|
||||
*/
|
||||
static PrivateKey generateEC(gnutls_ecc_curve_t curve = GNUTLS_ECC_CURVE_INVALID);
|
||||
|
||||
gnutls_privkey_t key {};
|
||||
gnutls_x509_privkey_t x509_key {};
|
||||
@@ -267,9 +273,15 @@ public:
|
||||
/**
|
||||
* Sign this revocation list using provided key and certificate.
|
||||
* Validity_period sets the duration until next update (default to no next update).
|
||||
* If `number` is non-zero, it is stored verbatim (big-endian) as the CRL Number
|
||||
* extension. Otherwise the number is initialized randomly on the first signature
|
||||
* and incremented on subsequent ones.
|
||||
*/
|
||||
void sign(const PrivateKey&, const Certificate&, duration validity_period = {});
|
||||
void sign(const Identity& id) { sign(*id.first, *id.second); }
|
||||
void sign(const PrivateKey&, const Certificate&, duration validity_period = {}, uint64_t number = 0);
|
||||
void sign(const Identity& id, duration validity_period = {}, uint64_t number = 0)
|
||||
{
|
||||
sign(*id.first, *id.second, validity_period, number);
|
||||
}
|
||||
|
||||
bool isSignedBy(const Certificate& issuer) const;
|
||||
|
||||
|
||||
@@ -159,7 +159,13 @@ public:
|
||||
{}
|
||||
|
||||
static Value::Filter getFilter() { return EncryptedValue::getFilter(); }
|
||||
virtual void unpackValue(const dht::Value& v) override
|
||||
{
|
||||
BaseClass::unpackValue(v);
|
||||
id = v.id;
|
||||
}
|
||||
|
||||
dht::Value::Id id {0}; /// Value ID of the trust request, used for deserialization only.
|
||||
std::string service;
|
||||
std::string conversationId;
|
||||
Blob payload;
|
||||
|
||||
@@ -76,8 +76,14 @@ public:
|
||||
|
||||
asio::io_context& io_context() const;
|
||||
|
||||
// Monotonic clock for elapsed-time statistics (request rate, uptime).
|
||||
// Must stay on steady_clock so NTP/wall-clock corrections don't skew durations.
|
||||
using clock = std::chrono::steady_clock;
|
||||
using time_point = clock::time_point;
|
||||
// Wall clock for persisted subscribe/permanent-put lease expirations,
|
||||
// so they map directly to epoch time and survive restarts.
|
||||
using sys_clock = std::chrono::system_clock;
|
||||
using sys_time_point = sys_clock::time_point;
|
||||
|
||||
struct PushStats
|
||||
{
|
||||
@@ -266,6 +272,9 @@ private:
|
||||
{
|
||||
std::mutex lock;
|
||||
std::string sessionId;
|
||||
/** Where to push: follows the latest subscription refresh of the device. */
|
||||
PushType type {PushType::None};
|
||||
std::string topic;
|
||||
PushSessionContext(const std::string& id)
|
||||
: sessionId(id)
|
||||
{}
|
||||
@@ -335,20 +344,16 @@ private:
|
||||
*
|
||||
* @param infoHash The information hash associated with the push listen request.
|
||||
* @param pushToken The push token associated with the push listen request.
|
||||
* @param type The type of the push listen request.
|
||||
* @param clientId The client ID associated with the push listen request.
|
||||
* @param sessionCtx The shared pointer to the push session context associated with the push listen request.
|
||||
* @param topic The topic associated with the push listen request.
|
||||
* @param sessionCtx The push session context of the listener, giving the session, push type and topic to use.
|
||||
* @param values The vector of shared pointers to values associated with the push listen request.
|
||||
* @param expired A boolean indicating whether the push listen request has expired.
|
||||
* @return true.
|
||||
*/
|
||||
bool handlePushListen(const InfoHash& infoHash,
|
||||
const std::string& pushToken,
|
||||
PushType type,
|
||||
const std::string& clientId,
|
||||
const std::shared_ptr<DhtProxyServer::PushSessionContext>& sessionCtx,
|
||||
const std::string& topic,
|
||||
const std::vector<std::shared_ptr<Value>>& values,
|
||||
bool expired);
|
||||
|
||||
@@ -401,12 +406,12 @@ private:
|
||||
std::shared_ptr<ConnectionListener> connListener_;
|
||||
struct PermanentPut
|
||||
{
|
||||
time_point expiration;
|
||||
sys_time_point expiration;
|
||||
std::string pushToken;
|
||||
std::string clientId;
|
||||
std::shared_ptr<PushSessionContext> sessionCtx;
|
||||
std::unique_ptr<asio::steady_timer> expireTimer;
|
||||
std::unique_ptr<asio::steady_timer> expireNotifyTimer;
|
||||
std::unique_ptr<asio::system_timer> expireTimer;
|
||||
std::unique_ptr<asio::system_timer> expireNotifyTimer;
|
||||
Sp<Value> value;
|
||||
PushType type;
|
||||
std::string topic;
|
||||
@@ -419,7 +424,7 @@ private:
|
||||
p.pack("value");
|
||||
p.pack(value);
|
||||
p.pack("exp");
|
||||
p.pack(to_time_t(expiration));
|
||||
p.pack(sys_clock::to_time_t(expiration));
|
||||
if (not clientId.empty()) {
|
||||
p.pack("cid");
|
||||
p.pack(clientId);
|
||||
@@ -460,12 +465,12 @@ private:
|
||||
#ifdef OPENDHT_PUSH_NOTIFICATIONS
|
||||
struct Listener
|
||||
{
|
||||
time_point expiration;
|
||||
sys_time_point expiration;
|
||||
std::string clientId;
|
||||
std::shared_ptr<PushSessionContext> sessionCtx;
|
||||
std::future<size_t> internalToken;
|
||||
std::unique_ptr<asio::steady_timer> expireTimer;
|
||||
std::unique_ptr<asio::steady_timer> expireNotifyTimer;
|
||||
std::unique_ptr<asio::system_timer> expireTimer;
|
||||
std::unique_ptr<asio::system_timer> expireNotifyTimer;
|
||||
PushType type;
|
||||
std::string topic;
|
||||
|
||||
@@ -476,7 +481,7 @@ private:
|
||||
p.pack("cid");
|
||||
p.pack(clientId);
|
||||
p.pack("exp");
|
||||
p.pack(to_time_t(expiration));
|
||||
p.pack(sys_clock::to_time_t(expiration));
|
||||
if (sessionCtx) {
|
||||
std::lock_guard l(sessionCtx->lock);
|
||||
p.pack("sid");
|
||||
|
||||
@@ -532,6 +532,8 @@ public:
|
||||
|
||||
/**
|
||||
* Insert a push notification to process for OpenDHT
|
||||
* Returns an immediately ready future with IgnoredStopped if the runner is idle,
|
||||
* or IgnoredDisabled if push notification support is disabled at build time.
|
||||
*/
|
||||
std::future<PushNotificationResult> pushNotificationReceived(const std::map<std::string, std::string>& data);
|
||||
|
||||
|
||||
+15
-14
@@ -51,11 +51,11 @@ using ConnectHandlerCb = std::function<void(const asio::error_code& ec, const as
|
||||
using ssl_socket_t = restinio::impl::tls_socket_t;
|
||||
using socket_t = asio::ip::tcp::socket;
|
||||
|
||||
class OPENDHT_PUBLIC Url
|
||||
struct OPENDHT_PUBLIC Url
|
||||
{
|
||||
public:
|
||||
Url() = default;
|
||||
Url(std::string_view url);
|
||||
Url(std::string url);
|
||||
|
||||
std::string url;
|
||||
std::string protocol {"http"};
|
||||
std::string_view user;
|
||||
@@ -106,9 +106,16 @@ public:
|
||||
|
||||
void timeout(const std::chrono::seconds& timeout, HandlerCb cb = {});
|
||||
|
||||
/**
|
||||
* Close the underlying socket.
|
||||
* Safe to call from any thread: the actual close is dispatched to
|
||||
* the thread running the io_context.
|
||||
*/
|
||||
void close();
|
||||
|
||||
private:
|
||||
void doClose();
|
||||
|
||||
template<typename T>
|
||||
T wrapCallback(T cb) const
|
||||
{
|
||||
@@ -149,7 +156,7 @@ public:
|
||||
using ResolverCb
|
||||
= std::function<void(const asio::error_code& ec, const std::vector<asio::ip::tcp::endpoint>& endpoints)>;
|
||||
|
||||
Resolver(asio::io_context& ctx, const std::string& url, std::shared_ptr<log::Logger> logger = {});
|
||||
Resolver(asio::io_context& ctx, std::string url, std::shared_ptr<log::Logger> logger = {});
|
||||
Resolver(asio::io_context& ctx,
|
||||
std::string_view host,
|
||||
std::string_view service,
|
||||
@@ -161,10 +168,6 @@ public:
|
||||
std::vector<asio::ip::tcp::endpoint> endpoints,
|
||||
const bool ssl = false,
|
||||
std::shared_ptr<log::Logger> logger = {});
|
||||
Resolver(asio::io_context& ctx,
|
||||
const std::string& url,
|
||||
std::vector<asio::ip::tcp::endpoint> endpoints,
|
||||
std::shared_ptr<log::Logger> logger = {});
|
||||
|
||||
~Resolver();
|
||||
|
||||
@@ -216,19 +219,19 @@ public:
|
||||
|
||||
// resolves implicitly
|
||||
Request(asio::io_context& ctx,
|
||||
const std::string& url,
|
||||
std::string url,
|
||||
const Json::Value& json,
|
||||
OnJsonCb jsoncb,
|
||||
std::shared_ptr<log::Logger> logger = {});
|
||||
Request(asio::io_context& ctx, const std::string& url, OnJsonCb jsoncb, std::shared_ptr<log::Logger> logger = {});
|
||||
Request(asio::io_context& ctx, std::string url, OnJsonCb jsoncb, std::shared_ptr<log::Logger> logger = {});
|
||||
|
||||
Request(asio::io_context& ctx, const std::string& url, std::shared_ptr<log::Logger> logger = {});
|
||||
Request(asio::io_context& ctx, std::string url, std::shared_ptr<log::Logger> logger = {});
|
||||
Request(asio::io_context& ctx,
|
||||
std::string_view host,
|
||||
std::string_view service,
|
||||
const bool ssl = false,
|
||||
std::shared_ptr<log::Logger> logger = {});
|
||||
Request(asio::io_context& ctx, const std::string& url, OnDoneCb onDone, std::shared_ptr<log::Logger> logger = {});
|
||||
Request(asio::io_context& ctx, std::string url, OnDoneCb onDone, std::shared_ptr<log::Logger> logger = {});
|
||||
|
||||
// user defined resolver
|
||||
Request(asio::io_context& ctx, std::shared_ptr<Resolver> resolver, sa_family_t family = AF_UNSPEC);
|
||||
@@ -344,8 +347,6 @@ private:
|
||||
|
||||
dht::crypto::Identity client_identity_;
|
||||
std::shared_ptr<dht::crypto::Certificate> server_ca_;
|
||||
std::string service_;
|
||||
std::string host_;
|
||||
|
||||
unsigned int id_;
|
||||
static std::atomic_uint ids_;
|
||||
|
||||
@@ -123,28 +123,19 @@ ee *
|
||||
*/
|
||||
static inline unsigned commonBits(const Prefix& p1, const Prefix& p2)
|
||||
{
|
||||
unsigned i, j;
|
||||
uint8_t x;
|
||||
/* size_ counts bits, so the comparison walks bits: reading it as a
|
||||
byte count would run past the end of the shorter content. */
|
||||
auto longest_prefix_size = std::min(p1.size_, p2.size_);
|
||||
|
||||
for (i = 0; i < longest_prefix_size; i++) {
|
||||
if (p1.content_.data()[i] != p2.content_.data()[i] or not p1.isFlagActive(i) or not p2.isFlagActive(i)) {
|
||||
unsigned common = 0;
|
||||
while (common < longest_prefix_size) {
|
||||
if (not p1.isFlagActive(common) or not p2.isFlagActive(common)
|
||||
or p1.isContentBitActive(common) != p2.isContentBitActive(common))
|
||||
break;
|
||||
}
|
||||
common++;
|
||||
}
|
||||
|
||||
if (i == longest_prefix_size)
|
||||
return 8 * longest_prefix_size;
|
||||
|
||||
x = p1.content_.data()[i] ^ p2.content_.data()[i];
|
||||
|
||||
j = 0;
|
||||
while ((x & 0x80) == 0) {
|
||||
x <<= 1;
|
||||
j++;
|
||||
}
|
||||
|
||||
return 8 * i + j;
|
||||
return common;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -205,7 +196,7 @@ private:
|
||||
for (auto i = copy.size(); i < size; i++)
|
||||
copy.push_back(0);
|
||||
|
||||
swapBit(copy, size_ + 1);
|
||||
swapBit(copy, size_);
|
||||
return copy;
|
||||
}
|
||||
|
||||
@@ -242,8 +233,9 @@ private:
|
||||
if (bit >= b.size() * 8)
|
||||
throw std::out_of_range("bit larger than prefix size.");
|
||||
|
||||
size_t offset_bit = (8 - bit) % 8;
|
||||
b[bit / 8] ^= (1 << offset_bit);
|
||||
/* Bit 0 is the most significant one of the first byte, as
|
||||
isActiveBit reads it. */
|
||||
b[bit / 8] ^= (1 << (7 - (bit % 8)));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -489,8 +489,12 @@ private:
|
||||
static constexpr size_t MTU {1280};
|
||||
static constexpr size_t MAX_PACKET_VALUE_SIZE {600};
|
||||
static constexpr size_t MAX_MESSAGE_VALUE_SIZE {56 * 1024};
|
||||
static constexpr size_t RX_MAX_EARLY_BYTES {MAX_MESSAGE_VALUE_SIZE + MAX_VALUE_SIZE + 32};
|
||||
static constexpr size_t RX_MAX_EARLY_PARTS {RX_MAX_EARLY_BYTES / MTU + 2};
|
||||
|
||||
void process(std::unique_ptr<ParsedMessage>&&, const SockAddr& from);
|
||||
bool isExpectedReply(const ParsedMessage&, const SockAddr& from);
|
||||
bool tryCompleteMessage(PartialMessage&, const SockAddr& from);
|
||||
|
||||
bool rateLimit(const SockAddr& addr);
|
||||
|
||||
@@ -527,7 +531,8 @@ private:
|
||||
{
|
||||
return packValueHeader(buf, values.begin(), values.end());
|
||||
}
|
||||
void maintainRxBuffer(Tid tid);
|
||||
using PartialMessageKey = std::pair<SockAddr, Tid>;
|
||||
void maintainRxBuffer(const PartialMessageKey& key);
|
||||
|
||||
/*************
|
||||
* Answers *
|
||||
@@ -573,7 +578,7 @@ private:
|
||||
|
||||
// requests handling
|
||||
std::map<Tid, Sp<Request>> requests {};
|
||||
std::map<Tid, PartialMessage> partial_messages;
|
||||
std::map<PartialMessageKey, PartialMessage> partial_messages;
|
||||
|
||||
MessageStats in_stats {}, out_stats {};
|
||||
std::set<SockAddr> blacklist {};
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace dht {
|
||||
namespace proxy {
|
||||
|
||||
constexpr const std::chrono::hours OP_TIMEOUT {24}; // one day
|
||||
constexpr const std::chrono::hours OP_MARGIN {2}; // two hours
|
||||
constexpr const std::chrono::hours OP_MARGIN {8}; // eight hours
|
||||
|
||||
using ListenToken = uint64_t;
|
||||
|
||||
|
||||
@@ -284,6 +284,11 @@ public:
|
||||
void forwardAllMessages(bool forward) { forward_all_ = forward; }
|
||||
|
||||
void setPushNotificationToken(const std::string& token = "") override { dht_->setPushNotificationToken(token); }
|
||||
void setPushNotificationTopic(const std::string& topic) override { dht_->setPushNotificationTopic(topic); }
|
||||
void setPushNotificationPlatform(const std::string& platform) override
|
||||
{
|
||||
dht_->setPushNotificationPlatform(platform);
|
||||
}
|
||||
|
||||
/**
|
||||
* Call linked callback with push_notification
|
||||
|
||||
@@ -10,7 +10,9 @@
|
||||
#include <future>
|
||||
#include <functional>
|
||||
|
||||
#include <ciso646> // fix windows compiler bug
|
||||
#if __cplusplus < 202002L
|
||||
#include <ciso646>
|
||||
#endif
|
||||
|
||||
namespace dht {
|
||||
|
||||
|
||||
@@ -85,6 +85,14 @@ using system_clock = std::chrono::system_clock;
|
||||
using time_point = clock::time_point;
|
||||
using duration = clock::duration;
|
||||
|
||||
OPENDHT_PUBLIC time_point from_system_time(system_clock::time_point t,
|
||||
time_point steady_now,
|
||||
system_clock::time_point system_now);
|
||||
OPENDHT_PUBLIC time_point from_system_time(system_clock::time_point t);
|
||||
OPENDHT_PUBLIC system_clock::time_point to_system_time(time_point t,
|
||||
time_point steady_now,
|
||||
system_clock::time_point system_now);
|
||||
OPENDHT_PUBLIC system_clock::time_point to_system_time(time_point t);
|
||||
OPENDHT_PUBLIC time_point from_time_t(std::time_t t);
|
||||
OPENDHT_PUBLIC std::time_t to_time_t(time_point t);
|
||||
|
||||
|
||||
+17
-1
@@ -742,6 +742,7 @@ struct OPENDHT_PUBLIC FieldValue
|
||||
switch (field) {
|
||||
case Value::Field::Id:
|
||||
case Value::Field::ValueType:
|
||||
case Value::Field::SeqNum:
|
||||
p.pack(intValue);
|
||||
break;
|
||||
case Value::Field::OwnerPk:
|
||||
@@ -773,6 +774,7 @@ struct OPENDHT_PUBLIC FieldValue
|
||||
switch (field) {
|
||||
case Value::Field::Id:
|
||||
case Value::Field::ValueType:
|
||||
case Value::Field::SeqNum:
|
||||
intValue = v->as<decltype(intValue)>();
|
||||
break;
|
||||
case Value::Field::OwnerPk:
|
||||
@@ -1015,7 +1017,7 @@ struct OPENDHT_PUBLIC Query
|
||||
*
|
||||
* where
|
||||
*
|
||||
* - $field$ = *|id|value_type|owner_pk|user_type
|
||||
* - $field$ = *|id|value_type|seq|owner_pk|user_type
|
||||
* - $value$ = $string$|$integer$
|
||||
* - $string$: a simple string WITHOUT SPACES.
|
||||
* - $integer$: a simple integer.
|
||||
@@ -1032,9 +1034,23 @@ struct OPENDHT_PUBLIC Query
|
||||
|
||||
/**
|
||||
* Tell if the query is satisfied by another query.
|
||||
*
|
||||
* The answer to `q` contains every value this query asks for, but it may
|
||||
* contain more: the caller is responsible for applying this query's own
|
||||
* filters to the values it receives.
|
||||
*/
|
||||
bool isSatisfiedBy(const Query& q) const;
|
||||
|
||||
/**
|
||||
* Tell if the projected answer to another query can be reported for this
|
||||
* query without any further filtering.
|
||||
*
|
||||
* A field-projected answer only carries the fields selected by `q`, so a
|
||||
* narrower filter can no longer be evaluated on it. Both queries must
|
||||
* therefore apply exactly the same filters.
|
||||
*/
|
||||
bool isProjectionSatisfiedBy(const Query& q) const;
|
||||
|
||||
template<typename Packer>
|
||||
void msgpack_pack(Packer& pk) const
|
||||
{
|
||||
|
||||
+59
-8
@@ -5,7 +5,7 @@ project(
|
||||
'cython',
|
||||
license: 'MIT',
|
||||
license_files: 'LICENSE',
|
||||
version: '4.0.0',
|
||||
version: '4.4.2',
|
||||
meson_version: '>=1.1.0',
|
||||
default_options: ['cpp_std=c++20', 'warning_level=3'],
|
||||
)
|
||||
@@ -15,10 +15,23 @@ nettle = dependency('nettle')
|
||||
msgpack = dependency('msgpack-cxx', required: false)
|
||||
argon2 = dependency('libargon2')
|
||||
fmt = dependency('fmt')
|
||||
openssl = dependency('openssl', required: get_option('proxy_client'))
|
||||
jsoncpp = dependency('jsoncpp', required: get_option('proxy_client'))
|
||||
llhttp = dependency('llhttp', 'libllhttp', required: get_option('proxy_client'))
|
||||
simdutf = dependency('simdutf', required: get_option('proxy_client'))
|
||||
|
||||
proxy_enabled = get_option('proxy_client').enabled() or get_option('proxy_server').enabled()
|
||||
|
||||
openssl = dependency('openssl', required: proxy_enabled)
|
||||
jsoncpp = dependency('jsoncpp', required: proxy_enabled)
|
||||
llhttp = dependency('llhttp', 'libllhttp', required: proxy_enabled)
|
||||
simdutf = dependency('simdutf', required: proxy_enabled)
|
||||
|
||||
# restinio is a header-only library that may not ship a pkg-config file
|
||||
restinio_dep = dependency('restinio', required: false)
|
||||
if not restinio_dep.found()
|
||||
cpp = meson.get_compiler('cpp')
|
||||
if cpp.has_header('restinio/all.hpp', required: proxy_enabled)
|
||||
restinio_dep = declare_dependency()
|
||||
endif
|
||||
endif
|
||||
|
||||
io_uring = host_machine.system() == 'linux' ? dependency('liburing', required: false) : disabler()
|
||||
|
||||
deps = [
|
||||
@@ -33,6 +46,10 @@ deps = [
|
||||
llhttp,
|
||||
]
|
||||
|
||||
if restinio_dep.found()
|
||||
deps += restinio_dep
|
||||
endif
|
||||
|
||||
if host_machine.system() == 'linux' and io_uring.found()
|
||||
deps += io_uring
|
||||
endif
|
||||
@@ -79,7 +96,7 @@ if get_option('indexation').enabled()
|
||||
opendht_src += 'src/indexation/pht.cpp'
|
||||
add_project_arguments('-DOPENDHT_INDEXATION', language: 'cpp')
|
||||
endif
|
||||
if jsoncpp.found()
|
||||
if jsoncpp.found() and simdutf.found()
|
||||
opendht_src += ['src/base64.cpp']
|
||||
add_project_arguments('-DOPENDHT_JSONCPP', language: 'cpp')
|
||||
conf_data.set('jsoncpp_lib', ', jsoncpp')
|
||||
@@ -192,15 +209,22 @@ if get_option('python').require(
|
||||
get_option('indexation').enabled(),
|
||||
error_message: 'python requires indexation',
|
||||
).allowed()
|
||||
py = import('python').find_installation()
|
||||
py = import('python').find_installation(pure: false)
|
||||
dep_py = py.dependency()
|
||||
py.extension_module(
|
||||
'opendht',
|
||||
'_core',
|
||||
'python/opendht.pyx',
|
||||
subdir: 'opendht',
|
||||
cython_args: ['--module-name', 'opendht._core'],
|
||||
override_options: ['cython_language=cpp'],
|
||||
dependencies: [opendht_dep, msgpack, jsoncpp, fmt, dep_py],
|
||||
install: true,
|
||||
)
|
||||
py.install_sources(
|
||||
'python/opendht/__init__.py',
|
||||
'python/opendht/aio.py',
|
||||
subdir: 'opendht',
|
||||
)
|
||||
endif
|
||||
|
||||
if get_option('tools').enabled()
|
||||
@@ -239,6 +263,15 @@ endif
|
||||
|
||||
# Unit tests
|
||||
if get_option('tests').enabled()
|
||||
if get_option('c').enabled()
|
||||
test_c = executable(
|
||||
'test_c',
|
||||
'tests/test_c.c',
|
||||
include_directories: ['c', opendht_interface_inc],
|
||||
link_with: opendht_c,
|
||||
)
|
||||
test('CBindings', test_c)
|
||||
endif
|
||||
cppunit = dependency('cppunit')
|
||||
|
||||
test_infohash = executable(
|
||||
@@ -275,6 +308,24 @@ if get_option('tests').enabled()
|
||||
)
|
||||
test('NetworkEngine', test_networkengine)
|
||||
|
||||
test_opcache = executable(
|
||||
'test_opcache',
|
||||
'tests/test_opcache.cpp',
|
||||
'tests/tests_runner.cpp',
|
||||
include_directories: opendht_inc,
|
||||
dependencies: [opendht_dep, cppunit, jsoncpp, fmt, openssl, msgpack],
|
||||
)
|
||||
test('OpCache', test_opcache)
|
||||
|
||||
test_valuecache = executable(
|
||||
'test_valuecache',
|
||||
'tests/test_valuecache.cpp',
|
||||
'tests/tests_runner.cpp',
|
||||
include_directories: opendht_inc,
|
||||
dependencies: [opendht_dep, cppunit, jsoncpp, fmt, openssl, msgpack],
|
||||
)
|
||||
test('ValueCache', test_valuecache)
|
||||
|
||||
test_crypto = executable(
|
||||
'test_crypto',
|
||||
'tests/test_crypto.cpp',
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
diff --git a/SMP/libgnutls.vcxproj.filters b/SMP/libgnutls.vcxproj.filters
|
||||
index b182189..91ee160 100644
|
||||
--- a/SMP/libgnutls.vcxproj.filters
|
||||
+++ b/SMP/libgnutls.vcxproj.filters
|
||||
@@ -103,9 +103,6 @@
|
||||
<Filter Include="Header Files\lib\inih">
|
||||
<UniqueIdentifier>{ae0c3eeb-53df-4c72-a85a-6b46de35e7ba}</UniqueIdentifier>
|
||||
</Filter>
|
||||
- <Filter Include="Source Files\libtasn1">
|
||||
- <UniqueIdentifier>{32be60b7-8c65-486e-9df5-7e529443cf07}</UniqueIdentifier>
|
||||
- </Filter>
|
||||
<Filter Include="Source Files\lib\nettle\gost">
|
||||
<UniqueIdentifier>{1f3549a8-3c3d-475f-8fd6-20451d336464}</UniqueIdentifier>
|
||||
</Filter>
|
||||
@@ -1553,30 +1550,6 @@
|
||||
<ClCompile Include="..\lib\nettle\sysrng-bcrypt.c">
|
||||
<Filter>Source Files\lib\nettle</Filter>
|
||||
</ClCompile>
|
||||
- <ClCompile Include="..\devel\libtasn1\lib\coding.c">
|
||||
- <Filter>Source Files\libtasn1</Filter>
|
||||
- </ClCompile>
|
||||
- <ClCompile Include="..\devel\libtasn1\lib\decoding.c">
|
||||
- <Filter>Source Files\libtasn1</Filter>
|
||||
- </ClCompile>
|
||||
- <ClCompile Include="..\devel\libtasn1\lib\element.c">
|
||||
- <Filter>Source Files\libtasn1</Filter>
|
||||
- </ClCompile>
|
||||
- <ClCompile Include="..\devel\libtasn1\lib\errors.c">
|
||||
- <Filter>Source Files\libtasn1</Filter>
|
||||
- </ClCompile>
|
||||
- <ClCompile Include="..\devel\libtasn1\lib\gstr.c">
|
||||
- <Filter>Source Files\libtasn1</Filter>
|
||||
- </ClCompile>
|
||||
- <ClCompile Include="..\devel\libtasn1\lib\parser_aux.c">
|
||||
- <Filter>Source Files\libtasn1</Filter>
|
||||
- </ClCompile>
|
||||
- <ClCompile Include="..\devel\libtasn1\lib\structure.c">
|
||||
- <Filter>Source Files\libtasn1</Filter>
|
||||
- </ClCompile>
|
||||
- <ClCompile Include="..\devel\libtasn1\lib\version.c">
|
||||
- <Filter>Source Files\libtasn1</Filter>
|
||||
- </ClCompile>
|
||||
<ClCompile Include="..\lib\nettle\gost\acpkm.c">
|
||||
<Filter>Source Files\lib\nettle\gost</Filter>
|
||||
</ClCompile>
|
||||
diff --git a/SMP/libgnutls_files.props b/SMP/libgnutls_files.props
|
||||
index adcb074..2317c2a 100644
|
||||
--- a/SMP/libgnutls_files.props
|
||||
+++ b/SMP/libgnutls_files.props
|
||||
@@ -179,13 +179,6 @@
|
||||
<ItemGroup>
|
||||
<ClCompile Include="lib\gnutls_asn1_tab.c" />
|
||||
<ClCompile Include="lib\pkix_asn1_tab.c" />
|
||||
- <ClCompile Include="..\devel\libtasn1\lib\coding.c" />
|
||||
- <ClCompile Include="..\devel\libtasn1\lib\decoding.c" />
|
||||
- <ClCompile Include="..\devel\libtasn1\lib\element.c" />
|
||||
- <ClCompile Include="..\devel\libtasn1\lib\gstr.c" />
|
||||
- <ClCompile Include="..\devel\libtasn1\lib\parser_aux.c" />
|
||||
- <ClCompile Include="..\devel\libtasn1\lib\structure.c" />
|
||||
- <ClCompile Include="..\devel\libtasn1\lib\version.c" />
|
||||
<ClCompile Include="..\gnulib\lib\c-strcasecmp.c" />
|
||||
<ClCompile Include="..\gnulib\lib\c-strncasecmp.c" />
|
||||
<ClCompile Include="..\gnulib\lib\cloexec.c" />
|
||||
@@ -572,9 +565,6 @@
|
||||
<ClCompile Include="..\gnulib\lib\readdir.c">
|
||||
<ForcedIncludeFiles>dirent-private.h;%(ForcedIncludeFiles)</ForcedIncludeFiles>
|
||||
</ClCompile>
|
||||
- <ClCompile Include="..\devel\libtasn1\lib\errors.c">
|
||||
- <ObjectFileName>$(IntDir)\tasn1_$(filename).obj</ObjectFileName>
|
||||
- </ClCompile>
|
||||
<ClCompile Include="..\lib\algorithms\ecc.c">
|
||||
<ObjectFileName>$(IntDir)\alg_%(Filename).obj</ObjectFileName>
|
||||
</ClCompile>
|
||||
diff --git a/SMP/libgnutls_winrt.vcxproj.filters b/SMP/libgnutls_winrt.vcxproj.filters
|
||||
index 014a408..ae5ecff 100644
|
||||
--- a/SMP/libgnutls_winrt.vcxproj.filters
|
||||
+++ b/SMP/libgnutls_winrt.vcxproj.filters
|
||||
@@ -103,9 +103,6 @@
|
||||
<Filter Include="Header Files\lib\inih">
|
||||
<UniqueIdentifier>{ae0c3eeb-53df-4c72-a85a-6b46de35e7ba}</UniqueIdentifier>
|
||||
</Filter>
|
||||
- <Filter Include="Source Files\libtasn1">
|
||||
- <UniqueIdentifier>{32be60b7-8c65-486e-9df5-7e529443cf07}</UniqueIdentifier>
|
||||
- </Filter>
|
||||
<Filter Include="Header Files\lib\nettle\gost">
|
||||
<UniqueIdentifier>{4fada990-3138-4089-a6c7-ae722a0e7fe9}</UniqueIdentifier>
|
||||
</Filter>
|
||||
@@ -1553,30 +1550,6 @@
|
||||
<ClCompile Include="..\lib\nettle\sysrng-bcrypt.c">
|
||||
<Filter>Source Files\lib\nettle</Filter>
|
||||
</ClCompile>
|
||||
- <ClCompile Include="..\devel\libtasn1\lib\coding.c">
|
||||
- <Filter>Source Files\libtasn1</Filter>
|
||||
- </ClCompile>
|
||||
- <ClCompile Include="..\devel\libtasn1\lib\decoding.c">
|
||||
- <Filter>Source Files\libtasn1</Filter>
|
||||
- </ClCompile>
|
||||
- <ClCompile Include="..\devel\libtasn1\lib\element.c">
|
||||
- <Filter>Source Files\libtasn1</Filter>
|
||||
- </ClCompile>
|
||||
- <ClCompile Include="..\devel\libtasn1\lib\errors.c">
|
||||
- <Filter>Source Files\libtasn1</Filter>
|
||||
- </ClCompile>
|
||||
- <ClCompile Include="..\devel\libtasn1\lib\gstr.c">
|
||||
- <Filter>Source Files\libtasn1</Filter>
|
||||
- </ClCompile>
|
||||
- <ClCompile Include="..\devel\libtasn1\lib\parser_aux.c">
|
||||
- <Filter>Source Files\libtasn1</Filter>
|
||||
- </ClCompile>
|
||||
- <ClCompile Include="..\devel\libtasn1\lib\structure.c">
|
||||
- <Filter>Source Files\libtasn1</Filter>
|
||||
- </ClCompile>
|
||||
- <ClCompile Include="..\devel\libtasn1\lib\version.c">
|
||||
- <Filter>Source Files\libtasn1</Filter>
|
||||
- </ClCompile>
|
||||
<ClCompile Include="..\lib\accelerated\afalg.c">
|
||||
<Filter>Source Files\lib\accelerated</Filter>
|
||||
</ClCompile>
|
||||
@@ -0,0 +1,12 @@
|
||||
--- a/lib/gnutls.pc.in
|
||||
+++ b/lib/gnutls.pc.in
|
||||
@@ -18,7 +18,7 @@
|
||||
Name: GnuTLS
|
||||
Description: Transport Security Layer implementation for the GNU system
|
||||
URL: https://www.gnutls.org/
|
||||
Version: @VERSION@
|
||||
-Libs: -L${libdir} -lgnutls
|
||||
+Libs: -L${libdir} @GNUTLS_LIBS@
|
||||
Libs.private: @LIBINTL@ @LIBSOCKET@ @INET_PTON_LIB@ @LIBPTHREAD@ @LIB_SELECT@ @TSS_LIBS@ @GMP_LIBS@ @LIBUNISTRING@ @LIBATOMIC_LIBS@ @GNUTLS_LIBS_PRIVATE@
|
||||
@GNUTLS_REQUIRES_PRIVATE@
|
||||
Cflags: -I${includedir}
|
||||
@@ -0,0 +1,187 @@
|
||||
set(GNULIB_REF "1cc0125a28cba4e8503d853bf99b854fe4dd454c")
|
||||
|
||||
vcpkg_from_github(
|
||||
OUT_SOURCE_PATH SOURCE_PATH
|
||||
REPO savoirfairelinux/gnutls
|
||||
REF 300b0c721fc1809f3d191b7f9e289c0267eabd01
|
||||
SHA512 8a74fbf0343e658f6321da0f28bcc05c580fb7b950f6edfb6a8fdaad6d814456ed2dbf94e8dd330c7740c836f01e04552e649d529ee29424c3641861c41344bf
|
||||
HEAD_REF fix/ci-msvc
|
||||
PATCHES
|
||||
external-libtasn1.patch
|
||||
pkgconfig.patch
|
||||
)
|
||||
|
||||
file(REMOVE_RECURSE "${SOURCE_PATH}/devel/perlasm")
|
||||
|
||||
vcpkg_download_distfile(
|
||||
GNULIB_SNAPSHOT
|
||||
URLS "https://gitlab.com/libidn/gnulib-mirror/-/archive/${GNULIB_REF}/gnulib-mirror-${GNULIB_REF}.tar.gz"
|
||||
FILENAME "gnulib-mirror-${GNULIB_REF}.tar.gz"
|
||||
SHA512 340539bd7b8e30b1e0f81a15fb207960e9ae3c4ef515f39f1d1bdb43ed52ad94a29c6a6ce8bbca3484ba5392e1dae7447c2f6b58124209614e884345e13914f5
|
||||
)
|
||||
|
||||
vcpkg_extract_source_archive(
|
||||
GNULIB_SOURCE_PATH
|
||||
ARCHIVE "${GNULIB_SNAPSHOT}"
|
||||
SOURCE_BASE ${GNULIB_REF}
|
||||
)
|
||||
|
||||
file(REMOVE_RECURSE "${SOURCE_PATH}/gnulib")
|
||||
file(RENAME "${GNULIB_SOURCE_PATH}" "${SOURCE_PATH}/gnulib")
|
||||
|
||||
include("${CURRENT_HOST_INSTALLED_DIR}/share/yasm-tool-helper/yasm-tool-helper.cmake")
|
||||
yasm_tool_helper(OUT_VAR YASM)
|
||||
file(TO_NATIVE_PATH "${YASM}" YASM)
|
||||
|
||||
if(VCPKG_LIBRARY_LINKAGE STREQUAL "dynamic")
|
||||
set(CONFIGURATION_RELEASE ReleaseDLL)
|
||||
set(CONFIGURATION_DEBUG DebugDLL)
|
||||
else()
|
||||
set(CONFIGURATION_RELEASE Release)
|
||||
set(CONFIGURATION_DEBUG Debug)
|
||||
endif()
|
||||
|
||||
if(VCPKG_TARGET_IS_UWP)
|
||||
string(APPEND CONFIGURATION_RELEASE WinRT)
|
||||
string(APPEND CONFIGURATION_DEBUG WinRT)
|
||||
endif()
|
||||
|
||||
set(_gnutlsproject "${SOURCE_PATH}/SMP/libgnutls.vcxproj")
|
||||
file(READ "${_gnutlsproject}" _contents)
|
||||
string(REPLACE [[<Import Project="$(VCTargetsPath)\BuildCustomizations\yasm.props" />]]
|
||||
"<Import Project=\"${CURRENT_HOST_INSTALLED_DIR}/share/vs-yasm/yasm.props\" />"
|
||||
_contents "${_contents}")
|
||||
string(REPLACE [[<Import Project="$(VCTargetsPath)\BuildCustomizations\yasm.targets" />]]
|
||||
"<Import Project=\"${CURRENT_HOST_INSTALLED_DIR}/share/vs-yasm/yasm.targets\" />"
|
||||
_contents "${_contents}")
|
||||
string(REGEX REPLACE "${VCPKG_ROOT_DIR}/installed/[^/]+/share" "${CURRENT_HOST_INSTALLED_DIR}/share" _contents "${_contents}") # Above already
|
||||
file(WRITE "${_gnutlsproject}" "${_contents}")
|
||||
|
||||
if(VCPKG_CRT_LINKAGE STREQUAL "static")
|
||||
set(RuntimeLibraryExt "")
|
||||
else()
|
||||
set(RuntimeLibraryExt "DLL")
|
||||
endif()
|
||||
|
||||
# patch output library file path and name
|
||||
foreach(PROPS IN ITEMS
|
||||
"${SOURCE_PATH}/SMP/smp_deps.props"
|
||||
"${SOURCE_PATH}/SMP/smp_winrt_deps.props")
|
||||
vcpkg_replace_string(
|
||||
"${PROPS}"
|
||||
[=[_winrt</TargetName>]=]
|
||||
[=[</TargetName>]=]
|
||||
IGNORE_UNCHANGED
|
||||
)
|
||||
vcpkg_replace_string(
|
||||
"${PROPS}"
|
||||
[=[<TargetName>lib$(RootNamespace)]=]
|
||||
[=[<TargetName>$(RootNamespace)]=]
|
||||
)
|
||||
endforeach()
|
||||
|
||||
# patch hogweed, gmp, nettle, zlib libraries file names; inject RuntimeLibrary property to control CRT linkage
|
||||
foreach(VCXPROJ IN ITEMS
|
||||
"${SOURCE_PATH}/SMP/libgnutls.vcxproj"
|
||||
"${SOURCE_PATH}/SMP/libgnutls_winrt.vcxproj")
|
||||
vcpkg_replace_string(
|
||||
"${VCXPROJ}"
|
||||
"_winrt.lib"
|
||||
".lib"
|
||||
IGNORE_UNCHANGED
|
||||
)
|
||||
vcpkg_replace_string(
|
||||
"${VCXPROJ}"
|
||||
"libhogweed"
|
||||
"hogweed"
|
||||
)
|
||||
vcpkg_replace_string(
|
||||
"${VCXPROJ}"
|
||||
"hogweedd"
|
||||
"hogweed"
|
||||
)
|
||||
vcpkg_replace_string(
|
||||
"${VCXPROJ}"
|
||||
"libgmp"
|
||||
"gmp"
|
||||
)
|
||||
vcpkg_replace_string(
|
||||
"${VCXPROJ}"
|
||||
"gmpd"
|
||||
"gmp"
|
||||
)
|
||||
vcpkg_replace_string(
|
||||
"${VCXPROJ}"
|
||||
"libnettle"
|
||||
"nettle"
|
||||
)
|
||||
vcpkg_replace_string(
|
||||
"${VCXPROJ}"
|
||||
"nettled"
|
||||
"nettle"
|
||||
)
|
||||
set(zlib_basename "zlib")
|
||||
if(EXISTS "${CURRENT_INSTALLED_DIR}/lib/zs.lib")
|
||||
set(zlib_basename "zs")
|
||||
elseif(EXISTS "${CURRENT_INSTALLED_DIR}/lib/z.lib")
|
||||
set(zlib_basename "z")
|
||||
endif()
|
||||
vcpkg_replace_string(
|
||||
"${VCXPROJ}"
|
||||
"libzlib"
|
||||
"${zlib_basename}"
|
||||
)
|
||||
vcpkg_replace_string(
|
||||
"${VCXPROJ}"
|
||||
"zlib"
|
||||
"${zlib_basename}"
|
||||
)
|
||||
vcpkg_replace_string(
|
||||
"${VCXPROJ}"
|
||||
[=[</DisableSpecificWarnings>]=]
|
||||
[=[</DisableSpecificWarnings><ForcedIncludeFiles>gnutls_msvc_fixes.h;%(ForcedIncludeFiles)</ForcedIncludeFiles><RuntimeLibrary>$(RuntimeLibrary)</RuntimeLibrary>]=]
|
||||
)
|
||||
endforeach()
|
||||
|
||||
vcpkg_install_msbuild(
|
||||
USE_VCPKG_INTEGRATION
|
||||
SOURCE_PATH "${SOURCE_PATH}"
|
||||
PROJECT_SUBPATH SMP/libgnutls.sln
|
||||
PLATFORM ${TRIPLET_SYSTEM_ARCH}
|
||||
LICENSE_SUBPATH LICENSE
|
||||
RELEASE_CONFIGURATION ${CONFIGURATION_RELEASE}
|
||||
DEBUG_CONFIGURATION ${CONFIGURATION_DEBUG}
|
||||
SKIP_CLEAN
|
||||
OPTIONS /p:YasmPath="${YASM}" /p:OutDir=..\\msvc
|
||||
OPTIONS_DEBUG /p:RuntimeLibrary=MultiThreadedDebug${RuntimeLibraryExt}
|
||||
OPTIONS_RELEASE /p:RuntimeLibrary=MultiThreaded${RuntimeLibraryExt}
|
||||
)
|
||||
|
||||
get_filename_component(SOURCE_PATH_SUFFIX "${SOURCE_PATH}" NAME)
|
||||
if(VCPKG_TARGET_IS_UWP)
|
||||
set(WINRT_SUBFOLDER libgnutls_winrt)
|
||||
endif()
|
||||
file(INSTALL "${CURRENT_BUILDTREES_DIR}/${TARGET_TRIPLET}-rel/${SOURCE_PATH_SUFFIX}/msvc/${WINRT_SUBFOLDER}/include" DESTINATION "${CURRENT_PACKAGES_DIR}")
|
||||
|
||||
set(GNUTLS_REQUIRES_PRIVATE "Requires.private: gmp, nettle, hogweed, libtasn1, zlib")
|
||||
set(GNUTLS_LIBS_PRIVATE "-lcrypt32 -lws2_32 -lkernel32 -lncrypt")
|
||||
|
||||
set(prefix "${CURRENT_INSTALLED_DIR}")
|
||||
set(exec_prefix "\${prefix}")
|
||||
set(libdir "\${prefix}/lib")
|
||||
set(includedir "\${prefix}/include")
|
||||
set(GNUTLS_LIBS "-lgnutls")
|
||||
configure_file("${SOURCE_PATH}/lib/gnutls.pc.in" "${CURRENT_PACKAGES_DIR}/lib/pkgconfig/gnutls.pc" @ONLY)
|
||||
|
||||
if(NOT VCPKG_BUILD_TYPE)
|
||||
set(prefix "${CURRENT_INSTALLED_DIR}/debug")
|
||||
set(exec_prefix "\${prefix}")
|
||||
set(libdir "\${prefix}/lib")
|
||||
set(includedir "\${prefix}/../include")
|
||||
set(GNUTLS_LIBS "-lgnutlsd")
|
||||
configure_file("${SOURCE_PATH}/lib/gnutls.pc.in" "${CURRENT_PACKAGES_DIR}/debug/lib/pkgconfig/gnutls.pc" @ONLY)
|
||||
endif()
|
||||
|
||||
vcpkg_fixup_pkgconfig()
|
||||
vcpkg_copy_pdbs()
|
||||
file(COPY "${CURRENT_PORT_DIR}/vcpkg-cmake-wrapper.cmake" DESTINATION "${CURRENT_PACKAGES_DIR}/share/gnutls")
|
||||
@@ -0,0 +1,2 @@
|
||||
find_library(GNUTLS_LIBRARY NAMES gnutls gnutlsd NAMES_PER_DIR)
|
||||
_find_package(${ARGS})
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "shiftmedia-libgnutls",
|
||||
"version": "3.8.13",
|
||||
"port-version": 0,
|
||||
"description": "GnuTLS fork maintained by Savoir-faire Linux with native Visual Studio project build tools.",
|
||||
"homepage": "https://github.com/savoirfairelinux/gnutls",
|
||||
"license": "LGPL-2.1-only",
|
||||
"supports": "windows & !arm & !mingw & !xbox",
|
||||
"dependencies": [
|
||||
"gmp",
|
||||
"libtasn1",
|
||||
"nettle",
|
||||
{
|
||||
"name": "vs-yasm",
|
||||
"host": true
|
||||
},
|
||||
{
|
||||
"name": "yasm-tool-helper",
|
||||
"host": true
|
||||
},
|
||||
"zlib"
|
||||
]
|
||||
}
|
||||
@@ -78,6 +78,177 @@ async def dht_async_demo(key_str: str):
|
||||
asyncio.run(dht_async_demo("unique_key"))
|
||||
```
|
||||
|
||||
## Node State Persistence
|
||||
|
||||
Set the state file path before starting a node:
|
||||
|
||||
```python
|
||||
import threading
|
||||
import opendht as dht
|
||||
|
||||
config = dht.DhtConfig()
|
||||
config.setPersistPath("dht-state.msgpack")
|
||||
node = dht.DhtRunner()
|
||||
node.run(config=config)
|
||||
stopped = threading.Event()
|
||||
node.shutdown(stopped.set)
|
||||
stopped.wait()
|
||||
node.join()
|
||||
```
|
||||
|
||||
The node loads its saved DHT node ID, known peers, and stored values on startup,
|
||||
and saves state during `shutdown()`. Wait for the shutdown callback before
|
||||
calling `join()` or reusing the state path; `join()` alone does not save state.
|
||||
Port hints are
|
||||
stored in a sibling file named `<path>_port.txt`; saved ports are reused when
|
||||
available. The parent directory must already exist and be writable. Use a
|
||||
different state path for each node.
|
||||
|
||||
Persistence is disabled by default. `config.setPersistPath("")` disables it
|
||||
explicitly. This state does not include private keys or certificates; persist
|
||||
cryptographic identities separately as described below.
|
||||
|
||||
## Identity Persistence
|
||||
|
||||
Keys can be imported from PEM or DER bytes. Private keys export PKCS#8 PEM;
|
||||
public keys export DER. Certificates export their complete PEM chain.
|
||||
|
||||
```python
|
||||
import opendht as dht
|
||||
|
||||
identity = dht.Identity.generate("my-node")
|
||||
|
||||
private_key_pem = identity.key.serialize(password)
|
||||
certificate_pem = bytes(identity.certificate)
|
||||
public_key_der = bytes(identity.publickey)
|
||||
|
||||
restored = dht.Identity(
|
||||
dht.PrivateKey(private_key_pem, password),
|
||||
dht.Certificate(certificate_pem),
|
||||
)
|
||||
public_key = dht.PublicKey(public_key_der)
|
||||
node = dht.DhtRunner()
|
||||
node.run(id=restored)
|
||||
```
|
||||
|
||||
Here `password` is an application-supplied string. Save the exported bytes using
|
||||
your application's storage, protect private key files, and keep the password
|
||||
separate from the key. `bytes(private_key)` and `private_key.serialize()` without
|
||||
a password export **unencrypted** private keys. Empty or malformed key data,
|
||||
incorrect passwords, and passwords containing NUL characters raise exceptions.
|
||||
Exporting an uninitialized key raises `ValueError`.
|
||||
|
||||
## Value Metadata
|
||||
|
||||
The following properties are readable and writable on `Value`, including values
|
||||
returned by `get()` and `listen()`:
|
||||
|
||||
| Property | Type | Meaning |
|
||||
|----------|------|---------|
|
||||
| `type` | unsigned 16-bit integer | Native value type identifier |
|
||||
| `seq` | unsigned 16-bit integer | Sequence number for signed value updates |
|
||||
| `priority` | unsigned integer | Push priority: `0` high, `1` normal |
|
||||
| `pushType` | string | Push notification type, such as `audioCall` |
|
||||
|
||||
```python
|
||||
value = dht.Value(b"some binary data")
|
||||
value.type = 42
|
||||
value.seq = 1
|
||||
value.priority = 1
|
||||
value.pushType = "audioCall"
|
||||
```
|
||||
|
||||
Set metadata before publishing the value. Numeric values outside the native
|
||||
integer range raise `OverflowError`.
|
||||
|
||||
`value.isSigned()` reports whether a public key and signature are present;
|
||||
it does not verify the signature. `value.isEncrypted()` reports whether the
|
||||
value currently contains ciphertext. Values delivered by `get()` and `listen()`
|
||||
are transparently decrypted when possible, so decrypted values return `False`
|
||||
from `isEncrypted()`, even if they were published using `putEncrypted()`.
|
||||
An uninitialized `PublicKey()` cannot be used as an encryption recipient and
|
||||
raises `ValueError`.
|
||||
|
||||
Register custom `ValueType` instances after `run()` and before `join()`.
|
||||
`registerType()` raises `RuntimeError` when the runner is stopped, `TypeError`
|
||||
for `None`, and `ValueError` for an uninitialized type.
|
||||
|
||||
## Observability
|
||||
|
||||
`getNodeInfo()` returns a read-only `NodeInfo` snapshot with node identifiers,
|
||||
IPv4/IPv6 routing statistics, bound ports, pending-operation count, and local
|
||||
and remote storage counters. `getNodesStats(socket.AF_INET)` and
|
||||
`getNodesStats(socket.AF_INET6)` return `NodeStats` snapshots. Snapshots remain
|
||||
valid after the runner stops. `getPublicAddress()` returns known public
|
||||
`SockAddr` objects; pass an address family to restrict the results, or use
|
||||
`getPublicAddressStr()` for strings. These lists may be empty before discovery.
|
||||
`getNodeMessageStats(incoming=True)` selects incoming message counters.
|
||||
|
||||
```python
|
||||
info = node.getNodeInfo()
|
||||
print(info.node_id, info.bound4, info.ipv4.good_nodes, info.storage_size)
|
||||
print(node.getPublicAddressStr())
|
||||
|
||||
node.setOnStatusChanged(lambda ipv4, ipv6: print(ipv4.name, ipv6.name))
|
||||
node.setOnStatusChanged(None)
|
||||
```
|
||||
|
||||
The status callback receives two `NodeStatus` values: `Disconnected`,
|
||||
`Connecting`, or `Connected`. Registration replaces the previous callback and
|
||||
persists across restarts; `None` disables it. Core callbacks run on the DHT
|
||||
thread while native locks are held: do not call snapshot getters, type
|
||||
registration, logging controls, `run()`, blocking operations, or `join()` from
|
||||
them. The asyncio wrapper schedules status callbacks on its event-loop thread,
|
||||
where snapshot getters are safe to use.
|
||||
|
||||
Call `node.connectivityChanged()` after a network-interface or IP-address
|
||||
change. It queues native connectivity recovery, including recontacting peers
|
||||
and renewing subscriptions. It is also available on the asyncio runner as a
|
||||
synchronous, nonblocking method.
|
||||
|
||||
## Queries And Filtered Listeners
|
||||
|
||||
`query()` retrieves metadata rather than complete value payloads. Each row is
|
||||
a dictionary keyed by `ValueField`. Network projections support `Id`,
|
||||
`ValueType`, `OwnerPk`, and `UserType`; values are integers, an `InfoHash` owner
|
||||
fingerprint, or a UTF-8 string respectively. Explicit sequence-number
|
||||
projections are not supported by the native wire format.
|
||||
|
||||
```python
|
||||
selection = dht.Select().field(dht.ValueField.Id).field(dht.ValueField.UserType)
|
||||
query = dht.Query()
|
||||
query.buildFrom(selection, dht.Where().userType("record"))
|
||||
rows = node.query(key, query=query)
|
||||
|
||||
query = dht.Query("SELECT id,user_type WHERE user_type=record")
|
||||
node.query(key, query_cb=lambda row: print(row) or True,
|
||||
done_cb=lambda ok, nodes: print(ok), query=query)
|
||||
```
|
||||
|
||||
Without an explicit query, all four supported metadata fields are selected.
|
||||
Supplying `query_cb` makes the core call nonblocking; returning `False` stops
|
||||
delivery. The blocking core API returns the collected rows, as `get()` does.
|
||||
The asyncio API offers `query()` as a context-managed async iterator and
|
||||
`await queryAll()` as a collector; these raise `RuntimeError` on failure.
|
||||
|
||||
```python
|
||||
rows = await node.queryAll(key, query=query)
|
||||
with node.query(key, query=query) as results:
|
||||
async for row in results:
|
||||
print(row[dht.ValueField.Id])
|
||||
```
|
||||
|
||||
Both core and asyncio `listen()` accept optional `where` and `filter` arguments.
|
||||
`Where` is sent to the DHT to narrow the subscription; the Python predicate
|
||||
runs locally. Existing two-argument core listeners are unchanged.
|
||||
|
||||
```python
|
||||
token = node.listen(key, lambda value, expired: True,
|
||||
where=dht.Where().userType("record"),
|
||||
filter=lambda value: len(value.data) < 1024)
|
||||
node.cancelListen(token)
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
For more information, examples and documentation, or to repport issues, visit: https://opendht.net
|
||||
|
||||
+450
-79
@@ -12,15 +12,21 @@ from libcpp.map cimport map as map
|
||||
from libcpp cimport bool
|
||||
from libcpp.utility cimport pair
|
||||
from libcpp.string cimport string
|
||||
from libcpp.memory cimport shared_ptr
|
||||
from libcpp.memory cimport shared_ptr, unique_ptr
|
||||
|
||||
from cython.operator cimport dereference as deref, preincrement as inc, predecrement as dec
|
||||
from cpython cimport ref
|
||||
from datetime import timedelta
|
||||
from enum import IntEnum as _IntEnum
|
||||
from operator import index as _index
|
||||
|
||||
cimport opendht_cpp as cpp
|
||||
|
||||
ctypedef cpp.Pht* _PhtPtr
|
||||
|
||||
import threading
|
||||
import socket as _socket
|
||||
import weakref as _weakref
|
||||
|
||||
cdef inline void _initialize_gnutls():
|
||||
cdef int err = cpp.gnutls_global_init()
|
||||
@@ -39,6 +45,58 @@ _initialize_gnutls()
|
||||
def version() -> str:
|
||||
return cpp.version().decode()
|
||||
|
||||
class NodeStatus(_IntEnum):
|
||||
Disconnected = 0
|
||||
Connecting = 1
|
||||
Connected = 2
|
||||
|
||||
class ValueField(_IntEnum):
|
||||
Id = 1
|
||||
ValueType = 2
|
||||
OwnerPk = 3
|
||||
SeqNum = 4
|
||||
UserType = 5
|
||||
|
||||
class _StatusCallbackState:
|
||||
__slots__ = ("callback", "__weakref__")
|
||||
def __init__(self):
|
||||
self.callback = None
|
||||
|
||||
cdef inline void status_callback(int ipv4, int ipv6, void* user_data) noexcept with gil:
|
||||
state = (<object>user_data)()
|
||||
if state is None:
|
||||
return
|
||||
callback = state.callback
|
||||
if callback is not None:
|
||||
callback(NodeStatus(ipv4), NodeStatus(ipv6))
|
||||
|
||||
cdef inline bool query_callback(shared_ptr[cpp.FieldValueIndex] value, void* user_data) noexcept with gil:
|
||||
cdef cpp.Blob blob
|
||||
row = {}
|
||||
for entry in value.get().index:
|
||||
field = ValueField(<int>entry.first)
|
||||
if field == ValueField.OwnerPk:
|
||||
owner = InfoHash()
|
||||
owner._infohash = entry.second.getHash()
|
||||
row[field] = owner
|
||||
elif field == ValueField.UserType:
|
||||
blob = entry.second.getBlob()
|
||||
row[field] = (<char*>blob.data())[:blob.size()].decode()
|
||||
else:
|
||||
row[field] = entry.second.getInt()
|
||||
return (<object>user_data)['query'](row)
|
||||
|
||||
cdef inline void query_done_callback(bool done, cpp.vector[shared_ptr[cpp.Node]]* nodes, void* user_data) noexcept with gil:
|
||||
callback = (<object>user_data)['done']
|
||||
if callback is not None:
|
||||
entries = []
|
||||
for node in deref(nodes):
|
||||
entry = NodeEntry()
|
||||
entry._v.first = node.get().getId()
|
||||
entry._v.second = node
|
||||
entries.append(entry)
|
||||
callback(done, entries)
|
||||
|
||||
cdef inline void lookup_callback(cpp.vector[cpp.shared_ptr[cpp.IndexValue]]* values, cpp.Prefix* p, void *user_data) noexcept with gil:
|
||||
cbs = <object>user_data
|
||||
if 'lookup' in cbs and cbs['lookup']:
|
||||
@@ -51,9 +109,11 @@ cdef inline void lookup_callback(cpp.vector[cpp.shared_ptr[cpp.IndexValue]]* val
|
||||
|
||||
cdef inline void shutdown_callback(void* user_data) noexcept with gil:
|
||||
cbs = <object>user_data
|
||||
if 'shutdown' in cbs and cbs['shutdown']:
|
||||
cbs['shutdown']()
|
||||
ref.Py_DECREF(cbs)
|
||||
try:
|
||||
if 'shutdown' in cbs and cbs['shutdown']:
|
||||
cbs['shutdown']()
|
||||
finally:
|
||||
ref.Py_DECREF(cbs)
|
||||
|
||||
cdef inline bool get_callback(shared_ptr[cpp.Value] value, void *user_data) noexcept with gil:
|
||||
cbs = <object>user_data
|
||||
@@ -72,22 +132,26 @@ cdef inline bool value_callback(shared_ptr[cpp.Value] value, bool expired, void
|
||||
return cb(pv, expired) if not f or f(pv) else True
|
||||
|
||||
cdef inline void done_callback(bool done, cpp.vector[shared_ptr[cpp.Node]]* nodes, void *user_data) noexcept with gil:
|
||||
node_ids = []
|
||||
for n in deref(nodes):
|
||||
h = NodeEntry()
|
||||
h._v.first = n.get().getId()
|
||||
h._v.second = n
|
||||
node_ids.append(h)
|
||||
cbs = <object>user_data
|
||||
if 'done' in cbs and cbs['done']:
|
||||
cbs['done'](done, node_ids)
|
||||
ref.Py_DECREF(cbs)
|
||||
try:
|
||||
node_ids = []
|
||||
for n in deref(nodes):
|
||||
h = NodeEntry()
|
||||
h._v.first = n.get().getId()
|
||||
h._v.second = n
|
||||
node_ids.append(h)
|
||||
if 'done' in cbs and cbs['done']:
|
||||
cbs['done'](done, node_ids)
|
||||
finally:
|
||||
ref.Py_DECREF(cbs)
|
||||
|
||||
cdef inline void done_callback_simple(bool done, void *user_data) noexcept with gil:
|
||||
cbs = <object>user_data
|
||||
if 'done' in cbs and cbs['done']:
|
||||
cbs['done'](done)
|
||||
ref.Py_DECREF(cbs)
|
||||
try:
|
||||
if 'done' in cbs and cbs['done']:
|
||||
cbs['done'](done)
|
||||
finally:
|
||||
ref.Py_DECREF(cbs)
|
||||
|
||||
cdef class _WithID(object):
|
||||
def __repr__(self):
|
||||
@@ -101,6 +165,12 @@ cdef class _WithPk(object):
|
||||
def __str__(self):
|
||||
return self.getLongId().toString().decode()
|
||||
|
||||
cdef inline unsigned _checked_bit_index(bit, unsigned bit_count) except *:
|
||||
bit = _index(bit)
|
||||
if bit < 0 or bit >= bit_count:
|
||||
raise IndexError("bit index out of range")
|
||||
return bit
|
||||
|
||||
cdef class InfoHash(_WithID):
|
||||
cdef cpp.InfoHash _infohash
|
||||
def __cinit__(self, bytes str=b''):
|
||||
@@ -116,9 +186,9 @@ cdef class InfoHash(_WithID):
|
||||
return self._infohash == other._infohash
|
||||
return NotImplemented
|
||||
def getBit(InfoHash self, bit):
|
||||
return self._infohash.getBit(bit)
|
||||
return self._infohash.getBit(_checked_bit_index(bit, 160))
|
||||
def setBit(InfoHash self, bit, b):
|
||||
self._infohash.setBit(bit, b)
|
||||
self._infohash.setBit(_checked_bit_index(bit, 160), b)
|
||||
def getId(InfoHash self):
|
||||
return self
|
||||
def toString(InfoHash self):
|
||||
@@ -154,9 +224,9 @@ cdef class PkId(_WithPk):
|
||||
return self._pkid == other._pkid
|
||||
return NotImplemented
|
||||
def getBit(PkId self, bit):
|
||||
return self._pkid.getBit(bit)
|
||||
return self._pkid.getBit(_checked_bit_index(bit, 256))
|
||||
def setBit(PkId self, bit, b):
|
||||
self._pkid.setBit(bit, b)
|
||||
self._pkid.setBit(_checked_bit_index(bit, 256), b)
|
||||
def getLongId(PkId self):
|
||||
return self
|
||||
def toString(PkId self):
|
||||
@@ -212,14 +282,92 @@ cdef class SockAddr(object):
|
||||
cdef class Node(_WithID):
|
||||
cdef shared_ptr[cpp.Node] _node
|
||||
def getId(self):
|
||||
if not self._node:
|
||||
raise ValueError("Node is not initialized")
|
||||
h = InfoHash()
|
||||
h._infohash = self._node.get().getId()
|
||||
return h
|
||||
def getAddr(self):
|
||||
if not self._node:
|
||||
raise ValueError("Node is not initialized")
|
||||
return self._node.get().getAddrStr()
|
||||
def isExpired(self):
|
||||
if not self._node:
|
||||
raise ValueError("Node is not initialized")
|
||||
return self._node.get().isExpired()
|
||||
|
||||
cdef class NodeStats(object):
|
||||
cdef cpp.NodeStats _stats
|
||||
@property
|
||||
def good_nodes(self):
|
||||
return self._stats.good_nodes
|
||||
@property
|
||||
def dubious_nodes(self):
|
||||
return self._stats.dubious_nodes
|
||||
@property
|
||||
def cached_nodes(self):
|
||||
return self._stats.cached_nodes
|
||||
@property
|
||||
def incoming_nodes(self):
|
||||
return self._stats.incoming_nodes
|
||||
@property
|
||||
def table_depth(self):
|
||||
return self._stats.table_depth
|
||||
@property
|
||||
def searches(self):
|
||||
return self._stats.searches
|
||||
@property
|
||||
def node_cache_size(self):
|
||||
return self._stats.node_cache_size
|
||||
def getKnownNodes(self):
|
||||
return self._stats.getKnownNodes()
|
||||
def getNetworkSizeEstimation(self):
|
||||
return self._stats.getNetworkSizeEstimation()
|
||||
|
||||
cdef class NodeInfo(object):
|
||||
cdef cpp.NodeInfo _info
|
||||
@property
|
||||
def id(self):
|
||||
result = InfoHash()
|
||||
result._infohash = self._info.id
|
||||
return result
|
||||
@property
|
||||
def node_id(self):
|
||||
result = InfoHash()
|
||||
result._infohash = self._info.node_id
|
||||
return result
|
||||
@property
|
||||
def ipv4(self):
|
||||
result = NodeStats()
|
||||
result._stats = self._info.ipv4
|
||||
return result
|
||||
@property
|
||||
def ipv6(self):
|
||||
result = NodeStats()
|
||||
result._stats = self._info.ipv6
|
||||
return result
|
||||
@property
|
||||
def ongoing_ops(self):
|
||||
return self._info.ongoing_ops
|
||||
@property
|
||||
def storage_values(self):
|
||||
return self._info.storage_values
|
||||
@property
|
||||
def storage_size(self):
|
||||
return self._info.storage_size
|
||||
@property
|
||||
def local_storage_values(self):
|
||||
return self._info.local_storage_values
|
||||
@property
|
||||
def local_storage_size(self):
|
||||
return self._info.local_storage_size
|
||||
@property
|
||||
def bound4(self):
|
||||
return self._info.bound4
|
||||
@property
|
||||
def bound6(self):
|
||||
return self._info.bound6
|
||||
|
||||
cdef class NodeEntry(_WithID):
|
||||
cdef cpp.pair[cpp.InfoHash, shared_ptr[cpp.Node]] _v
|
||||
def getId(self):
|
||||
@@ -253,8 +401,11 @@ cdef class Select(object):
|
||||
return self._select.toString().decode()
|
||||
def isSatisfiedBy(self, Select os):
|
||||
return self._select.isSatisfiedBy(os._select)
|
||||
def field(self, int field):
|
||||
self._select.field(<cpp.Field> field)
|
||||
def field(self, field):
|
||||
cdef int field_id = ValueField(_index(field))
|
||||
if field_id == ValueField.SeqNum:
|
||||
raise ValueError("The native wire format does not support sequence-number projections")
|
||||
self._select.field(<cpp.Field>field_id)
|
||||
return self
|
||||
|
||||
cdef class Where(object):
|
||||
@@ -290,6 +441,10 @@ cdef class Value(object):
|
||||
self._value.reset(new cpp.Value(id, val, len(val)))
|
||||
def __str__(self):
|
||||
return self._value.get().toString().decode()
|
||||
def isSigned(self) -> bool:
|
||||
return self._value.get().isSigned()
|
||||
def isEncrypted(self) -> bool:
|
||||
return self._value.get().isEncrypted()
|
||||
@property
|
||||
def owner(self) -> PublicKey:
|
||||
h = PublicKey()
|
||||
@@ -324,25 +479,51 @@ cdef class Value(object):
|
||||
def id(self, cpp.uint64_t value):
|
||||
self._value.get().id = value
|
||||
@property
|
||||
def type(self) -> int:
|
||||
return self._value.get().type
|
||||
@type.setter
|
||||
def type(self, cpp.uint16_t value):
|
||||
self._value.get().type = value
|
||||
@property
|
||||
def seq(self) -> int:
|
||||
return self._value.get().seq
|
||||
@seq.setter
|
||||
def seq(self, cpp.uint16_t value):
|
||||
self._value.get().seq = value
|
||||
@property
|
||||
def priority(self) -> int:
|
||||
return self._value.get().priority
|
||||
@priority.setter
|
||||
def priority(self, unsigned value):
|
||||
self._value.get().priority = value
|
||||
@property
|
||||
def pushType(self) -> str:
|
||||
return self._value.get().pushType.decode()
|
||||
@pushType.setter
|
||||
def pushType(self, str value):
|
||||
self._value.get().pushType = value.encode()
|
||||
@property
|
||||
def size(self) -> int:
|
||||
return self._value.get().size()
|
||||
|
||||
cdef class ValueType(object):
|
||||
cdef cpp.ValueType * _value
|
||||
cdef unique_ptr[cpp.ValueType] _value
|
||||
def __init__(self, cpp.uint16_t id, str name, expiration: timedelta):
|
||||
if not isinstance(expiration, timedelta):
|
||||
raise TypeError("expiration argument must be of type timedelta")
|
||||
cdef cpp.seconds duration = cpp.seconds(int(expiration.total_seconds()))
|
||||
self._value = new cpp.ValueType(id, name.encode(), duration)
|
||||
self._value.reset(new cpp.ValueType(id, name.encode(), duration))
|
||||
|
||||
cdef class NodeSetIter(object):
|
||||
cdef map[cpp.InfoHash, shared_ptr[cpp.Node]]* _nodes
|
||||
cdef NodeSet _owner
|
||||
cdef map[cpp.InfoHash, shared_ptr[cpp.Node]].iterator _curIter
|
||||
def __init__(self, NodeSet s):
|
||||
self._nodes = &s._nodes
|
||||
self._curIter = self._nodes.begin()
|
||||
def __init__(self, NodeSet s not None):
|
||||
self._owner = s
|
||||
self._curIter = self._owner._nodes.begin()
|
||||
def __iter__(self):
|
||||
return self
|
||||
def __next__(self):
|
||||
if self._curIter == self._nodes.end():
|
||||
if self._curIter == self._owner._nodes.end():
|
||||
raise StopIteration
|
||||
h = NodeEntry()
|
||||
h._v = deref(self._curIter)
|
||||
@@ -374,6 +555,8 @@ cdef class NodeSet(object):
|
||||
s = ''
|
||||
cdef map[cpp.InfoHash, shared_ptr[cpp.Node]].iterator it = self._nodes.begin()
|
||||
while it != self._nodes.end():
|
||||
if not deref(it).second:
|
||||
raise ValueError("Node is not initialized")
|
||||
s += deref(it).first.toString().decode() + ' ' + deref(it).second.get().getAddrStr().decode() + '\n'
|
||||
inc(it)
|
||||
return s
|
||||
@@ -383,6 +566,30 @@ cdef class NodeSet(object):
|
||||
cdef class PrivateKey(_WithPk):
|
||||
cdef shared_ptr[cpp.PrivateKey] _key
|
||||
|
||||
def __init__(self, bytes data=None, str password=""):
|
||||
cdef cpp.Blob encoded
|
||||
if '\0' in password:
|
||||
raise ValueError("Password must not contain NUL characters")
|
||||
if data is not None:
|
||||
if not data:
|
||||
raise ValueError("Private key data must not be empty")
|
||||
encoded = data
|
||||
self._key = cpp.make_shared[cpp.PrivateKey](encoded, <cpp.string>password.encode())
|
||||
|
||||
def serialize(self, str password="") -> bytes:
|
||||
"""Export PKCS#8 PEM, encrypted when a nonempty password is supplied."""
|
||||
if not self._key:
|
||||
raise ValueError("PrivateKey is not initialized")
|
||||
if '\0' in password:
|
||||
raise ValueError("Password must not contain NUL characters")
|
||||
cdef cpp.Blob encoded = self._key.get().serialize(password.encode())
|
||||
if encoded.empty():
|
||||
raise RuntimeError("Could not export private key")
|
||||
return (<char*>encoded.data())[:encoded.size()]
|
||||
|
||||
def __bytes__(self):
|
||||
return self.serialize()
|
||||
|
||||
def getId(self) -> InfoHash:
|
||||
if not self._key:
|
||||
raise ValueError("PrivateKey is not initialized")
|
||||
@@ -405,6 +612,8 @@ cdef class PrivateKey(_WithPk):
|
||||
return pk
|
||||
|
||||
def decrypt(self, bytes dat) -> bytes:
|
||||
if not self._key:
|
||||
raise ValueError("PrivateKey is not initialized")
|
||||
cdef size_t d_len = len(dat)
|
||||
cdef cpp.uint8_t* d_ptr = <cpp.uint8_t*>dat
|
||||
cdef cpp.Blob indat
|
||||
@@ -439,6 +648,25 @@ cdef class PrivateKey(_WithPk):
|
||||
|
||||
cdef class PublicKey(_WithPk):
|
||||
cdef cpp.shared_ptr[cpp.PublicKey] _key
|
||||
def __init__(self, bytes data=None):
|
||||
cdef cpp.Blob encoded
|
||||
if data is not None:
|
||||
if not data:
|
||||
raise ValueError("Public key data must not be empty")
|
||||
encoded = data
|
||||
self._key = cpp.make_shared[cpp.PublicKey](encoded)
|
||||
|
||||
def serialize(self) -> bytes:
|
||||
"""Export the public key as DER bytes."""
|
||||
if not self._key:
|
||||
raise ValueError("PublicKey is not initialized")
|
||||
cdef cpp.Blob encoded
|
||||
self._key.get().pack(encoded)
|
||||
return (<char*>encoded.data())[:encoded.size()]
|
||||
|
||||
def __bytes__(self):
|
||||
return self.serialize()
|
||||
|
||||
def getId(self) -> InfoHash:
|
||||
if not self._key:
|
||||
raise ValueError("PublicKey is not initialized")
|
||||
@@ -500,11 +728,17 @@ cdef class Certificate(_WithPk):
|
||||
return ""
|
||||
return self._cert.get().toString().decode()
|
||||
def getName(self) -> str:
|
||||
if not self._cert:
|
||||
raise ValueError("Certificate is not initialized")
|
||||
return self._cert.get().getName().decode()
|
||||
def revoke(self, PrivateKey k, Certificate c):
|
||||
if not self._cert or c is None or not c._cert:
|
||||
raise ValueError("Certificate is not initialized")
|
||||
if k is None or not k._key:
|
||||
raise ValueError("PrivateKey is not initialized")
|
||||
self._cert.get().revoke(deref(k._key.get()), deref(c._cert.get()));
|
||||
def __bytes__(self):
|
||||
return self._cert.get().toString().encode() if self._cert else b''
|
||||
return self._cert.get().toString() if self._cert else b''
|
||||
@property
|
||||
def issuer(self) -> Certificate|None:
|
||||
if self._cert and self._cert.get().issuer:
|
||||
@@ -514,6 +748,10 @@ cdef class Certificate(_WithPk):
|
||||
return None
|
||||
@staticmethod
|
||||
def generate(PrivateKey k, str name, Identity i = Identity(), bool is_ca = False):
|
||||
if k is None or not k._key:
|
||||
raise ValueError("PrivateKey is not initialized")
|
||||
if i is None:
|
||||
i = Identity()
|
||||
c = Certificate()
|
||||
c._cert = cpp.make_shared[cpp.Certificate](cpp.Certificate.generate(deref(k._key.get()), name.encode(), i._id, is_ca))
|
||||
return c
|
||||
@@ -528,10 +766,16 @@ cdef class VerifyResult(object):
|
||||
cdef class TrustList(object):
|
||||
cdef cpp.TrustList _trust
|
||||
def add(self, Certificate cert):
|
||||
if cert is None or not cert._cert:
|
||||
raise ValueError("Certificate is not initialized")
|
||||
self._trust.add(deref(cert._cert.get()))
|
||||
def remove(self, Certificate cert):
|
||||
if cert is None or not cert._cert:
|
||||
raise ValueError("Certificate is not initialized")
|
||||
self._trust.remove(deref(cert._cert.get()))
|
||||
def verify(self, Certificate cert):
|
||||
if cert is None or not cert._cert:
|
||||
raise ValueError("Certificate is not initialized")
|
||||
r = VerifyResult()
|
||||
r._result = self._trust.verify(deref(cert._cert.get()))
|
||||
return r
|
||||
@@ -539,7 +783,7 @@ cdef class TrustList(object):
|
||||
cdef class ListenToken(object):
|
||||
cdef cpp.InfoHash _h
|
||||
cdef cpp.shared_future[size_t] _t
|
||||
_cb = dict()
|
||||
cdef bool _cancelled
|
||||
|
||||
cdef class Identity(object):
|
||||
cdef cpp.Identity _id
|
||||
@@ -550,6 +794,8 @@ cdef class Identity(object):
|
||||
self._id.second = c._cert
|
||||
@staticmethod
|
||||
def generate(str name = "pydht", Identity ca = Identity(), unsigned bits = 4096):
|
||||
if ca is None:
|
||||
ca = Identity()
|
||||
i = Identity()
|
||||
i._id = cpp.generateIdentity(name.encode(), ca._id, bits)
|
||||
return i
|
||||
@@ -610,6 +856,13 @@ cdef class DhtConfig(object):
|
||||
self._config.dht_config.node_config.network = netid
|
||||
def setMaintainStorage(self, bool maintain_storage):
|
||||
self._config.dht_config.node_config.maintain_storage = maintain_storage
|
||||
def setPersistPath(self, str path):
|
||||
"""Set the native state file path; an empty string disables persistence."""
|
||||
if path is None:
|
||||
raise TypeError("path must be a string")
|
||||
if '\0' in path:
|
||||
raise ValueError("path must not contain NUL characters")
|
||||
self._config.dht_config.node_config.persist_path = path.encode()
|
||||
def setRateLimit(self, ssize_t max_req_per_sec, ssize_t max_peer_req_per_sec):
|
||||
self._config.dht_config.node_config.max_req_per_sec = max_req_per_sec
|
||||
self._config.dht_config.node_config.max_peer_req_per_sec = max_peer_req_per_sec
|
||||
@@ -630,24 +883,65 @@ cdef class DhtConfig(object):
|
||||
|
||||
cdef class DhtRunner(_WithID):
|
||||
cdef cpp.shared_ptr[cpp.DhtRunner] thisptr
|
||||
cdef map[_PhtPtr, shared_ptr[cpp.Pht]] _indexes
|
||||
cdef object _status_callback
|
||||
def __cinit__(self):
|
||||
self.thisptr.reset(new cpp.DhtRunner())
|
||||
self._status_callback = _StatusCallbackState()
|
||||
def __dealloc__(self):
|
||||
with nogil:
|
||||
if self.thisptr:
|
||||
self.thisptr.get().join()
|
||||
self._indexes.clear()
|
||||
self.thisptr.reset()
|
||||
def getId(self) -> InfoHash:
|
||||
h = InfoHash()
|
||||
if self.thisptr:
|
||||
h._infohash = self.thisptr.get().getId()
|
||||
with nogil:
|
||||
h._infohash = self.thisptr.get().getId()
|
||||
return h
|
||||
def getPublicKey(self) -> PublicKey:
|
||||
if not self.thisptr:
|
||||
raise ValueError("DhtRunner is not initialized")
|
||||
pk = PublicKey()
|
||||
pk._key = self.thisptr.get().getPublicKey()
|
||||
with nogil:
|
||||
pk._key = self.thisptr.get().getPublicKey()
|
||||
return pk
|
||||
def getNodeId(self) -> InfoHash:
|
||||
h = InfoHash()
|
||||
if self.thisptr:
|
||||
h._infohash = self.thisptr.get().getNodeId()
|
||||
with nogil:
|
||||
h._infohash = self.thisptr.get().getNodeId()
|
||||
return h
|
||||
def getNodeInfo(self) -> NodeInfo:
|
||||
result = NodeInfo()
|
||||
with nogil:
|
||||
result._info = self.thisptr.get().getNodeInfo()
|
||||
return result
|
||||
def getNodesStats(self, af=_socket.AF_INET) -> NodeStats:
|
||||
if af not in (_socket.AF_INET, _socket.AF_INET6):
|
||||
raise ValueError("af must be AF_INET or AF_INET6")
|
||||
info = self.getNodeInfo()
|
||||
return info.ipv4 if af == _socket.AF_INET else info.ipv6
|
||||
def getPublicAddress(self, cpp.sa_family_t af=0):
|
||||
if af not in (0, _socket.AF_INET, _socket.AF_INET6):
|
||||
raise ValueError("af must be AF_UNSPEC, AF_INET or AF_INET6")
|
||||
cdef cpp.vector[cpp.SockAddr] addresses
|
||||
with nogil:
|
||||
addresses = self.thisptr.get().getPublicAddress(af)
|
||||
result = []
|
||||
for address in addresses:
|
||||
value = SockAddr()
|
||||
value._addr = address
|
||||
result.append(value)
|
||||
return result
|
||||
def getPublicAddressStr(self, cpp.sa_family_t af=0):
|
||||
return [str(address) for address in self.getPublicAddress(af)]
|
||||
def setOnStatusChanged(self, callback=None):
|
||||
"""Replace the DHT-thread status callback, or disable it with None."""
|
||||
if callback is not None and not callable(callback):
|
||||
raise TypeError("callback must be callable or None")
|
||||
self._status_callback.callback = callback
|
||||
def ping(self, SockAddr addr, done_cb=None):
|
||||
if done_cb:
|
||||
cb_obj = {'done':done_cb}
|
||||
@@ -673,44 +967,80 @@ cdef class DhtRunner(_WithID):
|
||||
host_bytes = host.encode()
|
||||
port_bytes = port.encode() if port else b'4222'
|
||||
self.thisptr.get().bootstrap(<cpp.const_char*>host_bytes, <cpp.const_char*>port_bytes)
|
||||
def run(self, Identity id=None, is_bootstrap=False, cpp.in_port_t port=0, str ipv4="", str ipv6="", DhtConfig config=DhtConfig()):
|
||||
if id:
|
||||
config.setIdentity(id)
|
||||
def connectivityChanged(self):
|
||||
with nogil:
|
||||
self.thisptr.get().connectivityChanged()
|
||||
def run(self, Identity id=None, is_bootstrap=False, cpp.in_port_t port=0, str ipv4="", str ipv6="", DhtConfig config=None):
|
||||
cdef cpp.DhtRunnerConfig run_config = config._config if config is not None else cpp.DhtRunnerConfig()
|
||||
cdef cpp.string bind4, bind6, service
|
||||
cdef object status_ref = _weakref.ref(self._status_callback)
|
||||
if id is not None:
|
||||
run_config.dht_config.id = id._id
|
||||
if is_bootstrap:
|
||||
run_config.dht_config.node_config.is_bootstrap = True
|
||||
if ipv4 or ipv6:
|
||||
bind4 = ipv4.encode() if ipv4 else b''
|
||||
bind6 = ipv6.encode() if ipv6 else b''
|
||||
self.thisptr.get().run(bind4, bind6, str(port).encode(), config._config)
|
||||
service = str(port).encode()
|
||||
with nogil:
|
||||
cpp.runPythonRunnerAddress(deref(self.thisptr.get()), bind4.c_str(),
|
||||
bind6.c_str(), service.c_str(), run_config,
|
||||
status_callback, status_ref)
|
||||
else:
|
||||
self.thisptr.get().run(port, config._config)
|
||||
with nogil:
|
||||
cpp.runPythonRunner(deref(self.thisptr.get()), port, run_config,
|
||||
status_callback, status_ref)
|
||||
def join(self):
|
||||
self.thisptr.get().join()
|
||||
with nogil:
|
||||
self.thisptr.get().join()
|
||||
self._indexes.clear()
|
||||
def shutdown(self, shutdown_cb=None):
|
||||
cb_obj = {'shutdown':shutdown_cb}
|
||||
ref.Py_INCREF(cb_obj)
|
||||
self.thisptr.get().shutdown(cpp.bindShutdownCb(shutdown_callback, <void*>cb_obj))
|
||||
def registerType(self, ValueType type):
|
||||
self.thisptr.get().registerType(deref(type._value))
|
||||
def registerType(self, ValueType type not None):
|
||||
if not type._value:
|
||||
raise ValueError("ValueType is not initialized")
|
||||
cdef cpp.ValueType run_type = deref(type._value.get())
|
||||
with nogil:
|
||||
self.thisptr.get().registerType(run_type)
|
||||
def enableLogging(self):
|
||||
cpp.enableLogging(self.thisptr.get()[0])
|
||||
with nogil:
|
||||
cpp.enableLogging(self.thisptr.get()[0])
|
||||
def disableLogging(self):
|
||||
cpp.disableLogging(self.thisptr.get()[0])
|
||||
with nogil:
|
||||
cpp.disableLogging(self.thisptr.get()[0])
|
||||
def enableFileLogging(self, str path):
|
||||
cpp.enableFileLogging(self.thisptr.get()[0], path.encode())
|
||||
cdef cpp.string log_path = path.encode()
|
||||
with nogil:
|
||||
cpp.enableFileLogging(self.thisptr.get()[0], log_path)
|
||||
def isRunning(self):
|
||||
return self.thisptr.get().isRunning()
|
||||
def getBound(self, cpp.sa_family_t af = 0):
|
||||
s = SockAddr()
|
||||
s._addr = self.thisptr.get().getBound(af)
|
||||
with nogil:
|
||||
s._addr = self.thisptr.get().getBound(af)
|
||||
return s
|
||||
def getStorageLog(self):
|
||||
return self.thisptr.get().getStorageLog().decode()
|
||||
cdef cpp.string result
|
||||
with nogil:
|
||||
result = self.thisptr.get().getStorageLog()
|
||||
return result.decode()
|
||||
def getRoutingTablesLog(self, cpp.sa_family_t af):
|
||||
return self.thisptr.get().getRoutingTablesLog(af).decode()
|
||||
cdef cpp.string result
|
||||
with nogil:
|
||||
result = self.thisptr.get().getRoutingTablesLog(af)
|
||||
return result.decode()
|
||||
def getSearchesLog(self, cpp.sa_family_t af):
|
||||
return self.thisptr.get().getSearchesLog(af).decode()
|
||||
def getNodeMessageStats(self):
|
||||
cdef cpp.string result
|
||||
with nogil:
|
||||
result = self.thisptr.get().getSearchesLog(af)
|
||||
return result.decode()
|
||||
def getNodeMessageStats(self, bool incoming=False):
|
||||
stats = []
|
||||
cdef cpp.vector[unsigned] res = self.thisptr.get().getNodeMessageStats(False)
|
||||
cdef cpp.vector[unsigned] res
|
||||
with nogil:
|
||||
res = self.thisptr.get().getNodeMessageStats(incoming)
|
||||
for n in res:
|
||||
stats.append(n)
|
||||
return stats
|
||||
@@ -753,6 +1083,25 @@ cdef class DhtRunner(_WithID):
|
||||
lock.wait()
|
||||
return res
|
||||
|
||||
def query(self, InfoHash key not None, query_cb=None, done_cb=None, Query query=None):
|
||||
"""Return projected metadata rows, or stream them through query_cb."""
|
||||
if query is None:
|
||||
query = Query("SELECT id,value_type,owner_pk,user_type")
|
||||
if query_cb is None:
|
||||
completed = threading.Event()
|
||||
rows = []
|
||||
def on_row(row):
|
||||
rows.append(row)
|
||||
return True
|
||||
self.query(key, on_row, lambda ok, nodes: completed.set(), query)
|
||||
completed.wait()
|
||||
return rows
|
||||
if not callable(query_cb) or (done_cb is not None and not callable(done_cb)):
|
||||
raise TypeError("query callbacks must be callable")
|
||||
callbacks = {'query': query_cb, 'done': done_cb}
|
||||
cpp.queryPython(deref(self.thisptr.get()), key._infohash, query_callback,
|
||||
query_done_callback, callbacks, query._query)
|
||||
|
||||
def put(self, InfoHash key, Value val, done_cb=None, permanent=False):
|
||||
"""Publish a new value on the DHT at key.
|
||||
|
||||
@@ -776,7 +1125,7 @@ cdef class DhtRunner(_WithID):
|
||||
lock.notify()
|
||||
with lock:
|
||||
pending += 1
|
||||
self.put(key, val, done_cb=tmp_done)
|
||||
self.put(key, val, done_cb=tmp_done, permanent=permanent)
|
||||
while pending > 0:
|
||||
lock.wait()
|
||||
return ok
|
||||
@@ -798,7 +1147,7 @@ cdef class DhtRunner(_WithID):
|
||||
lock.notify()
|
||||
with lock:
|
||||
pending += 1
|
||||
self.putSigned(key, val, done_cb=tmp_done)
|
||||
self.putSigned(key, val, done_cb=tmp_done, permanent=permanent)
|
||||
while pending > 0:
|
||||
lock.wait()
|
||||
return ok
|
||||
@@ -806,6 +1155,8 @@ cdef class DhtRunner(_WithID):
|
||||
def putEncrypted(self, InfoHash key, Value val, to, done_cb=None, bool permanent=False):
|
||||
if to is None:
|
||||
raise ValueError("'to' must be provided")
|
||||
if isinstance(to, PublicKey) and not (<PublicKey>to)._key:
|
||||
raise ValueError("PublicKey is not initialized")
|
||||
if done_cb:
|
||||
cb_obj = {'done':done_cb}
|
||||
ref.Py_INCREF(cb_obj)
|
||||
@@ -845,19 +1196,23 @@ cdef class DhtRunner(_WithID):
|
||||
def cancelPut(self, InfoHash key, Value val):
|
||||
self.thisptr.get().cancelPut(key._infohash, val._value)
|
||||
|
||||
def listen(self, InfoHash key, value_cb):
|
||||
def listen(self, InfoHash key not None, value_cb, filter=None, Where where=None):
|
||||
if not callable(value_cb) or (filter is not None and not callable(filter)):
|
||||
raise TypeError("listener callbacks must be callable")
|
||||
if where is None:
|
||||
where = Where()
|
||||
t = ListenToken()
|
||||
t._h = key._infohash
|
||||
cb_obj = {'valcb':value_cb}
|
||||
t._cb['cb'] = cb_obj
|
||||
# avoid the callback being destructed if the token is destroyed
|
||||
ref.Py_INCREF(cb_obj)
|
||||
t._t = self.thisptr.get().listen(t._h, cpp.bindValueCb(value_callback, <void*>cb_obj)).share()
|
||||
cb_obj = {'valcb':value_cb, 'filter':filter}
|
||||
t._t = self.thisptr.get().listen(t._h, cpp.bindPythonValueCb(value_callback, cb_obj),
|
||||
cpp.nullptr, where._where).share()
|
||||
return t
|
||||
def cancelListen(self, ListenToken token):
|
||||
self.thisptr.get().cancelListen(token._h, token._t)
|
||||
ref.Py_DECREF(<object>token._cb['cb'])
|
||||
# fixme: not thread safe
|
||||
if token is None:
|
||||
raise TypeError("token must be a ListenToken")
|
||||
if not token._cancelled and token._t.valid():
|
||||
self.thisptr.get().cancelListen(token._h, token._t)
|
||||
token._cancelled = True
|
||||
|
||||
cdef class IndexValue(object):
|
||||
cdef cpp.shared_ptr[cpp.IndexValue] _value
|
||||
@@ -874,12 +1229,18 @@ cdef class IndexValue(object):
|
||||
return self._value.get().second
|
||||
|
||||
cdef class Pht(object):
|
||||
cdef cpp.Pht* thisptr
|
||||
def __cinit__(self, bytes name, key_spec, DhtRunner dht):
|
||||
"""PHT index; active native indexes live until the runner is joined."""
|
||||
cdef shared_ptr[cpp.Pht] thisptr
|
||||
cdef DhtRunner _runner
|
||||
def __cinit__(self, bytes name, key_spec, DhtRunner dht not None):
|
||||
cdef cpp.IndexKeySpec cpp_key_spec
|
||||
for kk, size in key_spec.items():
|
||||
cpp_key_spec[bytes(kk, 'utf-8')] = size
|
||||
self.thisptr = new cpp.Pht(name, cpp_key_spec, dht.thisptr)
|
||||
self._runner = dht
|
||||
self.thisptr.reset(new cpp.Pht(name, cpp_key_spec, dht.thisptr))
|
||||
def __dealloc__(self):
|
||||
with nogil:
|
||||
self.thisptr.reset()
|
||||
@property
|
||||
def MAX_NODE_ENTRY_COUNT(self):
|
||||
return cpp.PHT_MAX_NODE_ENTRY_COUNT
|
||||
@@ -892,16 +1253,21 @@ cdef class Pht(object):
|
||||
representation of the prefix where the value was indexed in
|
||||
the PHT.
|
||||
"""
|
||||
cb_obj = {'lookup':lookup_cb, 'done':done_cb} # TODO: donecallback is to be removed
|
||||
ref.Py_INCREF(cb_obj)
|
||||
cdef cpp.IndexKey cppk
|
||||
for kk, v in key.items():
|
||||
cppk[bytes(kk, 'utf-8')] = bytes(v)
|
||||
self.thisptr.lookup(
|
||||
cppk,
|
||||
cpp.Pht.bindLookupCb(lookup_callback, <void*>cb_obj),
|
||||
cpp.bindDoneCbSimple(done_callback_simple, <void*>cb_obj)
|
||||
)
|
||||
self._runner._indexes[self.thisptr.get()] = self.thisptr
|
||||
cb_obj = {'lookup':lookup_cb, 'done':done_cb} # TODO: donecallback is to be removed
|
||||
ref.Py_INCREF(cb_obj)
|
||||
try:
|
||||
self.thisptr.get().lookup(
|
||||
cppk,
|
||||
cpp.Pht.bindLookupCb(lookup_callback, <void*>cb_obj),
|
||||
cpp.bindDoneCbSimple(done_callback_simple, <void*>cb_obj)
|
||||
)
|
||||
except:
|
||||
ref.Py_DECREF(cb_obj)
|
||||
raise
|
||||
def insert(self, key, IndexValue value, done_cb=None):
|
||||
"""Add an index entry to the Index.
|
||||
|
||||
@@ -909,16 +1275,21 @@ cdef class Pht(object):
|
||||
value -- an IndexValue object describing the indexed value.
|
||||
done_cb -- Called when the operation is completed.
|
||||
"""
|
||||
cb_obj = {'done':done_cb}
|
||||
ref.Py_INCREF(cb_obj)
|
||||
cdef cpp.IndexKey cppk
|
||||
for kk, v in key.items():
|
||||
cppk[bytes(kk, 'utf-8')] = bytes(v)
|
||||
cdef cpp.IndexValue val
|
||||
val.first = (<InfoHash>value.getKey())._infohash
|
||||
val.second = value.getValueId()
|
||||
self.thisptr.insert(
|
||||
cppk,
|
||||
val,
|
||||
cpp.bindDoneCbSimple(done_callback_simple, <void*>cb_obj)
|
||||
)
|
||||
self._runner._indexes[self.thisptr.get()] = self.thisptr
|
||||
cb_obj = {'done':done_cb}
|
||||
ref.Py_INCREF(cb_obj)
|
||||
try:
|
||||
self.thisptr.get().insert(
|
||||
cppk,
|
||||
val,
|
||||
cpp.bindDoneCbSimple(done_callback_simple, <void*>cb_obj)
|
||||
)
|
||||
except:
|
||||
ref.Py_DECREF(cb_obj)
|
||||
raise
|
||||
+83
-24
@@ -3,12 +3,20 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from typing import Any, Callable, Iterable, List, Optional, Tuple
|
||||
from typing import Any, Callable, Generic, Iterable, List, Optional, Tuple, TypeVar
|
||||
|
||||
from . import _core as _dht
|
||||
|
||||
# Re-export core types for convenience
|
||||
from ._core import InfoHash, Value, ValueType, Where, SockAddr, PublicKey, PkId, DhtConfig, Identity
|
||||
from ._core import NodeInfo, NodeStats, NodeStatus, Query, Select, ValueField
|
||||
|
||||
_Result = TypeVar("_Result")
|
||||
|
||||
|
||||
def _set_result_if_pending(future: asyncio.Future[Any], result: Any) -> None:
|
||||
if not future.done():
|
||||
future.set_result(result)
|
||||
|
||||
|
||||
class _Listener:
|
||||
@@ -19,7 +27,11 @@ class _Listener:
|
||||
|
||||
__slots__ = ("_runner", "_token", "_loop", "_queue", "_closed")
|
||||
|
||||
def __init__(self, runner: DhtRunner, key: InfoHash) -> None:
|
||||
def __init__(
|
||||
self, runner: DhtRunner, key: InfoHash,
|
||||
where: Optional[Where] = None,
|
||||
filter: Optional[Callable[[Value], bool]] = None,
|
||||
) -> None:
|
||||
self._runner = runner
|
||||
self._loop = runner._loop
|
||||
self._queue: asyncio.Queue[Tuple[Value, bool]] = asyncio.Queue()
|
||||
@@ -31,7 +43,7 @@ class _Listener:
|
||||
self._loop.call_soon_threadsafe(self._queue.put_nowait, (v, expired))
|
||||
return True # continue listening
|
||||
|
||||
self._token = self._runner._dht.listen(key, _on_value)
|
||||
self._token = self._runner._dht.listen(key, _on_value, filter, where)
|
||||
|
||||
def __enter__(self) -> _Listener:
|
||||
return self
|
||||
@@ -62,10 +74,10 @@ class _Listener:
|
||||
self._queue.put_nowait(None)
|
||||
|
||||
|
||||
class _GetStream:
|
||||
"""Async iterator around DHT get callback.
|
||||
class _GetStream(Generic[_Result]):
|
||||
"""Async iterator around a DHT get or query callback.
|
||||
|
||||
Yields Value instances as they are found, then ends.
|
||||
Yields results as they are found, then ends.
|
||||
Raises on failure.
|
||||
"""
|
||||
__slots__ = ("_runner", "_loop", "_queue", "_closed", "_error")
|
||||
@@ -76,14 +88,15 @@ class _GetStream:
|
||||
key: InfoHash,
|
||||
where: Optional[Where],
|
||||
filter: Optional[Callable[[Value], bool]],
|
||||
query: Optional[Query] = None,
|
||||
) -> None:
|
||||
self._runner = runner
|
||||
self._loop = runner._loop
|
||||
self._queue: asyncio.Queue[Optional[Value]] = asyncio.Queue()
|
||||
self._queue: asyncio.Queue[Optional[_Result]] = asyncio.Queue()
|
||||
self._closed = False
|
||||
self._error: Optional[BaseException] = None
|
||||
|
||||
def _on_value(v: Value) -> bool:
|
||||
def _on_value(v: _Result) -> bool:
|
||||
# Called on DHT thread
|
||||
if self._closed:
|
||||
return False
|
||||
@@ -95,21 +108,24 @@ class _GetStream:
|
||||
if self._closed:
|
||||
return
|
||||
if not ok:
|
||||
self._error = RuntimeError("DHT get failed")
|
||||
self._error = RuntimeError("DHT query failed" if query is not None else "DHT get failed")
|
||||
self._loop.call_soon_threadsafe(self._queue.put_nowait, None)
|
||||
|
||||
self._runner._dht.get(key, _on_value, _on_done, filter, where)
|
||||
if query is None:
|
||||
self._runner._dht.get(key, _on_value, _on_done, filter, where)
|
||||
else:
|
||||
self._runner._dht.query(key, _on_value, _on_done, query)
|
||||
|
||||
def __enter__(self) -> _GetStream:
|
||||
def __enter__(self) -> _GetStream[_Result]:
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc, tb) -> None:
|
||||
self.cancel()
|
||||
|
||||
def __aiter__(self) -> _GetStream:
|
||||
def __aiter__(self) -> _GetStream[_Result]:
|
||||
return self
|
||||
|
||||
async def __anext__(self) -> Value:
|
||||
async def __anext__(self) -> _Result:
|
||||
if self._closed:
|
||||
raise StopAsyncIteration
|
||||
item = await self._queue.get()
|
||||
@@ -132,7 +148,7 @@ class DhtRunner:
|
||||
"""Async wrapper around opendht.DhtRunner.
|
||||
"""
|
||||
|
||||
__slots__ = ("_dht", "_loop", "_config", "_bootstrap")
|
||||
__slots__ = ("_dht", "_loop", "_config", "_bootstrap", "_status_generation")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -149,6 +165,7 @@ class DhtRunner:
|
||||
self._dht = core or _dht.DhtRunner()
|
||||
self._config = config
|
||||
self._bootstrap = list(bootstrap) if bootstrap is not None else None
|
||||
self._status_generation = [0]
|
||||
|
||||
def run(
|
||||
self,
|
||||
@@ -168,9 +185,32 @@ class DhtRunner:
|
||||
def bootstrap(self, host: str, port: Optional[str] = None) -> None:
|
||||
self._dht.bootstrap(host, port)
|
||||
|
||||
def setOnStatusChanged(
|
||||
self, callback: Optional[Callable[[NodeStatus, NodeStatus], None]] = None,
|
||||
) -> None:
|
||||
if callback is not None and not callable(callback):
|
||||
raise TypeError("callback must be callable or None")
|
||||
state = self._status_generation
|
||||
state[0] += 1
|
||||
if callback is None:
|
||||
self._dht.setOnStatusChanged(None)
|
||||
return
|
||||
generation = state[0]
|
||||
loop = self._loop
|
||||
|
||||
def _deliver(ipv4: NodeStatus, ipv6: NodeStatus) -> None:
|
||||
if state[0] == generation:
|
||||
callback(ipv4, ipv6)
|
||||
|
||||
def _on_status(ipv4: NodeStatus, ipv6: NodeStatus) -> None:
|
||||
if not loop.is_closed():
|
||||
loop.call_soon_threadsafe(_deliver, ipv4, ipv6)
|
||||
|
||||
self._dht.setOnStatusChanged(_on_status)
|
||||
|
||||
async def ping(self, addr: SockAddr) -> bool:
|
||||
fut: asyncio.Future[bool] = self._loop.create_future()
|
||||
self._dht.ping(addr, lambda ok: self._loop.call_soon_threadsafe(fut.set_result, bool(ok)))
|
||||
self._dht.ping(addr, lambda ok: self._loop.call_soon_threadsafe(_set_result_if_pending, fut, bool(ok)))
|
||||
return await fut
|
||||
|
||||
def get(
|
||||
@@ -179,9 +219,25 @@ class DhtRunner:
|
||||
*,
|
||||
where: Optional[Where] = None,
|
||||
filter: Optional[Callable[[Value], bool]] = None,
|
||||
):
|
||||
) -> _GetStream[Value]:
|
||||
# Return a streaming iterator of values; it ends on completion and raises on failure.
|
||||
return _GetStream(self, key, where, filter)
|
||||
return _GetStream[Value](self, key, where, filter)
|
||||
|
||||
def query(
|
||||
self, key: InfoHash, *, query: Optional[Query] = None,
|
||||
) -> _GetStream[dict[ValueField, Any]]:
|
||||
if query is None:
|
||||
query = Query("SELECT id,value_type,owner_pk,user_type")
|
||||
return _GetStream[dict[ValueField, Any]](self, key, None, None, query)
|
||||
|
||||
async def queryAll(
|
||||
self, key: InfoHash, *, query: Optional[Query] = None,
|
||||
) -> List[dict[ValueField, Any]]:
|
||||
rows: List[dict[ValueField, Any]] = []
|
||||
with self.query(key, query=query) as stream:
|
||||
async for row in stream:
|
||||
rows.append(row)
|
||||
return rows
|
||||
|
||||
async def getAll(
|
||||
self,
|
||||
@@ -220,7 +276,7 @@ class DhtRunner:
|
||||
fut: asyncio.Future[bool] = self._loop.create_future()
|
||||
|
||||
def _done(ok: bool, _nodes: Iterable[Any]) -> None:
|
||||
self._loop.call_soon_threadsafe(fut.set_result, bool(ok))
|
||||
self._loop.call_soon_threadsafe(_set_result_if_pending, fut, bool(ok))
|
||||
|
||||
self._dht.put(key, val, _done, permanent)
|
||||
return await fut
|
||||
@@ -229,7 +285,7 @@ class DhtRunner:
|
||||
fut: asyncio.Future[bool] = self._loop.create_future()
|
||||
|
||||
def _done(ok: bool, _nodes: Iterable[Any]) -> None:
|
||||
self._loop.call_soon_threadsafe(fut.set_result, bool(ok))
|
||||
self._loop.call_soon_threadsafe(_set_result_if_pending, fut, bool(ok))
|
||||
|
||||
self._dht.putSigned(key, val, _done, permanent)
|
||||
return await fut
|
||||
@@ -245,20 +301,23 @@ class DhtRunner:
|
||||
fut: asyncio.Future[bool] = self._loop.create_future()
|
||||
|
||||
def _done(ok: bool, _nodes: Iterable[Any]) -> None:
|
||||
self._loop.call_soon_threadsafe(fut.set_result, bool(ok))
|
||||
self._loop.call_soon_threadsafe(_set_result_if_pending, fut, bool(ok))
|
||||
|
||||
self._dht.putEncrypted(key, val, to, _done, permanent)
|
||||
return await fut
|
||||
|
||||
def listen(self, key: InfoHash) -> _Listener:
|
||||
return _Listener(self, key)
|
||||
def listen(
|
||||
self, key: InfoHash, *, where: Optional[Where] = None,
|
||||
filter: Optional[Callable[[Value], bool]] = None,
|
||||
) -> _Listener:
|
||||
return _Listener(self, key, where, filter)
|
||||
|
||||
async def cancelListen(self, listener: _Listener) -> None:
|
||||
await listener.cancel()
|
||||
listener.cancel()
|
||||
|
||||
async def shutdown(self) -> None:
|
||||
fut: asyncio.Future[None] = self._loop.create_future()
|
||||
self._dht.shutdown(lambda: self._loop.call_soon_threadsafe(fut.set_result, None))
|
||||
self._dht.shutdown(lambda: self._loop.call_soon_threadsafe(_set_result_if_pending, fut, None))
|
||||
await fut
|
||||
|
||||
async def join(self) -> None:
|
||||
|
||||
+176
-47
@@ -98,49 +98,53 @@ ctypedef vector[uint8_t] Blob
|
||||
|
||||
cdef extern from "opendht/crypto.h" namespace "dht::crypto":
|
||||
ctypedef pair[shared_ptr[PrivateKey], shared_ptr[Certificate]] Identity
|
||||
cdef Identity generateIdentity(string name, Identity ca, unsigned bits)
|
||||
cdef Identity generateIdentity(string name, Identity ca, unsigned bits) except +
|
||||
cdef Blob aesEncrypt(Blob data, string password) except +
|
||||
cdef Blob aesDecrypt(Blob encrypted, string password) except +
|
||||
|
||||
cdef cppclass PrivateKey:
|
||||
PrivateKey()
|
||||
shared_ptr[PublicKey] getSharedPublicKey() const
|
||||
Blob decrypt(Blob data) const
|
||||
Blob sign(Blob data) const
|
||||
PrivateKey() except +
|
||||
PrivateKey(Blob data, string password) except +
|
||||
shared_ptr[PublicKey] getSharedPublicKey() except +
|
||||
Blob serialize(string password) except +
|
||||
Blob decrypt(Blob data) except +
|
||||
Blob sign(Blob data) except +
|
||||
@staticmethod
|
||||
PrivateKey generate()
|
||||
PrivateKey generate() except +
|
||||
@staticmethod
|
||||
PrivateKey generateEC()
|
||||
PrivateKey generateEC() except +
|
||||
|
||||
cdef cppclass PublicKey:
|
||||
PublicKey()
|
||||
InfoHash getId() const
|
||||
PkId getLongId() const
|
||||
Blob encrypt(Blob data) const
|
||||
bool checkSignature(Blob data, Blob signature) const
|
||||
PublicKey() except +
|
||||
PublicKey(Blob data) except +
|
||||
InfoHash getId() except +
|
||||
PkId getLongId() except +
|
||||
void pack(Blob& data) except +
|
||||
Blob encrypt(Blob data) except +
|
||||
bool checkSignature(Blob data, Blob signature) except +
|
||||
bool operator==(PublicKey) const
|
||||
|
||||
cdef cppclass Certificate:
|
||||
Certificate()
|
||||
Certificate(string pem)
|
||||
InfoHash getId() const
|
||||
PkId getLongId() const
|
||||
string toString() const
|
||||
string getName() const
|
||||
void revoke(PrivateKey key, Certificate cert)
|
||||
Certificate() except +
|
||||
Certificate(string pem) except +
|
||||
InfoHash getId() except +
|
||||
PkId getLongId() except +
|
||||
string toString() except +
|
||||
string getName() except +
|
||||
void revoke(PrivateKey key, Certificate cert) except +
|
||||
@staticmethod
|
||||
Certificate generate(PrivateKey key, string name, Identity ca, bool is_ca)
|
||||
Certificate generate(PrivateKey key, string name, Identity ca, bool is_ca) except +
|
||||
shared_ptr[Certificate] issuer
|
||||
|
||||
cdef cppclass TrustList:
|
||||
cppclass VerifyResult:
|
||||
bool operator bool() const
|
||||
bool isValid() const
|
||||
string toString() const
|
||||
TrustList()
|
||||
void add(Certificate)
|
||||
void remove(Certificate)
|
||||
VerifyResult verify(Certificate);
|
||||
string toString() except +
|
||||
TrustList() except +
|
||||
void add(Certificate) except +
|
||||
void remove(Certificate) except +
|
||||
VerifyResult verify(Certificate) except +
|
||||
|
||||
ctypedef TrustList.VerifyResult TrustListVerifyResult
|
||||
|
||||
@@ -151,6 +155,7 @@ cdef extern from "opendht/value.h" namespace "dht::Value":
|
||||
cdef extern from "opendht/value.h" namespace "dht::Value::Field":
|
||||
cdef Field None
|
||||
cdef Field Id
|
||||
cdef Field ValueTypeField "dht::Value::Field::ValueType"
|
||||
cdef Field OwnerPk
|
||||
cdef Field SeqNum
|
||||
cdef Field UserType
|
||||
@@ -163,13 +168,20 @@ cdef extern from "opendht/value.h" namespace "dht":
|
||||
Value(const uint16_t t, const uint8_t* dat_ptr, size_t dat_len) except +
|
||||
string toString() const
|
||||
size_t size() const
|
||||
bool isSigned() const
|
||||
bool isEncrypted() const
|
||||
uint64_t id
|
||||
shared_ptr[PublicKey] owner
|
||||
InfoHash recipient
|
||||
vector[uint8_t] data
|
||||
string user_type
|
||||
uint16_t type
|
||||
uint16_t seq
|
||||
unsigned priority
|
||||
string pushType
|
||||
|
||||
cdef cppclass ValueType:
|
||||
ValueType() except +
|
||||
ValueType(uint16_t id, string name, seconds expiration) except +
|
||||
uint16_t id
|
||||
string name
|
||||
@@ -191,7 +203,7 @@ cdef extern from "opendht/value.h" namespace "dht":
|
||||
|
||||
cdef cppclass Where:
|
||||
Where() except +
|
||||
Where(const string& q_str)
|
||||
Where(const string& q_str) except +
|
||||
bool isSatisfiedBy(const Where& where) const
|
||||
Where& id(uint64_t id)
|
||||
Where& valueType(uint16_t type)
|
||||
@@ -200,6 +212,14 @@ cdef extern from "opendht/value.h" namespace "dht":
|
||||
Where& userType(string user_type)
|
||||
string toString() const
|
||||
|
||||
cdef cppclass FieldValue:
|
||||
uint64_t getInt() const
|
||||
InfoHash getHash() const
|
||||
Blob getBlob() const
|
||||
|
||||
cdef cppclass FieldValueIndex:
|
||||
map[Field, FieldValue] index
|
||||
|
||||
cdef extern from "opendht/node.h" namespace "dht":
|
||||
cdef cppclass Node:
|
||||
Node() except +
|
||||
@@ -208,6 +228,30 @@ cdef extern from "opendht/node.h" namespace "dht":
|
||||
bool isExpired() const
|
||||
|
||||
cdef extern from "opendht/callbacks.h" namespace "dht":
|
||||
cdef cppclass NodeStats:
|
||||
unsigned good_nodes
|
||||
unsigned dubious_nodes
|
||||
unsigned cached_nodes
|
||||
unsigned incoming_nodes
|
||||
unsigned table_depth
|
||||
unsigned searches
|
||||
unsigned node_cache_size
|
||||
unsigned getKnownNodes() const
|
||||
unsigned long getNetworkSizeEstimation() const
|
||||
|
||||
cdef cppclass NodeInfo:
|
||||
InfoHash id
|
||||
InfoHash node_id
|
||||
NodeStats ipv4
|
||||
NodeStats ipv6
|
||||
size_t ongoing_ops
|
||||
size_t storage_values
|
||||
size_t storage_size
|
||||
size_t local_storage_values
|
||||
size_t local_storage_size
|
||||
in_port_t bound4
|
||||
in_port_t bound6
|
||||
|
||||
ctypedef void (*ShutdownCallbackRaw)(void *user_data)
|
||||
ctypedef bool (*GetCallbackRaw)(shared_ptr[Value] values, void *user_data)
|
||||
ctypedef bool (*ValueCallbackRaw)(shared_ptr[Value] values, bool expired, void *user_data)
|
||||
@@ -243,9 +287,84 @@ cdef extern from "opendht/callbacks.h" namespace "dht":
|
||||
Config node_config
|
||||
Identity id
|
||||
|
||||
cdef extern from *:
|
||||
"""
|
||||
#include <Python.h>
|
||||
#include <opendht/dhtrunner.h>
|
||||
|
||||
static std::shared_ptr<PyObject> opendht_python_owner(PyObject* data)
|
||||
{
|
||||
auto state = PyGILState_Ensure();
|
||||
Py_INCREF(data);
|
||||
PyGILState_Release(state);
|
||||
return std::shared_ptr<PyObject>(data, [](PyObject* object) {
|
||||
auto state = PyGILState_Ensure();
|
||||
Py_DECREF(object);
|
||||
PyGILState_Release(state);
|
||||
});
|
||||
}
|
||||
|
||||
static dht::ValueCallback opendht_bind_python_value_cb(
|
||||
dht::ValueCallbackRaw callback, PyObject* data)
|
||||
{
|
||||
auto owner = opendht_python_owner(data);
|
||||
return [callback, owner = std::move(owner)](
|
||||
const std::vector<std::shared_ptr<dht::Value>>& values, bool expired) {
|
||||
for (const auto& value : values)
|
||||
if (!callback(value, expired, owner.get()))
|
||||
return false;
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
static dht::DhtRunner::Context opendht_python_context(
|
||||
void (*callback)(int, int, void*), PyObject* data)
|
||||
{
|
||||
dht::DhtRunner::Context context;
|
||||
auto owner = opendht_python_owner(data);
|
||||
context.statusChangedCallback = [callback, owner = std::move(owner)](
|
||||
dht::NodeStatus ipv4, dht::NodeStatus ipv6) {
|
||||
callback(static_cast<int>(ipv4), static_cast<int>(ipv6), owner.get());
|
||||
};
|
||||
return context;
|
||||
}
|
||||
|
||||
static void opendht_python_run(dht::DhtRunner& runner, in_port_t port,
|
||||
dht::DhtRunner::Config& config, void (*callback)(int, int, void*), PyObject* data)
|
||||
{
|
||||
runner.run(port, config, opendht_python_context(callback, data));
|
||||
}
|
||||
|
||||
static void opendht_python_run(dht::DhtRunner& runner,
|
||||
const char* ipv4, const char* ipv6, const char* port,
|
||||
dht::DhtRunner::Config& config, void (*callback)(int, int, void*), PyObject* data)
|
||||
{
|
||||
runner.run(ipv4, ipv6, port, config, opendht_python_context(callback, data));
|
||||
}
|
||||
|
||||
static void opendht_python_query(dht::DhtRunner& runner, const dht::InfoHash& key,
|
||||
bool (*callback)(std::shared_ptr<dht::FieldValueIndex>, void*),
|
||||
dht::DoneCallbackRaw done, PyObject* data, dht::Query query)
|
||||
{
|
||||
auto owner = opendht_python_owner(data);
|
||||
runner.query(key,
|
||||
[callback, owner](const std::vector<std::shared_ptr<dht::FieldValueIndex>>& rows) {
|
||||
for (const auto& row : rows)
|
||||
if (!callback(row, owner.get()))
|
||||
return false;
|
||||
return true;
|
||||
},
|
||||
[done, owner](bool ok, const std::vector<std::shared_ptr<dht::Node>>& nodes) {
|
||||
done(ok, const_cast<std::vector<std::shared_ptr<dht::Node>>*>(&nodes), owner.get());
|
||||
}, std::move(query));
|
||||
}
|
||||
"""
|
||||
ValueCallback bindPythonValueCb "opendht_bind_python_value_cb"(ValueCallbackRaw callback, object data) except +
|
||||
|
||||
ctypedef future[size_t] ListenToken
|
||||
ctypedef shared_future[size_t] SharedListenToken
|
||||
|
||||
cdef extern from "opendht/dhtrunner.h" namespace "dht":
|
||||
ctypedef future[size_t] ListenToken
|
||||
ctypedef shared_future[size_t] SharedListenToken
|
||||
cdef cppclass DhtRunner:
|
||||
DhtRunner() except +
|
||||
cppclass Config:
|
||||
@@ -264,37 +383,47 @@ cdef extern from "opendht/dhtrunner.h" namespace "dht":
|
||||
SockAddr bind4
|
||||
SockAddr bind6
|
||||
|
||||
InfoHash getId() const
|
||||
shared_ptr[PublicKey] getPublicKey() const
|
||||
InfoHash getNodeId() const
|
||||
InfoHash getId() except + nogil
|
||||
shared_ptr[PublicKey] getPublicKey() except + nogil
|
||||
InfoHash getNodeId() except + nogil
|
||||
NodeInfo getNodeInfo() except + nogil
|
||||
vector[SockAddr] getPublicAddress(sa_family_t af) except + nogil
|
||||
void connectivityChanged() except + nogil
|
||||
void bootstrap(const_char*, const_char*)
|
||||
void bootstrap(const SockAddr&, DoneCallbackSimple done_cb)
|
||||
void run(in_port_t, Config config)
|
||||
void run(const_char*, const_char*, const_char*, Config config)
|
||||
void join()
|
||||
void run(in_port_t, Config config) except +
|
||||
void run(const_char*, const_char*, const_char*, Config config) except +
|
||||
void join() except + nogil
|
||||
void shutdown(ShutdownCallback)
|
||||
void registerType(ValueType& value);
|
||||
void registerType(const ValueType& value) except + nogil
|
||||
bool isRunning()
|
||||
SockAddr getBound(sa_family_t af) const
|
||||
string getStorageLog() const
|
||||
string getRoutingTablesLog(sa_family_t af) const
|
||||
string getSearchesLog(sa_family_t af) const
|
||||
SockAddr getBound(sa_family_t af) except + nogil
|
||||
string getStorageLog() except + nogil
|
||||
string getRoutingTablesLog(sa_family_t af) except + nogil
|
||||
string getSearchesLog(sa_family_t af) except + nogil
|
||||
void get(InfoHash key, GetCallback get_cb, DoneCallback done_cb, nullptr_t f, Where w)
|
||||
void put(InfoHash key, shared_ptr[Value] val, DoneCallback done_cb, time_point created, bool permanent)
|
||||
void putSigned(InfoHash key, shared_ptr[Value] val, DoneCallback done_cb, bool permanent)
|
||||
void putEncrypted(InfoHash key, PkId to, shared_ptr[Value] val, DoneCallback done_cb, bool permanent)
|
||||
void putEncrypted(InfoHash key, shared_ptr[PublicKey] to, shared_ptr[Value] val, DoneCallback done_cb, bool permanent)
|
||||
void cancelPut(InfoHash key, shared_ptr[Value] val)
|
||||
ListenToken listen(InfoHash key, ValueCallback get_cb)
|
||||
void cancelListen(InfoHash key, SharedListenToken token)
|
||||
vector[unsigned] getNodeMessageStats(bool i)
|
||||
ListenToken listen(InfoHash key, ValueCallback get_cb, nullptr_t f, Where w) except +
|
||||
void cancelListen(InfoHash key, SharedListenToken token) except +
|
||||
vector[unsigned] getNodeMessageStats(bool i) except + nogil
|
||||
|
||||
ctypedef DhtRunner.Config DhtRunnerConfig
|
||||
|
||||
cdef extern from *:
|
||||
ctypedef void (*StatusCallbackRaw)(int ipv4, int ipv6, void* data)
|
||||
ctypedef bool (*QueryCallbackRaw)(shared_ptr[FieldValueIndex] row, void* data)
|
||||
void runPythonRunner "opendht_python_run"(DhtRunner& runner, in_port_t port, DhtRunnerConfig& config, StatusCallbackRaw callback, object data) except + nogil
|
||||
void runPythonRunnerAddress "opendht_python_run"(DhtRunner& runner, const_char* ipv4, const_char* ipv6, const_char* port, DhtRunnerConfig& config, StatusCallbackRaw callback, object data) except + nogil
|
||||
void queryPython "opendht_python_query"(DhtRunner& runner, InfoHash key, QueryCallbackRaw callback, DoneCallbackRaw done, object data, Query query) except +
|
||||
|
||||
cdef extern from "opendht/log.h" namespace "dht::log":
|
||||
void enableLogging(DhtRunner& dht)
|
||||
void disableLogging(DhtRunner& dht)
|
||||
void enableFileLogging(DhtRunner& dht, const string& path)
|
||||
void enableLogging(DhtRunner& dht) except + nogil
|
||||
void disableLogging(DhtRunner& dht) except + nogil
|
||||
void enableFileLogging(DhtRunner& dht, const string& path) except + nogil
|
||||
|
||||
cdef extern from "opendht/indexation/pht.h" namespace "dht::indexation":
|
||||
size_t PHT_MAX_NODE_ENTRY_COUNT "dht::indexation::Pht::MAX_NODE_ENTRY_COUNT"
|
||||
@@ -310,8 +439,8 @@ cdef extern from "opendht/indexation/pht.h" namespace "dht::indexation":
|
||||
cppclass LookupCallback:
|
||||
LookupCallback() except +
|
||||
Pht(string, IndexKeySpec, shared_ptr[DhtRunner]) except +
|
||||
void lookup(IndexKey k, LookupCallback cb, DoneCallbackSimple doneCb);
|
||||
void insert(IndexKey k, IndexValue v, DoneCallbackSimple cb)
|
||||
void lookup(IndexKey k, LookupCallback cb, DoneCallbackSimple doneCb) except +
|
||||
void insert(IndexKey k, IndexValue v, DoneCallbackSimple cb) except +
|
||||
@staticmethod
|
||||
LookupCallback bindLookupCb(LookupCallbackRaw cb, void *user_data)
|
||||
|
||||
|
||||
@@ -4,11 +4,11 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[tool.cibuildwheel]
|
||||
build = [
|
||||
'cp3{9,10,11,12,13,14}-manylinux_x86_64',
|
||||
'cp3{10,11,12,13,14}-manylinux_aarch64',
|
||||
'cp3{11,12,13,14}-macosx_x86_64',
|
||||
'cp3{11,12,13,14}-macosx_arm64',
|
||||
'cp3{11,12,13,14}-win_amd64',
|
||||
'cp3{11,12,13,14,15}-manylinux_x86_64',
|
||||
'cp3{11,12,13,14,15}-manylinux_aarch64',
|
||||
'cp3{11,12,13,14,15}-macosx_x86_64',
|
||||
'cp3{11,12,13,14,15}-macosx_arm64',
|
||||
'cp3{11,12,13,14,15}-win_amd64',
|
||||
]
|
||||
|
||||
[tool.cibuildwheel.linux]
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import runpy
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import netifaces
|
||||
|
||||
|
||||
class NodeClusterTester(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
script = Path(__file__).resolve().parents[1] / 'tools' / 'dhtcluster.py'
|
||||
with patch.dict('sys.modules', {'opendht': Mock()}):
|
||||
cls.NodeCluster = runpy.run_path(str(script))['NodeCluster']
|
||||
|
||||
def test_find_ip_interface(self):
|
||||
addresses = {
|
||||
netifaces.AF_INET: [{'addr': '127.0.0.1'}],
|
||||
netifaces.AF_INET6: [{'addr': '::1'}],
|
||||
}
|
||||
with patch('netifaces.ifaddresses', return_value=addresses) as ifaddresses:
|
||||
self.assertEqual(self.NodeCluster.find_ip('lo'), ('127.0.0.1', '::1'))
|
||||
self.assertTrue(ifaddresses.called)
|
||||
self.assertTrue(all(call.args == ('lo',) for call in ifaddresses.call_args_list))
|
||||
|
||||
def test_find_ip_any(self):
|
||||
with patch('netifaces.ifaddresses') as ifaddresses:
|
||||
for iface in (None, '', 'any'):
|
||||
with self.subTest(iface=iface):
|
||||
self.assertEqual(self.NodeCluster.find_ip(iface), ('0.0.0.0', '::0'))
|
||||
ifaddresses.assert_not_called()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
+1039
-5
File diff suppressed because it is too large.
Load diff
@@ -9,6 +9,7 @@ import argparse
|
||||
import logging
|
||||
import logging.handlers
|
||||
|
||||
import netifaces
|
||||
import opendht as dht
|
||||
|
||||
logger = logging.getLogger('dhtcluster')
|
||||
|
||||
+3
-3
@@ -1,8 +1,8 @@
|
||||
[package]
|
||||
name = "opendht"
|
||||
version = "0.1.0"
|
||||
authors = ["Sébastien Blin <sebastien.blin@savoirfairelinux.com>"]
|
||||
edition = "2021"
|
||||
version = "0.2.0"
|
||||
authors = ["Sébastien Blin <sebastien.blin@savoirfairelinux.com>", "Adrien Beraud <adrien.beraud@savoirfairelinux.com>"]
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
libc="0.2"
|
||||
|
||||
+93
-17
@@ -1,21 +1,97 @@
|
||||
# Rust bindings for opendht
|
||||
# Rust Bindings for OpenDHT
|
||||
|
||||
## Build
|
||||
## Build and Test
|
||||
|
||||
Install rust on your system (https://rustup.rs/ is the recommanded way). Once done:
|
||||
Install Rust and the native OpenDHT C library. From this directory:
|
||||
|
||||
```
|
||||
cargo build
|
||||
```
|
||||
|
||||
## Run tests
|
||||
|
||||
```
|
||||
cargo test
|
||||
```
|
||||
|
||||
## Run dht node
|
||||
|
||||
```
|
||||
```sh
|
||||
cargo build --all-targets
|
||||
cargo test --all-targets
|
||||
cargo test --doc
|
||||
cargo run --example dhtnode
|
||||
```
|
||||
```
|
||||
|
||||
To use an uninstalled native build, set its library search paths. For example,
|
||||
from the repository root on macOS:
|
||||
|
||||
```sh
|
||||
cmake -S . -B build-bindings -DOPENDHT_C=ON -DBUILD_TESTING=ON
|
||||
cmake --build build-bindings
|
||||
ctest --test-dir build-bindings -R test_c --output-on-failure
|
||||
LIBRARY_PATH="$PWD/build-bindings" \
|
||||
DYLD_LIBRARY_PATH="$PWD/build-bindings" \
|
||||
RUSTFLAGS="-L native=$PWD/build-bindings" \
|
||||
cargo test --manifest-path rust/Cargo.toml --all-targets
|
||||
```
|
||||
|
||||
On Linux use `LD_LIBRARY_PATH` instead of `DYLD_LIBRARY_PATH`; on Windows add
|
||||
the native DLL directory to `PATH`. When linking an AddressSanitizer native
|
||||
build, use the same compiler runtime, for example
|
||||
`RUSTFLAGS="-L native=$PWD/build-bindings -C linker=clang -C link-arg=-fsanitize=address"`.
|
||||
|
||||
## Ownership and Errors
|
||||
|
||||
Startup, crypto import/generation, serialization, and fallible settings return
|
||||
`std::io::Result`. Public-key `pack()` exports bytes; `unpack(bytes)` replaces a
|
||||
key only after successful import. Crypto methods accept binary buffers, including
|
||||
embedded NULs. Passwords and other C strings reject embedded NULs.
|
||||
|
||||
Callbacks are owned `Send + 'static` closures, not borrowed `&mut` closures.
|
||||
Use `move` captures and channels or `Arc` for shared application state. Native
|
||||
handles are thread-bound; send copied data through channels rather than moving
|
||||
a runner or value handle between threads. Handles own stable native pointers
|
||||
and can be moved out of or back into their Rust boxes on the same thread.
|
||||
Callback panics are contained at the
|
||||
FFI boundary; a panicking value callback stops its operation.
|
||||
|
||||
`Value::boxed()` creates an independent copy, including payload and metadata,
|
||||
so a retained value does not alias mutable native storage or in-flight puts.
|
||||
|
||||
```rust
|
||||
use opendht::{DhtRunner, InfoHash, Value};
|
||||
use std::sync::mpsc;
|
||||
|
||||
let mut runner = DhtRunner::new();
|
||||
runner.run(0)?;
|
||||
let hash = InfoHash::get("example");
|
||||
let (sent, received) = mpsc::channel();
|
||||
let token = runner.listen(&hash, move |value, expired| {
|
||||
if !expired { let _ = sent.send(value.as_bytes()); }
|
||||
true
|
||||
})?;
|
||||
runner.put(&hash, Value::new("hello"), |_| {}, false);
|
||||
let bytes = received.recv_timeout(std::time::Duration::from_secs(5))?;
|
||||
runner.cancel_listen(&hash, token);
|
||||
runner.join()?;
|
||||
# Ok::<(), Box<dyn std::error::Error>>(())
|
||||
```
|
||||
|
||||
`join()` waits for native callbacks, releases registrations, and permits restart.
|
||||
Do not invoke it from a DHT callback. `shutdown(callback)` requests graceful
|
||||
shutdown; wait for its callback before `join()` when persistence is required.
|
||||
Dropping a listener token frees only the token; explicitly call `cancel_listen`
|
||||
to remove the registration, or call `join()` to stop all operations. Pending
|
||||
get/put/query completions are reported as failure when the runner stops.
|
||||
|
||||
For non-threaded operation set `config.threaded = false` and repeatedly call
|
||||
`loop_once()`. Priority operations can take an iteration before normal work.
|
||||
`run_bind` selects IPv4/IPv6 bind addresses; `bound_port` reports OS-assigned ports.
|
||||
|
||||
## Extended APIs
|
||||
|
||||
- Values expose explicit IDs, user types, value types, sequence numbers,
|
||||
push priorities, and push types.
|
||||
- Keys and certificates support persistence; identities support RSA and EC
|
||||
generation. Private keys support binary signing and RSA decryption.
|
||||
- `get_filtered`, `listen_filtered`, and `query` accept native SQL-like filters,
|
||||
such as `WHERE value_type=17`, `WHERE seq=7`, and
|
||||
`SELECT id,user_type WHERE value_type=17`.
|
||||
Projected fields are optional owned values in `QueryResult`.
|
||||
- `register_type` installs native type policy and expiration settings.
|
||||
- Configuration accepts owned status and certificate-store callbacks.
|
||||
Certificate lookup/registration and connectivity changes are exposed.
|
||||
- Push token/topic/platform setters forward native settings.
|
||||
`push_notification_received` returns a `PushNotification`; call `poll()` for
|
||||
the native result. In non-threaded mode continue driving `loop_once()`.
|
||||
Disabled builds return `IgnoredDisabled`; stopped runners return the native
|
||||
ignored outcome. Push tokens remain valid after the runner stops.
|
||||
+19
-16
@@ -2,19 +2,18 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
extern crate opendht;
|
||||
use std::{ thread, time };
|
||||
use std::{thread, time};
|
||||
|
||||
use opendht::{ InfoHash, DhtRunner, DhtRunnerConfig, Value };
|
||||
use opendht::{DhtRunner, DhtRunnerConfig, InfoHash, Value};
|
||||
// use opendht::crypto::*;
|
||||
|
||||
fn main() {
|
||||
fn main() -> std::io::Result<()> {
|
||||
println!("{}", InfoHash::random());
|
||||
println!("{}", InfoHash::new());
|
||||
println!("{}", InfoHash::new().is_zero());
|
||||
println!("{}", InfoHash::get("alice"));
|
||||
println!("{}", InfoHash::get("alice").is_zero());
|
||||
|
||||
|
||||
let mut dht = DhtRunner::new();
|
||||
let /*mut*/ config = DhtRunnerConfig::new();
|
||||
//// If you want to inject a certificate, uncomment the following lines and previous mut.
|
||||
@@ -23,36 +22,40 @@ fn main() {
|
||||
//let cert = DhtCertificate::import("example.crt").ok().expect("Invalid cert file");
|
||||
//let pk = PrivateKey::import("example.key", "");
|
||||
//config.set_identity(cert, pk);
|
||||
dht.run_config(1412, config);
|
||||
dht.bootstrap("bootstrap.jami.net", 4222);
|
||||
dht.run_config(1412, config)?;
|
||||
dht.bootstrap("bootstrap.jami.net", 4222)?;
|
||||
println!("Current node id: {}", dht.node_id());
|
||||
|
||||
let /* mut */ data = 42;
|
||||
let mut get_cb = |v: Box<Value>| {
|
||||
let get_cb = move |v: Box<Value>| {
|
||||
//data += 1;
|
||||
println!("GET: VALUE CB - data: {} - v: {}", data, v);
|
||||
true
|
||||
};
|
||||
let mut done_cb = |ok: bool| {
|
||||
let done_cb = move |ok: bool| {
|
||||
println!("GET: DONE CB - data: {} - ok: {}", data, ok);
|
||||
};
|
||||
|
||||
dht.get(&InfoHash::get("alice"), &mut get_cb, &mut done_cb);
|
||||
dht.get(&InfoHash::get("alice"), get_cb, done_cb);
|
||||
|
||||
let mut put_done_cb = |ok: bool| {
|
||||
let put_done_cb = move |ok: bool| {
|
||||
println!("PUT: DONE CB - data: {} - ok: {}", data, ok);
|
||||
};
|
||||
dht.put(&InfoHash::get("bob"), Value::new("hi!"), &mut put_done_cb, false);
|
||||
|
||||
dht.put(&InfoHash::get("bob"), Value::new("hi!"), put_done_cb, false);
|
||||
|
||||
println!("Start listening /foo");
|
||||
let mut value_cb = |v, expired| {
|
||||
println!("LISTEN: DONE CB - data: {} - v: {} - expired: {}", data, v, expired);
|
||||
let value_cb = move |v, expired| {
|
||||
println!(
|
||||
"LISTEN: DONE CB - data: {} - v: {} - expired: {}",
|
||||
data, v, expired
|
||||
);
|
||||
true
|
||||
};
|
||||
let token = dht.listen(&InfoHash::get("foo"), &mut value_cb);
|
||||
let token = dht.listen(&InfoHash::get("foo"), value_cb)?;
|
||||
let one_min = time::Duration::from_secs(10);
|
||||
thread::sleep(one_min);
|
||||
dht.cancel_listen(&InfoHash::get("foo"), token);
|
||||
println!("Public ips: {:#?}", dht.public_addresses());
|
||||
}
|
||||
dht.join()?;
|
||||
Ok(())
|
||||
}
|
||||
+9
-7
@@ -1,18 +1,20 @@
|
||||
// Copyright (c) 2014-2026 Savoir-faire Linux Inc.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use crate::ffi::*;
|
||||
use crate::ffi::{self, DataView};
|
||||
|
||||
pub use crate::ffi::Blob;
|
||||
native_handle!(Blob, dht_blob_delete);
|
||||
|
||||
impl Blob {
|
||||
pub fn data(&self) -> DataView {
|
||||
unsafe { dht_blob_get_data(self) }
|
||||
unsafe { ffi::dht_blob_get_data(self.as_ptr()) }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Blob {
|
||||
fn drop(&mut self) {
|
||||
unsafe { dht_blob_delete(&mut *self) }
|
||||
pub fn as_bytes(&self) -> Vec<u8> {
|
||||
let data = self.data();
|
||||
if data.size == 0 {
|
||||
return Vec::new();
|
||||
}
|
||||
unsafe { std::slice::from_raw_parts(data.data, data.size).to_vec() }
|
||||
}
|
||||
}
|
||||
+137
-74
@@ -1,150 +1,213 @@
|
||||
// Copyright (c) 2014-2026 Savoir-faire Linux Inc.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#![allow(dead_code)]
|
||||
|
||||
pub use crate::ffi::*;
|
||||
use crate::{ffi, Blob, InfoHash, PkId};
|
||||
use std::ffi::CString;
|
||||
use std::fs::File;
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::io::prelude::*;
|
||||
use std::ptr;
|
||||
|
||||
native_handle!(PublicKey, dht_publickey_delete);
|
||||
native_handle!(PrivateKey, dht_privatekey_delete);
|
||||
native_handle!(DhtCertificate, dht_certificate_delete);
|
||||
|
||||
impl PublicKey {
|
||||
pub fn new(data: &str) -> Box<PublicKey> {
|
||||
unsafe { Box::from_raw(dht_publickey_import(data.as_ptr(), data.len())) }
|
||||
pub fn new(data: impl AsRef<[u8]>) -> io::Result<Box<PublicKey>> {
|
||||
let data = data.as_ref();
|
||||
unsafe { Self::from_raw(ffi::dht_publickey_import(data.as_ptr(), data.len())) }
|
||||
}
|
||||
|
||||
pub fn unpack(&mut self, data: Vec<u8>) -> i32 {
|
||||
unsafe { dht_publickey_unpack(&mut *self, data.as_ptr(), data.len()) }
|
||||
pub fn unpack(&mut self, data: impl AsRef<[u8]>) -> io::Result<()> {
|
||||
let data = data.as_ref();
|
||||
let result = unsafe { ffi::dht_publickey_unpack(self.as_ptr(), data.as_ptr(), data.len()) };
|
||||
if result == 0 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(io::Error::other("invalid public key"))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pack(&mut self, data: &str) -> i32 {
|
||||
let data = CString::new(data).unwrap();
|
||||
unsafe { dht_publickey_pack(&mut *self, data.as_ptr(), data.as_bytes().len()) }
|
||||
pub fn pack(&self) -> io::Result<Vec<u8>> {
|
||||
unsafe { Ok(Blob::from_raw(ffi::dht_publickey_export_blob(self.as_ptr()))?.as_bytes()) }
|
||||
}
|
||||
|
||||
pub fn id(&self) -> InfoHash {
|
||||
unsafe { dht_publickey_get_id(self) }
|
||||
unsafe { ffi::dht_publickey_get_id(self.as_ptr()) }
|
||||
}
|
||||
|
||||
pub fn long_id(&self) -> PkId {
|
||||
unsafe { dht_publickey_get_long_id(self) }
|
||||
unsafe { ffi::dht_publickey_get_long_id(self.as_ptr()) }
|
||||
}
|
||||
|
||||
pub fn check_signature(&self, data: &str, signature: &str) -> bool {
|
||||
let data = CString::new(data).unwrap();
|
||||
let signature = CString::new(signature).unwrap();
|
||||
pub fn check_signature(&self, data: impl AsRef<[u8]>, signature: impl AsRef<[u8]>) -> bool {
|
||||
let data = data.as_ref();
|
||||
let signature = signature.as_ref();
|
||||
unsafe {
|
||||
dht_publickey_check_signature(
|
||||
self,
|
||||
data.as_ptr(),
|
||||
data.as_bytes().len(),
|
||||
signature.as_ptr(),
|
||||
signature.as_bytes().len(),
|
||||
ffi::dht_publickey_check_signature(
|
||||
self.as_ptr(),
|
||||
data.as_ptr().cast(),
|
||||
data.len(),
|
||||
signature.as_ptr().cast(),
|
||||
signature.len(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn encrypt(&self, data: &str) -> Box<Blob> {
|
||||
let data = CString::new(data).unwrap();
|
||||
pub fn encrypt(&self, data: impl AsRef<[u8]>) -> io::Result<Box<Blob>> {
|
||||
let data = data.as_ref();
|
||||
unsafe {
|
||||
Box::from_raw(dht_publickey_encrypt(
|
||||
self,
|
||||
data.as_ptr(),
|
||||
data.as_bytes().len(),
|
||||
Blob::from_raw(ffi::dht_publickey_encrypt(
|
||||
self.as_ptr(),
|
||||
data.as_ptr().cast(),
|
||||
data.len(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for PublicKey {
|
||||
fn drop(&mut self) {
|
||||
unsafe { dht_publickey_delete(&mut *self) }
|
||||
}
|
||||
}
|
||||
|
||||
impl PrivateKey {
|
||||
pub fn new(key_length_bits: u32) -> Box<PrivateKey> {
|
||||
unsafe { Box::from_raw(dht_privatekey_generate(key_length_bits)) }
|
||||
pub fn new(key_length_bits: u32) -> io::Result<Box<PrivateKey>> {
|
||||
unsafe { Self::from_raw(ffi::dht_privatekey_generate(key_length_bits)) }
|
||||
}
|
||||
|
||||
pub fn new_ec() -> io::Result<Box<PrivateKey>> {
|
||||
unsafe { Self::from_raw(ffi::dht_privatekey_generate_ec()) }
|
||||
}
|
||||
|
||||
pub fn import(file: &str, password: &str) -> io::Result<Box<PrivateKey>> {
|
||||
let mut f = File::open(file)?;
|
||||
let mut buffer = Vec::new();
|
||||
f.read_to_end(&mut buffer)?;
|
||||
Ok(PrivateKey::from_bytes(&buffer, password))
|
||||
Self::from_bytes(&fs::read(file)?, password)
|
||||
}
|
||||
|
||||
pub fn from_bytes(buffer: &Vec<u8>, password: &str) -> Box<PrivateKey> {
|
||||
pub fn from_bytes(buffer: &[u8], password: &str) -> io::Result<Box<PrivateKey>> {
|
||||
let password = CString::new(password)?;
|
||||
unsafe {
|
||||
Box::from_raw(dht_privatekey_import(
|
||||
(&*buffer).as_ptr(),
|
||||
Self::from_raw(ffi::dht_privatekey_import(
|
||||
buffer.as_ptr(),
|
||||
buffer.len(),
|
||||
password.as_ptr() as *const i8,
|
||||
password.as_ptr(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn public_key(&self) -> Box<PublicKey> {
|
||||
unsafe { Box::from_raw(dht_privatekey_get_publickey(self)) }
|
||||
unsafe {
|
||||
PublicKey::from_raw(ffi::dht_privatekey_get_publickey(self.as_ptr()))
|
||||
.expect("private key has a public key")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for PrivateKey {
|
||||
fn drop(&mut self) {
|
||||
unsafe { dht_privatekey_delete(&mut *self) }
|
||||
pub fn serialize(&self, password: &str) -> io::Result<Vec<u8>> {
|
||||
let password = CString::new(password)?;
|
||||
unsafe {
|
||||
Ok(Blob::from_raw(ffi::dht_privatekey_export_blob(
|
||||
self.as_ptr(),
|
||||
password.as_ptr(),
|
||||
))?
|
||||
.as_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sign(&self, data: impl AsRef<[u8]>) -> io::Result<Vec<u8>> {
|
||||
let data = data.as_ref();
|
||||
unsafe {
|
||||
Ok(Blob::from_raw(ffi::dht_privatekey_sign(
|
||||
self.as_ptr(),
|
||||
data.as_ptr(),
|
||||
data.len(),
|
||||
))?
|
||||
.as_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn decrypt(&self, data: &[u8]) -> io::Result<Box<Blob>> {
|
||||
unsafe {
|
||||
Blob::from_raw(ffi::dht_privatekey_decrypt(
|
||||
self.as_ptr(),
|
||||
data.as_ptr().cast(),
|
||||
data.len(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DhtCertificate {
|
||||
pub fn import(file: &str) -> io::Result<Box<DhtCertificate>> {
|
||||
let mut f = File::open(file)?;
|
||||
let mut buffer = Vec::new();
|
||||
f.read_to_end(&mut buffer)?;
|
||||
Ok(DhtCertificate::from_bytes(&buffer))
|
||||
Self::from_bytes(&fs::read(file)?)
|
||||
}
|
||||
|
||||
pub fn from_bytes(buffer: &Vec<u8>) -> Box<DhtCertificate> {
|
||||
unsafe { Box::from_raw(dht_certificate_import((&*buffer).as_ptr(), buffer.len())) }
|
||||
pub fn from_bytes(buffer: &[u8]) -> io::Result<Box<DhtCertificate>> {
|
||||
unsafe { Self::from_raw(ffi::dht_certificate_import(buffer.as_ptr(), buffer.len())) }
|
||||
}
|
||||
|
||||
pub fn from_slice(buffer: &str) -> Box<DhtCertificate> {
|
||||
unsafe { Box::from_raw(dht_certificate_import((&*buffer).as_ptr(), buffer.len())) }
|
||||
pub fn from_slice(buffer: &str) -> io::Result<Box<DhtCertificate>> {
|
||||
Self::from_bytes(buffer.as_bytes())
|
||||
}
|
||||
|
||||
pub fn serialize(&self) -> io::Result<Vec<u8>> {
|
||||
unsafe { Ok(Blob::from_raw(ffi::dht_certificate_export(self.as_ptr()))?.as_bytes()) }
|
||||
}
|
||||
|
||||
pub fn id(&self) -> InfoHash {
|
||||
unsafe { dht_certificate_get_id(&*self) }
|
||||
unsafe { ffi::dht_certificate_get_id(self.as_ptr()) }
|
||||
}
|
||||
|
||||
pub fn long_id(&self) -> PkId {
|
||||
unsafe { dht_certificate_get_long_id(&*self) }
|
||||
unsafe { ffi::dht_certificate_get_long_id(self.as_ptr()) }
|
||||
}
|
||||
|
||||
pub fn publickey(&self) -> Box<PublicKey> {
|
||||
unsafe { Box::from_raw(dht_certificate_get_publickey(&*self)) }
|
||||
unsafe {
|
||||
PublicKey::from_raw(ffi::dht_certificate_get_publickey(self.as_ptr()))
|
||||
.expect("certificate has a public key")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for DhtCertificate {
|
||||
fn drop(&mut self) {
|
||||
unsafe { dht_certificate_delete(&mut *self) }
|
||||
}
|
||||
pub struct DhtIdentity {
|
||||
pub privatekey: Box<PrivateKey>,
|
||||
pub certificate: Box<DhtCertificate>,
|
||||
}
|
||||
|
||||
impl DhtIdentity {
|
||||
pub fn new(common_name: &str) -> DhtIdentity {
|
||||
unsafe { DhtIdentity::generate(common_name, Box::from_raw(ptr::null_mut())) }
|
||||
pub fn new(common_name: &str) -> io::Result<Self> {
|
||||
Self::generate_inner(common_name, None, false)
|
||||
}
|
||||
|
||||
pub fn generate(common_name: &str, ca: Box<DhtIdentity>) -> DhtIdentity {
|
||||
let common_name = CString::new(common_name).unwrap();
|
||||
unsafe { dht_identity_generate(common_name.as_ptr(), &*ca) }
|
||||
pub fn new_ec(common_name: &str) -> io::Result<Self> {
|
||||
Self::generate_inner(common_name, None, true)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for DhtIdentity {
|
||||
fn drop(&mut self) {
|
||||
unsafe { dht_identity_delete(&mut *self) }
|
||||
pub fn generate(common_name: &str, ca: &DhtIdentity) -> io::Result<Self> {
|
||||
Self::generate_inner(common_name, Some(ca), false)
|
||||
}
|
||||
|
||||
pub(crate) fn raw(&self) -> ffi::DhtIdentity {
|
||||
ffi::DhtIdentity {
|
||||
privatekey: self.privatekey.as_ptr(),
|
||||
certificate: self.certificate.as_ptr(),
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_inner(common_name: &str, ca: Option<&Self>, ec: bool) -> io::Result<Self> {
|
||||
let name = CString::new(common_name)?;
|
||||
let ca = ca.map(Self::raw);
|
||||
let ca_ptr = ca
|
||||
.as_ref()
|
||||
.map_or(ptr::null(), |identity| identity as *const _);
|
||||
unsafe {
|
||||
let mut raw = if ec {
|
||||
ffi::dht_identity_generate_ec(name.as_ptr(), ca_ptr)
|
||||
} else {
|
||||
ffi::dht_identity_generate(name.as_ptr(), ca_ptr)
|
||||
};
|
||||
if raw.privatekey.is_null() || raw.certificate.is_null() {
|
||||
ffi::dht_identity_delete(&mut raw);
|
||||
return Err(io::Error::other("identity generation failed"));
|
||||
}
|
||||
Ok(Self {
|
||||
privatekey: PrivateKey::from_raw(raw.privatekey)?,
|
||||
certificate: DhtCertificate::from_raw(raw.certificate)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
+715
-167
File diff suppressed because it is too large.
Load diff
+243
-78
@@ -3,71 +3,66 @@
|
||||
|
||||
#![allow(dead_code)]
|
||||
|
||||
use libc::{c_char, c_int, c_uint, c_void, in_port_t, size_t};
|
||||
use os_socketaddr::OsSocketAddr;
|
||||
use libc::{c_char, c_int, c_uint, c_void, in_port_t, sa_family_t, size_t, sockaddr, socklen_t};
|
||||
|
||||
const HASH_LEN: usize = 20;
|
||||
const PKID_LEN: usize = 32;
|
||||
|
||||
#[repr(C)]
|
||||
pub struct DataView
|
||||
{
|
||||
pub struct DataView {
|
||||
pub data: *const u8,
|
||||
pub size: size_t
|
||||
pub size: size_t,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct Value
|
||||
{
|
||||
_opaque: [u8; 0]
|
||||
pub struct Value {
|
||||
_opaque: [u8; 0],
|
||||
_thread_bound: std::marker::PhantomData<*mut ()>,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct Blob
|
||||
{
|
||||
_opaque: [u8; 0]
|
||||
pub struct Blob {
|
||||
_opaque: [u8; 0],
|
||||
_thread_bound: std::marker::PhantomData<*mut ()>,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(PartialEq)]
|
||||
pub struct InfoHash
|
||||
{
|
||||
pub struct InfoHash {
|
||||
pub d: [u8; HASH_LEN],
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct PkId
|
||||
{
|
||||
pub struct PkId {
|
||||
pub d: [u8; PKID_LEN],
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct PublicKey
|
||||
{
|
||||
_opaque: [u8; 0]
|
||||
pub struct PublicKey {
|
||||
_opaque: [u8; 0],
|
||||
_thread_bound: std::marker::PhantomData<*mut ()>,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct PrivateKey
|
||||
{
|
||||
_opaque: [u8; 0]
|
||||
pub struct PrivateKey {
|
||||
_opaque: [u8; 0],
|
||||
_thread_bound: std::marker::PhantomData<*mut ()>,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct OpToken
|
||||
{
|
||||
_opaque: [u8; 0]
|
||||
pub struct OpToken {
|
||||
_opaque: [u8; 0],
|
||||
_thread_bound: std::marker::PhantomData<*mut ()>,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct DhtRunner
|
||||
{
|
||||
_opaque: [u8; 0]
|
||||
pub struct DhtRunner {
|
||||
_opaque: [u8; 0],
|
||||
_thread_bound: std::marker::PhantomData<*mut ()>,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct DhtNodeConfig
|
||||
{
|
||||
pub struct DhtNodeConfig {
|
||||
pub node_id: InfoHash,
|
||||
pub network: u32,
|
||||
pub is_bootstrap: bool,
|
||||
@@ -76,28 +71,25 @@ pub struct DhtNodeConfig
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct DhtCertificate
|
||||
{
|
||||
_opaque: [u8; 0]
|
||||
pub struct DhtCertificate {
|
||||
_opaque: [u8; 0],
|
||||
_thread_bound: std::marker::PhantomData<*mut ()>,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct DhtIdentity
|
||||
{
|
||||
pub struct DhtIdentity {
|
||||
pub privatekey: *mut PrivateKey,
|
||||
pub certificate: *mut DhtCertificate,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct DhtSecureConfig
|
||||
{
|
||||
pub struct DhtSecureConfig {
|
||||
pub node_config: DhtNodeConfig,
|
||||
pub id: DhtIdentity,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct DhtRunnerConfig
|
||||
{
|
||||
pub struct DhtRunnerConfig {
|
||||
pub dht_config: DhtSecureConfig,
|
||||
pub threaded: bool,
|
||||
pub proxy_server: *const c_char,
|
||||
@@ -113,17 +105,55 @@ pub struct DhtRunnerConfig
|
||||
pub log: bool,
|
||||
}
|
||||
|
||||
pub type CertificateCallback = extern "C" fn(*const DhtCertificate, *mut c_void);
|
||||
|
||||
#[repr(C)]
|
||||
pub struct DhtRunnerContext {
|
||||
pub status_cb: Option<extern "C" fn(c_int, c_int, *mut c_void)>,
|
||||
pub certificate_store_cb:
|
||||
Option<extern "C" fn(*const PkId, CertificateCallback, *mut c_void, *mut c_void)>,
|
||||
pub released_cb: Option<extern "C" fn(*mut c_void)>,
|
||||
pub user_data: *mut c_void,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct QueryResult {
|
||||
pub fields: u32,
|
||||
pub id: u64,
|
||||
pub value_type: u16,
|
||||
pub seq: u16,
|
||||
pub owner: InfoHash,
|
||||
pub user_type: DataView,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct PushToken {
|
||||
_opaque: [u8; 0],
|
||||
_thread_bound: std::marker::PhantomData<*mut ()>,
|
||||
}
|
||||
|
||||
#[link(name = "opendht-c")]
|
||||
extern {
|
||||
unsafe extern "C" {
|
||||
// dht::Value
|
||||
pub fn dht_value_new(data: *const u8, size: size_t) -> *mut Value;
|
||||
pub fn dht_value_with_id_new(data: *const u8, size: size_t, id: u64) -> *mut Value;
|
||||
pub fn dht_value_ref(data: *const Value) -> *mut Value;
|
||||
pub fn dht_value_copy(data: *const Value) -> *mut Value;
|
||||
pub fn dht_value_unref(data: *mut Value);
|
||||
pub fn dht_value_get_data(data: *const Value) -> DataView;
|
||||
pub fn dht_value_get_id(data: *const Value) -> u64;
|
||||
pub fn dht_value_get_owner(data: *const Value) -> *mut PublicKey;
|
||||
pub fn dht_value_get_recipient(data: *const Value) -> InfoHash;
|
||||
pub fn dht_value_get_user_type(data: *const Value) -> *const c_char;
|
||||
pub fn dht_value_set_user_type(data: *mut Value, user_type: *const c_char);
|
||||
pub fn dht_value_get_type(data: *const Value) -> u16;
|
||||
pub fn dht_value_set_type(data: *mut Value, value_type: u16);
|
||||
pub fn dht_value_get_seq(data: *const Value) -> u16;
|
||||
pub fn dht_value_set_seq(data: *mut Value, seq: u16);
|
||||
pub fn dht_value_get_priority(data: *const Value) -> c_uint;
|
||||
pub fn dht_value_set_priority(data: *mut Value, priority: c_uint);
|
||||
pub fn dht_value_get_push_type(data: *const Value) -> *const c_char;
|
||||
pub fn dht_value_set_push_type(data: *mut Value, push_type: *const c_char);
|
||||
|
||||
// dht::Blob
|
||||
pub fn dht_blob_get_data(data: *const Blob) -> DataView;
|
||||
@@ -132,8 +162,9 @@ extern {
|
||||
// dht::InfoHash
|
||||
pub fn dht_infohash_print(h: *const InfoHash) -> *const c_char;
|
||||
pub fn dht_infohash_random(h: *mut InfoHash);
|
||||
pub fn dht_infohash_get(h: *mut InfoHash, dat: *mut u8, dat_size: size_t);
|
||||
pub fn dht_infohash_get(h: *mut InfoHash, dat: *const u8, dat_size: size_t);
|
||||
pub fn dht_infohash_from_hex(h: *mut InfoHash, dat: *const c_char);
|
||||
pub fn dht_infohash_from_hex_null(h: *mut InfoHash, dat: *const c_char);
|
||||
pub fn dht_infohash_is_zero(j: *const InfoHash) -> bool;
|
||||
|
||||
// dht::PkId
|
||||
@@ -142,27 +173,56 @@ extern {
|
||||
// dht::crypto::PublicKey
|
||||
pub fn dht_publickey_import(dat: *const u8, dat_size: size_t) -> *mut PublicKey;
|
||||
pub fn dht_publickey_delete(pk: *mut PublicKey);
|
||||
pub fn dht_publickey_unpack(pk: *mut PublicKey, dat: *const u8, dat_size: size_t) -> c_int;
|
||||
pub fn dht_publickey_pack(pk: *mut PublicKey, out: *const c_char, out_size: size_t) -> c_int;
|
||||
pub fn dht_publickey_export_blob(pk: *const PublicKey) -> *mut Blob;
|
||||
pub fn dht_publickey_unpack(pk: *mut PublicKey, data: *const u8, size: size_t) -> c_int;
|
||||
pub fn dht_publickey_get_id(pk: *const PublicKey) -> InfoHash;
|
||||
pub fn dht_publickey_get_long_id(pk: *const PublicKey) -> PkId;
|
||||
pub fn dht_publickey_check_signature(pk: *const PublicKey, data: *const c_char, data_size: size_t, signature: *const c_char, signature_size: size_t) -> bool;
|
||||
pub fn dht_publickey_encrypt(pk: *const PublicKey, data: *const c_char, data_size: size_t) -> *mut Blob;
|
||||
pub fn dht_publickey_check_signature(
|
||||
pk: *const PublicKey,
|
||||
data: *const c_char,
|
||||
data_size: size_t,
|
||||
signature: *const c_char,
|
||||
signature_size: size_t,
|
||||
) -> bool;
|
||||
pub fn dht_publickey_encrypt(
|
||||
pk: *const PublicKey,
|
||||
data: *const c_char,
|
||||
data_size: size_t,
|
||||
) -> *mut Blob;
|
||||
|
||||
// dht::crypto::PrivateKey
|
||||
pub fn dht_privatekey_generate(key_length_bits: c_uint) -> *mut PrivateKey;
|
||||
pub fn dht_privatekey_import(dat: *const u8, data_size: size_t, password: *const c_char) -> *mut PrivateKey;
|
||||
pub fn dht_privatekey_generate_ec() -> *mut PrivateKey;
|
||||
pub fn dht_privatekey_import(
|
||||
dat: *const u8,
|
||||
data_size: size_t,
|
||||
password: *const c_char,
|
||||
) -> *mut PrivateKey;
|
||||
pub fn dht_privatekey_get_publickey(pk: *const PrivateKey) -> *mut PublicKey;
|
||||
pub fn dht_privatekey_export_blob(pk: *const PrivateKey, password: *const c_char) -> *mut Blob;
|
||||
pub fn dht_privatekey_sign(pk: *const PrivateKey, data: *const u8, size: size_t) -> *mut Blob;
|
||||
pub fn dht_privatekey_decrypt(
|
||||
pk: *const PrivateKey,
|
||||
data: *const c_char,
|
||||
size: size_t,
|
||||
) -> *mut Blob;
|
||||
pub fn dht_privatekey_delete(pk: *mut PrivateKey);
|
||||
|
||||
// dht::crypto::Certificate
|
||||
pub fn dht_certificate_import(dat: *const u8, dat_size: size_t) -> *mut DhtCertificate;
|
||||
pub fn dht_certificate_export(cert: *const DhtCertificate) -> *mut Blob;
|
||||
pub fn dht_certificate_ref(cert: *const DhtCertificate) -> *mut DhtCertificate;
|
||||
pub fn dht_certificate_get_id(cert: *const DhtCertificate) -> InfoHash;
|
||||
pub fn dht_certificate_get_long_id(cert: *const DhtCertificate) -> PkId;
|
||||
pub fn dht_certificate_get_publickey(cert: *const DhtCertificate) -> *mut PublicKey;
|
||||
pub fn dht_certificate_delete(cert: *mut DhtCertificate);
|
||||
|
||||
pub fn dht_identity_generate(common_name: *const c_char, ca: *const DhtIdentity) -> DhtIdentity;
|
||||
pub fn dht_identity_generate(common_name: *const c_char, ca: *const DhtIdentity)
|
||||
-> DhtIdentity;
|
||||
pub fn dht_identity_generate_ec(
|
||||
common_name: *const c_char,
|
||||
ca: *const DhtIdentity,
|
||||
) -> DhtIdentity;
|
||||
pub fn dht_identity_delete(ca: *mut DhtIdentity);
|
||||
|
||||
// dht::OpToken
|
||||
@@ -174,37 +234,142 @@ extern {
|
||||
pub fn dht_runner_get_id(dht: *const DhtRunner) -> InfoHash;
|
||||
pub fn dht_runner_get_node_id(dht: *const DhtRunner) -> InfoHash;
|
||||
pub fn dht_runner_delete(dht: *mut DhtRunner);
|
||||
pub fn dht_runner_run(dht: *mut DhtRunner, port: in_port_t);
|
||||
pub fn dht_runner_run_config(dht: *mut DhtRunner, port: in_port_t, config: *const DhtRunnerConfig);
|
||||
pub fn dht_runner_run(dht: *mut DhtRunner, port: in_port_t) -> c_int;
|
||||
pub fn dht_runner_run_config(
|
||||
dht: *mut DhtRunner,
|
||||
port: in_port_t,
|
||||
config: *const DhtRunnerConfig,
|
||||
) -> c_int;
|
||||
pub fn dht_runner_run_bind(
|
||||
dht: *mut DhtRunner,
|
||||
ipv4: *const c_char,
|
||||
ipv6: *const c_char,
|
||||
port: in_port_t,
|
||||
config: *const DhtRunnerConfig,
|
||||
context: *const DhtRunnerContext,
|
||||
) -> c_int;
|
||||
pub fn dht_runner_connectivity_changed(dht: *mut DhtRunner) -> c_int;
|
||||
pub fn dht_runner_register_type(
|
||||
dht: *mut DhtRunner,
|
||||
id: u16,
|
||||
name: *const c_char,
|
||||
expiration_seconds: u64,
|
||||
insecure: bool,
|
||||
) -> c_int;
|
||||
pub fn dht_runner_get_publickey(dht: *const DhtRunner) -> *mut PublicKey;
|
||||
pub fn dht_runner_register_certificate(
|
||||
dht: *mut DhtRunner,
|
||||
cert: *const DhtCertificate,
|
||||
) -> c_int;
|
||||
pub fn dht_runner_find_certificate(
|
||||
dht: *mut DhtRunner,
|
||||
id: *const PkId,
|
||||
callback: CertificateCallback,
|
||||
released: extern "C" fn(*mut c_void),
|
||||
data: *mut c_void,
|
||||
);
|
||||
pub fn dht_runner_bootstrap(dht: *mut DhtRunner, host: *const c_char, service: *const c_char);
|
||||
pub fn dht_runner_get(dht: *mut DhtRunner, h: *const InfoHash,
|
||||
get_cb: extern fn(*mut Value, *mut c_void) -> bool,
|
||||
done_cb: extern fn(bool, *mut c_void),
|
||||
cb_user_data: *mut c_void);
|
||||
pub fn dht_runner_put(dht: *mut DhtRunner, h: *const InfoHash, v: *const Value,
|
||||
done_cb: extern fn(bool, *mut c_void),
|
||||
cb_user_data: *mut c_void,
|
||||
permanent: bool);
|
||||
pub fn dht_runner_put_signed(dht: *mut DhtRunner, h: *const InfoHash, v: *const Value,
|
||||
done_cb: extern fn(bool, *mut c_void),
|
||||
cb_user_data: *mut c_void,
|
||||
permanent: bool);
|
||||
pub fn dht_runner_put_encrypted(dht: *mut DhtRunner, h: *const InfoHash,
|
||||
to: *const PkId, v: *const Value,
|
||||
done_cb: extern fn(bool, *mut c_void),
|
||||
cb_user_data: *mut c_void,
|
||||
permanent: bool);
|
||||
pub fn dht_runner_put_permanent(dht: *mut DhtRunner, h: *const InfoHash, v: *const Value,
|
||||
done_cb: extern fn(bool, *mut c_void),
|
||||
cb_user_data: *mut c_void);
|
||||
pub fn dht_runner_get(
|
||||
dht: *mut DhtRunner,
|
||||
h: *const InfoHash,
|
||||
get_cb: extern "C" fn(*const Value, *mut c_void) -> bool,
|
||||
done_cb: extern "C" fn(bool, *mut c_void),
|
||||
cb_user_data: *mut c_void,
|
||||
);
|
||||
pub fn dht_runner_get_filtered(
|
||||
dht: *mut DhtRunner,
|
||||
h: *const InfoHash,
|
||||
filter: *const c_char,
|
||||
callback: extern "C" fn(*const Value, *mut c_void) -> bool,
|
||||
done: extern "C" fn(bool, *mut c_void),
|
||||
data: *mut c_void,
|
||||
);
|
||||
pub fn dht_runner_query(
|
||||
dht: *mut DhtRunner,
|
||||
h: *const InfoHash,
|
||||
query: *const c_char,
|
||||
callback: extern "C" fn(*const QueryResult, *mut c_void) -> bool,
|
||||
done: extern "C" fn(bool, *mut c_void),
|
||||
data: *mut c_void,
|
||||
);
|
||||
pub fn dht_runner_put(
|
||||
dht: *mut DhtRunner,
|
||||
h: *const InfoHash,
|
||||
v: *const Value,
|
||||
done_cb: extern "C" fn(bool, *mut c_void),
|
||||
cb_user_data: *mut c_void,
|
||||
permanent: bool,
|
||||
);
|
||||
pub fn dht_runner_put_signed(
|
||||
dht: *mut DhtRunner,
|
||||
h: *const InfoHash,
|
||||
v: *const Value,
|
||||
done_cb: extern "C" fn(bool, *mut c_void),
|
||||
cb_user_data: *mut c_void,
|
||||
permanent: bool,
|
||||
);
|
||||
pub fn dht_runner_put_encrypted(
|
||||
dht: *mut DhtRunner,
|
||||
h: *const InfoHash,
|
||||
to: *const PkId,
|
||||
v: *const Value,
|
||||
done_cb: extern "C" fn(bool, *mut c_void),
|
||||
cb_user_data: *mut c_void,
|
||||
permanent: bool,
|
||||
);
|
||||
pub fn dht_runner_cancel_put(dht: *mut DhtRunner, h: *const InfoHash, vid: u64);
|
||||
pub fn dht_runner_listen(dht: *mut DhtRunner, h: *const InfoHash,
|
||||
cb: extern fn(*mut Value, bool, *mut c_void) -> bool,
|
||||
done_cb: extern fn(*mut c_void),
|
||||
cb_user_data: *mut c_void) -> *mut OpToken;
|
||||
pub fn dht_runner_cancel_listen(dht: *mut DhtRunner, h: *const InfoHash,
|
||||
token: *const OpToken);
|
||||
pub fn dht_runner_shutdown(dht: *mut DhtRunner, done_cb: extern fn(bool, *mut c_void),
|
||||
cb_user_data: *mut c_void);
|
||||
pub fn dht_runner_get_public_address(dht: *const DhtRunner) -> *mut *mut OsSocketAddr;
|
||||
}
|
||||
pub fn dht_runner_listen(
|
||||
dht: *mut DhtRunner,
|
||||
h: *const InfoHash,
|
||||
cb: extern "C" fn(*const Value, bool, *mut c_void) -> bool,
|
||||
done_cb: extern "C" fn(*mut c_void),
|
||||
cb_user_data: *mut c_void,
|
||||
) -> *mut OpToken;
|
||||
pub fn dht_runner_listen_filtered(
|
||||
dht: *mut DhtRunner,
|
||||
h: *const InfoHash,
|
||||
filter: *const c_char,
|
||||
callback: extern "C" fn(*const Value, bool, *mut c_void) -> bool,
|
||||
released: extern "C" fn(*mut c_void),
|
||||
data: *mut c_void,
|
||||
) -> *mut OpToken;
|
||||
pub fn dht_runner_cancel_listen(dht: *mut DhtRunner, h: *const InfoHash, token: *mut OpToken);
|
||||
pub fn dht_runner_shutdown(
|
||||
dht: *mut DhtRunner,
|
||||
done_cb: extern "C" fn(*mut c_void),
|
||||
cb_user_data: *mut c_void,
|
||||
);
|
||||
pub fn dht_runner_get_public_address(dht: *const DhtRunner) -> *mut *mut sockaddr;
|
||||
pub fn dht_sockaddr_array_delete(addresses: *mut *mut sockaddr);
|
||||
pub fn dht_runner_loop(dht: *mut DhtRunner) -> c_int;
|
||||
pub fn dht_runner_join(dht: *mut DhtRunner) -> c_int;
|
||||
pub fn dht_runner_is_running(dht: *const DhtRunner) -> bool;
|
||||
pub fn dht_runner_get_bound_port(dht: *const DhtRunner, family: sa_family_t) -> in_port_t;
|
||||
pub fn dht_runner_ping(
|
||||
dht: *mut DhtRunner,
|
||||
addr: *mut sockaddr,
|
||||
len: socklen_t,
|
||||
callback: extern "C" fn(bool, *mut c_void),
|
||||
data: *mut c_void,
|
||||
);
|
||||
pub fn dht_runner_set_push_notification_token(
|
||||
dht: *mut DhtRunner,
|
||||
value: *const c_char,
|
||||
) -> c_int;
|
||||
pub fn dht_runner_set_push_notification_topic(
|
||||
dht: *mut DhtRunner,
|
||||
value: *const c_char,
|
||||
) -> c_int;
|
||||
pub fn dht_runner_set_push_notification_platform(
|
||||
dht: *mut DhtRunner,
|
||||
value: *const c_char,
|
||||
) -> c_int;
|
||||
pub fn dht_runner_push_notification_received(
|
||||
dht: *mut DhtRunner,
|
||||
keys: *const *const c_char,
|
||||
values: *const *const c_char,
|
||||
count: size_t,
|
||||
) -> *mut PushToken;
|
||||
pub fn dht_push_token_poll(token: *mut PushToken, result: *mut c_int) -> c_int;
|
||||
pub fn dht_push_token_delete(token: *mut PushToken);
|
||||
}
|
||||
@@ -42,7 +42,7 @@ impl InfoHash {
|
||||
let mut h = InfoHash::new();
|
||||
unsafe {
|
||||
let c_str = CString::new(data).unwrap();
|
||||
dht_infohash_from_hex(&mut h, c_str.as_ptr());
|
||||
dht_infohash_from_hex_null(&mut h, c_str.as_ptr());
|
||||
}
|
||||
h
|
||||
}
|
||||
|
||||
+33
-2
@@ -4,6 +4,34 @@
|
||||
extern crate libc;
|
||||
extern crate os_socketaddr;
|
||||
|
||||
macro_rules! native_handle {
|
||||
($name:ident, $delete:ident) => {
|
||||
pub struct $name {
|
||||
raw: std::ptr::NonNull<crate::ffi::$name>,
|
||||
}
|
||||
|
||||
impl $name {
|
||||
pub(crate) unsafe fn from_raw(
|
||||
raw: *mut crate::ffi::$name,
|
||||
) -> std::io::Result<Box<Self>> {
|
||||
let raw = std::ptr::NonNull::new(raw)
|
||||
.ok_or_else(|| std::io::Error::other("native operation failed"))?;
|
||||
Ok(Box::new(Self { raw }))
|
||||
}
|
||||
|
||||
pub(crate) fn as_ptr(&self) -> *mut crate::ffi::$name {
|
||||
self.raw.as_ptr()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for $name {
|
||||
fn drop(&mut self) {
|
||||
unsafe { crate::ffi::$delete(self.as_ptr()) }
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
mod blob;
|
||||
pub mod crypto;
|
||||
mod dhtrunner;
|
||||
@@ -13,7 +41,10 @@ mod pkid;
|
||||
mod value;
|
||||
|
||||
pub use blob::Blob;
|
||||
pub use dhtrunner::{ DhtRunner, DhtRunnerConfig, OpToken };
|
||||
pub use dhtrunner::{
|
||||
DhtNodeConfig, DhtRunner, DhtRunnerConfig, DhtSecureConfig, NodeStatus, OpToken,
|
||||
PushNotification, PushNotificationResult, QueryResult,
|
||||
};
|
||||
pub use infohash::InfoHash;
|
||||
pub use pkid::PkId;
|
||||
pub use value::{ DataView, Value };
|
||||
pub use value::{DataView, Value};
|
||||
+79
-38
@@ -2,91 +2,132 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
#![allow(dead_code)]
|
||||
|
||||
use crate::ffi::*;
|
||||
use crate::crypto::PublicKey;
|
||||
use crate::{ffi, InfoHash};
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::fmt;
|
||||
use std::ffi::{ CString, IntoStringError };
|
||||
use std::str;
|
||||
use std::io;
|
||||
use std::slice;
|
||||
use std::str;
|
||||
|
||||
pub use crate::ffi::{DataView, Value};
|
||||
pub use crate::ffi::DataView;
|
||||
|
||||
native_handle!(Value, dht_value_unref);
|
||||
|
||||
impl Value {
|
||||
pub fn new(data: &str) -> Box<Value> {
|
||||
pub fn with_id(data: &[u8], id: u64) -> Box<Value> {
|
||||
unsafe {
|
||||
Box::from_raw(dht_value_new(data.as_bytes().as_ptr(),
|
||||
data.as_bytes().len()))
|
||||
Self::from_raw(ffi::dht_value_with_id_new(data.as_ptr(), data.len(), id))
|
||||
.expect("value allocation failed")
|
||||
}
|
||||
}
|
||||
pub fn from_bytes(data: &Vec<u8>) -> Box<Value> {
|
||||
|
||||
pub fn new(data: &str) -> Box<Value> {
|
||||
Self::from_bytes(data.as_bytes())
|
||||
}
|
||||
pub fn from_bytes(data: &[u8]) -> Box<Value> {
|
||||
unsafe {
|
||||
Box::from_raw(dht_value_new(data.as_ptr(),
|
||||
data.len()))
|
||||
Self::from_raw(ffi::dht_value_new(data.as_ptr(), data.len()))
|
||||
.expect("value allocation failed")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn id(&self) -> u64 {
|
||||
unsafe {
|
||||
dht_value_get_id(self)
|
||||
}
|
||||
unsafe { ffi::dht_value_get_id(self.as_ptr()) }
|
||||
}
|
||||
|
||||
fn dataview(&self) -> DataView {
|
||||
unsafe {
|
||||
dht_value_get_data(self)
|
||||
}
|
||||
unsafe { ffi::dht_value_get_data(self.as_ptr()) }
|
||||
}
|
||||
|
||||
pub fn as_bytes(&self) -> Vec<u8> {
|
||||
unsafe {
|
||||
let dv = dht_value_get_data(self);
|
||||
let slice = slice::from_raw_parts_mut(dv.data as *mut _, dv.size);
|
||||
let dv = ffi::dht_value_get_data(self.as_ptr());
|
||||
if dv.size == 0 {
|
||||
return Vec::new();
|
||||
}
|
||||
let slice = slice::from_raw_parts(dv.data, dv.size);
|
||||
slice.to_vec()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn boxed(&mut self) -> Box<Value> {
|
||||
unsafe {
|
||||
Box::from_raw(dht_value_ref(self))
|
||||
}
|
||||
pub fn boxed(&self) -> Box<Value> {
|
||||
unsafe { Self::from_raw(ffi::dht_value_copy(self.as_ptr())).expect("value copy failed") }
|
||||
}
|
||||
|
||||
pub fn recipient(&self) -> InfoHash {
|
||||
unsafe {
|
||||
dht_value_get_recipient(self)
|
||||
}
|
||||
unsafe { ffi::dht_value_get_recipient(self.as_ptr()) }
|
||||
}
|
||||
|
||||
pub fn owner(&self) -> Option<Box<PublicKey>> {
|
||||
unsafe { PublicKey::from_raw(ffi::dht_value_get_owner(self.as_ptr())).ok() }
|
||||
}
|
||||
|
||||
pub fn user_type(&self) -> Result<String, str::Utf8Error> {
|
||||
unsafe {
|
||||
let ptr = dht_value_get_owner(self);
|
||||
if ptr.is_null() {
|
||||
return None;
|
||||
}
|
||||
Some(Box::from_raw(ptr))
|
||||
Ok(CStr::from_ptr(ffi::dht_value_get_user_type(self.as_ptr()))
|
||||
.to_str()?
|
||||
.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn user_type(&self) -> Result<String, IntoStringError> {
|
||||
pub fn set_user_type(&mut self, value: &str) -> io::Result<()> {
|
||||
let value = CString::new(value)?;
|
||||
unsafe {
|
||||
CString::from_raw(dht_value_get_user_type(self) as *mut _).into_string()
|
||||
ffi::dht_value_set_user_type(self.as_ptr(), value.as_ptr());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn value_type(&self) -> u16 {
|
||||
unsafe { ffi::dht_value_get_type(self.as_ptr()) }
|
||||
}
|
||||
pub fn set_value_type(&mut self, value: u16) {
|
||||
unsafe {
|
||||
ffi::dht_value_set_type(self.as_ptr(), value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Value {
|
||||
fn drop(&mut self) {
|
||||
pub fn seq(&self) -> u16 {
|
||||
unsafe { ffi::dht_value_get_seq(self.as_ptr()) }
|
||||
}
|
||||
pub fn set_seq(&mut self, value: u16) {
|
||||
unsafe {
|
||||
dht_value_unref(&mut *self)
|
||||
ffi::dht_value_set_seq(self.as_ptr(), value);
|
||||
}
|
||||
}
|
||||
pub fn priority(&self) -> u32 {
|
||||
unsafe { ffi::dht_value_get_priority(self.as_ptr()) }
|
||||
}
|
||||
pub fn set_priority(&mut self, value: u32) {
|
||||
unsafe {
|
||||
ffi::dht_value_set_priority(self.as_ptr(), value);
|
||||
}
|
||||
}
|
||||
pub fn push_type(&self) -> Result<String, str::Utf8Error> {
|
||||
unsafe {
|
||||
Ok(CStr::from_ptr(ffi::dht_value_get_push_type(self.as_ptr()))
|
||||
.to_str()?
|
||||
.to_owned())
|
||||
}
|
||||
}
|
||||
pub fn set_push_type(&mut self, value: &str) -> io::Result<()> {
|
||||
let value = CString::new(value)?;
|
||||
unsafe {
|
||||
ffi::dht_value_set_push_type(self.as_ptr(), value.as_ptr());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Value {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
unsafe {
|
||||
let dataview = self.dataview();
|
||||
if dataview.size == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
let slice = slice::from_raw_parts(dataview.data, dataview.size);
|
||||
write!(f, "{}", str::from_utf8(slice).unwrap_or(""))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,483 @@
|
||||
use opendht::crypto::{DhtCertificate, DhtIdentity, PrivateKey, PublicKey};
|
||||
use opendht::{DhtRunner, DhtRunnerConfig, InfoHash, Value};
|
||||
use std::sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
mpsc, Arc,
|
||||
};
|
||||
use std::time::Duration;
|
||||
|
||||
#[test]
|
||||
fn hex_hash_parsing_checks_string_lengths() {
|
||||
for length in 0..40 {
|
||||
assert!(InfoHash::from_hex(&"a".repeat(length)).is_zero());
|
||||
}
|
||||
let hash = InfoHash::get("hex parsing regression");
|
||||
let encoded = hash.to_string();
|
||||
assert!(hash == InfoHash::from_hex(&encoded));
|
||||
assert!(hash == InfoHash::from_hex(&encoded.to_uppercase()));
|
||||
assert!(InfoHash::from_hex(&"z".repeat(40)).is_zero());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn values_can_move_out_of_their_box() {
|
||||
let mut value = *Value::with_id(b"moved value", 42);
|
||||
value.set_seq(7);
|
||||
assert_eq!(value.id(), 42);
|
||||
assert_eq!(value.seq(), 7);
|
||||
assert_eq!(value.as_bytes(), b"moved value");
|
||||
let value = Box::new(value);
|
||||
assert_eq!(value.as_bytes(), b"moved value");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn native_handles_can_move_out_of_their_boxes() {
|
||||
let private = *PrivateKey::new(2048).unwrap();
|
||||
let public = *private.public_key();
|
||||
let signature = private.sign(b"moved handles").unwrap();
|
||||
assert!(public.check_signature(b"moved handles", &signature));
|
||||
let encrypted = *public.encrypt(b"moved blob").unwrap();
|
||||
assert_eq!(
|
||||
private.decrypt(&encrypted.as_bytes()).unwrap().as_bytes(),
|
||||
b"moved blob"
|
||||
);
|
||||
let certificate = *DhtIdentity::new_ec("moved certificate")
|
||||
.unwrap()
|
||||
.certificate;
|
||||
let restored = DhtCertificate::from_bytes(&certificate.serialize().unwrap()).unwrap();
|
||||
assert!(certificate.id() == restored.id());
|
||||
|
||||
let mut runner = *DhtRunner::new();
|
||||
let mut config = DhtRunnerConfig::new();
|
||||
config.threaded = false;
|
||||
runner.run_config(0, config).unwrap();
|
||||
let hash = InfoHash::get("moved listener token");
|
||||
let (sent, received) = mpsc::channel();
|
||||
let token = *runner
|
||||
.listen(&hash, move |value, expired| {
|
||||
if !expired {
|
||||
let value = *value;
|
||||
sent.send(value.as_bytes()).unwrap();
|
||||
}
|
||||
true
|
||||
})
|
||||
.unwrap();
|
||||
runner.put(&hash, Value::new("moved callback value"), |_| {}, false);
|
||||
runner.loop_once().unwrap();
|
||||
assert_eq!(
|
||||
received.recv_timeout(Duration::from_secs(5)).unwrap(),
|
||||
b"moved callback value"
|
||||
);
|
||||
runner.cancel_listen(&hash, Box::new(token));
|
||||
let push = runner.push_notification_received(&[]).unwrap();
|
||||
runner.loop_once().unwrap();
|
||||
let mut push = Box::new(push);
|
||||
assert!(push.poll().unwrap().is_some());
|
||||
runner.join().unwrap();
|
||||
let mut runner = Box::new(runner);
|
||||
runner.run(0).unwrap();
|
||||
runner.join().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn value_metadata_and_borrowed_strings() {
|
||||
let mut value = Value::with_id(b"binary\0value", 42);
|
||||
value.set_user_type("application/octet-stream").unwrap();
|
||||
value.set_value_type(17);
|
||||
value.set_seq(23);
|
||||
value.set_priority(1);
|
||||
value.set_push_type("audioCall").unwrap();
|
||||
assert_eq!(value.id(), 42);
|
||||
assert_eq!(value.as_bytes(), b"binary\0value");
|
||||
assert_eq!(value.user_type().unwrap(), "application/octet-stream");
|
||||
assert_eq!(value.user_type().unwrap(), "application/octet-stream");
|
||||
assert_eq!(value.value_type(), 17);
|
||||
assert_eq!(value.seq(), 23);
|
||||
assert_eq!(value.priority(), 1);
|
||||
assert_eq!(value.push_type().unwrap(), "audioCall");
|
||||
let mut copy = value.boxed();
|
||||
copy.set_priority(2);
|
||||
assert_eq!(value.priority(), 1);
|
||||
assert_eq!(copy.priority(), 2);
|
||||
assert!(value.owner().is_none());
|
||||
assert!(value.recipient().is_zero());
|
||||
assert!(Value::new("").as_bytes().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn crypto_errors_are_results() {
|
||||
assert!(PrivateKey::new(1).is_err());
|
||||
assert!(PrivateKey::from_bytes(&[1, 2, 3], "").is_err());
|
||||
assert!(PublicKey::new("not a key").is_err());
|
||||
assert!(DhtCertificate::from_bytes(&[1, 2, 3]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn binary_crypto_and_password_slices() {
|
||||
let private = PrivateKey::new(2048).unwrap();
|
||||
let backing = String::from("password-extra");
|
||||
let password = &backing[..8];
|
||||
let packed = private.serialize(password).unwrap();
|
||||
let restored = PrivateKey::from_bytes(&packed, password).unwrap();
|
||||
assert!(PrivateKey::from_bytes(&packed, "wrong").is_err());
|
||||
assert!(PrivateKey::from_bytes(&packed, "bad\0password").is_err());
|
||||
let mut public = PublicKey::new(restored.public_key().pack().unwrap()).unwrap();
|
||||
public.unpack(private.public_key().pack().unwrap()).unwrap();
|
||||
let data = b"binary\0message\xff";
|
||||
let signature = restored.sign(data).unwrap();
|
||||
assert!(public.check_signature(data, &signature));
|
||||
assert!(!public.check_signature(b"changed", &signature));
|
||||
let encrypted = public.encrypt(data).unwrap();
|
||||
assert_eq!(
|
||||
restored.decrypt(&encrypted.as_bytes()).unwrap().as_bytes(),
|
||||
data
|
||||
);
|
||||
assert!(restored.decrypt(b"invalid").is_err());
|
||||
let plain = private.serialize("").unwrap();
|
||||
assert!(PrivateKey::from_bytes(&plain, "").is_ok());
|
||||
let ec = PrivateKey::new_ec().unwrap();
|
||||
assert!(ec
|
||||
.public_key()
|
||||
.check_signature(data, ec.sign(data).unwrap()));
|
||||
assert!(ec.public_key().encrypt(data).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn identity_persistence() {
|
||||
let identity = DhtIdentity::new("binding regression").unwrap();
|
||||
let certificate = identity.certificate.serialize().unwrap();
|
||||
let restored = DhtCertificate::from_bytes(&certificate).unwrap();
|
||||
assert!(identity.certificate.id() == restored.id());
|
||||
assert!(DhtRunner::new().register_certificate(&restored).is_err());
|
||||
assert!(DhtIdentity::generate("child", &identity).is_ok());
|
||||
assert!(DhtIdentity::new_ec("elliptic curve").is_ok());
|
||||
}
|
||||
|
||||
struct DropCounter(Arc<AtomicUsize>);
|
||||
|
||||
impl Drop for DropCounter {
|
||||
fn drop(&mut self) {
|
||||
self.0.fetch_add(1, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn join_releases_pending_callbacks_and_allows_restart() {
|
||||
let mut runner = DhtRunner::new();
|
||||
let mut config = DhtRunnerConfig::new();
|
||||
config.threaded = false;
|
||||
let status_released = Arc::new(AtomicUsize::new(0));
|
||||
let status_counter = DropCounter(status_released.clone());
|
||||
config.set_status_callback(move |_, _| {
|
||||
let _ = &status_counter;
|
||||
});
|
||||
config.set_proxy_user_agent("regression").unwrap();
|
||||
config.set_proxy_user_agent("replacement").unwrap();
|
||||
runner.run_config(0, config).unwrap();
|
||||
assert!(runner.is_running());
|
||||
assert_ne!(runner.bound_port(libc::AF_INET as _), 0);
|
||||
let hash = InfoHash::get("callback ownership");
|
||||
let released = Arc::new(AtomicUsize::new(0));
|
||||
let counter = DropCounter(released.clone());
|
||||
let token = runner
|
||||
.listen(&hash, move |_, _| {
|
||||
let _ = &counter;
|
||||
true
|
||||
})
|
||||
.unwrap();
|
||||
let (sent, received) = mpsc::channel();
|
||||
runner.get(
|
||||
&hash,
|
||||
|_| true,
|
||||
move |ok| {
|
||||
sent.send(ok).unwrap();
|
||||
},
|
||||
);
|
||||
runner.join().unwrap();
|
||||
assert_eq!(status_released.load(Ordering::SeqCst), 1);
|
||||
assert_eq!(released.load(Ordering::SeqCst), 1);
|
||||
assert!(!received.recv_timeout(Duration::from_secs(5)).unwrap());
|
||||
drop(token);
|
||||
assert!(!runner.is_running());
|
||||
runner.run(0).unwrap();
|
||||
runner.join().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nonthreaded_loop_executes_owned_callbacks() {
|
||||
let mut runner = DhtRunner::new();
|
||||
let mut config = DhtRunnerConfig::new();
|
||||
config.threaded = false;
|
||||
runner.run_config(0, config).unwrap();
|
||||
let hash = InfoHash::get("loop progress");
|
||||
let seen = Arc::new(AtomicUsize::new(0));
|
||||
let callback_seen = seen.clone();
|
||||
let token = runner
|
||||
.listen(&hash, move |value, expired| {
|
||||
if !expired {
|
||||
assert_eq!(value.as_bytes(), b"test");
|
||||
callback_seen.fetch_add(1, Ordering::SeqCst);
|
||||
}
|
||||
true
|
||||
})
|
||||
.unwrap();
|
||||
runner.put(&hash, Value::new("test"), |_| {}, false);
|
||||
runner.loop_once().unwrap();
|
||||
assert_eq!(seen.load(Ordering::SeqCst), 1);
|
||||
runner.cancel_listen(&hash, token);
|
||||
runner.join().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shutdown_owns_its_callback_and_contains_panics() {
|
||||
let mut runner = DhtRunner::new();
|
||||
let (sent, received) = mpsc::channel();
|
||||
runner.shutdown(move || {
|
||||
sent.send(42).unwrap();
|
||||
});
|
||||
assert_eq!(received.recv_timeout(Duration::from_secs(5)).unwrap(), 42);
|
||||
runner.get(
|
||||
&InfoHash::new(),
|
||||
|_| true,
|
||||
|_| panic!("callback panic must not unwind into C"),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn startup_errors_reach_rust() {
|
||||
let socket4 = std::net::UdpSocket::bind("0.0.0.0:0").unwrap();
|
||||
let port = socket4.local_addr().unwrap().port();
|
||||
let _socket6 = std::net::UdpSocket::bind((std::net::Ipv6Addr::UNSPECIFIED, port)).unwrap();
|
||||
assert!(DhtRunner::new().run(port).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bound_filtered_queries_and_extension_hooks() {
|
||||
let mut runner = DhtRunner::new();
|
||||
let mut config = DhtRunnerConfig::new();
|
||||
config.threaded = false;
|
||||
config.set_status_callback(|_, _| {});
|
||||
config.set_certificate_store(|_| Vec::new());
|
||||
runner.run_bind("127.0.0.1", "::1", 0, config).unwrap();
|
||||
runner.register_type(17, "regression", 60, false).unwrap();
|
||||
runner.connectivity_changed().unwrap();
|
||||
runner.set_push_notification_token("token").unwrap();
|
||||
runner.set_push_notification_topic("topic").unwrap();
|
||||
runner.set_push_notification_platform("android").unwrap();
|
||||
runner.loop_once().unwrap();
|
||||
let hash = InfoHash::get("filtered query");
|
||||
let seen = Arc::new(AtomicUsize::new(0));
|
||||
let callback_seen = seen.clone();
|
||||
let token = runner
|
||||
.listen_filtered(&hash, "WHERE value_type=17", move |value, expired| {
|
||||
if !expired {
|
||||
assert_eq!(value.id(), 42);
|
||||
callback_seen.fetch_add(1, Ordering::SeqCst);
|
||||
}
|
||||
true
|
||||
})
|
||||
.unwrap();
|
||||
let mut value = Value::with_id(b"query result", 42);
|
||||
value.set_value_type(17);
|
||||
value.set_user_type("regression").unwrap();
|
||||
runner.put(&hash, value, |_| {}, false);
|
||||
runner.loop_once().unwrap();
|
||||
assert_eq!(seen.load(Ordering::SeqCst), 1);
|
||||
let (sent, received) = mpsc::channel();
|
||||
runner
|
||||
.query(
|
||||
&hash,
|
||||
"SELECT id,user_type WHERE value_type=17",
|
||||
move |fields| {
|
||||
sent.send((fields.id, fields.user_type)).unwrap();
|
||||
true
|
||||
},
|
||||
|_| {},
|
||||
)
|
||||
.unwrap();
|
||||
runner.loop_once().unwrap();
|
||||
let (id, user_type) = received.recv_timeout(Duration::from_secs(5)).unwrap();
|
||||
assert_eq!(id, Some(42));
|
||||
assert_eq!(user_type, Some(b"regression".to_vec()));
|
||||
let mut push = runner
|
||||
.push_notification_received(&[("session", "regression")])
|
||||
.unwrap();
|
||||
runner.loop_once().unwrap();
|
||||
assert!(push.poll().unwrap().is_some());
|
||||
runner.cancel_listen(&hash, token);
|
||||
runner.join().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn threaded_sequence_filters_roundtrip() {
|
||||
let mut source = DhtRunner::new();
|
||||
let mut destination = DhtRunner::new();
|
||||
let mut config = DhtRunnerConfig::new();
|
||||
config.dht_config.id = Some(DhtIdentity::new_ec("sequence publisher").unwrap());
|
||||
source.run_bind("127.0.0.1", "::1", 0, config).unwrap();
|
||||
destination
|
||||
.run_bind("127.0.0.1", "::1", 0, DhtRunnerConfig::new())
|
||||
.unwrap();
|
||||
let (sent, received) = mpsc::channel();
|
||||
destination.ping(
|
||||
([127, 0, 0, 1], source.bound_port(libc::AF_INET as _)).into(),
|
||||
move |ok| {
|
||||
sent.send(ok).unwrap();
|
||||
},
|
||||
);
|
||||
assert!(received.recv_timeout(Duration::from_secs(10)).unwrap());
|
||||
|
||||
let hash = InfoHash::get("network sequence filter");
|
||||
let (sent, received) = mpsc::channel();
|
||||
let token = destination
|
||||
.listen_filtered(&hash, "WHERE seq=7", move |value, expired| {
|
||||
if !expired {
|
||||
let _ = sent.send((value.id(), value.seq(), value.as_bytes()));
|
||||
}
|
||||
true
|
||||
})
|
||||
.unwrap();
|
||||
let mut value = Value::with_id(b"sequence seven", 42);
|
||||
value.set_seq(7);
|
||||
let (sent, done) = mpsc::channel();
|
||||
source.put_signed(
|
||||
&hash,
|
||||
value,
|
||||
move |ok| {
|
||||
let _ = sent.send(ok);
|
||||
},
|
||||
false,
|
||||
);
|
||||
assert!(done.recv_timeout(Duration::from_secs(10)).unwrap());
|
||||
assert_eq!(
|
||||
received.recv_timeout(Duration::from_secs(10)).unwrap(),
|
||||
(42, 7, b"sequence seven".to_vec())
|
||||
);
|
||||
|
||||
let (sent, received) = mpsc::channel();
|
||||
let (completed, done) = mpsc::channel();
|
||||
destination
|
||||
.get_filtered(
|
||||
&hash,
|
||||
"WHERE seq=7",
|
||||
move |value| {
|
||||
let _ = sent.send((value.id(), value.seq()));
|
||||
true
|
||||
},
|
||||
move |ok| {
|
||||
let _ = completed.send(ok);
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
received.recv_timeout(Duration::from_secs(10)).unwrap(),
|
||||
(42, 7)
|
||||
);
|
||||
assert!(done.recv_timeout(Duration::from_secs(10)).unwrap());
|
||||
|
||||
let (sent, received) = mpsc::channel();
|
||||
let (completed, done) = mpsc::channel();
|
||||
destination
|
||||
.query(
|
||||
&hash,
|
||||
"SELECT id,seq WHERE seq=7",
|
||||
move |row| {
|
||||
let _ = sent.send((row.id, row.seq));
|
||||
true
|
||||
},
|
||||
move |ok| {
|
||||
let _ = completed.send(ok);
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
received.recv_timeout(Duration::from_secs(10)).unwrap(),
|
||||
(Some(42), Some(7))
|
||||
);
|
||||
assert!(done.recv_timeout(Duration::from_secs(10)).unwrap());
|
||||
|
||||
let (sent, received) = mpsc::channel();
|
||||
let (completed, done) = mpsc::channel();
|
||||
destination
|
||||
.get_filtered(
|
||||
&hash,
|
||||
"WHERE seq=8",
|
||||
move |_| {
|
||||
let _ = sent.send(());
|
||||
true
|
||||
},
|
||||
move |ok| {
|
||||
let _ = completed.send(ok);
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert!(done.recv_timeout(Duration::from_secs(10)).unwrap());
|
||||
assert!(received.try_recv().is_err());
|
||||
destination.cancel_listen(&hash, token);
|
||||
destination.join().unwrap();
|
||||
source.join().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn threaded_signed_and_encrypted_roundtrips() {
|
||||
let mut source = DhtRunner::new();
|
||||
let mut destination = DhtRunner::new();
|
||||
for (runner, name) in [(&mut source, "source"), (&mut destination, "destination")] {
|
||||
let mut config = DhtRunnerConfig::new();
|
||||
config.dht_config.id = Some(DhtIdentity::new(name).unwrap());
|
||||
runner.run_bind("127.0.0.1", "::1", 0, config).unwrap();
|
||||
}
|
||||
let recipient = destination.public_key().unwrap().long_id();
|
||||
let (sent, received) = mpsc::channel();
|
||||
source.ping(
|
||||
([127, 0, 0, 1], destination.bound_port(libc::AF_INET as _)).into(),
|
||||
move |ok| {
|
||||
sent.send(ok).unwrap();
|
||||
},
|
||||
);
|
||||
assert!(received.recv_timeout(Duration::from_secs(10)).unwrap());
|
||||
for encrypted in [false, true] {
|
||||
let hash = InfoHash::get(if encrypted {
|
||||
"threaded encrypted"
|
||||
} else {
|
||||
"threaded signed"
|
||||
});
|
||||
let (sent, received) = mpsc::channel();
|
||||
let token = destination
|
||||
.listen(&hash, move |value, expired| {
|
||||
if !expired {
|
||||
let _ = sent.send(value.as_bytes());
|
||||
}
|
||||
true
|
||||
})
|
||||
.unwrap();
|
||||
let (sent, done) = mpsc::channel();
|
||||
let callback = move |ok| {
|
||||
let _ = sent.send(ok);
|
||||
};
|
||||
if encrypted {
|
||||
source.put_encrypted(
|
||||
&hash,
|
||||
&recipient,
|
||||
Value::new("network value"),
|
||||
callback,
|
||||
false,
|
||||
);
|
||||
} else {
|
||||
source.put_signed(&hash, Value::new("network value"), callback, false);
|
||||
}
|
||||
assert!(done.recv_timeout(Duration::from_secs(15)).unwrap());
|
||||
assert_eq!(
|
||||
received.recv_timeout(Duration::from_secs(15)).unwrap(),
|
||||
b"network value"
|
||||
);
|
||||
destination.cancel_listen(&hash, token);
|
||||
}
|
||||
for runner in [&mut source, &mut destination] {
|
||||
let (sent, received) = mpsc::channel();
|
||||
runner.shutdown(move || {
|
||||
sent.send(()).unwrap();
|
||||
});
|
||||
received.recv_timeout(Duration::from_secs(15)).unwrap();
|
||||
runner.join().unwrap();
|
||||
}
|
||||
}
|
||||
@@ -36,4 +36,19 @@ base64_decode(std::string_view str)
|
||||
return buffer;
|
||||
}
|
||||
|
||||
std::vector<unsigned char>
|
||||
base64_default_or_url_decode(std::string_view str)
|
||||
{
|
||||
std::vector<unsigned char> buffer(simdutf::maximal_binary_length_from_base64(str.data(), str.size()));
|
||||
simdutf::result r = simdutf::base64_to_binary(str.data(),
|
||||
str.size(),
|
||||
(char*) buffer.data(),
|
||||
simdutf::base64_options::base64_default_or_url);
|
||||
if (r.error) {
|
||||
throw std::invalid_argument(concat("Invalid base64 input: "sv, simdutf::error_to_string(r.error)));
|
||||
} else {
|
||||
buffer.resize(r.count);
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
} // namespace dht
|
||||
+8
-1
@@ -21,5 +21,12 @@ std::string base64_encode(const std::vector<unsigned char>& str);
|
||||
* @return a base64-decoded buffer
|
||||
*/
|
||||
std::vector<unsigned char> base64_decode(std::string_view str);
|
||||
/**
|
||||
* Decode a buffer in base64 or url style base64 (including '-' '_' ).
|
||||
*
|
||||
* @param str the input buffer
|
||||
* @return a base64-decoded buffer
|
||||
*/
|
||||
std::vector<unsigned char> base64_default_or_url_decode(std::string_view str);
|
||||
|
||||
} // namespace dht
|
||||
} // namespace dht
|
||||
+41
-76
@@ -571,6 +571,12 @@ PublicKey::toString() const
|
||||
}
|
||||
if (err != GNUTLS_E_SUCCESS)
|
||||
throw CryptoException(std::string("Could not print public key: ") + gnutls_strerror(err));
|
||||
// GnuTLS reports the PEM size including the terminating null byte, which
|
||||
// does not belong to the text and would end up embedded in whatever the
|
||||
// string is copied into, such as the "owner" field of a serialized value.
|
||||
ret.resize(sz);
|
||||
if (not ret.empty() and ret.back() == '\0')
|
||||
ret.pop_back();
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -592,7 +598,9 @@ PublicKey::checkSignature(const uint8_t* data, size_t data_len, const uint8_t* s
|
||||
return false;
|
||||
const gnutls_datum_t sig {(uint8_t*) signature, (unsigned) signature_len};
|
||||
const gnutls_datum_t dat {(uint8_t*) data, (unsigned) data_len};
|
||||
int rc = gnutls_pubkey_verify_data2(pk, GNUTLS_SIGN_RSA_SHA512, 0, &dat, &sig);
|
||||
auto algorithm = gnutls_pk_to_sign(static_cast<gnutls_pk_algorithm_t>(gnutls_pubkey_get_pk_algorithm(pk, nullptr)),
|
||||
GNUTLS_DIG_SHA512);
|
||||
int rc = gnutls_pubkey_verify_data2(pk, algorithm, 0, &dat, &sig);
|
||||
return rc >= 0;
|
||||
}
|
||||
|
||||
@@ -1288,14 +1296,16 @@ PrivateKey::generate(unsigned key_length, gnutls_pk_algorithm_t algo)
|
||||
}
|
||||
|
||||
PrivateKey
|
||||
PrivateKey::generateEC()
|
||||
PrivateKey::generateEC(gnutls_ecc_curve_t curve)
|
||||
{
|
||||
gnutls_x509_privkey_t key;
|
||||
if (gnutls_x509_privkey_init(&key) != GNUTLS_E_SUCCESS)
|
||||
throw CryptoException("Unable to initialize private key.");
|
||||
int err = gnutls_x509_privkey_generate(key,
|
||||
GNUTLS_PK_EC,
|
||||
gnutls_sec_param_to_pk_bits(GNUTLS_PK_EC, GNUTLS_SEC_PARAM_ULTRA),
|
||||
curve == GNUTLS_ECC_CURVE_INVALID
|
||||
? gnutls_sec_param_to_pk_bits(GNUTLS_PK_EC, GNUTLS_SEC_PARAM_ULTRA)
|
||||
: GNUTLS_CURVE_TO_BITS(curve),
|
||||
0);
|
||||
if (err != GNUTLS_E_SUCCESS) {
|
||||
gnutls_x509_privkey_deinit(key);
|
||||
@@ -1396,8 +1406,11 @@ setRandomSerial(gnutls_x509_crt_t cert)
|
||||
{
|
||||
std::random_device rdev;
|
||||
std::uniform_int_distribution<int64_t> dist {1};
|
||||
int64_t cert_serial = dist(rdev);
|
||||
gnutls_x509_crt_set_serial(cert, &cert_serial, sizeof(cert_serial));
|
||||
const auto cert_serial = dist(rdev);
|
||||
std::array<uint8_t, sizeof(cert_serial)> packed_serial;
|
||||
for (size_t i = 0; i < packed_serial.size(); i++)
|
||||
packed_serial[i] = cert_serial >> (8 * (packed_serial.size() - 1 - i));
|
||||
gnutls_x509_crt_set_serial(cert, packed_serial.data(), packed_serial.size());
|
||||
}
|
||||
|
||||
Certificate
|
||||
@@ -1648,24 +1661,8 @@ OcspResponse::toString(const bool compact) const
|
||||
gnutls_ocsp_cert_status_t
|
||||
OcspResponse::getCertificateStatus() const
|
||||
{
|
||||
int ret;
|
||||
unsigned int status;
|
||||
// MSVC/vcpkg GnuTLS cast workaround
|
||||
#if _WIN32 && (GNUTLS_VERSION_NUMBER == 0x030807)
|
||||
ret = gnutls_ocsp_resp_get_single(response,
|
||||
0,
|
||||
NULL,
|
||||
NULL,
|
||||
NULL,
|
||||
NULL,
|
||||
reinterpret_cast<gnutls_ocsp_cert_status_t*>(&status),
|
||||
NULL,
|
||||
NULL,
|
||||
NULL,
|
||||
NULL);
|
||||
#else
|
||||
ret = gnutls_ocsp_resp_get_single(response, 0, NULL, NULL, NULL, NULL, &status, NULL, NULL, NULL, NULL);
|
||||
#endif
|
||||
int ret = gnutls_ocsp_resp_get_single(response, 0, NULL, NULL, NULL, NULL, &status, NULL, NULL, NULL, NULL);
|
||||
if (ret < 0)
|
||||
throw CryptoException(gnutls_strerror(ret));
|
||||
return (gnutls_ocsp_cert_status_t) status;
|
||||
@@ -1724,22 +1721,7 @@ OcspResponse::verifyDirect(const Certificate& crt, const Blob& nonce)
|
||||
|
||||
// Check certificate revocation status
|
||||
unsigned int status_ocsp;
|
||||
// MSVC/vcpkg GnuTLS cast workaround
|
||||
#if _WIN32 && (GNUTLS_VERSION_NUMBER == 0x030807)
|
||||
ret = gnutls_ocsp_resp_get_single(response,
|
||||
0,
|
||||
NULL,
|
||||
NULL,
|
||||
NULL,
|
||||
NULL,
|
||||
reinterpret_cast<gnutls_ocsp_cert_status_t*>(&status_ocsp),
|
||||
NULL,
|
||||
NULL,
|
||||
NULL,
|
||||
NULL);
|
||||
#else
|
||||
ret = gnutls_ocsp_resp_get_single(response, 0, NULL, NULL, NULL, NULL, &status_ocsp, NULL, NULL, NULL, NULL);
|
||||
#endif
|
||||
if (ret < 0)
|
||||
throw CryptoException(gnutls_strerror(ret));
|
||||
return (gnutls_ocsp_cert_status_t) status_ocsp;
|
||||
@@ -1873,34 +1855,8 @@ RevocationList::getUpdateTime() const
|
||||
return std::chrono::system_clock::from_time_t(t);
|
||||
}
|
||||
|
||||
enum class Endian : uint32_t { LITTLE = 0, BIG = 1 };
|
||||
|
||||
template<typename T>
|
||||
T
|
||||
endian(T w, Endian endian = Endian::BIG)
|
||||
{
|
||||
// this gets optimized out into if (endian == host_endian) return w;
|
||||
union {
|
||||
uint64_t quad;
|
||||
uint32_t islittle;
|
||||
} t;
|
||||
t.quad = 1;
|
||||
if (t.islittle ^ (uint32_t) endian)
|
||||
return w;
|
||||
T r = 0;
|
||||
|
||||
// decent compilers will unroll this (gcc)
|
||||
// or even convert straight into single bswap (clang)
|
||||
for (size_t i = 0; i < sizeof(r); i++) {
|
||||
r <<= 8;
|
||||
r |= w & 0xff;
|
||||
w >>= 8;
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
void
|
||||
RevocationList::sign(const PrivateKey& key, const Certificate& ca, duration validity)
|
||||
RevocationList::sign(const PrivateKey& key, const Certificate& ca, duration validity, uint64_t crlNumber)
|
||||
{
|
||||
if (auto err = gnutls_x509_crl_set_version(crl, 2))
|
||||
throw CryptoException(std::string("Unable to set CRL version: ") + gnutls_strerror(err));
|
||||
@@ -1910,19 +1866,28 @@ RevocationList::sign(const PrivateKey& key, const Certificate& ca, duration vali
|
||||
throw CryptoException(std::string("Unable to set CRL update time: ") + gnutls_strerror(err));
|
||||
if (auto err = gnutls_x509_crl_set_next_update(crl, std::chrono::system_clock::to_time_t(next_update)))
|
||||
throw CryptoException(std::string("Unable to set CRL next update time: ") + gnutls_strerror(err));
|
||||
uint64_t number {0};
|
||||
size_t number_sz {sizeof(number)};
|
||||
unsigned critical {0};
|
||||
gnutls_x509_crl_get_number(crl, &number, &number_sz, &critical);
|
||||
uint64_t number {crlNumber};
|
||||
if (number == 0) {
|
||||
// initialize to a random number
|
||||
number_sz = sizeof(number);
|
||||
std::random_device rdev;
|
||||
std::generate_n((uint8_t*) &number, sizeof(number), std::bind(rand_byte, std::ref(rdev)));
|
||||
} else
|
||||
number = endian(endian(number) + 1);
|
||||
if (auto err = gnutls_x509_crl_set_number(crl, &number, sizeof(number)))
|
||||
throw CryptoException(std::string("Unable to set CRL update time: ") + gnutls_strerror(err));
|
||||
// The CRL Number extension holds a DER integer of variable length, so it must be
|
||||
// read as a big-endian byte string rather than straight into an uint64_t.
|
||||
auto current = getNumber();
|
||||
for (uint8_t byte : current)
|
||||
number = (number << 8) | byte;
|
||||
if (number == 0) {
|
||||
// Initialize to a random number.
|
||||
std::random_device rdev;
|
||||
do {
|
||||
std::generate_n((uint8_t*) &number, sizeof(number), std::bind(rand_byte, std::ref(rdev)));
|
||||
} while (number == 0);
|
||||
} else
|
||||
number++;
|
||||
}
|
||||
// Store the number in network byte order.
|
||||
uint8_t packedNumber[sizeof(number)];
|
||||
for (size_t i = 0; i < sizeof(packedNumber); i++)
|
||||
packedNumber[i] = (uint8_t) ((number >> (8 * (sizeof(packedNumber) - 1 - i))) & 0xff);
|
||||
if (auto err = gnutls_x509_crl_set_number(crl, packedNumber, sizeof(packedNumber)))
|
||||
throw CryptoException(std::string("Unable to set CRL number: ") + gnutls_strerror(err));
|
||||
if (auto err = gnutls_x509_crl_sign2(crl, ca.cert, key.x509_key, GNUTLS_DIG_SHA512, 0))
|
||||
throw CryptoException(std::string("Unable to sign certificate revocation list: ") + gnutls_strerror(err));
|
||||
// to be able to actually use the CRL we need to serialize/deserialize it
|
||||
|
||||
+39
-13
@@ -304,7 +304,7 @@ Dht::searchNodeGetDone(const net::Request& req, net::RequestAnswer&& answer, std
|
||||
should not be sent anymore */
|
||||
for (auto& g : sr->callbacks) {
|
||||
auto& q = g.second.query;
|
||||
if (q->isSatisfiedBy(*query) and q != query) {
|
||||
if (q != query and g.second.isSatisfiedBy(*query)) {
|
||||
auto dummy_req = std::make_shared<net::Request>();
|
||||
dummy_req->cancel();
|
||||
srn->getStatus[q] = std::move(dummy_req);
|
||||
@@ -402,17 +402,18 @@ Dht::searchSendGetValues(Sp<Search> sr, SearchNode* pn, bool update)
|
||||
|
||||
std::weak_ptr<Search> ws = sr;
|
||||
auto cb = sr->callbacks.begin();
|
||||
static const auto ANY_QUERY = std::make_shared<Query>(Select {}, Where {}, true);
|
||||
static const Get ANY_GET {time_point::min(), {}, std::make_shared<Query>(Select {}, Where {}, true), {}, {}, {}};
|
||||
do { /* for all requests to send */
|
||||
SearchNode* n = nullptr;
|
||||
auto& query = sr->callbacks.empty() ? ANY_QUERY : cb->second.query;
|
||||
const auto& get = sr->callbacks.empty() ? ANY_GET : cb->second;
|
||||
const auto& query = get.query;
|
||||
const time_point up = (not sr->callbacks.empty() and update) ? sr->getLastGetTime(*query) : time_point::min();
|
||||
|
||||
if (pn and pn->canGet(now, up, query)) {
|
||||
if (pn and pn->canGet(now, up, get)) {
|
||||
n = pn;
|
||||
} else {
|
||||
for (auto& sn : sr->nodes) {
|
||||
if (sn->canGet(now, up, query)) {
|
||||
if (sn->canGet(now, up, get)) {
|
||||
n = sn.get();
|
||||
break;
|
||||
}
|
||||
@@ -2270,8 +2271,16 @@ Dht::onDisconnected()
|
||||
void
|
||||
Dht::bootstrap()
|
||||
{
|
||||
if (dht4.status != NodeStatus::Disconnected || dht6.status != NodeStatus::Disconnected)
|
||||
if (dht4.status != NodeStatus::Disconnected || dht6.status != NodeStatus::Disconnected) {
|
||||
// The job that led here has already run, and the scheduler does not
|
||||
// clear the handle to a job it has executed. onDisconnected() reads a
|
||||
// set handle as "a bootstrap is already scheduled" and does nothing,
|
||||
// so leaving it set here means the node never tries to rejoin the
|
||||
// network once it has lost it. Clearing it is what says that no
|
||||
// bootstrap is pending any more.
|
||||
bootstrapJob.reset();
|
||||
return;
|
||||
}
|
||||
if (logger_)
|
||||
logger_->debug("[{}] Bootstraping", myid.to_view());
|
||||
bootstrap_pending = false;
|
||||
@@ -2355,6 +2364,9 @@ Dht::confirmNodes()
|
||||
std::vector<ValuesExport>
|
||||
Dht::exportValues() const
|
||||
{
|
||||
const auto& now = scheduler.time();
|
||||
const auto system_now = system_clock::now();
|
||||
|
||||
std::vector<ValuesExport> e {};
|
||||
e.reserve(store.size());
|
||||
for (const auto& h : store) {
|
||||
@@ -2367,7 +2379,7 @@ Dht::exportValues() const
|
||||
pk.pack_array(vals.size());
|
||||
for (const auto& v : vals) {
|
||||
pk.pack_array(4);
|
||||
pk.pack(v.created.time_since_epoch().count());
|
||||
pk.pack(to_system_time(v.created, now, system_now));
|
||||
v.data->msgpack_pack(pk);
|
||||
if (const auto& store_addr = v.store_bucket ? v.store_bucket->getAddr() : SockAddr {}) {
|
||||
pk.pack_bin(store_addr.getLength());
|
||||
@@ -2375,7 +2387,7 @@ Dht::exportValues() const
|
||||
} else {
|
||||
pk.pack_bin(0);
|
||||
}
|
||||
pk.pack(v.expiration.time_since_epoch().count());
|
||||
pk.pack(to_system_time(v.expiration, now, system_now));
|
||||
}
|
||||
ve.second = {buffer.data(), buffer.data() + buffer.size()};
|
||||
e.push_back(std::move(ve));
|
||||
@@ -2387,6 +2399,7 @@ void
|
||||
Dht::importValues(const std::vector<ValuesExport>& import)
|
||||
{
|
||||
const auto& now = scheduler.time();
|
||||
const auto system_now = system_clock::now();
|
||||
|
||||
unsigned imported = 0, ignored = 0;
|
||||
size_t imported_size = 0;
|
||||
@@ -2411,8 +2424,15 @@ Dht::importValues(const std::vector<ValuesExport>& import)
|
||||
Value tmp_val;
|
||||
SockAddr store_addr;
|
||||
try {
|
||||
val_time = time_point {
|
||||
time_point::duration {valel.via.array.ptr[0].as<time_point::duration::rep>()}};
|
||||
const bool uses_system_time = valel.via.array.ptr[0].type == msgpack::type::EXT;
|
||||
if (uses_system_time) {
|
||||
val_time = from_system_time(valel.via.array.ptr[0].as<system_clock::time_point>(),
|
||||
now,
|
||||
system_now);
|
||||
} else {
|
||||
val_time = time_point {
|
||||
time_point::duration {valel.via.array.ptr[0].as<time_point::duration::rep>()}};
|
||||
}
|
||||
tmp_val.msgpack_unpack(valel.via.array.ptr[1]);
|
||||
if (valel.via.array.size >= 3) {
|
||||
auto addr_blob = valel.via.array.ptr[2].as<Blob>();
|
||||
@@ -2422,8 +2442,14 @@ Dht::importValues(const std::vector<ValuesExport>& import)
|
||||
}
|
||||
}
|
||||
if (valel.via.array.size >= 4) {
|
||||
expiration = time_point {
|
||||
time_point::duration {valel.via.array.ptr[3].as<time_point::duration::rep>()}};
|
||||
if (uses_system_time) {
|
||||
expiration = from_system_time(valel.via.array.ptr[3].as<system_clock::time_point>(),
|
||||
now,
|
||||
system_now);
|
||||
} else {
|
||||
expiration = time_point {
|
||||
time_point::duration {valel.via.array.ptr[3].as<time_point::duration::rep>()}};
|
||||
}
|
||||
}
|
||||
} catch (const std::exception&) {
|
||||
if (logger_)
|
||||
@@ -2633,7 +2659,7 @@ Dht::onGetValuesDone(const Sp<Node>& node, net::RequestAnswer& a, Sp<Search>& sr
|
||||
for (auto& getp : sr->callbacks) { /* call all callbacks for this search */
|
||||
auto& get = getp.second;
|
||||
if (not(get.get_cb or get.query_cb)
|
||||
or (orig_query and get.query and not get.query->isSatisfiedBy(*orig_query)))
|
||||
or (orig_query and get.query and not get.isSatisfiedBy(*orig_query)))
|
||||
continue;
|
||||
|
||||
if (get.query_cb) { /* in case of a request with query */
|
||||
|
||||
+35
-21
@@ -25,8 +25,10 @@ struct DhtProxyClient::OperationState
|
||||
|
||||
struct DhtProxyClient::Listener
|
||||
{
|
||||
Listener(OpValueCache&& c)
|
||||
: cache(std::move(c))
|
||||
// The proxy server is a single logical source that re-delivers values
|
||||
// (push refresh, listen restart): don't count duplicates.
|
||||
Listener(ValueCallback&& cb)
|
||||
: cache(std::move(cb), false)
|
||||
{}
|
||||
|
||||
OpValueCache cache;
|
||||
@@ -34,26 +36,26 @@ struct DhtProxyClient::Listener
|
||||
Sp<OperationState> opstate;
|
||||
std::shared_ptr<http::Request> request;
|
||||
#ifdef OPENDHT_PUSH_NOTIFICATIONS
|
||||
std::unique_ptr<asio::steady_timer> refreshSubscriberTimer;
|
||||
std::unique_ptr<asio::system_timer> refreshSubscriberTimer;
|
||||
#endif
|
||||
};
|
||||
|
||||
struct PermanentPut
|
||||
{
|
||||
PermanentPut(const Sp<Value>& v, std::unique_ptr<asio::steady_timer>&& j, const Sp<std::atomic_bool>& o)
|
||||
PermanentPut(const Sp<Value>& v, std::unique_ptr<asio::system_timer>&& j, const Sp<std::atomic_bool>& o)
|
||||
: value(v)
|
||||
, refreshPutTimer(std::move(j))
|
||||
, ok(o)
|
||||
{}
|
||||
|
||||
Sp<Value> value;
|
||||
std::unique_ptr<asio::steady_timer> refreshPutTimer;
|
||||
std::unique_ptr<asio::system_timer> refreshPutTimer;
|
||||
Sp<std::atomic_bool> ok;
|
||||
};
|
||||
|
||||
struct DhtProxyClient::ProxySearch
|
||||
{
|
||||
SearchCache ops {};
|
||||
SearchCache ops {false};
|
||||
std::unique_ptr<asio::steady_timer> opExpirationTimer;
|
||||
std::map<size_t, Listener> listeners {};
|
||||
std::map<Value::Id, PermanentPut> puts {};
|
||||
@@ -424,7 +426,7 @@ DhtProxyClient::put(const InfoHash& key, Sp<Value> val, DoneCallback cb, time_po
|
||||
auto& search = searches_[key];
|
||||
if (val->id) {
|
||||
auto id = val->id;
|
||||
auto refreshPutTimer = std::make_unique<asio::steady_timer>(httpContext_,
|
||||
auto refreshPutTimer = std::make_unique<asio::system_timer>(httpContext_,
|
||||
proxy::OP_TIMEOUT - proxy::OP_MARGIN);
|
||||
refreshPutTimer->async_wait(
|
||||
std::bind(&DhtProxyClient::handleRefreshPut, this, std::placeholders::_1, key, id));
|
||||
@@ -540,7 +542,7 @@ DhtProxyClient::doPut(const InfoHash& key, Sp<Value> val, DoneCallbackSimple cb,
|
||||
auto it = search.pendingPuts.find(val);
|
||||
if (it != search.pendingPuts.end()) {
|
||||
auto sok = std::make_shared<std::atomic_bool>(ok);
|
||||
auto refreshPutTimer = std::make_unique<asio::steady_timer>(httpContext_,
|
||||
auto refreshPutTimer = std::make_unique<asio::system_timer>(httpContext_,
|
||||
proxy::OP_TIMEOUT
|
||||
- proxy::OP_MARGIN);
|
||||
refreshPutTimer->async_wait(
|
||||
@@ -891,8 +893,8 @@ DhtProxyClient::listen(const InfoHash& key, ValueCallback cb, Value::Filter filt
|
||||
* (if the proxy crash for any reason)
|
||||
*/
|
||||
if (!l->second.refreshSubscriberTimer)
|
||||
l->second.refreshSubscriberTimer = std::make_unique<asio::steady_timer>(httpContext_);
|
||||
l->second.refreshSubscriberTimer->expires_at(std::chrono::steady_clock::now() + proxy::OP_TIMEOUT
|
||||
l->second.refreshSubscriberTimer = std::make_unique<asio::system_timer>(httpContext_);
|
||||
l->second.refreshSubscriberTimer->expires_at(std::chrono::system_clock::now() + proxy::OP_TIMEOUT
|
||||
- proxy::OP_MARGIN);
|
||||
l->second.refreshSubscriberTimer->async_wait(
|
||||
std::bind(&DhtProxyClient::handleResubscribe, this, std::placeholders::_1, key, token, opstate));
|
||||
@@ -1192,9 +1194,9 @@ DhtProxyClient::restartListeners(const asio::error_code& ec)
|
||||
for (auto& put : search.second.puts) {
|
||||
doPut(key, put.second.value, [ok = put.second.ok](bool result) { *ok = result; }, time_point::max(), true);
|
||||
if (!put.second.refreshPutTimer) {
|
||||
put.second.refreshPutTimer = std::make_unique<asio::steady_timer>(httpContext_);
|
||||
put.second.refreshPutTimer = std::make_unique<asio::system_timer>(httpContext_);
|
||||
}
|
||||
put.second.refreshPutTimer->expires_at(std::chrono::steady_clock::now() + proxy::OP_TIMEOUT
|
||||
put.second.refreshPutTimer->expires_at(std::chrono::system_clock::now() + proxy::OP_TIMEOUT
|
||||
- proxy::OP_MARGIN);
|
||||
put.second.refreshPutTimer->async_wait(
|
||||
std::bind(&DhtProxyClient::handleRefreshPut, this, std::placeholders::_1, key, put.first));
|
||||
@@ -1275,10 +1277,10 @@ DhtProxyClient::pushNotificationReceived([[maybe_unused]] const std::map<std::st
|
||||
auto vid = std::stoull(vidIt->second);
|
||||
auto& put = search.puts.at(vid);
|
||||
if (!put.refreshPutTimer)
|
||||
put.refreshPutTimer = std::make_unique<asio::steady_timer>(httpContext_,
|
||||
std::chrono::steady_clock::now());
|
||||
put.refreshPutTimer = std::make_unique<asio::system_timer>(httpContext_,
|
||||
std::chrono::system_clock::now());
|
||||
else
|
||||
put.refreshPutTimer->expires_at(std::chrono::steady_clock::now());
|
||||
put.refreshPutTimer->expires_at(std::chrono::system_clock::now());
|
||||
put.refreshPutTimer->async_wait(
|
||||
std::bind(&DhtProxyClient::handleRefreshPut, this, std::placeholders::_1, key, vid));
|
||||
ret = PushNotificationResult::PutRefresh;
|
||||
@@ -1290,12 +1292,12 @@ DhtProxyClient::pushNotificationReceived([[maybe_unused]] const std::map<std::st
|
||||
: PushNotificationResult::ListenRefresh;
|
||||
}
|
||||
} else {
|
||||
auto key = InfoHash(notification.at("key"));
|
||||
system_clock::time_point sendTime = system_clock::time_point::min();
|
||||
try {
|
||||
sendTime = system_clock::time_point(std::chrono::milliseconds(std::stoull(notification.at("t"))));
|
||||
} catch (...) {
|
||||
}
|
||||
auto key = InfoHash(notification.at("key"));
|
||||
auto& search = searches_.at(key);
|
||||
auto expired = notification.find("exp");
|
||||
for (auto& list : search.listeners) {
|
||||
@@ -1308,13 +1310,25 @@ DhtProxyClient::pushNotificationReceived([[maybe_unused]] const std::map<std::st
|
||||
if (expired == notification.end()) {
|
||||
auto cb = list.second.cb;
|
||||
auto oldValues = list.second.cache.getValues();
|
||||
auto receivedIds = std::make_shared<std::set<Value::Id>>();
|
||||
get(
|
||||
key,
|
||||
[cb, sendTime](const std::vector<Sp<Value>>& vals) { return cb(vals, false, sendTime); },
|
||||
[cb, oldValues, sendTime](bool /*ok*/) {
|
||||
[cb, sendTime, receivedIds](const std::vector<Sp<Value>>& vals) {
|
||||
for (const auto& v : vals)
|
||||
receivedIds->insert(v->id);
|
||||
return cb(vals, false, sendTime);
|
||||
},
|
||||
[cb, oldValues, sendTime, receivedIds](bool ok) {
|
||||
if (!ok)
|
||||
return;
|
||||
// Decrement old values refcount to expire values not
|
||||
// present in the new list
|
||||
cb(oldValues, true, sendTime);
|
||||
std::vector<Sp<Value>> toExpire;
|
||||
for (const auto& v : oldValues)
|
||||
if (receivedIds->find(v->id) == receivedIds->end())
|
||||
toExpire.emplace_back(v);
|
||||
if (!toExpire.empty())
|
||||
cb(toExpire, true, sendTime);
|
||||
});
|
||||
ret = PushNotificationResult::Values;
|
||||
} else {
|
||||
@@ -1378,9 +1392,9 @@ DhtProxyClient::resubscribe([[maybe_unused]] const InfoHash& key,
|
||||
header.method(restinio::http_method_subscribe());
|
||||
header.request_target("/key/" + key.toString());
|
||||
if (!listener.refreshSubscriberTimer) {
|
||||
listener.refreshSubscriberTimer = std::make_unique<asio::steady_timer>(httpContext_);
|
||||
listener.refreshSubscriberTimer = std::make_unique<asio::system_timer>(httpContext_);
|
||||
}
|
||||
listener.refreshSubscriberTimer->expires_at(std::chrono::steady_clock::now() + proxy::OP_TIMEOUT - proxy::OP_MARGIN);
|
||||
listener.refreshSubscriberTimer->expires_at(std::chrono::system_clock::now() + proxy::OP_TIMEOUT - proxy::OP_MARGIN);
|
||||
listener.refreshSubscriberTimer->async_wait(
|
||||
std::bind(&DhtProxyClient::handleResubscribe, this, std::placeholders::_1, key, token, opstate));
|
||||
auto vcb = listener.cb;
|
||||
|
||||
+59
-51
@@ -210,7 +210,7 @@ DhtProxyServer::PermanentPut::msgpack_unpack(const msgpack::object& o)
|
||||
clientId = cid->as<std::string>();
|
||||
}
|
||||
if (auto exp = findMapValue(o, "exp"sv)) {
|
||||
expiration = from_time_t(exp->as<time_t>());
|
||||
expiration = sys_clock::from_time_t(exp->as<time_t>());
|
||||
}
|
||||
if (auto token = findMapValue(o, "token"sv)) {
|
||||
pushToken = token->as<std::string>();
|
||||
@@ -240,7 +240,7 @@ DhtProxyServer::Listener::msgpack_unpack(const msgpack::object& o)
|
||||
clientId = cid->as<std::string>();
|
||||
}
|
||||
if (auto exp = findMapValue(o, "exp"sv)) {
|
||||
expiration = from_time_t(exp->as<time_t>());
|
||||
expiration = sys_clock::from_time_t(exp->as<time_t>());
|
||||
}
|
||||
if (auto sid = findMapValue(o, "sid"sv)) {
|
||||
if (not sessionCtx)
|
||||
@@ -414,7 +414,7 @@ DhtProxyServer::loadState(Is& is, size_t size)
|
||||
logger_->debug("[proxy:server] Loading {} persistent puts", puts_.size());
|
||||
for (auto& put : puts_) {
|
||||
for (auto& pput : put.second.puts) {
|
||||
pput.second.expireTimer = std::make_unique<asio::steady_timer>(io_context(),
|
||||
pput.second.expireTimer = std::make_unique<asio::system_timer>(io_context(),
|
||||
pput.second.expiration);
|
||||
pput.second.expireTimer->async_wait(std::bind(&DhtProxyServer::handleCancelPermamentPut,
|
||||
this,
|
||||
@@ -437,7 +437,7 @@ DhtProxyServer::loadState(Is& is, size_t size)
|
||||
}
|
||||
return json;
|
||||
};
|
||||
pput.second.expireNotifyTimer = std::make_unique<asio::steady_timer>(io_context(),
|
||||
pput.second.expireNotifyTimer = std::make_unique<asio::system_timer>(io_context(),
|
||||
pput.second.expiration
|
||||
- proxy::OP_MARGIN);
|
||||
pput.second.expireNotifyTimer->async_wait(
|
||||
@@ -450,7 +450,7 @@ DhtProxyServer::loadState(Is& is, size_t size)
|
||||
pput.second.topic));
|
||||
}
|
||||
#endif
|
||||
dht_->put(put.first, pput.second.value, DoneCallbackSimple {}, time_point::max(), true);
|
||||
dht_->put(put.first, pput.second.value, DoneCallbackSimple {}, dht::time_point::max(), true);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -466,28 +466,31 @@ DhtProxyServer::loadState(Is& is, size_t size)
|
||||
for (auto& pushListener : pushListeners_) {
|
||||
for (auto& listeners : pushListener.second.listeners) {
|
||||
for (auto& listener : listeners.second) {
|
||||
if (not listener.sessionCtx)
|
||||
listener.sessionCtx = std::make_shared<PushSessionContext>("");
|
||||
{
|
||||
std::lock_guard l(listener.sessionCtx->lock);
|
||||
listener.sessionCtx->type = listener.type;
|
||||
listener.sessionCtx->topic = listener.topic;
|
||||
}
|
||||
// start listening
|
||||
listener.internalToken
|
||||
= dht_->listen(listeners.first,
|
||||
[this,
|
||||
infoHash = listeners.first,
|
||||
pushToken = pushListener.first,
|
||||
type = listener.type,
|
||||
clientId = listener.clientId,
|
||||
sessionCtx = listener.sessionCtx,
|
||||
topic = listener.topic](const std::vector<Sp<Value>>& values,
|
||||
bool expired) {
|
||||
sessionCtx = listener.sessionCtx](const std::vector<Sp<Value>>& values,
|
||||
bool expired) {
|
||||
return this->handlePushListen(infoHash,
|
||||
pushToken,
|
||||
type,
|
||||
clientId,
|
||||
sessionCtx,
|
||||
topic,
|
||||
values,
|
||||
expired);
|
||||
});
|
||||
// expire notify
|
||||
listener.expireNotifyTimer = std::make_unique<asio::steady_timer>(io_context(),
|
||||
listener.expireNotifyTimer = std::make_unique<asio::system_timer>(io_context(),
|
||||
listener.expiration
|
||||
- proxy::OP_MARGIN);
|
||||
auto jsonProvider = [infoHash = listeners.first.toString(),
|
||||
@@ -509,7 +512,7 @@ DhtProxyServer::loadState(Is& is, size_t size)
|
||||
listener.type,
|
||||
listener.topic));
|
||||
// cancel push listen
|
||||
listener.expireTimer = std::make_unique<asio::steady_timer>(io_context(),
|
||||
listener.expireTimer = std::make_unique<asio::system_timer>(io_context(),
|
||||
listener.expiration);
|
||||
listener.expireTimer->async_wait(std::bind(&DhtProxyServer::handleCancelPushListen,
|
||||
this,
|
||||
@@ -571,6 +574,18 @@ DhtProxyServer::~DhtProxyServer()
|
||||
ioContext_->stop();
|
||||
if (serverThread_.joinable())
|
||||
serverThread_.join();
|
||||
{
|
||||
// ~Request notifies DONE and its handler erases from requests_:
|
||||
// detach pending push requests so nothing calls back into this object.
|
||||
decltype(requests_) pending;
|
||||
{
|
||||
std::lock_guard l(requestLock_);
|
||||
pending = std::move(requests_);
|
||||
}
|
||||
for (auto& r : pending)
|
||||
r.second->add_on_state_change_callback({});
|
||||
pending.clear();
|
||||
}
|
||||
if (logger_)
|
||||
logger_->debug("[proxy:server] http server closed");
|
||||
}
|
||||
@@ -907,6 +922,7 @@ DhtProxyServer::subscribe(restinio::request_handle_t request, restinio::router::
|
||||
Json::Value r;
|
||||
auto* char_data = reinterpret_cast<const char*>(request->body().data());
|
||||
auto reader = std::unique_ptr<Json::CharReader>(jsonReaderBuilder_.newCharReader());
|
||||
|
||||
if (!reader->parse(char_data, char_data + request->body().size(), &r, &err)) {
|
||||
auto response = initHttpResponse(request->create_response(restinio::status_bad_request()));
|
||||
response.set_body(RESP_MSG_JSON_INCORRECT);
|
||||
@@ -944,21 +960,26 @@ DhtProxyServer::subscribe(restinio::request_handle_t request, restinio::router::
|
||||
listIt = std::prev(pushListeners.end());
|
||||
listIt->clientId = clientId;
|
||||
listIt->sessionCtx = std::make_shared<PushSessionContext>(sessionId);
|
||||
} else {
|
||||
}
|
||||
{
|
||||
// A refresh may come with another push platform or topic (e.g. a device
|
||||
// whose push configuration changed): values must then be pushed there.
|
||||
std::lock_guard l(listIt->sessionCtx->lock);
|
||||
listIt->sessionCtx->sessionId = sessionId;
|
||||
listIt->sessionCtx->type = type;
|
||||
listIt->sessionCtx->topic = topic;
|
||||
}
|
||||
auto& listener = *listIt;
|
||||
|
||||
// Expiration
|
||||
auto timeout = std::chrono::steady_clock::now() + proxy::OP_TIMEOUT;
|
||||
// Expiration (wall-clock: persisted as epoch time, survives restarts)
|
||||
auto timeout = sys_clock::now() + proxy::OP_TIMEOUT;
|
||||
listener.expiration = timeout;
|
||||
listener.type = type;
|
||||
listener.topic = topic;
|
||||
if (listener.expireNotifyTimer)
|
||||
listener.expireNotifyTimer->expires_at(timeout - proxy::OP_MARGIN);
|
||||
else
|
||||
listener.expireNotifyTimer = std::make_unique<asio::steady_timer>(io_context(), timeout - proxy::OP_MARGIN);
|
||||
listener.expireNotifyTimer = std::make_unique<asio::system_timer>(io_context(), timeout - proxy::OP_MARGIN);
|
||||
auto jsonProvider = [h = infoHash.toString(), clientId, sessionCtx = listener.sessionCtx]() {
|
||||
Json::Value json;
|
||||
json["timeout"] = h;
|
||||
@@ -975,7 +996,7 @@ DhtProxyServer::subscribe(restinio::request_handle_t request, restinio::router::
|
||||
listener.type,
|
||||
listener.topic));
|
||||
if (!listener.expireTimer)
|
||||
listener.expireTimer = std::make_unique<asio::steady_timer>(io_context(), timeout);
|
||||
listener.expireTimer = std::make_unique<asio::system_timer>(io_context(), timeout);
|
||||
else
|
||||
listener.expireTimer->expires_at(timeout);
|
||||
listener.expireTimer->async_wait(std::bind(&DhtProxyServer::handleCancelPushListen,
|
||||
@@ -1014,23 +1035,15 @@ DhtProxyServer::subscribe(restinio::request_handle_t request, restinio::router::
|
||||
// Add listen on dht
|
||||
if (logger_)
|
||||
logger_->debug("[proxy:server] [subscribe {}] new", infoHash);
|
||||
listener.internalToken = dht_->listen(infoHash,
|
||||
[this,
|
||||
infoHash,
|
||||
pushToken,
|
||||
type,
|
||||
clientId,
|
||||
sessionCtx = listener.sessionCtx,
|
||||
topic](const std::vector<Sp<Value>>& values, bool expired) {
|
||||
return this->handlePushListen(infoHash,
|
||||
pushToken,
|
||||
type,
|
||||
clientId,
|
||||
sessionCtx,
|
||||
topic,
|
||||
values,
|
||||
expired);
|
||||
});
|
||||
listener.internalToken = dht_->listen(
|
||||
infoHash,
|
||||
[this,
|
||||
infoHash,
|
||||
pushToken,
|
||||
clientId,
|
||||
sessionCtx = listener.sessionCtx](const std::vector<Sp<Value>>& values, bool expired) {
|
||||
return this->handlePushListen(infoHash, pushToken, clientId, sessionCtx, values, expired);
|
||||
});
|
||||
// Send response header
|
||||
auto response = initHttpResponse(request->create_response());
|
||||
response.set_body("{}\n");
|
||||
@@ -1137,10 +1150,8 @@ DhtProxyServer::handleCancelPushListen(const asio::error_code& ec,
|
||||
bool
|
||||
DhtProxyServer::handlePushListen(const InfoHash& infoHash,
|
||||
const std::string& pushToken,
|
||||
PushType type,
|
||||
const std::string& clientId,
|
||||
const std::shared_ptr<DhtProxyServer::PushSessionContext>& sessionCtx,
|
||||
const std::string& topic,
|
||||
const std::vector<std::shared_ptr<Value>>& values,
|
||||
bool expired)
|
||||
{
|
||||
@@ -1150,9 +1161,13 @@ DhtProxyServer::handlePushListen(const InfoHash& infoHash,
|
||||
using namespace std::chrono;
|
||||
json["t"] = Json::Value::Int64(duration_cast<milliseconds>(system_clock::now().time_since_epoch()).count());
|
||||
|
||||
PushType type;
|
||||
std::string topic;
|
||||
{
|
||||
std::lock_guard l(sessionCtx->lock);
|
||||
json["s"] = sessionCtx->sessionId;
|
||||
type = sessionCtx->type;
|
||||
topic = sessionCtx->topic;
|
||||
}
|
||||
|
||||
// Build a comma-separated list of ids from the values. This is intended to be used when
|
||||
@@ -1205,14 +1220,7 @@ DhtProxyServer::sendPushNotification(
|
||||
try {
|
||||
std::shared_ptr<http::Request> request;
|
||||
if (type == PushType::UnifiedPush) {
|
||||
http::Url tokenUrl(token);
|
||||
request = std::make_shared<http::Request>(io_context(),
|
||||
concat(tokenUrl.protocol, "://"sv, tokenUrl.host),
|
||||
tokenUrl.service,
|
||||
tokenUrl.protocol.find("https") == 0,
|
||||
logger_);
|
||||
request->set_target(tokenUrl.target);
|
||||
request->set_header_field(restinio::http_field_t::host, std::string(tokenUrl.host));
|
||||
request = std::make_shared<http::Request>(io_context(), token, logger_);
|
||||
} else {
|
||||
request = std::make_shared<http::Request>(io_context(),
|
||||
pushHostPort_.first,
|
||||
@@ -1231,8 +1239,8 @@ DhtProxyServer::sendPushNotification(
|
||||
Blob auth; ///< Auth secret
|
||||
auto delim = topicView.find('|');
|
||||
if (delim != std::string_view::npos) {
|
||||
pubKey = base64_decode(topicView.substr(0, delim));
|
||||
auth = base64_decode(topicView.substr(delim + 1));
|
||||
pubKey = base64_default_or_url_decode(topicView.substr(0, delim));
|
||||
auth = base64_default_or_url_decode(topicView.substr(delim + 1));
|
||||
}
|
||||
|
||||
request->set_header_field(restinio::http_field_t::ttl, "86400");
|
||||
@@ -1412,7 +1420,7 @@ DhtProxyServer::put(restinio::request_handle_t request, restinio::router::route_
|
||||
topic = pVal["topic"].asString();
|
||||
}
|
||||
std::lock_guard lock(lockSearchPuts_);
|
||||
auto timeout = std::chrono::steady_clock::now() + proxy::OP_TIMEOUT;
|
||||
auto timeout = sys_clock::now() + proxy::OP_TIMEOUT;
|
||||
auto& sPuts = puts_[infoHash];
|
||||
if (value->id == Value::INVALID_ID) {
|
||||
for (auto& pp : sPuts.puts) {
|
||||
@@ -1446,7 +1454,7 @@ DhtProxyServer::put(restinio::request_handle_t request, restinio::router::route_
|
||||
pput.expiration = timeout;
|
||||
if (not pput.expireTimer) {
|
||||
// cancel permanent put
|
||||
pput.expireTimer = std::make_unique<asio::steady_timer>(io_context(), timeout);
|
||||
pput.expireTimer = std::make_unique<asio::system_timer>(io_context(), timeout);
|
||||
#ifdef OPENDHT_PUSH_NOTIFICATIONS
|
||||
if (not pushToken.empty()) {
|
||||
pput.pushToken = pushToken;
|
||||
@@ -1485,7 +1493,7 @@ DhtProxyServer::put(restinio::request_handle_t request, restinio::router::route_
|
||||
return json;
|
||||
};
|
||||
if (!pput.expireNotifyTimer)
|
||||
pput.expireNotifyTimer = std::make_unique<asio::steady_timer>(io_context(),
|
||||
pput.expireNotifyTimer = std::make_unique<asio::system_timer>(io_context(),
|
||||
timeout - proxy::OP_MARGIN);
|
||||
else
|
||||
pput.expireNotifyTimer->expires_at(timeout - proxy::OP_MARGIN);
|
||||
@@ -1514,7 +1522,7 @@ DhtProxyServer::put(restinio::request_handle_t request, restinio::router::route_
|
||||
response.done();
|
||||
}
|
||||
},
|
||||
time_point::max(),
|
||||
dht::time_point::max(),
|
||||
permanent);
|
||||
return restinio::request_handling_status_t::accepted;
|
||||
} else {
|
||||
|
||||
+36
-3
@@ -99,6 +99,11 @@ DhtRunner::run(const Config& config, Context&& context)
|
||||
auto state_path = config.dht_config.node_config.persist_path;
|
||||
if (not state_path.empty())
|
||||
state_path += "_port.txt";
|
||||
// Ports loaded from the persisted state are a best-effort hint to keep a
|
||||
// stable identity across restarts; remember which ones we set so that we
|
||||
// can fall back to random ports if they are already in use (e.g. a fast
|
||||
// restart while the previous instance still holds them).
|
||||
bool triedSavedPort = false;
|
||||
if (not state_path.empty() && (local4.getPort() == 0 || local6.getPort() == 0)) {
|
||||
std::ifstream inConfig(state_path);
|
||||
if (inConfig.is_open()) {
|
||||
@@ -110,6 +115,7 @@ DhtRunner::run(const Config& config, Context&& context)
|
||||
fmt::ptr(this),
|
||||
port);
|
||||
local4.setPort(port);
|
||||
triedSavedPort = true;
|
||||
}
|
||||
}
|
||||
if (inConfig >> port) {
|
||||
@@ -119,6 +125,7 @@ DhtRunner::run(const Config& config, Context&& context)
|
||||
fmt::ptr(this),
|
||||
port);
|
||||
local6.setPort(port);
|
||||
triedSavedPort = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -136,7 +143,20 @@ DhtRunner::run(const Config& config, Context&& context)
|
||||
|
||||
if (config.proxy_server.empty()) {
|
||||
if (not context.sock) {
|
||||
context.sock.reset(new net::UdpSocket(local4, local6, context.logger));
|
||||
try {
|
||||
context.sock.reset(new net::UdpSocket(local4, local6, context.logger));
|
||||
} catch (const DhtException&) {
|
||||
if (not triedSavedPort)
|
||||
throw;
|
||||
// The saved ports are unavailable (typically still held by a
|
||||
// previous instance): fall back to OS-assigned ports.
|
||||
if (context.logger)
|
||||
context.logger->w("[runner %p] Saved ports unavailable, falling back to random ports",
|
||||
fmt::ptr(this));
|
||||
local4.setPort(0);
|
||||
local6.setPort(0);
|
||||
context.sock.reset(new net::UdpSocket(local4, local6, context.logger));
|
||||
}
|
||||
}
|
||||
context.sock->setOnReceive([&](net::PacketList&& pkts) {
|
||||
net::PacketList ret;
|
||||
@@ -387,6 +407,7 @@ DhtRunner::join()
|
||||
{
|
||||
std::lock_guard lck(dht_mtx);
|
||||
resetDht();
|
||||
statusCbs.clear();
|
||||
status4 = NodeStatus::Disconnected;
|
||||
status6 = NodeStatus::Disconnected;
|
||||
}
|
||||
@@ -521,6 +542,8 @@ void
|
||||
DhtRunner::registerType(const ValueType& type)
|
||||
{
|
||||
std::lock_guard lck(dht_mtx);
|
||||
if (not dht_)
|
||||
throw std::runtime_error("DHT runner is not running");
|
||||
dht_->registerType(type);
|
||||
}
|
||||
|
||||
@@ -534,8 +557,14 @@ DhtRunner::registerInsecureType(const ValueType& type)
|
||||
void
|
||||
DhtRunner::importValues(const std::vector<ValuesExport>& values)
|
||||
{
|
||||
std::lock_guard lck(dht_mtx);
|
||||
dht_->importValues(values);
|
||||
{
|
||||
std::lock_guard lck(dht_mtx);
|
||||
dht_->importValues(values);
|
||||
}
|
||||
// Recompute the runner wakeup after import schedules value expirations.
|
||||
std::lock_guard lck(storage_mtx);
|
||||
pending_ops_prio.emplace([](SecureDht&) {});
|
||||
cv.notify_all();
|
||||
}
|
||||
|
||||
unsigned
|
||||
@@ -1213,6 +1242,10 @@ DhtRunner::pushNotificationReceived([[maybe_unused]] const std::map<std::string,
|
||||
auto ret_token = std::make_shared<std::promise<PushNotificationResult>>();
|
||||
auto future = ret_token->get_future();
|
||||
std::lock_guard lck(storage_mtx);
|
||||
if (running == State::Idle) {
|
||||
ret_token->set_value(PushNotificationResult::IgnoredStopped);
|
||||
return future;
|
||||
}
|
||||
pending_ops_prio.emplace([ret_token, this, data](SecureDht&) {
|
||||
if (dht_)
|
||||
ret_token->set_value(dht_->pushNotificationReceived(data));
|
||||
|
||||
+50
-21
@@ -42,8 +42,8 @@ constexpr std::string_view ORIGIN_PROTOCOL {"//"sv};
|
||||
constexpr const char HTTP_HEADER_DELIM[] = "\r\n\r\n";
|
||||
constexpr unsigned MAX_REDIRECTS {5};
|
||||
|
||||
Url::Url(std::string_view url_str)
|
||||
: url(url_str)
|
||||
Url::Url(std::string url_str)
|
||||
: url(std::move(url_str))
|
||||
{
|
||||
std::string_view u = url;
|
||||
if (u.empty())
|
||||
@@ -233,11 +233,32 @@ Connection::Connection(asio::io_context& ctx,
|
||||
|
||||
Connection::~Connection()
|
||||
{
|
||||
close();
|
||||
// No asynchronous operation can be pending here: their handlers keep
|
||||
// the connection alive (see wrapCallback), so closing synchronously
|
||||
// is safe regardless of the calling thread.
|
||||
doClose();
|
||||
}
|
||||
|
||||
void
|
||||
Connection::close()
|
||||
{
|
||||
// asio sockets are not thread-safe. Closing from a thread other than the
|
||||
// one running the io_context races with in-flight operations on the same
|
||||
// socket (e.g. async_connect closing and reopening it to try the next
|
||||
// endpoint), corrupting the reactor's descriptor state.
|
||||
// Always perform the close on the io_context thread.
|
||||
if (ctx_.get_executor().running_in_this_thread()) {
|
||||
doClose();
|
||||
return;
|
||||
}
|
||||
asio::post(ctx_, [w = weak_from_this()] {
|
||||
if (auto sthis = w.lock())
|
||||
sthis->doClose();
|
||||
});
|
||||
}
|
||||
|
||||
void
|
||||
Connection::doClose()
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
asio::error_code ec;
|
||||
@@ -302,13 +323,13 @@ static X509*
|
||||
issuer_from_chain(STACK_OF(X509) * fullchain)
|
||||
{
|
||||
X509 *cert, *issuer;
|
||||
X509_NAME* issuer_name;
|
||||
const X509_NAME* issuer_name;
|
||||
|
||||
cert = cert_from_chain(fullchain);
|
||||
if ((issuer_name = X509_get_issuer_name(cert)) == nullptr)
|
||||
return nullptr;
|
||||
|
||||
issuer = X509_find_by_subject(fullchain, issuer_name);
|
||||
issuer = X509_find_by_subject(fullchain, const_cast<X509_NAME*>(issuer_name));
|
||||
return issuer;
|
||||
}
|
||||
|
||||
@@ -631,12 +652,13 @@ Connection::async_handshake(HandlerCb cb)
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
if (ssl_socket_) {
|
||||
std::weak_ptr<Connection> wthis = shared_from_this();
|
||||
// Keep the connection alive while the handshake is in flight, like
|
||||
// every other asynchronous operation (see wrapCallback).
|
||||
ssl_socket_
|
||||
->async_handshake(asio::ssl::stream<asio::ip::tcp::socket>::client, [wthis, cb](const asio::error_code& ec) {
|
||||
->async_handshake(asio::ssl::stream<asio::ip::tcp::socket>::client, [sthis = shared_from_this(), cb](const asio::error_code& ec) {
|
||||
if (ec == asio::error::operation_aborted)
|
||||
return;
|
||||
if (auto sthis = wthis.lock()) {
|
||||
{
|
||||
auto& this_ = *sthis;
|
||||
auto verify_ec = SSL_get_verify_result(this_.ssl_socket_->asio_ssl_stream().native_handle());
|
||||
if (this_.logger_) {
|
||||
@@ -830,8 +852,8 @@ Connection::timeout(const std::chrono::seconds& timeout, HandlerCb cb)
|
||||
|
||||
// Resolver
|
||||
|
||||
Resolver::Resolver(asio::io_context& ctx, const std::string& url, std::shared_ptr<dht::Logger> logger)
|
||||
: url_(url)
|
||||
Resolver::Resolver(asio::io_context& ctx, std::string url, std::shared_ptr<dht::Logger> logger)
|
||||
: url_(std::move(url))
|
||||
, resolver_(ctx)
|
||||
, destroyed_(std::make_shared<bool>(false))
|
||||
, logger_(logger)
|
||||
@@ -917,8 +939,16 @@ Resolver::add_callback(ResolverCb cb, sa_family_t family)
|
||||
else
|
||||
cb(ec, filter(endpoints, family));
|
||||
});
|
||||
else
|
||||
cb(ec_, family == AF_UNSPEC ? endpoints_ : filter(endpoints_, family));
|
||||
else {
|
||||
// Always dispatch the callback on the io_context thread. Invoking it
|
||||
// synchronously would run the caller's continuation (e.g. the whole
|
||||
// connect chain of Request::send) on the calling thread, mutating asio
|
||||
// objects concurrently with the io thread running the io_context.
|
||||
asio::post(resolver_.get_executor(),
|
||||
[cb = std::move(cb),
|
||||
ec = ec_,
|
||||
endpoints = family == AF_UNSPEC ? endpoints_ : filter(endpoints_, family)] { cb(ec, endpoints); });
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
@@ -967,14 +997,14 @@ Resolver::resolve(std::string_view host, std::string_view service)
|
||||
std::atomic_uint Request::ids_ {1};
|
||||
|
||||
Request::Request(asio::io_context& ctx,
|
||||
const std::string& url,
|
||||
std::string url,
|
||||
const Json::Value& json,
|
||||
OnJsonCb jsoncb,
|
||||
std::shared_ptr<dht::Logger> logger)
|
||||
: logger_(std::move(logger))
|
||||
, id_(Request::ids_++)
|
||||
, ctx_(ctx)
|
||||
, resolver_(std::make_shared<Resolver>(ctx, url, logger))
|
||||
, resolver_(std::make_shared<Resolver>(ctx, std::move(url), logger))
|
||||
{
|
||||
init_default_headers();
|
||||
set_header_field(restinio::http_field_t::content_type, std::string(HTTP_HEADER_CONTENT_TYPE_JSON));
|
||||
@@ -997,11 +1027,11 @@ Request::Request(asio::io_context& ctx,
|
||||
});
|
||||
}
|
||||
|
||||
Request::Request(asio::io_context& ctx, const std::string& url, OnJsonCb jsoncb, std::shared_ptr<dht::Logger> logger)
|
||||
Request::Request(asio::io_context& ctx, std::string url, OnJsonCb jsoncb, std::shared_ptr<dht::Logger> logger)
|
||||
: logger_(std::move(logger))
|
||||
, id_(Request::ids_++)
|
||||
, ctx_(ctx)
|
||||
, resolver_(std::make_shared<Resolver>(ctx, url, logger))
|
||||
, resolver_(std::make_shared<Resolver>(ctx, std::move(url), logger))
|
||||
{
|
||||
init_default_headers();
|
||||
set_header_field(restinio::http_field_t::accept, std::string(HTTP_HEADER_CONTENT_TYPE_JSON));
|
||||
@@ -1022,20 +1052,20 @@ Request::Request(asio::io_context& ctx, const std::string& url, OnJsonCb jsoncb,
|
||||
});
|
||||
}
|
||||
|
||||
Request::Request(asio::io_context& ctx, const std::string& url, std::shared_ptr<dht::Logger> logger)
|
||||
Request::Request(asio::io_context& ctx, std::string url, std::shared_ptr<dht::Logger> logger)
|
||||
: logger_(logger)
|
||||
, id_(Request::ids_++)
|
||||
, ctx_(ctx)
|
||||
, resolver_(std::make_shared<Resolver>(ctx, url, logger))
|
||||
, resolver_(std::make_shared<Resolver>(ctx, std::move(url), logger))
|
||||
{
|
||||
init_default_headers();
|
||||
}
|
||||
|
||||
Request::Request(asio::io_context& ctx, const std::string& url, OnDoneCb onDone, std::shared_ptr<dht::Logger> logger)
|
||||
Request::Request(asio::io_context& ctx, std::string url, OnDoneCb onDone, std::shared_ptr<dht::Logger> logger)
|
||||
: logger_(logger)
|
||||
, id_(Request::ids_++)
|
||||
, ctx_(ctx)
|
||||
, resolver_(std::make_shared<Resolver>(ctx, url, logger))
|
||||
, resolver_(std::make_shared<Resolver>(ctx, std::move(url), logger))
|
||||
{
|
||||
init_default_headers();
|
||||
add_on_done_callback(std::move(onDone));
|
||||
@@ -1086,7 +1116,6 @@ Request::Request(asio::io_context& ctx,
|
||||
, family_(family)
|
||||
, resolver_(resolver)
|
||||
{
|
||||
set_header_field(restinio::http_field_t::host, concat(get_url().host, ":", get_url().service));
|
||||
set_target(Url(target).target);
|
||||
}
|
||||
|
||||
|
||||
+128
-44
@@ -40,8 +40,44 @@ struct NetworkEngine::PartialMessage
|
||||
time_point start;
|
||||
time_point last_part;
|
||||
std::unique_ptr<ParsedMessage> msg;
|
||||
std::vector<std::unique_ptr<ParsedMessage>> early_parts;
|
||||
size_t early_bytes {};
|
||||
bool request_quota_consumed {};
|
||||
};
|
||||
|
||||
bool
|
||||
NetworkEngine::isExpectedReply(const ParsedMessage& msg, const SockAddr& from)
|
||||
{
|
||||
auto request = requests.find(msg.tid);
|
||||
if (request != requests.end() && request->second->pending() && !request->second->node->id
|
||||
&& request->second->node->getAddr() == from)
|
||||
return true;
|
||||
|
||||
auto node = cache.getNode(msg.id, from.getFamily());
|
||||
if (!node || node->getAddr() != from)
|
||||
return false;
|
||||
|
||||
auto pending = node->getRequest(msg.tid);
|
||||
return (pending && pending->pending()) || node->getSocket(msg.tid);
|
||||
}
|
||||
|
||||
bool
|
||||
NetworkEngine::tryCompleteMessage(PartialMessage& partial, const SockAddr& from)
|
||||
{
|
||||
try {
|
||||
if (!partial.msg->complete())
|
||||
return false;
|
||||
process(std::move(partial.msg), from);
|
||||
} catch (const std::exception& e) {
|
||||
if (logger_)
|
||||
logger_->warn("Error while processing partial message: {}", e.what());
|
||||
} catch (...) {
|
||||
if (logger_)
|
||||
logger_->warn("Unknown error while processing partial message");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<Blob>
|
||||
serializeValues(const std::vector<Sp<Value>>& st)
|
||||
{
|
||||
@@ -466,35 +502,58 @@ NetworkEngine::processMessage(const uint8_t* buf, size_t buflen, SockAddr f)
|
||||
|
||||
// partial value data
|
||||
if (msg->type == MessageType::ValueData) {
|
||||
auto pmsg_it = partial_messages.find(msg->tid);
|
||||
if (pmsg_it == partial_messages.end()) {
|
||||
if (logIncoming_)
|
||||
if (logger_)
|
||||
logger_->debug("Unable to find partial message");
|
||||
if (msg->fragment_parts.empty()
|
||||
|| std::any_of(msg->fragment_parts.begin(), msg->fragment_parts.end(), [](const auto& fragment) {
|
||||
return fragment.second.second.empty();
|
||||
})) {
|
||||
rateLimit(from);
|
||||
return;
|
||||
}
|
||||
if (!pmsg_it->second.from.equals(from)) {
|
||||
if (logger_)
|
||||
logger_->debug("Received partial message data from unexpected IP address");
|
||||
rateLimit(from);
|
||||
PartialMessageKey key {from, msg->tid};
|
||||
auto pmsg_it = partial_messages.find(key);
|
||||
if (pmsg_it == partial_messages.end()) {
|
||||
if (!rateLimit(from))
|
||||
return;
|
||||
auto inserted = partial_messages.emplace(key, PartialMessage {});
|
||||
pmsg_it = inserted.first;
|
||||
pmsg_it->second.from = from;
|
||||
pmsg_it->second.start = now;
|
||||
pmsg_it->second.last_part = now;
|
||||
pmsg_it->second.request_quota_consumed = true;
|
||||
scheduler.add(now + RX_MAX_PACKET_TIME, std::bind(&NetworkEngine::maintainRxBuffer, this, key));
|
||||
}
|
||||
auto& pmsg = pmsg_it->second;
|
||||
if (!pmsg.msg) {
|
||||
if (pmsg.early_parts.size() >= RX_MAX_EARLY_PARTS) {
|
||||
partial_messages.erase(pmsg_it);
|
||||
return;
|
||||
}
|
||||
size_t fragment_bytes {};
|
||||
for (const auto& [index, fragment] : msg->fragment_parts) {
|
||||
(void) index;
|
||||
if (fragment.second.size() > RX_MAX_EARLY_BYTES - fragment_bytes) {
|
||||
partial_messages.erase(pmsg_it);
|
||||
return;
|
||||
}
|
||||
fragment_bytes += fragment.second.size();
|
||||
}
|
||||
if (fragment_bytes > RX_MAX_EARLY_BYTES - pmsg.early_bytes) {
|
||||
partial_messages.erase(pmsg_it);
|
||||
return;
|
||||
}
|
||||
pmsg.early_bytes += fragment_bytes;
|
||||
pmsg.early_parts.emplace_back(std::move(msg));
|
||||
pmsg.last_part = now;
|
||||
scheduler.add(now + RX_TIMEOUT, std::bind(&NetworkEngine::maintainRxBuffer, this, key));
|
||||
return;
|
||||
}
|
||||
// append data block
|
||||
if (pmsg_it->second.msg->append(*msg)) {
|
||||
pmsg_it->second.last_part = now;
|
||||
// check data completion
|
||||
if (pmsg_it->second.msg->complete()) {
|
||||
try {
|
||||
// process the full message
|
||||
process(std::move(pmsg_it->second.msg), from);
|
||||
} catch (const std::exception& e) {
|
||||
if (logger_)
|
||||
logger_->warn("Error while processing partial message: {}", e.what());
|
||||
}
|
||||
if (pmsg.msg->append(*msg)) {
|
||||
pmsg.last_part = now;
|
||||
if (tryCompleteMessage(pmsg, from)) {
|
||||
partial_messages.erase(pmsg_it);
|
||||
} else
|
||||
scheduler.add(now + RX_TIMEOUT, std::bind(&NetworkEngine::maintainRxBuffer, this, msg->tid));
|
||||
scheduler.add(now + RX_TIMEOUT, std::bind(&NetworkEngine::maintainRxBuffer, this, key));
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -505,7 +564,32 @@ NetworkEngine::processMessage(const uint8_t* buf, size_t buflen, SockAddr f)
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg->type > MessageType::Reply) {
|
||||
if (msg->value_parts.empty()) {
|
||||
if (msg->type > MessageType::Reply) {
|
||||
/* Rate limit requests. */
|
||||
if (!rateLimit(from)) {
|
||||
if (logger_)
|
||||
logger_->warn("Dropping request due to rate limiting");
|
||||
return;
|
||||
}
|
||||
}
|
||||
try {
|
||||
process(std::move(msg), from);
|
||||
} catch (const std::exception& e) {
|
||||
if (logger_)
|
||||
logger_->warn("Error while processing message: {}", e.what());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
PartialMessageKey partial_key {from, msg->tid};
|
||||
if (msg->type == MessageType::Reply && !isExpectedReply(*msg, from))
|
||||
return;
|
||||
|
||||
const auto partial = partial_messages.find(partial_key);
|
||||
const bool request_quota_consumed = partial != partial_messages.end() && !partial->second.msg
|
||||
&& partial->second.request_quota_consumed;
|
||||
if (msg->type > MessageType::Reply && !request_quota_consumed) {
|
||||
/* Rate limit requests. */
|
||||
if (!rateLimit(from)) {
|
||||
if (logger_)
|
||||
@@ -514,28 +598,28 @@ NetworkEngine::processMessage(const uint8_t* buf, size_t buflen, SockAddr f)
|
||||
}
|
||||
}
|
||||
|
||||
if (msg->value_parts.empty()) {
|
||||
try {
|
||||
process(std::move(msg), from);
|
||||
} catch (const std::exception& e) {
|
||||
if (logger_)
|
||||
logger_->warn("Error while processing message: {}", e.what());
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// starting partial message session
|
||||
auto k = msg->tid;
|
||||
auto& pmsg = partial_messages[k];
|
||||
if (not pmsg.msg) {
|
||||
// starting partial message session
|
||||
auto inserted = partial_messages.emplace(partial_key, PartialMessage {});
|
||||
auto& pmsg = inserted.first->second;
|
||||
if (not pmsg.msg) {
|
||||
if (inserted.second) {
|
||||
pmsg.from = from;
|
||||
pmsg.msg = std::move(msg);
|
||||
pmsg.start = now;
|
||||
pmsg.last_part = now;
|
||||
scheduler.add(now + RX_MAX_PACKET_TIME, std::bind(&NetworkEngine::maintainRxBuffer, this, k));
|
||||
scheduler.add(now + RX_TIMEOUT, std::bind(&NetworkEngine::maintainRxBuffer, this, k));
|
||||
} else if (logger_)
|
||||
logger_->error("Partial message with given TID {} already exists", k);
|
||||
}
|
||||
scheduler.add(now + RX_MAX_PACKET_TIME, std::bind(&NetworkEngine::maintainRxBuffer, this, partial_key));
|
||||
}
|
||||
pmsg.msg = std::move(msg);
|
||||
pmsg.last_part = now;
|
||||
for (const auto& part : pmsg.early_parts)
|
||||
pmsg.msg->append(*part);
|
||||
pmsg.early_parts.clear();
|
||||
pmsg.early_bytes = 0;
|
||||
if (tryCompleteMessage(pmsg, from)) {
|
||||
partial_messages.erase(inserted.first);
|
||||
} else {
|
||||
scheduler.add(now + RX_TIMEOUT, std::bind(&NetworkEngine::maintainRxBuffer, this, partial_key));
|
||||
}
|
||||
} else if (logger_)
|
||||
logger_->error("Partial message from {} with given TID {} already exists", from.toString(), msg->tid);
|
||||
}
|
||||
|
||||
void
|
||||
@@ -1614,9 +1698,9 @@ NetworkEngine::sendError(const SockAddr& addr, Tid tid, uint16_t code, const std
|
||||
}
|
||||
|
||||
void
|
||||
NetworkEngine::maintainRxBuffer(Tid tid)
|
||||
NetworkEngine::maintainRxBuffer(const PartialMessageKey& key)
|
||||
{
|
||||
auto msg = partial_messages.find(tid);
|
||||
auto msg = partial_messages.find(key);
|
||||
if (msg != partial_messages.end()) {
|
||||
const auto& now = scheduler.time();
|
||||
if (msg->second.start + RX_MAX_PACKET_TIME < now || msg->second.last_part + RX_TIMEOUT < now) {
|
||||
|
||||
+27
-19
@@ -12,18 +12,20 @@ OpValueCache::onValuesAdded(const std::vector<Sp<Value>>& vals, const system_clo
|
||||
std::vector<Sp<Value>> newValues;
|
||||
for (const auto& v : vals) {
|
||||
auto viop = values.emplace(v->id, v);
|
||||
auto& stored = viop.first->second;
|
||||
if (viop.second) {
|
||||
newValues.emplace_back(v);
|
||||
} else if (*viop.first->second.data != *v) {
|
||||
// Special case for edition
|
||||
if (v->seq > viop.first->second.data->seq) {
|
||||
viop.first->second.data = v;
|
||||
newValues.emplace_back(v);
|
||||
}
|
||||
} else {
|
||||
viop.first->second.refCount++;
|
||||
} else if (*stored.data != *v) {
|
||||
if (v->seq <= stored.data->seq)
|
||||
continue; // older version from a lagging source
|
||||
// Edited: only the reporting source is known to hold this version
|
||||
stored.data = v;
|
||||
stored.refCount = 1;
|
||||
newValues.emplace_back(v);
|
||||
} else if (countSources) {
|
||||
stored.refCount++;
|
||||
}
|
||||
viop.first->second.updated = t;
|
||||
stored.updated = t;
|
||||
}
|
||||
return newValues.empty() ? true : callback(newValues, false);
|
||||
}
|
||||
@@ -37,6 +39,10 @@ OpValueCache::onValuesExpired(const std::vector<Sp<Value>>& vals, const system_c
|
||||
if (vit != values.end()) {
|
||||
if (vit->second.updated > t)
|
||||
continue;
|
||||
// Mirror onValuesAdded: a version that was never counted (different
|
||||
// content, not a newer seq) must not decrement the refcount
|
||||
if (*v != *vit->second.data and v->seq <= vit->second.data->seq)
|
||||
continue;
|
||||
|
||||
vit->second.updated = t;
|
||||
vit->second.refCount--;
|
||||
@@ -174,7 +180,7 @@ SearchCache::listen(const ValueCallback& get_cb, const Sp<Query>& q, Value::Filt
|
||||
auto op = getOp(q);
|
||||
if (op == ops.end()) {
|
||||
// New query
|
||||
op = ops.emplace(q, std::make_unique<OpCache>()).first;
|
||||
op = ops.emplace(q, std::make_unique<OpCache>(countSources_)).first;
|
||||
auto& cache = *op->second;
|
||||
cache.searchToken = onListen(
|
||||
q,
|
||||
@@ -224,7 +230,7 @@ SearchCache::expire(const time_point& now, const std::function<void(size_t)>& on
|
||||
auto ret = nextExpiration_;
|
||||
for (auto it = ops.begin(); it != ops.end();) {
|
||||
auto expiration = it->second->getExpiration();
|
||||
if (expiration < now) {
|
||||
if (expiration <= now) {
|
||||
auto cache = std::move(it->second);
|
||||
it = ops.erase(it);
|
||||
onCancel(cache->searchToken);
|
||||
@@ -240,15 +246,17 @@ SearchCache::expire(const time_point& now, const std::function<void(size_t)>& on
|
||||
bool
|
||||
SearchCache::get(const Value::Filter& f, const Sp<Query>& q, const GetCallback& gcb, const DoneCallback& dcb) const
|
||||
{
|
||||
if (not gcb)
|
||||
return false;
|
||||
auto op = getOp(q);
|
||||
if (op != ops.end()) {
|
||||
auto vals = op->second->get(f);
|
||||
if ((not vals.empty() and not gcb(vals)) or op->second->isSynced()) {
|
||||
dcb(true, {});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
if (op == ops.end() or not op->second->isSynced())
|
||||
return false;
|
||||
auto vals = op->second->get(f);
|
||||
if (not vals.empty())
|
||||
gcb(vals); // a consumer stopping early is not a failure
|
||||
if (dcb)
|
||||
dcb(true, {});
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<Sp<Value>>
|
||||
|
||||
+28
-12
@@ -21,19 +21,29 @@ struct OpCacheValueStorage
|
||||
class OpValueCache
|
||||
{
|
||||
public:
|
||||
OpValueCache(ValueCallback&& cb) noexcept
|
||||
/**
|
||||
* @param countSources When true, a value is kept until every source
|
||||
* that reported it has expired it (per-node caches,
|
||||
* IPv4/IPv6 searches, local storage). When false,
|
||||
* re-deliveries of the same value are not counted
|
||||
* and a single expiration removes it (single
|
||||
* logical source, e.g. a proxy server).
|
||||
*/
|
||||
OpValueCache(ValueCallback&& cb, bool countSources = true) noexcept
|
||||
: callback(std::forward<ValueCallback>(cb))
|
||||
, countSources(countSources)
|
||||
{}
|
||||
explicit OpValueCache(OpValueCache&& o) noexcept
|
||||
: values(std::move(o.values))
|
||||
, callback(std::move(o.callback))
|
||||
, countSources(o.countSources)
|
||||
{
|
||||
o.callback = {};
|
||||
}
|
||||
|
||||
static ValueCallback cacheCallback(ValueCallback&& cb, std::function<void()>&& onCancel)
|
||||
static ValueCallback cacheCallback(ValueCallback&& cb, std::function<void()>&& onCancel, bool countSources = true)
|
||||
{
|
||||
return [cache = std::make_shared<OpValueCache>(std::forward<ValueCallback>(cb)),
|
||||
return [cache = std::make_shared<OpValueCache>(std::forward<ValueCallback>(cb), countSources),
|
||||
onCancel = std::move(onCancel)](const std::vector<Sp<Value>>& vals, bool expired) {
|
||||
auto ret = cache->onValue(vals, expired);
|
||||
if (not ret)
|
||||
@@ -89,19 +99,22 @@ private:
|
||||
size_t syncedNodes {0};
|
||||
std::map<Value::Id, OpCacheValueStorage> values {};
|
||||
ValueCallback callback;
|
||||
bool countSources {true};
|
||||
};
|
||||
|
||||
class OpCache
|
||||
{
|
||||
public:
|
||||
OpCache()
|
||||
: cache([this](const std::vector<Sp<Value>>& vals, bool expired) {
|
||||
if (expired)
|
||||
onValuesExpired(vals);
|
||||
else
|
||||
onValuesAdded(vals);
|
||||
return true;
|
||||
})
|
||||
explicit OpCache(bool countSources = true)
|
||||
: cache(
|
||||
[this](const std::vector<Sp<Value>>& vals, bool expired) {
|
||||
if (expired)
|
||||
onValuesExpired(vals);
|
||||
else
|
||||
onValuesAdded(vals);
|
||||
return true;
|
||||
},
|
||||
countSources)
|
||||
{}
|
||||
|
||||
bool onValue(const std::vector<Sp<Value>>& vals, bool expired) { return cache.onValue(vals, expired); }
|
||||
@@ -156,7 +169,9 @@ private:
|
||||
class SearchCache
|
||||
{
|
||||
public:
|
||||
SearchCache() {}
|
||||
explicit SearchCache(bool countSources = true)
|
||||
: countSources_(countSources)
|
||||
{}
|
||||
SearchCache(SearchCache&&) = default;
|
||||
|
||||
using OnListen = std::function<size_t(Sp<Query>, ValueCallback, SyncCallback)>;
|
||||
@@ -192,6 +207,7 @@ private:
|
||||
|
||||
size_t nextToken_ {1};
|
||||
time_point nextExpiration_ {time_point::max()};
|
||||
bool countSources_ {true};
|
||||
};
|
||||
|
||||
} // namespace dht
|
||||
+14
-4
@@ -274,14 +274,21 @@ ParsedMessage::msgpack_unpack(const msgpack::object& msg)
|
||||
|
||||
if (!parsed.a && !parsed.r && !parsed.e && !parsed.u)
|
||||
throw msgpack::type_error();
|
||||
auto& req = parsed.a ? *parsed.a : (parsed.r ? *parsed.r : (parsed.u ? *parsed.u : *parsed.e));
|
||||
|
||||
if (parsed.e) {
|
||||
if (parsed.e->type != msgpack::type::ARRAY)
|
||||
if (parsed.e->type != msgpack::type::ARRAY or parsed.e->via.array.size < 1)
|
||||
throw msgpack::type_error();
|
||||
error_code = parsed.e->via.array.ptr[0].as<uint16_t>();
|
||||
}
|
||||
|
||||
/* An error-only message carries no request map. The error array must never
|
||||
be reached through the map view of the msgpack object union, and a
|
||||
request field that is not a map must be rejected rather than
|
||||
reinterpreted. */
|
||||
const msgpack::object* req = parsed.a ? parsed.a : (parsed.r ? parsed.r : parsed.u);
|
||||
if (req and req->type != msgpack::type::MAP)
|
||||
throw msgpack::type_error();
|
||||
|
||||
struct ParsedReq
|
||||
{
|
||||
msgpack::object* values;
|
||||
@@ -290,8 +297,9 @@ ParsedMessage::msgpack_unpack(const msgpack::object& msg)
|
||||
msgpack::object* want;
|
||||
} parsedReq {};
|
||||
|
||||
for (unsigned i = 0; i < req.via.map.size; i++) {
|
||||
auto& o = req.via.map.ptr[i];
|
||||
const uint32_t req_size = req ? req->via.map.size : 0;
|
||||
for (unsigned i = 0; i < req_size; i++) {
|
||||
auto& o = req->via.map.ptr[i];
|
||||
if (o.key.type != msgpack::type::STR)
|
||||
continue;
|
||||
auto key = o.key.as<std::string_view>();
|
||||
@@ -370,6 +378,8 @@ ParsedMessage::msgpack_unpack(const msgpack::object& msg)
|
||||
} else if (parsedReq.fields) {
|
||||
if (auto rfields = findMapValue(*parsedReq.fields, "f"sv)) {
|
||||
auto vfields = rfields->as<std::set<Value::Field>>();
|
||||
if (vfields.empty())
|
||||
throw msgpack::type_error();
|
||||
if (auto rvalues = findMapValue(*parsedReq.fields, "v"sv)) {
|
||||
if (rvalues->type != msgpack::type::ARRAY)
|
||||
throw msgpack::type_error();
|
||||
|
||||
+19
-4
@@ -21,6 +21,21 @@ struct Dht::Get
|
||||
QueryCallback query_cb;
|
||||
GetCallback get_cb;
|
||||
DoneCallback done_cb;
|
||||
|
||||
/**
|
||||
* Tells if the answer to `answered` can be reported to this operation as-is.
|
||||
*
|
||||
* A vanilla get applies its own filter to the values it receives, so a
|
||||
* broader answer is usable. A projected get only receives the fields
|
||||
* selected by `answered` and can no longer filter them, so it requires the
|
||||
* very same filters.
|
||||
*/
|
||||
bool isSatisfiedBy(const Query& answered) const
|
||||
{
|
||||
if (not query)
|
||||
return true;
|
||||
return query_cb ? query->isProjectionSatisfiedBy(answered) : query->isSatisfiedBy(answered);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -165,11 +180,11 @@ struct Dht::SearchNode
|
||||
*
|
||||
* @param now The time reference to now.
|
||||
* @param update The time of the last 'get' op satisfying this request.
|
||||
* @param q The query defining the "get" operation we're referring to.
|
||||
* @param get The "get" operation we're referring to.
|
||||
*
|
||||
* @return true if we can send get, else false.
|
||||
*/
|
||||
bool canGet(time_point now, time_point update, const Sp<Query>& q) const
|
||||
bool canGet(time_point now, time_point update, const Get& get) const
|
||||
{
|
||||
if (node->isExpired())
|
||||
return false;
|
||||
@@ -178,7 +193,7 @@ struct Dht::SearchNode
|
||||
for (const auto& s : getStatus) {
|
||||
if (s.second and s.second->pending())
|
||||
is_pending = true;
|
||||
if (s.first and q and q->isSatisfiedBy(*s.first) and s.second) {
|
||||
if (s.first and get.query and get.isSatisfiedBy(*s.first) and s.second) {
|
||||
if (s.second->pending())
|
||||
pending_sq_status = true;
|
||||
else if (s.second->completed() and not(update > s.second->reply_time))
|
||||
@@ -189,7 +204,7 @@ struct Dht::SearchNode
|
||||
}
|
||||
|
||||
return (not is_pending and now > last_get_reply + Node::NODE_EXPIRE_TIME)
|
||||
or not(completed_sq_status or pending_sq_status or hasStartedPagination(q));
|
||||
or not(completed_sq_status or pending_sq_status or hasStartedPagination(get.query));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+3
-1
@@ -232,6 +232,8 @@ Storage::store(const InfoHash& id, const Sp<Value>& value, time_point created, t
|
||||
if (it->data != value) {
|
||||
size_t size_old = it->data->size();
|
||||
ssize_t size_diff = size_new - (ssize_t) size_old;
|
||||
// Re-storing identical content is a refresh, not an edition
|
||||
size_t edited = (*it->data != *value) ? 1 : 0;
|
||||
// DHT_LOG.DEBUG("Updating %s -> %s", id.toString().c_str(), value->toString().c_str());
|
||||
// clear quota for previous value
|
||||
if (it->store_bucket)
|
||||
@@ -243,7 +245,7 @@ Storage::store(const InfoHash& id, const Sp<Value>& value, time_point created, t
|
||||
sb->insert(id, *value, expiration);
|
||||
it->data = value;
|
||||
total_size += size_diff;
|
||||
return std::make_pair(&(*it), StoreDiff {size_diff, 0, 0, 1});
|
||||
return std::make_pair(&(*it), StoreDiff {size_diff, 0, 0, edited});
|
||||
}
|
||||
} else {
|
||||
// DHT_LOG.DEBUG("Storing %s -> %s", id.toString().c_str(), value->toString().c_str());
|
||||
|
||||
+4
-2
@@ -6,8 +6,10 @@
|
||||
#include <atomic>
|
||||
#include <thread>
|
||||
#include <iostream>
|
||||
#include <ciso646> // fix windows compiler bug
|
||||
#include <cmath> // std::pow
|
||||
#include <cmath> // std::pow
|
||||
#if __cplusplus < 202002L
|
||||
#include <ciso646>
|
||||
#endif
|
||||
|
||||
namespace dht {
|
||||
|
||||
|
||||
+53
-14
@@ -238,28 +238,67 @@ operator==(const SockAddr& a, const SockAddr& b)
|
||||
return a.equals(b);
|
||||
}
|
||||
|
||||
time_point
|
||||
from_system_time(system_clock::time_point t, time_point steady_now, system_clock::time_point system_now)
|
||||
{
|
||||
if (t == system_clock::time_point::min())
|
||||
return time_point::min();
|
||||
if (t == system_clock::time_point::max())
|
||||
return time_point::max();
|
||||
// Saturate before the cast: the steady clock duration may hold a narrower range than the system one.
|
||||
constexpr auto max_dt = std::chrono::duration_cast<system_clock::duration>(duration::max());
|
||||
constexpr auto min_dt = std::chrono::duration_cast<system_clock::duration>(duration::min());
|
||||
auto system_dt = t - system_now;
|
||||
if (system_dt >= max_dt)
|
||||
return time_point::max();
|
||||
if (system_dt <= min_dt)
|
||||
return time_point::min();
|
||||
auto dt = std::chrono::duration_cast<duration>(system_dt);
|
||||
if (dt > duration(0) and steady_now > time_point::max() - dt)
|
||||
return time_point::max();
|
||||
else if (dt < duration(0) and steady_now < time_point::min() - dt)
|
||||
return time_point::min();
|
||||
return steady_now + dt;
|
||||
}
|
||||
|
||||
time_point
|
||||
from_system_time(system_clock::time_point t)
|
||||
{
|
||||
return from_system_time(t, clock::now(), system_clock::now());
|
||||
}
|
||||
|
||||
system_clock::time_point
|
||||
to_system_time(time_point t, time_point steady_now, system_clock::time_point system_now)
|
||||
{
|
||||
if (t == time_point::min())
|
||||
return system_clock::time_point::min();
|
||||
if (t == time_point::max())
|
||||
return system_clock::time_point::max();
|
||||
// Cast first: mixing clock durations of different periods would overflow the bound computations below.
|
||||
auto dt = std::chrono::duration_cast<system_clock::duration>(t - steady_now);
|
||||
if (dt > system_clock::duration(0) and system_now >= system_clock::time_point::max() - dt)
|
||||
return system_clock::time_point::max();
|
||||
else if (dt < system_clock::duration(0) and system_now <= system_clock::time_point::min() - dt)
|
||||
return system_clock::time_point::min();
|
||||
return system_now + dt;
|
||||
}
|
||||
|
||||
system_clock::time_point
|
||||
to_system_time(time_point t)
|
||||
{
|
||||
return to_system_time(t, clock::now(), system_clock::now());
|
||||
}
|
||||
|
||||
time_point
|
||||
from_time_t(std::time_t t)
|
||||
{
|
||||
auto dt = system_clock::from_time_t(t) - system_clock::now();
|
||||
auto now = clock::now();
|
||||
if (dt > system_clock::duration(0) and now > time_point::max() - dt)
|
||||
return time_point::max();
|
||||
else if (dt < system_clock::duration(0) and now < time_point::min() - dt)
|
||||
return time_point::min();
|
||||
return now + dt;
|
||||
return from_system_time(system_clock::from_time_t(t));
|
||||
}
|
||||
|
||||
std::time_t
|
||||
to_time_t(time_point t)
|
||||
{
|
||||
auto dt = t - clock::now();
|
||||
auto now = system_clock::now();
|
||||
if (dt > duration(0) and now >= system_clock::time_point::max() - dt)
|
||||
return system_clock::to_time_t(system_clock::time_point::max());
|
||||
else if (dt < duration(0) and now <= system_clock::time_point::min() - dt)
|
||||
return system_clock::to_time_t(system_clock::time_point::min());
|
||||
return system_clock::to_time_t(now + std::chrono::duration_cast<system_clock::duration>(dt));
|
||||
return system_clock::to_time_t(to_system_time(t));
|
||||
}
|
||||
|
||||
Blob
|
||||
|
||||
+14
-1
@@ -202,6 +202,9 @@ Value::Value(const Json::Value& json)
|
||||
const auto& jprio = json["prio"];
|
||||
if (jprio.isIntegral())
|
||||
priority = jprio.asUInt();
|
||||
const auto& jpt = json[VALUE_KEY_PUSHTYPE];
|
||||
if (jpt.isString())
|
||||
pushType = jpt.asString();
|
||||
}
|
||||
|
||||
Json::Value
|
||||
@@ -228,6 +231,8 @@ Value::toJson() const
|
||||
}
|
||||
if (priority)
|
||||
val["prio"] = priority;
|
||||
if (not pushType.empty())
|
||||
val[VALUE_KEY_PUSHTYPE] = pushType;
|
||||
return val;
|
||||
}
|
||||
|
||||
@@ -392,7 +397,7 @@ FieldValueIndex::containedIn(const FieldValueIndex& other) const
|
||||
return false;
|
||||
for (const auto& field : index) {
|
||||
auto other_field = other.index.find(field.first);
|
||||
if (other_field == other.index.end())
|
||||
if (other_field == other.index.end() or not (field.second == other_field->second))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -586,6 +591,14 @@ Query::isSatisfiedBy(const Query& q) const
|
||||
return none or (where.isSatisfiedBy(q.where) and select.isSatisfiedBy(q.select));
|
||||
}
|
||||
|
||||
bool
|
||||
Query::isProjectionSatisfiedBy(const Query& q) const
|
||||
{
|
||||
/* A projected answer can not be filtered afterwards, so requiring the
|
||||
filters of both queries to be a subset of each other, that is equal. */
|
||||
return none or (isSatisfiedBy(q) and q.where.isSatisfiedBy(where));
|
||||
}
|
||||
|
||||
std::ostream&
|
||||
operator<<(std::ostream& s, const dht::Select& select)
|
||||
{
|
||||
|
||||
+27
-6
@@ -71,11 +71,21 @@ public:
|
||||
{
|
||||
std::vector<Sp<Value>> expired_values;
|
||||
for (auto it = values.begin(); it != values.end();) {
|
||||
if (it->second.expiration <= now) {
|
||||
auto effectiveExpiration = it->second.expiration;
|
||||
if (status == ListenSyncStatus::SYNCED)
|
||||
// While the listen is synced, the remote node pushes
|
||||
// explicit expirations and periodic refreshes. Add a
|
||||
// grace period proportional to the value's type
|
||||
// expiration so that a single lost UDP refresh
|
||||
// notification does not cause a premature local drop.
|
||||
// A grace of one type-expiration covers exactly one
|
||||
// missed re-announcement cycle.
|
||||
effectiveExpiration += it->second.typeExpiration;
|
||||
if (effectiveExpiration <= now) {
|
||||
expired_values.emplace_back(std::move(it->second.data));
|
||||
it = values.erase(it);
|
||||
} else {
|
||||
next = std::min(next, it->second.expiration);
|
||||
next = std::min(next, effectiveExpiration);
|
||||
++it;
|
||||
}
|
||||
}
|
||||
@@ -148,12 +158,14 @@ private:
|
||||
Sp<Value> data {};
|
||||
time_point created {};
|
||||
time_point expiration {};
|
||||
duration typeExpiration {}; /* value type's expiration period, used as grace when synced */
|
||||
|
||||
CacheValueStorage() {}
|
||||
CacheValueStorage(const Sp<Value>& v, time_point t, time_point e)
|
||||
CacheValueStorage(const Sp<Value>& v, time_point t, time_point e, duration te)
|
||||
: data(v)
|
||||
, created(t)
|
||||
, expiration(e)
|
||||
, typeExpiration(te)
|
||||
{}
|
||||
};
|
||||
|
||||
@@ -166,15 +178,22 @@ private:
|
||||
{
|
||||
std::vector<Sp<Value>> nvals;
|
||||
for (const auto& value : new_values) {
|
||||
auto te = types.getType(value->type).expiration;
|
||||
auto v = values.find(value->id);
|
||||
if (v == values.end()) {
|
||||
// new value
|
||||
nvals.emplace_back(value);
|
||||
values.emplace(value->id, CacheValueStorage(value, now, now + types.getType(value->type).expiration));
|
||||
values.emplace(value->id, CacheValueStorage(value, now, now + te, te));
|
||||
} else {
|
||||
// refreshed value
|
||||
v->second.created = now;
|
||||
v->second.expiration = now + types.getType(v->second.data->type).expiration;
|
||||
v->second.expiration = now + te;
|
||||
v->second.typeExpiration = te;
|
||||
if (*value != *v->second.data and value->seq > v->second.data->seq) {
|
||||
// edited value (same rule as OpValueCache): emit as add
|
||||
v->second.data = value;
|
||||
nvals.emplace_back(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
CallbackQueue ret;
|
||||
@@ -201,8 +220,10 @@ private:
|
||||
auto v = values.find(vid);
|
||||
if (v == values.end())
|
||||
return;
|
||||
auto te = types.getType(v->second.data->type).expiration;
|
||||
v->second.created = now;
|
||||
v->second.expiration = now + types.getType(v->second.data->type).expiration;
|
||||
v->second.expiration = now + te;
|
||||
v->second.typeExpiration = te;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
#ifdef NDEBUG
|
||||
#undef NDEBUG
|
||||
#endif
|
||||
|
||||
#include "opendht_c.h"
|
||||
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <stdlib.h>
|
||||
#endif
|
||||
|
||||
static void
|
||||
done_cb(bool ok, void* user_data)
|
||||
{
|
||||
int* completed = user_data;
|
||||
*completed += ok ? 1 : 10;
|
||||
}
|
||||
|
||||
static bool
|
||||
get_cb(const dht_value* value, void* user_data)
|
||||
{
|
||||
(void) value;
|
||||
(void) user_data;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool
|
||||
listen_cb(const dht_value* value, bool expired, void* user_data)
|
||||
{
|
||||
(void) expired;
|
||||
(void) user_data;
|
||||
assert(dht_value_get_id(value) == 42);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void
|
||||
released_cb(void* user_data)
|
||||
{
|
||||
++*(int*) user_data;
|
||||
}
|
||||
|
||||
int
|
||||
main(void)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
_set_error_mode(_OUT_TO_STDERR);
|
||||
#endif
|
||||
setvbuf(stdout, NULL, _IONBF, 0);
|
||||
puts("C bindings: invalid crypto inputs");
|
||||
const uint8_t invalid[] = {1, 2, 3};
|
||||
assert(dht_publickey_import(invalid, sizeof(invalid)) == NULL);
|
||||
assert(dht_privatekey_import(invalid, sizeof(invalid), "") == NULL);
|
||||
assert(dht_certificate_import(invalid, sizeof(invalid)) == NULL);
|
||||
assert(dht_privatekey_generate(1) == NULL);
|
||||
|
||||
puts("C bindings: infohash roundtrip");
|
||||
dht_infohash hash;
|
||||
dht_infohash_get_from_string(&hash, "binding regression");
|
||||
assert(!dht_infohash_is_zero(&hash));
|
||||
dht_infohash restored;
|
||||
dht_infohash_from_hex_null(&restored, dht_infohash_print(&hash));
|
||||
assert(memcmp(&hash, &restored, sizeof(hash)) == 0);
|
||||
|
||||
puts("C bindings: value ownership and metadata");
|
||||
dht_value* value = dht_value_with_id_new(invalid, sizeof(invalid), 42);
|
||||
assert(value != NULL);
|
||||
assert(dht_value_get_id(value) == 42);
|
||||
dht_value_set_user_type(value, "application/octet-stream");
|
||||
assert(strcmp(dht_value_get_user_type(value), "application/octet-stream") == 0);
|
||||
dht_value* copy = dht_value_ref(value);
|
||||
dht_value_unref(value);
|
||||
dht_data_view data = dht_value_get_data(copy);
|
||||
assert(data.size == sizeof(invalid));
|
||||
assert(memcmp(data.data, invalid, data.size) == 0);
|
||||
dht_value_set_type(copy, 17);
|
||||
dht_value_set_seq(copy, 23);
|
||||
dht_value_set_priority(copy, 1);
|
||||
dht_value_set_push_type(copy, "audioCall");
|
||||
assert(dht_value_get_type(copy) == 17);
|
||||
assert(dht_value_get_seq(copy) == 23);
|
||||
assert(dht_value_get_priority(copy) == 1);
|
||||
assert(strcmp(dht_value_get_push_type(copy), "audioCall") == 0);
|
||||
dht_value_unref(copy);
|
||||
|
||||
puts("C bindings: RSA signing and key persistence");
|
||||
dht_privatekey* private_key = dht_privatekey_generate(2048);
|
||||
assert(private_key != NULL);
|
||||
dht_publickey* public_key = dht_privatekey_get_publickey(private_key);
|
||||
dht_blob* signature = dht_privatekey_sign(private_key, invalid, sizeof(invalid));
|
||||
assert(signature != NULL);
|
||||
dht_data_view signature_data = dht_blob_get_data(signature);
|
||||
assert(dht_publickey_check_signature(public_key,
|
||||
(const char*) invalid,
|
||||
sizeof(invalid),
|
||||
(const char*) signature_data.data,
|
||||
signature_data.size));
|
||||
dht_blob_delete(signature);
|
||||
dht_blob* packed = dht_privatekey_export_blob(private_key, "binding-password");
|
||||
assert(packed != NULL);
|
||||
dht_data_view packed_data = dht_blob_get_data(packed);
|
||||
dht_privatekey* imported = dht_privatekey_import(packed_data.data, packed_data.size, "binding-password");
|
||||
assert(imported != NULL);
|
||||
dht_privatekey_delete(imported);
|
||||
dht_blob_delete(packed);
|
||||
dht_privatekey_delete(private_key);
|
||||
dht_publickey_delete(public_key);
|
||||
|
||||
puts("C bindings: identity and certificate roundtrip");
|
||||
dht_identity identity = dht_identity_generate("binding regression", NULL);
|
||||
assert(identity.privatekey && identity.certificate);
|
||||
packed = dht_certificate_export(identity.certificate);
|
||||
assert(packed != NULL);
|
||||
packed_data = dht_blob_get_data(packed);
|
||||
dht_certificate* certificate = dht_certificate_import(packed_data.data, packed_data.size);
|
||||
assert(certificate != NULL);
|
||||
dht_certificate_delete(certificate);
|
||||
dht_blob_delete(packed);
|
||||
dht_identity_delete(&identity);
|
||||
|
||||
puts("C bindings: runner lifecycle and callbacks");
|
||||
dht_runner_config config;
|
||||
dht_runner_config_default(&config);
|
||||
config.threaded = false;
|
||||
dht_runner* runner = dht_runner_new();
|
||||
assert(dht_runner_run_config(runner, 0, &config) == 0);
|
||||
assert(dht_runner_get_bound_port(runner, AF_INET) != 0);
|
||||
int released = 0;
|
||||
dht_op_token* token = dht_runner_listen(runner, &hash, listen_cb, released_cb, &released);
|
||||
int completed = 0;
|
||||
value = dht_value_with_id_new(invalid, sizeof(invalid), 42);
|
||||
dht_runner_put(runner, &hash, value, done_cb, &completed, false);
|
||||
dht_value_unref(value);
|
||||
assert(completed == 0);
|
||||
assert(dht_runner_loop(runner) == 0);
|
||||
dht_runner_get(runner, &hash, get_cb, done_cb, &completed);
|
||||
assert(dht_runner_join(runner) == 0);
|
||||
assert(!dht_runner_is_running(runner));
|
||||
assert(released == 1);
|
||||
assert(completed >= 10);
|
||||
dht_op_token_delete(token);
|
||||
assert(dht_runner_run(runner, 0) == 0);
|
||||
assert(dht_runner_join(runner) == 0);
|
||||
dht_runner_delete(runner);
|
||||
puts("C binding regressions passed");
|
||||
return 0;
|
||||
}
|
||||
+188
-2
@@ -59,6 +59,24 @@ CryptoTester::testSignatureEncryption()
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
CryptoTester::testPublicKeyPemIsPlainText()
|
||||
{
|
||||
auto key = dht::crypto::PrivateKey::generate();
|
||||
auto pem = key.getPublicKey().toString();
|
||||
|
||||
// The exported key is PEM text: it must not carry the terminating null
|
||||
// byte GnuTLS counts in the exported size, which would otherwise be
|
||||
// embedded in anything the string is serialized into.
|
||||
CPPUNIT_ASSERT(not pem.empty());
|
||||
CPPUNIT_ASSERT_EQUAL(std::string::npos, pem.find('\0'));
|
||||
CPPUNIT_ASSERT_EQUAL('\n', pem.back());
|
||||
|
||||
// The exported text still reads back as the same key.
|
||||
dht::crypto::PublicKey reloaded(dht::Blob(pem.begin(), pem.end()));
|
||||
CPPUNIT_ASSERT_EQUAL(key.getPublicKey().getId(), reloaded.getId());
|
||||
}
|
||||
|
||||
void
|
||||
CryptoTester::testCertificateRevocation()
|
||||
{
|
||||
@@ -95,10 +113,159 @@ CryptoTester::testCertificateRevocation()
|
||||
CPPUNIT_ASSERT_MESSAGE(v.toString(), v);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
/** Read a CRL Number extension blob (big-endian, variable length) as an integer. */
|
||||
uint64_t
|
||||
crlNumberValue(const dht::Blob& number)
|
||||
{
|
||||
uint64_t value {0};
|
||||
for (uint8_t byte : number)
|
||||
value = (value << 8) | byte;
|
||||
return value;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void
|
||||
CryptoTester::testRevocationListNumber()
|
||||
{
|
||||
auto ca = dht::crypto::generateIdentity("ca");
|
||||
auto device = dht::crypto::generateIdentity("dev", ca);
|
||||
|
||||
dht::crypto::RevocationList crl;
|
||||
CPPUNIT_ASSERT_EQUAL(uint64_t(0), crlNumberValue(crl.getNumber()));
|
||||
|
||||
crl.revoke(*device.second);
|
||||
crl.sign(ca);
|
||||
auto first = crl.getNumber();
|
||||
// An unnumbered list gets a random number on its first signature.
|
||||
CPPUNIT_ASSERT(not first.empty());
|
||||
CPPUNIT_ASSERT(crlNumberValue(first) != 0);
|
||||
|
||||
// Re-signing keeps the same lineage and moves the number forward.
|
||||
crl.sign(ca);
|
||||
auto second = crl.getNumber();
|
||||
CPPUNIT_ASSERT_EQUAL(crlNumberValue(first) + 1, crlNumberValue(second));
|
||||
|
||||
crl.sign(ca);
|
||||
CPPUNIT_ASSERT_EQUAL(crlNumberValue(first) + 2, crlNumberValue(crl.getNumber()));
|
||||
|
||||
// The number is a DER integer, so it is stored with leading zeros stripped.
|
||||
// Incrementing must still work for numbers shorter than 8 bytes.
|
||||
for (uint64_t start : {uint64_t(1), uint64_t(0x7f), uint64_t(0x80), uint64_t(0xffff)}) {
|
||||
dht::crypto::RevocationList small;
|
||||
small.revoke(*device.second);
|
||||
small.sign(ca, {}, start);
|
||||
CPPUNIT_ASSERT_EQUAL(start, crlNumberValue(small.getNumber()));
|
||||
small.sign(ca);
|
||||
CPPUNIT_ASSERT_EQUAL(start + 1, crlNumberValue(small.getNumber()));
|
||||
small.sign(ca);
|
||||
CPPUNIT_ASSERT_EQUAL(start + 2, crlNumberValue(small.getNumber()));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
CryptoTester::testRevocationListExplicitNumber()
|
||||
{
|
||||
auto ca = dht::crypto::generateIdentity("ca");
|
||||
auto device = dht::crypto::generateIdentity("dev", ca);
|
||||
|
||||
// Values chosen to cover leading zero bytes, a full-width number and the largest
|
||||
// value that still encodes as a positive 8-byte DER integer.
|
||||
const std::vector<uint64_t> numbers {1,
|
||||
42,
|
||||
0xff,
|
||||
0x100,
|
||||
0x0000'0001'0000'0000ull,
|
||||
0x0000'02a1'0000'0001ull,
|
||||
0x7fff'ffff'ffff'ffffull};
|
||||
for (auto number : numbers) {
|
||||
dht::crypto::RevocationList crl;
|
||||
crl.revoke(*device.second);
|
||||
crl.sign(ca, {}, number);
|
||||
CPPUNIT_ASSERT_EQUAL(number, crlNumberValue(crl.getNumber()));
|
||||
CPPUNIT_ASSERT(crl.isSignedBy(*ca.second));
|
||||
CPPUNIT_ASSERT(crl.isRevoked(*device.second));
|
||||
|
||||
// The number must survive a pack/unpack round-trip.
|
||||
dht::crypto::RevocationList unpacked(crl.getPacked());
|
||||
CPPUNIT_ASSERT_EQUAL(number, crlNumberValue(unpacked.getNumber()));
|
||||
}
|
||||
|
||||
// An explicit number overrides the increment, in both directions.
|
||||
dht::crypto::RevocationList crl;
|
||||
crl.revoke(*device.second);
|
||||
crl.sign(ca, {}, 1000);
|
||||
CPPUNIT_ASSERT_EQUAL(uint64_t(1000), crlNumberValue(crl.getNumber()));
|
||||
crl.sign(ca, {}, 5);
|
||||
CPPUNIT_ASSERT_EQUAL(uint64_t(5), crlNumberValue(crl.getNumber()));
|
||||
|
||||
// Passing 0 falls back to the default behavior: increment the current number.
|
||||
crl.sign(ca, {}, 0);
|
||||
CPPUNIT_ASSERT_EQUAL(uint64_t(6), crlNumberValue(crl.getNumber()));
|
||||
}
|
||||
|
||||
void
|
||||
CryptoTester::testRevocationListPartitionedNumber()
|
||||
{
|
||||
// Two devices holding the same authority issue revocation lists independently.
|
||||
// RFC 5280 5.2.3 requires CRL numbers to increase monotonically for a given CRL
|
||||
// scope and issuer; partitioning the low bits by device keeps each sequence
|
||||
// monotonic while making collisions between devices impossible.
|
||||
constexpr unsigned TAG_BITS = 24;
|
||||
constexpr uint64_t TAG_A = 0x0a0b0c;
|
||||
constexpr uint64_t TAG_B = 0x0a0b0d;
|
||||
|
||||
auto ca = dht::crypto::generateIdentity("ca");
|
||||
auto account = dht::crypto::generateIdentity("acc", ca, 4096, true);
|
||||
auto deviceA = dht::crypto::generateIdentity("devA", account);
|
||||
auto deviceB = dht::crypto::generateIdentity("devB", account);
|
||||
|
||||
uint64_t previous {0};
|
||||
dht::crypto::RevocationList crlA;
|
||||
for (unsigned i = 1; i <= 4; i++) {
|
||||
crlA.revoke(*deviceA.second);
|
||||
crlA.sign(account, {}, (uint64_t(i) << TAG_BITS) | TAG_A);
|
||||
auto number = crlNumberValue(crlA.getNumber());
|
||||
CPPUNIT_ASSERT_EQUAL(TAG_A, number & ((uint64_t(1) << TAG_BITS) - 1));
|
||||
CPPUNIT_ASSERT(number > previous);
|
||||
CPPUNIT_ASSERT_EQUAL(uint64_t(0), number >> 63);
|
||||
previous = number;
|
||||
}
|
||||
|
||||
// Both devices branch from the same counter but never mint the same number.
|
||||
dht::crypto::RevocationList branchA;
|
||||
branchA.revoke(*deviceA.second);
|
||||
branchA.sign(account, {}, (uint64_t(9) << TAG_BITS) | TAG_A);
|
||||
dht::crypto::RevocationList branchB;
|
||||
branchB.revoke(*deviceB.second);
|
||||
branchB.sign(account, {}, (uint64_t(9) << TAG_BITS) | TAG_B);
|
||||
CPPUNIT_ASSERT(crlNumberValue(branchA.getNumber()) != crlNumberValue(branchB.getNumber()));
|
||||
|
||||
// Both are valid and revoke only their own target.
|
||||
CPPUNIT_ASSERT(branchA.isSignedBy(*account.second));
|
||||
CPPUNIT_ASSERT(branchB.isSignedBy(*account.second));
|
||||
CPPUNIT_ASSERT(branchA.isRevoked(*deviceA.second));
|
||||
CPPUNIT_ASSERT(not branchA.isRevoked(*deviceB.second));
|
||||
CPPUNIT_ASSERT(branchB.isRevoked(*deviceB.second));
|
||||
CPPUNIT_ASSERT(not branchB.isRevoked(*deviceA.second));
|
||||
|
||||
// A number minted by an older client is fully random, so bit 63 may be set. Masking
|
||||
// it out and taking the counter part still yields a strictly greater number.
|
||||
constexpr uint64_t legacy = 0xdead'beef'0123'4567ull;
|
||||
dht::crypto::RevocationList legacyCrl;
|
||||
legacyCrl.revoke(*deviceA.second);
|
||||
legacyCrl.sign(account, {}, legacy);
|
||||
auto counter = ((crlNumberValue(legacyCrl.getNumber()) & ~(uint64_t(1) << 63)) >> TAG_BITS) + 1;
|
||||
legacyCrl.sign(account, {}, (counter << TAG_BITS) | TAG_A);
|
||||
CPPUNIT_ASSERT(crlNumberValue(legacyCrl.getNumber()) > (legacy & ~(uint64_t(1) << 63)));
|
||||
CPPUNIT_ASSERT_EQUAL(uint64_t(0), crlNumberValue(legacyCrl.getNumber()) >> 63);
|
||||
}
|
||||
|
||||
void
|
||||
CryptoTester::testCertificateRequest()
|
||||
{
|
||||
// Generate CA
|
||||
{ // Generate CA
|
||||
auto ca = dht::crypto::generateIdentity("Test CA");
|
||||
|
||||
// Generate signed request
|
||||
@@ -149,6 +316,25 @@ CryptoTester::testCertificateSerialNumber()
|
||||
};
|
||||
auto serial = dht::crypto::Certificate(cert_pem).getSerialNumber();
|
||||
CPPUNIT_ASSERT(std::equal(SERIAL.begin(), SERIAL.end(), serial.begin(), serial.end()));
|
||||
|
||||
auto key = dht::crypto::PrivateKey::generateEC();
|
||||
for (unsigned i = 0; i < 32; i++) {
|
||||
auto generated = dht::crypto::Certificate::generate(key, "generated", {});
|
||||
auto generatedSerial = generated.getSerialNumber();
|
||||
CPPUNIT_ASSERT(not generatedSerial.empty());
|
||||
CPPUNIT_ASSERT_EQUAL(uint8_t {0}, uint8_t(generatedSerial.front() & 0x80));
|
||||
CPPUNIT_ASSERT(std::any_of(generatedSerial.begin(), generatedSerial.end(), [](uint8_t byte) { return byte != 0; }));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
CryptoTester::testEcKeyCurve()
|
||||
{
|
||||
auto key = dht::crypto::PrivateKey::generateEC(GNUTLS_ECC_CURVE_SECP256R1);
|
||||
unsigned int bits = 0;
|
||||
CPPUNIT_ASSERT_EQUAL(static_cast<int>(GNUTLS_PK_EC),
|
||||
gnutls_x509_privkey_get_pk_algorithm2(key.x509_key, &bits));
|
||||
CPPUNIT_ASSERT_EQUAL(256u, bits);
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -12,9 +12,14 @@ class CryptoTester : public CppUnit::TestFixture
|
||||
{
|
||||
CPPUNIT_TEST_SUITE(CryptoTester);
|
||||
CPPUNIT_TEST(testSignatureEncryption);
|
||||
CPPUNIT_TEST(testPublicKeyPemIsPlainText);
|
||||
CPPUNIT_TEST(testCertificateRevocation);
|
||||
CPPUNIT_TEST(testRevocationListNumber);
|
||||
CPPUNIT_TEST(testRevocationListExplicitNumber);
|
||||
CPPUNIT_TEST(testRevocationListPartitionedNumber);
|
||||
CPPUNIT_TEST(testCertificateRequest);
|
||||
CPPUNIT_TEST(testCertificateSerialNumber);
|
||||
CPPUNIT_TEST(testEcKeyCurve);
|
||||
CPPUNIT_TEST(testOcsp);
|
||||
CPPUNIT_TEST(testAesEncryption);
|
||||
CPPUNIT_TEST(testAesEncryptionWithMultipleKeySizes);
|
||||
@@ -36,10 +41,26 @@ public:
|
||||
* Test data signature, encryption and decryption
|
||||
*/
|
||||
void testSignatureEncryption();
|
||||
/**
|
||||
* Test that an exported public key is text only, with no embedded null byte
|
||||
*/
|
||||
void testPublicKeyPemIsPlainText();
|
||||
/**
|
||||
* Test certificate generation, validation and revocation
|
||||
*/
|
||||
void testCertificateRevocation();
|
||||
/**
|
||||
* Test the default CRL Number behavior: random on first signature, incremented after
|
||||
*/
|
||||
void testRevocationListNumber();
|
||||
/**
|
||||
* Test signing a revocation list with a caller-provided CRL Number
|
||||
*/
|
||||
void testRevocationListExplicitNumber();
|
||||
/**
|
||||
* Test that issuers sharing an authority can partition the CRL Number space
|
||||
*/
|
||||
void testRevocationListPartitionedNumber();
|
||||
/**
|
||||
* Test certificate requests
|
||||
*/
|
||||
@@ -48,6 +69,10 @@ public:
|
||||
* Test certificate serial number extraction
|
||||
*/
|
||||
void testCertificateSerialNumber();
|
||||
/**
|
||||
* Test EC private key generation with an explicit curve
|
||||
*/
|
||||
void testEcKeyCurve();
|
||||
/**
|
||||
* Test OCSP
|
||||
*/
|
||||
|
||||
+1666
-1
File diff suppressed because it is too large.
Load diff
+47
-1
@@ -8,7 +8,6 @@
|
||||
|
||||
#include <opendht/dhtrunner.h>
|
||||
#include <opendht/dht_proxy_server.h>
|
||||
#include <opendht/log.h>
|
||||
|
||||
namespace test {
|
||||
|
||||
@@ -17,10 +16,23 @@ class DhtProxyTester : public CppUnit::TestFixture
|
||||
CPPUNIT_TEST_SUITE(DhtProxyTester);
|
||||
CPPUNIT_TEST(testGetPut);
|
||||
CPPUNIT_TEST(testListen);
|
||||
CPPUNIT_TEST(testListenValueEdit);
|
||||
CPPUNIT_TEST(testPushListenValueEdit);
|
||||
CPPUNIT_TEST(testResubscribeGetValues);
|
||||
CPPUNIT_TEST(testPushNotification);
|
||||
CPPUNIT_TEST(testProxyServerClientFullChain);
|
||||
CPPUNIT_TEST(testPutGet40KChars);
|
||||
CPPUNIT_TEST(testFuzzy);
|
||||
CPPUNIT_TEST(testShutdownStop);
|
||||
CPPUNIT_TEST(testGetAfterListen);
|
||||
CPPUNIT_TEST(testListenDuplicatePut);
|
||||
CPPUNIT_TEST(testPushMultiValueEditExpire);
|
||||
CPPUNIT_TEST(testPushRefreshNoSpuriousExpire);
|
||||
CPPUNIT_TEST(testProxyListenEditChain);
|
||||
CPPUNIT_TEST(testPushRapidEditsNoPhantom);
|
||||
CPPUNIT_TEST(testPushRefreshAfterEditKeepsLatest);
|
||||
CPPUNIT_TEST(testPushPlatformFollowsResubscribe);
|
||||
CPPUNIT_TEST(testPushPlatformSetAfterStart);
|
||||
CPPUNIT_TEST_SUITE_END();
|
||||
|
||||
public:
|
||||
@@ -41,11 +53,28 @@ public:
|
||||
* Test listen
|
||||
*/
|
||||
void testListen();
|
||||
/**
|
||||
* Test that editing a value only triggers an add callback via proxy, no expire
|
||||
*/
|
||||
void testListenValueEdit();
|
||||
/**
|
||||
* Test that editing a value only triggers an add callback via push,
|
||||
* no phantom expire, and that real expiration is received.
|
||||
*/
|
||||
void testPushListenValueEdit();
|
||||
/**
|
||||
* When a proxy redo a subscribe on the proxy
|
||||
* it should retrieve existant values
|
||||
*/
|
||||
void testResubscribeGetValues();
|
||||
/**
|
||||
* Test push notification mechanism and OpValueCache
|
||||
*/
|
||||
void testPushNotification();
|
||||
/**
|
||||
* Test end-to-end chain through proxy server and proxy client
|
||||
*/
|
||||
void testProxyServerClientFullChain();
|
||||
/**
|
||||
* Test MTU put/get on dht
|
||||
*/
|
||||
@@ -54,6 +83,23 @@ public:
|
||||
void testFuzzy();
|
||||
|
||||
void testShutdownStop();
|
||||
void testGetAfterListen();
|
||||
void testListenDuplicatePut();
|
||||
void testPushMultiValueEditExpire();
|
||||
void testPushRefreshNoSpuriousExpire();
|
||||
void testProxyListenEditChain();
|
||||
void testPushRapidEditsNoPhantom();
|
||||
void testPushRefreshAfterEditKeepsLatest();
|
||||
/**
|
||||
* A device refreshing its subscription with another push platform
|
||||
* must then be notified on that platform.
|
||||
*/
|
||||
void testPushPlatformFollowsResubscribe();
|
||||
/**
|
||||
* A push platform set after the node started must be used
|
||||
* by its next subscriptions.
|
||||
*/
|
||||
void testPushPlatformSetAfterStart();
|
||||
|
||||
private:
|
||||
dht::DhtRunner::Config clientConfig {};
|
||||
|
||||
@@ -37,7 +37,7 @@ DhtProxyClientTester::testResubscribeUsesKeyRoute()
|
||||
#ifdef OPENDHT_PUSH_NOTIFICATIONS
|
||||
DhtProxyClient client({}, {}, [] {}, "http://127.0.0.1:8080", "OpenDHT-Test", "client-id", "push-token");
|
||||
|
||||
DhtProxyClient::Listener listener {OpValueCache([](const std::vector<Sp<Value>>&, bool) { return true; })};
|
||||
DhtProxyClient::Listener listener {ValueCallback([](const std::vector<Sp<Value>>&, bool) { return true; })};
|
||||
listener.opstate = std::make_shared<DhtProxyClient::OperationState>();
|
||||
listener.cb = [](const std::vector<Sp<Value>>&, bool, system_clock::time_point) {
|
||||
return true;
|
||||
@@ -67,7 +67,7 @@ DhtProxyClientTester::testSetPushNotificationTokenResubscribesWithNewToken()
|
||||
auto& search = client.searches_[key];
|
||||
auto [it, inserted] = search.listeners.emplace(std::piecewise_construct,
|
||||
std::forward_as_tuple(1),
|
||||
std::forward_as_tuple(OpValueCache(
|
||||
std::forward_as_tuple(ValueCallback(
|
||||
[](const std::vector<Sp<Value>>&, bool) { return true; })));
|
||||
CPPUNIT_ASSERT(inserted);
|
||||
|
||||
|
||||
+743
-50
@@ -9,11 +9,65 @@
|
||||
#include <future>
|
||||
#include <mutex>
|
||||
#include <condition_variable>
|
||||
#include <fstream>
|
||||
#include <cstdio>
|
||||
using namespace std::chrono_literals;
|
||||
using namespace std::literals;
|
||||
|
||||
namespace test {
|
||||
CPPUNIT_TEST_SUITE_REGISTRATION(DhtRunnerTester);
|
||||
CPPUNIT_TEST_SUITE_REGISTRATION(DhtRunnerPushTester);
|
||||
|
||||
namespace {
|
||||
#if defined(OPENDHT_PROXY_CLIENT) && defined(OPENDHT_PUSH_NOTIFICATIONS)
|
||||
constexpr auto stoppedPushResult = dht::PushNotificationResult::IgnoredStopped;
|
||||
#else
|
||||
constexpr auto stoppedPushResult = dht::PushNotificationResult::IgnoredDisabled;
|
||||
#endif
|
||||
}
|
||||
|
||||
void
|
||||
DhtRunnerPushTester::testPushBeforeRun()
|
||||
{
|
||||
dht::DhtRunner node;
|
||||
auto result = node.pushNotificationReceived({});
|
||||
CPPUNIT_ASSERT(result.wait_for(0s) == std::future_status::ready);
|
||||
CPPUNIT_ASSERT(result.get() == stoppedPushResult);
|
||||
}
|
||||
|
||||
void
|
||||
DhtRunnerPushTester::testPushAfterJoin()
|
||||
{
|
||||
dht::DhtRunner node;
|
||||
node.run(0);
|
||||
node.join();
|
||||
auto result = node.pushNotificationReceived({});
|
||||
CPPUNIT_ASSERT(result.wait_for(0s) == std::future_status::ready);
|
||||
CPPUNIT_ASSERT(result.get() == stoppedPushResult);
|
||||
}
|
||||
|
||||
void
|
||||
DhtRunnerPushTester::testPushBurstAfterJoin()
|
||||
{
|
||||
dht::DhtRunner node;
|
||||
node.run(0);
|
||||
node.join();
|
||||
for (unsigned i = 0; i < 32; ++i) {
|
||||
auto result = node.pushNotificationReceived({});
|
||||
CPPUNIT_ASSERT(result.wait_for(0s) == std::future_status::ready);
|
||||
CPPUNIT_ASSERT(result.get() == stoppedPushResult);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
DhtRunnerPushTester::testPushWhileRunning()
|
||||
{
|
||||
dht::DhtRunner node;
|
||||
node.run(0);
|
||||
auto result = node.pushNotificationReceived({});
|
||||
CPPUNIT_ASSERT(result.wait_for(5s) == std::future_status::ready);
|
||||
CPPUNIT_ASSERT(result.get() != dht::PushNotificationResult::IgnoredStopped);
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
T
|
||||
@@ -89,6 +143,42 @@ DhtRunnerTester::testGetPut()
|
||||
CPPUNIT_ASSERT(vals.front()->data == val_data);
|
||||
}
|
||||
|
||||
void
|
||||
DhtRunnerTester::testStoreEmptyValue()
|
||||
{
|
||||
dht::DhtRunner::Config config;
|
||||
config.dht_config.node_config.max_peer_req_per_sec = -1;
|
||||
config.dht_config.node_config.max_req_per_sec = -1;
|
||||
config.dht_config.node_config.max_store_size = 100; // Small size to force expireStore()
|
||||
config.dht_config.node_config.max_store_keys = -1;
|
||||
|
||||
dht::DhtRunner testNode;
|
||||
testNode.run(0, config);
|
||||
testNode.bootstrap(node1.getBound());
|
||||
|
||||
auto key = testNode.getId();
|
||||
auto val = std::make_shared<dht::Value>();
|
||||
val->data.clear(); // Ensure size is 0
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 0, val->size());
|
||||
|
||||
std::promise<bool> p;
|
||||
testNode.put(key, val, [&](bool ok) { p.set_value(ok); });
|
||||
p.get_future().get(); // Wait for put to finish
|
||||
|
||||
// Put a large value to exceed max_store_size and trigger expireStore()
|
||||
auto key2 = dht::InfoHash::get("large_value_test");
|
||||
auto val2 = std::make_shared<dht::Value>();
|
||||
val2->data = std::vector<uint8_t>(200, 0); // 200 bytes
|
||||
std::promise<bool> p2;
|
||||
testNode.put(key2, val2, [&](bool ok) { p2.set_value(ok); });
|
||||
p2.get_future().get(); // Wait for put to finish
|
||||
|
||||
// Wait a bit to ensure the value is processed
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(200));
|
||||
|
||||
testNode.join();
|
||||
}
|
||||
|
||||
void
|
||||
DhtRunnerTester::testPutDuplicate()
|
||||
{
|
||||
@@ -208,6 +298,42 @@ DhtRunnerTester::testImportValuesPreservesRemoteQuota()
|
||||
restored.join();
|
||||
}
|
||||
|
||||
void
|
||||
DhtRunnerTester::testExportValuesUsesSystemTime()
|
||||
{
|
||||
auto key = dht::InfoHash::get("export-system-time");
|
||||
const auto created = dht::clock::now();
|
||||
msgpack::sbuffer importBuffer;
|
||||
msgpack::packer<msgpack::sbuffer> pk(&importBuffer);
|
||||
pk.pack_array(1);
|
||||
pk.pack_array(2);
|
||||
pk.pack(created.time_since_epoch().count());
|
||||
dht::Value {std::string("persisted")}.msgpack_pack(pk);
|
||||
node1.importValues({
|
||||
dht::ValuesExport {key, {importBuffer.data(), importBuffer.data() + importBuffer.size()}}
|
||||
});
|
||||
|
||||
auto exported = node1.exportValues();
|
||||
CPPUNIT_ASSERT_EQUAL(size_t(1), exported.size());
|
||||
|
||||
msgpack::unpacked msg;
|
||||
msgpack::unpack(msg, reinterpret_cast<const char*>(exported.front().second.data()), exported.front().second.size());
|
||||
const auto values = msg.get();
|
||||
CPPUNIT_ASSERT(values.type == msgpack::type::ARRAY);
|
||||
CPPUNIT_ASSERT_EQUAL(uint32_t(1), values.via.array.size);
|
||||
const auto& persistedValue = values.via.array.ptr[0];
|
||||
CPPUNIT_ASSERT_EQUAL(uint32_t(4), persistedValue.via.array.size);
|
||||
CPPUNIT_ASSERT(persistedValue.via.array.ptr[0].type == msgpack::type::EXT);
|
||||
CPPUNIT_ASSERT(persistedValue.via.array.ptr[3].type == msgpack::type::EXT);
|
||||
|
||||
const auto persistedCreated = persistedValue.via.array.ptr[0].as<std::chrono::system_clock::time_point>();
|
||||
const auto persistedExpiration = persistedValue.via.array.ptr[3].as<std::chrono::system_clock::time_point>();
|
||||
const auto delta = std::chrono::system_clock::now() - persistedCreated;
|
||||
CPPUNIT_ASSERT(delta >= std::chrono::system_clock::duration::zero());
|
||||
CPPUNIT_ASSERT(delta < 5s);
|
||||
CPPUNIT_ASSERT(persistedExpiration > persistedCreated);
|
||||
}
|
||||
|
||||
void
|
||||
DhtRunnerTester::testImportValuesPreservesStoredExpiration()
|
||||
{
|
||||
@@ -265,14 +391,25 @@ DhtRunnerTester::testImportValuesPreservesStoredExpiration()
|
||||
void
|
||||
DhtRunnerTester::testImportPermanentValueExpires()
|
||||
{
|
||||
// Simulate importing a value that was originally stored as permanent
|
||||
// (expiration = time_point::max()). After import the permanent flag is lost,
|
||||
// so the value must expire within the type's normal expiration window.
|
||||
static const dht::ValueType SHORT_TYPE {201, "Short perm", 2s};
|
||||
static const dht::ValueType SHORT_TYPE {201, "Short perm", 500ms};
|
||||
|
||||
dht::DhtRunner::Config config;
|
||||
config.dht_config.node_config.max_peer_req_per_sec = -1;
|
||||
config.dht_config.node_config.max_req_per_sec = -1;
|
||||
config.dht_config.node_config.max_store_size = -1;
|
||||
config.dht_config.node_config.max_store_keys = -1;
|
||||
dht::DhtRunner restored;
|
||||
restored.run(0, config);
|
||||
restored.registerInsecureType(SHORT_TYPE);
|
||||
|
||||
// Ensure the runner is waiting on a previously computed scheduler deadline.
|
||||
std::promise<void> loopCompleted;
|
||||
auto loopCompletedFuture = loopCompleted.get_future();
|
||||
restored.getNodeInfo([&loopCompleted](auto) { loopCompleted.set_value(); });
|
||||
getFutureValue(std::move(loopCompletedFuture));
|
||||
|
||||
auto key = dht::InfoHash::get("import-permanent-expiration");
|
||||
auto now = dht::clock::now();
|
||||
|
||||
msgpack::sbuffer buffer;
|
||||
msgpack::packer<msgpack::sbuffer> pk(&buffer);
|
||||
pk.pack_array(1);
|
||||
@@ -280,63 +417,33 @@ DhtRunnerTester::testImportPermanentValueExpires()
|
||||
pk.pack(now.time_since_epoch().count());
|
||||
dht::Value {SHORT_TYPE.id, std::string("was-permanent")}.msgpack_pack(pk);
|
||||
pk.pack_bin(0);
|
||||
// Permanent values are exported with time_point::max() expiration
|
||||
pk.pack(dht::clock::time_point::max().time_since_epoch().count());
|
||||
|
||||
dht::DhtRunner restored;
|
||||
dht::DhtRunner::Config config;
|
||||
config.dht_config.node_config.max_peer_req_per_sec = -1;
|
||||
config.dht_config.node_config.max_req_per_sec = -1;
|
||||
config.dht_config.node_config.max_store_size = -1;
|
||||
config.dht_config.node_config.max_store_keys = -1;
|
||||
restored.run(0, config);
|
||||
|
||||
restored.importValues({
|
||||
dht::ValuesExport {key, {buffer.data(), buffer.data() + buffer.size()}}
|
||||
});
|
||||
|
||||
// Value should be present right after import
|
||||
auto exportedBefore = restored.exportValues();
|
||||
CPPUNIT_ASSERT(!exportedBefore.empty());
|
||||
|
||||
// Wait longer than the default USER_DATA expiration (10 min) is not
|
||||
// practical, but the type is unregistered so the default 10 min expiration
|
||||
// applies. Instead, register the short type so the cap uses 2 s.
|
||||
// Re-import into a fresh node that knows the short type.
|
||||
dht::DhtRunner restored2;
|
||||
restored2.run(0, config);
|
||||
restored2.registerInsecureType(SHORT_TYPE);
|
||||
restored2.importValues({
|
||||
dht::ValuesExport {key, {buffer.data(), buffer.data() + buffer.size()}}
|
||||
});
|
||||
|
||||
CPPUNIT_ASSERT(!restored2.exportValues().empty());
|
||||
|
||||
// Wait for the short expiration to pass
|
||||
std::this_thread::sleep_for(3s);
|
||||
|
||||
auto restoredValues = restored2.exportValues();
|
||||
bool found = false;
|
||||
for (const auto& ve : restoredValues) {
|
||||
if (ve.first == key) {
|
||||
auto containsValue = [&] {
|
||||
for (const auto& [exportedKey, data] : restored.exportValues()) {
|
||||
if (exportedKey != key)
|
||||
continue;
|
||||
msgpack::unpacked msg;
|
||||
msgpack::unpack(msg, (const char*) ve.second.data(), ve.second.size());
|
||||
msgpack::unpack(msg, reinterpret_cast<const char*>(data.data()), data.size());
|
||||
auto valarr = msg.get();
|
||||
CPPUNIT_ASSERT(valarr.type == msgpack::type::ARRAY);
|
||||
// The value should have expired
|
||||
CPPUNIT_ASSERT_EQUAL(uint32_t(0), valarr.via.array.size);
|
||||
found = true;
|
||||
return valarr.via.array.size != 0;
|
||||
}
|
||||
}
|
||||
// Either the key was cleaned up entirely or it was found with 0 values
|
||||
CPPUNIT_ASSERT(found || restoredValues.empty()
|
||||
|| std::none_of(restoredValues.begin(), restoredValues.end(),
|
||||
[&](const auto& ve) { return ve.first == key; }));
|
||||
return false;
|
||||
};
|
||||
|
||||
CPPUNIT_ASSERT(containsValue());
|
||||
auto timeout = dht::clock::now() + 2s;
|
||||
while (containsValue() and dht::clock::now() < timeout)
|
||||
std::this_thread::sleep_for(10ms);
|
||||
CPPUNIT_ASSERT(!containsValue());
|
||||
|
||||
restored.shutdown();
|
||||
restored.join();
|
||||
restored2.shutdown();
|
||||
restored2.join();
|
||||
}
|
||||
|
||||
void
|
||||
@@ -540,6 +647,143 @@ DhtRunnerTester::testShutdownCompletesWithPendingPut()
|
||||
clientNode.join();
|
||||
}
|
||||
|
||||
void
|
||||
DhtRunnerTester::testSavedPortFallback()
|
||||
{
|
||||
// A saved port that is already taken by another process (e.g. fast
|
||||
// application restart while the old instance still holds it) must not
|
||||
// prevent the node from starting: it should fall back to a random port.
|
||||
dht::DhtRunner::Config config;
|
||||
config.dht_config.node_config.persist_path = "/tmp/opendht_test_port_fallback";
|
||||
std::string portFile = config.dht_config.node_config.persist_path + "_port.txt";
|
||||
|
||||
// node1 (from setUp) holds a port; save it as our persisted port.
|
||||
auto takenPort = node1.getBoundPort();
|
||||
{
|
||||
std::ofstream out(portFile);
|
||||
out << takenPort << std::endl << takenPort << std::endl;
|
||||
}
|
||||
|
||||
dht::DhtRunner testNode;
|
||||
CPPUNIT_ASSERT_NO_THROW(testNode.run(0, config));
|
||||
CPPUNIT_ASSERT(testNode.getBoundPort());
|
||||
CPPUNIT_ASSERT(testNode.getBoundPort() != takenPort);
|
||||
|
||||
testNode.shutdown();
|
||||
testNode.join();
|
||||
std::remove(portFile.c_str());
|
||||
std::remove((config.dht_config.node_config.persist_path + "_port.txt").c_str());
|
||||
}
|
||||
|
||||
void
|
||||
DhtRunnerTester::testListenValueEdit()
|
||||
{
|
||||
// Register an editable type on both nodes so the DHT allows edits.
|
||||
static constexpr dht::ValueType::Id EDITABLE_TYPE_ID = 9999;
|
||||
const dht::ValueType editableType {EDITABLE_TYPE_ID,
|
||||
"editable-test",
|
||||
std::chrono::seconds(2),
|
||||
dht::ValueType::DEFAULT_STORE_POLICY,
|
||||
[](dht::InfoHash,
|
||||
const std::shared_ptr<dht::Value>&,
|
||||
std::shared_ptr<dht::Value>&,
|
||||
const dht::InfoHash&,
|
||||
const dht::SockAddr&) { return true; }};
|
||||
node1.registerType(editableType);
|
||||
node2.registerType(editableType);
|
||||
|
||||
auto key = dht::InfoHash::get("listenValueEdit");
|
||||
auto identity = dht::crypto::generateIdentity("ListenEditTester");
|
||||
|
||||
std::mutex mtx;
|
||||
std::condition_variable cv;
|
||||
std::vector<std::pair<dht::Value::Id, bool>> events; // (id, expired?)
|
||||
|
||||
auto ftoken = node1.listen(key, [&](const std::vector<std::shared_ptr<dht::Value>>& vals, bool expired) {
|
||||
std::lock_guard lk(mtx);
|
||||
for (const auto& v : vals) {
|
||||
events.emplace_back(v->id, expired);
|
||||
}
|
||||
cv.notify_all();
|
||||
return true;
|
||||
});
|
||||
|
||||
// Put initial value (signed, editable type)
|
||||
auto val1 = std::make_shared<dht::Value>("version1");
|
||||
val1->type = EDITABLE_TYPE_ID;
|
||||
val1->id = 42;
|
||||
val1->seq = 1;
|
||||
val1->sign(*identity.first);
|
||||
{
|
||||
std::promise<bool> p;
|
||||
node2.put(key, val1, [&](bool ok) { p.set_value(ok); });
|
||||
CPPUNIT_ASSERT(getFutureValue(p.get_future()));
|
||||
}
|
||||
|
||||
// Wait for the first value to be received
|
||||
{
|
||||
std::unique_lock lk(mtx);
|
||||
CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] {
|
||||
return std::any_of(events.begin(), events.end(), [](const auto& e) { return !e.second; });
|
||||
}));
|
||||
}
|
||||
|
||||
// Edit the value (same id, higher seq)
|
||||
auto val2 = std::make_shared<dht::Value>("version2");
|
||||
val2->type = EDITABLE_TYPE_ID;
|
||||
val2->id = 42;
|
||||
val2->seq = 2;
|
||||
val2->sign(*identity.first);
|
||||
{
|
||||
std::promise<bool> p;
|
||||
node2.put(key, val2, [&](bool ok) { p.set_value(ok); });
|
||||
CPPUNIT_ASSERT(getFutureValue(p.get_future()));
|
||||
}
|
||||
|
||||
// Wait for the edited value to be received
|
||||
{
|
||||
std::unique_lock lk(mtx);
|
||||
CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] {
|
||||
int adds = 0;
|
||||
for (const auto& e : events)
|
||||
if (!e.second)
|
||||
adds++;
|
||||
return adds >= 2;
|
||||
}));
|
||||
}
|
||||
|
||||
// Give some extra time for any stray callbacks
|
||||
std::this_thread::sleep_for(500ms);
|
||||
|
||||
// Verify: we should have exactly 2 add events, no expire events.
|
||||
// On value edition the listener should see only the new version
|
||||
// (as an add), not an expiration of the old version.
|
||||
{
|
||||
std::lock_guard lk(mtx);
|
||||
int addCount = 0;
|
||||
int expireCount = 0;
|
||||
for (const auto& e : events) {
|
||||
if (e.second)
|
||||
expireCount++;
|
||||
else
|
||||
addCount++;
|
||||
}
|
||||
CPPUNIT_ASSERT_EQUAL_MESSAGE("Should receive 2 add callbacks (original + edit)", 2, addCount);
|
||||
CPPUNIT_ASSERT_EQUAL_MESSAGE("Should receive no expire callbacks on edit", 0, expireCount);
|
||||
}
|
||||
|
||||
// Now wait for the edited value to actually expire.
|
||||
// Type expiration is 2s. The cache may add a grace period.
|
||||
{
|
||||
std::unique_lock lk(mtx);
|
||||
CPPUNIT_ASSERT_MESSAGE("Edited value should eventually expire", cv.wait_for(lk, 60s, [&] {
|
||||
return std::any_of(events.begin(), events.end(), [](const auto& e) { return e.second; });
|
||||
}));
|
||||
}
|
||||
|
||||
node1.cancelListen(key, ftoken.get());
|
||||
}
|
||||
|
||||
void
|
||||
DhtRunnerTester::testListen()
|
||||
{
|
||||
@@ -752,7 +996,7 @@ DhtRunnerTester::testIdOps()
|
||||
valueCountEdit++;
|
||||
cv.notify_all();
|
||||
});
|
||||
node2.listen(key2, [&](const std::vector<std::shared_ptr<dht::Value>>& values, bool /*expired*/) {
|
||||
auto listenToken = node2.listen(key2, [&](const std::vector<std::shared_ptr<dht::Value>>& values, bool /*expired*/) {
|
||||
for (const auto& v : values) {
|
||||
if (v->seq == 0)
|
||||
CPPUNIT_ASSERT_EQUAL("v1"s, dht::unpackMsg<std::string>(v->data));
|
||||
@@ -796,6 +1040,8 @@ DhtRunnerTester::testIdOps()
|
||||
});
|
||||
std::unique_lock lk(mutex);
|
||||
CPPUNIT_ASSERT(cv.wait_for(lk, 20s, [&] { return valueCountEdit == 4u; }));
|
||||
lk.unlock();
|
||||
node2.cancelListen(key2, listenToken.get());
|
||||
}
|
||||
|
||||
void
|
||||
@@ -886,4 +1132,451 @@ DhtRunnerTester::testMultithread()
|
||||
CPPUNIT_ASSERT_EQUAL(2 * N, putOkCount);
|
||||
}
|
||||
|
||||
void
|
||||
DhtRunnerTester::testGetAfterListen()
|
||||
{
|
||||
// Test that get() after a synced listen delivers values exactly once
|
||||
// from the cache, and that the GetCallback return value is honored.
|
||||
auto key = dht::InfoHash::get("getAfterListen");
|
||||
|
||||
// Put a value first
|
||||
auto val = std::make_shared<dht::Value>("cached_val");
|
||||
{
|
||||
std::promise<bool> p;
|
||||
node2.put(key, val, [&](bool ok) { p.set_value(ok); });
|
||||
CPPUNIT_ASSERT(getFutureValue(p.get_future()));
|
||||
}
|
||||
|
||||
// Start a listen on node1 and wait for it to sync
|
||||
std::mutex mtx;
|
||||
std::condition_variable cv;
|
||||
std::atomic_int listenAddCount {0};
|
||||
|
||||
auto ftoken = node1.listen(key, [&](const std::vector<std::shared_ptr<dht::Value>>& vals, bool expired) {
|
||||
if (!expired)
|
||||
listenAddCount += vals.size();
|
||||
cv.notify_all();
|
||||
return true;
|
||||
});
|
||||
|
||||
// Wait for listen to receive the value
|
||||
{
|
||||
std::unique_lock lk(mtx);
|
||||
CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] { return listenAddCount.load() >= 1; }));
|
||||
}
|
||||
|
||||
// Now do a get — should be served from the synced cache
|
||||
auto vals = getFutureValue(node1.get(key));
|
||||
CPPUNIT_ASSERT_MESSAGE("get() should return values", not vals.empty());
|
||||
CPPUNIT_ASSERT(val->data == vals.front()->data);
|
||||
|
||||
// The listen callback should not have been called again by the get
|
||||
std::this_thread::sleep_for(200ms);
|
||||
CPPUNIT_ASSERT_EQUAL_MESSAGE("Listen should not fire extra callbacks from get()", 1, listenAddCount.load());
|
||||
|
||||
node1.cancelListen(key, ftoken.get());
|
||||
}
|
||||
|
||||
void
|
||||
DhtRunnerTester::testListenDuplicatePut()
|
||||
{
|
||||
// Test that putting the exact same value multiple times only triggers
|
||||
// one add callback in the listener (no duplicate delivery).
|
||||
auto key = dht::InfoHash::get("listenDupPut");
|
||||
|
||||
std::mutex mtx;
|
||||
std::condition_variable cv;
|
||||
std::vector<std::pair<dht::Value::Id, bool>> events;
|
||||
|
||||
auto ftoken = node1.listen(key, [&](const std::vector<std::shared_ptr<dht::Value>>& vals, bool expired) {
|
||||
std::lock_guard lk(mtx);
|
||||
for (const auto& v : vals)
|
||||
events.emplace_back(v->id, expired);
|
||||
cv.notify_all();
|
||||
return true;
|
||||
});
|
||||
|
||||
// Put the same value 3 times
|
||||
auto val = std::make_shared<dht::Value>("duplicate_test");
|
||||
val->id = 123;
|
||||
for (int i = 0; i < 3; i++) {
|
||||
std::promise<bool> p;
|
||||
node2.put(key, val, [&](bool ok) { p.set_value(ok); });
|
||||
CPPUNIT_ASSERT(getFutureValue(p.get_future()));
|
||||
}
|
||||
|
||||
// Wait for at least 1 add to arrive
|
||||
{
|
||||
std::unique_lock lk(mtx);
|
||||
CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] {
|
||||
return std::any_of(events.begin(), events.end(), [](const auto& e) { return !e.second; });
|
||||
}));
|
||||
}
|
||||
|
||||
// Give time for any spurious callbacks
|
||||
std::this_thread::sleep_for(500ms);
|
||||
|
||||
// Should have exactly 1 add, 0 expires
|
||||
{
|
||||
std::lock_guard lk(mtx);
|
||||
int addCount = 0, expireCount = 0;
|
||||
for (const auto& e : events) {
|
||||
if (e.second)
|
||||
expireCount++;
|
||||
else
|
||||
addCount++;
|
||||
}
|
||||
CPPUNIT_ASSERT_EQUAL_MESSAGE("Duplicate puts should produce only 1 add callback", 1, addCount);
|
||||
CPPUNIT_ASSERT_EQUAL_MESSAGE("No expire callbacks expected", 0, expireCount);
|
||||
}
|
||||
|
||||
node1.cancelListen(key, ftoken.get());
|
||||
}
|
||||
|
||||
void
|
||||
DhtRunnerTester::testListenMultiSourceExpire()
|
||||
{
|
||||
// Test that when multiple nodes store the same value, the listener sees
|
||||
// exactly 1 add (no duplicates). When the value expires naturally, the
|
||||
// listener sees exactly 1 expire.
|
||||
static constexpr dht::ValueType::Id SHORT_TYPE_ID = 8888;
|
||||
const dht::ValueType shortType {SHORT_TYPE_ID,
|
||||
"short-lived",
|
||||
std::chrono::seconds(2),
|
||||
dht::ValueType::DEFAULT_STORE_POLICY,
|
||||
[](dht::InfoHash,
|
||||
const std::shared_ptr<dht::Value>&,
|
||||
std::shared_ptr<dht::Value>&,
|
||||
const dht::InfoHash&,
|
||||
const dht::SockAddr&) { return true; }};
|
||||
node1.registerType(shortType);
|
||||
node2.registerType(shortType);
|
||||
|
||||
auto key = dht::InfoHash::get("multiSourceExpire");
|
||||
auto identity = dht::crypto::generateIdentity("MultiSourceTester");
|
||||
|
||||
std::mutex mtx;
|
||||
std::condition_variable cv;
|
||||
std::vector<std::pair<dht::Value::Id, bool>> events;
|
||||
|
||||
// node1 listens; node1 and node2 put
|
||||
auto ftoken = node1.listen(key, [&](const std::vector<std::shared_ptr<dht::Value>>& vals, bool expired) {
|
||||
std::lock_guard lk(mtx);
|
||||
for (const auto& v : vals)
|
||||
events.emplace_back(v->id, expired);
|
||||
cv.notify_all();
|
||||
return true;
|
||||
});
|
||||
|
||||
// Both node1 and node2 put the same value (same id, same seq)
|
||||
auto val = std::make_shared<dht::Value>("multi_source");
|
||||
val->type = SHORT_TYPE_ID;
|
||||
val->id = 55;
|
||||
val->seq = 1;
|
||||
val->sign(*identity.first);
|
||||
|
||||
{
|
||||
std::promise<bool> p1, p2;
|
||||
node1.put(key, val, [&](bool ok) { p1.set_value(ok); });
|
||||
node2.put(key, val, [&](bool ok) { p2.set_value(ok); });
|
||||
CPPUNIT_ASSERT(getFutureValue(p1.get_future()));
|
||||
CPPUNIT_ASSERT(getFutureValue(p2.get_future()));
|
||||
}
|
||||
|
||||
// Wait for at least one add
|
||||
{
|
||||
std::unique_lock lk(mtx);
|
||||
CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] {
|
||||
return std::any_of(events.begin(), events.end(), [](const auto& e) { return !e.second; });
|
||||
}));
|
||||
}
|
||||
|
||||
// Allow time for duplicate callbacks
|
||||
std::this_thread::sleep_for(1s);
|
||||
|
||||
// Should have exactly 1 add (deduplication in OpValueCache)
|
||||
{
|
||||
std::lock_guard lk(mtx);
|
||||
int addCount = 0;
|
||||
for (const auto& e : events)
|
||||
if (!e.second)
|
||||
addCount++;
|
||||
CPPUNIT_ASSERT_EQUAL_MESSAGE("Multiple sources should produce only 1 add callback", 1, addCount);
|
||||
}
|
||||
|
||||
// Wait for natural expiration (type is 2s + grace period)
|
||||
{
|
||||
std::unique_lock lk(mtx);
|
||||
CPPUNIT_ASSERT_MESSAGE("Value should eventually expire", cv.wait_for(lk, 60s, [&] {
|
||||
return std::any_of(events.begin(), events.end(), [](const auto& e) { return e.second; });
|
||||
}));
|
||||
}
|
||||
|
||||
// Should have exactly 1 expire (no duplicate expires from multiple sources)
|
||||
{
|
||||
std::lock_guard lk(mtx);
|
||||
int expireCount = 0;
|
||||
for (const auto& e : events)
|
||||
if (e.second)
|
||||
expireCount++;
|
||||
CPPUNIT_ASSERT_EQUAL_MESSAGE("Should receive exactly 1 expire callback", 1, expireCount);
|
||||
}
|
||||
|
||||
node1.cancelListen(key, ftoken.get());
|
||||
}
|
||||
|
||||
void
|
||||
DhtRunnerTester::testListenEditChainThenExpire()
|
||||
{
|
||||
// Chain of 5 rapid edits (seq 1→5) on the same value, then natural expiration.
|
||||
// Must see 5 adds (one per edit), 0 expires during edits, then exactly 1 expire.
|
||||
static constexpr dht::ValueType::Id CHAIN_TYPE_ID = 7654;
|
||||
const dht::ValueType chainType {CHAIN_TYPE_ID,
|
||||
"chain-edit",
|
||||
std::chrono::seconds(2),
|
||||
dht::ValueType::DEFAULT_STORE_POLICY,
|
||||
[](dht::InfoHash,
|
||||
const std::shared_ptr<dht::Value>&,
|
||||
std::shared_ptr<dht::Value>&,
|
||||
const dht::InfoHash&,
|
||||
const dht::SockAddr&) { return true; }};
|
||||
node1.registerType(chainType);
|
||||
node2.registerType(chainType);
|
||||
|
||||
auto key = dht::InfoHash::get("editChainExpire");
|
||||
auto identity = dht::crypto::generateIdentity("ChainEditTester");
|
||||
|
||||
std::mutex mtx;
|
||||
std::condition_variable cv;
|
||||
struct Event
|
||||
{
|
||||
dht::Value::Id id;
|
||||
uint16_t seq;
|
||||
bool expired;
|
||||
};
|
||||
std::vector<Event> events;
|
||||
|
||||
auto ftoken = node1.listen(key, [&](const std::vector<std::shared_ptr<dht::Value>>& vals, bool expired) {
|
||||
std::lock_guard lk(mtx);
|
||||
for (const auto& v : vals)
|
||||
events.push_back({v->id, v->seq, expired});
|
||||
cv.notify_all();
|
||||
return true;
|
||||
});
|
||||
|
||||
// Rapid chain of 5 edits — wait for the first to arrive before continuing
|
||||
constexpr int NUM_EDITS = 5;
|
||||
{
|
||||
auto v = std::make_shared<dht::Value>("version1");
|
||||
v->type = CHAIN_TYPE_ID;
|
||||
v->id = 99;
|
||||
v->seq = 1;
|
||||
v->sign(*identity.first);
|
||||
std::promise<bool> p;
|
||||
node2.put(key, v, [&](bool ok) { p.set_value(ok); });
|
||||
CPPUNIT_ASSERT(getFutureValue(p.get_future()));
|
||||
}
|
||||
|
||||
// Wait for first add to arrive before sending more edits
|
||||
{
|
||||
std::unique_lock lk(mtx);
|
||||
CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] {
|
||||
return std::any_of(events.begin(), events.end(), [](const Event& e) { return !e.expired; });
|
||||
}));
|
||||
}
|
||||
|
||||
// Now send remaining edits rapidly
|
||||
for (int seq = 2; seq <= NUM_EDITS; seq++) {
|
||||
auto v = std::make_shared<dht::Value>("version" + std::to_string(seq));
|
||||
v->type = CHAIN_TYPE_ID;
|
||||
v->id = 99;
|
||||
v->seq = seq;
|
||||
v->sign(*identity.first);
|
||||
std::promise<bool> p;
|
||||
node2.put(key, v, [&](bool ok) { p.set_value(ok); });
|
||||
CPPUNIT_ASSERT(getFutureValue(p.get_future()));
|
||||
}
|
||||
|
||||
// Wait for edits to arrive (at least 2: initial + one or more updates)
|
||||
// Note: rapid edits may coalesce — the DHT stores only the latest seq,
|
||||
// so the listener may not see every intermediate version.
|
||||
{
|
||||
std::unique_lock lk(mtx);
|
||||
CPPUNIT_ASSERT(cv.wait_for(lk, 15s, [&] {
|
||||
int adds = 0;
|
||||
for (const auto& e : events)
|
||||
if (!e.expired)
|
||||
adds++;
|
||||
return adds >= 2;
|
||||
}));
|
||||
}
|
||||
|
||||
// Wait a bit more for any remaining events
|
||||
std::this_thread::sleep_for(1s);
|
||||
|
||||
// Verify: multiple adds, 0 expires during edits (no phantom expiry)
|
||||
int totalAdds;
|
||||
{
|
||||
std::lock_guard lk(mtx);
|
||||
totalAdds = 0;
|
||||
int expireCount = 0;
|
||||
for (const auto& e : events) {
|
||||
if (e.expired)
|
||||
expireCount++;
|
||||
else
|
||||
totalAdds++;
|
||||
}
|
||||
CPPUNIT_ASSERT_MESSAGE("Should receive at least 2 add callbacks", totalAdds >= 2);
|
||||
CPPUNIT_ASSERT_EQUAL_MESSAGE("No expires during edit chain", 0, expireCount);
|
||||
|
||||
// Verify the latest add has the highest seq
|
||||
uint16_t maxSeq = 0;
|
||||
for (const auto& e : events)
|
||||
if (!e.expired && e.seq > maxSeq)
|
||||
maxSeq = e.seq;
|
||||
CPPUNIT_ASSERT_EQUAL_MESSAGE("Latest add should have the final seq", (uint16_t) NUM_EDITS, maxSeq);
|
||||
}
|
||||
|
||||
// Wait for natural expiration
|
||||
{
|
||||
std::unique_lock lk(mtx);
|
||||
CPPUNIT_ASSERT_MESSAGE("Value should eventually expire", cv.wait_for(lk, 60s, [&] {
|
||||
return std::any_of(events.begin(), events.end(), [](const Event& e) { return e.expired; });
|
||||
}));
|
||||
}
|
||||
|
||||
// Verify exactly 1 expire with the latest seq
|
||||
{
|
||||
std::lock_guard lk(mtx);
|
||||
int expireCount = 0;
|
||||
uint16_t expiredSeq = 0;
|
||||
for (const auto& e : events) {
|
||||
if (e.expired) {
|
||||
expireCount++;
|
||||
expiredSeq = e.seq;
|
||||
}
|
||||
}
|
||||
CPPUNIT_ASSERT_EQUAL_MESSAGE("Should receive exactly 1 expire", 1, expireCount);
|
||||
CPPUNIT_ASSERT_EQUAL_MESSAGE("Expired value should be the latest version", (uint16_t) NUM_EDITS, expiredSeq);
|
||||
}
|
||||
|
||||
node1.cancelListen(key, ftoken.get());
|
||||
}
|
||||
|
||||
void
|
||||
DhtRunnerTester::testListenMultiValuePartialExpire()
|
||||
{
|
||||
// Two values with different types: one short-lived (2s), one long-lived (60s).
|
||||
// The short one expires while the long one stays. Verify isolation:
|
||||
// no spurious expire for the long-lived value.
|
||||
static constexpr dht::ValueType::Id SHORT_TYPE_ID = 3001;
|
||||
static constexpr dht::ValueType::Id LONG_TYPE_ID = 3002;
|
||||
const dht::ValueType shortType {SHORT_TYPE_ID,
|
||||
"short-partial",
|
||||
std::chrono::seconds(2),
|
||||
dht::ValueType::DEFAULT_STORE_POLICY,
|
||||
[](dht::InfoHash,
|
||||
const std::shared_ptr<dht::Value>&,
|
||||
std::shared_ptr<dht::Value>&,
|
||||
const dht::InfoHash&,
|
||||
const dht::SockAddr&) { return true; }};
|
||||
const dht::ValueType longType {LONG_TYPE_ID,
|
||||
"long-partial",
|
||||
std::chrono::seconds(600),
|
||||
dht::ValueType::DEFAULT_STORE_POLICY,
|
||||
[](dht::InfoHash,
|
||||
const std::shared_ptr<dht::Value>&,
|
||||
std::shared_ptr<dht::Value>&,
|
||||
const dht::InfoHash&,
|
||||
const dht::SockAddr&) { return true; }};
|
||||
node1.registerType(shortType);
|
||||
node1.registerType(longType);
|
||||
node2.registerType(shortType);
|
||||
node2.registerType(longType);
|
||||
|
||||
auto key = dht::InfoHash::get("multiValuePartialExpire");
|
||||
auto identity = dht::crypto::generateIdentity("PartialExpireTester");
|
||||
|
||||
std::mutex mtx;
|
||||
std::condition_variable cv;
|
||||
struct Event
|
||||
{
|
||||
dht::Value::Id id;
|
||||
bool expired;
|
||||
};
|
||||
std::vector<Event> events;
|
||||
|
||||
auto ftoken = node1.listen(key, [&](const std::vector<std::shared_ptr<dht::Value>>& vals, bool expired) {
|
||||
std::lock_guard lk(mtx);
|
||||
for (const auto& v : vals)
|
||||
events.push_back({v->id, expired});
|
||||
cv.notify_all();
|
||||
return true;
|
||||
});
|
||||
|
||||
// Put short-lived value (id=1)
|
||||
auto shortVal = std::make_shared<dht::Value>("short");
|
||||
shortVal->type = SHORT_TYPE_ID;
|
||||
shortVal->id = 1;
|
||||
shortVal->seq = 1;
|
||||
shortVal->sign(*identity.first);
|
||||
{
|
||||
std::promise<bool> p;
|
||||
node2.put(key, shortVal, [&](bool ok) { p.set_value(ok); });
|
||||
CPPUNIT_ASSERT(getFutureValue(p.get_future()));
|
||||
}
|
||||
|
||||
// Put long-lived value (id=2)
|
||||
auto longVal = std::make_shared<dht::Value>("long");
|
||||
longVal->type = LONG_TYPE_ID;
|
||||
longVal->id = 2;
|
||||
longVal->seq = 1;
|
||||
longVal->sign(*identity.first);
|
||||
{
|
||||
std::promise<bool> p;
|
||||
node2.put(key, longVal, [&](bool ok) { p.set_value(ok); });
|
||||
CPPUNIT_ASSERT(getFutureValue(p.get_future()));
|
||||
}
|
||||
|
||||
// Wait for both adds
|
||||
{
|
||||
std::unique_lock lk(mtx);
|
||||
CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] {
|
||||
int adds = 0;
|
||||
for (const auto& e : events)
|
||||
if (!e.expired)
|
||||
adds++;
|
||||
return adds >= 2;
|
||||
}));
|
||||
}
|
||||
|
||||
// Wait for short-lived value to expire
|
||||
{
|
||||
std::unique_lock lk(mtx);
|
||||
CPPUNIT_ASSERT_MESSAGE("Short-lived value should expire", cv.wait_for(lk, 60s, [&] {
|
||||
return std::any_of(events.begin(), events.end(), [](const Event& e) { return e.id == 1 && e.expired; });
|
||||
}));
|
||||
}
|
||||
|
||||
// Give extra time for any spurious callbacks
|
||||
std::this_thread::sleep_for(1s);
|
||||
|
||||
// Verify: value 1 expired, value 2 did NOT expire
|
||||
{
|
||||
std::lock_guard lk(mtx);
|
||||
bool longExpired = std::any_of(events.begin(), events.end(), [](const Event& e) {
|
||||
return e.id == 2 && e.expired;
|
||||
});
|
||||
CPPUNIT_ASSERT_MESSAGE("Long-lived value should NOT be expired", !longExpired);
|
||||
|
||||
int shortExpires = 0;
|
||||
for (const auto& e : events)
|
||||
if (e.id == 1 && e.expired)
|
||||
shortExpires++;
|
||||
CPPUNIT_ASSERT_EQUAL_MESSAGE("Short-lived value should expire exactly once", 1, shortExpires);
|
||||
}
|
||||
|
||||
node1.cancelListen(key, ftoken.get());
|
||||
}
|
||||
|
||||
} // namespace test
|
||||
@@ -10,6 +10,22 @@
|
||||
|
||||
namespace test {
|
||||
|
||||
class DhtRunnerPushTester : public CppUnit::TestFixture
|
||||
{
|
||||
CPPUNIT_TEST_SUITE(DhtRunnerPushTester);
|
||||
CPPUNIT_TEST(testPushBeforeRun);
|
||||
CPPUNIT_TEST(testPushAfterJoin);
|
||||
CPPUNIT_TEST(testPushBurstAfterJoin);
|
||||
CPPUNIT_TEST(testPushWhileRunning);
|
||||
CPPUNIT_TEST_SUITE_END();
|
||||
|
||||
public:
|
||||
void testPushBeforeRun();
|
||||
void testPushAfterJoin();
|
||||
void testPushBurstAfterJoin();
|
||||
void testPushWhileRunning();
|
||||
};
|
||||
|
||||
class DhtRunnerTester : public CppUnit::TestFixture
|
||||
{
|
||||
CPPUNIT_TEST_SUITE(DhtRunnerTester);
|
||||
@@ -18,15 +34,24 @@ class DhtRunnerTester : public CppUnit::TestFixture
|
||||
CPPUNIT_TEST(testPutDuplicate);
|
||||
CPPUNIT_TEST(testPutOverride);
|
||||
CPPUNIT_TEST(testListen);
|
||||
CPPUNIT_TEST(testListenValueEdit);
|
||||
CPPUNIT_TEST(testListenLotOfBytes);
|
||||
CPPUNIT_TEST(testStoreEmptyValue);
|
||||
CPPUNIT_TEST(testIdOps);
|
||||
CPPUNIT_TEST(testImportValuesPreservesRemoteQuota);
|
||||
CPPUNIT_TEST(testExportValuesUsesSystemTime);
|
||||
CPPUNIT_TEST(testImportValuesPreservesStoredExpiration);
|
||||
CPPUNIT_TEST(testImportPermanentValueExpires);
|
||||
CPPUNIT_TEST(testBootstrapSetsConnectingState);
|
||||
CPPUNIT_TEST(testBootstrapThenPutNoRace);
|
||||
CPPUNIT_TEST(testBootstrapMissingNodeThenPutFails);
|
||||
CPPUNIT_TEST(testShutdownCompletesWithPendingPut);
|
||||
CPPUNIT_TEST(testSavedPortFallback);
|
||||
CPPUNIT_TEST(testGetAfterListen);
|
||||
CPPUNIT_TEST(testListenDuplicatePut);
|
||||
CPPUNIT_TEST(testListenMultiSourceExpire);
|
||||
CPPUNIT_TEST(testListenEditChainThenExpire);
|
||||
CPPUNIT_TEST(testListenMultiValuePartialExpire);
|
||||
CPPUNIT_TEST_SUITE_END();
|
||||
|
||||
dht::DhtRunner node1 {};
|
||||
@@ -66,16 +91,31 @@ public:
|
||||
*/
|
||||
void testIdOps();
|
||||
void testImportValuesPreservesRemoteQuota();
|
||||
void testExportValuesUsesSystemTime();
|
||||
void testImportValuesPreservesStoredExpiration();
|
||||
void testImportPermanentValueExpires();
|
||||
void testBootstrapSetsConnectingState();
|
||||
void testBootstrapThenPutNoRace();
|
||||
void testBootstrapMissingNodeThenPutFails();
|
||||
void testShutdownCompletesWithPendingPut();
|
||||
void testSavedPortFallback();
|
||||
void testGetAfterListen();
|
||||
void testListenDuplicatePut();
|
||||
void testListenMultiSourceExpire();
|
||||
void testListenEditChainThenExpire();
|
||||
void testListenMultiValuePartialExpire();
|
||||
/**
|
||||
* Test that editing a value only triggers an add callback, no expire
|
||||
*/
|
||||
void testListenValueEdit();
|
||||
/**
|
||||
* Test listen method with lot of datas
|
||||
*/
|
||||
void testListenLotOfBytes();
|
||||
/**
|
||||
* Test storing an empty value
|
||||
*/
|
||||
void testStoreEmptyValue();
|
||||
/**
|
||||
* Test multithread
|
||||
*/
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
|
||||
@@ -332,6 +333,36 @@ HttpTester::test_parse_url_just_slash()
|
||||
CPPUNIT_ASSERT_EQUAL(std::string("/"), parsed.target);
|
||||
}
|
||||
|
||||
void
|
||||
HttpTester::test_completed_resolver_callback_uses_io_thread()
|
||||
{
|
||||
asio::io_context ioContext;
|
||||
const asio::ip::tcp::endpoint endpoint(asio::ip::address_v4::loopback(), 80);
|
||||
dht::http::Resolver resolver(ioContext, {endpoint});
|
||||
const auto callerThread = std::this_thread::get_id();
|
||||
std::thread::id callbackThread;
|
||||
asio::error_code callbackError;
|
||||
std::vector<asio::ip::tcp::endpoint> resolvedEndpoints;
|
||||
std::atomic_bool callbackCalled {false};
|
||||
|
||||
resolver.add_callback([&](const asio::error_code& ec, const auto& endpoints) {
|
||||
callbackError = ec;
|
||||
callbackThread = std::this_thread::get_id();
|
||||
resolvedEndpoints = endpoints;
|
||||
callbackCalled = true;
|
||||
});
|
||||
|
||||
CPPUNIT_ASSERT(!callbackCalled);
|
||||
std::thread ioThread([&] { ioContext.run(); });
|
||||
ioThread.join();
|
||||
|
||||
CPPUNIT_ASSERT(callbackCalled);
|
||||
CPPUNIT_ASSERT(!callbackError);
|
||||
CPPUNIT_ASSERT(callbackThread != callerThread);
|
||||
CPPUNIT_ASSERT_EQUAL(size_t(1), resolvedEndpoints.size());
|
||||
CPPUNIT_ASSERT_EQUAL(endpoint, resolvedEndpoints.front());
|
||||
}
|
||||
|
||||
void
|
||||
HttpTester::test_send_json()
|
||||
{
|
||||
|
||||
@@ -39,6 +39,7 @@ class HttpTester : public CppUnit::TestFixture
|
||||
CPPUNIT_TEST(test_parse_url_complex_path_query_fragment);
|
||||
CPPUNIT_TEST(test_parse_url_empty);
|
||||
CPPUNIT_TEST(test_parse_url_just_slash);
|
||||
CPPUNIT_TEST(test_completed_resolver_callback_uses_io_thread);
|
||||
// send
|
||||
CPPUNIT_TEST(test_send_json);
|
||||
CPPUNIT_TEST_SUITE_END();
|
||||
@@ -82,6 +83,7 @@ public:
|
||||
void test_parse_url_complex_path_query_fragment();
|
||||
void test_parse_url_empty();
|
||||
void test_parse_url_just_slash();
|
||||
void test_completed_resolver_callback_uses_io_thread();
|
||||
/**
|
||||
* Test send(json)
|
||||
*/
|
||||
|
||||
+338
-29
@@ -214,9 +214,12 @@ makeEngine(std::unique_ptr<TestDatagramSocket>&& socket,
|
||||
std::mt19937_64& rd,
|
||||
int& onNewNodeCalls)
|
||||
{
|
||||
net::NetworkConfig config {};
|
||||
config.max_req_per_sec = -1;
|
||||
config.max_peer_req_per_sec = -1;
|
||||
return net::NetworkEngine(
|
||||
myid,
|
||||
{},
|
||||
config,
|
||||
std::move(socket),
|
||||
{},
|
||||
rd,
|
||||
@@ -234,6 +237,21 @@ makeEngine(std::unique_ptr<TestDatagramSocket>&& socket,
|
||||
[](Sp<Node>, const InfoHash&, const Blob&, const Value::Id&) { return net::RequestAnswer {}; });
|
||||
}
|
||||
|
||||
static Sp<Node>
|
||||
addPendingGetRequest(net::NetworkEngine& engine, const InfoHash& id, const SockAddr& addr, Tid tid)
|
||||
{
|
||||
auto node = engine.insertNode(id, addr);
|
||||
auto request = std::make_shared<net::Request>(
|
||||
MessageType::GetValues,
|
||||
tid,
|
||||
node,
|
||||
Blob {},
|
||||
[](const net::Request&, ParsedMessage&&) {},
|
||||
[](const net::Request&, bool) {});
|
||||
node->requested(request);
|
||||
return node;
|
||||
}
|
||||
|
||||
void
|
||||
NetworkEngineTester::setUp()
|
||||
{}
|
||||
@@ -243,24 +261,130 @@ NetworkEngineTester::tearDown()
|
||||
{}
|
||||
|
||||
void
|
||||
NetworkEngineTester::testIgnoresUnknownPartialData()
|
||||
NetworkEngineTester::testCompletesPartialSessionWithDataBeforeHeader()
|
||||
{
|
||||
Scheduler scheduler;
|
||||
std::mt19937_64 rd(0);
|
||||
InfoHash myid = InfoHash::getRandom(rd);
|
||||
InfoHash remoteId = InfoHash::getRandom(rd);
|
||||
int onNewNodeCalls = 0;
|
||||
auto engine = makeEngine(std::make_unique<TestDatagramSocket>(), scheduler, myid, rd, onNewNodeCalls);
|
||||
|
||||
Blob fragment = {1, 2, 3};
|
||||
auto packet = makeValueDataPacketBlob(99, 0, 0, fragment);
|
||||
auto serialized = serializeValue("fragment before header");
|
||||
auto from = makeIPv4("127.0.0.2", 5000);
|
||||
addPendingGetRequest(engine, remoteId, from, 99);
|
||||
auto initialOnNewNodeCalls = onNewNodeCalls;
|
||||
auto fragment = makeValueDataPacketBlob(99, 0, 0, serialized);
|
||||
auto header = makeReplyHeaderPacketBlob(remoteId, 99, {serialized.size()});
|
||||
|
||||
engine.processMessage(fragment.data(), fragment.size(), from);
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 1, engine.getPartialCount());
|
||||
CPPUNIT_ASSERT_EQUAL(initialOnNewNodeCalls, onNewNodeCalls);
|
||||
|
||||
engine.processMessage(header.data(), header.size(), from);
|
||||
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 0, engine.getPartialCount());
|
||||
CPPUNIT_ASSERT(onNewNodeCalls > initialOnNewNodeCalls);
|
||||
}
|
||||
|
||||
void
|
||||
NetworkEngineTester::testBuffersLargeValueBeforeHeader()
|
||||
{
|
||||
Scheduler scheduler;
|
||||
std::mt19937_64 rd(8);
|
||||
InfoHash myid = InfoHash::getRandom(rd);
|
||||
InfoHash remoteId = InfoHash::getRandom(rd);
|
||||
int onNewNodeCalls = 0;
|
||||
auto engine = makeEngine(std::make_unique<TestDatagramSocket>(), scheduler, myid, rd, onNewNodeCalls);
|
||||
|
||||
auto serialized = serializeValue(std::string(57 * 1024, 'l'));
|
||||
auto from = makeIPv4("127.0.0.2", 5008);
|
||||
addPendingGetRequest(engine, remoteId, from, 101);
|
||||
auto initialOnNewNodeCalls = onNewNodeCalls;
|
||||
for (size_t offset = 0; offset < serialized.size(); offset += TEST_MTU) {
|
||||
auto end = std::min(offset + TEST_MTU, serialized.size());
|
||||
Blob data(serialized.begin() + offset, serialized.begin() + end);
|
||||
auto fragment = makeValueDataPacketBlob(101, 0, offset, data);
|
||||
engine.processMessage(fragment.data(), fragment.size(), from);
|
||||
}
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 1, engine.getPartialCount());
|
||||
|
||||
auto header = makeReplyHeaderPacketBlob(remoteId, 101, {serialized.size()});
|
||||
engine.processMessage(header.data(), header.size(), from);
|
||||
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 0, engine.getPartialCount());
|
||||
CPPUNIT_ASSERT(onNewNodeCalls > initialOnNewNodeCalls);
|
||||
}
|
||||
|
||||
void
|
||||
NetworkEngineTester::testIgnoresEmptyPartialData()
|
||||
{
|
||||
Scheduler scheduler;
|
||||
std::mt19937_64 rd(7);
|
||||
InfoHash myid = InfoHash::getRandom(rd);
|
||||
int onNewNodeCalls = 0;
|
||||
auto engine = makeEngine(std::make_unique<TestDatagramSocket>(), scheduler, myid, rd, onNewNodeCalls);
|
||||
|
||||
auto packet = makeValueDataPacketBlob(100, 0, 0, {});
|
||||
auto from = makeIPv4("127.0.0.2", 5007);
|
||||
engine.processMessage(packet.data(), packet.size(), from);
|
||||
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 0, engine.getPartialCount());
|
||||
CPPUNIT_ASSERT_EQUAL(0, onNewNodeCalls);
|
||||
}
|
||||
|
||||
void
|
||||
NetworkEngineTester::testRejectsUnsolicitedPartialReply()
|
||||
{
|
||||
Scheduler scheduler;
|
||||
std::mt19937_64 rd(9);
|
||||
InfoHash myid = InfoHash::getRandom(rd);
|
||||
InfoHash remoteId = InfoHash::getRandom(rd);
|
||||
int onNewNodeCalls = 0;
|
||||
auto engine = makeEngine(std::make_unique<TestDatagramSocket>(), scheduler, myid, rd, onNewNodeCalls);
|
||||
|
||||
auto from = makeIPv4("127.0.0.2", 5009);
|
||||
auto header = makeReplyHeaderPacketBlob(remoteId, 102, {TEST_MTU});
|
||||
engine.processMessage(header.data(), header.size(), from);
|
||||
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 0, engine.getPartialCount());
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 0, engine.getNodeCacheSize());
|
||||
}
|
||||
|
||||
void
|
||||
NetworkEngineTester::testDropsMalformedCompletedPartialMessage()
|
||||
{
|
||||
Scheduler scheduler;
|
||||
std::mt19937_64 rd(10);
|
||||
InfoHash myid = InfoHash::getRandom(rd);
|
||||
InfoHash remoteId = InfoHash::getRandom(rd);
|
||||
int onNewNodeCalls = 0;
|
||||
auto engine = makeEngine(std::make_unique<TestDatagramSocket>(), scheduler, myid, rd, onNewNodeCalls);
|
||||
|
||||
auto from = makeIPv4("127.0.0.2", 5010);
|
||||
addPendingGetRequest(engine, remoteId, from, 103);
|
||||
Blob malformed {0xc1};
|
||||
auto header = makeReplyHeaderPacketBlob(remoteId, 103, {malformed.size()});
|
||||
auto fragment = makeValueDataPacketBlob(103, 0, 0, malformed);
|
||||
|
||||
engine.processMessage(header.data(), header.size(), from);
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 1, engine.getPartialCount());
|
||||
engine.processMessage(fragment.data(), fragment.size(), from);
|
||||
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 0, engine.getPartialCount());
|
||||
|
||||
auto earlyFrom = makeIPv4("127.0.0.2", 5011);
|
||||
addPendingGetRequest(engine, remoteId, earlyFrom, 104);
|
||||
auto earlyHeader = makeReplyHeaderPacketBlob(remoteId, 104, {malformed.size()});
|
||||
auto earlyFragment = makeValueDataPacketBlob(104, 0, 0, malformed);
|
||||
|
||||
engine.processMessage(earlyFragment.data(), earlyFragment.size(), earlyFrom);
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 1, engine.getPartialCount());
|
||||
engine.processMessage(earlyHeader.data(), earlyHeader.size(), earlyFrom);
|
||||
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 0, engine.getPartialCount());
|
||||
}
|
||||
|
||||
void
|
||||
NetworkEngineTester::testCompletesPartialSessionFromSameSource()
|
||||
{
|
||||
@@ -274,6 +398,8 @@ NetworkEngineTester::testCompletesPartialSessionFromSameSource()
|
||||
std::string data(TEST_MTU * 2 + 64, 'z');
|
||||
auto serialized = serializeValue(data);
|
||||
auto from = makeIPv4("127.0.0.2", 5001);
|
||||
addPendingGetRequest(engine, remoteId, from, 7);
|
||||
auto initialOnNewNodeCalls = onNewNodeCalls;
|
||||
auto header = makeReplyHeaderPacketBlob(remoteId, 7, {serialized.size()});
|
||||
|
||||
engine.processMessage(header.data(), header.size(), from);
|
||||
@@ -287,45 +413,40 @@ NetworkEngineTester::testCompletesPartialSessionFromSameSource()
|
||||
}
|
||||
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 0, engine.getPartialCount());
|
||||
CPPUNIT_ASSERT(onNewNodeCalls > 0);
|
||||
CPPUNIT_ASSERT(onNewNodeCalls > initialOnNewNodeCalls);
|
||||
}
|
||||
|
||||
void
|
||||
NetworkEngineTester::testKeepsSessionForWrongSourceFragment()
|
||||
NetworkEngineTester::testSeparatesPartialSessionsBySource()
|
||||
{
|
||||
Scheduler scheduler;
|
||||
std::mt19937_64 rd(2);
|
||||
InfoHash myid = InfoHash::getRandom(rd);
|
||||
InfoHash remoteId = InfoHash::getRandom(rd);
|
||||
InfoHash firstRemoteId = InfoHash::getRandom(rd);
|
||||
InfoHash secondRemoteId = InfoHash::getRandom(rd);
|
||||
int onNewNodeCalls = 0;
|
||||
auto engine = makeEngine(std::make_unique<TestDatagramSocket>(), scheduler, myid, rd, onNewNodeCalls);
|
||||
|
||||
std::string data(TEST_MTU + 32, 'w');
|
||||
auto serialized = serializeValue(data);
|
||||
auto goodFrom = makeIPv4("127.0.0.2", 5002);
|
||||
auto badFrom = makeIPv4("127.0.0.3", 5002);
|
||||
auto header = makeReplyHeaderPacketBlob(remoteId, 11, {serialized.size()});
|
||||
auto serialized = serializeValue("same transaction, different peers");
|
||||
auto firstFrom = makeIPv4("127.0.0.2", 5002);
|
||||
auto secondFrom = makeIPv4("127.0.0.3", 5002);
|
||||
addPendingGetRequest(engine, firstRemoteId, firstFrom, 11);
|
||||
addPendingGetRequest(engine, secondRemoteId, secondFrom, 11);
|
||||
auto initialOnNewNodeCalls = onNewNodeCalls;
|
||||
auto firstHeader = makeReplyHeaderPacketBlob(firstRemoteId, 11, {serialized.size()});
|
||||
auto secondHeader = makeReplyHeaderPacketBlob(secondRemoteId, 11, {serialized.size()});
|
||||
|
||||
engine.processMessage(header.data(), header.size(), goodFrom);
|
||||
engine.processMessage(firstHeader.data(), firstHeader.size(), firstFrom);
|
||||
engine.processMessage(secondHeader.data(), secondHeader.size(), secondFrom);
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 2, engine.getPartialCount());
|
||||
|
||||
auto fragment = makeValueDataPacketBlob(11, 0, 0, serialized);
|
||||
engine.processMessage(fragment.data(), fragment.size(), firstFrom);
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 1, engine.getPartialCount());
|
||||
|
||||
auto firstEnd = std::min(TEST_MTU, serialized.size());
|
||||
Blob firstFragment(serialized.begin(), serialized.begin() + firstEnd);
|
||||
auto firstPacket = makeValueDataPacketBlob(11, 0, 0, firstFragment);
|
||||
engine.processMessage(firstPacket.data(), firstPacket.size(), badFrom);
|
||||
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 1, engine.getPartialCount());
|
||||
CPPUNIT_ASSERT_EQUAL(0, onNewNodeCalls);
|
||||
|
||||
engine.processMessage(firstPacket.data(), firstPacket.size(), goodFrom);
|
||||
if (firstEnd < serialized.size()) {
|
||||
Blob tail(serialized.begin() + firstEnd, serialized.end());
|
||||
auto tailPacket = makeValueDataPacketBlob(11, 0, firstEnd, tail);
|
||||
engine.processMessage(tailPacket.data(), tailPacket.size(), goodFrom);
|
||||
}
|
||||
engine.processMessage(fragment.data(), fragment.size(), secondFrom);
|
||||
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 0, engine.getPartialCount());
|
||||
CPPUNIT_ASSERT(onNewNodeCalls > 0);
|
||||
CPPUNIT_ASSERT(onNewNodeCalls > initialOnNewNodeCalls);
|
||||
}
|
||||
|
||||
void
|
||||
@@ -546,6 +667,194 @@ NetworkEngineTester::testUnauthorizedListenFlushClearsListenState()
|
||||
CPPUNIT_ASSERT(!listenStatusIt->second.refresh);
|
||||
}
|
||||
|
||||
void
|
||||
NetworkEngineTester::testGetRequestReuse()
|
||||
{
|
||||
struct ReuseCase
|
||||
{
|
||||
const char* name;
|
||||
bool projected;
|
||||
Query requested;
|
||||
Query answered;
|
||||
bool shouldSend;
|
||||
};
|
||||
const Select ids = Select {}.field(Value::Field::Id);
|
||||
const Select idTypes = Select {}.field(Value::Field::Id).field(Value::Field::ValueType);
|
||||
const std::vector<ReuseCase> cases {
|
||||
{"different projection filters", true, Query {ids, Where {}.valueType(1)}, Query {ids}, true },
|
||||
{"matching projection filters",
|
||||
true, Query {ids, Where {}.valueType(1)},
|
||||
Query {ids, Where {}.valueType(1)},
|
||||
false },
|
||||
{"wider matching projection",
|
||||
true, Query {ids, Where {}.valueType(1)},
|
||||
Query {idTypes, Where {}.valueType(1)},
|
||||
false },
|
||||
{"narrower projection", true, Query {idTypes, Where {}.valueType(1)}, Query {ids, Where {}.valueType(1)}, true },
|
||||
{"filtered answer is incomplete", true, Query {ids}, Query {ids, Where {}.valueType(1)}, true },
|
||||
{"ordinary get can filter locally", false, Query {Select {}, Where {}.valueType(1)}, Query {}, false},
|
||||
};
|
||||
|
||||
for (auto state : {net::Request::State::PENDING, net::Request::State::COMPLETED}) {
|
||||
for (const auto& testCase : cases) {
|
||||
Config config {};
|
||||
config.max_req_per_sec = -1;
|
||||
config.max_peer_req_per_sec = -1;
|
||||
Dht localDht(std::make_unique<TestDatagramSocket>(), config, {}, std::make_unique<std::mt19937_64>(42));
|
||||
const auto now = localDht.scheduler.time();
|
||||
auto search = std::make_shared<Dht::Search>();
|
||||
search->id = InfoHash::getRandom(localDht.rd);
|
||||
search->af = AF_INET;
|
||||
auto requested = std::make_shared<Query>(testCase.requested);
|
||||
auto answered = std::make_shared<Query>(testCase.answered);
|
||||
Dht::Get operation {};
|
||||
operation.start = now - std::chrono::seconds {1};
|
||||
operation.query = requested;
|
||||
if (testCase.projected)
|
||||
operation.query_cb = [](const std::vector<Sp<FieldValueIndex>>&) {
|
||||
return true;
|
||||
};
|
||||
else
|
||||
operation.get_cb = [](const std::vector<Sp<Value>>&) {
|
||||
return true;
|
||||
};
|
||||
operation.done_cb = [](bool, const std::vector<Sp<Node>>&) {
|
||||
};
|
||||
search->callbacks.emplace(operation.start, operation);
|
||||
auto peer = std::make_shared<Node>(InfoHash::getRandom(localDht.rd),
|
||||
makeIPv4("127.0.0.2", 5005),
|
||||
localDht.rd,
|
||||
false);
|
||||
auto searchNode = std::make_unique<Dht::SearchNode>(peer);
|
||||
searchNode->last_get_reply = now;
|
||||
auto reply = std::make_shared<net::Request>(state);
|
||||
reply->node = peer;
|
||||
reply->reply_time = now;
|
||||
searchNode->getStatus[answered] = reply;
|
||||
auto searchNodePtr = search->nodes.emplace_back(std::move(searchNode)).get();
|
||||
|
||||
auto sent = localDht.searchSendGetValues(search, searchNodePtr, true);
|
||||
|
||||
CPPUNIT_ASSERT_EQUAL_MESSAGE(testCase.name, testCase.shouldSend, sent != nullptr);
|
||||
CPPUNIT_ASSERT_EQUAL_MESSAGE(testCase.name,
|
||||
testCase.shouldSend,
|
||||
searchNodePtr->getStatus.count(requested) != 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
NetworkEngineTester::testFilteredProjectionDoesNotWaitForUnrelatedPeer()
|
||||
{
|
||||
Config config {};
|
||||
config.max_req_per_sec = -1;
|
||||
config.max_peer_req_per_sec = -1;
|
||||
auto socket = std::make_unique<TestDatagramSocket>();
|
||||
auto socketPtr = socket.get();
|
||||
auto random = std::make_unique<std::mt19937_64>(43);
|
||||
Dht localDht(std::move(socket), config, {}, std::move(random));
|
||||
const auto now = localDht.scheduler.time();
|
||||
auto search = std::make_shared<Dht::Search>();
|
||||
search->id = InfoHash::getRandom(localDht.rd);
|
||||
search->af = AF_INET;
|
||||
search->refill_time = now;
|
||||
search->nextSearchStep = localDht.scheduler.add(time_point::max(), [] {});
|
||||
auto broad = std::make_shared<Query>(Select {}.field(Value::Field::Id));
|
||||
auto filtered = std::make_shared<Query>(Select {}.field(Value::Field::Id), Where {}.valueType(1));
|
||||
Dht::Get broadOperation {};
|
||||
broadOperation.start = now - std::chrono::seconds {2};
|
||||
broadOperation.query = broad;
|
||||
broadOperation.query_cb = [](const std::vector<Sp<FieldValueIndex>>&) {
|
||||
return true;
|
||||
};
|
||||
broadOperation.done_cb = [](bool, const std::vector<Sp<Node>>&) {
|
||||
};
|
||||
search->callbacks.emplace(broadOperation.start, broadOperation);
|
||||
size_t filteredCallbacks = 0;
|
||||
Dht::Get filteredOperation {};
|
||||
filteredOperation.start = now - std::chrono::seconds {1};
|
||||
filteredOperation.query = filtered;
|
||||
filteredOperation.query_cb = [&](const std::vector<Sp<FieldValueIndex>>&) {
|
||||
++filteredCallbacks;
|
||||
return true;
|
||||
};
|
||||
filteredOperation.done_cb = [](bool, const std::vector<Sp<Node>>&) {
|
||||
};
|
||||
search->callbacks.emplace(filteredOperation.start, filteredOperation);
|
||||
for (unsigned peerIndex = 0; peerIndex < 2; ++peerIndex) {
|
||||
auto peer = std::make_shared<Node>(InfoHash::getRandom(localDht.rd),
|
||||
makeIPv4(peerIndex == 0 ? "127.0.0.2" : "127.0.0.3", 5005),
|
||||
localDht.rd,
|
||||
false);
|
||||
auto searchNode = std::make_unique<Dht::SearchNode>(peer);
|
||||
searchNode->last_get_reply = now;
|
||||
searchNode->token = Blob {1};
|
||||
auto state = peerIndex == 0 ? net::Request::State::COMPLETED : net::Request::State::PENDING;
|
||||
auto request = std::make_shared<net::Request>(state);
|
||||
request->node = peer;
|
||||
request->reply_time = now;
|
||||
searchNode->getStatus[broad] = request;
|
||||
search->nodes.emplace_back(std::move(searchNode));
|
||||
}
|
||||
auto& readyPeer = *search->nodes.front();
|
||||
auto reply = readyPeer.getStatus.at(broad);
|
||||
net::RequestAnswer answer {};
|
||||
answer.ntoken = Blob {1};
|
||||
Value value {(const uint8_t*) "a", 1};
|
||||
value.id = 42;
|
||||
answer.fields.emplace_back(std::make_shared<FieldValueIndex>(value, broad->select));
|
||||
|
||||
socketPtr->sends.clear();
|
||||
localDht.searchNodeGetDone(*reply, std::move(answer), search, broad);
|
||||
localDht.searchStep(search);
|
||||
|
||||
CPPUNIT_ASSERT_EQUAL(size_t {0}, filteredCallbacks);
|
||||
CPPUNIT_ASSERT(not socketPtr->sends.empty());
|
||||
CPPUNIT_ASSERT(readyPeer.getStatus.count(filtered) != 0);
|
||||
|
||||
auto filteredReply = std::make_shared<net::Request>(net::Request::State::COMPLETED);
|
||||
filteredReply->node = readyPeer.node;
|
||||
filteredReply->reply_time = now;
|
||||
readyPeer.getStatus[filtered] = filteredReply;
|
||||
net::RequestAnswer filteredAnswer {};
|
||||
filteredAnswer.ntoken = Blob {1};
|
||||
filteredAnswer.fields.emplace_back(std::make_shared<FieldValueIndex>(value, filtered->select));
|
||||
localDht.searchNodeGetDone(*filteredReply, std::move(filteredAnswer), search, filtered);
|
||||
|
||||
CPPUNIT_ASSERT_EQUAL(size_t {1}, filteredCallbacks);
|
||||
}
|
||||
|
||||
void
|
||||
NetworkEngineTester::testFindNodeRequestReuse()
|
||||
{
|
||||
Config config {};
|
||||
config.max_req_per_sec = -1;
|
||||
config.max_peer_req_per_sec = -1;
|
||||
Dht localDht(std::make_unique<TestDatagramSocket>(), config, {}, std::make_unique<std::mt19937_64>(44));
|
||||
const auto now = localDht.scheduler.time();
|
||||
auto search = std::make_shared<Dht::Search>();
|
||||
search->id = InfoHash::getRandom(localDht.rd);
|
||||
search->af = AF_INET;
|
||||
auto peer = std::make_shared<Node>(InfoHash::getRandom(localDht.rd),
|
||||
makeIPv4("127.0.0.2", 5005),
|
||||
localDht.rd,
|
||||
false);
|
||||
auto searchNode = std::make_unique<Dht::SearchNode>(peer);
|
||||
searchNode->last_get_reply = now;
|
||||
auto searchNodePtr = search->nodes.emplace_back(std::move(searchNode)).get();
|
||||
|
||||
CPPUNIT_ASSERT(localDht.searchSendGetValues(search) == searchNodePtr);
|
||||
CPPUNIT_ASSERT_EQUAL(size_t {1}, searchNodePtr->getStatus.size());
|
||||
CPPUNIT_ASSERT(searchNodePtr->getStatus.begin()->second->getType() == MessageType::FindNode);
|
||||
CPPUNIT_ASSERT(not localDht.searchSendGetValues(search));
|
||||
|
||||
auto reply = std::make_shared<net::Request>(net::Request::State::COMPLETED);
|
||||
reply->reply_time = now;
|
||||
searchNodePtr->getStatus.begin()->second = reply;
|
||||
|
||||
CPPUNIT_ASSERT(not localDht.searchSendGetValues(search));
|
||||
}
|
||||
|
||||
} // namespace test
|
||||
|
||||
#endif
|
||||
@@ -13,13 +13,20 @@ class NetworkEngineTester : public CppUnit::TestFixture
|
||||
#ifdef _MSC_VER
|
||||
CPPUNIT_TEST(testDisabledOnMsvc);
|
||||
#else
|
||||
CPPUNIT_TEST(testIgnoresUnknownPartialData);
|
||||
CPPUNIT_TEST(testCompletesPartialSessionWithDataBeforeHeader);
|
||||
CPPUNIT_TEST(testBuffersLargeValueBeforeHeader);
|
||||
CPPUNIT_TEST(testIgnoresEmptyPartialData);
|
||||
CPPUNIT_TEST(testRejectsUnsolicitedPartialReply);
|
||||
CPPUNIT_TEST(testDropsMalformedCompletedPartialMessage);
|
||||
CPPUNIT_TEST(testCompletesPartialSessionFromSameSource);
|
||||
CPPUNIT_TEST(testKeepsSessionForWrongSourceFragment);
|
||||
CPPUNIT_TEST(testSeparatesPartialSessionsBySource);
|
||||
CPPUNIT_TEST(testListenConfirmationCarriesToken);
|
||||
CPPUNIT_TEST(testListenConfirmationUpdatesSearchNodeToken);
|
||||
CPPUNIT_TEST(testListenReopensSocketAfterNodeExpiration);
|
||||
CPPUNIT_TEST(testUnauthorizedListenFlushClearsListenState);
|
||||
CPPUNIT_TEST(testGetRequestReuse);
|
||||
CPPUNIT_TEST(testFilteredProjectionDoesNotWaitForUnrelatedPeer);
|
||||
CPPUNIT_TEST(testFindNodeRequestReuse);
|
||||
#endif
|
||||
CPPUNIT_TEST_SUITE_END();
|
||||
|
||||
@@ -30,13 +37,20 @@ public:
|
||||
#ifdef _MSC_VER
|
||||
void testDisabledOnMsvc();
|
||||
#else
|
||||
void testIgnoresUnknownPartialData();
|
||||
void testCompletesPartialSessionWithDataBeforeHeader();
|
||||
void testBuffersLargeValueBeforeHeader();
|
||||
void testIgnoresEmptyPartialData();
|
||||
void testRejectsUnsolicitedPartialReply();
|
||||
void testDropsMalformedCompletedPartialMessage();
|
||||
void testCompletesPartialSessionFromSameSource();
|
||||
void testKeepsSessionForWrongSourceFragment();
|
||||
void testSeparatesPartialSessionsBySource();
|
||||
void testListenConfirmationCarriesToken();
|
||||
void testListenConfirmationUpdatesSearchNodeToken();
|
||||
void testListenReopensSocketAfterNodeExpiration();
|
||||
void testUnauthorizedListenFlushClearsListenState();
|
||||
void testGetRequestReuse();
|
||||
void testFilteredProjectionDoesNotWaitForUnrelatedPeer();
|
||||
void testFindNodeRequestReuse();
|
||||
#endif
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,723 @@
|
||||
// Copyright (c) 2014-2026 Savoir-faire Linux Inc.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "test_opcache.h"
|
||||
|
||||
// Hack to test internal class
|
||||
#include "../src/op_cache.cpp"
|
||||
|
||||
namespace test {
|
||||
CPPUNIT_TEST_SUITE_REGISTRATION(OpCacheTester);
|
||||
|
||||
namespace {
|
||||
|
||||
using Values = std::vector<std::shared_ptr<dht::Value>>;
|
||||
|
||||
struct Event
|
||||
{
|
||||
dht::Value::Id id;
|
||||
uint16_t seq;
|
||||
bool expired;
|
||||
};
|
||||
|
||||
std::shared_ptr<dht::Value>
|
||||
makeValue(dht::Value::Id id, uint16_t seq, std::string data)
|
||||
{
|
||||
auto v = std::make_shared<dht::Value>();
|
||||
v->id = id;
|
||||
v->seq = seq;
|
||||
v->data = {data.begin(), data.end()};
|
||||
return v;
|
||||
}
|
||||
|
||||
dht::ValueCallback
|
||||
recordEvents(std::vector<Event>& events)
|
||||
{
|
||||
return [&events](const Values& vals, bool expired) {
|
||||
for (const auto& v : vals)
|
||||
events.push_back({v->id, v->seq, expired});
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
unsigned
|
||||
count(const std::vector<Event>& events, bool expired)
|
||||
{
|
||||
unsigned n = 0;
|
||||
for (const auto& e : events)
|
||||
if (e.expired == expired)
|
||||
n++;
|
||||
return n;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void
|
||||
OpCacheTester::setUp()
|
||||
{}
|
||||
|
||||
void
|
||||
OpCacheTester::tearDown()
|
||||
{}
|
||||
|
||||
void
|
||||
OpCacheTester::testBasicAddExpire()
|
||||
{
|
||||
dht::OpValueCache cache([](const Values&, bool) { return true; });
|
||||
|
||||
auto v1 = makeValue(10, 0, "A");
|
||||
cache.onValuesAdded({v1});
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
|
||||
CPPUNIT_ASSERT(cache.get(10));
|
||||
|
||||
std::vector<dht::Value::Id> ids = {v1->id};
|
||||
cache.onValuesExpired(ids);
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
|
||||
CPPUNIT_ASSERT(!cache.get(10));
|
||||
}
|
||||
|
||||
void
|
||||
OpCacheTester::testMultipleSources()
|
||||
{
|
||||
// Two sources report the same value: it must survive until both have
|
||||
// expired it, and the user must see exactly one add and one expire.
|
||||
std::vector<Event> events;
|
||||
dht::OpValueCache cache(recordEvents(events));
|
||||
|
||||
auto v = makeValue(20, 0, "B");
|
||||
cache.onValuesAdded({v}); // source A
|
||||
cache.onValuesAdded({v}); // source B
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
|
||||
CPPUNIT_ASSERT_EQUAL(1u, count(events, false));
|
||||
|
||||
cache.onValuesExpired({v}); // source A goes away
|
||||
CPPUNIT_ASSERT_EQUAL_MESSAGE("Value still held by source B", (size_t) 1, cache.size());
|
||||
CPPUNIT_ASSERT_EQUAL(0u, count(events, true));
|
||||
|
||||
cache.onValuesExpired({v}); // source B goes away
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
|
||||
CPPUNIT_ASSERT_EQUAL(1u, count(events, true));
|
||||
}
|
||||
|
||||
void
|
||||
OpCacheTester::testEditResetsRefCount()
|
||||
{
|
||||
// A and B hold v1. A reports the edit v2: only A is known to hold v2, so
|
||||
// the refcount restarts at 1. B then confirms v2 (refcount 2).
|
||||
std::vector<Event> events;
|
||||
dht::OpValueCache cache(recordEvents(events));
|
||||
|
||||
auto v1 = makeValue(1, 1, "data1");
|
||||
auto v2 = makeValue(1, 2, "data2");
|
||||
|
||||
cache.onValuesAdded({v1}); // A
|
||||
cache.onValuesAdded({v1}); // B
|
||||
cache.onValuesAdded({v2}); // A edits
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
|
||||
CPPUNIT_ASSERT_EQUAL(v2->seq, cache.get(1)->seq);
|
||||
CPPUNIT_ASSERT_EQUAL(2u, count(events, false));
|
||||
|
||||
cache.onValuesExpired({v2}); // A expires v2 -> nobody known to hold it
|
||||
CPPUNIT_ASSERT_EQUAL_MESSAGE("Edit reset the refcount to the editing source", (size_t) 0, cache.size());
|
||||
CPPUNIT_ASSERT_EQUAL(1u, count(events, true));
|
||||
|
||||
// Same scenario, but B confirms v2 before A expires it.
|
||||
events.clear();
|
||||
cache.onValuesAdded({v1}); // A
|
||||
cache.onValuesAdded({v1}); // B
|
||||
cache.onValuesAdded({v2}); // A edits
|
||||
cache.onValuesAdded({v2}); // B confirms
|
||||
cache.onValuesExpired({v2});
|
||||
CPPUNIT_ASSERT_EQUAL_MESSAGE("Still held by B", (size_t) 1, cache.size());
|
||||
cache.onValuesExpired({v2});
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
|
||||
CPPUNIT_ASSERT_EQUAL(2u, count(events, false));
|
||||
CPPUNIT_ASSERT_EQUAL(1u, count(events, true));
|
||||
}
|
||||
|
||||
void
|
||||
OpCacheTester::testEditChainMultiSource()
|
||||
{
|
||||
// Rapid edit chain seen from two sources with interleaving: the user
|
||||
// must see each version once as an add and a single final expire.
|
||||
std::vector<Event> events;
|
||||
dht::OpValueCache cache(recordEvents(events));
|
||||
|
||||
auto v1 = makeValue(7, 1, "v1");
|
||||
auto v2 = makeValue(7, 2, "v2");
|
||||
auto v3 = makeValue(7, 3, "v3");
|
||||
|
||||
cache.onValuesAdded({v1}); // A
|
||||
cache.onValuesAdded({v1}); // B
|
||||
cache.onValuesAdded({v2}); // A
|
||||
cache.onValuesAdded({v3}); // A (B skipped v2)
|
||||
cache.onValuesAdded({v2}); // B, late and stale: ignored
|
||||
cache.onValuesAdded({v3}); // B
|
||||
CPPUNIT_ASSERT_EQUAL(3u, count(events, false));
|
||||
CPPUNIT_ASSERT_EQUAL((uint16_t) 3, cache.get(7)->seq);
|
||||
|
||||
cache.onValuesExpired({v3}); // A
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
|
||||
cache.onValuesExpired({v3}); // B
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
|
||||
CPPUNIT_ASSERT_EQUAL(1u, count(events, true));
|
||||
CPPUNIT_ASSERT_EQUAL((uint16_t) 3, events.back().seq);
|
||||
}
|
||||
|
||||
void
|
||||
OpCacheTester::testStaleAddIgnored()
|
||||
{
|
||||
dht::OpValueCache cache([](const Values&, bool) { return true; });
|
||||
|
||||
auto v1 = makeValue(30, 10, "C");
|
||||
auto v2 = makeValue(30, 20, "D");
|
||||
auto v3 = makeValue(30, 5, "E");
|
||||
|
||||
cache.onValuesAdded({v1});
|
||||
CPPUNIT_ASSERT_EQUAL((uint16_t) 10, cache.get(30)->seq);
|
||||
cache.onValuesAdded({v2});
|
||||
CPPUNIT_ASSERT_EQUAL((uint16_t) 20, cache.get(30)->seq);
|
||||
|
||||
// Older version from a lagging source: neither stored nor counted
|
||||
cache.onValuesAdded({v3});
|
||||
CPPUNIT_ASSERT_EQUAL((uint16_t) 20, cache.get(30)->seq);
|
||||
cache.onValuesExpired({v2});
|
||||
CPPUNIT_ASSERT_MESSAGE("Stale add must not have incremented the refcount", !cache.get(30));
|
||||
}
|
||||
|
||||
void
|
||||
OpCacheTester::testStaleExpireIgnored()
|
||||
{
|
||||
// A source that still holds an older version expiring it must not
|
||||
// affect the newer version held by others.
|
||||
std::vector<Event> events;
|
||||
dht::OpValueCache cache(recordEvents(events));
|
||||
|
||||
auto v1 = makeValue(31, 1, "old");
|
||||
auto v2 = makeValue(31, 2, "new");
|
||||
|
||||
cache.onValuesAdded({v1}); // A
|
||||
cache.onValuesAdded({v1}); // B
|
||||
cache.onValuesAdded({v2}); // A edits; B never receives v2
|
||||
|
||||
cache.onValuesExpired({v1}); // B expires its stale copy
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
|
||||
CPPUNIT_ASSERT_EQUAL(0u, count(events, true));
|
||||
|
||||
cache.onValuesExpired({v2}); // A
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
|
||||
CPPUNIT_ASSERT_EQUAL(1u, count(events, true));
|
||||
CPPUNIT_ASSERT_EQUAL((uint16_t) 2, events.back().seq);
|
||||
}
|
||||
|
||||
void
|
||||
OpCacheTester::testSameSeqConflictExpireIgnored()
|
||||
{
|
||||
// A different payload with the same seq (e.g. the same signed content
|
||||
// re-signed with a randomized signature) is ignored on add and not
|
||||
// counted: its expiration must be ignored too.
|
||||
std::vector<Event> events;
|
||||
dht::OpValueCache cache(recordEvents(events));
|
||||
|
||||
auto x = makeValue(60, 0, "x");
|
||||
auto y = makeValue(60, 0, "y");
|
||||
|
||||
cache.onValuesAdded({x}); // A
|
||||
cache.onValuesAdded({y}); // B: same seq, different content
|
||||
CPPUNIT_ASSERT_EQUAL(1u, count(events, false));
|
||||
CPPUNIT_ASSERT(cache.get(60)->data == x->data);
|
||||
|
||||
cache.onValuesExpired({y}); // B
|
||||
CPPUNIT_ASSERT_EQUAL_MESSAGE("An uncounted version must not expire the value", (size_t) 1, cache.size());
|
||||
CPPUNIT_ASSERT_EQUAL(0u, count(events, true));
|
||||
|
||||
cache.onValuesExpired({x}); // A
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
|
||||
CPPUNIT_ASSERT_EQUAL(1u, count(events, true));
|
||||
}
|
||||
|
||||
void
|
||||
OpCacheTester::testCallbacks()
|
||||
{
|
||||
std::vector<Event> events;
|
||||
dht::OpValueCache cache(recordEvents(events));
|
||||
|
||||
auto v1 = makeValue(40, 0, "");
|
||||
cache.onValuesAdded({v1});
|
||||
CPPUNIT_ASSERT_EQUAL(1u, count(events, false));
|
||||
|
||||
// Duplicate from another source: counted, but not re-delivered
|
||||
cache.onValuesAdded({v1});
|
||||
CPPUNIT_ASSERT_EQUAL(1u, count(events, false));
|
||||
|
||||
auto v2 = makeValue(40, 100, "MOD");
|
||||
cache.onValuesAdded({v2});
|
||||
CPPUNIT_ASSERT_EQUAL(2u, count(events, false));
|
||||
|
||||
// The edit reset the refcount: one expire removes it
|
||||
cache.onValuesExpired({v2});
|
||||
CPPUNIT_ASSERT_EQUAL(1u, count(events, true));
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
|
||||
}
|
||||
|
||||
void
|
||||
OpCacheTester::testFilters()
|
||||
{
|
||||
dht::OpValueCache cache([](const Values&, bool) { return true; });
|
||||
|
||||
auto v1 = makeValue(50, 0, "");
|
||||
v1->type = 1;
|
||||
auto v2 = makeValue(51, 0, "");
|
||||
v2->type = 2;
|
||||
cache.onValuesAdded({v1, v2});
|
||||
|
||||
auto res1 = cache.get(dht::Value::TypeFilter(1));
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 1, res1.size());
|
||||
CPPUNIT_ASSERT_EQUAL(v1->id, res1[0]->id);
|
||||
|
||||
auto res2 = cache.get(dht::Value::TypeFilter(2));
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 1, res2.size());
|
||||
CPPUNIT_ASSERT_EQUAL(v2->id, res2[0]->id);
|
||||
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 2, cache.getValues().size());
|
||||
}
|
||||
|
||||
void
|
||||
OpCacheTester::testSyncStatus()
|
||||
{
|
||||
dht::OpValueCache cache([](const Values&, bool) { return true; });
|
||||
|
||||
CPPUNIT_ASSERT(!cache.isSynced());
|
||||
cache.onNodeChanged(dht::ListenSyncStatus::ADDED);
|
||||
CPPUNIT_ASSERT(!cache.isSynced());
|
||||
cache.onNodeChanged(dht::ListenSyncStatus::SYNCED);
|
||||
CPPUNIT_ASSERT(cache.isSynced());
|
||||
cache.onNodeChanged(dht::ListenSyncStatus::ADDED);
|
||||
CPPUNIT_ASSERT(!cache.isSynced());
|
||||
cache.onNodeChanged(dht::ListenSyncStatus::SYNCED);
|
||||
CPPUNIT_ASSERT(cache.isSynced());
|
||||
cache.onNodeChanged(dht::ListenSyncStatus::UNSYNCED);
|
||||
CPPUNIT_ASSERT(!cache.isSynced());
|
||||
cache.onNodeChanged(dht::ListenSyncStatus::REMOVED);
|
||||
CPPUNIT_ASSERT(cache.isSynced());
|
||||
}
|
||||
|
||||
void
|
||||
OpCacheTester::testGetWhileSynced()
|
||||
{
|
||||
dht::SearchCache searchCache;
|
||||
int getCallCount = 0;
|
||||
bool doneCalled = false;
|
||||
bool doneSuccess = false;
|
||||
|
||||
auto v1 = makeValue(100, 0, "X");
|
||||
auto v2 = makeValue(101, 0, "Y");
|
||||
auto query = std::make_shared<dht::Query>();
|
||||
|
||||
auto token = searchCache.listen([](const Values&, bool) { return true; },
|
||||
query,
|
||||
{},
|
||||
[&](const std::shared_ptr<dht::Query>&,
|
||||
dht::ValueCallback vcb,
|
||||
dht::SyncCallback scb) -> size_t {
|
||||
scb(dht::ListenSyncStatus::ADDED);
|
||||
vcb({v1, v2}, false);
|
||||
scb(dht::ListenSyncStatus::SYNCED);
|
||||
return 1;
|
||||
});
|
||||
CPPUNIT_ASSERT(token != 0);
|
||||
|
||||
bool result = searchCache.get(
|
||||
{},
|
||||
query,
|
||||
[&](const Values& vals) {
|
||||
getCallCount++;
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 2, vals.size());
|
||||
return true;
|
||||
},
|
||||
[&](bool success, const std::vector<std::shared_ptr<dht::Node>>&) {
|
||||
doneCalled = true;
|
||||
doneSuccess = success;
|
||||
});
|
||||
|
||||
CPPUNIT_ASSERT_MESSAGE("get() should be served from a synced cache", result);
|
||||
CPPUNIT_ASSERT_EQUAL(1, getCallCount);
|
||||
CPPUNIT_ASSERT(doneCalled);
|
||||
CPPUNIT_ASSERT(doneSuccess);
|
||||
}
|
||||
|
||||
void
|
||||
OpCacheTester::testGetWhileNotSynced()
|
||||
{
|
||||
// An unsynced cache must not answer a get: the caller falls back to the
|
||||
// network and values are delivered once, from the network only.
|
||||
dht::SearchCache searchCache;
|
||||
std::vector<dht::Value::Id> deliveries;
|
||||
|
||||
auto v1 = makeValue(200, 0, "A");
|
||||
auto query = std::make_shared<dht::Query>();
|
||||
dht::SyncCallback storedScb;
|
||||
|
||||
auto token = searchCache.listen([](const Values&, bool) { return true; },
|
||||
query,
|
||||
{},
|
||||
[&](const std::shared_ptr<dht::Query>&,
|
||||
dht::ValueCallback vcb,
|
||||
dht::SyncCallback scb) -> size_t {
|
||||
storedScb = scb;
|
||||
scb(dht::ListenSyncStatus::ADDED);
|
||||
vcb({v1}, false);
|
||||
return 1;
|
||||
});
|
||||
CPPUNIT_ASSERT(token != 0);
|
||||
|
||||
bool doneCalled = false;
|
||||
auto gcb = [&](const Values& vals) {
|
||||
for (const auto& v : vals)
|
||||
deliveries.emplace_back(v->id);
|
||||
return true;
|
||||
};
|
||||
auto dcb = [&](bool, const std::vector<std::shared_ptr<dht::Node>>&) {
|
||||
doneCalled = true;
|
||||
};
|
||||
|
||||
CPPUNIT_ASSERT(!searchCache.get({}, query, gcb, dcb));
|
||||
CPPUNIT_ASSERT(deliveries.empty());
|
||||
CPPUNIT_ASSERT(!doneCalled);
|
||||
|
||||
// Simulated network reply
|
||||
gcb({v1});
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 1, deliveries.size());
|
||||
|
||||
storedScb(dht::ListenSyncStatus::SYNCED);
|
||||
deliveries.clear();
|
||||
CPPUNIT_ASSERT(searchCache.get({}, query, gcb, dcb));
|
||||
CPPUNIT_ASSERT(doneCalled);
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 1, deliveries.size());
|
||||
}
|
||||
|
||||
void
|
||||
OpCacheTester::testGetEmptySynced()
|
||||
{
|
||||
dht::SearchCache searchCache;
|
||||
int getCallCount = 0;
|
||||
bool doneCalled = false;
|
||||
bool doneSuccess = false;
|
||||
auto query = std::make_shared<dht::Query>();
|
||||
|
||||
auto token = searchCache.listen([](const Values&, bool) { return true; },
|
||||
query,
|
||||
{},
|
||||
[&](const std::shared_ptr<dht::Query>&,
|
||||
dht::ValueCallback,
|
||||
dht::SyncCallback scb) -> size_t {
|
||||
scb(dht::ListenSyncStatus::ADDED);
|
||||
scb(dht::ListenSyncStatus::SYNCED);
|
||||
return 1;
|
||||
});
|
||||
CPPUNIT_ASSERT(token != 0);
|
||||
|
||||
bool result = searchCache.get(
|
||||
{},
|
||||
query,
|
||||
[&](const Values&) {
|
||||
getCallCount++;
|
||||
return true;
|
||||
},
|
||||
[&](bool success, const std::vector<std::shared_ptr<dht::Node>>&) {
|
||||
doneCalled = true;
|
||||
doneSuccess = success;
|
||||
});
|
||||
|
||||
CPPUNIT_ASSERT(result);
|
||||
CPPUNIT_ASSERT_EQUAL(0, getCallCount);
|
||||
CPPUNIT_ASSERT(doneCalled);
|
||||
CPPUNIT_ASSERT(doneSuccess);
|
||||
}
|
||||
|
||||
void
|
||||
OpCacheTester::testGetWithoutCallback()
|
||||
{
|
||||
// Dht::query() passes no GetCallback: the cache cannot answer it and
|
||||
// must let the network path run instead of calling an empty function.
|
||||
dht::SearchCache searchCache;
|
||||
auto v1 = makeValue(300, 0, "Q");
|
||||
auto query = std::make_shared<dht::Query>();
|
||||
|
||||
searchCache.listen([](const Values&, bool) { return true; },
|
||||
query,
|
||||
{},
|
||||
[&](const std::shared_ptr<dht::Query>&, dht::ValueCallback vcb, dht::SyncCallback scb) -> size_t {
|
||||
scb(dht::ListenSyncStatus::ADDED);
|
||||
vcb({v1}, false);
|
||||
scb(dht::ListenSyncStatus::SYNCED);
|
||||
return 1;
|
||||
});
|
||||
|
||||
bool doneCalled = false;
|
||||
CPPUNIT_ASSERT(!searchCache.get({}, query, {}, [&](bool, const std::vector<std::shared_ptr<dht::Node>>&) {
|
||||
doneCalled = true;
|
||||
}));
|
||||
CPPUNIT_ASSERT(!doneCalled);
|
||||
}
|
||||
|
||||
void
|
||||
OpCacheTester::testGetCallbackReturnsFalse()
|
||||
{
|
||||
// A consumer returning false means "I have enough", not a failure.
|
||||
dht::SearchCache searchCache;
|
||||
auto v1 = makeValue(1000, 0, "X");
|
||||
auto query = std::make_shared<dht::Query>();
|
||||
|
||||
searchCache.listen([](const Values&, bool) { return true; },
|
||||
query,
|
||||
{},
|
||||
[&](const std::shared_ptr<dht::Query>&, dht::ValueCallback vcb, dht::SyncCallback scb) -> size_t {
|
||||
scb(dht::ListenSyncStatus::ADDED);
|
||||
vcb({v1}, false);
|
||||
scb(dht::ListenSyncStatus::SYNCED);
|
||||
return 1;
|
||||
});
|
||||
|
||||
bool doneCalled = false;
|
||||
bool doneSuccess = false;
|
||||
int gcbCallCount = 0;
|
||||
bool result = searchCache.get(
|
||||
{},
|
||||
query,
|
||||
[&](const Values&) {
|
||||
gcbCallCount++;
|
||||
return false;
|
||||
},
|
||||
[&](bool success, const std::vector<std::shared_ptr<dht::Node>>&) {
|
||||
doneCalled = true;
|
||||
doneSuccess = success;
|
||||
});
|
||||
|
||||
CPPUNIT_ASSERT(result);
|
||||
CPPUNIT_ASSERT_EQUAL(1, gcbCallCount);
|
||||
CPPUNIT_ASSERT(doneCalled);
|
||||
CPPUNIT_ASSERT_MESSAGE("Stopping early is reported as success, like the network path", doneSuccess);
|
||||
}
|
||||
|
||||
void
|
||||
OpCacheTester::testTimestampOrdering()
|
||||
{
|
||||
dht::OpValueCache cache([](const Values&, bool) { return true; });
|
||||
|
||||
auto v1 = makeValue(400, 0, "T");
|
||||
auto t1 = std::chrono::system_clock::now();
|
||||
auto t2 = t1 + std::chrono::seconds(10);
|
||||
|
||||
cache.onValue({v1}, false, t2);
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
|
||||
|
||||
cache.onValue({v1}, true, t1);
|
||||
CPPUNIT_ASSERT_EQUAL_MESSAGE("Stale expiration should be ignored", (size_t) 1, cache.size());
|
||||
|
||||
cache.onValue({v1}, true, t2);
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
|
||||
}
|
||||
|
||||
void
|
||||
OpCacheTester::testSourceDropKeepsValue()
|
||||
{
|
||||
// Two per-node ValueCaches feed one Search-level OpCache. When one node
|
||||
// leaves the search (its ValueCache is destroyed, expiring everything it
|
||||
// held) the value must stay visible as long as the other node holds it.
|
||||
std::vector<Event> events;
|
||||
dht::SearchCache searchCache;
|
||||
auto query = std::make_shared<dht::Query>();
|
||||
dht::ValueCallback toOpCache;
|
||||
dht::SyncCallback toOpCacheSync;
|
||||
|
||||
searchCache.listen(recordEvents(events),
|
||||
query,
|
||||
{},
|
||||
[&](const std::shared_ptr<dht::Query>&, dht::ValueCallback vcb, dht::SyncCallback scb) -> size_t {
|
||||
toOpCache = std::move(vcb);
|
||||
toOpCacheSync = std::move(scb);
|
||||
return 1;
|
||||
});
|
||||
|
||||
dht::TypeStore types;
|
||||
auto now = dht::clock::now();
|
||||
auto v = makeValue(42, 0, "hello");
|
||||
|
||||
auto makeNodeCache = [&] {
|
||||
return std::make_unique<dht::ValueCache>([&](const Values& vals, bool expired) { toOpCache(vals, expired); },
|
||||
[&](dht::ListenSyncStatus s) { toOpCacheSync(s); });
|
||||
};
|
||||
auto nodeA = makeNodeCache();
|
||||
auto nodeB = makeNodeCache();
|
||||
nodeA->onValues({v}, {}, {}, types, now);
|
||||
nodeA->onSynced(true);
|
||||
nodeB->onValues({v}, {}, {}, types, now);
|
||||
nodeB->onSynced(true);
|
||||
CPPUNIT_ASSERT_EQUAL(1u, count(events, false));
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 1, searchCache.get(dht::Value::Filter {}).size());
|
||||
|
||||
nodeA.reset();
|
||||
CPPUNIT_ASSERT_EQUAL_MESSAGE("No expire while another node holds the value", 0u, count(events, true));
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 1, searchCache.get(dht::Value::Filter {}).size());
|
||||
|
||||
nodeB->onValues({v}, {}, {}, types, now + std::chrono::seconds(1));
|
||||
CPPUNIT_ASSERT_EQUAL_MESSAGE("Refresh from the remaining node is not re-delivered", 1u, count(events, false));
|
||||
|
||||
nodeB.reset();
|
||||
CPPUNIT_ASSERT_EQUAL_MESSAGE("Last holder gone: expire once", 1u, count(events, true));
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 0, searchCache.get(dht::Value::Filter {}).size());
|
||||
}
|
||||
|
||||
void
|
||||
OpCacheTester::testSeqOnlyRefreshNotCounted()
|
||||
{
|
||||
// A node re-announcing the same content with a bumped seq must not be
|
||||
// counted as an additional source by the OpValueCache above it, or the
|
||||
// value would survive the node's departure.
|
||||
std::vector<Event> events;
|
||||
dht::OpValueCache parent(recordEvents(events));
|
||||
dht::TypeStore types;
|
||||
auto now = dht::clock::now();
|
||||
|
||||
auto v = makeValue(42, 0, "same");
|
||||
auto vBumped = makeValue(42, 1, "same");
|
||||
{
|
||||
dht::ValueCache node([&](const Values& vals, bool expired) { parent.onValue(vals, expired); });
|
||||
node.onValues({v}, {}, {}, types, now);
|
||||
node.onValues({vBumped}, {}, {}, types, now);
|
||||
CPPUNIT_ASSERT_EQUAL(1u, count(events, false));
|
||||
}
|
||||
CPPUNIT_ASSERT_EQUAL_MESSAGE("Node gone: its value must be expired once", 1u, count(events, true));
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 0, parent.size());
|
||||
}
|
||||
|
||||
void
|
||||
OpCacheTester::testHighChurnRefCountConsistency()
|
||||
{
|
||||
dht::OpValueCache cache([](const Values&, bool) { return true; });
|
||||
auto v = makeValue(800, 0, "CHURN");
|
||||
|
||||
for (int i = 0; i < 50; i++) {
|
||||
cache.onNodeChanged(dht::ListenSyncStatus::ADDED);
|
||||
cache.onValuesAdded({v});
|
||||
cache.onValuesExpired({v});
|
||||
cache.onNodeChanged(dht::ListenSyncStatus::REMOVED);
|
||||
}
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
|
||||
|
||||
// Overlapping churn: add A, add B, expire A, expire B
|
||||
for (int i = 0; i < 20; i++) {
|
||||
cache.onNodeChanged(dht::ListenSyncStatus::ADDED);
|
||||
cache.onValuesAdded({v});
|
||||
cache.onNodeChanged(dht::ListenSyncStatus::ADDED);
|
||||
cache.onValuesAdded({v});
|
||||
|
||||
cache.onValuesExpired({v});
|
||||
cache.onNodeChanged(dht::ListenSyncStatus::REMOVED);
|
||||
CPPUNIT_ASSERT_EQUAL_MESSAGE("Value kept while one source remains", (size_t) 1, cache.size());
|
||||
|
||||
cache.onValuesExpired({v});
|
||||
cache.onNodeChanged(dht::ListenSyncStatus::REMOVED);
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
|
||||
}
|
||||
|
||||
// Extra expirations from sources that never added must be no-ops
|
||||
cache.onValuesExpired({v});
|
||||
cache.onValuesExpired({v});
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
|
||||
}
|
||||
|
||||
void
|
||||
OpCacheTester::testValueUpdateSingleSourcePhantom()
|
||||
{
|
||||
// Editing a value (same id, higher seq) is delivered as a single add of
|
||||
// the new version, without a phantom expiration of the old one.
|
||||
std::vector<Event> events;
|
||||
dht::OpValueCache cache(recordEvents(events));
|
||||
|
||||
auto v1 = makeValue(700, 1, "OLD");
|
||||
auto v2 = makeValue(700, 2, "NEW");
|
||||
|
||||
cache.onValuesAdded({v1});
|
||||
events.clear();
|
||||
cache.onValuesAdded({v2});
|
||||
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 1, events.size());
|
||||
CPPUNIT_ASSERT(!events[0].expired);
|
||||
CPPUNIT_ASSERT_EQUAL(v2->id, events[0].id);
|
||||
CPPUNIT_ASSERT_EQUAL(v2->seq, cache.get(700)->seq);
|
||||
}
|
||||
|
||||
void
|
||||
OpCacheTester::testExpireAfterUpdate()
|
||||
{
|
||||
std::vector<Event> events;
|
||||
dht::OpValueCache cache(recordEvents(events));
|
||||
|
||||
auto v1 = makeValue(1200, 1, "V1");
|
||||
auto v2 = makeValue(1200, 5, "V2");
|
||||
|
||||
cache.onValuesAdded({v1});
|
||||
cache.onValuesAdded({v2});
|
||||
CPPUNIT_ASSERT_EQUAL(v2->seq, cache.get(1200)->seq);
|
||||
|
||||
cache.onValuesExpired({v2});
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
|
||||
CPPUNIT_ASSERT_EQUAL(1u, count(events, true));
|
||||
CPPUNIT_ASSERT_EQUAL_MESSAGE("Expired value should be the updated one", v2->seq, events.back().seq);
|
||||
|
||||
// Once gone, an older version is accepted again
|
||||
cache.onValuesAdded({v1});
|
||||
CPPUNIT_ASSERT_EQUAL(v1->seq, cache.get(1200)->seq);
|
||||
}
|
||||
|
||||
void
|
||||
OpCacheTester::testSingleSourceMode()
|
||||
{
|
||||
// Single-source caches (proxy client) get the same values re-delivered
|
||||
// on refresh: duplicates must not be counted, one expire removes.
|
||||
std::vector<Event> events;
|
||||
dht::OpValueCache cache(recordEvents(events), false);
|
||||
auto v = makeValue(1100, 0, "DUP");
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
cache.onValuesAdded({v});
|
||||
CPPUNIT_ASSERT_EQUAL(1u, count(events, false));
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
|
||||
|
||||
cache.onValuesExpired({v});
|
||||
CPPUNIT_ASSERT_EQUAL(1u, count(events, true));
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
|
||||
|
||||
cache.onValuesExpired({v});
|
||||
CPPUNIT_ASSERT_EQUAL(1u, count(events, true));
|
||||
}
|
||||
|
||||
void
|
||||
OpCacheTester::testSingleSourceModeSearchCache()
|
||||
{
|
||||
std::vector<Event> events;
|
||||
dht::SearchCache searchCache(false);
|
||||
auto query = std::make_shared<dht::Query>();
|
||||
dht::ValueCallback vcbStored;
|
||||
|
||||
searchCache.listen(recordEvents(events),
|
||||
query,
|
||||
{},
|
||||
[&](const std::shared_ptr<dht::Query>&, dht::ValueCallback vcb, dht::SyncCallback) -> size_t {
|
||||
vcbStored = std::move(vcb);
|
||||
return 1;
|
||||
});
|
||||
|
||||
auto v = makeValue(1300, 0, "S");
|
||||
vcbStored({v}, false);
|
||||
vcbStored({v}, false); // re-delivery after listen restart
|
||||
CPPUNIT_ASSERT_EQUAL(1u, count(events, false));
|
||||
|
||||
vcbStored({v}, true);
|
||||
CPPUNIT_ASSERT_EQUAL(1u, count(events, true));
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 0, searchCache.get(dht::Value::Filter {}).size());
|
||||
}
|
||||
|
||||
} // namespace test
|
||||
@@ -0,0 +1,67 @@
|
||||
// Copyright (c) 2014-2026 Savoir-faire Linux Inc.
|
||||
// SPDX-License-Identifier: MIT
|
||||
#pragma once
|
||||
|
||||
#include <cppunit/TestFixture.h>
|
||||
#include <cppunit/extensions/HelperMacros.h>
|
||||
|
||||
namespace test {
|
||||
|
||||
class OpCacheTester : public CppUnit::TestFixture
|
||||
{
|
||||
CPPUNIT_TEST_SUITE(OpCacheTester);
|
||||
CPPUNIT_TEST(testBasicAddExpire);
|
||||
CPPUNIT_TEST(testMultipleSources);
|
||||
CPPUNIT_TEST(testEditResetsRefCount);
|
||||
CPPUNIT_TEST(testEditChainMultiSource);
|
||||
CPPUNIT_TEST(testStaleAddIgnored);
|
||||
CPPUNIT_TEST(testStaleExpireIgnored);
|
||||
CPPUNIT_TEST(testSameSeqConflictExpireIgnored);
|
||||
CPPUNIT_TEST(testCallbacks);
|
||||
CPPUNIT_TEST(testFilters);
|
||||
CPPUNIT_TEST(testSyncStatus);
|
||||
CPPUNIT_TEST(testGetWhileSynced);
|
||||
CPPUNIT_TEST(testGetWhileNotSynced);
|
||||
CPPUNIT_TEST(testGetEmptySynced);
|
||||
CPPUNIT_TEST(testGetWithoutCallback);
|
||||
CPPUNIT_TEST(testGetCallbackReturnsFalse);
|
||||
CPPUNIT_TEST(testTimestampOrdering);
|
||||
CPPUNIT_TEST(testSourceDropKeepsValue);
|
||||
CPPUNIT_TEST(testSeqOnlyRefreshNotCounted);
|
||||
CPPUNIT_TEST(testHighChurnRefCountConsistency);
|
||||
CPPUNIT_TEST(testValueUpdateSingleSourcePhantom);
|
||||
CPPUNIT_TEST(testExpireAfterUpdate);
|
||||
CPPUNIT_TEST(testSingleSourceMode);
|
||||
CPPUNIT_TEST(testSingleSourceModeSearchCache);
|
||||
CPPUNIT_TEST_SUITE_END();
|
||||
|
||||
public:
|
||||
void setUp();
|
||||
void tearDown();
|
||||
|
||||
void testBasicAddExpire();
|
||||
void testMultipleSources();
|
||||
void testEditResetsRefCount();
|
||||
void testEditChainMultiSource();
|
||||
void testStaleAddIgnored();
|
||||
void testStaleExpireIgnored();
|
||||
void testSameSeqConflictExpireIgnored();
|
||||
void testCallbacks();
|
||||
void testFilters();
|
||||
void testSyncStatus();
|
||||
void testGetWhileSynced();
|
||||
void testGetWhileNotSynced();
|
||||
void testGetEmptySynced();
|
||||
void testGetWithoutCallback();
|
||||
void testGetCallbackReturnsFalse();
|
||||
void testTimestampOrdering();
|
||||
void testSourceDropKeepsValue();
|
||||
void testSeqOnlyRefreshNotCounted();
|
||||
void testHighChurnRefCountConsistency();
|
||||
void testValueUpdateSingleSourcePhantom();
|
||||
void testExpireAfterUpdate();
|
||||
void testSingleSourceMode();
|
||||
void testSingleSourceModeSearchCache();
|
||||
};
|
||||
|
||||
} // namespace test
|
||||
@@ -328,6 +328,104 @@ ParsedMessageTester::testParseRejectsInvalidValuesField()
|
||||
CPPUNIT_ASSERT_THROW(parsed.msgpack_unpack(msg.get()), msgpack::type_error);
|
||||
}
|
||||
|
||||
void
|
||||
ParsedMessageTester::testParseRejectsEmptyFieldSelection()
|
||||
{
|
||||
msgpack::sbuffer buffer;
|
||||
msgpack::packer<msgpack::sbuffer> pk(&buffer);
|
||||
|
||||
pk.pack_map(3);
|
||||
pk.pack(KEY_R);
|
||||
pk.pack_map(1);
|
||||
pk.pack(KEY_REQ_FIELDS);
|
||||
pk.pack_map(2);
|
||||
pk.pack("f");
|
||||
pk.pack_array(0);
|
||||
pk.pack("v");
|
||||
pk.pack_array(0);
|
||||
pk.pack(KEY_TID);
|
||||
pk.pack(1u);
|
||||
pk.pack(KEY_Y);
|
||||
pk.pack(KEY_R);
|
||||
|
||||
auto msg = msgpack::unpack(buffer.data(), buffer.size());
|
||||
ParsedMessage parsed;
|
||||
CPPUNIT_ASSERT_THROW(parsed.msgpack_unpack(msg.get()), msgpack::type_error);
|
||||
}
|
||||
|
||||
void
|
||||
ParsedMessageTester::testParseRejectsEmptyErrorArray()
|
||||
{
|
||||
msgpack::sbuffer buffer;
|
||||
msgpack::packer<msgpack::sbuffer> pk(&buffer);
|
||||
|
||||
pk.pack_map(3);
|
||||
pk.pack(KEY_E);
|
||||
pk.pack_array(0);
|
||||
pk.pack(KEY_TID);
|
||||
pk.pack(1u);
|
||||
pk.pack(KEY_Y);
|
||||
pk.pack(KEY_E);
|
||||
|
||||
auto msg = msgpack::unpack(buffer.data(), buffer.size());
|
||||
ParsedMessage parsed;
|
||||
CPPUNIT_ASSERT_THROW(parsed.msgpack_unpack(msg.get()), msgpack::type_error);
|
||||
}
|
||||
|
||||
void
|
||||
ParsedMessageTester::testParseRejectsNonMapRequest()
|
||||
{
|
||||
msgpack::sbuffer buffer;
|
||||
msgpack::packer<msgpack::sbuffer> pk(&buffer);
|
||||
|
||||
pk.pack_map(4);
|
||||
pk.pack(KEY_A);
|
||||
pk.pack_array(2);
|
||||
pk.pack(1u);
|
||||
pk.pack(2u);
|
||||
pk.pack(KEY_Q);
|
||||
pk.pack(QUERY_PING);
|
||||
pk.pack(KEY_TID);
|
||||
pk.pack(1u);
|
||||
pk.pack(KEY_Y);
|
||||
pk.pack("q");
|
||||
|
||||
auto msg = msgpack::unpack(buffer.data(), buffer.size());
|
||||
ParsedMessage parsed;
|
||||
CPPUNIT_ASSERT_THROW(parsed.msgpack_unpack(msg.get()), msgpack::type_error);
|
||||
}
|
||||
|
||||
void
|
||||
ParsedMessageTester::testParseErrorOnlyMessageKeepsRequestFieldsEmpty()
|
||||
{
|
||||
msgpack::sbuffer buffer;
|
||||
msgpack::packer<msgpack::sbuffer> pk(&buffer);
|
||||
|
||||
// An error message with no request map: the error array must not be walked
|
||||
// as if it were a sequence of key/value pairs.
|
||||
pk.pack_map(3);
|
||||
pk.pack(KEY_E);
|
||||
pk.pack_array(2);
|
||||
pk.pack(401u);
|
||||
pk.pack("unauthorized");
|
||||
pk.pack(KEY_TID);
|
||||
pk.pack(9u);
|
||||
pk.pack(KEY_Y);
|
||||
pk.pack(KEY_E);
|
||||
|
||||
auto msg = msgpack::unpack(buffer.data(), buffer.size());
|
||||
ParsedMessage parsed;
|
||||
parsed.msgpack_unpack(msg.get());
|
||||
|
||||
CPPUNIT_ASSERT(parsed.type == MessageType::Error);
|
||||
CPPUNIT_ASSERT_EQUAL((uint16_t) 401, parsed.error_code);
|
||||
CPPUNIT_ASSERT(not parsed.id);
|
||||
CPPUNIT_ASSERT(parsed.token.empty());
|
||||
CPPUNIT_ASSERT(parsed.values.empty());
|
||||
CPPUNIT_ASSERT(parsed.nodes4_raw.empty());
|
||||
CPPUNIT_ASSERT(parsed.nodes6_raw.empty());
|
||||
}
|
||||
|
||||
void
|
||||
ParsedMessageTester::testParseIgnoresIncompleteValueDataEntry()
|
||||
{
|
||||
|
||||
@@ -24,6 +24,10 @@ class ParsedMessageTester : public CppUnit::TestFixture
|
||||
CPPUNIT_TEST(testParseRejectsNonMapPacket);
|
||||
CPPUNIT_TEST(testParseRejectsInvalidValueDataPayload);
|
||||
CPPUNIT_TEST(testParseRejectsInvalidValuesField);
|
||||
CPPUNIT_TEST(testParseRejectsEmptyFieldSelection);
|
||||
CPPUNIT_TEST(testParseRejectsEmptyErrorArray);
|
||||
CPPUNIT_TEST(testParseRejectsNonMapRequest);
|
||||
CPPUNIT_TEST(testParseErrorOnlyMessageKeepsRequestFieldsEmpty);
|
||||
CPPUNIT_TEST(testParseIgnoresIncompleteValueDataEntry);
|
||||
CPPUNIT_TEST(testCompleteEmpty);
|
||||
CPPUNIT_TEST(testCompleteIncomplete);
|
||||
@@ -53,6 +57,10 @@ public:
|
||||
void testParseRejectsNonMapPacket();
|
||||
void testParseRejectsInvalidValueDataPayload();
|
||||
void testParseRejectsInvalidValuesField();
|
||||
void testParseRejectsEmptyFieldSelection();
|
||||
void testParseRejectsEmptyErrorArray();
|
||||
void testParseRejectsNonMapRequest();
|
||||
void testParseErrorOnlyMessageKeepsRequestFieldsEmpty();
|
||||
void testParseIgnoresIncompleteValueDataEntry();
|
||||
void testCompleteEmpty();
|
||||
void testCompleteIncomplete();
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
// Copyright (c) 2014-2026 Savoir-faire Linux Inc.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "test_pht.h"
|
||||
|
||||
// opendht
|
||||
#include "opendht/indexation/pht.h"
|
||||
|
||||
namespace test {
|
||||
CPPUNIT_TEST_SUITE_REGISTRATION(PhtTester);
|
||||
|
||||
using dht::indexation::Prefix;
|
||||
|
||||
void
|
||||
PhtTester::testCommonBits()
|
||||
{
|
||||
// Prefix::size_ counts bits, so two prefixes that agree everywhere have
|
||||
// as many bits in common as they are long, and no more.
|
||||
const Prefix same {dht::Blob(20, 0xAB)};
|
||||
CPPUNIT_ASSERT_EQUAL((unsigned) 160, Prefix::commonBits(same, Prefix {dht::Blob(20, 0xAB)}));
|
||||
|
||||
// A difference in the very last bit leaves every other bit in common.
|
||||
dht::Blob other(20, 0xAB);
|
||||
other[19] = 0xAA;
|
||||
CPPUNIT_ASSERT_EQUAL((unsigned) 159, Prefix::commonBits(same, Prefix {other}));
|
||||
|
||||
// A difference in the very first bit leaves nothing in common.
|
||||
dht::Blob first(20, 0xAB);
|
||||
first[0] = 0x2B;
|
||||
CPPUNIT_ASSERT_EQUAL((unsigned) 0, Prefix::commonBits(same, Prefix {first}));
|
||||
|
||||
// Bits beyond the shorter prefix are not compared, whatever the content
|
||||
// they are cut from holds.
|
||||
const Prefix shorter = same.getPrefix(3);
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 3, shorter.size_);
|
||||
CPPUNIT_ASSERT_EQUAL((unsigned) 3, Prefix::commonBits(shorter, same));
|
||||
CPPUNIT_ASSERT_EQUAL((unsigned) 3, Prefix::commonBits(same, shorter));
|
||||
}
|
||||
|
||||
void
|
||||
PhtTester::testSibling()
|
||||
{
|
||||
// The sibling of a node in the trie is the prefix that differs from it in
|
||||
// its last bit, and in that bit only.
|
||||
for (size_t length : {1, 7, 8, 9, 15, 16, 159}) {
|
||||
const Prefix prefix = Prefix {dht::Blob(20, 0xAB)}.getPrefix(length);
|
||||
const Prefix sibling = prefix.getSibling();
|
||||
|
||||
CPPUNIT_ASSERT_EQUAL(prefix.size_, sibling.size_);
|
||||
CPPUNIT_ASSERT_EQUAL((unsigned) (length - 1), Prefix::commonBits(prefix, sibling));
|
||||
CPPUNIT_ASSERT(prefix.isContentBitActive(length - 1) != sibling.isContentBitActive(length - 1));
|
||||
|
||||
// Being siblings is symmetric.
|
||||
CPPUNIT_ASSERT(sibling.getSibling().content_ == prefix.content_);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
PhtTester::testPadding()
|
||||
{
|
||||
for (size_t keySize : {0, 1, 2, 20}) {
|
||||
for (size_t paddingSize : {1, 4}) {
|
||||
dht::Blob expected(keySize, 0xAB);
|
||||
Prefix prefix {expected};
|
||||
expected.resize(keySize + paddingSize, 0);
|
||||
expected[keySize] = 0x80;
|
||||
|
||||
prefix.addPaddingContent(expected.size());
|
||||
|
||||
CPPUNIT_ASSERT(prefix.content_ == expected);
|
||||
CPPUNIT_ASSERT_EQUAL(keySize * 8, prefix.size_);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace test
|
||||
@@ -0,0 +1,31 @@
|
||||
// Copyright (c) 2014-2026 Savoir-faire Linux Inc.
|
||||
// SPDX-License-Identifier: MIT
|
||||
#pragma once
|
||||
|
||||
// cppunit
|
||||
#include <cppunit/TestFixture.h>
|
||||
#include <cppunit/extensions/HelperMacros.h>
|
||||
|
||||
namespace test {
|
||||
|
||||
class PhtTester : public CppUnit::TestFixture
|
||||
{
|
||||
CPPUNIT_TEST_SUITE(PhtTester);
|
||||
CPPUNIT_TEST(testCommonBits);
|
||||
CPPUNIT_TEST(testSibling);
|
||||
CPPUNIT_TEST(testPadding);
|
||||
CPPUNIT_TEST_SUITE_END();
|
||||
|
||||
public:
|
||||
/**
|
||||
* Test how many bits two prefixes have in common
|
||||
*/
|
||||
void testCommonBits();
|
||||
/**
|
||||
* Test the prefix of the sibling node in the trie
|
||||
*/
|
||||
void testSibling();
|
||||
void testPadding();
|
||||
};
|
||||
|
||||
} // namespace test
|
||||
@@ -58,10 +58,87 @@ ValueTester::testFilter()
|
||||
CPPUNIT_ASSERT(isBoth(value3));
|
||||
}
|
||||
|
||||
void
|
||||
ValueTester::testFieldValueIndexContainedIn()
|
||||
{
|
||||
dht::Value first {(const uint8_t*) "a", 1};
|
||||
first.id = 1;
|
||||
dht::Value second {(const uint8_t*) "b", 1};
|
||||
second.id = 2;
|
||||
|
||||
const dht::Select select {dht::Select {}.field(dht::Value::Field::Id)};
|
||||
dht::FieldValueIndex first_index {first, select};
|
||||
dht::FieldValueIndex second_index {second, select};
|
||||
|
||||
// Same projected field, different values: neither index contains the other.
|
||||
CPPUNIT_ASSERT(not first_index.containedIn(second_index));
|
||||
CPPUNIT_ASSERT(not second_index.containedIn(first_index));
|
||||
|
||||
// An index is always contained in an equal one.
|
||||
dht::FieldValueIndex first_copy {first, select};
|
||||
CPPUNIT_ASSERT(first_index.containedIn(first_copy));
|
||||
CPPUNIT_ASSERT(first_copy.containedIn(first_index));
|
||||
|
||||
// A narrower projection with a matching value stays contained in a wider one.
|
||||
const dht::Select wider {dht::Select {}.field(dht::Value::Field::Id).field(dht::Value::Field::ValueType)};
|
||||
dht::FieldValueIndex first_wide {first, wider};
|
||||
CPPUNIT_ASSERT(first_index.containedIn(first_wide));
|
||||
CPPUNIT_ASSERT(not second_index.containedIn(first_wide));
|
||||
}
|
||||
|
||||
void
|
||||
ValueTester::testProjectionSatisfiedBy()
|
||||
{
|
||||
const dht::Query broad {dht::Select {}.field(dht::Value::Field::Id)};
|
||||
const dht::Query filtered {dht::Select {}.field(dht::Value::Field::Id),
|
||||
dht::Where {}.valueType(1)};
|
||||
|
||||
// The unfiltered answer contains every value the filtered query asks for,
|
||||
// so it satisfies it, but it also contains values it must not report.
|
||||
CPPUNIT_ASSERT(filtered.isSatisfiedBy(broad));
|
||||
CPPUNIT_ASSERT(not filtered.isProjectionSatisfiedBy(broad));
|
||||
|
||||
// The filtered answer is incomplete for the broad query either way.
|
||||
CPPUNIT_ASSERT(not broad.isSatisfiedBy(filtered));
|
||||
CPPUNIT_ASSERT(not broad.isProjectionSatisfiedBy(filtered));
|
||||
|
||||
// Identical filters stay reusable, including for a wider projection.
|
||||
const dht::Query same {dht::Select {}.field(dht::Value::Field::Id),
|
||||
dht::Where {}.valueType(1)};
|
||||
const dht::Query wider {dht::Select {}.field(dht::Value::Field::Id).field(dht::Value::Field::ValueType),
|
||||
dht::Where {}.valueType(1)};
|
||||
CPPUNIT_ASSERT(filtered.isProjectionSatisfiedBy(same));
|
||||
CPPUNIT_ASSERT(filtered.isProjectionSatisfiedBy(wider));
|
||||
CPPUNIT_ASSERT(not wider.isProjectionSatisfiedBy(filtered));
|
||||
}
|
||||
|
||||
void
|
||||
ValueTester::tearDown()
|
||||
{}
|
||||
|
||||
void
|
||||
ValueTester::testSequenceFilterMsgpackRoundTrip()
|
||||
{
|
||||
for (uint64_t sequence : {0u, 1u, 65535u}) {
|
||||
dht::FieldValue original(dht::Value::Field::SeqNum, sequence);
|
||||
msgpack::sbuffer buffer;
|
||||
msgpack::pack(buffer, original);
|
||||
|
||||
msgpack::unpacked message;
|
||||
msgpack::unpack(message, buffer.data(), buffer.size());
|
||||
dht::FieldValue restored;
|
||||
restored.msgpack_unpack(message.get());
|
||||
CPPUNIT_ASSERT(restored.getField() == dht::Value::Field::SeqNum);
|
||||
CPPUNIT_ASSERT_EQUAL(sequence, restored.getInt());
|
||||
|
||||
dht::Value value;
|
||||
value.seq = sequence;
|
||||
CPPUNIT_ASSERT(restored.getLocalFilter()(value));
|
||||
value.seq = sequence == 0 ? 1 : 0;
|
||||
CPPUNIT_ASSERT(!restored.getLocalFilter()(value));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
ValueTester::testPushTypeMsgpackRoundTrip()
|
||||
{
|
||||
@@ -137,4 +214,29 @@ ValueTester::testPushTypePreservedAfterEncrypt()
|
||||
CPPUNIT_ASSERT(encrypted.isEncrypted());
|
||||
}
|
||||
|
||||
void
|
||||
ValueTester::testPushTypeJsonRoundTrip()
|
||||
{
|
||||
#ifdef OPENDHT_JSONCPP
|
||||
dht::Value original {(const uint8_t*) "hello", 5};
|
||||
original.id = 42;
|
||||
original.priority = 1;
|
||||
original.pushType = "videoCall";
|
||||
|
||||
dht::Value restored(original.toJson());
|
||||
|
||||
CPPUNIT_ASSERT_EQUAL(original.id, restored.id);
|
||||
CPPUNIT_ASSERT_EQUAL(original.priority, restored.priority);
|
||||
CPPUNIT_ASSERT_EQUAL(original.pushType, restored.pushType);
|
||||
|
||||
// No pushType: the field must stay absent from the JSON and empty after parse.
|
||||
dht::Value plain {(const uint8_t*) "data", 4};
|
||||
plain.id = 7;
|
||||
auto json = plain.toJson();
|
||||
CPPUNIT_ASSERT(!json.isMember("pt"));
|
||||
dht::Value plainRestored(json);
|
||||
CPPUNIT_ASSERT_EQUAL(std::string(), plainRestored.pushType);
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace test
|
||||
@@ -13,9 +13,13 @@ class ValueTester : public CppUnit::TestFixture
|
||||
CPPUNIT_TEST_SUITE(ValueTester);
|
||||
CPPUNIT_TEST(testConstructors);
|
||||
CPPUNIT_TEST(testFilter);
|
||||
CPPUNIT_TEST(testSequenceFilterMsgpackRoundTrip);
|
||||
CPPUNIT_TEST(testPushTypeMsgpackRoundTrip);
|
||||
CPPUNIT_TEST(testPushTypeAbsentAfterUnpack);
|
||||
CPPUNIT_TEST(testPushTypePreservedAfterEncrypt);
|
||||
CPPUNIT_TEST(testPushTypeJsonRoundTrip);
|
||||
CPPUNIT_TEST(testFieldValueIndexContainedIn);
|
||||
CPPUNIT_TEST(testProjectionSatisfiedBy);
|
||||
CPPUNIT_TEST_SUITE_END();
|
||||
|
||||
public:
|
||||
@@ -35,9 +39,19 @@ public:
|
||||
* Test compare operators
|
||||
*/
|
||||
void testFilter();
|
||||
void testSequenceFilterMsgpackRoundTrip();
|
||||
void testPushTypeMsgpackRoundTrip();
|
||||
void testPushTypeAbsentAfterUnpack();
|
||||
void testPushTypePreservedAfterEncrypt();
|
||||
void testPushTypeJsonRoundTrip();
|
||||
/**
|
||||
* Test that projected index containment compares field values, not only keys
|
||||
*/
|
||||
void testFieldValueIndexContainedIn();
|
||||
/**
|
||||
* Test that a projected query only reuses answers filtered exactly like it
|
||||
*/
|
||||
void testProjectionSatisfiedBy();
|
||||
};
|
||||
|
||||
} // namespace test
|
||||
@@ -0,0 +1,601 @@
|
||||
// Copyright (c) 2014-2026 Savoir-faire Linux Inc.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "test_valuecache.h"
|
||||
#include "../src/value_cache.h"
|
||||
|
||||
namespace test {
|
||||
CPPUNIT_TEST_SUITE_REGISTRATION(ValueCacheTester);
|
||||
|
||||
void
|
||||
ValueCacheTester::setUp()
|
||||
{}
|
||||
|
||||
void
|
||||
ValueCacheTester::tearDown()
|
||||
{}
|
||||
|
||||
void
|
||||
ValueCacheTester::testUpdate()
|
||||
{
|
||||
int addCount = 0;
|
||||
dht::ValueCache cache([&](const std::vector<std::shared_ptr<dht::Value>>& vals, bool expired) {
|
||||
if (!expired)
|
||||
addCount += vals.size();
|
||||
});
|
||||
|
||||
dht::TypeStore types;
|
||||
|
||||
auto v1 = std::make_shared<dht::Value>();
|
||||
v1->id = 1;
|
||||
v1->seq = 1;
|
||||
v1->data = {'A'};
|
||||
|
||||
auto v2 = std::make_shared<dht::Value>();
|
||||
v2->id = 1;
|
||||
v2->seq = 2;
|
||||
v2->data = {'B'};
|
||||
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
|
||||
// Add v1
|
||||
cache.onValues({v1}, {}, {}, types, now);
|
||||
CPPUNIT_ASSERT_EQUAL(1, addCount);
|
||||
|
||||
// Update v1 -> v2
|
||||
cache.onValues({v2}, {}, {}, types, now);
|
||||
CPPUNIT_ASSERT_EQUAL_MESSAGE("Callback should be called for update", 2, addCount);
|
||||
}
|
||||
|
||||
void
|
||||
ValueCacheTester::testUpdateExpiration()
|
||||
{
|
||||
int addCount = 0;
|
||||
int expireCount = 0;
|
||||
dht::ValueCache cache([&](const std::vector<std::shared_ptr<dht::Value>>& vals, bool expired) {
|
||||
if (expired)
|
||||
expireCount += vals.size();
|
||||
else
|
||||
addCount += vals.size();
|
||||
});
|
||||
|
||||
dht::TypeStore types;
|
||||
|
||||
auto v1 = std::make_shared<dht::Value>();
|
||||
v1->id = 1;
|
||||
v1->seq = 1;
|
||||
v1->data = {'A'};
|
||||
|
||||
auto v2 = std::make_shared<dht::Value>();
|
||||
v2->id = 1;
|
||||
v2->seq = 2;
|
||||
v2->data = {'B'};
|
||||
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
|
||||
// Add v1
|
||||
cache.onValues({v1}, {}, {}, types, now);
|
||||
CPPUNIT_ASSERT_EQUAL(1, addCount);
|
||||
CPPUNIT_ASSERT_EQUAL(0, expireCount);
|
||||
|
||||
// Update v1 -> v2: only the new value is emitted as an add,
|
||||
// no expire callback for the old value.
|
||||
cache.onValues({v2}, {}, {}, types, now);
|
||||
CPPUNIT_ASSERT_EQUAL(2, addCount);
|
||||
CPPUNIT_ASSERT_EQUAL_MESSAGE("No expire callback on edit", 0, expireCount);
|
||||
}
|
||||
|
||||
void
|
||||
ValueCacheTester::testExpiration()
|
||||
{
|
||||
int expireCount = 0;
|
||||
dht::ValueCache cache([&](const std::vector<std::shared_ptr<dht::Value>>& vals, bool expired) {
|
||||
if (expired)
|
||||
expireCount += vals.size();
|
||||
});
|
||||
|
||||
dht::TypeStore types;
|
||||
auto v1 = std::make_shared<dht::Value>();
|
||||
v1->id = 10;
|
||||
v1->data = {'A'};
|
||||
|
||||
// Default expiration is 10 minutes
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
cache.onValues({v1}, {}, {}, types, now);
|
||||
|
||||
// Advance time past expiration
|
||||
auto future = now + std::chrono::minutes(11);
|
||||
|
||||
// expireValues returns next expiration time
|
||||
cache.expireValues(future);
|
||||
|
||||
CPPUNIT_ASSERT_EQUAL(1, expireCount);
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
|
||||
}
|
||||
|
||||
void
|
||||
ValueCacheTester::testRefresh()
|
||||
{
|
||||
int expireCount = 0;
|
||||
dht::ValueCache cache([&](const std::vector<std::shared_ptr<dht::Value>>& vals, bool expired) {
|
||||
if (expired)
|
||||
expireCount += vals.size();
|
||||
});
|
||||
|
||||
dht::TypeStore types;
|
||||
auto v1 = std::make_shared<dht::Value>();
|
||||
v1->id = 20;
|
||||
v1->data = {'B'};
|
||||
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
cache.onValues({v1}, {}, {}, types, now);
|
||||
|
||||
// Refresh at +5 mins
|
||||
auto later = now + std::chrono::minutes(5);
|
||||
cache.onValues({}, {v1->id}, {}, types, later);
|
||||
|
||||
// Should not expire at +11 mins (original expiration was +10, so +10 from start)
|
||||
// But refresh should extend it to +10 from 'later' = +15 from start.
|
||||
|
||||
auto checkTime = now + std::chrono::minutes(11);
|
||||
cache.expireValues(checkTime);
|
||||
|
||||
CPPUNIT_ASSERT_EQUAL(0, expireCount);
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
|
||||
|
||||
// Should expire at +16 mins
|
||||
auto future = now + std::chrono::minutes(16);
|
||||
cache.expireValues(future);
|
||||
CPPUNIT_ASSERT_EQUAL(1, expireCount);
|
||||
}
|
||||
|
||||
void
|
||||
ValueCacheTester::testExplicitExpiration()
|
||||
{
|
||||
int expireCount = 0;
|
||||
dht::ValueCache cache([&](const std::vector<std::shared_ptr<dht::Value>>& vals, bool expired) {
|
||||
if (expired)
|
||||
expireCount += vals.size();
|
||||
});
|
||||
|
||||
dht::TypeStore types;
|
||||
auto v1 = std::make_shared<dht::Value>();
|
||||
v1->id = 30;
|
||||
v1->data = {'C'};
|
||||
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
cache.onValues({v1}, {}, {}, types, now);
|
||||
|
||||
cache.onValues({}, {}, {v1->id}, types, now);
|
||||
|
||||
CPPUNIT_ASSERT_EQUAL(1, expireCount);
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
|
||||
}
|
||||
|
||||
void
|
||||
ValueCacheTester::testClear()
|
||||
{
|
||||
dht::ValueCache cache([](const std::vector<std::shared_ptr<dht::Value>>&, bool) {});
|
||||
dht::TypeStore types;
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
|
||||
auto v1 = std::make_shared<dht::Value>();
|
||||
v1->id = 1;
|
||||
auto v2 = std::make_shared<dht::Value>();
|
||||
v2->id = 2;
|
||||
|
||||
cache.onValues({v1, v2}, {}, {}, types, now);
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 2, cache.size());
|
||||
|
||||
auto cbs = cache.clear();
|
||||
// Execute callbacks
|
||||
for (auto& cb : cbs)
|
||||
cb();
|
||||
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
|
||||
}
|
||||
|
||||
void
|
||||
ValueCacheTester::testSyncStatus()
|
||||
{
|
||||
std::vector<dht::ListenSyncStatus> statuses;
|
||||
{
|
||||
dht::ValueCache cache([](const std::vector<std::shared_ptr<dht::Value>>&, bool) {},
|
||||
[&](dht::ListenSyncStatus status) { statuses.push_back(status); });
|
||||
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 1, statuses.size());
|
||||
CPPUNIT_ASSERT(statuses.back() == dht::ListenSyncStatus::ADDED);
|
||||
|
||||
cache.onSynced(true);
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 2, statuses.size());
|
||||
CPPUNIT_ASSERT(statuses.back() == dht::ListenSyncStatus::SYNCED);
|
||||
|
||||
// Calling onSynced(true) again should not trigger the callback
|
||||
cache.onSynced(true);
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 2, statuses.size());
|
||||
|
||||
cache.onSynced(false);
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 3, statuses.size());
|
||||
CPPUNIT_ASSERT(statuses.back() == dht::ListenSyncStatus::UNSYNCED);
|
||||
|
||||
// Calling onSynced(false) again should not trigger the callback
|
||||
cache.onSynced(false);
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 3, statuses.size());
|
||||
|
||||
// Set to synced before destruction to test UNSYNCED on destruction
|
||||
cache.onSynced(true);
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 4, statuses.size());
|
||||
CPPUNIT_ASSERT(statuses.back() == dht::ListenSyncStatus::SYNCED);
|
||||
}
|
||||
|
||||
// Destructor should have been called
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 6, statuses.size());
|
||||
CPPUNIT_ASSERT(statuses[4] == dht::ListenSyncStatus::UNSYNCED);
|
||||
CPPUNIT_ASSERT(statuses[5] == dht::ListenSyncStatus::REMOVED);
|
||||
}
|
||||
|
||||
void
|
||||
ValueCacheTester::testMaxValues()
|
||||
{
|
||||
dht::ValueCache cache([](const std::vector<std::shared_ptr<dht::Value>>&, bool) {});
|
||||
dht::TypeStore types;
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
|
||||
// MAX_VALUES is 4096
|
||||
for (int i = 0; i < 4100; ++i) {
|
||||
auto v = std::make_shared<dht::Value>();
|
||||
v->id = (dht::Value::Id) i + 1;
|
||||
cache.onValues({v}, {}, {}, types, now + std::chrono::milliseconds(i));
|
||||
}
|
||||
|
||||
// Should be capped at 4096
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 4096, cache.size());
|
||||
}
|
||||
|
||||
void
|
||||
ValueCacheTester::testUpdateTypeExpiration()
|
||||
{
|
||||
// Test that when a value is updated with a different type, the expiration
|
||||
// uses the new type's expiration period, not the old one.
|
||||
int addCount = 0;
|
||||
int expireCount = 0;
|
||||
dht::ValueCache cache([&](const std::vector<std::shared_ptr<dht::Value>>& vals, bool expired) {
|
||||
if (expired)
|
||||
expireCount += vals.size();
|
||||
else
|
||||
addCount += vals.size();
|
||||
});
|
||||
|
||||
// Register a custom type with a short expiration (2 minutes)
|
||||
dht::TypeStore types;
|
||||
dht::ValueType shortType(42, "short", std::chrono::minutes(2));
|
||||
types.registerType(shortType);
|
||||
|
||||
// Register a custom type with a long expiration (30 minutes)
|
||||
dht::ValueType longType(99, "long", std::chrono::minutes(30));
|
||||
types.registerType(longType);
|
||||
|
||||
auto v1 = std::make_shared<dht::Value>();
|
||||
v1->id = 1;
|
||||
v1->type = 42; // short type: 2 min expiration
|
||||
v1->data = {'A'};
|
||||
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
|
||||
// Add v1 with short type
|
||||
cache.onValues({v1}, {}, {}, types, now);
|
||||
CPPUNIT_ASSERT_EQUAL(1, addCount);
|
||||
|
||||
// Update with new type (long expiration)
|
||||
auto v2 = std::make_shared<dht::Value>();
|
||||
v2->id = 1;
|
||||
v2->seq = 1;
|
||||
v2->type = 99; // long type: 30 min expiration
|
||||
v2->data = {'B'};
|
||||
|
||||
cache.onValues({v2}, {}, {}, types, now);
|
||||
CPPUNIT_ASSERT_EQUAL(2, addCount);
|
||||
|
||||
// At +3 minutes: should NOT be expired (long type has 30 min expiration)
|
||||
// Before the fix, the old type (2 min) would be used, causing expiration here
|
||||
expireCount = 0;
|
||||
auto at3min = now + std::chrono::minutes(3);
|
||||
cache.expireValues(at3min);
|
||||
CPPUNIT_ASSERT_EQUAL_MESSAGE("Value updated to long-expiry type should NOT expire at 3 min", 0, expireCount);
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
|
||||
|
||||
// At +31 minutes: should be expired
|
||||
auto at31min = now + std::chrono::minutes(31);
|
||||
cache.expireValues(at31min);
|
||||
CPPUNIT_ASSERT_EQUAL(1, expireCount);
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
|
||||
}
|
||||
|
||||
void
|
||||
ValueCacheTester::testSameSeqEditIgnored()
|
||||
{
|
||||
// Like OpValueCache, an edit is only recognized by an increasing seq:
|
||||
// a different payload with the same seq is a refresh, not an edit.
|
||||
int addCount = 0;
|
||||
dht::ValueCache cache([&](const std::vector<std::shared_ptr<dht::Value>>& vals, bool expired) {
|
||||
if (!expired)
|
||||
addCount += vals.size();
|
||||
});
|
||||
dht::TypeStore types;
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
|
||||
auto v1 = std::make_shared<dht::Value>();
|
||||
v1->id = 1;
|
||||
v1->seq = 3;
|
||||
v1->data = {'A'};
|
||||
cache.onValues({v1}, {}, {}, types, now);
|
||||
CPPUNIT_ASSERT_EQUAL(1, addCount);
|
||||
|
||||
auto sameSeq = std::make_shared<dht::Value>();
|
||||
sameSeq->id = 1;
|
||||
sameSeq->seq = 3;
|
||||
sameSeq->data = {'B'};
|
||||
cache.onValues({sameSeq}, {}, {}, types, now);
|
||||
CPPUNIT_ASSERT_EQUAL(1, addCount);
|
||||
|
||||
auto older = std::make_shared<dht::Value>();
|
||||
older->id = 1;
|
||||
older->seq = 2;
|
||||
older->data = {'C'};
|
||||
cache.onValues({older}, {}, {}, types, now);
|
||||
CPPUNIT_ASSERT_EQUAL(1, addCount);
|
||||
|
||||
auto newer = std::make_shared<dht::Value>();
|
||||
newer->id = 1;
|
||||
newer->seq = 4;
|
||||
newer->data = {'D'};
|
||||
cache.onValues({newer}, {}, {}, types, now);
|
||||
CPPUNIT_ASSERT_EQUAL(2, addCount);
|
||||
}
|
||||
|
||||
void
|
||||
ValueCacheTester::testSameContentHigherSeqIsRefresh()
|
||||
{
|
||||
// Value::operator== ignores seq: identical content with a bumped seq is
|
||||
// the same value re-announced, not an edition. Re-emitting it would make
|
||||
// a source-counting OpValueCache count this node twice.
|
||||
int addCount = 0;
|
||||
dht::ValueCache cache([&](const std::vector<std::shared_ptr<dht::Value>>& vals, bool expired) {
|
||||
if (!expired)
|
||||
addCount += vals.size();
|
||||
});
|
||||
dht::TypeStore types;
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
|
||||
auto v1 = std::make_shared<dht::Value>();
|
||||
v1->id = 1;
|
||||
v1->seq = 0;
|
||||
v1->data = {'A'};
|
||||
cache.onValues({v1}, {}, {}, types, now);
|
||||
CPPUNIT_ASSERT_EQUAL(1, addCount);
|
||||
|
||||
auto sameContent = std::make_shared<dht::Value>();
|
||||
sameContent->id = 1;
|
||||
sameContent->seq = 1;
|
||||
sameContent->data = {'A'};
|
||||
auto later = now + std::chrono::minutes(5);
|
||||
cache.onValues({sameContent}, {}, {}, types, later);
|
||||
CPPUNIT_ASSERT_EQUAL_MESSAGE("Same content with a higher seq is a refresh, not an edit", 1, addCount);
|
||||
|
||||
// The refresh still extends the expiration (default type: 10 minutes)
|
||||
cache.expireValues(now + std::chrono::minutes(11));
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
|
||||
|
||||
auto edited = std::make_shared<dht::Value>();
|
||||
edited->id = 1;
|
||||
edited->seq = 2;
|
||||
edited->data = {'B'};
|
||||
cache.onValues({edited}, {}, {}, types, later);
|
||||
CPPUNIT_ASSERT_EQUAL(2, addCount);
|
||||
}
|
||||
|
||||
void
|
||||
ValueCacheTester::testShortExpirationRefreshLost()
|
||||
{
|
||||
// Demonstrates that a value with a short expiration type survives
|
||||
// past its nominal expiration when the listen is synced, thanks to
|
||||
// a proportional grace period (= typeExpiration).
|
||||
//
|
||||
// For a 5s type: effective expiration when synced = expiration + 5s = 10s
|
||||
//
|
||||
// Scenario:
|
||||
// t=0 : Value V added → nominal expiration t+5s, effective t+10s
|
||||
// t=6 : Past nominal but within grace → alive
|
||||
// t=8 : Refresh arrives → V refreshed → expiration reset to t8+5s, effective t8+10s=t18s
|
||||
// t=14: Still alive (within refreshed effective range)
|
||||
// t=20: No more refreshes → past t8+10s=t18s → expired
|
||||
|
||||
int addCount = 0;
|
||||
int expireCount = 0;
|
||||
dht::ValueCache cache([&](const std::vector<std::shared_ptr<dht::Value>>& vals, bool expired) {
|
||||
if (expired)
|
||||
expireCount += vals.size();
|
||||
else
|
||||
addCount += vals.size();
|
||||
});
|
||||
|
||||
// Register a type with a very short expiration (5 seconds)
|
||||
dht::TypeStore types;
|
||||
dht::ValueType shortType(100, "short_lived", std::chrono::seconds(5));
|
||||
types.registerType(shortType);
|
||||
|
||||
auto v1 = std::make_shared<dht::Value>();
|
||||
v1->id = 1;
|
||||
v1->type = 100;
|
||||
v1->data = {'V'};
|
||||
|
||||
auto t0 = std::chrono::steady_clock::now();
|
||||
|
||||
// t=0: Listen response arrives with V
|
||||
cache.onValues({v1}, {}, {}, types, t0);
|
||||
CPPUNIT_ASSERT_EQUAL(1, addCount);
|
||||
CPPUNIT_ASSERT_EQUAL(0, expireCount);
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
|
||||
|
||||
// Mark cache as synced (listen is active)
|
||||
cache.onSynced(true);
|
||||
|
||||
// t=6: Past nominal 5s expiration, but grace (5s) gives effective 10s
|
||||
auto t6 = t0 + std::chrono::seconds(6);
|
||||
cache.expireValues(t6);
|
||||
CPPUNIT_ASSERT_EQUAL_MESSAGE("Value should survive past nominal expiration thanks to proportional grace",
|
||||
0,
|
||||
expireCount);
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
|
||||
|
||||
// t=8: Value refresh arrives → expiration reset to t8+5s=t13s, effective t8+10s=t18s
|
||||
auto t8 = t0 + std::chrono::seconds(8);
|
||||
cache.onValues({}, {v1->id}, {}, types, t8);
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
|
||||
|
||||
// t=14: Within refreshed effective range (t18s) → alive
|
||||
auto t14 = t0 + std::chrono::seconds(14);
|
||||
cache.expireValues(t14);
|
||||
CPPUNIT_ASSERT_EQUAL_MESSAGE("Value should survive past short expiration after refresh", 0, expireCount);
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
|
||||
|
||||
// t=20: No more refreshes → past effective t18s → must expire
|
||||
auto t20 = t0 + std::chrono::seconds(20);
|
||||
cache.expireValues(t20);
|
||||
CPPUNIT_ASSERT_EQUAL_MESSAGE("Value must eventually expire even while synced if server stops refreshing",
|
||||
1,
|
||||
expireCount);
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
|
||||
}
|
||||
|
||||
void
|
||||
ValueCacheTester::testShortExpirationHighChurn()
|
||||
{
|
||||
// Simulates high churn: multiple rapid add/expire/refresh cycles with
|
||||
// short-lived values, verifying the cache stays consistent.
|
||||
|
||||
int addCount = 0;
|
||||
int expireCount = 0;
|
||||
dht::ValueCache cache([&](const std::vector<std::shared_ptr<dht::Value>>& vals, bool expired) {
|
||||
if (expired)
|
||||
expireCount += vals.size();
|
||||
else
|
||||
addCount += vals.size();
|
||||
});
|
||||
|
||||
dht::TypeStore types;
|
||||
dht::ValueType shortType(101, "fast_churn", std::chrono::seconds(3));
|
||||
types.registerType(shortType);
|
||||
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
|
||||
// Simulate 10 churn cycles: add value, advance time, refresh, advance, expire
|
||||
for (int cycle = 0; cycle < 10; cycle++) {
|
||||
auto v = std::make_shared<dht::Value>();
|
||||
v->id = static_cast<dht::Value::Id>(500 + cycle);
|
||||
v->type = 101;
|
||||
v->data = {static_cast<uint8_t>('A' + cycle)};
|
||||
|
||||
auto t = now + std::chrono::seconds(cycle * 10);
|
||||
|
||||
// Add the value
|
||||
cache.onValues({v}, {}, {}, types, t);
|
||||
|
||||
// Refresh halfway through
|
||||
auto tRefresh = t + std::chrono::seconds(2);
|
||||
cache.onValues({}, {v->id}, {}, types, tRefresh);
|
||||
|
||||
// Explicit expire from server
|
||||
auto tExpire = t + std::chrono::seconds(8);
|
||||
cache.onValues({}, {}, {v->id}, types, tExpire);
|
||||
}
|
||||
|
||||
// All 10 values were added and explicitly expired
|
||||
CPPUNIT_ASSERT_EQUAL(10, addCount);
|
||||
CPPUNIT_ASSERT_EQUAL(10, expireCount);
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
|
||||
}
|
||||
|
||||
void
|
||||
ValueCacheTester::testGracePeriodWhileSynced()
|
||||
{
|
||||
// Verify that when the listen is synced, the proportional grace period
|
||||
// (= typeExpiration) delays time-based expiration but doesn't prevent
|
||||
// it entirely. For a 2s type: effective expiration = 2s + 2s = 4s.
|
||||
|
||||
int expireCount = 0;
|
||||
dht::ValueCache cache([&](const std::vector<std::shared_ptr<dht::Value>>& vals, bool expired) {
|
||||
if (expired)
|
||||
expireCount += vals.size();
|
||||
});
|
||||
|
||||
dht::TypeStore types;
|
||||
dht::ValueType shortType(110, "short", std::chrono::seconds(2));
|
||||
types.registerType(shortType);
|
||||
|
||||
auto v1 = std::make_shared<dht::Value>();
|
||||
v1->id = 1;
|
||||
v1->type = 110;
|
||||
v1->data = {'X'};
|
||||
|
||||
auto t0 = std::chrono::steady_clock::now();
|
||||
cache.onValues({v1}, {}, {}, types, t0);
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
|
||||
|
||||
// Mark as synced
|
||||
cache.onSynced(true);
|
||||
|
||||
// t=3: Past nominal 2s, within effective 4s → grace protects
|
||||
auto t3 = t0 + std::chrono::seconds(3);
|
||||
cache.expireValues(t3);
|
||||
CPPUNIT_ASSERT_EQUAL_MESSAGE("Synced cache: grace period should prevent early expiry", 0, expireCount);
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
|
||||
|
||||
// t=5: Past effective 4s (2s + 2s grace) → must expire
|
||||
auto t5 = t0 + std::chrono::seconds(5);
|
||||
cache.expireValues(t5);
|
||||
CPPUNIT_ASSERT_EQUAL_MESSAGE("Synced cache: value must eventually expire after grace period", 1, expireCount);
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
|
||||
}
|
||||
|
||||
void
|
||||
ValueCacheTester::testUnsyncExpiresImmediately()
|
||||
{
|
||||
// When the cache goes from synced to unsynced, the grace period is
|
||||
// removed and already-past-due values expire on the next check.
|
||||
// Type expiration = 5s. Grace when synced = 5s → effective = 10s.
|
||||
|
||||
int expireCount = 0;
|
||||
dht::ValueCache cache([&](const std::vector<std::shared_ptr<dht::Value>>& vals, bool expired) {
|
||||
if (expired)
|
||||
expireCount += vals.size();
|
||||
});
|
||||
|
||||
dht::TypeStore types;
|
||||
dht::ValueType shortType(120, "short", std::chrono::seconds(5));
|
||||
types.registerType(shortType);
|
||||
|
||||
auto v1 = std::make_shared<dht::Value>();
|
||||
v1->id = 1;
|
||||
v1->type = 120;
|
||||
v1->data = {'U'};
|
||||
|
||||
auto t0 = std::chrono::steady_clock::now();
|
||||
cache.onValues({v1}, {}, {}, types, t0);
|
||||
cache.onSynced(true);
|
||||
|
||||
// t=7: past nominal 5s, within effective 10s while synced → alive
|
||||
auto t7 = t0 + std::chrono::seconds(7);
|
||||
cache.expireValues(t7);
|
||||
CPPUNIT_ASSERT_EQUAL(0, expireCount);
|
||||
|
||||
// Now unsync (node went away / connectivity loss)
|
||||
cache.onSynced(false);
|
||||
|
||||
// Same time point t=7: past nominal 5s, no grace → expires
|
||||
cache.expireValues(t7);
|
||||
CPPUNIT_ASSERT_EQUAL_MESSAGE("Unsynced cache should immediately expire past-due values", 1, expireCount);
|
||||
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
|
||||
}
|
||||
|
||||
} // namespace test
|
||||
@@ -0,0 +1,51 @@
|
||||
// Copyright (c) 2014-2026 Savoir-faire Linux Inc.
|
||||
// SPDX-License-Identifier: MIT
|
||||
#pragma once
|
||||
|
||||
#include <cppunit/TestFixture.h>
|
||||
#include <cppunit/extensions/HelperMacros.h>
|
||||
|
||||
namespace test {
|
||||
|
||||
class ValueCacheTester : public CppUnit::TestFixture
|
||||
{
|
||||
CPPUNIT_TEST_SUITE(ValueCacheTester);
|
||||
CPPUNIT_TEST(testUpdate);
|
||||
CPPUNIT_TEST(testUpdateExpiration);
|
||||
CPPUNIT_TEST(testExpiration);
|
||||
CPPUNIT_TEST(testRefresh);
|
||||
CPPUNIT_TEST(testExplicitExpiration);
|
||||
CPPUNIT_TEST(testClear);
|
||||
CPPUNIT_TEST(testSyncStatus);
|
||||
CPPUNIT_TEST(testMaxValues);
|
||||
CPPUNIT_TEST(testUpdateTypeExpiration);
|
||||
CPPUNIT_TEST(testSameSeqEditIgnored);
|
||||
CPPUNIT_TEST(testSameContentHigherSeqIsRefresh);
|
||||
CPPUNIT_TEST(testShortExpirationRefreshLost);
|
||||
CPPUNIT_TEST(testShortExpirationHighChurn);
|
||||
CPPUNIT_TEST(testGracePeriodWhileSynced);
|
||||
CPPUNIT_TEST(testUnsyncExpiresImmediately);
|
||||
CPPUNIT_TEST_SUITE_END();
|
||||
|
||||
public:
|
||||
void setUp();
|
||||
void tearDown();
|
||||
|
||||
void testUpdate();
|
||||
void testUpdateExpiration();
|
||||
void testExpiration();
|
||||
void testRefresh();
|
||||
void testExplicitExpiration();
|
||||
void testClear();
|
||||
void testSyncStatus();
|
||||
void testMaxValues();
|
||||
void testUpdateTypeExpiration();
|
||||
void testSameSeqEditIgnored();
|
||||
void testSameContentHigherSeqIsRefresh();
|
||||
void testShortExpirationRefreshLost();
|
||||
void testShortExpirationHighChurn();
|
||||
void testGracePeriodWhileSynced();
|
||||
void testUnsyncExpiresImmediately();
|
||||
};
|
||||
|
||||
} // namespace test
|
||||
@@ -1,9 +1,12 @@
|
||||
{
|
||||
"default-registry": {
|
||||
"kind": "git",
|
||||
"baseline": "66c0373dc7fca549e5803087b9487edfe3aca0a1",
|
||||
"baseline": "9eec002df1cd109f8f104cd966a763daf9751489",
|
||||
"repository": "https://github.com/microsoft/vcpkg"
|
||||
},
|
||||
"overlay-ports": [
|
||||
"ports"
|
||||
],
|
||||
"registries": [
|
||||
{
|
||||
"kind": "artifact",
|
||||
|
||||
+9
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "opendht",
|
||||
"version-string": "4.0.0",
|
||||
"version-string": "4.4.2",
|
||||
"port-version": 1,
|
||||
"description": "OpenDHT: A lightweight C++17 Distributed Hash Table library.",
|
||||
"homepage": "https://opendht.net",
|
||||
@@ -16,7 +16,14 @@
|
||||
},
|
||||
"fmt",
|
||||
"nettle",
|
||||
"shiftmedia-libgnutls",
|
||||
{
|
||||
"name": "shiftmedia-libgnutls",
|
||||
"platform": "windows & !mingw"
|
||||
},
|
||||
{
|
||||
"name": "libgnutls",
|
||||
"platform": "!windows | mingw"
|
||||
},
|
||||
"msgpack",
|
||||
"jsoncpp",
|
||||
"asio",
|
||||
|
||||
Reference in new issue
Block a user