Import opendht_2.1.10.orig.tar.gz
authorAlexandre Viau <aviau@debian.org>
Fri, 1 Jan 2021 19:02:19 +0000 (14:02 -0500)
committerAlexandre Viau <aviau@debian.org>
Fri, 1 Jan 2021 19:02:19 +0000 (14:02 -0500)
[dgit import orig opendht_2.1.10.orig.tar.gz]

169 files changed:
.clang-tidy [new file with mode: 0644]
.github/workflows/ccpp.yml [new file with mode: 0644]
.gitignore [new file with mode: 0644]
.gitmodules [new file with mode: 0644]
.travis.yml [new file with mode: 0644]
CMakeLists.txt [new file with mode: 0644]
COPYING [new file with mode: 0644]
Makefile.am [new file with mode: 0644]
README.md [new file with mode: 0644]
autogen.sh [new file with mode: 0755]
c/opendht.cpp [new file with mode: 0644]
c/opendht_c.h [new file with mode: 0644]
cmake/CheckAtomic.cmake [new file with mode: 0644]
cmake/FindMsgpack.cmake [new file with mode: 0644]
cmake/FindReadline.cmake [new file with mode: 0644]
cmake/FindRestinio.cmake [new file with mode: 0644]
configure.ac [new file with mode: 0644]
doc/CMakeLists.txt [new file with mode: 0644]
doc/Doxyfile.in [new file with mode: 0644]
doc/Makefile.am [new file with mode: 0644]
doc/connectivity-loss-by-search-criteria.pdf [new file with mode: 0644]
doc/connectivity-loss-by-search-criteria.tex [new file with mode: 0644]
doc/dhtnode.1 [new file with mode: 0644]
docker/Dockerfile [new file with mode: 0644]
docker/DockerfileBionic [new file with mode: 0644]
docker/DockerfileDeps [new file with mode: 0644]
docker/DockerfileDepsBionic [new file with mode: 0644]
docker/DockerfileDepsLlvm [new file with mode: 0644]
docker/DockerfileLlvm [new file with mode: 0644]
docker/DockerfileTravis [new file with mode: 0644]
docker/DockerfileTravisLlvm [new file with mode: 0644]
docker/DockerfileTravisProxy [new file with mode: 0644]
include/opendht.h [new file with mode: 0644]
include/opendht/callbacks.h [new file with mode: 0644]
include/opendht/crypto.h [new file with mode: 0644]
include/opendht/def.h [new file with mode: 0644]
include/opendht/default_types.h [new file with mode: 0644]
include/opendht/dht.h [new file with mode: 0644]
include/opendht/dht_interface.h [new file with mode: 0644]
include/opendht/dht_proxy_client.h [new file with mode: 0644]
include/opendht/dht_proxy_server.h [new file with mode: 0644]
include/opendht/dhtrunner.h [new file with mode: 0644]
include/opendht/http.h [new file with mode: 0644]
include/opendht/indexation/pht.h [new file with mode: 0644]
include/opendht/infohash.h [new file with mode: 0644]
include/opendht/log.h [new file with mode: 0644]
include/opendht/log_enable.h [new file with mode: 0644]
include/opendht/network_engine.h [new file with mode: 0644]
include/opendht/network_utils.h [new file with mode: 0644]
include/opendht/node.h [new file with mode: 0644]
include/opendht/node_cache.h [new file with mode: 0644]
include/opendht/peer_discovery.h [new file with mode: 0644]
include/opendht/proxy.h [new file with mode: 0644]
include/opendht/rate_limiter.h [new file with mode: 0644]
include/opendht/rng.h [new file with mode: 0644]
include/opendht/routing_table.h [new file with mode: 0644]
include/opendht/scheduler.h [new file with mode: 0644]
include/opendht/securedht.h [new file with mode: 0644]
include/opendht/sockaddr.h [new file with mode: 0644]
include/opendht/thread_pool.h [new file with mode: 0644]
include/opendht/utils.h [new file with mode: 0644]
include/opendht/value.h [new file with mode: 0644]
m4/ax_cxx_compile_stdcxx.m4 [new file with mode: 0644]
opendht-c.pc.in [new file with mode: 0644]
opendht.pc.in [new file with mode: 0644]
python/CMakeLists.txt [new file with mode: 0644]
python/Makefile.am [new file with mode: 0644]
python/opendht.pyx [new file with mode: 0644]
python/opendht_cpp.pxd [new file with mode: 0644]
python/setup.py.in [new file with mode: 0644]
python/tests/opendht_tests.py [new file with mode: 0644]
python/tools/README.md [new file with mode: 0644]
python/tools/benchmark.py [new file with mode: 0755]
python/tools/dht/__init__.py [new file with mode: 0644]
python/tools/dht/network.py [new file with mode: 0755]
python/tools/dht/tests.py [new file with mode: 0644]
python/tools/dht/virtual_network_builder.py [new file with mode: 0755]
python/tools/dhtcluster.py [new file with mode: 0755]
python/tools/http_server.py [new file with mode: 0755]
python/tools/network_monitor.py [new file with mode: 0755]
python/tools/pingpong.py [new file with mode: 0755]
python/tools/scanner.py [new file with mode: 0755]
resources/opendht_logo.svg [new file with mode: 0644]
resources/opendht_logo_512.png [new file with mode: 0644]
rust/.gitignore [new file with mode: 0644]
rust/Cargo.toml [new file with mode: 0644]
rust/README.md [new file with mode: 0644]
rust/examples/dhtnode.rs [new file with mode: 0644]
rust/src/blob.rs [new file with mode: 0644]
rust/src/crypto.rs [new file with mode: 0644]
rust/src/dhtrunner.rs [new file with mode: 0644]
rust/src/ffi.rs [new file with mode: 0644]
rust/src/infohash.rs [new file with mode: 0644]
rust/src/lib.rs [new file with mode: 0644]
rust/src/pkid.rs [new file with mode: 0644]
rust/src/value.rs [new file with mode: 0644]
src/Makefile.am [new file with mode: 0644]
src/base64.cpp [new file with mode: 0644]
src/base64.h [new file with mode: 0644]
src/callbacks.cpp [new file with mode: 0644]
src/compat/msvc/unistd.h [new file with mode: 0644]
src/compat/msvc/wingetopt.c [new file with mode: 0644]
src/compat/msvc/wingetopt.h [new file with mode: 0644]
src/compat/os_cert.cpp [new file with mode: 0644]
src/compat/os_cert.h [new file with mode: 0644]
src/crypto.cpp [new file with mode: 0644]
src/default_types.cpp [new file with mode: 0644]
src/dht.cpp [new file with mode: 0644]
src/dht_proxy_client.cpp [new file with mode: 0644]
src/dht_proxy_server.cpp [new file with mode: 0644]
src/dhtrunner.cpp [new file with mode: 0644]
src/http.cpp [new file with mode: 0644]
src/indexation/pht.cpp [new file with mode: 0644]
src/infohash.cpp [new file with mode: 0644]
src/listener.h [new file with mode: 0644]
src/log.cpp [new file with mode: 0644]
src/net.h [new file with mode: 0644]
src/network_engine.cpp [new file with mode: 0644]
src/network_utils.cpp [new file with mode: 0644]
src/node.cpp [new file with mode: 0644]
src/node_cache.cpp [new file with mode: 0644]
src/op_cache.cpp [new file with mode: 0644]
src/op_cache.h [new file with mode: 0644]
src/parsed_message.h [new file with mode: 0644]
src/peer_discovery.cpp [new file with mode: 0644]
src/request.h [new file with mode: 0644]
src/rng.cpp [new file with mode: 0644]
src/routing_table.cpp [new file with mode: 0644]
src/search.h [new file with mode: 0644]
src/securedht.cpp [new file with mode: 0644]
src/storage.h [new file with mode: 0644]
src/thread_pool.cpp [new file with mode: 0644]
src/utils.cpp [new file with mode: 0644]
src/value.cpp [new file with mode: 0644]
src/value_cache.h [new file with mode: 0644]
tests/Makefile.am [new file with mode: 0644]
tests/cryptotester.cpp [new file with mode: 0644]
tests/cryptotester.h [new file with mode: 0644]
tests/dhtproxytester.cpp [new file with mode: 0644]
tests/dhtproxytester.h [new file with mode: 0644]
tests/dhtrunnertester.cpp [new file with mode: 0644]
tests/dhtrunnertester.h [new file with mode: 0644]
tests/httptester.cpp [new file with mode: 0644]
tests/httptester.h [new file with mode: 0644]
tests/infohashtester.cpp [new file with mode: 0644]
tests/infohashtester.h [new file with mode: 0644]
tests/peerdiscoverytester.cpp [new file with mode: 0644]
tests/peerdiscoverytester.h [new file with mode: 0644]
tests/tests_runner.cpp [new file with mode: 0644]
tests/threadpooltester.cpp [new file with mode: 0644]
tests/threadpooltester.h [new file with mode: 0644]
tests/valuetester.cpp [new file with mode: 0644]
tests/valuetester.h [new file with mode: 0644]
tools/CMakeLists.txt [new file with mode: 0644]
tools/Makefile.am [new file with mode: 0644]
tools/dhtchat.cpp [new file with mode: 0644]
tools/dhtcnode.c [new file with mode: 0644]
tools/dhtnode.cpp [new file with mode: 0644]
tools/dhtproxy_stats.py [new file with mode: 0644]
tools/dhtscanner.cpp [new file with mode: 0644]
tools/durl.cpp [new file with mode: 0644]
tools/perftest.cpp [new file with mode: 0644]
tools/proxy_loadtester.py [new file with mode: 0644]
tools/proxy_node.html [new file with mode: 0644]
tools/systemd/dhtcluster.conf [new file with mode: 0644]
tools/systemd/dhtcluster.service.in [new file with mode: 0644]
tools/systemd/dhtnode.conf [new file with mode: 0644]
tools/systemd/dhtnode.service.in [new file with mode: 0644]
tools/tools_common.h [new file with mode: 0644]

