--- /dev/null
+Checks: '-*,clang-diagnostic-*,performance-*,bugprone-*,llvm-*,clang-analyzer-*,misc-*,-llvm-namespace-comment,-misc-unused-parameters,-misc-non-private-member-variables-in-classes,-llvm-header-guard,-llvm-include-order,-bugprone-suspicious-string-compare'
--- /dev/null
+{
+ "name": "C++",
+ "build": {
+ "dockerfile": "../docker/DockerfileDepsLlvm",
+ },
+ "runArgs": [ "--cap-add=SYS_PTRACE", "--security-opt", "seccomp=unconfined"],
+ "settings": {},
+ "extensions": [
+ "ms-vscode.cpptools"
+ ],
+ "forwardPorts": [4222],
+}
--- /dev/null
+name: C/C++ CI
+
+on: [push, pull_request]
+
+jobs:
+ build-ubuntu:
+ name: Ubuntu/GCC Autotools build
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - name: Dependencies
+ run: |
+ sudo apt install libncurses5-dev libreadline-dev nettle-dev libasio-dev \
+ libgnutls28-dev libuv1-dev cython3 python3-dev python3-setuptools libcppunit-dev libjsoncpp-dev \
+ autotools-dev autoconf libfmt-dev libhttp-parser-dev libmsgpack-dev libargon2-0-dev
+ - name: Configure
+ run: |
+ ./autogen.sh
+ ./configure
+ - name: Build
+ run: make
+
+ build-ubuntu-meson:
+ name: Ubuntu/GCC Meson build
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - name: Dependencies
+ run: |
+ sudo apt install meson ninja-build libncurses5-dev libreadline-dev nettle-dev libasio-dev \
+ libgnutls28-dev libuv1-dev cython3 python3-dev python3-setuptools libcppunit-dev libjsoncpp-dev \
+ libfmt-dev libhttp-parser-dev libmsgpack-dev libargon2-0-dev
+ - name: Configure
+ run: meson setup build .
+ - name: Build
+ run: cd build && ninja
+
+ build-ubuntu-minimal:
+ name: Ubuntu/GCC minimal build
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - name: Dependencies
+ run: |
+ sudo apt install libncurses5-dev libreadline-dev nettle-dev libfmt-dev \
+ libgnutls28-dev libcppunit-dev libmsgpack-dev libargon2-0-dev
+ - name: Configure
+ run: |
+ mkdir build && cd build
+ cmake .. -DCMAKE_INSTALL_PREFIX=/usr/local -DCMAKE_BUILD_TYPE=Debug \
+ -DOPENDHT_C=Off -DOPENDHT_TESTS=On -DOPENDHT_PEER_DISCOVERY=Off -DOPENDHT_PYTHON=Off \
+ -DOPENDHT_TOOLS=On -DOPENDHT_PROXY_SERVER=Off -DOPENDHT_PROXY_CLIENT=Off
+ - name: Build
+ run: cd build && make
+ - name: Unit tests
+ run: cd build && ./opendht_unit_tests
+
+ build-macos:
+ name: macOS/Clang build
+ runs-on: macos-11
+ steps:
+ - uses: actions/checkout@v3
+ - name: Dependencies
+ run: |
+ brew install msgpack-cxx asio gnutls nettle readline fmt jsoncpp argon2 openssl http-parser cppunit
+
+ - name: restinio
+ run: |
+ mkdir restinio && cd restinio
+ wget https://github.com/aberaud/restinio/archive/6fd08b65f6f15899dd0de3c801f6a5462b811c64.tar.gz
+ ls -l && tar -xzf 6fd08b65f6f15899dd0de3c801f6a5462b811c64.tar.gz
+ cd restinio-6fd08b65f6f15899dd0de3c801f6a5462b811c64/dev
+ cmake -DCMAKE_INSTALL_PREFIX=/usr/local -DRESTINIO_TEST=OFF -DRESTINIO_SAMPLE=OFF \
+ -DRESTINIO_INSTALL_SAMPLES=OFF -DRESTINIO_BENCH=OFF -DRESTINIO_INSTALL_BENCHES=OFF \
+ -DRESTINIO_FIND_DEPS=ON -DRESTINIO_ALLOW_SOBJECTIZER=Off -DRESTINIO_USE_BOOST_ASIO=none .
+ make -j8 && sudo make install
+ cd ../../.. && rm -rf restinio
+
+ - name: Configure
+ run: |
+ mkdir build && cd build
+ export PATH="/opt/homebrew/opt/openssl@3/bin:$PATH"
+ export LDFLAGS="-L/usr/local/opt/openssl@3/lib"
+ export CPPFLAGS="-I/usr/local/opt/openssl@3/include"
+ export PKG_CONFIG_PATH="/usr/local/opt/openssl@3/lib/pkgconfig"
+ cmake .. -DCMAKE_INSTALL_PREFIX=/usr/local -DCMAKE_BUILD_TYPE=Debug \
+ -DOPENDHT_C=On -DOPENDHT_TESTS=On -DOPENDHT_PEER_DISCOVERY=On -DOPENDHT_PYTHON=Off \
+ -DOPENDHT_TOOLS=On -DOPENDHT_PROXY_SERVER=On -DOPENDHT_PROXY_CLIENT=On -DOPENDHT_PUSH_NOTIFICATIONS=On
+
+ - name: Build
+ run: cd build && make
+
--- /dev/null
+name: Clang Static Analysis
+on: [push, pull_request]
+
+jobs:
+ clang-analyzer:
+ name: Clang Static Analysis
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - name: deps
+ run: |
+ sudo apt install libncurses5-dev libreadline-dev nettle-dev \
+ libgnutls28-dev libuv1-dev cython3 python3-dev python3-setuptools libcppunit-dev libjsoncpp-dev \
+ autotools-dev autoconf libfmt-dev libhttp-parser-dev libmsgpack-dev libargon2-0-dev libasio-dev \
+ llvm llvm-dev clang clang-tools && \
+ sudo apt remove gcc g++
+
+ - name: restinio
+ run: |
+ mkdir restinio && cd restinio \
+ && wget https://github.com/aberaud/restinio/archive/6fd08b65f6f15899dd0de3c801f6a5462b811c64.tar.gz \
+ && ls -l && tar -xzf 6fd08b65f6f15899dd0de3c801f6a5462b811c64.tar.gz \
+ && cd restinio-6fd08b65f6f15899dd0de3c801f6a5462b811c64/dev \
+ && cmake -DCMAKE_INSTALL_PREFIX=/usr -DRESTINIO_TEST=OFF -DRESTINIO_SAMPLE=OFF \
+ -DRESTINIO_INSTALL_SAMPLES=OFF -DRESTINIO_BENCH=OFF -DRESTINIO_INSTALL_BENCHES=OFF \
+ -DRESTINIO_FIND_DEPS=ON -DRESTINIO_ALLOW_SOBJECTIZER=Off -DRESTINIO_USE_BOOST_ASIO=none . \
+ && make -j8 && sudo make install \
+ && cd ../../.. && rm -rf restinio
+
+ - name: cmake
+ run: |
+ mkdir build && cd build && \
+ cmake .. -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Debug -DOPENDHT_C=On -DOPENDHT_PEER_DISCOVERY=On -DOPENDHT_PYTHON=Off -DOPENDHT_TOOLS=On -DOPENDHT_PROXY_SERVER=On -DOPENDHT_PROXY_CLIENT=On -DOPENDHT_PUSH_NOTIFICATIONS=On
+ - name: scan-build
+ run: cd build && scan-build --status-bugs make
--- /dev/null
+name: "CodeQL"
+
+on:
+ push:
+ branches: [ "master" ]
+ pull_request:
+ # The branches below must be a subset of the branches above
+ branches: [ "master" ]
+
+jobs:
+ analyze:
+ name: Analyze
+ runs-on: ubuntu-latest
+ container:
+ image: ghcr.io/savoirfairelinux/opendht/opendht-deps:latest
+
+ permissions:
+ actions: read
+ contents: read
+ security-events: write
+
+ strategy:
+ fail-fast: false
+ matrix:
+ language: [ 'cpp' ]
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v3
+
+ # Initializes the CodeQL tools for scanning.
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@v2
+ with:
+ languages: ${{ matrix.language }}
+
+ # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
+ # If this step fails, then you should remove it and run the build manually (see below)
+ - name: Autobuild
+ uses: github/codeql-action/autobuild@v2
+
+ # ℹ️ Command-line programs to run using the OS shell.
+ # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
+
+ # If the Autobuild fails above, remove it and uncomment the following three lines.
+ # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
+
+ # - run: |
+ # echo "Run, Build Application using script"
+ # ./location_of_script_within_repo/buildscript.sh
+
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@v2
--- /dev/null
+name: Release actions
+
+on:
+ push:
+ tags:
+ - 'v*'
+
+env:
+ REGISTRY: ghcr.io
+ IMAGE_NAME_DEPS: ${{ github.repository }}/opendht-deps
+ IMAGE_NAME: ${{ github.repository }}/opendht
+ IMAGE_NAME_DEPS_LLVM: ${{ github.repository }}/opendht-deps-llvm
+ IMAGE_NAME_LLVM: ${{ github.repository }}/opendht-llvm
+ IMAGE_NAME_DHTNODE: ${{ github.repository }}/dhtnode
+ IMAGE_NAME_ALPINE: ${{ github.repository }}/opendht-alpine
+ IMAGE_NAME_ALPINE_DEPS: ${{ github.repository }}/opendht-deps-alpine
+
+jobs:
+ build-and-push-deps-image:
+ name: Dependency Docker image
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ packages: write
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v3
+
+ - name: Log in to the Container registry
+ uses: docker/login-action@v2
+ with:
+ registry: ${{ env.REGISTRY }}
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Extract metadata (tags, labels) for Docker
+ id: meta
+ uses: docker/metadata-action@v4
+ with:
+ images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_DEPS }}
+
+ - name: Build and push Docker image
+ uses: docker/build-push-action@v3
+ with:
+ context: .
+ file: docker/DockerfileDeps
+ push: true
+ tags: ${{ steps.meta.outputs.tags }}
+ labels: ${{ steps.meta.outputs.labels }}
+
+ build-and-push-image:
+ name: OpenDHT Docker image
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ packages: write
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v3
+
+ - name: Log in to the Container registry
+ uses: docker/login-action@v2
+ with:
+ registry: ${{ env.REGISTRY }}
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Extract metadata (tags, labels) for Docker
+ id: meta
+ uses: docker/metadata-action@v4
+ with:
+ images: |
+ ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
+
+ - name: Build and push Docker image
+ uses: docker/build-push-action@v3
+ with:
+ context: .
+ file: docker/Dockerfile
+ 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
+ permissions:
+ contents: read
+ packages: write
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v3
+
+ - name: Log in to the Container registry
+ uses: docker/login-action@v2
+ with:
+ registry: ${{ env.REGISTRY }}
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Extract metadata (tags, labels) for Docker
+ id: meta
+ uses: docker/metadata-action@v4
+ with:
+ images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_DEPS_LLVM }}
+
+ - name: Build and push Docker image
+ uses: docker/build-push-action@v3
+ with:
+ context: .
+ file: docker/DockerfileDepsLlvm
+ push: true
+ tags: ${{ steps.meta.outputs.tags }}
+ labels: ${{ steps.meta.outputs.labels }}
+
+ build-and-push-image-llvm:
+ name: OpenDHT Docker image (LLVM)
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ packages: write
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v3
+
+ - name: Log in to the Container registry
+ uses: docker/login-action@v2
+ with:
+ registry: ${{ env.REGISTRY }}
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Extract metadata (tags, labels) for Docker
+ id: meta
+ uses: docker/metadata-action@v4
+ with:
+ images: |
+ ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_LLVM }}
+
+ - name: Build and push Docker image
+ uses: docker/build-push-action@v3
+ with:
+ context: .
+ file: docker/DockerfileLlvm
+ push: true
+ tags: ${{ steps.meta.outputs.tags }}
+ labels: ${{ steps.meta.outputs.labels }}
+
+ build-and-push-image-dhtnode:
+ name: dhtnode Docker image
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ packages: write
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v3
+
+ - name: Log in to the Container registry
+ uses: docker/login-action@v2
+ with:
+ registry: ${{ env.REGISTRY }}
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Extract metadata (tags, labels) for Docker
+ id: meta
+ uses: docker/metadata-action@v4
+ with:
+ images: |
+ ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_DHTNODE }}
+
+ - name: Build and push Docker image
+ uses: docker/build-push-action@v3
+ with:
+ context: .
+ file: docker/DockerfileDhtnode
+ push: true
+ tags: ${{ steps.meta.outputs.tags }}
+ labels: ${{ steps.meta.outputs.labels }}
+
+ build-and-push-image-alpine-deps:
+ name: Alpine Deps Docker image
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ packages: write
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v3
+
+ - name: Log in to the Container registry
+ uses: docker/login-action@v2
+ with:
+ registry: ${{ env.REGISTRY }}
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Extract metadata (tags, labels) for Docker
+ id: meta
+ uses: docker/metadata-action@v4
+ with:
+ images: |
+ ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_ALPINE_DEPS }}
+
+ - name: Build and push Docker image
+ uses: docker/build-push-action@v3
+ with:
+ context: .
+ file: docker/DockerfileDepsAlpine
+ push: true
+ tags: ${{ steps.meta.outputs.tags }}
+ labels: ${{ steps.meta.outputs.labels }}
+
+ build-and-push-image-alpine:
+ name: Alpine Docker image
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ packages: write
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v3
+
+ - name: Log in to the Container registry
+ uses: docker/login-action@v2
+ with:
+ registry: ${{ env.REGISTRY }}
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Extract metadata (tags, labels) for Docker
+ id: meta
+ uses: docker/metadata-action@v4
+ with:
+ images: |
+ ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_ALPINE }}
+
+ - name: Build and push Docker image
+ uses: docker/build-push-action@v3
+ with:
+ context: .
+ file: docker/DockerfileAlpine
+ push: true
+ tags: ${{ steps.meta.outputs.tags }}
+ labels: ${{ steps.meta.outputs.labels }}
+
+ build-python-wheel:
+ name: Release and build Python Wheel package
+ runs-on: ubuntu-latest
+ container: ghcr.io/savoirfairelinux/opendht/opendht-deps:latest
+
+ permissions:
+ contents: write
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v3
+
+ - name: cmake
+ run: |
+ mkdir build && cd build && \
+ cmake .. -DCMAKE_INSTALL_PREFIX=/usr \
+ -DCMAKE_BUILD_TYPE=Release \
+ -DOPENDHT_STATIC=Off \
+ -DOPENDHT_PYTHON=On \
+ -DOPENDHT_C=Off \
+ -DOPENDHT_PEER_DISCOVERY=On \
+ -DOPENDHT_TOOLS=Off \
+ -DOPENDHT_PROXY_SERVER=On \
+ -DOPENDHT_PROXY_CLIENT=On
+
+ - name: build
+ run: cd build && make dist
+
+ - uses: actions/upload-artifact@v3
+ with:
+ name: opendht-wheels-linux
+ path: build/python/dist/*.whl
+
+ - name: Create Release
+ id: create_release
+ uses: softprops/action-gh-release@v1
+ with:
+ draft: true
+ files: build/python/dist/*.whl
--- /dev/null
+# Compiled Object files
+*.slo
+*.lo
+*.o
+*.obj
+
+# Compiled Dynamic libraries
+*.so
+*.dylib
+*.dll
+
+# Compiled Static libraries
+*.lai
+*.la
+*.a
+*.lib
+
+# Executables
+*.exe
+*.out
+*.app
+**/dhtnode
+**/dhtchat
+**/dhtscanner
+
+# Autotools
+/ac
+Makefile
+Makefile.in
+/aclocal.m4
+/autom4te.cache/
+/config.*
+/configure
+/depcomp
+/install-sh
+/libtool
+/ltmain.sh
+/m4/
+/missing
+/stamp-h?
+.deps/
+.dirstamp
+.libs/
+*.l[ao]
+*~
+*.pc
+
+# KDevelop
+.kdev4/
+*.kdev4
+
+# VSCode
+.vscode/*
+
+# Eclipse CDT
+.cproject
+.project
+.autotools
+language.settings.xml
+
+# Python
+*.pyc
+*.stamp
+python/opendht.cpp
+python/setup.py
+
+# Doxygen
+doc/Doxyfile
+
+# git backup files
+*.orig
+
+# vim swap files
+*.swp
+*.swo
+
+# build dir
+build
+build_dev
+.DS_Store
--- /dev/null
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "dhtnode",
+ "type": "cppdbg",
+ "request": "launch",
+ "program": "${workspaceFolder}/build_dev/tools/dhtnode",
+ "args": ["-v", "-p", "4222"],
+ "cwd": "${workspaceFolder}/build_dev",
+ "environment": [],
+ "setupCommands": [
+ {
+ "description": "Enable pretty-printing for gdb",
+ "text": "-enable-pretty-printing",
+ "ignoreFailures": true
+ }
+ ],
+ "preLaunchTask": "build",
+ "linux": {
+ "MIMode": "gdb",
+ "externalConsole": false,
+ },
+ "osx": {
+ "MIMode": "lldb",
+ "externalConsole": true,
+ },
+ },
+ {
+ "name": "tests",
+ "type": "cppdbg",
+ "request": "launch",
+ "program": "${workspaceFolder}/build_dev/opendht_unit_tests",
+ "cwd": "${workspaceFolder}/build_dev",
+ "environment": [],
+ "setupCommands": [
+ {
+ "description": "Enable pretty-printing for gdb",
+ "text": "-enable-pretty-printing",
+ "ignoreFailures": true
+ }
+ ],
+ "preLaunchTask": "build",
+ "linux": {
+ "MIMode": "gdb",
+ "externalConsole": false,
+ },
+ "osx": {
+ "MIMode": "lldb",
+ "externalConsole": true,
+ },
+ }
+ ]
+}
--- /dev/null
+{
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "label": "build",
+ "type": "shell",
+ "options": {
+ "cwd": "${workspaceRoot}/build_dev"
+ },
+ "command": "make",
+ "args": ["-j4"],
+ "dependsOn": "cmake"
+ },
+ {
+ "label": "cmake",
+ "type": "shell",
+ "options": {
+ "cwd": "${workspaceRoot}/build_dev"
+ },
+ "command": "cmake",
+ "args": [
+ "${workspaceRoot}",
+ "-DCMAKE_EXPORT_COMPILE_COMMANDS=On",
+ "-DCMAKE_BUILD_TYPE=Debug",
+ "-DOPENDHT_SANITIZE=On",
+ "-DOPENDHT_PROXY_CLIENT=On",
+ "-DOPENDHT_PROXY_SERVER=On",
+ "-DOPENDHT_TESTS=On",
+ "-DOPENDHT_C=On"
+ ],
+ "dependsOn": "builddir"
+ },
+ {
+ "label": "builddir",
+ "type": "shell",
+ "options": {
+ "cwd": "${workspaceRoot}"
+ },
+ "command": "mkdir",
+ "args": [
+ "-p",
+ "${workspaceRoot}/build_dev"
+ ]
+ },
+ ]
+}
--- /dev/null
+cmake_minimum_required (VERSION 3.10..3.20)
+if(POLICY CMP0073)
+ cmake_policy(SET CMP0073 NEW)
+endif()
+if(POLICY CMP0074)
+ cmake_policy(SET CMP0074 NEW)
+endif()
+project (opendht)
+
+include(CMakePackageConfigHelpers)
+include(CMakeDependentOption)
+include(CheckIncludeFileCXX)
+include(FindPkgConfig)
+include(cmake/CheckAtomic.cmake)
+include(CTest)
+
+set (opendht_VERSION_MAJOR 3)
+set (opendht_VERSION_MINOR 0.0)
+set (opendht_VERSION ${opendht_VERSION_MAJOR}.${opendht_VERSION_MINOR})
+set (PACKAGE_VERSION ${opendht_VERSION})
+set (VERSION "${opendht_VERSION}")
+
+# Options
+option (BUILD_SHARED_LIBS "Build shared library" ON)
+CMAKE_DEPENDENT_OPTION (OPENDHT_STATIC "Build static library" OFF BUILD_SHARED_LIBS ON)
+option (OPENDHT_PYTHON "Build Python bindings" OFF)
+option (OPENDHT_TOOLS "Build DHT tools" ON)
+option (OPENDHT_SYSTEMD "Install systemd module" OFF)
+option (OPENDHT_SYSTEMD_UNIT_FILE_LOCATION "Where to install systemd unit file")
+option (OPENDHT_SANITIZE "Build with address sanitizer and stack protector" OFF)
+option (OPENDHT_PROXY_SERVER "Enable DHT proxy server, use Restinio and jsoncpp" OFF)
+option (OPENDHT_PUSH_NOTIFICATIONS "Enable push notifications support" OFF)
+option (OPENDHT_PROXY_SERVER_IDENTITY "Allow clients to use the node identity" OFF)
+option (OPENDHT_PROXY_CLIENT "Enable DHT proxy client, use Restinio and jsoncpp" OFF)
+option (OPENDHT_PROXY_OPENSSL "Build DHT proxy with OpenSSL" ON)
+CMAKE_DEPENDENT_OPTION(OPENDHT_HTTP "Build embedded http(s) client" OFF "NOT OPENDHT_PROXY_SERVER;NOT OPENDHT_PROXY_CLIENT" ON)
+option (OPENDHT_PEER_DISCOVERY "Enable multicast peer discovery" ON)
+option (OPENDHT_INDEX "Build DHT indexation feature" OFF)
+option (OPENDHT_TESTS_NETWORK "Enable unit tests that require network access" ON)
+option (OPENDHT_C "Build C bindings" OFF)
+
+find_package(Doxygen)
+option (OPENDHT_DOCUMENTATION "Create and install the HTML based API documentation (requires Doxygen)" ${DOXYGEN_FOUND})
+
+# Build flags
+set (CMAKE_CXX_STANDARD 17)
+set (CMAKE_CXX_STANDARD_REQUIRED on)
+
+# Dependencies
+if (NOT HAVE_CXX_ATOMICS_WITHOUT_LIB)
+ link_libraries (atomic)
+endif ()
+
+list (APPEND CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake")
+if (NOT MSVC)
+ set (THREADS_PREFER_PTHREAD_FLAG TRUE)
+ find_package (Threads)
+ find_package (PkgConfig REQUIRED)
+ pkg_search_module (GnuTLS REQUIRED IMPORTED_TARGET gnutls)
+ pkg_search_module (Nettle REQUIRED IMPORTED_TARGET nettle)
+ check_include_file_cxx(msgpack.hpp HAVE_MSGPACKCXX)
+ if (NOT HAVE_MSGPACKCXX)
+ find_package(msgpack QUIET CONFIG NAMES msgpack msgpackc-cxx)
+ if (NOT msgpack_FOUND)
+ find_package(msgpack REQUIRED CONFIG NAMES msgpack-cxx)
+ set(MSGPACK_TARGET "msgpack-cxx")
+ else()
+ set(MSGPACK_TARGET "msgpackc-cxx")
+ endif()
+ endif()
+ if (OPENDHT_TOOLS)
+ find_package (Readline 6 REQUIRED)
+ endif ()
+ pkg_search_module(argon2 REQUIRED IMPORTED_TARGET libargon2)
+ set(argon2_lib ", libargon2")
+ pkg_search_module(Jsoncpp IMPORTED_TARGET jsoncpp)
+ if (Jsoncpp_FOUND)
+ add_definitions(-DOPENDHT_JSONCPP)
+ set(jsoncpp_lib ", jsoncpp")
+ list (APPEND opendht_SOURCES
+ src/base64.h
+ src/base64.cpp
+ )
+ endif()
+
+ if (OPENDHT_HTTP OR OPENDHT_PEER_DISCOVERY)
+ find_path(ASIO_INCLUDE_DIR asio.hpp REQUIRED)
+ endif ()
+
+ find_package(fmt)
+
+ if (OPENDHT_HTTP)
+ find_package(Restinio REQUIRED)
+ find_library(HTTP_PARSER_LIBRARY http_parser)
+ add_library(http_parser SHARED IMPORTED)
+ set(http_parser_lib "-lhttp_parser")
+ if (NOT Jsoncpp_FOUND)
+ message(SEND_ERROR "Jsoncpp is required for DHT proxy support")
+ endif()
+ if (OPENDHT_PROXY_OPENSSL)
+ # https://cmake.org/cmake/help/latest/module/FindOpenSSL.html
+ pkg_search_module(OPENSSL REQUIRED IMPORTED_TARGET openssl)
+ if (OPENSSL_FOUND)
+ message(STATUS "Found OpenSSL ${OPENSSL_VERSION} ${OPENSSL_INCLUDE_DIRS}")
+ set(openssl_lib ", openssl")
+ else ()
+ message(SEND_ERROR "OpenSSL is required for DHT proxy as specified")
+ endif()
+ endif()
+ else ()
+ set(OPENDHT_PROXY_OPENSSL OFF)
+ endif ()
+else ()
+ set (WIN32_DEP_DIR ${PROJECT_SOURCE_DIR}/../)
+ include_directories(${WIN32_DEP_DIR}/../msvc/include) # SMP gnutls
+ include_directories(${WIN32_DEP_DIR}/argon2/include)
+ include_directories(${WIN32_DEP_DIR}/jsoncpp/include)
+ list (APPEND opendht_SOURCES
+ src/base64.h
+ src/base64.cpp
+ )
+ add_definitions(-DOPENDHT_JSONCPP)
+ include_directories(${WIN32_DEP_DIR}/msgpack-c/include)
+ if (OPENDHT_HTTP OR OPENDHT_PEER_DISCOVERY)
+ include_directories(
+ ${WIN32_DEP_DIR}/asio/asio/include
+ ${WIN32_DEP_DIR}/openssl/include
+ ${WIN32_DEP_DIR}/restinio/dev
+ ${WIN32_DEP_DIR}/fmt/include
+ ${WIN32_DEP_DIR}/http_parser
+ )
+ endif ()
+endif ()
+
+if (OPENDHT_HTTP OR OPENDHT_PEER_DISCOVERY)
+ add_definitions(-DASIO_STANDALONE)
+endif()
+
+if (NOT MSVC)
+ set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-return-type -Wno-deprecated -Wall -Wextra -Wnon-virtual-dtor -pedantic-errors -fvisibility=hidden")
+ if (OPENDHT_SANITIZE)
+ set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -fstack-protector-strong")
+ set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=address -fstack-protector-strong")
+ endif ()
+else ()
+ add_definitions(-D_WINSOCK_DEPRECATED_NO_WARNINGS
+ -D_CRT_SECURE_NO_WARNINGS
+ -DWIN32_LEAN_AND_MEAN
+ -DSTATIC_GETOPT
+ -DGNUTLS_INTERNAL_BUILD)
+ set(DISABLE_MSC_WARNINGS "/wd4101 /wd4244 /wd4267 /wd4273 /wd4804 /wd4834 /wd4996")
+ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${DISABLE_MSC_WARNINGS}")
+ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /MP")
+endif ()
+set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DMSGPACK_NO_BOOST -DMSGPACK_DISABLE_LEGACY_NIL -DMSGPACK_DISABLE_LEGACY_CONVERT")
+
+add_definitions(-DPACKAGE_VERSION="${opendht_VERSION}")
+
+if (ASIO_INCLUDE_DIR)
+ include_directories (SYSTEM "${ASIO_INCLUDE_DIR}")
+endif ()
+if (Restinio_INCLUDE_DIR)
+ include_directories (SYSTEM "${Restinio_INCLUDE_DIR}")
+endif ()
+include_directories (
+ ./
+ include/
+ include/opendht/
+ ${CMAKE_CURRENT_BINARY_DIR}/include/
+)
+
+# Install dirs
+include (GNUInstallDirs)
+set (prefix ${CMAKE_INSTALL_PREFIX})
+set (exec_prefix "\${prefix}")
+set (libdir "${CMAKE_INSTALL_FULL_LIBDIR}")
+set (includedir "${CMAKE_INSTALL_FULL_INCLUDEDIR}")
+set (bindir "${CMAKE_INSTALL_FULL_BINDIR}")
+set (sysconfdir "${CMAKE_INSTALL_FULL_SYSCONFDIR}")
+set (top_srcdir "${CMAKE_CURRENT_SOURCE_DIR}")
+
+# Sources
+list (APPEND opendht_SOURCES
+ src/utils.cpp
+ src/crypto.cpp
+ src/default_types.cpp
+ src/node.cpp
+ src/value.cpp
+ src/dht.cpp
+ src/op_cache.cpp
+ src/storage.h
+ src/listener.h
+ src/search.h
+ src/value_cache.h
+ src/op_cache.h
+ src/net.h
+ src/parsed_message.h
+ src/request.h
+ src/callbacks.cpp
+ src/routing_table.cpp
+ src/node_cache.cpp
+ src/network_engine.cpp
+ src/securedht.cpp
+ src/dhtrunner.cpp
+ src/log.cpp
+ src/network_utils.cpp
+ src/thread_pool.cpp
+)
+
+list (APPEND opendht_HEADERS
+ include/opendht/def.h
+ include/opendht/utils.h
+ include/opendht/sockaddr.h
+ include/opendht/rng.h
+ include/opendht/crypto.h
+ include/opendht/infohash.h
+ include/opendht/default_types.h
+ include/opendht/node.h
+ include/opendht/value.h
+ include/opendht/dht.h
+ include/opendht/dht_interface.h
+ include/opendht/callbacks.h
+ include/opendht/routing_table.h
+ include/opendht/node_cache.h
+ include/opendht/network_engine.h
+ include/opendht/scheduler.h
+ include/opendht/rate_limiter.h
+ include/opendht/securedht.h
+ include/opendht/log.h
+ include/opendht/logger.h
+ include/opendht/thread_pool.h
+ include/opendht/network_utils.h
+ include/opendht.h
+)
+
+if (OPENDHT_PEER_DISCOVERY)
+ list (APPEND opendht_SOURCES src/peer_discovery.cpp)
+ list (APPEND opendht_HEADERS include/opendht/peer_discovery.h)
+ add_definitions(-DOPENDHT_PEER_DISCOVERY)
+endif()
+
+if (OPENDHT_PYTHON)
+ message("Indexation enabled since it is required for Python support")
+ set(OPENDHT_INDEX ON)
+endif()
+if (OPENDHT_INDEX)
+ list (APPEND opendht_SOURCES src/indexation/pht.cpp)
+ list (APPEND opendht_HEADERS include/opendht/indexation/pht.h)
+ add_definitions(-DOPENDHT_INDEXATION)
+endif()
+
+if (OPENDHT_PROXY_SERVER)
+ add_definitions(-DOPENDHT_PROXY_SERVER)
+ if (OPENDHT_PROXY_SERVER_IDENTITY)
+ add_definitions(-DOPENDHT_PROXY_SERVER_IDENTITY)
+ endif()
+ list (APPEND opendht_HEADERS
+ include/opendht/dht_proxy_server.h
+ )
+ list (APPEND opendht_SOURCES
+ src/dht_proxy_server.cpp
+ )
+endif ()
+
+if (OPENDHT_PROXY_CLIENT)
+ add_definitions(-DOPENDHT_PROXY_CLIENT)
+ list (APPEND opendht_HEADERS
+ include/opendht/dht_proxy_client.h
+ )
+ list (APPEND opendht_SOURCES
+ src/dht_proxy_client.cpp
+ )
+endif ()
+
+if (OPENDHT_HTTP)
+ if (OPENDHT_PUSH_NOTIFICATIONS)
+ message("Using push notification")
+ add_definitions(-DOPENDHT_PUSH_NOTIFICATIONS)
+ endif ()
+ list (APPEND opendht_HEADERS
+ include/opendht/proxy.h
+ include/opendht/http.h
+ src/compat/os_cert.h
+ )
+ list (APPEND opendht_SOURCES
+ src/http.cpp
+ src/compat/os_cert.cpp
+ )
+endif ()
+
+if (MSVC)
+ list (APPEND opendht_HEADERS src/compat/msvc/unistd.h)
+endif ()
+
+# Targets
+if (MSVC)
+ if (OPENDHT_STATIC)
+ if (OPENDHT_TOOLS)
+ function (add_obj_lib name libfile)
+ add_library(${name} OBJECT IMPORTED)
+ set_property(TARGET ${name} PROPERTY IMPORTED_OBJECTS ${libfile})
+ endfunction ()
+ add_obj_lib (win32_json ${WIN32_DEP_DIR}/../msvc/lib/x64/lib_json.lib)
+ add_obj_lib (win32_gnutls ${WIN32_DEP_DIR}/../msvc/lib/x64/libgnutls.lib)
+ add_obj_lib (win32_argon2 ${WIN32_DEP_DIR}/argon2/vs2015/Argon2Ref/vs2015/build/Argon2Ref.lib)
+ list (APPEND obj_libs
+ $<TARGET_OBJECTS:win32_json>
+ $<TARGET_OBJECTS:win32_gnutls>
+ $<TARGET_OBJECTS:win32_argon2>
+ )
+ if (OPENDHT_HTTP)
+ add_obj_lib (win32_fmt ${WIN32_DEP_DIR}/fmt/msvc/Release/fmt.lib)
+ add_obj_lib (win32_http_parser ${WIN32_DEP_DIR}/http_parser/x64/Release/http-parser.lib)
+ add_obj_lib (win32_ssl ${WIN32_DEP_DIR}/openssl/libssl_static.lib)
+ add_obj_lib (win32_crypto ${WIN32_DEP_DIR}/openssl/libcrypto_static.lib)
+ list (APPEND obj_libs
+ $<TARGET_OBJECTS:win32_fmt>
+ $<TARGET_OBJECTS:win32_http_parser>
+ $<TARGET_OBJECTS:win32_ssl>
+ $<TARGET_OBJECTS:win32_crypto>
+ )
+ endif ()
+ else ()
+ list (APPEND win32_Libs
+ ${PROJECT_SOURCE_DIR}/../../msvc/lib/x64/libgnutls.lib
+ ${PROJECT_SOURCE_DIR}/../../msvc/lib/x64/lib_json.lib
+ ${PROJECT_SOURCE_DIR}/../argon2/vs2015/Argon2Ref/vs2015/build/Argon2Ref.lib
+ )
+ list (APPEND win32_Libs
+ ${PROJECT_SOURCE_DIR}/../fmt/msvc/Release/fmt.lib
+ ${PROJECT_SOURCE_DIR}/../http_parser/x64/Release/http-parser.lib
+ ${PROJECT_SOURCE_DIR}/../openssl/libssl.lib
+ ${PROJECT_SOURCE_DIR}/../openssl/libcrypto.lib
+ )
+ endif ()
+ set(CMAKE_STATIC_LINKER_FLAGS "${CMAKE_STATIC_LINKER_FLAGS} /ignore:4006")
+ endif()
+endif ()
+
+add_library (opendht
+ ${opendht_SOURCES}
+ ${opendht_HEADERS}
+ ${obj_libs}
+)
+set_target_properties (opendht PROPERTIES OUTPUT_NAME "opendht")
+
+if (NOT HAVE_MSGPACKCXX)
+ target_link_libraries(opendht PUBLIC ${MSGPACK_TARGET})
+endif()
+if (APPLE)
+ target_link_libraries(opendht PRIVATE "-framework CoreFoundation" "-framework Security")
+endif()
+if (MSVC)
+ if (OPENDHT_STATIC)
+ target_link_libraries(opendht PUBLIC ${Win32_STATIC_LIBRARIES} ${Win32_IMPORT_LIBRARIES})
+ set_target_properties (opendht PROPERTIES OUTPUT_NAME "libopendht")
+ endif()
+else()
+ target_link_libraries(opendht
+ PRIVATE
+ PkgConfig::argon2
+ PkgConfig::Nettle
+ ${HTTP_PARSER_LIBRARY}
+ PUBLIC
+ ${CMAKE_THREAD_LIBS_INIT}
+ PkgConfig::GnuTLS
+ fmt::fmt
+ )
+ if (Jsoncpp_FOUND)
+ target_link_libraries(opendht PUBLIC PkgConfig::Jsoncpp)
+ endif()
+ if (OPENDHT_PROXY_OPENSSL)
+ target_link_libraries(opendht PUBLIC PkgConfig::OPENSSL)
+ endif()
+endif()
+
+if (BUILD_SHARED_LIBS)
+ set_target_properties (opendht PROPERTIES IMPORT_SUFFIX "_import.lib")
+ set_target_properties (opendht PROPERTIES SOVERSION ${opendht_VERSION_MAJOR} VERSION ${opendht_VERSION})
+ target_compile_definitions(opendht PRIVATE OPENDHT_BUILD)
+endif ()
+install (TARGETS opendht DESTINATION ${CMAKE_INSTALL_LIBDIR} EXPORT opendht)
+
+if (OPENDHT_C)
+ add_library (opendht-c
+ c/opendht.cpp
+ c/opendht_c.h
+ )
+ target_compile_definitions(opendht-c PRIVATE OPENDHT_C_BUILD)
+ target_link_libraries(opendht-c PRIVATE opendht)
+ set_target_properties (opendht-c PROPERTIES SOVERSION ${opendht_VERSION_MAJOR} VERSION ${opendht_VERSION})
+ install (TARGETS opendht-c DESTINATION ${CMAKE_INSTALL_LIBDIR} EXPORT opendht-c)
+
+ # PkgConfig module
+ configure_file (
+ opendht-c.pc.in
+ opendht-c.pc
+ @ONLY
+ )
+ install (FILES ${CMAKE_CURRENT_BINARY_DIR}/opendht-c.pc DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig)
+ install (FILES c/opendht_c.h DESTINATION ${CMAKE_INSTALL_PREFIX}/include/opendht)
+endif ()
+
+if (OPENDHT_TOOLS)
+ add_subdirectory(tools)
+endif ()
+add_subdirectory(doc)
+
+if (OPENDHT_PYTHON)
+ add_subdirectory(python)
+endif ()
+
+# CMake module
+write_basic_package_version_file("${CMAKE_CURRENT_BINARY_DIR}/opendhtConfigVersion.cmake"
+ VERSION ${opendht_VERSION}
+ COMPATIBILITY AnyNewerVersion
+)
+# PkgConfig module
+configure_file (
+ opendht.pc.in
+ opendht.pc
+ @ONLY
+)
+
+# Install targets
+install (DIRECTORY include DESTINATION ${CMAKE_INSTALL_PREFIX})
+install (FILES ${CMAKE_CURRENT_BINARY_DIR}/opendht.pc DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig)
+install (EXPORT opendht DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/opendht FILE opendhtConfig.cmake)
+install (FILES ${CMAKE_CURRENT_BINARY_DIR}/opendhtConfigVersion.cmake DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/opendht)
+
+# Unit tests
+if (BUILD_TESTING AND NOT MSVC)
+ pkg_search_module(Cppunit REQUIRED IMPORTED_TARGET cppunit)
+ # unit testing
+ list (APPEND test_FILES
+ tests/infohashtester.h
+ tests/infohashtester.cpp
+ tests/valuetester.h
+ tests/valuetester.cpp
+ tests/cryptotester.h
+ tests/cryptotester.cpp
+ tests/dhtrunnertester.h
+ tests/dhtrunnertester.cpp
+ tests/threadpooltester.h
+ tests/threadpooltester.cpp
+ )
+ if (OPENDHT_TESTS_NETWORK)
+ if (OPENDHT_PROXY_SERVER AND OPENDHT_PROXY_CLIENT)
+ list (APPEND test_FILES
+ tests/httptester.h
+ tests/httptester.cpp
+ tests/dhtproxytester.h
+ tests/dhtproxytester.cpp
+ )
+ endif()
+ if (OPENDHT_PEER_DISCOVERY)
+ list (APPEND test_FILES
+ tests/peerdiscoverytester.h
+ tests/peerdiscoverytester.cpp
+ )
+ endif()
+ endif()
+ add_executable(opendht_unit_tests
+ tests/tests_runner.cpp
+ ${test_FILES}
+ )
+ target_link_libraries(opendht_unit_tests PRIVATE
+ opendht
+ ${CMAKE_THREAD_LIBS_INIT}
+ PkgConfig::Cppunit
+ )
+ add_test(TEST opendht_unit_tests)
+endif()
--- /dev/null
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ <program> Copyright (C) <year> <name of author>
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<http://www.gnu.org/licenses/>.
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.
--- /dev/null
+AM_CXXFLAGS = -pthread
+
+SUBDIRS =
+
+SUBDIRS += src
+
+if ENABLE_C
+SUBDIRS += c
+endif
+
+if ENABLE_TOOLS
+SUBDIRS += tools
+endif
+
+if USE_CYTHON
+SUBDIRS += python
+endif
+
+if ENABLE_TESTS
+SUBDIRS += tests
+endif
+
+SUBDIRS += doc
+
+ACLOCAL_AMFLAGS = -I m4
+
+DOC_FILES = \
+ README.md \
+ COPYING
+
+EXTRA_DIST = \
+ $(DOC_FILES)
+
+pkgconfigdir = $(libdir)/pkgconfig
+pkgconfig_DATA = opendht.pc
--- /dev/null
+<img src="https://raw.githubusercontent.com/savoirfairelinux/opendht/master/resources/opendht_logo_512.png" width="100" align="right">
+<br>
+<h1 style="margin-top:10px">
+ <a id="user-content-opendht-" class="anchor" href="/savoirfairelinux/opendht/blob/master/README.md#opendht-" aria-hidden="true"></a>OpenDHT
+</h1>
+
+A lightweight C++17 Distributed Hash Table implementation.
+
+OpenDHT provides an easy to use distributed in-memory data store.
+Every node in the network can read and write values to the store.
+Values are distributed over the network, with redundancy.
+
+ * Lightweight and scalable, designed for large networks and small devices
+ * High resilience to network disruption
+ * Public key cryptography layer providing optional data signature and encryption (using GnuTLS)
+ * IPv4 and IPv6 support
+ * Clean and powerful **C++17** map API
+ * Bindings for **C, Rust & Python 3**
+ * REST API with optional HTTP client+server with push notification support
+
+## Documentation
+See the wiki: <https://github.com/savoirfairelinux/opendht/wiki>
+
+#### How to run a node
+
+You can help contributing to the public network by running a stable node with a public IP address.
+https://github.com/savoirfairelinux/opendht/wiki/Running-a-node-with-dhtnode
+
+#### How-to build and install
+
+Build instructions: <https://github.com/savoirfairelinux/opendht/wiki/Build-the-library>
+
+## Examples
+### C++ example
+The `tools` directory includes simple example programs :
+* `dhtnode`, a command line tool, allowing to run a DHT node and perform operations supported by the library (get, put etc.) with text values.
+* `dhtchat`, a very simple IM client working over the dht.
+
+Example program launching a DHT node, connecting to the network and performing some basic operations:
+```c++
+#include <opendht.h>
+#include <vector>
+
+int main()
+{
+ dht::DhtRunner node;
+
+ // Launch a dht node on a new thread, using a
+ // generated RSA key pair, and listen on port 4222.
+ node.run(4222, dht::crypto::generateIdentity(), true);
+
+ // Join the network through any running node,
+ // here using a known bootstrap node.
+ node.bootstrap("bootstrap.jami.net", "4222");
+
+ // put some data on the dht
+ std::vector<uint8_t> some_data(5, 10);
+ node.put("unique_key", some_data);
+
+ // put some data on the dht, signed with our generated private key
+ node.putSigned("unique_key_42", some_data);
+
+ // get data from the dht
+ node.get("other_unique_key", [](const std::vector<std::shared_ptr<dht::Value>>& values) {
+ // Callback called when values are found
+ for (const auto& value : values)
+ std::cout << "Found value: " << *value << std::endl;
+ return true; // return false to stop the search
+ });
+
+ // wait for dht threads to end
+ node.join();
+ return 0;
+}
+```
+### Python 3 example
+```python
+import opendht as dht
+
+node = dht.DhtRunner()
+node.run()
+
+# Join the network through any running node,
+# here using a known bootstrap node.
+node.bootstrap("bootstrap.jami.net", "4222")
+
+# blocking call (provide callback arguments to make the call non-blocking)
+node.put(dht.InfoHash.get("unique_key"), dht.Value(b'some binary data'))
+
+results = node.get(dht.InfoHash.get("unique_key"))
+for r in results:
+ print(r)
+```
+
+## Dependencies
+- msgpack-c 1.2+, used for data serialization.
+- GnuTLS 3.3+, used for cryptographic operations.
+- Nettle 2.4+, a GnuTLS dependency for crypto.
+- {fmt} 9.0+, for log formatting.
+- (optional) restinio used for the REST API.
+- (optional) jsoncpp 1.7.4-3+, used for the REST API.
+- Build tested with GCC 7+ (GNU/Linux, Windows with MinGW), Clang/LLVM (GNU/Linux, Android, macOS, iOS).
+- Build tested with Microsoft Visual Studio 2019, 2022
+
+## Contact
+
+IRC: join us on Libera.chat at [`#opendht`](https://web.libera.chat/#opendht).
+
+## License
+Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+
+OpenDHT is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+See COPYING or https://www.gnu.org/licenses/gpl-3.0.en.html for the full GPLv3 license.
+
+## Acknowledgements
+This project was originally based on https://github.com/jech/dht by Juliusz Chroboczek.
+It is independent from another project called OpenDHT (Sean Rhea. Ph.D. Thesis, 2005), now extinct.
--- /dev/null
+autoreconf --install --verbose -Wall
--- /dev/null
+lib_LTLIBRARIES = libopendht-c.la
+noinst_HEADERS = opendht_c.h
+
+AM_CPPFLAGS = -DOPENDHT_C_BUILD -isystem @top_srcdir@/include @JsonCpp_CFLAGS@ @MsgPack_CFLAGS@
+if OPENDHT_SHARED
+AM_CPPFLAGS += -Dopendht_c_EXPORTS
+endif
+
+libopendht_c_la_LIBADD = $(LIBOBJS) ../src/.libs/libopendht.la
+
+libopendht_c_la_SOURCES = \
+ opendht.cpp
+
+nobase_include_HEADERS = \
+ opendht_c.h
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ * Author : Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "opendht_c.h"
+
+#include <opendht.h>
+#include <opendht/log.h>
+
+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>;
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <errno.h>
+
+const char* dht_version()
+{
+ return dht::version();
+}
+
+// dht::InfoHash
+
+inline dht_infohash dht_infohash_to_c(const dht::InfoHash& h) {
+ dht_infohash ret;
+ *reinterpret_cast<dht::InfoHash*>(&ret) = h;
+ return ret;
+}
+
+inline dht_pkid dht_pkid_to_c(const dht::PkId& h) {
+ dht_pkid ret;
+ *reinterpret_cast<dht::PkId*>(&ret) = h;
+ return ret;
+}
+
+const char* dht_infohash_print(const dht_infohash* h) {
+ return reinterpret_cast<const dht::InfoHash*>(h)->to_c_str();
+}
+
+void dht_infohash_zero(dht_infohash* h) {
+ *reinterpret_cast<dht::InfoHash*>(h) = dht::InfoHash{};
+}
+
+void dht_infohash_random(dht_infohash* h) {
+ *reinterpret_cast<dht::InfoHash*>(h) = dht::InfoHash::getRandom();
+}
+
+void dht_infohash_get(dht_infohash* h, const uint8_t* dat, size_t dat_size) {
+ *reinterpret_cast<dht::InfoHash*>(h) = dht::InfoHash::get(dat, dat_size);
+}
+
+void dht_infohash_get_from_string(dht_infohash* h, const char* dat) {
+ *reinterpret_cast<dht::InfoHash*>(h) = dht::InfoHash::get((const uint8_t*)dat, (size_t)strlen(dat));
+}
+
+bool dht_infohash_is_zero(const dht_infohash* h) {
+ return !static_cast<bool>(*reinterpret_cast<const dht::InfoHash*>(h));
+}
+
+void dht_infohash_from_hex(dht_infohash* h, const char* dat) {
+ *h = dht_infohash_to_c(dht::InfoHash(std::string_view(dat, HASH_LEN*2)));
+}
+
+void dht_infohash_from_hex_null(dht_infohash* h, const char* dat) {
+ *h = dht_infohash_to_c(dht::InfoHash(std::string_view(dat)));
+}
+
+const char* dht_pkid_print(const dht_pkid* h) {
+ return reinterpret_cast<const dht::PkId*>(h)->to_c_str();
+}
+
+// dht::Blob
+void dht_blob_delete(dht_blob* data) {
+ delete reinterpret_cast<dht::Blob*>(data);
+}
+
+dht_data_view dht_blob_get_data(const dht_blob* data) {
+ dht_data_view view;
+ view.data = reinterpret_cast<const dht::Blob*>(data)->data();
+ view.size = reinterpret_cast<const dht::Blob*>(data)->size();
+ return view;
+}
+
+// dht::Value
+dht_data_view dht_value_get_data(const dht_value* data) {
+ const ValueSp& vsp(*reinterpret_cast<const ValueSp*>(data));
+ dht_data_view view;
+ view.data = vsp->data.data();
+ view.size = vsp->data.size();
+ return view;
+}
+
+dht_value_id dht_value_get_id(const dht_value* data) {
+ const ValueSp& vsp(*reinterpret_cast<const ValueSp*>(data));
+ return vsp->id;
+}
+
+dht_publickey* dht_value_get_owner(const dht_value* data) {
+ const ValueSp& vsp(*reinterpret_cast<const ValueSp*>(data));
+ return vsp->owner ? reinterpret_cast<dht_publickey*>(new PubkeySp(vsp->owner)) : nullptr;
+}
+
+dht_infohash dht_value_get_recipient(const dht_value* data) {
+ const ValueSp& vsp(*reinterpret_cast<const ValueSp*>(data));
+ return dht_infohash_to_c(vsp->recipient);
+}
+
+const char* dht_value_get_user_type(const dht_value* data) {
+ const ValueSp& vsp(*reinterpret_cast<const ValueSp*>(data));
+ return vsp->user_type.c_str();
+}
+
+void dht_value_set_user_type(dht_value* data, const char* user_type) {
+ (*reinterpret_cast<ValueSp*>(data))->user_type = user_type;
+}
+
+dht_value* dht_value_new(const uint8_t* data, size_t size) {
+ return reinterpret_cast<dht_value*>(new ValueSp(std::make_shared<dht::Value>(data, size)));
+}
+
+dht_value* dht_value_new_from_string(const char* str) {
+ ValueSp value = std::make_shared<dht::Value>((const uint8_t*)str, strlen(str));
+ value->user_type = "text/plain";
+ return reinterpret_cast<dht_value*>(new ValueSp(std::move(value)));
+}
+
+dht_value* dht_value_ref(const dht_value* v) {
+ return reinterpret_cast<dht_value*>(new ValueSp(*reinterpret_cast<const ValueSp*>(v)));
+}
+
+void dht_value_unref(dht_value* v) {
+ delete reinterpret_cast<ValueSp*>(v);
+}
+
+// dht::crypto::PublicKey
+dht_publickey* dht_publickey_import(const uint8_t* dat, size_t dat_size) {
+ try {
+ return reinterpret_cast<dht_publickey*>(new PubkeySp(std::make_shared<const dht::crypto::PublicKey>(dat, dat_size)));
+ } catch (const dht::crypto::CryptoException& e) {
+ return nullptr;
+ }
+}
+
+void dht_publickey_delete(dht_publickey* pk) {
+ delete reinterpret_cast<PubkeySp*>(pk);
+}
+
+int dht_publickey_export(const dht_publickey* pk, char* out, size_t* outlen) {
+ const auto& pkey = *reinterpret_cast<const PubkeySp*>(pk);
+ return pkey->pack((uint8_t*)out, outlen);
+}
+
+dht_infohash dht_publickey_get_id(const dht_publickey* pk) {
+ const auto& pkey = *reinterpret_cast<const PubkeySp*>(pk);
+ return dht_infohash_to_c(pkey->getId());
+}
+
+dht_pkid dht_publickey_get_long_id(const dht_publickey* pk) {
+ const auto& pkey = *reinterpret_cast<const PubkeySp*>(pk);
+ return dht_pkid_to_c(pkey->getLongId());
+}
+
+bool dht_publickey_check_signature(const dht_publickey* pk, const char* data, size_t data_size, const char* signature, size_t signature_size) {
+ const auto& pkey = *reinterpret_cast<const PubkeySp*>(pk);
+ return pkey->checkSignature((const uint8_t*)data, data_size, (const uint8_t*)signature, signature_size);
+}
+
+dht_blob* dht_publickey_encrypt(const dht_publickey* pk, const char* data, size_t data_size) {
+ const auto& pkey = *reinterpret_cast<const PubkeySp*>(pk);
+ try {
+ auto rdata = std::make_unique<dht::Blob>();
+ *rdata = pkey->encrypt((const uint8_t*)data, data_size);
+ return (dht_blob*)rdata.release();
+ } catch (...) {
+ return nullptr;
+ }
+}
+
+// dht::crypto::PrivateKey
+dht_privatekey* dht_privatekey_generate(unsigned key_length_bits) {
+ if (key_length_bits == 0)
+ key_length_bits = 4096;
+ return reinterpret_cast<dht_privatekey*>(new PrivkeySp(std::make_shared<dht::crypto::PrivateKey>(dht::crypto::PrivateKey::generate(key_length_bits))));
+}
+
+dht_privatekey* dht_privatekey_import(const uint8_t* dat, size_t dat_size, const char* password) {
+ try {
+ return reinterpret_cast<dht_privatekey*>(new PrivkeySp(std::make_shared<dht::crypto::PrivateKey>(dat, dat_size, password)));
+ } catch (const dht::crypto::CryptoException& e) {
+ return nullptr;
+ }
+}
+
+int dht_privatekey_export(const dht_privatekey* k, char* out, size_t* out_size, const char* password) {
+ if (!out or !out_size or !*out_size)
+ return -1;
+ const auto& key = *reinterpret_cast<const PrivkeySp*>(k);
+ return key->serialize((uint8_t*)out, out_size, password);
+}
+
+dht_publickey* dht_privatekey_get_publickey(const dht_privatekey* k) {
+ const auto& key = *reinterpret_cast<const PrivkeySp*>(k);
+ return reinterpret_cast<dht_publickey*>(new PubkeySp(key->getSharedPublicKey()));
+}
+
+dht_blob* dht_privatekey_decrypt(const dht_privatekey* k, const char* data, size_t data_size) {
+ const auto& key = *reinterpret_cast<const PrivkeySp*>(k);
+ try {
+ auto rdata = std::make_unique<dht::Blob>();
+ *rdata = key->decrypt((const uint8_t*)data, data_size);
+ return (dht_blob*)rdata.release();
+ } catch (...) {
+ return nullptr;
+ }
+}
+
+void dht_privatekey_delete(dht_privatekey* pk) {
+ delete reinterpret_cast<PrivkeySp*>(pk);
+}
+
+// dht::crypto::Certificate
+dht_certificate* dht_certificate_import(const uint8_t* dat, size_t dat_size) {
+ try {
+ return reinterpret_cast<dht_certificate*>(new CertSp(std::make_shared<dht::crypto::Certificate>(dat, dat_size)));
+ } catch (const dht::crypto::CryptoException& e) {
+ return nullptr;
+ }
+}
+
+void dht_certificate_delete(dht_certificate* c) {
+ delete reinterpret_cast<CertSp*>(c);
+}
+
+dht_infohash dht_certificate_get_id(const dht_certificate* c) {
+ const auto& cert = *reinterpret_cast<const CertSp*>(c);
+ return dht_infohash_to_c(cert->getId());
+}
+
+dht_pkid dht_certificate_get_long_id(const dht_certificate* c) {
+ const auto& cert = *reinterpret_cast<const CertSp*>(c);
+ return dht_pkid_to_c(cert->getLongId());
+}
+
+dht_publickey* dht_certificate_get_publickey(const dht_certificate* c) {
+ const auto& cert = *reinterpret_cast<const CertSp*>(c);
+ return reinterpret_cast<dht_publickey*>(new PubkeySp(cert->getSharedPublicKey()));
+}
+
+// dht::crypto::Identity
+inline dht::crypto::Identity dht_identity_from_c(const dht_identity* cid) {
+ dht::crypto::Identity id {};
+ if (cid and cid->privatekey)
+ id.first = *reinterpret_cast<const PrivkeySp*>(cid->privatekey);
+ if (cid and cid->certificate)
+ id.second = *reinterpret_cast<const CertSp*>(cid->certificate);
+ return id;
+}
+
+inline dht_identity dht_identity_to_c(const dht::crypto::Identity& id) {
+ dht_identity cid {};
+ cid.privatekey = id.first ? reinterpret_cast<dht_privatekey*>(new PrivkeySp(id.first)) : NULL;
+ cid.certificate = id.second ? reinterpret_cast<dht_certificate*>(new CertSp(id.second)) : NULL;
+ return cid;
+}
+
+OPENDHT_C_PUBLIC dht_identity dht_identity_generate(const char* common_name, const dht_identity* ca) {
+ return dht_identity_to_c(dht::crypto::generateIdentity(common_name, dht_identity_from_c(ca)));
+}
+
+OPENDHT_C_PUBLIC void dht_identity_delete(dht_identity* id) {
+ if (id->certificate) {
+ dht_certificate_delete(id->certificate);
+ id->certificate = NULL;
+ }
+ if (id->privatekey) {
+ dht_privatekey_delete(id->privatekey);
+ id->privatekey = NULL;
+ }
+}
+
+// config
+void dht_runner_config_default(dht_runner_config* config) {
+ bzero(config, sizeof(dht_runner_config));
+ config->threaded = true;
+}
+
+// dht::DhtRunner
+dht_runner* dht_runner_new(void) {
+ return reinterpret_cast<dht_runner*>(new dht::DhtRunner);
+}
+
+void dht_runner_delete(dht_runner* runner) {
+ delete reinterpret_cast<dht::DhtRunner*>(runner);
+}
+
+int dht_runner_run(dht_runner* r, in_port_t port) {
+ auto runner = reinterpret_cast<dht::DhtRunner*>(r);
+ try {
+ runner->run(port, {}, true);
+ } catch(...) {
+ return ENOTCONN;
+ }
+ return 0;
+}
+
+int dht_runner_run_config(dht_runner* r, in_port_t port, const dht_runner_config* conf) {
+ auto runner = reinterpret_cast<dht::DhtRunner*>(r);
+ try {
+ dht::DhtRunner::Config config;
+ config.dht_config.node_config.is_bootstrap = conf->dht_config.node_config.is_bootstrap;
+ config.dht_config.node_config.maintain_storage = conf->dht_config.node_config.maintain_storage;
+ config.dht_config.node_config.node_id = *reinterpret_cast<const dht::InfoHash*>(&conf->dht_config.node_config.node_id);
+ config.dht_config.node_config.network = conf->dht_config.node_config.network;
+ config.dht_config.node_config.persist_path = conf->dht_config.node_config.persist_path
+ ? std::string(conf->dht_config.node_config.persist_path) : std::string{};
+
+ if (conf->dht_config.id.privatekey)
+ config.dht_config.id.first = *reinterpret_cast<const PrivkeySp*>(conf->dht_config.id.privatekey);
+
+ if (conf->dht_config.id.certificate)
+ config.dht_config.id.second = *reinterpret_cast<const CertSp*>(conf->dht_config.id.certificate);
+
+ config.threaded = conf->threaded;
+ config.proxy_server = conf->proxy_server ? std::string(conf->proxy_server) : std::string{};
+ config.push_node_id = conf->push_node_id ? std::string(conf->push_node_id) : std::string{};
+ config.push_token = conf->push_token ? std::string(conf->push_token) : std::string{};
+ config.push_topic = conf->push_topic ? std::string(conf->push_topic) : std::string{};
+ config.push_platform = conf->push_platform ? std::string(conf->push_platform) : std::string{};
+ config.peer_discovery = conf->peer_discovery;
+ config.peer_publish = conf->peer_publish;
+
+ dht::DhtRunner::Context context;
+ if (conf->log) {
+ context.logger = dht::log::getStdLogger();
+ }
+ runner->run(port, config, std::move(context));
+ } catch(...) {
+ return ENOTCONN;
+ }
+ return 0;
+}
+
+void dht_runner_ping(dht_runner* r, struct sockaddr* addr, socklen_t addr_len, dht_done_cb done_cb, void* cb_user_data) {
+ auto runner = reinterpret_cast<dht::DhtRunner*>(r);
+ if (done_cb) {
+ runner->bootstrap(dht::SockAddr(addr, addr_len), [done_cb, cb_user_data](bool ok){
+ done_cb(ok, cb_user_data);
+ });
+ } else {
+ runner->bootstrap(dht::SockAddr(addr, addr_len));
+ }
+}
+
+void dht_runner_bootstrap(dht_runner* r, const char* host, const char* service) {
+ auto runner = reinterpret_cast<dht::DhtRunner*>(r);
+ if (service)
+ runner->bootstrap(host, service);
+ else
+ runner->bootstrap(host);
+}
+
+void dht_runner_get(dht_runner* r, const dht_infohash* h, dht_get_cb cb, dht_done_cb done_cb, void* cb_user_data) {
+ auto runner = reinterpret_cast<dht::DhtRunner*>(r);
+ auto hash = reinterpret_cast<const dht::InfoHash*>(h);
+ runner->get(*hash, [cb,cb_user_data](std::shared_ptr<dht::Value> value){
+ return cb(reinterpret_cast<const dht_value*>(&value), cb_user_data);
+ }, [done_cb, cb_user_data](bool ok){
+ if (done_cb)
+ done_cb(ok, cb_user_data);
+ });
+}
+
+struct ScopeGuardCb {
+ ScopeGuardCb(dht_shutdown_cb cb, void* data)
+ : onDestroy(cb), userData(data) {}
+
+ ~ScopeGuardCb() {
+ if (onDestroy)
+ onDestroy((void*)userData);
+ }
+private:
+ const dht_shutdown_cb onDestroy;
+ void const* userData;
+};
+
+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) {
+ auto runner = reinterpret_cast<dht::DhtRunner*>(r);
+ auto hash = reinterpret_cast<const dht::InfoHash*>(h);
+ auto fret = new std::future<size_t>;
+ *fret = runner->listen(*hash, [
+ cb,
+ cb_user_data,
+ guard = done_cb ? std::make_shared<ScopeGuardCb>(done_cb, cb_user_data) : std::shared_ptr<ScopeGuardCb>{}
+ ](const std::vector<std::shared_ptr<dht::Value>>& values, bool expired) {
+ for (const auto& value : values) {
+ if (not cb(reinterpret_cast<const dht_value*>(&value), expired, cb_user_data))
+ return false;
+ }
+ return true;
+ });
+ return (dht_op_token*)fret;
+}
+
+void dht_runner_cancel_listen(dht_runner* r, const dht_infohash* h, dht_op_token* t) {
+ auto runner = reinterpret_cast<dht::DhtRunner*>(r);
+ auto hash = reinterpret_cast<const dht::InfoHash*>(h);
+ auto token = reinterpret_cast<std::future<size_t>*>(t);
+ runner->cancelListen(*hash, std::move(*token));
+}
+
+void dht_op_token_delete(dht_op_token* token) {
+ delete reinterpret_cast<std::future<size_t>*>(token);
+}
+
+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) {
+ auto runner = reinterpret_cast<dht::DhtRunner*>(r);
+ auto hash = reinterpret_cast<const dht::InfoHash*>(h);
+ auto value = reinterpret_cast<const ValueSp*>(v);
+ runner->put(*hash, *value, [done_cb, cb_user_data](bool ok){
+ if (done_cb)
+ done_cb(ok, cb_user_data);
+ }, dht::time_point::max(), permanent);
+}
+
+void dht_runner_put_signed(dht_runner* r, const dht_infohash* h, const dht_value* v, dht_done_cb done_cb, void* cb_user_data, bool permanent) {
+ auto runner = reinterpret_cast<dht::DhtRunner*>(r);
+ auto hash = reinterpret_cast<const dht::InfoHash*>(h);
+ auto value = reinterpret_cast<const ValueSp*>(v);
+ runner->putSigned(*hash, *value, [done_cb, cb_user_data](bool ok){
+ if (done_cb)
+ done_cb(ok, cb_user_data);
+ }, permanent);
+}
+
+void dht_runner_put_encrypted(dht_runner* r, const dht_infohash* h, const dht_infohash* to, const dht_value* v, dht_done_cb done_cb, void* cb_user_data, bool permanent) {
+ auto runner = reinterpret_cast<dht::DhtRunner*>(r);
+ auto hash = reinterpret_cast<const dht::InfoHash*>(h);
+ auto toHash = reinterpret_cast<const dht::InfoHash*>(to);
+ auto value = reinterpret_cast<const ValueSp*>(v);
+ runner->putEncrypted(*hash, *toHash, *value, [done_cb, cb_user_data](bool ok){
+ if (done_cb)
+ done_cb(ok, cb_user_data);
+ }, permanent);
+}
+
+void dht_runner_cancel_put(dht_runner* r, const dht_infohash* h, dht_value_id value_id) {
+ auto runner = reinterpret_cast<dht::DhtRunner*>(r);
+ auto hash = reinterpret_cast<const dht::InfoHash*>(h);
+ runner->cancelPut(*hash, value_id);
+}
+
+void dht_runner_shutdown(dht_runner* r, dht_shutdown_cb done_cb, void* cb_user_data) {
+ auto runner = reinterpret_cast<dht::DhtRunner*>(r);
+ runner->shutdown([done_cb, cb_user_data](){
+ if (done_cb)
+ done_cb(cb_user_data);
+ });
+}
+
+bool dht_runner_is_running(const dht_runner* r) {
+ if (not r) return false;
+ auto runner = reinterpret_cast<const dht::DhtRunner*>(r);
+ return runner->isRunning();
+}
+
+in_port_t dht_runner_get_bound_port(const dht_runner* r, sa_family_t af) {
+ auto runner = reinterpret_cast<const dht::DhtRunner*>(r);
+ return runner->getBoundPort(af);
+}
+
+dht_infohash dht_runner_get_node_id(const dht_runner* r) {
+ auto runner = reinterpret_cast<const dht::DhtRunner*>(r);
+ dht_infohash ret;
+ *reinterpret_cast<dht::InfoHash*>(&ret) = runner->getNodeId();
+ return ret;
+}
+
+dht_infohash dht_runner_get_id(const dht_runner* r) {
+ auto runner = reinterpret_cast<const dht::DhtRunner*>(r);
+ dht_infohash ret;
+ *reinterpret_cast<dht::InfoHash*>(&ret) = runner->getId();
+ return ret;
+}
+
+struct sockaddr** dht_runner_get_public_address(const dht_runner* r) {
+ auto runner = reinterpret_cast<const dht::DhtRunner*>(r);
+ auto addrs = const_cast<dht::DhtRunner*>(runner)->getPublicAddress();
+ if (addrs.empty())
+ return nullptr;
+ auto ret = (struct sockaddr**)malloc(sizeof(struct sockaddr*) * (addrs.size() + 1));
+ for (size_t i=0; i<addrs.size(); i++)
+ ret[i] = addrs[i].release();
+ ret[addrs.size()] = nullptr;
+ return ret;
+}
+
+#ifdef __cplusplus
+}
+#endif
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ * Author : Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <opendht/def.h>
+#include <sys/socket.h>
+#include <netinet/in.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stddef.h>
+
+// Non-owning data view
+struct OPENDHT_C_PUBLIC dht_data_view {
+ const uint8_t* data;
+ size_t size;
+};
+typedef struct dht_data_view dht_data_view;
+
+OPENDHT_C_PUBLIC const char* dht_version(void);
+
+// dht::Blob
+struct OPENDHT_C_PUBLIC dht_blob;
+typedef struct dht_blob dht_blob;
+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
+struct OPENDHT_C_PUBLIC dht_infohash { uint8_t d[HASH_LEN]; };
+typedef struct dht_infohash dht_infohash;
+OPENDHT_C_PUBLIC void dht_infohash_zero(dht_infohash* h);
+OPENDHT_C_PUBLIC void dht_infohash_random(dht_infohash* h);
+OPENDHT_C_PUBLIC void dht_infohash_from_hex(dht_infohash* h, const char* dat);
+OPENDHT_C_PUBLIC void dht_infohash_from_hex_null(dht_infohash* h, const char* dat);
+OPENDHT_C_PUBLIC void dht_infohash_get(dht_infohash* h, const uint8_t* dat, size_t dat_size);
+OPENDHT_C_PUBLIC void dht_infohash_get_from_string(dht_infohash* h, const char* str);
+OPENDHT_C_PUBLIC const char* dht_infohash_print(const dht_infohash* h);
+OPENDHT_C_PUBLIC bool dht_infohash_is_zero(const dht_infohash* h);
+
+// dht::PkId
+struct OPENDHT_C_PUBLIC dht_pkid { uint8_t d[32]; };
+typedef struct dht_pkid dht_pkid;
+OPENDHT_C_PUBLIC const char* dht_pkid_print(const dht_pkid* h);
+
+// dht::crypto::PublicKey
+struct OPENDHT_C_PUBLIC dht_publickey;
+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_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(const dht_publickey* pk, const char* data, size_t data_size, const char* signature, size_t signature_size);
+OPENDHT_C_PUBLIC dht_blob* dht_publickey_encrypt(const dht_publickey* pk, const char* data, size_t data_size);
+
+// dht::crypto::PrivateKey
+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_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_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*);
+
+// dht::crypto::Certificate
+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_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*);
+OPENDHT_C_PUBLIC void dht_certificate_delete(dht_certificate*);
+
+struct OPENDHT_PUBLIC dht_identity {
+ dht_privatekey* privatekey;
+ dht_certificate* certificate;
+};
+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 void dht_identity_delete(dht_identity*);
+
+// dht::Value
+struct OPENDHT_C_PUBLIC dht_value;
+typedef struct dht_value dht_value;
+typedef uint64_t dht_value_id;
+OPENDHT_C_PUBLIC dht_value* dht_value_new(const uint8_t* data, size_t size);
+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 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);
+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);
+
+// 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);
+
+struct OPENDHT_C_PUBLIC dht_op_token;
+typedef struct dht_op_token dht_op_token;
+OPENDHT_C_PUBLIC void dht_op_token_delete(dht_op_token* token);
+
+// config
+struct OPENDHT_PUBLIC dht_node_config {
+ dht_infohash node_id;
+ uint32_t network;
+ bool is_bootstrap;
+ bool maintain_storage;
+ const char* persist_path;
+};
+typedef struct dht_node_config dht_node_config;
+
+struct OPENDHT_PUBLIC dht_secure_config {
+ dht_node_config node_config;
+ dht_identity id;
+};
+typedef struct dht_secure_config dht_secure_config;
+
+struct OPENDHT_PUBLIC dht_runner_config {
+ dht_secure_config dht_config;
+ bool threaded;
+ const char* proxy_server;
+ const char* push_node_id;
+ const char* push_token;
+ const char* push_topic;
+ const char* push_platform;
+ bool peer_discovery;
+ bool peer_publish;
+ dht_certificate* server_ca;
+ dht_identity client_identity;
+ bool log;
+};
+typedef struct dht_runner_config dht_runner_config;
+OPENDHT_C_PUBLIC void dht_runner_config_default(dht_runner_config* config);
+
+// dht::DhtRunner
+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);
+/* 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 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 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 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, const dht_value* value, dht_done_cb done_cb, void* cb_user_data, bool permanent);
+OPENDHT_C_PUBLIC void dht_runner_put_signed(dht_runner* runner, const dht_infohash* hash, const dht_value* value, dht_done_cb done_cb, void* cb_user_data, bool permanent);
+OPENDHT_C_PUBLIC void dht_runner_put_encrypted(dht_runner* runner, const dht_infohash* hash, const dht_infohash* to, const dht_value* value, dht_done_cb done_cb, void* cb_user_data, bool permanent);
+OPENDHT_C_PUBLIC void dht_runner_cancel_put(dht_runner* runner, const dht_infohash* hash, dht_value_id value_id);
+OPENDHT_C_PUBLIC void dht_runner_shutdown(dht_runner* runner, dht_shutdown_cb done_cb, void* cb_user_data);
+OPENDHT_C_PUBLIC dht_infohash dht_runner_get_node_id(const dht_runner* runner);
+OPENDHT_C_PUBLIC dht_infohash dht_runner_get_id(const dht_runner* runner);
+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);
+
+#ifdef __cplusplus
+}
+#endif
--- /dev/null
+# atomic builtins are required for threading support.
+
+INCLUDE(CheckCXXSourceCompiles)
+INCLUDE(CheckLibraryExists)
+INCLUDE("${CMAKE_CURRENT_LIST_DIR}/DetermineGCCCompatible.cmake")
+
+# Sometimes linking against libatomic is required for atomic ops, if
+# the platform doesn't support lock-free atomics.
+
+function(check_working_cxx_atomics varname)
+ set(OLD_CMAKE_REQUIRED_FLAGS ${CMAKE_REQUIRED_FLAGS})
+ set(CMAKE_REQUIRED_FLAGS "${CMAKE_REQUIRED_FLAGS} -std=c++11")
+ CHECK_CXX_SOURCE_COMPILES("
+#include <atomic>
+std::atomic<int> x;
+std::atomic<short> y;
+std::atomic<char> z;
+int main() {
+ ++z;
+ ++y;
+ return ++x;
+}
+" ${varname})
+ set(CMAKE_REQUIRED_FLAGS ${OLD_CMAKE_REQUIRED_FLAGS})
+endfunction(check_working_cxx_atomics)
+
+function(check_working_cxx_atomics64 varname)
+ set(OLD_CMAKE_REQUIRED_FLAGS ${CMAKE_REQUIRED_FLAGS})
+ set(CMAKE_REQUIRED_FLAGS "-std=c++11 ${CMAKE_REQUIRED_FLAGS}")
+ CHECK_CXX_SOURCE_COMPILES("
+#include <atomic>
+#include <cstdint>
+std::atomic<uint64_t> x (0);
+int main() {
+ uint64_t i = x.load(std::memory_order_relaxed);
+ (void)i;
+ return 0;
+}
+" ${varname})
+ set(CMAKE_REQUIRED_FLAGS ${OLD_CMAKE_REQUIRED_FLAGS})
+endfunction(check_working_cxx_atomics64)
+
+
+# Check for (non-64-bit) atomic operations.
+if(MSVC)
+ set(HAVE_CXX_ATOMICS_WITHOUT_LIB True)
+elseif(LLVM_COMPILER_IS_GCC_COMPATIBLE OR CMAKE_CXX_COMPILER_ID MATCHES "XL")
+ # First check if atomics work without the library.
+ check_working_cxx_atomics(HAVE_CXX_ATOMICS_WITHOUT_LIB)
+ # If not, check if the library exists, and atomics work with it.
+ if(NOT HAVE_CXX_ATOMICS_WITHOUT_LIB)
+ list(APPEND CMAKE_REQUIRED_LIBRARIES "atomic")
+ check_working_cxx_atomics(HAVE_CXX_ATOMICS_WITH_LIB)
+ if (NOT HAVE_CXX_ATOMICS_WITH_LIB)
+ message(FATAL_ERROR "Host compiler must support std::atomic!")
+ endif()
+ endif()
+endif()
+
+# Check for 64 bit atomic operations.
+if(MSVC)
+ set(HAVE_CXX_ATOMICS64_WITHOUT_LIB True)
+elseif(LLVM_COMPILER_IS_GCC_COMPATIBLE OR CMAKE_CXX_COMPILER_ID MATCHES "XL")
+ # First check if atomics work without the library.
+ check_working_cxx_atomics64(HAVE_CXX_ATOMICS64_WITHOUT_LIB)
+ # If not, check if the library exists, and atomics work with it.
+ if(NOT HAVE_CXX_ATOMICS64_WITHOUT_LIB)
+ list(APPEND CMAKE_REQUIRED_LIBRARIES "atomic")
+ check_working_cxx_atomics64(HAVE_CXX_ATOMICS64_WITH_LIB)
+ if (NOT HAVE_CXX_ATOMICS64_WITH_LIB)
+ message(FATAL_ERROR "Host compiler must support 64-bit std::atomic!")
+ endif()
+ endif()
+endif()
+
+## TODO: This define is only used for the legacy atomic operations in
+## llvm's Atomic.h, which should be replaced. Other code simply
+## assumes C++11 <atomic> works.
+CHECK_CXX_SOURCE_COMPILES("
+#ifdef _MSC_VER
+#include <windows.h>
+#endif
+int main() {
+#ifdef _MSC_VER
+ volatile LONG val = 1;
+ MemoryBarrier();
+ InterlockedCompareExchange(&val, 0, 1);
+ InterlockedIncrement(&val);
+ InterlockedDecrement(&val);
+#else
+ volatile unsigned long val = 1;
+ __sync_synchronize();
+ __sync_val_compare_and_swap(&val, 1, 0);
+ __sync_add_and_fetch(&val, 1);
+ __sync_sub_and_fetch(&val, 1);
+#endif
+ return 0;
+ }
+" LLVM_HAS_ATOMICS)
+
+if( NOT LLVM_HAS_ATOMICS )
+ message(STATUS "Warning: LLVM will be built thread-unsafe because atomic builtins are missing")
+endif()
\ No newline at end of file
--- /dev/null
+# Determine if the compiler has GCC-compatible command-line syntax.
+
+if(NOT DEFINED LLVM_COMPILER_IS_GCC_COMPATIBLE)
+ if(CMAKE_COMPILER_IS_GNUCXX)
+ set(LLVM_COMPILER_IS_GCC_COMPATIBLE ON)
+ elseif( MSVC )
+ set(LLVM_COMPILER_IS_GCC_COMPATIBLE OFF)
+ elseif( "${CMAKE_CXX_COMPILER_ID}" MATCHES "Clang" )
+ set(LLVM_COMPILER_IS_GCC_COMPATIBLE ON)
+ elseif( "${CMAKE_CXX_COMPILER_ID}" MATCHES "Intel" )
+ set(LLVM_COMPILER_IS_GCC_COMPATIBLE ON)
+ endif()
+endif()
--- /dev/null
+# - Try to find readline, a library for easy editing of command lines.
+# Variables used by this module:
+# READLINE_ROOT_DIR - Readline root directory
+# Variables defined by this module:
+# READLINE_FOUND - system has Readline
+# READLINE_INCLUDE_DIR - the Readline include directory (cached)
+# READLINE_INCLUDE_DIRS - the Readline include directories
+# (identical to READLINE_INCLUDE_DIR)
+# READLINE_LIBRARY - the Readline library (cached)
+# READLINE_LIBRARIES - the Readline library plus the libraries it
+# depends on
+
+# Copyright (C) 2009
+# ASTRON (Netherlands Institute for Radio Astronomy)
+# P.O.Box 2, 7990 AA Dwingeloo, The Netherlands
+#
+# This program is free software; you can redistribute it and/or modify
+# modify it under the terms of the GNU General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+# $Id: FindReadline.cmake 15228 2010-03-16 09:27:26Z loose $
+
+if(NOT READLINE_FOUND)
+
+ find_path(READLINE_INCLUDE_DIR readline/readline.h
+ HINTS ${READLINE_ROOT_DIR} PATH_SUFFIXES include)
+ find_library(READLINE_LIBRARY readline
+ HINTS ${READLINE_ROOT_DIR} PATH_SUFFIXES lib)
+ find_library(NCURSES_LIBRARY ncurses) # readline depends on libncurses
+ mark_as_advanced(READLINE_INCLUDE_DIR READLINE_LIBRARY NCURSES_LIBRARY)
+
+ include(FindPackageHandleStandardArgs)
+ find_package_handle_standard_args(Readline DEFAULT_MSG
+ READLINE_LIBRARY NCURSES_LIBRARY READLINE_INCLUDE_DIR)
+
+ set(READLINE_INCLUDE_DIRS ${READLINE_INCLUDE_DIR})
+ set(READLINE_LIBRARIES ${READLINE_LIBRARY} ${NCURSES_LIBRARY})
+
+endif(NOT READLINE_FOUND)
--- /dev/null
+# header-only does not produce a library
+if(NOT Restinio_FOUND)
+ find_path (Restinio_INCLUDE_DIR restinio
+ HINTS
+ "/usr/include"
+ "/usr/local/include"
+ "/opt/local/include")
+ include(FindPackageHandleStandardArgs)
+ find_package_handle_standard_args(Restinio DEFAULT_MSG Restinio_INCLUDE_DIR)
+ if (Restinio_INCLUDE_DIR)
+ set(Restinio_FOUND TRUE)
+ set(Restinio_INCLUDE_DIRS ${Restinio_INCLUDE_DIR})
+ endif()
+endif()
--- /dev/null
+dnl define macros
+m4_define([opendht_major_version], 3)
+m4_define([opendht_minor_version], 0)
+m4_define([opendht_patch_version], 0)
+m4_define([opendht_version],
+ [opendht_major_version.opendht_minor_version.opendht_patch_version])
+
+AC_INIT(opendht, [opendht_version])
+AC_CONFIG_AUX_DIR(ac)
+AM_INIT_AUTOMAKE([foreign subdir-objects])
+AC_CONFIG_HEADERS([config.h])
+AC_CONFIG_MACRO_DIR([m4])
+AC_CANONICAL_HOST
+
+AC_SUBST(OPENDHT_MAJOR_VERSION, opendht_major_version)
+AC_SUBST(OPENDHT_MINOR_VERSION, opendht_minor_version)
+AC_SUBST(OPENDHT_PATCH_VERSION, opendht_patch_version)
+
+AC_ARG_ENABLE([debug], AS_HELP_STRING([--enable-debug], [Build in debug mode, adds stricter warnings, disables optimization]))
+AS_IF([test "x$enable_debug" = "xyes"],
+ [CXXFLAGS="${CXXFLAGS} -g -Og -Wno-return-type -Wall -Wextra -Wnon-virtual-dtor -Wno-deprecated -pedantic-errors"],
+ [CXXFLAGS="${CXXFLAGS} -O3 -Wno-deprecated -pedantic-errors -fvisibility=hidden"])
+
+CPPFLAGS+=" -DOPENDHT_BUILD"
+AM_CONDITIONAL([OPENDHT_SHARED], [test "x$enable_shared" != xno])
+AM_COND_IF(OPENDHT_SHARED, [
+ CPPFLAGS+=" -Dopendht_EXPORTS"
+])
+
+AM_PROG_AR
+LT_INIT()
+LT_LANG(C++)
+AC_LANG(C++)
+AC_PROG_CXX
+AX_CXX_COMPILE_STDCXX(17,[noext],[mandatory])
+
+dnl Check for C binding
+AC_ARG_ENABLE([c], [AS_HELP_STRING([--disable-c], [Disable DHT C binding])])
+AM_CONDITIONAL(ENABLE_C, test x$enable_c != "xno")
+AM_COND_IF(ENABLE_C, [
+ AC_DEFINE([OPENDHT_C], [], [Define if DHT C biding is enabled])
+])
+
+dnl Check for indexation
+AC_ARG_ENABLE([indexation], [AS_HELP_STRING([--disable-indexation], [Disable DHT indexation])])
+AM_CONDITIONAL(ENABLE_INDEXATION, test x$enable_indexation != "xno")
+AM_COND_IF(ENABLE_INDEXATION, [
+ AC_DEFINE([OPENDHT_INDEXATION], [], [Define if DHT indexation is enabled])
+])
+
+AC_ARG_ENABLE([peer-discovery], [AS_HELP_STRING([--disable-peer-discovery], [Disable peer-discovery])])
+AM_CONDITIONAL(ENABLE_PEER_DISCOVERY, test x$enable_peer_discovery != "xno")
+AM_COND_IF(ENABLE_PEER_DISCOVERY, [AC_DEFINE([OPENDHT_PEER_DISCOVERY], [], [Define if peer discovery is enabled])])
+
+dnl Check for Doxygen
+AC_ARG_ENABLE([doc], AS_HELP_STRING([--enable-doc], [Enable documentation generation (doxygen)]))
+AS_IF([test "x$enable_doc" = "xyes"], [
+ AC_CHECK_PROGS([DOXYGEN], [doxygen])
+ AS_IF([test -z "$DOXYGEN"], [AC_MSG_WARN([Doxygen not found - continuing without Doxygen support])])
+])
+AM_CONDITIONAL([HAVE_DOXYGEN], [test -n "$DOXYGEN"])
+
+dnl Check for Python
+AC_ARG_ENABLE([python], [AS_HELP_STRING([--disable-python], [Disble python binding])])
+AS_IF([test "x$enable_python" != "xno"], [
+ AM_PATH_PYTHON([3.3],, [:])
+ AS_IF([test -n "$PYTHON"],[
+ echo 'import Cython' | $PYTHON
+ AS_IF([test $? == 0],[CYTHON=yes],[AC_MSG_WARN([Cython not found - continuing without python support])])
+ AC_CHECK_PROGS([PIP], [pip3])
+ AS_IF([test -z "$PIP"],[AC_MSG_WARN([pip not found - continuing without python uninstall support])])
+ ])
+ ])
+AM_CONDITIONAL([USE_CYTHON], [test -n "$CYTHON"])
+AM_CONDITIONAL([HAVE_PIP], [test -n "$PIP"])
+
+case "${host_os}" in
+ "")
+ SYS=unknown
+ ;;
+ *android*)
+ SYS=android
+ ;;
+ linux*)
+ SYS=linux
+ ;;
+ darwin*)
+ SYS=darwin
+ ;;
+ mingw32*)
+ SYS=mingw32
+ WIN32=1
+ AC_DEFINE([_POSIX_SOURCE], [1], [IEEE Std 1003.1.])
+ AC_DEFINE([_POSIX_C_SOURCE], [200809L], [IEEE Std 1003.1.])
+ AC_DEFINE([_XOPEN_SOURCE], [700], [POSIX and XPG 7th edition])
+ AC_DEFINE([_XOPEN_SOURCE_EXTENDED], [1], [XPG things and X/Open Unix extensions.])
+ AC_DEFINE([_BSD_SOURCE], [1], [ISO C, POSIX, and 4.3BSD things.])
+ LDFLAGS="${LDFLAGS} -lws2_32"
+ AC_SUBST(WINDOWS_ARCH)
+ AC_SUBST(PROGRAMFILES)
+ ;;
+ *)
+ SYS="${host_os}"
+ ;;
+esac
+
+AM_CONDITIONAL(WIN32, [test "x$SYS" = "xmingw32"])
+AS_IF([test "x$SYS" = "xandroid"],
+ [], [LDFLAGS="${LDFLAGS} -lpthread"])
+
+PKG_PROG_PKG_CONFIG()
+
+AC_ARG_ENABLE([proxy_server], AS_HELP_STRING([--enable-proxy-server], [Enable proxy server ability]), proxy_server=yes, proxy_server=no)
+AM_CONDITIONAL(ENABLE_PROXY_SERVER, test x$proxy_server == xyes)
+
+AC_ARG_ENABLE([push_notifications], AS_HELP_STRING([--enable-push-notifications], [Enable push notifications support]), push_notifications=yes, push_notifications=no)
+AM_CONDITIONAL(ENABLE_PUSH_NOTIFICATIONS, test x$push_notifications == xyes)
+
+AC_ARG_ENABLE([proxy_server_identity], AS_HELP_STRING([--enable-proxy-server-identity],
+ [Enable proxy server ability]), proxy_server_identity=yes, proxy_server_identity=no)
+AM_CONDITIONAL(ENABLE_PROXY_SERVER_IDENTITY, test x$proxy_server_identity == xyes -a x$proxy_server == xyes)
+AC_ARG_ENABLE([proxy_server_identity], AS_HELP_STRING([--enable-proxy-server-identity],
+ [Enable proxy server ability]), proxy_server_identity=yes, proxy_server_identity=no)
+
+AC_ARG_ENABLE([proxy_client], AS_HELP_STRING([--enable-proxy-client], [Enable proxy client ability]), proxy_client=yes, proxy_client=no)
+AM_CONDITIONAL(ENABLE_PROXY_CLIENT, test x$proxy_client == xyes)
+
+AC_ARG_ENABLE([tests], AS_HELP_STRING([--enable-tests], [Enable tests]), build_tests=yes, build_tests=no)
+AM_CONDITIONAL(ENABLE_TESTS, test "x$build_tests" == xyes)
+AS_IF([test "x$build_tests" == xyes], [
+ PKG_CHECK_MODULES([CppUnit], [cppunit >= 1.12])
+])
+
+AM_CONDITIONAL(PROXY_CLIENT_OR_SERVER, test x$proxy_client == xyes || test x$proxy_server == xyes)
+
+PKG_CHECK_MODULES([Nettle], [nettle >= 2.4])
+PKG_CHECK_MODULES([GnuTLS], [gnutls >= 3.3])
+AC_CHECK_HEADERS([msgpack.hpp], [], [
+ PKG_CHECK_MODULES([MsgPack], [msgpack >= 1.2])
+])
+PKG_CHECK_MODULES([Argon2], [libargon2])
+AC_SUBST(argon2_lib, [", libargon2"])
+
+AC_ARG_WITH([jsoncpp], AS_HELP_STRING([--without-jsoncpp], [Build without JsonCpp support]))
+AS_IF([test "x$with_jsoncpp" != "xno"],
+ [PKG_CHECK_MODULES([JsonCpp], [jsoncpp >= 1.7.2], [have_jsoncpp=yes], [have_jsoncpp=no])],
+ [have_jsoncpp=no])
+AS_IF([test "x$have_jsoncpp" = "xyes"], [
+ AC_MSG_NOTICE([Using JsonCpp])
+ CPPFLAGS+=" -DOPENDHT_JSONCPP"
+ AC_SUBST(jsoncpp_lib, [", jsoncpp"])
+], [
+ AC_MSG_NOTICE([Not using JsonCpp])
+ AM_COND_IF(PROXY_CLIENT_OR_SERVER, AC_MSG_ERROR(["JsonCpp is required for proxy/push notification support"]))
+])
+
+AC_ARG_WITH([openssl], AS_HELP_STRING([--without-openssl], [Build with OpenSSL support]))
+AS_IF([test "x$with_openssl" != "xno"],
+ [PKG_CHECK_MODULES([OpenSSL], [openssl >= 1.1], [have_openssl=yes], [have_openssl=no])],
+ [have_openssl=no])
+AS_IF([test "x$have_openssl" = "xyes"], [
+ AC_MSG_NOTICE([Using OpenSSL])
+ AC_SUBST(openssl_lib, [", openssl"])
+], [
+ AC_MSG_NOTICE([Not using OpenSSL])
+])
+
+PKG_CHECK_MODULES([Fmt], [fmt >= 5.3.0], [have_fmt=yes], [have_fmt=no])
+AS_IF([test "x$have_fmt" = "xyes"], [
+ AC_MSG_NOTICE([Using libfmt])
+], [
+ AC_MSG_NOTICE([Missing libfmt files])
+])
+
+AM_COND_IF([PROXY_CLIENT_OR_SERVER], [
+ AC_CHECK_HEADERS([asio.hpp],, AC_MSG_ERROR([Missing Asio headers files]))
+ CXXFLAGS="${CXXFLAGS} -DASIO_STANDALONE"
+ # http_parser has no pkgconfig, instead we check with:
+ AC_CHECK_LIB(http_parser, exit,, AC_MSG_ERROR([Missing HttpParser library files]))
+ AC_CHECK_HEADERS([http_parser.h], [http_parser_headers=yes; break;])
+ AC_SUBST(http_parser_lib, ["-lhttp_parser"])
+ AS_IF([test "x$http_parser_headers" != "xyes"], AC_MSG_ERROR([Missing HttpParser headers files]))
+])
+
+CXXFLAGS="${CXXFLAGS} -DMSGPACK_NO_BOOST -DMSGPACK_DISABLE_LEGACY_NIL -DMSGPACK_DISABLE_LEGACY_CONVERT"
+
+AC_ARG_ENABLE([tools], AS_HELP_STRING([--disable-tools],[Disable tools (CLI DHT node)]),,build_tools=yes)
+AM_CONDITIONAL(ENABLE_TOOLS, test x$build_tools == xyes)
+AM_COND_IF([ENABLE_TOOLS], [
+ AC_CHECK_HEADERS([readline/readline.h readline/history.h], [], [
+ AC_MSG_ERROR([unable to find readline.h])
+ ])
+])
+
+AM_COND_IF(ENABLE_PROXY_SERVER, AC_DEFINE([OPENDHT_PROXY_SERVER], [], [Building with proxy server]))
+AM_COND_IF(ENABLE_PROXY_CLIENT, AC_DEFINE([OPENDHT_PROXY_CLIENT], [], [Building with proxy client]))
+AM_COND_IF(ENABLE_PUSH_NOTIFICATIONS, [CPPFLAGS+=" -DOPENDHT_PUSH_NOTIFICATIONS"], [])
+AM_COND_IF(ENABLE_PROXY_SERVER_IDENTITY, [CPPFLAGS+=" -DOPENDHT_PROXY_SERVER_IDENTITY"], [])
+
+dnl Configure setup.py if we build the python module
+AC_SUBST(CURRENT_SOURCE_DIR, ".")
+AC_SUBST(CURRENT_BINARY_DIR, ".")
+AC_SUBST(PROJECT_SOURCE_DIR, "..")
+AC_SUBST(PROJECT_BINARY_DIR, "../src/.libs")
+
+AC_CONFIG_FILES([Makefile
+ src/Makefile
+ c/Makefile
+ tools/Makefile
+ python/Makefile
+ python/setup.py
+ tests/Makefile
+ doc/Makefile
+ doc/Doxyfile
+ opendht.pc])
+AC_OUTPUT
--- /dev/null
+if (OPENDHT_TOOLS)
+ INSTALL(FILES ${CMAKE_CURRENT_SOURCE_DIR}/dhtnode.1 DESTINATION ${CMAKE_INSTALL_PREFIX}/share/man/man1)
+endif ()
+
+if (OPENDHT_DOCUMENTATION)
+ if (NOT DOXYGEN_FOUND)
+ message(FATAL_ERROR "Doxygen is needed to build the documentation.")
+ endif()
+ configure_file (Doxyfile.in Doxyfile @ONLY)
+ add_custom_target(doc ALL
+ COMMAND ${DOXYGEN_EXECUTABLE} ${CMAKE_CURRENT_BINARY_DIR}/Doxyfile
+ WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
+ COMMENT "Generating API documentation with Doxygen"
+ VERBATIM)
+ install(DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/html DESTINATION share/doc/opendht)
+endif()
--- /dev/null
+# Doxyfile 1.8.9.1
+
+# This file describes the settings to be used by the documentation system
+# doxygen (www.doxygen.org) for this project.
+
+#---------------------------------------------------------------------------
+# Project related configuration options
+#---------------------------------------------------------------------------
+
+DOXYFILE_ENCODING = UTF-8
+
+PROJECT_NAME = @PACKAGE_NAME@
+PROJECT_NUMBER = @PACKAGE_VERSION@
+PROJECT_BRIEF = "C++ Distributed Hash Table"
+PROJECT_LOGO =
+
+OUTPUT_DIRECTORY =
+CREATE_SUBDIRS = NO
+ALLOW_UNICODE_NAMES = NO
+OUTPUT_LANGUAGE = English
+BRIEF_MEMBER_DESC = YES
+REPEAT_BRIEF = YES
+ABBREVIATE_BRIEF =
+ALWAYS_DETAILED_SEC = NO
+INLINE_INHERITED_MEMB = NO
+FULL_PATH_NAMES = YES
+STRIP_FROM_PATH =
+STRIP_FROM_INC_PATH =
+SHORT_NAMES = NO
+JAVADOC_AUTOBRIEF = NO
+QT_AUTOBRIEF = NO
+MULTILINE_CPP_IS_BRIEF = NO
+INHERIT_DOCS = YES
+SEPARATE_MEMBER_PAGES = NO
+TAB_SIZE = 4
+ALIASES =
+TCL_SUBST =
+OPTIMIZE_OUTPUT_FOR_C = NO
+OPTIMIZE_OUTPUT_JAVA = NO
+OPTIMIZE_FOR_FORTRAN = NO
+OPTIMIZE_OUTPUT_VHDL = NO
+EXTENSION_MAPPING =
+MARKDOWN_SUPPORT = YES
+AUTOLINK_SUPPORT = YES
+BUILTIN_STL_SUPPORT = YES
+CPP_CLI_SUPPORT = NO
+SIP_SUPPORT = NO
+IDL_PROPERTY_SUPPORT = YES
+DISTRIBUTE_GROUP_DOC = NO
+SUBGROUPING = YES
+INLINE_GROUPED_CLASSES = NO
+INLINE_SIMPLE_STRUCTS = NO
+TYPEDEF_HIDES_STRUCT = NO
+LOOKUP_CACHE_SIZE = 0
+
+#---------------------------------------------------------------------------
+# Build related configuration options
+#---------------------------------------------------------------------------
+
+EXTRACT_ALL = NO
+EXTRACT_PRIVATE = NO
+EXTRACT_PACKAGE = NO
+EXTRACT_STATIC = YES
+EXTRACT_LOCAL_CLASSES = YES
+EXTRACT_LOCAL_METHODS = NO
+EXTRACT_ANON_NSPACES = NO
+HIDE_UNDOC_MEMBERS = NO
+HIDE_UNDOC_CLASSES = NO
+HIDE_FRIEND_COMPOUNDS = NO
+HIDE_IN_BODY_DOCS = NO
+INTERNAL_DOCS = NO
+CASE_SENSE_NAMES = YES
+HIDE_SCOPE_NAMES = NO
+HIDE_COMPOUND_REFERENCE= NO
+SHOW_INCLUDE_FILES = YES
+SHOW_GROUPED_MEMB_INC = NO
+FORCE_LOCAL_INCLUDES = NO
+INLINE_INFO = YES
+SORT_MEMBER_DOCS = YES
+SORT_BRIEF_DOCS = NO
+SORT_MEMBERS_CTORS_1ST = NO
+SORT_GROUP_NAMES = NO
+SORT_BY_SCOPE_NAME = NO
+STRICT_PROTO_MATCHING = NO
+GENERATE_TODOLIST = YES
+GENERATE_TESTLIST = YES
+GENERATE_BUGLIST = YES
+GENERATE_DEPRECATEDLIST= YES
+ENABLED_SECTIONS =
+MAX_INITIALIZER_LINES = 30
+SHOW_USED_FILES = YES
+SHOW_FILES = YES
+SHOW_NAMESPACES = YES
+FILE_VERSION_FILTER =
+LAYOUT_FILE =
+CITE_BIB_FILES =
+
+#---------------------------------------------------------------------------
+# Configuration options related to warning and progress messages
+#---------------------------------------------------------------------------
+
+QUIET = YES
+WARNINGS = YES
+WARN_IF_UNDOCUMENTED = NO
+WARN_IF_DOC_ERROR = YES
+WARN_NO_PARAMDOC = NO
+WARN_FORMAT = "$file:$line: $text"
+WARN_LOGFILE =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the input files
+#---------------------------------------------------------------------------
+
+INPUT = @top_srcdir@/include
+INPUT_ENCODING = UTF-8
+FILE_PATTERNS = *.cpp *.h
+RECURSIVE = YES
+
+EXCLUDE = @top_srcdir@/include/opendht/serialize.h
+EXCLUDE_SYMLINKS = NO
+EXCLUDE_PATTERNS =
+EXCLUDE_SYMBOLS =
+
+EXAMPLE_PATH =
+EXAMPLE_PATTERNS =
+EXAMPLE_RECURSIVE = NO
+
+IMAGE_PATH =
+INPUT_FILTER =
+FILTER_PATTERNS =
+FILTER_SOURCE_FILES = NO
+FILTER_SOURCE_PATTERNS =
+USE_MDFILE_AS_MAINPAGE =
+
+#---------------------------------------------------------------------------
+# Configuration options related to source browsing
+#---------------------------------------------------------------------------
+
+# If the SOURCE_BROWSER tag is set to YES then a list of source files will be
+# generated. Documented entities will be cross-referenced with these sources.
+#
+# Note: To get rid of all source code in the generated output, make sure that
+# also VERBATIM_HEADERS is set to NO.
+# The default value is: NO.
+
+SOURCE_BROWSER = YES
+INLINE_SOURCES = NO
+STRIP_CODE_COMMENTS = YES
+REFERENCED_BY_RELATION = NO
+REFERENCES_RELATION = NO
+REFERENCES_LINK_SOURCE = YES
+SOURCE_TOOLTIPS = YES
+USE_HTAGS = NO
+VERBATIM_HEADERS = YES
+CLANG_ASSISTED_PARSING = NO
+CLANG_OPTIONS =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the alphabetical class index
+#---------------------------------------------------------------------------
+
+ALPHABETICAL_INDEX = NO
+COLS_IN_ALPHA_INDEX = 5
+IGNORE_PREFIX =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the HTML output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_HTML tag is set to YES, doxygen will generate HTML output
+# The default value is: YES.
+
+GENERATE_HTML = YES
+
+# The HTML_OUTPUT tag is used to specify where the HTML docs will be put. If a
+# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
+# it.
+# The default directory is: html.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_OUTPUT = html
+
+# The HTML_FILE_EXTENSION tag can be used to specify the file extension for each
+# generated HTML page (for example: .htm, .php, .asp).
+# The default value is: .html.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_FILE_EXTENSION = .html
+
+# The HTML_HEADER tag can be used to specify a user-defined HTML header file for
+# each generated HTML page. If the tag is left blank doxygen will generate a
+# standard header.
+#
+# To get valid HTML the header file that includes any scripts and style sheets
+# that doxygen needs, which is dependent on the configuration options used (e.g.
+# the setting GENERATE_TREEVIEW). It is highly recommended to start with a
+# default header using
+# doxygen -w html new_header.html new_footer.html new_stylesheet.css
+# YourConfigFile
+# and then modify the file new_header.html. See also section "Doxygen usage"
+# for information on how to generate the default header that doxygen normally
+# uses.
+# Note: The header is subject to change so you typically have to regenerate the
+# default header when upgrading to a newer version of doxygen. For a description
+# of the possible markers and block names see the documentation.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_HEADER =
+
+# The HTML_FOOTER tag can be used to specify a user-defined HTML footer for each
+# generated HTML page. If the tag is left blank doxygen will generate a standard
+# footer. See HTML_HEADER for more information on how to generate a default
+# footer and what special commands can be used inside the footer. See also
+# section "Doxygen usage" for information on how to generate the default footer
+# that doxygen normally uses.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_FOOTER =
+
+# The HTML_STYLESHEET tag can be used to specify a user-defined cascading style
+# sheet that is used by each HTML page. It can be used to fine-tune the look of
+# the HTML output. If left blank doxygen will generate a default style sheet.
+# See also section "Doxygen usage" for information on how to generate the style
+# sheet that doxygen normally uses.
+# Note: It is recommended to use HTML_EXTRA_STYLESHEET instead of this tag, as
+# it is more robust and this tag (HTML_STYLESHEET) will in the future become
+# obsolete.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_STYLESHEET =
+
+# The HTML_EXTRA_STYLESHEET tag can be used to specify additional user-defined
+# cascading style sheets that are included after the standard style sheets
+# created by doxygen. Using this option one can overrule certain style aspects.
+# This is preferred over using HTML_STYLESHEET since it does not replace the
+# standard style sheet and is therefore more robust against future updates.
+# Doxygen will copy the style sheet files to the output directory.
+# Note: The order of the extra style sheet files is of importance (e.g. the last
+# style sheet in the list overrules the setting of the previous ones in the
+# list). For an example see the documentation.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_EXTRA_STYLESHEET =
+
+# The HTML_EXTRA_FILES tag can be used to specify one or more extra images or
+# other source files which should be copied to the HTML output directory. Note
+# that these files will be copied to the base HTML output directory. Use the
+# $relpath^ marker in the HTML_HEADER and/or HTML_FOOTER files to load these
+# files. In the HTML_STYLESHEET file, use the file name only. Also note that the
+# files will be copied as-is; there are no commands or markers available.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_EXTRA_FILES =
+
+# The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. Doxygen
+# will adjust the colors in the style sheet and background images according to
+# this color. Hue is specified as an angle on a colorwheel, see
+# http://en.wikipedia.org/wiki/Hue for more information. For instance the value
+# 0 represents red, 60 is yellow, 120 is green, 180 is cyan, 240 is blue, 300
+# purple, and 360 is red again.
+# Minimum value: 0, maximum value: 359, default value: 220.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_COLORSTYLE_HUE = 220
+
+# The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of the colors
+# in the HTML output. For a value of 0 the output will use grayscales only. A
+# value of 255 will produce the most vivid colors.
+# Minimum value: 0, maximum value: 255, default value: 100.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_COLORSTYLE_SAT = 100
+
+# The HTML_COLORSTYLE_GAMMA tag controls the gamma correction applied to the
+# luminance component of the colors in the HTML output. Values below 100
+# gradually make the output lighter, whereas values above 100 make the output
+# darker. The value divided by 100 is the actual gamma applied, so 80 represents
+# a gamma of 0.8, The value 220 represents a gamma of 2.2, and 100 does not
+# change the gamma.
+# Minimum value: 40, maximum value: 240, default value: 80.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_COLORSTYLE_GAMMA = 80
+
+# If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML
+# page will contain the date and time when the page was generated. Setting this
+# to NO can help when comparing the output of multiple runs.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_TIMESTAMP = YES
+
+# If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML
+# documentation will contain sections that can be hidden and shown after the
+# page has loaded.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_DYNAMIC_SECTIONS = NO
+
+# With HTML_INDEX_NUM_ENTRIES one can control the preferred number of entries
+# shown in the various tree structured indices initially; the user can expand
+# and collapse entries dynamically later on. Doxygen will expand the tree to
+# such a level that at most the specified number of entries are visible (unless
+# a fully collapsed tree already exceeds this amount). So setting the number of
+# entries 1 will produce a full collapsed tree by default. 0 is a special value
+# representing an infinite number of entries and will result in a full expanded
+# tree by default.
+# Minimum value: 0, maximum value: 9999, default value: 100.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_INDEX_NUM_ENTRIES = 100
+
+# If the GENERATE_DOCSET tag is set to YES, additional index files will be
+# generated that can be used as input for Apple's Xcode 3 integrated development
+# environment (see: http://developer.apple.com/tools/xcode/), introduced with
+# OSX 10.5 (Leopard). To create a documentation set, doxygen will generate a
+# Makefile in the HTML output directory. Running make will produce the docset in
+# that directory and running make install will install the docset in
+# ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find it at
+# startup. See http://developer.apple.com/tools/creatingdocsetswithdoxygen.html
+# for more information.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+GENERATE_DOCSET = NO
+
+# This tag determines the name of the docset feed. A documentation feed provides
+# an umbrella under which multiple documentation sets from a single provider
+# (such as a company or product suite) can be grouped.
+# The default value is: Doxygen generated docs.
+# This tag requires that the tag GENERATE_DOCSET is set to YES.
+
+DOCSET_FEEDNAME = "Doxygen generated docs"
+
+# This tag specifies a string that should uniquely identify the documentation
+# set bundle. This should be a reverse domain-name style string, e.g.
+# com.mycompany.MyDocSet. Doxygen will append .docset to the name.
+# The default value is: org.doxygen.Project.
+# This tag requires that the tag GENERATE_DOCSET is set to YES.
+
+DOCSET_BUNDLE_ID = org.doxygen.Project
+
+# The DOCSET_PUBLISHER_ID tag specifies a string that should uniquely identify
+# the documentation publisher. This should be a reverse domain-name style
+# string, e.g. com.mycompany.MyDocSet.documentation.
+# The default value is: org.doxygen.Publisher.
+# This tag requires that the tag GENERATE_DOCSET is set to YES.
+
+DOCSET_PUBLISHER_ID = com.savoirfairelinux
+
+# The DOCSET_PUBLISHER_NAME tag identifies the documentation publisher.
+# The default value is: Publisher.
+# This tag requires that the tag GENERATE_DOCSET is set to YES.
+
+DOCSET_PUBLISHER_NAME = Savoir-faire Linux
+
+# If the GENERATE_HTMLHELP tag is set to YES then doxygen generates three
+# additional HTML index files: index.hhp, index.hhc, and index.hhk. The
+# index.hhp is a project file that can be read by Microsoft's HTML Help Workshop
+# (see: http://www.microsoft.com/en-us/download/details.aspx?id=21138) on
+# Windows.
+#
+# The HTML Help Workshop contains a compiler that can convert all HTML output
+# generated by doxygen into a single compiled HTML file (.chm). Compiled HTML
+# files are now used as the Windows 98 help format, and will replace the old
+# Windows help format (.hlp) on all Windows platforms in the future. Compressed
+# HTML files also contain an index, a table of contents, and you can search for
+# words in the documentation. The HTML workshop also contains a viewer for
+# compressed HTML files.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+GENERATE_HTMLHELP = NO
+
+# The CHM_FILE tag can be used to specify the file name of the resulting .chm
+# file. You can add a path in front of the file if the result should not be
+# written to the html output directory.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+CHM_FILE =
+
+# The HHC_LOCATION tag can be used to specify the location (absolute path
+# including file name) of the HTML help compiler (hhc.exe). If non-empty,
+# doxygen will try to run the HTML help compiler on the generated index.hhp.
+# The file has to be specified with full path.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+HHC_LOCATION =
+
+# The GENERATE_CHI flag controls if a separate .chi index file is generated
+# (YES) or that it should be included in the master .chm file (NO).
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+GENERATE_CHI = NO
+
+# The CHM_INDEX_ENCODING is used to encode HtmlHelp index (hhk), content (hhc)
+# and project file content.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+CHM_INDEX_ENCODING =
+
+# The BINARY_TOC flag controls whether a binary table of contents is generated
+# (YES) or a normal table of contents (NO) in the .chm file. Furthermore it
+# enables the Previous and Next buttons.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+BINARY_TOC = NO
+
+# The TOC_EXPAND flag can be set to YES to add extra items for group members to
+# the table of contents of the HTML help documentation and to the tree view.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+TOC_EXPAND = NO
+
+# If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and
+# QHP_VIRTUAL_FOLDER are set, an additional index file will be generated that
+# can be used as input for Qt's qhelpgenerator to generate a Qt Compressed Help
+# (.qch) of the generated HTML documentation.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+GENERATE_QHP = NO
+
+# If the QHG_LOCATION tag is specified, the QCH_FILE tag can be used to specify
+# the file name of the resulting .qch file. The path specified is relative to
+# the HTML output folder.
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QCH_FILE =
+
+# The QHP_NAMESPACE tag specifies the namespace to use when generating Qt Help
+# Project output. For more information please see Qt Help Project / Namespace
+# (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#namespace).
+# The default value is: org.doxygen.Project.
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHP_NAMESPACE = org.doxygen.Project
+
+# The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating Qt
+# Help Project output. For more information please see Qt Help Project / Virtual
+# Folders (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#virtual-
+# folders).
+# The default value is: doc.
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHP_VIRTUAL_FOLDER = doc
+
+# If the QHP_CUST_FILTER_NAME tag is set, it specifies the name of a custom
+# filter to add. For more information please see Qt Help Project / Custom
+# Filters (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#custom-
+# filters).
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHP_CUST_FILTER_NAME =
+
+# The QHP_CUST_FILTER_ATTRS tag specifies the list of the attributes of the
+# custom filter to add. For more information please see Qt Help Project / Custom
+# Filters (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#custom-
+# filters).
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHP_CUST_FILTER_ATTRS =
+
+# The QHP_SECT_FILTER_ATTRS tag specifies the list of the attributes this
+# project's filter section matches. Qt Help Project / Filter Attributes (see:
+# http://qt-project.org/doc/qt-4.8/qthelpproject.html#filter-attributes).
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHP_SECT_FILTER_ATTRS =
+
+# The QHG_LOCATION tag can be used to specify the location of Qt's
+# qhelpgenerator. If non-empty doxygen will try to run qhelpgenerator on the
+# generated .qhp file.
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHG_LOCATION =
+
+# If the GENERATE_ECLIPSEHELP tag is set to YES, additional index files will be
+# generated, together with the HTML files, they form an Eclipse help plugin. To
+# install this plugin and make it available under the help contents menu in
+# Eclipse, the contents of the directory containing the HTML and XML files needs
+# to be copied into the plugins directory of eclipse. The name of the directory
+# within the plugins directory should be the same as the ECLIPSE_DOC_ID value.
+# After copying Eclipse needs to be restarted before the help appears.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+GENERATE_ECLIPSEHELP = NO
+
+# A unique identifier for the Eclipse help plugin. When installing the plugin
+# the directory name containing the HTML and XML files should also have this
+# name. Each documentation set should have its own identifier.
+# The default value is: org.doxygen.Project.
+# This tag requires that the tag GENERATE_ECLIPSEHELP is set to YES.
+
+ECLIPSE_DOC_ID = org.doxygen.Project
+
+# If you want full control over the layout of the generated HTML pages it might
+# be necessary to disable the index and replace it with your own. The
+# DISABLE_INDEX tag can be used to turn on/off the condensed index (tabs) at top
+# of each HTML page. A value of NO enables the index and the value YES disables
+# it. Since the tabs in the index contain the same information as the navigation
+# tree, you can set this option to YES if you also set GENERATE_TREEVIEW to YES.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+DISABLE_INDEX = NO
+
+# The GENERATE_TREEVIEW tag is used to specify whether a tree-like index
+# structure should be generated to display hierarchical information. If the tag
+# value is set to YES, a side panel will be generated containing a tree-like
+# index structure (just like the one that is generated for HTML Help). For this
+# to work a browser that supports JavaScript, DHTML, CSS and frames is required
+# (i.e. any modern browser). Windows users are probably better off using the
+# HTML help feature. Via custom style sheets (see HTML_EXTRA_STYLESHEET) one can
+# further fine-tune the look of the index. As an example, the default style
+# sheet generated by doxygen has an example that shows how to put an image at
+# the root of the tree instead of the PROJECT_NAME. Since the tree basically has
+# the same information as the tab index, you could consider setting
+# DISABLE_INDEX to YES when enabling this option.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+GENERATE_TREEVIEW = NO
+
+# The ENUM_VALUES_PER_LINE tag can be used to set the number of enum values that
+# doxygen will group on one line in the generated HTML documentation.
+#
+# Note that a value of 0 will completely suppress the enum values from appearing
+# in the overview section.
+# Minimum value: 0, maximum value: 20, default value: 4.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+ENUM_VALUES_PER_LINE = 4
+
+# If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be used
+# to set the initial width (in pixels) of the frame in which the tree is shown.
+# Minimum value: 0, maximum value: 1500, default value: 250.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+TREEVIEW_WIDTH = 250
+
+# If the EXT_LINKS_IN_WINDOW option is set to YES, doxygen will open links to
+# external symbols imported via tag files in a separate window.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+EXT_LINKS_IN_WINDOW = NO
+
+# Use this tag to change the font size of LaTeX formulas included as images in
+# the HTML documentation. When you change the font size after a successful
+# doxygen run you need to manually remove any form_*.png images from the HTML
+# output directory to force them to be regenerated.
+# Minimum value: 8, maximum value: 50, default value: 10.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+FORMULA_FONTSIZE = 10
+
+# Use the FORMULA_TRANPARENT tag to determine whether or not the images
+# generated for formulas are transparent PNGs. Transparent PNGs are not
+# supported properly for IE 6.0, but are supported on all modern browsers.
+#
+# Note that when changing this option you need to delete any form_*.png files in
+# the HTML output directory before the changes have effect.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+FORMULA_TRANSPARENT = YES
+
+# Enable the USE_MATHJAX option to render LaTeX formulas using MathJax (see
+# http://www.mathjax.org) which uses client side Javascript for the rendering
+# instead of using pre-rendered bitmaps. Use this if you do not have LaTeX
+# installed or if you want to formulas look prettier in the HTML output. When
+# enabled you may also need to install MathJax separately and configure the path
+# to it using the MATHJAX_RELPATH option.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+USE_MATHJAX = NO
+
+# When MathJax is enabled you can set the default output format to be used for
+# the MathJax output. See the MathJax site (see:
+# http://docs.mathjax.org/en/latest/output.html) for more details.
+# Possible values are: HTML-CSS (which is slower, but has the best
+# compatibility), NativeMML (i.e. MathML) and SVG.
+# The default value is: HTML-CSS.
+# This tag requires that the tag USE_MATHJAX is set to YES.
+
+MATHJAX_FORMAT = HTML-CSS
+
+# When MathJax is enabled you need to specify the location relative to the HTML
+# output directory using the MATHJAX_RELPATH option. The destination directory
+# should contain the MathJax.js script. For instance, if the mathjax directory
+# is located at the same level as the HTML output directory, then
+# MATHJAX_RELPATH should be ../mathjax. The default value points to the MathJax
+# Content Delivery Network so you can quickly see the result without installing
+# MathJax. However, it is strongly recommended to install a local copy of
+# MathJax from http://www.mathjax.org before deployment.
+# The default value is: http://cdn.mathjax.org/mathjax/latest.
+# This tag requires that the tag USE_MATHJAX is set to YES.
+
+MATHJAX_RELPATH = http://cdn.mathjax.org/mathjax/latest
+
+# The MATHJAX_EXTENSIONS tag can be used to specify one or more MathJax
+# extension names that should be enabled during MathJax rendering. For example
+# MATHJAX_EXTENSIONS = TeX/AMSmath TeX/AMSsymbols
+# This tag requires that the tag USE_MATHJAX is set to YES.
+
+MATHJAX_EXTENSIONS =
+
+# The MATHJAX_CODEFILE tag can be used to specify a file with javascript pieces
+# of code that will be used on startup of the MathJax code. See the MathJax site
+# (see: http://docs.mathjax.org/en/latest/output.html) for more details. For an
+# example see the documentation.
+# This tag requires that the tag USE_MATHJAX is set to YES.
+
+MATHJAX_CODEFILE =
+
+# When the SEARCHENGINE tag is enabled doxygen will generate a search box for
+# the HTML output. The underlying search engine uses javascript and DHTML and
+# should work on any modern browser. Note that when using HTML help
+# (GENERATE_HTMLHELP), Qt help (GENERATE_QHP), or docsets (GENERATE_DOCSET)
+# there is already a search function so this one should typically be disabled.
+# For large projects the javascript based search engine can be slow, then
+# enabling SERVER_BASED_SEARCH may provide a better solution. It is possible to
+# search using the keyboard; to jump to the search box use <access key> + S
+# (what the <access key> is depends on the OS and browser, but it is typically
+# <CTRL>, <ALT>/<option>, or both). Inside the search box use the <cursor down
+# key> to jump into the search results window, the results can be navigated
+# using the <cursor keys>. Press <Enter> to select an item or <escape> to cancel
+# the search. The filter options can be selected when the cursor is inside the
+# search box by pressing <Shift>+<cursor down>. Also here use the <cursor keys>
+# to select a filter and <Enter> or <escape> to activate or cancel the filter
+# option.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+SEARCHENGINE = YES
+
+# When the SERVER_BASED_SEARCH tag is enabled the search engine will be
+# implemented using a web server instead of a web client using Javascript. There
+# are two flavors of web server based searching depending on the EXTERNAL_SEARCH
+# setting. When disabled, doxygen will generate a PHP script for searching and
+# an index file used by the script. When EXTERNAL_SEARCH is enabled the indexing
+# and searching needs to be provided by external tools. See the section
+# "External Indexing and Searching" for details.
+# The default value is: NO.
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+SERVER_BASED_SEARCH = NO
+
+# When EXTERNAL_SEARCH tag is enabled doxygen will no longer generate the PHP
+# script for searching. Instead the search results are written to an XML file
+# which needs to be processed by an external indexer. Doxygen will invoke an
+# external search engine pointed to by the SEARCHENGINE_URL option to obtain the
+# search results.
+#
+# Doxygen ships with an example indexer (doxyindexer) and search engine
+# (doxysearch.cgi) which are based on the open source search engine library
+# Xapian (see: http://xapian.org/).
+#
+# See the section "External Indexing and Searching" for details.
+# The default value is: NO.
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+EXTERNAL_SEARCH = NO
+
+# The SEARCHENGINE_URL should point to a search engine hosted by a web server
+# which will return the search results when EXTERNAL_SEARCH is enabled.
+#
+# Doxygen ships with an example indexer (doxyindexer) and search engine
+# (doxysearch.cgi) which are based on the open source search engine library
+# Xapian (see: http://xapian.org/). See the section "External Indexing and
+# Searching" for details.
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+SEARCHENGINE_URL =
+
+# When SERVER_BASED_SEARCH and EXTERNAL_SEARCH are both enabled the unindexed
+# search data is written to a file for indexing by an external tool. With the
+# SEARCHDATA_FILE tag the name of this file can be specified.
+# The default file is: searchdata.xml.
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+SEARCHDATA_FILE = searchdata.xml
+
+# When SERVER_BASED_SEARCH and EXTERNAL_SEARCH are both enabled the
+# EXTERNAL_SEARCH_ID tag can be used as an identifier for the project. This is
+# useful in combination with EXTRA_SEARCH_MAPPINGS to search through multiple
+# projects and redirect the results back to the right project.
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+EXTERNAL_SEARCH_ID =
+
+# The EXTRA_SEARCH_MAPPINGS tag can be used to enable searching through doxygen
+# projects other than the one defined by this configuration file, but that are
+# all added to the same external search index. Each project needs to have a
+# unique id set via EXTERNAL_SEARCH_ID. The search mapping then maps the id of
+# to a relative location where the documentation can be found. The format is:
+# EXTRA_SEARCH_MAPPINGS = tagname1=loc1 tagname2=loc2 ...
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+EXTRA_SEARCH_MAPPINGS =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the LaTeX output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_LATEX tag is set to YES, doxygen will generate LaTeX output.
+# The default value is: YES.
+
+GENERATE_LATEX = NO
+
+# The LATEX_OUTPUT tag is used to specify where the LaTeX docs will be put. If a
+# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
+# it.
+# The default directory is: latex.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_OUTPUT = latex
+
+# The LATEX_CMD_NAME tag can be used to specify the LaTeX command name to be
+# invoked.
+#
+# Note that when enabling USE_PDFLATEX this option is only used for generating
+# bitmaps for formulas in the HTML output, but not in the Makefile that is
+# written to the output directory.
+# The default file is: latex.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_CMD_NAME = latex
+
+# The MAKEINDEX_CMD_NAME tag can be used to specify the command name to generate
+# index for LaTeX.
+# The default file is: makeindex.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+MAKEINDEX_CMD_NAME = makeindex
+
+# If the COMPACT_LATEX tag is set to YES, doxygen generates more compact LaTeX
+# documents. This may be useful for small projects and may help to save some
+# trees in general.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+COMPACT_LATEX = NO
+
+# The PAPER_TYPE tag can be used to set the paper type that is used by the
+# printer.
+# Possible values are: a4 (210 x 297 mm), letter (8.5 x 11 inches), legal (8.5 x
+# 14 inches) and executive (7.25 x 10.5 inches).
+# The default value is: a4.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+PAPER_TYPE = a4
+
+# The EXTRA_PACKAGES tag can be used to specify one or more LaTeX package names
+# that should be included in the LaTeX output. To get the times font for
+# instance you can specify
+# EXTRA_PACKAGES=times
+# If left blank no extra packages will be included.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+EXTRA_PACKAGES =
+
+# The LATEX_HEADER tag can be used to specify a personal LaTeX header for the
+# generated LaTeX document. The header should contain everything until the first
+# chapter. If it is left blank doxygen will generate a standard header. See
+# section "Doxygen usage" for information on how to let doxygen write the
+# default header to a separate file.
+#
+# Note: Only use a user-defined header if you know what you are doing! The
+# following commands have a special meaning inside the header: $title,
+# $datetime, $date, $doxygenversion, $projectname, $projectnumber,
+# $projectbrief, $projectlogo. Doxygen will replace $title with the empty
+# string, for the replacement values of the other commands the user is referred
+# to HTML_HEADER.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_HEADER =
+
+# The LATEX_FOOTER tag can be used to specify a personal LaTeX footer for the
+# generated LaTeX document. The footer should contain everything after the last
+# chapter. If it is left blank doxygen will generate a standard footer. See
+# LATEX_HEADER for more information on how to generate a default footer and what
+# special commands can be used inside the footer.
+#
+# Note: Only use a user-defined footer if you know what you are doing!
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_FOOTER =
+
+# The LATEX_EXTRA_STYLESHEET tag can be used to specify additional user-defined
+# LaTeX style sheets that are included after the standard style sheets created
+# by doxygen. Using this option one can overrule certain style aspects. Doxygen
+# will copy the style sheet files to the output directory.
+# Note: The order of the extra style sheet files is of importance (e.g. the last
+# style sheet in the list overrules the setting of the previous ones in the
+# list).
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_EXTRA_STYLESHEET =
+
+# The LATEX_EXTRA_FILES tag can be used to specify one or more extra images or
+# other source files which should be copied to the LATEX_OUTPUT output
+# directory. Note that the files will be copied as-is; there are no commands or
+# markers available.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_EXTRA_FILES =
+
+# If the PDF_HYPERLINKS tag is set to YES, the LaTeX that is generated is
+# prepared for conversion to PDF (using ps2pdf or pdflatex). The PDF file will
+# contain links (just like the HTML output) instead of page references. This
+# makes the output suitable for online browsing using a PDF viewer.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+PDF_HYPERLINKS = YES
+
+# If the USE_PDFLATEX tag is set to YES, doxygen will use pdflatex to generate
+# the PDF file directly from the LaTeX files. Set this option to YES, to get a
+# higher quality PDF documentation.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+USE_PDFLATEX = YES
+
+# If the LATEX_BATCHMODE tag is set to YES, doxygen will add the \batchmode
+# command to the generated LaTeX files. This will instruct LaTeX to keep running
+# if errors occur, instead of asking the user for help. This option is also used
+# when generating formulas in HTML.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_BATCHMODE = NO
+
+# If the LATEX_HIDE_INDICES tag is set to YES then doxygen will not include the
+# index chapters (such as File Index, Compound Index, etc.) in the output.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_HIDE_INDICES = NO
+
+# If the LATEX_SOURCE_CODE tag is set to YES then doxygen will include source
+# code with syntax highlighting in the LaTeX output.
+#
+# Note that which sources are shown also depends on other settings such as
+# SOURCE_BROWSER.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_SOURCE_CODE = NO
+
+# The LATEX_BIB_STYLE tag can be used to specify the style to use for the
+# bibliography, e.g. plainnat, or ieeetr. See
+# http://en.wikipedia.org/wiki/BibTeX and \cite for more info.
+# The default value is: plain.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_BIB_STYLE = plain
+
+#---------------------------------------------------------------------------
+# Configuration options related to the RTF output
+#---------------------------------------------------------------------------
+
+GENERATE_RTF = NO
+RTF_OUTPUT = rtf
+COMPACT_RTF = NO
+RTF_HYPERLINKS = NO
+RTF_STYLESHEET_FILE =
+RTF_EXTENSIONS_FILE =
+RTF_SOURCE_CODE = NO
+
+#---------------------------------------------------------------------------
+# Configuration options related to the man page output
+#---------------------------------------------------------------------------
+
+GENERATE_MAN = NO
+MAN_OUTPUT = man
+MAN_EXTENSION = .3
+MAN_SUBDIR =
+MAN_LINKS = NO
+
+#---------------------------------------------------------------------------
+# Configuration options related to the XML output
+#---------------------------------------------------------------------------
+
+GENERATE_XML = NO
+XML_OUTPUT = xml
+XML_PROGRAMLISTING = YES
+
+#---------------------------------------------------------------------------
+# Configuration options related to the DOCBOOK output
+#---------------------------------------------------------------------------
+
+GENERATE_DOCBOOK = NO
+DOCBOOK_OUTPUT = docbook
+DOCBOOK_PROGRAMLISTING = NO
+
+#---------------------------------------------------------------------------
+# Configuration options for the AutoGen Definitions output
+#---------------------------------------------------------------------------
+
+GENERATE_AUTOGEN_DEF = NO
+
+#---------------------------------------------------------------------------
+# Configuration options related to the Perl module output
+#---------------------------------------------------------------------------
+
+GENERATE_PERLMOD = NO
+PERLMOD_LATEX = NO
+PERLMOD_PRETTY = YES
+PERLMOD_MAKEVAR_PREFIX =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the preprocessor
+#---------------------------------------------------------------------------
+
+ENABLE_PREPROCESSING = YES
+MACRO_EXPANSION = NO
+EXPAND_ONLY_PREDEF = NO
+SEARCH_INCLUDES = YES
+INCLUDE_PATH =
+INCLUDE_FILE_PATTERNS =
+PREDEFINED =
+EXPAND_AS_DEFINED =
+SKIP_FUNCTION_MACROS = YES
+
+#---------------------------------------------------------------------------
+# Configuration options related to external references
+#---------------------------------------------------------------------------
+
+TAGFILES =
+GENERATE_TAGFILE =
+ALLEXTERNALS = NO
+EXTERNAL_GROUPS = YES
+EXTERNAL_PAGES = YES
+PERL_PATH = /usr/bin/perl
+
+#---------------------------------------------------------------------------
+# Configuration options related to the dot tool
+#---------------------------------------------------------------------------
+
+CLASS_DIAGRAMS = YES
+MSCGEN_PATH =
+DIA_PATH =
+HIDE_UNDOC_RELATIONS = YES
+HAVE_DOT = YES
+DOT_NUM_THREADS = 0
+DOT_FONTNAME = Helvetica
+DOT_FONTSIZE = 10
+DOT_FONTPATH =
+CLASS_GRAPH = YES
+COLLABORATION_GRAPH = YES
+GROUP_GRAPHS = YES
+UML_LOOK = NO
+UML_LIMIT_NUM_FIELDS = 10
+TEMPLATE_RELATIONS = NO
+INCLUDE_GRAPH = YES
+INCLUDED_BY_GRAPH = YES
+CALL_GRAPH = NO
+CALLER_GRAPH = NO
+GRAPHICAL_HIERARCHY = YES
+DIRECTORY_GRAPH = YES
+DOT_IMAGE_FORMAT = png
+INTERACTIVE_SVG = NO
+DOT_PATH =
+DOTFILE_DIRS =
+MSCFILE_DIRS =
+DIAFILE_DIRS =
+PLANTUML_JAR_PATH =
+PLANTUML_INCLUDE_PATH =
+DOT_GRAPH_MAX_NODES = 50
+MAX_DOT_GRAPH_DEPTH = 0
+DOT_TRANSPARENT = NO
+DOT_MULTI_TARGETS = NO
+GENERATE_LEGEND = YES
+DOT_CLEANUP = YES
--- /dev/null
+dist_man1_MANS = dhtnode.1
+
+noinst_HEADERS = \
+ Doxyfile
+
+if HAVE_DOXYGEN
+doxyfile.stamp: Doxyfile
+ $(DOXYGEN) Doxyfile
+ echo stamp > doxyfile.stamp
+
+CLEANFILES = doxyfile.stamp
+
+all-local: doxyfile.stamp
+clean-local:
+ rm -rf $(top_srcdir)/doc/man $(top_srcdir)/doc/html
+endif
--- /dev/null
+\documentclass[11pt]{article}
+
+\usepackage[utf8x]{inputenc}
+\usepackage[top=2cm,bottom=2cm]{geometry}
+
+\usepackage{forloop}
+\newcounter{counter}
+
+% maths
+\usepackage{amssymb}
+\usepackage{amsmath}
+\usepackage{mathrsfs}
+\usepackage{shadethm}
+\usepackage{amsthm}
+\newshadetheorem{shadeDef}{Definition}[section]
+\newtheorem{remark}{Remark}[section]
+\renewcommand{\P}{\mathbb{P}}
+
+\usepackage{float}
+\usepackage{booktabs}
+
+\setlength{\parindent}{0ex}
+\setlength{\parskip}{0.5em}
+
+\begin{document}
+ \title{Annex 1: Probabilistic analysis of connectivity changes}
+ \author{Adrien Béraud, Simon Désaulniers, Guillaume Roguez}
+ \maketitle
+ \pagestyle{empty}
+ \begin{shadeDef}
+ A node flagged as \emph{``expired''} by a node $n$ is a node which has not responded to any
+ of $n$'s last three requests.
+ \end{shadeDef}
+ \begin{remark}
+ An expired node will not be contacted before 10 minutes from its expiration time.
+ \end{remark}
+
+ Let $N$ the DHT network, $n_0\in N$, a given node and the following probabilistic events:
+ \begin{itemize}
+ \item $A$: $\forall n \in N$ $n$ is unreachable by $n_0$, \emph{i.e.} $n_0$ lost connection
+ with $N$;
+ \item $B$: $S\subset N$, the nodes unreachable by $n_0$ with $k={|S|\over|N|}$;
+ \item $C$: $m \le |N|$ nodes are flagged as ``expired''.
+ \end{itemize}
+
+ We are interested in knowing $\P(A|C)$, \emph{i.e.} the probability of the event where $A$ occurs
+ prior to $C$. From the above, we immediately get
+ $$\left\{
+ \begin{array}{ll}
+ \P(C|A) & = 1\\
+ \P(A) + \P(B) & = 1
+ \end{array}
+ \right.$$
+ Also, the event $A|C$ can be abstracted as the urn problem of draw without replacement. Then,
+ $$\P(C|B) = \prod_{i=0}^m \left[k|N| - i \over |N|\right] = \prod_{i=0}^m \left[k - { i \over |N| }\right]$$
+ Furthermore, using Bayes' theroem we have
+ \begin{align*}
+ \P(A|C) & = { \P(C|A)\P(A) \over \P(C|A)\P(A) + \P(C|B)\P(B)}\\
+ & = { \P(A) \over \P(A) + \P(C|B)\P(B) }\\
+ & = { \P(A) \over \P(A) + \P(C|B)\left[1 - \P(A)\right] }\\
+ \Rightarrow \forloop{counter}{0}{\value{counter} < 5}{\qquad}\quad \P(A) & =
+ \P(A|C)\left[\P(A) + \P(C|B)\left(1 - \P(A)\right) \right] \\
+ \Rightarrow \forloop{counter}{0}{\value{counter} < 2}{\qquad}\;\:\, \P(A)\left[{ 1 \over \P(A|C)} - 1\right] & =
+ \P(C|B)\left(1 - \P(A)\right) \\
+ \end{align*}
+ Finally,
+ \begin{equation}
+ \label{eq:final}
+ \left[{\P(A) \over 1 - \P(A)}\right]\left[{ 1 \over \P(A|C)} - 1\right] =
+ \prod_{i=0}^m \left[k - { i \over |N| }\right]
+ \end{equation}
+
+ From \eqref{eq:final}, we may set a plausible configuration $\{\P(A),\P(A|C),k,|N|\}$ letting us
+ produce results such as in table \ref{tbl:k_1_2}, \ref{tbl:k_2_3} and \ref{tbl:k_3_4}.
+
+ \begin{table}[H]
+ \centering
+ \caption{The values for $m$ assuming $\P(A|C) \ge 0.95,\, k = {1 \over 2}$}
+ \label{tbl:k_1_2}
+ \begin{tabular}{lcccc}
+ \toprule
+ $|N| \diagdown \P(A)$ & ${1 \over 10 }$ & ${1 \over 100 }$ & ${1 \over 1000 }$ & ${1 \over 10000 }$\\
+ \midrule
+ $2^0$ & 1 & 1 & 1 & 1\\
+ $2^1$ & 1 & 1 & 1 & 1\\
+ $2^2$ & 2 & 2 & 2 & 2\\
+ $2^3$ & 4 & 4 & 4 & 4\\
+ $2^4$ & 5 & 6 & 7 & 8\\
+ $2^5$ & 5 & 7 & 9 & 10\\
+ $2^6$ & 6 & 9 & 11 & 13\\
+ $2^7$ & 6 & 9 & 12 & 14\\
+ $2^8$ & 7 & 10 & 13 & 16\\
+ $2^9$ & 7 & 10 & 13 & 16\\
+ $2^{10}$ & 7 & 10 & 13 & 17\\
+ \bottomrule
+ \end{tabular}
+ \end{table}
+ \begin{table}[H]
+ \centering
+ \caption{The values for $m$ assuming $\P(A|C) \ge 0.95,\, k = {2 \over 3}$}
+ \label{tbl:k_2_3}
+ \begin{tabular}{lcccc}
+ \toprule
+ $|N| \diagdown \P(A)$ & ${1 \over 10 }$ & ${1 \over 100 }$ & ${1 \over 1000 }$ & ${1 \over 10000 }$\\
+ \midrule
+ $2^0$ & 1 & 1 & 1 & 1\\
+ $2^1$ & 2 & 2 & 2 & 2\\
+ $2^2$ & 3 & 3 & 4 & 4\\
+ $2^3$ & 5 & 5 & 6 & 8\\
+ $2^4$ & 6 & 8 & 9 & 10\\
+ $2^5$ & 8 & 10 & 12 & 14\\
+ $2^6$ & 9 & 13 & 16 & 18\\
+ $2^7$ & 11 & 15 & 18 & 22\\
+ $2^8$ & 11 & 16 & 21 & 25\\
+ $2^9$ & 12 & 17 & 22 & 27\\
+ $2^{10}$ & 12 & 18 & 23 & 28\\
+ \bottomrule
+ \end{tabular}
+ \end{table}
+ \begin{table}[H]
+ \centering
+ \caption{The values for $m$ assuming $\P(A|C) \ge 0.95,\, k = {3 \over 4}$}
+ \label{tbl:k_3_4}
+ \begin{tabular}{lcccc}
+ \toprule
+ $|N| \diagdown \P(A)$ & ${1 \over 10 }$ & ${1 \over 100 }$ & ${1 \over 1000 }$ & ${1 \over 10000 }$\\
+ \midrule
+ $2^0$ & 1 & 1 & 1 & 1\\
+ $2^1$ & 2 & 2 & 2 & 2\\
+ $2^2$ & 3 & 3 & 3 & 3\\
+ $2^3$ & 5 & 6 & 6 & 6\\
+ $2^4$ & 7 & 9 & 10 & 11\\
+ $2^5$ & 10 & 12 & 14 & 16\\
+ $2^6$ & 12 & 16 & 19 & 22\\
+ $2^7$ & 14 & 19 & 23 & 27\\
+ $2^8$ & 15 & 21 & 27 & 32\\
+ $2^9$ & 16 & 23 & 30 & 36\\
+ $2^{10}$ & 17 & 24 & 31 & 38\\
+ \bottomrule
+ \end{tabular}
+ \end{table}
+\end{document}
--- /dev/null
+.TH DHTNODE 1 2019-06-08
+.SH NAME
+.B dhtnode
+- a simple OpenDHT command line node runner.
+.SH SYNOPSIS
+.B dhtnode
+[\fB\-h\fR]
+[\fB\-v\fR [\fB\-l\fR \fIlogfile\fR] [\fB\-L\fR]]
+[\fB\-i\fR [\fB\-\-save\-identity\fR \fIfile\fR]]
+[\fB\-d\fR] [\fB\-s\fR]
+[\fB\-n\fR \fInetwork_id\fR]
+[\fB\-p\fR \fIlocal_port\fR]
+[\fB\-b\fR \fIbootstrap_host\fR[:\fIport\fR]]
+[\fB\-\-certificate\fR \fIfile\fR]
+[\fB\-\-privkey\fR \fIfile\fR]
+[\fB\-\-privkey\-password\fR \fIpassword\fR]
+[\fB\-\-proxyserver\fR \fIport\fR]
+[\fB\-\-proxyclient\fR \fIserver\fR]
+.SH DESCRIPTION
+Runs an OpenDHT node, with a CLI (default) or as a daemon (with \fB'-d'\fP or \fB'-s'\fP).
+Commands available in the interactive shell are:
+.EX
+ h, help Print this help message.
+ q, quit Quit the program.
+ log Start/stop printing DHT logs.
+
+ Node information:
+ ll Print basic information and stats about the current node.
+ ls Print basic information about current searches.
+ ld Print basic information about currenty stored values on this node.
+ lr Print the full current routing table of this node
+
+ Operations on the DHT:
+ b ip:port Ping potential node at given IP address/port.
+ g [key] Get values at [key].
+ l [key] Listen for value changes at [key].
+ p [key] [str] Put string value at [key].
+ s [key] [str] Put string value at [key], signed with our generated
+ private key.
+ e [key] [dest] [str] Put string value at [key], encrypted for [dest] with
+ its public key (if found).
+.EE
+.SH OPTIONS
+.TP
+\fB\-h\fP
+Prints some help.
+.TP
+\fB\-v\fP
+Enable the verbose mode (log to stdout/stderr by default)
+.TP
+\fB\-l\fP \fIlog_file\fP
+Write log to file instead of stdout/stderr
+.TP
+\fB\-L\fP
+Write log to syslog instead of stdout/stderr
+.TP
+\fB\-i\fP
+Generate cryptographic identity for the node.
+.TP
+\fB\-\-save\-identity\fP \fIfile\fP
+Save generated identity (certificate and private key) to given file prefix.
+.TP
+\fB\-\-certificate\fP \fIfile\fP
+Load identity certificate from given file.
+.TP
+\fB\-\-privkey\fP \fIfile\fP
+Load identity private key from given file.
+.TP
+\fB\-\-privkey\-password\fP \fIpassword\fP
+Password to use for private key encryption or decryption (optional).
+.TP
+\fB\-d\fP
+Run the program in daemon mode (will fork in the background).
+.TP
+\fB\-s\fP
+Run the program in service mode (non\-forking daemon).
+.TP
+\fB\-D\fP
+Enables multicast automatic local peer discovery.
+.TP
+\fB\-f\fP \fIfile\fP
+Specify a file path to persist/load the node state.
+.TP
+\fB\-n\fP \fInetwork_id\fP
+Specify the network id. This let you connect to distinct networks and prevents
+the merge of two different networks (available since OpenDHT v0.6.1).
+.TP
+\fB\-p\fP \fIlocal_port\fP
+Use UDP port \fIlocal_port\fP for the program to bind to.
+.TP
+\fB\-b\fP \fIbootstrap_host\fP[:\fIport\fP]
+The program needs to be given a node to connect to the network. You use this
+option to provide the ip address of that node.
+.TP
+\fB\-\-proxyserver\fP \fIlocal_port\fP
+Run a proxy server bound to this DHT node on HTTP port \fIlocal_port\fP
+.TP
+\fB\-\-proxyclient\fP \fIserver\fP
+Run this DHT node in proxy client mode, and connect to \fIserver\fP
+.SH AUTHORS
+.TP
+Program written by
+.IP \(bu
+.\}
+Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+.TP
+Man page written by
+.IP \(bu
+.\}
+Simon Désaulniers <sim.desaulniers@gmail.com>
--- /dev/null
+FROM ghcr.io/savoirfairelinux/opendht/opendht-deps: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 \
+ -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
--- /dev/null
+FROM ghcr.io/savoirfairelinux/opendht/opendht-deps-alpine:latest AS build
+LABEL maintainer="Adrien Béraud <adrien.beraud@savoirfairelinux.com>"
+LABEL org.opencontainers.image.source https://github.com/savoirfairelinux/opendht
+
+COPY . opendht
+
+RUN mkdir /install
+ENV DESTDIR /install
+
+RUN cd opendht && mkdir build && cd build \
+ && cmake .. -DCMAKE_INSTALL_PREFIX=/usr \
+ -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
+
+FROM alpine:3.18 AS install
+COPY --from=build /install /
+RUN apk add --no-cache \
+ libstdc++ \
+ gnutls \
+ nettle \
+ openssl \
+ argon2-dev \
+ jsoncpp \
+ fmt \
+ http-parser \
+ readline \
+ ncurses
+CMD ["dhtnode", "-b", "bootstrap.jami.net", "-p", "4222", "--proxyserver", "8080"]
+EXPOSE 4222/udp
+EXPOSE 8080/tcp
--- /dev/null
+FROM ghcr.io/savoirfairelinux/opendht/opendht-deps-bionic: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 \
+ -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 \
+ && make -j8 && make install \
+ && cd ../.. && rm -rf opendht
--- /dev/null
+FROM ubuntu:22.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 \
+ libtool autotools-dev autoconf \
+ cython3 python3-dev python3-setuptools python3-build python3-virtualenv \
+ libncurses5-dev libreadline-dev nettle-dev libcppunit-dev \
+ libgnutls28-dev libuv1-dev libjsoncpp-dev libargon2-dev \
+ libssl-dev libfmt-dev libhttp-parser-dev libasio-dev libmsgpack-dev \
+ && apt-get clean && rm -rf /var/lib/apt/lists/* /var/cache/apt/*
+
+RUN echo "*** Downloading RESTinio ***" \
+ && mkdir restinio && cd restinio \
+ && wget https://github.com/aberaud/restinio/archive/6fd08b65f6f15899dd0de3c801f6a5462b811c64.tar.gz \
+ && ls -l && tar -xzf 6fd08b65f6f15899dd0de3c801f6a5462b811c64.tar.gz \
+ && cd restinio-6fd08b65f6f15899dd0de3c801f6a5462b811c64/dev \
+ && cmake -DCMAKE_INSTALL_PREFIX=/usr -DRESTINIO_TEST=OFF -DRESTINIO_SAMPLE=OFF \
+ -DRESTINIO_INSTALL_SAMPLES=OFF -DRESTINIO_BENCH=OFF -DRESTINIO_INSTALL_BENCHES=OFF \
+ -DRESTINIO_FIND_DEPS=ON -DRESTINIO_ALLOW_SOBJECTIZER=Off -DRESTINIO_USE_BOOST_ASIO=none . \
+ && make -j8 && make install \
+ && cd ../../.. && rm -rf restinio
--- /dev/null
+FROM alpine:3.18
+LABEL maintainer="Adrien Béraud <adrien.beraud@savoirfairelinux.com>"
+LABEL org.opencontainers.image.source https://github.com/savoirfairelinux/opendht
+
+RUN apk add --no-cache \
+ build-base cmake ninja git wget \
+ cython python3-dev py3-setuptools \
+ ncurses-dev readline-dev nettle-dev \
+ cppunit-dev gnutls-dev jsoncpp-dev \
+ argon2-dev openssl-dev fmt-dev \
+ http-parser-dev asio-dev msgpack-cxx-dev \
+ && rm -rf /var/cache/apk/*
+
+RUN echo "*** Downloading RESTinio ***" \
+ && mkdir restinio && cd restinio \
+ && wget https://github.com/aberaud/restinio/archive/6fd08b65f6f15899dd0de3c801f6a5462b811c64.tar.gz \
+ && tar -xzf 6fd08b65f6f15899dd0de3c801f6a5462b811c64.tar.gz \
+ && cd restinio-6fd08b65f6f15899dd0de3c801f6a5462b811c64/dev \
+ && cmake -DCMAKE_INSTALL_PREFIX=/usr -DRESTINIO_TEST=OFF -DRESTINIO_SAMPLE=OFF \
+ -DRESTINIO_INSTALL_SAMPLES=OFF -DRESTINIO_BENCH=OFF -DRESTINIO_INSTALL_BENCHES=OFF \
+ -DRESTINIO_FIND_DEPS=ON -DRESTINIO_ALLOW_SOBJECTIZER=Off -DRESTINIO_USE_BOOST_ASIO=none . \
+ && make -j8 && make install \
+ && cd ../../.. && rm -rf restinio
--- /dev/null
+FROM ubuntu:18.04
+LABEL maintainer="Adrien Béraud <adrien.beraud@savoirfairelinux.com>"
+LABEL org.opencontainers.image.source https://github.com/savoirfairelinux/opendht
+
+RUN echo "APT::Acquire::Retries \"3\";" > /etc/apt/apt.conf.d/80-retries
+RUN apt-get update && apt-get install -y \
+ apt-transport-https build-essential pkg-config git wget libncurses5-dev libreadline-dev nettle-dev \
+ libgnutls28-dev libuv1-dev cython3 python3-dev python3-setuptools libcppunit-dev libjsoncpp-dev \
+ libargon2-0-dev \
+ autotools-dev autoconf libfmt-dev libhttp-parser-dev libmsgpack-dev libssl-dev python3-pip \
+ && apt-get clean && rm -rf /var/lib/apt/lists/* /var/cache/apt/*
+
+RUN pip3 install --upgrade cmake
+
+# libasio-dev (1.10) is too old
+RUN echo "** Building a recent version of asio ***" \
+ && wget https://github.com/aberaud/asio/archive/a7d66ef4017d8f1b7f2cef1bb4ba8e23b0961571.tar.gz \
+ && tar -xvf a7d66ef4017d8f1b7f2cef1bb4ba8e23b0961571.tar.gz && cd asio-a7d66ef4017d8f1b7f2cef1bb4ba8e23b0961571/asio \
+ && ./autogen.sh && ./configure --prefix=/usr --without-boost --disable-examples --disable-tests \
+ && make install \
+ && cd ../../ && rm -rf asio*
+
+RUN echo "*** Downloading RESTinio ***" \
+ && mkdir restinio && cd restinio \
+ && wget https://github.com/aberaud/restinio/archive/8d5d3e8237e0947adb9ba1ffc8281f4ad7cb2a59.tar.gz \
+ && ls -l && tar -xzf 8d5d3e8237e0947adb9ba1ffc8281f4ad7cb2a59.tar.gz \
+ && cd restinio-8d5d3e8237e0947adb9ba1ffc8281f4ad7cb2a59/dev \
+ && cmake -DCMAKE_INSTALL_PREFIX=/usr -DRESTINIO_TEST=OFF -DRESTINIO_SAMPLE=OFF \
+ -DRESTINIO_INSTALL_SAMPLES=OFF -DRESTINIO_BENCH=OFF -DRESTINIO_INSTALL_BENCHES=OFF \
+ -DRESTINIO_FIND_DEPS=ON -DRESTINIO_ALLOW_SOBJECTIZER=Off -DRESTINIO_USE_BOOST_ASIO=none . \
+ && make -j8 && make install \
+ && cd ../../ && rm -rf restinio*
--- /dev/null
+FROM ubuntu:20.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 \
+ libtool autotools-dev autoconf \
+ cython3 python3-dev python3-setuptools \
+ libncurses5-dev libreadline-dev nettle-dev libcppunit-dev \
+ libgnutls28-dev libuv1-dev libjsoncpp-dev libargon2-dev \
+ libssl-dev libfmt-dev libhttp-parser-dev libasio-dev libmsgpack-dev \
+ && apt-get clean && rm -rf /var/lib/apt/lists/* /var/cache/apt/*
+
+RUN echo "*** Downloading RESTinio ***" \
+ && mkdir restinio && cd restinio \
+ && wget https://github.com/aberaud/restinio/archive/e0a261dd8488246a3cb8bbb3ea781ea5139c3c94.tar.gz \
+ && ls -l && tar -xzf e0a261dd8488246a3cb8bbb3ea781ea5139c3c94.tar.gz \
+ && cd restinio-e0a261dd8488246a3cb8bbb3ea781ea5139c3c94/dev \
+ && cmake -DCMAKE_INSTALL_PREFIX=/usr -DRESTINIO_TEST=OFF -DRESTINIO_SAMPLE=OFF \
+ -DRESTINIO_INSTALL_SAMPLES=OFF -DRESTINIO_BENCH=OFF -DRESTINIO_INSTALL_BENCHES=OFF \
+ -DRESTINIO_FIND_DEPS=ON -DRESTINIO_ALLOW_SOBJECTIZER=Off -DRESTINIO_USE_BOOST_ASIO=none . \
+ && make -j8 && make install \
+ && cd ../../.. && rm -rf restinio
--- /dev/null
+FROM ubuntu:22.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 llvm llvm-dev lldb clang gdb make cmake pkg-config \
+ libtool git wget libncurses5-dev libreadline-dev \
+ nettle-dev libgnutls28-dev libuv1-dev libmsgpack-dev libjsoncpp-dev cython3 python3-dev \
+ python3-setuptools libcppunit-dev python3-pip python3-build python3-virtualenv \
+ autotools-dev autoconf libssl-dev libargon2-dev \
+ libfmt-dev libhttp-parser-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/*
+
+ENV CC cc
+ENV CXX c++
+
+RUN echo "*** Downloading RESTinio ***" \
+ && mkdir restinio && cd restinio \
+ && wget https://github.com/aberaud/restinio/archive/6fd08b65f6f15899dd0de3c801f6a5462b811c64.tar.gz \
+ && ls -l && tar -xzf 6fd08b65f6f15899dd0de3c801f6a5462b811c64.tar.gz \
+ && cd restinio-6fd08b65f6f15899dd0de3c801f6a5462b811c64/dev \
+ && cmake -DCMAKE_INSTALL_PREFIX=/usr -DRESTINIO_TEST=OFF -DRESTINIO_SAMPLE=OFF \
+ -DRESTINIO_INSTALL_SAMPLES=OFF -DRESTINIO_BENCH=OFF -DRESTINIO_INSTALL_BENCHES=OFF \
+ -DRESTINIO_FIND_DEPS=ON -DRESTINIO_ALLOW_SOBJECTIZER=Off -DRESTINIO_USE_BOOST_ASIO=none . \
+ && make -j8 && make install \
+ && cd ../../ && rm -rf restinio*
--- /dev/null
+FROM ghcr.io/savoirfairelinux/opendht/opendht:latest
+CMD ["dhtnode", "-b", "bootstrap.jami.net", "-p", "4222", "--proxyserver", "8080"]
+EXPOSE 4222/udp
+EXPOSE 8080/tcp
--- /dev/null
+FROM ghcr.io/savoirfairelinux/opendht/opendht-deps-bionic: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 \
+ -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 \
+ && make -j8 && make install \
+ && cd ../.. && rm -rf opendht
--- /dev/null
+FROM ghcr.io/savoirfairelinux/opendht/opendht-deps-llvm: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 \
+ -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
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ * Author : Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "opendht/dhtrunner.h"
+#include "opendht/default_types.h"
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ * Authors: Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ * Simon Désaulniers <simon.desaulniers@savoirfairelinux.com>
+ * Sébastien Blin <sebastien.blin@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "infohash.h"
+#include "value.h"
+
+#include <vector>
+#include <memory>
+#include <functional>
+#include <string>
+
+#ifdef OPENDHT_JSONCPP
+#include <json/json.h>
+#endif
+
+namespace dht {
+
+struct Node;
+
+/**
+ * Current status of a DHT node.
+ */
+enum class NodeStatus {
+ Disconnected, // 0 nodes
+ Connecting, // 1+ nodes
+ Connected // 1+ good nodes
+};
+
+inline constexpr const char*
+statusToStr(NodeStatus status) {
+ return status == NodeStatus::Connected ? "connected" : (
+ status == NodeStatus::Connecting ? "connecting" :
+ "disconnected");
+}
+
+struct OPENDHT_PUBLIC NodeStats {
+ unsigned good_nodes {0},
+ dubious_nodes {0},
+ cached_nodes {0},
+ incoming_nodes {0};
+ unsigned table_depth {0};
+ unsigned searches {0};
+ unsigned node_cache_size {0};
+ unsigned getKnownNodes() const { return good_nodes + dubious_nodes; }
+ unsigned long getNetworkSizeEstimation() const { return 8 * std::exp2(table_depth); }
+ std::string toString() const;
+
+#ifdef OPENDHT_JSONCPP
+ /**
+ * Build a json object from a NodeStats
+ */
+ Json::Value toJson() const;
+ NodeStats() {};
+ explicit NodeStats(const Json::Value& v);
+#endif
+
+ MSGPACK_DEFINE_MAP(good_nodes, dubious_nodes, cached_nodes, incoming_nodes, table_depth, searches, node_cache_size)
+};
+
+struct OPENDHT_PUBLIC NodeInfo {
+ InfoHash id;
+ InfoHash node_id;
+ NodeStats ipv4 {};
+ NodeStats ipv6 {};
+ size_t ongoing_ops {0};
+ size_t storage_values {0};
+ size_t storage_size {0};
+ in_port_t bound4 {0};
+ in_port_t bound6 {0};
+
+#ifdef OPENDHT_JSONCPP
+ /**
+ * Build a json object from a NodeStats
+ */
+ Json::Value toJson() const;
+ NodeInfo() {};
+ explicit NodeInfo(const Json::Value& v);
+#endif
+
+ MSGPACK_DEFINE_MAP(id, node_id, ipv4, ipv6)
+};
+
+/**
+ * Dht configuration.
+ */
+struct OPENDHT_PUBLIC Config {
+ /** DHT node ID */
+ InfoHash node_id {};
+
+ /**
+ * DHT network ID. A node will only talk with other nodes having
+ * the same network ID.
+ * Network ID 0 (default) represents the main public network.
+ */
+ NetId network {0};
+
+ /** For testing purposes only, enables bootstrap mode */
+ bool is_bootstrap {false};
+
+ /** Makes the DHT responsible to maintain its stored values. Consumes more ressources. */
+ bool maintain_storage {false};
+
+ /** If set, the dht will load its state from this file on start and save its state in this file on shutdown */
+ std::string persist_path {};
+
+ /** If non-0, overrides the default global rate-limit. -1 means no limit. */
+ ssize_t max_req_per_sec {0};
+
+ /** 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, overrides the default maximum number of searches. -1 means no limit. */
+ ssize_t max_searches {0};
+
+ /* If non-0, overrides the default maximum store size. -1 means no limit. */
+ ssize_t max_store_size {0};
+
+ /* If non-0, overrides the default maximum store key count. -1 means no limit. */
+ ssize_t max_store_keys {0};
+
+ /**
+ * Use appropriate bahavior for a public IP, stable node:
+ * - No connectivity change triggered when a search fails
+ * - Larger listen refresh time
+ */
+ bool public_stable {false};
+};
+
+/**
+ * SecureDht configuration.
+ */
+struct OPENDHT_PUBLIC SecureDhtConfig
+{
+ Config node_config {};
+ crypto::Identity id {};
+
+ /**
+ * Cache all encountered public keys and certificates,
+ * for use by the certificate store, putEncrypted and putSigned
+ */
+ bool cert_cache_all {false};
+};
+
+static constexpr size_t DEFAULT_STORAGE_LIMIT {1024 * 1024 * 64};
+
+using ValuesExport = std::pair<InfoHash, Blob>;
+
+using QueryCallback = std::function<bool(const std::vector<std::shared_ptr<FieldValueIndex>>& fields)>;
+using GetCallback = std::function<bool(const std::vector<std::shared_ptr<Value>>& values)>;
+using ValueCallback = std::function<bool(const std::vector<std::shared_ptr<Value>>& values, bool expired)>;
+using GetCallbackSimple = std::function<bool(std::shared_ptr<Value> value)>;
+using ShutdownCallback = std::function<void()>;
+using IdentityAnnouncedCb = std::function<void(bool)>;
+using PublicAddressChangedCb = std::function<void(std::vector<SockAddr>)>;
+
+using CertificateStoreQuery = std::function<std::vector<std::shared_ptr<crypto::Certificate>>(const InfoHash& pk_id)>;
+using DoneCallback = std::function<void(bool success, const std::vector<std::shared_ptr<Node>>& nodes)>;
+using DoneCallbackSimple = std::function<void(bool success)>;
+
+typedef bool (*GetCallbackRaw)(std::shared_ptr<Value>, void *user_data);
+typedef bool (*ValueCallbackRaw)(std::shared_ptr<Value>, bool expired, void *user_data);
+typedef void (*DoneCallbackRaw)(bool, std::vector<std::shared_ptr<Node>>*, void *user_data);
+typedef void (*ShutdownCallbackRaw)(void *user_data);
+typedef void (*DoneCallbackSimpleRaw)(bool, void *user_data);
+typedef bool (*FilterRaw)(const Value&, void *user_data);
+
+
+OPENDHT_PUBLIC GetCallbackSimple bindGetCb(GetCallbackRaw raw_cb, void* user_data);
+OPENDHT_PUBLIC GetCallback bindGetCb(GetCallbackSimple cb);
+OPENDHT_PUBLIC ValueCallback bindValueCb(ValueCallbackRaw raw_cb, void* user_data);
+OPENDHT_PUBLIC ShutdownCallback bindShutdownCb(ShutdownCallbackRaw shutdown_cb_raw, void* user_data);
+OPENDHT_PUBLIC DoneCallback bindDoneCb(DoneCallbackSimple donecb);
+OPENDHT_PUBLIC DoneCallback bindDoneCb(DoneCallbackRaw raw_cb, void* user_data);
+OPENDHT_PUBLIC DoneCallbackSimple bindDoneCbSimple(DoneCallbackSimpleRaw raw_cb, void* user_data);
+OPENDHT_PUBLIC Value::Filter bindFilterRaw(FilterRaw raw_filter, void* user_data);
+
+}
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ * Author : Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ * Vsevolod Ivanov <vsevolod.ivanov@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "infohash.h"
+#include "utils.h"
+#include "rng.h"
+
+extern "C" {
+#include <gnutls/gnutls.h>
+#include <gnutls/abstract.h>
+#include <gnutls/x509.h>
+#include <gnutls/ocsp.h>
+}
+
+#include <vector>
+#include <memory>
+#include <atomic>
+#include <mutex>
+#include <string_view>
+
+#ifdef _WIN32
+#include <iso646.h>
+#endif
+
+namespace dht {
+
+/**
+ * Contains all crypto primitives
+ */
+namespace crypto {
+
+class OPENDHT_PUBLIC CryptoException : public std::runtime_error {
+public:
+ explicit CryptoException(const std::string& str) : std::runtime_error(str) {};
+ explicit CryptoException(const char* str) : std::runtime_error(str) {};
+ CryptoException(const CryptoException& e) noexcept = default;
+ CryptoException& operator=(const CryptoException&) noexcept = default;
+};
+
+/**
+ * Exception thrown when a decryption error happened.
+ */
+class OPENDHT_PUBLIC DecryptError : public CryptoException {
+public:
+ explicit DecryptError(const std::string& str) : CryptoException(str) {};
+ explicit DecryptError(const char* str) : CryptoException(str) {};
+ DecryptError(const DecryptError& e) noexcept = default;
+ DecryptError& operator=(const DecryptError&) noexcept = default;
+};
+
+struct PrivateKey;
+struct Certificate;
+class RevocationList;
+
+using Identity = std::pair<std::shared_ptr<PrivateKey>, std::shared_ptr<Certificate>>;
+
+/**
+ * A public key.
+ */
+struct OPENDHT_PUBLIC PublicKey
+{
+ PublicKey();
+
+ /**
+ * Takes ownership of an existing gnutls_pubkey.
+ */
+ PublicKey(gnutls_pubkey_t k) : pk(k) {}
+
+ /** Import public key from serialized data */
+ PublicKey(const uint8_t* dat, size_t dat_size);
+ PublicKey(const Blob& pk) : PublicKey(pk.data(), pk.size()) {}
+ PublicKey(std::string_view pk) : PublicKey((const uint8_t*)pk.data(), pk.size()) {}
+ PublicKey(PublicKey&& o) noexcept : pk(o.pk) { o.pk = nullptr; }
+
+ ~PublicKey();
+ explicit operator bool() const { return pk; }
+ bool operator ==(const PublicKey& o) const {
+ return pk == o.pk || getLongId() == o.getLongId();
+ }
+ bool operator !=(const PublicKey& o) const {
+ return !(*this == o);
+ }
+
+ PublicKey& operator=(PublicKey&& o) noexcept;
+
+ /**
+ * Get public key fingerprint
+ */
+ const InfoHash& getId() const;
+
+ /**
+ * Get public key long fingerprint
+ */
+ const PkId& getLongId() const;
+
+ bool checkSignature(const uint8_t* data, size_t data_len, const uint8_t* signature, size_t signature_len) const;
+ inline bool checkSignature(const Blob& data, const Blob& signature) const {
+ return checkSignature(data.data(), data.size(), signature.data(), signature.size());
+ }
+
+ Blob encrypt(const uint8_t* data, size_t data_len) const;
+ inline Blob encrypt(const Blob& data) const {
+ return encrypt(data.data(), data.size());
+ }
+ inline Blob encrypt(std::string_view data) const {
+ return encrypt((const uint8_t*)data.data(), data.size());
+ }
+ void pack(Blob& b) const;
+ int pack(uint8_t* out, size_t* out_len) const;
+ void unpack(const uint8_t* dat, size_t dat_size);
+
+ std::string toString() const;
+
+ template <typename Packer>
+ void msgpack_pack(Packer& p) const
+ {
+ Blob b;
+ pack(b);
+ p.pack_bin(b.size());
+ p.pack_bin_body((const char*)b.data(), b.size());
+ }
+
+ void msgpack_unpack(const msgpack::object& o);
+
+ gnutls_digest_algorithm_t getPreferredDigest() const;
+
+ gnutls_pubkey_t pk {nullptr};
+private:
+ mutable InfoHash cachedId_ {};
+ mutable PkId cachedLongId_ {};
+ mutable std::atomic_bool idCached_ {false};
+ mutable std::atomic_bool longIdCached_ {false};
+
+ PublicKey(const PublicKey&) = delete;
+ PublicKey& operator=(const PublicKey&) = delete;
+ void encryptBloc(const uint8_t* src, size_t src_size, uint8_t* dst, size_t dst_size) const;
+};
+
+/**
+ * A private key, including the corresponding public key.
+ */
+struct OPENDHT_PUBLIC PrivateKey
+{
+ PrivateKey();
+ //PrivateKey(gnutls_privkey_t k) : key(k) {}
+
+ /**
+ * Takes ownership of an existing gnutls_x509_privkey.
+ */
+ PrivateKey(gnutls_x509_privkey_t k);
+
+ PrivateKey(PrivateKey&& o) noexcept;
+ PrivateKey& operator=(PrivateKey&& o) noexcept;
+
+ PrivateKey(const uint8_t* src, size_t src_size, const char* password = nullptr);
+ PrivateKey(const Blob& src, const std::string& password = {}) : PrivateKey(src.data(), src.size(), password.c_str()) {}
+ PrivateKey(std::string_view src, const std::string& password = {}) : PrivateKey((const uint8_t*)src.data(), src.size(), password.c_str()) {}
+
+ ~PrivateKey();
+ explicit operator bool() const { return key; }
+
+ const PublicKey& getPublicKey() const;
+ const std::shared_ptr<PublicKey>& getSharedPublicKey() const;
+
+ int serialize(uint8_t* out, size_t* out_len, const std::string& password = {}) const;
+ Blob serialize(const std::string& password = {}) const;
+
+ /**
+ * Sign the provided binary object.
+ * @returns the signature data.
+ */
+ Blob sign(const uint8_t* data, size_t data_len) const;
+ inline Blob sign(std::string_view dat) const { return sign((const uint8_t*)dat.data(), dat.size()); }
+ inline Blob sign(const Blob& dat) const { return sign(dat.data(), dat.size()); }
+
+ /**
+ * Try to decrypt the provided cypher text.
+ * In case of failure a CryptoException is thrown.
+ * @returns the decrypted data.
+ */
+ Blob decrypt(const uint8_t* cypher, size_t cypher_len) const;
+ Blob decrypt(const Blob& cypher) const { return decrypt(cypher.data(), cypher.size()); }
+
+ /**
+ * Generate a new RSA key pair
+ * @param key_length : size of the modulus in bits
+ * Minimim value: 2048
+ * Recommended values: 4096, 8192
+ */
+ static PrivateKey generate(unsigned key_length = 4096);
+ static PrivateKey generateEC();
+
+ gnutls_privkey_t key {};
+ gnutls_x509_privkey_t x509_key {};
+private:
+ PrivateKey(const PrivateKey&) = delete;
+ PrivateKey& operator=(const PrivateKey&) = delete;
+ Blob decryptBloc(const uint8_t* src, size_t src_size) const;
+
+ mutable std::mutex publicKeyMutex_ {};
+ mutable std::shared_ptr<PublicKey> publicKey_ {};
+};
+
+class OPENDHT_PUBLIC RevocationList
+{
+ using clock = std::chrono::system_clock;
+ using time_point = clock::time_point;
+ using duration = clock::duration;
+public:
+ RevocationList();
+ RevocationList(const Blob& b);
+ RevocationList(RevocationList&& o) noexcept : crl(o.crl) { o.crl = nullptr; }
+ ~RevocationList();
+
+ RevocationList& operator=(RevocationList&& o) { crl = o.crl; o.crl = nullptr; return *this; }
+
+ void pack(Blob& b) const;
+ void unpack(const uint8_t* dat, size_t dat_size);
+ Blob getPacked() const {
+ Blob b;
+ pack(b);
+ return b;
+ }
+
+ template <typename Packer>
+ void msgpack_pack(Packer& p) const
+ {
+ Blob b = getPacked();
+ p.pack_bin(b.size());
+ p.pack_bin_body((const char*)b.data(), b.size());
+ }
+
+ void msgpack_unpack(const msgpack::object& o);
+
+ void revoke(const Certificate& crt, time_point t = time_point::min());
+
+ bool isRevoked(const Certificate& crt) const;
+
+ /**
+ * Sign this revocation list using provided key and certificate.
+ * Validity_period sets the duration until next update (default to no next update).
+ */
+ 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;
+
+ std::string toString() const;
+
+ /**
+ * Read the CRL number extension field.
+ */
+ Blob getNumber() const;
+
+ /** Read CRL issuer Common Name (CN) */
+ std::string getIssuerName() const;
+
+ /** Read CRL issuer User ID (UID) */
+ std::string getIssuerUID() const;
+
+ time_point getUpdateTime() const;
+ time_point getNextUpdateTime() const;
+
+ gnutls_x509_crl_t get() { return crl; }
+ gnutls_x509_crl_t getCopy() const {
+ if (not crl)
+ return nullptr;
+ auto copy = RevocationList(getPacked());
+ gnutls_x509_crl_t ret = copy.crl;
+ copy.crl = nullptr;
+ return ret;
+ }
+
+private:
+ gnutls_x509_crl_t crl {};
+ RevocationList(const RevocationList&) = delete;
+ RevocationList& operator=(const RevocationList&) = delete;
+};
+
+enum class NameType { UNKNOWN = 0, RFC822, DNS, URI, IP };
+
+class OPENDHT_PUBLIC CertificateRequest {
+public:
+ CertificateRequest();
+ CertificateRequest(const uint8_t* data, size_t size);
+ CertificateRequest(std::string_view src) : CertificateRequest((const uint8_t*)src.data(), src.size()) {}
+ CertificateRequest(const Blob& data) : CertificateRequest(data.data(), data.size()) {}
+
+ CertificateRequest(CertificateRequest&& o) noexcept : request(std::move(o.request)) {
+ o.request = nullptr;
+ }
+ CertificateRequest& operator=(CertificateRequest&& o) noexcept;
+
+ ~CertificateRequest();
+
+ void setName(const std::string& name);
+ void setUID(const std::string& name);
+ void setAltName(NameType type, const std::string& name);
+
+ std::string getName() const;
+ std::string getUID() const;
+
+ void sign(const PrivateKey& key, const std::string& password = {});
+
+ bool verify() const;
+
+ Blob pack() const;
+ std::string toString() const;
+
+ gnutls_x509_crq_t get() const { return request; }
+private:
+ CertificateRequest(const CertificateRequest& o) = delete;
+ CertificateRequest& operator=(const CertificateRequest& o) = delete;
+ gnutls_x509_crq_t request {nullptr};
+};
+
+class OPENDHT_PUBLIC OcspRequest
+{
+public:
+ OcspRequest(gnutls_ocsp_req_t r) : request(r) {}
+ OcspRequest(const uint8_t* dat_ptr, size_t dat_size);
+ OcspRequest(std::string_view dat): OcspRequest((const uint8_t*)dat.data(), dat.size()) {}
+ ~OcspRequest();
+
+ /*
+ * Get OCSP Request in readable format.
+ */
+ std::string toString(const bool compact = true) const;
+
+ Blob pack() const;
+ Blob getNonce() const;
+private:
+ gnutls_ocsp_req_t request;
+};
+
+class OPENDHT_PUBLIC OcspResponse
+{
+public:
+ OcspResponse(const uint8_t* dat_ptr, size_t dat_size);
+ OcspResponse(std::string_view response) : OcspResponse((const uint8_t*)response.data(), response.size()) {}
+ ~OcspResponse();
+
+ Blob pack() const;
+ /*
+ * Get OCSP Response in readable format.
+ */
+ std::string toString(const bool compact = true) const;
+
+ /*
+ * Get OCSP response certificate status.
+ * Return certificate status.
+ * http://www.gnu.org/software/gnutls/reference/gnutls-ocsp.html#gnutls-ocsp-cert-status-t
+ */
+ gnutls_ocsp_cert_status_t getCertificateStatus() const;
+
+ /*
+ * Verify OCSP response and return OCSP status.
+ * Throws CryptoException in case of error in the response.
+ * http://www.gnu.org/software/gnutls/reference/gnutls-ocsp.html#gnutls-ocsp-verify-reason-t
+ */
+ gnutls_ocsp_cert_status_t verifyDirect(const Certificate& crt, const Blob& nonce);
+
+private:
+ gnutls_ocsp_resp_t response;
+};
+
+struct OPENDHT_PUBLIC Certificate {
+ Certificate() noexcept {}
+
+ /**
+ * Take ownership of existing gnutls structure
+ */
+ Certificate(gnutls_x509_crt_t crt) noexcept : cert(crt) {}
+
+ Certificate(Certificate&& o) noexcept
+ : cert(o.cert)
+ , issuer(std::move(o.issuer))
+ , publicKey_(std::move(o.publicKey_))
+ { o.cert = nullptr; };
+
+ /**
+ * Import certificate (PEM or DER) or certificate chain (PEM),
+ * ordered from subject to issuer
+ */
+ Certificate(const Blob& crt);
+ Certificate(const uint8_t* dat, size_t dat_size) : cert(nullptr) {
+ unpack(dat, dat_size);
+ }
+ Certificate(std::string_view pem) : Certificate((const uint8_t*)pem.data(), pem.size()) {}
+
+ /**
+ * Import certificate chain (PEM or DER),
+ * ordered from subject to issuer
+ */
+ template<typename Iterator>
+ Certificate(const Iterator& begin, const Iterator& end) {
+ unpack(begin, end);
+ }
+
+ /**
+ * Import certificate chain (PEM or DER),
+ * ordered from subject to issuer
+ */
+ template<typename Iterator>
+ Certificate(const std::vector<std::pair<Iterator, Iterator>>& certs) {
+ unpack(certs);
+ }
+
+ Certificate& operator=(Certificate&& o) noexcept;
+ ~Certificate();
+
+ void pack(Blob& b) const;
+ void unpack(const uint8_t* dat, size_t dat_size);
+ Blob getPacked() const {
+ Blob b;
+ pack(b);
+ return b;
+ }
+
+ /**
+ * Import certificate chain (PEM or DER).
+ * Certificates are not checked during import.
+ *
+ * Iterator is the type of an iterator or pointer to
+ * gnutls_x509_crt_t or Blob instances to import, that should be
+ * ordered from subject to issuer.
+ */
+ template<typename Iterator>
+ void unpack(const Iterator& begin, const Iterator& end)
+ {
+ std::shared_ptr<Certificate> tmp_subject {};
+ std::shared_ptr<Certificate> first {};
+ for (Iterator icrt = begin; icrt < end; ++icrt) {
+ auto tmp_crt = std::make_shared<Certificate>(*icrt);
+ if (tmp_subject)
+ tmp_subject->issuer = tmp_crt;
+ tmp_subject = std::move(tmp_crt);
+ if (!first)
+ first = tmp_subject;
+ }
+ *this = first ? std::move(*first) : Certificate();
+ }
+
+ /**
+ * Import certificate chain (PEM or DER).
+ * Certificates are not checked during import.
+ *
+ * Iterator is the type of an iterator or pointer to the bytes of
+ * the certificates to import.
+ *
+ * @param certs list of (begin, end) iterator pairs, pointing to the
+ * PEM or DER certificate data to import, that should be
+ * ordered from subject to issuer.
+ */
+ template<typename Iterator>
+ void unpack(const std::vector<std::pair<Iterator, Iterator>>& certs)
+ {
+ std::shared_ptr<Certificate> tmp_issuer;
+ // reverse iteration
+ for (auto li = certs.rbegin(); li != certs.rend(); ++li) {
+ Certificate tmp_crt;
+ gnutls_x509_crt_init(&tmp_crt.cert);
+ const gnutls_datum_t crt_dt {(uint8_t*)&(*li->first), (unsigned)(li->second-li->first)};
+ int err = gnutls_x509_crt_import(tmp_crt.cert, &crt_dt, GNUTLS_X509_FMT_PEM);
+ if (err != GNUTLS_E_SUCCESS)
+ err = gnutls_x509_crt_import(tmp_crt.cert, &crt_dt, GNUTLS_X509_FMT_DER);
+ if (err != GNUTLS_E_SUCCESS)
+ throw CryptoException(std::string("Could not read certificate - ") + gnutls_strerror(err));
+ tmp_crt.issuer = tmp_issuer;
+ tmp_issuer = std::make_shared<Certificate>(std::move(tmp_crt));
+ }
+ *this = tmp_issuer ? std::move(*tmp_issuer) : Certificate();
+ }
+
+ template <typename Packer>
+ void msgpack_pack(Packer& p) const
+ {
+ Blob b;
+ pack(b);
+ p.pack_bin(b.size());
+ p.pack_bin_body((const char*)b.data(), b.size());
+ }
+
+ void msgpack_unpack(const msgpack::object& o);
+
+ explicit operator bool() const { return cert; }
+ const PublicKey& getPublicKey() const;
+ const std::shared_ptr<PublicKey>& getSharedPublicKey() const;
+
+ /** Same as getPublicKey().getId() */
+ const InfoHash& getId() const;
+ /** Same as getPublicKey().getLongId() */
+ const PkId& getLongId() const;
+
+ Blob getSerialNumber() const;
+
+ /** Read certificate Common Name (CN) */
+ std::string getName() const;
+
+ /** Read certificate User ID (UID) */
+ std::string getUID() const;
+
+ /** Read certificate issuer Common Name (CN) */
+ std::string getIssuerName() const;
+
+ /** Read certificate issuer User ID (UID) */
+ std::string getIssuerUID() const;
+
+ /** Read certificate alternative names */
+ std::vector<std::pair<NameType, std::string>> getAltNames() const;
+
+ std::chrono::system_clock::time_point getActivation() const;
+ std::chrono::system_clock::time_point getExpiration() const;
+
+ /**
+ * Returns true if the certificate is marked as a Certificate Authority
+ * and has necessary key usage flags to sign certificates.
+ */
+ bool isCA() const;
+
+ /**
+ * PEM encoded certificate.
+ * If chain is true, the issuer chain will be included (default).
+ */
+ std::string toString(bool chain = true) const;
+
+ std::string print() const;
+
+ /**
+ * As a CA, revoke a certificate, adding it to
+ * the attached Certificate Revocation List (CRL)
+ */
+ void revoke(const PrivateKey&, const Certificate&);
+
+ /**
+ * Get the list of certificates revoked as as CA.
+ */
+ std::vector<std::shared_ptr<RevocationList>> getRevocationLists() const;
+
+ /**
+ * Attach existing revocation list.
+ */
+ void addRevocationList(RevocationList&&);
+ void addRevocationList(std::shared_ptr<RevocationList>);
+
+ static Certificate generate(const PrivateKey& key, const std::string& name = "dhtnode", const Identity& ca = {}, bool is_ca = false, int64_t validity = 0);
+ static Certificate generate(const CertificateRequest& request, const Identity& ca, int64_t validity = 0);
+
+ gnutls_x509_crt_t getCopy() const {
+ if (not cert)
+ return nullptr;
+ auto copy = Certificate(getPacked());
+ gnutls_x509_crt_t ret = copy.cert;
+ copy.cert = nullptr;
+ return ret;
+ }
+
+ std::vector<gnutls_x509_crt_t>
+ getChain(bool copy = false) const
+ {
+ if (not cert)
+ return {};
+ std::vector<gnutls_x509_crt_t> crts;
+ for (auto c = this; c; c = c->issuer.get())
+ crts.emplace_back(copy ? c->getCopy() : c->cert);
+ return crts;
+ }
+
+ std::pair<
+ std::vector<gnutls_x509_crt_t>,
+ std::vector<gnutls_x509_crl_t>
+ >
+ getChainWithRevocations(bool copy = false) const
+ {
+ if (not cert)
+ return {};
+ std::vector<gnutls_x509_crt_t> crts;
+ std::vector<gnutls_x509_crl_t> crls;
+ for (auto c = this; c; c = c->issuer.get()) {
+ crts.emplace_back(copy ? c->getCopy() : c->cert);
+ crls.reserve(crls.size() + c->revocation_lists.size());
+ for (const auto& crl : c->revocation_lists)
+ crls.emplace_back(copy ? crl->getCopy() : crl->get());
+ }
+ return {crts, crls};
+ }
+
+ gnutls_digest_algorithm_t getPreferredDigest() const;
+
+ /*
+ * Generate OCSP request.
+ * Return GnuTLS error code.
+ * https://www.gnutls.org/manual/html_node/Error-codes.html
+ */
+ std::pair<std::string, Blob> generateOcspRequest(gnutls_x509_crt_t& issuer);
+
+ /**
+ * Change certificate's expiration
+ */
+ void setValidity(const Identity& ca, int64_t validity);
+ void setValidity(const PrivateKey& key, int64_t validity);
+
+ gnutls_x509_crt_t cert {nullptr};
+ std::shared_ptr<Certificate> issuer {};
+ std::shared_ptr<OcspResponse> ocspResponse;
+private:
+ Certificate(const Certificate&) = delete;
+ Certificate& operator=(const Certificate&) = delete;
+ mutable InfoHash cachedId_ {};
+ mutable PkId cachedLongId_ {};
+ mutable std::atomic_bool idCached_ {false};
+ mutable std::atomic_bool longIdCached_ {false};
+
+ struct crlNumberCmp {
+ bool operator() (const std::shared_ptr<RevocationList>& lhs, const std::shared_ptr<RevocationList>& rhs) const {
+ return lhs->getNumber() < rhs->getNumber();
+ }
+ };
+
+ std::set<std::shared_ptr<RevocationList>, crlNumberCmp> revocation_lists;
+
+ mutable std::mutex publicKeyMutex_ {};
+ mutable std::shared_ptr<PublicKey> publicKey_ {};
+};
+
+struct OPENDHT_PUBLIC TrustList
+{
+ struct VerifyResult {
+ int ret;
+ unsigned result;
+ bool hasError() const { return ret < 0; }
+ bool isValid() const { return !hasError() and !(result & GNUTLS_CERT_INVALID); }
+ explicit operator bool() const { return isValid(); }
+ std::string toString() const;
+ OPENDHT_PUBLIC friend std::ostream& operator<< (std::ostream& s, const VerifyResult& h);
+ };
+
+ TrustList();
+ TrustList(TrustList&& o) noexcept : trust(std::move(o.trust)) {
+ o.trust = nullptr;
+ }
+ TrustList& operator=(TrustList&& o) noexcept;
+ ~TrustList();
+ void add(const Certificate& crt);
+ void add(const RevocationList& crl);
+ void remove(const Certificate& crt, bool parents = true);
+ VerifyResult verify(const Certificate& crt) const;
+
+private:
+ TrustList(const TrustList& o) = delete;
+ TrustList& operator=(const TrustList& o) = delete;
+ gnutls_x509_trust_list_t trust {nullptr};
+};
+
+template <class T>
+class OPENDHT_PUBLIC secure_vector
+{
+public:
+ secure_vector() {}
+ secure_vector(secure_vector<T> const&) = default;
+ secure_vector(secure_vector<T> &&) = default;
+ explicit secure_vector(unsigned size): data_(size) {}
+ explicit secure_vector(unsigned size, T _item): data_(size, _item) {}
+ explicit secure_vector(const std::vector<T>& c): data_(c) {}
+ secure_vector(std::vector<T>&& c): data_(std::move(c)) {}
+ ~secure_vector() { clean(); }
+
+ static secure_vector<T> getRandom(size_t size) {
+ secure_vector<T> ret(size);
+ crypto::random_device rdev;
+#ifdef _WIN32
+ std::uniform_int_distribution<int> rand_byte{ 0, std::numeric_limits<uint8_t>::max() };
+#else
+ std::uniform_int_distribution<uint8_t> rand_byte;
+#endif
+ std::generate_n((uint8_t*)ret.data_.data(), ret.size()*sizeof(T), std::bind(rand_byte, std::ref(rdev)));
+ return ret;
+ }
+ secure_vector<T>& operator=(const secure_vector<T>& c) {
+ if (&c == this)
+ return *this;
+ clean();
+ data_ = c.data_;
+ return *this;
+ }
+ secure_vector<T>& operator=(secure_vector<T>&& c) {
+ if (&c == this)
+ return *this;
+ clean();
+ data_ = std::move(c.data_);
+ return *this;
+ }
+ secure_vector<T>& operator=(std::vector<T>&& c) {
+ clean();
+ data_ = std::move(c);
+ return *this;
+ }
+ std::vector<T>& writable() { clean(); return data_; }
+ const std::vector<T>& makeInsecure() const { return data_; }
+ const uint8_t* data() const { return data_.data(); }
+
+ void clean() {
+ clean(data_.begin(), data_.end());
+ }
+
+ void clear() { clean(); data_.clear(); }
+
+ size_t size() const { return data_.size(); }
+ bool empty() const { return data_.empty(); }
+
+ void swap(secure_vector<T>& other) { data_.swap(other.data_); }
+ void resize(size_t s) {
+ if (s == data_.size()) return;
+ if (s < data_.size()) {
+ //shrink
+ clean(data_.begin()+s, data_.end());
+ data_.resize(s);
+ } else {
+ //grow
+ auto data = std::move(data_); // move protected data
+ clear();
+ data_.resize(s);
+ std::copy(data.begin(), data.end(), data_.begin());
+ clean(data.begin(), data.end());
+ }
+ }
+
+private:
+ /**
+ * Securely wipe memory
+ */
+ static void clean(const typename std::vector<T>::iterator& i, const typename std::vector<T>::iterator& j) {
+ volatile uint8_t* b = reinterpret_cast<uint8_t*>(&*i);
+ volatile uint8_t* e = reinterpret_cast<uint8_t*>(&*j);
+ std::fill(b, e, 0);
+ }
+
+ std::vector<T> data_;
+};
+
+using SecureBlob = secure_vector<uint8_t>;
+
+/**
+ * Generate an RSA key pair (4096 bits) and a certificate.
+ * @param name the name used in the generated certificate
+ * @param ca if set, the certificate authority that will sign the generated certificate.
+ * If not set, the generated certificate will be a self-signed CA.
+ * @param key_length stength of the generated private key (bits).
+ */
+OPENDHT_PUBLIC Identity generateIdentity(const std::string& name, const Identity& ca, unsigned key_length, bool is_ca);
+OPENDHT_PUBLIC Identity generateIdentity(const std::string& name = "dhtnode", const Identity& ca = {}, unsigned key_length = 4096);
+
+OPENDHT_PUBLIC Identity generateEcIdentity(const std::string& name, const Identity& ca, bool is_ca);
+OPENDHT_PUBLIC Identity generateEcIdentity(const std::string& name = "dhtnode", const Identity& ca = {});
+
+OPENDHT_PUBLIC void saveIdentity(const Identity& id, const std::string& path, const std::string& privkey_password = {});
+
+/**
+ * Performs SHA512, SHA256 or SHA1, depending on hash_length.
+ * Attempts to choose an hash function with
+ * output size of at least hash_length bytes, Current implementation
+ * will use SHA1 for hash_length up to 20 bytes,
+ * will use SHA256 for hash_length up to 32 bytes,
+ * will use SHA512 for hash_length of 33 bytes and more.
+ */
+OPENDHT_PUBLIC Blob hash(const Blob& data, size_t hash_length = 512/8);
+
+OPENDHT_PUBLIC void hash(const uint8_t* data, size_t data_length, uint8_t* hash, size_t hash_length);
+
+/**
+ * Generates an encryption key from a text password,
+ * making the key longer to bruteforce.
+ * The generated key also depends on a unique salt value of any size,
+ * that can be transmitted in clear, and will be generated if
+ * not provided (32 bytes).
+ */
+OPENDHT_PUBLIC Blob stretchKey(const std::string& password, Blob& salt, size_t key_length = 512/8);
+
+/**
+ * AES-GCM encryption. Key must be 128, 192 or 256 bits long (16, 24 or 32 bytes).
+ */
+OPENDHT_PUBLIC Blob aesEncrypt(const uint8_t* data, size_t data_length, const Blob& key);
+OPENDHT_PUBLIC inline Blob aesEncrypt(const Blob& data, const Blob& key) {
+ return aesEncrypt(data.data(), data.size(), key);
+}
+OPENDHT_PUBLIC Blob aesEncrypt(const Blob& data, const std::string& password);
+
+/**
+ * AES-GCM decryption.
+ */
+OPENDHT_PUBLIC Blob aesDecrypt(const uint8_t* data, size_t data_length, const Blob& key);
+OPENDHT_PUBLIC inline Blob aesDecrypt(const Blob& data, const Blob& key) { return aesDecrypt(data.data(), data.size(), key); }
+OPENDHT_PUBLIC Blob aesDecrypt(const uint8_t* data, size_t data_length, const std::string& password);
+OPENDHT_PUBLIC inline Blob aesDecrypt(const Blob& data, const std::string& password) { return aesDecrypt(data.data(), data.size(), password); }
+
+}
+}
--- /dev/null
+#pragma once
+
+// Generic helper definitions for shared library support
+#if defined _WIN32 || defined __CYGWIN__
+ #define OPENDHT_IMPORT __declspec(dllimport)
+ #define OPENDHT_EXPORT __declspec(dllexport)
+ #define OPENDHT_HIDDEN
+#else
+ #define OPENDHT_IMPORT __attribute__ ((visibility ("default")))
+ #define OPENDHT_EXPORT __attribute__ ((visibility ("default")))
+ #define OPENDHT_HIDDEN __attribute__ ((visibility ("hidden")))
+#endif
+
+// Now we use the generic helper definitions above to define OPENDHT_PUBLIC and OPENDHT_LOCAL.
+// OPENDHT_PUBLIC is used for the public API symbols. It either DLL imports or DLL exports (or does nothing for static build)
+// OPENDHT_LOCAL is used for non-api symbols.
+
+#ifdef opendht_EXPORTS // defined if OpenDHT is compiled as a shared library
+ #ifdef OPENDHT_BUILD // defined if we are building the OpenDHT shared library (instead of using it)
+ #define OPENDHT_PUBLIC OPENDHT_EXPORT
+ #else
+ #define OPENDHT_PUBLIC OPENDHT_IMPORT
+ #endif // OPENDHT_BUILD
+ #define OPENDHT_LOCAL OPENDHT_HIDDEN
+#else // opendht_EXPORTS is not defined: this means OpenDHT is a static lib.
+ #define OPENDHT_PUBLIC
+ #define OPENDHT_LOCAL
+#endif // opendht_EXPORTS
+
+
+#ifdef opendht_c_EXPORTS // defined if OpenDHT is compiled as a shared library
+ #ifdef OPENDHT_C_BUILD // defined if we are building the OpenDHT shared library (instead of using it)
+ #define OPENDHT_C_PUBLIC OPENDHT_EXPORT
+ #else
+ #define OPENDHT_C_PUBLIC OPENDHT_IMPORT
+ #endif // OPENDHT_BUILD
+ #define OPENDHT_C_LOCAL OPENDHT_HIDDEN
+#else // opendht_EXPORTS is not defined: this means OpenDHT is a static lib.
+ #define OPENDHT_C_PUBLIC
+ #define OPENDHT_C_LOCAL
+#endif // opendht_EXPORTS
+
+// bytes
+#define HASH_LEN 20u
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ * Author : Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "value.h"
+#include "sockaddr.h"
+
+namespace dht {
+enum class ImStatus : uint8_t {
+ NONE = 0,
+ TYPING,
+ RECEIVED,
+ READ
+};
+}
+MSGPACK_ADD_ENUM(dht::ImStatus)
+
+namespace dht {
+
+class OPENDHT_PUBLIC DhtMessage : public Value::Serializable<DhtMessage>
+{
+public:
+ static const ValueType TYPE;
+
+ DhtMessage(const std::string& s = {}, const Blob& msg = {}) : service(s), data(msg) {}
+
+ std::string getService() const {
+ return service;
+ }
+
+ static Value::Filter getFilter() { return {}; }
+
+ static bool storePolicy(InfoHash key, std::shared_ptr<Value>& value, const InfoHash& from, const SockAddr&);
+
+ static Value::Filter ServiceFilter(const std::string& s);
+
+ /** print value for debugging */
+ friend std::ostream& operator<< (std::ostream&, const DhtMessage&);
+
+ std::string service;
+ Blob data;
+ MSGPACK_DEFINE(service, data)
+};
+
+template <typename T>
+class OPENDHT_PUBLIC SignedValue : public Value::Serializable<T>
+{
+private:
+ using BaseClass = Value::Serializable<T>;
+
+public:
+ virtual void unpackValue(const Value& v) override {
+ if (v.owner) {
+ owner = v.owner;
+ from = v.owner->getId();
+ }
+ BaseClass::unpackValue(v);
+ }
+
+ static Value::Filter getFilter() {
+ return [](const Value& v){ return v.isSigned(); };
+ }
+
+ Sp<crypto::PublicKey> owner;
+ dht::InfoHash from;
+};
+
+template <typename T>
+class OPENDHT_PUBLIC EncryptedValue : public SignedValue<T>
+{
+public:
+ using BaseClass = SignedValue<T>;
+
+public:
+ virtual void unpackValue(const Value& v) override {
+ to = v.recipient;
+ BaseClass::unpackValue(v);
+ }
+
+ static Value::Filter getFilter() {
+ return Value::Filter::chain(
+ BaseClass::getFilter(),
+ [](const Value& v) { return static_cast<bool>(v.recipient); }
+ );
+ }
+
+ dht::InfoHash to;
+};
+
+
+
+
+class OPENDHT_PUBLIC ImMessage : public SignedValue<ImMessage>
+{
+private:
+ using BaseClass = SignedValue<ImMessage>;
+
+public:
+ static const ValueType TYPE;
+
+ ImMessage() {}
+ ImMessage(dht::Value::Id id, std::string&& m, long d = 0)
+ : id(id), msg(std::move(m)), date(d) {}
+ ImMessage(dht::Value::Id id, std::string &&dt, std::string &&m, long d = 0)
+ : id(id), msg(std::move(m)), datatype(std::move(dt)), date(d) {}
+ ImMessage(dht::Value::Id id, std::string &&dt, std::string &&m, std::map<std::string, std::string> &&md, long d = 0)
+ : id(id), msg(std::move(m)), datatype(std::move(dt)), metadatas(std::move(md)), date(d) {}
+
+ virtual void unpackValue(const Value& v) override {
+ to = v.recipient;
+ SignedValue::unpackValue(v);
+ }
+
+ dht::InfoHash to;
+ dht::Value::Id id {0};
+ std::string msg;
+ std::string datatype;
+ std::map<std::string, std::string> metadatas;
+ long date {0};
+ ImStatus status {ImStatus::NONE};
+
+ MSGPACK_DEFINE_MAP(id, msg, date, status, datatype, metadatas)
+};
+
+class OPENDHT_PUBLIC TrustRequest : public EncryptedValue<TrustRequest>
+{
+private:
+ using BaseClass = EncryptedValue<TrustRequest>;
+
+public:
+ static const ValueType TYPE;
+
+ TrustRequest() {}
+ TrustRequest(std::string s, std::string ci = {}) : service(s), conversationId(ci) {}
+ TrustRequest(std::string s, std::string ci, const Blob& d) : service(s), conversationId(ci), payload(d) {}
+
+ static Value::Filter getFilter() {
+ return EncryptedValue::getFilter();
+ }
+
+ std::string service;
+ std::string conversationId;
+ Blob payload;
+ bool confirm {false};
+ MSGPACK_DEFINE_MAP(service, conversationId, payload, confirm)
+};
+
+class OPENDHT_PUBLIC IceCandidates : public EncryptedValue<IceCandidates>
+{
+private:
+ using BaseClass = EncryptedValue<IceCandidates>;
+
+public:
+ static const ValueType TYPE;
+
+ IceCandidates() {}
+ IceCandidates(Value::Id msg_id, Blob ice) : id(msg_id), ice_data(ice) {}
+
+ static Value::Filter getFilter() {
+ return EncryptedValue::getFilter();
+ }
+
+ template <typename Packer>
+ void msgpack_pack(Packer& pk) const
+ {
+ pk.pack_array(2);
+ pk.pack(id);
+#if 1
+ pk.pack_bin(ice_data.size());
+ pk.pack_bin_body((const char*)ice_data.data(), ice_data.size());
+#else
+ // hack for backward compatibility with old opendht compiled with msgpack 1.0
+ // remove when enough people have moved to new versions
+ pk.pack_array(ice_data.size());
+ for (uint8_t b : ice_data)
+ pk.pack(b);
+#endif
+ }
+
+ virtual void msgpack_unpack(const msgpack::object& o)
+ {
+ if (o.type != msgpack::type::ARRAY) throw msgpack::type_error();
+ if (o.via.array.size < 2) throw msgpack::type_error();
+ id = o.via.array.ptr[0].as<Value::Id>();
+ ice_data = unpackBlob(o.via.array.ptr[1]);
+ }
+
+ Value::Id id {0};
+ Blob ice_data;
+};
+
+/* "Peer" announcement
+ */
+class OPENDHT_PUBLIC IpServiceAnnouncement : public Value::Serializable<IpServiceAnnouncement>
+{
+private:
+ using BaseClass = Value::Serializable<IpServiceAnnouncement>;
+
+public:
+ static const ValueType TYPE;
+
+ IpServiceAnnouncement(sa_family_t family = AF_UNSPEC, in_port_t p = 0) {
+ addr.setFamily(family);
+ addr.setPort(p);
+ }
+
+ IpServiceAnnouncement(const SockAddr& sa) : addr(sa) {}
+
+ IpServiceAnnouncement(const Blob& b) {
+ msgpack_unpack(unpackMsg(b).get());
+ }
+
+ template <typename Packer>
+ void msgpack_pack(Packer& pk) const
+ {
+ pk.pack_bin(addr.getLength());
+ pk.pack_bin_body((const char*)addr.get(), addr.getLength());
+ }
+
+ virtual void msgpack_unpack(const msgpack::object& o)
+ {
+ if (o.type == msgpack::type::BIN)
+ addr = {(sockaddr*)o.via.bin.ptr, (socklen_t)o.via.bin.size};
+ else
+ throw msgpack::type_error();
+ }
+
+ in_port_t getPort() const {
+ return addr.getPort();
+ }
+ void setPort(in_port_t p) {
+ addr.setPort(p);
+ }
+
+ const SockAddr& getPeerAddr() const {
+ return addr;
+ }
+
+ virtual const ValueType& getType() const {
+ return TYPE;
+ }
+
+ static bool storePolicy(InfoHash, std::shared_ptr<Value>&, const InfoHash&, const SockAddr&);
+
+ /** print value for debugging */
+ friend std::ostream& operator<< (std::ostream&, const IpServiceAnnouncement&);
+
+private:
+ SockAddr addr;
+};
+
+
+OPENDHT_PUBLIC extern const std::array<std::reference_wrapper<const ValueType>, 5> DEFAULT_TYPES;
+
+OPENDHT_PUBLIC extern const std::array<std::reference_wrapper<const ValueType>, 1> DEFAULT_INSECURE_TYPES;
+
+}
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ * Authors: Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ * Simon Désaulniers <simon.desaulniers@savoirfairelinux.com>
+ * Sébastien Blin <sebastien.blin@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "infohash.h"
+#include "value.h"
+#include "utils.h"
+#include "network_engine.h"
+#include "scheduler.h"
+#include "routing_table.h"
+#include "callbacks.h"
+#include "dht_interface.h"
+
+#include <string>
+#include <array>
+#include <vector>
+#include <map>
+#include <functional>
+#include <memory>
+
+#ifdef _WIN32
+#include <iso646.h>
+#endif
+
+namespace dht {
+
+namespace net {
+struct Request;
+} /* namespace net */
+
+struct Storage;
+struct ValueStorage;
+class StorageBucket;
+struct Listener;
+struct LocalListener;
+
+/**
+ * Main Dht class.
+ * Provides a Distributed Hash Table node.
+ *
+ * Must be given open UDP sockets and ::periodic must be
+ * called regularly.
+ */
+class OPENDHT_PUBLIC Dht final : public DhtInterface {
+public:
+ /**
+ * Initialise the Dht with two open sockets (for IPv4 and IP6)
+ * and an ID for the node.
+ */
+ Dht(std::unique_ptr<net::DatagramSocket>&& sock, const Config& config, const Sp<Logger>& l = {});
+
+ virtual ~Dht();
+
+ /**
+ * Get the ID of the node.
+ */
+ inline const InfoHash& getNodeId() const override { return myid; }
+ void setOnPublicAddressChanged(PublicAddressChangedCb cb) override {
+ publicAddressChangedCb_ = std::move(cb);
+ }
+
+ NodeStatus updateStatus(sa_family_t af) override;
+
+ /**
+ * Get the current status of the node for the given family.
+ */
+ NodeStatus getStatus(sa_family_t af) const override {
+ return dht(af).status;
+ }
+
+ NodeStatus getStatus() const override {
+ return std::max(getStatus(AF_INET), getStatus(AF_INET6));
+ }
+
+ net::DatagramSocket* getSocket() const override { return network_engine.getSocket(); };
+
+ /**
+ * Performs final operations before quitting.
+ */
+ void shutdown(ShutdownCallback cb, bool stop = false) override;
+
+ /**
+ * Returns true if the node is running (have access to an open socket).
+ *
+ * af: address family. If non-zero, will return true if the node
+ * is running for the provided family.
+ */
+ bool isRunning(sa_family_t af = 0) const override;
+
+ virtual void registerType(const ValueType& type) override {
+ types.registerType(type);
+ }
+ const ValueType& getType(ValueType::Id type_id) const override {
+ return types.getType(type_id);
+ }
+
+ void addBootstrap(const std::string& host, const std::string& service) override {
+ bootstrap_nodes.emplace_back(host, service);
+ startBootstrap();
+ }
+
+ void clearBootstrap() override {
+ bootstrap_nodes.clear();
+ }
+
+ /**
+ * Insert a node in the main routing table.
+ * The node is not pinged, so this should be
+ * used to bootstrap efficiently from previously known nodes.
+ */
+ void insertNode(const InfoHash& id, const SockAddr&) override;
+ void insertNode(const NodeExport& n) override {
+ insertNode(n.id, n.addr);
+ }
+
+ void pingNode(SockAddr, DoneCallbackSimple&& cb={}) override;
+
+ time_point periodic(const uint8_t *buf, size_t buflen, SockAddr, const time_point& now) override;
+ time_point periodic(const uint8_t *buf, size_t buflen, const sockaddr* from, socklen_t fromlen, const time_point& now) override {
+ return periodic(buf, buflen, SockAddr(from, fromlen), now);
+ }
+
+ /**
+ * Get a value by searching on all available protocols (IPv4, IPv6),
+ * and call the provided get callback when values are found at key.
+ * The operation will start as soon as the node is connected to the network.
+ * @param cb a function called when new values are found on the network.
+ * It should return false to stop the operation.
+ * @param donecb a function called when the operation is complete.
+ cb and donecb won't be called again afterward.
+ * @param f a filter function used to prefilter values.
+ */
+ virtual void get(const InfoHash& key, GetCallback cb, DoneCallback donecb={}, Value::Filter&& f={}, Where&& w = {}) override;
+ virtual void get(const InfoHash& key, GetCallback cb, DoneCallbackSimple donecb={}, Value::Filter&& f={}, Where&& w = {}) override {
+ get(key, cb, bindDoneCb(donecb), std::forward<Value::Filter>(f), std::forward<Where>(w));
+ }
+ virtual void get(const InfoHash& key, GetCallbackSimple cb, DoneCallback donecb={}, Value::Filter&& f={}, Where&& w = {}) override {
+ get(key, bindGetCb(cb), donecb, std::forward<Value::Filter>(f), std::forward<Where>(w));
+ }
+ virtual void get(const InfoHash& key, GetCallbackSimple cb, DoneCallbackSimple donecb, Value::Filter&& f={}, Where&& w = {}) override {
+ get(key, bindGetCb(cb), bindDoneCb(donecb), std::forward<Value::Filter>(f), std::forward<Where>(w));
+ }
+ /**
+ * Similar to Dht::get, but sends a Query to filter data remotely.
+ * @param key the key for which to query data for.
+ * @param cb a function called when new values are found on the network.
+ * It should return false to stop the operation.
+ * @param done_cb a function called when the operation is complete.
+ cb and done_cb won't be called again afterward.
+ * @param q a query used to filter values on the remotes before they send a
+ * response.
+ */
+ virtual void query(const InfoHash& key, QueryCallback cb, DoneCallback done_cb = {}, Query&& q = {}) override;
+ virtual void query(const InfoHash& key, QueryCallback cb, DoneCallbackSimple done_cb = {}, Query&& q = {}) override {
+ query(key, cb, bindDoneCb(done_cb), std::forward<Query>(q));
+ }
+
+ /**
+ * Get locally stored data for the given hash.
+ */
+ std::vector<Sp<Value>> getLocal(const InfoHash& key, const Value::Filter& f = {}) const override;
+
+ /**
+ * Get locally stored data for the given key and value id.
+ */
+ Sp<Value> getLocalById(const InfoHash& key, Value::Id vid) const override;
+
+ /**
+ * Announce a value on all available protocols (IPv4, IPv6).
+ *
+ * The operation will start as soon as the node is connected to the network.
+ * The done callback will be called once, when the first announce succeeds, or fails.
+ */
+ void put(const InfoHash& key,
+ Sp<Value>,
+ DoneCallback cb=nullptr,
+ time_point created=time_point::max(),
+ bool permanent = false) override;
+ void put(const InfoHash& key,
+ const Sp<Value>& v,
+ DoneCallbackSimple cb,
+ time_point created=time_point::max(),
+ bool permanent = false) override
+ {
+ put(key, v, bindDoneCb(cb), created, permanent);
+ }
+
+ void put(const InfoHash& key,
+ Value&& v,
+ DoneCallback cb=nullptr,
+ time_point created=time_point::max(),
+ bool permanent = false) override
+ {
+ put(key, std::make_shared<Value>(std::move(v)), cb, created, permanent);
+ }
+ void put(const InfoHash& key,
+ Value&& v,
+ DoneCallbackSimple cb,
+ time_point created=time_point::max(),
+ bool permanent = false) override
+ {
+ put(key, std::forward<Value>(v), bindDoneCb(cb), created, permanent);
+ }
+
+ /**
+ * Get data currently being put at the given hash.
+ */
+ std::vector<Sp<Value>> getPut(const InfoHash&) const override;
+
+ /**
+ * Get data currently being put at the given hash with the given id.
+ */
+ Sp<Value> getPut(const InfoHash&, const Value::Id&) const override;
+
+ /**
+ * Stop any put/announce operation at the given location,
+ * for the value with the given id.
+ */
+ bool cancelPut(const InfoHash&, const Value::Id&) override;
+
+ /**
+ * Listen on the network for any changes involving a specified hash.
+ * The node will register to receive updates from relevent nodes when
+ * new values are added or removed.
+ *
+ * @return a token to cancel the listener later.
+ */
+ size_t listen(const InfoHash&, ValueCallback, Value::Filter={}, Where={}) override;
+
+ size_t listen(const InfoHash& key, GetCallback cb, Value::Filter f={}, Where w={}) override {
+ return listen(key, [cb](const std::vector<Sp<Value>>& vals, bool expired){
+ if (not expired)
+ return cb(vals);
+ return true;
+ }, std::forward<Value::Filter>(f), std::forward<Where>(w));
+ }
+ size_t listen(const InfoHash& key, GetCallbackSimple cb, Value::Filter f={}, Where w={}) override {
+ return listen(key, bindGetCb(cb), std::forward<Value::Filter>(f), std::forward<Where>(w));
+ }
+
+ bool cancelListen(const InfoHash&, size_t token) override;
+
+ /**
+ * Inform the DHT of lower-layer connectivity changes.
+ * This will cause the DHT to assume a public IP address change.
+ * The DHT will recontact neighbor nodes, re-register for listen ops etc.
+ */
+ void connectivityChanged(sa_family_t) override;
+ void connectivityChanged() override {
+ connectivityChanged(AF_INET);
+ connectivityChanged(AF_INET6);
+ }
+
+ /**
+ * Get the list of good nodes for local storage saving purposes
+ * The list is ordered to minimize the back-to-work delay.
+ */
+ std::vector<NodeExport> exportNodes() const override;
+
+ 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;
+ std::string getStorageLog(const InfoHash&) const override;
+
+ std::string getRoutingTablesLog(sa_family_t) const override;
+ std::string getSearchesLog(sa_family_t) const override;
+ std::string getSearchLog(const InfoHash&, sa_family_t af = AF_UNSPEC) const override;
+
+ void dumpTables() const override;
+ std::vector<unsigned> getNodeMessageStats(bool in = false) override {
+ return network_engine.getNodeMessageStats(in);
+ }
+
+ /**
+ * Set the in-memory storage limit in bytes
+ */
+ void setStorageLimit(size_t limit = DEFAULT_STORAGE_LIMIT) override {
+ max_store_size = limit;
+ }
+ size_t getStorageLimit() const override {
+ return max_store_size;
+ }
+
+ /**
+ * Returns the total memory usage of stored values and the number
+ * of stored values.
+ */
+ std::pair<size_t, size_t> getStoreSize() const override {
+ return {total_store_size, total_values};
+ }
+
+ std::vector<SockAddr> getPublicAddress(sa_family_t family = 0) override;
+
+ void pushNotificationReceived(const std::map<std::string, std::string>&) override {}
+ void resubscribe(unsigned) {}
+
+private:
+
+ /* When performing a search, we search for up to SEARCH_NODES closest nodes
+ to the destination, and use the additional ones to backtrack if any of
+ the target 8 turn out to be dead. */
+ static constexpr unsigned SEARCH_NODES {14};
+
+ /* The number of bad nodes is limited in order to help determine
+ * presence of connectivity changes. See
+ * https://github.com/savoirfairelinux/opendht/issues/137 for details.
+ *
+ * According to the tables, 25 is a good average value for big networks. If
+ * the network is small, normal search expiration process will handle the
+ * situation.
+ * */
+ static constexpr unsigned SEARCH_MAX_BAD_NODES {25};
+
+ /* Concurrent search nodes requested count */
+ static constexpr unsigned MAX_REQUESTED_SEARCH_NODES {4};
+
+ /* Number of listening nodes */
+ static constexpr unsigned LISTEN_NODES {4};
+
+ /* The maximum number of hashes we're willing to track. */
+ static constexpr unsigned MAX_HASHES {1024 * 1024 * 1024};
+
+ /* The maximum number of searches we keep data about. */
+ static constexpr unsigned MAX_SEARCHES {1024 * 1024};
+
+ static constexpr std::chrono::minutes MAX_STORAGE_MAINTENANCE_EXPIRE_TIME {10};
+
+ /* The time after which we consider a search to be expirable. */
+ static constexpr std::chrono::minutes SEARCH_EXPIRE_TIME {62};
+
+ /* Timeout for listen */
+ static constexpr duration LISTEN_EXPIRE_TIME {std::chrono::seconds(30)};
+ static constexpr duration LISTEN_EXPIRE_TIME_PUBLIC {std::chrono::minutes(5)};
+
+ static constexpr duration REANNOUNCE_MARGIN {std::chrono::seconds(10)};
+
+ static constexpr std::chrono::seconds BOOTSTRAP_PERIOD {10};
+
+ static constexpr size_t TOKEN_SIZE {32};
+
+ // internal structures
+ struct SearchNode;
+ struct Get;
+ struct Announce;
+ struct Search;
+
+ // prevent copy
+ Dht(const Dht&) = delete;
+ Dht& operator=(const Dht&) = delete;
+
+ std::mt19937_64 rd {crypto::getSeededRandomEngine<std::mt19937_64>()};
+
+ InfoHash myid {};
+
+ uint64_t secret {};
+ uint64_t oldsecret {};
+
+ // registred types
+ TypeStore types;
+
+ using SearchMap = std::map<InfoHash, Sp<Search>>;
+ using ReportedAddr = std::pair<unsigned, SockAddr>;
+
+ struct Kad {
+ RoutingTable buckets {};
+ SearchMap searches {};
+ unsigned pending_pings {0};
+ NodeStatus status;
+ std::vector<ReportedAddr> reported_addr;
+
+ NodeStatus getStatus(time_point now) const;
+ NodeStats getNodesStats(time_point now, const InfoHash& myid) const;
+ };
+
+ Kad dht4 {};
+ Kad dht6 {};
+ PublicAddressChangedCb publicAddressChangedCb_ {};
+
+ std::vector<std::pair<std::string,std::string>> bootstrap_nodes {};
+ std::chrono::steady_clock::duration bootstrap_period {BOOTSTRAP_PERIOD};
+ Sp<Scheduler::Job> bootstrapJob {};
+
+ std::map<InfoHash, Storage> store;
+ std::map<SockAddr, StorageBucket, SockAddr::ipCmp> store_quota;
+ size_t total_values {0};
+ size_t total_store_size {0};
+ size_t max_store_keys {MAX_HASHES};
+ size_t max_store_size {DEFAULT_STORAGE_LIMIT};
+
+ size_t max_searches {MAX_SEARCHES};
+ size_t search_id {0};
+
+ // map a global listen token to IPv4, IPv6 specific listen tokens.
+ // 0 is the invalid token.
+ std::map<size_t, std::tuple<size_t, size_t, size_t>> listeners {};
+ size_t listener_token {1};
+
+
+ // timing
+ Scheduler scheduler;
+ Sp<Scheduler::Job> nextNodesConfirmation {};
+ Sp<Scheduler::Job> nextStorageMaintenance {};
+
+ net::NetworkEngine network_engine;
+
+ std::string persistPath;
+
+ // are we a bootstrap node ?
+ // note: Any running node can be used as a bootstrap node.
+ // Only nodes running only as bootstrap nodes should
+ // be put in bootstrap mode.
+ const bool is_bootstrap {false};
+ const bool maintain_storage {false};
+ const bool public_stable {false};
+
+ inline const duration& getListenExpiration() const {
+ 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
+ void storageAddListener(const InfoHash& id, const Sp<Node>& node, size_t tid, Query&& = {}, int version = 0);
+ bool storageStore(const InfoHash& id, const Sp<Value>& value, time_point created, const SockAddr& sa = {}, bool permanent = false);
+ bool storageRefresh(const InfoHash& id, Value::Id vid);
+ void expireStore();
+ void expireStorage(InfoHash h);
+ void expireStore(decltype(store)::iterator);
+
+ void storageRemoved(const InfoHash& id, Storage& st, const std::vector<Sp<Value>>& values, size_t totalSize);
+ void storageChanged(const InfoHash& id, Storage& st, const Sp<Value>&, bool newValue);
+ std::string printStorageLog(const decltype(store)::value_type&) const;
+
+ /**
+ * For a given storage, if values don't belong there anymore because this
+ * node is too far from the target, values are sent to the appropriate
+ * nodes.
+ */
+ void dataPersistence(InfoHash id);
+ size_t maintainStorage(decltype(store)::value_type&, bool force=false, const DoneCallback& donecb={});
+
+ // Buckets
+ Kad& dht(sa_family_t af) { return af == AF_INET ? dht4 : dht6; }
+ const Kad& dht(sa_family_t af) const { return af == AF_INET ? dht4 : dht6; }
+ RoutingTable& buckets(sa_family_t af) { return dht(af).buckets; }
+ const RoutingTable& buckets(sa_family_t af) const { return dht(af).buckets; }
+ Bucket* findBucket(const InfoHash& id, sa_family_t af) {
+ auto& b = buckets(af);
+ auto it = b.findBucket(id);
+ return it == b.end() ? nullptr : &(*it);
+ }
+ const Bucket* findBucket(const InfoHash& id, sa_family_t af) const {
+ return const_cast<Dht*>(this)->findBucket(id, af);
+ }
+
+ void expireBuckets(RoutingTable&);
+ void sendCachedPing(Bucket& b);
+ bool bucketMaintenance(RoutingTable&);
+ void dumpBucket(const Bucket& b, std::ostream& out) const;
+ void bootstrap();
+ void startBootstrap();
+ void stopBootstrap();
+
+ // Nodes
+ void onNewNode(const Sp<Node>& node, int confirm);
+ const Sp<Node> findNode(const InfoHash& id, sa_family_t af) const;
+ bool trySearchInsert(const Sp<Node>& node);
+
+ // Searches
+ inline SearchMap& searches(sa_family_t af) { return dht(af).searches; }
+ inline const SearchMap& searches(sa_family_t af) const { return dht(af).searches; }
+
+ /**
+ * Low-level method that will perform a search on the DHT for the specified
+ * infohash (id), using the specified IP version (IPv4 or IPv6).
+ */
+ Sp<Search> search(const InfoHash& id, sa_family_t af, GetCallback = {}, QueryCallback = {}, DoneCallback = {}, Value::Filter = {}, const Sp<Query>& q = {});
+
+ void announce(const InfoHash& id, sa_family_t af, Sp<Value> value, DoneCallback callback, time_point created=time_point::max(), bool permanent = false);
+ size_t listenTo(const InfoHash& id, sa_family_t af, ValueCallback cb, Value::Filter f = {}, const Sp<Query>& q = {});
+
+ /**
+ * Refill the search with good nodes if possible.
+ *
+ * @param sr The search to refill.
+ *
+ * @return the number inserted nodes.
+ */
+ unsigned refill(Search& sr);
+ void expireSearches();
+
+ void confirmNodes();
+ void expire();
+
+ void onConnected();
+ void onDisconnected();
+
+ /**
+ * Generic function to execute when a 'get' request has completed.
+ *
+ * @param status The request passed by the network engine.
+ * @param answer The answer from the network engine.
+ * @param ws A weak pointer to the search concerned by the request.
+ * @param query The query sent to the node.
+ */
+ void searchNodeGetDone(const net::Request& status,
+ net::RequestAnswer&& answer,
+ std::weak_ptr<Search> ws,
+ Sp<Query> query);
+
+ /**
+ * Generic function to execute when a 'get' request expires.
+ *
+ * @param status The request passed by the network engine.
+ * @param over Whether we're done to try sending the request to the node
+ * or not. This lets us mark a node as candidate.
+ * @param ws A weak pointer to the search concerned by the request.
+ * @param query The query sent to the node.
+ */
+ void searchNodeGetExpired(const net::Request& status, bool over, std::weak_ptr<Search> ws, Sp<Query> query);
+
+ /**
+ * This method recovers sends individual request for values per id.
+ *
+ * @param ws A weak pointer to the Search.
+ * @param query The initial query passed through the API.
+ * @param n The node to which send the requests.
+ */
+ void paginate(std::weak_ptr<Search> ws, Sp<Query> query, SearchNode* n);
+
+ /**
+ * If update is true, this method will also send message to synced but non-updated search nodes.
+ */
+ SearchNode* searchSendGetValues(Sp<Search> sr, SearchNode *n = nullptr, bool update = true);
+
+ /**
+ * Forwards an 'announce' request for a list of nodes to the network engine.
+ *
+ * @param sr The search for which we want to announce a value.
+ * @param announce The 'announce' structure.
+ */
+ void searchSendAnnounceValue(const Sp<Search>& sr, unsigned syncLevel = TARGET_NODES);
+
+ /**
+ * Main process of a Search's operations. This function will demand the
+ * network engine to send requests packets for all pending operations
+ * ('get', 'put' and 'listen').
+ *
+ * @param sr The search to execute its operations.
+ */
+ void searchStep(std::weak_ptr<Search> ws);
+
+ void searchSynchedNodeListen(const Sp<Search>&, SearchNode&);
+
+ void dumpSearch(const Search& sr, std::ostream& out) const;
+
+ bool neighbourhoodMaintenance(RoutingTable&);
+
+ void onError(Sp<net::Request> node, net::DhtProtocolException e);
+ /* when our address is reported by a distant peer. */
+ void onReportedAddr(const InfoHash& id, const SockAddr&);
+ /* when we receive a ping request */
+ net::RequestAnswer onPing(Sp<Node> node);
+ /* when we receive a "find node" request */
+ net::RequestAnswer onFindNode(Sp<Node> node, const InfoHash& hash, want_t want);
+ void onFindNodeDone(const Sp<Node>& status,
+ net::RequestAnswer& a,
+ Sp<Search> sr);
+ /* when we receive a "get values" request */
+ net::RequestAnswer onGetValues(Sp<Node> node,
+ const InfoHash& hash,
+ want_t want,
+ const Query& q);
+ 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,
+ const Blob& token,
+ size_t socket_id,
+ const Query& query,
+ int version = 0);
+ void onListenDone(const Sp<Node>& status,
+ 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 Blob& token,
+ const Value::Id& vid);
+ void onAnnounceDone(const Sp<Node>& status,
+ net::RequestAnswer& a,
+ Sp<Search>& sr);
+};
+
+}
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ * Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "infohash.h"
+#include "logger.h"
+#include "node_export.h"
+
+#include <queue>
+
+namespace dht {
+
+namespace net {
+ class DatagramSocket;
+}
+
+class OPENDHT_PUBLIC DhtInterface {
+public:
+ DhtInterface() = default;
+ DhtInterface(const Logger& l) : logger_(std::make_shared<Logger>(l)) {}
+ DhtInterface(const std::shared_ptr<Logger>& l) : logger_(l) {}
+ virtual ~DhtInterface() = default;
+
+ // [[deprecated]]
+ using Status = NodeStatus;
+ // [[deprecated]]
+ using NodeExport = dht::NodeExport;
+
+ /**
+ * Get the current status of the node for the given family.
+ */
+ virtual NodeStatus updateStatus(sa_family_t af) { return getStatus(af); }
+ virtual NodeStatus getStatus(sa_family_t af) const = 0;
+ virtual NodeStatus getStatus() const = 0;
+
+ void addOnConnectedCallback(std::function<void()> cb) {
+ onConnectCallbacks_.emplace(std::move(cb));
+ }
+ virtual void setOnPublicAddressChanged(PublicAddressChangedCb) {}
+
+ virtual net::DatagramSocket* getSocket() const { return {}; }
+
+ /**
+ * Get the ID of the DHT node.
+ */
+ virtual const InfoHash& getNodeId() const = 0;
+
+ /**
+ * Performs final operations before quitting.
+ * stop: if true, cancel ongoing operations and call their 'done'
+ * callbacks synchronously.
+ */
+ virtual void shutdown(ShutdownCallback cb, bool stop = false) = 0;
+
+ /**
+ * Returns true if the node is running (have access to an open socket).
+ *
+ * af: address family. If non-zero, will return true if the node
+ * is running for the provided family.
+ */
+ virtual bool isRunning(sa_family_t af = 0) const = 0;
+
+ virtual void registerType(const ValueType& type) = 0;
+
+ virtual const ValueType& getType(ValueType::Id type_id) const = 0;
+
+ virtual void addBootstrap(const std::string& /*host*/, const std::string& /*service*/) {};
+ virtual void clearBootstrap() {};
+
+ /**
+ * Insert a node in the main routing table.
+ * The node is not pinged, so this should be
+ * used to bootstrap efficiently from previously known nodes.
+ */
+ virtual void insertNode(const InfoHash& id, const SockAddr&) = 0;
+ virtual void insertNode(const NodeExport& n) = 0;
+
+ virtual void pingNode(SockAddr, DoneCallbackSimple&& cb={}) = 0;
+
+ virtual time_point periodic(const uint8_t *buf, size_t buflen, SockAddr, const time_point& now) = 0;
+ virtual time_point periodic(const uint8_t *buf, size_t buflen, const sockaddr* from, socklen_t fromlen, const time_point& now) = 0;
+
+ /**
+ * Get a value by searching on all available protocols (IPv4, IPv6),
+ * and call the provided get callback when values are found at key.
+ * The operation will start as soon as the node is connected to the network.
+ * @param cb a function called when new values are found on the network.
+ * It should return false to stop the operation.
+ * @param donecb a function called when the operation is complete.
+ cb and donecb won't be called again afterward.
+ * @param f a filter function used to prefilter values.
+ */
+ virtual void get(const InfoHash& key, GetCallback cb, DoneCallback donecb={}, Value::Filter&& f={}, Where&& w = {}) = 0;
+ virtual void get(const InfoHash& key, GetCallback cb, DoneCallbackSimple donecb={}, Value::Filter&& f={}, Where&& w = {}) = 0;
+ virtual void get(const InfoHash& key, GetCallbackSimple cb, DoneCallback donecb={}, Value::Filter&& f={}, Where&& w = {}) = 0;
+ virtual void get(const InfoHash& key, GetCallbackSimple cb, DoneCallbackSimple donecb, Value::Filter&& f={}, Where&& w = {}) = 0;
+
+ /**
+ * Similar to Dht::get, but sends a Query to filter data remotely.
+ * @param key the key for which to query data for.
+ * @param cb a function called when new values are found on the network.
+ * It should return false to stop the operation.
+ * @param done_cb a function called when the operation is complete.
+ cb and done_cb won't be called again afterward.
+ * @param q a query used to filter values on the remotes before they send a
+ * response.
+ */
+ virtual void query(const InfoHash& key, QueryCallback cb, DoneCallback done_cb = {}, Query&& q = {}) = 0;
+ virtual void query(const InfoHash& key, QueryCallback cb, DoneCallbackSimple done_cb = {}, Query&& q = {}) = 0;
+
+ /**
+ * Get locally stored data for the given hash.
+ */
+ virtual std::vector<Sp<Value>> getLocal(const InfoHash& key, const Value::Filter& f = {}) const = 0;
+
+ /**
+ * Get locally stored data for the given key and value id.
+ */
+ virtual Sp<Value> getLocalById(const InfoHash& key, Value::Id vid) const = 0;
+
+ /**
+ * Announce a value on all available protocols (IPv4, IPv6).
+ *
+ * The operation will start as soon as the node is connected to the network.
+ * The done callback will be called once, when the first announce succeeds, or fails.
+ */
+ virtual void put(const InfoHash& key,
+ Sp<Value>,
+ DoneCallback cb=nullptr,
+ time_point created=time_point::max(),
+ bool permanent = false) = 0;
+ virtual void put(const InfoHash& key,
+ const Sp<Value>& v,
+ DoneCallbackSimple cb,
+ time_point created=time_point::max(),
+ bool permanent = false) = 0;
+ virtual void put(const InfoHash& key,
+ Value&& v,
+ DoneCallback cb=nullptr,
+ time_point created=time_point::max(),
+ bool permanent = false) = 0;
+ virtual void put(const InfoHash& key,
+ Value&& v,
+ DoneCallbackSimple cb,
+ time_point created=time_point::max(),
+ bool permanent = false) = 0;
+
+ /**
+ * Get data currently being put at the given hash.
+ */
+ virtual std::vector<Sp<Value>> getPut(const InfoHash&) const = 0;
+
+ /**
+ * Get data currently being put at the given hash with the given id.
+ */
+ virtual Sp<Value> getPut(const InfoHash&, const Value::Id&) const = 0;
+
+ /**
+ * Stop any put/announce operation at the given location,
+ * for the value with the given id.
+ */
+ virtual bool cancelPut(const InfoHash&, const Value::Id&) = 0;
+
+ /**
+ * Listen on the network for any changes involving a specified hash.
+ * The node will register to receive updates from relevent nodes when
+ * new values are added or removed.
+ *
+ * @return a token to cancel the listener later.
+ */
+ virtual size_t listen(const InfoHash&, GetCallback, Value::Filter={}, Where w = {}) = 0;
+ virtual size_t listen(const InfoHash& key, GetCallbackSimple cb, Value::Filter f={}, Where w = {}) = 0;
+ virtual size_t listen(const InfoHash&, ValueCallback, Value::Filter={}, Where w = {}) = 0;
+
+ virtual bool cancelListen(const InfoHash&, size_t token) = 0;
+
+ /**
+ * Inform the DHT of lower-layer connectivity changes.
+ * This will cause the DHT to assume a public IP address change.
+ * The DHT will recontact neighbor nodes, re-register for listen ops etc.
+ */
+ virtual void connectivityChanged(sa_family_t) = 0;
+ virtual void connectivityChanged() = 0;
+
+ /**
+ * Get the list of good nodes for local storage saving purposes
+ * The list is ordered to minimize the back-to-work delay.
+ */
+ virtual std::vector<NodeExport> exportNodes() const = 0;
+
+ virtual std::vector<ValuesExport> exportValues() const = 0;
+ virtual void importValues(const std::vector<ValuesExport>&) = 0;
+
+ virtual NodeStats getNodesStats(sa_family_t af) const = 0;
+
+ virtual std::string getStorageLog() const = 0;
+ virtual std::string getStorageLog(const InfoHash&) const = 0;
+
+ virtual std::string getRoutingTablesLog(sa_family_t) const = 0;
+ virtual std::string getSearchesLog(sa_family_t) const = 0;
+ virtual std::string getSearchLog(const InfoHash&, sa_family_t af = AF_UNSPEC) const = 0;
+
+ virtual void dumpTables() const = 0;
+ virtual std::vector<unsigned> getNodeMessageStats(bool in = false) = 0;
+
+ /**
+ * Set the in-memory storage limit in bytes
+ */
+ virtual void setStorageLimit(size_t limit = DEFAULT_STORAGE_LIMIT) = 0;
+ virtual size_t getStorageLimit() const = 0;
+
+ /**
+ * Returns the total memory usage of stored values and the number
+ * of stored values.
+ */
+ virtual std::pair<size_t, size_t> getStoreSize() const = 0;
+
+ virtual std::vector<SockAddr> getPublicAddress(sa_family_t family = 0) = 0;
+
+ virtual void setLogger(const Logger& l) {
+ if (logger_)
+ *logger_ = l;
+ else
+ logger_= std::make_shared<Logger>(l);
+ }
+
+ virtual void setLogger(const std::shared_ptr<Logger>& l) {
+ logger_ = l;
+ }
+
+ /**
+ * Only print logs related to the given InfoHash (if given), or disable filter (if zeroes).
+ */
+ virtual void setLogFilter(const InfoHash& f)
+ {
+ if (logger_)
+ logger_->setFilter(f);
+ }
+
+ virtual void setPushNotificationToken(const std::string&) {};
+
+ virtual void setPushNotificationTopic(const std::string&) {};
+ virtual void setPushNotificationPlatform(const std::string&) {};
+
+ /**
+ * Call linked callback with a push notification
+ * @param notification to process
+ */
+ virtual void pushNotificationReceived(const std::map<std::string, std::string>& data) = 0;
+
+protected:
+ std::shared_ptr<Logger> logger_ {};
+ std::queue<std::function<void()>> onConnectCallbacks_ {};
+};
+
+} // namespace dht
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ * Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com>
+ * Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ * Vsevolod Ivanov <vsevolod.ivanov@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <functional>
+#include <mutex>
+
+#include "callbacks.h"
+#include "def.h"
+#include "dht_interface.h"
+#include "proxy.h"
+#include "http.h"
+
+#include <restinio/all.hpp>
+#include <json/json.h>
+
+#include <chrono>
+#include <vector>
+#include <functional>
+
+namespace Json {
+class Value;
+}
+
+namespace http {
+class Resolver;
+class Request;
+}
+
+namespace dht {
+
+class OPENDHT_PUBLIC DhtProxyClient final : public DhtInterface {
+public:
+
+ DhtProxyClient();
+
+ explicit DhtProxyClient(
+ std::shared_ptr<crypto::Certificate> serverCA, crypto::Identity clientIdentity,
+ std::function<void()> loopSignal, const std::string& serverHost,
+ const std::string& pushClientId = "", std::shared_ptr<Logger> logger = {});
+
+ void setHeaderFields(http::Request& request);
+
+ virtual void setPushNotificationToken(const std::string& token) override {
+#ifdef OPENDHT_PUSH_NOTIFICATIONS
+ deviceKey_ = token;
+#else
+ (void) token;
+#endif
+ }
+
+ virtual void setPushNotificationTopic(const std::string& topic) override {
+#ifdef OPENDHT_PUSH_NOTIFICATIONS
+ notificationTopic_ = topic;
+#else
+ (void) topic;
+#endif
+ }
+
+ virtual void setPushNotificationPlatform(const std::string& platform) override {
+#ifdef OPENDHT_PUSH_NOTIFICATIONS
+ platform_ = platform;
+#else
+ (void) platform;
+#endif
+ }
+
+ virtual ~DhtProxyClient();
+
+ /**
+ * Get the ID of the node.
+ */
+ inline const InfoHash& getNodeId() const override { return myid; }
+ void setOnPublicAddressChanged(PublicAddressChangedCb cb) override {
+ publicAddressChangedCb_ = std::move(cb);
+ }
+
+ /**
+ * Get the current status of the node for the given family.
+ */
+ NodeStatus getStatus(sa_family_t af) const override;
+ NodeStatus getStatus() const override {
+ return std::max(getStatus(AF_INET), getStatus(AF_INET6));
+ }
+
+ /**
+ * Performs final operations before quitting.
+ */
+ void shutdown(ShutdownCallback cb, bool) override;
+
+ /**
+ * Returns true if the node is running (have access to an open socket).
+ *
+ * af: address family. If non-zero, will return true if the node
+ * is running for the provided family.
+ */
+ bool isRunning(sa_family_t af = 0) const override;
+
+ /**
+ * Get a value by asking the proxy and call the provided get callback when
+ * values are found at key.
+ * The operation will start as soon as the node is connected to the network.
+ * @param cb a function called when new values are found on the network.
+ * It should return false to stop the operation.
+ * @param donecb a function called when the operation is complete.
+ cb and donecb won't be called again afterward.
+ * @param f a filter function used to prefilter values.
+ */
+ virtual void get(const InfoHash& key, GetCallback cb, DoneCallback donecb={}, Value::Filter&& f={}, Where&& w = {}) override;
+ virtual void get(const InfoHash& key, GetCallback cb, DoneCallbackSimple donecb={}, Value::Filter&& f={}, Where&& w = {}) override {
+ get(key, cb, bindDoneCb(std::move(donecb)), std::forward<Value::Filter>(f), std::forward<Where>(w));
+ }
+ virtual void get(const InfoHash& key, GetCallbackSimple cb, DoneCallback donecb={}, Value::Filter&& f={}, Where&& w = {}) override {
+ get(key, bindGetCb(cb), std::move(donecb), std::forward<Value::Filter>(f), std::forward<Where>(w));
+ }
+ virtual void get(const InfoHash& key, GetCallbackSimple cb, DoneCallbackSimple donecb, Value::Filter&& f={}, Where&& w = {}) override {
+ get(key, bindGetCb(cb), bindDoneCb(std::move(donecb)), std::forward<Value::Filter>(f), std::forward<Where>(w));
+ }
+
+ /**
+ * Announce a value on all available protocols (IPv4, IPv6).
+ *
+ * The operation will start as soon as the node is connected to the network.
+ * The done callback will be called once, when the first announce succeeds, or fails.
+ * NOTE: For now, created parameter is ignored.
+ */
+ void put(const InfoHash& key,
+ Sp<Value>,
+ DoneCallback cb=nullptr,
+ time_point created=time_point::max(),
+ bool permanent = false) override;
+ void put(const InfoHash& key,
+ const Sp<Value>& v,
+ DoneCallbackSimple cb,
+ time_point created=time_point::max(),
+ bool permanent = false) override
+ {
+ put(key, v, bindDoneCb(std::move(cb)), created, permanent);
+ }
+
+ void put(const InfoHash& key,
+ Value&& v,
+ DoneCallback cb=nullptr,
+ time_point created=time_point::max(),
+ bool permanent = false) override
+ {
+ put(key, std::make_shared<Value>(std::move(v)), std::move(cb), created, permanent);
+ }
+ void put(const InfoHash& key,
+ Value&& v,
+ DoneCallbackSimple cb,
+ time_point created=time_point::max(),
+ bool permanent = false) override
+ {
+ put(key, std::forward<Value>(v), bindDoneCb(std::move(cb)), created, permanent);
+ }
+
+ /**
+ * @param af the socket family
+ * @return node stats from the proxy
+ */
+ NodeStats getNodesStats(sa_family_t af) const override;
+
+ /**
+ * @param family the socket family
+ * @return public address
+ */
+ std::vector<SockAddr> getPublicAddress(sa_family_t family = 0) override;
+
+ /**
+ * Listen on the network for any changes involving a specified hash.
+ * The node will register to receive updates from relevent nodes when
+ * new values are added or removed.
+ *
+ * @return a token to cancel the listener later.
+ */
+ virtual size_t listen(const InfoHash&, ValueCallback, Value::Filter={}, Where={}) override;
+
+ virtual size_t listen(const InfoHash& key, GetCallback cb, Value::Filter f={}, Where w={}) override {
+ return listen(key, [cb=std::move(cb)](const std::vector<Sp<Value>>& vals, bool expired){
+ if (not expired)
+ return cb(vals);
+ return true;
+ }, std::forward<Value::Filter>(f), std::forward<Where>(w));
+ }
+ virtual size_t listen(const InfoHash& key, GetCallbackSimple cb, Value::Filter f={}, Where w={}) override {
+ return listen(key, bindGetCb(std::move(cb)), std::forward<Value::Filter>(f), std::forward<Where>(w));
+ }
+ /*
+ * This function relies on the cache implementation.
+ * It means that there are no true cancel here, it keeps the caching in higher priority.
+ */
+ virtual bool cancelListen(const InfoHash& key, size_t token) override;
+
+ /**
+ * Call linked callback with a push notification
+ * @param notification to process
+ */
+ void pushNotificationReceived(const std::map<std::string, std::string>& notification) override;
+
+ time_point periodic(const uint8_t*, size_t, SockAddr, const time_point& now) override;
+ time_point periodic(const uint8_t* buf, size_t buflen, const sockaddr* from, socklen_t fromlen, const time_point& now) override {
+ return periodic(buf, buflen, SockAddr(from, fromlen), now);
+ }
+
+ /**
+ * Similar to Dht::get, but sends a Query to filter data remotely.
+ * @param key the key for which to query data for.
+ * @param cb a function called when new values are found on the network.
+ * It should return false to stop the operation.
+ * @param done_cb a function called when the operation is complete.
+ cb and done_cb won't be called again afterward.
+ * @param q a query used to filter values on the remotes before they send a
+ * response.
+ */
+ virtual void query(const InfoHash& /*key*/, QueryCallback /*cb*/, DoneCallback /*done_cb*/ = {}, Query&& /*q*/ = {}) override { }
+ virtual void query(const InfoHash& key, QueryCallback cb, DoneCallbackSimple done_cb = {}, Query&& q = {}) override {
+ query(key, cb, bindDoneCb(std::move(done_cb)), std::forward<Query>(q));
+ }
+
+ /**
+ * Get data currently being put at the given hash.
+ */
+ std::vector<Sp<Value>> getPut(const InfoHash&) const override;
+
+ /**
+ * Get data currently being put at the given hash with the given id.
+ */
+ Sp<Value> getPut(const InfoHash&, const Value::Id&) const override;
+
+ /**
+ * Stop any put/announce operation at the given location,
+ * for the value with the given id.
+ */
+ bool cancelPut(const InfoHash&, const Value::Id&) override;
+
+ void pingNode(SockAddr, DoneCallbackSimple&& /*cb*/={}) override { }
+
+ virtual void registerType(const ValueType& type) override {
+ types.registerType(type);
+ }
+ const ValueType& getType(ValueType::Id type_id) const override {
+ return types.getType(type_id);
+ }
+
+ std::vector<Sp<Value>> getLocal(const InfoHash& k, const Value::Filter& filter) const override;
+ Sp<Value> getLocalById(const InfoHash& k, Value::Id id) const override;
+
+ /**
+ * NOTE: The following methods will not be implemented because the
+ * DhtProxyClient doesn't have any storage nor synchronization process
+ */
+ void insertNode(const InfoHash&, const SockAddr&) override { }
+ void insertNode(const NodeExport&) override { }
+ std::pair<size_t, size_t> getStoreSize() const override { return {}; }
+ std::vector<NodeExport> exportNodes() const override { return {}; }
+ std::vector<ValuesExport> exportValues() const override { return {}; }
+ void importValues(const std::vector<ValuesExport>&) override {}
+ std::string getStorageLog() const override { return {}; }
+ std::string getStorageLog(const InfoHash&) const override { return {}; }
+ std::string getRoutingTablesLog(sa_family_t) const override { return {}; }
+ std::string getSearchesLog(sa_family_t) const override { return {}; }
+ std::string getSearchLog(const InfoHash&, sa_family_t) const override { return {}; }
+ void dumpTables() const override {}
+ std::vector<unsigned> getNodeMessageStats(bool) override { return {}; }
+ void setStorageLimit(size_t) override {}
+ virtual size_t getStorageLimit() const override { return 0; }
+ void connectivityChanged(sa_family_t) override {
+ getProxyInfos();
+ }
+ void connectivityChanged() override {
+ getProxyInfos();
+ loopSignal_();
+ }
+
+private:
+ /**
+ * Start the connection with a server.
+ */
+ void startProxy();
+ void stop();
+
+ /**
+ * Get informations from the proxy node
+ * @return the JSON returned by the proxy
+ */
+ struct InfoState;
+ void getProxyInfos();
+ void queryProxyInfo(const std::shared_ptr<InfoState>& infoState, const std::shared_ptr<http::Resolver>& resolver, sa_family_t family);
+ void onProxyInfos(const Json::Value& val, const sa_family_t family);
+ SockAddr parsePublicAddress(const Json::Value& val);
+
+ void opFailed();
+
+ void handleExpireListener(const asio::error_code &ec, const InfoHash& key);
+
+ struct Listener;
+ struct OperationState;
+ enum class ListenMethod {
+ LISTEN,
+ SUBSCRIBE,
+ RESUBSCRIBE,
+ };
+ using CacheValueCallback = std::function<bool(const std::vector<std::shared_ptr<Value>>& values, bool expired, system_clock::time_point)>;
+
+ /**
+ * Send Listen with httpClient_
+ */
+ void sendListen(const restinio::http_request_header_t& header, const CacheValueCallback& cb,
+ const Sp<OperationState>& opstate, Listener& listener, ListenMethod method = ListenMethod::LISTEN);
+ void handleResubscribe(const asio::error_code& ec, const InfoHash& key,
+ const size_t token, std::shared_ptr<OperationState> opstate);
+
+ void doPut(const InfoHash&, Sp<Value>, DoneCallbackSimple, time_point created, bool permanent);
+ void handleRefreshPut(const asio::error_code& ec, InfoHash key, Value::Id id);
+
+ /**
+ * Initialize statusIpvX_
+ */
+ void getConnectivityStatus();
+ /**
+ * cancel all Listeners
+ */
+ void cancelAllListeners();
+
+ std::atomic_bool isDestroying_ {false};
+
+ std::string proxyUrl_;
+ dht::crypto::Identity clientIdentity_;
+ std::shared_ptr<dht::crypto::Certificate> serverCertificate_;
+ //std::pair<std::string, std::string> serverHostService_;
+ std::string pushClientId_;
+ std::string pushSessionId_;
+
+ mutable std::mutex lockCurrentProxyInfos_;
+ NodeStatus statusIpv4_ {NodeStatus::Disconnected};
+ NodeStatus statusIpv6_ {NodeStatus::Disconnected};
+ NodeStats stats4_ {};
+ NodeStats stats6_ {};
+ SockAddr localAddrv4_;
+ SockAddr localAddrv6_;
+ SockAddr publicAddressV4_;
+ SockAddr publicAddressV6_;
+ std::atomic_bool launchConnectedCbs_ {false};
+ PublicAddressChangedCb publicAddressChangedCb_ {};
+
+ InfoHash myid {};
+
+ // registred types
+ TypeStore types;
+
+ /*
+ * ASIO I/O Context for sockets in httpClient_
+ * Note: Each context is used in one thread only
+ */
+ asio::io_context httpContext_;
+ mutable std::mutex resolverLock_;
+ std::shared_ptr<http::Resolver> resolver_;
+
+ mutable std::mutex requestLock_;
+ std::map<unsigned, std::shared_ptr<http::Request>> requests_;
+ /*
+ * Thread for executing the http io_context.run() blocking call
+ */
+ std::thread httpClientThread_;
+
+ /**
+ * Store listen requests.
+ */
+ struct ProxySearch;
+
+ mutable std::mutex searchLock_;
+ size_t listenerToken_ {0};
+ std::map<InfoHash, ProxySearch> searches_;
+
+ /**
+ * Callbacks should be executed in the main thread.
+ */
+ std::mutex lockCallbacks_;
+ std::vector<std::function<void()>> callbacks_;
+
+ Sp<InfoState> infoState_;
+
+ /**
+ * Retrieve if we can connect to the proxy (update statusIpvX_)
+ */
+ void handleProxyConfirm(const asio::error_code &ec);
+ std::unique_ptr<asio::steady_timer> nextProxyConfirmationTimer_;
+ std::unique_ptr<asio::steady_timer> listenerRestartTimer_;
+
+ /**
+ * Relaunch LISTEN requests if the client disconnect/reconnect.
+ */
+ void restartListeners(const asio::error_code &ec);
+
+ /**
+ * Refresh a listen via a token
+ * @param token
+ */
+ void resubscribe(const InfoHash& key, const size_t token, Listener& listener);
+
+ /**
+ * If we want to use push notifications by default.
+ * NOTE: empty by default to avoid to use services like FCM or APN.
+ */
+ std::string deviceKey_ {};
+
+ /**
+ * Notification topic for ios notifications.
+ */
+ std::string notificationTopic_ {};
+
+ /**
+ * Platform for push notifications (supported android, ios, unifiedpush)
+ */
+ std::string platform_
+#ifdef __ANDROID__
+ {"android"};
+#else
+#ifdef __APPLE__
+ {"ios"};
+#else
+ {};
+#endif
+#endif
+
+ const std::function<void()> loopSignal_;
+
+#ifdef OPENDHT_PUSH_NOTIFICATIONS
+ std::string fillBody(bool resubscribe);
+ void getPushRequest(Json::Value&) const;
+#endif // OPENDHT_PUSH_NOTIFICATIONS
+
+ Json::StreamWriterBuilder jsonBuilder_;
+ std::unique_ptr<Json::CharReader> jsonReader_;
+
+ std::shared_ptr<http::Request> buildRequest(const std::string& target = {});
+};
+
+}
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ * Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com>
+ * Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ * Vsevolod Ivanov <vsevolod.ivanov@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "callbacks.h"
+#include "def.h"
+#include "infohash.h"
+#include "proxy.h"
+#include "scheduler.h"
+#include "sockaddr.h"
+#include "value.h"
+#include "http.h"
+
+#include <restinio/all.hpp>
+#include <restinio/tls.hpp>
+#include <json/json.h>
+
+#include <memory>
+#include <mutex>
+
+namespace dht {
+enum class PushType {
+ None = 0,
+ Android,
+ iOS,
+ UnifiedPush
+};
+}
+MSGPACK_ADD_ENUM(dht::PushType)
+
+namespace http {
+class Request;
+struct ListenerSession;
+}
+
+namespace Json {
+class Value;
+}
+
+namespace dht {
+
+class DhtRunner;
+
+using RestRouter = restinio::router::express_router_t<>;
+using RequestStatus = restinio::request_handling_status_t;
+
+struct OPENDHT_PUBLIC ProxyServerConfig {
+ std::string address {};
+ in_port_t port {8000};
+ std::string pushServer {};
+ std::string persistStatePath {};
+ dht::crypto::Identity identity {};
+ std::string bundleId {};
+};
+
+/**
+ * Describes the REST API
+ */
+class OPENDHT_PUBLIC DhtProxyServer
+{
+public:
+ /**
+ * Start the Http server for OpenDHT
+ * @param dht the DhtRunner linked to this proxy server
+ * @param port to listen
+ * @param pushServer where to push notifications
+ * @note if the server fails to start (if port is already used or reserved),
+ * it will fails silently
+ */
+ DhtProxyServer(const std::shared_ptr<DhtRunner>& dht,
+ const ProxyServerConfig& config = {},
+ const std::shared_ptr<log::Logger>& logger = {});
+
+ virtual ~DhtProxyServer();
+
+ DhtProxyServer(const DhtProxyServer& other) = delete;
+ DhtProxyServer(DhtProxyServer&& other) = delete;
+ DhtProxyServer& operator=(const DhtProxyServer& other) = delete;
+ DhtProxyServer& operator=(DhtProxyServer&& other) = delete;
+
+ asio::io_context& io_context() const;
+
+ struct ServerStats {
+ /** Current number of listen operations */
+ size_t listenCount {0};
+ /** Current number of permanent put operations (hash used) */
+ size_t putCount {0};
+ /** Current number of permanent put values */
+ size_t totalPermanentPuts {0};
+ /** Current number of push tokens with at least one listen operation */
+ size_t pushListenersCount {0};
+ /** Average requests per second */
+ double requestRate {0};
+ /** Node Info **/
+ std::shared_ptr<NodeInfo> nodeInfo {};
+
+ std::string toString() const {
+ std::ostringstream ss;
+ ss << "Listens: " << listenCount << " Puts: " << putCount << " PushListeners: " << pushListenersCount << std::endl;
+ ss << "Requests: " << requestRate << " per second." << std::endl;
+ if (nodeInfo) {
+ auto& ipv4 = nodeInfo->ipv4;
+ if (ipv4.table_depth > 1)
+ ss << "IPv4 Network estimation: " << ipv4.getNetworkSizeEstimation() << std::endl;;
+ auto& ipv6 = nodeInfo->ipv6;
+ if (ipv6.table_depth > 1)
+ ss << "IPv6 Network estimation: " << ipv6.getNetworkSizeEstimation() << std::endl;;
+ }
+ return ss.str();
+ }
+
+ /**
+ * Build a json object from a NodeStats
+ */
+ Json::Value toJson() const {
+ Json::Value result;
+ result["listenCount"] = static_cast<Json::UInt64>(listenCount);
+ result["putCount"] = static_cast<Json::UInt64>(putCount);
+ result["totalPermanentPuts"] = static_cast<Json::UInt64>(totalPermanentPuts);
+ result["pushListenersCount"] = static_cast<Json::UInt64>(pushListenersCount);
+ result["requestRate"] = requestRate;
+ if (nodeInfo)
+ result["nodeInfo"] = nodeInfo->toJson();
+ return result;
+ }
+ };
+
+ std::shared_ptr<ServerStats> stats() const { return stats_; }
+
+ std::shared_ptr<ServerStats> updateStats(std::shared_ptr<NodeInfo> info) const;
+
+ std::shared_ptr<DhtRunner> getNode() const { return dht_; }
+
+private:
+ class ConnectionListener;
+ struct RestRouterTraitsTls;
+ struct RestRouterTraits;
+
+ template <typename HttpResponse>
+ static HttpResponse initHttpResponse(HttpResponse response);
+ static restinio::request_handling_status_t serverError(restinio::request_t& request);
+
+ template< typename ServerSettings >
+ void addServerSettings(ServerSettings& serverSettings,
+ const unsigned int max_pipelined_requests = 16);
+
+ std::unique_ptr<RestRouter> createRestRouter();
+
+ void onConnectionClosed(restinio::connection_id_t);
+
+ /**
+ * Return the PublicKey id, the node id and node stats
+ * Method: GET "/"
+ * Result: HTTP 200, body: Node infos in JSON format
+ * On error: HTTP 503, body: {"err":"xxxx"}
+ * @param session
+ */
+ RequestStatus getNodeInfo(restinio::request_handle_t request,
+ restinio::router::route_params_t params) const;
+
+ /**
+ * Return ServerStats in JSON format
+ * Method: STATS "/"
+ * Result: HTTP 200, body: Node infos in JSON format
+ * @param session
+ */
+ RequestStatus getStats(restinio::request_handle_t request,
+ restinio::router::route_params_t params);
+
+ /**
+ * Return Values of an infoHash
+ * Method: GET "/{InfoHash: .*}"
+ * Return: Multiple JSON object in parts. Example:
+ * Value in JSON format\n
+ * Value in JSON format
+ *
+ * On error: HTTP 503, body: {"err":"xxxx"}
+ * @param session
+ */
+ RequestStatus get(restinio::request_handle_t request,
+ restinio::router::route_params_t params);
+
+ /**
+ * Listen incoming Values of an infoHash.
+ * Method: LISTEN "/{InfoHash: .*}"
+ * Return: Multiple JSON object in parts. Example:
+ * Value in JSON format\n
+ * Value in JSON format
+ *
+ * On error: HTTP 503, body: {"err":"xxxx"}
+ * @param session
+ */
+ RequestStatus listen(restinio::request_handle_t request,
+ restinio::router::route_params_t params);
+
+ /**
+ * Put a value on the DHT
+ * Method: POST "/{InfoHash: .*}"
+ * body = Value to put in JSON
+ * Return: HTTP 200 if success and the value put in JSON
+ * On error: HTTP 503, body: {"err":"xxxx"} if no dht
+ * HTTP 400, body: {"err":"xxxx"} if bad json or HTTP 502 if put fails
+ * @param session
+ */
+ RequestStatus put(restinio::request_handle_t request,
+ restinio::router::route_params_t params);
+
+ void handleCancelPermamentPut(const asio::error_code &ec, const InfoHash& key, Value::Id vid);
+
+#ifdef OPENDHT_PROXY_SERVER_IDENTITY
+ /**
+ * Put a value to sign by the proxy on the DHT
+ * Method: SIGN "/{InfoHash: .*}"
+ * body = Value to put in JSON
+ * Return: HTTP 200 if success and the value put in JSON
+ * On error: HTTP 503, body: {"err":"xxxx"} if no dht
+ * HTTP 400, body: {"err":"xxxx"} if bad json
+ * @param session
+ */
+ RequestStatus putSigned(restinio::request_handle_t request,
+ restinio::router::route_params_t params) const;
+
+ /**
+ * Put a value to encrypt by the proxy on the DHT
+ * Method: ENCRYPT "/{hash: .*}"
+ * body = Value to put in JSON + "to":"infoHash"
+ * Return: HTTP 200 if success and the value put in JSON
+ * On error: HTTP 503, body: {"err":"xxxx"} if no dht
+ * HTTP 400, body: {"err":"xxxx"} if bad json
+ * @param session
+ */
+ RequestStatus putEncrypted(restinio::request_handle_t request,
+ restinio::router::route_params_t params);
+
+#endif // OPENDHT_PROXY_SERVER_IDENTITY
+
+ /**
+ * Return Values of an infoHash filtered by a value id
+ * Method: GET "/{InfoHash: .*}/{ValueId: .*}"
+ * Return: Multiple JSON object in parts. Example:
+ * Value in JSON format\n
+ * Value in JSON format
+ *
+ * On error: HTTP 503, body: {"err":"xxxx"}
+ * @param session
+ */
+ RequestStatus getFiltered(restinio::request_handle_t request,
+ restinio::router::route_params_t params);
+
+ /**
+ * Respond allowed Methods
+ * Method: OPTIONS "/{hash: .*}"
+ * Return: HTTP 200 + Allow: allowed methods
+ * See https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/OPTIONS
+ * @param session
+ */
+ RequestStatus options(restinio::request_handle_t request,
+ restinio::router::route_params_t params);
+
+#ifdef OPENDHT_PUSH_NOTIFICATIONS
+ PushType getTypeFromString(const std::string& type);
+ std::string getDefaultTopic(PushType type);
+
+ RequestStatus pingPush(restinio::request_handle_t request,
+ restinio::router::route_params_t /*params*/);
+ /**
+ * Subscribe to push notifications for an iOS or Android device.
+ * Method: SUBSCRIBE "/{InfoHash: .*}"
+ * Body: {"key": "device_key", (optional)"isAndroid":false (default true)}"
+ * Return: {"token": x}" where x if a token to save
+ * @note: the listen will timeout after six hours (and send a push notification).
+ * so you need to refresh the operation each six hours.
+ * @param session
+ */
+ RequestStatus subscribe(restinio::request_handle_t request,
+ restinio::router::route_params_t params);
+
+ /**
+ * Unsubscribe to push notifications for an iOS or Android device.
+ * Method: UNSUBSCRIBE "/{InfoHash: .*}"
+ * Body: {"key": "device_key", "token": x} where x if the token to cancel
+ * Return: nothing
+ * @param session
+ */
+ RequestStatus unsubscribe(restinio::request_handle_t request,
+ restinio::router::route_params_t params);
+
+ /**
+ * Send a push notification via a gorush push gateway
+ * @param key of the device
+ * @param json, the content to send
+ */
+ void sendPushNotification(const std::string& key, Json::Value&& json, PushType type, bool highPriority, const std::string& topic);
+
+ /**
+ * Send push notification with an expire timeout.
+ * @param ec
+ * @param pushToken
+ * @param json
+ * @param type
+ * @param topic
+ */
+ void handleNotifyPushListenExpire(const asio::error_code &ec, const std::string pushToken,
+ std::function<Json::Value()> json, PushType type, const std::string& topic);
+
+ /**
+ * Remove a push listener between a client and a hash
+ * @param ec
+ * @param pushToken
+ * @param key
+ * @param clientId
+ */
+ void handleCancelPushListen(const asio::error_code &ec, const std::string pushToken,
+ const InfoHash key, const std::string clientId);
+
+#endif //OPENDHT_PUSH_NOTIFICATIONS
+
+ void handlePrintStats(const asio::error_code &ec);
+ void updateStats();
+
+ template <typename Os>
+ void saveState(Os& stream);
+
+ template <typename Is>
+ void loadState(Is& is, size_t size);
+
+ using clock = std::chrono::steady_clock;
+ using time_point = clock::time_point;
+
+ std::shared_ptr<asio::io_context> ioContext_;
+ std::shared_ptr<DhtRunner> dht_;
+ Json::StreamWriterBuilder jsonBuilder_;
+ Json::CharReaderBuilder jsonReaderBuilder_;
+ std::mt19937_64 rd {crypto::getSeededRandomEngine<std::mt19937_64>()};
+
+ std::string persistPath_;
+
+ // http server
+ std::thread serverThread_;
+ std::unique_ptr<restinio::http_server_t<RestRouterTraitsTls>> httpsServer_;
+ std::unique_ptr<restinio::http_server_t<RestRouterTraits>> httpServer_;
+
+ // http client
+ std::pair<std::string, std::string> pushHostPort_;
+
+ mutable std::mutex requestLock_;
+ std::map<unsigned int /*id*/, std::shared_ptr<http::Request>> requests_;
+
+ std::shared_ptr<log::Logger> logger_;
+
+ std::shared_ptr<ServerStats> stats_;
+ std::shared_ptr<NodeInfo> nodeInfo_ {};
+ std::unique_ptr<asio::steady_timer> printStatsTimer_;
+
+ // Thread-safe access to listeners map.
+ std::mutex lockListener_;
+ // Shared with connection listener.
+ std::map<restinio::connection_id_t, http::ListenerSession> listeners_;
+ // Connection Listener observing conn state changes.
+ std::shared_ptr<ConnectionListener> connListener_;
+
+ struct PushSessionContext {
+ std::mutex lock;
+ std::string sessionId;
+ PushSessionContext(const std::string& id) : sessionId(id) {}
+ };
+ struct PermanentPut {
+ time_point expiration;
+ std::string pushToken;
+ std::string clientId;
+ std::shared_ptr<PushSessionContext> sessionCtx;
+ std::unique_ptr<asio::steady_timer> expireTimer;
+ std::unique_ptr<asio::steady_timer> expireNotifyTimer;
+ Sp<Value> value;
+ PushType type;
+ std::string topic;
+
+ template <typename Packer>
+ void msgpack_pack(Packer& p) const
+ {
+ p.pack_map(2 + (sessionCtx ? 1 : 0) + (clientId.empty() ? 0 : 1) + (type == PushType::None ? 0 : 2) + (topic.empty() ? 0 : 1));
+ p.pack("value"); p.pack(value);
+ p.pack("exp"); p.pack(to_time_t(expiration));
+ if (not clientId.empty()) {
+ p.pack("cid"); p.pack(clientId);
+ }
+ if (sessionCtx) {
+ std::lock_guard<std::mutex> l(sessionCtx->lock);
+ p.pack("sid"); p.pack(sessionCtx->sessionId);
+ }
+ if (type != PushType::None) {
+ p.pack("t"); p.pack(type);
+ p.pack("token"); p.pack(pushToken);
+ }
+ if (not topic.empty()) {
+ p.pack("top"); p.pack(topic);
+ }
+ }
+
+ void msgpack_unpack(const msgpack::object& o);
+ };
+ struct SearchPuts {
+ std::map<dht::Value::Id, PermanentPut> puts;
+ MSGPACK_DEFINE_ARRAY(puts)
+ };
+ std::mutex lockSearchPuts_;
+ std::map<InfoHash, SearchPuts> puts_;
+
+ mutable std::atomic<size_t> requestNum_ {0};
+ mutable std::atomic<time_point> lastStatsReset_ {time_point::min()};
+
+ std::string pushServer_;
+ std::string bundleId_;
+
+#ifdef OPENDHT_PUSH_NOTIFICATIONS
+ struct Listener {
+ time_point expiration;
+ std::string clientId;
+ std::shared_ptr<PushSessionContext> sessionCtx;
+ std::future<size_t> internalToken;
+ std::unique_ptr<asio::steady_timer> expireTimer;
+ std::unique_ptr<asio::steady_timer> expireNotifyTimer;
+ PushType type;
+ std::string topic;
+
+ template <typename Packer>
+ void msgpack_pack(Packer& p) const
+ {
+ p.pack_map(3 + (sessionCtx ? 1 : 0) + (topic.empty() ? 0 : 1));
+ p.pack("cid"); p.pack(clientId);
+ p.pack("exp"); p.pack(to_time_t(expiration));
+ if (sessionCtx) {
+ std::lock_guard<std::mutex> l(sessionCtx->lock);
+ p.pack("sid"); p.pack(sessionCtx->sessionId);
+ }
+ p.pack("t"); p.pack(type);
+ if (!topic.empty()) {
+ p.pack("top"); p.pack(topic);
+ }
+ }
+
+ void msgpack_unpack(const msgpack::object& o);
+ };
+ struct PushListener {
+ std::map<InfoHash, std::vector<Listener>> listeners;
+ MSGPACK_DEFINE_ARRAY(listeners)
+ };
+ std::map<std::string, PushListener> pushListeners_;
+#endif //OPENDHT_PUSH_NOTIFICATIONS
+};
+
+}
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ * Authors: Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ * Simon Désaulniers <simon.desaulniers@savoirfairelinux.com>
+ * Sébastien Blin <sebastien.blin@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "def.h"
+#include "infohash.h"
+#include "value.h"
+#include "callbacks.h"
+#include "sockaddr.h"
+#include "logger.h"
+#include "network_utils.h"
+#include "node_export.h"
+
+#include <thread>
+#include <mutex>
+#include <atomic>
+#include <condition_variable>
+#include <future>
+#include <exception>
+#include <queue>
+#include <chrono>
+
+namespace dht {
+
+struct Node;
+class SecureDht;
+class PeerDiscovery;
+struct SecureDhtConfig;
+
+/**
+ * Provides a thread-safe interface to run the (secure) DHT.
+ * The class will open sockets on the provided port and will
+ * either wait for (expectedly frequent) calls to ::loop() or start an internal
+ * thread that will update the DHT when appropriate.
+ */
+class OPENDHT_PUBLIC DhtRunner {
+
+public:
+ using StatusCallback = std::function<void(NodeStatus, NodeStatus)>;
+
+ struct Config {
+ SecureDhtConfig dht_config {};
+ bool threaded {true};
+ std::string proxy_server {};
+ std::string push_node_id {};
+ std::string push_token {};
+ std::string push_topic {};
+ std::string push_platform {};
+ bool peer_discovery {false};
+ bool peer_publish {false};
+ std::shared_ptr<dht::crypto::Certificate> server_ca;
+ dht::crypto::Identity client_identity;
+ SockAddr bind4 {}, bind6 {};
+ };
+
+ struct Context {
+ std::shared_ptr<Logger> logger {};
+ std::unique_ptr<net::DatagramSocket> sock;
+ std::shared_ptr<PeerDiscovery> peerDiscovery {};
+ StatusCallback statusChangedCallback {};
+ CertificateStoreQuery certificateStore {};
+ IdentityAnnouncedCb identityAnnouncedCb {};
+ PublicAddressChangedCb publicAddressChangedCb {};
+ Context() {}
+ };
+
+ DhtRunner();
+ virtual ~DhtRunner();
+
+ void get(InfoHash id, GetCallbackSimple cb, DoneCallback donecb={}, Value::Filter f = {}, Where w = {}) {
+ get(id, bindGetCb(cb), donecb, f, w);
+ }
+
+ void get(InfoHash id, GetCallbackSimple cb, DoneCallbackSimple donecb={}, Value::Filter f = {}, Where w = {}) {
+ get(id, bindGetCb(cb), donecb, f, w);
+ }
+
+ void get(InfoHash hash, GetCallback vcb, DoneCallback dcb, Value::Filter f={}, Where w = {});
+
+ void get(InfoHash id, GetCallback cb, DoneCallbackSimple donecb={}, Value::Filter f = {}, Where w = {}) {
+ get(id, cb, bindDoneCb(donecb), f, w);
+ }
+ void get(const std::string& key, GetCallback vcb, DoneCallbackSimple dcb={}, Value::Filter f = {}, Where w = {});
+
+ template <class T>
+ void get(InfoHash hash, std::function<bool(std::vector<T>&&)> cb, DoneCallbackSimple dcb={})
+ {
+ get(hash, [cb=std::move(cb)](const std::vector<std::shared_ptr<Value>>& vals) {
+ return cb(unpackVector<T>(vals));
+ },
+ dcb,
+ getFilterSet<T>());
+ }
+ template <class T>
+ void get(InfoHash hash, std::function<bool(T&&)> cb, DoneCallbackSimple dcb={})
+ {
+ get(hash, [cb=std::move(cb)](const std::vector<std::shared_ptr<Value>>& vals) {
+ for (const auto& v : vals) {
+ try {
+ if (not cb(Value::unpack<T>(*v)))
+ return false;
+ } catch (const std::exception&) {
+ continue;
+ }
+ }
+ return true;
+ },
+ dcb,
+ getFilterSet<T>());
+ }
+
+ std::future<std::vector<std::shared_ptr<dht::Value>>> get(InfoHash key, Value::Filter f = {}, Where w = {}) {
+ auto p = std::make_shared<std::promise<std::vector<std::shared_ptr< dht::Value >>>>();
+ auto values = std::make_shared<std::vector<std::shared_ptr< dht::Value >>>();
+ get(key, [=](const std::vector<std::shared_ptr<dht::Value>>& vlist) {
+ values->insert(values->end(), vlist.begin(), vlist.end());
+ return true;
+ }, [=](bool) {
+ p->set_value(std::move(*values));
+ },
+ f, w);
+ return p->get_future();
+ }
+
+ template <class T>
+ std::future<std::vector<T>> get(InfoHash key) {
+ auto p = std::make_shared<std::promise<std::vector<T>>>();
+ auto values = std::make_shared<std::vector<T>>();
+ get<T>(key, [=](T&& v) {
+ values->emplace_back(std::move(v));
+ return true;
+ }, [=](bool) {
+ p->set_value(std::move(*values));
+ });
+ return p->get_future();
+ }
+
+ void query(const InfoHash& hash, QueryCallback cb, DoneCallback done_cb = {}, Query q = {});
+ void query(const InfoHash& hash, QueryCallback cb, DoneCallbackSimple done_cb = {}, Query q = {}) {
+ query(hash, cb, bindDoneCb(done_cb), q);
+ }
+
+ std::future<size_t> listen(InfoHash key, ValueCallback vcb, Value::Filter f = {}, Where w = {});
+
+ std::future<size_t> listen(InfoHash key, GetCallback cb, Value::Filter f={}, Where w={}) {
+ return listen(key, [cb=std::move(cb)](const std::vector<Sp<Value>>& vals, bool expired){
+ if (not expired)
+ return cb(vals);
+ return true;
+ }, std::forward<Value::Filter>(f), std::forward<Where>(w));
+ }
+ std::future<size_t> listen(const std::string& key, GetCallback vcb, Value::Filter f = {}, Where w = {});
+ std::future<size_t> listen(InfoHash key, GetCallbackSimple cb, Value::Filter f = {}, Where w = {}) {
+ return listen(key, bindGetCb(cb), f, w);
+ }
+
+ template <class T>
+ std::future<size_t> listen(InfoHash hash, std::function<bool(std::vector<T>&&)> cb)
+ {
+ return listen(hash, [cb=std::move(cb)](const std::vector<std::shared_ptr<Value>>& vals) {
+ return cb(unpackVector<T>(vals));
+ },
+ getFilterSet<T>());
+ }
+ template <class T>
+ std::future<size_t> listen(InfoHash hash, std::function<bool(std::vector<T>&&, bool)> cb)
+ {
+ return listen(hash, [cb=std::move(cb)](const std::vector<std::shared_ptr<Value>>& vals, bool expired) {
+ return cb(unpackVector<T>(vals), expired);
+ },
+ getFilterSet<T>());
+ }
+
+ template <typename T>
+ std::future<size_t> listen(InfoHash hash, std::function<bool(T&&)> cb, Value::Filter f = {}, Where w = {})
+ {
+ return listen(hash, [cb=std::move(cb)](const std::vector<std::shared_ptr<Value>>& vals) {
+ for (const auto& v : vals) {
+ try {
+ if (not cb(Value::unpack<T>(*v)))
+ return false;
+ } catch (const std::exception&) {
+ continue;
+ }
+ }
+ return true;
+ },
+ getFilterSet<T>(f), w);
+ }
+ template <typename T>
+ std::future<size_t> listen(InfoHash hash, std::function<bool(T&&, bool)> cb, Value::Filter f = {}, Where w = {})
+ {
+ return listen(hash, [cb=std::move(cb)](const std::vector<std::shared_ptr<Value>>& vals, bool expired) {
+ for (const auto& v : vals) {
+ try {
+ if (not cb(Value::unpack<T>(*v), expired))
+ return false;
+ } catch (const std::exception&) {
+ continue;
+ }
+ }
+ return true;
+ },
+ getFilterSet<T>(f), w);
+ }
+
+ void cancelListen(InfoHash h, size_t token);
+ void cancelListen(InfoHash h, std::shared_future<size_t> token);
+
+ void put(InfoHash hash, std::shared_ptr<Value> value, DoneCallback cb={}, time_point created=time_point::max(), bool permanent = false);
+ void put(InfoHash hash, std::shared_ptr<Value> value, DoneCallbackSimple cb, time_point created=time_point::max(), bool permanent = false) {
+ put(hash, value, bindDoneCb(cb), created, permanent);
+ }
+
+ void put(InfoHash hash, Value&& value, DoneCallback cb={}, time_point created=time_point::max(), bool permanent = false);
+ void put(InfoHash hash, Value&& value, DoneCallbackSimple cb, time_point created=time_point::max(), bool permanent = false) {
+ put(hash, std::forward<Value>(value), bindDoneCb(cb), created, permanent);
+ }
+ void put(const std::string& key, Value&& value, DoneCallbackSimple cb={}, time_point created=time_point::max(), bool permanent = false);
+
+ void cancelPut(const InfoHash& h, Value::Id id);
+ void cancelPut(const InfoHash& h, const std::shared_ptr<Value>& value);
+
+ void putSigned(InfoHash hash, std::shared_ptr<Value> value, DoneCallback cb={}, bool permanent = false);
+ void putSigned(InfoHash hash, std::shared_ptr<Value> value, DoneCallbackSimple cb, bool permanent = false) {
+ putSigned(hash, value, bindDoneCb(cb), permanent);
+ }
+
+ void putSigned(InfoHash hash, Value&& value, DoneCallback cb={}, bool permanent = false);
+ void putSigned(InfoHash hash, Value&& value, DoneCallbackSimple cb, bool permanent = false) {
+ putSigned(hash, std::forward<Value>(value), bindDoneCb(cb), permanent);
+ }
+ void putSigned(const std::string& key, Value&& value, DoneCallbackSimple cb={}, bool permanent = false);
+
+ void putEncrypted(InfoHash hash, InfoHash to, std::shared_ptr<Value> value, DoneCallback cb={}, bool permanent = false);
+ void putEncrypted(InfoHash hash, InfoHash to, std::shared_ptr<Value> value, DoneCallbackSimple cb, bool permanent = false) {
+ putEncrypted(hash, to, value, bindDoneCb(cb), permanent);
+ }
+
+ void putEncrypted(InfoHash hash, InfoHash to, Value&& value, DoneCallback cb={}, bool permanent = false);
+ void putEncrypted(InfoHash hash, InfoHash to, Value&& value, DoneCallbackSimple cb, bool permanent = false) {
+ putEncrypted(hash, to, std::forward<Value>(value), bindDoneCb(cb), permanent);
+ }
+ void putEncrypted(const std::string& key, InfoHash to, Value&& value, DoneCallback cb={}, bool permanent = false);
+
+ void putEncrypted(InfoHash hash, const std::shared_ptr<crypto::PublicKey>& to, std::shared_ptr<Value> value, DoneCallback cb={}, bool permanent = false);
+ void putEncrypted(InfoHash hash, const std::shared_ptr<crypto::PublicKey>& to, std::shared_ptr<Value> value, DoneCallbackSimple cb, bool permanent = false) {
+ putEncrypted(hash, to, value, bindDoneCb(cb), permanent);
+ }
+
+ void putEncrypted(InfoHash hash, const std::shared_ptr<crypto::PublicKey>& to, Value&& value, DoneCallback cb={}, bool permanent = false);
+ void putEncrypted(InfoHash hash, const std::shared_ptr<crypto::PublicKey>& to, Value&& value, DoneCallbackSimple cb, bool permanent = false) {
+ putEncrypted(hash, to, std::forward<Value>(value), bindDoneCb(cb), permanent);
+ }
+
+
+ /**
+ * Insert known nodes to the routing table, without necessarly ping them.
+ * Usefull to restart a node and get things running fast without putting load on the network.
+ */
+ void bootstrap(std::vector<SockAddr> nodes, DoneCallbackSimple cb={});
+ void bootstrap(SockAddr addr, DoneCallbackSimple cb={});
+
+ /**
+ * Insert known nodes to the routing table, without necessarly ping them.
+ * Usefull to restart a node and get things running fast without putting load on the network.
+ */
+ void bootstrap(std::vector<NodeExport> nodes);
+
+ /**
+ * Add host:service to bootstrap nodes, and ping this node.
+ * DNS resolution is performed asynchronously.
+ * When disconnected, all bootstrap nodes added with this method will be tried regularly until connection
+ * to the DHT network is established.
+ */
+ void bootstrap(const std::string& host, const std::string& service);
+ void bootstrap(const std::string& hostService);
+
+ /**
+ * Insert known nodes to the routing table, without necessarly ping them.
+ * Usefull to restart a node and get things running fast without putting load on the network.
+ */
+ void bootstrap(const InfoHash& id, const SockAddr& address);
+
+ /**
+ * Clear the list of bootstrap added using bootstrap(const std::string&, const std::string&).
+ */
+ void clearBootstrap();
+
+ /**
+ * Inform the DHT of lower-layer connectivity changes.
+ * This will cause the DHT to assume an IP address change.
+ * The DHT will recontact neighbor nodes, re-register for listen ops etc.
+ */
+ void connectivityChanged();
+
+ void dumpTables() const;
+
+ /**
+ * Get the public key fingerprint if an identity is used with this node, 0 otherwise.
+ */
+ InfoHash getId() const;
+ std::shared_ptr<crypto::PublicKey> getPublicKey() const;
+
+ /**
+ * Get the ID of the DHT node.
+ */
+ InfoHash getNodeId() const;
+
+ /**
+ * Returns the currently bound address.
+ * @param f: address family of the bound address to retreive.
+ */
+ SockAddr getBound(sa_family_t f = AF_INET) const;
+
+ /**
+ * Returns the currently bound port, in host byte order.
+ * @param f: address family of the bound port to retreive.
+ */
+ in_port_t getBoundPort(sa_family_t f = AF_INET) const;
+
+ std::pair<size_t, size_t> getStoreSize() const;
+
+ void getStorageLimit() const;
+ void setStorageLimit(size_t limit = DEFAULT_STORAGE_LIMIT);
+
+ std::vector<NodeExport> exportNodes() const;
+
+ std::vector<ValuesExport> exportValues() const;
+
+ void setLogger(const Sp<Logger>& logger = {});
+ void setLogger(const Logger& logger) {
+ setLogger(std::make_shared<Logger>(logger));
+ }
+
+ /**
+ * Only print logs related to the given InfoHash (if given), or disable filter (if zeroes).
+ */
+ void setLogFilter(const InfoHash& f = {});
+
+ void registerType(const ValueType& type);
+
+ void importValues(const std::vector<ValuesExport>& values);
+
+ bool isRunning() const {
+ return running != State::Idle;
+ }
+
+ NodeStats getNodesStats(sa_family_t af) const;
+ unsigned getNodesStats(sa_family_t af, unsigned *good_return, unsigned *dubious_return, unsigned *cached_return, unsigned *incoming_return) const;
+ NodeInfo getNodeInfo() const;
+ void getNodeInfo(std::function<void(std::shared_ptr<NodeInfo>)>);
+
+ std::vector<unsigned> getNodeMessageStats(bool in = false) const;
+ std::string getStorageLog() const;
+ std::string getStorageLog(const InfoHash&) const;
+ std::string getRoutingTablesLog(sa_family_t af) const;
+ std::string getSearchesLog(sa_family_t af = AF_UNSPEC) const;
+ std::string getSearchLog(const InfoHash&, sa_family_t af = AF_UNSPEC) const;
+ std::vector<SockAddr> getPublicAddress(sa_family_t af = AF_UNSPEC) const;
+ std::vector<std::string> getPublicAddressStr(sa_family_t af = AF_UNSPEC) const;
+ void getPublicAddress(std::function<void(std::vector<SockAddr>&&)>, sa_family_t af = AF_UNSPEC);
+
+ // securedht methods
+
+ void findCertificate(InfoHash hash, std::function<void(const std::shared_ptr<crypto::Certificate>&)>);
+ void registerCertificate(std::shared_ptr<crypto::Certificate> cert);
+ void setLocalCertificateStore(CertificateStoreQuery&& query_method);
+
+ /**
+ * @param port: Local port to bind. Both IPv4 and IPv6 will be tried (ANY).
+ * @param identity: RSA key pair to use for cryptographic operations.
+ * @param threaded: If false, ::loop() must be called periodically. Otherwise a thread is launched.
+ * @param cb: Optional callback to receive general state information.
+ */
+ void run(in_port_t port = dht::net::DHT_DEFAULT_PORT, const crypto::Identity& identity = {}, bool threaded = true, NetId network = 0) {
+ Config config;
+ config.dht_config.node_config.network = network;
+ config.dht_config.id = identity;
+ config.threaded = threaded;
+ run(port, config);
+ }
+ void run(in_port_t port, Config& config, Context&& context = {});
+
+ /**
+ * Same as @run(sockaddr_in, sockaddr_in6, Identity, bool, StatusCallback), but with string IP addresses and service (port).
+ */
+ void run(const char* ip4, const char* ip6, const char* service, Config& config, Context&& context = {});
+
+ void run(const Config& config, Context&& context);
+
+ void setOnStatusChanged(StatusCallback&& cb) {
+ statusCb = std::move(cb);
+ }
+
+ /**
+ * In non-threaded mode, the user should call this method
+ * regularly and everytime a new packet is received.
+ * @return the next op
+ */
+ time_point loop() {
+ std::lock_guard<std::mutex> lck(dht_mtx);
+ return loop_();
+ }
+
+ /**
+ * Gracefuly disconnect from network.
+ */
+ void shutdown(ShutdownCallback cb = {}, bool stop = false);
+
+ /**
+ * Quit and wait for all threads to terminate.
+ * No callbacks will be called after this method returns.
+ * All internal state will be lost. The DHT can then be run again with @run().
+ */
+ void join();
+
+ std::shared_ptr<PeerDiscovery> getPeerDiscovery() const { return peerDiscovery_; };
+
+ void setProxyServer(const std::string& proxy, const std::string& pushNodeId = "");
+
+ /**
+ * Start or stop the proxy
+ * @param proxify if we want to use the proxy
+ */
+ void enableProxy(bool proxify);
+
+ /* Push notification methods */
+
+ /**
+ * Updates the push notification device token
+ */
+ void setPushNotificationToken(const std::string& token);
+
+ /**
+ * Sets the push notification topic
+ */
+ void setPushNotificationTopic(const std::string& topic);
+
+ /**
+ * Sets the push notification platform
+ */
+ void setPushNotificationPlatform(const std::string& platform);
+
+ /**
+ * Insert a push notification to process for OpenDHT
+ */
+ void pushNotificationReceived(const std::map<std::string, std::string>& data);
+
+ /* Proxy server mothods */
+ void forwardAllMessages(bool forward);
+
+private:
+ enum class State {
+ Idle,
+ Running,
+ Stopping
+ };
+
+ time_point loop_();
+
+ NodeStatus getStatus() const {
+ return std::max(status4, status6);
+ }
+
+ bool checkShutdown();
+ void opEnded();
+ DoneCallback bindOpDoneCallback(DoneCallback&& cb);
+ DoneCallbackSimple bindOpDoneCallback(DoneCallbackSimple&& cb);
+
+ /** DHT instance */
+ std::unique_ptr<SecureDht> dht_;
+
+ /** true if we are currently using a proxy */
+ std::atomic_bool use_proxy {false};
+
+ /** Current configuration */
+ Config config_;
+ IdentityAnnouncedCb identityAnnouncedCb_;
+
+ /**
+ * reset dht clients
+ */
+ void resetDht();
+
+ mutable std::mutex dht_mtx {};
+ std::thread dht_thread {};
+ std::condition_variable cv {};
+ std::mutex sock_mtx {};
+ net::PacketList rcv {};
+ decltype(rcv) rcv_free {};
+
+ std::queue<std::function<void(SecureDht&)>> pending_ops_prio {};
+ std::queue<std::function<void(SecureDht&)>> pending_ops {};
+ std::mutex storage_mtx {};
+
+ std::atomic<State> running {State::Idle};
+ std::atomic_size_t ongoing_ops {0};
+ std::vector<ShutdownCallback> shutdownCallbacks_;
+
+ NodeStatus status4 {NodeStatus::Disconnected},
+ status6 {NodeStatus::Disconnected};
+ StatusCallback statusCb {nullptr};
+
+ /** PeerDiscovery Parameters */
+ std::shared_ptr<PeerDiscovery> peerDiscovery_;
+
+ /**
+ * The Logger instance is used in enableProxy and other methods that
+ * would create instances of classes using a common logger.
+ */
+ std::shared_ptr<dht::Logger> logger_;
+};
+
+}
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ * Author: Vsevolod Ivanov <vsevolod.ivanov@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "def.h"
+#include "infohash.h"
+#include "crypto.h"
+
+// some libraries may try to redefine snprintf
+// but restinio will use it in std namespace
+#ifdef _MSC_VER
+# undef snprintf
+# define snprintf snprintf
+#endif
+
+#include <asio/ssl/context.hpp>
+#include <restinio/http_headers.hpp>
+#include <restinio/message_builders.hpp>
+
+#include <memory>
+#include <queue>
+#include <mutex>
+
+namespace Json {
+class Value;
+}
+
+extern "C" {
+struct http_parser;
+struct http_parser_settings;
+}
+
+namespace restinio {
+namespace impl {
+class tls_socket_t;
+}
+}
+
+namespace dht {
+namespace log {
+struct Logger;
+}
+
+namespace crypto {
+struct Certificate;
+}
+
+namespace http {
+
+using HandlerCb = std::function<void(const asio::error_code& ec)>;
+using BytesHandlerCb = std::function<void(const asio::error_code& ec, size_t bytes)>;
+using ConnectHandlerCb = std::function<void(const asio::error_code& ec,
+ const asio::ip::tcp::endpoint& endpoint)>;
+
+using ssl_socket_t = restinio::impl::tls_socket_t;
+using socket_t = asio::ip::tcp::socket;
+
+class OPENDHT_PUBLIC Url
+{
+public:
+ Url() = default;
+ Url(const std::string& url);
+ std::string url;
+ std::string protocol {"http"};
+ std::string host;
+ std::string service;
+ std::string target;
+ std::string query;
+ std::string fragment;
+
+ std::string toString() const;
+};
+
+class OPENDHT_PUBLIC Connection : public std::enable_shared_from_this<Connection>
+{
+public:
+ Connection(asio::io_context& ctx, const bool ssl = true, std::shared_ptr<log::Logger> l = {});
+ Connection(asio::io_context& ctx, std::shared_ptr<dht::crypto::Certificate> server_ca,
+ const dht::crypto::Identity& identity, std::shared_ptr<log::Logger> l = {});
+ ~Connection();
+
+ inline unsigned int id() const { return id_; };
+ bool is_open() const;
+ bool is_ssl() const;
+ void checkOcsp(bool check = true) { checkOcsp_ = check; }
+
+ void set_ssl_verification(const std::string& hostname, const asio::ssl::verify_mode verify_mode);
+
+ asio::streambuf& input();
+ std::istream& data() { return istream_; }
+
+ std::string read_bytes(size_t bytes = 0);
+ std::string read_until(const char delim);
+
+ void async_connect(std::vector<asio::ip::tcp::endpoint>&& endpoints, ConnectHandlerCb);
+ void async_handshake(HandlerCb cb);
+ void async_write(BytesHandlerCb cb);
+ void async_read_until(const char* delim, BytesHandlerCb cb);
+ void async_read_until(char delim, BytesHandlerCb cb);
+ void async_read(size_t bytes, BytesHandlerCb cb);
+ void async_read_some(size_t bytes, BytesHandlerCb cb);
+
+ const asio::ip::address& local_address() const;
+
+ void timeout(const std::chrono::seconds& timeout, HandlerCb cb = {});
+
+ void close();
+
+private:
+
+ template<typename T>
+ T wrapCallback(T cb) const {
+ return [t=shared_from_this(),cb=std::move(cb)](auto ...params) {
+ cb(params...);
+ };
+ }
+
+ mutable std::mutex mutex_;
+
+ unsigned int id_;
+ static std::atomic_uint ids_;
+
+ asio::io_context& ctx_;
+ std::unique_ptr<socket_t> socket_;
+ std::shared_ptr<asio::ssl::context> ssl_ctx_;
+ std::unique_ptr<ssl_socket_t> ssl_socket_;
+
+ asio::ip::tcp::endpoint endpoint_;
+
+ asio::streambuf write_buf_;
+ asio::streambuf read_buf_;
+ std::istream istream_;
+
+ asio::ip::address local_address_;
+
+ std::unique_ptr<asio::steady_timer> timeout_timer_;
+ std::shared_ptr<log::Logger> logger_;
+ bool checkOcsp_ {false};
+};
+
+/**
+ * Session value associated with a connection_id_t key.
+ */
+struct ListenerSession
+{
+ ListenerSession() = default;
+ dht::InfoHash hash;
+ std::future<size_t> token;
+ std::shared_ptr<restinio::response_builder_t<restinio::chunked_output_t>> response;
+};
+
+/* @class Resolver
+ * @brief The purpose is to only resolve once to avoid mutliple dns requests per operation.
+ */
+class OPENDHT_PUBLIC Resolver
+{
+public:
+ using ResolverCb = std::function<void(const asio::error_code& ec,
+ const std::vector<asio::ip::tcp::endpoint>& endpoints)>;
+
+ Resolver(asio::io_context& ctx, const std::string& url, std::shared_ptr<log::Logger> logger = {});
+ Resolver(asio::io_context& ctx, const std::string& host, const std::string& service,
+ const bool ssl = false, std::shared_ptr<log::Logger> logger = {});
+
+ // use already resolved endpoints with classes using this resolver
+ Resolver(asio::io_context& ctx, 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();
+
+ inline const Url& get_url() const {
+ return url_;
+ }
+
+ void add_callback(ResolverCb cb, sa_family_t family = AF_UNSPEC);
+
+ std::shared_ptr<log::Logger> getLogger() const {
+ return logger_;
+ }
+
+private:
+ void resolve(const std::string& host, const std::string& service);
+
+ mutable std::mutex mutex_;
+
+ Url url_;
+ asio::error_code ec_;
+ asio::ip::tcp::resolver resolver_;
+ std::shared_ptr<bool> destroyed_;
+ std::vector<asio::ip::tcp::endpoint> endpoints_;
+
+ bool completed_ {false};
+ std::queue<ResolverCb> cbs_;
+
+ std::shared_ptr<log::Logger> logger_;
+};
+
+class Request;
+
+struct Response
+{
+ unsigned status_code {0};
+ std::map<std::string, std::string> headers;
+ std::string body;
+ bool aborted {false};
+ std::weak_ptr<Request> request;
+};
+
+class OPENDHT_PUBLIC Request : public std::enable_shared_from_this<Request>
+{
+public:
+ enum class State {
+ CREATED,
+ SENDING,
+ HEADER_RECEIVED,
+ RECEIVING,
+ DONE
+ };
+ using OnStatusCb = std::function<void(unsigned status_code)>;
+ using OnDataCb = std::function<void(const char* at, size_t length)>;
+ using OnStateChangeCb = std::function<void(State state, const Response& response)>;
+ using OnJsonCb = std::function<void(Json::Value value, const Response& response)>;
+ using OnDoneCb = std::function<void(const Response& response)>;
+
+ // resolves implicitly
+ Request(asio::io_context& ctx, const std::string& url, const Json::Value& json, OnJsonCb jsoncb,
+ std::shared_ptr<log::Logger> logger = {});
+ Request(asio::io_context& ctx, const std::string& url, OnJsonCb jsoncb,
+ std::shared_ptr<log::Logger> logger = {});
+
+ Request(asio::io_context& ctx, const std::string& url, std::shared_ptr<log::Logger> logger = {});
+ Request(asio::io_context& ctx, const std::string& host, const std::string& service,
+ const bool ssl = false, std::shared_ptr<log::Logger> logger = {});
+ Request(asio::io_context& ctx, const std::string& url, OnDoneCb onDone, std::shared_ptr<log::Logger> logger = {});
+
+ // user defined resolver
+ Request(asio::io_context& ctx, std::shared_ptr<Resolver> resolver, sa_family_t family = AF_UNSPEC);
+ Request(asio::io_context& ctx, std::shared_ptr<Resolver> resolver, const std::string& target, sa_family_t family = AF_UNSPEC);
+
+ // user defined resolved endpoints
+ Request(asio::io_context& ctx, std::vector<asio::ip::tcp::endpoint>&& endpoints,
+ const bool ssl = false, std::shared_ptr<log::Logger> logger = {});
+
+ ~Request();
+
+ inline unsigned int id() const { return id_; };
+ void set_connection(std::shared_ptr<Connection> connection);
+ std::shared_ptr<Connection> get_connection() const;
+ inline const Url& get_url() const {
+ return resolver_->get_url();
+ };
+
+ void timeout(const std::chrono::seconds& timeout, HandlerCb cb = {}) {
+ timeout_ = timeout;
+ timeoutCb_ = cb;
+ }
+
+ /** The previous request in case of redirect following */
+ std::shared_ptr<Request> getPrevious() const {
+ return prev_.lock();
+ }
+
+ inline std::string& to_string() {
+ return request_;
+ }
+
+ void set_certificate_authority(std::shared_ptr<dht::crypto::Certificate> certificate);
+ void set_identity(const dht::crypto::Identity& identity);
+ void set_logger(std::shared_ptr<log::Logger> logger);
+
+ /**
+ * Define the HTTP header/body as per https://tools.ietf.org/html/rfc7230.
+ */
+ void set_header(restinio::http_request_header_t header);
+ void set_method(restinio::http_method_id_t method);
+ void set_target(std::string target);
+ void set_header_field(restinio::http_field_t field, std::string value);
+ void set_connection_type(restinio::http_connection_header_t connection);
+ void set_body(std::string body);
+ void set_auth(const std::string& username, const std::string& password);
+
+ void add_on_status_callback(OnStatusCb cb);
+ void add_on_body_callback(OnDataCb cb);
+ void add_on_state_change_callback(OnStateChangeCb cb);
+ void add_on_done_callback(OnDoneCb cb);
+
+ void send();
+
+ /** Send and block for response */
+ const Response& await();
+
+ /**
+ * User action to cancel the Request and call the completion callbacks.
+ */
+ void cancel();
+ void terminate(const asio::error_code& ec);
+
+private:
+ using OnCompleteCb = std::function<void()>;
+
+ struct Callbacks {
+ OnStatusCb on_status;
+ OnDataCb on_header_field;
+ OnDataCb on_header_value;
+ OnDataCb on_body;
+ OnStateChangeCb on_state_change;
+ };
+
+ static std::string getRelativePath(const Url& origin, const std::string& path);
+
+ void notify_state_change(State state);
+
+ void build();
+
+ static std::string url_encode(std::string_view value);
+
+ void init_default_headers();
+ /**
+ * Initialized and wraps the http_parser callbacks with our user defined callbacks.
+ */
+ void init_parser();
+
+ void connect(std::vector<asio::ip::tcp::endpoint>&& endpoints, HandlerCb cb = {});
+
+ void post();
+
+ void handle_request(const asio::error_code& ec);
+ void handle_response(const asio::error_code& ec, size_t bytes);
+
+ void onHeadersComplete();
+ void onBody(const char* at, size_t length);
+ void onComplete();
+
+ mutable std::mutex mutex_;
+
+ std::shared_ptr<log::Logger> logger_;
+
+ restinio::http_request_header_t header_;
+ std::map<restinio::http_field_t, std::string> headers_;
+ restinio::http_connection_header_t connection_type_ {restinio::http_connection_header_t::close};
+ std::string body_;
+
+ Callbacks cbs_;
+ State state_;
+
+ 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_;
+ asio::io_context& ctx_;
+ sa_family_t family_ = AF_UNSPEC;
+ std::shared_ptr<Connection> conn_;
+ std::shared_ptr<Resolver> resolver_;
+
+ Response response_ {};
+ std::string request_;
+ std::atomic<bool> finishing_ {false};
+ std::unique_ptr<http_parser> parser_;
+ std::unique_ptr<http_parser_settings> parser_s_;
+
+ // Next request in case of redirect following
+ std::shared_ptr<Request> next_;
+ std::weak_ptr<Request> prev_;
+ unsigned num_redirect {0};
+ bool follow_redirect {true};
+
+ HandlerCb timeoutCb_ {};
+ std::chrono::seconds timeout_ {0};
+};
+
+} // namespace http
+} // namespace dht
+
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ * Author(s) : Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ * Simon Désaulniers <simon.desaulniers@savoirfairelinux.com>
+ * Nicolas Reynaud <nicolas.reynaud@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <string>
+#include <vector>
+#include <memory>
+#include <map>
+#include <functional>
+#include <stdexcept>
+#include <bitset>
+#include <iostream>
+#include <sstream>
+
+#include "../value.h"
+#include "../dhtrunner.h"
+#include "../infohash.h"
+
+namespace dht {
+namespace indexation {
+
+/*!
+ * @class Prefix
+ * @brief A blob structure which prefixes a Key in the PHT.
+ * @details
+ * Since the PHT structure is a "trie", every node in this structure have a
+ * label which is defined by the path from the root of the trie to the node. If
+ * the node in question is a leaf, *the label is a prefix of all the keys
+ * contained in the leaf*.
+ */
+struct OPENDHT_PUBLIC Prefix {
+ Prefix() {}
+ Prefix(InfoHash h) : size_(h.size() * 8), content_(h.begin(), h.end()) { }
+ Prefix(const Blob& d, const Blob& f={}) : size_(d.size()*8), flags_(f), content_(d) { }
+
+ Prefix(const Prefix& p, size_t first) :
+ size_(std::min(first, p.content_.size()*8)),
+ content_(Blob(p.content_.begin(), p.content_.begin()+size_/8))
+ {
+
+ auto rem = size_ % 8;
+ if ( not p.flags_.empty() ) {
+ flags_ = Blob(p.flags_.begin(), p.flags_.begin()+size_/8);
+ if (rem)
+ flags_.push_back(p.flags_[size_/8] & (0xFF << (8 - rem)));
+ }
+
+ if (rem)
+ content_.push_back(p.content_[size_/8] & (0xFF << (8 - rem)));
+ }
+
+ /**
+ * Get a sub prefix of the Prefix
+ *
+ * @param len lenght of the prefix to get, could be negative
+ * if len is negativ then you will get the prefix
+ * of size of the previous prefix minus len
+ *
+ * @return Sub-prefix of size len or if len is negative sub-prefix of size
+ * of prefix minus len
+ *
+ * @throw out_of_range if len is larger than size of the content
+ */
+ Prefix getPrefix(ssize_t len) const {
+ if ((size_t)std::abs(len) >= content_.size() * 8)
+ throw std::out_of_range("len larger than prefix size.");
+ if (len < 0)
+ len += size_;
+
+ return Prefix(*this, len);
+ }
+
+ /**
+ * Flags are considered as active if flag is empty or if the flag
+ * at pos 'pos' is active
+ee *
+ * @see isActiveBit in private function
+ */
+ bool isFlagActive(size_t pos) const {
+ return flags_.empty() or isActiveBit(flags_, pos);
+ }
+
+ /**
+ * @see isActiveBit in private function
+ */
+ bool isContentBitActive(size_t pos) const {
+ return isActiveBit(content_, pos);
+ }
+
+ Prefix getFullSize() { return Prefix(*this, content_.size()*8); }
+
+ /**
+ * This methods gets the prefix of its sibling in the PHT structure.
+ *
+ * @return The prefix of this sibling.
+ */
+ Prefix getSibling() const {
+ Prefix copy = *this;
+ if ( size_ )
+ copy.swapContentBit(size_ - 1);
+
+ return copy;
+ }
+
+ InfoHash hash() const {
+ Blob copy(content_);
+ copy.push_back(size_);
+ return InfoHash::get(copy);
+ }
+
+ /**
+ * This method count total of bit in common between 2 prefix
+ *
+ * @param p1 first prefix to compared
+ * @param p2 second prefix to compared
+ * @return Lenght of the larger common prefix between both
+ */
+ static inline unsigned commonBits(const Prefix& p1, const Prefix& p2) {
+ unsigned i, j;
+ uint8_t x;
+ auto longest_prefix_size = std::min(p1.size_, p2.size_);
+
+ for (i = 0; i < longest_prefix_size; i++) {
+ if (p1.content_.data()[i] != p2.content_.data()[i]
+ or not p1.isFlagActive(i)
+ or not p2.isFlagActive(i) ) {
+
+ break;
+ }
+ }
+
+ 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;
+ }
+
+ /**
+ * @see doc of swap private function
+ */
+ void swapContentBit(size_t bit) {
+ swapBit(content_, bit);
+ }
+
+ /**
+ * @see doc of swap private function
+ */
+ void swapFlagBit(size_t bit) {
+ swapBit(flags_, bit);
+ }
+
+ /**
+ * @see doc of addPadding private function
+ */
+ void addPaddingContent(size_t size) {
+ content_ = addPadding(content_, size);
+ }
+
+ void updateFlags() {
+ /* Fill first known bit */
+ auto csize = size_ - flags_.size() * 8;
+ while(csize >= 8) {
+ flags_.push_back(0xFF);
+ csize -= 8;
+ }
+
+ /* if needed fill remaining bit */
+ if ( csize )
+ flags_.push_back(0xFF << (8 - csize));
+
+ /* Complet vector space missing */
+ for ( auto i = flags_.size(); i < content_.size(); i++ )
+ flags_.push_back(0xFF);
+ }
+
+ std::string toString() const;
+
+ size_t size_ {0};
+
+ /* Will contain flags according to content_.
+ If flags_[i] == 0, then content_[i] is unknown
+ else if flags_[i] == 1, then content_[i] is known */
+ Blob flags_ {};
+ Blob content_ {};
+
+private:
+
+ /**
+ * Add a padding to the input blob
+ *
+ * @param toP : Prefix where to add a padding
+ * @param size : Final size of the prefix with padding
+ *
+ * @return Copy of the input Blob but with a padding
+ */
+ Blob addPadding(Blob toP, size_t size) {
+ Blob copy = toP;
+ for ( auto i = copy.size(); i < size; i++ )
+ copy.push_back(0);
+
+ swapBit(copy, size_ + 1);
+ return copy;
+ }
+
+ /**
+ * Check if the bit a pos 'pos' is active, i.e. equal to 1
+ *
+ * @param b : Blob to check
+ * @param pos : Position to check
+ *
+ * @return true if the bit is equal to 1, false otherwise
+ *
+ * @throw out_of_range if bit is superior to blob size * 8
+ */
+ bool isActiveBit(const Blob &b, size_t pos) const {
+ if ( pos >= content_.size() * 8 )
+ throw std::out_of_range("Can't detect active bit at pos, pos larger than prefix size or empty prefix");
+
+ return ((b[pos / 8] >> (7 - (pos % 8)) ) & 1) == 1;
+ }
+
+ /**
+ * Swap bit at position bit [from 0 to 1 and vice-versa]
+ *
+ * @param b : Blob to swap
+ * @param bit : Bit to swap on b
+ *
+ * @return the input prefix with the bit at pos 'bit' swapped
+ *
+ * @throw out_of_range if bit is superior to blob size * 8
+ */
+ void swapBit(Blob &b, size_t bit) {
+ if ( bit >= b.size() * 8 )
+ throw std::out_of_range("bit larger than prefix size.");
+
+ size_t offset_bit = (8 - bit) % 8;
+ b[bit / 8] ^= (1 << offset_bit);
+ }
+};
+
+using Value = std::pair<InfoHash, dht::Value::Id>;
+struct OPENDHT_PUBLIC IndexEntry : public dht::Value::Serializable<IndexEntry> {
+ static const ValueType TYPE;
+
+ virtual void unpackValue(const dht::Value& v) {
+ Serializable<IndexEntry>::unpackValue(v);
+ name = v.user_type;
+ }
+
+ virtual dht::Value packValue() const {
+ auto pack = Serializable<IndexEntry>::packValue();
+ pack.user_type = name;
+ return pack;
+ }
+
+ Blob prefix;
+ Value value;
+ std::string name;
+ MSGPACK_DEFINE_MAP(prefix, value)
+};
+
+class OPENDHT_PUBLIC Pht {
+ static constexpr const char* INVALID_KEY = "Key does not match the PHT key spec.";
+
+ /* Prefixes the user_type for all dht values put on the DHT */
+ static constexpr const char* INDEX_PREFIX = "index.pht.";
+
+public:
+
+ /* This is the maximum number of entries per node. This parameter is
+ * critical and influences the traffic a lot during a lookup operation.
+ */
+ static constexpr const size_t MAX_NODE_ENTRY_COUNT {16};
+
+ /* A key for a an index entry */
+ using Key = std::map<std::string, Blob>;
+
+ /* Specifications of the keys. It defines the number, the length and the
+ * serialization order of fields. */
+ using KeySpec = std::map<std::string, size_t>;
+ using LookupCallback = std::function<void(std::vector<std::shared_ptr<Value>>& values, const Prefix& p)>;
+
+ typedef void (*LookupCallbackRaw)(std::vector<std::shared_ptr<Value>>* values, Prefix* p, void *user_data);
+ static LookupCallback
+ bindLookupCb(LookupCallbackRaw raw_cb, void* user_data) {
+ if (not raw_cb) return {};
+ return [=](std::vector<std::shared_ptr<Value>>& values, const Prefix& p) {
+ raw_cb((std::vector<std::shared_ptr<Value>>*) &values, (Prefix*) &p, user_data);
+ };
+ }
+ using LookupCallbackSimple = std::function<void(std::vector<std::shared_ptr<Value>>& values)>;
+ typedef void (*LookupCallbackSimpleRaw)(std::vector<std::shared_ptr<Value>>* values, void *user_data);
+ static LookupCallbackSimple
+ bindLookupCbSimple(LookupCallbackSimpleRaw raw_cb, void* user_data) {
+ if (not raw_cb) return {};
+ return [=](std::vector<std::shared_ptr<Value>>& values) {
+ raw_cb((std::vector<std::shared_ptr<Value>>*) &values, user_data);
+ };
+ }
+
+ Pht(std::string name, KeySpec k_spec, std::shared_ptr<DhtRunner> dht)
+ : name_(INDEX_PREFIX + name), canary_(name_ + ".canary"), keySpec_(k_spec), dht_(dht) {}
+
+ virtual ~Pht () { }
+
+ /**
+ * Lookup a key for a value.
+ */
+ void lookup(Key k, LookupCallback cb = {}, DoneCallbackSimple done_cb = {}, bool exact_match = true);
+ void lookup(Key k, LookupCallbackSimple cb = {}, DoneCallbackSimple done_cb = {}, bool exact_match = true)
+ {
+ lookup(k, [cb=std::move(cb)](std::vector<std::shared_ptr<Value>>& values, Prefix) { cb(values); }, done_cb, exact_match);
+ }
+
+ /**
+ * Wrapper function which call the private one.
+ *
+ * @param k : Key to insert [i.e map of string, blob]
+ * @param v : Value to insert
+ * @param done_cb : Callbakc which going to be call when all the insert is done
+ */
+ void insert(Key k, Value v, DoneCallbackSimple done_cb = {}) {
+ Prefix p = linearize(k);
+
+ auto lo = std::make_shared<int>(0);
+ auto hi = std::make_shared<int>(p.size_);
+
+ IndexEntry entry;
+ entry.value = v;
+ entry.prefix = p.content_;
+ entry.name = name_;
+
+ Pht::insert(p, entry, lo, hi, clock::now(), true, done_cb);
+ }
+
+private:
+
+ /**
+ * Insert function which really insert onto the pht
+ *
+ * @param kp : Prefix to insert (linearize the the key)
+ * @param entry : Entry created from the value
+ * @param lo : Lowest point to start in the prefix
+ * @param hi : Highest point to end in the prefix
+ * @param time_p : Timepoint to use for the insertion into the dht (must be < now)
+ * @param check_split : If this flag is true then the algoritm will not use the merge algorithm
+ * @param done_cb : Callback to call when the insert is done
+ */
+
+ void insert(const Prefix& kp, IndexEntry entry, std::shared_ptr<int> lo, std::shared_ptr<int> hi, time_point time_p,
+ bool check_split, DoneCallbackSimple done_cb = {});
+
+ class Cache {
+ public:
+ /**
+ * Insert all needed node into the tree according to a prefix
+ * @param p : Prefix that we need to insert
+ */
+ void insert(const Prefix& p);
+
+ /**
+ * Lookup into the tree to return the maximum prefix length in the cache tree
+ *
+ * @param p : Prefix that we are looking for
+ *
+ * @return : The size of the longest prefix known in the cache between 0 and p.size_
+ */
+
+ int lookup(const Prefix& p);
+
+ private:
+ static constexpr const size_t MAX_ELEMENT {1024};
+ static constexpr const std::chrono::minutes NODE_EXPIRE_TIME {5};
+
+ struct Node {
+ time_point last_reply; /* Made the assocation between leaves and leaves multimap */
+ std::shared_ptr<Node> parent; /* Share_ptr to the parent, it allow the self destruction of tree */
+ std::weak_ptr<Node> left_child; /* Left child, for bit equal to 1 */
+ std::weak_ptr<Node> right_child; /* Right child, for bit equal to 0 */
+ };
+
+ std::weak_ptr<Node> root_; /* Root of the tree */
+
+ /**
+ * This mutlimap contains all prefix insert in the tree in time order
+ * We could then delete the last one if there is too much node
+ * The tree will self destroy is branch ( thanks to share_ptr )
+ */
+ std::multimap<time_point, std::shared_ptr<Node>> leaves_;
+ };
+
+ /* Callback used for insert value by using the pht */
+ using RealInsertCallback = std::function<void(const Prefix& p, IndexEntry entry)>;
+ using LookupCallbackWrapper = std::function<void(std::vector<std::shared_ptr<IndexEntry>>& values, const Prefix& p)>;
+
+ /**
+ * Performs a step in the lookup operation. Each steps are performed
+ * asynchronously.
+ *
+ * @param k : Prefix on which the lookup is performed
+ * @param lo : lowest bound on the prefix (where to start)
+ * @param hi : highest bound on the prefix (where to stop)
+ * @param vals : Shared ptr to a vector of IndexEntry (going to contains all values found)
+ * @param cb : Callback to use at the end of the lookupStep (call on the value of vals)
+ * @param done_cb : Callback at the end of the lookupStep
+ * @param max_common_prefix_len: used in the inexacte lookup match case, indicate the longest common prefix found
+ * @param start : If start is set then lo and hi will be ignore for the first step, if the step fail lo and hi will be used
+ * @param all_values : If all value is true then all value met during the lookupstep will be in the vector vals
+ */
+ void lookupStep(Prefix k, std::shared_ptr<int> lo, std::shared_ptr<int> hi,
+ std::shared_ptr<std::vector<std::shared_ptr<IndexEntry>>> vals,
+ LookupCallbackWrapper cb, DoneCallbackSimple done_cb,
+ std::shared_ptr<unsigned> max_common_prefix_len,
+ int start = -1, bool all_values = false);
+
+ /**
+ * Apply the zcurve algorithm on the list of input prefix
+ *
+ * @param all_prefix : Vector of prefix to interleave
+ *
+ * @return The output prefix where all flags and content are interleaves
+ */
+ Prefix zcurve(const std::vector<Prefix>& all_prefix) const;
+
+ /**
+ * Linearizes the key into a unidimensional key. A pht only takes
+ * unidimensional key.
+ *
+ * @param Key The initial key.
+ *
+ * @return the prefix of the linearized key.
+ */
+ virtual Prefix linearize(Key k) const;
+
+ /**
+ * Looking where to put the data cause if there is free space on the node
+ * above then this node will became the real leave.
+ *
+ * @param p Share_ptr on the Prefix to check
+ * @param entry The entry to put at the prefix p
+ * @param end_cb Callback to use at the end of counting
+ */
+ void getRealPrefix(const std::shared_ptr<Prefix>& p, IndexEntry entry, RealInsertCallback end_cb );
+
+ /**
+ * Looking where to put the data cause if there is free space on the node
+ * above then this node will became the real leave.
+ *
+ * @param p Share_ptr on the Prefix to check
+ * @param entry The entry to put at the prefix p
+ * @param end_cb Callback to use at the end of counting
+ */
+ void checkPhtUpdate(Prefix p, IndexEntry entry, time_point time_p);
+
+ /**
+ * Search for the split location by comparing 'compared' to all values in vals.
+ *
+ * @param compared : Value which going to be compared
+ * @param vals : The vector of values to compare with comapred
+ * @return position compared diverge from all others
+ */
+ static size_t findSplitLocation(const Prefix& compared, const std::vector<std::shared_ptr<IndexEntry>>& vals) {
+ for ( size_t i = 0; i < compared.content_.size() * 8 - 1; i++ )
+ for ( auto const& v : vals)
+ if ( Prefix(v->prefix).isContentBitActive(i) != compared.isContentBitActive(i) )
+ return i + 1;
+ return compared.content_.size() * 8 - 1;
+ }
+
+ /**
+ * Put canary from the split point until the last known canary and add the prefix at the good place
+ *
+ * @param insert : Prefix to insertm but also prefix which going to check where we need to split
+ * @param vals : Vector of vals for the comparaison
+ * @param entry : Entry to put on the pht
+ * @param end_cb : Callback to apply to the insert prefi (here does the insert)
+ */
+ void split(const Prefix& insert, const std::vector<std::shared_ptr<IndexEntry>>& vals, IndexEntry entry, RealInsertCallback end_cb);
+
+ /**
+ * Tells if the key is valid according to the key spec.
+ */
+ bool validKey(const Key& k) const {
+ return k.size() == keySpec_.size() and
+ std::equal(k.begin(), k.end(), keySpec_.begin(),
+ [&](const Key::value_type& key, const KeySpec::value_type& key_spec) {
+ return key.first == key_spec.first and key.second.size() <= key_spec.second;
+ }
+ );
+ }
+
+ /**
+ * Updates the canary token on the node responsible for the specified
+ * Prefix.
+ */
+ void updateCanary(Prefix p);
+
+ const std::string name_;
+ const std::string canary_;
+ const KeySpec keySpec_;
+ Cache cache_;
+ std::shared_ptr<DhtRunner> dht_;
+};
+
+} /* indexation */
+} /* dht */
+
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ * Author : Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "def.h"
+#include "rng.h"
+
+#include <msgpack.hpp>
+#include <fmt/core.h>
+
+#ifndef _WIN32
+#include <netinet/in.h>
+#include <netdb.h>
+#ifdef __ANDROID__
+typedef uint16_t in_port_t;
+#endif
+#else
+#include <iso646.h>
+#include <ws2tcpip.h>
+typedef uint16_t sa_family_t;
+typedef uint16_t in_port_t;
+#endif
+
+#include <iostream>
+#include <iomanip>
+#include <array>
+#include <vector>
+#include <string_view>
+#include <algorithm>
+#include <stdexcept>
+#include <sstream>
+
+#include <cstring>
+#include <cstddef>
+
+namespace dht {
+
+using byte = uint8_t;
+
+namespace crypto {
+ OPENDHT_PUBLIC void hash(const uint8_t* data, size_t data_length, uint8_t* hash, size_t hash_length);
+}
+
+/**
+ * Represents an Hash,
+ * a byte array of N bytes.
+ * Hashes identify nodes and values in the Dht.
+ */
+template <size_t N>
+class OPENDHT_PUBLIC Hash {
+public:
+ using T = std::array<uint8_t, N>;
+ typedef typename T::iterator iterator;
+ typedef typename T::const_iterator const_iterator;
+
+ Hash() noexcept {
+ data_.fill(0);
+ }
+ Hash(const uint8_t* h, size_t data_len) {
+ if (data_len < N)
+ data_.fill(0);
+ else
+ std::copy_n(h, N, data_.begin());
+ }
+ /**
+ * Constructor from an hexadecimal string (without "0x").
+ * hex must be at least 2.HASH_LEN characters long.
+ * If too long, only the first 2.HASH_LEN characters are read.
+ */
+ explicit Hash(std::string_view hex) {
+ if (hex.size() < 2*N)
+ data_.fill(0);
+ else
+ fromString(hex.data());
+ }
+
+ Hash(const msgpack::object& o) {
+ msgpack_unpack(o);
+ }
+
+ static constexpr size_t size() noexcept { return N; }
+ const uint8_t* data() const { return data_.data(); }
+ uint8_t* data() { return data_.data(); }
+ iterator begin() { return data_.begin(); }
+ const_iterator cbegin() const { return data_.cbegin(); }
+ iterator end() { return data_.end(); }
+ const_iterator cend() const { return data_.cend(); }
+
+ static constexpr inline Hash zero() noexcept { return Hash{}; }
+
+ bool operator==(const Hash& h) const {
+ return data_ == h.data_;
+ }
+ bool operator!=(const Hash& h) const { return !(*this == h); }
+
+ bool operator<(const Hash& o) const {
+ for(unsigned i = 0; i < N; i++) {
+ if(data_[i] != o.data_[i])
+ return data_[i] < o.data_[i];
+ }
+ return false;
+ }
+
+ Hash operator^(const Hash& o) const {
+ Hash result;
+ for(auto i = 0u; i < N; i++) {
+ result[i] = data_[i] ^ o.data_[i];
+ }
+ return result;
+ }
+
+ explicit operator bool() const {
+ auto a = reinterpret_cast<const uint32_t*>(data_.data());
+ auto b = reinterpret_cast<const uint32_t*>(data_.data() + N);
+ for (; a != b; a++) {
+ if (*a)
+ return true;
+ }
+ return false;
+ }
+
+ uint8_t& operator[](size_t index) { return data_[index]; }
+ const uint8_t& operator[](size_t index) const { return data_[index]; }
+
+ /**
+ * Find the lowest 1 bit in an id.
+ * Result will allways be lower than 8*N
+ */
+ inline int lowbit() const {
+ int i, j;
+ for(i = N-1; i >= 0; i--)
+ if(data_[i] != 0)
+ break;
+ if(i < 0)
+ return -1;
+ for(j = 7; j >= 0; j--)
+ if((data_[i] & (0x80 >> j)) != 0)
+ break;
+ return 8 * i + j;
+ }
+
+ static inline int cmp(const Hash& id1, const Hash& id2) {
+ return std::memcmp(id1.data_.data(), id2.data_.data(), N);
+ }
+
+ /** Find how many bits two ids have in common. */
+ static inline unsigned
+ commonBits(const Hash& id1, const Hash& id2)
+ {
+ unsigned i, j;
+ uint8_t x;
+ for(i = 0; i < N; i++) {
+ if(id1.data_[i] != id2.data_[i])
+ break;
+ }
+
+ if(i == N)
+ return 8*N;
+
+ x = id1.data_[i] ^ id2.data_[i];
+
+ j = 0;
+ while((x & 0x80) == 0) {
+ x <<= 1;
+ j++;
+ }
+
+ return 8 * i + j;
+ }
+
+ /** Determine whether id1 or id2 is closer to this */
+ int
+ xorCmp(const Hash& id1, const Hash& id2) const
+ {
+ for (unsigned i = 0; i < N; i++) {
+ if(id1.data_[i] == id2.data_[i])
+ continue;
+ uint8_t xor1 = id1.data_[i] ^ data_[i];
+ uint8_t xor2 = id2.data_[i] ^ data_[i];
+ return (xor1 < xor2) ? -1 : 1;
+ }
+ return 0;
+ }
+
+ bool
+ getBit(unsigned nbit) const
+ {
+ auto& num = *(data_.cbegin()+(nbit/8));
+ unsigned bit = 7 - (nbit % 8);
+ return (num >> bit) & 1;
+ }
+
+ void
+ setBit(unsigned nbit, bool b)
+ {
+ auto& num = data_[nbit/8];
+ unsigned bit = 7 - (nbit % 8);
+ num ^= (-b ^ num) & (1 << bit);
+ }
+
+ double toFloat() const {
+ using D = size_t;
+ double v = 0.;
+ for (size_t i = 0; i < std::min<size_t>(N, sizeof(D)-1); i++)
+ v += *(data_.cbegin()+i) / (double)((D)1 << 8*(i+1));
+ return v;
+ }
+
+ static inline Hash get(std::string_view data) {
+ return get((const uint8_t*)data.data(), data.size());
+ }
+
+ static inline Hash get(const std::vector<uint8_t>& data) {
+ return get(data.data(), data.size());
+ }
+
+ template <size_t H>
+ static Hash get(const Hash<H>& o) {
+ return get(o.data(), o.size());
+ }
+
+ /**
+ * Computes the hash from a given data buffer of size data_len.
+ */
+ static Hash get(const uint8_t* data, size_t data_len)
+ {
+ Hash ret;
+ crypto::hash(data, data_len, ret.data(), N);
+ return ret;
+ }
+
+ static Hash getRandom();
+
+ template <typename Rd>
+ static Hash getRandom(Rd&);
+
+ template <size_t M>
+ OPENDHT_PUBLIC friend std::ostream& operator<< (std::ostream& s, const Hash<M>& h);
+
+ template <size_t M>
+ OPENDHT_PUBLIC friend std::istream& operator>> (std::istream& s, Hash<M>& h);
+
+ /** Returns view to thread-allocated memory, only valid until the next call to this function. */
+ std::string_view to_view() const { return std::string_view(to_c_str(), N*2); }
+ const char* to_c_str() const;
+
+ std::string toString() const;
+
+ template <typename Packer>
+ void msgpack_pack(Packer& pk) const
+ {
+ pk.pack_bin(N);
+ pk.pack_bin_body((char*)data_.data(), N);
+ }
+
+ void msgpack_unpack(msgpack::object o) {
+ if (o.type != msgpack::type::BIN or o.via.bin.size != N)
+ throw msgpack::type_error();
+ std::copy_n(o.via.bin.ptr, N, data_.data());
+ }
+private:
+ T data_;
+ void fromString(const char*);
+};
+
+#define HASH_LEN 20u
+using InfoHash = Hash<HASH_LEN>;
+using h256 = Hash<32>;
+using PkId = h256;
+
+template <size_t N>
+std::ostream& operator<< (std::ostream& s, const Hash<N>& h)
+{
+ s.write(h.to_c_str(), N*2);
+ return s;
+}
+
+template <size_t N>
+std::istream& operator>> (std::istream& s, Hash<N>& h)
+{
+ std::array<char, h.size()*2> dat;
+ s.exceptions(std::istream::eofbit | std::istream::failbit);
+ s.read(&(*dat.begin()), dat.size());
+ fromString(dat.data());
+ return s;
+}
+
+template <size_t N>
+void
+Hash<N>::fromString(const char* in) {
+ auto hex2bin = [](char c) -> uint8_t {
+ if (c >= 'a' and c <= 'f') return 10 + c - 'a';
+ else if (c >= 'A' and c <= 'F') return 10 + c - 'A';
+ else if (c >= '0' and c <= '9') return c - '0';
+ else throw std::domain_error("not an hex character");
+ };
+ try {
+ for (size_t i=0; i<N; i++)
+ data_[i] = (hex2bin(in[2*i]) << 4) | hex2bin(in[2*i+1]);
+ } catch (const std::domain_error&) {
+ data_.fill(0);
+ }
+}
+
+template <size_t N>
+Hash<N>
+Hash<N>::getRandom()
+{
+ Hash h;
+ crypto::random_device rdev;
+ std::uniform_int_distribution<uint32_t> rand_int;
+ auto a = reinterpret_cast<uint32_t*>(h.data());
+ auto b = reinterpret_cast<uint32_t*>(h.data() + h.size());
+ std::generate(a, b, std::bind(rand_int, std::ref(rdev)));
+ return h;
+}
+
+template <size_t N>
+template <typename Rd>
+Hash<N>
+Hash<N>::getRandom(Rd& rdev)
+{
+ Hash h;
+ std::uniform_int_distribution<uint32_t> rand_int;
+ auto a = reinterpret_cast<uint32_t*>(h.data());
+ auto b = reinterpret_cast<uint32_t*>(h.data() + h.size());
+ std::generate(a, b, std::bind(rand_int, std::ref(rdev)));
+ return h;
+}
+
+struct alignas(std::max_align_t) HexMap : public std::array<std::array<char, 2>, 256> {
+ HexMap() {
+ for (size_t i=0; i<size(); i++) {
+ auto& e = (*this)[i];
+ e[0] = hex_digits[(i >> 4) & 0x0F];
+ e[1] = hex_digits[i & 0x0F];
+ }
+ }
+private:
+ static constexpr const char* hex_digits = "0123456789abcdef";
+};
+
+OPENDHT_PUBLIC extern const HexMap hex_map;
+
+inline std::string
+toHex(const uint8_t* data, size_t size) {
+ std::string ret(size * 2, '\0');
+ for (size_t i=0; i<size; i++) {
+ auto b = ret.data()+i*2;
+ const auto& m = hex_map[data[i]];
+ *((uint16_t*)b) = *((uint16_t*)&m);
+ }
+ return ret;
+}
+
+inline std::string
+toHex(const std::vector<uint8_t>& data) {
+ return toHex(data.data(), data.size());
+}
+
+template <size_t N>
+const char*
+Hash<N>::to_c_str() const
+{
+ alignas(std::max_align_t) thread_local std::array<char, N*2+1> buf;
+ for (size_t i=0; i<N; i++) {
+ auto b = buf.data()+i*2;
+ const auto& m = hex_map[data_[i]];
+ *((uint16_t*)b) = *((uint16_t*)&m);
+ }
+ return buf.data();
+}
+
+template <size_t N>
+std::string
+Hash<N>::toString() const
+{
+ return std::string(to_c_str(), N*2);
+}
+
+}
+
+template <size_t N>
+struct fmt::formatter<dht::Hash<N>>: formatter<string_view> {
+ constexpr auto format(const dht::Hash<N>& c, format_context& ctx) const {
+ return formatter<string_view>::format(c.to_view(), ctx);
+ }
+};
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ *
+ * Author: Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "def.h"
+#include "logger.h"
+
+#include <iostream>
+#include <memory>
+
+namespace dht {
+
+class DhtRunner;
+
+/**
+ * Logging-related functions
+ */
+namespace log {
+
+OPENDHT_PUBLIC
+std::shared_ptr<Logger> getStdLogger();
+
+OPENDHT_PUBLIC
+std::shared_ptr<Logger> getFileLogger(const std::string &path);
+
+OPENDHT_PUBLIC
+std::shared_ptr<Logger> getSyslogLogger(const char* name);
+
+OPENDHT_PUBLIC void
+enableLogging(dht::DhtRunner &dht);
+
+OPENDHT_PUBLIC void
+enableFileLogging(dht::DhtRunner &dht, const std::string &path);
+
+OPENDHT_PUBLIC void
+disableLogging(dht::DhtRunner &dht);
+
+OPENDHT_PUBLIC void
+enableSyslog(dht::DhtRunner &dht, const char* name);
+
+} /* log */
+} /* dht */
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ * Author : Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "infohash.h"
+
+#include <fmt/format.h>
+#include <fmt/printf.h>
+
+#include <functional>
+#include <string_view>
+#include <cstdarg>
+
+namespace dht {
+namespace log {
+
+enum class LogLevel {
+ debug, warning, error
+};
+
+using LogMethod = std::function<void(LogLevel, std::string&&)>;
+
+struct OPENDHT_PUBLIC Logger {
+ Logger() = delete;
+ Logger(LogMethod&& l)
+ : logger(std::move(l)) {
+ if (!logger)
+ throw std::invalid_argument{"logger and loggerf must be set"};
+ }
+ void setFilter(const InfoHash& f) {
+ filter_ = f;
+ filterEnable_ = static_cast<bool>(filter_);
+ }
+ inline void log0(LogLevel level, fmt::string_view format, fmt::printf_args args) const {
+ if (not filterEnable_)
+ logger(level, fmt::vsprintf(format, args));
+ }
+ inline void log1(LogLevel level, const InfoHash& f, fmt::string_view format, fmt::printf_args args) const {
+ if (not filterEnable_ or f == filter_)
+ logger(level, fmt::vsprintf(format, args));
+ }
+ inline void log2(LogLevel level, const InfoHash& f1, const InfoHash& f2, fmt::string_view format, fmt::printf_args args) const {
+ if (not filterEnable_ or f1 == filter_ or f2 == filter_)
+ logger(level, fmt::vsprintf(format, args));
+ }
+ template<typename S, typename... Args>
+ inline void debug(S&& format, Args&&... args) const {
+ logger(LogLevel::debug, fmt::format(format, args...));
+ }
+ template<typename S, typename... Args>
+ inline void warn(S&& format, Args&&... args) const {
+ logger(LogLevel::warning, fmt::format(format, args...));
+ }
+ template<typename S, typename... Args>
+ inline void error(S&& format, Args&&... args) const {
+ logger(LogLevel::error, fmt::format(format, args...));
+ }
+ template <typename... T>
+ inline void d(fmt::format_string<T...> format, T&&... args) const {
+ log0(LogLevel::debug, format, fmt::make_printf_args(args...));
+ }
+ template <typename... T>
+ inline void d(const InfoHash& f, fmt::format_string<T...> format, T&&... args) const {
+ log1(LogLevel::debug, f, format, fmt::make_printf_args(args...));
+ }
+ template <typename... T>
+ inline void d(const InfoHash& f1, const InfoHash& f2, fmt::format_string<T...> format, T&&... args) const {
+ log2(LogLevel::debug, f1, f2, format, fmt::make_printf_args(args...));
+ }
+ template <typename... T>
+ inline void w(fmt::format_string<T...> format, T&&... args) const {
+ log0(LogLevel::warning, format, fmt::make_printf_args(args...));
+ }
+ template <typename... T>
+ inline void w(const InfoHash& f, fmt::format_string<T...> format, T&&... args) const {
+ log1(LogLevel::warning, f, format, fmt::make_printf_args(args...));
+ }
+ template <typename... T>
+ inline void w(const InfoHash& f1, const InfoHash& f2, fmt::format_string<T...> format, T&&... args) const {
+ log2(LogLevel::warning, f1, f2, format, fmt::make_printf_args(args...));
+ }
+ template <typename... T>
+ inline void e(fmt::format_string<T...> format, T&&... args) const {
+ log0(LogLevel::error, format, fmt::make_printf_args(args...));
+ }
+ template <typename... T>
+ inline void e(const InfoHash& f, fmt::format_string<T...> format, T&&... args) const {
+ log1(LogLevel::error, f, format, fmt::make_printf_args(args...));
+ }
+ template <typename... T>
+ inline void e(const InfoHash& f1, const InfoHash& f2, fmt::format_string<T...> format, T&&... args) const {
+ log2(LogLevel::error, f1, f2, format, fmt::make_printf_args(args...));
+ }
+private:
+ LogMethod logger = {};
+ bool filterEnable_ {false};
+ InfoHash filter_ {};
+};
+
+}
+using Logger = log::Logger;
+}
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ * Author(s) : Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ * Simon Désaulniers <simon.desaulniers@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "node_cache.h"
+#include "value.h"
+#include "infohash.h"
+#include "node.h"
+#include "scheduler.h"
+#include "utils.h"
+#include "rng.h"
+#include "rate_limiter.h"
+#include "logger.h"
+#include "network_utils.h"
+
+#include <vector>
+#include <string>
+#include <functional>
+#include <algorithm>
+#include <memory>
+#include <queue>
+
+namespace dht {
+namespace net {
+
+struct Request;
+struct Socket;
+struct TransId;
+
+#ifndef MSG_CONFIRM
+#define MSG_CONFIRM 0
+#endif
+
+struct NetworkConfig {
+ NetId network {0};
+ ssize_t max_req_per_sec {0};
+ ssize_t max_peer_req_per_sec {0};
+};
+
+class DhtProtocolException : public DhtException {
+public:
+ // sent to another peer (http-like).
+ static const constexpr uint16_t NON_AUTHORITATIVE_INFORMATION {203}; /* incomplete request packet. */
+ static const constexpr uint16_t UNAUTHORIZED {401}; /* wrong tokens. */
+ static const constexpr uint16_t NOT_FOUND {404}; /* storage not found */
+ // for internal use (custom).
+ static const constexpr uint16_t INVALID_TID_SIZE {421}; /* id was truncated. */
+ static const constexpr uint16_t UNKNOWN_TID {422}; /* unknown tid */
+ static const constexpr uint16_t WRONG_NODE_INFO_BUF_LEN {423}; /* node info length is wrong */
+
+ static const std::string GET_NO_INFOHASH; /* received "get" request with no infohash */
+ static const std::string LISTEN_NO_INFOHASH; /* got "listen" request without infohash */
+ static const std::string LISTEN_WRONG_TOKEN; /* wrong token in "listen" request */
+ static const std::string PUT_NO_INFOHASH; /* no infohash in "put" request */
+ static const std::string PUT_WRONG_TOKEN; /* got "put" request with wrong token */
+ static const std::string STORAGE_NOT_FOUND; /* got access request for an unknown storage */
+ static const std::string PUT_INVALID_ID; /* invalid id in "put" request */
+
+ DhtProtocolException(uint16_t code, const std::string& msg="", InfoHash failing_node_id={})
+ : DhtException(msg), msg(msg), code(code), failing_node_id(failing_node_id) {}
+
+ const std::string& getMsg() const { return msg; }
+ uint16_t getCode() const { return code; }
+ const InfoHash& getNodeId() const { return failing_node_id; }
+
+private:
+ std::string msg;
+ uint16_t code;
+ InfoHash failing_node_id;
+};
+
+struct ParsedMessage;
+
+/**
+ * Answer for a request.
+ */
+struct RequestAnswer {
+ Blob ntoken {};
+ Value::Id vid {};
+ std::vector<Sp<Value>> values {};
+ std::vector<Value::Id> refreshed_values {};
+ std::vector<Value::Id> expired_values {};
+ std::vector<Sp<FieldValueIndex>> fields {};
+ std::vector<Sp<Node>> nodes4 {};
+ std::vector<Sp<Node>> nodes6 {};
+ RequestAnswer() {}
+ RequestAnswer(ParsedMessage&& msg);
+};
+
+/*!
+ * @class NetworkEngine
+ * @brief An abstraction of communication protocol on the network.
+ * @details
+ * The NetworkEngine processes all requests to nodes by offering a public
+ * interface for handling sending and receiving packets. The following
+ * parameters specify callbacks for DHT work:
+ *
+ * @param onError callback for handling error messages.
+ * @param onNewNode callback for handling new nodes.
+ * @param onReportedAddr callback for reporting an our address as seen from the other peer.
+ * @param onPing callback for ping request.
+ * @param onFindNode callback for "find node" request.
+ * @param onGetValues callback for "get values" request.
+ * @param onListen callback for "listen" request.
+ * @param onAnnounce callback for "announce" request.
+ * @param onRefresh callback for "refresh" request.
+ */
+class NetworkEngine final
+{
+private:
+ /**
+ * Called when we receive an error message.
+ */
+ std::function<void(Sp<Request>, DhtProtocolException)> onError;
+
+ /**
+ * Called for every packets received for handling new nodes contacting us.
+ *
+ * @param node: the node
+ * @param confirm: 1 if the node sent a message, 2 if it sent us a reply.
+ */
+ std::function<void(const Sp<Node>&, int)> onNewNode;
+ /**
+ * Called when an addres is reported from a requested node.
+ *
+ * @param h: id
+ * @param saddr_len (type: socklen_t) lenght of the sockaddr struct.
+ */
+ std::function<void(const InfoHash&, const SockAddr&)> onReportedAddr;
+ /**
+ * Called on ping reception.
+ *
+ * @param node (type: Sp<Node>) the requesting node.
+ */
+ std::function<RequestAnswer(Sp<Node>)> onPing {};
+ /**
+ * Called on find node request.
+ *
+ * @param node (type: Sp<Node>) the requesting node.
+ * @param h (type: InfoHash) hash of the value of interest.
+ * @param want (type: want_t) states if nodes sent in the response are ipv4
+ * or ipv6.
+ */
+ std::function<RequestAnswer(Sp<Node>, const InfoHash&, want_t)> onFindNode {};
+ /**
+ * Called on "get values" request.
+ *
+ * @param node (type: Sp<Node>) the requesting node.
+ * @param h (type: InfoHash) hash of the value of interest.
+ * @param want (type: want_t) states if nodes sent in the response are ipv4
+ * or ipv6.
+ */
+ std::function<RequestAnswer(Sp<Node>, const InfoHash&, want_t, const Query&)> onGetValues {};
+ /**
+ * 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&,
+ const Blob&,
+ Tid,
+ const Query&,
+ int)> onListen {};
+ /**
+ * 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 Blob&,
+ const std::vector<Sp<Value>>&,
+ const time_point&)> onAnnounce {};
+ /**
+ * 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 Blob&,
+ const Value::Id&)> onRefresh {};
+
+public:
+ using RequestCb = std::function<void(const Request&, RequestAnswer&&)>;
+ using RequestErrorCb = std::function<bool(const Request&, DhtProtocolException&&)>;
+ using RequestExpiredCb = std::function<void(const Request&, bool)>;
+
+ NetworkEngine(
+ InfoHash& myid,
+ NetworkConfig config,
+ std::unique_ptr<DatagramSocket>&& sock,
+ const Sp<Logger>& log,
+ std::mt19937_64& rd,
+ Scheduler& scheduler,
+ decltype(NetworkEngine::onError)&& onError,
+ decltype(NetworkEngine::onNewNode)&& onNewNode,
+ decltype(NetworkEngine::onReportedAddr)&& onReportedAddr,
+ decltype(NetworkEngine::onPing)&& onPing,
+ decltype(NetworkEngine::onFindNode)&& onFindNode,
+ decltype(NetworkEngine::onGetValues)&& onGetValues,
+ decltype(NetworkEngine::onListen)&& onListen,
+ decltype(NetworkEngine::onAnnounce)&& onAnnounce,
+ decltype(NetworkEngine::onRefresh)&& onRefresh);
+
+ ~NetworkEngine();
+
+ net::DatagramSocket* getSocket() const { return dht_socket.get(); };
+
+ void clear();
+
+ /**
+ * Sends values (with closest nodes) to a listener.
+ *
+ * @deprecated
+ * @param sa The address of the listener.
+ * @param sslen The length of the sockaddr structure.
+ * @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.
+ * @param version If version = 1, a request will be used to answer to the listener
+ */
+ void tellListener(const Sp<Node>& n, 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, const Query& q, int version);
+
+ 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);
+
+ bool isRunning(sa_family_t af) const;
+ inline want_t want () const { return dht_socket->hasIPv4() and dht_socket->hasIPv6() ? (WANT4 | WANT6) : -1; }
+
+ void connectivityChanged(sa_family_t);
+
+ /**************
+ * Requests *
+ **************/
+
+ /**
+ * Send a "ping" request to a given node.
+ *
+ * @param n The node.
+ * @param on_done Request callback when the request is completed.
+ * @param on_expired Request callback when the request expires.
+ *
+ * @return the request with information concerning its success.
+ */
+ Sp<Request>
+ sendPing(const Sp<Node>& n, RequestCb&& on_done, RequestExpiredCb&& on_expired);
+
+ /**
+ * Send a "ping" request to a given node.
+ *
+ * @param sa The node's ip sockaddr info.
+ * @param salen The associated sockaddr struct length.
+ * @param on_done Request callback when the request is completed.
+ * @param on_expired Request callback when the request expires.
+ *
+ * @return the request with information concerning its success.
+ */
+ Sp<Request>
+ sendPing(SockAddr&& sa, RequestCb&& on_done, RequestExpiredCb&& on_expired) {
+ return sendPing(std::make_shared<Node>(InfoHash::zero(), std::move(sa), rd),
+ std::forward<RequestCb>(on_done),
+ std::forward<RequestExpiredCb>(on_expired));
+ }
+
+ /**
+ * Send a "find node" request to a given node.
+ *
+ * @param n The node.
+ * @param target The target hash.
+ * @param want Indicating wether IPv4 or IPv6 are wanted in response.
+ * Use NetworkEngine::want()
+ * @param on_done Request callback when the request is completed.
+ * @param on_expired Request callback when the request expires.
+ *
+ * @return the request with information concerning its success.
+ */
+ Sp<Request> sendFindNode(const Sp<Node>& n,
+ const InfoHash& hash,
+ want_t want = -1,
+ RequestCb&& on_done = {},
+ RequestExpiredCb&& on_expired = {});
+ /**
+ * Send a "get" request to a given node.
+ *
+ * @param n The node.
+ * @param hash The target hash.
+ * @param query The query describing filters.
+ * @param token A security token.
+ * @param want Indicating wether IPv4 or IPv6 are wanted in response.
+ * Use NetworkEngine::want()
+ * @param on_done Request callback when the request is completed.
+ * @param on_expired Request callback when the request expires.
+ *
+ * @return the request with information concerning its success.
+ */
+ Sp<Request> sendGetValues(const Sp<Node>& n,
+ const InfoHash& hash,
+ const Query& query,
+ want_t want,
+ RequestCb&& on_done,
+ RequestExpiredCb&& on_expired);
+ /**
+ * Send a "listen" request to a given node.
+ *
+ * @param n The node.
+ * @param hash The storage's hash.
+ * @param query The query describing filters.
+ * @param token A security token.
+ * @param previous The previous request "listen" sent to this node.
+ * @param socket **UNUSED** The socket for further response.
+ *
+ * For backward compatibility purpose, sendListen has to
+ * handle creation of the socket. Therefor, you cannot
+ * use openSocket yourself. TODO: Once we don't support
+ * the old "listen" negociation, sendListen shall not
+ * create the socket itself.
+ *
+ * @param on_done Request callback when the request is completed.
+ * @param on_expired Request callback when the request expires.
+ * @param socket_cb Callback to execute each time new updates arrive on
+ * the socket.
+ *
+ * @return the request with information concerning its success.
+ */
+ Sp<Request> sendListen(const Sp<Node>& n,
+ const InfoHash& hash,
+ const Query& query,
+ const Blob& token,
+ Tid socketId,
+ RequestCb&& on_done,
+ RequestExpiredCb&& on_expired);
+ /**
+ * Send a "announce" request to a given node.
+ *
+ * @param n The node.
+ * @param hash The target hash.
+ * @param created The time when the value was created (avoiding extended
+ * value lifetime)
+ * @param token A security token.
+ * @param on_done Request callback when the request is completed.
+ * @param on_expired Request callback when the request expires.
+ *
+ * @return the request with information concerning its success.
+ */
+ Sp<Request> sendAnnounceValue(const Sp<Node>& n,
+ const InfoHash& hash,
+ const Sp<Value>& v,
+ time_point created,
+ const Blob& token,
+ RequestCb&& on_done,
+ RequestExpiredCb&& on_expired);
+ /**
+ * Send a "refresh" request to a given node. Asks a node to keep the
+ * associated value Value.type.expiration more minutes in its storage.
+ *
+ * @param n The node.
+ * @param hash The target hash.
+ * @param vid The value id.
+ * @param token A security token.
+ * @param on_done Request callback when the request is completed.
+ * @param on_expired Request callback when the request expires.
+ *
+ * @return the request with information concerning its success.
+ */
+ Sp<Request> sendRefreshValue(const Sp<Node>& n,
+ const InfoHash& hash,
+ const Value::Id& vid,
+ const Blob& token,
+ RequestCb&& on_done,
+ RequestErrorCb&& on_error,
+ RequestExpiredCb&& on_expired);
+ /**
+ * Send a "update" request to a given node. Used for Listen operations
+ *
+ * @param n The node.
+ * @param hash The target hash.
+ * @param values The values.
+ * @param created Time id.
+ * @param token A security token.
+ * @param sid The socket id.
+ *
+ * @return the request with information concerning its success.
+ */
+ void sendUpdateValues(const Sp<Node>& n,
+ const InfoHash& infohash,
+ std::vector<Sp<Value>>&& values,
+ time_point created,
+ const Blob& token,
+ size_t sid);
+ Sp<Request> sendUpdateValues(const Sp<Node>& n,
+ const InfoHash& infohash,
+ std::vector<Sp<Value>>::iterator begin,
+ std::vector<Sp<Value>>::iterator end,
+ time_point created,
+ const Blob& token,
+ size_t sid);
+
+ /**
+ * Parses a message and calls appropriate callbacks.
+ *
+ * @param buf The buffer containing the binary message.
+ * @param buflen The length of the buffer.
+ * @param from The address info of the sender.
+ * @param fromlen The length of the corresponding sockaddr structure.
+ * @param now The time to adjust the clock in the network engine.
+ */
+ void processMessage(const uint8_t *buf, size_t buflen, SockAddr addr);
+
+ Sp<Node> insertNode(const InfoHash& id, const SockAddr& addr) {
+ auto n = cache.getNode(id, addr, scheduler.time(), 0);
+ onNewNode(n, 0);
+ return n;
+ }
+
+ std::vector<unsigned> getNodeMessageStats(bool in) {
+ auto& st = in ? in_stats : out_stats;
+ std::vector<unsigned> stats {st.ping, st.find, st.get, st.listen, st.put};
+ st = {};
+ return stats;
+ }
+
+ void blacklistNode(const Sp<Node>& n);
+
+ std::vector<Sp<Node>> getCachedNodes(const InfoHash& id, sa_family_t sa_f, size_t count) {
+ return cache.getCachedNodes(id, sa_f, count);
+ }
+
+ size_t getNodeCacheSize() const {
+ return cache.size();
+ }
+ size_t getNodeCacheSize(sa_family_t af) const {
+ return cache.size(af);
+ }
+
+ size_t getRateLimiterSize() const {
+ return address_rate_limiter.size();
+ }
+
+ size_t getPartialCount() const {
+ return partial_messages.size();
+ }
+
+private:
+
+ struct PartialMessage;
+
+ /***************
+ * Constants *
+ ***************/
+ /* the length of a node info buffer in ipv4 format */
+ static const constexpr size_t NODE4_INFO_BUF_LEN {HASH_LEN + sizeof(in_addr) + sizeof(in_port_t)};
+ /* the length of a node info buffer in ipv6 format */
+ static const constexpr size_t NODE6_INFO_BUF_LEN {HASH_LEN + sizeof(in6_addr) + sizeof(in_port_t)};
+ /* after a UDP reply, the period during which we tell the link layer about it */
+ static constexpr std::chrono::seconds UDP_REPLY_TIME {15};
+
+ /* Max. time to receive a full fragmented packet */
+ static constexpr std::chrono::seconds RX_MAX_PACKET_TIME {10};
+ /* Max. time between packet fragments */
+ static constexpr std::chrono::seconds RX_TIMEOUT {3};
+ /* The maximum number of nodes that we snub. There is probably little
+ reason to increase this value. */
+ static constexpr unsigned BLACKLISTED_MAX {10};
+
+ 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};
+
+ void process(std::unique_ptr<ParsedMessage>&&, 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);
+
+ /**
+ * Sends a request to a node. Request::MAX_ATTEMPT_COUNT attempts will
+ * be made before the request expires.
+ */
+ void sendRequest(const Sp<Request>& request);
+
+ struct MessageStats {
+ unsigned ping {0};
+ unsigned find {0};
+ unsigned get {0};
+ unsigned put {0};
+ unsigned listen {0};
+ unsigned refresh {0};
+ unsigned updateValue {0};
+ };
+
+
+ // basic wrapper for socket sendto function
+ int send(const SockAddr& addr, const char *buf, size_t len, bool confirmed = false);
+
+ 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;
+ std::vector<Blob> packValueHeader(msgpack::sbuffer& buf, const std::vector<Sp<Value>>& values) const {
+ return packValueHeader(buf, values.begin(), values.end());
+ }
+ void maintainRxBuffer(Tid tid);
+
+ /*************
+ * Answers *
+ *************/
+ /* answer to a ping request */
+ void sendPong(const SockAddr& addr, Tid tid);
+ /* answer to findnodes/getvalues request */
+ void sendNodesValues(const SockAddr& addr,
+ Tid tid,
+ const Blob& nodes,
+ const Blob& nodes6,
+ const std::vector<Sp<Value>>& st,
+ const Query& query,
+ const Blob& token);
+ Blob bufferNodes(sa_family_t af, const InfoHash& id, std::vector<Sp<Node>>& nodes);
+
+ 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 SockAddr& addr, Tid tid);
+ /* answer to put request */
+ void sendValueAnnounced(const SockAddr& addr, Tid, Value::Id);
+ /* answer in case of error */
+ 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;
+ const NetworkConfig config {};
+ const std::unique_ptr<DatagramSocket> dht_socket;
+ Sp<Logger> logger_;
+ std::mt19937_64& rd;
+
+ NodeCache cache;
+
+ // 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<Tid, PartialMessage> partial_messages;
+
+ MessageStats in_stats {}, out_stats {};
+ std::set<SockAddr> blacklist {};
+
+ Scheduler& scheduler;
+
+ bool logIncoming_ {false};
+};
+
+} /* namespace net */
+} /* namespace dht */
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ * Author(s) : Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+#pragma once
+
+#include "def.h"
+
+#include "sockaddr.h"
+#include "utils.h"
+#include "logger.h"
+
+#ifdef _WIN32
+#include <ws2tcpip.h>
+#include <winsock2.h>
+#else
+#include <sys/socket.h>
+#include <netinet/in.h>
+#include <unistd.h>
+#endif
+
+#include <functional>
+#include <thread>
+#include <atomic>
+#include <mutex>
+#include <list>
+
+namespace dht {
+namespace net {
+
+static const constexpr in_port_t DHT_DEFAULT_PORT = 4222;
+static const constexpr size_t RX_QUEUE_MAX_SIZE = 1024 * 64;
+static const constexpr std::chrono::milliseconds RX_QUEUE_MAX_DELAY(650);
+
+int bindSocket(const SockAddr& addr, SockAddr& bound);
+
+bool setNonblocking(int fd, bool nonblocking = true);
+
+#ifdef _WIN32
+void udpPipe(int fds[2]);
+#endif
+struct ReceivedPacket {
+ Blob data;
+ SockAddr from;
+ time_point received;
+};
+using PacketList = std::list<ReceivedPacket>;
+
+class OPENDHT_PUBLIC DatagramSocket {
+public:
+ /** A function that takes a list of new received packets and
+ * optionally returns consumed packets for recycling.
+ **/
+ using OnReceive = std::function<PacketList(PacketList&& packets)>;
+ virtual ~DatagramSocket() {};
+
+ virtual int sendTo(const SockAddr& dest, const uint8_t* data, size_t size, bool replied) = 0;
+
+ inline void setOnReceive(OnReceive&& cb) {
+ std::lock_guard<std::mutex> lk(lock);
+ rx_callback = std::move(cb);
+ }
+
+ virtual bool hasIPv4() const = 0;
+ virtual bool hasIPv6() const = 0;
+
+ SockAddr getBound(sa_family_t family = AF_UNSPEC) const {
+ std::lock_guard<std::mutex> lk(lock);
+ return getBoundRef(family);
+ }
+ in_port_t getPort(sa_family_t family = AF_UNSPEC) const {
+ std::lock_guard<std::mutex> lk(lock);
+ return getBoundRef(family).getPort();
+ }
+
+ virtual const SockAddr& getBoundRef(sa_family_t family = AF_UNSPEC) const = 0;
+
+ /** Virtual resolver mothod allows to implement custom resolver */
+ virtual std::vector<SockAddr> resolve(const std::string& host, const std::string& service = {}) {
+ return SockAddr::resolve(host, service);
+ }
+
+ virtual void stop() = 0;
+protected:
+
+ PacketList getNewPacket() {
+ PacketList pkts;
+ if (toRecycle_.empty()) {
+ pkts.emplace_back();
+ } else {
+ auto begIt = toRecycle_.begin();
+ auto begItNext = std::next(begIt);
+ pkts.splice(pkts.end(), toRecycle_, begIt, begItNext);
+ }
+ return pkts;
+ }
+
+ inline void onReceived(PacketList&& packets) {
+ std::lock_guard<std::mutex> lk(lock);
+ if (rx_callback) {
+ auto r = rx_callback(std::move(packets));
+ if (not r.empty() and toRecycle_.size() < RX_QUEUE_MAX_SIZE)
+ toRecycle_.splice(toRecycle_.end(), std::move(r));
+ }
+ }
+protected:
+ mutable std::mutex lock;
+private:
+ OnReceive rx_callback;
+ PacketList toRecycle_;
+};
+
+class OPENDHT_PUBLIC UdpSocket : public DatagramSocket {
+public:
+ UdpSocket(in_port_t port, const std::shared_ptr<Logger>& l = {});
+ UdpSocket(const SockAddr& bind4, const SockAddr& bind6, const std::shared_ptr<Logger>& l = {});
+ ~UdpSocket();
+
+ int sendTo(const SockAddr& dest, const uint8_t* data, size_t size, bool replied) override;
+
+ const SockAddr& getBoundRef(sa_family_t family = AF_UNSPEC) const override {
+ return (family == AF_INET6) ? bound6 : bound4;
+ }
+
+ bool hasIPv4() const override {
+ std::lock_guard<std::mutex> lk(lock);
+ return s4 != -1;
+ }
+ bool hasIPv6() const override {
+ std::lock_guard<std::mutex> lk(lock);
+ return s6 != -1;
+ }
+
+ void stop() override;
+private:
+ std::shared_ptr<Logger> logger;
+ int s4 {-1};
+ int s6 {-1};
+ int stopfd {-1};
+ SockAddr bound4, bound6;
+ std::thread rcv_thread {};
+ std::atomic_bool running {false};
+
+ void openSockets(const SockAddr& bind4, const SockAddr& bind6);
+};
+
+}
+}
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ * Author(s) : Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ * Simon Désaulniers <simon.desaulniers@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "infohash.h" // includes socket structures
+#include "utils.h"
+#include "sockaddr.h"
+#include "node_export.h"
+
+#include <list>
+#include <map>
+
+namespace dht {
+
+struct Node;
+namespace net {
+struct Request;
+struct Socket;
+struct RequestAnswer;
+} /* namespace net */
+
+using Tid = uint32_t;
+using SocketCb = std::function<void(const Sp<Node>&, net::RequestAnswer&&)>;
+struct Socket {
+ Socket() {}
+ Socket(SocketCb&& on_receive) :
+ on_receive(std::move(on_receive)) {}
+ SocketCb on_receive {};
+};
+
+struct Node {
+ const InfoHash id;
+
+ Node(const InfoHash& id, const SockAddr& addr, std::mt19937_64& rd, bool client=false);
+ Node(const InfoHash& id, SockAddr&& addr, std::mt19937_64& rd, bool client=false);
+ Node(const InfoHash& id, const sockaddr* sa, socklen_t salen, std::mt19937_64& rd)
+ : Node(id, SockAddr(sa, salen), rd) {}
+
+ const InfoHash& getId() const {
+ return id;
+ }
+ const SockAddr& getAddr() const { return addr; }
+ std::string getAddrStr() const {
+ return addr.toString();
+ }
+ bool isClient() const { return is_client; }
+ bool isIncoming() { return time > reply_time; }
+
+ const time_point& getTime() const { return time; }
+ const time_point& getReplyTime() const { return reply_time; }
+ void setTime(const time_point& t) { time = t; }
+
+ /**
+ * Makes notice about an additionnal authentication error with this node. Up
+ * to MAX_AUTH_ERRORS errors are accepted in order to let the node recover.
+ * Upon this limit, the node expires.
+ */
+ void authError() {
+ if (++auth_errors > MAX_AUTH_ERRORS)
+ setExpired();
+ }
+ void authSuccess() { auth_errors = 0; }
+
+ bool isExpired() const { return expired_; }
+ bool isGood(time_point now) const;
+ bool isPendingMessage() const;
+ size_t getPendingMessageCount() const;
+
+ bool isOld(const time_point& now) const {
+ return time + NODE_EXPIRE_TIME < now;
+ }
+ bool isRemovable(const time_point& now) const {
+ return isExpired() and isOld(now);
+ }
+
+ NodeExport exportNode() const {
+ NodeExport ne;
+ ne.id = id;
+ ne.addr = addr;
+ return ne;
+ }
+ sa_family_t getFamily() const { return addr.getFamily(); }
+
+ void update(const SockAddr&);
+
+ void requested(const Sp<net::Request>& req);
+ void received(time_point now, const Sp<net::Request>& req);
+ Sp<net::Request> getRequest(Tid tid);
+ void cancelRequest(const Sp<net::Request>& req);
+
+ void setExpired();
+
+ /**
+ * Opens a socket on which a node will be able allowed to write for further
+ * additionnal updates following the response to a previous request.
+ *
+ * @param node The node which will be allowed to write on this socket.
+ * @param cb The callback to execute once updates arrive on the socket.
+ *
+ * @return the socket.
+ */
+ Tid openSocket(SocketCb&& cb);
+
+ Sp<Socket> getSocket(Tid id);
+
+ /**
+ * Closes a socket so that no further data will be red on that socket.
+ *
+ * @param socket The socket to close.
+ */
+ void closeSocket(Tid id);
+
+ /**
+ * Resets the state of the node so it's not expired anymore.
+ */
+ void reset() { expired_ = false; reply_time = time_point::min(); }
+
+ /**
+ * Generates a new request id, skipping the invalid id.
+ *
+ * @return the new id.
+ */
+ Tid getNewTid() {
+ ++transaction_id;
+ return transaction_id ? transaction_id : ++transaction_id;
+ }
+
+ std::string toString() const;
+
+ OPENDHT_PUBLIC friend std::ostream& operator<< (std::ostream& s, const Node& h);
+
+ static constexpr const std::chrono::minutes NODE_GOOD_TIME {120};
+
+ /* The time after which we consider a node to be expirable. */
+ static constexpr const std::chrono::minutes NODE_EXPIRE_TIME {10};
+
+ /* Time for a request to timeout */
+ static constexpr const std::chrono::seconds MAX_RESPONSE_TIME {1};
+
+private:
+ /* Number of times we accept authentication errors from this node. */
+ static const constexpr unsigned MAX_AUTH_ERRORS {3};
+
+ SockAddr addr;
+ bool is_client {false};
+ time_point time {time_point::min()}; /* last time eared about */
+ time_point reply_time {time_point::min()}; /* time of last correct reply received */
+ unsigned auth_errors {0};
+ bool expired_ {false};
+ Tid transaction_id;
+ using TransactionDist = std::uniform_int_distribution<decltype(transaction_id)>;
+
+ std::map<Tid, Sp<net::Request>> requests_ {};
+ std::map<Tid, Sp<Socket>> sockets_;
+};
+
+}
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ * Author(s) : Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "node.h"
+
+#include <list>
+#include <memory>
+
+namespace dht {
+
+struct NodeCache {
+ size_t size(sa_family_t family) const {
+ return cache(family).count();
+ }
+ size_t size() const {
+ return size(AF_INET) + size(AF_INET6);
+ }
+
+ Sp<Node> getNode(const InfoHash& id, sa_family_t family);
+ Sp<Node> getNode(const InfoHash& id, const SockAddr&, time_point now, bool confirmed, bool client=false);
+ std::vector<Sp<Node>> getCachedNodes(const InfoHash& id, sa_family_t sa_f, size_t count) const;
+
+ /**
+ * Reset the connectivity state of every node,
+ * Giving them a new chance if they where expired.
+ * To use in case of connectivity change etc.
+ */
+ void clearBadNodes(sa_family_t family = 0);
+
+ NodeCache(std::mt19937_64& r) : rd(r) {};
+ ~NodeCache();
+
+private:
+ class NodeMap : private std::map<InfoHash, std::weak_ptr<Node>> {
+ public:
+ Sp<Node> getNode(const InfoHash& id);
+ Sp<Node> getNode(const InfoHash& id, const SockAddr&, time_point now, bool confirmed, bool client, std::mt19937_64& rd);
+ std::vector<Sp<Node>> getCachedNodes(const InfoHash& id, size_t count) const;
+ void clearBadNodes();
+ void setExpired();
+ void cleanup();
+ size_t count() const { return size(); }
+ private:
+ size_t cleanup_counter {0};
+ };
+
+ const NodeMap& cache(sa_family_t af) const { return af == AF_INET ? cache_4 : cache_6; }
+ NodeMap& cache(sa_family_t af) { return af == AF_INET ? cache_4 : cache_6; }
+ NodeMap cache_4;
+ NodeMap cache_6;
+ std::mt19937_64& rd;
+};
+
+}
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+#pragma once
+
+#include "def.h"
+#include "infohash.h"
+#include "sockaddr.h"
+
+#include <string_view>
+
+namespace dht {
+using namespace std::literals;
+
+struct OPENDHT_PUBLIC NodeExport {
+ InfoHash id;
+ SockAddr addr;
+
+ template <typename Packer>
+ void msgpack_pack(Packer& pk) const
+ {
+ pk.pack_map(2);
+ pk.pack("id"sv);
+ pk.pack(id);
+ pk.pack("addr"sv);
+ pk.pack_bin(addr.getLength());
+ pk.pack_bin_body((const char*)addr.get(), (size_t)addr.getLength());
+ }
+
+ void msgpack_unpack(msgpack::object o);
+
+ OPENDHT_PUBLIC friend std::ostream& operator<< (std::ostream& s, const NodeExport& h);
+};
+
+}
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ * Author(s) : Mingrui Zhang <mingrui.zhang@savoirfairelinux.com>
+ * Vsevolod Ivanov <vsevolod.ivanov@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "def.h"
+#include "sockaddr.h"
+#include "infohash.h"
+#include "logger.h"
+
+#include <thread>
+
+namespace asio {
+class io_context;
+}
+
+namespace dht {
+
+class OPENDHT_PUBLIC PeerDiscovery
+{
+public:
+ static constexpr in_port_t DEFAULT_PORT = 8888;
+ using ServiceDiscoveredCallback = std::function<void(msgpack::object&&, SockAddr&&)>;
+
+ PeerDiscovery(in_port_t port = DEFAULT_PORT, std::shared_ptr<asio::io_context> ioContext = {}, std::shared_ptr<Logger> logger = {});
+ ~PeerDiscovery();
+
+ /**
+ * startDiscovery - Keep Listening data from the sender until node is joinned or stop is called
+ */
+ void startDiscovery(const std::string &type, ServiceDiscoveredCallback callback);
+
+ template<typename T>
+ void startDiscovery(const std::string &type, std::function<void(T&&, SockAddr&&)> cb) {
+ startDiscovery(type, [cb](msgpack::object&& ob, SockAddr&& addr) {
+ cb(ob.as<T>(), std::move(addr));
+ });
+ }
+
+ /**
+ * startPublish - Keeping sending data until node is joinned or stop is called
+ */
+ void startPublish(const std::string &type, const msgpack::sbuffer &pack_buf);
+ void startPublish(sa_family_t domain, const std::string &type, const msgpack::sbuffer &pack_buf);
+
+ template<typename T>
+ void startPublish(const std::string &type, const T& object) {
+ msgpack::sbuffer buf;
+ msgpack::pack(buf, object);
+ startPublish(type, buf);
+ }
+
+ /**
+ * Thread Stopper
+ */
+ void stop();
+
+ /**
+ * Remove possible callBack to discovery
+ */
+ bool stopDiscovery(const std::string &type);
+
+ /**
+ * Remove different serivce message to send
+ */
+ bool stopPublish(const std::string &type);
+ bool stopPublish(sa_family_t domain, const std::string &type);
+
+ void connectivityChanged();
+
+private:
+ class DomainPeerDiscovery;
+ std::unique_ptr<DomainPeerDiscovery> peerDiscovery4_;
+ std::unique_ptr<DomainPeerDiscovery> peerDiscovery6_;
+ std::shared_ptr<asio::io_context> ioContext_;
+ std::thread ioRunnner_;
+};
+
+}
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ * Author: Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+#pragma once
+
+#include <chrono>
+
+namespace dht {
+namespace proxy {
+
+constexpr const std::chrono::hours OP_TIMEOUT {24}; // one day
+constexpr const std::chrono::hours OP_MARGIN {2}; // two hours
+constexpr const char* const HTTP_PROTO {"http://"};
+using ListenToken = uint64_t;
+
+}
+}
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ * Author : Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "utils.h"
+#include <queue>
+
+namespace dht {
+
+class RateLimiter {
+public:
+ RateLimiter(size_t quota, const duration& period = std::chrono::seconds(1))
+ : quota_(quota), period_(period) {}
+
+ /** Clear outdated records and return current quota usage */
+ size_t maintain(const time_point& now) {
+ auto limit = now - period_;
+ while (not records.empty() and records.front() < limit)
+ records.pop();
+ return records.size();
+ }
+ /** Return false if quota is reached, insert record and return true otherwise. */
+ bool limit(const time_point& now) {
+ if (quota_ == std::numeric_limits<size_t>::max())
+ return true;
+ if (maintain(now) >= quota_)
+ return false;
+ records.emplace(now);
+ return true;
+ }
+ bool empty() const {
+ return records.empty();
+ }
+private:
+ const size_t quota_;
+ const duration period_;
+ std::queue<time_point> records {};
+};
+
+}
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ * Author : Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <random>
+#include <algorithm>
+#include <functional>
+#include <thread>
+#include <stdexcept>
+
+namespace dht {
+namespace crypto {
+
+#ifndef _MSC_VER
+#ifdef _WIN32
+
+/**
+ * Hardware random number generator using Intel RDRAND/RDSEED,
+ * API-compatible with std::random_device.
+ */
+class random_device {
+public:
+ using result_type = std::random_device::result_type;
+ using pseudo_engine = std::mt19937_64;
+
+ /**
+ * Current implementation assumption : result_type must be of a size
+ * supported by Intel RDRAND/RDSEED.
+ * result_type is unsigned int so this is currently safe.
+ */
+ static_assert(
+ sizeof(result_type) == 2 ||
+ sizeof(result_type) == 4 ||
+ sizeof(result_type) == 8,
+ "result_type must be 16, 32 or 64 bits");
+
+ random_device();
+
+ result_type operator()();
+
+ static constexpr result_type min() {
+ return std::numeric_limits<result_type>::lowest();
+ }
+
+ static constexpr result_type max() {
+ return std::numeric_limits<result_type>::max();
+ }
+
+ double entropy() const {
+ if (hasRdrand() or hasRdseed())
+ return 1.;
+ return 0.;
+ }
+
+ static bool hasRdrand() {
+ static const bool hasrdrand = _hasRdrand();
+ return hasrdrand;
+ }
+
+ static bool hasRdseed() {
+ static const bool hasrdseed = _hasRdseed();
+ return hasrdseed;
+ }
+
+private:
+ random_device& operator=(random_device&) = delete;
+
+ pseudo_engine gen;
+ std::uniform_int_distribution<result_type> dis {};
+
+ static bool hasIntelCpu();
+ static bool _hasRdrand();
+ static bool _hasRdseed();
+
+ struct CPUIDinfo {
+ unsigned int EAX;
+ unsigned int EBX;
+ unsigned int ECX;
+ unsigned int EDX;
+ CPUIDinfo(const unsigned int func, const unsigned int subfunc);
+ };
+ bool rdrandStep(result_type* r);
+ bool rdrand(result_type* r);
+ bool rdseedStep(result_type* r);
+ bool rdseed(result_type* r);
+};
+
+#else
+
+using random_device = std::random_device;
+
+#endif
+#else
+using random_device = std::random_device;
+#endif
+
+template<class T = std::mt19937, std::size_t N = T::state_size>
+auto getSeededRandomEngine () -> typename std::enable_if<!!N, T>::type {
+ typename T::result_type random_data[N];
+ for (unsigned i=0; i<256; i++) {
+ try {
+ random_device source;
+ std::generate(std::begin(random_data), std::end(random_data), std::ref(source));
+ std::seed_seq seeds(std::begin(random_data), std::end(random_data));
+ T seededEngine (seeds);
+ return seededEngine;
+ } catch (...) {
+ std::this_thread::sleep_for(std::chrono::milliseconds(1));
+ }
+ }
+ throw std::runtime_error("Can't seed random engine");
+}
+
+}} // dht::crypto
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ * Author(s) : Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ * Simon Désaulniers <simon.desaulniers@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "node.h"
+
+namespace dht {
+
+static constexpr unsigned TARGET_NODES {8};
+namespace net {
+class NetworkEngine;
+}
+
+struct Bucket {
+ Bucket() : cached() {}
+ Bucket(sa_family_t af, const InfoHash& f = {}, time_point t = time_point::min())
+ : af(af), first(f), time(t), cached() {}
+ sa_family_t af {0};
+ InfoHash first {};
+ time_point time {time_point::min()}; /* time of last reply in this bucket */
+ std::list<Sp<Node>> nodes {};
+ Sp<Node> cached; /* the address of a likely candidate */
+
+ /** Return a random node in a bucket. */
+ Sp<Node> randomNode(std::mt19937_64& rd);
+
+ void sendCachedPing(net::NetworkEngine& ne);
+ void connectivityChanged() {
+ time = time_point::min();
+ for (auto& node : nodes)
+ node->setTime(time_point::min());
+ }
+};
+
+class RoutingTable : public std::list<Bucket> {
+public:
+ using std::list<Bucket>::list;
+
+ time_point grow_time {time_point::min()};
+ bool is_client {false};
+
+ InfoHash middle(const RoutingTable::const_iterator&) const;
+
+ std::vector<Sp<Node>> findClosestNodes(const InfoHash id, time_point now, size_t count = TARGET_NODES) const;
+
+ RoutingTable::iterator findBucket(const InfoHash& id);
+ RoutingTable::const_iterator findBucket(const InfoHash& id) const;
+
+ /**
+ * Return true if the id is in the bucket's range.
+ */
+ inline bool contains(const RoutingTable::const_iterator& bucket, const InfoHash& id) const {
+ return InfoHash::cmp(bucket->first, id) <= 0
+ && (std::next(bucket) == end() || InfoHash::cmp(id, std::next(bucket)->first) < 0);
+ }
+
+ /**
+ * Return true if the table has no bucket ore one empty buket.
+ */
+ inline bool isEmpty() const {
+ return empty() || (size() == 1 && front().nodes.empty());
+ }
+
+ void connectivityChanged(const time_point& now) {
+ grow_time = now;
+ for (auto& b : *this)
+ b.connectivityChanged();
+ }
+
+ bool onNewNode(const Sp<Node>& node, int comfirm, const time_point& now, const InfoHash& myid, net::NetworkEngine& ne);
+
+ /**
+ * Return a random id in the bucket's range.
+ */
+ InfoHash randomId(const RoutingTable::const_iterator& bucket, std::mt19937_64& rd) const;
+
+ unsigned depth(const RoutingTable::const_iterator& bucket) const;
+
+ /**
+ * Split a bucket in two equal parts.
+ */
+ bool split(const RoutingTable::iterator& b);
+};
+
+}
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ * Author(s) : Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ * Simon Désaulniers <simon.desaulniers@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+
+#pragma once
+
+#include "utils.h"
+
+#include <functional>
+#include <map>
+
+namespace dht {
+
+/*!
+ * @class Scheduler
+ * @brief Job scheduler
+ * @details
+ * Maintains the timings upon which to execute a job.
+ */
+class Scheduler {
+public:
+ struct Job {
+ Job(std::function<void()>&& f, time_point t) : do_(std::move(f)), t_(t) {}
+ std::function<void()> do_;
+ const time_point t_;
+ void cancel() { do_ = {}; }
+ };
+
+ /**
+ * Adds another job to the queue.
+ *
+ * @param time The time upon which the job shall be executed.
+ * @param job_func The job function to execute.
+ *
+ * @return pointer to the newly scheduled job.
+ */
+ Sp<Scheduler::Job> add(time_point t, std::function<void()>&& job_func) {
+ auto job = std::make_shared<Job>(std::move(job_func), t);
+ if (t != time_point::max())
+ timers.emplace(std::move(t), job);
+ return job;
+ }
+
+ /**
+ * Reschedules a job.
+ *
+ * @param job The job to edit.
+ * @param t The time at which the job shall be rescheduled.
+ */
+ void edit(Sp<Scheduler::Job>& job, time_point t) {
+ if (not job)
+ return;
+ // std::function move doesn't garantee to leave the object empty.
+ // Force clearing old value.
+ auto task = std::move(job->do_);
+ cancel(job);
+ job = add(t, std::move(task));
+ }
+
+ bool cancel(Sp<Scheduler::Job>& job) {
+ if (job) {
+ job->cancel();
+ for (auto r = timers.equal_range(job->t_); r.first != r.second; ++r.first) {
+ if (r.first->second == job) {
+ timers.erase(r.first);
+ job.reset();
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Runs the jobs to do up to now.
+ *
+ * @return The time for the next job to run.
+ */
+ time_point run() {
+ while (not timers.empty()) {
+ auto timer = timers.begin();
+ /*
+ * Running jobs scheduled before "now" prevents run+rescheduling
+ * loops before this method ends. It is garanteed by the fact that a
+ * job will at least be scheduled for "now" and not before.
+ */
+ if (timer->first > now)
+ break;
+
+ auto job = std::move(timer->second);
+ timers.erase(timer);
+
+ if (job->do_)
+ job->do_();
+ }
+ return getNextJobTime();
+ }
+
+ inline time_point getNextJobTime() const {
+ return timers.empty() ? time_point::max() : timers.begin()->first;
+ }
+
+ /**
+ * Accessors for the common time reference used for synchronizing
+ * operations.
+ */
+ inline const time_point& time() const { return now; }
+ inline time_point syncTime() { return (now = clock::now()); }
+ inline void syncTime(const time_point& n) { now = n; }
+
+private:
+ time_point now {clock::now()};
+ std::multimap<time_point, Sp<Job>> timers {}; /* the jobs ordered by time */
+};
+
+}
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ * Authors: Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ * Simon Désaulniers <simon.desaulniers@savoirfairelinux.com>
+ * Sébastien Blin <sebastien.blin@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "dht.h"
+#include "crypto.h"
+
+#include <map>
+#include <vector>
+#include <memory>
+#include <random>
+
+namespace dht {
+
+class OPENDHT_PUBLIC SecureDht final : public DhtInterface {
+public:
+
+ typedef std::function<void(bool)> SignatureCheckCallback;
+
+ using Config = SecureDhtConfig;
+
+ static dht::Config getConfig(const SecureDht::Config& conf)
+ {
+ auto c = conf.node_config;
+ if (not c.node_id and conf.id.second)
+ c.node_id = InfoHash::get("node:"+conf.id.second->getId().toString());
+ return c;
+ }
+
+ /**
+ * s, s6: bound socket descriptors for IPv4 and IPv6, respectively.
+ * For the Dht to be initialised, at least one of them must be >= 0.
+ * id: the identity to use for the crypto layer and to compute
+ * our own hash on the Dht.
+ */
+ SecureDht(std::unique_ptr<DhtInterface> dht, Config config, IdentityAnnouncedCb iacb = {}, const std::shared_ptr<Logger>& l = {});
+
+ virtual ~SecureDht();
+
+ InfoHash getId() const {
+ return key_ ? key_->getPublicKey().getId() : InfoHash();
+ }
+ PkId getLongId() const {
+ return key_ ? key_->getPublicKey().getLongId() : PkId();
+ }
+ Sp<crypto::PublicKey> getPublicKey() const {
+ return key_ ? key_->getSharedPublicKey() : Sp<crypto::PublicKey>{};
+ }
+
+ ValueType secureType(ValueType&& type);
+
+ ValueType secureType(const ValueType& type) {
+ ValueType tmp_type = type;
+ return secureType(std::move(tmp_type));
+ }
+
+ void registerType(const ValueType& type) override {
+ if (dht_)
+ dht_->registerType(secureType(type));
+ }
+ void registerType(ValueType&& type) {
+ if (dht_)
+ dht_->registerType(secureType(std::forward<ValueType>(type)));
+ }
+ void registerInsecureType(const ValueType& type) {
+ if (dht_)
+ dht_->registerType(type);
+ }
+
+ /**
+ * "Secure" get(), that will check the signature of signed data, and decrypt encrypted data.
+ * If the signature can't be checked, or if the data can't be decrypted, it is not returned.
+ * Public, non-signed & non-encrypted data is retransmitted as-is.
+ */
+ void get(const InfoHash& id, GetCallback cb, DoneCallback donecb={}, Value::Filter&& = {}, Where&& w = {}) override;
+ void get(const InfoHash& id, GetCallback cb, DoneCallbackSimple donecb={}, Value::Filter&& f = {}, Where&& w = {}) override {
+ get(id, cb, bindDoneCb(donecb), std::forward<Value::Filter>(f), std::forward<Where>(w));
+ }
+ void get(const InfoHash& key, GetCallbackSimple cb, DoneCallback donecb={}, Value::Filter&& f={}, Where&& w = {}) override {
+ get(key, bindGetCb(cb), donecb, std::forward<Value::Filter>(f), std::forward<Where>(w));
+ }
+ void get(const InfoHash& key, GetCallbackSimple cb, DoneCallbackSimple donecb, Value::Filter&& f={}, Where&& w = {}) override {
+ get(key, bindGetCb(cb), bindDoneCb(donecb), std::forward<Value::Filter>(f), std::forward<Where>(w));
+ }
+
+ /**
+ * Will take ownership of the value, sign it using our private key and put it in the DHT.
+ */
+ void putSigned(const InfoHash& hash, Sp<Value> val, DoneCallback callback, bool permanent = false);
+ void putSigned(const InfoHash& hash, Value&& v, DoneCallback callback, bool permanent = false) {
+ putSigned(hash, std::make_shared<Value>(std::move(v)), callback, permanent);
+ }
+
+ /**
+ * Will sign the data using our private key, encrypt it using the recipient' public key,
+ * and put it in the DHT.
+ * The operation will be immediate if the recipient' public key is known (otherwise it will be retrived first).
+ */
+ void putEncrypted(const InfoHash& hash, const InfoHash& to, Sp<Value> val, DoneCallback callback, bool permanent = false);
+ void putEncrypted(const InfoHash& hash, const InfoHash& to, Value&& v, DoneCallback callback, bool permanent = false) {
+ putEncrypted(hash, to, std::make_shared<Value>(std::move(v)), callback, permanent);
+ }
+ void putEncrypted(const InfoHash& hash, const crypto::PublicKey& to, Sp<Value> val, DoneCallback callback, bool permanent = false);
+ void putEncrypted(const InfoHash& hash, const crypto::PublicKey& to, Value&& v, DoneCallback callback, bool permanent = false) {
+ putEncrypted(hash, to, std::make_shared<Value>(std::move(v)), callback, permanent);
+ }
+
+ /**
+ * Take ownership of the value and sign it using our private key.
+ */
+ void sign(Value& v) const;
+
+ Value encrypt(Value& v, const crypto::PublicKey& to) const;
+
+ Value decrypt(const Value& v);
+
+ void findCertificate(const InfoHash& node, const std::function<void(const Sp<crypto::Certificate>)>& cb);
+ void findPublicKey(const InfoHash& node, const std::function<void(const Sp<crypto::PublicKey>)>& cb);
+
+ Sp<crypto::Certificate> registerCertificate(const InfoHash& node, const Blob& cert);
+ void registerCertificate(Sp<crypto::Certificate>& cert);
+
+ Sp<crypto::Certificate> getCertificate(const InfoHash& node) const;
+ Sp<crypto::PublicKey> getPublicKey(const InfoHash& node) const;
+
+ /**
+ * Allows to set a custom callback called by the library to find a locally-stored certificate.
+ * The search key used is the public key ID, so there may be multiple certificates retured, signed with
+ * the same private key.
+ */
+ void setLocalCertificateStore(CertificateStoreQuery&& query_method) {
+ localQueryMethod_ = std::move(query_method);
+ }
+ void setOnPublicAddressChanged(PublicAddressChangedCb cb) override {
+ dht_->setOnPublicAddressChanged(cb);
+ }
+
+ /**
+ * SecureDht to Dht proxy
+ */
+ void shutdown(ShutdownCallback cb, bool stop = false) override {
+ dht_->shutdown(cb, stop);
+ }
+ void dumpTables() const override {
+ dht_->dumpTables();
+ }
+ inline const InfoHash& getNodeId() const override { return dht_->getNodeId(); }
+
+ std::pair<size_t, size_t> getStoreSize() const override {
+ return dht_->getStoreSize();
+ }
+ std::string getStorageLog() const override {
+ return dht_->getStorageLog();
+ }
+ std::string getStorageLog(const InfoHash& h) const override {
+ return dht_->getStorageLog(h);
+ }
+ void setStorageLimit(size_t limit = DEFAULT_STORAGE_LIMIT) override {
+ dht_->setStorageLimit(limit);
+ }
+ size_t getStorageLimit() const override {
+ return dht_->getStorageLimit();
+ }
+
+ std::vector<NodeExport> exportNodes() const override {
+ return dht_->exportNodes();
+ }
+ std::vector<ValuesExport> exportValues() const override {
+ return dht_->exportValues();
+ }
+ void importValues(const std::vector<ValuesExport>& v) override {
+ dht_->importValues(v);
+ }
+ NodeStats getNodesStats(sa_family_t af) const override {
+ return dht_->getNodesStats(af);
+ }
+ std::vector<unsigned> getNodeMessageStats(bool in = false) override {
+ return dht_->getNodeMessageStats(in);
+ }
+ std::string getRoutingTablesLog(sa_family_t af) const override {
+ return dht_->getRoutingTablesLog(af);
+ }
+ std::string getSearchesLog(sa_family_t af) const override {
+ return dht_->getSearchesLog(af);
+ }
+ std::string getSearchLog(const InfoHash& h, sa_family_t af = AF_UNSPEC) const override {
+ return dht_->getSearchLog(h, af);
+ }
+ std::vector<SockAddr> getPublicAddress(sa_family_t family = 0) override {
+ return dht_->getPublicAddress(family);
+ }
+ time_point periodic(const uint8_t *buf, size_t buflen, SockAddr sa, const time_point& now) override {
+ return dht_->periodic(buf, buflen, std::move(sa), now);
+ }
+ time_point periodic(const uint8_t *buf, size_t buflen, const sockaddr* from, socklen_t fromlen, const time_point& now) override {
+ return dht_->periodic(buf, buflen, from, fromlen, now);
+ }
+ NodeStatus updateStatus(sa_family_t af) override {
+ return dht_->updateStatus(af);
+ }
+ NodeStatus getStatus(sa_family_t af) const override {
+ return dht_->getStatus(af);
+ }
+ NodeStatus getStatus() const override {
+ return dht_->getStatus();
+ }
+ net::DatagramSocket* getSocket() const override {
+ return dht_->getSocket();
+ };
+ bool isRunning(sa_family_t af = 0) const override {
+ return dht_->isRunning(af);
+ }
+ const ValueType& getType(ValueType::Id type_id) const override {
+ return dht_->getType(type_id);
+ }
+ void addBootstrap(const std::string& host, const std::string& service) override {
+ dht_->addBootstrap(host, service);
+ }
+ void clearBootstrap() override {
+ dht_->clearBootstrap();
+ }
+ void insertNode(const InfoHash& id, const SockAddr& sa) override {
+ dht_->insertNode(id, sa);
+ }
+ void insertNode(const NodeExport& n) override {
+ dht_->insertNode(n);
+ }
+ void pingNode(SockAddr sa, DoneCallbackSimple&& cb={}) override {
+ dht_->pingNode(std::move(sa), std::move(cb));
+ }
+ void query(const InfoHash& key, QueryCallback cb, DoneCallback done_cb = {}, Query&& q = {}) override {
+ dht_->query(key, cb, done_cb, std::move(q));
+ }
+ void query(const InfoHash& key, QueryCallback cb, DoneCallbackSimple done_cb = {}, Query&& q = {}) override {
+ dht_->query(key, cb, done_cb, std::move(q));
+ }
+ std::vector<Sp<Value>> getLocal(const InfoHash& key, const Value::Filter& f = {}) const override {
+ return dht_->getLocal(key, f);
+ }
+ Sp<Value> getLocalById(const InfoHash& key, Value::Id vid) const override {
+ return dht_->getLocalById(key, vid);
+ }
+ void put(const InfoHash& key,
+ Sp<Value> v,
+ DoneCallback cb=nullptr,
+ time_point created=time_point::max(),
+ bool permanent = false) override
+ {
+ dht_->put(key, v, cb, created, permanent);
+ }
+ void put(const InfoHash& key,
+ const Sp<Value>& v,
+ DoneCallbackSimple cb,
+ time_point created=time_point::max(),
+ bool permanent = false) override
+ {
+ dht_->put(key, v, cb, created, permanent);
+ }
+
+ void put(const InfoHash& key,
+ Value&& v,
+ DoneCallback cb=nullptr,
+ time_point created=time_point::max(),
+ bool permanent = false) override
+ {
+ dht_->put(key, std::move(v), cb, created, permanent);
+ }
+ void put(const InfoHash& key,
+ Value&& v,
+ DoneCallbackSimple cb,
+ time_point created=time_point::max(),
+ bool permanent = false) override
+ {
+ dht_->put(key, std::move(v), cb, created, permanent);
+ }
+ std::vector<Sp<Value>> getPut(const InfoHash& h) const override {
+ return dht_->getPut(h);
+ }
+ Sp<Value> getPut(const InfoHash& h, const Value::Id& vid) const override {
+ return dht_->getPut(h, vid);
+ }
+ bool cancelPut(const InfoHash& h, const Value::Id& vid) override {
+ return dht_->cancelPut(h, vid);
+ }
+
+ size_t listen(const InfoHash& key, ValueCallback, Value::Filter={}, Where={}) override;
+ size_t listen(const InfoHash& key, GetCallback cb, Value::Filter = {}, Where w = {}) override;
+ size_t listen(const InfoHash& key, GetCallbackSimple cb, Value::Filter f={}, Where w = {}) override {
+ return listen(key, bindGetCb(cb), f, w);
+ }
+ bool cancelListen(const InfoHash& h, size_t token) override {
+ return dht_->cancelListen(h, token);
+ }
+ void connectivityChanged(sa_family_t af) override {
+ dht_->connectivityChanged(af);
+ }
+ void connectivityChanged() override {
+ dht_->connectivityChanged();
+ }
+
+ void forwardAllMessages(bool forward) {
+ forward_all_ = forward;
+ }
+
+ void setPushNotificationToken(const std::string& token = "") override {
+ dht_->setPushNotificationToken(token);
+ }
+
+ /**
+ * Call linked callback with push_notification
+ * @param notification to process
+ */
+ void pushNotificationReceived(const std::map<std::string, std::string>& notification) override {
+ dht_->pushNotificationReceived(notification);
+ }
+
+ void setLogger(const Logger& logger) override {
+ DhtInterface::setLogger(logger);
+ dht_->setLogger(logger);
+ }
+
+ void setLogger(const std::shared_ptr<Logger>& logger) override {
+ DhtInterface::setLogger(logger);
+ dht_->setLogger(logger);
+ }
+
+ /**
+ * Only print logs related to the given InfoHash (if given), or disable filter (if zeroes).
+ */
+ void setLogFilter(const InfoHash& f) override {
+ DhtInterface::setLogFilter(f);
+ dht_->setLogFilter(f);
+ }
+
+private:
+ std::unique_ptr<DhtInterface> dht_;
+ // prevent copy
+ SecureDht(const SecureDht&) = delete;
+ SecureDht& operator=(const SecureDht&) = delete;
+
+ Sp<Value> checkValue(const Sp<Value>& v);
+ ValueCallback getCallbackFilter(const ValueCallback&, Value::Filter&&);
+ GetCallback getCallbackFilter(const GetCallback&, Value::Filter&&);
+
+ Sp<crypto::PrivateKey> key_ {};
+ Sp<crypto::Certificate> certificate_ {};
+
+ // method to query the local certificate store
+ CertificateStoreQuery localQueryMethod_ {};
+
+ // our certificate cache
+ std::map<InfoHash, Sp<crypto::Certificate>> nodesCertificates_ {};
+ std::map<InfoHash, Sp<crypto::PublicKey>> nodesPubKeys_ {};
+
+ std::atomic_bool forward_all_ {false};
+ bool enableCache_ {false};
+};
+
+const ValueType CERTIFICATE_TYPE = {
+ 8, "Certificate", std::chrono::hours(24 * 7),
+ // A certificate can only be stored at its public key ID.
+ [](InfoHash id, Sp<Value>& v, const InfoHash&, const SockAddr&) {
+ try {
+ crypto::Certificate crt(v->data);
+ // TODO check certificate signature
+ return crt.getPublicKey().getId() == id || InfoHash::get(crt.getPublicKey().getLongId()) == id;
+ } catch (const std::exception& e) {}
+ return false;
+ },
+ [](InfoHash, const Sp<Value>& o, Sp<Value>& n, const InfoHash&, const SockAddr&) {
+ try {
+ return crypto::Certificate(o->data).getPublicKey().getId() == crypto::Certificate(n->data).getPublicKey().getId();
+ } catch (const std::exception& e) {}
+ return false;
+ }
+};
+
+}
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ * Author : Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "def.h"
+
+#include <fmt/core.h>
+#include <fmt/format.h>
+#include <fmt/ostream.h>
+
+#ifndef _WIN32
+#include <sys/socket.h>
+#include <netinet/in.h>
+#include <arpa/inet.h>
+#ifdef __ANDROID__
+typedef uint16_t in_port_t;
+#endif
+#else
+#include <iso646.h>
+#include <stdint.h>
+#include <winsock2.h>
+#include <ws2def.h>
+#include <ws2tcpip.h>
+typedef uint16_t sa_family_t;
+typedef uint16_t in_port_t;
+#endif
+
+#include <string>
+#include <memory>
+#include <vector>
+#include <stdexcept>
+#include <stdlib.h>
+
+#include <cstring>
+#include <cstddef>
+
+namespace dht {
+
+OPENDHT_PUBLIC void print_addr(std::ostream& os, const sockaddr* sa, socklen_t slen);
+OPENDHT_PUBLIC std::string print_addr(const sockaddr* sa, socklen_t slen);
+OPENDHT_PUBLIC std::string print_addr(const sockaddr_storage& ss, socklen_t sslen);
+
+/**
+ * A Socket Address (sockaddr*), with abstraction for IPv4, IPv6 address families.
+ */
+class OPENDHT_PUBLIC SockAddr {
+public:
+ SockAddr() {}
+ SockAddr(const SockAddr& o) {
+ set(o.get(), o.getLength());
+ }
+ SockAddr(SockAddr&& o) noexcept : addr(std::move(o.addr)), len(o.len) {
+ o.len = 0;
+ }
+
+ /**
+ * Build from existing address.
+ */
+ SockAddr(const sockaddr* sa, socklen_t length) {
+ if (length > static_cast<socklen_t>(sizeof(sockaddr_storage)))
+ throw std::runtime_error("Socket address length is too large");
+ set(sa, length);
+ }
+ SockAddr(const sockaddr* sa) {
+ socklen_t len = 0;
+ if (sa) {
+ if (sa->sa_family == AF_INET)
+ len = sizeof(sockaddr_in);
+ else if(sa->sa_family == AF_INET6)
+ len = sizeof(sockaddr_in6);
+ else
+ throw std::runtime_error("Unknown address family");
+ }
+ set(sa, len);
+ }
+
+ /**
+ * Build from an existing sockaddr_storage structure.
+ */
+ SockAddr(const sockaddr_storage& ss, socklen_t len) : SockAddr((const sockaddr*)&ss, len) {}
+
+ static std::vector<SockAddr> resolve(const std::string& host, const std::string& service = {});
+
+ bool operator<(const SockAddr& o) const {
+ if (len != o.len)
+ return len < o.len;
+ return std::memcmp((const uint8_t*)get(), (const uint8_t*)o.get(), len) < 0;
+ }
+
+ bool equals(const SockAddr& o) const {
+ return len == o.len
+ && std::memcmp((const uint8_t*)get(), (const uint8_t*)o.get(), len) == 0;
+ }
+ SockAddr& operator=(const SockAddr& o) {
+ set(o.get(), o.getLength());
+ return *this;
+ }
+ SockAddr& operator=(SockAddr&& o) {
+ len = o.len;
+ o.len = 0;
+ addr = std::move(o.addr);
+ return *this;
+ }
+
+ std::string toString() const {
+ return print_addr(get(), getLength());
+ }
+
+ /**
+ * Returns the address family or AF_UNSPEC if the address is not set.
+ */
+ sa_family_t getFamily() const { return len ? addr->sa_family : AF_UNSPEC; }
+
+ /**
+ * Resize the managed structure to the appropriate size (if needed),
+ * in which case the sockaddr structure is cleared to zero,
+ * and set the address family field (sa_family).
+ */
+ void setFamily(sa_family_t af) {
+ socklen_t new_length;
+ switch(af) {
+ case AF_INET:
+ new_length = sizeof(sockaddr_in);
+ break;
+ case AF_INET6:
+ new_length = sizeof(sockaddr_in6);
+ break;
+ default:
+ new_length = 0;
+ }
+ if (new_length != len) {
+ len = new_length;
+ if (len) addr.reset((sockaddr*)::calloc(len, 1));
+ else addr.reset();
+ }
+ if (len)
+ addr->sa_family = af;
+ }
+
+ /**
+ * Set Network Interface to any
+ */
+ void setAny() {
+ auto family = getFamily();
+ switch(family) {
+ case AF_INET:
+ getIPv4().sin_addr.s_addr = htonl(INADDR_ANY);
+ break;
+ case AF_INET6:
+ getIPv6().sin6_addr = in6addr_any;
+ break;
+ }
+ }
+
+ /**
+ * Retreive the port (in host byte order) or 0 if the address is not
+ * of a supported family.
+ */
+ in_port_t getPort() const {
+ switch(getFamily()) {
+ case AF_INET:
+ return ntohs(getIPv4().sin_port);
+ case AF_INET6:
+ return ntohs(getIPv6().sin6_port);
+ default:
+ return 0;
+ }
+ }
+ /**
+ * Set the port. The address must be of a supported family.
+ * @param p The port in host byte order.
+ */
+ void setPort(in_port_t p) {
+ switch(getFamily()) {
+ case AF_INET:
+ getIPv4().sin_port = htons(p);
+ break;
+ case AF_INET6:
+ getIPv6().sin6_port = htons(p);
+ break;
+ }
+ }
+
+ /**
+ * Set the address part of the socket address from a numeric IP address (string representation).
+ * Family must be already set. Throws in case of parse failue.
+ */
+ void setAddress(const char* address);
+
+ /**
+ * Returns the accessible byte length at the pointer returned by #get().
+ * If zero, #get() returns null.
+ */
+ socklen_t getLength() const { return len; }
+
+ /**
+ * An address is defined to be true if its length is not zero.
+ */
+ explicit operator bool() const noexcept {
+ return len;
+ }
+
+ /**
+ * Returns the address to the managed sockaddr structure.
+ * The accessible length is returned by #getLength().
+ */
+ const sockaddr* get() const { return addr.get(); }
+
+ /**
+ * Returns the address to the managed sockaddr structure.
+ * The accessible length is returned by #getLength().
+ */
+ sockaddr* get() { return addr.get(); }
+
+ inline const sockaddr_in& getIPv4() const {
+ return *reinterpret_cast<const sockaddr_in*>(get());
+ }
+ inline const sockaddr_in6& getIPv6() const {
+ return *reinterpret_cast<const sockaddr_in6*>(get());
+ }
+ inline sockaddr_in& getIPv4() {
+ return *reinterpret_cast<sockaddr_in*>(get());
+ }
+ inline sockaddr_in6& getIPv6() {
+ return *reinterpret_cast<sockaddr_in6*>(get());
+ }
+
+ /**
+ * Releases the ownership of the managed object, if any.
+ * The caller is responsible for deleting the object with free().
+ */
+ inline sockaddr* release() {
+ len = 0;
+ return addr.release();
+ }
+
+ /**
+ * Return true if address is a loopback IP address.
+ */
+ bool isLoopback() const;
+
+ /**
+ * Return true if address is not a public IP address.
+ */
+ bool isPrivate() const;
+
+ bool isUnspecified() const;
+
+ bool isMappedIPv4() const;
+ SockAddr getMappedIPv4();
+ SockAddr getMappedIPv6();
+
+ /**
+ * A comparator to classify IP addresses, only considering the
+ * first 64 bits in IPv6.
+ */
+ struct ipCmp {
+ bool operator()(const SockAddr& a, const SockAddr& b) const {
+ if (a.len != b.len)
+ return a.len < b.len;
+ socklen_t start, len;
+ switch(a.getFamily()) {
+ case AF_INET:
+ start = offsetof(sockaddr_in, sin_addr);
+ len = sizeof(in_addr);
+ break;
+ case AF_INET6:
+ start = offsetof(sockaddr_in6, sin6_addr);
+ // don't consider more than 64 bits (IPv6)
+ len = 8;
+ break;
+ default:
+ start = 0;
+ len = a.len;
+ break;
+ }
+ return std::memcmp((uint8_t*)a.get()+start,
+ (uint8_t*)b.get()+start, len) < 0;
+ }
+ };
+ OPENDHT_PUBLIC friend std::ostream& operator<< (std::ostream& s, const SockAddr& h) {
+ print_addr(s, h.get(), h.getLength());
+ return s;
+ }
+
+private:
+ struct free_delete { void operator()(void* p) { ::free(p); } };
+ std::unique_ptr<sockaddr, free_delete> addr {};
+ socklen_t len {0};
+
+ void set(const sockaddr* sa, socklen_t length) {
+ if (len != length) {
+ len = length;
+ if (len) addr.reset((sockaddr*)::malloc(len));
+ else addr.reset();
+ }
+ if (len)
+ std::memcpy((uint8_t*)get(), (const uint8_t*)sa, len);
+ }
+
+};
+
+OPENDHT_PUBLIC bool operator==(const SockAddr& a, const SockAddr& b);
+
+}
+
+#if FMT_VERSION >= 90000
+template <> struct fmt::formatter<dht::SockAddr> : ostream_formatter {};
+#endif
\ No newline at end of file
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ *
+ * Author: Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "def.h"
+
+#include <condition_variable>
+#include <vector>
+#include <queue>
+#include <future>
+#include <functional>
+
+#include <ciso646> // fix windows compiler bug
+
+namespace dht {
+
+class OPENDHT_PUBLIC ThreadPool {
+public:
+ static ThreadPool& computation();
+ static ThreadPool& io();
+
+ ThreadPool();
+ ThreadPool(unsigned minThreads, unsigned maxThreads = 0);
+ ~ThreadPool();
+
+ void run(std::function<void()>&& cb);
+
+ template<class T>
+ std::future<T> get(std::function<T()>&& cb) {
+ auto ret = std::make_shared<std::promise<T>>();
+ run([cb = std::move(cb), ret]() mutable {
+ try {
+ ret->set_value(cb());
+ } catch (...) {
+ try {
+ ret->set_exception(std::current_exception());
+ } catch(...) {}
+ }
+ });
+ return ret->get_future();
+ }
+ template<class T>
+ std::shared_future<T> getShared(std::function<T()>&& cb) {
+ return get(std::move(cb));
+ }
+
+ void stop(bool wait = true);
+ void join();
+
+private:
+ std::mutex lock_ {};
+ std::condition_variable cv_ {};
+ std::queue<std::function<void()>> tasks_ {};
+ std::vector<std::unique_ptr<std::thread>> threads_;
+ unsigned readyThreads_ {0};
+ bool running_ {true};
+
+ unsigned minThreads_;
+ const unsigned maxThreads_;
+ std::chrono::steady_clock::duration threadExpirationDelay {std::chrono::minutes(5)};
+ double threadDelayRatio_ {2};
+
+ void threadEnded(std::thread&);
+};
+
+class OPENDHT_PUBLIC Executor : public std::enable_shared_from_this<Executor> {
+public:
+ Executor(ThreadPool& pool, unsigned maxConcurrent = 1)
+ : threadPool_(pool), maxConcurrent_(maxConcurrent)
+ {}
+
+ void run(std::function<void()>&& task);
+
+private:
+ std::reference_wrapper<ThreadPool> threadPool_;
+ const unsigned maxConcurrent_ {1};
+ std::mutex lock_ {};
+ unsigned current_ {0};
+ std::queue<std::function<void()>> tasks_ {};
+
+ void run_(std::function<void()>&& task);
+ void schedule();
+};
+
+class OPENDHT_PUBLIC ExecutionContext {
+public:
+ ExecutionContext(ThreadPool& pool)
+ : threadPool_(pool), state_(std::make_shared<SharedState>())
+ {}
+
+ ~ExecutionContext() {
+ state_->destroy();
+ }
+
+ /** Wait for ongoing tasks to complete execution and drop other pending tasks */
+ void stop() {
+ state_->destroy(false);
+ }
+
+ void run(std::function<void()>&& task) {
+ std::lock_guard<std::mutex> lock(state_->mtx);
+ if (state_->shutdown_) return;
+ state_->pendingTasks++;
+ threadPool_.get().run([task = std::move(task), state = state_] {
+ state->run(task);
+ });
+ }
+
+private:
+ struct SharedState {
+ std::mutex mtx {};
+ std::condition_variable cv {};
+ unsigned pendingTasks {0};
+ unsigned ongoingTasks {0};
+ /** When true, prevents new tasks to be scheduled */
+ bool shutdown_ {false};
+ /** When true, prevents scheduled tasks to be executed */
+ std::atomic_bool destroyed {false};
+
+ void destroy(bool wait = true) {
+ std::unique_lock<std::mutex> lock(mtx);
+ if (destroyed) return;
+ if (wait) {
+ cv.wait(lock, [this] { return pendingTasks == 0 && ongoingTasks == 0; });
+ }
+ shutdown_ = true;
+ if (not wait) {
+ cv.wait(lock, [this] { return ongoingTasks == 0; });
+ }
+ destroyed = true;
+ }
+
+ void run(const std::function<void()>& task) {
+ {
+ std::lock_guard<std::mutex> lock(mtx);
+ pendingTasks--;
+ ongoingTasks++;
+ }
+ if (destroyed) return;
+ task();
+ {
+ std::lock_guard<std::mutex> lock(mtx);
+ ongoingTasks--;
+ cv.notify_all();
+ }
+ }
+ };
+ std::reference_wrapper<ThreadPool> threadPool_;
+ std::shared_ptr<SharedState> state_;
+};
+
+}
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ * Author : Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "def.h"
+
+#include <msgpack.hpp>
+
+#include <chrono>
+#include <random>
+#include <functional>
+#include <map>
+
+#include <cstdarg>
+
+#define WANT4 1
+#define WANT6 2
+
+/**
+ * OpenDHT C++ namespace
+ */
+namespace dht {
+
+using NetId = uint32_t;
+using want_t = int_fast8_t;
+
+OPENDHT_PUBLIC const char* version();
+
+// shortcut for std::shared_ptr
+template<class T>
+using Sp = std::shared_ptr<T>;
+
+template <typename Key, typename Item, typename Condition>
+void erase_if(std::map<Key, Item>& map, const Condition& condition)
+{
+ for (auto it = map.begin(); it != map.end(); ) {
+ if (condition(*it)) {
+ it = map.erase(it);
+ } else { ++it; }
+ }
+}
+
+/**
+ * Split "[host]:port" or "host:port" to pair<"host", "port">.
+ */
+OPENDHT_PUBLIC std::pair<std::string, std::string>
+splitPort(const std::string& s);
+
+class OPENDHT_PUBLIC DhtException : public std::runtime_error {
+public:
+ DhtException(const std::string &str = "") :
+ std::runtime_error("DhtException occurred: " + str) {}
+};
+
+class OPENDHT_PUBLIC SocketException : public DhtException {
+public:
+ SocketException(int err) :
+ DhtException(strerror(err)) {}
+};
+
+// Time related definitions and utility functions
+
+using clock = std::chrono::steady_clock;
+using system_clock = std::chrono::system_clock;
+using time_point = clock::time_point;
+using duration = clock::duration;
+
+time_point from_time_t(std::time_t t);
+std::time_t to_time_t(time_point t);
+
+inline std::string
+to_str(double d) {
+ char buf[16];
+ auto ret = snprintf(buf, sizeof(buf), "%.3g", d);
+ return (ret < 0) ? std::to_string(d) : std::string(buf, ret);
+}
+
+/**
+ * Converts std::chrono::duration to floating-point seconds.
+ */
+template <class DT>
+static double
+print_dt(DT d) {
+ return std::chrono::duration_cast<std::chrono::duration<double>>(d).count();
+}
+
+template <class DT>
+static std::string
+print_duration(DT d) {
+ if (d < std::chrono::seconds(0)) {
+ return "-" + print_duration(-d);
+ } else if (d < std::chrono::milliseconds(1)) {
+ return to_str(std::chrono::duration_cast<std::chrono::duration<double, std::micro>>(d).count()) + " us";
+ } else if (d < std::chrono::seconds(1)) {
+ return to_str(std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(d).count()) + " ms";
+ } else if (d < std::chrono::minutes(1)) {
+ return to_str(std::chrono::duration_cast<std::chrono::duration<double>>(d).count()) + " s";
+ } else if (d < std::chrono::hours(1)) {
+ return to_str(std::chrono::duration_cast<std::chrono::duration<double, std::ratio<60>>>(d).count()) + " min";
+ } else {
+ return to_str(std::chrono::duration_cast<std::chrono::duration<double, std::ratio<3600>>>(d).count()) + " h";
+ }
+}
+
+template <class TimePoint>
+static std::string
+print_time_relative(TimePoint now, TimePoint d) {
+ if (d == TimePoint::min()) return "never";
+ if (d == now) return "now";
+ return (d > now) ? std::string("in ") + print_duration(d - now)
+ : print_duration(now - d) + std::string(" ago");
+}
+
+template <typename Duration = duration>
+class uniform_duration_distribution : public std::uniform_int_distribution<typename Duration::rep> {
+ using Base = std::uniform_int_distribution<typename Duration::rep>;
+ using param_type = typename Base::param_type;
+public:
+ uniform_duration_distribution(Duration min, Duration max) : Base(min.count(), max.count()) {}
+ template <class Generator>
+ Duration operator()(Generator && g) {
+ return Duration(Base::operator()(g));
+ }
+ template< class Generator >
+ Duration operator()( Generator && g, const param_type& params ) {
+ return Duration(Base::operator()(g, params));
+ }
+};
+
+// Serialization related definitions and utility functions
+
+/**
+ * Arbitrary binary data.
+ */
+using Blob = std::vector<uint8_t>;
+
+/**
+ * Provides backward compatibility with msgpack 1.0
+ */
+OPENDHT_PUBLIC Blob unpackBlob(const msgpack::object& o);
+
+template <typename Type>
+Blob
+packMsg(const Type& t) {
+ msgpack::sbuffer buffer;
+ msgpack::packer<msgpack::sbuffer> pk(&buffer);
+ pk.pack(t);
+ return {buffer.data(), buffer.data()+buffer.size()};
+}
+
+template <typename Type>
+Type
+unpackMsg(Blob b) {
+ msgpack::unpacked msg_res = msgpack::unpack((const char*)b.data(), b.size());
+ return msg_res.get().as<Type>();
+}
+
+msgpack::unpacked unpackMsg(Blob b);
+
+msgpack::object* findMapValue(const msgpack::object& map, const char* key, size_t length);
+
+inline msgpack::object* findMapValue(const msgpack::object& map, const char* key) {
+ return findMapValue(map, key, strlen(key));
+}
+inline msgpack::object* findMapValue(const msgpack::object& map, std::string_view key) {
+ return findMapValue(map, key.data(), key.size());
+}
+
+} // namespace dht
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ * Author(s) : Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ * Simon Désaulniers <simon.desaulniers@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "infohash.h"
+#include "crypto.h"
+#include "utils.h"
+#include "sockaddr.h"
+
+#include <msgpack.hpp>
+
+#include <string>
+#include <string_view>
+#include <sstream>
+#include <bitset>
+#include <vector>
+#include <iostream>
+#include <algorithm>
+#include <functional>
+#include <memory>
+#include <chrono>
+#include <set>
+
+#ifdef OPENDHT_JSONCPP
+#include <json/json.h>
+#endif
+
+namespace dht {
+using namespace std::literals;
+
+static constexpr auto VALUE_KEY_ID("id");
+static const std::string VALUE_KEY_DAT("dat");
+static const std::string VALUE_KEY_PRIO("p");
+static const std::string VALUE_KEY_SIGNATURE("sig");
+
+static const std::string VALUE_KEY_SEQ("seq");
+static const std::string VALUE_KEY_DATA("data");
+static const std::string VALUE_KEY_OWNER("owner");
+static const std::string VALUE_KEY_TYPE("type");
+static const std::string VALUE_KEY_TO("to");
+static const std::string VALUE_KEY_BODY("body");
+static const std::string VALUE_KEY_USERTYPE("utype");
+
+struct Value;
+struct Query;
+
+/**
+ * A storage policy is applied once to every incoming value storage requests.
+ * If the policy returns false, the value is dropped.
+ *
+ * @param key: the key where the storage is requested.
+ * @param value: the value to be stored. The value can be edited by the storage policy.
+ * @param from: id of the requesting node.
+ * @param form_addr: network address of the incoming request.
+ * @param from_len: network address lendth of the incoming request.
+ */
+using StorePolicy = std::function<bool(InfoHash key, std::shared_ptr<Value>& value, const InfoHash& from, const SockAddr& addr)>;
+
+/**
+ * An edition policy is applied once to every incoming value storage requests,
+ * if a value already exists for this key and value id.
+ * If the policy returns false, the edition request is ignored.
+ * The default behavior is to deny edition (see {ValueType::DEFAULT_EDIT_POLICY}).
+ * Some {ValueType}s may override this behavior (e.g. SignedValue).
+ *
+ * @param key: the key where the value is stored.
+ * @param old_val: the previously stored value.
+ * @param new_val: the new value to be stored. The value can be edited by the edit policy.
+ * @param from: id of the requesting node.
+ * @param form_addr: network address of the incoming request.
+ * @param from_len: network address lendth of the incoming request.
+ */
+using EditPolicy = std::function<bool(InfoHash key, const std::shared_ptr<Value>& old_val, std::shared_ptr<Value>& new_val, const InfoHash& from, const SockAddr& addr)>;
+
+static constexpr const size_t MAX_VALUE_SIZE {1024 * 64};
+
+struct OPENDHT_PUBLIC ValueType {
+ typedef uint16_t Id;
+
+ static bool DEFAULT_STORE_POLICY(InfoHash, const std::shared_ptr<Value>& v, const InfoHash&, const SockAddr&);
+ static bool DEFAULT_EDIT_POLICY(InfoHash, const std::shared_ptr<Value>&, std::shared_ptr<Value>&, const InfoHash&, const SockAddr&) {
+ return false;
+ }
+
+ ValueType () {}
+
+ ValueType (Id id, std::string name, duration e = std::chrono::minutes(10))
+ : id(id), name(name), expiration(e) {}
+
+ ValueType (Id id, std::string name, duration e, StorePolicy sp, EditPolicy ep = DEFAULT_EDIT_POLICY)
+ : id(id), name(name), expiration(e), storePolicy(sp), editPolicy(ep) {}
+
+ virtual ~ValueType() {}
+
+ bool operator==(const ValueType& o) {
+ return id == o.id;
+ }
+
+ // Generic value type
+ static const ValueType USER_DATA;
+
+
+ Id id {0};
+ std::string name {};
+ duration expiration {std::chrono::minutes(10)};
+ StorePolicy storePolicy {DEFAULT_STORE_POLICY};
+ EditPolicy editPolicy {DEFAULT_EDIT_POLICY};
+};
+
+class TypeStore {
+public:
+ void registerType(const ValueType& type) {
+ types[type.id] = type;
+ }
+ const ValueType& getType(ValueType::Id type_id) const {
+ const auto& t_it = types.find(type_id);
+ return (t_it == types.end()) ? ValueType::USER_DATA : t_it->second;
+ }
+private:
+ std::map<ValueType::Id, ValueType> types {};
+};
+
+struct CryptoValueCache;
+
+/**
+ * A "value" is data potentially stored on the Dht, with some metadata.
+ *
+ * It can be an IP:port announced for a service, a public key, or any kind of
+ * light user-defined data (recommended: less than 512 bytes).
+ *
+ * Values are stored at a given InfoHash in the Dht, but also have a
+ * unique ID to distinguish between values stored at the same location.
+ */
+struct OPENDHT_PUBLIC Value
+{
+ enum class Field : int {
+ None = 0,
+ Id, /* Value::id */
+ ValueType, /* Value::type */
+ OwnerPk, /* Value::owner */
+ SeqNum, /* Value::seq */
+ UserType, /* Value::user_type */
+
+ COUNT /* the total number of fields */
+ };
+
+ typedef uint64_t Id;
+ static const constexpr Id INVALID_ID {0};
+
+ class Filter : public std::function<bool(const Value&)> {
+ public:
+ Filter() {}
+
+ template<typename Functor>
+ Filter(Functor f) : std::function<bool(const Value&)>::function(f) {}
+
+ inline Filter chain(Filter&& f2) {
+ auto f1 = *this;
+ return chain(std::move(f1), std::move(f2));
+ }
+ inline Filter chainOr(Filter&& f2) {
+ auto f1 = *this;
+ return chainOr(std::move(f1), std::move(f2));
+ }
+ static inline Filter chain(Filter&& f1, Filter&& f2) {
+ if (not f1) return std::move(f2);
+ if (not f2) return std::move(f1);
+ return [f1 = std::move(f1), f2 = std::move(f2)](const Value& v) {
+ return f1(v) and f2(v);
+ };
+ }
+ static inline Filter chain(const Filter& f1, const Filter& f2) {
+ if (not f1) return f2;
+ if (not f2) return f1;
+ return [f1,f2](const Value& v) {
+ return f1(v) and f2(v);
+ };
+ }
+ static inline Filter chainAll(std::vector<Filter>&& set) {
+ if (set.empty()) return {};
+ return [set = std::move(set)](const Value& v) {
+ for (const auto& f : set)
+ if (f and not f(v))
+ return false;
+ return true;
+ };
+ }
+ static inline Filter chain(std::initializer_list<Filter> l) {
+ return chainAll(std::vector<Filter>(l.begin(), l.end()));
+ }
+ static inline Filter chainOr(Filter&& f1, Filter&& f2) {
+ if (not f1 or not f2) return {};
+ return [f1=std::move(f1),f2=std::move(f2)](const Value& v) {
+ return f1(v) or f2(v);
+ };
+ }
+ static inline Filter notFilter(Filter&& f) {
+ if (not f) return [](const Value&) { return false; };
+ return [f = std::move(f)](const Value& v) { return not f(v); };
+ }
+ std::vector<Sp<Value>> filter(const std::vector<Sp<Value>>& values) {
+ if (not (*this))
+ return values;
+ std::vector<Sp<Value>> ret;
+ for (const auto& v : values)
+ if ((*this)(v))
+ ret.emplace_back(v);
+ return ret;
+ }
+ };
+
+ /* Sneaky functions disguised in classes */
+
+ static inline Filter AllFilter() {
+ return {};
+ }
+
+ static inline Filter TypeFilter(const ValueType& t) {
+ return [tid = t.id](const Value& v) {
+ return v.type == tid;
+ };
+ }
+ static inline Filter TypeFilter(const ValueType::Id& tid) {
+ return [tid](const Value& v) {
+ return v.type == tid;
+ };
+ }
+
+ static inline Filter IdFilter(const Id id) {
+ return [id](const Value& v) {
+ return v.id == id;
+ };
+ }
+
+ static inline Filter RecipientFilter(const InfoHash& r) {
+ return [r](const Value& v) {
+ return v.recipient == r;
+ };
+ }
+
+ static inline Filter OwnerFilter(const crypto::PublicKey& pk) {
+ return OwnerFilter(pk.getId());
+ }
+
+ static inline Filter OwnerFilter(const InfoHash& pkh) {
+ return [pkh](const Value& v) {
+ return v.owner and v.owner->getId() == pkh;
+ };
+ }
+
+ static inline Filter SeqNumFilter(uint16_t seq_no) {
+ return [seq_no](const Value& v) {
+ return v.seq == seq_no;
+ };
+ }
+
+ static inline Filter UserTypeFilter(std::string ut) {
+ return [ut = std::move(ut)](const Value& v) {
+ return v.user_type == ut;
+ };
+ }
+
+ class SerializableBase
+ {
+ public:
+ SerializableBase() {}
+ virtual ~SerializableBase() {};
+ virtual const ValueType& getType() const = 0;
+ virtual void unpackValue(const Value& v) = 0;
+ virtual Value packValue() const = 0;
+ };
+
+ template <typename Derived, typename Base=SerializableBase>
+ class Serializable : public Base
+ {
+ public:
+ using Base::Base;
+
+ virtual const ValueType& getType() const {
+ return Derived::TYPE;
+ }
+
+ virtual void unpackValue(const Value& v) {
+ auto msg = msgpack::unpack((const char*)v.data.data(), v.data.size());
+ msg.get().convert(*static_cast<Derived*>(this));
+ }
+
+ virtual Value packValue() const {
+ return Value {getType(), static_cast<const Derived&>(*this)};
+ }
+ };
+
+ template <typename T,
+ typename std::enable_if<std::is_base_of<SerializableBase, T>::value, T>::type* = nullptr>
+ static Value pack(const T& obj)
+ {
+ return obj.packValue();
+ }
+
+ template <typename T,
+ typename std::enable_if<!std::is_base_of<SerializableBase, T>::value, T>::type* = nullptr>
+ static Value pack(const T& obj)
+ {
+ return {ValueType::USER_DATA.id, packMsg<T>(obj)};
+ }
+
+ template <typename T,
+ typename std::enable_if<std::is_base_of<SerializableBase, T>::value, T>::type* = nullptr>
+ static T unpack(const Value& v)
+ {
+ T msg;
+ msg.unpackValue(v);
+ return msg;
+ }
+
+ template <typename T,
+ typename std::enable_if<!std::is_base_of<SerializableBase, T>::value, T>::type* = nullptr>
+ static T unpack(const Value& v)
+ {
+ return unpackMsg<T>(v.data);
+ }
+
+ template <typename T>
+ T unpack()
+ {
+ return unpack<T>(*this);
+ }
+
+ inline bool isEncrypted() const {
+ return not cypher.empty();
+ }
+ inline bool isSigned() const {
+ return owner and not signature.empty();
+ }
+
+ /**
+ * Sign the value using the provided private key.
+ * Afterward, checkSignature() will return true and owner will
+ * be set to the corresponding public key.
+ */
+ void sign(const crypto::PrivateKey& key);
+
+ /**
+ * Check that the value is signed and that the signature matches.
+ * If true, the owner field will contain the signer public key.
+ */
+ inline bool checkSignature() const {
+ return isSigned() and owner->checkSignature(getToSign(), signature);
+ }
+
+ inline std::shared_ptr<crypto::PublicKey> getOwner() const {
+ return owner;
+ }
+
+ /**
+ * Sign the value with from and returns the encrypted version for to.
+ */
+ Value encrypt(const crypto::PrivateKey& from, const crypto::PublicKey& to);
+
+ Value() {}
+
+ Value (Id id) : id(id) {}
+
+ /** Generic constructor */
+ Value(ValueType::Id t, const Blob& data, Id id = INVALID_ID)
+ : id(id), type(t), data(data) {}
+ Value(ValueType::Id t, Blob&& data, Id id = INVALID_ID)
+ : id(id), type(t), data(std::move(data)) {}
+ Value(ValueType::Id t, const uint8_t* dat_ptr, size_t dat_len, Id id = INVALID_ID)
+ : id(id), type(t), data(dat_ptr, dat_ptr+dat_len) {}
+
+#ifdef OPENDHT_JSONCPP
+ /**
+ * Build a value from a json object
+ * @param json
+ */
+ Value(const Json::Value& json);
+#endif
+
+ template <typename Type>
+ Value(ValueType::Id t, const Type& d, Id id = INVALID_ID)
+ : id(id), type(t), data(packMsg(d)) {}
+
+ template <typename Type>
+ Value(const ValueType& t, const Type& d, Id id = INVALID_ID)
+ : id(id), type(t.id), data(packMsg(d)) {}
+
+ /** Custom user data constructor */
+ Value(const Blob& userdata) : data(userdata) {}
+ Value(Blob&& userdata) : data(std::move(userdata)) {}
+ Value(const uint8_t* dat_ptr, size_t dat_len) : data(dat_ptr, dat_ptr+dat_len) {}
+
+ Value(Value&& o) noexcept
+ : id(o.id), owner(std::move(o.owner)), recipient(o.recipient),
+ type(o.type), data(std::move(o.data)), user_type(std::move(o.user_type)), seq(o.seq)
+ , signature(std::move(o.signature)), cypher(std::move(o.cypher))
+ , priority(o.priority) {}
+
+ template <typename Type>
+ Value(const Type& vs)
+ : Value(pack<Type>(vs)) {}
+
+ /**
+ * Unpack a serialized value
+ */
+ Value(const msgpack::object& o) {
+ msgpack_unpack(o);
+ }
+
+ /**
+ * Returns true if value contents are equals (not considering the value ID)
+ */
+ inline bool contentEquals(const Value& o) const {
+ return isEncrypted() ? cypher == o.cypher :
+ ((owner == o.owner || (owner and o.owner and *owner == *o.owner))
+ && type == o.type
+ && data == o.data
+ && user_type == o.user_type
+ && signature == o.signature);
+ }
+
+ inline bool operator== (const Value& o) const {
+ return id == o.id and contentEquals(o);
+ }
+ inline bool operator!= (const Value& o) const {
+ return !(*this == o);
+ }
+
+ inline void setRecipient(const InfoHash& r) {
+ recipient = r;
+ }
+
+ inline void setCypher(Blob&& c) {
+ cypher = std::move(c);
+ }
+
+ /**
+ * Pack part of the data to be signed (must always be done the same way)
+ */
+ inline Blob getToSign() const {
+ msgpack::sbuffer buffer;
+ msgpack::packer<msgpack::sbuffer> pk(&buffer);
+ msgpack_pack_to_sign(pk);
+ return {buffer.data(), buffer.data()+buffer.size()};
+ }
+
+ /**
+ * Pack part of the data to be encrypted
+ */
+ inline Blob getToEncrypt() const {
+ msgpack::sbuffer buffer;
+ msgpack::packer<msgpack::sbuffer> pk(&buffer);
+ msgpack_pack_to_encrypt(pk);
+ return {buffer.data(), buffer.data()+buffer.size()};
+ }
+
+ /** print value for debugging */
+ OPENDHT_PUBLIC friend std::ostream& operator<< (std::ostream& s, const Value& v);
+
+ inline std::string toString() const {
+ std::ostringstream ss;
+ ss << *this;
+ return ss.str();
+ }
+
+#ifdef OPENDHT_JSONCPP
+ /**
+ * Build a json object from a value
+ * Example:
+ * {
+ * "data":"base64ofdata",
+ * id":"0", "seq":0,"type":3
+ * }
+ */
+ Json::Value toJson() const;
+#endif
+
+ /** Return the size in bytes used by this value in memory (minimum). */
+ size_t size() const;
+
+ template <typename Packer>
+ void msgpack_pack_to_sign(Packer& pk) const
+ {
+ bool has_owner = owner && *owner;
+ pk.pack_map((user_type.empty()?0:1) + (has_owner?(recipient ? 5 : 4):2));
+ if (has_owner) { // isSigned
+ pk.pack(VALUE_KEY_SEQ); pk.pack(seq);
+ pk.pack(VALUE_KEY_OWNER); owner->msgpack_pack(pk);
+ if (recipient) {
+ pk.pack(VALUE_KEY_TO); pk.pack(recipient);
+ }
+ }
+ pk.pack(VALUE_KEY_TYPE); pk.pack(type);
+ pk.pack(VALUE_KEY_DATA); pk.pack_bin(data.size());
+ pk.pack_bin_body((const char*)data.data(), data.size());
+ if (not user_type.empty()) {
+ pk.pack(VALUE_KEY_USERTYPE); pk.pack(user_type);
+ }
+ }
+
+ template <typename Packer>
+ void msgpack_pack_to_encrypt(Packer& pk) const
+ {
+ if (isEncrypted()) {
+ pk.pack_bin(cypher.size());
+ pk.pack_bin_body((const char*)cypher.data(), cypher.size());
+ } else {
+ pk.pack_map(isSigned() ? 2 : 1);
+ pk.pack(VALUE_KEY_BODY); msgpack_pack_to_sign(pk);
+ if (isSigned()) {
+ pk.pack(VALUE_KEY_SIGNATURE); pk.pack_bin(signature.size());
+ pk.pack_bin_body((const char*)signature.data(), signature.size());
+ }
+ }
+ }
+
+ template <typename Packer>
+ void msgpack_pack(Packer& pk) const
+ {
+ pk.pack_map(2 + (priority?1:0));
+ pk.pack(VALUE_KEY_ID); pk.pack(id);
+ pk.pack(VALUE_KEY_DAT); msgpack_pack_to_encrypt(pk);
+ if (priority) {
+ pk.pack(VALUE_KEY_PRIO); pk.pack(priority);
+ }
+ }
+
+ template <typename Packer>
+ void msgpack_pack_fields(const std::set<Value::Field>& fields, Packer& pk) const
+ {
+ for (const auto& field : fields)
+ switch (field) {
+ case Value::Field::Id:
+ pk.pack(static_cast<uint64_t>(id));
+ break;
+ case Value::Field::ValueType:
+ pk.pack(static_cast<uint64_t>(type));
+ break;
+ case Value::Field::OwnerPk:
+ if (owner)
+ owner->msgpack_pack(pk);
+ else
+ InfoHash().msgpack_pack(pk);
+ break;
+ case Value::Field::SeqNum:
+ pk.pack(static_cast<uint64_t>(seq));
+ break;
+ case Value::Field::UserType:
+ pk.pack(user_type);
+ break;
+ default:
+ break;
+ }
+ }
+
+ void msgpack_unpack(const msgpack::object& o);
+ void msgpack_unpack_body(const msgpack::object& o);
+ Blob getPacked() const {
+ msgpack::sbuffer buffer;
+ msgpack::packer<msgpack::sbuffer> pk(&buffer);
+ pk.pack(*this);
+ return {buffer.data(), buffer.data()+buffer.size()};
+ }
+
+ void msgpack_unpack_fields(const std::set<Value::Field>& fields, const msgpack::object& o, unsigned offset);
+
+ Id id {INVALID_ID};
+
+ /**
+ * Public key of the signer.
+ */
+ std::shared_ptr<crypto::PublicKey> owner {};
+
+ /**
+ * Hash of the recipient (optional).
+ * Should only be present for encrypted values.
+ * Can optionally be present for signed values.
+ */
+ InfoHash recipient {};
+
+ /**
+ * Type of data.
+ */
+ ValueType::Id type {ValueType::USER_DATA.id};
+ Blob data {};
+
+ /**
+ * Custom user-defined type
+ */
+ std::string user_type {};
+
+ /**
+ * Sequence number to avoid replay attacks
+ */
+ uint16_t seq {0};
+
+ /**
+ * Optional signature.
+ */
+ Blob signature {};
+
+ /**
+ * Hold encrypted version of the data.
+ */
+ Blob cypher {};
+
+ /**
+ * Priority of this value (used for push notifications)
+ * 0 = high priority
+ * 1 = normal priority
+ */
+ unsigned priority {0};
+
+ inline bool isSignatureChecked() const {
+ return signatureChecked;
+ }
+ inline bool isDecrypted() const {
+ return decrypted;
+ }
+ bool checkSignature();
+ Sp<Value> decrypt(const crypto::PrivateKey& key);
+
+private:
+ /* Cache for crypto ops */
+ bool signatureChecked {false};
+ bool signatureValid {false};
+ bool decrypted {false};
+ Sp<Value> decryptedValue {};
+};
+
+using ValuesExport = std::pair<InfoHash, Blob>;
+
+/**
+ * @class FieldValue
+ * @brief Describes a value filter.
+ * @details
+ * This structure holds the value for a specified field. It's type can either be
+ * uint64_t, InfoHash or Blob.
+ */
+struct OPENDHT_PUBLIC FieldValue
+{
+ FieldValue() {}
+ FieldValue(Value::Field f, uint64_t int_value) : field(f), intValue(int_value) {}
+ FieldValue(Value::Field f, InfoHash hash_value) : field(f), hashValue(hash_value) {}
+ FieldValue(Value::Field f, Blob blob_value) : field(f), blobValue(std::move(blob_value)) {}
+
+ bool operator==(const FieldValue& fd) const;
+
+ // accessors
+ Value::Field getField() const { return field; }
+ uint64_t getInt() const { return intValue; }
+ InfoHash getHash() const { return hashValue; }
+ Blob getBlob() const { return blobValue; }
+
+ template <typename Packer>
+ void msgpack_pack(Packer& p) const {
+ p.pack_map(2);
+ p.pack("f"sv); p.pack(static_cast<uint8_t>(field));
+
+ p.pack("v"sv);
+ switch (field) {
+ case Value::Field::Id:
+ case Value::Field::ValueType:
+ p.pack(intValue);
+ break;
+ case Value::Field::OwnerPk:
+ p.pack(hashValue);
+ break;
+ case Value::Field::UserType:
+ p.pack_bin(blobValue.size());
+ p.pack_bin_body((const char*)blobValue.data(), blobValue.size());
+ break;
+ default:
+ throw msgpack::type_error();
+ }
+ }
+
+ void msgpack_unpack(const msgpack::object& msg) {
+ hashValue = {};
+ blobValue.clear();
+
+ if (auto f = findMapValue(msg, "f"sv))
+ field = (Value::Field)f->as<unsigned>();
+ else
+ throw msgpack::type_error();
+
+ auto v = findMapValue(msg, "v"sv);
+ if (not v)
+ throw msgpack::type_error();
+ else
+ switch (field) {
+ case Value::Field::Id:
+ case Value::Field::ValueType:
+ intValue = v->as<decltype(intValue)>();
+ break;
+ case Value::Field::OwnerPk:
+ hashValue = v->as<decltype(hashValue)>();
+ break;
+ case Value::Field::UserType:
+ blobValue = unpackBlob(*v);
+ break;
+ default:
+ throw msgpack::type_error();
+ }
+ }
+
+ Value::Filter getLocalFilter() const;
+
+private:
+ Value::Field field {Value::Field::None};
+ // three possible value types
+ uint64_t intValue {};
+ InfoHash hashValue {};
+ Blob blobValue {};
+};
+
+/**
+ * @class Select
+ * @brief Serializable Value field selection.
+ * @details
+ * This is a container for a list of FieldSelectorDescription instances. It
+ * describes a complete SELECT query for dht::Value.
+ */
+struct OPENDHT_PUBLIC Select
+{
+ Select() { }
+ Select(std::string_view q_str);
+
+ bool isSatisfiedBy(const Select& os) const;
+
+ /**
+ * Selects a field of type Value::Field.
+ *
+ * @param field the field to require.
+ *
+ * @return the resulting Select instance.
+ */
+ Select& field(Value::Field field) {
+ if (std::find(fieldSelection_.begin(), fieldSelection_.end(), field) == fieldSelection_.end())
+ fieldSelection_.emplace_back(field);
+ return *this;
+ }
+
+ /**
+ * Computes the set of selected fields based on previous require* calls.
+ *
+ * @return the set of fields.
+ */
+ std::set<Value::Field> getSelection() const {
+ return {fieldSelection_.begin(), fieldSelection_.end()};
+ }
+
+ template <typename Packer>
+ void msgpack_pack(Packer& pk) const { pk.pack(fieldSelection_); }
+ void msgpack_unpack(const msgpack::object& o) {
+ fieldSelection_ = o.as<decltype(fieldSelection_)>();
+ }
+
+ std::string toString() const {
+ std::ostringstream ss;
+ ss << *this;
+ return ss.str();
+ }
+
+ bool empty() const { return fieldSelection_.empty(); }
+
+ OPENDHT_PUBLIC friend std::ostream& operator<<(std::ostream& s, const dht::Select& q);
+private:
+ std::vector<Value::Field> fieldSelection_ {};
+};
+
+/**
+ * @class Where
+ * @brief Serializable dht::Value filter.
+ * @details
+ * This is container for a list of FieldValue instances. It describes a
+ * complete WHERE query for dht::Value.
+ */
+struct OPENDHT_PUBLIC Where
+{
+ Where() { }
+ Where(std::string_view q_str);
+
+ bool isSatisfiedBy(const Where& where) const;
+
+ /**
+ * Adds restriction on Value::Id based on the id argument.
+ *
+ * @param id the id.
+ *
+ * @return the resulting Where instance.
+ */
+ Where&& id(Value::Id id) {
+ FieldValue fv {Value::Field::Id, id};
+ if (std::find(filters_.begin(), filters_.end(), fv) == filters_.end())
+ filters_.emplace_back(std::move(fv));
+ return std::move(*this);
+ }
+
+ /**
+ * Adds restriction on Value::ValueType based on the type argument.
+ *
+ * @param type the value type.
+ *
+ * @return the resulting Where instance.
+ */
+ Where&& valueType(ValueType::Id type) {
+ FieldValue fv {Value::Field::ValueType, type};
+ if (std::find(filters_.begin(), filters_.end(), fv) == filters_.end())
+ filters_.emplace_back(std::move(fv));
+ return std::move(*this);
+ }
+
+ /**
+ * Adds restriction on Value::OwnerPk based on the owner_pk_hash argument.
+ *
+ * @param owner_pk_hash the owner public key fingerprint.
+ *
+ * @return the resulting Where instance.
+ */
+ Where&& owner(InfoHash owner_pk_hash) {
+ FieldValue fv {Value::Field::OwnerPk, owner_pk_hash};
+ if (std::find(filters_.begin(), filters_.end(), fv) == filters_.end())
+ filters_.emplace_back(std::move(fv));
+ return std::move(*this);
+ }
+
+ /**
+ * Adds restriction on Value::OwnerPk based on the owner_pk_hash argument.
+ *
+ * @param owner_pk_hash the owner public key fingerprint.
+ *
+ * @return the resulting Where instance.
+ */
+ Where&& seq(uint16_t seq_no) {
+ FieldValue fv {Value::Field::SeqNum, seq_no};
+ if (std::find(filters_.begin(), filters_.end(), fv) == filters_.end())
+ filters_.emplace_back(std::move(fv));
+ return std::move(*this);
+ }
+
+ /**
+ * Adds restriction on Value::UserType based on the user_type argument.
+ *
+ * @param user_type the user type.
+ *
+ * @return the resulting Where instance.
+ */
+ Where&& userType(std::string_view user_type) {
+ FieldValue fv {Value::Field::UserType, Blob {user_type.begin(), user_type.end()}};
+ if (std::find(filters_.begin(), filters_.end(), fv) == filters_.end())
+ filters_.emplace_back(std::move(fv));
+ return std::move(*this);
+ }
+
+ /**
+ * Computes the Value::Filter based on the list of field value set.
+ *
+ * @return the resulting Value::Filter.
+ */
+ Value::Filter getFilter() const {
+ if (filters_.empty())
+ return {};
+ if (filters_.size() == 1)
+ return filters_[0].getLocalFilter();
+ std::vector<Value::Filter> fset;
+ fset.reserve(filters_.size());
+ for (const auto& f : filters_) {
+ if (auto lf = f.getLocalFilter())
+ fset.emplace_back(std::move(lf));
+ }
+ return Value::Filter::chainAll(std::move(fset));
+ }
+
+ template <typename Packer>
+ void msgpack_pack(Packer& pk) const { pk.pack(filters_); }
+ void msgpack_unpack(const msgpack::object& o) {
+ filters_.clear();
+ filters_ = o.as<decltype(filters_)>();
+ }
+
+ std::string toString() const {
+ std::ostringstream ss;
+ ss << *this;
+ return ss.str();
+ }
+
+ bool empty() const {
+ return filters_.empty();
+ }
+
+ OPENDHT_PUBLIC friend std::ostream& operator<<(std::ostream& s, const dht::Where& q);
+
+private:
+ std::vector<FieldValue> filters_;
+};
+
+/**
+ * @class Query
+ * @brief Describes a query destined to another peer.
+ * @details
+ * This class describes the list of filters on field values and the field
+ * itselves to include in the peer response to a GET operation. See
+ * FieldValue.
+ */
+struct OPENDHT_PUBLIC Query
+{
+ static const std::string QUERY_PARSE_ERROR;
+
+ Query(Select s = {}, Where w = {}, bool none = false) : select(std::move(s)), where(std::move(w)), none(none) { };
+
+ /**
+ * Initializes a query based on a SQL-ish formatted string. The abstract
+ * form of such a string is the following:
+ *
+ * [SELECT $field$ [WHERE $field$=$value$]]
+ *
+ * where
+ *
+ * - $field$ = *|id|value_type|owner_pk|user_type
+ * - $value$ = $string$|$integer$
+ * - $string$: a simple string WITHOUT SPACES.
+ * - $integer$: a simple integer.
+ */
+ Query(std::string_view q_str) {
+ auto pos_W = q_str.find("WHERE");
+ auto pos_w = q_str.find("where");
+ auto pos = std::min(pos_W != std::string_view::npos ? pos_W : q_str.size(),
+ pos_w != std::string_view::npos ? pos_w : q_str.size());
+ select = q_str.substr(0, pos);
+ where = q_str.substr(pos, q_str.size()-pos);
+ }
+
+ /**
+ * Tell if the query is satisfied by another query.
+ */
+ bool isSatisfiedBy(const Query& q) const;
+
+ template <typename Packer>
+ void msgpack_pack(Packer& pk) const {
+ pk.pack_map(2);
+ pk.pack(std::string("s")); pk.pack(select); /* packing field selectors */
+ pk.pack(std::string("w")); pk.pack(where); /* packing filters */
+ }
+
+ void msgpack_unpack(const msgpack::object& o);
+
+ std::string toString() const {
+ std::ostringstream ss;
+ ss << *this;
+ return ss.str();
+ }
+
+ OPENDHT_PUBLIC friend std::ostream& operator<<(std::ostream& s, const dht::Query& q) {
+ return s << "Query[" << q.select << " " << q.where << "]";
+ }
+
+ Select select {};
+ Where where {};
+ bool none {false}; /* When true, any query satisfies this. */
+};
+
+/*!
+ * @class FieldValueIndex
+ * @brief An index for field values.
+ * @details
+ * This structures is meant to manipulate a subset of fields normally contained
+ * in Value.
+ */
+struct OPENDHT_PUBLIC FieldValueIndex {
+ FieldValueIndex() {}
+ FieldValueIndex(const Value& v, const Select& s = {});
+ /**
+ * Tells if all the fields of this are contained in the other
+ * FieldValueIndex with the same value.
+ *
+ * @param other The other FieldValueIndex instance.
+ */
+ bool containedIn(const FieldValueIndex& other) const;
+
+ OPENDHT_PUBLIC friend std::ostream& operator<<(std::ostream& os, const FieldValueIndex& fvi);
+
+ void msgpack_unpack_fields(const std::set<Value::Field>& fields,
+ const msgpack::object& o,
+ unsigned offset);
+
+ std::map<Value::Field, FieldValue> index {};
+};
+
+template <typename T,
+ typename std::enable_if<std::is_base_of<Value::SerializableBase, T>::value, T>::type* = nullptr>
+Value::Filter
+getFilterSet(Value::Filter f)
+{
+ return Value::Filter::chain({
+ Value::TypeFilter(T::TYPE),
+ T::getFilter(),
+ f
+ });
+}
+
+template <typename T,
+ typename std::enable_if<!std::is_base_of<Value::SerializableBase, T>::value, T>::type* = nullptr>
+Value::Filter
+getFilterSet(Value::Filter f)
+{
+ return f;
+}
+
+template <typename T,
+ typename std::enable_if<std::is_base_of<Value::SerializableBase, T>::value, T>::type* = nullptr>
+Value::Filter
+getFilterSet()
+{
+ return Value::Filter::chain({
+ Value::TypeFilter(T::TYPE),
+ T::getFilter()
+ });
+}
+
+template <typename T,
+ typename std::enable_if<!std::is_base_of<Value::SerializableBase, T>::value, T>::type* = nullptr>
+Value::Filter
+getFilterSet()
+{
+ return {};
+}
+
+template <class T>
+std::vector<T>
+unpackVector(const std::vector<std::shared_ptr<Value>>& vals) {
+ std::vector<T> ret;
+ ret.reserve(vals.size());
+ for (const auto& v : vals) {
+ try {
+ ret.emplace_back(Value::unpack<T>(*v));
+ } catch (const std::exception&) {}
+ }
+ return ret;
+}
+
+#ifdef OPENDHT_JSONCPP
+uint64_t unpackId(const Json::Value& json, const std::string& key);
+#endif
+
+}
+
+MSGPACK_ADD_ENUM(dht::Value::Field)
--- /dev/null
+# ===========================================================================
+# https://www.gnu.org/software/autoconf-archive/ax_cxx_compile_stdcxx.html
+# ===========================================================================
+#
+# SYNOPSIS
+#
+# AX_CXX_COMPILE_STDCXX(VERSION, [ext|noext], [mandatory|optional])
+#
+# DESCRIPTION
+#
+# Check for baseline language coverage in the compiler for the specified
+# version of the C++ standard. If necessary, add switches to CXX and
+# CXXCPP to enable support. VERSION may be '11', '14', '17', or '20' for
+# the respective C++ standard version.
+#
+# The second argument, if specified, indicates whether you insist on an
+# extended mode (e.g. -std=gnu++11) or a strict conformance mode (e.g.
+# -std=c++11). If neither is specified, you get whatever works, with
+# preference for no added switch, and then for an extended mode.
+#
+# The third argument, if specified 'mandatory' or if left unspecified,
+# indicates that baseline support for the specified C++ standard is
+# required and that the macro should error out if no mode with that
+# support is found. If specified 'optional', then configuration proceeds
+# regardless, after defining HAVE_CXX${VERSION} if and only if a
+# supporting mode is found.
+#
+# LICENSE
+#
+# Copyright (c) 2008 Benjamin Kosnik <bkoz@redhat.com>
+# Copyright (c) 2012 Zack Weinberg <zackw@panix.com>
+# Copyright (c) 2013 Roy Stogner <roystgnr@ices.utexas.edu>
+# Copyright (c) 2014, 2015 Google Inc.; contributed by Alexey Sokolov <sokolov@google.com>
+# Copyright (c) 2015 Paul Norman <penorman@mac.com>
+# Copyright (c) 2015 Moritz Klammler <moritz@klammler.eu>
+# Copyright (c) 2016, 2018 Krzesimir Nowak <qdlacz@gmail.com>
+# Copyright (c) 2019 Enji Cooper <yaneurabeya@gmail.com>
+# Copyright (c) 2020 Jason Merrill <jason@redhat.com>
+# Copyright (c) 2021 Jörn Heusipp <osmanx@problemloesungsmaschine.de>
+#
+# Copying and distribution of this file, with or without modification, are
+# permitted in any medium without royalty provided the copyright notice
+# and this notice are preserved. This file is offered as-is, without any
+# warranty.
+
+#serial 14
+
+dnl This macro is based on the code from the AX_CXX_COMPILE_STDCXX_11 macro
+dnl (serial version number 13).
+
+AC_DEFUN([AX_CXX_COMPILE_STDCXX], [dnl
+ m4_if([$1], [11], [ax_cxx_compile_alternatives="11 0x"],
+ [$1], [14], [ax_cxx_compile_alternatives="14 1y"],
+ [$1], [17], [ax_cxx_compile_alternatives="17 1z"],
+ [$1], [20], [ax_cxx_compile_alternatives="20"],
+ [m4_fatal([invalid first argument `$1' to AX_CXX_COMPILE_STDCXX])])dnl
+ m4_if([$2], [], [],
+ [$2], [ext], [],
+ [$2], [noext], [],
+ [m4_fatal([invalid second argument `$2' to AX_CXX_COMPILE_STDCXX])])dnl
+ m4_if([$3], [], [ax_cxx_compile_cxx$1_required=true],
+ [$3], [mandatory], [ax_cxx_compile_cxx$1_required=true],
+ [$3], [optional], [ax_cxx_compile_cxx$1_required=false],
+ [m4_fatal([invalid third argument `$3' to AX_CXX_COMPILE_STDCXX])])
+ AC_LANG_PUSH([C++])dnl
+ ac_success=no
+
+ m4_if([$2], [], [dnl
+ AC_CACHE_CHECK(whether $CXX supports C++$1 features by default,
+ ax_cv_cxx_compile_cxx$1,
+ [AC_COMPILE_IFELSE([AC_LANG_SOURCE([_AX_CXX_COMPILE_STDCXX_testbody_$1])],
+ [ax_cv_cxx_compile_cxx$1=yes],
+ [ax_cv_cxx_compile_cxx$1=no])])
+ if test x$ax_cv_cxx_compile_cxx$1 = xyes; then
+ ac_success=yes
+ fi])
+
+ m4_if([$2], [noext], [], [dnl
+ if test x$ac_success = xno; then
+ for alternative in ${ax_cxx_compile_alternatives}; do
+ switch="-std=gnu++${alternative}"
+ cachevar=AS_TR_SH([ax_cv_cxx_compile_cxx$1_$switch])
+ AC_CACHE_CHECK(whether $CXX supports C++$1 features with $switch,
+ $cachevar,
+ [ac_save_CXX="$CXX"
+ CXX="$CXX $switch"
+ AC_COMPILE_IFELSE([AC_LANG_SOURCE([_AX_CXX_COMPILE_STDCXX_testbody_$1])],
+ [eval $cachevar=yes],
+ [eval $cachevar=no])
+ CXX="$ac_save_CXX"])
+ if eval test x\$$cachevar = xyes; then
+ CXX="$CXX $switch"
+ if test -n "$CXXCPP" ; then
+ CXXCPP="$CXXCPP $switch"
+ fi
+ ac_success=yes
+ break
+ fi
+ done
+ fi])
+
+ m4_if([$2], [ext], [], [dnl
+ if test x$ac_success = xno; then
+ dnl HP's aCC needs +std=c++11 according to:
+ dnl http://h21007.www2.hp.com/portal/download/files/unprot/aCxx/PDF_Release_Notes/769149-001.pdf
+ dnl Cray's crayCC needs "-h std=c++11"
+ for alternative in ${ax_cxx_compile_alternatives}; do
+ for switch in -std=c++${alternative} +std=c++${alternative} "-h std=c++${alternative}"; do
+ cachevar=AS_TR_SH([ax_cv_cxx_compile_cxx$1_$switch])
+ AC_CACHE_CHECK(whether $CXX supports C++$1 features with $switch,
+ $cachevar,
+ [ac_save_CXX="$CXX"
+ CXX="$CXX $switch"
+ AC_COMPILE_IFELSE([AC_LANG_SOURCE([_AX_CXX_COMPILE_STDCXX_testbody_$1])],
+ [eval $cachevar=yes],
+ [eval $cachevar=no])
+ CXX="$ac_save_CXX"])
+ if eval test x\$$cachevar = xyes; then
+ CXX="$CXX $switch"
+ if test -n "$CXXCPP" ; then
+ CXXCPP="$CXXCPP $switch"
+ fi
+ ac_success=yes
+ break
+ fi
+ done
+ if test x$ac_success = xyes; then
+ break
+ fi
+ done
+ fi])
+ AC_LANG_POP([C++])
+ if test x$ax_cxx_compile_cxx$1_required = xtrue; then
+ if test x$ac_success = xno; then
+ AC_MSG_ERROR([*** A compiler with support for C++$1 language features is required.])
+ fi
+ fi
+ if test x$ac_success = xno; then
+ HAVE_CXX$1=0
+ AC_MSG_NOTICE([No compiler with C++$1 support was found])
+ else
+ HAVE_CXX$1=1
+ AC_DEFINE(HAVE_CXX$1,1,
+ [define if the compiler supports basic C++$1 syntax])
+ fi
+ AC_SUBST(HAVE_CXX$1)
+])
+
+
+dnl Test body for checking C++11 support
+
+m4_define([_AX_CXX_COMPILE_STDCXX_testbody_11],
+ _AX_CXX_COMPILE_STDCXX_testbody_new_in_11
+)
+
+dnl Test body for checking C++14 support
+
+m4_define([_AX_CXX_COMPILE_STDCXX_testbody_14],
+ _AX_CXX_COMPILE_STDCXX_testbody_new_in_11
+ _AX_CXX_COMPILE_STDCXX_testbody_new_in_14
+)
+
+dnl Test body for checking C++17 support
+
+m4_define([_AX_CXX_COMPILE_STDCXX_testbody_17],
+ _AX_CXX_COMPILE_STDCXX_testbody_new_in_11
+ _AX_CXX_COMPILE_STDCXX_testbody_new_in_14
+ _AX_CXX_COMPILE_STDCXX_testbody_new_in_17
+)
+
+dnl Test body for checking C++20 support
+
+m4_define([_AX_CXX_COMPILE_STDCXX_testbody_20],
+ _AX_CXX_COMPILE_STDCXX_testbody_new_in_11
+ _AX_CXX_COMPILE_STDCXX_testbody_new_in_14
+ _AX_CXX_COMPILE_STDCXX_testbody_new_in_17
+ _AX_CXX_COMPILE_STDCXX_testbody_new_in_20
+)
+
+
+dnl Tests for new features in C++11
+
+m4_define([_AX_CXX_COMPILE_STDCXX_testbody_new_in_11], [[
+
+// If the compiler admits that it is not ready for C++11, why torture it?
+// Hopefully, this will speed up the test.
+
+#ifndef __cplusplus
+
+#error "This is not a C++ compiler"
+
+#elif __cplusplus < 201103L
+
+#error "This is not a C++11 compiler"
+
+#else
+
+namespace cxx11
+{
+
+ namespace test_static_assert
+ {
+
+ template <typename T>
+ struct check
+ {
+ static_assert(sizeof(int) <= sizeof(T), "not big enough");
+ };
+
+ }
+
+ namespace test_final_override
+ {
+
+ struct Base
+ {
+ virtual ~Base() {}
+ virtual void f() {}
+ };
+
+ struct Derived : public Base
+ {
+ virtual ~Derived() override {}
+ virtual void f() override {}
+ };
+
+ }
+
+ namespace test_double_right_angle_brackets
+ {
+
+ template < typename T >
+ struct check {};
+
+ typedef check<void> single_type;
+ typedef check<check<void>> double_type;
+ typedef check<check<check<void>>> triple_type;
+ typedef check<check<check<check<void>>>> quadruple_type;
+
+ }
+
+ namespace test_decltype
+ {
+
+ int
+ f()
+ {
+ int a = 1;
+ decltype(a) b = 2;
+ return a + b;
+ }
+
+ }
+
+ namespace test_type_deduction
+ {
+
+ template < typename T1, typename T2 >
+ struct is_same
+ {
+ static const bool value = false;
+ };
+
+ template < typename T >
+ struct is_same<T, T>
+ {
+ static const bool value = true;
+ };
+
+ template < typename T1, typename T2 >
+ auto
+ add(T1 a1, T2 a2) -> decltype(a1 + a2)
+ {
+ return a1 + a2;
+ }
+
+ int
+ test(const int c, volatile int v)
+ {
+ static_assert(is_same<int, decltype(0)>::value == true, "");
+ static_assert(is_same<int, decltype(c)>::value == false, "");
+ static_assert(is_same<int, decltype(v)>::value == false, "");
+ auto ac = c;
+ auto av = v;
+ auto sumi = ac + av + 'x';
+ auto sumf = ac + av + 1.0;
+ static_assert(is_same<int, decltype(ac)>::value == true, "");
+ static_assert(is_same<int, decltype(av)>::value == true, "");
+ static_assert(is_same<int, decltype(sumi)>::value == true, "");
+ static_assert(is_same<int, decltype(sumf)>::value == false, "");
+ static_assert(is_same<int, decltype(add(c, v))>::value == true, "");
+ return (sumf > 0.0) ? sumi : add(c, v);
+ }
+
+ }
+
+ namespace test_noexcept
+ {
+
+ int f() { return 0; }
+ int g() noexcept { return 0; }
+
+ static_assert(noexcept(f()) == false, "");
+ static_assert(noexcept(g()) == true, "");
+
+ }
+
+ namespace test_constexpr
+ {
+
+ template < typename CharT >
+ unsigned long constexpr
+ strlen_c_r(const CharT *const s, const unsigned long acc) noexcept
+ {
+ return *s ? strlen_c_r(s + 1, acc + 1) : acc;
+ }
+
+ template < typename CharT >
+ unsigned long constexpr
+ strlen_c(const CharT *const s) noexcept
+ {
+ return strlen_c_r(s, 0UL);
+ }
+
+ static_assert(strlen_c("") == 0UL, "");
+ static_assert(strlen_c("1") == 1UL, "");
+ static_assert(strlen_c("example") == 7UL, "");
+ static_assert(strlen_c("another\0example") == 7UL, "");
+
+ }
+
+ namespace test_rvalue_references
+ {
+
+ template < int N >
+ struct answer
+ {
+ static constexpr int value = N;
+ };
+
+ answer<1> f(int&) { return answer<1>(); }
+ answer<2> f(const int&) { return answer<2>(); }
+ answer<3> f(int&&) { return answer<3>(); }
+
+ void
+ test()
+ {
+ int i = 0;
+ const int c = 0;
+ static_assert(decltype(f(i))::value == 1, "");
+ static_assert(decltype(f(c))::value == 2, "");
+ static_assert(decltype(f(0))::value == 3, "");
+ }
+
+ }
+
+ namespace test_uniform_initialization
+ {
+
+ struct test
+ {
+ static const int zero {};
+ static const int one {1};
+ };
+
+ static_assert(test::zero == 0, "");
+ static_assert(test::one == 1, "");
+
+ }
+
+ namespace test_lambdas
+ {
+
+ void
+ test1()
+ {
+ auto lambda1 = [](){};
+ auto lambda2 = lambda1;
+ lambda1();
+ lambda2();
+ }
+
+ int
+ test2()
+ {
+ auto a = [](int i, int j){ return i + j; }(1, 2);
+ auto b = []() -> int { return '0'; }();
+ auto c = [=](){ return a + b; }();
+ auto d = [&](){ return c; }();
+ auto e = [a, &b](int x) mutable {
+ const auto identity = [](int y){ return y; };
+ for (auto i = 0; i < a; ++i)
+ a += b--;
+ return x + identity(a + b);
+ }(0);
+ return a + b + c + d + e;
+ }
+
+ int
+ test3()
+ {
+ const auto nullary = [](){ return 0; };
+ const auto unary = [](int x){ return x; };
+ using nullary_t = decltype(nullary);
+ using unary_t = decltype(unary);
+ const auto higher1st = [](nullary_t f){ return f(); };
+ const auto higher2nd = [unary](nullary_t f1){
+ return [unary, f1](unary_t f2){ return f2(unary(f1())); };
+ };
+ return higher1st(nullary) + higher2nd(nullary)(unary);
+ }
+
+ }
+
+ namespace test_variadic_templates
+ {
+
+ template <int...>
+ struct sum;
+
+ template <int N0, int... N1toN>
+ struct sum<N0, N1toN...>
+ {
+ static constexpr auto value = N0 + sum<N1toN...>::value;
+ };
+
+ template <>
+ struct sum<>
+ {
+ static constexpr auto value = 0;
+ };
+
+ static_assert(sum<>::value == 0, "");
+ static_assert(sum<1>::value == 1, "");
+ static_assert(sum<23>::value == 23, "");
+ static_assert(sum<1, 2>::value == 3, "");
+ static_assert(sum<5, 5, 11>::value == 21, "");
+ static_assert(sum<2, 3, 5, 7, 11, 13>::value == 41, "");
+
+ }
+
+ // http://stackoverflow.com/questions/13728184/template-aliases-and-sfinae
+ // Clang 3.1 fails with headers of libstd++ 4.8.3 when using std::function
+ // because of this.
+ namespace test_template_alias_sfinae
+ {
+
+ struct foo {};
+
+ template<typename T>
+ using member = typename T::member_type;
+
+ template<typename T>
+ void func(...) {}
+
+ template<typename T>
+ void func(member<T>*) {}
+
+ void test();
+
+ void test() { func<foo>(0); }
+
+ }
+
+} // namespace cxx11
+
+#endif // __cplusplus >= 201103L
+
+]])
+
+
+dnl Tests for new features in C++14
+
+m4_define([_AX_CXX_COMPILE_STDCXX_testbody_new_in_14], [[
+
+// If the compiler admits that it is not ready for C++14, why torture it?
+// Hopefully, this will speed up the test.
+
+#ifndef __cplusplus
+
+#error "This is not a C++ compiler"
+
+#elif __cplusplus < 201402L
+
+#error "This is not a C++14 compiler"
+
+#else
+
+namespace cxx14
+{
+
+ namespace test_polymorphic_lambdas
+ {
+
+ int
+ test()
+ {
+ const auto lambda = [](auto&&... args){
+ const auto istiny = [](auto x){
+ return (sizeof(x) == 1UL) ? 1 : 0;
+ };
+ const int aretiny[] = { istiny(args)... };
+ return aretiny[0];
+ };
+ return lambda(1, 1L, 1.0f, '1');
+ }
+
+ }
+
+ namespace test_binary_literals
+ {
+
+ constexpr auto ivii = 0b0000000000101010;
+ static_assert(ivii == 42, "wrong value");
+
+ }
+
+ namespace test_generalized_constexpr
+ {
+
+ template < typename CharT >
+ constexpr unsigned long
+ strlen_c(const CharT *const s) noexcept
+ {
+ auto length = 0UL;
+ for (auto p = s; *p; ++p)
+ ++length;
+ return length;
+ }
+
+ static_assert(strlen_c("") == 0UL, "");
+ static_assert(strlen_c("x") == 1UL, "");
+ static_assert(strlen_c("test") == 4UL, "");
+ static_assert(strlen_c("another\0test") == 7UL, "");
+
+ }
+
+ namespace test_lambda_init_capture
+ {
+
+ int
+ test()
+ {
+ auto x = 0;
+ const auto lambda1 = [a = x](int b){ return a + b; };
+ const auto lambda2 = [a = lambda1(x)](){ return a; };
+ return lambda2();
+ }
+
+ }
+
+ namespace test_digit_separators
+ {
+
+ constexpr auto ten_million = 100'000'000;
+ static_assert(ten_million == 100000000, "");
+
+ }
+
+ namespace test_return_type_deduction
+ {
+
+ auto f(int& x) { return x; }
+ decltype(auto) g(int& x) { return x; }
+
+ template < typename T1, typename T2 >
+ struct is_same
+ {
+ static constexpr auto value = false;
+ };
+
+ template < typename T >
+ struct is_same<T, T>
+ {
+ static constexpr auto value = true;
+ };
+
+ int
+ test()
+ {
+ auto x = 0;
+ static_assert(is_same<int, decltype(f(x))>::value, "");
+ static_assert(is_same<int&, decltype(g(x))>::value, "");
+ return x;
+ }
+
+ }
+
+} // namespace cxx14
+
+#endif // __cplusplus >= 201402L
+
+]])
+
+
+dnl Tests for new features in C++17
+
+m4_define([_AX_CXX_COMPILE_STDCXX_testbody_new_in_17], [[
+
+// If the compiler admits that it is not ready for C++17, why torture it?
+// Hopefully, this will speed up the test.
+
+#ifndef __cplusplus
+
+#error "This is not a C++ compiler"
+
+#elif __cplusplus < 201703L
+
+#error "This is not a C++17 compiler"
+
+#else
+
+#include <initializer_list>
+#include <utility>
+#include <type_traits>
+
+namespace cxx17
+{
+
+ namespace test_constexpr_lambdas
+ {
+
+ constexpr int foo = [](){return 42;}();
+
+ }
+
+ namespace test::nested_namespace::definitions
+ {
+
+ }
+
+ namespace test_fold_expression
+ {
+
+ template<typename... Args>
+ int multiply(Args... args)
+ {
+ return (args * ... * 1);
+ }
+
+ template<typename... Args>
+ bool all(Args... args)
+ {
+ return (args && ...);
+ }
+
+ }
+
+ namespace test_extended_static_assert
+ {
+
+ static_assert (true);
+
+ }
+
+ namespace test_auto_brace_init_list
+ {
+
+ auto foo = {5};
+ auto bar {5};
+
+ static_assert(std::is_same<std::initializer_list<int>, decltype(foo)>::value);
+ static_assert(std::is_same<int, decltype(bar)>::value);
+ }
+
+ namespace test_typename_in_template_template_parameter
+ {
+
+ template<template<typename> typename X> struct D;
+
+ }
+
+ namespace test_fallthrough_nodiscard_maybe_unused_attributes
+ {
+
+ int f1()
+ {
+ return 42;
+ }
+
+ [[nodiscard]] int f2()
+ {
+ [[maybe_unused]] auto unused = f1();
+
+ switch (f1())
+ {
+ case 17:
+ f1();
+ [[fallthrough]];
+ case 42:
+ f1();
+ }
+ return f1();
+ }
+
+ }
+
+ namespace test_extended_aggregate_initialization
+ {
+
+ struct base1
+ {
+ int b1, b2 = 42;
+ };
+
+ struct base2
+ {
+ base2() {
+ b3 = 42;
+ }
+ int b3;
+ };
+
+ struct derived : base1, base2
+ {
+ int d;
+ };
+
+ derived d1 {{1, 2}, {}, 4}; // full initialization
+ derived d2 {{}, {}, 4}; // value-initialized bases
+
+ }
+
+ namespace test_general_range_based_for_loop
+ {
+
+ struct iter
+ {
+ int i;
+
+ int& operator* ()
+ {
+ return i;
+ }
+
+ const int& operator* () const
+ {
+ return i;
+ }
+
+ iter& operator++()
+ {
+ ++i;
+ return *this;
+ }
+ };
+
+ struct sentinel
+ {
+ int i;
+ };
+
+ bool operator== (const iter& i, const sentinel& s)
+ {
+ return i.i == s.i;
+ }
+
+ bool operator!= (const iter& i, const sentinel& s)
+ {
+ return !(i == s);
+ }
+
+ struct range
+ {
+ iter begin() const
+ {
+ return {0};
+ }
+
+ sentinel end() const
+ {
+ return {5};
+ }
+ };
+
+ void f()
+ {
+ range r {};
+
+ for (auto i : r)
+ {
+ [[maybe_unused]] auto v = i;
+ }
+ }
+
+ }
+
+ namespace test_lambda_capture_asterisk_this_by_value
+ {
+
+ struct t
+ {
+ int i;
+ int foo()
+ {
+ return [*this]()
+ {
+ return i;
+ }();
+ }
+ };
+
+ }
+
+ namespace test_enum_class_construction
+ {
+
+ enum class byte : unsigned char
+ {};
+
+ byte foo {42};
+
+ }
+
+ namespace test_constexpr_if
+ {
+
+ template <bool cond>
+ int f ()
+ {
+ if constexpr(cond)
+ {
+ return 13;
+ }
+ else
+ {
+ return 42;
+ }
+ }
+
+ }
+
+ namespace test_selection_statement_with_initializer
+ {
+
+ int f()
+ {
+ return 13;
+ }
+
+ int f2()
+ {
+ if (auto i = f(); i > 0)
+ {
+ return 3;
+ }
+
+ switch (auto i = f(); i + 4)
+ {
+ case 17:
+ return 2;
+
+ default:
+ return 1;
+ }
+ }
+
+ }
+
+ namespace test_template_argument_deduction_for_class_templates
+ {
+
+ template <typename T1, typename T2>
+ struct pair
+ {
+ pair (T1 p1, T2 p2)
+ : m1 {p1},
+ m2 {p2}
+ {}
+
+ T1 m1;
+ T2 m2;
+ };
+
+ void f()
+ {
+ [[maybe_unused]] auto p = pair{13, 42u};
+ }
+
+ }
+
+ namespace test_non_type_auto_template_parameters
+ {
+
+ template <auto n>
+ struct B
+ {};
+
+ B<5> b1;
+ B<'a'> b2;
+
+ }
+
+ namespace test_structured_bindings
+ {
+
+ int arr[2] = { 1, 2 };
+ std::pair<int, int> pr = { 1, 2 };
+
+ auto f1() -> int(&)[2]
+ {
+ return arr;
+ }
+
+ auto f2() -> std::pair<int, int>&
+ {
+ return pr;
+ }
+
+ struct S
+ {
+ int x1 : 2;
+ volatile double y1;
+ };
+
+ S f3()
+ {
+ return {};
+ }
+
+ auto [ x1, y1 ] = f1();
+ auto& [ xr1, yr1 ] = f1();
+ auto [ x2, y2 ] = f2();
+ auto& [ xr2, yr2 ] = f2();
+ const auto [ x3, y3 ] = f3();
+
+ }
+
+ namespace test_exception_spec_type_system
+ {
+
+ struct Good {};
+ struct Bad {};
+
+ void g1() noexcept;
+ void g2();
+
+ template<typename T>
+ Bad
+ f(T*, T*);
+
+ template<typename T1, typename T2>
+ Good
+ f(T1*, T2*);
+
+ static_assert (std::is_same_v<Good, decltype(f(g1, g2))>);
+
+ }
+
+ namespace test_inline_variables
+ {
+
+ template<class T> void f(T)
+ {}
+
+ template<class T> inline T g(T)
+ {
+ return T{};
+ }
+
+ template<> inline void f<>(int)
+ {}
+
+ template<> int g<>(int)
+ {
+ return 5;
+ }
+
+ }
+
+} // namespace cxx17
+
+#endif // __cplusplus < 201703L
+
+]])
+
+
+dnl Tests for new features in C++20
+
+m4_define([_AX_CXX_COMPILE_STDCXX_testbody_new_in_20], [[
+
+#ifndef __cplusplus
+
+#error "This is not a C++ compiler"
+
+#elif __cplusplus < 202002L
+
+#error "This is not a C++20 compiler"
+
+#else
+
+#include <version>
+
+namespace cxx20
+{
+
+// As C++20 supports feature test macros in the standard, there is no
+// immediate need to actually test for feature availability on the
+// Autoconf side.
+
+} // namespace cxx20
+
+#endif // __cplusplus < 202002L
+
+]])
--- /dev/null
+project('opendht', 'c', 'cpp',
+ version: '3.0.0',
+ default_options: [
+ 'cpp_std=c++17',
+ 'warning_level=3'
+ ])
+
+gnutls = dependency('gnutls')
+nettle = dependency('nettle')
+msgpack = dependency('msgpack-cxx', required : false)
+argon2 = dependency('libargon2')
+openssl = dependency('openssl', required: get_option('proxy_client'))
+jsoncpp = dependency('jsoncpp', required: get_option('proxy_client'))
+fmt = dependency('fmt')
+
+dirs=[]
+if host_machine.system() == 'freebsd'
+ dirs+='/usr/local/lib'
+elif host_machine.system() == 'darwin'
+ dirs+='/opt/homebrew/lib'
+endif
+http_parser = meson.get_compiler('c').find_library('http_parser', dirs: dirs, required: get_option('proxy_client'))
+deps = [fmt, gnutls, nettle, msgpack, argon2, openssl, jsoncpp, http_parser]
+
+add_project_arguments('-DMSGPACK_NO_BOOST', language : 'cpp')
+add_project_arguments(['-Wno-return-type','-Wno-deprecated','-Wnon-virtual-dtor','-pedantic-errors','-fvisibility=hidden'], language : 'cpp')
+
+opendht_inc = include_directories('include/opendht')
+opendht_interface_inc = include_directories('include', is_system: true)
+opendht_src = [
+ 'src/utils.cpp',
+ 'src/crypto.cpp',
+ 'src/default_types.cpp',
+ 'src/node.cpp',
+ 'src/value.cpp',
+ 'src/dht.cpp',
+ 'src/callbacks.cpp',
+ 'src/routing_table.cpp',
+ 'src/node_cache.cpp',
+ 'src/network_engine.cpp',
+ 'src/securedht.cpp',
+ 'src/dhtrunner.cpp',
+ 'src/log.cpp',
+ 'src/op_cache.cpp',
+ 'src/network_utils.cpp'
+]
+if jsoncpp.found()
+ opendht_src += ['src/base64.cpp']
+ add_project_arguments('-DOPENDHT_JSONCPP', language : 'cpp')
+endif
+if http_parser.found()
+ opendht_src += ['src/http.cpp', 'src/compat/os_cert.cpp']
+ if host_machine.system() == 'darwin'
+ deps+=dependency('appleframeworks', modules : ['CoreFoundation', 'Security'])
+ endif
+endif
+if get_option('proxy_client').enabled()
+ opendht_src += ['src/dht_proxy_client.cpp']
+ add_project_arguments('-DOPENDHT_PROXY_CLIENT', language : 'cpp')
+endif
+if get_option('proxy_server').enabled()
+ opendht_src += 'src/dht_proxy_server.cpp'
+ add_project_arguments('-DOPENDHT_PROXY_SERVER', language : 'cpp')
+endif
+if get_option('peer_discovery').enabled()
+ opendht_src += 'src/peer_discovery.cpp'
+ add_project_arguments('-DOPENDHT_PEER_DISCOVERY', language : 'cpp')
+endif
+opendht = shared_library('opendht',
+ opendht_src,
+ include_directories : opendht_inc,
+ dependencies : deps,
+ cpp_args : ['-DOPENHT_BUILD', '-Dopendht_EXPORTS'],
+ install : true)
+
+readline = meson.get_compiler('c').find_library('readline', required: get_option('tools'))
+if get_option('tools').enabled()
+ dhtnode = executable('dhtnode', 'tools/dhtnode.cpp',
+ include_directories : opendht_interface_inc,
+ link_with : opendht,
+ dependencies : [readline, msgpack, fmt],
+ install : true)
+ dhtchat = executable('dhtchat', 'tools/dhtchat.cpp',
+ include_directories : opendht_interface_inc,
+ link_with : opendht,
+ dependencies : [readline, msgpack, fmt],
+ install : true)
+ dhtscanner = executable('dhtscanner', 'tools/dhtscanner.cpp',
+ include_directories : opendht_interface_inc,
+ link_with : opendht,
+ dependencies : [readline, msgpack, fmt],
+ install : true)
+ if http_parser.found()
+ durl = executable('durl', 'tools/durl.cpp',
+ include_directories : opendht_interface_inc,
+ link_with : opendht,
+ dependencies : [msgpack, openssl])
+ endif
+endif
--- /dev/null
+option('proxy_client', type : 'feature', value : 'disabled')
+option('proxy_server', type : 'feature', value : 'disabled')
+option('peer_discovery', type : 'feature', value : 'enabled')
+option('tools', type : 'feature', value : 'enabled')
--- /dev/null
+prefix=@prefix@
+exec_prefix=@exec_prefix@
+libdir=@libdir@
+includedir=@includedir@
+Name: OpenDHT (C Wrapper)
+Description: Distributed Hash Table library (C Wrapper)
+Version: @VERSION@
+Libs: -L${libdir} -lopendht-c
+Requires.private: opendht gnutls >= 3.1 @argon2_lib@
+Cflags: -I${includedir}
--- /dev/null
+prefix=@prefix@
+exec_prefix=@exec_prefix@
+libdir=@libdir@
+includedir=@includedir@
+Name: OpenDHT
+Description: C++17 Distributed Hash Table library
+Version: @VERSION@
+Libs: -L${libdir} -lopendht
+Libs.private: @http_parser_lib@ -pthread
+Requires: gnutls >= 3.3@jsoncpp_lib@@openssl_lib@
+Requires.private: nettle >= 2.4@argon2_lib@
+Cflags: -I${includedir}
--- /dev/null
+
+set(CURRENT_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR})
+set(CURRENT_BINARY_DIR ${CMAKE_CURRENT_BINARY_DIR})
+
+configure_file(setup.py.in setup.py)
+configure_file(pyproject.toml pyproject.toml COPYONLY)
+
+add_custom_target(python ALL
+ COMMAND python3 setup.py build
+ DEPENDS opendht opendht_cpp.pxd opendht.pyx pyproject.toml)
+
+add_custom_target(dist
+ COMMAND python3 -m build
+ DEPENDS opendht opendht_cpp.pxd opendht.pyx pyproject.toml)
+
+install(CODE "execute_process(COMMAND python3 setup.py install --root=\$ENV{DESTDIR}/ WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR})")
+if (OPENDHT_TOOLS)
+ install(PROGRAMS tools/dhtcluster.py DESTINATION ${CMAKE_INSTALL_BINDIR} RENAME dhtcluster)
+endif()
\ No newline at end of file
--- /dev/null
+if USE_CYTHON
+
+noinst_HEADERS = \
+ opendht.pyx \
+ opendht_cpp.pxd \
+ pyproject.toml
+
+PYTHON_INSTALL_RECORD = $(builddir)/install_record.txt
+
+pybuild.stamp:
+ LDFLAGS="-L$(top_srcdir)/src/.libs" $(PYTHON) setup.py build_ext --inplace
+ echo stamp > pybuild.stamp
+
+CLEANFILES = pybuild.stamp
+
+all-local: pybuild.stamp
+clean-local:
+ rm -rf $(builddir)/build $(builddir)/*.so $(PYTHON_INSTALL_RECORD)
+
+install-exec-local:
+ $(PYTHON) setup.py install --root=$(DESTDIR)/ --record $(PYTHON_INSTALL_RECORD)
+ rm -rf $(builddir)/build
+
+if HAVE_PIP
+uninstall-local:
+ /usr/bin/yes | $(PIP) uninstall $(PACKAGE)
+endif
+
+endif
+
--- /dev/null
+# distutils: language = c++
+# distutils: extra_compile_args = -std=c++17
+# distutils: include_dirs = ../../include
+# distutils: library_dirs = ../../src
+# distutils: libraries = opendht gnutls
+# cython: language_level=3
+#
+# Copyright (c) 2015-2023 Savoir-faire Linux Inc.
+# Author(s): Guillaume Roguez <guillaume.roguez@savoirfairelinux.com>
+# Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+# Simon Désaulniers <sim.desaulniers@gmail.com>
+#
+# This wrapper is written for Cython 0.22
+#
+# This file is part of OpenDHT Python Wrapper.
+#
+# OpenDHT Python Wrapper is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# OpenDHT Python Wrapper is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with OpenDHT Python Wrapper. If not, see <https://www.gnu.org/licenses/>.
+
+from libcpp.map cimport map as map
+from libcpp cimport bool
+from libcpp.utility cimport pair
+from libcpp.string cimport string
+from libcpp.memory cimport shared_ptr
+
+from cython.parallel import parallel, prange
+from cython.operator cimport dereference as deref, preincrement as inc, predecrement as dec
+from cpython cimport ref
+from datetime import timedelta
+
+cimport opendht_cpp as cpp
+
+import threading
+
+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']:
+ vals = []
+ for val in deref(values):
+ v = IndexValue()
+ v._value = val
+ vals.append(v)
+ cbs['lookup'](vals, p.toString())
+
+cdef inline void shutdown_callback(void* user_data) noexcept with gil:
+ cbs = <object>user_data
+ if 'shutdown' in cbs and cbs['shutdown']:
+ cbs['shutdown']()
+ ref.Py_DECREF(cbs)
+
+cdef inline bool get_callback(shared_ptr[cpp.Value] value, void *user_data) noexcept with gil:
+ cbs = <object>user_data
+ cb = cbs['get']
+ f = cbs['filter'] if 'filter' in cbs else None
+ pv = Value()
+ pv._value = value
+ return cb(pv) if not f or f(pv) else True
+
+cdef inline bool value_callback(shared_ptr[cpp.Value] value, bool expired, void *user_data) noexcept with gil:
+ cbs = <object>user_data
+ cb = cbs['valcb']
+ f = cbs['filter'] if 'filter' in cbs else None
+ pv = Value()
+ pv._value = value
+ return cb(pv, expired) if not f or f(pv) else True
+
+cdef inline void done_callback(bool done, cpp.vector[shared_ptr[cpp.Node]]* nodes, void *user_data) noexcept with gil:
+ node_ids = []
+ for n in deref(nodes):
+ h = NodeEntry()
+ h._v.first = n.get().getId()
+ h._v.second = n
+ node_ids.append(h)
+ cbs = <object>user_data
+ if 'done' in cbs and cbs['done']:
+ cbs['done'](done, node_ids)
+ ref.Py_DECREF(cbs)
+
+cdef inline void done_callback_simple(bool done, void *user_data) noexcept with gil:
+ cbs = <object>user_data
+ if 'done' in cbs and cbs['done']:
+ cbs['done'](done)
+ ref.Py_DECREF(cbs)
+
+cdef class _WithID(object):
+ def __repr__(self):
+ return "<%s '%s'>" % (self.__class__.__name__, str(self))
+ def __str__(self):
+ return self.getId().toString().decode()
+
+cdef class InfoHash(_WithID):
+ cdef cpp.InfoHash _infohash
+ def __cinit__(self, bytes str=b''):
+ self._infohash = cpp.InfoHash(str) if str else cpp.InfoHash()
+ def __bool__(InfoHash self):
+ return <bool>self._infohash
+ def __richcmp__(InfoHash self, InfoHash other, int op):
+ if op == 0:
+ return self._infohash < other._infohash
+ if op == 1:
+ return self._infohash < other._infohash or self._infohash == other._infohash
+ if op == 2:
+ return self._infohash == other._infohash
+ return NotImplemented
+ def getBit(InfoHash self, bit):
+ return self._infohash.getBit(bit)
+ def setBit(InfoHash self, bit, b):
+ self._infohash.setBit(bit, b)
+ def getId(InfoHash self):
+ return self
+ def toString(InfoHash self):
+ return self._infohash.toString()
+ def toFloat(InfoHash self):
+ return self._infohash.toFloat()
+ @staticmethod
+ def commonBits(InfoHash a, InfoHash b):
+ return cpp.InfoHash.commonBits(a._infohash, b._infohash)
+ @staticmethod
+ def get(str key):
+ h = InfoHash()
+ h._infohash = cpp.InfoHash.get(key.encode())
+ return h
+ @staticmethod
+ def getRandom():
+ h = InfoHash()
+ h._infohash = cpp.InfoHash.getRandom()
+ return h
+
+cdef class SockAddr(object):
+ cdef cpp.SockAddr _addr
+ def toString(SockAddr self):
+ return self._addr.toString()
+ def getPort(SockAddr self):
+ return self._addr.getPort()
+ def getFamily(SockAddr self):
+ return self._addr.getFamily()
+ def setPort(SockAddr self, cpp.in_port_t port):
+ return self._addr.setPort(port)
+ def setFamily(SockAddr self, cpp.sa_family_t af):
+ return self._addr.setFamily(af)
+ def isLoopback(SockAddr self):
+ return self._addr.isLoopback()
+ def isPrivate(SockAddr self):
+ return self._addr.isPrivate()
+ def isUnspecified(SockAddr self):
+ return self._addr.isUnspecified()
+ def __str__(self):
+ return self.toString().decode()
+ def __repr__(self):
+ return "<%s '%s'>" % (self.__class__.__name__, str(self))
+
+cdef class Node(_WithID):
+ cdef shared_ptr[cpp.Node] _node
+ def getId(self):
+ h = InfoHash()
+ h._infohash = self._node.get().getId()
+ return h
+ def getAddr(self):
+ return self._node.get().getAddrStr()
+ def isExpired(self):
+ return self._node.get().isExpired()
+
+cdef class NodeEntry(_WithID):
+ cdef cpp.pair[cpp.InfoHash, shared_ptr[cpp.Node]] _v
+ def getId(self):
+ h = InfoHash()
+ h._infohash = self._v.first
+ return h
+ def getNode(self):
+ n = Node()
+ n._node = self._v.second
+ return n
+
+cdef class Query(object):
+ cdef cpp.Query _query
+ def __cinit__(self, str q_str=''):
+ self._query = cpp.Query(q_str.encode())
+ def __str__(self):
+ return self._query.toString().decode()
+ def buildFrom(self, Select s, Where w):
+ self._query = cpp.Query(s._select, w._where)
+ def isSatisfiedBy(self, Query q):
+ return self._query.isSatisfiedBy(q._query)
+
+cdef class Select(object):
+ cdef cpp.Select _select
+ def __cinit__(self, str q_str=None):
+ if q_str:
+ self._select = cpp.Select(q_str.encode())
+ else:
+ self._select = cpp.Select()
+ def __str__(self):
+ return self._select.toString().decode()
+ def isSatisfiedBy(self, Select os):
+ return self._select.isSatisfiedBy(os._select)
+ def field(self, int field):
+ self._select.field(<cpp.Field> field)
+ return self
+
+cdef class Where(object):
+ cdef cpp.Where _where
+ def __cinit__(self, str q_str=None):
+ if q_str:
+ self._where = cpp.Where(q_str.encode())
+ else:
+ self._where = cpp.Where()
+ def __str__(self):
+ return self._where.toString().decode()
+ def isSatisfiedBy(self, Where where):
+ return self._where.isSatisfiedBy(where._where)
+ def id(self, cpp.uint64_t id):
+ self._where.id(id)
+ return self
+ def valueType(self, cpp.uint16_t type):
+ self._where.valueType(type)
+ return self
+ def owner(self, InfoHash owner_pk_hash):
+ self._where.owner(owner_pk_hash._infohash)
+ return self
+ def seq(self, cpp.uint16_t seq_no):
+ self._where.seq(seq_no)
+ return self
+ def userType(self, str user_type):
+ self._where.userType(user_type.encode())
+ return self
+
+cdef class Value(object):
+ cdef shared_ptr[cpp.Value] _value
+ def __init__(self, bytes val=b'', cpp.uint16_t id=0):
+ self._value.reset(new cpp.Value(id, val, len(val)))
+
+ def __str__(self):
+ return self._value.get().toString().decode()
+ property owner:
+ def __get__(self):
+ h = InfoHash()
+ h._infohash = self._value.get().owner.get().getId()
+ return h
+ property recipient:
+ def __get__(self):
+ h = InfoHash()
+ h._infohash = self._value.get().recipient
+ return h
+ def __set__(self, InfoHash h):
+ self._value.get().recipient = h._infohash
+ property data:
+ def __get__(self):
+ return string(<char*>self._value.get().data.data(), self._value.get().data.size())
+ def __set__(self, bytes value):
+ self._value.get().data = value
+ property user_type:
+ def __get__(self):
+ return self._value.get().user_type.decode()
+ def __set__(self, str t):
+ self._value.get().user_type = t.encode()
+ property id:
+ def __get__(self):
+ return self._value.get().id
+ def __set__(self, cpp.uint64_t value):
+ self._value.get().id = value
+ property size:
+ def __get__(self):
+ return self._value.get().size()
+
+cdef class ValueType(object):
+ 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 = new cpp.ValueType(id, name.encode(), duration)
+
+cdef class NodeSetIter(object):
+ cdef map[cpp.InfoHash, shared_ptr[cpp.Node]]* _nodes
+ cdef map[cpp.InfoHash, shared_ptr[cpp.Node]].iterator _curIter
+ def __init__(self, NodeSet s):
+ self._nodes = &s._nodes
+ self._curIter = self._nodes.begin()
+ def __next__(self):
+ if self._curIter == self._nodes.end():
+ raise StopIteration
+ h = NodeEntry()
+ h._v = deref(self._curIter)
+ inc(self._curIter)
+ return h
+
+cdef class NodeSet(object):
+ cdef map[cpp.InfoHash, shared_ptr[cpp.Node]] _nodes
+ def size(self):
+ return self._nodes.size()
+ def insert(self, NodeEntry l):
+ return self._nodes.insert(l._v).second
+ def extend(self, li):
+ for n in li:
+ self.insert(n)
+ def first(self):
+ if self._nodes.empty():
+ raise IndexError()
+ h = InfoHash()
+ h._infohash = deref(self._nodes.begin()).first
+ return h
+ def last(self):
+ if self._nodes.empty():
+ raise IndexError()
+ h = InfoHash()
+ h._infohash = deref(dec(self._nodes.end())).first
+ return h
+ def __str__(self):
+ s = ''
+ cdef map[cpp.InfoHash, shared_ptr[cpp.Node]].iterator it = self._nodes.begin()
+ while it != self._nodes.end():
+ s += deref(it).first.toString().decode() + ' ' + deref(it).second.get().getAddrStr().decode() + '\n'
+ inc(it)
+ return s
+ def __iter__(self):
+ return NodeSetIter(self)
+
+cdef class PrivateKey(_WithID):
+ cdef shared_ptr[cpp.PrivateKey] _key
+ def getId(self):
+ h = InfoHash()
+ h._infohash = self._key.get().getSharedPublicKey().get().getId()
+ return h
+ def getPublicKey(self):
+ pk = PublicKey()
+ pk._key = self._key.get().getSharedPublicKey()
+ return pk
+ def decrypt(self, bytes dat):
+ cdef size_t d_len = len(dat)
+ cdef cpp.uint8_t* d_ptr = <cpp.uint8_t*>dat
+ cdef cpp.Blob indat
+ indat.assign(d_ptr, <cpp.uint8_t*>(d_ptr + d_len))
+ cdef cpp.Blob decrypted = self._key.get().decrypt(indat)
+ cdef char* decrypted_c_str = <char *>decrypted.data()
+ cdef Py_ssize_t length = decrypted.size()
+ return decrypted_c_str[:length]
+ def __str__(self):
+ return self.getId().toString().decode()
+ @staticmethod
+ def generate():
+ k = PrivateKey()
+ k._key = cpp.make_shared[cpp.PrivateKey](cpp.PrivateKey.generate())
+ return k
+ @staticmethod
+ def generateEC():
+ k = PrivateKey()
+ k._key = cpp.make_shared[cpp.PrivateKey](cpp.PrivateKey.generateEC())
+ return k
+
+cdef class PublicKey(_WithID):
+ cdef cpp.shared_ptr[cpp.PublicKey] _key
+ def getId(self):
+ h = InfoHash()
+ h._infohash = self._key.get().getId()
+ return h
+ def encrypt(self, bytes dat):
+ cdef size_t d_len = len(dat)
+ cdef cpp.uint8_t* d_ptr = <cpp.uint8_t*>dat
+ cdef cpp.Blob indat
+ indat.assign(d_ptr, <cpp.uint8_t*>(d_ptr + d_len))
+ cdef cpp.Blob encrypted = self._key.get().encrypt(indat)
+ cdef char* encrypted_c_str = <char *>encrypted.data()
+ cdef Py_ssize_t length = encrypted.size()
+ return encrypted_c_str[:length]
+
+cdef class Certificate(_WithID):
+ cdef shared_ptr[cpp.Certificate] _cert
+ def __init__(self, bytes dat = None):
+ if dat:
+ self._cert = cpp.make_shared[cpp.Certificate](<cpp.string>dat)
+ def getId(self):
+ h = InfoHash()
+ if self._cert:
+ h._infohash = self._cert.get().getId()
+ return h
+ def toString(self):
+ return self._cert.get().toString().decode()
+ def getName(self):
+ return self._cert.get().getName()
+ def revoke(self, PrivateKey k, Certificate c):
+ 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''
+ property issuer:
+ def __get__(self):
+ c = Certificate()
+ c._cert = self._cert.get().issuer
+ return c;
+ @staticmethod
+ def generate(PrivateKey k, str name, Identity i = Identity(), bool is_ca = False):
+ c = Certificate()
+ c._cert = cpp.make_shared[cpp.Certificate](cpp.Certificate.generate(deref(k._key.get()), name.encode(), i._id, is_ca))
+ return c
+
+cdef class VerifyResult(object):
+ cdef cpp.TrustListVerifyResult _result
+ def __bool__(self):
+ return self._result.isValid()
+ def __str(self):
+ return self._result.toString()
+
+cdef class TrustList(object):
+ cdef cpp.TrustList _trust
+ def add(self, Certificate cert):
+ self._trust.add(deref(cert._cert.get()))
+ def remove(self, Certificate cert):
+ self._trust.remove(deref(cert._cert.get()))
+ def verify(self, Certificate cert):
+ r = VerifyResult()
+ r._result = self._trust.verify(deref(cert._cert.get()))
+ return r
+
+cdef class ListenToken(object):
+ cdef cpp.InfoHash _h
+ cdef cpp.shared_future[size_t] _t
+ _cb = dict()
+
+cdef class Identity(object):
+ cdef cpp.Identity _id
+ def __init__(self, PrivateKey k = None, Certificate c = None):
+ if k:
+ self._id.first = k._key
+ if c:
+ self._id.second = c._cert
+ @staticmethod
+ def generate(str name = "pydht", Identity ca = Identity(), unsigned bits = 4096):
+ i = Identity()
+ i._id = cpp.generateIdentity(name.encode(), ca._id, bits)
+ return i
+ property publickey:
+ def __get__(self):
+ k = PublicKey()
+ k._key = self._id.first.get().getSharedPublicKey()
+ return k
+ property certificate:
+ def __get__(self):
+ c = Certificate()
+ c._cert = self._id.second
+ return c
+ property key:
+ def __get__(self):
+ k = PrivateKey()
+ k._key = self._id.first
+ return k
+
+cdef class DhtConfig(object):
+ cdef cpp.DhtRunnerConfig _config
+ def __init__(self):
+ self._config = cpp.DhtRunnerConfig()
+ self._config.threaded = True;
+ def setIdentity(self, Identity id):
+ self._config.dht_config.id = id._id
+ def setBootstrapMode(self, bool bootstrap):
+ self._config.dht_config.node_config.is_bootstrap = bootstrap
+ def setNodeId(self, InfoHash id):
+ self._config.dht_config.node_config.node_id = id._infohash
+ def setNetwork(self, unsigned netid):
+ 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 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
+
+cdef class DhtRunner(_WithID):
+ cdef cpp.shared_ptr[cpp.DhtRunner] thisptr
+ def __cinit__(self):
+ self.thisptr.reset(new cpp.DhtRunner())
+ def getId(self):
+ h = InfoHash()
+ if self.thisptr:
+ h._infohash = self.thisptr.get().getId()
+ return h
+ def getNodeId(self):
+ return self.thisptr.get().getNodeId().toString()
+ def ping(self, SockAddr addr, done_cb=None):
+ if done_cb:
+ cb_obj = {'done':done_cb}
+ ref.Py_INCREF(cb_obj)
+ self.thisptr.get().bootstrap(addr._addr, cpp.bindDoneCbSimple(done_callback_simple, <void*>cb_obj))
+ else:
+ lock = threading.Condition()
+ pending = 0
+ ok = False
+ def tmp_done(ok_ret):
+ nonlocal pending, ok, lock
+ with lock:
+ ok = ok_ret
+ pending -= 1
+ lock.notify()
+ with lock:
+ pending += 1
+ self.ping(addr, done_cb=tmp_done)
+ while pending > 0:
+ lock.wait()
+ return ok
+ def bootstrap(self, str host, str port=None):
+ host_bytes = host.encode()
+ port_bytes = port.encode() if port else b'4222'
+ self.thisptr.get().bootstrap(<cpp.const_char*>host_bytes, <cpp.const_char*>port_bytes)
+ def run(self, Identity id=None, is_bootstrap=False, cpp.in_port_t port=0, str ipv4="", str ipv6="", DhtConfig config=DhtConfig()):
+ if id:
+ config.setIdentity(id)
+ if ipv4 or ipv6:
+ bind4 = ipv4.encode() if ipv4 else b''
+ bind6 = ipv6.encode() if ipv6 else b''
+ self.thisptr.get().run(bind4, bind6, str(port).encode(), config._config)
+ else:
+ self.thisptr.get().run(port, config._config)
+ def join(self):
+ 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):
+ self.thisptr.get().registerType(deref(type._value))
+ def enableLogging(self):
+ cpp.enableLogging(self.thisptr.get()[0])
+ def disableLogging(self):
+ cpp.disableLogging(self.thisptr.get()[0])
+ def enableFileLogging(self, str 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()
+ s._addr = self.thisptr.get().getBound(af)
+ return s
+ def getStorageLog(self):
+ return self.thisptr.get().getStorageLog().decode()
+ def getRoutingTablesLog(self, cpp.sa_family_t af):
+ return self.thisptr.get().getRoutingTablesLog(af).decode()
+ def getSearchesLog(self, cpp.sa_family_t af):
+ return self.thisptr.get().getSearchesLog(af).decode()
+ def getNodeMessageStats(self):
+ stats = []
+ cdef cpp.vector[unsigned] res = self.thisptr.get().getNodeMessageStats(False)
+ for n in res:
+ stats.append(n)
+ return stats
+
+ def get(self, InfoHash key, get_cb=None, done_cb=None, filter=None, Where where=None):
+ """Retreive values associated with a key on the DHT.
+
+ key -- the key for which to search
+ get_cb -- is set, makes the operation non-blocking. Called when a value
+ is found on the DHT.
+ done_cb -- optional callback used when get_cb is set. Called when the
+ operation is completed.
+ """
+ if get_cb:
+ cb_obj = {'get':get_cb, 'done':done_cb, 'filter':filter}
+ ref.Py_INCREF(cb_obj)
+ if where is None:
+ where = Where()
+ self.thisptr.get().get(key._infohash, cpp.bindGetCb(get_callback, <void*>cb_obj),
+ cpp.bindDoneCb(done_callback, <void*>cb_obj),
+ cpp.nullptr, #filter implemented in the get_callback
+ where._where)
+ else:
+ lock = threading.Condition()
+ pending = 0
+ res = []
+ def tmp_get(v):
+ nonlocal res
+ res.append(v)
+ return True
+ def tmp_done(ok, nodes):
+ nonlocal pending, lock
+ with lock:
+ pending -= 1
+ lock.notify()
+ with lock:
+ pending += 1
+ self.get(key, get_cb=tmp_get, done_cb=tmp_done, filter=filter, where=where)
+ while pending > 0:
+ lock.wait()
+ return res
+ def put(self, InfoHash key, Value val, done_cb=None):
+ """Publish a new value on the DHT at key.
+
+ key -- the DHT key where to put the value
+ val -- the value to put on the DHT
+ done_cb -- optional callback called when the operation is completed.
+ """
+ if done_cb:
+ cb_obj = {'done':done_cb}
+ ref.Py_INCREF(cb_obj)
+ self.thisptr.get().put(key._infohash, val._value, cpp.bindDoneCb(done_callback, <void*>cb_obj))
+ else:
+ lock = threading.Condition()
+ pending = 0
+ ok = False
+ def tmp_done(ok_ret, nodes):
+ nonlocal pending, ok, lock
+ with lock:
+ ok = ok_ret
+ pending -= 1
+ lock.notify()
+ with lock:
+ pending += 1
+ self.put(key, val, done_cb=tmp_done)
+ while pending > 0:
+ lock.wait()
+ return ok
+ def listen(self, InfoHash key, value_cb):
+ t = ListenToken()
+ t._h = key._infohash
+ cb_obj = {'valcb':value_cb}
+ t._cb['cb'] = cb_obj
+ # avoid the callback being destructed if the token is destroyed
+ ref.Py_INCREF(cb_obj)
+ t._t = self.thisptr.get().listen(t._h, cpp.bindValueCb(value_callback, <void*>cb_obj)).share()
+ return t
+ def cancelListen(self, ListenToken token):
+ self.thisptr.get().cancelListen(token._h, token._t)
+ ref.Py_DECREF(<object>token._cb['cb'])
+ # fixme: not thread safe
+
+cdef class IndexValue(object):
+ cdef cpp.shared_ptr[cpp.IndexValue] _value
+ def __init__(self, InfoHash h=None, cpp.uint64_t vid=0):
+ cdef cpp.InfoHash hh = h._infohash
+ self._value.reset(new cpp.IndexValue(hh, vid))
+ def __str__(self):
+ return "(" + self.getKey().toString().decode() +", "+ str(self.getValueId()) +")"
+ def getKey(self):
+ h = InfoHash()
+ h._infohash = self._value.get().first
+ return h
+ def getValueId(self):
+ return self._value.get().second
+
+cdef class Pht(object):
+ 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.thisptr = new cpp.Pht(name, cpp_key_spec, dht.thisptr)
+ property MAX_NODE_ENTRY_COUNT:
+ def __get__(self):
+ return cpp.PHT_MAX_NODE_ENTRY_COUNT
+ def lookup(self, key, lookup_cb=None, done_cb=None):
+ """Query the Index with a specified key.
+
+ key -- the key for to the entry in the index.
+ lookup_cb -- function called when the operation is completed. This
+ function takes a list of IndexValue objects and a string
+ representation of the prefix where the value was indexed in
+ the PHT.
+ """
+ cb_obj = {'lookup':lookup_cb, 'done':done_cb} # TODO: donecallback is to be removed
+ ref.Py_INCREF(cb_obj)
+ cdef cpp.IndexKey cppk
+ for kk, v in key.items():
+ cppk[bytes(kk, 'utf-8')] = bytes(v)
+ self.thisptr.lookup(
+ cppk,
+ cpp.Pht.bindLookupCb(lookup_callback, <void*>cb_obj),
+ cpp.bindDoneCbSimple(done_callback_simple, <void*>cb_obj)
+ )
+ def insert(self, key, IndexValue value, done_cb=None):
+ """Add an index entry to the Index.
+
+ key -- the key for to the entry in the index.
+ value -- an IndexValue object describing the indexed value.
+ done_cb -- Called when the operation is completed.
+ """
+ cb_obj = {'done':done_cb}
+ ref.Py_INCREF(cb_obj)
+ cdef cpp.IndexKey cppk
+ for kk, v in key.items():
+ cppk[bytes(kk, 'utf-8')] = bytes(v)
+ cdef cpp.IndexValue val
+ val.first = (<InfoHash>value.getKey())._infohash
+ val.second = value.getValueId()
+ self.thisptr.insert(
+ cppk,
+ val,
+ cpp.bindDoneCbSimple(done_callback_simple, <void*>cb_obj)
+ )
--- /dev/null
+# Copyright (c) 2015-2023 Savoir-faire Linux Inc.
+# Author(s): Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+# Simon Désaulniers <sim.desaulniers@gmail.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; If not, see <https://www.gnu.org/licenses/>.
+
+from libc.stdint cimport *
+from libcpp cimport bool, nullptr_t, nullptr
+from libcpp.string cimport string
+from libcpp.vector cimport vector
+from libcpp.utility cimport pair
+from libcpp.map cimport map
+from libc.string cimport const_char, const_uchar
+
+ctypedef uint16_t in_port_t
+ctypedef unsigned short int sa_family_t;
+
+cdef extern from "<iostream>" namespace "std::chrono" nogil:
+ cdef cppclass duration[ulong]:
+ duration() except +
+
+ cdef cppclass seconds:
+ duration seconds(uint64_t) except +
+ duration seconds()
+
+cdef extern from "<memory>" namespace "std" nogil:
+ cdef cppclass shared_ptr[T]:
+ shared_ptr() except +
+ shared_ptr(T*) except +
+ T* get()
+ T operator*()
+ bool operator bool() const
+ void reset(T*)
+ shared_ptr[T] make_shared[T](...) except +
+
+cdef extern from "<functional>" namespace "std" nogil:
+ cdef cppclass hash[T]:
+ hash() except +
+ size_t get "operator()"(T)
+
+cdef extern from "<future>" namespace "std" nogil:
+ cdef cppclass shared_future[T]:
+ shared_future() except +
+ bool valid() const
+
+ cdef cppclass future[T]:
+ future() except +
+ bool valid() const
+ shared_future[T] share()
+
+cdef extern from "opendht/infohash.h" namespace "dht":
+ cdef cppclass InfoHash:
+ InfoHash() except +
+ InfoHash(string s) except +
+ string toString() const
+ bool getBit(unsigned bit) const
+ void setBit(unsigned bit, bool b)
+ double toFloat() const
+ @staticmethod
+ unsigned commonBits(InfoHash a, InfoHash b)
+ @staticmethod
+ InfoHash get(string s)
+ @staticmethod
+ InfoHash getRandom()
+ bool operator==(InfoHash) const
+ bool operator<(InfoHash) const
+ bool operator bool() const
+
+cdef extern from "opendht/sockaddr.h" namespace "dht":
+ cdef cppclass SockAddr:
+ SockAddr() except +
+ string toString() const
+ in_port_t getPort() const
+ void setPort(in_port_t p)
+ sa_family_t getFamily() const
+ void setFamily(sa_family_t f)
+ bool isLoopback() const
+ bool isPrivate() const
+ bool isUnspecified() const
+
+ctypedef vector[uint8_t] Blob
+
+cdef extern from "opendht/crypto.h" namespace "dht::crypto":
+ ctypedef pair[shared_ptr[PrivateKey], shared_ptr[Certificate]] Identity
+ cdef Identity generateIdentity(string name, Identity ca, unsigned bits)
+
+ cdef cppclass PrivateKey:
+ PrivateKey()
+ shared_ptr[PublicKey] getSharedPublicKey() const
+ Blob decrypt(Blob data) const
+ @staticmethod
+ PrivateKey generate()
+ @staticmethod
+ PrivateKey generateEC()
+
+ cdef cppclass PublicKey:
+ PublicKey()
+ InfoHash getId() const
+ Blob encrypt(Blob data) const
+
+ cdef cppclass Certificate:
+ Certificate()
+ Certificate(string pem)
+ InfoHash getId() 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)
+ shared_ptr[Certificate] issuer
+
+ cdef cppclass TrustList:
+ cppclass VerifyResult:
+ bool operator bool() const
+ bool isValid() const
+ string toString() const
+ TrustList()
+ void add(Certificate)
+ void remove(Certificate)
+ VerifyResult verify(Certificate);
+
+ctypedef TrustList.VerifyResult TrustListVerifyResult
+
+cdef extern from "opendht/value.h" namespace "dht::Value":
+ cdef cppclass Field:
+ pass
+
+cdef extern from "opendht/value.h" namespace "dht::Value::Field":
+ cdef Field None
+ cdef Field Id
+ cdef Field OwnerPk
+ cdef Field SeqNum
+ cdef Field UserType
+ cdef Field COUNT
+
+cdef extern from "opendht/value.h" namespace "dht":
+ cdef cppclass Value:
+ Value() except +
+ Value(vector[uint8_t]) except +
+ Value(const uint16_t t, const uint8_t* dat_ptr, size_t dat_len) except +
+ string toString() const
+ size_t size() const
+ uint64_t id
+ shared_ptr[PublicKey] owner
+ InfoHash recipient
+ vector[uint8_t] data
+ string user_type
+
+ cdef cppclass ValueType:
+ ValueType(uint16_t id, string name, seconds expiration) except +
+ uint16_t id
+ string name
+ seconds expiration
+
+ cdef cppclass Query:
+ Query() except +
+ Query(Select s, Where w) except +
+ Query(string q_str) except +
+ bool isSatisfiedBy(const Query& q) const
+ string toString() const
+
+ cdef cppclass Select:
+ Select() except +
+ Select(const string& q_str) except +
+ bool isSatisfiedBy(const Select& os) const
+ Select& field(Field field)
+ string toString() const
+
+ cdef cppclass Where:
+ Where() except +
+ Where(const string& q_str)
+ bool isSatisfiedBy(const Where& where) const
+ Where& id(uint64_t id)
+ Where& valueType(uint16_t type)
+ Where& owner(InfoHash owner_pk_hash)
+ Where& seq(uint16_t seq_no)
+ Where& userType(string user_type)
+ string toString() const
+
+cdef extern from "opendht/node.h" namespace "dht":
+ cdef cppclass Node:
+ Node() except +
+ InfoHash getId() const
+ string getAddrStr() const
+ bool isExpired() const
+
+cdef extern from "opendht/callbacks.h" namespace "dht":
+ 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)
+ ctypedef void (*DoneCallbackRaw)(bool done, vector[shared_ptr[Node]]* nodes, void *user_data)
+ ctypedef void (*DoneCallbackSimpleRaw)(bool done, void *user_data)
+
+ cppclass ShutdownCallback:
+ ShutdownCallback() except +
+ cppclass GetCallback:
+ GetCallback() except +
+ cppclass ValueCallback:
+ ValueCallback() except +
+ cppclass DoneCallback:
+ DoneCallback() except +
+ cppclass DoneCallbackSimple:
+ DoneCallbackSimple() except +
+
+ cdef ShutdownCallback bindShutdownCb(ShutdownCallbackRaw cb, void *user_data)
+ cdef GetCallback bindGetCb(GetCallbackRaw cb, void *user_data)
+ cdef ValueCallback bindValueCb(ValueCallbackRaw cb, void *user_data)
+ cdef DoneCallback bindDoneCb(DoneCallbackRaw cb, void *user_data)
+ cdef DoneCallbackSimple bindDoneCbSimple(DoneCallbackSimpleRaw cb, void *user_data)
+
+ cppclass Config:
+ InfoHash node_id
+ uint32_t network
+ bool is_bootstrap
+ bool maintain_storage
+ string persist_path
+ size_t max_req_per_sec
+ size_t max_peer_req_per_sec
+ cppclass SecureDhtConfig:
+ Config node_config
+ Identity id
+
+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:
+ SecureDhtConfig dht_config
+ bool threaded
+ InfoHash getId() const
+ InfoHash getNodeId() const
+ void bootstrap(const_char*, const_char*)
+ void bootstrap(const SockAddr&, DoneCallbackSimple done_cb)
+ void run(in_port_t, Config config)
+ void run(const_char*, const_char*, const_char*, Config config)
+ void join()
+ void shutdown(ShutdownCallback)
+ void registerType(ValueType& value);
+ bool isRunning()
+ SockAddr getBound(sa_family_t af) const
+ string getStorageLog() const
+ string getRoutingTablesLog(sa_family_t af) const
+ string getSearchesLog(sa_family_t af) const
+ 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)
+ 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 "opendht/log.h" namespace "dht::log":
+ 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"
+ cdef cppclass Prefix:
+ Prefix() except +
+ Prefix(vector[uint8_t]) except +
+ string toString() const
+ ctypedef pair[InfoHash, uint64_t] IndexValue "dht::indexation::Value"
+ ctypedef map[string, vector[uint8_t]] IndexKey "dht::indexation::Pht::Key"
+ ctypedef map[string, uint32_t] IndexKeySpec "dht::indexation::Pht::KeySpec"
+ ctypedef void (*LookupCallbackRaw)(vector[shared_ptr[IndexValue]]* values, Prefix* p, void* user_data);
+ cdef cppclass Pht:
+ cppclass LookupCallback:
+ LookupCallback() except +
+ Pht(string, IndexKeySpec, shared_ptr[DhtRunner]) except +
+ void lookup(IndexKey k, LookupCallback cb, DoneCallbackSimple doneCb);
+ void insert(IndexKey k, IndexValue v, DoneCallbackSimple cb)
+ @staticmethod
+ LookupCallback bindLookupCb(LookupCallbackRaw cb, void *user_data)
--- /dev/null
+[build-system]
+requires = ["setuptools", "wheel", "Cython"]
+build-backend = "setuptools.build_meta"
--- /dev/null
+# Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+# Author: Guillaume Roguez <guillaume.roguez@savoirfairelinux.com>
+# Author: Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+#
+# A Python3 wrapper to access to OpenDHT API
+# This wrapper is written for Cython 0.22
+#
+# This file is part of OpenDHT Python Wrapper.
+#
+# OpenDHT Python Wrapper is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# OpenDHT Python Wrapper is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with OpenDHT Python Wrapper. If not, see <https://www.gnu.org/licenses/>.
+#
+
+from setuptools import setup, Extension
+from Cython.Build import cythonize
+from Cython.Distutils import build_ext
+
+setup(name="opendht",
+ version="@PACKAGE_VERSION@",
+ description="Python wrapper for OpenDHT",
+ url='https://github.com/savoirfairelinux/opendht',
+ author="Adrien Béraud, Guillaume Roguez, Simon Désaulniers",
+ license="GPLv3",
+ classifiers=[
+ 'Development Status :: 5 - Production/Stable',
+ 'Intended Audience :: Developers',
+ 'Topic :: Software Development :: Libraries :: Python Modules',
+ 'Topic :: System :: Distributed Computing',
+ 'Topic :: System :: Networking'
+ 'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)',
+ 'Programming Language :: Python :: 3',
+ 'Programming Language :: Python :: 3.7',
+ ],
+ cmdclass = { 'build_ext' : build_ext },
+ ext_modules = cythonize(Extension(
+ "opendht",
+ ["@CURRENT_SOURCE_DIR@/opendht.pyx"],
+ include_dirs = ['@PROJECT_SOURCE_DIR@/include'],
+ language="c++",
+ extra_compile_args=["-std=c++17"],
+ extra_link_args=["-std=c++17"],
+ libraries=["opendht"],
+ library_dirs = ['@CURRENT_BINARY_DIR@', '@PROJECT_BINARY_DIR@']
+ ))
+)
--- /dev/null
+import unittest
+import opendht as dht
+
+class OpenDhtTester(unittest.TestCase):
+
+ # test that DhtRunner can be instatiated and deleted without throwing
+ def test_instance(self):
+ for i in range(10):
+ r = dht.DhtRunner()
+ r.run()
+ del r
+
+ # test that bootstraping works (raw address)
+ def test_bootstrap(self):
+ a = dht.DhtRunner()
+ a.run()
+ b = dht.DhtRunner()
+ b.run()
+ self.assertTrue(b.ping(a.getBound()))
+
+ def test_crypto(self):
+ i = dht.Identity.generate("id")
+ message = dht.InfoHash.getRandom().toString()
+ encrypted = i.publickey.encrypt(message)
+ decrypted = i.key.decrypt(encrypted)
+ self.assertTrue(message == decrypted)
+
+ def test_crypto_ec(self):
+ key = dht.PrivateKey.generateEC()
+ cert = dht.Certificate.generate(key, "CA", is_ca=True)
+ ca_id = dht.Identity(key, cert)
+ self.assertTrue(cert.getId() == key.getPublicKey().getId())
+ key2 = dht.PrivateKey.generateEC()
+ cert2 = dht.Certificate.generate(key2, "cert", ca_id)
+ trust = dht.TrustList()
+ trust.add(cert)
+ self.assertTrue(trust.verify(cert2))
+
+ def test_trust(self):
+ main_id = dht.Identity.generate("id_1")
+ sub_id1 = dht.Identity.generate("sid_1", main_id)
+ sub_id2 = dht.Identity.generate("sid_2", main_id)
+ main_id.certificate.revoke(main_id.key, sub_id2.certificate)
+ main_id2 = dht.Identity.generate("id_2")
+ trust = dht.TrustList()
+ trust.add(main_id.certificate)
+ self.assertTrue(trust.verify(main_id.certificate))
+ self.assertTrue(trust.verify(sub_id1.certificate))
+ self.assertFalse(trust.verify(sub_id2.certificate))
+ self.assertFalse(trust.verify(main_id2.certificate))
+
+if __name__ == '__main__':
+ unittest.main()
--- /dev/null
+# Benchmark
+
+The `benchmark.py` script is used for testing OpenDHT in various cases. If you
+run `benchmark.py --help`, you should find the following text:
+
+ optional arguments:
+ -h, --help show this help message and exit
+ --performance Launches performance benchmark test. Available args
+ for "-t" are: gets.
+ --data-persistence Launches data persistence benchmark test. Available
+ args for "-t" are: delete, replace, mult_time.
+ Available args for "-o" are : dump_str_log,
+ keep_alive, trigger, traffic_plot, op_plot. Use "-m"
+ to specify the number of producers on the DHT.Use "-e"
+ to specify the number of values to put on the DHT.
+
+These options specify the feature to be tested. Each feature has its own tests.
+You specify the test by using `-t` flag (see `benchmark.py --help` for full
+help).
+
+## Python dependencies
+
+- pyroute2 >=0.6.9
+- matplotlib
+- GeoIP (used by `scanner.py` for drawing map of the world)
+- ipaddress
+- netifaces
+- networkx
+- numpy
+
+## Usage
+
+Before running the script, you have to build and install OpenDHT and its cython
+wrapper (`cython3` has to be installed) on the system so that it can be found by
+the benchmark script.
+
+ $ cd $OPENDHT_SRC_DIR
+ $ ./autogen.sh
+ $ ./configure
+ $ make && sudo make install
+
+Then, you can use the script like so:
+
+ $ cd $OPENDHT_SRC_DIR/python/tools/
+ $ python3 benchmark.py --performance -t gets -n 2048
--- /dev/null
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+# Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+# Author(s): Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+# Simon Désaulniers <sim.desaulniers@gmail.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import sys
+import subprocess
+import signal
+import argparse
+import time
+import random
+
+from dht.network import DhtNetwork
+from dht.network import DhtNetworkSubProcess
+from dht.tests import PerformanceTest, PersistenceTest, PhtTest
+from dht import virtual_network_builder
+from dht import network as dhtnetwork
+
+from opendht import *
+
+
+class WorkBench():
+ """
+ This contains the initialisation information, such as ipv4/ipv6, number of
+ nodes and cluster to create, etc. This class is also used to initialise and
+ finish the network.
+ """
+ def __init__(self, ifname='ethdht', virtual_locs=8, node_num=32, remote_bootstrap=None, loss=0, delay=0, disable_ipv4=False,
+ disable_ipv6=False):
+ self.ifname = ifname
+ self.virtual_locs = virtual_locs
+ self.node_num = node_num
+ self.clusters = min(virtual_locs, node_num)
+ self.node_per_loc = int(self.node_num / self.clusters)
+ self.loss = loss
+ self.delay = delay
+ self.disable_ipv4 = disable_ipv4
+ self.disable_ipv6 = disable_ipv6
+
+ self.remote_bootstrap = remote_bootstrap
+ self.local_bootstrap = None
+ self.bs_port = "5000"
+ self.procs = [None for _ in range(self.clusters)]
+
+ def get_bootstrap(self):
+ if not self.local_bootstrap:
+ self.local_bootstrap = DhtNetwork(iface='br'+self.ifname,
+ first_bootstrap=False if self.remote_bootstrap else True,
+ bootstrap=[(self.remote_bootstrap, self.bs_port)] if self.remote_bootstrap else [])
+ return self.local_bootstrap
+
+ def create_virtual_net(self):
+ if self.virtual_locs > 1:
+ cmd = ["python3", os.path.abspath(virtual_network_builder.__file__),
+ "-i", self.ifname,
+ "-n", str(self.clusters),
+ '-l', str(self.loss),
+ '-d', str(self.delay)]
+ if not self.disable_ipv4:
+ cmd.append('-4')
+ if not self.disable_ipv6:
+ cmd.append('-6')
+ print(cmd)
+ p = subprocess.Popen(cmd, stdout=subprocess.PIPE)
+ output, err = p.communicate()
+ print(output.decode())
+
+ def destroy_virtual_net(self):
+ print('Shuting down the virtual IP network.')
+ subprocess.call(["python3", os.path.abspath(virtual_network_builder.__file__), "-i", self.ifname, "-n", str(self.clusters), "-r"])
+
+ def start_cluster(self, i):
+ if self.local_bootstrap:
+ cmd = ["python3", os.path.abspath(dhtnetwork.__file__), "-n", str(self.node_per_loc), '-I', self.ifname+str(i)+'.1']
+ if self.remote_bootstrap:
+ cmd.extend(['-b', self.remote_bootstrap, '-bp', "5000"])
+ else:
+ if not self.disable_ipv4 and self.local_bootstrap.ip4:
+ cmd.extend(['-b', self.local_bootstrap.ip4])
+ if not self.disable_ipv6 and self.local_bootstrap.ip6:
+ cmd.extend(['-b6', self.local_bootstrap.ip6])
+ lock = threading.Condition()
+ def dcb(success):
+ nonlocal lock
+ if not success:
+ DhtNetwork.Log.err("Failed to initialize network...")
+ with lock:
+ lock.notify()
+ with lock:
+ self.procs[i] = DhtNetworkSubProcess('node'+str(i), cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
+ self.procs[i].sendPing(done_cb=dcb)
+ lock.wait()
+ else:
+ raise Exception('First create bootstrap.')
+
+ def stop_cluster(self, i):
+ """
+ Stops a cluster sub process. All nodes are put down without graceful
+ shutdown.
+ """
+ if self.procs[i]:
+ try:
+ self.procs[i].quit()
+ except Exception as e:
+ print(e)
+ self.procs[i] = None
+
+ def replace_cluster(self):
+ """
+ Same as stop_cluster(), but creates a new cluster right after.
+ """
+ n = random.randrange(0, self.clusters)
+ self.stop_cluster(n)
+ self.start_cluster(n)
+
+ def resize_clusters(self, n):
+ """
+ Resizes the list of clusters to be of length ``n``.
+ """
+ procs_count = len(self.procs)
+ if procs_count < n:
+ for i in range(n-procs_count):
+ self.procs.append(None)
+ self.start_cluster(procs_count+i)
+ else:
+ for i in range(procs_count-n):
+ self.stop_cluster(procs_count-i-1)
+
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser(description='Run, test and benchmark a '\
+ 'DHT network on a local virtual network with simulated packet '\
+ 'loss and latency.')
+ ifConfArgs = parser.add_argument_group('Virtual interface configuration')
+ ifConfArgs.add_argument('-i', '--ifname', default='ethdht', help='interface name')
+ ifConfArgs.add_argument('-n', '--node-num', type=int, default=32, help='number of dht nodes to run')
+ ifConfArgs.add_argument('-v', '--virtual-locs', type=int, default=8,
+ help='number of virtual locations (node clusters)')
+ ifConfArgs.add_argument('-l', '--loss', type=int, default=0, help='simulated cluster packet loss (percent)')
+ ifConfArgs.add_argument('-d', '--delay', type=int, default=0, help='simulated cluster latency (ms)')
+ ifConfArgs.add_argument('-b', '--bootstrap', default=None, help='Bootstrap node to use (if any)')
+ ifConfArgs.add_argument('-no4', '--disable-ipv4', action="store_true", help='Enable IPv4')
+ ifConfArgs.add_argument('-no6', '--disable-ipv6', action="store_true", help='Enable IPv6')
+
+ testArgs = parser.add_argument_group('Test arguments')
+ testArgs.add_argument('--bs-dht-log', action='store_true', default=False, help='Enables dht log in bootstrap.')
+ testArgs.add_argument('-t', '--test', type=str, default=None, required=True, help='Specifies the test.')
+ testArgs.add_argument('-o', '--opt', type=str, default=[], nargs='+',
+ help='Options passed to tests routines.')
+ testArgs.add_argument('-m', type=int, default=None, help='Generic size option passed to tests.')
+ testArgs.add_argument('-e', type=int, default=None, help='Generic size option passed to tests.')
+
+ featureArgs = parser.add_mutually_exclusive_group(required=True)
+ featureArgs.add_argument('--performance', action='store_true', default=False,
+ help='Launches performance benchmark test. Available args for "-t" are: gets.')
+ featureArgs.add_argument('--pht', action='store_true', default=False,
+ help='Launches PHT benchmark test. '\
+ 'Available args for "-t" are: insert. '\
+ 'Timer available by adding "timer" to "-o" args'\
+ 'Use "-m" option for fixing number of keys to create during the test.')
+ featureArgs.add_argument('--data-persistence', action='store_true', default=0,
+ help='Launches data persistence benchmark test. '\
+ 'Available args for "-t" are: delete, replace, mult_time. '\
+ 'Available args for "-o" are : dump_str_log, keep_alive, trigger, traffic_plot, op_plot. '\
+ 'Use "-m" to specify the number of producers on the DHT. '\
+ 'Use "-e" to specify the number of values to put on the DHT.')
+
+ args = parser.parse_args()
+ test_opt = { o : True for o in args.opt }
+
+ wb = WorkBench(args.ifname, args.virtual_locs, args.node_num, loss=args.loss,
+ delay=args.delay, disable_ipv4=args.disable_ipv4,
+ disable_ipv6=args.disable_ipv6)
+ wb.create_virtual_net()
+ bootstrap = wb.get_bootstrap()
+
+ bs_dht_log_enabled = False
+ def toggle_bs_dht_log(signum, frame):
+ global bs_dht_log_enabled, bootstrap
+ if bs_dht_log_enabled:
+ bootstrap.front().disableLogging()
+ bs_dht_log_enabled = False
+ else:
+ bootstrap.front().enableLogging()
+ bs_dht_log_enabled = True
+ signal.signal(signal.SIGUSR1, toggle_bs_dht_log)
+
+ if args.bs_dht_log:
+ bs_dht_log_enabled = True
+ bootstrap.front().enableLogging()
+
+ bootstrap.resize(1)
+ print("Launching", wb.node_num, "nodes (", wb.clusters, "clusters of", wb.node_per_loc, "nodes)")
+
+ try:
+ for i in range(wb.clusters):
+ wb.start_cluster(i)
+
+ # recover -e and -m values.
+ if args.e:
+ test_opt.update({ 'num_values' : args.e })
+ if args.m:
+ test_opt.update({ 'num_producers' : args.m })
+
+ # run the test
+ if args.performance:
+ PerformanceTest(args.test, wb, test_opt).run()
+ elif args.data_persistence:
+ PersistenceTest(args.test, wb, test_opt).run()
+ elif args.pht:
+ if args.m:
+ test_opt.update({ 'num_keys' : args.m })
+ PhtTest(args.test, wb, test_opt).run()
+
+ except Exception as e:
+ print(e)
+ finally:
+ for p in wb.procs:
+ if p:
+ p.quit()
+ bootstrap.resize(0)
+ sys.stdout.write('Shutting down the virtual IP network... ')
+ sys.stdout.flush()
+ wb.destroy_virtual_net()
+ print('Done.')
--- /dev/null
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015-2023 Savoir-faire Linux Inc.
+# Author(s): Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+# Simon Désaulniers <sim.desaulniers@gmail.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import sys
+import signal
+import random
+import time
+import threading
+import queue
+import re
+import traceback
+
+import ipaddress
+import netifaces
+import numpy as np
+from pyroute2 import NSPopen
+import msgpack
+
+from opendht import *
+
+
+# useful functions
+b_space_join = lambda *l: b' '.join(map(bytes, l))
+
+class DhtNetworkSubProcess(NSPopen):
+ """
+ Handles communication with DhtNetwork sub process.
+
+ When instanciated, the object's thread is started and will read the sub
+ process' stdout until it finds 'DhtNetworkSubProcess.NOTIFY_TOKEN' token,
+ therefor, waits for the sub process to spawn.
+ """
+ # Packet types
+ REQUEST = 'DhtNetworkSubProcess.request'
+ ANSWER = 'DhtNetworkSubProcess.answer'
+ OUT = 'DhtNetworkSubProcess.out'
+
+ # requests
+ PING_REQ = "p"
+ NODE_PUT_REQ = "np" # "np <hash> <value>"
+ NEW_NODE_REQ = "nn" # "nn"
+ REMOVE_NODE_REQ = "rn" # "rn <id0>[ <id1>[ id2[...]]]"
+ SHUTDOWN_NODE_REQ = "sdn" # "sdn <id0>[ <id1>[ id2[...]]]"
+ SHUTDOWN_REPLACE_NODE_REQ = "sdrn" # "sdn <id0>[ <id1>[ id2[...]]]"
+ SHUTDOWN_CLUSTER_REQ = "sdc" # "sdc"
+ DUMP_STORAGE_REQ = "strl" # "strl"
+ MESSAGE_STATS = "gms" # "gms"
+
+ def __init__(self, ns, cmd, quit=False, **kwargs):
+ super(DhtNetworkSubProcess, self).__init__(ns, cmd, **kwargs)
+ self._setStdoutFlags()
+ self._virtual_ns = ns
+
+ self._quit = quit
+ self._lock = threading.Condition()
+ self._in_queue = queue.Queue()
+ self._callbacks = {}
+ self._tid = 0
+
+ # starting thread
+ self._thread = threading.Thread(target=self._communicate)
+ self._thread.start()
+
+ def __repr__(self):
+ return 'DhtNetwork on virtual namespace "%s"' % self._virtual_ns
+
+ def _setStdoutFlags(self):
+ """
+ Sets non-blocking read flags for subprocess stdout file descriptor.
+ """
+ import fcntl
+ flags = self.stdout.fcntl(fcntl.F_GETFL)
+ self.stdout.fcntl(fcntl.F_SETFL, flags | os.O_NDELAY)
+
+ def _communicate(self):
+ """
+ Communication thread. This reads and writes to the sub process.
+ """
+ sleep_time = 0.1
+
+ while not self._quit:
+ with self._lock:
+ try:
+ packet = self._in_queue.get_nowait()
+
+ # sending data to sub process
+ self.stdin.write(packet)
+ self.stdin.flush()
+ except queue.Empty:
+ pass
+
+ # reading from sub process
+ out_string = ''
+ for p in msgpack.Unpacker(self.stdout):
+ if isinstance(p, dict):
+ self._process_packet(p)
+ else:
+ # Some non-msgpack data could slip into the stream. We
+ # have to treat those as characters.
+ out_string += chr(p)
+ if out_string:
+ print(out_string)
+
+ #waiting for next stdin req to send
+ self._lock.wait(timeout=sleep_time)
+
+ with self._lock:
+ self._lock.notify()
+
+ def _stop_communicating(self):
+ """
+ Stops the I/O thread from communicating with the subprocess.
+ """
+ if not self._quit:
+ self._quit = True
+ with self._lock:
+ self._lock.notify()
+ self._lock.wait()
+
+ def quit(self):
+ """
+ Notifies thread and sub process to terminate. This is blocking call
+ until the sub process finishes.
+ """
+ self._stop_communicating()
+ self.send_signal(signal.SIGINT);
+ self.wait()
+ self.release()
+
+ def _send(self, msg):
+ """
+ Send data to sub process.
+ """
+ with self._lock:
+ self._in_queue.put(msg)
+ self._lock.notify()
+
+ def _process_packet(self, p):
+ """
+ Process msgpack packet received from
+ """
+ if not b'tid' in p:
+ DhtNetwork.Log.err('Bad packet...')
+ try:
+ self._callbacks[p[b'tid']](p)
+ except KeyError:
+ DhtNetwork.Log.err('Unknown tid...')
+
+
+ def _sendRequest(self, request, tid, done_cb):
+ """
+ Sends a request to the sub network and wait for output.
+
+ @param request: The serialized request.
+ @type request: Msgpack object
+ """
+ self._callbacks[tid] = done_cb
+ self._send(request)
+
+ def sendPing(self, done_cb=None):
+ """Sends a ping request to the DhtNetworkSubProcess.
+
+ @param done_cb: The callback to be executed when we get a response. This
+ function takes a boolean "success" as parameter.
+ @type done_cb: Function
+ """
+ self._tid += 1
+ def dcb(packet):
+ try:
+ done_cb(packet[b'success'])
+ except KeyError:
+ done_cb(False)
+ self._sendRequest(msgpack.packb({
+ DhtNetworkSubProcess.REQUEST : True,
+ 'tid' : self._tid,
+ 'req' : DhtNetworkSubProcess.PING_REQ
+ }), self._tid, dcb)
+
+ def sendGetMessageStats(self, done_cb=None):
+ """
+ Sends DhtNetwork sub process statistics request about nodes messages
+ sent.
+
+ @param done_cb: A function taking as parameter the returned list of
+ stats.
+ @type done_cb: function
+
+ @return: A list [num_nodes, ping, find, get, put, listen].
+ @rtype : list
+ """
+ self._tid += 1
+ def dcb(packet):
+ nonlocal done_cb
+ if not done_cb:
+ return
+ try:
+ stats = packet[b'stats']
+ done_cb([] if not isinstance(stats, list) else done_cb(stats))
+ except KeyError:
+ done_cb([])
+ self._sendRequest(msgpack.packb({
+ DhtNetworkSubProcess.REQUEST : True,
+ 'tid' : self._tid,
+ 'req' : DhtNetworkSubProcess.MESSAGE_STATS
+ }), self._tid, dcb)
+
+ def sendClusterPutRequest(self, _hash, value, done_cb=None):
+ """
+ Sends a put operation request.
+
+ @param _hash: the hash of the value.
+ @type _hash: bytes.
+ @param value: the value.
+ @type value: bytes.
+ @param done_cb: A function taking as parameter a boolean "success".
+ @type done_cb: function
+ """
+ self._tid += 1
+ def dcb(packet):
+ nonlocal done_cb
+ if not done_cb:
+ return
+ try:
+ done_cb(packet[b'success'])
+ except KeyError:
+ done_cb(False)
+ self._sendRequest(msgpack.packb({
+ DhtNetworkSubProcess.REQUEST : True,
+ 'tid' : self._tid,
+ 'req' : DhtNetworkSubProcess.NODE_PUT_REQ,
+ 'hash' : _hash,
+ 'value' : value
+ }), self._tid, dcb)
+
+ def sendClusterRequest(self, request, ids=[], done_cb=None):
+ """
+ Send request to a list of nodes or the whole cluster.
+
+ @param request: The request. Possible values are:
+ DhtNetworkSubProcess.REMOVE_NODE_REQ
+ DhtNetworkSubProcess.SHUTDOWN_NODE_REQ
+ DhtNetworkSubProcess.SHUTDOWN_REPLACE_NODE_REQ
+ DhtNetworkSubProcess.SHUTDOWN_CLUSTER_REQ
+ DhtNetworkSubProcess.DUMP_STORAGE_REQ
+ @type request: bytes
+ @param ids: The list of ids concerned by the request.
+ @type ids: list
+ """
+ self._tid += 1
+ def dcb(packet):
+ nonlocal done_cb
+ if not done_cb:
+ return
+ try:
+ done_cb(packet[b'success'])
+ except KeyError:
+ done_cb(False)
+ self._sendRequest(msgpack.packb({
+ DhtNetworkSubProcess.REQUEST : True,
+ 'tid' : self._tid,
+ 'req' : request,
+ 'ids' : ids
+ }), self._tid, dcb)
+
+
+class DhtNetwork(object):
+ nodes = []
+
+ class Log(object):
+ BOLD = "\033[1m"
+ NORMAL = "\033[0m"
+ WHITE = "\033[97m"
+ RED = "\033[31m"
+ YELLOW = "\033[33m"
+
+ @staticmethod
+ def _log_with_color(*to_print, color=None):
+ color = color if color else DhtNetwork.Log.WHITE
+ print('%s%s[DhtNetwork-%s]%s%s' %
+ (DhtNetwork.Log.BOLD, color, DhtNetwork.iface, DhtNetwork.Log.NORMAL, color),
+ *to_print, DhtNetwork.Log.NORMAL, file=sys.stderr)
+
+ @staticmethod
+ def log(*to_print):
+ DhtNetwork.Log._log_with_color(*to_print, color=DhtNetwork.Log.WHITE)
+
+ @staticmethod
+ def warn(*to_print):
+ DhtNetwork.Log._log_with_color(*to_print, color=DhtNetwork.Log.YELLOW)
+
+ @staticmethod
+ def err(*to_print):
+ DhtNetwork.Log._log_with_color(*to_print, color=DhtNetwork.Log.RED)
+
+ @staticmethod
+ def run_node(ip4, ip6, p, bootstrap=[], is_bootstrap=False):
+ DhtNetwork.Log.log("run_node", ip4, ip6, p, bootstrap)
+ n = DhtRunner()
+ n.run(ipv4=ip4 if ip4 else "", ipv6=ip6 if ip6 else "", port=p, is_bootstrap=is_bootstrap)
+ for b in bootstrap:
+ n.bootstrap(b[0], b[1])
+ time.sleep(.01)
+ return ((ip4, ip6, p), n, id)
+
+ @staticmethod
+ def find_ip(iface):
+ if not iface or iface == 'any':
+ return ('0.0.0.0','')
+
+ if_ip4 = netifaces.ifaddresses(iface)[netifaces.AF_INET][0]['addr']
+ if_ip6 = netifaces.ifaddresses(iface)[netifaces.AF_INET6][0]['addr']
+ return (if_ip4, if_ip6)
+
+ def __init__(self, iface=None, ip4=None, ip6=None, port=4000, bootstrap=[], first_bootstrap=False):
+ DhtNetwork.iface = iface
+ self.port = port
+ ips = DhtNetwork.find_ip(iface)
+ self.ip4 = ip4 if ip4 else ips[0]
+ self.ip6 = ip6 if ip6 else ips[1]
+ self.bootstrap = bootstrap
+ if first_bootstrap:
+ DhtNetwork.Log.log("Starting bootstrap node")
+ self.nodes.append(DhtNetwork.run_node(self.ip4, self.ip6, self.port, self.bootstrap, is_bootstrap=True))
+ self.bootstrap = [(self.ip4, str(self.port))]
+ self.port += 1
+ #print(self.ip4, self.ip6, self.port)
+
+ def front(self):
+ if len(self.nodes) == 0:
+ return None
+ return self.nodes[0][1]
+
+ def get(self, i=None):
+ if not self.nodes:
+ return None
+
+ if i is None:
+ l = list(self.nodes)
+ random.shuffle(l)
+ return l[0][1]
+ else:
+ return self.nodes[i][1]
+
+ def getNodeInfoById(self, id=None):
+ if id:
+ for n in self.nodes:
+ if n[1].getNodeId() == id:
+ return n
+ return None
+
+ def launch_node(self):
+ n = DhtNetwork.run_node(self.ip4, self.ip6, self.port, self.bootstrap)
+ self.nodes.append(n)
+ if not self.bootstrap:
+ DhtNetwork.Log.log("Using fallback bootstrap", self.ip4, self.port)
+ self.bootstrap = [(self.ip4, str(self.port))]
+ self.port += 1
+ return n
+
+ def end_node(self, id=None, shutdown=False, last_msg_stats=None):
+ """
+ Ends a running node.
+
+ @param id: The 40 hex chars id of the node.
+ @type id: bytes
+
+ @return: If a node was deleted or not.
+ @rtype : boolean
+ """
+ lock = threading.Condition()
+ def shutdown_cb():
+ nonlocal lock
+ DhtNetwork.Log.log('Done.')
+ with lock:
+ lock.notify()
+
+ if not self.nodes:
+ return
+ elif id:
+ n = self.getNodeInfoById(id)
+ if n:
+ if shutdown:
+ with lock:
+ DhtNetwork.Log.log('Waiting for node to shutdown... ')
+ n[1].shutdown(shutdown_cb)
+ lock.wait()
+ if last_msg_stats:
+ last_msg_stats.append(self.getMessageStats())
+ n[1].join()
+ self.nodes.remove(n)
+ DhtNetwork.Log.log(id, 'deleted !')
+ return True
+ else:
+ return False
+ else:
+ n = self.nodes.pop()
+ n[1].join()
+ return True
+
+ def replace_node(self, id=None, shutdown=False, last_msg_stats=None):
+ random.shuffle(self.nodes)
+ deleted = self.end_node(id=id, shutdown=shutdown, last_msg_stats=last_msg_stats)
+ if deleted:
+ self.launch_node()
+
+ def resize(self, n):
+ n = min(n, 500)
+ l = len(self.nodes)
+ if n == l:
+ return
+ if n > l:
+ DhtNetwork.Log.log("Launching", n-l, "nodes", self.ip4, self.ip6)
+ for i in range(l, n):
+ self.launch_node()
+ else:
+ DhtNetwork.Log.log("Ending", l-n, "nodes", self.ip4, self.ip6)
+ #random.shuffle(self.nodes)
+ for i in range(n, l):
+ self.end_node()
+
+ def getMessageStats(self):
+ stats = np.array([0,0,0,0,0])
+ for n in self.nodes:
+ stats += np.array(n[1].getNodeMessageStats())
+ stats_list = [len(self.nodes)]
+ stats_list.extend(stats.tolist())
+ return stats_list
+
+
+if __name__ == '__main__':
+ import argparse
+
+ lock = threading.Condition()
+ quit = False
+
+ def send_msgpack_packet(packet):
+ sys.stdout.buffer.write(packet)
+ sys.stdout.buffer.flush()
+
+ def notify_benchmark(packet, success):
+ """Notifies the benchmark when an operation has been completed.
+
+ @param success: If the operation has been successful
+ @type success: boolean
+ @param packet: The packet we are providing an answer for.
+ @type packet: dict
+ """
+ send_msgpack_packet(msgpack.packb({
+ DhtNetworkSubProcess.ANSWER : True,
+ 'tid' : packet[b'tid'],
+ 'success' : success
+ }))
+
+ def send_stats(packet, stats):
+ send_msgpack_packet(msgpack.packb({
+ DhtNetworkSubProcess.ANSWER : True,
+ 'tid' : packet[b'tid'],
+ 'stats' : stats
+ }))
+
+ def listen_to_mother_nature(q):
+ global quit
+ while not quit:
+ for p in msgpack.Unpacker(sys.stdin.buffer.raw):
+ if isinstance(p, dict) and DhtNetworkSubProcess.REQUEST.encode() in p:
+ with lock:
+ q.put(p)
+ lock.notify()
+
+ def handler(signum, frame):
+ global quit
+ with lock:
+ quit = True
+ lock.notify()
+
+ signal.signal(signal.SIGALRM, handler)
+ signal.signal(signal.SIGABRT, handler)
+ signal.signal(signal.SIGINT, handler)
+ signal.signal(signal.SIGTERM, handler)
+
+ net = None
+ try:
+ parser = argparse.ArgumentParser(description='Create a dht network of -n nodes')
+ parser.add_argument('-n', '--node-num', help='number of dht nodes to run', type=int, default=32)
+ parser.add_argument('-I', '--iface', help='local interface to bind', default='any')
+ parser.add_argument('-p', '--port', help='start of port range (port, port+node_num)', type=int, default=4000)
+ parser.add_argument('-b', '--bootstrap', help='bootstrap address')
+ parser.add_argument('-b6', '--bootstrap6', help='bootstrap address (IPv6)')
+ parser.add_argument('-bp', '--bootstrap-port', help='bootstrap port', default="4000")
+ args = parser.parse_args()
+
+ bs = []
+ if args.bootstrap:
+ bs.append((args.bootstrap, args.bootstrap_port))
+ if args.bootstrap6:
+ bs.append((args.bootstrap6, args.bootstrap_port))
+
+ net = DhtNetwork(iface=args.iface, port=args.port, bootstrap=bs)
+ net.resize(args.node_num)
+
+ q = queue.Queue()
+ t = threading.Thread(target=listen_to_mother_nature, args=tuple([q]))
+ t.daemon = True
+ t.start()
+
+ msg_stats = []
+
+ with lock:
+ while not quit:
+ try:
+ packet = q.get_nowait()
+ except queue.Empty:
+ lock.wait()
+ else:
+ NODE_PUT_REQ = DhtNetworkSubProcess.NODE_PUT_REQ
+ NEW_NODE_REQ = DhtNetworkSubProcess.NEW_NODE_REQ
+ REMOVE_NODE_REQ = DhtNetworkSubProcess.REMOVE_NODE_REQ
+ SHUTDOWN_NODE_REQ = DhtNetworkSubProcess.SHUTDOWN_NODE_REQ
+ SHUTDOWN_REPLACE_NODE_REQ = DhtNetworkSubProcess.SHUTDOWN_REPLACE_NODE_REQ
+ SHUTDOWN_CLUSTER_REQ = DhtNetworkSubProcess.SHUTDOWN_CLUSTER_REQ
+ DUMP_STORAGE_REQ = DhtNetworkSubProcess.DUMP_STORAGE_REQ
+ MESSAGE_STATS = DhtNetworkSubProcess.MESSAGE_STATS
+
+ req = packet[b'req'].decode()
+ success = True
+ if req in [SHUTDOWN_NODE_REQ, SHUTDOWN_REPLACE_NODE_REQ, REMOVE_NODE_REQ]:
+ def delete_request(req, nid):
+ global msg_stats
+ if not nid:
+ return
+ if req == SHUTDOWN_NODE_REQ:
+ net.end_node(id=nid, shutdown=True, last_msg_stats=msg_stats)
+ elif req == SHUTDOWN_REPLACE_NODE_REQ:
+ net.replace_node(id=nid, shutdown=True, last_msg_stats=msg_stats)
+ elif req == REMOVE_NODE_REQ:
+ net.end_node(id=nid, last_msg_stats=msg_stats)
+
+ nodes = packet[b'ids']
+ if nodes:
+ for nid in nodes:
+ delete_request(req, nid)
+ else:
+ n = net.get()
+ if n:
+ delete_request(req, n.getNodeId())
+ else:
+ success = False
+ elif req == SHUTDOWN_CLUSTER_REQ:
+ for n in net.nodes:
+ net.end_node(id=n[2], shutdown=True, last_msg_stats=msg_stats)
+ quit = True
+ elif req == NEW_NODE_REQ:
+ net.launch_node()
+ elif req == NODE_PUT_REQ:
+ _hash = packet[b'hash']
+ v = packet[b'value']
+ n = net.get()
+ if n:
+ n.put(InfoHash(_hash), Value(v))
+ else:
+ success = False
+ elif req == DUMP_STORAGE_REQ:
+ hashes = packet[b'ids']
+ for n in [m[1] for m in net.nodes if m[1].getNodeId() in hashes]:
+ net.log(n.getStorageLog())
+ elif req == MESSAGE_STATS:
+ stats = sum([np.array(x) for x in [net.getMessageStats()]+msg_stats])
+ send_stats(packet, [int(_) for _ in stats])
+ msg_stats.clear()
+ continue
+ notify_benchmark(packet, success)
+ except Exception as e:
+ traceback.print_tb(e.__traceback__)
+ print(type(e).__name__+':', e, file=sys.stderr)
+ finally:
+ if net:
+ net.resize(0)
--- /dev/null
+# -*- coding: utf-8 -*-
+# Copyright (C) 2015-2023 Savoir-Faire Linux Inc.
+# Author(s): Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+# Simon Désaulniers <sim.desaulniers@gmail.com>
+
+import sys
+import os
+import threading
+import random
+import string
+import time
+import subprocess
+import re
+import traceback
+import collections
+
+from matplotlib.ticker import FuncFormatter
+import math
+
+import numpy as np
+import matplotlib.pyplot as plt
+import networkx as nx
+from networkx.drawing.nx_agraph import graphviz_layout
+
+
+from opendht import *
+from dht.network import DhtNetwork, DhtNetworkSubProcess
+
+############
+# Common #
+############
+
+# matplotlib display format for bits (b, Kb, Mb)
+bit_format = None
+Kbit_format = FuncFormatter(lambda x, pos: '%1.1f' % (x*1024**-1) + 'Kb')
+Mbit_format = FuncFormatter(lambda x, pos: '%1.1f' % (x*1024**-2) + 'Mb')
+
+def random_str_val(size=1024):
+ """Creates a random string value of specified size.
+
+ @param size: Size, in bytes, of the value.
+ @type size: int
+
+ @return: Random string value
+ @rtype : str
+ """
+ return ''.join(random.choice(string.hexdigits) for _ in range(size))
+
+
+def random_hash():
+ """Creates random InfoHash.
+ """
+ return InfoHash(random_str_val(size=40).encode())
+
+def timer(f, *args):
+ """
+ Start a timer which count time taken for execute function f
+
+ @param f : Function to time
+ @type f : function
+
+ @param args : Arguments of the function f
+ @type args : list
+
+ @rtype : timer
+ @return : Time taken by the function f
+ """
+ start = time.time()
+ f(*args)
+
+ return time.time() - start
+
+def reset_before_test(featureTestMethod):
+ """
+ This is a decorator for all test methods needing reset().
+
+ @param featureTestMethod: The method to be decorated. All decorated methods
+ must have 'self' object as first arg.
+ @type featureTestMethod: function
+ """
+ def call(*args, **kwargs):
+ self = args[0]
+ if isinstance(self, FeatureTest):
+ self._reset()
+ return featureTestMethod(*args, **kwargs)
+ return call
+
+def display_plot(yvals, xvals=None, yformatter=None, display_time=3, **kwargs):
+ """
+ Displays a plot of data in interactive mode. This method is made to be
+ called successively for plot refreshing.
+
+ @param yvals: Ordinate values (float).
+ @type yvals: list
+ @param xvals: Abscissa values (float).
+ @type xvals: list
+ @param yformatter: The matplotlib FuncFormatter to use for y values.
+ @type yformatter: matplotlib.ticker.FuncFormatter
+ @param displaytime: The time matplotlib can take to refresht the plot.
+ @type displaytime: int
+ """
+ plt.ion()
+ plt.clf()
+ plt.show()
+ if yformatter:
+ plt.axes().yaxis.set_major_formatter(Kbit_format)
+ if xvals:
+ plt.plot(xvals, yvals, **kwargs)
+ else:
+ plt.plot(yvals, **kwargs)
+ plt.pause(display_time)
+
+def display_traffic_plot(ifname):
+ """Displays the traffic plot for a given interface name.
+
+ @param ifname: Interface name.
+ @type ifname: string
+ """
+ ydata = []
+ xdata = []
+ # warning: infinite loop
+ interval = 2
+ for rate in iftop_traffic_data(ifname, interval=interval):
+ ydata.append(rate)
+ xdata.append((xdata[-1] if len(xdata) > 0 else 0) + interval)
+ display_plot(ydata, xvals=xdata, yformatter=Kbit_format, color='blue')
+
+def iftop_traffic_data(ifname, interval=2, rate_type='send_receive'):
+ """
+ Generator (yields data) function collecting traffic data from iftop
+ subprocess.
+
+ @param ifname: Interface to listen to.
+ @type ifname: string
+ @param interval: Interval of time between to data collections. Possible
+ values are 2, 10 or 40.
+ @type interval: int
+ @param rates: (default: send_receive) Wether to pick "send", "receive"
+ or "send and receive" rates. Possible values : "send",
+ "receive" and "send_receive".
+ @type rates: string
+ @param _format: Format in which to display data on the y axis.
+ Possible values: Mb, Kb or b.
+ @type _format: string
+ """
+ # iftop stdout string format
+ SEND_RATE_STR = "Total send rate"
+ RECEIVE_RATE_STR = "Total receive rate"
+ SEND_RECEIVE_RATE_STR = "Total send and receive rate"
+ RATE_STR = {
+ "send" : SEND_RATE_STR,
+ "receive" : RECEIVE_RATE_STR,
+ "send_receive" : SEND_RECEIVE_RATE_STR
+ }
+ TWO_SECONDS_RATE_COL = 0
+ TEN_SECONDS_RATE_COL = 1
+ FOURTY_SECONDS_RATE_COL = 2
+ COLS = {
+ 2 : TWO_SECONDS_RATE_COL,
+ 10 : TEN_SECONDS_RATE_COL,
+ 40 : FOURTY_SECONDS_RATE_COL
+ }
+ FLOAT_REGEX = "[0-9]+[.]*[0-9]*"
+ BIT_REGEX = "[KM]*b"
+
+ iftop = subprocess.Popen(["iftop", "-i", ifname, "-t"], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
+ while True:
+ line = iftop.stdout.readline().decode()
+ if RATE_STR[rate_type] in line:
+ rate, unit = re.findall("("+FLOAT_REGEX+")("+BIT_REGEX+")", line)[COLS[interval]]
+ rate = float(rate)
+ if unit == "Kb":
+ rate *= 1024
+ elif unit == "Mb":
+ rate *= 1024**2
+ yield rate
+
+###########
+# Tests #
+###########
+
+class FeatureTest(object):
+ """
+ This is a base test.
+ """
+
+ done = 0
+ lock = None
+
+ def __init__(self, test, workbench):
+ """
+ @param test: The test string indicating the test to run. This string is
+ determined in the child classes.
+ @type test: string
+
+ @param workbench: A WorkBench object to use inside this test.
+ @type workbench: WorkBench
+ """
+ self._test = test
+ self._workbench = workbench
+ self._bootstrap = self._workbench.get_bootstrap()
+
+ def _reset(self):
+ """
+ Resets some static variables.
+
+ This method is most likely going to be called before each tests.
+ """
+ FeatureTest.done = 0
+ FeatureTest.lock = threading.Condition()
+
+ def run(self):
+ raise NotImplementedError('This method must be implemented.')
+
+##################################
+# PHT #
+##################################
+
+class PhtTest(FeatureTest):
+ """TODO
+ """
+
+ indexEntries = None
+ prefix = None
+ key = None
+
+ def __init__(self, test, workbench, opts):
+ """
+ @param test: is one of the following:
+ - 'insert': indexes a considerable amount of data in
+ the PHT structure.
+ TODO
+ @type test: string
+
+ @param opts: Dictionnary containing options for the test. Allowed
+ options are:
+ - 'num_keys': this specifies the number of keys to insert
+ in the PHT during the test.
+ @type opts: dict
+ """
+ super(PhtTest, self).__init__(test, workbench)
+ self._num_keys = opts['num_keys'] if 'num_keys' in opts else 32
+ self._timer = True if 'timer' in opts else False
+
+ def _reset(self):
+ super(PhtTest, self)._reset()
+ PhtTest.indexEntries = []
+
+ @staticmethod
+ def lookupCb(vals, prefix):
+ PhtTest.indexEntries = list(vals)
+ PhtTest.prefix = prefix.decode()
+ DhtNetwork.log('Index name: <todo>')
+ DhtNetwork.log('Leaf prefix:', prefix)
+ for v in vals:
+ DhtNetwork.log('[ENTRY]:', v)
+
+ @staticmethod
+ def lookupDoneCb(ok):
+ DhtNetwork.log('[LOOKUP]:', PhtTest.key, "--", "success!" if ok else "Fail...")
+ with FeatureTest.lock:
+ FeatureTest.lock.notify()
+
+ @staticmethod
+ def insertDoneCb(ok):
+ DhtNetwork.log('[INSERT]:', PhtTest.key, "--", "success!" if ok else "Fail...")
+ with FeatureTest.lock:
+ FeatureTest.lock.notify()
+
+ @staticmethod
+ def drawTrie(trie_dict):
+ """
+ Draws the trie structure of the PHT from dictionnary.
+
+ @param trie_dict: Dictionnary of index entries (prefix -> entry).
+ @type trie_dict: dict
+ """
+ prefixes = list(trie_dict.keys())
+ if len(prefixes) == 0:
+ return
+
+ edges = list([])
+ for prefix in prefixes:
+ for i in range(-1, len(prefix)-1):
+ u = prefix[:i+1]
+ x = ("." if i == -1 else u, u+"0")
+ y = ("." if i == -1 else u, u+"1")
+ if x not in edges:
+ edges.append(x)
+ if y not in edges:
+ edges.append(y)
+
+ # TODO: use a binary tree position layout...
+ # UPDATE : In a better way [change lib]
+ G = nx.Graph(sorted(edges, key=lambda x: len(x[0])))
+ plt.title("PHT: Tree")
+ pos=graphviz_layout(G,prog='dot')
+ nx.draw(G, pos, with_labels=True, node_color='white')
+ plt.show()
+
+ def run(self):
+ try:
+ if self._test == 'insert':
+ self._insertTest()
+ except Exception as e:
+ print(e)
+ finally:
+ self._bootstrap.resize(1)
+
+ ###########
+ # Tests #
+ ###########
+
+ @reset_before_test
+ def _insertTest(self):
+ """TODO: Docstring for _massIndexTest.
+ """
+ bootstrap = self._bootstrap
+ bootstrap.resize(2)
+ dht = bootstrap.get(1)
+
+ NUM_DIG = max(math.log(self._num_keys, 2)/4, 5) # at least 5 digit keys.
+ keyspec = collections.OrderedDict([('foo', NUM_DIG)])
+ pht = Pht(b'foo_index', keyspec, dht)
+
+ DhtNetwork.log('PHT has',
+ pht.MAX_NODE_ENTRY_COUNT,
+ 'node'+ ('s' if pht.MAX_NODE_ENTRY_COUNT > 1 else ''),
+ 'per leaf bucket.')
+ keys = [{
+ [_ for _ in keyspec.keys()][0] :
+ ''.join(random.SystemRandom().choice(string.hexdigits)
+ for _ in range(NUM_DIG)).encode()
+ } for n in range(self._num_keys)]
+ all_entries = {}
+
+ # Index all entries.
+ for key in keys:
+ PhtTest.key = key
+ with FeatureTest.lock:
+ time_taken = timer(pht.insert, key, IndexValue(random_hash()), PhtTest.insertDoneCb)
+ if self._timer:
+ DhtNetwork.log('This insert step took : ', time_taken, 'second')
+ FeatureTest.lock.wait()
+
+ time.sleep(1)
+
+ # Recover entries now that the trie is complete.
+ for key in keys:
+ PhtTest.key = key
+ with FeatureTest.lock:
+ time_taken = timer(pht.lookup, key, PhtTest.lookupCb, PhtTest.lookupDoneCb)
+ if self._timer:
+ DhtNetwork.log('This lookup step took : ', time_taken, 'second')
+ FeatureTest.lock.wait()
+
+ all_entries[PhtTest.prefix] = [e.__str__()
+ for e in PhtTest.indexEntries]
+
+ for p in all_entries.keys():
+ DhtNetwork.log('All entries under prefix', p, ':')
+ DhtNetwork.log(all_entries[p])
+ PhtTest.drawTrie(all_entries)
+
+##################################
+# DHT #
+##################################
+
+class DhtFeatureTest(FeatureTest):
+ """
+ This is a base dht test.
+ """
+ #static variables used by class callbacks
+ successfullTransfer = lambda lv,fv: len(lv) == len(fv)
+ foreignNodes = None
+ foreignValues = None
+
+ def __init__(self, test, workbench):
+ super(DhtFeatureTest, self).__init__(test, workbench)
+
+ def _reset(self):
+ super(DhtFeatureTest, self)._reset()
+ DhtFeatureTest.foreignNodes = []
+ DhtFeatureTest.foreignValues = []
+
+ @staticmethod
+ def getcb(value):
+ vstr = value.__str__()[:100]
+ DhtNetwork.Log.log('[GET]: %s' % vstr + ("..." if len(vstr) > 100 else ""))
+ DhtFeatureTest.foreignValues.append(value)
+ return True
+
+ @staticmethod
+ def putDoneCb(ok, nodes):
+ with FeatureTest.lock:
+ if not ok:
+ DhtNetwork.Log.log("[PUT]: failed!")
+ FeatureTest.done -= 1
+ FeatureTest.lock.notify()
+
+ @staticmethod
+ def getDoneCb(ok, nodes):
+ with FeatureTest.lock:
+ if not ok:
+ DhtNetwork.Log.log("[GET]: failed!")
+ else:
+ for node in nodes:
+ if not node.getNode().isExpired():
+ DhtFeatureTest.foreignNodes.append(node.getId().toString())
+ FeatureTest.done -= 1
+ FeatureTest.lock.notify()
+
+ def _dhtPut(self, producer, _hash, *values):
+ with FeatureTest.lock:
+ for val in values:
+ vstr = val.__str__()[:100]
+ DhtNetwork.Log.log('[PUT]:', _hash.toString(), '->', vstr + ("..." if len(vstr) > 100 else ""))
+ FeatureTest.done += 1
+ producer.put(_hash, val, DhtFeatureTest.putDoneCb)
+ while FeatureTest.done > 0:
+ FeatureTest.lock.wait()
+
+ def _dhtGet(self, consumer, _hash):
+ DhtFeatureTest.foreignValues = []
+ DhtFeatureTest.foreignNodes = []
+ with FeatureTest.lock:
+ FeatureTest.done += 1
+ DhtNetwork.Log.log('[GET]:', _hash.toString())
+ consumer.get(_hash, DhtFeatureTest.getcb, DhtFeatureTest.getDoneCb)
+ while FeatureTest.done > 0:
+ FeatureTest.lock.wait()
+
+ def _gottaGetThemAllPokeNodes(self, consumer, hashes, nodes=None):
+ for h in hashes:
+ self._dhtGet(consumer, h)
+ if nodes is not None:
+ for n in DhtFeatureTest.foreignNodes:
+ nodes.add(n)
+
+class PersistenceTest(DhtFeatureTest):
+ """
+ This tests persistence of data on the network.
+ """
+
+ def __init__(self, test, workbench, opts):
+ """
+ @param test: is one of the following:
+ - 'mult_time': test persistence of data based on internal
+ OpenDHT storage maintenance timings.
+ - 'delete': test persistence of data upon deletion of
+ nodes.
+ - 'replace': replacing cluster successively.
+ @type test: string
+
+
+ OPTIONS
+
+ - dump_str_log: Enables storage log at test ending.
+ - keep_alive: Keeps the test running indefinately. This may be useful
+ to manually analyse the network traffic during a longer
+ period.
+ - num_producers: Number of producers of data during a DHT test.
+ - num_values: Number of values to initialize the DHT with.
+ """
+
+ # opts
+ super(PersistenceTest, self).__init__(test, workbench)
+ self._traffic_plot = True if 'traffic_plot' in opts else False
+ self._dump_storage = True if 'dump_str_log' in opts else False
+ self._op_plot = True if 'op_plot' in opts else False
+ self._keep_alive = True if 'keep_alive' in opts else False
+ self._num_producers = opts['num_producers'] if 'num_producers' in opts else None
+ self._num_values = opts['num_values'] if 'num_values' in opts else None
+
+ def _trigger_dp(self, trigger_nodes, _hash, count=1):
+ """
+ Triggers the data persistence over time. In order to this, `count` nodes
+ are created with an id around the hash of a value.
+
+ @param trigger_nodes: List of created nodes. The nodes created in this
+ function are append to this list.
+ @type trigger_nodes: list
+ @param _hash: Is the id of the value around which creating nodes.
+ @type _hash: InfoHash
+ @param count: The number of nodes to create with id around the id of
+ value.
+ @type count: int
+ """
+ _hash_str = _hash.toString().decode()
+ _hash_int = int(_hash_str, 16)
+ for i in range(int(-count/2), int(count/2)+1):
+ _hash_str = '{:40x}'.format(_hash_int + i)
+ config = DhtConfig()
+ config.setNodeId(InfoHash(_hash_str.encode()))
+ n = DhtRunner()
+ n.run(config=config)
+ n.bootstrap(self._bootstrap.ip4,
+ str(self._bootstrap.port))
+ DhtNetwork.log('Node','['+_hash_str+']',
+ 'started around', _hash.toString().decode()
+ if n.isRunning() else
+ 'failed to start...'
+ )
+ trigger_nodes.append(n)
+
+ def _result(self, local_values, new_nodes):
+ bootstrap = self._bootstrap
+ if not DhtFeatureTest.successfullTransfer(local_values, DhtFeatureTest.foreignValues):
+ DhtNetwork.Log.log('[GET]: Only %s on %s values persisted.' %
+ (len(DhtFeatureTest.foreignValues), len(local_values)))
+ else:
+ DhtNetwork.Log.log('[GET]: All values successfully persisted.')
+ if DhtFeatureTest.foreignValues:
+ if new_nodes:
+ DhtNetwork.Log.log('Values are newly found on:')
+ for node in new_nodes:
+ DhtNetwork.Log.log(node)
+ if self._dump_storage:
+ DhtNetwork.Log.log('Dumping all storage log from '\
+ 'hosting nodes.')
+ for proc in self._workbench.procs:
+ proc.sendClusterRequest(DhtNetworkSubProcess.DUMP_STORAGE_REQ, DhtFeatureTest.foreignNodes)
+ else:
+ DhtNetwork.Log.log("Values didn't reach new hosting nodes after shutdown.")
+
+ def run(self):
+ try:
+ if self._test == 'normal':
+ self._totallyNormalTest()
+ elif self._test == 'delete':
+ self._deleteTest()
+ elif self._test == 'replace':
+ self._replaceClusterTest()
+ elif self._test == 'mult_time':
+ self._multTimeTest()
+ else:
+ raise NameError("This test is not defined '" + self._test + "'")
+ except Exception as e:
+ traceback.print_tb(e.__traceback__)
+ print(type(e).__name__+':', e, file=sys.stderr)
+ finally:
+ if self._traffic_plot or self._op_plot:
+ plot_fname = "traffic-plot"
+ print('plot saved to', plot_fname)
+ plt.savefig(plot_fname)
+ self._bootstrap.resize(1)
+
+ ###########
+ # Tests #
+ ###########
+
+ @reset_before_test
+ def _totallyNormalTest(self):
+ """
+ Reproduces a network in a realistic state.
+ """
+ trigger_nodes = []
+ wb = self._workbench
+ bootstrap = self._bootstrap
+ # Value representing an ICE packet. Each ICE packet is around 1KB.
+ VALUE_SIZE = 1024
+ num_values_per_hash = self._num_values/wb.node_num if self._num_values else 5
+
+ # nodes and values counters
+ total_nr_values = 0
+ nr_nodes = wb.node_num
+ op_cv = threading.Condition()
+
+ # values string in string format. Used for sending cluster request.
+ hashes = [random_hash() for _ in range(wb.node_num)]
+
+ def normalBehavior(do, t):
+ nonlocal total_nr_values, op_cv
+ while True:
+ with op_cv:
+ do()
+ time.sleep(random.uniform(0.0, float(t)))
+
+ def putRequest():
+ nonlocal hashes, VALUE_SIZE, total_nr_values
+ lock = threading.Condition()
+ def dcb(success):
+ nonlocal total_nr_values, lock
+ if success:
+ total_nr_values += 1
+ DhtNetwork.Log.log("INFO: "+ str(total_nr_values)+" values put on the dht since begining")
+ with lock:
+ lock.notify()
+ with lock:
+ DhtNetwork.Log.warn("Random value put on the DHT...")
+ random.choice(wb.procs).sendClusterPutRequest(random.choice(hashes).toString(),
+ random_str_val(size=VALUE_SIZE).encode(),
+ done_cb=dcb)
+ lock.wait()
+
+ puts = threading.Thread(target=normalBehavior, args=(putRequest, 30.0/wb.node_num))
+ puts.daemon = True
+ puts.start()
+
+ def newNodeRequest():
+ nonlocal nr_nodes
+ lock = threading.Condition()
+ def dcb(success):
+ nonlocal nr_nodes, lock
+ nr_nodes += 1
+ DhtNetwork.Log.log("INFO: now "+str(nr_nodes)+" nodes on the dht")
+ with lock:
+ lock.notify()
+ with lock:
+ DhtNetwork.Log.warn("Node joining...")
+ random.choice(wb.procs).sendClusterRequest(DhtNetworkSubProcess.NEW_NODE_REQ, done_cb=dcb)
+ lock.wait()
+
+ connections = threading.Thread(target=normalBehavior, args=(newNodeRequest, 1*50.0/wb.node_num))
+ connections.daemon = True
+ connections.start()
+
+ def shutdownNodeRequest():
+ nonlocal nr_nodes
+ lock = threading.Condition()
+ def dcb(success):
+ nonlocal nr_nodes, lock
+ if success:
+ nr_nodes -= 1
+ DhtNetwork.Log.log("INFO: now "+str(nr_nodes)+" nodes on the dht")
+ else:
+ DhtNetwork.Log.err("Oops.. No node to shutodwn.")
+
+ with lock:
+ lock.notify()
+ with lock:
+ DhtNetwork.Log.warn("Node shutting down...")
+ random.choice(wb.procs).sendClusterRequest(DhtNetworkSubProcess.SHUTDOWN_NODE_REQ, done_cb=dcb)
+ lock.wait()
+
+ shutdowns = threading.Thread(target=normalBehavior, args=(shutdownNodeRequest, 1*60.0/wb.node_num))
+ shutdowns.daemon = True
+ shutdowns.start()
+
+ if self._traffic_plot:
+ display_traffic_plot('br'+wb.ifname)
+ else:
+ # blocks in matplotlib thread
+ while True:
+ plt.pause(3600)
+
+
+ @reset_before_test
+ def _deleteTest(self):
+ """
+ It uses Dht shutdown call from the API to gracefuly finish the nodes one
+ after the other.
+ """
+ bootstrap = self._bootstrap
+
+ ops_count = []
+
+ bootstrap.resize(3)
+ consumer = bootstrap.get(1)
+ producer = bootstrap.get(2)
+
+ myhash = random_hash()
+ local_values = [Value(b'foo'), Value(b'bar'), Value(b'foobar')]
+
+ self._dhtPut(producer, myhash, *local_values)
+
+ #checking if values were transfered
+ self._dhtGet(consumer, myhash)
+ if not DhtFeatureTest.successfullTransfer(local_values, DhtFeatureTest.foreignValues):
+ if DhtFeatureTest.foreignValues:
+ DhtNetwork.Log.log('[GET]: Only ', len(DhtFeatureTest.foreignValues) ,' on ',
+ len(local_values), ' values successfully put.')
+ else:
+ DhtNetwork.Log.log('[GET]: 0 values successfully put')
+
+
+ if DhtFeatureTest.foreignValues and DhtFeatureTest.foreignNodes:
+ DhtNetwork.Log.log('Values are found on :')
+ for node in DhtFeatureTest.foreignNodes:
+ DhtNetwork.Log.log(node)
+
+ for _ in range(max(1, int(self._workbench.node_num/32))):
+ DhtNetwork.Log.log('Removing all nodes hosting target values...')
+ cluster_ops_count = 0
+ for proc in self._workbench.procs:
+ DhtNetwork.Log.log('[REMOVE]: sending shutdown request to', proc)
+ lock = threading.Condition()
+ def dcb(success):
+ nonlocal lock
+ if not success:
+ DhtNetwork.Log.err("Failed to shutdown.")
+ with lock:
+ lock.notify()
+
+ with lock:
+ proc.sendClusterRequest(
+ DhtNetworkSubProcess.SHUTDOWN_NODE_REQ,
+ DhtFeatureTest.foreignNodes,
+ done_cb=dcb
+ )
+ lock.wait()
+ DhtNetwork.Log.log('sending message stats request')
+ def msg_dcb(stats):
+ nonlocal cluster_ops_count, lock
+ if stats:
+ cluster_ops_count += sum(stats[1:])
+ with lock:
+ lock.notify()
+ with lock:
+ proc.sendGetMessageStats(done_cb=msg_dcb)
+ lock.wait()
+ DhtNetwork.Log.log("5 seconds wait...")
+ time.sleep(5)
+ ops_count.append(cluster_ops_count/self._workbench.node_num)
+
+ # checking if values were transfered to new nodes
+ foreignNodes_before_delete = DhtFeatureTest.foreignNodes
+ DhtNetwork.Log.log('[GET]: trying to fetch persistent values')
+ self._dhtGet(consumer, myhash)
+ new_nodes = set(DhtFeatureTest.foreignNodes) - set(foreignNodes_before_delete)
+
+ self._result(local_values, new_nodes)
+
+ if self._op_plot:
+ display_plot(ops_count, color='blue')
+ else:
+ DhtNetwork.Log.log("[GET]: either couldn't fetch values or nodes hosting values...")
+
+ if traffic_plot_thread:
+ print("Traffic plot running for ever. Ctrl-c for stopping it.")
+ traffic_plot_thread.join()
+
+ @reset_before_test
+ def _replaceClusterTest(self):
+ """
+ It replaces all clusters one after the other.
+ """
+ clusters = 8
+
+ bootstrap = self._bootstrap
+
+ bootstrap.resize(3)
+ consumer = bootstrap.get(1)
+ producer = bootstrap.get(2)
+
+ myhash = random_hash()
+ local_values = [Value(b'foo'), Value(b'bar'), Value(b'foobar')]
+
+ self._dhtPut(producer, myhash, *local_values)
+ self._dhtGet(consumer, myhash)
+ initial_nodes = DhtFeatureTest.foreignNodes
+
+ DhtNetwork.Log.log('Replacing', clusters, 'random clusters successively...')
+ for n in range(clusters):
+ i = random.randint(0, len(self._workbench.procs)-1)
+ proc = self._workbench.procs[i]
+ DhtNetwork.Log.log('Replacing', proc)
+ proc.sendClusterRequest(DhtNetworkSubProcess.SHUTDOWN_CLUSTER_REQ)
+ self._workbench.stop_cluster(i)
+ self._workbench.start_cluster(i)
+
+ DhtNetwork.Log.log('[GET]: trying to fetch persistent values')
+ self._dhtGet(consumer, myhash)
+ new_nodes = set(DhtFeatureTest.foreignNodes) - set(initial_nodes)
+
+ self._result(local_values, new_nodes)
+
+ @reset_before_test
+ def _multTimeTest(self):
+ """
+ Multiple put() calls are made from multiple nodes to multiple hashes
+ after what a set of 8 nodes is created around each hashes in order to
+ enable storage maintenance each nodes. Therefor, this tests will wait 10
+ minutes for the nodes to trigger storage maintenance.
+ """
+ trigger_nodes = []
+ bootstrap = self._bootstrap
+
+ N_PRODUCERS = self._num_producers if self._num_values else 16
+ DP_TIMEOUT = 1
+
+ hashes = []
+
+ # Generating considerable amount of values of size 1KB.
+ VALUE_SIZE = 1024
+ NUM_VALUES = self._num_values if self._num_values else 50
+ values = [Value(random_str_val(size=VALUE_SIZE).encode()) for _ in range(NUM_VALUES)]
+
+ bootstrap.resize(N_PRODUCERS+2)
+ consumer = bootstrap.get(N_PRODUCERS+1)
+ producers = (bootstrap.get(n) for n in range(1,N_PRODUCERS+1))
+ for p in producers:
+ hashes.append(random_hash())
+ self._dhtPut(p, hashes[-1], *values)
+
+ once = True
+ while self._keep_alive or once:
+ nodes = set([])
+ self._gottaGetThemAllPokeNodes(consumer, hashes, nodes=nodes)
+
+ DhtNetwork.Log.log("Values are found on:")
+ for n in nodes:
+ DhtNetwork.Log.log(n)
+
+ DhtNetwork.Log.log("Creating 8 nodes around all of these hashes...")
+ for _hash in hashes:
+ self._trigger_dp(trigger_nodes, _hash, count=8)
+
+ DhtNetwork.Log.log('Waiting', DP_TIMEOUT+1, 'minutes for normal storage maintenance.')
+ time.sleep((DP_TIMEOUT+1)*60)
+
+ DhtNetwork.Log.log('Deleting old nodes from previous search.')
+ for proc in self._workbench.procs:
+ DhtNetwork.Log.log('[REMOVE]: sending delete request to', proc)
+ proc.sendClusterRequest(
+ DhtNetworkSubProcess.REMOVE_NODE_REQ,
+ nodes)
+
+ # new consumer (fresh cache)
+ bootstrap.resize(N_PRODUCERS+1)
+ bootstrap.resize(N_PRODUCERS+2)
+ consumer = bootstrap.get(N_PRODUCERS+1)
+
+ nodes_after_time = set([])
+ self._gottaGetThemAllPokeNodes(consumer, hashes, nodes=nodes_after_time)
+ self._result(values, nodes_after_time - nodes)
+
+ once = False
+
+
+class PerformanceTest(DhtFeatureTest):
+ """
+ Tests for general performance of dht operations.
+ """
+
+ def __init__(self, test, workbench, opts):
+ """
+ @param test: is one of the following:
+ - 'gets': multiple get operations and statistical results.
+ - 'delete': perform multiple put() operations followed
+ by targeted deletion of nodes hosting the values. Doing
+ so until half of the nodes on the network remain.
+ @type test: string
+ """
+ super(PerformanceTest, self).__init__(test, workbench)
+
+ def run(self):
+ try:
+ if self._test == 'gets':
+ self._getsTimesTest()
+ elif self._test == 'delete':
+ self._delete()
+ else:
+ raise NameError("This test is not defined '" + self._test + "'")
+ except Exception as e:
+ traceback.print_tb(e.__traceback__)
+ print(type(e).__name__+':', e, file=sys.stderr)
+ finally:
+ self._bootstrap.resize(1)
+
+
+ ###########
+ # Tests #
+ ###########
+
+ @reset_before_test
+ def _getsTimesTest(self):
+ """
+ Tests for performance of the DHT doing multiple get() operation.
+ """
+ bootstrap = self._bootstrap
+
+ plt.ion()
+
+ fig, axes = plt.subplots(2, 1)
+ fig.tight_layout()
+
+ lax = axes[0]
+ hax = axes[1]
+
+ lines = None#ax.plot([])
+ #plt.ylabel('time (s)')
+ hax.set_ylim(0, 2)
+
+ # let the network stabilise
+ plt.pause(20)
+
+ #start = time.time()
+ times = []
+
+ lock = threading.Condition()
+ done = 0
+
+ def getcb(v):
+ nonlocal bootstrap
+ DhtNetwork.Log.log("found", v)
+ return True
+
+ def donecb(ok, nodes, start):
+ nonlocal bootstrap, lock, done, times
+ t = time.time()-start
+ with lock:
+ if not ok:
+ DhtNetwork.Log.log("failed !")
+ times.append(t)
+ done -= 1
+ lock.notify()
+
+ def update_plot():
+ nonlocal lines
+ while lines:
+ l = lines.pop()
+ l.remove()
+ del l
+ if len(times) > 1:
+ n, bins, lines = hax.hist(times, 100, normed=1, histtype='stepfilled', color='g')
+ hax.set_ylim(min(n), max(n))
+ lines.extend(lax.plot(times, color='blue'))
+ plt.draw()
+
+ def run_get():
+ nonlocal done
+ done += 1
+ start = time.time()
+ bootstrap.front().get(InfoHash.getRandom(), getcb, lambda ok, nodes: donecb(ok, nodes, start))
+
+ plt.pause(5)
+
+ plt.show()
+ update_plot()
+
+ times = []
+ for n in range(10):
+ self._workbench.replace_cluster()
+ plt.pause(2)
+ DhtNetwork.Log.log("Getting 50 random hashes succesively.")
+ for i in range(50):
+ with lock:
+ for _ in range(1):
+ run_get()
+ while done > 0:
+ lock.wait()
+ update_plot()
+ plt.pause(.1)
+ update_plot()
+ print("Took", np.sum(times), "mean", np.mean(times), "std", np.std(times), "min", np.min(times), "max", np.max(times))
+
+ print('GET calls timings benchmark test : DONE. ' \
+ 'Close Matplotlib window for terminating the program.')
+ plt.ioff()
+ plt.show()
+
+ @reset_before_test
+ def _delete(self):
+ """
+ Tests for performance of get() and put() operations on the network while
+ deleting around the target hash.
+ """
+
+ bootstrap = self._bootstrap
+
+ bootstrap.resize(3)
+ consumer = bootstrap.get(1)
+ producer = bootstrap.get(2)
+
+ myhash = random_hash()
+ local_values = [Value(b'foo'), Value(b'bar'), Value(b'foobar')]
+
+ for _ in range(max(1, int(self._workbench.node_num/32))):
+ self._dhtGet(consumer, myhash)
+ DhtNetwork.Log.log("Waiting 15 seconds...")
+ time.sleep(15)
+
+ self._dhtPut(producer, myhash, *local_values)
+
+ #checking if values were transfered
+ self._dhtGet(consumer, myhash)
+ DhtNetwork.Log.log('Values are found on :')
+ for node in DhtFeatureTest.foreignNodes:
+ DhtNetwork.Log.log(node)
+
+ if not DhtFeatureTest.successfullTransfer(local_values, DhtFeatureTest.foreignValues):
+ if DhtFeatureTest.foreignValues:
+ DhtNetwork.Log.log('[GET]: Only ', len(DhtFeatureTest.foreignValues) ,' on ',
+ len(local_values), ' values successfully put.')
+ else:
+ DhtNetwork.Log.log('[GET]: 0 values successfully put')
+
+ DhtNetwork.Log.log('Removing all nodes hosting target values...')
+ for proc in self._workbench.procs:
+ DhtNetwork.Log.log('[REMOVE]: sending shutdown request to', proc)
+ proc.sendClusterRequest(
+ DhtNetworkSubProcess.SHUTDOWN_NODE_REQ,
+ DhtFeatureTest.foreignNodes
+ )
--- /dev/null
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015-2023 Savoir-faire Linux Inc.
+# Author: Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; If not, see <http://www.gnu.org/licenses/>.
+import argparse
+import os
+import subprocess
+
+from pyroute2 import NDB, NSPopen
+
+
+def int_range(mini, maxi):
+ def check_ifnum(arg):
+ try:
+ ret = int(arg)
+ except ValueError:
+ raise argparse.ArgumentTypeError('must be an integer')
+ if ret > maxi or ret < mini:
+ raise argparse.ArgumentTypeError(
+ f'must be {mini} <= int <= {maxi}'
+ )
+ return ret
+
+ return check_ifnum
+
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser(
+ description='Creates a virtual network topology for testing'
+ )
+ parser.add_argument(
+ '-i', '--ifname', help='interface name', default='ethdht'
+ )
+ parser.add_argument(
+ '-n',
+ '--ifnum',
+ type=int_range(1, 245),
+ help='number of isolated interfaces to create',
+ default=1,
+ )
+ parser.add_argument(
+ '-r',
+ '--remove',
+ help='remove instead of adding network interfaces',
+ action='store_true',
+ )
+ parser.add_argument(
+ '-l',
+ '--loss',
+ help='simulated packet loss (percent)',
+ type=int,
+ default=0,
+ )
+ parser.add_argument(
+ '-d', '--delay', help='simulated latency (ms)', type=int, default=0
+ )
+ parser.add_argument(
+ '-4', '--ipv4', help='Enable IPv4', action='store_true'
+ )
+ parser.add_argument(
+ '-6', '--ipv6', help='Enable IPv6', action='store_true'
+ )
+ parser.add_argument(
+ '-b',
+ '--debug',
+ help='Turn on debug logging and dump topology databases',
+ action='store_true',
+ )
+ parser.add_argument(
+ '-v',
+ '--verbose',
+ help='Turn on verbose output on netns and interfaces operations',
+ action='store_true',
+ )
+
+ args = parser.parse_args()
+
+ local_addr4 = '10.0.42.'
+ local_addr6 = '2001:db9::'
+ bripv4 = f'{local_addr4}1/24'
+ bripv6 = f'{local_addr6}1/64'
+ bridge_name = f'br{args.ifname}'
+ tap_name = f'tap{args.ifname}'
+ veth_names = []
+ namespaces = []
+ ipv4addrs = []
+ ipv6addrs = []
+ for ifn in range(args.ifnum):
+ namespaces.append(f'node{ifn}')
+ veth_names.append(f'{args.ifname}{ifn}')
+ ipv4addrs.append(f'{local_addr4}{ifn+8}/24' if args.ipv4 else None)
+ ipv6addrs.append(f'{local_addr6}{ifn+8}/64' if args.ipv6 else None)
+
+ with NDB(log='debug' if args.debug else None) as ndb:
+ if args.remove:
+ # cleanup interfaces in the main namespace
+ for iface in veth_names + [bridge_name] + [tap_name]:
+ if iface in ndb.interfaces:
+ ndb.interfaces[iface].remove().commit()
+ if args.verbose:
+ print(f'link: del main/{iface}')
+
+ # cleanup namespaces
+ for nsname in namespaces:
+ try:
+ ndb.netns[nsname].remove().commit()
+ if args.verbose:
+ print(f'netns: del {nsname}')
+ except KeyError:
+ pass
+ else:
+ # create ports
+ for veth, nsname, ipv4addr, ipv6addr in zip(
+ veth_names, namespaces, ipv4addrs, ipv6addrs
+ ):
+ # create a network namespace and launch NDB for it
+ #
+ # another possible solution could be simply to attach
+ # the namespace to the main NDB instance, but it can
+ # take a lot of memory in case of many interfaces, thus
+ # launch and discard netns NDB instances
+ netns = NDB(
+ log='debug' if args.debug else None,
+ sources=[
+ {
+ 'target': 'localhost',
+ 'netns': nsname,
+ 'kind': 'netns',
+ }
+ ],
+ )
+ if args.verbose:
+ print(f'netns: add {nsname}')
+ # create the port and push the peer into the namespace
+ (
+ ndb.interfaces.create(
+ **{
+ 'ifname': veth,
+ 'kind': 'veth',
+ 'state': 'up',
+ 'peer': {'ifname': veth, 'net_ns_fd': nsname},
+ }
+ ).commit()
+ )
+ if args.verbose:
+ print(f'link: add main/{veth} <-> {nsname}/{veth}')
+ # bring up namespace's loopback
+ (
+ netns.interfaces.wait(ifname='lo', timeout=3)
+ .set('state', 'up')
+ .commit()
+ )
+ if args.verbose:
+ print(f'link: set {nsname}/lo')
+ # bring up the peer
+ with netns.interfaces.wait(ifname=veth, timeout=3) as i:
+ i.set('state', 'up')
+ if args.ipv4:
+ i.add_ip(ipv4addr)
+ if args.ipv6:
+ i.add_ip(ipv6addr)
+ if args.verbose:
+ print(f'link: set {nsname}/{veth}, {ipv4addr}, {ipv6addr}')
+ # disconnect the namespace NDB agent, not removing the NS
+ if args.debug:
+ fname = f'{nsname}-ndb.db'
+ print(f'dump: netns topology database {fname}')
+ netns.schema.backup(fname)
+ netns.close()
+ # set up the emulation QDisc
+ nsp = NSPopen(
+ nsname,
+ [
+ 'tc',
+ 'qdisc',
+ 'add',
+ 'dev',
+ veth,
+ 'root',
+ 'netem',
+ 'delay',
+ f'{args.delay}ms',
+ f'{int(args.delay)/2}ms',
+ 'loss',
+ f'{args.loss}%',
+ '25%',
+ ],
+ stdout=subprocess.PIPE,
+ )
+ nsp.communicate()
+ nsp.wait()
+ nsp.release()
+ if args.verbose:
+ print(
+ f'netem: add {nsname}/{veth}, '
+ f'{args.delay}, {args.loss}'
+ )
+
+ # create the tap
+ #
+ # for some reason we should create the tap inteface first,
+ # and only then bring it up, thus two commit() calls
+ (
+ ndb.interfaces.create(
+ kind='tuntap', ifname=tap_name, mode='tap'
+ )
+ .commit()
+ .set('state', 'up')
+ .commit()
+ )
+ if args.verbose:
+ print(f'link: add main/{tap_name}')
+
+ # create the bridge and add all the ports
+ with ndb.interfaces.create(
+ ifname=bridge_name, kind='bridge', state='up'
+ ) as i:
+ if args.ipv4:
+ i.add_ip(bripv4)
+ if args.ipv6:
+ i.add_ip(bripv6)
+ for iface in veth_names + [tap_name]:
+ i.add_port(iface)
+ if args.verbose:
+ print(f'link: add main/{bridge_name}, {bripv4}, {bripv6}')
+
+ with open(os.devnull, 'w') as fnull:
+ if args.ipv4:
+ subprocess.call(
+ [
+ 'sysctl',
+ '-w',
+ f'net.ipv4.conf.{bridge_name}.forwarding=1',
+ ],
+ stdout=fnull,
+ )
+ if args.verbose:
+ print(f'sysctl: set {bridge_name} ipv4 forwarding')
+ if args.ipv6:
+ subprocess.call(
+ [
+ 'sysctl',
+ '-w',
+ f'net.ipv6.conf.{bridge_name}.forwarding=1',
+ ],
+ stdout=fnull,
+ )
+ if args.verbose:
+ print(f'sysctl: set {bridge_name} ipv4 forwarding')
+
+ if args.debug:
+ fname = 'main-ndb.db'
+ print('dump: the main netns topology database')
+ ndb.schema.backup(fname)
--- /dev/null
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+# Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+# Author(s): Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; If not, see <http://www.gnu.org/licenses/>.
+
+import os, sys, time, cmd
+import signal
+import argparse
+import logging
+import logging.handlers
+
+import opendht as dht
+
+logger = logging.getLogger('dhtcluster')
+
+class NodeCluster(object):
+ nodes = []
+ node_uid = 0
+
+ @staticmethod
+ def run_node(ip4, ip6, p, bootstrap=None, is_bootstrap=False, logfile=None):
+ logger.info("Running node on port %s with bootstrap %s", p, bootstrap)
+ n = dht.DhtRunner()
+ n.run(ipv4=ip4 if ip4 else "", ipv6=ip6 if ip6 else "", port=p, is_bootstrap=is_bootstrap)
+ if logfile:
+ n.enableFileLogging(logfile)
+ if bootstrap:
+ n.bootstrap(bootstrap[0], bootstrap[1])
+ time.sleep(.01)
+ return ((ip4, ip6, p), n, id)
+
+ @staticmethod
+ def find_ip(iface):
+ if not iface or iface == 'any':
+ return ('0.0.0.0','::0')
+
+ if_ip4 = netifaces.ifaddresses(iface)[netifaces.AF_INET][0]['addr']
+ if_ip6 = netifaces.ifaddresses(iface)[netifaces.AF_INET6][0]['addr']
+ return (if_ip4, if_ip6)
+
+ def __init__(self, iface=None, ip4=None, ip6=None, port=4000, bootstrap=None, first_bootstrap=False, logfile=None):
+ NodeCluster.iface = iface
+ self.port = port
+ ips = NodeCluster.find_ip(iface)
+ self.logfile = logfile
+ self.ip4 = ip4 if ip4 else ips[0]
+ self.ip6 = ip6 if ip6 else ips[1]
+ self.bootstrap = bootstrap
+ if bootstrap:
+ self.bootstrap = (bootstrap.hostname, str(bootstrap.port) if bootstrap.port else "4222")
+ else:
+ logger.info("Using fallback bootstrap %s %s", self.ip4, self.port)
+ self.bootstrap = ((self.ip4, str(self.port)))
+ if first_bootstrap:
+ logger.info("Starting bootstrap node")
+ self.nodes.append(NodeCluster.run_node(self.ip4, self.ip6, self.port, self.bootstrap, is_bootstrap=True))
+ self.bootstrap = ((self.ip4, str(self.port)))
+ self.port += 1
+ #print(self.ip4, self.ip6, self.port)
+
+ def front(self):
+ return self.nodes[0][1] if self.nodes else None
+
+ def get(self, i):
+ if not self.nodes or i < 0 or i >= len(self.nodes):
+ return None
+ return self.nodes[i][1]
+
+ def getNodeInfoById(self, id=None):
+ if id:
+ for n in self.nodes:
+ if n[1].getNodeId() == id:
+ return n
+ return None
+
+ def launch_node(self):
+ node_logfile = (self.logfile + str(self.node_uid) + '.log') if self.logfile else None
+ n = NodeCluster.run_node(self.ip4, self.ip6, self.port, self.bootstrap, logfile=node_logfile)
+ self.nodes.append(n)
+ self.port += 1
+ self.node_uid += 1
+ return n
+
+ def end_node(self):
+ if not self.nodes:
+ return
+ else:
+ n = self.nodes.pop()
+ n[1].join()
+ return True
+
+ def resize(self, n):
+ n = min(n, 500)
+ l = len(self.nodes)
+ if n == l:
+ return
+ if n > l:
+ logger.info("Launching %d nodes bound on IPv4 %s IPv6 %s", n-l, self.ip4, self.ip6)
+ for i in range(l, n):
+ self.launch_node()
+ else:
+ logger.info("Ending %d nodes", l-n)
+ for i in range(n, l):
+ self.end_node()
+
+ def close(self):
+ self.resize(0)
+
+ def getMessageStats(self):
+ stats = np.array([0,0,0,0,0])
+ for n in self.nodes:
+ stats += np.array(n[1].getNodeMessageStats())
+ stats_list = [len(self.nodes)]
+ stats_list.extend(stats.tolist())
+ return stats_list
+
+class ClusterShell(cmd.Cmd):
+ intro = 'Welcome to the OpenDHT node cluster control. Type help or ? to list commands.\n'
+ prompt = '>> '
+ net = None
+ node = None
+ log = False
+ def __init__(self, network):
+ super(ClusterShell, self).__init__()
+ self.net = network
+ def setNode(self, node=None, num=0):
+ if node == self.node:
+ return
+ if self.node:
+ self.node.disableLogging()
+ self.node = node
+ if self.node:
+ self.prompt = '('+str(num)+') >> '
+ if self.log:
+ self.node.enableLogging()
+ else:
+ self.prompt = '>> '
+ def do_exit(self, arg):
+ self.close()
+ return True
+ def do_node(self, arg):
+ if not arg:
+ self.setNode()
+ else:
+ nodenum = int(arg)
+ node = self.net.get(nodenum-1)
+ if not node:
+ print("Invalid node number:", nodenum, " (accepted: 1-", len(self.net.nodes), ")")
+ else:
+ self.setNode(node, nodenum)
+ def do_resize(self, arg):
+ if not arg:
+ return
+ try:
+ nodenum = int(arg)
+ self.net.resize(nodenum)
+ except Exception as e:
+ print("Can't resize:", e)
+ def do_ll(self, arg):
+ if self.node:
+ print('Node', self.node.getNodeId().decode())
+ else:
+ print(len(self.net.nodes), 'nodes running.')
+ def do_ls(self, arg):
+ if self.node:
+ print(self.node.getSearchesLog(0))
+ else:
+ print('No node selected.')
+ def do_log(self, arg):
+ if self.node:
+ self.log = not self.log
+ if self.log:
+ self.node.enableLogging()
+ else:
+ self.node.disableLogging()
+ def do_EOF(self, line):
+ self.close()
+ return True
+ def close(self):
+ if self.net:
+ self.net.close()
+ self.net = None
+
+if __name__ == '__main__':
+ import argparse
+ from urllib.parse import urlparse
+ net = None
+ run = True
+ def clean_quit():
+ global net, run
+ if run:
+ run = False
+ if net:
+ net.resize(0)
+ net = None
+ def quit_signal(signum, frame):
+ clean_quit()
+
+ try:
+ parser = argparse.ArgumentParser(description='Create a dht network of -n nodes')
+ parser.add_argument('-n', '--node-num', help='number of dht nodes to run', type=int, default=32)
+ parser.add_argument('-I', '--iface', help='local interface to bind', default='any')
+ parser.add_argument('-p', '--port', help='start of port range (port, port+node_num)', type=int, default=4000)
+ parser.add_argument('-b', '--bootstrap', help='bootstrap address')
+ group = parser.add_mutually_exclusive_group()
+ group.add_argument('-d', '--daemonize', help='daemonize process', action='store_true')
+ group.add_argument('-s', '--service', help='service mode (not forking)', action='store_true')
+ parser.add_argument('-l', '--log', help='log file prefix')
+ args = parser.parse_args()
+
+ if args.bootstrap:
+ args.bootstrap = urlparse('dht://'+args.bootstrap)
+
+ # setup logging
+ if args.daemonize or args.service:
+ syslog = logging.handlers.SysLogHandler(address = '/dev/log')
+ syslog.setLevel(logging.DEBUG)
+ logger.setLevel(logging.DEBUG)
+ logger.addHandler(syslog)
+ else:
+ logging.basicConfig(stream=sys.stdout,level=logging.DEBUG)
+
+ # start cluster
+ net = NodeCluster(iface=args.iface, port=args.port, bootstrap=args.bootstrap, logfile=args.log)
+
+ # main loop
+ if args.daemonize:
+ import daemon
+ context = daemon.DaemonContext()
+ context.signal_map = {
+ signal.SIGHUP: 'terminate',
+ signal.SIGTERM: quit_signal,
+ signal.SIGINT: quit_signal,
+ signal.SIGQUIT: quit_signal
+ }
+ with context:
+ net.resize(args.node_num)
+ while net:
+ time.sleep(1)
+ elif args.service:
+ signal.signal(signal.SIGTERM, quit_signal)
+ signal.signal(signal.SIGINT, quit_signal)
+ signal.signal(signal.SIGQUIT, quit_signal)
+ net.resize(args.node_num)
+ while net:
+ time.sleep(1)
+ else:
+ net.resize(args.node_num)
+ ClusterShell(net).cmdloop()
+ except Exception:
+ logger.error("Exception ", exc_info=1)
+ finally:
+ clean_quit()
+ logger.warning("Ending node cluster")
+ for handler in logger.handlers:
+ handler.close()
+ logger.removeHandler(handler)
--- /dev/null
+#!/usr/bin/env python3
+# Copyright (c) 2016-2023 Savoir-faire Linux Inc.
+# Author: Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; If not, see <http://www.gnu.org/licenses/>.
+
+from twisted.web import server, resource
+from twisted.internet import reactor, endpoints
+from urllib.parse import urlparse
+
+import opendht as dht
+import base64, json
+
+class DhtServer(resource.Resource):
+ isLeaf = True
+ node = dht.DhtRunner()
+
+ def __init__(self, port, bootstrap):
+ self.node.run(port=port)
+ b_url = urlparse('//'+bootstrap)
+ self.node.bootstrap(b_url.hostname, str(b_url.port) if b_url.port else '4222')
+
+ def render_GET(self, req):
+ uri = req.uri[1:].decode().rsplit('?', 1)[0]
+ h = dht.InfoHash(uri.encode()) if len(uri) == 40 else dht.InfoHash.get(uri)
+ w = dht.Where('WHERE '+''.join(k.decode()+'='+req.args[k][0].decode()+','
+ for k in req.args.keys()
+ if k in [b'id', b'user_type', b'value_type', b'owner', b'seq'])[:-1])
+ print('GET', '"'+uri+'"', h, w)
+ res = self.node.get(h, where=w)
+ req.setHeader(b"content-type", b"application/json")
+ return json.dumps({'{:x}'.format(v.id):{'base64':base64.b64encode(v.data).decode()} for v in res}).encode()
+
+ def render_POST(self, req):
+ uri = req.uri[1:]
+ data = req.args[b'data'][0] if b'data' in req.args else None
+ user_type = req.args[b'user_type'][0].decode() if b'user_type' in req.args else ""
+ try:
+ vid = int(req.args[b'id'][0].decode()) if b'id' in req.args else 0
+ except ValueError:
+ vid = 0
+ if not data and b'base64' in req.args:
+ data = base64.b64decode(req.args[b'base64'][0])
+ h = dht.InfoHash(uri) if len(uri) == 40 else dht.InfoHash.get(uri.decode())
+ print('POST', h, data)
+ req.setHeader(b"content-type", b"application/json")
+ if data:
+ v = dht.Value(data)
+ if vid != 0:
+ v.id = vid
+ v.user_type = user_type
+ self.node.put(h, v)
+ return json.dumps({'success':True}).encode()
+ else:
+ req.setResponseCode(400)
+ return json.dumps({'success':False, 'error':'no data parameter'}).encode()
+
+
+if __name__ == '__main__':
+ import argparse
+ parser = argparse.ArgumentParser(description='Launch an OpenDHT node with an HTTP control interface')
+ parser.add_argument('-p', '--port', help='OpenDHT port to bind', type=int, default=4222)
+ parser.add_argument('-hp', '--http-port', help='HTTP port to bind', type=int, default=8080)
+ parser.add_argument('-b', '--bootstrap', help='bootstrap address', default="bootstrap.jami.net:4222")
+ args = parser.parse_args()
+ endpoints.serverFromString(reactor, "tcp:"+str(args.http_port)).listen(server.Site(DhtServer(args.port, args.bootstrap)))
+ reactor.run()
--- /dev/null
+#!/usr/bin/env python3
+# Copyright (c) 2015-2023 Savoir-faire Linux Inc.
+# Author: Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; If not, see <http://www.gnu.org/licenses/>.
+
+import time
+import argparse
+import time
+import asyncio
+from datetime import datetime
+
+import opendht as dht
+
+parser = argparse.ArgumentParser(description='Create a dht network of -n nodes')
+parser.add_argument('-b', '--bootstrap', help='bootstrap address', default='bootstrap.jami.net')
+parser.add_argument('-n', '--num-ops', help='number of concurrent operations on the DHT', type=int, default=8)
+parser.add_argument('-p', '--period', help='duration between each test (seconds)', type=int, default=60)
+parser.add_argument('-t', '--timeout', help='timeout for a test to complete (seconds)', type=float, default=15)
+args = parser.parse_args()
+
+node1 = dht.DhtRunner()
+node1.run()
+
+node2 = dht.DhtRunner()
+node2.run()
+
+node1.bootstrap(args.bootstrap)
+node2.bootstrap(args.bootstrap)
+loop = asyncio.get_event_loop()
+
+pending_tests = {}
+keys = [dht.InfoHash.getRandom() for _ in range(args.num_ops)]
+
+def listen_cb(key, val, expired):
+ global pending_tests
+ kstr = str(key)
+ if kstr in pending_tests:
+ if pending_tests[kstr]['v'].id == val.id:
+ pending_tests.pop(kstr, None)
+ else:
+ print("Expected vid", val.id, "got", pending_tests[kstr]['v'].id)
+ return True
+
+def listen(key):
+ node1.listen(key, lambda v, e: loop.call_soon_threadsafe(listen_cb, key, v, e))
+
+for key in keys:
+ listen(key)
+
+next_test = time.time()
+while True:
+ start = time.time()
+ #print(datetime.fromtimestamp(start).strftime('%Y-%m-%d %H:%M:%S'), 'Test started')
+ for key in keys:
+ val = dht.Value(str(dht.InfoHash.getRandom()).encode())
+ pending_tests[str(key)] = {'v':val, 'c':0}
+ node2.put(key, val, lambda ok, nodes: ok)
+ while len(pending_tests):
+ loop.stop()
+ loop.run_forever()
+ time.sleep(1)
+ if time.time()-start > args.timeout:
+ print('Test timeout !')
+ exit(1)
+
+ end = time.time()
+ print(datetime.fromtimestamp(end).strftime('%Y-%m-%d %H:%M:%S'),
+ 'Test completed successfully in', end-start)
+ next_test += args.period
+ if next_test > end:
+ time.sleep(next_test-end)
--- /dev/null
+#!/usr/bin/env python3
+# Copyright (c) 2015-2023 Savoir-faire Linux Inc.
+# Author: Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; If not, see <https://www.gnu.org/licenses/>.
+
+import opendht as dht
+import time
+import asyncio
+
+config = dht.DhtConfig()
+config.setRateLimit(-1, -1)
+
+ping_node = dht.DhtRunner()
+ping_node.run(config=config)
+#ping_node.enableLogging()
+#ping_node.bootstrap("bootstrap.jami.net", "4222")
+pong_node = dht.DhtRunner()
+pong_node.run(config=config)
+#pong_node.enableLogging()
+pong_node.ping(ping_node.getBound())
+
+loc_ping = dht.InfoHash.get("toto99")
+loc_pong = dht.InfoHash.get(str(loc_ping))
+
+net = []
+for i in range(1,10):
+ node = dht.DhtRunner()
+ node.run(config=config)
+ node.ping(ping_node.getBound())
+ net.append(node)
+
+i = 0
+MAX = 2048
+
+loop = asyncio.get_event_loop()
+
+def done(h, ok):
+ pass
+ #print(h, "over", ok)
+
+def ping(node, h):
+ global i
+ i += 1
+ if i < MAX:
+ node.put(h, dht.Value(b"hey"), lambda ok, nodes: done(node.getNodeId().decode(), ok))
+ else:
+ loop.stop()
+
+def pong(node, h):
+ #print(node.getNodeId().decode(), "got ping", h, i)
+ loop.call_soon_threadsafe(ping, node, h)
+ return True
+
+t1 = time.time()
+
+ping_node.listen(loc_ping, lambda v, e: pong(pong_node, loc_pong) if not e else True)
+pong_node.listen(loc_pong, lambda v, e: pong(ping_node, loc_ping) if not e else True)
+
+ping(pong_node, loc_ping)
+
+loop.run_forever()
+
+t2 = time.time()
+
+print(MAX, "ping-pong done, took", t2 - t1, "s")
+print(1000 * (t2 - t1)/MAX, "ms per rt", MAX/(t2 - t1), "rt per s")
--- /dev/null
+#!/usr/bin/env python3
+# Copyright (c) 2015-2023 Savoir-faire Linux Inc.
+# Author: Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; If not, see <http://www.gnu.org/licenses/>.
+
+import time, sys, os
+from pprint import pprint
+from math import cos, sin, pi
+import urllib3
+import gzip
+import queue
+from geoip import geolite2
+
+sys.path.append('..')
+from opendht import *
+
+import numpy as np
+import matplotlib.pyplot as plt
+import matplotlib.patches as mpatches
+from matplotlib.colors import colorConverter
+from matplotlib.collections import RegularPolyCollection
+from matplotlib.widgets import Button
+from mpl_toolkits.basemap import Basemap
+
+import GeoIP
+
+http = urllib3.PoolManager()
+
+run = True
+done = 0
+all_nodes = NodeSet()
+nodes_ip4s = {}
+nodes_ip6s = {}
+lats = []
+lons = []
+cities=[]
+xys = []
+colors = []
+all_lines = []
+points = []
+not_found = []
+
+plt.ion()
+plt.figaspect(2.)
+
+fig, axes = plt.subplots(2, 1)
+#fig.set_size_inches(8,16,forward=True)
+fig.tight_layout()
+fig.canvas.set_window_title('OpenDHT scanner')
+
+mpx = axes[0]
+mpx.set_title("Node GeoIP")
+
+m = Basemap(projection='robin', resolution = 'l', area_thresh = 1000.0, lat_0=0, lon_0=0, ax=mpx)
+m.fillcontinents(color='#cccccc',lake_color='white')
+m.drawparallels(np.arange(-90.,120.,30.))
+m.drawmeridians(np.arange(0.,420.,60.))
+m.drawmapboundary(fill_color='white')
+plt.show()
+
+ringx = axes[1]
+ringx.set_title("Node IDs")
+ringx.set_autoscale_on(False)
+ringx.set_aspect('equal', 'datalim')
+ringx.set_xlim(-2.,2.)
+ringx.set_ylim(-1.5,1.5)
+
+exitax = plt.axes([0.92, 0.95, 0.07, 0.04])
+exitbtn = Button(exitax, 'Exit')
+reloadax = plt.axes([0.92, 0.90, 0.07, 0.04])
+button = Button(reloadax, 'Reload')
+
+collection = None
+infos = [ringx.text(1.2, -0.8, ""),
+ ringx.text(1.2, -0.9, "")]
+
+def exitcb(arg):
+ global run
+ run = False
+exitbtn.on_clicked(exitcb)
+
+def gcb(v):
+ return True
+
+r = DhtRunner()
+r.run(port=4112)
+r.bootstrap("bootstrap.jami.net", "4222")
+
+plt.pause(1)
+
+q = queue.Queue()
+
+def step(cur_h, cur_depth):
+ global done
+ done += 1
+ a = 2.*pi*cur_h.toFloat()
+ b = a + 2.*pi/(2**(cur_depth))
+ arc = []
+ lines = []
+ q.put((stepUi, (cur_h, cur_depth, arc, lines)))
+ print("step", cur_h, cur_depth)
+ r.get(cur_h, gcb, lambda d, nodes: stepdone(cur_h, cur_depth, d, nodes, arc, lines))
+
+def stepdone(cur_h, cur_depth, d, nodes, arc, lines):
+ #print("stepdone", cur_h, cur_depth)
+ q.put((nextstepUi, (nodes, arc, lines)))
+ nextstep(cur_h, cur_depth, d, nodes)
+
+def nextstep(cur_h, cur_depth, ok, nodes):
+ global done
+ if nodes:
+ commonBits = 0
+ if len(nodes) > 1:
+ snodes = NodeSet()
+ snodes.extend(nodes)
+ commonBits = InfoHash.commonBits(snodes.first(), snodes.last())
+ depth = min(8, commonBits+6)
+ if cur_depth < depth:
+ for b in range(cur_depth, depth):
+ new_h = InfoHash(cur_h.toString())
+ new_h.setBit(b, 1)
+ step(new_h, b+1)
+ else:
+ print("step done with no nodes", ok, cur_h.toString().decode())
+ done -= 1
+
+def stepUi(cur_h, cur_depth, arc, lines):
+ global all_lines
+ a = 2.*pi*cur_h.toFloat()
+ b = a + 2.*pi/(2**(cur_depth))
+ arc.append(ringx.add_patch(mpatches.Wedge([0.,0,], 1., a*180/pi, b*180/pi, fill=True, color="blue", alpha=0.5)))
+ lines.extend(ringx.plot([0, cos(a)], [0, sin(a)], 'k-', lw=1.2))
+ all_lines.extend(lines)
+
+def nextstepUi(nodes, arc=None, lines=[]):
+ for a in arc:
+ if a:
+ a.remove()
+ del a
+ for l in lines:
+ l.set_color('#aaaaaa')
+ if nodes:
+ appendNodes(nodes)
+
+def appendNodes(nodes):
+ global all_nodes
+ for n in nodes:
+ if all_nodes.insert(n):
+ appendNewNode(n)
+
+def appendNewNode(n):
+ global nodes_ip4s, nodes_ip6s, colors, xys
+ addr = b':'.join(n.getNode().getAddr().split(b':')[0:-1]).decode()
+ colors.append('red' if n.getNode().isExpired() else 'blue')
+ node_val = n.getId().toFloat()
+ xys.append((cos(node_val*2*pi), sin(node_val*2*pi)))
+ georecord = None
+ if addr[0] == '[':
+ addr = addr[1:-1]
+ if addr in nodes_ip6s:
+ nodes_ip6s[addr][1] += 1
+ else:
+ georecord = geolite2.lookup(addr)
+ nodes_ip6s[addr] = [n, 1, georecord]
+ else:
+ if addr in nodes_ip4s:
+ nodes_ip4s[addr][1] += 1
+ else:
+ georecord = geolite2.lookup(addr)
+ nodes_ip4s[addr] = [n, 1, georecord]
+ if georecord:
+ appendMapPoint(georecord)
+
+def appendMapPoint(res):
+ global lons, lats, cities
+ lons.append(res.location[1])
+ lats.append(res.location[0])
+ cities.append(res.city if res.city else (str(int(res.location[0]))+'-'+str(int(res.location[1]))))
+
+def restart(arg):
+ global collection, all_lines, all_nodes, points, done, nodes_ip4s, nodes_ip6s, lats, lons, cities, xys, colors
+ if done:
+ return
+ for l in all_lines:
+ l.remove()
+ del l
+ all_lines = []
+ all_nodes = NodeSet()
+ nodes_ip4s = {}
+ nodes_ip6s = {}
+ lats = []
+ lons = []
+ cities=[]
+ xys = []
+ colors = []
+ if collection:
+ collection.remove()
+ del collection
+ collection = None
+ for p in points:
+ p.remove()
+ del p
+ points = []
+
+ print(arg)
+ start_h = InfoHash()
+ start_h.setBit(159, 1)
+ step(start_h, 0)
+ plt.draw()
+
+def num_nodes(node_set):
+ return sorted([x for x in node_set.items()], key=lambda ip: ip[1][1])
+
+def update_plot():
+ #print("update_plot", done)
+ global m, collection, points
+ for p in points:
+ p.remove()
+ del p
+ points = []
+ x,y = m(lons,lats)
+ points.extend(m.plot(x,y,'bo'))
+ for name, xpt, ypt in zip(cities, x, y):
+ points.append(mpx.text(xpt+50000, ypt+50000, name))
+ if collection:
+ collection.remove()
+ del collection
+ collection = None
+ collection = ringx.add_collection(RegularPolyCollection(
+ int(fig.dpi), 6, sizes=(10,), facecolors=colors,
+ offsets = xys, transOffset = ringx.transData))
+
+ infos[0].set_text("{} different IPv4s".format(len(nodes_ip4s)))
+ infos[1].set_text("{} different IPv6s".format(len(nodes_ip6s)))
+
+def d(arg):
+ pass
+
+def handle_tasks():
+ while True:
+ try:
+ f = q.get_nowait()
+ f[0](*f[1])
+ q.task_done()
+ except Exception as e:
+ break;
+ update_plot()
+
+if run:
+ # start first step
+ start_h = InfoHash()
+ start_h.setBit(159, 1)
+ step(start_h, 0)
+
+while run:
+ while run and done > 0:
+ handle_tasks()
+ plt.pause(.5)
+
+ if not run:
+ break
+
+ button.on_clicked(restart)
+
+ node_ip4s = num_nodes(nodes_ip4s)
+ node_ip6s = num_nodes(nodes_ip6s)
+
+ print(all_nodes.size(), " nodes found")
+ print(all_nodes)
+ #print(len(not_found), " nodes not geolocalized")
+ #for n in not_found:
+ # print(n)
+ print('')
+ print(len(node_ip4s), " different IPv4s :")
+ for ip in node_ip4s:
+ print(ip[0], ":", str(ip[1][1]), "nodes", ("(" + ip[1][2]['city'] + ")") if ip[1][2] and ip[1][2]['city'] else "")
+ print('')
+ print(len(node_ip6s), " different IPv6s :")
+ for ip in node_ip6s:
+ print(ip[0], ":", str(ip[1][1]), "nodes", ("(" + ip[1][2]['city'] + ")") if ip[1][2] and ip[1][2]['city'] else "")
+
+ handle_tasks()
+ while run and done == 0:
+ plt.pause(.5)
+ button.on_clicked(d)
+ plt.draw()
+
+all_nodes = []
+r.join()
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ id="svg3492"
+ version="1.1"
+ inkscape:version="0.91 r13725"
+ xml:space="preserve"
+ width="256"
+ height="256"
+ viewBox="0 0 256 255.99999"
+ inkscape:export-xdpi="90"
+ inkscape:export-ydpi="90"><title
+ id="title3357">OpenDHT</title><metadata
+ id="metadata3498"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title>OpenDHT</dc:title><dc:rights><cc:Agent><dc:title></dc:title></cc:Agent></dc:rights><dc:creator><cc:Agent><dc:title>Savoir-faire Linux Inc.</dc:title></cc:Agent></dc:creator><dc:relation>https://opendht.net</dc:relation></cc:Work></rdf:RDF></metadata><defs
+ id="defs3496"><clipPath
+ clipPathUnits="userSpaceOnUse"
+ id="clipPath3534"><path
+ d="m 0,0 1152,0 0,648 L 0,648 0,0 Z"
+ id="path3536"
+ inkscape:connector-curvature="0" /></clipPath></defs><sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1855"
+ inkscape:window-height="1056"
+ id="namedview3494"
+ showgrid="false"
+ units="px"
+ fit-margin-top="5"
+ fit-margin-right="5"
+ fit-margin-bottom="5"
+ fit-margin-left="5"
+ inkscape:zoom="1.9555556"
+ inkscape:cx="63.161953"
+ inkscape:cy="141.16919"
+ inkscape:window-x="1985"
+ inkscape:window-y="24"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="g3532" /><g
+ id="g3500"
+ inkscape:groupmode="layer"
+ inkscape:label="SFL-logo-DHT"
+ transform="matrix(1.25,0,0,-1.25,-589.00821,604.10631)"><g
+ id="g3530"><g
+ id="g3532"
+ clip-path="url(#clipPath3534)"><g
+ id="g3359"
+ transform="matrix(0.9931555,0,0,0.99058777,1.5489503,0.9813209)"><g
+ transform="translate(485.3798,383.7826)"
+ id="g3538"><path
+ inkscape:connector-curvature="0"
+ id="path3540"
+ style="fill:#47b3d1;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ d="m 0,0 c 0,50.03 40.554,90.586 90.586,90.586 50.028,0 90.587,-40.556 90.587,-90.586 0,-50.03 -40.559,-90.588 -90.587,-90.588 C 40.554,-90.588 0,-50.03 0,0" /></g><g
+ transform="translate(576.0338,313.2273)"
+ id="g3542"><path
+ inkscape:connector-curvature="0"
+ id="path3544"
+ style="fill:#0091ba;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ d="m 0,0 c 0,54.307 -24.009,102.977 -61.954,136.075 -17.613,-16.526 -28.632,-39.996 -28.632,-66.056 0,-49.629 39.916,-89.912 89.393,-90.555 0.396,-0.009 0.792,-0.033 1.193,-0.033 L 0,-0.082 0,0 Z" /></g><g
+ transform="translate(575.9661,292.6626)"
+ id="g3546"><path
+ inkscape:connector-curvature="0"
+ id="path3548"
+ style="fill:#007aa3;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ d="m 0,0 c -1.439,49.482 -41.105,89.355 -90.504,91.12 0,-0.178 -0.015,-0.355 -0.015,-0.536 C -90.519,40.578 -49.998,0.039 0,0" /></g><g
+ transform="translate(532.4411,337.497)"
+ id="g3550"><path
+ inkscape:connector-curvature="0"
+ id="path3552"
+ style="fill:#006382;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ d="m 0,0 c -10.555,0 -20.236,-3.754 -27.777,-10 16.565,-21.182 42.472,-34.815 71.442,-34.834 0.01,0.415 -0.072,0.821 -0.072,1.241 C 43.593,-19.516 24.077,0 0,0" /></g><g
+ transform="translate(576.0338,313.2273)"
+ id="g3554"><path
+ inkscape:connector-curvature="0"
+ id="path3556"
+ style="fill:#0091ba;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ d="M 0,0 C 0,54.307 24.014,102.977 61.954,136.075 79.567,119.549 90.586,96.079 90.586,70.019 90.586,20.39 50.675,-19.893 1.198,-20.536 0.797,-20.545 0.406,-20.569 0.005,-20.569 l 0,20.487 C 0.005,-0.053 0,-0.029 0,0" /></g><g
+ transform="translate(576.1063,292.6626)"
+ id="g3558"><path
+ inkscape:connector-curvature="0"
+ id="path3560"
+ style="fill:#007aa3;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ d="m 0,0 c 1.44,49.482 41.105,89.355 90.504,91.12 0,-0.178 0.01,-0.355 0.01,-0.536 C 90.514,40.578 49.998,0.039 0,0" /></g><g
+ transform="translate(619.6266,337.497)"
+ id="g3562"><path
+ inkscape:connector-curvature="0"
+ id="path3564"
+ style="fill:#006382;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ d="m 0,0 c 10.56,0 20.241,-3.754 27.782,-10 -16.57,-21.182 -42.405,-34.815 -71.375,-34.834 -0.009,0.415 0,0.821 0,1.241 C -43.593,-19.516 -24.072,0 0,0" /></g></g></g></g></g></svg>
--- /dev/null
+*.lock
+target
--- /dev/null
+[package]
+name = "opendht"
+version = "0.1.0"
+authors = ["Sébastien Blin <sebastien.blin@savoirfairelinux.com>"]
+edition = "2018"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+libc="0.2.0"
+os_socketaddr="0.2.3"
\ No newline at end of file
--- /dev/null
+# Rust bindings for opendht
+
+## Build
+
+Install rust on your system (https://rustup.rs/ is the recommanded way). Once done:
+
+```
+cargo build
+```
+
+## Run tests
+
+```
+cargo test
+```
+
+## Run dht node
+
+```
+cargo run --example dhtnode
+```
\ No newline at end of file
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ * Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+extern crate opendht;
+use std::{ thread, time };
+
+use opendht::{ InfoHash, DhtRunner, DhtRunnerConfig, Value };
+// use opendht::crypto::*;
+
+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.
+ //// Note: you can generate a certificate with
+ //// openssl req -x509 -newkey rsa:4096 -sha256 -days 3650 -nodes -keyout example.key -out example.crt -subj /CN=example.com
+ //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);
+ println!("Current node id: {}", dht.node_id());
+
+ let /* mut */ data = 42;
+ let mut get_cb = |v: Box<Value>| {
+ //data += 1;
+ println!("GET: VALUE CB - data: {} - v: {}", data, v);
+ true
+ };
+ let mut done_cb = |ok: bool| {
+ println!("GET: DONE CB - data: {} - ok: {}", data, ok);
+ };
+
+ dht.get(&InfoHash::get("alice"), &mut get_cb, &mut done_cb);
+
+ let mut put_done_cb = |ok: bool| {
+ println!("PUT: DONE CB - data: {} - ok: {}", data, ok);
+ };
+ dht.put(&InfoHash::get("bob"), Value::new("hi!"), &mut put_done_cb, false);
+
+
+ println!("Start listening /foo");
+ let mut value_cb = |v, expired| {
+ println!("LISTEN: DONE CB - data: {} - v: {} - expired: {}", data, v, expired);
+ true
+ };
+ 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());
+}
\ No newline at end of file
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ * Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use crate::ffi::*;
+
+pub use crate::ffi::Blob;
+
+impl Blob {
+ pub fn data(&self) -> DataView {
+ unsafe {
+ dht_blob_get_data(self)
+ }
+ }
+}
+
+impl Drop for Blob {
+ fn drop(&mut self) {
+ unsafe {
+ dht_blob_delete(&mut *self)
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ * Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#![allow(dead_code)]
+
+pub use crate::ffi::*;
+use std::ffi::CString;
+use std::io;
+use std::io::prelude::*;
+use std::fs::File;
+use std::ptr;
+
+impl PublicKey {
+ 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: Vec<u8>) -> i32 {
+ unsafe {
+ dht_publickey_unpack(&mut *self, data.as_ptr(), data.len())
+ }
+ }
+
+ 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 {
+ dht_publickey_get_id(self)
+ }
+ }
+
+ pub fn long_id(&self) -> PkId {
+ unsafe {
+ dht_publickey_get_long_id(self)
+ }
+ }
+
+ pub fn check_signature(&self, data: &str, signature: &str) -> bool {
+ let data = CString::new(data).unwrap();
+ let signature = CString::new(signature).unwrap();
+ unsafe {
+ dht_publickey_check_signature(self,
+ data.as_ptr(), data.as_bytes().len(),
+ signature.as_ptr(), signature.as_bytes().len())
+ }
+ }
+
+ pub fn encrypt(&self, data: &str) -> Box<Blob> {
+ let data = CString::new(data).unwrap();
+ unsafe {
+ Box::from_raw(dht_publickey_encrypt(self,
+ data.as_ptr(), data.as_bytes().len()))
+ }
+ }
+}
+
+impl Drop for PublicKey {
+ fn drop(&mut self) {
+ unsafe {
+ dht_publickey_delete(&mut *self)
+ }
+ }
+}
+
+impl PrivateKey {
+ pub fn new(key_length_bits: u32) -> Box<PrivateKey> {
+ unsafe {
+ Box::from_raw(dht_privatekey_generate(key_length_bits))
+ }
+ }
+
+ pub fn import(file: &str, password: &str) -> io::Result<Box<PrivateKey>> {
+ let mut f = File::open(file)?;
+ let mut buffer = Vec::new();
+ f.read_to_end(&mut buffer)?;
+ Ok(PrivateKey::from_bytes(&buffer, password))
+ }
+
+ pub fn from_bytes(buffer: &Vec<u8>, password: &str) -> Box<PrivateKey> {
+ unsafe {
+ Box::from_raw(dht_privatekey_import((&*buffer).as_ptr(), buffer.len(),
+ password.as_ptr() as *const i8))
+ }
+ }
+
+ pub fn public_key(&self) -> Box<PublicKey> {
+ unsafe {
+ Box::from_raw(dht_privatekey_get_publickey(self))
+ }
+ }
+
+}
+
+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>> {
+ 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: &Vec<u8>) -> Box<DhtCertificate> {
+ unsafe {
+ Box::from_raw(dht_certificate_import((&*buffer).as_ptr(), buffer.len()))
+ }
+ }
+
+ pub fn from_slice(buffer: &str) -> Box<DhtCertificate> {
+ unsafe {
+ Box::from_raw(dht_certificate_import((&*buffer).as_ptr(), buffer.len()))
+ }
+ }
+
+ pub fn id(&self) -> InfoHash {
+ unsafe {
+ dht_certificate_get_id(&*self)
+ }
+ }
+
+ pub fn long_id(&self) -> PkId {
+ unsafe {
+ dht_certificate_get_long_id(&*self)
+ }
+ }
+
+ pub fn publickey(&self) -> Box<PublicKey> {
+ unsafe {
+ Box::from_raw(dht_certificate_get_publickey(&*self))
+ }
+ }
+}
+
+impl Drop for DhtCertificate {
+ fn drop(&mut self) {
+ unsafe {
+ dht_certificate_delete(&mut *self)
+ }
+ }
+}
+
+impl DhtIdentity {
+ pub fn new(common_name: &str) -> DhtIdentity {
+ unsafe {
+ DhtIdentity::generate(common_name, Box::from_raw(ptr::null_mut()))
+ }
+ }
+
+ 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)
+ }
+ }
+}
+
+impl Drop for DhtIdentity {
+ fn drop(&mut self) {
+ unsafe {
+ dht_identity_delete(&mut *self)
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ * Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#![allow(dead_code)]
+
+use libc::c_void;
+use std::ffi::CString;
+use std::ptr;
+
+pub use crate::ffi::*;
+use std::net::SocketAddr;
+use os_socketaddr::OsSocketAddr;
+
+impl DhtRunnerConfig {
+
+ pub fn new() -> Box<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: ptr::null(),
+ },
+ id: DhtIdentity {
+ privatekey: ptr::null_mut(),
+ certificate: ptr::null_mut(),
+ },
+ },
+ threaded: false,
+ proxy_server: ptr::null(),
+ push_node_id: ptr::null(),
+ push_token: ptr::null(),
+ push_topic: ptr::null(),
+ push_platform: ptr::null(),
+ peer_discovery: false,
+ peer_publish: false,
+ server_ca: ptr::null_mut(),
+ client_identity: DhtIdentity {
+ privatekey: ptr::null_mut(),
+ certificate: ptr::null_mut(),
+ },
+ });
+ unsafe {
+ dht_runner_config_default(&mut *config);
+ }
+ config
+ }
+
+ pub fn set_proxy_server(&mut self, proxy_server: &str) {
+ self.proxy_server = CString::new(proxy_server).unwrap().as_ptr();
+ }
+
+ pub fn set_push_node_id(&mut self, push_node_id: &str) {
+ self.push_node_id = CString::new(push_node_id).unwrap().as_ptr();
+ }
+
+ pub fn set_push_token(&mut self, push_token: &str) {
+ self.push_token = CString::new(push_token).unwrap().as_ptr();
+ }
+
+ pub fn set_push_topic(&mut self, push_topic: &str) {
+ self.push_topic = CString::new(push_topic).unwrap().as_ptr();
+ }
+
+ pub fn set_push_platform(&mut self, push_platform: &str) {
+ self.push_platform = CString::new(push_platform).unwrap().as_ptr();
+ }
+
+ pub fn set_identity(&mut self, certificate: Box<DhtCertificate>, privatekey: Box<PrivateKey>) {
+ self.dht_config.id.privatekey = Box::into_raw(privatekey);
+ self.dht_config.id.certificate = Box::into_raw(certificate);
+ }
+
+}
+
+impl DhtNodeConfig
+{
+ pub fn set_persist_path(&mut self, persist_path: &str) {
+ self.persist_path = CString::new(persist_path).unwrap().as_ptr();
+ }
+}
+
+struct GetHandler<'a>
+{
+ get_cb: &'a mut(dyn FnMut(Box<Value>) -> bool),
+ done_cb: &'a mut(dyn FnMut(bool))
+}
+
+impl<'a> GetHandler<'a>
+{
+ fn get_cb(&mut self, v: Box<Value>) -> bool{
+ (self.get_cb)(v)
+ }
+
+ fn done_cb(&mut self, ok: bool) {
+ (self.done_cb)(ok)
+ }
+}
+
+extern fn get_handler_cb(v: *mut Value, ptr: *mut c_void) -> bool {
+ if ptr.is_null() {
+ return true;
+ }
+ unsafe {
+ let handler = ptr as *mut GetHandler;
+ (*handler).get_cb((*v).boxed())
+ }
+}
+
+extern fn done_handler_cb(ok: bool, ptr: *mut c_void) {
+ unsafe {
+ let handler = Box::from_raw(ptr as *mut GetHandler);
+ (*handler.done_cb)(ok)
+ }
+}
+
+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 fn put_handler_done(ok: bool, ptr: *mut c_void) {
+ unsafe {
+ let handler = Box::from_raw(ptr as *mut PutHandler);
+ (*handler.done_cb)(ok)
+ }
+}
+
+struct ListenHandler<'a>
+{
+ cb: &'a mut(dyn FnMut(Box<Value>, bool) -> bool)
+}
+
+impl<'a> ListenHandler<'a>
+{
+ fn cb(&mut self, v: Box<Value>, expired: bool) -> bool {
+ (self.cb)(v, expired)
+ }
+}
+
+extern fn listen_handler(v: *mut Value, expired: bool, ptr: *mut c_void) -> bool {
+ unsafe {
+ let handler = ptr as *mut ListenHandler;
+ (*handler).cb((*v).boxed(), expired)
+ }
+}
+
+extern fn listen_handler_done(ptr: *mut c_void) {
+ unsafe {
+ Box::from_raw(ptr as *mut ListenHandler);
+ }
+}
+
+impl DhtRunner {
+ pub fn new() -> Box<DhtRunner> {
+ unsafe {
+ Box::from_raw(dht_runner_new())
+ }
+ }
+
+ 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>) {
+ unsafe {
+ dht_runner_run_config(&mut *self, port, &*config)
+ }
+ }
+
+ pub fn bootstrap(&mut self, host: &str, service: u16) {
+ unsafe {
+ dht_runner_bootstrap(&mut *self,
+ CString::new(host).unwrap().as_ptr(),
+ CString::new(service.to_string()).unwrap().as_ptr())
+ }
+ }
+
+ pub fn node_id(&self) -> InfoHash {
+ unsafe {
+ dht_runner_get_node_id(&*self)
+ }
+ }
+
+ pub fn id(&self) -> InfoHash {
+ unsafe {
+ dht_runner_get_id(&*self)
+ }
+ }
+
+ pub fn get<'a>(&mut self, h: &InfoHash,
+ get_cb: &'a mut(dyn FnMut(Box<Value>) -> bool),
+ done_cb: &'a mut(dyn FnMut(bool))) {
+ let handler = Box::new(GetHandler {
+ get_cb,
+ done_cb,
+ });
+ let handler = Box::into_raw(handler) as *mut c_void;
+ unsafe {
+ dht_runner_get(&mut *self, h, get_handler_cb, done_handler_cb, handler)
+ }
+ }
+
+ pub fn put<'a>(&mut self, h: &InfoHash, v: Box<Value>,
+ done_cb: &'a mut(dyn FnMut(bool)), permanent: bool) {
+ let handler = Box::new(PutHandler {
+ done_cb,
+ });
+ let handler = Box::into_raw(handler) as *mut c_void;
+ unsafe {
+ dht_runner_put(&mut *self, h, &*v, put_handler_done, handler, permanent)
+ }
+ }
+
+ pub fn put_signed<'a>(&mut self, h: &InfoHash, v: Box<Value>,
+ done_cb: &'a mut(dyn FnMut(bool)), permanent: bool) {
+ let handler = Box::new(PutHandler {
+ done_cb,
+ });
+ let handler = Box::into_raw(handler) as *mut c_void;
+ unsafe {
+ dht_runner_put_signed(&mut *self, h, &*v, put_handler_done, handler, permanent)
+ }
+ }
+
+ pub fn put_encrypted<'a>(&mut self, h: &InfoHash, to: &InfoHash, v: Box<Value>,
+ done_cb: &'a mut(dyn FnMut(bool)), permanent: bool) {
+ let handler = Box::new(PutHandler {
+ done_cb,
+ });
+ let handler = Box::into_raw(handler) as *mut c_void;
+ unsafe {
+ dht_runner_put_encrypted(&mut *self, h, to, &*v, put_handler_done, handler, permanent)
+ }
+ }
+
+ pub fn cancel_put<'a>(&mut self, h: &InfoHash, vid: u64) {
+ unsafe {
+ dht_runner_cancel_put(&mut *self, h, vid)
+ }
+ }
+
+ pub fn listen<'a>(&mut self, h: &InfoHash,
+ 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 {
+ Box::from_raw(dht_runner_listen(&mut *self, h, listen_handler, listen_handler_done, handler))
+ }
+ }
+
+ pub fn cancel_listen(&mut self, h: &InfoHash, token: Box<OpToken>) {
+ unsafe {
+ dht_runner_cancel_listen(&mut *self, h, &*token)
+ }
+ }
+
+ pub fn shutdown(&mut self,
+ done_cb: extern 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 mut addresses = dht_runner_get_public_address(&*self);
+ while !addresses.is_null() && !(*addresses).is_null() {
+ let sock = (*(*addresses)).into_addr();
+ if sock.is_some() {
+ result.push(sock.unwrap());
+ }
+ addresses = (addresses as usize + std::mem::size_of::<*mut OsSocketAddr>()) as *mut *mut OsSocketAddr;
+ }
+ }
+ 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)
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ * Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#![allow(dead_code)]
+
+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 data: *const u8,
+ pub size: size_t
+}
+
+#[repr(C)]
+pub struct Value
+{
+ _opaque: [u8; 0]
+}
+
+#[repr(C)]
+pub struct Blob
+{
+ _opaque: [u8; 0]
+}
+
+#[repr(C)]
+#[derive(PartialEq)]
+pub struct InfoHash
+{
+ pub d: [u8; HASH_LEN],
+}
+
+#[repr(C)]
+pub struct PkId
+{
+ pub d: [u8; PKID_LEN],
+}
+
+#[repr(C)]
+pub struct PublicKey
+{
+ _opaque: [u8; 0]
+}
+
+#[repr(C)]
+pub struct PrivateKey
+{
+ _opaque: [u8; 0]
+}
+
+#[repr(C)]
+pub struct OpToken
+{
+ _opaque: [u8; 0]
+}
+
+#[repr(C)]
+pub struct DhtRunner
+{
+ _opaque: [u8; 0]
+}
+
+#[repr(C)]
+pub struct DhtNodeConfig
+{
+ pub node_id: InfoHash,
+ pub network: u32,
+ pub is_bootstrap: bool,
+ pub maintain_storage: bool,
+ pub persist_path: *const c_char,
+}
+
+#[repr(C)]
+pub struct DhtCertificate
+{
+ _opaque: [u8; 0]
+}
+
+#[repr(C)]
+pub struct DhtIdentity
+{
+ pub privatekey: *mut PrivateKey,
+ pub certificate: *mut DhtCertificate,
+}
+
+#[repr(C)]
+pub struct DhtSecureConfig
+{
+ pub node_config: DhtNodeConfig,
+ pub id: DhtIdentity,
+}
+
+#[repr(C)]
+pub struct DhtRunnerConfig
+{
+ pub dht_config: DhtSecureConfig,
+ pub threaded: bool,
+ pub proxy_server: *const c_char,
+ pub push_node_id: *const c_char,
+ pub push_token: *const c_char,
+ pub push_topic: *const c_char,
+ pub push_platform: *const c_char,
+ pub peer_discovery: bool,
+ pub peer_publish: bool,
+ pub server_ca: *mut DhtCertificate,
+ pub client_identity: DhtIdentity,
+}
+
+
+#[link(name = "opendht-c")]
+extern {
+ // dht::Value
+ pub fn dht_value_new(data: *const u8, size: size_t) -> *mut Value;
+ pub fn dht_value_ref(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;
+
+ // dht::Blob
+ pub fn dht_blob_get_data(data: *const Blob) -> DataView;
+ pub fn dht_blob_delete(data: *mut Blob);
+
+ // dht::InfoHash
+ pub fn dht_infohash_print(h: *const InfoHash) -> *const c_char;
+ pub fn dht_infohash_random(h: *mut InfoHash);
+ pub fn dht_infohash_get(h: *mut InfoHash, dat: *mut u8, dat_size: size_t);
+ pub fn dht_infohash_from_hex(h: *mut InfoHash, dat: *const c_char);
+ pub fn dht_infohash_is_zero(j: *const InfoHash) -> bool;
+
+ // dht::PkId
+ pub fn dht_pkid_print(h: *const PkId) -> *const c_char;
+
+ // dht::crypto::PublicKey
+ pub fn dht_publickey_import(dat: *const u8, dat_size: size_t) -> *mut PublicKey;
+ pub fn dht_publickey_delete(pk: *mut PublicKey);
+ pub fn dht_publickey_unpack(pk: *mut PublicKey, dat: *const u8, dat_size: size_t) -> c_int;
+ pub fn dht_publickey_pack(pk: *mut PublicKey, out: *const c_char, out_size: size_t) -> c_int;
+ pub fn dht_publickey_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;
+
+ // dht::crypto::PrivateKey
+ pub fn dht_privatekey_generate(key_length_bits: c_uint) -> *mut PrivateKey;
+ pub fn dht_privatekey_import(dat: *const u8, data_size: size_t, password: *const c_char) -> *mut PrivateKey;
+ pub fn dht_privatekey_get_publickey(pk: *const PrivateKey) -> *mut PublicKey;
+ 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_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_delete(ca: *mut DhtIdentity);
+
+ // dht::OpToken
+ pub fn dht_op_token_delete(token: *mut OpToken);
+
+ // dht::DhtRunner
+ pub fn dht_runner_config_default(config: *mut DhtRunnerConfig);
+ pub fn dht_runner_new() -> *mut DhtRunner;
+ pub fn dht_runner_get_id(dht: *const DhtRunner) -> InfoHash;
+ pub fn dht_runner_get_node_id(dht: *const DhtRunner) -> InfoHash;
+ pub fn dht_runner_delete(dht: *mut DhtRunner);
+ pub fn dht_runner_run(dht: *mut DhtRunner, port: in_port_t);
+ pub fn dht_runner_run_config(dht: *mut DhtRunner, port: in_port_t, config: *const DhtRunnerConfig);
+ pub fn dht_runner_bootstrap(dht: *mut DhtRunner, host: *const c_char, service: *const c_char);
+ pub fn dht_runner_get(dht: *mut DhtRunner, h: *const InfoHash,
+ get_cb: extern fn(*mut Value, *mut c_void) -> bool,
+ done_cb: extern fn(bool, *mut c_void),
+ cb_user_data: *mut c_void);
+ pub fn dht_runner_put(dht: *mut DhtRunner, h: *const InfoHash, v: *const Value,
+ done_cb: extern fn(bool, *mut c_void),
+ cb_user_data: *mut c_void,
+ permanent: bool);
+ pub fn dht_runner_put_signed(dht: *mut DhtRunner, h: *const InfoHash, v: *const Value,
+ done_cb: extern fn(bool, *mut c_void),
+ cb_user_data: *mut c_void,
+ permanent: bool);
+ pub fn dht_runner_put_encrypted(dht: *mut DhtRunner, h: *const InfoHash,
+ to: *const 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_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 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;
+}
\ No newline at end of file
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ * Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use crate::ffi::*;
+use std::ffi::CStr;
+use std::ffi::CString;
+use std::fmt;
+
+pub use crate::ffi::InfoHash;
+
+impl InfoHash {
+ pub fn new() -> InfoHash {
+ InfoHash {
+ d: [0; 20]
+ }
+ }
+
+ pub fn random() -> InfoHash {
+ let mut h = InfoHash::new();
+ unsafe {
+ dht_infohash_random(&mut h);
+ }
+ h
+ }
+
+ pub fn get(data: &str) -> InfoHash {
+ let mut h = InfoHash::new();
+ unsafe {
+ let c_str = CString::new(data).unwrap();
+ dht_infohash_get(&mut h, c_str.as_ptr() as *mut u8, data.len());
+ }
+ h
+ }
+
+ pub fn from_bytes(data: &Vec<u8>) -> InfoHash {
+ let mut h = InfoHash::new();
+ unsafe {
+ dht_infohash_get(&mut h, data.as_ptr() as *mut u8, data.len());
+ }
+ h
+ }
+
+ pub fn from_hex(data: &str) -> InfoHash {
+ let mut h = InfoHash::new();
+ unsafe {
+ let c_str = CString::new(data).unwrap();
+ dht_infohash_from_hex(&mut h, c_str.as_ptr());
+ }
+ h
+ }
+
+ pub fn is_zero(&self) -> bool {
+ unsafe {
+ dht_infohash_is_zero(self)
+ }
+ }
+}
+
+impl fmt::Display for InfoHash {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ unsafe {
+ let self_str = CStr::from_ptr(
+ dht_infohash_print(self)
+ ).to_str().unwrap_or("");
+ write!(f, "{}", self_str)
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ * Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+extern crate libc;
+extern crate os_socketaddr;
+
+mod blob;
+pub mod crypto;
+mod dhtrunner;
+mod ffi;
+mod infohash;
+mod pkid;
+mod value;
+
+pub use blob::Blob;
+pub use dhtrunner::{ DhtRunner, DhtRunnerConfig, OpToken };
+pub use infohash::InfoHash;
+pub use pkid::PkId;
+pub use value::{ DataView, Value };
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ * Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use crate::ffi::*;
+use std::ffi::CStr;
+use std::fmt;
+
+pub use crate::ffi::PkId;
+
+impl fmt::Display for PkId {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ unsafe {
+ let self_str = CStr::from_ptr(
+ dht_pkid_print(self)
+ ).to_str().unwrap_or("");
+ write!(f, "{}", self_str)
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ * Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+#![allow(dead_code)]
+
+use crate::ffi::*;
+use std::fmt;
+use std::ffi::{ CString, IntoStringError };
+use std::str;
+use std::slice;
+
+pub use crate::ffi::{DataView, Value};
+
+impl Value {
+ pub fn new(data: &str) -> Box<Value> {
+ unsafe {
+ 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 {
+ dht_value_get_id(self)
+ }
+ }
+
+ fn dataview(&self) -> DataView {
+ unsafe {
+ dht_value_get_data(self)
+ }
+ }
+
+ pub fn as_bytes(&self) -> Vec<u8> {
+ unsafe {
+ let dv = dht_value_get_data(self);
+ let slice = slice::from_raw_parts_mut(dv.data as *mut _, dv.size);
+ slice.to_vec()
+ }
+ }
+
+ pub fn boxed(&mut self) -> Box<Value> {
+ unsafe {
+ Box::from_raw(dht_value_ref(self))
+ }
+ }
+
+ pub fn recipient(&self) -> InfoHash {
+ unsafe {
+ dht_value_get_recipient(self)
+ }
+ }
+
+ pub fn owner(&self) -> Option<Box<PublicKey>> {
+ unsafe {
+ let ptr = dht_value_get_owner(self);
+ if ptr.is_null() {
+ return None;
+ }
+ Some(Box::from_raw(ptr))
+ }
+ }
+
+ pub fn user_type(&self) -> Result<String, IntoStringError> {
+ unsafe {
+ CString::from_raw(dht_value_get_user_type(self) as *mut _).into_string()
+ }
+ }
+}
+
+impl Drop for Value {
+ fn drop(&mut self) {
+ unsafe {
+ dht_value_unref(&mut *self)
+ }
+ }
+}
+
+impl fmt::Display for Value {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ unsafe {
+ let dataview = self.dataview();
+ let slice = slice::from_raw_parts(dataview.data, dataview.size);
+ write!(f, "{}", str::from_utf8(slice).unwrap_or(""))
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+lib_LTLIBRARIES = libopendht.la
+
+libopendht_la_CPPFLAGS = @CPPFLAGS@ -I$(top_srcdir)/include/opendht @Argon2_CFLAGS@ @JsonCpp_CFLAGS@ @MsgPack_CFLAGS@ @OpenSSL_CFLAGS@ @Fmt_CFLAGS@
+libopendht_la_LIBADD = @Argon2_LIBS@ @JsonCpp_LIBS@ @GnuTLS_LIBS@ @Nettle_LIBS@ @OpenSSL_LIBS@ @Fmt_LIBS@
+libopendht_la_LDFLAGS = @LDFLAGS@ -version-number @OPENDHT_MAJOR_VERSION@:@OPENDHT_MINOR_VERSION@:@OPENDHT_PATCH_VERSION@
+libopendht_la_SOURCES = \
+ dht.cpp \
+ storage.h \
+ listener.h \
+ request.h \
+ search.h \
+ value_cache.h \
+ op_cache.h \
+ op_cache.cpp \
+ net.h \
+ parsed_message.h \
+ node_cache.cpp \
+ callbacks.cpp \
+ routing_table.cpp \
+ network_engine.cpp \
+ utils.cpp \
+ node.cpp \
+ value.cpp \
+ crypto.cpp \
+ securedht.cpp \
+ dhtrunner.cpp \
+ default_types.cpp \
+ log.cpp \
+ network_utils.cpp \
+ thread_pool.cpp
+
+if WIN32
+libopendht_la_SOURCES += rng.cpp
+endif
+
+nobase_include_HEADERS = \
+ ../include/opendht.h \
+ ../include/opendht/def.h \
+ ../include/opendht/dht.h \
+ ../include/opendht/callbacks.h \
+ ../include/opendht/node_cache.h \
+ ../include/opendht/node_export.h \
+ ../include/opendht/routing_table.h \
+ ../include/opendht/network_engine.h \
+ ../include/opendht/scheduler.h \
+ ../include/opendht/rate_limiter.h \
+ ../include/opendht/utils.h \
+ ../include/opendht/sockaddr.h \
+ ../include/opendht/infohash.h \
+ ../include/opendht/node.h \
+ ../include/opendht/value.h \
+ ../include/opendht/crypto.h \
+ ../include/opendht/securedht.h \
+ ../include/opendht/dhtrunner.h \
+ ../include/opendht/default_types.h \
+ ../include/opendht/log.h \
+ ../include/opendht/logger.h \
+ ../include/opendht/network_utils.h \
+ ../include/opendht/rng.h \
+ ../include/opendht/thread_pool.h
+
+if ENABLE_PROXY_SERVER
+libopendht_la_SOURCES += dht_proxy_server.cpp
+nobase_include_HEADERS += ../include/opendht/dht_proxy_server.h
+endif
+
+if ENABLE_PROXY_CLIENT
+libopendht_la_SOURCES += dht_proxy_client.cpp
+nobase_include_HEADERS += ../include/opendht/dht_proxy_client.h ../include/opendht/dht_interface.h
+endif
+
+libopendht_la_SOURCES += base64.h base64.cpp
+if PROXY_CLIENT_OR_SERVER
+libopendht_la_SOURCES += http.cpp compat/os_cert.cpp
+nobase_include_HEADERS += ../include/opendht/proxy.h ../include/opendht/http.h compat/os_cert.h
+endif
+
+if ENABLE_PEER_DISCOVERY
+libopendht_la_SOURCES += peer_discovery.cpp
+nobase_include_HEADERS += ../include/opendht/peer_discovery.h
+endif
+
+if ENABLE_INDEXATION
+libopendht_la_SOURCES += indexation/pht.cpp
+nobase_include_HEADERS += ../include/opendht/indexation/pht.h
+endif
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "base64.h"
+
+#include <cstdint>
+#include <cstdlib>
+
+/* Mainly based on the following stackoverflow question:
+ * http://stackoverflow.com/questions/342409/how-do-i-base64-encode-decode-in-c
+ */
+static const char encoding_table[] = {
+ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K',
+ 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V',
+ 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g',
+ 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r',
+ 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2',
+ '3', '4', '5', '6', '7', '8', '9', '+', '/'
+};
+
+static const size_t mod_table[] = { 0, 2, 1 };
+
+char *base64_encode(const uint8_t *input, size_t input_length,
+ char *output, size_t *output_length)
+{
+ size_t i, j;
+ size_t out_sz = *output_length;
+ *output_length = 4 * ((input_length + 2) / 3);
+ if (out_sz < *output_length || output == nullptr)
+ return nullptr;
+
+ for (i = 0, j = 0; i < input_length; ) {
+ uint8_t octet_a = i < input_length ? input[i++] : 0;
+ uint8_t octet_b = i < input_length ? input[i++] : 0;
+ uint8_t octet_c = i < input_length ? input[i++] : 0;
+
+ uint32_t triple = (octet_a << 0x10) + (octet_b << 0x08) + octet_c;
+
+ output[j++] = encoding_table[(triple >> 3 * 6) & 0x3F];
+ output[j++] = encoding_table[(triple >> 2 * 6) & 0x3F];
+ output[j++] = encoding_table[(triple >> 1 * 6) & 0x3F];
+ output[j++] = encoding_table[(triple >> 0 * 6) & 0x3F];
+ }
+
+ for (i = 0; i < mod_table[input_length % 3]; i++)
+ output[*output_length - 1 - i] = '=';
+
+ return output;
+}
+
+uint8_t *base64_decode(const char *input, size_t input_length,
+ uint8_t *output, size_t *output_length)
+{
+ size_t i, j;
+ uint8_t decoding_table[256];
+
+ uint8_t c;
+ for (c = 0; c < 64; c++)
+ decoding_table[static_cast<int>(encoding_table[c])] = c;
+
+ if (input_length % 4 != 0 || input_length < 2)
+ return nullptr;
+
+ size_t out_sz = *output_length;
+ *output_length = input_length / 4 * 3;
+ if (input[input_length - 1] == '=')
+ (*output_length)--;
+ if (input[input_length - 2] == '=')
+ (*output_length)--;
+
+ if (out_sz < *output_length || output == nullptr)
+ return nullptr;
+
+ for (i = 0, j = 0; i < input_length;) {
+ uint8_t sextet_a = input[i] == '=' ? 0 & i++
+ : decoding_table[static_cast<int>(input[i++])];
+ uint8_t sextet_b = input[i] == '=' ? 0 & i++
+ : decoding_table[static_cast<int>(input[i++])];
+ uint8_t sextet_c = input[i] == '=' ? 0 & i++
+ : decoding_table[static_cast<int>(input[i++])];
+ uint8_t sextet_d = input[i] == '=' ? 0 & i++
+ : decoding_table[static_cast<int>(input[i++])];
+
+ uint32_t triple = (sextet_a << 3 * 6) +
+ (sextet_b << 2 * 6) +
+ (sextet_c << 1 * 6) +
+ (sextet_d << 0 * 6);
+
+ if (j < *output_length)
+ output[j++] = (triple >> 2 * 8) & 0xFF;
+ if (j < *output_length)
+ output[j++] = (triple >> 1 * 8) & 0xFF;
+ if (j < *output_length)
+ output[j++] = (triple >> 0 * 8) & 0xFF;
+ }
+
+ return output;
+}
+
+std::string
+base64_encode(const std::vector<uint8_t>::const_iterator begin,
+ const std::vector<uint8_t>::const_iterator end)
+{
+ size_t output_length = 4 * ((std::distance(begin, end) + 2) / 3);
+ std::string out;
+ out.resize(output_length);
+ base64_encode(&(*begin), std::distance(begin, end),
+ &(*out.begin()), &output_length);
+ out.resize(output_length);
+ return out;
+}
+
+
+std::string
+base64_encode(const std::vector<unsigned char>& str)
+{
+ return base64_encode(str.cbegin(), str.cend());
+}
+
+std::vector<unsigned char>
+base64_decode(const std::string& str)
+{
+ size_t output_length = str.length() / 4 * 3 + 2;
+ std::vector<uint8_t> output;
+ output.resize(output_length);
+ base64_decode(str.data(), str.size(), output.data(), &output_length);
+ output.resize(output_length);
+ return output;
+}
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <string>
+#include <vector>
+
+/**
+ * Encode a buffer in base64.
+ *
+ * @param str the input buffer
+ * @return a base64-encoded buffer
+ */
+std::string base64_encode(const std::vector<unsigned char>& str);
+/**
+ * Decode a buffer in base64.
+ *
+ * @param str the input buffer
+ * @return a base64-decoded buffer
+ */
+std::vector<unsigned char> base64_decode(const std::string& str);
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ * Author(s) : Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "callbacks.h"
+
+namespace dht {
+
+
+GetCallbackSimple
+bindGetCb(GetCallbackRaw raw_cb, void* user_data)
+{
+ if (not raw_cb) return {};
+ return [=](const std::shared_ptr<Value>& value) {
+ return raw_cb(value, user_data);
+ };
+}
+
+GetCallback
+bindGetCb(GetCallbackSimple cb)
+{
+ if (not cb) return {};
+ return [cb=std::move(cb)](const std::vector<std::shared_ptr<Value>>& values) {
+ for (const auto& v : values)
+ if (not cb(v))
+ return false;
+ return true;
+ };
+}
+
+ValueCallback
+bindValueCb(ValueCallbackRaw raw_cb, void* user_data)
+{
+ if (not raw_cb) return {};
+ return [=](const std::vector<std::shared_ptr<Value>>& values, bool expired) {
+ for (const auto& v : values)
+ if (not raw_cb(v, expired, user_data))
+ return false;
+ return true;
+ };
+}
+
+ShutdownCallback
+bindShutdownCb(ShutdownCallbackRaw shutdown_cb_raw, void* user_data)
+{
+ return [=]() { shutdown_cb_raw(user_data); };
+}
+
+DoneCallback
+bindDoneCb(DoneCallbackSimple donecb)
+{
+ if (not donecb) return {};
+ using namespace std::placeholders;
+ return std::bind(std::move(donecb), _1);
+}
+
+DoneCallback
+bindDoneCb(DoneCallbackRaw raw_cb, void* user_data)
+{
+ if (not raw_cb) return {};
+ return [=](bool success, const std::vector<std::shared_ptr<Node>>& nodes) {
+ raw_cb(success, (std::vector<std::shared_ptr<Node>>*)&nodes, user_data);
+ };
+}
+
+DoneCallbackSimple
+bindDoneCbSimple(DoneCallbackSimpleRaw raw_cb, void* user_data) {
+ if (not raw_cb) return {};
+ return [=](bool success) {
+ raw_cb(success, user_data);
+ };
+}
+
+std::string
+NodeStats::toString() const
+{
+ std::ostringstream ss;
+ ss << "Known nodes: " << good_nodes << " good, " << dubious_nodes << " dubious, " << incoming_nodes << " incoming." << std::endl;
+ ss << searches << " searches, " << node_cache_size << " total cached nodes" << std::endl;
+ if (table_depth > 1) {
+ ss << "Routing table depth: " << table_depth << std::endl;
+ ss << "Network size estimation: " << getNetworkSizeEstimation() << " nodes" << std::endl;
+ }
+ return ss.str();
+}
+
+#ifdef OPENDHT_JSONCPP
+/**
+ * Build a json object from a NodeStats
+ */
+Json::Value
+NodeStats::toJson() const
+{
+ Json::Value val;
+ val["good"] = static_cast<Json::LargestUInt>(good_nodes);
+ val["dubious"] = static_cast<Json::LargestUInt>(dubious_nodes);
+ val["incoming"] = static_cast<Json::LargestUInt>(incoming_nodes);
+ if (table_depth > 1) {
+ val["table_depth"] = static_cast<Json::LargestUInt>(table_depth);
+ val["network_size_estimation"] = static_cast<Json::LargestUInt>(getNetworkSizeEstimation());
+ }
+ return val;
+}
+
+NodeStats::NodeStats(const Json::Value& val)
+{
+ if (val.isMember("good"))
+ good_nodes = static_cast<unsigned>(val["good"].asLargestUInt());
+ if (val.isMember("dubious"))
+ dubious_nodes = static_cast<unsigned>(val["dubious"].asLargestUInt());
+ if (val.isMember("incoming"))
+ incoming_nodes = static_cast<unsigned>(val["incoming"].asLargestUInt());
+ if (val.isMember("table_depth"))
+ table_depth = static_cast<unsigned>(val["table_depth"].asLargestUInt());
+}
+
+/**
+ * Build a json object from a NodeStats
+ */
+Json::Value
+NodeInfo::toJson() const
+{
+ Json::Value val;
+ if (id)
+ val["id"] = id.toString();
+ val["node_id"] = node_id.toString();
+ val["ipv4"] = ipv4.toJson();
+ val["ipv6"] = ipv6.toJson();
+ val["ops"] = Json::Value::LargestUInt(ongoing_ops);
+ return val;
+}
+
+NodeInfo::NodeInfo(const Json::Value& v)
+{
+ if (v.isMember("id"))
+ id = InfoHash(v["id"].asString());
+ node_id = InfoHash(v["node_id"].asString());
+ ipv4 = NodeStats(v["ipv4"]);
+ ipv6 = NodeStats(v["ipv6"]);
+ ongoing_ops = v["ops"].asLargestUInt();
+}
+
+#endif
+
+
+}
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ *
+ * Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#pragma once
+
+#ifndef __STRICT_ANSI__
+
+#include <stdlib.h>
+#include <process.h>
+#include <direct.h>
+#include <fcntl.h>
+
+#define R_OK 4
+#define W_OK 2
+#define F_OK 0
+
+#ifndef STDIN_FILENO
+#define STDIN_FILENO 0
+#endif
+
+#ifndef STDOUT_FILENO
+#define STDOUT_FILENO 1
+#endif
+
+#ifndef STDERR_FILENO
+#define STDERR_FILENO 2
+#endif
+
+#define srandom srand
+#define random rand
+
+#define inline __inline
+typedef int mode_t;
+#include <BaseTsd.h>
+
+#endif
--- /dev/null
+/* Getopt for Microsoft C\r
+This code is a modification of the Free Software Foundation, Inc.\r
+Getopt library for parsing command line argument the purpose was\r
+to provide a Microsoft Visual C friendly derivative. This code\r
+provides functionality for both Unicode and Multibyte builds.\r
+Date: 02/03/2011 - Ludvik Jerabek - Initial Release\r
+Version: 1.0\r
+Comment: Supports getopt, getopt_long, and getopt_long_only\r
+and POSIXLY_CORRECT environment flag\r
+License: LGPL\r
+Revisions:\r
+02/03/2011 - Ludvik Jerabek - Initial Release\r
+02/20/2011 - Ludvik Jerabek - Fixed compiler warnings at Level 4\r
+07/05/2011 - Ludvik Jerabek - Added no_argument, required_argument, optional_argument defs\r
+08/03/2011 - Ludvik Jerabek - Fixed non-argument runtime bug which caused runtime exception\r
+08/09/2011 - Ludvik Jerabek - Added code to export functions for DLL and LIB\r
+02/15/2012 - Ludvik Jerabek - Fixed _GETOPT_THROW definition missing in implementation file\r
+08/01/2012 - Ludvik Jerabek - Created separate functions for char and wchar_t characters so single dll can do both unicode and ansi\r
+10/15/2012 - Ludvik Jerabek - Modified to match latest GNU features\r
+06/19/2015 - Ludvik Jerabek - Fixed maximum option limitation caused by option_a (255) and option_w (65535) structure val variable\r
+**DISCLAIMER**\r
+THIS MATERIAL IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND,\r
+EITHER EXPRESS OR IMPLIED, INCLUDING, BUT Not LIMITED TO, THE\r
+IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR\r
+PURPOSE, OR NON-INFRINGEMENT. SOME JURISDICTIONS DO NOT ALLOW THE\r
+EXCLUSION OF IMPLIED WARRANTIES, SO THE ABOVE EXCLUSION MAY NOT\r
+APPLY TO YOU. IN NO EVENT WILL I BE LIABLE TO ANY PARTY FOR ANY\r
+DIRECT, INDIRECT, SPECIAL OR OTHER CONSEQUENTIAL DAMAGES FOR ANY\r
+USE OF THIS MATERIAL INCLUDING, WITHOUT LIMITATION, ANY LOST\r
+PROFITS, BUSINESS INTERRUPTION, LOSS OF PROGRAMS OR OTHER DATA ON\r
+YOUR INFORMATION HANDLING SYSTEM OR OTHERWISE, EVEN If WE ARE\r
+EXPRESSLY ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.\r
+*/\r
+#ifndef _CRT_SECURE_NO_WARNINGS\r
+# define _CRT_SECURE_NO_WARNINGS\r
+#endif\r
+#include <stdlib.h>\r
+#include <stdio.h>\r
+#include <malloc.h>\r
+#include "wingetopt.h"\r
+\r
+#ifdef __cplusplus\r
+ #define _GETOPT_THROW throw()\r
+#else\r
+ #define _GETOPT_THROW\r
+#endif\r
+\r
+int optind = 1;\r
+int opterr = 1;\r
+int optopt = '?';\r
+enum ENUM_ORDERING { REQUIRE_ORDER, PERMUTE, RETURN_IN_ORDER };\r
+\r
+//\r
+//\r
+// Ansi structures and functions follow\r
+//\r
+//\r
+\r
+static struct _getopt_data_a\r
+{\r
+ int optind;\r
+ int opterr;\r
+ int optopt;\r
+ char *optarg;\r
+ int __initialized;\r
+ char *__nextchar;\r
+ enum ENUM_ORDERING __ordering;\r
+ int __posixly_correct;\r
+ int __first_nonopt;\r
+ int __last_nonopt;\r
+} getopt_data_a;\r
+char *optarg_a;\r
+\r
+static void exchange_a(char **argv, struct _getopt_data_a *d)\r
+{\r
+ int bottom = d->__first_nonopt;\r
+ int middle = d->__last_nonopt;\r
+ int top = d->optind;\r
+ char *tem;\r
+ while (top > middle && middle > bottom)\r
+ {\r
+ if (top - middle > middle - bottom)\r
+ {\r
+ int len = middle - bottom;\r
+ register int i;\r
+ for (i = 0; i < len; i++)\r
+ {\r
+ tem = argv[bottom + i];\r
+ argv[bottom + i] = argv[top - (middle - bottom) + i];\r
+ argv[top - (middle - bottom) + i] = tem;\r
+ }\r
+ top -= len;\r
+ }\r
+ else\r
+ {\r
+ int len = top - middle;\r
+ register int i;\r
+ for (i = 0; i < len; i++)\r
+ {\r
+ tem = argv[bottom + i];\r
+ argv[bottom + i] = argv[middle + i];\r
+ argv[middle + i] = tem;\r
+ }\r
+ bottom += len;\r
+ }\r
+ }\r
+ d->__first_nonopt += (d->optind - d->__last_nonopt);\r
+ d->__last_nonopt = d->optind;\r
+}\r
+static const char *_getopt_initialize_a (const char *optstring, struct _getopt_data_a *d, int posixly_correct)\r
+{\r
+ d->__first_nonopt = d->__last_nonopt = d->optind;\r
+ d->__nextchar = NULL;\r
+ d->__posixly_correct = posixly_correct | !!getenv("POSIXLY_CORRECT");\r
+ if (optstring[0] == '-')\r
+ {\r
+ d->__ordering = RETURN_IN_ORDER;\r
+ ++optstring;\r
+ }\r
+ else if (optstring[0] == '+')\r
+ {\r
+ d->__ordering = REQUIRE_ORDER;\r
+ ++optstring;\r
+ }\r
+ else if (d->__posixly_correct)\r
+ d->__ordering = REQUIRE_ORDER;\r
+ else\r
+ d->__ordering = PERMUTE;\r
+ return optstring;\r
+}\r
+int _getopt_internal_r_a (int argc, char *const *argv, const char *optstring, const struct option_a *longopts, int *longind, int long_only, struct _getopt_data_a *d, int posixly_correct)\r
+{\r
+ int print_errors = d->opterr;\r
+ if (argc < 1)\r
+ return -1;\r
+ d->optarg = NULL;\r
+ if (d->optind == 0 || !d->__initialized)\r
+ {\r
+ if (d->optind == 0)\r
+ d->optind = 1;\r
+ optstring = _getopt_initialize_a (optstring, d, posixly_correct);\r
+ d->__initialized = 1;\r
+ }\r
+ else if (optstring[0] == '-' || optstring[0] == '+')\r
+ optstring++;\r
+ if (optstring[0] == ':')\r
+ print_errors = 0;\r
+ if (d->__nextchar == NULL || *d->__nextchar == '\0')\r
+ {\r
+ if (d->__last_nonopt > d->optind)\r
+ d->__last_nonopt = d->optind;\r
+ if (d->__first_nonopt > d->optind)\r
+ d->__first_nonopt = d->optind;\r
+ if (d->__ordering == PERMUTE)\r
+ {\r
+ if (d->__first_nonopt != d->__last_nonopt && d->__last_nonopt != d->optind)\r
+ exchange_a ((char **) argv, d);\r
+ else if (d->__last_nonopt != d->optind)\r
+ d->__first_nonopt = d->optind;\r
+ while (d->optind < argc && (argv[d->optind][0] != '-' || argv[d->optind][1] == '\0'))\r
+ d->optind++;\r
+ d->__last_nonopt = d->optind;\r
+ }\r
+ if (d->optind != argc && !strcmp(argv[d->optind], "--"))\r
+ {\r
+ d->optind++;\r
+ if (d->__first_nonopt != d->__last_nonopt && d->__last_nonopt != d->optind)\r
+ exchange_a((char **) argv, d);\r
+ else if (d->__first_nonopt == d->__last_nonopt)\r
+ d->__first_nonopt = d->optind;\r
+ d->__last_nonopt = argc;\r
+ d->optind = argc;\r
+ }\r
+ if (d->optind == argc)\r
+ {\r
+ if (d->__first_nonopt != d->__last_nonopt)\r
+ d->optind = d->__first_nonopt;\r
+ return -1;\r
+ }\r
+ if ((argv[d->optind][0] != '-' || argv[d->optind][1] == '\0'))\r
+ {\r
+ if (d->__ordering == REQUIRE_ORDER)\r
+ return -1;\r
+ d->optarg = argv[d->optind++];\r
+ return 1;\r
+ }\r
+ d->__nextchar = (argv[d->optind] + 1 + (longopts != NULL && argv[d->optind][1] == '-'));\r
+ }\r
+ if (longopts != NULL && (argv[d->optind][1] == '-' || (long_only && (argv[d->optind][2] || !strchr(optstring, argv[d->optind][1])))))\r
+ {\r
+ char *nameend;\r
+ unsigned int namelen;\r
+ const struct option_a *p;\r
+ const struct option_a *pfound = NULL;\r
+ struct option_list\r
+ {\r
+ const struct option_a *p;\r
+ struct option_list *next;\r
+ } *ambig_list = NULL;\r
+ int exact = 0;\r
+ int indfound = -1;\r
+ int option_index;\r
+ for (nameend = d->__nextchar; *nameend && *nameend != '='; nameend++);\r
+ namelen = (unsigned int)(nameend - d->__nextchar);\r
+ for (p = longopts, option_index = 0; p->name; p++, option_index++)\r
+ if (!strncmp(p->name, d->__nextchar, namelen))\r
+ {\r
+ if (namelen == (unsigned int)strlen(p->name))\r
+ {\r
+ pfound = p;\r
+ indfound = option_index;\r
+ exact = 1;\r
+ break;\r
+ }\r
+ else if (pfound == NULL)\r
+ {\r
+ pfound = p;\r
+ indfound = option_index;\r
+ }\r
+ else if (long_only || pfound->has_arg != p->has_arg || pfound->flag != p->flag || pfound->val != p->val)\r
+ {\r
+ struct option_list *newp = (struct option_list*)alloca(sizeof(*newp));\r
+ newp->p = p;\r
+ newp->next = ambig_list;\r
+ ambig_list = newp;\r
+ }\r
+ }\r
+ if (ambig_list != NULL && !exact)\r
+ {\r
+ if (print_errors)\r
+ {\r
+ struct option_list first;\r
+ first.p = pfound;\r
+ first.next = ambig_list;\r
+ ambig_list = &first;\r
+ fprintf (stderr, "%s: option '%s' is ambiguous; possibilities:", argv[0], argv[d->optind]);\r
+ do\r
+ {\r
+ fprintf (stderr, " '--%s'", ambig_list->p->name);\r
+ ambig_list = ambig_list->next;\r
+ }\r
+ while (ambig_list != NULL);\r
+ fputc ('\n', stderr);\r
+ }\r
+ d->__nextchar += strlen(d->__nextchar);\r
+ d->optind++;\r
+ d->optopt = 0;\r
+ return '?';\r
+ }\r
+ if (pfound != NULL)\r
+ {\r
+ option_index = indfound;\r
+ d->optind++;\r
+ if (*nameend)\r
+ {\r
+ if (pfound->has_arg)\r
+ d->optarg = nameend + 1;\r
+ else\r
+ {\r
+ if (print_errors)\r
+ {\r
+ if (argv[d->optind - 1][1] == '-')\r
+ {\r
+ fprintf(stderr, "%s: option '--%s' doesn't allow an argument\n",argv[0], pfound->name);\r
+ }\r
+ else\r
+ {\r
+ fprintf(stderr, "%s: option '%c%s' doesn't allow an argument\n",argv[0], argv[d->optind - 1][0],pfound->name);\r
+ }\r
+ }\r
+ d->__nextchar += strlen(d->__nextchar);\r
+ d->optopt = pfound->val;\r
+ return '?';\r
+ }\r
+ }\r
+ else if (pfound->has_arg == 1)\r
+ {\r
+ if (d->optind < argc)\r
+ d->optarg = argv[d->optind++];\r
+ else\r
+ {\r
+ if (print_errors)\r
+ {\r
+ fprintf(stderr,"%s: option '--%s' requires an argument\n",argv[0], pfound->name);\r
+ }\r
+ d->__nextchar += strlen(d->__nextchar);\r
+ d->optopt = pfound->val;\r
+ return optstring[0] == ':' ? ':' : '?';\r
+ }\r
+ }\r
+ d->__nextchar += strlen(d->__nextchar);\r
+ if (longind != NULL)\r
+ *longind = option_index;\r
+ if (pfound->flag)\r
+ {\r
+ *(pfound->flag) = pfound->val;\r
+ return 0;\r
+ }\r
+ return pfound->val;\r
+ }\r
+ if (!long_only || argv[d->optind][1] == '-' || strchr(optstring, *d->__nextchar) == NULL)\r
+ {\r
+ if (print_errors)\r
+ {\r
+ if (argv[d->optind][1] == '-')\r
+ {\r
+ fprintf(stderr, "%s: unrecognized option '--%s'\n",argv[0], d->__nextchar);\r
+ }\r
+ else\r
+ {\r
+ fprintf(stderr, "%s: unrecognized option '%c%s'\n",argv[0], argv[d->optind][0], d->__nextchar);\r
+ }\r
+ }\r
+ d->__nextchar = (char *)"";\r
+ d->optind++;\r
+ d->optopt = 0;\r
+ return '?';\r
+ }\r
+ }\r
+ {\r
+ char c = *d->__nextchar++;\r
+ char *temp = (char*)strchr(optstring, c);\r
+ if (*d->__nextchar == '\0')\r
+ ++d->optind;\r
+ if (temp == NULL || c == ':' || c == ';')\r
+ {\r
+ if (print_errors)\r
+ {\r
+ fprintf(stderr, "%s: invalid option -- '%c'\n", argv[0], c);\r
+ }\r
+ d->optopt = c;\r
+ return '?';\r
+ }\r
+ if (temp[0] == 'W' && temp[1] == ';')\r
+ {\r
+ char *nameend;\r
+ const struct option_a *p;\r
+ const struct option_a *pfound = NULL;\r
+ int exact = 0;\r
+ int ambig = 0;\r
+ int indfound = 0;\r
+ int option_index;\r
+ if (longopts == NULL)\r
+ goto no_longs;\r
+ if (*d->__nextchar != '\0')\r
+ {\r
+ d->optarg = d->__nextchar;\r
+ d->optind++;\r
+ }\r
+ else if (d->optind == argc)\r
+ {\r
+ if (print_errors)\r
+ {\r
+ fprintf(stderr,"%s: option requires an argument -- '%c'\n",argv[0], c);\r
+ }\r
+ d->optopt = c;\r
+ if (optstring[0] == ':')\r
+ c = ':';\r
+ else\r
+ c = '?';\r
+ return c;\r
+ }\r
+ else\r
+ d->optarg = argv[d->optind++];\r
+ for (d->__nextchar = nameend = d->optarg; *nameend && *nameend != '='; nameend++);\r
+ for (p = longopts, option_index = 0; p->name; p++, option_index++)\r
+ if (!strncmp(p->name, d->__nextchar, nameend - d->__nextchar))\r
+ {\r
+ if ((unsigned int) (nameend - d->__nextchar) == strlen(p->name))\r
+ {\r
+ pfound = p;\r
+ indfound = option_index;\r
+ exact = 1;\r
+ break;\r
+ }\r
+ else if (pfound == NULL)\r
+ {\r
+ pfound = p;\r
+ indfound = option_index;\r
+ }\r
+ else if (long_only || pfound->has_arg != p->has_arg || pfound->flag != p->flag || pfound->val != p->val)\r
+ ambig = 1;\r
+ }\r
+ if (ambig && !exact)\r
+ {\r
+ if (print_errors)\r
+ {\r
+ fprintf(stderr, "%s: option '-W %s' is ambiguous\n",argv[0], d->optarg);\r
+ }\r
+ d->__nextchar += strlen(d->__nextchar);\r
+ d->optind++;\r
+ return '?';\r
+ }\r
+ if (pfound != NULL)\r
+ {\r
+ option_index = indfound;\r
+ if (*nameend)\r
+ {\r
+ if (pfound->has_arg)\r
+ d->optarg = nameend + 1;\r
+ else\r
+ {\r
+ if (print_errors)\r
+ {\r
+ fprintf(stderr, "%s: option '-W %s' doesn't allow an argument\n",argv[0], pfound->name);\r
+ }\r
+ d->__nextchar += strlen(d->__nextchar);\r
+ return '?';\r
+ }\r
+ }\r
+ else if (pfound->has_arg == 1)\r
+ {\r
+ if (d->optind < argc)\r
+ d->optarg = argv[d->optind++];\r
+ else\r
+ {\r
+ if (print_errors)\r
+ {\r
+ fprintf(stderr, "%s: option '-W %s' requires an argument\n",argv[0], pfound->name);\r
+ }\r
+ d->__nextchar += strlen(d->__nextchar);\r
+ return optstring[0] == ':' ? ':' : '?';\r
+ }\r
+ }\r
+ else\r
+ d->optarg = NULL;\r
+ d->__nextchar += strlen(d->__nextchar);\r
+ if (longind != NULL)\r
+ *longind = option_index;\r
+ if (pfound->flag)\r
+ {\r
+ *(pfound->flag) = pfound->val;\r
+ return 0;\r
+ }\r
+ return pfound->val;\r
+ }\r
+no_longs:\r
+ d->__nextchar = NULL;\r
+ return 'W';\r
+ }\r
+ if (temp[1] == ':')\r
+ {\r
+ if (temp[2] == ':')\r
+ {\r
+ if (*d->__nextchar != '\0')\r
+ {\r
+ d->optarg = d->__nextchar;\r
+ d->optind++;\r
+ }\r
+ else\r
+ d->optarg = NULL;\r
+ d->__nextchar = NULL;\r
+ }\r
+ else\r
+ {\r
+ if (*d->__nextchar != '\0')\r
+ {\r
+ d->optarg = d->__nextchar;\r
+ d->optind++;\r
+ }\r
+ else if (d->optind == argc)\r
+ {\r
+ if (print_errors)\r
+ {\r
+ fprintf(stderr,"%s: option requires an argument -- '%c'\n",argv[0], c);\r
+ }\r
+ d->optopt = c;\r
+ if (optstring[0] == ':')\r
+ c = ':';\r
+ else\r
+ c = '?';\r
+ }\r
+ else\r
+ d->optarg = argv[d->optind++];\r
+ d->__nextchar = NULL;\r
+ }\r
+ }\r
+ return c;\r
+ }\r
+}\r
+int _getopt_internal_a (int argc, char *const *argv, const char *optstring, const struct option_a *longopts, int *longind, int long_only, int posixly_correct)\r
+{\r
+ int result;\r
+ getopt_data_a.optind = optind;\r
+ getopt_data_a.opterr = opterr;\r
+ result = _getopt_internal_r_a (argc, argv, optstring, longopts,longind, long_only, &getopt_data_a,posixly_correct);\r
+ optind = getopt_data_a.optind;\r
+ optarg_a = getopt_data_a.optarg;\r
+ optopt = getopt_data_a.optopt;\r
+ return result;\r
+}\r
+int getopt_a (int argc, char *const *argv, const char *optstring) _GETOPT_THROW\r
+{\r
+ return _getopt_internal_a (argc, argv, optstring, (const struct option_a *) 0, (int *) 0, 0, 0);\r
+}\r
+int getopt_long_a (int argc, char *const *argv, const char *options, const struct option_a *long_options, int *opt_index) _GETOPT_THROW\r
+{\r
+ return _getopt_internal_a (argc, argv, options, long_options, opt_index, 0, 0);\r
+}\r
+int getopt_long_only_a (int argc, char *const *argv, const char *options, const struct option_a *long_options, int *opt_index) _GETOPT_THROW\r
+{\r
+ return _getopt_internal_a (argc, argv, options, long_options, opt_index, 1, 0);\r
+}\r
+int _getopt_long_r_a (int argc, char *const *argv, const char *options, const struct option_a *long_options, int *opt_index, struct _getopt_data_a *d)\r
+{\r
+ return _getopt_internal_r_a (argc, argv, options, long_options, opt_index,0, d, 0);\r
+}\r
+int _getopt_long_only_r_a (int argc, char *const *argv, const char *options, const struct option_a *long_options, int *opt_index, struct _getopt_data_a *d)\r
+{\r
+ return _getopt_internal_r_a (argc, argv, options, long_options, opt_index, 1, d, 0);\r
+}\r
+\r
+//\r
+//\r
+// Unicode Structures and Functions\r
+//\r
+//\r
+\r
+static struct _getopt_data_w\r
+{\r
+ int optind;\r
+ int opterr;\r
+ int optopt;\r
+ wchar_t *optarg;\r
+ int __initialized;\r
+ wchar_t *__nextchar;\r
+ enum ENUM_ORDERING __ordering;\r
+ int __posixly_correct;\r
+ int __first_nonopt;\r
+ int __last_nonopt;\r
+} getopt_data_w;\r
+wchar_t *optarg_w;\r
+\r
+static void exchange_w(wchar_t **argv, struct _getopt_data_w *d)\r
+{\r
+ int bottom = d->__first_nonopt;\r
+ int middle = d->__last_nonopt;\r
+ int top = d->optind;\r
+ wchar_t *tem;\r
+ while (top > middle && middle > bottom)\r
+ {\r
+ if (top - middle > middle - bottom)\r
+ {\r
+ int len = middle - bottom;\r
+ register int i;\r
+ for (i = 0; i < len; i++)\r
+ {\r
+ tem = argv[bottom + i];\r
+ argv[bottom + i] = argv[top - (middle - bottom) + i];\r
+ argv[top - (middle - bottom) + i] = tem;\r
+ }\r
+ top -= len;\r
+ }\r
+ else\r
+ {\r
+ int len = top - middle;\r
+ register int i;\r
+ for (i = 0; i < len; i++)\r
+ {\r
+ tem = argv[bottom + i];\r
+ argv[bottom + i] = argv[middle + i];\r
+ argv[middle + i] = tem;\r
+ }\r
+ bottom += len;\r
+ }\r
+ }\r
+ d->__first_nonopt += (d->optind - d->__last_nonopt);\r
+ d->__last_nonopt = d->optind;\r
+}\r
+static const wchar_t *_getopt_initialize_w (const wchar_t *optstring, struct _getopt_data_w *d, int posixly_correct)\r
+{\r
+ d->__first_nonopt = d->__last_nonopt = d->optind;\r
+ d->__nextchar = NULL;\r
+ d->__posixly_correct = posixly_correct | !!_wgetenv(L"POSIXLY_CORRECT");\r
+ if (optstring[0] == L'-')\r
+ {\r
+ d->__ordering = RETURN_IN_ORDER;\r
+ ++optstring;\r
+ }\r
+ else if (optstring[0] == L'+')\r
+ {\r
+ d->__ordering = REQUIRE_ORDER;\r
+ ++optstring;\r
+ }\r
+ else if (d->__posixly_correct)\r
+ d->__ordering = REQUIRE_ORDER;\r
+ else\r
+ d->__ordering = PERMUTE;\r
+ return optstring;\r
+}\r
+int _getopt_internal_r_w (int argc, wchar_t *const *argv, const wchar_t *optstring, const struct option_w *longopts, int *longind, int long_only, struct _getopt_data_w *d, int posixly_correct)\r
+{\r
+ int print_errors = d->opterr;\r
+ if (argc < 1)\r
+ return -1;\r
+ d->optarg = NULL;\r
+ if (d->optind == 0 || !d->__initialized)\r
+ {\r
+ if (d->optind == 0)\r
+ d->optind = 1;\r
+ optstring = _getopt_initialize_w (optstring, d, posixly_correct);\r
+ d->__initialized = 1;\r
+ }\r
+ else if (optstring[0] == L'-' || optstring[0] == L'+')\r
+ optstring++;\r
+ if (optstring[0] == L':')\r
+ print_errors = 0;\r
+ if (d->__nextchar == NULL || *d->__nextchar == L'\0')\r
+ {\r
+ if (d->__last_nonopt > d->optind)\r
+ d->__last_nonopt = d->optind;\r
+ if (d->__first_nonopt > d->optind)\r
+ d->__first_nonopt = d->optind;\r
+ if (d->__ordering == PERMUTE)\r
+ {\r
+ if (d->__first_nonopt != d->__last_nonopt && d->__last_nonopt != d->optind)\r
+ exchange_w((wchar_t **) argv, d);\r
+ else if (d->__last_nonopt != d->optind)\r
+ d->__first_nonopt = d->optind;\r
+ while (d->optind < argc && (argv[d->optind][0] != L'-' || argv[d->optind][1] == L'\0'))\r
+ d->optind++;\r
+ d->__last_nonopt = d->optind;\r
+ }\r
+ if (d->optind != argc && !wcscmp(argv[d->optind], L"--"))\r
+ {\r
+ d->optind++;\r
+ if (d->__first_nonopt != d->__last_nonopt && d->__last_nonopt != d->optind)\r
+ exchange_w((wchar_t **) argv, d);\r
+ else if (d->__first_nonopt == d->__last_nonopt)\r
+ d->__first_nonopt = d->optind;\r
+ d->__last_nonopt = argc;\r
+ d->optind = argc;\r
+ }\r
+ if (d->optind == argc)\r
+ {\r
+ if (d->__first_nonopt != d->__last_nonopt)\r
+ d->optind = d->__first_nonopt;\r
+ return -1;\r
+ }\r
+ if ((argv[d->optind][0] != L'-' || argv[d->optind][1] == L'\0'))\r
+ {\r
+ if (d->__ordering == REQUIRE_ORDER)\r
+ return -1;\r
+ d->optarg = argv[d->optind++];\r
+ return 1;\r
+ }\r
+ d->__nextchar = (argv[d->optind] + 1 + (longopts != NULL && argv[d->optind][1] == L'-'));\r
+ }\r
+ if (longopts != NULL && (argv[d->optind][1] == L'-' || (long_only && (argv[d->optind][2] || !wcschr(optstring, argv[d->optind][1])))))\r
+ {\r
+ wchar_t *nameend;\r
+ unsigned int namelen;\r
+ const struct option_w *p;\r
+ const struct option_w *pfound = NULL;\r
+ struct option_list\r
+ {\r
+ const struct option_w *p;\r
+ struct option_list *next;\r
+ } *ambig_list = NULL;\r
+ int exact = 0;\r
+ int indfound = -1;\r
+ int option_index;\r
+ for (nameend = d->__nextchar; *nameend && *nameend != L'='; nameend++);\r
+ namelen = (unsigned int)(nameend - d->__nextchar);\r
+ for (p = longopts, option_index = 0; p->name; p++, option_index++)\r
+ if (!wcsncmp(p->name, d->__nextchar, namelen))\r
+ {\r
+ if (namelen == (unsigned int)wcslen(p->name))\r
+ {\r
+ pfound = p;\r
+ indfound = option_index;\r
+ exact = 1;\r
+ break;\r
+ }\r
+ else if (pfound == NULL)\r
+ {\r
+ pfound = p;\r
+ indfound = option_index;\r
+ }\r
+ else if (long_only || pfound->has_arg != p->has_arg || pfound->flag != p->flag || pfound->val != p->val)\r
+ {\r
+ struct option_list *newp = (struct option_list*)alloca(sizeof(*newp));\r
+ newp->p = p;\r
+ newp->next = ambig_list;\r
+ ambig_list = newp;\r
+ }\r
+ }\r
+ if (ambig_list != NULL && !exact)\r
+ {\r
+ if (print_errors)\r
+ {\r
+ struct option_list first;\r
+ first.p = pfound;\r
+ first.next = ambig_list;\r
+ ambig_list = &first;\r
+ fwprintf(stderr, L"%s: option '%s' is ambiguous; possibilities:", argv[0], argv[d->optind]);\r
+ do\r
+ {\r
+ fwprintf (stderr, L" '--%s'", ambig_list->p->name);\r
+ ambig_list = ambig_list->next;\r
+ }\r
+ while (ambig_list != NULL);\r
+ fputwc (L'\n', stderr);\r
+ }\r
+ d->__nextchar += wcslen(d->__nextchar);\r
+ d->optind++;\r
+ d->optopt = 0;\r
+ return L'?';\r
+ }\r
+ if (pfound != NULL)\r
+ {\r
+ option_index = indfound;\r
+ d->optind++;\r
+ if (*nameend)\r
+ {\r
+ if (pfound->has_arg)\r
+ d->optarg = nameend + 1;\r
+ else\r
+ {\r
+ if (print_errors)\r
+ {\r
+ if (argv[d->optind - 1][1] == L'-')\r
+ {\r
+ fwprintf(stderr, L"%s: option '--%s' doesn't allow an argument\n",argv[0], pfound->name);\r
+ }\r
+ else\r
+ {\r
+ fwprintf(stderr, L"%s: option '%c%s' doesn't allow an argument\n",argv[0], argv[d->optind - 1][0],pfound->name);\r
+ }\r
+ }\r
+ d->__nextchar += wcslen(d->__nextchar);\r
+ d->optopt = pfound->val;\r
+ return L'?';\r
+ }\r
+ }\r
+ else if (pfound->has_arg == 1)\r
+ {\r
+ if (d->optind < argc)\r
+ d->optarg = argv[d->optind++];\r
+ else\r
+ {\r
+ if (print_errors)\r
+ {\r
+ fwprintf(stderr,L"%s: option '--%s' requires an argument\n",argv[0], pfound->name);\r
+ }\r
+ d->__nextchar += wcslen(d->__nextchar);\r
+ d->optopt = pfound->val;\r
+ return optstring[0] == L':' ? L':' : L'?';\r
+ }\r
+ }\r
+ d->__nextchar += wcslen(d->__nextchar);\r
+ if (longind != NULL)\r
+ *longind = option_index;\r
+ if (pfound->flag)\r
+ {\r
+ *(pfound->flag) = pfound->val;\r
+ return 0;\r
+ }\r
+ return pfound->val;\r
+ }\r
+ if (!long_only || argv[d->optind][1] == L'-' || wcschr(optstring, *d->__nextchar) == NULL)\r
+ {\r
+ if (print_errors)\r
+ {\r
+ if (argv[d->optind][1] == L'-')\r
+ {\r
+ fwprintf(stderr, L"%s: unrecognized option '--%s'\n",argv[0], d->__nextchar);\r
+ }\r
+ else\r
+ {\r
+ fwprintf(stderr, L"%s: unrecognized option '%c%s'\n",argv[0], argv[d->optind][0], d->__nextchar);\r
+ }\r
+ }\r
+ d->__nextchar = (wchar_t *)L"";\r
+ d->optind++;\r
+ d->optopt = 0;\r
+ return L'?';\r
+ }\r
+ }\r
+ {\r
+ wchar_t c = *d->__nextchar++;\r
+ wchar_t *temp = (wchar_t*)wcschr(optstring, c);\r
+ if (*d->__nextchar == L'\0')\r
+ ++d->optind;\r
+ if (temp == NULL || c == L':' || c == L';')\r
+ {\r
+ if (print_errors)\r
+ {\r
+ fwprintf(stderr, L"%s: invalid option -- '%c'\n", argv[0], c);\r
+ }\r
+ d->optopt = c;\r
+ return L'?';\r
+ }\r
+ if (temp[0] == L'W' && temp[1] == L';')\r
+ {\r
+ wchar_t *nameend;\r
+ const struct option_w *p;\r
+ const struct option_w *pfound = NULL;\r
+ int exact = 0;\r
+ int ambig = 0;\r
+ int indfound = 0;\r
+ int option_index;\r
+ if (longopts == NULL)\r
+ goto no_longs;\r
+ if (*d->__nextchar != L'\0')\r
+ {\r
+ d->optarg = d->__nextchar;\r
+ d->optind++;\r
+ }\r
+ else if (d->optind == argc)\r
+ {\r
+ if (print_errors)\r
+ {\r
+ fwprintf(stderr,L"%s: option requires an argument -- '%c'\n",argv[0], c);\r
+ }\r
+ d->optopt = c;\r
+ if (optstring[0] == L':')\r
+ c = L':';\r
+ else\r
+ c = L'?';\r
+ return c;\r
+ }\r
+ else\r
+ d->optarg = argv[d->optind++];\r
+ for (d->__nextchar = nameend = d->optarg; *nameend && *nameend != L'='; nameend++);\r
+ for (p = longopts, option_index = 0; p->name; p++, option_index++)\r
+ if (!wcsncmp(p->name, d->__nextchar, nameend - d->__nextchar))\r
+ {\r
+ if ((unsigned int) (nameend - d->__nextchar) == wcslen(p->name))\r
+ {\r
+ pfound = p;\r
+ indfound = option_index;\r
+ exact = 1;\r
+ break;\r
+ }\r
+ else if (pfound == NULL)\r
+ {\r
+ pfound = p;\r
+ indfound = option_index;\r
+ }\r
+ else if (long_only || pfound->has_arg != p->has_arg || pfound->flag != p->flag || pfound->val != p->val)\r
+ ambig = 1;\r
+ }\r
+ if (ambig && !exact)\r
+ {\r
+ if (print_errors)\r
+ {\r
+ fwprintf(stderr, L"%s: option '-W %s' is ambiguous\n",argv[0], d->optarg);\r
+ }\r
+ d->__nextchar += wcslen(d->__nextchar);\r
+ d->optind++;\r
+ return L'?';\r
+ }\r
+ if (pfound != NULL)\r
+ {\r
+ option_index = indfound;\r
+ if (*nameend)\r
+ {\r
+ if (pfound->has_arg)\r
+ d->optarg = nameend + 1;\r
+ else\r
+ {\r
+ if (print_errors)\r
+ {\r
+ fwprintf(stderr, L"%s: option '-W %s' doesn't allow an argument\n",argv[0], pfound->name);\r
+ }\r
+ d->__nextchar += wcslen(d->__nextchar);\r
+ return L'?';\r
+ }\r
+ }\r
+ else if (pfound->has_arg == 1)\r
+ {\r
+ if (d->optind < argc)\r
+ d->optarg = argv[d->optind++];\r
+ else\r
+ {\r
+ if (print_errors)\r
+ {\r
+ fwprintf(stderr, L"%s: option '-W %s' requires an argument\n",argv[0], pfound->name);\r
+ }\r
+ d->__nextchar += wcslen(d->__nextchar);\r
+ return optstring[0] == L':' ? L':' : L'?';\r
+ }\r
+ }\r
+ else\r
+ d->optarg = NULL;\r
+ d->__nextchar += wcslen(d->__nextchar);\r
+ if (longind != NULL)\r
+ *longind = option_index;\r
+ if (pfound->flag)\r
+ {\r
+ *(pfound->flag) = pfound->val;\r
+ return 0;\r
+ }\r
+ return pfound->val;\r
+ }\r
+no_longs:\r
+ d->__nextchar = NULL;\r
+ return L'W';\r
+ }\r
+ if (temp[1] == L':')\r
+ {\r
+ if (temp[2] == L':')\r
+ {\r
+ if (*d->__nextchar != L'\0')\r
+ {\r
+ d->optarg = d->__nextchar;\r
+ d->optind++;\r
+ }\r
+ else\r
+ d->optarg = NULL;\r
+ d->__nextchar = NULL;\r
+ }\r
+ else\r
+ {\r
+ if (*d->__nextchar != L'\0')\r
+ {\r
+ d->optarg = d->__nextchar;\r
+ d->optind++;\r
+ }\r
+ else if (d->optind == argc)\r
+ {\r
+ if (print_errors)\r
+ {\r
+ fwprintf(stderr,L"%s: option requires an argument -- '%c'\n",argv[0], c);\r
+ }\r
+ d->optopt = c;\r
+ if (optstring[0] == L':')\r
+ c = L':';\r
+ else\r
+ c = L'?';\r
+ }\r
+ else\r
+ d->optarg = argv[d->optind++];\r
+ d->__nextchar = NULL;\r
+ }\r
+ }\r
+ return c;\r
+ }\r
+}\r
+int _getopt_internal_w (int argc, wchar_t *const *argv, const wchar_t *optstring, const struct option_w *longopts, int *longind, int long_only, int posixly_correct)\r
+{\r
+ int result;\r
+ getopt_data_w.optind = optind;\r
+ getopt_data_w.opterr = opterr;\r
+ result = _getopt_internal_r_w (argc, argv, optstring, longopts,longind, long_only, &getopt_data_w,posixly_correct);\r
+ optind = getopt_data_w.optind;\r
+ optarg_w = getopt_data_w.optarg;\r
+ optopt = getopt_data_w.optopt;\r
+ return result;\r
+}\r
+int getopt_w (int argc, wchar_t *const *argv, const wchar_t *optstring) _GETOPT_THROW\r
+{\r
+ return _getopt_internal_w (argc, argv, optstring, (const struct option_w *) 0, (int *) 0, 0, 0);\r
+}\r
+int getopt_long_w (int argc, wchar_t *const *argv, const wchar_t *options, const struct option_w *long_options, int *opt_index) _GETOPT_THROW\r
+{\r
+ return _getopt_internal_w (argc, argv, options, long_options, opt_index, 0, 0);\r
+}\r
+int getopt_long_only_w (int argc, wchar_t *const *argv, const wchar_t *options, const struct option_w *long_options, int *opt_index) _GETOPT_THROW\r
+{\r
+ return _getopt_internal_w (argc, argv, options, long_options, opt_index, 1, 0);\r
+}\r
+int _getopt_long_r_w (int argc, wchar_t *const *argv, const wchar_t *options, const struct option_w *long_options, int *opt_index, struct _getopt_data_w *d)\r
+{\r
+ return _getopt_internal_r_w (argc, argv, options, long_options, opt_index,0, d, 0);\r
+}\r
+int _getopt_long_only_r_w (int argc, wchar_t *const *argv, const wchar_t *options, const struct option_w *long_options, int *opt_index, struct _getopt_data_w *d)\r
+{\r
+ return _getopt_internal_r_w (argc, argv, options, long_options, opt_index, 1, d, 0);\r
+}
\ No newline at end of file
--- /dev/null
+#ifndef __WINGETOPT_H_\r
+ #define __WINGETOPT_H_\r
+\r
+ #ifdef _GETOPT_API\r
+ #undef _GETOPT_API\r
+ #endif\r
+\r
+ #if defined(EXPORTS_GETOPT) && defined(STATIC_GETOPT)\r
+ #error "The preprocessor definitions of EXPORTS_GETOPT and STATIC_GETOPT can only be used individually"\r
+ #elif defined(STATIC_GETOPT)\r
+ #define _GETOPT_API\r
+ #elif defined(EXPORTS_GETOPT)\r
+ #define _GETOPT_API __declspec(dllexport)\r
+ #else\r
+ #define _GETOPT_API __declspec(dllimport)\r
+ #endif\r
+\r
+ // Change behavior for C\C++\r
+ #ifdef __cplusplus\r
+ #define _BEGIN_EXTERN_C extern "C" {\r
+ #define _END_EXTERN_C }\r
+ #define _GETOPT_THROW throw()\r
+ #else\r
+ #define _BEGIN_EXTERN_C\r
+ #define _END_EXTERN_C\r
+ #define _GETOPT_THROW\r
+ #endif\r
+\r
+ // Standard GNU options\r
+ #define null_argument 0 /*Argument Null*/\r
+ #define no_argument 0 /*Argument Switch Only*/\r
+ #define required_argument 1 /*Argument Required*/\r
+ #define optional_argument 2 /*Argument Optional*/\r
+\r
+\r
+ // Shorter Options\r
+ #define ARG_NULL 0 /*Argument Null*/\r
+ #define ARG_NONE 0 /*Argument Switch Only*/\r
+ #define ARG_REQ 1 /*Argument Required*/\r
+ #define ARG_OPT 2 /*Argument Optional*/\r
+\r
+ #include <string.h>\r
+ #include <wchar.h>\r
+\r
+_BEGIN_EXTERN_C\r
+\r
+ extern _GETOPT_API int optind;\r
+ extern _GETOPT_API int opterr;\r
+ extern _GETOPT_API int optopt;\r
+\r
+ // Ansi\r
+ struct option_a\r
+ {\r
+ const char* name;\r
+ int has_arg;\r
+ int *flag;\r
+ int val;\r
+ };\r
+ extern _GETOPT_API char *optarg_a;\r
+ extern _GETOPT_API int getopt_a(int argc, char *const *argv, const char *optstring) _GETOPT_THROW;\r
+ extern _GETOPT_API int getopt_long_a(int argc, char *const *argv, const char *options, const struct option_a *long_options, int *opt_index) _GETOPT_THROW;\r
+ extern _GETOPT_API int getopt_long_only_a(int argc, char *const *argv, const char *options, const struct option_a *long_options, int *opt_index) _GETOPT_THROW;\r
+\r
+ // Unicode\r
+ struct option_w\r
+ {\r
+ const wchar_t* name;\r
+ int has_arg;\r
+ int *flag;\r
+ int val;\r
+ };\r
+ extern _GETOPT_API wchar_t *optarg_w;\r
+ extern _GETOPT_API int getopt_w(int argc, wchar_t *const *argv, const wchar_t *optstring) _GETOPT_THROW;\r
+ extern _GETOPT_API int getopt_long_w(int argc, wchar_t *const *argv, const wchar_t *options, const struct option_w *long_options, int *opt_index) _GETOPT_THROW;\r
+ extern _GETOPT_API int getopt_long_only_w(int argc, wchar_t *const *argv, const wchar_t *options, const struct option_w *long_options, int *opt_index) _GETOPT_THROW;\r
+\r
+_END_EXTERN_C\r
+\r
+ #undef _BEGIN_EXTERN_C\r
+ #undef _END_EXTERN_C\r
+ #undef _GETOPT_THROW\r
+ #undef _GETOPT_API\r
+\r
+ #ifdef _UNICODE\r
+ #define getopt getopt_w\r
+ #define getopt_long getopt_long_w\r
+ #define getopt_long_only getopt_long_only_w\r
+ #define option option_w\r
+ #define optarg optarg_w\r
+ #else\r
+ #define getopt getopt_a\r
+ #define getopt_long getopt_long_a\r
+ #define getopt_long_only getopt_long_only_a\r
+ #define option option_a\r
+ #define optarg optarg_a\r
+ #endif\r
+#endif // __WINGETOPT_H_
\ No newline at end of file
--- /dev/null
+/*\r
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.\r
+ * Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>\r
+ *\r
+ * This program is free software; you can redistribute it and/or modify\r
+ * it under the terms of the GNU General Public License as published by\r
+ * the Free Software Foundation; either version 3 of the License, or\r
+ * (at your option) any later version.\r
+ *\r
+ * This program is distributed in the hope that it will be useful,\r
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of\r
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\r
+ * GNU General Public License for more details.\r
+ *\r
+ * You should have received a copy of the GNU General Public License\r
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.\r
+ */\r
+\r
+#include "os_cert.h"\r
+\r
+#include "logger.h"\r
+\r
+#ifdef _WIN32\r
+#include <windows.h>\r
+#include <wincrypt.h>\r
+#pragma comment(lib, "crypt32.lib")\r
+#endif\r
+#include <openssl/ssl.h>\r
+\r
+#ifdef TARGET_OS_OSX\r
+#include <CoreFoundation/CoreFoundation.h>\r
+#include <Security/Security.h>\r
+#endif /*TARGET_OS_OSX*/\r
+\r
+#if EMBEDDED_ASN1_TIME_PARSE\r
+#undef X509_NAME\r
+#undef OCSP_REQUEST\r
+#undef OCSP_RESPONSE\r
+#define GENTIME_LENGTH 15\r
+#define UTCTIME_LENGTH 13\r
+#define ATOI2(ar) ((ar) += 2, ((ar)[-2] - '0') * 10 + ((ar)[-1] - '0'))\r
+extern "C" {\r
+// Taken from libressl/src/crypto/asn1/a_time_tm.c\r
+int\r
+ASN1_time_parse(const char* bytes, size_t len, struct tm* tm, int mode)\r
+{\r
+ size_t i;\r
+ int type = 0;\r
+ struct tm ltm;\r
+ struct tm* lt;\r
+ const char* p;\r
+\r
+ if (bytes == NULL)\r
+ return (-1);\r
+\r
+ /* Constrain to valid lengths. */\r
+ if (len != UTCTIME_LENGTH && len != GENTIME_LENGTH)\r
+ return (-1);\r
+\r
+ lt = tm;\r
+ if (lt == NULL) {\r
+ memset(<m, 0, sizeof(ltm));\r
+ lt = <m;\r
+ }\r
+\r
+ /* Timezone is required and must be GMT (Zulu). */\r
+ if (bytes[len - 1] != 'Z')\r
+ return (-1);\r
+\r
+ /* Make sure everything else is digits. */\r
+ for (i = 0; i < len - 1; i++) {\r
+ if (isdigit((unsigned char) bytes[i]))\r
+ continue;\r
+ return (-1);\r
+ }\r
+\r
+ /*\r
+ * Validate and convert the time\r
+ */\r
+ p = bytes;\r
+ switch (len) {\r
+ case GENTIME_LENGTH:\r
+ if (mode == V_ASN1_UTCTIME)\r
+ return (-1);\r
+ lt->tm_year = (ATOI2(p) * 100) - 1900; /* cc */\r
+ type = V_ASN1_GENERALIZEDTIME;\r
+ /* FALLTHROUGH */\r
+ case UTCTIME_LENGTH:\r
+ if (type == 0) {\r
+ if (mode == V_ASN1_GENERALIZEDTIME)\r
+ return (-1);\r
+ type = V_ASN1_UTCTIME;\r
+ }\r
+ lt->tm_year += ATOI2(p); /* yy */\r
+ if (type == V_ASN1_UTCTIME) {\r
+ if (lt->tm_year < 50)\r
+ lt->tm_year += 100;\r
+ }\r
+ lt->tm_mon = ATOI2(p) - 1; /* mm */\r
+ if (lt->tm_mon < 0 || lt->tm_mon > 11)\r
+ return (-1);\r
+ lt->tm_mday = ATOI2(p); /* dd */\r
+ if (lt->tm_mday < 1 || lt->tm_mday > 31)\r
+ return (-1);\r
+ lt->tm_hour = ATOI2(p); /* HH */\r
+ if (lt->tm_hour < 0 || lt->tm_hour > 23)\r
+ return (-1);\r
+ lt->tm_min = ATOI2(p); /* MM */\r
+ if (lt->tm_min < 0 || lt->tm_min > 59)\r
+ return (-1);\r
+ lt->tm_sec = ATOI2(p); /* SS */\r
+ /* Leap second 60 is not accepted. Reconsider later? */\r
+ if (lt->tm_sec < 0 || lt->tm_sec > 59)\r
+ return (-1);\r
+ break;\r
+ default:\r
+ return (-1);\r
+ }\r
+\r
+ return type;\r
+}\r
+}\r
+#endif /* EMBEDDED_ASN1_TIME_PARSE */\r
+\r
+namespace dht {\r
+namespace http {\r
+\r
+PEMCache::PEMCache(const std::shared_ptr<Logger>& l)\r
+ : logger(l)\r
+{\r
+#ifdef _WIN32\r
+ PCCERT_CONTEXT pContext = NULL;\r
+ HCERTSTORE hSystemStore;\r
+ hSystemStore = CertOpenSystemStore(NULL, "ROOT");\r
+ if (!hSystemStore) {\r
+ if (logger)\r
+ logger->e("couldn't open the system cert store");\r
+ return;\r
+ }\r
+ while (pContext = CertEnumCertificatesInStore(hSystemStore, pContext)) {\r
+ const uint8_t* encoded_cert = pContext->pbCertEncoded;\r
+ X509Ptr x509cert(d2i_X509(nullptr, &encoded_cert, pContext->cbCertEncoded), &X509_free);\r
+ if (x509cert)\r
+ pems_.emplace_back(std::move(x509cert));\r
+ }\r
+ CertCloseStore(hSystemStore, 0);\r
+#elif TARGET_OS_OSX\r
+ CFArrayRef result = NULL;\r
+ OSStatus osStatus;\r
+\r
+ if ((osStatus = SecTrustCopyAnchorCertificates(&result)) != 0) {\r
+ if (logger) {\r
+ CFStringRef statusString = SecCopyErrorMessageString(osStatus, NULL);\r
+ logger->d("Error enumerating certificates: %s",\r
+ CFStringGetCStringPtr(statusString, kCFStringEncodingASCII));\r
+ CFRelease(statusString);\r
+ }\r
+ if (result != NULL) {\r
+ CFRelease(result);\r
+ }\r
+ return;\r
+ }\r
+ CFDataRef rawData = NULL;\r
+ for (CFIndex i = 0; i < CFArrayGetCount(result); i++) {\r
+ SecCertificateRef cert = (SecCertificateRef) CFArrayGetValueAtIndex(result, i);\r
+ rawData = SecCertificateCopyData(cert);\r
+ if (!rawData) {\r
+ if (logger)\r
+ logger->e("couldn't copy raw certificate data");\r
+ break;\r
+ }\r
+ const uint8_t* rawDataPtr = CFDataGetBytePtr(rawData);\r
+ X509Ptr x509cert(d2i_X509(nullptr, &rawDataPtr, CFDataGetLength(rawData)), &X509_free);\r
+ if (x509cert)\r
+ pems_.emplace_back(std::move(x509cert));\r
+ CFRelease(rawData);\r
+ rawData = NULL;\r
+ }\r
+ if (result != NULL) {\r
+ CFRelease(result);\r
+ }\r
+ if (rawData != NULL) {\r
+ CFRelease(rawData);\r
+ }\r
+#endif /*TARGET_OS_OSX, _WIN32*/\r
+}\r
+\r
+void\r
+PEMCache::fillX509Store(SSL_CTX* ctx)\r
+{\r
+ if (logger)\r
+ logger->d("adding %d decoded certs to X509 store", pems_.size());\r
+ if (X509_STORE* store = SSL_CTX_get_cert_store(ctx)) {\r
+ for (const auto& pem : pems_) {\r
+ if (X509_STORE_add_cert(store, pem.get()) != 1)\r
+ if (logger)\r
+ logger->w("couldn't add local certificate");\r
+ }\r
+ } else if (logger)\r
+ logger->e("couldn't get the context cert store");\r
+}\r
+\r
+} /*namespace http*/\r
+} /*namespace dht*/\r
--- /dev/null
+/*\r
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.\r
+ * Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>\r
+ *\r
+ * This program is free software; you can redistribute it and/or modify\r
+ * it under the terms of the GNU General Public License as published by\r
+ * the Free Software Foundation; either version 3 of the License, or\r
+ * (at your option) any later version.\r
+ *\r
+ * This program is distributed in the hope that it will be useful,\r
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of\r
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\r
+ * GNU General Public License for more details.\r
+ *\r
+ * You should have received a copy of the GNU General Public License\r
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.\r
+ */\r
+\r
+#pragma once\r
+\r
+#include "logger.h"\r
+\r
+#include <memory>\r
+\r
+#include <openssl/x509.h>\r
+\r
+#if (defined(LIBRESSL_VERSION_NUMBER) && (LIBRESSL_VERSION_NUMBER > 0x20501000L))\r
+#define EMBEDDED_ASN1_TIME_PARSE 0\r
+#else\r
+#define EMBEDDED_ASN1_TIME_PARSE 1\r
+#endif\r
+\r
+#if EMBEDDED_ASN1_TIME_PARSE\r
+#define V_ASN1_UTCTIME 23\r
+#define V_ASN1_GENERALIZEDTIME 24\r
+\r
+extern "C" {\r
+/*\r
+ * Parse an RFC 5280 format ASN.1 time string.\r
+ *\r
+ * mode must be:\r
+ * 0 if we expect to parse a time as specified in RFC 5280 for an X509 object.\r
+ * V_ASN1_UTCTIME if we wish to parse an RFC5280 format UTC time.\r
+ * V_ASN1_GENERALIZEDTIME if we wish to parse an RFC5280 format Generalized time.\r
+ *\r
+ * Returns:\r
+ * -1 if the string was invalid.\r
+ * V_ASN1_UTCTIME if the string validated as a UTC time string.\r
+ * V_ASN1_GENERALIZEDTIME if the string validated as a Generalized time string.\r
+ *\r
+ * Fills in *tm with the corresponding time if tm is non NULL.\r
+ */\r
+int ASN1_time_parse(const char* bytes, size_t len, struct tm* tm, int mode);\r
+}\r
+#endif\r
+\r
+namespace dht {\r
+namespace http {\r
+\r
+// A singleton class used to cache the decoded certificates\r
+// loaded from local cert stores that need to be added to the\r
+// ssl context prior to each request.\r
+class PEMCache\r
+{\r
+ PEMCache(const std::shared_ptr<Logger>& l);\r
+\r
+ using X509Ptr = std::unique_ptr<X509, void(*)(X509*)>;\r
+ std::vector<X509Ptr> pems_;\r
+ std::shared_ptr<Logger> logger;\r
+\r
+public:\r
+ static PEMCache& instance(const std::shared_ptr<Logger>& l)\r
+ {\r
+ static PEMCache instance_(l);\r
+ return instance_;\r
+ }\r
+\r
+ void fillX509Store(SSL_CTX* ctx);\r
+};\r
+}\r
+} // namespace dht\r
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ * Author : Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ * Vsevolod Ivanov <vsevolod.ivanov@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "crypto.h"
+#include "rng.h"
+
+extern "C" {
+#include <gnutls/gnutls.h>
+#include <gnutls/abstract.h>
+#include <gnutls/x509.h>
+#include <nettle/gcm.h>
+#include <nettle/aes.h>
+#include <gnutls/crypto.h>
+
+#include <argon2.h>
+}
+
+#include <random>
+#include <sstream>
+#include <fstream>
+#include <stdexcept>
+#include <cassert>
+
+#ifdef _WIN32
+static std::uniform_int_distribution<int> rand_byte{ 0, std::numeric_limits<uint8_t>::max() };
+#else
+static std::uniform_int_distribution<uint8_t> rand_byte;
+#endif
+
+#define DHT_AES_LEGACY_DECRYPT 1
+
+namespace dht {
+namespace crypto {
+
+static constexpr std::array<size_t, 3> AES_LENGTHS {{128/8, 192/8, 256/8}};
+static constexpr size_t PASSWORD_SALT_LENGTH {16};
+
+constexpr gnutls_digest_algorithm_t gnutlsHashAlgo(size_t min_res) {
+ return (min_res > 256/8) ? GNUTLS_DIG_SHA512 : (
+ (min_res > 160/8) ? GNUTLS_DIG_SHA256 : (
+ GNUTLS_DIG_SHA1));
+}
+
+constexpr size_t gnutlsHashSize(gnutls_digest_algorithm_t algo) {
+ return (algo == GNUTLS_DIG_SHA512) ? 512/8 : (
+ (algo == GNUTLS_DIG_SHA256) ? 256/8 : (
+ (algo == GNUTLS_DIG_SHA1) ? 160/8 : 0 ));
+}
+
+size_t aesKeySize(size_t max)
+{
+ size_t aes_key_len = 0;
+ for (size_t s : AES_LENGTHS) {
+ if (s <= max) aes_key_len = s;
+ else break;
+ }
+ return aes_key_len;
+}
+
+bool aesKeySizeGood(size_t key_size)
+{
+ for (auto& i : AES_LENGTHS)
+ if (key_size == i)
+ return true;
+ return false;
+}
+
+#ifndef GCM_DIGEST_SIZE
+#define GCM_DIGEST_SIZE GCM_BLOCK_SIZE
+#endif
+
+Blob aesEncrypt(const uint8_t* data, size_t data_length, const Blob& key)
+{
+ if (not aesKeySizeGood(key.size()))
+ throw DecryptError("Wrong key size");
+
+ Blob ret(data_length + GCM_IV_SIZE + GCM_DIGEST_SIZE);
+ {
+ crypto::random_device rdev;
+ std::generate_n(ret.begin(), GCM_IV_SIZE, std::bind(rand_byte, std::ref(rdev)));
+ }
+ struct gcm_aes_ctx aes;
+ gcm_aes_set_key(&aes, key.size(), key.data());
+ gcm_aes_set_iv(&aes, GCM_IV_SIZE, ret.data());
+ gcm_aes_encrypt(&aes, data_length, ret.data() + GCM_IV_SIZE, data);
+ gcm_aes_digest(&aes, GCM_DIGEST_SIZE, ret.data() + GCM_IV_SIZE + data_length);
+ return ret;
+}
+
+Blob aesEncrypt(const Blob& data, const std::string& password)
+{
+ Blob salt;
+ Blob key = stretchKey(password, salt, 256 / 8);
+ Blob encrypted = aesEncrypt(data, key);
+ encrypted.insert(encrypted.begin(), salt.begin(), salt.end());
+ return encrypted;
+}
+
+Blob aesDecrypt(const uint8_t* data, size_t data_length, const Blob& key)
+{
+ if (not aesKeySizeGood(key.size()))
+ throw DecryptError("Wrong key size");
+
+ if (data_length <= GCM_IV_SIZE + GCM_DIGEST_SIZE)
+ throw DecryptError("Wrong data size");
+
+ std::array<uint8_t, GCM_DIGEST_SIZE> digest;
+
+ struct gcm_aes_ctx aes;
+ gcm_aes_set_key(&aes, key.size(), key.data());
+ gcm_aes_set_iv(&aes, GCM_IV_SIZE, data);
+
+ size_t data_sz = data_length - GCM_IV_SIZE - GCM_DIGEST_SIZE;
+ Blob ret(data_sz);
+ gcm_aes_decrypt(&aes, data_sz, ret.data(), data + GCM_IV_SIZE);
+ gcm_aes_digest(&aes, GCM_DIGEST_SIZE, digest.data());
+
+ if (not std::equal(digest.begin(), digest.end(), data + data_length - GCM_DIGEST_SIZE)) {
+#if DHT_AES_LEGACY_DECRYPT
+ //gcm_aes_decrypt(&aes, data_sz, ret.data(), data + GCM_IV_SIZE);
+ Blob ret_tmp(data_sz);
+ struct gcm_aes_ctx aes_d;
+ gcm_aes_set_key(&aes_d, key.size(), key.data());
+ gcm_aes_set_iv(&aes_d, GCM_IV_SIZE, data);
+ gcm_aes_update(&aes_d, ret.size(), ret.data());
+ gcm_aes_encrypt(&aes_d, ret.size(), ret_tmp.data(), ret.data());
+ gcm_aes_digest(&aes_d, GCM_DIGEST_SIZE, digest.data());
+
+ if (not std::equal(digest.begin(), digest.end(), data + data_length - GCM_DIGEST_SIZE))
+ throw DecryptError("Can't decrypt data");
+#else
+ throw DecryptError("Can't decrypt data");
+#endif
+ }
+
+ return ret;
+}
+
+Blob aesDecrypt(const uint8_t* data, size_t data_len, const std::string& password)
+{
+ if (data_len <= PASSWORD_SALT_LENGTH)
+ throw DecryptError("Wrong data size");
+ Blob salt {data, data+PASSWORD_SALT_LENGTH};
+ Blob key = stretchKey(password, salt, 256/8);
+ return aesDecrypt(data+PASSWORD_SALT_LENGTH, data_len - PASSWORD_SALT_LENGTH, key);
+}
+
+Blob stretchKey(const std::string& password, Blob& salt, size_t key_length)
+{
+ if (salt.empty()) {
+ salt.resize(PASSWORD_SALT_LENGTH);
+ crypto::random_device rdev;
+ std::generate_n(salt.begin(), salt.size(), std::bind(rand_byte, std::ref(rdev)));
+ }
+ Blob res;
+ res.resize(32);
+ auto ret = argon2i_hash_raw(16, 64*1024, 1, password.data(), password.size(), salt.data(), salt.size(), res.data(), res.size());
+ if (ret != ARGON2_OK)
+ throw CryptoException("Can't compute argon2i !");
+ return hash(res, key_length);
+}
+
+Blob hash(const Blob& data, size_t hash_len)
+{
+ auto algo = gnutlsHashAlgo(hash_len);
+ size_t res_size = gnutlsHashSize(algo);
+ Blob res;
+ res.resize(res_size);
+ const gnutls_datum_t gdat {(uint8_t*)data.data(), (unsigned)data.size()};
+ if (auto err = gnutls_fingerprint(algo, &gdat, res.data(), &res_size))
+ throw CryptoException(std::string("Can't compute hash: ") + gnutls_strerror(err));
+ res.resize(std::min(hash_len, res_size));
+ return res;
+}
+
+void hash(const uint8_t* data, size_t data_length, uint8_t* hash, size_t hash_length)
+{
+ auto algo = gnutlsHashAlgo(hash_length);
+ size_t res_size = hash_length;
+ const gnutls_datum_t gdat {(uint8_t*)data, (unsigned)data_length};
+ if (auto err = gnutls_fingerprint(algo, &gdat, hash, &res_size))
+ throw CryptoException(std::string("Can't compute hash: ") + gnutls_strerror(err));
+}
+
+PrivateKey::PrivateKey()
+{}
+
+PrivateKey::PrivateKey(gnutls_x509_privkey_t k) : x509_key(k)
+{
+ gnutls_privkey_init(&key);
+ if (gnutls_privkey_import_x509(key, k, GNUTLS_PRIVKEY_IMPORT_COPY) != GNUTLS_E_SUCCESS) {
+ key = nullptr;
+ throw CryptoException("Can't load generic private key !");
+ }
+}
+
+PrivateKey::PrivateKey(const uint8_t* src, size_t src_size, const char* password_ptr)
+{
+ int err = gnutls_x509_privkey_init(&x509_key);
+ if (err != GNUTLS_E_SUCCESS)
+ throw CryptoException("Can't initialize private key !");
+
+ const gnutls_datum_t dt {(uint8_t*)src, static_cast<unsigned>(src_size)};
+ int flags = password_ptr ? GNUTLS_PKCS_PLAIN
+ : ( GNUTLS_PKCS_PBES2_AES_128 | GNUTLS_PKCS_PBES2_AES_192 | GNUTLS_PKCS_PBES2_AES_256
+ | GNUTLS_PKCS_PKCS12_3DES | GNUTLS_PKCS_PKCS12_ARCFOUR | GNUTLS_PKCS_PKCS12_RC2_40);
+
+ err = gnutls_x509_privkey_import2(x509_key, &dt, GNUTLS_X509_FMT_PEM, password_ptr, flags);
+ if (err != GNUTLS_E_SUCCESS) {
+ int err_der = gnutls_x509_privkey_import2(x509_key, &dt, GNUTLS_X509_FMT_DER, password_ptr, flags);
+ if (err_der != GNUTLS_E_SUCCESS) {
+ gnutls_x509_privkey_deinit(x509_key);
+ if (err == GNUTLS_E_DECRYPTION_FAILED or err_der == GNUTLS_E_DECRYPTION_FAILED)
+ throw DecryptError("Can't decrypt private key");
+ else
+ throw CryptoException(std::string("Can't load private key: PEM: ") + gnutls_strerror(err)
+ + " DER: " + gnutls_strerror(err_der));
+ }
+ }
+
+ gnutls_privkey_init(&key);
+ if (gnutls_privkey_import_x509(key, x509_key, GNUTLS_PRIVKEY_IMPORT_COPY) != GNUTLS_E_SUCCESS) {
+ throw CryptoException("Can't load generic private key !");
+ }
+}
+
+PrivateKey::PrivateKey(PrivateKey&& o) noexcept : key(o.key), x509_key(o.x509_key)
+{
+ o.key = nullptr;
+ o.x509_key = nullptr;
+}
+
+PrivateKey::~PrivateKey()
+{
+ if (key) {
+ gnutls_privkey_deinit(key);
+ key = nullptr;
+ }
+ if (x509_key) {
+ gnutls_x509_privkey_deinit(x509_key);
+ x509_key = nullptr;
+ }
+}
+
+PrivateKey&
+PrivateKey::operator=(PrivateKey&& o) noexcept
+{
+ if (key) {
+ gnutls_privkey_deinit(key);
+ key = nullptr;
+ }
+ if (x509_key) {
+ gnutls_x509_privkey_deinit(x509_key);
+ x509_key = nullptr;
+ }
+ key = o.key; x509_key = o.x509_key;
+ o.key = nullptr; o.x509_key = nullptr;
+ return *this;
+}
+
+Blob
+PrivateKey::sign(const uint8_t* data, size_t data_length) const
+{
+ if (!key)
+ throw CryptoException("Can't sign data: no private key set !");
+ if (std::numeric_limits<unsigned>::max() < data_length)
+ throw CryptoException("Can't sign data: too large !");
+ gnutls_datum_t sig;
+ const gnutls_datum_t dat {(unsigned char*)data, (unsigned)data_length};
+ if (gnutls_privkey_sign_data(key, GNUTLS_DIG_SHA512, 0, &dat, &sig) != GNUTLS_E_SUCCESS)
+ throw CryptoException("Can't sign data !");
+ Blob ret(sig.data, sig.data+sig.size);
+ gnutls_free(sig.data);
+ return ret;
+}
+
+Blob
+PrivateKey::decryptBloc(const uint8_t* src, size_t src_size) const
+{
+ const gnutls_datum_t dat {(uint8_t*)src, (unsigned)src_size};
+ gnutls_datum_t out;
+ int err = gnutls_privkey_decrypt_data(key, 0, &dat, &out);
+ if (err != GNUTLS_E_SUCCESS)
+ throw DecryptError(std::string("Can't decrypt data: ") + gnutls_strerror(err));
+ Blob ret {out.data, out.data+out.size};
+ gnutls_free(out.data);
+ return ret;
+}
+
+Blob
+PrivateKey::decrypt(const uint8_t* cypher, size_t cypher_len) const
+{
+ if (!key)
+ throw CryptoException("Can't decrypt data without private key !");
+
+ unsigned key_len = 0;
+ int err = gnutls_privkey_get_pk_algorithm(key, &key_len);
+ if (err < 0)
+ throw CryptoException("Can't read public key length !");
+ if (err != GNUTLS_PK_RSA)
+ throw CryptoException("Must be an RSA key");
+
+ unsigned cypher_block_sz = key_len / 8;
+ if (cypher_len < cypher_block_sz)
+ throw DecryptError("Unexpected cipher length");
+ else if (cypher_len == cypher_block_sz)
+ return decryptBloc(cypher, cypher_block_sz);
+
+ return aesDecrypt(cypher + cypher_block_sz, cypher_len - cypher_block_sz, decryptBloc(cypher, cypher_block_sz));
+}
+
+Blob
+PrivateKey::serialize(const std::string& password) const
+{
+ if (!x509_key)
+ return {};
+ size_t buf_sz = 8192;
+ Blob buffer;
+ buffer.resize(buf_sz);
+ auto err = serialize(buffer.data(), &buf_sz, password);
+ if (err != GNUTLS_E_SUCCESS) {
+ std::cerr << "Could not export private key - " << gnutls_strerror(err) << std::endl;
+ return {};
+ }
+ buffer.resize(buf_sz);
+ return buffer;
+}
+
+int
+PrivateKey::serialize(uint8_t* out, size_t* out_len, const std::string& password) const
+{
+ if (!x509_key)
+ return -1;
+ return password.empty()
+ ? gnutls_x509_privkey_export_pkcs8(x509_key, GNUTLS_X509_FMT_PEM, nullptr, GNUTLS_PKCS_PLAIN, out, out_len)
+ : gnutls_x509_privkey_export_pkcs8(x509_key, GNUTLS_X509_FMT_PEM, password.c_str(), GNUTLS_PKCS_PBES2_AES_256, out, out_len);
+}
+
+const PublicKey&
+PrivateKey::getPublicKey() const
+{
+ return *getSharedPublicKey();
+}
+
+const std::shared_ptr<PublicKey>&
+PrivateKey::getSharedPublicKey() const
+{
+ std::lock_guard<std::mutex> lock(publicKeyMutex_);
+ if (not publicKey_) {
+ auto pk = std::make_shared<PublicKey>();
+ if (auto err = gnutls_pubkey_import_privkey(pk->pk, key, GNUTLS_KEY_KEY_CERT_SIGN | GNUTLS_KEY_CRL_SIGN, 0))
+ throw CryptoException(std::string("Can't retreive public key: ") + gnutls_strerror(err));
+ publicKey_ = std::move(pk);
+ }
+ return publicKey_;
+}
+
+PublicKey::PublicKey()
+{
+ if (auto err = gnutls_pubkey_init(&pk))
+ throw CryptoException(std::string("Can't initialize public key: ") + gnutls_strerror(err));
+}
+
+PublicKey::PublicKey(const uint8_t* dat, size_t dat_size) : PublicKey()
+{
+ unpack(dat, dat_size);
+}
+
+PublicKey::~PublicKey()
+{
+ if (pk) {
+ gnutls_pubkey_deinit(pk);
+ pk = nullptr;
+ }
+}
+
+PublicKey&
+PublicKey::operator=(PublicKey&& o) noexcept
+{
+ if (pk)
+ gnutls_pubkey_deinit(pk);
+ pk = o.pk;
+ o.pk = nullptr;
+ return *this;
+}
+
+void
+PublicKey::pack(Blob& b) const
+{
+ if (not pk)
+ throw CryptoException(std::string("Could not export public key: null key"));
+ std::vector<uint8_t> tmp(2048);
+ size_t sz = tmp.size();
+ if (int err = pack(tmp.data(), &sz))
+ throw CryptoException(std::string("Could not export public key: ") + gnutls_strerror(err));
+ tmp.resize(sz);
+ b.insert(b.end(), tmp.begin(), tmp.end());
+}
+
+int
+PublicKey::pack(uint8_t* out, size_t* out_len) const
+{
+ return gnutls_pubkey_export(pk, GNUTLS_X509_FMT_DER, out, out_len);
+}
+
+void
+PublicKey::unpack(const uint8_t* data, size_t data_size)
+{
+ const gnutls_datum_t dat {(uint8_t*)data, (unsigned)data_size};
+ int err = gnutls_pubkey_import(pk, &dat, GNUTLS_X509_FMT_PEM);
+ if (err != GNUTLS_E_SUCCESS)
+ err = gnutls_pubkey_import(pk, &dat, GNUTLS_X509_FMT_DER);
+ if (err != GNUTLS_E_SUCCESS)
+ throw CryptoException(std::string("Could not read public key: ") + gnutls_strerror(err));
+}
+
+std::string
+PublicKey::toString() const
+{
+ if (not pk)
+ throw CryptoException(std::string("Could not print public key: null key"));
+ std::string ret;
+ size_t sz = ret.size();
+ int err = gnutls_pubkey_export(pk, GNUTLS_X509_FMT_PEM, (void*)ret.data(), &sz);
+ if (err == GNUTLS_E_SHORT_MEMORY_BUFFER) {
+ ret.resize(sz);
+ err = gnutls_pubkey_export(pk, GNUTLS_X509_FMT_PEM, (void*)ret.data(), &sz);
+ }
+ if (err != GNUTLS_E_SUCCESS)
+ throw CryptoException(std::string("Could not print public key: ") + gnutls_strerror(err));
+ return ret;
+}
+
+void
+PublicKey::msgpack_unpack(const msgpack::object& o)
+{
+ if (o.type == msgpack::type::BIN)
+ unpack((const uint8_t*)o.via.bin.ptr, o.via.bin.size);
+ else {
+ Blob dat = unpackBlob(o);
+ unpack(dat.data(), dat.size());
+ }
+}
+
+bool PublicKey::checkSignature(const uint8_t* data, size_t data_len, const uint8_t* signature, size_t signature_len) const
+{
+ if (!pk)
+ return false;
+ const gnutls_datum_t sig {(uint8_t*)signature, (unsigned)signature_len};
+ const gnutls_datum_t dat {(uint8_t*)data, (unsigned)data_len};
+ int rc = gnutls_pubkey_verify_data2(pk, GNUTLS_SIGN_RSA_SHA512, 0, &dat, &sig);
+ return rc >= 0;
+}
+
+void
+PublicKey::encryptBloc(const uint8_t* src, size_t src_size, uint8_t* dst, size_t dst_size) const
+{
+ const gnutls_datum_t key_dat {(uint8_t*)src, (unsigned)src_size};
+ gnutls_datum_t encrypted;
+ auto err = gnutls_pubkey_encrypt_data(pk, 0, &key_dat, &encrypted);
+ if (err != GNUTLS_E_SUCCESS)
+ throw CryptoException(std::string("Can't encrypt data: ") + gnutls_strerror(err));
+ if (encrypted.size != dst_size)
+ throw CryptoException("Unexpected cypherblock size");
+ std::copy_n(encrypted.data, encrypted.size, dst);
+ gnutls_free(encrypted.data);
+}
+
+Blob
+PublicKey::encrypt(const uint8_t* data, size_t data_len) const
+{
+ if (!pk)
+ throw CryptoException("Can't read public key !");
+
+ unsigned key_len = 0;
+ int err = gnutls_pubkey_get_pk_algorithm(pk, &key_len);
+ if (err < 0)
+ throw CryptoException("Can't read public key length !");
+ if (err != GNUTLS_PK_RSA)
+ throw CryptoException("Must be an RSA key");
+
+ const unsigned max_block_sz = key_len / 8 - 11;
+ const unsigned cypher_block_sz = key_len / 8;
+
+ /* Use plain RSA if the data is small enough */
+ if (data_len <= max_block_sz) {
+ Blob ret(cypher_block_sz);
+ encryptBloc(data, data_len, ret.data(), cypher_block_sz);
+ return ret;
+ }
+
+ /* Otherwise use RSA+AES-GCM,
+ using the max. AES key size that can fit
+ in a single RSA packet () */
+ unsigned aes_key_sz = aesKeySize(max_block_sz);
+ if (aes_key_sz == 0)
+ throw CryptoException("Key is not long enough for AES128");
+ Blob key(aes_key_sz);
+ {
+ crypto::random_device rdev;
+ std::generate_n(key.begin(), key.size(), std::bind(rand_byte, std::ref(rdev)));
+ }
+ auto data_encrypted = aesEncrypt(data, data_len, key);
+
+ Blob ret;
+ ret.reserve(cypher_block_sz + data_encrypted.size());
+
+ ret.resize(cypher_block_sz);
+ encryptBloc(key.data(), key.size(), ret.data(), cypher_block_sz);
+ ret.insert(ret.end(), data_encrypted.begin(), data_encrypted.end());
+ return ret;
+}
+
+const InfoHash&
+PublicKey::getId() const
+{
+ if (pk and not idCached_.load()) {
+ InfoHash id;
+ size_t sz = id.size();
+ if (auto err = gnutls_pubkey_get_key_id(pk, 0, id.data(), &sz))
+ throw CryptoException(std::string("Can't get public key ID: ") + gnutls_strerror(err));
+ if (sz != id.size())
+ throw CryptoException("Can't get public key ID: wrong output length.");
+ cachedId_ = id;
+ idCached_.store(true);
+ }
+ return cachedId_;
+}
+
+const PkId&
+PublicKey::getLongId() const
+{
+ if (pk and not longIdCached_.load()) {
+ PkId h;
+ size_t sz = h.size();
+ if (auto err = gnutls_pubkey_get_key_id(pk, GNUTLS_KEYID_USE_SHA256, h.data(), &sz))
+ throw CryptoException(std::string("Can't get 256 bits public key ID: ") + gnutls_strerror(err));
+ if (sz != h.size())
+ throw CryptoException("Can't get 256 bits public key ID: wrong output length.");
+ cachedLongId_ = h;
+ longIdCached_.store(true);
+ }
+ return cachedLongId_;
+}
+
+gnutls_digest_algorithm_t
+PublicKey::getPreferredDigest() const
+{
+ gnutls_digest_algorithm_t dig;
+ int result = gnutls_pubkey_get_preferred_hash_algorithm(pk, &dig, nullptr);
+ if (result < 0)
+ return GNUTLS_DIG_UNKNOWN;
+ return dig;
+}
+
+// Certificate Request
+
+static NameType
+typeFromGnuTLS(gnutls_x509_subject_alt_name_t type)
+{
+ switch(type) {
+ case GNUTLS_SAN_DNSNAME:
+ return NameType::DNS;
+ case GNUTLS_SAN_RFC822NAME:
+ return NameType::RFC822;
+ case GNUTLS_SAN_URI:
+ return NameType::URI;
+ case GNUTLS_SAN_IPADDRESS:
+ return NameType::IP;
+ default:
+ return NameType::UNKNOWN;
+ }
+}
+
+static gnutls_x509_subject_alt_name_t
+GnuTLSFromType(NameType type)
+{
+ switch(type) {
+ case NameType::DNS:
+ return GNUTLS_SAN_DNSNAME;
+ case NameType::RFC822:
+ return GNUTLS_SAN_RFC822NAME;
+ case NameType::URI:
+ return GNUTLS_SAN_URI;
+ case NameType::IP:
+ return GNUTLS_SAN_IPADDRESS;
+ default:
+ return (gnutls_x509_subject_alt_name_t)0;
+ }
+}
+
+static std::string
+getDN(gnutls_x509_crq_t request, const char* oid)
+{
+ std::string dn;
+ dn.resize(512);
+ size_t dn_sz = dn.size();
+ int ret = gnutls_x509_crq_get_dn_by_oid(request, oid, 0, 0, &(*dn.begin()), &dn_sz);
+ if (ret != GNUTLS_E_SUCCESS)
+ return {};
+ dn.resize(dn_sz);
+ return dn;
+}
+
+CertificateRequest::CertificateRequest()
+{
+ if (auto err = gnutls_x509_crq_init(&request))
+ throw CryptoException(std::string("Can't initialize certificate request: ") + gnutls_strerror(err));
+}
+
+CertificateRequest::CertificateRequest(const uint8_t* data, size_t size) : CertificateRequest()
+{
+ const gnutls_datum_t dat {(uint8_t*)data, (unsigned)size};
+ if (auto err = gnutls_x509_crq_import(request, &dat, GNUTLS_X509_FMT_PEM))
+ throw CryptoException(std::string("Can't import certificate request: ") + gnutls_strerror(err));
+}
+
+CertificateRequest::~CertificateRequest()
+{
+ if (request) {
+ gnutls_x509_crq_deinit(request);
+ request = nullptr;
+ }
+}
+
+CertificateRequest&
+CertificateRequest::operator=(CertificateRequest&& o) noexcept
+{
+ if (request)
+ gnutls_x509_crq_deinit(request);
+ request = o.request;
+ o.request = nullptr;
+ return *this;
+}
+
+void
+CertificateRequest::setAltName(NameType type, const std::string& name)
+{
+ gnutls_x509_crq_set_subject_alt_name(request, GnuTLSFromType(type), name.data(), name.size(), 0);
+}
+
+void
+CertificateRequest::setName(const std::string& name)
+{
+ gnutls_x509_crq_set_dn_by_oid(request, GNUTLS_OID_X520_COMMON_NAME, 0, name.data(), name.length());
+}
+
+std::string
+CertificateRequest::getName() const
+{
+ return getDN(request, GNUTLS_OID_X520_COMMON_NAME);
+}
+
+std::string
+CertificateRequest::getUID() const
+{
+ return getDN(request, GNUTLS_OID_LDAP_UID);
+}
+
+void
+CertificateRequest::setUID(const std::string& uid)
+{
+ gnutls_x509_crq_set_dn_by_oid(request, GNUTLS_OID_LDAP_UID, 0, uid.data(), uid.length());
+}
+
+void
+CertificateRequest::sign(const PrivateKey& key, const std::string& password)
+{
+ gnutls_x509_crq_set_version(request, 1);
+ if (not password.empty())
+ gnutls_x509_crq_set_challenge_password(request, password.c_str());
+
+ if (auto err = gnutls_x509_crq_set_key(request, key.x509_key))
+ throw CryptoException(std::string("Can't set certificate request key: ") + gnutls_strerror(err));
+
+#if GNUTLS_VERSION_NUMBER < 0x030601
+ if (auto err = gnutls_x509_crq_privkey_sign(request, key.key, key.getPublicKey().getPreferredDigest(), 0))
+ throw CryptoException(std::string("Can't sign certificate request: ") + gnutls_strerror(err));
+#else
+ if (auto err = gnutls_x509_crq_privkey_sign(request, key.key, GNUTLS_DIG_UNKNOWN, 0))
+ throw CryptoException(std::string("Can't sign certificate request: ") + gnutls_strerror(err));
+#endif
+}
+
+bool
+CertificateRequest::verify() const
+{
+ return gnutls_x509_crq_verify(request, 0) >= 0;
+}
+
+Blob
+CertificateRequest::pack() const
+{
+ gnutls_datum_t dat {nullptr, 0};
+ if (auto err = gnutls_x509_crq_export2(request, GNUTLS_X509_FMT_PEM, &dat))
+ throw CryptoException(std::string("Can't export certificate request: ") + gnutls_strerror(err));
+ Blob ret(dat.data, dat.data + dat.size);
+ gnutls_free(dat.data);
+ return ret;
+}
+
+std::string
+CertificateRequest::toString() const
+{
+ gnutls_datum_t dat {nullptr, 0};
+ if (auto err = gnutls_x509_crq_export2(request, GNUTLS_X509_FMT_PEM, &dat))
+ throw CryptoException(std::string("Can't export certificate request: ") + gnutls_strerror(err));
+ std::string ret(dat.data, dat.data + dat.size);
+ gnutls_free(dat.data);
+ return ret;
+}
+
+// Certificate
+
+static std::string
+getDN(gnutls_x509_crt_t cert, const char* oid, bool issuer = false)
+{
+ std::string dn;
+ dn.resize(512);
+ size_t dn_sz = dn.size();
+ int ret = issuer
+ ? gnutls_x509_crt_get_issuer_dn_by_oid(cert, oid, 0, 0, &(*dn.begin()), &dn_sz)
+ : gnutls_x509_crt_get_dn_by_oid( cert, oid, 0, 0, &(*dn.begin()), &dn_sz);
+ if (ret != GNUTLS_E_SUCCESS)
+ return {};
+ dn.resize(dn_sz);
+ return dn;
+}
+
+Certificate::Certificate(const Blob& certData)
+{
+ unpack(certData.data(), certData.size());
+}
+
+Certificate&
+Certificate::operator=(Certificate&& o) noexcept
+{
+ if (cert)
+ gnutls_x509_crt_deinit(cert);
+ cert = o.cert;
+ o.cert = nullptr;
+ issuer = std::move(o.issuer);
+ return *this;
+}
+
+void
+Certificate::unpack(const uint8_t* dat, size_t dat_size)
+{
+ if (cert) {
+ gnutls_x509_crt_deinit(cert);
+ cert = nullptr;
+ }
+ gnutls_x509_crt_t* cert_list;
+ unsigned cert_num;
+ const gnutls_datum_t crt_dt {(uint8_t*)dat, (unsigned)dat_size};
+ int err = gnutls_x509_crt_list_import2(&cert_list, &cert_num, &crt_dt, GNUTLS_X509_FMT_PEM, GNUTLS_X509_CRT_LIST_FAIL_IF_UNSORTED);
+ if (err != GNUTLS_E_SUCCESS)
+ err = gnutls_x509_crt_list_import2(&cert_list, &cert_num, &crt_dt, GNUTLS_X509_FMT_DER, GNUTLS_X509_CRT_LIST_FAIL_IF_UNSORTED);
+ if (err != GNUTLS_E_SUCCESS || cert_num == 0) {
+ cert = nullptr;
+ throw CryptoException(std::string("Could not read certificate - ") + gnutls_strerror(err));
+ }
+
+ cert = cert_list[0];
+ Certificate* crt = this;
+ size_t i = 1;
+ while (crt and i < cert_num) {
+ crt->issuer = std::make_shared<Certificate>(cert_list[i++]);
+ crt = crt->issuer.get();
+ }
+ gnutls_free(cert_list);
+}
+
+void
+Certificate::msgpack_unpack(const msgpack::object& o)
+{
+ if (o.type == msgpack::type::BIN)
+ unpack((const uint8_t*)o.via.bin.ptr, o.via.bin.size);
+ else {
+ Blob dat = unpackBlob(o);
+ unpack(dat.data(), dat.size());
+ }
+}
+
+void
+Certificate::pack(Blob& b) const
+{
+ const Certificate* crt = this;
+ while (crt) {
+ std::string str;
+ size_t buf_sz = 8192;
+ str.resize(buf_sz);
+ if (int err = gnutls_x509_crt_export(crt->cert, GNUTLS_X509_FMT_PEM, &(*str.begin()), &buf_sz)) {
+ std::cerr << "Could not export certificate - " << gnutls_strerror(err) << std::endl;
+ return;
+ }
+ str.resize(buf_sz);
+ b.insert(b.end(), str.begin(), str.end());
+ crt = crt->issuer.get();
+ }
+}
+
+Certificate::~Certificate()
+{
+ if (cert) {
+ gnutls_x509_crt_deinit(cert);
+ cert = nullptr;
+ }
+}
+
+const PublicKey&
+Certificate::getPublicKey() const
+{
+ return *getSharedPublicKey();
+}
+
+const std::shared_ptr<PublicKey>&
+Certificate::getSharedPublicKey() const
+{
+ std::lock_guard<std::mutex> lock(publicKeyMutex_);
+ if (not publicKey_) {
+ auto pk = std::make_shared<PublicKey>();
+ if (auto err = gnutls_pubkey_import_x509(pk->pk, cert, 0))
+ throw CryptoException(std::string("Can't get certificate public key: ") + gnutls_strerror(err));
+ publicKey_ = std::move(pk);
+ }
+ return publicKey_;
+}
+
+const InfoHash&
+Certificate::getId() const
+{
+ if (cert and not idCached_.load()) {
+ InfoHash id;
+ size_t sz = id.size();
+ if (auto err = gnutls_x509_crt_get_key_id(cert, 0, id.data(), &sz))
+ throw CryptoException(std::string("Can't get certificate public key ID: ") + gnutls_strerror(err));
+ if (sz != id.size())
+ throw CryptoException("Can't get certificate public key ID: wrong output length.");
+ cachedId_ = id;
+ idCached_.store(true);
+ }
+ return cachedId_;
+}
+
+const PkId&
+Certificate::getLongId() const
+{
+ if (cert and not longIdCached_.load()) {
+ PkId id;
+ size_t sz = id.size();
+ if (auto err = gnutls_x509_crt_get_key_id(cert, GNUTLS_KEYID_USE_SHA256, id.data(), &sz))
+ throw CryptoException(std::string("Can't get certificate 256 bits public key ID: ") + gnutls_strerror(err));
+ if (sz != id.size())
+ throw CryptoException("Can't get certificate 256 bits public key ID: wrong output length.");
+ cachedLongId_ = id;
+ longIdCached_.store(true);
+ }
+ return cachedLongId_;
+}
+
+Blob
+Certificate::getSerialNumber() const
+{
+ if (not cert)
+ return {};
+ // extract from certificate
+ unsigned char serial[64];
+ auto size = sizeof(serial);
+ gnutls_x509_crt_get_serial(cert, &serial, &size);
+ return {serial, serial + size};
+}
+
+std::string
+Certificate::getName() const
+{
+ return getDN(cert, GNUTLS_OID_X520_COMMON_NAME);
+}
+
+std::string
+Certificate::getUID() const
+{
+ return getDN(cert, GNUTLS_OID_LDAP_UID);
+}
+
+std::string
+Certificate::getIssuerName() const
+{
+ return getDN(cert, GNUTLS_OID_X520_COMMON_NAME, true);
+}
+
+std::string
+Certificate::getIssuerUID() const
+{
+ return getDN(cert, GNUTLS_OID_LDAP_UID, true);
+}
+
+std::vector<std::pair<NameType, std::string>>
+Certificate::getAltNames() const
+{
+ std::vector<std::pair<NameType, std::string>> names;
+ unsigned i = 0;
+ std::string name;
+ while (true) {
+ name.resize(512);
+ size_t name_sz = name.size();
+ unsigned type;
+ int ret = gnutls_x509_crt_get_subject_alt_name2(cert, i++, &(*name.begin()), &name_sz, &type, nullptr);
+ if (ret == GNUTLS_E_REQUESTED_DATA_NOT_AVAILABLE)
+ break;
+ name.resize(name_sz);
+ names.emplace_back(typeFromGnuTLS((gnutls_x509_subject_alt_name_t)type), name);
+ }
+ return names;
+}
+
+bool
+Certificate::isCA() const
+{
+ unsigned critical;
+ bool ca_flag = gnutls_x509_crt_get_ca_status(cert, &critical) > 0;
+ if (ca_flag) {
+ unsigned usage;
+ auto ret = gnutls_x509_crt_get_key_usage(cert, &usage, &critical);
+ /* Conforming CAs MUST include this extension in certificates that
+ contain public keys that are used to validate digital signatures on
+ other public key certificates or CRLs. */
+ if (ret < 0)
+ return false;
+ if (not critical)
+ return true;
+ return usage & GNUTLS_KEY_KEY_CERT_SIGN;
+ }
+ return false;
+}
+
+std::string
+Certificate::toString(bool chain) const
+{
+ std::ostringstream ss;
+ const Certificate* crt = this;
+ while (crt) {
+ std::string str;
+ size_t buf_sz = 8192;
+ str.resize(buf_sz);
+ if (int err = gnutls_x509_crt_export(crt->cert, GNUTLS_X509_FMT_PEM, &(*str.begin()), &buf_sz)) {
+ std::cerr << "Could not export certificate - " << gnutls_strerror(err) << std::endl;
+ return {};
+ }
+ str.resize(buf_sz);
+ ss << str;
+ if (not chain)
+ break;
+ crt = crt->issuer.get();
+ }
+ return ss.str();
+}
+
+std::string
+Certificate::print() const
+{
+ gnutls_datum_t out;
+ gnutls_x509_crt_print(cert, GNUTLS_CRT_PRINT_FULL, &out);
+ std::string ret(out.data, out.data+out.size);
+ gnutls_free(out.data);
+ return ret;
+}
+
+void
+Certificate::revoke(const PrivateKey& key, const Certificate& to_revoke)
+{
+ if (revocation_lists.empty())
+ revocation_lists.emplace(std::make_shared<RevocationList>());
+ auto& list = *(*revocation_lists.begin());
+ list.revoke(to_revoke);
+ list.sign(key, *this);
+}
+
+void
+Certificate::addRevocationList(RevocationList&& list)
+{
+ addRevocationList(std::make_shared<RevocationList>(std::forward<RevocationList>(list)));
+}
+
+void
+Certificate::addRevocationList(std::shared_ptr<RevocationList> list)
+{
+ if (revocation_lists.find(list) != revocation_lists.end())
+ return; // Already in the list
+ if (not list->isSignedBy(*this))
+ throw CryptoException("CRL is not signed by this certificate");
+ revocation_lists.emplace(std::move(list));
+}
+
+std::chrono::system_clock::time_point
+Certificate::getActivation() const
+{
+ auto t = gnutls_x509_crt_get_activation_time(cert);
+ if (t == (time_t)-1)
+ return std::chrono::system_clock::time_point::min();
+ return std::chrono::system_clock::from_time_t(t);
+}
+
+std::chrono::system_clock::time_point
+Certificate::getExpiration() const
+{
+ auto t = gnutls_x509_crt_get_expiration_time(cert);
+ if (t == (time_t)-1)
+ return std::chrono::system_clock::time_point::min();
+ return std::chrono::system_clock::from_time_t(t);
+}
+
+gnutls_digest_algorithm_t
+Certificate::getPreferredDigest() const
+{
+ return getPublicKey().getPreferredDigest();
+}
+
+std::pair<std::string, Blob>
+Certificate::generateOcspRequest(gnutls_x509_crt_t& issuer)
+{
+ gnutls_ocsp_req_t rreq;
+ int err = gnutls_ocsp_req_init(&rreq);
+ if (err < 0)
+ throw CryptoException(gnutls_strerror(err));
+ std::unique_ptr<struct gnutls_ocsp_req_int, decltype(&gnutls_ocsp_req_deinit)> req(rreq, &gnutls_ocsp_req_deinit);
+ err = gnutls_ocsp_req_add_cert(req.get(), GNUTLS_DIG_SHA1, issuer, cert);
+ if (err < 0)
+ throw CryptoException(gnutls_strerror(err));
+ Blob noncebuf(32);
+ gnutls_datum_t nonce = { noncebuf.data(), (unsigned)noncebuf.size() };
+ err = gnutls_rnd(GNUTLS_RND_NONCE, nonce.data, nonce.size);
+ if (err < 0)
+ throw CryptoException(gnutls_strerror(err));
+ err = gnutls_ocsp_req_set_nonce(req.get(), 0, &nonce);
+ if (err < 0)
+ throw CryptoException(gnutls_strerror(err));
+ gnutls_datum_t rdata;
+ err = gnutls_ocsp_req_export(req.get(), &rdata);
+ if (err != 0)
+ throw CryptoException(gnutls_strerror(err));
+ std::string ret((char*)rdata.data, (char*)rdata.data + rdata.size);
+ gnutls_free(rdata.data);
+ return std::make_pair<std::string, Blob>(std::move(ret), std::move(noncebuf));
+}
+
+// PrivateKey
+
+PrivateKey
+PrivateKey::generate(unsigned key_length)
+{
+ gnutls_x509_privkey_t key;
+ if (gnutls_x509_privkey_init(&key) != GNUTLS_E_SUCCESS)
+ throw CryptoException("Can't initialize private key.");
+ int err = gnutls_x509_privkey_generate(key, GNUTLS_PK_RSA, key_length, 0);
+ if (err != GNUTLS_E_SUCCESS) {
+ gnutls_x509_privkey_deinit(key);
+ throw CryptoException(std::string("Can't generate RSA key pair: ") + gnutls_strerror(err));
+ }
+ return PrivateKey{key};
+}
+
+PrivateKey
+PrivateKey::generateEC()
+{
+ gnutls_x509_privkey_t key;
+ if (gnutls_x509_privkey_init(&key) != GNUTLS_E_SUCCESS)
+ throw CryptoException("Can't initialize private key.");
+ int err = gnutls_x509_privkey_generate(key, GNUTLS_PK_EC, gnutls_sec_param_to_pk_bits(GNUTLS_PK_EC, GNUTLS_SEC_PARAM_ULTRA), 0);
+ if (err != GNUTLS_E_SUCCESS) {
+ gnutls_x509_privkey_deinit(key);
+ throw CryptoException(std::string("Can't generate EC key pair: ") + gnutls_strerror(err));
+ }
+ return PrivateKey{key};
+}
+
+Identity
+generateIdentity(const std::string& name, const Identity& ca, unsigned key_length, bool is_ca)
+{
+ auto key = std::make_shared<PrivateKey>(PrivateKey::generate(key_length));
+ auto cert = std::make_shared<Certificate>(Certificate::generate(*key, name, ca, is_ca));
+ return {std::move(key), std::move(cert)};
+}
+
+
+Identity
+generateIdentity(const std::string& name, const Identity& ca, unsigned key_length) {
+ return generateIdentity(name, ca, key_length, !ca.first || !ca.second);
+}
+
+Identity
+generateEcIdentity(const std::string& name, const Identity& ca, bool is_ca)
+{
+ auto key = std::make_shared<PrivateKey>(PrivateKey::generateEC());
+ auto cert = std::make_shared<Certificate>(Certificate::generate(*key, name, ca, is_ca));
+ return {std::move(key), std::move(cert)};
+}
+
+Identity
+generateEcIdentity(const std::string& name, const Identity& ca) {
+ return generateEcIdentity(name, ca, !ca.first || !ca.second);
+}
+
+void
+saveIdentity(const Identity& id, const std::string& path, const std::string& privkey_password)
+{
+ {
+ auto ca_key_data = id.first->serialize(privkey_password);
+ std::ofstream key_file(path + ".pem");
+ key_file.write((char*)ca_key_data.data(), ca_key_data.size());
+ }
+ {
+ auto ca_key_data = id.second->getPacked();
+ std::ofstream crt_file(path + ".crt");
+ crt_file.write((char*)ca_key_data.data(), ca_key_data.size());
+ }
+}
+
+void
+setValidityPeriod(gnutls_x509_crt_t cert, int64_t validity)
+{
+ int64_t now = time(nullptr);
+ /* 2038 bug: don't allow time wrap */
+ auto boundTime = [](int64_t t) -> time_t {
+ return std::min<int64_t>(t, std::numeric_limits<time_t>::max());
+ };
+ gnutls_x509_crt_set_activation_time(cert, boundTime(now));
+ gnutls_x509_crt_set_expiration_time(cert, boundTime(now + validity));
+}
+
+void
+setRandomSerial(gnutls_x509_crt_t cert)
+{
+ random_device rdev;
+ std::uniform_int_distribution<int64_t> dist{1};
+ int64_t cert_serial = dist(rdev);
+ gnutls_x509_crt_set_serial(cert, &cert_serial, sizeof(cert_serial));
+}
+
+Certificate
+Certificate::generate(const PrivateKey& key, const std::string& name, const Identity& ca, bool is_ca, int64_t validity)
+{
+ gnutls_x509_crt_t cert;
+ if (not key.x509_key or gnutls_x509_crt_init(&cert) != GNUTLS_E_SUCCESS)
+ return {};
+ Certificate ret {cert};
+
+ setValidityPeriod(cert, validity <= 0 ? 10 * 365 * 24 * 60 * 60 : validity);
+ if (int err = gnutls_x509_crt_set_key(cert, key.x509_key)) {
+ throw CryptoException(std::string("Error when setting certificate key ") + gnutls_strerror(err));
+ }
+ if (int err = gnutls_x509_crt_set_version(cert, 3)) {
+ throw CryptoException(std::string("Error when setting certificate version ") + gnutls_strerror(err));
+ }
+
+ // TODO: compute the subject key using the recommended RFC method
+ const auto& pk = key.getPublicKey();
+ auto pk_id = pk.getId();
+ const std::string uid_str = pk_id.toString();
+
+ gnutls_x509_crt_set_subject_key_id(cert, &pk_id, sizeof(pk_id));
+ gnutls_x509_crt_set_dn_by_oid(cert, GNUTLS_OID_X520_COMMON_NAME, 0, name.data(), name.length());
+ gnutls_x509_crt_set_dn_by_oid(cert, GNUTLS_OID_LDAP_UID, 0, uid_str.data(), uid_str.length());
+
+ setRandomSerial(cert);
+
+ unsigned key_usage = 0;
+ if (is_ca) {
+ gnutls_x509_crt_set_ca_status(cert, 1);
+ key_usage |= GNUTLS_KEY_KEY_CERT_SIGN | GNUTLS_KEY_CRL_SIGN;
+ } else {
+ key_usage |= GNUTLS_KEY_DIGITAL_SIGNATURE | GNUTLS_KEY_DATA_ENCIPHERMENT;
+ }
+ gnutls_x509_crt_set_key_usage(cert, key_usage);
+
+ if (ca.first && ca.second) {
+ if (not ca.second->isCA()) {
+ throw CryptoException("Signing certificate must be CA");
+ }
+ if (int err = gnutls_x509_crt_privkey_sign(cert, ca.second->cert, ca.first->key, pk.getPreferredDigest(), 0)) {
+ throw CryptoException(std::string("Error when signing certificate ") + gnutls_strerror(err));
+ }
+ ret.issuer = ca.second;
+ } else {
+ if (int err = gnutls_x509_crt_privkey_sign(cert, cert, key.key, pk.getPreferredDigest(), 0)) {
+ throw CryptoException(std::string("Error when signing certificate ") + gnutls_strerror(err));
+ }
+ }
+
+ return ret.getPacked();
+}
+
+Certificate
+Certificate::generate(const CertificateRequest& request, const Identity& ca, int64_t validity)
+{
+ gnutls_x509_crt_t cert;
+ if (auto err = gnutls_x509_crt_init(&cert))
+ throw CryptoException(std::string("Can't initialize certificate: ") + gnutls_strerror(err));
+ Certificate ret {cert};
+ if (auto err = gnutls_x509_crt_set_crq(cert, request.get()))
+ throw CryptoException(std::string("Can't initialize certificate: ") + gnutls_strerror(err));
+
+ if (auto err = gnutls_x509_crt_set_version(cert, 3)) {
+ throw CryptoException(std::string("Can't set certificate version: ") + gnutls_strerror(err));
+ }
+
+ setValidityPeriod(cert, validity <= 0 ? 10 * 365 * 24 * 60 * 60 : validity);
+ setRandomSerial(cert);
+
+ if (auto err = gnutls_x509_crt_privkey_sign(cert, ca.second->cert, ca.first->key, ca.second->getPreferredDigest(), 0)) {
+ throw CryptoException(std::string("Can't sign certificate: ") + gnutls_strerror(err));
+ }
+ ret.issuer = ca.second;
+
+ return ret.getPacked();
+}
+
+void
+Certificate::setValidity(const Identity& ca, int64_t validity)
+{
+ setValidityPeriod(cert, validity);
+ setRandomSerial(cert);
+ if (ca.first && ca.second) {
+ if (not ca.second->isCA()) {
+ throw CryptoException("Signing certificate must be CA");
+ }
+ if (int err = gnutls_x509_crt_privkey_sign(cert, ca.second->cert, ca.first->key, ca.second->getPreferredDigest(), 0)) {
+ throw CryptoException(std::string("Error when signing certificate ") + gnutls_strerror(err));
+ }
+ }
+}
+
+void
+Certificate::setValidity(const PrivateKey& key, int64_t validity)
+{
+ setValidityPeriod(cert, validity);
+ setRandomSerial(cert);
+ const auto& pk = key.getPublicKey();
+ if (int err = gnutls_x509_crt_privkey_sign(cert, cert, key.key, pk.getPreferredDigest(), 0)) {
+ throw CryptoException(std::string("Error when signing certificate ") + gnutls_strerror(err));
+ }
+}
+
+std::vector<std::shared_ptr<RevocationList>>
+Certificate::getRevocationLists() const
+{
+ std::vector<std::shared_ptr<RevocationList>> ret;
+ ret.reserve(revocation_lists.size());
+ for (const auto& crl : revocation_lists)
+ ret.emplace_back(crl);
+ return ret;
+}
+
+// OcspRequest
+
+OcspRequest::OcspRequest(const uint8_t* dat_ptr, size_t dat_size)
+{
+ int ret = gnutls_ocsp_req_init(&request);
+ if (ret < 0)
+ throw CryptoException(gnutls_strerror(ret));
+ gnutls_datum_t dat = {(unsigned char*)dat_ptr,(unsigned int)dat_size};
+ ret = gnutls_ocsp_req_import(request, &dat);
+ if (ret < 0){
+ gnutls_ocsp_req_deinit(request);
+ throw CryptoException(gnutls_strerror(ret));
+ }
+}
+
+OcspRequest::~OcspRequest()
+{
+ if (request) {
+ gnutls_ocsp_req_deinit(request);
+ request = nullptr;
+ }
+}
+
+std::string
+OcspRequest::toString(const bool compact) const
+{
+ int ret;
+ gnutls_datum_t dat;
+ ret = gnutls_ocsp_req_print(request, compact ? GNUTLS_OCSP_PRINT_COMPACT : GNUTLS_OCSP_PRINT_FULL, &dat);
+
+ std::string str;
+ if (ret == 0) {
+ str = std::string((const char*)dat.data, (size_t)dat.size);
+ gnutls_free(dat.data);
+ } else
+ throw CryptoException(gnutls_strerror(ret));
+ return str;
+}
+
+Blob
+OcspRequest::pack() const
+{
+ gnutls_datum_t dat;
+ int err = gnutls_ocsp_req_export(request, &dat);
+ if (err < 0)
+ throw CryptoException(gnutls_strerror(err));
+ Blob ret {dat.data, dat.data + dat.size};
+ gnutls_free(dat.data);
+ return ret;
+}
+
+Blob
+OcspRequest::getNonce() const
+{
+ gnutls_datum_t dat;
+ unsigned critical;
+ int err = gnutls_ocsp_req_get_nonce(request, &critical, &dat);
+ if (err < 0)
+ throw CryptoException(gnutls_strerror(err));
+ Blob ret {dat.data, dat.data + dat.size};
+ gnutls_free(dat.data);
+ return ret;
+}
+
+// OcspResponse
+
+OcspResponse::OcspResponse(const uint8_t* dat_ptr, size_t dat_size)
+{
+ int ret = gnutls_ocsp_resp_init(&response);
+ if (ret < 0)
+ throw CryptoException(gnutls_strerror(ret));
+ gnutls_datum_t dat = {(unsigned char*)dat_ptr,(unsigned int)dat_size};
+ ret = gnutls_ocsp_resp_import(response, &dat);
+ if (ret < 0){
+ gnutls_ocsp_resp_deinit(response);
+ throw CryptoException(gnutls_strerror(ret));
+ }
+}
+
+OcspResponse::~OcspResponse()
+{
+ gnutls_ocsp_resp_deinit(response);
+}
+
+Blob
+OcspResponse::pack() const
+{
+ gnutls_datum_t dat;
+ int err = gnutls_ocsp_resp_export(response, &dat);
+ if (err < 0)
+ throw CryptoException(gnutls_strerror(err));
+ Blob ret {dat.data, dat.data + dat.size};
+ gnutls_free(dat.data);
+ return ret;
+}
+
+std::string
+OcspResponse::toString(const bool compact) const
+{
+ int ret;
+ std::string str;
+ gnutls_datum_t dat;
+ ret = gnutls_ocsp_resp_print(response, compact ? GNUTLS_OCSP_PRINT_COMPACT : GNUTLS_OCSP_PRINT_FULL, &dat);
+ if (ret == 0)
+ str = std::string((const char*)dat.data, (size_t)dat.size);
+ gnutls_free(dat.data);
+ if (ret < 0)
+ throw CryptoException(gnutls_strerror(ret));
+ return str;
+}
+
+
+gnutls_ocsp_cert_status_t
+OcspResponse::getCertificateStatus() const
+{
+ int ret;
+ unsigned int status;
+ ret = gnutls_ocsp_resp_get_single(response, 0, NULL, NULL, NULL, NULL, &status, NULL, NULL, NULL, NULL);
+ if (ret < 0)
+ throw CryptoException(gnutls_strerror(ret));
+ return (gnutls_ocsp_cert_status_t) status;
+}
+
+gnutls_ocsp_cert_status_t
+OcspResponse::verifyDirect(const Certificate& crt, const Blob& nonce)
+{
+ // Check OCSP response
+ int ret = gnutls_ocsp_resp_get_status(response);
+ if (ret < 0)
+ throw CryptoException(gnutls_strerror(ret));
+ gnutls_ocsp_resp_status_t status = (gnutls_ocsp_resp_status_t)ret;
+ if (status != GNUTLS_OCSP_RESP_SUCCESSFUL)
+ throw CryptoException("OCSP request unsuccessful: " + std::to_string(ret));
+
+ if (not nonce.empty()) {
+ // Ensure no replay attack has been done
+ gnutls_datum_t rnonce;
+ ret = gnutls_ocsp_resp_get_nonce(response, NULL, &rnonce);
+ if (ret < 0)
+ throw CryptoException(gnutls_strerror(ret));
+ if (rnonce.size != nonce.size() || memcmp(nonce.data(), rnonce.data, nonce.size()) != 0){
+ gnutls_free(rnonce.data);
+ throw CryptoException(gnutls_strerror(GNUTLS_E_OCSP_RESPONSE_ERROR));
+ }
+ gnutls_free(rnonce.data);
+ }
+
+ // Verify signature of the Basic OCSP response against the public key in the issuer certificate.
+ unsigned int verify = 0;
+ ret = gnutls_ocsp_resp_verify_direct(response, crt.issuer->cert, &verify, 0);
+ if (ret < 0)
+ throw CryptoException(gnutls_strerror(ret));
+ if (verify) {
+ if (verify & GNUTLS_OCSP_VERIFY_SIGNER_NOT_FOUND)
+ throw CryptoException("Signer cert not found");
+ if (verify & GNUTLS_OCSP_VERIFY_SIGNER_KEYUSAGE_ERROR)
+ throw CryptoException("Signer cert keyusage error");
+ if (verify & GNUTLS_OCSP_VERIFY_UNTRUSTED_SIGNER)
+ throw CryptoException("Signer cert is not trusted");
+ if (verify & GNUTLS_OCSP_VERIFY_INSECURE_ALGORITHM)
+ throw CryptoException("Insecure algorithm");
+ if (verify & GNUTLS_OCSP_VERIFY_SIGNATURE_FAILURE)
+ throw CryptoException("Signature failure");
+ if (verify & GNUTLS_OCSP_VERIFY_CERT_NOT_ACTIVATED)
+ throw CryptoException("Signer cert not yet activated");
+ if (verify & GNUTLS_OCSP_VERIFY_CERT_EXPIRED)
+ throw CryptoException("Signer cert expired");
+ throw CryptoException(gnutls_strerror(GNUTLS_E_OCSP_RESPONSE_ERROR));
+ }
+
+ // Check whether the OCSP response is about the provided certificate.
+ if ((ret = gnutls_ocsp_resp_check_crt(response, 0, crt.cert)) < 0)
+ throw CryptoException(gnutls_strerror(ret));
+
+ // Check certificate revocation status
+ unsigned status_ocsp;
+ ret = gnutls_ocsp_resp_get_single(response, 0, NULL, NULL, NULL, NULL, &status_ocsp, NULL, NULL, NULL, NULL);
+ if (ret < 0)
+ throw CryptoException(gnutls_strerror(ret));
+ return (gnutls_ocsp_cert_status_t)status_ocsp;
+}
+
+// RevocationList
+
+RevocationList::RevocationList()
+{
+ gnutls_x509_crl_init(&crl);
+}
+
+RevocationList::RevocationList(const Blob& b)
+{
+ gnutls_x509_crl_init(&crl);
+ try {
+ unpack(b.data(), b.size());
+ } catch (const std::exception& e) {
+ gnutls_x509_crl_deinit(crl);
+ crl = nullptr;
+ throw;
+ }
+}
+
+RevocationList::~RevocationList()
+{
+ if (crl) {
+ gnutls_x509_crl_deinit(crl);
+ crl = nullptr;
+ }
+}
+
+void
+RevocationList::pack(Blob& b) const
+{
+ gnutls_datum_t gdat {nullptr, 0};
+ if (auto err = gnutls_x509_crl_export2(crl, GNUTLS_X509_FMT_DER, &gdat)) {
+ throw CryptoException(std::string("Can't export CRL: ") + gnutls_strerror(err));
+ }
+ b.insert(b.end(), gdat.data, gdat.data + gdat.size);
+ gnutls_free(gdat.data);
+}
+
+void
+RevocationList::unpack(const uint8_t* dat, size_t dat_size)
+{
+ if (std::numeric_limits<unsigned>::max() < dat_size)
+ throw CryptoException("Can't load CRL: too large!");
+ const gnutls_datum_t gdat {(uint8_t*)dat, (unsigned)dat_size};
+ if (auto err_pem = gnutls_x509_crl_import(crl, &gdat, GNUTLS_X509_FMT_PEM))
+ if (auto err_der = gnutls_x509_crl_import(crl, &gdat, GNUTLS_X509_FMT_DER)) {
+ throw CryptoException(std::string("Can't load CRL: PEM: ") + gnutls_strerror(err_pem)
+ + " DER: " + gnutls_strerror(err_der));
+ }
+}
+
+void
+RevocationList::msgpack_unpack(const msgpack::object& o)
+{
+ try {
+ if (o.type == msgpack::type::BIN)
+ unpack((const uint8_t*)o.via.bin.ptr, o.via.bin.size);
+ else {
+ Blob dat = unpackBlob(o);
+ unpack(dat.data(), dat.size());
+ }
+ } catch (...) {
+ throw msgpack::type_error();
+ }
+}
+
+bool
+RevocationList::isRevoked(const Certificate& crt) const
+{
+ auto ret = gnutls_x509_crt_check_revocation(crt.cert, &crl, 1);
+ if (ret < 0)
+ throw CryptoException(std::string("Can't check certificate revocation status: ") + gnutls_strerror(ret));
+ return ret != 0;
+}
+
+void
+RevocationList::revoke(const Certificate& crt, std::chrono::system_clock::time_point t)
+{
+ if (t == time_point::min())
+ t = clock::now();
+ if (auto err = gnutls_x509_crl_set_crt(crl, crt.cert, std::chrono::system_clock::to_time_t(t)))
+ throw CryptoException(std::string("Can't revoke certificate: ") + gnutls_strerror(err));
+}
+
+static std::string
+getCRLIssuerDN(gnutls_x509_crl_t cert, const char* oid)
+{
+ std::string dn;
+ dn.resize(512);
+ size_t dn_sz = dn.size();
+ int ret = gnutls_x509_crl_get_issuer_dn_by_oid(cert, oid, 0, 0, &(*dn.begin()), &dn_sz);
+ if (ret != GNUTLS_E_SUCCESS)
+ return {};
+ dn.resize(dn_sz);
+ return dn;
+}
+
+std::string
+RevocationList::getIssuerName() const
+{
+ return getCRLIssuerDN(crl, GNUTLS_OID_X520_COMMON_NAME);
+}
+
+/** Read CRL issuer User ID (UID) */
+std::string
+RevocationList::getIssuerUID() const
+{
+ return getCRLIssuerDN(crl, GNUTLS_OID_LDAP_UID);
+}
+
+RevocationList::time_point
+RevocationList::getNextUpdateTime() const
+{
+ auto t = gnutls_x509_crl_get_next_update(crl);
+ if (t == (time_t)-1)
+ return std::chrono::system_clock::time_point::min();
+ return std::chrono::system_clock::from_time_t(t);
+}
+
+RevocationList::time_point
+RevocationList::getUpdateTime() const
+{
+ auto t = gnutls_x509_crl_get_this_update(crl);
+ if (t == (time_t)-1)
+ return std::chrono::system_clock::time_point::min();
+ 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)
+{
+ if (auto err = gnutls_x509_crl_set_version(crl, 2))
+ throw CryptoException(std::string("Can't set CRL version: ") + gnutls_strerror(err));
+ auto now = std::chrono::system_clock::now();
+ auto next_update = (validity == duration{}) ? ca.getExpiration() : now + validity;
+ if (auto err = gnutls_x509_crl_set_this_update(crl, std::chrono::system_clock::to_time_t(now)))
+ throw CryptoException(std::string("Can't 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("Can't set CRL next update time: ") + gnutls_strerror(err));
+ uint64_t number {0};
+ size_t number_sz {sizeof(number)};
+ unsigned critical {0};
+ gnutls_x509_crl_get_number(crl, &number, &number_sz, &critical);
+ if (number == 0) {
+ // initialize to a random number
+ number_sz = sizeof(number);
+ 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("Can't 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("Can't sign certificate revocation list: ") + gnutls_strerror(err));
+ // to be able to actually use the CRL we need to serialize/deserialize it
+ auto packed = getPacked();
+ unpack(packed.data(), packed.size());
+}
+
+bool
+RevocationList::isSignedBy(const Certificate& issuer) const
+{
+ unsigned result {0};
+ auto err = gnutls_x509_crl_verify(crl, &issuer.cert, 1, 0, &result);
+ if (err < 0) {
+ //std::cout << "Can't verify CRL: " << err << " " << result << " " << gnutls_strerror(err) << std::endl;
+ return false;
+ }
+ return result == 0;
+}
+
+
+Blob
+RevocationList::getNumber() const
+{
+ Blob number(20);
+ size_t number_sz {number.size()};
+ unsigned critical {0};
+ gnutls_x509_crl_get_number(crl, number.data(), &number_sz, &critical);
+ if (number_sz != number.size())
+ number.resize(number_sz);
+ return number;
+}
+
+std::string
+RevocationList::toString() const
+{
+ gnutls_datum_t out;
+ gnutls_x509_crl_print(crl, GNUTLS_CRT_PRINT_FULL, &out);
+ std::string ret(out.data, out.data+out.size);
+ gnutls_free(out.data);
+ return ret;
+}
+
+// TrustList
+
+TrustList::TrustList() {
+ gnutls_x509_trust_list_init(&trust, 0);
+}
+
+TrustList::~TrustList() {
+ gnutls_x509_trust_list_deinit(trust, 1);
+}
+
+TrustList&
+TrustList::operator=(TrustList&& o) noexcept
+{
+ if (trust)
+ gnutls_x509_trust_list_deinit(trust, true);
+ trust = o.trust;
+ o.trust = nullptr;
+ return *this;
+}
+
+void TrustList::add(const Certificate& crt)
+{
+ auto chain = crt.getChainWithRevocations(true);
+ gnutls_x509_trust_list_add_cas(trust, chain.first.data(), chain.first.size(), GNUTLS_TL_NO_DUPLICATES);
+ if (not chain.second.empty())
+ gnutls_x509_trust_list_add_crls(
+ trust,
+ chain.second.data(), chain.second.size(),
+ GNUTLS_TL_VERIFY_CRL | GNUTLS_TL_NO_DUPLICATES, 0);
+}
+
+void TrustList::add(const RevocationList& crl)
+{
+ auto copy = crl.getCopy();
+ gnutls_x509_trust_list_add_crls(trust, ©, 1, GNUTLS_TL_VERIFY_CRL | GNUTLS_TL_NO_DUPLICATES, 0);
+}
+
+void TrustList::remove(const Certificate& crt, bool parents)
+{
+ gnutls_x509_trust_list_remove_cas(trust, &crt.cert, 1);
+ if (parents) {
+ for (auto c = crt.issuer; c; c = c->issuer)
+ gnutls_x509_trust_list_remove_cas(trust, &c->cert, 1);
+ }
+}
+
+TrustList::VerifyResult
+TrustList::verify(const Certificate& crt) const
+{
+ auto chain = crt.getChain();
+ VerifyResult ret;
+ ret.ret = gnutls_x509_trust_list_verify_crt2(
+ trust,
+ chain.data(), chain.size(),
+ nullptr, 0,
+ GNUTLS_PROFILE_TO_VFLAGS(GNUTLS_PROFILE_MEDIUM),
+ &ret.result, nullptr);
+ return ret;
+}
+
+std::string
+TrustList::VerifyResult::toString() const
+{
+ std::ostringstream ss;
+ ss << *this;
+ return ss.str();
+}
+
+std::ostream& operator<< (std::ostream& o, const TrustList::VerifyResult& h)
+{
+ if (h.ret < 0) {
+ o << "Error verifying certificate: " << gnutls_strerror(h.ret) << std::endl;
+ } else if (h.result & GNUTLS_CERT_INVALID) {
+ o << "Certificate check failed with code: " << h.result << std::endl;
+ if (h.result & GNUTLS_CERT_SIGNATURE_FAILURE)
+ o << "* The signature verification failed." << std::endl;
+ if (h.result & GNUTLS_CERT_REVOKED)
+ o << "* Certificate is revoked" << std::endl;
+ if (h.result & GNUTLS_CERT_SIGNER_NOT_FOUND)
+ o << "* Certificate's issuer is not known" << std::endl;
+ if (h.result & GNUTLS_CERT_SIGNER_NOT_CA)
+ o << "* Certificate's issuer not a CA" << std::endl;
+ if (h.result & GNUTLS_CERT_SIGNER_CONSTRAINTS_FAILURE)
+ o << "* Certificate's signer constraints were violated" << std::endl;
+ if (h.result & GNUTLS_CERT_INSECURE_ALGORITHM)
+ o << "* Certificate was signed using an insecure algorithm" << std::endl;
+ if (h.result & GNUTLS_CERT_NOT_ACTIVATED)
+ o << "* Certificate is not yet activated" << std::endl;
+ if (h.result & GNUTLS_CERT_EXPIRED)
+ o << "* Certificate has expired" << std::endl;
+ if (h.result & GNUTLS_CERT_UNEXPECTED_OWNER)
+ o << "* The owner is not the expected one" << std::endl;
+ if (h.result & GNUTLS_CERT_PURPOSE_MISMATCH)
+ o << "* Certificate or an intermediate does not match the intended purpose" << std::endl;
+ if (h.result & GNUTLS_CERT_MISMATCH)
+ o << "* Certificate presented isn't the expected one" << std::endl;
+ } else {
+ o << "Certificate is valid" << std::endl;
+ }
+ return o;
+}
+
+}
+}
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ * Author : Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "default_types.h"
+
+namespace dht {
+
+std::ostream& operator<< (std::ostream& s, const DhtMessage& v)
+{
+ s << "DhtMessage: service " << v.service << std::endl;
+ return s;
+}
+
+bool
+DhtMessage::storePolicy(InfoHash h, std::shared_ptr<Value>& v, const InfoHash& f, const SockAddr& sa)
+{
+ try {
+ auto msg = unpackMsg<DhtMessage>(v->data);
+ if (msg.service.empty())
+ return false;
+ } catch (const std::exception& e) {}
+ return ValueType::DEFAULT_STORE_POLICY(h, v, f, sa);
+}
+
+Value::Filter
+DhtMessage::ServiceFilter(const std::string& s)
+{
+ return Value::Filter::chain(
+ Value::TypeFilter(TYPE),
+ [s](const Value& v) {
+ try {
+ return unpackMsg<DhtMessage>(v.data).service == s;
+ } catch (const std::exception& e) {
+ return false;
+ }
+ }
+ );
+}
+
+std::ostream& operator<< (std::ostream& s, const IpServiceAnnouncement& v)
+{
+ if (v.addr) {
+ s << "Peer: ";
+ s << "port " << v.getPort();
+ char hbuf[NI_MAXHOST];
+ if (getnameinfo(v.addr.get(), v.addr.getLength(), hbuf, sizeof(hbuf), nullptr, 0, NI_NUMERICHOST) == 0) {
+ s << " addr " << std::string(hbuf, strlen(hbuf));
+ }
+ }
+ return s;
+}
+
+bool
+IpServiceAnnouncement::storePolicy(InfoHash h, std::shared_ptr<Value>& v, const InfoHash& f, const SockAddr& sa)
+{
+ try {
+ auto msg = unpackMsg<IpServiceAnnouncement>(v->data);
+ if (msg.getPort() == 0)
+ return false;
+ IpServiceAnnouncement sa_addr {sa};
+ sa_addr.setPort(msg.getPort());
+ // argument v is modified (not the value).
+ v = std::make_shared<Value>(IpServiceAnnouncement::TYPE, sa_addr, v->id);
+ return ValueType::DEFAULT_STORE_POLICY(h, v, f, sa);
+ } catch (const std::exception& e) {}
+ return false;
+}
+
+const ValueType DhtMessage::TYPE(1, "DHT message", std::chrono::minutes(5), DhtMessage::storePolicy);
+const ValueType IpServiceAnnouncement::TYPE(2, "Internet Service Announcement", std::chrono::minutes(15), IpServiceAnnouncement::storePolicy);
+const ValueType ImMessage::TYPE = {3, "IM message", std::chrono::minutes(5)};
+const ValueType TrustRequest::TYPE = {4, "Certificate trust request", std::chrono::hours(24*7)};
+const ValueType IceCandidates::TYPE = {5, "ICE candidates", std::chrono::minutes(1)};
+
+const std::array<std::reference_wrapper<const ValueType>, 5>
+DEFAULT_TYPES
+{{
+ ValueType::USER_DATA,
+ DhtMessage::TYPE,
+ ImMessage::TYPE,
+ IceCandidates::TYPE,
+ TrustRequest::TYPE
+}};
+
+const std::array<std::reference_wrapper<const ValueType>, 1>
+DEFAULT_INSECURE_TYPES
+{{
+ IpServiceAnnouncement::TYPE
+}};
+
+}
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ * Author(s) : Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ * Simon Désaulniers <simon.desaulniers@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "dht.h"
+#include "rng.h"
+#include "search.h"
+#include "storage.h"
+#include "request.h"
+
+#include <msgpack.hpp>
+
+#include <algorithm>
+#include <random>
+#include <sstream>
+#include <fstream>
+
+#include <inttypes.h>
+
+namespace dht {
+
+using namespace std::placeholders;
+
+constexpr std::chrono::minutes Dht::MAX_STORAGE_MAINTENANCE_EXPIRE_TIME;
+constexpr std::chrono::minutes Dht::SEARCH_EXPIRE_TIME;
+constexpr std::chrono::seconds Dht::BOOTSTRAP_PERIOD;
+constexpr duration Dht::LISTEN_EXPIRE_TIME;
+constexpr duration Dht::LISTEN_EXPIRE_TIME_PUBLIC;
+constexpr duration Dht::REANNOUNCE_MARGIN;
+static constexpr size_t MAX_REQUESTS_PER_SEC {8 * 1024};
+static constexpr duration BOOTSTRAP_PERIOD_MAX {std::chrono::hours(24)};
+
+NodeStatus
+Dht::updateStatus(sa_family_t af)
+{
+ auto& d = dht(af);
+ auto old = d.status;
+ d.status = d.getStatus(scheduler.time());
+ if (d.status != old) {
+ auto& other = dht(af == AF_INET ? AF_INET6 : AF_INET);
+ if (other.status == NodeStatus::Disconnected && d.status == NodeStatus::Disconnected) {
+ onDisconnected();
+ } else if (other.status == NodeStatus::Connected || d.status == NodeStatus::Connected) {
+ onConnected();
+ }
+ }
+ return d.status;
+}
+
+NodeStatus
+Dht::Kad::getStatus(time_point now) const
+{
+ unsigned dubious = 0;
+ for (const auto& b : buckets) {
+ for (auto& n : b.nodes) {
+ if (n->isGood(now)) {
+ return NodeStatus::Connected;
+ } else if (not n->isExpired())
+ dubious++;
+ }
+ }
+ auto& ping = pending_pings;
+ if (dubious or ping)
+ return NodeStatus::Connecting;
+ return NodeStatus::Disconnected;
+}
+
+void
+Dht::shutdown(ShutdownCallback cb, bool stop)
+{
+ if (not persistPath.empty())
+ saveState(persistPath);
+
+ if (stop) {
+ for (auto dht : {&dht4, &dht6}) {
+ for (auto& sr : dht->searches) {
+ for (const auto& r : sr.second->callbacks)
+ r.second.done_cb(false, {});
+ sr.second->callbacks.clear();
+ for (const auto& a : sr.second->announce) {
+ if (a.callback) a.callback(false, {});
+ }
+ sr.second->announce.clear();
+ sr.second->listeners.clear();
+ }
+ }
+ network_engine.clear();
+ }
+
+ if (not maintain_storage) {
+ if (cb) cb();
+ return;
+ }
+
+ // Last store maintenance
+ scheduler.syncTime();
+ auto remaining = std::make_shared<int>(0);
+ auto str_donecb = [=](bool, const std::vector<Sp<Node>>&) {
+ --*remaining;
+ if (logger_)
+ logger_->w("shuting down node: %u ops remaining", *remaining);
+ if (!*remaining && cb) { cb(); }
+ };
+
+ for (auto& str : store)
+ *remaining += maintainStorage(str, true, str_donecb);
+
+ if (logger_)
+ logger_->w("shuting down node: after storage, %u ops", *remaining);
+
+ if (!*remaining) {
+ if (cb) cb();
+ }
+}
+
+bool
+Dht::isRunning(sa_family_t af) const { return network_engine.isRunning(af); }
+
+/* Every bucket contains an unordered list of nodes. */
+const Sp<Node>
+Dht::findNode(const InfoHash& id, sa_family_t af) const
+{
+ if (const Bucket* b = findBucket(id, af))
+ for (const auto& n : b->nodes)
+ if (n->id == id) return n;
+ return {};
+}
+
+/* Every bucket caches the address of a likely node. Ping it. */
+void
+Dht::sendCachedPing(Bucket& b)
+{
+ if (b.cached)
+ if (logger_)
+ logger_->d(b.cached->id, "[node %s] sending ping to cached node", b.cached->toString().c_str());
+ b.sendCachedPing(network_engine);
+}
+
+std::vector<SockAddr>
+Dht::getPublicAddress(sa_family_t family)
+{
+ std::vector<SockAddr> ret;
+ if (family == AF_UNSPEC) {
+ auto& d4 = dht(AF_INET).reported_addr;
+ auto& d6 = dht(AF_INET6).reported_addr;
+ ret.reserve(d4.size() + d6.size());
+ for (const auto& a : d4)
+ ret.emplace_back(a.second);
+ for (const auto& a : d6)
+ ret.emplace_back(a.second);
+ return ret;
+ }
+ auto& reported_addr = dht(family).reported_addr;
+ ret.reserve(!family ? reported_addr.size() : reported_addr.size()/2);
+ for (const auto& addr : reported_addr)
+ if (!family || family == addr.second.getFamily())
+ ret.emplace_back(addr.second);
+ return ret;
+}
+
+bool
+Dht::trySearchInsert(const Sp<Node>& node)
+{
+ const auto& now = scheduler.time();
+ if (not node) return false;
+
+ auto& srs = searches(node->getFamily());
+ auto closest = srs.lower_bound(node->id);
+ bool inserted {false};
+
+ // insert forward
+ for (auto it = closest; it != srs.end(); it++) {
+ auto& s = *it->second;
+ if (s.insertNode(node, now)) {
+ inserted = true;
+ scheduler.edit(s.nextSearchStep, now);
+ } else if (not s.expired and not s.done)
+ break;
+ }
+ // insert backward
+ for (auto it = closest; it != srs.begin();) {
+ --it;
+ auto& s = *it->second;
+ if (s.insertNode(node, now)) {
+ inserted = true;
+ scheduler.edit(s.nextSearchStep, now);
+ } else if (not s.expired and not s.done)
+ break;
+ }
+ return inserted;
+}
+
+void
+Dht::reportedAddr(const SockAddr& addr)
+{
+ auto& reported_addr = dht(addr.getFamily()).reported_addr;
+ auto firstBefore = reported_addr.empty() ? nullptr : reported_addr.begin()->second.get();
+ auto it = std::find_if(reported_addr.begin(), reported_addr.end(), [&](const ReportedAddr& a){
+ return a.second == addr;
+ });
+ if (it == reported_addr.end()) {
+ if (reported_addr.size() < 32)
+ reported_addr.emplace_back(1, addr);
+ } else
+ it->first++;
+ std::sort(reported_addr.begin(), reported_addr.end(), [](const ReportedAddr& a, const ReportedAddr& b) {
+ return a.first > b.first;
+ });
+ if (publicAddressChangedCb_) {
+ auto firstAfter = reported_addr.begin()->second.get();
+ if (firstBefore != firstAfter) {
+ auto& otherDht = dht(addr.getFamily() == AF_INET ? AF_INET6 : AF_INET).reported_addr;
+ std::vector<SockAddr> v;
+ v.reserve(otherDht.empty() ? 1 : 2);
+ v.emplace_back(reported_addr.begin()->second);
+ if (not otherDht.empty())
+ v.emplace_back(otherDht.begin()->second);
+ publicAddressChangedCb_(std::move(v));
+ }
+ }
+}
+
+/* We just learnt about a node, not necessarily a new one. Confirm is 1 if
+ the node sent a message, 2 if it sent us a reply. */
+void
+Dht::onNewNode(const Sp<Node>& node, int confirm)
+{
+ const auto& now = scheduler.time();
+ auto& b = buckets(node->getFamily());
+ auto wasEmpty = confirm < 2 && b.grow_time < now - std::chrono::minutes(5);
+ if (b.onNewNode(node, confirm, now, myid, network_engine) or confirm) {
+ trySearchInsert(node);
+ if (wasEmpty) {
+ scheduler.edit(nextNodesConfirmation, now + std::chrono::seconds(1));
+ }
+ }
+}
+
+/* Called periodically to purge known-bad nodes. Note that we're very
+ conservative here: broken nodes in the table don't do much harm, we'll
+ recover as soon as we find better ones. */
+void
+Dht::expireBuckets(RoutingTable& list)
+{
+ for (auto& b : list) {
+ bool changed = false;
+ b.nodes.remove_if([&changed](const Sp<Node>& n) {
+ if (n->isExpired()) {
+ changed = true;
+ return true;
+ }
+ return false;
+ });
+ if (changed)
+ sendCachedPing(b);
+ }
+}
+
+void
+Dht::expireSearches()
+{
+ auto t = scheduler.time() - SEARCH_EXPIRE_TIME;
+ auto expired = [&](std::pair<const InfoHash, Sp<Search>>& srp) {
+ auto& sr = *srp.second;
+ auto b = sr.callbacks.empty() && sr.announce.empty() && sr.listeners.empty() && sr.step_time < t;
+ if (b) {
+ if (logger_)
+ logger_->d(srp.first, "[search %s] removing search", srp.first.toString().c_str());
+ sr.clear();
+ return b;
+ } else { return false; }
+ };
+ erase_if(dht4.searches, expired);
+ erase_if(dht6.searches, expired);
+}
+
+void
+Dht::searchNodeGetDone(const net::Request& req,
+ net::RequestAnswer&& answer,
+ std::weak_ptr<Search> ws,
+ Sp<Query> query)
+{
+ const auto& now = scheduler.time();
+ if (auto sr = ws.lock()) {
+ sr->insertNode(req.node, now, answer.ntoken);
+ if (auto srn = sr->getNode(req.node)) {
+ /* all other get requests which are satisfied by this answer
+ should not be sent anymore */
+ for (auto& g : sr->callbacks) {
+ auto& q = g.second.query;
+ if (q->isSatisfiedBy(*query) and q != query) {
+ auto dummy_req = std::make_shared<net::Request>();
+ dummy_req->cancel();
+ srn->getStatus[q] = std::move(dummy_req);
+ }
+ }
+ auto syncTime = srn->getSyncTime(scheduler.time());
+ if (srn->syncJob)
+ scheduler.edit(srn->syncJob, syncTime);
+ else
+ srn->syncJob = scheduler.add(syncTime, std::bind(&Dht::searchStep, this, ws));
+ }
+ onGetValuesDone(req.node, answer, sr, query);
+ }
+}
+
+void
+Dht::searchNodeGetExpired(const net::Request& status,
+ bool over,
+ std::weak_ptr<Search> ws,
+ Sp<Query> query)
+{
+ if (auto sr = ws.lock()) {
+ if (auto srn = sr->getNode(status.node)) {
+ srn->candidate = not over;
+ if (over)
+ srn->getStatus.erase(query);
+ }
+ scheduler.edit(sr->nextSearchStep, scheduler.time());
+ }
+}
+
+void Dht::paginate(std::weak_ptr<Search> ws, Sp<Query> query, SearchNode* n) {
+ auto sr = ws.lock();
+ if (not sr) return;
+ auto select_q = std::make_shared<Query>(Select {}.field(Value::Field::Id), query ? query->where : Where {});
+ auto onSelectDone = [this,ws,query](const net::Request& status,
+ net::RequestAnswer&& answer) mutable {
+ // Retrieve search
+ auto sr = ws.lock();
+ if (not sr) return;
+ const auto& id = sr->id;
+ // Retrieve search node
+ auto sn = sr->getNode(status.node);
+ if (not sn) return;
+ // backward compatibility
+ if (answer.fields.empty()) {
+ searchNodeGetDone(status, std::move(answer), ws, query);
+ return;
+ }
+ for (const auto& fvi : answer.fields) {
+ try {
+ auto vid = fvi->index.at(Value::Field::Id).getInt();
+ if (vid == Value::INVALID_ID) continue;
+ auto query_for_vid = std::make_shared<Query>(Select {}, Where {}.id(vid));
+ sn->pagination_queries[query].push_back(query_for_vid);
+ if (logger_)
+ logger_->d(id, sn->node->id, "[search %s] [node %s] sending %s",
+ id.toString().c_str(), sn->node->toString().c_str(), query_for_vid->toString().c_str());
+ sn->getStatus[query_for_vid] = network_engine.sendGetValues(status.node,
+ id,
+ *query_for_vid,
+ -1,
+ std::bind(&Dht::searchNodeGetDone, this, _1, _2, ws, query),
+ std::bind(&Dht::searchNodeGetExpired, this, _1, _2, ws, query_for_vid)
+ );
+ } catch (const std::out_of_range&) {
+ if (logger_)
+ logger_->e(id, sn->node->id, "[search %s] [node %s] received non-id field in response to "\
+ "'SELECT id' request...",
+ id.toString().c_str(), sn->node->toString().c_str());
+ }
+ }
+ };
+ /* add pagination query key for tracking ongoing requests. */
+ n->pagination_queries[query].push_back(select_q);
+
+ if (logger_)
+ logger_->d(sr->id, n->node->id, "[search %s] [node %s] sending %s",
+ sr->id.toString().c_str(), n->node->toString().c_str(), select_q->toString().c_str());
+ n->getStatus[select_q] = network_engine.sendGetValues(n->node,
+ sr->id,
+ *select_q,
+ -1,
+ onSelectDone,
+ std::bind(&Dht::searchNodeGetExpired, this, _1, _2, ws, select_q)
+ );
+}
+
+Dht::SearchNode*
+Dht::searchSendGetValues(Sp<Search> sr, SearchNode* pn, bool update)
+{
+ if (sr->done or sr->currentlySolicitedNodeCount() >= MAX_REQUESTED_SEARCH_NODES)
+ return nullptr;
+
+ const auto& now = scheduler.time();
+
+ std::weak_ptr<Search> ws = sr;
+ auto cb = sr->callbacks.begin();
+ static const auto ANY_QUERY = std::make_shared<Query>(Select {}, Where {}, true);
+ do { /* for all requests to send */
+ SearchNode* n = nullptr;
+ auto& query = sr->callbacks.empty() ? ANY_QUERY : cb->second.query;
+ const time_point up = (not sr->callbacks.empty() and update)
+ ? sr->getLastGetTime(*query)
+ : time_point::min();
+
+ if (pn and pn->canGet(now, up, query)) {
+ n = pn;
+ } else {
+ for (auto& sn : sr->nodes) {
+ if (sn->canGet(now, up, query)) {
+ n = sn.get();
+ break;
+ }
+ }
+ }
+
+ if (sr->callbacks.empty()) { /* 'find_node' request */
+ if (not n)
+ return nullptr;
+
+ /* if (logger_)
+ logger_->d(sr->id, n->node->id, "[search %s] [node %s] sending 'find_node'",
+ sr->id.toString().c_str(), n->node->toString().c_str());*/
+ n->getStatus[query] = network_engine.sendFindNode(n->node,
+ sr->id,
+ -1,
+ std::bind(&Dht::searchNodeGetDone, this, _1, _2, ws, query),
+ std::bind(&Dht::searchNodeGetExpired, this, _1, _2, ws, query));
+
+ } else { /* 'get' request */
+ if (not n)
+ continue;
+
+ if (query and not query->select.empty()) {
+ /* The request contains a select. No need to paginate... */
+ /* if (logger_)
+ logger_->d(sr->id, n->node->id, "[search %s] [node %s] sending 'get'",
+ sr->id.toString().c_str(), n->node->toString().c_str());*/
+ n->getStatus[query] = network_engine.sendGetValues(n->node,
+ sr->id,
+ *query,
+ -1,
+ std::bind(&Dht::searchNodeGetDone, this, _1, _2, ws, query),
+ std::bind(&Dht::searchNodeGetExpired, this, _1, _2, ws, query));
+ } else
+ paginate(ws, query, n);
+ }
+
+ /* We only try to send one request. return. */
+ return n;
+
+ } while (++cb != sr->callbacks.end());
+
+ /* no request were sent */
+ return nullptr;
+}
+
+void Dht::searchSendAnnounceValue(const Sp<Search>& sr, unsigned syncLevel) {
+ if (sr->announce.empty())
+ return;
+ std::weak_ptr<Search> ws = sr;
+
+ auto onDone = [this,ws](const net::Request& req, net::RequestAnswer&& answer)
+ { /* when put done */
+ if (auto sr = ws.lock()) {
+ onAnnounceDone(req.node, answer, sr);
+ scheduler.edit(sr->nextSearchStep, scheduler.time());
+ }
+ };
+
+ auto onExpired = [this,ws](const net::Request&, bool over)
+ { /* when put expired */
+ if (over)
+ if (auto sr = ws.lock())
+ scheduler.edit(sr->nextSearchStep, scheduler.time());
+ };
+
+ auto onSelectDone =
+ [this,ws,onDone,onExpired](const net::Request& req, net::RequestAnswer&& answer) mutable
+ { /* on probing done */
+ auto sr = ws.lock();
+ if (not sr) return;
+ const auto& now = scheduler.time();
+ sr->insertNode(req.node, scheduler.time(), answer.ntoken);
+ auto sn = sr->getNode(req.node);
+ if (not sn) return;
+
+ if (not sn->isSynced(now)) {
+ /* Search is now unsynced. Let's call searchStep to sync again. */
+ scheduler.edit(sr->nextSearchStep, now);
+ return;
+ }
+ for (auto& a : sr->announce) {
+ if (sn->getAnnounceTime(a.value->id) > now)
+ continue;
+ bool hasValue {false};
+ uint16_t seq_no = 0;
+ try {
+ const auto& f = std::find_if(answer.fields.cbegin(), answer.fields.cend(),
+ [&a](const Sp<FieldValueIndex>& i){
+ return i->index.at(Value::Field::Id).getInt() == a.value->id;
+ });
+ if (f != answer.fields.cend() and *f) {
+ hasValue = true;
+ seq_no = static_cast<uint16_t>((*f)->index.at(Value::Field::SeqNum).getInt());
+ }
+ } catch (std::out_of_range&) { }
+
+ auto next_refresh_time = now + getType(a.value->type).expiration;
+ auto& acked = sn->acked[a.value->id];
+ scheduler.cancel(acked.refresh);
+ /* only put the value if the node doesn't already have it */
+ if (not hasValue or seq_no < a.value->seq) {
+ if (logger_)
+ logger_->d(sr->id, sn->node->id, "[search %s] [node %s] sending 'put' (vid: %016" PRIx64 ")",
+ sr->id.toString().c_str(), sn->node->toString().c_str(), a.value->id);
+ auto created = a.permanent ? time_point::max() : a.created;
+ acked = {
+ network_engine.sendAnnounceValue(sn->node, sr->id, a.value, created, sn->token, onDone, onExpired),
+ next_refresh_time
+ };
+ } else if (hasValue and a.permanent) {
+ if (logger_)
+ logger_->w(sr->id, sn->node->id, "[search %s] [node %s] sending 'refresh' (vid: %016" PRIx64 ")",
+ sr->id.toString().c_str(), sn->node->toString().c_str(), a.value->id);
+ acked = {
+ network_engine.sendRefreshValue(sn->node, sr->id, a.value->id, sn->token, onDone,
+ [this, ws, node=sn->node, v=a.value,
+ onDone,
+ onExpired,
+ created = a.permanent ? time_point::max() : a.created,
+ next_refresh_time
+ ](const net::Request& /*req*/, net::DhtProtocolException&& e){
+ if (e.getCode() == net::DhtProtocolException::NOT_FOUND) {
+ if (logger_)
+ logger_->e(node->id, "[node %s] returned error 404: storage not found", node->toString().c_str());
+ if (auto sr = ws.lock()) {
+ if (auto sn = sr->getNode(node)) {
+ sn->acked[v->id] = {
+ network_engine.sendAnnounceValue(sn->node, sr->id, v, created, sn->token, onDone, onExpired),
+ next_refresh_time
+ };
+ scheduler.edit(sr->nextSearchStep, scheduler.time());
+ return true;
+ }
+ }
+ }
+ return false;
+ }, onExpired),
+ next_refresh_time
+ };
+ } else {
+ if (logger_)
+ logger_->w(sr->id, sn->node->id, "[search %s] [node %s] already has value (vid: %016" PRIx64 "). Aborting.",
+ sr->id.toString().c_str(), sn->node->toString().c_str(), a.value->id);
+ auto ack_req = std::make_shared<net::Request>(net::Request::State::COMPLETED);
+ ack_req->reply_time = now;
+ acked = {std::move(ack_req), next_refresh_time};
+
+ /* step to clear announces */
+ scheduler.edit(sr->nextSearchStep, now);
+ }
+ if (a.permanent) {
+ acked.refresh = scheduler.add(next_refresh_time - REANNOUNCE_MARGIN, std::bind(&Dht::searchStep, this, ws));
+ }
+ }
+ };
+
+ static const auto PROBE_QUERY = std::make_shared<Query>(Select {}.field(Value::Field::Id).field(Value::Field::SeqNum));
+
+ const auto& now = scheduler.time();
+ unsigned i = 0;
+ for (auto& np : sr->nodes) {
+ auto& n = *np;
+ if (not n.isSynced(now))
+ continue;
+
+ auto gs = n.probe_query ? n.getStatus.find(n.probe_query) : n.getStatus.end();
+ if (gs != n.getStatus.end() and gs->second and gs->second->pending()) {
+ continue;
+ }
+
+ bool sendQuery = false;
+ for (auto& a : sr->announce) {
+ if (n.getAnnounceTime(a.value->id) <= now) {
+ if (a.permanent) {
+ sendQuery = true;
+ } else {
+ if (logger_)
+ logger_->w(sr->id, n.node->id, "[search %s] [node %s] sending 'put' (vid: %016" PRIx64 ")",
+ sr->id.toString().c_str(), n.node->toString().c_str(), a.value->id);
+ n.acked[a.value->id] = {
+ network_engine.sendAnnounceValue(n.node, sr->id, a.value, a.created, n.token, onDone, onExpired),
+ now + getType(a.value->type).expiration
+ };
+ }
+ }
+ }
+
+ if (sendQuery) {
+ n.probe_query = PROBE_QUERY;
+ if (logger_)
+ logger_->d(sr->id, n.node->id, "[search %s] [node %s] sending %s",
+ sr->id.toString().c_str(), n.node->toString().c_str(), n.probe_query->toString().c_str());
+ auto req = network_engine.sendGetValues(n.node,
+ sr->id,
+ *PROBE_QUERY,
+ -1,
+ onSelectDone,
+ std::bind(&Dht::searchNodeGetExpired, this, _1, _2, ws, PROBE_QUERY));
+ n.getStatus[PROBE_QUERY] = std::move(req);
+ }
+ if (not n.candidate and ++i == syncLevel)
+ break;
+ }
+}
+
+void
+Dht::searchSynchedNodeListen(const Sp<Search>& sr, SearchNode& n)
+{
+ const auto& listenExp = getListenExpiration();
+ std::weak_ptr<Search> ws = sr;
+ for (const auto& l : sr->listeners) {
+ const auto& query = l.second.query;
+
+ auto r = n.listenStatus.find(query);
+ if (n.getListenTime(r, listenExp) > scheduler.time())
+ continue;
+ // if (logger_)
+ // logger_->d(sr->id, n.node->id, "[search %s] [node %s] sending 'listen'",
+ // sr->id.toString().c_str(), n.node->toString().c_str());
+
+ if (r == n.listenStatus.end()) {
+ r = n.listenStatus.emplace(std::piecewise_construct,
+ std::forward_as_tuple(query),
+ std::forward_as_tuple(
+ l.second.get_cb,
+ l.second.sync_cb,
+ n.node->openSocket([this,ws,query](const Sp<Node>& node, net::RequestAnswer&& answer) mutable {
+ /* on new values */
+ if (auto sr = ws.lock()) {
+ scheduler.edit(sr->nextSearchStep, scheduler.time());
+ sr->insertNode(node, scheduler.time(), answer.ntoken);
+ if (auto sn = sr->getNode(node)) {
+ sn->onValues(query, std::move(answer), types, scheduler);
+ }
+ }
+ }))).first;
+ r->second.cacheExpirationJob = scheduler.add(time_point::max(), [this,ws,query,node=n.node]{
+ if (auto sr = ws.lock()) {
+ if (auto sn = sr->getNode(node)) {
+ sn->expireValues(query, scheduler);
+ }
+ }
+ });
+ }
+ auto new_req = network_engine.sendListen(n.node, sr->id, *query, n.token, r->second.socketId,
+ [this,ws,query](const net::Request& req, net::RequestAnswer&& answer) mutable
+ { /* on done */
+ if (auto sr = ws.lock()) {
+ scheduler.edit(sr->nextSearchStep, scheduler.time());
+ if (auto sn = sr->getNode(req.node)) {
+ auto job = scheduler.add(sn->getListenTime(query, getListenExpiration()), std::bind(&Dht::searchStep, this, ws));
+ sn->onListenSynced(query, true, std::move(job));
+ }
+ onListenDone(req.node, answer, sr);
+ }
+ },
+ [this,ws,query](const net::Request& req, bool over) mutable
+ { /* on request expired */
+ if (auto sr = ws.lock()) {
+ scheduler.edit(sr->nextSearchStep, scheduler.time());
+ if (over)
+ if (auto sn = sr->getNode(req.node))
+ sn->listenStatus.erase(query);
+ }
+ }
+ );
+ // Here the request may have failed and the CachedListenStatus removed
+ r = n.listenStatus.find(query);
+ if (r != n.listenStatus.end()) {
+ r->second.req = new_req;
+ }
+ }
+}
+
+/* When a search is in progress, we periodically call search_step to send
+ further requests. */
+void
+Dht::searchStep(std::weak_ptr<Search> ws)
+{
+ auto sr = ws.lock();
+ if (not sr or sr->expired or sr->done) return;
+
+ const auto& now = scheduler.time();
+ auto level = sr->syncLevel(now);
+ constexpr auto MARGIN = 1;
+ bool preSynced = level > MARGIN;
+ auto syncLevel = preSynced ? level - MARGIN : 0;
+ /*if (auto req_count = sr->currentlySolicitedNodeCount())
+ if (logger_)
+ logger_->d(sr->id, "[search %s IPv%c] step (%d requests)",
+ sr->id.toString().c_str(), sr->af == AF_INET ? '4' : '6', req_count);*/
+ sr->step_time = now;
+
+ if (sr->refill_time + Node::NODE_EXPIRE_TIME < now and sr->nodes.size()-sr->getNumberOfBadNodes() < SEARCH_NODES)
+ refill(*sr);
+
+ /* Check if the first TARGET_NODES (8) live nodes have replied. */
+ if (sr->isSynced(now)) {
+ if (not (sr->callbacks.empty() and sr->announce.empty())) {
+ // search is synced but some (newer) get operations are not complete
+ // Call callbacks when done
+ std::vector<Get> completed_gets;
+ for (auto b = sr->callbacks.begin(); b != sr->callbacks.end();) {
+ if (sr->isDone(b->second)) {
+ sr->setDone(b->second);
+ completed_gets.emplace_back(std::move(b->second));
+ b = sr->callbacks.erase(b);
+ }
+ else
+ ++b;
+ }
+ // clear corresponding queries
+ for (const auto& get : completed_gets)
+ for (auto& sn : sr->nodes) {
+ sn->getStatus.erase(get.query);
+ sn->pagination_queries.erase(get.query);
+ }
+
+ /* clearing callbacks for announced values */
+ sr->checkAnnounced();
+
+ if (sr->callbacks.empty() && sr->announce.empty() && sr->listeners.empty())
+ sr->setDone();
+ }
+
+ // true if this node is part of the target nodes cluter.
+ /*bool in = sr->id.xorCmp(myid, sr->nodes.back().node->id) < 0;
+
+ logger__DBG("[search %s IPv%c] synced%s",
+ sr->id.toString().c_str(), sr->af == AF_INET ? '4' : '6', in ? ", in" : "");*/
+
+ if (not sr->listeners.empty()) {
+ unsigned i = 0;
+ for (auto& n : sr->nodes) {
+ if (not n->isSynced(now))
+ continue;
+ searchSynchedNodeListen(sr, *n);
+ if (not n->candidate and ++i == LISTEN_NODES)
+ break;
+ }
+ }
+
+ // Announce requests
+ searchSendAnnounceValue(sr);
+
+ if (sr->callbacks.empty() && sr->announce.empty() && sr->listeners.empty())
+ sr->setDone();
+ } else if (preSynced) {
+ if (not sr->listeners.empty()) {
+ auto nl = std::min(syncLevel, LISTEN_NODES);
+ unsigned i = 0;
+ for (auto& n : sr->nodes) {
+ if (not n->isSynced(now))
+ break;
+ searchSynchedNodeListen(sr, *n);
+ if (++i == nl)
+ break;
+ }
+ }
+
+ // Announce requests
+ searchSendAnnounceValue(sr, syncLevel);
+ }
+
+ while (sr->currentlySolicitedNodeCount() < MAX_REQUESTED_SEARCH_NODES and searchSendGetValues(sr));
+
+
+ if (sr->getNumberOfConsecutiveBadNodes() >= std::min<size_t>(sr->nodes.size(), SEARCH_MAX_BAD_NODES))
+ {
+ if (logger_)
+ logger_->w(sr->id, "[search %s IPv%c] expired", sr->id.toString().c_str(), sr->af == AF_INET ? '4' : '6');
+ sr->expire();
+ if (not public_stable)
+ connectivityChanged(sr->af);
+ }
+
+ /* dumpSearch(*sr, std::cout); */
+}
+
+unsigned Dht::refill(Dht::Search& sr) {
+ const auto& now = scheduler.time();
+ sr.refill_time = now;
+ /* we search for up to SEARCH_NODES good nodes. */
+ auto cached_nodes = network_engine.getCachedNodes(sr.id, sr.af, SEARCH_NODES);
+
+ if (cached_nodes.empty()) {
+ if (logger_)
+ logger_->e(sr.id, "[search %s IPv%c] no nodes from cache while refilling search",
+ sr.id.toString().c_str(), (sr.af == AF_INET) ? '4' : '6');
+ return 0;
+ }
+
+ unsigned inserted = 0;
+ for (auto& i : cached_nodes) {
+ /* try to insert the nodes. Search::insertNode will know how many to insert. */
+ if (sr.insertNode(i, now))
+ ++inserted;
+ }
+ if (logger_)
+ logger_->d(sr.id, "[search %s IPv%c] refilled search with %u nodes from node cache",
+ sr.id.toString().c_str(), (sr.af == AF_INET) ? '4' : '6', inserted);
+ return inserted;
+}
+
+
+/* Start a search. */
+Sp<Dht::Search>
+Dht::search(const InfoHash& id, sa_family_t af, GetCallback gcb, QueryCallback qcb, DoneCallback dcb, Value::Filter f, const Sp<Query>& q)
+{
+ if (!isRunning(af)) {
+ if (logger_)
+ logger_->e(id, "[search %s IPv%c] unsupported protocol", id.toString().c_str(), (af == AF_INET) ? '4' : '6');
+ if (dcb)
+ dcb(false, {});
+ return {};
+ }
+
+ auto& srs = searches(af);
+ const auto& srp = srs.find(id);
+ Sp<Search> sr {};
+
+ if (srp != srs.end()) {
+ sr = srp->second;
+ sr->done = false;
+ sr->expired = false;
+ } else {
+ if (srs.size() < max_searches) {
+ sr = std::make_shared<Search>();
+ srs.emplace(id, sr);
+ } else {
+ for (auto it = srs.begin(); it!=srs.end();) {
+ auto& s = *it->second;
+ if ((s.done or s.expired) and s.announce.empty() and s.listeners.empty()) {
+ sr = it->second;
+ break;
+ }
+ }
+ if (not sr) {
+ if (logger_)
+ logger_->e(id, "[search %s IPv%c] maximum number of searches reached !", id.toString().c_str(), (af == AF_INET) ? '4' : '6');
+ return {};
+ }
+ }
+ sr->af = af;
+ sr->tid = search_id++;
+ sr->step_time = time_point::min();
+ sr->id = id;
+ sr->done = false;
+ sr->expired = false;
+ sr->nodes.clear();
+ sr->nodes.reserve(SEARCH_NODES+1);
+ sr->nextSearchStep = scheduler.add(time_point::max(), std::bind(&Dht::searchStep, this, std::weak_ptr<Search>(sr)));
+ if (logger_)
+ logger_->w(id, "[search %s IPv%c] new search", id.toString().c_str(), (af == AF_INET) ? '4' : '6');
+ if (search_id == 0)
+ search_id++;
+ }
+
+ sr->get(f, q, qcb, gcb, dcb, scheduler);
+ refill(*sr);
+
+ return sr;
+}
+
+void
+Dht::announce(const InfoHash& id,
+ sa_family_t af,
+ Sp<Value> value,
+ DoneCallback callback,
+ time_point created,
+ bool permanent)
+{
+ auto& srs = searches(af);
+ auto srp = srs.find(id);
+ if (auto sr = srp == srs.end() ? search(id, af) : srp->second) {
+ sr->put(value, callback, created, permanent);
+ scheduler.edit(sr->nextSearchStep, scheduler.time());
+ } else if (callback) {
+ callback(false, {});
+ }
+}
+
+size_t
+Dht::listenTo(const InfoHash& id, sa_family_t af, ValueCallback cb, Value::Filter f, const Sp<Query>& q)
+{
+ if (!isRunning(af))
+ return 0;
+ // logger__ERR("[search %s IPv%c] search_time is now in %lfs", sr->id.toString().c_str(), (sr->af == AF_INET) ? '4' : '6', print_dt(tm-clock::now()));
+
+ //logger__WARN("listenTo %s", id.toString().c_str());
+ auto& srs = searches(af);
+ auto srp = srs.find(id);
+ Sp<Search> sr = (srp == srs.end()) ? search(id, af) : srp->second;
+ if (!sr)
+ throw DhtException("Can't create search");
+ if (logger_)
+ logger_->w(id, "[search %s IPv%c] listen", id.to_c_str(), (af == AF_INET) ? '4' : '6');
+ return sr->listen(cb, std::move(f), q, scheduler);
+}
+
+size_t
+Dht::listen(const InfoHash& id, ValueCallback cb, Value::Filter f, Where where)
+{
+ if (not id) {
+ if (logger_)
+ logger_->w(id, "Listen called with invalid key");
+ return 0;
+ }
+ scheduler.syncTime();
+
+ auto token = ++listener_token;
+ auto gcb = OpValueCache::cacheCallback(std::move(cb), [this, id, token]{
+ cancelListen(id, token);
+ });
+
+ auto query = std::make_shared<Query>(Select{}, std::move(where));
+ auto filter = Value::Filter::chain(std::move(f), query->where.getFilter());
+ auto st = store.find(id);
+ if (st == store.end() && store.size() < max_store_keys)
+ st = store.emplace(id, scheduler.time() + MAX_STORAGE_MAINTENANCE_EXPIRE_TIME).first;
+
+ size_t tokenlocal = 0;
+ if (st != store.end()) {
+ tokenlocal = st->second.listen(gcb, filter, query);
+ if (tokenlocal == 0)
+ return 0;
+ }
+
+ auto token4 = Dht::listenTo(id, AF_INET, gcb, filter, query);
+ auto token6 = token4 == 0 ? 0 : Dht::listenTo(id, AF_INET6, gcb, filter, query);
+ if (token6 == 0 && st != store.end()) {
+ st->second.cancelListen(tokenlocal);
+ return 0;
+ }
+
+ listeners.emplace(token, std::make_tuple(tokenlocal, token4, token6));
+ return token;
+}
+
+bool
+Dht::cancelListen(const InfoHash& id, size_t token)
+{
+ scheduler.syncTime();
+
+ auto it = listeners.find(token);
+ if (it == listeners.end()) {
+ if (logger_)
+ logger_->w(id, "Listen token not found: %d", token);
+ return false;
+ }
+ if (logger_)
+ logger_->d(id, "cancelListen %s with token %d", id.toString().c_str(), token);
+ if (auto tokenlocal = std::get<0>(it->second)) {
+ auto st = store.find(id);
+ if (st != store.end())
+ st->second.cancelListen(tokenlocal);
+ }
+ auto searches_cancel_listen = [this,&id](std::map<InfoHash, Sp<Search>>& srs, size_t token) {
+ if (token) {
+ auto srp = srs.find(id);
+ if (srp != srs.end())
+ srp->second->cancelListen(token, scheduler);
+ }
+ };
+ searches_cancel_listen(dht4.searches, std::get<1>(it->second));
+ searches_cancel_listen(dht6.searches, std::get<2>(it->second));
+ listeners.erase(it);
+ return true;
+}
+
+struct OpStatus {
+ struct Status {
+ bool done {false};
+ bool ok {false};
+ Status(bool done=false, bool ok=false) : done(done), ok(ok) {}
+ };
+ Status status;
+ Status status4;
+ Status status6;
+};
+
+template <typename T>
+struct GetStatus : public OpStatus {
+ T values;
+ std::vector<Sp<Node>> nodes;
+};
+
+void
+Dht::put(const InfoHash& id, Sp<Value> val, DoneCallback callback, time_point created, bool permanent)
+{
+ if (not id or not val) {
+ if (logger_)
+ logger_->w(id, "Put called with invalid key or value");
+ if (callback)
+ callback(false, {});
+ return;
+ }
+ if (val->id == Value::INVALID_ID)
+ val->id = std::uniform_int_distribution<Value::Id>{1}(rd);
+ scheduler.syncTime();
+ const auto& now = scheduler.time();
+ created = std::min(now, created);
+ storageStore(id, val, created, {}, permanent);
+
+ if (logger_)
+ logger_->d(id, "put: adding %s -> %s", id.toString().c_str(), val->toString().c_str());
+
+ auto op = std::make_shared<OpStatus>();
+ auto donecb = [callback](const std::vector<Sp<Node>>& nodes, OpStatus& op) {
+ // Callback as soon as the value is announced on one of the available networks
+ if (callback and not op.status.done and (op.status4.done && op.status6.done)) {
+ callback(op.status4.ok or op.status6.ok, nodes);
+ op.status.done = true;
+ }
+ };
+ announce(id, AF_INET, val, [=](bool ok4, const std::vector<Sp<Node>>& nodes) {
+ if (logger_)
+ logger_->d(id, "Announce done IPv4 %d", ok4);
+ auto& o = *op;
+ o.status4 = {true, ok4};
+ donecb(nodes, o);
+ }, created, permanent);
+ announce(id, AF_INET6, val, [=](bool ok6, const std::vector<Sp<Node>>& nodes) {
+ if (logger_)
+ logger_->d(id, "Announce done IPv6 %d", ok6);
+ auto& o = *op;
+ o.status6 = {true, ok6};
+ donecb(nodes, o);
+ }, created, permanent);
+}
+
+template <typename T>
+void doneCallbackWrapper(DoneCallback dcb, const std::vector<Sp<Node>>& nodes, GetStatus<T>& op) {
+ if (op.status.done)
+ return;
+ op.nodes.insert(op.nodes.end(), nodes.begin(), nodes.end());
+ if (op.status.ok or (op.status4.done and op.status6.done)) {
+ bool ok = op.status.ok or op.status4.ok or op.status6.ok;
+ op.status.done = true;
+ if (dcb)
+ dcb(ok, op.nodes);
+ }
+}
+
+template <typename T, typename St, typename Cb, typename Av, typename Cv>
+bool callbackWrapper(Cb get_cb, DoneCallback done_cb, const std::vector<Sp<T>>& values,
+ Av add_values, Cv cache_values, GetStatus<St>& op)
+{
+ if (op.status.done)
+ return false;
+ auto newvals = add_values(values);
+ if (not newvals.empty()) {
+ op.status.ok = !get_cb(newvals);
+ cache_values(newvals);
+ }
+ doneCallbackWrapper(done_cb, {}, op);
+ return !op.status.ok;
+}
+
+void
+Dht::get(const InfoHash& id, GetCallback getcb, DoneCallback donecb, Value::Filter&& filter, Where&& where)
+{
+ if (not id) {
+ if (logger_)
+ logger_->w(id, "Get called with invalid key");
+ if (donecb)
+ donecb(false, {});
+ return;
+ }
+ scheduler.syncTime();
+
+ auto op = std::make_shared<GetStatus<std::map<Value::Id, Sp<Value>>>>();
+ auto gcb = [getcb, donecb, op](const std::vector<Sp<Value>>& vals) {
+ auto& o = *op;
+ return callbackWrapper(getcb, donecb, vals, [&o](const std::vector<Sp<Value>>& values) {
+ std::vector<Sp<Value>> newvals {};
+ for (const auto& v : values) {
+ auto it = o.values.find(v->id);
+ if (it == o.values.cend() or (it->second != v && !(*it->second == *v))) {
+ newvals.push_back(v);
+ }
+ }
+ return newvals;
+ }, [&o](const std::vector<Sp<Value>>& newvals) {
+ for (const auto& v : newvals)
+ o.values[v->id] = v;
+ }, o);
+ };
+
+ auto q = std::make_shared<Query>(Select {}, std::move(where));
+ auto f = filter.chain(q->where.getFilter());
+
+ /* Try to answer this search locally. */
+ gcb(getLocal(id, f));
+
+ Dht::search(id, AF_INET, gcb, {}, [=](bool ok, const std::vector<Sp<Node>>& nodes) {
+ //logger__WARN("DHT done IPv4");
+ op->status4 = {true, ok};
+ doneCallbackWrapper(donecb, nodes, *op);
+ }, f, q);
+ Dht::search(id, AF_INET6, gcb, {}, [=](bool ok, const std::vector<Sp<Node>>& nodes) {
+ //logger__WARN("DHT done IPv6");
+ op->status6 = {true, ok};
+ doneCallbackWrapper(donecb, nodes, *op);
+ }, f, q);
+}
+
+void Dht::query(const InfoHash& id, QueryCallback cb, DoneCallback done_cb, Query&& q)
+{
+ if (not id) {
+ if (logger_)
+ logger_->w(id, "Query called with invalid key");
+ if (done_cb)
+ done_cb(false, {});
+ return;
+ }
+ scheduler.syncTime();
+ auto op = std::make_shared<GetStatus<std::vector<Sp<FieldValueIndex>>>>();
+ auto f = q.where.getFilter();
+ auto qcb = [cb, done_cb, op](const std::vector<Sp<FieldValueIndex>>& fields){
+ auto& o = *op;
+ return callbackWrapper(cb, done_cb, fields, [&](const std::vector<Sp<FieldValueIndex>>& fields) {
+ std::vector<Sp<FieldValueIndex>> newvals {};
+ for (const auto& f : fields) {
+ auto it = std::find_if(o.values.cbegin(), o.values.cend(),
+ [&](const Sp<FieldValueIndex>& sf) {
+ return sf == f or f->containedIn(*sf);
+ });
+ if (it == o.values.cend()) {
+ auto lesser = std::find_if(o.values.begin(), o.values.end(),
+ [&](const Sp<FieldValueIndex>& sf) {
+ return sf->containedIn(*f);
+ });
+ if (lesser != o.values.end())
+ o.values.erase(lesser);
+ newvals.push_back(f);
+ }
+ }
+ return newvals;
+ }, [&](const std::vector<Sp<FieldValueIndex>>& fields){
+ o.values.insert(o.values.end(), fields.begin(), fields.end());
+ }, o);
+ };
+
+ /* Try to answer this search locally. */
+ auto values = getLocal(id, f);
+ std::vector<Sp<FieldValueIndex>> local_fields(values.size());
+ std::transform(values.begin(), values.end(), local_fields.begin(), [&q](const Sp<Value>& v) {
+ return std::make_shared<FieldValueIndex>(*v, q.select);
+ });
+ qcb(local_fields);
+
+ auto sq = std::make_shared<Query>(std::move(q));
+ Dht::search(id, AF_INET, {}, qcb, [=](bool ok, const std::vector<Sp<Node>>& nodes) {
+ //logger__WARN("DHT done IPv4");
+ op->status4 = {true, ok};
+ doneCallbackWrapper(done_cb, nodes, *op);
+ }, f, sq);
+ Dht::search(id, AF_INET6, {}, qcb, [=](bool ok, const std::vector<Sp<Node>>& nodes) {
+ //logger__WARN("DHT done IPv6");
+ op->status6 = {true, ok};
+ doneCallbackWrapper(done_cb, nodes, *op);
+ }, f, sq);
+}
+
+std::vector<Sp<Value>>
+Dht::getLocal(const InfoHash& id, const Value::Filter& f) const
+{
+ auto s = store.find(id);
+ if (s == store.end()) return {};
+ return s->second.get(f);
+}
+
+Sp<Value>
+Dht::getLocalById(const InfoHash& id, Value::Id vid) const
+{
+ auto s = store.find(id);
+ if (s != store.end())
+ return s->second.getById(vid);
+ return {};
+}
+
+std::vector<Sp<Value>>
+Dht::getPut(const InfoHash& id) const
+{
+ std::vector<Sp<Value>> ret;
+ auto find_values = [&](const std::map<InfoHash, Sp<Search>>& srs) {
+ auto srp = srs.find(id);
+ if (srp == srs.end()) return;
+ auto vals = srp->second->getPut();
+ ret.insert(ret.end(), vals.begin(), vals.end());
+ };
+ find_values(dht4.searches);
+ find_values(dht6.searches);
+ return ret;
+}
+
+Sp<Value>
+Dht::getPut(const InfoHash& id, const Value::Id& vid) const
+{
+ auto find_value = [&](const std::map<InfoHash, Sp<Search>>& srs) {
+ auto srp = srs.find(id);
+ return (srp != srs.end()) ? srp->second->getPut(vid) : Sp<Value> {};
+ };
+ if (auto v4 = find_value(dht4.searches))
+ return v4;
+ if (auto v6 = find_value(dht6.searches))
+ return v6;
+ return {};
+}
+
+bool
+Dht::cancelPut(const InfoHash& id, const Value::Id& vid)
+{
+ bool canceled {false};
+ auto sr_cancel_put = [&](std::map<InfoHash, Sp<Search>>& srs) {
+ auto srp = srs.find(id);
+ return (srp != srs.end()) ? srp->second->cancelPut(vid) : false;
+ };
+ canceled |= sr_cancel_put(dht4.searches);
+ canceled |= sr_cancel_put(dht6.searches);
+ if (canceled) {
+ auto st = store.find(id);
+ if (st != store.end()) {
+ if (auto value = st->second.remove(id, vid))
+ storageRemoved(id, st->second, {value}, value->size());
+ }
+ }
+ return canceled;
+}
+
+// Storage
+
+void
+Dht::storageChanged(const InfoHash& id, Storage& st, const Sp<Value>& v, bool newValue)
+{
+ if (newValue) {
+ if (not st.local_listeners.empty()) {
+ if (logger_)
+ logger_->d(id, "[store %s] %lu local listeners", id.toString().c_str(), st.local_listeners.size());
+ std::vector<std::pair<ValueCallback, std::vector<Sp<Value>>>> cbs;
+ cbs.reserve(st.local_listeners.size());
+ for (const auto& l : st.local_listeners) {
+ std::vector<Sp<Value>> vals;
+ if (not l.second.filter or l.second.filter(*v))
+ vals.push_back(v);
+ if (not vals.empty()) {
+ if (logger_)
+ logger_->d(id, "[store %s] sending update local listener with token %lu",
+ id.toString().c_str(),
+ l.first);
+ cbs.emplace_back(l.second.get_cb, std::move(vals));
+ }
+ }
+ // listeners are copied: they may be deleted by the callback
+ for (auto& cb : cbs)
+ cb.first(cb.second, false);
+ }
+ }
+
+ if (not st.listeners.empty()) {
+ if (logger_)
+ logger_->d(id, "[store %s] %lu remote listeners", id.toString().c_str(), st.listeners.size());
+ for (const auto& node_listeners : st.listeners) {
+ for (const auto& l : node_listeners.second) {
+ auto f = l.second.query.where.getFilter();
+ if (f and not f(*v))
+ continue;
+ if (logger_)
+ logger_->w(id, node_listeners.first->id, "[store %s] [node %s] sending update",
+ id.toString().c_str(),
+ node_listeners.first->toString().c_str());
+ std::vector<Sp<Value>> vals {};
+ vals.push_back(v);
+ Blob ntoken = makeToken(node_listeners.first->getAddr(), false);
+ network_engine.tellListener(node_listeners.first, l.first, id, 0, ntoken, {}, {},
+ std::move(vals), l.second.query, l.second.version);
+ }
+ }
+ }
+}
+
+bool
+Dht::storageStore(const InfoHash& id, const Sp<Value>& value, time_point created, const SockAddr& sa, bool permanent)
+{
+ const auto& now = scheduler.time();
+ created = std::min(created, now);
+ auto expiration = permanent ? time_point::max() : created + getType(value->type).expiration;
+ if (expiration < now)
+ return false;
+
+ auto st = store.find(id);
+ if (st == store.end()) {
+ if (store.size() >= max_store_keys)
+ return false;
+ auto st_i = store.emplace(id, now);
+ st = st_i.first;
+ if (maintain_storage and st_i.second)
+ scheduler.add(st->second.maintenance_time, std::bind(&Dht::dataPersistence, this, id));
+ }
+
+ StorageBucket* store_bucket {nullptr};
+ if (sa)
+ store_bucket = &store_quota[sa];
+
+ auto store = st->second.store(id, value, created, expiration, store_bucket);
+ if (auto vs = store.first) {
+ total_store_size += store.second.size_diff;
+ total_values += store.second.values_diff;
+ scheduler.cancel(vs->expiration_job);
+ if (not permanent) {
+ vs->expiration_job = scheduler.add(expiration, std::bind(&Dht::expireStorage, this, id));
+ }
+ if (total_store_size > max_store_size) {
+ auto value = vs->data;
+ auto value_diff = store.second.values_diff;
+ auto value_edit = store.second.edited_values;
+ expireStore();
+ storageChanged(id, st->second, value, value_diff > 0 || value_edit > 0);
+ } else {
+ storageChanged(id, st->second, vs->data, store.second.values_diff > 0 || store.second.edited_values > 0);
+ }
+ }
+
+ return std::get<0>(store);
+}
+
+void
+Dht::storageAddListener(const InfoHash& id, const Sp<Node>& node, size_t socket_id, Query&& query, int version)
+{
+ const auto& now = scheduler.time();
+ auto st = store.find(id);
+ if (st == store.end()) {
+ if (store.size() >= max_store_keys)
+ return;
+ st = store.emplace(id, now).first;
+ }
+ auto& node_listeners = st->second.listeners[node];
+ auto l = node_listeners.find(socket_id);
+ if (l == node_listeners.end()) {
+ auto vals = st->second.get(query.where.getFilter());
+ if (not vals.empty()) {
+ network_engine.tellListener(node, socket_id, id, WANT4 | WANT6, makeToken(node->getAddr(), false),
+ dht4.buckets.findClosestNodes(id, now, TARGET_NODES), dht6.buckets.findClosestNodes(id, now, TARGET_NODES),
+ std::move(vals), query, version);
+ }
+ node_listeners.emplace(socket_id, Listener {now, std::forward<Query>(query), version});
+ }
+ else
+ l->second.refresh(now, std::forward<Query>(query));
+}
+
+void
+Dht::expireStore(decltype(store)::iterator i)
+{
+ const auto& id = i->first;
+ auto& st = i->second;
+ auto stats = st.expire(id, scheduler.time());
+ if (not stats.second.empty()) {
+ storageRemoved(id, st, stats.second, -stats.first);
+ }
+}
+
+void
+Dht::expireStorage(InfoHash h)
+{
+ auto i = store.find(h);
+ if (i != store.end())
+ expireStore(i);
+}
+
+void
+Dht::storageRemoved(const InfoHash& id, Storage& st, const std::vector<Sp<Value>>& values, size_t totalSize)
+{
+ if (logger_)
+ logger_->d(id, "[store %s] discarded %ld values (%ld bytes)",
+ id.toString().c_str(), values.size(), totalSize);
+
+ total_store_size -= totalSize;
+ total_values -= values.size();
+
+ if (not st.listeners.empty()) {
+ if (logger_)
+ logger_->d(id, "[store %s] %lu remote listeners", id.toString().c_str(), st.listeners.size());
+
+ std::vector<Value::Id> ids;
+ ids.reserve(values.size());
+ for (const auto& v : values)
+ ids.emplace_back(v->id);
+
+ for (const auto& node_listeners : st.listeners) {
+ for (const auto& l : node_listeners.second) {
+ if (logger_)
+ logger_->w(id, node_listeners.first->id, "[store %s] [node %s] sending expired",
+ id.toString().c_str(),
+ node_listeners.first->toString().c_str());
+ Blob ntoken = makeToken(node_listeners.first->getAddr(), false);
+ network_engine.tellListenerExpired(node_listeners.first, l.first, id, ntoken, ids, l.second.version);
+ }
+ }
+ }
+ for (const auto& local_listeners : st.local_listeners) {
+ local_listeners.second.get_cb(values, true);
+ }
+}
+
+void
+Dht::expireStore()
+{
+ // removing expired values
+ for (auto i = store.begin(); i != store.end();) {
+ expireStore(i);
+
+ if (i->second.empty() && i->second.listeners.empty() && i->second.local_listeners.empty()) {
+ if (logger_)
+ logger_->d(i->first, "[store %s] discarding empty storage", i->first.toString().c_str());
+ i = store.erase(i);
+ }
+ else
+ ++i;
+ }
+
+ // remove more values if storage limit is exceeded
+ while (total_store_size > max_store_size) {
+ // find IP using the most storage
+ if (store_quota.empty()) {
+ if (logger_)
+ logger_->w("No space left: local data consumes all the quota!");
+ break;
+ }
+ auto largest = store_quota.begin();
+ for (auto it = std::next(largest); it != store_quota.end(); ++it) {
+ if (it->second.size() > largest->second.size())
+ largest = it;
+ }
+ if (largest != store_quota.end()) {
+ while (true) {
+ auto exp_value = largest->second.getOldest();
+ auto storage = store.find(exp_value.first);
+ if (storage != store.end()) {
+ if (logger_)
+ logger_->w("Storage quota full: discarding value from %s at %s %016" PRIx64, largest->first.toString().c_str(), exp_value.first.to_c_str(), exp_value.second);
+
+ if (auto value = storage->second.remove(exp_value.first, exp_value.second)) {
+ storageRemoved(storage->first, storage->second, {value}, value->size());
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ // remove unused quota entires
+ for (auto i = store_quota.begin(); i != store_quota.end();) {
+ if (i->second.size() == 0)
+ i = store_quota.erase(i);
+ else
+ ++i;
+ }
+}
+
+void
+Dht::connectivityChanged(sa_family_t af)
+{
+ const auto& now = scheduler.time();
+ scheduler.edit(nextNodesConfirmation, now);
+ auto& dht = this->dht(af);
+ dht.buckets.connectivityChanged(now);
+ dht.reported_addr.clear();
+ network_engine.connectivityChanged(af);
+ startBootstrap(); // will only happen if disconnected
+}
+
+void
+Dht::rotateSecrets()
+{
+ oldsecret = secret;
+ secret = std::uniform_int_distribution<uint64_t>{}(rd);
+ uniform_duration_distribution<> time_dist(std::chrono::minutes(15), std::chrono::minutes(45));
+ auto rotate_secrets_time = scheduler.time() + time_dist(rd);
+ scheduler.add(rotate_secrets_time, std::bind(&Dht::rotateSecrets, this));
+}
+
+Blob
+Dht::makeToken(const SockAddr& addr, bool old) const
+{
+ const void *ip;
+ size_t iplen;
+ in_port_t port;
+
+ auto family = addr.getFamily();
+ if (family == AF_INET) {
+ const auto& sin = addr.getIPv4();
+ ip = &sin.sin_addr;
+ iplen = 4;
+ port = sin.sin_port;
+ } else if (family == AF_INET6) {
+ const auto& sin6 = addr.getIPv6();
+ ip = &sin6.sin6_addr;
+ iplen = 16;
+ port = sin6.sin6_port;
+ } else {
+ return {};
+ }
+
+ const auto& c1 = old ? oldsecret : secret;
+ Blob data;
+ data.reserve(sizeof(secret)+sizeof(in_port_t)+iplen);
+ data.insert(data.end(), (uint8_t*)&c1, ((uint8_t*)&c1) + sizeof(c1));
+ data.insert(data.end(), (uint8_t*)ip, (uint8_t*)ip+iplen);
+ data.insert(data.end(), (uint8_t*)&port, ((uint8_t*)&port)+sizeof(in_port_t));
+ return crypto::hash(data, TOKEN_SIZE);
+}
+
+bool
+Dht::tokenMatch(const Blob& token, const SockAddr& addr) const
+{
+ if (not addr or token.size() != TOKEN_SIZE)
+ return false;
+ if (token == makeToken(addr, false))
+ return true;
+ if (token == makeToken(addr, true))
+ return true;
+ return false;
+}
+
+NodeStats
+Dht::getNodesStats(sa_family_t af) const
+{
+ NodeStats stats = dht(af).getNodesStats(scheduler.time(), myid);
+ stats.node_cache_size = network_engine.getNodeCacheSize(af);
+ return stats;
+}
+
+NodeStats
+Dht::Kad::getNodesStats(time_point now, const InfoHash& myid) const
+{
+ NodeStats stats {};
+ for (const auto& b : buckets) {
+ for (auto& n : b.nodes) {
+ if (n->isGood(now)) {
+ stats.good_nodes++;
+ if (n->isIncoming())
+ stats.incoming_nodes++;
+ } else if (not n->isExpired())
+ stats.dubious_nodes++;
+ }
+ if (b.cached)
+ stats.cached_nodes++;
+ }
+ stats.table_depth = buckets.depth(buckets.findBucket(myid));
+ stats.searches = searches.size();
+ return stats;
+}
+
+void
+Dht::dumpBucket(const Bucket& b, std::ostream& out) const
+{
+ const auto& now = scheduler.time();
+ using namespace std::chrono;
+ out << b.first << " count: " << b.nodes.size() << " updated: " << print_time_relative(now, b.time);
+ if (b.cached)
+ out << " (cached)";
+ out << std::endl;
+ for (auto& n : b.nodes) {
+ out << " Node " << n->toString();
+ const auto& t = n->getTime();
+ const auto& r = n->getReplyTime();
+ if (t != r)
+ out << " updated: " << print_time_relative(now, t) << ", replied: " << print_time_relative(now, r);
+ else
+ out << " updated: " << print_time_relative(now, t);
+ if (n->isExpired())
+ out << " [expired]";
+ else if (n->isGood(now))
+ out << " [good]";
+ out << std::endl;
+ }
+}
+
+void
+Dht::dumpSearch(const Search& sr, std::ostream& out) const
+{
+ const auto& now = scheduler.time();
+ const auto& listen_expire = getListenExpiration();
+ using namespace std::chrono;
+ fmt::print(out, "Search IPv{} {} gets: {} last step: {}{}{}{}{}\n",
+ (sr.af == AF_INET6 ? '6' : '4'),
+ sr.id,
+ sr.callbacks.size(),
+ print_time_relative(now, sr.step_time),
+ (sr.done ? " [done]"sv : ""sv),
+ (sr.expired ? " [expired]"sv : ""sv),
+ (sr.isSynced(now) ? " [synced]"sv : " [not synced]"sv),
+ (sr.isListening(now, listen_expire) ? " [listening]"sv : ""sv)
+ );
+
+ // printing the queries
+ if (sr.callbacks.size() + sr.listeners.size() > 0)
+ fmt::print(out, "Queries:\n");
+ for (const auto& cb : sr.callbacks) {
+ out << *cb.second.query << std::endl;
+ }
+ for (const auto& l : sr.listeners) {
+ out << *l.second.query << std::endl;
+ }
+
+ for (const auto& a : sr.announce) {
+ bool announced = sr.isAnnounced(a.value->id);
+ out << "Announcement: " << *a.value << (announced ? " [announced]" : "") << std::endl;
+ }
+
+ fmt::print(out, " Common bits InfoHash Conn. Get Ops IP\n");
+ auto last_get = sr.getLastGetTime();
+ for (const auto& np : sr.nodes) {
+ auto& n = *np;
+ auto listen = (not sr.listeners.empty() ? (n.listenStatus.empty() ? " " : fmt::format("[{}] ", (n.isListening(now,listen_expire) ? 'l' : (n.pending(n.listenStatus) ? 'f' : ' ')))) : "");
+ std::string announce;
+ if (not sr.announce.empty()) {
+ if (n.acked.empty()) {
+ announce = std::string(sr.announce.size() + 3, ' ');
+ } else {
+ announce = "[";
+ for (const auto& a : sr.announce) {
+ auto ack = n.acked.find(a.value->id);
+ if (ack == n.acked.end() or not ack->second.req) {
+ announce += ' ';
+ } else {
+ announce += ack->second.req->getStateChar();
+ }
+ }
+ announce += "] ";
+ }
+ }
+
+ fmt::print(out, "{:3} {} {} [{}{}] {}{}{}\n",
+ InfoHash::commonBits(sr.id, n.node->id),
+ n.node->id,
+ (findNode(n.node->id, sr.af) ? '*' : ' '),
+ (n.pending(n.getStatus) ? (n.candidate ? 'c' : 'f') : ' '),
+ (n.isSynced(now) ? (n.last_get_reply > last_get ? 'u' : 's') : '-'),
+ listen,
+ announce,
+ n.node->getAddrStr()
+ );
+ }
+}
+
+void
+Dht::dumpTables() const
+{
+ std::ostringstream out;
+ out << "My id " << myid << std::endl;
+
+ out << "Buckets IPv4 :" << std::endl;
+ for (const auto& b : dht4.buckets)
+ dumpBucket(b, out);
+ out << "Buckets IPv6 :" << std::endl;
+ for (const auto& b : dht6.buckets)
+ dumpBucket(b, out);
+
+ auto dump_searches = [&](std::map<InfoHash, Sp<Search>> srs) {
+ for (auto& srp : srs)
+ dumpSearch(*srp.second, out);
+ };
+ dump_searches(dht4.searches);
+ dump_searches(dht6.searches);
+ out << std::endl;
+
+ out << getStorageLog() << std::endl;
+
+ if (logger_)
+ logger_->d("%s", out.str().c_str());
+}
+
+std::string
+Dht::getStorageLog() const
+{
+ std::ostringstream out;
+ for (const auto& s : store)
+ out << printStorageLog(s);
+ out << std::endl << std::endl;
+ std::multimap<size_t, const SockAddr*> q_map;
+ for (const auto& ip : store_quota)
+ if (ip.second.size())
+ q_map.emplace(ip.second.size(), &ip.first);
+ for (auto ip = q_map.rbegin(); ip != q_map.rend(); ++ip)
+ out << "IP " << ip->second->toString() << " uses " << ip->first << " bytes" << std::endl;
+ out << std::endl;
+ out << "Total " << store.size() << " storages, " << total_values << " values (";
+ if (total_store_size < 1024)
+ out << total_store_size << " bytes)";
+ else
+ out << (total_store_size/1024) << " / " << (max_store_size/1024) << " KB)";
+ out << std::endl;
+ return out.str();
+}
+
+std::string
+Dht::getStorageLog(const InfoHash& h) const
+{
+ auto s = store.find(h);
+ if (s == store.end()) {
+ std::ostringstream out;
+ out << "Storage " << h << " empty" << std::endl;
+ return out.str();
+ }
+ return printStorageLog(*s);
+}
+
+std::string
+Dht::printStorageLog(const decltype(store)::value_type& s) const
+{
+ std::ostringstream out;
+ using namespace std::chrono;
+ const auto& st = s.second;
+ out << "Storage " << s.first << " "
+ << st.listeners.size() << " list., "
+ << st.valueCount() << " values ("
+ << st.totalSize() << " bytes)" << std::endl;
+ if (not st.local_listeners.empty())
+ out << " " << st.local_listeners.size() << " local listeners" << std::endl;
+ for (const auto& node_listeners : st.listeners) {
+ const auto& node = node_listeners.first;
+ out << " " << "Listener " << node->toString() << " : " << node_listeners.second.size() << " entries" << std::endl;
+ }
+ return out.str();
+}
+
+std::string
+Dht::getRoutingTablesLog(sa_family_t af) const
+{
+ std::ostringstream out;
+ for (const auto& b : buckets(af))
+ dumpBucket(b, out);
+ return out.str();
+}
+
+std::string
+Dht::getSearchesLog(sa_family_t af) const
+{
+ std::ostringstream out;
+ auto num_searches = dht4.searches.size() + dht6.searches.size();
+ if (num_searches > 8) {
+ if (not af or af == AF_INET)
+ for (const auto& sr : dht4.searches)
+ out << "[search " << sr.first << " IPv4]" << std::endl;
+ if (not af or af == AF_INET6)
+ for (const auto& sr : dht6.searches)
+ out << "[search " << sr.first << " IPv6]" << std::endl;
+ } else {
+ out << "s:synched, u:updated, a:announced, c:candidate, f:cur req, x:expired, *:known" << std::endl;
+ if (not af or af == AF_INET)
+ for (const auto& sr : dht4.searches)
+ dumpSearch(*sr.second, out);
+ if (not af or af == AF_INET6)
+ for (const auto& sr : dht6.searches)
+ dumpSearch(*sr.second, out);
+ }
+ out << "Total: " << num_searches << " searches (" << dht4.searches.size() << " IPv4, " << dht6.searches.size() << " IPv6)." << std::endl;
+ return out.str();
+}
+
+std::string
+Dht::getSearchLog(const InfoHash& id, sa_family_t af) const
+{
+ std::ostringstream out;
+ if (af == AF_UNSPEC) {
+ out << getSearchLog(id, AF_INET) << getSearchLog(id, AF_INET6);
+ } else {
+ auto& srs = searches(af);
+ auto sr = srs.find(id);
+ if (sr != srs.end())
+ dumpSearch(*sr->second, out);
+ }
+ return out.str();
+}
+
+Dht::~Dht()
+{
+ for (auto& s : dht4.searches)
+ s.second->clear();
+ for (auto& s : dht6.searches)
+ s.second->clear();
+}
+
+net::NetworkConfig
+fromDhtConfig(const Config& config)
+{
+ net::NetworkConfig netConf;
+ netConf.network = config.network;
+ netConf.max_req_per_sec = config.max_req_per_sec ? config.max_req_per_sec : MAX_REQUESTS_PER_SEC;
+ netConf.max_peer_req_per_sec = config.max_peer_req_per_sec
+ ? config.max_peer_req_per_sec
+ : netConf.max_req_per_sec/8;
+ return netConf;
+}
+
+Dht::Dht(std::unique_ptr<net::DatagramSocket>&& sock, const Config& config, const Sp<Logger>& l)
+ : DhtInterface(l),
+ myid(config.node_id ? config.node_id : InfoHash::getRandom(rd)),
+ store(),
+ store_quota(),
+ max_store_keys(config.max_store_keys ? (int)config.max_store_keys : MAX_HASHES),
+ max_store_size(config.max_store_size ? (int)config.max_store_size : DEFAULT_STORAGE_LIMIT),
+ max_searches(config.max_searches ? (int)config.max_searches : MAX_SEARCHES),
+ network_engine(myid, fromDhtConfig(config), std::move(sock), logger_, rd, scheduler,
+ std::bind(&Dht::onError, this, _1, _2),
+ std::bind(&Dht::onNewNode, this, _1, _2),
+ std::bind(&Dht::onReportedAddr, this, _1, _2),
+ std::bind(&Dht::onPing, this, _1),
+ std::bind(&Dht::onFindNode, this, _1, _2, _3),
+ std::bind(&Dht::onGetValues, this, _1, _2, _3, _4),
+ std::bind(&Dht::onListen, this, _1, _2, _3, _4, _5, _6),
+ std::bind(&Dht::onAnnounce, this, _1, _2, _3, _4, _5),
+ std::bind(&Dht::onRefresh, this, _1, _2, _3, _4)),
+ persistPath(config.persist_path),
+ is_bootstrap(config.is_bootstrap),
+ maintain_storage(config.maintain_storage),
+ public_stable(config.public_stable)
+{
+ scheduler.syncTime();
+ auto s = network_engine.getSocket();
+ if (not s or (not s->hasIPv4() and not s->hasIPv6()))
+ throw DhtException("Opened socket required");
+ if (s->hasIPv4()) {
+ dht4.buckets = {Bucket {AF_INET}};
+ dht4.buckets.is_client = config.is_bootstrap;
+ }
+ if (s->hasIPv6()) {
+ dht6.buckets = {Bucket {AF_INET6}};
+ dht6.buckets.is_client = config.is_bootstrap;
+ }
+
+ search_id = std::uniform_int_distribution<decltype(search_id)>{}(rd);
+
+ uniform_duration_distribution<> time_dis {std::chrono::seconds(3), std::chrono::seconds(5)};
+ nextNodesConfirmation = scheduler.add(scheduler.time() + time_dis(rd), std::bind(&Dht::confirmNodes, this));
+
+ // Fill old secret
+ secret = std::uniform_int_distribution<uint64_t>{}(rd);
+ rotateSecrets();
+
+ if (not persistPath.empty())
+ loadState(persistPath);
+
+ expire();
+
+ if (logger_)
+ logger_->debug("DHT node initialised with ID {:s}", myid);
+}
+
+bool
+Dht::neighbourhoodMaintenance(RoutingTable& list)
+{
+ //logger__DBG("neighbourhoodMaintenance");
+ auto b = list.findBucket(myid);
+ if (b == list.end())
+ return false;
+
+ InfoHash id = myid;
+#ifdef _WIN32
+ std::uniform_int_distribution<int> rand_byte{ 0, std::numeric_limits<uint8_t>::max() };
+#else
+ std::uniform_int_distribution<uint8_t> rand_byte;
+#endif
+ id[HASH_LEN-1] = rand_byte(rd);
+
+ std::bernoulli_distribution rand_trial(1./8.);
+ auto q = b;
+ if (std::next(q) != list.end() && (q->nodes.empty() || rand_trial(rd)))
+ q = std::next(q);
+ if (b != list.begin() && (q->nodes.empty() || rand_trial(rd))) {
+ auto r = std::prev(b);
+ if (!r->nodes.empty())
+ q = r;
+ }
+
+ auto n = q->randomNode(rd);
+ if (n) {
+ if (logger_)
+ logger_->d(id, n->id, "[node %s] sending [find %s] for neighborhood maintenance",
+ n->toString().c_str(), id.toString().c_str());
+ /* Since our node-id is the same in both DHTs, it's probably
+ profitable to query both families. */
+ network_engine.sendFindNode(n, id, network_engine.want());
+ }
+
+ return true;
+}
+
+bool
+Dht::bucketMaintenance(RoutingTable& list)
+{
+ std::bernoulli_distribution rand_trial(1./8.);
+ std::bernoulli_distribution rand_trial_38(1./38.);
+
+ bool sent {false};
+ for (auto b = list.begin(); b != list.end(); ++b) {
+ if (b->time < scheduler.time() - std::chrono::minutes(10) || b->nodes.empty()) {
+ /* This bucket hasn't seen any positive confirmation for a long
+ time. Pick a random id in this bucket's range, and send a request
+ to a random node. */
+ InfoHash id = list.randomId(b, rd);
+ auto q = b;
+ /* If the bucket is empty, we try to fill it from a neighbour.
+ We also sometimes do it gratuitiously to recover from
+ buckets full of broken nodes. */
+ if (std::next(b) != list.end() && (q->nodes.empty() || rand_trial(rd)))
+ q = std::next(b);
+ if (b != list.begin() && (q->nodes.empty() || rand_trial(rd))) {
+ auto r = std::prev(b);
+ if (!r->nodes.empty())
+ q = r;
+ }
+
+ auto n = q->randomNode(rd);
+ if (n and not n->isPendingMessage()) {
+ want_t want = -1;
+
+ if (network_engine.want() != want) {
+ auto otherbucket = findBucket(id, q->af == AF_INET ? AF_INET6 : AF_INET);
+ if (otherbucket && otherbucket->nodes.size() < TARGET_NODES)
+ /* The corresponding bucket in the other family
+ is emptyish -- querying both is useful. */
+ want = WANT4 | WANT6;
+ else if (rand_trial_38(rd))
+ /* Most of the time, this just adds overhead.
+ However, it might help stitch back one of
+ the DHTs after a network collapse, so query
+ both, but only very occasionally. */
+ want = WANT4 | WANT6;
+ }
+
+ if (logger_)
+ logger_->d(id, n->id, "[node %s] sending find %s for bucket maintenance", n->toString().c_str(), id.toString().c_str());
+ //auto start = scheduler.time();
+ network_engine.sendFindNode(n, id, want, nullptr, [this,n](const net::Request&, bool over) {
+ if (over) {
+ const auto& end = scheduler.time();
+ // using namespace std::chrono;
+ // if (logger_)
+ // logger_->d(n->id, "[node %s] bucket maintenance op expired after %s", n->toString().c_str(), print_duration(end-start).c_str());
+ scheduler.edit(nextNodesConfirmation, end + Node::MAX_RESPONSE_TIME);
+ }
+ });
+ sent = true;
+ }
+ }
+ }
+ return sent;
+}
+
+void
+Dht::dataPersistence(InfoHash id)
+{
+ const auto& now = scheduler.time();
+ auto str = store.find(id);
+ if (str != store.end() and now > str->second.maintenance_time) {
+ if (logger_)
+ logger_->d(id, "[storage %s] maintenance (%u values, %u bytes)",
+ id.toString().c_str(), str->second.valueCount(), str->second.totalSize());
+ maintainStorage(*str);
+ str->second.maintenance_time = now + MAX_STORAGE_MAINTENANCE_EXPIRE_TIME;
+ scheduler.add(str->second.maintenance_time, std::bind(&Dht::dataPersistence, this, id));
+ }
+}
+
+size_t
+Dht::maintainStorage(decltype(store)::value_type& storage, bool force, const DoneCallback& donecb)
+{
+ const auto& now = scheduler.time();
+ size_t announce_per_af = 0;
+
+ auto maintain = [&](sa_family_t af){
+ bool want = true;
+ auto nodes = buckets(af).findClosestNodes(storage.first, now);
+ if (!nodes.empty()) {
+ if (force || storage.first.xorCmp(nodes.back()->id, myid) < 0) {
+ for (auto &value : storage.second.getValues()) {
+ const auto& vt = getType(value.data->type);
+ if (force || value.created + vt.expiration > now + MAX_STORAGE_MAINTENANCE_EXPIRE_TIME) {
+ // gotta put that value there
+ announce(storage.first, af, value.data, donecb, value.created);
+ ++announce_per_af;
+ }
+ }
+ want = false;
+ }
+ }
+ return want;
+ };
+ bool want4 = maintain(AF_INET), want6 = maintain(AF_INET6);
+
+ if (not want4 and not want6) {
+ if (logger_)
+ logger_->d(storage.first, "Discarding storage values %s", storage.first.toString().c_str());
+ auto diff = storage.second.clear();
+ total_store_size += diff.size_diff;
+ total_values += diff.values_diff;
+ }
+
+ return announce_per_af;
+}
+
+time_point
+Dht::periodic(const uint8_t *buf, size_t buflen, SockAddr from, const time_point& now)
+{
+ scheduler.syncTime(now);
+ if (buflen) {
+ try {
+ network_engine.processMessage(buf, buflen, std::move(from));
+ } catch (const std::exception& e) {
+ if (logger_)
+ logger_->w("Can't process message: %s", e.what());
+ }
+ }
+ return scheduler.run();
+}
+
+void
+Dht::expire()
+{
+ uniform_duration_distribution<> time_dis(std::chrono::minutes(2), std::chrono::minutes(6));
+ auto expire_stuff_time = scheduler.time() + duration(time_dis(rd));
+
+ expireBuckets(dht4.buckets);
+ expireBuckets(dht6.buckets);
+ expireStore();
+ expireSearches();
+ scheduler.add(expire_stuff_time, std::bind(&Dht::expire, this));
+}
+
+void
+Dht::onConnected()
+{
+ stopBootstrap();
+ auto callbacks = std::move(onConnectCallbacks_);
+ while (not callbacks.empty()) {
+ callbacks.front()();
+ callbacks.pop();
+ }
+}
+
+void
+Dht::onDisconnected()
+{
+ if (not bootstrapJob)
+ bootstrap();
+}
+
+void
+Dht::bootstrap()
+{
+ if (dht4.status != NodeStatus::Disconnected || dht6.status != NodeStatus::Disconnected)
+ return;
+ if (logger_)
+ logger_->d(myid, "Bootstraping");
+ for (const auto& boootstrap : bootstrap_nodes) {
+ try {
+ auto ips = network_engine.getSocket()->resolve(boootstrap.first, boootstrap.second);
+ for (auto& ip : ips) {
+ if (ip.getPort() == 0)
+ ip.setPort(net::DHT_DEFAULT_PORT);
+ pingNode(ip);
+ }
+ } catch (const std::exception& e) {
+ if (logger_)
+ logger_->e(myid, "Can't resolve %s:%s: %s", boootstrap.first.c_str(), boootstrap.second.c_str(), e.what());
+ }
+ }
+ scheduler.cancel(bootstrapJob);
+ bootstrapJob = scheduler.add(scheduler.time() + bootstrap_period, std::bind(&Dht::bootstrap, this));
+ bootstrap_period = std::min(bootstrap_period * 2, BOOTSTRAP_PERIOD_MAX);
+}
+
+void
+Dht::startBootstrap()
+{
+ stopBootstrap();
+ bootstrapJob = scheduler.add(scheduler.time(), std::bind(&Dht::bootstrap, this));
+}
+
+void
+Dht::stopBootstrap()
+{
+ scheduler.cancel(bootstrapJob);
+ bootstrap_period = BOOTSTRAP_PERIOD;
+}
+
+void
+Dht::confirmNodes()
+{
+ using namespace std::chrono;
+ bool soon = false;
+ const auto& now = scheduler.time();
+
+ if (dht4.searches.empty() and dht4.status == NodeStatus::Connected) {
+ if (logger_)
+ logger_->d(myid, "[confirm nodes] initial IPv4 'get' for my id (%s)", myid.toString().c_str());
+ search(myid, AF_INET);
+ }
+ if (dht6.searches.empty() and dht6.status == NodeStatus::Connected) {
+ if (logger_)
+ logger_->d(myid, "[confirm nodes] initial IPv6 'get' for my id (%s)", myid.toString().c_str());
+ search(myid, AF_INET6);
+ }
+
+ soon |= bucketMaintenance(dht4.buckets);
+ soon |= bucketMaintenance(dht6.buckets);
+
+ if (!soon) {
+ if (dht4.buckets.grow_time >= now - seconds(150))
+ soon |= neighbourhoodMaintenance(dht4.buckets);
+ if (dht6.buckets.grow_time >= now - seconds(150))
+ soon |= neighbourhoodMaintenance(dht6.buckets);
+ }
+
+ /* In order to maintain all buckets' age within 600 seconds, worst
+ case is roughly 27 seconds, assuming the table is 22 bits deep.
+ We want to keep a margin for neighborhood maintenance, so keep
+ this within 25 seconds. */
+ auto time_dis = soon
+ ? uniform_duration_distribution<> {seconds(5) , seconds(25)}
+ : uniform_duration_distribution<> {seconds(60), seconds(180)};
+ auto confirm_nodes_time = now + time_dis(rd);
+
+ scheduler.edit(nextNodesConfirmation, confirm_nodes_time);
+}
+
+std::vector<ValuesExport>
+Dht::exportValues() const
+{
+ std::vector<ValuesExport> e {};
+ e.reserve(store.size());
+ for (const auto& h : store) {
+ ValuesExport ve;
+ ve.first = h.first;
+
+ msgpack::sbuffer buffer;
+ msgpack::packer<msgpack::sbuffer> pk(&buffer);
+ const auto& vals = h.second.getValues();
+ pk.pack_array(vals.size());
+ for (const auto& v : vals) {
+ pk.pack_array(2);
+ pk.pack(v.created.time_since_epoch().count());
+ v.data->msgpack_pack(pk);
+ }
+ ve.second = {buffer.data(), buffer.data()+buffer.size()};
+ e.push_back(std::move(ve));
+ }
+ return e;
+}
+
+void
+Dht::importValues(const std::vector<ValuesExport>& import)
+{
+ const auto& now = scheduler.time();
+
+ for (const auto& value : import) {
+ if (value.second.empty())
+ continue;
+
+ try {
+ msgpack::unpacked msg;
+ msgpack::unpack(msg, (const char*)value.second.data(), value.second.size());
+ auto valarr = msg.get();
+ if (valarr.type != msgpack::type::ARRAY)
+ throw msgpack::type_error();
+ for (unsigned i = 0; i < valarr.via.array.size; i++) {
+ auto& valel = valarr.via.array.ptr[i];
+ if (valel.type != msgpack::type::ARRAY or valel.via.array.size < 2)
+ throw msgpack::type_error();
+ time_point val_time;
+ Value tmp_val;
+ try {
+ val_time = time_point{time_point::duration{valel.via.array.ptr[0].as<time_point::duration::rep>()}};
+ tmp_val.msgpack_unpack(valel.via.array.ptr[1]);
+ } catch (const std::exception&) {
+ if (logger_)
+ logger_->e(value.first, "Error reading value at %s", value.first.toString().c_str());
+ continue;
+ }
+ val_time = std::min(val_time, now);
+ storageStore(value.first, std::make_shared<Value>(std::move(tmp_val)), val_time);
+ }
+ } catch (const std::exception&) {
+ if (logger_)
+ logger_->e(value.first, "Error reading values at %s", value.first.toString().c_str());
+ continue;
+ }
+ }
+}
+
+
+std::vector<NodeExport>
+Dht::exportNodes() const
+{
+ const auto& now = scheduler.time();
+ std::vector<NodeExport> nodes;
+ const auto b4 = dht4.buckets.findBucket(myid);
+ if (b4 != dht4.buckets.end()) {
+ for (auto& n : b4->nodes)
+ if (n->isGood(now))
+ nodes.emplace_back(n->exportNode());
+ }
+ const auto b6 = dht6.buckets.findBucket(myid);
+ if (b6 != dht6.buckets.end()) {
+ for (auto& n : b6->nodes)
+ if (n->isGood(now))
+ nodes.emplace_back(n->exportNode());
+ }
+ for (auto b = dht4.buckets.begin(); b != dht4.buckets.end(); ++b) {
+ if (b == b4) continue;
+ for (auto& n : b->nodes)
+ if (n->isGood(now))
+ nodes.emplace_back(n->exportNode());
+ }
+ for (auto b = dht6.buckets.begin(); b != dht6.buckets.end(); ++b) {
+ if (b == b6) continue;
+ for (auto& n : b->nodes)
+ if (n->isGood(now))
+ nodes.emplace_back(n->exportNode());
+ }
+ return nodes;
+}
+
+void
+Dht::insertNode(const InfoHash& id, const SockAddr& addr)
+{
+ if (addr.getFamily() != AF_INET && addr.getFamily() != AF_INET6)
+ return;
+ scheduler.syncTime();
+ network_engine.insertNode(id, addr);
+}
+
+void
+Dht::pingNode(SockAddr sa, DoneCallbackSimple&& cb)
+{
+ scheduler.syncTime();
+ if (logger_)
+ logger_->debug("Sending ping to {}", sa);
+ auto& count = dht(sa.getFamily()).pending_pings;
+ count++;
+ network_engine.sendPing(std::move(sa), [&count,cb](const net::Request&, net::RequestAnswer&&) {
+ count--;
+ if (cb)
+ cb(true);
+ }, [&count,cb](const net::Request&, bool last){
+ if (last) {
+ count--;
+ if (cb)
+ cb(false);
+ }
+ });
+}
+
+void
+Dht::onError(Sp<net::Request> req, net::DhtProtocolException e) {
+ const auto& node = req->node;
+ if (e.getCode() == net::DhtProtocolException::UNAUTHORIZED) {
+ if (logger_)
+ logger_->e(node->id, "[node %s] token flush", node->toString().c_str());
+ node->authError();
+ for (auto& srp : searches(node->getFamily())) {
+ auto& sr = srp.second;
+ for (auto& n : sr->nodes) {
+ if (n->node != node) continue;
+ n->token.clear();
+ n->last_get_reply = time_point::min();
+ searchSendGetValues(sr);
+ scheduler.edit(sr->nextSearchStep, scheduler.time());
+ break;
+ }
+ }
+ } else if (e.getCode() == net::DhtProtocolException::NOT_FOUND) {
+ if (logger_)
+ logger_->e(node->id, "[node %s] returned error 404: storage not found", node->toString().c_str());
+ node->cancelRequest(req);
+ }
+}
+
+void
+Dht::onReportedAddr(const InfoHash& /*id*/, const SockAddr& addr)
+{
+ if (addr)
+ reportedAddr(addr);
+}
+
+net::RequestAnswer
+Dht::onPing(Sp<Node>)
+{
+ return {};
+}
+
+net::RequestAnswer
+Dht::onFindNode(Sp<Node> node, const InfoHash& target, want_t want)
+{
+ const auto& now = scheduler.time();
+ net::RequestAnswer answer;
+ answer.ntoken = makeToken(node->getAddr(), false);
+ if (want & WANT4)
+ answer.nodes4 = dht4.buckets.findClosestNodes(target, now, TARGET_NODES);
+ if (want & WANT6)
+ answer.nodes6 = dht6.buckets.findClosestNodes(target, now, TARGET_NODES);
+ return answer;
+}
+
+net::RequestAnswer
+Dht::onGetValues(Sp<Node> node, const InfoHash& hash, want_t, const Query& query)
+{
+ if (not hash) {
+ if (logger_)
+ logger_->w("[node %s] Eek! Got get_values with no info_hash", node->toString().c_str());
+ throw net::DhtProtocolException {
+ net::DhtProtocolException::NON_AUTHORITATIVE_INFORMATION,
+ net::DhtProtocolException::GET_NO_INFOHASH
+ };
+ }
+ const auto& now = scheduler.time();
+ net::RequestAnswer answer {};
+ auto st = store.find(hash);
+ answer.ntoken = makeToken(node->getAddr(), false);
+ answer.nodes4 = dht4.buckets.findClosestNodes(hash, now, TARGET_NODES);
+ answer.nodes6 = dht6.buckets.findClosestNodes(hash, now, TARGET_NODES);
+ if (st != store.end() && not st->second.empty()) {
+ answer.values = st->second.get(query.where.getFilter());
+ if (logger_)
+ logger_->d(hash, "[node %s] sending %u values", node->toString().c_str(), answer.values.size());
+ }
+ return answer;
+}
+
+void Dht::onGetValuesDone(const Sp<Node>& node,
+ net::RequestAnswer& a,
+ Sp<Search>& sr,
+ const Sp<Query>& orig_query)
+{
+ if (not sr) {
+ if (logger_)
+ logger_->w("[search unknown] got reply to 'get'. Ignoring.");
+ return;
+ }
+
+ /* if (logger_)
+ logger_->d(sr->id, "[search %s] [node %s] got reply to 'get' with %u nodes",
+ sr->id.toString().c_str(), node->toString().c_str(), a.nodes4.size()+a.nodes6.size());*/
+
+ if (not a.ntoken.empty()) {
+ if (not a.values.empty() or not a.fields.empty()) {
+ if (logger_)
+ logger_->d(sr->id, node->id, "[search %s] [node %s] found %u values",
+ sr->id.toString().c_str(), node->toString().c_str(), a.values.size());
+ for (auto& getp : sr->callbacks) { /* call all callbacks for this search */
+ auto& get = getp.second;
+ if (not (get.get_cb or get.query_cb) or
+ (orig_query and get.query and not get.query->isSatisfiedBy(*orig_query)))
+ continue;
+
+ if (get.query_cb) { /* in case of a request with query */
+ if (not a.fields.empty()) {
+ get.query_cb(a.fields);
+ } else if (not a.values.empty()) {
+ std::vector<Sp<FieldValueIndex>> fields;
+ fields.reserve(a.values.size());
+ for (const auto& v : a.values)
+ fields.emplace_back(std::make_shared<FieldValueIndex>(*v, orig_query ? orig_query->select : Select {}));
+ get.query_cb(fields);
+ }
+ } else if (get.get_cb) { /* in case of a vanilla get request */
+ std::vector<Sp<Value>> tmp;
+ for (const auto& v : a.values)
+ if (not get.filter or get.filter(*v))
+ tmp.emplace_back(v);
+ if (not tmp.empty())
+ get.get_cb(tmp);
+ }
+ }
+
+ /* callbacks for local search listeners */
+ /*std::vector<std::pair<ValueCallback, std::vector<Sp<Value>>>> tmp_lists;
+ for (auto& l : sr->listeners) {
+ if (!l.second.get_cb or (orig_query and l.second.query and not l.second.query->isSatisfiedBy(*orig_query)))
+ continue;
+ std::vector<Sp<Value>> tmp;
+ for (const auto& v : a.values)
+ if (not l.second.filter or l.second.filter(*v))
+ tmp.emplace_back(v);
+ if (not tmp.empty())
+ tmp_lists.emplace_back(l.second.get_cb, std::move(tmp));
+ }
+ for (auto& l : tmp_lists)
+ l.first(l.second, false);*/
+ } else if (not a.expired_values.empty()) {
+ if (logger_)
+ logger_->w(sr->id, node->id, "[search %s] [node %s] %u expired values",
+ sr->id.toString().c_str(), node->toString().c_str(), a.expired_values.size());
+ }
+ } else {
+ if (logger_)
+ logger_->w(sr->id, "[node %s] no token provided. Ignoring response content.", node->toString().c_str());
+ network_engine.blacklistNode(node);
+ }
+
+ if (not sr->done) {
+ searchSendGetValues(sr);
+
+ // Force to recompute the next step time
+ scheduler.edit(sr->nextSearchStep, scheduler.time());
+ }
+}
+
+net::RequestAnswer
+Dht::onListen(Sp<Node> node, const InfoHash& hash, const Blob& token, size_t socket_id, const Query& query, int version)
+{
+ if (not hash) {
+ if (logger_)
+ logger_->w(node->id, "[node %s] listen with no info_hash", node->toString().c_str());
+ throw net::DhtProtocolException {
+ net::DhtProtocolException::NON_AUTHORITATIVE_INFORMATION,
+ net::DhtProtocolException::LISTEN_NO_INFOHASH
+ };
+ }
+ if (not tokenMatch(token, node->getAddr())) {
+ if (logger_)
+ logger_->w(hash, node->id, "[node %s] incorrect token %s for 'listen'", node->toString().c_str(), hash.toString().c_str());
+ throw net::DhtProtocolException {net::DhtProtocolException::UNAUTHORIZED, net::DhtProtocolException::LISTEN_WRONG_TOKEN};
+ }
+ Query q = query;
+ storageAddListener(hash, node, socket_id, std::move(q), version);
+ return {};
+}
+
+void
+Dht::onListenDone(const Sp<Node>& /* node */, net::RequestAnswer& /* answer */, Sp<Search>& sr)
+{
+ // if (logger_)
+ // logger_->d(sr->id, node->id, "[search %s] [node %s] got listen confirmation",
+ // sr->id.toString().c_str(), node->toString().c_str(), answer.values.size());
+
+ if (not sr->done) {
+ const auto& now = scheduler.time();
+ searchSendGetValues(sr);
+ scheduler.edit(sr->nextSearchStep, now);
+ }
+}
+
+net::RequestAnswer
+Dht::onAnnounce(Sp<Node> n,
+ const InfoHash& hash,
+ const Blob& token,
+ const std::vector<Sp<Value>>& values,
+ const time_point& creation_date)
+{
+ auto& node = *n;
+ if (not hash) {
+ if (logger_)
+ logger_->w(node.id, "put with no info_hash");
+ throw net::DhtProtocolException {
+ net::DhtProtocolException::NON_AUTHORITATIVE_INFORMATION,
+ net::DhtProtocolException::PUT_NO_INFOHASH
+ };
+ }
+ if (!tokenMatch(token, node.getAddr())) {
+ if (logger_)
+ logger_->w(hash, node.id, "[node %s] incorrect token %s for 'put'", node.toString().c_str(), hash.toString().c_str());
+ throw net::DhtProtocolException {net::DhtProtocolException::UNAUTHORIZED, net::DhtProtocolException::PUT_WRONG_TOKEN};
+ }
+ {
+ // We store a value only if we think we're part of the
+ // SEARCH_NODES nodes around the target id.
+ auto closest_nodes = buckets(node.getFamily()).findClosestNodes(hash, scheduler.time(), SEARCH_NODES);
+ if (closest_nodes.size() >= TARGET_NODES and hash.xorCmp(closest_nodes.back()->id, myid) < 0) {
+ if (logger_)
+ logger_->w(hash, node.id, "[node %s] announce too far from the target. Dropping value.", node.toString().c_str());
+ return {};
+ }
+ }
+
+ auto created = std::min(creation_date, scheduler.time());
+ for (const auto& v : values) {
+ if (v->id == Value::INVALID_ID) {
+ if (logger_)
+ logger_->w(hash, node.id, "[value %s] incorrect value id", hash.toString().c_str());
+ throw net::DhtProtocolException {
+ net::DhtProtocolException::NON_AUTHORITATIVE_INFORMATION,
+ net::DhtProtocolException::PUT_INVALID_ID
+ };
+ }
+ auto lv = getLocalById(hash, v->id);
+ Sp<Value> vc = v;
+ if (lv) {
+ if (*lv == *vc) {
+ storageRefresh(hash, v->id);
+ if (logger_)
+ logger_->d(hash, node.id, "[store %s] [node %s] refreshed value %016" PRIx64, hash.toString().c_str(), node.toString().c_str(), v->id);
+ } else {
+ const auto& type = getType(lv->type);
+ if (type.editPolicy(hash, lv, vc, node.id, node.getAddr())) {
+ if (logger_)
+ logger_->d(hash, node.id, "[store %s] editing %s",
+ hash.toString().c_str(), vc->toString().c_str());
+ storageStore(hash, vc, created, node.getAddr());
+ } else {
+ if (logger_)
+ logger_->d(hash, node.id, "[store %s] rejecting edition of %s because of storage policy",
+ hash.toString().c_str(), vc->toString().c_str());
+ }
+ }
+ } else {
+ // Allow the value to be edited by the storage policy
+ const auto& type = getType(vc->type);
+ if (type.storePolicy(hash, vc, node.id, node.getAddr())) {
+ // if (logger_)
+ // logger_->d(hash, node.id, "[store %s] storing %s", hash.toString().c_str(), std::to_string(vc->id).c_str());
+ storageStore(hash, vc, created, node.getAddr());
+ } else {
+ if (logger_)
+ logger_->d(hash, node.id, "[store %s] rejecting storage of %s",
+ hash.toString().c_str(), vc->toString().c_str());
+ }
+ }
+ }
+ return {};
+}
+
+net::RequestAnswer
+Dht::onRefresh(Sp<Node> node, const InfoHash& hash, const Blob& token, const Value::Id& vid)
+{
+ using namespace net;
+
+ if (not tokenMatch(token, node->getAddr())) {
+ if (logger_)
+ logger_->w(hash, node->id, "[node %s] incorrect token %s for 'put'", node->toString().c_str(), hash.toString().c_str());
+ throw DhtProtocolException {DhtProtocolException::UNAUTHORIZED, DhtProtocolException::PUT_WRONG_TOKEN};
+ }
+ if (storageRefresh(hash, vid)) {
+ if (logger_)
+ logger_->d(hash, node->id, "[store %s] [node %s] refreshed value %016" PRIx64, hash.toString().c_str(), node->toString().c_str(), vid);
+ } else {
+ if (logger_)
+ logger_->d(hash, node->id, "[store %s] [node %s] got refresh for unknown value",
+ hash.toString().c_str(), node->toString().c_str());
+ throw DhtProtocolException {DhtProtocolException::NOT_FOUND, DhtProtocolException::STORAGE_NOT_FOUND};
+ }
+ return {};
+}
+
+bool
+Dht::storageRefresh(const InfoHash& id, Value::Id vid)
+{
+ const auto& now = scheduler.time();
+ auto s = store.find(id);
+ if (s != store.end()) {
+ // Values like for a permanent put can be refreshed. So, inform remote listeners that the value
+ // need to be refreshed
+ auto& st = s->second;
+ if (not st.listeners.empty()) {
+ if (logger_)
+ logger_->d(id, "[store %s] %lu remote listeners", id.toString().c_str(), st.listeners.size());
+ std::vector<Value::Id> ids = {vid};
+ for (const auto& node_listeners : st.listeners) {
+ for (const auto& l : node_listeners.second) {
+ if (logger_)
+ logger_->w(id, node_listeners.first->id, "[store %s] [node %s] sending refresh",
+ id.toString().c_str(),
+ node_listeners.first->toString().c_str());
+ Blob ntoken = makeToken(node_listeners.first->getAddr(), false);
+ network_engine.tellListenerRefreshed(node_listeners.first, l.first, id, ntoken, ids, l.second.version);
+ }
+ }
+ }
+
+ auto expiration = s->second.refresh(id, now, vid, types);
+ if (expiration.first) {
+ scheduler.cancel(expiration.first->expiration_job);
+ if (expiration.second != time_point::max()) {
+ expiration.first->expiration_job = scheduler.add(expiration.second, std::bind(&Dht::expireStorage, this, id));
+ }
+ }
+ return true;
+ }
+ return false;
+}
+
+void
+Dht::onAnnounceDone(const Sp<Node>& node, net::RequestAnswer& answer, Sp<Search>& sr)
+{
+ if (logger_)
+ logger_->d(sr->id, node->id, "[search %s] [node %s] got reply to put!",
+ sr->id.toString().c_str(), node->toString().c_str());
+ searchSendGetValues(sr);
+ sr->checkAnnounced(answer.vid);
+}
+
+
+struct DhtState {
+ unsigned v {1};
+ InfoHash id;
+ std::vector<NodeExport> nodes;
+ std::vector<ValuesExport> values;
+
+ MSGPACK_DEFINE_MAP(v, id, nodes, values)
+};
+
+void
+Dht::saveState(const std::string& path) const
+{
+ DhtState state;
+ state.id = myid;
+ state.nodes = exportNodes();
+ state.values = exportValues();
+ std::ofstream file(path);
+ msgpack::pack(file, state);
+}
+
+void
+Dht::loadState(const std::string& path)
+{
+ if (logger_)
+ logger_->d("Importing state from %s", path.c_str());
+ try {
+ // Import nodes from binary file
+ msgpack::unpacker pac;
+ {
+ // Read whole file
+ std::ifstream file(path, std::ios::binary|std::ios::ate);
+ if (!file.is_open()) {
+ return;
+ }
+ auto size = file.tellg();
+ file.seekg (0, std::ios::beg);
+ pac.reserve_buffer(size);
+ file.read (pac.buffer(), size);
+ pac.buffer_consumed(size);
+ }
+ // Import nodes
+ msgpack::object_handle oh;
+ if (pac.next(oh)) {
+ auto state = oh.get().as<DhtState>();
+ if (logger_)
+ logger_->d("Importing %zu nodes", state.nodes.size());
+ if (state.id)
+ myid = state.id;
+ std::vector<Sp<Node>> tmpNodes;
+ tmpNodes.reserve(state.nodes.size());
+ for (const auto& node : state.nodes)
+ tmpNodes.emplace_back(network_engine.insertNode(node.id, node.addr));
+ importValues(state.values);
+ }
+ } catch (const std::exception& e) {
+ if (logger_)
+ logger_->w("Error importing state from %s: %s", path.c_str(), e.what());
+ }
+}
+
+}
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ * Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com>
+ * Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ * Vsevolod Ivanov <vsevolod.ivanov@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "dht_proxy_client.h"
+#include "dhtrunner.h"
+#include "op_cache.h"
+#include "utils.h"
+
+#include <http_parser.h>
+#include <deque>
+
+
+namespace dht {
+
+struct DhtProxyClient::InfoState {
+ std::atomic_uint ipv4 {0}, ipv6 {0};
+ std::atomic_bool cancel {false};
+};
+
+struct DhtProxyClient::OperationState {
+ std::atomic_bool ok {true};
+ std::atomic_bool stop {false};
+};
+
+struct DhtProxyClient::Listener
+{
+ Listener(OpValueCache&& c):
+ cache(std::move(c)) {}
+
+ OpValueCache cache;
+ CacheValueCallback cb;
+ Sp<OperationState> opstate;
+ std::shared_ptr<http::Request> request;
+#ifdef OPENDHT_PUSH_NOTIFICATIONS
+ std::unique_ptr<asio::steady_timer> refreshSubscriberTimer;
+#endif
+};
+
+struct PermanentPut {
+ PermanentPut(const Sp<Value>& v, std::unique_ptr<asio::steady_timer>&& j,
+ const Sp<std::atomic_bool>& o):
+ value(v), refreshPutTimer(std::move(j)), ok(o)
+ {}
+
+ Sp<Value> value;
+ std::unique_ptr<asio::steady_timer> refreshPutTimer;
+ Sp<std::atomic_bool> ok;
+};
+
+struct DhtProxyClient::ProxySearch {
+ SearchCache ops {};
+ std::unique_ptr<asio::steady_timer> opExpirationTimer;
+ std::map<size_t, Listener> listeners {};
+ std::map<Value::Id, PermanentPut> puts {};
+ std::set<Sp<Value>> pendingPuts {};
+};
+
+struct LineSplit {
+ void append(const char* d, size_t l) {
+ buf_.insert(buf_.end(), d, d+l);
+ }
+ bool getLine(char c) {
+ auto it = buf_.begin();
+ while (it != buf_.end()) {
+ if (*(it++) == c) {
+ line_.clear();
+ line_.insert(line_.end(), buf_.begin(), it);
+ buf_.erase(buf_.begin(), it);
+ return true;
+ }
+ }
+ return false;
+ }
+ const std::string& line() const { return line_; }
+private:
+ std::deque<char> buf_ {};
+ std::string line_ {};
+};
+
+std::string
+getRandomSessionId(size_t length = 8) {
+ static constexpr const char chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!#$%&()*+,./:;<=>?@[]^_`{|}~";
+ std::string str(length, 0);
+ crypto::random_device rdev;
+ std::uniform_int_distribution<> dist(0, (sizeof(chars)/sizeof(char)) - 2);
+ std::generate_n( str.begin(), length, [&]{ return chars[dist(rdev)]; } );
+ return str;
+}
+
+DhtProxyClient::DhtProxyClient() {}
+
+DhtProxyClient::DhtProxyClient(
+ std::shared_ptr<dht::crypto::Certificate> serverCA, dht::crypto::Identity clientIdentity,
+ std::function<void()> signal, const std::string& serverHost,
+ const std::string& pushClientId, std::shared_ptr<dht::Logger> logger)
+ : DhtInterface(logger)
+ , proxyUrl_(serverHost)
+ , clientIdentity_(clientIdentity), serverCertificate_(serverCA)
+ , pushClientId_(pushClientId), pushSessionId_(getRandomSessionId())
+ , loopSignal_(signal)
+ , jsonReader_(Json::CharReaderBuilder{}.newCharReader())
+{
+ localAddrv4_.setFamily(AF_INET);
+ localAddrv6_.setFamily(AF_INET6);
+
+ jsonBuilder_["commentStyle"] = "None";
+ jsonBuilder_["indentation"] = "";
+ if (logger_) {
+ if (serverCertificate_)
+ logger_->d("[proxy:client] using ca certificate for ssl:\n%s",
+ serverCertificate_->toString(false/*chain*/).c_str());
+ if (clientIdentity_.first and clientIdentity_.second)
+ logger_->d("[proxy:client] using client certificate for ssl:\n%s",
+ clientIdentity_.second->toString(false/*chain*/).c_str());
+ }
+ // run http client
+ httpClientThread_ = std::thread([this](){
+ try {
+ if (logger_)
+ logger_->d("[proxy:client] starting io_context");
+ // Ensures the httpContext_ won't run out of work
+ auto work = asio::make_work_guard(httpContext_);
+ httpContext_.run();
+ if (logger_)
+ logger_->d("[proxy:client] http client io_context stopped");
+ }
+ catch(const std::exception& ex){
+ if (logger_)
+ logger_->e("[proxy:client] run error: %s", ex.what());
+ }
+ });
+ if (!proxyUrl_.empty())
+ startProxy();
+}
+
+void
+DhtProxyClient::startProxy()
+{
+ if (proxyUrl_.empty())
+ return;
+
+ if (logger_)
+ logger_->d("[proxy:client] start proxy with %s", proxyUrl_.c_str());
+
+ nextProxyConfirmationTimer_ = std::make_unique<asio::steady_timer>(httpContext_, std::chrono::steady_clock::now());
+ nextProxyConfirmationTimer_->async_wait(std::bind(&DhtProxyClient::handleProxyConfirm, this, std::placeholders::_1));
+
+ listenerRestartTimer_ = std::make_unique<asio::steady_timer>(httpContext_);
+
+ loopSignal_();
+}
+
+void
+DhtProxyClient::handleProxyConfirm(const asio::error_code &ec)
+{
+ if (ec == asio::error::operation_aborted)
+ return;
+ else if (ec){
+ if (logger_)
+ logger_->e("[proxy:client] confirm error: %s", ec.message().c_str());
+ return;
+ }
+ if (proxyUrl_.empty())
+ return;
+ getConnectivityStatus();
+}
+
+DhtProxyClient::~DhtProxyClient()
+{
+ stop();
+}
+
+void
+DhtProxyClient::stop()
+{
+ if (not isDestroying_.exchange(true)) {
+ {
+ std::lock_guard<std::mutex> l(resolverLock_);
+ resolver_.reset();
+ }
+ cancelAllListeners();
+ if (infoState_)
+ infoState_->cancel = true;
+ {
+ std::lock_guard<std::mutex> lock(requestLock_);
+ for (auto& request : requests_)
+ request.second->cancel();
+ }
+ if (not httpContext_.stopped())
+ httpContext_.stop();
+ if (httpClientThread_.joinable())
+ httpClientThread_.join();
+ requests_.clear();
+ }
+}
+
+std::vector<Sp<Value>>
+DhtProxyClient::getLocal(const InfoHash& k, const Value::Filter& filter) const {
+ std::lock_guard<std::mutex> lock(searchLock_);
+ auto s = searches_.find(k);
+ if (s == searches_.end())
+ return {};
+ return s->second.ops.get(filter);
+}
+
+Sp<Value>
+DhtProxyClient::getLocalById(const InfoHash& k, Value::Id id) const {
+ std::lock_guard<std::mutex> lock(searchLock_);
+ auto s = searches_.find(k);
+ if (s == searches_.end())
+ return {};
+ return s->second.ops.get(id);
+}
+
+void
+DhtProxyClient::cancelAllListeners()
+{
+ std::lock_guard<std::mutex> lock(searchLock_);
+ if (logger_)
+ logger_->d("[proxy:client] [listeners] [%zu searches] cancel all", searches_.size());
+ for (auto& s: searches_) {
+ s.second.ops.cancelAll([&](size_t token){
+ auto l = s.second.listeners.find(token);
+ if (l == s.second.listeners.end())
+ return;
+ l->second.opstate->stop.store(true);
+ l->second.request->cancel();
+ // implicit request.reset()
+ s.second.listeners.erase(token);
+ });
+ }
+}
+
+void
+DhtProxyClient::shutdown(ShutdownCallback cb, bool)
+{
+ stop();
+ if (cb)
+ cb();
+}
+
+NodeStatus
+DhtProxyClient::getStatus(sa_family_t af) const
+{
+ std::lock_guard<std::mutex> l(lockCurrentProxyInfos_);
+ switch (af)
+ {
+ case AF_INET:
+ return statusIpv4_;
+ case AF_INET6:
+ return statusIpv6_;
+ default:
+ return NodeStatus::Disconnected;
+ }
+}
+
+bool
+DhtProxyClient::isRunning(sa_family_t af) const
+{
+ std::lock_guard<std::mutex> l(lockCurrentProxyInfos_);
+ switch (af)
+ {
+ case AF_INET:
+ return statusIpv4_ != NodeStatus::Disconnected;
+ case AF_INET6:
+ return statusIpv6_ != NodeStatus::Disconnected;
+ default:
+ return false;
+ }
+}
+
+time_point
+DhtProxyClient::periodic(const uint8_t*, size_t, SockAddr, const time_point& /*now*/)
+{
+ // Exec all currently stored callbacks
+ decltype(callbacks_) callbacks;
+ {
+ std::lock_guard<std::mutex> lock(lockCallbacks_);
+ callbacks = std::move(callbacks_);
+ }
+ for (auto& callback : callbacks)
+ callback();
+ callbacks.clear();
+ return time_point::max();
+}
+
+void
+DhtProxyClient::setHeaderFields(http::Request& request){
+ request.set_header_field(restinio::http_field_t::accept, "*/*");
+ request.set_header_field(restinio::http_field_t::content_type, "application/json");
+}
+
+void
+DhtProxyClient::get(const InfoHash& key, GetCallback cb, DoneCallback donecb, Value::Filter&& f, Where&& w)
+{
+ if (logger_)
+ logger_->d("[proxy:client] [get] [search %s]", key.to_c_str());
+
+ if (isDestroying_) {
+ if (donecb) donecb(false, {});
+ return;
+ }
+ try {
+ auto request = buildRequest("/key/" + key.toString());
+ auto reqid = request->id();
+ //request->set_connection_type(restinio::http_connection_header_t::keep_alive);
+ request->set_method(restinio::http_method_get());
+ setHeaderFields(*request);
+
+ auto opstate = std::make_shared<OperationState>();
+
+ request->add_on_body_callback([
+ this,
+ key,
+ opstate,
+ filter = Value::Filter::chain(std::move(f), w.getFilter()),
+ rxBuf = std::make_shared<LineSplit>(),
+ cb
+ ](const char* at, size_t length){
+ try {
+ rxBuf->append(at, length);
+ // one value per body line
+ std::vector<Sp<Value>> values;
+ while (rxBuf->getLine('\n') and !opstate->stop) {
+ std::string err;
+ Json::Value json;
+ const auto& line = rxBuf->line();
+ if (!jsonReader_->parse(line.data(), line.data() + line.size(), &json, &err)){
+ opstate->ok.store(false);
+ return;
+ }
+ auto value = std::make_shared<Value>(json);
+ if ((not filter or filter(*value)) and cb)
+ values.emplace_back(std::move(value));
+ }
+ if (not values.empty() and cb) {
+ {
+ std::lock_guard<std::mutex> lock(lockCallbacks_);
+ callbacks_.emplace_back([opstate, cb, values = std::move(values)](){
+ if (not opstate->stop.load() and not cb(values)){
+ opstate->stop.store(true);
+ }
+ });
+ }
+ loopSignal_();
+ }
+ } catch(const std::exception& e) {
+ if (logger_)
+ logger_->e("[proxy:client] [get %s] body parsing error: %s", key.to_c_str(), e.what());
+ opstate->ok.store(false);
+ }
+ });
+ request->add_on_done_callback([this, reqid, opstate, donecb, key] (const http::Response& response){
+ if (response.status_code != 200) {
+ if (logger_)
+ logger_->e("[proxy:client] [get %s] failed with code=%i", key.to_c_str(), response.status_code);
+ opstate->ok.store(false);
+ if (not response.aborted and response.status_code == 0)
+ opFailed();
+ }
+ if (donecb) {
+ {
+ std::lock_guard<std::mutex> lock(lockCallbacks_);
+ callbacks_.emplace_back([donecb, opstate](){
+ donecb(opstate->ok, {});
+ opstate->stop.store(true);
+ });
+ }
+ loopSignal_();
+ }
+ if (not isDestroying_) {
+ std::lock_guard<std::mutex> l(requestLock_);
+ requests_.erase(reqid);
+ }
+ });
+ {
+ std::lock_guard<std::mutex> l(requestLock_);
+ requests_[reqid] = request;
+ }
+ request->send();
+ }
+ catch (const std::exception &e){
+ if (logger_)
+ logger_->e("[proxy:client] [get %s] error: %s", key.to_c_str(), e.what());
+ }
+}
+
+void
+DhtProxyClient::put(const InfoHash& key, Sp<Value> val, DoneCallback cb, time_point created, bool permanent)
+{
+ if (not val or isDestroying_) {
+ if (cb) cb(false, {});
+ return;
+ }
+ if (logger_)
+ logger_->d("[proxy:client] [put] [search %s]", key.to_c_str());
+
+ std::shared_ptr<std::atomic_bool> ok;
+ if (permanent) {
+ std::lock_guard<std::mutex> lock(searchLock_);
+ ok = std::make_shared<std::atomic_bool>(true);
+ auto& search = searches_[key];
+ if (val->id) {
+ auto id = val->id;
+ auto refreshPutTimer = std::make_unique<asio::steady_timer>(httpContext_, proxy::OP_TIMEOUT - proxy::OP_MARGIN);
+ refreshPutTimer->async_wait(std::bind(&DhtProxyClient::handleRefreshPut, this, std::placeholders::_1, key, id));
+ search.puts.erase(id);
+ search.puts.emplace(std::piecewise_construct,
+ std::forward_as_tuple(id),
+ std::forward_as_tuple(val, std::move(refreshPutTimer), ok));
+ } else {
+ search.pendingPuts.emplace(val);
+ }
+ }
+ doPut(key, val, [this, cb, ok](bool result){
+ if (ok)
+ *ok = result;
+ if (cb) {
+ std::lock_guard<std::mutex> lock(lockCallbacks_);
+ callbacks_.emplace_back([cb, result](){
+ cb(result, {});
+ });
+ }
+ loopSignal_();
+ }, created, permanent);
+}
+
+void
+DhtProxyClient::handleRefreshPut(const asio::error_code &ec, InfoHash key, Value::Id id)
+{
+ if (ec == asio::error::operation_aborted)
+ return;
+ else if (ec){
+ if (logger_)
+ logger_->e("[proxy:client] [put] [refresh %s] %s", key.toString().c_str(), ec.message().c_str());
+ return;
+ }
+ if (logger_)
+ logger_->d("[proxy:client] [put] [refresh %s]", key.to_c_str());
+ std::lock_guard<std::mutex> lock(searchLock_);
+ auto search = searches_.find(key);
+ if (search != searches_.end()) {
+ auto p = search->second.puts.find(id);
+ if (p != search->second.puts.end()){
+ doPut(key, p->second.value, [ok = p->second.ok](bool result){
+ *ok = result;
+ }, time_point::max(), true);
+ p->second.refreshPutTimer->expires_after(proxy::OP_TIMEOUT - proxy::OP_MARGIN);
+ p->second.refreshPutTimer->async_wait(std::bind(&DhtProxyClient::handleRefreshPut, this, std::placeholders::_1, key, id));
+ }
+ }
+}
+
+std::shared_ptr<http::Request>
+DhtProxyClient::buildRequest(const std::string& target)
+{
+ std::unique_lock<std::mutex> l(resolverLock_);
+ auto resolver = resolver_;
+ l.unlock();
+ if (not resolver)
+ resolver = std::make_shared<http::Resolver>(httpContext_, proxyUrl_, logger_);
+ auto request = target.empty()
+ ? std::make_shared<http::Request>(httpContext_, resolver)
+ : std::make_shared<http::Request>(httpContext_, resolver, target);
+ if (serverCertificate_)
+ request->set_certificate_authority(serverCertificate_);
+ if (clientIdentity_.first and clientIdentity_.second)
+ request->set_identity(clientIdentity_);
+ request->set_header_field(restinio::http_field_t::user_agent, "RESTinio client");
+ return request;
+}
+
+void
+DhtProxyClient::doPut(const InfoHash& key, Sp<Value> val, DoneCallbackSimple cb, time_point /*created*/, bool permanent)
+{
+ if (logger_)
+ logger_->debug("[proxy:client] [put] [search {}] executing for {}", key, val->toString());
+
+ try {
+ auto request = buildRequest("/key/" + key.toString());
+ auto reqid = request->id();
+ request->set_method(restinio::http_method_post());
+ setHeaderFields(*request);
+
+ auto json = val->toJson();
+ if (permanent) {
+ if (deviceKey_.empty()) {
+ json["permanent"] = true;
+ } else {
+#ifdef OPENDHT_PUSH_NOTIFICATIONS
+ Json::Value refresh;
+ getPushRequest(refresh);
+ json["permanent"] = refresh;
+#else
+ json["permanent"] = true;
+#endif
+ }
+ }
+ request->set_body(Json::writeString(jsonBuilder_, json));
+ request->add_on_done_callback([this, reqid, cb, val, key, permanent] (const http::Response& response){
+ bool ok = response.status_code == 200;
+ if (ok) {
+ if (val->id == Value::INVALID_ID) {
+ std::string err;
+ Json::Value parsedValue;
+ if (jsonReader_->parse(response.body.data(), response.body.data() + response.body.size(), &parsedValue, &err)){
+ auto id = dht::Value(parsedValue).id;
+ val->id = id;
+ if (permanent) {
+ std::lock_guard<std::mutex> lock(searchLock_);
+ auto& search = searches_[key];
+ auto it = search.pendingPuts.find(val);
+ if (it != search.pendingPuts.end()) {
+ auto sok = std::make_shared<std::atomic_bool>(ok);
+ auto refreshPutTimer = std::make_unique<asio::steady_timer>(httpContext_, proxy::OP_TIMEOUT - proxy::OP_MARGIN);
+ refreshPutTimer->async_wait(std::bind(&DhtProxyClient::handleRefreshPut, this, std::placeholders::_1, key, id));
+ search.puts.emplace(std::piecewise_construct,
+ std::forward_as_tuple(id),
+ std::forward_as_tuple(val, std::move(refreshPutTimer), sok));
+ search.pendingPuts.erase(it);
+ }
+ }
+ } else {
+ if (logger_)
+ logger_->error("[proxy:client] [put] failed to parse value from server: {}", err);
+ }
+ }
+ } else {
+ if (logger_)
+ logger_->error("[proxy:client] [put] failed with code={:d}", response.status_code);
+ if (not response.aborted and response.status_code == 0)
+ opFailed();
+ }
+ if (cb)
+ cb(ok);
+ if (not isDestroying_) {
+ std::lock_guard<std::mutex> l(requestLock_);
+ requests_.erase(reqid);
+ }
+ });
+ {
+ std::lock_guard<std::mutex> l(requestLock_);
+ requests_[reqid] = request;
+ }
+ request->send();
+ }
+ catch (const std::exception &e){
+ if (logger_)
+ logger_->error("[proxy:client] [put {}] error: {}", key, e.what());
+ opFailed();
+ }
+}
+
+/**
+ * Get data currently being put at the given hash.
+ */
+std::vector<Sp<Value>>
+DhtProxyClient::getPut(const InfoHash& key) const {
+ std::vector<Sp<Value>> ret;
+ auto search = searches_.find(key);
+ if (search != searches_.end()) {
+ ret.reserve(search->second.puts.size());
+ for (const auto& put : search->second.puts)
+ ret.emplace_back(put.second.value);
+ }
+ return ret;
+}
+
+/**
+ * Get data currently being put at the given hash with the given id.
+ */
+Sp<Value>
+DhtProxyClient::getPut(const InfoHash& key, const Value::Id& id) const {
+ auto search = searches_.find(key);
+ if (search == searches_.end())
+ return {};
+ auto val = search->second.puts.find(id);
+ if (val == search->second.puts.end())
+ return {};
+ return val->second.value;
+}
+
+/**
+ * Stop any put/announce operation at the given location,
+ * for the value with the given id.
+ */
+bool
+DhtProxyClient::cancelPut(const InfoHash& key, const Value::Id& id)
+{
+ auto search = searches_.find(key);
+ if (search == searches_.end())
+ return false;
+ if (logger_)
+ logger_->d("[proxy:client] [put] [search %s] cancel", key.to_c_str());
+ return search->second.puts.erase(id) > 0;
+}
+
+NodeStats
+DhtProxyClient::getNodesStats(sa_family_t af) const
+{
+ return af == AF_INET ? stats4_ : stats6_;
+}
+
+void
+DhtProxyClient::getProxyInfos()
+{
+ if (logger_)
+ logger_->d("[proxy:client] [info] requesting proxy server node information");
+ auto infoState = std::make_shared<InfoState>();
+ {
+ std::lock_guard<std::mutex> l(lockCurrentProxyInfos_);
+ if (infoState_)
+ infoState_->cancel = true;
+ infoState_ = infoState;
+ if (statusIpv4_ == NodeStatus::Disconnected)
+ statusIpv4_ = NodeStatus::Connecting;
+ if (statusIpv6_ == NodeStatus::Disconnected)
+ statusIpv6_ = NodeStatus::Connecting;
+ }
+ if (logger_)
+ logger_->d("[proxy:client] [status] sending request");
+
+ auto resolver = std::make_shared<http::Resolver>(httpContext_, proxyUrl_, logger_);
+ queryProxyInfo(infoState, resolver, AF_INET);
+ queryProxyInfo(infoState, resolver, AF_INET6);
+ std::lock_guard<std::mutex> l(resolverLock_);
+ resolver_ = resolver;
+}
+
+void
+DhtProxyClient::queryProxyInfo(const Sp<InfoState>& infoState, const Sp<http::Resolver>& resolver, sa_family_t family)
+{
+ if (logger_)
+ logger_->d("[proxy:client] [status] query ipv%i info", family == AF_INET ? 4 : 6);
+ try {
+ auto request = std::make_shared<http::Request>(httpContext_, resolver, family);
+ if (serverCertificate_)
+ request->set_certificate_authority(serverCertificate_);
+ auto reqid = request->id();
+ request->set_method(restinio::http_method_get());
+ setHeaderFields(*request);
+ request->add_on_done_callback([this, reqid, family, infoState] (const http::Response& response){
+ if (infoState->cancel.load())
+ return;
+ if (response.status_code != 200) {
+ if (logger_)
+ logger_->e("[proxy:client] [status] ipv%i failed with code=%i",
+ family == AF_INET ? 4 : 6, response.status_code);
+ // pass along the failures
+ if ((family == AF_INET and infoState->ipv4 == 0) or (family == AF_INET6 and infoState->ipv6 == 0))
+ onProxyInfos(Json::Value{}, family);
+ } else {
+ std::string err;
+ Json::Value proxyInfos;
+ if (!jsonReader_->parse(response.body.data(), response.body.data() + response.body.size(), &proxyInfos, &err)){
+ onProxyInfos(Json::Value{}, family);
+ } else if (not infoState->cancel) {
+ if (auto req = response.request.lock()) {
+ if (auto conn = req->get_connection()) {
+ const auto& localAddr = conn->local_address();
+ proxyInfos["local_ip"] = localAddr.to_string();
+ }
+ }
+ onProxyInfos(proxyInfos, family);
+ }
+ }
+ if (not isDestroying_) {
+ std::lock_guard<std::mutex> l(requestLock_);
+ requests_.erase(reqid);
+ }
+ });
+
+ if (infoState->cancel.load())
+ return;
+ {
+ std::lock_guard<std::mutex> l(requestLock_);
+ requests_[reqid] = request;
+ }
+ request->send();
+ }
+ catch (const std::exception &e){
+ if (logger_)
+ logger_->e("[proxy:client] [status] error sending request: %s", e.what());
+ }
+}
+
+void
+DhtProxyClient::onProxyInfos(const Json::Value& proxyInfos, const sa_family_t family)
+{
+ if (isDestroying_)
+ return;
+ std::unique_lock<std::mutex> l(lockCurrentProxyInfos_);
+ auto oldStatus = std::max(statusIpv4_, statusIpv6_);
+ auto& status = family == AF_INET ? statusIpv4_ : statusIpv6_;
+ auto ipChanged = false;
+ auto& pubAddress = family == AF_INET? publicAddressV4_ : publicAddressV6_;
+ auto& localAddress = family == AF_INET? localAddrv4_ : localAddrv6_;
+ if (not proxyInfos.isMember("node_id")) {
+ if (logger_)
+ logger_->e("[proxy:client] [info] request failed for %s", family == AF_INET ? "ipv4" : "ipv6");
+ status = NodeStatus::Disconnected;
+ if (pubAddress) {
+ pubAddress = {};
+ ipChanged = true;
+ }
+ } else {
+ if (logger_)
+ logger_->debug("[proxy:client] [info] got proxy reply for {}",
+ family == AF_INET ? "ipv4" : "ipv6");
+ try {
+ myid = InfoHash(proxyInfos["node_id"].asString());
+ stats4_ = NodeStats(proxyInfos["ipv4"]);
+ stats6_ = NodeStats(proxyInfos["ipv6"]);
+ auto publicIp = parsePublicAddress(proxyInfos["public_ip"]);
+ ipChanged = pubAddress && pubAddress.toString() != publicIp.toString();
+ bool pub = ipChanged || !pubAddress;
+ pubAddress = publicIp;
+
+ if (proxyInfos.isMember("local_ip")) {
+ std::string localIp = proxyInfos["local_ip"].asString();
+ if (localAddress.toString() != localIp) {
+ localAddress.setAddress(localIp.c_str());
+ ipChanged = (bool)localAddress;
+ }
+ }
+
+ if (pub && publicAddressChangedCb_) {
+ std::vector<SockAddr> addresses;
+ if (publicAddressV4_)
+ addresses.emplace_back(publicAddressV4_);
+ if (publicAddressV6_)
+ addresses.emplace_back(publicAddressV6_);
+ std::lock_guard<std::mutex> lock(lockCallbacks_);
+ callbacks_.emplace_back([cb=publicAddressChangedCb_, addresses = std::move(addresses)](){
+ cb(std::move(addresses));
+ });
+ }
+
+ if (!ipChanged && stats4_.good_nodes + stats6_.good_nodes)
+ status = NodeStatus::Connected;
+ else if (!ipChanged && stats4_.dubious_nodes + stats6_.dubious_nodes)
+ status = NodeStatus::Connecting;
+ else
+ status = NodeStatus::Disconnected;
+
+ } catch (const std::exception& e) {
+ if (logger_)
+ logger_->e("[proxy:client] [info] error processing: %s", e.what());
+ }
+ }
+ auto newStatus = std::max(statusIpv4_, statusIpv6_);
+ if (newStatus == NodeStatus::Connected) {
+ if (oldStatus == NodeStatus::Disconnected || oldStatus == NodeStatus::Connecting || launchConnectedCbs_) {
+ launchConnectedCbs_ = false;
+ listenerRestartTimer_->expires_at(std::chrono::steady_clock::now());
+ listenerRestartTimer_->async_wait(std::bind(&DhtProxyClient::restartListeners, this, std::placeholders::_1));
+ if (not onConnectCallbacks_.empty()) {
+ std::lock_guard<std::mutex> lock(lockCallbacks_);
+ callbacks_.emplace_back([cbs = std::move(onConnectCallbacks_)]() mutable {
+ while (not cbs.empty()) {
+ cbs.front()();
+ cbs.pop();
+ }
+ });
+ }
+ }
+ nextProxyConfirmationTimer_->expires_at(std::chrono::steady_clock::now() + std::chrono::minutes(15));
+ nextProxyConfirmationTimer_->async_wait(std::bind(&DhtProxyClient::handleProxyConfirm, this, std::placeholders::_1));
+ }
+ else if (newStatus == NodeStatus::Disconnected) {
+ auto next = std::chrono::steady_clock::now();
+ if (!ipChanged)
+ next += std::chrono::minutes(1);
+ nextProxyConfirmationTimer_->expires_at(next);
+ nextProxyConfirmationTimer_->async_wait(std::bind(&DhtProxyClient::handleProxyConfirm, this, std::placeholders::_1));
+ }
+ l.unlock();
+ loopSignal_();
+}
+
+SockAddr
+DhtProxyClient::parsePublicAddress(const Json::Value& val)
+{
+ auto public_ip = val.asString();
+ auto hostAndService = splitPort(public_ip);
+ auto sa = SockAddr::resolve(hostAndService.first);
+ if (sa.empty()) return {};
+ return sa.front().getMappedIPv4();
+}
+
+std::vector<SockAddr>
+DhtProxyClient::getPublicAddress(sa_family_t family)
+{
+ std::lock_guard<std::mutex> l(lockCurrentProxyInfos_);
+ std::vector<SockAddr> result;
+ if (publicAddressV6_ && family != AF_INET) result.emplace_back(publicAddressV6_);
+ if (publicAddressV4_ && family != AF_INET6) result.emplace_back(publicAddressV4_);
+ return result;
+}
+
+size_t
+DhtProxyClient::listen(const InfoHash& key, ValueCallback cb, Value::Filter filter, Where where)
+{
+ if (logger_)
+ logger_->d("[proxy:client] [listen] [search %s]", key.to_c_str());
+ if (isDestroying_)
+ return 0;
+
+ std::lock_guard<std::mutex> lock(searchLock_);
+ auto& search = searches_[key];
+ auto query = std::make_shared<Query>(Select{}, std::move(where));
+ return search.ops.listen(cb, query, std::move(filter), [this, key](Sp<Query>, ValueCallback cb, SyncCallback) -> size_t {
+ // Find search
+ auto search = searches_.find(key);
+ if (search == searches_.end()) {
+ if (logger_)
+ logger_->e("[proxy:client] [listen] [search %s] search not found", key.to_c_str());
+ return 0;
+ }
+ if (logger_)
+ logger_->d("[proxy:client] [listen] [search %s] sending %s", key.to_c_str(),
+ deviceKey_.empty() ? "listen" : "subscribe");
+ // Add listener
+ auto token = ++listenerToken_;
+ auto l = search->second.listeners.find(token);
+ if (l == search->second.listeners.end()) {
+ l = search->second.listeners.emplace(std::piecewise_construct,
+ std::forward_as_tuple(token),
+ std::forward_as_tuple(std::move(cb))).first;
+ } else {
+ if (l->second.opstate)
+ l->second.opstate->stop = true;
+ }
+ // Add cache callback
+ auto opstate = std::make_shared<OperationState>();
+ l->second.opstate = opstate;
+ l->second.cb = [this,key,token,opstate](const std::vector<Sp<Value>>& values, bool expired, system_clock::time_point t){
+ if (opstate->stop)
+ return false;
+ std::lock_guard<std::mutex> lock(searchLock_);
+ auto s = searches_.find(key);
+ if (s != searches_.end()) {
+ auto l = s->second.listeners.find(token);
+ if (l != s->second.listeners.end()) {
+ return l->second.cache.onValue(values, expired, t);
+ }
+ }
+ return false;
+ };
+#ifdef OPENDHT_PUSH_NOTIFICATIONS
+ if (not deviceKey_.empty()) {
+ /*
+ * Relaunch push listeners even if a timeout is not received
+ * (if the proxy crash for any reason)
+ */
+ if (!l->second.refreshSubscriberTimer)
+ l->second.refreshSubscriberTimer = std::make_unique<asio::steady_timer>(httpContext_);
+ l->second.refreshSubscriberTimer->expires_at(std::chrono::steady_clock::now() +
+ proxy::OP_TIMEOUT - proxy::OP_MARGIN);
+ l->second.refreshSubscriberTimer->async_wait(std::bind(&DhtProxyClient::handleResubscribe, this,
+ std::placeholders::_1, key, token, opstate));
+ }
+#endif
+ ListenMethod method;
+ restinio::http_request_header_t header;
+ if (deviceKey_.empty()){ // listen
+ method = ListenMethod::LISTEN;
+ header.method(restinio::http_method_get());
+ header.request_target("/key/" + key.toString() + "/listen");
+ }
+ else {
+ method = ListenMethod::SUBSCRIBE;
+ header.method(restinio::http_method_subscribe());
+ header.request_target("/key/" + key.toString());
+ }
+ sendListen(header, l->second.cb, opstate, l->second, method);
+ return token;
+ });
+}
+
+void
+DhtProxyClient::handleResubscribe(const asio::error_code &ec, const InfoHash& key,
+ const size_t token, std::shared_ptr<OperationState> opstate)
+{
+ if (ec == asio::error::operation_aborted)
+ return;
+ else if (ec){
+ if (logger_)
+ logger_->e("[proxy:client] [resubscribe %s] %s", key.toString().c_str(), ec.message().c_str());
+ return;
+ }
+ if (opstate->stop)
+ return;
+ std::lock_guard<std::mutex> lock(searchLock_);
+ auto s = searches_.find(key);
+ if (s != searches_.end()){
+ auto l = s->second.listeners.find(token);
+ if (l != s->second.listeners.end()) {
+ resubscribe(key, token, l->second);
+ }
+ else {
+ if (logger_)
+ logger_->e("[proxy:client] [resubscribe %s] token not found", key.toString().c_str());
+ }
+ }
+}
+
+bool
+DhtProxyClient::cancelListen(const InfoHash& key, size_t gtoken)
+{
+ if (logger_)
+ logger_->d(key, "[proxy:client] [search %s] cancel listen %zu", key.to_c_str(), gtoken);
+
+ std::lock_guard<std::mutex> lock(searchLock_);
+ // find the listener in cache
+ auto it = searches_.find(key);
+ if (it == searches_.end())
+ return false;
+ auto& ops = it->second.ops;
+ bool canceled = ops.cancelListen(gtoken, std::chrono::steady_clock::now());
+
+ // define real cancel listen only once
+ if (not it->second.opExpirationTimer)
+ it->second.opExpirationTimer = std::make_unique<asio::steady_timer>(httpContext_, ops.getExpiration());
+ else
+ it->second.opExpirationTimer->expires_at(ops.getExpiration());
+ it->second.opExpirationTimer->async_wait(std::bind(&DhtProxyClient::handleExpireListener, this, std::placeholders::_1, key));
+ return canceled;
+}
+
+void
+DhtProxyClient::handleExpireListener(const asio::error_code &ec, const InfoHash& key)
+{
+ if (ec == asio::error::operation_aborted)
+ return;
+ else if (ec){
+ if (logger_)
+ logger_->e("[proxy:client] [listen %s] error in cancel: %s", key.toString().c_str(), ec.message().c_str());
+ return;
+ }
+ if (logger_)
+ logger_->d("[proxy:client] [listen %s] expire listener", key.toString().c_str());
+
+ std::lock_guard<std::mutex> lock(searchLock_);
+ auto search = searches_.find(key);
+ if (search == searches_.end())
+ return;
+
+ // everytime a new expiry is set, a previous gets aborted
+ time_point next = search->second.ops.expire(std::chrono::steady_clock::now(), [&](size_t ltoken) {
+ auto it = search->second.listeners.find(ltoken);
+ if (it == search->second.listeners.end())
+ return;
+
+ auto& listener = it->second;
+ listener.opstate->stop = true;
+
+ if (not deviceKey_.empty()) {
+ // UNSUBSCRIBE
+ auto request = buildRequest("/key/" + key.toString());
+ auto reqid = request->id();
+ try {
+ request->set_method(restinio::http_method_unsubscribe());
+ setHeaderFields(*request);
+
+ Json::Value body;
+ body["key"] = deviceKey_;
+ body["client_id"] = pushClientId_;
+ request->set_body(Json::writeString(jsonBuilder_, body));
+ request->add_on_done_callback([this, reqid, key] (const http::Response& response){
+ if (response.status_code != 200) {
+ if (logger_)
+ logger_->e("[proxy:client] [unsubscribe %s] failed with code=%i",
+ key.to_c_str(), response.status_code);
+ if (not response.aborted and response.status_code == 0)
+ opFailed();
+ }
+ if (not isDestroying_) {
+ std::lock_guard<std::mutex> l(requestLock_);
+ requests_.erase(reqid);
+ }
+ });
+ {
+ std::lock_guard<std::mutex> l(requestLock_);
+ requests_[reqid] = request;
+ }
+ request->send();
+ }
+ catch (const std::exception &e){
+ if (logger_)
+ logger_->e("[proxy:client] [unsubscribe %s] failed: %s", key.to_c_str(), e.what());
+ }
+ } else {
+ // stop the request
+ listener.request.reset();
+ }
+ search->second.listeners.erase(it);
+ if (logger_)
+ logger_->d("[proxy:client] [listen:cancel] [search %s] %zu listener remaining",
+ key.to_c_str(), search->second.listeners.size());
+ });
+ if (next != time_point::max()){
+ search->second.opExpirationTimer->expires_at(next);
+ search->second.opExpirationTimer->async_wait(std::bind(
+ &DhtProxyClient::handleExpireListener, this, std::placeholders::_1, key));
+ }
+ if (search->second.listeners.empty()){
+ searches_.erase(search);
+ }
+}
+
+void
+DhtProxyClient::sendListen(const restinio::http_request_header_t& header,
+ const CacheValueCallback& cb,
+ const Sp<OperationState>& opstate,
+ Listener& listener, ListenMethod method)
+{
+ if (logger_)
+ logger_->e("[proxy:client] [listen] sendListen: %d", (int)method);
+ try {
+ auto request = buildRequest();
+ listener.request = request;
+ auto reqid = request->id();
+ request->set_header(header);
+ setHeaderFields(*request);
+ if (method == ListenMethod::LISTEN)
+ request->set_connection_type(restinio::http_connection_header_t::keep_alive);
+#ifdef OPENDHT_PUSH_NOTIFICATIONS
+ std::string body;
+ if (method != ListenMethod::LISTEN)
+ body = fillBody(method == ListenMethod::RESUBSCRIBE);
+ request->set_body(body);
+#endif
+ auto rxBuf = std::make_shared<LineSplit>();
+ request->add_on_body_callback([this, reqid, opstate, rxBuf, cb](const char* at, size_t length){
+ try {
+ auto& b = *rxBuf;
+ b.append(at, length);
+
+ // one value per body line
+ while (b.getLine('\n') and !opstate->stop) {
+ std::string err;
+ Json::Value json;
+ const auto& line = b.line();
+ if (!jsonReader_->parse(line.data(), line.data() + line.size(), &json, &err)){
+ opstate->ok.store(false);
+ return;
+ }
+ if (json.size() == 0) { // it's the end
+ break;
+ }
+
+ auto value = std::make_shared<Value>(json);
+ if (cb){
+ auto expired = json.get("expired", Json::Value(false)).asBool();
+ {
+ std::lock_guard<std::mutex> lock(lockCallbacks_);
+ callbacks_.emplace_back([cb, value, opstate, expired]() {
+ if (not opstate->stop.load() and not cb({value}, expired, system_clock::time_point::min()))
+ opstate->stop.store(true);
+ });
+ }
+ loopSignal_();
+ }
+ }
+ } catch(const std::exception& e) {
+ if (logger_)
+ logger_->e("[proxy:client] [listen] request #%i error in parsing: %s", reqid, e.what());
+ opstate->ok.store(false);
+ }
+ });
+ request->add_on_done_callback([this, opstate, reqid] (const http::Response& response) {
+ if (response.status_code != 200) {
+ if (logger_)
+ logger_->e("[proxy:client] [listen] send request #%i failed with code=%i",
+ reqid, response.status_code);
+ opstate->ok.store(false);
+ if (not response.aborted and response.status_code == 0)
+ opFailed();
+ }
+ if (not isDestroying_) {
+ std::lock_guard<std::mutex> l(requestLock_);
+ requests_.erase(reqid);
+ }
+ });
+ {
+ std::lock_guard<std::mutex> l(requestLock_);
+ requests_[reqid] = request;
+ }
+ request->send();
+ }
+ catch (const std::exception &e){
+ if (logger_)
+ logger_->e("[proxy:client] [listen] request failed: %s", e.what());
+ }
+}
+
+void
+DhtProxyClient::opFailed()
+{
+ if (isDestroying_)
+ return;
+ if (logger_)
+ logger_->e("[proxy:client] proxy request failed");
+ {
+ std::lock_guard<std::mutex> l(lockCurrentProxyInfos_);
+ statusIpv4_ = NodeStatus::Disconnected;
+ statusIpv6_ = NodeStatus::Disconnected;
+ }
+ getConnectivityStatus();
+ loopSignal_();
+}
+
+void
+DhtProxyClient::getConnectivityStatus()
+{
+ if (logger_)
+ logger_->d("[proxy:client] [connectivity] get status");
+ if (!isDestroying_)
+ getProxyInfos();
+}
+
+void
+DhtProxyClient::restartListeners(const asio::error_code &ec)
+{
+ if (ec == asio::error::operation_aborted)
+ return;
+ else if (ec){
+ if (logger_)
+ logger_->e("[proxy:client] restart error: %s", ec.message().c_str());
+ return;
+ }
+
+ if (isDestroying_)
+ return;
+ if (logger_)
+ logger_->d("[proxy:client] [listeners] refresh permanent puts");
+
+ std::lock_guard<std::mutex> lock(searchLock_);
+ for (auto& search : searches_) {
+ const auto& key = search.first;
+ for (auto& put : search.second.puts) {
+ doPut(key, put.second.value, [ok = put.second.ok](bool result){
+ *ok = result;
+ }, time_point::max(), true);
+ if (!put.second.refreshPutTimer) {
+ put.second.refreshPutTimer = std::make_unique<asio::steady_timer>(httpContext_);
+ }
+ put.second.refreshPutTimer->expires_at(std::chrono::steady_clock::now() + proxy::OP_TIMEOUT - proxy::OP_MARGIN);
+ put.second.refreshPutTimer->async_wait(std::bind(&DhtProxyClient::handleRefreshPut, this,
+ std::placeholders::_1, key, put.first));
+ }
+ // Restart failed pending puts
+ for (auto& pput : search.second.pendingPuts) {
+ doPut(key, pput, {}, time_point::max(), true);
+ }
+ }
+ if (not deviceKey_.empty()) {
+ if (logger_)
+ logger_->d("[proxy:client] [listeners] resubscribe due to a connectivity change");
+ // Connectivity changed, refresh all subscribe
+ for (auto& search : searches_)
+ for (auto& listener : search.second.listeners)
+ if (!listener.second.opstate->ok)
+ resubscribe(search.first, listener.first, listener.second);
+ return;
+ }
+ if (logger_)
+ logger_->d("[proxy:client] [listeners] restarting listeners");
+ for (auto& search: searches_) {
+ for (auto& l: search.second.listeners) {
+ auto& listener = l.second;
+ if (auto opstate = listener.opstate)
+ opstate->stop = true;
+ listener.request->cancel();
+ listener.request.reset();
+ }
+ }
+ for (auto& search: searches_) {
+ for (auto& l: search.second.listeners) {
+ auto& listener = l.second;
+ auto opstate = listener.opstate;
+ // Redo listen
+ opstate->stop.store(false);
+ opstate->ok.store(true);
+ auto cb = listener.cb;
+ // define header
+ restinio::http_request_header_t header;
+ header.method(restinio::http_method_get());
+ header.request_target("/key/" + search.first.toString() + "/listen");
+ sendListen(header, cb, opstate, listener, ListenMethod::LISTEN);
+ }
+ }
+}
+
+void
+DhtProxyClient::pushNotificationReceived([[maybe_unused]] const std::map<std::string, std::string>& notification)
+{
+#ifdef OPENDHT_PUSH_NOTIFICATIONS
+ {
+ // If a push notification is received, the proxy is up and running
+ std::lock_guard<std::mutex> l(lockCurrentProxyInfos_);
+ auto oldStatus = std::max(statusIpv4_, statusIpv6_);
+ if (oldStatus != NodeStatus::Connected)
+ launchConnectedCbs_ = true;
+ statusIpv4_ = NodeStatus::Connected;
+ statusIpv6_ = NodeStatus::Connected;
+ }
+ auto launchLoop = false;
+ try {
+ auto sessionId = notification.find("s");
+ if (sessionId != notification.end() and sessionId->second != pushSessionId_) {
+ if (logger_)
+ logger_->d("[proxy:client] [push] ignoring push for other session");
+ return;
+ }
+ std::lock_guard<std::mutex> lock(searchLock_);
+ auto timeout = notification.find("timeout");
+ if (timeout != notification.cend()) {
+ InfoHash key(timeout->second);
+ auto& search = searches_.at(key);
+ auto vidIt = notification.find("vid");
+ if (vidIt != notification.end()) {
+ // Refresh put
+ auto vid = std::stoull(vidIt->second);
+ auto& put = search.puts.at(vid);
+ if (!put.refreshPutTimer)
+ put.refreshPutTimer = std::make_unique<asio::steady_timer>(httpContext_, std::chrono::steady_clock::now());
+ else
+ put.refreshPutTimer->expires_at(std::chrono::steady_clock::now());
+ put.refreshPutTimer->async_wait(std::bind(&DhtProxyClient::handleRefreshPut, this, std::placeholders::_1, key, vid));
+ } else {
+ // Refresh listen
+ for (auto& list : search.listeners)
+ resubscribe(key, list.first, list.second);
+ }
+ } else {
+ auto key = InfoHash(notification.at("key"));
+ system_clock::time_point sendTime = system_clock::time_point::min();
+ try {
+ sendTime = system_clock::time_point(std::chrono::milliseconds(std::stoull(notification.at("t"))));
+ } catch (...) {}
+ auto& search = searches_.at(key);
+ for (auto& list : search.listeners) {
+ if (list.second.opstate->stop)
+ continue;
+ if (logger_)
+ logger_->d("[proxy:client] [push] [search %s] received", key.to_c_str());
+ auto expired = notification.find("exp");
+ auto token = list.first;
+ auto opstate = list.second.opstate;
+ if (expired == notification.end()) {
+ auto cb = list.second.cb;
+ auto oldValues = list.second.cache.getValues();
+ get(key, [cb, sendTime](const std::vector<Sp<Value>>& vals) {
+ return cb(vals, false, sendTime);
+ }, [cb, oldValues, sendTime](bool /*ok*/) {
+ // Decrement old values refcount to expire values not
+ // present in the new list
+ cb(oldValues, true, sendTime);
+ });
+ } else {
+ std::stringstream ss(expired->second);
+ std::vector<Value::Id> ids;
+ while(ss.good()) {
+ std::string substr;
+ getline(ss, substr, ',');
+ ids.emplace_back(std::stoull(substr));
+ }
+ {
+ std::lock_guard<std::mutex> lockCb(lockCallbacks_);
+ callbacks_.emplace_back([this, key, token, opstate, ids, sendTime]() {
+ if (opstate->stop)
+ return;
+ std::lock_guard<std::mutex> lock(searchLock_);
+ auto s = searches_.find(key);
+ if (s == searches_.end())
+ return;
+ auto l = s->second.listeners.find(token);
+ if (l == s->second.listeners.end())
+ return;
+ if (not opstate->stop and not l->second.cache.onValuesExpired(ids, sendTime))
+ opstate->stop = true;
+ });
+ }
+ launchLoop = true;
+ }
+ }
+ }
+ } catch (const std::exception& e) {
+ if (logger_)
+ logger_->e("[proxy:client] [push] receive error: %s", e.what());
+ }
+ if (launchLoop)
+ loopSignal_();
+#endif
+}
+
+void
+DhtProxyClient::resubscribe([[maybe_unused]] const InfoHash& key, [[maybe_unused]] const size_t token, [[maybe_unused]] Listener& listener)
+{
+#ifdef OPENDHT_PUSH_NOTIFICATIONS
+ if (deviceKey_.empty())
+ return;
+ if (logger_)
+ logger_->d("[proxy:client] [resubscribe] [search %s]", key.to_c_str());
+
+ auto opstate = listener.opstate;
+ listener.request.reset(); // This will update ok to false
+ opstate->ok = true;
+
+ restinio::http_request_header_t header;
+ header.method(restinio::http_method_subscribe());
+ header.request_target("/" + key.toString());
+ if (!listener.refreshSubscriberTimer){
+ listener.refreshSubscriberTimer = std::make_unique<asio::steady_timer>(httpContext_);
+ }
+ listener.refreshSubscriberTimer->expires_at(std::chrono::steady_clock::now() +
+ proxy::OP_TIMEOUT - proxy::OP_MARGIN);
+ listener.refreshSubscriberTimer->async_wait(std::bind(&DhtProxyClient::handleResubscribe, this,
+ std::placeholders::_1, key, token, opstate));
+ auto vcb = listener.cb;
+ sendListen(header, vcb, opstate, listener, ListenMethod::RESUBSCRIBE);
+#endif
+}
+
+#ifdef OPENDHT_PUSH_NOTIFICATIONS
+void
+DhtProxyClient::getPushRequest(Json::Value& body) const
+{
+ body["key"] = deviceKey_;
+ body["client_id"] = pushClientId_;
+ body["session_id"] = pushSessionId_;
+ body["topic"] = notificationTopic_;
+ body["platform"] = platform_;
+}
+
+std::string
+DhtProxyClient::fillBody(bool resubscribe)
+{
+ // Fill body with
+ // {
+ // "key":"device_key",
+ // }
+ Json::Value body;
+ getPushRequest(body);
+ if (resubscribe) {
+ // This is the first listen, we want to retrieve previous values.
+ body["refresh"] = true;
+ }
+ auto content = Json::writeString(jsonBuilder_, body) + "\n";
+ std::replace(content.begin(), content.end(), '\n', ' ');
+ return content;
+}
+#endif // OPENDHT_PUSH_NOTIFICATIONS
+
+} // namespace dht
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ * Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com>
+ * Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ * Vsevolod Ivanov <vsevolod.ivanov@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "dht_proxy_server.h"
+
+#include "default_types.h"
+#include "dhtrunner.h"
+
+#include <msgpack.hpp>
+#include <json/json.h>
+
+#include <chrono>
+#include <functional>
+#include <limits>
+#include <iostream>
+#include <fstream>
+
+using namespace std::placeholders;
+using namespace std::chrono_literals;
+using namespace std::literals;
+
+namespace dht {
+constexpr char RESP_MSG_JSON_INCORRECT[] = "{\"err:\":\"Incorrect JSON\"}";
+constexpr char RESP_MSG_SERVICE_UNAVAILABLE[] = "{\"err\":\"Incorrect DhtRunner\"}";
+constexpr char RESP_MSG_INTERNAL_SERVER_ERRROR[] = "{\"err\":\"Internal server error\"}";
+constexpr char RESP_MSG_MISSING_PARAMS[] = "{\"err\":\"Missing parameters\"}";
+constexpr char RESP_MSG_PUT_FAILED[] = "{\"err\":\"Put failed\"}";
+#ifdef OPENDHT_PROXY_SERVER_IDENTITY
+constexpr char RESP_MSG_DESTINATION_NOT_FOUND[] = "{\"err\":\"No destination found\"}";
+#endif
+#ifdef OPENDHT_PUSH_NOTIFICATIONS
+constexpr char RESP_MSG_NO_TOKEN[] = "{\"err\":\"No token\"}";
+#endif
+
+constexpr const std::chrono::minutes PRINT_STATS_PERIOD {2};
+
+using ResponseByParts = restinio::chunked_output_t;
+using ResponseByPartsBuilder = restinio::response_builder_t<ResponseByParts>;
+
+class opendht_logger_t
+{
+public:
+ opendht_logger_t(std::shared_ptr<Logger> logger = {}) : m_logger(std::move(logger)) {}
+
+ template <typename Builder>
+ void trace(Builder&& /* msg_builder */) {
+ /* if (m_logger) m_logger->d("[proxy:server] %s", msg_builder().c_str()); */
+ }
+
+ template <typename Builder>
+ void info(Builder&& msg_builder) {
+ if (m_logger) m_logger->d("[proxy:server] %s", msg_builder().c_str());
+ }
+
+ template <typename Builder>
+ void warn(Builder&& msg_builder) {
+ if (m_logger) m_logger->w("[proxy:server] %s", msg_builder().c_str());
+ }
+
+ template <typename Builder>
+ void error(Builder&& msg_builder) {
+ if (m_logger) m_logger->e("[proxy:server] %s", msg_builder().c_str());
+ }
+
+private:
+ std::shared_ptr<Logger> m_logger;
+};
+
+restinio::request_handling_status_t
+DhtProxyServer::serverError(restinio::request_t& request) {
+ auto response = initHttpResponse(request.create_response(restinio::status_internal_server_error()));
+ response.set_body(RESP_MSG_INTERNAL_SERVER_ERRROR);
+ return response.done();
+}
+
+// connection listener
+
+class DhtProxyServer::ConnectionListener
+{
+public:
+ ConnectionListener() {};
+ explicit ConnectionListener(std::function<void(restinio::connection_id_t)> onClosed) : onClosed_(std::move(onClosed)) {};
+ ~ConnectionListener() {};
+
+ /**
+ * Connection state change used to handle Listeners disconnects.
+ * RESTinio >= 0.5.1 https://github.com/Stiffstream/restinio/issues/28
+ */
+ void state_changed(const restinio::connection_state::notice_t& notice) noexcept;
+
+private:
+ std::function<void(restinio::connection_id_t)> onClosed_;
+};
+
+void
+DhtProxyServer::ConnectionListener::state_changed(const restinio::connection_state::notice_t& notice) noexcept
+{
+ if (restinio::holds_alternative<restinio::connection_state::closed_t>(notice.cause())) {
+ onClosed_(notice.connection_id());
+ }
+}
+
+void
+DhtProxyServer::onConnectionClosed(restinio::connection_id_t id)
+{
+ std::lock_guard<std::mutex> lock(lockListener_);
+ auto it = listeners_.find(id);
+ if (it != listeners_.end()) {
+ dht_->cancelListen(it->second.hash, std::move(it->second.token));
+ listeners_.erase(it);
+ if (logger_)
+ logger_->d("[proxy:server] [connection:%li] listener cancelled, %li still connected", id, listeners_.size());
+ }
+}
+
+struct DhtProxyServer::RestRouterTraitsTls : public restinio::default_tls_traits_t
+{
+ using timer_manager_t = restinio::asio_timer_manager_t;
+ using logger_t = opendht_logger_t;
+ using request_handler_t = RestRouter;
+ using connection_state_listener_t = ConnectionListener;
+};
+struct DhtProxyServer::RestRouterTraits : public restinio::default_traits_t
+{
+ using timer_manager_t = restinio::asio_timer_manager_t;
+ using logger_t = opendht_logger_t;
+ using request_handler_t = RestRouter;
+ using connection_state_listener_t = ConnectionListener;
+};
+
+void
+DhtProxyServer::PermanentPut::msgpack_unpack(const msgpack::object& o)
+{
+ if (auto cid = findMapValue(o, "cid"sv)) {
+ clientId = cid->as<std::string>();
+ }
+ if (auto exp = findMapValue(o, "exp"sv)) {
+ expiration = from_time_t(exp->as<time_t>());
+ }
+ if (auto token = findMapValue(o, "token"sv)) {
+ pushToken = token->as<std::string>();
+ }
+ if (auto sid = findMapValue(o, "sid"sv)) {
+ if (not sessionCtx)
+ sessionCtx = std::make_shared<PushSessionContext>(sid->as<std::string>());
+ else
+ sessionCtx->sessionId = sid->as<std::string>();
+ }
+ if (auto t = findMapValue(o, "t"sv)) {
+ type = t->as<PushType>();
+ }
+ if (auto val = findMapValue(o, "value"sv)) {
+ value = std::make_shared<dht::Value>(*val);
+ }
+ if (auto top = findMapValue(o, "top"sv)) {
+ topic = top->as<std::string>();
+ }
+}
+
+#ifdef OPENDHT_PUSH_NOTIFICATIONS
+void
+DhtProxyServer::Listener::msgpack_unpack(const msgpack::object& o)
+{
+ if (auto cid = findMapValue(o, "cid"sv)) {
+ clientId = cid->as<std::string>();
+ }
+ if (auto exp = findMapValue(o, "exp"sv)) {
+ expiration = from_time_t(exp->as<time_t>());
+ }
+ if (auto sid = findMapValue(o, "sid"sv)) {
+ if (not sessionCtx)
+ sessionCtx = std::make_shared<PushSessionContext>(sid->as<std::string>());
+ else
+ sessionCtx->sessionId = sid->as<std::string>();
+ }
+ if (auto t = findMapValue(o, "t"sv)) {
+ type = t->as<PushType>();
+ }
+ if (auto top = findMapValue(o, "top"sv)) {
+ topic = top->as<std::string>();
+ }
+}
+#endif
+
+DhtProxyServer::DhtProxyServer(const std::shared_ptr<DhtRunner>& dht,
+ const ProxyServerConfig& config,
+ const std::shared_ptr<dht::Logger>& logger
+)
+ : ioContext_(std::make_shared<asio::io_context>()),
+ dht_(dht), persistPath_(config.persistStatePath), logger_(logger),
+ printStatsTimer_(std::make_unique<asio::steady_timer>(*ioContext_, 3s)),
+ connListener_(std::make_shared<ConnectionListener>(std::bind(&DhtProxyServer::onConnectionClosed, this, std::placeholders::_1))),
+ pushServer_(config.pushServer),
+ bundleId_(config.bundleId)
+{
+ if (not dht_)
+ throw std::invalid_argument("A DHT instance must be provided");
+
+ if (logger_)
+ logger_->d("[proxy:server] [init] running on %i", config.port);
+ if (not pushServer_.empty()){
+#ifdef OPENDHT_PUSH_NOTIFICATIONS
+ if (logger_)
+ logger_->d("[proxy:server] [init] using push server %s", pushServer_.c_str());
+#else
+ if (logger_)
+ logger_->e("[proxy:server] [init] opendht built without push notification support");
+#endif
+ }
+
+ jsonBuilder_["commentStyle"] = "None";
+ jsonBuilder_["indentation"] = "";
+
+ if (!pushServer_.empty()){
+ // no host delim, assume port only
+ if (pushServer_.find(":") == std::string::npos)
+ pushServer_ = "localhost:" + pushServer_;
+ // define http request destination for push notifications
+ pushHostPort_ = splitPort(pushServer_);
+ if (logger_)
+ logger_->d("Using push server for notifications: %s:%s", pushHostPort_.first.c_str(),
+ pushHostPort_.second.c_str());
+ }
+ if (config.identity.first and config.identity.second) {
+ asio::error_code ec;
+ // define tls context
+ asio::ssl::context tls_context { asio::ssl::context::sslv23 };
+ tls_context.set_options(asio::ssl::context::default_workarounds
+ | asio::ssl::context::no_sslv2
+ | asio::ssl::context::single_dh_use, ec);
+ if (ec)
+ throw std::runtime_error("Error setting tls context options: " + ec.message());
+ // add more security options
+#ifdef SSL_OP_NO_RENEGOTIATION
+ SSL_CTX_set_options(tls_context.native_handle(), SSL_OP_NO_RENEGOTIATION); // CVE-2009-3555
+#endif
+ // node private key
+ auto key = config.identity.first->serialize();
+ tls_context.use_private_key(asio::const_buffer{key.data(), key.size()},
+ asio::ssl::context::file_format::pem, ec);
+ if (ec)
+ throw std::runtime_error("Error setting node's private key: " + ec.message());
+ // certificate chain
+ auto certchain = config.identity.second->toString(true/*chain*/);
+ tls_context.use_certificate_chain(asio::const_buffer{certchain.data(), certchain.size()}, ec);
+ if (ec)
+ throw std::runtime_error("Error setting certificate chain: " + ec.message());
+ if (logger_)
+ logger_->d("[proxy:server] using certificate chain for ssl:\n%s", certchain.c_str());
+ // build http server
+ auto settings = restinio::run_on_this_thread_settings_t<RestRouterTraitsTls>();
+ addServerSettings(settings);
+ if (not config.address.empty())
+ settings.address(config.address);
+ settings.port(config.port);
+ settings.tls_context(std::move(tls_context));
+ httpsServer_ = std::make_unique<restinio::http_server_t<RestRouterTraitsTls>>(
+ ioContext_,
+ std::forward<restinio::run_on_this_thread_settings_t<RestRouterTraitsTls>>(std::move(settings))
+ );
+ // run http server
+ serverThread_ = std::thread([this]{
+ httpsServer_->open_async([]{/*ok*/}, [](std::exception_ptr ex){
+ std::rethrow_exception(ex);
+ });
+ httpsServer_->io_context().run();
+ });
+ }
+ else {
+ auto settings = restinio::run_on_this_thread_settings_t<RestRouterTraits>();
+ addServerSettings(settings);
+ if (not config.address.empty())
+ settings.address(config.address);
+ settings.port(config.port);
+ httpServer_ = std::make_unique<restinio::http_server_t<RestRouterTraits>>(
+ ioContext_,
+ std::forward<restinio::run_on_this_thread_settings_t<RestRouterTraits>>(std::move(settings))
+ );
+ // run http server
+ serverThread_ = std::thread([this](){
+ httpServer_->open_async([]{/*ok*/}, [](std::exception_ptr ex){
+ std::rethrow_exception(ex);
+ });
+ httpServer_->io_context().run();
+ });
+ }
+ dht->forwardAllMessages(true);
+ updateStats();
+ printStatsTimer_->async_wait(std::bind(&DhtProxyServer::handlePrintStats, this, std::placeholders::_1));
+
+ if (not persistPath_.empty()) {
+ try {
+ std::ifstream stateFile(persistPath_, std::ios::binary | std::ios::ate);
+ if (stateFile) {
+ std::streamsize size = stateFile.tellg();
+ stateFile.seekg(0, std::ios::beg);
+ if (logger_)
+ logger_->d("Loading proxy state from %.*s (%td bytes)", (int)persistPath_.size(), persistPath_.c_str(), size);
+ loadState(stateFile, size);
+ }
+ } catch (const std::exception& e) {
+ if (logger_)
+ logger_->e("Error loading state from file: %s", e.what());
+ }
+ }
+}
+
+template <typename Os>
+void
+DhtProxyServer::saveState(Os& stream) {
+ msgpack::packer<Os> pk(&stream);
+ pk.pack_map(2);
+ {
+ std::lock_guard<std::mutex> lock(lockSearchPuts_);
+ pk.pack("puts");
+ pk.pack(puts_);
+ }
+#ifdef OPENDHT_PUSH_NOTIFICATIONS
+ {
+ std::lock_guard<std::mutex> lock(lockListener_);
+ pk.pack("pushListeners");
+ pk.pack(pushListeners_);
+ }
+#endif
+}
+
+template <typename Is>
+void
+DhtProxyServer::loadState(Is& is, size_t size) {
+ msgpack::unpacker pac;
+ pac.reserve_buffer(size);
+ if (is.read(pac.buffer(), size)) {
+ pac.buffer_consumed(size);
+
+ msgpack::object_handle oh;
+ while (pac.next(oh)) {
+ if (oh.get().type != msgpack::type::MAP)
+ continue;
+ if (auto puts = findMapValue(oh.get(), "puts"sv)) {
+ std::lock_guard<std::mutex> lock(lockSearchPuts_);
+ puts_ = puts->as<decltype(puts_)>();
+ if (logger_)
+ logger_->d("Loading %zu persistent puts", puts_.size());
+ for (auto& put : puts_) {
+ for (auto& pput : put.second.puts) {
+ pput.second.expireTimer = std::make_unique<asio::steady_timer>(io_context(), pput.second.expiration);
+ pput.second.expireTimer->async_wait(std::bind(&DhtProxyServer::handleCancelPermamentPut, this,
+ std::placeholders::_1, put.first, pput.first));
+#ifdef OPENDHT_PUSH_NOTIFICATIONS
+ if (not pput.second.pushToken.empty()) {
+ auto jsonProvider = [infoHash=put.first.toString(), clientId=pput.second.clientId, vid = pput.first, sessionCtx = pput.second.sessionCtx](){
+ Json::Value json;
+ json["timeout"] = infoHash;
+ json["to"] = clientId;
+ json["vid"] = std::to_string(vid);
+ if (sessionCtx) {
+ std::lock_guard<std::mutex> l(sessionCtx->lock);
+ json["s"] = sessionCtx->sessionId;
+ }
+ return json;
+ };
+ pput.second.expireNotifyTimer = std::make_unique<asio::steady_timer>(io_context(), pput.second.expiration - proxy::OP_MARGIN);
+ pput.second.expireNotifyTimer->async_wait(std::bind(
+ &DhtProxyServer::handleNotifyPushListenExpire, this,
+ std::placeholders::_1, pput.second.pushToken, std::move(jsonProvider), pput.second.type, pput.second.topic));
+ }
+#endif
+ dht_->put(put.first, pput.second.value, DoneCallbackSimple{}, time_point::max(), true);
+ }
+ }
+ } else {
+ if (logger_)
+ logger_->d("No persistent puts in state");
+ }
+#ifdef OPENDHT_PUSH_NOTIFICATIONS
+ if (auto pushListeners = findMapValue(oh.get(), "pushListeners"sv)) {
+ std::lock_guard<std::mutex> lock(lockListener_);
+ pushListeners_ = pushListeners->as<decltype(pushListeners_)>();
+ if (logger_)
+ logger_->d("Loading %zu push listeners", pushListeners_.size());
+ for (auto& pushListener : pushListeners_) {
+ for (auto& listeners : pushListener.second.listeners) {
+ for (auto& listener : listeners.second) {
+ listener.internalToken = dht_->listen(listeners.first,
+ [this, infoHash=listeners.first, pushToken=pushListener.first, type=listener.type, clientId=listener.clientId, sessionCtx = listener.sessionCtx, topic=listener.topic]
+ (const std::vector<std::shared_ptr<Value>>& values, bool expired) {
+ // Build message content
+ Json::Value json;
+ json["key"] = infoHash.toString();
+ json["to"] = clientId;
+ json["t"] = Json::Value::Int64(std::chrono::duration_cast<std::chrono::milliseconds>(system_clock::now().time_since_epoch()).count());
+ {
+ std::lock_guard<std::mutex> l(sessionCtx->lock);
+ json["s"] = sessionCtx->sessionId;
+ }
+ if (expired and values.size() < 2){
+ std::ostringstream ss;
+ for(size_t i = 0; i < values.size(); ++i){
+ if(i != 0) ss << ",";
+ ss << values[i]->id;
+ }
+ json["exp"] = ss.str();
+ }
+ auto maxPrio = 1000u;
+ for (const auto& v : values)
+ maxPrio = std::min(maxPrio, v->priority);
+ sendPushNotification(pushToken, std::move(json), type, !expired and maxPrio == 0, topic);
+ return true;
+ }
+ );
+ // expire notify
+ listener.expireNotifyTimer = std::make_unique<asio::steady_timer>(io_context(), listener.expiration - proxy::OP_MARGIN);
+ auto jsonProvider = [infoHash = listeners.first.toString(), clientId = listener.clientId, sessionCtx = listener.sessionCtx](){
+ Json::Value json;
+ json["timeout"] = infoHash;
+ json["to"] = clientId;
+ std::lock_guard<std::mutex> l(sessionCtx->lock);
+ json["s"] = sessionCtx->sessionId;
+ return json;
+ };
+ listener.expireNotifyTimer->async_wait(std::bind(&DhtProxyServer::handleNotifyPushListenExpire, this,
+ std::placeholders::_1, pushListener.first, std::move(jsonProvider), listener.type, listener.topic));
+ // cancel push listen
+ listener.expireTimer = std::make_unique<asio::steady_timer>(io_context(), listener.expiration);
+ listener.expireTimer->async_wait(std::bind(&DhtProxyServer::handleCancelPushListen, this,
+ std::placeholders::_1, pushListener.first, listeners.first, listener.clientId));
+ }
+ }
+ }
+ } else {
+ if (logger_)
+ logger_->d("No push listeners in state");
+ }
+#endif
+ }
+ if (logger_)
+ logger_->d("loading ended");
+ }
+}
+
+
+asio::io_context&
+DhtProxyServer::io_context() const
+{
+ return *ioContext_;
+}
+
+DhtProxyServer::~DhtProxyServer()
+{
+ if (not persistPath_.empty()) {
+ if (logger_)
+ logger_->d("Saving proxy state to %.*s", (int)persistPath_.size(), persistPath_.c_str());
+ std::ofstream stateFile(persistPath_, std::ios::binary);
+ saveState(stateFile);
+ }
+ if (dht_) {
+ std::lock_guard<std::mutex> lock(lockListener_);
+ for (auto& l : listeners_) {
+ dht_->cancelListen(l.second.hash, std::move(l.second.token));
+ if (l.second.response)
+ l.second.response->done();
+ }
+#ifdef OPENDHT_PUSH_NOTIFICATIONS
+ for (auto& lm: pushListeners_) {
+ for (auto& ls: lm.second.listeners)
+ for (auto& l : ls.second) {
+ if (l.expireNotifyTimer)
+ l.expireNotifyTimer->cancel();
+ if (l.expireTimer)
+ l.expireTimer->cancel();
+ dht_->cancelListen(ls.first, std::move(l.internalToken));
+ }
+ }
+ pushListeners_.clear();
+#endif
+ }
+ if (logger_)
+ logger_->d("[proxy:server] closing http server");
+ ioContext_->stop();
+ if (serverThread_.joinable())
+ serverThread_.join();
+ if (logger_)
+ logger_->d("[proxy:server] http server closed");
+}
+
+template< typename ServerSettings >
+void
+DhtProxyServer::addServerSettings(ServerSettings& settings, const unsigned int max_pipelined_requests)
+{
+ using namespace std::chrono;
+ /**
+ * If max_pipelined_requests is greater than 1 then RESTinio will continue
+ * to read from the socket after parsing the first request.
+ * In that case, RESTinio can detect the disconnection
+ * and calls state listener as expected.
+ * https://github.com/Stiffstream/restinio/issues/28
+ */
+ settings.max_pipelined_requests(max_pipelined_requests);
+ // one less to detect the listener disconnect
+ settings.concurrent_accepts_count(max_pipelined_requests - 1);
+ settings.separate_accept_and_create_connect(true);
+ settings.logger(logger_);
+ settings.protocol(restinio::asio_ns::ip::tcp::v6());
+ settings.request_handler(createRestRouter());
+ // time limits // ~ 0.8 month
+ std::chrono::milliseconds timeout_request(std::numeric_limits<int>::max());
+ settings.read_next_http_message_timelimit(timeout_request);
+ settings.write_http_response_timelimit(60s);
+ settings.handle_request_timeout(timeout_request);
+ // socket options
+ settings.socket_options_setter([](auto & options){
+ options.set_option(asio::ip::tcp::no_delay{true});
+ options.set_option(asio::socket_base::keep_alive{true});
+ });
+ settings.connection_state_listener(connListener_);
+}
+
+std::shared_ptr<DhtProxyServer::ServerStats>
+DhtProxyServer::updateStats(std::shared_ptr<NodeInfo> info) const
+{
+ auto now = clock::now();
+ auto last = lastStatsReset_.exchange(now);
+ auto count = requestNum_.exchange(0);
+ auto dt = std::chrono::duration<double>(now - last);
+ auto sstats = std::make_shared<ServerStats>();
+ auto& stats = *sstats;
+ stats.requestRate = count / dt.count();
+#ifdef OPENDHT_PUSH_NOTIFICATIONS
+ stats.pushListenersCount = pushListeners_.size();
+#endif
+ stats.totalPermanentPuts = 0;
+ std::for_each(puts_.begin(), puts_.end(), [&stats](const auto& put) {
+ stats.totalPermanentPuts += put.second.puts.size();
+ });
+ stats.putCount = puts_.size();
+ stats.listenCount = listeners_.size();
+ stats.nodeInfo = std::move(info);
+ return sstats;
+}
+
+void
+DhtProxyServer::updateStats() {
+ dht_->getNodeInfo([this](std::shared_ptr<NodeInfo> newInfo){
+ stats_ = updateStats(newInfo);
+ nodeInfo_ = newInfo;
+ if (logger_) {
+ auto str = Json::writeString(jsonBuilder_, newInfo->toJson());
+ logger_->d("[proxy:server] [stats] %s", str.c_str());
+ }
+ });
+}
+
+void
+DhtProxyServer::handlePrintStats(const asio::error_code &ec)
+{
+ if (ec == asio::error::operation_aborted)
+ return;
+ updateStats();
+ printStatsTimer_->expires_at(printStatsTimer_->expiry() + PRINT_STATS_PERIOD);
+ printStatsTimer_->async_wait(std::bind(&DhtProxyServer::handlePrintStats, this, std::placeholders::_1));
+}
+
+template <typename HttpResponse>
+HttpResponse DhtProxyServer::initHttpResponse(HttpResponse response)
+{
+ response.append_header("Server", "RESTinio");
+ response.append_header(restinio::http_field::content_type, "application/json");
+ response.append_header(restinio::http_field::access_control_allow_origin, "*");
+ return response;
+}
+
+std::unique_ptr<RestRouter>
+DhtProxyServer::createRestRouter()
+{
+ using namespace std::placeholders;
+ auto router = std::make_unique<RestRouter>();
+
+ // **************************** LEGACY ROUTES ****************************
+ // node.info
+ router->http_get("/", std::bind(&DhtProxyServer::getNodeInfo, this, _1, _2));
+ // key.options
+ router->add_handler(restinio::http_method_options(),
+ "/:hash", std::bind(&DhtProxyServer::options, this, _1, _2));
+ // key.get
+ router->http_get("/:hash", std::bind(&DhtProxyServer::get, this, _1, _2));
+ // key.post
+ router->http_post("/:hash", std::bind(&DhtProxyServer::put, this, _1, _2));
+#ifdef OPENDHT_PUSH_NOTIFICATIONS
+ // key.subscribe
+ router->add_handler(restinio::http_method_subscribe(),
+ "/:hash", std::bind(&DhtProxyServer::subscribe, this, _1, _2));
+ // key.unsubscribe
+ router->add_handler(restinio::http_method_unsubscribe(),
+ "/:hash", std::bind(&DhtProxyServer::unsubscribe, this, _1, _2));
+#endif //OPENDHT_PUSH_NOTIFICATIONS
+#ifdef OPENDHT_PROXY_SERVER_IDENTITY
+#endif // OPENDHT_PROXY_SERVER_IDENTITY
+
+ // **************************** NEW ROUTES ****************************
+ // node.info
+ router->http_get("/node/info", std::bind(&DhtProxyServer::getNodeInfo, this, _1, _2));
+ // node.stats
+ router->http_get("/node/stats", std::bind(&DhtProxyServer::getStats, this, _1, _2));
+ // key.options
+ router->http_get("/key/:hash/options", std::bind(&DhtProxyServer::options, this, _1, _2));
+ // key.get
+ router->http_get("/key/:hash", std::bind(&DhtProxyServer::get, this, _1, _2));
+ // key.post
+ router->http_post("/key/:hash", std::bind(&DhtProxyServer::put, this, _1, _2));
+ // key.listen
+ router->http_get("/key/:hash/listen", std::bind(&DhtProxyServer::listen, this, _1, _2));
+#ifdef OPENDHT_PUSH_NOTIFICATIONS
+ // node.pingPush
+ router->http_post("/node/pingPush", std::bind(&DhtProxyServer::pingPush, this, _1, _2));
+ // key.subscribe
+ router->add_handler(restinio::http_method_subscribe(),
+ "/key/:hash", std::bind(&DhtProxyServer::subscribe, this, _1, _2));
+ // key.unsubscribe
+ router->add_handler(restinio::http_method_unsubscribe(),
+ "/key/:hash", std::bind(&DhtProxyServer::unsubscribe, this, _1, _2));
+#endif //OPENDHT_PUSH_NOTIFICATIONS
+#ifdef OPENDHT_PROXY_SERVER_IDENTITY
+ // key.sign
+ router->http_post("/key/:hash/sign", std::bind(&DhtProxyServer::putSigned, this, _1, _2));
+ // key.encrypt
+ router->http_post("/key/:hash/encrypt", std::bind(&DhtProxyServer::putEncrypted, this, _1, _2));
+#endif // OPENDHT_PROXY_SERVER_IDENTITY
+
+ return router;
+}
+
+RequestStatus
+DhtProxyServer::getNodeInfo(restinio::request_handle_t request,
+ restinio::router::route_params_t /*params*/) const
+{
+ try {
+ if (auto nodeInfo = nodeInfo_) {
+ auto result = nodeInfo->toJson();
+ // [ipv6:ipv4]:port or ipv4:port
+ result["public_ip"] = request->remote_endpoint().address().to_string();
+ auto response = initHttpResponse(request->create_response());
+ response.append_body(Json::writeString(jsonBuilder_, result) + "\n");
+ return response.done();
+ }
+ auto response = initHttpResponse(request->create_response(restinio::status_service_unavailable()));
+ response.set_body(RESP_MSG_SERVICE_UNAVAILABLE);
+ return response.done();
+ } catch (...) {
+ return serverError(*request);
+ }
+}
+
+RequestStatus
+DhtProxyServer::getStats(restinio::request_handle_t request,
+ restinio::router::route_params_t /*params*/)
+{
+ requestNum_++;
+ try {
+ if (auto stats = stats_) {
+ auto response = initHttpResponse(request->create_response());
+ response.append_body(Json::writeString(jsonBuilder_, stats->toJson()) + "\n");
+ return response.done();
+ } else {
+ auto response = initHttpResponse(request->create_response(restinio::status_service_unavailable()));
+ response.set_body(RESP_MSG_SERVICE_UNAVAILABLE);
+ return response.done();
+ }
+ } catch (...){
+ return serverError(*request);
+ }
+}
+
+RequestStatus
+DhtProxyServer::get(restinio::request_handle_t request,
+ restinio::router::route_params_t params)
+{
+ requestNum_++;
+ try {
+ InfoHash infoHash(params["hash"]);
+ if (!infoHash)
+ infoHash = InfoHash::get(params["hash"]);
+ auto response = std::make_shared<ResponseByPartsBuilder>(
+ initHttpResponse(request->create_response<ResponseByParts>()));
+ response->flush();
+ dht_->get(infoHash, [this, response](const std::vector<Sp<Value>>& values) {
+ std::ostringstream output;
+ for (const auto& value : values) {
+ output << Json::writeString(jsonBuilder_, value->toJson()) << "\n";
+ }
+ response->append_chunk(output.str());
+ response->flush();
+ return true;
+ },
+ [response] (bool /*ok*/){
+ response->done();
+ });
+ return restinio::request_handling_status_t::accepted;
+ } catch (const std::exception& e){
+ return serverError(*request);
+ }
+}
+
+RequestStatus
+DhtProxyServer::listen(restinio::request_handle_t request,
+ restinio::router::route_params_t params)
+{
+ requestNum_++;
+
+ try {
+ InfoHash infoHash(params["hash"]);
+ if (!infoHash)
+ infoHash = InfoHash::get(params["hash"]);
+ auto response = std::make_shared<ResponseByPartsBuilder>(
+ initHttpResponse(request->create_response<ResponseByParts>()));
+ response->flush();
+ std::lock_guard<std::mutex> lock(lockListener_);
+ // save the listener to handle a disconnect
+ auto &session = listeners_[request->connection_id()];
+ session.hash = infoHash;
+ session.response = response;
+ session.token = dht_->listen(infoHash, [this, response]
+ (const std::vector<Sp<Value>>& values, bool expired){
+ for (const auto& value: values){
+ auto jsonVal = value->toJson();
+ if (expired)
+ jsonVal["expired"] = true;
+ response->append_chunk(Json::writeString(jsonBuilder_, jsonVal) + "\n");
+ }
+ response->flush();
+ return true;
+ });
+ return restinio::request_handling_status_t::accepted;
+ } catch (const std::exception& e){
+ return serverError(*request);
+ }
+}
+
+#ifdef OPENDHT_PUSH_NOTIFICATIONS
+
+PushType
+DhtProxyServer::getTypeFromString(const std::string& type) {
+ if (type == "android")
+ return PushType::Android;
+ else if (type == "ios")
+ return PushType::iOS;
+ else if (type == "unifiedpush")
+ return PushType::UnifiedPush;
+ return PushType::None;
+}
+
+std::string
+DhtProxyServer::getDefaultTopic(PushType) {
+ return bundleId_;
+}
+
+RequestStatus
+DhtProxyServer::pingPush(restinio::request_handle_t request,
+ restinio::router::route_params_t /*params*/)
+{
+ requestNum_++;
+ try {
+ std::string err;
+ Json::Value r;
+ auto* char_data = reinterpret_cast<const char*>(request->body().data());
+ auto reader = std::unique_ptr<Json::CharReader>(jsonReaderBuilder_.newCharReader());
+ if (!reader->parse(char_data, char_data + request->body().size(), &r, &err)){
+ auto response = initHttpResponse(request->create_response(restinio::status_bad_request()));
+ response.set_body(RESP_MSG_JSON_INCORRECT);
+ return response.done();
+ }
+ const Json::Value& root(r); // parse using const Json so [] never creates element
+ auto pushToken = root["key"].asString();
+ if (pushToken.empty()){
+ auto response = initHttpResponse(request->create_response(restinio::status_bad_request()));
+ response.set_body(RESP_MSG_NO_TOKEN);
+ return response.done();
+ }
+ auto type = getTypeFromString(root["platform"].asString());
+ auto topic = root["topic"].asString();
+ if (topic.empty()) {
+ topic = getDefaultTopic(type);
+ }
+ Json::Value json;
+ json["to"] = root["client_id"];
+ json["pong"] = true;
+ sendPushNotification(pushToken, std::move(json), type, true, topic);
+ } catch (...) {
+ return serverError(*request);
+ }
+ return restinio::request_handling_status_t::accepted;
+}
+
+RequestStatus
+DhtProxyServer::subscribe(restinio::request_handle_t request,
+ restinio::router::route_params_t params)
+{
+ requestNum_++;
+ try {
+ InfoHash infoHash(params["hash"]);
+ if (!infoHash)
+ infoHash = InfoHash::get(params["hash"]);
+
+ std::string err;
+ Json::Value r;
+ auto* char_data = reinterpret_cast<const char*>(request->body().data());
+ auto reader = std::unique_ptr<Json::CharReader>(jsonReaderBuilder_.newCharReader());
+ if (!reader->parse(char_data, char_data + request->body().size(), &r, &err)){
+ auto response = initHttpResponse(request->create_response(restinio::status_bad_request()));
+ response.set_body(RESP_MSG_JSON_INCORRECT);
+ return response.done();
+ }
+ const Json::Value& root(r); // parse using const Json so [] never creates element
+ auto pushToken = root["key"].asString();
+ if (pushToken.empty()){
+ auto response = initHttpResponse(request->create_response(restinio::status_bad_request()));
+ response.set_body(RESP_MSG_NO_TOKEN);
+ return response.done();
+ }
+ auto type = getTypeFromString(root["platform"].asString());
+ auto topic = root["topic"].asString();
+ if (topic.empty()) {
+ topic = getDefaultTopic(type);
+ }
+ auto clientId = root["client_id"].asString();
+ auto sessionId = root["session_id"].asString();
+
+ if (logger_)
+ logger_->d("[proxy:server] [subscribe %s] [client %s] [session %s]", infoHash.toString().c_str(), clientId.c_str(), sessionId.c_str());
+
+ // Insert new or return existing push listeners of a token
+ std::lock_guard<std::mutex> lock(lockListener_);
+ auto& pushListener = pushListeners_[pushToken];
+ auto& pushListeners = pushListener.listeners[infoHash];
+
+ auto listIt = std::find_if(pushListeners.begin(), pushListeners.end(), [&](const Listener& l) {
+ return l.clientId == clientId;
+ });
+ bool newListener = listIt == pushListeners.end();
+ if (newListener) {
+ pushListeners.emplace_back(Listener{});
+ listIt = std::prev(pushListeners.end());
+ listIt->clientId = clientId;
+ listIt->sessionCtx = std::make_shared<PushSessionContext>(sessionId);
+ } else {
+ std::lock_guard<std::mutex> l(listIt->sessionCtx->lock);
+ listIt->sessionCtx->sessionId = sessionId;
+ }
+ auto& listener = *listIt;
+
+ // Expiration
+ auto timeout = std::chrono::steady_clock::now() + proxy::OP_TIMEOUT;
+ listener.expiration = timeout;
+ listener.type = type;
+ listener.topic = topic;
+ if (listener.expireNotifyTimer)
+ listener.expireNotifyTimer->expires_at(timeout - proxy::OP_MARGIN);
+ else
+ listener.expireNotifyTimer = std::make_unique<asio::steady_timer>(io_context(), timeout - proxy::OP_MARGIN);
+ auto jsonProvider = [h=infoHash.toString(), clientId, sessionCtx = listener.sessionCtx](){
+ Json::Value json;
+ json["timeout"] = h;
+ json["to"] = clientId;
+ std::lock_guard<std::mutex> l(sessionCtx->lock);
+ json["s"] = sessionCtx->sessionId;
+ return json;
+ };
+ listener.expireNotifyTimer->async_wait(std::bind(&DhtProxyServer::handleNotifyPushListenExpire, this,
+ std::placeholders::_1, pushToken, std::move(jsonProvider), listener.type, listener.topic));
+ if (!listener.expireTimer)
+ listener.expireTimer = std::make_unique<asio::steady_timer>(io_context(), timeout);
+ else
+ listener.expireTimer->expires_at(timeout);
+ listener.expireTimer->async_wait(std::bind(&DhtProxyServer::handleCancelPushListen, this,
+ std::placeholders::_1, pushToken, infoHash, clientId));
+
+ // Send response
+ if (not newListener) {
+ if (logger_)
+ logger_->d("[proxy:server] [subscribe] found [client %s]", listener.clientId.c_str());
+ // Send response header
+ auto response = std::make_shared<ResponseByPartsBuilder>(initHttpResponse(request->create_response<ResponseByParts>()));
+ response->flush();
+ if (!root["refresh"].asBool()) {
+ // No Refresh
+ dht_->get(infoHash, [this, response](const Sp<Value>& value){
+ auto output = Json::writeString(jsonBuilder_, value->toJson()) + "\n";
+ response->append_chunk(output);
+ response->flush();
+ return true;
+ },
+ [response] (bool){
+ response->done();
+ });
+ } else {
+ // Refresh
+ response->append_chunk("{}\n");
+ return response->done();
+ }
+ } else {
+ // =========== No existing listener for an infoHash ============
+ // Add listen on dht
+ listener.internalToken = dht_->listen(infoHash,
+ [this, infoHash, pushToken, type, clientId, sessionCtx = listener.sessionCtx, topic]
+ (const std::vector<std::shared_ptr<Value>>& values, bool expired){
+ // Build message content
+ Json::Value json;
+ json["key"] = infoHash.toString();
+ json["to"] = clientId;
+ json["t"] = Json::Value::Int64(std::chrono::duration_cast<std::chrono::milliseconds>(system_clock::now().time_since_epoch()).count());
+ {
+ std::lock_guard<std::mutex> l(sessionCtx->lock);
+ json["s"] = sessionCtx->sessionId;
+ }
+ if (expired and values.size() < 2){
+ std::ostringstream ss;
+ for(size_t i = 0; i < values.size(); ++i) {
+ if(i != 0) ss << ",";
+ ss << values[i]->id;
+ }
+ json["exp"] = ss.str();
+ }
+ auto maxPrio = 1000u;
+ for (const auto& v : values)
+ maxPrio = std::min(maxPrio, v->priority);
+ sendPushNotification(pushToken, std::move(json), type, !expired and maxPrio == 0, topic);
+ return true;
+ }
+ );
+ auto response = initHttpResponse(request->create_response());
+ response.set_body("{}\n");
+ return response.done();
+ }
+ }
+ catch (...) {
+ return serverError(*request);
+ }
+ return restinio::request_handling_status_t::accepted;
+}
+
+RequestStatus
+DhtProxyServer::unsubscribe(restinio::request_handle_t request,
+ restinio::router::route_params_t params)
+{
+ requestNum_++;
+
+ InfoHash infoHash(params["hash"]);
+ if (!infoHash)
+ infoHash = InfoHash::get(params["hash"]);
+
+ if (logger_)
+ logger_->d("[proxy:server] [unsubscribe %s]", infoHash.toString().c_str());
+
+ try {
+ std::string err;
+ Json::Value root;
+ auto* char_data = reinterpret_cast<const char*>(request->body().data());
+ auto reader = std::unique_ptr<Json::CharReader>(jsonReaderBuilder_.newCharReader());
+
+ if (!reader->parse(char_data, char_data + request->body().size(), &root, &err)){
+ auto response = initHttpResponse(
+ request->create_response(restinio::status_bad_request()));
+ response.set_body(RESP_MSG_JSON_INCORRECT);
+ return response.done();
+ }
+ auto pushToken = root["key"].asString();
+ if (pushToken.empty())
+ return restinio::request_handling_status_t::rejected;
+ auto clientId = root["client_id"].asString();
+
+ handleCancelPushListen(asio::error_code() /*success*/, pushToken, infoHash, clientId);
+ auto response = initHttpResponse(request->create_response());
+ return response.done();
+ }
+ catch (...) {
+ return serverError(*request);
+ }
+}
+
+void
+DhtProxyServer::handleNotifyPushListenExpire(const asio::error_code &ec, const std::string pushToken,
+ std::function<Json::Value()> jsonProvider, PushType type, const std::string& topic)
+{
+ if (ec == asio::error::operation_aborted)
+ return;
+ else if (ec) {
+ if (logger_)
+ logger_->e("[proxy:server] [subscribe] error sending put refresh: %s", ec.message().c_str());
+ }
+ if (logger_)
+ logger_->d("[proxy:server] [subscribe] sending refresh to %s token", pushToken.c_str());
+ sendPushNotification(pushToken, jsonProvider(), type, false, topic);
+}
+
+void
+DhtProxyServer::handleCancelPushListen(const asio::error_code &ec, const std::string pushToken,
+ const InfoHash key, const std::string clientId)
+{
+ if (ec == asio::error::operation_aborted)
+ return;
+ else if (ec){
+ if (logger_)
+ logger_->e("[proxy:server] [listen:push %s] error cancel: %s",
+ key.toString().c_str(), ec.message().c_str());
+ }
+ if (logger_)
+ logger_->d("[proxy:server] [listen:push %s] cancelled for %s",
+ key.toString().c_str(), clientId.c_str());
+ std::lock_guard<std::mutex> lock(lockListener_);
+
+ auto pushListener = pushListeners_.find(pushToken);
+ if (pushListener == pushListeners_.end())
+ return;
+ auto listeners = pushListener->second.listeners.find(key);
+ if (listeners == pushListener->second.listeners.end())
+ return;
+
+ for (auto listener = listeners->second.begin(); listener != listeners->second.end();){
+ if (listener->clientId == clientId){
+ if (dht_)
+ dht_->cancelListen(key, std::move(listener->internalToken));
+ listener = listeners->second.erase(listener);
+ } else {
+ ++listener;
+ }
+ }
+ if (listeners->second.empty())
+ pushListener->second.listeners.erase(listeners);
+ if (pushListener->second.listeners.empty())
+ pushListeners_.erase(pushListener);
+}
+
+void
+DhtProxyServer::sendPushNotification(const std::string& token, Json::Value&& json, PushType type, bool highPriority, const std::string& topic)
+{
+ if (pushServer_.empty() && type != PushType::UnifiedPush)
+ return;
+
+ unsigned reqid = 0;
+ try {
+ std::shared_ptr<http::Request> request;
+ http::Url tokenUrl(token);
+ if (type == PushType::UnifiedPush)
+ request = std::make_shared<http::Request>(io_context(), tokenUrl.protocol + "://" + tokenUrl.host, logger_);
+ else
+ request = std::make_shared<http::Request>(io_context(), pushHostPort_.first, pushHostPort_.second, pushHostPort_.first.find("https://") == 0, logger_);;
+ reqid = request->id();
+ request->set_target(type == PushType::UnifiedPush ? (tokenUrl.target) : "/api/push");
+ request->set_method(restinio::http_method_post());
+ request->set_header_field(restinio::http_field_t::host, type == PushType::UnifiedPush ? tokenUrl.host.c_str() : pushServer_.c_str());
+ request->set_header_field(restinio::http_field_t::user_agent, "RESTinio client");
+ request->set_header_field(restinio::http_field_t::accept, "*/*");
+ request->set_header_field(restinio::http_field_t::content_type, "application/json");
+
+ if (type == PushType::UnifiedPush) {
+ Json::Value notification(Json::objectValue);
+ notification["message"] = Json::writeString(jsonBuilder_, std::move(json));
+ notification["topic"] = token;
+ notification["priority"] = highPriority ? 5 : 1;
+ request->set_body(Json::writeString(jsonBuilder_, std::move(json)));
+ } else {
+ // NOTE: see https://github.com/appleboy/gorush
+ auto isResubscribe = json.isMember("timeout");
+ Json::Value notification(Json::objectValue);
+ Json::Value tokens(Json::arrayValue);
+ tokens[0] = token;
+ notification["tokens"] = std::move(tokens);
+ notification["platform"] = type == PushType::Android ? 2 : 1;
+ notification["data"] = std::move(json);
+ notification["priority"] = highPriority ? "high" : "normal";
+ if (type == PushType::Android)
+ notification["time_to_live"] = 3600 * 24; // 24 hours
+ else {
+ const auto expiration = std::chrono::system_clock::now() + std::chrono::hours(24);
+ uint32_t exp = std::chrono::duration_cast<std::chrono::seconds>(expiration.time_since_epoch()).count();
+ notification["expiration"] = exp;
+ if (!topic.empty())
+ notification["topic"] = topic;
+ if (highPriority || isResubscribe) {
+ Json::Value alert(Json::objectValue);
+ alert["title"]="hello";
+ notification["push_type"] = "alert";
+ notification["alert"] = alert;
+ notification["mutable_content"] = true;
+ notification["priority"] = "high";
+ } else {
+ notification["push_type"] = "background";
+ notification["content_available"] = true;
+ }
+ }
+
+ Json::Value notifications(Json::arrayValue);
+ notifications[0] = notification;
+
+ Json::Value content;
+ content["notifications"] = std::move(notifications);
+ request->set_body(Json::writeString(jsonBuilder_, content));
+ }
+
+ request->add_on_state_change_callback([this, reqid]
+ (http::Request::State state, const http::Response& response){
+ if (state == http::Request::State::DONE){
+ if (logger_ and response.status_code != 200)
+ logger_->e("[proxy:server] [notification] push failed: %i", response.status_code);
+ std::lock_guard<std::mutex> l(requestLock_);
+ requests_.erase(reqid);
+ }
+ });
+ {
+ std::lock_guard<std::mutex> l(requestLock_);
+ requests_[reqid] = request;
+ }
+ request->send();
+ }
+ catch (const std::exception &e){
+ if (logger_)
+ logger_->e("[proxy:server] [notification] error send push: %s", e.what());
+ if (reqid) {
+ std::lock_guard<std::mutex> l(requestLock_);
+ requests_.erase(reqid);
+ }
+ }
+}
+
+#endif //OPENDHT_PUSH_NOTIFICATIONS
+
+void
+DhtProxyServer::handleCancelPermamentPut(const asio::error_code &ec, const InfoHash& key, Value::Id vid)
+{
+ if (ec == asio::error::operation_aborted)
+ return;
+ else if (ec){
+ if (logger_)
+ logger_->e("[proxy:server] [put:permament] error sending put refresh: %s", ec.message().c_str());
+ }
+ if (logger_)
+ logger_->d("[proxy:server] [put %s] cancel permament put %i", key.toString().c_str(), vid);
+ std::lock_guard<std::mutex> lock(lockSearchPuts_);
+ auto sPuts = puts_.find(key);
+ if (sPuts == puts_.end())
+ return;
+ auto& sPutsMap = sPuts->second.puts;
+ auto put = sPutsMap.find(vid);
+ if (put == sPutsMap.end())
+ return;
+ if (dht_)
+ dht_->cancelPut(key, vid);
+ if (put->second.expireTimer)
+ put->second.expireTimer->cancel();
+ if (put->second.expireNotifyTimer)
+ put->second.expireNotifyTimer->cancel();
+ sPutsMap.erase(put);
+ if (sPutsMap.empty())
+ puts_.erase(sPuts);
+}
+
+RequestStatus
+DhtProxyServer::put(restinio::request_handle_t request,
+ restinio::router::route_params_t params)
+{
+ requestNum_++;
+ InfoHash infoHash(params["hash"]);
+ if (!infoHash)
+ infoHash = InfoHash::get(params["hash"]);
+
+ if (request->body().empty()){
+ auto response = initHttpResponse(request->create_response(restinio::status_bad_request()));
+ response.set_body(RESP_MSG_MISSING_PARAMS);
+ return response.done();
+ }
+
+ try {
+ std::string err;
+ Json::Value root;
+ auto* char_data = reinterpret_cast<const char*>(request->body().data());
+ auto reader = std::unique_ptr<Json::CharReader>(jsonReaderBuilder_.newCharReader());
+
+ if (reader->parse(char_data, char_data + request->body().size(), &root, &err)){
+ auto value = std::make_shared<Value>(root);
+ bool permanent = root.isMember("permanent");
+ if (logger_)
+ logger_->d("[proxy:server] [put %s] %s %s", infoHash.toString().c_str(),
+ value->toString().c_str(), (permanent ? "permanent" : ""));
+ if (permanent) {
+ std::string pushToken, clientId, sessionId, platform, topic;
+ auto& pVal = root["permanent"];
+ if (pVal.isObject()){
+ pushToken = pVal["key"].asString();
+ clientId = pVal["client_id"].asString();
+ platform = pVal["platform"].asString();
+ sessionId = pVal["session_id"].asString();
+ topic = pVal["topic"].asString();
+ }
+ std::lock_guard<std::mutex> lock(lockSearchPuts_);
+ auto timeout = std::chrono::steady_clock::now() + proxy::OP_TIMEOUT;
+ auto& sPuts = puts_[infoHash];
+ if (value->id == Value::INVALID_ID) {
+ for (auto& pp : sPuts.puts) {
+ if (pp.second.pushToken == pushToken
+ and pp.second.clientId == clientId
+ and pp.second.value->contentEquals(*value))
+ {
+ pp.second.expireTimer->expires_at(timeout);
+ pp.second.expireTimer->async_wait(std::bind(&DhtProxyServer::handleCancelPermamentPut, this,
+ std::placeholders::_1, infoHash, pp.second.value->id));
+ if (not sessionId.empty()) {
+ if (not pp.second.sessionCtx)
+ pp.second.sessionCtx = std::make_shared<PushSessionContext>(sessionId);
+ else {
+ std::lock_guard<std::mutex> l(pp.second.sessionCtx->lock);
+ pp.second.sessionCtx->sessionId = sessionId;
+ }
+ }
+ auto response = initHttpResponse(request->create_response());
+ response.append_body(Json::writeString(jsonBuilder_, pp.second.value->toJson()) + "\n");
+ return response.done();
+ }
+ }
+ value->id = std::uniform_int_distribution<Value::Id>{1}(rd);
+ }
+
+ auto vid = value->id;
+ auto& pput = sPuts.puts[vid];
+ pput.value = value;
+ pput.expiration = timeout;
+ if (not pput.expireTimer) {
+ // cancel permanent put
+ pput.expireTimer = std::make_unique<asio::steady_timer>(io_context(), timeout);
+#ifdef OPENDHT_PUSH_NOTIFICATIONS
+ if (not pushToken.empty()) {
+ pput.pushToken = pushToken;
+ pput.clientId = clientId;
+ pput.type = getTypeFromString(platform);
+ if (topic.empty())
+ topic = getDefaultTopic(pput.type);
+ pput.topic = topic;
+ pput.sessionCtx = std::make_shared<PushSessionContext>(sessionId);
+ }
+#endif
+ } else {
+ if (not sessionId.empty()) {
+ if (not pput.sessionCtx)
+ pput.sessionCtx = std::make_shared<PushSessionContext>(sessionId);
+ else {
+ std::lock_guard<std::mutex> l(pput.sessionCtx->lock);
+ pput.sessionCtx->sessionId = sessionId;
+ }
+ }
+ pput.expireTimer->expires_at(timeout);
+ }
+ pput.expireTimer->async_wait(std::bind(&DhtProxyServer::handleCancelPermamentPut, this,
+ std::placeholders::_1, infoHash, vid));
+
+#ifdef OPENDHT_PUSH_NOTIFICATIONS
+ // notify put permanent expiration
+ if (pput.sessionCtx) {
+ auto jsonProvider = [infoHash, clientId, vid, sessionCtx = pput.sessionCtx](){
+ Json::Value json;
+ json["timeout"] = infoHash.toString();
+ json["to"] = clientId;
+ json["vid"] = std::to_string(vid);
+ std::lock_guard<std::mutex> l(sessionCtx->lock);
+ json["s"] = sessionCtx->sessionId;
+ return json;
+ };
+ if (!pput.expireNotifyTimer)
+ pput.expireNotifyTimer = std::make_unique<asio::steady_timer>(io_context(),
+ timeout - proxy::OP_MARGIN);
+ else
+ pput.expireNotifyTimer->expires_at(timeout - proxy::OP_MARGIN);
+ pput.expireNotifyTimer->async_wait(std::bind(
+ &DhtProxyServer::handleNotifyPushListenExpire, this,
+ std::placeholders::_1, pushToken, std::move(jsonProvider), pput.type, pput.topic));
+ }
+#endif
+ }
+
+ dht_->put(infoHash, value, [this, request, value](bool ok){
+ if (ok){
+ auto response = initHttpResponse(request->create_response());
+ response.append_body(Json::writeString(jsonBuilder_, value->toJson()) + "\n");
+ response.done();
+ } else {
+ auto response = initHttpResponse(request->create_response(restinio::status_bad_gateway()));
+ response.set_body(RESP_MSG_PUT_FAILED);
+ response.done();
+ }
+ }, time_point::max(), permanent);
+ return restinio::request_handling_status_t::accepted;
+ } else {
+ auto response = initHttpResponse(request->create_response(restinio::status_bad_request()));
+ response.set_body(RESP_MSG_JSON_INCORRECT);
+ return response.done();
+ }
+ } catch (const std::exception& e){
+ if (logger_)
+ logger_->d("[proxy:server] error in put: %s", e.what());
+ return serverError(*request);
+ }
+}
+
+#ifdef OPENDHT_PROXY_SERVER_IDENTITY
+
+RequestStatus
+DhtProxyServer::putSigned(restinio::request_handle_t request,
+ restinio::router::route_params_t params) const
+{
+ requestNum_++;
+ InfoHash infoHash(params["hash"]);
+ if (!infoHash)
+ infoHash = InfoHash::get(params["hash"]);
+
+ if (request->body().empty()){
+ auto response = initHttpResponse(request->create_response(restinio::status_bad_request()));
+ response.set_body(RESP_MSG_MISSING_PARAMS);
+ return response.done();
+ }
+
+ try {
+ std::string err;
+ Json::Value root;
+ auto* char_data = reinterpret_cast<const char*>(request->body().data());
+ auto reader = std::unique_ptr<Json::CharReader>(jsonReaderBuilder_.newCharReader());
+
+ if (reader->parse(char_data, char_data + request->body().size(), &root, &err)){
+
+ auto value = std::make_shared<Value>(root);
+
+ dht_->putSigned(infoHash, value, [this, request, value](bool ok){
+ if (ok){
+ auto output = Json::writeString(jsonBuilder_, value->toJson()) + "\n";
+ auto response = initHttpResponse(request->create_response());
+ response.append_body(output);
+ response.done();
+ } else {
+ auto response = initHttpResponse(request->create_response(restinio::status_bad_gateway()));
+ response.set_body(RESP_MSG_PUT_FAILED);
+ response.done();
+ }
+ });
+ return restinio::request_handling_status_t::accepted;
+ } else {
+ auto response = initHttpResponse(request->create_response(restinio::status_bad_request()));
+ response.set_body(RESP_MSG_JSON_INCORRECT);
+ return response.done();
+ }
+ } catch (const std::exception& e){
+ if (logger_)
+ logger_->d("[proxy:server] error in putSigned: %s", e.what());
+ return serverError(*request);
+ }
+}
+
+RequestStatus
+DhtProxyServer::putEncrypted(restinio::request_handle_t request,
+ restinio::router::route_params_t params)
+{
+ requestNum_++;
+ InfoHash infoHash(params["hash"]);
+ if (!infoHash)
+ infoHash = InfoHash::get(params["hash"]);
+
+ if (request->body().empty()){
+ auto response = initHttpResponse(request->create_response(restinio::status_bad_request()));
+ response.set_body(RESP_MSG_MISSING_PARAMS);
+ return response.done();
+ }
+
+ try {
+ std::string err;
+ Json::Value root;
+ auto* char_data = reinterpret_cast<const char*>(request->body().data());
+ auto reader = std::unique_ptr<Json::CharReader>(jsonReaderBuilder_.newCharReader());
+
+ if (reader->parse(char_data, char_data + request->body().size(), &root, &err)){
+ InfoHash to(root["to"].asString());
+ if (!to){
+ auto response = initHttpResponse(request->create_response(restinio::status_bad_request()));
+ response.set_body(RESP_MSG_DESTINATION_NOT_FOUND);
+ return response.done();
+ }
+ auto value = std::make_shared<Value>(root);
+ dht_->putEncrypted(infoHash, to, value, [this, request, value](bool ok){
+ if (ok){
+ auto response = initHttpResponse(request->create_response());
+ response.append_body(Json::writeString(jsonBuilder_, value->toJson()) + "\n");
+ response.done();
+ } else {
+ auto response = initHttpResponse(request->create_response(restinio::status_bad_gateway()));
+ response.set_body(RESP_MSG_PUT_FAILED);
+ response.done();
+ }
+ });
+ return restinio::request_handling_status_t::accepted;
+ } else {
+ auto response = initHttpResponse(request->create_response(restinio::status_bad_request()));
+ response.set_body(RESP_MSG_JSON_INCORRECT);
+ return response.done();
+ }
+ } catch (const std::exception& e){
+ if (logger_)
+ logger_->d("[proxy:server] error in put: %s", e.what());
+ return serverError(*request);
+ }
+}
+
+#endif // OPENDHT_PROXY_SERVER_IDENTITY
+
+RequestStatus
+DhtProxyServer::options(restinio::request_handle_t request,
+ restinio::router::route_params_t /*params*/)
+{
+ requestNum_++;
+#ifdef OPENDHT_PROXY_SERVER_IDENTITY
+ const auto methods = "OPTIONS, GET, POST, LISTEN, SIGN, ENCRYPT";
+#else
+ const auto methods = "OPTIONS, GET, POST, LISTEN";
+#endif
+ auto response = initHttpResponse(request->create_response());
+ response.append_header(restinio::http_field::access_control_allow_methods, methods);
+ response.append_header(restinio::http_field::access_control_allow_headers, "content-type");
+ response.append_header(restinio::http_field::access_control_max_age, "86400");
+ return response.done();
+}
+
+RequestStatus
+DhtProxyServer::getFiltered(restinio::request_handle_t request,
+ restinio::router::route_params_t params)
+{
+ requestNum_++;
+ auto query = params["value"];
+ InfoHash infoHash(params["hash"]);
+ if (!infoHash)
+ infoHash = InfoHash::get(params["hash"]);
+
+ try {
+ auto response = std::make_shared<ResponseByPartsBuilder>(
+ initHttpResponse(request->create_response<ResponseByParts>()));
+ response->flush();
+ dht_->get(infoHash,
+ [this, response](const Sp<Value>& value) {
+ response->append_chunk(Json::writeString(jsonBuilder_, value->toJson()) + "\n");
+ response->flush();
+ return true;
+ },
+ [response] (bool /*ok*/){
+ response->done();
+ },
+ {}, query);
+ return restinio::request_handling_status_t::accepted;
+ } catch (const std::exception& e){
+ return serverError(*request);
+ }
+}
+
+}
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ * Authors: Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ * Simon Désaulniers <simon.desaulniers@savoirfairelinux.com>
+ * Sébastien Blin <sebastien.blin@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "dhtrunner.h"
+#include "securedht.h"
+#include "network_utils.h"
+#ifdef OPENDHT_PEER_DISCOVERY
+#include "peer_discovery.h"
+#endif
+#ifdef OPENDHT_PROXY_CLIENT
+#include "dht_proxy_client.h"
+#endif
+
+#include <fstream>
+
+namespace dht {
+
+static const std::string PEER_DISCOVERY_DHT_SERVICE = "dht";
+
+struct NodeInsertionPack {
+ dht::InfoHash nodeId;
+ in_port_t port;
+ dht::NetId net;
+ MSGPACK_DEFINE(nodeId, port, net)
+};
+
+DhtRunner::DhtRunner() : dht_()
+{
+#ifdef _WIN32
+ WSADATA wsd;
+ if (WSAStartup(MAKEWORD(2,2), &wsd) != 0)
+ throw DhtException("Can't initialize Winsock2");
+#endif
+}
+
+DhtRunner::~DhtRunner()
+{
+ join();
+#ifdef _WIN32
+ WSACleanup();
+#endif
+}
+
+void
+DhtRunner::run(in_port_t port, Config& config, Context&& context)
+{
+ config.bind4.setFamily(AF_INET);
+ config.bind4.setPort(port);
+ config.bind6.setFamily(AF_INET6);
+ config.bind6.setPort(port);
+ run(config, std::move(context));
+}
+
+void
+DhtRunner::run(const char* ip4, const char* ip6, const char* service, Config& config, Context&& context)
+{
+ auto res4 = SockAddr::resolve(ip4, service);
+ auto res6 = SockAddr::resolve(ip6, service);
+ if (res4.empty())
+ res4.emplace_back();
+ if (res6.empty())
+ res6.emplace_back();
+ config.bind4 = std::move(res4.front());
+ config.bind6 = std::move(res6.front());
+ run(config, std::move(context));
+}
+
+void
+DhtRunner::run(const Config& config, Context&& context)
+{
+ std::lock_guard<std::mutex> lck(dht_mtx);
+ auto expected = State::Idle;
+ if (not running.compare_exchange_strong(expected, State::Running)) {
+ if (context.logger)
+ context.logger->w("[runner %p] Node is already running. Call join() first before calling run() again.", fmt::ptr(this));
+ return;
+ }
+
+ try {
+ auto local4 = config.bind4;
+ auto local6 = config.bind6;
+ if (not local4 and not local6) {
+ if (context.logger)
+ context.logger->w("[runner %p] No address to bind specified in the configuration, using default addresses", fmt::ptr(this));
+ local4.setFamily(AF_INET);
+ local6.setFamily(AF_INET6);
+ }
+ auto state_path = config.dht_config.node_config.persist_path;
+ if (not state_path.empty())
+ state_path += "_port.txt";
+ if (not state_path.empty() && (local4.getPort() == 0 || local6.getPort() == 0)) {
+ std::ifstream inConfig(state_path);
+ if (inConfig.is_open()) {
+ in_port_t port;
+ if (inConfig >> port) {
+ if (local4.getPort() == 0) {
+ if (context.logger)
+ context.logger->d("[runner %p] Using IPv4 port %hu from saved configuration", fmt::ptr(this), port);
+ local4.setPort(port);
+ }
+ }
+ if (inConfig >> port) {
+ if (local6.getPort() == 0) {
+ if (context.logger)
+ context.logger->d("[runner %p] Using IPv6 port %hu from saved configuration", fmt::ptr(this), port);
+ local6.setPort(port);
+ }
+ }
+ }
+ }
+
+ if (context.logger) {
+ logger_ = context.logger;
+ logger_->d("[runner %p] state changed to Running", fmt::ptr(this));
+ }
+
+#ifdef OPENDHT_PROXY_CLIENT
+ config_ = config;
+ identityAnnouncedCb_ = context.identityAnnouncedCb;
+#endif
+
+ if (config.proxy_server.empty()) {
+ if (not context.sock) {
+ context.sock.reset(new net::UdpSocket(local4, local6, context.logger));
+ }
+ context.sock->setOnReceive([&] (net::PacketList&& pkts) {
+ net::PacketList ret;
+ {
+ std::lock_guard<std::mutex> lck(sock_mtx);
+ rcv.splice(rcv.end(), std::move(pkts));
+ size_t dropped = 0;
+ while (rcv.size() > net::RX_QUEUE_MAX_SIZE) {
+ rcv.pop_front();
+ dropped++;
+ }
+ if (dropped and logger_) {
+ logger_->w("[runner %p] dropped %zu packets: queue is full!", fmt::ptr(this), dropped);
+ }
+ ret = std::move(rcv_free);
+ }
+ cv.notify_all();
+ return ret;
+ });
+ if (not state_path.empty()) {
+ std::ofstream outConfig(state_path);
+ outConfig << context.sock->getBoundRef(AF_INET).getPort() << std::endl;
+ outConfig << context.sock->getBoundRef(AF_INET6).getPort() << std::endl;
+ }
+ auto dht = std::make_unique<Dht>(std::move(context.sock), SecureDht::getConfig(config.dht_config), context.logger);
+ dht_ = std::make_unique<SecureDht>(std::move(dht), config.dht_config, std::move(context.identityAnnouncedCb), context.logger);
+ } else {
+ enableProxy(true);
+ }
+ } catch(const std::exception& e) {
+ config_ = {};
+ identityAnnouncedCb_ = {};
+ dht_.reset();
+ running = State::Idle;
+ throw;
+ }
+
+ if (context.statusChangedCallback) {
+ statusCb = std::move(context.statusChangedCallback);
+ }
+ if (context.certificateStore) {
+ dht_->setLocalCertificateStore(std::move(context.certificateStore));
+ }
+ if (context.publicAddressChangedCb) {
+ dht_->setOnPublicAddressChanged(std::move(context.publicAddressChangedCb));
+ }
+
+ if (not config.threaded)
+ return;
+ dht_thread = std::thread([this]() {
+ while (running != State::Idle) {
+ std::unique_lock<std::mutex> lk(dht_mtx);
+ time_point wakeup = loop_();
+
+ auto hasJobToDo = [this]() {
+ if (running == State::Idle)
+ return true;
+ {
+ std::lock_guard<std::mutex> lck(sock_mtx);
+ if (not rcv.empty())
+ return true;
+ }
+ {
+ std::lock_guard<std::mutex> lck(storage_mtx);
+ if (not pending_ops_prio.empty())
+ return true;
+ auto s = getStatus();
+ if (not pending_ops.empty() and (s == NodeStatus::Connected or s == NodeStatus::Disconnected))
+ return true;
+ }
+ return false;
+ };
+ if (wakeup == time_point::max())
+ cv.wait(lk, hasJobToDo);
+ else
+ cv.wait_until(lk, wakeup, hasJobToDo);
+ }
+ });
+
+ if (config.proxy_server.empty()) {
+ if (config.peer_discovery or config.peer_publish) {
+#ifdef OPENDHT_PEER_DISCOVERY
+ peerDiscovery_ = context.peerDiscovery ?
+ std::move(context.peerDiscovery) :
+ std::make_shared<PeerDiscovery>();
+#else
+ std::cerr << "Peer discovery requested but OpenDHT built without peer discovery support." << std::endl;
+#endif
+ }
+
+#ifdef OPENDHT_PEER_DISCOVERY
+ auto netId = config.dht_config.node_config.network;
+ if (config.peer_discovery) {
+ peerDiscovery_->startDiscovery<NodeInsertionPack>(PEER_DISCOVERY_DHT_SERVICE, [this, netId](NodeInsertionPack&& v, SockAddr&& addr){
+ addr.setPort(v.port);
+ if (v.nodeId != dht_->getNodeId() && netId == v.net){
+ bootstrap(v.nodeId, addr);
+ }
+ });
+ }
+ if (config.peer_publish) {
+ msgpack::sbuffer sbuf_node;
+ NodeInsertionPack adc;
+ adc.net = netId;
+ adc.nodeId = dht_->getNodeId();
+ if (auto socket = dht_->getSocket()) {
+ // IPv4
+ if (const auto& bound4 = socket->getBoundRef(AF_INET)) {
+ adc.port = bound4.getPort();
+ msgpack::pack(sbuf_node, adc);
+ peerDiscovery_->startPublish(AF_INET, PEER_DISCOVERY_DHT_SERVICE, sbuf_node);
+ }
+ // IPv6
+ if (const auto& bound6 = socket->getBoundRef(AF_INET6)) {
+ adc.port = bound6.getPort();
+ sbuf_node.clear();
+ msgpack::pack(sbuf_node, adc);
+ peerDiscovery_->startPublish(AF_INET6, PEER_DISCOVERY_DHT_SERVICE, sbuf_node);
+ }
+ }
+ }
+#endif
+ }
+}
+
+void
+DhtRunner::shutdown(ShutdownCallback cb, bool stop) {
+ std::unique_lock<std::mutex> lck(storage_mtx);
+ auto expected = State::Running;
+ if (not running.compare_exchange_strong(expected, State::Stopping)) {
+ if (expected == State::Stopping and ongoing_ops) {
+ if (cb)
+ shutdownCallbacks_.emplace_back(std::move(cb));
+ }
+ else if (cb) {
+ lck.unlock();
+ cb();
+ }
+ return;
+ }
+ if (logger_)
+ logger_->d("[runner %p] state changed to Stopping, %zu ongoing ops", fmt::ptr(this), ongoing_ops.load());
+ ongoing_ops++;
+ shutdownCallbacks_.emplace_back(std::move(cb));
+ pending_ops.emplace([=](SecureDht&) mutable {
+ auto onShutdown = [this]{ opEnded(); };
+ if (dht_)
+ dht_->shutdown(onShutdown, stop);
+ else
+ opEnded();
+ });
+ cv.notify_all();
+}
+
+void
+DhtRunner::opEnded() {
+ if (--ongoing_ops == 0)
+ checkShutdown();
+}
+
+DoneCallback
+DhtRunner::bindOpDoneCallback(DoneCallback&& cb) {
+ return [this, cb = std::move(cb)](bool ok, const std::vector<std::shared_ptr<Node>>& nodes){
+ if (cb) cb(ok, nodes);
+ opEnded();
+ };
+}
+
+DoneCallbackSimple
+DhtRunner::bindOpDoneCallback(DoneCallbackSimple&& cb) {
+ return [this, cb = std::move(cb)](bool ok){
+ if (cb) cb(ok);
+ opEnded();
+ };
+}
+
+bool
+DhtRunner::checkShutdown() {
+ decltype(shutdownCallbacks_) cbs;
+ {
+ std::lock_guard<std::mutex> lck(storage_mtx);
+ if (running != State::Stopping or ongoing_ops)
+ return false;
+ cbs = std::move(shutdownCallbacks_);
+ }
+ for (auto& cb : cbs)
+ if (cb) cb();
+ return true;
+}
+
+void
+DhtRunner::join()
+{
+ {
+ std::lock_guard<std::mutex> lck(dht_mtx);
+ if (running.exchange(State::Idle) == State::Idle)
+ return;
+ cv.notify_all();
+#ifdef OPENDHT_PEER_DISCOVERY
+ if (peerDiscovery_)
+ peerDiscovery_->stop();
+#endif
+ if (dht_)
+ if (auto sock = dht_->getSocket())
+ sock->stop();
+ if (logger_)
+ logger_->d("[runner %p] state changed to Idle", fmt::ptr(this));
+ }
+
+ if (dht_thread.joinable())
+ dht_thread.join();
+
+ {
+ std::lock_guard<std::mutex> lck(storage_mtx);
+ if (ongoing_ops and logger_) {
+ logger_->w("[runner %p] stopping with %zu remaining ops", fmt::ptr(this), ongoing_ops.load());
+ }
+ pending_ops = decltype(pending_ops)();
+ pending_ops_prio = decltype(pending_ops_prio)();
+ ongoing_ops = 0;
+ shutdownCallbacks_.clear();
+ }
+ {
+ std::lock_guard<std::mutex> lck(dht_mtx);
+ resetDht();
+ status4 = NodeStatus::Disconnected;
+ status6 = NodeStatus::Disconnected;
+ }
+}
+
+SockAddr
+DhtRunner::getBound(sa_family_t af) const {
+ std::lock_guard<std::mutex> lck(dht_mtx);
+ if (dht_)
+ if (auto sock = dht_->getSocket())
+ return sock->getBound(af);
+ return SockAddr{};
+}
+
+in_port_t
+DhtRunner::getBoundPort(sa_family_t af) const {
+ std::lock_guard<std::mutex> lck(dht_mtx);
+ if (dht_)
+ if (auto sock = dht_->getSocket())
+ return sock->getPort(af);
+ return 0;
+}
+
+void
+DhtRunner::dumpTables() const
+{
+ std::lock_guard<std::mutex> lck(dht_mtx);
+ dht_->dumpTables();
+}
+
+InfoHash
+DhtRunner::getId() const
+{
+ return dht_ ? dht_->getId() : InfoHash{};
+}
+
+std::shared_ptr<crypto::PublicKey>
+DhtRunner::getPublicKey() const
+{
+ return dht_ ? dht_->getPublicKey() : std::shared_ptr<crypto::PublicKey>{};
+}
+
+InfoHash
+DhtRunner::getNodeId() const
+{
+ return dht_ ? dht_->getNodeId() : InfoHash{};
+}
+
+
+std::pair<size_t, size_t>
+DhtRunner::getStoreSize() const {
+ std::lock_guard<std::mutex> lck(dht_mtx);
+ if (!dht_)
+ return {};
+ return dht_->getStoreSize();
+}
+
+void
+DhtRunner::setStorageLimit(size_t limit) {
+ std::lock_guard<std::mutex> lck(dht_mtx);
+ if (!dht_)
+ throw std::runtime_error("dht is not running");
+ return dht_->setStorageLimit(limit);
+}
+
+std::vector<NodeExport>
+DhtRunner::exportNodes() const {
+ std::lock_guard<std::mutex> lck(dht_mtx);
+ if (!dht_)
+ return {};
+ return dht_->exportNodes();
+}
+
+std::vector<ValuesExport>
+DhtRunner::exportValues() const {
+ std::lock_guard<std::mutex> lck(dht_mtx);
+ if (!dht_)
+ return {};
+ return dht_->exportValues();
+}
+
+void
+DhtRunner::setLogger(const Sp<Logger>& logger) {
+ std::lock_guard<std::mutex> lck(dht_mtx);
+ logger_ = logger;
+ if (dht_)
+ dht_->setLogger(logger);
+}
+
+void
+DhtRunner::setLogFilter(const InfoHash& f) {
+ std::lock_guard<std::mutex> lck(dht_mtx);
+ if (dht_)
+ dht_->setLogFilter(f);
+}
+
+void
+DhtRunner::registerType(const ValueType& type) {
+ std::lock_guard<std::mutex> lck(dht_mtx);
+ dht_->registerType(type);
+}
+
+void
+DhtRunner::importValues(const std::vector<ValuesExport>& values) {
+ std::lock_guard<std::mutex> lck(dht_mtx);
+ dht_->importValues(values);
+}
+
+unsigned
+DhtRunner::getNodesStats(sa_family_t af, unsigned *good_return, unsigned *dubious_return, unsigned *cached_return, unsigned *incoming_return) const
+{
+ std::lock_guard<std::mutex> lck(dht_mtx);
+ const auto stats = dht_->getNodesStats(af);
+ if (good_return)
+ *good_return = stats.good_nodes;
+ if (dubious_return)
+ *dubious_return = stats.dubious_nodes;
+ if (cached_return)
+ *cached_return = stats.cached_nodes;
+ if (incoming_return)
+ *incoming_return = stats.incoming_nodes;
+ return stats.good_nodes + stats.dubious_nodes;
+}
+
+NodeStats
+DhtRunner::getNodesStats(sa_family_t af) const
+{
+ std::lock_guard<std::mutex> lck(dht_mtx);
+ return dht_->getNodesStats(af);
+}
+
+NodeInfo
+DhtRunner::getNodeInfo() const {
+ std::lock_guard<std::mutex> lck(dht_mtx);
+ NodeInfo info {};
+ if (dht_) {
+ info.id = dht_->getId();
+ info.node_id = dht_->getNodeId();
+ info.ipv4 = dht_->getNodesStats(AF_INET);
+ info.ipv6 = dht_->getNodesStats(AF_INET6);
+ if (auto sock = dht_->getSocket()) {
+ info.bound4 = sock->getBoundRef(AF_INET).getPort();
+ info.bound6 = sock->getBoundRef(AF_INET6).getPort();
+ }
+ }
+ info.ongoing_ops = ongoing_ops;
+ return info;
+}
+
+void
+DhtRunner::getNodeInfo(std::function<void(std::shared_ptr<NodeInfo>)> cb)
+{
+ std::lock_guard<std::mutex> lck(storage_mtx);
+ ongoing_ops++;
+ pending_ops_prio.emplace([cb = std::move(cb), this](SecureDht& dht){
+ auto sinfo = std::make_shared<NodeInfo>();
+ auto& info = *sinfo;
+ info.id = dht.getId();
+ info.node_id = dht.getNodeId();
+ info.ipv4 = dht.getNodesStats(AF_INET);
+ info.ipv6 = dht.getNodesStats(AF_INET6);
+ std::tie(info.storage_size, info.storage_values) = dht.getStoreSize();
+ if (auto sock = dht.getSocket()) {
+ info.bound4 = sock->getBoundRef(AF_INET).getPort();
+ info.bound6 = sock->getBoundRef(AF_INET6).getPort();
+ }
+ info.ongoing_ops = ongoing_ops;
+ cb(std::move(sinfo));
+ opEnded();
+ });
+ cv.notify_all();
+}
+
+std::vector<unsigned>
+DhtRunner::getNodeMessageStats(bool in) const
+{
+ std::lock_guard<std::mutex> lck(dht_mtx);
+ return dht_->getNodeMessageStats(in);
+}
+
+std::string
+DhtRunner::getStorageLog() const
+{
+ std::lock_guard<std::mutex> lck(dht_mtx);
+ return dht_->getStorageLog();
+}
+std::string
+DhtRunner::getStorageLog(const InfoHash& f) const
+{
+ std::lock_guard<std::mutex> lck(dht_mtx);
+ return dht_->getStorageLog(f);
+}
+std::string
+DhtRunner::getRoutingTablesLog(sa_family_t af) const
+{
+ std::lock_guard<std::mutex> lck(dht_mtx);
+ return dht_->getRoutingTablesLog(af);
+}
+std::string
+DhtRunner::getSearchesLog(sa_family_t af) const
+{
+ std::lock_guard<std::mutex> lck(dht_mtx);
+ return dht_->getSearchesLog(af);
+}
+std::string
+DhtRunner::getSearchLog(const InfoHash& f, sa_family_t af) const
+{
+ std::lock_guard<std::mutex> lck(dht_mtx);
+ return dht_->getSearchLog(f, af);
+}
+std::vector<SockAddr>
+DhtRunner::getPublicAddress(sa_family_t af) const
+{
+ std::lock_guard<std::mutex> lck(dht_mtx);
+ if (dht_)
+ return dht_->getPublicAddress(af);
+ return {};
+}
+std::vector<std::string>
+DhtRunner::getPublicAddressStr(sa_family_t af) const
+{
+ auto addrs = getPublicAddress(af);
+ std::vector<std::string> ret(addrs.size());
+ std::transform(addrs.begin(), addrs.end(), ret.begin(), [](const SockAddr& a) { return a.toString(); });
+ return ret;
+}
+
+void
+DhtRunner::getPublicAddress(std::function<void(std::vector<SockAddr>&&)> cb, sa_family_t af)
+{
+ std::lock_guard<std::mutex> lck(storage_mtx);
+ ongoing_ops++;
+ pending_ops_prio.emplace([cb = std::move(cb), this, af](SecureDht& dht){
+ cb(dht.getPublicAddress(af));
+ opEnded();
+ });
+ cv.notify_all();
+}
+
+void
+DhtRunner::registerCertificate(std::shared_ptr<crypto::Certificate> cert) {
+ std::lock_guard<std::mutex> lck(dht_mtx);
+ dht_->registerCertificate(cert);
+}
+
+void
+DhtRunner::setLocalCertificateStore(CertificateStoreQuery&& query_method) {
+ std::lock_guard<std::mutex> lck(dht_mtx);
+ if (dht_)
+ dht_->setLocalCertificateStore(std::forward<CertificateStoreQuery>(query_method));
+}
+
+time_point
+DhtRunner::loop_()
+{
+ if (not dht_)
+ return {};
+
+ decltype(pending_ops) ops {};
+ {
+ std::lock_guard<std::mutex> lck(storage_mtx);
+ auto s = getStatus();
+ ops = (pending_ops_prio.empty() && (s == NodeStatus::Connected or s == NodeStatus::Disconnected)) ?
+ std::move(pending_ops) : std::move(pending_ops_prio);
+ }
+ while (not ops.empty()) {
+ ops.front()(*dht_);
+ ops.pop();
+ }
+
+ time_point wakeup {};
+ decltype(rcv) received {};
+ decltype(rcv) received_treated {};
+ {
+ std::lock_guard<std::mutex> lck(sock_mtx);
+ // move to stack
+ received = std::move(rcv);
+ }
+
+ // Discard old packets
+ size_t dropped {0};
+ if (not received.empty()) {
+ auto limit = clock::now() - net::RX_QUEUE_MAX_DELAY;
+ auto it = received.begin();
+ while (it != received.end() and it->received < limit) {
+ it->data.clear();
+ ++it;
+ dropped++;
+ }
+ received_treated.splice(received_treated.end(), received, received.begin(), it);
+ }
+
+ // Handle packets
+ if (not received.empty()) {
+ for (auto& pkt : received) {
+ auto now = clock::now();
+ if (now - pkt.received > net::RX_QUEUE_MAX_DELAY)
+ dropped++;
+ else
+ wakeup = dht_->periodic(pkt.data.data(), pkt.data.size(), std::move(pkt.from), now);
+ pkt.data.clear();
+ }
+ received_treated.splice(received_treated.end(), std::move(received));
+ } else {
+ // Or just run the scheduler
+ wakeup = dht_->periodic(nullptr, 0, nullptr, 0, clock::now());
+ }
+
+ if (not received_treated.empty()) {
+ std::lock_guard<std::mutex> lck(sock_mtx);
+ if (rcv_free.size() < net::RX_QUEUE_MAX_SIZE)
+ rcv_free.splice(rcv_free.end(), std::move(received_treated));
+ }
+
+ if (dropped && logger_)
+ logger_->e("[runner %p] Dropped %zu packets with high delay.", fmt::ptr(this), dropped);
+
+ NodeStatus nstatus4 = dht_->updateStatus(AF_INET);
+ NodeStatus nstatus6 = dht_->updateStatus(AF_INET6);
+ if (nstatus4 != status4 || nstatus6 != status6) {
+ status4 = nstatus4;
+ status6 = nstatus6;
+ if (statusCb)
+ statusCb(status4, status6);
+ }
+
+ return wakeup;
+}
+
+void
+DhtRunner::get(InfoHash hash, GetCallback vcb, DoneCallback dcb, Value::Filter f, Where w)
+{
+ std::unique_lock<std::mutex> lck(storage_mtx);
+ if (running != State::Running) {
+ lck.unlock();
+ if (dcb) dcb(false, {});
+ return;
+ }
+ ongoing_ops++;
+ pending_ops.emplace([=](SecureDht& dht) mutable {
+ dht.get(hash, std::move(vcb), bindOpDoneCallback(std::move(dcb)), std::move(f), std::move(w));
+ });
+ cv.notify_all();
+}
+
+void
+DhtRunner::get(const std::string& key, GetCallback vcb, DoneCallbackSimple dcb, Value::Filter f, Where w)
+{
+ get(InfoHash::get(key), std::move(vcb), std::move(dcb), std::move(f), std::move(w));
+}
+void
+DhtRunner::query(const InfoHash& hash, QueryCallback cb, DoneCallback done_cb, Query q) {
+ std::unique_lock<std::mutex> lck(storage_mtx);
+ if (running != State::Running) {
+ lck.unlock();
+ if (done_cb) done_cb(false, {});
+ return;
+ }
+ ongoing_ops++;
+ pending_ops.emplace([=](SecureDht& dht) mutable {
+ dht.query(hash, std::move(cb), bindOpDoneCallback(std::move(done_cb)), std::move(q));
+ });
+ cv.notify_all();
+}
+
+std::future<size_t>
+DhtRunner::listen(InfoHash hash, ValueCallback vcb, Value::Filter f, Where w)
+{
+ auto ret_token = std::make_shared<std::promise<size_t>>();
+ std::unique_lock<std::mutex> lck(storage_mtx);
+ if (running != State::Running) {
+ lck.unlock();
+ ret_token->set_value(0);
+ return ret_token->get_future();
+ }
+ pending_ops.emplace([=](SecureDht& dht) mutable {
+ ret_token->set_value(dht.listen(hash, std::move(vcb), std::move(f), std::move(w)));
+ });
+ cv.notify_all();
+ return ret_token->get_future();
+}
+
+std::future<size_t>
+DhtRunner::listen(const std::string& key, GetCallback vcb, Value::Filter f, Where w)
+{
+ return listen(InfoHash::get(key), std::move(vcb), std::move(f), std::move(w));
+}
+
+void
+DhtRunner::cancelListen(InfoHash h, size_t token)
+{
+ std::lock_guard<std::mutex> lck(storage_mtx);
+ if (running != State::Running)
+ return;
+ ongoing_ops++;
+ pending_ops.emplace([=](SecureDht& dht) {
+ dht.cancelListen(h, token);
+ opEnded();
+ });
+ cv.notify_all();
+}
+
+void
+DhtRunner::cancelListen(InfoHash h, std::shared_future<size_t> ftoken)
+{
+ std::lock_guard<std::mutex> lck(storage_mtx);
+ if (running != State::Running)
+ return;
+ ongoing_ops++;
+ pending_ops.emplace([this, h, ftoken = std::move(ftoken)](SecureDht& dht) {
+ dht.cancelListen(h, ftoken.get());
+ opEnded();
+ });
+ cv.notify_all();
+}
+
+void
+DhtRunner::put(InfoHash hash, Value&& value, DoneCallback cb, time_point created, bool permanent)
+{
+ std::unique_lock<std::mutex> lck(storage_mtx);
+ if (running != State::Running) {
+ lck.unlock();
+ if (cb) cb(false, {});
+ return;
+ }
+ ongoing_ops++;
+ pending_ops.emplace([=,
+ cb = std::move(cb),
+ sv = std::make_shared<Value>(std::move(value))
+ ] (SecureDht& dht) mutable {
+ dht.put(hash, sv, bindOpDoneCallback(std::move(cb)), created, permanent);
+ });
+ cv.notify_all();
+}
+
+void
+DhtRunner::put(InfoHash hash, std::shared_ptr<Value> value, DoneCallback cb, time_point created, bool permanent)
+{
+ std::unique_lock<std::mutex> lck(storage_mtx);
+ if (running != State::Running) {
+ lck.unlock();
+ if (cb) cb(false, {});
+ return;
+ }
+ ongoing_ops++;
+ pending_ops.emplace([=, value = std::move(value), cb = std::move(cb)](SecureDht& dht) mutable {
+ dht.put(hash, value, bindOpDoneCallback(std::move(cb)), created, permanent);
+ });
+ cv.notify_all();
+}
+
+void
+DhtRunner::put(const std::string& key, Value&& value, DoneCallbackSimple cb, time_point created, bool permanent)
+{
+ put(InfoHash::get(key), std::forward<Value>(value), std::move(cb), created, permanent);
+}
+
+void
+DhtRunner::cancelPut(const InfoHash& h, Value::Id id)
+{
+ std::lock_guard<std::mutex> lck(storage_mtx);
+ pending_ops.emplace([=](SecureDht& dht) {
+ dht.cancelPut(h, id);
+ });
+ cv.notify_all();
+}
+
+void
+DhtRunner::cancelPut(const InfoHash& h, const std::shared_ptr<Value>& value)
+{
+ std::lock_guard<std::mutex> lck(storage_mtx);
+ pending_ops.emplace([=](SecureDht& dht) {
+ dht.cancelPut(h, value->id);
+ });
+ cv.notify_all();
+}
+
+void
+DhtRunner::putSigned(InfoHash hash, std::shared_ptr<Value> value, DoneCallback cb, bool permanent)
+{
+ std::unique_lock<std::mutex> lck(storage_mtx);
+ if (running != State::Running) {
+ lck.unlock();
+ if (cb) cb(false, {});
+ return;
+ }
+ ongoing_ops++;
+ pending_ops.emplace([=,
+ cb = std::move(cb),
+ value = std::move(value)
+ ](SecureDht& dht) mutable {
+ dht.putSigned(hash, value, bindOpDoneCallback(std::move(cb)), permanent);
+ });
+ cv.notify_all();
+}
+
+void
+DhtRunner::putSigned(InfoHash hash, Value&& value, DoneCallback cb, bool permanent)
+{
+ putSigned(hash, std::make_shared<Value>(std::move(value)), std::move(cb), permanent);
+}
+
+void
+DhtRunner::putSigned(const std::string& key, Value&& value, DoneCallbackSimple cb, bool permanent)
+{
+ putSigned(InfoHash::get(key), std::forward<Value>(value), std::move(cb), permanent);
+}
+
+void
+DhtRunner::putEncrypted(InfoHash hash, InfoHash to, std::shared_ptr<Value> value, DoneCallback cb, bool permanent)
+{
+ std::unique_lock<std::mutex> lck(storage_mtx);
+ if (running != State::Running) {
+ lck.unlock();
+ if (cb) cb(false, {});
+ return;
+ }
+ ongoing_ops++;
+ pending_ops.emplace([=,
+ cb = std::move(cb),
+ value = std::move(value)
+ ] (SecureDht& dht) mutable {
+ dht.putEncrypted(hash, to, value, bindOpDoneCallback(std::move(cb)), permanent);
+ });
+ cv.notify_all();
+}
+
+void
+DhtRunner::putEncrypted(InfoHash hash, InfoHash to, Value&& value, DoneCallback cb, bool permanent)
+{
+ putEncrypted(hash, to, std::make_shared<Value>(std::move(value)), std::move(cb), permanent);
+}
+
+void
+DhtRunner::putEncrypted(const std::string& key, InfoHash to, Value&& value, DoneCallback cb, bool permanent)
+{
+ putEncrypted(InfoHash::get(key), to, std::forward<Value>(value), std::move(cb), permanent);
+}
+
+void
+DhtRunner::putEncrypted(InfoHash hash, const std::shared_ptr<crypto::PublicKey>& to, std::shared_ptr<Value> value, DoneCallback cb, bool permanent)
+{
+ std::unique_lock<std::mutex> lck(storage_mtx);
+ if (running != State::Running) {
+ lck.unlock();
+ if (cb) cb(false, {});
+ return;
+ }
+ ongoing_ops++;
+ pending_ops.emplace([=,
+ cb = std::move(cb),
+ value = std::move(value)
+ ] (SecureDht& dht) mutable {
+ dht.putEncrypted(hash, *to, value, bindOpDoneCallback(std::move(cb)), permanent);
+ });
+ cv.notify_all();
+}
+
+void
+DhtRunner::putEncrypted(InfoHash hash, const std::shared_ptr<crypto::PublicKey>& to, Value&& value, DoneCallback cb, bool permanent)
+{
+ putEncrypted(hash, to, std::make_shared<Value>(std::move(value)), std::move(cb), permanent);
+}
+
+void
+DhtRunner::bootstrap(const std::string& host, const std::string& service)
+{
+ std::lock_guard<std::mutex> lck(storage_mtx);
+ pending_ops_prio.emplace([host, service] (SecureDht& dht) mutable {
+ dht.addBootstrap(host, service);
+ });
+ cv.notify_all();
+}
+
+void
+DhtRunner::bootstrap(const std::string& hostService)
+{
+ std::lock_guard<std::mutex> lck(storage_mtx);
+ pending_ops_prio.emplace([host_service = splitPort(hostService)] (SecureDht& dht) mutable {
+ dht.addBootstrap(host_service.first, host_service.second);
+ });
+ cv.notify_all();
+}
+
+void
+DhtRunner::clearBootstrap()
+{
+ std::lock_guard<std::mutex> lck(storage_mtx);
+ pending_ops_prio.emplace([] (SecureDht& dht) mutable {
+ dht.clearBootstrap();
+ });
+ cv.notify_all();
+}
+
+void
+DhtRunner::bootstrap(std::vector<SockAddr> nodes, DoneCallbackSimple cb)
+{
+ if (running != State::Running) {
+ cb(false);
+ return;
+ }
+ std::lock_guard<std::mutex> lck(storage_mtx);
+ ongoing_ops++;
+ pending_ops_prio.emplace([
+ cb = bindOpDoneCallback(std::move(cb)),
+ nodes = std::move(nodes)
+ ] (SecureDht& dht) mutable {
+ auto rem = cb ? std::make_shared<std::pair<size_t, bool>>(nodes.size(), false) : nullptr;
+ for (auto& node : nodes) {
+ if (node.getPort() == 0)
+ node.setPort(net::DHT_DEFAULT_PORT);
+ dht.pingNode(std::move(node), [rem,cb](bool ok) {
+ auto& r = *rem;
+ r.first--;
+ r.second |= ok;
+ if (r.first == 0) {
+ cb(r.second);
+ }
+ });
+ }
+ });
+ cv.notify_all();
+}
+
+void
+DhtRunner::bootstrap(SockAddr addr, DoneCallbackSimple cb)
+{
+ std::unique_lock<std::mutex> lck(storage_mtx);
+ if (running != State::Running) {
+ lck.unlock();
+ if (cb) cb(false);
+ return;
+ }
+ ongoing_ops++;
+ pending_ops_prio.emplace([addr = std::move(addr), cb = bindOpDoneCallback(std::move(cb))](SecureDht& dht) mutable {
+ dht.pingNode(std::move(addr), std::move(cb));
+ });
+ cv.notify_all();
+}
+
+void
+DhtRunner::bootstrap(const InfoHash& id, const SockAddr& address)
+{
+ std::lock_guard<std::mutex> lck(storage_mtx);
+ if (running != State::Running)
+ return;
+ pending_ops_prio.emplace([id, address](SecureDht& dht) mutable {
+ dht.insertNode(id, address);
+ });
+ cv.notify_all();
+}
+
+void
+DhtRunner::bootstrap(std::vector<NodeExport> nodes)
+{
+ std::lock_guard<std::mutex> lck(storage_mtx);
+ if (running != State::Running)
+ return;
+ pending_ops_prio.emplace([nodes = std::move(nodes)](SecureDht& dht) {
+ for (auto& node : nodes)
+ dht.insertNode(node);
+ });
+ cv.notify_all();
+}
+
+void
+DhtRunner::connectivityChanged()
+{
+ std::lock_guard<std::mutex> lck(storage_mtx);
+ pending_ops_prio.emplace([=](SecureDht& dht) {
+ dht.connectivityChanged();
+#ifdef OPENDHT_PEER_DISCOVERY
+ if (peerDiscovery_)
+ peerDiscovery_->connectivityChanged();
+#endif
+ });
+ cv.notify_all();
+}
+
+void
+DhtRunner::findCertificate(InfoHash hash, std::function<void(const Sp<crypto::Certificate>&)> cb) {
+ std::unique_lock<std::mutex> lck(storage_mtx);
+ if (running != State::Running) {
+ lck.unlock();
+ cb({});
+ return;
+ }
+ ongoing_ops++;
+ pending_ops.emplace([this, hash, cb = std::move(cb)] (SecureDht& dht) {
+ dht.findCertificate(hash, [this, cb = std::move(cb)](const Sp<crypto::Certificate>& crt){
+ cb(crt);
+ opEnded();
+ });
+ });
+ cv.notify_all();
+}
+
+void
+DhtRunner::resetDht()
+{
+ peerDiscovery_.reset();
+ dht_.reset();
+}
+
+void
+DhtRunner::setProxyServer(const std::string& proxy, const std::string& pushNodeId)
+{
+#ifdef OPENDHT_PROXY_CLIENT
+ std::lock_guard<std::mutex> lck(dht_mtx);
+ if (config_.proxy_server == proxy and config_.push_node_id == pushNodeId)
+ return;
+ config_.proxy_server = proxy;
+ config_.push_node_id = pushNodeId;
+ enableProxy(use_proxy and not config_.proxy_server.empty());
+#else
+ if (not proxy.empty())
+ throw std::runtime_error("DHT proxy requested but OpenDHT built without proxy support.");
+ (void) pushNodeId;
+#endif
+}
+
+void
+DhtRunner::enableProxy(bool proxify)
+{
+#ifdef OPENDHT_PROXY_CLIENT
+ if (dht_) {
+ dht_->shutdown({});
+ }
+ if (proxify) {
+ // Init the proxy client
+ auto dht_via_proxy = std::make_unique<DhtProxyClient>(
+ config_.server_ca,
+ config_.client_identity,
+ [this]{
+ if (config_.threaded) {
+ std::lock_guard<std::mutex> lck(storage_mtx);
+ pending_ops_prio.emplace([=](SecureDht&) mutable {});
+ cv.notify_all();
+ }
+ },
+ config_.proxy_server, config_.push_node_id, logger_);
+ if (not config_.push_token.empty())
+ dht_via_proxy->setPushNotificationToken(config_.push_token);
+ if (not config_.push_topic.empty())
+ dht_via_proxy->setPushNotificationTopic(config_.push_topic);
+ if (not config_.push_platform.empty())
+ dht_via_proxy->setPushNotificationPlatform(config_.push_platform);
+ dht_ = std::make_unique<SecureDht>(std::move(dht_via_proxy), config_.dht_config, identityAnnouncedCb_, logger_);
+ // and use it
+ use_proxy = proxify;
+ } else {
+ use_proxy = proxify;
+ }
+#else
+ if (proxify)
+ throw std::runtime_error("DHT proxy requested but OpenDHT built without proxy support.");
+#endif
+}
+
+void
+DhtRunner::forwardAllMessages(bool forward)
+{
+ std::lock_guard<std::mutex> lck(dht_mtx);
+ if (dht_)
+ dht_->forwardAllMessages(forward);
+}
+
+/**
+ * Updates the push notification device token
+ */
+void
+DhtRunner::setPushNotificationToken(const std::string& token) {
+ std::lock_guard<std::mutex> lck(dht_mtx);
+#if defined(OPENDHT_PROXY_CLIENT) && defined(OPENDHT_PUSH_NOTIFICATIONS)
+ config_.push_token = token;
+ if (dht_)
+ dht_->setPushNotificationToken(token);
+#else
+ (void) token;
+#endif
+}
+
+void
+DhtRunner::setPushNotificationTopic(const std::string& topic) {
+ std::lock_guard<std::mutex> lck(dht_mtx);
+#if defined(OPENDHT_PROXY_CLIENT) && defined(OPENDHT_PUSH_NOTIFICATIONS)
+ config_.push_topic = topic;
+ if (dht_)
+ dht_->setPushNotificationTopic(topic);
+#else
+ (void) topic;
+#endif
+}
+
+void
+DhtRunner::setPushNotificationPlatform(const std::string& platform) {
+ std::lock_guard<std::mutex> lck(dht_mtx);
+#if defined(OPENDHT_PROXY_CLIENT) && defined(OPENDHT_PUSH_NOTIFICATIONS)
+ config_.push_platform = platform;
+ if (dht_)
+ dht_->setPushNotificationPlatform(platform);
+#else
+ (void) platform;
+#endif
+}
+
+void
+DhtRunner::pushNotificationReceived(const std::map<std::string, std::string>& data)
+{
+#if defined(OPENDHT_PROXY_CLIENT) && defined(OPENDHT_PUSH_NOTIFICATIONS)
+ std::lock_guard<std::mutex> lck(storage_mtx);
+ pending_ops_prio.emplace([=](SecureDht&) {
+ if (dht_)
+ dht_->pushNotificationReceived(data);
+ });
+ cv.notify_all();
+#else
+ (void) data;
+#endif
+}
+
+}
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ * Author: Vsevolod Ivanov <vsevolod.ivanov@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "http.h"
+#include "logger.h"
+#include "crypto.h"
+#include "base64.h"
+#include "compat/os_cert.h"
+
+#include <asio.hpp>
+#include <restinio/impl/tls_socket.hpp>
+#include <http_parser.h>
+#include <json/json.h>
+
+#include <openssl/ocsp.h>
+#include <openssl/ssl.h>
+#include <openssl/asn1.h>
+#include <openssl/x509.h>
+#include <openssl/x509_vfy.h>
+
+#include <cctype>
+#include <iomanip>
+#include <sstream>
+
+#define MAXAGE_SEC (14*24*60*60)
+#define JITTER_SEC (60)
+#define OCSP_MAX_RESPONSE_SIZE (20480)
+#ifdef _WIN32
+#define timegm _mkgmtime
+#endif
+
+namespace dht {
+namespace http {
+
+constexpr const char HTTP_HEADER_CONTENT_TYPE_JSON[] = "application/json";
+constexpr const char HTTP_HEADER_DELIM[] = "\r\n\r\n";
+constexpr const char HTTP_PROTOCOL[] = "http://";
+constexpr const char HTTPS_PROTOCOL[] = "https://";
+constexpr const char ORIGIN_PROTOCOL[] = "//";
+constexpr unsigned MAX_REDIRECTS {5};
+
+Url::Url(const std::string& url): url(url)
+{
+ size_t addr_begin = 0;
+ // protocol
+ const size_t proto_end = url.find("://");
+ if (proto_end != std::string::npos){
+ addr_begin = proto_end + 3;
+ if (url.substr(0, proto_end) == "https"){
+ protocol = "https";
+ }
+ }
+ // host and service
+ size_t addr_size = url.substr(addr_begin).find("/");
+ if (addr_size == std::string::npos)
+ addr_size = url.size() - addr_begin;
+ auto host_service = splitPort(url.substr(addr_begin, addr_size));
+ host = host_service.first;
+ if (!host_service.second.empty())
+ service = host_service.second;
+ // target, query and fragment
+ size_t query_begin = url.find("?");
+ auto addr_end = addr_begin + addr_size;
+ if (addr_end < url.size())
+ target = url.substr(addr_end);
+ size_t fragment_begin = url.find("#");
+ if (fragment_begin == std::string::npos){
+ query = url.substr(query_begin + 1);
+ } else {
+ target = url.substr(addr_end, fragment_begin - addr_end);
+ query = url.substr(query_begin + 1, fragment_begin - query_begin - 1);
+ fragment = url.substr(fragment_begin);
+ }
+}
+
+std::string
+Url::toString() const
+{
+ std::ostringstream ss;
+ if (not protocol.empty()) {
+ ss << protocol << "://";
+ }
+ ss << host;
+ if (not service.empty()) {
+ ss << ':' << service;
+ }
+ ss << target;
+ return ss.str();
+}
+
+// connection
+
+std::atomic_uint Connection::ids_ {1};
+
+std::shared_ptr<asio::ssl::context>
+newTlsClientContext(const std::shared_ptr<dht::Logger>& logger)
+{
+ auto ctx = std::make_shared<asio::ssl::context>(asio::ssl::context::tls_client);
+ ctx->set_verify_mode(asio::ssl::verify_peer | asio::ssl::verify_fail_if_no_peer_cert);
+
+ if (char* path = getenv("CA_ROOT_FILE")) {
+ if (logger)
+ logger->debug("Using CA file: {}", path);
+ ctx->load_verify_file(path);
+ } else if (char* path = getenv("CA_ROOT_PATH")) {
+ if (logger)
+ logger->debug("Using CA path: {}", path);
+ ctx->add_verify_path(path);
+ } else {
+#ifdef __ANDROID__
+ if (logger)
+ logger->d("Using CA path: /system/etc/security/cacerts");
+ ctx->add_verify_path("/system/etc/security/cacerts");
+#elif defined(WIN32) || defined(TARGET_OS_OSX)
+ PEMCache::instance(logger).fillX509Store(ctx->native_handle());
+#else
+ if (logger)
+ logger->d("Using default CA path");
+ ctx->set_default_verify_paths();
+#endif
+ }
+ return ctx;
+}
+
+Connection::Connection(asio::io_context& ctx, const bool ssl, std::shared_ptr<dht::Logger> l)
+ : id_(Connection::ids_++), ctx_(ctx), istream_(&read_buf_), logger_(l)
+{
+ if (ssl) {
+ ssl_ctx_ = newTlsClientContext(l);
+ ssl_socket_ = std::make_unique<ssl_socket_t>(ctx_, ssl_ctx_);
+ if (logger_)
+ logger_->d("[connection:%i] start https session with system CA", id_);
+ }
+ else {
+ socket_ = std::make_unique<socket_t>(ctx);
+ if (logger_)
+ logger_->d("[connection:%i] start http session", id_);
+ }
+}
+
+Connection::Connection(asio::io_context& ctx, std::shared_ptr<dht::crypto::Certificate> server_ca,
+ const dht::crypto::Identity& identity, std::shared_ptr<dht::Logger> l)
+ : id_(Connection::ids_++), ctx_(ctx), istream_(&read_buf_), logger_(l)
+{
+ asio::error_code ec;
+ if (server_ca) {
+ ssl_ctx_ = std::make_shared<asio::ssl::context>(asio::ssl::context::tls_client);
+ ssl_ctx_->set_verify_mode(asio::ssl::verify_peer | asio::ssl::verify_fail_if_no_peer_cert);
+ auto ca = server_ca->toString(false/*chain*/);
+ ssl_ctx_->add_certificate_authority(asio::const_buffer{ca.data(), ca.size()}, ec);
+ if (ec)
+ throw std::runtime_error("Error adding certificate authority: " + ec.message());
+ else if (logger_)
+ logger_->d("[connection:%i] start https with custom CA %s", id_, server_ca->getUID().c_str());
+ } else {
+ ssl_ctx_ = newTlsClientContext(l);
+ if (logger_)
+ logger_->d("[connection:%i] start https session with system CA", id_);
+ }
+ if (identity.first){
+ auto key = identity.first->serialize();
+ ssl_ctx_->use_private_key(asio::const_buffer{key.data(), key.size()},
+ asio::ssl::context::file_format::pem, ec);
+ if (ec)
+ throw std::runtime_error("Error setting client private key: " + ec.message());
+ }
+ if (identity.second){
+ auto cert = identity.second->toString(true/*chain*/);
+ ssl_ctx_->use_certificate_chain(asio::const_buffer{cert.data(), cert.size()}, ec);
+ if (ec)
+ throw std::runtime_error("Error adding client certificate: " + ec.message());
+ else if (logger_)
+ logger_->d("[connection:%i] client certificate %s", id_, identity.second->getUID().c_str());
+ }
+ ssl_socket_ = std::make_unique<ssl_socket_t>(ctx_, ssl_ctx_);
+}
+
+Connection::~Connection() {
+ close();
+}
+
+void
+Connection::close()
+{
+ std::lock_guard<std::mutex> lock(mutex_);
+ asio::error_code ec;
+ if (ssl_socket_) {
+ if (ssl_socket_->is_open())
+ ssl_socket_->close(ec);
+ }
+ else if (socket_) {
+ if (socket_->is_open())
+ socket_->close(ec);
+ }
+ if (ec and logger_)
+ logger_->e("[connection:%i] error closing: %s", id_, ec.message().c_str());
+}
+
+bool
+Connection::is_open() const
+{
+ if (ssl_socket_) return ssl_socket_->is_open();
+ else if (socket_) return socket_->is_open();
+ else return false;
+}
+
+bool
+Connection::is_ssl() const
+{
+ return ssl_ctx_ ? true : false;
+}
+
+static time_t
+parse_ocsp_time(ASN1_GENERALIZEDTIME* gt)
+{
+ struct tm tm;
+ time_t rv = -1;
+
+ if (gt == nullptr)
+ return -1;
+ // RFC 6960 specifies that all times in OCSP must be GENERALIZEDTIME
+ if (ASN1_time_parse((const char*)gt->data, gt->length, &tm, V_ASN1_GENERALIZEDTIME) == -1)
+ return -1;
+ if ((rv = timegm(&tm)) == -1)
+ return -1;
+ return rv;
+}
+
+static inline X509*
+cert_from_chain(STACK_OF(X509)* fullchain)
+{
+ return sk_X509_value(fullchain, 0);
+}
+
+static X509*
+issuer_from_chain(STACK_OF(X509)* fullchain)
+{
+ X509 *cert, *issuer;
+ X509_NAME *issuer_name;
+
+ cert = cert_from_chain(fullchain);
+ if ((issuer_name = X509_get_issuer_name(cert)) == nullptr)
+ return nullptr;
+
+ issuer = X509_find_by_subject(fullchain, issuer_name);
+ return issuer;
+}
+
+using OscpRequestPtr = std::unique_ptr<OCSP_REQUEST, decltype(&OCSP_REQUEST_free)>;
+struct OscpRequestInfo {
+ OscpRequestPtr req {nullptr, &OCSP_REQUEST_free};
+ std::string data;
+ std::string url;
+};
+
+static std::unique_ptr<OscpRequestInfo>
+ocspRequestFromCert(STACK_OF(X509)* fullchain, const std::shared_ptr<Logger>& logger, bool nonce = false)
+{
+ if (fullchain == nullptr)
+ return {};
+
+ if (sk_X509_num(fullchain) <= 1) {
+ if (logger)
+ logger->e("Cert does not contain a cert chain");
+ return {};
+ }
+ X509* cert = cert_from_chain(fullchain);
+ if (cert == nullptr) {
+ if (logger)
+ logger->e("No certificate found");
+ return {};
+ }
+ X509* issuer = issuer_from_chain(fullchain);
+ if (issuer == nullptr) {
+ if (logger)
+ logger->e("Unable to find issuer for cert");
+ return {};
+ }
+
+ auto urls = X509_get1_ocsp(cert);
+ if (urls == nullptr || sk_OPENSSL_STRING_num(urls) <= 0) {
+ if (logger)
+ logger->e("Certificate contains no OCSP url");
+ return {};
+ }
+ auto url = sk_OPENSSL_STRING_value(urls, 0);
+ if (url == nullptr)
+ return {};
+
+ auto request = std::make_unique<OscpRequestInfo>();
+ request->req = OscpRequestPtr(OCSP_REQUEST_new(), &OCSP_REQUEST_free);
+ request->url = url;
+ X509_email_free(urls);
+
+ OCSP_CERTID* id = OCSP_cert_to_id(EVP_sha1(), cert, issuer);
+ if (id == nullptr) {
+ if (logger)
+ logger->e("Unable to get certificate id from cert");
+ return {};
+ }
+ if (OCSP_request_add0_id(request->req.get(), id) == nullptr) {
+ if (logger)
+ logger->e("Unable to add certificate id to request");
+ return {};
+ }
+
+ if (nonce)
+ OCSP_request_add1_nonce(request->req.get(), nullptr, -1);
+
+ int size;
+ uint8_t* data {nullptr};
+ if ((size = i2d_OCSP_REQUEST(request->req.get(), &data)) <= 0) {
+ if (logger)
+ logger->e("Unable to encode ocsp request");
+ return {};
+ }
+ if (data == nullptr) {
+ if (logger)
+ logger->e("Unable to allocte memory");
+ return {};
+ }
+ request->data = std::string((char*)data, (char*)data+size);
+ free(data);
+ return request;
+}
+
+bool
+ocspValidateResponse(const OscpRequestInfo& info, STACK_OF(X509)* fullchain, const std::string& response, X509_STORE *store, const std::shared_ptr<Logger>& logger)
+{
+ ASN1_GENERALIZEDTIME *revtime = nullptr, *thisupd = nullptr, *nextupd = nullptr;
+ const uint8_t* p = (const uint8_t*)response.data();
+ int status, cert_status=0, crl_reason=0;
+ time_t now, rev_t = -1, this_t, next_t;
+
+ X509* cert = cert_from_chain(fullchain);
+ if (cert == nullptr) {
+ if (logger)
+ logger->e("No certificate found");
+ return false;
+ }
+ X509* issuer = issuer_from_chain(fullchain);
+ if (issuer == nullptr) {
+ if (logger)
+ logger->e("Unable to find issuer for cert");
+ return false;
+ }
+
+ OCSP_CERTID *cidr;
+ if ((cidr = OCSP_cert_to_id(nullptr, cert, issuer)) == nullptr) {
+ if (logger)
+ logger->e("Unable to get issuer cert/CID");
+ return false;
+ }
+ std::unique_ptr<OCSP_CERTID, decltype(&OCSP_CERTID_free)> cid(cidr, &OCSP_CERTID_free);
+
+ OCSP_RESPONSE *r;
+ if ((r = d2i_OCSP_RESPONSE(nullptr, &p, response.size())) == nullptr) {
+ if (logger)
+ logger->e("OCSP response unserializable");
+ return false;
+ }
+ std::unique_ptr<OCSP_RESPONSE, decltype(&OCSP_RESPONSE_free)> resp(r, &OCSP_RESPONSE_free);
+
+ OCSP_BASICRESP *brespr;
+ if ((brespr = OCSP_response_get1_basic(resp.get())) == nullptr) {
+ if (logger)
+ logger->e("Failed to load OCSP response");
+ return false;
+ }
+ std::unique_ptr<OCSP_BASICRESP, decltype(&OCSP_BASICRESP_free)> bresp(brespr, &OCSP_BASICRESP_free);
+
+ if (OCSP_basic_verify(bresp.get(), fullchain, store, OCSP_TRUSTOTHER) != 1) {
+ if (logger)
+ logger->w("OCSP verify failed");
+ return false;
+ }
+ printf("OCSP response signature validated\n");
+
+ status = OCSP_response_status(resp.get());
+ if (status != OCSP_RESPONSE_STATUS_SUCCESSFUL) {
+ if (logger)
+ logger->w("OCSP Failure: code %d (%s)", status, OCSP_response_status_str(status));
+ return false;
+ }
+
+ // Check the nonce if we sent one
+ if (OCSP_check_nonce(info.req.get(), bresp.get()) <= 0) {
+ if (logger)
+ logger->w("No OCSP nonce, or mismatch");
+ return false;
+ }
+
+ if (OCSP_resp_find_status(bresp.get(), cid.get(), &cert_status, &crl_reason,
+ &revtime, &thisupd, &nextupd) != 1) {
+ if (logger)
+ logger->w("OCSP verify failed: no result for cert");
+ return false;
+ }
+
+ if (revtime && (rev_t = parse_ocsp_time(revtime)) == -1) {
+ if (logger)
+ logger->w("Unable to parse revocation time in OCSP reply");
+ return false;
+ }
+ // Belt and suspenders, Treat it as revoked if there is either
+ // a revocation time, or status revoked.
+ if (rev_t != -1 || cert_status == V_OCSP_CERTSTATUS_REVOKED) {
+ if (logger)
+ logger->w("Invalid OCSP reply: certificate is revoked");
+ if (rev_t != -1) {
+ if (logger)
+ logger->w("Certificate revoked at: %s", ctime(&rev_t));
+ }
+ return false;
+ }
+ if ((this_t = parse_ocsp_time(thisupd)) == -1) {
+ if (logger)
+ logger->w("unable to parse this update time in OCSP reply");
+ return false;
+ }
+ if ((next_t = parse_ocsp_time(nextupd)) == -1) {
+ if (logger)
+ logger->w("unable to parse next update time in OCSP reply");
+ return false;
+ }
+
+ // Don't allow this update to precede next update
+ if (this_t >= next_t) {
+ if (logger)
+ logger->w("Invalid OCSP reply: this update >= next update");
+ return false;
+ }
+
+ now = time(nullptr);
+ // Check that this update is not more than JITTER seconds in the future.
+ if (this_t > now + JITTER_SEC) {
+ if (logger)
+ logger->e("Invalid OCSP reply: this update is in the future (%s)", ctime(&this_t));
+ return false;
+ }
+
+ // Check that this update is not more than MAXSEC in the past.
+ if (this_t < now - MAXAGE_SEC) {
+ if (logger)
+ logger->e("Invalid OCSP reply: this update is too old (%s)", ctime(&this_t));
+ return false;
+ }
+
+ // Check that next update is still valid
+ if (next_t < now - JITTER_SEC) {
+ if (logger)
+ logger->w("Invalid OCSP reply: reply has expired (%s)", ctime(&next_t));
+ return false;
+ }
+
+ if (logger) {
+ logger->d("OCSP response validated");
+ logger->d(" This Update: %s", ctime(&this_t));
+ logger->d(" Next Update: %s", ctime(&next_t));
+ }
+ return true;
+}
+
+void
+Connection::set_ssl_verification(const std::string& hostname, const asio::ssl::verify_mode verify_mode)
+{
+ std::lock_guard<std::mutex> lock(mutex_);
+ if (ssl_socket_) {
+ // Set SNI Hostname (many hosts need this to handshake successfully)
+ SSL_set_tlsext_host_name(ssl_socket_->asio_ssl_stream().native_handle(), hostname.c_str());
+ ssl_socket_->asio_ssl_stream().set_verify_mode(verify_mode);
+ if (verify_mode != asio::ssl::verify_none) {
+ ssl_socket_->asio_ssl_stream().set_verify_callback([
+ id = id_, logger = logger_, hostname, checkOcsp = checkOcsp_
+ ] (bool preverified, asio::ssl::verify_context& ctx) -> bool {
+ X509* cert = X509_STORE_CTX_get_current_cert(ctx.native_handle());
+ if (logger) {
+ char subject_name[1024];
+ X509_NAME_oneline(X509_get_subject_name(cert), subject_name, 1024);
+ logger->d("[connection:%i] verify %s compliance to RFC 2818:\n%s", id, hostname.c_str(), subject_name);
+ }
+
+ // starts from CA and goes down the presented chain
+ auto verifier = asio::ssl::rfc2818_verification(hostname);
+ bool verified = verifier(preverified, ctx);
+ auto verify_ec = X509_STORE_CTX_get_error(ctx.native_handle());
+ if (verify_ec != 0 /*X509_V_OK*/ and logger)
+ logger->e("[http::connection:%i] ssl verification error=%i %d", id, verify_ec, verified);
+ if (verified and checkOcsp) {
+ std::unique_ptr<stack_st_X509, void(*)(stack_st_X509*)> chain(
+ X509_STORE_CTX_get1_chain(ctx.native_handle()),
+ [](stack_st_X509* c){ sk_X509_pop_free(c, X509_free); });
+ if (auto ocspInfo = ocspRequestFromCert(chain.get(), logger)) {
+ if (logger)
+ logger->w("[http::connection:%i] TLS OCSP server: %s, request size: %zu", id, ocspInfo->url.c_str(), ocspInfo->data.size());
+ bool ocspVerified = false;
+ asio::io_context io_ctx;
+ auto ocspReq = std::make_shared<Request>(io_ctx, ocspInfo->url, [&](const Response& ocspResp){
+ if (ocspResp.status_code == 200) {
+ ocspVerified = ocspValidateResponse(*ocspInfo, chain.get(), ocspResp.body, X509_STORE_CTX_get0_store(ctx.native_handle()), logger);
+ } else {
+ if (logger)
+ logger->w("[http::connection:%i] TLS OCSP check error", id);
+ }
+ }, logger);
+ ocspReq->set_method(restinio::http_method_post());
+ ocspReq->set_header_field(restinio::http_field_t::content_type, "application/ocsp-request");
+ ocspReq->set_body(ocspInfo->data);
+ ocspReq->send();
+ io_ctx.run();
+ if (not ocspVerified)
+ return false;
+ }
+ }
+ return verified;
+ }
+ );
+ }
+ }
+}
+
+asio::streambuf&
+Connection::input()
+{
+ return write_buf_;
+}
+
+std::string
+Connection::read_bytes(size_t bytes)
+{
+ std::lock_guard<std::mutex> lock(mutex_);
+ if (bytes == 0)
+ bytes = read_buf_.in_avail();
+ std::string content;
+ content.resize(bytes);
+ auto rb = istream_.readsome(&content[0], bytes);
+ content.resize(rb);
+ return content;
+}
+
+std::string
+Connection::read_until(const char delim)
+{
+ std::string content;
+ std::getline(istream_, content, delim);
+ return content;
+}
+
+void
+Connection::async_connect(std::vector<asio::ip::tcp::endpoint>&& endpoints, ConnectHandlerCb cb)
+{
+ std::lock_guard<std::mutex> lock(mutex_);
+ if (!ssl_socket_ && !socket_) {
+ cb(asio::error::operation_aborted, {});
+ return;
+ }
+ auto& base = ssl_socket_? ssl_socket_->lowest_layer() : *socket_;
+
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-variable"
+ ConnectHandlerCb wcb = [this, &base, cb=std::move(cb)](const asio::error_code& ec, const asio::ip::tcp::endpoint& endpoint) {
+ if (!ec) {
+ auto socket = base.native_handle();
+ local_address_ = base.local_endpoint().address();
+ // Once connected, set a keep alive on the TCP socket with 30 seconds delay
+ // This will generate broken pipes as soon as possible.
+ // Note this needs to be done once connected to have a valid native_handle()
+ uint32_t start = 30;
+ uint32_t interval = 30;
+ uint32_t cnt = 1;
+#ifdef _WIN32
+ std::string val = "1";
+ setsockopt(socket, SOL_SOCKET, SO_KEEPALIVE, val.c_str(), sizeof(val));
+
+ // TCP_KEEPIDLE and TCP_KEEPINTVL are available since Win 10 version 1709
+ // TCP_KEEPCNT since Win 10 version 1703
+#ifdef TCP_KEEPIDLE
+ std::string start_str = std::to_string(start);
+ setsockopt(socket, IPPROTO_TCP, TCP_KEEPIDLE,
+ start_str.c_str(), sizeof(start_str));
+#endif
+#ifdef TCP_KEEPINTVL
+ std::string interval_str = std::to_string(interval);
+ setsockopt(socket, IPPROTO_TCP, TCP_KEEPINTVL,
+ interval_str.c_str(), sizeof(interval_str));
+#endif
+#ifdef TCP_KEEPCNT
+ std::string cnt_str = std::to_string(cnt);
+ setsockopt(socket, IPPROTO_TCP, TCP_KEEPCNT,
+ cnt_str.c_str(), sizeof(cnt_str));
+#endif
+#else
+ uint32_t val = 1;
+ setsockopt(socket, SOL_SOCKET, SO_KEEPALIVE, &val, sizeof(uint32_t));
+#ifdef __APPLE__
+ // Apple devices only have one parameter
+ setsockopt(socket, IPPROTO_TCP, TCP_KEEPALIVE, &start, sizeof(uint32_t));
+#else
+ // Linux based systems
+ setsockopt(socket, SOL_TCP, TCP_KEEPIDLE, &start, sizeof(uint32_t));
+ setsockopt(socket, SOL_TCP, TCP_KEEPINTVL, &interval, sizeof(uint32_t));
+ setsockopt(socket, SOL_TCP, TCP_KEEPCNT, &cnt, sizeof(uint32_t));
+#endif
+#endif
+ }
+ if (cb)
+ cb(ec, endpoint);
+ };
+#pragma GCC diagnostic pop
+
+ if (ssl_socket_)
+ asio::async_connect(ssl_socket_->lowest_layer(), std::move(endpoints), wrapCallback(std::move(wcb)));
+ else
+ asio::async_connect(*socket_, std::move(endpoints), wrapCallback(std::move(wcb)));
+}
+
+void
+Connection::async_handshake(HandlerCb cb)
+{
+ std::lock_guard<std::mutex> lock(mutex_);
+ if (ssl_socket_) {
+ std::weak_ptr<Connection> wthis = shared_from_this();
+ ssl_socket_->async_handshake(asio::ssl::stream<asio::ip::tcp::socket>::client,
+ [wthis, cb](const asio::error_code& ec)
+ {
+ if (ec == asio::error::operation_aborted)
+ return;
+ if (auto sthis = wthis.lock()) {
+ auto& this_ = *sthis;
+ auto verify_ec = SSL_get_verify_result(this_.ssl_socket_->asio_ssl_stream().native_handle());
+ if (this_.logger_) {
+ if (verify_ec == X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT /*18*/
+ || verify_ec == X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN /*19*/)
+ this_.logger_->d("[connection:%i] self-signed certificate in handshake: %i", this_.id_, verify_ec);
+ else if (verify_ec != X509_V_OK)
+ this_.logger_->e("[connection:%i] verify handshake error: %i", this_.id_, verify_ec);
+ else
+ this_.logger_->w("[connection:%i] verify handshake success", this_.id_);
+ }
+ }
+ if (cb)
+ cb(ec);
+ });
+ }
+ else if (socket_)
+ cb(asio::error::no_protocol_option);
+ else if (cb)
+ cb(asio::error::operation_aborted);
+}
+
+void
+Connection::async_write(BytesHandlerCb cb)
+{
+ std::lock_guard<std::mutex> lock(mutex_);
+ if (!is_open()) {
+ if (cb) ctx_.post([cb](){ cb(asio::error::broken_pipe, 0); });
+ return;
+ }
+ if (ssl_socket_) asio::async_write(*ssl_socket_, write_buf_, wrapCallback(std::move(cb)));
+ else if (socket_) asio::async_write(*socket_, write_buf_, wrapCallback(std::move(cb)));
+ else if (cb) ctx_.post([cb](){ cb(asio::error::operation_aborted, 0); });
+}
+
+void
+Connection::async_read_until(const char* delim, BytesHandlerCb cb)
+{
+ std::lock_guard<std::mutex> lock(mutex_);
+ if (!is_open()) {
+ if (cb) ctx_.post([cb](){ cb(asio::error::broken_pipe, 0); });
+ return;
+ }
+ if (ssl_socket_) asio::async_read_until(*ssl_socket_, read_buf_, delim, wrapCallback(std::move(cb)));
+ else if (socket_) asio::async_read_until(*socket_, read_buf_, delim, wrapCallback(std::move(cb)));
+ else if (cb) ctx_.post([cb](){ cb(asio::error::operation_aborted, 0); });
+}
+
+void
+Connection::async_read_until(char delim, BytesHandlerCb cb)
+{
+ std::lock_guard<std::mutex> lock(mutex_);
+ if (!is_open()) {
+ if (cb) ctx_.post([cb](){ cb(asio::error::broken_pipe, 0); });
+ return;
+ }
+ if (ssl_socket_) asio::async_read_until(*ssl_socket_, read_buf_, delim, wrapCallback(std::move(cb)));
+ else if (socket_) asio::async_read_until(*socket_, read_buf_, delim, wrapCallback(std::move(cb)));
+ else if (cb) ctx_.post([cb](){ cb(asio::error::operation_aborted, 0); });
+}
+
+void
+Connection::async_read(size_t bytes, BytesHandlerCb cb)
+{
+ std::lock_guard<std::mutex> lock(mutex_);
+ if (!is_open()) {
+ if (cb) ctx_.post([cb](){ cb(asio::error::broken_pipe, 0); });
+ return;
+ }
+ if (ssl_socket_) asio::async_read(*ssl_socket_, read_buf_, asio::transfer_exactly(bytes), wrapCallback(std::move(cb)));
+ else if (socket_) asio::async_read(*socket_, read_buf_, asio::transfer_exactly(bytes), wrapCallback(std::move(cb)));
+ else if (cb) ctx_.post([cb](){ cb(asio::error::operation_aborted, 0); });
+}
+
+void
+Connection::async_read_some(size_t bytes, BytesHandlerCb cb)
+{
+ std::lock_guard<std::mutex> lock(mutex_);
+ if (!is_open()) {
+ if (cb) ctx_.post([cb](){ cb(asio::error::broken_pipe, 0); });
+ return;
+ }
+ auto buf = read_buf_.prepare(bytes);
+ auto onEnd = [this_=shared_from_this(), cb=std::move(cb)](const asio::error_code& ec, size_t t){
+ this_->read_buf_.commit(t);
+ cb(ec, t);
+ };
+ if (ssl_socket_) ssl_socket_->async_read_some(buf, onEnd);
+ else socket_->async_read_some(buf, onEnd);
+}
+
+const asio::ip::address&
+Connection::local_address() const
+{
+ return local_address_;
+}
+
+void
+Connection::timeout(const std::chrono::seconds& timeout, HandlerCb cb)
+{
+ if (!timeout_timer_)
+ timeout_timer_ = std::make_unique<asio::steady_timer>(ctx_);
+ timeout_timer_->expires_at(std::chrono::steady_clock::now() + timeout);
+ timeout_timer_->async_wait([id=id_, logger=logger_, cb](const asio::error_code &ec){
+ if (ec == asio::error::operation_aborted)
+ return;
+ else if (ec){
+ if (logger)
+ logger->e("[connection:%i] timeout error: %s", id, ec.message().c_str());
+ }
+ if (cb)
+ cb(ec);
+ });
+}
+
+// Resolver
+
+Resolver::Resolver(asio::io_context& ctx, const std::string& url, std::shared_ptr<dht::Logger> logger)
+ : url_(url), resolver_(ctx), destroyed_(std::make_shared<bool>(false)), logger_(logger)
+{
+ resolve(url_.host, url_.service.empty() ? url_.protocol : url_.service);
+}
+
+Resolver::Resolver(asio::io_context& ctx, const std::string& host, const std::string& service,
+ const bool ssl, std::shared_ptr<dht::Logger> logger)
+ : resolver_(ctx), destroyed_(std::make_shared<bool>(false)), logger_(logger)
+{
+ url_.host = host;
+ url_.service = service;
+ url_.protocol = (ssl ? "https" : "http");
+ resolve(url_.host, url_.service.empty() ? url_.protocol : url_.service);
+}
+
+Resolver::Resolver(asio::io_context& ctx, std::vector<asio::ip::tcp::endpoint> endpoints, const bool ssl,
+ std::shared_ptr<dht::Logger> logger)
+ : resolver_(ctx), destroyed_(std::make_shared<bool>(false)), logger_(logger)
+{
+ url_.protocol = (ssl ? "https" : "http");
+ endpoints_ = std::move(endpoints);
+ completed_ = true;
+}
+
+Resolver::~Resolver()
+{
+ decltype(cbs_) cbs;
+ {
+ std::lock_guard<std::mutex> lock(mutex_);
+ cbs = std::move(cbs_);
+ }
+ while (not cbs.empty()){
+ auto cb = cbs.front();
+ if (cb)
+ cb(asio::error::operation_aborted, {});
+ cbs.pop();
+ }
+ *destroyed_ = true;
+}
+
+inline
+std::vector<asio::ip::tcp::endpoint>
+filter(const std::vector<asio::ip::tcp::endpoint>& epts, sa_family_t family)
+{
+ if (family == AF_UNSPEC)
+ return epts;
+ std::vector<asio::ip::tcp::endpoint> ret;
+ for (const auto& ep : epts) {
+ if (family == AF_INET && ep.address().is_v4())
+ ret.emplace_back(ep);
+ else if (family == AF_INET6 && ep.address().is_v6())
+ ret.emplace_back(ep);
+ }
+ return ret;
+}
+
+void
+Resolver::add_callback(ResolverCb cb, sa_family_t family)
+{
+ std::lock_guard<std::mutex> lock(mutex_);
+ if (!completed_)
+ cbs_.emplace(family == AF_UNSPEC ? std::move(cb) : [cb, family](const asio::error_code& ec, const std::vector<asio::ip::tcp::endpoint>& endpoints){
+ if (ec)
+ cb(ec, endpoints);
+ else
+ cb(ec, filter(endpoints, family));
+ });
+ else
+ cb(ec_, family == AF_UNSPEC ? endpoints_ : filter(endpoints_, family));
+}
+
+void
+Resolver::resolve(const std::string& host, const std::string& service)
+{
+ asio::ip::tcp::resolver::query query_(host, service);
+ resolver_.async_resolve(query_, [this, host, service, destroyed = destroyed_]
+ (const asio::error_code& ec, asio::ip::tcp::resolver::results_type endpoints)
+ {
+ if (ec == asio::error::operation_aborted or *destroyed)
+ return;
+ if (logger_) {
+ if (ec)
+ logger_->e("[http:client] [resolver] error for %s:%s: %s",
+ host.c_str(), service.c_str(), ec.message().c_str());
+ }
+ decltype(cbs_) cbs;
+ {
+ std::lock_guard<std::mutex> lock(mutex_);
+ ec_ = ec;
+ endpoints_ = std::vector<asio::ip::tcp::endpoint>{endpoints.begin(), endpoints.end()};
+ completed_ = true;
+ cbs = std::move(cbs_);
+ }
+ while (not cbs.empty()){
+ auto cb = cbs.front();
+ if (cb)
+ cb(ec, endpoints_);
+ cbs.pop();
+ }
+ });
+}
+
+// Request
+
+std::atomic_uint Request::ids_ {1};
+
+
+Request::Request(asio::io_context& ctx, const std::string& url, const Json::Value& json, OnJsonCb jsoncb,
+ std::shared_ptr<dht::Logger> logger)
+ : logger_(std::move(logger)), id_(Request::ids_++), ctx_(ctx),
+ resolver_(std::make_shared<Resolver>(ctx, url, logger))
+{
+ init_default_headers();
+ set_header_field(restinio::http_field_t::content_type, HTTP_HEADER_CONTENT_TYPE_JSON);
+ set_header_field(restinio::http_field_t::accept, HTTP_HEADER_CONTENT_TYPE_JSON);
+ Json::StreamWriterBuilder wbuilder;
+ set_method(restinio::http_method_post());
+ set_body(Json::writeString(wbuilder, json));
+ add_on_done_callback([this, jsoncb](const Response& response){
+ Json::Value json;
+ if (response.status_code != 0) {
+ std::string err;
+ Json::CharReaderBuilder rbuilder;
+ auto reader = std::unique_ptr<Json::CharReader>(rbuilder.newCharReader());
+ if (!reader->parse(response.body.data(), response.body.data() + response.body.size(), &json, &err) and logger_)
+ logger_->e("[http:request:%i] can't parse response to json", id_, err.c_str());
+ }
+ if (jsoncb)
+ jsoncb(std::move(json), response);
+ });
+}
+
+Request::Request(asio::io_context& ctx, const std::string& url, OnJsonCb jsoncb, std::shared_ptr<dht::Logger> logger)
+ : logger_(std::move(logger)), id_(Request::ids_++), ctx_(ctx),
+ resolver_(std::make_shared<Resolver>(ctx, url, logger))
+{
+ init_default_headers();
+ set_header_field(restinio::http_field_t::accept, HTTP_HEADER_CONTENT_TYPE_JSON);
+ Json::StreamWriterBuilder wbuilder;
+ set_method(restinio::http_method_get());
+ add_on_done_callback([this, jsoncb](const Response& response) {
+ Json::Value json;
+ if (response.status_code != 0) {
+ std::string err;
+ Json::CharReaderBuilder rbuilder;
+ auto reader = std::unique_ptr<Json::CharReader>(rbuilder.newCharReader());
+ if (!reader->parse(response.body.data(), response.body.data() + response.body.size(), &json, &err) and logger_)
+ logger_->e("[http:request:%i] can't parse response to json", id_, err.c_str());
+ }
+ if (jsoncb)
+ jsoncb(std::move(json), response);
+ });
+}
+
+Request::Request(asio::io_context& ctx, const std::string& url, std::shared_ptr<dht::Logger> logger)
+ : logger_(logger), id_(Request::ids_++), ctx_(ctx),
+ resolver_(std::make_shared<Resolver>(ctx, url, logger))
+{
+ init_default_headers();
+}
+
+Request::Request(asio::io_context& ctx, const std::string& url, OnDoneCb onDone, std::shared_ptr<dht::Logger> logger)
+ : logger_(logger), id_(Request::ids_++), ctx_(ctx), resolver_(std::make_shared<Resolver>(ctx, url, logger))
+{
+ init_default_headers();
+ add_on_done_callback(std::move(onDone));
+}
+
+Request::Request(asio::io_context& ctx, const std::string& host, const std::string& service,
+ const bool ssl, std::shared_ptr<dht::Logger> logger)
+ : logger_(logger), id_(Request::ids_++), ctx_(ctx),
+ resolver_(std::make_shared<Resolver>(ctx, host, service, ssl, logger))
+{
+ init_default_headers();
+}
+
+Request::Request(asio::io_context& ctx, std::shared_ptr<Resolver> resolver, sa_family_t family)
+ : logger_(resolver->getLogger()), id_(Request::ids_++), ctx_(ctx), family_(family), resolver_(resolver)
+{
+ init_default_headers();
+}
+
+Request::Request(asio::io_context& ctx, std::vector<asio::ip::tcp::endpoint>&& endpoints, const bool ssl,
+ std::shared_ptr<dht::Logger> logger)
+ : logger_(logger), id_(Request::ids_++), ctx_(ctx),
+ resolver_(std::make_shared<Resolver>(ctx, std::move(endpoints), ssl, logger))
+{
+ init_default_headers();
+}
+
+Request::Request(asio::io_context& ctx, std::shared_ptr<Resolver> resolver, const std::string& target, sa_family_t family)
+ : logger_(resolver->getLogger()), id_(Request::ids_++), ctx_(ctx), family_(family), resolver_(resolver)
+{
+ set_header_field(restinio::http_field_t::host, get_url().host + ":" + get_url().service);
+ set_target(Url(target).target);
+}
+
+Request::~Request()
+{
+ resolver_.reset();
+ terminate(asio::error::connection_aborted);
+}
+
+void
+Request::init_default_headers()
+{
+ const auto& url = resolver_->get_url();
+ set_header_field(restinio::http_field_t::user_agent, "Mozilla/5.0");
+ set_header_field(restinio::http_field_t::accept, "text/html");
+ set_target(url.target);
+}
+
+void
+Request::cancel()
+{
+ if (auto c = conn_)
+ c->close();
+}
+
+void
+Request::set_connection(std::shared_ptr<Connection> connection) {
+ conn_ = std::move(connection);
+}
+
+std::shared_ptr<Connection>
+Request::get_connection() const {
+ return conn_;
+}
+
+void
+Request::set_certificate_authority(std::shared_ptr<dht::crypto::Certificate> certificate) {
+ server_ca_ = certificate;
+}
+
+void
+Request::set_identity(const dht::crypto::Identity& identity) {
+ client_identity_ = identity;
+}
+
+void
+Request::set_logger(std::shared_ptr<dht::Logger> logger) {
+ logger_ = logger;
+}
+
+void
+Request::set_header(restinio::http_request_header_t header)
+{
+ header_ = header;
+}
+
+void
+Request::set_method(restinio::http_method_id_t method) {
+ header_.method(method);
+}
+
+void
+Request::set_target(std::string target) {
+ header_.request_target(target.empty() ? "/" : std::move(target));
+}
+
+void
+Request::set_header_field(restinio::http_field_t field, std::string value) {
+ headers_[field] = std::move(value);
+}
+
+void
+Request::set_connection_type(restinio::http_connection_header_t connection) {
+ connection_type_ = connection;
+}
+
+void
+Request::set_body(std::string body) {
+ body_ = std::move(body);
+}
+
+void
+Request::set_auth(const std::string& username, const std::string& password)
+{
+ std::vector<uint8_t> creds;
+ creds.reserve(username.size() + password.size() + 1);
+ creds.insert(creds.end(), username.begin(), username.end());
+ creds.emplace_back(':');
+ creds.insert(creds.end(), password.begin(), password.end());
+ set_header_field(restinio::http_field_t::authorization, "Basic " + base64_encode(creds));
+}
+
+void
+Request::build()
+{
+ std::ostringstream request;
+ bool append_body = !body_.empty();
+
+ // first header
+ request << header_.method().c_str() << " " << header_.request_target() << " " <<
+ "HTTP/" << header_.http_major() << "." << header_.http_minor() << "\r\n";
+
+ // other headers
+ for (auto header: headers_){
+ request << restinio::field_to_string(header.first) << ": " << header.second << "\r\n";
+ if (header.first == restinio::http_field_t::expect and header.second == "100-continue")
+ append_body = false;
+ }
+
+ // last connection header
+ const char* conn_str = nullptr;
+ switch (connection_type_){
+ case restinio::http_connection_header_t::keep_alive:
+ conn_str = "keep-alive";
+ break;
+ case restinio::http_connection_header_t::upgrade:
+ if (logger_)
+ logger_->e("Unsupported connection type 'upgrade', fallback to 'close'");
+ // fallthrough
+ case restinio::http_connection_header_t::close:
+ conn_str = "close"; // default
+ break;
+ }
+ if (conn_str)
+ request << "Connection: " << conn_str << "\r\n";
+
+ // body & content-length
+ if (append_body) {
+ request << "Content-Length: " << body_.size() << "\r\n\r\n"
+ << body_;
+ } else
+ request << "\r\n";
+ request_ = request.str();
+}
+
+// https://stackoverflow.com/a/17708801
+std::string
+Request::url_encode(std::string_view value)
+{
+ std::ostringstream escaped;
+ escaped.fill('0');
+ escaped << std::hex;
+
+ for (const char& c : value) {
+ // Keep alphanumeric and other accepted characters intact
+ if (std::isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') {
+ escaped << c;
+ continue;
+ }
+
+ // Any other characters are percent-encoded
+ escaped << std::uppercase;
+ escaped << '%' << std::setw(2) << static_cast<int>(static_cast<unsigned char>(c));
+ escaped << std::nouppercase;
+ }
+
+ return escaped.str();
+}
+
+void
+Request::add_on_status_callback(OnStatusCb cb) {
+ cbs_.on_status = std::move(cb);
+}
+
+void
+Request::add_on_body_callback(OnDataCb cb) {
+ cbs_.on_body = std::move(cb);
+}
+
+void
+Request::add_on_state_change_callback(OnStateChangeCb cb) {
+ cbs_.on_state_change = std::move(cb);
+}
+
+void
+Request::add_on_done_callback(OnDoneCb cb) {
+ add_on_state_change_callback([onDone=std::move(cb)](State state, const Response& response){
+ if (state == Request::State::DONE)
+ onDone(response);
+ });
+}
+
+void
+Request::notify_state_change(State state) {
+ state_ = state;
+ if (cbs_.on_state_change)
+ cbs_.on_state_change(state, response_);
+}
+
+void
+Request::init_parser()
+{
+ response_.request = shared_from_this();
+
+ if (!parser_)
+ parser_ = std::make_unique<http_parser>();
+ http_parser_init(parser_.get(), HTTP_RESPONSE);
+ parser_->data = static_cast<void*>(this);
+
+ if (!parser_s_)
+ parser_s_ = std::make_unique<http_parser_settings>();
+ http_parser_settings_init(parser_s_.get());
+
+ cbs_.on_status = [this, statusCb = std::move(cbs_.on_status)](unsigned int status_code){
+ response_.status_code = status_code;
+ if (statusCb)
+ statusCb(status_code);
+ };
+ auto header_field = std::make_shared<std::string>();
+ cbs_.on_header_field = [header_field](const char* at, size_t length) {
+ *header_field = std::string(at, length);
+ };
+ cbs_.on_header_value = [this, header_field](const char* at, size_t length) {
+ response_.headers[*header_field] = std::string(at, length);
+ };
+
+ // http_parser raw c callback (note: no context can be passed into them)
+ parser_s_->on_status = [](http_parser* parser, const char* /*at*/, size_t /*length*/) -> int {
+ static_cast<Request*>(parser->data)->cbs_.on_status(parser->status_code);
+ return 0;
+ };
+ parser_s_->on_header_field = [](http_parser* parser, const char* at, size_t length) -> int {
+ static_cast<Request*>(parser->data)->cbs_.on_header_field(at, length);
+ return 0;
+ };
+ parser_s_->on_header_value = [](http_parser* parser, const char* at, size_t length) -> int {
+ static_cast<Request*>(parser->data)->cbs_.on_header_value(at, length);
+ return 0;
+ };
+ parser_s_->on_body = [](http_parser* parser, const char* at, size_t length) -> int {
+ static_cast<Request*>(parser->data)->onBody(at, length);
+ return 0;
+ };
+ parser_s_->on_headers_complete = [](http_parser* parser) -> int {
+ static_cast<Request*>(parser->data)->onHeadersComplete();
+ return 0;
+ };
+ parser_s_->on_message_complete = [](http_parser* parser) -> int {
+ static_cast<Request*>(parser->data)->onComplete();
+ return 0;
+ };
+}
+
+void
+Request::connect(std::vector<asio::ip::tcp::endpoint>&& endpoints, HandlerCb cb)
+{
+ if (endpoints.empty()){
+ if (logger_)
+ logger_->e("[http:request:%i] connect: no endpoints provided", id_);
+ if (cb)
+ cb(asio::error::connection_aborted);
+ return;
+ }
+ if (logger_){
+ std::string eps = "";
+ for (const auto& endpoint : endpoints)
+ eps.append(endpoint.address().to_string() + ":" + std::to_string(endpoint.port()) + " ");
+ logger_->d("[http:request:%i] connect begin: %s", id_, eps.c_str());
+ }
+ bool isHttps = get_url().protocol == "https";
+ if (isHttps) {
+ if (server_ca_ or client_identity_.first)
+ conn_ = std::make_shared<Connection>(ctx_, server_ca_, client_identity_, logger_);
+ else
+ conn_ = std::make_shared<Connection>(ctx_, true/*ssl*/, logger_);
+ conn_->set_ssl_verification(get_url().host, asio::ssl::verify_peer | asio::ssl::verify_fail_if_no_peer_cert);
+ }
+ else
+ conn_ = std::make_shared<Connection>(ctx_, false/*ssl*/, logger_);
+
+ if (conn_ && timeoutCb_)
+ conn_->timeout(timeout_, std::move(timeoutCb_));
+
+ // try to connect to any until one works
+ std::weak_ptr<Request> wthis = shared_from_this();
+ conn_->async_connect(std::move(endpoints), [wthis, cb, isHttps]
+ (const asio::error_code& ec, const asio::ip::tcp::endpoint& endpoint){
+ auto sthis = wthis.lock();
+ if (not sthis)
+ return;
+ auto& this_ = *sthis;
+ std::lock_guard<std::mutex> lock(this_.mutex_);
+ if (ec == asio::error::operation_aborted){
+ this_.terminate(ec);
+ return;
+ }
+ else if (ec) {
+ if (this_.logger_)
+ this_.logger_->e("[http:request:%i] connect failed with all endpoints: %s", this_.id_, ec.message().c_str());
+ } else {
+ const auto& url = this_.get_url();
+ auto port = endpoint.port();
+ if ((!isHttps && port == (in_port_t)80)
+ || (isHttps && port == (in_port_t)443))
+ this_.set_header_field(restinio::http_field_t::host, url.host);
+ else
+ this_.set_header_field(restinio::http_field_t::host, url.host + ":" + std::to_string(port));
+
+ if (isHttps) {
+ if (this_.conn_ and this_.conn_->is_open() and this_.conn_->is_ssl()) {
+ this_.conn_->async_handshake([id = this_.id_, cb, logger = this_.logger_](const asio::error_code& ec){
+ if (ec == asio::error::operation_aborted)
+ return;
+ if (ec and logger)
+ logger->e("[http:request:%i] handshake error: %s", id, ec.message().c_str());
+ //else if (logger)
+ // logger->d("[http:request:%i] handshake success", id);
+ if (cb)
+ cb(ec);
+ });
+ }
+ else if (cb)
+ cb(asio::error::operation_aborted);
+ return;
+ }
+ }
+ if (cb)
+ cb(ec);
+ });
+}
+
+void
+Request::send()
+{
+ notify_state_change(State::CREATED);
+
+ std::weak_ptr<Request> wthis = shared_from_this();
+ resolver_->add_callback([wthis](const asio::error_code& ec,
+ std::vector<asio::ip::tcp::endpoint> endpoints) {
+ if (auto sthis = wthis.lock()) {
+ auto& this_ = *sthis;
+ std::lock_guard<std::mutex> lock(this_.mutex_);
+ if (ec){
+ if (this_.logger_)
+ this_.logger_->e("[http:request:%i] resolve error: %s", this_.id_, ec.message().c_str());
+ this_.terminate(asio::error::connection_aborted);
+ }
+ else if (!this_.conn_ or !this_.conn_->is_open()) {
+ this_.connect(std::move(endpoints), [wthis](const asio::error_code &ec) {
+ if (auto sthis = wthis.lock()) {
+ if (ec)
+ sthis->terminate(asio::error::not_connected);
+ else
+ sthis->post();
+ }
+ });
+ }
+ else
+ this_.post();
+ }
+ }, family_);
+}
+
+void
+Request::post()
+{
+ if (!conn_ or !conn_->is_open()){
+ terminate(asio::error::not_connected);
+ return;
+ }
+ build();
+ init_parser();
+
+ if (logger_)
+ logger_->debug("[http:request:{}] sending {} bytes", id_, request_.size());
+
+ // write the request to buffer
+ std::ostream request_stream(&conn_->input());
+ request_stream << request_;
+
+ // send the request
+ notify_state_change(State::SENDING);
+
+ std::weak_ptr<Request> wthis = shared_from_this();
+ conn_->async_write([wthis](const asio::error_code& ec, size_t) {
+ if (auto sthis = wthis.lock())
+ sthis->handle_request(ec);
+ });
+}
+
+void
+Request::terminate(const asio::error_code& ec)
+{
+ if (finishing_.exchange(true))
+ return;
+
+ response_.aborted = ec == asio::error::operation_aborted;
+ if (ec == asio::error::basic_errors::broken_pipe)
+ response_.status_code = 0U; // Avoid to give a successful answer (happen with a broken pipe, takes the last status)
+
+ if (logger_) {
+ if (ec and ec != asio::error::eof and !response_.aborted)
+ logger_->e("[http:request:%i] end with error: %s", id_, ec.message().c_str());
+ else
+ logger_->d("[http:request:%i] done with status code %u", id_, response_.status_code);
+ }
+
+ if (!parser_ or !http_should_keep_alive(parser_.get()))
+ if (auto c = conn_)
+ c->close();
+ notify_state_change(State::DONE);
+}
+
+void
+Request::handle_request(const asio::error_code& ec)
+{
+ std::lock_guard<std::mutex> lock(mutex_);
+ if (ec and ec != asio::error::eof){
+ terminate(ec);
+ return;
+ }
+ if (!conn_->is_open()){
+ terminate(asio::error::not_connected);
+ return;
+ }
+ // if (logger_)
+ // logger_->d("[http:request:%i] send success", id_);
+ // read response
+ notify_state_change(State::RECEIVING);
+
+ std::weak_ptr<Request> wthis = shared_from_this();
+ conn_->async_read_until(HTTP_HEADER_DELIM, [wthis](const asio::error_code& ec, size_t n_bytes){
+ if (auto sthis = wthis.lock())
+ sthis->handle_response(ec, n_bytes);
+ });
+}
+
+void
+Request::handle_response(const asio::error_code& ec, size_t /* n_bytes */)
+{
+ std::lock_guard<std::mutex> lock(mutex_);
+ if (ec && ec != asio::error::eof){
+ terminate(ec);
+ return;
+ }
+ auto request = (ec == asio::error::eof) ? std::string{} : conn_->read_bytes();
+ size_t ret = http_parser_execute(parser_.get(), parser_s_.get(), request.c_str(), request.size());
+ if (ret != request.size()) {
+ if (logger_)
+ logger_->e("Error parsing HTTP: %zu %s %s", ret,
+ http_errno_name(HTTP_PARSER_ERRNO(parser_)),
+ http_errno_description(HTTP_PARSER_ERRNO(parser_)));
+ terminate(asio::error::basic_errors::broken_pipe);
+ return;
+ }
+
+ if (state_ != State::DONE and parser_ and not http_body_is_final(parser_.get())) {
+ auto toRead = parser_->content_length ? std::min<uint64_t>(parser_->content_length, 64 * 1024) : 64 * 1024;
+ std::weak_ptr<Request> wthis = shared_from_this();
+ conn_->async_read_some(toRead, [wthis](const asio::error_code& ec, size_t bytes){
+ if (auto sthis = wthis.lock())
+ sthis->handle_response(ec, bytes);
+ });
+ }
+}
+
+void
+Request::onBody(const char* at, size_t length)
+{
+ if (cbs_.on_body)
+ cbs_.on_body(at, length);
+ else
+ response_.body.insert(response_.body.end(), at, at+length);
+}
+
+void
+Request::onComplete() {
+ terminate(asio::error::eof);
+}
+
+void
+Request::onHeadersComplete() {
+ notify_state_change(State::HEADER_RECEIVED);
+
+ if (response_.status_code == restinio::status_code::moved_permanently.raw_code() or
+ response_.status_code == restinio::status_code::found.raw_code())
+ {
+ auto location_it = response_.headers.find(restinio::field_to_string(restinio::http_field_t::location));
+ if (location_it == response_.headers.end()){
+ if (logger_)
+ logger_->e("[http:client] [request:%i] got redirect without location", id_);
+ terminate(asio::error::connection_aborted);
+ }
+
+ if (follow_redirect and num_redirect < MAX_REDIRECTS) {
+ auto newUrl = getRelativePath(get_url(), location_it->second);
+ if (logger_)
+ logger_->w("[http:client] [request:%i] redirect to %s", id_, newUrl.c_str());
+ auto next = std::make_shared<Request>(ctx_, newUrl, logger_);
+ next->set_method(header_.method());
+ next->headers_ = std::move(headers_);
+ next->body_ = std::move(body_);
+ next->cbs_ = std::move(cbs_);
+ next->num_redirect = num_redirect + 1;
+ next_ = next;
+ next->prev_ = shared_from_this();
+ next->send();
+ } else {
+ if (logger_)
+ logger_->e("[http:client] [request:%i] got redirect without location", id_);
+ terminate(asio::error::connection_aborted);
+ }
+ } else {
+ auto expect_it = headers_.find(restinio::http_field_t::expect);
+ if (expect_it != headers_.end() and (expect_it->second == "100-continue") and response_.status_code != 200){
+ notify_state_change(State::SENDING);
+ request_.append(body_);
+ std::ostream request_stream(&conn_->input());
+ request_stream << body_ << "\r\n";
+ std::weak_ptr<Request> wthis = shared_from_this();
+ conn_->async_write([wthis](const asio::error_code& ec, size_t) {
+ if (auto sthis = wthis.lock())
+ sthis->handle_request(ec);
+ });
+ }
+ }
+}
+
+bool startsWith(const std::string& haystack, const std::string& needle) {
+ return needle.length() <= haystack.length()
+ && std::equal(needle.begin(), needle.end(), haystack.begin());
+}
+
+std::string
+Request::getRelativePath(const Url& origin, const std::string& path)
+{
+ if (startsWith(path, HTTP_PROTOCOL)
+ || startsWith(path, HTTPS_PROTOCOL)
+ || startsWith(path, ORIGIN_PROTOCOL)) {
+ // Absolute path
+ return path;
+ }
+ Url newPath = origin;
+ if (not path.empty() and path[0] == '/') {
+ newPath.target = path;
+ } else {
+ if (newPath.target.empty())
+ newPath.target.push_back('/');
+ newPath.target += path;
+ }
+ return newPath.toString();
+}
+
+const Response&
+Request::await()
+{
+ std::mutex mtx;
+ std::unique_lock<std::mutex> lock(mtx);
+ std::condition_variable cv;
+ bool ok {false};
+ add_on_done_callback([&](const Response&){
+ std::lock_guard<std::mutex> lk(mtx);
+ ok = true;
+ cv.notify_all();
+ });
+ cv.wait(lock, [&]{ return ok; });
+ return response_;
+}
+
+} // namespace http
+} // namespace dht
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ * Author(s) : Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ * Simon Désaulniers <simon.desaulniers@savoirfairelinux.com>
+ * Nicolas Reynaud <nicolas.reynaud@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "indexation/pht.h"
+#include "rng.h"
+
+namespace dht {
+namespace indexation {
+
+/**
+ * Output the blob into string and readable way
+ *
+ * @param bl : Blob to print
+ *
+ * @return string that represent the blob into a readable way
+ */
+static std::string blobToString(const Blob &bl) {
+ std::ostringstream ss;
+ auto bn = bl.size() % 8;
+ auto n = bl.size() / 8;
+
+ for (size_t i = 0; i < bl.size(); i++)
+ ss << std::bitset<8>(bl[i]) << " ";
+ if (bn)
+ for (unsigned b=0; b < bn; b++)
+ ss << (char)((bl[n] & (1 << (7 - b))) ? '1':'0');
+
+ return ss.str();
+}
+
+std::string Prefix::toString() const {
+ std::ostringstream ss;
+
+ ss << "Prefix : " << std::endl << "\tContent_ : \"";
+ ss << blobToString(content_);
+ ss << "\"" << std::endl;
+
+ ss << "\tFlags_ : \"";
+ ss << blobToString(flags_);
+ ss << "\"" << std::endl;
+
+ return ss.str();
+}
+
+void Pht::Cache::insert(const Prefix& p) {
+ size_t i = 0;
+ auto now = clock::now();
+
+ std::shared_ptr<Node> curr_node;
+
+ while ((leaves_.size() > 0
+ and leaves_.begin()->first + NODE_EXPIRE_TIME < now)
+ or leaves_.size() > MAX_ELEMENT) {
+
+ leaves_.erase(leaves_.begin());
+ }
+
+ if (not (curr_node = root_.lock()) ) {
+ /* Root does not exist, need to create one*/
+ curr_node = std::make_shared<Node>();
+ root_ = curr_node;
+ }
+
+ curr_node->last_reply = now;
+
+ /* Iterate through all bit of the Blob */
+ for ( i = 0; i < p.size_; i++ ) {
+
+ /* According to the bit define which node is the next one */
+ auto& next = ( p.isContentBitActive(i) ) ? curr_node->right_child : curr_node->left_child;
+
+ /**
+ * If lock, node exists
+ * else create it
+ */
+ if (auto n = next.lock()) {
+ curr_node = std::move(n);
+ } else {
+ /* Create the next node if doesn't exist*/
+ auto tmp_curr_node = std::make_shared<Node>();
+ tmp_curr_node->parent = curr_node;
+ next = tmp_curr_node;
+ curr_node = std::move(tmp_curr_node);
+ }
+
+ curr_node->last_reply = now;
+ }
+
+ /* Insert the leaf (curr_node) into the multimap */
+ leaves_.emplace(std::move(now), std::move(curr_node) );
+}
+
+int Pht::Cache::lookup(const Prefix& p) {
+ int pos = -1;
+ auto now = clock::now(), last_node_time = now;
+
+ /* Before lookup remove the useless one [i.e. too old] */
+ while ( leaves_.size() > 0
+ and leaves_.begin()->first + NODE_EXPIRE_TIME < now ) {
+
+ leaves_.erase(leaves_.begin());
+ }
+
+ auto next = root_;
+ std::shared_ptr<Node> curr_node;
+
+ while ( auto n = next.lock() ) {
+ ++pos;
+ /* Safe since pos is equal to 0 until here */
+ if ( (unsigned) pos >= p.content_.size() * 8) break;
+
+ curr_node = n;
+ last_node_time = curr_node->last_reply;
+ curr_node->last_reply = now;
+
+ /* Get the Prefix bit by bit, starting from left */
+ next = ( p.isContentBitActive(pos) ) ? curr_node->right_child : curr_node->left_child;
+ }
+
+ if ( pos >= 0 ) {
+ auto to_erase = leaves_.find(last_node_time);
+ if ( to_erase != leaves_.end() )
+ leaves_.erase( to_erase );
+
+ leaves_.emplace( std::move(now), std::move(curr_node) );
+ }
+
+ return pos;
+}
+
+const ValueType IndexEntry::TYPE = ValueType::USER_DATA;
+constexpr std::chrono::minutes Pht::Cache::NODE_EXPIRE_TIME;
+
+void Pht::lookupStep(Prefix p, std::shared_ptr<int> lo, std::shared_ptr<int> hi,
+ std::shared_ptr<std::vector<std::shared_ptr<IndexEntry>>> vals,
+ LookupCallbackWrapper cb, DoneCallbackSimple done_cb,
+ std::shared_ptr<unsigned> max_common_prefix_len,
+ int start, bool all_values)
+{
+ struct node_lookup_result {
+ bool done {false};
+ bool is_pht {false};
+ };
+
+ /* start could be under 0 but after the compare it to 0 it always will be unsigned, so we can cast it*/
+ auto mid = (start >= 0) ? (unsigned) start : (*lo + *hi)/2;
+
+ auto first_res = std::make_shared<node_lookup_result>();
+ auto second_res = std::make_shared<node_lookup_result>();
+
+ auto on_done = [=](bool ok) {
+ bool is_leaf = first_res->is_pht and not second_res->is_pht;
+ if (not ok) {
+ if (done_cb)
+ done_cb(false);
+ }
+ else if (is_leaf or *lo > *hi) {
+ // leaf node
+ Prefix to_insert = p.getPrefix(mid);
+ cache_.insert(to_insert);
+
+ if (cb) {
+ if (vals->size() == 0 and max_common_prefix_len and mid > 0) {
+ auto p_ = (p.getPrefix(mid)).getSibling().getFullSize();
+ *lo = mid;
+ *hi = p_.size_;
+ lookupStep(p_, lo, hi, vals, cb, done_cb, max_common_prefix_len, -1, all_values);
+ }
+
+ cb(*vals, to_insert);
+ }
+
+ if (done_cb)
+ done_cb(true);
+ } else if (first_res->is_pht) {
+ // internal node
+ *lo = mid+1;
+ lookupStep(p, lo, hi, vals, cb, done_cb, max_common_prefix_len, -1, all_values);
+ } else {
+ // first get failed before second.
+ if (done_cb)
+ done_cb(false);
+ }
+ };
+
+ if (*lo <= *hi) {
+ auto pht_filter = [&](const dht::Value& v) {
+ return v.user_type.compare(0, name_.size(), name_) == 0;
+ };
+
+ auto on_get = [=](const std::shared_ptr<dht::Value>& value, std::shared_ptr<node_lookup_result> res) {
+ if (value->user_type == canary_) {
+ res->is_pht = true;
+ }
+ else {
+ IndexEntry entry;
+ entry.unpackValue(*value);
+
+ auto it = std::find_if(vals->cbegin(), vals->cend(), [&](const std::shared_ptr<IndexEntry>& ie) {
+ return ie->value == entry.value;
+ });
+
+ /* If we already got the value then get the next one */
+ if (it != vals->cend())
+ return true;
+
+ if (max_common_prefix_len) { /* inexact match case */
+ auto common_bits = Prefix::commonBits(p, entry.prefix);
+
+ if (vals->empty()) {
+ vals->emplace_back(std::make_shared<IndexEntry>(entry));
+ *max_common_prefix_len = common_bits;
+ }
+ else {
+ if (common_bits == *max_common_prefix_len) /* this is the max so far */
+ vals->emplace_back(std::make_shared<IndexEntry>(entry));
+ else if (common_bits > *max_common_prefix_len) { /* new max found! */
+ vals->clear();
+ vals->emplace_back(std::make_shared<IndexEntry>(entry));
+ *max_common_prefix_len = common_bits;
+ }
+ }
+ } else if (all_values or entry.prefix == p.content_) /* exact match case */
+ vals->emplace_back(std::make_shared<IndexEntry>(entry));
+ }
+
+ return true;
+ };
+
+ dht_->get(p.getPrefix(mid).hash(),
+ std::bind(on_get, std::placeholders::_1, first_res),
+ [=](bool ok) {
+ if (not ok) {
+ // DHT failed
+ first_res->done = true;
+ if (done_cb and second_res->done)
+ on_done(false);
+ }
+ else {
+ if (not first_res->is_pht) {
+ // Not a PHT node.
+ *hi = mid-1;
+ lookupStep(p, lo, hi, vals, cb, done_cb, max_common_prefix_len, -1, all_values);
+ } else {
+ first_res->done = true;
+ if (second_res->done or mid >= p.size_ - 1)
+ on_done(true);
+ }
+ }
+ }, pht_filter);
+
+ if (mid < p.size_ - 1)
+ dht_->get(p.getPrefix(mid+1).hash(),
+ std::bind(on_get, std::placeholders::_1, second_res),
+ [=](bool ok) {
+ if (not ok) {
+ // DHT failed
+ second_res->done = true;
+ if (done_cb and first_res->done)
+ on_done(false);
+ }
+ else {
+ second_res->done = true;
+ if (first_res->done)
+ on_done(true);
+ }
+ }, pht_filter);
+ } else {
+ on_done(true);
+ }
+}
+
+void Pht::lookup(Key k, Pht::LookupCallback cb, DoneCallbackSimple done_cb, bool exact_match) {
+ auto prefix = linearize(k);
+ auto values = std::make_shared<std::vector<std::shared_ptr<IndexEntry>>>();
+
+ auto lo = std::make_shared<int>(0);
+ auto hi = std::make_shared<int>(prefix.size_);
+ std::shared_ptr<unsigned> max_common_prefix_len = not exact_match ? std::make_shared<unsigned>(0) : nullptr;
+
+ lookupStep(prefix, lo, hi, values,
+ [=](std::vector<std::shared_ptr<IndexEntry>>& entries, const Prefix& p) {
+ std::vector<std::shared_ptr<Value>> vals(entries.size());
+
+ std::transform(entries.begin(), entries.end(), vals.begin(),
+ [](const std::shared_ptr<IndexEntry>& ie) {
+ return std::make_shared<Value>(ie->value);
+ });
+
+ cb(vals, p);
+ }, done_cb, max_common_prefix_len, cache_.lookup(prefix));
+}
+
+void Pht::updateCanary(Prefix p) {
+ // TODO: change this... copy value
+ dht::Value canary_value;
+ canary_value.user_type = canary_;
+
+ dht_->put(p.hash(), std::move(canary_value),
+ [=](bool){
+ static std::bernoulli_distribution d(0.5);
+ crypto::random_device rd;
+ if (p.size_ and d(rd))
+ updateCanary(p.getPrefix(-1));
+ }
+ );
+
+ if (p.size_) {
+ dht::Value canary_second_value;
+ canary_second_value.user_type = canary_;
+ dht_->put(p.getSibling().hash(), std::move(canary_second_value));
+ }
+}
+
+void Pht::insert(const Prefix& kp, IndexEntry entry, std::shared_ptr<int> lo, std::shared_ptr<int> hi, time_point time_p,
+ bool check_split, DoneCallbackSimple done_cb) {
+
+ if (time_p + ValueType::USER_DATA.expiration < clock::now()) return;
+
+ auto vals = std::make_shared<std::vector<std::shared_ptr<IndexEntry>>>();
+ auto final_prefix = std::make_shared<Prefix>();
+
+ lookupStep(kp, lo, hi, vals,
+ [=](std::vector<std::shared_ptr<IndexEntry>>&, Prefix p) {
+ *final_prefix = Prefix(p);
+ },
+ [=](bool ok){
+ if (not ok) {
+ if (done_cb)
+ done_cb(false);
+ } else {
+
+ RealInsertCallback real_insert = [=](const Prefix& p, IndexEntry entry) {
+ updateCanary(p);
+ checkPhtUpdate(p, entry, time_p);
+ cache_.insert(p);
+ dht_->put(p.hash(), std::move(entry), done_cb , time_p);
+ };
+
+ if ( not check_split or final_prefix->size_ == kp.size_ ) {
+ real_insert(*final_prefix, std::move(entry));
+ } else {
+ if ( vals->size() < MAX_NODE_ENTRY_COUNT ) {
+ getRealPrefix(final_prefix, std::move(entry), real_insert);
+ }
+ else {
+ split(*final_prefix, *vals, entry, real_insert);
+ }
+ }
+ }
+ }, nullptr, cache_.lookup(kp), true);
+}
+
+Prefix Pht::zcurve(const std::vector<Prefix>& all_prefix) const {
+ Prefix p;
+
+ if ( all_prefix.size() == 1 )
+ return all_prefix[0];
+
+ /* All prefix got the same size (thanks to padding) */
+ size_t prefix_size = all_prefix[0].content_.size();
+
+ /* Loop on all uint8_t of the input prefix */
+ for ( size_t j = 0, bit = 0; j < prefix_size; j++) {
+
+ uint8_t mask = 0x80;
+ /* For each of the 8 bits of the input uint8_t */
+ for ( int i = 0; i < 8; ) {
+
+ uint8_t flags = 0;
+ uint8_t content = 0;
+
+ /* For each bit of the output uint8_t */
+ for ( int k = 0 ; k < 8; k++ ) {
+
+ auto diff = k - i;
+
+ /*get the content 'c', and the flag 'f' of the input prefix */
+ auto c = all_prefix[bit].content_[j] & mask;
+ auto f = all_prefix[bit].flags_[j] & mask;
+
+ /* Move this bit at the right position according to the diff
+ and merge it into content and flags in the same way */
+ content |= ( diff >= 0 ) ? c >> diff : c << std::abs(diff);
+ flags |= ( diff >= 0 ) ? f >> diff : f << std::abs(diff);
+
+ /* If we are on the last prefix of the vector get back to the first and
+ ,move the mask in order to get the n + 1nth bit */
+ if ( ++bit == all_prefix.size() ) { bit = 0; ++i; mask >>= 1; }
+ }
+
+ /* Add the next flags + content to the output prefix */
+ p.content_.push_back(content);
+ p.flags_.push_back(flags);
+ p.size_ += 8;
+ }
+ }
+
+ return p;
+}
+
+Prefix Pht::linearize(Key k) const {
+ if (not validKey(k)) { throw std::invalid_argument(INVALID_KEY); }
+
+ std::vector<Prefix> all_prefix;
+ all_prefix.reserve(k.size());
+
+ /* Get the max size of the keyspec and take it for size limit (for padding) */
+ auto max = std::max_element(keySpec_.begin(), keySpec_.end(),
+ [](const std::pair<std::string, size_t>& a, const std::pair<std::string, size_t>& b) {
+ return a.second < b.second;
+ })->second + 1;
+
+ for ( auto const& it : k ) {
+ Prefix p = Blob {it.second.begin(), it.second.end()};
+ p.addPaddingContent(max);
+ p.updateFlags();
+
+ all_prefix.emplace_back(std::move(p));
+ }
+
+ return zcurve(all_prefix);
+}
+
+void Pht::getRealPrefix(const std::shared_ptr<Prefix>& p, IndexEntry entry, RealInsertCallback end_cb )
+{
+ if ( p->size_ == 0 ) {
+ end_cb(*p, std::move(entry));
+ return;
+ }
+
+ struct OpState {
+ unsigned entry_count {0}; /* Total number of data on 3 nodes */
+ unsigned ended {0}; /* How many ops have ended */
+ Prefix parent;
+ OpState(Prefix p) : parent(p) {}
+ };
+ auto op_state = std::make_shared<OpState>(p->getPrefix(-1));
+
+ auto pht_filter = [&](const dht::Value& v) {
+ return v.user_type.compare(0, name_.size(), name_) == 0;
+ };
+
+ /* Lambda will count total number of data node */
+ auto count = [=]( const std::shared_ptr<dht::Value>& value ) {
+ if (value->user_type != canary_)
+ op_state->entry_count++;
+ return true;
+ };
+
+ auto on_done = [=] ( bool ) {
+ op_state->ended++;
+ /* Only the last one do the CallBack*/
+ if (op_state->ended == 3) {
+ if (op_state->entry_count < MAX_NODE_ENTRY_COUNT)
+ end_cb(op_state->parent, std::move(entry));
+ else
+ end_cb(*p, std::move(entry));
+ }
+ };
+
+ dht_->get(op_state->parent.hash(),
+ count,
+ on_done,
+ pht_filter
+ );
+
+ dht_->get(p->hash(),
+ count,
+ on_done,
+ pht_filter
+ );
+
+ dht_->get(p->getSibling().hash(),
+ count,
+ on_done,
+ pht_filter
+ );
+}
+
+void Pht::checkPhtUpdate(Prefix p, IndexEntry entry, time_point time_p) {
+
+ Prefix full = entry.prefix;
+ if ( p.content_.size() * 8 >= full.content_.size() * 8 ) return;
+
+ auto next_prefix = full.getPrefix( p.size_ + 1 );
+
+ dht_->listen(next_prefix.hash(),
+ [=](const std::shared_ptr<dht::Value> &value) {
+ if (value->user_type == canary_) {
+ insert(full, entry, std::make_shared<int>(0), std::make_shared<int>(full.size_), time_p, false, nullptr);
+
+ /* Cancel listen since we found where we need to update*/
+ return false;
+ }
+
+ return true;
+ },
+ [=](const dht::Value& v) {
+ /* Filter value v thats start with the same name as ours */
+ return v.user_type.compare(0, name_.size(), name_) == 0;
+ }
+ );
+}
+
+void Pht::split(const Prefix& insert, const std::vector<std::shared_ptr<IndexEntry>>& vals, IndexEntry entry, RealInsertCallback end_cb ) {
+ const auto full = Prefix(entry.prefix);
+
+ auto loc = findSplitLocation(full, vals);
+ const auto prefix_to_insert = full.getPrefix(loc);
+
+ for(;loc != insert.size_ - 1; loc--) {
+ updateCanary(full.getPrefix(loc));
+ }
+
+ end_cb(prefix_to_insert, entry);
+}
+
+} /* indexation */
+
+} /* dht */
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ * Author(s) : Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "value.h"
+#include "utils.h"
+#include "callbacks.h"
+
+namespace dht {
+
+/**
+ * Foreign nodes asking for updates about an InfoHash.
+ */
+struct Listener {
+ time_point time;
+ Query query;
+ int version;
+
+ Listener(time_point t, Query&& q, int version = 0) : time(t), query(std::move(q)), version(version) {}
+
+ void refresh(time_point t, Query&& q) {
+ time = t;
+ query = std::move(q);
+ }
+};
+
+/**
+ * A single "listen" operation data
+ */
+struct LocalListener {
+ Sp<Query> query;
+ Value::Filter filter;
+ ValueCallback get_cb;
+};
+
+}
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ *
+ * Author: Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "log.h"
+#include "dhtrunner.h"
+
+#include <fmt/format.h>
+#include <fmt/ostream.h>
+#include <fmt/printf.h>
+
+#ifndef _WIN32
+#include <syslog.h>
+#endif
+
+#include <fstream>
+#include <chrono>
+
+namespace dht {
+namespace log {
+
+/**
+ * Terminal colors for logging
+ */
+namespace Color {
+ enum Code {
+ FG_RED = 31,
+ FG_GREEN = 32,
+ FG_YELLOW = 33,
+ FG_BLUE = 34,
+ FG_DEFAULT = 39,
+ BG_RED = 41,
+ BG_GREEN = 42,
+ BG_BLUE = 44,
+ BG_DEFAULT = 49
+ };
+ class Modifier {
+ const Code code;
+ public:
+ constexpr Modifier(Code pCode) : code(pCode) {}
+ friend std::ostream&
+ operator<<(std::ostream& os, const Modifier& mod) {
+ return os << "\033[" << mod.code << 'm';
+ }
+ };
+}
+
+constexpr const Color::Modifier def(Color::FG_DEFAULT);
+constexpr const Color::Modifier red(Color::FG_RED);
+constexpr const Color::Modifier yellow(Color::FG_YELLOW);
+
+/**
+ * Print va_list to std::ostream (used for logging).
+ */
+void
+printfLog(std::ostream& s, const std::string& message) {
+ using namespace std::chrono;
+ using log_precision = microseconds;
+ auto num = duration_cast<log_precision>(steady_clock::now().time_since_epoch()).count();
+ constexpr auto den = log_precision::period::den;
+ fmt::print(s, "[{:06d}.{:06d}] ", num / den, num % den);
+ s << message << std::endl;
+}
+void
+printLog(std::ostream& s, fmt::string_view format, fmt::format_args args) {
+ using namespace std::chrono;
+ using log_precision = microseconds;
+ auto num = duration_cast<log_precision>(steady_clock::now().time_since_epoch()).count();
+ constexpr auto den = log_precision::period::den;
+ fmt::print(s, "[{:06d}.{:06d}] ", num / den, num % den);
+ fmt::vprint(s, format, args);
+ s << std::endl;
+}
+
+std::shared_ptr<Logger>
+getStdLogger() {
+ return std::make_shared<Logger>(
+ [](LogLevel level, std::string&& message) {
+ if (level == LogLevel::error)
+ std::cerr << red;
+ else if (level == LogLevel::warning)
+ std::cerr << yellow;
+ printfLog(std::cerr, message);
+ std::cerr << def;
+ }
+ );
+}
+
+std::shared_ptr<Logger>
+getFileLogger(const std::string &path) {
+ auto logfile = std::make_shared<std::ofstream>();
+ logfile->open(path, std::ios::out);
+ return std::make_shared<Logger>(
+ [logfile](LogLevel /*level*/, std::string&& message) {
+ printfLog(*logfile, message);
+ }
+ );
+}
+
+#ifndef _WIN32
+constexpr
+int syslogLevel(LogLevel level) {
+ switch (level) {
+ case LogLevel::error:
+ return LOG_ERR;
+ case LogLevel::warning:
+ return LOG_WARNING;
+ case LogLevel::debug:
+ return LOG_INFO;
+ }
+ return LOG_ERR;
+}
+#endif
+
+std::shared_ptr<Logger>
+getSyslogLogger(const char* name) {
+#ifndef _WIN32
+ struct Syslog {
+ explicit Syslog(const char* n) {
+ openlog(n, LOG_NDELAY, LOG_USER);
+ }
+ ~Syslog() {
+ closelog();
+ }
+ };
+ // syslog is global. Existing instance must be reused.
+ static std::weak_ptr<Syslog> opened_logfile;
+ auto logfile = opened_logfile.lock();
+ if (not logfile) {
+ logfile = std::make_shared<Syslog>(name);
+ opened_logfile = logfile;
+ }
+ return std::make_shared<Logger>(
+ [logfile](LogLevel level, std::string&& message) {
+ syslog(syslogLevel(level), "%s", message.c_str());
+ });
+#else
+ return getStdLogger();
+#endif
+}
+
+void
+enableLogging(dht::DhtRunner &dht) {
+ dht.setLogger(getStdLogger());
+}
+
+void
+enableFileLogging(dht::DhtRunner &dht, const std::string &path) {
+ dht.setLogger(getFileLogger(path));
+}
+
+OPENDHT_PUBLIC void
+enableSyslog(dht::DhtRunner &dht, const char* name) {
+ dht.setLogger(getSyslogLogger(name));
+}
+
+void
+disableLogging(dht::DhtRunner &dht) {
+ dht.setLogger();
+}
+
+}
+}
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ * Author(s) : Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+namespace dht {
+namespace net {
+
+enum class MessageType {
+ Error = 0,
+ Reply,
+ Ping,
+ FindNode,
+ GetValues,
+ AnnounceValue,
+ Refresh,
+ Listen,
+ ValueData,
+ ValueUpdate,
+ UpdateValue
+};
+
+} /* namespace net */
+} /* dht */
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ * Author(s) : Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ * Simon Désaulniers <simon.desaulniers@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "network_engine.h"
+#include "request.h"
+#include "default_types.h"
+#include "logger.h"
+#include "parsed_message.h"
+
+#include <msgpack.hpp>
+#include <chrono>
+#include <string_view>
+
+namespace dht {
+namespace net {
+using namespace std::chrono_literals;
+
+const std::string DhtProtocolException::GET_NO_INFOHASH {"Get_values with no info_hash"};
+const std::string DhtProtocolException::LISTEN_NO_INFOHASH {"Listen with no info_hash"};
+const std::string DhtProtocolException::LISTEN_WRONG_TOKEN {"Listen with wrong token"};
+const std::string DhtProtocolException::PUT_NO_INFOHASH {"Put with no info_hash"};
+const std::string DhtProtocolException::PUT_WRONG_TOKEN {"Put with wrong token"};
+const std::string DhtProtocolException::PUT_INVALID_ID {"Put with invalid id"};
+const std::string DhtProtocolException::STORAGE_NOT_FOUND {"Access operation for unknown storage"};
+
+constexpr std::chrono::seconds NetworkEngine::UDP_REPLY_TIME;
+constexpr std::chrono::seconds NetworkEngine::RX_MAX_PACKET_TIME;
+constexpr std::chrono::seconds NetworkEngine::RX_TIMEOUT;
+
+/* OpenDHT User Agent (UA) */
+constexpr std::string_view OPENDHT_UA {"o2"};
+
+static constexpr uint8_t v4prefix[16] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xFF, 0xFF, 0, 0, 0, 0};
+
+constexpr unsigned SEND_NODES {8};
+
+
+struct NetworkEngine::PartialMessage {
+ SockAddr from;
+ time_point start;
+ time_point last_part;
+ std::unique_ptr<ParsedMessage> msg;
+};
+
+std::vector<Blob>
+serializeValues(const std::vector<Sp<Value>>& st)
+{
+ std::vector<Blob> svals;
+ svals.reserve(st.size());
+ for (const auto& v : st)
+ svals.emplace_back(packMsg(v));
+ return svals;
+}
+
+void
+packToken(msgpack::packer<msgpack::sbuffer>& pk, const Blob& token)
+{
+ pk.pack_bin(token.size());
+ pk.pack_bin_body((char*)token.data(), token.size());
+}
+
+RequestAnswer::RequestAnswer(ParsedMessage&& msg)
+ : ntoken(std::move(msg.token)),
+ values(std::move(msg.values)),
+ refreshed_values(std::move(msg.refreshed_values)),
+ expired_values(std::move(msg.expired_values)),
+ fields(std::move(msg.fields)),
+ nodes4(std::move(msg.nodes4)),
+ nodes6(std::move(msg.nodes6))
+{}
+
+NetworkEngine::NetworkEngine(InfoHash& myid, NetworkConfig c,
+ std::unique_ptr<DatagramSocket>&& sock,
+ const Sp<Logger>& log,
+ std::mt19937_64& rand,
+ Scheduler& scheduler,
+ decltype(NetworkEngine::onError)&& onError,
+ decltype(NetworkEngine::onNewNode)&& onNewNode,
+ decltype(NetworkEngine::onReportedAddr)&& onReportedAddr,
+ decltype(NetworkEngine::onPing)&& onPing,
+ decltype(NetworkEngine::onFindNode)&& onFindNode,
+ decltype(NetworkEngine::onGetValues)&& onGetValues,
+ decltype(NetworkEngine::onListen)&& onListen,
+ decltype(NetworkEngine::onAnnounce)&& onAnnounce,
+ decltype(NetworkEngine::onRefresh)&& onRefresh) :
+ onError(std::move(onError)),
+ onNewNode(std::move(onNewNode)),
+ onReportedAddr(std::move(onReportedAddr)),
+ onPing(std::move(onPing)),
+ onFindNode(std::move(onFindNode)),
+ onGetValues(std::move(onGetValues)),
+ onListen(std::move(onListen)),
+ onAnnounce(std::move(onAnnounce)),
+ onRefresh(std::move(onRefresh)),
+ myid(myid), config(c), dht_socket(std::move(sock)), logger_(log), rd(rand),
+ cache(rd),
+ rate_limiter(config.max_req_per_sec),
+ scheduler(scheduler)
+{}
+
+NetworkEngine::~NetworkEngine() {
+ clear();
+}
+
+void
+NetworkEngine::tellListener(const Sp<Node>& node, 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,
+ const Query& query, int version)
+{
+ auto nnodes = bufferNodes(node->getFamily(), hash, want, nodes, nodes6);
+ try {
+ if (version >= 1) {
+ sendUpdateValues(node, hash, std::move(values), scheduler.time(), ntoken, socket_id);
+ } else {
+ sendNodesValues(node->getAddr(), socket_id, nnodes.first, nnodes.second, std::move(values), query, ntoken);
+ }
+ } catch (const std::overflow_error& e) {
+ if (logger_)
+ logger_->e("Can't send value: buffer not large enough !");
+ }
+}
+
+void
+NetworkEngine::tellListenerRefreshed(const Sp<Node>& n, Tid socket_id, const InfoHash&, const Blob& token, const std::vector<Value::Id>& values, int version)
+{
+ msgpack::sbuffer buffer;
+ msgpack::packer<msgpack::sbuffer> pk(&buffer);
+
+ pk.pack_map(4 + (version >= 1 ? 1 : 0) + (config.network?1:0));
+
+ pk.pack(version >= 1 ? KEY_A : KEY_U);
+ pk.pack_map(1 + (version >= 1 ? 1 : 0) + (not values.empty()?1:0) + (not token.empty()?1:0));
+ pk.pack(KEY_REQ_ID); pk.pack(myid);
+ if (version >= 1) {
+ pk.pack(KEY_REQ_SID); pk.pack(socket_id);
+ }
+ if (not token.empty()) {
+ pk.pack(KEY_REQ_TOKEN); packToken(pk, token);
+ }
+ if (not values.empty()) {
+ pk.pack(KEY_REQ_REFRESHED);
+ pk.pack(values);
+ if (logger_)
+ logger_->d(n->id, "[node %s] sending %zu refreshed values", n->toString().c_str(), values.size());
+ }
+
+ pk.pack(KEY_Y); pk.pack(version >= 1 ? KEY_Q : KEY_R);
+ pk.pack(KEY_UA); pk.pack(OPENDHT_UA);
+ if (config.network) {
+ pk.pack(KEY_NETID); pk.pack(config.network);
+ }
+
+ if (version >= 1) {
+ Tid tid (n->getNewTid());
+
+ pk.pack(KEY_Q); pk.pack(QUERY_UPDATE);
+ pk.pack(KEY_TID); pk.pack(tid);
+
+ auto req = std::make_shared<Request>(MessageType::UpdateValue, tid, n,
+ Blob(buffer.data(), buffer.data() + buffer.size()),
+ [=](const Request&, ParsedMessage&&) { /* on done */ },
+ [=](const Request&, bool) { /* on expired */ }
+ );
+ sendRequest(req);
+ ++out_stats.updateValue;
+ return;
+ }
+ pk.pack(KEY_TID); pk.pack(socket_id);
+
+ // send response
+ send(n->getAddr(), buffer.data(), buffer.size());
+}
+
+void
+NetworkEngine::tellListenerExpired(const Sp<Node>& n, Tid socket_id, const InfoHash&, const Blob& token, const std::vector<Value::Id>& values, int version)
+{
+ msgpack::sbuffer buffer;
+ msgpack::packer<msgpack::sbuffer> pk(&buffer);
+
+ pk.pack_map(4 + (version >= 1 ? 1 : 0) + (config.network?1:0));
+
+ pk.pack(version >= 1 ? KEY_A : KEY_U);
+ pk.pack_map(1 + (version >= 1 ? 1 : 0) + (not values.empty()?1:0) + (not token.empty()?1:0));
+ pk.pack(KEY_REQ_ID); pk.pack(myid);
+ if (version >= 1) {
+ pk.pack(KEY_REQ_SID); pk.pack(socket_id);
+ }
+ if (not token.empty()) {
+ pk.pack(KEY_REQ_TOKEN); packToken(pk, token);
+ }
+ if (not values.empty()) {
+ pk.pack(KEY_REQ_EXPIRED);
+ pk.pack(values);
+ if (logger_)
+ logger_->d(n->id, "[node %s] sending %zu expired values", n->toString().c_str(), values.size());
+ }
+
+ pk.pack(KEY_Y); pk.pack(version >= 1 ? KEY_Q : KEY_R);
+ pk.pack(KEY_UA); pk.pack(OPENDHT_UA);
+ if (config.network) {
+ pk.pack(KEY_NETID); pk.pack(config.network);
+ }
+
+ if (version >= 1) {
+ Tid tid (n->getNewTid());
+
+ pk.pack(KEY_Q); pk.pack(QUERY_UPDATE);
+ pk.pack(KEY_TID); pk.pack(tid);
+
+ auto req = std::make_shared<Request>(MessageType::UpdateValue, tid, n,
+ Blob(buffer.data(), buffer.data() + buffer.size()),
+ [=](const Request&, ParsedMessage&&) { /* on done */ },
+ [=](const Request&, bool) { /* on expired */ }
+ );
+ sendRequest(req);
+ ++out_stats.updateValue;
+ return;
+ }
+ pk.pack(KEY_TID); pk.pack(socket_id);
+
+ // send response
+ send(n->getAddr(), buffer.data(), buffer.size());
+}
+
+
+bool
+NetworkEngine::isRunning(sa_family_t af) const
+{
+ switch (af) {
+ case 0:
+ return dht_socket->hasIPv4() or dht_socket->hasIPv6();
+ case AF_INET:
+ return dht_socket->hasIPv4();
+ case AF_INET6:
+ return dht_socket->hasIPv6();
+ default:
+ return false;
+ }
+}
+
+void
+NetworkEngine::clear()
+{
+ for (auto& request : requests) {
+ request.second->cancel();
+ request.second->node->setExpired();
+ }
+ requests.clear();
+}
+
+void
+NetworkEngine::connectivityChanged(sa_family_t af)
+{
+ cache.clearBadNodes(af);
+}
+
+void
+NetworkEngine::requestStep(Sp<Request> sreq)
+{
+ auto& req = *sreq;
+ if (not req.pending())
+ return;
+
+ auto now = scheduler.time();
+ auto& node = *req.node;
+ if (req.isExpired(now)) {
+ // if (logger_)
+ // logger_->d(node.id, "[node %s] expired !", node.toString().c_str());
+ node.setExpired();
+ if (not node.id)
+ requests.erase(req.tid);
+ return;
+ } else if (req.attempt_count == 1) {
+ req.on_expired(req, false);
+ }
+
+ auto err = send(node.getAddr(), (char*)req.msg.data(), req.msg.size(), node.getReplyTime() < now - UDP_REPLY_TIME);
+ if (err == ENETUNREACH ||
+ err == EHOSTUNREACH ||
+ err == EAFNOSUPPORT ||
+ err == EPIPE ||
+ err == EPERM)
+ {
+ node.setExpired();
+ if (not node.id)
+ requests.erase(req.tid);
+ } else {
+ req.last_try = now;
+ if (err != EAGAIN) {
+ ++req.attempt_count;
+ req.attempt_duration +=
+ req.attempt_duration + uniform_duration_distribution<>(0ms, ((duration)Node::MAX_RESPONSE_TIME)/4)(rd);
+ if (not req.parts.empty()){
+ sendValueParts(req.tid, req.parts, node.getAddr());
+ }
+ }
+ std::weak_ptr<Request> wreq = sreq;
+ scheduler.add(req.last_try + req.attempt_duration, [this,wreq] {
+ if (auto req = wreq.lock())
+ requestStep(req);
+ });
+ }
+}
+
+/**
+ * Sends a request to a node. Request::MAX_ATTEMPT_COUNT attempts will
+ * be made before the request expires.
+ */
+void
+NetworkEngine::sendRequest(const Sp<Request>& request)
+{
+ auto& node = *request->node;
+ if (not node.id)
+ requests.emplace(request->tid, request);
+ request->start = scheduler.time();
+ node.requested(request);
+ requestStep(request);
+}
+
+
+/* Rate control for requests we receive. */
+bool
+NetworkEngine::rateLimit(const SockAddr& addr)
+{
+ const auto& now = scheduler.time();
+
+ // occasional IP limiter maintenance (a few times every second at max rate)
+ if (limiter_maintenance++ == config.max_peer_req_per_sec) {
+ for (auto it = address_rate_limiter.begin(); it != address_rate_limiter.end();) {
+ if (it->second.maintain(now) == 0)
+ address_rate_limiter.erase(it++);
+ else
+ ++it;
+ }
+ limiter_maintenance = 0;
+ }
+
+ // invoke per IP, then global rate limiter
+ return (config.max_peer_req_per_sec < 0
+ or address_rate_limiter
+ .emplace(addr, config.max_peer_req_per_sec).first->second
+ .limit(now))
+ and rate_limiter.limit(now);
+}
+
+bool
+NetworkEngine::isMartian(const SockAddr& addr)
+{
+ if (addr.getPort() == 0)
+ return true;
+ switch(addr.getFamily()) {
+ case AF_INET: {
+ const auto& sin = addr.getIPv4();
+ const uint8_t* address = (const uint8_t*)&sin.sin_addr;
+ return (address[0] == 0) ||
+ ((address[0] & 0xE0) == 0xE0);
+ }
+ case AF_INET6: {
+ if (addr.getLength() < sizeof(sockaddr_in6))
+ return true;
+ const auto& sin6 = addr.getIPv6();
+ const uint8_t* address = (const uint8_t*)&sin6.sin6_addr;
+ return address[0] == 0xFF ||
+ memcmp(address, InfoHash::zero().data(), 16) == 0 ||
+ memcmp(address, v4prefix, 12) == 0;
+ }
+ default:
+ return true;
+ }
+}
+
+/* The internal blacklist is an LRU cache of nodes that have sent
+ incorrect messages. */
+void
+NetworkEngine::blacklistNode(const Sp<Node>& n)
+{
+ n->setExpired();
+ blacklist.emplace(n->getAddr());
+}
+
+bool
+NetworkEngine::isNodeBlacklisted(const SockAddr& addr) const
+{
+ return blacklist.find(addr) != blacklist.end();
+}
+
+void
+NetworkEngine::processMessage(const uint8_t *buf, size_t buflen, SockAddr f)
+{
+ auto from = f.getMappedIPv4();
+ if (isMartian(from)) {
+ if (logger_)
+ logger_->w("Received packet from martian node %s", from.toString().c_str());
+ return;
+ }
+
+ if (isNodeBlacklisted(from)) {
+ if (logger_)
+ logger_->w("Received packet from blacklisted node %s", from.toString().c_str());
+ return;
+ }
+
+ auto msg = std::make_unique<ParsedMessage>();
+ try {
+ msgpack::unpacked msg_res = msgpack::unpack((const char*)buf, buflen);
+ msg->msgpack_unpack(msg_res.get());
+ } catch (const std::exception& e) {
+ if (logger_)
+ logger_->w("Can't parse message of size %lu: %s", buflen, e.what());
+ // if (logger_)
+ // logger_->DBG.logPrintable(buf, buflen);
+ return;
+ }
+
+ if (msg->network != config.network) {
+ if (logger_)
+ logger_->d("Received message from other config.network %u", msg->network);
+ return;
+ }
+
+ const auto& now = scheduler.time();
+
+ // partial value data
+ if (msg->type == MessageType::ValueData) {
+ auto pmsg_it = partial_messages.find(msg->tid);
+ if (pmsg_it == partial_messages.end()) {
+ if (logIncoming_)
+ if (logger_)
+ logger_->d("Can't find partial message");
+ rateLimit(from);
+ return;
+ }
+ if (!pmsg_it->second.from.equals(from)) {
+ if (logger_)
+ logger_->d("Received partial message data from unexpected IP address");
+ rateLimit(from);
+ return;
+ }
+ // append data block
+ if (pmsg_it->second.msg->append(*msg)) {
+ pmsg_it->second.last_part = now;
+ // check data completion
+ if (pmsg_it->second.msg->complete()) {
+ try {
+ // process the full message
+ process(std::move(pmsg_it->second.msg), from);
+ partial_messages.erase(pmsg_it);
+ } catch (...) {
+ return;
+ }
+ } else
+ scheduler.add(now + RX_TIMEOUT, std::bind(&NetworkEngine::maintainRxBuffer, this, msg->tid));
+ }
+ return;
+ }
+
+ if (msg->id == myid or not msg->id) {
+ if (logger_)
+ logger_->d("Received message from self");
+ return;
+ }
+
+ if (msg->type > MessageType::Reply) {
+ /* Rate limit requests. */
+ if (!rateLimit(from)) {
+ if (logger_)
+ logger_->w("Dropping request due to rate limiting");
+ return;
+ }
+ }
+
+ if (msg->value_parts.empty()) {
+ try {
+ process(std::move(msg), from);
+ } catch(...) {
+ return;
+ }
+ } else {
+ // starting partial message session
+ auto k = msg->tid;
+ auto& pmsg = partial_messages[k];
+ if (not pmsg.msg) {
+ pmsg.from = from;
+ pmsg.msg = std::move(msg);
+ pmsg.start = now;
+ pmsg.last_part = now;
+ scheduler.add(now + RX_MAX_PACKET_TIME, std::bind(&NetworkEngine::maintainRxBuffer, this, k));
+ scheduler.add(now + RX_TIMEOUT, std::bind(&NetworkEngine::maintainRxBuffer, this, k));
+ } else if (logger_)
+ logger_->e("Partial message with given TID %u already exists", k);
+ }
+}
+
+void
+NetworkEngine::process(std::unique_ptr<ParsedMessage>&& msg, const SockAddr& from)
+{
+ const auto& now = scheduler.time();
+ auto node = cache.getNode(msg->id, from, now, true, msg->is_client);
+
+ if (msg->type == MessageType::ValueUpdate) {
+ auto rsocket = node->getSocket(msg->tid);
+ if (not rsocket)
+ throw DhtProtocolException {DhtProtocolException::UNKNOWN_TID, "Can't find socket", msg->id};
+ node->received(now, {});
+ onNewNode(node, 2);
+ deserializeNodes(*msg, from);
+ rsocket->on_receive(node, std::move(*msg));
+ }
+ else if (msg->type == MessageType::Error or msg->type == MessageType::Reply) {
+ auto rsocket = node->getSocket(msg->tid);
+ auto req = node->getRequest(msg->tid);
+
+ /* either response for a request or data for an opened socket */
+ if (not req and not rsocket) {
+ auto req_it = requests.find(msg->tid);
+ if (req_it != requests.end() and not req_it->second->node->id) {
+ req = req_it->second;
+ req->node = node;
+ requests.erase(req_it);
+ } else {
+ node->received(now, req);
+ if (not node->isClient())
+ onNewNode(node, 1);
+ if (logger_)
+ logger_->d(node->id, "[node %s] can't find transaction with id %u", node->toString().c_str(), msg->tid);
+ return;
+ }
+ }
+
+ node->received(now, req);
+
+ if (not node->isClient())
+ onNewNode(node, 2);
+ onReportedAddr(msg->id, msg->addr);
+
+ if (req and (req->cancelled() or req->expired() or req->completed())) {
+ if (logger_)
+ logger_->w(node->id, "[node %s] response to expired, cancelled or completed request", node->toString().c_str());
+ return;
+ }
+
+ switch (msg->type) {
+ case MessageType::Error: {
+ if (msg->id and req and (
+ (msg->error_code == DhtProtocolException::NOT_FOUND and req->getType() == MessageType::Refresh) or
+ (msg->error_code == DhtProtocolException::UNAUTHORIZED and (req->getType() == MessageType::AnnounceValue
+ or req->getType() == MessageType::Listen))))
+ {
+ req->last_try = time_point::min();
+ req->reply_time = time_point::min();
+ if (not req->setError(DhtProtocolException {msg->error_code}))
+ onError(req, DhtProtocolException {msg->error_code});
+ } else {
+ if (logIncoming_)
+ if (logger_)
+ logger_->w(msg->id, "[node %s %s] received unknown error message %u",
+ msg->id.toString().c_str(), from.toString().c_str(), msg->error_code);
+ }
+ break;
+ }
+ case MessageType::Reply:
+ if (req) { /* request reply */
+ auto& r = *req;
+ if (r.getType() == MessageType::AnnounceValue
+ or r.getType() == MessageType::Listen
+ or r.getType() == MessageType::Refresh) {
+ r.node->authSuccess();
+ }
+ r.reply_time = scheduler.time();
+
+ deserializeNodes(*msg, from);
+ r.setDone(std::move(*msg));
+ break;
+ } else { /* request socket data */
+ deserializeNodes(*msg, from);
+ rsocket->on_receive(node, std::move(*msg));
+ }
+ break;
+ default:
+ break;
+ }
+ } else {
+ node->received(now, {});
+ if (not node->isClient())
+ onNewNode(node, 1);
+ try {
+ switch (msg->type) {
+ case MessageType::Ping:
+ ++in_stats.ping;
+ if (logIncoming_)
+ if (logger_)
+ logger_->d(node->id, "[node %s] sending pong", node->toString().c_str());
+ onPing(node);
+ sendPong(from, msg->tid);
+ break;
+ case MessageType::FindNode: {
+ // if (logger_)
+ // logger_->d(msg->target, node->id, "[node %s] got 'find' request for %s (%d)", node->toString().c_str(), msg->target.toString().c_str(), msg->want);
+ ++in_stats.find;
+ RequestAnswer answer = onFindNode(node, msg->target, msg->want);
+ auto nnodes = bufferNodes(from.getFamily(), msg->target, msg->want, answer.nodes4, answer.nodes6);
+ sendNodesValues(from, msg->tid, nnodes.first, nnodes.second, {}, {}, answer.ntoken);
+ break;
+ }
+ case MessageType::GetValues: {
+ // if (logger_)
+ // logger_->d(msg->info_hash, node->id, "[node %s] got 'get' request for %s", node->toString().c_str(), msg->info_hash.toString().c_str());
+ ++in_stats.get;
+ RequestAnswer answer = onGetValues(node, msg->info_hash, msg->want, msg->query);
+ auto nnodes = bufferNodes(from.getFamily(), msg->info_hash, msg->want, answer.nodes4, answer.nodes6);
+ sendNodesValues(from, msg->tid, nnodes.first, nnodes.second, answer.values, msg->query, answer.ntoken);
+ break;
+ }
+ case MessageType::AnnounceValue: {
+ if (logIncoming_ and logger_)
+ logger_->d(msg->info_hash, node->id, "[node %s] got 'put' request for %s", node->toString().c_str(), msg->info_hash.toString().c_str());
+ ++in_stats.put;
+ onAnnounce(node, msg->info_hash, msg->token, msg->values, msg->created);
+
+ /* Note that if storageStore failed, we lie to the requestor.
+ This is to prevent them from backtracking, and hence
+ polluting the DHT. */
+ for (auto& v : msg->values) {
+ sendValueAnnounced(from, msg->tid, v->id);
+ }
+ break;
+ }
+ case MessageType::Refresh:
+ if (logIncoming_ and logger_)
+ logger_->d(msg->info_hash, node->id, "[node %s] got 'refresh' request for %s", node->toString().c_str(), msg->info_hash.toString().c_str());
+ onRefresh(node, msg->info_hash, msg->token, msg->value_id);
+ /* Same note as above in MessageType::AnnounceValue applies. */
+ sendValueAnnounced(from, msg->tid, msg->value_id);
+ break;
+ case MessageType::Listen: {
+ if (logIncoming_ and logger_)
+ logger_->d(msg->info_hash, node->id, "[node %s] got 'listen' request for %s", node->toString().c_str(), msg->info_hash.toString().c_str());
+ ++in_stats.listen;
+ RequestAnswer answer = onListen(node, msg->info_hash, msg->token, msg->socket_id, std::move(msg->query), msg->version);
+ auto nnodes = bufferNodes(from.getFamily(), msg->info_hash, msg->want, answer.nodes4, answer.nodes6);
+ sendListenConfirmation(from, msg->tid);
+ break;
+ }
+ case MessageType::UpdateValue: {
+ if (logIncoming_ and logger_)
+ logger_->d(msg->info_hash, node->id, "[node %s] got 'update' request for %s", node->toString().c_str(), msg->info_hash.toString().c_str());
+ ++in_stats.updateValue;
+ if (auto rsocket = node->getSocket(msg->socket_id))
+ rsocket->on_receive(node, std::move(*msg));
+ else if (logger_)
+ logger_->e(msg->info_hash, node->id, "[node %s] 'update' request without socket for %s", node->toString().c_str(), msg->info_hash.toString().c_str());
+ sendListenConfirmation(from, msg->tid);
+ break;
+ }
+ default:
+ break;
+ }
+ } catch (const std::overflow_error& e) {
+ if (logger_)
+ logger_->e("Can't send value: buffer not large enough !");
+ } catch (const DhtProtocolException& e) {
+ sendError(from, msg->tid, e.getCode(), e.getMsg().c_str(), true);
+ }
+ }
+}
+
+void
+insertAddr(msgpack::packer<msgpack::sbuffer>& pk, const SockAddr& addr)
+{
+ size_t addr_len = std::min<size_t>(addr.getLength(),
+ (addr.getFamily() == AF_INET) ? sizeof(in_addr) : sizeof(in6_addr));
+ void* addr_ptr = (addr.getFamily() == AF_INET) ? (void*)&addr.getIPv4().sin_addr
+ : (void*)&addr.getIPv6().sin6_addr;
+ pk.pack("sa");
+ pk.pack_bin(addr_len);
+ pk.pack_bin_body((char*)addr_ptr, addr_len);
+}
+
+int
+NetworkEngine::send(const SockAddr& addr, const char *buf, size_t len, bool confirmed)
+{
+ return dht_socket ? dht_socket->sendTo(addr, (const uint8_t*)buf, len, confirmed) : ENOTCONN;
+}
+
+Sp<Request>
+NetworkEngine::sendPing(const Sp<Node>& node, RequestCb&& on_done, RequestExpiredCb&& on_expired) {
+ Tid tid (node->getNewTid());
+ msgpack::sbuffer buffer;
+ msgpack::packer<msgpack::sbuffer> pk(&buffer);
+ pk.pack_map(5+(config.network?1:0));
+
+ pk.pack(KEY_A); pk.pack_map(1);
+ pk.pack(KEY_REQ_ID); pk.pack(myid);
+
+ pk.pack(KEY_Q); pk.pack(QUERY_PING);
+ pk.pack(KEY_TID); pk.pack(tid);
+ pk.pack(KEY_Y); pk.pack(KEY_Q);
+ pk.pack(KEY_UA); pk.pack(OPENDHT_UA);
+ if (config.network) {
+ pk.pack(KEY_NETID); pk.pack(config.network);
+ }
+
+ auto req = std::make_shared<Request>(MessageType::Ping, tid, node,
+ Blob(buffer.data(), buffer.data() + buffer.size()),
+ [=](const Request& req_status, ParsedMessage&&) {
+ if (logger_)
+ logger_->d(req_status.node->id, "[node %s] got pong !", req_status.node->toString().c_str());
+ if (on_done) {
+ on_done(req_status, {});
+ }
+ },
+ [=](const Request& req_status, bool done) { /* on expired */
+ if (on_expired) {
+ on_expired(req_status, done);
+ }
+ }
+ );
+ sendRequest(req);
+ ++out_stats.ping;
+ return req;
+}
+
+void
+NetworkEngine::sendPong(const SockAddr& addr, Tid tid) {
+ msgpack::sbuffer buffer;
+ msgpack::packer<msgpack::sbuffer> pk(&buffer);
+ pk.pack_map(4+(config.network?1:0));
+
+ pk.pack(KEY_R); pk.pack_map(2);
+ pk.pack(KEY_REQ_ID); pk.pack(myid);
+ insertAddr(pk, addr);
+
+ pk.pack(KEY_TID); pk.pack(tid);
+ pk.pack(KEY_Y); pk.pack(KEY_R);
+ pk.pack(KEY_UA); pk.pack(OPENDHT_UA);
+ if (config.network) {
+ pk.pack(KEY_NETID); pk.pack(config.network);
+ }
+
+ send(addr, buffer.data(), buffer.size());
+}
+
+Sp<Request>
+NetworkEngine::sendFindNode(const Sp<Node>& n, const InfoHash& target, want_t want,
+ RequestCb&& on_done, RequestExpiredCb&& on_expired) {
+ Tid tid (n->getNewTid());
+ msgpack::sbuffer buffer;
+ msgpack::packer<msgpack::sbuffer> pk(&buffer);
+ pk.pack_map(5+(config.network?1:0));
+
+ pk.pack(KEY_A); pk.pack_map(2 + (want>0?1:0));
+ pk.pack(KEY_REQ_ID); pk.pack(myid);
+ pk.pack(KEY_REQ_TARGET); pk.pack(target);
+ if (want > 0) {
+ pk.pack(KEY_REQ_WANT);
+ pk.pack_array(((want & WANT4)?1:0) + ((want & WANT6)?1:0));
+ if (want & WANT4) pk.pack(AF_INET);
+ if (want & WANT6) pk.pack(AF_INET6);
+ }
+
+ pk.pack(KEY_Q); pk.pack(QUERY_FIND);
+ pk.pack(KEY_TID); pk.pack(tid);
+ pk.pack(KEY_Y); pk.pack(KEY_Q);
+ pk.pack(KEY_UA); pk.pack(OPENDHT_UA);
+ if (config.network) {
+ pk.pack(KEY_NETID); pk.pack(config.network);
+ }
+
+ auto req = std::make_shared<Request>(MessageType::FindNode, tid, n,
+ Blob(buffer.data(), buffer.data() + buffer.size()),
+ [=](const Request& req_status, ParsedMessage&& msg) { /* on done */
+ if (on_done) {
+ on_done(req_status, {std::forward<ParsedMessage>(msg)});
+ }
+ },
+ [=](const Request& req_status, bool done) { /* on expired */
+ if (on_expired) {
+ on_expired(req_status, done);
+ }
+ }
+ );
+ sendRequest(req);
+ ++out_stats.find;
+ return req;
+}
+
+
+Sp<Request>
+NetworkEngine::sendGetValues(const Sp<Node>& n, const InfoHash& info_hash, const Query& query, want_t want,
+ RequestCb&& on_done, RequestExpiredCb&& on_expired) {
+ Tid tid (n->getNewTid());
+ msgpack::sbuffer buffer;
+ msgpack::packer<msgpack::sbuffer> pk(&buffer);
+ pk.pack_map(5+(config.network?1:0));
+
+ unsigned sendQuery = (not query.where.empty() or not query.select.empty()) ? 1 : 0;
+ unsigned sendWant = (want > 0) ? 1 : 0;
+
+ pk.pack(KEY_A); pk.pack_map(2 + sendQuery + sendWant);
+ pk.pack(KEY_REQ_ID); pk.pack(myid);
+ pk.pack(KEY_REQ_H); pk.pack(info_hash);
+ if (sendQuery) {
+ pk.pack(KEY_Q); pk.pack(query);
+ }
+ if (sendWant) {
+ pk.pack(KEY_REQ_WANT);
+ unsigned sendWant4 = (want & WANT4) ? 1 : 0;
+ unsigned sendWant6 = (want & WANT6) ? 1 : 0;
+ pk.pack_array(sendWant4 + sendWant6);
+ if (sendWant4) pk.pack(AF_INET);
+ if (sendWant6) pk.pack(AF_INET6);
+ }
+
+ pk.pack(KEY_Q); pk.pack(QUERY_GET);
+ pk.pack(KEY_TID); pk.pack(tid);
+ pk.pack(KEY_Y); pk.pack(KEY_Q);
+ pk.pack(KEY_UA); pk.pack(OPENDHT_UA);
+ if (config.network) {
+ pk.pack(KEY_NETID); pk.pack(config.network);
+ }
+
+ auto req = std::make_shared<Request>(MessageType::GetValues, tid, n,
+ Blob(buffer.data(), buffer.data() + buffer.size()),
+ [=](const Request& req_status, ParsedMessage&& msg) { /* on done */
+ if (on_done) {
+ on_done(req_status, {std::forward<ParsedMessage>(msg)});
+ }
+ },
+ [=](const Request& req_status, bool done) { /* on expired */
+ if (on_expired) {
+ on_expired(req_status, done);
+ }
+ }
+ );
+ sendRequest(req);
+ ++out_stats.get;
+ return req;
+}
+
+SockAddr deserializeIPv4(const uint8_t* ni) {
+ SockAddr addr;
+ addr.setFamily(AF_INET);
+ auto& sin = addr.getIPv4();
+ std::memcpy(&sin.sin_addr, ni, 4);
+ std::memcpy(&sin.sin_port, ni + 4, 2);
+ return addr;
+}
+SockAddr deserializeIPv6(const uint8_t* ni) {
+ SockAddr addr;
+ addr.setFamily(AF_INET6);
+ auto& sin6 = addr.getIPv6();
+ std::memcpy(&sin6.sin6_addr, ni, 16);
+ std::memcpy(&sin6.sin6_port, ni + 16, 2);
+ return addr;
+}
+
+void
+NetworkEngine::deserializeNodes(ParsedMessage& msg, const SockAddr& from) {
+ if (msg.nodes4_raw.size() % NODE4_INFO_BUF_LEN != 0 || msg.nodes6_raw.size() % NODE6_INFO_BUF_LEN != 0) {
+ throw DhtProtocolException {DhtProtocolException::WRONG_NODE_INFO_BUF_LEN};
+ }
+ // deserialize nodes
+ const auto& now = scheduler.time();
+ for (unsigned i = 0, n = msg.nodes4_raw.size() / NODE4_INFO_BUF_LEN; i < n; i++) {
+ const uint8_t* ni = msg.nodes4_raw.data() + i * NODE4_INFO_BUF_LEN;
+ const auto& ni_id = *reinterpret_cast<const InfoHash*>(ni);
+ if (ni_id == myid)
+ continue;
+ SockAddr addr = deserializeIPv4(ni + ni_id.size());
+ if (addr.isLoopback() and from.getFamily() == AF_INET) {
+ auto port = addr.getPort();
+ addr = from;
+ addr.setPort(port);
+ }
+ if (isMartian(addr) || isNodeBlacklisted(addr))
+ continue;
+ msg.nodes4.emplace_back(cache.getNode(ni_id, addr, now, false));
+ onNewNode(msg.nodes4.back(), 0);
+ }
+ for (unsigned i = 0, n = msg.nodes6_raw.size() / NODE6_INFO_BUF_LEN; i < n; i++) {
+ const uint8_t* ni = msg.nodes6_raw.data() + i * NODE6_INFO_BUF_LEN;
+ const auto& ni_id = *reinterpret_cast<const InfoHash*>(ni);
+ if (ni_id == myid)
+ continue;
+ SockAddr addr = deserializeIPv6(ni + ni_id.size());
+ if (addr.isLoopback() and from.getFamily() == AF_INET6) {
+ auto port = addr.getPort();
+ addr = from;
+ addr.setPort(port);
+ }
+ if (isMartian(addr) || isNodeBlacklisted(addr))
+ continue;
+ msg.nodes6.emplace_back(cache.getNode(ni_id, addr, now, false));
+ onNewNode(msg.nodes6.back(), 0);
+ }
+}
+
+std::vector<Blob>
+NetworkEngine::packValueHeader(msgpack::sbuffer& buffer, std::vector<Sp<Value>>::const_iterator b, std::vector<Sp<Value>>::const_iterator e) const
+{
+ std::vector<Blob> svals;
+ size_t total_size = 0;
+
+ svals.reserve(std::distance(b, e));
+ for (; b != e; ++b) {
+ svals.emplace_back(packMsg(*b));
+ total_size += svals.back().size();
+ }
+
+ msgpack::packer<msgpack::sbuffer> pk(&buffer);
+ pk.pack(KEY_REQ_VALUES);
+ pk.pack_array(svals.size());
+ // try to put everything in a single UDP packet
+ if (svals.size() < 50 && total_size < MAX_PACKET_VALUE_SIZE) {
+ for (const auto& b : svals)
+ buffer.write((const char*)b.data(), b.size());
+ // if (logger_)
+ // logger_->d("sending %lu bytes of values", total_size);
+ svals.clear();
+ } else {
+ for (const auto& b : svals)
+ pk.pack(b.size());
+ }
+ return svals;
+}
+
+void
+NetworkEngine::sendValueParts(Tid tid, const std::vector<Blob>& svals, const SockAddr& addr)
+{
+ msgpack::sbuffer buffer;
+ unsigned i=0;
+ for (const auto& v: svals) {
+ size_t start {0}, end;
+ do {
+ end = std::min(start + MTU, v.size());
+ buffer.clear();
+ msgpack::packer<msgpack::sbuffer> pk(&buffer);
+ pk.pack_map(3+(config.network?1:0));
+ if (config.network) {
+ pk.pack(KEY_NETID); pk.pack(config.network);
+ }
+ pk.pack(KEY_Y); pk.pack(KEY_V);
+ pk.pack(KEY_TID); pk.pack(tid);
+ pk.pack(KEY_V); pk.pack_map(1);
+ pk.pack(i); pk.pack_map(2);
+ pk.pack("o"sv); pk.pack(start);
+ pk.pack("d"sv); pk.pack_bin(end-start);
+ pk.pack_bin_body((const char*)v.data()+start, end-start);
+ send(addr, buffer.data(), buffer.size());
+ start = end;
+ } while (start != v.size());
+ i++;
+ }
+}
+
+void
+NetworkEngine::sendNodesValues(const SockAddr& addr, Tid tid, const Blob& nodes, const Blob& nodes6,
+ const std::vector<Sp<Value>>& st, const Query& query, const Blob& token)
+{
+ msgpack::sbuffer buffer;
+ msgpack::packer<msgpack::sbuffer> pk(&buffer);
+ pk.pack_map(4+(config.network?1:0));
+
+ pk.pack(KEY_R);
+ pk.pack_map(2 + (not st.empty()?1:0) + (nodes.size()>0?1:0) + (nodes6.size()>0?1:0) + (not token.empty()?1:0));
+ pk.pack(KEY_REQ_ID); pk.pack(myid);
+ insertAddr(pk, addr);
+ if (nodes.size() > 0) {
+ pk.pack(KEY_REQ_NODES4);
+ pk.pack_bin(nodes.size());
+ pk.pack_bin_body((const char*)nodes.data(), nodes.size());
+ }
+ if (nodes6.size() > 0) {
+ pk.pack(KEY_REQ_NODES6);
+ pk.pack_bin(nodes6.size());
+ pk.pack_bin_body((const char*)nodes6.data(), nodes6.size());
+ }
+ if (not token.empty()) {
+ pk.pack(KEY_REQ_TOKEN); packToken(pk, token);
+ }
+ std::vector<Blob> svals {};
+ if (not st.empty()) { /* pack complete values */
+ if (query.select.empty()) {
+ svals = packValueHeader(buffer, st);
+ } else { /* pack fields */
+ auto fields = query.select.getSelection();
+ pk.pack(KEY_REQ_FIELDS);
+ pk.pack_map(2);
+ pk.pack("f"sv); pk.pack(fields);
+ pk.pack("v"sv); pk.pack_array(st.size()*fields.size());
+ for (const auto& v : st)
+ v->msgpack_pack_fields(fields, pk);
+ //DHT_LOG_DBG("sending closest nodes (%d+%d nodes.), %u value headers containing %u fields",
+ // nodes.size(), nodes6.size(), st.size(), fields.size());
+ }
+ }
+
+ pk.pack(KEY_TID); pk.pack(tid);
+ pk.pack(KEY_Y); pk.pack(KEY_R);
+ pk.pack(KEY_UA); pk.pack(OPENDHT_UA);
+ if (config.network) {
+ pk.pack(KEY_NETID); pk.pack(config.network);
+ }
+
+ // send response
+ send(addr, buffer.data(), buffer.size());
+
+ // send parts
+ if (not svals.empty())
+ sendValueParts(tid, svals, addr);
+}
+
+Blob
+NetworkEngine::bufferNodes(sa_family_t af, const InfoHash& id, std::vector<Sp<Node>>& nodes)
+{
+ std::sort(nodes.begin(), nodes.end(), [&](const Sp<Node>& a, const Sp<Node>& b){
+ return id.xorCmp(a->id, b->id) < 0;
+ });
+ size_t nnode = std::min<size_t>(SEND_NODES, nodes.size());
+ Blob bnodes;
+ if (af == AF_INET) {
+ bnodes.resize(NODE4_INFO_BUF_LEN * nnode);
+ for (size_t i=0; i<nnode; i++) {
+ const Node& n = *nodes[i];
+ const auto& sin = n.getAddr().getIPv4();
+ auto dest = bnodes.data() + NODE4_INFO_BUF_LEN * i;
+ memcpy(dest, n.id.data(), HASH_LEN);
+ memcpy(dest + HASH_LEN, &sin.sin_addr, sizeof(in_addr));
+ memcpy(dest + HASH_LEN + sizeof(in_addr), &sin.sin_port, sizeof(in_port_t));
+ }
+ } else if (af == AF_INET6) {
+ bnodes.resize(NODE6_INFO_BUF_LEN * nnode);
+ for (size_t i=0; i<nnode; i++) {
+ const Node& n = *nodes[i];
+ const auto& sin6 = n.getAddr().getIPv6();
+ auto dest = bnodes.data() + NODE6_INFO_BUF_LEN * i;
+ memcpy(dest, n.id.data(), HASH_LEN);
+ memcpy(dest + HASH_LEN, &sin6.sin6_addr, sizeof(in6_addr));
+ memcpy(dest + HASH_LEN + sizeof(in6_addr), &sin6.sin6_port, sizeof(in_port_t));
+ }
+ }
+ return bnodes;
+}
+
+std::pair<Blob, Blob>
+NetworkEngine::bufferNodes(sa_family_t af, const InfoHash& id, want_t want,
+ std::vector<Sp<Node>>& nodes4, std::vector<Sp<Node>>& nodes6)
+{
+ if (want < 0)
+ want = af == AF_INET ? WANT4 : WANT6;
+
+ Blob bnodes4;
+ if (want & WANT4)
+ bnodes4 = bufferNodes(AF_INET, id, nodes4);
+
+ Blob bnodes6;
+ if (want & WANT6)
+ bnodes6 = bufferNodes(AF_INET6, id, nodes6);
+
+ return {std::move(bnodes4), std::move(bnodes6)};
+}
+
+Sp<Request>
+NetworkEngine::sendListen(const Sp<Node>& n,
+ const InfoHash& hash,
+ const Query& query,
+ const Blob& token,
+ Tid socketId,
+ RequestCb&& on_done,
+ RequestExpiredCb&& on_expired)
+{
+ Tid tid (n->getNewTid());
+ msgpack::sbuffer buffer;
+ msgpack::packer<msgpack::sbuffer> pk(&buffer);
+ pk.pack_map(5+(config.network?1:0));
+
+ auto has_query = not query.where.empty() or not query.select.empty();
+ pk.pack(KEY_A); pk.pack_map(5 + has_query);
+ pk.pack(KEY_REQ_ID); pk.pack(myid);
+ pk.pack(KEY_VERSION); pk.pack(1);
+ pk.pack(KEY_REQ_H); pk.pack(hash);
+ pk.pack(KEY_REQ_TOKEN); packToken(pk, token);
+ pk.pack(KEY_REQ_SID); pk.pack(socketId);
+ if (has_query) {
+ pk.pack(KEY_REQ_QUERY); pk.pack(query);
+ }
+
+ pk.pack(KEY_Q); pk.pack(QUERY_LISTEN);
+ pk.pack(KEY_TID); pk.pack(tid);
+ pk.pack(KEY_Y); pk.pack(KEY_Q);
+ pk.pack(KEY_UA); pk.pack(OPENDHT_UA);
+ if (config.network) {
+ pk.pack(KEY_NETID); pk.pack(config.network);
+ }
+
+ auto req = std::make_shared<Request>(MessageType::Listen, tid, n,
+ Blob(buffer.data(), buffer.data() + buffer.size()),
+ [=](const Request& req_status, ParsedMessage&& msg) { /* on done */
+ if (on_done)
+ on_done(req_status, {std::forward<ParsedMessage>(msg)});
+ },
+ [=](const Request& req_status, bool done) { /* on expired */
+ if (on_expired)
+ on_expired(req_status, done);
+ }
+ );
+ sendRequest(req);
+ ++out_stats.listen;
+ return req;
+}
+
+void
+NetworkEngine::sendListenConfirmation(const SockAddr& addr, Tid tid) {
+ msgpack::sbuffer buffer;
+ msgpack::packer<msgpack::sbuffer> pk(&buffer);
+ pk.pack_map(4+(config.network?1:0));
+
+ pk.pack(KEY_R); pk.pack_map(2);
+ pk.pack(KEY_REQ_ID); pk.pack(myid);
+ insertAddr(pk, addr);
+
+ pk.pack(KEY_TID); pk.pack(tid);
+ pk.pack(KEY_Y); pk.pack(KEY_R);
+ pk.pack(KEY_UA); pk.pack(OPENDHT_UA);
+ if (config.network) {
+ pk.pack(KEY_NETID); pk.pack(config.network);
+ }
+
+ send(addr, buffer.data(), buffer.size());
+}
+
+Sp<Request>
+NetworkEngine::sendAnnounceValue(const Sp<Node>& n,
+ const InfoHash& infohash,
+ const Sp<Value>& value,
+ time_point created,
+ const Blob& token,
+ RequestCb&& on_done,
+ RequestExpiredCb&& on_expired)
+{
+ Tid tid (n->getNewTid());
+ msgpack::sbuffer buffer;
+ msgpack::packer<msgpack::sbuffer> pk(&buffer);
+ pk.pack_map(5+(config.network?1:0));
+
+ bool add_created = created < scheduler.time();
+ pk.pack(KEY_A); pk.pack_map(add_created ? 5 : 4);
+ pk.pack(KEY_REQ_ID); pk.pack(myid);
+ pk.pack(KEY_REQ_H); pk.pack(infohash);
+ auto v = packValueHeader(buffer, {value});
+ if (add_created) {
+ pk.pack(KEY_REQ_CREATION);
+ pk.pack(to_time_t(created));
+ }
+ pk.pack(KEY_REQ_TOKEN); pk.pack(token);
+
+ pk.pack(KEY_Q); pk.pack(QUERY_PUT);
+ pk.pack(KEY_TID); pk.pack(tid);
+ pk.pack(KEY_Y); pk.pack(KEY_Q);
+ pk.pack(KEY_UA); pk.pack(OPENDHT_UA);
+ if (config.network) {
+ pk.pack(KEY_NETID); pk.pack(config.network);
+ }
+
+ auto req = std::make_shared<Request>(MessageType::AnnounceValue, tid, n,
+ Blob(buffer.data(), buffer.data() + buffer.size()),
+ [=](const Request& req_status, ParsedMessage&& msg) { /* on done */
+ if (msg.value_id == Value::INVALID_ID) {
+ if (logger_)
+ logger_->d(infohash, "Unknown search or announce!");
+ } else {
+ if (on_done) {
+ RequestAnswer answer {};
+ answer.vid = msg.value_id;
+ on_done(req_status, std::move(answer));
+ }
+ }
+ },
+ [=](const Request& req_status, bool done) { /* on expired */
+ if (on_expired) {
+ on_expired(req_status, done);
+ }
+ }
+ );
+ req->parts = std::move(v);
+ sendRequest(req);
+ ++out_stats.put;
+ return req;
+}
+
+void
+NetworkEngine::sendUpdateValues(const Sp<Node>& n,
+ const InfoHash& infohash,
+ std::vector<Sp<Value>>&& values,
+ time_point created,
+ const Blob& token,
+ size_t socket_id)
+{
+ size_t total_size = 0;
+
+ auto b = values.begin(), e = values.begin();
+ while (e != values.end()) {
+ if (total_size >= MAX_MESSAGE_VALUE_SIZE) {
+ sendUpdateValues(n, infohash, b, e, created, token, socket_id);
+ b = e;
+ total_size = 0;
+ }
+ total_size += (*e)->size();
+ ++e;
+ }
+ if (b != e) {
+ sendUpdateValues(n, infohash, b, e, created, token, socket_id);
+ }
+}
+
+Sp<Request>
+NetworkEngine::sendUpdateValues(const Sp<Node>& n,
+ const InfoHash& infohash,
+ std::vector<Sp<Value>>::iterator begin,
+ std::vector<Sp<Value>>::iterator end,
+ time_point created,
+ const Blob& token,
+ size_t socket_id)
+{
+ Tid tid (n->getNewTid());
+ Tid sid (socket_id);
+
+ msgpack::sbuffer buffer;
+ msgpack::packer<msgpack::sbuffer> pk(&buffer);
+ pk.pack_map(5+(config.network?1:0));
+
+ pk.pack(KEY_A); pk.pack_map((created < scheduler.time() ? 7 : 6));
+ pk.pack(KEY_REQ_ID); pk.pack(myid);
+ pk.pack(KEY_VERSION); pk.pack(1);
+ pk.pack(KEY_REQ_H); pk.pack(infohash);
+ pk.pack(KEY_REQ_SID); pk.pack(sid);
+ auto v = packValueHeader(buffer, begin, end);
+ if (created < scheduler.time()) {
+ pk.pack(KEY_REQ_CREATION);
+ pk.pack(to_time_t(created));
+ }
+ pk.pack(KEY_REQ_TOKEN); pk.pack(token);
+
+ pk.pack(KEY_Q); pk.pack(QUERY_UPDATE);
+ pk.pack(KEY_TID); pk.pack(tid);
+ pk.pack(KEY_Y); pk.pack(KEY_Q);
+ pk.pack(KEY_UA); pk.pack(OPENDHT_UA);
+ if (config.network) {
+ pk.pack(KEY_NETID); pk.pack(config.network);
+ }
+
+ auto req = std::make_shared<Request>(MessageType::UpdateValue, tid, n,
+ Blob(buffer.data(), buffer.data() + buffer.size()),
+ [=](const Request&, ParsedMessage&&) { /* on done */ },
+ [=](const Request&, bool) { /* on expired */ }
+ );
+ req->parts = std::move(v);
+ sendRequest(req);
+ ++out_stats.updateValue;
+ return req;
+}
+
+Sp<Request>
+NetworkEngine::sendRefreshValue(const Sp<Node>& n,
+ const InfoHash& infohash,
+ const Value::Id& vid,
+ const Blob& token,
+ RequestCb&& on_done,
+ RequestErrorCb&& on_error,
+ RequestExpiredCb&& on_expired)
+{
+ Tid tid (n->getNewTid());
+ msgpack::sbuffer buffer;
+ msgpack::packer<msgpack::sbuffer> pk(&buffer);
+ pk.pack_map(5+(config.network?1:0));
+
+ pk.pack(KEY_A); pk.pack_map(4);
+ pk.pack(KEY_REQ_ID); pk.pack(myid);
+ pk.pack(KEY_REQ_H); pk.pack(infohash);
+ pk.pack(KEY_REQ_VALUE_ID); pk.pack(vid);
+ pk.pack(KEY_REQ_TOKEN); pk.pack(token);
+
+ pk.pack(KEY_Q); pk.pack(QUERY_REFRESH);
+ pk.pack(KEY_TID); pk.pack(tid);
+ pk.pack(KEY_Y); pk.pack(KEY_Q);
+ pk.pack(KEY_UA); pk.pack(OPENDHT_UA);
+ if (config.network) {
+ pk.pack(KEY_NETID); pk.pack(config.network);
+ }
+
+ auto req = std::make_shared<Request>(MessageType::Refresh, tid, n,
+ Blob(buffer.data(), buffer.data() + buffer.size()),
+ [=](const Request& req_status, ParsedMessage&& msg) { /* on done */
+ if (msg.value_id == Value::INVALID_ID) {
+ if (logger_)
+ logger_->d(infohash, "Unknown search or announce!");
+ } else {
+ if (on_done) {
+ RequestAnswer answer {};
+ answer.vid = msg.value_id;
+ on_done(req_status, std::move(answer));
+ }
+ }
+ },
+ on_error,
+ [=](const Request& req_status, bool done) { /* on expired */
+ if (on_expired) {
+ on_expired(req_status, done);
+ }
+ }
+ );
+ sendRequest(req);
+ ++out_stats.refresh;
+ return req;
+}
+
+void
+NetworkEngine::sendValueAnnounced(const SockAddr& addr, Tid tid, Value::Id vid) {
+ msgpack::sbuffer buffer;
+ msgpack::packer<msgpack::sbuffer> pk(&buffer);
+ pk.pack_map(4+(config.network?1:0));
+
+ pk.pack(KEY_R); pk.pack_map(3);
+ pk.pack(KEY_REQ_ID); pk.pack(myid);
+ pk.pack(KEY_REQ_VALUE_ID); pk.pack(vid);
+ insertAddr(pk, addr);
+
+ pk.pack(KEY_TID); pk.pack(tid);
+ pk.pack(KEY_Y); pk.pack(KEY_R);
+ pk.pack(KEY_UA); pk.pack(OPENDHT_UA);
+ if (config.network) {
+ pk.pack(KEY_NETID); pk.pack(config.network);
+ }
+
+ send(addr, buffer.data(), buffer.size());
+}
+
+void
+NetworkEngine::sendError(const SockAddr& addr,
+ Tid tid,
+ uint16_t code,
+ const std::string& message,
+ bool include_id)
+{
+ msgpack::sbuffer buffer;
+ msgpack::packer<msgpack::sbuffer> pk(&buffer);
+ pk.pack_map(4 + (include_id?1:0) + (config.network?1:0));
+
+ pk.pack(KEY_E); pk.pack_array(2);
+ pk.pack(code);
+ pk.pack(message);
+
+ if (include_id) {
+ pk.pack(KEY_R); pk.pack_map(1);
+ pk.pack(KEY_REQ_ID); pk.pack(myid);
+ }
+
+ pk.pack(KEY_TID); pk.pack(tid);
+ pk.pack(KEY_Y); pk.pack(KEY_E);
+ pk.pack(KEY_UA); pk.pack(OPENDHT_UA);
+ if (config.network) {
+ pk.pack(KEY_NETID); pk.pack(config.network);
+ }
+
+ send(addr, buffer.data(), buffer.size());
+}
+
+void
+NetworkEngine::maintainRxBuffer(Tid tid)
+{
+ auto msg = partial_messages.find(tid);
+ if (msg != partial_messages.end()) {
+ const auto& now = scheduler.time();
+ if (msg->second.start + RX_MAX_PACKET_TIME < now
+ || msg->second.last_part + RX_TIMEOUT < now) {
+ if (logger_)
+ logger_->w("Dropping expired partial message from %s", msg->second.from.toString().c_str());
+ partial_messages.erase(msg);
+ }
+ }
+}
+
+
+} /* namespace net */
+} /* namespace dht */
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ * Author(s) : Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "network_utils.h"
+
+#ifdef _WIN32
+#include "utils.h"
+#include <io.h>
+#include <string>
+#include <cstring>
+#define close(x) closesocket(x)
+#define write(s, b, f) send(s, b, (int)strlen(b), 0)
+#else
+#include <sys/select.h>
+#include <fcntl.h>
+#endif
+
+#include <iostream>
+
+namespace dht {
+namespace net {
+
+int
+bindSocket(const SockAddr& addr, SockAddr& bound)
+{
+ bool is_ipv6 = addr.getFamily() == AF_INET6;
+ int sock = socket(is_ipv6 ? PF_INET6 : PF_INET, SOCK_DGRAM, 0);
+ if (sock < 0)
+ throw DhtException(std::string("Can't open socket: ") + strerror(sock));
+ int set = 1;
+#ifdef SO_NOSIGPIPE
+ setsockopt(sock, SOL_SOCKET, SO_NOSIGPIPE, (const char*)&set, sizeof(set));
+#endif
+ if (is_ipv6)
+ setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, (const char*)&set, sizeof(set));
+ net::setNonblocking(sock);
+ int rc = bind(sock, addr.get(), addr.getLength());
+ if (rc < 0) {
+ rc = errno;
+ close(sock);
+ throw DhtException("Can't bind socket on " + addr.toString() + " " + strerror(rc));
+ }
+ sockaddr_storage ss;
+ socklen_t ss_len = sizeof(ss);
+ getsockname(sock, (sockaddr*)&ss, &ss_len);
+ bound = {ss, ss_len};
+ return sock;
+}
+
+bool
+setNonblocking(int fd, bool nonblocking)
+{
+#ifdef _WIN32
+ unsigned long mode = !!nonblocking;
+ int rc = ioctlsocket(fd, FIONBIO, &mode);
+ return rc == 0;
+#else
+ int rc = fcntl(fd, F_GETFL, 0);
+ if (rc < 0)
+ return false;
+ rc = fcntl(fd, F_SETFL, nonblocking ? (rc | O_NONBLOCK) : (rc & ~O_NONBLOCK));
+ return rc >= 0;
+#endif
+}
+
+#ifdef _WIN32
+void udpPipe(int fds[2])
+{
+ int lst = socket(AF_INET, SOCK_DGRAM, 0);
+ if (lst < 0)
+ throw DhtException(std::string("Can't open socket: ") + strerror(WSAGetLastError()));
+ sockaddr_in inaddr;
+ sockaddr addr;
+ memset(&inaddr, 0, sizeof(inaddr));
+ memset(&addr, 0, sizeof(addr));
+ inaddr.sin_family = AF_INET;
+ inaddr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
+ inaddr.sin_port = 0;
+ int yes = 1;
+ setsockopt(lst, SOL_SOCKET, SO_REUSEADDR, (char*)&yes, sizeof(yes));
+ int rc = bind(lst, (sockaddr*)&inaddr, sizeof(inaddr));
+ if (rc < 0) {
+ close(lst);
+ throw DhtException("Can't bind socket on " + print_addr((sockaddr*)&inaddr, sizeof(inaddr)) + " " + strerror(rc));
+ }
+ socklen_t len = sizeof(addr);
+ getsockname(lst, &addr, &len);
+ fds[0] = lst;
+ fds[1] = socket(AF_INET, SOCK_DGRAM, 0);
+ connect(fds[1], &addr, len);
+}
+#endif
+
+UdpSocket::UdpSocket(in_port_t port, const std::shared_ptr<Logger>& l) : logger(l) {
+ SockAddr bind4;
+ bind4.setFamily(AF_INET);
+ bind4.setPort(port);
+ SockAddr bind6;
+ bind6.setFamily(AF_INET6);
+ bind6.setPort(port);
+ std::lock_guard<std::mutex> lk(lock);
+ openSockets(bind4, bind6);
+}
+
+UdpSocket::UdpSocket(const SockAddr& bind4, const SockAddr& bind6, const std::shared_ptr<Logger>& l) : logger(l)
+{
+ std::lock_guard<std::mutex> lk(lock);
+ openSockets(bind4, bind6);
+}
+
+UdpSocket::~UdpSocket() {
+ stop();
+ if (rcv_thread.joinable())
+ rcv_thread.join();
+}
+
+int
+UdpSocket::sendTo(const SockAddr& dest, const uint8_t* data, size_t size, bool replied) {
+ if (not dest)
+ return EFAULT;
+
+ int s;
+ switch (dest.getFamily()) {
+ case AF_INET: s = s4; break;
+ case AF_INET6: s = s6; break;
+ default: s = -1; break;
+ }
+
+ if (s < 0)
+ return EAFNOSUPPORT;
+
+ int flags = 0;
+#ifdef MSG_CONFIRM
+ if (replied)
+ flags |= MSG_CONFIRM;
+#else
+ (void) replied;
+#endif
+#ifdef MSG_NOSIGNAL
+ flags |= MSG_NOSIGNAL;
+#endif
+
+ if (sendto(s, (const char*)data, size, flags, dest.get(), dest.getLength()) == -1) {
+ int err = errno;
+ if (logger)
+ logger->d("Can't send message to %s: %s", dest.toString().c_str(), strerror(err));
+ if (err == EPIPE || err == ENOTCONN || err == ECONNRESET) {
+ std::lock_guard<std::mutex> lk(lock);
+ auto bind4 = std::move(bound4), bind6 = std::move(bound6);
+ openSockets(bind4, bind6);
+ return sendTo(dest, data, size, false);
+ }
+ return err;
+ }
+ return 0;
+}
+
+void
+UdpSocket::openSockets(const SockAddr& bind4, const SockAddr& bind6)
+{
+ stop();
+ if (rcv_thread.joinable())
+ rcv_thread.join();
+
+ int stopfds[2];
+#ifndef _WIN32
+ auto status = pipe(stopfds);
+ if (status == -1) {
+ throw DhtException(std::string("Can't open pipe: ") + strerror(errno));
+ }
+#else
+ udpPipe(stopfds);
+#endif
+ int stop_readfd = stopfds[0];
+
+ stopfd = stopfds[1];
+ s4 = -1;
+ s6 = -1;
+
+ bound4 = {};
+ if (bind4) {
+ try {
+ s4 = bindSocket(bind4, bound4);
+ } catch (const DhtException& e) {
+ if (logger)
+ logger->e("Can't bind inet socket: %s", e.what());
+ }
+ }
+
+#if 1
+ bound6 = {};
+ if (bind6) {
+ if (bind6.getPort() == 0) {
+ // Attempt to use the same port as IPv4 with IPv6
+ if (auto p4 = bound4.getPort()) {
+ auto b6 = bind6;
+ b6.setPort(p4);
+ try {
+ s6 = bindSocket(b6, bound6);
+ } catch (const DhtException& e) {
+ if (logger)
+ logger->e("Can't bind inet6 socket: %s", e.what());
+ }
+ }
+ }
+ if (s6 == -1) {
+ try {
+ s6 = bindSocket(bind6, bound6);
+ } catch (const DhtException& e) {
+ if (logger)
+ logger->e("Can't bind inet6 socket: %s", e.what());
+ }
+ }
+ }
+#endif
+
+ if (s4 == -1 && s6 == -1) {
+ throw DhtException("Can't bind socket");
+ }
+
+ running = true;
+ rcv_thread = std::thread([this, stop_readfd, ls4=s4, ls6=s6]() mutable {
+ int selectFd = std::max({ls4, ls6, stop_readfd}) + 1;
+ try {
+ while (running) {
+ fd_set readfds;
+
+ FD_ZERO(&readfds);
+ FD_SET(stop_readfd, &readfds);
+ if(ls4 >= 0)
+ FD_SET(ls4, &readfds);
+ if(ls6 >= 0)
+ FD_SET(ls6, &readfds);
+
+ int rc = select(selectFd, &readfds, nullptr, nullptr, nullptr);
+ if (rc < 0) {
+ if (errno != EINTR) {
+ if (logger)
+ logger->e("Select error: %s", strerror(errno));
+ std::this_thread::sleep_for(std::chrono::seconds(1));
+ }
+ }
+
+ if (not running)
+ break;
+
+ if (rc > 0) {
+ std::array<uint8_t, 1024 * 64> buf;
+ sockaddr_storage from;
+ socklen_t from_len = sizeof(from);
+
+ if (FD_ISSET(stop_readfd, &readfds)) {
+ if (recv(stop_readfd, (char*)buf.data(), buf.size(), 0) < 0) {
+ if (logger)
+ logger->e("Got stop packet error: %s", strerror(errno));
+ break;
+ }
+ }
+ else if (ls4 >= 0 && FD_ISSET(ls4, &readfds))
+ rc = recvfrom(ls4, (char*)buf.data(), buf.size(), 0, (sockaddr*)&from, &from_len);
+ else if (ls6 >= 0 && FD_ISSET(ls6, &readfds))
+ rc = recvfrom(ls6, (char*)buf.data(), buf.size(), 0, (sockaddr*)&from, &from_len);
+ else
+ continue;
+
+ if (rc > 0) {
+ auto pkts = getNewPacket();
+ auto& pkt = pkts.front();
+ pkt.data.insert(pkt.data.end(), buf.begin(), buf.begin()+rc);
+ pkt.from = {from, from_len};
+ pkt.received = clock::now();
+ onReceived(std::move(pkts));
+ } else if (rc == -1) {
+ if (logger)
+ logger->e("Error receiving packet: %s", strerror(errno));
+ int err = errno;
+ if (err == EPIPE || err == ENOTCONN || err == ECONNRESET) {
+ if (not running) break;
+ std::unique_lock<std::mutex> lk(lock, std::try_to_lock);
+ if (lk.owns_lock()) {
+ if (not running) break;
+ if (ls4 >= 0) {
+ close(ls4);
+ try {
+ ls4 = bindSocket(bound4, bound4);
+ } catch (const DhtException& e) {
+ if (logger)
+ logger->e("Can't bind inet socket: %s", e.what());
+ }
+ }
+ if (ls6 >= 0) {
+ close(ls6);
+ try {
+ ls6 = bindSocket(bound6, bound6);
+ } catch (const DhtException& e) {
+ if (logger)
+ logger->e("Can't bind inet6 socket: %s", e.what());
+ }
+ }
+ if (ls4 < 0 && ls6 < 0)
+ break;
+ s4 = ls4;
+ s6 = ls6;
+ selectFd = std::max({ls4, ls6, stop_readfd}) + 1;
+ } else {
+ break;
+ }
+ }
+ }
+ }
+ }
+ } catch (const std::exception& e) {
+ if (logger)
+ logger->e("Error in UdpSocket rx thread: %s", e.what());
+ }
+ if (ls4 >= 0)
+ close(ls4);
+ if (ls6 >= 0)
+ close(ls6);
+ if (stop_readfd != -1)
+ close(stop_readfd);
+ if (stopfd != -1)
+ close(stopfd);
+ std::unique_lock<std::mutex> lk(lock, std::try_to_lock);
+ if (lk.owns_lock()) {
+ s4 = -1;
+ s6 = -1;
+ bound4 = {};
+ bound6 = {};
+ stopfd = -1;
+ }
+ });
+}
+
+void
+UdpSocket::stop()
+{
+ if (running.exchange(false)) {
+ auto sfd = stopfd;
+ if (sfd != -1 && write(sfd, "\0", 1) == -1) {
+ if (logger)
+ logger->e("Can't write to stop fd");
+ }
+ }
+}
+
+}
+}
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ * Author(s) : Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ * Simon Désaulniers <simon.desaulniers@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+
+#include "node.h"
+#include "request.h"
+#include "rng.h"
+
+#include <sstream>
+
+namespace dht {
+
+constexpr std::chrono::minutes Node::NODE_EXPIRE_TIME;
+constexpr std::chrono::minutes Node::NODE_GOOD_TIME;
+constexpr std::chrono::seconds Node::MAX_RESPONSE_TIME;
+
+Node::Node(const InfoHash& id, const SockAddr& addr, std::mt19937_64& rd, bool client)
+: id(id), addr(addr), is_client(client), sockets_()
+{
+ transaction_id = std::uniform_int_distribution<Tid>{1}(rd);
+}
+
+Node::Node(const InfoHash& id, SockAddr&& addr, std::mt19937_64& rd, bool client)
+: id(id), addr(std::move(addr)), is_client(client), sockets_()
+{
+ transaction_id = std::uniform_int_distribution<Tid>{1}(rd);
+}
+
+/* This is our definition of a known-good node. */
+bool
+Node::isGood(time_point now) const
+{
+ return not expired_ &&
+ reply_time >= now - NODE_GOOD_TIME &&
+ time >= now - NODE_EXPIRE_TIME;
+}
+
+bool
+Node::isPendingMessage() const
+{
+ for (const auto& r : requests_) {
+ if (r.second->pending())
+ return true;
+ }
+ return false;
+}
+
+size_t
+Node::getPendingMessageCount() const
+{
+ size_t count {0};
+ for (const auto& r : requests_) {
+ if (r.second->pending())
+ count++;
+ }
+ return count;
+}
+
+void
+Node::update(const SockAddr& new_addr)
+{
+ addr = new_addr;
+}
+
+/** To be called when a message was sent to the node */
+void
+Node::requested(const Sp<net::Request>& req)
+{
+ auto e = requests_.emplace(req->getTid(), req);
+ if (not e.second and req != e.first->second) {
+ // Should not happen !
+ // Try to handle this scenario as well as we can
+ e.first->second->setExpired();
+ e.first->second = req;
+ }
+}
+
+/** To be called when a message was received from the node.
+ Req should be true if the message was an aswer to a request we made*/
+void
+Node::received(time_point now, const Sp<net::Request>& req)
+{
+ time = now;
+ expired_ = false;
+ if (req) {
+ reply_time = now;
+ requests_.erase(req->getTid());
+ }
+}
+
+Sp<net::Request>
+Node::getRequest(Tid tid)
+{
+ auto it = requests_.find(tid);
+ return it != requests_.end() ? it->second : nullptr;
+}
+
+void
+Node::cancelRequest(const Sp<net::Request>& req)
+{
+ if (req) {
+ req->cancel();
+ requests_.erase(req->getTid());
+ }
+}
+
+void
+Node::setExpired()
+{
+ expired_ = true;
+ for (auto r : requests_) {
+ r.second->setExpired();
+ }
+ requests_.clear();
+ sockets_.clear();
+}
+
+Tid
+Node::openSocket(SocketCb&& cb)
+{
+ if (++transaction_id == 0)
+ transaction_id = 1;
+
+ sockets_[transaction_id] = std::make_shared<Socket>(std::move(cb));
+ return transaction_id;
+}
+
+Sp<Socket>
+Node::getSocket(Tid id)
+{
+ auto it = sockets_.find(id);
+ return it == sockets_.end() ? nullptr : it->second;
+}
+
+void
+Node::closeSocket(Tid id)
+{
+ if (id) {
+ sockets_.erase(id);
+ //DHT_LOG.w("Closing socket (tid: %d), %lu remaining", socket->id, sockets_.size());
+ }
+}
+
+std::string
+Node::toString() const
+{
+ std::ostringstream ss;
+ ss << (*this);
+ return ss.str();
+}
+
+std::ostream& operator<< (std::ostream& s, const Node& h)
+{
+ s << h.id << " " << h.addr.toString();
+ return s;
+}
+
+void
+NodeExport::msgpack_unpack(msgpack::object o)
+{
+ if (o.type != msgpack::type::MAP)
+ throw msgpack::type_error();
+ if (o.via.map.size < 2)
+ throw msgpack::type_error();
+ if (o.via.map.ptr[0].key.as<std::string_view>() != "id"sv)
+ throw msgpack::type_error();
+ if (o.via.map.ptr[1].key.as<std::string_view>() != "addr"sv)
+ throw msgpack::type_error();
+ const auto& maddr = o.via.map.ptr[1].val;
+ if (maddr.type != msgpack::type::BIN)
+ throw msgpack::type_error();
+ if (maddr.via.bin.size > sizeof(sockaddr_storage))
+ throw msgpack::type_error();
+ id.msgpack_unpack(o.via.map.ptr[0].val);
+ addr = {(const sockaddr*)maddr.via.bin.ptr, (socklen_t)maddr.via.bin.size};
+}
+
+std::ostream& operator<< (std::ostream& s, const NodeExport& h)
+{
+ msgpack::pack(s, h);
+ return s;
+}
+
+}
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ * Author(s) : Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "node_cache.h"
+
+namespace dht {
+
+constexpr size_t CLEANUP_MAX_NODES {4 * 1024};
+constexpr size_t CLEANUP_FREQ {4 * 1024};
+
+NodeCache::~NodeCache()
+{
+ cache_4.setExpired();
+ cache_6.setExpired();
+}
+
+Sp<Node>
+NodeCache::getNode(const InfoHash& id, sa_family_t family) {
+ return cache(family).getNode(id);
+}
+
+Sp<Node>
+NodeCache::getNode(const InfoHash& id, const SockAddr& addr, time_point now, bool confirm, bool client) {
+ if (not id)
+ return std::make_shared<Node>(id, addr, rd, client);
+ return cache(addr.getFamily()).getNode(id, addr, now, confirm, client, rd);
+}
+
+std::vector<Sp<Node>>
+NodeCache::getCachedNodes(const InfoHash& id, sa_family_t sa_f, size_t count) const
+{
+ return cache(sa_f).getCachedNodes(id, count);
+}
+
+std::vector<Sp<Node>>
+NodeCache::NodeMap::getCachedNodes(const InfoHash& id, size_t count) const
+{
+ std::vector<Sp<Node>> nodes;
+ nodes.reserve(std::min(size(), count));
+ const_iterator it;
+ auto dec_it = [this](const_iterator& it) {
+ auto ret = it;
+ it = (it == cbegin()) ? cend() : std::prev(it);
+ return ret;
+ };
+
+ auto it_p = lower_bound(id),
+ it_n = it_p;
+ if (not empty())
+ dec_it(it_p); /* Create 2 separate iterator if we could */
+
+ while (nodes.size() < count and (it_n != cend() or it_p != cend())) {
+ /* If one of the iterator is at the end, then take the other one
+ If they are both in middle of somewhere comapre both and take
+ the closest to the id. */
+ if (it_p == cend()) it = it_n++;
+ else if (it_n == cend()) it = dec_it(it_p);
+ else it = id.xorCmp(it_p->first, it_n->first) < 0 ? dec_it(it_p) : it_n++;
+
+ if (auto n = it->second.lock())
+ if ( not n->isExpired() and not n->isClient() )
+ nodes.emplace_back(std::move(n));
+ }
+
+ return nodes;
+}
+
+void
+NodeCache::clearBadNodes(sa_family_t family)
+{
+ if (family == 0) {
+ clearBadNodes(AF_INET);
+ clearBadNodes(AF_INET6);
+ } else {
+ cache(family).clearBadNodes();
+ }
+}
+
+Sp<Node>
+NodeCache::NodeMap::getNode(const InfoHash& id)
+{
+ auto wn = find(id);
+ if (wn == end())
+ return {};
+ if (auto n = wn->second.lock())
+ return n;
+ erase(wn);
+ return {};
+}
+
+Sp<Node>
+NodeCache::NodeMap::getNode(const InfoHash& id, const SockAddr& addr, time_point now, bool confirm, bool client, std::mt19937_64& rd)
+{
+ auto& nref = (*this)[id];
+ auto node = nref.lock();
+ if (not node) {
+ node = std::make_shared<Node>(id, addr, rd, client);
+ nref = node;
+ if (cleanup_counter++ == CLEANUP_FREQ) {
+ cleanup();
+ cleanup_counter = 0;
+ }
+ } else if (confirm or node->isOld(now)) {
+ node->update(addr);
+ }
+ return node;
+}
+
+void
+NodeCache::NodeMap::clearBadNodes() {
+ for (auto it = cbegin(); it != cend();) {
+ if (auto n = it->second.lock()) {
+ n->reset();
+ ++it;
+ } else {
+ erase(it++);
+ }
+ }
+ cleanup_counter = 0;
+}
+
+void
+NodeCache::NodeMap::setExpired() {
+ for (auto& wn : *this)
+ if (auto n = wn.second.lock())
+ n->setExpired();
+ clear();
+ cleanup_counter = 0;
+}
+
+void
+NodeCache::NodeMap::cleanup()
+{
+ auto it = lower_bound(InfoHash::getRandom());
+ for (size_t n = 0, maxNodes = std::min(size(), CLEANUP_MAX_NODES); n != maxNodes; n++) {
+ if (it == end())
+ it = begin();
+ if (it->second.expired())
+ erase(it++);
+ else
+ ++it;
+ }
+}
+
+}
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ * Author(s) : Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+#include "op_cache.h"
+
+namespace dht {
+
+constexpr const std::chrono::seconds OpCache::EXPIRATION;
+
+bool
+OpValueCache::onValuesAdded(const std::vector<Sp<Value>>& vals, const system_clock::time_point& t) {
+ std::vector<Sp<Value>> newValues;
+ for (const auto& v : vals) {
+ auto viop = values.emplace(v->id, v);
+ if (viop.second) {
+ newValues.emplace_back(v);
+ } else if (*viop.first->second.data != *v) {
+ // Special case for edition
+ if (v->seq > viop.first->second.data->seq) {
+ viop.first->second.data = v;
+ newValues.emplace_back(v);
+ }
+ } else {
+ viop.first->second.refCount++;
+ }
+ viop.first->second.updated = t;
+ }
+ return newValues.empty() ? true : callback(newValues, false);
+}
+
+bool
+OpValueCache::onValuesExpired(const std::vector<Sp<Value>>& vals, const system_clock::time_point& t) {
+ std::vector<Sp<Value>> expiredValues;
+ for (const auto& v : vals) {
+ auto vit = values.find(v->id);
+ if (vit != values.end()) {
+ if (vit->second.updated > t)
+ continue;
+
+ vit->second.updated = t;
+ vit->second.refCount--;
+ if (not vit->second.refCount) {
+ expiredValues.emplace_back(std::move(vit->second.data));
+ values.erase(vit);
+ }
+ }
+ }
+ return expiredValues.empty() ? true : callback(expiredValues, true);
+}
+
+bool
+OpValueCache::onValuesExpired(const std::vector<Value::Id>& vids, const system_clock::time_point& t)
+{
+ std::vector<Sp<Value>> expiredValues;
+ for (const auto& vid : vids) {
+ auto vit = values.find(vid);
+ if (vit != values.end()) {
+ if (vit->second.updated > t)
+ continue;
+
+ vit->second.updated = t;
+ vit->second.refCount--;
+ if (not vit->second.refCount) {
+ expiredValues.emplace_back(std::move(vit->second.data));
+ values.erase(vit);
+ }
+ }
+ }
+ return expiredValues.empty() ? true : callback(expiredValues, true);
+}
+
+std::vector<Sp<Value>>
+OpValueCache::get(const Value::Filter& filter) const {
+ std::vector<Sp<Value>> ret;
+ if (not filter)
+ ret.reserve(values.size());
+ for (const auto& v : values)
+ if (not filter or filter(*v.second.data))
+ ret.emplace_back(v.second.data);
+ return ret;
+}
+
+Sp<Value>
+OpValueCache::get(Value::Id id) const {
+ auto v = values.find(id);
+ if (v == values.end())
+ return {};
+ return v->second.data;
+}
+
+std::vector<Sp<Value>>
+OpValueCache::getValues() const {
+ std::vector<Sp<Value>> ret;
+ ret.reserve(values.size());
+ for (const auto& v : values)
+ ret.emplace_back(v.second.data);
+ return ret;
+}
+
+void
+OpCache::onValuesAdded(const std::vector<Sp<Value>>& vals) {
+ if (not listeners.empty()) {
+ std::vector<LocalListener> list;
+ list.reserve(listeners.size());
+ for (const auto& l : listeners)
+ list.emplace_back(l.second);
+ for (auto& l : list)
+ l.get_cb(l.filter.filter(vals), false);
+ }
+}
+
+void
+OpCache::onValuesExpired(const std::vector<Sp<Value>>& vals) {
+ if (not listeners.empty()) {
+ std::vector<LocalListener> list;
+ list.reserve(listeners.size());
+ for (const auto& l : listeners)
+ list.emplace_back(l.second);
+ for (auto& l : list)
+ l.get_cb(l.filter.filter(vals), true);
+ }
+}
+
+time_point
+OpCache::getExpiration() const {
+ if (not listeners.empty())
+ return time_point::max();
+ return lastRemoved + EXPIRATION;
+}
+
+SearchCache::OpMap::iterator
+SearchCache::getOp(const Sp<Query>& q)
+{
+ // find exact match
+ auto op = ops.find(q);
+ if (op != ops.end())
+ return op;
+ // find satisfying query
+ for (auto it = ops.begin(); it != ops.end(); it++) {
+ if (q->isSatisfiedBy(*it->first)) {
+ return it;
+ }
+ }
+ return ops.end();
+}
+
+SearchCache::OpMap::const_iterator
+SearchCache::getOp(const Sp<Query>& q) const
+{
+ // find exact match
+ auto op = ops.find(q);
+ if (op != ops.cend())
+ return op;
+ // find satisfying query
+ for (auto it = ops.begin(); it != ops.end(); it++) {
+ if (q->isSatisfiedBy(*it->first)) {
+ return it;
+ }
+ }
+ return ops.cend();
+}
+
+size_t
+SearchCache::listen(const ValueCallback& get_cb, const Sp<Query>& q, Value::Filter&& filter, const OnListen& onListen)
+{
+ // find exact match
+ auto op = getOp(q);
+ if (op == ops.end()) {
+ // New query
+ op = ops.emplace(q, std::make_unique<OpCache>()).first;
+ auto& cache = *op->second;
+ cache.searchToken = onListen(q, [&](const std::vector<Sp<Value>>& values, bool expired){
+ return cache.onValue(values, expired);
+ }, [&](ListenSyncStatus status) {
+ cache.onNodeChanged(status);
+ });
+ }
+ auto token = nextToken_++;
+ if (nextToken_ == 0)
+ nextToken_++;
+ return op->second->addListener(token, get_cb, q, std::move(filter)) ? token : 0;
+}
+
+bool
+SearchCache::cancelListen(size_t gtoken, const time_point& now) {
+ for (auto& op : ops) {
+ if (op.second->removeListener(gtoken, now)) {
+ nextExpiration_ = std::min(nextExpiration_, op.second->getExpiration());
+ return true;
+ }
+ }
+ return false;
+}
+
+void
+SearchCache::cancelAll(const std::function<void(size_t)>& onCancel) {
+ for (auto& op : ops) {
+ auto cache = std::move(op.second);
+ cache->removeAll();
+ onCancel(cache->searchToken);
+ }
+ ops.clear();
+}
+
+time_point
+SearchCache::expire(const time_point& now, const std::function<void(size_t)>& onCancel) {
+ nextExpiration_ = time_point::max();
+ auto ret = nextExpiration_;
+ for (auto it = ops.begin(); it != ops.end();) {
+ auto expiration = it->second->getExpiration();
+ if (expiration < now) {
+ auto cache = std::move(it->second);
+ it = ops.erase(it);
+ onCancel(cache->searchToken);
+ } else {
+ nextExpiration_ = std::min(nextExpiration_, expiration);
+ ret = nextExpiration_;
+ ++it;
+ }
+ }
+ return ret;
+}
+
+bool
+SearchCache::get(const Value::Filter& f, const Sp<Query>& q, const GetCallback& gcb, const DoneCallback& dcb) const
+{
+ auto op = getOp(q);
+ if (op != ops.end()) {
+ auto vals = op->second->get(f);
+ if ((not vals.empty() and not gcb(vals)) or op->second->isSynced()) {
+ dcb(true, {});
+ return true;
+ }
+ }
+ return false;
+}
+
+std::vector<Sp<Value>>
+SearchCache::get(const Value::Filter& filter) const {
+ if (ops.size() == 1)
+ return ops.begin()->second->get(filter);
+ std::map<Value::Id, Sp<Value>> c;
+ for (const auto& op : ops) {
+ for (const auto& v : op.second->get(filter))
+ c.emplace(v->id, v);
+ }
+ std::vector<Sp<Value>> ret;
+ ret.reserve(c.size());
+ for (auto& v : c)
+ ret.emplace_back(std::move(v.second));
+ return ret;
+}
+
+Sp<Value>
+SearchCache::get(Value::Id id) const {
+ for (const auto& op : ops)
+ if (auto v = op.second->get(id))
+ return v;
+ return {};
+}
+
+}
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ * Author(s) : Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+#pragma once
+
+#include "value.h"
+#include "value_cache.h"
+#include "listener.h"
+
+namespace dht {
+
+struct OpCacheValueStorage
+{
+ Sp<Value> data {};
+ unsigned refCount {1};
+ system_clock::time_point updated {system_clock::time_point::min()};
+ explicit OpCacheValueStorage(Sp<Value> val) : data(std::move(val)) {}
+};
+
+class OpValueCache {
+public:
+ OpValueCache(ValueCallback&& cb) noexcept : callback(std::forward<ValueCallback>(cb)) {}
+ explicit OpValueCache(OpValueCache&& o) noexcept : values(std::move(o.values)), callback(std::move(o.callback)) {
+ o.callback = {};
+ }
+
+ static ValueCallback cacheCallback(ValueCallback&& cb, std::function<void()>&& onCancel) {
+ return [
+ cache = std::make_shared<OpValueCache>(std::forward<ValueCallback>(cb)),
+ onCancel = std::move(onCancel)
+ ](const std::vector<Sp<Value>>& vals, bool expired){
+ auto ret = cache->onValue(vals, expired);
+ if (not ret)
+ onCancel();
+ return ret;
+ };
+ }
+
+ bool onValue(const std::vector<Sp<Value>>& vals, bool expired, const system_clock::time_point& t = system_clock::time_point::min()) {
+ return expired
+ ? onValuesExpired(vals, t)
+ : onValuesAdded(vals, t);
+ }
+
+ bool onValuesAdded(const std::vector<Sp<Value>>& vals, const system_clock::time_point& t = system_clock::time_point::min());
+ bool onValuesExpired(const std::vector<Sp<Value>>& vals, const system_clock::time_point& t = system_clock::time_point::min());
+ bool onValuesExpired(const std::vector<Value::Id>& vals, const system_clock::time_point& t = system_clock::time_point::min());
+
+ void onNodeChanged(ListenSyncStatus status) {
+ switch (status) {
+ case ListenSyncStatus::ADDED: nodes++; break;
+ case ListenSyncStatus::REMOVED: nodes--; break;
+ case ListenSyncStatus::SYNCED : syncedNodes++; break;
+ case ListenSyncStatus::UNSYNCED: syncedNodes--; break;
+ }
+ }
+
+ bool isSynced() const { return nodes > 0 and syncedNodes == nodes; }
+
+ std::vector<Sp<Value>> get(const Value::Filter& filter) const;
+ Sp<Value> get(Value::Id id) const;
+ std::vector<Sp<Value>> getValues() const;
+ size_t size() const { return values.size(); }
+
+private:
+ OpValueCache(const OpValueCache&) = delete;
+ OpValueCache& operator=(const OpValueCache&) = delete;
+
+ size_t nodes {0};
+ size_t syncedNodes {0};
+ std::map<Value::Id, OpCacheValueStorage> values {};
+ ValueCallback callback;
+};
+
+class OpCache {
+public:
+ OpCache() : cache([this](const std::vector<Sp<Value>>& vals, bool expired){
+ if (expired)
+ onValuesExpired(vals);
+ else
+ onValuesAdded(vals);
+ return true;
+ }) {}
+
+ bool onValue(const std::vector<Sp<Value>>& vals, bool expired) {
+ cache.onValue(vals, expired);
+ return not listeners.empty();
+ }
+ void onNodeChanged(ListenSyncStatus status) {
+ cache.onNodeChanged(status);
+ }
+
+ void onValuesAdded(const std::vector<Sp<Value>>& vals);
+ void onValuesExpired(const std::vector<Sp<Value>>& vals);
+
+ bool addListener(size_t token, const ValueCallback& cb, const Sp<Query>& q, Value::Filter&& filter) {
+ auto cached = cache.get(filter);
+ if (not cached.empty() and not cb(cached, false)) {
+ return false;
+ }
+ listeners.emplace(token, LocalListener{q, std::move(filter), cb});
+ return true;
+ }
+
+ bool removeListener(size_t token, const time_point& now) {
+ lastRemoved = now;
+ return listeners.erase(token) > 0;
+ }
+
+ void removeAll() {
+ listeners.clear();
+ }
+
+ bool isDone() {
+ return listeners.empty();
+ }
+
+ std::vector<Sp<Value>> get(const Value::Filter& filter) const {
+ return cache.get(filter);
+ }
+
+ Sp<Value> get(Value::Id id) const {
+ return cache.get(id);
+ }
+
+ bool isSynced() const {
+ return cache.isSynced();
+ }
+
+ bool isExpired(const time_point& now) const {
+ return listeners.empty() and (lastRemoved + EXPIRATION < now);
+ }
+ time_point getExpiration() const;
+
+ size_t size() const {
+ return cache.size();
+ }
+
+ size_t searchToken {0};
+private:
+ constexpr static const std::chrono::seconds EXPIRATION {60};
+ OpCache(const OpCache&) = delete;
+ OpCache& operator=(const OpCache&) = delete;
+
+ OpValueCache cache;
+ std::map<size_t, LocalListener> listeners;
+ time_point lastRemoved {clock::now()};
+};
+
+class SearchCache {
+public:
+ SearchCache() {}
+ SearchCache(SearchCache&&) = default;
+
+ using OnListen = std::function<size_t(Sp<Query>, ValueCallback, SyncCallback)>;
+ size_t listen(const ValueCallback& get_cb, const Sp<Query>& q, Value::Filter&& filter, const OnListen& onListen);
+
+ bool cancelListen(size_t gtoken, const time_point& now);
+ void cancelAll(const std::function<void(size_t)>& onCancel);
+
+ time_point expire(const time_point& now, const std::function<void(size_t)>& onCancel);
+ time_point getExpiration() const {
+ return nextExpiration_;
+ }
+
+ bool get(const Value::Filter& f, const Sp<Query>& q, const GetCallback& gcb, const DoneCallback& dcb) const;
+ std::vector<Sp<Value>> get(const Value::Filter& filter) const;
+ Sp<Value> get(Value::Id id) const;
+
+ std::pair<size_t, size_t> size() const {
+ size_t tot = 0;
+ for (const auto& c : ops)
+ tot += c.second->size();
+ return {ops.size(), tot};
+ }
+
+private:
+ SearchCache(const SearchCache&) = delete;
+ SearchCache& operator=(const SearchCache&) = delete;
+
+ using OpMap = std::map<Sp<Query>, std::unique_ptr<OpCache>>;
+ OpMap ops {};
+ OpMap::iterator getOp(const Sp<Query>& q);
+ OpMap::const_iterator getOp(const Sp<Query>& q) const;
+
+ size_t nextToken_ {1};
+ time_point nextExpiration_ {time_point::max()};
+};
+
+}
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ * Author(s) : Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+ #pragma once
+
+#include "infohash.h"
+#include "sockaddr.h"
+#include "net.h"
+
+#include <map>
+#include <string_view>
+
+using namespace std::literals;
+
+namespace dht {
+namespace net {
+
+static constexpr auto KEY_Y = "y"sv;
+static constexpr auto KEY_R = "r"sv;
+static constexpr auto KEY_U = "u"sv;
+static constexpr auto KEY_E = "e"sv;
+static constexpr auto KEY_V = "p"sv;
+static constexpr auto KEY_TID = "t"sv;
+static constexpr auto KEY_UA = "v"sv;
+static constexpr auto KEY_NETID = "n"sv;
+static constexpr auto KEY_ISCLIENT = "s"sv;
+static constexpr auto KEY_Q = "q"sv;
+static constexpr auto KEY_A = "a"sv;
+
+static constexpr auto KEY_REQ_SID = "sid"sv;
+static constexpr auto KEY_REQ_ID = "id"sv;
+static constexpr auto KEY_REQ_H = "h"sv;
+static constexpr auto KEY_REQ_TARGET = "target"sv;
+static constexpr auto KEY_REQ_QUERY = "q"sv;
+static constexpr auto KEY_REQ_TOKEN = "token"sv;
+static constexpr auto KEY_REQ_VALUE_ID = "vid"sv;
+static constexpr auto KEY_REQ_NODES4 = "n4"sv;
+static constexpr auto KEY_REQ_NODES6 = "n6"sv;
+static constexpr auto KEY_REQ_CREATION = "c"sv;
+static constexpr auto KEY_REQ_ADDRESS = "sa"sv;
+static constexpr auto KEY_REQ_VALUES = "values"sv;
+static constexpr auto KEY_REQ_EXPIRED = "exp"sv;
+static constexpr auto KEY_REQ_REFRESHED = "re"sv;
+static constexpr auto KEY_REQ_FIELDS = "fileds"sv;
+static constexpr auto KEY_REQ_WANT = "w"sv;
+static constexpr auto KEY_VERSION = "ve"sv;
+
+static constexpr auto QUERY_PING = "ping"sv;
+static constexpr auto QUERY_FIND = "find"sv;
+static constexpr auto QUERY_GET = "get"sv;
+static constexpr auto QUERY_UPDATE = "update"sv;
+static constexpr auto QUERY_PUT = "put"sv;
+static constexpr auto QUERY_LISTEN = "listen"sv;
+static constexpr auto QUERY_REFRESH = "refresh"sv;
+
+Tid unpackTid(const msgpack::object& o) {
+ switch (o.type) {
+ case msgpack::type::POSITIVE_INTEGER:
+ return o.as<Tid>();
+ default:
+ return ntohl(*reinterpret_cast<const uint32_t*>(o.as<std::array<char, 4>>().data()));
+ }
+}
+
+struct ParsedMessage {
+ MessageType type;
+ /* Node ID of the sender */
+ InfoHash id;
+ /* Network id */
+ NetId network {0};
+ /** Is a client node */
+ bool is_client {false};
+ /* hash for which values are requested */
+ InfoHash info_hash;
+ /* target id around which to find nodes */
+ InfoHash target;
+ /* transaction id */
+ Tid tid {0};
+ /* tid for packets going through request socket */
+ Tid socket_id {0};
+ /* security token */
+ Blob token;
+ /* the value id (announce confirmation) */
+ Value::Id value_id {0};
+ /* time when value was first created */
+ time_point created { time_point::max() };
+ /* IPv4 nodes in response to a 'find' request */
+ Blob nodes4_raw, nodes6_raw;
+ std::vector<Sp<Node>> nodes4, nodes6;
+ /* values to store or retreive request */
+ std::vector<Sp<Value>> values;
+ std::vector<Value::Id> refreshed_values {};
+ std::vector<Value::Id> expired_values {};
+ /* index for fields values */
+ std::vector<Sp<FieldValueIndex>> fields;
+ /** When part of the message header: {index -> (total size, {})}
+ * When part of partial value data: {index -> (offset, part_data)} */
+ std::map<unsigned, std::pair<unsigned, Blob>> value_parts;
+ /* query describing a filter to apply on values. */
+ Query query;
+ /* states if ipv4 or ipv6 request */
+ want_t want;
+ /* error code in case of error */
+ uint16_t error_code;
+ /* reported address by the distant node */
+ std::string ua;
+ int version {0};
+ SockAddr addr;
+ void msgpack_unpack(const msgpack::object& o);
+
+ bool append(const ParsedMessage& block);
+ bool complete();
+};
+
+bool
+ParsedMessage::append(const ParsedMessage& block)
+{
+ bool ret(false);
+ for (const auto& ve : block.value_parts) {
+ auto part_val = value_parts.find(ve.first);
+ if (part_val == value_parts.end()
+ || part_val->second.second.size() >= part_val->second.first)
+ continue;
+ // TODO: handle out-of-order packets
+ if (ve.second.first != part_val->second.second.size()) {
+ //std::cout << "skipping out-of-order packet" << std::endl;
+ continue;
+ }
+ ret = true;
+ part_val->second.second.insert(part_val->second.second.end(),
+ ve.second.second.begin(),
+ ve.second.second.end());
+ }
+ return ret;
+}
+
+bool
+ParsedMessage::complete()
+{
+ for (auto& e : value_parts) {
+ if (e.second.first > e.second.second.size()) {
+ //std::cout << "uncomplete part " << e.first << ": " << e.second.second.size() << "/" << e.second.first << std::endl;
+ return false;
+ }
+ }
+ for (auto& e : value_parts) {
+ msgpack::unpacked msg;
+ msgpack::unpack(msg, (const char*)e.second.second.data(), e.second.second.size());
+ values.emplace_back(std::make_shared<Value>(msg.get()));
+ }
+ return true;
+}
+
+void
+ParsedMessage::msgpack_unpack(const msgpack::object& msg)
+{
+ if (msg.type != msgpack::type::MAP) throw msgpack::type_error();
+
+ struct ParsedMsg {
+ msgpack::object* y;
+ msgpack::object* r;
+ msgpack::object* u;
+ msgpack::object* e;
+ msgpack::object* v;
+ msgpack::object* a;
+ std::string_view q;
+ } parsed {};
+
+ for (unsigned i = 0; i < msg.via.map.size; i++) {
+ auto& o = msg.via.map.ptr[i];
+ if (o.key.type != msgpack::type::STR)
+ continue;
+ auto key = o.key.as<std::string_view>();
+ if (key == KEY_Y)
+ parsed.y = &o.val;
+ else if (key == KEY_R)
+ parsed.r = &o.val;
+ else if (key == KEY_U)
+ parsed.u = &o.val;
+ else if (key == KEY_E)
+ parsed.e = &o.val;
+ else if (key == KEY_V)
+ parsed.v = &o.val;
+ else if (key == KEY_TID)
+ tid = unpackTid(o.val);
+ else if (key == KEY_UA)
+ ua = o.val.as<std::string>();
+ else if (key == KEY_NETID)
+ network = o.val.as<NetId>();
+ else if (key == KEY_ISCLIENT)
+ is_client = o.val.as<bool>();
+ else if (key == KEY_Q)
+ parsed.q = o.val.as<std::string_view>();
+ else if (key == KEY_A)
+ parsed.a = &o.val;
+ }
+
+ if (parsed.e)
+ type = MessageType::Error;
+ else if (parsed.r)
+ type = MessageType::Reply;
+ else if (parsed.v)
+ type = MessageType::ValueData;
+ else if (parsed.u)
+ type = MessageType::ValueUpdate;
+ else if (parsed.y and parsed.y->as<std::string_view>() != "q"sv)
+ throw msgpack::type_error();
+ else if (parsed.q == QUERY_PING)
+ type = MessageType::Ping;
+ else if (parsed.q == QUERY_FIND)
+ type = MessageType::FindNode;
+ else if (parsed.q == QUERY_GET)
+ type = MessageType::GetValues;
+ else if (parsed.q == QUERY_LISTEN)
+ type = MessageType::Listen;
+ else if (parsed.q == QUERY_PUT)
+ type = MessageType::AnnounceValue;
+ else if (parsed.q == QUERY_REFRESH)
+ type = MessageType::Refresh;
+ else if (parsed.q == QUERY_UPDATE)
+ type = MessageType::UpdateValue;
+ else
+ throw msgpack::type_error();
+
+ if (type == MessageType::ValueData) {
+ if (parsed.v->type != msgpack::type::MAP)
+ throw msgpack::type_error();
+ for (size_t i = 0; i < parsed.v->via.map.size; ++i) {
+ auto& vdat = parsed.v->via.map.ptr[i];
+ auto o = findMapValue(vdat.val, "o"sv);
+ auto d = findMapValue(vdat.val, "d"sv);
+ if (not o or not d)
+ continue;
+ value_parts.emplace(vdat.key.as<unsigned>(), std::pair<size_t, Blob>(o->as<size_t>(), unpackBlob(*d)));
+ }
+ return;
+ }
+
+ if (!parsed.a && !parsed.r && !parsed.e && !parsed.u)
+ throw msgpack::type_error();
+ auto& req = parsed.a ? *parsed.a : (parsed.r ? *parsed.r : (parsed.u ? *parsed.u : *parsed.e));
+
+ if (parsed.e) {
+ if (parsed.e->type != msgpack::type::ARRAY)
+ throw msgpack::type_error();
+ error_code = parsed.e->via.array.ptr[0].as<uint16_t>();
+ }
+
+ struct ParsedReq {
+ msgpack::object* values;
+ msgpack::object* fields;
+ msgpack::object* sa;
+ msgpack::object* want;
+ } parsedReq {};
+
+ for (unsigned i = 0; i < req.via.map.size; i++) {
+ auto& o = req.via.map.ptr[i];
+ if (o.key.type != msgpack::type::STR)
+ continue;
+ auto key = o.key.as<std::string_view>();
+ if (key == KEY_REQ_SID)
+ socket_id = unpackTid(o.val);
+ else if (key == KEY_REQ_ID)
+ id = {o.val};
+ else if (key == KEY_REQ_H)
+ info_hash = {o.val};
+ else if (key == KEY_REQ_TARGET)
+ target = {o.val};
+ else if (key == KEY_REQ_QUERY)
+ query.msgpack_unpack(o.val);
+ else if (key == KEY_REQ_TOKEN)
+ token = unpackBlob(o.val);
+ else if (key == KEY_REQ_VALUE_ID)
+ value_id = o.val.as<Value::Id>();
+ else if (key == KEY_REQ_NODES4)
+ nodes4_raw = unpackBlob(o.val);
+ else if (key == KEY_REQ_NODES6)
+ nodes6_raw = unpackBlob(o.val);
+ else if (key == KEY_REQ_ADDRESS)
+ parsedReq.sa = &o.val;
+ else if (key == KEY_REQ_CREATION)
+ created = from_time_t(o.val.as<std::time_t>());
+ else if (key == KEY_REQ_VALUES)
+ parsedReq.values = &o.val;
+ else if (key == KEY_REQ_EXPIRED)
+ expired_values = o.val.as<decltype(expired_values)>();
+ else if (key == KEY_REQ_REFRESHED)
+ refreshed_values = o.val.as<decltype(refreshed_values)>();
+ else if (key == KEY_REQ_FIELDS)
+ parsedReq.fields = &o.val;
+ else if (key == KEY_REQ_WANT)
+ parsedReq.want = &o.val;
+ else if (key == KEY_VERSION)
+ version = o.val.as<int>();
+ }
+
+ if (parsedReq.sa) {
+ if (parsedReq.sa->type != msgpack::type::BIN)
+ throw msgpack::type_error();
+ auto l = parsedReq.sa->via.bin.size;
+ if (l == sizeof(in_addr)) {
+ addr.setFamily(AF_INET);
+ auto& a = addr.getIPv4();
+ a.sin_port = 0;
+ std::copy_n(parsedReq.sa->via.bin.ptr, l, (char*)&a.sin_addr);
+ } else if (l == sizeof(in6_addr)) {
+ addr.setFamily(AF_INET6);
+ auto& a = addr.getIPv6();
+ a.sin6_port = 0;
+ std::copy_n(parsedReq.sa->via.bin.ptr, l, (char*)&a.sin6_addr);
+ }
+ } else
+ addr = {};
+
+ if (parsedReq.values) {
+ if (parsedReq.values->type != msgpack::type::ARRAY)
+ throw msgpack::type_error();
+ for (size_t i = 0; i < parsedReq.values->via.array.size; i++) {
+ auto& packed_v = parsedReq.values->via.array.ptr[i];
+ if (packed_v.type == msgpack::type::POSITIVE_INTEGER) {
+ // Skip oversize values with a small margin for header overhead
+ if (packed_v.via.u64 > MAX_VALUE_SIZE + 32)
+ continue;
+ value_parts.emplace(i, std::make_pair(packed_v.via.u64, Blob{}));
+ } else {
+ try {
+ values.emplace_back(std::make_shared<Value>(parsedReq.values->via.array.ptr[i]));
+ } catch (const std::exception& e) {
+ //DHT_LOG_WARN("Error reading value: %s", e.what());
+ }
+ }
+ }
+ } else if (parsedReq.fields) {
+ if (auto rfields = findMapValue(*parsedReq.fields, "f"sv)) {
+ auto vfields = rfields->as<std::set<Value::Field>>();
+ if (auto rvalues = findMapValue(*parsedReq.fields, "v"sv)) {
+ if (rvalues->type != msgpack::type::ARRAY)
+ throw msgpack::type_error();
+ size_t val_num = rvalues->via.array.size / vfields.size();
+ for (size_t i = 0; i < val_num; ++i) {
+ try {
+ auto v = std::make_shared<FieldValueIndex>();
+ v->msgpack_unpack_fields(vfields, *rvalues, i*vfields.size());
+ fields.emplace_back(std::move(v));
+ } catch (const std::exception& e) { }
+ }
+ }
+ } else {
+ throw msgpack::type_error();
+ }
+ }
+
+ if (parsedReq.want) {
+ if (parsedReq.want->type != msgpack::type::ARRAY)
+ throw msgpack::type_error();
+ want = 0;
+ for (unsigned i=0; i<parsedReq.want->via.array.size; i++) {
+ auto& val = parsedReq.want->via.array.ptr[i];
+ try {
+ auto w = val.as<sa_family_t>();
+ if (w == AF_INET)
+ want |= WANT4;
+ else if(w == AF_INET6)
+ want |= WANT6;
+ } catch (const std::exception& e) {};
+ }
+ } else {
+ want = -1;
+ }
+}
+
+} /* namespace net */
+} /* namespace dht */
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ * Author(s) : Mingrui Zhang <mingrui.zhang@savoirfairelinux.com>
+ * Vsevolod Ivanov <vsevolod.ivanov@savoirfairelinux.com>
+ * Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "peer_discovery.h"
+#include "network_utils.h"
+#include "utils.h"
+
+#include <asio.hpp>
+
+using namespace std::literals;
+
+namespace dht {
+
+// Organization-local Scope multicast
+constexpr char MULTICAST_ADDRESS_IPV4[] = "239.192.0.1";
+constexpr char MULTICAST_ADDRESS_IPV6[] = "ff08::101";
+
+class PeerDiscovery::DomainPeerDiscovery
+{
+public:
+ DomainPeerDiscovery(asio::ip::udp domain, in_port_t port, Sp<asio::io_context> ioContext = {}, Sp<Logger> logger = {});
+ ~DomainPeerDiscovery();
+
+ void startDiscovery(const std::string &type, ServiceDiscoveredCallback callback);
+ void startPublish(const std::string &type, const msgpack::sbuffer &pack_buf);
+
+ void stop();
+
+ bool stopDiscovery(const std::string &type);
+ bool stopPublish(const std::string &type);
+
+ void connectivityChanged();
+
+private:
+ Sp<Logger> logger_;
+ //dmtx_ for callbackmap_ and drunning_ (write)
+ std::mutex dmtx_;
+ //mtx_ for messages_ and lrunning (listen)
+ std::mutex mtx_;
+ std::shared_ptr<asio::io_context> ioContext_;
+ asio::ip::udp::socket sockFd_;
+ asio::ip::udp::endpoint sockAddrSend_;
+
+ std::array<char, 64 * 1024> receiveBuf_;
+ asio::ip::udp::endpoint receiveFrom_;
+
+ msgpack::sbuffer sbuf_;
+ std::map<std::string, msgpack::sbuffer> messages_;
+ std::map<std::string, ServiceDiscoveredCallback, std::less<>> callbackmap_;
+ bool lrunning_ {false};
+ bool drunning_ {false};
+
+ void loopListener();
+ void query(const asio::ip::udp::endpoint& peer);
+ void reloadMessages();
+
+ void stopDiscovery();
+ void stopPublish();
+
+ void publish(const asio::ip::udp::endpoint& peer);
+
+ void reDiscover();
+};
+
+PeerDiscovery::DomainPeerDiscovery::DomainPeerDiscovery(asio::ip::udp domain, in_port_t port, Sp<asio::io_context> ioContext, Sp<Logger> logger)
+ : logger_(logger)
+ , ioContext_(ioContext)
+ , sockFd_(*ioContext_, domain)
+ , sockAddrSend_(asio::ip::address::from_string(domain.family() == AF_INET ? MULTICAST_ADDRESS_IPV4
+ : MULTICAST_ADDRESS_IPV6), port)
+{
+ try {
+ sockFd_.set_option(asio::ip::multicast::join_group(sockAddrSend_.address()));
+ sockFd_.set_option(asio::ip::udp::socket::reuse_address(true));
+ sockFd_.bind({domain, port});
+ } catch (const std::exception& e) {
+ if (logger_)
+ logger_->e("Can't start peer discovery for %s: %s",
+ domain.family() == AF_INET ? "IPv4" : "IPv6", e.what());
+ }
+}
+
+PeerDiscovery::DomainPeerDiscovery::~DomainPeerDiscovery()
+{
+ stop();
+ sockFd_.close();
+}
+
+void
+PeerDiscovery::DomainPeerDiscovery::startDiscovery(const std::string &type, ServiceDiscoveredCallback callback)
+{
+ std::lock_guard<std::mutex> lck(dmtx_);
+ callbackmap_[type] = callback;
+ if (not drunning_) {
+ drunning_ = true;
+ ioContext_->post([this] () {
+ loopListener();
+ query(sockAddrSend_);
+ });
+ }
+}
+
+void
+PeerDiscovery::DomainPeerDiscovery::loopListener()
+{
+ std::lock_guard<std::mutex> lck(dmtx_);
+ if (not drunning_)
+ return;
+ sockFd_.async_receive_from(asio::buffer(receiveBuf_), receiveFrom_, [this](const asio::error_code& error, size_t bytes) {
+ if (error == asio::error::operation_aborted)
+ return;
+ if (error) {
+ if (logger_)
+ logger_->e("Error receiving message: %s", error.message().c_str());
+ }
+ try {
+ auto rcv = msgpack::unpack(receiveBuf_.data(), bytes);
+ msgpack::object obj = rcv.get();
+
+ if (obj.type == msgpack::type::STR) {
+ if (lrunning_ and obj.as<std::string_view>() == "q"sv)
+ publish(receiveFrom_);
+ } else if (obj.type == msgpack::type::MAP) {
+ for (unsigned i = 0; i < obj.via.map.size; i++) {
+ auto& o = obj.via.map.ptr[i];
+ if (o.key.type != msgpack::type::STR)
+ continue;
+ ServiceDiscoveredCallback cb;
+ {
+ std::lock_guard<std::mutex> lck(dmtx_);
+ if (drunning_) {
+ auto callback = callbackmap_.find(o.key.as<std::string_view>());
+ if (callback != callbackmap_.end())
+ cb = callback->second;
+ } else
+ return;
+ }
+ if (cb)
+ cb(std::move(o.val), SockAddr{ receiveFrom_.data(), (socklen_t)receiveFrom_.size() });
+ }
+ } else {
+ throw msgpack::type_error{};
+ }
+ } catch (const std::exception& e) {
+ if (logger_)
+ logger_->e("Error receiving packet: %s", e.what());
+ }
+ loopListener();
+ });
+}
+
+void
+PeerDiscovery::DomainPeerDiscovery::query(const asio::ip::udp::endpoint& peer)
+{
+ std::lock_guard<std::mutex> lck(dmtx_);
+ if (not drunning_)
+ return;
+
+ msgpack::sbuffer pbuf_request;
+ msgpack::pack(pbuf_request, "q");
+
+ sockFd_.async_send_to(asio::buffer(pbuf_request.data(), pbuf_request.size()), peer,
+ [logger=logger_, peer] (const asio::error_code& ec, size_t)
+ {
+ if (ec and (ec != asio::error::operation_aborted) and logger)
+ logger->w("Error sending packet to: %s with err: %s",
+ peer.address().to_string().c_str(),
+ ec.message().c_str());
+ }
+ );
+}
+
+void
+PeerDiscovery::DomainPeerDiscovery::publish(const asio::ip::udp::endpoint& peer)
+{
+ std::lock_guard<std::mutex> lck(mtx_);
+ if (not lrunning_)
+ return;
+
+ sockFd_.async_send_to(asio::buffer((const void*)sbuf_.data(), sbuf_.size()), peer,
+ [logger=logger_, peer] (const asio::error_code& ec, size_t)
+ {
+ if (ec and (ec != asio::error::operation_aborted) and logger)
+ logger->w("Error sending packet to: %s with err: %s",
+ peer.address().to_string().c_str(),
+ ec.message().c_str());
+ }
+ );
+}
+
+
+void
+PeerDiscovery::DomainPeerDiscovery::startPublish(const std::string &type, const msgpack::sbuffer &pack_buf)
+{
+ msgpack::sbuffer pack_buf_c(pack_buf.size());
+ pack_buf_c.write(pack_buf.data(), pack_buf.size());
+
+ std::lock_guard<std::mutex> lck(mtx_);
+ messages_[type] = std::move(pack_buf_c);
+ reloadMessages();
+ lrunning_ = true;
+ ioContext_->post([this] () { publish(sockAddrSend_); });
+}
+
+bool
+PeerDiscovery::DomainPeerDiscovery::stopDiscovery(const std::string& type)
+{
+ std::lock_guard<std::mutex> lck(dmtx_);
+ if (callbackmap_.erase(type) > 0) {
+ if (callbackmap_.empty())
+ stopDiscovery();
+ return true;
+ }
+ return false;
+}
+
+bool
+PeerDiscovery::DomainPeerDiscovery::stopPublish(const std::string& type)
+{
+ std::lock_guard<std::mutex> lck(mtx_);
+ if (messages_.erase(type) > 0) {
+ if (messages_.empty())
+ stopPublish();
+ else
+ reloadMessages();
+ return true;
+ }
+ return false;
+}
+
+void
+PeerDiscovery::DomainPeerDiscovery::stopDiscovery()
+{
+ drunning_ = false;
+}
+
+void
+PeerDiscovery::DomainPeerDiscovery::stopPublish()
+{
+ lrunning_ = false;
+}
+
+void
+PeerDiscovery::DomainPeerDiscovery::stop()
+{
+ {
+ std::lock_guard<std::mutex> lck(dmtx_);
+ stopDiscovery();
+ }
+ {
+ std::lock_guard<std::mutex> lck(mtx_);
+ stopPublish();
+ }
+}
+
+void
+PeerDiscovery::DomainPeerDiscovery::reloadMessages()
+{
+ sbuf_.clear();
+ msgpack::packer<msgpack::sbuffer> pk(&sbuf_);
+ pk.pack_map(messages_.size());
+ for (const auto& m : messages_) {
+ pk.pack(m.first);
+ sbuf_.write(m.second.data(), m.second.size());
+ }
+}
+
+void
+PeerDiscovery::DomainPeerDiscovery::reDiscover()
+{
+ asio::error_code ec;
+
+ sockFd_.set_option(asio::ip::multicast::join_group(sockAddrSend_.address()), ec);
+ if (ec and logger_)
+ logger_->w("can't multicast on %s: %s",
+ sockAddrSend_.address().to_string().c_str(),
+ ec.message().c_str());
+ query(sockAddrSend_);
+}
+
+void
+PeerDiscovery::DomainPeerDiscovery::connectivityChanged()
+{
+ reDiscover();
+ publish(sockAddrSend_);
+}
+
+PeerDiscovery::PeerDiscovery(in_port_t port, Sp<asio::io_context> ioContext, Sp<Logger> logger)
+{
+ if (not ioContext) {
+ ioContext = std::make_shared<asio::io_context>();
+ ioContext_ = ioContext;
+ ioRunnner_ = std::thread([logger, ioContext] {
+ try {
+ if (logger)
+ logger->d("[peerdiscovery] starting io_context");
+ auto work = asio::make_work_guard(*ioContext);
+ ioContext->run();
+ if (logger)
+ logger->d("[peerdiscovery] io_context stopped");
+ }
+ catch (const std::exception& ex){
+ if (logger)
+ logger->e("[peerdiscovery] run error: %s", ex.what());
+ }
+ });
+ }
+
+ try {
+ peerDiscovery4_.reset(new DomainPeerDiscovery(asio::ip::udp::v4(), port, ioContext, logger));
+ } catch(const std::exception& e){
+ if (logger)
+ logger->e("[peerdiscovery] can't start IPv4: %s", e.what());
+ }
+ try {
+ peerDiscovery6_.reset(new DomainPeerDiscovery(asio::ip::udp::v6(), port, ioContext, logger));
+ } catch(const std::exception& e) {
+ if (logger)
+ logger->e("[peerdiscovery] can't start IPv6: %s", e.what());
+ }
+}
+
+PeerDiscovery::~PeerDiscovery() {
+ stop();
+ if (ioContext_)
+ ioContext_->stop();
+ if (ioRunnner_.joinable())
+ ioRunnner_.join();
+}
+
+void
+PeerDiscovery::startDiscovery(const std::string& type, ServiceDiscoveredCallback callback)
+{
+ if (peerDiscovery4_) peerDiscovery4_->startDiscovery(type, callback);
+ if (peerDiscovery6_) peerDiscovery6_->startDiscovery(type, callback);
+}
+
+void
+PeerDiscovery::startPublish(const std::string& type, const msgpack::sbuffer& pack_buf)
+{
+ if (peerDiscovery4_) peerDiscovery4_->startPublish(type, pack_buf);
+ if (peerDiscovery6_) peerDiscovery6_->startPublish(type, pack_buf);
+}
+
+void
+PeerDiscovery::startPublish(sa_family_t domain, const std::string& type, const msgpack::sbuffer& pack_buf)
+{
+ if (domain == AF_INET) {
+ if (peerDiscovery4_) peerDiscovery4_->startPublish(type, pack_buf);
+ } else if (domain == AF_INET6) {
+ if (peerDiscovery6_) peerDiscovery6_->startPublish(type, pack_buf);
+ }
+}
+
+void
+PeerDiscovery::stop()
+{
+ if (peerDiscovery4_) peerDiscovery4_->stop();
+ if (peerDiscovery6_) peerDiscovery6_->stop();
+}
+
+bool
+PeerDiscovery::stopDiscovery(const std::string &type)
+{
+ bool stopped4 = peerDiscovery4_ and peerDiscovery4_->stopDiscovery(type);
+ bool stopped6 = peerDiscovery6_ and peerDiscovery6_->stopDiscovery(type);
+ return stopped4 or stopped6;
+}
+
+bool
+PeerDiscovery::stopPublish(const std::string &type)
+{
+ bool stopped4 = peerDiscovery4_ and peerDiscovery4_->stopPublish(type);
+ bool stopped6 = peerDiscovery6_ and peerDiscovery6_->stopPublish(type);
+ return stopped4 or stopped6;
+}
+
+bool
+PeerDiscovery::stopPublish(sa_family_t domain, const std::string& type)
+{
+ if (domain == AF_INET) {
+ return peerDiscovery4_ and peerDiscovery4_->stopPublish(type);
+ } else if (domain == AF_INET6) {
+ return peerDiscovery6_ and peerDiscovery6_->stopPublish(type);
+ }
+ return false;
+}
+
+void
+PeerDiscovery::connectivityChanged()
+{
+ if (peerDiscovery4_)
+ peerDiscovery4_->connectivityChanged();
+ if (peerDiscovery6_)
+ peerDiscovery6_->connectivityChanged();
+}
+
+} /* namespace dht */
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ * Author(s) : Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ * Simon Désaulniers <simon.desaulniers@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "net.h"
+#include "value.h"
+
+namespace dht {
+struct Node;
+namespace net {
+
+class NetworkEngine;
+class DhtProtocolException;
+struct ParsedMessage;
+
+/*!
+ * @class Request
+ * @brief An atomic request destined to a node.
+ * @details
+ * A request contains data used by the NetworkEngine to process a request
+ * desitned to specific node and std::function callbacks to execute when the
+ * request is done.
+ */
+struct Request {
+ friend class dht::net::NetworkEngine;
+
+ Sp<Node> node {}; /* the node to whom the request is destined. */
+ time_point reply_time {time_point::min()}; /* time when we received the response to the request. */
+
+ enum class State
+ {
+ PENDING,
+ CANCELLED,
+ EXPIRED,
+ COMPLETED
+ };
+
+ bool expired() const { return state_ == State::EXPIRED; }
+ bool completed() const { return state_ == State::COMPLETED; }
+ bool cancelled() const { return state_ == State::CANCELLED; }
+ bool pending() const { return state_ == State::PENDING; }
+ bool over() const { return not pending(); }
+ State getState() const { return state_; }
+ char getStateChar() const {
+ switch (state_) {
+ case State::PENDING: return 'f';
+ case State::CANCELLED: return 'c';
+ case State::EXPIRED: return 'e';
+ case State::COMPLETED: return 'a';
+ default: return '?';
+ }
+ }
+
+ Request(State state = State::PENDING) : state_(state) {}
+ Request(MessageType type, Tid tid,
+ Sp<Node> node,
+ Blob&& msg,
+ std::function<void(const Request&, ParsedMessage&&)> on_done,
+ std::function<void(const Request&, bool)> on_expired) :
+ node(std::move(node)), tid(tid), type(type), on_done(std::move(on_done)), on_expired(std::move(on_expired)), msg(std::move(msg)) { }
+ Request(MessageType type, Tid tid,
+ Sp<Node> node,
+ Blob&& msg,
+ std::function<void(const Request&, ParsedMessage&&)> on_done,
+ std::function<bool(const Request&, DhtProtocolException&&)> on_error,
+ std::function<void(const Request&, bool)> on_expired) :
+ node(std::move(node)), tid(tid), type(type), on_done(std::move(on_done)), on_error(std::move(on_error)), on_expired(std::move(on_expired)), msg(std::move(msg)) { }
+
+ Tid getTid() const { return tid; }
+ MessageType getType() const { return type; }
+
+ void setExpired() {
+ if (pending()) {
+ state_ = Request::State::EXPIRED;
+ on_expired(*this, true);
+ clear();
+ }
+ }
+ void setDone(ParsedMessage&& msg) {
+ if (pending()) {
+ state_ = Request::State::COMPLETED;
+ on_done(*this, std::forward<ParsedMessage>(msg));
+ clear();
+ }
+ }
+ bool setError(DhtProtocolException&& e) {
+ if (pending()) {
+ state_ = Request::State::EXPIRED;
+ bool handled = on_error and on_error(*this, std::forward<DhtProtocolException>(e));
+ clear();
+ return handled;
+ }
+ return true;
+ }
+
+ void cancel() {
+ if (pending()) {
+ state_ = State::CANCELLED;
+ clear();
+ }
+ }
+
+private:
+ static const constexpr size_t MAX_ATTEMPT_COUNT {3};
+
+ bool isExpired(time_point now) const {
+ return pending() and now > last_try + attempt_duration and attempt_count >= Request::MAX_ATTEMPT_COUNT;
+ }
+
+ void clear() {
+ on_done = {};
+ on_error = {};
+ on_expired = {};
+ msg = {};
+ parts = {};
+ }
+
+ const Tid tid {0}; /* the request id. */
+ const MessageType type {};
+ State state_ {State::PENDING};
+
+ unsigned attempt_count {0}; /* number of attempt to process the request. */
+ duration attempt_duration {((duration)Node::MAX_RESPONSE_TIME)/2};
+ time_point start {time_point::min()}; /* time when the request is created. */
+ time_point last_try {time_point::min()}; /* time of the last attempt to process the request. */
+
+ std::function<void(const Request&, ParsedMessage&&)> on_done {};
+ std::function<bool(const Request&, DhtProtocolException&&)> on_error {};
+ std::function<void(const Request&, bool)> on_expired {};
+
+ Blob msg {}; /* the serialized message. */
+ std::vector<Blob> parts;
+};
+
+} /* namespace net */
+}
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ * Author : Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "rng.h"
+
+#include <chrono>
+#include <cstring>
+
+namespace dht {
+namespace crypto {
+
+random_device::random_device() :
+ gen(std::chrono::system_clock::now().time_since_epoch().count() ^ std::chrono::high_resolution_clock::now().count())
+{}
+
+random_device::result_type
+random_device::operator()()
+{
+ result_type prand = dis(gen);
+ result_type hwrand;
+ if (hasRdseed() and rdseed(&hwrand))
+ prand ^= hwrand;
+ else if (hasRdrand() and rdrand(&hwrand))
+ prand ^= hwrand;
+ return prand;
+}
+
+random_device::CPUIDinfo::CPUIDinfo(const unsigned int func, const unsigned int subfunc)
+{
+ __asm__ __volatile__ (
+ "cpuid"
+ : "=a"(EAX), "=b"(EBX), "=c"(ECX), "=d"(EDX)
+ : "a"(func), "c"(subfunc)
+ );
+}
+
+bool
+random_device::hasIntelCpu()
+{
+ CPUIDinfo info (0, 0);
+ return (memcmp((char *) (&info.EBX), "Genu", 4) == 0
+ && memcmp((char *) (&info.EDX), "ineI", 4) == 0
+ && memcmp((char *) (&info.ECX), "ntel", 4) == 0);
+}
+
+bool
+random_device::_hasRdrand()
+{
+ return hasIntelCpu() && (CPUIDinfo {1, 0}.ECX & (1 << 30));
+}
+
+bool
+random_device::_hasRdseed()
+{
+ return hasIntelCpu() && (CPUIDinfo {7, 0}.ECX & (1 << 18));
+}
+
+bool
+random_device::rdrandStep(result_type* r)
+{
+ unsigned char ok;
+ asm volatile ("rdrand %0; setc %1"
+ : "=r" (*r), "=qm" (ok));
+ return ok;
+}
+
+bool
+random_device::rdrand(result_type* r)
+{
+ result_type res;
+ unsigned retries = 8;
+ while (retries--)
+ if (rdrandStep(&res)) {
+ *r = res;
+ return true;
+ }
+ return false;
+}
+
+bool
+random_device::rdseedStep(result_type* r)
+{
+ unsigned char ok;
+ asm volatile ("rdseed %0; setc %1"
+ : "=r" (*r), "=qm" (ok));
+ return ok;
+}
+
+bool
+random_device::rdseed(result_type* r)
+{
+ result_type res;
+ unsigned retries = 256;
+ while (retries--)
+ if (rdseedStep(&res)) {
+ *r = res;
+ return true;
+ }
+ return false;
+}
+
+}}
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ * Author(s) : Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "routing_table.h"
+
+#include "network_engine.h"
+#include "rng.h"
+
+#include <memory>
+
+namespace dht {
+
+Sp<Node>
+Bucket::randomNode(std::mt19937_64& rd)
+{
+ if (nodes.empty())
+ return nullptr;
+ unsigned expired_node_count = std::count_if(nodes.cbegin(), nodes.cend(), [](const decltype(nodes)::value_type& node) {
+ return node->isExpired();
+ });
+ auto prioritize_not_expired = expired_node_count < nodes.size();
+
+ std::uniform_int_distribution<unsigned> rand_node(0, prioritize_not_expired
+ ? nodes.size() - expired_node_count - 1
+ : nodes.size()-1);
+ unsigned nn = rand_node(rd);
+ for (auto& n : nodes) {
+ if (not (prioritize_not_expired and n->isExpired())) {
+ if (not nn--)
+ return n;
+ }
+ }
+ return nodes.back();
+}
+
+void Bucket::sendCachedPing(net::NetworkEngine& ne)
+{
+ if (not cached)
+ return;
+ //DHT_LOG.d(b.cached->id, "[node %s] sending ping to cached node", cached->toString().c_str());
+ ne.sendPing(cached, nullptr, nullptr);
+ cached = {};
+}
+
+InfoHash
+RoutingTable::randomId(const RoutingTable::const_iterator& it, std::mt19937_64& rd) const
+{
+ int bit1 = it->first.lowbit();
+ int bit2 = std::next(it) != end() ? std::next(it)->first.lowbit() : -1;
+ int bit = std::max(bit1, bit2) + 1;
+
+ if (bit >= 8*(int)HASH_LEN)
+ return it->first;
+
+#ifdef _WIN32
+ std::uniform_int_distribution<int> rand_byte{ 0, std::numeric_limits<uint8_t>::max() };
+#else
+ std::uniform_int_distribution<uint8_t> rand_byte;
+#endif
+
+ int b = bit/8;
+ InfoHash id_return;
+ std::copy_n(it->first.cbegin(), b, id_return.begin());
+ id_return[b] = it->first[b] & (0xFF00 >> (bit % 8));
+ id_return[b] |= rand_byte(rd) >> (bit % 8);
+ for (unsigned i = b + 1; i < HASH_LEN; i++)
+ id_return[i] = rand_byte(rd);
+ return id_return;
+}
+
+InfoHash
+RoutingTable::middle(const RoutingTable::const_iterator& it) const
+{
+ unsigned bit = depth(it);
+ if (bit >= 8*HASH_LEN)
+ throw std::out_of_range("End of table");
+
+ InfoHash id = it->first;
+ id.setBit(bit, true);
+ return id;
+}
+
+unsigned
+RoutingTable::depth(const RoutingTable::const_iterator& it) const
+{
+ if (it == end())
+ return 0;
+ int bit1 = it->first.lowbit();
+ int bit2 = std::next(it) != end() ? std::next(it)->first.lowbit() : -1;
+ return std::max(bit1, bit2)+1;
+}
+
+std::vector<Sp<Node>>
+RoutingTable::findClosestNodes(const InfoHash id, time_point now, size_t count) const
+{
+ std::vector<Sp<Node>> nodes;
+ nodes.reserve(count);
+ auto bucket = findBucket(id);
+
+ if (bucket == end()) { return nodes; }
+
+ auto sortedBucketInsert = [&](const Bucket &b) {
+ for (auto n : b.nodes) {
+ if (not n->isGood(now))
+ continue;
+
+ auto here = std::find_if(nodes.begin(), nodes.end(),
+ [&id,&n](Sp<Node> &node) {
+ return id.xorCmp(n->id, node->id) < 0;
+ }
+ );
+ nodes.insert(here, n);
+ }
+ };
+
+ auto itn = bucket;
+ auto itp = (bucket == begin()) ? end() : std::prev(bucket);
+ while (nodes.size() < count && (itn != end() || itp != end())) {
+ if (itn != end()) {
+ sortedBucketInsert(*itn);
+ itn = std::next(itn);
+ }
+ if (itp != end()) {
+ sortedBucketInsert(*itp);
+ itp = (itp == begin()) ? end() : std::prev(itp);
+ }
+ }
+
+ // shrink to the count closest nodes.
+ if (nodes.size() > count) {
+ nodes.resize(count);
+ }
+ return nodes;
+}
+
+RoutingTable::iterator
+RoutingTable::findBucket(const InfoHash& id)
+{
+ if (empty())
+ return end();
+ auto b = begin();
+ while (true) {
+ auto next = std::next(b);
+ if (next == end())
+ return b;
+ if (InfoHash::cmp(id, next->first) < 0)
+ return b;
+ b = next;
+ }
+}
+
+RoutingTable::const_iterator
+RoutingTable::findBucket(const InfoHash& id) const
+{
+ /* Avoid code duplication for the const version */
+ const_iterator it = const_cast<RoutingTable*>(this)->findBucket(id);
+ return it;
+}
+
+/* Split a bucket into two equal parts. */
+bool
+RoutingTable::split(const RoutingTable::iterator& b)
+{
+ InfoHash new_id;
+ try {
+ new_id = middle(b);
+ } catch (const std::out_of_range& e) {
+ return false;
+ }
+
+ // Insert new bucket
+ insert(std::next(b), Bucket {b->af, new_id, b->time});
+
+ // Re-assign nodes
+ std::list<Sp<Node>> nodes {};
+ nodes.splice(nodes.begin(), b->nodes);
+ while (!nodes.empty()) {
+ auto n = nodes.begin();
+ auto b = findBucket((*n)->id);
+ if (b == end())
+ nodes.erase(n);
+ else
+ b->nodes.splice(b->nodes.begin(), nodes, n);
+ }
+ return true;
+}
+
+bool
+RoutingTable::onNewNode(const Sp<Node>& node, int confirm, const time_point& now, const InfoHash& myid, net::NetworkEngine& ne) {
+ auto b = findBucket(node->id);
+ if (b == end()) return false;
+
+ if (confirm == 2)
+ b->time = now;
+
+ for (auto& n : b->nodes) {
+ if (n == node)
+ return false;
+ }
+
+ bool mybucket = contains(b, myid);
+ if (mybucket) {
+ grow_time = now;
+ //scheduler.edit(nextNodesConfirmation, now);
+ }
+
+ if (b->nodes.size() >= TARGET_NODES) {
+ /* Try to get rid of an expired node. */
+ for (auto& n : b->nodes)
+ if (n->isExpired()) {
+ n = node;
+ return true;
+ }
+ /* Bucket full. Ping a dubious node */
+ bool dubious = false;
+ for (auto& n : b->nodes) {
+ /* Pick the first dubious node that we haven't pinged in the
+ last 9 seconds. This gives nodes the time to reply, but
+ tends to concentrate on the same nodes, so that we get rid
+ of bad nodes fast. */
+ if (not n->isGood(now)) {
+ dubious = true;
+ if (not n->isPendingMessage()) {
+ //DHT_LOG.d(n->id, "[node %s] sending ping to dubious node", n->toString().c_str());
+ ne.sendPing(n, nullptr, nullptr);
+ break;
+ }
+ }
+ }
+
+ if ((mybucket || (is_client and depth(b) < 6)) && (!dubious || size() == 1)) {
+ //DHT_LOG.d("Splitting from depth %u", depth(b));
+ b->sendCachedPing(ne);
+ split(b);
+ return onNewNode(node, confirm, now, myid, ne);
+ }
+
+ /* No space for this node. Cache it away for later. */
+ if (confirm or not b->cached)
+ b->cached = node;
+ } else {
+ /* Create a new node. */
+ b->nodes.emplace_front(node);
+ }
+ return true;
+}
+
+
+
+}
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ * Author(s) : Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "value.h"
+#include "request.h"
+#include "listener.h"
+#include "value_cache.h"
+#include "op_cache.h"
+
+namespace dht {
+
+/**
+ * A single "get" operation data
+ */
+struct Dht::Get {
+ time_point start;
+ Value::Filter filter;
+ Sp<Query> query;
+ QueryCallback query_cb;
+ GetCallback get_cb;
+ DoneCallback done_cb;
+};
+
+/**
+ * A single "put" operation data
+ */
+struct Dht::Announce {
+ bool permanent;
+ Sp<Value> value;
+ time_point created;
+ DoneCallback callback;
+};
+
+struct Dht::SearchNode {
+
+ struct AnnounceStatus {
+ Sp<net::Request> req {};
+ Sp<Scheduler::Job> refresh {};
+ time_point refresh_time;
+ AnnounceStatus(){};
+ AnnounceStatus(Sp<net::Request> r, time_point t): req(std::move(r)), refresh_time(t)
+ {}
+ };
+ /**
+ * Foreach value id, we keep track of a pair (net::Request, time_point) where the
+ * request is the request returned by the network engine and the time_point
+ * is the next time at which the value must be refreshed.
+ */
+ using AnnounceStatusMap = std::map<Value::Id, AnnounceStatus>;
+ /**
+ * Foreach Query, we keep track of the request returned by the network
+ * engine when we sent the "get".
+ */
+ using SyncStatus = std::map<Sp<Query>, Sp<net::Request>>;
+
+ struct CachedListenStatus {
+ ValueCache cache;
+ Sp<Scheduler::Job> refresh {};
+ Sp<Scheduler::Job> cacheExpirationJob {};
+ Sp<net::Request> req {};
+ Tid socketId {0};
+ CachedListenStatus(ValueStateCallback&& cb, SyncCallback scb, Tid sid)
+ : cache(std::forward<ValueStateCallback>(cb), std::forward<SyncCallback>(scb)), socketId(sid) {}
+ CachedListenStatus(CachedListenStatus&&) = delete;
+ CachedListenStatus(const CachedListenStatus&) = delete;
+ ~CachedListenStatus() {
+ if (socketId and req and req->node) {
+ req->node->closeSocket(socketId);
+ }
+ }
+ };
+ using NodeListenerStatus = std::map<Sp<Query>, CachedListenStatus>;
+
+ Sp<Node> node {}; /* the node info */
+
+ /* queries sent for finding out values hosted by the node */
+ Sp<Query> probe_query {};
+ /* queries substituting formal 'get' requests */
+ std::map<Sp<Query>, std::vector<Sp<Query>>> pagination_queries {};
+
+ SyncStatus getStatus {}; /* get/sync status */
+ NodeListenerStatus listenStatus {}; /* listen status */
+ AnnounceStatusMap acked {}; /* announcement status for a given value id */
+
+ Blob token {}; /* last token the node sent to us after a get request */
+ time_point last_get_reply {time_point::min()}; /* last time received valid token */
+ Sp<Scheduler::Job> syncJob {};
+ bool candidate {false}; /* A search node is candidate if the search is/was synced and this
+ node is a new candidate for inclusion. */
+
+ SearchNode() : node() {}
+ SearchNode(const SearchNode&) = delete;
+ SearchNode(SearchNode&&) = delete;
+ SearchNode& operator=(const SearchNode&) = delete;
+ SearchNode& operator=(SearchNode&&) = delete;
+
+ SearchNode(const Sp<Node>& node) : node(node) {}
+ ~SearchNode() {
+ if (node) {
+ cancelGet();
+ cancelListen();
+ cancelAnnounce();
+ }
+ }
+
+ /**
+ * Can we use this node to listen/announce now ?
+ */
+ bool isSynced(const time_point& now) const {
+ return not node->isExpired() and
+ not token.empty() and last_get_reply >= now - Node::NODE_EXPIRE_TIME;
+ }
+
+ time_point getSyncTime(const time_point& now) const {
+ if (node->isExpired() or token.empty())
+ return now;
+ return last_get_reply + Node::NODE_EXPIRE_TIME;
+ }
+
+ friend std::ostream& operator<< (std::ostream& s, const SearchNode& node) {
+ s << "getStatus:" << node.getStatus.size()
+ << " listenStatus:" << node.listenStatus.size()
+ << " acked:" << node.acked.size()
+ << " cache:" << (node.listenStatus.empty() ? 0 : node.listenStatus.begin()->second.cache.size()) << std::endl;
+ return s;
+ }
+
+ /**
+ * Could a particular "get" request be sent to this node now ?
+ *
+ * A 'get' request can be sent when all of the following requirements are
+ * met:
+ *
+ * - The node is not expired;
+ * - The pagination process for this particular 'get' must not have begun;
+ * - There hasn't been any response for a request, satisfying the initial
+ * request, anytime following the initial request.
+ * - No other request satisfying the request must be pending;
+ *
+ * @param now The time reference to now.
+ * @param update The time of the last 'get' op satisfying this request.
+ * @param q The query defining the "get" operation we're referring to.
+ *
+ * @return true if we can send get, else false.
+ */
+ bool canGet(time_point now, time_point update, const Sp<Query>& q) const {
+ if (node->isExpired())
+ return false;
+
+ bool is_pending {false},
+ completed_sq_status {false},
+ pending_sq_status {false};
+ for (const auto& s : getStatus) {
+ if (s.second and s.second->pending())
+ is_pending = true;
+ if (s.first and q and q->isSatisfiedBy(*s.first) and s.second) {
+ if (s.second->pending())
+ pending_sq_status = true;
+ else if (s.second->completed() and not (update > s.second->reply_time))
+ completed_sq_status = true;
+ if (completed_sq_status and pending_sq_status)
+ break;
+ }
+ }
+
+ return (not is_pending and now > last_get_reply + Node::NODE_EXPIRE_TIME) or
+ not (completed_sq_status or pending_sq_status or hasStartedPagination(q));
+ }
+
+ /**
+ * Tells if we have started sending a 'get' request in paginated form.
+ *
+ * @param q The query as an id for a given 'get' request.
+ *
+ * @return true if pagination process has started, else false.
+ */
+ bool hasStartedPagination(const Sp<Query>& q) const {
+ const auto& pqs = pagination_queries.find(q);
+ if (pqs == pagination_queries.cend() or pqs->second.empty())
+ return false;
+ return std::find_if(pqs->second.cbegin(), pqs->second.cend(),
+ [this](const Sp<Query>& query) {
+ const auto& req = getStatus.find(query);
+ return req != getStatus.cend() and req->second;
+ }) != pqs->second.cend();
+ };
+
+
+ /**
+ * Tell if the node has finished responding to a given 'get' request.
+ *
+ * A 'get' request can be divided in multiple requests called "pagination
+ * requests". If this is the case, we have to check if they're all finished.
+ * Otherwise, we only check for the single request.
+ *
+ * @param get The 'get' request data structure;
+ *
+ * @return true if it has finished, else false.
+ */
+ bool isDone(const Get& get) const {
+ if (hasStartedPagination(get.query)) {
+ const auto& pqs = pagination_queries.find(get.query);
+ auto paginationPending = std::find_if(pqs->second.cbegin(), pqs->second.cend(),
+ [this](const Sp<Query>& query) {
+ const auto& req = getStatus.find(query);
+ return req != getStatus.cend() and req->second and req->second->pending();
+ }) != pqs->second.cend();
+ return not paginationPending;
+ } else { /* no pagination yet */
+ const auto& gs = get.query ? getStatus.find(get.query) : getStatus.cend();
+ return gs != getStatus.end() and gs->second and not gs->second->pending();
+ }
+ }
+
+ void cancelGet() {
+ for (const auto& status : getStatus) {
+ if (status.second->pending()) {
+ node->cancelRequest(status.second);
+ }
+ }
+ getStatus.clear();
+ }
+
+ void onValues(const Sp<Query>& q, net::RequestAnswer&& answer, const TypeStore& types, Scheduler& scheduler)
+ {
+ auto l = listenStatus.find(q);
+ if (l != listenStatus.end()) {
+ auto next = l->second.cache.onValues(answer.values,
+ answer.refreshed_values,
+ answer.expired_values, types, scheduler.time());
+ scheduler.edit(l->second.cacheExpirationJob, next);
+ }
+ }
+
+ void onListenSynced(const Sp<Query>& q, bool synced = true, Sp<Scheduler::Job> refreshJob = {}) {
+ auto l = listenStatus.find(q);
+ if (l != listenStatus.end()) {
+ if (l->second.refresh)
+ l->second.refresh->cancel();
+ l->second.refresh = std::move(refreshJob);
+ l->second.cache.onSynced(synced);
+ }
+ }
+
+ void expireValues(const Sp<Query>& q, Scheduler& scheduler) {
+ auto l = listenStatus.find(q);
+ if (l != listenStatus.end()) {
+ auto next = l->second.cache.expireValues(scheduler.time());
+ scheduler.edit(l->second.cacheExpirationJob, next);
+ }
+ }
+
+ /**
+ * Tells if a request in the status map is expired.
+ *
+ * @param status A SyncStatus reference.
+ *
+ * @return true if there exists an expired request, else false.
+ */
+ /*static bool expired(const SyncStatus& status) const {
+ return std::find_if(status.begin(), status.end(),
+ [](const SyncStatus::value_type& r){
+ return r.second and r.second->expired();
+ }) != status.end();
+ }*/
+
+ /**
+ * Tells if a request in the status map is pending.
+ *
+ * @param status A SyncStatus reference.
+ *
+ * @return true if there exists an expired request, else false.
+ */
+ static bool pending(const SyncStatus& status) {
+ return std::find_if(status.cbegin(), status.cend(),
+ [](const SyncStatus::value_type& r){
+ return r.second and r.second->pending();
+ }) != status.cend();
+ }
+ static bool pending(const NodeListenerStatus& status) {
+ return std::find_if(status.begin(), status.end(),
+ [](const NodeListenerStatus::value_type& r){
+ return r.second.req and r.second.req->pending();
+ }) != status.end();
+ }
+
+ bool pendingGet() const { return pending(getStatus); }
+
+ bool isAnnounced(Value::Id vid) const {
+ auto ack = acked.find(vid);
+ if (ack == acked.end() or not ack->second.req)
+ return false;
+ return ack->second.req->completed();
+ }
+ void cancelAnnounce() {
+ for (const auto& status : acked) {
+ const auto& req = status.second.req;
+ if (req and req->pending()) {
+ node->cancelRequest(req);
+ }
+ if (status.second.refresh)
+ status.second.refresh->cancel();
+ }
+ acked.clear();
+ }
+
+ inline bool isListening(time_point now, duration listen_expire) const {
+ auto ls = listenStatus.begin();
+ for ( ; ls != listenStatus.end() ; ++ls) {
+ if (isListening(now, ls, listen_expire)) {
+ break;
+ }
+ }
+ return ls != listenStatus.end();
+ }
+ inline bool isListening(time_point now, const Sp<Query>& q, duration listen_expire) const {
+ const auto& ls = listenStatus.find(q);
+ if (ls == listenStatus.end())
+ return false;
+ else
+ return isListening(now, ls, listen_expire);
+ }
+ inline bool isListening(time_point now, NodeListenerStatus::const_iterator listen_status, duration listen_expire) const {
+ if (listen_status == listenStatus.end() or not listen_status->second.req)
+ return false;
+ return listen_status->second.req->reply_time + listen_expire > now;
+ }
+ void cancelListen() {
+ for (const auto& status : listenStatus) {
+ node->cancelRequest(status.second.req);
+ if (status.second.refresh)
+ status.second.refresh->cancel();
+ if (status.second.cacheExpirationJob)
+ status.second.cacheExpirationJob->cancel();
+ }
+ listenStatus.clear();
+ }
+ void cancelListen(const Sp<Query>& query) {
+ auto it = listenStatus.find(query);
+ if (it != listenStatus.end()) {
+ node->cancelRequest(it->second.req);
+ if (it->second.refresh)
+ it->second.refresh->cancel();
+ listenStatus.erase(it);
+ }
+ }
+
+ /**
+ * Assuming the node is synced, should a "put" request be sent to this node now ?
+ */
+ time_point getAnnounceTime(Value::Id vid) const {
+ const auto& ack = acked.find(vid);
+ if (ack == acked.cend() or not ack->second.req) {
+ return time_point::min();
+ }
+ if (ack->second.req->completed()) {
+ return ack->second.refresh_time - REANNOUNCE_MARGIN;
+ }
+ return ack->second.req->pending() ? time_point::max() : time_point::min();
+ }
+
+ /**
+ * Assuming the node is synced, should the "listen" request with Query q be
+ * sent to this node now ?
+ */
+ time_point getListenTime(const decltype(listenStatus)::const_iterator listen_status, duration listen_expire) const {
+ if (listen_status == listenStatus.cend() or not listen_status->second.req)
+ return time_point::min();
+ return listen_status->second.req->pending() ? time_point::max() :
+ listen_status->second.req->reply_time + listen_expire - REANNOUNCE_MARGIN;
+ }
+ time_point getListenTime(const Sp<Query>& q, duration listen_expire) const {
+ return getListenTime(listenStatus.find(q), listen_expire);
+ }
+
+ /**
+ * Is this node expired or candidate
+ */
+ bool isBad() const {
+ return not node or node->isExpired() or candidate;
+ }
+};
+
+/**
+ * A search is a list of the nodes we think are responsible
+ * for storing values for a given hash.
+ */
+struct Dht::Search {
+ InfoHash id {};
+ sa_family_t af;
+
+ uint16_t tid;
+ time_point refill_time {time_point::min()};
+ time_point step_time {time_point::min()}; /* the time of the last search step */
+ Sp<Scheduler::Job> nextSearchStep {};
+
+ bool expired {false}; /* no node, or all nodes expired */
+ bool done {false}; /* search is over, cached for later */
+ std::vector<std::unique_ptr<SearchNode>> nodes {};
+
+ /* pending puts */
+ std::vector<Announce> announce {};
+
+ /* pending gets */
+ std::multimap<time_point, Get> callbacks {};
+
+ /* listeners */
+ struct SearchListener {
+ Sp<Query> query;
+ ValueCallback get_cb;
+ SyncCallback sync_cb;
+ };
+ std::map<size_t, SearchListener> listeners {};
+ size_t listener_token = 1;
+
+ /* Cache */
+ SearchCache cache;
+ Sp<Scheduler::Job> opExpirationJob;
+
+ ~Search() {
+ if (opExpirationJob)
+ opExpirationJob->cancel();
+ for (auto& g : callbacks) {
+ g.second.done_cb(false, {});
+ g.second.done_cb = {};
+ }
+ for (auto& a : announce) {
+ a.callback(false, {});
+ a.callback = {};
+ }
+ }
+
+ friend std::ostream& operator<< (std::ostream& s, const Search& sr) {
+ auto csize = sr.cache.size();
+ s << "announce:" << sr.announce.size()
+ << " gets:" << sr.callbacks.size()
+ << " listeners:" << sr.listeners.size()
+ << " cache:" << csize.first << ',' << csize.second << std::endl;
+ s << "nodes:" << std::endl;
+ for (const auto& n : sr.nodes)
+ s << *n;
+ return s;
+ }
+
+ /**
+ * @returns true if the node was not present and added to the search
+ */
+ bool insertNode(const Sp<Node>& n, time_point now, const Blob& token={});
+
+ SearchNode* getNode(const Sp<Node>& n) {
+ auto srn = std::find_if(nodes.begin(), nodes.end(), [&](const std::unique_ptr<SearchNode>& sn) {
+ return n == sn->node;
+ });
+ return (srn == nodes.end()) ? nullptr : (*srn).get();
+ }
+
+ /* number of concurrent sync requests */
+ unsigned currentlySolicitedNodeCount() const {
+ unsigned count = 0;
+ for (const auto& n : nodes)
+ if (not n->isBad() and n->pendingGet())
+ count++;
+ return count;
+ }
+
+ /**
+ * Can we use this search to announce ?
+ */
+ bool isSynced(time_point now) const;
+
+ unsigned syncLevel(time_point now) const;
+
+ /**
+ * Get the time of the last "get" operation performed on this search,
+ * or time_point::min() if no such operation have been performed.
+ *
+ * @param query The query identifying a 'get' request.
+ */
+ time_point getLastGetTime(const Query&) const;
+ time_point getLastGetTime() const;
+
+ /**
+ * Is this get operation done ?
+ */
+ bool isDone(const Get& get) const;
+
+ /**
+ * Sets a consistent state of the search after a given 'get' operation as
+ * been completed.
+ *
+ * This will also make sure to call the associated 'done callback'.
+ *
+ * @param get The 'get' operation which is now over.
+ */
+ void setDone(const Get& get) {
+ for (auto& n : nodes) {
+ auto pqs = n->pagination_queries.find(get.query);
+ if (pqs != n->pagination_queries.cend()) {
+ for (auto& pq : pqs->second)
+ n->getStatus.erase(pq);
+ }
+ n->getStatus.erase(get.query);
+ }
+ if (get.done_cb)
+ get.done_cb(true, getNodes());
+ }
+
+ /**
+ * Set the search in a consistent state after the search is done. This is
+ * the opportunity we have to clear some entries in the SearchNodes status
+ * maps.
+ */
+ void setDone() {
+ for (auto& n : nodes) {
+ n->getStatus.clear();
+ n->listenStatus.clear();
+ n->acked.clear();
+ }
+ done = true;
+ }
+
+ bool isAnnounced(Value::Id id) const;
+ bool isListening(time_point now, duration exp) const;
+
+ void get(const Value::Filter& f, const Sp<Query>& q, const QueryCallback& qcb, const GetCallback& gcb, const DoneCallback& dcb, Scheduler& scheduler) {
+ if (gcb or qcb) {
+ if (not cache.get(f, q, gcb, dcb)) {
+ const auto& now = scheduler.time();
+ callbacks.emplace(now, Get { now, f, q, qcb, gcb, dcb });
+ scheduler.edit(nextSearchStep, now);
+ }
+ }
+ }
+
+ size_t listen(const ValueCallback& cb, Value::Filter&& f, const Sp<Query>& q, Scheduler& scheduler) {
+ //DHT_LOG.e(id, "[search %s IPv%c] listen", id.toString().c_str(), (af == AF_INET) ? '4' : '6');
+ return cache.listen(cb, q, std::move(f), [&](const Sp<Query>& q, ValueCallback vcb, SyncCallback scb){
+ done = false;
+ auto token = ++listener_token;
+ listeners.emplace(token, SearchListener{q, vcb, scb});
+ scheduler.edit(nextSearchStep, scheduler.time());
+ return token;
+ });
+ }
+
+ void cancelListen(size_t token, Scheduler& scheduler) {
+ cache.cancelListen(token, scheduler.time());
+ if (not opExpirationJob)
+ opExpirationJob = scheduler.add(time_point::max(), [this,&scheduler]{
+ auto nextExpire = cache.expire(scheduler.time(), [&](size_t t){
+ const auto& ll = listeners.find(t);
+ if (ll != listeners.cend()) {
+ auto query = ll->second.query;
+ listeners.erase(ll);
+ if (listeners.empty()) {
+ for (auto& sn : nodes) sn->cancelListen();
+ } else if (query) {
+ for (auto& sn : nodes) sn->cancelListen(query);
+ }
+ }
+ });
+ scheduler.edit(opExpirationJob, nextExpire);
+ });
+ scheduler.edit(opExpirationJob, cache.getExpiration());
+ }
+
+ std::vector<Sp<Value>> getPut() const {
+ std::vector<Sp<Value>> ret;
+ ret.reserve(announce.size());
+ for (const auto& a : announce)
+ ret.push_back(a.value);
+ return ret;
+ }
+
+ Sp<Value> getPut(Value::Id vid) const {
+ for (auto& a : announce) {
+ if (a.value->id == vid)
+ return a.value;
+ }
+ return {};
+ }
+
+ bool cancelPut(Value::Id vid) {
+ bool canceled {false};
+ for (auto it = announce.begin(); it != announce.end();) {
+ if (it->value->id == vid) {
+ canceled = true;
+ it = announce.erase(it);
+ }
+ else
+ ++it;
+ }
+ for (auto& n : nodes) {
+ auto ackIt = n->acked.find(vid);
+ if (ackIt != n->acked.end()) {
+ if (ackIt->second.req)
+ ackIt->second.req->cancel();
+ if (ackIt->second.refresh)
+ ackIt->second.refresh->cancel();
+ n->acked.erase(ackIt);
+ }
+ }
+ return canceled;
+ }
+
+ void put(const Sp<Value>& value, DoneCallback callback, time_point created, bool permanent) {
+ done = false;
+ expired = false;
+ auto a_sr = std::find_if(announce.begin(), announce.end(), [&](const Announce& a){
+ return a.value->id == value->id;
+ });
+ if (a_sr == announce.end()) {
+ announce.emplace_back(Announce {permanent, value, created, callback});
+ for (auto& n : nodes) {
+ n->probe_query.reset();
+ n->acked[value->id].req.reset();
+ }
+ } else {
+ a_sr->permanent = permanent;
+ a_sr->created = created;
+ if (a_sr->value != value) {
+ a_sr->value = value;
+ for (auto& n : nodes) {
+ n->acked[value->id].req.reset();
+ n->probe_query.reset();
+ }
+ }
+ if (isAnnounced(value->id)) {
+ if (a_sr->callback)
+ a_sr->callback(true, {});
+ a_sr->callback = {};
+ if (callback)
+ callback(true, {});
+ return;
+ } else {
+ if (a_sr->callback)
+ a_sr->callback(false, {});
+ a_sr->callback = callback;
+ }
+ }
+ }
+
+ /**
+ * @return The number of non-good search nodes.
+ */
+ unsigned getNumberOfBadNodes() const {
+ return std::count_if(nodes.begin(), nodes.end(), [](const std::unique_ptr<SearchNode>& sn) {
+ return sn->isBad();
+ });
+ }
+ unsigned getNumberOfConsecutiveBadNodes() const {
+ unsigned count = 0;
+ for (const auto& sn : nodes) {
+ if (not sn->node->isExpired())
+ break;
+ ++count;
+ }
+ return count;
+ }
+
+ /**
+ * Removes a node which have been expired for at least
+ * NODE::NODE_EXPIRE_TIME minutes. The search for an expired node starts
+ * from the end.
+ *
+ * @param now The reference to now.
+ *
+ * @return true if a node has been removed, else false.
+ */
+ bool removeExpiredNode(const time_point& now) {
+ for (auto e = nodes.cend(); e != nodes.cbegin();) {
+ const Node& n = *(*(--e))->node;
+ if (n.isRemovable(now)) {
+ //std::cout << "Removing expired node " << n.id << " from IPv" << (af==AF_INET?'4':'6') << " search " << id << std::endl;
+ nodes.erase(e);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * This method is called when we have discovered that the search is expired.
+ * We have to
+ *
+ * - remove all nodes from the search;
+ * - clear (non-permanent) callbacks;
+ */
+ void expire() {
+ // no nodes or all expired nodes. This is most likely a connectivity change event.
+ expired = true;
+
+ nodes.clear();
+ if (announce.empty() && listeners.empty())
+ // Listening or announcing requires keeping the cluster up to date.
+ setDone();
+ {
+ auto get_cbs = std::move(callbacks);
+ for (const auto& g : get_cbs) {
+ if (g.second.done_cb)
+ g.second.done_cb(false, {});
+ }
+ }
+ {
+ std::vector<DoneCallback> a_cbs;
+ a_cbs.reserve(announce.size());
+ for (auto ait = announce.begin() ; ait != announce.end(); ) {
+ if (ait->callback)
+ a_cbs.emplace_back(std::move(ait->callback));
+ if (not ait->permanent)
+ ait = announce.erase(ait);
+ else
+ ait++;
+ }
+ for (const auto& a : a_cbs)
+ a(false, {});
+ }
+ }
+
+ /**
+ * If the value was just successfully announced, call the callback and erase it if not permanent.
+ *
+ * @param vid The id of the announced value.
+ * @param types The sequence of existing types.
+ * @param now The time reference to now.
+ */
+ void checkAnnounced(Value::Id vid = Value::INVALID_ID) {
+ auto announced = std::partition(announce.begin(), announce.end(),
+ [this,&vid](Announce& a) {
+ if (vid != Value::INVALID_ID and (!a.value || a.value->id != vid))
+ return true;
+ if (isAnnounced(a.value->id)) {
+ if (a.callback) {
+ a.callback(true, getNodes());
+ a.callback = nullptr;
+ }
+ if (not a.permanent)
+ return false;
+ }
+ return true;
+ });
+ // remove acked for cleared annouces
+ for (auto it = announced; it != announce.end(); ++it) {
+ for (auto& n : nodes) {
+ auto ackIt = n->acked.find(it->value->id);
+ if (ackIt != n->acked.end()) {
+ if (ackIt->second.req)
+ ackIt->second.req->cancel();
+ n->acked.erase(ackIt);
+ }
+ }
+ }
+ announce.erase(announced, announce.end());
+ }
+
+ std::vector<Sp<Node>> getNodes() const;
+
+ void clear() {
+ announce.clear();
+ callbacks.clear();
+ listeners.clear();
+ nodes.clear();
+ nextSearchStep.reset();
+ }
+};
+
+
+/* A search contains a list of nodes, sorted by decreasing distance to the
+ target. We just got a new candidate, insert it at the right spot or
+ discard it. */
+bool
+Dht::Search::insertNode(const Sp<Node>& snode, time_point now, const Blob& token)
+{
+ auto& node = *snode;
+ const auto& nid = node.id;
+
+ if (node.getFamily() != af)
+ return false;
+
+ bool found = false;
+ auto n = nodes.end();
+ while (n != nodes.begin()) {
+ --n;
+ if ((*n)->node == snode) {
+ found = true;
+ break;
+ }
+
+ /* Node not found. We could insert it after this one. */
+ if (id.xorCmp(nid, (*n)->node->id) > 0) {
+ ++n;
+ break;
+ }
+ }
+
+ if (not found) {
+ // find if and where to trim excessive nodes
+ auto t = nodes.cend();
+ size_t bad = 0; // number of bad nodes (if search is not expired)
+ bool full {false}; // is the search full (has the maximum nodes)
+ if (expired) {
+ // if the search is expired, trim to SEARCH_NODES nodes
+ if (nodes.size() >= SEARCH_NODES) {
+ full = true;
+ t = nodes.begin() + SEARCH_NODES;
+ }
+ } else {
+ // otherwise, trim to SEARCH_NODES nodes, not counting bad nodes
+ bad = getNumberOfBadNodes();
+ full = nodes.size() - bad >= SEARCH_NODES;
+ while (std::distance(nodes.cbegin(), t) - bad > SEARCH_NODES) {
+ --t;
+ if ((*t)->isBad())
+ bad--;
+ }
+ }
+
+ if (full) {
+ bool addNode = n < t;
+ if (t != nodes.cend())
+ nodes.resize(std::distance(nodes.cbegin(), t));
+ if (not addNode)
+ return false;
+ }
+
+ // Reset search timer if the search is empty
+ if (nodes.empty()) {
+ step_time = time_point::min();
+ }
+ n = nodes.emplace(n, std::make_unique<SearchNode>(snode));
+ node.setTime(now);
+ if (node.isExpired()) {
+ if (not expired)
+ bad++;
+ } else if (expired) {
+ bad = nodes.size() - 1;
+ expired = false;
+ }
+
+ while (nodes.size() - bad > SEARCH_NODES) {
+ if (not expired and nodes.back()->isBad())
+ bad--;
+ nodes.pop_back();
+ }
+ }
+ if (not token.empty()) {
+ (*n)->candidate = false;
+ (*n)->last_get_reply = now;
+ if (token.size() <= 64)
+ (*n)->token = token;
+ expired = false;
+ }
+ if (not found)
+ removeExpiredNode(now);
+ return not found;
+}
+
+std::vector<Sp<Node>>
+Dht::Search::getNodes() const
+{
+ std::vector<Sp<Node>> ret {};
+ ret.reserve(nodes.size());
+ for (const auto& sn : nodes)
+ ret.emplace_back(sn->node);
+ return ret;
+}
+
+bool
+Dht::Search::isSynced(time_point now) const
+{
+ unsigned i = 0;
+ for (const auto& n : nodes) {
+ if (n->isBad())
+ continue;
+ if (not n->isSynced(now))
+ return false;
+ if (++i == TARGET_NODES)
+ break;
+ }
+ return i > 0;
+}
+
+unsigned
+Dht::Search::syncLevel(time_point now) const
+{
+ unsigned i = 0;
+ for (const auto& n : nodes) {
+ if (n->isBad())
+ continue;
+ if (not n->isSynced(now))
+ return i;
+ if (++i == TARGET_NODES)
+ break;
+ }
+ return i;
+}
+
+time_point
+Dht::Search::getLastGetTime(const Query& q) const
+{
+ time_point last = time_point::min();
+ for (const auto& g : callbacks)
+ last = std::max(last, (q.isSatisfiedBy(*g.second.query) ? g.second.start : time_point::min()));
+ return last;
+}
+
+time_point
+Dht::Search::getLastGetTime() const
+{
+ time_point last = time_point::min();
+ for (const auto& g : callbacks)
+ last = std::max(last, g.second.start);
+ return last;
+}
+
+bool
+Dht::Search::isDone(const Get& get) const
+{
+ unsigned i = 0;
+ for (const auto& sn : nodes) {
+ if (sn->isBad())
+ continue;
+ if (not sn->isDone(get))
+ return false;
+ if (++i == TARGET_NODES)
+ break;
+ }
+ return true;
+}
+
+bool
+Dht::Search::isAnnounced(Value::Id id) const
+{
+ if (nodes.empty())
+ return false;
+ unsigned i = 0;
+ for (const auto& n : nodes) {
+ if (n->isBad())
+ continue;
+ if (not n->isAnnounced(id))
+ return false;
+ if (++i == TARGET_NODES)
+ return true;
+ }
+ return i;
+}
+
+bool
+Dht::Search::isListening(time_point now, duration listen_expire) const
+{
+ if (nodes.empty() or listeners.empty())
+ return false;
+ unsigned i = 0;
+ for (const auto& n : nodes) {
+ if (n->isBad())
+ continue;
+ SearchNode::NodeListenerStatus::const_iterator ls {};
+ for (ls = n->listenStatus.begin(); ls != n->listenStatus.end() ; ++ls) {
+ if (n->isListening(now, ls, listen_expire))
+ break;
+ }
+ if (ls == n->listenStatus.end())
+ return false;
+ if (++i == LISTEN_NODES)
+ break;
+ }
+ return i;
+}
+
+}
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ * Authors: Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ * Simon Désaulniers <simon.desaulniers@savoirfairelinux.com>
+ * Sébastien Blin <sebastien.blin@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "securedht.h"
+#include "rng.h"
+
+#include "default_types.h"
+
+extern "C" {
+#include <gnutls/gnutls.h>
+#include <gnutls/abstract.h>
+#include <gnutls/x509.h>
+}
+
+#include <random>
+
+namespace dht {
+
+SecureDht::SecureDht(std::unique_ptr<DhtInterface> dht, SecureDht::Config conf, IdentityAnnouncedCb iacb, const std::shared_ptr<Logger>& l)
+: DhtInterface(l), dht_(std::move(dht)), key_(conf.id.first), certificate_(conf.id.second), enableCache_(conf.cert_cache_all)
+{
+ if (!dht_) return;
+ for (const auto& type : DEFAULT_TYPES)
+ registerType(type);
+
+ for (const auto& type : DEFAULT_INSECURE_TYPES)
+ registerInsecureType(type);
+
+ registerInsecureType(CERTIFICATE_TYPE);
+
+ if (certificate_) {
+ auto certId = certificate_->getId();
+ if (key_ and certId != key_->getPublicKey().getId())
+ throw DhtException("SecureDht: provided certificate doesn't match private key.");
+ dht_->addOnConnectedCallback([this, certId, cb=std::move(iacb)]{
+ dht_->put(certId, Value {
+ CERTIFICATE_TYPE,
+ *certificate_,
+ 1
+ }, [this, certId, cb=std::move(cb)](bool ok) {
+ if (cb) cb(ok);
+ if (logger_)
+ logger_->d(certId, "SecureDht: certificate announcement %s", ok ? "succeeded" : "failed");
+ }, {}, true);
+ });
+ }
+}
+
+SecureDht::~SecureDht(){
+ dht_.reset();
+}
+
+ValueType
+SecureDht::secureType(ValueType&& type)
+{
+ type.storePolicy = [type](InfoHash id, Sp<Value>& v, const InfoHash& nid, const SockAddr& a) {
+ if (v->isSigned())
+ return v->checkSignature();
+ return type.storePolicy(id, v, nid, a);
+ };
+ type.editPolicy = [this,type](InfoHash id, const Sp<Value>& o, Sp<Value>& n, const InfoHash& nid, const SockAddr& a) {
+ if (not o->isSigned())
+ return type.editPolicy(id, o, n, nid, a);
+ if (*o->owner != *n->owner or not n->isSigned()) {
+ if (logger_)
+ logger_->w("Edition forbidden: not signed or wrong owner.");
+ return false;
+ }
+ if (not n->checkSignature()) {
+ if (logger_)
+ logger_->w("Edition forbidden: signature verification failed.");
+ return false;
+ }
+ if (o->seq == n->seq) {
+ // If the data is exactly the same,
+ // it can be reannounced, possibly by someone else.
+ if (o->getToSign() != n->getToSign()) {
+ if (logger_)
+ logger_->w("Edition forbidden: sequence number must be increasing.");
+ return false;
+ }
+ }
+ else if (n->seq < o->seq)
+ return false;
+ return true;
+ };
+ return type;
+}
+
+Sp<crypto::Certificate>
+SecureDht::getCertificate(const InfoHash& node) const
+{
+ if (node == getId())
+ return certificate_;
+ auto it = nodesCertificates_.find(node);
+ if (it == nodesCertificates_.end())
+ return nullptr;
+ else
+ return it->second;
+}
+
+Sp<crypto::PublicKey>
+SecureDht::getPublicKey(const InfoHash& node) const
+{
+ if (node == getId())
+ return certificate_->getSharedPublicKey();
+ auto it = nodesPubKeys_.find(node);
+ if (it == nodesPubKeys_.end())
+ return nullptr;
+ else
+ return it->second;
+}
+
+Sp<crypto::Certificate>
+SecureDht::registerCertificate(const InfoHash& node, const Blob& data)
+{
+ Sp<crypto::Certificate> crt;
+ try {
+ crt = std::make_shared<crypto::Certificate>(data);
+ } catch (const std::exception& e) {
+ return nullptr;
+ }
+ InfoHash h = crt->getPublicKey().getId();
+ if (node == h) {
+ if (logger_)
+ logger_->d("Registering certificate for %s", h.toString().c_str());
+ auto it = nodesCertificates_.find(h);
+ if (it == nodesCertificates_.end())
+ std::tie(it, std::ignore) = nodesCertificates_.emplace(h, std::move(crt));
+ else
+ it->second = std::move(crt);
+ return it->second;
+ } else {
+ if (logger_)
+ logger_->w("Certificate %s for node %s does not match node id !", h.toString().c_str(), node.toString().c_str());
+ return nullptr;
+ }
+}
+
+void
+SecureDht::registerCertificate(Sp<crypto::Certificate>& cert)
+{
+ if (cert)
+ nodesCertificates_[cert->getId()] = cert;
+}
+
+void
+SecureDht::findCertificate(const InfoHash& node, const std::function<void(const Sp<crypto::Certificate>)>& cb)
+{
+ Sp<crypto::Certificate> b = getCertificate(node);
+ if (b && *b) {
+ if (logger_)
+ logger_->d("Using certificate from cache for %s", node.to_c_str());
+ if (cb)
+ cb(b);
+ return;
+ }
+ if (localQueryMethod_) {
+ auto res = localQueryMethod_(node);
+ if (not res.empty()) {
+ if (logger_)
+ logger_->d("Registering certificate from local store for %s", node.to_c_str());
+ nodesCertificates_.emplace(node, res.front());
+ if (cb)
+ cb(res.front());
+ return;
+ }
+ }
+
+ auto found = std::make_shared<bool>(false);
+ dht_->get(node, [cb,node,found,this](const std::vector<Sp<Value>>& vals) {
+ for (const auto& v : vals) {
+ if (auto cert = registerCertificate(node, v->data)) {
+ *found = true;
+ if (logger_)
+ logger_->d(node, "Found certificate for %s", node.to_c_str());
+ if (cb)
+ cb(cert);
+ return false;
+ }
+ }
+ return !*found;
+ }, [cb,found](bool) {
+ if (!*found and cb)
+ cb(nullptr);
+ }, Value::TypeFilter(CERTIFICATE_TYPE));
+}
+
+void
+SecureDht::findPublicKey(const InfoHash& node, const std::function<void(const Sp<crypto::PublicKey>)>& cb)
+{
+ auto pk = getPublicKey(node);
+ if (pk && *pk) {
+ if (logger_)
+ logger_->d(node, "Found public key from cache for %s", node.to_c_str());
+ if (cb)
+ cb(pk);
+ return;
+ }
+ findCertificate(node, [=](const Sp<crypto::Certificate>& crt) {
+ if (crt && *crt) {
+ auto pk = crt->getSharedPublicKey();
+ if (*pk) {
+ nodesPubKeys_[pk->getId()] = pk;
+ if (cb) cb(pk);
+ return;
+ }
+ }
+ if (cb) cb(nullptr);
+ });
+}
+
+Sp<Value>
+SecureDht::checkValue(const Sp<Value>& v)
+{
+ // Decrypt encrypted values
+ if (v->isEncrypted()) {
+ if (not key_) {
+#ifdef OPENDHT_PROXY_SERVER
+ if (forward_all_) // We are currently a proxy, send messages to clients.
+ return v;
+#endif
+ return {};
+ }
+ try {
+ auto isDecrypted = v->isDecrypted();
+ if (auto decrypted_val = v->decrypt(*key_)) {
+ auto cacheValue = not isDecrypted and decrypted_val->owner;
+ if (cacheValue)
+ nodesPubKeys_[decrypted_val->owner->getId()] = decrypted_val->owner;
+ return decrypted_val;
+ }
+ } catch (const std::exception& e) {
+ if (logger_)
+ logger_->w("Could not decrypt value %s : %s", v->toString().c_str(), e.what());
+ }
+ }
+ // Check signed values
+ else if (v->isSigned()) {
+ auto cacheValue = not v->isSignatureChecked() and enableCache_ and v->owner;
+ if (v->checkSignature()) {
+ if (cacheValue)
+ nodesPubKeys_[v->owner->getId()] = v->owner;
+ return v;
+ } else if (logger_)
+ logger_->w("Signature verification failed for %s", v->toString().c_str());
+ }
+ // Forward normal values
+ else {
+ return v;
+ }
+ return {};
+}
+
+ValueCallback
+SecureDht::getCallbackFilter(const ValueCallback& cb, Value::Filter&& filter)
+{
+ return [=](const std::vector<Sp<Value>>& values, bool expired) {
+ std::vector<Sp<Value>> tmpvals {};
+ if (not filter)
+ tmpvals.reserve(values.size());
+ for (const auto& v : values) {
+ if (auto nv = checkValue(v))
+ if (not filter or filter(*nv))
+ tmpvals.emplace_back(std::move(nv));
+ }
+ if (cb and not tmpvals.empty())
+ return cb(tmpvals, expired);
+ return true;
+ };
+}
+
+
+GetCallback
+SecureDht::getCallbackFilter(const GetCallback& cb, Value::Filter&& filter)
+{
+ return [=](const std::vector<Sp<Value>>& values) {
+ std::vector<Sp<Value>> tmpvals {};
+ if (not filter)
+ tmpvals.reserve(values.size());
+ for (const auto& v : values) {
+ if (auto nv = checkValue(v))
+ if (not filter or filter(*nv))
+ tmpvals.emplace_back(std::move(nv));
+ }
+ if (cb and not tmpvals.empty())
+ return cb(tmpvals);
+ return true;
+ };
+}
+
+void
+SecureDht::get(const InfoHash& id, GetCallback cb, DoneCallback donecb, Value::Filter&& f, Where&& w)
+{
+ dht_->get(id, getCallbackFilter(cb, std::forward<Value::Filter>(f)), donecb, {}, std::forward<Where>(w));
+}
+
+size_t
+SecureDht::listen(const InfoHash& id, ValueCallback cb, Value::Filter f, Where w)
+{
+ return dht_->listen(id, getCallbackFilter(cb, std::forward<Value::Filter>(f)), {}, std::forward<Where>(w));
+}
+
+
+size_t
+SecureDht::listen(const InfoHash& id, GetCallback cb, Value::Filter f, Where w)
+{
+ return dht_->listen(id, getCallbackFilter(cb, std::forward<Value::Filter>(f)), {}, std::forward<Where>(w));
+}
+
+void
+SecureDht::putSigned(const InfoHash& hash, Sp<Value> val, DoneCallback callback, bool permanent)
+{
+ if (not key_ or not hash or not val) {
+ if (callback)
+ callback(false, {});
+ return;
+ }
+ if (val->id == Value::INVALID_ID) {
+ crypto::random_device rdev;
+ std::uniform_int_distribution<Value::Id> rand_id;
+ val->id = rand_id(rdev);
+ }
+
+ // Check if we are already announcing a value
+ auto p = dht_->getPut(hash, val->id);
+ if (p && val->seq <= p->seq) {
+ val->seq = p->seq + 1;
+ }
+
+ // Check if data already exists on the dht
+ get(hash,
+ [val,this] (const std::vector<Sp<Value>>& vals) {
+ if (logger_)
+ logger_->d("Found online previous value being announced.");
+ for (const auto& v : vals) {
+ if (!v->isSigned()) {
+ if (logger_)
+ logger_->e("Existing non-signed value seems to exists at this location.");
+ } else if (not v->owner or v->owner->getId() != getId()) {
+ if (logger_)
+ logger_->e("Existing signed value belonging to someone else seems to exists at this location.");
+ } else if (val->seq <= v->seq)
+ val->seq = v->seq + 1;
+ }
+ return true;
+ },
+ [hash,val,this,callback,permanent] (bool /* ok */) {
+ sign(*val);
+ dht_->put(hash, val, callback, time_point::max(), permanent);
+ },
+ Value::IdFilter(val->id),
+ Where().id(val->id)
+ );
+}
+
+void
+SecureDht::putEncrypted(const InfoHash& hash, const InfoHash& to, Sp<Value> val, DoneCallback callback, bool permanent)
+{
+ if (not key_) {
+ if (callback)
+ callback(false, {});
+ return;
+ }
+ findPublicKey(to, [this, hash, val = std::move(val), callback = std::move(callback), permanent](const Sp<crypto::PublicKey>& pk) {
+ if(!pk || !*pk) {
+ if (callback)
+ callback(false, {});
+ return;
+ }
+ if (logger_)
+ logger_->w("Encrypting data for PK: %s", pk->getId().toString().c_str());
+ try {
+ dht_->put(hash, encrypt(*val, *pk), callback, time_point::max(), permanent);
+ } catch (const std::exception& e) {
+ if (logger_)
+ logger_->e("Error putting encrypted data: %s", e.what());
+ if (callback)
+ callback(false, {});
+ }
+ });
+}
+
+void
+SecureDht::putEncrypted(const InfoHash& hash, const crypto::PublicKey& pk, Sp<Value> val, DoneCallback callback, bool permanent)
+{
+ if (not key_) {
+ if (callback)
+ callback(false, {});
+ return;
+ }
+ if (logger_)
+ logger_->w("Encrypting data for PK: %s", pk.getLongId().to_c_str());
+ try {
+ dht_->put(hash, encrypt(*val, pk), callback, time_point::max(), permanent);
+ } catch (const std::exception& e) {
+ if (logger_)
+ logger_->e("Error putting encrypted data: %s", e.what());
+ if (callback)
+ callback(false, {});
+ }
+}
+
+void
+SecureDht::sign(Value& v) const
+{
+ v.sign(*key_);
+}
+
+Value
+SecureDht::encrypt(Value& v, const crypto::PublicKey& to) const
+{
+ return v.encrypt(*key_, to);
+}
+
+Value
+SecureDht::decrypt(const Value& v)
+{
+ if (not v.isEncrypted())
+ throw DhtException("Data is not encrypted.");
+
+ auto decrypted = key_->decrypt(v.cypher);
+
+ Value ret {v.id};
+ auto msg = msgpack::unpack((const char*)decrypted.data(), decrypted.size());
+ ret.msgpack_unpack_body(msg.get());
+
+ if (ret.recipient != getId())
+ throw crypto::DecryptError("Recipient mismatch");
+ if (not ret.owner or not ret.owner->checkSignature(ret.getToSign(), ret.signature))
+ throw crypto::DecryptError("Signature mismatch");
+
+ return ret;
+}
+
+}
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ * Author(s) : Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "infohash.h"
+#include "value.h"
+#include "listener.h"
+
+#include <map>
+#include <utility>
+
+namespace dht {
+
+/**
+ * Tracks storage usage per IP or IP range
+ */
+class StorageBucket {
+public:
+ void insert(const InfoHash& id, const Value& value, time_point expiration) {
+ totalSize_ += value.size();
+ storedValues_.emplace(expiration, std::pair<InfoHash, Value::Id>(id, value.id));
+ }
+ void erase(const InfoHash& id, const Value& value, time_point expiration) {
+ auto range = storedValues_.equal_range(expiration);
+ for (auto rit = range.first; rit != range.second;) {
+ if (rit->second.first == id && rit->second.second == value.id) {
+ totalSize_ -= value.size();
+ storedValues_.erase(rit);
+ return;
+ } else
+ ++rit;
+ }
+ // printf("StorageBucket::erase can't find value %s %016" PRIx64 "\n", id.to_c_str(), value.id);
+ }
+ void refresh(const InfoHash& id, const Value& value, time_point old_expiration, time_point expiration) {
+ auto range = storedValues_.equal_range(old_expiration);
+ for (auto rit = range.first; rit != range.second;) {
+ if (rit->second.first == id && rit->second.second == value.id) {
+ storedValues_.erase(rit);
+ storedValues_.emplace(expiration, std::pair<InfoHash, Value::Id>(id, value.id));
+ return;
+ } else
+ ++rit;
+ }
+ // printf("StorageBucket::refresh can't find value %s %016" PRIx64 "\n", id.to_c_str(), value.id);
+ insert(id, value, expiration);
+ }
+ size_t size() const { return totalSize_; }
+ std::pair<InfoHash, Value::Id> getOldest() const { return storedValues_.empty() ? std::pair<InfoHash, Value::Id>{} : storedValues_.begin()->second; }
+private:
+ std::multimap<time_point, std::pair<InfoHash, Value::Id>> storedValues_;
+ size_t totalSize_ {0};
+};
+
+struct ValueStorage {
+ Sp<Value> data {};
+ time_point created {};
+ time_point expiration {};
+ Sp<Scheduler::Job> expiration_job {};
+ StorageBucket* store_bucket {nullptr};
+
+ ValueStorage() {}
+ ValueStorage(const Sp<Value>& v, time_point t, time_point e)
+ : data(v), created(t), expiration(e) {}
+};
+
+
+struct Storage {
+ time_point maintenance_time {};
+ std::map<Sp<Node>, std::map<size_t, Listener>> listeners;
+ std::map<size_t, LocalListener> local_listeners {};
+ size_t listener_token {1};
+
+ /* The maximum number of values we store for a given hash. */
+ static constexpr unsigned MAX_VALUES {64 * 1024};
+
+ /**
+ * Changes caused by an operation on the storage.
+ */
+ struct StoreDiff {
+ /** Difference in stored size caused by the op */
+ ssize_t size_diff;
+ /** Difference in number of values */
+ ssize_t values_diff;
+ /** Difference in number of listeners */
+ ssize_t listeners_diff;
+ /** Number of edited values */
+ size_t edited_values;
+ };
+
+ Storage() {}
+ Storage(time_point t) : maintenance_time(t) {}
+ Storage(Storage&& o) noexcept = default;
+ Storage& operator=(Storage&& o) = default;
+
+ bool empty() const {
+ return values.empty();
+ }
+
+ StoreDiff clear();
+
+ size_t valueCount() const {
+ return values.size();
+ }
+
+ size_t totalSize() const {
+ return total_size;
+ }
+
+ const std::vector<ValueStorage>& getValues() const { return values; }
+
+ Sp<Value> getById(Value::Id vid) const {
+ for (auto& v : values)
+ if (v.data->id == vid) return v.data;
+ return {};
+ }
+
+ std::vector<Sp<Value>> get(const Value::Filter& f = {}) const {
+ std::vector<Sp<Value>> newvals {};
+ if (not f) newvals.reserve(values.size());
+ for (auto& v : values) {
+ if (not f || f(*v.data))
+ newvals.push_back(v.data);
+ }
+ return newvals;
+ }
+
+ /**
+ * Stores a new value in this storage, or replace a previous value
+ *
+ * @return <storage, change_size, change_value_num>
+ * storage: set if a change happened
+ * change_size: size difference
+ * change_value_num: change of value number (0 or 1)
+ */
+ std::pair<ValueStorage*, StoreDiff>
+ store(const InfoHash& id, const Sp<Value>&, time_point created, time_point expiration, StorageBucket*);
+
+ /**
+ * Refreshes the time point of the value's lifetime begining.
+ *
+ * @param now The reference to now
+ * @param vid The value id
+ * @return time of the next expiration, time_point::max() if no expiration
+ */
+ std::pair<ValueStorage*, time_point>
+ refresh(const InfoHash& id, const time_point& now, const Value::Id& vid, const TypeStore& types) {
+ for (auto& vs : values)
+ if (vs.data->id == vid) {
+ vs.created = now;
+ auto oldExp = vs.expiration;
+ vs.expiration = std::max(oldExp, now + types.getType(vs.data->type).expiration);
+ if (vs.store_bucket)
+ vs.store_bucket->refresh(id, *vs.data, oldExp, vs.expiration);
+ return {&vs, vs.expiration};
+ }
+ return {nullptr, time_point::max()};
+ }
+
+ size_t listen(ValueCallback& cb, Value::Filter& f, const Sp<Query>& q);
+
+ void cancelListen(size_t token) {
+ local_listeners.erase(token);
+ }
+
+ Sp<Value> remove(const InfoHash& id, Value::Id);
+
+ std::pair<ssize_t, std::vector<Sp<Value>>> expire(const InfoHash& id, time_point now);
+
+private:
+ Storage(const Storage&) = delete;
+ Storage& operator=(const Storage&) = delete;
+
+ std::vector<ValueStorage> values {};
+ size_t total_size {};
+};
+
+
+size_t
+Storage::listen(ValueCallback& gcb, Value::Filter& filter, const Sp<Query>& query)
+{
+ if (not empty()) {
+ std::vector<Sp<Value>> newvals = get(filter);
+ if (not newvals.empty()) {
+ if (!gcb(newvals, false))
+ return 0;
+ }
+ }
+ auto tokenlocal = ++listener_token;
+ local_listeners.emplace(tokenlocal, LocalListener{query, filter, gcb});
+ return tokenlocal;
+}
+
+
+std::pair<ValueStorage*, Storage::StoreDiff>
+Storage::store(const InfoHash& id, const Sp<Value>& value, time_point created, time_point expiration, StorageBucket* sb)
+{
+ auto it = std::find_if (values.begin(), values.end(), [&](const ValueStorage& vr) {
+ return vr.data == value || vr.data->id == value->id;
+ });
+ ssize_t size_new = value->size();
+ if (it != values.end()) {
+ /* Already there, only need to refresh */
+ it->created = created;
+ if (it->data != value) {
+ size_t size_old = it->data->size();
+ ssize_t size_diff = size_new - (ssize_t)size_old;
+ //DHT_LOG.DEBUG("Updating %s -> %s", id.toString().c_str(), value->toString().c_str());
+ // clear quota for previous value
+ if (it->store_bucket)
+ it->store_bucket->erase(id, *value, it->expiration);
+ it->expiration = expiration;
+ // update quota for new value
+ it->store_bucket = sb;
+ if (sb)
+ sb->insert(id, *value, expiration);
+ it->data = value;
+ total_size += size_diff;
+ return std::make_pair(&(*it), StoreDiff{size_diff, 0, 0, 1});
+ }
+ } else {
+ //DHT_LOG.DEBUG("Storing %s -> %s", id.toString().c_str(), value->toString().c_str());
+ if (values.size() < MAX_VALUES) {
+ total_size += size_new;
+ values.emplace_back(value, created, expiration);
+ values.back().store_bucket = sb;
+ if (sb)
+ sb->insert(id, *value, expiration);
+ return std::make_pair(&values.back(), StoreDiff{size_new, 1, 0, 0});
+ }
+ }
+ return std::make_pair(nullptr, StoreDiff{});
+}
+
+Sp<Value>
+Storage::remove(const InfoHash& id, Value::Id vid)
+{
+ auto it = std::find_if (values.begin(), values.end(), [&](const ValueStorage& vr) {
+ return vr.data->id == vid;
+ });
+ if (it == values.end())
+ return {};
+ ssize_t size = it->data->size();
+ if (it->store_bucket)
+ it->store_bucket->erase(id, *it->data, it->expiration);
+ if (it->expiration_job)
+ it->expiration_job->cancel();
+ total_size -= size;
+ auto value = it->data;
+ values.erase(it);
+ return value;
+}
+
+Storage::StoreDiff
+Storage::clear()
+{
+ ssize_t num_values = values.size();
+ ssize_t tot_size = total_size;
+ values.clear();
+ total_size = 0;
+ return {-tot_size, -num_values, 0, 0};
+}
+
+std::pair<ssize_t, std::vector<Sp<Value>>>
+Storage::expire(const InfoHash& id, time_point now)
+{
+ // expire listeners
+ for (auto nl_it = listeners.begin(); nl_it != listeners.end();) {
+ auto& node_listeners = nl_it->second;
+ for (auto l = node_listeners.cbegin(); l != node_listeners.cend();) {
+ bool expired = l->second.time + Node::NODE_EXPIRE_TIME < now;
+ if (expired)
+ l = node_listeners.erase(l);
+ else
+ ++l;
+ }
+ if (node_listeners.empty()) {
+ nl_it = listeners.erase(nl_it);
+ }
+ else
+ ++nl_it;
+ }
+
+ // expire values
+ auto r = std::partition(values.begin(), values.end(), [&](const ValueStorage& v) {
+ return v.expiration > now;
+ });
+ std::vector<Sp<Value>> ret;
+ ret.reserve(std::distance(r, values.end()));
+ ssize_t size_diff {0};
+ std::for_each(r, values.end(), [&](const ValueStorage& v) {
+ size_diff -= v.data->size();
+ if (v.store_bucket)
+ v.store_bucket->erase(id, *v.data, v.expiration);
+ if (v.expiration_job)
+ v.expiration_job->cancel();
+ ret.emplace_back(std::move(v.data));
+ });
+ total_size += size_diff;
+ values.erase(r, values.end());
+ return {size_diff, std::move(ret)};
+}
+
+}
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ *
+ * Author: Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "thread_pool.h"
+
+#include <atomic>
+#include <thread>
+#include <iostream>
+#include <ciso646> // fix windows compiler bug
+#include <cmath> // std::pow
+
+namespace dht {
+
+constexpr const size_t IO_THREADS_MAX {512};
+
+ThreadPool&
+ThreadPool::computation()
+{
+ static ThreadPool pool;
+ return pool;
+}
+
+ThreadPool&
+ThreadPool::io()
+{
+ static ThreadPool pool(std::thread::hardware_concurrency(), IO_THREADS_MAX);
+ return pool;
+}
+
+
+ThreadPool::ThreadPool(unsigned minThreads, unsigned maxThreads)
+ : minThreads_(std::max(minThreads, 1u))
+ , maxThreads_(maxThreads ? std::max(minThreads_, maxThreads) : minThreads_)
+{
+ threads_.reserve(maxThreads_);
+ if (minThreads_ != maxThreads_) {
+ threadDelayRatio_ = std::pow(3, 1.0 / (maxThreads_ - minThreads_));
+ }
+}
+
+ThreadPool::ThreadPool()
+ : ThreadPool(std::max(std::thread::hardware_concurrency(), 4u))
+{}
+
+ThreadPool::~ThreadPool()
+{
+ join();
+}
+
+void
+ThreadPool::run(std::function<void()>&& cb)
+{
+ std::unique_lock<std::mutex> l(lock_);
+ if (not cb or not running_) return;
+
+ // launch new thread if necessary
+ if (not readyThreads_ && threads_.size() < maxThreads_) {
+ try {
+ bool permanent_thread = threads_.size() < minThreads_;
+ auto& thread = *threads_.emplace_back(std::make_unique<std::thread>());
+ thread = std::thread([this, permanent_thread, e=threadExpirationDelay, &thread]() {
+ while (true) {
+ std::function<void()> task;
+
+ // pick task from queue
+ {
+ std::unique_lock<std::mutex> l(lock_);
+ readyThreads_++;
+ auto waitCond = [&](){ return not running_ or not tasks_.empty(); };
+ if (permanent_thread)
+ cv_.wait(l, waitCond);
+ else
+ cv_.wait_for(l, e, waitCond);
+ readyThreads_--;
+ if (not running_ or tasks_.empty())
+ break;
+ task = std::move(tasks_.front());
+ tasks_.pop();
+ }
+
+ // run task
+ try {
+ task();
+ } catch (const std::exception& e) {
+ // LOG_ERR("Exception running task: %s", e.what());
+ std::cerr << "Exception running task: " << e.what() << std::endl;
+ }
+ }
+ if (not permanent_thread)
+ threadEnded(thread);
+ });
+ } catch(const std::exception& e) {
+ std::cerr << "Exception starting thread: " << e.what() << std::endl;
+ if (threads_.empty())
+ throw;
+ }
+ }
+
+ // push task to queue
+ tasks_.emplace(std::move(cb));
+
+ // notify thread
+ cv_.notify_one();
+}
+
+void
+ThreadPool::threadEnded(std::thread& thread)
+{
+ std::lock_guard<std::mutex> l(lock_);
+ tasks_.emplace([this,t=std::reference_wrapper<std::thread>(thread)]{
+ std::lock_guard<std::mutex> l(lock_);
+ for (auto it = threads_.begin(); it != threads_.end(); ++it) {
+ if (&*(*it) == &t.get()) {
+ t.get().join();
+ threads_.erase(it);
+ break;
+ }
+ }
+ });
+ // A thread expired, maybe after handling a one-time burst of tasks.
+ // If new threads start later, increase the expiration delay.
+ if (threadExpirationDelay > std::chrono::hours(24 * 7)) {
+ // If we reach 7 days, assume the thread is regularly used at full capacity
+ minThreads_ = std::min(minThreads_+1, maxThreads_);
+ } else {
+ threadExpirationDelay *= threadDelayRatio_;
+ }
+ cv_.notify_one();
+}
+
+void
+ThreadPool::stop(bool wait)
+{
+ std::unique_lock<std::mutex> l(lock_);
+ if (wait) {
+ cv_.wait(l, [&](){ return tasks_.empty(); });
+ }
+ running_ = false;
+ tasks_ = {};
+ cv_.notify_all();
+}
+
+void
+ThreadPool::join()
+{
+ stop();
+ for (auto& t : threads_)
+ t->join();
+ threads_.clear();
+ tasks_ = {};
+}
+
+void
+Executor::run(std::function<void()>&& task)
+{
+ std::lock_guard<std::mutex> l(lock_);
+ if (current_ < maxConcurrent_) {
+ run_(std::move(task));
+ } else {
+ tasks_.emplace(std::move(task));
+ }
+}
+
+void
+Executor::run_(std::function<void()>&& task)
+{
+ current_++;
+ std::weak_ptr<Executor> w = shared_from_this();
+ threadPool_.get().run([w,task = std::move(task)] {
+ try {
+ task();
+ } catch (const std::exception& e) {
+ std::cerr << "Exception running task: " << e.what() << std::endl;
+ }
+ if (auto sthis = w.lock()) {
+ auto& this_ = *sthis;
+ std::lock_guard<std::mutex> l(this_.lock_);
+ this_.current_--;
+ this_.schedule();
+ }
+ });
+}
+
+void
+Executor::schedule()
+{
+ if (not tasks_.empty() and current_ < maxConcurrent_) {
+ run_(std::move(tasks_.front()));
+ tasks_.pop();
+ }
+}
+
+}
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ * Author : Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "utils.h"
+#include "sockaddr.h"
+#include "default_types.h"
+
+/* An IPv4 equivalent to IN6_IS_ADDR_UNSPECIFIED */
+#ifndef IN_IS_ADDR_UNSPECIFIED
+#define IN_IS_ADDR_UNSPECIFIED(a) (((long int) (a)->s_addr) == 0x00000000)
+#endif /* IN_IS_ADDR_UNSPECIFIED */
+
+#ifndef PACKAGE_VERSION
+#define PACKAGE_VERSION "(unknown version)"
+#endif
+
+namespace dht {
+
+const char* version() {
+ return PACKAGE_VERSION;
+}
+
+const HexMap hex_map = {};
+
+static constexpr std::array<uint8_t, 12> MAPPED_IPV4_PREFIX {{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff}};
+
+std::pair<std::string, std::string>
+splitPort(const std::string& s) {
+ if (s.empty())
+ return {};
+ if (s[0] == '[') {
+ std::size_t closure = s.find_first_of(']');
+ std::size_t found = s.find_last_of(':');
+ if (closure == std::string::npos)
+ return {s, ""};
+ if (found == std::string::npos or found < closure)
+ return {s.substr(1,closure-1), ""};
+ return {s.substr(1,closure-1), s.substr(found+1)};
+ }
+ std::size_t found = s.find_last_of(':');
+ std::size_t first = s.find_first_of(':');
+ if (found == std::string::npos or found != first)
+ return {s, ""};
+ return {s.substr(0,found), s.substr(found+1)};
+}
+
+std::vector<SockAddr>
+SockAddr::resolve(const std::string& host, const std::string& service)
+{
+ std::vector<SockAddr> ips {};
+ if (host.empty())
+ return ips;
+
+ addrinfo hints;
+ memset(&hints, 0, sizeof(hints));
+ hints.ai_socktype = SOCK_DGRAM;
+ addrinfo* info = nullptr;
+ int rc = getaddrinfo(host.c_str(), service.empty() ? nullptr : service.c_str(), &hints, &info);
+ if(rc != 0)
+ throw std::invalid_argument(std::string("Error: `") + host + ":" + service + "`: " + gai_strerror(rc));
+
+ for (addrinfo* infop = info; infop; infop = infop->ai_next)
+ ips.emplace_back(infop->ai_addr, infop->ai_addrlen);
+ freeaddrinfo(info);
+ return ips;
+}
+
+void
+SockAddr::setAddress(const char* address)
+{
+ auto family = getFamily();
+ void* addr = nullptr;
+ switch (family) {
+ case AF_INET:
+ addr = &getIPv4().sin_addr;
+ break;
+ case AF_INET6:
+ addr = &getIPv6().sin6_addr;
+ break;
+ default:
+ throw std::runtime_error("Unknown address family");
+ }
+ if (inet_pton(family, address, addr) <= 0)
+ throw std::runtime_error(std::string("Can't parse IP address: ") + strerror(errno));
+}
+
+void print_addr(std::ostream& out, const sockaddr* sa, socklen_t slen)
+{
+ char hbuf[NI_MAXHOST];
+ char sbuf[NI_MAXSERV];
+ if (sa and slen and !getnameinfo(sa, slen, hbuf, sizeof(hbuf), sbuf, sizeof(sbuf), NI_NUMERICHOST | NI_NUMERICSERV)) {
+ if (sa->sa_family == AF_INET6)
+ out << '[' << hbuf << ']';
+ else
+ out << hbuf;
+ if (std::strcmp(sbuf, "0"))
+ out << ':' << sbuf;
+ } else
+ out << "[invalid address]";
+}
+
+std::string
+print_addr(const sockaddr* sa, socklen_t slen)
+{
+ std::ostringstream out;
+ print_addr(out, sa, slen);
+ return out.str();
+}
+
+std::string
+print_addr(const sockaddr_storage& ss, socklen_t sslen)
+{
+ return print_addr((const sockaddr*)&ss, sslen);
+}
+
+bool
+SockAddr::isUnspecified() const
+{
+ switch (getFamily()) {
+ case AF_INET:
+ return IN_IS_ADDR_UNSPECIFIED(&getIPv4().sin_addr);
+ case AF_INET6:
+ return IN6_IS_ADDR_UNSPECIFIED(reinterpret_cast<const in6_addr*>(&getIPv6().sin6_addr));
+ default:
+ return true;
+ }
+}
+
+bool
+SockAddr::isLoopback() const
+{
+ switch (getFamily()) {
+ case AF_INET: {
+ auto addr_host = ntohl(getIPv4().sin_addr.s_addr);
+ uint8_t b1 = (uint8_t)(addr_host >> 24);
+ return b1 == 127;
+ }
+ case AF_INET6:
+ return IN6_IS_ADDR_LOOPBACK(reinterpret_cast<const in6_addr*>(&getIPv6().sin6_addr));
+ default:
+ return false;
+ }
+}
+
+bool
+SockAddr::isPrivate() const
+{
+ if (isLoopback()) {
+ return true;
+ }
+ switch (getFamily()) {
+ case AF_INET: {
+ auto addr_host = ntohl(getIPv4().sin_addr.s_addr);
+ uint8_t b1, b2;
+ b1 = (uint8_t)(addr_host >> 24);
+ b2 = (uint8_t)((addr_host >> 16) & 0x0ff);
+ // 10.x.y.z
+ if (b1 == 10)
+ return true;
+ // 172.16.0.0 - 172.31.255.255
+ if ((b1 == 172) && (b2 >= 16) && (b2 <= 31))
+ return true;
+ // 192.168.0.0 - 192.168.255.255
+ if ((b1 == 192) && (b2 == 168))
+ return true;
+ return false;
+ }
+ case AF_INET6: {
+ const uint8_t* addr6 = reinterpret_cast<const uint8_t*>(&getIPv6().sin6_addr);
+ if (addr6[0] == 0xfc)
+ return true;
+ return false;
+ }
+ default:
+ return false;
+ }
+}
+
+bool
+SockAddr::isMappedIPv4() const
+{
+ if (getFamily() != AF_INET6)
+ return false;
+ const uint8_t* addr6 = reinterpret_cast<const uint8_t*>(&getIPv6().sin6_addr);
+ return std::equal(MAPPED_IPV4_PREFIX.begin(), MAPPED_IPV4_PREFIX.end(), addr6);
+}
+
+SockAddr
+SockAddr::getMappedIPv4()
+{
+ if (not isMappedIPv4())
+ return std::move(*this);
+ SockAddr ret;
+ ret.setFamily(AF_INET);
+ ret.setPort(getPort());
+ auto addr6 = reinterpret_cast<const uint8_t*>(&getIPv6().sin6_addr);
+ auto addr4 = reinterpret_cast<uint8_t*>(&ret.getIPv4().sin_addr);
+ addr6 += MAPPED_IPV4_PREFIX.size();
+ std::copy_n(addr6, sizeof(in_addr), addr4);
+ return ret;
+}
+
+SockAddr
+SockAddr::getMappedIPv6()
+{
+ auto family = getFamily();
+ if (family != AF_INET)
+ return std::move(*this);
+ SockAddr ret;
+ ret.setFamily(AF_INET6);
+ ret.setPort(getPort());
+ auto addr4 = reinterpret_cast<const uint8_t*>(&getIPv4().sin_addr);
+ auto addr6 = reinterpret_cast<uint8_t*>(&ret.getIPv6().sin6_addr);
+ std::copy(MAPPED_IPV4_PREFIX.begin(), MAPPED_IPV4_PREFIX.end(), addr6);
+ std::copy_n(addr4, sizeof(in_addr), addr6 + MAPPED_IPV4_PREFIX.size());
+ return ret;
+}
+
+bool operator==(const SockAddr& a, const SockAddr& b) {
+ return a.equals(b);
+}
+
+time_point from_time_t(std::time_t t) {
+ auto dt = system_clock::from_time_t(t) - system_clock::now();
+ auto now = clock::now();
+ if (dt > system_clock::duration(0) and now > time_point::max() - dt)
+ return time_point::max();
+ else if (dt < system_clock::duration(0) and now < time_point::min() - dt)
+ return time_point::min();
+ return now + dt;
+}
+
+std::time_t to_time_t(time_point t) {
+ auto dt = t - clock::now();
+ auto now = system_clock::now();
+ if (dt > duration(0) and now >= system_clock::time_point::max() - dt)
+ return system_clock::to_time_t(system_clock::time_point::max());
+ else if (dt < duration(0) and now <= system_clock::time_point::min() - dt)
+ return system_clock::to_time_t(system_clock::time_point::min());
+ return system_clock::to_time_t(now + std::chrono::duration_cast<system_clock::duration>(dt));
+}
+
+Blob
+unpackBlob(const msgpack::object& o) {
+ switch (o.type) {
+ case msgpack::type::BIN:
+ return {o.via.bin.ptr, o.via.bin.ptr+o.via.bin.size};
+ case msgpack::type::STR:
+ return {o.via.str.ptr, o.via.str.ptr+o.via.str.size};
+ case msgpack::type::ARRAY: {
+ Blob ret(o.via.array.size);
+ std::transform(o.via.array.ptr, o.via.array.ptr+o.via.array.size, ret.begin(), [](const msgpack::object& b) {
+ return b.as<uint8_t>();
+ });
+ return ret;
+ }
+ default:
+ throw msgpack::type_error();
+ }
+}
+
+msgpack::unpacked
+unpackMsg(Blob b) {
+ return msgpack::unpack((const char*)b.data(), b.size());
+}
+
+msgpack::object*
+findMapValue(const msgpack::object& map, const char* key, size_t key_length) {
+ if (map.type != msgpack::type::MAP) throw msgpack::type_error();
+ for (unsigned i = 0; i < map.via.map.size; i++) {
+ auto& o = map.via.map.ptr[i];
+ if (o.key.type == msgpack::type::STR
+ && key_length == o.key.via.str.size
+ && std::strncmp(o.key.via.str.ptr, key, o.key.via.str.size) == 0)
+ return &o.val;
+ }
+ return nullptr;
+}
+
+}
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ * Author(s) : Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ * Simon Désaulniers <simon.desaulniers@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "value.h"
+
+#include "default_types.h"
+#include "securedht.h" // print certificate ID
+
+#ifdef OPENDHT_JSONCPP
+#include "base64.h"
+#endif
+
+
+namespace dht {
+
+const std::string Query::QUERY_PARSE_ERROR {"Error parsing query."};
+
+Value::Filter bindFilterRaw(FilterRaw raw_filter, void* user_data) {
+ if (not raw_filter) return {};
+ return [=](const Value& value) {
+ return raw_filter(value, user_data);
+ };
+}
+
+std::ostream& operator<< (std::ostream& s, const Value& v)
+{
+ auto flags(s.flags());
+ s << "Value[id:" << std::hex << v.id << std::dec << ' ';
+ if (v.isEncrypted())
+ s << "encrypted ";
+ else if (v.isSigned()) {
+ s << "signed (v" << v.seq << ") ";
+ if (v.recipient)
+ s << "decrypted ";
+ }
+ if (not v.isEncrypted()) {
+ if (v.type == IpServiceAnnouncement::TYPE.id) {
+ s << IpServiceAnnouncement(v.data);
+ } else if (v.type == CERTIFICATE_TYPE.id) {
+ s << "Certificate";
+#ifdef OPENDHT_LOG_CRT_ID
+ try {
+ auto h = crypto::Certificate(v.data).getPublicKey().getLongId();
+ s << " with ID " << h;
+ } catch (const std::exception& e) {
+ s << " (invalid)";
+ }
+#endif
+ } else {
+ if (v.user_type.empty())
+ s << "data:";
+ else
+ s << "data(" << v.user_type << "):";
+ if (v.user_type == "text/plain") {
+ s << '"';
+ s.write((const char*)v.data.data(), v.data.size());
+ s << '"';
+ } else if (v.data.size() < 1024) {
+ s << toHex(v.data.data(), v.data.size());
+ } else {
+ s << v.data.size() << " bytes";
+ }
+ }
+ }
+ s << ']';
+ s.flags(flags);
+ return s;
+}
+
+const ValueType ValueType::USER_DATA = {0, "User Data"};
+
+bool
+ValueType::DEFAULT_STORE_POLICY(InfoHash, const std::shared_ptr<Value>& v, const InfoHash&, const SockAddr&)
+{
+ return v->size() <= MAX_VALUE_SIZE;
+}
+
+size_t
+Value::size() const
+{
+ return cypher.size() + data.size() + signature.size() + user_type.size();
+}
+
+void
+Value::msgpack_unpack(const msgpack::object& o)
+{
+ if (o.type != msgpack::type::MAP or o.via.map.size < 2)
+ throw msgpack::type_error();
+
+ if (auto rid = findMapValue(o, VALUE_KEY_ID)) {
+ id = rid->as<Id>();
+ } else
+ throw msgpack::type_error();
+
+ if (auto rdat = findMapValue(o, VALUE_KEY_DAT)) {
+ msgpack_unpack_body(*rdat);
+ } else
+ throw msgpack::type_error();
+
+ if (auto rprio = findMapValue(o, VALUE_KEY_PRIO)) {
+ priority = rprio->as<unsigned>();
+ }
+}
+
+void
+Value::msgpack_unpack_body(const msgpack::object& o)
+{
+ owner = {};
+ recipient = {};
+ cypher.clear();
+ signature.clear();
+ data.clear();
+ type = 0;
+
+ if (o.type == msgpack::type::BIN) {
+ auto dat = o.as<std::vector<char>>();
+ cypher = {dat.begin(), dat.end()};
+ } else {
+ if (o.type != msgpack::type::MAP)
+ throw msgpack::type_error();
+ auto rbody = findMapValue(o, VALUE_KEY_BODY);
+ if (not rbody)
+ throw msgpack::type_error();
+
+ if (auto rdata = findMapValue(*rbody, VALUE_KEY_DATA)) {
+ data = unpackBlob(*rdata);
+ } else
+ throw msgpack::type_error();
+
+ if (auto rtype = findMapValue(*rbody, VALUE_KEY_TYPE)) {
+ type = rtype->as<ValueType::Id>();
+ } else
+ throw msgpack::type_error();
+
+ if (auto rutype = findMapValue(*rbody, VALUE_KEY_USERTYPE)) {
+ user_type = rutype->as<std::string>();
+ }
+
+ if (auto rowner = findMapValue(*rbody, VALUE_KEY_OWNER)) {
+ if (auto rseq = findMapValue(*rbody, VALUE_KEY_SEQ))
+ seq = rseq->as<decltype(seq)>();
+ else
+ throw msgpack::type_error();
+ crypto::PublicKey new_owner;
+ new_owner.msgpack_unpack(*rowner);
+ owner = std::make_shared<crypto::PublicKey>(std::move(new_owner));
+ if (auto rrecipient = findMapValue(*rbody, VALUE_KEY_TO)) {
+ recipient = rrecipient->as<InfoHash>();
+ }
+
+ if (auto rsig = findMapValue(o, VALUE_KEY_SIGNATURE)) {
+ signature = unpackBlob(*rsig);
+ } else
+ throw msgpack::type_error();
+ }
+ }
+}
+
+#ifdef OPENDHT_JSONCPP
+Value::Value(const Json::Value& json)
+{
+ id = Value::Id(unpackId(json, VALUE_KEY_ID));
+ const auto& jcypher = json["cypher"];
+ if (jcypher.isString())
+ cypher = base64_decode(jcypher.asString());
+ const auto& jsig = json[VALUE_KEY_SIGNATURE];
+ if (jsig.isString())
+ signature = base64_decode(jsig.asString());
+ const auto& jseq = json[VALUE_KEY_SEQ];
+ if (!jseq.isNull())
+ seq = jseq.asInt();
+ const auto& jowner = json[VALUE_KEY_OWNER];
+ if (jowner.isString()) {
+ auto ownerStr = jowner.asString();
+ auto ownerBlob = std::vector<unsigned char>(ownerStr.begin(), ownerStr.end());
+ owner = std::make_shared<crypto::PublicKey>(ownerBlob);
+ }
+ const auto& jto = json[VALUE_KEY_TO];
+ if (jto.isString())
+ recipient = InfoHash(jto.asString());
+ const auto& jtype = json[VALUE_KEY_TYPE];
+ if (!jtype.isNull())
+ type = jtype.asInt();
+ const auto& jdata = json[VALUE_KEY_DATA];
+ if (jdata.isString())
+ data = base64_decode(jdata.asString());
+ const auto& jutype = json[VALUE_KEY_USERTYPE];
+ if (jutype.isString())
+ user_type = jutype.asString();
+ const auto& jprio = json["prio"];
+ if (jprio.isIntegral())
+ priority = jprio.asUInt();
+}
+
+Json::Value
+Value::toJson() const
+{
+ Json::Value val;
+ val[VALUE_KEY_ID] = std::to_string(id);
+ if (isEncrypted()) {
+ val["cypher"] = base64_encode(cypher);
+ } else {
+ if (isSigned())
+ val[VALUE_KEY_SIGNATURE] = base64_encode(signature);
+ bool has_owner = owner && *owner;
+ if (has_owner) { // isSigned
+ val[VALUE_KEY_SEQ] = seq;
+ val[VALUE_KEY_OWNER] = owner->toString();
+ if (recipient)
+ val[VALUE_KEY_TO] = recipient.toString();
+ }
+ val[VALUE_KEY_TYPE] = type;
+ val[VALUE_KEY_DATA] = base64_encode(data);
+ if (not user_type.empty())
+ val[VALUE_KEY_USERTYPE] = user_type;
+ }
+ if (priority)
+ val["prio"] = priority;
+ return val;
+}
+
+uint64_t
+unpackId(const Json::Value& json, const std::string& key) {
+ uint64_t ret = 0;
+ try {
+ const auto& t = json[key];
+ if (t.isString()) {
+ ret = std::stoull(t.asString());
+ } else {
+ ret = t.asLargestUInt();
+ }
+ } catch (...) {}
+ return ret;
+}
+#endif
+
+void
+Value::sign(const crypto::PrivateKey& key)
+{
+ if (isEncrypted())
+ throw DhtException("Can't sign encrypted data.");
+ owner = key.getSharedPublicKey();
+ signature = key.sign(getToSign());
+}
+
+Value
+Value::encrypt(const crypto::PrivateKey& from, const crypto::PublicKey& to)
+{
+ if (isEncrypted())
+ throw DhtException("Data is already encrypted.");
+ setRecipient(to.getId());
+ sign(from);
+ Value nv {id};
+ nv.setCypher(to.encrypt(getToEncrypt()));
+ return nv;
+}
+
+bool
+Value::checkSignature()
+{
+ if (!signatureChecked) {
+ signatureChecked = true;
+ if (isSigned()) {
+ signatureValid = owner and owner->checkSignature(getToSign(), signature);
+ } else {
+ signatureValid = true;
+ }
+ }
+ return signatureValid;
+}
+
+Sp<Value>
+Value::decrypt(const crypto::PrivateKey& key)
+{
+ if (not decrypted) {
+ decrypted = true;
+ if (isEncrypted()) {
+ auto decryptedBlob = key.decrypt(cypher);
+ auto msg = msgpack::unpack((const char*)decryptedBlob.data(), decryptedBlob.size());
+ auto v = std::make_unique<Value>(id);
+ v->msgpack_unpack_body(msg.get());
+ if (v->recipient != key.getPublicKey().getId())
+ throw crypto::DecryptError("Recipient mismatch");
+ // Ignore values belonging to other people
+ if (not v->owner or not v->owner->checkSignature(v->getToSign(), v->signature))
+ throw crypto::DecryptError("Signature mismatch");
+ decryptedValue = std::move(v);
+ }
+ }
+ return decryptedValue;
+}
+
+bool
+FieldValue::operator==(const FieldValue& vfd) const
+{
+ if (field != vfd.field)
+ return false;
+ switch (field) {
+ case Value::Field::Id:
+ case Value::Field::ValueType:
+ case Value::Field::SeqNum:
+ return intValue == vfd.intValue;
+ case Value::Field::OwnerPk:
+ return hashValue == vfd.hashValue;
+ case Value::Field::UserType:
+ return blobValue == vfd.blobValue;
+ case Value::Field::None:
+ return true;
+ default:
+ return false;
+ }
+}
+
+Value::Filter
+FieldValue::getLocalFilter() const
+{
+ switch (field) {
+ case Value::Field::Id:
+ return Value::IdFilter(intValue);
+ case Value::Field::ValueType:
+ return Value::TypeFilter(intValue);
+ case Value::Field::OwnerPk:
+ return Value::OwnerFilter(hashValue);
+ case Value::Field::SeqNum:
+ return Value::SeqNumFilter(intValue);
+ case Value::Field::UserType:
+ return Value::UserTypeFilter(std::string {blobValue.begin(), blobValue.end()});
+ default:
+ return {};
+ }
+}
+
+FieldValueIndex::FieldValueIndex(const Value& v, const Select& s)
+{
+ if (not s.empty()) {
+ auto selection = s.getSelection();
+ std::transform(selection.begin(), selection.end(), std::inserter(index, index.end()),
+ [](const std::set<Value::Field>::value_type& f) {
+ return std::make_pair(f, FieldValue {});
+ });
+ } else {
+ index.clear();
+ for (size_t f = 1 ; f < static_cast<int>(Value::Field::COUNT) ; ++f)
+ index[static_cast<Value::Field>(f)] = {};
+ }
+ for (const auto& fvp : index) {
+ const auto& f = fvp.first;
+ switch (f) {
+ case Value::Field::Id:
+ index[f] = {f, v.id};
+ break;
+ case Value::Field::ValueType:
+ index[f] = {f, v.type};
+ break;
+ case Value::Field::OwnerPk:
+ index[f] = {f, v.owner ? v.owner->getId() : InfoHash() };
+ break;
+ case Value::Field::SeqNum:
+ index[f] = {f, v.seq};
+ break;
+ case Value::Field::UserType:
+ index[f] = {f, Blob {v.user_type.begin(), v.user_type.end()}};
+ break;
+ default:
+ break;
+ }
+ }
+}
+
+bool FieldValueIndex::containedIn(const FieldValueIndex& other) const {
+ if (index.size() > other.index.size())
+ return false;
+ for (const auto& field : index) {
+ auto other_field = other.index.find(field.first);
+ if (other_field == other.index.end())
+ return false;
+ }
+ return true;
+}
+
+std::ostream& operator<<(std::ostream& os, const FieldValueIndex& fvi) {
+ os << "Index[";
+ for (auto v = fvi.index.begin(); v != fvi.index.end(); ++v) {
+ switch (v->first) {
+ case Value::Field::Id: {
+ auto flags(os.flags());
+ os << "Id:" << std::hex << v->second.getInt();
+ os.flags(flags);
+ break;
+ }
+ case Value::Field::ValueType:
+ os << "ValueType:" << v->second.getInt();
+ break;
+ case Value::Field::OwnerPk:
+ os << "Owner:" << v->second.getHash();
+ break;
+ case Value::Field::SeqNum:
+ os << "Seq:" << v->second.getInt();
+ break;
+ case Value::Field::UserType: {
+ auto ut = v->second.getBlob();
+ os << "UserType:" << std::string(ut.begin(), ut.end());
+ break;
+ }
+ default:
+ break;
+ }
+ os << (std::next(v) != fvi.index.end() ? "," : "");
+ }
+ return os << "]";
+}
+
+void
+FieldValueIndex::msgpack_unpack_fields(const std::set<Value::Field>& fields, const msgpack::object& o, unsigned offset)
+{
+ index.clear();
+
+ unsigned j = 0;
+ for (const auto& field : fields) {
+ auto& field_value = o.via.array.ptr[offset+(j++)];
+ switch (field) {
+ case Value::Field::Id:
+ case Value::Field::ValueType:
+ case Value::Field::SeqNum:
+ index[field] = FieldValue(field, field_value.as<uint64_t>());
+ break;
+ case Value::Field::OwnerPk:
+ index[field] = FieldValue(field, field_value.as<InfoHash>());
+ break;
+ case Value::Field::UserType:
+ index[field] = FieldValue(field, field_value.as<Blob>());
+ break;
+ default:
+ throw msgpack::type_error();
+ }
+ }
+}
+
+void trim_str(std::string& str) {
+ auto first = std::min(str.size(), str.find_first_not_of(' '));
+ auto last = std::min(str.size(), str.find_last_not_of(' '));
+ str = str.substr(first, last - first + 1);
+}
+
+Select::Select(std::string_view q_str) {
+ std::istringstream q_iss {std::string(q_str)};
+ std::string token {};
+ q_iss >> token;
+
+ if (token == "SELECT" or token == "select") {
+ q_iss >> token;
+ std::istringstream fields {token};
+
+ while (std::getline(fields, token, ',')) {
+ trim_str(token);
+ if (token == VALUE_KEY_ID)
+ field(Value::Field::Id);
+ else if (token == "value_type")
+ field(Value::Field::ValueType);
+ else if (token == "owner_pk")
+ field(Value::Field::OwnerPk);
+ if (token == VALUE_KEY_SEQ)
+ field(Value::Field::SeqNum);
+ else if (token == "user_type")
+ field(Value::Field::UserType);
+ }
+ }
+}
+
+Where::Where(std::string_view q_str) {
+ std::istringstream q_iss {std::string(q_str)};
+ std::string token {};
+ q_iss >> token;
+ if (token == "WHERE" or token == "where") {
+ std::getline(q_iss, token);
+ std::istringstream restrictions {token};
+ while (std::getline(restrictions, token, ',')) {
+ trim_str(token);
+ std::istringstream eq_ss {token};
+ std::string field_str, value_str;
+ std::getline(eq_ss, field_str, '=');
+ trim_str(field_str);
+ std::getline(eq_ss, value_str, '=');
+ trim_str(value_str);
+
+ if (not value_str.empty()) {
+ uint64_t v = 0;
+ std::string s {};
+ std::istringstream convert {value_str};
+ convert >> v;
+ if (not convert
+ and value_str.size() > 1
+ and value_str[0] == '"'
+ and value_str[value_str.size()-1] == '"')
+ s = value_str.substr(1, value_str.size()-2);
+ else
+ s = value_str;
+ if (field_str == VALUE_KEY_ID)
+ id(v);
+ else if (field_str == "value_type")
+ valueType(v);
+ else if (field_str == "owner_pk")
+ owner(InfoHash(s));
+ else if (field_str == VALUE_KEY_SEQ)
+ seq(v);
+ else if (field_str == "user_type")
+ userType(s);
+ else
+ throw std::invalid_argument(Query::QUERY_PARSE_ERROR + " (WHERE) wrong token near: " + field_str);
+ }
+ }
+ }
+}
+
+void
+Query::msgpack_unpack(const msgpack::object& o)
+{
+ if (o.type != msgpack::type::MAP)
+ throw msgpack::type_error();
+
+ auto rfilters = findMapValue(o, "w"sv); /* unpacking filters */
+ if (rfilters)
+ where.msgpack_unpack(*rfilters);
+ else
+ throw msgpack::type_error();
+
+ auto rfield_selector = findMapValue(o, "s"sv); /* unpacking field selectors */
+ if (rfield_selector)
+ select.msgpack_unpack(*rfield_selector);
+ else
+ throw msgpack::type_error();
+}
+
+template <typename T>
+bool subset(const std::vector<T>& fds, const std::vector<T>& qfds)
+{
+ for (const auto& fd : fds) {
+ if (std::find_if(qfds.begin(), qfds.end(), [&fd](const T& _vfd) { return fd == _vfd; }) == qfds.end())
+ return false;
+ }
+ return true;
+}
+
+bool Select::isSatisfiedBy(const Select& os) const {
+ /* empty, means all values are selected. */
+ return fieldSelection_.empty() ?
+ os.fieldSelection_.empty() :
+ subset(fieldSelection_, os.fieldSelection_);
+}
+
+bool Where::isSatisfiedBy(const Where& ow) const {
+ return subset(ow.filters_, filters_);
+}
+
+bool Query::isSatisfiedBy(const Query& q) const {
+ return none or (where.isSatisfiedBy(q.where) and select.isSatisfiedBy(q.select));
+}
+
+std::ostream& operator<<(std::ostream& s, const dht::Select& select) {
+ s << "SELECT ";
+ if (select.fieldSelection_.empty())
+ s << '*';
+ else
+ for (auto fs = select.fieldSelection_.begin(); fs != select.fieldSelection_.end();) {
+ switch (*fs) {
+ case Value::Field::Id:
+ s << VALUE_KEY_ID;
+ break;
+ case Value::Field::ValueType:
+ s << "value_type";
+ break;
+ case Value::Field::UserType:
+ s << "user_type";
+ break;
+ case Value::Field::OwnerPk:
+ s << "owner_public_key";
+ break;
+ case Value::Field::SeqNum:
+ s << VALUE_KEY_SEQ;
+ break;
+ default:
+ break;
+ }
+ if (++fs != select.fieldSelection_.end())
+ s << ',';
+ }
+ return s;
+}
+
+std::ostream& operator<<(std::ostream& s, const dht::Where& where) {
+ if (not where.filters_.empty()) {
+ s << "WHERE ";
+ for (auto f = where.filters_.begin() ; f != where.filters_.end() ; ++f) {
+ switch (f->getField()) {
+ case Value::Field::Id:
+ s << VALUE_KEY_ID << '=' << f->getInt();
+ break;
+ case Value::Field::ValueType:
+ s << "value_type=" << f->getInt();
+ break;
+ case Value::Field::OwnerPk:
+ s << "owner_pk_hash=" << f->getHash();
+ break;
+ case Value::Field::SeqNum:
+ s << VALUE_KEY_SEQ << '=' << f->getInt();
+ break;
+ case Value::Field::UserType: {
+ auto b = f->getBlob();
+ s << "user_type=" << std::string {b.begin(), b.end()};
+ break;
+ }
+ default:
+ break;
+ }
+ s << (std::next(f) != where.filters_.end() ? "," : "");
+ }
+ }
+ return s;
+}
+
+
+}
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ * Author(s) : Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+#pragma once
+
+#include "value.h"
+
+namespace dht {
+
+using ValueStateCallback = std::function<void(const std::vector<Sp<Value>>&, bool)>;
+enum class ListenSyncStatus { ADDED, SYNCED, UNSYNCED, REMOVED };
+using SyncCallback = std::function<void(ListenSyncStatus)>;
+using CallbackQueue = std::list<std::function<void()>>;
+
+class ValueCache {
+public:
+ explicit ValueCache(ValueStateCallback&& cb, SyncCallback&& scb = {})
+ : callback(std::forward<ValueStateCallback>(cb)), syncCallback(std::move(scb))
+ {
+ if (syncCallback)
+ syncCallback(ListenSyncStatus::ADDED);
+ }
+ ValueCache(ValueCache&& o) noexcept : values(std::move(o.values)), callback(std::move(o.callback)), syncCallback(std::move(o.syncCallback)) {
+ o.callback = {};
+ o.syncCallback = {};
+ }
+
+ ~ValueCache() {
+ auto q = clear();
+ for (auto& cb: q)
+ cb();
+ if (syncCallback) {
+ if (status == ListenSyncStatus::SYNCED)
+ syncCallback(ListenSyncStatus::UNSYNCED);
+ syncCallback(ListenSyncStatus::REMOVED);
+ }
+ }
+
+ CallbackQueue clear() {
+ std::vector<Sp<Value>> expired_values;
+ expired_values.reserve(values.size());
+ for (auto& v : values)
+ expired_values.emplace_back(std::move(v.second.data));
+ values.clear();
+ CallbackQueue ret;
+ if (not expired_values.empty() and callback) {
+ ret.emplace_back([expired_values = std::move(expired_values), cb = callback]{
+ cb(expired_values, true);
+ });
+ }
+ return ret;
+ }
+
+ time_point expireValues(const time_point& now) {
+ time_point ret = time_point::max();
+ auto cbs = expireValues(now, ret);
+ while (not cbs.empty()) {
+ cbs.front()();
+ cbs.pop_front();
+ }
+ return ret;
+ }
+
+ CallbackQueue expireValues(const time_point& now, time_point& next) {
+ std::vector<Sp<Value>> expired_values;
+ for (auto it = values.begin(); it != values.end();) {
+ if (it->second.expiration <= now) {
+ expired_values.emplace_back(std::move(it->second.data));
+ it = values.erase(it);
+ } else {
+ next = std::min(next, it->second.expiration);
+ ++it;
+ }
+ }
+ while (values.size() > MAX_VALUES) {
+ // too many values, remove oldest values
+ time_point oldest_creation = time_point::max();
+ auto oldest_value = values.end();
+ for (auto it = values.begin(); it != values.end(); ++it)
+ if (it->second.created < oldest_creation) {
+ oldest_value = it;
+ oldest_creation = it->second.created;
+ }
+ if (oldest_value != values.end()) {
+ expired_values.emplace_back(std::move(oldest_value->second.data));
+ values.erase(oldest_value);
+ }
+ }
+ CallbackQueue ret;
+ if (not expired_values.empty() and callback) {
+ ret.emplace_back([cb = callback, expired_values = std::move(expired_values)]{
+ cb(expired_values, true);
+ });
+ }
+ return ret;
+ }
+
+ time_point onValues
+ (const std::vector<Sp<Value>>& new_values,
+ const std::vector<Value::Id>& refreshed_values,
+ const std::vector<Value::Id>& expired_values,
+ const TypeStore& types, const time_point& now)
+ {
+ CallbackQueue cbs;
+ time_point ret = time_point::max();
+ if (not new_values.empty())
+ cbs.splice(cbs.end(), addValues(new_values, types, now));
+ for (const auto& vid : refreshed_values)
+ refreshValue(vid, types, now);
+ for (const auto& vid : expired_values)
+ cbs.splice(cbs.end(), expireValue(vid));
+ cbs.splice(cbs.end(), expireValues(now, ret));
+ while (not cbs.empty()) {
+ cbs.front()();
+ cbs.pop_front();
+ }
+ return ret;
+ }
+
+ void onSynced(bool synced) {
+ auto newStatus = synced ? ListenSyncStatus::SYNCED : ListenSyncStatus::UNSYNCED;
+ if (status != newStatus) {
+ status = newStatus;
+ if (syncCallback)
+ syncCallback(newStatus);
+ }
+ }
+
+ size_t size() const {
+ return values.size();
+ }
+
+private:
+ // prevent copy
+ ValueCache(const ValueCache&) = delete;
+ ValueCache& operator=(const ValueCache&) = delete;
+ ValueCache& operator=(ValueCache&&) = delete;
+
+ /* The maximum number of values we store in the cache. */
+ static constexpr unsigned MAX_VALUES {4096};
+
+ struct CacheValueStorage {
+ Sp<Value> data {};
+ time_point created {};
+ time_point expiration {};
+
+ CacheValueStorage() {}
+ CacheValueStorage(const Sp<Value>& v, time_point t, time_point e)
+ : data(v), created(t), expiration(e) {}
+ };
+
+ std::map<Value::Id, CacheValueStorage> values;
+ ValueStateCallback callback;
+ SyncCallback syncCallback;
+ ListenSyncStatus status {ListenSyncStatus::UNSYNCED};
+
+ CallbackQueue addValues(const std::vector<Sp<Value>>& new_values, const TypeStore& types, const time_point& now) {
+ std::vector<Sp<Value>> nvals;
+ for (const auto& value : new_values) {
+ auto v = values.find(value->id);
+ if (v == values.end()) {
+ // new value
+ nvals.emplace_back(value);
+ values.emplace(value->id, CacheValueStorage(value, now, now + types.getType(value->type).expiration));
+ } else {
+ // refreshed value
+ v->second.created = now;
+ v->second.expiration = now + types.getType(v->second.data->type).expiration;
+ }
+ }
+ CallbackQueue ret;
+ if (callback and not nvals.empty())
+ ret.emplace_back([cb = callback, nvals = std::move(nvals)]{
+ cb(nvals, false);
+ });
+ return ret;
+ }
+ CallbackQueue expireValue(Value::Id vid) {
+ auto v = values.find(vid);
+ if (v == values.end())
+ return {};
+ std::vector<Sp<Value>> val {std::move(v->second.data)};
+ values.erase(v);
+ CallbackQueue ret;
+ ret.emplace_back([cb = callback, val = std::move(val)]{
+ if (cb) cb(val, true);
+ });
+ return ret;
+ }
+ void refreshValue(Value::Id vid, const TypeStore& types, const time_point& now) {
+ auto v = values.find(vid);
+ if (v == values.end())
+ return;
+ v->second.created = now;
+ v->second.expiration = now + types.getType(v->second.data->type).expiration;
+ }
+};
+
+}
--- /dev/null
+if ENABLE_TESTS
+bin_PROGRAMS = opendht_unit_tests
+
+AM_CPPFLAGS = -I../include -DOPENDHT_JSONCPP
+
+nobase_include_HEADERS = infohashtester.h valuetester.h cryptotester.h dhtrunnertester.h httptester.h dhtproxytester.h
+opendht_unit_tests_SOURCES = tests_runner.cpp cryptotester.cpp infohashtester.cpp valuetester.cpp dhtrunnertester.cpp httptester.cpp dhtproxytester.cpp
+opendht_unit_tests_LDFLAGS = -lopendht -lcppunit -ljsoncpp -L@top_builddir@/src/.libs @GnuTLS_LIBS@
+endif
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ *
+ * Author: Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ * Vsevolod Ivanov <vsevolod.ivanov@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "cryptotester.h"
+
+#include <opendht/crypto.h>
+
+namespace test {
+CPPUNIT_TEST_SUITE_REGISTRATION(CryptoTester);
+
+void
+CryptoTester::setUp() {
+
+}
+
+void
+CryptoTester::testSignatureEncryption() {
+ auto key = dht::crypto::PrivateKey::generate();
+ const auto& public_key = key.getPublicKey();
+
+ std::vector<uint8_t> data1 {5, 10};
+ std::vector<uint8_t> data2(64 * 1024, 10);
+
+ std::vector<uint8_t> signature1 = key.sign(data1);
+ std::vector<uint8_t> signature2 = key.sign(data2);
+
+ // check signature
+ CPPUNIT_ASSERT(public_key.checkSignature(data1, signature1));
+ CPPUNIT_ASSERT(public_key.checkSignature(data2, signature2));
+ signature1[7]++;
+ signature2[8]--;
+ CPPUNIT_ASSERT(!public_key.checkSignature(data1, signature1));
+ CPPUNIT_ASSERT(!public_key.checkSignature(data2, signature2));
+
+ // encrypt data
+ {
+ std::vector<uint8_t> encrypted = public_key.encrypt(data1);
+ std::vector<uint8_t> decrypted = key.decrypt(encrypted);
+ CPPUNIT_ASSERT(data1 == decrypted);
+ }
+
+ {
+ std::vector<uint8_t> encrypted = public_key.encrypt(data2);
+ std::vector<uint8_t> decrypted = key.decrypt(encrypted);
+ CPPUNIT_ASSERT(data2 == decrypted);
+ }
+
+ // encrypt data (invalid)
+ {
+ std::vector<uint8_t> encrypted = public_key.encrypt(data1);
+ encrypted[1]++;
+ CPPUNIT_ASSERT_THROW(key.decrypt(encrypted), std::runtime_error);
+ }
+
+ {
+ std::vector<uint8_t> encrypted = public_key.encrypt(data2);
+ encrypted[2]++;
+ CPPUNIT_ASSERT_THROW(key.decrypt(encrypted), std::runtime_error);
+ }
+}
+
+void
+CryptoTester::testCertificateRevocation()
+{
+ auto ca1 = dht::crypto::generateIdentity("ca1");
+ auto account1 = dht::crypto::generateIdentity("acc1", ca1, 4096, true);
+ auto device11 = dht::crypto::generateIdentity("dev11", account1);
+ auto device12 = dht::crypto::generateIdentity("dev12", account1);
+
+ dht::crypto::TrustList list;
+ list.add(*ca1.second);
+ auto v = list.verify(*account1.second);
+ CPPUNIT_ASSERT_MESSAGE(v.toString(), v);
+
+ list.add(*account1.second);
+ v = list.verify(*device11.second);
+ CPPUNIT_ASSERT_MESSAGE(v.toString(), v);
+ v = list.verify(*device12.second);
+ CPPUNIT_ASSERT_MESSAGE(v.toString(), v);
+
+ auto ca2 = dht::crypto::generateIdentity("ca2");
+ auto account2 = dht::crypto::generateIdentity("acc2", ca2, 4096, true);
+ auto device2 = dht::crypto::generateIdentity("dev2", account2);
+
+ v = list.verify(*device2.second);
+ CPPUNIT_ASSERT_MESSAGE(v.toString(), !v);
+
+ account1.second->revoke(*account1.first, *device11.second);
+ dht::crypto::TrustList list2;
+ list2.add(*account1.second);
+
+ v = list2.verify(*device11.second);
+ CPPUNIT_ASSERT_MESSAGE(v.toString(), !v);
+ v = list2.verify(*device12.second);
+ CPPUNIT_ASSERT_MESSAGE(v.toString(), v);
+}
+
+void
+CryptoTester::testCertificateRequest()
+{
+ // Generate CA
+ auto ca = dht::crypto::generateIdentity("Test CA");
+
+ // Generate signed request
+ auto deviceKey = dht::crypto::PrivateKey::generate();
+ auto request = dht::crypto::CertificateRequest();
+ request.setName("Test Device");
+ request.sign(deviceKey);
+
+ // Export/import request
+ auto importedRequest = dht::crypto::CertificateRequest(request.pack());
+ CPPUNIT_ASSERT(importedRequest.verify());
+
+ // Generate/sign certificate from request
+ auto signedCert = dht::crypto::Certificate::generate(request, ca);
+ CPPUNIT_ASSERT_EQUAL(ca.second->getName(), signedCert.getIssuerName());
+ CPPUNIT_ASSERT_EQUAL(request.getName(), signedCert.getName());
+
+ // Check generated certificate
+ dht::crypto::TrustList list;
+ list.add(*ca.second);
+ auto v = list.verify(signedCert);
+ CPPUNIT_ASSERT_MESSAGE(v.toString(), v);
+}
+
+void CryptoTester::testCertificateSerialNumber()
+{
+ static const std::string cert_pem = "-----BEGIN CERTIFICATE-----"
+"MIICDjCCAZSgAwIBAgIIS90uAKp+u/swCgYIKoZIzj0EAwMwTDEQMA4GA1UEAxMH"
+"ZGh0bm9kZTE4MDYGCgmSJomT8ixkAQETKDBlNDQxZTA4YWJmYTQzYTc3ZTVjZDBm"
+"Y2QzMzAzMTc4MjYxMTk0MzIwHhcNMTkxMTA3MDA0MTMwWhcNMjkxMTA0MDA0MTMw"
+"WjBMMRAwDgYDVQQDEwdkaHRub2RlMTgwNgYKCZImiZPyLGQBARMoMGU0NDFlMDhh"
+"YmZhNDNhNzdlNWNkMGZjZDMzMDMxNzgyNjExOTQzMjB2MBAGByqGSM49AgEGBSuB"
+"BAAiA2IABCZ7sBp0Pu+b5yIifoNXchU9crv9won0in++COWynvM4GCLF2Gk6QGhh"
+"YLDxNGsyQjGR7z5AGibvYhNLU0JA4RbmxYWHw4g3wBrPA1jm9hGZ8y5Y8R97d0Hl"
+"VpyreEMjRKNDMEEwHQYDVR0OBBYEFA5EHgir+kOnflzQ/NMwMXgmEZQyMA8GA1Ud"
+"EwEB/wQFMAMBAf8wDwYDVR0PAQH/BAUDAwcGADAKBggqhkjOPQQDAwNoADBlAjEA"
+"kKF/6WReNytwSrJ8JSTToc7xWS5SvAa23Fnla4mywZUxUFS4VSxCMQTjQCknO3iZ"
+"AjBgxXyXYqn0d7vz7S6oAY5TdaD5YFT5MD2c1MAAp8pxQSwdPa9k0ZSoGIEn31Z0"
+"GxU="
+"-----END CERTIFICATE-----";
+ /*
+ * $ openssl x509 -in cert.pem -noout -serial
+ * serial=4BDD2E00AA7EBBFB
+ */
+ static constexpr std::array<uint8_t, 8> SERIAL {{0x4b,0xdd,0x2e,0x00,0xaa,0x7e,0xbb,0xfb}};
+ auto serial = dht::crypto::Certificate(cert_pem).getSerialNumber();
+ CPPUNIT_ASSERT(std::equal(SERIAL.begin(), SERIAL.end(), serial.begin(), serial.end()));
+}
+
+void CryptoTester::testOcsp() {
+ auto ca = dht::crypto::generateIdentity("Test CA");
+ auto device = dht::crypto::generateIdentity("Test Device", ca);
+ auto ocspRequest = device.second->generateOcspRequest(ca.second->cert);
+ auto req = dht::crypto::OcspRequest((const uint8_t*)ocspRequest.first.data(), ocspRequest.first.size());
+ CPPUNIT_ASSERT(ocspRequest.second == req.getNonce());
+}
+
+void
+CryptoTester::tearDown() {
+
+}
+} // namespace test
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ *
+ * Author: Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ * Vsevolod Ivanov <vsevolod.ivanov@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+// cppunit
+#include <cppunit/TestFixture.h>
+#include <cppunit/extensions/HelperMacros.h>
+
+namespace test {
+
+class CryptoTester : public CppUnit::TestFixture {
+ CPPUNIT_TEST_SUITE(CryptoTester);
+ CPPUNIT_TEST(testSignatureEncryption);
+ CPPUNIT_TEST(testCertificateRevocation);
+ CPPUNIT_TEST(testCertificateRequest);
+ CPPUNIT_TEST(testCertificateSerialNumber);
+ CPPUNIT_TEST(testOcsp);
+ CPPUNIT_TEST_SUITE_END();
+
+ public:
+ /**
+ * Method automatically called before each test by CppUnit
+ */
+ void setUp();
+ /**
+ * Method automatically called after each test CppUnit
+ */
+ void tearDown();
+ /**
+ * Test data signature, encryption and decryption
+ */
+ void testSignatureEncryption();
+ /**
+ * Test certificate generation, validation and revocation
+ */
+ void testCertificateRevocation();
+ /**
+ * Test certificate requests
+ */
+ void testCertificateRequest();
+ /**
+ * Test certificate serial number extraction
+ */
+ void testCertificateSerialNumber();
+ /**
+ * Test OCSP
+ */
+ void testOcsp();
+};
+
+} // namespace test
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ *
+ * Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com>
+ * Vsevolod Ivanov <vsevolod.ivanov@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "dhtproxytester.h"
+
+// std
+#include <iostream>
+#include <string>
+
+#include <chrono>
+#include <condition_variable>
+
+using namespace std::chrono_literals;
+
+namespace test {
+CPPUNIT_TEST_SUITE_REGISTRATION(DhtProxyTester);
+
+void
+DhtProxyTester::setUp() {
+ clientConfig.dht_config.node_config.max_peer_req_per_sec = -1;
+ clientConfig.dht_config.node_config.max_req_per_sec = -1;
+
+ nodePeer.run(0, clientConfig);
+
+ nodeProxy = std::make_shared<dht::DhtRunner>();
+ nodeProxy->run(0, clientConfig);
+ nodeProxy->bootstrap(nodePeer.getBound());
+
+ auto serverCAIdentity = dht::crypto::generateEcIdentity("DHT Node CA");
+
+ dht::ProxyServerConfig serverConfig;
+ //serverConfig.identity = dht::crypto::generateIdentity("DHT Node", serverCAIdentity);
+ serverConfig.port = 8080;
+ // serverConfig.pushServer = "127.0.0.1:8090";
+ serverProxy = std::make_unique<dht::DhtProxyServer>(nodeProxy, serverConfig);
+
+ /*clientConfig.server_ca = serverCAIdentity.second;
+ clientConfig.client_identity = dht::crypto::generateIdentity("DhtProxyTester");
+ clientConfig.push_node_id = "dhtnode";*/
+ clientConfig.proxy_server = "http://127.0.0.1:8080";//"https://127.0.0.1:8080";
+}
+
+void
+DhtProxyTester::tearDown() {
+ nodePeer.join();
+ nodeClient.join();
+
+ bool done = false;
+ std::condition_variable cv;
+ std::mutex cv_m;
+ nodeProxy->shutdown([&]{
+ std::lock_guard<std::mutex> lk(cv_m);
+ done = true;
+ cv.notify_all();
+ });
+ std::unique_lock<std::mutex> lk(cv_m);
+ CPPUNIT_ASSERT(cv.wait_for(lk, 15s, [&]{ return done; }));
+ serverProxy.reset();
+ nodeProxy.reset();
+}
+
+void
+DhtProxyTester::testGetPut() {
+ nodeClient.run(0, clientConfig);
+
+ bool done = false;
+ std::condition_variable cv;
+ std::mutex cv_m;
+
+ auto key = dht::InfoHash::get("GLaDOs");
+ dht::Value val {"Hey! It's been a long time. How have you been?"};
+ auto val_data = val.data;
+ {
+ nodePeer.put(key, std::move(val), [&](bool) {
+ std::lock_guard<std::mutex> lk(cv_m);
+ done = true;
+ cv.notify_all();
+ });
+ std::unique_lock<std::mutex> lk(cv_m);
+ CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&]{ return done; }));
+ }
+
+ auto vals = nodeClient.get(key).get();
+ CPPUNIT_ASSERT(not vals.empty());
+ CPPUNIT_ASSERT(vals.front()->data == val_data);
+}
+
+void
+DhtProxyTester::testListen() {
+ nodeClient.run(0, clientConfig);
+
+ std::condition_variable cv;
+ std::mutex cv_m;
+ std::unique_lock<std::mutex> lk(cv_m);
+ auto key = dht::InfoHash::get("GLaDOs");
+ bool done = false;
+
+ // If a peer send a value, the listen operation from the client
+ // should retrieve this value
+ dht::Value firstVal {"Hey! It's been a long time. How have you been?"};
+ auto firstVal_data = firstVal.data;
+ nodePeer.put(key, std::move(firstVal), [&](bool ok) {
+ CPPUNIT_ASSERT(ok);
+ std::lock_guard<std::mutex> lk(cv_m);
+ done = true;
+ cv.notify_all();
+ });
+ CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&]{ return done; }));
+ done = false;
+
+ std::vector<dht::Blob> values;
+ auto token = nodeClient.listen(key, [&](const std::vector<std::shared_ptr<dht::Value>>& v, bool expired) {
+ if (not expired) {
+ std::lock_guard<std::mutex> lk(cv_m);
+ for (const auto& value : v)
+ values.emplace_back(value->data);
+ done = true;
+ cv.notify_all();
+ }
+ return true;
+ });
+ CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&]{ return done; }));
+ done = false;
+ // Here values should contains 1 values
+ CPPUNIT_ASSERT_EQUAL(static_cast<int>(values.size()), 1);
+ CPPUNIT_ASSERT(values.front() == firstVal_data);
+
+ // And the listen should retrieve futures values
+ // All values
+ dht::Value secondVal {"You're a monster"};
+ auto secondVal_data = secondVal.data;
+ nodePeer.put(key, std::move(secondVal));
+ CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&]{ return done; }));
+ nodeClient.cancelListen(key, std::move(token));
+ // Here values should contains 2 values
+ CPPUNIT_ASSERT_EQUAL(static_cast<int>(values.size()), 2);
+ CPPUNIT_ASSERT(values.back() == secondVal_data);
+}
+
+void
+DhtProxyTester::testResubscribeGetValues() {
+ clientConfig.push_token = "atlas";
+ nodeClient.run(0, clientConfig);
+
+ bool done = false;
+ std::condition_variable cv;
+ std::mutex cv_m;
+ std::unique_lock<std::mutex> lk(cv_m);
+ auto key = dht::InfoHash::get("GLaDOs");
+
+ // If a peer sent a value, the listen operation from the client
+ // should retrieve this value
+ dht::Value firstVal {"Hey! It's been a long time. How have you been?"};
+ auto firstVal_data = firstVal.data;
+ nodePeer.put(key, std::move(firstVal), [&](bool ok) {
+ std::lock_guard<std::mutex> lk(cv_m);
+ CPPUNIT_ASSERT(ok);
+ done = true;
+ cv.notify_all();
+ });
+ CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&]{ return done; }));
+ done = false;
+
+ // Send a first subscribe, the value is sent via a push notification
+ // So ignore values here.
+ nodeClient.listen(key, [&](const std::vector<std::shared_ptr<dht::Value>>&, bool) {
+ return true;
+ });
+ cv.wait_for(lk, std::chrono::seconds(1));
+
+ // Reboot node (to avoid cache)
+ nodeClient.join();
+ clientConfig.push_token = "";
+ nodeClient.run(0, clientConfig);
+
+ // For the second subscribe, the proxy will return the value in the body
+ std::vector<std::shared_ptr<dht::Value>> values;
+ auto ftoken = nodeClient.listen(key, [&](const std::vector<std::shared_ptr<dht::Value>>& v, bool expired) {
+ if (not expired) {
+ std::lock_guard<std::mutex> lk(cv_m);
+ values.insert(values.end(), v.begin(), v.end());
+ done = true;
+ cv.notify_all();
+ }
+ return true;
+ });
+
+ CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&]{ return done; }));
+ auto token = ftoken.get();
+ CPPUNIT_ASSERT(token);
+ nodeClient.cancelListen(key, token);
+ // Here values should still contains 1 values
+ CPPUNIT_ASSERT_EQUAL((size_t)1u, values.size());
+ CPPUNIT_ASSERT(firstVal_data == values.front()->data);
+}
+
+void
+DhtProxyTester::testPutGet40KChars()
+{
+ nodeClient.run(0, clientConfig);
+ constexpr size_t N = 40000;
+
+ // Arrange
+ auto key = dht::InfoHash::get("testPutGet40KChars");
+ std::vector<std::shared_ptr<dht::Value>> values;
+ std::vector<uint8_t> mtu;
+ mtu.reserve(N);
+ for (size_t i = 0; i < N; i++)
+ mtu.emplace_back((i % 2) ? 'T' : 'M');
+ std::condition_variable cv;
+ std::mutex cv_m;
+ std::unique_lock<std::mutex> lk(cv_m);
+ bool done_put = false;
+ bool done_get = false;
+
+ // Act
+ dht::Value val {mtu};
+ nodePeer.put(key, std::move(val), [&](bool ok) {
+ std::lock_guard<std::mutex> lk(cv_m);
+ done_put = ok;
+ cv.notify_all();
+ });
+ CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&]{ return done_put; }));
+
+ nodeClient.get(key, [&](const std::vector<std::shared_ptr<dht::Value>>& vals){
+ values.insert(values.end(), vals.begin(), vals.end());
+ return true;
+ },[&](bool ok){
+ std::lock_guard<std::mutex> lk(cv_m);
+ done_get = ok;
+ cv.notify_all();
+ });
+ CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&]{ return done_get; }));
+
+ // Assert
+ CPPUNIT_ASSERT_EQUAL((size_t)1u, values.size());
+ for (const auto &value: values)
+ CPPUNIT_ASSERT(value->data == mtu);
+}
+
+void
+DhtProxyTester::testFuzzy()
+{
+ constexpr size_t N = 40000;
+
+ // Arrange
+ auto key = dht::InfoHash::get("testFuzzy");
+ std::vector<std::shared_ptr<dht::Value>> values;
+ std::vector<uint8_t> mtu;
+ mtu.reserve(N);
+ for (size_t i = 0; i < N; i++)
+ mtu.emplace_back((i % 2) ? 'T' : 'M');
+
+ // Act
+ for (size_t i = 0; i < 100; i++) {
+ auto nodeTest = std::make_shared<dht::DhtRunner>();
+ nodeTest->run(0, clientConfig);
+ nodeTest->put(key, dht::Value(mtu), [&](bool ok) {
+ CPPUNIT_ASSERT(ok);
+ });
+ nodeTest->get(key, [&](const std::vector<std::shared_ptr<dht::Value>>& vals){
+ values.insert(values.end(), vals.begin(), vals.end());
+ return true;
+ },[&](bool ok){
+ CPPUNIT_ASSERT(ok);
+ });
+ std::this_thread::sleep_for(5ms);
+ }
+
+ // Assert
+ for (const auto &value: values)
+ CPPUNIT_ASSERT(value->data == mtu);
+}
+
+void
+DhtProxyTester::testShutdownStop()
+{
+ constexpr size_t N = 40000;
+ constexpr unsigned C = 100;
+
+ // Arrange
+ auto key = dht::InfoHash::get("testShutdownStop");
+ std::vector<std::shared_ptr<dht::Value>> values;
+ std::vector<uint8_t> mtu;
+ mtu.reserve(N);
+ for (size_t i = 0; i < N; i++)
+ mtu.emplace_back((i % 2) ? 'T' : 'M');
+
+ std::atomic_uint callback_count {0};
+
+ // Act
+ for (size_t i = 0; i < C; i++) {
+ auto nodeTest = std::make_shared<dht::DhtRunner>();
+ nodeTest->run(0, clientConfig);
+ nodeTest->put(key, dht::Value(mtu), [&](bool /*ok*/) {
+ callback_count++;
+ });
+ nodeTest->get(key, [&](const std::vector<std::shared_ptr<dht::Value>>& vals){
+ values.insert(values.end(), vals.begin(), vals.end());
+ return true;
+ },[&](bool /*ok*/){
+ callback_count++;
+ });
+ bool done = false;
+ std::condition_variable cv;
+ std::mutex cv_m;
+ nodeTest->shutdown([&]{
+ std::lock_guard<std::mutex> lk(cv_m);
+ done = true;
+ cv.notify_all();
+ }, true);
+ std::unique_lock<std::mutex> lk(cv_m);
+ CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&]{ return done; }));
+ }
+ CPPUNIT_ASSERT_EQUAL(2*C, callback_count.load());
+}
+
+} // namespace test
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ *
+ * Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+// cppunit
+#include <cppunit/TestFixture.h>
+#include <cppunit/extensions/HelperMacros.h>
+
+#include <opendht/dhtrunner.h>
+#include <opendht/dht_proxy_server.h>
+#include <opendht/log.h>
+
+namespace test {
+
+class DhtProxyTester : public CppUnit::TestFixture {
+ CPPUNIT_TEST_SUITE(DhtProxyTester);
+ CPPUNIT_TEST(testGetPut);
+ CPPUNIT_TEST(testListen);
+ CPPUNIT_TEST(testResubscribeGetValues);
+ CPPUNIT_TEST(testPutGet40KChars);
+ CPPUNIT_TEST(testFuzzy);
+ CPPUNIT_TEST(testShutdownStop);
+ CPPUNIT_TEST_SUITE_END();
+
+ public:
+ /**
+ * Method automatically called before each test by CppUnit
+ * Init nodes
+ */
+ void setUp();
+ /**
+ * Method automatically called after each test CppUnit
+ */
+ void tearDown();
+ /**
+ * Test get and put methods
+ */
+ void testGetPut();
+ /**
+ * Test listen
+ */
+ void testListen();
+ /**
+ * When a proxy redo a subscribe on the proxy
+ * it should retrieve existant values
+ */
+ void testResubscribeGetValues();
+ /**
+ * Test MTU put/get on dht
+ */
+ void testPutGet40KChars();
+
+ void testFuzzy();
+
+ void testShutdownStop();
+
+ private:
+ dht::DhtRunner::Config clientConfig {};
+ dht::DhtRunner nodePeer;
+ dht::DhtRunner nodeClient;
+ std::shared_ptr<dht::DhtRunner> nodeProxy;
+ std::unique_ptr<dht::DhtProxyServer> serverProxy;
+};
+
+} // namespace test
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ *
+ * Author: Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "dhtrunnertester.h"
+
+#include <opendht/thread_pool.h>
+
+#include <chrono>
+#include <mutex>
+#include <condition_variable>
+using namespace std::chrono_literals;
+using namespace std::literals;
+
+namespace test {
+CPPUNIT_TEST_SUITE_REGISTRATION(DhtRunnerTester);
+
+void
+DhtRunnerTester::setUp() {
+ dht::DhtRunner::Config config;
+ config.dht_config.node_config.max_peer_req_per_sec = -1;
+ config.dht_config.node_config.max_req_per_sec = -1;
+ config.dht_config.node_config.max_store_size = -1;
+ config.dht_config.node_config.max_store_keys = -1;
+
+ node1.run(0, config);
+ node2.run(0, config);
+ node2.bootstrap(node1.getBound());
+}
+
+void
+DhtRunnerTester::tearDown() {
+ unsigned done {0};
+ std::condition_variable cv;
+ std::mutex cv_m;
+ auto shutdown = [&]{
+ std::lock_guard<std::mutex> lk(cv_m);
+ done++;
+ cv.notify_all();
+ };
+ node1.shutdown(shutdown);
+ node2.shutdown(shutdown);
+ std::unique_lock<std::mutex> lk(cv_m);
+ CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]{ return done == 2u; }));
+ node1.join();
+ node2.join();
+}
+
+void
+DhtRunnerTester::testConstructors() {
+ CPPUNIT_ASSERT(node1.getBoundPort());
+ CPPUNIT_ASSERT_EQUAL(node1.getBoundPort(), node1.getBound().getPort());
+ CPPUNIT_ASSERT(node2.getBoundPort());
+ CPPUNIT_ASSERT_EQUAL(node2.getBoundPort(), node2.getBound().getPort());
+
+ dht::DhtRunner::Config config {};
+ dht::DhtRunner::Context context {};
+ dht::DhtRunner testNode;
+ testNode.run(config, std::move(context));
+ CPPUNIT_ASSERT(testNode.getBoundPort());
+}
+
+void
+DhtRunnerTester::testGetPut() {
+ auto key = dht::InfoHash::get("123");
+ dht::Value val {"hey"};
+ auto val_data = val.data;
+ std::promise<bool> p;
+ node2.put(key, std::move(val), [&](bool ok){
+ p.set_value(ok);
+ });
+ CPPUNIT_ASSERT(p.get_future().get());
+ auto vals = node1.get(key).get();
+ CPPUNIT_ASSERT(not vals.empty());
+ CPPUNIT_ASSERT(vals.front()->data == val_data);
+}
+
+void
+DhtRunnerTester::testListen() {
+ std::mutex mutex;
+ std::condition_variable cv;
+ std::atomic_uint valueCounta(0);
+ std::atomic_uint valueCountb(0);
+ std::atomic_uint valueCountc(0);
+ std::atomic_uint valueCountd(0);
+
+ unsigned putCount(0);
+ unsigned putOkCount1(0);
+ unsigned putOkCount2(0);
+ unsigned putOkCount3(0);
+
+ auto a = dht::InfoHash::get("234");
+ auto b = dht::InfoHash::get("2345");
+ auto c = dht::InfoHash::get("23456");
+ auto d = dht::InfoHash::get("234567");
+ constexpr unsigned N = 256;
+ constexpr unsigned SZ = 56 * 1024;
+
+ auto ftokena = node1.listen(a, [&](const std::vector<std::shared_ptr<dht::Value>>& values, bool expired) {
+ if (expired)
+ valueCounta -= values.size();
+ else
+ valueCounta += values.size();
+ return true;
+ });
+
+ auto ftokenb = node1.listen(b, [&](const std::shared_ptr<dht::Value>&) {
+ /*if (expired)
+ valueCountb -= values.size();
+ else
+ valueCountb += values.size();*/
+ valueCountb++;
+ return false;
+ });
+
+ auto ftokenc = node1.listen(c, [&](const std::vector<std::shared_ptr<dht::Value>>& values, bool expired) {
+ if (expired)
+ valueCountc -= values.size();
+ else
+ valueCountc += values.size();
+ return true;
+ });
+
+ auto ftokend = node1.listen(d, [&](const std::vector<std::shared_ptr<dht::Value>>& values, bool expired) {
+ if (expired)
+ valueCountd -= values.size();
+ else
+ valueCountd += values.size();
+ return true;
+ });
+
+ std::vector<uint8_t> mtu;
+ mtu.reserve(SZ);
+ for (size_t i = 0; i < SZ; i++)
+ mtu.emplace_back((i % 2) ? 'T' : 'M');
+
+ for (unsigned i=0; i<N; i++) {
+ node2.put(a, dht::Value("v1"), [&](bool ok) {
+ std::lock_guard<std::mutex> lock(mutex);
+ putCount++;
+ if (ok) putOkCount1++;
+ cv.notify_all();
+ });
+ node2.put(b, dht::Value("v2"), [&](bool ok) {
+ std::lock_guard<std::mutex> lock(mutex);
+ putCount++;
+ if (ok) putOkCount2++;
+ cv.notify_all();
+ });
+ auto bigVal = std::make_shared<dht::Value>();
+ bigVal->data = mtu;
+ node2.put(c, std::move(bigVal), [&](bool ok) {
+ std::lock_guard<std::mutex> lock(mutex);
+ putCount++;
+ if (ok) putOkCount3++;
+ cv.notify_all();
+ });
+ }
+
+ {
+ std::unique_lock<std::mutex> lk(mutex);
+ CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]{ return putCount == N * 3u; }));
+ CPPUNIT_ASSERT_EQUAL(N, putOkCount1);
+ CPPUNIT_ASSERT_EQUAL(N, putOkCount2);
+ CPPUNIT_ASSERT_EQUAL(N, putOkCount3);
+ }
+
+ CPPUNIT_ASSERT(ftokena.valid());
+ CPPUNIT_ASSERT(ftokenb.valid());
+ CPPUNIT_ASSERT(ftokenc.valid());
+ CPPUNIT_ASSERT(ftokend.valid());
+
+ auto tokena = ftokena.get();
+ auto tokenc = ftokenc.get();
+ auto tokend = ftokend.get();
+ // tokenb might be 0 since the callback returns false.
+
+ CPPUNIT_ASSERT(tokena);
+ CPPUNIT_ASSERT(tokenc);
+ CPPUNIT_ASSERT(tokend);
+ CPPUNIT_ASSERT_EQUAL(N, valueCounta.load());
+ CPPUNIT_ASSERT_EQUAL(1u, valueCountb.load());
+ CPPUNIT_ASSERT_EQUAL(N, valueCountc.load());
+ CPPUNIT_ASSERT_EQUAL(0u, valueCountd.load());
+
+ node1.cancelListen(a, tokena);
+ node1.cancelListen(b, std::move(ftokenb));
+ node1.cancelListen(c, tokenc);
+ node1.cancelListen(d, tokend);
+}
+
+void
+DhtRunnerTester::testIdOps() {
+ std::mutex mutex;
+ std::condition_variable cv;
+ unsigned valueCount(0);
+ unsigned valueCountEdit(0);
+
+ dht::DhtRunner::Config config2;
+ config2.dht_config.node_config.max_peer_req_per_sec = -1;
+ config2.dht_config.node_config.max_req_per_sec = -1;
+ config2.dht_config.id = dht::crypto::generateIdentity();
+
+ dht::DhtRunner::Context context2;
+ context2.identityAnnouncedCb = [&](bool ok) {
+ CPPUNIT_ASSERT(ok);
+ std::lock_guard<std::mutex> lk(mutex);
+ valueCount++;
+ cv.notify_all();
+ };
+
+ node2.join();
+ node2.run(42232, config2, std::move(context2));
+ node2.bootstrap(node1.getBound());
+
+ {
+ std::unique_lock<std::mutex> lk(mutex);
+ CPPUNIT_ASSERT(cv.wait_for(lk, 20s, [&]{ return valueCount == 1; }));
+ }
+
+ node1.findCertificate(node2.getId(), [&](const std::shared_ptr<dht::crypto::Certificate>& crt){
+ CPPUNIT_ASSERT(crt);
+ std::lock_guard<std::mutex> lk(mutex);
+ valueCount++;
+ cv.notify_all();
+ });
+
+ {
+ std::unique_lock<std::mutex> lk(mutex);
+ CPPUNIT_ASSERT(cv.wait_for(lk, 20s, [&]{ return valueCount == 2; }));
+ }
+
+ dht::DhtRunner::Context context1;
+ context1.identityAnnouncedCb = [&](bool ok) {
+ CPPUNIT_ASSERT(ok);
+ std::lock_guard<std::mutex> lk(mutex);
+ valueCount++;
+ cv.notify_all();
+ };
+
+ config2.dht_config.id = dht::crypto::generateIdentity();
+ node1.join();
+ node1.run(42222, config2, std::move(context1));
+ node1.bootstrap(node2.getBound());
+
+ auto key = dht::InfoHash::get("key");
+ node1.putEncrypted(key, node2.getId(), dht::Value("yo"), [&](bool ok){
+ CPPUNIT_ASSERT(ok);
+ std::lock_guard<std::mutex> lk(mutex);
+ valueCount++;
+ cv.notify_all();
+ });
+
+ node1.putEncrypted(key, node2.getPublicKey(), dht::Value("yo"), [&](bool ok){
+ CPPUNIT_ASSERT(ok);
+ std::lock_guard<std::mutex> lk(mutex);
+ valueCount++;
+ cv.notify_all();
+ });
+
+ node2.listen<std::string>(key, [&](std::string&& value){
+ CPPUNIT_ASSERT_EQUAL("yo"s, value);
+ std::lock_guard<std::mutex> lk(mutex);
+ valueCount++;
+ cv.notify_all();
+ return true;
+ });
+
+ auto key2 = dht::InfoHash::get("key2");
+ auto editValue = std::make_shared<dht::Value>("v1");
+ node1.putSigned(key2, editValue, [&](bool ok){
+ CPPUNIT_ASSERT(ok);
+ std::lock_guard<std::mutex> lk(mutex);
+ valueCountEdit++;
+ cv.notify_all();
+ });
+ node2.listen(key2, [&](const std::vector<std::shared_ptr<dht::Value>>& values, bool expired){
+ for (const auto& v : values) {
+ if (v->seq == 0)
+ CPPUNIT_ASSERT_EQUAL("v1"s, dht::unpackMsg<std::string>(v->data));
+ else if (v->seq == 1)
+ CPPUNIT_ASSERT_EQUAL("v2"s, dht::unpackMsg<std::string>(v->data));
+ CPPUNIT_ASSERT_EQUAL(v->owner->getLongId(), node1.getPublicKey()->getLongId());
+ }
+ std::lock_guard<std::mutex> lk(mutex);
+ valueCountEdit += values.size();
+ cv.notify_all();
+ return true;
+ });
+
+ {
+ std::unique_lock<std::mutex> lk(mutex);
+ CPPUNIT_ASSERT(cv.wait_for(lk, 20s, [&]{ return valueCount == 7 && valueCountEdit == 2; }));
+ }
+
+ // editValue->data = dht::packMsg("v2");
+ editValue = std::make_shared<dht::Value>(editValue->id);
+ editValue->data = dht::packMsg("v2");
+ node1.putSigned(key2, editValue, [&](bool ok){
+ CPPUNIT_ASSERT(ok);
+ std::lock_guard<std::mutex> lk(mutex);
+ valueCountEdit++;
+ cv.notify_all();
+ });
+ std::unique_lock<std::mutex> lk(mutex);
+ CPPUNIT_ASSERT(cv.wait_for(lk, 20s, [&]{ return valueCountEdit == 4; }));
+}
+
+void
+DhtRunnerTester::testListenLotOfBytes() {
+ std::mutex mutex;
+ std::condition_variable cv;
+ std::atomic_uint valueCount(0);
+ unsigned putCount(0);
+ unsigned putOkCount(0);
+
+ std::string data(10000, 'a');
+
+ auto foo = dht::InfoHash::get("foo");
+ constexpr unsigned N = 1024;
+
+ for (unsigned i=0; i<N; i++) {
+ node2.put(foo, data, [&](bool ok) {
+ std::lock_guard<std::mutex> lock(mutex);
+ putCount++;
+ if (ok) putOkCount++;
+ cv.notify_all();
+ });
+ }
+ {
+ std::unique_lock<std::mutex> lk(mutex);
+ CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]{ return putCount == N; }));
+ }
+
+ dht::DhtRunner node3 {};
+ dht::DhtRunner::Config config;
+ config.dht_config.node_config.max_peer_req_per_sec = -1;
+ config.dht_config.node_config.max_req_per_sec = -1;
+ node3.run(42242, config);
+ node3.bootstrap(node1.getBound());
+
+ auto ftokenfoo = node3.listen(foo, [&](const std::shared_ptr<dht::Value>&) {
+ valueCount++;
+ cv.notify_all();
+ return true;
+ });
+
+ {
+ std::unique_lock<std::mutex> lk(mutex);
+ CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]{ return valueCount == N; }));
+ }
+
+ node3.cancelListen(foo, ftokenfoo.get());
+}
+
+
+void
+DhtRunnerTester::testMultithread() {
+ std::mutex mutex;
+ std::condition_variable cv;
+ unsigned putCount(0);
+ unsigned putOkCount(0);
+
+ constexpr unsigned N = 2048;
+
+ for (unsigned i=0; i<N; i++) {
+ dht::ThreadPool::computation().run([&]{
+ node2.put(dht::InfoHash::get("123" + std::to_string(i)), "hehe", [&](bool ok) {
+ std::lock_guard<std::mutex> lock(mutex);
+ putCount++;
+ if (ok) putOkCount++;
+ cv.notify_all();
+ });
+ node2.get(dht::InfoHash::get("123" + std::to_string(N-i-1)), [](const std::shared_ptr<dht::Value>&){
+ return true;
+ }, [&](bool ok) {
+ std::lock_guard<std::mutex> lock(mutex);
+ putCount++;
+ if (ok) putOkCount++;
+ cv.notify_all();
+ });
+ });
+ }
+ std::unique_lock<std::mutex> lk(mutex);
+ CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]{ return putCount == 2*N; }));
+ CPPUNIT_ASSERT_EQUAL(2*N, putOkCount);
+
+}
+
+
+} // namespace test
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ *
+ * Author: Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+// cppunit
+#include <cppunit/TestFixture.h>
+#include <cppunit/extensions/HelperMacros.h>
+
+#include <opendht/dhtrunner.h>
+
+namespace test {
+
+class DhtRunnerTester : public CppUnit::TestFixture {
+ CPPUNIT_TEST_SUITE(DhtRunnerTester);
+ CPPUNIT_TEST(testConstructors);
+ CPPUNIT_TEST(testGetPut);
+ CPPUNIT_TEST(testListen);
+ CPPUNIT_TEST(testListenLotOfBytes);
+ CPPUNIT_TEST(testIdOps);
+ CPPUNIT_TEST_SUITE_END();
+
+ dht::DhtRunner node1 {};
+ dht::DhtRunner node2 {};
+ public:
+ /**
+ * Method automatically called before each test by CppUnit
+ */
+ void setUp();
+ /**
+ * Method automatically called after each test CppUnit
+ */
+ void tearDown();
+ /**
+ * Test the differents behaviors of constructors
+ */
+ void testConstructors();
+ /**
+ * Test get and put methods
+ */
+ void testGetPut();
+ /**
+ * Test listen method
+ */
+ void testListen();
+ /**
+ * Test methods requiring a node identity
+ */
+ void testIdOps();
+ /**
+ * Test listen method with lot of datas
+ */
+ void testListenLotOfBytes();
+ /**
+ * Test multithread
+ */
+ void testMultithread();
+
+};
+
+} // namespace test
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ *
+ * Author: Vsevolod Ivanov <vsevolod.ivanov@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "httptester.h"
+
+#include <opendht/value.h>
+#include <opendht/dhtrunner.h>
+
+#include <iostream>
+#include <string>
+#include <chrono>
+#include <condition_variable>
+
+namespace test {
+CPPUNIT_TEST_SUITE_REGISTRATION(HttpTester);
+
+void
+HttpTester::setUp() {
+ nodePeer = std::make_shared<dht::DhtRunner>();
+ nodePeer->run(0);
+
+ auto nodeProxy = std::make_shared<dht::DhtRunner>();
+ nodeProxy->run(0, /*identity*/{}, /*threaded*/true);
+ nodeProxy->bootstrap(nodePeer->getBound());
+
+ dht::ProxyServerConfig config;
+ config.port = 8080;
+ config.pushServer = "127.0.0.1:8090";
+ serverProxy = std::make_unique<dht::DhtProxyServer>(nodeProxy, config);
+
+ // Wait for the server to start
+ std::this_thread::sleep_for(std::chrono::milliseconds(100));
+}
+
+void
+HttpTester::tearDown() {
+ serverProxy.reset();
+ nodePeer->join();
+}
+
+void
+HttpTester::test_parse_url() {
+ // Arrange
+ std::string url = "http://google.com/";
+ // Act
+ dht::http::Url parsed (url);
+ // Assert
+ CPPUNIT_ASSERT(parsed.url == url);
+ CPPUNIT_ASSERT(parsed.protocol == "http");
+ CPPUNIT_ASSERT(parsed.host == "google.com");
+ CPPUNIT_ASSERT(parsed.target == "/");
+}
+
+void
+HttpTester::test_parse_https_url_no_service() {
+ // Arrange
+ std::string url = "https://jami.net/";
+ // Act
+ dht::http::Url parsed (url);
+ // Assert
+ CPPUNIT_ASSERT(parsed.url == url);
+ CPPUNIT_ASSERT(parsed.protocol == "https");
+ CPPUNIT_ASSERT(parsed.host == "jami.net");
+ CPPUNIT_ASSERT(parsed.target == "/");
+}
+
+void
+HttpTester::test_parse_url_no_prefix_no_target() {
+ // Arrange
+ std::string url = "google.com";
+ // Act
+ dht::http::Url parsed (url);
+ // Assert
+ CPPUNIT_ASSERT(parsed.url == url);
+ CPPUNIT_ASSERT(parsed.protocol == "http");
+ CPPUNIT_ASSERT(parsed.host == "google.com");
+ CPPUNIT_ASSERT(parsed.target == "");
+}
+
+void
+HttpTester::test_parse_url_target() {
+ // Arrange
+ std::string url = "https://www.google.com:666/going/under";
+ // Act
+ dht::http::Url parsed (url);
+ // Assert
+ CPPUNIT_ASSERT(parsed.url == url);
+ CPPUNIT_ASSERT(parsed.protocol == "https");
+ CPPUNIT_ASSERT(parsed.host == "www.google.com");
+ CPPUNIT_ASSERT(parsed.service == "666");
+ CPPUNIT_ASSERT(parsed.target == "/going/under");
+}
+
+void
+HttpTester::test_parse_url_query() {
+ // Arrange
+ std::string url = "http://google.com/?key=1";
+ // Act
+ dht::http::Url parsed (url);
+ // Assert
+ CPPUNIT_ASSERT(parsed.url == url);
+ CPPUNIT_ASSERT(parsed.protocol == "http");
+ CPPUNIT_ASSERT(parsed.host == "google.com");
+ CPPUNIT_ASSERT(parsed.target == "/?key=1");
+ CPPUNIT_ASSERT(parsed.query == "key=1");
+}
+
+void
+HttpTester::test_parse_url_fragment() {
+ // Arrange
+ std::string url = "http://google.com/?key=1#some-important-id";
+ // Act
+ dht::http::Url parsed (url);
+ // Assert
+ CPPUNIT_ASSERT(parsed.url == url);
+ CPPUNIT_ASSERT(parsed.protocol == "http");
+ CPPUNIT_ASSERT(parsed.host == "google.com");
+ CPPUNIT_ASSERT(parsed.target == "/?key=1");
+ CPPUNIT_ASSERT(parsed.query == "key=1");
+ CPPUNIT_ASSERT(parsed.fragment == "#some-important-id");
+}
+
+void
+HttpTester::test_parse_url_ipv4() {
+ // Arrange
+ std::string url = "http://172.217.13.132/";
+ // Act
+ dht::http::Url parsed (url);
+ // Assert
+ CPPUNIT_ASSERT(parsed.url == url);
+ CPPUNIT_ASSERT(parsed.protocol == "http");
+ CPPUNIT_ASSERT(parsed.host == "172.217.13.132");
+ CPPUNIT_ASSERT(parsed.target == "/");
+}
+
+void
+HttpTester::test_parse_url_no_prefix_no_target_ipv4() {
+ // Arrange
+ std::string url = "172.217.13.132";
+ // Act
+ dht::http::Url parsed (url);
+ // Assert
+ CPPUNIT_ASSERT(parsed.url == url);
+ CPPUNIT_ASSERT(parsed.protocol == "http");
+ CPPUNIT_ASSERT(parsed.host == "172.217.13.132");
+}
+
+void
+HttpTester::test_parse_url_target_ipv4() {
+ // Arrange
+ std::string url = "https://172.217.13.132:666/going/under";
+ // Act
+ dht::http::Url parsed (url);
+ // Assert
+ CPPUNIT_ASSERT(parsed.url == url);
+ CPPUNIT_ASSERT(parsed.protocol == "https");
+ CPPUNIT_ASSERT(parsed.host == "172.217.13.132");
+ CPPUNIT_ASSERT(parsed.service == "666");
+ CPPUNIT_ASSERT(parsed.target == "/going/under");
+}
+
+void
+HttpTester::test_parse_url_ipv6() {
+ // Arrange
+ std::string url = "http://[2607:f8b0:4006:804::2004]/";
+ // Act
+ dht::http::Url parsed (url);
+ // Assert
+ CPPUNIT_ASSERT(parsed.url == url);
+ CPPUNIT_ASSERT(parsed.protocol == "http");
+ CPPUNIT_ASSERT(parsed.host == "2607:f8b0:4006:804::2004");
+ CPPUNIT_ASSERT(parsed.target == "/");
+}
+
+void
+HttpTester::test_parse_url_no_prefix_no_target_ipv6() {
+ // Arrange
+ std::string url = "2607:f8b0:4006:804::2004";
+ // Act
+ dht::http::Url parsed (url);
+ // Assert
+ CPPUNIT_ASSERT(parsed.url == url);
+ CPPUNIT_ASSERT(parsed.protocol == "http");
+ CPPUNIT_ASSERT(parsed.host == "2607:f8b0:4006:804::2004");
+}
+
+void
+HttpTester::test_parse_url_target_ipv6() {
+ // Arrange
+ std::string url = "https://[2607:f8b0:4006:804::2004]:666/going/under";
+ // Act
+ dht::http::Url parsed (url);
+ // Assert
+ CPPUNIT_ASSERT(parsed.url == url);
+ CPPUNIT_ASSERT(parsed.protocol == "https");
+ CPPUNIT_ASSERT(parsed.host == "2607:f8b0:4006:804::2004");
+ CPPUNIT_ASSERT(parsed.service == "666");
+ CPPUNIT_ASSERT(parsed.target == "/going/under");
+}
+
+void
+HttpTester::test_send_json() {
+ // Arrange
+ std::condition_variable cv;
+ std::mutex cv_m;
+ std::unique_lock<std::mutex> lk(cv_m);
+ bool done = false;
+ unsigned int status = 0;
+
+ auto json = dht::Value("hey").toJson();
+ Json::Value resp_val;
+
+ // Act
+ auto request = std::make_shared<dht::http::Request>(serverProxy->io_context(),
+ "http://127.0.0.1:8080/key",
+ json,
+ [&](Json::Value value, const dht::http::Response& response) {
+ std::lock_guard<std::mutex> lk(cv_m);
+ resp_val = std::move(value);
+ status = response.status_code;
+ done = true;
+ cv.notify_all();
+ });
+ request->send();
+
+ // Assert
+ CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(10), [&]{ return done; }));
+ CPPUNIT_ASSERT_EQUAL(200u, status);
+ CPPUNIT_ASSERT_EQUAL(json["data"].asString(), resp_val["data"].asString());
+ done = false;
+
+#if 0
+ request = std::make_shared<dht::http::Request>(serverProxy->io_context(),
+ "http://google.ca",
+ [&](const dht::http::Response& response) {
+ std::lock_guard<std::mutex> lk(cv_m);
+ status = response.status_code;
+ done = true;
+ cv.notify_all();
+ });
+ request->send();
+
+ CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(10), [&]{ return done; }));
+ CPPUNIT_ASSERT(status != 0);
+ done = false;
+
+ request = std::make_shared<dht::http::Request>(serverProxy->io_context(),
+ "https://google.ca",
+ [&](const dht::http::Response& response) {
+ std::lock_guard<std::mutex> lk(cv_m);
+ status = response.status_code;
+ done = true;
+ cv.notify_all();
+ });
+ request->send();
+
+ CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(10), [&]{ return done; }));
+ CPPUNIT_ASSERT(status != 0);
+ done = false;
+
+ request = std::make_shared<dht::http::Request>(serverProxy->io_context(),
+ "https://google.ca/sdbjklwGBIP",
+ [&](const dht::http::Response& response) {
+ std::lock_guard<std::mutex> lk(cv_m);
+ status = response.status_code;
+ done = true;
+ cv.notify_all();
+ });
+ request->send();
+
+ CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(10), [&]{ return done; }));
+ CPPUNIT_ASSERT_EQUAL(404u, status);
+#endif
+}
+
+} // namespace test
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ *
+ * Author: Vsevolod Ivanov <vsevolod.ivanov@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+// cppunit
+#include <cppunit/TestFixture.h>
+#include <cppunit/extensions/HelperMacros.h>
+
+#include <opendht/http.h>
+#include <opendht/dht_proxy_server.h>
+#include <asio.hpp>
+
+#include <thread>
+#include <memory>
+
+namespace test {
+
+class HttpTester : public CppUnit::TestFixture {
+ CPPUNIT_TEST_SUITE(HttpTester);
+ // parse_url
+ CPPUNIT_TEST(test_parse_url);
+ CPPUNIT_TEST(test_parse_https_url_no_service);
+ CPPUNIT_TEST(test_parse_url_no_prefix_no_target);
+ CPPUNIT_TEST(test_parse_url_target);
+ CPPUNIT_TEST(test_parse_url_query);
+ CPPUNIT_TEST(test_parse_url_fragment);
+ CPPUNIT_TEST(test_parse_url_ipv4);
+ CPPUNIT_TEST(test_parse_url_no_prefix_no_target_ipv4);
+ CPPUNIT_TEST(test_parse_url_target_ipv4);
+ CPPUNIT_TEST(test_parse_url_ipv6);
+ CPPUNIT_TEST(test_parse_url_no_prefix_no_target_ipv6);
+ CPPUNIT_TEST(test_parse_url_target_ipv6);
+ // send
+ CPPUNIT_TEST(test_send_json);
+ CPPUNIT_TEST_SUITE_END();
+
+ public:
+ /**
+ * Method automatically called before each test by CppUnit
+ * Init nodes
+ */
+ void setUp();
+ /**
+ * Method automatically called after each test CppUnit
+ */
+ void tearDown();
+ /**
+ * Test parse urls
+ */
+ void test_parse_url();
+ void test_parse_https_url_no_service();
+ void test_parse_url_no_prefix_no_target();
+ void test_parse_url_target();
+ void test_parse_url_query();
+ void test_parse_url_fragment();
+ /**
+ * Test parse urls (ipv4)
+ */
+ void test_parse_url_ipv4();
+ void test_parse_url_no_prefix_no_target_ipv4();
+ void test_parse_url_target_ipv4();
+ /**
+ * Test parse urls (ipv6)
+ */
+ void test_parse_url_ipv6();
+ void test_parse_url_no_prefix_no_target_ipv6();
+ void test_parse_url_target_ipv6();
+ /**
+ * Test send(json)
+ */
+ void test_send_json();
+
+ private:
+ std::shared_ptr<dht::DhtRunner> nodePeer;
+ std::unique_ptr<dht::DhtProxyServer> serverProxy;
+};
+
+} // namespace test
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ *
+ * Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "infohashtester.h"
+
+// std
+#include <iostream>
+#include <string>
+
+// opendht
+#include "opendht/infohash.h"
+
+namespace test {
+CPPUNIT_TEST_SUITE_REGISTRATION(InfoHashTester);
+
+void
+InfoHashTester::setUp() {
+
+}
+
+void
+InfoHashTester::testConstructors() {
+ // Default constructor creates a null infohash
+ auto nullHash = dht::InfoHash();
+ CPPUNIT_ASSERT_EQUAL((size_t)20u, nullHash.size());
+ CPPUNIT_ASSERT(!nullHash);
+ // Build from a uint8_t. if length to short, should get a null infohash
+ uint8_t to_short[] = {0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8};
+ auto infohash = dht::InfoHash(to_short, 8);
+ CPPUNIT_ASSERT_EQUAL((size_t)20u, infohash.size());
+ CPPUNIT_ASSERT_EQUAL(std::string("0000000000000000000000000000000000000000"), infohash.toString());
+ // Build from a uint8_t. if length is enough, data should contains the uint8_t
+ uint8_t enough[] = {0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa,
+ 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa};
+ infohash = dht::InfoHash(enough, 20);
+ CPPUNIT_ASSERT(infohash.size() == 20);
+ const auto* data = infohash.data();
+ for (auto i = 0; i < 20; ++i) {
+ CPPUNIT_ASSERT_EQUAL(enough[i], data[i]);
+ }
+ // if too long, should be cutted to 20
+ uint8_t tooLong[] = {0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa,
+ 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa, 0xb0};
+ infohash = dht::InfoHash(tooLong, 21);
+ CPPUNIT_ASSERT(infohash.size() == 20);
+ const auto* data2 = infohash.data();
+ for (auto i = 0; i < 20; ++i) {
+ CPPUNIT_ASSERT_EQUAL(enough[i], data2[i]);
+ }
+ // Build from string
+ auto infohashFromStr = dht::InfoHash("0102030405060708090A0102030405060708090A");
+ CPPUNIT_ASSERT_EQUAL((size_t)20u, infohashFromStr.size());
+ const auto* dataStr = infohashFromStr.data();
+ for (auto i = 0; i < 20; ++i) {
+ CPPUNIT_ASSERT_EQUAL((int)dataStr[i], (int)data[i]);
+ }
+}
+
+void
+InfoHashTester::testComparators() {
+ auto nullHash = dht::InfoHash();
+ auto minHash = dht::InfoHash("0000000000000000000000000000000000111110");
+ auto maxHash = dht::InfoHash("0111110000000000000000000000000000000000");
+ // operator ==
+ CPPUNIT_ASSERT_EQUAL(minHash, minHash);
+ CPPUNIT_ASSERT_EQUAL(minHash, dht::InfoHash("0000000000000000000000000000000000111110"));
+ CPPUNIT_ASSERT(!(minHash == maxHash));
+ // operator !=
+ CPPUNIT_ASSERT(!(minHash != minHash));
+ CPPUNIT_ASSERT(!(minHash != dht::InfoHash("0000000000000000000000000000000000111110")));
+ CPPUNIT_ASSERT(minHash != maxHash);
+ // operator<
+ CPPUNIT_ASSERT(nullHash < minHash);
+ CPPUNIT_ASSERT(nullHash < maxHash);
+ CPPUNIT_ASSERT(minHash < maxHash);
+ CPPUNIT_ASSERT(!(minHash < nullHash));
+ CPPUNIT_ASSERT(!(maxHash < nullHash));
+ CPPUNIT_ASSERT(!(maxHash < minHash));
+ CPPUNIT_ASSERT(!(minHash < minHash));
+ // bool()
+ CPPUNIT_ASSERT(maxHash);
+ CPPUNIT_ASSERT(!nullHash);
+
+}
+
+void
+InfoHashTester::testLowBit() {
+ auto nullHash = dht::InfoHash();
+ auto minHash = dht::InfoHash("0000000000000000000000000000000000000010");
+ auto maxHash = dht::InfoHash("0100000000000000000000000000000000000000");
+ CPPUNIT_ASSERT_EQUAL(nullHash.lowbit(), -1);
+ CPPUNIT_ASSERT_EQUAL(minHash.lowbit(), 155);
+ CPPUNIT_ASSERT_EQUAL(maxHash.lowbit(), 7);
+}
+
+void
+InfoHashTester::testCommonBits() {
+ auto nullHash = dht::InfoHash();
+ auto minHash = dht::InfoHash("0000000000000000000000000000000000000010");
+ auto maxHash = dht::InfoHash("0100000000000000000000000000000000000000");
+ CPPUNIT_ASSERT_EQUAL(dht::InfoHash::commonBits(nullHash, nullHash), (unsigned)160);
+ CPPUNIT_ASSERT_EQUAL(dht::InfoHash::commonBits(nullHash, minHash), (unsigned)155);
+ CPPUNIT_ASSERT_EQUAL(dht::InfoHash::commonBits(nullHash, maxHash), (unsigned)7);
+ CPPUNIT_ASSERT_EQUAL(dht::InfoHash::commonBits(minHash, maxHash), (unsigned)7);
+}
+
+void
+InfoHashTester::testXorCmp() {
+ auto nullHash = dht::InfoHash();
+ auto minHash = dht::InfoHash("0000000000000000000000000000000000000010");
+ auto maxHash = dht::InfoHash("0100000000000000000000000000000000000000");
+ CPPUNIT_ASSERT_EQUAL(minHash.xorCmp(nullHash, maxHash), -1);
+ CPPUNIT_ASSERT_EQUAL(minHash.xorCmp(maxHash, nullHash), 1);
+ CPPUNIT_ASSERT_EQUAL(minHash.xorCmp(minHash, maxHash), -1);
+ CPPUNIT_ASSERT_EQUAL(minHash.xorCmp(maxHash, minHash), 1);
+ CPPUNIT_ASSERT_EQUAL(nullHash.xorCmp(minHash, maxHash), -1);
+ CPPUNIT_ASSERT_EQUAL(nullHash.xorCmp(maxHash, minHash), 1);
+ // Because hashes are circular in distance.
+ CPPUNIT_ASSERT_EQUAL(maxHash.xorCmp(nullHash, minHash), -1);
+ CPPUNIT_ASSERT_EQUAL(maxHash.xorCmp(minHash, nullHash), 1);
+}
+
+void
+InfoHashTester::testHex() {
+ const std::string TEST_HASH_STR("01b20304d5060708090a010203e05060708090ae");
+ dht::InfoHash TEST_HASH(TEST_HASH_STR);
+ CPPUNIT_ASSERT_EQUAL(TEST_HASH_STR, TEST_HASH.toString());
+ CPPUNIT_ASSERT_EQUAL(TEST_HASH_STR, dht::toHex(TEST_HASH.data(), TEST_HASH.size()));
+}
+
+void
+InfoHashTester::tearDown() {
+
+}
+} // namespace test
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ *
+ * Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+// cppunit
+#include <cppunit/TestFixture.h>
+#include <cppunit/extensions/HelperMacros.h>
+
+namespace test {
+
+class InfoHashTester : public CppUnit::TestFixture {
+ CPPUNIT_TEST_SUITE(InfoHashTester);
+ CPPUNIT_TEST(testConstructors);
+ CPPUNIT_TEST(testComparators);
+ CPPUNIT_TEST(testLowBit);
+ CPPUNIT_TEST(testCommonBits);
+ CPPUNIT_TEST(testXorCmp);
+ CPPUNIT_TEST(testHex);
+ CPPUNIT_TEST_SUITE_END();
+
+ public:
+ /**
+ * Method automatically called before each test by CppUnit
+ */
+ void setUp();
+ /**
+ * Method automatically called after each test CppUnit
+ */
+ void tearDown();
+ /**
+ * Test the differents behaviors of constructors
+ */
+ void testConstructors();
+ /**
+ * Test compare operators
+ */
+ void testComparators();
+ /**
+ * Test lowbit method
+ */
+ void testLowBit();
+ /**
+ * Test commonBits method
+ */
+ void testCommonBits();
+ /**
+ * Test xorCmp operators
+ */
+ void testXorCmp();
+
+ /**
+ * Test hex conversion
+ */
+ void testHex();
+};
+
+} // namespace test
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ *
+ * Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "peerdiscoverytester.h"
+#include "opendht/value.h"
+
+#include <mutex>
+#include <condition_variable>
+
+namespace test {
+
+constexpr unsigned MULTICAST_PORT = 2222;
+const std::string DHT_NODE_NAME {"dht"};
+const std::string JAMI_NODE_NAME {"jami"};
+
+struct DhtNode {
+ dht::InfoHash nodeid;
+ in_port_t node_port;
+ dht::NetId nid;
+ MSGPACK_DEFINE(nodeid, node_port, nid)
+};
+struct JamiNode {
+ int num;
+ char cha;
+ std::string str;
+ MSGPACK_DEFINE(num, cha, str)
+};
+
+CPPUNIT_TEST_SUITE_REGISTRATION(PeerDiscoveryTester);
+
+void PeerDiscoveryTester::setUp(){}
+
+void PeerDiscoveryTester::testMulticastToTwoNodes()
+{
+ DhtNode dhtNode;
+ dhtNode.nid = 10;
+ dhtNode.node_port = 50000;
+ dhtNode.nodeid = dht::InfoHash::get("opendht01");
+
+ JamiNode jamiNode;
+ jamiNode.num = 100;
+ jamiNode.cha = 'a';
+ jamiNode.str = "jami01";
+
+ std::mutex lock;
+ std::condition_variable cv;
+ unsigned countDht {0};
+ unsigned countJami {0};
+ {
+ std::unique_lock<std::mutex> l(lock);
+ dht::PeerDiscovery testDht(MULTICAST_PORT);
+ dht::PeerDiscovery testJami(MULTICAST_PORT);
+
+ testJami.startDiscovery<DhtNode>(DHT_NODE_NAME,[&](DhtNode&& v, dht::SockAddr&&){
+ CPPUNIT_ASSERT_EQUAL(dhtNode.node_port, v.node_port);
+ CPPUNIT_ASSERT_EQUAL(dhtNode.nodeid, v.nodeid);
+ CPPUNIT_ASSERT_EQUAL(dhtNode.nid, v.nid);
+ {
+ std::lock_guard<std::mutex> l(lock);
+ countDht++;
+ }
+ cv.notify_all();
+ });
+
+ testJami.startDiscovery(JAMI_NODE_NAME,[&](msgpack::object&& obj, dht::SockAddr&&){
+ auto v = obj.as<JamiNode>();
+ CPPUNIT_ASSERT_EQUAL(jamiNode.num, v.num);
+ CPPUNIT_ASSERT_EQUAL(jamiNode.cha, v.cha);
+ CPPUNIT_ASSERT_EQUAL(jamiNode.str, v.str);
+ {
+ std::lock_guard<std::mutex> l(lock);
+ countJami++;
+ }
+ cv.notify_all();
+ });
+
+ testDht.startPublish(DHT_NODE_NAME, dhtNode);
+ CPPUNIT_ASSERT(cv.wait_for(l, std::chrono::seconds(5), [&]{
+ return countDht > 0;
+ }));
+
+ testDht.startPublish(JAMI_NODE_NAME, jamiNode);
+ CPPUNIT_ASSERT(cv.wait_for(l, std::chrono::seconds(5), [&]{
+ return countDht > 1 and countJami > 0;
+ }));
+ // we don't verify count values since its a continious multicasting
+
+ l.unlock();
+ testDht.stopPublish(DHT_NODE_NAME);
+ testDht.stopPublish(JAMI_NODE_NAME);
+ testJami.stopDiscovery(DHT_NODE_NAME);
+ testJami.stopDiscovery(JAMI_NODE_NAME);
+ }
+}
+
+void PeerDiscoveryTester::tearDown(){}
+
+} // namespace test
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ *
+ * Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "opendht/peer_discovery.h"
+
+#include <cppunit/TestFixture.h>
+#include <cppunit/extensions/HelperMacros.h>
+
+namespace test {
+
+class OPENDHT_PUBLIC PeerDiscoveryTester : public CppUnit::TestFixture {
+
+ CPPUNIT_TEST_SUITE(PeerDiscoveryTester);
+ CPPUNIT_TEST(testMulticastToTwoNodes);
+ CPPUNIT_TEST_SUITE_END();
+
+ public:
+ /**
+ * Method automatically called before each test by CppUnit
+ */
+ void setUp();
+ /**
+ * Method automatically called after each test CppUnit
+ */
+ void tearDown();
+ /**
+ * Test Multicast on two nodes
+ */
+ void testMulticastToTwoNodes();
+};
+
+} // namespace test
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ * Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+#include <cppunit/extensions/TestFactoryRegistry.h>
+#include <cppunit/ui/text/TestRunner.h>
+#include <cppunit/CompilerOutputter.h>
+#include <iostream>
+
+int main(int /*argc*/, char** /*argv*/) {
+ CppUnit::TestFactoryRegistry ®istry = CppUnit::TestFactoryRegistry::getRegistry();
+ CppUnit::Test *suite = registry.makeTest();
+ if (suite->countTestCases() == 0) {
+ std::cout << "No test cases specified for suite" << std::endl;
+ return 1;
+ }
+ CppUnit::TextUi::TestRunner runner;
+ runner.addTest(suite);
+ auto result = runner.run() ? 0 : 1;
+ return result;
+}
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ *
+ * Author: Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "threadpooltester.h"
+
+#include "opendht/thread_pool.h"
+#include <atomic>
+#include <thread>
+
+namespace test {
+CPPUNIT_TEST_SUITE_REGISTRATION(ThreadPoolTester);
+using clock = std::chrono::steady_clock;
+
+void
+ThreadPoolTester::setUp() {
+
+}
+
+void
+ThreadPoolTester::testThreadPool() {
+ dht::ThreadPool pool(16);
+
+ constexpr unsigned N = 64 * 1024;
+ std::atomic_uint count {0};
+ for (unsigned i=0; i<N; i++)
+ pool.run([&] {
+ count++;
+ });
+
+ auto start = clock::now();
+ while (count.load() != N && clock::now() - start < std::chrono::seconds(10))
+ std::this_thread::sleep_for(std::chrono::milliseconds(10));
+
+ pool.join();
+ CPPUNIT_ASSERT_EQUAL(N, count.load());
+}
+
+void
+ThreadPoolTester::testExecutor()
+{
+ dht::ThreadPool pool(8);
+ auto executor1 = std::make_shared<dht::Executor>(pool, 1);
+ auto executor4 = std::make_shared<dht::Executor>(pool, 4);
+ auto executor8 = std::make_shared<dht::Executor>(pool, 8);
+
+ constexpr unsigned N = 64 * 1024;
+ unsigned count1 {0};
+ std::atomic_uint count4 {0};
+ std::atomic_uint count8 {0};
+ for (unsigned i=0; i<N; i++) {
+ executor1->run([&] { count1++; });
+ executor4->run([&] { count4++; });
+ executor8->run([&] { count8++; });
+ }
+
+ auto start = clock::now();
+ while ((count1 != N ||
+ count4.load() != N ||
+ count8.load() != N) && clock::now() - start < std::chrono::seconds(20))
+ {
+ std::this_thread::sleep_for(std::chrono::milliseconds(10));
+ }
+ executor1.reset();
+ executor4.reset();
+ executor8.reset();
+ CPPUNIT_ASSERT_EQUAL(N, count1);
+ CPPUNIT_ASSERT_EQUAL(N, count4.load());
+ CPPUNIT_ASSERT_EQUAL(N, count8.load());
+}
+
+void
+ThreadPoolTester::testContext()
+{
+ std::atomic_uint count {0};
+ constexpr unsigned N = 64 * 1024;
+
+ {
+ dht::ExecutionContext ctx(dht::ThreadPool::computation());
+ for (unsigned i=0; i<N; i++) {
+ ctx.run([&] { count++; });
+ }
+ }
+
+ CPPUNIT_ASSERT_EQUAL(N, count.load());
+
+}
+
+void
+ThreadPoolTester::tearDown() {
+}
+
+} // namespace test
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ *
+ * Author: Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+// cppunit
+#include <cppunit/TestFixture.h>
+#include <cppunit/extensions/HelperMacros.h>
+
+namespace test {
+
+class ThreadPoolTester : public CppUnit::TestFixture {
+ CPPUNIT_TEST_SUITE(ThreadPoolTester);
+ CPPUNIT_TEST(testThreadPool);
+ CPPUNIT_TEST(testExecutor);
+ CPPUNIT_TEST(testContext);
+ CPPUNIT_TEST_SUITE_END();
+
+ public:
+ /**
+ * Method automatically called before each test by CppUnit
+ */
+ void setUp();
+ /**
+ * Method automatically called after each test CppUnit
+ */
+ void tearDown();
+
+ void testThreadPool();
+ void testExecutor();
+ void testContext();
+};
+
+} // namespace test
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ *
+ * Author: Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "valuetester.h"
+
+#include <iostream>
+#include <string>
+
+// opendht
+#include "opendht/value.h"
+
+namespace test {
+CPPUNIT_TEST_SUITE_REGISTRATION(ValueTester);
+
+void
+ValueTester::setUp() {
+
+}
+
+void
+ValueTester::testConstructors() {
+ std::string the_data {"42 cats"};
+ dht::Value the_dht_value {(const uint8_t*)the_data.data(), the_data.size()};
+ std::string from_value {the_dht_value.data.begin(), the_dht_value.data.end()};
+ CPPUNIT_ASSERT_EQUAL(the_data, from_value);
+}
+
+void
+ValueTester::testFilter()
+{
+ dht::Value::Filter defaultFiler {};
+
+ auto isPairSize = dht::Value::Filter([](const dht::Value& v) {
+ return v.data.size() % 2 == 0;
+ });
+
+ auto isUserTypeTest = dht::Value::Filter([](const dht::Value& v) {
+ return v.user_type == "test";
+ });
+
+ std::string data1 {"42 cats"};
+ dht::Value value1 {(const uint8_t*)data1.data(), data1.size()};
+ value1.user_type = "test";
+
+ std::string data2 {"420 cats"};
+ dht::Value value2 {(const uint8_t*)data2.data(), data2.size()};
+ dht::Value value3 {(const uint8_t*)data2.data(), data2.size()};
+ value3.user_type = "test";
+
+ CPPUNIT_ASSERT(!isPairSize(value1));
+ CPPUNIT_ASSERT(isUserTypeTest(value1));
+
+ auto isBoth = dht::Value::Filter::chain(isPairSize, isUserTypeTest);
+ auto isUserTypeTest2 = dht::Value::Filter::chain(defaultFiler, isUserTypeTest);
+
+ CPPUNIT_ASSERT(isUserTypeTest2(value1));
+ CPPUNIT_ASSERT(!isUserTypeTest2(value2));
+ CPPUNIT_ASSERT(!isBoth(value1));
+ CPPUNIT_ASSERT(!isBoth(value2));
+ CPPUNIT_ASSERT(isBoth(value3));
+}
+
+void
+ValueTester::tearDown() {
+
+}
+} // namespace test
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ *
+ * Author: Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+// cppunit
+#include <cppunit/TestFixture.h>
+#include <cppunit/extensions/HelperMacros.h>
+
+namespace test {
+
+class ValueTester : public CppUnit::TestFixture {
+ CPPUNIT_TEST_SUITE(ValueTester);
+ CPPUNIT_TEST(testConstructors);
+ CPPUNIT_TEST(testFilter);
+ CPPUNIT_TEST_SUITE_END();
+
+ public:
+ /**
+ * Method automatically called before each test by CppUnit
+ */
+ void setUp();
+ /**
+ * Method automatically called after each test CppUnit
+ */
+ void tearDown();
+ /**
+ * Test the differents behaviors of constructors
+ */
+ void testConstructors();
+ /**
+ * Test compare operators
+ */
+ void testFilter();
+};
+
+} // namespace test
--- /dev/null
+if (OPENDHT_STATIC)
+ if (MSVC)
+ set (MSC_COMPAT_SOURCES ${MSC_COMPAT_DIR}/wingetopt.c)
+ endif ()
+endif ()
+
+function (configure_tool name extra_files)
+ add_executable (${name} ${name}.cpp ${extra_files})
+ add_dependencies(${name} opendht)
+ target_link_libraries (${name} LINK_PUBLIC opendht ${READLINE_LIBRARIES})
+ if (MSVC)
+ target_sources(${name} PRIVATE ${MSC_COMPAT_SOURCES})
+ target_include_directories (${name} PRIVATE ${MSC_COMPAT_DIR})
+ endif ()
+endfunction ()
+
+configure_tool (dhtnode tools_common.h)
+configure_tool (dhtscanner tools_common.h)
+configure_tool (dhtchat tools_common.h)
+if (NOT MSVC)
+ configure_tool (perftest tools_common.h)
+endif ()
+if (OPENDHT_HTTP)
+ configure_tool (durl tools_common.h)
+endif ()
+
+if (OPENDHT_C)
+ add_executable (dhtcnode dhtcnode.c)
+ add_dependencies(dhtcnode opendht-c)
+ target_link_libraries (dhtcnode LINK_PUBLIC opendht-c ${READLINE_LIBRARIES})
+ target_include_directories (dhtcnode SYSTEM PRIVATE ${PROJECT_SOURCE_DIR}/c)
+endif ()
+
+if (NOT DEFINED CMAKE_INSTALL_BINDIR)
+ set(CMAKE_INSTALL_BINDIR bin)
+endif ()
+
+install (TARGETS dhtnode dhtscanner dhtchat RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
+
+if (OPENDHT_SYSTEMD)
+ if (NOT DEFINED OPENDHT_SYSTEMD_UNIT_FILE_LOCATION OR NOT OPENDHT_SYSTEMD_UNIT_FILE_LOCATION)
+ execute_process(COMMAND ${PKG_CONFIG_EXECUTABLE} systemd --variable=systemdsystemunitdir
+ OUTPUT_VARIABLE SYSTEMD_UNIT_INSTALL_DIR)
+ message("-- Using Systemd unit installation directory by pkg-config: " ${SYSTEMD_UNIT_INSTALL_DIR})
+ else()
+ message("-- Using Systemd unit installation directory requested: " ${OPENDHT_SYSTEMD_UNIT_FILE_LOCATION})
+ set(SYSTEMD_UNIT_INSTALL_DIR ${OPENDHT_SYSTEMD_UNIT_FILE_LOCATION})
+ endif()
+
+ configure_file (
+ systemd/dhtnode.service.in
+ systemd/dhtnode.service
+ @ONLY
+ )
+ if (SYSTEMD_UNIT_INSTALL_DIR)
+ string(REGEX REPLACE "[ \t\n]+" "" SYSTEMD_UNIT_INSTALL_DIR "${SYSTEMD_UNIT_INSTALL_DIR}")
+ set (systemdunitdir "${SYSTEMD_UNIT_INSTALL_DIR}")
+ install (FILES ${CMAKE_CURRENT_BINARY_DIR}/systemd/dhtnode.service DESTINATION ${systemdunitdir})
+ install (FILES systemd/dhtnode.conf DESTINATION ${sysconfdir})
+ else()
+ message(WARNING "Systemd unit installation directory not found. The systemd unit won't be installed.")
+ endif()
+
+ if (OPENDHT_PYTHON)
+ configure_file (
+ systemd/dhtcluster.service.in
+ systemd/dhtcluster.service
+ @ONLY
+ )
+ if (SYSTEMD_UNIT_INSTALL_DIR)
+ install (FILES ${CMAKE_CURRENT_BINARY_DIR}/systemd/dhtcluster.service DESTINATION ${systemdunitdir})
+ install (FILES systemd/dhtcluster.conf DESTINATION ${sysconfdir})
+ endif()
+ endif()
+endif ()
--- /dev/null
+bin_PROGRAMS = dhtnode dhtchat dhtscanner
+noinst_HEADERS = tools_common.h
+
+AM_CPPFLAGS = -isystem @top_srcdir@/include @JsonCpp_CFLAGS@ @MsgPack_CFLAGS@
+
+dhtnode_SOURCES = dhtnode.cpp
+dhtnode_LDFLAGS = -lopendht -lreadline -lfmt -L@top_builddir@/src/.libs @GnuTLS_LIBS@
+
+dhtchat_SOURCES = dhtchat.cpp
+dhtchat_LDFLAGS = -lopendht -lreadline -lfmt -L@top_builddir@/src/.libs @GnuTLS_LIBS@
+
+dhtscanner_SOURCES = dhtscanner.cpp
+dhtscanner_LDFLAGS = -lopendht -lreadline -lfmt -L@top_builddir@/src/.libs @GnuTLS_LIBS@
+
+if ENABLE_C
+bin_PROGRAMS += dhtcnode
+dhtcnode_CFLAGS = -std=c11 -isystem @top_srcdir@/c -isystem @top_srcdir@/include
+dhtcnode_SOURCES = dhtcnode.c
+dhtcnode_LDFLAGS = -lopendht-c -lreadline -lfmt -L@top_builddir@/c/.libs
+endif
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ *
+ * Author: Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "tools_common.h"
+#include <opendht/rng.h>
+
+extern "C" {
+#include <gnutls/gnutls.h>
+}
+#include <ctime>
+
+using namespace dht;
+
+static std::mt19937_64 rd {dht::crypto::random_device{}()};
+static std::uniform_int_distribution<dht::Value::Id> rand_id;
+
+const std::string printTime(const std::time_t& now) {
+ struct tm tstruct = *localtime(&now);
+ char buf[80];
+ strftime(buf, sizeof(buf), "%Y-%m-%d %X", &tstruct);
+ return buf;
+}
+
+void print_usage() {
+ std::cout << "Usage: dhtchat [-n network_id] [-p local_port] [-b bootstrap_host[:port]]" << std::endl << std::endl;
+ std::cout << "dhtchat, a simple OpenDHT command line chat client." << std::endl;
+ std::cout << "Report bugs to: https://opendht.net" << std::endl;
+}
+
+int
+main(int argc, char **argv)
+{
+ auto params = parseArgs(argc, argv);
+ if (params.help) {
+ print_usage();
+ return 0;
+ }
+#ifdef _MSC_VER
+ gnutls_global_init();
+#endif
+
+ DhtRunner dht;
+ try {
+ params.generate_identity = true;
+ auto dhtConf = getDhtConfig(params);
+ dht.run(params.port, dhtConf.first, std::move(dhtConf.second));
+
+ if (not params.bootstrap.empty())
+ dht.bootstrap(params.bootstrap);
+
+ print_node_info(dht.getNodeInfo());
+ std::cout << " type 'c {hash}' to join a channel" << std::endl << std::endl;
+
+ bool connected {false};
+ InfoHash room;
+ std::future<size_t> token;
+
+ const InfoHash myid = dht.getId();
+
+#ifndef _MSC_VER
+ // using the GNU History API
+ using_history();
+#endif
+
+ while (true)
+ {
+ // using the GNU Readline API
+ std::string line = readLine(connected ? PROMPT : "> ");
+ if (!line.empty() && line[0] == '\0')
+ break;
+ if (line.empty())
+ continue;
+
+ std::istringstream iss(line);
+ std::string op, idstr;
+ iss >> op;
+ if (not connected) {
+ if (op == "x" || op == "q" || op == "exit" || op == "quit")
+ break;
+ else if (op == "c") {
+ iss >> idstr;
+ room = InfoHash(idstr);
+ if (not room) {
+ room = InfoHash::get(idstr);
+ std::cout << "Joining h(" << idstr << ") = " << room << std::endl;
+ }
+
+ token = dht.listen<dht::ImMessage>(room, [&](dht::ImMessage&& msg) {
+ if (msg.from != myid)
+ std::cout << msg.from.toString() << " at " << printTime(msg.date)
+ << " (took " << print_dt(std::chrono::system_clock::now() - std::chrono::system_clock::from_time_t(msg.date))
+ << "s) " << (msg.to == myid ? "ENCRYPTED ":"") << ": " << msg.id << " - " << msg.msg << std::endl;
+ return true;
+ });
+ connected = true;
+ } else {
+ std::cout << "Unknown command. Type 'c {hash}' to join a channel" << std::endl << std::endl;
+ }
+ } else {
+ auto now = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
+ if (op == "d") {
+ dht.cancelListen(room, std::move(token));
+ connected = false;
+ continue;
+ } else if (op == "e") {
+ iss >> idstr;
+ std::getline(iss, line);
+ dht.putEncrypted(room, InfoHash(idstr), dht::ImMessage(rand_id(rd), std::move(line), now), [](bool ok) {
+ //dht.cancelPut(room, id);
+ if (not ok)
+ std::cout << "Message publishing failed !" << std::endl;
+ });
+ } else {
+ dht.putSigned(room, dht::ImMessage(rand_id(rd), std::move(line), now), [](bool ok) {
+ //dht.cancelPut(room, id);
+ if (not ok)
+ std::cout << "Message publishing failed !" << std::endl;
+ });
+ }
+ }
+ }
+ } catch(const std::exception&e) {
+ std::cerr << std::endl << e.what() << std::endl;
+ }
+
+ std::cout << std::endl << "Stopping node..." << std::endl;
+ dht.join();
+#ifdef _MSC_VER
+ gnutls_global_deinit();
+#endif
+ return 0;
+}
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ * Author : Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <opendht_c.h>
+
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <stdatomic.h>
+
+#include <getopt.h>
+#include <readline/readline.h>
+#include <readline/history.h>
+#include <arpa/inet.h>
+
+struct op_context {
+ dht_runner* runner;
+ atomic_bool stop;
+};
+struct listen_context {
+ dht_runner* runner;
+ dht_op_token* token;
+ size_t count;
+};
+
+bool dht_value_callback(const dht_value* value, bool expired, void* user_data)
+{
+ struct listen_context* ctx = (struct listen_context*) user_data;
+ if (expired)
+ ctx->count--;
+ else
+ ctx->count++;
+ dht_data_view data = dht_value_get_data(value);
+ printf("Listen: %s value: %.*s (total %zu).\n", expired ? "expired" : "new", (int)data.size, data.data, ctx->count);
+ return true;
+}
+
+bool dht_get_callback(const dht_value* value, void* user_data)
+{
+ dht_runner* runner = (dht_runner*)user_data;
+ dht_data_view data = dht_value_get_data(value);
+ printf("Get callback: %.*s.\n", (int)data.size, data.data);
+ return true;
+}
+
+void dht_get_done_callback(bool ok, void* user_data)
+{
+ dht_runner* runner = (dht_runner*)user_data;
+ printf("Get completed: %s\n", ok ? "success !" : "failure :-(");
+}
+
+void dht_put_done_callback(bool ok, void* user_data)
+{
+ dht_runner* runner = (dht_runner*)user_data;
+ printf("Put completed: %s\n", ok ? "success !" : "failure :-(");
+}
+
+void dht_shutdown_callback(void* user_data)
+{
+ printf("Stopped.\n");
+ struct op_context* ctx = (struct op_context*)user_data;
+ atomic_store(&ctx->stop, true);
+}
+
+void listen_context_free(void* user_data)
+{
+ printf("listen_context_free.\n");
+ struct listen_context* ctx = (struct listen_context*)user_data;
+ dht_op_token_delete(ctx->token);
+ free(ctx);
+}
+
+char* print_addr(const struct sockaddr* addr) {
+ char* s = NULL;
+ switch(addr->sa_family) {
+ case AF_INET: {
+ struct sockaddr_in *addr_in = (struct sockaddr_in *)addr;
+ s = malloc(INET_ADDRSTRLEN);
+ inet_ntop(AF_INET, &(addr_in->sin_addr), s, INET_ADDRSTRLEN);
+ break;
+ }
+ case AF_INET6: {
+ struct sockaddr_in6 *addr_in6 = (struct sockaddr_in6 *)addr;
+ s = malloc(INET6_ADDRSTRLEN);
+ inet_ntop(AF_INET6, &(addr_in6->sin6_addr), s, INET6_ADDRSTRLEN);
+ break;
+ }
+ default:
+ break;
+ }
+ return s;
+}
+
+struct dht_params {
+ bool help;
+ bool version;
+ bool generate_identity;
+ bool service;
+ bool peer_discovery;
+ bool log;
+ const char* bootstrap;
+ unsigned network;
+ in_port_t port;
+};
+
+static const struct option long_options[] = {
+ {"help", no_argument , NULL, 'h'},
+ {"port", required_argument, NULL, 'p'},
+ {"net", required_argument, NULL, 'n'},
+ {"bootstrap", required_argument, NULL, 'b'},
+ {"identity", no_argument , NULL, 'i'},
+ {"verbose", no_argument , NULL, 'v'},
+ {"service", no_argument , NULL, 's'},
+ {"peer-discovery", no_argument , NULL, 'D'},
+ {"no-rate-limit", no_argument , NULL, 'U'},
+ {"persist", required_argument, NULL, 'f'},
+ {"logfile", required_argument, NULL, 'l'},
+ {"syslog", no_argument , NULL, 'L'},
+ {"version", no_argument , NULL, 'V'},
+ {NULL, 0 , NULL, 0}
+};
+
+struct dht_params
+parse_args(int argc, char **argv) {
+ struct dht_params params;
+ memset(¶ms, 0, sizeof params);
+ int opt;
+ while ((opt = getopt_long(argc, argv, "hisvDp:n:b:f:l:", long_options, NULL)) != -1) {
+ switch (opt) {
+ case 'p': {
+ int port_arg = atoi(optarg);
+ if (port_arg >= 0 && port_arg < 0x10000)
+ params.port = port_arg;
+ }
+ break;
+ case 'D':
+ params.peer_discovery = true;
+ break;
+ case 'n':
+ params.network = strtoul(optarg, NULL, 0);
+ break;
+ case 'b':
+ params.bootstrap = (optarg[0] == '=') ? optarg+1 : optarg;
+ break;
+ case 'h':
+ params.help = true;
+ break;
+ case 'v':
+ params.log = true;
+ break;
+ case 'i':
+ params.generate_identity = true;
+ break;
+ case 's':
+ params.service = true;
+ break;
+ case 'V':
+ params.version = true;
+ break;
+ default:
+ break;
+ }
+ }
+ return params;
+}
+
+dht_infohash parse_key(const char* key_str) {
+ dht_infohash key;
+ dht_infohash_from_hex_null(&key, key_str);
+ if (dht_infohash_is_zero(&key)) {
+ dht_infohash_get_from_string(&key, key_str);
+ printf("Using h(%s) = %s\n", key_str, dht_infohash_print(&key));
+ }
+ return key;
+}
+
+int main(int argc, char **argv)
+{
+ struct dht_params params = parse_args(argc, argv);
+
+ if (params.version) {
+ printf("OpenDHT version %s\n", dht_version());
+ return EXIT_SUCCESS;
+ }
+
+ dht_runner* runner = dht_runner_new();
+ dht_runner_config dht_config;
+ dht_runner_config_default(&dht_config);
+ dht_config.peer_discovery = params.peer_discovery; // Look for other peers on the network
+ dht_config.peer_publish = params.peer_discovery; // Publish our own peer info
+ dht_config.dht_config.node_config.network = params.network;
+ dht_config.log = params.log;
+ dht_runner_run_config(runner, params.port, &dht_config);
+
+ if (params.bootstrap) {
+ printf("Bootstrap using %s\n", params.bootstrap);
+ dht_runner_bootstrap(runner, params.bootstrap, NULL);
+ }
+
+ char cmd[64];
+ char arg[64];
+ char value[256];
+ dht_infohash key;
+ while (true) {
+ const char* line_read = readline("> ");
+ if (!line_read)
+ break;
+ if (!*line_read)
+ continue;
+ add_history(line_read);
+
+ memset(cmd, 0, sizeof cmd);
+ memset(arg, 0, sizeof arg);
+ memset(value, 0, sizeof value);
+ sscanf(line_read, "%63s %63s %255s", cmd, arg, value);
+
+ if (!strcmp(cmd, "la")) {
+ struct sockaddr** addrs = dht_runner_get_public_address(runner);
+ if (addrs) {
+ for (struct sockaddr** addrIt = addrs; *addrIt; addrIt++) {
+ struct sockaddr* addr = *addrIt;
+ char* addr_str = print_addr(addr);
+ free(addr);
+ printf("Found public address: %s\n", addr_str);
+ free(addr_str);
+ }
+ free(addrs);
+ }
+ }
+ else if (!strcmp(cmd, "ll")) {
+ key = dht_runner_get_node_id(runner);
+ printf("DHT node %s running on port %u\n", dht_infohash_print(&key), dht_runner_get_bound_port(runner, AF_INET));
+ }
+ else if (!strcmp(cmd, "g")) {
+ key = parse_key(arg);
+ dht_runner_get(runner, &key, dht_get_callback, dht_get_done_callback, runner);
+ }
+ else if (!strcmp(cmd, "l")) {
+ key = parse_key(arg);
+ struct listen_context* ctx = malloc(sizeof(struct listen_context));
+ ctx->runner = runner;
+ ctx->count = 0;
+ ctx->token = dht_runner_listen(runner, &key, dht_value_callback, listen_context_free, ctx);
+ }
+ else if (!strcmp(cmd, "p")) {
+ key = parse_key(arg);
+ dht_value* val = dht_value_new_from_string(value);
+ dht_runner_put(runner, &key, val, dht_put_done_callback, runner, true);
+ dht_value_unref(val);
+ }
+ else {
+ printf("Unkown command: %s\n", cmd);
+ }
+ }
+
+ // Graceful shutdown
+ printf("Stopping…\n");
+ struct op_context ctx;
+ ctx.runner = runner;
+ atomic_init(&ctx.stop, false);
+ dht_runner_shutdown(runner, dht_shutdown_callback, &ctx);
+
+ // Wait until shutdown callback is called
+ while (!atomic_load(&ctx.stop)) {
+ usleep(10000);
+ }
+ dht_runner_delete(runner);
+ return EXIT_SUCCESS;
+}
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ *
+ * Authors: Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ * Simon Désaulniers <simon.desaulniers@savoirfairelinux.com>
+ * Sébastien Blin <sebastien.blin@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "tools_common.h"
+extern "C" {
+#include <gnutls/gnutls.h>
+}
+
+#include <set>
+#include <thread> // std::this_thread::sleep_for
+
+using namespace dht;
+
+void print_info() {
+ std::cout << "dhtnode, a simple OpenDHT command line node runner." << std::endl;
+ std::cout << "Report bugs to: https://opendht.net" << std::endl;
+}
+
+void print_version() {
+ std::cout << "OpenDHT version " << dht::version() << std::endl;
+ print_info();
+}
+
+void print_usage() {
+ std::cout << "Usage: dhtnode [-v [-l logfile]] [-i] [-d] [-n network_id] [-p local_port] [-b bootstrap_host[:port]] [--proxyserver local_port] [--proxyserverssl local_port] [--bundleid bundleid] [--pushserver endpoint]" << std::endl << std::endl;
+ print_info();
+}
+
+void print_id_req() {
+ std::cout << "An identity is required to perform this operation (run with -i)" << std::endl;
+}
+
+void print_help() {
+ std::cout << "OpenDHT command line interface (CLI)" << std::endl;
+ std::cout << "Possible commands:" << std::endl
+ << " h, help Print this help message." << std::endl
+ << " x, quit Quit the program." << std::endl
+ << " log Start/stop printing DHT logs." << std::endl;
+
+ std::cout << std::endl << "Node information:" << std::endl
+ << " ll Print basic information and stats about the current node." << std::endl
+ << " ls [key] Print basic information about current search(es)." << std::endl
+ << " ld [key] Print basic information about currenty stored values on this node (or key)." << std::endl
+ << " lr Print the full current routing table of this node." << std::endl;
+
+#ifdef OPENDHT_PROXY_SERVER
+ std::cout << std::endl << "Operations with the proxy:" << std::endl
+#ifdef OPENDHT_PUSH_NOTIFICATIONS
+ << " pst [port] <pushServer> Start the proxy interface on port." << std::endl
+#else
+ << " pst [port] Start the proxy interface on port." << std::endl
+ << " psx [port] Start the proxy ssl interface on port." << std::endl
+#endif // OPENDHT_PUSH_NOTIFICATIONS
+ << " psp [port] Stop the proxy interface on port." << std::endl;
+#endif //OPENDHT_PROXY_SERVER
+
+#ifdef OPENDHT_PROXY_CLIENT
+ std::cout << std::endl << "Operations with the proxy:" << std::endl
+#ifdef OPENDHT_PUSH_NOTIFICATIONS
+ << " stt [server_address] <device_key> Start the proxy client." << std::endl
+ << " rs [token] Resubscribe to opendht." << std::endl
+ << " rp [token] Inject a push notification in Opendht." << std::endl
+#else
+ << " stt [server_address] Start the proxy client." << std::endl
+#endif // OPENDHT_PUSH_NOTIFICATIONS
+ << " stp Stop the proxy client." << std::endl;
+#endif //OPENDHT_PROXY_CLIENT
+
+ std::cout << std::endl << "Operations on the DHT:" << std::endl
+ << " b <ip:port> Ping potential node at given IP address/port." << std::endl
+ << " g <key> Get values at <key>." << std::endl
+ << " l <key> Listen for value changes at <key>." << std::endl
+ << " cl <key> <token> Cancel listen for <token> and <key>." << std::endl
+ << " p <key> <str> Put string value at <key>." << std::endl
+ << " pp <key> <str> Put string value at <key> (persistent version)." << std::endl
+ << " cpp <key> <id> Cancel persistent put operation for <key> and value <id>." << std::endl
+ << " s <key> <str> Put string value at <key>, signed with our generated private key." << std::endl
+ << " e <key> <dest> <str> Put string value at <key>, encrypted for <dest> with its public key (if found)." << std::endl
+ << " cc Trigger connectivity changed signal." << std::endl;
+
+#ifdef OPENDHT_INDEXATION
+ std::cout << std::endl << "Indexation operations on the DHT:" << std::endl
+ << " il <name> <key> [exact match] Lookup the index named <name> with the key <key>." << std::endl
+ << " Set [exact match] to 'false' for inexact match lookup." << std::endl
+ << " ii <name> <key> <value> Inserts the value <value> under the key <key> in the index named <name>." << std::endl
+ << std::endl;
+#endif
+}
+
+void cmd_loop(std::shared_ptr<DhtRunner>& node, dht_params& params
+#ifdef OPENDHT_PROXY_SERVER
+ , std::map<in_port_t, std::unique_ptr<DhtProxyServer>>& proxies
+#endif
+)
+{
+ print_node_info(node->getNodeInfo());
+ std::cout << " (type 'h' or 'help' for a list of possible commands)" << std::endl << std::endl;
+
+#ifndef _MSC_VER
+ // using the GNU History API
+ using_history();
+#endif
+
+#ifdef OPENDHT_INDEXATION
+ std::map<std::string, indexation::Pht> indexes;
+#endif
+
+ while (true)
+ {
+ // using the GNU Readline API
+ std::string line = readLine();
+ if (!line.empty() && line[0] == '\0')
+ break;
+
+ std::istringstream iss(line);
+ std::string op, idstr, value, index, keystr, pushServer, deviceKey;
+ iss >> op;
+
+ if (op == "x" || op == "exit" || op == "quit") {
+ break;
+ } else if (op == "h" || op == "help") {
+ print_help();
+ continue;
+ } else if (op == "ll") {
+ node->getNodeInfo([&](const std::shared_ptr<dht::NodeInfo>& nodeInfo) {
+ print_node_info(*nodeInfo);
+ std::cout << nodeInfo->ongoing_ops << " ongoing operations" << std::endl;
+ std::cout << "Storage has " << nodeInfo->storage_values << " values, using " << (nodeInfo->storage_size/1024) << " KB" << std::endl;
+ std::cout << "IPv4 stats:" << std::endl;
+ std::cout << nodeInfo->ipv4.toString() << std::endl;
+ std::cout << "IPv6 stats:" << std::endl;
+ std::cout << nodeInfo->ipv6.toString() << std::endl;
+#ifdef OPENDHT_PROXY_SERVER
+ for (const auto& proxy : proxies) {
+ std::cout << "Stats for proxy server on port " << proxy.first << std::endl;
+ if (auto stats = proxy.second->stats())
+ std::cout << " " << stats->toString() << std::endl;
+ else
+ std::cout << " (stats not available yet)" << std::endl;
+ }
+#endif
+ });
+ continue;
+ } else if (op == "lr") {
+ std::cout << "IPv4 routing table:" << std::endl;
+ std::cout << node->getRoutingTablesLog(AF_INET) << std::endl;
+ std::cout << "IPv6 routing table:" << std::endl;
+ std::cout << node->getRoutingTablesLog(AF_INET6) << std::endl;
+ continue;
+ } else if (op == "ld") {
+ iss >> idstr;
+ InfoHash filter(idstr);
+ if (filter)
+ std::cout << node->getStorageLog(filter) << std::endl;
+ else
+ std::cout << node->getStorageLog() << std::endl;
+ continue;
+ } else if (op == "ls") {
+ iss >> idstr;
+ InfoHash filter(idstr);
+ if (filter)
+ std::cout << node->getSearchLog(filter) << std::endl;
+ else
+ std::cout << node->getSearchesLog() << std::endl;
+ continue;
+ } else if (op == "la") {
+ std::cout << "Reported public addresses:" << std::endl;
+ auto addrs = node->getPublicAddressStr();
+ for (const auto& addr : addrs)
+ std::cout << addr << std::endl;
+ continue;
+ } else if (op == "b") {
+ iss >> idstr;
+ if (not idstr.empty())
+ node->bootstrap(idstr);
+ else
+ std::cerr << "No service provided. Syntax: b hostname:port" << std::endl;
+ continue;
+ } else if (op == "log") {
+ iss >> idstr;
+ InfoHash filter(idstr);
+ params.log = filter ? true : !params.log;
+ if (params.log)
+ log::enableLogging(*node);
+ else
+ log::disableLogging(*node);
+ node->setLogFilter(filter);
+ continue;
+ } else if (op == "cc") {
+ node->connectivityChanged();
+ continue;
+ }
+#ifdef OPENDHT_PROXY_SERVER
+ else if (op == "pst") {
+#ifdef OPENDHT_PUSH_NOTIFICATIONS
+ iss >> idstr >> pushServer;
+#else
+ iss >> idstr;
+#endif // OPENDHT_PUSH_NOTIFICATIONS
+ try {
+ in_port_t port = std::stoi(idstr);
+ ProxyServerConfig serverConfig;
+ serverConfig.port = port;
+ serverConfig.pushServer = pushServer;
+ proxies.emplace(port, std::make_unique<DhtProxyServer>(node, serverConfig));
+ } catch (...) { }
+ continue;
+ } else if (op == "psx") {
+#ifdef OPENDHT_PUSH_NOTIFICATIONS
+ iss >> idstr >> pushServer;
+#else
+ iss >> idstr;
+#endif // OPENDHT_PUSH_NOTIFICATIONS
+ try {
+ if (params.proxy_id.first and params.proxy_id.second){
+ in_port_t port = std::stoi(idstr);
+ ProxyServerConfig serverConfig;
+ serverConfig.identity = params.proxy_id;
+ serverConfig.port = port;
+ serverConfig.pushServer = pushServer;
+ proxies.emplace(port, std::make_unique<DhtProxyServer>(node, serverConfig));
+ }
+ else {
+ std::cerr << "Missing Identity private key or certificate" << std::endl;
+ }
+ } catch (...) { }
+ continue;
+ } else if (op == "psp") {
+ iss >> idstr;
+ try {
+ auto it = proxies.find(std::stoi(idstr));
+ if (it != proxies.end())
+ proxies.erase(it);
+ } catch (...) { }
+ continue;
+ }
+#endif //OPENDHT_PROXY_SERVER
+#ifdef OPENDHT_PROXY_CLIENT
+ else if (op == "stt") {
+ node->enableProxy(true);
+ continue;
+ } else if (op == "stp") {
+ node->enableProxy(false);
+ continue;
+ }
+#ifdef OPENDHT_PUSH_NOTIFICATIONS
+ else if (op == "rp") {
+ iss >> value;
+ node->pushNotificationReceived({{"to", "dhtnode"}, {"token", value}});
+ continue;
+ }
+#endif // OPENDHT_PUSH_NOTIFICATIONS
+#endif //OPENDHT_PROXY_CLIENT
+
+ if (op.empty())
+ continue;
+
+ static const std::set<std::string> VALID_OPS {"g", "l", "cl", "il", "ii", "p", "pp", "cpp", "s", "e", "a", "q"};
+ if (VALID_OPS.find(op) == VALID_OPS.cend()) {
+ std::cout << "Unknown command: " << op << std::endl;
+ std::cout << " (type 'h' or 'help' for a list of possible commands)" << std::endl;
+ continue;
+ }
+ dht::InfoHash id;
+
+ if (false) {}
+#ifdef OPENDHT_INDEXATION
+ else if (op == "il" or op == "ii") {
+ // Pht syntax
+ iss >> index >> keystr;
+ auto new_index = std::find_if(indexes.begin(), indexes.end(),
+ [&](std::pair<const std::string, indexation::Pht>& i) {
+ return i.first == index;
+ }) == indexes.end();
+ if (not index.size()) {
+ std::cerr << "You must enter the index name." << std::endl;
+ continue;
+ } else if (new_index) {
+ using namespace dht::indexation;
+ try {
+ auto key = createPhtKey(parseStringMap(keystr));
+ Pht::KeySpec ks;
+ std::transform(key.begin(), key.end(), std::inserter(ks, ks.end()), [](Pht::Key::value_type& f) {
+ return std::make_pair(f.first, f.second.size());
+ });
+ indexes.emplace(index, Pht {index, std::move(ks), node});
+ } catch (std::invalid_argument& e) { std::cout << e.what() << std::endl; }
+ }
+ }
+#endif
+ else {
+ // Dht syntax
+ iss >> idstr;
+ id = dht::InfoHash(idstr);
+ if (not id) {
+ if (idstr.empty()) {
+ std::cerr << "Syntax error: invalid InfoHash." << std::endl;
+ continue;
+ }
+ id = InfoHash::get(idstr);
+ std::cout << "Using h(" << idstr << ") = " << id << std::endl;
+ }
+ }
+
+ // Dht
+ auto start = std::chrono::high_resolution_clock::now();
+ if (op == "g") {
+ std::string rem;
+ std::getline(iss, rem);
+ auto total = std::make_shared<size_t>();
+ node->get(id, [start, total](const std::vector<std::shared_ptr<Value>>& values) {
+ auto now = std::chrono::high_resolution_clock::now();
+ (*total) += values.size();
+ std::cout << "Get: found " << values.size() << " value(s) after " << print_duration(now-start) << " (total " << *total << ')' << std::endl;
+ for (const auto& value : values)
+ std::cout << "\t" << *value << std::endl;
+ return true;
+ }, [start, total](bool ok) {
+ auto end = std::chrono::high_resolution_clock::now();
+ std::cout << "Get: " << (ok ? "completed" : "failure") << ", took " << print_duration(end-start) << " (total " << *total << ')' << std::endl;
+ }, {}, dht::Where {rem});
+ }
+ else if (op == "q") {
+ std::string rem;
+ std::getline(iss, rem);
+ node->query(id, [start](const std::vector<std::shared_ptr<FieldValueIndex>>& field_value_indexes) {
+ auto now = std::chrono::high_resolution_clock::now();
+ for (auto& index : field_value_indexes) {
+ std::cout << "Query: found field value index after " << print_duration(now-start) << std::endl;
+ std::cout << "\t" << *index << std::endl;
+ }
+ return true;
+ }, [start](bool ok) {
+ auto end = std::chrono::high_resolution_clock::now();
+ std::cout << "Query: " << (ok ? "completed" : "failure") << ", took " << print_duration(end-start) << std::endl;
+ }, dht::Query {rem});
+ }
+ else if (op == "l") {
+ std::string rem;
+ std::getline(iss, rem);
+ auto total = std::make_shared<size_t>();
+ auto token = node->listen(id, [total](const std::vector<std::shared_ptr<Value>>& values, bool expired) {
+ if (expired)
+ (*total) -= values.size();
+ else
+ (*total) += values.size();
+ std::cout << "Listen: found " << values.size() << " values" << (expired ? " expired" : "") << " (total " << *total << ')' << std::endl;
+ for (const auto& value : values)
+ std::cout << "\t" << *value << std::endl;
+ return true;
+ }, {}, dht::Where {rem});
+ auto t = token.get();
+ std::cout << "Listening, token: " << t << std::endl;
+ }
+ if (op == "cl") {
+ std::string rem;
+ iss >> rem;
+ size_t token;
+ try {
+ token = std::stoul(rem);
+ } catch(...) {
+ std::cerr << "Syntax: cl [key] [token]" << std::endl;
+ continue;
+ }
+ node->cancelListen(id, token);
+ }
+ else if (op == "p") {
+ std::string v;
+ iss >> v;
+ auto value = std::make_shared<dht::Value>(
+ dht::ValueType::USER_DATA.id,
+ std::vector<uint8_t> {v.begin(), v.end()});
+ value->user_type = "text/plain";
+ node->put(id, value, [start, value](bool ok) {
+ auto end = std::chrono::high_resolution_clock::now();
+ auto flags(std::cout.flags());
+ std::cout << "Put: " << (ok ? "success" : "failure") << ", took " << print_duration(end-start) << ". Value ID: " << std::hex << value->id << std::endl;
+ std::cout.flags(flags);
+ });
+ }
+ else if (op == "pp") {
+ std::string v;
+ iss >> v;
+ auto value = std::make_shared<dht::Value>(
+ dht::ValueType::USER_DATA.id,
+ std::vector<uint8_t> {v.begin(), v.end()}
+ );
+ value->user_type = "text/plain";
+ node->put(id, value, [start, value](bool ok) {
+ auto end = std::chrono::high_resolution_clock::now();
+ auto flags(std::cout.flags());
+ std::cout << "Put: " << (ok ? "success" : "failure") << ", took " << print_duration(end-start) << ". Value ID: " << std::hex << value->id << std::endl;
+ std::cout.flags(flags);
+ }, time_point::max(), true);
+ }
+ else if (op == "cpp") {
+ std::string rem;
+ iss >> rem;
+ node->cancelPut(id, std::stoul(rem, nullptr, 16));
+ }
+ else if (op == "s") {
+ if (not params.id.first) {
+ print_id_req();
+ continue;
+ }
+ std::string v;
+ iss >> v;
+ auto value = std::make_shared<dht::Value>();
+ value->data = {v.begin(), v.end()};
+ value->user_type = "text/plain";
+ node->putSigned(id, std::move(value), [start](bool ok) {
+ auto end = std::chrono::high_resolution_clock::now();
+ std::cout << "Put signed: " << (ok ? "success" : "failure") << " (took " << print_duration(end-start) << "s)" << std::endl;
+ });
+ }
+ else if (op == "e") {
+ if (not params.id.first) {
+ print_id_req();
+ continue;
+ }
+ std::string tostr;
+ std::string v;
+ iss >> tostr >> v;
+ auto value = std::make_shared<dht::Value>(
+ dht::ValueType::USER_DATA.id,
+ std::vector<uint8_t> {v.begin(), v.end()}
+ );
+ value->user_type = "text/plain";
+ node->putEncrypted(id, InfoHash(tostr), std::move(value), [start](bool ok) {
+ auto end = std::chrono::high_resolution_clock::now();
+ std::cout << "Put encrypted: " << (ok ? "success" : "failure") << " (took " << print_duration(end-start) << std::endl;
+ });
+ }
+ else if (op == "a") {
+ in_port_t port;
+ iss >> port;
+ node->put(id, dht::Value {dht::IpServiceAnnouncement::TYPE.id, dht::IpServiceAnnouncement(port)}, [start](bool ok) {
+ auto end = std::chrono::high_resolution_clock::now();
+ std::cout << "Announce: " << (ok ? "success" : "failure") << " (took " << print_duration(end-start) << std::endl;
+ });
+ }
+#ifdef OPENDHT_INDEXATION
+ else if (op == "il") {
+ std::string exact_match;
+ iss >> exact_match;
+ try {
+ auto key = createPhtKey(parseStringMap(keystr));
+ indexes.at(index).lookup(key,
+ [=](std::vector<std::shared_ptr<indexation::Value>>& vals, indexation::Prefix p) {
+ if (vals.empty())
+ return;
+ std::cout << "Pht::lookup: found entries!" << std::endl
+ << p.toString() << std::endl
+ << " hash: " << p.hash() << std::endl;
+ std::cout << " entries:" << std::endl;
+ for (const auto& v : vals)
+ std::cout << " " << v->first.toString() << "[vid: " << std::hex << v->second << std::dec << "]" << std::endl;
+ },
+ [start](bool ok) {
+ auto end = std::chrono::high_resolution_clock::now();
+ std::cout << "Pht::lookup: " << (ok ? "done." : "failed.")
+ << " took " << print_duration(end-start) << std::endl;
+
+ }, exact_match.size() != 0 and exact_match == "false" ? false : true
+ );
+ }
+ catch (std::invalid_argument& e) { std::cout << e.what() << std::endl; }
+ catch (std::out_of_range& e) { }
+ }
+ else if (op == "ii") {
+ iss >> idstr;
+ InfoHash h {idstr};
+ if (not isInfoHash(h))
+ continue;
+
+ indexation::Value v {h, 0};
+ try {
+ auto key = createPhtKey(parseStringMap(keystr));
+ indexes.at(index).insert(key, v,
+ [=](bool ok) {
+ std::cout << "Pht::insert: " << (ok ? "done." : "failed.") << std::endl;
+ }
+ );
+ }
+ catch (std::invalid_argument& e) { std::cout << e.what() << std::endl; }
+ catch (std::out_of_range& e) { }
+ }
+#endif
+ }
+
+ std::cout << std::endl << "Stopping node..." << std::endl;
+}
+
+int
+main(int argc, char **argv)
+{
+#ifdef _MSC_VER
+ gnutls_global_init();
+#endif
+ auto params = parseArgs(argc, argv);
+ if (params.help) {
+ print_usage();
+ return 0;
+ }
+ if (params.version) {
+ print_version();
+ return 0;
+ }
+
+ if (params.daemonize) {
+ daemonize();
+ }
+ setupSignals();
+
+ auto node = std::make_shared<DhtRunner>();
+ try {
+ auto dhtConf = getDhtConfig(params);
+ node->run(params.port, dhtConf.first, std::move(dhtConf.second));
+
+ if (not params.bootstrap.empty()) {
+ std::cout << "Bootstrap: " << params.bootstrap << std::endl;
+ node->bootstrap(params.bootstrap);
+ }
+
+#ifdef OPENDHT_PROXY_SERVER
+ std::map<in_port_t, std::unique_ptr<DhtProxyServer>> proxies;
+#endif
+ if (params.proxyserver or params.proxyserverssl) {
+#ifdef OPENDHT_PROXY_SERVER
+ ProxyServerConfig serverConfig;
+ serverConfig.pushServer = params.pushserver;
+ serverConfig.bundleId = params.bundle_id;
+ serverConfig.address = params.proxy_address;
+ if (params.proxyserverssl and params.proxy_id.first and params.proxy_id.second){
+ serverConfig.identity = params.proxy_id;
+ serverConfig.port = params.proxyserverssl;
+ if (not params.persist_path.empty())
+ serverConfig.persistStatePath = params.persist_path + '_' + std::to_string(serverConfig.port);
+ proxies.emplace(params.proxyserverssl, std::make_unique<DhtProxyServer>(node, serverConfig, dhtConf.second.logger));
+ }
+ if (params.proxyserver) {
+ serverConfig.identity = {};
+ serverConfig.port = params.proxyserver;
+ if (not params.persist_path.empty())
+ serverConfig.persistStatePath = params.persist_path + '_' + std::to_string(serverConfig.port);
+ proxies.emplace(params.proxyserver, std::make_unique<DhtProxyServer>(node, serverConfig, dhtConf.second.logger));
+ }
+#else
+ std::cerr << "DHT proxy server requested but OpenDHT built without proxy server support." << std::endl;
+ exit(EXIT_FAILURE);
+#endif
+ }
+
+ if (params.daemonize or params.service)
+ while (runner.wait());
+ else
+ cmd_loop(node, params
+#ifdef OPENDHT_PROXY_SERVER
+ , proxies
+#endif
+ );
+
+ } catch(const std::exception&e) {
+ std::cerr << std::endl << e.what() << std::endl;
+ }
+
+ std::condition_variable cv;
+ std::mutex m;
+ bool done {false};
+
+ node->shutdown([&]()
+ {
+ done = true;
+ cv.notify_all();
+ });
+
+ // wait for shutdown
+ std::unique_lock<std::mutex> lk(m);
+ cv.wait(lk, [&](){ return done; });
+
+ node->join();
+#ifdef _MSC_VER
+ gnutls_global_deinit();
+#endif
+ return 0;
+}
--- /dev/null
+import requests
+import time
+
+ts = time.time()
+
+stats_total = {"users":0, "pushListenersCount":0, "listenCount":0, "totalListeners":0, "totalPermanentPuts":0, "timestamp": str(ts)}
+
+for i in range(80,101):
+ print("Collecting stats for proxy " + str(i))
+ response = requests.request('GET', 'http://127.0.0.1:' + str(i) + '/node/stats')
+
+ if response.status_code == 200:
+ result = response.json()
+
+ stats = {}
+ # Get Total users
+ try:
+ stats['users'] = int(int(result["putCount"])/2)
+ stats_total['users'] += int(int(result["putCount"])/2)
+ except:
+ pass
+ # Get android push
+ try:
+ stats['pushListenersCount'] = int(result["pushListenersCount"])
+ stats_total['pushListenersCount'] += int(result["pushListenersCount"])
+ except:
+ pass
+ # Get Listeners
+ try:
+ stats['listenCount'] = int(result["listenCount"])
+ stats_total['listenCount'] += int(result["listenCount"])
+ except:
+ pass
+ # Get permanents put nb
+ try:
+ stats['totalListeners'] = int(result["pushListenersCount"]) + int(result["listenCount"])
+ stats_total['totalListeners'] += int(result["pushListenersCount"]) + int(result["listenCount"])
+ except:
+ pass
+ try:
+ stats['totalPermanentPuts'] = int(result["totalPermanentPuts"])
+ stats_total['totalPermanentPuts'] += int(result["totalPermanentPuts"])
+ except:
+ pass
+
+ stats['timestamp'] = str(ts)
+
+ #with open("stats_proxy_" + str(i), "a") as stat_file:
+ # stat_file.write(str(stats))
+ # stat_file.write('\n')
+
+with open("stats_proxy_total", "w") as stat_file:
+ stat_file.write(str(stats_total))
+ stat_file.write('\n')
\ No newline at end of file
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ *
+ * Author: Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "tools_common.h"
+#include <opendht/node.h>
+
+extern "C" {
+#include <gnutls/gnutls.h>
+}
+#include <set>
+#include <condition_variable>
+#include <mutex>
+
+using namespace dht;
+
+void print_usage() {
+ std::cout << "Usage: dhtscanner [-n network_id] [-p local_port] [-b bootstrap_host[:port]]" << std::endl << std::endl;
+ std::cout << "dhtscanner, a simple OpenDHT command line utility generating scan result the network." << std::endl;
+ std::cout << "Report bugs to: https://opendht.net" << std::endl;
+}
+
+struct snode_compare {
+ bool operator() (const std::shared_ptr<Node>& lhs, const std::shared_ptr<Node>& rhs) const{
+ return (lhs->id < rhs->id) ||
+ (lhs->id == rhs->id && lhs->getFamily() == AF_INET && rhs->getFamily() == AF_INET6);
+ }
+};
+
+using NodeSet = std::set<std::shared_ptr<Node>, snode_compare>;
+std::condition_variable cv;
+
+void
+step(DhtRunner& dht, std::atomic_uint& done, std::shared_ptr<NodeSet> all_nodes, dht::InfoHash cur_h, unsigned cur_depth)
+{
+ std::cout << "step at " << cur_h << ", depth " << cur_depth << std::endl;
+ done++;
+ dht.get(cur_h, [all_nodes](const std::vector<std::shared_ptr<Value>>& /*values*/) {
+ return true;
+ }, [&,all_nodes,cur_h,cur_depth](bool, const std::vector<std::shared_ptr<Node>>& nodes) {
+ all_nodes->insert(nodes.begin(), nodes.end());
+ NodeSet sbuck {nodes.begin(), nodes.end()};
+ if (not sbuck.empty()) {
+ unsigned bdepth = sbuck.size()==1 ? 0u : InfoHash::commonBits((*sbuck.begin())->id, (*sbuck.rbegin())->id);
+ unsigned target_depth = std::min(8u, bdepth+6u);
+ std::cout << cur_h << " : " << nodes.size() << " nodes; target is " << target_depth << " bits deep (cur " << cur_depth << ")" << std::endl;
+ for (unsigned b = cur_depth ; b < target_depth; b++) {
+ auto new_h = cur_h;
+ new_h.setBit(b, 1);
+ step(dht, done, all_nodes, new_h, b+1);
+ }
+ }
+ done--;
+ std::cout << done.load() << " operations left, " << all_nodes->size() << " nodes found." << std::endl;
+ cv.notify_one();
+ });
+}
+
+int
+main(int argc, char **argv)
+{
+#ifdef _MSC_VER
+ gnutls_global_init();
+#endif
+ auto params = parseArgs(argc, argv);
+ if (params.help) {
+ print_usage();
+ return 0;
+ }
+
+ DhtRunner dht;
+ try {
+ auto dhtConf = getDhtConfig(params);
+ dht.run(params.port, dhtConf.first, std::move(dhtConf.second));
+
+ if (not params.bootstrap.empty())
+ dht.bootstrap(params.bootstrap);
+
+ print_node_info(dht.getNodeInfo());
+ std::cout << "Scanning network..." << std::endl;
+ auto all_nodes = std::make_shared<NodeSet>();
+
+ // Set hash to 1 because 0 is the null hash
+ dht::InfoHash cur_h {};
+ cur_h.setBit(8*HASH_LEN-1, 1);
+
+ std::this_thread::sleep_for(std::chrono::seconds(2));
+
+ std::atomic_uint done {0};
+ step(dht, done, all_nodes, cur_h, 0);
+
+ {
+ std::mutex m;
+ std::unique_lock<std::mutex> lk(m);
+ cv.wait(lk, [&](){
+ return done.load() == 0;
+ });
+ }
+
+ std::cout << std::endl << "Scan ended: " << all_nodes->size() << " nodes found." << std::endl;
+ for (const auto& n : *all_nodes)
+ std::cout << "Node " << *n << std::endl;
+ } catch(const std::exception&e) {
+ std::cerr << std::endl << e.what() << std::endl;
+ }
+
+ dht.join();
+#ifdef _MSC_VER
+ gnutls_global_deinit();
+#endif
+ return 0;
+}
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ *
+ * Authors: Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+#include <opendht/http.h>
+#include <opendht/log.h>
+
+#include <asio/io_context.hpp>
+#ifndef _MSC_VER
+#include <getopt.h>
+#else
+#include "wingetopt.h"
+#endif
+
+using namespace dht;
+
+void print_info() {
+ std::cout << "durl, a simple http(s) client." << std::endl;
+ std::cout << "Report bugs to: https://opendht.net" << std::endl;
+}
+
+void print_version() {
+ std::cout << "OpenDHT version " << dht::version() << std::endl;
+ print_info();
+}
+
+void print_usage() {
+ std::cout << "Usage: durl url" << std::endl << std::endl;
+ print_info();
+}
+
+static const constexpr struct option long_options[] = {
+ {"help", no_argument , nullptr, 'h'},
+ {"headers", no_argument , nullptr, 'H'},
+ {"verbose", no_argument , nullptr, 'v'},
+ {"version", no_argument , nullptr, 'V'},
+ {nullptr, 0 , nullptr, 0}
+};
+
+struct durl_params {
+ std::string url;
+ bool help {false};
+ bool log {false};
+ bool headers {false};
+ bool version {false};
+};
+
+durl_params
+parseArgs(int argc, char **argv) {
+ durl_params params;
+ int opt;
+ while ((opt = getopt_long(argc, argv, "hvVH", long_options, nullptr)) != -1) {
+ switch (opt) {
+ case 'V':
+ params.version = true;
+ break;
+ case 'h':
+ params.help = true;
+ break;
+ case 'H':
+ params.headers = true;
+ break;
+ case 'v':
+ params.log = true;
+ break;
+ default:
+ break;
+ }
+ }
+
+ if (optind < argc) {
+ params.url = argv[optind++];
+ }
+
+ return params;
+}
+
+int
+main(int argc, char **argv)
+{
+ auto params = parseArgs(argc, argv);
+ if (params.help) {
+ print_usage();
+ return 0;
+ }
+ if (params.version) {
+ print_version();
+ return 0;
+ }
+
+ std::shared_ptr<dht::Logger> logger;
+ if (params.log) {
+ logger = dht::log::getStdLogger();
+ }
+
+ asio::io_context ctx;
+ auto request = std::make_shared<dht::http::Request>(ctx, params.url, [&](const dht::http::Response& response){
+ if (params.headers) {
+ for (const auto& header : response.headers)
+ std::cout << header.first << ": " << header.second << std::endl;
+ std::cout << std::endl;
+ }
+ std::cout << response.body << std::endl;
+ ctx.stop();
+ }, logger);
+ request->send();
+ auto work = asio::make_work_guard(ctx);
+ ctx.run();
+ return 0;
+}
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ *
+ * Author: Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "tools_common.h"
+#include <opendht/node.h>
+
+extern "C" {
+#include <gnutls/gnutls.h>
+}
+#include <condition_variable>
+#include <mutex>
+#include <atomic>
+
+void print_usage() {
+ std::cout << "Usage: perftest" << std::endl << std::endl;
+ std::cout << "perftest, a simple OpenDHT basic performance tester." << std::endl;
+ std::cout << "Report bugs to: https://opendht.net" << std::endl;
+}
+constexpr unsigned PINGPONG_MAX = 2048;
+using namespace dht;
+
+namespace tests {
+
+using clock = std::chrono::high_resolution_clock;
+using duration = clock::duration;
+
+duration
+benchPingPong(unsigned netSize, unsigned n_parallel) {
+ DhtRunner::Config config {};
+ config.dht_config.node_config.max_peer_req_per_sec = -1;
+ config.dht_config.node_config.max_req_per_sec = -1;
+
+ DhtRunner ping_node, pong_node;
+
+ std::vector<std::pair<InfoHash, InfoHash>> locs;
+ locs.reserve(n_parallel);
+ for (unsigned i=0; i<n_parallel; i++) {
+ auto loc_ping = InfoHash::get("toto" + std::to_string(i));
+ locs.emplace_back(loc_ping, InfoHash::get(loc_ping.toString()));
+ }
+
+ ping_node.run(0, config);
+ pong_node.run(0, config);
+ auto bindAddr = ping_node.getBound();
+ pong_node.bootstrap(bindAddr);
+
+ std::vector<std::unique_ptr<DhtRunner>> nodes;
+ nodes.reserve(netSize);
+ for (unsigned i=0; i<netSize; i++) {
+ auto node = std::make_unique<DhtRunner>();
+ node->run(0, config);
+ node->bootstrap(bindAddr);
+ nodes.emplace_back(std::move(node));
+ }
+
+ // std::this_thread::sleep_for(std::chrono::seconds(1));
+
+ std::condition_variable cv;
+ unsigned i {0};
+ std::mutex m;
+
+ unsigned max = PINGPONG_MAX * n_parallel;
+
+ auto ping = [&](DhtRunner& node, InfoHash h) {
+ std::lock_guard<std::mutex> lk(m);
+ if (i < max) {
+ i++;
+ node.put(h, Value("hey"));
+ }
+ cv.notify_one();
+ };
+
+ for (unsigned i=0; i<n_parallel; i++) {
+ ping_node.listen(locs[i].first, [&,i](const std::shared_ptr<Value>&){
+ ping(pong_node, locs[i].second);
+ return true;
+ });
+ pong_node.listen(locs[i].second, [&,i](const std::shared_ptr<Value>&){
+ ping(ping_node, locs[i].first);
+ return true;
+ });
+ }
+
+ auto start = clock::now();
+
+ for (unsigned i=0; i<n_parallel; i++)
+ ping(pong_node, locs[i].first);
+
+ {
+ std::unique_lock<std::mutex> lk(m);
+ if (not cv.wait_for(lk, std::chrono::minutes(1), [&](){ return i == max; })) {
+ throw std::runtime_error(std::string("Timeout: ") + std::to_string(i));
+ }
+ }
+
+ auto end = clock::now();
+
+ for (auto& node : nodes)
+ node->shutdown();
+ ping_node.shutdown();
+ pong_node.shutdown();
+
+ for (auto& node : nodes)
+ node->join();
+ ping_node.join();
+ pong_node.join();
+
+ return end-start;
+}
+
+}
+
+int
+main(int argc, char **argv)
+{
+#ifdef _MSC_VER
+ gnutls_global_init();
+#endif
+ auto params = parseArgs(argc, argv);
+ if (params.help) {
+ print_usage();
+ return 0;
+ }
+
+ duration totalTime {0};
+ unsigned totalOps {0};
+
+ for (unsigned nparallel = 1; nparallel <= 32; nparallel *= 2) {
+ unsigned max = PINGPONG_MAX * nparallel;
+ std::vector<duration> results {};
+ results.reserve(8);
+ duration total {0};
+ for (unsigned i=2; i<32; i *= 2) {
+ auto dt = tests::benchPingPong(i - 2, nparallel);
+ std::cout << "Network size: " << i << std::endl;
+ std::cout << max << " ping-pong done, took " << print_duration(dt) << std::endl;
+ std::cout << print_duration(dt/max) << " per rt, "
+ << max/std::chrono::duration<double>(dt).count() << " ping per s" << std::endl << std::endl;
+ total += dt;
+ totalOps += max;
+ results.emplace_back(dt);
+ }
+
+ totalTime += total;
+
+ std::cout << "Total for " << nparallel << std::endl;
+ auto totNum = max*results.size();
+ std::cout << totNum << " ping-pong done, took " << print_duration(total) << std::endl;
+ std::cout << print_duration(total/totNum) << " per rt, "
+ << totNum/std::chrono::duration<double>(total).count() << " ping per s" << std::endl << std::endl;
+ }
+
+ std::cout << std::endl << "Grand total: " << print_duration(totalTime) << " for " << totalOps << std::endl;
+ std::cout << print_duration(totalTime/totalOps) << " per rt, "
+ << totalOps/std::chrono::duration<double>(totalTime).count() << " ping per s" << std::endl << std::endl;
+
+#ifdef _MSC_VER
+ gnutls_global_deinit();
+#endif
+ return 0;
+}
--- /dev/null
+# Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+# Author: Vsevolod Ivanov <vsevolod.ivanov@savoirfairelinux.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+#
+# Manually run with Web UI:
+# locust -f tester.py --host http://127.0.0.1:8080
+#
+# Run in Terminal:
+# locust -f tester.py --host http://127.0.0.1:8080 \
+# --clients 100 --hatch-rate 1 --run-time 10s --no-web --only-summary
+
+from locust import HttpLocust, TaskSet
+from random import randint
+import urllib.request
+import base64
+import json
+
+words_url = "http://svnweb.freebsd.org/csrg/share/dict/words?view=co&content-type=text/plain"
+words_resp = urllib.request.urlopen(words_url)
+words = words_resp.read().decode().splitlines()
+
+headers = {'content-type': 'application/json'}
+
+def rand_list_value(mylist):
+ return mylist[randint(0, len(mylist) - 1)]
+
+def put_key(l):
+ key = rand_list_value(words)
+ val = rand_list_value(words)
+ print("Put/get: key={} value={}".format(key, val))
+ data = base64.b64encode(val.encode()).decode()
+ print("Base64 encoding: value={} encoded={}".format(val, data))
+ l.client.post("/" + key, data=json.dumps({"data": data}),
+ headers=headers, catch_response=True)
+
+def get_key(l):
+ key = rand_list_value(words)
+ print("Get: key={}".format(key))
+ l.client.get("/" + key)
+
+def get_stats(l):
+ l.client.get("/stats")
+
+def subscribe(l):
+ key = rand_list_value(words)
+ print("Subscribe: key={}".format(key))
+ l.client.get("/" + key + "/subscribe")
+
+def listen(l):
+ key = rand_list_value(words)
+ print("Listen: key={}".format(key))
+ l.client.get("/" + key + "/listen")
+
+class UserBehavior(TaskSet):
+ tasks = {get_key: 5, put_key: 5, get_stats: 1, subscribe: 1, listen: 1}
+
+ def on_start(self):
+ put_key(self)
+ get_key(self)
+ subscribe(self)
+ listen(self)
+
+ def on_stop(self):
+ get_stats(self)
+
+class WebsiteUser(HttpLocust):
+ task_set = UserBehavior
+ min_wait = 5000
+ max_wait = 9000
+ print("Initiate the benchmark at http://127.0.0.1:8089/")
--- /dev/null
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
+ <title>OpenDHT tester</title>
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css" />
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.css" />
+ <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
+ <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script>
+ <script type="text/javascript">"use strict";
+let onGet;
+let onPut;
+let setServer;
+const valueGetElement = function(o) {
+ const d = window.atob(o.data);
+ return d;
+};
+$(function() {
+ let request = undefined;
+ let server;
+ const getTools = $("#getTools");
+ const getBtn = $("#getBtn");
+ const getDropdown = $("#getDropdown");
+ const listenBtn = $("#listenBtn").click(function(){onGet('LISTEN');});
+ const setGetRequest = function() {
+ getBtn.button('loading');
+ getStopBtn.appendTo(getTools);
+ getDropdown.hide();
+ }
+ const clearGetRequest = function() {
+ if (request === undefined)
+ return;
+ request.abort();
+ request = undefined;
+ getStopBtn.detach();
+ getDropdown.show();
+ getBtn.button('reset');
+ }
+ const getStopBtn = $("#getStopBtn").detach().click(clearGetRequest);
+ const putBtn = $("#putBtn");
+ const result = $("#dhtResult");
+ const group = $('<ul class="list-group"/>').appendTo(result);
+ onGet = function (method) {
+ if (request !== undefined)
+ return false;
+ const input = $("#getKey").val();
+ group.empty();
+ let lastAppended = 0;
+ let start = new Date().getTime();
+ request = new XMLHttpRequest();
+ request.onreadystatechange = function(event) {
+ if (this.readyState >= XMLHttpRequest.LOADING) {
+ if (this.readyState == XMLHttpRequest.DONE) {
+ clearGetRequest();
+ }
+ if (this.status === 200) {
+ const elements = this.responseText.split("\n");
+ const elementsLength = elements.length;
+ const now = new Date().getTime();
+ for (let i = lastAppended; i < elementsLength; i++) {
+ const element = elements[i];
+ if (!element || element.length == 0)
+ return;
+ const o = JSON.parse(element);
+ if (o.expired) {
+ $('#value'+o.id).slideUp(100, (e) => $(e.target).remove());
+ } else {
+ const d = window.atob(o.data);
+ const delay = Math.max(0, start-now);
+ $('<li class="list-group-item" id="value'+o.id+'"/>').append(valueGetElement(o)).appendTo(group).hide().delay(delay).slideDown(100);
+ lastAppended = i+1;
+ start = Math.max(start, now)+25;
+ }
+ }
+ } else if (this.status !== 0) {
+ group.empty().append($('<li class="list-group-item list-group-item-danger"/>').text("Error loading content: " + this.statusText));
+ }
+ }
+ };
+ request.onerror = function(event) {
+ clearGetRequest();
+ group.empty().append($('<li class="list-group-item list-group-item-danger"/>').text("Error loading content."));
+ };
+ request.open(method, server + input, true);
+ request.send(null);
+ setGetRequest();
+ return false;
+ };
+
+ onPut = function( ) {
+ const key = $("#getKey").val();
+ const value = $("#putValue").val();
+ $.ajax({
+ url: server + key,
+ type: 'POST',
+ data: JSON.stringify({
+ data:window.btoa(value)
+ }),
+ contentType: 'application/json; charset=utf-8',
+ dataType: 'json',
+ success: function( result ) {
+ putBtn.button('reset');
+ //$('<li class="list-group-item list-group-item-success"/>').append(valueGetElement(result)).appendTo(group.empty());
+ },
+ error: function(result) {
+ putBtn.button('reset');
+ group.empty().append($('<li class="list-group-item list-group-item-danger"/>').text(result.statusText));
+ }
+ });
+ putBtn.button('loading');
+ return false;
+ };
+
+ const serverValue = $("#serverValue");
+ const serverStatus = $("#serverStatus");
+ const serverBtn = $("#serverBtn");
+ setServer = function(event) {
+ server = serverValue.val() + '/';
+ serverStatus.empty();
+ serverBtn.button('loading');
+ $.getJSON(server, function(data){
+ $('<span><b>Node</b> '+data.node_id+'</span>').appendTo(serverStatus).hide().fadeIn();
+ }).fail(function(error) {
+ var message = " Cant' access node."
+ if (serverValue.val().indexOf("https") != -1){
+ message += "</br></br>Self-signed certificate must be allowed in browser."
+ }
+ serverStatus.html("<div class='alert alert-danger' style='margin-bottom: 0px;'>" +
+ "<span class='glyphicon glyphicon-remove' aria-hidden='true'></span>" +
+ message + "</div>");
+ }).always(function(error) {
+ serverBtn.button('reset');
+ });
+ return false;
+ };
+ setServer();
+});
+ </script>
+</head>
+<body>
+ <div class="container" style="max-width: 730px;">
+ <header class="page-header">
+ <div class="row">
+ <div class="col-sm-5">
+ <h1>OpenDHT tester</h1>
+ </div>
+ <div class="col-sm-7">
+ <div class="well well-sm" style="margin-top:10px; margin-bottom:0px;">
+ <form id="serverForm" class="form-inline" onsubmit="return setServer();" style="margin-bottom:4px;">
+ <div class="input-group">
+ <input type="text" class="form-control" id="serverValue" placeholder="Proxy server" value="http://127.0.0.1:8080"/>
+ <span class="input-group-btn">
+ <button id="serverBtn" type="submit" class="btn btn-default" data-loading-text="<i class='fa fa-circle-o-notch fa-spin'></i>"><span class="glyphicon glyphicon-refresh" aria-hidden="true"></span></button>
+ </span>
+ </div>
+ </form>
+ <div id="serverStatus"><i class='fa fa-circle-o-notch fa-spin'></i></div>
+ </div>
+ </div>
+ </div>
+ </header>
+ <div class="panel panel-default" id="dhtResult">
+ <div class="panel-heading">
+ <div class="row">
+ <div class="col-xs-6">
+ <form class="form-inline" onsubmit="return onGet('GET');">
+ <div class="input-group">
+ <input type="text" class="form-control" id="getKey" placeholder="Key" aria-label="Key" />
+ <span class="input-group-btn" id="getTools">
+ <button id="getBtn" class="btn btn-default" data-loading-text="<i class='fa fa-circle-o-notch fa-spin'></i>" type="submit">Get</button>
+ <button id="getDropdown"type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <span class="caret"></span> <span class="sr-only">Toggle Dropdown</span> </button>
+ <ul class="dropdown-menu">
+ <li><a id="listenBtn" href="#">Listen</a></li>
+ </ul>
+ <button id="getStopBtn" class="btn btn-default" type="submit"><span class="glyphicon glyphicon-remove" aria-hidden="true"></span></button>
+ </span>
+ </div>
+ </form>
+ </div>
+ <div class="col-xs-6">
+ <form class="form-inline" onsubmit="return onPut();">
+ <div class="input-group">
+ <input type="text" class="form-control input-group-input" id="putValue" placeholder="Value" />
+ <span class="input-group-btn">
+ <button id="putBtn" type="submit" class="btn btn-default" data-loading-text="<i class='fa fa-circle-o-notch fa-spin'></i> Loading">Put</button>
+ </span>
+ </div>
+ </form>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+</body>
+</html>
--- /dev/null
+DHT_ARGS=-b bootstrap.jami.net -p 4222 -n 16
\ No newline at end of file
--- /dev/null
+[Unit]
+Description=OpenDHT node cluster
+After=network.target
+
+[Service]
+EnvironmentFile=@sysconfdir@/dhtcluster.conf
+ExecStart=@bindir@/dhtcluster -s $DHT_ARGS
+KillMode=process
+Restart=on-failure
+Type=simple
+ProtectSystem=strict
+ProtectHome=yes
+ProtectKernelTunables=yes
+ProtectKernelModules=yes
+ProtectControlGroups=yes
+PrivateDevices=yes
+PrivateUsers=yes
+
+[Install]
+WantedBy=multi-user.target
--- /dev/null
+DHT_ARGS=-b bootstrap.jami.net -p 4222 -v
\ No newline at end of file
--- /dev/null
+[Unit]
+Description=OpenDHT standalone node
+Documentation=man:dhtnode(1)
+After=network.target
+
+[Service]
+Type=simple
+User=opendht
+Group=opendht
+ExecStart=@bindir@/dhtnode -s $DHT_ARGS
+Restart=on-failure
+RestartSec=2s
+LimitNOFILE=65536
+DynamicUser=yes
+EnvironmentFile=@sysconfdir@/dhtnode.conf
+KillMode=process
+WorkingDirectory=/tmp
+
+# Hardening
+CapabilityBoundingSet=CAP_NET_BIND_SERVICE
+LockPersonality=yes
+NoNewPrivileges=yes
+PrivateDevices=yes
+PrivateTmp=yes
+PrivateUsers=yes
+ProtectClock=yes
+ProtectControlGroups=yes
+ProtectHome=yes
+ProtectHostname=yes
+ProtectKernelLogs=yes
+ProtectKernelModules=yes
+ProtectKernelTunables=yes
+ProtectSystem=strict
+ReadOnlyDirectories=/
+ReadWriteDirectories=-/proc/self
+ReadWriteDirectories=-/var/run
+RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
+RestrictNamespaces=yes
+RestrictRealtime=yes
+SystemCallArchitectures=native
+SystemCallFilter=@system-service
+
+[Install]
+WantedBy=multi-user.target
--- /dev/null
+/*
+ * Copyright (C) 2014-2023 Savoir-faire Linux Inc.
+ *
+ * Author: Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ * Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+// Common utility methods used by C++ OpenDHT tools.
+#pragma once
+
+#include <opendht.h>
+#include <opendht/log.h>
+#include <opendht/crypto.h>
+#include <opendht/network_utils.h>
+#ifdef OPENDHT_INDEXATION
+#include <opendht/indexation/pht.h>
+#endif
+#ifdef OPENDHT_PROXY_SERVER
+#include <opendht/dht_proxy_server.h>
+#endif
+
+#include <fmt/ranges.h>
+
+#ifndef _MSC_VER
+#include <getopt.h>
+#include <readline/readline.h>
+#include <readline/history.h>
+#else
+#define SIGHUP 0
+#include "wingetopt.h"
+#include <io.h>
+#endif
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <signal.h>
+#include <unistd.h>
+
+#include <string>
+#include <vector>
+#include <chrono>
+#include <mutex>
+#include <condition_variable>
+#include <iostream>
+#include <sstream>
+#include <fstream>
+
+/*
+ * The mapString shall have the following format:
+ *
+ * k1:v1[,k2:v2[,...]]
+ */
+std::map<std::string, std::string> parseStringMap(std::string mapString) {
+ std::istringstream keySs(mapString);
+ std::string mapStr;
+ std::map<std::string, std::string> map;
+
+ while (std::getline(keySs, mapStr, ',')) {
+ std::istringstream mapSs(mapStr);
+ std::string key, value;
+
+ while (std::getline(mapSs, key, ':')) {
+ std::getline(mapSs, value, ':');
+ map[key] = { value };
+ }
+ }
+ return map;
+}
+
+#ifdef OPENDHT_INDEXATION
+dht::indexation::Pht::Key createPhtKey(std::map<std::string, std::string> pht_key_str_map) {
+ dht::indexation::Pht::Key pht_key;
+ for (auto f : pht_key_str_map) {
+ dht::Blob prefix {f.second.begin(), f.second.end()};
+ pht_key.emplace(f.first, std::move(prefix));
+ }
+ return pht_key;
+}
+#endif
+
+bool isInfoHash(const dht::InfoHash& h) {
+ if (not h) {
+ std::cout << "Syntax error: invalid InfoHash." << std::endl;
+ return false;
+ }
+ return true;
+}
+
+std::vector<uint8_t>
+loadFile(const std::string& path)
+{
+ std::vector<uint8_t> buffer;
+ std::ifstream file(path, std::ios::binary);
+ if (!file)
+ throw std::runtime_error("Can't read file: "+path);
+ file.seekg(0, std::ios::end);
+ auto size = file.tellg();
+ if (size > std::numeric_limits<unsigned>::max())
+ throw std::runtime_error("File is too big: "+path);
+ buffer.resize(size);
+ file.seekg(0, std::ios::beg);
+ if (!file.read((char*)buffer.data(), size))
+ throw std::runtime_error("Can't load file: "+path);
+ return buffer;
+}
+
+struct dht_params {
+ bool help {false}; // print help and exit
+ bool version {false};
+ bool generate_identity {false};
+ bool daemonize {false};
+ bool service {false};
+ bool peer_discovery {false};
+ bool log {false};
+ bool syslog {false};
+ std::string logfile {};
+ std::string bootstrap {};
+ dht::NetId network {0};
+ in_port_t port {0};
+ in_port_t proxyserver {0};
+ in_port_t proxyserverssl {0};
+ std::string proxyclient {};
+ std::string proxy_address {};
+ std::string pushserver {};
+ std::string devicekey {};
+ std::string bundle_id {};
+ std::string persist_path {};
+ dht::crypto::Identity id {};
+ dht::crypto::Identity proxy_id {};
+ std::string privkey_pwd {};
+ std::string proxy_privkey_pwd {};
+ std::string save_identity {};
+ bool no_rate_limit {false};
+ bool public_stable {false};
+};
+
+std::pair<dht::DhtRunner::Config, dht::DhtRunner::Context>
+getDhtConfig(dht_params& params)
+{
+ if (not params.id.first and params.generate_identity) {
+ auto node_ca = std::make_unique<dht::crypto::Identity>(dht::crypto::generateEcIdentity("DHT Node CA"));
+ params.id = dht::crypto::generateIdentity("DHT Node", *node_ca);
+ if (not params.save_identity.empty()) {
+ dht::crypto::saveIdentity(*node_ca, params.save_identity + "_ca", params.privkey_pwd);
+ dht::crypto::saveIdentity(params.id, params.save_identity, params.privkey_pwd);
+ }
+ }
+
+ dht::DhtRunner::Config config {};
+ config.dht_config.node_config.network = params.network;
+ config.dht_config.node_config.maintain_storage = false;
+ config.dht_config.node_config.persist_path = params.persist_path;
+ config.dht_config.node_config.public_stable = params.public_stable;
+ config.dht_config.id = params.id;
+ config.dht_config.cert_cache_all = static_cast<bool>(params.id.first);
+ config.threaded = true;
+ config.proxy_server = params.proxyclient;
+ config.push_node_id = "dhtnode";
+ config.push_token = params.devicekey;
+ config.peer_discovery = params.peer_discovery;
+ config.peer_publish = params.peer_discovery;
+ if (params.no_rate_limit) {
+ config.dht_config.node_config.max_req_per_sec = -1;
+ config.dht_config.node_config.max_peer_req_per_sec = -1;
+ config.dht_config.node_config.max_searches = -1;
+ config.dht_config.node_config.max_store_size = -1;
+ }
+
+ dht::DhtRunner::Context context {};
+ if (params.log) {
+ if (params.syslog or (params.daemonize and params.logfile.empty()))
+ context.logger = dht::log::getSyslogLogger("dhtnode");
+ else if (not params.logfile.empty())
+ context.logger = dht::log::getFileLogger(params.logfile);
+ else
+ context.logger = dht::log::getStdLogger();
+ }
+ if (context.logger) {
+ context.statusChangedCallback = [logger = context.logger](dht::NodeStatus status4, dht::NodeStatus status6) {
+ logger->w("Connectivity changed: IPv4: %s, IPv6: %s", dht::statusToStr(status4), dht::statusToStr(status6));
+ };
+ context.publicAddressChangedCb = [logger = context.logger](std::vector<dht::SockAddr> addrs) {
+ logger->warn("Public address changed: {}", addrs);
+ };
+ }
+ return {std::move(config), std::move(context)};
+}
+
+void print_node_info(const dht::NodeInfo& info) {
+ std::cout << "OpenDHT node " << info.node_id << " running on ";
+ if (info.bound4 == info.bound6)
+ std::cout << "port " << info.bound4 << std::endl;
+ else
+ std::cout << "IPv4 port " << info.bound4 << ", IPv6 port " << info.bound6 << std::endl;
+ if (info.id)
+ std::cout << "Public key ID " << info.id << std::endl;
+}
+
+static const constexpr struct option long_options[] = {
+ {"help", no_argument , nullptr, 'h'},
+ {"port", required_argument, nullptr, 'p'},
+ {"net", required_argument, nullptr, 'n'},
+ {"bootstrap", required_argument, nullptr, 'b'},
+ {"identity", no_argument , nullptr, 'i'},
+ {"save-identity", required_argument, nullptr, 'I'},
+ {"certificate", required_argument, nullptr, 'c'},
+ {"privkey", required_argument, nullptr, 'k'},
+ {"privkey-password", required_argument, nullptr, 'm'},
+ {"verbose", no_argument , nullptr, 'v'},
+ {"daemonize", no_argument , nullptr, 'd'},
+ {"service", no_argument , nullptr, 's'},
+ {"peer-discovery", no_argument , nullptr, 'D'},
+ {"no-rate-limit", no_argument , nullptr, 'U'},
+ {"public-stable", no_argument , nullptr, 'P'},
+ {"persist", required_argument, nullptr, 'f'},
+ {"logfile", required_argument, nullptr, 'l'},
+ {"syslog", no_argument , nullptr, 'L'},
+ {"proxyserver", required_argument, nullptr, 'S'},
+ {"proxyserverssl", required_argument, nullptr, 'e'},
+ {"proxy-addr", required_argument, nullptr, 'a'},
+ {"proxy-certificate", required_argument, nullptr, 'w'},
+ {"proxy-privkey", required_argument, nullptr, 'K'},
+ {"proxy-privkey-password", required_argument, nullptr, 'M'},
+ {"proxyclient", required_argument, nullptr, 'C'},
+ {"pushserver", required_argument, nullptr, 'y'},
+ {"devicekey", required_argument, nullptr, 'z'},
+ {"bundleid", required_argument, nullptr, 'u'},
+ {"version", no_argument , nullptr, 'V'},
+ {nullptr, 0 , nullptr, 0}
+};
+
+dht_params
+parseArgs(int argc, char **argv) {
+ dht_params params;
+ int opt;
+ std::string privkey;
+ std::string proxy_privkey;
+ while ((opt = getopt_long(argc, argv, "hidsvODUPp:n:b:f:l:", long_options, nullptr)) != -1) {
+ switch (opt) {
+ case 'p': {
+ int port_arg = atoi(optarg);
+ if (port_arg >= 0 && port_arg < 0x10000)
+ params.port = port_arg;
+ else
+ std::cout << "Invalid port: " << port_arg << std::endl;
+ }
+ break;
+ case 'S': {
+ int port_arg = atoi(optarg);
+ if (port_arg >= 0 && port_arg < 0x10000)
+ params.proxyserver = port_arg;
+ else
+ std::cout << "Invalid port: " << port_arg << std::endl;
+ }
+ break;
+ case 'e': {
+ int port_arg = atoi(optarg);
+ if (port_arg >= 0 && port_arg < 0x10000)
+ params.proxyserverssl = port_arg;
+ else
+ std::cout << "Invalid port: " << port_arg << std::endl;
+ }
+ break;
+ case 'D':
+ params.peer_discovery = true;
+ break;
+ case 'a':
+ params.proxy_address = optarg;
+ break;
+ case 'y':
+ params.pushserver = optarg;
+ break;
+ case 'C':
+ params.proxyclient = optarg;
+ break;
+ case 'z':
+ params.devicekey = optarg;
+ break;
+ case 'f':
+ params.persist_path = optarg;
+ break;
+ case 'n':
+ params.network = strtoul(optarg, nullptr, 0);
+ break;
+ case 'U':
+ params.no_rate_limit = true;
+ break;
+ case 'P':
+ params.public_stable = true;
+ break;
+ case 'b':
+ params.bootstrap = (optarg[0] == '=') ? optarg+1 : optarg;
+ break;
+ case 'V':
+ params.version = true;
+ break;
+ case 'h':
+ params.help = true;
+ break;
+ case 'l':
+ params.logfile = optarg;
+ break;
+ case 'L':
+ params.log = true;
+ params.syslog = true;
+ break;
+ case 'v':
+ params.log = true;
+ break;
+ case 'i':
+ params.generate_identity = true;
+ break;
+ case 'd':
+ params.daemonize = true;
+ break;
+ case 's':
+ params.service = true;
+ break;
+ case 'c': {
+ try {
+ params.id.second = std::make_shared<dht::crypto::Certificate>(loadFile(optarg));
+ } catch (const std::exception& e) {
+ throw std::runtime_error(std::string("Error loading certificate: ") + e.what());
+ }
+ break;
+ }
+ case 'w': {
+ try {
+ params.proxy_id.second = std::make_shared<dht::crypto::Certificate>(loadFile(optarg));
+ } catch (const std::exception& e) {
+ throw std::runtime_error(std::string("Error loading proxy certificate: ") + e.what());
+ }
+ break;
+ }
+ case 'k':
+ privkey = optarg;
+ break;
+ case 'u':
+ params.bundle_id = optarg;
+ break;
+ case 'K':
+ proxy_privkey = optarg;
+ break;
+ case 'm':
+ params.privkey_pwd = optarg;
+ break;
+ case 'M':
+ params.proxy_privkey_pwd = optarg;
+ break;
+ case 'I':
+ params.save_identity = optarg;
+ break;
+ default:
+ break;
+ }
+ }
+ if (not privkey.empty()) {
+ try {
+ params.id.first = std::make_shared<dht::crypto::PrivateKey>(loadFile(privkey),
+ params.privkey_pwd);
+ } catch (const std::exception& e) {
+ throw std::runtime_error(std::string("Error loading private key: ") + e.what());
+ }
+ }
+ if (not proxy_privkey.empty()) {
+ try {
+ params.proxy_id.first = std::make_shared<dht::crypto::PrivateKey>(loadFile(proxy_privkey),
+ params.proxy_privkey_pwd);
+ } catch (const std::exception& e) {
+ throw std::runtime_error(std::string("Error loading proxy private key: ") + e.what());
+ }
+ }
+ if (params.save_identity.empty())
+ params.privkey_pwd.clear();
+ return params;
+}
+
+static const constexpr char* PROMPT = ">> ";
+
+std::string
+readLine(const char* prefix = PROMPT)
+{
+#ifndef _MSC_VER
+ const char* line_read = readline(prefix);
+ if (line_read && *line_read)
+ add_history(line_read);
+
+#else
+ char line_read[512];
+ std::cout << PROMPT;
+ fgets(line_read, 512 , stdin);
+#endif
+ return line_read ? std::string(line_read) : std::string("\0", 1);
+}
+
+struct ServiceRunner {
+ bool wait() {
+ std::unique_lock<std::mutex> lock(m);
+ cv.wait(lock, [&]{return terminate.load();});
+ return !terminate;
+ }
+ void kill() {
+ terminate = true;
+ cv.notify_all();
+ }
+private:
+ std::condition_variable cv;
+ std::mutex m;
+ std::atomic_bool terminate {false};
+};
+
+ServiceRunner runner;
+
+void signal_handler(int sig)
+{
+ switch(sig) {
+ case SIGHUP:
+ break;
+ case SIGINT:
+ close(STDIN_FILENO);
+ // fall through
+ case SIGTERM:
+ runner.kill();
+ break;
+ }
+}
+
+void setupSignals()
+{
+#ifndef _MSC_VER
+ signal(SIGCHLD,SIG_IGN); /* ignore child */
+ signal(SIGTSTP,SIG_IGN); /* ignore tty signals */
+ signal(SIGTTOU,SIG_IGN);
+ signal(SIGTTIN,SIG_IGN);
+ signal(SIGHUP,signal_handler); /* catch hangup signal */
+ signal(SIGINT,signal_handler); /* catch interrupt signal */
+ signal(SIGTERM,signal_handler); /* catch kill signal */
+#endif
+}
+
+void daemonize()
+{
+#ifndef _MSC_VER
+ pid_t pid = fork();
+ if (pid < 0) exit(EXIT_FAILURE);
+ if (pid > 0) exit(EXIT_SUCCESS);
+
+ umask(0);
+
+ pid_t sid = setsid();
+ if (sid < 0) {
+ exit(EXIT_FAILURE);
+ }
+
+ close(STDIN_FILENO);
+ close(STDOUT_FILENO);
+ close(STDERR_FILENO);
+#endif
+}