mirror of
https://github.com/savoirfairelinux/opendht.git
synced 2026-11-08 12:28:42 +00:00
Compare commits
163
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dff27203a4 | ||
|
|
506d9f9cbb | ||
|
|
349f4e5fc6 | ||
|
|
11d69d02e8 | ||
|
|
3c87991e92 | ||
|
|
52287bf608 | ||
|
|
061ce2deb4 | ||
|
|
64b536fb53 | ||
|
|
67c76f17de | ||
|
|
25fde756e8 | ||
|
|
f6b3eda280 | ||
|
|
9aee407a42 | ||
|
|
722ef44c7d | ||
|
|
6ed2720bd7 | ||
|
|
9005f73141 | ||
|
|
9785c0a127 | ||
|
|
5662cc36d1 | ||
|
|
e116cfe548 | ||
|
|
1c5e8378b8 | ||
|
|
c630e80345 | ||
|
|
17f83edce4 | ||
|
|
afab251376 | ||
|
|
86d452fbb6 | ||
|
|
08ff775232 | ||
|
|
738e0e22bf | ||
|
|
8ec5b47c86 | ||
|
|
e1c31568f9 | ||
|
|
45bad769b4 | ||
|
|
09780fd923 | ||
|
|
747e70d1ae | ||
|
|
05abfa89bf | ||
|
|
b714714128 | ||
|
|
2e8b36477d | ||
|
|
14543ae477 | ||
|
|
044135732f | ||
|
|
21b7f0d757 | ||
|
|
a5bfd3d933 | ||
|
|
af38323728 | ||
|
|
62e0ded803 | ||
|
|
a09cfefe5e | ||
|
|
13f340694e | ||
|
|
3723c4acf1 | ||
|
|
f19bca0df1 | ||
|
|
fbc7f90897 | ||
|
|
3a3c421ef9 | ||
|
|
9ec7a58ee3 | ||
|
|
ce73eb052e | ||
|
|
d716fae4bc | ||
|
|
a040bfeebb | ||
|
|
3223742d70 | ||
|
|
d2d56fcc20 | ||
|
|
af814f027f | ||
|
|
2016a9e2fa | ||
|
|
1db8abb7fc | ||
|
|
3f7b1bc3f5 | ||
|
|
019c25633e | ||
|
|
8193f43593 | ||
|
|
39564f14c1 | ||
|
|
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 |
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
|
||||
@@ -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"
|
||||
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-dev libargon2-dev"
|
||||
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-cxx-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-cxx-dev libargon2-dev"
|
||||
UBUNTU_MINIMAL_DEPS: "libncurses5-dev libreadline-dev nettle-dev libfmt-dev libgnutls28-dev libcppunit-dev libmsgpack-cxx-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]
|
||||
|
||||
@@ -33,6 +33,18 @@ jobs:
|
||||
- name: Build with Ninja
|
||||
run: cd build && ninja
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Test Rust bindings
|
||||
env:
|
||||
LIBRARY_PATH: ${{ github.workspace }}/build
|
||||
LD_LIBRARY_PATH: ${{ github.workspace }}/build
|
||||
RUSTFLAGS: -L native=${{ github.workspace }}/build
|
||||
run: |
|
||||
cargo test --manifest-path rust/Cargo.toml --all-targets
|
||||
cargo test --manifest-path rust/Cargo.toml --all-features --all-targets
|
||||
cargo test --manifest-path rust/Cargo.toml --all-features --doc
|
||||
|
||||
build-ubuntu-minimal:
|
||||
name: Ubuntu/GCC minimal build
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -12,7 +12,7 @@ jobs:
|
||||
sudo apt update && \
|
||||
sudo apt install libncurses5-dev libreadline-dev nettle-dev \
|
||||
libgnutls28-dev libuv1-dev python3-dev python3-setuptools libcppunit-dev libjsoncpp-dev \
|
||||
libfmt-dev libmsgpack-dev libargon2-dev libasio-dev \
|
||||
libfmt-dev libmsgpack-cxx-dev libargon2-dev libasio-dev \
|
||||
llvm llvm-dev clang clang-tools && \
|
||||
sudo apt remove gcc g++
|
||||
|
||||
@@ -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 .
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
name: Node.js Package
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ['**']
|
||||
paths:
|
||||
- 'nodejs/**'
|
||||
- '.github/workflows/nodejs-package.yml'
|
||||
- 'src/**'
|
||||
- 'include/**'
|
||||
- 'cmake/**'
|
||||
- 'CMakeLists.txt'
|
||||
- 'vcpkg*.json'
|
||||
- 'ports/**'
|
||||
- 'LICENSE'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'nodejs/**'
|
||||
- '.github/workflows/nodejs-package.yml'
|
||||
- 'src/**'
|
||||
- 'include/**'
|
||||
- 'cmake/**'
|
||||
- 'CMakeLists.txt'
|
||||
- 'vcpkg*.json'
|
||||
- 'ports/**'
|
||||
- 'LICENSE'
|
||||
workflow_call:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build ${{ matrix.target }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
container:
|
||||
image: ${{ matrix.container }}
|
||||
credentials:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout-minutes: 45
|
||||
defaults:
|
||||
run:
|
||||
working-directory: nodejs
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-26.04
|
||||
target: linux-x64
|
||||
container: ghcr.io/${{ github.repository }}/opendht-deps-noble:latest
|
||||
- os: macos-15
|
||||
target: darwin-arm64
|
||||
- os: macos-15-intel
|
||||
target: darwin-x64
|
||||
- os: windows-2022
|
||||
target: win32-x64
|
||||
env:
|
||||
VCPKG_DISABLE_METRICS: '1'
|
||||
VCPKG_DEFAULT_TRIPLET: x64-windows-release
|
||||
VCPKG_INSTALLED_DIR: ${{ github.workspace }}/vcpkg_installed
|
||||
VCPKG_BINARY_SOURCES: clear;files,${{ github.workspace }}/vcpkg-binary-cache,readwrite
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: npm
|
||||
cache-dependency-path: nodejs/package-lock.json
|
||||
- name: Install macOS dependencies
|
||||
if: runner.os == 'macOS'
|
||||
run: brew install cmake ninja pkg-config dylibbundler gnutls nettle argon2 msgpack-cxx fmt jsoncpp simdutf asio llhttp openssl
|
||||
- name: Install restinio
|
||||
if: runner.os == 'macOS'
|
||||
env:
|
||||
RESTINIO_VERSION: '0.7.10'
|
||||
run: |
|
||||
include="$RUNNER_TEMP/opendht-proxy/include"
|
||||
mkdir -p "$include/nonstd"
|
||||
curl -fsSL --retry 3 https://raw.githubusercontent.com/martinmoene/expected-lite/v0.10.0/include/nonstd/expected.hpp -o "$include/nonstd/expected.hpp"
|
||||
curl -fsSL --retry 3 "https://github.com/Stiffstream/restinio/releases/download/v${RESTINIO_VERSION}/restinio-${RESTINIO_VERSION}.tar.bz2" -o "$RUNNER_TEMP/restinio.tar.bz2"
|
||||
tar -xjf "$RUNNER_TEMP/restinio.tar.bz2" -C "$RUNNER_TEMP"
|
||||
cp -R "$RUNNER_TEMP/restinio-${RESTINIO_VERSION}/dev/restinio" "$include/"
|
||||
echo "CMAKE_PREFIX_PATH=$RUNNER_TEMP/opendht-proxy" >> "$GITHUB_ENV"
|
||||
- name: Set up MSVC
|
||||
if: runner.os == 'Windows'
|
||||
uses: ilammy/msvc-dev-cmd@v1
|
||||
with:
|
||||
arch: x64
|
||||
- name: Set up vcpkg
|
||||
if: runner.os == 'Windows'
|
||||
uses: lukka/run-vcpkg@v11
|
||||
with:
|
||||
vcpkgJsonGlob: vcpkg.json
|
||||
vcpkgConfigurationJsonGlob: vcpkg-configuration.json
|
||||
- name: Cache Windows native dependencies
|
||||
if: runner.os == 'Windows'
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: ${{ github.workspace }}/vcpkg-binary-cache
|
||||
key: node-vcpkg-${{ env.VCPKG_DEFAULT_TRIPLET }}-${{ hashFiles('vcpkg.json', 'vcpkg-configuration.json', 'ports/**') }}-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
restore-keys: |
|
||||
node-vcpkg-${{ env.VCPKG_DEFAULT_TRIPLET }}-${{ hashFiles('vcpkg.json', 'vcpkg-configuration.json', 'ports/**') }}-
|
||||
- run: npm ci --no-audit --no-fund
|
||||
- name: Build Unix addon
|
||||
if: runner.os != 'Windows'
|
||||
shell: bash
|
||||
run: |
|
||||
if [[ "$RUNNER_OS" == 'macOS' ]]; then
|
||||
export MACOSX_DEPLOYMENT_TARGET=15.0
|
||||
openssl_prefix=$(brew --prefix openssl)
|
||||
export PKG_CONFIG_PATH="$openssl_prefix/lib/pkgconfig${PKG_CONFIG_PATH:+:$PKG_CONFIG_PATH}"
|
||||
fi
|
||||
npm run build -- --CDOPENDHT_DOWNLOAD_DEPS=OFF
|
||||
- name: Build Windows addon
|
||||
if: runner.os == 'Windows'
|
||||
shell: pwsh
|
||||
run: |
|
||||
npm run build -- `
|
||||
"--CDCMAKE_TOOLCHAIN_FILE=$env:VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake" `
|
||||
"--CDVCPKG_MANIFEST_DIR=$env:GITHUB_WORKSPACE" `
|
||||
"--CDVCPKG_INSTALLED_DIR=$env:VCPKG_INSTALLED_DIR" `
|
||||
"--CDVCPKG_TARGET_TRIPLET=$env:VCPKG_DEFAULT_TRIPLET" `
|
||||
--CDVCPKG_MANIFEST_NO_DEFAULT_FEATURES=ON `
|
||||
--CDVCPKG_MANIFEST_FEATURES=proxy `
|
||||
--CDCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreadedDLL `
|
||||
--CDOPENDHT_DOWNLOAD_DEPS=OFF
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
"$env:VCPKG_INSTALLED_DIR/$env:VCPKG_DEFAULT_TRIPLET/bin" >> $env:GITHUB_PATH
|
||||
"OPENDHT_LIBRARY_PATH=$env:VCPKG_INSTALLED_DIR/$env:VCPKG_DEFAULT_TRIPLET/bin" >> $env:GITHUB_ENV
|
||||
- run: npm test
|
||||
- run: npm run typecheck
|
||||
- run: npm run bundle
|
||||
- name: Include static simdutf licenses
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
mkdir -p prebuilds/linux-x64/licenses/simdutf
|
||||
cp /usr/share/doc/simdutf/LICENSE-* prebuilds/linux-x64/licenses/simdutf/
|
||||
- run: npm run test:package
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: opendht-node-prebuild-${{ matrix.target }}
|
||||
path: nodejs/prebuilds/
|
||||
if-no-files-found: error
|
||||
|
||||
package:
|
||||
name: Assemble npm archive
|
||||
needs: build
|
||||
runs-on: ubuntu-26.04
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '22'
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: opendht-node-prebuild-*
|
||||
path: nodejs/prebuilds
|
||||
merge-multiple: true
|
||||
- name: Set release version
|
||||
if: github.event_name == 'push' && github.ref_type == 'tag'
|
||||
working-directory: nodejs
|
||||
env:
|
||||
RELEASE_TAG: ${{ github.ref_name }}
|
||||
run: |
|
||||
release_version=$(npm version "${RELEASE_TAG#v}" --no-git-tag-version --ignore-scripts --allow-same-version)
|
||||
if [[ "$release_version" != "$RELEASE_TAG" ]]; then
|
||||
printf '::error::Release tag %s is not an exact npm semantic version (normalized to %s).\n' "$RELEASE_TAG" "$release_version" >&2
|
||||
exit 1
|
||||
fi
|
||||
- name: Pack all platforms
|
||||
working-directory: nodejs
|
||||
run: |
|
||||
npm ci --ignore-scripts
|
||||
mkdir -p dist
|
||||
npm pack --pack-destination dist
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: opendht-node-npm
|
||||
path: nodejs/dist/*.tgz
|
||||
if-no-files-found: error
|
||||
|
||||
smoke:
|
||||
name: Install on ${{ matrix.os }} / Node ${{ matrix.node }}
|
||||
needs: package
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 10
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-24.04, ubuntu-26.04, macos-15, macos-15-intel, windows-2022]
|
||||
node: ['22', '24', '26']
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: opendht-node-npm
|
||||
path: nodejs/dist
|
||||
- name: Test installed archive without a native build toolchain
|
||||
working-directory: nodejs
|
||||
run: npm run test:package -- dist
|
||||
@@ -4,10 +4,10 @@ permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
push:
|
||||
branches: ['**']
|
||||
pull_request:
|
||||
workflow_call:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
|
||||
# Run locally with: pipx run cibuildwheel --platform linux python
|
||||
- name: Build wheels
|
||||
uses: pypa/cibuildwheel@v4.1.1
|
||||
uses: pypa/cibuildwheel@v4.3.5
|
||||
with:
|
||||
package-dir: python
|
||||
|
||||
@@ -36,36 +36,28 @@ jobs:
|
||||
name: Build wheels on Windows
|
||||
runs-on: windows-latest
|
||||
env:
|
||||
VCPKG_VERSION: "2026.06.01"
|
||||
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@v7
|
||||
|
||||
- name: Cache vcpkg installed packages
|
||||
id: cache-vcpkg
|
||||
- name: Set up vcpkg
|
||||
uses: lukka/run-vcpkg@v11
|
||||
with:
|
||||
vcpkgJsonGlob: vcpkg.json
|
||||
vcpkgConfigurationJsonGlob: vcpkg-configuration.json
|
||||
|
||||
- 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 }}-${{ env.VCPKG_DEFAULT_TRIPLET }}-${{ 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=${{ env.VCPKG_DEFAULT_TRIPLET }} `
|
||||
--x-install-root=${{ env.VCPKG_INSTALLED_DIR }}
|
||||
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: 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 `
|
||||
@@ -90,19 +82,3 @@ jobs:
|
||||
with:
|
||||
name: opendht-wheels-win_amd64
|
||||
path: ./build/wheelhouse/*.whl
|
||||
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build_wheels, build_wheels_windows]
|
||||
steps:
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: opendht-wheels-*
|
||||
path: dist
|
||||
merge-multiple: true
|
||||
|
||||
- name: Publish package
|
||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
password: ${{ secrets.PYPI_API_TOKEN }}
|
||||
@@ -8,7 +8,9 @@ on:
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME_DEPS: ${{ github.repository }}/opendht-deps
|
||||
IMAGE_NAME_DEPS_NOBLE: ${{ github.repository }}/opendht-deps-noble
|
||||
IMAGE_NAME: ${{ github.repository }}/opendht
|
||||
IMAGE_NAME_NOBLE: ${{ github.repository }}/opendht-noble
|
||||
IMAGE_NAME_DEPS_LLVM: ${{ github.repository }}/opendht-deps-llvm
|
||||
IMAGE_NAME_LLVM: ${{ github.repository }}/opendht-llvm
|
||||
IMAGE_NAME_DHTNODE: ${{ github.repository }}/dhtnode
|
||||
@@ -16,6 +18,19 @@ env:
|
||||
IMAGE_NAME_ALPINE_DEPS: ${{ github.repository }}/opendht-deps-alpine
|
||||
|
||||
jobs:
|
||||
nodejs-package:
|
||||
name: Build and verify Node.js package
|
||||
uses: ./.github/workflows/nodejs-package.yml
|
||||
permissions:
|
||||
contents: read
|
||||
packages: read
|
||||
|
||||
python-wheels:
|
||||
name: Build Python wheels
|
||||
uses: ./.github/workflows/python-wheels.yml
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
build-and-push-deps-image:
|
||||
name: Dependency Docker image
|
||||
runs-on: ubuntu-latest
|
||||
@@ -56,6 +71,46 @@ jobs:
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
build-and-push-deps-image-noble:
|
||||
name: Dependency Docker image (Ubuntu 24.04)
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_DEPS_NOBLE }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
file: docker/DockerfileDepsNoble
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
build-and-push-image:
|
||||
name: OpenDHT Docker image
|
||||
runs-on: ubuntu-latest
|
||||
@@ -98,6 +153,47 @@ jobs:
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
build-and-push-image-noble:
|
||||
name: OpenDHT Docker image (Ubuntu 24.04)
|
||||
runs-on: ubuntu-latest
|
||||
needs: build-and-push-deps-image-noble
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_NOBLE }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
file: docker/DockerfileNoble
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
build-and-push-deps-image-llvm:
|
||||
name: Dependency Docker image (LLVM)
|
||||
runs-on: ubuntu-latest
|
||||
@@ -309,12 +405,10 @@ jobs:
|
||||
name: Build Windows MSI Installer
|
||||
runs-on: windows-latest
|
||||
env:
|
||||
VCPKG_VERSION: "2026.06.01"
|
||||
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:
|
||||
@@ -323,6 +417,12 @@ jobs:
|
||||
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 +432,14 @@ jobs:
|
||||
wix extension add --global WixToolset.UI.wixext/6.0.1
|
||||
shell: pwsh
|
||||
|
||||
- name: Cache vcpkg installed packages
|
||||
id: cache-vcpkg
|
||||
- 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 +447,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,12 +500,10 @@ jobs:
|
||||
name: Build Windows dhtnode
|
||||
runs-on: windows-latest
|
||||
env:
|
||||
VCPKG_VERSION: "2026.06.01"
|
||||
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:
|
||||
@@ -427,33 +512,23 @@ jobs:
|
||||
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
|
||||
- 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 +536,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 `
|
||||
@@ -493,7 +568,7 @@ jobs:
|
||||
create-github-release:
|
||||
name: Create GitHub Release
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build-windows-msi, build-windows-dhtnode]
|
||||
needs: [build-windows-msi, build-windows-dhtnode, nodejs-package]
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
@@ -513,11 +588,99 @@ jobs:
|
||||
name: opendht-windows-dhtnode
|
||||
path: downloaded_artifacts/static_dhtnode
|
||||
|
||||
- name: Download npm archive
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: opendht-node-npm
|
||||
path: downloaded_artifacts/nodejs
|
||||
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
uses: softprops/action-gh-release@v3
|
||||
with:
|
||||
draft: true
|
||||
prerelease: ${{ contains(github.ref_name, '-') }}
|
||||
files: |
|
||||
downloaded_artifacts/windows_msi/*.msi
|
||||
downloaded_artifacts/static_dhtnode/**/*dhtnode.exe
|
||||
downloaded_artifacts/nodejs/*.tgz
|
||||
|
||||
publish-npm:
|
||||
name: Publish opendht to npm
|
||||
needs: create-github-release
|
||||
runs-on: ubuntu-26.04
|
||||
environment:
|
||||
name: npm
|
||||
url: https://www.npmjs.com/package/opendht
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '24'
|
||||
registry-url: https://registry.npmjs.org/
|
||||
package-manager-cache: false
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: opendht-node-npm
|
||||
path: nodejs/dist
|
||||
- name: Publish verified archive
|
||||
working-directory: nodejs
|
||||
env:
|
||||
RELEASE_TAG: ${{ github.ref_name }}
|
||||
run: |
|
||||
dist_tag=latest
|
||||
if [[ "${RELEASE_TAG#v}" == *-* ]]; then
|
||||
dist_tag=next
|
||||
fi
|
||||
npm publish ./dist/*.tgz --access public --provenance --tag "$dist_tag"
|
||||
|
||||
publish-pypi:
|
||||
name: Publish opendht to PyPI
|
||||
needs: [create-github-release, python-wheels]
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: pypi
|
||||
url: https://pypi.org/p/opendht
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: opendht-wheels-*
|
||||
path: dist
|
||||
merge-multiple: true
|
||||
- name: Publish wheels
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
|
||||
publish-crate:
|
||||
name: Publish opendht to crates.io
|
||||
needs: create-github-release
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: crates-io
|
||||
url: https://crates.io/crates/opendht
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: cargo-edit
|
||||
- name: Set crate version from release tag
|
||||
working-directory: rust
|
||||
env:
|
||||
RELEASE_TAG: ${{ github.ref_name }}
|
||||
run: cargo set-version "${RELEASE_TAG#v}"
|
||||
- name: Authenticate with crates.io
|
||||
id: auth
|
||||
uses: rust-lang/crates-io-auth-action@v1
|
||||
- name: Publish crate
|
||||
working-directory: rust
|
||||
env:
|
||||
CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }}
|
||||
run: cargo publish --allow-dirty
|
||||
@@ -9,18 +9,16 @@ on:
|
||||
branches: [ main, master ]
|
||||
|
||||
env:
|
||||
VCPKG_VERSION: "2026.06.01"
|
||||
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]
|
||||
@@ -32,43 +30,31 @@ jobs:
|
||||
- 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
|
||||
- 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]
|
||||
@@ -80,42 +66,32 @@ jobs:
|
||||
- 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
|
||||
- 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:
|
||||
@@ -127,20 +103,23 @@ jobs:
|
||||
- name: Checkout repository
|
||||
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
|
||||
- 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]
|
||||
@@ -204,20 +185,23 @@ jobs:
|
||||
- name: Checkout repository
|
||||
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
|
||||
- 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 `
|
||||
@@ -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,64 @@ cmake -DOPENDHT_PYTHON=ON \
|
||||
| `OPENDHT_PUSH_NOTIFICATIONS` | OFF | Push notification support |
|
||||
| `OPENDHT_SANITIZE` | OFF | AddressSanitizer + stack protector |
|
||||
|
||||
## Node.js Bindings
|
||||
|
||||
Node-API bindings wrap the C++ API directly with `node-addon-api`, including
|
||||
RSA/EC keys, certificates, identities, and signed/encrypted DHT operations.
|
||||
With the native dependencies above, Node.js 22.9+, and CMake installed, run
|
||||
`npm ci`, `npm run build`, `npm test`, and `npm run typecheck` from `nodejs/`.
|
||||
This creates a separate native build without changing the existing build tree.
|
||||
See [the Node.js binding guide](nodejs/README.md) for the API and current limits.
|
||||
`npm run package` creates a platform prebuild with bundled native dependencies;
|
||||
`npm run test:package` verifies installation outside the source tree. The Node.js
|
||||
package CI builds Linux x64, macOS ARM64/x64, and Windows x64 prebuilds and checks
|
||||
the assembled npm archive on Node 22, 24, and 26 before release attachment.
|
||||
|
||||
## 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 build guide](rust/BUILD.md) for build and library-path settings.
|
||||
|
||||
`dht_infohash` and `dht_pkid` have `uint32_t` alignment, matching the native hash
|
||||
types, while retaining their 20-byte and 32-byte sizes. Rebuild consumers against
|
||||
the updated header; foreign-language declarations must match this alignment.
|
||||
|
||||
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 +181,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.
|
||||
+19
-4
@@ -10,10 +10,10 @@ if(POLICY CMP0167)
|
||||
endif()
|
||||
|
||||
set (opendht_VERSION_MAJOR 4)
|
||||
set (opendht_VERSION_MINOR 3.0)
|
||||
set (opendht_VERSION_MINOR 5.0)
|
||||
set (opendht_VERSION ${opendht_VERSION_MAJOR}.${opendht_VERSION_MINOR})
|
||||
set (PACKAGE_VERSION ${opendht_VERSION})
|
||||
set (VERSION "${opendht_VERSION}")
|
||||
set (PACKAGE_VERSION "${opendht_VERSION}-rc.1")
|
||||
set (VERSION "${PACKAGE_VERSION}")
|
||||
project (opendht
|
||||
VERSION ${opendht_VERSION}
|
||||
DESCRIPTION "OpenDHT: A lightweight C++17 Distributed Hash Table library."
|
||||
@@ -421,7 +421,7 @@ else ()
|
||||
endif ()
|
||||
set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DMSGPACK_NO_BOOST -DMSGPACK_DISABLE_LEGACY_NIL -DMSGPACK_DISABLE_LEGACY_CONVERT")
|
||||
|
||||
add_definitions(-DPACKAGE_VERSION="${opendht_VERSION}")
|
||||
add_definitions(-DPACKAGE_VERSION="${PACKAGE_VERSION}")
|
||||
|
||||
if (ASIO_INCLUDE_DIR)
|
||||
include_directories (SYSTEM "${ASIO_INCLUDE_DIR}")
|
||||
@@ -845,6 +845,15 @@ 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)
|
||||
if (MSVC)
|
||||
target_link_libraries(test_c PRIVATE ${GNUTLS_LIBRARIES})
|
||||
endif()
|
||||
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)
|
||||
@@ -875,6 +884,12 @@ if (BUILD_TESTING)
|
||||
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
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
<a id="user-content-opendht-" class="anchor" href="/savoirfairelinux/opendht/blob/master/README.md#opendht-" aria-hidden="true"></a>OpenDHT
|
||||
</h1>
|
||||
|
||||

|
||||
[](https://pypi.org/project/opendht/)
|
||||
[](https://crates.io/crates/opendht)
|
||||
[](https://www.npmjs.com/package/opendht)
|
||||
[](https://github.com/savoirfairelinux/opendht/pkgs/container/opendht%2Fopendht)
|
||||
|
||||
A lightweight C++17 Distributed Hash Table implementation.
|
||||
|
||||
@@ -17,7 +20,7 @@ Values are distributed over the network, with redundancy.
|
||||
* Public key cryptography layer providing optional data signature and encryption (using GnuTLS)
|
||||
* IPv4 and IPv6 support
|
||||
* Clean and powerful **C++17** map API
|
||||
* Bindings for **C, Rust & Python 3**
|
||||
* Bindings for **C, Rust, Node.js & Python 3**
|
||||
* REST API with optional HTTP client+server with push notification support
|
||||
|
||||
## Documentation
|
||||
|
||||
+573
-99
@@ -5,12 +5,35 @@
|
||||
|
||||
#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>;
|
||||
|
||||
static_assert(sizeof(dht_infohash) == sizeof(dht::InfoHash));
|
||||
static_assert(alignof(dht_infohash) == alignof(dht::InfoHash));
|
||||
static_assert(sizeof(dht_pkid) == sizeof(dht::PkId));
|
||||
static_assert(alignof(dht_pkid) == alignof(dht::PkId));
|
||||
|
||||
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 +179,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 +247,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 +283,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 +301,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 +365,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 +379,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 +390,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 +461,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 +531,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 +545,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 +566,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 +598,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 +631,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 +803,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 +848,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 +897,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 +971,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 +986,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 +1007,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 +1027,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 +1073,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
|
||||
+104
-2
@@ -39,9 +39,17 @@ OPENDHT_C_PUBLIC dht_data_view dht_blob_get_data(const dht_blob* data);
|
||||
OPENDHT_C_PUBLIC void dht_blob_delete(dht_blob* data);
|
||||
|
||||
// dht::InfoHash
|
||||
#ifdef __cplusplus
|
||||
#define OPENDHT_C_ALIGN_UINT32 alignas(uint32_t)
|
||||
#elif defined(_MSC_VER)
|
||||
#define OPENDHT_C_ALIGN_UINT32 __declspec(align(4))
|
||||
#else
|
||||
#define OPENDHT_C_ALIGN_UINT32 _Alignas(uint32_t)
|
||||
#endif
|
||||
|
||||
struct OPENDHT_C_PUBLIC dht_infohash
|
||||
{
|
||||
uint8_t d[HASH_LEN];
|
||||
OPENDHT_C_ALIGN_UINT32 uint8_t d[HASH_LEN];
|
||||
};
|
||||
typedef struct dht_infohash dht_infohash;
|
||||
OPENDHT_C_PUBLIC void dht_infohash_zero(dht_infohash* h);
|
||||
@@ -56,9 +64,10 @@ OPENDHT_C_PUBLIC bool dht_infohash_is_zero(const dht_infohash* h);
|
||||
// dht::PkId
|
||||
struct OPENDHT_C_PUBLIC dht_pkid
|
||||
{
|
||||
uint8_t d[32];
|
||||
OPENDHT_C_ALIGN_UINT32 uint8_t d[32];
|
||||
};
|
||||
typedef struct dht_pkid dht_pkid;
|
||||
#undef OPENDHT_C_ALIGN_UINT32
|
||||
OPENDHT_C_PUBLIC const char* dht_pkid_print(const dht_pkid* h);
|
||||
|
||||
// dht::crypto::PublicKey
|
||||
@@ -67,6 +76,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 +88,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 +101,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 +115,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 +126,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 +134,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 +228,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 +300,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
|
||||
}
|
||||
|
||||
+12
-11
@@ -5,15 +5,16 @@ LABEL org.opencontainers.image.source=https://github.com/savoirfairelinux/opendh
|
||||
COPY . opendht
|
||||
|
||||
RUN cd opendht && mkdir build && cd build \
|
||||
&& cmake .. -DCMAKE_INSTALL_PREFIX=/usr \
|
||||
-DBUILD_TESTING=Off \
|
||||
-DCMAKE_INTERPROCEDURAL_OPTIMIZATION=On \
|
||||
-DOPENDHT_C=On \
|
||||
-DOPENDHT_PEER_DISCOVERY=On \
|
||||
-DOPENDHT_PYTHON=On \
|
||||
-DOPENDHT_TOOLS=On \
|
||||
-DOPENDHT_PROXY_SERVER=On \
|
||||
-DOPENDHT_PROXY_CLIENT=On \
|
||||
-DOPENDHT_SYSTEMD=On \
|
||||
&& make -j8 && make install \
|
||||
&& cmake -GNinja .. -DCMAKE_INSTALL_PREFIX=/usr \
|
||||
-DBUILD_TESTING=Off \
|
||||
-DCMAKE_INTERPROCEDURAL_OPTIMIZATION=On \
|
||||
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
|
||||
-DOPENDHT_C=On \
|
||||
-DOPENDHT_PEER_DISCOVERY=On \
|
||||
-DOPENDHT_PYTHON=On \
|
||||
-DOPENDHT_TOOLS=On \
|
||||
-DOPENDHT_PROXY_SERVER=On \
|
||||
-DOPENDHT_PROXY_CLIENT=On \
|
||||
-DOPENDHT_SYSTEMD=On \
|
||||
&& ninja && ninja install \
|
||||
&& cd ../.. && rm -rf opendht
|
||||
@@ -10,6 +10,7 @@ ENV DESTDIR=/install
|
||||
RUN cd opendht && mkdir build && cd build \
|
||||
&& cmake .. -DCMAKE_INSTALL_PREFIX=/usr \
|
||||
-DCMAKE_INTERPROCEDURAL_OPTIMIZATION=On \
|
||||
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
|
||||
-DBUILD_TESTING=Off \
|
||||
-DOPENDHT_C=On \
|
||||
-DOPENDHT_PEER_DISCOVERY=On \
|
||||
@@ -20,7 +21,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
|
||||
|
||||
@@ -8,23 +8,25 @@ RUN apt-get update && apt-get install -y \
|
||||
&& echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
build-essential pkg-config cmake git wget meson \
|
||||
build-essential pkg-config cmake git wget meson ninja-build \
|
||||
libtool \
|
||||
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
|
||||
|
||||
@@ -7,7 +7,7 @@ RUN apk add --no-cache \
|
||||
cython python3-dev py3-setuptools \
|
||||
ncurses-dev readline-dev nettle-dev \
|
||||
cppunit-dev gnutls-dev jsoncpp-dev \
|
||||
argon2-dev openssl-dev fmt-dev \
|
||||
argon2-dev openssl-dev fmt-dev simdutf-dev \
|
||||
llhttp-dev asio-dev msgpack-cxx-dev \
|
||||
&& rm -rf /var/cache/apk/*
|
||||
|
||||
@@ -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 . \
|
||||
|
||||
@@ -18,15 +18,29 @@ RUN apt-get update && apt-get install -y \
|
||||
|
||||
RUN pip3 install meson Cython
|
||||
|
||||
ARG SIMDUTF_VERSION=8.0.0
|
||||
RUN mkdir simdutf \
|
||||
&& wget -q "https://github.com/simdutf/simdutf/archive/refs/tags/v${SIMDUTF_VERSION}.tar.gz" -O simdutf.tar.gz \
|
||||
&& tar -xzf simdutf.tar.gz -C simdutf --strip-components=1 \
|
||||
&& cmake -S simdutf -B simdutf/build -DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_INSTALL_PREFIX=/usr/local -DCMAKE_POSITION_INDEPENDENT_CODE=ON \
|
||||
-DBUILD_SHARED_LIBS=OFF -DSIMDUTF_TESTS=OFF -DSIMDUTF_TOOLS=OFF \
|
||||
&& cmake --build simdutf/build --parallel 2 \
|
||||
&& cmake --install simdutf/build \
|
||||
&& install -D simdutf/LICENSE-MIT /usr/share/doc/simdutf/LICENSE-MIT \
|
||||
&& install -D simdutf/LICENSE-APACHE /usr/share/doc/simdutf/LICENSE-APACHE \
|
||||
&& rm -rf simdutf simdutf.tar.gz
|
||||
|
||||
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 . \
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
FROM ubuntu:24.04
|
||||
LABEL maintainer="Adrien Béraud <adrien.beraud@savoirfairelinux.com>"
|
||||
LABEL org.opencontainers.image.source=https://github.com/savoirfairelinux/opendht
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
dialog apt-utils \
|
||||
&& apt-get clean \
|
||||
&& echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
build-essential pkg-config cmake git wget meson ninja-build patchelf \
|
||||
libtool \
|
||||
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-cxx-dev \
|
||||
&& apt-get clean && rm -rf /var/lib/apt/lists/* /var/cache/apt/*
|
||||
|
||||
ARG LLHTTP_VERSION=9.2.1
|
||||
RUN mkdir llhttp \
|
||||
&& wget -q "https://github.com/nodejs/llhttp/archive/refs/tags/release/v${LLHTTP_VERSION}.tar.gz" -O llhttp.tar.gz \
|
||||
&& tar -xzf llhttp.tar.gz -C llhttp --strip-components=1 \
|
||||
&& cmake -S llhttp -B llhttp/build -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr \
|
||||
&& cmake --build llhttp/build --parallel 2 \
|
||||
&& cmake --install llhttp/build \
|
||||
&& rm -rf llhttp llhttp.tar.gz
|
||||
|
||||
ARG SIMDUTF_VERSION=8.0.0
|
||||
RUN mkdir simdutf \
|
||||
&& wget -q "https://github.com/simdutf/simdutf/archive/refs/tags/v${SIMDUTF_VERSION}.tar.gz" -O simdutf.tar.gz \
|
||||
&& tar -xzf simdutf.tar.gz -C simdutf --strip-components=1 \
|
||||
&& cmake -S simdutf -B simdutf/build -DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_INSTALL_PREFIX=/usr/local -DCMAKE_POSITION_INDEPENDENT_CODE=ON \
|
||||
-DBUILD_SHARED_LIBS=OFF -DSIMDUTF_TESTS=OFF -DSIMDUTF_TOOLS=OFF \
|
||||
&& cmake --build simdutf/build --parallel 2 \
|
||||
&& cmake --install simdutf/build \
|
||||
&& install -D simdutf/LICENSE-MIT /usr/share/doc/simdutf/LICENSE-MIT \
|
||||
&& install -D simdutf/LICENSE-APACHE /usr/share/doc/simdutf/LICENSE-APACHE \
|
||||
&& rm -rf simdutf simdutf.tar.gz
|
||||
|
||||
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${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 . \
|
||||
&& make -j2 && make install \
|
||||
&& cd ../../ && rm -rf restinio* \
|
||||
&& ldconfig
|
||||
@@ -8,6 +8,7 @@ RUN cd opendht && mkdir build && cd build \
|
||||
&& cmake .. -DCMAKE_INSTALL_PREFIX=/usr \
|
||||
-DBUILD_TESTING=Off \
|
||||
-DCMAKE_INTERPROCEDURAL_OPTIMIZATION=On \
|
||||
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
|
||||
-DOPENDHT_C=On \
|
||||
-DOPENDHT_PEER_DISCOVERY=On \
|
||||
-DOPENDHT_PYTHON=On \
|
||||
|
||||
@@ -7,6 +7,7 @@ COPY . opendht
|
||||
RUN cd opendht && mkdir build && cd build \
|
||||
&& cmake .. -DCMAKE_INSTALL_PREFIX=/usr \
|
||||
-DCMAKE_INTERPROCEDURAL_OPTIMIZATION=On \
|
||||
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
|
||||
-DOPENDHT_C=On \
|
||||
-DOPENDHT_PEER_DISCOVERY=On \
|
||||
-DOPENDHT_PYTHON=On \
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
FROM ghcr.io/savoirfairelinux/opendht/opendht-deps-noble:latest
|
||||
LABEL maintainer="Adrien Béraud <adrien.beraud@savoirfairelinux.com>"
|
||||
LABEL org.opencontainers.image.source=https://github.com/savoirfairelinux/opendht
|
||||
|
||||
COPY . opendht
|
||||
|
||||
RUN cd opendht && mkdir build && cd build \
|
||||
&& cmake .. -DCMAKE_INSTALL_PREFIX=/usr \
|
||||
-DBUILD_TESTING=Off \
|
||||
-DCMAKE_INTERPROCEDURAL_OPTIMIZATION=On \
|
||||
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
|
||||
-DOPENDHT_C=On \
|
||||
-DOPENDHT_PEER_DISCOVERY=On \
|
||||
-DOPENDHT_PYTHON=On \
|
||||
-DOPENDHT_TOOLS=On \
|
||||
-DOPENDHT_PROXY_SERVER=On \
|
||||
-DOPENDHT_PROXY_CLIENT=On \
|
||||
-DOPENDHT_SYSTEMD=On \
|
||||
&& make -j8 && make install \
|
||||
&& cd ../.. && rm -rf opendht
|
||||
@@ -19,7 +19,9 @@
|
||||
#include <mutex>
|
||||
|
||||
namespace dht {
|
||||
enum class PushType { None = 0, Android, iOS, UnifiedPush };
|
||||
// Appended only: the values are serialized as integers by MSGPACK_ADD_ENUM,
|
||||
// so reordering would break compatibility with deployed proxies.
|
||||
enum class PushType { None = 0, Android, iOS, UnifiedPush, Huawei };
|
||||
}
|
||||
MSGPACK_ADD_ENUM(dht::PushType)
|
||||
|
||||
@@ -132,6 +134,7 @@ public:
|
||||
PushStats androidPush;
|
||||
PushStats iosPush;
|
||||
PushStats unifiedPush;
|
||||
PushStats huaweiPush;
|
||||
|
||||
/** Average requests per second */
|
||||
double requestRate {0};
|
||||
@@ -272,6 +275,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)
|
||||
{}
|
||||
@@ -341,20 +347,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);
|
||||
|
||||
@@ -398,6 +400,7 @@ private:
|
||||
PushStats androidPush_;
|
||||
PushStats iosPush_;
|
||||
PushStats unifiedPush_;
|
||||
PushStats huaweiPush_;
|
||||
|
||||
// Thread-safe access to listeners map.
|
||||
std::mutex lockListener_;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
+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
|
||||
{
|
||||
|
||||
+44
-4
@@ -5,7 +5,7 @@ project(
|
||||
'cython',
|
||||
license: 'MIT',
|
||||
license_files: 'LICENSE',
|
||||
version: '4.3.0',
|
||||
version: '4.5.0-rc.1',
|
||||
meson_version: '>=1.1.0',
|
||||
default_options: ['cpp_std=c++20', 'warning_level=3'],
|
||||
)
|
||||
@@ -55,7 +55,12 @@ if host_machine.system() == 'linux' and io_uring.found()
|
||||
endif
|
||||
conf_data = configuration_data()
|
||||
|
||||
add_project_arguments('-DMSGPACK_NO_BOOST', '-DASIO_STANDALONE', language: 'cpp')
|
||||
add_project_arguments(
|
||||
'-DMSGPACK_NO_BOOST',
|
||||
'-DASIO_STANDALONE',
|
||||
'-DPACKAGE_VERSION="' + meson.project_version() + '"',
|
||||
language: 'cpp',
|
||||
)
|
||||
if io_uring.found()
|
||||
add_project_arguments('-DASIO_HAS_IO_URING', '-DASIO_DISABLE_EPOLL', language: 'cpp')
|
||||
conf_data.set('iouring_lib', ', liburing')
|
||||
@@ -209,15 +214,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()
|
||||
@@ -256,6 +268,16 @@ 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],
|
||||
dependencies: [gnutls],
|
||||
link_with: opendht_c,
|
||||
)
|
||||
test('CBindings', test_c)
|
||||
endif
|
||||
cppunit = dependency('cppunit')
|
||||
|
||||
test_infohash = executable(
|
||||
@@ -292,6 +314,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,5 @@
|
||||
node_modules/
|
||||
build*/
|
||||
prebuilds/
|
||||
dist/
|
||||
examples/dhtnode.js
|
||||
@@ -0,0 +1,64 @@
|
||||
cmake_minimum_required(VERSION 3.21)
|
||||
project(opendht_node LANGUAGES C CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
||||
set(OPENDHT_TOOLS OFF CACHE BOOL "Build DHT tools")
|
||||
set(OPENDHT_PYTHON OFF CACHE BOOL "Build Python bindings")
|
||||
set(OPENDHT_PROXY_CLIENT ON CACHE BOOL "Enable DHT proxy client")
|
||||
set(OPENDHT_PEER_DISCOVERY OFF CACHE BOOL "Enable multicast peer discovery")
|
||||
set(OPENDHT_DOCUMENTATION OFF CACHE BOOL "Build documentation")
|
||||
set(BUILD_TESTING OFF CACHE BOOL "Build native tests")
|
||||
add_subdirectory(.. native EXCLUDE_FROM_ALL)
|
||||
|
||||
find_program(NODE_EXECUTABLE node REQUIRED)
|
||||
execute_process(
|
||||
COMMAND "${NODE_EXECUTABLE}" --input-type=module -e "import addon from 'node-addon-api'; process.stdout.write(addon.include_dir)"
|
||||
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
|
||||
OUTPUT_VARIABLE NODE_ADDON_API_DIR
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
COMMAND_ERROR_IS_FATAL ANY
|
||||
)
|
||||
|
||||
add_library(opendht_node MODULE addon.cpp crypto.cpp ${CMAKE_JS_SRC})
|
||||
target_compile_definitions(opendht_node PRIVATE NAPI_VERSION=8 NAPI_CPP_EXCEPTIONS)
|
||||
target_include_directories(opendht_node PRIVATE
|
||||
${CMAKE_JS_INC}
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/${NODE_ADDON_API_DIR}"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/../include"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/native/include"
|
||||
)
|
||||
target_link_libraries(opendht_node PRIVATE opendht ${CMAKE_JS_LIB})
|
||||
if(OPENDHT_SANITIZE AND CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
|
||||
target_compile_options(opendht_node PRIVATE -fsanitize=address -fstack-protector-strong)
|
||||
target_link_options(opendht_node PRIVATE -fsanitize=address)
|
||||
endif()
|
||||
set_target_properties(opendht_node PROPERTIES
|
||||
PREFIX ""
|
||||
SUFFIX ".node"
|
||||
OUTPUT_NAME "opendht"
|
||||
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/$<CONFIG>"
|
||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/$<CONFIG>"
|
||||
)
|
||||
if(APPLE)
|
||||
target_link_options(opendht_node PRIVATE "-undefined" "dynamic_lookup")
|
||||
endif()
|
||||
if(WIN32)
|
||||
add_custom_command(TARGET opendht_node POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
$<TARGET_RUNTIME_DLLS:opendht_node> $<TARGET_FILE_DIR:opendht_node>
|
||||
COMMAND_EXPAND_LISTS
|
||||
)
|
||||
endif()
|
||||
if(MSVC)
|
||||
target_compile_options(opendht_node PRIVATE /EHsc)
|
||||
target_include_directories(opendht_node PRIVATE
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/../src/compat/msvc"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/../src/compat/win32"
|
||||
)
|
||||
if(CMAKE_JS_NODELIB_DEF AND CMAKE_JS_NODELIB_TARGET)
|
||||
execute_process(COMMAND ${CMAKE_AR} /def:${CMAKE_JS_NODELIB_DEF}
|
||||
/out:${CMAKE_JS_NODELIB_TARGET} ${CMAKE_STATIC_LINKER_FLAGS})
|
||||
endif()
|
||||
endif()
|
||||
@@ -0,0 +1,243 @@
|
||||
# OpenDHT Node.js Bindings
|
||||
|
||||
Initial Node-API bindings using `node-addon-api` directly over the C++ library.
|
||||
Node.js 22.9 or later is required; the addon targets Node-API version 8.
|
||||
|
||||
## Install
|
||||
|
||||
```sh
|
||||
npm install opendht
|
||||
```
|
||||
|
||||
## Build
|
||||
|
||||
Install the native dependencies described in [BUILD.md](../BUILD.md), plus Node.js,
|
||||
a C++17 compiler, and CMake 3.21 or later. Ninja is optional. Proxy-client support
|
||||
defaults to enabled and requires ASIO, llhttp, RESTinio, and expected-lite headers.
|
||||
|
||||
```sh
|
||||
cd nodejs
|
||||
npm ci
|
||||
npm run build
|
||||
npm test
|
||||
npm run typecheck
|
||||
```
|
||||
|
||||
CMake.js builds OpenDHT from the parent source directory in `nodejs/build/native`.
|
||||
It does not change the repository's existing build directory. Tools, Python,
|
||||
peer discovery, documentation, and native tests default to disabled in this build.
|
||||
Native CMake options can be passed through, for example:
|
||||
|
||||
```sh
|
||||
npm run build -- --CDOPENDHT_PEER_DISCOVERY=ON
|
||||
```
|
||||
|
||||
For an existing build, change cached options directly with CMake, for example
|
||||
`cmake -S . -B build -DOPENDHT_PROXY_CLIENT=ON`, then rebuild. A custom build can
|
||||
disable proxy support with `-DOPENDHT_PROXY_CLIENT=OFF`; requesting a proxy then
|
||||
throws an explicit unsupported-build error.
|
||||
|
||||
The loader checks local builds, then compatible Node-API binaries under `prebuilds/`.
|
||||
Set `OPENDHT_NODE_BINDING` to an absolute addon path to select an explicit build.
|
||||
Packaged binaries include OpenDHT and non-system native dependencies, so archive
|
||||
installation does not require a compiler or a system OpenDHT installation.
|
||||
|
||||
## Usage
|
||||
|
||||
```js
|
||||
import { DhtRunner, hash } from 'opendht';
|
||||
|
||||
async function main() {
|
||||
const writer = new DhtRunner();
|
||||
const reader = new DhtRunner();
|
||||
try {
|
||||
writer.run({ port: 0, ipv4: '127.0.0.1', ipv6: '::1' });
|
||||
reader.run({ port: 0, ipv4: '127.0.0.1', ipv6: '::1' });
|
||||
writer.bootstrap('127.0.0.1', reader.getBoundPort());
|
||||
reader.bootstrap('127.0.0.1', writer.getBoundPort());
|
||||
const key = hash('example');
|
||||
const stored = await writer.put(key, Buffer.from('hello'), { id: 42n });
|
||||
if (!stored) throw new Error('DHT put failed');
|
||||
for await (const value of reader.get(key)) {
|
||||
console.log(value.id, value.data.toString());
|
||||
}
|
||||
} finally {
|
||||
await Promise.all([writer.close(), reader.close()]);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
- `hash(string | Uint8Array)` returns a SHA-1 hash as a 40-character hex string.
|
||||
All get/put/listen keys must be hex hashes, not unhashed application keys.
|
||||
- `version` is the underlying OpenDHT version.
|
||||
- `run({port, ipv4, ipv6, network, persistPath, identity})` starts a threaded node. Port 0
|
||||
selects a free port. The defaults bind both IP families on all interfaces.
|
||||
`persistPath` is optional; use an empty string to disable persistence.
|
||||
`proxyServer` and `proxyUserAgent` configure proxy mode (see below).
|
||||
- `getBoundPort(4 | 6)` defaults to IPv4; `getNodeId()` returns the node's hex ID.
|
||||
- `getStatus()` returns the latest JS-dispatched `{ipv4, ipv6}` connectivity
|
||||
snapshot. Each family is `'disconnected'`, `'connecting'`, or `'connected'`.
|
||||
`runner.on('statusChanged', status => {})` receives changes on the JS thread.
|
||||
New and closing/closed runners report both families as disconnected. No status
|
||||
events are delivered after close begins; close does not emit a final event.
|
||||
- `bootstrap(host, port)` queues bootstrap discovery. Returning does not mean the
|
||||
peer is connected; subsequent DHT operations report their own completion.
|
||||
- `put(key, Uint8Array, {id, type, userType, permanent})` copies the input and
|
||||
resolves to the native success boolean. IDs are unsigned 64-bit `bigint` values.
|
||||
- `get(key)` returns an async iterator of values as they arrive. The iterator
|
||||
ends when the lookup completes, or rejects with `ERR_DHT_GET_FAILED` on failure.
|
||||
`getAll(key)` returns a single promise resolving to the complete value array.
|
||||
Values contain `id`, `type`, `seq`, `userType`, copied `Buffer` data, `isSigned`,
|
||||
`isEncrypted`, and an `owner` long fingerprint (or null for unsigned values).
|
||||
- `listen(key, (value, expired) => {})` returns a listener with idempotent
|
||||
`cancel()`. Queued deliveries are suppressed after cancellation. Callback
|
||||
return values are ignored; callbacks may cancel or close the runner.
|
||||
- `listen(key)` without a callback returns an async iterator of `{value, expired}`
|
||||
events. It continues until cancelled or the runner closes.
|
||||
- `close()` is idempotent and returns the same promise on repeated calls. It
|
||||
waits off the JS thread for graceful shutdown, then joins the native threads.
|
||||
After two seconds it forces the join if graceful shutdown has not completed.
|
||||
Pending operations reject with `ERR_DHT_CLOSED`. A closed runner cannot restart.
|
||||
- `ref()` and `unref()` control whether the runner keeps Node's event loop alive.
|
||||
Running nodes are referenced by default. `Symbol.asyncDispose` calls `close()`.
|
||||
|
||||
Callbacks run only on their owning JS thread. There is no polling timer or worker
|
||||
thread waiting on each get/put. Explicitly close runners; GC cleanup and Node
|
||||
environment cleanup hooks are fallbacks, including forced worker termination.
|
||||
Pending operations, active listeners, and registered status handlers keep their
|
||||
runner alive until completion, cancellation, handler removal, or explicit close.
|
||||
Graceful persistence cannot be guaranteed if the process is killed.
|
||||
|
||||
The binding does not cap the callback queue, accumulated `getAll()` results, or
|
||||
iterator buffers. Slow iterator consumers can accumulate values without
|
||||
backpressure. Queued callbacks retain their native values until delivery or cleanup.
|
||||
|
||||
## Async Iteration
|
||||
|
||||
`get()` and callback-free `listen()` start immediately and return iterators with
|
||||
`next()`, `return()`, `throw()`, `cancel()`, and `Symbol.asyncDispose`.
|
||||
Leaving a `for await` loop with `break` or an exception cancels the subscription.
|
||||
Explicit cancellation discards buffered values and settles pending reads as done.
|
||||
Closing the runner rejects active get iterators with `ERR_DHT_CLOSED`; listener
|
||||
iterators end normally. Successful get completion preserves unread buffered values.
|
||||
|
||||
```js
|
||||
for await (const value of runner.get(hash('example'))) {
|
||||
console.log(value.id, value.data);
|
||||
}
|
||||
const snapshots = await runner.getAll(hash('example'));
|
||||
|
||||
for await (const { value, expired } of runner.listen(hash('updates'))) {
|
||||
console.log(value.id, expired);
|
||||
if (expired) break;
|
||||
}
|
||||
```
|
||||
|
||||
## Proxy Configuration
|
||||
|
||||
Pass the URL of an OpenDHT HTTP(S) proxy at startup:
|
||||
|
||||
```js
|
||||
runner.run({
|
||||
proxyServer: 'https://proxy.example.com',
|
||||
proxyUserAgent: 'my-app/1.0',
|
||||
});
|
||||
```
|
||||
|
||||
This selects the native OpenDHT proxy client, not an HTTP CONNECT or SOCKS proxy.
|
||||
The node does not bind UDP sockets in this mode; `port`, `ipv4`, `ipv6`, and
|
||||
`bootstrap()` are for direct DHT nodes. Omit `proxyServer` or use an empty string
|
||||
for direct networking. Proxy settings are startup-only; create a new runner to
|
||||
change transports or servers. Packaged binaries include proxy-client support.
|
||||
|
||||
## Crypto
|
||||
|
||||
`PrivateKey.generate(options)` and `Identity.generate(name, options)` return
|
||||
promises and generate keys off the JS thread. Options accept `algorithm: 'ec'`
|
||||
(the default, using OpenDHT's default EC curve) or `'rsa'`. RSA `bits` must be an
|
||||
integer between 2048 and 8192; the default is 2048.
|
||||
|
||||
- `new PrivateKey(bytes, password?)` imports PEM/DER; `serialize(password?)`
|
||||
exports PKCS#8 PEM. A nonempty password encrypts the exported key.
|
||||
- `privateKey.getPublicKey()` returns an owned public key. `sign(bytes)` and
|
||||
`decrypt(bytes)` return copied Buffers.
|
||||
- `new PublicKey(bytes)` imports a public key; `serialize()` exports DER.
|
||||
`checkSignature(data, signature)` returns a boolean. `encrypt(bytes)` requires
|
||||
an RSA key; EC keys support signatures, not this encryption scheme.
|
||||
- `new Certificate(bytes)` imports PEM/DER or a PEM certificate chain;
|
||||
`serialize()` exports a PEM chain. Import does not establish trust or validate
|
||||
the chain against a trust store. `getPublicKey()` returns its public key.
|
||||
- Public keys and certificates expose `getId()` (40 hex characters) and
|
||||
`getLongId()` (64 hex characters).
|
||||
- `new Identity(privateKey, certificate)` requires matching fingerprints.
|
||||
Its read-only `privateKey` and `certificate` properties return owned wrappers.
|
||||
- Pass `identity` to `run()` to enable `putSigned(key, data, options?)` and
|
||||
`putEncrypted(key, data, recipientPublicKey, options?)`. Both return the same
|
||||
success boolean as `put()`. Encrypted values are signed by the sender and
|
||||
transparently decrypted for the recipient by OpenDHT's get/listen layer.
|
||||
|
||||
```js
|
||||
import { Identity, DhtRunner } from 'opendht';
|
||||
|
||||
async function createAuthenticatedNode() {
|
||||
const identity = await Identity.generate('example', { algorithm: 'rsa', bits: 2048 });
|
||||
const runner = new DhtRunner();
|
||||
runner.run({ port: 0, identity });
|
||||
return { runner, identity };
|
||||
}
|
||||
```
|
||||
|
||||
Crypto methods take Uint8Array/Buffer inputs, not implicit text encodings. Imports
|
||||
must be nonempty. Malformed handles/data,
|
||||
mismatched identities, and unsupported encryption keys raise JS exceptions.
|
||||
Only generation is asynchronous; import/export/sign/encrypt/decrypt are
|
||||
synchronous and should be used from worker threads for large workloads.
|
||||
|
||||
## Packaging And CI
|
||||
|
||||
From a successful Release build:
|
||||
|
||||
```sh
|
||||
npm run package
|
||||
npm run test:package
|
||||
```
|
||||
|
||||
`bundle` copies the addon into `prebuilds/<platform>-<arch>/`, gathers its runtime
|
||||
dependencies, rewrites native loader paths, and copies dependency license
|
||||
notices. macOS needs `dylibbundler` and Apple developer tools; Linux needs
|
||||
`patchelf` and Debian/Ubuntu dependency copyright files. Windows uses the vcpkg
|
||||
installed tree (`VCPKG_INSTALLED_DIR`, `VCPKG_DEFAULT_TRIPLET`) and MSVC tools.
|
||||
`OPENDHT_LIBRARY_PATH` adds native library search directories when bundling.
|
||||
An explicit addon path may be passed to `npm run bundle -- /absolute/addon.node`.
|
||||
Bundling replaces only the current platform's generated prebuild directory.
|
||||
|
||||
`test:package` creates an npm archive, installs it in a temporary consumer with
|
||||
install scripts disabled and no dev dependencies, forces prebuild-only loading,
|
||||
and runs the full runtime suite. Pass an archive file or a directory containing
|
||||
one archive to test it directly, for example `npm run test:package -- dist`.
|
||||
Only runtime JS, declarations, the example's TypeScript source, documentation,
|
||||
and prebuilds are included in the archive. `prepack` compiles the example, so
|
||||
archive creation needs the development dependencies; archive installation does not.
|
||||
|
||||
The [Node.js package workflow](../.github/workflows/nodejs-package.yml) builds:
|
||||
|
||||
| Target | Build Baseline |
|
||||
| --- | --- |
|
||||
| Linux x64 | Ubuntu 24.04 / glibc 2.39 (Noble dependency image) |
|
||||
| macOS ARM64 and x64 | macOS 15 |
|
||||
| Windows x64 | Windows Server 2022 / MSVC |
|
||||
|
||||
CI combines platform prebuilds into one npm archive and verifies that archive on
|
||||
Node 22, 24, and 26 on each platform, including Ubuntu 24.04 and 26.04. Branch
|
||||
pushes, pull requests, and manual runs build and test without publishing.
|
||||
musl/Alpine and other architectures are not currently built. Node-API stability
|
||||
does not remove OS/libc or native dependency compatibility requirements. Review
|
||||
the included dependency licenses and source-distribution obligations before
|
||||
redistributing binaries.
|
||||
|
||||
Query filters, AbortSignal support, certificate trust-store validation, CA
|
||||
issuance controls, and runtime proxy switching remain future work.
|
||||
@@ -0,0 +1,734 @@
|
||||
#include <napi.h>
|
||||
#include <opendht.h>
|
||||
#include <gnutls/gnutls.h>
|
||||
#include "crypto.h"
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <condition_variable>
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace {
|
||||
|
||||
struct GnuTlsContext
|
||||
{
|
||||
const int result = gnutls_global_init();
|
||||
|
||||
~GnuTlsContext()
|
||||
{
|
||||
if (result == GNUTLS_E_SUCCESS)
|
||||
gnutls_global_deinit();
|
||||
}
|
||||
};
|
||||
|
||||
using opendht_node::guarded;
|
||||
|
||||
uint32_t
|
||||
unsignedNumber(Napi::Value value, uint32_t maximum, const char* name)
|
||||
{
|
||||
if (!value.IsNumber())
|
||||
throw Napi::TypeError::New(value.Env(), std::string(name) + " must be a number");
|
||||
const auto number = value.As<Napi::Number>().DoubleValue();
|
||||
if (!std::isfinite(number) || number < 0 || number > maximum || std::floor(number) != number)
|
||||
throw Napi::RangeError::New(value.Env(), std::string(name) + " is out of range");
|
||||
return static_cast<uint32_t>(number);
|
||||
}
|
||||
|
||||
uint64_t
|
||||
unsignedBigInt(Napi::Value value)
|
||||
{
|
||||
if (!value.IsBigInt())
|
||||
throw Napi::TypeError::New(value.Env(), "id must be a bigint");
|
||||
int sign = 0;
|
||||
size_t count = 1;
|
||||
uint64_t word = 0;
|
||||
value.As<Napi::BigInt>().ToWords(&sign, &count, &word);
|
||||
if (sign || count > 1)
|
||||
throw Napi::RangeError::New(value.Env(), "id must fit in an unsigned 64-bit integer");
|
||||
return word;
|
||||
}
|
||||
|
||||
std::string
|
||||
text(Napi::Value value, const char* name)
|
||||
{
|
||||
if (!value.IsString())
|
||||
throw Napi::TypeError::New(value.Env(), std::string(name) + " must be a string");
|
||||
auto result = value.As<Napi::String>().Utf8Value();
|
||||
if (result.find('\0') != std::string::npos)
|
||||
throw Napi::TypeError::New(value.Env(), std::string(name) + " must not contain NUL bytes");
|
||||
return result;
|
||||
}
|
||||
|
||||
dht::InfoHash
|
||||
infoHash(Napi::Value value)
|
||||
{
|
||||
const auto result = text(value, "hash");
|
||||
if (result.size() != 40 || result.find_first_not_of("0123456789abcdefABCDEF") != std::string::npos)
|
||||
throw Napi::TypeError::New(value.Env(), "Expected a 40-character hexadecimal hash");
|
||||
return dht::InfoHash(result);
|
||||
}
|
||||
|
||||
Napi::Object
|
||||
optionsObject(Napi::Value value)
|
||||
{
|
||||
if (!value.IsObject() || value.IsArray() || value.IsNull())
|
||||
throw Napi::TypeError::New(value.Env(), "options must be an object");
|
||||
return value.As<Napi::Object>();
|
||||
}
|
||||
|
||||
Napi::Object
|
||||
valueToJS(Napi::Env env, const dht::Value& value)
|
||||
{
|
||||
auto result = Napi::Object::New(env);
|
||||
result.Set("id", Napi::BigInt::New(env, value.id));
|
||||
result.Set("type", Napi::Number::New(env, value.type));
|
||||
result.Set("seq", Napi::Number::New(env, value.seq));
|
||||
result.Set("data", Napi::Buffer<uint8_t>::Copy(env, value.data.data(), value.data.size()));
|
||||
result.Set("userType", Napi::String::New(env, value.user_type));
|
||||
result.Set("owner",
|
||||
value.owner ? Napi::Value(Napi::String::New(env, value.owner->getLongId().to_view()))
|
||||
: Napi::Value(env.Null()));
|
||||
result.Set("isSigned", Napi::Boolean::New(env, value.isSigned()));
|
||||
result.Set("isEncrypted", Napi::Boolean::New(env, value.isEncrypted()));
|
||||
return result;
|
||||
}
|
||||
|
||||
struct PendingOperation
|
||||
{
|
||||
Napi::Promise::Deferred deferred;
|
||||
bool get;
|
||||
std::vector<std::shared_ptr<const dht::Value>> values;
|
||||
Napi::FunctionReference onValue;
|
||||
std::shared_ptr<std::atomic_bool> cancelled;
|
||||
};
|
||||
|
||||
struct Subscription
|
||||
{
|
||||
dht::InfoHash key;
|
||||
std::shared_future<size_t> token;
|
||||
Napi::FunctionReference callback;
|
||||
};
|
||||
|
||||
struct RunnerState
|
||||
{
|
||||
using Callback = std::function<void(RunnerState&)>;
|
||||
|
||||
explicit RunnerState(Napi::Env environment)
|
||||
: env(environment)
|
||||
{}
|
||||
|
||||
static std::shared_ptr<RunnerState> create(Napi::Env env)
|
||||
{
|
||||
auto state = std::make_shared<RunnerState>(env);
|
||||
auto dispatchOwner = std::make_unique<std::shared_ptr<RunnerState>>(state);
|
||||
napi_threadsafe_function function;
|
||||
if (napi_create_threadsafe_function(env,
|
||||
nullptr,
|
||||
nullptr,
|
||||
Napi::String::New(env, "OpenDHT callbacks"),
|
||||
0,
|
||||
1,
|
||||
dispatchOwner.get(),
|
||||
finalizeDispatcher,
|
||||
state.get(),
|
||||
dispatch,
|
||||
&function)
|
||||
!= napi_ok)
|
||||
throw Napi::Error::New(env, "Could not create OpenDHT callback dispatcher");
|
||||
dispatchOwner.release();
|
||||
state->dispatcher = Napi::ThreadSafeFunction(function);
|
||||
state->dispatcher.Unref(env);
|
||||
auto closeOwner = std::make_unique<std::shared_ptr<RunnerState>>(state);
|
||||
if (napi_create_async_work(env,
|
||||
nullptr,
|
||||
Napi::String::New(env, "OpenDHT close"),
|
||||
executeClose,
|
||||
completeClose,
|
||||
closeOwner.get(),
|
||||
&state->closeWork)
|
||||
!= napi_ok) {
|
||||
state->dispatcher.Release();
|
||||
throw Napi::Error::New(env, "Could not create OpenDHT cleanup work");
|
||||
}
|
||||
if (napi_add_async_cleanup_hook(env, cleanup, state.get(), &state->cleanupHook) != napi_ok) {
|
||||
napi_delete_async_work(env, state->closeWork);
|
||||
state->dispatcher.Release();
|
||||
throw Napi::Error::New(env, "Could not register OpenDHT cleanup hook");
|
||||
}
|
||||
state->closeOwner = closeOwner.release();
|
||||
return state;
|
||||
}
|
||||
|
||||
void requireRunning() const
|
||||
{
|
||||
if (closing)
|
||||
throw Napi::Error::New(env, "DHT runner is closed");
|
||||
if (!started)
|
||||
throw Napi::Error::New(env, "DHT runner is not running");
|
||||
}
|
||||
|
||||
void startClose()
|
||||
{
|
||||
if (closing.exchange(true))
|
||||
return;
|
||||
if (napi_queue_async_work(env, closeWork) != napi_ok) {
|
||||
executeClose(env, closeOwner);
|
||||
completeClose(env, napi_ok, closeOwner);
|
||||
}
|
||||
}
|
||||
|
||||
void send(Callback callback)
|
||||
{
|
||||
if (closing)
|
||||
return;
|
||||
auto task = std::make_unique<Callback>(std::move(callback));
|
||||
if (dispatcher.NonBlockingCall(static_cast<void*>(task.get())) == napi_ok)
|
||||
task.release();
|
||||
}
|
||||
|
||||
void sendValue(uint64_t operation, std::shared_ptr<const dht::Value> value, bool expired = false)
|
||||
{
|
||||
send([operation, value = std::move(value), expired](RunnerState& state) mutable {
|
||||
const auto listener = state.listeners.find(operation);
|
||||
if (listener != state.listeners.end()) {
|
||||
listener->second.callback.Call({valueToJS(state.env, *value), Napi::Boolean::New(state.env, expired)});
|
||||
} else if (const auto pending = state.operations.find(operation); pending != state.operations.end()) {
|
||||
if (!pending->second.onValue.IsEmpty())
|
||||
pending->second.onValue.Call({valueToJS(state.env, *value)});
|
||||
else
|
||||
pending->second.values.emplace_back(std::move(value));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void completeOperation(uint64_t identifier, bool ok)
|
||||
{
|
||||
const auto pending = operations.find(identifier);
|
||||
if (pending == operations.end())
|
||||
return;
|
||||
auto operation = std::move(pending->second);
|
||||
operations.erase(pending);
|
||||
if (!operation.get) {
|
||||
operation.deferred.Resolve(Napi::Boolean::New(env, ok));
|
||||
} else if (!ok) {
|
||||
auto error = Napi::Error::New(env, "DHT get failed");
|
||||
error.Set("code", "ERR_DHT_GET_FAILED");
|
||||
operation.deferred.Reject(error.Value());
|
||||
} else if (!operation.onValue.IsEmpty()) {
|
||||
operation.deferred.Resolve(env.Undefined());
|
||||
} else {
|
||||
auto values = Napi::Array::New(env, operation.values.size());
|
||||
for (size_t index = 0; index < operation.values.size(); ++index)
|
||||
values.Set(static_cast<uint32_t>(index), valueToJS(env, *operation.values[index]));
|
||||
operation.deferred.Resolve(values);
|
||||
}
|
||||
}
|
||||
|
||||
Napi::Object status() const
|
||||
{
|
||||
auto result = Napi::Object::New(env);
|
||||
result.Set("ipv4", Napi::String::New(env, dht::statusToStr(closing ? dht::NodeStatus::Disconnected : status4)));
|
||||
result.Set("ipv6", Napi::String::New(env, dht::statusToStr(closing ? dht::NodeStatus::Disconnected : status6)));
|
||||
return result;
|
||||
}
|
||||
|
||||
static void dispatch(napi_env environment, napi_value, void* context, void* data)
|
||||
{
|
||||
auto state = static_cast<RunnerState*>(context);
|
||||
std::unique_ptr<Callback> callback(static_cast<Callback*>(data));
|
||||
if (!environment || state->envClosing || state->closing)
|
||||
return;
|
||||
try {
|
||||
(*callback)(*state);
|
||||
} catch (const Napi::Error& error) {
|
||||
napi_fatal_exception(environment, error.Value());
|
||||
} catch (const std::exception& error) {
|
||||
napi_fatal_exception(environment, Napi::Error::New(environment, error.what()).Value());
|
||||
}
|
||||
}
|
||||
|
||||
static void finalizeDispatcher(napi_env, void* data, void*)
|
||||
{
|
||||
delete static_cast<std::shared_ptr<RunnerState>*>(data);
|
||||
}
|
||||
|
||||
static void cleanup(napi_async_cleanup_hook_handle, void* data)
|
||||
{
|
||||
auto state = static_cast<RunnerState*>(data);
|
||||
state->envClosing = true;
|
||||
state->startClose();
|
||||
}
|
||||
|
||||
static void executeClose(napi_env, void* data)
|
||||
{
|
||||
auto state = *static_cast<std::shared_ptr<RunnerState>*>(data);
|
||||
struct ShutdownSignal
|
||||
{
|
||||
std::mutex mutex;
|
||||
std::condition_variable condition;
|
||||
bool done = false;
|
||||
};
|
||||
try {
|
||||
auto signal = std::make_shared<ShutdownSignal>();
|
||||
state->runner.shutdown(
|
||||
[signal] {
|
||||
std::lock_guard lock(signal->mutex);
|
||||
signal->done = true;
|
||||
signal->condition.notify_one();
|
||||
},
|
||||
true);
|
||||
std::unique_lock lock(signal->mutex);
|
||||
signal->condition.wait_for(lock, std::chrono::seconds(2), [&] { return signal->done; });
|
||||
} catch (const std::exception& error) {
|
||||
state->closeError = error.what();
|
||||
}
|
||||
state->runner.join();
|
||||
}
|
||||
|
||||
Napi::Error makeCloseError() const
|
||||
{
|
||||
auto error = Napi::Error::New(env, closeError.empty() ? "DHT runner is closed" : closeError);
|
||||
error.Set("code", "ERR_DHT_CLOSED");
|
||||
return error;
|
||||
}
|
||||
|
||||
static void completeClose(napi_env environment, napi_status, void* data)
|
||||
{
|
||||
std::unique_ptr<std::shared_ptr<RunnerState>> owner(static_cast<std::shared_ptr<RunnerState>*>(data));
|
||||
auto state = *owner;
|
||||
state->finished = true;
|
||||
if (!state->envClosing) {
|
||||
state->dispatcher.Release();
|
||||
auto error = state->makeCloseError();
|
||||
for (auto& pending : state->operations)
|
||||
pending.second.deferred.Reject(error.Value());
|
||||
if (state->closeDeferred) {
|
||||
if (state->closeError.empty())
|
||||
state->closeDeferred->Resolve(Napi::Env(environment).Undefined());
|
||||
else
|
||||
state->closeDeferred->Reject(error.Value());
|
||||
}
|
||||
}
|
||||
state->operations.clear();
|
||||
state->listeners.clear();
|
||||
state->closeDeferred.reset();
|
||||
state->closePromise.Reset();
|
||||
state->onStatus.Reset();
|
||||
state->owner.Reset();
|
||||
napi_delete_async_work(environment, state->closeWork);
|
||||
napi_remove_async_cleanup_hook(state->cleanupHook);
|
||||
state->closeOwner = nullptr;
|
||||
}
|
||||
|
||||
Napi::Env env;
|
||||
dht::DhtRunner runner;
|
||||
Napi::ThreadSafeFunction dispatcher;
|
||||
napi_async_work closeWork {};
|
||||
napi_async_cleanup_hook_handle cleanupHook {};
|
||||
std::shared_ptr<RunnerState>* closeOwner {};
|
||||
std::atomic_bool closing {false};
|
||||
bool envClosing = false;
|
||||
bool started = false;
|
||||
bool hasIdentity = false;
|
||||
bool finished = false;
|
||||
uint64_t nextOperation = 1;
|
||||
std::string closeError;
|
||||
std::unordered_map<uint64_t, PendingOperation> operations;
|
||||
std::unordered_map<uint64_t, Subscription> listeners;
|
||||
std::optional<Napi::Promise::Deferred> closeDeferred;
|
||||
Napi::Reference<Napi::Promise> closePromise;
|
||||
Napi::FunctionReference onStatus;
|
||||
Napi::ObjectReference owner;
|
||||
dht::NodeStatus status4 = dht::NodeStatus::Disconnected;
|
||||
dht::NodeStatus status6 = dht::NodeStatus::Disconnected;
|
||||
};
|
||||
|
||||
class Runner : public Napi::ObjectWrap<Runner>
|
||||
{
|
||||
public:
|
||||
explicit Runner(const Napi::CallbackInfo& info)
|
||||
: Napi::ObjectWrap<Runner>(info)
|
||||
, state_(RunnerState::create(info.Env()))
|
||||
{
|
||||
if (info.Length() && info[0].IsFunction())
|
||||
state_->onStatus = Napi::Persistent(info[0].As<Napi::Function>());
|
||||
}
|
||||
|
||||
~Runner() override { state_->startClose(); }
|
||||
|
||||
static Napi::Function define(Napi::Env env)
|
||||
{
|
||||
return DefineClass(env,
|
||||
"NativeRunner",
|
||||
{
|
||||
InstanceMethod("run", &Runner::run),
|
||||
InstanceMethod("close", &Runner::close),
|
||||
InstanceMethod("isRunning", &Runner::isRunning),
|
||||
InstanceMethod("getBoundPort", &Runner::getBoundPort),
|
||||
InstanceMethod("getNodeId", &Runner::getNodeId),
|
||||
InstanceMethod("getStatus", &Runner::getStatus),
|
||||
InstanceMethod("bootstrap", &Runner::bootstrap),
|
||||
InstanceMethod("get", &Runner::get),
|
||||
InstanceMethod("getAll", &Runner::getAll),
|
||||
InstanceMethod("cancelGet", &Runner::cancelGet),
|
||||
InstanceMethod("put", &Runner::put),
|
||||
InstanceMethod("putSigned", &Runner::putSigned),
|
||||
InstanceMethod("putEncrypted", &Runner::putEncrypted),
|
||||
InstanceMethod("listen", &Runner::listen),
|
||||
InstanceMethod("cancelListen", &Runner::cancelListen),
|
||||
InstanceMethod("ref", &Runner::ref),
|
||||
InstanceMethod("unref", &Runner::unref),
|
||||
InstanceMethod("setOwner", &Runner::setOwner),
|
||||
});
|
||||
}
|
||||
|
||||
private:
|
||||
Napi::Value run(const Napi::CallbackInfo& info)
|
||||
{
|
||||
return guarded(info, [&]() -> Napi::Value {
|
||||
if (state_->closing)
|
||||
throw Napi::Error::New(info.Env(), "DHT runner is closed");
|
||||
if (state_->started)
|
||||
throw Napi::Error::New(info.Env(), "DHT runner is already running");
|
||||
auto options = info.Length() && !info[0].IsUndefined() ? optionsObject(info[0])
|
||||
: Napi::Object::New(info.Env());
|
||||
const auto port = options.Has("port") ? unsignedNumber(options.Get("port"), 65535, "port") : 0;
|
||||
const auto ipv4 = options.Has("ipv4") ? text(options.Get("ipv4"), "ipv4") : "0.0.0.0";
|
||||
const auto ipv6 = options.Has("ipv6") ? text(options.Get("ipv6"), "ipv6") : "::";
|
||||
dht::DhtRunner::Config config;
|
||||
if (options.Has("network"))
|
||||
config.dht_config.node_config.network = unsignedNumber(options.Get("network"), UINT32_MAX, "network");
|
||||
if (options.Has("persistPath"))
|
||||
config.dht_config.node_config.persist_path = text(options.Get("persistPath"), "persistPath");
|
||||
if (options.Has("identity"))
|
||||
config.dht_config.id = opendht_node::readIdentity(options.Get("identity"));
|
||||
if (options.Has("proxyServer"))
|
||||
config.proxy_server = text(options.Get("proxyServer"), "proxyServer");
|
||||
if (options.Has("proxyUserAgent")) {
|
||||
config.proxy_user_agent = text(options.Get("proxyUserAgent"), "proxyUserAgent");
|
||||
if (config.proxy_user_agent.find_first_of("\r\n") != std::string::npos)
|
||||
throw Napi::TypeError::New(info.Env(), "proxyUserAgent must not contain line breaks");
|
||||
}
|
||||
dht::DhtRunner::Context context;
|
||||
const std::weak_ptr<RunnerState> state = state_;
|
||||
context.statusChangedCallback = [state](dht::NodeStatus ipv4, dht::NodeStatus ipv6) {
|
||||
if (auto owner = state.lock())
|
||||
owner->send([ipv4, ipv6](RunnerState& state) {
|
||||
state.status4 = ipv4;
|
||||
state.status6 = ipv6;
|
||||
if (!state.onStatus.IsEmpty())
|
||||
state.onStatus.Call({state.status()});
|
||||
});
|
||||
};
|
||||
const auto service = std::to_string(port);
|
||||
state_->runner.run(ipv4.c_str(), ipv6.c_str(), service.c_str(), config, std::move(context));
|
||||
state_->started = true;
|
||||
state_->hasIdentity = bool(config.dht_config.id.first);
|
||||
state_->dispatcher.Ref(info.Env());
|
||||
return info.Env().Undefined();
|
||||
});
|
||||
}
|
||||
|
||||
Napi::Value close(const Napi::CallbackInfo& info)
|
||||
{
|
||||
return guarded(info, [&]() -> Napi::Value {
|
||||
if (!state_->closePromise.IsEmpty())
|
||||
return state_->closePromise.Value();
|
||||
auto deferred = Napi::Promise::Deferred::New(info.Env());
|
||||
if (state_->finished) {
|
||||
if (!state_->closeError.empty())
|
||||
deferred.Reject(state_->makeCloseError().Value());
|
||||
else
|
||||
deferred.Resolve(info.Env().Undefined());
|
||||
} else {
|
||||
state_->closeDeferred = deferred;
|
||||
state_->closePromise = Napi::Persistent(deferred.Promise());
|
||||
state_->startClose();
|
||||
}
|
||||
return deferred.Promise();
|
||||
});
|
||||
}
|
||||
|
||||
Napi::Value isRunning(const Napi::CallbackInfo& info)
|
||||
{
|
||||
return Napi::Boolean::New(info.Env(), !state_->closing && state_->runner.isRunning());
|
||||
}
|
||||
|
||||
Napi::Value getStatus(const Napi::CallbackInfo& info)
|
||||
{
|
||||
return guarded(info, [&]() -> Napi::Value { return state_->status(); });
|
||||
}
|
||||
|
||||
Napi::Value getBoundPort(const Napi::CallbackInfo& info)
|
||||
{
|
||||
return guarded(info, [&]() -> Napi::Value {
|
||||
state_->requireRunning();
|
||||
const auto family = info.Length() ? unsignedNumber(info[0], 6, "family") : 4;
|
||||
if (family != 4 && family != 6)
|
||||
throw Napi::RangeError::New(info.Env(), "family must be 4 or 6");
|
||||
return Napi::Number::New(info.Env(), state_->runner.getBoundPort(family == 4 ? AF_INET : AF_INET6));
|
||||
});
|
||||
}
|
||||
|
||||
Napi::Value getNodeId(const Napi::CallbackInfo& info)
|
||||
{
|
||||
return guarded(info, [&]() -> Napi::Value {
|
||||
state_->requireRunning();
|
||||
return Napi::String::New(info.Env(), state_->runner.getNodeId().to_view());
|
||||
});
|
||||
}
|
||||
|
||||
Napi::Value bootstrap(const Napi::CallbackInfo& info)
|
||||
{
|
||||
return guarded(info, [&]() -> Napi::Value {
|
||||
state_->requireRunning();
|
||||
const auto host = text(info[0], "host");
|
||||
const auto port = unsignedNumber(info[1], 65535, "port");
|
||||
if (host.empty() || !port)
|
||||
throw Napi::RangeError::New(info.Env(), "bootstrap requires a host and nonzero port");
|
||||
state_->runner.bootstrap(host, std::to_string(port));
|
||||
return info.Env().Undefined();
|
||||
});
|
||||
}
|
||||
|
||||
Napi::Value get(const Napi::CallbackInfo& info) { return getValues(info, true); }
|
||||
|
||||
Napi::Value getAll(const Napi::CallbackInfo& info) { return getValues(info, false); }
|
||||
|
||||
Napi::Value getValues(const Napi::CallbackInfo& info, bool stream)
|
||||
{
|
||||
return guarded(info, [&]() -> Napi::Value {
|
||||
state_->requireRunning();
|
||||
const auto key = infoHash(info[0]);
|
||||
if (stream && !info[1].IsFunction())
|
||||
throw Napi::TypeError::New(info.Env(), "callback must be a function");
|
||||
const auto operation = state_->nextOperation++;
|
||||
auto deferred = Napi::Promise::Deferred::New(info.Env());
|
||||
auto cancelled = std::make_shared<std::atomic_bool>(false);
|
||||
PendingOperation pending {deferred, true, {}, {}, cancelled};
|
||||
if (stream)
|
||||
pending.onValue = Napi::Persistent(info[1].As<Napi::Function>());
|
||||
state_->operations.emplace(operation, std::move(pending));
|
||||
const std::weak_ptr<RunnerState> state = state_;
|
||||
try {
|
||||
state_->runner.get(key,
|
||||
dht::GetCallback([state, operation, cancelled](
|
||||
const std::vector<std::shared_ptr<dht::Value>>& values) {
|
||||
const auto owner = state.lock();
|
||||
if (!owner || owner->closing || *cancelled)
|
||||
return false;
|
||||
for (const auto& value : values)
|
||||
owner->sendValue(operation, value);
|
||||
return !owner->closing && !*cancelled;
|
||||
}),
|
||||
dht::DoneCallbackSimple([state, operation](bool ok) {
|
||||
if (const auto owner = state.lock())
|
||||
owner->send([operation, ok](RunnerState& state) {
|
||||
state.completeOperation(operation, ok);
|
||||
});
|
||||
}));
|
||||
} catch (...) {
|
||||
state_->operations.erase(operation);
|
||||
throw;
|
||||
}
|
||||
if (stream) {
|
||||
auto result = Napi::Object::New(info.Env());
|
||||
result.Set("token", Napi::BigInt::New(info.Env(), operation));
|
||||
result.Set("done", deferred.Promise());
|
||||
return result;
|
||||
}
|
||||
return deferred.Promise();
|
||||
});
|
||||
}
|
||||
|
||||
Napi::Value cancelGet(const Napi::CallbackInfo& info)
|
||||
{
|
||||
return guarded(info, [&]() -> Napi::Value {
|
||||
const auto operation = state_->operations.find(unsignedBigInt(info[0]));
|
||||
if (operation != state_->operations.end() && !operation->second.onValue.IsEmpty()) {
|
||||
*operation->second.cancelled = true;
|
||||
operation->second.deferred.Resolve(info.Env().Undefined());
|
||||
state_->operations.erase(operation);
|
||||
}
|
||||
return info.Env().Undefined();
|
||||
});
|
||||
}
|
||||
|
||||
Napi::Value put(const Napi::CallbackInfo& info) { return putValue(info, PutMode::Plain); }
|
||||
|
||||
Napi::Value putSigned(const Napi::CallbackInfo& info) { return putValue(info, PutMode::Signed); }
|
||||
|
||||
Napi::Value putEncrypted(const Napi::CallbackInfo& info) { return putValue(info, PutMode::Encrypted); }
|
||||
|
||||
enum class PutMode { Plain, Signed, Encrypted };
|
||||
|
||||
Napi::Value putValue(const Napi::CallbackInfo& info, PutMode mode)
|
||||
{
|
||||
return guarded(info, [&]() -> Napi::Value {
|
||||
state_->requireRunning();
|
||||
if (mode != PutMode::Plain && !state_->hasIdentity)
|
||||
throw Napi::Error::New(info.Env(), "Signed and encrypted puts require an identity");
|
||||
std::shared_ptr<dht::crypto::PublicKey> recipient;
|
||||
if (mode == PutMode::Encrypted) {
|
||||
recipient = opendht_node::readEncryptionKey(info[3]);
|
||||
}
|
||||
const auto key = infoHash(info[0]);
|
||||
if (!info[1].IsTypedArray() || info[1].As<Napi::TypedArray>().TypedArrayType() != napi_uint8_array)
|
||||
throw Napi::TypeError::New(info.Env(), "data must be a Uint8Array");
|
||||
const auto data = info[1].As<Napi::Uint8Array>();
|
||||
dht::Blob bytes(data.ElementLength());
|
||||
if (!bytes.empty())
|
||||
std::copy_n(data.Data(), bytes.size(), bytes.data());
|
||||
auto value = std::make_shared<dht::Value>(std::move(bytes));
|
||||
auto options = info.Length() > 2 && !info[2].IsUndefined() ? optionsObject(info[2])
|
||||
: Napi::Object::New(info.Env());
|
||||
if (options.Has("id"))
|
||||
value->id = unsignedBigInt(options.Get("id"));
|
||||
if (options.Has("type"))
|
||||
value->type = unsignedNumber(options.Get("type"), 65535, "type");
|
||||
if (options.Has("userType")) {
|
||||
if (!options.Get("userType").IsString())
|
||||
throw Napi::TypeError::New(info.Env(), "userType must be a string");
|
||||
value->user_type = options.Get("userType").As<Napi::String>().Utf8Value();
|
||||
}
|
||||
bool permanent = false;
|
||||
if (options.Has("permanent")) {
|
||||
if (!options.Get("permanent").IsBoolean())
|
||||
throw Napi::TypeError::New(info.Env(), "permanent must be a boolean");
|
||||
permanent = options.Get("permanent").As<Napi::Boolean>().Value();
|
||||
}
|
||||
const auto operation = state_->nextOperation++;
|
||||
auto deferred = Napi::Promise::Deferred::New(info.Env());
|
||||
state_->operations.emplace(operation, PendingOperation {deferred, false, {}, {}, {}});
|
||||
const std::weak_ptr<RunnerState> state = state_;
|
||||
try {
|
||||
dht::DoneCallbackSimple done = [state, operation](bool ok) {
|
||||
if (const auto owner = state.lock())
|
||||
owner->send([operation, ok](RunnerState& state) { state.completeOperation(operation, ok); });
|
||||
};
|
||||
if (mode == PutMode::Signed)
|
||||
state_->runner.putSigned(key, value, std::move(done), permanent);
|
||||
else if (mode == PutMode::Encrypted)
|
||||
state_->runner.putEncrypted(key, recipient, value, std::move(done), permanent);
|
||||
else
|
||||
state_->runner.put(key, value, std::move(done), dht::time_point::max(), permanent);
|
||||
} catch (...) {
|
||||
state_->operations.erase(operation);
|
||||
throw;
|
||||
}
|
||||
return deferred.Promise();
|
||||
});
|
||||
}
|
||||
|
||||
Napi::Value listen(const Napi::CallbackInfo& info)
|
||||
{
|
||||
return guarded(info, [&]() -> Napi::Value {
|
||||
state_->requireRunning();
|
||||
const auto key = infoHash(info[0]);
|
||||
if (!info[1].IsFunction())
|
||||
throw Napi::TypeError::New(info.Env(), "callback must be a function");
|
||||
const auto operation = state_->nextOperation++;
|
||||
const std::weak_ptr<RunnerState> state = state_;
|
||||
auto token = state_->runner
|
||||
.listen(key,
|
||||
dht::ValueCallback(
|
||||
[state, operation](const std::vector<std::shared_ptr<dht::Value>>& values,
|
||||
bool expired) {
|
||||
const auto owner = state.lock();
|
||||
if (!owner || owner->closing)
|
||||
return false;
|
||||
for (const auto& value : values)
|
||||
owner->sendValue(operation, value, expired);
|
||||
return !owner->closing;
|
||||
}))
|
||||
.share();
|
||||
state_->listeners.emplace(operation,
|
||||
Subscription {key,
|
||||
std::move(token),
|
||||
Napi::Persistent(info[1].As<Napi::Function>())});
|
||||
return Napi::BigInt::New(info.Env(), operation);
|
||||
});
|
||||
}
|
||||
|
||||
Napi::Value cancelListen(const Napi::CallbackInfo& info)
|
||||
{
|
||||
return guarded(info, [&]() -> Napi::Value {
|
||||
if (state_->closing)
|
||||
return info.Env().Undefined();
|
||||
const auto listener = state_->listeners.find(unsignedBigInt(info[0]));
|
||||
if (listener != state_->listeners.end()) {
|
||||
state_->runner.cancelListen(listener->second.key, listener->second.token);
|
||||
state_->listeners.erase(listener);
|
||||
}
|
||||
return info.Env().Undefined();
|
||||
});
|
||||
}
|
||||
|
||||
Napi::Value ref(const Napi::CallbackInfo& info)
|
||||
{
|
||||
return guarded(info, [&]() -> Napi::Value {
|
||||
state_->requireRunning();
|
||||
state_->dispatcher.Ref(info.Env());
|
||||
return info.This();
|
||||
});
|
||||
}
|
||||
|
||||
Napi::Value unref(const Napi::CallbackInfo& info)
|
||||
{
|
||||
return guarded(info, [&]() -> Napi::Value {
|
||||
state_->requireRunning();
|
||||
state_->dispatcher.Unref(info.Env());
|
||||
return info.This();
|
||||
});
|
||||
}
|
||||
|
||||
Napi::Value setOwner(const Napi::CallbackInfo& info)
|
||||
{
|
||||
if (!state_->closing) {
|
||||
state_->owner.Reset();
|
||||
if (info[0].IsObject())
|
||||
state_->owner = Napi::Persistent(info[0].As<Napi::Object>());
|
||||
}
|
||||
return info.Env().Undefined();
|
||||
}
|
||||
|
||||
std::shared_ptr<RunnerState> state_;
|
||||
};
|
||||
|
||||
Napi::Value
|
||||
hash(const Napi::CallbackInfo& info)
|
||||
{
|
||||
const auto env = info.Env();
|
||||
dht::InfoHash result;
|
||||
if (info.Length() == 1 && info[0].IsString()) {
|
||||
result = dht::InfoHash::get(info[0].As<Napi::String>().Utf8Value());
|
||||
} else if (info.Length() == 1 && info[0].IsTypedArray()
|
||||
&& info[0].As<Napi::TypedArray>().TypedArrayType() == napi_uint8_array) {
|
||||
const auto data = info[0].As<Napi::Uint8Array>();
|
||||
result = dht::InfoHash::get(data.Data(), data.ElementLength());
|
||||
} else {
|
||||
throw Napi::TypeError::New(env, "Expected a string or Uint8Array");
|
||||
}
|
||||
return Napi::String::New(env, result.to_view());
|
||||
}
|
||||
|
||||
Napi::Object
|
||||
initialize(Napi::Env env, Napi::Object exports)
|
||||
{
|
||||
static const GnuTlsContext crypto;
|
||||
if (crypto.result != GNUTLS_E_SUCCESS)
|
||||
throw Napi::Error::New(env, std::string("Unable to initialize GnuTLS: ") + gnutls_strerror(crypto.result));
|
||||
opendht_node::initializeCrypto(env, exports);
|
||||
exports.Set("NativeRunner", Runner::define(env));
|
||||
exports.Set("hash", Napi::Function::New(env, hash));
|
||||
exports.Set("version", Napi::String::New(env, dht::version()));
|
||||
return exports;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
NODE_API_MODULE(opendht, initialize)
|
||||
@@ -0,0 +1,304 @@
|
||||
#include "crypto.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <variant>
|
||||
|
||||
namespace opendht_node {
|
||||
namespace {
|
||||
|
||||
using PublicKey = std::shared_ptr<dht::crypto::PublicKey>;
|
||||
using PrivateKey = std::shared_ptr<dht::crypto::PrivateKey>;
|
||||
using Certificate = std::shared_ptr<dht::crypto::Certificate>;
|
||||
using Identity = dht::crypto::Identity;
|
||||
using Handle = std::variant<PublicKey, PrivateKey, Certificate, Identity>;
|
||||
|
||||
constexpr napi_type_tag cryptoTag {0x37cdf3e4648e4cb5ULL, 0x964fb031062c245aULL};
|
||||
|
||||
Handle&
|
||||
handle(Napi::Value value)
|
||||
{
|
||||
bool matches = false;
|
||||
if (!value.IsExternal() || napi_check_object_type_tag(value.Env(), value, &cryptoTag, &matches) != napi_ok
|
||||
|| !matches)
|
||||
throw Napi::TypeError::New(value.Env(), "Invalid OpenDHT crypto handle");
|
||||
return *value.As<Napi::External<Handle>>().Data();
|
||||
}
|
||||
|
||||
template<typename Type>
|
||||
Type
|
||||
read(Napi::Value value)
|
||||
{
|
||||
if (auto result = std::get_if<Type>(&handle(value)))
|
||||
return *result;
|
||||
throw Napi::TypeError::New(value.Env(), "Wrong OpenDHT crypto handle type");
|
||||
}
|
||||
|
||||
Napi::Value
|
||||
wrap(Napi::Env env, Handle value)
|
||||
{
|
||||
auto owner = std::make_unique<Handle>(std::move(value));
|
||||
auto result = Napi::External<Handle>::New(env, owner.get(), [](Napi::Env, Handle* data) { delete data; });
|
||||
owner.release();
|
||||
if (napi_type_tag_object(env, result, &cryptoTag) != napi_ok)
|
||||
throw Napi::Error::New(env, "Could not tag OpenDHT crypto handle");
|
||||
return result;
|
||||
}
|
||||
|
||||
dht::Blob
|
||||
bytes(Napi::Value value)
|
||||
{
|
||||
if (!value.IsTypedArray() || value.As<Napi::TypedArray>().TypedArrayType() != napi_uint8_array)
|
||||
throw Napi::TypeError::New(value.Env(), "Expected a Uint8Array");
|
||||
auto array = value.As<Napi::Uint8Array>();
|
||||
dht::Blob result(array.ElementLength());
|
||||
if (!result.empty())
|
||||
std::copy_n(array.Data(), result.size(), result.data());
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string
|
||||
string(Napi::Value value)
|
||||
{
|
||||
if (!value.IsString())
|
||||
throw Napi::TypeError::New(value.Env(), "Expected a string");
|
||||
auto result = value.As<Napi::String>().Utf8Value();
|
||||
if (result.find('\0') != std::string::npos)
|
||||
throw Napi::TypeError::New(value.Env(), "Crypto strings must not contain NUL bytes");
|
||||
return result;
|
||||
}
|
||||
|
||||
Napi::Buffer<uint8_t>
|
||||
buffer(Napi::Env env, const dht::Blob& value)
|
||||
{
|
||||
return Napi::Buffer<uint8_t>::Copy(env, value.data(), value.size());
|
||||
}
|
||||
|
||||
PublicKey
|
||||
publicKey(const Handle& value)
|
||||
{
|
||||
if (auto key = std::get_if<PublicKey>(&value))
|
||||
return *key;
|
||||
if (auto key = std::get_if<PrivateKey>(&value))
|
||||
return (*key)->getSharedPublicKey();
|
||||
if (auto certificate = std::get_if<Certificate>(&value))
|
||||
return (*certificate)->getSharedPublicKey();
|
||||
return std::get<Identity>(value).second->getSharedPublicKey();
|
||||
}
|
||||
|
||||
class GenerateWorker : public Napi::AsyncWorker
|
||||
{
|
||||
public:
|
||||
GenerateWorker(Napi::Env env, bool identity, std::string name, bool rsa, unsigned bits)
|
||||
: Napi::AsyncWorker(env, "OpenDHT crypto generation")
|
||||
, deferred_(Napi::Promise::Deferred::New(env))
|
||||
, identity_(identity)
|
||||
, name_(std::move(name))
|
||||
, rsa_(rsa)
|
||||
, bits_(bits)
|
||||
{}
|
||||
|
||||
Napi::Promise promise() const { return deferred_.Promise(); }
|
||||
|
||||
void Execute() override
|
||||
{
|
||||
try {
|
||||
if (identity_)
|
||||
result_ = rsa_ ? dht::crypto::generateIdentity(name_, {}, bits_)
|
||||
: dht::crypto::generateEcIdentity(name_);
|
||||
else
|
||||
result_ = std::make_shared<dht::crypto::PrivateKey>(rsa_ ? dht::crypto::PrivateKey::generate(bits_)
|
||||
: dht::crypto::PrivateKey::generateEC());
|
||||
} catch (const std::exception& error) {
|
||||
SetError(error.what());
|
||||
}
|
||||
}
|
||||
|
||||
void OnOK() override { deferred_.Resolve(wrap(Env(), std::move(result_))); }
|
||||
void OnError(const Napi::Error& error) override { deferred_.Reject(error.Value()); }
|
||||
|
||||
private:
|
||||
Napi::Promise::Deferred deferred_;
|
||||
bool identity_;
|
||||
std::string name_;
|
||||
bool rsa_;
|
||||
unsigned bits_;
|
||||
Handle result_;
|
||||
};
|
||||
|
||||
Napi::Value
|
||||
generate(const Napi::CallbackInfo& info)
|
||||
{
|
||||
return guarded(info, [&]() -> Napi::Value {
|
||||
const auto kind = string(info[0]);
|
||||
const auto name = string(info[1]);
|
||||
const auto algorithm = string(info[2]);
|
||||
if (algorithm != "rsa" && algorithm != "ec")
|
||||
throw Napi::TypeError::New(info.Env(), "algorithm must be rsa or ec");
|
||||
if (!info[3].IsNumber())
|
||||
throw Napi::TypeError::New(info.Env(), "bits must be a number");
|
||||
const auto bits = info[3].As<Napi::Number>().DoubleValue();
|
||||
if (!std::isfinite(bits) || std::floor(bits) != bits || bits < 2048 || bits > 8192)
|
||||
throw Napi::RangeError::New(info.Env(), "RSA bits must be an integer between 2048 and 8192");
|
||||
if (kind != "private" && kind != "identity")
|
||||
throw Napi::TypeError::New(info.Env(), "Invalid crypto generation type");
|
||||
auto worker = std::make_unique<GenerateWorker>(info.Env(), kind == "identity", name, algorithm == "rsa", bits);
|
||||
auto promise = worker->promise();
|
||||
worker->Queue();
|
||||
worker.release();
|
||||
return promise;
|
||||
});
|
||||
}
|
||||
|
||||
Napi::Value
|
||||
import(const Napi::CallbackInfo& info)
|
||||
{
|
||||
return guarded(info, [&]() -> Napi::Value {
|
||||
const auto kind = string(info[0]);
|
||||
auto data = bytes(info[1]);
|
||||
if (data.empty())
|
||||
throw Napi::TypeError::New(info.Env(), "Crypto imports must not be empty");
|
||||
if (kind == "private")
|
||||
return wrap(info.Env(), std::make_shared<dht::crypto::PrivateKey>(data, string(info[2])));
|
||||
if (kind == "public")
|
||||
return wrap(info.Env(), std::make_shared<dht::crypto::PublicKey>(data));
|
||||
if (kind == "certificate")
|
||||
return wrap(info.Env(), std::make_shared<dht::crypto::Certificate>(data));
|
||||
throw Napi::TypeError::New(info.Env(), "Invalid crypto import type");
|
||||
});
|
||||
}
|
||||
|
||||
Napi::Value
|
||||
serialize(const Napi::CallbackInfo& info)
|
||||
{
|
||||
return guarded(info, [&]() -> Napi::Value {
|
||||
auto& value = handle(info[0]);
|
||||
if (auto key = std::get_if<PrivateKey>(&value))
|
||||
return buffer(info.Env(), (*key)->serialize(string(info[1])));
|
||||
if (auto certificate = std::get_if<Certificate>(&value))
|
||||
return buffer(info.Env(), (*certificate)->getPacked());
|
||||
dht::Blob result;
|
||||
readPublicKey(info[0])->pack(result);
|
||||
return buffer(info.Env(), result);
|
||||
});
|
||||
}
|
||||
|
||||
Napi::Value
|
||||
getPublicKey(const Napi::CallbackInfo& info)
|
||||
{
|
||||
return guarded(info, [&] { return wrap(info.Env(), publicKey(handle(info[0]))); });
|
||||
}
|
||||
|
||||
Napi::Value
|
||||
getId(const Napi::CallbackInfo& info)
|
||||
{
|
||||
return guarded(info, [&]() -> Napi::Value {
|
||||
const auto key = publicKey(handle(info[0]));
|
||||
if (!info[1].IsBoolean())
|
||||
throw Napi::TypeError::New(info.Env(), "Expected a boolean");
|
||||
return Napi::String::New(info.Env(),
|
||||
info[1].As<Napi::Boolean>().Value() ? key->getLongId().toString()
|
||||
: key->getId().toString());
|
||||
});
|
||||
}
|
||||
|
||||
Napi::Value
|
||||
sign(const Napi::CallbackInfo& info)
|
||||
{
|
||||
return guarded(info, [&]() -> Napi::Value {
|
||||
return buffer(info.Env(), read<PrivateKey>(info[0])->sign(bytes(info[1])));
|
||||
});
|
||||
}
|
||||
|
||||
Napi::Value
|
||||
verify(const Napi::CallbackInfo& info)
|
||||
{
|
||||
return guarded(info, [&]() -> Napi::Value {
|
||||
return Napi::Boolean::New(info.Env(), readPublicKey(info[0])->checkSignature(bytes(info[1]), bytes(info[2])));
|
||||
});
|
||||
}
|
||||
|
||||
Napi::Value
|
||||
encrypt(const Napi::CallbackInfo& info)
|
||||
{
|
||||
return guarded(info, [&]() -> Napi::Value {
|
||||
const auto key = readEncryptionKey(info[0]);
|
||||
return buffer(info.Env(), key->encrypt(bytes(info[1])));
|
||||
});
|
||||
}
|
||||
|
||||
Napi::Value
|
||||
decrypt(const Napi::CallbackInfo& info)
|
||||
{
|
||||
return guarded(info, [&]() -> Napi::Value {
|
||||
return buffer(info.Env(), read<PrivateKey>(info[0])->decrypt(bytes(info[1])));
|
||||
});
|
||||
}
|
||||
|
||||
Napi::Value
|
||||
identity(const Napi::CallbackInfo& info)
|
||||
{
|
||||
return guarded(info, [&]() -> Napi::Value {
|
||||
auto key = read<PrivateKey>(info[0]);
|
||||
auto certificate = read<Certificate>(info[1]);
|
||||
if (key->getPublicKey().getLongId() != certificate->getLongId())
|
||||
throw Napi::TypeError::New(info.Env(), "Identity key and certificate do not match");
|
||||
return wrap(info.Env(), Identity {std::move(key), std::move(certificate)});
|
||||
});
|
||||
}
|
||||
|
||||
Napi::Value
|
||||
identityPart(const Napi::CallbackInfo& info)
|
||||
{
|
||||
return guarded(info, [&]() -> Napi::Value {
|
||||
const auto value = readIdentity(info[0]);
|
||||
if (!info[1].IsBoolean())
|
||||
throw Napi::TypeError::New(info.Env(), "Expected a boolean");
|
||||
return info[1].As<Napi::Boolean>().Value() ? wrap(info.Env(), value.first) : wrap(info.Env(), value.second);
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
dht::crypto::Identity
|
||||
readIdentity(Napi::Value value)
|
||||
{
|
||||
return read<Identity>(value);
|
||||
}
|
||||
std::shared_ptr<dht::crypto::PublicKey>
|
||||
readPublicKey(Napi::Value value)
|
||||
{
|
||||
return read<PublicKey>(value);
|
||||
}
|
||||
|
||||
std::shared_ptr<dht::crypto::PublicKey>
|
||||
readEncryptionKey(Napi::Value value)
|
||||
{
|
||||
const auto key = readPublicKey(value);
|
||||
const auto algorithm = gnutls_pubkey_get_pk_algorithm(key->pk, nullptr);
|
||||
if (algorithm != GNUTLS_PK_RSA
|
||||
#if GNUTLS_VERSION_NUMBER >= 0x030804
|
||||
&& algorithm != GNUTLS_PK_RSA_OAEP
|
||||
#endif
|
||||
)
|
||||
throw Napi::TypeError::New(value.Env(), "Encryption requires an RSA public key");
|
||||
return key;
|
||||
}
|
||||
|
||||
void
|
||||
initializeCrypto(Napi::Env env, Napi::Object exports)
|
||||
{
|
||||
exports.Set("cryptoGenerate", Napi::Function::New(env, generate));
|
||||
exports.Set("cryptoImport", Napi::Function::New(env, import));
|
||||
exports.Set("cryptoSerialize", Napi::Function::New(env, serialize));
|
||||
exports.Set("cryptoPublicKey", Napi::Function::New(env, getPublicKey));
|
||||
exports.Set("cryptoId", Napi::Function::New(env, getId));
|
||||
exports.Set("cryptoSign", Napi::Function::New(env, sign));
|
||||
exports.Set("cryptoVerify", Napi::Function::New(env, verify));
|
||||
exports.Set("cryptoEncrypt", Napi::Function::New(env, encrypt));
|
||||
exports.Set("cryptoDecrypt", Napi::Function::New(env, decrypt));
|
||||
exports.Set("cryptoIdentity", Napi::Function::New(env, identity));
|
||||
exports.Set("cryptoIdentityPart", Napi::Function::New(env, identityPart));
|
||||
}
|
||||
|
||||
} // namespace opendht_node
|
||||
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
|
||||
#include <napi.h>
|
||||
#include <opendht/crypto.h>
|
||||
|
||||
namespace opendht_node {
|
||||
|
||||
template<typename Callback>
|
||||
Napi::Value
|
||||
guarded(const Napi::CallbackInfo& info, Callback&& callback)
|
||||
{
|
||||
try {
|
||||
return callback();
|
||||
} catch (const Napi::Error&) {
|
||||
throw;
|
||||
} catch (const std::exception& error) {
|
||||
throw Napi::Error::New(info.Env(), error.what());
|
||||
}
|
||||
}
|
||||
|
||||
dht::crypto::Identity readIdentity(Napi::Value value);
|
||||
std::shared_ptr<dht::crypto::PublicKey> readPublicKey(Napi::Value value);
|
||||
std::shared_ptr<dht::crypto::PublicKey> readEncryptionKey(Napi::Value value);
|
||||
void initializeCrypto(Napi::Env env, Napi::Object exports);
|
||||
|
||||
} // namespace opendht_node
|
||||
@@ -0,0 +1,56 @@
|
||||
export default function createCrypto(native) {
|
||||
const handles = new WeakMap();
|
||||
|
||||
function handle(value, Type) {
|
||||
if (!(value instanceof Type) || !handles.has(value)) throw new TypeError(`Expected ${Type.name}`);
|
||||
return handles.get(value);
|
||||
}
|
||||
|
||||
function wrap(Type, nativeHandle) {
|
||||
const result = Object.create(Type.prototype);
|
||||
handles.set(result, nativeHandle);
|
||||
return result;
|
||||
}
|
||||
|
||||
function generate(kind, name, options = {}) {
|
||||
if (!options || typeof options !== 'object' || Array.isArray(options)) throw new TypeError('Invalid crypto options');
|
||||
return native.cryptoGenerate(kind, name, options.algorithm ?? 'ec', options.bits ?? 2048);
|
||||
}
|
||||
|
||||
class PublicKey {
|
||||
constructor(data) { handles.set(this, native.cryptoImport('public', data)); }
|
||||
serialize() { return native.cryptoSerialize(handle(this, PublicKey)); }
|
||||
getId() { return native.cryptoId(handle(this, PublicKey), false); }
|
||||
getLongId() { return native.cryptoId(handle(this, PublicKey), true); }
|
||||
checkSignature(data, signature) { return native.cryptoVerify(handle(this, PublicKey), data, signature); }
|
||||
encrypt(data) { return native.cryptoEncrypt(handle(this, PublicKey), data); }
|
||||
}
|
||||
|
||||
class PrivateKey {
|
||||
constructor(data, password = '') { handles.set(this, native.cryptoImport('private', data, password)); }
|
||||
static generate(options) { return generate('private', '', options).then(value => wrap(PrivateKey, value)); }
|
||||
serialize(password = '') { return native.cryptoSerialize(handle(this, PrivateKey), password); }
|
||||
getPublicKey() { return wrap(PublicKey, native.cryptoPublicKey(handle(this, PrivateKey))); }
|
||||
sign(data) { return native.cryptoSign(handle(this, PrivateKey), data); }
|
||||
decrypt(data) { return native.cryptoDecrypt(handle(this, PrivateKey), data); }
|
||||
}
|
||||
|
||||
class Certificate {
|
||||
constructor(data) { handles.set(this, native.cryptoImport('certificate', data)); }
|
||||
serialize() { return native.cryptoSerialize(handle(this, Certificate)); }
|
||||
getPublicKey() { return wrap(PublicKey, native.cryptoPublicKey(handle(this, Certificate))); }
|
||||
getId() { return native.cryptoId(handle(this, Certificate), false); }
|
||||
getLongId() { return native.cryptoId(handle(this, Certificate), true); }
|
||||
}
|
||||
|
||||
class Identity {
|
||||
constructor(privateKey, certificate) {
|
||||
handles.set(this, native.cryptoIdentity(handle(privateKey, PrivateKey), handle(certificate, Certificate)));
|
||||
}
|
||||
static generate(name = 'dhtnode', options) { return generate('identity', name, options).then(value => wrap(Identity, value)); }
|
||||
get privateKey() { return wrap(PrivateKey, native.cryptoIdentityPart(handle(this, Identity), true)); }
|
||||
get certificate() { return wrap(Certificate, native.cryptoIdentityPart(handle(this, Identity), false)); }
|
||||
}
|
||||
|
||||
return { PublicKey, PrivateKey, Certificate, Identity, handle };
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { clearLine, createInterface, cursorTo, type Interface } from 'node:readline';
|
||||
import { parseArgs } from 'node:util';
|
||||
import type { ConnectivityStatus, Listener, Value } from '../index.js';
|
||||
|
||||
const commands = 'get KEY | put KEY TEXT | sign KEY TEXT | listen KEY | cancel KEY | help | quit';
|
||||
|
||||
function portNumber(value: string, name: string, minimum = 0): number {
|
||||
const port = Number(value);
|
||||
if (!Number.isInteger(port) || port < minimum || port > 65535) {
|
||||
throw new Error(`${name} must be an integer between ${minimum} and 65535`);
|
||||
}
|
||||
return port;
|
||||
}
|
||||
|
||||
function errorMessage(error: unknown): string {
|
||||
return error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
|
||||
function report(error: unknown): void {
|
||||
console.error(errorMessage(error));
|
||||
process.exitCode = 1;
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const { values } = parseArgs({
|
||||
options: {
|
||||
help: { type: 'boolean', short: 'h' },
|
||||
port: { type: 'string', short: 'p', default: '4222' },
|
||||
ipv4: { type: 'string', default: '0.0.0.0' },
|
||||
ipv6: { type: 'string', default: '::' },
|
||||
bootstrap: { type: 'string', short: 'b' },
|
||||
'bootstrap-port': { type: 'string', default: '4222' },
|
||||
},
|
||||
});
|
||||
if (values.help) {
|
||||
console.log(`Usage: npm run dhtnode -- [options]
|
||||
|
||||
-p, --port PORT Local UDP port (default: 4222; 0 selects a free port)
|
||||
-b, --bootstrap HOST Optional bootstrap hostname or IP address
|
||||
--bootstrap-port PORT Bootstrap UDP port (default: 4222)
|
||||
--ipv4 ADDRESS IPv4 bind address (default: 0.0.0.0)
|
||||
--ipv6 ADDRESS IPv6 bind address (default: ::)
|
||||
-h, --help Show help
|
||||
|
||||
Commands: ${commands}
|
||||
Keys are text labels hashed with SHA-1. Values are UTF-8 text.`);
|
||||
return;
|
||||
}
|
||||
const port = portNumber(values.port, 'port');
|
||||
const bootstrapPort = portNumber(values['bootstrap-port'], 'bootstrap-port', 1);
|
||||
const { DhtRunner, Identity, hash } = await import('../index.js');
|
||||
const identity = await Identity.generate('dhtnode.ts');
|
||||
const runner = new DhtRunner();
|
||||
const listeners = new Map<string, Listener>();
|
||||
const pending = new Set<Promise<void>>();
|
||||
let lines: Interface | undefined;
|
||||
let closing: Promise<void> | undefined;
|
||||
|
||||
function close(): Promise<void> {
|
||||
if (!closing) {
|
||||
for (const listener of listeners.values()) listener.cancel();
|
||||
listeners.clear();
|
||||
lines?.close();
|
||||
closing = runner.close();
|
||||
}
|
||||
return closing;
|
||||
}
|
||||
|
||||
function print(message: string): void {
|
||||
if (process.stdout.isTTY && lines) {
|
||||
clearLine(process.stdout, 0);
|
||||
cursorTo(process.stdout, 0);
|
||||
}
|
||||
console.log(message);
|
||||
if (!closing && process.stdin.isTTY) lines?.prompt(true);
|
||||
}
|
||||
|
||||
const stop = (): void => { close().catch(report); };
|
||||
const printValue = (label: string, value: Value): void => {
|
||||
print(`${label} ${value.id}: ${JSON.stringify(value.data.toString('utf8'))}${value.isSigned ? ' [signed]' : ''}`);
|
||||
};
|
||||
const printStatus = (status: ConnectivityStatus): void => {
|
||||
print(`Connectivity: IPv4 ${status.ipv4}, IPv6 ${status.ipv6}`);
|
||||
};
|
||||
|
||||
async function execute(line: string): Promise<void> {
|
||||
const [, command, key, data] = line.trim().match(/^(\S+)(?:\s+(\S+))?(?:\s+(.+))?$/) || [];
|
||||
if (!command) return;
|
||||
if (command === 'help') {
|
||||
print(commands);
|
||||
return;
|
||||
}
|
||||
if (!key) throw new Error('Expected a key. Type help for commands.');
|
||||
const location = hash(key);
|
||||
switch (command) {
|
||||
case 'get': {
|
||||
const found = await runner.getAll(location);
|
||||
if (closing) return;
|
||||
for (const value of found) printValue(key, value);
|
||||
if (!found.length) print(`No values for ${key}`);
|
||||
break;
|
||||
}
|
||||
case 'put':
|
||||
case 'sign': {
|
||||
if (data === undefined) throw new Error(`Usage: ${command} KEY TEXT`);
|
||||
const payload = Buffer.from(data, 'utf8');
|
||||
const ok = command === 'sign' ? await runner.putSigned(location, payload)
|
||||
: await runner.put(location, payload);
|
||||
if (closing) return;
|
||||
if (!ok) throw new Error(`Put failed for ${key}`);
|
||||
print(`Stored ${key}`);
|
||||
break;
|
||||
}
|
||||
case 'listen':
|
||||
if (!listeners.has(key)) {
|
||||
listeners.set(key, runner.listen(location, (value, expired) => {
|
||||
printValue(`${expired ? 'Expired' : 'Value'} ${key}`, value);
|
||||
}));
|
||||
}
|
||||
print(`Listening ${key}`);
|
||||
break;
|
||||
case 'cancel':
|
||||
listeners.get(key)?.cancel();
|
||||
listeners.delete(key);
|
||||
print(`Cancelled ${key}`);
|
||||
break;
|
||||
default:
|
||||
throw new Error('Unknown command. Type help for commands.');
|
||||
}
|
||||
}
|
||||
|
||||
runner.on('error', error => { report(error); stop(); });
|
||||
runner.on('statusChanged', printStatus);
|
||||
process.on('SIGINT', stop);
|
||||
process.on('SIGTERM', stop);
|
||||
try {
|
||||
runner.run({ port, ipv4: values.ipv4, ipv6: values.ipv6, identity });
|
||||
if (values.bootstrap) runner.bootstrap(values.bootstrap, bootstrapPort);
|
||||
lines = createInterface({ input: process.stdin, output: process.stdout });
|
||||
lines.on('SIGINT', stop);
|
||||
lines.setPrompt('dht> ');
|
||||
print(`Node ${runner.getNodeId()} listening on UDP ${runner.getBoundPort()}`);
|
||||
printStatus(runner.getStatus());
|
||||
if (process.stdin.isTTY) print(commands);
|
||||
for await (const line of lines) {
|
||||
if (closing) break;
|
||||
if (/^(quit|exit)(?:\s|$)/.test(line.trim())) {
|
||||
stop();
|
||||
break;
|
||||
}
|
||||
const operation = execute(line).catch(error => {
|
||||
if (!closing) console.error(errorMessage(error));
|
||||
}).finally(() => { pending.delete(operation); });
|
||||
pending.add(operation);
|
||||
if (process.stdin.isTTY) lines.prompt();
|
||||
}
|
||||
if (!closing) await Promise.all(pending);
|
||||
} finally {
|
||||
try {
|
||||
await close();
|
||||
await Promise.all(pending);
|
||||
} finally {
|
||||
process.removeListener('SIGINT', stop);
|
||||
process.removeListener('SIGTERM', stop);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(report);
|
||||
Vendored
+120
@@ -0,0 +1,120 @@
|
||||
import { Buffer } from 'node:buffer';
|
||||
import { EventEmitter } from 'node:events';
|
||||
|
||||
export const version: string;
|
||||
export function hash(data: string | Uint8Array): string;
|
||||
|
||||
export interface KeyOptions {
|
||||
algorithm?: 'ec' | 'rsa';
|
||||
bits?: number;
|
||||
}
|
||||
|
||||
export class PublicKey {
|
||||
constructor(data: Uint8Array);
|
||||
serialize(): Buffer;
|
||||
getId(): string;
|
||||
getLongId(): string;
|
||||
checkSignature(data: Uint8Array, signature: Uint8Array): boolean;
|
||||
encrypt(data: Uint8Array): Buffer;
|
||||
}
|
||||
|
||||
export class PrivateKey {
|
||||
constructor(data: Uint8Array, password?: string);
|
||||
static generate(options?: KeyOptions): Promise<PrivateKey>;
|
||||
serialize(password?: string): Buffer;
|
||||
getPublicKey(): PublicKey;
|
||||
sign(data: Uint8Array): Buffer;
|
||||
decrypt(data: Uint8Array): Buffer;
|
||||
}
|
||||
|
||||
export class Certificate {
|
||||
constructor(data: Uint8Array);
|
||||
serialize(): Buffer;
|
||||
getPublicKey(): PublicKey;
|
||||
getId(): string;
|
||||
getLongId(): string;
|
||||
}
|
||||
|
||||
export class Identity {
|
||||
constructor(privateKey: PrivateKey, certificate: Certificate);
|
||||
static generate(name?: string, options?: KeyOptions): Promise<Identity>;
|
||||
readonly privateKey: PrivateKey;
|
||||
readonly certificate: Certificate;
|
||||
}
|
||||
|
||||
export interface RunOptions {
|
||||
port?: number;
|
||||
ipv4?: string;
|
||||
ipv6?: string;
|
||||
network?: number;
|
||||
persistPath?: string;
|
||||
identity?: Identity;
|
||||
proxyServer?: string;
|
||||
proxyUserAgent?: string;
|
||||
}
|
||||
|
||||
export interface PutOptions {
|
||||
id?: bigint;
|
||||
type?: number;
|
||||
userType?: string;
|
||||
permanent?: boolean;
|
||||
}
|
||||
|
||||
export interface Value {
|
||||
id: bigint;
|
||||
type: number;
|
||||
seq: number;
|
||||
data: Buffer;
|
||||
userType: string;
|
||||
owner: string | null;
|
||||
isSigned: boolean;
|
||||
isEncrypted: boolean;
|
||||
}
|
||||
|
||||
export interface Listener {
|
||||
cancel(): void;
|
||||
}
|
||||
|
||||
export interface ListenEvent {
|
||||
value: Value;
|
||||
expired: boolean;
|
||||
}
|
||||
|
||||
export interface ValueIterator<T> extends AsyncIterableIterator<T, undefined>, Listener, AsyncDisposable {
|
||||
return(): Promise<IteratorResult<T, undefined>>;
|
||||
throw(error?: unknown): Promise<IteratorResult<T, undefined>>;
|
||||
}
|
||||
|
||||
export interface ListenerIterator extends ValueIterator<ListenEvent> { }
|
||||
|
||||
export type NodeStatus = 'disconnected' | 'connecting' | 'connected';
|
||||
|
||||
export interface ConnectivityStatus {
|
||||
ipv4: NodeStatus;
|
||||
ipv6: NodeStatus;
|
||||
}
|
||||
|
||||
export interface RunnerEvents {
|
||||
statusChanged: [status: ConnectivityStatus];
|
||||
error: [error: Error];
|
||||
}
|
||||
|
||||
export class DhtRunner extends EventEmitter<RunnerEvents> implements AsyncDisposable {
|
||||
run(options?: RunOptions): void;
|
||||
isRunning(): boolean;
|
||||
getBoundPort(family?: 4 | 6): number;
|
||||
getNodeId(): string;
|
||||
getStatus(): ConnectivityStatus;
|
||||
bootstrap(host: string, port: number): void;
|
||||
get(key: string): ValueIterator<Value>;
|
||||
getAll(key: string): Promise<Value[]>;
|
||||
put(key: string, data: Uint8Array, options?: PutOptions): Promise<boolean>;
|
||||
putSigned(key: string, data: Uint8Array, options?: PutOptions): Promise<boolean>;
|
||||
putEncrypted(key: string, data: Uint8Array, recipient: PublicKey, options?: PutOptions): Promise<boolean>;
|
||||
listen(key: string): ListenerIterator;
|
||||
listen(key: string, callback: (value: Value, expired: boolean) => void): Listener;
|
||||
close(): Promise<void>;
|
||||
ref(): this;
|
||||
unref(): this;
|
||||
[Symbol.asyncDispose](): Promise<void>;
|
||||
}
|
||||
+187
@@ -0,0 +1,187 @@
|
||||
import { EventEmitter } from 'node:events';
|
||||
import { constants } from 'node:os';
|
||||
import { dlopen } from 'node:process';
|
||||
import load from 'node-gyp-build';
|
||||
import createCrypto from './crypto.js';
|
||||
|
||||
const binding = { exports: {} };
|
||||
const flags = process.platform === 'linux'
|
||||
? constants.dlopen.RTLD_LAZY | constants.dlopen.RTLD_DEEPBIND
|
||||
: undefined;
|
||||
dlopen(binding, process.env.OPENDHT_NODE_BINDING || load.path(import.meta.dirname),
|
||||
flags);
|
||||
const native = binding.exports;
|
||||
const { PublicKey, PrivateKey, Certificate, Identity, handle } = createCrypto(native);
|
||||
|
||||
function eventReporter(owner, event) {
|
||||
return value => { owner.deref()?.emit(event, value); };
|
||||
}
|
||||
|
||||
class DhtRunner extends EventEmitter {
|
||||
#native;
|
||||
#closing;
|
||||
#listeners = 0;
|
||||
#pending = 0;
|
||||
#iterators = new Set();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const owner = new WeakRef(this);
|
||||
this.#native = new native.NativeRunner(eventReporter(owner, 'statusChanged'));
|
||||
}
|
||||
|
||||
#updateOwner() {
|
||||
this.#native.setOwner(this.#listeners || this.#pending || this.listenerCount('statusChanged') ? this : null);
|
||||
}
|
||||
|
||||
#track(operation) {
|
||||
this.#pending++;
|
||||
this.#updateOwner();
|
||||
return operation.finally(() => {
|
||||
this.#pending--;
|
||||
this.#updateOwner();
|
||||
});
|
||||
}
|
||||
|
||||
on(event, listener) { super.on(event, listener); this.#updateOwner(); return this; }
|
||||
addListener(event, listener) { return this.on(event, listener); }
|
||||
prependListener(event, listener) { super.prependListener(event, listener); this.#updateOwner(); return this; }
|
||||
removeListener(event, listener) { super.removeListener(event, listener); this.#updateOwner(); return this; }
|
||||
off(event, listener) { return this.removeListener(event, listener); }
|
||||
removeAllListeners(...args) { super.removeAllListeners(...args); this.#updateOwner(); return this; }
|
||||
|
||||
run(options) {
|
||||
if (typeof options?.proxyServer === 'string' && options.proxyServer) {
|
||||
const proxy = new URL(options.proxyServer);
|
||||
if (proxy.protocol !== 'http:' && proxy.protocol !== 'https:')
|
||||
throw new TypeError('proxyServer must be an HTTP(S) URL');
|
||||
}
|
||||
if (options?.identity !== undefined) options = { ...options, identity: handle(options.identity, Identity) };
|
||||
this.#native.run(options);
|
||||
}
|
||||
isRunning() { return this.#native.isRunning(); }
|
||||
getBoundPort(family = 4) { return this.#native.getBoundPort(family); }
|
||||
getNodeId() { return this.#native.getNodeId(); }
|
||||
getStatus() { return this.#native.getStatus(); }
|
||||
bootstrap(host, port) { this.#native.bootstrap(host, port); }
|
||||
get(key) {
|
||||
return this.#iterate((push, finish) => {
|
||||
const operation = this.#native.get(key, push);
|
||||
this.#track(operation.done).then(() => finish(), finish);
|
||||
return () => this.#native.cancelGet(operation.token);
|
||||
}, true);
|
||||
}
|
||||
getAll(key) { return this.#track(this.#native.getAll(key)); }
|
||||
put(key, data, options) { return this.#track(this.#native.put(key, data, options)); }
|
||||
putSigned(key, data, options) { return this.#track(this.#native.putSigned(key, data, options)); }
|
||||
putEncrypted(key, data, recipient, options) {
|
||||
return this.#track(this.#native.putEncrypted(key, data, options, handle(recipient, PublicKey)));
|
||||
}
|
||||
|
||||
listen(key, callback) {
|
||||
if (callback === undefined) {
|
||||
return this.#iterate(push => {
|
||||
const subscription = this.listen(key, (value, expired) => push({ value, expired }));
|
||||
return () => subscription.cancel();
|
||||
});
|
||||
}
|
||||
const token = this.#native.listen(key, callback);
|
||||
this.#listeners++;
|
||||
this.#updateOwner();
|
||||
let cancelled = false;
|
||||
return {
|
||||
cancel: () => {
|
||||
if (cancelled) return;
|
||||
this.#native.cancelListen(token);
|
||||
cancelled = true;
|
||||
this.#listeners--;
|
||||
this.#updateOwner();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
#iterate(subscribe, rejectOnClose = false) {
|
||||
let values = [];
|
||||
let offset = 0;
|
||||
const pending = [];
|
||||
let done = false;
|
||||
let failure;
|
||||
const finish = error => {
|
||||
if (done) return;
|
||||
done = true;
|
||||
failure = error;
|
||||
unsubscribe();
|
||||
this.#iterators.delete(onClose);
|
||||
if (failure) {
|
||||
values = [];
|
||||
offset = 0;
|
||||
}
|
||||
for (const { resolve, reject } of pending.splice(0)) {
|
||||
if (failure) reject(failure);
|
||||
else resolve({ value: undefined, done: true });
|
||||
}
|
||||
};
|
||||
const cancel = () => {
|
||||
values = [];
|
||||
offset = 0;
|
||||
failure = undefined;
|
||||
finish();
|
||||
};
|
||||
const onClose = () => {
|
||||
if (rejectOnClose) {
|
||||
const error = new Error('DHT runner is closed');
|
||||
error.code = 'ERR_DHT_CLOSED';
|
||||
finish(error);
|
||||
} else cancel();
|
||||
};
|
||||
const unsubscribe = subscribe(value => {
|
||||
if (done) return;
|
||||
const waiter = pending.shift();
|
||||
if (waiter) waiter.resolve({ value, done: false });
|
||||
else values.push(value);
|
||||
}, finish);
|
||||
this.#iterators.add(onClose);
|
||||
return {
|
||||
next() {
|
||||
if (failure) return Promise.reject(failure);
|
||||
if (offset < values.length) {
|
||||
const value = values[offset];
|
||||
values[offset++] = undefined;
|
||||
if (offset === values.length) {
|
||||
values = [];
|
||||
offset = 0;
|
||||
}
|
||||
return Promise.resolve({ value, done: false });
|
||||
}
|
||||
if (done) return Promise.resolve({ value: undefined, done: true });
|
||||
return new Promise((resolve, reject) => pending.push({ resolve, reject }));
|
||||
},
|
||||
return() {
|
||||
cancel();
|
||||
return Promise.resolve({ value: undefined, done: true });
|
||||
},
|
||||
throw(error) {
|
||||
cancel();
|
||||
return Promise.reject(error);
|
||||
},
|
||||
cancel,
|
||||
[Symbol.asyncIterator]() { return this; },
|
||||
[Symbol.asyncDispose]() { cancel(); return Promise.resolve(); },
|
||||
};
|
||||
}
|
||||
|
||||
close() {
|
||||
if (!this.#closing) {
|
||||
this.#closing = this.#native.close();
|
||||
for (const cancel of this.#iterators) cancel();
|
||||
}
|
||||
return this.#closing;
|
||||
}
|
||||
ref() { this.#native.ref(); return this; }
|
||||
unref() { this.#native.unref(); return this; }
|
||||
[Symbol.asyncDispose]() { return this.close(); }
|
||||
}
|
||||
|
||||
export { DhtRunner, PublicKey, PrivateKey, Certificate, Identity };
|
||||
export const hash = native.hash;
|
||||
export const version = native.version;
|
||||
Generated
+904
@@ -0,0 +1,904 @@
|
||||
{
|
||||
"name": "opendht",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "opendht",
|
||||
"version": "0.1.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"node-gyp-build": "^4.8.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.20.5",
|
||||
"cmake-js": "^8.0.0",
|
||||
"node-addon-api": "^8.5.0",
|
||||
"typescript": "^7.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/fs-minipass": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
|
||||
"integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"minipass": "^7.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.20.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.5.tgz",
|
||||
"integrity": "sha512-U2+DNr+wSjpsTS/wZGYHq7GcwfuSmKiKvoPvK22zwTlRhU91yOniN4qRR5KhIjvif7ysw/dz/hKmfDH0Ris4aA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-aix-ppc64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz",
|
||||
"integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-darwin-arm64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz",
|
||||
"integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-darwin-x64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz",
|
||||
"integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-freebsd-arm64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz",
|
||||
"integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-freebsd-x64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz",
|
||||
"integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-linux-arm": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz",
|
||||
"integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-linux-arm64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz",
|
||||
"integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-linux-loong64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz",
|
||||
"integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-linux-mips64el": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz",
|
||||
"integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-linux-ppc64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz",
|
||||
"integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-linux-riscv64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz",
|
||||
"integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-linux-s390x": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz",
|
||||
"integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-linux-x64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz",
|
||||
"integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-netbsd-arm64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz",
|
||||
"integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-netbsd-x64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz",
|
||||
"integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-openbsd-arm64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz",
|
||||
"integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-openbsd-x64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz",
|
||||
"integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-sunos-x64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz",
|
||||
"integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-win32-arm64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz",
|
||||
"integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-win32-x64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz",
|
||||
"integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/chownr": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
|
||||
"integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
||||
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^4.2.0",
|
||||
"strip-ansi": "^6.0.1",
|
||||
"wrap-ansi": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/cmake-js": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cmake-js/-/cmake-js-8.0.0.tgz",
|
||||
"integrity": "sha512-YbUP88RDwCvoQkZhRtGURYm9RIpWdtvZuhT87fKNoLjk8kIFIFeARpKfuZQGdwfH99GZpUmqSfcDrK62X7lTgg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.4.3",
|
||||
"fs-extra": "^11.3.3",
|
||||
"node-api-headers": "^1.8.0",
|
||||
"rc": "1.2.8",
|
||||
"semver": "^7.7.3",
|
||||
"tar": "^7.5.6",
|
||||
"url-join": "^4.0.1",
|
||||
"which": "^6.0.0",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"bin": {
|
||||
"cmake-js": "bin/cmake-js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.17.0 || >=22.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/deep-extend": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
|
||||
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
||||
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/fs-extra": {
|
||||
"version": "11.4.1",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.4.1.tgz",
|
||||
"integrity": "sha512-KYAb4c9BJQI6QqGKthV68OHe0badztdXJWKo0WtBA9IuCFPTKvE5ZdUBglP833aMjhaSPNO4A5j/EkzZtGlKjA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.0",
|
||||
"jsonfile": "^6.0.1",
|
||||
"universalify": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.14"
|
||||
}
|
||||
},
|
||||
"node_modules/get-caller-file": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "6.* || 8.* || >= 10.*"
|
||||
}
|
||||
},
|
||||
"node_modules/graceful-fs": {
|
||||
"version": "4.2.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ini": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
|
||||
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/isexe": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz",
|
||||
"integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/jsonfile": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz",
|
||||
"integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"universalify": "^2.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"graceful-fs": "^4.1.6"
|
||||
}
|
||||
},
|
||||
"node_modules/minimist": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/minipass": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",
|
||||
"integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/minizlib": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz",
|
||||
"integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"minipass": "^7.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-addon-api": {
|
||||
"version": "8.9.2",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.9.2.tgz",
|
||||
"integrity": "sha512-VijLXbi3UACN69I0JVXJsX4tjACjNoQDgv2gTF6sx2wWEi8tkSg2eX8p5gSIFi8z2+DL3oHmY6OyKce38SDolg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18 || ^20 || >= 21"
|
||||
}
|
||||
},
|
||||
"node_modules/node-api-headers": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/node-api-headers/-/node-api-headers-1.9.0.tgz",
|
||||
"integrity": "sha512-2oNILP4jXwRB4ywnYKjVk1YyJ96n2D4EOVJO6S3oYZ5PtbJrw3Yt9TpAuX3nBLMuzn74rnfGQrv13pS9vC+YiA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-gyp-build": {
|
||||
"version": "4.8.4",
|
||||
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
|
||||
"integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"node-gyp-build": "bin.js",
|
||||
"node-gyp-build-optional": "optional.js",
|
||||
"node-gyp-build-test": "build-test.js"
|
||||
}
|
||||
},
|
||||
"node_modules/rc": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
|
||||
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
|
||||
"dev": true,
|
||||
"license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
|
||||
"dependencies": {
|
||||
"deep-extend": "^0.6.0",
|
||||
"ini": "~1.3.0",
|
||||
"minimist": "^1.2.0",
|
||||
"strip-json-comments": "~2.0.1"
|
||||
},
|
||||
"bin": {
|
||||
"rc": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.8.5",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
|
||||
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-json-comments": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
|
||||
"integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tar": {
|
||||
"version": "7.5.22",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz",
|
||||
"integrity": "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"@isaacs/fs-minipass": "^4.0.0",
|
||||
"chownr": "^3.0.0",
|
||||
"minipass": "^7.1.2",
|
||||
"minizlib": "^3.1.0",
|
||||
"yallist": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz",
|
||||
"integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@typescript/typescript-aix-ppc64": "7.0.2",
|
||||
"@typescript/typescript-darwin-arm64": "7.0.2",
|
||||
"@typescript/typescript-darwin-x64": "7.0.2",
|
||||
"@typescript/typescript-freebsd-arm64": "7.0.2",
|
||||
"@typescript/typescript-freebsd-x64": "7.0.2",
|
||||
"@typescript/typescript-linux-arm": "7.0.2",
|
||||
"@typescript/typescript-linux-arm64": "7.0.2",
|
||||
"@typescript/typescript-linux-loong64": "7.0.2",
|
||||
"@typescript/typescript-linux-mips64el": "7.0.2",
|
||||
"@typescript/typescript-linux-ppc64": "7.0.2",
|
||||
"@typescript/typescript-linux-riscv64": "7.0.2",
|
||||
"@typescript/typescript-linux-s390x": "7.0.2",
|
||||
"@typescript/typescript-linux-x64": "7.0.2",
|
||||
"@typescript/typescript-netbsd-arm64": "7.0.2",
|
||||
"@typescript/typescript-netbsd-x64": "7.0.2",
|
||||
"@typescript/typescript-openbsd-arm64": "7.0.2",
|
||||
"@typescript/typescript-openbsd-x64": "7.0.2",
|
||||
"@typescript/typescript-sunos-x64": "7.0.2",
|
||||
"@typescript/typescript-win32-arm64": "7.0.2",
|
||||
"@typescript/typescript-win32-x64": "7.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/universalify": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
|
||||
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/url-join": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz",
|
||||
"integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz",
|
||||
"integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"isexe": "^4.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"node-which": "bin/which.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.17.0 || >=22.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/y18n": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
|
||||
"integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs": {
|
||||
"version": "17.7.3",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz",
|
||||
"integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cliui": "^8.0.1",
|
||||
"escalade": "^3.1.1",
|
||||
"get-caller-file": "^2.0.5",
|
||||
"require-directory": "^2.1.1",
|
||||
"string-width": "^4.2.3",
|
||||
"y18n": "^5.0.5",
|
||||
"yargs-parser": "^21.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs-parser": {
|
||||
"version": "21.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
|
||||
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"name": "opendht",
|
||||
"version": "0.1.0",
|
||||
"description": "Distributed hash table for Node.js",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/savoirfairelinux/opendht.git",
|
||||
"directory": "nodejs"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
},
|
||||
"type": "module",
|
||||
"main": "index.js",
|
||||
"types": "index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./index.d.ts",
|
||||
"import": "./index.js"
|
||||
},
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"files": [
|
||||
"index.js",
|
||||
"index.d.ts",
|
||||
"crypto.js",
|
||||
"README.md",
|
||||
"examples/dhtnode.js",
|
||||
"examples/dhtnode.ts",
|
||||
"prebuilds/"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=22.9.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "cmake-js compile",
|
||||
"postbuild": "npm run build:examples",
|
||||
"build:examples": "tsc --strict --types node --target es2024 --module Node16 --moduleResolution Node16 --rootDir examples --outDir examples examples/dhtnode.ts",
|
||||
"dhtnode": "node examples/dhtnode.js",
|
||||
"prepack": "npm run build:examples",
|
||||
"bundle": "node scripts/bundle.js",
|
||||
"package": "npm run bundle && npm pack --pack-destination dist",
|
||||
"test:package": "node scripts/test-package.js",
|
||||
"test": "node --test test/*.test.js",
|
||||
"typecheck": "tsc --noEmit --strict --types node --target es2024 --module Node16 --moduleResolution Node16 test/types.ts examples/dhtnode.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"node-gyp-build": "^4.8.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.20.5",
|
||||
"cmake-js": "^8.0.0",
|
||||
"node-addon-api": "^8.8.0",
|
||||
"typescript": "^7.0.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
const root = path.resolve(import.meta.dirname, '..');
|
||||
const repository = path.dirname(root);
|
||||
const input = path.resolve(process.argv[2] || path.join(root, 'build/Release/opendht.node'));
|
||||
const target = `${process.platform}-${process.arch}`;
|
||||
const output = path.join(root, 'prebuilds', target);
|
||||
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), 'opendht-bundle-'));
|
||||
const run = (command, args) => execFileSync(command, args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'inherit'] });
|
||||
|
||||
function copyNotices(dependencies) {
|
||||
const directory = path.join(output, 'licenses');
|
||||
fs.mkdirSync(directory, { recursive: true });
|
||||
fs.copyFileSync(path.join(repository, 'LICENSE'), path.join(directory, 'OpenDHT-LICENSE.txt'));
|
||||
const copied = new Set();
|
||||
for (const dependency of dependencies) {
|
||||
const real = fs.realpathSync(dependency);
|
||||
if (!path.relative(repository, real).startsWith('..')) continue;
|
||||
let name;
|
||||
let notices;
|
||||
if (process.platform === 'darwin') {
|
||||
const match = real.match(/^(.*\/Cellar\/([^/]+)\/[^/]+)\//);
|
||||
if (!match) throw new Error(`Cannot locate license notices for ${dependency}`);
|
||||
name = match[2];
|
||||
notices = fs.readdirSync(match[1]).filter(file => /^(COPYING|LICENSE|COPYRIGHT)([._-]|$)/i.test(file))
|
||||
.map(file => path.join(match[1], file));
|
||||
} else if (process.platform === 'linux' && /^libllhttp\.so(?:\.|$)/.test(path.basename(real))) {
|
||||
name = 'llhttp';
|
||||
notices = [path.join(repository, 'resources', 'llhttp-LICENSE-MIT')];
|
||||
} else if (process.platform === 'linux') {
|
||||
let owner;
|
||||
try { owner = run('dpkg-query', ['-S', real]); }
|
||||
catch { owner = run('dpkg-query', ['-S', real.replace(/^\/usr\//, '/')]); }
|
||||
name = owner.split('\n')[0].split(': ')[0].split(':')[0];
|
||||
notices = [path.join('/usr/share/doc', name, 'copyright')];
|
||||
} else {
|
||||
const installed = process.env.VCPKG_INSTALLED_DIR;
|
||||
const triplet = process.env.VCPKG_DEFAULT_TRIPLET || 'x64-windows-release';
|
||||
if (!installed) throw new Error('VCPKG_INSTALLED_DIR is required for Windows dependency notices');
|
||||
const share = path.join(installed, triplet, 'share');
|
||||
for (const entry of fs.readdirSync(share)) {
|
||||
const copyright = path.join(share, entry, 'copyright');
|
||||
if (fs.existsSync(copyright)) fs.copyFileSync(copyright, path.join(directory, `${entry}-copyright.txt`));
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (!notices.length) throw new Error(`Missing license notices for ${name}`);
|
||||
if (copied.has(name)) continue;
|
||||
for (const notice of notices) fs.copyFileSync(notice, path.join(directory, `${name}-${path.basename(notice)}.txt`));
|
||||
copied.add(name);
|
||||
}
|
||||
}
|
||||
|
||||
function fixMachO(file) {
|
||||
const commands = run('otool', ['-l', file]);
|
||||
const rpaths = [...commands.matchAll(/cmd LC_RPATH\s+cmdsize \d+\s+path (.+?) \(offset \d+\)/g)];
|
||||
for (const rpath of new Set(rpaths.map(match => match[1]))) run('install_name_tool', ['-delete_rpath', rpath, file]);
|
||||
const dependencies = [...run('otool', ['-L', file]).matchAll(/^\s+(.+?) \(compatibility version/gm)];
|
||||
for (const [, dependency] of dependencies) {
|
||||
if (!/^(@loader_path\/|\/usr\/lib\/|\/System\/Library\/)/.test(dependency)) {
|
||||
throw new Error(`Non-relocatable dependency in ${path.basename(file)}: ${dependency}`);
|
||||
}
|
||||
}
|
||||
run('codesign', ['--force', '--sign', '-', file]);
|
||||
}
|
||||
|
||||
try {
|
||||
if (!['darwin', 'linux', 'win32'].includes(process.platform)) throw new Error(`Unsupported platform ${target}`);
|
||||
if (!fs.existsSync(input)) throw new Error(`Build the Release addon first: ${input}`);
|
||||
if (process.platform === 'linux' && !process.report.getReport().header.glibcVersionRuntime) {
|
||||
throw new Error('Linux prebuilds currently require glibc');
|
||||
}
|
||||
const directories = [path.dirname(input), ...(process.env.OPENDHT_LIBRARY_PATH || '').split(path.delimiter).filter(Boolean)];
|
||||
const dependencyList = path.join(temporary, 'dependencies.json');
|
||||
run('cmake', [`-DADDON=${input}`, `-DOUTPUT=${dependencyList}`, `-DSEARCH_DIRECTORIES=${directories.join(';')}`,
|
||||
'-P', path.join(import.meta.dirname, 'dependencies.cmake')]);
|
||||
const dependencies = JSON.parse(fs.readFileSync(dependencyList, 'utf8'));
|
||||
fs.rmSync(output, { recursive: true, force: true });
|
||||
fs.mkdirSync(output, { recursive: true });
|
||||
const binary = path.join(output, process.platform === 'linux' ? 'node.napi.glibc.node' : 'node.napi.node');
|
||||
fs.copyFileSync(input, binary);
|
||||
if (process.platform === 'darwin') {
|
||||
run('dylibbundler', ['-b', '-of', '-ns', '-x', binary, '-d', `${output}/`, '-p', '@loader_path/', '-s', path.dirname(input)]);
|
||||
for (const file of fs.readdirSync(output)) fixMachO(path.join(output, file));
|
||||
} else {
|
||||
for (const dependency of dependencies) fs.copyFileSync(dependency, path.join(output, path.basename(dependency)));
|
||||
if (process.platform === 'linux') {
|
||||
for (const file of fs.readdirSync(output)) run('patchelf', ['--set-rpath', '$ORIGIN', path.join(output, file)]);
|
||||
}
|
||||
}
|
||||
copyNotices(dependencies);
|
||||
fs.writeFileSync(path.join(output, 'manifest.json'), JSON.stringify({
|
||||
platform: process.platform,
|
||||
arch: process.arch,
|
||||
napi: 8,
|
||||
libraries: fs.readdirSync(output).filter(file => /\.(node|dll|dylib)$|\.so(\.|$)/.test(file)).sort(),
|
||||
}, null, 2) + '\n');
|
||||
fs.mkdirSync(path.join(root, 'dist'), { recursive: true });
|
||||
console.log(`Bundled ${target}: ${output}`);
|
||||
} finally {
|
||||
fs.rmSync(temporary, { recursive: true, force: true });
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
cmake_minimum_required(VERSION 3.21)
|
||||
|
||||
if(NOT EXISTS "${ADDON}" OR NOT DEFINED OUTPUT)
|
||||
message(FATAL_ERROR "ADDON and OUTPUT are required")
|
||||
endif()
|
||||
|
||||
set(pre_exclude)
|
||||
set(post_exclude)
|
||||
if(CMAKE_HOST_SYSTEM_NAME STREQUAL "Darwin")
|
||||
list(APPEND pre_exclude "^/usr/lib/" "^/System/Library/")
|
||||
list(APPEND post_exclude "^/usr/lib/" "^/System/Library/")
|
||||
elseif(CMAKE_HOST_SYSTEM_NAME STREQUAL "Linux")
|
||||
list(APPEND pre_exclude "^ld-linux" "^lib(c|m|dl|rt|pthread|resolv|util|gcc_s|stdc\\+\\+)\\.so")
|
||||
elseif(CMAKE_HOST_WIN32)
|
||||
list(APPEND pre_exclude "[Nn][Oo][Dd][Ee]\\.exe" "[Aa][Pp][Ii]-[Mm][Ss]-.*" "[Ee][Xx][Tt]-[Mm][Ss]-.*")
|
||||
list(APPEND post_exclude ".*[Ss][Yy][Ss][Tt][Ee][Mm]32[/\\\\].*")
|
||||
endif()
|
||||
|
||||
file(GET_RUNTIME_DEPENDENCIES
|
||||
MODULES "${ADDON}"
|
||||
DIRECTORIES ${SEARCH_DIRECTORIES}
|
||||
RESOLVED_DEPENDENCIES_VAR dependencies
|
||||
UNRESOLVED_DEPENDENCIES_VAR unresolved
|
||||
CONFLICTING_DEPENDENCIES_PREFIX conflicts
|
||||
PRE_EXCLUDE_REGEXES ${pre_exclude}
|
||||
POST_EXCLUDE_REGEXES ${post_exclude}
|
||||
)
|
||||
if(unresolved OR conflicts_FILENAMES)
|
||||
message(FATAL_ERROR "Unresolved dependencies: ${unresolved}; conflicting dependencies: ${conflicts_FILENAMES}")
|
||||
endif()
|
||||
|
||||
set(result "[]")
|
||||
set(index 0)
|
||||
foreach(dependency IN LISTS dependencies)
|
||||
file(TO_CMAKE_PATH "${dependency}" dependency)
|
||||
string(JSON result SET "${result}" ${index} "\"${dependency}\"")
|
||||
math(EXPR index "${index} + 1")
|
||||
endforeach()
|
||||
file(WRITE "${OUTPUT}" "${result}\n")
|
||||
@@ -0,0 +1,38 @@
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
const root = path.resolve(import.meta.dirname, '..');
|
||||
const npm = process.env.npm_execpath;
|
||||
if (!npm) throw new Error('Run this check with npm run test:package');
|
||||
fs.mkdirSync(path.join(root, 'dist'), { recursive: true });
|
||||
let archive = process.argv[2] ? path.resolve(process.argv[2]) : path.join(root, 'dist',
|
||||
JSON.parse(execFileSync(process.execPath, [npm, 'pack', '--json', '--pack-destination', 'dist'],
|
||||
{ cwd: root, encoding: 'utf8' }))[0].filename);
|
||||
if (fs.statSync(archive).isDirectory()) {
|
||||
const archives = fs.readdirSync(archive).filter(file => file.endsWith('.tgz'));
|
||||
if (archives.length !== 1) throw new Error('Expected exactly one npm archive');
|
||||
archive = path.join(archive, archives[0]);
|
||||
}
|
||||
const consumer = fs.mkdtempSync(path.join(os.tmpdir(), 'opendht-consumer-'));
|
||||
const env = { ...process.env, PREBUILDS_ONLY: '1' };
|
||||
for (const variable of ['OPENDHT_NODE_BINDING', 'NODE_PATH', 'LD_LIBRARY_PATH', 'DYLD_LIBRARY_PATH', 'DYLD_FALLBACK_LIBRARY_PATH']) delete env[variable];
|
||||
const excluded = [path.join(root, 'build'), ...(process.env.OPENDHT_LIBRARY_PATH || '').split(path.delimiter).filter(Boolean)];
|
||||
const pathKey = Object.keys(env).find(key => key.toLowerCase() === 'path');
|
||||
if (pathKey) env[pathKey] = env[pathKey].split(path.delimiter)
|
||||
.filter(entry => !excluded.some(directory => path.resolve(entry).toLowerCase().startsWith(path.resolve(directory).toLowerCase())))
|
||||
.join(path.delimiter);
|
||||
try {
|
||||
execFileSync(process.execPath, [npm, 'install', '--prefix', consumer, '--ignore-scripts', '--omit=dev', '--no-audit', '--no-fund', archive],
|
||||
{ cwd: consumer, env, stdio: 'inherit' });
|
||||
const { name } = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
|
||||
const installed = path.join(consumer, 'node_modules', ...name.split('/'));
|
||||
execFileSync(process.execPath, ['--input-type=module', '-e', 'import load from "node-gyp-build"; console.log(load.path(process.cwd()))'],
|
||||
{ cwd: installed, env, stdio: 'inherit' });
|
||||
fs.cpSync(path.join(root, 'test'), path.join(installed, 'test'), { recursive: true });
|
||||
execFileSync(process.execPath, [npm, 'test'], { cwd: installed, env, stdio: 'inherit' });
|
||||
console.log(`Installed archive verified: ${archive}`);
|
||||
} finally {
|
||||
fs.rmSync(consumer, { recursive: true, force: true });
|
||||
}
|
||||
@@ -0,0 +1,549 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { execFile } from 'node:child_process';
|
||||
import { EventEmitter, once } from 'node:events';
|
||||
import { createServer } from 'node:http';
|
||||
import { test } from 'node:test';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { promisify } from 'node:util';
|
||||
import { Worker } from 'node:worker_threads';
|
||||
import { DhtRunner, hash, version } from '../index.js';
|
||||
|
||||
const moduleURL = new URL('../index.js', import.meta.url).href;
|
||||
const exampleURL = new URL('../examples/dhtnode.js', import.meta.url);
|
||||
const example = fileURLToPath(exampleURL);
|
||||
|
||||
function startNode() {
|
||||
const runner = new DhtRunner();
|
||||
runner.run({ port: 0, ipv4: '127.0.0.1', ipv6: '::1' });
|
||||
return runner;
|
||||
}
|
||||
|
||||
test('hashes strings and binary data without truncating NUL bytes', () => {
|
||||
assert.match(version, /^\d+\./);
|
||||
assert.equal(hash('hello'), 'aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d');
|
||||
assert.equal(hash('hello'), hash(Buffer.from('hello')));
|
||||
assert.notEqual(hash('hello\0world'), hash('hello'));
|
||||
assert.throws(() => hash(42), TypeError);
|
||||
});
|
||||
|
||||
test('runner lifecycle and argument validation', async () => {
|
||||
const runner = new DhtRunner();
|
||||
assert.equal(runner.isRunning(), false);
|
||||
assert.throws(() => runner.get(hash('idle')), /running/i);
|
||||
assert.throws(() => runner.getAll(hash('idle')), /running/i);
|
||||
assert.throws(() => runner.run({ port: -1 }), RangeError);
|
||||
assert.throws(() => runner.run({ port: 65536 }), RangeError);
|
||||
assert.throws(() => runner.run({ port: 1.5 }), RangeError);
|
||||
runner.run({ port: 0, ipv4: '127.0.0.1', ipv6: '::1' });
|
||||
try {
|
||||
assert.equal(runner.isRunning(), true);
|
||||
assert.ok(runner.getBoundPort() > 0);
|
||||
assert.match(runner.getNodeId(), /^[0-9a-f]{40}$/);
|
||||
assert.throws(() => runner.run(), /already/i);
|
||||
assert.throws(() => runner.get('short'), /hash/i);
|
||||
assert.throws(() => runner.get('z'.repeat(40)), /hash/i);
|
||||
assert.throws(() => runner.put(hash('bad'), 'text'), TypeError);
|
||||
assert.throws(() => runner.put(hash('bad'), Buffer.alloc(0), { id: 1 }), TypeError);
|
||||
assert.throws(() => runner.put(hash('bad'), Buffer.alloc(0), { id: -1n }), RangeError);
|
||||
assert.throws(() => runner.put(hash('bad'), Buffer.alloc(0), { id: 1n << 64n }), RangeError);
|
||||
assert.throws(() => runner.put(hash('bad'), Buffer.alloc(0), { type: 65536 }), RangeError);
|
||||
} finally {
|
||||
const closing = runner.close();
|
||||
assert.equal(runner.close(), closing);
|
||||
await closing;
|
||||
}
|
||||
assert.equal(runner.isRunning(), false);
|
||||
assert.throws(() => runner.run(), /closed/i);
|
||||
assert.throws(() => runner.get(hash('closed')), /closed/i);
|
||||
});
|
||||
|
||||
test('closing an idle runner is supported', async () => {
|
||||
const runner = new DhtRunner();
|
||||
await runner.close();
|
||||
await runner.close();
|
||||
assert.equal(runner.isRunning(), false);
|
||||
});
|
||||
|
||||
test('runner reports per-family connectivity changes and snapshots', { timeout: 15000 }, async () => {
|
||||
const runner = new DhtRunner();
|
||||
const peer = startNode();
|
||||
const changes = [];
|
||||
runner.on('statusChanged', status => changes.push(status));
|
||||
try {
|
||||
assert.deepEqual(runner.getStatus(), { ipv4: 'disconnected', ipv6: 'disconnected' });
|
||||
runner.run({ port: 0, ipv4: '127.0.0.1', ipv6: '::1' });
|
||||
const connected = new Promise((resolve, reject) => {
|
||||
const signal = AbortSignal.timeout(10000);
|
||||
signal.addEventListener('abort', () => reject(signal.reason), { once: true });
|
||||
runner.on('statusChanged', status => {
|
||||
if (status.ipv4 === 'connected') resolve(status);
|
||||
});
|
||||
});
|
||||
runner.bootstrap('127.0.0.1', peer.getBoundPort());
|
||||
peer.bootstrap('127.0.0.1', runner.getBoundPort());
|
||||
const status = await connected;
|
||||
assert.equal(status.ipv4, 'connected');
|
||||
assert.ok(['disconnected', 'connecting', 'connected'].includes(status.ipv6));
|
||||
assert.deepEqual(runner.getStatus(), status);
|
||||
status.ipv4 = 'changed by consumer';
|
||||
assert.equal(runner.getStatus().ipv4, 'connected');
|
||||
await runner.close();
|
||||
const count = changes.length;
|
||||
await new Promise(resolve => setImmediate(resolve));
|
||||
assert.equal(changes.length, count);
|
||||
assert.deepEqual(runner.getStatus(), { ipv4: 'disconnected', ipv6: 'disconnected' });
|
||||
} finally {
|
||||
await Promise.all([runner.close(), peer.close()]);
|
||||
}
|
||||
});
|
||||
|
||||
test('invalid bind addresses produce JS exceptions', async () => {
|
||||
const runner = new DhtRunner();
|
||||
try {
|
||||
assert.throws(() => runner.run({ ipv4: '192.0.2.1', ipv6: '2001:db8::1' }));
|
||||
} finally {
|
||||
await runner.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('two loopback nodes exchange binary values and cancel listeners', { timeout: 20000 }, async () => {
|
||||
const writer = startNode();
|
||||
const reader = startNode();
|
||||
const events = new EventEmitter();
|
||||
const key = hash('node-addon-api integration');
|
||||
const payload = Buffer.from([0, 1, 255, 0, 42]);
|
||||
let subscription;
|
||||
let retainedValue;
|
||||
try {
|
||||
writer.bootstrap('127.0.0.1', reader.getBoundPort());
|
||||
reader.bootstrap('127.0.0.1', writer.getBoundPort());
|
||||
subscription = reader.listen(key, (value, expired) => {
|
||||
if (!expired) events.emit('value', value);
|
||||
});
|
||||
const received = once(events, 'value', { signal: AbortSignal.timeout(10000) });
|
||||
assert.equal(await writer.put(key, payload, {
|
||||
id: 18446744073709551614n,
|
||||
type: 0,
|
||||
userType: 'nodejs-test',
|
||||
permanent: true,
|
||||
}), true);
|
||||
const [value] = await received;
|
||||
assert.equal(value.id, 18446744073709551614n);
|
||||
assert.equal(value.type, 0);
|
||||
assert.ok(Buffer.isBuffer(value.data));
|
||||
assert.deepEqual(value.data, payload);
|
||||
assert.equal(value.userType, 'nodejs-test');
|
||||
assert.equal(typeof value.seq, 'number');
|
||||
assert.equal(value.owner, null);
|
||||
assert.equal(value.isSigned, false);
|
||||
assert.equal(value.isEncrypted, false);
|
||||
value.data.fill(7);
|
||||
const values = await reader.getAll(key);
|
||||
retainedValue = values.find(candidate => candidate.id === value.id);
|
||||
assert.ok(retainedValue);
|
||||
assert.deepEqual(retainedValue.data, payload);
|
||||
subscription.cancel();
|
||||
subscription.cancel();
|
||||
} finally {
|
||||
subscription?.cancel();
|
||||
await Promise.all([writer.close(), reader.close()]);
|
||||
}
|
||||
assert.deepEqual(retainedValue.data, payload);
|
||||
});
|
||||
|
||||
test('close settles pending operations and suppresses queued listener deliveries', { timeout: 10000 }, async () => {
|
||||
const runner = startNode();
|
||||
let deliveries = 0;
|
||||
const subscription = runner.listen(hash('closing'), () => deliveries++);
|
||||
const pending = runner.getAll(hash('unreachable'));
|
||||
const settled = assert.rejects(pending, { code: 'ERR_DHT_CLOSED' });
|
||||
await runner.close();
|
||||
await settled;
|
||||
subscription.cancel();
|
||||
assert.equal(deliveries, 0);
|
||||
});
|
||||
|
||||
test('get iterators settle pending reads on cancellation and reject on runner close', { timeout: 10000 }, async () => {
|
||||
const runner = startNode();
|
||||
try {
|
||||
const cancelled = runner.get(hash('cancel get'));
|
||||
assert.equal(cancelled[Symbol.asyncIterator](), cancelled);
|
||||
const pending = [cancelled.next(), cancelled.next()];
|
||||
await cancelled.return();
|
||||
cancelled.cancel();
|
||||
assert.deepEqual(await Promise.all(pending), [
|
||||
{ value: undefined, done: true },
|
||||
{ value: undefined, done: true },
|
||||
]);
|
||||
assert.deepEqual(await cancelled.next(), { value: undefined, done: true });
|
||||
|
||||
const closing = runner.get(hash('close get'));
|
||||
const rejected = assert.rejects(closing.next(), { code: 'ERR_DHT_CLOSED' });
|
||||
await runner.close();
|
||||
await rejected;
|
||||
await assert.rejects(closing.next(), { code: 'ERR_DHT_CLOSED' });
|
||||
} finally {
|
||||
await runner.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('async listeners deliver queued values and cancel on loop exit', { timeout: 15000 }, async () => {
|
||||
const writer = startNode();
|
||||
const reader = startNode();
|
||||
const key = hash('async listener');
|
||||
let iterator;
|
||||
try {
|
||||
writer.bootstrap('127.0.0.1', reader.getBoundPort());
|
||||
reader.bootstrap('127.0.0.1', writer.getBoundPort());
|
||||
iterator = reader.listen(key);
|
||||
assert.equal(iterator[Symbol.asyncIterator](), iterator);
|
||||
const pending = iterator.next();
|
||||
assert.equal(await writer.put(key, Buffer.from('first'), { id: 1n }), true);
|
||||
const first = await pending;
|
||||
assert.equal(first.done, false);
|
||||
assert.equal(first.value.expired, false);
|
||||
assert.equal(first.value.value.id, 1n);
|
||||
assert.equal(first.value.value.data.toString(), 'first');
|
||||
|
||||
assert.equal(await writer.put(key, Buffer.from('second'), { id: 2n }), true);
|
||||
for await (const { value, expired } of iterator) {
|
||||
assert.equal(expired, false);
|
||||
assert.equal(value.id, 2n);
|
||||
assert.equal(value.data.toString(), 'second');
|
||||
break;
|
||||
}
|
||||
assert.deepEqual(await iterator.next(), { value: undefined, done: true });
|
||||
iterator.cancel();
|
||||
} finally {
|
||||
iterator?.cancel();
|
||||
await Promise.all([writer.close(), reader.close()]);
|
||||
}
|
||||
});
|
||||
|
||||
test('async listener cancellation settles concurrent reads and drops buffered values', { timeout: 10000 }, async () => {
|
||||
const runner = startNode();
|
||||
const peer = startNode();
|
||||
const key = hash('async cancellation');
|
||||
let iterator;
|
||||
try {
|
||||
runner.bootstrap('127.0.0.1', peer.getBoundPort());
|
||||
peer.bootstrap('127.0.0.1', runner.getBoundPort());
|
||||
iterator = runner.listen(key);
|
||||
const pending = [iterator.next(), iterator.next()];
|
||||
iterator.cancel();
|
||||
iterator.cancel();
|
||||
assert.deepEqual(await Promise.all(pending), [
|
||||
{ value: undefined, done: true },
|
||||
{ value: undefined, done: true },
|
||||
]);
|
||||
assert.equal(await runner.put(key, Buffer.from('cancelled'), { id: 3n }), true);
|
||||
assert.deepEqual(await iterator.next(), { value: undefined, done: true });
|
||||
|
||||
const buffered = runner.listen(key);
|
||||
await runner.getAll(key);
|
||||
await buffered.return();
|
||||
assert.deepEqual(await buffered.next(), { value: undefined, done: true });
|
||||
} finally {
|
||||
iterator?.cancel();
|
||||
await Promise.all([runner.close(), peer.close()]);
|
||||
}
|
||||
});
|
||||
|
||||
test('async listener throw, disposal, and runner close settle pending reads', { timeout: 10000 }, async () => {
|
||||
const runner = startNode();
|
||||
try {
|
||||
const thrown = runner.listen(hash('async throw'));
|
||||
const pendingThrow = thrown.next();
|
||||
const failure = new Error('consumer failed');
|
||||
await assert.rejects(thrown.throw(failure), error => error === failure);
|
||||
assert.deepEqual(await pendingThrow, { value: undefined, done: true });
|
||||
assert.deepEqual(await thrown.next(), { value: undefined, done: true });
|
||||
|
||||
const disposed = runner.listen(hash('async dispose'));
|
||||
const pendingDispose = disposed.next();
|
||||
await disposed[Symbol.asyncDispose]();
|
||||
assert.deepEqual(await pendingDispose, { value: undefined, done: true });
|
||||
|
||||
const closing = runner.listen(hash('async close'));
|
||||
const pendingClose = [closing.next(), closing.next()];
|
||||
const close = runner.close();
|
||||
assert.equal(runner.close(), close);
|
||||
assert.deepEqual(await Promise.all(pendingClose), [
|
||||
{ value: undefined, done: true },
|
||||
{ value: undefined, done: true },
|
||||
]);
|
||||
await close;
|
||||
assert.deepEqual(await closing.next(), { value: undefined, done: true });
|
||||
assert.throws(() => runner.listen(hash('closed iterator')), /closed/i);
|
||||
} finally {
|
||||
await runner.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('proxy configuration validates option types before starting', async () => {
|
||||
const runner = new DhtRunner();
|
||||
try {
|
||||
assert.throws(() => runner.run({ proxyServer: 42 }), TypeError);
|
||||
assert.throws(() => runner.run({ proxyUserAgent: false }), TypeError);
|
||||
assert.throws(() => runner.run({ proxyUserAgent: 'client\r\nInjected: header' }), TypeError);
|
||||
assert.throws(() => runner.run({ proxyServer: 'http://127.0.0.1\0' }), TypeError);
|
||||
assert.throws(() => runner.run({ proxyServer: 'ftp://127.0.0.1' }), /http/i);
|
||||
assert.throws(() => runner.run({ proxyServer: 'not a URL' }), /url/i);
|
||||
assert.equal(runner.isRunning(), false);
|
||||
runner.run({ port: 0, ipv4: '127.0.0.1', ipv6: '::1', proxyServer: '' });
|
||||
assert.ok(runner.getBoundPort() > 0);
|
||||
} finally {
|
||||
await runner.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('proxy configuration routes operations through HTTP with a custom user agent', { timeout: 15000 }, async () => {
|
||||
const runner = new DhtRunner();
|
||||
const requests = [];
|
||||
const values = new Map();
|
||||
let streamNextGet = false;
|
||||
let streamResponse;
|
||||
const server = createServer(async (request, response) => {
|
||||
response.setHeader('content-type', 'application/json');
|
||||
if (request.url === '/') {
|
||||
response.end(JSON.stringify({
|
||||
node_id: hash('proxy fixture'),
|
||||
ipv4: { good: 1 },
|
||||
ipv6: {},
|
||||
public_ip: '127.0.0.1',
|
||||
}));
|
||||
return;
|
||||
}
|
||||
requests.push({ method: request.method, url: request.url, userAgent: request.headers['user-agent'] });
|
||||
if (request.url === `/key/${hash('proxy failure')}`) {
|
||||
response.statusCode = 500;
|
||||
response.end();
|
||||
return;
|
||||
}
|
||||
if (request.method === 'POST') {
|
||||
const chunks = [];
|
||||
for await (const chunk of request) chunks.push(chunk);
|
||||
const value = JSON.parse(Buffer.concat(chunks).toString());
|
||||
values.set(request.url, value);
|
||||
response.end(JSON.stringify(value));
|
||||
} else {
|
||||
const value = values.get(request.url);
|
||||
const body = value ? `${JSON.stringify(value)}\n` : '';
|
||||
if (streamNextGet) {
|
||||
streamNextGet = false;
|
||||
streamResponse = response;
|
||||
response.write(body);
|
||||
} else response.end(body);
|
||||
}
|
||||
});
|
||||
server.listen(0, '127.0.0.1');
|
||||
try {
|
||||
await once(server, 'listening');
|
||||
runner.run({
|
||||
proxyServer: `http://127.0.0.1:${server.address().port}`,
|
||||
proxyUserAgent: 'opendht-node-test/1.0',
|
||||
persistPath: '',
|
||||
});
|
||||
const key = hash('proxy configuration');
|
||||
const payload = Buffer.from([0, 255, 42]);
|
||||
assert.equal(await runner.put(key, payload, { id: 42n }), true);
|
||||
const received = await runner.getAll(key);
|
||||
assert.equal(received.length, 1);
|
||||
assert.equal(received[0].id, 42n);
|
||||
assert.deepEqual(received[0].data, payload);
|
||||
|
||||
streamNextGet = true;
|
||||
const iterator = runner.get(key);
|
||||
const first = await iterator.next();
|
||||
assert.equal(first.done, false);
|
||||
assert.equal(first.value.id, 42n);
|
||||
assert.deepEqual(first.value.data, payload);
|
||||
assert.equal(streamResponse.writableEnded, false);
|
||||
streamResponse.end();
|
||||
assert.deepEqual(await iterator.next(), { value: undefined, done: true });
|
||||
assert.deepEqual(await iterator.next(), { value: undefined, done: true });
|
||||
assert.deepEqual(requests.map(request => request.method), ['POST', 'GET', 'GET']);
|
||||
assert.ok(requests.every(request => request.url === `/key/${key}`));
|
||||
assert.ok(requests.every(request => request.userAgent === 'opendht-node-test/1.0'));
|
||||
|
||||
const empty = runner.get(hash('proxy empty'));
|
||||
assert.deepEqual(await empty.next(), { value: undefined, done: true });
|
||||
const failed = runner.get(hash('proxy failure'));
|
||||
await assert.rejects(failed.next(), { code: 'ERR_DHT_GET_FAILED' });
|
||||
await assert.rejects(runner.getAll(hash('proxy failure')), { code: 'ERR_DHT_GET_FAILED' });
|
||||
|
||||
const all = [];
|
||||
for await (const value of runner.get(key)) all.push(value);
|
||||
assert.equal(all.length, 1);
|
||||
assert.deepEqual(all[0].data, payload);
|
||||
|
||||
streamNextGet = true;
|
||||
const cancelled = runner.get(key);
|
||||
for await (const value of cancelled) {
|
||||
assert.equal(value.id, 42n);
|
||||
break;
|
||||
}
|
||||
assert.equal(streamResponse.writableEnded, false);
|
||||
assert.deepEqual(await cancelled.next(), { value: undefined, done: true });
|
||||
streamResponse.end();
|
||||
} finally {
|
||||
streamResponse?.end();
|
||||
await runner.close();
|
||||
await new Promise((resolve, reject) => server.close(error => error ? reject(error) : resolve()));
|
||||
}
|
||||
});
|
||||
|
||||
test('worker termination cleans up native runners', { timeout: 10000 }, async () => {
|
||||
const source = `
|
||||
import { parentPort } from 'node:worker_threads';
|
||||
import { DhtRunner, hash } from ${JSON.stringify(moduleURL)};
|
||||
const runner = new DhtRunner();
|
||||
runner.run({ port: 0, ipv4: '127.0.0.1', ipv6: '::1' });
|
||||
runner.listen(hash('worker'), () => {});
|
||||
parentPort.postMessage('ready');
|
||||
`;
|
||||
const worker = new Worker(new URL(`data:text/javascript,${encodeURIComponent(source)}`));
|
||||
try {
|
||||
await once(worker, 'message');
|
||||
} finally {
|
||||
await worker.terminate();
|
||||
}
|
||||
});
|
||||
|
||||
test('listener callbacks may cancel themselves and close the runner', { timeout: 10000 }, async () => {
|
||||
const runner = startNode();
|
||||
const key = hash('reentrant listener');
|
||||
let deliveries = 0;
|
||||
let finish;
|
||||
const received = new Promise(resolve => { finish = resolve; });
|
||||
const subscription = runner.listen(key, () => {
|
||||
deliveries++;
|
||||
subscription.cancel();
|
||||
finish(runner.close());
|
||||
});
|
||||
const puts = [];
|
||||
for (let index = 0; index < 3000; index++) {
|
||||
puts.push(runner.put(key, Buffer.from('value'), { id: BigInt(index + 1) }).catch(error => {
|
||||
assert.equal(error.code, 'ERR_DHT_CLOSED');
|
||||
}));
|
||||
}
|
||||
try {
|
||||
await received;
|
||||
await Promise.all(puts);
|
||||
assert.equal(deliveries, 1);
|
||||
} finally {
|
||||
await runner.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('unref and garbage collection do not strand native threads', { timeout: 20000 }, async () => {
|
||||
for (const finish of ['runner.unref();', 'runner = null; setImmediate(() => global.gc());']) {
|
||||
await promisify(execFile)(process.execPath, ['--input-type=module', '--expose-gc', '-e', `
|
||||
import { DhtRunner } from ${JSON.stringify(moduleURL)};
|
||||
let runner = new DhtRunner();
|
||||
runner.run({ port: 0, ipv4: '127.0.0.1', ipv6: '::1' });
|
||||
${finish}
|
||||
`], { timeout: 8000 });
|
||||
}
|
||||
});
|
||||
|
||||
test('callback queue completes bursts beyond the former event limit', { timeout: 15000 }, async () => {
|
||||
const runner = startNode();
|
||||
const key = hash('unbounded callback queue');
|
||||
const errors = [];
|
||||
const count = 3000;
|
||||
runner.on('error', error => errors.push(error));
|
||||
const puts = [];
|
||||
try {
|
||||
for (let index = 0; index < count; index++)
|
||||
puts.push(runner.put(key, Buffer.from('value'), { id: BigInt(index + 1) }));
|
||||
const results = await Promise.all(puts);
|
||||
assert.equal(results.length, count);
|
||||
assert.ok(results.every(result => typeof result === 'boolean'));
|
||||
assert.equal(runner.isRunning(), true);
|
||||
assert.deepEqual(errors, []);
|
||||
} finally {
|
||||
await runner.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('dhtnode example prints help and validates its port', async () => {
|
||||
const { stdout } = await promisify(execFile)(process.execPath, [example, '--help']);
|
||||
assert.match(stdout, /Usage:.*dhtnode/);
|
||||
assert.match(stdout, /bootstrap/);
|
||||
await assert.rejects(promisify(execFile)(process.execPath, [example, '--port', '65536']), error => {
|
||||
assert.equal(error.code, 1);
|
||||
assert.match(error.stderr, /port/i);
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
test('dhtnode example accepts piped commands and shuts down on EOF', { timeout: 20000 }, async () => {
|
||||
const peer = startNode();
|
||||
try {
|
||||
const execution = promisify(execFile)(process.execPath, [example,
|
||||
'--port', '0', '--ipv4', '127.0.0.1',
|
||||
'--bootstrap', '127.0.0.1', '--bootstrap-port', String(peer.getBoundPort()),
|
||||
], { timeout: 15000 });
|
||||
let output = '';
|
||||
let sentReads = false;
|
||||
execution.child.stdout.on('data', data => {
|
||||
output += data;
|
||||
if (!sentReads && /Stored example/.test(output) && /Stored signed/.test(output)) {
|
||||
sentReads = true;
|
||||
execution.child.stdin.end('get example\nget signed\ncancel example\n');
|
||||
}
|
||||
});
|
||||
execution.child.stdin.write('listen example\nput example hello world\nsign signed signed message\n');
|
||||
const { stdout, stderr } = await execution;
|
||||
assert.equal(stderr, '');
|
||||
assert.match(stdout, /Node [0-9a-f]{40}/);
|
||||
assert.match(stdout, /hello world/);
|
||||
assert.match(stdout, /signed message.*\[signed\]/);
|
||||
assert.match(stdout, /Cancelled example/);
|
||||
assert.match(stdout, /Connectivity: IPv4 connected, IPv6 (disconnected|connecting|connected)/);
|
||||
} finally {
|
||||
await peer.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('dhtnode example handles commands and quit while a get is pending', { timeout: 10000 }, async () => {
|
||||
const execution = promisify(execFile)(process.execPath, ['--input-type=module', '-e', `
|
||||
import { DhtRunner } from ${JSON.stringify(moduleURL)};
|
||||
const close = DhtRunner.prototype.close;
|
||||
let rejectGet;
|
||||
DhtRunner.prototype.getAll = function () {
|
||||
console.log('Get pending');
|
||||
return new Promise((resolve, reject) => { rejectGet = reject; });
|
||||
};
|
||||
DhtRunner.prototype.close = function () {
|
||||
rejectGet?.(new Error('DHT runner is closed'));
|
||||
return close.call(this);
|
||||
};
|
||||
await import(${JSON.stringify(exampleURL.href)});
|
||||
`, '--',
|
||||
'--port', '0', '--ipv4', '127.0.0.1', '--ipv6', '::1',
|
||||
], { timeout: 8000 });
|
||||
let output = '';
|
||||
execution.child.stdout.on('data', data => {
|
||||
output += data;
|
||||
if (/get KEY \| put KEY TEXT/.test(output)) execution.child.stdin.end('quit\n');
|
||||
});
|
||||
execution.child.stdin.write('get waiting\nhelp\n');
|
||||
const { stdout, stderr } = await execution;
|
||||
assert.match(stdout, /Get pending/);
|
||||
assert.match(stdout, /get KEY \| put KEY TEXT/);
|
||||
assert.equal(stderr, '');
|
||||
});
|
||||
|
||||
test('dhtnode example shuts down on SIGTERM', { timeout: 10000 }, async () => {
|
||||
if (process.platform === 'win32') return;
|
||||
const execution = promisify(execFile)(process.execPath, [example, '--port', '0', '--ipv4', '127.0.0.1'], { timeout: 8000 });
|
||||
let output = '';
|
||||
execution.child.stdout.on('data', data => {
|
||||
output += data;
|
||||
if (/Node [0-9a-f]{40}/.test(output)) execution.child.kill('SIGTERM');
|
||||
});
|
||||
const { stderr } = await execution;
|
||||
assert.equal(stderr, '');
|
||||
});
|
||||
@@ -0,0 +1,93 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { test } from 'node:test';
|
||||
import { Certificate, DhtRunner, Identity, PrivateKey, PublicKey, hash } from '../index.js';
|
||||
|
||||
const payload = Buffer.from([0, 255, 1, 0, 42]);
|
||||
|
||||
test('RSA keys sign, encrypt, and survive password-protected serialization', async () => {
|
||||
const key = await PrivateKey.generate({ algorithm: 'rsa', bits: 2048 });
|
||||
const publicKey = key.getPublicKey();
|
||||
assert.match(publicKey.getId(), /^[0-9a-f]{40}$/);
|
||||
assert.match(publicKey.getLongId(), /^[0-9a-f]{64}$/);
|
||||
const signature = key.sign(payload);
|
||||
assert.equal(publicKey.checkSignature(payload, signature), true);
|
||||
assert.equal(publicKey.checkSignature(Buffer.from('other'), signature), false);
|
||||
assert.deepEqual(key.decrypt(publicKey.encrypt(payload)), payload);
|
||||
const restored = new PrivateKey(key.serialize('test password'), 'test password');
|
||||
const restoredPublic = new PublicKey(publicKey.serialize());
|
||||
assert.equal(restoredPublic.getLongId(), publicKey.getLongId());
|
||||
assert.equal(restoredPublic.checkSignature(payload, restored.sign(payload)), true);
|
||||
assert.deepEqual(restored.decrypt(restoredPublic.encrypt(payload)), payload);
|
||||
assert.throws(() => new PrivateKey(key.serialize('password'), 'wrong password'));
|
||||
assert.throws(() => key.decrypt(Buffer.from('invalid ciphertext')));
|
||||
assert.throws(() => key.serialize('bad\0password'), TypeError);
|
||||
});
|
||||
|
||||
test('EC keys sign and invalid crypto inputs raise JS errors', async () => {
|
||||
const key = await PrivateKey.generate();
|
||||
const restored = new PrivateKey(key.serialize());
|
||||
assert.equal(restored.getPublicKey().checkSignature(payload, key.sign(payload)), true);
|
||||
const largePayload = Buffer.alloc(16 * 1024 * 1024 + 1, 42);
|
||||
assert.equal(restored.getPublicKey().checkSignature(largePayload, key.sign(largePayload)), true);
|
||||
assert.throws(() => key.getPublicKey().encrypt(payload));
|
||||
for (const Type of [PrivateKey, PublicKey, Certificate]) {
|
||||
assert.throws(() => new Type(Buffer.alloc(0)));
|
||||
assert.throws(() => new Type(Buffer.from('not a key or certificate')));
|
||||
assert.throws(() => new Type(null), TypeError);
|
||||
}
|
||||
assert.throws(() => PrivateKey.generate({ algorithm: 'unknown' }), TypeError);
|
||||
assert.throws(() => PrivateKey.generate({ algorithm: 'rsa', bits: 1 }), RangeError);
|
||||
assert.throws(() => PrivateKey.generate({ algorithm: 'rsa', bits: 2048.5 }), RangeError);
|
||||
assert.throws(() => key.sign('not bytes'), TypeError);
|
||||
assert.throws(() => PrivateKey.prototype.sign.call({}, payload), TypeError);
|
||||
});
|
||||
|
||||
test('identities own matching keys and certificates and support import', async () => {
|
||||
const identity = await Identity.generate('nodejs crypto');
|
||||
const certificate = new Certificate(identity.certificate.serialize());
|
||||
const key = new PrivateKey(identity.privateKey.serialize());
|
||||
const restored = new Identity(key, certificate);
|
||||
assert.equal(restored.certificate.getLongId(), key.getPublicKey().getLongId());
|
||||
assert.equal(certificate.getPublicKey().checkSignature(payload, key.sign(payload)), true);
|
||||
assert.throws(() => new Identity({}, certificate), TypeError);
|
||||
const other = await PrivateKey.generate();
|
||||
assert.throws(() => new Identity(other, certificate), /match/i);
|
||||
});
|
||||
|
||||
test('signed and encrypted values round-trip between authenticated nodes', { timeout: 30000 }, async () => {
|
||||
const writerIdentity = await Identity.generate('writer');
|
||||
const readerIdentity = await Identity.generate('reader', { algorithm: 'rsa', bits: 2048 });
|
||||
const writer = new DhtRunner();
|
||||
const reader = new DhtRunner();
|
||||
try {
|
||||
writer.run({ port: 0, ipv4: '127.0.0.1', ipv6: '::1', identity: writerIdentity });
|
||||
reader.run({ port: 0, ipv4: '127.0.0.1', ipv6: '::1', identity: readerIdentity });
|
||||
writer.bootstrap('127.0.0.1', reader.getBoundPort());
|
||||
reader.bootstrap('127.0.0.1', writer.getBoundPort());
|
||||
const signedKey = hash('nodejs signed value');
|
||||
assert.equal(await writer.putSigned(signedKey, payload, { id: 41n }), true);
|
||||
const signed = await reader.getAll(signedKey);
|
||||
assert.ok(signed.some(value => value.data.equals(payload) && value.isSigned));
|
||||
assert.equal(signed[0].owner, writerIdentity.certificate.getLongId());
|
||||
const encryptedKey = hash('nodejs encrypted value');
|
||||
assert.equal(await writer.putEncrypted(encryptedKey, payload, readerIdentity.certificate.getPublicKey(), { id: 42n }), true);
|
||||
const decrypted = await reader.getAll(encryptedKey);
|
||||
assert.ok(decrypted.some(value => value.data.equals(payload)));
|
||||
assert.throws(() => writer.putEncrypted(encryptedKey, payload, {}), TypeError);
|
||||
assert.throws(() => writer.putEncrypted(encryptedKey, payload, writerIdentity.certificate.getPublicKey()), /RSA/i);
|
||||
} finally {
|
||||
await Promise.all([writer.close(), reader.close()]);
|
||||
}
|
||||
});
|
||||
|
||||
test('signed operations require an identity and reject stopped runners', async () => {
|
||||
const runner = new DhtRunner();
|
||||
try {
|
||||
assert.throws(() => runner.run({ identity: {} }), TypeError);
|
||||
runner.run({ port: 0, ipv4: '127.0.0.1', ipv6: '::1' });
|
||||
assert.throws(() => runner.putSigned(hash('unsigned'), payload), /identity/i);
|
||||
} finally {
|
||||
await runner.close();
|
||||
}
|
||||
assert.throws(() => runner.putSigned(hash('closed'), payload), /closed/i);
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { test } from 'node:test';
|
||||
|
||||
const root = path.resolve(import.meta.dirname, '..');
|
||||
|
||||
test('package exposes ESM-only named exports', () => {
|
||||
const manifest = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
||||
assert.equal(manifest.type, 'module');
|
||||
assert.equal(manifest.exports['.'].import, './index.js');
|
||||
assert.equal(manifest.exports['.'].require, undefined);
|
||||
const stdout = execFileSync(process.execPath, ['--input-type=module', '-e', `
|
||||
import { DhtRunner, hash } from ${JSON.stringify(manifest.name)};
|
||||
console.log(hash('hello'));
|
||||
const runner = new DhtRunner();
|
||||
await runner.close();
|
||||
`], { cwd: root, encoding: 'utf8' });
|
||||
assert.equal(stdout.trim(), 'aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d');
|
||||
});
|
||||
|
||||
test('npm archive contains runtime files and excludes development artifacts', () => {
|
||||
const npm = process.env.npm_execpath;
|
||||
assert.ok(npm, 'Run the package contract with npm test');
|
||||
const [archive] = JSON.parse(execFileSync(process.execPath,
|
||||
[npm, 'pack', '--dry-run', '--json', '--ignore-scripts'],
|
||||
{ cwd: root, encoding: 'utf8' }));
|
||||
const files = archive.files.map(file => file.path);
|
||||
for (const file of ['index.js', 'index.d.ts', 'crypto.js', 'README.md', 'examples/dhtnode.js', 'examples/dhtnode.ts']) assert.ok(files.includes(file), file);
|
||||
assert.equal(files.some(file => /^(build[^/]*|node_modules|test|scripts)\//.test(file)), false);
|
||||
assert.equal(files.some(file => /\.(cpp|h|tsbuildinfo)$/.test(file)), false);
|
||||
assert.ok(files.every(file => /^(index\.js|index\.d\.ts|crypto\.js|README\.md|package\.json|examples\/dhtnode\.(js|ts)|prebuilds\/)/.test(file)));
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
import { Certificate, DhtRunner, Identity, PrivateKey, PublicKey, hash, version } from '../index.js';
|
||||
import type { ConnectivityStatus, ListenEvent, ListenerIterator, NodeStatus, Value, ValueIterator } from '../index.js';
|
||||
|
||||
async function exercise(): Promise<void> {
|
||||
const privateKey: PrivateKey = await PrivateKey.generate({ algorithm: 'rsa', bits: 2048 });
|
||||
const publicKey: PublicKey = new PublicKey(privateKey.getPublicKey().serialize());
|
||||
const identity: Identity = await Identity.generate('typed identity');
|
||||
const restored = new Identity(identity.privateKey, new Certificate(identity.certificate.serialize()));
|
||||
const valid: boolean = publicKey.checkSignature(Buffer.alloc(0), privateKey.sign(Buffer.alloc(0)));
|
||||
const plaintext: Buffer = privateKey.decrypt(publicKey.encrypt(Buffer.from('secret')));
|
||||
const runner = new DhtRunner();
|
||||
runner.on('error', (error: Error) => { throw error; });
|
||||
runner.on('statusChanged', status => {
|
||||
const ipv4: NodeStatus = status.ipv4;
|
||||
const ipv6: NodeStatus = status.ipv6;
|
||||
void [ipv4, ipv6];
|
||||
});
|
||||
const status: ConnectivityStatus = runner.getStatus();
|
||||
runner.run({ port: 0, network: 1, persistPath: '', identity: restored });
|
||||
runner.bootstrap('127.0.0.1', runner.getBoundPort(4));
|
||||
const key: string = hash(new Uint8Array([0, 255]));
|
||||
const accepted: boolean = await runner.put(key, Buffer.from(version), { id: 42n, permanent: true });
|
||||
const signed: boolean = await runner.putSigned(key, plaintext);
|
||||
const encrypted: boolean = await runner.putEncrypted(key, plaintext, publicKey);
|
||||
const values: Value[] = await runner.getAll(key);
|
||||
const get: ValueIterator<Value> = runner.get(key);
|
||||
for await (const value of get) {
|
||||
const payload: Buffer = value.data;
|
||||
void payload;
|
||||
}
|
||||
const listener = runner.listen(key, (value, expired) => {
|
||||
const payload: Buffer = value.data;
|
||||
const id: bigint = value.id;
|
||||
void [payload, id, expired];
|
||||
});
|
||||
runner.ref().unref();
|
||||
listener.cancel();
|
||||
const iterator: ListenerIterator = runner.listen(key);
|
||||
for await (const event of iterator) {
|
||||
const typed: ListenEvent = event;
|
||||
const value: Value = typed.value;
|
||||
const expired: boolean = typed.expired;
|
||||
void [value, expired];
|
||||
break;
|
||||
}
|
||||
iterator.cancel();
|
||||
await iterator.return();
|
||||
await iterator[Symbol.asyncDispose]();
|
||||
await runner[Symbol.asyncDispose]();
|
||||
const proxy = new DhtRunner();
|
||||
proxy.run({ proxyServer: 'https://proxy.example.com', proxyUserAgent: 'typed-client/1.0' });
|
||||
await proxy.close();
|
||||
void [accepted, values, valid, signed, encrypted, status];
|
||||
}
|
||||
|
||||
void exercise;
|
||||
@@ -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')
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
This software is licensed under the MIT License.
|
||||
|
||||
Copyright Fedor Indutny, 2018.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to permit
|
||||
persons to whom the Software is furnished to do so, subject to the
|
||||
following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included
|
||||
in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
|
||||
NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
||||
USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -0,0 +1,70 @@
|
||||
# Building and Testing the Rust Crate
|
||||
|
||||
The `opendht` crate links to the native OpenDHT C library. Cargo does not build
|
||||
or bundle that library. Install Rust and the native dependencies described in
|
||||
the [OpenDHT build guide](../BUILD.md).
|
||||
|
||||
## Install the Native Library
|
||||
|
||||
From the root of an OpenDHT source checkout, enable the C bindings:
|
||||
|
||||
```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
|
||||
cmake --install build-bindings
|
||||
```
|
||||
|
||||
Installation may require administrator privileges, depending on the install
|
||||
prefix. Set `CMAKE_INSTALL_PREFIX` when configuring to choose another location.
|
||||
The linker and runtime loader must be able to find the installed libraries.
|
||||
Applications can then depend on `opendht` from crates.io as shown in the
|
||||
[README](README.md).
|
||||
|
||||
## Use an Uninstalled Native Build
|
||||
|
||||
You can use the native build directly by setting its library search paths.
|
||||
For example, from the repository root on macOS:
|
||||
|
||||
```sh
|
||||
export LIBRARY_PATH="$PWD/build-bindings${LIBRARY_PATH:+:$LIBRARY_PATH}"
|
||||
export DYLD_LIBRARY_PATH="$PWD/build-bindings${DYLD_LIBRARY_PATH:+:$DYLD_LIBRARY_PATH}"
|
||||
export 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`. For an application outside this checkout,
|
||||
use absolute paths to the native build and run Cargo in the application directory.
|
||||
|
||||
When linking an AddressSanitizer native build, use the same compiler runtime.
|
||||
For example, with Clang:
|
||||
|
||||
```sh
|
||||
export RUSTFLAGS="-L native=$PWD/build-bindings -C linker=clang -C link-arg=-fsanitize=address"
|
||||
```
|
||||
|
||||
## Build and Test the Crate
|
||||
|
||||
With the native library available, run from the `rust` directory:
|
||||
|
||||
```sh
|
||||
cargo build --all-targets
|
||||
cargo test --all-targets
|
||||
cargo test --doc
|
||||
cargo test --all-features --all-targets
|
||||
cargo test --all-features --doc
|
||||
```
|
||||
|
||||
## Run the Examples
|
||||
|
||||
From the `rust` directory:
|
||||
|
||||
```sh
|
||||
cargo run --example dhtnode
|
||||
cargo run --features async --example dhtnode_async
|
||||
cargo run --features serde --example dhtnode_async
|
||||
```
|
||||
|
||||
The callback example contacts a public bootstrap server. The async example uses
|
||||
two loopback nodes; enabling `serde` also demonstrates typed MessagePack records.
|
||||
+23
-4
@@ -1,11 +1,30 @@
|
||||
[package]
|
||||
name = "opendht"
|
||||
version = "0.1.0"
|
||||
authors = ["Sébastien Blin <sebastien.blin@savoirfairelinux.com>"]
|
||||
edition = "2021"
|
||||
version = "0.6.0"
|
||||
authors = ["Sébastien Blin <sebastien.blin@savoirfairelinux.com>", "Adrien Beraud <adrien.beraud@savoirfairelinux.com>"]
|
||||
edition = "2024"
|
||||
description = "Rust bindings for OpenDHT"
|
||||
license = "MIT"
|
||||
license-file = "../LICENSE"
|
||||
repository = "https://github.com/savoirfairelinux/opendht"
|
||||
|
||||
[features]
|
||||
async = ["dep:futures-channel", "dep:futures-core"]
|
||||
serde = ["async", "dep:serde", "dep:rmp-serde"]
|
||||
|
||||
[[example]]
|
||||
name = "dhtnode_async"
|
||||
required-features = ["async"]
|
||||
|
||||
[dependencies]
|
||||
libc="0.2"
|
||||
os_socketaddr="0.2.3"
|
||||
futures-channel = { version = "0.3", optional = true }
|
||||
futures-core = { version = "0.3", optional = true }
|
||||
serde = { version = "1", optional = true }
|
||||
rmp-serde = { version = "1", optional = true }
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
[dev-dependencies]
|
||||
futures = "0.3"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_bytes = "0.11"
|
||||
+142
-11
@@ -1,21 +1,152 @@
|
||||
# Rust bindings for opendht
|
||||
# OpenDHT: a Distributed Hash Table
|
||||
|
||||
## Build
|
||||
OpenDHT is a Kademlia-based distributed hash table for storing and retrieving
|
||||
small values over a peer-to-peer network without a central server. Common uses
|
||||
include peer discovery, service announcements, and decentralized applications.
|
||||
|
||||
Install rust on your system (https://rustup.rs/ is the recommanded way). Once done:
|
||||
The `opendht` crate provides Rust bindings to [OpenDHT](https://opendht.net), with
|
||||
callback-based and runtime-independent async APIs. Capabilities include signed
|
||||
and encrypted values, filtered reads, metadata queries, and typed MessagePack
|
||||
records through Serde.
|
||||
|
||||
```
|
||||
cargo build
|
||||
## Get Started
|
||||
|
||||
Add the crate to your dependencies:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
opendht = "0.6"
|
||||
```
|
||||
|
||||
## Run tests
|
||||
The native OpenDHT library with C bindings must be installed separately; it is
|
||||
not bundled with the crate. See the
|
||||
[build guide](https://github.com/savoirfairelinux/opendht/blob/master/rust/BUILD.md)
|
||||
for native installation, library paths, and source builds.
|
||||
|
||||
```
|
||||
cargo test
|
||||
`DhtRunner` manages a node, `InfoHash` identifies a DHT key, and `Value` contains
|
||||
the payload and metadata stored under that key. `put` publishes values, `get`
|
||||
retrieves them, and `listen` subscribes to value updates and expiration events.
|
||||
|
||||
This example starts a node on an OS-assigned port, bootstraps into the public
|
||||
network, and listens for a value it publishes.
|
||||
|
||||
```rust
|
||||
use opendht::{DhtRunner, InfoHash, Value};
|
||||
use std::{sync::mpsc, time::Duration};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut runner = DhtRunner::new();
|
||||
runner.run(0)?;
|
||||
runner.bootstrap("bootstrap.jami.net", 4222)?;
|
||||
|
||||
let key = InfoHash::get("my-app/hello");
|
||||
let (sender, receiver) = mpsc::channel();
|
||||
let listener = runner.listen(&key, move |value, expired| {
|
||||
if !expired {
|
||||
let _ = sender.send(value);
|
||||
}
|
||||
true
|
||||
})?;
|
||||
|
||||
runner.put(&key, Value::new("Hello from Rust!"), |_| {}, false);
|
||||
let value = receiver.recv_timeout(Duration::from_secs(5))?;
|
||||
println!("{}", String::from_utf8_lossy(value.as_slice()));
|
||||
|
||||
runner.cancel_listen(&key, listener);
|
||||
runner.join()?;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## Run dht node
|
||||
A bootstrap server supplies initial peers. Applications can use public
|
||||
bootstrap servers or known peers on a private network. Values expire, and their
|
||||
availability depends on participating nodes; the DHT does not provide durable
|
||||
storage guarantees.
|
||||
|
||||
## Optional Features
|
||||
|
||||
| Feature | Capabilities |
|
||||
|---------|--------------|
|
||||
| Default | Callback-based `DhtRunner`, binary values, keys, certificates, and signed/encrypted operations. |
|
||||
| `async` | `aio::AsyncDhtRunner` with publication futures, bounded read/update streams, and automatic listener cancellation on drop. |
|
||||
| `serde` | Serde-backed MessagePack records with `put_typed`, `get_typed`, and `listen_typed`. Enables `async`. |
|
||||
|
||||
Enable `async` in the dependency configuration:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
opendht = { version = "0.6", features = ["async"] }
|
||||
futures = "0.3"
|
||||
```
|
||||
cargo run --example dhtnode
|
||||
```
|
||||
|
||||
Create the adapter with `AsyncDhtRunner::new(runner)` after starting a
|
||||
`DhtRunner`. The adapter is runtime-independent and thread-bound; use a local
|
||||
executor or Tokio's `LocalSet`. Dropping a listener stream cancels its
|
||||
subscription. Streams use bounded buffers and report `WouldBlock` on overflow.
|
||||
|
||||
This example waits for a bootstrap peer, publishes a value with `.await`, and
|
||||
reads matching values as a stream:
|
||||
|
||||
```rust,no_run
|
||||
use futures::{executor::block_on, TryStreamExt};
|
||||
use opendht::{aio::AsyncDhtRunner, DhtRunner, InfoHash, Value};
|
||||
use std::{io, net::ToSocketAddrs};
|
||||
|
||||
fn main() -> io::Result<()> {
|
||||
let mut runner = DhtRunner::new();
|
||||
runner.run(0)?;
|
||||
let runner = AsyncDhtRunner::new(runner);
|
||||
let peers: Vec<_> = ("bootstrap.jami.net", 4222).to_socket_addrs()?.collect();
|
||||
|
||||
block_on(async {
|
||||
runner.bootstrap(&peers).await?;
|
||||
let key = InfoHash::get("my-app/hello-async");
|
||||
runner.put(&key, Value::new("Hello from async Rust!"), false).await?;
|
||||
|
||||
let mut values = runner.get(&key);
|
||||
while let Some(value) = values.try_next().await? {
|
||||
println!("{}", String::from_utf8_lossy(value.as_slice()));
|
||||
}
|
||||
|
||||
runner.shutdown().await?;
|
||||
runner.join()
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### Typed Records
|
||||
|
||||
The `serde` feature adds typed async operations and also enables `async`.
|
||||
`put_typed` serializes records implementing `Serialize`; `get_typed` and
|
||||
`listen_typed` decode records implementing `DeserializeOwned`. Typed reads retain
|
||||
the decoded record alongside its original payload and native metadata.
|
||||
|
||||
Structs are serialized as compact MessagePack arrays. Field order is part of
|
||||
the wire format and must remain compatible across schema changes. `serde_bytes`
|
||||
encodes binary fields using MessagePack's binary representation.
|
||||
|
||||
## Working With Values
|
||||
|
||||
GET and LISTEN callbacks and async streams deliver owned `Box<Value>` instances.
|
||||
Received values implement `Send` but not `Sync`; they can be retained after the
|
||||
runner stops, moved to another thread, modified, and republished. `as_slice()`
|
||||
borrows the payload without copying; `as_bytes()` allocates a separate `Vec<u8>`.
|
||||
|
||||
Values expose metadata including IDs, types, sequence numbers, and owner keys.
|
||||
Filters select values by metadata, and queries return selected metadata fields.
|
||||
|
||||
`DhtRunner::listen` returns a token that must be passed to `cancel_listen` to
|
||||
unsubscribe. Dropping the token does not cancel the registration; dropping an
|
||||
async listener stream does.
|
||||
|
||||
`join()` stops the runner and waits for its threads to exit. It must not be
|
||||
called from a callback. For graceful shutdown and configured state persistence,
|
||||
wait for `shutdown` to complete before calling `join()`.
|
||||
|
||||
## Learn More
|
||||
|
||||
- [API reference](https://docs.rs/opendht)
|
||||
- [Callback example](https://github.com/savoirfairelinux/opendht/blob/master/rust/examples/dhtnode.rs)
|
||||
- [Async and typed two-node example](https://github.com/savoirfairelinux/opendht/blob/master/rust/examples/dhtnode_async.rs)
|
||||
- [Build and test guide](https://github.com/savoirfairelinux/opendht/blob/master/rust/BUILD.md)
|
||||
- [OpenDHT project and issue tracker](https://github.com/savoirfairelinux/opendht)
|
||||
+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(())
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
use futures::{executor::block_on, StreamExt, TryStreamExt};
|
||||
use opendht::{aio::AsyncDhtRunner, DhtRunner, DhtRunnerConfig, InfoHash, Value};
|
||||
use std::error::Error;
|
||||
use std::io;
|
||||
use std::net::SocketAddr;
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
let mut source = DhtRunner::new();
|
||||
let mut destination = DhtRunner::new();
|
||||
source.run_bind("127.0.0.1", "::1", 0, DhtRunnerConfig::new())?;
|
||||
destination.run_bind("127.0.0.1", "::1", 0, DhtRunnerConfig::new())?;
|
||||
let address = SocketAddr::from(([127, 0, 0, 1], destination.bound_port(libc::AF_INET as _)));
|
||||
let source = AsyncDhtRunner::new(source);
|
||||
let destination = AsyncDhtRunner::new(destination);
|
||||
|
||||
block_on(async {
|
||||
source.bootstrap(&[address]).await?;
|
||||
let hash = InfoHash::get("async example");
|
||||
let mut updates = destination.listen(&hash)?;
|
||||
source
|
||||
.put(&hash, Value::with_id(b"hello from Rust async", 42), false)
|
||||
.await?;
|
||||
let event = updates
|
||||
.next()
|
||||
.await
|
||||
.ok_or_else(|| io::Error::other("listener ended before receiving a value"))??;
|
||||
println!(
|
||||
"LISTEN {}: {} (expired: {})",
|
||||
event.value.id(),
|
||||
String::from_utf8_lossy(event.value.as_slice()),
|
||||
event.expired,
|
||||
);
|
||||
drop(updates);
|
||||
|
||||
let values: Vec<_> = destination.get(&hash).try_collect().await?;
|
||||
println!("GET: {} values", values.len());
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
{
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
struct Service {
|
||||
version: u32,
|
||||
name: String,
|
||||
port: u16,
|
||||
#[serde(with = "serde_bytes")]
|
||||
payload: Vec<u8>,
|
||||
}
|
||||
|
||||
let hash = InfoHash::get("typed async example");
|
||||
let mut updates = destination.listen_typed::<Service>(&hash)?;
|
||||
let record = Service {
|
||||
version: 1,
|
||||
name: "chat".into(),
|
||||
port: 4222,
|
||||
payload: vec![0, 0x80, 0xff],
|
||||
};
|
||||
source.put_typed(&hash, &record, false).await?;
|
||||
let event = updates.next().await.ok_or_else(|| {
|
||||
io::Error::other("typed listener ended before receiving a value")
|
||||
})??;
|
||||
println!(
|
||||
"TYPED LISTEN {}: {:?}",
|
||||
event.value.raw.id(),
|
||||
event.value.data
|
||||
);
|
||||
drop(updates);
|
||||
let values: Vec<_> = destination
|
||||
.get_typed::<Service>(&hash)
|
||||
.try_collect()
|
||||
.await?;
|
||||
println!("TYPED GET: {} records", values.len());
|
||||
}
|
||||
|
||||
source.shutdown().await?;
|
||||
destination.shutdown().await?;
|
||||
source.join()?;
|
||||
destination.join()?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
+429
@@ -0,0 +1,429 @@
|
||||
//! Runtime-independent futures and bounded streams, enabled by the `async` feature.
|
||||
//!
|
||||
//! Runners and listeners stay on their creating thread. Use a local executor or
|
||||
//! Tokio `LocalSet`; received values own their native storage and can be
|
||||
//! transferred between threads without another payload copy.
|
||||
|
||||
use crate::{DhtRunner, InfoHash, OpToken, PkId, QueryResult, Value};
|
||||
use futures_channel::{mpsc, oneshot};
|
||||
use futures_core::Stream;
|
||||
use std::cell::RefCell;
|
||||
use std::future::Future;
|
||||
use std::io;
|
||||
use std::net::SocketAddr;
|
||||
use std::pin::Pin;
|
||||
use std::rc::Rc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::task::{Context, Poll};
|
||||
|
||||
/// A listener update; expiration events retain the expired value's metadata.
|
||||
pub struct ValueEvent {
|
||||
pub value: Box<Value>,
|
||||
pub expired: bool,
|
||||
}
|
||||
|
||||
/// A native completion mapped to an I/O result.
|
||||
///
|
||||
/// Operations start immediately, before this future is polled. Dropping it
|
||||
/// abandons the result but does not undo a put, ping, bootstrap, or shutdown.
|
||||
#[must_use = "operations run immediately; await their result or explicitly drop it"]
|
||||
pub struct Operation {
|
||||
receiver: oneshot::Receiver<io::Result<()>>,
|
||||
}
|
||||
|
||||
impl Future for Operation {
|
||||
type Output = io::Result<()>;
|
||||
|
||||
fn poll(mut self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
Pin::new(&mut self.receiver).poll(context).map(|outcome| {
|
||||
outcome.unwrap_or_else(|_| Err(stopped("DHT operation ended without completion")))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn operation() -> (oneshot::Sender<io::Result<()>>, Operation) {
|
||||
let (sender, receiver) = oneshot::channel();
|
||||
(sender, Operation { receiver })
|
||||
}
|
||||
|
||||
fn outcome(success: bool) -> io::Result<()> {
|
||||
if success {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(io::Error::other(
|
||||
"DHT operation failed or the runner stopped",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn stopped(message: &'static str) -> io::Error {
|
||||
io::Error::new(io::ErrorKind::Interrupted, message)
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct StreamCompletion(Arc<Mutex<Option<oneshot::Sender<io::Result<()>>>>>);
|
||||
|
||||
impl StreamCompletion {
|
||||
fn finish(&self, result: io::Result<()>) {
|
||||
let sender = self
|
||||
.0
|
||||
.lock()
|
||||
.unwrap_or_else(|error| error.into_inner())
|
||||
.take();
|
||||
if let Some(sender) = sender {
|
||||
let _ = sender.send(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct StreamSender<T> {
|
||||
values: mpsc::Sender<T>,
|
||||
completion: StreamCompletion,
|
||||
}
|
||||
|
||||
impl<T> StreamSender<T> {
|
||||
fn send(&mut self, value: io::Result<T>) -> bool {
|
||||
let value = match value {
|
||||
Ok(value) => value,
|
||||
Err(error) => {
|
||||
self.completion.finish(Err(error));
|
||||
return false;
|
||||
}
|
||||
};
|
||||
match self.values.try_send(value) {
|
||||
Ok(()) => true,
|
||||
Err(error) => {
|
||||
if error.is_full() {
|
||||
self.completion.finish(Err(io::Error::new(
|
||||
io::ErrorKind::WouldBlock,
|
||||
"DHT stream capacity exceeded; restart the operation to resynchronize",
|
||||
)));
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A bounded get or query stream.
|
||||
///
|
||||
/// Buffered items precede a terminal error. Native failure, runner shutdown,
|
||||
/// and buffer overflow are never reported as successful EOF.
|
||||
/// Dropping the stream stops delivery; native work stops at its next callback
|
||||
/// or when the runner is joined.
|
||||
#[must_use = "streams must be polled to receive values and completion errors"]
|
||||
pub struct OperationStream<T> {
|
||||
values: mpsc::Receiver<T>,
|
||||
completion: oneshot::Receiver<io::Result<()>>,
|
||||
completed: bool,
|
||||
outcome: Option<io::Result<()>>,
|
||||
}
|
||||
|
||||
impl<T: Unpin> Stream for OperationStream<T> {
|
||||
type Item = io::Result<T>;
|
||||
|
||||
fn poll_next(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
let stream = self.get_mut();
|
||||
if !stream.completed
|
||||
&& let Poll::Ready(result) = Pin::new(&mut stream.completion).poll(context)
|
||||
{
|
||||
stream.completed = true;
|
||||
stream.outcome = Some(
|
||||
result.unwrap_or_else(|_| Err(stopped("DHT stream ended without completion"))),
|
||||
);
|
||||
stream.values.close();
|
||||
}
|
||||
match Pin::new(&mut stream.values).poll_next(context) {
|
||||
Poll::Ready(Some(value)) => Poll::Ready(Some(Ok(value))),
|
||||
Poll::Ready(None) if stream.completed => {
|
||||
Poll::Ready(stream.outcome.take().and_then(Result::err).map(Err))
|
||||
}
|
||||
_ => Poll::Pending,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn stream_channel<T>(capacity: usize) -> (StreamSender<T>, OperationStream<T>) {
|
||||
let (values, receiver) = mpsc::channel(capacity - 1);
|
||||
let (completed, completion) = oneshot::channel();
|
||||
(
|
||||
StreamSender {
|
||||
values,
|
||||
completion: StreamCompletion(Arc::new(Mutex::new(Some(completed)))),
|
||||
},
|
||||
OperationStream {
|
||||
values: receiver,
|
||||
completion,
|
||||
completed: false,
|
||||
outcome: None,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
struct RunnerState {
|
||||
runner: RefCell<Box<DhtRunner>>,
|
||||
stopping: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl Drop for RunnerState {
|
||||
fn drop(&mut self) {
|
||||
self.stopping.store(true, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
/// A listener registration that cancels itself when dropped or terminated.
|
||||
///
|
||||
/// The runner is retained until this registration is dropped. Calling `join()`
|
||||
/// stops all registrations and makes pending listeners report an error.
|
||||
#[must_use = "dropping a listener cancels its native registration"]
|
||||
pub struct Listener {
|
||||
state: Rc<RunnerState>,
|
||||
hash: InfoHash,
|
||||
token: Option<Box<OpToken>>,
|
||||
stream: OperationStream<ValueEvent>,
|
||||
}
|
||||
|
||||
impl Listener {
|
||||
fn cancel(&mut self) {
|
||||
if let Some(token) = self.token.take() {
|
||||
self.state
|
||||
.runner
|
||||
.borrow_mut()
|
||||
.cancel_listen(&self.hash, token);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Stream for Listener {
|
||||
type Item = io::Result<ValueEvent>;
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
let result = Pin::new(&mut self.stream).poll_next(context);
|
||||
if matches!(result, Poll::Ready(None) | Poll::Ready(Some(Err(_)))) {
|
||||
self.cancel();
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Listener {
|
||||
fn drop(&mut self) {
|
||||
self.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
/// A shared, thread-bound async adapter for an already configured runner.
|
||||
///
|
||||
/// Clones share a runner without making native handles `Send` or `Sync`.
|
||||
/// Native calls are short; futures and streams never hold a mutable runner
|
||||
/// borrow while waiting. The default stream capacity is 64 items.
|
||||
///
|
||||
/// ```compile_fail
|
||||
/// use opendht::{aio::AsyncDhtRunner, DhtRunner};
|
||||
/// let runner = AsyncDhtRunner::new(DhtRunner::new());
|
||||
/// std::thread::spawn(move || runner.join().unwrap());
|
||||
/// ```
|
||||
#[derive(Clone)]
|
||||
pub struct AsyncDhtRunner {
|
||||
state: Rc<RunnerState>,
|
||||
capacity: usize,
|
||||
}
|
||||
|
||||
impl AsyncDhtRunner {
|
||||
pub fn new(runner: Box<DhtRunner>) -> Self {
|
||||
Self::with_capacity(runner, 64).expect("default stream capacity is valid")
|
||||
}
|
||||
|
||||
/// Set the maximum number of queued values per get, query, or listener.
|
||||
/// A slow consumer receives `WouldBlock` and the stream terminates.
|
||||
pub fn with_capacity(runner: Box<DhtRunner>, capacity: usize) -> io::Result<Self> {
|
||||
if capacity == 0 {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"stream capacity must be positive",
|
||||
));
|
||||
}
|
||||
Ok(Self {
|
||||
state: Rc::new(RunnerState {
|
||||
runner: RefCell::new(runner),
|
||||
stopping: Arc::new(AtomicBool::new(false)),
|
||||
}),
|
||||
capacity,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn put(&self, hash: &InfoHash, value: Box<Value>, permanent: bool) -> Operation {
|
||||
let (sender, pending) = operation();
|
||||
self.state.runner.borrow_mut().put(
|
||||
hash,
|
||||
value,
|
||||
move |success| {
|
||||
let _ = sender.send(outcome(success));
|
||||
},
|
||||
permanent,
|
||||
);
|
||||
pending
|
||||
}
|
||||
|
||||
pub fn put_signed(&self, hash: &InfoHash, value: Box<Value>, permanent: bool) -> Operation {
|
||||
let (sender, pending) = operation();
|
||||
self.state.runner.borrow_mut().put_signed(
|
||||
hash,
|
||||
value,
|
||||
move |success| {
|
||||
let _ = sender.send(outcome(success));
|
||||
},
|
||||
permanent,
|
||||
);
|
||||
pending
|
||||
}
|
||||
|
||||
pub fn put_encrypted(
|
||||
&self,
|
||||
hash: &InfoHash,
|
||||
recipient: &PkId,
|
||||
value: Box<Value>,
|
||||
permanent: bool,
|
||||
) -> Operation {
|
||||
let (sender, pending) = operation();
|
||||
self.state.runner.borrow_mut().put_encrypted(
|
||||
hash,
|
||||
recipient,
|
||||
value,
|
||||
move |success| {
|
||||
let _ = sender.send(outcome(success));
|
||||
},
|
||||
permanent,
|
||||
);
|
||||
pending
|
||||
}
|
||||
|
||||
pub fn ping(&self, address: SocketAddr) -> Operation {
|
||||
let (sender, pending) = operation();
|
||||
self.state
|
||||
.runner
|
||||
.borrow_mut()
|
||||
.ping(address, move |success| {
|
||||
let _ = sender.send(outcome(success));
|
||||
});
|
||||
pending
|
||||
}
|
||||
|
||||
/// Bootstrap pre-resolved addresses concurrently; one successful peer suffices.
|
||||
/// DNS resolution is left to the application's executor.
|
||||
pub fn bootstrap(&self, addresses: &[SocketAddr]) -> Operation {
|
||||
let (sender, pending) = operation();
|
||||
if addresses.is_empty() {
|
||||
let _ = sender.send(Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"at least one bootstrap address is required",
|
||||
)));
|
||||
return pending;
|
||||
}
|
||||
let completion = StreamCompletion(Arc::new(Mutex::new(Some(sender))));
|
||||
let remaining = Arc::new(std::sync::atomic::AtomicUsize::new(addresses.len()));
|
||||
for &address in addresses {
|
||||
let completion = completion.clone();
|
||||
let remaining = remaining.clone();
|
||||
self.state
|
||||
.runner
|
||||
.borrow_mut()
|
||||
.ping(address, move |success| {
|
||||
if success {
|
||||
completion.finish(Ok(()));
|
||||
}
|
||||
if remaining.fetch_sub(1, Ordering::SeqCst) == 1 {
|
||||
completion.finish(outcome(false));
|
||||
}
|
||||
});
|
||||
}
|
||||
pending
|
||||
}
|
||||
|
||||
pub fn get(&self, hash: &InfoHash) -> OperationStream<Box<Value>> {
|
||||
let (mut sender, stream) = stream_channel(self.capacity);
|
||||
let completion = sender.completion.clone();
|
||||
self.state.runner.borrow_mut().get(
|
||||
hash,
|
||||
move |value| sender.send(Ok(value)),
|
||||
move |success| completion.finish(outcome(success)),
|
||||
);
|
||||
stream
|
||||
}
|
||||
|
||||
pub fn get_filtered(
|
||||
&self,
|
||||
hash: &InfoHash,
|
||||
filter: &str,
|
||||
) -> io::Result<OperationStream<Box<Value>>> {
|
||||
let (mut sender, stream) = stream_channel(self.capacity);
|
||||
let completion = sender.completion.clone();
|
||||
self.state.runner.borrow_mut().get_filtered(
|
||||
hash,
|
||||
filter,
|
||||
move |value| sender.send(Ok(value)),
|
||||
move |success| completion.finish(outcome(success)),
|
||||
)?;
|
||||
Ok(stream)
|
||||
}
|
||||
|
||||
pub fn query(&self, hash: &InfoHash, query: &str) -> io::Result<OperationStream<QueryResult>> {
|
||||
let (mut sender, stream) = stream_channel(self.capacity);
|
||||
let completion = sender.completion.clone();
|
||||
self.state.runner.borrow_mut().query(
|
||||
hash,
|
||||
query,
|
||||
move |row| sender.send(Ok(row)),
|
||||
move |success| completion.finish(outcome(success)),
|
||||
)?;
|
||||
Ok(stream)
|
||||
}
|
||||
|
||||
pub fn listen(&self, hash: &InfoHash) -> io::Result<Listener> {
|
||||
self.listen_filtered(hash, "")
|
||||
}
|
||||
|
||||
pub fn listen_filtered(&self, hash: &InfoHash, filter: &str) -> io::Result<Listener> {
|
||||
let (mut sender, stream) = stream_channel(self.capacity);
|
||||
let token = self.state.runner.borrow_mut().listen_filtered(
|
||||
hash,
|
||||
filter,
|
||||
move |value, expired| sender.send(Ok(ValueEvent { value, expired })),
|
||||
)?;
|
||||
Ok(Listener {
|
||||
state: self.state.clone(),
|
||||
hash: InfoHash { d: hash.d },
|
||||
token: Some(token),
|
||||
stream,
|
||||
})
|
||||
}
|
||||
|
||||
/// Request graceful shutdown. Call `join()` after this future succeeds when
|
||||
/// persistence is required. Joining first reports an interrupted shutdown.
|
||||
pub fn shutdown(&self) -> Operation {
|
||||
let (sender, pending) = operation();
|
||||
let stopping = self.state.stopping.clone();
|
||||
self.state.runner.borrow_mut().shutdown(move || {
|
||||
let result = if stopping.load(Ordering::SeqCst) {
|
||||
Err(stopped("runner joined before graceful shutdown completed"))
|
||||
} else {
|
||||
Ok(())
|
||||
};
|
||||
let _ = sender.send(result);
|
||||
});
|
||||
pending
|
||||
}
|
||||
|
||||
/// Drive a non-threaded runner without waiting for any async operation.
|
||||
pub fn loop_once(&self) -> io::Result<()> {
|
||||
self.state.runner.borrow_mut().loop_once()
|
||||
}
|
||||
|
||||
/// Stop the runner and wait for native callbacks. This is a blocking call,
|
||||
/// not an async wait, and must not be invoked from a native DHT callback.
|
||||
pub fn join(&self) -> io::Result<()> {
|
||||
self.state.stopping.store(true, Ordering::SeqCst);
|
||||
self.state.runner.borrow_mut().join()
|
||||
}
|
||||
}
|
||||
+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)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
+717
-167
File diff suppressed because it is too large.
Load diff
+245
-80
@@ -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)]
|
||||
#[repr(C, align(4))]
|
||||
#[derive(PartialEq)]
|
||||
pub struct InfoHash
|
||||
{
|
||||
pub struct InfoHash {
|
||||
pub d: [u8; HASH_LEN],
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct PkId
|
||||
{
|
||||
#[repr(C, align(4))]
|
||||
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
|
||||
}
|
||||
|
||||
+39
-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;
|
||||
@@ -12,8 +40,17 @@ mod infohash;
|
||||
mod pkid;
|
||||
mod value;
|
||||
|
||||
#[cfg(feature = "async")]
|
||||
pub mod aio;
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
pub mod typed;
|
||||
|
||||
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};
|
||||
@@ -0,0 +1,201 @@
|
||||
//! MessagePack-encoded typed operations, enabled by the `serde` feature.
|
||||
//!
|
||||
//! This feature enables `async` and adds methods to [`AsyncDhtRunner`]. Values
|
||||
//! use compact MessagePack payloads, with no binding-specific envelope or
|
||||
//! implicit schema version. Structs are encoded as arrays in field order.
|
||||
//! Applications can include a version field in their records and use
|
||||
//! `serde_bytes` for binary fields rather than arrays of integers.
|
||||
//!
|
||||
//! ```no_run
|
||||
//! use futures::TryStreamExt;
|
||||
//! use opendht::{aio::AsyncDhtRunner, InfoHash};
|
||||
//! use serde::{Deserialize, Serialize};
|
||||
//!
|
||||
//! #[derive(Debug, Serialize, Deserialize)]
|
||||
//! struct Service {
|
||||
//! version: u32,
|
||||
//! name: String,
|
||||
//! port: u16,
|
||||
//! #[serde(with = "serde_bytes")]
|
||||
//! payload: Vec<u8>,
|
||||
//! }
|
||||
//!
|
||||
//! async fn exchange(
|
||||
//! runner: &AsyncDhtRunner,
|
||||
//! key: &InfoHash,
|
||||
//! ) -> Result<(), Box<dyn std::error::Error>> {
|
||||
//! let mut updates = runner.listen_typed::<Service>(key)?;
|
||||
//! let record = Service {
|
||||
//! version: 1, name: "chat".into(), port: 4222, payload: vec![0, 0x80, 0xff],
|
||||
//! };
|
||||
//! runner.put_typed(key, &record, false).await?;
|
||||
//! if let Some(event) = updates.try_next().await? {
|
||||
//! println!("{}: {:?}", event.value.raw.id(), event.value.data);
|
||||
//! }
|
||||
//! drop(updates);
|
||||
//! let records: Vec<_> = runner.get_typed::<Service>(key).try_collect().await?;
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
|
||||
use crate::aio::{AsyncDhtRunner, ValueEvent};
|
||||
use crate::{InfoHash, Value};
|
||||
use futures_core::Stream;
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use std::error::Error;
|
||||
use std::fmt;
|
||||
use std::io;
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
|
||||
/// A decoded record together with its original payload and native metadata.
|
||||
pub struct TypedValue<T> {
|
||||
pub data: T,
|
||||
pub raw: Box<Value>,
|
||||
}
|
||||
|
||||
impl<T: fmt::Debug> fmt::Debug for TypedValue<T> {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter
|
||||
.debug_struct("TypedValue")
|
||||
.field("data", &self.data)
|
||||
.field("id", &self.raw.id())
|
||||
.field("seq", &self.raw.seq())
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
/// A decoded listener update, including whether the record expired.
|
||||
#[derive(Debug)]
|
||||
pub struct TypedEvent<T> {
|
||||
pub value: TypedValue<T>,
|
||||
pub expired: bool,
|
||||
}
|
||||
|
||||
/// Typed operations distinguish native failures from payload format errors.
|
||||
#[derive(Debug)]
|
||||
#[non_exhaustive]
|
||||
pub enum TypedError {
|
||||
Operation(io::Error),
|
||||
Serialize(rmp_serde::encode::Error),
|
||||
Deserialize {
|
||||
id: u64,
|
||||
source: rmp_serde::decode::Error,
|
||||
},
|
||||
}
|
||||
|
||||
impl fmt::Display for TypedError {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Operation(error) => write!(formatter, "DHT operation failed: {error}"),
|
||||
Self::Serialize(error) => write!(formatter, "could not serialize typed value: {error}"),
|
||||
Self::Deserialize { id, source } => {
|
||||
write!(formatter, "could not deserialize DHT value {id}: {source}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for TypedError {
|
||||
fn source(&self) -> Option<&(dyn Error + 'static)> {
|
||||
match self {
|
||||
Self::Operation(error) => Some(error),
|
||||
Self::Serialize(error) => Some(error),
|
||||
Self::Deserialize { source, .. } => Some(source),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: DeserializeOwned> TryFrom<Box<Value>> for TypedValue<T> {
|
||||
type Error = TypedError;
|
||||
|
||||
fn try_from(raw: Box<Value>) -> Result<Self, Self::Error> {
|
||||
let data =
|
||||
rmp_serde::from_slice(raw.as_slice()).map_err(|source| TypedError::Deserialize {
|
||||
id: raw.id(),
|
||||
source,
|
||||
})?;
|
||||
Ok(Self { data, raw })
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: DeserializeOwned> TryFrom<ValueEvent> for TypedEvent<T> {
|
||||
type Error = TypedError;
|
||||
|
||||
fn try_from(event: ValueEvent) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
value: event.value.try_into()?,
|
||||
expired: event.expired,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct DecodeStream<Source: Stream, Output> {
|
||||
source: Source,
|
||||
decode: fn(Source::Item) -> Result<Output, TypedError>,
|
||||
}
|
||||
|
||||
impl<Source: Stream + Unpin, Output> Stream for DecodeStream<Source, Output> {
|
||||
type Item = Result<Output, TypedError>;
|
||||
|
||||
fn poll_next(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
let stream = self.get_mut();
|
||||
Pin::new(&mut stream.source)
|
||||
.poll_next(context)
|
||||
.map(|item| item.map(stream.decode))
|
||||
}
|
||||
}
|
||||
|
||||
fn decode<Input, Output: TryFrom<Input, Error = TypedError>>(
|
||||
input: io::Result<Input>,
|
||||
) -> Result<Output, TypedError> {
|
||||
input.map_err(TypedError::Operation)?.try_into()
|
||||
}
|
||||
|
||||
impl AsyncDhtRunner {
|
||||
/// Serialize a record as MessagePack and publish it when this future is polled.
|
||||
///
|
||||
/// Unlike untyped `put`, this operation is lazy. Serialization failure does
|
||||
/// not start a native put. Dropping it after publication starts does not
|
||||
/// undo the put. Use the untyped API to customize value IDs and metadata.
|
||||
pub async fn put_typed<T: Serialize + ?Sized>(
|
||||
&self,
|
||||
hash: &InfoHash,
|
||||
value: &T,
|
||||
permanent: bool,
|
||||
) -> Result<(), TypedError> {
|
||||
let bytes = rmp_serde::to_vec(value).map_err(TypedError::Serialize)?;
|
||||
self.put(hash, Value::from_bytes(&bytes), permanent)
|
||||
.await
|
||||
.map_err(TypedError::Operation)
|
||||
}
|
||||
|
||||
/// Start a get and decode each MessagePack payload on the consumer's thread.
|
||||
///
|
||||
/// A malformed or incompatible record yields a per-value error; polling can
|
||||
/// continue to subsequent records. Native errors remain terminal. The stream
|
||||
/// does not borrow the runner or hash, and records need not be `Unpin`.
|
||||
pub fn get_typed<T: DeserializeOwned>(
|
||||
&self,
|
||||
hash: &InfoHash,
|
||||
) -> impl Stream<Item = Result<TypedValue<T>, TypedError>> + Unpin + use<T> {
|
||||
DecodeStream {
|
||||
source: self.get(hash),
|
||||
decode: decode::<_, TypedValue<T>>,
|
||||
}
|
||||
}
|
||||
|
||||
/// Listen for MessagePack records, preserving metadata and expiration events.
|
||||
///
|
||||
/// Decode errors do not cancel the registration. Dropping this stream does
|
||||
/// cancel it, including when a short-circuiting consumer stops on an error.
|
||||
pub fn listen_typed<T: DeserializeOwned>(
|
||||
&self,
|
||||
hash: &InfoHash,
|
||||
) -> io::Result<impl Stream<Item = Result<TypedEvent<T>, TypedError>> + Unpin + use<T>> {
|
||||
Ok(DecodeStream {
|
||||
source: self.listen(hash)?,
|
||||
decode: decode::<_, TypedEvent<T>>,
|
||||
})
|
||||
}
|
||||
}
|
||||
+141
-42
@@ -2,91 +2,190 @@
|
||||
// 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);
|
||||
|
||||
/// Rust owns an unaliased native value: constructors allocate it, received
|
||||
/// values are deep-copied once, and boxed() deep-copies explicitly. Puts consume
|
||||
/// the handle before native code retains it. Shared owner keys are const and
|
||||
/// their lazy fingerprints are initialized before received values cross threads.
|
||||
/// Mutation requires exclusive access; native storage may be freed on any thread.
|
||||
/// Value is Send, not Sync.
|
||||
unsafe impl Send for Value {}
|
||||
|
||||
impl AsRef<[u8]> for Value {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
self.as_slice()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for Value {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter
|
||||
.debug_struct("Value")
|
||||
.field("id", &self.id())
|
||||
.field("seq", &self.seq())
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl Value {
|
||||
pub fn new(data: &str) -> Box<Value> {
|
||||
pub(crate) unsafe fn copy_from_received(value: *const ffi::Value) -> io::Result<Box<Self>> {
|
||||
let value = unsafe { Self::from_raw(ffi::dht_value_copy(value))? };
|
||||
if let Some(owner) = value.owner() {
|
||||
owner.id();
|
||||
owner.long_id();
|
||||
}
|
||||
Ok(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);
|
||||
slice.to_vec()
|
||||
self.as_slice().to_vec()
|
||||
}
|
||||
|
||||
/// Borrow the payload without allocating or copying it.
|
||||
pub fn as_slice(&self) -> &[u8] {
|
||||
let view = self.dataview();
|
||||
if view.size == 0 {
|
||||
&[]
|
||||
} else {
|
||||
unsafe { slice::from_raw_parts(view.data, view.size) }
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
let ptr = dht_value_get_owner(self);
|
||||
if ptr.is_null() {
|
||||
return None;
|
||||
}
|
||||
Some(Box::from_raw(ptr))
|
||||
}
|
||||
unsafe { PublicKey::from_raw(ffi::dht_value_get_owner(self.as_ptr())).ok() }
|
||||
}
|
||||
|
||||
pub fn user_type(&self) -> Result<String, IntoStringError> {
|
||||
pub fn user_type(&self) -> Result<&str, str::Utf8Error> {
|
||||
unsafe { CStr::from_ptr(ffi::dht_value_get_user_type(self.as_ptr())).to_str() }
|
||||
}
|
||||
|
||||
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<&str, str::Utf8Error> {
|
||||
unsafe { CStr::from_ptr(ffi::dht_value_get_push_type(self.as_ptr())).to_str() }
|
||||
}
|
||||
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(""))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn value_moves_between_threads_without_copying_payload() {
|
||||
let mut value = Value::from_bytes(b"native\0payload\xff");
|
||||
value.set_user_type("borrowed metadata").unwrap();
|
||||
let view = unsafe { ffi::dht_value_get_data(value.as_ptr()) };
|
||||
let value = std::thread::spawn(move || {
|
||||
value.set_seq(7);
|
||||
value
|
||||
})
|
||||
.join()
|
||||
.unwrap();
|
||||
assert_eq!(value.as_slice().as_ptr(), view.data);
|
||||
assert_eq!(value.as_slice(), b"native\0payload\xff");
|
||||
assert_eq!(value.seq(), 7);
|
||||
let user_type: &str = value.user_type().unwrap();
|
||||
assert_eq!(user_type, "borrowed metadata");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_value_payload_is_borrowed_safely() {
|
||||
let value = Value::from_bytes(&[]);
|
||||
assert!(value.as_slice().is_empty());
|
||||
assert_eq!(value.as_slice(), &[]);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
+4
-33
@@ -598,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;
|
||||
}
|
||||
|
||||
@@ -1659,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;
|
||||
@@ -1735,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;
|
||||
|
||||
+7
-6
@@ -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;
|
||||
}
|
||||
@@ -2658,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 */
|
||||
|
||||
+50
-30
@@ -97,7 +97,7 @@ std::string
|
||||
DhtProxyServer::ServerStats::toString() const
|
||||
{
|
||||
auto ret = fmt::format("Listens: {}, Puts: {}, PushListeners: {}\n"
|
||||
"Push requests in the last {}: [Android: {}], [iOS: {}], [Unified: {}]\n"
|
||||
"Push requests in the last {}: [Android: {}], [iOS: {}], [Unified: {}], [Huawei: {}]\n"
|
||||
"Requests: {} per second.",
|
||||
listenCount,
|
||||
putCount,
|
||||
@@ -106,6 +106,7 @@ DhtProxyServer::ServerStats::toString() const
|
||||
androidPush.toString(),
|
||||
iosPush.toString(),
|
||||
unifiedPush.toString(),
|
||||
huaweiPush.toString(),
|
||||
requestRate);
|
||||
if (nodeInfo) {
|
||||
auto& ipv4 = nodeInfo->ipv4;
|
||||
@@ -131,6 +132,7 @@ DhtProxyServer::ServerStats::toJson() const
|
||||
result["androidPush"] = androidPush.toJson();
|
||||
result["iosPush"] = iosPush.toJson();
|
||||
result["unifiedPush"] = unifiedPush.toJson();
|
||||
result["huaweiPush"] = huaweiPush.toJson();
|
||||
result["requestRate"] = requestRate;
|
||||
if (nodeInfo)
|
||||
result["nodeInfo"] = nodeInfo->toJson();
|
||||
@@ -466,23 +468,26 @@ 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);
|
||||
});
|
||||
@@ -636,6 +641,7 @@ DhtProxyServer::updateStats(std::shared_ptr<NodeInfo> info) const
|
||||
stats.androidPush = androidPush_;
|
||||
stats.iosPush = iosPush_;
|
||||
stats.unifiedPush = unifiedPush_;
|
||||
stats.huaweiPush = huaweiPush_;
|
||||
}
|
||||
#endif
|
||||
stats.serverStartTime = serverStartTime_;
|
||||
@@ -861,6 +867,8 @@ DhtProxyServer::getTypeFromString(const std::string& type)
|
||||
return PushType::iOS;
|
||||
else if (type == "unifiedpush")
|
||||
return PushType::UnifiedPush;
|
||||
else if (type == "huawei")
|
||||
return PushType::Huawei;
|
||||
return PushType::None;
|
||||
}
|
||||
|
||||
@@ -957,9 +965,14 @@ 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;
|
||||
|
||||
@@ -1027,23 +1040,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");
|
||||
@@ -1150,10 +1155,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)
|
||||
{
|
||||
@@ -1163,9 +1166,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
|
||||
@@ -1273,15 +1280,25 @@ DhtProxyServer::sendPushNotification(
|
||||
Json::Value tokens(Json::arrayValue);
|
||||
tokens[0] = token;
|
||||
notification["tokens"] = std::move(tokens);
|
||||
notification["platform"] = type == PushType::Android ? 2 : 1;
|
||||
notification["data"] = std::move(json);
|
||||
// gorush platform codes: 1 = iOS (APNs), 2 = Android (FCM), 3 = Huawei (HMS)
|
||||
notification["platform"] = type == PushType::Android ? 2
|
||||
: type == PushType::Huawei ? 3
|
||||
: 1;
|
||||
auto priority = highPriority ? "high" : "normal";
|
||||
if (type == PushType::Android) {
|
||||
if (type == PushType::Huawei) {
|
||||
// HMS carries the payload as a serialized JSON string in its own field:
|
||||
// the generic "data" object is ignored for this platform by gorush.
|
||||
notification["huawei_data"] = Json::writeString(jsonBuilder_, json);
|
||||
notification["huawei_ttl"] = "86400s"; // time to live = 24 hours
|
||||
notification["priority"] = priority;
|
||||
} else if (type == PushType::Android) {
|
||||
notification["data"] = std::move(json);
|
||||
Json::Value androidConfig(Json::objectValue);
|
||||
androidConfig["priority"] = priority;
|
||||
androidConfig["ttl"] = "86400s"; // time to live = 24 hours
|
||||
notification["android"] = std::move(androidConfig);
|
||||
} else {
|
||||
notification["data"] = std::move(json);
|
||||
notification["priority"] = priority;
|
||||
const auto expiration = std::chrono::system_clock::now() + std::chrono::hours(24);
|
||||
uint32_t exp = std::chrono::duration_cast<std::chrono::seconds>(expiration.time_since_epoch()).count();
|
||||
@@ -1334,6 +1351,9 @@ DhtProxyServer::sendPushNotification(
|
||||
case PushType::UnifiedPush:
|
||||
unifiedPush_.increment(highPriority);
|
||||
break;
|
||||
case PushType::Huawei:
|
||||
huaweiPush_.increment(highPriority);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -407,6 +407,7 @@ DhtRunner::join()
|
||||
{
|
||||
std::lock_guard lck(dht_mtx);
|
||||
resetDht();
|
||||
statusCbs.clear();
|
||||
status4 = NodeStatus::Disconnected;
|
||||
status6 = NodeStatus::Disconnected;
|
||||
}
|
||||
@@ -541,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);
|
||||
}
|
||||
|
||||
@@ -1239,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));
|
||||
|
||||
+4
-2
@@ -39,8 +39,10 @@ OpValueCache::onValuesExpired(const std::vector<Sp<Value>>& vals, const system_c
|
||||
if (vit != values.end()) {
|
||||
if (vit->second.updated > t)
|
||||
continue;
|
||||
if (v->seq < vit->second.data->seq)
|
||||
continue; // source held an older version, not counted
|
||||
// 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--;
|
||||
|
||||
+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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+9
-1
@@ -397,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;
|
||||
@@ -591,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)
|
||||
{
|
||||
|
||||
+1
-1
@@ -189,7 +189,7 @@ private:
|
||||
v->second.created = now;
|
||||
v->second.expiration = now + te;
|
||||
v->second.typeExpiration = te;
|
||||
if (value->seq > v->second.data->seq) {
|
||||
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);
|
||||
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
#ifdef NDEBUG
|
||||
#undef NDEBUG
|
||||
#endif
|
||||
|
||||
#include "opendht_c.h"
|
||||
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#ifdef _MSC_VER
|
||||
#include <gnutls/gnutls.h>
|
||||
#endif
|
||||
|
||||
_Static_assert(_Alignof(dht_infohash) == _Alignof(uint32_t), "InfoHash alignment must match uint32_t");
|
||||
_Static_assert(_Alignof(dht_pkid) == _Alignof(uint32_t), "PkId alignment must match uint32_t");
|
||||
_Static_assert(sizeof(dht_infohash) == HASH_LEN, "InfoHash size must remain unchanged");
|
||||
_Static_assert(sizeof(dht_pkid) == 32, "PkId size must remain unchanged");
|
||||
|
||||
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 _MSC_VER
|
||||
int crypto_error = gnutls_global_init();
|
||||
if (crypto_error != GNUTLS_E_SUCCESS) {
|
||||
fprintf(stderr, "Failed to initialize GnuTLS: %s\n", gnutls_strerror(crypto_error));
|
||||
return 1;
|
||||
}
|
||||
#endif
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
struct {
|
||||
uint8_t prefix;
|
||||
dht_infohash hash;
|
||||
dht_pkid pkid;
|
||||
} embedded = {0};
|
||||
assert((uintptr_t) &embedded.hash % _Alignof(uint32_t) == 0);
|
||||
assert((uintptr_t) &embedded.pkid % _Alignof(uint32_t) == 0);
|
||||
dht_infohash_get_from_string(&embedded.hash, "abc");
|
||||
assert(!dht_infohash_is_zero(&embedded.hash));
|
||||
assert(strcmp(dht_infohash_print(&embedded.hash), "a9993e364706816aba3e25717850c26c9cd0d89d") == 0);
|
||||
dht_infohash_from_hex_null(&embedded.hash, "a9993e364706816aba3e25717850c26c9cd0d89d");
|
||||
dht_infohash_from_hex(&restored, dht_infohash_print(&embedded.hash));
|
||||
assert(memcmp(&embedded.hash, &restored, sizeof(restored)) == 0);
|
||||
dht_infohash_zero(&embedded.hash);
|
||||
assert(dht_infohash_is_zero(&embedded.hash));
|
||||
dht_infohash_random(&embedded.hash);
|
||||
assert(!dht_infohash_is_zero(&embedded.hash));
|
||||
assert(strcmp(dht_pkid_print(&embedded.pkid),
|
||||
"0000000000000000000000000000000000000000000000000000000000000000") == 0);
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
#ifdef _MSC_VER
|
||||
gnutls_global_deinit();
|
||||
#endif
|
||||
puts("C binding regressions passed");
|
||||
return 0;
|
||||
}
|
||||
+335
-152
@@ -7,8 +7,13 @@
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <sstream>
|
||||
#include <thread>
|
||||
#include <asio.hpp>
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
@@ -16,6 +21,148 @@ using namespace std::chrono_literals;
|
||||
namespace test {
|
||||
CPPUNIT_TEST_SUITE_REGISTRATION(DhtProxyTester);
|
||||
|
||||
#ifdef OPENDHT_PUSH_NOTIFICATIONS
|
||||
namespace {
|
||||
// The result is produced on the DHT thread, which may be blocked in a listen
|
||||
// callback waiting for the same mutex: never hold it while waiting.
|
||||
dht::PushNotificationResult
|
||||
sendPush(dht::DhtRunner& node, const std::map<std::string, std::string>& data, std::unique_lock<std::mutex>& lk)
|
||||
{
|
||||
lk.unlock();
|
||||
auto ret = node.pushNotificationReceived(data).get();
|
||||
lk.lock();
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Data of a gorush notification, as a client receives it from the push service.
|
||||
std::map<std::string, std::string>
|
||||
toPushData(const Json::Value& data)
|
||||
{
|
||||
std::map<std::string, std::string> notif;
|
||||
for (const auto& name : data.getMemberNames()) {
|
||||
const auto& value = data[name];
|
||||
if (value.isString())
|
||||
notif[name] = value.asString();
|
||||
else if (value.isBool())
|
||||
notif[name] = value.asBool() ? "true" : "false";
|
||||
else if (value.isUInt64())
|
||||
notif[name] = std::to_string(value.asUInt64());
|
||||
else if (value.isInt64())
|
||||
notif[name] = std::to_string(value.asInt64());
|
||||
}
|
||||
return notif;
|
||||
}
|
||||
|
||||
// Stand-in for the push gateway (gorush) the proxy server posts to:
|
||||
// hands every notification it receives to the test.
|
||||
class PushGateway
|
||||
{
|
||||
public:
|
||||
using OnNotification = std::function<void(const Json::Value& notification)>;
|
||||
|
||||
PushGateway(uint16_t port, OnNotification onNotification)
|
||||
: acceptor_(io_, asio::ip::tcp::endpoint(asio::ip::address_v4::loopback(), port))
|
||||
, onNotification_(std::move(onNotification))
|
||||
, thread_([this] { run(); })
|
||||
{}
|
||||
|
||||
~PushGateway()
|
||||
{
|
||||
stop_ = true;
|
||||
if (thread_.joinable())
|
||||
thread_.join();
|
||||
}
|
||||
|
||||
private:
|
||||
void run()
|
||||
{
|
||||
try {
|
||||
acceptor_.non_blocking(true);
|
||||
while (not stop_.load()) {
|
||||
asio::ip::tcp::socket socket(io_);
|
||||
std::error_code ec;
|
||||
acceptor_.accept(socket, ec);
|
||||
if (ec == asio::error::would_block || ec == asio::error::try_again) {
|
||||
std::this_thread::sleep_for(5ms);
|
||||
continue;
|
||||
}
|
||||
if (ec)
|
||||
continue;
|
||||
socket.non_blocking(true);
|
||||
|
||||
std::string raw;
|
||||
raw.reserve(4096);
|
||||
auto deadline = std::chrono::steady_clock::now() + 2s;
|
||||
while (raw.find("\r\n\r\n") == std::string::npos && std::chrono::steady_clock::now() < deadline
|
||||
&& not stop_.load()) {
|
||||
std::array<char, 2048> chunk {};
|
||||
size_t n = socket.read_some(asio::buffer(chunk), ec);
|
||||
if (!ec) {
|
||||
raw.append(chunk.data(), n);
|
||||
} else if (ec == asio::error::would_block || ec == asio::error::try_again) {
|
||||
std::this_thread::sleep_for(2ms);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
auto sep = raw.find("\r\n\r\n");
|
||||
if (sep == std::string::npos)
|
||||
continue;
|
||||
|
||||
std::string head = raw.substr(0, sep + 4);
|
||||
auto clPos = head.find("Content-Length:");
|
||||
size_t contentLength = 0;
|
||||
if (clPos != std::string::npos) {
|
||||
clPos += std::string("Content-Length:").size();
|
||||
while (clPos < head.size() && (head[clPos] == ' ' || head[clPos] == '\t'))
|
||||
++clPos;
|
||||
size_t clEnd = head.find("\r\n", clPos);
|
||||
contentLength = std::stoul(head.substr(clPos, clEnd - clPos));
|
||||
}
|
||||
|
||||
std::string body = raw.substr(sep + 4);
|
||||
while (body.size() < contentLength && std::chrono::steady_clock::now() < deadline && not stop_.load()) {
|
||||
std::array<char, 2048> chunk {};
|
||||
size_t n = socket.read_some(asio::buffer(chunk), ec);
|
||||
if (!ec) {
|
||||
body.append(chunk.data(), n);
|
||||
} else if (ec == asio::error::would_block || ec == asio::error::try_again) {
|
||||
std::this_thread::sleep_for(2ms);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (body.size() > contentLength)
|
||||
body.resize(contentLength);
|
||||
|
||||
Json::Value payload;
|
||||
std::string err;
|
||||
auto reader = std::unique_ptr<Json::CharReader>(Json::CharReaderBuilder().newCharReader());
|
||||
if (reader->parse(body.data(), body.data() + body.size(), &payload, &err)
|
||||
&& payload.isMember("notifications") && payload["notifications"].isArray()
|
||||
&& !payload["notifications"].empty())
|
||||
onNotification_(payload["notifications"][0]);
|
||||
|
||||
static const std::string okResp = "HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nOK";
|
||||
asio::write(socket, asio::buffer(okResp), ec);
|
||||
}
|
||||
} catch (...) {
|
||||
}
|
||||
}
|
||||
|
||||
asio::io_context io_;
|
||||
asio::ip::tcp::acceptor acceptor_;
|
||||
OnNotification onNotification_;
|
||||
std::atomic_bool stop_ {false};
|
||||
std::thread thread_;
|
||||
};
|
||||
|
||||
// gorush "platform" values used by the proxy server.
|
||||
constexpr int GORUSH_PLATFORM_IOS = 1;
|
||||
constexpr int GORUSH_PLATFORM_ANDROID = 2;
|
||||
} // namespace
|
||||
#endif
|
||||
|
||||
void
|
||||
DhtProxyTester::setUp()
|
||||
{
|
||||
@@ -332,7 +479,7 @@ DhtProxyTester::testPushListenValueEdit()
|
||||
push_data["t"] = std::to_string(
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch())
|
||||
.count());
|
||||
nodeClient.pushNotificationReceived(push_data).get();
|
||||
sendPush(nodeClient, push_data, lk);
|
||||
|
||||
// Wait for first add callback
|
||||
CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] {
|
||||
@@ -357,7 +504,7 @@ DhtProxyTester::testPushListenValueEdit()
|
||||
push_data["t"] = std::to_string(
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch())
|
||||
.count());
|
||||
nodeClient.pushNotificationReceived(push_data).get();
|
||||
sendPush(nodeClient, push_data, lk);
|
||||
|
||||
// Wait for the second add
|
||||
CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] {
|
||||
@@ -389,7 +536,7 @@ DhtProxyTester::testPushListenValueEdit()
|
||||
push_data["t"] = std::to_string(
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch())
|
||||
.count());
|
||||
CPPUNIT_ASSERT(nodeClient.pushNotificationReceived(push_data).get() == dht::PushNotificationResult::ValuesExpired);
|
||||
CPPUNIT_ASSERT(sendPush(nodeClient, push_data, lk) == dht::PushNotificationResult::ValuesExpired);
|
||||
|
||||
// Wait for the expire callback
|
||||
CPPUNIT_ASSERT_MESSAGE("Should receive expire callback via push", cv.wait_for(lk, 10s, [&] {
|
||||
@@ -541,7 +688,7 @@ DhtProxyTester::testPushNotification()
|
||||
push_data["t"] = std::to_string(
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch())
|
||||
.count());
|
||||
CPPUNIT_ASSERT(nodeClient.pushNotificationReceived(push_data).get() == dht::PushNotificationResult::Values);
|
||||
CPPUNIT_ASSERT(sendPush(nodeClient, push_data, lk) == dht::PushNotificationResult::Values);
|
||||
|
||||
// Wait for client to receive the value
|
||||
CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] { return gotValues; }));
|
||||
@@ -570,7 +717,7 @@ DhtProxyTester::testPushNotification()
|
||||
push_data["t"] = std::to_string(
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch())
|
||||
.count());
|
||||
CPPUNIT_ASSERT(nodeClient.pushNotificationReceived(push_data).get() == dht::PushNotificationResult::Values);
|
||||
CPPUNIT_ASSERT(sendPush(nodeClient, push_data, lk) == dht::PushNotificationResult::Values);
|
||||
|
||||
// Wait for client to receive the edited value
|
||||
CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] { return gotValues; }));
|
||||
@@ -587,7 +734,7 @@ DhtProxyTester::testPushNotification()
|
||||
push_data["t"] = std::to_string(
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch())
|
||||
.count());
|
||||
CPPUNIT_ASSERT(nodeClient.pushNotificationReceived(push_data).get() == dht::PushNotificationResult::ValuesExpired);
|
||||
CPPUNIT_ASSERT(sendPush(nodeClient, push_data, lk) == dht::PushNotificationResult::ValuesExpired);
|
||||
|
||||
cv.wait_for(lk, 500ms, [&] { return gotExpiredValues; });
|
||||
|
||||
@@ -618,113 +765,13 @@ DhtProxyTester::testProxyServerClientFullChain()
|
||||
uint16_t pushPort = 1024 + (std::rand() % (65535 - 1024));
|
||||
uint16_t proxyPort = 1024 + (std::rand() % (65535 - 1024));
|
||||
|
||||
std::atomic_bool relayStop {false};
|
||||
std::atomic_size_t relayedPushCount {0};
|
||||
std::thread pushRelay([&] {
|
||||
try {
|
||||
asio::io_context io;
|
||||
asio::ip::tcp::acceptor acceptor(io, asio::ip::tcp::endpoint(asio::ip::address_v4::loopback(), pushPort));
|
||||
acceptor.non_blocking(true);
|
||||
|
||||
while (not relayStop.load()) {
|
||||
asio::ip::tcp::socket socket(io);
|
||||
std::error_code ec;
|
||||
acceptor.accept(socket, ec);
|
||||
if (ec == asio::error::would_block || ec == asio::error::try_again) {
|
||||
std::this_thread::sleep_for(5ms);
|
||||
continue;
|
||||
}
|
||||
if (ec)
|
||||
continue;
|
||||
|
||||
socket.non_blocking(true);
|
||||
|
||||
std::string raw;
|
||||
raw.reserve(4096);
|
||||
auto deadline = std::chrono::steady_clock::now() + 2s;
|
||||
while (raw.find("\r\n\r\n") == std::string::npos && std::chrono::steady_clock::now() < deadline
|
||||
&& not relayStop.load()) {
|
||||
std::array<char, 2048> chunk {};
|
||||
size_t n = socket.read_some(asio::buffer(chunk), ec);
|
||||
if (!ec) {
|
||||
raw.append(chunk.data(), n);
|
||||
} else if (ec == asio::error::would_block || ec == asio::error::try_again) {
|
||||
std::this_thread::sleep_for(2ms);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (raw.find("\r\n\r\n") == std::string::npos)
|
||||
continue;
|
||||
|
||||
auto sep = raw.find("\r\n\r\n");
|
||||
std::string head = raw.substr(0, sep + 4);
|
||||
auto clPos = head.find("Content-Length:");
|
||||
size_t contentLength = 0;
|
||||
if (clPos != std::string::npos) {
|
||||
clPos += std::string("Content-Length:").size();
|
||||
while (clPos < head.size() && (head[clPos] == ' ' || head[clPos] == '\t'))
|
||||
++clPos;
|
||||
size_t clEnd = head.find("\r\n", clPos);
|
||||
contentLength = std::stoul(head.substr(clPos, clEnd - clPos));
|
||||
}
|
||||
|
||||
std::string body = raw.substr(sep + 4);
|
||||
while (body.size() < contentLength && std::chrono::steady_clock::now() < deadline
|
||||
&& not relayStop.load()) {
|
||||
std::array<char, 2048> chunk {};
|
||||
size_t n = socket.read_some(asio::buffer(chunk), ec);
|
||||
if (!ec) {
|
||||
body.append(chunk.data(), n);
|
||||
} else if (ec == asio::error::would_block || ec == asio::error::try_again) {
|
||||
std::this_thread::sleep_for(2ms);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (body.size() > contentLength)
|
||||
body.resize(contentLength);
|
||||
|
||||
Json::Value payload;
|
||||
std::string err;
|
||||
auto reader = std::unique_ptr<Json::CharReader>(Json::CharReaderBuilder().newCharReader());
|
||||
if (reader->parse(body.data(), body.data() + body.size(), &payload, &err)
|
||||
&& payload.isMember("notifications") && payload["notifications"].isArray()
|
||||
&& !payload["notifications"].empty() && payload["notifications"][0].isMember("data")) {
|
||||
const auto& data = payload["notifications"][0]["data"];
|
||||
std::map<std::string, std::string> notif;
|
||||
for (const auto& name : data.getMemberNames()) {
|
||||
const auto& value = data[name];
|
||||
if (value.isString())
|
||||
notif[name] = value.asString();
|
||||
else if (value.isBool())
|
||||
notif[name] = value.asBool() ? "true" : "false";
|
||||
else if (value.isUInt64())
|
||||
notif[name] = std::to_string(value.asUInt64());
|
||||
else if (value.isInt64())
|
||||
notif[name] = std::to_string(value.asInt64());
|
||||
}
|
||||
nodeClient.pushNotificationReceived(notif);
|
||||
relayedPushCount++;
|
||||
}
|
||||
|
||||
static const std::string okResp = "HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nOK";
|
||||
asio::write(socket, asio::buffer(okResp), ec);
|
||||
}
|
||||
} catch (...) {
|
||||
}
|
||||
PushGateway pushGateway(pushPort, [&](const Json::Value& notification) {
|
||||
if (!notification.isMember("data"))
|
||||
return;
|
||||
nodeClient.pushNotificationReceived(toPushData(notification["data"]));
|
||||
relayedPushCount++;
|
||||
});
|
||||
struct RelayGuard
|
||||
{
|
||||
std::atomic_bool& stop;
|
||||
std::thread& th;
|
||||
~RelayGuard()
|
||||
{
|
||||
stop = true;
|
||||
if (th.joinable())
|
||||
th.join();
|
||||
}
|
||||
} relayGuard {relayStop, pushRelay};
|
||||
|
||||
serverProxy.reset();
|
||||
dht::ProxyServerConfig serverConfig;
|
||||
@@ -1118,7 +1165,8 @@ DhtProxyTester::testPushMultiValueEditExpire()
|
||||
auto key = dht::InfoHash::get("pushMultiEditExpire");
|
||||
auto identity = dht::crypto::generateIdentity("MultiEditAuthor");
|
||||
|
||||
struct Event {
|
||||
struct Event
|
||||
{
|
||||
dht::Value::Id id;
|
||||
uint16_t seq;
|
||||
bool expired;
|
||||
@@ -1176,7 +1224,7 @@ DhtProxyTester::testPushMultiValueEditExpire()
|
||||
push_data["t"] = std::to_string(
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch())
|
||||
.count());
|
||||
nodeClient.pushNotificationReceived(push_data).get();
|
||||
sendPush(nodeClient, push_data, lk);
|
||||
|
||||
// Wait for both values to arrive
|
||||
CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] {
|
||||
@@ -1209,7 +1257,7 @@ DhtProxyTester::testPushMultiValueEditExpire()
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch())
|
||||
.count());
|
||||
push_data.erase("exp");
|
||||
nodeClient.pushNotificationReceived(push_data).get();
|
||||
sendPush(nodeClient, push_data, lk);
|
||||
|
||||
// Wait for the edited value (id=100, seq=2) to arrive
|
||||
CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] {
|
||||
@@ -1232,13 +1280,11 @@ DhtProxyTester::testPushMultiValueEditExpire()
|
||||
push_data["t"] = std::to_string(
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch())
|
||||
.count());
|
||||
CPPUNIT_ASSERT(nodeClient.pushNotificationReceived(push_data).get() == dht::PushNotificationResult::ValuesExpired);
|
||||
CPPUNIT_ASSERT(sendPush(nodeClient, push_data, lk) == dht::PushNotificationResult::ValuesExpired);
|
||||
|
||||
// Wait for expire callback for value B
|
||||
CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] {
|
||||
return std::any_of(events.begin(), events.end(), [](const Event& e) {
|
||||
return e.id == 200 && e.expired;
|
||||
});
|
||||
return std::any_of(events.begin(), events.end(), [](const Event& e) { return e.id == 200 && e.expired; });
|
||||
}));
|
||||
|
||||
// Verify: value A (edited) should NOT have been expired
|
||||
@@ -1288,7 +1334,8 @@ DhtProxyTester::testPushRefreshNoSpuriousExpire()
|
||||
auto key = dht::InfoHash::get("pushRefreshNoExpire");
|
||||
auto identity = dht::crypto::generateIdentity("RefreshAuthor");
|
||||
|
||||
struct Event {
|
||||
struct Event
|
||||
{
|
||||
dht::Value::Id id;
|
||||
bool expired;
|
||||
};
|
||||
@@ -1328,7 +1375,7 @@ DhtProxyTester::testPushRefreshNoSpuriousExpire()
|
||||
push_data["t"] = std::to_string(
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch())
|
||||
.count());
|
||||
nodeClient.pushNotificationReceived(push_data).get();
|
||||
sendPush(nodeClient, push_data, lk);
|
||||
|
||||
// Wait for all 3 values to arrive
|
||||
CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] {
|
||||
@@ -1354,7 +1401,7 @@ DhtProxyTester::testPushRefreshNoSpuriousExpire()
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch())
|
||||
.count());
|
||||
push_data.erase("exp");
|
||||
nodeClient.pushNotificationReceived(push_data).get();
|
||||
sendPush(nodeClient, push_data, lk);
|
||||
|
||||
// Wait a bit for any callbacks
|
||||
cv.wait_for(lk, 1s);
|
||||
@@ -1375,13 +1422,11 @@ DhtProxyTester::testPushRefreshNoSpuriousExpire()
|
||||
push_data["t"] = std::to_string(
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch())
|
||||
.count());
|
||||
nodeClient.pushNotificationReceived(push_data).get();
|
||||
sendPush(nodeClient, push_data, lk);
|
||||
|
||||
// Wait for expire callback
|
||||
CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] {
|
||||
return std::any_of(events.begin(), events.end(), [](const Event& e) {
|
||||
return e.id == 20 && e.expired;
|
||||
});
|
||||
return std::any_of(events.begin(), events.end(), [](const Event& e) { return e.id == 20 && e.expired; });
|
||||
}));
|
||||
|
||||
// Verify only value 20 expired, not 10 or 30
|
||||
@@ -1426,7 +1471,12 @@ DhtProxyTester::testProxyListenEditChain()
|
||||
auto key = dht::InfoHash::get("proxyEditChain");
|
||||
auto identity = dht::crypto::generateIdentity("ProxyChainEditTester");
|
||||
|
||||
struct Event { dht::Value::Id id; uint16_t seq; bool expired; };
|
||||
struct Event
|
||||
{
|
||||
dht::Value::Id id;
|
||||
uint16_t seq;
|
||||
bool expired;
|
||||
};
|
||||
std::vector<Event> events;
|
||||
|
||||
auto ftoken = nodeClient.listen(key, [&](const std::vector<std::shared_ptr<dht::Value>>& vals, bool expired) {
|
||||
@@ -1493,8 +1543,7 @@ DhtProxyTester::testProxyListenEditChain()
|
||||
for (const auto& e : events)
|
||||
if (!e.expired && e.seq > maxSeq)
|
||||
maxSeq = e.seq;
|
||||
CPPUNIT_ASSERT_EQUAL_MESSAGE("Latest add should be the final version",
|
||||
(uint16_t) NUM_EDITS, maxSeq);
|
||||
CPPUNIT_ASSERT_EQUAL_MESSAGE("Latest add should be the final version", (uint16_t) NUM_EDITS, maxSeq);
|
||||
}
|
||||
|
||||
// Wait for natural expiration
|
||||
@@ -1518,8 +1567,7 @@ DhtProxyTester::testProxyListenEditChain()
|
||||
}
|
||||
}
|
||||
CPPUNIT_ASSERT_EQUAL_MESSAGE("Should have exactly 1 expire via proxy", 1, expireCount);
|
||||
CPPUNIT_ASSERT_EQUAL_MESSAGE("Expired value should be the latest",
|
||||
(uint16_t) NUM_EDITS, expiredSeq);
|
||||
CPPUNIT_ASSERT_EQUAL_MESSAGE("Expired value should be the latest", (uint16_t) NUM_EDITS, expiredSeq);
|
||||
}
|
||||
|
||||
nodeClient.cancelListen(key, ftoken.get());
|
||||
@@ -1557,7 +1605,12 @@ DhtProxyTester::testPushRapidEditsNoPhantom()
|
||||
auto key = dht::InfoHash::get("pushRapidEdits");
|
||||
auto identity = dht::crypto::generateIdentity("RapidEditAuthor");
|
||||
|
||||
struct Event { dht::Value::Id id; uint16_t seq; bool expired; };
|
||||
struct Event
|
||||
{
|
||||
dht::Value::Id id;
|
||||
uint16_t seq;
|
||||
bool expired;
|
||||
};
|
||||
std::vector<Event> events;
|
||||
|
||||
auto ftoken = nodeClient.listen(key, [&](const std::vector<std::shared_ptr<dht::Value>>& vals, bool expired) {
|
||||
@@ -1593,12 +1646,9 @@ DhtProxyTester::testPushRapidEditsNoPhantom()
|
||||
std::map<std::string, std::string> push_data;
|
||||
push_data["key"] = key.toString();
|
||||
push_data["t"] = std::to_string(
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::system_clock::now().time_since_epoch())
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch())
|
||||
.count());
|
||||
lk.unlock();
|
||||
nodeClient.pushNotificationReceived(push_data).get();
|
||||
lk.lock();
|
||||
sendPush(nodeClient, push_data, lk);
|
||||
}
|
||||
|
||||
// Wait for edits to arrive (at least 2: initial + latest)
|
||||
@@ -1669,7 +1719,12 @@ DhtProxyTester::testPushRefreshAfterEditKeepsLatest()
|
||||
auto key = dht::InfoHash::get("pushRefreshAfterEdit");
|
||||
auto identity = dht::crypto::generateIdentity("RefreshEditAuthor");
|
||||
|
||||
struct Event { dht::Value::Id id; uint16_t seq; bool expired; };
|
||||
struct Event
|
||||
{
|
||||
dht::Value::Id id;
|
||||
uint16_t seq;
|
||||
bool expired;
|
||||
};
|
||||
std::vector<Event> events;
|
||||
|
||||
auto ftoken = nodeClient.listen(key, [&](const std::vector<std::shared_ptr<dht::Value>>& vals, bool expired) {
|
||||
@@ -1705,12 +1760,9 @@ DhtProxyTester::testPushRefreshAfterEditKeepsLatest()
|
||||
std::map<std::string, std::string> push_data;
|
||||
push_data["key"] = key.toString();
|
||||
push_data["t"] = std::to_string(
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::system_clock::now().time_since_epoch())
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch())
|
||||
.count());
|
||||
lk.unlock();
|
||||
nodeClient.pushNotificationReceived(push_data).get();
|
||||
lk.lock();
|
||||
sendPush(nodeClient, push_data, lk);
|
||||
}
|
||||
|
||||
// Wait for initial add
|
||||
@@ -1742,12 +1794,9 @@ DhtProxyTester::testPushRefreshAfterEditKeepsLatest()
|
||||
std::map<std::string, std::string> push_data;
|
||||
push_data["key"] = key.toString();
|
||||
push_data["t"] = std::to_string(
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::system_clock::now().time_since_epoch())
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch())
|
||||
.count());
|
||||
lk.unlock();
|
||||
nodeClient.pushNotificationReceived(push_data).get();
|
||||
lk.lock();
|
||||
sendPush(nodeClient, push_data, lk);
|
||||
}
|
||||
|
||||
// Wait for the edit add (seq=3)
|
||||
@@ -1765,12 +1814,9 @@ DhtProxyTester::testPushRefreshAfterEditKeepsLatest()
|
||||
std::map<std::string, std::string> push_data;
|
||||
push_data["key"] = key.toString();
|
||||
push_data["t"] = std::to_string(
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::system_clock::now().time_since_epoch())
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch())
|
||||
.count());
|
||||
lk.unlock();
|
||||
nodeClient.pushNotificationReceived(push_data).get();
|
||||
lk.lock();
|
||||
sendPush(nodeClient, push_data, lk);
|
||||
}
|
||||
|
||||
// Wait a bit for any callbacks
|
||||
@@ -1809,4 +1855,141 @@ DhtProxyTester::testPushRefreshAfterEditKeepsLatest()
|
||||
#endif
|
||||
}
|
||||
|
||||
void
|
||||
DhtProxyTester::testPushPlatformFollowsResubscribe()
|
||||
{
|
||||
#ifndef OPENDHT_PUSH_NOTIFICATIONS
|
||||
fmt::print(stderr, "Push notifications are not supported in this build, skipping test\n");
|
||||
return;
|
||||
#else
|
||||
uint16_t pushPort = 1024 + (std::rand() % (65535 - 1024));
|
||||
uint16_t proxyPort = 1024 + (std::rand() % (65535 - 1024));
|
||||
|
||||
std::mutex cv_m;
|
||||
std::condition_variable cv;
|
||||
std::map<std::string, int> pushedPlatform; // value id -> gorush platform of its push
|
||||
PushGateway pushGateway(pushPort, [&](const Json::Value& notification) {
|
||||
std::lock_guard lk(cv_m);
|
||||
std::istringstream ids(notification["data"]["ids"].asString());
|
||||
for (std::string id; std::getline(ids, id, ',');)
|
||||
pushedPlatform.emplace(id, notification["platform"].asInt());
|
||||
cv.notify_all();
|
||||
});
|
||||
|
||||
serverProxy.reset();
|
||||
dht::ProxyServerConfig serverConfig;
|
||||
serverConfig.port = proxyPort;
|
||||
serverConfig.pushServer = "127.0.0.1:" + std::to_string(pushPort);
|
||||
serverProxy = std::make_unique<dht::DhtProxyServer>(nodeProxy, serverConfig);
|
||||
|
||||
auto key = dht::InfoHash::get("push-platform-resubscribe");
|
||||
auto putValue = [&](dht::Value::Id id) {
|
||||
dht::Value value {"push-platform"};
|
||||
value.id = id;
|
||||
bool stored = false;
|
||||
nodePeer.put(key, std::move(value), [&](bool ok) {
|
||||
std::lock_guard lk(cv_m);
|
||||
stored = ok;
|
||||
cv.notify_all();
|
||||
});
|
||||
std::unique_lock lk(cv_m);
|
||||
CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] { return stored; }));
|
||||
};
|
||||
auto waitPushPlatform = [&](dht::Value::Id id) {
|
||||
auto sid = std::to_string(id);
|
||||
std::unique_lock lk(cv_m);
|
||||
CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] { return pushedPlatform.count(sid) != 0; }));
|
||||
return pushedPlatform[sid];
|
||||
};
|
||||
constexpr dht::Value::Id FIRST_ID = 1111111;
|
||||
constexpr dht::Value::Id SECOND_ID = 2222222;
|
||||
putValue(FIRST_ID);
|
||||
|
||||
clientConfig.proxy_server = "http://127.0.0.1:" + std::to_string(proxyPort);
|
||||
clientConfig.push_token = "platform-token";
|
||||
clientConfig.push_node_id = "platform-device";
|
||||
|
||||
// The device first subscribes with a wrong push platform...
|
||||
clientConfig.push_platform = "ios";
|
||||
nodeClient.run(0, clientConfig);
|
||||
nodeClient.listen(key, [](const std::vector<std::shared_ptr<dht::Value>>&, bool) { return true; });
|
||||
CPPUNIT_ASSERT_EQUAL(GORUSH_PLATFORM_IOS, waitPushPlatform(FIRST_ID));
|
||||
|
||||
// ...then refreshes the same subscription (same token and client id) as an Android device.
|
||||
clientConfig.push_platform = "android";
|
||||
dht::DhtRunner androidClient;
|
||||
androidClient.run(0, clientConfig);
|
||||
bool refreshed = false;
|
||||
androidClient.listen(key, [&](const std::vector<std::shared_ptr<dht::Value>>& values, bool expired) {
|
||||
std::lock_guard lk(cv_m);
|
||||
if (!expired && !values.empty()) {
|
||||
refreshed = true;
|
||||
cv.notify_all();
|
||||
}
|
||||
return true;
|
||||
});
|
||||
{
|
||||
std::unique_lock lk(cv_m);
|
||||
CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] { return refreshed; }));
|
||||
}
|
||||
|
||||
// New values must now be pushed to the device as an Android device.
|
||||
putValue(SECOND_ID);
|
||||
CPPUNIT_ASSERT_EQUAL(GORUSH_PLATFORM_ANDROID, waitPushPlatform(SECOND_ID));
|
||||
androidClient.join();
|
||||
#endif
|
||||
}
|
||||
|
||||
void
|
||||
DhtProxyTester::testPushPlatformSetAfterStart()
|
||||
{
|
||||
#ifndef OPENDHT_PUSH_NOTIFICATIONS
|
||||
fmt::print(stderr, "Push notifications are not supported in this build, skipping test\n");
|
||||
return;
|
||||
#else
|
||||
uint16_t pushPort = 1024 + (std::rand() % (65535 - 1024));
|
||||
uint16_t proxyPort = 1024 + (std::rand() % (65535 - 1024));
|
||||
|
||||
std::mutex cv_m;
|
||||
std::condition_variable cv;
|
||||
std::vector<int> platforms;
|
||||
PushGateway pushGateway(pushPort, [&](const Json::Value& notification) {
|
||||
std::lock_guard lk(cv_m);
|
||||
platforms.emplace_back(notification["platform"].asInt());
|
||||
cv.notify_all();
|
||||
});
|
||||
|
||||
serverProxy.reset();
|
||||
dht::ProxyServerConfig serverConfig;
|
||||
serverConfig.port = proxyPort;
|
||||
serverConfig.pushServer = "127.0.0.1:" + std::to_string(pushPort);
|
||||
serverProxy = std::make_unique<dht::DhtProxyServer>(nodeProxy, serverConfig);
|
||||
|
||||
auto key = dht::InfoHash::get("push-platform-set-after-start");
|
||||
bool stored = false;
|
||||
nodePeer.put(key, dht::Value {"value"}, [&](bool ok) {
|
||||
std::lock_guard lk(cv_m);
|
||||
stored = ok;
|
||||
cv.notify_all();
|
||||
});
|
||||
{
|
||||
std::unique_lock lk(cv_m);
|
||||
CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] { return stored; }));
|
||||
}
|
||||
|
||||
// The node starts before the push platform of the device is known.
|
||||
clientConfig.proxy_server = "http://127.0.0.1:" + std::to_string(proxyPort);
|
||||
clientConfig.push_token = "late-platform-token";
|
||||
clientConfig.push_node_id = "late-platform-device";
|
||||
clientConfig.push_platform = "";
|
||||
nodeClient.run(0, clientConfig);
|
||||
nodeClient.setPushNotificationPlatform("android");
|
||||
|
||||
nodeClient.listen(key, [](const std::vector<std::shared_ptr<dht::Value>>&, bool) { return true; });
|
||||
std::unique_lock lk(cv_m);
|
||||
CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] { return not platforms.empty(); }));
|
||||
CPPUNIT_ASSERT_EQUAL(GORUSH_PLATFORM_ANDROID, platforms.front());
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace test
|
||||
@@ -31,6 +31,8 @@ class DhtProxyTester : public CppUnit::TestFixture
|
||||
CPPUNIT_TEST(testProxyListenEditChain);
|
||||
CPPUNIT_TEST(testPushRapidEditsNoPhantom);
|
||||
CPPUNIT_TEST(testPushRefreshAfterEditKeepsLatest);
|
||||
CPPUNIT_TEST(testPushPlatformFollowsResubscribe);
|
||||
CPPUNIT_TEST(testPushPlatformSetAfterStart);
|
||||
CPPUNIT_TEST_SUITE_END();
|
||||
|
||||
public:
|
||||
@@ -88,6 +90,16 @@ public:
|
||||
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 {};
|
||||
|
||||
@@ -16,6 +16,58 @@ 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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -667,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
|
||||
@@ -24,6 +24,9 @@ class NetworkEngineTester : public CppUnit::TestFixture
|
||||
CPPUNIT_TEST(testListenConfirmationUpdatesSearchNodeToken);
|
||||
CPPUNIT_TEST(testListenReopensSocketAfterNodeExpiration);
|
||||
CPPUNIT_TEST(testUnauthorizedListenFlushClearsListenState);
|
||||
CPPUNIT_TEST(testGetRequestReuse);
|
||||
CPPUNIT_TEST(testFilteredProjectionDoesNotWaitForUnrelatedPeer);
|
||||
CPPUNIT_TEST(testFindNodeRequestReuse);
|
||||
#endif
|
||||
CPPUNIT_TEST_SUITE_END();
|
||||
|
||||
@@ -45,6 +48,9 @@ public:
|
||||
void testListenConfirmationUpdatesSearchNodeToken();
|
||||
void testListenReopensSocketAfterNodeExpiration();
|
||||
void testUnauthorizedListenFlushClearsListenState();
|
||||
void testGetRequestReuse();
|
||||
void testFilteredProjectionDoesNotWaitForUnrelatedPeer();
|
||||
void testFindNodeRequestReuse();
|
||||
#endif
|
||||
};
|
||||
|
||||
|
||||
@@ -210,6 +210,32 @@ OpCacheTester::testStaleExpireIgnored()
|
||||
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()
|
||||
{
|
||||
@@ -543,6 +569,29 @@ OpCacheTester::testSourceDropKeepsValue()
|
||||
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()
|
||||
{
|
||||
|
||||
@@ -16,6 +16,7 @@ class OpCacheTester : public CppUnit::TestFixture
|
||||
CPPUNIT_TEST(testEditChainMultiSource);
|
||||
CPPUNIT_TEST(testStaleAddIgnored);
|
||||
CPPUNIT_TEST(testStaleExpireIgnored);
|
||||
CPPUNIT_TEST(testSameSeqConflictExpireIgnored);
|
||||
CPPUNIT_TEST(testCallbacks);
|
||||
CPPUNIT_TEST(testFilters);
|
||||
CPPUNIT_TEST(testSyncStatus);
|
||||
@@ -26,6 +27,7 @@ class OpCacheTester : public CppUnit::TestFixture
|
||||
CPPUNIT_TEST(testGetCallbackReturnsFalse);
|
||||
CPPUNIT_TEST(testTimestampOrdering);
|
||||
CPPUNIT_TEST(testSourceDropKeepsValue);
|
||||
CPPUNIT_TEST(testSeqOnlyRefreshNotCounted);
|
||||
CPPUNIT_TEST(testHighChurnRefCountConsistency);
|
||||
CPPUNIT_TEST(testValueUpdateSingleSourcePhantom);
|
||||
CPPUNIT_TEST(testExpireAfterUpdate);
|
||||
@@ -43,6 +45,7 @@ public:
|
||||
void testEditChainMultiSource();
|
||||
void testStaleAddIgnored();
|
||||
void testStaleExpireIgnored();
|
||||
void testSameSeqConflictExpireIgnored();
|
||||
void testCallbacks();
|
||||
void testFilters();
|
||||
void testSyncStatus();
|
||||
@@ -53,6 +56,7 @@ public:
|
||||
void testGetCallbackReturnsFalse();
|
||||
void testTimestampOrdering();
|
||||
void testSourceDropKeepsValue();
|
||||
void testSeqOnlyRefreshNotCounted();
|
||||
void testHighChurnRefCountConsistency();
|
||||
void testValueUpdateSingleSourcePhantom();
|
||||
void testExpireAfterUpdate();
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -13,10 +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:
|
||||
@@ -36,10 +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
|
||||
@@ -353,6 +353,47 @@ ValueCacheTester::testSameSeqEditIgnored()
|
||||
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()
|
||||
{
|
||||
|
||||
@@ -20,6 +20,7 @@ class ValueCacheTester : public CppUnit::TestFixture
|
||||
CPPUNIT_TEST(testMaxValues);
|
||||
CPPUNIT_TEST(testUpdateTypeExpiration);
|
||||
CPPUNIT_TEST(testSameSeqEditIgnored);
|
||||
CPPUNIT_TEST(testSameContentHigherSeqIsRefresh);
|
||||
CPPUNIT_TEST(testShortExpirationRefreshLost);
|
||||
CPPUNIT_TEST(testShortExpirationHighChurn);
|
||||
CPPUNIT_TEST(testGracePeriodWhileSynced);
|
||||
@@ -40,6 +41,7 @@ public:
|
||||
void testMaxValues();
|
||||
void testUpdateTypeExpiration();
|
||||
void testSameSeqEditIgnored();
|
||||
void testSameContentHigherSeqIsRefresh();
|
||||
void testShortExpirationRefreshLost();
|
||||
void testShortExpirationHighChurn();
|
||||
void testGracePeriodWhileSynced();
|
||||
|
||||
Loaded 100 of 103 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user