Compare commits

..
Author SHA1 Message Date
Adrien Beraud 370f619ac4 python: support DESTDIR in pip install step
Use --root $DESTDIR when the environment variable is set so that
staged installs (e.g. 'make DESTDIR=/tmp/pkg install') place the
Python package under the staging root.

Also pass --no-build-isolation and --prefix to avoid rebuilding in an
isolated venv and to honour CMAKE_INSTALL_PREFIX.
2026-05-25 13:40:55 -04:00
149 changed files with 2063 additions and 24757 deletions

No files matched your search

-12
View File
@@ -1,12 +0,0 @@
.git
.vscode
.DS_Store
build
build-*
build_*
install
vcpkg_installed
rust/target
**/__pycache__
python/dist
python/*.egg-info
+10 -22
View File
@@ -2,12 +2,12 @@ name: C/C++ CI
env:
LLHTTP_VERSION: "v9.2.1"
RESTINIO_VERSION: "0.7.10"
RESTINIO_VERSION: "0.7.8.1"
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-cxx-dev libargon2-dev"
DEBIAN12_DEPS: "git libtool pkg-config wget cmake libncurses5-dev libreadline-dev nettle-dev libasio-dev libgnutls28-dev libuv1-dev python3-pip python3-dev python3-setuptools libcppunit-dev libjsoncpp-dev libfmt-dev libmsgpack-dev libargon2-dev"
on: [push, pull_request]
@@ -16,7 +16,7 @@ jobs:
name: Ubuntu/GCC Meson build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
- name: Install Dependencies
run: |
@@ -33,18 +33,6 @@ 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
@@ -52,7 +40,7 @@ jobs:
matrix:
cxx_standard: [17, 20, 23]
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
- name: Install Minimal Dependencies
run: |
@@ -77,12 +65,12 @@ jobs:
- name: Run Unit tests
run: ctest --output-on-failure --test-dir build
build-debian13:
name: Debian 13/GCC build
build-debian12:
name: Debian 12/GCC build
runs-on: ubuntu-latest
container:
image: debian:13
image: debian:12
volumes:
- .:/workspace
options: --workdir /workspace
@@ -90,12 +78,12 @@ jobs:
DEBIAN_FRONTEND: noninteractive
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
- name: Install Dependencies
run: |
apt-get update
apt-get install -y $DEBIAN13_DEPS
apt-get install -y $DEBIAN12_DEPS
- name: Build llhttp dependency
run: |
@@ -143,7 +131,7 @@ jobs:
matrix:
cxx_standard: [17, 20, 23]
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
- name: Install Homebrew Dependencies
run: brew install msgpack-cxx asio gnutls nettle readline fmt jsoncpp argon2 openssl cppunit
+4 -6
View File
@@ -6,7 +6,7 @@ jobs:
name: Clang Static Analysis
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
- name: deps
run: |
sudo apt update && \
@@ -23,13 +23,11 @@ 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/v${RESTINIO_VERSION}/restinio-${RESTINIO_VERSION}.tar.bz2"
ls -l && tar -xjf "restinio-${RESTINIO_VERSION}.tar.bz2"
cd "restinio-${RESTINIO_VERSION}/dev"
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
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 -1
View File
@@ -27,7 +27,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v7
uses: actions/checkout@v6
- name: Initialize CodeQL
uses: github/codeql-action/init@v4
-206
View File
@@ -1,206 +0,0 @@
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@v6
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@v6
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@v6
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
+44 -20
View File
@@ -4,10 +4,10 @@ permissions:
contents: read
on:
release:
types: [published]
push:
branches: ['**']
pull_request:
workflow_call:
workflow_dispatch:
jobs:
@@ -16,14 +16,14 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, ubuntu-24.04-arm, macos-15]
os: [ubuntu-latest, ubuntu-24.04-arm, macos-latest]
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
# Run locally with: pipx run cibuildwheel --platform linux python
- name: Build wheels
uses: pypa/cibuildwheel@v4.3.4
uses: pypa/cibuildwheel@v3.4.1
with:
package-dir: python
@@ -36,28 +36,36 @@ jobs:
name: Build wheels on Windows
runs-on: windows-latest
env:
VCPKG_VERSION: "2025.07.25"
VCPKG_INSTALLATION_ROOT: "C:\\vcpkg"
VCPKG_DISABLE_METRICS: "1"
VCPKG_DEFAULT_TRIPLET: x64-windows-release
VCPKG_INSTALLED_DIR: ${{ github.workspace }}\vcpkg_installed
VCPKG_BINARY_SOURCES: clear;files,${{ github.workspace }}/vcpkg-binary-cache,readwrite
VCPKG_DEFAULT_BINARY_CACHE: ${{ github.workspace }}\vcpkg_binary_cache
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
- name: Set up vcpkg
uses: lukka/run-vcpkg@v11
- name: Cache vcpkg installed packages
id: cache-vcpkg
uses: actions/cache@v5
with:
vcpkgJsonGlob: vcpkg.json
vcpkgConfigurationJsonGlob: vcpkg-configuration.json
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: Cache vcpkg binary packages
uses: actions/cache@v6
with:
path: ${{ github.workspace }}/vcpkg-binary-cache
key: ${{ runner.os }}-vcpkg-binary-${{ env.VCPKG_DEFAULT_TRIPLET }}-${{ hashFiles('vcpkg.json', 'vcpkg-configuration.json', 'ports/**') }}-${{ github.run_id }}-${{ github.run_attempt }}
restore-keys: |
${{ runner.os }}-vcpkg-binary-${{ env.VCPKG_DEFAULT_TRIPLET }}-${{ hashFiles('vcpkg.json', 'vcpkg-configuration.json', 'ports/**') }}-
${{ runner.os }}-vcpkg-binary-${{ env.VCPKG_DEFAULT_TRIPLET }}-
- name: Setup vcpkg and install dependencies
if: steps.cache-vcpkg.outputs.cache-hit != 'true'
run: |
New-Item -Path "${{ env.VCPKG_INSTALLED_DIR }}" -ItemType Directory -Force
New-Item -Path "${{ env.VCPKG_DEFAULT_BINARY_CACHE }}" -ItemType Directory -Force
vcpkg install `
--recurse `
--clean-after-build `
--triplet=${{ env.VCPKG_DEFAULT_TRIPLET }} `
--x-install-root=${{ env.VCPKG_INSTALLED_DIR }}
shell: pwsh
- name: Build wheels
run: |
@@ -65,7 +73,7 @@ jobs:
cmake -S . -B build `
-DCMAKE_BUILD_TYPE=Release `
-DVCPKG_TARGET_TRIPLET=${{ env.VCPKG_DEFAULT_TRIPLET }} `
"-DCMAKE_TOOLCHAIN_FILE=$env:VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake" `
-DCMAKE_TOOLCHAIN_FILE=${{ env.VCPKG_INSTALLATION_ROOT }}\scripts\buildsystems\vcpkg.cmake `
-DVCPKG_INSTALLED_DIR=${{ env.VCPKG_INSTALLED_DIR }} `
-DBUILD_TESTING=OFF `
-DOPENDHT_C=OFF `
@@ -82,3 +90,19 @@ 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 }}
+61 -223
View File
@@ -8,9 +8,7 @@ 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
@@ -18,19 +16,6 @@ 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
@@ -40,7 +25,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v7
uses: actions/checkout@v6
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
@@ -71,46 +56,6 @@ 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
@@ -121,7 +66,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v7
uses: actions/checkout@v6
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
@@ -153,47 +98,6 @@ 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
@@ -203,7 +107,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v7
uses: actions/checkout@v6
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
@@ -244,7 +148,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v7
uses: actions/checkout@v6
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
@@ -286,7 +190,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v7
uses: actions/checkout@v6
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
@@ -327,7 +231,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v7
uses: actions/checkout@v6
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
@@ -369,7 +273,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v7
uses: actions/checkout@v6
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
@@ -405,24 +309,20 @@ jobs:
name: Build Windows MSI Installer
runs-on: windows-latest
env:
VCPKG_VERSION: "2025.07.25"
VCPKG_INSTALLATION_ROOT: "C:\\vcpkg"
VCPKG_DISABLE_METRICS: "1"
VCPKG_DEFAULT_TRIPLET: x64-windows
VCPKG_INSTALLED_DIR: ${{ github.workspace }}\vcpkg_installed
VCPKG_BINARY_SOURCES: clear;files,${{ github.workspace }}/vcpkg-binary-cache,readwrite
VCPKG_DEFAULT_BINARY_CACHE: ${{ github.workspace }}\vcpkg_binary_cache
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v7
uses: actions/checkout@v6
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
@@ -432,14 +332,27 @@ jobs:
wix extension add --global WixToolset.UI.wixext/6.0.1
shell: pwsh
- name: Cache vcpkg binary packages
uses: actions/cache@v6
- name: Cache vcpkg installed packages
id: cache-vcpkg
uses: actions/cache@v5
with:
path: ${{ github.workspace }}/vcpkg-binary-cache
key: ${{ runner.os }}-vcpkg-binary-${{ env.VCPKG_DEFAULT_TRIPLET }}-${{ hashFiles('vcpkg.json', 'vcpkg-configuration.json', 'ports/**') }}-${{ github.run_id }}-${{ github.run_attempt }}
restore-keys: |
${{ runner.os }}-vcpkg-binary-${{ env.VCPKG_DEFAULT_TRIPLET }}-${{ hashFiles('vcpkg.json', 'vcpkg-configuration.json', 'ports/**') }}-
${{ runner.os }}-vcpkg-binary-${{ env.VCPKG_DEFAULT_TRIPLET }}-
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
- name: Configure CMake for MSI
shell: pwsh
@@ -447,7 +360,7 @@ jobs:
mkdir build -ErrorAction SilentlyContinue
cmake -B build `
-S . `
"-DCMAKE_TOOLCHAIN_FILE=$env:VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake" `
-DCMAKE_TOOLCHAIN_FILE=${{ env.VCPKG_INSTALLATION_ROOT }}/scripts/buildsystems/vcpkg.cmake `
-DVCPKG_TARGET_TRIPLET=x64-windows `
-DVCPKG_INSTALLED_DIR=${{ env.VCPKG_INSTALLED_DIR }} `
-DVCPKG_MANIFEST_FEATURES=test `
@@ -500,35 +413,47 @@ jobs:
name: Build Windows dhtnode
runs-on: windows-latest
env:
VCPKG_VERSION: "2025.07.25"
VCPKG_INSTALLATION_ROOT: "C:\\vcpkg"
VCPKG_DISABLE_METRICS: "1"
VCPKG_DEFAULT_TRIPLET: x64-windows-static-release
VCPKG_INSTALLED_DIR: ${{ github.workspace }}\vcpkg_installed
VCPKG_BINARY_SOURCES: clear;files,${{ github.workspace }}/vcpkg-binary-cache,readwrite
VCPKG_DEFAULT_BINARY_CACHE: ${{ github.workspace }}\vcpkg_binary_cache
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v7
uses: actions/checkout@v6
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 binary packages
uses: actions/cache@v6
- name: Cache vcpkg static installed packages
id: cache-vcpkg-static
uses: actions/cache@v5
with:
path: ${{ github.workspace }}/vcpkg-binary-cache
key: ${{ runner.os }}-vcpkg-binary-${{ env.VCPKG_DEFAULT_TRIPLET }}-${{ hashFiles('vcpkg.json', 'vcpkg-configuration.json', 'ports/**') }}-${{ github.run_id }}-${{ github.run_attempt }}
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') }}
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 }}-
${{ 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
- name: Configure CMake for Static Build
shell: pwsh
@@ -536,7 +461,7 @@ jobs:
mkdir build_static -ErrorAction SilentlyContinue
cmake -B build_static `
-S . `
"-DCMAKE_TOOLCHAIN_FILE=$env:VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake" `
-DCMAKE_TOOLCHAIN_FILE=${{ env.VCPKG_INSTALLATION_ROOT }}/scripts/buildsystems/vcpkg.cmake `
-DVCPKG_TARGET_TRIPLET=x64-windows-static-release `
-DVCPKG_INSTALLED_DIR=${{ env.VCPKG_INSTALLED_DIR }} `
-DVCPKG_MANIFEST_FEATURES=test `
@@ -568,13 +493,13 @@ jobs:
create-github-release:
name: Create GitHub Release
runs-on: ubuntu-latest
needs: [build-windows-msi, build-windows-dhtnode, nodejs-package]
needs: [build-windows-msi, build-windows-dhtnode]
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v7
uses: actions/checkout@v6
- name: Download Windows MSI artifact
uses: actions/download-artifact@v8
@@ -588,12 +513,6 @@ 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
@@ -602,84 +521,3 @@ jobs:
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@v6
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
+91 -75
View File
@@ -9,89 +9,113 @@ on:
branches: [ main, master ]
env:
VCPKG_VERSION: "2026.01.16"
VCPKG_INSTALLATION_ROOT: "C:\\vcpkg"
VCPKG_DISABLE_METRICS: "1"
VCPKG_INSTALLED_DIR: ${{ github.workspace }}\vcpkg_installed
VCPKG_BINARY_SOURCES: clear;files,${{ github.workspace }}/vcpkg-binary-cache,readwrite
VCPKG_DEFAULT_BINARY_CACHE: ${{ github.workspace }}\vcpkg_binary_cache
jobs:
prepare-dependencies:
name: Prepare Windows Dependencies
runs-on: windows-latest
env:
VCPKG_DEFAULT_TRIPLET: ${{ matrix.arch }}-windows
outputs:
vcpkg_cache_key: ${{ steps.generate_cache_key.outputs.key }}
strategy:
matrix:
arch: [x64]
steps:
- name: Checkout repository
uses: actions/checkout@v7
uses: actions/checkout@v6
- name: Set up MSVC
uses: microsoft/setup-msbuild@v3
- name: Cache vcpkg binary packages
uses: actions/cache@v6
with:
path: ${{ github.workspace }}/vcpkg-binary-cache
key: ${{ runner.os }}-vcpkg-binary-${{ env.VCPKG_DEFAULT_TRIPLET }}-${{ hashFiles('vcpkg.json', 'vcpkg-configuration.json', 'ports/**') }}-${{ github.run_id }}-${{ github.run_attempt }}
restore-keys: |
${{ runner.os }}-vcpkg-binary-${{ env.VCPKG_DEFAULT_TRIPLET }}-${{ hashFiles('vcpkg.json', 'vcpkg-configuration.json', 'ports/**') }}-
${{ runner.os }}-vcpkg-binary-${{ env.VCPKG_DEFAULT_TRIPLET }}-
- name: 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: Set up vcpkg and install dependencies
uses: lukka/run-vcpkg@v11
- name: Cache vcpkg installed packages
id: cache-vcpkg
uses: actions/cache@v5
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]`]
path: |
${{ env.VCPKG_INSTALLED_DIR }}
${{ env.VCPKG_DEFAULT_BINARY_CACHE }}
key: ${{ steps.generate_cache_key.outputs.key }}
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-
- 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
prepare-static-dependencies:
name: Prepare Windows Static Dependencies
runs-on: windows-latest
env:
VCPKG_DEFAULT_TRIPLET: ${{ matrix.arch }}-windows-static-release
outputs:
vcpkg_static_cache_key: ${{ steps.generate_static_cache_key.outputs.key }}
strategy:
matrix:
arch: [x64]
steps:
- name: Checkout repository
uses: actions/checkout@v7
uses: actions/checkout@v6
- name: Set up MSVC
uses: microsoft/setup-msbuild@v3
- name: Cache vcpkg binary packages
uses: actions/cache@v6
with:
path: ${{ github.workspace }}/vcpkg-binary-cache
key: ${{ runner.os }}-vcpkg-binary-${{ env.VCPKG_DEFAULT_TRIPLET }}-${{ hashFiles('vcpkg.json', 'vcpkg-configuration.json', 'ports/**') }}-${{ github.run_id }}-${{ github.run_attempt }}
restore-keys: |
${{ runner.os }}-vcpkg-binary-${{ env.VCPKG_DEFAULT_TRIPLET }}-${{ hashFiles('vcpkg.json', 'vcpkg-configuration.json', 'ports/**') }}-
${{ runner.os }}-vcpkg-binary-${{ env.VCPKG_DEFAULT_TRIPLET }}-
- name: 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: Set up vcpkg and install dependencies
uses: lukka/run-vcpkg@v11
- name: Cache vcpkg static installed packages
id: cache-vcpkg-static
uses: actions/cache@v5
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]`]
path: |
${{ env.VCPKG_INSTALLED_DIR }}
${{ env.VCPKG_DEFAULT_BINARY_CACHE }}
key: ${{ steps.generate_static_cache_key.outputs.key }}
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-
- 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
build-windows:
name: Windows MSVC Build
runs-on: windows-latest
needs: prepare-dependencies
env:
VCPKG_DEFAULT_TRIPLET: ${{ matrix.arch }}-windows
strategy:
matrix:
@@ -101,25 +125,22 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v7
- name: Set up vcpkg
uses: lukka/run-vcpkg@v11
with:
vcpkgJsonGlob: vcpkg.json
vcpkgConfigurationJsonGlob: vcpkg-configuration.json
uses: actions/checkout@v6
- name: Set up MSVC
uses: microsoft/setup-msbuild@v3
- name: Restore vcpkg binary packages
uses: actions/cache/restore@v6
- name: Restore vcpkg installed packages
id: cache-vcpkg-restore
uses: actions/cache/restore@v5
with:
path: ${{ github.workspace }}/vcpkg-binary-cache
key: ${{ runner.os }}-vcpkg-binary-${{ env.VCPKG_DEFAULT_TRIPLET }}-${{ hashFiles('vcpkg.json', 'vcpkg-configuration.json', 'ports/**') }}-${{ github.run_id }}-${{ github.run_attempt }}
path: |
${{ env.VCPKG_INSTALLED_DIR }}
${{ env.VCPKG_DEFAULT_BINARY_CACHE }}
key: ${{ needs.prepare-dependencies.outputs.vcpkg_cache_key }}
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 }}-
${{ runner.os }}-vcpkg-${{ env.VCPKG_VERSION }}-${{ matrix.arch }}-shared-${{ hashFiles('**/vcpkg.json', '**/vcpkg-configuration.json') }}
${{ runner.os }}-vcpkg-${{ env.VCPKG_VERSION }}-${{ matrix.arch }}-shared-
- name: Configure CMake
shell: pwsh
@@ -127,7 +148,7 @@ jobs:
mkdir build -ErrorAction SilentlyContinue
cmake -B build `
-S . `
"-DCMAKE_TOOLCHAIN_FILE=$env:VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake" `
-DCMAKE_TOOLCHAIN_FILE=${{ env.VCPKG_INSTALLATION_ROOT }}/scripts/buildsystems/vcpkg.cmake `
-DVCPKG_TARGET_TRIPLET=${{ matrix.arch }}-windows `
-DVCPKG_INSTALLED_DIR=${{ env.VCPKG_INSTALLED_DIR }} `
-DVCPKG_MANIFEST_FEATURES=test `
@@ -173,8 +194,6 @@ 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]
@@ -183,25 +202,22 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v7
- name: Set up vcpkg
uses: lukka/run-vcpkg@v11
with:
vcpkgJsonGlob: vcpkg.json
vcpkgConfigurationJsonGlob: vcpkg-configuration.json
uses: actions/checkout@v6
- name: Set up MSVC
uses: microsoft/setup-msbuild@v3
- name: Restore vcpkg binary packages
uses: actions/cache/restore@v6
- name: Restore vcpkg static installed packages
id: cache-vcpkg-static-restore
uses: actions/cache/restore@v5
with:
path: ${{ github.workspace }}/vcpkg-binary-cache
key: ${{ runner.os }}-vcpkg-binary-${{ env.VCPKG_DEFAULT_TRIPLET }}-${{ hashFiles('vcpkg.json', 'vcpkg-configuration.json', 'ports/**') }}-${{ github.run_id }}-${{ github.run_attempt }}
path: |
${{ env.VCPKG_INSTALLED_DIR }}
${{ env.VCPKG_DEFAULT_BINARY_CACHE }}
key: ${{ needs.prepare-static-dependencies.outputs.vcpkg_static_cache_key }}
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 }}-
${{ runner.os }}-vcpkg-${{ env.VCPKG_VERSION }}-${{ matrix.arch }}-static-${{ hashFiles('**/vcpkg.json', '**/vcpkg-configuration.json') }}
${{ runner.os }}-vcpkg-${{ env.VCPKG_VERSION }}-${{ matrix.arch }}-static-
- name: Configure CMake for Static Build
shell: pwsh
@@ -209,7 +225,7 @@ jobs:
mkdir build -ErrorAction SilentlyContinue
cmake -B build `
-S . `
"-DCMAKE_TOOLCHAIN_FILE=$env:VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake" `
-DCMAKE_TOOLCHAIN_FILE=${{ env.VCPKG_INSTALLATION_ROOT }}/scripts/buildsystems/vcpkg.cmake `
-DVCPKG_TARGET_TRIPLET=${{ matrix.arch }}-windows-static-release `
-DVCPKG_INSTALLED_DIR=${{ env.VCPKG_INSTALLED_DIR }} `
-DVCPKG_MANIFEST_FEATURES=test `
@@ -259,7 +275,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v7
uses: actions/checkout@v6
- name: Setup MinGW
uses: msys2/setup-msys2@v2
@@ -288,7 +304,7 @@ jobs:
shell: msys2 {0}
env:
LLHTTP_VERSION: "v9.2.1"
RESTINIO_VERSION: "0.7.10"
RESTINIO_VERSION: "0.7.6"
EXPECTED_LITE_URL: "https://raw.githubusercontent.com/martinmoene/expected-lite/master/include/nonstd/expected.hpp"
run: |
MSYS2_SYSROOT=$(cygpath -u "$MSYSTEM_PREFIX")
+2 -22
View File
@@ -115,36 +115,16 @@ 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.
See [the Rust build guide](rust/BUILD.md) for build instructions.
## 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) 2026.10.16 and required dependencies:
1. Install [VCPKG](https://github.com/microsoft/vcpkg) and required dependencies:
```ps1
git clone https://github.com/microsoft/vcpkg.git
cd vcpkg
git checkout 9eec002df1cd109f8f104cd966a763daf9751489
.\bootstrap-vcpkg.bat
cd vcpkg; .\bootstrap-vcpkg.bat
```
2. Use CMake preset for vcpkg:
+2 -28
View File
@@ -1,4 +1,4 @@
cmake_minimum_required (VERSION 3.21...4.4)
cmake_minimum_required (VERSION 3.21...3.31)
if(POLICY CMP0091)
cmake_policy(SET CMP0091 NEW)
endif()
@@ -10,7 +10,7 @@ if(POLICY CMP0167)
endif()
set (opendht_VERSION_MAJOR 4)
set (opendht_VERSION_MINOR 4.99)
set (opendht_VERSION_MINOR 0.0)
set (opendht_VERSION ${opendht_VERSION_MAJOR}.${opendht_VERSION_MINOR})
set (PACKAGE_VERSION ${opendht_VERSION})
set (VERSION "${opendht_VERSION}")
@@ -117,7 +117,6 @@ if (NOT MSVC)
find_package (PkgConfig REQUIRED)
pkg_search_module (GnuTLS REQUIRED IMPORTED_TARGET gnutls)
pkg_search_module (Nettle REQUIRED IMPORTED_TARGET nettle)
pkg_search_module (Hogweed REQUIRED IMPORTED_TARGET hogweed>=3.1)
pkg_search_module (argon2 REQUIRED IMPORTED_TARGET libargon2)
set(argon2_lib ", libargon2")
pkg_search_module (Jsoncpp IMPORTED_TARGET jsoncpp)
@@ -468,7 +467,6 @@ list (APPEND opendht_SOURCES
src/routing_table.cpp
src/node_cache.cpp
src/network_engine.cpp
src/transport.cpp
src/securedht.cpp
src/dhtrunner.cpp
src/log.cpp
@@ -493,7 +491,6 @@ list (APPEND opendht_HEADERS
include/opendht/routing_table.h
include/opendht/node_cache.h
include/opendht/network_engine.h
include/opendht/transport.h
include/opendht/scheduler.h
include/opendht/rate_limiter.h
include/opendht/securedht.h
@@ -616,7 +613,6 @@ else()
PRIVATE
PkgConfig::argon2
PkgConfig::Nettle
PkgConfig::Hogweed
${SIMDUTF_LIBRARIES}
PUBLIC
${CMAKE_THREAD_LIBS_INIT}
@@ -641,7 +637,6 @@ else()
PRIVATE
argon2
nettle
hogweed
PUBLIC
${CMAKE_THREAD_LIBS_INIT}
${GNUTLS_LIBRARIES}
@@ -850,15 +845,6 @@ 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)
@@ -874,8 +860,6 @@ if (BUILD_TESTING)
tests/test_value.cpp
tests/test_crypto.h
tests/test_crypto.cpp
tests/test_transport.h
tests/test_transport.cpp
tests/test_dhtrunner.h
tests/test_dhtrunner.cpp
tests/test_storage.h
@@ -886,17 +870,7 @@ if (BUILD_TESTING)
tests/test_parsedmessage.cpp
tests/test_networkengine.h
tests/test_networkengine.cpp
tests/test_opcache.h
tests/test_opcache.cpp
tests/test_valuecache.h
tests/test_valuecache.cpp
)
if (OPENDHT_INDEX)
list (APPEND test_FILES
tests/test_pht.h
tests/test_pht.cpp
)
endif()
if (OPENDHT_PROXY_CLIENT)
list (APPEND test_FILES
tests/test_dhtproxy_client.h
+2 -5
View File
@@ -4,10 +4,7 @@
<a id="user-content-opendht-" class="anchor" href="/savoirfairelinux/opendht/blob/master/README.md#opendht-" aria-hidden="true"></a>OpenDHT
</h1>
[![PyPI - Version](https://img.shields.io/pypi/v/opendht?style=flat)](https://pypi.org/project/opendht/)
[![crates.io - Version](https://img.shields.io/crates/v/opendht?style=flat)](https://crates.io/crates/opendht)
[![npm - Version](https://img.shields.io/npm/v/opendht?style=flat)](https://www.npmjs.com/package/opendht)
[![Docker Image](https://img.shields.io/badge/Docker-ghcr.io-2496ED?style=flat&logo=docker&logoColor=white)](https://github.com/savoirfairelinux/opendht/pkgs/container/opendht%2Fopendht)
![PyPI - Version](https://img.shields.io/pypi/v/opendht?style=flat)
A lightweight C++17 Distributed Hash Table implementation.
@@ -20,7 +17,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, Node.js & Python 3**
* Bindings for **C, Rust & Python 3**
* REST API with optional HTTP client+server with push notification support
## Documentation
+99 -573
View File
@@ -5,35 +5,12 @@
#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
@@ -179,47 +156,6 @@ 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)
{
@@ -247,29 +183,6 @@ 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)
{
@@ -283,7 +196,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 (...) {
} catch (const dht::crypto::CryptoException& e) {
return nullptr;
}
}
@@ -301,29 +214,6 @@ 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)
{
@@ -365,12 +255,8 @@ dht_privatekey_generate(unsigned key_length_bits)
{
if (key_length_bits == 0)
key_length_bits = 4096;
try {
return reinterpret_cast<dht_privatekey*>(new PrivkeySp(
std::make_shared<dht::crypto::PrivateKey>(dht::crypto::PrivateKey::generate(key_length_bits))));
} catch (...) {
return nullptr;
}
return reinterpret_cast<dht_privatekey*>(
new PrivkeySp(std::make_shared<dht::crypto::PrivateKey>(dht::crypto::PrivateKey::generate(key_length_bits))));
}
dht_privatekey*
@@ -379,7 +265,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 (...) {
} catch (const dht::crypto::CryptoException& e) {
return nullptr;
}
}
@@ -390,43 +276,7 @@ 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);
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;
}
return key->serialize((uint8_t*) out, out_size, password);
}
dht_publickey*
@@ -461,28 +311,11 @@ 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 (...) {
} catch (const dht::crypto::CryptoException& e) {
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)
{
@@ -531,10 +364,8 @@ inline dht_identity
dht_identity_to_c(const dht::crypto::Identity& id)
{
dht_identity cid {};
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());
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;
return cid;
}
@@ -545,12 +376,7 @@ extern "C" {
OPENDHT_C_PUBLIC dht_identity
dht_identity_generate(const char* common_name, const dht_identity* ca)
{
try {
return dht_identity_to_c(
dht::crypto::generateIdentity(common_name ? common_name : "dhtnode", dht_identity_from_c(ca)));
} catch (...) {
return {};
}
return dht_identity_to_c(dht::crypto::generateIdentity(common_name, dht_identity_from_c(ca)));
}
OPENDHT_C_PUBLIC void
@@ -566,17 +392,6 @@ 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)
@@ -598,28 +413,6 @@ 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)
{
@@ -631,163 +424,61 @@ 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* 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)
dht_runner_run_config(dht_runner* r, in_port_t port, const dht_runner_config* conf)
{
auto runner = reinterpret_cast<dht::DhtRunner*>(r);
try {
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::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);
dht::DhtRunner::Context context;
if (conf->log) {
context.logger = dht::log::getStdLogger();
}
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));
runner->run(port, 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);
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);
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));
}
}
@@ -803,29 +494,18 @@ 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);
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);
}
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);
});
}
struct ScopeGuardCb
@@ -848,38 +528,23 @@ 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 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;
}
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;
}
void
@@ -897,73 +562,6 @@ 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)
@@ -971,12 +569,15 @@ 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);
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);
}
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);
}
void
@@ -986,12 +587,14 @@ 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);
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);
}
runner->putSigned(
*hash,
*value,
[done_cb, cb_user_data](bool ok) {
if (done_cb)
done_cb(ok, cb_user_data);
},
permanent);
}
void
@@ -1007,12 +610,15 @@ 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);
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);
}
runner->putEncrypted(
*hash,
*toHash,
*value,
[done_cb, cb_user_data](bool ok) {
if (done_cb)
done_cb(ok, cb_user_data);
},
permanent);
}
void
@@ -1027,8 +633,10 @@ void
dht_runner_shutdown(dht_runner* r, dht_shutdown_cb done_cb, void* cb_user_data)
{
auto runner = reinterpret_cast<dht::DhtRunner*>(r);
auto guard = std::make_shared<ScopeGuardCb>(done_cb, cb_user_data);
runner->shutdown([guard]() {});
runner->shutdown([done_cb, cb_user_data]() {
if (done_cb)
done_cb(cb_user_data);
});
}
bool
@@ -1073,94 +681,12 @@ 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
+2 -104
View File
@@ -39,17 +39,9 @@ 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
{
OPENDHT_C_ALIGN_UINT32 uint8_t d[HASH_LEN];
uint8_t d[HASH_LEN];
};
typedef struct dht_infohash dht_infohash;
OPENDHT_C_PUBLIC void dht_infohash_zero(dht_infohash* h);
@@ -64,10 +56,9 @@ OPENDHT_C_PUBLIC bool dht_infohash_is_zero(const dht_infohash* h);
// dht::PkId
struct OPENDHT_C_PUBLIC dht_pkid
{
OPENDHT_C_ALIGN_UINT32 uint8_t d[32];
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
@@ -76,8 +67,6 @@ 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(
@@ -88,11 +77,8 @@ 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*);
@@ -101,8 +87,6 @@ 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*);
@@ -115,7 +99,6 @@ 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
@@ -126,7 +109,6 @@ 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);
@@ -134,53 +116,12 @@ 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;
@@ -228,50 +169,17 @@ 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,
@@ -300,16 +208,6 @@ 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
}
+1 -59
View File
@@ -27,19 +27,6 @@
[\fB\-y\fR \fIendpoint\fR]
[\fB\-z\fR \fIkey\fR]
[\fB\-u\fR \fIbundleid\fR]
[\fB\-\-no\-encryption\fR]
[\fB\-\-transport\-key\fR \fIfile\fR]
[\fB\-\-network\-authority\fR \fIkey\fR]
[\fB\-\-network\-credential\fR \fIfile\fR]
.br
.B dhtnode
\fB\-\-gen\-network\-authority\fR \fIprefix\fR
.br
.B dhtnode
\fB\-\-issue\-credential\fR \fIfile\fR
\fB\-\-authority\-key\fR \fIfile\fR
\fB\-\-transport\-key\fR \fIfile\fR
[\fB\-\-validity\fR \fIdays\fR]
.SH DESCRIPTION
Runs an OpenDHT node, with an interactive CLI (default) or as a daemon (with \fB'-d'\fP or \fB'-s'\fP).
Commands available in the interactive shell are:
@@ -135,9 +122,7 @@ Disable rate limiting (allows unlimited requests and storage).
Enable public stable mode.
.TP
\fB\-f\fP \fIpath\fP, \fB\-\-persist\fP \fIpath\fP
Specify a file path to persist/load the node state (routing table, values and
generated transport key). State is written to a temporary file and renamed
over the destination only after the write succeeds. Save failures are logged.
Specify a file path to persist/load the node state (routing table and values).
.TP
\fB\-n\fP \fInetwork_id\fP, \fB\-\-net\fP \fInetwork_id\fP
Specify the network id. This lets you connect to distinct networks and prevents
@@ -179,40 +164,6 @@ Device key for push notifications.
.TP
\fB\-u\fP \fIbundleid\fP, \fB\-\-bundleid\fP \fIbundleid\fP
Bundle ID for push notifications.
.SH TRANSPORT ENCRYPTION
The DHT protocol is encrypted with nodes supporting it, and stays in plaintext
with older nodes. The node ID derives from the transport key.
.TP
\fB\-\-no\-encryption\fP
Disable protocol encryption, using a random node ID like older versions.
.TP
\fB\-\-transport\-key\fP \fIfile\fP
Load the transport key from \fIfile\fP, or create it if it doesn't exist.
Keeps the same node ID across restarts. Otherwise, a key is generated and kept
in the node state if \fB\-\-persist\fP is used, or generated on each start.
.TP
\fB\-\-network\-authority\fP \fIkey\fP
Join a private network: only accept nodes with a credential issued by the
authority with public key \fIkey\fP (hexadecimal). Can be repeated.
Requires \fB\-\-network\-credential\fP and \fB\-\-transport\-key\fP.
.TP
\fB\-\-network\-credential\fP \fIfile\fP
Credential of our transport key, issued by a network authority.
.TP
\fB\-\-gen\-network\-authority\fP \fIprefix\fP
Generate a network authority, save its secret key to \fIprefix\fP.key and its
public key to \fIprefix\fP.pub, and exit. Refuses to overwrite either existing
output path, including a dangling symlink.
.TP
\fB\-\-issue\-credential\fP \fIfile\fP
Issue a credential for the key in \fB\-\-transport\-key\fP (created if needed),
signed with \fB\-\-authority\-key\fP, save it to \fIfile\fP and exit.
.TP
\fB\-\-authority\-key\fP \fIfile\fP
Network authority secret key used to issue credentials.
.TP
\fB\-\-validity\fP \fIdays\fP
Validity of issued credentials (default: 365 days).
.SH EXAMPLES
Run a node using bootstrap.jami.net as a bootstrap:
.PP
@@ -237,15 +188,6 @@ Use a proxy client to connect to a proxy server:
.EX
dhtnode --proxyclient dhtproxy.jami.net:80
.EE
.PP
Create a private network and run one of its nodes:
.PP
.EX
dhtnode --gen-network-authority net
dhtnode --issue-credential node1.cred --authority-key net.key --transport-key node1.key
dhtnode --transport-key node1.key --network-credential node1.cred \\
--network-authority $(cat net.pub) -b 192.0.2.1:4222
.EE
.SH AUTHORS
.TP
Program written by
+11 -12
View File
@@ -5,16 +5,15 @@ LABEL org.opencontainers.image.source=https://github.com/savoirfairelinux/opendh
COPY . opendht
RUN cd opendht && mkdir build && cd build \
&& 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 \
&& 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 \
&& cd ../.. && rm -rf opendht
+1 -2
View File
@@ -10,7 +10,6 @@ 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 \
@@ -21,7 +20,7 @@ RUN cd opendht && mkdir build && cd build \
-DOPENDHT_SYSTEMD=On \
&& make -j8 && make install
FROM alpine:3.24 AS install
FROM alpine:3.23 AS install
COPY --from=build /install /
RUN apk add --no-cache \
libstdc++ \
+6 -8
View File
@@ -1,4 +1,4 @@
FROM ubuntu:26.04
FROM ubuntu:24.04
LABEL maintainer="Adrien Béraud <adrien.beraud@savoirfairelinux.com>"
LABEL org.opencontainers.image.source=https://github.com/savoirfairelinux/opendht
@@ -8,25 +8,23 @@ 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 ninja-build \
build-essential pkg-config cmake git wget meson \
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 \
libllhttp-dev libsimdutf-dev \
libssl-dev libfmt-dev libasio-dev libmsgpack-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/v${RESTINIO_VERSION}/restinio-${RESTINIO_VERSION}.tar.bz2" \
&& ls -l && tar -xjf "restinio-${RESTINIO_VERSION}.tar.bz2" \
&& cd "restinio-${RESTINIO_VERSION}/dev" \
&& 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 \
&& 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 . \
+5 -6
View File
@@ -1,4 +1,4 @@
FROM alpine:3.24
FROM alpine:3.23
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 simdutf-dev \
argon2-dev openssl-dev fmt-dev \
llhttp-dev asio-dev msgpack-cxx-dev \
&& rm -rf /var/cache/apk/*
@@ -15,12 +15,11 @@ 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" \
&& 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 \
&& 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 . \
+3 -4
View File
@@ -15,12 +15,11 @@ 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" \
&& 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 \
&& 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 . \
+3 -17
View File
@@ -18,29 +18,15 @@ 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${RESTINIO_VERSION}/restinio-${RESTINIO_VERSION}.tar.bz2" \
&& ls -l && tar -xjf "restinio-${RESTINIO_VERSION}.tar.bz2" \
&& cd "restinio-${RESTINIO_VERSION}/dev" \
&& 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 \
&& 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 . \
+6 -7
View File
@@ -1,4 +1,4 @@
FROM ubuntu:26.04
FROM ubuntu:24.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-cxx-dev libjsoncpp-dev python3-dev \
nettle-dev libgnutls28-dev libuv1-dev libmsgpack-dev libjsoncpp-dev python3-dev \
python3-setuptools libcppunit-dev python3-pip python3-build python3-virtualenv \
libssl-dev libargon2-dev libllhttp-dev libsimdutf-dev \
libssl-dev libargon2-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,12 +26,11 @@ 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" \
&& 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 \
&& 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 . \
-56
View File
@@ -1,56 +0,0 @@
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
-1
View File
@@ -8,7 +8,6 @@ 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 \
-1
View File
@@ -7,7 +7,6 @@ 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 \
-20
View File
@@ -1,20 +0,0 @@
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
-564
View File
@@ -1,564 +0,0 @@
# Transport encryption: cryptographic design
This document specifies how OpenDHT encrypts and authenticates the UDP DHT protocol between nodes (the "transport"), and how private networks restrict participation with a small public-key infrastructure.
Implementation: [include/opendht/transport.h](../include/opendht/transport.h), [src/transport.cpp](../src/transport.cpp), and the send/receive paths in [src/network_engine.cpp](../src/network_engine.cpp).
## 1. Scope
### Goals
- **Confidentiality and integrity** of DHT protocol messages exchanged between two nodes that both support encryption.
- **Authenticated node IDs**: a node can only send encrypted messages under the node ID that derives from its own key.
- **Backward compatibility**: nodes without this feature keep working with new nodes, in plaintext.
- **Optional private networks**: when configured, a node only talks to nodes holding a credential signed by a trusted authority.
- **Permanent membership**: grants do not expire. Without a compromise or an intentional membership change, no grant renewal, authority rotation, or operator maintenance is required.
- **Forward secrecy**: recorded traffic stays confidential after a node's long-term key is compromised, once the ephemeral keys used for it are erased (at most one hour later). The first packet to a peer has weaker guarantees ([§5.2](#52-key-schedule)).
- **Address validation**: a Session or Data envelope proves that its sender receives packets at its source address. It replaces the legacy write tokens between encrypted nodes ([§6.6](#66-write-authorization)).
- **Low cost**: routing discovery also establishes the session, with a few X25519 operations per peer every 30 minutes. Once a session is established, 30 bytes of overhead per packet, minus the 40-byte token in messages that carried one.
### Non-goals
- **End-to-end protection of values.** Encryption is hop-by-hop. Nodes storing a value see it in clear. Use `SecureDht` (signed or encrypted values) for end-to-end protection. `SecureDht` identities are unrelated to transport keys.
- **Hiding metadata**: packet sizes, timing and addresses are visible, and so are the sender's public keys until a session is established.
- The REST proxy (`DhtProxyServer`/`DhtProxyClient`) and local peer discovery are not covered.
## 2. Notation
| Notation | Meaning |
|---|---|
| `a ‖ b` | Concatenation of byte strings |
| `"text"` | ASCII bytes, without terminator |
| `u32be(x)` | Unsigned integer, big-endian, 4 bytes |
| `sk_A`, `pk_A` | Static transport secret and public key of node A |
| `esk_A`, `epk_A` | Ephemeral secret and public key of node A for the address of its peer |
| `DH(x, Y)` | `X25519(x, Y)` |
| `N` | Network ID (`Config::network`, 32-bit, `0` by default) |
| `K_A→B` | Key encrypting envelopes from A to B |
All keys are 32 bytes. Integers on the wire are big-endian.
## 3. Primitives
| Purpose | Primitive | Reference |
|---|---|---|
| Key agreement | X25519 | RFC 7748 |
| Key derivation | HKDF-SHA256 | RFC 5869 |
| Packet encryption | ChaCha20-Poly1305 AEAD, 96-bit nonce, 128-bit tag | RFC 8439 |
| Node ID | SHA-256, truncated to 160 bits | FIPS 180-4 |
| Credential signature | Ed25519 | RFC 8032 |
| Randomness | `gnutls_rnd` (`GNUTLS_RND_KEY` for keys, `GNUTLS_RND_NONCE` for nonces) | |
Primitives come from Nettle (libnettle and libhogweed) and GnuTLS, which OpenDHT already depends on. All primitives are used through their standard interfaces with no modification. The authentication tag is compared in constant time. Secret keys are erased with `gnutls_memset`.
## 4. Keys and identities
### 4.1 Transport key
Each node has a static X25519 key pair, its long-term identity:
```
sk = 32 random bytes
pk = X25519(sk, 9)
```
The key is either given in `Config::transport.key`, or generated on start. A generated key is saved in the node state when `Config::persist_path` is set (see [§8](#8-key-persistence)). Otherwise, it changes on every restart, and so does the node ID.
The transport key is used only for the transport. It is independent of the `SecureDht` identity (an RSA certificate).
### 4.2 Node ID
When transport encryption is enabled, the node ID is derived from the public key:
```
nodeId(pk) = SHA-256("opendht-node-id-v1" ‖ pk)[0..20)
```
`Config::node_id` is ignored in that case. This binding is what authenticates node IDs: a receiver checks that the ID claimed inside a decrypted message equals `nodeId(pk)` of the static key the envelope was authenticated with (see [§6.5](#65-receiving)). Holding the ID therefore requires holding `sk`.
When encryption is disabled (`TransportConfig::enabled = false`), the node uses the legacy behavior: a random ID, or `Config::node_id`.
### 4.3 Network authority (private networks)
An authority is an Ed25519 key pair. Its secret is the 32-byte Ed25519 seed. Its public key identifies the private network, and nodes are configured with a list of trusted authority public keys (`TransportConfig::authorities`). A network is private if and only if this list is non-empty.
Any one trusted authority can grant membership. A node can trust multiple authorities simultaneously, allowing old and replacement grants to coexist during an explicit transition. There is no scheduled authority rotation.
### 4.4 Credential (private networks)
A membership grant, called a credential in the API and CLI, is an authority's signature over a transport public key. It never expires and contains no timestamp. It is 97 bytes:
```
offset size field
0 1 version = 0x02
1 32 pk transport public key of the node
33 64 signature Ed25519 signature by the authority
```
The signature covers:
```
"opendht-credential-v2" ‖ 0x02 ‖ pk
```
A credential is **valid** when its signature verifies under at least one trusted authority. Admission does not depend on a system clock. The same grant can be reused across restarts and session-key rotations without an update.
Version 1 expiring credentials are rejected, not reinterpreted as permanent grants. Migrating from that format requires issuing a version 2 grant once.
Credentials are not bound to a network ID. An authority key therefore defines a network: use distinct authorities for distinct networks.
## 5. Key agreement
### 5.1 Ephemeral keys
Each node has an epoch secret `S`: 32 random bytes, replaced every 30 minutes (an *epoch*). Its ephemeral key for a peer at address `addr` derives from it:
```
addr = IP (4 bytes for IPv4, including IPv4-mapped IPv6, else 16 bytes) ‖ port (2 bytes, network order)
esk(addr) = HMAC-SHA256(key = S, "opendht-address-key-v1" ‖ addr)
epk(addr) = X25519(esk(addr), 9)
```
- The node always sends to `addr` with `epk(addr)` from its **current** epoch.
- It still accepts envelopes addressed to its key from the **previous** epoch, so peers have one epoch to learn the new key.
- On rotation, the previous epoch secret and every key derived from it are erased.
An ephemeral secret therefore lives at most one hour. Epoch secrets are never persisted, and a restart starts with a new one.
Nodes learn each other's ephemeral key from the header of every envelope ([§6.1](#61-envelope)). A node records a peer's ephemeral key only from an envelope it authenticated.
`epk(addr)` is only ever sent to `addr`. Learning it therefore requires receiving packets at `addr`, or observing the path to it. This is what makes a Session envelope a proof of address ([§6.6](#66-write-authorization)). Deriving keys per address costs one more X25519 base-point multiplication per new address, and nothing per packet.
### 5.2 Key schedule
Each envelope from a sender A to a receiver B is encrypted with a key `K_A→B`, derived with HKDF-SHA256 (RFC 5869):
```
prk = HMAC-SHA256(key = "opendht-transport-v2", ikm)
K_A→B = HMAC-SHA256(key = prk, info ‖ 0x01)
```
The input keying material `ikm` and `info` depend on what A knows about B.
**Session**: A knows B's ephemeral key `epk_B`:
```
ikm = DH(esk_A, epk_B) ‖ DH(esk_A, pk_B) ‖ DH(sk_A, epk_B) ‖ DH(sk_A, pk_B)
info = u32be(N) ‖ 0x02 ‖ pk_A ‖ epk_A ‖ pk_B ‖ epk_B
```
**Initial**: A doesn't know B's ephemeral key, typically on first contact:
```
ikm = DH(esk_A, pk_B) ‖ DH(sk_A, pk_B)
info = u32be(N) ‖ 0x01 ‖ pk_A ‖ epk_A ‖ pk_B
```
B computes the same values with its own secrets, since `DH(x_A, Y_B) = DH(y_B, X_A)`. Any DH result equal to zero (a low-order public key) is rejected.
Properties:
- **Directional keys.** `info` orders the keys as sender, then receiver, so `K_A→B ≠ K_B→A`. Each node derives both from the same four DH values.
- **Authentication.** Every key includes `DH(sk_A, pk_B)`, so only the holders of `sk_A` or `sk_B` can produce or read the envelope. Every envelope thus proves possession of the sender's static key.
- **Forward secrecy.** A Session key includes `DH(esk_A, epk_B)`. Once both ephemeral secrets are erased, the key cannot be recomputed, even with both static secrets.
- **Key-compromise impersonation resistance.** With `sk_B`, an attacker still cannot impersonate A to B in a Session envelope: it cannot compute `DH(sk_A, epk_B)` without `sk_A` or `esk_B`.
- **Initial keys are weaker**, as for the first message of Noise IK. Without `epk_B`, an Initial key is only forward-secret against a later compromise of `sk_A`, not of `sk_B`. It also allows impersonating A to B with `sk_B`. Initial envelopes are only sent until A learns `epk_B`, usually for a single packet.
- **Binding.** `info` includes `N`, the envelope type and all public keys involved. Nodes with different network IDs cannot decrypt each other's packets.
- **Cost.** A Session key costs four X25519 operations, plus one for our ephemeral key. It is cached in the epoch, so it is derived once per peer address and ephemeral key pair, once the peer has proven to receive at that address ([§6.5](#65-receiving)). An Initial key costs two X25519 operations, plus one for our ephemeral key, and is not cached. Each epoch caches at most 16384 sessions, evicting the least recently used; beyond that, keys are derived for each packet.
## 6. Wire protocol
### 6.1 Envelope
Initial and Session envelopes carry the sender keys:
```
offset size field
0 1 marker = 0xC1
1 1 type: 1 = Initial, 2 = Session, 3 = Hello; | 0x80 if a credential is included
2 32 pk_sender
34 32 epk_sender
66 12 nonce (random)
78 len ciphertext
78+len 16 Poly1305 tag
```
Once the session is confirmed ([§6.4](#64-sending)), Data envelopes leave them out. The receiver finds the session keys from the source address:
```
offset size field
0 1 marker = 0xC1
1 1 type = 4 (Data), never with a credential
2 12 nonce (random)
14 len ciphertext
14+len 16 Poly1305 tag
```
- The ciphertext is `ChaCha20-Poly1305(K_sender→receiver, nonce, plaintext, aad = header)`.
- The header is bytes `[0, 66)`, or `[0, 2)` for Data: marker, type, and the sender keys. Because it is the associated data, an attacker cannot change the type, the credential flag or the sender keys without the tag check failing.
- The plaintext is the msgpack-encoded DHT message. With the credential flag, it is preceded by the sender's 97-byte credential.
- The overhead is 30 bytes for Data, 94 bytes for Initial and Session, or 191 bytes with a credential.
- Nonces are 96 bits from the system CSPRNG. For `n` packets under one key, the probability of a nonce collision is about $n^2 / 2^{97}$. Keys change at least every epoch, so this is negligible.
`0xC1` is the one byte value that msgpack never uses. Every legacy OpenDHT message is a msgpack map, which starts with `0x80`–`0x8F` or `0xDE`–`0xDF`. This marker therefore unambiguously separates envelopes from plaintext messages:
- legacy nodes fail to parse an envelope and drop it, without blacklisting the sender;
- new nodes still parse plaintext messages normally.
### 6.2 Hello (private networks only)
`Hello` introduces a node to a peer whose key is unknown. This happens when pinging a bare address, typically a bootstrap node. A Hello is **not encrypted**:
```
0xC1 ‖ 0x03 ‖ pk_sender ‖ epk_sender ‖ credential (97 bytes) ‖ msgpack ping
```
The sender cannot encrypt to a key it doesn't know, and X25519 keys cannot sign, so **a Hello proves nothing about its sender**. Its credential travels in clear and can be copied. The receiver therefore:
- answers only a ping, and only if the credential is valid for `pk_sender`;
- answers with a Session envelope to `pk_sender` and `epk_sender`, so only the holder of `sk_sender` can read the answer;
- keeps no state from the Hello: it does not add, move or authorize the claimed node.
The sender proves possession of its key with its next envelope.
### 6.3 Key advertisement in plaintext messages
New nodes add fields to plaintext msgpack messages. Legacy nodes ignore unknown fields.
| Field | Location | Content |
|---|---|---|
| `k` | top level | Sender's transport public key (bin, 32 bytes) |
| `k4` | reply body, next to `n4` | Concatenated public keys of the listed IPv4 nodes, 32 bytes per node, all-zero if unknown |
| `k6` | reply body, next to `n6` | Same for IPv6 |
Rules for using these fields:
- **`k` is sent only when it is needed**: in public mode, in plaintext messages to nodes whose key is not yet known. Encrypted messages do not need it, because the sender key is in the envelope header.
- **Listed keys are checked before use.** A key from `k`, `k4` or `k6` is attached to a node only if `nodeId(key)` equals the node's ID. Otherwise it is ignored.
- **The binding limits what a dishonest node can do with a node list.** It cannot attach a forged key to an existing node ID. It can still list a wrong address, which at worst causes encrypted packets to be sent to the wrong host. That host cannot read them.
- `k4` and `k6` are omitted if no listed node has a known key. They are ignored if their length is not exactly 32 × the number of listed nodes.
### 6.4 Sending
To send a message to node B:
1. If transport encryption is disabled, send plaintext.
2. If B's static key is known (B has a peer record), send an envelope with our ephemeral key for B's address:
- a Data envelope if our session with B's address is **confirmed**;
- otherwise a Session envelope if B's ephemeral key is known, or else an Initial envelope. In a private network, both include our credential.
3. Else, in a private network:
- if B's ID is unknown (a ping to a bare address), send a `Hello`;
- otherwise refuse to send (`EPERM`).
4. Else (public network, key unknown), send plaintext, including `k`.
Once a node's key is known, nothing is ever sent to it in plaintext. Failed key agreement, including a low-order public key, refuses the send (`EPERM`); it never falls back to plaintext.
**Confirmation.** A session with an address is confirmed when B shows that it holds the session keys and accepted us:
- B answers, in a Session or Data envelope, a request we sent to its node ID **in a Session or Data envelope**. B cached the session when it opened our envelope ([§6.5](#65-receiving)), and answered with keys of our current epoch. Requests sent in an Initial envelope or a Hello are excluded: B answered them without caching a session, so our next request is a Session envelope, and its answer confirms;
- or we receive a Data envelope under our current epoch. B only sends one after we confirmed it the same way.
The confirmation is lost when the session keys change: on epoch rotation, or when B's ephemeral key changes through accepted traffic. An Initial request leaves existing state untouched and is answered without reusing a cached session or its confirmation.
**Establishment.** Only `ping` and `find_node` requests may precede address validation. Application requests wait until B's ephemeral key is known, or a successful control reply identifies a genuinely keyless legacy peer. Learning the ephemeral from an accepted Initial reply is enough to send a Session: neither address-indexed cache admission nor Data confirmation requires another handshake. Direct NetworkEngine application requests share one engine-owned ping; canceling a caller's ping cannot strand them. Private bare-address bootstrap remains a ping-only Hello.
**Recovery.** At the first response timeout, a request forgets B's ephemeral key and starts recovering the session, for example after B restarts. Application requests to B are withheld, including new requests, while one internal ping retries in Initial envelopes (with our credential in a private network). `find_node` remains available and its successful reply can also release the waiters. An accepted reply completes its own request before held work is released, so a delayed original reply is not retransmitted during attachment. Released requests receive fresh response timers; withholding consumes no transmission attempts. The internal probe's first timeout notifies waiting operations so searches can replace unresponsive peers promptly. Probe exhaustion or rejection terminates the waiters. Their payloads never enter Initial envelopes, and no `401` challenge/retry exchange is used.
**Value searches.** A cold peer first receives `find_node(search_target)`. Its reply supplies its ephemeral and closer candidates in the same exchange. The existing distance-ordered search dispatch contacts closer candidates immediately, within the existing four-active-peer limit; it does not wait for a SELECT reply from the farther responder. This routing probe has separate state: it cannot satisfy a GET, pagination, or an empty-result callback. Responders remain eligible for subsequent value retrieval. A cold holder typically needs three exchanges (`find_node`, SELECT IDs, GET by ID), while a session-ready holder retains its usual two. A routing probe's first timeout frees its active search slot while protocol retries continue, rather than waiting for final expiration.
Value data is fragmented so that each packet fits in the 1280-byte IPv6 minimum MTU: `1280 − 40 − 8 = 1232` bytes of UDP payload, less 48 bytes reserved for the msgpack framing of a fragment, less the overhead of the envelope the fragments are sent in. Fragments carry `1184` bytes of value data in plaintext, `1154` in Data envelopes, `1090` in Initial or Session envelopes, and `993` with a credential.
This sizes value fragments only. Complete replies and node lists are not yet subject to a whole-datagram MTU budget; node-list trimming and packet-size enforcement remain separate work.
### 6.5 Receiving
For each received packet, a node applies the following checks in order:
1. **Address filters.** Drop the packet if it comes from a martian or blacklisted address. This is unchanged.
2. **Envelopes.** If the packet starts with `0xC1`:
1. If encryption is disabled, drop it.
2. Drop it if its type is unknown, or if `pk_sender` is our own key. This rejects reflected packets.
3. **Hello:**
- Drop it outside private networks, or if it carries no message.
- Apply the public-key limit, then verify the credential: its key must equal `pk_sender` and it must be valid. Drop the packet on failure.
- Continue with steps 4 and 5, then answer as described in [§6.2](#62-hello-private-networks-only). The Hello is not processed any further.
4. **Initial:** apply the public-key limit, derive the Initial key from `pk_sender` and `epk_sender`, and decrypt.
5. **Session:** first try the keys cached in each epoch for (source address, `pk_sender`, `epk_sender`). Otherwise, apply the public-key limit, then for our current epoch then the previous one: derive the keys with our ephemeral key for the source address, and decrypt. Keep newly derived keys pending until the packet passes authorization and message validation.
6. **Data:** decrypt with the session cached for the source address in our current epoch, then in the previous one. Its `pk_sender` and `epk_sender` are those of the session. This needs no key derivation and no public-key limit.
7. Drop the packet if it was not decrypted.
8. **Credential flag:** strip the 97-byte credential. In a private network, verify it as for Hello, unless the peer is already authorized; on success, record the peer as authorized, without an expiry.
**No credential:** in a private network, drop the packet unless the peer is already authorized.
9. Record `epk_sender` on a temporary peer record. It is **validated for the source address** if the envelope was a Session or Data envelope: opening it required our ephemeral key for that address, which was only ever sent there. Existing peer state is updated only after parsing, ID binding and the processing checks below.
**Session cache.** The epoch caches one session per source address, including its verified admission in private networks. Authorization therefore survives the release of a client-mode peer's node record. Newly derived Session keys enter the cache only after credential authorization, parsing, network and node ID checks, and basic fragment validation. Replies must match a pending request or listener. Admission is per packet, so subsequent Data fragments can decrypt before the whole message is assembled. Sending to the exact address for which the peer's ephemeral key is validated can also cache a session. Answers to Initial and Hello envelopes never use or change cached sessions: their source address may be spoofed. If both the peer record and cached admission are lost, the same permanent credential is verified again; no replacement grant is needed.
3. **Plaintext in a private network.** Drop any packet that is not an envelope.
4. **Parsing.** Parse the msgpack message and apply the network ID check. This is unchanged.
5. **Envelope ID binding.** For an envelope, the message's node ID must equal `nodeId(pk_sender)`, otherwise drop the message. Value data fragments are exempt because they carry no node ID. Fragment buffers are keyed by source address, transaction ID and sender key, with a separate keyless buffer for plaintext. Only parts from the same sender can join a header, and a response header must match a pending request or listener. Foreign-key or plaintext parts cannot reserve the expected sender's buffer, even when they arrive first. Initial envelopes carrying value fragments or fragmented-message headers are dropped: encrypted value data requires a Session or Data envelope.
6. **Plaintext with `k`.** For a plaintext message carrying `k`, attach the key to the sender if it matches the sender's ID.
7. **Processing.**
- A Session or Data envelope can move the sender's node record to the packet's source address.
- Initial requests use transient node records. Only `ping` and `find_node` receive successful replies; GET (including SELECT) and writes receive a short `401` error, and value updates are dropped. Rejected GETs never invoke storage or serialize values. These requests do not add or move an established node, count as activity, or change its ephemeral key or session confirmation.
- Initial replies and errors are accepted only for a pending request at the expected source address, never for a listener. They cannot move a known keyed node to a different address.
- Replies and Data envelopes can confirm the session ([§6.4](#64-sending)).
A **plaintext** message claiming the ID of a node whose key is known is untrusted, even from that node's recorded address:
- it does not move the node record and does not count as activity from that node;
- replies, errors, value updates and writes are dropped;
- `ping` and `find_node` receive control replies using a transient node record and an Initial envelope to the known key; GET receives a short `401`, even when the plaintext packet is the first advertisement of a matching `k`. Truly keyless legacy GETs remain unchanged.
**Rate limits.** A node keeps separate budgets, each with a global quota and a quota per source address (per /64 prefix for IPv6):
- **Session requests:** requests in Session or Data envelopes, whose source address is validated. `Config::max_req_per_sec`, by default 8192 per second, and 1024 per address.
- **Other requests:** plaintext requests and requests in Initial or Hello envelopes, whose source address can be spoofed. `Config::max_unauth_req_per_sec`, with the same defaults. A flood of them does not affect requests from established peers.
- **Public-key work:** one unit per packet needing a key agreement or a signature verification, charged before that work. `Config::max_crypto_per_sec`, by default 512 per second, and 64 per address. A unit covers the packet and its answer, including credential verification:
- each Initial envelope (two X25519 operations, and five more to answer a request);
- each Session envelope from an uncached (address, `pk_sender`, `epk_sender`) triple (five X25519 operations per epoch tried);
- each cached Session envelope whose peer has not yet been authorized and needs credential verification (one charge, not an additional charge for packets already admitted for key agreement);
- each Hello (one Ed25519 verification, and the key derivation of its answer);
- each plaintext request from a sender whose key is known or advertised, answered in an Initial envelope (three X25519 operations).
Initial and uncached Session envelopes from an address where we await an answer, from the sender's node record or from a request sent by address, use a separate budget of the same size. Our own key exchanges thus complete during a flood.
Packets from peers with cached session keys cost one AEAD operation, except when a private-network credential needs verification. Each epoch caches at most 16384 sessions, evicting the least recently used one to store a new one. An evicted peer recovers after one retransmission. Only a peer that received a packet from us at its address can take a cache slot, so a slot costs the attacker a reachable address and a round trip.
### 6.6 Write authorization
`put`, `refresh` and `listen` create state on the receiver, and `listen` makes it send value updates to the requester's address. The receiver therefore requires proof that the requester receives packets at its source address. Without it, an attacker could spoof a victim's address to fill its storage quota, or direct update traffic to it.
- **Encrypted requests** must arrive in a **Session** or **Data** envelope. Our ephemeral key for the source address is only sent to that address ([§5.1](#51-ephemeral-keys)), so opening the envelope proves the sender received one of our packets there. Requests in Initial envelopes are refused with error `401`; normal senders establish the session through `ping` or `find_node` before sending application requests.
- **Plaintext requests** must carry a token, as in the legacy protocol. The token is sent in answers to `find`, `get` and `listen`, and in value updates, to the address of the node record:
```
token(addr) = HMAC-SHA256(key = S, "opendht-address-token-v1" ‖ addr)
```
A token is accepted in the epoch it was issued and the next one. Legacy nodes treat tokens as opaque, so changing their derivation keeps compatibility.
Messages to and from encrypted nodes carry no token. This saves 40 bytes in each answer to `find`, `get` and `listen`, and in each `put`, `refresh`, `listen` and value update. A node that receives an encrypted answer may write to its sender.
## 7. Message flows
### 7.1 Public network
Encryption is opportunistic, and keys travel with node IDs.
```mermaid
sequenceDiagram
participant A as A (new)
participant B as B (new)
participant L as L (legacy)
A->>B: plaintext ping + k = pk_A (B's key unknown)
Note over B: attaches pk_A to A (nodeId check)
B->>A: Initial envelope (pong)
Note over A: decrypts, checks ID = nodeId(pk_B), learns epk_B
A->>B: Session envelope (request)
B->>A: Session envelope (reply)
Note over A: session confirmed
A->>B: Data envelope (request)
Note over B: session confirmed
B->>A: Data envelope (all further traffic)
A->>L: plaintext + k (ignored by L)
L->>A: plaintext
```
- The first request to a node with an unknown key, typically a bootstrap ping, is in plaintext. Its reply is an Initial envelope. The next request and its reply use Session envelopes, and all later traffic uses Data envelopes.
- Nodes learned through `k4`/`k6` get an Initial envelope as the first packet. Its answer is a Session envelope, but the answerer caches nothing yet: the next request is a Session envelope, and its answer confirms the session. Data envelopes follow.
- Legacy nodes see plaintext from new nodes and are unaffected.
### 7.2 Private network
Only nodes with a credential participate. Nodes never send or accept plaintext.
```mermaid
sequenceDiagram
participant A
participant B as B (bootstrap)
A->>B: Hello(pk_A, epk_A, cred_A, ping)
Note over B: verifies cred_A, keeps no state
B->>A: Session(cred_B, pong)
Note over A: verifies cred_B, authorizes B
A->>B: Session(cred_A, request)
Note over B: verifies cred_A, authorizes A, caches the session
B->>A: Session(cred_B, reply)
Note over A: session confirmed
A->>B: Data (without credential)
```
- **Credential exchange.** Each side attaches its credential to its Initial and Session envelopes. Data envelopes, sent once the peer has accepted us, never carry it.
- **Authorization lifetime.** Admission does not expire. Session keys still rotate for forward secrecy, independently of membership. A lost admission record is recovered using the same grant, without operator action.
- **Node lists.** Listed nodes without a valid matching key are discarded.
- **Startup checks.** The node refuses to start (`DhtException`) if:
- encryption is disabled while authorities are configured;
- the configured credential does not match the transport key;
- the credential is not valid for the configured authorities.
The credential and trusted authorities are read on start. Restart with the updated configuration when explicitly changing a grant or the trust set; this also discards cached admissions.
## 8. Key persistence
If the following conditions all hold, the static secret key is saved in the state file as the msgpack field `transport_key` (32 bytes):
- encryption is enabled;
- no key was configured;
- `Config::persist_path` is set.
When the key is saved:
- the state is written to a randomly named temporary file in the destination directory;
- owner-only read/write permissions are requested before any secret data is written;
- the temporary file is closed successfully before it is renamed over the state file;
- a save failure is logged, the temporary file is removed, and the previous state file is left intact;
- on start, the key is restored before any packet is exchanged, so the node keeps its ID across restarts.
The node also stores its ID in the state file, so older versions reading this state keep the same ID. A configured key is never written to the state file. Ephemeral keys and session keys are never persisted.
`dhtnode --transport-key <file>` loads the raw 32-byte secret from `<file>`. If the file does not exist, `dhtnode` creates it using the same temporary-file and permission handling. Unreadable or invalid existing keys are not replaced.
File operations use portable C++ filesystem APIs. Permission semantics depend on the platform; exclusive file creation and power-loss durability are not guaranteed. Use an owner-controlled directory and a single writer for each path.
## 9. Security analysis
### 9.1 Properties
Against an attacker who controls the network but holds no node's secret key:
- **Confidentiality and integrity.** Traffic between two nodes that know each other's keys is confidential and integrity-protected. The protection is IND-CCA2 and INT-CTXT, from ChaCha20-Poly1305 under keys derived from X25519.
- **ID authentication.** An encrypted message under node ID `X` can only be produced by the holder of the static secret key behind `X`.
- Producing a key for a given ID requires a SHA-256 preimage on 160 bits.
- Choosing the first `b` bits of one's own ID costs about $2^b$ key generations. Legacy nodes choose their ID freely.
- **Forward secrecy.** Session envelopes recorded by the attacker stay confidential if static keys are compromised later, once both ephemeral secrets are erased (at most one hour after use). Initial envelopes stay confidential if the sender's static key is compromised, but not the receiver's.
- **Key-compromise impersonation.** With a node's static secret, an attacker can decrypt the Initial envelopes sent to it and impersonate other nodes to it in Initial envelopes, but not in Session envelopes.
- **Address stickiness.** Spoofed plaintext, Hellos and replayed Initial envelopes cannot redirect a node record whose key is known. Moving it requires a Session or Data envelope from the new address (see [§6.2](#62-hello-private-networks-only) and [§6.5](#65-receiving)).
- **Fragment integrity.** Once a fragmented message arrives in envelopes, only parts from the same sender key are merged. Spoofed plaintext or foreign-key parts cannot alter or block it ([§6.5](#65-receiving), step 5).
- **Address validation.** Session and Data envelopes only open from the address our ephemeral key was sent to. An off-path attacker cannot send one from a spoofed address, even with its own valid static key, so it cannot `put`, `refresh` or `listen` on behalf of another address.
- **Private-network admission.** Without a valid credential and its matching secret key, a node:
- cannot read any traffic;
- cannot get answers it can decrypt;
- is never listed by honest nodes.
- **Network separation.** Different network IDs derive different keys.
- **DoS cost.** An unauthenticated Data envelope costs a lookup and at most two AEAD operations. Other envelopes pass the public-key limit before any key derivation or signature verification.
### 9.2 Limitations
Each item states who it applies to, what that party can do, and what it cannot do.
- **Encryption is hop-by-hop.** Nodes that store values read them. Only `SecureDht` protects values end to end ([§1](#1-scope)).
- **Public mode is trust on first use.** Applies in public networks, to an attacker who modifies traffic on the path between two nodes A and B before A knows B's key.
- A node learns a peer's key from the first plaintext message that carries it with the peer's ID: `k` in a message from the peer, or `k4`/`k6` in a reply from any node. An attacker that removes these fields from every such message keeps A and B in plaintext, and reads and modifies their traffic. The downgrade ends with the first message carrying a key that the attacker misses: the node that receives it then sends only envelopes to that peer, which the attacker cannot alter, and treats plaintext claiming the peer's ID as untrusted ([§6.5](#65-receiving), step 7). The attacker can still drop their traffic.
- The attacker cannot bind its own key to B's ID. It can rewrite B's plaintext messages under an ID and key of its own, so that A talks to the attacker's node at B's address, and the attacker relays to B. A then knows the attacker's node at that address, not B.
- Until A knows a key for an ID, any plaintext sender can claim that ID from any address. The claim lasts until A learns the key: plaintext claiming the ID is untrusted from then on, and the first Session or Data envelope under it moves the node record to its sender's address.
- A key received in an encrypted reply cannot be stripped. One received in a plaintext reply can, by an attacker on that reply's path.
- The first request to a node with an unknown key, for example a bootstrap ping, is in plaintext.
- Private networks are not affected: they never send or accept plaintext, and credentials authenticate keys.
- **Compromise of a static secret key.** Applies to whoever obtains `sk_B`, for example from B's key file or state file, for as long as B keeps that key.
- It can act as B from any address, and talk to any node under B's ID.
- It can read every Initial envelope ever sent to B, including recorded ones. Initial keys depend only on `sk_B` and the sender's public keys.
- It can send Initial envelopes to B under any other node's ID. B answers `ping` and `find_node` in them, under keys the attacker cannot derive, and changes no state ([§6.5](#65-receiving), step 7). GET and writes are refused with `401`.
- It cannot read Session or Data envelopes between B and other nodes, recorded or live, nor impersonate another node to B in them. That requires B's ephemeral secret, or the other node's static secret.
- In a private network, it stays admitted for as long as B's granting authority is trusted. The credential is not secret: Hellos carry it in clear.
- Compromise of a running node also exposes its epoch secrets, and with them all sessions of its current and previous epoch.
Recover by changing the key. This changes the node ID, and in a private network needs a new credential. Changing B's key alone does not exclude the compromised key: its grant remains valid until its authority is distrusted, as described below. The old key cannot be revoked in a public network.
- **Initial envelopes are not forward-secret against the receiver.** An Initial envelope stays confidential if the sender's static key is compromised later, but not if the receiver's is. Initial envelopes carry:
- `ping` or `find_node` to a node whose ephemeral key is unknown, including their retransmissions;
- control replies to plaintext requests advertising a key. Application requests wait for a Session ([§6.4](#64-sending)).
Routing targets and control traffic remain exposed to a later compromise of the receiver's static key. Application GET/SELECT and write payloads do not enter Initial envelopes. Answers to Initial envelopes are Session envelopes, and are forward-secret.
- **No replay protection.** Nodes keep no record of received nonces. A captured envelope is accepted again as long as the receiver holds the keys that open it:
- **Initial envelopes** open from any address, for as long as the receiver's static key is unchanged. A replayed `ping` or `find_node` is answered again, to the replayer's source address, under keys the replayer cannot derive. GET and writes are refused with `401`, value updates are dropped, and replies and errors are accepted only for a request pending toward that address ([§6.5](#65-receiving), step 7). Reflection remains for control replies: anyone holding a captured Initial request, or a Hello, can make the receiver send an encrypted answer to an address of its choice. In a private network this includes outsiders, because the captured packet carries its sender's credential. Rate limits bound request frequency, not reply size; node-list packet-size limits remain separate work.
- **Session and Data envelopes** open only from their original source address, which the replayer must spoof, and only until the epoch that opened them is erased: at most one hour. A replayed request is processed again as its sender, including a write the sender did make: a repeated `put`, `refresh` or `listen` extends the life of a value or of a listener. A replayed value update is accepted while its listen is open, and repeats it: the values it lists are added or refreshed again, and the expirations it announces are signaled again. A replayed reply or error is accepted only if its transaction ID is pending. Transaction IDs are 32-bit counters per peer with a random start, so an old ID is pending again only after $2^{32}$ requests to that peer.
- **On-path attackers get write authorization for the addresses they observe.** Applies to an attacker that reads the packets we send to an address `addr`. Our ephemeral key `epk(addr)` is in the clear header of every Initial and Session envelope we send there: at each first contact, retransmission and epoch rotation. Data envelopes do not carry it.
- With its own key pair, the attacker can send us Session envelopes with source address `addr`, and they open. It then has what the owner of `addr` has: write authorization ([§6.6](#66-write-authorization)). Its `put` and `refresh` consume the storage quota of `addr`, and its `listen` directs value updates to `addr`, all under the attacker's own node ID. Each such envelope also replaces the session cached for `addr`, so Data envelopes from the real owner are dropped until its next retransmission establishes a new session ([§6.4](#64-sending), recovery).
- It cannot impersonate the node at `addr`, nor read or alter its traffic. That requires the static secret of that node.
- A captured `epk(addr)` is usable until the epoch that produced it is erased: at most one hour.
- In a private network, the attacker's key must also hold a valid credential.
This is the same exposure as legacy tokens, which are sent in clear to the address they authorize. Address validation defends against off-path spoofing only.
- **Address changes.** Our ephemeral key for a peer is derived from its address. When a peer's address or port changes, for example after a NAT rebinding, its Session and Data envelopes to us no longer open, and ours no longer reach it. Its next retransmission is a ping in an Initial envelope, which starts a new session for the new address, and the request follows in a Session envelope ([§6.4](#64-sending), recovery).
- **No Sybil resistance.** An identity costs one key generation. Encryption prevents choosing a node ID ([§9.1](#91-properties)), not creating many of them. In public networks, legacy nodes still choose their IDs freely.
- **Denial of service.** Public-key operations on unauthenticated packets are bounded by the public-key limit ([§6.5](#65-receiving)): by default 512 packets per second globally, and 64 per source address or IPv6 /64 prefix.
- An attacker spending that budget from many or spoofed addresses drops Initial, Hello and uncached Session envelopes from new peers, and plaintext requests from keyed senders. Answers to our own requests have a separate budget, which only an attacker spoofing the addresses of the peers we contact can spend. Data envelopes, cached sessions and requests from established peers are unaffected.
- Likewise, a flood of plaintext or Initial requests drops other requests without a session, as in the legacy protocol, but not those of established peers.
- A unit stands for three to ten X25519 operations, so the default allows about 5000 per second. `Config::max_crypto_per_sec` adapts it to the CPU available.
- An attacker reachable at many addresses, for example an IPv6 prefix, can fill the session cache of an epoch: one slot costs it one round trip. Evicted peers have their Data envelopes dropped until their next retransmission, which starts a new session.
- **No per-grant revocation.** Applies to private networks. A grant is permanent: a compromised or excluded node can participate for as long as any authority that signed a grant for its key remains trusted. Compromise of an authority allows issuing arbitrary new grants. Exclusion requires every remaining node to remove the affected authority from its trust set and restart, discarding cached admissions. This also excludes every member relying only on that authority; retained members need grants from another trusted authority. Multiple accepted authorities provide a transition path, but do not neutralize a compromised authority while it is still trusted. There is no clock-based fallback, revocation list, or automatic expiry.
- **Metadata.** Anyone observing the traffic sees:
- in Initial and Session envelopes, the sender's static key, hence its node ID, and its ephemeral key: who talks to whom, at each first contact, retransmission and epoch rotation;
- in Data envelopes, only the type; the sender is still identified by its address, from the Session envelopes that preceded;
- in every envelope, the type, the credential flag, the size and the timing, which can reveal the kind of message, for example fragmented value data;
- Hello messages entirely, including the credential, which reveals the node's key. A copied credential is useless without the matching secret key;
- in public networks, plaintext messages entirely, including node lists.
Node addresses are not hidden: they are in node lists, and on the packets themselves.
- **Node state.** The saved node list holds IDs and addresses, but no keys. In a public network, saved nodes are contacted again in plaintext, with the first-contact exposure above. In a private network, a saved node with an ID but no key cannot be contacted ([§6.4](#64-sending), step 3): a restarted node needs a bootstrap address to rejoin, and reuses saved nodes once it learns their keys from node lists.
## 10. Operating a private network
With `dhtnode`:
```sh
# Once: create the authority (net.key = secret seed, net.pub = hex public key)
dhtnode --gen-network-authority net
# Once per node: create its transport key and permanent membership grant
dhtnode --transport-key node1.key --issue-credential node1.cred --authority-key net.key
# Run a node
dhtnode --transport-key node1.key --network-credential node1.cred --network-authority $(cat net.pub)
```
Authority generation refuses to run if either output path already exists, including a dangling symlink.
Keep the authority secret offline. Distribute only `net.pub` and the per-node credentials. Without a compromise or an intentional membership change, keep these keys, grants, and trust sets unchanged: there is no scheduled renewal or maintenance.
`--network-authority` can be repeated to trust several authorities during an explicit transition:
```sh
dhtnode --transport-key node1.key --network-credential node1.cred \
--network-authority $(cat net.pub) --network-authority $(cat replacement.pub)
```
To move away from an authority:
1. Create a replacement authority and configure retained members to trust both old and replacement public keys. Restart them with this overlapping trust set.
2. Issue replacement grants only to retained, uncompromised transport keys and restart those nodes with their new grants. Old and replacement grants interoperate while both authorities are trusted. A compromised transport key must be replaced as well.
3. Remove the old authority from every retained node's configuration and restart them, clearing prior admissions. Only grants from the remaining trusted authorities are then accepted.
A compromised authority or a compromised key's old grant remains usable wherever the old authority is still trusted. The overlap is a transition mechanism, not immediate exclusion; it ends only when all retained nodes stop trusting the affected authority. Uncompromised grants from other authorities do not need updating.
From the library, set `Config::transport` before starting:
- `authorities`;
- `key`, which must be the key the credential was issued for;
- `credential`.
## 11. Constants
| Name | Value |
|---|---|
| Envelope marker | `0xC1` |
| Envelope types | `Initial = 1`, `Session = 2`, `Hello = 3`, `Data = 4` |
| Credential flag | `0x80` |
| Key, nonce, tag sizes | 32, 12, 16 bytes |
| Envelope header | 66 bytes, 2 for Data |
| Envelope overhead | 30 bytes for Data, 94 for Initial and Session (191 with credential) |
| Epoch (ephemeral keys and tokens) | 30 minutes, previous epoch accepted for one more period |
| Cached sessions per epoch | 16384, least recently used evicted |
| Credential size, version | 97 bytes, `0x02` |
| Node ID context | `"opendht-node-id-v1"` |
| HKDF salt | `"opendht-transport-v2"` |
| Credential signature context | `"opendht-credential-v2"` |
| Ephemeral key, token contexts | `"opendht-address-key-v1"`, `"opendht-address-token-v1"` |
| Token size | 32 bytes |
| Value fragment size | 1184 bytes, less the envelope overhead (1154 to 993) |
+1 -19
View File
@@ -4,7 +4,6 @@
#include "infohash.h"
#include "value.h"
#include "transport.h"
#include <vector>
#include <memory>
@@ -98,7 +97,7 @@ struct OPENDHT_PUBLIC NodeInfo
*/
struct OPENDHT_PUBLIC Config
{
/** DHT node ID. Ignored if transport encryption is enabled: the node ID derives from the transport key. */
/** DHT node ID */
InfoHash node_id {};
/**
@@ -123,20 +122,6 @@ struct OPENDHT_PUBLIC Config
/** If non-0, overrides the default per-IP address rate-limit. -1 means no limit. */
ssize_t max_peer_req_per_sec {0};
/**
* If non-0, override the rate-limits of requests from senders without an encrypted session
* (default: same as above). -1 means no limit.
*/
ssize_t max_unauth_req_per_sec {0};
ssize_t max_peer_unauth_req_per_sec {0};
/**
* If non-0, override the rate-limits of packets needing a transport key agreement
* or credential check. -1 means no limit.
*/
ssize_t max_crypto_per_sec {0};
ssize_t max_peer_crypto_per_sec {0};
/* If non-0, overrides the default maximum number of searches. -1 means no limit. */
ssize_t max_searches {0};
@@ -158,9 +143,6 @@ struct OPENDHT_PUBLIC Config
/* Client mode, node will not be used by other nodes to store data. */
bool client_mode {false};
/** Encryption of the DHT protocol, and optional private network */
TransportConfig transport {};
};
/**
+3 -15
View File
@@ -204,13 +204,7 @@ struct OPENDHT_PUBLIC PrivateKey
* @param algo : the public key algorithm to use, GNUTLS_PK_RSA or GNUTLS_PK_RSA_OAEP
*/
static PrivateKey generate(unsigned key_length = 4096, gnutls_pk_algorithm_t algo = GNUTLS_PK_RSA);
/**
* Generate a new EC key pair
* @param curve : the elliptic curve to use. If GNUTLS_ECC_CURVE_INVALID is provided, the default curve will be used
* (currently GNUTLS_ECC_CURVE_SECP384R1).
*/
static PrivateKey generateEC(gnutls_ecc_curve_t curve = GNUTLS_ECC_CURVE_INVALID);
static PrivateKey generateEC();
gnutls_privkey_t key {};
gnutls_x509_privkey_t x509_key {};
@@ -273,15 +267,9 @@ public:
/**
* Sign this revocation list using provided key and certificate.
* Validity_period sets the duration until next update (default to no next update).
* If `number` is non-zero, it is stored verbatim (big-endian) as the CRL Number
* extension. Otherwise the number is initialized randomly on the first signature
* and incremented on subsequent ones.
*/
void sign(const PrivateKey&, const Certificate&, duration validity_period = {}, uint64_t number = 0);
void sign(const Identity& id, duration validity_period = {}, uint64_t number = 0)
{
sign(*id.first, *id.second, validity_period, number);
}
void sign(const PrivateKey&, const Certificate&, duration validity_period = {});
void sign(const Identity& id) { sign(*id.first, *id.second); }
bool isSignedBy(const Certificate& issuer) const;
-6
View File
@@ -159,13 +159,7 @@ public:
{}
static Value::Filter getFilter() { return EncryptedValue::getFilter(); }
virtual void unpackValue(const dht::Value& v) override
{
BaseClass::unpackValue(v);
id = v.id;
}
dht::Value::Id id {0}; /// Value ID of the trust request, used for deserialization only.
std::string service;
std::string conversationId;
Blob payload;
+16 -10
View File
@@ -277,6 +277,9 @@ public:
std::vector<ValuesExport> exportValues() const override;
void importValues(const std::vector<ValuesExport>&) override;
void saveState(const std::string& path) const;
void loadState(const std::string& path);
NodeStats getNodesStats(sa_family_t af) const override;
std::string getStorageLog() const override;
@@ -364,6 +367,8 @@ private:
static constexpr std::chrono::seconds BOOTSTRAP_PERIOD {10};
static constexpr size_t TOKEN_SIZE {32};
// internal structures
struct SearchNode;
struct Get;
@@ -374,14 +379,13 @@ private:
Dht(const Dht&) = delete;
Dht& operator=(const Dht&) = delete;
void saveState(const std::string& path) const;
/* Only called on construction: may replace the transport key */
void loadState(const std::string& path);
std::mt19937_64 rd;
InfoHash myid {};
uint64_t secret {};
uint64_t oldsecret {};
// registred types
TypeStore types;
@@ -434,8 +438,6 @@ private:
net::NetworkEngine network_engine;
std::string persistPath;
/* The transport key was generated: keep it in the persisted state */
bool persistTransportKey {false};
// are we a bootstrap node ?
// note: Any running node can be used as a bootstrap node.
@@ -450,6 +452,11 @@ private:
return public_stable ? LISTEN_EXPIRE_TIME_PUBLIC : LISTEN_EXPIRE_TIME;
}
void rotateSecrets();
Blob makeToken(const SockAddr&, bool old) const;
bool tokenMatch(const Blob& token, const SockAddr&) const;
void reportedAddr(const SockAddr&);
// Storage
@@ -569,8 +576,6 @@ private:
* @param query The query sent to the node.
*/
void searchNodeGetExpired(const net::Request& status, bool over, std::weak_ptr<Search> ws, Sp<Query> query);
void searchNodeRoutingDone(const net::Request& req, net::RequestAnswer&& answer, std::weak_ptr<Search> ws);
void searchNodeRoutingExpired(const net::Request& req, bool over, std::weak_ptr<Search> ws);
/**
* This method recovers sends individual request for values per id.
@@ -622,14 +627,15 @@ private:
void onGetValuesDone(const Sp<Node>& status, net::RequestAnswer& a, Sp<Search>& sr, const Sp<Query>& orig_query);
/* when we receive a listen request */
net::RequestAnswer onListen(
Sp<Node> node, const InfoHash& hash, size_t socket_id, const Query& query, int version = 0);
Sp<Node> node, const InfoHash& hash, const Blob& token, size_t socket_id, const Query& query, int version = 0);
void onListenDone(const Sp<Node>& node, net::RequestAnswer& a, Sp<Search>& sr);
/* when we receive an announce request */
net::RequestAnswer onAnnounce(Sp<Node> node,
const InfoHash& hash,
const Blob& token,
const std::vector<Sp<Value>>& v,
const time_point& created);
net::RequestAnswer onRefresh(Sp<Node> node, const InfoHash& hash, const Value::Id& vid);
net::RequestAnswer onRefresh(Sp<Node> node, const InfoHash& hash, const Blob& token, const Value::Id& vid);
void onAnnounceDone(const Sp<Node>& status, net::RequestAnswer& a, Sp<Search>& sr);
};
+14 -23
View File
@@ -19,9 +19,7 @@
#include <mutex>
namespace dht {
// 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 };
enum class PushType { None = 0, Android, iOS, UnifiedPush };
}
MSGPACK_ADD_ENUM(dht::PushType)
@@ -78,14 +76,8 @@ public:
asio::io_context& io_context() const;
// Monotonic clock for elapsed-time statistics (request rate, uptime).
// Must stay on steady_clock so NTP/wall-clock corrections don't skew durations.
using clock = std::chrono::steady_clock;
using time_point = clock::time_point;
// Wall clock for persisted subscribe/permanent-put lease expirations,
// so they map directly to epoch time and survive restarts.
using sys_clock = std::chrono::system_clock;
using sys_time_point = sys_clock::time_point;
struct PushStats
{
@@ -134,7 +126,6 @@ public:
PushStats androidPush;
PushStats iosPush;
PushStats unifiedPush;
PushStats huaweiPush;
/** Average requests per second */
double requestRate {0};
@@ -275,9 +266,6 @@ 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)
{}
@@ -347,16 +335,20 @@ 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 push session context of the listener, giving the session, push type and topic to use.
* @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 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);
@@ -400,7 +392,6 @@ private:
PushStats androidPush_;
PushStats iosPush_;
PushStats unifiedPush_;
PushStats huaweiPush_;
// Thread-safe access to listeners map.
std::mutex lockListener_;
@@ -410,12 +401,12 @@ private:
std::shared_ptr<ConnectionListener> connListener_;
struct PermanentPut
{
sys_time_point expiration;
time_point expiration;
std::string pushToken;
std::string clientId;
std::shared_ptr<PushSessionContext> sessionCtx;
std::unique_ptr<asio::system_timer> expireTimer;
std::unique_ptr<asio::system_timer> expireNotifyTimer;
std::unique_ptr<asio::steady_timer> expireTimer;
std::unique_ptr<asio::steady_timer> expireNotifyTimer;
Sp<Value> value;
PushType type;
std::string topic;
@@ -428,7 +419,7 @@ private:
p.pack("value");
p.pack(value);
p.pack("exp");
p.pack(sys_clock::to_time_t(expiration));
p.pack(to_time_t(expiration));
if (not clientId.empty()) {
p.pack("cid");
p.pack(clientId);
@@ -469,12 +460,12 @@ private:
#ifdef OPENDHT_PUSH_NOTIFICATIONS
struct Listener
{
sys_time_point expiration;
time_point expiration;
std::string clientId;
std::shared_ptr<PushSessionContext> sessionCtx;
std::future<size_t> internalToken;
std::unique_ptr<asio::system_timer> expireTimer;
std::unique_ptr<asio::system_timer> expireNotifyTimer;
std::unique_ptr<asio::steady_timer> expireTimer;
std::unique_ptr<asio::steady_timer> expireNotifyTimer;
PushType type;
std::string topic;
@@ -485,7 +476,7 @@ private:
p.pack("cid");
p.pack(clientId);
p.pack("exp");
p.pack(sys_clock::to_time_t(expiration));
p.pack(to_time_t(expiration));
if (sessionCtx) {
std::lock_guard l(sessionCtx->lock);
p.pack("sid");
-2
View File
@@ -532,8 +532,6 @@ 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);
+14 -15
View File
@@ -51,11 +51,11 @@ using ConnectHandlerCb = std::function<void(const asio::error_code& ec, const as
using ssl_socket_t = restinio::impl::tls_socket_t;
using socket_t = asio::ip::tcp::socket;
struct OPENDHT_PUBLIC Url
class OPENDHT_PUBLIC Url
{
public:
Url() = default;
Url(std::string url);
Url(std::string_view url);
std::string url;
std::string protocol {"http"};
std::string_view user;
@@ -106,16 +106,9 @@ public:
void timeout(const std::chrono::seconds& timeout, HandlerCb cb = {});
/**
* Close the underlying socket.
* Safe to call from any thread: the actual close is dispatched to
* the thread running the io_context.
*/
void close();
private:
void doClose();
template<typename T>
T wrapCallback(T cb) const
{
@@ -156,7 +149,7 @@ public:
using ResolverCb
= std::function<void(const asio::error_code& ec, const std::vector<asio::ip::tcp::endpoint>& endpoints)>;
Resolver(asio::io_context& ctx, std::string url, std::shared_ptr<log::Logger> logger = {});
Resolver(asio::io_context& ctx, const std::string& url, std::shared_ptr<log::Logger> logger = {});
Resolver(asio::io_context& ctx,
std::string_view host,
std::string_view service,
@@ -168,6 +161,10 @@ public:
std::vector<asio::ip::tcp::endpoint> endpoints,
const bool ssl = false,
std::shared_ptr<log::Logger> logger = {});
Resolver(asio::io_context& ctx,
const std::string& url,
std::vector<asio::ip::tcp::endpoint> endpoints,
std::shared_ptr<log::Logger> logger = {});
~Resolver();
@@ -219,19 +216,19 @@ public:
// resolves implicitly
Request(asio::io_context& ctx,
std::string url,
const std::string& url,
const Json::Value& json,
OnJsonCb jsoncb,
std::shared_ptr<log::Logger> logger = {});
Request(asio::io_context& ctx, std::string url, OnJsonCb jsoncb, std::shared_ptr<log::Logger> logger = {});
Request(asio::io_context& ctx, const std::string& url, OnJsonCb jsoncb, std::shared_ptr<log::Logger> logger = {});
Request(asio::io_context& ctx, std::string url, std::shared_ptr<log::Logger> logger = {});
Request(asio::io_context& ctx, const std::string& url, std::shared_ptr<log::Logger> logger = {});
Request(asio::io_context& ctx,
std::string_view host,
std::string_view service,
const bool ssl = false,
std::shared_ptr<log::Logger> logger = {});
Request(asio::io_context& ctx, std::string url, OnDoneCb onDone, std::shared_ptr<log::Logger> logger = {});
Request(asio::io_context& ctx, const std::string& url, OnDoneCb onDone, std::shared_ptr<log::Logger> logger = {});
// user defined resolver
Request(asio::io_context& ctx, std::shared_ptr<Resolver> resolver, sa_family_t family = AF_UNSPEC);
@@ -347,6 +344,8 @@ private:
dht::crypto::Identity client_identity_;
std::shared_ptr<dht::crypto::Certificate> server_ca_;
std::string service_;
std::string host_;
unsigned int id_;
static std::atomic_uint ids_;
+20 -12
View File
@@ -123,19 +123,28 @@ ee *
*/
static inline unsigned commonBits(const Prefix& p1, const Prefix& p2)
{
/* size_ counts bits, so the comparison walks bits: reading it as a
byte count would run past the end of the shorter content. */
unsigned i, j;
uint8_t x;
auto longest_prefix_size = std::min(p1.size_, p2.size_);
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))
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)) {
break;
common++;
}
}
return 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;
}
/**
@@ -196,7 +205,7 @@ private:
for (auto i = copy.size(); i < size; i++)
copy.push_back(0);
swapBit(copy, size_);
swapBit(copy, size_ + 1);
return copy;
}
@@ -233,9 +242,8 @@ private:
if (bit >= b.size() * 8)
throw std::out_of_range("bit larger than prefix size.");
/* Bit 0 is the most significant one of the first byte, as
isActiveBit reads it. */
b[bit / 8] ^= (1 << (7 - (bit % 8)));
size_t offset_bit = (8 - bit) % 8;
b[bit / 8] ^= (1 << offset_bit);
}
};
+37 -195
View File
@@ -12,16 +12,13 @@
#include "rate_limiter.h"
#include "logger.h"
#include "network_utils.h"
#include "transport.h"
#include <vector>
#include <string>
#include <functional>
#include <algorithm>
#include <list>
#include <memory>
#include <queue>
#include <tuple>
namespace dht {
namespace net {
@@ -37,18 +34,9 @@ struct TransId;
struct NetworkConfig
{
NetId network {0};
/* Requests in Session or Data envelopes, from a validated address */
ssize_t max_req_per_sec {0};
ssize_t max_peer_req_per_sec {0};
/* Other requests. 0: same as above */
ssize_t max_unauth_req_per_sec {0};
ssize_t max_peer_unauth_req_per_sec {0};
/* Packets needing a key agreement or signature check. 0: same as max_req_per_sec */
ssize_t max_crypto_per_sec {0};
ssize_t max_peer_crypto_per_sec {0};
bool is_client {false};
/** Transport encryption is used only if enabled and a key is set */
TransportConfig transport {};
};
class DhtProtocolException : public DhtException
@@ -95,8 +83,7 @@ struct ParsedMessage;
*/
struct RequestAnswer
{
/* Write token of the node; empty if it needs none (encrypted), nullopt if not granted. */
std::optional<Blob> ntoken {};
Blob ntoken {};
Value::Id vid {};
std::vector<Sp<Value>> values {};
std::vector<Value::Id> refreshed_values {};
@@ -173,31 +160,34 @@ private:
*/
std::function<RequestAnswer(Sp<Node>, const InfoHash&, want_t, const Query&)> onGetValues {};
/**
* Called on listen request, from a node proven to receive at its address.
* Called on listen request.
*
* @param node (type: Sp<Node>) the requesting node.
* @param h (type: InfoHash) hash of the value of interest.
* @param token (type: Blob) security token.
* @param rid (type: uint16_t) request id.
*/
std::function<RequestAnswer(Sp<Node>, const InfoHash&, Tid, const Query&, int)> onListen {};
std::function<RequestAnswer(Sp<Node>, const InfoHash&, const Blob&, Tid, const Query&, int)> onListen {};
/**
* Called on announce request, from a node proven to receive at its address.
* Called on announce request.
*
* @param node (type: Sp<Node>) the requesting node.
* @param h (type: InfoHash) hash of the value of interest.
* @param token (type: Blob) security token.
* @param values (type: std::vector<Sp<Value>>) values to store.
* @param created (type: time_point) time when the value was created.
*/
std::function<RequestAnswer(Sp<Node>, const InfoHash&, const std::vector<Sp<Value>>&, const time_point&)>
std::function<RequestAnswer(Sp<Node>, const InfoHash&, const Blob&, const std::vector<Sp<Value>>&, const time_point&)>
onAnnounce {};
/**
* Called on refresh request, from a node proven to receive at its address.
* Called on refresh request.
*
* @param node (type: Sp<Node>) the requesting node.
* @param h (type: InfoHash) hash of the value of interest.
* @param token (type: Blob) security token.
* @param vid (type: Value::id) the value id.
*/
std::function<RequestAnswer(Sp<Node>, const InfoHash&, const Value::Id&)> onRefresh {};
std::function<RequestAnswer(Sp<Node>, const InfoHash&, const Blob&, const Value::Id&)> onRefresh {};
public:
using RequestCb = std::function<void(const Request&, RequestAnswer&&)>;
@@ -224,15 +214,6 @@ public:
net::DatagramSocket* getSocket() const { return dht_socket.get(); };
/** Our transport key, or nullptr if transport encryption is disabled */
const transport::KeyPair* getTransportKey() const { return transport_ ? &config.transport.key : nullptr; }
/** Replaces our transport key. Only valid before any message is exchanged. */
void setTransportKey(const transport::KeyPair& key)
{
if (transport_)
config.transport.key = key;
}
void clear();
/**
@@ -244,6 +225,7 @@ public:
* @param socket_id The tid to use to write to the request socket.
* @param hash The hash key of the value.
* @param want Wether to send ipv4 and/or ipv6 nodes.
* @param ntoken Listen security token.
* @param nodes The ipv4 closest nodes.
* @param nodes6 The ipv6 closest nodes.
* @param values The values to send.
@@ -253,6 +235,7 @@ public:
Tid socket_id,
const InfoHash& hash,
want_t want,
const Blob& ntoken,
std::vector<Sp<Node>>&& nodes,
std::vector<Sp<Node>>&& nodes6,
std::vector<Sp<Value>>&& values,
@@ -262,11 +245,13 @@ public:
void tellListenerRefreshed(const Sp<Node>& n,
Tid socket_id,
const InfoHash& hash,
const Blob& ntoken,
const std::vector<Value::Id>& values,
int version);
void tellListenerExpired(const Sp<Node>& n,
Tid socket_id,
const InfoHash& hash,
const Blob& ntoken,
const std::vector<Value::Id>& values,
int version);
@@ -279,8 +264,6 @@ public:
* Requests *
**************/
bool needsSession(const Node& node) const;
/**
* Send a "ping" request to a given node.
*
@@ -478,10 +461,7 @@ public:
size_t getNodeCacheSize() const { return cache.size(); }
size_t getNodeCacheSize(sa_family_t af) const { return cache.size(af); }
size_t getRateLimiterSize() const
{
return requestLimiter_.size() + unauthLimiter_.size() + cryptoLimiter_.size() + replyCryptoLimiter_.size();
}
size_t getRateLimiterSize() const { return address_rate_limiter.size(); }
size_t getPartialCount() const { return partial_messages.size(); }
@@ -506,136 +486,18 @@ private:
reason to increase this value. */
static constexpr unsigned BLACKLISTED_MAX {10};
/* IPv6 minimum MTU, less the IPv6 and UDP headers */
static constexpr size_t MAX_PACKET_SIZE {1280 - 40 - 8};
/* Upper bound of the msgpack framing around a value fragment */
static constexpr size_t FRAGMENT_HEADER_SIZE {48};
/* Value fragment sizes keeping each packet within the minimum MTU, less the envelope */
static constexpr size_t FRAGMENT_SIZE {MAX_PACKET_SIZE - FRAGMENT_HEADER_SIZE};
static constexpr size_t ENCRYPTED_FRAGMENT_SIZE {FRAGMENT_SIZE - transport::MAX_ENVELOPE_OVERHEAD};
static constexpr size_t MTU {1280};
static constexpr size_t MAX_PACKET_VALUE_SIZE {600};
static constexpr size_t MAX_MESSAGE_VALUE_SIZE {56 * 1024};
static constexpr size_t RX_MAX_EARLY_BYTES {MAX_MESSAGE_VALUE_SIZE + MAX_VALUE_SIZE + 32};
static constexpr size_t RX_MAX_EARLY_PARTS {RX_MAX_EARLY_BYTES / ENCRYPTED_FRAGMENT_SIZE + 2};
/* Lifetime of ephemeral transport keys and write tokens: in use for one period, then accepted for another */
static constexpr std::chrono::minutes EPOCH_DURATION {30};
/* Cached session keys per epoch */
static constexpr size_t MAX_SESSIONS {16 * 1024};
static constexpr size_t TOKEN_SIZE {transport::KEY_SIZE};
struct Session
{
/* our ephemeral key for the peer address */
transport::Key ephemeral;
transport::Key peer;
transport::Key peerEphemeral;
transport::SessionKeys keys;
/* the peer has these keys and accepted us: send Data envelopes */
bool confirmed {false};
bool authorized {false};
};
/* The secret from which ephemeral keys and tokens derive, and session keys, all erased together.
* A session is cached for an address only once its peer proved to receive there: it opened an envelope
* encrypted with our key for that address. Replies to Initial and Hello envelopes never cache one,
* since their source address can be spoofed. */
struct Epoch
{
struct Entry
{
Session session;
std::list<SockAddr>::iterator used;
};
transport::Key secret {};
std::map<SockAddr, Entry> sessions {};
/* most recently used first */
std::list<SockAddr> used {};
explicit operator bool() const { return secret != transport::Key {}; }
transport::KeyPair ephemeral(const SockAddr& addr) const;
Blob token(const SockAddr& addr) const;
/* Marks the session as used */
Session* find(const SockAddr& addr);
Session* find(const SockAddr& addr, const transport::Key& peer, const transport::Key& peerEphemeral);
/* Evicts the least recently used session when full */
void store(const SockAddr& addr, const Session& session);
void wipe();
};
struct PendingSession
{
Epoch* epoch;
Session session;
};
void rotateEpoch();
/* Session with the peer at `addr`, or nullopt for an invalid peer key. Cached if the peer ephemeral key is validated. */
std::optional<Session> session(const transport::Peer& peer, const SockAddr& addr);
std::optional<Blob> openSession(const transport::Header& header,
const uint8_t* buf,
size_t buflen,
const SockAddr& from,
std::optional<PendingSession>& pending,
Session*& cached);
/* Fills the sender keys of `header` from the session */
std::optional<Blob> openData(
transport::Header& header, const uint8_t* buf, size_t buflen, const SockAddr& from, Session*& cached);
struct OpenedEnvelope
{
Sp<transport::Peer> peer;
Blob payload;
/* unauthenticated */
bool hello {false};
/* Session or Data envelope: the sender got our ephemeral key for its address */
bool session {false};
std::optional<PendingSession> pending {};
Session* cached {};
};
std::optional<OpenedEnvelope> openEnvelope(const uint8_t* buf, size_t buflen, const SockAddr& from);
bool authorize(transport::Peer& peer, const uint8_t* credential, size_t size) const;
void attachPeer(Node& node, const Sp<transport::Peer>& peer);
Sp<transport::Peer> makePeer(const InfoHash& id, const transport::Key& key) const;
void processHello(const ParsedMessage& msg, const Sp<transport::Peer>& peer, const SockAddr& from);
/* Advertise our transport key in plaintext messages to nodes not known to support encryption */
bool advertiseKey(const Node& node) const
{
return transport_ and not config.transport.isPrivate() and not node.getTransport();
}
/* Messages to the node are encrypted */
bool encrypts(const Node& node) const { return transport_ and node.getTransport(); }
/* Token granting writes to a plaintext node. Encrypted nodes need none. */
Blob writeToken(const Node& node) const { return encrypts(node) ? Blob {} : epoch_.token(node.getAddr()); }
bool tokenMatch(const Blob& token, const SockAddr& addr) const;
/* Writes need proof that the sender receives at its address: a Session envelope, or else a token */
void authorizeWrite(const ParsedMessage& msg, const Node& node, const std::string& error) const;
/* Number of optional top-level fields packed by packOptionalFields */
unsigned optionalFields(bool key) const
{
return (config.network ? 1 : 0) + (config.is_client ? 1 : 0) + (key ? 1 : 0);
}
void packOptionalFields(msgpack::packer<msgpack::sbuffer>& pk, bool key) const;
void process(std::unique_ptr<ParsedMessage>&&, const SockAddr& from);
bool isExpectedReply(const ParsedMessage&, const SockAddr& from);
bool tryCompleteMessage(PartialMessage&, const SockAddr& from);
/* Admits a request, unless `charged` already: Session and Data senders have their own budget.
* A plaintext request whose answer takes an Initial envelope also needs crypto budget. */
bool limitRequest(const ParsedMessage& msg, const SockAddr& from, bool charged = false);
/* Admits a packet needing a key agreement, from a separate budget if we await a reply from there */
bool limitCrypto(const transport::Key& sender, const SockAddr& from);
bool expectsReply(const transport::Key& sender, const SockAddr& from);
bool rateLimit(const SockAddr& addr);
static bool isMartian(const SockAddr& addr);
bool isNodeBlacklisted(const SockAddr& addr) const;
void requestStep(Sp<Request> req);
void transmitRequest(const Sp<Request>& req);
void notifyRequestTimeout(Request& req);
void completeSession(Node& node);
/* Re-establishes the session with a ping in an Initial envelope, unless one is pending */
void recoverSession(const Sp<Node>& node);
/* Sends the requests withheld during the recovery, under the new session */
void resumeRequests(Node& node);
/**
* Sends a request to a node. Request::MAX_ATTEMPT_COUNT attempts will
@@ -655,13 +517,9 @@ private:
};
// basic wrapper for socket sendto function
int sendRaw(const SockAddr& addr, const uint8_t* buf, size_t len, bool confirmed = false);
// send to a node, encrypted if supported
int send(const Node& node, const SockAddr& addr, const char* buf, size_t len, bool confirmed = false);
/* Bytes added by the envelope that send() uses for the node at `addr` */
size_t envelopeOverhead(const Node& node, const SockAddr& addr);
int send(const SockAddr& addr, const char* buf, size_t len, bool confirmed = false);
void sendValueParts(Tid tid, const std::vector<Blob>& svals, const Node& node, const SockAddr& addr);
void sendValueParts(Tid tid, const std::vector<Blob>& svals, const SockAddr& addr);
std::vector<Blob> packValueHeader(msgpack::sbuffer&,
std::vector<Sp<Value>>::const_iterator,
std::vector<Sp<Value>>::const_iterator) const;
@@ -669,69 +527,53 @@ private:
{
return packValueHeader(buf, values.begin(), values.end());
}
using PartialMessageKey = std::tuple<SockAddr, Tid, transport::Key>;
void maintainRxBuffer(const PartialMessageKey& key);
void maintainRxBuffer(Tid tid);
/*************
* Answers *
*************/
/* answer to a ping request */
void sendPong(const Node& node, const SockAddr& addr, Tid tid);
struct BufferedNodes
{
Blob nodes4, nodes6;
/* transport keys, aligned with nodes, zeros if unknown. Empty if none is known. */
Blob keys4, keys6;
};
void sendPong(const SockAddr& addr, Tid tid);
/* answer to findnodes/getvalues request */
void sendNodesValues(const Node& node,
const SockAddr& addr,
void sendNodesValues(const SockAddr& addr,
Tid tid,
const BufferedNodes& nodes,
const Blob& nodes,
const Blob& nodes6,
const std::vector<Sp<Value>>& st,
const Query& query,
const Blob& token);
std::pair<Blob, Blob> bufferNodes(sa_family_t af, const InfoHash& id, std::vector<Sp<Node>>& nodes);
Blob bufferNodes(sa_family_t af, const InfoHash& id, std::vector<Sp<Node>>& nodes);
BufferedNodes bufferNodes(
std::pair<Blob, Blob> bufferNodes(
sa_family_t af, const InfoHash& id, want_t want, std::vector<Sp<Node>>& nodes, std::vector<Sp<Node>>& nodes6);
/* answer to a listen request */
void sendListenConfirmation(const Node& node, const SockAddr& addr, Tid tid, const Blob& token = {});
void sendListenConfirmation(const SockAddr& addr, Tid tid, const Blob& token = {});
/* answer to put request */
void sendValueAnnounced(const Node& node, const SockAddr& addr, Tid, Value::Id);
void sendValueAnnounced(const SockAddr& addr, Tid, Value::Id);
/* answer in case of error */
void sendError(const Node& node,
const SockAddr& addr,
Tid tid,
uint16_t code,
const std::string& message,
bool include_id = false);
void sendError(const SockAddr& addr, Tid tid, uint16_t code, const std::string& message, bool include_id = false);
void deserializeNodes(ParsedMessage& msg, const SockAddr& from);
/* DHT info */
const InfoHash& myid;
NetworkConfig config {};
const bool transport_ {false};
const Blob credential_ {};
Epoch epoch_ {};
Epoch previousEpoch_ {};
Sp<Scheduler::Job> epochRotation_ {};
const NetworkConfig config {};
const std::unique_ptr<DatagramSocket> dht_socket;
Sp<Logger> logger_;
std::mt19937_64& rd;
NodeCache cache;
/* Requests from Session/Data senders, other requests, public-key work for unexpected and expected senders */
AddressRateLimiter requestLimiter_;
AddressRateLimiter unauthLimiter_;
AddressRateLimiter cryptoLimiter_;
AddressRateLimiter replyCryptoLimiter_;
// global limiting should be triggered by at least 8 different IPs
using IpLimiter = RateLimiter;
using IpLimiterMap = std::map<SockAddr, IpLimiter, SockAddr::ipCmp>;
IpLimiterMap address_rate_limiter;
RateLimiter rate_limiter;
ssize_t limiter_maintenance {0};
// requests handling
std::map<Tid, Sp<Request>> requests {};
std::map<PartialMessageKey, PartialMessage> partial_messages;
std::map<Tid, PartialMessage> partial_messages;
MessageStats in_stats {}, out_stats {};
std::set<SockAddr> blacklist {};
-9
View File
@@ -18,9 +18,6 @@ struct Request;
struct Socket;
struct RequestAnswer;
} /* namespace net */
namespace transport {
struct Peer;
}
using Tid = uint32_t;
using SocketCb = std::function<void(const Sp<Node>&, net::RequestAnswer&&)>;
@@ -49,10 +46,6 @@ struct Node
bool isClient() const { return is_client; }
bool isIncoming() { return time > reply_time; }
/** Transport encryption state, if the node is known to support it */
const Sp<transport::Peer>& getTransport() const { return transport_; }
void setTransport(const Sp<transport::Peer>& peer) { transport_ = peer; }
const time_point& getTime() const { return time; }
const time_point& getReplyTime() const { return reply_time; }
void setTime(const time_point& t) { time = t; }
@@ -91,7 +84,6 @@ struct Node
void requested(const Sp<net::Request>& req);
void received(time_point now, const Sp<net::Request>& req);
Sp<net::Request> getRequest(Tid tid);
const std::map<Tid, Sp<net::Request>>& getRequests() const { return requests_; }
void cancelRequest(const Sp<net::Request>& req);
void setExpired();
@@ -163,7 +155,6 @@ private:
std::map<Tid, Sp<net::Request>> requests_ {};
std::map<Tid, Sp<Socket>> sockets_;
Sp<transport::Peer> transport_ {};
};
} // namespace dht
-1
View File
@@ -24,7 +24,6 @@ struct NodeCache
* To use in case of connectivity change etc.
*/
void clearBadNodes(sa_family_t family = 0);
void clear();
NodeCache(std::mt19937_64& r)
: rd(r) {};
+1 -1
View File
@@ -8,7 +8,7 @@ namespace dht {
namespace proxy {
constexpr const std::chrono::hours OP_TIMEOUT {24}; // one day
constexpr const std::chrono::hours OP_MARGIN {8}; // eight hours
constexpr const std::chrono::hours OP_MARGIN {2}; // two hours
using ListenToken = uint64_t;
-56
View File
@@ -3,9 +3,6 @@
#pragma once
#include "utils.h"
#include "sockaddr.h"
#include <map>
#include <queue>
namespace dht {
@@ -26,11 +23,6 @@ public:
records.pop();
return records.size();
}
/** Whether quota remains, without using it */
bool available(const time_point& now)
{
return quota_ == std::numeric_limits<size_t>::max() or maintain(now) < quota_;
}
/** Return false if quota is reached, insert record and return true otherwise. */
bool limit(const time_point& now)
{
@@ -49,52 +41,4 @@ private:
std::queue<time_point> records {};
};
/**
* A global quota and a quota per address. IPv6 addresses share the quota of their /64 prefix.
* Negative quotas are unlimited.
*/
class AddressRateLimiter
{
public:
AddressRateLimiter(ssize_t quota, ssize_t peerQuota, const duration& period = std::chrono::seconds(1))
: global_(quota, period)
, peerQuota_(peerQuota)
, period_(period)
{}
bool limit(const SockAddr& addr, const time_point& now)
{
// Checked first so that dropped packets create no per-address record
if (not global_.available(now))
return false;
if (peerQuota_ >= 0) {
maintain(now);
auto it = peers_.find(addr);
if (it == peers_.end())
it = peers_.emplace(addr, RateLimiter(peerQuota_, period_)).first;
if (not it->second.limit(now))
return false;
}
return global_.limit(now);
}
/** Number of addresses with a record */
size_t size() const { return peers_.size(); }
private:
void maintain(const time_point& now)
{
if (now < nextMaintenance_)
return;
for (auto it = peers_.begin(); it != peers_.end();)
it = it->second.maintain(now) ? std::next(it) : peers_.erase(it);
nextMaintenance_ = now + period_;
}
RateLimiter global_;
const ssize_t peerQuota_;
const duration period_;
std::map<SockAddr, RateLimiter, SockAddr::ipCmp> peers_ {};
time_point nextMaintenance_ {time_point::min()};
};
} // namespace dht
-5
View File
@@ -284,11 +284,6 @@ 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
+1 -3
View File
@@ -10,9 +10,7 @@
#include <future>
#include <functional>
#if __cplusplus < 202002L
#include <ciso646>
#endif
#include <ciso646> // fix windows compiler bug
namespace dht {
-219
View File
@@ -1,219 +0,0 @@
// Copyright (c) 2014-2026 Savoir-faire Linux Inc.
// SPDX-License-Identifier: MIT
#pragma once
#include "def.h"
#include "infohash.h"
#include "sockaddr.h"
#include "utils.h"
#include <array>
#include <optional>
#include <vector>
namespace dht {
namespace transport {
constexpr size_t KEY_SIZE {32};
constexpr size_t SIGNATURE_SIZE {64};
using Key = std::array<uint8_t, KEY_SIZE>;
using Signature = std::array<uint8_t, SIGNATURE_SIZE>;
/** Erases secret key material. */
OPENDHT_PUBLIC void wipe(Key& key);
/**
* X25519 key pair used to encrypt the DHT protocol: the static key of a node, from which its node ID derives,
* or a periodically rotated ephemeral key.
*/
struct OPENDHT_PUBLIC KeyPair
{
Key secret {};
Key pub {};
static KeyPair generate();
static KeyPair fromSecret(const Key& secret);
explicit operator bool() const { return pub != Key {}; }
};
/** Node ID of a node using the transport public key `pub`. */
OPENDHT_PUBLIC InfoHash nodeId(const Key& pub);
/** Secret from the system CSPRNG. */
OPENDHT_PUBLIC Key randomSecret();
/** Ephemeral key pair used with the peer at `address` (IP and port), derived from an epoch secret. */
OPENDHT_PUBLIC KeyPair addressKey(const Key& epochSecret, const uint8_t* address, size_t size);
/** Write token of the plaintext peer at `address` (IP and port), derived from an epoch secret. */
OPENDHT_PUBLIC Key addressToken(const Key& epochSecret, const uint8_t* address, size_t size);
/** Non-expiring authority-signed grant binding a transport public key to a private network. */
struct OPENDHT_PUBLIC Credential
{
static constexpr size_t PACKED_SIZE {1 + KEY_SIZE + SIGNATURE_SIZE};
Key key {};
Signature signature {};
Blob pack() const;
/** @throws std::invalid_argument if the data is not a packed credential */
static Credential unpack(const uint8_t* data, size_t size);
static Credential unpack(const Blob& b) { return unpack(b.data(), b.size()); }
/** True if signed by one of the trusted authorities. */
bool verify(const std::vector<Key>& authorities) const;
explicit operator bool() const { return key != Key {}; }
};
/** Ed25519 key of a private network authority, issuing node credentials. */
struct OPENDHT_PUBLIC Authority
{
Key secret {};
Key pub {};
static Authority generate();
static Authority fromSecret(const Key& secret);
Credential issue(const Key& transportKey) const;
};
/** Transport state of a remote node, attached to its Node. */
struct Peer
{
explicit Peer(const Key& k)
: key(k)
{}
const Key key;
/** Last authenticated ephemeral key of the peer */
std::optional<Key> ephemeral {};
/** Address proven by the Session or Data envelope carrying the ephemeral key */
SockAddr validatedAddress {};
/** Private networks: the peer presented a grant from a trusted authority */
bool authorized {false};
/** Our session was lost: requests other than pings wait for a new ephemeral key */
bool recovering {false};
void setEphemeral(const Key& k, const SockAddr& address = {})
{
ephemeral = k;
validatedAddress = address;
}
void resetEphemeral()
{
ephemeral.reset();
validatedAddress = {};
}
};
/*
* Envelope: marker | type | sender static key | sender ephemeral key | nonce | ChaCha20-Poly1305(payload) | tag
* Data envelope: marker | type | nonce | ChaCha20-Poly1305(payload) | tag, keys found from the source address.
* The marker is never a valid msgpack first byte, so legacy nodes drop envelopes.
* Hello (private networks only, plaintext): marker | type | sender static key | sender ephemeral key | credential | message
*/
constexpr uint8_t ENVELOPE_MARKER {0xC1};
/** Initial: the sender doesn't know the receiver ephemeral key. Session: it does. Data: the receiver has the session. */
enum class EnvelopeType : uint8_t { Initial = 1, Session = 2, Hello = 3, Data = 4 };
/** Type flag: the payload starts with the sender credential */
constexpr uint8_t CREDENTIAL_FLAG {0x80};
constexpr size_t NONCE_SIZE {12};
constexpr size_t TAG_SIZE {16};
constexpr size_t HEADER_SIZE {2 + 2 * KEY_SIZE};
constexpr size_t DATA_HEADER_SIZE {2};
constexpr size_t ENVELOPE_OVERHEAD {HEADER_SIZE + NONCE_SIZE + TAG_SIZE};
constexpr size_t DATA_ENVELOPE_OVERHEAD {DATA_HEADER_SIZE + NONCE_SIZE + TAG_SIZE};
constexpr size_t MAX_ENVELOPE_OVERHEAD {ENVELOPE_OVERHEAD + Credential::PACKED_SIZE};
inline bool
isEnvelope(const uint8_t* data, size_t size)
{
return size >= DATA_HEADER_SIZE and data[0] == ENVELOPE_MARKER;
}
struct Header
{
EnvelopeType type;
bool credential;
/* not in Data envelopes */
Key sender;
Key ephemeral;
};
/** Envelope header, or nullopt if not an envelope of a known type. */
OPENDHT_PUBLIC std::optional<Header> parseHeader(const uint8_t* data, size_t size);
/** Keys to send to and receive from a peer. */
struct SessionKeys
{
Key tx;
Key rx;
/** Erases secret key material. */
void wipe()
{
transport::wipe(tx);
transport::wipe(rx);
}
/** Destructor that wipes secret key material. */
~SessionKeys()
{
wipe();
}
};
/** Session keys with a peer whose ephemeral key is known, or nullopt for an invalid peer key. */
OPENDHT_PUBLIC std::optional<SessionKeys> sessionKeys(
const KeyPair& own, const KeyPair& ephemeral, const Key& peer, const Key& peerEphemeral, NetId network);
/** Key of an Initial envelope to `peer`, or nullopt for an invalid peer key. */
OPENDHT_PUBLIC std::optional<Key> initialSendKey(const KeyPair& own,
const KeyPair& ephemeral,
const Key& peer,
NetId network);
/** Key of an Initial envelope from `peer`, or nullopt for an invalid peer key. */
OPENDHT_PUBLIC std::optional<Key> initialReceiveKey(const KeyPair& own,
const Key& peer,
const Key& peerEphemeral,
NetId network);
/** Encrypts `credential || data`, authenticating the envelope header. */
OPENDHT_PUBLIC Blob seal(const Key& key,
EnvelopeType type,
const Key& sender,
const Key& ephemeral,
const uint8_t* data,
size_t size,
const Blob& credential = {});
/** Encrypts `data` in a Data envelope. */
OPENDHT_PUBLIC Blob sealData(const Key& key, const uint8_t* data, size_t size);
/** Decrypted payload, or nullopt if the envelope is not authentic. */
OPENDHT_PUBLIC std::optional<Blob> open(const Key& key, const uint8_t* data, size_t size);
OPENDHT_PUBLIC Blob
hello(const Key& sender, const Key& ephemeral, const Blob& credential, const uint8_t* data, size_t size);
} // namespace transport
/**
* Encryption of the DHT protocol. Peers that don't support it keep talking in plaintext,
* unless the node is part of a private network.
*/
struct TransportConfig
{
/** Advertise our key and encrypt traffic with nodes supporting it. Disabling uses a legacy random node ID. */
bool enabled {true};
/** Transport key. If empty, generated on start, and kept in the persisted state if Config::persist_path is set. */
transport::KeyPair key {};
/** Network authorities. If set, only nodes with a credential issued by one of them are accepted. */
std::vector<transport::Key> authorities {};
/** Credential of `key`, required for private networks. */
transport::Credential credential {};
bool isPrivate() const { return not authorities.empty(); }
};
} // namespace dht
-8
View File
@@ -85,14 +85,6 @@ using system_clock = std::chrono::system_clock;
using time_point = clock::time_point;
using duration = clock::duration;
OPENDHT_PUBLIC time_point from_system_time(system_clock::time_point t,
time_point steady_now,
system_clock::time_point system_now);
OPENDHT_PUBLIC time_point from_system_time(system_clock::time_point t);
OPENDHT_PUBLIC system_clock::time_point to_system_time(time_point t,
time_point steady_now,
system_clock::time_point system_now);
OPENDHT_PUBLIC system_clock::time_point to_system_time(time_point t);
OPENDHT_PUBLIC time_point from_time_t(std::time_t t);
OPENDHT_PUBLIC std::time_t to_time_t(time_point t);
+1 -17
View File
@@ -742,7 +742,6 @@ 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:
@@ -774,7 +773,6 @@ 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:
@@ -1017,7 +1015,7 @@ struct OPENDHT_PUBLIC Query
*
* where
*
* - $field$ = *|id|value_type|seq|owner_pk|user_type
* - $field$ = *|id|value_type|owner_pk|user_type
* - $value$ = $string$|$integer$
* - $string$: a simple string WITHOUT SPACES.
* - $integer$: a simple integer.
@@ -1034,23 +1032,9 @@ 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
{
+8 -113
View File
@@ -5,41 +5,26 @@ project(
'cython',
license: 'MIT',
license_files: 'LICENSE',
version: '4.4.99',
version: '4.0.0',
meson_version: '>=1.1.0',
default_options: ['cpp_std=c++20', 'warning_level=3'],
)
gnutls = dependency('gnutls')
nettle = dependency('nettle')
hogweed = dependency('hogweed', version: '>=3.1')
msgpack = dependency('msgpack-cxx', required: false)
argon2 = dependency('libargon2')
fmt = dependency('fmt')
proxy_enabled = get_option('proxy_client').enabled() or get_option('proxy_server').enabled()
openssl = dependency('openssl', required: proxy_enabled)
jsoncpp = dependency('jsoncpp', required: proxy_enabled)
llhttp = dependency('llhttp', 'libllhttp', required: proxy_enabled)
simdutf = dependency('simdutf', required: proxy_enabled)
# restinio is a header-only library that may not ship a pkg-config file
restinio_dep = dependency('restinio', required: false)
if not restinio_dep.found()
cpp = meson.get_compiler('cpp')
if cpp.has_header('restinio/all.hpp', required: proxy_enabled)
restinio_dep = declare_dependency()
endif
endif
openssl = dependency('openssl', required: get_option('proxy_client'))
jsoncpp = dependency('jsoncpp', required: get_option('proxy_client'))
llhttp = dependency('llhttp', 'libllhttp', required: get_option('proxy_client'))
simdutf = dependency('simdutf', required: get_option('proxy_client'))
io_uring = host_machine.system() == 'linux' ? dependency('liburing', required: false) : disabler()
deps = [
fmt,
gnutls,
nettle,
hogweed,
msgpack,
argon2,
openssl,
@@ -48,10 +33,6 @@ deps = [
llhttp,
]
if restinio_dep.found()
deps += restinio_dep
endif
if host_machine.system() == 'linux' and io_uring.found()
deps += io_uring
endif
@@ -86,7 +67,6 @@ opendht_src = [
'src/routing_table.cpp',
'src/node_cache.cpp',
'src/network_engine.cpp',
'src/transport.cpp',
'src/securedht.cpp',
'src/dhtrunner.cpp',
'src/log.cpp',
@@ -99,7 +79,7 @@ if get_option('indexation').enabled()
opendht_src += 'src/indexation/pht.cpp'
add_project_arguments('-DOPENDHT_INDEXATION', language: 'cpp')
endif
if jsoncpp.found() and simdutf.found()
if jsoncpp.found()
opendht_src += ['src/base64.cpp']
add_project_arguments('-DOPENDHT_JSONCPP', language: 'cpp')
conf_data.set('jsoncpp_lib', ', jsoncpp')
@@ -208,68 +188,19 @@ if get_option('c').enabled()
endif
endif
rust_option = get_option('rust').require(
get_option('c').enabled(),
error_message: 'rust requires c',
).require(
get_option('default_library') != 'static',
error_message: 'rust requires shared libraries',
)
if rust_option.allowed()
cargo = find_program('cargo', required: rust_option)
if cargo.found()
rust_env = environment()
rust_env.prepend('LIBRARY_PATH', meson.current_build_dir())
rust_env.prepend('LD_LIBRARY_PATH', meson.current_build_dir())
rust_env.prepend('DYLD_LIBRARY_PATH', meson.current_build_dir())
rust_env.prepend('PATH', meson.current_build_dir())
cargo_args = [
'--manifest-path', meson.current_source_dir() / 'rust/Cargo.toml',
'--target-dir', meson.current_build_dir() / 'rust',
]
rust_build = custom_target(
'rust-bindings',
output: 'rust-build.json',
command: [cargo, 'build', '--lib', '--examples', '--message-format=json-render-diagnostics'] + cargo_args,
env: rust_env,
depends: opendht_c,
capture: true,
build_by_default: true,
build_always_stale: true,
)
if get_option('tests').enabled()
test(
'Rust',
cargo,
args: ['test'] + cargo_args,
env: rust_env,
depends: rust_build,
timeout: 120,
)
endif
endif
endif
if get_option('python').require(
get_option('indexation').enabled(),
error_message: 'python requires indexation',
).allowed()
py = import('python').find_installation(pure: false)
py = import('python').find_installation()
dep_py = py.dependency()
py.extension_module(
'_core',
'opendht',
'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()
@@ -308,16 +239,6 @@ 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(
@@ -354,24 +275,6 @@ 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',
@@ -380,14 +283,6 @@ if get_option('tests').enabled()
)
test('Crypto', test_crypto, timeout: 60)
test_transport = executable(
'test_transport',
'tests/test_transport.cpp',
'tests/tests_runner.cpp',
dependencies: [opendht_dep, cppunit, jsoncpp, fmt, openssl, msgpack],
)
test('Transport', test_transport)
test_dhtrunner = executable(
'test_dhtrunner',
'tests/test_dhtrunner.cpp',
-1
View File
@@ -4,7 +4,6 @@ option('push_notifications', type : 'feature', value : 'disabled')
option('peer_discovery', type : 'feature', value : 'enabled')
option('tools', type : 'feature', value : 'enabled')
option('c', type : 'feature', value : 'enabled')
option('rust', type : 'feature', value : 'disabled')
option('indexation', type : 'feature', value : 'enabled')
option('python', type : 'feature', value : 'enabled')
option('tests', type : 'feature', value : 'enabled')
-5
View File
@@ -1,5 +0,0 @@
node_modules/
build*/
prebuilds/
dist/
examples/dhtnode.js
-64
View File
@@ -1,64 +0,0 @@
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()
-243
View File
@@ -1,243 +0,0 @@
# 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.
-734
View File
@@ -1,734 +0,0 @@
#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)
-304
View File
@@ -1,304 +0,0 @@
#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
-26
View File
@@ -1,26 +0,0 @@
#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
-56
View File
@@ -1,56 +0,0 @@
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 };
}
-171
View File
@@ -1,171 +0,0 @@
#!/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);
-120
View File
@@ -1,120 +0,0 @@
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
View File
@@ -1,187 +0,0 @@
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;
-904
View File
@@ -1,904 +0,0 @@
{
"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"
}
}
}
}
-58
View File
@@ -1,58 +0,0 @@
{
"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"
}
}
-105
View File
@@ -1,105 +0,0 @@
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 });
}
-39
View File
@@ -1,39 +0,0 @@
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")
-38
View File
@@ -1,38 +0,0 @@
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 });
}
-549
View File
@@ -1,549 +0,0 @@
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, '');
});
-93
View File
@@ -1,93 +0,0 @@
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);
});
-34
View File
@@ -1,34 +0,0 @@
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)));
});
-56
View File
@@ -1,56 +0,0 @@
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;
+1 -1
View File
@@ -8,5 +8,5 @@ Version: @VERSION@
Libs: -L${libdir} -lopendht -lfmt
Libs.private: @http_lib@ -pthread
Requires: gnutls >= 3.3@jsoncpp_lib@@openssl_lib@
Requires.private: nettle >= 2.4, hogweed >= 3.1@argon2_lib@@llhttp_lib@@iouring_lib@@simdutf_lib@
Requires.private: nettle >= 2.4@argon2_lib@@llhttp_lib@@iouring_lib@@simdutf_lib@
Cflags: -I${includedir}@opendht_public_cflags@
@@ -1,118 +0,0 @@
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>
@@ -1,12 +0,0 @@
--- 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}
-187
View File
@@ -1,187 +0,0 @@
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")
@@ -1,2 +0,0 @@
find_library(GNUTLS_LIBRARY NAMES gnutls gnutlsd NAMES_PER_DIR)
_find_package(${ARGS})
-23
View File
@@ -1,23 +0,0 @@
{
"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"
]
}
+11 -1
View File
@@ -28,8 +28,18 @@ install(CODE [[
if(NOT _pip_check EQUAL 0)
message(WARNING "Skipping Python package install: pip not available for ${Python3_EXECUTABLE}.")
else()
set(_pip_install_args
install
--no-deps
--no-build-isolation
--break-system-packages
--prefix ${CMAKE_INSTALL_PREFIX})
if(DEFINED ENV{DESTDIR} AND NOT "$ENV{DESTDIR}" STREQUAL "")
list(APPEND _pip_install_args --root "$ENV{DESTDIR}")
endif()
list(APPEND _pip_install_args .)
execute_process(
COMMAND ${Python3_EXECUTABLE} -m pip install --no-deps --break-system-packages .
COMMAND ${Python3_EXECUTABLE} -m pip ${_pip_install_args}
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/python
OUTPUT_VARIABLE _pip_output
ERROR_VARIABLE _pip_error
-171
View File
@@ -78,177 +78,6 @@ 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
+79 -453
View File
@@ -12,21 +12,15 @@ 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, unique_ptr
from libcpp.memory cimport shared_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()
@@ -45,58 +39,6 @@ _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']:
@@ -109,11 +51,9 @@ 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
try:
if 'shutdown' in cbs and cbs['shutdown']:
cbs['shutdown']()
finally:
ref.Py_DECREF(cbs)
if 'shutdown' in cbs and cbs['shutdown']:
cbs['shutdown']()
ref.Py_DECREF(cbs)
cdef inline bool get_callback(shared_ptr[cpp.Value] value, void *user_data) noexcept with gil:
cbs = <object>user_data
@@ -132,26 +72,22 @@ 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
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)
if 'done' in cbs and cbs['done']:
cbs['done'](done, node_ids)
ref.Py_DECREF(cbs)
cdef inline void done_callback_simple(bool done, void *user_data) noexcept with gil:
cbs = <object>user_data
try:
if 'done' in cbs and cbs['done']:
cbs['done'](done)
finally:
ref.Py_DECREF(cbs)
if 'done' in cbs and cbs['done']:
cbs['done'](done)
ref.Py_DECREF(cbs)
cdef class _WithID(object):
def __repr__(self):
@@ -165,12 +101,6 @@ 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''):
@@ -186,9 +116,9 @@ cdef class InfoHash(_WithID):
return self._infohash == other._infohash
return NotImplemented
def getBit(InfoHash self, bit):
return self._infohash.getBit(_checked_bit_index(bit, 160))
return self._infohash.getBit(bit)
def setBit(InfoHash self, bit, b):
self._infohash.setBit(_checked_bit_index(bit, 160), b)
self._infohash.setBit(bit, b)
def getId(InfoHash self):
return self
def toString(InfoHash self):
@@ -224,9 +154,9 @@ cdef class PkId(_WithPk):
return self._pkid == other._pkid
return NotImplemented
def getBit(PkId self, bit):
return self._pkid.getBit(_checked_bit_index(bit, 256))
return self._pkid.getBit(bit)
def setBit(PkId self, bit, b):
self._pkid.setBit(_checked_bit_index(bit, 256), b)
self._pkid.setBit(bit, b)
def getLongId(PkId self):
return self
def toString(PkId self):
@@ -282,92 +212,14 @@ 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):
@@ -401,11 +253,8 @@ cdef class Select(object):
return self._select.toString().decode()
def isSatisfiedBy(self, Select os):
return self._select.isSatisfiedBy(os._select)
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)
def field(self, int field):
self._select.field(<cpp.Field> field)
return self
cdef class Where(object):
@@ -441,10 +290,6 @@ 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()
@@ -479,51 +324,25 @@ 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 unique_ptr[cpp.ValueType] _value
cdef 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.reset(new cpp.ValueType(id, name.encode(), duration))
self._value = new cpp.ValueType(id, name.encode(), duration)
cdef class NodeSetIter(object):
cdef NodeSet _owner
cdef map[cpp.InfoHash, shared_ptr[cpp.Node]]* _nodes
cdef map[cpp.InfoHash, shared_ptr[cpp.Node]].iterator _curIter
def __init__(self, NodeSet s not None):
self._owner = s
self._curIter = self._owner._nodes.begin()
def __iter__(self):
return self
def __init__(self, NodeSet s):
self._nodes = &s._nodes
self._curIter = self._nodes.begin()
def __next__(self):
if self._curIter == self._owner._nodes.end():
if self._curIter == self._nodes.end():
raise StopIteration
h = NodeEntry()
h._v = deref(self._curIter)
@@ -555,8 +374,6 @@ 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
@@ -566,30 +383,6 @@ 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")
@@ -612,8 +405,6 @@ 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
@@ -648,25 +439,6 @@ 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")
@@ -728,17 +500,11 @@ 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() if self._cert else b''
return self._cert.get().toString().encode() if self._cert else b''
@property
def issuer(self) -> Certificate|None:
if self._cert and self._cert.get().issuer:
@@ -748,10 +514,6 @@ 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
@@ -766,16 +528,10 @@ 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
@@ -783,7 +539,7 @@ cdef class TrustList(object):
cdef class ListenToken(object):
cdef cpp.InfoHash _h
cdef cpp.shared_future[size_t] _t
cdef bool _cancelled
_cb = dict()
cdef class Identity(object):
cdef cpp.Identity _id
@@ -794,8 +550,6 @@ 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
@@ -856,19 +610,9 @@ 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
def setCryptoRateLimit(self, ssize_t max_crypto_per_sec, ssize_t max_peer_crypto_per_sec):
self._config.dht_config.node_config.max_crypto_per_sec = max_crypto_per_sec
self._config.dht_config.node_config.max_peer_crypto_per_sec = max_peer_crypto_per_sec
def setProxyInfo(self, str proxy_server, str push_node_id="", str push_token="", str push_topic="", str push_platform="", str user_agent="", Certificate server_ca = Certificate()):
self._config.proxy_server = proxy_server.encode()
self._config.push_node_id = push_node_id.encode()
@@ -886,65 +630,24 @@ 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:
with nogil:
h._infohash = self.thisptr.get().getId()
h._infohash = self.thisptr.get().getId()
return h
def getPublicKey(self) -> PublicKey:
if not self.thisptr:
raise ValueError("DhtRunner is not initialized")
pk = PublicKey()
with nogil:
pk._key = self.thisptr.get().getPublicKey()
pk._key = self.thisptr.get().getPublicKey()
return pk
def getNodeId(self) -> InfoHash:
h = InfoHash()
if self.thisptr:
with nogil:
h._infohash = self.thisptr.get().getNodeId()
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}
@@ -970,80 +673,44 @@ 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 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
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)
if ipv4 or ipv6:
bind4 = ipv4.encode() if ipv4 else b''
bind6 = ipv6.encode() if ipv6 else b''
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)
self.thisptr.get().run(bind4, bind6, str(port).encode(), config._config)
else:
with nogil:
cpp.runPythonRunner(deref(self.thisptr.get()), port, run_config,
status_callback, status_ref)
self.thisptr.get().run(port, config._config)
def join(self):
with nogil:
self.thisptr.get().join()
self._indexes.clear()
self.thisptr.get().join()
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 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 registerType(self, ValueType type):
self.thisptr.get().registerType(deref(type._value))
def enableLogging(self):
with nogil:
cpp.enableLogging(self.thisptr.get()[0])
cpp.enableLogging(self.thisptr.get()[0])
def disableLogging(self):
with nogil:
cpp.disableLogging(self.thisptr.get()[0])
cpp.disableLogging(self.thisptr.get()[0])
def enableFileLogging(self, str path):
cdef cpp.string log_path = path.encode()
with nogil:
cpp.enableFileLogging(self.thisptr.get()[0], log_path)
cpp.enableFileLogging(self.thisptr.get()[0], path.encode())
def isRunning(self):
return self.thisptr.get().isRunning()
def getBound(self, cpp.sa_family_t af = 0):
s = SockAddr()
with nogil:
s._addr = self.thisptr.get().getBound(af)
s._addr = self.thisptr.get().getBound(af)
return s
def getStorageLog(self):
cdef cpp.string result
with nogil:
result = self.thisptr.get().getStorageLog()
return result.decode()
return self.thisptr.get().getStorageLog().decode()
def getRoutingTablesLog(self, cpp.sa_family_t af):
cdef cpp.string result
with nogil:
result = self.thisptr.get().getRoutingTablesLog(af)
return result.decode()
return self.thisptr.get().getRoutingTablesLog(af).decode()
def getSearchesLog(self, cpp.sa_family_t af):
cdef cpp.string result
with nogil:
result = self.thisptr.get().getSearchesLog(af)
return result.decode()
def getNodeMessageStats(self, bool incoming=False):
return self.thisptr.get().getSearchesLog(af).decode()
def getNodeMessageStats(self):
stats = []
cdef cpp.vector[unsigned] res
with nogil:
res = self.thisptr.get().getNodeMessageStats(incoming)
cdef cpp.vector[unsigned] res = self.thisptr.get().getNodeMessageStats(False)
for n in res:
stats.append(n)
return stats
@@ -1086,25 +753,6 @@ 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.
@@ -1128,7 +776,7 @@ cdef class DhtRunner(_WithID):
lock.notify()
with lock:
pending += 1
self.put(key, val, done_cb=tmp_done, permanent=permanent)
self.put(key, val, done_cb=tmp_done)
while pending > 0:
lock.wait()
return ok
@@ -1150,7 +798,7 @@ cdef class DhtRunner(_WithID):
lock.notify()
with lock:
pending += 1
self.putSigned(key, val, done_cb=tmp_done, permanent=permanent)
self.putSigned(key, val, done_cb=tmp_done)
while pending > 0:
lock.wait()
return ok
@@ -1158,8 +806,6 @@ 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)
@@ -1199,23 +845,19 @@ cdef class DhtRunner(_WithID):
def cancelPut(self, InfoHash key, Value val):
self.thisptr.get().cancelPut(key._infohash, val._value)
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()
def listen(self, InfoHash key, value_cb):
t = ListenToken()
t._h = key._infohash
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()
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()
return t
def cancelListen(self, ListenToken token):
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
self.thisptr.get().cancelListen(token._h, token._t)
ref.Py_DECREF(<object>token._cb['cb'])
# fixme: not thread safe
cdef class IndexValue(object):
cdef cpp.shared_ptr[cpp.IndexValue] _value
@@ -1232,18 +874,12 @@ cdef class IndexValue(object):
return self._value.get().second
cdef class Pht(object):
"""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.Pht* thisptr
def __cinit__(self, bytes name, key_spec, DhtRunner dht):
cdef cpp.IndexKeySpec cpp_key_spec
for kk, size in key_spec.items():
cpp_key_spec[bytes(kk, 'utf-8')] = size
self._runner = dht
self.thisptr.reset(new cpp.Pht(name, cpp_key_spec, dht.thisptr))
def __dealloc__(self):
with nogil:
self.thisptr.reset()
self.thisptr = new cpp.Pht(name, cpp_key_spec, dht.thisptr)
@property
def MAX_NODE_ENTRY_COUNT(self):
return cpp.PHT_MAX_NODE_ENTRY_COUNT
@@ -1256,21 +892,16 @@ 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._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
self.thisptr.lookup(
cppk,
cpp.Pht.bindLookupCb(lookup_callback, <void*>cb_obj),
cpp.bindDoneCbSimple(done_callback_simple, <void*>cb_obj)
)
def insert(self, key, IndexValue value, done_cb=None):
"""Add an index entry to the Index.
@@ -1278,21 +909,16 @@ 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._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
self.thisptr.insert(
cppk,
val,
cpp.bindDoneCbSimple(done_callback_simple, <void*>cb_obj)
)
+24 -83
View File
@@ -3,20 +3,12 @@
from __future__ import annotations
import asyncio
from typing import Any, Callable, Generic, Iterable, List, Optional, Tuple, TypeVar
from typing import Any, Callable, Iterable, List, Optional, Tuple
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:
@@ -27,11 +19,7 @@ class _Listener:
__slots__ = ("_runner", "_token", "_loop", "_queue", "_closed")
def __init__(
self, runner: DhtRunner, key: InfoHash,
where: Optional[Where] = None,
filter: Optional[Callable[[Value], bool]] = None,
) -> None:
def __init__(self, runner: DhtRunner, key: InfoHash) -> None:
self._runner = runner
self._loop = runner._loop
self._queue: asyncio.Queue[Tuple[Value, bool]] = asyncio.Queue()
@@ -43,7 +31,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, filter, where)
self._token = self._runner._dht.listen(key, _on_value)
def __enter__(self) -> _Listener:
return self
@@ -74,10 +62,10 @@ class _Listener:
self._queue.put_nowait(None)
class _GetStream(Generic[_Result]):
"""Async iterator around a DHT get or query callback.
class _GetStream:
"""Async iterator around DHT get callback.
Yields results as they are found, then ends.
Yields Value instances as they are found, then ends.
Raises on failure.
"""
__slots__ = ("_runner", "_loop", "_queue", "_closed", "_error")
@@ -88,15 +76,14 @@ class _GetStream(Generic[_Result]):
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[_Result]] = asyncio.Queue()
self._queue: asyncio.Queue[Optional[Value]] = asyncio.Queue()
self._closed = False
self._error: Optional[BaseException] = None
def _on_value(v: _Result) -> bool:
def _on_value(v: Value) -> bool:
# Called on DHT thread
if self._closed:
return False
@@ -108,24 +95,21 @@ class _GetStream(Generic[_Result]):
if self._closed:
return
if not ok:
self._error = RuntimeError("DHT query failed" if query is not None else "DHT get failed")
self._error = RuntimeError("DHT get failed")
self._loop.call_soon_threadsafe(self._queue.put_nowait, None)
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)
self._runner._dht.get(key, _on_value, _on_done, filter, where)
def __enter__(self) -> _GetStream[_Result]:
def __enter__(self) -> _GetStream:
return self
def __exit__(self, exc_type, exc, tb) -> None:
self.cancel()
def __aiter__(self) -> _GetStream[_Result]:
def __aiter__(self) -> _GetStream:
return self
async def __anext__(self) -> _Result:
async def __anext__(self) -> Value:
if self._closed:
raise StopAsyncIteration
item = await self._queue.get()
@@ -148,7 +132,7 @@ class DhtRunner:
"""Async wrapper around opendht.DhtRunner.
"""
__slots__ = ("_dht", "_loop", "_config", "_bootstrap", "_status_generation")
__slots__ = ("_dht", "_loop", "_config", "_bootstrap")
def __init__(
self,
@@ -165,7 +149,6 @@ 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,
@@ -185,32 +168,9 @@ 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(_set_result_if_pending, fut, bool(ok)))
self._dht.ping(addr, lambda ok: self._loop.call_soon_threadsafe(fut.set_result, bool(ok)))
return await fut
def get(
@@ -219,25 +179,9 @@ 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[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
return _GetStream(self, key, where, filter)
async def getAll(
self,
@@ -276,7 +220,7 @@ class DhtRunner:
fut: asyncio.Future[bool] = self._loop.create_future()
def _done(ok: bool, _nodes: Iterable[Any]) -> None:
self._loop.call_soon_threadsafe(_set_result_if_pending, fut, bool(ok))
self._loop.call_soon_threadsafe(fut.set_result, bool(ok))
self._dht.put(key, val, _done, permanent)
return await fut
@@ -285,7 +229,7 @@ class DhtRunner:
fut: asyncio.Future[bool] = self._loop.create_future()
def _done(ok: bool, _nodes: Iterable[Any]) -> None:
self._loop.call_soon_threadsafe(_set_result_if_pending, fut, bool(ok))
self._loop.call_soon_threadsafe(fut.set_result, bool(ok))
self._dht.putSigned(key, val, _done, permanent)
return await fut
@@ -301,23 +245,20 @@ class DhtRunner:
fut: asyncio.Future[bool] = self._loop.create_future()
def _done(ok: bool, _nodes: Iterable[Any]) -> None:
self._loop.call_soon_threadsafe(_set_result_if_pending, fut, bool(ok))
self._loop.call_soon_threadsafe(fut.set_result, bool(ok))
self._dht.putEncrypted(key, val, to, _done, permanent)
return await fut
def listen(
self, key: InfoHash, *, where: Optional[Where] = None,
filter: Optional[Callable[[Value], bool]] = None,
) -> _Listener:
return _Listener(self, key, where, filter)
def listen(self, key: InfoHash) -> _Listener:
return _Listener(self, key)
async def cancelListen(self, listener: _Listener) -> None:
listener.cancel()
await listener.cancel()
async def shutdown(self) -> None:
fut: asyncio.Future[None] = self._loop.create_future()
self._dht.shutdown(lambda: self._loop.call_soon_threadsafe(_set_result_if_pending, fut, None))
self._dht.shutdown(lambda: self._loop.call_soon_threadsafe(fut.set_result, None))
await fut
async def join(self) -> None:
+47 -178
View File
@@ -98,53 +98,49 @@ 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) except +
cdef Identity generateIdentity(string name, Identity ca, unsigned bits)
cdef Blob aesEncrypt(Blob data, string password) except +
cdef Blob aesDecrypt(Blob encrypted, string password) except +
cdef cppclass PrivateKey:
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 +
PrivateKey()
shared_ptr[PublicKey] getSharedPublicKey() const
Blob decrypt(Blob data) const
Blob sign(Blob data) const
@staticmethod
PrivateKey generate() except +
PrivateKey generate()
@staticmethod
PrivateKey generateEC() except +
PrivateKey generateEC()
cdef cppclass PublicKey:
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 +
PublicKey()
InfoHash getId() const
PkId getLongId() const
Blob encrypt(Blob data) const
bool checkSignature(Blob data, Blob signature) const
bool operator==(PublicKey) const
cdef cppclass Certificate:
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 +
Certificate()
Certificate(string pem)
InfoHash getId() const
PkId getLongId() const
string toString() const
string getName() const
void revoke(PrivateKey key, Certificate cert)
@staticmethod
Certificate generate(PrivateKey key, string name, Identity ca, bool is_ca) except +
Certificate generate(PrivateKey key, string name, Identity ca, bool is_ca)
shared_ptr[Certificate] issuer
cdef cppclass TrustList:
cppclass VerifyResult:
bool operator bool() const
bool isValid() const
string toString() except +
TrustList() except +
void add(Certificate) except +
void remove(Certificate) except +
VerifyResult verify(Certificate) except +
string toString() const
TrustList()
void add(Certificate)
void remove(Certificate)
VerifyResult verify(Certificate);
ctypedef TrustList.VerifyResult TrustListVerifyResult
@@ -155,7 +151,6 @@ 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
@@ -168,20 +163,13 @@ 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
@@ -203,7 +191,7 @@ cdef extern from "opendht/value.h" namespace "dht":
cdef cppclass Where:
Where() except +
Where(const string& q_str) except +
Where(const string& q_str)
bool isSatisfiedBy(const Where& where) const
Where& id(uint64_t id)
Where& valueType(uint16_t type)
@@ -212,14 +200,6 @@ 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 +
@@ -228,30 +208,6 @@ 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)
@@ -283,90 +239,13 @@ cdef extern from "opendht/callbacks.h" namespace "dht":
string persist_path
size_t max_req_per_sec
size_t max_peer_req_per_sec
ssize_t max_crypto_per_sec
ssize_t max_peer_crypto_per_sec
cppclass SecureDhtConfig:
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:
@@ -385,47 +264,37 @@ cdef extern from "opendht/dhtrunner.h" namespace "dht":
SockAddr bind4
SockAddr bind6
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
InfoHash getId() const
shared_ptr[PublicKey] getPublicKey() const
InfoHash getNodeId() const
void bootstrap(const_char*, const_char*)
void bootstrap(const SockAddr&, DoneCallbackSimple done_cb)
void run(in_port_t, Config config) except +
void run(const_char*, const_char*, const_char*, Config config) except +
void join() except + nogil
void run(in_port_t, Config config)
void run(const_char*, const_char*, const_char*, Config config)
void join()
void shutdown(ShutdownCallback)
void registerType(const ValueType& value) except + nogil
void registerType(ValueType& value);
bool isRunning()
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
SockAddr getBound(sa_family_t af) const
string getStorageLog() const
string getRoutingTablesLog(sa_family_t af) const
string getSearchesLog(sa_family_t af) const
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, nullptr_t f, Where w) except +
void cancelListen(InfoHash key, SharedListenToken token) except +
vector[unsigned] getNodeMessageStats(bool i) except + nogil
ListenToken listen(InfoHash key, ValueCallback get_cb)
void cancelListen(InfoHash key, SharedListenToken token)
vector[unsigned] getNodeMessageStats(bool i)
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) except + nogil
void disableLogging(DhtRunner& dht) except + nogil
void enableFileLogging(DhtRunner& dht, const string& path) except + nogil
void enableLogging(DhtRunner& dht)
void disableLogging(DhtRunner& dht)
void enableFileLogging(DhtRunner& dht, const string& path)
cdef extern from "opendht/indexation/pht.h" namespace "dht::indexation":
size_t PHT_MAX_NODE_ENTRY_COUNT "dht::indexation::Pht::MAX_NODE_ENTRY_COUNT"
@@ -441,8 +310,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) except +
void insert(IndexKey k, IndexValue v, DoneCallbackSimple cb) except +
void lookup(IndexKey k, LookupCallback cb, DoneCallbackSimple doneCb);
void insert(IndexKey k, IndexValue v, DoneCallbackSimple cb)
@staticmethod
LookupCallback bindLookupCb(LookupCallbackRaw cb, void *user_data)
+5 -5
View File
@@ -4,11 +4,11 @@ build-backend = "setuptools.build_meta"
[tool.cibuildwheel]
build = [
'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',
'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',
]
[tool.cibuildwheel.linux]
-35
View File
@@ -1,35 +0,0 @@
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()
File diff suppressed because it is too large. Load diff
-1
View File
@@ -9,7 +9,6 @@ import argparse
import logging
import logging.handlers
import netifaces
import opendht as dht
logger = logging.getLogger('dhtcluster')
-1
View File
@@ -11,7 +11,6 @@ from opendht import aio as dht
async def main():
config = dht.DhtConfig()
config.setRateLimit(-1, -1)
config.setCryptoRateLimit(-1, -1)
ping_node = dht.DhtRunner()
pong_node = dht.DhtRunner()
-22
View File
@@ -1,22 +0,0 @@
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.
-70
View File
@@ -1,70 +0,0 @@
# 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.
+4 -23
View File
@@ -1,30 +1,11 @@
[package]
name = "opendht"
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"]
version = "0.1.0"
authors = ["Sébastien Blin <sebastien.blin@savoirfairelinux.com>"]
edition = "2021"
[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 }
[dev-dependencies]
futures = "0.3"
serde = { version = "1", features = ["derive"] }
serde_bytes = "0.11"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+11 -142
View File
@@ -1,152 +1,21 @@
# OpenDHT: a Distributed Hash Table
# Rust bindings for opendht
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.
## Build
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.
Install rust on your system (https://rustup.rs/ is the recommanded way). Once done:
## Get Started
Add the crate to your dependencies:
```toml
[dependencies]
opendht = "0.6"
```
cargo build
```
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.
## Run tests
`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(())
}
```
cargo test
```
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.
## Run dht node
## 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"
```
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)
cargo run --example dhtnode
```
+16 -19
View File
@@ -2,18 +2,19 @@
// SPDX-License-Identifier: MIT
extern crate opendht;
use std::{thread, time};
use std::{ thread, time };
use opendht::{DhtRunner, DhtRunnerConfig, InfoHash, Value};
use opendht::{ InfoHash, DhtRunner, DhtRunnerConfig, Value };
// use opendht::crypto::*;
fn main() -> std::io::Result<()> {
fn main() {
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.
@@ -22,40 +23,36 @@ fn main() -> std::io::Result<()> {
//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 get_cb = move |v: Box<Value>| {
let mut get_cb = |v: Box<Value>| {
//data += 1;
println!("GET: VALUE CB - data: {} - v: {}", data, v);
true
};
let done_cb = move |ok: bool| {
let mut done_cb = |ok: bool| {
println!("GET: DONE CB - data: {} - ok: {}", data, ok);
};
dht.get(&InfoHash::get("alice"), get_cb, done_cb);
dht.get(&InfoHash::get("alice"), &mut get_cb, &mut done_cb);
let put_done_cb = move |ok: bool| {
let mut put_done_cb = |ok: bool| {
println!("PUT: DONE CB - data: {} - ok: {}", data, ok);
};
dht.put(&InfoHash::get("bob"), Value::new("hi!"), put_done_cb, false);
dht.put(&InfoHash::get("bob"), Value::new("hi!"), &mut put_done_cb, false);
println!("Start listening /foo");
let value_cb = move |v, expired| {
println!(
"LISTEN: DONE CB - data: {} - v: {} - expired: {}",
data, v, expired
);
let mut value_cb = |v, expired| {
println!("LISTEN: DONE CB - data: {} - v: {} - expired: {}", data, v, expired);
true
};
let token = dht.listen(&InfoHash::get("foo"), value_cb)?;
let token = dht.listen(&InfoHash::get("foo"), &mut 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(())
}
}
-80
View File
@@ -1,80 +0,0 @@
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
View File
@@ -1,429 +0,0 @@
//! 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()
}
}
+7 -9
View File
@@ -1,20 +1,18 @@
// Copyright (c) 2014-2026 Savoir-faire Linux Inc.
// SPDX-License-Identifier: MIT
use crate::ffi::{self, DataView};
use crate::ffi::*;
native_handle!(Blob, dht_blob_delete);
pub use crate::ffi::Blob;
impl Blob {
pub fn data(&self) -> DataView {
unsafe { ffi::dht_blob_get_data(self.as_ptr()) }
unsafe { dht_blob_get_data(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() }
impl Drop for Blob {
fn drop(&mut self) {
unsafe { dht_blob_delete(&mut *self) }
}
}
+73 -136
View File
@@ -1,213 +1,150 @@
// Copyright (c) 2014-2026 Savoir-faire Linux Inc.
// SPDX-License-Identifier: MIT
use crate::{ffi, Blob, InfoHash, PkId};
#![allow(dead_code)]
pub use crate::ffi::*;
use std::ffi::CString;
use std::fs;
use std::fs::File;
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: 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 new(data: &str) -> Box<PublicKey> {
unsafe { Box::from_raw(dht_publickey_import(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 unpack(&mut self, data: Vec<u8>) -> i32 {
unsafe { dht_publickey_unpack(&mut *self, data.as_ptr(), data.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 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 id(&self) -> InfoHash {
unsafe { ffi::dht_publickey_get_id(self.as_ptr()) }
unsafe { dht_publickey_get_id(self) }
}
pub fn long_id(&self) -> PkId {
unsafe { ffi::dht_publickey_get_long_id(self.as_ptr()) }
unsafe { dht_publickey_get_long_id(self) }
}
pub fn check_signature(&self, data: impl AsRef<[u8]>, signature: impl AsRef<[u8]>) -> bool {
let data = data.as_ref();
let signature = signature.as_ref();
pub fn check_signature(&self, data: &str, signature: &str) -> bool {
let data = CString::new(data).unwrap();
let signature = CString::new(signature).unwrap();
unsafe {
ffi::dht_publickey_check_signature(
self.as_ptr(),
data.as_ptr().cast(),
data.len(),
signature.as_ptr().cast(),
signature.len(),
dht_publickey_check_signature(
self,
data.as_ptr(),
data.as_bytes().len(),
signature.as_ptr(),
signature.as_bytes().len(),
)
}
}
pub fn encrypt(&self, data: impl AsRef<[u8]>) -> io::Result<Box<Blob>> {
let data = data.as_ref();
pub fn encrypt(&self, data: &str) -> Box<Blob> {
let data = CString::new(data).unwrap();
unsafe {
Blob::from_raw(ffi::dht_publickey_encrypt(
self.as_ptr(),
data.as_ptr().cast(),
data.len(),
Box::from_raw(dht_publickey_encrypt(
self,
data.as_ptr(),
data.as_bytes().len(),
))
}
}
}
impl PrivateKey {
pub fn new(key_length_bits: u32) -> io::Result<Box<PrivateKey>> {
unsafe { Self::from_raw(ffi::dht_privatekey_generate(key_length_bits)) }
impl Drop for PublicKey {
fn drop(&mut self) {
unsafe { dht_publickey_delete(&mut *self) }
}
}
pub fn new_ec() -> io::Result<Box<PrivateKey>> {
unsafe { Self::from_raw(ffi::dht_privatekey_generate_ec()) }
impl PrivateKey {
pub fn new(key_length_bits: u32) -> Box<PrivateKey> {
unsafe { Box::from_raw(dht_privatekey_generate(key_length_bits)) }
}
pub fn import(file: &str, password: &str) -> io::Result<Box<PrivateKey>> {
Self::from_bytes(&fs::read(file)?, password)
let mut f = File::open(file)?;
let mut buffer = Vec::new();
f.read_to_end(&mut buffer)?;
Ok(PrivateKey::from_bytes(&buffer, password))
}
pub fn from_bytes(buffer: &[u8], password: &str) -> io::Result<Box<PrivateKey>> {
let password = CString::new(password)?;
pub fn from_bytes(buffer: &Vec<u8>, password: &str) -> Box<PrivateKey> {
unsafe {
Self::from_raw(ffi::dht_privatekey_import(
buffer.as_ptr(),
Box::from_raw(dht_privatekey_import(
(&*buffer).as_ptr(),
buffer.len(),
password.as_ptr(),
password.as_ptr() as *const i8,
))
}
}
pub fn public_key(&self) -> Box<PublicKey> {
unsafe {
PublicKey::from_raw(ffi::dht_privatekey_get_publickey(self.as_ptr()))
.expect("private key has a public key")
}
unsafe { Box::from_raw(dht_privatekey_get_publickey(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 Drop for PrivateKey {
fn drop(&mut self) {
unsafe { dht_privatekey_delete(&mut *self) }
}
}
impl DhtCertificate {
pub fn import(file: &str) -> io::Result<Box<DhtCertificate>> {
Self::from_bytes(&fs::read(file)?)
let mut f = File::open(file)?;
let mut buffer = Vec::new();
f.read_to_end(&mut buffer)?;
Ok(DhtCertificate::from_bytes(&buffer))
}
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_bytes(buffer: &Vec<u8>) -> 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 from_slice(buffer: &str) -> Box<DhtCertificate> {
unsafe { Box::from_raw(dht_certificate_import((&*buffer).as_ptr(), buffer.len())) }
}
pub fn id(&self) -> InfoHash {
unsafe { ffi::dht_certificate_get_id(self.as_ptr()) }
unsafe { dht_certificate_get_id(&*self) }
}
pub fn long_id(&self) -> PkId {
unsafe { ffi::dht_certificate_get_long_id(self.as_ptr()) }
unsafe { dht_certificate_get_long_id(&*self) }
}
pub fn publickey(&self) -> Box<PublicKey> {
unsafe {
PublicKey::from_raw(ffi::dht_certificate_get_publickey(self.as_ptr()))
.expect("certificate has a public key")
}
unsafe { Box::from_raw(dht_certificate_get_publickey(&*self)) }
}
}
pub struct DhtIdentity {
pub privatekey: Box<PrivateKey>,
pub certificate: Box<DhtCertificate>,
impl Drop for DhtCertificate {
fn drop(&mut self) {
unsafe { dht_certificate_delete(&mut *self) }
}
}
impl DhtIdentity {
pub fn new(common_name: &str) -> io::Result<Self> {
Self::generate_inner(common_name, None, false)
pub fn new(common_name: &str) -> DhtIdentity {
unsafe { DhtIdentity::generate(common_name, Box::from_raw(ptr::null_mut())) }
}
pub fn new_ec(common_name: &str) -> io::Result<Self> {
Self::generate_inner(common_name, None, true)
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 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)?,
})
}
impl Drop for DhtIdentity {
fn drop(&mut self) {
unsafe { dht_identity_delete(&mut *self) }
}
}
+149 -699
View File
@@ -1,712 +1,257 @@
// Copyright (c) 2014-2026 Savoir-faire Linux Inc.
// SPDX-License-Identifier: MIT
#![allow(dead_code)]
use libc::c_void;
use std::ffi::CString;
use std::io;
use std::panic::{catch_unwind, AssertUnwindSafe};
use std::ptr;
use crate::crypto::{DhtCertificate, DhtIdentity, PrivateKey, PublicKey};
use crate::{ffi, InfoHash, PkId, Value};
pub use crate::ffi::*;
use os_socketaddr::OsSocketAddr;
use std::net::SocketAddr;
use std::sync::Mutex;
native_handle!(DhtRunner, dht_runner_delete);
native_handle!(OpToken, dht_op_token_delete);
native_handle!(PushToken, dht_push_token_delete);
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum NodeStatus {
Disconnected,
Connecting,
Connected,
}
impl NodeStatus {
fn from_raw(value: i32) -> Self {
match value {
1 => Self::Connecting,
2 => Self::Connected,
_ => Self::Disconnected,
}
}
}
pub struct QueryResult {
pub id: Option<u64>,
pub value_type: Option<u16>,
pub seq: Option<u16>,
pub owner: Option<InfoHash>,
pub user_type: Option<Vec<u8>>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PushNotificationResult {
PutRefresh,
ListenRefresh,
Values,
ValuesExpired,
IgnoredWrongSession,
IgnoredNoOp,
IgnoredStopped,
IgnoredDisabled,
Error,
}
pub struct PushNotification {
token: Box<PushToken>,
result: Option<PushNotificationResult>,
}
impl PushNotification {
pub fn poll(&mut self) -> io::Result<Option<PushNotificationResult>> {
if self.result.is_some() {
return Ok(self.result);
}
let mut outcome = 0;
let code = unsafe { ffi::dht_push_token_poll(self.token.as_ptr(), &mut outcome) };
if code == libc::EAGAIN {
return Ok(None);
}
result(code)?;
self.result = Some(match outcome {
0 => PushNotificationResult::PutRefresh,
1 => PushNotificationResult::ListenRefresh,
2 => PushNotificationResult::Values,
3 => PushNotificationResult::ValuesExpired,
128 => PushNotificationResult::IgnoredWrongSession,
129 => PushNotificationResult::IgnoredNoOp,
130 => PushNotificationResult::IgnoredStopped,
131 => PushNotificationResult::IgnoredDisabled,
_ => PushNotificationResult::Error,
});
Ok(self.result)
}
}
type StatusCallback = Box<dyn FnMut(NodeStatus, NodeStatus) + Send>;
type CertificateStore = Box<dyn FnMut(&PkId) -> Vec<Box<DhtCertificate>> + Send>;
struct ContextCallbacks {
status: Option<StatusCallback>,
store: Option<CertificateStore>,
}
pub struct DhtNodeConfig {
pub node_id: InfoHash,
pub network: u32,
pub is_bootstrap: bool,
pub maintain_storage: bool,
persist_path: Option<CString>,
}
pub struct DhtSecureConfig {
pub node_config: DhtNodeConfig,
pub id: Option<DhtIdentity>,
}
pub struct DhtRunnerConfig {
pub dht_config: DhtSecureConfig,
pub threaded: bool,
proxy_server: Option<CString>,
push_node_id: Option<CString>,
push_token: Option<CString>,
push_topic: Option<CString>,
push_platform: Option<CString>,
proxy_user_agent: Option<CString>,
pub peer_discovery: bool,
pub peer_publish: bool,
pub server_ca: Option<Box<DhtCertificate>>,
pub client_identity: Option<DhtIdentity>,
pub log: bool,
status_callback: Option<StatusCallback>,
certificate_store: Option<CertificateStore>,
}
fn string_ptr(value: &Option<CString>) -> *const libc::c_char {
value.as_ref().map_or(ptr::null(), |value| value.as_ptr())
}
macro_rules! string_setting {
($method:ident, $field:ident) => {
pub fn $method(&mut self, value: &str) -> io::Result<()> {
self.$field = Some(CString::new(value)?);
Ok(())
}
};
}
impl DhtRunnerConfig {
pub fn new() -> Box<DhtRunnerConfig> {
Box::new(DhtRunnerConfig {
let mut config: Box<DhtRunnerConfig> = Box::new(DhtRunnerConfig {
dht_config: DhtSecureConfig {
node_config: DhtNodeConfig {
node_id: InfoHash::new(),
network: 0,
is_bootstrap: false,
maintain_storage: false,
persist_path: None,
persist_path: ptr::null(),
},
id: DhtIdentity {
privatekey: ptr::null_mut(),
certificate: ptr::null_mut(),
},
id: None,
},
threaded: true,
proxy_server: None,
push_node_id: None,
push_token: None,
push_topic: None,
push_platform: None,
proxy_user_agent: None,
threaded: false,
proxy_server: ptr::null(),
push_node_id: ptr::null(),
push_token: ptr::null(),
push_topic: ptr::null(),
push_platform: ptr::null(),
proxy_user_agent: ptr::null(),
peer_discovery: false,
peer_publish: false,
server_ca: None,
client_identity: None,
log: false,
status_callback: None,
certificate_store: None,
})
server_ca: ptr::null_mut(),
client_identity: DhtIdentity {
privatekey: ptr::null_mut(),
certificate: ptr::null_mut(),
},
log: true,
});
unsafe {
dht_runner_config_default(&mut *config);
}
config
}
string_setting!(set_proxy_server, proxy_server);
string_setting!(set_push_node_id, push_node_id);
string_setting!(set_push_token, push_token);
string_setting!(set_push_topic, push_topic);
string_setting!(set_push_platform, push_platform);
string_setting!(set_proxy_user_agent, proxy_user_agent);
pub fn set_status_callback(
&mut self,
callback: impl FnMut(NodeStatus, NodeStatus) + Send + 'static,
) {
self.status_callback = Some(Box::new(callback));
pub fn set_proxy_server(&mut self, proxy_server: &str) {
self.proxy_server = CString::new(proxy_server).unwrap().into_raw();
}
pub fn set_certificate_store(
&mut self,
callback: impl FnMut(&PkId) -> Vec<Box<DhtCertificate>> + Send + 'static,
) {
self.certificate_store = Some(Box::new(callback));
pub fn set_push_node_id(&mut self, push_node_id: &str) {
self.push_node_id = CString::new(push_node_id).unwrap().into_raw();
}
pub fn set_push_token(&mut self, push_token: &str) {
self.push_token = CString::new(push_token).unwrap().into_raw();
}
pub fn set_push_topic(&mut self, push_topic: &str) {
self.push_topic = CString::new(push_topic).unwrap().into_raw();
}
pub fn set_push_platform(&mut self, push_platform: &str) {
self.push_platform = CString::new(push_platform).unwrap().into_raw();
}
pub fn set_proxy_user_agent(&mut self, proxy_user_agent: &str) {
self.proxy_user_agent = CString::new(proxy_user_agent).unwrap().into_raw();
}
pub fn set_identity(&mut self, certificate: Box<DhtCertificate>, privatekey: Box<PrivateKey>) {
self.dht_config.id = Some(DhtIdentity {
certificate,
privatekey,
});
}
fn raw(&self) -> ffi::DhtRunnerConfig {
let node = &self.dht_config.node_config;
let empty_identity = || ffi::DhtIdentity {
privatekey: ptr::null_mut(),
certificate: ptr::null_mut(),
};
ffi::DhtRunnerConfig {
dht_config: ffi::DhtSecureConfig {
node_config: ffi::DhtNodeConfig {
node_id: InfoHash { d: node.node_id.d },
network: node.network,
is_bootstrap: node.is_bootstrap,
maintain_storage: node.maintain_storage,
persist_path: string_ptr(&node.persist_path),
},
id: self
.dht_config
.id
.as_ref()
.map_or_else(empty_identity, DhtIdentity::raw),
},
threaded: self.threaded,
proxy_server: string_ptr(&self.proxy_server),
push_node_id: string_ptr(&self.push_node_id),
push_token: string_ptr(&self.push_token),
push_topic: string_ptr(&self.push_topic),
push_platform: string_ptr(&self.push_platform),
proxy_user_agent: string_ptr(&self.proxy_user_agent),
peer_discovery: self.peer_discovery,
peer_publish: self.peer_publish,
server_ca: self
.server_ca
.as_ref()
.map_or(ptr::null_mut(), |certificate| certificate.as_ptr()),
client_identity: self
.client_identity
.as_ref()
.map_or_else(empty_identity, DhtIdentity::raw),
log: self.log,
}
self.dht_config.id.privatekey = Box::into_raw(privatekey);
self.dht_config.id.certificate = Box::into_raw(certificate);
}
}
impl DhtNodeConfig {
string_setting!(set_persist_path, persist_path);
pub fn set_persist_path(&mut self, persist_path: &str) {
self.persist_path = CString::new(persist_path).unwrap().into_raw();
}
}
struct GetHandler {
get_cb: Box<dyn FnMut(Box<Value>) -> bool + Send>,
done_cb: Box<dyn FnOnce(bool) + Send>,
struct GetHandler<'a> {
get_cb: &'a mut (dyn FnMut(Box<Value>) -> bool),
done_cb: &'a mut (dyn FnMut(bool)),
}
impl GetHandler {
impl<'a> GetHandler<'a> {
fn get_cb(&mut self, v: Box<Value>) -> bool {
(self.get_cb)(v)
}
}
fn callback<T>(fallback: T, operation: impl FnOnce() -> T) -> T {
catch_unwind(AssertUnwindSafe(operation)).unwrap_or(fallback)
}
fn result(code: i32) -> io::Result<()> {
if code == 0 {
Ok(())
} else {
Err(io::Error::from_raw_os_error(code))
fn done_cb(&mut self, ok: bool) {
(self.done_cb)(ok)
}
}
extern "C" fn get_handler_cb(value: *const ffi::Value, ptr: *mut c_void) -> bool {
extern "C" fn get_handler_cb(v: *mut Value, ptr: *mut c_void) -> bool {
if ptr.is_null() {
return true;
}
callback(false, || unsafe {
unsafe {
let handler = ptr as *mut GetHandler;
(*handler).get_cb(Value::copy_from_received(value).expect("value copy failed"))
})
(*handler).get_cb((*v).boxed())
}
}
extern "C" fn done_handler_cb(ok: bool, ptr: *mut c_void) {
callback((), || unsafe {
unsafe {
let handler = Box::from_raw(ptr as *mut GetHandler);
(handler.done_cb)(ok)
})
(*handler.done_cb)(ok)
}
}
struct PutHandler {
done_cb: Box<dyn FnOnce(bool) + Send>,
struct PutHandler<'a> {
done_cb: &'a mut (dyn FnMut(bool)),
}
impl<'a> PutHandler<'a> {
fn done_cb(&mut self, ok: bool) {
(self.done_cb)(ok)
}
}
extern "C" fn put_handler_done(ok: bool, ptr: *mut c_void) {
callback((), || unsafe {
unsafe {
let handler = Box::from_raw(ptr as *mut PutHandler);
(handler.done_cb)(ok)
})
(*handler.done_cb)(ok)
}
}
struct ListenHandler {
cb: Box<dyn FnMut(Box<Value>, bool) -> bool + Send>,
struct ListenHandler<'a> {
cb: &'a mut (dyn FnMut(Box<Value>, bool) -> bool),
}
impl ListenHandler {
impl<'a> ListenHandler<'a> {
fn cb(&mut self, v: Box<Value>, expired: bool) -> bool {
(self.cb)(v, expired)
}
}
extern "C" fn listen_handler(value: *const ffi::Value, expired: bool, ptr: *mut c_void) -> bool {
callback(false, || unsafe {
extern "C" fn listen_handler(v: *mut Value, expired: bool, ptr: *mut c_void) -> bool {
unsafe {
let handler = ptr as *mut ListenHandler;
(*handler).cb(
Value::copy_from_received(value).expect("value copy failed"),
expired,
)
})
(*handler).cb((*v).boxed(), expired)
}
}
extern "C" fn listen_handler_done(ptr: *mut c_void) {
callback((), || unsafe {
drop(Box::from_raw(ptr as *mut ListenHandler));
})
unsafe {
Box::from_raw(ptr as *mut ListenHandler);
}
}
extern "C" fn shutdown_handler(ptr: *mut c_void) {
callback((), || unsafe {
let handler = Box::from_raw(ptr as *mut Box<dyn FnOnce() + Send>);
handler();
})
}
extern "C" fn context_released(data: *mut c_void) {
callback((), || unsafe {
drop(Box::from_raw(data as *mut Mutex<ContextCallbacks>));
});
}
extern "C" fn status_handler(ipv4: i32, ipv6: i32, data: *mut c_void) {
callback((), || unsafe {
let mut handlers = (*(data as *mut Mutex<ContextCallbacks>))
.lock()
.unwrap_or_else(|error| error.into_inner());
if let Some(handler) = &mut handlers.status {
handler(NodeStatus::from_raw(ipv4), NodeStatus::from_raw(ipv6));
}
});
}
extern "C" fn certificate_store_handler(
id: *const PkId,
emit: ffi::CertificateCallback,
emit_data: *mut c_void,
data: *mut c_void,
) {
callback((), || unsafe {
let mut handlers = (*(data as *mut Mutex<ContextCallbacks>))
.lock()
.unwrap_or_else(|error| error.into_inner());
if let Some(handler) = &mut handlers.store {
for certificate in handler(&*id) {
emit(certificate.as_ptr(), emit_data);
}
}
});
}
struct QueryHandler {
callback: Box<dyn FnMut(QueryResult) -> bool + Send>,
done: Box<dyn FnOnce(bool) + Send>,
}
extern "C" fn query_handler(fields: *const ffi::QueryResult, data: *mut c_void) -> bool {
callback(false, || unsafe {
let fields = &*fields;
let user_type = if fields.fields & 16 != 0 {
Some(if fields.user_type.size == 0 {
Vec::new()
} else {
std::slice::from_raw_parts(fields.user_type.data, fields.user_type.size).to_vec()
})
} else {
None
};
let result = QueryResult {
id: (fields.fields & 1 != 0).then_some(fields.id),
value_type: (fields.fields & 2 != 0).then_some(fields.value_type),
seq: (fields.fields & 4 != 0).then_some(fields.seq),
owner: (fields.fields & 8 != 0).then_some(InfoHash { d: fields.owner.d }),
user_type,
};
((*(data as *mut QueryHandler)).callback)(result)
})
}
extern "C" fn query_done(ok: bool, data: *mut c_void) {
callback((), || unsafe {
let handler = Box::from_raw(data as *mut QueryHandler);
(handler.done)(ok);
});
}
type CertificateCompletion = Box<dyn FnOnce(Option<Box<DhtCertificate>>) + Send>;
struct CertificateHandler {
callback: Option<CertificateCompletion>,
}
extern "C" fn certificate_handler(certificate: *const ffi::DhtCertificate, data: *mut c_void) {
callback((), || unsafe {
let handler = &mut *(data as *mut CertificateHandler);
if let Some(done) = handler.callback.take() {
done(DhtCertificate::from_raw(ffi::dht_certificate_ref(certificate)).ok());
}
});
}
extern "C" fn certificate_released(data: *mut c_void) {
callback((), || unsafe {
let mut handler = Box::from_raw(data as *mut CertificateHandler);
if let Some(done) = handler.callback.take() {
done(None);
}
});
}
#[allow(clippy::boxed_local)]
impl DhtRunner {
pub fn new() -> Box<DhtRunner> {
unsafe { Self::from_raw(ffi::dht_runner_new()).expect("runner allocation failed") }
unsafe { Box::from_raw(dht_runner_new()) }
}
pub fn run(&mut self, port: u16) -> io::Result<()> {
result(unsafe { ffi::dht_runner_run(self.as_ptr(), port) })
pub fn run(&mut self, port: u16) {
unsafe { dht_runner_run(&mut *self, port) }
}
pub fn run_config(&mut self, port: u16, config: Box<DhtRunnerConfig>) -> io::Result<()> {
self.run_bind("0.0.0.0", "::", port, config)
pub fn run_config(&mut self, port: u16, config: Box<DhtRunnerConfig>) {
unsafe { dht_runner_run_config(&mut *self, port, &*config) }
}
pub fn run_bind(
&mut self,
ipv4: &str,
ipv6: &str,
port: u16,
mut config: Box<DhtRunnerConfig>,
) -> io::Result<()> {
let ipv4 = CString::new(ipv4)?;
let ipv6 = CString::new(ipv6)?;
let context = ffi::DhtRunnerContext {
status_cb: config
.status_callback
.as_ref()
.map(|_| status_handler as extern "C" fn(_, _, _)),
certificate_store_cb: config
.certificate_store
.as_ref()
.map(|_| certificate_store_handler as extern "C" fn(_, _, _, _)),
released_cb: Some(context_released),
user_data: Box::into_raw(Box::new(Mutex::new(ContextCallbacks {
status: config.status_callback.take(),
store: config.certificate_store.take(),
})))
.cast(),
};
result(unsafe {
ffi::dht_runner_run_bind(
self.as_ptr(),
ipv4.as_ptr(),
ipv6.as_ptr(),
port,
&config.raw(),
&context,
pub fn bootstrap(&mut self, host: &str, service: u16) {
unsafe {
dht_runner_bootstrap(
&mut *self,
CString::new(host).unwrap().into_raw(),
CString::new(service.to_string()).unwrap().into_raw(),
)
})
}
pub fn bootstrap(&mut self, host: &str, service: u16) -> io::Result<()> {
let host = CString::new(host)?;
let service = CString::new(service.to_string())?;
unsafe { ffi::dht_runner_bootstrap(self.as_ptr(), host.as_ptr(), service.as_ptr()) }
Ok(())
}
}
pub fn node_id(&self) -> InfoHash {
unsafe { ffi::dht_runner_get_node_id(self.as_ptr()) }
unsafe { dht_runner_get_node_id(&*self) }
}
pub fn id(&self) -> InfoHash {
unsafe { ffi::dht_runner_get_id(self.as_ptr()) }
unsafe { dht_runner_get_id(&*self) }
}
pub fn public_key(&self) -> Option<Box<PublicKey>> {
unsafe { PublicKey::from_raw(ffi::dht_runner_get_publickey(self.as_ptr())).ok() }
}
pub fn register_certificate(&mut self, certificate: &DhtCertificate) -> io::Result<()> {
result(unsafe { ffi::dht_runner_register_certificate(self.as_ptr(), certificate.as_ptr()) })
}
pub fn find_certificate(
&mut self,
id: &PkId,
callback: impl FnOnce(Option<Box<DhtCertificate>>) + Send + 'static,
) {
let handler = Box::new(CertificateHandler {
callback: Some(Box::new(callback)),
});
unsafe {
ffi::dht_runner_find_certificate(
self.as_ptr(),
id,
certificate_handler,
certificate_released,
Box::into_raw(handler).cast(),
);
}
}
pub fn register_type(
&mut self,
id: u16,
name: &str,
expiration_seconds: u64,
insecure: bool,
) -> io::Result<()> {
let name = CString::new(name)?;
result(unsafe {
ffi::dht_runner_register_type(
self.as_ptr(),
id,
name.as_ptr(),
expiration_seconds,
insecure,
)
})
}
pub fn connectivity_changed(&mut self) -> io::Result<()> {
result(unsafe { ffi::dht_runner_connectivity_changed(self.as_ptr()) })
}
/// Deliver owned values that callbacks may mutate or move to another thread.
pub fn get(
pub fn get<'a>(
&mut self,
h: &InfoHash,
get_cb: impl FnMut(Box<Value>) -> bool + Send + 'static,
done_cb: impl FnOnce(bool) + Send + 'static,
get_cb: &'a mut (dyn FnMut(Box<Value>) -> bool),
done_cb: &'a mut (dyn FnMut(bool)),
) {
let handler = Box::new(GetHandler {
get_cb: Box::new(get_cb),
done_cb: Box::new(done_cb),
});
let handler = Box::new(GetHandler { get_cb, done_cb });
let handler = Box::into_raw(handler) as *mut c_void;
unsafe { ffi::dht_runner_get(self.as_ptr(), h, get_handler_cb, done_handler_cb, handler) }
unsafe { dht_runner_get(&mut *self, h, get_handler_cb, done_handler_cb, handler) }
}
pub fn get_filtered(
&mut self,
hash: &InfoHash,
filter: &str,
get_cb: impl FnMut(Box<Value>) -> bool + Send + 'static,
done_cb: impl FnOnce(bool) + Send + 'static,
) -> io::Result<()> {
let filter = CString::new(filter)?;
let handler = Box::new(GetHandler {
get_cb: Box::new(get_cb),
done_cb: Box::new(done_cb),
});
unsafe {
ffi::dht_runner_get_filtered(
self.as_ptr(),
hash,
filter.as_ptr(),
get_handler_cb,
done_handler_cb,
Box::into_raw(handler).cast(),
);
}
Ok(())
}
pub fn query(
&mut self,
hash: &InfoHash,
query: &str,
query_cb: impl FnMut(QueryResult) -> bool + Send + 'static,
done_cb: impl FnOnce(bool) + Send + 'static,
) -> io::Result<()> {
let query = CString::new(query)?;
let handler = Box::new(QueryHandler {
callback: Box::new(query_cb),
done: Box::new(done_cb),
});
unsafe {
ffi::dht_runner_query(
self.as_ptr(),
hash,
query.as_ptr(),
query_handler,
query_done,
Box::into_raw(handler).cast(),
);
}
Ok(())
}
pub fn put(
pub fn put<'a>(
&mut self,
h: &InfoHash,
v: Box<Value>,
done_cb: impl FnOnce(bool) + Send + 'static,
done_cb: &'a mut (dyn FnMut(bool)),
permanent: bool,
) {
let handler = Box::new(PutHandler {
done_cb: Box::new(done_cb),
});
let handler = Box::new(PutHandler { done_cb });
let handler = Box::into_raw(handler) as *mut c_void;
unsafe {
ffi::dht_runner_put(
self.as_ptr(),
h,
v.as_ptr(),
put_handler_done,
handler,
permanent,
)
}
unsafe { dht_runner_put(&mut *self, h, &*v, put_handler_done, handler, permanent) }
}
pub fn put_signed(
pub fn put_signed<'a>(
&mut self,
h: &InfoHash,
v: Box<Value>,
done_cb: impl FnOnce(bool) + Send + 'static,
done_cb: &'a mut (dyn FnMut(bool)),
permanent: bool,
) {
let handler = Box::new(PutHandler {
done_cb: Box::new(done_cb),
});
let handler = Box::new(PutHandler { done_cb });
let handler = Box::into_raw(handler) as *mut c_void;
unsafe {
ffi::dht_runner_put_signed(
self.as_ptr(),
h,
v.as_ptr(),
put_handler_done,
handler,
permanent,
)
}
unsafe { dht_runner_put_signed(&mut *self, h, &*v, put_handler_done, handler, permanent) }
}
pub fn put_encrypted(
pub fn put_encrypted<'a>(
&mut self,
h: &InfoHash,
to: &PkId,
v: Box<Value>,
done_cb: impl FnOnce(bool) + Send + 'static,
done_cb: &'a mut (dyn FnMut(bool)),
permanent: bool,
) {
let handler = Box::new(PutHandler {
done_cb: Box::new(done_cb),
});
let handler = Box::new(PutHandler { done_cb });
let handler = Box::into_raw(handler) as *mut c_void;
unsafe {
ffi::dht_runner_put_encrypted(
self.as_ptr(),
h,
to,
v.as_ptr(),
put_handler_done,
handler,
permanent,
)
dht_runner_put_encrypted(&mut *self, h, to, &*v, put_handler_done, handler, permanent)
}
}
pub fn cancel_put(&mut self, h: &InfoHash, vid: u64) {
unsafe { ffi::dht_runner_cancel_put(self.as_ptr(), h, vid) }
pub fn cancel_put<'a>(&mut self, h: &InfoHash, vid: u64) {
unsafe { dht_runner_cancel_put(&mut *self, h, vid) }
}
/// Registers an owned callback receiving ordinary owned `Box<Value>` objects.
/// It is released after cancellation or `join()`.
/// Borrowed callbacks cannot outlive their stack frame:
///
/// ```compile_fail
/// use opendht::{DhtRunner, InfoHash};
/// let mut runner = DhtRunner::new();
/// let mut callback = |_, _| true;
/// runner.listen(&InfoHash::new(), &mut callback);
/// ```
///
/// Captures must be safe to transfer to the DHT thread:
///
/// ```compile_fail
/// use opendht::{DhtRunner, InfoHash};
/// use std::rc::Rc;
/// let mut runner = DhtRunner::new();
/// let state = Rc::new(42);
/// runner.listen(&InfoHash::new(), move |_, _| { println!("{state}"); true });
/// ```
pub fn listen(
pub fn listen<'a>(
&mut self,
h: &InfoHash,
cb: impl FnMut(Box<Value>, bool) -> bool + Send + 'static,
) -> io::Result<Box<OpToken>> {
let handler = Box::new(ListenHandler { cb: Box::new(cb) });
cb: &'a mut (dyn FnMut(Box<Value>, bool) -> bool),
) -> Box<OpToken> {
let handler = Box::new(ListenHandler { cb });
let handler = Box::into_raw(handler) as *mut c_void;
unsafe {
OpToken::from_raw(ffi::dht_runner_listen(
self.as_ptr(),
Box::from_raw(dht_runner_listen(
&mut *self,
h,
listen_handler,
listen_handler_done,
@@ -716,137 +261,42 @@ impl DhtRunner {
}
pub fn cancel_listen(&mut self, h: &InfoHash, token: Box<OpToken>) {
unsafe { ffi::dht_runner_cancel_listen(self.as_ptr(), h, token.as_ptr()) }
unsafe { dht_runner_cancel_listen(&mut *self, h, &*token) }
}
pub fn listen_filtered(
pub fn shutdown(
&mut self,
hash: &InfoHash,
filter: &str,
cb: impl FnMut(Box<Value>, bool) -> bool + Send + 'static,
) -> io::Result<Box<OpToken>> {
let filter = CString::new(filter)?;
let handler = Box::new(ListenHandler { cb: Box::new(cb) });
unsafe {
OpToken::from_raw(ffi::dht_runner_listen_filtered(
self.as_ptr(),
hash,
filter.as_ptr(),
listen_handler,
listen_handler_done,
Box::into_raw(handler).cast(),
))
}
}
pub fn shutdown(&mut self, done_cb: impl FnOnce() + Send + 'static) {
let handler: Box<Box<dyn FnOnce() + Send>> = Box::new(Box::new(done_cb));
unsafe {
ffi::dht_runner_shutdown(
self.as_ptr(),
shutdown_handler,
Box::into_raw(handler).cast(),
)
}
}
pub fn join(&mut self) -> io::Result<()> {
result(unsafe { ffi::dht_runner_join(self.as_ptr()) })
}
pub fn loop_once(&mut self) -> io::Result<()> {
result(unsafe { ffi::dht_runner_loop(self.as_ptr()) })
}
pub fn is_running(&self) -> bool {
unsafe { ffi::dht_runner_is_running(self.as_ptr()) }
}
pub fn bound_port(&self, family: libc::sa_family_t) -> u16 {
unsafe { ffi::dht_runner_get_bound_port(self.as_ptr(), family) }
}
pub fn ping(&mut self, address: SocketAddr, done_cb: impl FnOnce(bool) + Send + 'static) {
let address = OsSocketAddr::from(address);
let handler = Box::new(PutHandler {
done_cb: Box::new(done_cb),
});
unsafe {
ffi::dht_runner_ping(
self.as_ptr(),
address.as_ptr().cast_mut(),
address.len(),
put_handler_done,
Box::into_raw(handler).cast(),
);
}
}
pub fn set_push_notification_token(&mut self, value: &str) -> io::Result<()> {
let value = CString::new(value)?;
result(unsafe {
ffi::dht_runner_set_push_notification_token(self.as_ptr(), value.as_ptr())
})
}
pub fn set_push_notification_topic(&mut self, value: &str) -> io::Result<()> {
let value = CString::new(value)?;
result(unsafe {
ffi::dht_runner_set_push_notification_topic(self.as_ptr(), value.as_ptr())
})
}
pub fn set_push_notification_platform(&mut self, value: &str) -> io::Result<()> {
let value = CString::new(value)?;
result(unsafe {
ffi::dht_runner_set_push_notification_platform(self.as_ptr(), value.as_ptr())
})
}
pub fn push_notification_received(
&mut self,
data: &[(&str, &str)],
) -> io::Result<PushNotification> {
let keys = data
.iter()
.map(|(key, _)| CString::new(*key))
.collect::<Result<Vec<_>, _>>()?;
let values = data
.iter()
.map(|(_, value)| CString::new(*value))
.collect::<Result<Vec<_>, _>>()?;
let key_ptrs = keys.iter().map(|key| key.as_ptr()).collect::<Vec<_>>();
let value_ptrs = values
.iter()
.map(|value| value.as_ptr())
.collect::<Vec<_>>();
let token = unsafe {
PushToken::from_raw(ffi::dht_runner_push_notification_received(
self.as_ptr(),
key_ptrs.as_ptr(),
value_ptrs.as_ptr(),
data.len(),
))?
};
Ok(PushNotification {
token,
result: None,
})
done_cb: extern "C" fn(bool, *mut c_void),
cb_user_data: *mut c_void,
) {
unsafe { dht_runner_shutdown(&mut *self, done_cb, cb_user_data) }
}
pub fn public_addresses(&self) -> Vec<SocketAddr> {
let mut result = Vec::new();
unsafe {
let original = ffi::dht_runner_get_public_address(self.as_ptr());
let mut addresses = original;
let mut addresses = dht_runner_get_public_address(&*self);
while !addresses.is_null() && !(*addresses).is_null() {
let length = match (**addresses).sa_family as i32 {
libc::AF_INET => std::mem::size_of::<libc::sockaddr_in>(),
libc::AF_INET6 => std::mem::size_of::<libc::sockaddr_in6>(),
_ => 0,
};
let sock = OsSocketAddr::copy_from_raw(*addresses, length as _).into_addr();
if let Some(address) = sock {
result.push(address);
let sock = (*(*addresses)).into_addr();
if sock.is_some() {
result.push(sock.unwrap());
}
addresses = addresses.add(1);
addresses = (addresses as usize + std::mem::size_of::<*mut OsSocketAddr>())
as *mut *mut OsSocketAddr;
}
ffi::dht_sockaddr_array_delete(original);
}
result
}
}
impl Drop for DhtRunner {
fn drop(&mut self) {
unsafe { dht_runner_delete(&mut *self) }
}
}
impl Drop for OpToken {
fn drop(&mut self) {
unsafe { dht_op_token_delete(&mut *self) }
}
}
+80 -245
View File
@@ -3,66 +3,71 @@
#![allow(dead_code)]
use libc::{c_char, c_int, c_uint, c_void, in_port_t, sa_family_t, size_t, sockaddr, socklen_t};
use libc::{c_char, c_int, c_uint, c_void, in_port_t, size_t};
use os_socketaddr::OsSocketAddr;
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],
_thread_bound: std::marker::PhantomData<*mut ()>,
pub struct Value
{
_opaque: [u8; 0]
}
#[repr(C)]
pub struct Blob {
_opaque: [u8; 0],
_thread_bound: std::marker::PhantomData<*mut ()>,
pub struct Blob
{
_opaque: [u8; 0]
}
#[repr(C, align(4))]
#[repr(C)]
#[derive(PartialEq)]
pub struct InfoHash {
pub struct InfoHash
{
pub d: [u8; HASH_LEN],
}
#[repr(C, align(4))]
pub struct PkId {
#[repr(C)]
pub struct PkId
{
pub d: [u8; PKID_LEN],
}
#[repr(C)]
pub struct PublicKey {
_opaque: [u8; 0],
_thread_bound: std::marker::PhantomData<*mut ()>,
pub struct PublicKey
{
_opaque: [u8; 0]
}
#[repr(C)]
pub struct PrivateKey {
_opaque: [u8; 0],
_thread_bound: std::marker::PhantomData<*mut ()>,
pub struct PrivateKey
{
_opaque: [u8; 0]
}
#[repr(C)]
pub struct OpToken {
_opaque: [u8; 0],
_thread_bound: std::marker::PhantomData<*mut ()>,
pub struct OpToken
{
_opaque: [u8; 0]
}
#[repr(C)]
pub struct DhtRunner {
_opaque: [u8; 0],
_thread_bound: std::marker::PhantomData<*mut ()>,
pub struct DhtRunner
{
_opaque: [u8; 0]
}
#[repr(C)]
pub struct DhtNodeConfig {
pub struct DhtNodeConfig
{
pub node_id: InfoHash,
pub network: u32,
pub is_bootstrap: bool,
@@ -71,25 +76,28 @@ pub struct DhtNodeConfig {
}
#[repr(C)]
pub struct DhtCertificate {
_opaque: [u8; 0],
_thread_bound: std::marker::PhantomData<*mut ()>,
pub struct DhtCertificate
{
_opaque: [u8; 0]
}
#[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,
@@ -105,55 +113,17 @@ 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")]
unsafe extern "C" {
extern {
// 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;
@@ -162,9 +132,8 @@ unsafe extern "C" {
// 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: *const u8, dat_size: size_t);
pub fn dht_infohash_get(h: *mut InfoHash, dat: *mut 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
@@ -173,56 +142,27 @@ unsafe extern "C" {
// 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_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_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_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_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_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_ec(
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_delete(ca: *mut DhtIdentity);
// dht::OpToken
@@ -234,142 +174,37 @@ unsafe extern "C" {
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) -> 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_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_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 "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_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_cancel_put(dht: *mut DhtRunner, h: *const InfoHash, vid: u64);
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);
}
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;
}
+1 -1
View File
@@ -42,7 +42,7 @@ impl InfoHash {
let mut h = InfoHash::new();
unsafe {
let c_str = CString::new(data).unwrap();
dht_infohash_from_hex_null(&mut h, c_str.as_ptr());
dht_infohash_from_hex(&mut h, c_str.as_ptr());
}
h
}
+2 -39
View File
@@ -4,34 +4,6 @@
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;
@@ -40,17 +12,8 @@ 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::{
DhtNodeConfig, DhtRunner, DhtRunnerConfig, DhtSecureConfig, NodeStatus, OpToken,
PushNotification, PushNotificationResult, QueryResult,
};
pub use dhtrunner::{ DhtRunner, DhtRunnerConfig, OpToken };
pub use infohash::InfoHash;
pub use pkid::PkId;
pub use value::{DataView, Value};
pub use value::{ DataView, Value };
-201
View File
@@ -1,201 +0,0 @@
//! 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>>,
})
}
}
+45 -144
View File
@@ -2,148 +2,82 @@
// SPDX-License-Identifier: MIT
#![allow(dead_code)]
use crate::crypto::PublicKey;
use crate::{ffi, InfoHash};
use std::ffi::{CStr, CString};
use crate::ffi::*;
use std::fmt;
use std::io;
use std::slice;
use std::ffi::{ CString, IntoStringError };
use std::str;
use std::slice;
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()
}
}
pub use crate::ffi::{DataView, Value};
impl 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 {
Self::from_raw(ffi::dht_value_with_id_new(data.as_ptr(), data.len(), id))
.expect("value allocation failed")
}
}
pub fn new(data: &str) -> Box<Value> {
Self::from_bytes(data.as_bytes())
}
pub fn from_bytes(data: &[u8]) -> Box<Value> {
unsafe {
Self::from_raw(ffi::dht_value_new(data.as_ptr(), data.len()))
.expect("value allocation failed")
Box::from_raw(dht_value_new(data.as_bytes().as_ptr(),
data.as_bytes().len()))
}
}
pub fn from_bytes(data: &Vec<u8>) -> Box<Value> {
unsafe {
Box::from_raw(dht_value_new(data.as_ptr(),
data.len()))
}
}
pub fn id(&self) -> u64 {
unsafe { ffi::dht_value_get_id(self.as_ptr()) }
unsafe {
dht_value_get_id(self)
}
}
fn dataview(&self) -> DataView {
unsafe { ffi::dht_value_get_data(self.as_ptr()) }
unsafe {
dht_value_get_data(self)
}
}
pub fn as_bytes(&self) -> Vec<u8> {
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) }
unsafe {
let dv = dht_value_get_data(self);
let slice = slice::from_raw_parts_mut(dv.data as *mut _, dv.size);
slice.to_vec()
}
}
pub fn boxed(&self) -> Box<Value> {
unsafe { Self::from_raw(ffi::dht_value_copy(self.as_ptr())).expect("value copy failed") }
pub fn boxed(&mut self) -> Box<Value> {
unsafe {
Box::from_raw(dht_value_ref(self))
}
}
pub fn recipient(&self) -> InfoHash {
unsafe { ffi::dht_value_get_recipient(self.as_ptr()) }
unsafe {
dht_value_get_recipient(self)
}
}
pub fn owner(&self) -> Option<Box<PublicKey>> {
unsafe { PublicKey::from_raw(ffi::dht_value_get_owner(self.as_ptr())).ok() }
unsafe {
let ptr = dht_value_get_owner(self);
if ptr.is_null() {
return None;
}
Some(Box::from_raw(ptr))
}
}
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 user_type(&self) -> Result<String, IntoStringError> {
unsafe {
CString::from_raw(dht_value_get_user_type(self) as *mut _).into_string()
}
}
}
pub fn set_user_type(&mut self, value: &str) -> io::Result<()> {
let value = CString::new(value)?;
impl Drop for Value {
fn drop(&mut self) {
unsafe {
ffi::dht_value_set_user_type(self.as_ptr(), value.as_ptr());
dht_value_unref(&mut *self)
}
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);
}
}
pub fn seq(&self) -> u16 {
unsafe { ffi::dht_value_get_seq(self.as_ptr()) }
}
pub fn set_seq(&mut self, value: u16) {
unsafe {
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(())
}
}
@@ -151,41 +85,8 @@ 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
-15
View File
@@ -36,19 +36,4 @@ base64_decode(std::string_view str)
return buffer;
}
std::vector<unsigned char>
base64_default_or_url_decode(std::string_view str)
{
std::vector<unsigned char> buffer(simdutf::maximal_binary_length_from_base64(str.data(), str.size()));
simdutf::result r = simdutf::base64_to_binary(str.data(),
str.size(),
(char*) buffer.data(),
simdutf::base64_options::base64_default_or_url);
if (r.error) {
throw std::invalid_argument(concat("Invalid base64 input: "sv, simdutf::error_to_string(r.error)));
} else {
buffer.resize(r.count);
}
return buffer;
}
} // namespace dht
+1 -8
View File
@@ -21,12 +21,5 @@ std::string base64_encode(const std::vector<unsigned char>& str);
* @return a base64-decoded buffer
*/
std::vector<unsigned char> base64_decode(std::string_view str);
/**
* Decode a buffer in base64 or url style base64 (including '-' '_' ).
*
* @param str the input buffer
* @return a base64-decoded buffer
*/
std::vector<unsigned char> base64_default_or_url_decode(std::string_view str);
} // namespace dht
} // namespace dht
+76 -41
View File
@@ -571,12 +571,6 @@ PublicKey::toString() const
}
if (err != GNUTLS_E_SUCCESS)
throw CryptoException(std::string("Could not print public key: ") + gnutls_strerror(err));
// GnuTLS reports the PEM size including the terminating null byte, which
// does not belong to the text and would end up embedded in whatever the
// string is copied into, such as the "owner" field of a serialized value.
ret.resize(sz);
if (not ret.empty() and ret.back() == '\0')
ret.pop_back();
return ret;
}
@@ -598,9 +592,7 @@ 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};
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);
int rc = gnutls_pubkey_verify_data2(pk, GNUTLS_SIGN_RSA_SHA512, 0, &dat, &sig);
return rc >= 0;
}
@@ -1296,16 +1288,14 @@ PrivateKey::generate(unsigned key_length, gnutls_pk_algorithm_t algo)
}
PrivateKey
PrivateKey::generateEC(gnutls_ecc_curve_t curve)
PrivateKey::generateEC()
{
gnutls_x509_privkey_t key;
if (gnutls_x509_privkey_init(&key) != GNUTLS_E_SUCCESS)
throw CryptoException("Unable to initialize private key.");
int err = gnutls_x509_privkey_generate(key,
GNUTLS_PK_EC,
curve == GNUTLS_ECC_CURVE_INVALID
? gnutls_sec_param_to_pk_bits(GNUTLS_PK_EC, GNUTLS_SEC_PARAM_ULTRA)
: GNUTLS_CURVE_TO_BITS(curve),
gnutls_sec_param_to_pk_bits(GNUTLS_PK_EC, GNUTLS_SEC_PARAM_ULTRA),
0);
if (err != GNUTLS_E_SUCCESS) {
gnutls_x509_privkey_deinit(key);
@@ -1406,11 +1396,8 @@ setRandomSerial(gnutls_x509_crt_t cert)
{
std::random_device rdev;
std::uniform_int_distribution<int64_t> dist {1};
const auto cert_serial = dist(rdev);
std::array<uint8_t, sizeof(cert_serial)> packed_serial;
for (size_t i = 0; i < packed_serial.size(); i++)
packed_serial[i] = cert_serial >> (8 * (packed_serial.size() - 1 - i));
gnutls_x509_crt_set_serial(cert, packed_serial.data(), packed_serial.size());
int64_t cert_serial = dist(rdev);
gnutls_x509_crt_set_serial(cert, &cert_serial, sizeof(cert_serial));
}
Certificate
@@ -1661,8 +1648,24 @@ OcspResponse::toString(const bool compact) const
gnutls_ocsp_cert_status_t
OcspResponse::getCertificateStatus() const
{
int ret;
unsigned int status;
int ret = gnutls_ocsp_resp_get_single(response, 0, NULL, NULL, NULL, NULL, &status, NULL, NULL, NULL, NULL);
// 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
if (ret < 0)
throw CryptoException(gnutls_strerror(ret));
return (gnutls_ocsp_cert_status_t) status;
@@ -1721,7 +1724,22 @@ 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;
@@ -1855,8 +1873,34 @@ RevocationList::getUpdateTime() const
return std::chrono::system_clock::from_time_t(t);
}
enum class Endian : uint32_t { LITTLE = 0, BIG = 1 };
template<typename T>
T
endian(T w, Endian endian = Endian::BIG)
{
// this gets optimized out into if (endian == host_endian) return w;
union {
uint64_t quad;
uint32_t islittle;
} t;
t.quad = 1;
if (t.islittle ^ (uint32_t) endian)
return w;
T r = 0;
// decent compilers will unroll this (gcc)
// or even convert straight into single bswap (clang)
for (size_t i = 0; i < sizeof(r); i++) {
r <<= 8;
r |= w & 0xff;
w >>= 8;
}
return r;
}
void
RevocationList::sign(const PrivateKey& key, const Certificate& ca, duration validity, uint64_t crlNumber)
RevocationList::sign(const PrivateKey& key, const Certificate& ca, duration validity)
{
if (auto err = gnutls_x509_crl_set_version(crl, 2))
throw CryptoException(std::string("Unable to set CRL version: ") + gnutls_strerror(err));
@@ -1866,28 +1910,19 @@ RevocationList::sign(const PrivateKey& key, const Certificate& ca, duration vali
throw CryptoException(std::string("Unable to set CRL update time: ") + gnutls_strerror(err));
if (auto err = gnutls_x509_crl_set_next_update(crl, std::chrono::system_clock::to_time_t(next_update)))
throw CryptoException(std::string("Unable to set CRL next update time: ") + gnutls_strerror(err));
uint64_t number {crlNumber};
uint64_t number {0};
size_t number_sz {sizeof(number)};
unsigned critical {0};
gnutls_x509_crl_get_number(crl, &number, &number_sz, &critical);
if (number == 0) {
// The CRL Number extension holds a DER integer of variable length, so it must be
// read as a big-endian byte string rather than straight into an uint64_t.
auto current = getNumber();
for (uint8_t byte : current)
number = (number << 8) | byte;
if (number == 0) {
// Initialize to a random number.
std::random_device rdev;
do {
std::generate_n((uint8_t*) &number, sizeof(number), std::bind(rand_byte, std::ref(rdev)));
} while (number == 0);
} else
number++;
}
// Store the number in network byte order.
uint8_t packedNumber[sizeof(number)];
for (size_t i = 0; i < sizeof(packedNumber); i++)
packedNumber[i] = (uint8_t) ((number >> (8 * (sizeof(packedNumber) - 1 - i))) & 0xff);
if (auto err = gnutls_x509_crl_set_number(crl, packedNumber, sizeof(packedNumber)))
throw CryptoException(std::string("Unable to set CRL number: ") + gnutls_strerror(err));
// initialize to a random number
number_sz = sizeof(number);
std::random_device rdev;
std::generate_n((uint8_t*) &number, sizeof(number), std::bind(rand_byte, std::ref(rdev)));
} else
number = endian(endian(number) + 1);
if (auto err = gnutls_x509_crl_set_number(crl, &number, sizeof(number)))
throw CryptoException(std::string("Unable to set CRL update time: ") + gnutls_strerror(err));
if (auto err = gnutls_x509_crl_sign2(crl, ca.cert, key.x509_key, GNUTLS_DIG_SHA512, 0))
throw CryptoException(std::string("Unable to sign certificate revocation list: ") + gnutls_strerror(err));
// to be able to actually use the CRL we need to serialize/deserialize it
Loaded 100 of 149 files, more files were not shown because too many files have changed in this diff. Show more