diff --git a/.clang-tidy b/.clang-tidy
new file mode 100644 (file)
index 0000000..26374af
--- /dev/null
@@ -0,0 +1 @@
+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'
diff --git a/.github/workflows/ccpp.yml b/.github/workflows/ccpp.yml
new file mode 100644 (file)
index 0000000..8e0321e
--- /dev/null
@@ -0,0 +1,34 @@
+name: C/C++ CI
+
+on: [push]
+
+jobs:
+  build:
+
+    runs-on: ubuntu-latest
+    
+    steps:
+    - uses: actions/checkout@v1
+    - 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
+    - name: autogen
+      run: ./autogen.sh
+    - name: argon2
+      run: |
+        cd argon2 && make && sudo make install && cd ..
+    - name: asio
+      run: |
+        wget https://github.com/aberaud/asio/archive/b2b7a1c166390459e1c169c8ae9ef3234b361e3f.tar.gz \
+        && tar -xvf b2b7a1c166390459e1c169c8ae9ef3234b361e3f.tar.gz && cd asio-b2b7a1c166390459e1c169c8ae9ef3234b361e3f/asio \
+        && ./autogen.sh && ./configure --prefix=/usr --without-boost --disable-examples --disable-tests  \
+        && sudo make install \
+        && cd ../../ && rm -rf asio*
+    - name: configure
+      run: ./configure
+    - name: make
+      run: make
+    - name: make check
+      run: make check
diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..0165417
--- /dev/null
@@ -0,0 +1,75 @@
+# Compiled Object files
+*.slo
+*.lo
+*.o
+*.obj
+
+# Compiled Dynamic libraries
+*.so
+*.dylib
+*.dll
+
+# Compiled Static libraries
+*.lai
+*.la
+*.a
+*.lib
+
+# Executables
+*.exe
+*.out
+*.app
+
+# 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
diff --git a/.gitmodules b/.gitmodules
new file mode 100644 (file)
index 0000000..ef38b69
--- /dev/null
@@ -0,0 +1,4 @@
+[submodule "argon2"]
+       path = argon2
+       url = https://github.com/P-H-C/phc-winner-argon2
+       ignore = dirty
diff --git a/.travis.yml b/.travis.yml
new file mode 100644 (file)
index 0000000..2c45aa2
--- /dev/null
@@ -0,0 +1,67 @@
+dist: xenial
+sudo: required
+
+services:
+  - docker
+
+language: cpp
+
+env:
+  matrix:
+    - OPENDHT_TEST_JOB="opendht.classic"
+    - OPENDHT_TEST_JOB="opendht.llvm"
+    - OPENDHT_TEST_JOB="opendht.proxyserver"
+    - OPENDHT_TEST_JOB="opendht.proxyclient"
+    - OPENDHT_TEST_JOB="opendht.push"
+
+before_install:
+  - |
+    if [[ "$OPENDHT_TEST_JOB" == *"opendht.classic"* ]]; then
+      docker pull aberaud/opendht-deps-bionic;
+    fi
+    if [[ "$OPENDHT_TEST_JOB" == *"opendht.proxyclient"* ]] || [[ "$OPENDHT_TEST_JOB" == *"opendht.proxyserver"* ]] || [[ "$OPENDHT_TEST_JOB" == *"opendht.push"* ]]; then
+      docker pull aberaud/opendht-deps;
+    fi
+    if [[ "$OPENDHT_TEST_JOB" == *"opendht.llvm"* ]]; then
+      docker pull aberaud/opendht-deps-llvm
+    fi
+
+script:
+  - |
+    # classic build
+    if [[ "$OPENDHT_TEST_JOB" == *"opendht.classic"* ]]; then
+      docker build -t opendht -f docker/DockerfileTravis .;
+    fi
+
+  - |
+    # proxy builds
+    if [[ "$OPENDHT_TEST_JOB" != *"opendht.llvm"* ]] && [[ "$OPENDHT_TEST_JOB" != *"opendht.classic"* ]]; then
+      docker build -t opendht-proxy -f docker/DockerfileTravisProxy .;
+      options='-DOPENDHT_SANITIZE=On ';
+      if [[ "$OPENDHT_TEST_JOB" == *"opendht.proxyserver"* ]] || [[ "$OPENDHT_TEST_JOB" == *"opendht.push"* ]]; then
+        options+='-DOPENDHT_PROXY_SERVER=ON ';
+      else
+        options+='-DOPENDHT_PROXY_SERVER=OFF ';
+      fi
+      if [[ "$OPENDHT_TEST_JOB" == *"opendht.proxyclient"* ]] || [[ "$OPENDHT_TEST_JOB" == *"opendht.push"* ]]; then
+        options+='-DOPENDHT_PROXY_CLIENT=ON ';
+      else
+        options+='-DOPENDHT_PROXY_CLIENT=OFF ';
+      fi
+      if [[ "$OPENDHT_TEST_JOB" == *"opendht.push"* ]]; then
+        options+='-DOPENDHT_PUSH_NOTIFICATIONS=ON ';
+      else
+        options+='-DOPENDHT_PUSH_NOTIFICATIONS=OFF ';
+      fi
+      docker run opendht-proxy /bin/sh -c "cd /root/opendht && mkdir build && cd build && cmake -DCMAKE_INSTALL_PREFIX=/usr -DOPENDHT_PYTHON=ON -DOPENDHT_LTO=ON -DOPENDHT_TESTS=ON $options .. && make -j8 && ./opendht_unit_tests && make install";
+    fi
+
+  - |
+    # llvm build
+    if [[ "$OPENDHT_TEST_JOB" == *"opendht.llvm"* ]]; then
+      docker build -f docker/DockerfileTravisLlvm .
+    fi
+
+notifications:
+  email:
+      - adrien.beraud@savoirfairelinux.com
diff --git a/CMakeLists.txt b/CMakeLists.txt
new file mode 100644 (file)
index 0000000..9fff55d
--- /dev/null
@@ -0,0 +1,539 @@
+cmake_minimum_required (VERSION 3.1)
+project (opendht)
+
+include(CMakePackageConfigHelpers)
+include(CMakeDependentOption)
+include(FindPkgConfig)
+include(cmake/CheckAtomic.cmake)
+
+set (opendht_VERSION_MAJOR 2)
+set (opendht_VERSION_MINOR 1.9)
+set (opendht_VERSION ${opendht_VERSION_MAJOR}.${opendht_VERSION_MINOR})
+set (PACKAGE_VERSION ${opendht_VERSION})
+set (VERSION "${opendht_VERSION}")
+
+# Options
+option (OPENDHT_STATIC "Build static library" ON)
+option (OPENDHT_SHARED "Build shared library" ON)
+option (OPENDHT_LOG "Build with logs" 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_ARGON2 "Use included argon2 sources" OFF)
+option (OPENDHT_LTO "Build with LTO" OFF)
+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)
+option (OPENDHT_PROXY_HTTP_PARSER_FORK "Build DHT proxy with custom http_parser to support old API" OFF)
+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 "Add unit tests executable" 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})
+
+# Dependencies
+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)
+    find_package (GnuTLS 3.3 REQUIRED)
+    pkg_search_module (Nettle nettle)
+    find_package (Msgpack 1.2 REQUIRED)
+    if (OPENDHT_TOOLS)
+        find_package (Readline 6 REQUIRED)
+    endif ()
+    if (NOT OPENDHT_ARGON2)
+        pkg_search_module(argon2 libargon2)
+        if (argon2_FOUND)
+            message("-- Found Argon2: " ${argon2_LIBRARY_DIRS} " (found version \"" ${argon2_VERSION} "\")")
+            link_directories (${argon2_LIBRARY_DIRS})
+        else ()
+            message("Argon2 not found, using included version.")
+            set(OPENDHT_ARGON2 ON)
+        endif()
+    endif ()
+
+    pkg_search_module(Jsoncpp jsoncpp)
+    if (Jsoncpp_FOUND)
+        add_definitions(-DOPENDHT_JSONCPP)
+        list (APPEND opendht_SOURCES
+          src/base64.h
+          src/base64.cpp
+        )
+    endif()
+
+    if (OPENDHT_HTTP)
+        find_path(ASIO_INCLUDE_DIR asio.hpp REQUIRED)
+        find_package(Restinio REQUIRED)
+        find_library(FMT_LIBRARY fmt)
+        add_library(fmt SHARED IMPORTED)
+        find_library(HTTP_PARSER_LIBRARY http_parser)
+        add_library(http_parser SHARED IMPORTED)
+        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 openssl)
+            if (OPENSSL_FOUND)
+                message(STATUS "Found OpenSSL ${OPENSSL_VERSION} ${OPENSSL_INCLUDE_DIRS}")
+                include_directories(SYSTEM ${OPENSSL_INCLUDE_DIRS})
+                link_directories (${OPENSSL_LIBRARY_DIRS})
+            else ()
+                message(SEND_ERROR "OpenSSL is required for DHT proxy as specified")
+            endif()
+        endif()
+        if (OPENDHT_PROXY_HTTP_PARSER_FORK)
+            add_definitions(-DOPENDHT_PROXY_HTTP_PARSER_FORK)
+        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()
+
+# Build flags
+set (CMAKE_CXX_STANDARD 14)
+set (CMAKE_CXX_STANDARD_REQUIRED on)
+
+if (NOT MSVC)
+    set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-return-type -Wall -Wextra -Wnon-virtual-dtor -pedantic-errors -fvisibility=hidden")
+    if (OPENDHT_SANITIZE)
+        set (CMAKE_CXX_FLAGS "${CMAKE_CXX_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_DISABLE_LEGACY_NIL -DMSGPACK_DISABLE_LEGACY_CONVERT")
+
+if (NOT CMAKE_BUILD_TYPE)
+    set(CMAKE_BUILD_TYPE Release)
+endif ()
+add_definitions(-DPACKAGE_VERSION="${opendht_VERSION}")
+if (OPENDHT_LOG)
+    add_definitions(-DOPENDHT_LOG=true)
+else ()
+    add_definitions(-DOPENDHT_LOG=false)
+endif()
+if (OPENDHT_LTO AND NOT MSVC)
+    set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -flto")
+    if (CMAKE_COMPILER_IS_GNUCC)
+        set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fuse-linker-plugin")
+        set (CMAKE_AR        "gcc-ar")
+        set (CMAKE_NM        "gcc-nm")
+        set (CMAKE_RANLIB    "gcc-ranlib")
+    endif ()
+endif ()
+
+if (MSGPACK_INCLUDE_DIRS)
+    include_directories (SYSTEM "${MSGPACK_INCLUDE_DIRS}")
+endif ()
+if (GNUTLS_INCLUDE_DIRS)
+    include_directories (SYSTEM "${GNUTLS_INCLUDE_DIRS}")
+endif ()
+if (Nettle_INCLUDE_DIRS)
+    include_directories (SYSTEM "${Nettle_INCLUDE_DIRS}")
+endif ()
+if (ASIO_INCLUDE_DIR)
+    include_directories (SYSTEM "${ASIO_INCLUDE_DIR}")
+endif ()
+if (Restinio_INCLUDE_DIR)
+    include_directories (SYSTEM "${Restinio_INCLUDE_DIR}")
+endif ()
+if (Jsoncpp_INCLUDE_DIRS)
+    include_directories (SYSTEM "${Jsoncpp_INCLUDE_DIRS}")
+endif ()
+link_directories (${Nettle_LIBRARY_DIRS})
+link_directories (${Jsoncpp_LIBRARY_DIRS})
+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 "\${exec_prefix}/${CMAKE_INSTALL_LIBDIR}")
+set (includedir "\${prefix}/${CMAKE_INSTALL_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/infohash.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/log_enable.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(OPENDHT_ARGON2)
+    # make sure argon2 submodule is up to date and initialized
+    message("Initializing Argon2 submodule")
+    execute_process(COMMAND git submodule update --init)
+
+    # add local argon2 files to build
+    list (APPEND opendht_SOURCES
+        argon2/src/argon2.c
+        argon2/src/core.c
+        argon2/src/blake2/blake2b.c
+        argon2/src/thread.c
+        argon2/src/ref.c
+        argon2/src/encoding.c
+    )
+    include_directories(argon2/include/)
+endif()
+
+if (MSVC)
+    list (APPEND opendht_HEADERS src/compat/msvc/unistd.h)
+endif ()
+
+# Targets
+if (OPENDHT_STATIC)
+    if (NOT MSVC)
+        add_library (opendht-static STATIC
+            ${opendht_SOURCES}
+            ${opendht_HEADERS}
+        )
+        set_target_properties (opendht-static PROPERTIES OUTPUT_NAME "opendht")
+        if (OPENDHT_ARGON2)
+            target_include_directories(opendht-static SYSTEM PRIVATE argon2)
+        else ()
+            target_include_directories(opendht-static SYSTEM PRIVATE ${argon2_INCLUDE_DIRS})
+        endif ()
+        target_link_libraries(opendht-static
+            PRIVATE  ${argon2_LIBRARIES}
+            PUBLIC ${CMAKE_THREAD_LIBS_INIT} ${GNUTLS_LIBRARIES} ${Nettle_STATIC_LIBRARIES}
+                   ${Jsoncpp_STATIC_LIBRARIES} ${FMT_LIBRARY} ${HTTP_PARSER_LIBRARY}
+                   ${OPENSSL_STATIC_LIBRARIES})
+    else ()
+        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 ()
+        add_library (opendht-static STATIC
+            ${opendht_SOURCES}
+            ${opendht_HEADERS}
+            ${obj_libs}
+        )
+        target_link_libraries(opendht-static PUBLIC ${Win32_STATIC_LIBRARIES} ${Win32_IMPORT_LIBRARIES})
+        set(CMAKE_STATIC_LINKER_FLAGS "${CMAKE_STATIC_LINKER_FLAGS} /ignore:4006")
+        set_target_properties (opendht-static PROPERTIES OUTPUT_NAME "libopendht")
+    endif()
+    install (TARGETS opendht-static DESTINATION ${CMAKE_INSTALL_LIBDIR} EXPORT opendht)
+endif ()
+
+if (OPENDHT_SHARED)
+    add_library (opendht SHARED
+        ${opendht_SOURCES}
+        ${opendht_HEADERS}
+    )
+    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)
+    if (OPENDHT_ARGON2)
+        target_include_directories(opendht SYSTEM PRIVATE argon2)
+    else ()
+        target_link_libraries(opendht PRIVATE ${argon2_LIBRARIES})
+        target_include_directories(opendht SYSTEM PRIVATE ${argon2_INCLUDE_DIRS})
+    endif ()
+    if (APPLE)
+        target_link_libraries(opendht
+            PUBLIC ${CMAKE_THREAD_LIBS_INIT} ${OPENSSL_LIBRARIES}
+            PRIVATE ${GNUTLS_LIBRARIES} ${Nettle_LIBRARIES}
+                    ${Jsoncpp_LIBRARIES}
+                    ${FMT_LIBRARY} ${HTTP_PARSER_LIBRARY}
+            SYSTEM "-framework CoreFoundation" "-framework Security")
+    else ()
+        target_link_libraries(opendht
+            PUBLIC ${CMAKE_THREAD_LIBS_INIT} ${OPENSSL_LIBRARIES}
+            PRIVATE ${GNUTLS_LIBRARIES} ${Nettle_LIBRARIES}
+                    ${Jsoncpp_LIBRARIES}
+                    ${FMT_LIBRARY} ${HTTP_PARSER_LIBRARY})
+    endif ()
+
+    install (TARGETS opendht DESTINATION ${CMAKE_INSTALL_LIBDIR} EXPORT opendht)
+endif ()
+
+if (OPENDHT_C)
+    add_library (opendht-c SHARED
+        c/opendht.cpp
+        c/opendht_c.h
+    )
+    target_compile_definitions(opendht-c PRIVATE OPENDHT_C_BUILD)
+    if (OPENDHT_SHARED)
+        target_link_libraries(opendht-c opendht)
+    else ()
+        target_link_libraries(opendht-c opendht-static)
+    endif ()
+
+    # PkgConfig module
+    configure_file (
+        opendht-c.pc.in
+        opendht-c.pc
+        @ONLY
+    )
+    install (TARGETS opendht-c DESTINATION ${CMAKE_INSTALL_LIBDIR} EXPORT opendht-c)
+    install (FILES ${CMAKE_CURRENT_BINARY_DIR}/opendht-c.pc DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig)
+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 (OPENDHT_TESTS)
+    pkg_search_module(Cppunit REQUIRED 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_include_directories(opendht_unit_tests SYSTEM PRIVATE ${Cppunit_INCLUDE_DIR})
+    target_link_directories(opendht_unit_tests PRIVATE ${Cppunit_LIBRARY_DIRS})
+    if (OPENDHT_SHARED)
+        target_link_libraries(opendht_unit_tests opendht)
+    else ()
+        target_link_libraries(opendht_unit_tests opendht-static)
+    endif ()
+    target_link_libraries(opendht_unit_tests
+       ${CMAKE_THREAD_LIBS_INIT}
+       ${Cppunit_LIBRARIES}
+       ${GNUTLS_LIBRARIES}
+       ${Jsoncpp_LIBRARIES}
+    )
+    if (OPENDHT_PROXY_OPENSSL)
+        target_link_libraries(opendht_unit_tests ${OPENSSL_LIBRARIES})
+    endif()
+    enable_testing()
+    add_test(TEST opendht_unit_tests)
+endif()
diff --git a/COPYING b/COPYING
new file mode 100644 (file)
index 0000000..94a9ed0
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,674 @@
+                    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>.
diff --git a/Makefile.am b/Makefile.am
new file mode 100644 (file)
index 0000000..7ba6efb
--- /dev/null
@@ -0,0 +1,31 @@
+AM_CXXFLAGS = -pthread
+
+SUBDIRS =
+
+SUBDIRS += src
+
+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
diff --git a/README.md b/README.md
new file mode 100644 (file)
index 0000000..34fb21b
--- /dev/null
+++ b/README.md
@@ -0,0 +1,122 @@
+<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++14 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++14** 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 build and install
+
+Build instructions: <https://github.com/savoirfairelinux/opendht/wiki/Build-the-library>
+
+#### How-to build a simple client app
+```bash
+g++ main.cpp -std=c++14 -lopendht
+```
+
+## 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.
+- (optional) restinio used for the REST API.
+- (optional) jsoncpp 1.7.4-3+, used for the REST API.
+- Build tested with GCC 5.2+ (GNU/Linux, Windows with MinGW), Clang/LLVM (GNU/Linux, Android, macOS, iOS).
+- Build tested with Microsoft Visual Studio 2015, 2017, 2019
+
+## Contact
+
+IRC: join us on Freenode at [`#opendht`](https://webchat.freenode.net/?channels=%23opendht).
+
+## License
+Copyright (C) 2014-2020 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.
+
+## Donations
+We gratefully accept Bitcoin donations to support OpenDHT development at: `bitcoin:3EykSd1An888efq4Bq3KaV3hJ3JQ4FPnwm`.
diff --git a/autogen.sh b/autogen.sh
new file mode 100755 (executable)
index 0000000..1a4d907
--- /dev/null
@@ -0,0 +1,2 @@
+git submodule update --init
+autoreconf --install --verbose -Wall
diff --git a/c/opendht.cpp b/c/opendht.cpp
new file mode 100644 (file)
index 0000000..282bc5d
--- /dev/null
@@ -0,0 +1,430 @@
+#include "opendht_c.h"
+#include "opendht.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
+
+// 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(dat, HASH_LEN*2)));
+}
+
+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();
+}
+
+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_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);
+    auto rdata = new dht::Blob;
+    *rdata = pkey->encrypt((const uint8_t*)data, data_size);
+    return (dht_blob*)rdata;
+}
+
+// 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(std::make_shared<dht::crypto::PublicKey>(key->getPublicKey())));
+}
+
+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(std::make_shared<dht::crypto::PublicKey>(cert->getPublicKey())));
+}
+
+// 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() {
+    return reinterpret_cast<dht_runner*>(new dht::DhtRunner);
+}
+
+void dht_runner_delete(dht_runner* runner) {
+    delete reinterpret_cast<dht::DhtRunner*>(runner);
+}
+
+void dht_runner_run(dht_runner* r, in_port_t port) {
+    auto runner = reinterpret_cast<dht::DhtRunner*>(r);
+    runner->run(port, {}, true);
+}
+
+void dht_runner_run_config(dht_runner* r, in_port_t port, const dht_runner_config* conf) {
+    auto runner = reinterpret_cast<dht::DhtRunner*>(r);
+    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.peer_discovery = conf->peer_discovery;
+    config.peer_publish = conf->peer_publish;
+    runner->run(port, config);
+}
+
+void dht_runner_ping(dht_runner* r, struct sockaddr* addr, socklen_t addr_len) {
+    auto runner = reinterpret_cast<dht::DhtRunner*>(r);
+    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>;
+    auto guard = done_cb ? std::make_shared<ScopeGuardCb>(done_cb, cb_user_data) : std::shared_ptr<ScopeGuardCb>{};
+    *fret = runner->listen(*hash, [cb,cb_user_data, guard](const std::vector<std::shared_ptr<dht::Value>>& values, bool expired) {
+        for (const auto& value : values) {
+            if (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);
+    });
+}
+
+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++) {
+        if (auto len = addrs[i].getLength()) {
+            ret[i] = (struct sockaddr*)malloc(len);
+            memcpy((struct sockaddr*)ret[i], addrs[i].get(), len);
+        } else {
+            ret[i] = nullptr;
+        }
+    }
+    ret[addrs.size()] = nullptr;
+    return ret;
+}
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/c/opendht_c.h b/c/opendht_c.h
new file mode 100644 (file)
index 0000000..be856cb
--- /dev/null
@@ -0,0 +1,156 @@
+#pragma once
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include "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;
+
+// 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_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 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_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);
+
+// 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;
+    bool peer_discovery;
+    bool peer_publish;
+    dht_certificate* server_ca;
+    dht_identity client_identity;
+};
+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();
+OPENDHT_C_PUBLIC void dht_runner_delete(dht_runner* runner);
+OPENDHT_C_PUBLIC void dht_runner_run(dht_runner* runner, in_port_t port);
+OPENDHT_C_PUBLIC void 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);
+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 struct sockaddr** dht_runner_get_public_address(const dht_runner* runner);
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/cmake/CheckAtomic.cmake b/cmake/CheckAtomic.cmake
new file mode 100644 (file)
index 0000000..d4a6711
--- /dev/null
@@ -0,0 +1,95 @@
+# University of Illinois/NCSA
+# Open Source License
+#
+# Copyright (c) 2003-2017 University of Illinois at Urbana-Champaign.
+# All rights reserved.
+#
+# Developed by:
+#
+#     LLVM Team
+#
+#     University of Illinois at Urbana-Champaign
+#
+#     http://llvm.org
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy of
+# this software and associated documentation files (the "Software"), to deal with
+# the Software without restriction, including without limitation the rights to
+# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+# of the Software, and to permit persons to whom the Software is furnished to do
+# so, subject to the following conditions:
+#
+#     * Redistributions of source code must retain the above copyright notice,
+#       this list of conditions and the following disclaimers.
+#
+#     * Redistributions in binary form must reproduce the above copyright notice,
+#       this list of conditions and the following disclaimers in the
+#       documentation and/or other materials provided with the distribution.
+#
+#     * Neither the names of the LLVM Team, University of Illinois at
+#       Urbana-Champaign, nor the names of its contributors may be used to
+#       endorse or promote products derived from this Software without specific
+#       prior written permission.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
+# CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE
+# SOFTWARE.
+
+include(CheckCXXSourceCompiles)
+include(CheckLibraryExists)
+
+# 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})
+  get_directory_property(compile_options COMPILE_OPTIONS)
+  set(CMAKE_REQUIRED_FLAGS ${compile_options})
+  CHECK_CXX_SOURCE_COMPILES("
+#include <atomic>
+#include <cstdint>
+std::atomic<int64_t> v1;
+std::atomic<int64_t> v2;
+int main(int, char**) {
+  return v1 + v2;
+}" ${varname})
+  set(CMAKE_REQUIRED_FLAGS ${OLD_CMAKE_REQUIRED_FLAGS})
+endfunction(check_working_cxx_atomics)
+
+
+if(NOT DEFINED PROXYGEN_COMPILER_IS_GCC_COMPATIBLE)
+  if(CMAKE_COMPILER_IS_GNUCXX)
+    set(PROXYGEN_COMPILER_IS_GCC_COMPATIBLE ON)
+  elseif(MSVC)
+    set(PROXYGEN_COMPILER_IS_GCC_COMPATIBLE OFF)
+  elseif("${CMAKE_CXX_COMPILER_ID}" MATCHES "Clang")
+    set(PROXYGEN_COMPILER_IS_GCC_COMPATIBLE ON)
+  elseif("${CMAKE_CXX_COMPILER_ID}" MATCHES "Intel")
+    set(PROXYGEN_COMPILER_IS_GCC_COMPATIBLE ON)
+  endif()
+endif()
+
+# This isn't necessary on MSVC, so avoid command-line switch annoyance
+# by only running on GCC-like hosts.
+if(PROXYGEN_COMPILER_IS_GCC_COMPATIBLE)
+  # 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)
+    check_library_exists(atomic __atomic_fetch_add_4 "" HAVE_LIBATOMIC)
+    if(HAVE_LIBATOMIC)
+      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()
+      list(APPEND CMAKE_CXX_STANDARD_LIBRARIES -latomic)
+    else()
+      message(FATAL_ERROR "Host compiler appears to require libatomic, but cannot find it.")
+    endif()
+  endif()
+endif()
diff --git a/cmake/FindMsgpack.cmake b/cmake/FindMsgpack.cmake
new file mode 100644 (file)
index 0000000..7d9dd81
--- /dev/null
@@ -0,0 +1,64 @@
+# - Try to find msgpack
+# Once done this will define
+#  MSGPACK_FOUND - System has msgpack
+#  MSGPACK_INCLUDE_DIRS - The msgpack include directories
+#  MSGPACK_LIBRARIES - The libraries needed to use msgpack
+
+if(NOT MSGPACK_USE_BUNDLED)
+  find_package(PkgConfig)
+  if (PKG_CONFIG_FOUND)
+    pkg_search_module(PC_MSGPACK QUIET
+      msgpackc>=${Msgpack_FIND_VERSION}
+      msgpack>=${Msgpack_FIND_VERSION})
+  endif()
+else()
+  set(PC_MSGPACK_INCLUDEDIR)
+  set(PC_MSGPACK_INCLUDE_DIRS)
+  set(PC_MSGPACK_LIBDIR)
+  set(PC_MSGPACK_LIBRARY_DIRS)
+  set(LIMIT_SEARCH NO_DEFAULT_PATH)
+endif()
+
+set(MSGPACK_DEFINITIONS ${PC_MSGPACK_CFLAGS_OTHER})
+
+find_path(MSGPACK_INCLUDE_DIR msgpack/version_master.h
+  HINTS ${PC_MSGPACK_INCLUDEDIR} ${PC_MSGPACK_INCLUDE_DIRS}
+  ${LIMIT_SEARCH})
+
+if(MSGPACK_INCLUDE_DIR)
+  file(READ ${MSGPACK_INCLUDE_DIR}/msgpack/version_master.h msgpack_version_h)
+  string(REGEX REPLACE ".*MSGPACK_VERSION_MAJOR +([0-9]+).*" "\\1" MSGPACK_VERSION_MAJOR "${msgpack_version_h}")
+  string(REGEX REPLACE ".*MSGPACK_VERSION_MINOR +([0-9]+).*" "\\1" MSGPACK_VERSION_MINOR "${msgpack_version_h}")
+  string(REGEX REPLACE ".*MSGPACK_VERSION_REVISION +([0-9]+).*" "\\1" MSGPACK_VERSION_REVISION "${msgpack_version_h}")
+  set(MSGPACK_VERSION_STRING "${MSGPACK_VERSION_MAJOR}.${MSGPACK_VERSION_MINOR}.${MSGPACK_VERSION_REVISION}")
+else()
+  set(MSGPACK_VERSION_STRING)
+endif()
+
+# If we're asked to use static linkage, add libmsgpack{,c}.a as a preferred library name.
+if(MSGPACK_USE_STATIC)
+  list(APPEND MSGPACK_NAMES
+    "${CMAKE_STATIC_LIBRARY_PREFIX}msgpackc${CMAKE_STATIC_LIBRARY_SUFFIX}"
+    "${CMAKE_STATIC_LIBRARY_PREFIX}msgpack${CMAKE_STATIC_LIBRARY_SUFFIX}")
+endif()
+
+list(APPEND MSGPACK_NAMES msgpackc msgpack)
+
+find_library(MSGPACK_LIBRARY NAMES ${MSGPACK_NAMES}
+  # Check each directory for all names to avoid using headers/libraries from
+  # different places.
+  NAMES_PER_DIR
+  HINTS ${PC_MSGPACK_LIBDIR} ${PC_MSGPACK_LIBRARY_DIRS}
+  ${LIMIT_SEARCH})
+
+mark_as_advanced(MSGPACK_INCLUDE_DIR MSGPACK_LIBRARY)
+
+set(MSGPACK_LIBRARIES ${MSGPACK_LIBRARY})
+set(MSGPACK_INCLUDE_DIRS ${MSGPACK_INCLUDE_DIR})
+
+include(FindPackageHandleStandardArgs)
+# handle the QUIETLY and REQUIRED arguments and set MSGPACK_FOUND to TRUE
+# if all listed variables are TRUE
+find_package_handle_standard_args(Msgpack
+  REQUIRED_VARS MSGPACK_LIBRARY MSGPACK_INCLUDE_DIR
+  VERSION_VAR MSGPACK_VERSION_STRING)
diff --git a/cmake/FindReadline.cmake b/cmake/FindReadline.cmake
new file mode 100644 (file)
index 0000000..6807812
--- /dev/null
@@ -0,0 +1,48 @@
+# - 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)
diff --git a/cmake/FindRestinio.cmake b/cmake/FindRestinio.cmake
new file mode 100644 (file)
index 0000000..a0887b3
--- /dev/null
@@ -0,0 +1,14 @@
+# 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()
diff --git a/configure.ac b/configure.ac
new file mode 100644 (file)
index 0000000..23a317b
--- /dev/null
@@ -0,0 +1,235 @@
+dnl define macros
+m4_define([opendht_major_version], 2)
+m4_define([opendht_minor_version], 1)
+m4_define([opendht_patch_version], 9)
+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 -Wno-return-type -Wall -Wextra -Wnon-virtual-dtor -O0 -pedantic-errors"],
+      [CXXFLAGS="${CXXFLAGS} -O3 -pedantic-errors"])
+
+AC_PROG_CXX
+AM_PROG_AR
+
+dnl Check for logs
+AC_ARG_ENABLE([logs], [AS_HELP_STRING([--disable-logs], [Disable DHT logs])])
+AS_IF([test "x$enable_logs" != "xno"], [
+       AC_DEFINE([OPENDHT_LOG], [true], [Define if DHT logs are enabled])
+], [
+       AC_DEFINE([OPENDHT_LOG], [false], [Define if DHT logs are 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"])
+
+LT_INIT()
+LT_LANG(C++)
+
+AX_CXX_COMPILE_STDCXX(14,[noext],[mandatory])
+
+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)
+AM_COND_IF([ENABLE_TESTS], [
+       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])
+PKG_CHECK_MODULES([MsgPack], [msgpack >= 1.2])
+
+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])
+])
+
+AC_ARG_WITH([http_parser_fork], AS_HELP_STRING([--with-http-parser-fork], [Build with http_parser fork to support old API]))
+AS_IF([test "x$with_http_parser_fork" = "xyes"],[
+    AC_MSG_NOTICE([Using http_parser fork])
+    AC_DEFINE([OPENDHT_PROXY_HTTP_PARSER_FORK], [], [Define if using http parser fork])
+], [
+    AC_MSG_NOTICE([Not using http_parser fork])
+])
+
+AM_COND_IF([PROXY_CLIENT_OR_SERVER], [
+    AC_CHECK_HEADERS([asio.hpp], exit,, AC_MSG_ERROR([Missing Asio headers files]))
+    CXXFLAGS="${CXXFLAGS} -DASIO_STANDALONE"
+    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])
+    ])
+    # 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_DISABLE_LEGACY_NIL -DMSGPACK_DISABLE_LEGACY_CONVERT"
+
+dnl Check for Argon2
+AC_ARG_WITH([argon2], AS_HELP_STRING([--without-argon2], [Use included Argon2]))
+AS_IF([test "x$with_argon2" != "xno"],
+                       [PKG_CHECK_MODULES([Argon2], [libargon2], [have_argon2=yes], [have_argon2=no])],
+                       [have_argon2=no])
+AS_IF([test "x$have_argon2" = "xyes"], [
+  AC_MSG_NOTICE([Using system Argon2])
+  AC_SUBST(argon2_lib, [", libargon2"])
+], [
+               AS_IF([test "x$with_argon2" = "xyes"], [
+                               AC_MSG_ERROR([Argon2 requested but not found])
+               ],[
+                               AC_MSG_NOTICE([Using included Argon2])
+                               AC_SUBST(Argon2_CFLAGS, "-I\${top_srcdir}/argon2/src -I\${top_srcdir}/argon2/include")
+                               AC_SUBST(Argon2_LIBS, "libargon2.la")
+                               AC_SUBST(Argon2_LDFLAGS, "-L\${abs_top_srcdir}/argon2/src/.libs")
+               ])
+])
+
+AM_CONDITIONAL([WITH_INCLUDED_ARGON2], [test "x$have_argon2" = "xno"])
+
+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
+                 tools/Makefile
+                 python/Makefile
+                 python/setup.py
+                 tests/Makefile
+                 doc/Makefile
+                 doc/Doxyfile
+                 opendht.pc])
+AC_OUTPUT
diff --git a/doc/CMakeLists.txt b/doc/CMakeLists.txt
new file mode 100644 (file)
index 0000000..681cb28
--- /dev/null
@@ -0,0 +1,16 @@
+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()
diff --git a/doc/Doxyfile.in b/doc/Doxyfile.in
new file mode 100644 (file)
index 0000000..19dfa98
--- /dev/null
@@ -0,0 +1,983 @@
+# 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
diff --git a/doc/Makefile.am b/doc/Makefile.am
new file mode 100644 (file)
index 0000000..0ac8f91
--- /dev/null
@@ -0,0 +1,16 @@
+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
diff --git a/doc/connectivity-loss-by-search-criteria.pdf b/doc/connectivity-loss-by-search-criteria.pdf
new file mode 100644 (file)
index 0000000..eb67f56
Binary files /dev/null and b/doc/connectivity-loss-by-search-criteria.pdf differ
diff --git a/doc/connectivity-loss-by-search-criteria.tex b/doc/connectivity-loss-by-search-criteria.tex
new file mode 100644 (file)
index 0000000..8662c50
--- /dev/null
@@ -0,0 +1,142 @@
+\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}
diff --git a/doc/dhtnode.1 b/doc/dhtnode.1
new file mode 100644 (file)
index 0000000..29f5e47
--- /dev/null
@@ -0,0 +1,109 @@
+.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:
+.EE
+    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).
+.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>
diff --git a/docker/Dockerfile b/docker/Dockerfile
new file mode 100644 (file)
index 0000000..2b4a744
--- /dev/null
@@ -0,0 +1,6 @@
+FROM aberaud/opendht-deps
+MAINTAINER Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+RUN git clone https://github.com/savoirfairelinux/opendht.git \
+       && cd opendht && mkdir build && cd build \
+       && cmake .. -DCMAKE_INSTALL_PREFIX=/usr -DOPENDHT_PYTHON=On -DOPENDHT_LTO=On -DOPENDHT_PROXY_SERVER=On -DOPENDHT_PROXY_CLIENT=On && make -j8 && make install \
+       && cd ../.. && rm -rf opendht
diff --git a/docker/DockerfileBionic b/docker/DockerfileBionic
new file mode 100644 (file)
index 0000000..4e2c272
--- /dev/null
@@ -0,0 +1,13 @@
+FROM docker.pkg.github.com/savoirfairelinux/opendht/opendht-deps-bionic:2.1.3
+MAINTAINER Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+
+RUN git clone https://github.com/savoirfairelinux/opendht.git \
+       && cd opendht && mkdir build && cd build \
+       && cmake .. -DCMAKE_INSTALL_PREFIX=/usr \
+               -DOPENDHT_PROXY_CLIENT=On \
+               -DOPENDHT_PROXY_SERVER=On \
+               -DOPENDHT_C=On \
+               -DOPENDHT_PYTHON=On \
+               -DOPENDHT_LTO=On \
+               && make -j8 && make install \
+       && cd ../.. && rm -rf opendht
diff --git a/docker/DockerfileDeps b/docker/DockerfileDeps
new file mode 100644 (file)
index 0000000..57cb87b
--- /dev/null
@@ -0,0 +1,27 @@
+FROM ubuntu:20.04
+MAINTAINER Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+
+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 \
+        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
+
+RUN echo "*** Downloading RESTinio ***" \
+    && mkdir restinio && cd restinio \
+    && wget https://github.com/aberaud/restinio/archive/2c0b6f5e5ba04d7a74e8406a3df1fd433680599d.tar.gz \
+    && ls -l && tar -xzf 2c0b6f5e5ba04d7a74e8406a3df1fd433680599d.tar.gz \
+    && cd restinio-2c0b6f5e5ba04d7a74e8406a3df1fd433680599d/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
diff --git a/docker/DockerfileDepsBionic b/docker/DockerfileDepsBionic
new file mode 100644 (file)
index 0000000..ad85458
--- /dev/null
@@ -0,0 +1,30 @@
+FROM ubuntu:18.04
+MAINTAINER Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+
+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 \
+        autotools-dev autoconf libfmt-dev libhttp-parser-dev libmsgpack-dev libssl-dev \
+    && apt-get clean
+
+RUN apt-get update && apt-get install -y python3-pip && 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*
diff --git a/docker/DockerfileDepsLlvm b/docker/DockerfileDepsLlvm
new file mode 100644 (file)
index 0000000..a207d30
--- /dev/null
@@ -0,0 +1,28 @@
+FROM ubuntu:20.04
+MAINTAINER Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+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 clang make cmake pkg-config 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 \
+       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
+
+ENV CC cc
+ENV CXX c++
+
+RUN echo "*** Downloading RESTinio ***" \
+    && mkdir restinio && cd restinio \
+    && wget https://github.com/aberaud/restinio/archive/2c0b6f5e5ba04d7a74e8406a3df1fd433680599d.tar.gz \
+    && ls -l && tar -xzf 2c0b6f5e5ba04d7a74e8406a3df1fd433680599d.tar.gz \
+    && cd restinio-2c0b6f5e5ba04d7a74e8406a3df1fd433680599d/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*
diff --git a/docker/DockerfileLlvm b/docker/DockerfileLlvm
new file mode 100644 (file)
index 0000000..62a08cd
--- /dev/null
@@ -0,0 +1,6 @@
+FROM aberaud/opendht-deps-llvm
+MAINTAINER Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+RUN git clone https://github.com/savoirfairelinux/opendht.git \
+       && cd opendht && mkdir build && cd build \
+       && cmake .. -DCMAKE_INSTALL_PREFIX=/usr -DOPENDHT_PYTHON=On -DOPENDHT_LTO=On -DOPENDHT_PROXY_SERVER=On -DOPENDHT_PROXY_CLIENT=On && make -j8 && make install \
+       && cd ../.. && rm -rf opendht
diff --git a/docker/DockerfileTravis b/docker/DockerfileTravis
new file mode 100644 (file)
index 0000000..625efb7
--- /dev/null
@@ -0,0 +1,9 @@
+FROM aberaud/opendht-deps-bionic
+MAINTAINER Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+
+RUN apt-get update && apt-get install -y python3-pip && pip3 install --upgrade cmake
+
+COPY . /root/opendht
+RUN cd /root/opendht && mkdir build && cd build \
+       && cmake -DCMAKE_INSTALL_PREFIX=/usr -DOPENDHT_PYTHON=On -DOPENDHT_C=On -DOPENDHT_LTO=On -DOPENDHT_TESTS=ON ..  \
+       && make -j8 && ./opendht_unit_tests && make install
diff --git a/docker/DockerfileTravisLlvm b/docker/DockerfileTravisLlvm
new file mode 100644 (file)
index 0000000..679e882
--- /dev/null
@@ -0,0 +1,7 @@
+FROM aberaud/opendht-deps-llvm
+MAINTAINER Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+
+COPY . /root/opendht
+RUN cd /root/opendht && mkdir build && cd build \
+       && cmake -DCMAKE_INSTALL_PREFIX=/usr -DOPENDHT_PYTHON=On -DOPENDHT_C=On -DOPENDHT_TESTS=ON .. \
+       && make -j8 && ./opendht_unit_tests && make install
diff --git a/docker/DockerfileTravisProxy b/docker/DockerfileTravisProxy
new file mode 100644 (file)
index 0000000..a4a8835
--- /dev/null
@@ -0,0 +1,4 @@
+FROM aberaud/opendht-deps
+MAINTAINER Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+
+COPY . /root/opendht
diff --git a/include/opendht.h b/include/opendht.h
new file mode 100644 (file)
index 0000000..bfd6c00
--- /dev/null
@@ -0,0 +1,22 @@
+/*
+ *  Copyright (C) 2014-2020 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"
diff --git a/include/opendht/callbacks.h b/include/opendht/callbacks.h
new file mode 100644 (file)
index 0000000..b45e3f9
--- /dev/null
@@ -0,0 +1,190 @@
+/*
+ *  Copyright (C) 2014-2020 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};
+    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};
+
+    /** 
+     * 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 CertificateStoreQuery = std::function<std::vector<std::shared_ptr<crypto::Certificate>>(const InfoHash& pk_id)>;
+
+typedef bool (*GetCallbackRaw)(std::shared_ptr<Value>, void *user_data);
+typedef bool (*ValueCallbackRaw)(std::shared_ptr<Value>, bool expired, void *user_data);
+
+using DoneCallback = std::function<void(bool success, const std::vector<std::shared_ptr<Node>>& nodes)>;
+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);
+
+using DoneCallbackSimple = std::function<void(bool success)>;
+
+OPENDHT_PUBLIC GetCallbackSimple bindGetCb(const GetCallbackRaw& raw_cb, void* user_data);
+OPENDHT_PUBLIC GetCallback bindGetCb(const GetCallbackSimple& cb);
+OPENDHT_PUBLIC ValueCallback bindValueCb(const ValueCallbackRaw& raw_cb, void* user_data);
+OPENDHT_PUBLIC ShutdownCallback bindShutdownCb(const ShutdownCallbackRaw& shutdown_cb_raw, void* user_data);
+OPENDHT_PUBLIC DoneCallback bindDoneCb(DoneCallbackSimple donecb);
+OPENDHT_PUBLIC DoneCallback bindDoneCb(const DoneCallbackRaw& raw_cb, void* user_data);
+OPENDHT_PUBLIC DoneCallbackSimple bindDoneCbSimple(const DoneCallbackSimpleRaw& raw_cb, void* user_data);
+OPENDHT_PUBLIC Value::Filter bindFilterRaw(const FilterRaw& raw_filter, void* user_data);
+
+}
diff --git a/include/opendht/crypto.h b/include/opendht/crypto.h
new file mode 100644 (file)
index 0000000..e3e0a20
--- /dev/null
@@ -0,0 +1,756 @@
+/*
+ *  Copyright (C) 2014-2020 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>
+
+#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) {}
+    PublicKey(const uint8_t* dat, size_t dat_size);
+    PublicKey(const Blob& pk) : PublicKey(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 || getId() == o.getId();
+    }
+    bool operator !=(const PublicKey& o) const {
+        return !(*this == o);
+    }
+
+    PublicKey& operator=(PublicKey&& o) noexcept;
+
+    /**
+     * Get public key fingerprint
+     */
+    InfoHash getId() const;
+
+    /**
+     * Get public key long fingerprint
+     */
+    PkId getLongId() const;
+
+    bool checkSignature(const uint8_t* data, size_t data_len, const uint8_t* signature, size_t signature_len) const;
+    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;
+    Blob encrypt(const Blob& data) const {
+        return encrypt(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:
+    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.data()) {}
+    ~PrivateKey();
+    explicit operator bool() const { return key; }
+
+    PublicKey getPublicKey() 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 Blob&) const;
+
+    /**
+     * Try to decrypt the provided cypher text.
+     * In case of failure a CryptoException is thrown.
+     * @returns the decrypted data.
+     */
+    Blob decrypt(const Blob& cypher) const;
+
+    /**
+     * 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;
+
+    //friend dht::crypto::Identity dht::crypto::generateIdentity(const std::string&, dht::crypto::Identity, unsigned key_length);
+};
+
+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(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 OcspResponse
+{
+public:
+    OcspResponse(const uint8_t* dat_ptr, size_t dat_size);
+    OcspResponse(const std::string& 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.
+     * Return OCSP verify reason.
+     * http://www.gnu.org/software/gnutls/reference/gnutls-ocsp.html#gnutls-ocsp-verify-reason-t
+     */
+    gnutls_ocsp_verify_reason_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)) { o.cert = nullptr; };
+
+    /**
+     * Import certificate (PEM or DER) or certificate chain (PEM),
+     * ordered from subject to issuer
+     */
+    Certificate(const Blob& crt);
+    Certificate(const std::string& pem) : cert(nullptr) {
+        unpack((const uint8_t*)pem.data(), pem.size());
+    }
+    Certificate(const uint8_t* dat, size_t dat_size) : cert(nullptr) {
+        unpack(dat, dat_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; }
+    PublicKey getPublicKey() const;
+
+    /** Same as getPublicKey().getId() */
+    InfoHash getId() const;
+    /** Same as getPublicKey().getLongId() */
+    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);
+    static Certificate generate(const CertificateRequest& request, const Identity& ca);
+
+    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);
+
+    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;
+
+    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;
+};
+
+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 Blob& data, const Blob& key);
+OPENDHT_PUBLIC Blob aesDecrypt(const Blob& data, const std::string& password);
+
+}
+}
diff --git a/include/opendht/def.h b/include/opendht/def.h
new file mode 100644 (file)
index 0000000..493ceee
--- /dev/null
@@ -0,0 +1,44 @@
+#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
diff --git a/include/opendht/default_types.h b/include/opendht/default_types.h
new file mode 100644 (file)
index 0000000..ef3434c
--- /dev/null
@@ -0,0 +1,269 @@
+/*
+ *  Copyright (C) 2014-2020 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)
+            from = v.owner->getId();
+        BaseClass::unpackValue(v);
+    }
+
+    static Value::Filter getFilter() {
+        return [](const Value& v){ return v.isSigned(); };
+    }
+
+    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) : service(s) {}
+    TrustRequest(std::string s, const Blob& d) : service(s), payload(d) {}
+
+    static Value::Filter getFilter() {
+        return EncryptedValue::getFilter();
+    }
+
+    std::string service;
+    Blob payload;
+    bool confirm {false};
+    MSGPACK_DEFINE_MAP(service, 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;
+
+}
diff --git a/include/opendht/dht.h b/include/opendht/dht.h
new file mode 100644 (file)
index 0000000..061e221
--- /dev/null
@@ -0,0 +1,624 @@
+/*
+ *  Copyright (C) 2014-2020 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:
+
+    Dht();
+
+    /**
+     * 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 = {});
+
+    Dht(std::unique_ptr<net::DatagramSocket>&& sock, const Config& config, const Logger& l = {})
+        : Dht(std::move(sock), config, std::make_shared<Logger>(l)) {}
+
+    virtual ~Dht();
+
+    /**
+     * Get the ID of the node.
+     */
+    inline const InfoHash& getNodeId() const override { return myid; }
+
+    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) 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);
+        onDisconnected();
+    }
+
+    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, SockAddr(n.ss, n.sslen));
+    }
+
+    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 {
+        reported_addr.clear();
+        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;
+    }
+
+    /**
+     * 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};
+
+    /* 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 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>>;
+    struct Kad {
+        RoutingTable buckets {};
+        SearchMap searches {};
+        unsigned pending_pings {0};
+        NodeStatus status;
+
+        NodeStatus getStatus(time_point now) const;
+        NodeStats getNodesStats(time_point now, const InfoHash& myid) const;
+    };
+
+    Kad dht4 {};
+    Kad dht6 {};
+
+    std::vector<std::pair<std::string,std::string>> bootstrap_nodes {};
+    std::chrono::steady_clock::duration bootstrap_period {std::chrono::seconds(10)};
+    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;
+    using ReportedAddr = std::pair<unsigned, SockAddr>;
+    std::vector<ReportedAddr> reported_addr;
+
+    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 storageErase(const InfoHash& id, Value::Id vid);
+    bool storageRefresh(const InfoHash& id, Value::Id vid);
+    void expireStore();
+    void expireStorage(InfoHash h);
+    void expireStore(decltype(store)::iterator);
+
+    void storageChanged(const InfoHash& id, Storage& st, ValueStorage&, 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;
+
+    // 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 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);
+
+    /**
+     * 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(Sp<Search>);
+    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);
+};
+
+}
diff --git a/include/opendht/dht_interface.h b/include/opendht/dht_interface.h
new file mode 100644 (file)
index 0000000..7a9f2cd
--- /dev/null
@@ -0,0 +1,269 @@
+/*
+ *  Copyright (C) 2014-2020 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 "log_enable.h"
+
+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;
+
+    virtual net::DatagramSocket* getSocket() const { return {}; };
+
+    /**
+     * Get the ID of the DHT node.
+     */
+    virtual const InfoHash& getNodeId() const = 0;
+
+    /**
+     * Performs final operations before quitting.
+     */
+    virtual void shutdown(ShutdownCallback cb) = 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;
+
+    /**
+     * 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;
+
+    /**
+     * Enable or disable logging of DHT internal messages
+     */
+    virtual void setLoggers(LogMethod error = {}, LogMethod warn = {}, LogMethod debug = {}) {
+        if (logger_) {
+            logger_->DBG = std::move(debug);
+            logger_->WARN = std::move(warn);
+            logger_->ERR = std::move(error);
+        } else
+            logger_= std::make_shared<Logger>(std::move(error), std::move(warn), std::move(debug));
+    }
+
+    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&) {};
+
+    /**
+     * 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_ {};
+};
+
+} // namespace dht
diff --git a/include/opendht/dht_proxy_client.h b/include/opendht/dht_proxy_client.h
new file mode 100644 (file)
index 0000000..848ea0a
--- /dev/null
@@ -0,0 +1,414 @@
+/*
+ *  Copyright (C) 2014-2020 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 ~DhtProxyClient();
+
+    /**
+     * Get the ID of the node.
+     */
+    inline const InfoHash& getNodeId() const override { return myid; }
+
+    /**
+     * 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) 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(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));
+    }
+
+    /**
+     * 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(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);
+    }
+
+    /**
+     * @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](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(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(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 {}
+    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 publicAddressV4_;
+    SockAddr publicAddressV6_;
+
+    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_;
+    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);
+    Sp<asio::steady_timer> nextProxyConfirmationTimer_;
+    Sp<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_ {};
+
+    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 = {});
+};
+
+}
diff --git a/include/opendht/dht_proxy_server.h b/include/opendht/dht_proxy_server.h
new file mode 100644 (file)
index 0000000..5dcc419
--- /dev/null
@@ -0,0 +1,454 @@
+/*
+ *  Copyright (C) 2014-2020 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
+};
+}
+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 ProxyServerConfig {
+    in_port_t port {8000};
+    std::string pushServer {};
+    std::string persistStatePath {};
+    dht::crypto::Identity identity {};
+};
+
+/**
+ * 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<dht::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
+    /**
+     * 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);
+
+    /**
+     * Send push notification with an expire timeout.
+     * @param ec
+     * @param pushToken
+     * @param json
+     * @param type
+     */
+    void handleNotifyPushListenExpire(const asio::error_code &ec, const std::string pushToken,
+                                      std::function<Json::Value()> json, PushType type);
+
+    /**
+     * 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<dht::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;
+
+        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));
+            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);
+            }
+        }
+
+        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_;
+
+#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;
+
+        template <typename Packer>
+        void msgpack_pack(Packer& p) const
+        {
+            p.pack_map(sessionCtx ? 4 : 3);
+            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);
+        }
+
+        void msgpack_unpack(const msgpack::object& o);
+    };
+    struct PushListener {
+        std::map<InfoHash, std::vector<Listener>> listeners;
+        MSGPACK_DEFINE_ARRAY(listeners)
+    };
+    std::mutex lockPushListeners_;
+    std::map<std::string, PushListener> pushListeners_;
+    proxy::ListenToken tokenPushNotif_ {0};
+#endif //OPENDHT_PUSH_NOTIFICATIONS
+};
+
+}
diff --git a/include/opendht/dhtrunner.h b/include/opendht/dhtrunner.h
new file mode 100644 (file)
index 0000000..41fbd98
--- /dev/null
@@ -0,0 +1,530 @@
+/*
+ *  Copyright (C) 2014-2020 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 "log_enable.h"
+#include "network_utils.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 {};
+        bool peer_discovery {false};
+        bool peer_publish {false};
+        std::shared_ptr<dht::crypto::Certificate> server_ca;
+        dht::crypto::Identity client_identity;
+    };
+
+    struct Context {
+        std::shared_ptr<Logger> logger {};
+        std::unique_ptr<net::DatagramSocket> sock;
+        std::shared_ptr<PeerDiscovery> peerDiscovery {};
+        StatusCallback statusChangedCallback {};
+        CertificateStoreQuery certificateStore {};
+        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, [=](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, [=](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](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, [=](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, [=](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, [=](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, [=](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);
+
+    /**
+     * 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(const 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(const 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;
+
+    /**
+     * 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 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));
+    }
+    void setLoggers(LogMethod err = {}, LogMethod warn = {}, LogMethod debug = {});
+
+    /**
+     * 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);
+    std::vector<std::string> getPublicAddressStr(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, const Config& config, Context&& context = {});
+
+    /**
+     * @param local4: Local IPv4 address and port to bind. Can be null.
+     * @param local6: Local IPv6 address and port to bind. Can be null.
+     *         You should allways bind to a global IPv6 address.
+     * @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(SockAddr& local4, SockAddr& local6, const 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, const 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 = {});
+
+    /**
+     * 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
+     * @param deviceKey non empty to enable push notifications
+     */
+    void enableProxy(bool proxify);
+
+    /* Push notification methods */
+
+    /**
+     * Updates the push notification device token
+     */
+    void setPushNotificationToken(const std::string& token);
+
+    /**
+     * 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:
+    static constexpr std::chrono::seconds BOOTSTRAP_PERIOD {10};
+
+    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);
+
+    /** Local DHT instance */
+    std::unique_ptr<SecureDht> dht_;
+
+    /** Proxy client instance */
+    std::unique_ptr<SecureDht> dht_via_proxy_;
+
+    /** true if we are currently using a proxy */
+    std::atomic_bool use_proxy {false};
+
+    /** Current configuration */
+    Config config_;
+
+    /**
+     * reset dht clients
+     */
+    void resetDht();
+    /**
+     * @return the current active DHT
+     */
+    SecureDht* activeDht() const;
+
+    /**
+     * Store current listeners and translates global tokens for each client.
+     */
+    struct Listener;
+    std::map<size_t, Listener> listeners_;
+    size_t listener_token_ {1};
+
+    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_;
+};
+
+}
diff --git a/include/opendht/http.h b/include/opendht/http.h
new file mode 100644 (file)
index 0000000..76ba5f0
--- /dev/null
@@ -0,0 +1,389 @@
+/*
+ *  Copyright (C) 2014-2020 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 {
+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<dht::Logger> l = {});
+    Connection(asio::io_context& ctx, std::shared_ptr<dht::crypto::Certificate> server_ca,
+               const dht::crypto::Identity& identity, std::shared_ptr<dht::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);
+
+    void timeout(const std::chrono::seconds timeout, HandlerCb cb = {});
+    void close();
+
+private:
+
+    template<typename T>
+    T wrapCallabck(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_;
+
+    std::unique_ptr<asio::steady_timer> timeout_timer_;
+    std::shared_ptr<dht::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<dht::Logger> logger = {});
+    Resolver(asio::io_context& ctx, const std::string& host, const std::string& service,
+             const bool ssl = false, std::shared_ptr<dht::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<dht::Logger> logger = {});
+    Resolver(asio::io_context& ctx, const std::string& url, std::vector<asio::ip::tcp::endpoint> endpoints,
+            std::shared_ptr<dht::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<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<dht::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<dht::Logger> logger = {});
+    Request(asio::io_context& ctx, const std::string& url, OnJsonCb jsoncb,
+            std::shared_ptr<dht::Logger> logger = {});
+
+    Request(asio::io_context& ctx, const std::string& url, std::shared_ptr<dht::Logger> logger = {});
+    Request(asio::io_context& ctx, const std::string& host, const std::string& service,
+            const bool ssl = false, std::shared_ptr<dht::Logger> logger = {});
+    Request(asio::io_context& ctx, const std::string& url, OnDoneCb onDone, std::shared_ptr<dht::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<dht::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();
+    };
+
+    /** 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<dht::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();
+
+    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<dht::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};
+};
+
+} // namespace http
+} // namespace dht
+
+#ifdef OPENDHT_PROXY_HTTP_PARSER_FORK
+namespace restinio
+{
+/* Custom HTTP-methods for RESTinio > 0.5.0.
+ * https://github.com/Stiffstream/restinio/issues/26
+ */
+constexpr const restinio::http_method_id_t method_listen {HTTP_LISTEN, "LISTEN"};
+constexpr const restinio::http_method_id_t method_stats {HTTP_STATS, "STATS"};
+constexpr const restinio::http_method_id_t method_sign {HTTP_SIGN, "SIGN"};
+constexpr const restinio::http_method_id_t method_encrypt {HTTP_ENCRYPT, "ENCRYPT"};
+} // namespace restinio
+#endif
diff --git a/include/opendht/indexation/pht.h b/include/opendht/indexation/pht.h
new file mode 100644 (file)
index 0000000..6113a17
--- /dev/null
@@ -0,0 +1,533 @@
+/*
+ *  Copyright (C) 2014-2020 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, [=](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 */
+
diff --git a/include/opendht/infohash.h b/include/opendht/infohash.h
new file mode 100644 (file)
index 0000000..cf774cd
--- /dev/null
@@ -0,0 +1,416 @@
+/*
+ *  Copyright (C) 2014-2020 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>
+
+#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 <algorithm>
+#include <stdexcept>
+#include <sstream>
+#include <cstring>
+
+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 InfoHash.
+ * An InfoHash is a byte array of HASH_LEN bytes.
+ * InfoHashes 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(const std::string& hex);
+
+    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(); }
+
+    bool operator==(const Hash& h) const {
+        auto a = reinterpret_cast<const uint32_t*>(data_.data());
+        auto b = reinterpret_cast<const uint32_t*>(h.data_.data());
+        constexpr unsigned n = N / sizeof(uint32_t);
+        for (unsigned i=0; i < n; i++)
+            if (a[i] != b[i])
+                return false;
+        return true;
+    }
+    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;
+    }
+
+    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;
+    }
+
+    /**
+     * Forget about the ``XOR-metric''.  An id is just a path from the
+     * root of the tree, so bits are numbered from the start.
+     */
+    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++) {
+            uint8_t xor1, xor2;
+            if(id1.data_[i] == id2.data_[i])
+                continue;
+            xor1 = id1.data_[i] ^ data_[i];
+            xor2 = id2.data_[i] ^ data_[i];
+            if(xor1 < xor2)
+                return -1;
+            else
+                return 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(const std::string& 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());
+    }
+
+    /**
+     * 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);
+
+    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>
+Hash<N>::Hash(const std::string& hex) {
+    if (hex.size() < 2*N)
+        data_.fill(0);
+    else
+        fromString(hex.c_str());
+}
+
+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 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
+{
+    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);
+}
+
+const InfoHash zeroes {};
+
+struct OPENDHT_PUBLIC NodeExport {
+    InfoHash id;
+    sockaddr_storage ss;
+    socklen_t sslen;
+
+    template <typename Packer>
+    void msgpack_pack(Packer& pk) const
+    {
+        pk.pack_map(2);
+        pk.pack(std::string("id"));
+        pk.pack(id);
+        pk.pack(std::string("addr"));
+        pk.pack_bin(sslen);
+        pk.pack_bin_body((char*)&ss, sslen);
+    }
+
+    void msgpack_unpack(msgpack::object o);
+
+    OPENDHT_PUBLIC friend std::ostream& operator<< (std::ostream& s, const NodeExport& h);
+};
+
+}
diff --git a/include/opendht/log.h b/include/opendht/log.h
new file mode 100644 (file)
index 0000000..f5af1bc
--- /dev/null
@@ -0,0 +1,95 @@
+/*
+ *  Copyright (C) 2014-2020 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 "log_enable.h"
+
+#include <iostream>
+#include <memory>
+
+namespace dht {
+
+class DhtRunner;
+
+/**
+ * Logging-related functions
+ */
+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).
+ */
+OPENDHT_PUBLIC void
+printLog(std::ostream &s, char const *m, va_list args);
+
+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  */
diff --git a/include/opendht/log_enable.h b/include/opendht/log_enable.h
new file mode 100644 (file)
index 0000000..3811e14
--- /dev/null
@@ -0,0 +1,191 @@
+/*
+ *  Copyright (C) 2014-2020 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 HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "infohash.h"
+
+#include <cstdarg>
+
+#ifndef OPENDHT_LOG
+#define OPENDHT_LOG true
+#endif
+
+namespace dht {
+
+// Logging related utility functions
+
+/**
+ * Wrapper for logging methods
+ */
+struct LogMethod {
+    LogMethod() = default;
+
+    LogMethod(LogMethod&& l) : func(std::move(l.func)) {}
+    LogMethod(const LogMethod& l) : func(l.func) {}
+
+    LogMethod& operator=(dht::LogMethod&& l) {
+        func = std::forward<LogMethod>(l.func);
+        return *this;
+    }
+    LogMethod& operator=(const dht::LogMethod& l) {
+        func = l.func;
+        return *this;
+    }
+
+    template<typename T>
+    explicit LogMethod(T&& t) : func(std::forward<T>(t)) {}
+
+    template<typename T>
+    LogMethod(const T& t) : func(t) {}
+
+    void operator()(char const* format, ...) const {
+        va_list args;
+        va_start(args, format);
+        func(format, args);
+        va_end(args);
+    }
+    inline void log(char const* format, va_list args) const {
+        func(format, args);
+    }
+    explicit operator bool() const {
+        return (bool)func;
+    }
+
+    void logPrintable(const uint8_t *buf, size_t buflen) const {
+        std::string buf_clean(buflen, '\0');
+        for (size_t i=0; i<buflen; i++)
+            buf_clean[i] = isprint(buf[i]) ? buf[i] : '.';
+        (*this)("%s", buf_clean.c_str());
+    }
+private:
+    std::function<void(char const*, va_list)> func;
+};
+
+struct Logger {
+    LogMethod ERR = {};
+    LogMethod WARN = {};
+    LogMethod DBG = {};
+
+    Logger() = default;
+    Logger(LogMethod&& err, LogMethod&& warn, LogMethod&& dbg)
+        : ERR(std::move(err)), WARN(std::move(warn)), DBG(std::move(dbg)) {}
+    void setFilter(const InfoHash& f) {
+        filter_ = f;
+        filterEnable_ = static_cast<bool>(filter_);
+    }
+    inline void log0(const LogMethod& logger, char const* format, va_list args) const {
+#if OPENDHT_LOG
+        if (logger and not filterEnable_)
+            logger.log(format, args);
+#endif
+    }
+    inline void log1(const LogMethod& logger, const InfoHash& f, char const* format, va_list args) const {
+#if OPENDHT_LOG
+        if (logger and (not filterEnable_ or f == filter_))
+            logger.log(format, args);
+#endif
+    }
+    inline void log2(const LogMethod& logger, const InfoHash& f1, const InfoHash& f2, char const* format, va_list args) const {
+#if OPENDHT_LOG
+        if (logger and (not filterEnable_ or f1 == filter_ or f2 == filter_))
+            logger.log(format, args);
+#endif
+    }
+    inline void d(char const* format, ...) const {
+#if OPENDHT_LOG
+        va_list args;
+        va_start(args, format);
+        log0(DBG, format, args);
+        va_end(args);
+#endif
+    }
+    inline void d(const InfoHash& f, char const* format, ...) const {
+#if OPENDHT_LOG
+        va_list args;
+        va_start(args, format);
+        log1(DBG, f, format, args);
+        va_end(args);
+#endif
+    }
+    inline void d(const InfoHash& f1, const InfoHash& f2, char const* format, ...) const {
+#if OPENDHT_LOG
+        va_list args;
+        va_start(args, format);
+        log2(DBG, f1, f2, format, args);
+        va_end(args);
+#endif
+    }
+    inline void w(char const* format, ...) const {
+#if OPENDHT_LOG
+        va_list args;
+        va_start(args, format);
+        log0(WARN, format, args);
+        va_end(args);
+#endif
+    }
+    inline void w(const InfoHash& f, char const* format, ...) const {
+#if OPENDHT_LOG
+        va_list args;
+        va_start(args, format);
+        log1(WARN, f, format, args);
+        va_end(args);
+#endif
+    }
+    inline void w(const InfoHash& f1, const InfoHash& f2, char const* format, ...) const {
+#if OPENDHT_LOG
+        va_list args;
+        va_start(args, format);
+        log2(WARN, f1, f2, format, args);
+        va_end(args);
+#endif
+    }
+    inline void e(char const* format, ...) const {
+#if OPENDHT_LOG
+        va_list args;
+        va_start(args, format);
+        log0(ERR, format, args);
+        va_end(args);
+#endif
+    }
+    inline void e(const InfoHash& f, char const* format, ...) const {
+#if OPENDHT_LOG
+        va_list args;
+        va_start(args, format);
+        log1(ERR, f, format, args);
+        va_end(args);
+#endif
+    }
+    inline void e(const InfoHash& f1, const InfoHash& f2, char const* format, ...) const {
+#if OPENDHT_LOG
+        va_list args;
+        va_start(args, format);
+        log2(ERR, f1, f2, format, args);
+        va_end(args);
+#endif
+    }
+private:
+    bool filterEnable_ {false};
+    InfoHash filter_ {};
+};
+
+}
diff --git a/include/opendht/network_engine.h b/include/opendht/network_engine.h
new file mode 100644 (file)
index 0000000..3523125
--- /dev/null
@@ -0,0 +1,594 @@
+/*
+ *  Copyright (C) 2014-2020 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 "log_enable.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(const Sp<Logger>& log, std::mt19937_64& rd, Scheduler& scheduler, std::unique_ptr<DatagramSocket>&& sock);
+    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(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(Sp<Node> n, Tid socket_id, const InfoHash& hash, const Blob& ntoken, const std::vector<Value::Id>& values, int version);
+    void tellListenerExpired(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(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>(zeroes, 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(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(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(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(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(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.
+     */
+    Sp<Request> sendUpdateValues(Sp<Node> n,
+                                 const InfoHash& infohash,
+                                 const std::vector<Sp<Value>>& values,
+                                 time_point created,
+                                 const Blob& token,
+                                 const 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 const std::string my_v;
+
+    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&, const std::vector<Sp<Value>>&);
+    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 */
diff --git a/include/opendht/network_utils.h b/include/opendht/network_utils.h
new file mode 100644 (file)
index 0000000..4559ea3
--- /dev/null
@@ -0,0 +1,161 @@
+/*
+ *  Copyright (C) 2014-2020 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 "log_enable.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 * 16;
+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);
+};
+
+}
+}
diff --git a/include/opendht/node.h b/include/opendht/node.h
new file mode 100644 (file)
index 0000000..47c443b
--- /dev/null
@@ -0,0 +1,174 @@
+/*
+ *  Copyright (C) 2014-2020 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 <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) {}
+
+    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.sslen = addr.getLength();
+        std::memcpy(&ne.ss, addr.get(), ne.sslen);
+        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_;
+};
+
+}
diff --git a/include/opendht/node_cache.h b/include/opendht/node_cache.h
new file mode 100644 (file)
index 0000000..19a8d3b
--- /dev/null
@@ -0,0 +1,71 @@
+/*
+ *  Copyright (C) 2014-2020 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;
+};
+
+}
diff --git a/include/opendht/peer_discovery.h b/include/opendht/peer_discovery.h
new file mode 100644 (file)
index 0000000..2eedbb2
--- /dev/null
@@ -0,0 +1,95 @@
+/*
+ *  Copyright (C) 2014-2020 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 "log_enable.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_;
+};
+
+}
diff --git a/include/opendht/proxy.h b/include/opendht/proxy.h
new file mode 100644 (file)
index 0000000..2cc292a
--- /dev/null
@@ -0,0 +1,31 @@
+/*
+ *  Copyright (C) 2014-2020 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;
+
+}
+}
diff --git a/include/opendht/rate_limiter.h b/include/opendht/rate_limiter.h
new file mode 100644 (file)
index 0000000..ec71d64
--- /dev/null
@@ -0,0 +1,56 @@
+/*
+ *  Copyright (C) 2014-2020 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 {};
+};
+
+}
diff --git a/include/opendht/rng.h b/include/opendht/rng.h
new file mode 100644 (file)
index 0000000..c1419b7
--- /dev/null
@@ -0,0 +1,130 @@
+/*
+ *  Copyright (C) 2014-2020 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
diff --git a/include/opendht/routing_table.h b/include/opendht/routing_table.h
new file mode 100644 (file)
index 0000000..234488d
--- /dev/null
@@ -0,0 +1,102 @@
+/*
+ *  Copyright (C) 2014-2020 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);
+};
+
+}
diff --git a/include/opendht/scheduler.h b/include/opendht/scheduler.h
new file mode 100644 (file)
index 0000000..01cee92
--- /dev/null
@@ -0,0 +1,123 @@
+/*
+ *  Copyright (C) 2014-2020 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) : do_(std::move(f)) {}
+        std::function<void()> do_;
+        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));
+        if (t != time_point::max())
+            timers.emplace(std::move(t), job);
+        return job;
+    }
+
+    void add(const Sp<Scheduler::Job>& job, time_point t) {
+        if (t != time_point::max())
+            timers.emplace(std::move(t), 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_);
+        job->do_ = {};
+        job = add(t, std::move(task));
+    }
+
+    /**
+     * 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 */
+};
+
+}
diff --git a/include/opendht/securedht.h b/include/opendht/securedht.h
new file mode 100644 (file)
index 0000000..9f6ff4a
--- /dev/null
@@ -0,0 +1,384 @@
+/*
+ *  Copyright (C) 2014-2020 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;
+    }
+
+    SecureDht() {}
+
+    /**
+     * 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);
+
+    virtual ~SecureDht();
+
+    InfoHash getId() const {
+        return key_ ? key_->getPublicKey().getId() : InfoHash();
+    }
+    PkId getLongId() const {
+        return key_ ? key_->getPublicKey().getLongId() : PkId();
+    }
+
+    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);
+    }
+
+    /**
+     * 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<const crypto::PublicKey>)>& cb);
+
+    const Sp<crypto::Certificate> registerCertificate(const InfoHash& node, const Blob& cert);
+    void registerCertificate(Sp<crypto::Certificate>& cert);
+
+    const Sp<crypto::Certificate> getCertificate(const InfoHash& node) const;
+    const Sp<const 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);
+    }
+
+    /**
+     * SecureDht to Dht proxy
+     */
+    void shutdown(ShutdownCallback cb) override {
+        dht_->shutdown(cb);
+    }
+    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);
+    }
+    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<const 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;
+        } 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;
+    }
+};
+
+}
diff --git a/include/opendht/sockaddr.h b/include/opendht/sockaddr.h
new file mode 100644 (file)
index 0000000..790204d
--- /dev/null
@@ -0,0 +1,302 @@
+/*
+ *  Copyright (C) 2014-2020 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"
+
+#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 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 > 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 > sizeof(sa_family_t))
+            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(); }
+
+    const sockaddr_in& getIPv4() const {
+        return *reinterpret_cast<const sockaddr_in*>(get());
+    }
+    const sockaddr_in6& getIPv6() const {
+        return *reinterpret_cast<const sockaddr_in6*>(get());
+    }
+    sockaddr_in& getIPv4() {
+        return *reinterpret_cast<sockaddr_in*>(get());
+    }
+    sockaddr_in6& getIPv6() {
+        return *reinterpret_cast<sockaddr_in6*>(get());
+    }
+
+    /**
+     * 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;
+        }
+    };
+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);
+
+}
diff --git a/include/opendht/thread_pool.h b/include/opendht/thread_pool.h
new file mode 100644 (file)
index 0000000..d7a2995
--- /dev/null
@@ -0,0 +1,96 @@
+/*
+ *  Copyright (C) 2014-2020 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>
+
+namespace dht {
+
+class OPENDHT_PUBLIC ThreadPool {
+public:
+    static ThreadPool& computation();
+    static ThreadPool& io();
+
+    ThreadPool();
+    ThreadPool(size_t maxThreads);
+    ~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();
+    void join();
+
+private:
+    struct ThreadState;
+    std::queue<std::function<void()>> tasks_ {};
+    std::vector<std::unique_ptr<ThreadState>> threads_;
+    unsigned readyThreads_ {0};
+    std::mutex lock_ {};
+    std::condition_variable cv_ {};
+
+    const unsigned maxThreads_;
+    bool running_ {true};
+};
+
+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();
+};
+
+}
diff --git a/include/opendht/utils.h b/include/opendht/utils.h
new file mode 100644 (file)
index 0000000..93b6e8d
--- /dev/null
@@ -0,0 +1,185 @@
+/*
+ *  Copyright (C) 2014-2020 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, const std::string& key) {
+    return findMapValue(map, key.c_str(), key.size());
+}
+
+} // namespace dht
diff --git a/include/opendht/value.h b/include/opendht/value.h
new file mode 100644 (file)
index 0000000..198d5f6
--- /dev/null
@@ -0,0 +1,1063 @@
+/*
+ *  Copyright (C) 2014-2020 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 <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 {
+
+static const std::string 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 {60 * 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) {}
+
+        Filter chain(Filter&& f2) {
+            auto f1 = *this;
+            return chain(std::move(f1), std::move(f2));
+        }
+        Filter chainOr(Filter&& f2) {
+            auto f1 = *this;
+            return chainOr(std::move(f1), std::move(f2));
+        }
+        static 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 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 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 Filter chain(std::initializer_list<Filter> l) {
+            return chainAll(std::vector<Filter>(l.begin(), l.end()));
+        }
+        static Filter chainOr(Filter&& f1, Filter&& f2) {
+            if (not f1 or not f2) return {};
+            return [f1,f2](const Value& v) {
+                return f1(v) or f2(v);
+            };
+        }
+        static Filter notFilter(Filter&& f) {
+            if (not f) return [](const Value&) { return false; };
+            return [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 const Filter AllFilter() {
+        return {};
+    }
+
+    static Filter TypeFilter(const ValueType& t) {
+        const auto tid = t.id;
+        return [tid](const Value& v) {
+            return v.type == tid;
+        };
+    }
+    static Filter TypeFilter(const ValueType::Id& tid) {
+        return [tid](const Value& v) {
+            return v.type == tid;
+        };
+    }
+
+    static Filter IdFilter(const Id id) {
+        return [id](const Value& v) {
+            return v.id == id;
+        };
+    }
+
+    static Filter RecipientFilter(const InfoHash& r) {
+        return [r](const Value& v) {
+            return v.recipient == r;
+        };
+    }
+
+    static Filter OwnerFilter(const crypto::PublicKey& pk) {
+        return OwnerFilter(pk.getId());
+    }
+
+    static Filter OwnerFilter(const InfoHash& pkh) {
+        return [pkh](const Value& v) {
+            return v.owner and v.owner->getId() == pkh;
+        };
+    }
+
+    static Filter SeqNumFilter(uint16_t seq_no) {
+        return [seq_no](const Value& v) {
+            return v.seq == seq_no;
+        };
+    }
+
+    static Filter UserTypeFilter(const std::string& ut) {
+        return [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);
+    }
+
+    bool isEncrypted() const {
+        return not cypher.empty();
+    }
+    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) {
+        if (isEncrypted())
+            throw DhtException("Can't sign encrypted data.");
+        owner = std::make_shared<const crypto::PublicKey>(key.getPublicKey());
+        signature = key.sign(getToSign());
+    }
+
+    /**
+     * Check that the value is signed and that the signature matches.
+     * If true, the owner field will contain the signer public key.
+     */
+    bool checkSignature() const {
+        return isSigned() and owner->checkSignature(getToSign(), signature);
+    }
+
+    std::shared_ptr<const crypto::PublicKey> getOwner() const {
+        return std::static_pointer_cast<const crypto::PublicKey>(owner);
+    }
+
+    /**
+     * Sign the value with from and returns the encrypted version for to.
+     */
+    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;
+    }
+
+    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) {
+        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) {
+        return id == o.id and contentEquals(o);
+    }
+
+    void setRecipient(const InfoHash& r) {
+        recipient = r;
+    }
+
+    void setCypher(Blob&& c) {
+        cypher = std::move(c);
+    }
+
+    /**
+     * Pack part of the data to be signed (must always be done the same way)
+     */
+    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
+     */
+    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);
+
+    std::string toString() const {
+        std::stringstream 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<const 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};
+
+private:
+    friend class SecureDht;
+    /* 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(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(std::string("f")); p.pack(static_cast<uint8_t>(field));
+
+        p.pack(std::string("v"));
+        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"))
+            field = (Value::Field)f->as<unsigned>();
+        else
+            throw msgpack::type_error();
+
+        auto v = findMapValue(msg, "v");
+        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(const std::string& 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::stringstream 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(const std::string& 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 *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 *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 *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 *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 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 *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 {};
+        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::stringstream 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 q_str) {
+        auto pos_W = q_str.find("WHERE");
+        auto pos_w = q_str.find("where");
+        auto pos = std::min(pos_W != std::string::npos ? pos_W : q_str.size(),
+                            pos_w != std::string::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::stringstream 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)
diff --git a/m4/ax_cxx_compile_stdcxx.m4 b/m4/ax_cxx_compile_stdcxx.m4
new file mode 100644 (file)
index 0000000..43087b2
--- /dev/null
@@ -0,0 +1,951 @@
+# ===========================================================================
+#  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' (for the C++11 standard)
+#   or '14' (for the C++14 standard).
+#
+#   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 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>
+#
+#   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 11
+
+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"],
+        [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], [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
+)
+
+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  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
+
+]])
diff --git a/opendht-c.pc.in b/opendht-c.pc.in
new file mode 100644 (file)
index 0000000..18f7198
--- /dev/null
@@ -0,0 +1,10 @@
+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}
diff --git a/opendht.pc.in b/opendht.pc.in
new file mode 100644 (file)
index 0000000..9491c4e
--- /dev/null
@@ -0,0 +1,11 @@
+prefix=@prefix@
+exec_prefix=@exec_prefix@
+libdir=@libdir@
+includedir=@includedir@
+Name: OpenDHT
+Description: C++14 Distributed Hash Table library
+Version: @VERSION@
+Libs: -L${libdir} -lopendht
+Libs.private: @http_parser_lib@ -pthread
+Requires.private: gnutls >= 3.3, nettle >= 2.4@argon2_lib@@jsoncpp_lib@@openssl_lib@
+Cflags: -I${includedir}
diff --git a/python/CMakeLists.txt b/python/CMakeLists.txt
new file mode 100644 (file)
index 0000000..2ce4bdc
--- /dev/null
@@ -0,0 +1,14 @@
+
+set(CURRENT_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR})
+set(CURRENT_BINARY_DIR ${CMAKE_CURRENT_BINARY_DIR})
+
+configure_file(setup.py.in setup.py)
+
+add_custom_target(python ALL
+    COMMAND python3 setup.py build
+    DEPENDS opendht opendht_cpp.pxd opendht.pyx)
+
+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
diff --git a/python/Makefile.am b/python/Makefile.am
new file mode 100644 (file)
index 0000000..7f25662
--- /dev/null
@@ -0,0 +1,29 @@
+if USE_CYTHON
+
+noinst_HEADERS = \
+       opendht.pyx \
+       opendht_cpp.pxd
+
+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
+
diff --git a/python/opendht.pyx b/python/opendht.pyx
new file mode 100644 (file)
index 0000000..c888c0f
--- /dev/null
@@ -0,0 +1,681 @@
+# distutils: language = c++
+# distutils: extra_compile_args = -std=c++14
+# distutils: include_dirs = ../../include
+# distutils: library_dirs = ../../src
+# distutils: libraries = opendht gnutls
+# cython: language_level=3
+#
+# Copyright (c) 2015-2019 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
+
+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) 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) 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) 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) 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) 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) 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''):
+        self._value.reset(new cpp.Value(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 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().getPublicKey().getId()
+        return h
+    def getPublicKey(self):
+        pk = PublicKey()
+        pk._key = self._key.get().getPublicKey()
+        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.PublicKey _key
+    def getId(self):
+        h = InfoHash()
+        h._infohash = self._key.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.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().getPublicKey()
+            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 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)
+        )
diff --git a/python/opendht_cpp.pxd b/python/opendht_cpp.pxd
new file mode 100644 (file)
index 0000000..1bc866a
--- /dev/null
@@ -0,0 +1,272 @@
+# Copyright (c) 2015-2019 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 "<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()
+        PublicKey getPublicKey() 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 ValueType
+    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 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 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)
+        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)
diff --git a/python/setup.py.in b/python/setup.py.in
new file mode 100644 (file)
index 0000000..e72f80b
--- /dev/null
@@ -0,0 +1,58 @@
+# Copyright (C) 2014-2020 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 :: 3 - Alpha',
+          '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.2',
+          'Programming Language :: Python :: 3.3',
+          'Programming Language :: Python :: 3.4',
+          'Programming Language :: Python :: 3.5',
+      ],
+      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++14"],
+          extra_link_args=["-std=c++14"],
+          libraries=["opendht"],
+          library_dirs = ['@CURRENT_BINARY_DIR@', '@PROJECT_BINARY_DIR@']
+      ))
+)
diff --git a/python/tests/opendht_tests.py b/python/tests/opendht_tests.py
new file mode 100644 (file)
index 0000000..7f49d04
--- /dev/null
@@ -0,0 +1,53 @@
+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()
diff --git a/python/tools/README.md b/python/tools/README.md
new file mode 100644 (file)
index 0000000..ffc6b6e
--- /dev/null
@@ -0,0 +1,45 @@
+# 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.3.14
+- 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
diff --git a/python/tools/benchmark.py b/python/tools/benchmark.py
new file mode 100755 (executable)
index 0000000..b90b4d2
--- /dev/null
@@ -0,0 +1,240 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+# Copyright (C) 2014-2020 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.')
diff --git a/python/tools/dht/__init__.py b/python/tools/dht/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/python/tools/dht/network.py b/python/tools/dht/network.py
new file mode 100755 (executable)
index 0000000..6a84d83
--- /dev/null
@@ -0,0 +1,594 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015-2019 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.netns.process.proxy 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)
diff --git a/python/tools/dht/tests.py b/python/tools/dht/tests.py
new file mode 100644 (file)
index 0000000..18daa03
--- /dev/null
@@ -0,0 +1,995 @@
+# -*- coding: utf-8 -*-
+# Copyright (C) 2015-2019 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
+                )
diff --git a/python/tools/dht/virtual_network_builder.py b/python/tools/dht/virtual_network_builder.py
new file mode 100755 (executable)
index 0000000..1757770
--- /dev/null
@@ -0,0 +1,121 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015-2019 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, subprocess
+
+from pyroute2 import IPDB, NetNS
+from pyroute2.netns.process.proxy import NSPopen
+
+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, 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")
+
+    args = parser.parse_args()
+
+    local_addr4 = '10.0.42.'
+    local_addr6 = '2001:db9::'
+    brige_name = 'br'+args.ifname
+
+    ip = None
+    try:
+        ip = IPDB()
+        if args.remove:
+            # cleanup interfaces
+            for ifn in range(args.ifnum):
+                iface = args.ifname+str(ifn)
+                if iface in ip.interfaces:
+                    with ip.interfaces[iface] as i:
+                        i.remove()
+            if 'tap'+args.ifname in ip.interfaces:
+                with ip.interfaces['tap'+args.ifname] as i:
+                    i.remove()
+            if brige_name in ip.interfaces:
+                with ip.interfaces[brige_name] as i:
+                    i.remove()
+            for ifn in range(args.ifnum):
+                netns = NetNS('node'+str(ifn))
+                netns.close()
+                netns.remove()
+        else:
+            for ifn in range(args.ifnum):
+                iface = args.ifname+str(ifn)
+                if not iface in ip.interfaces:
+                    ip.create(kind='veth', ifname=iface, peer=iface+'.1').commit()
+
+            ip.create(kind='tuntap', ifname='tap'+args.ifname, mode='tap').commit()
+
+            with ip.create(kind='bridge', ifname=brige_name) as i:
+                for ifn in range(args.ifnum):
+                    iface = args.ifname+str(ifn)
+                    i.add_port(ip.interfaces[iface])
+                i.add_port(ip.interfaces['tap'+args.ifname])
+                if args.ipv4:
+                    i.add_ip(local_addr4+'1/24')
+                if args.ipv6:
+                    i.add_ip(local_addr6+'1/64')
+                i.up()
+
+            with ip.interfaces['tap'+args.ifname] as tap:
+                tap.up()
+
+            for ifn in range(args.ifnum):
+                iface = args.ifname+str(ifn)
+
+                nns = NetNS('node'+str(ifn))
+                iface1 = iface+'.1'
+                with ip.interfaces[iface1] as i:
+                    i.net_ns_fd = nns.netns
+
+                with ip.interfaces[iface] as i:
+                    i.up()
+
+                ip_ns = IPDB(nl=nns)
+                try:
+                    with ip_ns.interfaces.lo as lo:
+                        lo.up()
+                    with ip_ns.interfaces[iface1] as i:
+                        if args.ipv4:
+                            i.add_ip(local_addr4+str(ifn+8)+'/24')
+                        if args.ipv6:
+                            i.add_ip(local_addr6+str(ifn+8)+'/64')
+                        i.up()
+                finally:
+                    ip_ns.release()
+
+                nsp = NSPopen(nns.netns, ["tc", "qdisc", "add", "dev", iface1, "root", "netem", "delay", str(args.delay)+"ms", str(int(args.delay/2))+"ms", "loss", str(args.loss)+"%", "25%"], stdout=subprocess.PIPE)
+                #print(nsp.communicate()[0].decode())
+                nsp.communicate()
+                nsp.wait()
+                nsp.release()
+
+            if args.ipv4:
+                subprocess.call(["sysctl", "-w", "net.ipv4.conf."+brige_name+".forwarding=1"])
+            if args.ipv6:
+                subprocess.call(["sysctl", "-w", "net.ipv6.conf."+brige_name+".forwarding=1"])
+
+    except Exception as e:
+          print('Error',e)
+    finally:
+        if ip:
+            ip.release()
diff --git a/python/tools/dhtcluster.py b/python/tools/dhtcluster.py
new file mode 100755 (executable)
index 0000000..cc3121a
--- /dev/null
@@ -0,0 +1,270 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+# Copyright (C) 2014-2020 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)
diff --git a/python/tools/http_server.py b/python/tools/http_server.py
new file mode 100755 (executable)
index 0000000..d2da4c0
--- /dev/null
@@ -0,0 +1,78 @@
+#!/usr/bin/env python3
+# Copyright (c) 2016-2019 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.ring.cx:4222")
+    args = parser.parse_args()
+    endpoints.serverFromString(reactor, "tcp:"+str(args.http_port)).listen(server.Site(DhtServer(args.port, args.bootstrap)))
+    reactor.run()
diff --git a/python/tools/network_monitor.py b/python/tools/network_monitor.py
new file mode 100755 (executable)
index 0000000..074bee2
--- /dev/null
@@ -0,0 +1,83 @@
+#!/usr/bin/env python3
+# Copyright (c) 2015-2019 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.ring.cx')
+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)
diff --git a/python/tools/pingpong.py b/python/tools/pingpong.py
new file mode 100755 (executable)
index 0000000..14c9032
--- /dev/null
@@ -0,0 +1,79 @@
+#!/usr/bin/env python3
+# Copyright (c) 2015-2019 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.ring.cx", "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")
diff --git a/python/tools/scanner.py b/python/tools/scanner.py
new file mode 100755 (executable)
index 0000000..17d9e7c
--- /dev/null
@@ -0,0 +1,301 @@
+#!/usr/bin/env python3
+# Copyright (c) 2015-2019 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()
diff --git a/resources/opendht_logo.svg b/resources/opendht_logo.svg
new file mode 100644 (file)
index 0000000..ae96801
--- /dev/null
@@ -0,0 +1,105 @@
+<?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>
diff --git a/resources/opendht_logo_512.png b/resources/opendht_logo_512.png
new file mode 100644 (file)
index 0000000..b47dc97
Binary files /dev/null and b/resources/opendht_logo_512.png differ
diff --git a/rust/.gitignore b/rust/.gitignore
new file mode 100644 (file)
index 0000000..3e2f2a8
--- /dev/null
@@ -0,0 +1,2 @@
+*.lock
+target
diff --git a/rust/Cargo.toml b/rust/Cargo.toml
new file mode 100644 (file)
index 0000000..666c8c1
--- /dev/null
@@ -0,0 +1,11 @@
+[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.1.0"
\ No newline at end of file
diff --git a/rust/README.md b/rust/README.md
new file mode 100644 (file)
index 0000000..31eb0df
--- /dev/null
@@ -0,0 +1,21 @@
+# 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
diff --git a/rust/examples/dhtnode.rs b/rust/examples/dhtnode.rs
new file mode 100644 (file)
index 0000000..c491b87
--- /dev/null
@@ -0,0 +1,73 @@
+/*
+ *  Copyright (C) 2014-2020 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
diff --git a/rust/src/blob.rs b/rust/src/blob.rs
new file mode 100644 (file)
index 0000000..ff2743b
--- /dev/null
@@ -0,0 +1,37 @@
+/*
+ *  Copyright (C) 2014-2020 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
diff --git a/rust/src/crypto.rs b/rust/src/crypto.rs
new file mode 100644 (file)
index 0000000..afb6751
--- /dev/null
@@ -0,0 +1,194 @@
+/*
+ *  Copyright (C) 2014-2020 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
diff --git a/rust/src/dhtrunner.rs b/rust/src/dhtrunner.rs
new file mode 100644 (file)
index 0000000..12d31fe
--- /dev/null
@@ -0,0 +1,315 @@
+/*
+ *  Copyright (C) 2014-2020 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(),
+            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_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
diff --git a/rust/src/ffi.rs b/rust/src/ffi.rs
new file mode 100644 (file)
index 0000000..c2c36f2
--- /dev/null
@@ -0,0 +1,222 @@
+/*
+ *  Copyright (C) 2014-2020 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 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
diff --git a/rust/src/infohash.rs b/rust/src/infohash.rs
new file mode 100644 (file)
index 0000000..059128f
--- /dev/null
@@ -0,0 +1,83 @@
+/*
+ *  Copyright (C) 2014-2020 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
diff --git a/rust/src/lib.rs b/rust/src/lib.rs
new file mode 100644 (file)
index 0000000..b9040de
--- /dev/null
@@ -0,0 +1,34 @@
+/*
+ *  Copyright (C) 2014-2020 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 };
diff --git a/rust/src/pkid.rs b/rust/src/pkid.rs
new file mode 100644 (file)
index 0000000..1ce604e
--- /dev/null
@@ -0,0 +1,34 @@
+/*
+ *  Copyright (C) 2014-2020 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
diff --git a/rust/src/value.rs b/rust/src/value.rs
new file mode 100644 (file)
index 0000000..9276a8e
--- /dev/null
@@ -0,0 +1,107 @@
+/*
+ *  Copyright (C) 2014-2020 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
diff --git a/src/Makefile.am b/src/Makefile.am
new file mode 100644 (file)
index 0000000..b41ec42
--- /dev/null
@@ -0,0 +1,116 @@
+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@ @Argon2_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 \
+        infohash.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/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/log_enable.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
+
+clean-local:
+       rm -rf libargon2.la
+
+######################
+#  ARGON2 submodule  #
+######################
+
+if WITH_INCLUDED_ARGON2
+noinst_LTLIBRARIES         = libargon2.la
+libopendht_la_DEPENDENCIES = libargon2.la
+
+libargon2_la_CFLAGS  = -std=c89 -fPIC -pthread -O3 -Wall -I@top_builddir@/argon2/include -I@top_builddir@/argon2/src
+libargon2_la_SOURCES = \
+        @top_builddir@/argon2/src/argon2.c \
+        @top_builddir@/argon2/src/core.c \
+        @top_builddir@/argon2/src/blake2/blake2b.c \
+        @top_builddir@/argon2/src/thread.c \
+        @top_builddir@/argon2/src/ref.c \
+        @top_builddir@/argon2/src/encoding.c
+
+noinst_HEADERS = \
+        @top_builddir@/argon2/include/argon2.h \
+        @top_builddir@/argon2/src/blake2/blake2.h \
+        @top_builddir@/argon2/src/blake2/blake2-impl.h \
+        @top_builddir@/argon2/src/blake2/blamka-round-ref.h \
+        @top_builddir@/argon2/src/core.h \
+        @top_builddir@/argon2/src/encoding.h \
+        @top_builddir@/argon2/src/thread.h
+endif
diff --git a/src/base64.cpp b/src/base64.cpp
new file mode 100644 (file)
index 0000000..d2406d7
--- /dev/null
@@ -0,0 +1,143 @@
+/*
+ *  Copyright (C) 2014-2020 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;
+}
diff --git a/src/base64.h b/src/base64.h
new file mode 100644 (file)
index 0000000..ecd7fd3
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ *  Copyright (C) 2014-2020 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);
diff --git a/src/callbacks.cpp b/src/callbacks.cpp
new file mode 100644 (file)
index 0000000..c1e3e14
--- /dev/null
@@ -0,0 +1,164 @@
+/*
+ *  Copyright (C) 2014-2020 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(const 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(const GetCallbackSimple& cb)
+{
+    if (not cb) return {};
+    return [=](const std::vector<std::shared_ptr<Value>>& values) {
+        for (const auto& v : values)
+            if (not cb(v))
+                return false;
+        return true;
+    };
+}
+
+ValueCallback
+bindValueCb(const 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(const 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(donecb, _1);
+}
+
+DoneCallback
+bindDoneCb(const 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(const 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::stringstream 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
+
+
+}
diff --git a/src/compat/msvc/unistd.h b/src/compat/msvc/unistd.h
new file mode 100644 (file)
index 0000000..c164e76
--- /dev/null
@@ -0,0 +1,53 @@
+/*
+ *  Copyright (C) 2014-2020 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
diff --git a/src/compat/msvc/wingetopt.c b/src/compat/msvc/wingetopt.c
new file mode 100644 (file)
index 0000000..79ba5fe
--- /dev/null
@@ -0,0 +1,971 @@
+/* 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
diff --git a/src/compat/msvc/wingetopt.h b/src/compat/msvc/wingetopt.h
new file mode 100644 (file)
index 0000000..bf1864a
--- /dev/null
@@ -0,0 +1,97 @@
+#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
diff --git a/src/compat/os_cert.cpp b/src/compat/os_cert.cpp
new file mode 100644 (file)
index 0000000..9dd9611
--- /dev/null
@@ -0,0 +1,208 @@
+/*\r
+ *  Copyright (C) 2020 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 "log_enable.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(&ltm, 0, sizeof(ltm));\r
+        lt = &ltm;\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->w("adding %d decoded certs to X509 store", pems_.size());\r
+    X509_STORE* store = SSL_CTX_get_cert_store(ctx);\r
+    if (store == nullptr) {\r
+        if (logger)\r
+            logger->e("couldn't get the context cert store");\r
+        return;\r
+    }\r
+    for (const auto& pem : pems_) {\r
+        if (X509_STORE_add_cert(store, pem.get()) == 1)\r
+            continue;\r
+        if (logger)\r
+            logger->d("couldn't add local certificate");\r
+    }\r
+}\r
+\r
+} /*namespace http*/\r
+} /*namespace dht*/\r
diff --git a/src/compat/os_cert.h b/src/compat/os_cert.h
new file mode 100644 (file)
index 0000000..b037037
--- /dev/null
@@ -0,0 +1,81 @@
+/*\r
+ *  Copyright (C) 2020 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 "log_enable.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
diff --git a/src/crypto.cpp b/src/crypto.cpp
new file mode 100644 (file)
index 0000000..9c2800f
--- /dev/null
@@ -0,0 +1,1671 @@
+/*
+ *  Copyright (C) 2014-2020 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
+
+// support for GnuTLS < 3.4.
+#if GNUTLS_VERSION_NUMBER < 0x030400
+#define GNUTLS_PKCS_PKCS12_3DES GNUTLS_PKCS_USE_PKCS12_3DES
+#define GNUTLS_PKCS_PKCS12_ARCFOUR GNUTLS_PKCS_USE_PKCS12_ARCFOUR
+#define GNUTLS_PKCS_PKCS12_RC2_40 GNUTLS_PKCS_USE_PKCS12_RC2_40
+#define GNUTLS_PKCS_PBES2_3DES GNUTLS_PKCS_USE_PBES2_3DES
+#define GNUTLS_PKCS_PBES2_AES_128 GNUTLS_PKCS_USE_PBES2_AES_128
+#define GNUTLS_PKCS_PBES2_AES_192 GNUTLS_PKCS_USE_PBES2_AES_192
+#define GNUTLS_PKCS_PBES2_AES_256 GNUTLS_PKCS_USE_PBES2_AES_256
+#endif
+
+#define DHT_AES_LEGACY_ENCRYPT 1
+#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(int 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());
+#if DHT_AES_LEGACY_ENCRYPT
+    gcm_aes_update(&aes, data_length, data);
+#endif
+
+    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 Blob& data, const Blob& key)
+{
+    if (not aesKeySizeGood(key.size()))
+        throw DecryptError("Wrong key size");
+
+    if (data.size() <= 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.data());
+
+    size_t data_sz = data.size() - GCM_IV_SIZE - GCM_DIGEST_SIZE;
+    Blob ret(data_sz);
+    gcm_aes_decrypt(&aes, data_sz, ret.data(), data.data() + GCM_IV_SIZE);
+    gcm_aes_digest(&aes, GCM_DIGEST_SIZE, digest.data());
+
+    if (not std::equal(digest.begin(), digest.end(), data.end() - GCM_DIGEST_SIZE)) {
+#if DHT_AES_LEGACY_DECRYPT
+        //gcm_aes_decrypt(&aes, data_sz, ret.data(), 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.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.end() - GCM_DIGEST_SIZE))
+            throw DecryptError("Can't decrypt data");
+#else
+        throw DecryptError("Can't decrypt data");
+#endif
+    }
+
+    return ret;
+}
+
+Blob aesDecrypt(const Blob& data, const std::string& password)
+{
+    if (data.size() <= PASSWORD_SALT_LENGTH)
+        throw DecryptError("Wrong data size");
+    Blob salt {data.begin(), data.begin()+PASSWORD_SALT_LENGTH};
+    Blob key = stretchKey(password, salt, 256/8);
+    Blob encrypted {data.begin()+PASSWORD_SALT_LENGTH, data.end()};
+    return aesDecrypt(encrypted, 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 Blob& data) const
+{
+    if (!key)
+        throw CryptoException("Can't sign data: no private key set !");
+    if (std::numeric_limits<unsigned>::max() < data.size())
+        throw CryptoException("Can't sign data: too large !");
+    gnutls_datum_t sig;
+    const gnutls_datum_t dat {(unsigned char*)data.data(), (unsigned)data.size()};
+    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 Blob& cipher) 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 (cipher.size() < cypher_block_sz)
+        throw DecryptError("Unexpected cipher length");
+    else if (cipher.size() == cypher_block_sz)
+        return decryptBloc(cipher.data(), cypher_block_sz);
+
+    return aesDecrypt(Blob {cipher.begin() + cypher_block_sz, cipher.end()}, decryptBloc(cipher.data(), 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);
+}
+
+PublicKey
+PrivateKey::getPublicKey() const
+{
+    PublicKey pk;
+    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));
+    return pk;
+}
+
+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;
+}
+
+InfoHash
+PublicKey::getId() const
+{
+    if (not pk)
+        return {};
+    InfoHash id;
+    size_t sz = id.size();
+#if GNUTLS_VERSION_NUMBER < 0x030401
+    const int flags = 0;
+#else
+    const int flags = (id.size() == 32) ? GNUTLS_KEYID_USE_SHA256 : 0;
+#endif
+    if (auto err = gnutls_pubkey_get_key_id(pk, flags, 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.");
+    return id;
+}
+
+PkId
+PublicKey::getLongId() const
+{
+    if (not pk)
+        return {};
+#if GNUTLS_VERSION_NUMBER < 0x030401
+    throw CryptoException("Can't get 256 bits public key ID: GnuTLS 3.4.1 or higher required.");
+#else
+    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.");
+    return h;
+#endif
+}
+
+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;
+    }
+}
+
+PublicKey
+Certificate::getPublicKey() const
+{
+    PublicKey pk_ret;
+    if (auto err = gnutls_pubkey_import_x509(pk_ret.pk, cert, 0))
+        throw CryptoException(std::string("Can't get certificate public key: ") + gnutls_strerror(err));
+    return pk_ret;
+}
+
+InfoHash
+Certificate::getId() const
+{
+    if (not cert)
+        return {};
+    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.");
+    return id;
+}
+
+PkId
+Certificate::getLongId() const
+{
+    if (not cert)
+        return {};
+#if GNUTLS_VERSION_NUMBER < 0x030401
+    throw CryptoException("Can't get certificate 256 bits public key ID: GnuTLS 3.4.1 or higher required.");
+#else
+    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.");
+    return id;
+#endif
+}
+
+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);
+    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<uint64_t> dist{};
+    uint64_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)
+{
+    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, 10 * 365 * 24 * 60 * 60);
+    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
+    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)
+{
+    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, 10 * 365 * 24 * 60 * 60);
+    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();
+}
+
+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;
+}
+
+// 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 ret = gnutls_ocsp_resp_export(response, &dat);
+    if (ret < 0)
+        throw CryptoException(gnutls_strerror(ret));
+    return {dat.data, dat.data + dat.size};
+}
+
+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_verify_reason_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));
+
+    // 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));
+
+    // 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));
+
+    // 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));
+    gnutls_ocsp_cert_status_t certStatus = (gnutls_ocsp_cert_status_t)ret;
+    if (certStatus == GNUTLS_OCSP_CERT_REVOKED)
+        throw CryptoException("Certificate was revoked");
+    else if (certStatus != GNUTLS_OCSP_CERT_GOOD)
+        throw CryptoException("Certificate is unknown");
+
+    // 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);
+
+    return (gnutls_ocsp_verify_reason_t) verify;
+}
+
+// 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 e;
+    }
+}
+
+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, &copy, 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 GNUTLS_VERSION_NUMBER >= 0x030401
+        if (h.result & GNUTLS_CERT_PURPOSE_MISMATCH)
+            o << "* Certificate or an intermediate does not match the intended purpose" << std::endl;
+#endif
+        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;
+}
+
+}
+}
diff --git a/src/default_types.cpp b/src/default_types.cpp
new file mode 100644 (file)
index 0000000..73ec669
--- /dev/null
@@ -0,0 +1,106 @@
+/*
+ *  Copyright (C) 2014-2020 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
+}};
+
+}
diff --git a/src/dht.cpp b/src/dht.cpp
new file mode 100644 (file)
index 0000000..ef17480
--- /dev/null
@@ -0,0 +1,2574 @@
+/*
+ *  Copyright (C) 2014-2020 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>
+
+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 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};
+
+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) {
+            // On connected
+            if (bootstrapJob) {
+                bootstrapJob->cancel();
+                bootstrapJob.reset();
+            }
+            bootstrap_period = std::chrono::seconds(10);
+        }
+    }
+    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)
+{
+    if (not persistPath.empty())
+        saveState(persistPath);
+
+    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::sort(reported_addr.begin(), reported_addr.end(), [](const ReportedAddr& a, const ReportedAddr& b) {
+        return a.first > b.first;
+    });
+    std::vector<SockAddr> ret;
+    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 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++;
+}
+
+/* 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, sr));
+        }
+        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) {
+    if (sr->announce.empty())
+        return;
+    unsigned i = 0;
+    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;
+            /* 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: %d)",
+                        sr->id.toString().c_str(), sn->node->toString().c_str(), a.value->id);
+                auto created = a.permanent ? time_point::max() : a.created;
+                sn->acked[a.value->id] = {
+                    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: %d)",
+                        sr->id.toString().c_str(), sn->node->toString().c_str(), a.value->id);
+                sn->acked[a.value->id] = {
+                    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: %d). 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;
+                sn->acked[a.value->id] = std::make_pair(std::move(ack_req), next_refresh_time);
+
+                /* step to clear announces */
+                scheduler.edit(sr->nextSearchStep, now);
+            }
+            if (a.permanent) {
+                scheduler.add(next_refresh_time - REANNOUNCE_MARGIN, [this,ws] {
+                    if (auto sr = ws.lock()) {
+                        searchStep(sr);
+                    }
+                });
+            }
+        }
+    };
+
+    static const auto PROBE_QUERY = std::make_shared<Query>(Select {}.field(Value::Field::Id).field(Value::Field::SeqNum));
+
+    const auto& now = scheduler.time();
+    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: %d)",
+                            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 == TARGET_NODES)
+            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)) {
+                        scheduler.add(sn->getListenTime(query, getListenExpiration()), std::bind(&Dht::searchStep, this, sr));
+                        sn->onListenSynced(query);
+                    }
+                    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(Sp<Search> sr)
+{
+    if (not sr or sr->expired or sr->done) return;
+
+    const auto& now = scheduler.time();
+    /*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();
+    }
+
+    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, 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, f, q, scheduler);
+}
+
+size_t
+Dht::listen(const InfoHash& id, ValueCallback cb, Value::Filter f, Where where)
+{
+    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 = f.chain(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 val) {
+        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)
+{
+    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)
+{
+    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)
+        storageErase(id, vid);
+    return canceled;
+}
+
+// Storage
+
+void
+Dht::storageChanged(const InfoHash& id, Storage& st, ValueStorage& 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.data))
+                    vals.push_back(v.data);
+                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.data))
+                    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.data);
+                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;
+        if (not permanent) {
+            scheduler.add(expiration, std::bind(&Dht::expireStorage, this, id));
+        }
+        if (total_store_size > max_store_size) {
+            expireStore();
+        }
+        storageChanged(id, st->second, *vs, store.second.values_diff > 0);
+    }
+
+    return std::get<0>(store);
+}
+
+bool
+Dht::storageErase(const InfoHash& id, Value::Id vid)
+{
+    auto st = store.find(id);
+    if (st == store.end())
+        return false;
+    auto ret = st->second.remove(id, vid);
+    total_store_size += ret.size_diff;
+    total_values += ret.values_diff;
+    return ret.values_diff;
+}
+
+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());
+    total_store_size += stats.first;
+    total_values -= stats.second.size();
+    if (not stats.second.empty()) {
+        if (logger_)
+            logger_->d(id, "[store %s] discarded %ld expired values (%ld bytes)",
+            id.toString().c_str(), stats.second.size(), -stats.first);
+
+        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(stats.second.size());
+            for (const auto& v : stats.second)
+                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(stats.second, true);
+        }
+    }
+}
+
+void
+Dht::expireStorage(InfoHash h)
+{
+    auto i = store.find(h);
+    if (i != store.end())
+        expireStore(i);
+}
+
+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 = ++largest; it != store_quota.end(); ++it) {
+            if (it->second.size() > largest->second.size())
+                largest = it;
+        }
+        if (logger_)
+            logger_->w("No space left: discarding value of largest consumer %s", largest->first.toString().c_str());
+        while (true) {
+            auto exp_value = largest->second.getOldest();
+            auto storage = store.find(exp_value.first);
+            if (storage != store.end()) {
+                auto ret = storage->second.remove(exp_value.first, exp_value.second);
+                total_store_size += ret.size_diff;
+                total_values += ret.values_diff;
+                if (logger_)
+                    logger_->w("Discarded %ld bytes, still %ld used", largest->first.toString().c_str(), total_store_size);
+                if (ret.values_diff)
+                    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);
+    buckets(af).connectivityChanged(now);
+    network_engine.connectivityChanged(af);
+    reported_addr.erase(std::remove_if(reported_addr.begin(), reported_addr.end(), [&](const ReportedAddr& addr){
+        return addr.second.getFamily() == af;
+    }), reported_addr.end());
+}
+
+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;
+    out << std::endl << "Search IPv" << (sr.af == AF_INET6 ? '6' : '4') << ' ' << sr.id << " gets: " << sr.callbacks.size();
+    out << ", last step: " << print_time_relative(now, sr.step_time);
+    if (sr.done)
+        out << " [done]";
+    if (sr.expired)
+        out << " [expired]";
+    bool synced = sr.isSynced(now);
+    out << (synced ? " [synced]" : " [not synced]");
+    if (synced && sr.isListening(now, listen_expire))
+        out << " [listening]";
+    out << std::endl;
+
+    /*printing the queries*/
+    if (sr.callbacks.size() + sr.listeners.size() > 0)
+        out << "Queries:" << std::endl;
+    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;
+    }
+
+    out << " Common bits    InfoHash                       Conn. Get   Ops  IP" << std::endl;
+    auto last_get = sr.getLastGetTime();
+    for (const auto& np : sr.nodes) {
+        auto& n = *np;
+        out << std::setfill (' ') << std::setw(3) << InfoHash::commonBits(sr.id, n.node->id) << ' ' << n.node->id;
+        out << ' ' << (findNode(n.node->id, sr.af) ? '*' : ' ');
+        out << " [";
+        if (auto pendingCount = n.node->getPendingMessageCount())
+            out << pendingCount;
+        else
+            out << ' ';
+        out << (n.node->isExpired() ? 'x' : ' ') << "]";
+
+        // Get status
+        {
+            char g_i = n.pending(n.getStatus) ? (n.candidate ? 'c' : 'f') : ' ';
+            char s_i = n.isSynced(now) ? (n.last_get_reply > last_get ? 'u' : 's') : '-';
+            out << " [" << s_i << g_i << "] ";
+        }
+
+        // Listen status
+        if (not sr.listeners.empty()) {
+            if (n.listenStatus.empty())
+                out << "    ";
+            else
+                out << "["
+                    << (n.isListening(now,listen_expire) ? 'l' : (n.pending(n.listenStatus) ? 'f' : ' ')) << "] ";
+        }
+
+        // Announce status
+        if (not sr.announce.empty()) {
+            if (n.acked.empty()) {
+                out << "   ";
+                for (size_t a=0; a < sr.announce.size(); a++)
+                    out << ' ';
+            } else {
+                out << "[";
+                for (const auto& a : sr.announce) {
+                    auto ack = n.acked.find(a.value->id);
+                    if (ack == n.acked.end() or not ack->second.first) {
+                        out << ' ';
+                    } else {
+                        out << ack->second.first->getStateChar();
+                    }
+                }
+                out << "] ";
+            }
+        }
+        out << n.node->getAddrStr() << std::endl;
+    }
+}
+
+void
+Dht::dumpTables() const
+{
+    std::stringstream 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::stringstream 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::stringstream 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::stringstream 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::stringstream out;
+    for (const auto& b : buckets(af))
+        dumpBucket(b, out);
+    return out.str();
+}
+
+std::string
+Dht::getSearchesLog(sa_family_t af) const
+{
+    std::stringstream 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::stringstream 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() : store(), network_engine(logger_, rd, scheduler, {}) {}
+
+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_size ? (int)config.max_store_size : MAX_HASHES),
+    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_->d("DHT node initialised with ID %s", myid.toString().c_str());
+}
+
+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::onDisconnected()
+{
+    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());
+        }
+    }
+    if (bootstrapJob)
+        bootstrapJob->cancel();
+    bootstrapJob = scheduler.add(scheduler.time() + bootstrap_period, std::bind(&Dht::onDisconnected, this));
+    bootstrap_period *= 2;
+}
+
+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.push_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.push_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.push_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.push_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_->d("Sending ping to %s", sa.toString().c_str());
+    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 %s", hash.toString().c_str(), node.toString().c_str(), std::to_string(v->id).c_str());
+            } 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 %s", hash.toString().c_str(), node->toString().c_str(), std::to_string(vid).c_str());
+    } 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(now, vid, types);
+        if (expiration != time_point::max())
+            scheduler.add(expiration, 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, SockAddr(node.ss, node.sslen)));
+            importValues(state.values);
+        }
+    } catch (const std::exception& e) {
+        if (logger_)
+            logger_->w("Error importing state from %s: %s", path.c_str(), e.what());
+    }
+}
+
+}
diff --git a/src/dht_proxy_client.cpp b/src/dht_proxy_client.cpp
new file mode 100644 (file)
index 0000000..e4fea2e
--- /dev/null
@@ -0,0 +1,1328 @@
+/*
+ *  Copyright (C) 2014-2020 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))
+    {}
+
+    unsigned callbackId;
+    OpValueCache cache;
+    CacheValueCallback cb;
+    Sp<OperationState> opstate;
+    std::shared_ptr<http::Request> request;
+    std::unique_ptr<asio::steady_timer> refreshSubscriberTimer;
+};
+
+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())
+{
+    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_shared<asio::steady_timer>(httpContext_, std::chrono::steady_clock::now());
+    nextProxyConfirmationTimer_->async_wait(std::bind(&DhtProxyClient::handleProxyConfirm, this, std::placeholders::_1));
+
+    listenerRestartTimer_ = std::make_shared<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)) {
+        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)
+{
+    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.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>();
+        Value::Filter filter = w.empty() ? f : f.chain(w.getFilter());
+
+        auto rxBuf = std::make_shared<LineSplit>();
+        request->add_on_body_callback([this, key, opstate, filter, rxBuf, cb](const char* at, size_t length){
+            try {
+                auto& b = *rxBuf;
+                b.append(at, length);
+                // one value per body line
+                std::vector<Sp<Value>> values;
+                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;
+                    }
+                    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)
+{
+    auto resolver = resolver_;
+    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_->d("[proxy:client] [put] [search %s] executing for %s", key.to_c_str(), val->toString().c_str());
+
+    try {
+        auto request = buildRequest("/" + 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_->e("[proxy:client] [status] failed to parse value from  server", response.status_code);
+                    }
+                }
+            } else {
+                if (logger_)
+                    logger_->e("[proxy:client] [status] failed with code=%i", 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_->e("[proxy:client] [put %s] error: %s", key.to_c_str(), e.what());
+    }
+}
+
+/**
+ * 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);
+    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) {
+                    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_;
+    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;
+    } else {
+        if (logger_)
+            logger_->d("[proxy:client] [info] got proxy reply for %s",
+                       family == AF_INET ? "ipv4" : "ipv6");
+        try {
+            myid = InfoHash(proxyInfos["node_id"].asString());
+            stats4_ = NodeStats(proxyInfos["ipv4"]);
+            stats6_ = NodeStats(proxyInfos["ipv6"]);
+            if (stats4_.good_nodes + stats6_.good_nodes)
+                status = NodeStatus::Connected;
+            else if (stats4_.dubious_nodes + stats6_.dubious_nodes)
+                status = NodeStatus::Connecting;
+            else
+                status = NodeStatus::Disconnected;
+
+            auto publicIp = parsePublicAddress(proxyInfos["public_ip"]);
+            auto publicFamily = publicIp.getFamily();
+            if (publicFamily == AF_INET)
+                publicAddressV4_ = publicIp;
+            else if (publicFamily == AF_INET6)
+                publicAddressV6_ = publicIp;
+        } 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) {
+            listenerRestartTimer_->expires_at(std::chrono::steady_clock::now());
+            listenerRestartTimer_->async_wait(std::bind(&DhtProxyClient::restartListeners, this, std::placeholders::_1));
+        }
+        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) {
+        nextProxyConfirmationTimer_->expires_at(std::chrono::steady_clock::now() + std::chrono::minutes(1));
+        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, 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;
+        };
+        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));
+        }
+        ListenMethod method;
+        restinio::http_request_header_t header;
+        if (deviceKey_.empty()){ // listen
+            method = ListenMethod::LISTEN;
+#ifdef OPENDHT_PROXY_HTTP_PARSER_FORK
+            header.method(restinio::method_listen);
+            header.request_target("/" + key.toString());
+#else
+            header.method(restinio::http_method_get());
+            header.request_target("/key/" + key.toString() + "/listen");
+#endif
+        }
+        else {
+            method = ListenMethod::SUBSCRIBE;
+            header.method(restinio::http_method_subscribe());
+            header.request_target("/" + 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.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_) {
+        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));
+        }
+    }
+    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;
+#ifdef OPENDHT_PROXY_HTTP_PARSER_FORK
+            header.method(restinio::method_listen);
+            header.request_target("/" + search.first.toString());
+#else
+            header.method(restinio::http_method_get());
+            header.request_target("/key/" + search.first.toString() + "/listen");
+#endif
+            sendListen(header, cb, opstate, listener, ListenMethod::LISTEN);
+        }
+    }
+}
+
+void
+DhtProxyClient::pushNotificationReceived(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_);
+        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> lock(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_();
+#else
+    (void) notification;
+#endif
+}
+
+void
+DhtProxyClient::resubscribe(const InfoHash& key, const size_t token, 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;
+    opstate->stop = true;
+    if (listener.request){
+        listener.request.reset();
+    }
+    opstate->stop = 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);
+#else
+    (void) key;
+    (void) listener;
+#endif
+}
+
+#ifdef OPENDHT_PUSH_NOTIFICATIONS
+void
+DhtProxyClient::getPushRequest(Json::Value& body) const
+{
+    body["key"] = deviceKey_;
+    body["client_id"] = pushClientId_;
+    body["session_id"] = pushSessionId_;
+#ifdef __ANDROID__
+    body["platform"] = "android";
+#endif
+#ifdef __APPLE__
+    body["platform"] = "apple";
+#endif
+}
+
+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
diff --git a/src/dht_proxy_server.cpp b/src/dht_proxy_server.cpp
new file mode 100644 (file)
index 0000000..d1f801c
--- /dev/null
@@ -0,0 +1,1409 @@
+/*
+ *  Copyright (C) 2014-2020 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;
+
+#ifdef OPENDHT_PROXY_HTTP_PARSER_FORK
+namespace restinio {
+struct custom_http_methods_t
+{
+    static constexpr restinio::http_method_id_t from_nodejs(int m) noexcept {
+        if(m == method_listen.raw_id())
+            return method_listen;
+        else if(m == method_stats.raw_id())
+            return method_stats;
+        else if(m == method_sign.raw_id())
+            return method_sign;
+        else if(m == method_encrypt.raw_id())
+            return method_encrypt;
+        else
+            return restinio::default_http_methods_t::from_nodejs(m);
+    }
+};
+}
+#endif
+
+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() {};
+    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;
+#ifdef OPENDHT_PROXY_HTTP_PARSER_FORK
+    using http_methods_mapper_t = restinio::custom_http_methods_t;
+#endif
+    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;
+#ifdef OPENDHT_PROXY_HTTP_PARSER_FORK
+    using http_methods_mapper_t = restinio::custom_http_methods_t;
+#endif
+    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")) {
+        clientId = cid->as<std::string>();
+    }
+    if (auto exp = findMapValue(o, "exp")) {
+        expiration = from_time_t(exp->as<time_t>());
+    }
+    if (auto token = findMapValue(o, "token")) {
+        pushToken = token->as<std::string>();
+    }
+    if (auto sid = findMapValue(o, "sid")) {
+        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")) {
+        type = t->as<PushType>();
+    }
+    if (auto val = findMapValue(o, "value")) {
+        value = std::make_shared<dht::Value>(*val);
+    }
+}
+
+#ifdef OPENDHT_PUSH_NOTIFICATIONS
+void
+DhtProxyServer::Listener::msgpack_unpack(const msgpack::object& o)
+{
+    if (auto cid = findMapValue(o, "cid")) {
+        clientId = cid->as<std::string>();
+    }
+    if (auto exp = findMapValue(o, "exp")) {
+        expiration = from_time_t(exp->as<time_t>());
+    }
+    if (auto sid = findMapValue(o, "sid")) {
+        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")) {
+        type = t->as<PushType>();
+    }
+}
+#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)
+{
+    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);
+        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);
+        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")) {
+                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));
+                        }
+#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 listeners = findMapValue(oh.get(), "pushListeners")) {
+                std::lock_guard<std::mutex> lock(lockListener_);
+                pushListeners_ = listeners->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]
+                                (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::stringstream 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);
+                                    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));
+                            // 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));
+#ifdef OPENDHT_PROXY_HTTP_PARSER_FORK
+    // node.stats
+    router->add_handler(restinio::custom_http_methods_t::from_nodejs(restinio::method_stats.raw_id()),
+                        "/", std::bind(&DhtProxyServer::getStats, this, _1, _2));
+#endif
+    // 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_PROXY_HTTP_PARSER_FORK
+    // key.listen
+    router->add_handler(restinio::custom_http_methods_t::from_nodejs(restinio::method_listen.raw_id()),
+                        "/:hash", std::bind(&DhtProxyServer::listen, this, _1, _2));
+#endif
+#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
+#ifdef OPENDHT_PROXY_HTTP_PARSER_FORK
+    // key.sign
+    router->add_handler(restinio::custom_http_methods_t::from_nodejs(restinio::method_sign.raw_id()),
+                        "/:hash", std::bind(&DhtProxyServer::putSigned, this, _1, _2));
+    // key.encrypt
+    router->add_handler(restinio::custom_http_methods_t::from_nodejs(restinio::method_encrypt.raw_id()),
+                        "/:hash", std::bind(&DhtProxyServer::putEncrypted, this, _1, _2));
+#endif
+#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
+    // 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"].to_string());
+        if (!infoHash)
+            infoHash = InfoHash::get(params["hash"].to_string());
+        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::stringstream 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"].to_string());
+        if (!infoHash)
+            infoHash = InfoHash::get(params["hash"].to_string());
+        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
+
+RequestStatus
+DhtProxyServer::subscribe(restinio::request_handle_t request,
+                          restinio::router::route_params_t params)
+{
+    requestNum_++;
+    try {
+        InfoHash infoHash(params["hash"].to_string());
+        if (!infoHash)
+            infoHash = InfoHash::get(params["hash"].to_string());
+
+        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 = root["platform"].asString() == "android" ? PushType::Android : PushType::iOS;
+        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(lockPushListeners_);
+        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;
+        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));
+        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]
+                (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::stringstream 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);
+                    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"].to_string());
+    if (!infoHash)
+        infoHash = InfoHash::get(params["hash"].to_string());
+
+    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)
+{
+    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 put refresh to %s token", pushToken.c_str());
+    sendPushNotification(pushToken, jsonProvider(), type, false);
+}
+
+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)
+{
+    if (pushServer_.empty())
+        return;
+
+    unsigned reqid = 0;
+    try {
+        auto request = std::make_shared<http::Request>(io_context(), pushHostPort_.first, pushHostPort_.second,
+                                                                    httpsServer_ ? true : false, logger_);
+        reqid = request->id();
+        request->set_target("/api/push");
+        request->set_method(restinio::http_method_post());
+        request->set_header_field(restinio::http_field_t::host, 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");
+
+        // NOTE: see https://github.com/appleboy/gorush
+        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";
+        notification["time_to_live"] = 600;
+
+        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: %i", 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"].to_string());
+    if (!infoHash)
+        infoHash = InfoHash::get(params["hash"].to_string());
+
+    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;
+                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();
+                }
+                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_, 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) {
+                    auto &ctx = io_context();
+                    // cancel permanent put
+                    pput.expireTimer = std::make_unique<asio::steady_timer>(ctx, timeout);
+#ifdef OPENDHT_PUSH_NOTIFICATIONS
+                    if (not pushToken.empty()){
+                        bool isAndroid = platform == "android";
+                        pput.pushToken = pushToken;
+                        pput.clientId = clientId;
+                        pput.type = isAndroid ? PushType::Android : PushType::iOS;
+                        pput.sessionCtx = std::make_shared<PushSessionContext>(sessionId);
+                        // notify push listen expire
+                        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>(ctx,
+                                                     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));
+                    }
+#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);
+                    if (pput.expireNotifyTimer)
+                        pput.expireNotifyTimer->expires_at(timeout - proxy::OP_MARGIN);
+                }
+                pput.expireTimer->async_wait(std::bind(&DhtProxyServer::handleCancelPermamentPut, this,
+                                                std::placeholders::_1, infoHash, vid));
+            }
+            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"].to_string());
+    if (!infoHash)
+        infoHash = InfoHash::get(params["hash"].to_string());
+
+    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"].to_string());
+    if (!infoHash)
+        infoHash = InfoHash::get(params["hash"].to_string());
+
+    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 value = params["value"].to_string();
+    InfoHash infoHash(params["hash"].to_string());
+    if (!infoHash)
+        infoHash = InfoHash::get(params["hash"].to_string());
+
+    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();
+            },
+            {}, value);
+        return restinio::request_handling_status_t::accepted;
+    } catch (const std::exception& e){
+        return serverError(*request);
+    }
+}
+
+}
diff --git a/src/dhtrunner.cpp b/src/dhtrunner.cpp
new file mode 100644 (file)
index 0000000..2ece5e2
--- /dev/null
@@ -0,0 +1,1200 @@
+/*
+ *  Copyright (C) 2014-2020 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 {
+
+constexpr std::chrono::seconds DhtRunner::BOOTSTRAP_PERIOD;
+static const std::string PEER_DISCOVERY_DHT_SERVICE = "dht";
+
+struct DhtRunner::Listener {
+    size_t tokenClassicDht {0};
+    size_t tokenProxyDht {0};
+    ValueCallback gcb;
+    InfoHash hash {};
+    Value::Filter f;
+    Where w;
+};
+
+struct NodeInsertionPack {
+    dht::InfoHash nodeId;
+    in_port_t port;
+    dht::NetId net;
+    MSGPACK_DEFINE(nodeId, port, net)
+};
+
+DhtRunner::DhtRunner() : dht_()
+#ifdef OPENDHT_PROXY_CLIENT
+, dht_via_proxy_()
+#endif //OPENDHT_PROXY_CLIENT
+{
+#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, const Config& config, Context&& context)
+{
+    SockAddr sin4;
+    sin4.setFamily(AF_INET);
+    sin4.setPort(port);
+    SockAddr sin6;
+    sin6.setFamily(AF_INET6);
+    sin6.setPort(port);
+    run(sin4, sin6, config, std::move(context));
+}
+
+void
+DhtRunner::run(const char* ip4, const char* ip6, const char* service, const 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();
+    run(res4.front(), res6.front(), config, std::move(context));
+}
+
+void
+DhtRunner::run(SockAddr& local4, SockAddr& local6, const Config& config, Context&& context)
+{
+    if (running == State::Idle) {
+        auto state_path = config.dht_config.node_config.persist_path;
+        if (not state_path.empty()) {
+            state_path += "_port.txt";
+            std::ifstream inConfig(state_path);
+            if (inConfig.is_open()) {
+                in_port_t port;
+                if (inConfig >> port) {
+                    if (context.logger)
+                        context.logger->d("[runner %p] Using IPv4 port %hu from saved configuration", this, port);
+                    if (local4.getPort() == 0)
+                        local4.setPort(port);
+                }
+                if (inConfig >> port) {
+                    if (context.logger)
+                        context.logger->d("[runner %p] Using IPv6 port %hu from saved configuration", this, port);
+                    if (local6.getPort() == 0)
+                        local6.setPort(port);
+                }
+            }
+        }
+
+        if (not context.sock)
+            context.sock.reset(new net::UdpSocket(local4, local6, context.logger));
+
+        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;
+        }
+        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))
+        return;
+
+    if (context.logger) {
+        logger_ = context.logger;
+        logger_->d("[runner %p] state changed to Running", this);
+    }
+
+    context.sock->setOnReceive([&] (net::PacketList&& pkts) {
+        net::PacketList ret;
+        {
+            std::lock_guard<std::mutex> lck(sock_mtx);
+            auto maxSize = net::RX_QUEUE_MAX_SIZE - pkts.size();
+            while (rcv.size() > maxSize) {
+                if (logger_)
+                    logger_->e("Dropping packet: queue is full!");
+                rcv.pop_front();
+            }
+
+            rcv.splice(rcv.end(), std::move(pkts));
+            ret = std::move(rcv_free);
+        }
+        cv.notify_all();
+        return ret;
+    });
+
+    auto dht = std::unique_ptr<DhtInterface>(new Dht(std::move(context.sock), SecureDht::getConfig(config.dht_config), context.logger));
+    dht_ = std::unique_ptr<SecureDht>(new SecureDht(std::move(dht), config.dht_config));
+
+#ifdef OPENDHT_PROXY_CLIENT
+    config_ = config;
+#endif
+    enableProxy(not config.proxy_server.empty());
+    if (context.logger and dht_via_proxy_) {
+        dht_via_proxy_->setLogger(context.logger);
+    }
+    if (context.statusChangedCallback) {
+        statusCb = std::move(context.statusChangedCallback);
+    }
+    if (context.certificateStore) {
+        dht_->setLocalCertificateStore(std::move(context.certificateStore));
+        if (dht_via_proxy_)
+            dht_via_proxy_->setLocalCertificateStore(std::move(context.certificateStore));
+    }
+
+    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.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();
+        // IPv4
+        if (const auto& bound4 = dht_->getSocket()->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 = dht_->getSocket()->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) {
+    auto expected = State::Running;
+    if (not running.compare_exchange_strong(expected, State::Stopping)) {
+        if (expected == State::Stopping and ongoing_ops) {
+            std::lock_guard<std::mutex> lck(storage_mtx);
+            shutdownCallbacks_.emplace_back(std::move(cb));
+        }
+        else if (cb) cb();
+        return;
+    }
+    if (logger_)
+        logger_->d("[runner %p] state changed to Stopping, %zu ongoing ops", this, ongoing_ops.load());
+    std::lock_guard<std::mutex> lck(storage_mtx);
+    ongoing_ops++;
+    shutdownCallbacks_.emplace_back(std::move(cb));
+    pending_ops_prio.emplace([=](SecureDht&) mutable {
+        auto onShutdown = [this]{ opEnded(); };
+#ifdef OPENDHT_PROXY_CLIENT
+        if (dht_via_proxy_)
+            dht_via_proxy_->shutdown(onShutdown);
+#endif
+        if (dht_)
+            dht_->shutdown(onShutdown);
+    });
+    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() {
+    if (running != State::Stopping or ongoing_ops)
+        return false;
+    decltype(shutdownCallbacks_) cbs;
+    {
+        std::lock_guard<std::mutex> lck(storage_mtx);
+        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", this);
+    }
+
+    if (dht_thread.joinable())
+        dht_thread.join();
+
+    {
+        std::lock_guard<std::mutex> lck(storage_mtx);
+        pending_ops = decltype(pending_ops)();
+        pending_ops_prio = decltype(pending_ops_prio)();
+        ongoing_ops = 0;
+    }
+    {
+        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);
+    activeDht()->dumpTables();
+}
+
+InfoHash
+DhtRunner::getId() const
+{
+    if (auto dht = activeDht())
+        return dht->getId();
+    return {};
+}
+
+InfoHash
+DhtRunner::getNodeId() const
+{
+    if (auto dht = activeDht())
+        return dht->getNodeId();
+    return {};
+}
+
+
+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);
+#ifdef OPENDHT_PROXY_CLIENT
+    if (dht_via_proxy_)
+        dht_via_proxy_->setLogger(logger);
+#endif
+}
+
+void
+DhtRunner::setLoggers(LogMethod error, LogMethod warn, LogMethod debug) {
+    Logger logger {std::move(error), std::move(warn), std::move(debug)};
+    setLogger(logger);
+}
+
+void
+DhtRunner::setLogFilter(const InfoHash& f) {
+    std::lock_guard<std::mutex> lck(dht_mtx);
+    if (dht_)
+        dht_->setLogFilter(f);
+#ifdef OPENDHT_PROXY_CLIENT
+    if (dht_via_proxy_)
+        dht_via_proxy_->setLogFilter(f);
+#endif
+}
+
+void
+DhtRunner::registerType(const ValueType& type) {
+    std::lock_guard<std::mutex> lck(dht_mtx);
+    activeDht()->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 = activeDht()->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 activeDht()->getNodesStats(af);
+}
+
+NodeInfo
+DhtRunner::getNodeInfo() const {
+    std::lock_guard<std::mutex> lck(dht_mtx);
+    NodeInfo info {};
+    if (auto dht = activeDht()) {
+        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);
+        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 activeDht()->getNodeMessageStats(in);
+}
+
+std::string
+DhtRunner::getStorageLog() const
+{
+    std::lock_guard<std::mutex> lck(dht_mtx);
+    return activeDht()->getStorageLog();
+}
+std::string
+DhtRunner::getStorageLog(const InfoHash& f) const
+{
+    std::lock_guard<std::mutex> lck(dht_mtx);
+    return activeDht()->getStorageLog(f);
+}
+std::string
+DhtRunner::getRoutingTablesLog(sa_family_t af) const
+{
+    std::lock_guard<std::mutex> lck(dht_mtx);
+    return activeDht()->getRoutingTablesLog(af);
+}
+std::string
+DhtRunner::getSearchesLog(sa_family_t af) const
+{
+    std::lock_guard<std::mutex> lck(dht_mtx);
+    return activeDht()->getSearchesLog(af);
+}
+std::string
+DhtRunner::getSearchLog(const InfoHash& f, sa_family_t af) const
+{
+    std::lock_guard<std::mutex> lck(dht_mtx);
+    return activeDht()->getSearchLog(f, af);
+}
+std::vector<SockAddr>
+DhtRunner::getPublicAddress(sa_family_t af)
+{
+    std::lock_guard<std::mutex> lck(dht_mtx);
+    if (auto dht = activeDht())
+        return dht->getPublicAddress(af);
+    return {};
+}
+std::vector<std::string>
+DhtRunner::getPublicAddressStr(sa_family_t af)
+{
+    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::registerCertificate(std::shared_ptr<crypto::Certificate> cert) {
+    std::lock_guard<std::mutex> lck(dht_mtx);
+    activeDht()->registerCertificate(cert);
+}
+void
+DhtRunner::setLocalCertificateStore(CertificateStoreQuery&& query_method) {
+    std::lock_guard<std::mutex> lck(dht_mtx);
+#ifdef OPENDHT_PROXY_CLIENT
+    if (dht_via_proxy_)
+        dht_via_proxy_->setLocalCertificateStore(std::forward<CertificateStoreQuery>(query_method));
+#endif
+    if (dht_)
+        dht_->setLocalCertificateStore(std::forward<CertificateStoreQuery>(query_method));
+}
+
+time_point
+DhtRunner::loop_()
+{
+    auto dht = activeDht();
+    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)
+        std::cerr << "Dropped " << dropped << " packets with high delay" << std::endl;
+
+    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)
+{
+    if (running != State::Running) {
+        if (dcb) dcb(false, {});
+        return;
+    }
+    std::lock_guard<std::mutex> lck(storage_mtx);
+    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) {
+    if (running != State::Running) {
+        if (done_cb) done_cb(false, {});
+        return;
+    }
+    std::lock_guard<std::mutex> lck(storage_mtx);
+    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>>();
+    if (running != State::Running) {
+        ret_token->set_value(0);
+        return ret_token->get_future();
+    }
+    std::lock_guard<std::mutex> lck(storage_mtx);
+    pending_ops.emplace([=](SecureDht& dht) mutable {
+#ifdef OPENDHT_PROXY_CLIENT
+        auto tokenbGlobal = listener_token_++;
+        auto& listener = listeners_[tokenbGlobal];
+        listener.hash = hash;
+        listener.f = std::move(f);
+        listener.w = std::move(w);
+        listener.gcb = [hash,vcb,tokenbGlobal,this](const std::vector<Sp<Value>>& vals, bool expired) {
+            if (not vcb(vals, expired)) {
+                cancelListen(hash, tokenbGlobal);
+                return false;
+            }
+            return true;
+        };
+        if (auto token = dht.listen(hash, listener.gcb, listener.f, listener.w)) {
+            if (use_proxy)  listener.tokenProxyDht = token;
+            else            listener.tokenClassicDht = token;
+        }
+        ret_token->set_value(tokenbGlobal);
+#else
+        ret_token->set_value(dht.listen(hash, std::move(vcb), std::move(f), std::move(w)));
+#endif
+    });
+    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);
+#ifdef OPENDHT_PROXY_CLIENT
+    pending_ops.emplace([=](SecureDht&) {
+        auto it = listeners_.find(token);
+        if (it == listeners_.end()) return;
+        if (it->second.tokenClassicDht)
+            dht_->cancelListen(h, it->second.tokenClassicDht);
+        if (it->second.tokenProxyDht and dht_via_proxy_)
+            dht_via_proxy_->cancelListen(h, it->second.tokenProxyDht);
+        listeners_.erase(it);
+    });
+#else
+    pending_ops.emplace([=](SecureDht& dht) {
+        dht.cancelListen(h, token);
+    });
+#endif // OPENDHT_PROXY_CLIENT
+    cv.notify_all();
+}
+
+void
+DhtRunner::cancelListen(InfoHash h, std::shared_future<size_t> ftoken)
+{
+    std::lock_guard<std::mutex> lck(storage_mtx);
+#ifdef OPENDHT_PROXY_CLIENT
+    pending_ops.emplace([=](SecureDht&) {
+        auto it = listeners_.find(ftoken.get());
+        if (it == listeners_.end()) return;
+        if (it->second.tokenClassicDht)
+            dht_->cancelListen(h, it->second.tokenClassicDht);
+        if (it->second.tokenProxyDht and dht_via_proxy_)
+            dht_via_proxy_->cancelListen(h, it->second.tokenProxyDht);
+        listeners_.erase(it);
+    });
+#else
+    pending_ops.emplace([=](SecureDht& dht) {
+        dht.cancelListen(h, ftoken.get());
+    });
+#endif // OPENDHT_PROXY_CLIENT
+    cv.notify_all();
+}
+
+void
+DhtRunner::put(InfoHash hash, Value&& value, DoneCallback cb, time_point created, bool permanent)
+{
+    if (running != State::Running) {
+        if (cb) cb(false, {});
+        return;
+    }
+    std::lock_guard<std::mutex> lck(storage_mtx);
+    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)
+{
+    if (running != State::Running) {
+        if (cb) cb(false, {});
+        return;
+    }
+    std::lock_guard<std::mutex> lck(storage_mtx);
+    ongoing_ops++;
+    pending_ops.emplace([=, 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)
+{
+    if (running != State::Running) {
+        if (cb) cb(false, {});
+        return;
+    }
+    std::lock_guard<std::mutex> lck(storage_mtx);
+    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)
+{
+    if (running != State::Running) {
+        if (cb) cb(false, {});
+        return;
+    }
+    std::lock_guard<std::mutex> lck(storage_mtx);
+    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::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(const SockAddr& addr, DoneCallbackSimple&& cb)
+{
+    if (running != State::Running) {
+        if (cb) cb(false);
+        return;
+    }
+    std::lock_guard<std::mutex> lck(storage_mtx);
+    ongoing_ops++;
+    pending_ops_prio.emplace([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)
+{
+    if (running != State::Running)
+        return;
+    std::unique_lock<std::mutex> lck(storage_mtx);
+    pending_ops_prio.emplace([id, address](SecureDht& dht) mutable {
+        dht.insertNode(id, address);
+    });
+    cv.notify_all();
+}
+
+void
+DhtRunner::bootstrap(const std::vector<NodeExport>& nodes)
+{
+    if (running != State::Running)
+        return;
+    std::lock_guard<std::mutex> lck(storage_mtx);
+    pending_ops_prio.emplace([=](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) {
+    if (running != State::Running) {
+        cb({});
+        return;
+    }
+    std::lock_guard<std::mutex> lck(storage_mtx);
+    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();
+#ifdef OPENDHT_PROXY_CLIENT
+    listeners_.clear();
+    dht_via_proxy_.reset();
+#endif // OPENDHT_PROXY_CLIENT
+    dht_.reset();
+}
+
+SecureDht*
+DhtRunner::activeDht() const
+{
+#ifdef OPENDHT_PROXY_CLIENT
+    return use_proxy? dht_via_proxy_.get() : dht_.get();
+#else
+    return dht_.get();
+#endif // OPENDHT_PROXY_CLIENT
+}
+
+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())
+        std::cerr << "DHT proxy requested but OpenDHT built without proxy support." << std::endl;
+#endif
+}
+
+void
+DhtRunner::enableProxy(bool proxify)
+{
+#ifdef OPENDHT_PROXY_CLIENT
+    if (dht_via_proxy_) {
+        dht_via_proxy_->shutdown({});
+    }
+    if (proxify) {
+        // Init the proxy client
+        auto dht_via_proxy = std::unique_ptr<DhtInterface>(
+            new 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_)
+        );
+#ifdef OPENDHT_PUSH_NOTIFICATIONS
+        if (not config_.push_token.empty())
+            dht_via_proxy->setPushNotificationToken(config_.push_token);
+#endif
+        dht_via_proxy_ = std::unique_ptr<SecureDht>(new SecureDht(std::move(dht_via_proxy), config_.dht_config));
+        // add current listeners
+        for (auto& l: listeners_)
+            l.second.tokenProxyDht = dht_via_proxy_->listen(l.second.hash, l.second.gcb, l.second.f, l.second.w);
+        // and use it
+        use_proxy = proxify;
+    } else {
+        use_proxy = proxify;
+        std::lock_guard<std::mutex> lck(storage_mtx);
+        if (not listeners_.empty()) {
+            pending_ops.emplace([this](SecureDht& /*dht*/) mutable {
+                if (not dht_)
+                    return;
+                for (auto& l : listeners_) {
+                    if (not l.second.tokenClassicDht) {
+                        l.second.tokenClassicDht = dht_->listen(l.second.hash, l.second.gcb, l.second.f, l.second.w);
+                    }
+                }
+            });
+        }
+    }
+#else
+    if (proxify)
+        std::cerr << "DHT proxy requested but OpenDHT built without proxy support." << std::endl;
+#endif
+}
+
+void
+DhtRunner::forwardAllMessages(bool forward)
+{
+    std::lock_guard<std::mutex> lck(dht_mtx);
+#ifdef OPENDHT_PROXY_SERVER
+#ifdef OPENDHT_PROXY_CLIENT
+    if (dht_via_proxy_)
+        dht_via_proxy_->forwardAllMessages(forward);
+#endif // OPENDHT_PROXY_CLIENT
+    if (dht_)
+        dht_->forwardAllMessages(forward);
+#else
+    (void) forward;
+#endif // OPENDHT_PROXY_SERVER
+}
+
+/**
+ * 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_via_proxy_)
+        dht_via_proxy_->setPushNotificationToken(token);
+#else
+    (void) token;
+#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_via_proxy_)
+                dht_via_proxy_->pushNotificationReceived(data);
+        });
+    }
+    cv.notify_all();
+#else
+    (void) data;
+#endif
+}
+
+}
diff --git a/src/http.cpp b/src/http.cpp
new file mode 100644 (file)
index 0000000..983bcc4
--- /dev/null
@@ -0,0 +1,1428 @@
+/*
+ *  Copyright (C) 2014-2020 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 "log_enable.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>
+
+#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::stringstream 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->d("Using CA file: %s", path);
+        ctx->load_verify_file(path);
+    } else if (char* path = getenv("CA_ROOT_PATH")) {
+        if (logger)
+            logger->d("Using CA path: %s", 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_)
+        asio::async_connect(ssl_socket_->lowest_layer(), std::move(endpoints), wrapCallabck(std::move(cb)));
+    else if (socket_)
+        asio::async_connect(*socket_, std::move(endpoints), wrapCallabck(std::move(cb)));
+    else if (cb)
+        cb(asio::error::operation_aborted, {});
+}
+
+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_, wrapCallabck(std::move(cb)));
+    else if (socket_) asio::async_write(*socket_, write_buf_, wrapCallabck(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, wrapCallabck(std::move(cb)));
+    else if (socket_) asio::async_read_until(*socket_, read_buf_, delim, wrapCallabck(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, wrapCallabck(std::move(cb)));
+    else if (socket_) asio::async_read_until(*socket_, read_buf_, delim, wrapCallabck(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), wrapCallabck(std::move(cb)));
+    else if (socket_) asio::async_read(*socket_, read_buf_, asio::transfer_exactly(bytes), wrapCallabck(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);
+}
+
+void
+Connection::timeout(const std::chrono::seconds timeout, HandlerCb cb)
+{
+    if (!is_open()){
+        if (logger_)
+            logger_->e("[connection:%i] closed, can't timeout", id_);
+        if (cb)
+            cb(asio::error::operation_aborted);
+        return;
+    }
+    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::stringstream 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();
+}
+
+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_);
+
+    // 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_->d("[http:request:%i] sending %zu 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 (logger_) {
+        if (ec and ec != asio::error::eof and ec != asio::error::operation_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& resp){
+        std::lock_guard<std::mutex> lk(mtx);
+        ok = true;
+        cv.notify_all();
+    });
+    cv.wait(lock, [&]{ return ok; });
+    return response_;
+}
+
+} // namespace http
+} // namespace dht
diff --git a/src/indexation/pht.cpp b/src/indexation/pht.cpp
new file mode 100644 (file)
index 0000000..2494365
--- /dev/null
@@ -0,0 +1,538 @@
+/*
+ *  Copyright (C) 2014-2020 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::stringstream 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::stringstream 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 */
diff --git a/src/infohash.cpp b/src/infohash.cpp
new file mode 100644 (file)
index 0000000..69c79b8
--- /dev/null
@@ -0,0 +1,56 @@
+/*
+ *  Copyright (C) 2014-2020 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 "infohash.h"
+
+#include <functional>
+#include <sstream>
+#include <cstdio>
+
+namespace dht {
+
+const HexMap hex_map = {};
+
+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>() != "id")
+        throw msgpack::type_error();
+    if (o.via.map.ptr[1].key.as<std::string>() != "addr")
+        throw msgpack::type_error();
+    const auto& addr = o.via.map.ptr[1].val;
+    if (addr.type != msgpack::type::BIN)
+        throw msgpack::type_error();
+    if (addr.via.bin.size > sizeof(sockaddr_storage))
+        throw msgpack::type_error();
+    id.msgpack_unpack(o.via.map.ptr[0].val);
+    sslen = addr.via.bin.size;
+    std::copy_n(addr.via.bin.ptr, addr.via.bin.size, (char*)&ss);
+}
+
+std::ostream& operator<< (std::ostream& s, const NodeExport& h)
+{
+    msgpack::pack(s, h);
+    return s;
+}
+
+}
diff --git a/src/listener.h b/src/listener.h
new file mode 100644 (file)
index 0000000..18663f0
--- /dev/null
@@ -0,0 +1,52 @@
+/*
+ *  Copyright (C) 2014-2020 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;
+};
+
+}
diff --git a/src/log.cpp b/src/log.cpp
new file mode 100644 (file)
index 0000000..b9a6220
--- /dev/null
@@ -0,0 +1,137 @@
+/*
+ *  Copyright (C) 2014-2020 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"
+
+#ifndef _WIN32
+#include <syslog.h>
+#endif
+
+#include <fstream>
+#include <chrono>
+
+namespace dht {
+namespace log {
+
+/**
+ * Print va_list to std::ostream (used for logging).
+ */
+void
+printLog(std::ostream& s, char const *m, va_list args) {
+    // print log to buffer
+    std::array<char, 8192> buffer;
+    int ret = vsnprintf(buffer.data(), buffer.size(), m, args);
+    if (ret < 0)
+        return;
+
+    // write timestamp
+    using namespace std::chrono;
+    using log_precision = microseconds;
+    constexpr auto den = log_precision::period::den;
+    auto num = duration_cast<log_precision>(steady_clock::now().time_since_epoch()).count();
+    s << "[" << std::setfill('0') << std::setw(6) << num / den << "."
+             << std::setfill('0') << std::setw(6) << num % den << "]" << " ";
+
+    // write log
+    s.write(buffer.data(), std::min((size_t) ret, buffer.size()));
+    if ((size_t) ret >= buffer.size())
+        s << "[[TRUNCATED]]";
+    s << std::endl;
+}
+
+std::shared_ptr<Logger>
+getStdLogger() {
+    return std::make_shared<Logger>(
+        [](char const *m, va_list args) {
+            std::cerr << red;
+            printLog(std::cerr, m, args);
+            std::cerr << def;
+        },
+        [](char const *m, va_list args) {
+            std::cout << yellow;
+            printLog(std::cout, m, args);
+            std::cout << def;
+        },
+        [](char const *m, va_list args) { printLog(std::cout, m, args); }
+    );
+}
+
+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>(
+        [=](char const *m, va_list args) { printLog(*logfile, m, args); },
+        [=](char const *m, va_list args) { printLog(*logfile, m, args); },
+        [=](char const *m, va_list args) { printLog(*logfile, m, args); }
+    );
+}
+
+std::shared_ptr<Logger>
+getSyslogLogger(const char* name) {
+#ifndef _WIN32
+    struct Syslog {
+        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](char const *m, va_list args) { vsyslog(LOG_ERR, m, args); },
+        [logfile](char const *m, va_list args) { vsyslog(LOG_WARNING, m, args); },
+        [logfile](char const *m, va_list args) { vsyslog(LOG_INFO, m, args); }
+    );
+#else
+    return std::make_shared<Logger>();
+#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();
+}
+
+}
+}
diff --git a/src/net.h b/src/net.h
new file mode 100644 (file)
index 0000000..dd4b914
--- /dev/null
+++ b/src/net.h
@@ -0,0 +1,39 @@
+/*
+ *  Copyright (C) 2014-2020 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 */
diff --git a/src/network_engine.cpp b/src/network_engine.cpp
new file mode 100644 (file)
index 0000000..1099162
--- /dev/null
@@ -0,0 +1,1366 @@
+/*
+ *  Copyright (C) 2014-2020 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 "log_enable.h"
+#include "parsed_message.h"
+
+#include <msgpack.hpp>
+
+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;
+
+const std::string NetworkEngine::my_v {"RNG1"};
+
+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(const Sp<Logger>& log, std::mt19937_64& rand, Scheduler& scheduler, std::unique_ptr<DatagramSocket>&& sock)
+    : myid(zeroes), dht_socket(std::move(sock)), logger_(log), rd(rand), cache(rd), rate_limiter((size_t)-1), scheduler(scheduler)
+{}
+
+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(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, values, scheduler.time(), ntoken, socket_id);
+        } else {
+            sendNodesValues(node->getAddr(), socket_id, nnodes.first, nnodes.second, values, query, ntoken);
+        }
+    } catch (const std::overflow_error& e) {
+        if (logger_)
+            logger_->e("Can't send value: buffer not large enough !");
+    }
+}
+
+void
+NetworkEngine::tellListenerRefreshed(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(my_v);
+    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(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(my_v);
+    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 ||
+              (address[0] == 0xFE && (address[1] & 0xC0) == 0x80) ||
+               memcmp(address, zeroes.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;
+    }
+
+    std::unique_ptr<ParsedMessage> msg {new 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()) {
+                // process the full message
+                process(std::move(pmsg_it->second.msg), from);
+                partial_messages.erase(pmsg_it);
+            } 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()) {
+        process(std::move(msg), from);
+    } 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 already exists");
+    }
+}
+
+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(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(my_v);
+    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(my_v);
+    if (config.network) {
+        pk.pack(KEY_NETID); pk.pack(config.network);
+    }
+
+    send(addr, buffer.data(), buffer.size());
+}
+
+Sp<Request>
+NetworkEngine::sendFindNode(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(my_v);
+    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(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(my_v);
+    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, const std::vector<Sp<Value>>& st)
+{
+    auto svals = serializeValues(st);
+    size_t total_size = 0;
+    for (const auto& v : svals)
+        total_size += v.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(std::string("o")); pk.pack(start);
+                    pk.pack(std::string("d")); 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(std::string("f")); pk.pack(fields);
+            pk.pack(std::string("v")); 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(my_v);
+    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(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(my_v);
+    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(my_v);
+    if (config.network) {
+        pk.pack(KEY_NETID); pk.pack(config.network);
+    }
+
+    send(addr, buffer.data(), buffer.size());
+}
+
+Sp<Request>
+NetworkEngine::sendAnnounceValue(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));
+
+    pk.pack(KEY_A); pk.pack_map((created < scheduler.time() ? 5 : 4));
+      pk.pack(KEY_REQ_ID);     pk.pack(myid);
+      pk.pack(KEY_REQ_H);      pk.pack(infohash);
+      auto v = packValueHeader(buffer, {value});
+      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_PUT);
+    pk.pack(KEY_TID); pk.pack(tid);
+    pk.pack(KEY_Y);   pk.pack(KEY_Q);
+    pk.pack(KEY_UA);  pk.pack(my_v);
+    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;
+}
+
+Sp<Request>
+NetworkEngine::sendUpdateValues(Sp<Node> n,
+                                const InfoHash& infohash,
+                                const std::vector<Sp<Value>>& values,
+                                time_point created,
+                                const Blob& token,
+                                const 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, values);
+      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(my_v);
+    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(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(my_v);
+    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(my_v);
+    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(my_v);
+    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 */
diff --git a/src/network_utils.cpp b/src/network_utils.cpp
new file mode 100644 (file)
index 0000000..b7e3588
--- /dev/null
@@ -0,0 +1,361 @@
+/*
+ *  Copyright (C) 2014-2020 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;
+#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");
+        }
+    }
+}
+
+}
+}
diff --git a/src/node.cpp b/src/node.cpp
new file mode 100644 (file)
index 0000000..d207cb6
--- /dev/null
@@ -0,0 +1,177 @@
+/*
+ *  Copyright (C) 2014-2020 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;
+
+    auto sock = std::make_shared<Socket>(std::move(cb));
+    auto s = sockets_.emplace(transaction_id, std::move(sock));
+    if (not s.second)
+        s.first->second = std::move(sock);
+    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::stringstream ss;
+    ss << (*this);
+    return ss.str();
+}
+
+std::ostream& operator<< (std::ostream& s, const Node& h)
+{
+    s << h.id << " " << h.addr.toString();
+    return s;
+}
+
+}
diff --git a/src/node_cache.cpp b/src/node_cache.cpp
new file mode 100644 (file)
index 0000000..dc25b38
--- /dev/null
@@ -0,0 +1,160 @@
+/*
+ *  Copyright (C) 2014-2020 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 {1024};
+constexpr size_t CLEANUP_FREQ {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;
+    }
+}
+
+}
diff --git a/src/op_cache.cpp b/src/op_cache.cpp
new file mode 100644 (file)
index 0000000..6c6ebb7
--- /dev/null
@@ -0,0 +1,270 @@
+/*
+ *  Copyright (C) 2014-2020 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 {
+            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, const Value::Filter& filter, const OnListen& onListen)
+{
+    // find exact match
+    auto op = getOp(q);
+    if (op == ops.end()) {
+        // New query
+        op = ops.emplace(q, std::unique_ptr<OpCache>(new 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, 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 {};
+}
+
+}
diff --git a/src/op_cache.h b/src/op_cache.h
new file mode 100644 (file)
index 0000000..5166815
--- /dev/null
@@ -0,0 +1,192 @@
+/*
+ *  Copyright (C) 2014-2020 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()};
+    OpCacheValueStorage(Sp<Value> val) : data(val) {}
+};
+
+class OpValueCache {
+public:
+    OpValueCache(ValueCallback&& cb) noexcept : callback(std::forward<ValueCallback>(cb)) {}
+    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;
+
+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, const Value::Filter& filter) {
+        auto cached = cache.get(filter);
+        if (not cached.empty() and not cb(cached, false)) {
+            return false;
+        }
+        listeners.emplace(token, LocalListener{q, 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 searchToken;
+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, const 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;
+
+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()};
+};
+
+}
diff --git a/src/parsed_message.h b/src/parsed_message.h
new file mode 100644 (file)
index 0000000..70de16d
--- /dev/null
@@ -0,0 +1,383 @@
+/*
+ *  Copyright (C) 2014-2020 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>
+
+namespace dht {
+namespace net {
+
+static const std::string KEY_Y {"y"};
+static const std::string KEY_R {"r"};
+static const std::string KEY_U {"u"};
+static const std::string KEY_E {"e"};
+static const std::string KEY_V {"p"};
+static const std::string KEY_TID {"t"};
+static const std::string KEY_UA {"v"};
+static const std::string KEY_NETID {"n"};
+static const std::string KEY_ISCLIENT {"s"};
+static const std::string KEY_Q {"q"};
+static const std::string KEY_A {"a"};
+
+static const std::string KEY_REQ_SID {"sid"};
+static const std::string KEY_REQ_ID {"id"};
+static const std::string KEY_REQ_H {"h"};
+static const std::string KEY_REQ_TARGET {"target"};
+static const std::string KEY_REQ_QUERY {"q"};
+static const std::string KEY_REQ_TOKEN {"token"};
+static const std::string KEY_REQ_VALUE_ID {"vid"};
+static const std::string KEY_REQ_NODES4 {"n4"};
+static const std::string KEY_REQ_NODES6 {"n6"};
+static const std::string KEY_REQ_CREATION {"c"};
+static const std::string KEY_REQ_ADDRESS {"sa"};
+static const std::string KEY_REQ_VALUES {"values"};
+static const std::string KEY_REQ_EXPIRED {"exp"};
+static const std::string KEY_REQ_REFRESHED {"re"};
+static const std::string KEY_REQ_FIELDS {"fileds"};
+static const std::string KEY_REQ_WANT {"w"};
+static const std::string KEY_VERSION {"ve"};
+
+static const std::string QUERY_PING {"ping"};
+static const std::string QUERY_FIND {"find"};
+static const std::string QUERY_GET {"get"};
+static const std::string QUERY_UPDATE {"update"};
+static const std::string QUERY_PUT {"put"};
+static const std::string QUERY_LISTEN {"listen"};
+static const std::string QUERY_REFRESH {"refresh"};
+
+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) {
+        //std::cout << "part " << e.first << ": " << e.second.second.size() << "/" << e.second.first << std::endl;
+        if (e.second.first > e.second.second.size())
+            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 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>();
+        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>();
+        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>() != "q")
+        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");
+            auto d = findMapValue(vdat.val, "d");
+            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>();
+        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")) {
+            auto vfields = rfields->as<std::set<Value::Field>>();
+            if (auto rvalues = findMapValue(*parsedReq.fields, "v")) {
+                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 */
diff --git a/src/peer_discovery.cpp b/src/peer_discovery.cpp
new file mode 100644 (file)
index 0000000..56ccad9
--- /dev/null
@@ -0,0 +1,414 @@
+/*
+ *  Copyright (C) 2014-2020 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>
+
+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> 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>() == "q")
+                    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;
+                    auto key = o.key.as<std::string>();
+                    ServiceDiscoveredCallback cb;
+                    {
+                        std::lock_guard<std::mutex> lck(dmtx_);
+                        if (drunning_) {
+                            auto callback = callbackmap_.find(key);
+                            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(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_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 */
diff --git a/src/request.h b/src/request.h
new file mode 100644 (file)
index 0000000..5c468c0
--- /dev/null
@@ -0,0 +1,153 @@
+/*
+ *  Copyright (C) 2014-2020 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(node), tid(tid), type(type), on_done(on_done), on_expired(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(node), tid(tid), type(type), on_done(on_done), on_error(on_error), on_expired(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  */
+}
diff --git a/src/rng.cpp b/src/rng.cpp
new file mode 100644 (file)
index 0000000..3ea47db
--- /dev/null
@@ -0,0 +1,117 @@
+/*
+ *  Copyright (C) 2014-2020 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;
+}
+
+}}
diff --git a/src/routing_table.cpp b/src/routing_table.cpp
new file mode 100644 (file)
index 0000000..e8b9ea6
--- /dev/null
@@ -0,0 +1,265 @@
+/*
+ *  Copyright (C) 2014-2020 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;
+}
+
+
+
+}
diff --git a/src/search.h b/src/search.h
new file mode 100644 (file)
index 0000000..b7a5ffc
--- /dev/null
@@ -0,0 +1,929 @@
+/*
+ *  Copyright (C) 2014-2020 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 {
+    /**
+     * 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 AnnounceStatus = std::map<Value::Id, std::pair<Sp<net::Request>, time_point>>;
+    /**
+     * 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> 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 */
+    AnnounceStatus 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;
+    }
+
+    /**
+     * 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 pending {false},
+             completed_sq_status {false},
+             pending_sq_status {false};
+        for (const auto& s : getStatus) {
+            if (s.second and s.second->pending())
+                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 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) {
+        auto l = listenStatus.find(q);
+        if (l != listenStatus.end()) {
+            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.end();
+    }
+    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.first)
+            return false;
+        return ack->second.first->completed();
+    }
+    void cancelAnnounce() {
+        for (const auto& status : acked) {
+            const auto& req = status.second.first;
+            if (req and req->pending()) {
+                node->cancelRequest(req);
+            }
+        }
+        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);
+        listenStatus.clear();
+    }
+    void cancelListen(const Sp<Query>& query) {
+        auto it = listenStatus.find(query);
+        if (it != listenStatus.end()) {
+            node->cancelRequest(it->second.req);
+            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.first) {
+            return time_point::min();
+        }
+        if (ack->second.first->completed()) {
+            return ack->second.second - REANNOUNCE_MARGIN;
+        }
+        return ack->second.first->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& get : callbacks) {
+            get.second.done_cb(false, {});
+            get.second.done_cb = {};
+        }
+        for (auto& put : announce) {
+            put.callback(false, {});
+            put.callback = {};
+        }
+    }
+
+    /**
+     * @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(), [&](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;
+
+    /**
+     * 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, const 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, 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){
+                    Sp<Query> query;
+                    const auto& ll = listeners.find(t);
+                    if (ll != listeners.cend()) {
+                        query = ll->second.query;
+                        listeners.erase(ll);
+                    }
+                    for (auto& sn : nodes) {
+                        if (listeners.empty())
+                            sn->cancelListen();
+                        else if (query)
+                            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.first)
+                    ackIt->second.first->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].first.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].first.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;
+        std::find_if(nodes.begin(), nodes.end(), [&count](const std::unique_ptr<SearchNode>& sn) {
+            if (not sn->node->isExpired())
+                return true;
+            ++count;
+            return false;
+        });
+        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.first)
+                        ackIt->second.first->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;
+}
+
+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;
+}
+
+}
diff --git a/src/securedht.cpp b/src/securedht.cpp
new file mode 100644 (file)
index 0000000..e4dfc22
--- /dev/null
@@ -0,0 +1,458 @@
+/*
+ *  Copyright (C) 2014-2020 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)
+: 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_->put(certId, Value {
+            CERTIFICATE_TYPE,
+            *certificate_,
+            1
+        }, [this, certId](bool ok) {
+            if (ok)
+                if (logger_)
+                    logger_->d(certId, "SecureDht: public key announced successfully");
+        }, {}, true);
+    }
+}
+
+SecureDht::~SecureDht() = default;
+
+ValueType
+SecureDht::secureType(ValueType&& type)
+{
+    type.storePolicy = [this,type](InfoHash id, Sp<Value>& v, const InfoHash& nid, const SockAddr& a) {
+        if (v->isSigned()) {
+            if (!v->signatureChecked) {
+                v->signatureChecked = true;
+                v->signatureValid = v->owner and v->owner->checkSignature(v->getToSign(), v->signature);
+            }
+            if (!v->signatureValid) {
+                if (logger_)
+                    logger_->w("Signature verification failed");
+                return false;
+            }
+        }
+        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 (!o->isSigned())
+            return type.editPolicy(id, o, n, nid, a);
+        if (o->owner != n->owner) {
+            if (logger_)
+                logger_->w("Edition forbidden: owner changed.");
+            return false;
+        }
+        if (!n->signatureChecked) {
+            n->signatureChecked = true;
+            n->signatureValid = o->owner and o->owner->checkSignature(n->getToSign(), n->signature);
+        }
+        if (!n->signatureValid) {
+            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;
+}
+
+const 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;
+}
+
+const Sp<const crypto::PublicKey>
+SecureDht::getPublicKey(const InfoHash& node) const
+{
+    if (node == getId())
+        return std::make_shared<crypto::PublicKey>(certificate_->getPublicKey());
+    auto it = nodesPubKeys_.find(node);
+    if (it == nodesPubKeys_.end())
+        return nullptr;
+    else
+        return it->second;
+}
+
+const 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) {
+        if (*found)
+            return false;
+        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 true;
+    }, [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<const 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 = std::make_shared<crypto::PublicKey>(crt->getPublicKey());
+            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 {};
+        }
+        if (v->decrypted) {
+            return v->decryptedValue;
+        }
+        v->decrypted = true;
+        try {
+            Value decrypted_val (decrypt(*v));
+            if (decrypted_val.recipient == getId()) {
+                if (decrypted_val.owner)
+                    nodesPubKeys_[decrypted_val.owner->getId()] = decrypted_val.owner;
+                v->decryptedValue = std::make_shared<Value>(std::move(decrypted_val));
+                return v->decryptedValue;
+            }
+            // Ignore values belonging to other people
+        } 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()) {
+        if (v->signatureChecked) {
+            return v->signatureValid ? v : Sp<Value>{};
+        }
+        v->signatureChecked = true;
+        if (v->owner and v->owner->checkSignature(v->getToSign(), v->signature)) {
+            v->signatureValid = true;
+            if (enableCache_)
+                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_)  {
+        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),
+        std::move(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, [=](const Sp<const 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::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;
+}
+
+}
diff --git a/src/storage.h b/src/storage.h
new file mode 100644 (file)
index 0000000..c41740f
--- /dev/null
@@ -0,0 +1,298 @@
+/*
+ *  Copyright (C) 2014-2020 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 size = value.size();
+        totalSize_ -= size;
+        auto range = storedValues_.equal_range(expiration);
+        for (auto rit = range.first; rit != range.second;) {
+            if (rit->second.first == id && rit->second.second == value.id) {
+                storedValues_.erase(rit);
+                break;
+            } else
+                ++rit;
+        }
+    }
+    size_t size() const { return totalSize_; }
+    std::pair<InfoHash, Value::Id> getOldest() const { return 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 {};
+    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 {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;
+    };
+
+    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
+     */
+    time_point refresh(const time_point& now, const Value::Id& vid, const TypeStore& types) {
+        for (auto& vs : values)
+            if (vs.data->id == vid) {
+                vs.created = now;
+                vs.expiration = std::max(vs.expiration, now + types.getType(vs.data->type).expiration);
+                return vs.expiration;
+            }
+        return time_point::max();
+    }
+
+    size_t listen(ValueCallback& cb, Value::Filter& f, const Sp<Query>& q);
+
+    void cancelListen(size_t token) {
+        local_listeners.erase(token);
+    }
+
+    StoreDiff 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;
+        size_t size_old = it->data->size();
+        ssize_t size_diff = size_new - (ssize_t)size_old;
+        if (it->data != value) {
+            //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});
+        }
+        return std::make_pair(nullptr, StoreDiff{});
+    } 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});
+        }
+        return std::make_pair(nullptr, StoreDiff{});
+    }
+}
+
+Storage::StoreDiff
+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);
+    total_size -= size;
+    values.erase(it);
+    return {-size, -1, 0};
+}
+
+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};
+}
+
+std::pair<ssize_t, std::vector<Sp<Value>>>
+Storage::expire(const InfoHash& id, time_point now)
+{
+    // expire listeners
+    ssize_t del_listen {0};
+    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);
+            del_listen--;
+        }
+        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 {};
+    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);
+        ret.emplace_back(std::move(v.data));
+    });
+    total_size += size_diff;
+    values.erase(r, values.end());
+    return {size_diff, std::move(ret)};
+}
+
+}
diff --git a/src/thread_pool.cpp b/src/thread_pool.cpp
new file mode 100644 (file)
index 0000000..fb56365
--- /dev/null
@@ -0,0 +1,174 @@
+/*
+ *  Copyright (C) 2014-2020 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
+
+namespace dht {
+
+constexpr const size_t IO_THREADS_MAX {64};
+
+struct ThreadPool::ThreadState
+{
+    std::thread thread {};
+    std::atomic_bool run {true};
+};
+
+ThreadPool&
+ThreadPool::computation()
+{
+    static ThreadPool pool;
+    return pool;
+}
+
+ThreadPool&
+ThreadPool::io()
+{
+    static ThreadPool pool(IO_THREADS_MAX);
+    return pool;
+}
+
+
+ThreadPool::ThreadPool(size_t maxThreads) : maxThreads_(maxThreads)
+{
+    threads_.reserve(maxThreads_);
+}
+
+ThreadPool::ThreadPool()
+ : ThreadPool(std::max<size_t>(std::thread::hardware_concurrency(), 4))
+{}
+
+ThreadPool::~ThreadPool()
+{
+    join();
+}
+
+void
+ThreadPool::run(std::function<void()>&& cb)
+{
+    std::unique_lock<std::mutex> l(lock_);
+    if (not running_) return;
+
+    // launch new thread if necessary
+    if (not readyThreads_ && threads_.size() < maxThreads_) {
+        threads_.emplace_back(new ThreadState());
+        auto& t = *threads_.back();
+        t.thread = std::thread([&]() {
+            while (t.run) {
+                std::function<void()> task;
+
+                // pick task from queue
+                {
+                    std::unique_lock<std::mutex> l(lock_);
+                    readyThreads_++;
+                    cv_.wait(l, [&](){
+                        return not t.run or not tasks_.empty();
+                    });
+                    readyThreads_--;
+                    if (not t.run)
+                        break;
+                    task = std::move(tasks_.front());
+                    tasks_.pop();
+                }
+
+                // run task
+                try {
+                    if (task)
+                        task();
+                } catch (const std::exception& e) {
+                    // LOG_ERR("Exception running task: %s", e.what());
+                    std::cerr << "Exception running task: " << e.what() << std::endl;
+                }
+            }
+        });
+    }
+
+    // push task to queue
+    tasks_.emplace(std::move(cb));
+
+    // notify thread
+    cv_.notify_one();
+}
+
+void
+ThreadPool::stop()
+{
+    {
+        std::lock_guard<std::mutex> l(lock_);
+        running_ = false;
+    }
+    for (auto& t : threads_)
+        t->run = false;
+    cv_.notify_all();
+}
+
+void
+ThreadPool::join()
+{
+    stop();
+    for (auto& t : threads_)
+        t->thread.join();
+    threads_.clear();
+}
+
+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] {
+        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();
+    }
+}
+
+}
diff --git a/src/utils.cpp b/src/utils.cpp
new file mode 100644 (file)
index 0000000..be69eac
--- /dev/null
@@ -0,0 +1,292 @@
+/*
+ *  Copyright (C) 2014-2020 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 {
+
+static constexpr std::array<uint8_t, 12> MAPPED_IPV4_PREFIX {{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff}};
+
+const char* version() {
+    return PACKAGE_VERSION;
+}
+
+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));
+}
+
+std::string
+print_addr(const sockaddr* sa, socklen_t slen)
+{
+    char hbuf[NI_MAXHOST];
+    char sbuf[NI_MAXSERV];
+    std::stringstream out;
+    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]";
+    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;
+}
+
+}
diff --git a/src/value.cpp b/src/value.cpp
new file mode 100644 (file)
index 0000000..6d50412
--- /dev/null
@@ -0,0 +1,586 @@
+/*
+ *  Copyright (C) 2014-2020 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<const 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<const 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
+
+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(const std::string& q_str) {
+    std::istringstream q_iss {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(const std::string& q_str) {
+    std::istringstream q_iss {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"); /* unpacking filters */
+       if (rfilters)
+        where.msgpack_unpack(*rfilters);
+       else
+               throw msgpack::type_error();
+
+       auto rfield_selector = findMapValue(o, "s"); /* unpacking field selectors */
+       if (rfield_selector)
+        select.msgpack_unpack(*rfield_selector);
+       else
+               throw msgpack::type_error();
+}
+
+template <typename T>
+bool subset(std::vector<T> fds, std::vector<T> qfds)
+{
+    for (auto& fd : fds) {
+        if (std::find_if(qfds.begin(), qfds.end(), [&fd](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;
+}
+
+
+}
diff --git a/src/value_cache.h b/src/value_cache.h
new file mode 100644 (file)
index 0000000..b168ee6
--- /dev/null
@@ -0,0 +1,212 @@
+/*
+ *  Copyright (C) 2014-2020 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:
+    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 (const auto& v : values)
+            expired_values.emplace_back(std::move(v.second.data));
+        values.clear();
+        CallbackQueue ret;
+        if (not expired_values.empty() and callback) {
+            auto cb = callback;
+            ret.emplace_back([expired_values, cb]{
+                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) {
+            auto cb = callback;
+            ret.emplace_back([cb, expired_values]{
+                if (cb) cb(expired_values, true);
+            });
+        }
+        return ret;
+    }
+
+    time_point onValues
+        (const std::vector<Sp<Value>>& 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 values.empty())
+            cbs.splice(cbs.end(), addValues(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);
+        }
+    }
+
+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;
+            }
+        }
+        auto cb = callback;
+        CallbackQueue ret;
+        if (not nvals.empty())
+            ret.emplace_back([cb, nvals]{
+                if (cb) 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;
+    }
+};
+
+}
diff --git a/tests/Makefile.am b/tests/Makefile.am
new file mode 100644 (file)
index 0000000..13ea421
--- /dev/null
@@ -0,0 +1,9 @@
+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
diff --git a/tests/cryptotester.cpp b/tests/cryptotester.cpp
new file mode 100644 (file)
index 0000000..bf092c8
--- /dev/null
@@ -0,0 +1,168 @@
+/*
+ *  Copyright (C) 2014-2020 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();
+    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));
+
+    // 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::tearDown() {
+
+}
+}  // namespace test
diff --git a/tests/cryptotester.h b/tests/cryptotester.h
new file mode 100644 (file)
index 0000000..04ce97c
--- /dev/null
@@ -0,0 +1,64 @@
+/*
+ *  Copyright (C) 2014-2020 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_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();
+};
+
+}  // namespace test
diff --git a/tests/dhtproxytester.cpp b/tests/dhtproxytester.cpp
new file mode 100644 (file)
index 0000000..5036c12
--- /dev/null
@@ -0,0 +1,292 @@
+/*
+ *  Copyright (C) 2014-2020 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, 5s, [&]{ 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);
+}
+
+}  // namespace test
diff --git a/tests/dhtproxytester.h b/tests/dhtproxytester.h
new file mode 100644 (file)
index 0000000..6937c3e
--- /dev/null
@@ -0,0 +1,79 @@
+/*
+ *  Copyright (C) 2014-2020 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_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();
+
+ 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
diff --git a/tests/dhtrunnertester.cpp b/tests/dhtrunnertester.cpp
new file mode 100644 (file)
index 0000000..9f3d63c
--- /dev/null
@@ -0,0 +1,217 @@
+/*
+ *  Copyright (C) 2014-2020 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 <chrono>
+#include <mutex>
+#include <condition_variable>
+using namespace std::chrono_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;
+
+    node1.run(42222, config);
+    node2.run(42232, 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, 5s, [&]{ return done == 2; }));
+    node1.join();
+    node2.join();
+}
+
+void
+DhtRunnerTester::testConstructors() {
+    CPPUNIT_ASSERT(node1.getBoundPort() == 42222);
+    CPPUNIT_ASSERT(node2.getBoundPort() == 42232);
+}
+
+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 valueCount(0);
+    unsigned putCount(0);
+    unsigned putOkCount(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::shared_ptr<dht::Value>&) {
+        valueCount++;
+        return true;
+    });
+
+    auto ftokenb = node1.listen(b, [&](const std::shared_ptr<dht::Value>&) {
+        valueCount++;
+        return false;
+    });
+
+    auto ftokenc = node1.listen(c, [&](const std::shared_ptr<dht::Value>&) {
+        valueCount++;
+        return true;
+    });
+
+    auto ftokend = node1.listen(d, [&](const std::shared_ptr<dht::Value>&) {
+        valueCount++;
+        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) putOkCount++;
+            cv.notify_all();
+        });
+        node2.put(b, dht::Value("v2"), [&](bool ok) {
+            std::lock_guard<std::mutex> lock(mutex);
+            putCount++;
+            if (ok) putOkCount++;
+            cv.notify_all();
+        });
+        auto bigVal = std::make_shared<dht::Value>();
+        bigVal->data = mtu;
+        node2.put(c, bigVal, [&](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 * 3u; }));
+        CPPUNIT_ASSERT_EQUAL(N * 3u, putOkCount);
+    }
+
+    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 * 2u + 1u, valueCount.load());
+
+    node1.cancelListen(a, tokena);
+    node1.cancelListen(b, std::move(ftokenb));
+    node1.cancelListen(c, tokenc);
+    node1.cancelListen(d, tokend);
+}
+
+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 = 50;
+
+    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());
+}
+
+}  // namespace test
diff --git a/tests/dhtrunnertester.h b/tests/dhtrunnertester.h
new file mode 100644 (file)
index 0000000..1d23382
--- /dev/null
@@ -0,0 +1,67 @@
+/*
+ *  Copyright (C) 2014-2020 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_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 listen method with lot of datas
+     */
+    void testListenLotOfBytes();
+};
+
+}  // namespace test
diff --git a/tests/httptester.cpp b/tests/httptester.cpp
new file mode 100644 (file)
index 0000000..2f2dc41
--- /dev/null
@@ -0,0 +1,289 @@
+/*
+ *  Copyright (C) 2014-2020 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);
+}
+
+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
diff --git a/tests/httptester.h b/tests/httptester.h
new file mode 100644 (file)
index 0000000..1f50af9
--- /dev/null
@@ -0,0 +1,95 @@
+/*
+ *  Copyright (C) 2014-2020 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
diff --git a/tests/infohashtester.cpp b/tests/infohashtester.cpp
new file mode 100644 (file)
index 0000000..6245c93
--- /dev/null
@@ -0,0 +1,151 @@
+/*
+ *  Copyright (C) 2014-2020 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
diff --git a/tests/infohashtester.h b/tests/infohashtester.h
new file mode 100644 (file)
index 0000000..75d9f40
--- /dev/null
@@ -0,0 +1,74 @@
+/*
+ *  Copyright (C) 2014-2020 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
diff --git a/tests/peerdiscoverytester.cpp b/tests/peerdiscoverytester.cpp
new file mode 100644 (file)
index 0000000..3f36e44
--- /dev/null
@@ -0,0 +1,114 @@
+/*
+ *  Copyright (C) 2014-2020 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
diff --git a/tests/peerdiscoverytester.h b/tests/peerdiscoverytester.h
new file mode 100644 (file)
index 0000000..f5bef85
--- /dev/null
@@ -0,0 +1,50 @@
+/*
+ *  Copyright (C) 2014-2020 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
diff --git a/tests/tests_runner.cpp b/tests/tests_runner.cpp
new file mode 100644 (file)
index 0000000..dc06c70
--- /dev/null
@@ -0,0 +1,35 @@
+/*
+ *  Copyright (C) 2014-2020 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 &registry = 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;
+}
diff --git a/tests/threadpooltester.cpp b/tests/threadpooltester.cpp
new file mode 100644 (file)
index 0000000..4295b87
--- /dev/null
@@ -0,0 +1,90 @@
+/*
+ *  Copyright (C) 2014-2020 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>
+
+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(count.load() == N);
+}
+
+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;
+    std::atomic_uint 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.load() != 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.load());
+    CPPUNIT_ASSERT_EQUAL(N, count4.load());
+    CPPUNIT_ASSERT_EQUAL(N, count8.load());
+}
+
+void
+ThreadPoolTester::tearDown() {
+}
+
+}  // namespace test
diff --git a/tests/threadpooltester.h b/tests/threadpooltester.h
new file mode 100644 (file)
index 0000000..4dc289c
--- /dev/null
@@ -0,0 +1,48 @@
+/*
+ *  Copyright (C) 2014-2020 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_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();
+};
+
+}  // namespace test
diff --git a/tests/valuetester.cpp b/tests/valuetester.cpp
new file mode 100644 (file)
index 0000000..784bda4
--- /dev/null
@@ -0,0 +1,83 @@
+/*
+ *  Copyright (C) 2014-2020 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
diff --git a/tests/valuetester.h b/tests/valuetester.h
new file mode 100644 (file)
index 0000000..f14449d
--- /dev/null
@@ -0,0 +1,53 @@
+/*
+ *  Copyright (C) 2014-2020 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
diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt
new file mode 100644 (file)
index 0000000..2e70c3b
--- /dev/null
@@ -0,0 +1,69 @@
+if (OPENDHT_SHARED)
+    set (OPENDHT_LIBS opendht)
+else ()
+    set (OPENDHT_LIBS 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})
+    target_link_libraries (${name} LINK_PUBLIC ${READLINE_LIBRARIES})
+    target_link_libraries (${name} LINK_PUBLIC ${OPENDHT_LIBS})
+    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)
+    target_link_libraries (dhtcnode LINK_PUBLIC opendht-c ${READLINE_LIBRARIES})
+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)
+        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()
+    string(REGEX REPLACE "[ \t\n]+" "" SYSTEMD_UNIT_INSTALL_DIR "${SYSTEMD_UNIT_INSTALL_DIR}")
+    set (systemdunitdir "${SYSTEMD_UNIT_INSTALL_DIR}")
+
+    configure_file (
+        systemd/dhtnode.service.in
+        systemd/dhtnode.service
+        @ONLY
+    )
+    install (FILES ${CMAKE_CURRENT_BINARY_DIR}/systemd/dhtnode.service DESTINATION ${systemdunitdir})
+    install (FILES systemd/dhtnode.conf DESTINATION ${sysconfdir})
+    if (OPENDHT_PYTHON)
+        configure_file (
+            systemd/dhtcluster.service.in
+            systemd/dhtcluster.service
+            @ONLY
+        )
+        install (FILES ${CMAKE_CURRENT_BINARY_DIR}/systemd/dhtcluster.service DESTINATION ${systemdunitdir})
+        install (FILES systemd/dhtcluster.conf DESTINATION ${sysconfdir})
+    endif()
+endif ()
diff --git a/tools/Makefile.am b/tools/Makefile.am
new file mode 100644 (file)
index 0000000..a9c4b18
--- /dev/null
@@ -0,0 +1,13 @@
+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 -L@top_builddir@/src/.libs @Argon2_LDFLAGS@ @GnuTLS_LIBS@
+
+dhtchat_SOURCES = dhtchat.cpp
+dhtchat_LDFLAGS = -lopendht -lreadline -L@top_builddir@/src/.libs @Argon2_LDFLAGS@ @GnuTLS_LIBS@
+
+dhtscanner_SOURCES = dhtscanner.cpp
+dhtscanner_LDFLAGS = -lopendht -lreadline -L@top_builddir@/src/.libs @Argon2_LDFLAGS@ @GnuTLS_LIBS@
diff --git a/tools/dhtchat.cpp b/tools/dhtchat.cpp
new file mode 100644 (file)
index 0000000..66d455c
--- /dev/null
@@ -0,0 +1,151 @@
+/*
+ *  Copyright (C) 2014-2020 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;
+}
diff --git a/tools/dhtcnode.c b/tools/dhtcnode.c
new file mode 100644 (file)
index 0000000..ab3a4b8
--- /dev/null
@@ -0,0 +1,125 @@
+#include <c/opendht_c.h>
+
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <arpa/inet.h>
+
+struct op_context {
+    dht_runner* runner;
+    int d;
+};
+
+bool dht_value_callback(const dht_value* value, bool expired, void* user_data)
+{
+    dht_data_view data = dht_value_get_data(value);
+    printf("Value callback %s: %.*s.\n", expired ? "expired" : "new", (int)data.size, data.data);
+    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_done_callback(bool ok, void* user_data)
+{
+    dht_runner* runner = (dht_runner*)user_data;
+    printf("Done callback. %s\n", ok ? "Success !" : "Failure :-(");
+}
+
+void op_context_free(void* user_data)
+{
+    struct op_context* ctx = (struct op_context*)user_data;
+    printf("op_context_free %d.\n", ctx->d);
+    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;
+}
+
+int main()
+{
+    dht_identity id = dht_identity_generate("testNode", NULL);
+    dht_infohash cert_id = dht_certificate_get_id(id.certificate);
+    printf("Cert ID: %s\n", dht_infohash_print(&cert_id));
+
+    dht_publickey* pk = dht_certificate_get_publickey(id.certificate);
+    dht_infohash pk_id = dht_publickey_get_id(pk);
+    printf("PK ID: %s\n", dht_infohash_print(&pk_id));
+    dht_publickey_delete(pk);
+
+    pk = dht_privatekey_get_publickey(id.privatekey);
+    pk_id = dht_publickey_get_id(pk);
+    printf("Key ID: %s\n", dht_infohash_print(&pk_id));
+    dht_publickey_delete(pk);
+
+    dht_identity_delete(&id);
+
+    dht_runner* runner = dht_runner_new();
+    dht_runner_run(runner, 4040);
+
+    dht_infohash h;
+    dht_infohash_random(&h);
+
+    printf("random hash: %s\n", dht_infohash_print(&h));
+
+    // Put data
+    const char* data_str = "yo, this is some data";
+    dht_value* val = dht_value_new(data_str, strlen(data_str));
+    dht_runner_put(runner, &h, val, dht_done_callback, runner, false);
+    dht_value_unref(val);
+
+    // Get data
+    dht_runner_get(runner, &h, dht_get_callback, dht_done_callback, runner);
+
+    // Listen for data
+    struct op_context* ctx = malloc(sizeof(struct op_context));
+    ctx->runner = runner;
+    ctx->d = 42;
+    dht_op_token* token = dht_runner_listen(runner, &h, dht_value_callback, op_context_free, ctx);
+
+    sleep(1);
+
+    dht_runner_bootstrap(runner, "bootstrap.jami.net", NULL);
+
+    sleep(2);
+
+    struct sockaddr** addrs = dht_runner_get_public_address(runner);
+    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);
+
+    dht_runner_cancel_listen(runner, &h, token);
+    dht_op_token_delete(token);
+
+    dht_runner_delete(runner);
+    return 0;
+}
diff --git a/tools/dhtnode.cpp b/tools/dhtnode.cpp
new file mode 100644 (file)
index 0000000..8b8adb7
--- /dev/null
@@ -0,0 +1,597 @@
+/*
+ *  Copyright (C) 2014-2020 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]" << 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 << "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);
+            node->get(id, [start](const std::vector<std::shared_ptr<Value>>& values) {
+                auto now = std::chrono::high_resolution_clock::now();
+                std::cout << "Get: found " << values.size() << " value(s) after " << print_duration(now-start) << std::endl;
+                for (const auto& value : values)
+                    std::cout << "\t" << *value << std::endl;
+                return true;
+            }, [start](bool ok) {
+                auto end = std::chrono::high_resolution_clock::now();
+                std::cout << "Get: " << (ok ? "completed" : "failure") << ", took " << print_duration(end-start) << 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 token = node->listen(id, [](const std::vector<std::shared_ptr<Value>>& values, bool expired) {
+                std::cout << "Listen: found " << values.size() << " values" << (expired ? " expired" : "") << 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: " << v->second << "]" << 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;
+            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;
+}
diff --git a/tools/dhtproxy_stats.py b/tools/dhtproxy_stats.py
new file mode 100644 (file)
index 0000000..fa0e302
--- /dev/null
@@ -0,0 +1,54 @@
+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
diff --git a/tools/dhtscanner.cpp b/tools/dhtscanner.cpp
new file mode 100644 (file)
index 0000000..6600639
--- /dev/null
@@ -0,0 +1,130 @@
+/*
+ *  Copyright (C) 2014-2020 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;
+}
diff --git a/tools/durl.cpp b/tools/durl.cpp
new file mode 100644 (file)
index 0000000..368b0ef
--- /dev/null
@@ -0,0 +1,124 @@
+/*
+ *  Copyright (C) 2014-2020 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;
+}
diff --git a/tools/perftest.cpp b/tools/perftest.cpp
new file mode 100644 (file)
index 0000000..59faf7a
--- /dev/null
@@ -0,0 +1,180 @@
+/*
+ *  Copyright (C) 2014-2020 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;
+}
diff --git a/tools/proxy_loadtester.py b/tools/proxy_loadtester.py
new file mode 100644 (file)
index 0000000..b6e555f
--- /dev/null
@@ -0,0 +1,82 @@
+# Copyright (C) 2014-2020 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/")
diff --git a/tools/proxy_node.html b/tools/proxy_node.html
new file mode 100644 (file)
index 0000000..13c94d6
--- /dev/null
@@ -0,0 +1,196 @@
+<!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>
diff --git a/tools/systemd/dhtcluster.conf b/tools/systemd/dhtcluster.conf
new file mode 100644 (file)
index 0000000..7311d6d
--- /dev/null
@@ -0,0 +1 @@
+DHT_ARGS=-b bootstrap.ring.cx -p 4222 -n 16
\ No newline at end of file
diff --git a/tools/systemd/dhtcluster.service.in b/tools/systemd/dhtcluster.service.in
new file mode 100644 (file)
index 0000000..5909349
--- /dev/null
@@ -0,0 +1,20 @@
+[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
diff --git a/tools/systemd/dhtnode.conf b/tools/systemd/dhtnode.conf
new file mode 100644 (file)
index 0000000..99db362
--- /dev/null
@@ -0,0 +1 @@
+DHT_ARGS=-b bootstrap.jami.net -p 4222 -v
\ No newline at end of file
diff --git a/tools/systemd/dhtnode.service.in b/tools/systemd/dhtnode.service.in
new file mode 100644 (file)
index 0000000..0cf50e9
--- /dev/null
@@ -0,0 +1,44 @@
+[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
diff --git a/tools/tools_common.h b/tools/tools_common.h
new file mode 100644 (file)
index 0000000..dfe6bd3
--- /dev/null
@@ -0,0 +1,457 @@
+/*
+ *  Copyright (C) 2014-2020 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
+
+#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 pushserver {};
+    std::string devicekey {};
+    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.WARN("Connectivity changed: IPv4: %s, IPv6: %s", dht::statusToStr(status4), dht::statusToStr(status6));
+        };
+    }
+    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-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'},
+    {"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, "hidsvDUPp: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 '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 '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
+}