Compare commits

..
Author SHA1 Message Date
Cyrille BéraudandCopilot 9e3d8c0200 proxy server: only send call pushes at high priority
Replace the priority-only heuristic in handlePushListen
(highPriority = !expired && minPriority == 0) with isHighPriorityPush,
so the proxy promotes a notification to high (urgent) push priority
only for real-time call signaling.

Previously every value published at the default priority 0 — including
the frequent connection-request refreshes that carry an empty
pushType — triggered a high-priority push. On mobile this floods the
platform's adaptive high-priority quota; once tripped, the platform
silently drops genuine call pushes, so incoming calls fail to ring
while the device is in the background.

Restricting high priority to audioCall/videoCall keeps calls waking the
device while the surrounding presence/sync noise is delivered at normal
priority. This is a strict subset of the previous high-priority set, so
no notification that was normal before becomes high.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-18 07:51:31 -04:00
Cyrille BéraudandCopilot e0ecf06f52 proxy server: add testable high-priority push decision
Extract the decision of whether a push-listen notification must be
delivered at high (urgent) FCM/APNs priority into a static, pure
helper (isHighPriorityPush) so it can be unit-tested without a push
gateway.

Only real-time call signaling — values whose pushType is "audioCall"
or "videoCall", published at high priority (priority == 0) — warrants
an urgent push. Presence/connection-request refreshes (empty
pushType), non-call types and expirations are not urgent.

The helper is not wired in yet; this commit only adds it and its
tests, keeping the current handlePushListen behavior unchanged.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-18 07:51:22 -04:00
51 changed files with 437 additions and 4729 deletions

No files matched your search

+9 -9
View File
@@ -7,7 +7,7 @@ env:
UBUNTU_FULL_DEPS: "libncurses5-dev libreadline-dev nettle-dev libasio-dev libgnutls28-dev libuv1-dev python3-pip python3-dev python3-setuptools libcppunit-dev libjsoncpp-dev libfmt-dev libmsgpack-dev libargon2-dev"
UBUNTU_MESON_DEPS: "ninja-build libncurses5-dev libreadline-dev nettle-dev libasio-dev libgnutls28-dev libuv1-dev python3-dev python3-setuptools python3-pip libcppunit-dev libjsoncpp-dev libfmt-dev libmsgpack-dev libargon2-dev"
UBUNTU_MINIMAL_DEPS: "libncurses5-dev libreadline-dev nettle-dev libfmt-dev libgnutls28-dev libcppunit-dev libmsgpack-dev libargon2-dev"
DEBIAN13_DEPS: "git libtool pkg-config wget cmake libncurses5-dev libreadline-dev nettle-dev libasio-dev libgnutls28-dev libuv1-dev python3-pip python3-dev python3-setuptools libcppunit-dev libjsoncpp-dev libfmt-dev libmsgpack-dev libargon2-dev"
DEBIAN12_DEPS: "git libtool pkg-config wget cmake libncurses5-dev libreadline-dev nettle-dev libasio-dev libgnutls28-dev libuv1-dev python3-pip python3-dev python3-setuptools libcppunit-dev libjsoncpp-dev libfmt-dev libmsgpack-dev libargon2-dev"
on: [push, pull_request]
@@ -16,7 +16,7 @@ jobs:
name: Ubuntu/GCC Meson build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
- name: Install Dependencies
run: |
@@ -40,7 +40,7 @@ jobs:
matrix:
cxx_standard: [17, 20, 23]
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
- name: Install Minimal Dependencies
run: |
@@ -65,12 +65,12 @@ jobs:
- name: Run Unit tests
run: ctest --output-on-failure --test-dir build
build-debian13:
name: Debian 13/GCC build
build-debian12:
name: Debian 12/GCC build
runs-on: ubuntu-latest
container:
image: debian:13
image: debian:12
volumes:
- .:/workspace
options: --workdir /workspace
@@ -78,12 +78,12 @@ jobs:
DEBIAN_FRONTEND: noninteractive
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
- name: Install Dependencies
run: |
apt-get update
apt-get install -y $DEBIAN13_DEPS
apt-get install -y $DEBIAN12_DEPS
- name: Build llhttp dependency
run: |
@@ -131,7 +131,7 @@ jobs:
matrix:
cxx_standard: [17, 20, 23]
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
- name: Install Homebrew Dependencies
run: brew install msgpack-cxx asio gnutls nettle readline fmt jsoncpp argon2 openssl cppunit
+1 -1
View File
@@ -6,7 +6,7 @@ jobs:
name: Clang Static Analysis
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
- name: deps
run: |
sudo apt update && \
+1 -1
View File
@@ -27,7 +27,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v7
uses: actions/checkout@v6
- name: Initialize CodeQL
uses: github/codeql-action/init@v4
+6 -6
View File
@@ -16,14 +16,14 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, ubuntu-24.04-arm, macos-15]
os: [ubuntu-latest, ubuntu-24.04-arm, macos-latest]
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
# Run locally with: pipx run cibuildwheel --platform linux python
- name: Build wheels
uses: pypa/cibuildwheel@v4.1.1
uses: pypa/cibuildwheel@v3.4.1
with:
package-dir: python
@@ -36,7 +36,7 @@ jobs:
name: Build wheels on Windows
runs-on: windows-latest
env:
VCPKG_VERSION: "2026.06.01"
VCPKG_VERSION: "2025.07.25"
VCPKG_INSTALLATION_ROOT: "C:\\vcpkg"
VCPKG_DISABLE_METRICS: "1"
VCPKG_DEFAULT_TRIPLET: x64-windows-release
@@ -44,11 +44,11 @@ jobs:
VCPKG_DEFAULT_BINARY_CACHE: ${{ github.workspace }}\vcpkg_binary_cache
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
- name: Cache vcpkg installed packages
id: cache-vcpkg
uses: actions/cache@v6
uses: actions/cache@v5
with:
path: |
${{ env.VCPKG_INSTALLED_DIR }}
+14 -14
View File
@@ -25,7 +25,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v7
uses: actions/checkout@v6
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
@@ -66,7 +66,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v7
uses: actions/checkout@v6
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
@@ -107,7 +107,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v7
uses: actions/checkout@v6
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
@@ -148,7 +148,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v7
uses: actions/checkout@v6
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
@@ -190,7 +190,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v7
uses: actions/checkout@v6
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
@@ -231,7 +231,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v7
uses: actions/checkout@v6
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
@@ -273,7 +273,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v7
uses: actions/checkout@v6
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
@@ -309,7 +309,7 @@ jobs:
name: Build Windows MSI Installer
runs-on: windows-latest
env:
VCPKG_VERSION: "2026.06.01"
VCPKG_VERSION: "2025.07.25"
VCPKG_INSTALLATION_ROOT: "C:\\vcpkg"
VCPKG_DISABLE_METRICS: "1"
VCPKG_DEFAULT_TRIPLET: x64-windows
@@ -319,7 +319,7 @@ jobs:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v7
uses: actions/checkout@v6
with:
submodules: recursive
@@ -334,7 +334,7 @@ jobs:
- name: Cache vcpkg installed packages
id: cache-vcpkg
uses: actions/cache@v6
uses: actions/cache@v5
with:
path: |
${{ env.VCPKG_INSTALLED_DIR }}
@@ -413,7 +413,7 @@ jobs:
name: Build Windows dhtnode
runs-on: windows-latest
env:
VCPKG_VERSION: "2026.06.01"
VCPKG_VERSION: "2025.07.25"
VCPKG_INSTALLATION_ROOT: "C:\\vcpkg"
VCPKG_DISABLE_METRICS: "1"
VCPKG_DEFAULT_TRIPLET: x64-windows-static-release
@@ -423,7 +423,7 @@ jobs:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v7
uses: actions/checkout@v6
with:
submodules: recursive
@@ -432,7 +432,7 @@ jobs:
- name: Cache vcpkg static installed packages
id: cache-vcpkg-static
uses: actions/cache@v6
uses: actions/cache@v5
with:
path: |
${{ env.VCPKG_INSTALLED_DIR }}
@@ -499,7 +499,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v7
uses: actions/checkout@v6
- name: Download Windows MSI artifact
uses: actions/download-artifact@v8
+10 -10
View File
@@ -9,7 +9,7 @@ on:
branches: [ main, master ]
env:
VCPKG_VERSION: "2026.06.01"
VCPKG_VERSION: "2026.01.16"
VCPKG_INSTALLATION_ROOT: "C:\\vcpkg"
VCPKG_DISABLE_METRICS: "1"
VCPKG_INSTALLED_DIR: ${{ github.workspace }}\vcpkg_installed
@@ -27,7 +27,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v7
uses: actions/checkout@v6
- name: Set up MSVC
uses: microsoft/setup-msbuild@v3
@@ -41,7 +41,7 @@ jobs:
- name: Cache vcpkg installed packages
id: cache-vcpkg
uses: actions/cache@v6
uses: actions/cache@v5
with:
path: |
${{ env.VCPKG_INSTALLED_DIR }}
@@ -75,7 +75,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v7
uses: actions/checkout@v6
- name: Set up MSVC
uses: microsoft/setup-msbuild@v3
@@ -89,7 +89,7 @@ jobs:
- name: Cache vcpkg static installed packages
id: cache-vcpkg-static
uses: actions/cache@v6
uses: actions/cache@v5
with:
path: |
${{ env.VCPKG_INSTALLED_DIR }}
@@ -125,14 +125,14 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v7
uses: actions/checkout@v6
- name: Set up MSVC
uses: microsoft/setup-msbuild@v3
- name: Restore vcpkg installed packages
id: cache-vcpkg-restore
uses: actions/cache/restore@v6
uses: actions/cache/restore@v5
with:
path: |
${{ env.VCPKG_INSTALLED_DIR }}
@@ -202,14 +202,14 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v7
uses: actions/checkout@v6
- name: Set up MSVC
uses: microsoft/setup-msbuild@v3
- name: Restore vcpkg static installed packages
id: cache-vcpkg-static-restore
uses: actions/cache/restore@v6
uses: actions/cache/restore@v5
with:
path: |
${{ env.VCPKG_INSTALLED_DIR }}
@@ -275,7 +275,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v7
uses: actions/checkout@v6
- name: Setup MinGW
uses: msys2/setup-msys2@v2
+2 -6
View File
@@ -1,4 +1,4 @@
cmake_minimum_required (VERSION 3.21...4.4)
cmake_minimum_required (VERSION 3.21...3.31)
if(POLICY CMP0091)
cmake_policy(SET CMP0091 NEW)
endif()
@@ -10,7 +10,7 @@ if(POLICY CMP0167)
endif()
set (opendht_VERSION_MAJOR 4)
set (opendht_VERSION_MINOR 3.0)
set (opendht_VERSION_MINOR 0.0)
set (opendht_VERSION ${opendht_VERSION_MAJOR}.${opendht_VERSION_MINOR})
set (PACKAGE_VERSION ${opendht_VERSION})
set (VERSION "${opendht_VERSION}")
@@ -870,10 +870,6 @@ if (BUILD_TESTING)
tests/test_parsedmessage.cpp
tests/test_networkengine.h
tests/test_networkengine.cpp
tests/test_opcache.h
tests/test_opcache.cpp
tests/test_valuecache.h
tests/test_valuecache.cpp
)
if (OPENDHT_PROXY_CLIENT)
list (APPEND test_FILES
+3 -15
View File
@@ -204,13 +204,7 @@ struct OPENDHT_PUBLIC PrivateKey
* @param algo : the public key algorithm to use, GNUTLS_PK_RSA or GNUTLS_PK_RSA_OAEP
*/
static PrivateKey generate(unsigned key_length = 4096, gnutls_pk_algorithm_t algo = GNUTLS_PK_RSA);
/**
* Generate a new EC key pair
* @param curve : the elliptic curve to use. If GNUTLS_ECC_CURVE_INVALID is provided, the default curve will be used
* (currently GNUTLS_ECC_CURVE_SECP384R1).
*/
static PrivateKey generateEC(gnutls_ecc_curve_t curve = GNUTLS_ECC_CURVE_INVALID);
static PrivateKey generateEC();
gnutls_privkey_t key {};
gnutls_x509_privkey_t x509_key {};
@@ -273,15 +267,9 @@ public:
/**
* Sign this revocation list using provided key and certificate.
* Validity_period sets the duration until next update (default to no next update).
* If `number` is non-zero, it is stored verbatim (big-endian) as the CRL Number
* extension. Otherwise the number is initialized randomly on the first signature
* and incremented on subsequent ones.
*/
void sign(const PrivateKey&, const Certificate&, duration validity_period = {}, uint64_t number = 0);
void sign(const Identity& id, duration validity_period = {}, uint64_t number = 0)
{
sign(*id.first, *id.second, validity_period, number);
}
void sign(const PrivateKey&, const Certificate&, duration validity_period = {});
void sign(const Identity& id) { sign(*id.first, *id.second); }
bool isSignedBy(const Certificate& issuer) const;
+28 -14
View File
@@ -76,14 +76,8 @@ public:
asio::io_context& io_context() const;
// Monotonic clock for elapsed-time statistics (request rate, uptime).
// Must stay on steady_clock so NTP/wall-clock corrections don't skew durations.
using clock = std::chrono::steady_clock;
using time_point = clock::time_point;
// Wall clock for persisted subscribe/permanent-put lease expirations,
// so they map directly to epoch time and survive restarts.
using sys_clock = std::chrono::system_clock;
using sys_time_point = sys_clock::time_point;
struct PushStats
{
@@ -152,6 +146,26 @@ public:
std::shared_ptr<DhtRunner> getNode() const { return dht_; }
#ifdef OPENDHT_PUSH_NOTIFICATIONS
/**
* Decide whether a batch of values notified to a push listener must be
* delivered as a high (urgent) priority push notification.
*
* Only real-time call signaling — values whose pushType is "audioCall" or
* "videoCall", published at high priority (priority == 0) — warrants an
* urgent push. Every other notification (presence/connection-request
* refreshes carrying an empty pushType, value expirations, etc.) is sent at
* normal priority so it does not saturate the mobile platform's adaptive
* high-priority quota; once that quota is tripped, the platform silently
* drops genuine call pushes.
*
* @param values the values being notified to the listener.
* @param expired whether this notification is for expired (removed) values.
* @return true if the notification must be sent at high priority.
*/
static bool isHighPriorityPush(const std::vector<std::shared_ptr<Value>>& values, bool expired) noexcept;
#endif // OPENDHT_PUSH_NOTIFICATIONS
private:
class ConnectionListener;
struct RestRouterTraitsTls;
@@ -407,12 +421,12 @@ private:
std::shared_ptr<ConnectionListener> connListener_;
struct PermanentPut
{
sys_time_point expiration;
time_point expiration;
std::string pushToken;
std::string clientId;
std::shared_ptr<PushSessionContext> sessionCtx;
std::unique_ptr<asio::system_timer> expireTimer;
std::unique_ptr<asio::system_timer> expireNotifyTimer;
std::unique_ptr<asio::steady_timer> expireTimer;
std::unique_ptr<asio::steady_timer> expireNotifyTimer;
Sp<Value> value;
PushType type;
std::string topic;
@@ -425,7 +439,7 @@ private:
p.pack("value");
p.pack(value);
p.pack("exp");
p.pack(sys_clock::to_time_t(expiration));
p.pack(to_time_t(expiration));
if (not clientId.empty()) {
p.pack("cid");
p.pack(clientId);
@@ -466,12 +480,12 @@ private:
#ifdef OPENDHT_PUSH_NOTIFICATIONS
struct Listener
{
sys_time_point expiration;
time_point expiration;
std::string clientId;
std::shared_ptr<PushSessionContext> sessionCtx;
std::future<size_t> internalToken;
std::unique_ptr<asio::system_timer> expireTimer;
std::unique_ptr<asio::system_timer> expireNotifyTimer;
std::unique_ptr<asio::steady_timer> expireTimer;
std::unique_ptr<asio::steady_timer> expireNotifyTimer;
PushType type;
std::string topic;
@@ -482,7 +496,7 @@ private:
p.pack("cid");
p.pack(clientId);
p.pack("exp");
p.pack(sys_clock::to_time_t(expiration));
p.pack(to_time_t(expiration));
if (sessionCtx) {
std::lock_guard l(sessionCtx->lock);
p.pack("sid");
-7
View File
@@ -106,16 +106,9 @@ public:
void timeout(const std::chrono::seconds& timeout, HandlerCb cb = {});
/**
* Close the underlying socket.
* Safe to call from any thread: the actual close is dispatched to
* the thread running the io_context.
*/
void close();
private:
void doClose();
template<typename T>
T wrapCallback(T cb) const
{
+2 -7
View File
@@ -489,12 +489,8 @@ private:
static constexpr size_t MTU {1280};
static constexpr size_t MAX_PACKET_VALUE_SIZE {600};
static constexpr size_t MAX_MESSAGE_VALUE_SIZE {56 * 1024};
static constexpr size_t RX_MAX_EARLY_BYTES {MAX_MESSAGE_VALUE_SIZE + MAX_VALUE_SIZE + 32};
static constexpr size_t RX_MAX_EARLY_PARTS {RX_MAX_EARLY_BYTES / MTU + 2};
void process(std::unique_ptr<ParsedMessage>&&, const SockAddr& from);
bool isExpectedReply(const ParsedMessage&, const SockAddr& from);
bool tryCompleteMessage(PartialMessage&, const SockAddr& from);
bool rateLimit(const SockAddr& addr);
@@ -531,8 +527,7 @@ private:
{
return packValueHeader(buf, values.begin(), values.end());
}
using PartialMessageKey = std::pair<SockAddr, Tid>;
void maintainRxBuffer(const PartialMessageKey& key);
void maintainRxBuffer(Tid tid);
/*************
* Answers *
@@ -578,7 +573,7 @@ private:
// requests handling
std::map<Tid, Sp<Request>> requests {};
std::map<PartialMessageKey, PartialMessage> partial_messages;
std::map<Tid, PartialMessage> partial_messages;
MessageStats in_stats {}, out_stats {};
std::set<SockAddr> blacklist {};
+1 -1
View File
@@ -8,7 +8,7 @@ namespace dht {
namespace proxy {
constexpr const std::chrono::hours OP_TIMEOUT {24}; // one day
constexpr const std::chrono::hours OP_MARGIN {8}; // eight hours
constexpr const std::chrono::hours OP_MARGIN {2}; // two hours
using ListenToken = uint64_t;
+1 -3
View File
@@ -10,9 +10,7 @@
#include <future>
#include <functional>
#if __cplusplus < 202002L
#include <ciso646>
#endif
#include <ciso646> // fix windows compiler bug
namespace dht {
-8
View File
@@ -85,14 +85,6 @@ using system_clock = std::chrono::system_clock;
using time_point = clock::time_point;
using duration = clock::duration;
OPENDHT_PUBLIC time_point from_system_time(system_clock::time_point t,
time_point steady_now,
system_clock::time_point system_now);
OPENDHT_PUBLIC time_point from_system_time(system_clock::time_point t);
OPENDHT_PUBLIC system_clock::time_point to_system_time(time_point t,
time_point steady_now,
system_clock::time_point system_now);
OPENDHT_PUBLIC system_clock::time_point to_system_time(time_point t);
OPENDHT_PUBLIC time_point from_time_t(std::time_t t);
OPENDHT_PUBLIC std::time_t to_time_t(time_point t);
+6 -23
View File
@@ -5,7 +5,7 @@ project(
'cython',
license: 'MIT',
license_files: 'LICENSE',
version: '4.3.0',
version: '4.0.0',
meson_version: '>=1.1.0',
default_options: ['cpp_std=c++20', 'warning_level=3'],
)
@@ -15,23 +15,10 @@ nettle = dependency('nettle')
msgpack = dependency('msgpack-cxx', required: false)
argon2 = dependency('libargon2')
fmt = dependency('fmt')
proxy_enabled = get_option('proxy_client').enabled() or get_option('proxy_server').enabled()
openssl = dependency('openssl', required: proxy_enabled)
jsoncpp = dependency('jsoncpp', required: proxy_enabled)
llhttp = dependency('llhttp', 'libllhttp', required: proxy_enabled)
simdutf = dependency('simdutf', required: proxy_enabled)
# restinio is a header-only library that may not ship a pkg-config file
restinio_dep = dependency('restinio', required: false)
if not restinio_dep.found()
cpp = meson.get_compiler('cpp')
if cpp.has_header('restinio/all.hpp', required: proxy_enabled)
restinio_dep = declare_dependency()
endif
endif
openssl = dependency('openssl', required: get_option('proxy_client'))
jsoncpp = dependency('jsoncpp', required: get_option('proxy_client'))
llhttp = dependency('llhttp', 'libllhttp', required: get_option('proxy_client'))
simdutf = dependency('simdutf', required: get_option('proxy_client'))
io_uring = host_machine.system() == 'linux' ? dependency('liburing', required: false) : disabler()
deps = [
@@ -46,10 +33,6 @@ deps = [
llhttp,
]
if restinio_dep.found()
deps += restinio_dep
endif
if host_machine.system() == 'linux' and io_uring.found()
deps += io_uring
endif
@@ -96,7 +79,7 @@ if get_option('indexation').enabled()
opendht_src += 'src/indexation/pht.cpp'
add_project_arguments('-DOPENDHT_INDEXATION', language: 'cpp')
endif
if jsoncpp.found() and simdutf.found()
if jsoncpp.found()
opendht_src += ['src/base64.cpp']
add_project_arguments('-DOPENDHT_JSONCPP', language: 'cpp')
conf_data.set('jsoncpp_lib', ', jsoncpp')
+43 -37
View File
@@ -571,12 +571,6 @@ PublicKey::toString() const
}
if (err != GNUTLS_E_SUCCESS)
throw CryptoException(std::string("Could not print public key: ") + gnutls_strerror(err));
// GnuTLS reports the PEM size including the terminating null byte, which
// does not belong to the text and would end up embedded in whatever the
// string is copied into, such as the "owner" field of a serialized value.
ret.resize(sz);
if (not ret.empty() and ret.back() == '\0')
ret.pop_back();
return ret;
}
@@ -1294,16 +1288,14 @@ PrivateKey::generate(unsigned key_length, gnutls_pk_algorithm_t algo)
}
PrivateKey
PrivateKey::generateEC(gnutls_ecc_curve_t curve)
PrivateKey::generateEC()
{
gnutls_x509_privkey_t key;
if (gnutls_x509_privkey_init(&key) != GNUTLS_E_SUCCESS)
throw CryptoException("Unable to initialize private key.");
int err = gnutls_x509_privkey_generate(key,
GNUTLS_PK_EC,
curve == GNUTLS_ECC_CURVE_INVALID
? gnutls_sec_param_to_pk_bits(GNUTLS_PK_EC, GNUTLS_SEC_PARAM_ULTRA)
: GNUTLS_CURVE_TO_BITS(curve),
gnutls_sec_param_to_pk_bits(GNUTLS_PK_EC, GNUTLS_SEC_PARAM_ULTRA),
0);
if (err != GNUTLS_E_SUCCESS) {
gnutls_x509_privkey_deinit(key);
@@ -1404,11 +1396,8 @@ setRandomSerial(gnutls_x509_crt_t cert)
{
std::random_device rdev;
std::uniform_int_distribution<int64_t> dist {1};
const auto cert_serial = dist(rdev);
std::array<uint8_t, sizeof(cert_serial)> packed_serial;
for (size_t i = 0; i < packed_serial.size(); i++)
packed_serial[i] = cert_serial >> (8 * (packed_serial.size() - 1 - i));
gnutls_x509_crt_set_serial(cert, packed_serial.data(), packed_serial.size());
int64_t cert_serial = dist(rdev);
gnutls_x509_crt_set_serial(cert, &cert_serial, sizeof(cert_serial));
}
Certificate
@@ -1884,8 +1873,34 @@ RevocationList::getUpdateTime() const
return std::chrono::system_clock::from_time_t(t);
}
enum class Endian : uint32_t { LITTLE = 0, BIG = 1 };
template<typename T>
T
endian(T w, Endian endian = Endian::BIG)
{
// this gets optimized out into if (endian == host_endian) return w;
union {
uint64_t quad;
uint32_t islittle;
} t;
t.quad = 1;
if (t.islittle ^ (uint32_t) endian)
return w;
T r = 0;
// decent compilers will unroll this (gcc)
// or even convert straight into single bswap (clang)
for (size_t i = 0; i < sizeof(r); i++) {
r <<= 8;
r |= w & 0xff;
w >>= 8;
}
return r;
}
void
RevocationList::sign(const PrivateKey& key, const Certificate& ca, duration validity, uint64_t crlNumber)
RevocationList::sign(const PrivateKey& key, const Certificate& ca, duration validity)
{
if (auto err = gnutls_x509_crl_set_version(crl, 2))
throw CryptoException(std::string("Unable to set CRL version: ") + gnutls_strerror(err));
@@ -1895,28 +1910,19 @@ RevocationList::sign(const PrivateKey& key, const Certificate& ca, duration vali
throw CryptoException(std::string("Unable to set CRL update time: ") + gnutls_strerror(err));
if (auto err = gnutls_x509_crl_set_next_update(crl, std::chrono::system_clock::to_time_t(next_update)))
throw CryptoException(std::string("Unable to set CRL next update time: ") + gnutls_strerror(err));
uint64_t number {crlNumber};
uint64_t number {0};
size_t number_sz {sizeof(number)};
unsigned critical {0};
gnutls_x509_crl_get_number(crl, &number, &number_sz, &critical);
if (number == 0) {
// The CRL Number extension holds a DER integer of variable length, so it must be
// read as a big-endian byte string rather than straight into an uint64_t.
auto current = getNumber();
for (uint8_t byte : current)
number = (number << 8) | byte;
if (number == 0) {
// Initialize to a random number.
std::random_device rdev;
do {
std::generate_n((uint8_t*) &number, sizeof(number), std::bind(rand_byte, std::ref(rdev)));
} while (number == 0);
} else
number++;
}
// Store the number in network byte order.
uint8_t packedNumber[sizeof(number)];
for (size_t i = 0; i < sizeof(packedNumber); i++)
packedNumber[i] = (uint8_t) ((number >> (8 * (sizeof(packedNumber) - 1 - i))) & 0xff);
if (auto err = gnutls_x509_crl_set_number(crl, packedNumber, sizeof(packedNumber)))
throw CryptoException(std::string("Unable to set CRL number: ") + gnutls_strerror(err));
// initialize to a random number
number_sz = sizeof(number);
std::random_device rdev;
std::generate_n((uint8_t*) &number, sizeof(number), std::bind(rand_byte, std::ref(rdev)));
} else
number = endian(endian(number) + 1);
if (auto err = gnutls_x509_crl_set_number(crl, &number, sizeof(number)))
throw CryptoException(std::string("Unable to set CRL update time: ") + gnutls_strerror(err));
if (auto err = gnutls_x509_crl_sign2(crl, ca.cert, key.x509_key, GNUTLS_DIG_SHA512, 0))
throw CryptoException(std::string("Unable to sign certificate revocation list: ") + gnutls_strerror(err));
// to be able to actually use the CRL we need to serialize/deserialize it
+7 -32
View File
@@ -2270,16 +2270,8 @@ Dht::onDisconnected()
void
Dht::bootstrap()
{
if (dht4.status != NodeStatus::Disconnected || dht6.status != NodeStatus::Disconnected) {
// The job that led here has already run, and the scheduler does not
// clear the handle to a job it has executed. onDisconnected() reads a
// set handle as "a bootstrap is already scheduled" and does nothing,
// so leaving it set here means the node never tries to rejoin the
// network once it has lost it. Clearing it is what says that no
// bootstrap is pending any more.
bootstrapJob.reset();
if (dht4.status != NodeStatus::Disconnected || dht6.status != NodeStatus::Disconnected)
return;
}
if (logger_)
logger_->debug("[{}] Bootstraping", myid.to_view());
bootstrap_pending = false;
@@ -2363,9 +2355,6 @@ Dht::confirmNodes()
std::vector<ValuesExport>
Dht::exportValues() const
{
const auto& now = scheduler.time();
const auto system_now = system_clock::now();
std::vector<ValuesExport> e {};
e.reserve(store.size());
for (const auto& h : store) {
@@ -2378,7 +2367,7 @@ Dht::exportValues() const
pk.pack_array(vals.size());
for (const auto& v : vals) {
pk.pack_array(4);
pk.pack(to_system_time(v.created, now, system_now));
pk.pack(v.created.time_since_epoch().count());
v.data->msgpack_pack(pk);
if (const auto& store_addr = v.store_bucket ? v.store_bucket->getAddr() : SockAddr {}) {
pk.pack_bin(store_addr.getLength());
@@ -2386,7 +2375,7 @@ Dht::exportValues() const
} else {
pk.pack_bin(0);
}
pk.pack(to_system_time(v.expiration, now, system_now));
pk.pack(v.expiration.time_since_epoch().count());
}
ve.second = {buffer.data(), buffer.data() + buffer.size()};
e.push_back(std::move(ve));
@@ -2398,7 +2387,6 @@ void
Dht::importValues(const std::vector<ValuesExport>& import)
{
const auto& now = scheduler.time();
const auto system_now = system_clock::now();
unsigned imported = 0, ignored = 0;
size_t imported_size = 0;
@@ -2423,15 +2411,8 @@ Dht::importValues(const std::vector<ValuesExport>& import)
Value tmp_val;
SockAddr store_addr;
try {
const bool uses_system_time = valel.via.array.ptr[0].type == msgpack::type::EXT;
if (uses_system_time) {
val_time = from_system_time(valel.via.array.ptr[0].as<system_clock::time_point>(),
now,
system_now);
} else {
val_time = time_point {
time_point::duration {valel.via.array.ptr[0].as<time_point::duration::rep>()}};
}
val_time = time_point {
time_point::duration {valel.via.array.ptr[0].as<time_point::duration::rep>()}};
tmp_val.msgpack_unpack(valel.via.array.ptr[1]);
if (valel.via.array.size >= 3) {
auto addr_blob = valel.via.array.ptr[2].as<Blob>();
@@ -2441,14 +2422,8 @@ Dht::importValues(const std::vector<ValuesExport>& import)
}
}
if (valel.via.array.size >= 4) {
if (uses_system_time) {
expiration = from_system_time(valel.via.array.ptr[3].as<system_clock::time_point>(),
now,
system_now);
} else {
expiration = time_point {
time_point::duration {valel.via.array.ptr[3].as<time_point::duration::rep>()}};
}
expiration = time_point {
time_point::duration {valel.via.array.ptr[3].as<time_point::duration::rep>()}};
}
} catch (const std::exception&) {
if (logger_)
+21 -35
View File
@@ -25,10 +25,8 @@ struct DhtProxyClient::OperationState
struct DhtProxyClient::Listener
{
// The proxy server is a single logical source that re-delivers values
// (push refresh, listen restart): don't count duplicates.
Listener(ValueCallback&& cb)
: cache(std::move(cb), false)
Listener(OpValueCache&& c)
: cache(std::move(c))
{}
OpValueCache cache;
@@ -36,26 +34,26 @@ struct DhtProxyClient::Listener
Sp<OperationState> opstate;
std::shared_ptr<http::Request> request;
#ifdef OPENDHT_PUSH_NOTIFICATIONS
std::unique_ptr<asio::system_timer> refreshSubscriberTimer;
std::unique_ptr<asio::steady_timer> refreshSubscriberTimer;
#endif
};
struct PermanentPut
{
PermanentPut(const Sp<Value>& v, std::unique_ptr<asio::system_timer>&& j, const Sp<std::atomic_bool>& o)
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::system_timer> refreshPutTimer;
std::unique_ptr<asio::steady_timer> refreshPutTimer;
Sp<std::atomic_bool> ok;
};
struct DhtProxyClient::ProxySearch
{
SearchCache ops {false};
SearchCache ops {};
std::unique_ptr<asio::steady_timer> opExpirationTimer;
std::map<size_t, Listener> listeners {};
std::map<Value::Id, PermanentPut> puts {};
@@ -426,7 +424,7 @@ DhtProxyClient::put(const InfoHash& key, Sp<Value> val, DoneCallback cb, time_po
auto& search = searches_[key];
if (val->id) {
auto id = val->id;
auto refreshPutTimer = std::make_unique<asio::system_timer>(httpContext_,
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));
@@ -542,7 +540,7 @@ DhtProxyClient::doPut(const InfoHash& key, Sp<Value> val, DoneCallbackSimple cb,
auto it = search.pendingPuts.find(val);
if (it != search.pendingPuts.end()) {
auto sok = std::make_shared<std::atomic_bool>(ok);
auto refreshPutTimer = std::make_unique<asio::system_timer>(httpContext_,
auto refreshPutTimer = std::make_unique<asio::steady_timer>(httpContext_,
proxy::OP_TIMEOUT
- proxy::OP_MARGIN);
refreshPutTimer->async_wait(
@@ -893,8 +891,8 @@ DhtProxyClient::listen(const InfoHash& key, ValueCallback cb, Value::Filter filt
* (if the proxy crash for any reason)
*/
if (!l->second.refreshSubscriberTimer)
l->second.refreshSubscriberTimer = std::make_unique<asio::system_timer>(httpContext_);
l->second.refreshSubscriberTimer->expires_at(std::chrono::system_clock::now() + proxy::OP_TIMEOUT
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));
@@ -1194,9 +1192,9 @@ DhtProxyClient::restartListeners(const asio::error_code& ec)
for (auto& put : search.second.puts) {
doPut(key, put.second.value, [ok = put.second.ok](bool result) { *ok = result; }, time_point::max(), true);
if (!put.second.refreshPutTimer) {
put.second.refreshPutTimer = std::make_unique<asio::system_timer>(httpContext_);
put.second.refreshPutTimer = std::make_unique<asio::steady_timer>(httpContext_);
}
put.second.refreshPutTimer->expires_at(std::chrono::system_clock::now() + proxy::OP_TIMEOUT
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));
@@ -1277,10 +1275,10 @@ DhtProxyClient::pushNotificationReceived([[maybe_unused]] const std::map<std::st
auto vid = std::stoull(vidIt->second);
auto& put = search.puts.at(vid);
if (!put.refreshPutTimer)
put.refreshPutTimer = std::make_unique<asio::system_timer>(httpContext_,
std::chrono::system_clock::now());
put.refreshPutTimer = std::make_unique<asio::steady_timer>(httpContext_,
std::chrono::steady_clock::now());
else
put.refreshPutTimer->expires_at(std::chrono::system_clock::now());
put.refreshPutTimer->expires_at(std::chrono::steady_clock::now());
put.refreshPutTimer->async_wait(
std::bind(&DhtProxyClient::handleRefreshPut, this, std::placeholders::_1, key, vid));
ret = PushNotificationResult::PutRefresh;
@@ -1292,12 +1290,12 @@ DhtProxyClient::pushNotificationReceived([[maybe_unused]] const std::map<std::st
: PushNotificationResult::ListenRefresh;
}
} else {
auto key = InfoHash(notification.at("key"));
system_clock::time_point sendTime = system_clock::time_point::min();
try {
sendTime = system_clock::time_point(std::chrono::milliseconds(std::stoull(notification.at("t"))));
} catch (...) {
}
auto key = InfoHash(notification.at("key"));
auto& search = searches_.at(key);
auto expired = notification.find("exp");
for (auto& list : search.listeners) {
@@ -1310,25 +1308,13 @@ DhtProxyClient::pushNotificationReceived([[maybe_unused]] const std::map<std::st
if (expired == notification.end()) {
auto cb = list.second.cb;
auto oldValues = list.second.cache.getValues();
auto receivedIds = std::make_shared<std::set<Value::Id>>();
get(
key,
[cb, sendTime, receivedIds](const std::vector<Sp<Value>>& vals) {
for (const auto& v : vals)
receivedIds->insert(v->id);
return cb(vals, false, sendTime);
},
[cb, oldValues, sendTime, receivedIds](bool ok) {
if (!ok)
return;
[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
std::vector<Sp<Value>> toExpire;
for (const auto& v : oldValues)
if (receivedIds->find(v->id) == receivedIds->end())
toExpire.emplace_back(v);
if (!toExpire.empty())
cb(toExpire, true, sendTime);
cb(oldValues, true, sendTime);
});
ret = PushNotificationResult::Values;
} else {
@@ -1392,9 +1378,9 @@ DhtProxyClient::resubscribe([[maybe_unused]] const InfoHash& key,
header.method(restinio::http_method_subscribe());
header.request_target("/key/" + key.toString());
if (!listener.refreshSubscriberTimer) {
listener.refreshSubscriberTimer = std::make_unique<asio::system_timer>(httpContext_);
listener.refreshSubscriberTimer = std::make_unique<asio::steady_timer>(httpContext_);
}
listener.refreshSubscriberTimer->expires_at(std::chrono::system_clock::now() + proxy::OP_TIMEOUT - proxy::OP_MARGIN);
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;
+43 -34
View File
@@ -210,7 +210,7 @@ DhtProxyServer::PermanentPut::msgpack_unpack(const msgpack::object& o)
clientId = cid->as<std::string>();
}
if (auto exp = findMapValue(o, "exp"sv)) {
expiration = sys_clock::from_time_t(exp->as<time_t>());
expiration = from_time_t(exp->as<time_t>());
}
if (auto token = findMapValue(o, "token"sv)) {
pushToken = token->as<std::string>();
@@ -240,7 +240,7 @@ DhtProxyServer::Listener::msgpack_unpack(const msgpack::object& o)
clientId = cid->as<std::string>();
}
if (auto exp = findMapValue(o, "exp"sv)) {
expiration = sys_clock::from_time_t(exp->as<time_t>());
expiration = from_time_t(exp->as<time_t>());
}
if (auto sid = findMapValue(o, "sid"sv)) {
if (not sessionCtx)
@@ -414,7 +414,7 @@ DhtProxyServer::loadState(Is& is, size_t size)
logger_->debug("[proxy:server] Loading {} persistent puts", puts_.size());
for (auto& put : puts_) {
for (auto& pput : put.second.puts) {
pput.second.expireTimer = std::make_unique<asio::system_timer>(io_context(),
pput.second.expireTimer = std::make_unique<asio::steady_timer>(io_context(),
pput.second.expiration);
pput.second.expireTimer->async_wait(std::bind(&DhtProxyServer::handleCancelPermamentPut,
this,
@@ -437,7 +437,7 @@ DhtProxyServer::loadState(Is& is, size_t size)
}
return json;
};
pput.second.expireNotifyTimer = std::make_unique<asio::system_timer>(io_context(),
pput.second.expireNotifyTimer = std::make_unique<asio::steady_timer>(io_context(),
pput.second.expiration
- proxy::OP_MARGIN);
pput.second.expireNotifyTimer->async_wait(
@@ -450,7 +450,7 @@ DhtProxyServer::loadState(Is& is, size_t size)
pput.second.topic));
}
#endif
dht_->put(put.first, pput.second.value, DoneCallbackSimple {}, dht::time_point::max(), true);
dht_->put(put.first, pput.second.value, DoneCallbackSimple {}, time_point::max(), true);
}
}
} else {
@@ -487,7 +487,7 @@ DhtProxyServer::loadState(Is& is, size_t size)
expired);
});
// expire notify
listener.expireNotifyTimer = std::make_unique<asio::system_timer>(io_context(),
listener.expireNotifyTimer = std::make_unique<asio::steady_timer>(io_context(),
listener.expiration
- proxy::OP_MARGIN);
auto jsonProvider = [infoHash = listeners.first.toString(),
@@ -509,7 +509,7 @@ DhtProxyServer::loadState(Is& is, size_t size)
listener.type,
listener.topic));
// cancel push listen
listener.expireTimer = std::make_unique<asio::system_timer>(io_context(),
listener.expireTimer = std::make_unique<asio::steady_timer>(io_context(),
listener.expiration);
listener.expireTimer->async_wait(std::bind(&DhtProxyServer::handleCancelPushListen,
this,
@@ -571,18 +571,6 @@ DhtProxyServer::~DhtProxyServer()
ioContext_->stop();
if (serverThread_.joinable())
serverThread_.join();
{
// ~Request notifies DONE and its handler erases from requests_:
// detach pending push requests so nothing calls back into this object.
decltype(requests_) pending;
{
std::lock_guard l(requestLock_);
pending = std::move(requests_);
}
for (auto& r : pending)
r.second->add_on_state_change_callback({});
pending.clear();
}
if (logger_)
logger_->debug("[proxy:server] http server closed");
}
@@ -919,7 +907,6 @@ DhtProxyServer::subscribe(restinio::request_handle_t request, restinio::router::
Json::Value r;
auto* char_data = reinterpret_cast<const char*>(request->body().data());
auto reader = std::unique_ptr<Json::CharReader>(jsonReaderBuilder_.newCharReader());
if (!reader->parse(char_data, char_data + request->body().size(), &r, &err)) {
auto response = initHttpResponse(request->create_response(restinio::status_bad_request()));
response.set_body(RESP_MSG_JSON_INCORRECT);
@@ -963,15 +950,15 @@ DhtProxyServer::subscribe(restinio::request_handle_t request, restinio::router::
}
auto& listener = *listIt;
// Expiration (wall-clock: persisted as epoch time, survives restarts)
auto timeout = sys_clock::now() + proxy::OP_TIMEOUT;
// Expiration
auto timeout = std::chrono::steady_clock::now() + proxy::OP_TIMEOUT;
listener.expiration = timeout;
listener.type = type;
listener.topic = topic;
if (listener.expireNotifyTimer)
listener.expireNotifyTimer->expires_at(timeout - proxy::OP_MARGIN);
else
listener.expireNotifyTimer = std::make_unique<asio::system_timer>(io_context(), timeout - proxy::OP_MARGIN);
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;
@@ -988,7 +975,7 @@ DhtProxyServer::subscribe(restinio::request_handle_t request, restinio::router::
listener.type,
listener.topic));
if (!listener.expireTimer)
listener.expireTimer = std::make_unique<asio::system_timer>(io_context(), timeout);
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,
@@ -1147,6 +1134,25 @@ DhtProxyServer::handleCancelPushListen(const asio::error_code& ec,
pushListeners_.erase(pushListener);
}
bool
DhtProxyServer::isHighPriorityPush(const std::vector<std::shared_ptr<Value>>& values, bool expired) noexcept
{
// Expired-value notifications (de-registration) are never time-critical.
if (expired)
return false;
// Promote to high (urgent) push priority only for real-time call signaling.
// Background traffic — most notably connection-request refreshes, which
// carry an empty pushType — stays at normal priority so it does not trip the
// mobile platform's adaptive high-priority quota, which would otherwise
// silently drop genuine call pushes once the high-priority noise ratio gets
// too high.
for (const auto& v : values) {
if (v->priority == 0 and (v->pushType == "videoCall" or v->pushType == "audioCall"))
return true;
}
return false;
}
bool
DhtProxyServer::handlePushListen(const InfoHash& infoHash,
const std::string& pushToken,
@@ -1189,20 +1195,23 @@ DhtProxyServer::handlePushListen(const InfoHash& infoHash,
json["exp"] = json["ids"];
}
auto minPriority = 1000u;
for (const auto& v : values)
minPriority = std::min(minPriority, v->priority);
const bool highPriority = isHighPriorityPush(values, expired);
if (logger_)
logger_->debug("[proxy:server] [listen {}] [client {}] [session {}] [expired {}] [priority {}] [values {}]",
if (logger_) {
auto minPriority = 1000u;
for (const auto& v : values)
minPriority = std::min(minPriority, v->priority);
logger_->debug("[proxy:server] [listen {}] [client {}] [session {}] [expired {}] [priority {}] [high {}] [values {}]",
infoHash,
clientId,
sessionCtx->sessionId,
expired,
minPriority,
highPriority,
values.size());
}
sendPushNotification(pushToken, std::move(json), type, !expired and minPriority == 0, topic);
sendPushNotification(pushToken, std::move(json), type, highPriority, topic);
return true;
}
@@ -1418,7 +1427,7 @@ DhtProxyServer::put(restinio::request_handle_t request, restinio::router::route_
topic = pVal["topic"].asString();
}
std::lock_guard lock(lockSearchPuts_);
auto timeout = sys_clock::now() + proxy::OP_TIMEOUT;
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) {
@@ -1452,7 +1461,7 @@ DhtProxyServer::put(restinio::request_handle_t request, restinio::router::route_
pput.expiration = timeout;
if (not pput.expireTimer) {
// cancel permanent put
pput.expireTimer = std::make_unique<asio::system_timer>(io_context(), timeout);
pput.expireTimer = std::make_unique<asio::steady_timer>(io_context(), timeout);
#ifdef OPENDHT_PUSH_NOTIFICATIONS
if (not pushToken.empty()) {
pput.pushToken = pushToken;
@@ -1491,7 +1500,7 @@ DhtProxyServer::put(restinio::request_handle_t request, restinio::router::route_
return json;
};
if (!pput.expireNotifyTimer)
pput.expireNotifyTimer = std::make_unique<asio::system_timer>(io_context(),
pput.expireNotifyTimer = std::make_unique<asio::steady_timer>(io_context(),
timeout - proxy::OP_MARGIN);
else
pput.expireNotifyTimer->expires_at(timeout - proxy::OP_MARGIN);
@@ -1520,7 +1529,7 @@ DhtProxyServer::put(restinio::request_handle_t request, restinio::router::route_
response.done();
}
},
dht::time_point::max(),
time_point::max(),
permanent);
return restinio::request_handling_status_t::accepted;
} else {
+3 -29
View File
@@ -99,11 +99,6 @@ DhtRunner::run(const Config& config, Context&& context)
auto state_path = config.dht_config.node_config.persist_path;
if (not state_path.empty())
state_path += "_port.txt";
// Ports loaded from the persisted state are a best-effort hint to keep a
// stable identity across restarts; remember which ones we set so that we
// can fall back to random ports if they are already in use (e.g. a fast
// restart while the previous instance still holds them).
bool triedSavedPort = false;
if (not state_path.empty() && (local4.getPort() == 0 || local6.getPort() == 0)) {
std::ifstream inConfig(state_path);
if (inConfig.is_open()) {
@@ -115,7 +110,6 @@ DhtRunner::run(const Config& config, Context&& context)
fmt::ptr(this),
port);
local4.setPort(port);
triedSavedPort = true;
}
}
if (inConfig >> port) {
@@ -125,7 +119,6 @@ DhtRunner::run(const Config& config, Context&& context)
fmt::ptr(this),
port);
local6.setPort(port);
triedSavedPort = true;
}
}
}
@@ -143,20 +136,7 @@ DhtRunner::run(const Config& config, Context&& context)
if (config.proxy_server.empty()) {
if (not context.sock) {
try {
context.sock.reset(new net::UdpSocket(local4, local6, context.logger));
} catch (const DhtException&) {
if (not triedSavedPort)
throw;
// The saved ports are unavailable (typically still held by a
// previous instance): fall back to OS-assigned ports.
if (context.logger)
context.logger->w("[runner %p] Saved ports unavailable, falling back to random ports",
fmt::ptr(this));
local4.setPort(0);
local6.setPort(0);
context.sock.reset(new net::UdpSocket(local4, local6, context.logger));
}
context.sock.reset(new net::UdpSocket(local4, local6, context.logger));
}
context.sock->setOnReceive([&](net::PacketList&& pkts) {
net::PacketList ret;
@@ -554,14 +534,8 @@ DhtRunner::registerInsecureType(const ValueType& type)
void
DhtRunner::importValues(const std::vector<ValuesExport>& values)
{
{
std::lock_guard lck(dht_mtx);
dht_->importValues(values);
}
// Recompute the runner wakeup after import schedules value expirations.
std::lock_guard lck(storage_mtx);
pending_ops_prio.emplace([](SecureDht&) {});
cv.notify_all();
std::lock_guard lck(dht_mtx);
dht_->importValues(values);
}
unsigned
+8 -38
View File
@@ -233,32 +233,11 @@ Connection::Connection(asio::io_context& ctx,
Connection::~Connection()
{
// No asynchronous operation can be pending here: their handlers keep
// the connection alive (see wrapCallback), so closing synchronously
// is safe regardless of the calling thread.
doClose();
close();
}
void
Connection::close()
{
// asio sockets are not thread-safe. Closing from a thread other than the
// one running the io_context races with in-flight operations on the same
// socket (e.g. async_connect closing and reopening it to try the next
// endpoint), corrupting the reactor's descriptor state.
// Always perform the close on the io_context thread.
if (ctx_.get_executor().running_in_this_thread()) {
doClose();
return;
}
asio::post(ctx_, [w = weak_from_this()] {
if (auto sthis = w.lock())
sthis->doClose();
});
}
void
Connection::doClose()
{
std::lock_guard lock(mutex_);
asio::error_code ec;
@@ -323,13 +302,13 @@ static X509*
issuer_from_chain(STACK_OF(X509) * fullchain)
{
X509 *cert, *issuer;
const X509_NAME* issuer_name;
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, const_cast<X509_NAME*>(issuer_name));
issuer = X509_find_by_subject(fullchain, issuer_name);
return issuer;
}
@@ -652,13 +631,12 @@ Connection::async_handshake(HandlerCb cb)
{
std::lock_guard lock(mutex_);
if (ssl_socket_) {
// Keep the connection alive while the handshake is in flight, like
// every other asynchronous operation (see wrapCallback).
std::weak_ptr<Connection> wthis = shared_from_this();
ssl_socket_
->async_handshake(asio::ssl::stream<asio::ip::tcp::socket>::client, [sthis = shared_from_this(), cb](const asio::error_code& ec) {
->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_) {
@@ -939,16 +917,8 @@ Resolver::add_callback(ResolverCb cb, sa_family_t family)
else
cb(ec, filter(endpoints, family));
});
else {
// Always dispatch the callback on the io_context thread. Invoking it
// synchronously would run the caller's continuation (e.g. the whole
// connect chain of Request::send) on the calling thread, mutating asio
// objects concurrently with the io thread running the io_context.
asio::post(resolver_.get_executor(),
[cb = std::move(cb),
ec = ec_,
endpoints = family == AF_UNSPEC ? endpoints_ : filter(endpoints_, family)] { cb(ec, endpoints); });
}
else
cb(ec_, family == AF_UNSPEC ? endpoints_ : filter(endpoints_, family));
}
void
+44 -128
View File
@@ -40,44 +40,8 @@ struct NetworkEngine::PartialMessage
time_point start;
time_point last_part;
std::unique_ptr<ParsedMessage> msg;
std::vector<std::unique_ptr<ParsedMessage>> early_parts;
size_t early_bytes {};
bool request_quota_consumed {};
};
bool
NetworkEngine::isExpectedReply(const ParsedMessage& msg, const SockAddr& from)
{
auto request = requests.find(msg.tid);
if (request != requests.end() && request->second->pending() && !request->second->node->id
&& request->second->node->getAddr() == from)
return true;
auto node = cache.getNode(msg.id, from.getFamily());
if (!node || node->getAddr() != from)
return false;
auto pending = node->getRequest(msg.tid);
return (pending && pending->pending()) || node->getSocket(msg.tid);
}
bool
NetworkEngine::tryCompleteMessage(PartialMessage& partial, const SockAddr& from)
{
try {
if (!partial.msg->complete())
return false;
process(std::move(partial.msg), from);
} catch (const std::exception& e) {
if (logger_)
logger_->warn("Error while processing partial message: {}", e.what());
} catch (...) {
if (logger_)
logger_->warn("Unknown error while processing partial message");
}
return true;
}
std::vector<Blob>
serializeValues(const std::vector<Sp<Value>>& st)
{
@@ -502,58 +466,35 @@ NetworkEngine::processMessage(const uint8_t* buf, size_t buflen, SockAddr f)
// partial value data
if (msg->type == MessageType::ValueData) {
if (msg->fragment_parts.empty()
|| std::any_of(msg->fragment_parts.begin(), msg->fragment_parts.end(), [](const auto& fragment) {
return fragment.second.second.empty();
})) {
auto pmsg_it = partial_messages.find(msg->tid);
if (pmsg_it == partial_messages.end()) {
if (logIncoming_)
if (logger_)
logger_->debug("Unable to find partial message");
rateLimit(from);
return;
}
PartialMessageKey key {from, msg->tid};
auto pmsg_it = partial_messages.find(key);
if (pmsg_it == partial_messages.end()) {
if (!rateLimit(from))
return;
auto inserted = partial_messages.emplace(key, PartialMessage {});
pmsg_it = inserted.first;
pmsg_it->second.from = from;
pmsg_it->second.start = now;
pmsg_it->second.last_part = now;
pmsg_it->second.request_quota_consumed = true;
scheduler.add(now + RX_MAX_PACKET_TIME, std::bind(&NetworkEngine::maintainRxBuffer, this, key));
}
auto& pmsg = pmsg_it->second;
if (!pmsg.msg) {
if (pmsg.early_parts.size() >= RX_MAX_EARLY_PARTS) {
partial_messages.erase(pmsg_it);
return;
}
size_t fragment_bytes {};
for (const auto& [index, fragment] : msg->fragment_parts) {
(void) index;
if (fragment.second.size() > RX_MAX_EARLY_BYTES - fragment_bytes) {
partial_messages.erase(pmsg_it);
return;
}
fragment_bytes += fragment.second.size();
}
if (fragment_bytes > RX_MAX_EARLY_BYTES - pmsg.early_bytes) {
partial_messages.erase(pmsg_it);
return;
}
pmsg.early_bytes += fragment_bytes;
pmsg.early_parts.emplace_back(std::move(msg));
pmsg.last_part = now;
scheduler.add(now + RX_TIMEOUT, std::bind(&NetworkEngine::maintainRxBuffer, this, key));
if (!pmsg_it->second.from.equals(from)) {
if (logger_)
logger_->debug("Received partial message data from unexpected IP address");
rateLimit(from);
return;
}
// append data block
if (pmsg.msg->append(*msg)) {
pmsg.last_part = now;
if (tryCompleteMessage(pmsg, from)) {
if (pmsg_it->second.msg->append(*msg)) {
pmsg_it->second.last_part = now;
// check data completion
if (pmsg_it->second.msg->complete()) {
try {
// process the full message
process(std::move(pmsg_it->second.msg), from);
} catch (const std::exception& e) {
if (logger_)
logger_->warn("Error while processing partial message: {}", e.what());
}
partial_messages.erase(pmsg_it);
} else
scheduler.add(now + RX_TIMEOUT, std::bind(&NetworkEngine::maintainRxBuffer, this, key));
scheduler.add(now + RX_TIMEOUT, std::bind(&NetworkEngine::maintainRxBuffer, this, msg->tid));
}
return;
}
@@ -564,32 +505,7 @@ NetworkEngine::processMessage(const uint8_t* buf, size_t buflen, SockAddr f)
return;
}
if (msg->value_parts.empty()) {
if (msg->type > MessageType::Reply) {
/* Rate limit requests. */
if (!rateLimit(from)) {
if (logger_)
logger_->warn("Dropping request due to rate limiting");
return;
}
}
try {
process(std::move(msg), from);
} catch (const std::exception& e) {
if (logger_)
logger_->warn("Error while processing message: {}", e.what());
}
return;
}
PartialMessageKey partial_key {from, msg->tid};
if (msg->type == MessageType::Reply && !isExpectedReply(*msg, from))
return;
const auto partial = partial_messages.find(partial_key);
const bool request_quota_consumed = partial != partial_messages.end() && !partial->second.msg
&& partial->second.request_quota_consumed;
if (msg->type > MessageType::Reply && !request_quota_consumed) {
if (msg->type > MessageType::Reply) {
/* Rate limit requests. */
if (!rateLimit(from)) {
if (logger_)
@@ -598,28 +514,28 @@ NetworkEngine::processMessage(const uint8_t* buf, size_t buflen, SockAddr f)
}
}
// starting partial message session
auto inserted = partial_messages.emplace(partial_key, PartialMessage {});
auto& pmsg = inserted.first->second;
if (not pmsg.msg) {
if (inserted.second) {
if (msg->value_parts.empty()) {
try {
process(std::move(msg), from);
} catch (const std::exception& e) {
if (logger_)
logger_->warn("Error while processing message: {}", e.what());
return;
}
} else {
// starting partial message session
auto k = msg->tid;
auto& pmsg = partial_messages[k];
if (not pmsg.msg) {
pmsg.from = from;
pmsg.msg = std::move(msg);
pmsg.start = now;
scheduler.add(now + RX_MAX_PACKET_TIME, std::bind(&NetworkEngine::maintainRxBuffer, this, partial_key));
}
pmsg.msg = std::move(msg);
pmsg.last_part = now;
for (const auto& part : pmsg.early_parts)
pmsg.msg->append(*part);
pmsg.early_parts.clear();
pmsg.early_bytes = 0;
if (tryCompleteMessage(pmsg, from)) {
partial_messages.erase(inserted.first);
} else {
scheduler.add(now + RX_TIMEOUT, std::bind(&NetworkEngine::maintainRxBuffer, this, partial_key));
}
} else if (logger_)
logger_->error("Partial message from {} with given TID {} already exists", from.toString(), msg->tid);
pmsg.last_part = now;
scheduler.add(now + RX_MAX_PACKET_TIME, std::bind(&NetworkEngine::maintainRxBuffer, this, k));
scheduler.add(now + RX_TIMEOUT, std::bind(&NetworkEngine::maintainRxBuffer, this, k));
} else if (logger_)
logger_->error("Partial message with given TID {} already exists", k);
}
}
void
@@ -1698,9 +1614,9 @@ NetworkEngine::sendError(const SockAddr& addr, Tid tid, uint16_t code, const std
}
void
NetworkEngine::maintainRxBuffer(const PartialMessageKey& key)
NetworkEngine::maintainRxBuffer(Tid tid)
{
auto msg = partial_messages.find(key);
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) {
+19 -25
View File
@@ -12,20 +12,18 @@ OpValueCache::onValuesAdded(const std::vector<Sp<Value>>& vals, const system_clo
std::vector<Sp<Value>> newValues;
for (const auto& v : vals) {
auto viop = values.emplace(v->id, v);
auto& stored = viop.first->second;
if (viop.second) {
newValues.emplace_back(v);
} else if (*stored.data != *v) {
if (v->seq <= stored.data->seq)
continue; // older version from a lagging source
// Edited: only the reporting source is known to hold this version
stored.data = v;
stored.refCount = 1;
newValues.emplace_back(v);
} else if (countSources) {
stored.refCount++;
} else if (*viop.first->second.data != *v) {
// Special case for edition
if (v->seq > viop.first->second.data->seq) {
viop.first->second.data = v;
newValues.emplace_back(v);
}
} else {
viop.first->second.refCount++;
}
stored.updated = t;
viop.first->second.updated = t;
}
return newValues.empty() ? true : callback(newValues, false);
}
@@ -39,8 +37,6 @@ OpValueCache::onValuesExpired(const std::vector<Sp<Value>>& vals, const system_c
if (vit != values.end()) {
if (vit->second.updated > t)
continue;
if (v->seq < vit->second.data->seq)
continue; // source held an older version, not counted
vit->second.updated = t;
vit->second.refCount--;
@@ -178,7 +174,7 @@ SearchCache::listen(const ValueCallback& get_cb, const Sp<Query>& q, Value::Filt
auto op = getOp(q);
if (op == ops.end()) {
// New query
op = ops.emplace(q, std::make_unique<OpCache>(countSources_)).first;
op = ops.emplace(q, std::make_unique<OpCache>()).first;
auto& cache = *op->second;
cache.searchToken = onListen(
q,
@@ -228,7 +224,7 @@ SearchCache::expire(const time_point& now, const std::function<void(size_t)>& on
auto ret = nextExpiration_;
for (auto it = ops.begin(); it != ops.end();) {
auto expiration = it->second->getExpiration();
if (expiration <= now) {
if (expiration < now) {
auto cache = std::move(it->second);
it = ops.erase(it);
onCancel(cache->searchToken);
@@ -244,17 +240,15 @@ SearchCache::expire(const time_point& now, const std::function<void(size_t)>& on
bool
SearchCache::get(const Value::Filter& f, const Sp<Query>& q, const GetCallback& gcb, const DoneCallback& dcb) const
{
if (not gcb)
return false;
auto op = getOp(q);
if (op == ops.end() or not op->second->isSynced())
return false;
auto vals = op->second->get(f);
if (not vals.empty())
gcb(vals); // a consumer stopping early is not a failure
if (dcb)
dcb(true, {});
return true;
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>>
+12 -28
View File
@@ -21,29 +21,19 @@ struct OpCacheValueStorage
class OpValueCache
{
public:
/**
* @param countSources When true, a value is kept until every source
* that reported it has expired it (per-node caches,
* IPv4/IPv6 searches, local storage). When false,
* re-deliveries of the same value are not counted
* and a single expiration removes it (single
* logical source, e.g. a proxy server).
*/
OpValueCache(ValueCallback&& cb, bool countSources = true) noexcept
OpValueCache(ValueCallback&& cb) noexcept
: callback(std::forward<ValueCallback>(cb))
, countSources(countSources)
{}
explicit OpValueCache(OpValueCache&& o) noexcept
: values(std::move(o.values))
, callback(std::move(o.callback))
, countSources(o.countSources)
{
o.callback = {};
}
static ValueCallback cacheCallback(ValueCallback&& cb, std::function<void()>&& onCancel, bool countSources = true)
static ValueCallback cacheCallback(ValueCallback&& cb, std::function<void()>&& onCancel)
{
return [cache = std::make_shared<OpValueCache>(std::forward<ValueCallback>(cb), countSources),
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)
@@ -99,22 +89,19 @@ private:
size_t syncedNodes {0};
std::map<Value::Id, OpCacheValueStorage> values {};
ValueCallback callback;
bool countSources {true};
};
class OpCache
{
public:
explicit OpCache(bool countSources = true)
: cache(
[this](const std::vector<Sp<Value>>& vals, bool expired) {
if (expired)
onValuesExpired(vals);
else
onValuesAdded(vals);
return true;
},
countSources)
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) { return cache.onValue(vals, expired); }
@@ -169,9 +156,7 @@ private:
class SearchCache
{
public:
explicit SearchCache(bool countSources = true)
: countSources_(countSources)
{}
SearchCache() {}
SearchCache(SearchCache&&) = default;
using OnListen = std::function<size_t(Sp<Query>, ValueCallback, SyncCallback)>;
@@ -207,7 +192,6 @@ private:
size_t nextToken_ {1};
time_point nextExpiration_ {time_point::max()};
bool countSources_ {true};
};
} // namespace dht
+4 -14
View File
@@ -274,21 +274,14 @@ ParsedMessage::msgpack_unpack(const msgpack::object& msg)
if (!parsed.a && !parsed.r && !parsed.e && !parsed.u)
throw msgpack::type_error();
auto& req = parsed.a ? *parsed.a : (parsed.r ? *parsed.r : (parsed.u ? *parsed.u : *parsed.e));
if (parsed.e) {
if (parsed.e->type != msgpack::type::ARRAY or parsed.e->via.array.size < 1)
if (parsed.e->type != msgpack::type::ARRAY)
throw msgpack::type_error();
error_code = parsed.e->via.array.ptr[0].as<uint16_t>();
}
/* An error-only message carries no request map. The error array must never
be reached through the map view of the msgpack object union, and a
request field that is not a map must be rejected rather than
reinterpreted. */
const msgpack::object* req = parsed.a ? parsed.a : (parsed.r ? parsed.r : parsed.u);
if (req and req->type != msgpack::type::MAP)
throw msgpack::type_error();
struct ParsedReq
{
msgpack::object* values;
@@ -297,9 +290,8 @@ ParsedMessage::msgpack_unpack(const msgpack::object& msg)
msgpack::object* want;
} parsedReq {};
const uint32_t req_size = req ? req->via.map.size : 0;
for (unsigned i = 0; i < req_size; i++) {
auto& o = req->via.map.ptr[i];
for (unsigned i = 0; i < req.via.map.size; i++) {
auto& o = req.via.map.ptr[i];
if (o.key.type != msgpack::type::STR)
continue;
auto key = o.key.as<std::string_view>();
@@ -378,8 +370,6 @@ ParsedMessage::msgpack_unpack(const msgpack::object& msg)
} else if (parsedReq.fields) {
if (auto rfields = findMapValue(*parsedReq.fields, "f"sv)) {
auto vfields = rfields->as<std::set<Value::Field>>();
if (vfields.empty())
throw msgpack::type_error();
if (auto rvalues = findMapValue(*parsedReq.fields, "v"sv)) {
if (rvalues->type != msgpack::type::ARRAY)
throw msgpack::type_error();
+1 -3
View File
@@ -232,8 +232,6 @@ Storage::store(const InfoHash& id, const Sp<Value>& value, time_point created, t
if (it->data != value) {
size_t size_old = it->data->size();
ssize_t size_diff = size_new - (ssize_t) size_old;
// Re-storing identical content is a refresh, not an edition
size_t edited = (*it->data != *value) ? 1 : 0;
// DHT_LOG.DEBUG("Updating %s -> %s", id.toString().c_str(), value->toString().c_str());
// clear quota for previous value
if (it->store_bucket)
@@ -245,7 +243,7 @@ Storage::store(const InfoHash& id, const Sp<Value>& value, time_point created, t
sb->insert(id, *value, expiration);
it->data = value;
total_size += size_diff;
return std::make_pair(&(*it), StoreDiff {size_diff, 0, 0, edited});
return std::make_pair(&(*it), StoreDiff {size_diff, 0, 0, 1});
}
} else {
// DHT_LOG.DEBUG("Storing %s -> %s", id.toString().c_str(), value->toString().c_str());
+2 -4
View File
@@ -6,10 +6,8 @@
#include <atomic>
#include <thread>
#include <iostream>
#include <cmath> // std::pow
#if __cplusplus < 202002L
#include <ciso646>
#endif
#include <ciso646> // fix windows compiler bug
#include <cmath> // std::pow
namespace dht {
+14 -53
View File
@@ -238,67 +238,28 @@ operator==(const SockAddr& a, const SockAddr& b)
return a.equals(b);
}
time_point
from_system_time(system_clock::time_point t, time_point steady_now, system_clock::time_point system_now)
{
if (t == system_clock::time_point::min())
return time_point::min();
if (t == system_clock::time_point::max())
return time_point::max();
// Saturate before the cast: the steady clock duration may hold a narrower range than the system one.
constexpr auto max_dt = std::chrono::duration_cast<system_clock::duration>(duration::max());
constexpr auto min_dt = std::chrono::duration_cast<system_clock::duration>(duration::min());
auto system_dt = t - system_now;
if (system_dt >= max_dt)
return time_point::max();
if (system_dt <= min_dt)
return time_point::min();
auto dt = std::chrono::duration_cast<duration>(system_dt);
if (dt > duration(0) and steady_now > time_point::max() - dt)
return time_point::max();
else if (dt < duration(0) and steady_now < time_point::min() - dt)
return time_point::min();
return steady_now + dt;
}
time_point
from_system_time(system_clock::time_point t)
{
return from_system_time(t, clock::now(), system_clock::now());
}
system_clock::time_point
to_system_time(time_point t, time_point steady_now, system_clock::time_point system_now)
{
if (t == time_point::min())
return system_clock::time_point::min();
if (t == time_point::max())
return system_clock::time_point::max();
// Cast first: mixing clock durations of different periods would overflow the bound computations below.
auto dt = std::chrono::duration_cast<system_clock::duration>(t - steady_now);
if (dt > system_clock::duration(0) and system_now >= system_clock::time_point::max() - dt)
return system_clock::time_point::max();
else if (dt < system_clock::duration(0) and system_now <= system_clock::time_point::min() - dt)
return system_clock::time_point::min();
return system_now + dt;
}
system_clock::time_point
to_system_time(time_point t)
{
return to_system_time(t, clock::now(), system_clock::now());
}
time_point
from_time_t(std::time_t t)
{
return from_system_time(system_clock::from_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)
{
return system_clock::to_time_t(to_system_time(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
-5
View File
@@ -202,9 +202,6 @@ Value::Value(const Json::Value& json)
const auto& jprio = json["prio"];
if (jprio.isIntegral())
priority = jprio.asUInt();
const auto& jpt = json[VALUE_KEY_PUSHTYPE];
if (jpt.isString())
pushType = jpt.asString();
}
Json::Value
@@ -231,8 +228,6 @@ Value::toJson() const
}
if (priority)
val["prio"] = priority;
if (not pushType.empty())
val[VALUE_KEY_PUSHTYPE] = pushType;
return val;
}
+6 -27
View File
@@ -71,21 +71,11 @@ public:
{
std::vector<Sp<Value>> expired_values;
for (auto it = values.begin(); it != values.end();) {
auto effectiveExpiration = it->second.expiration;
if (status == ListenSyncStatus::SYNCED)
// While the listen is synced, the remote node pushes
// explicit expirations and periodic refreshes. Add a
// grace period proportional to the value's type
// expiration so that a single lost UDP refresh
// notification does not cause a premature local drop.
// A grace of one type-expiration covers exactly one
// missed re-announcement cycle.
effectiveExpiration += it->second.typeExpiration;
if (effectiveExpiration <= now) {
if (it->second.expiration <= now) {
expired_values.emplace_back(std::move(it->second.data));
it = values.erase(it);
} else {
next = std::min(next, effectiveExpiration);
next = std::min(next, it->second.expiration);
++it;
}
}
@@ -158,14 +148,12 @@ private:
Sp<Value> data {};
time_point created {};
time_point expiration {};
duration typeExpiration {}; /* value type's expiration period, used as grace when synced */
CacheValueStorage() {}
CacheValueStorage(const Sp<Value>& v, time_point t, time_point e, duration te)
CacheValueStorage(const Sp<Value>& v, time_point t, time_point e)
: data(v)
, created(t)
, expiration(e)
, typeExpiration(te)
{}
};
@@ -178,22 +166,15 @@ private:
{
std::vector<Sp<Value>> nvals;
for (const auto& value : new_values) {
auto te = types.getType(value->type).expiration;
auto v = values.find(value->id);
if (v == values.end()) {
// new value
nvals.emplace_back(value);
values.emplace(value->id, CacheValueStorage(value, now, now + te, te));
values.emplace(value->id, CacheValueStorage(value, now, now + types.getType(value->type).expiration));
} else {
// refreshed value
v->second.created = now;
v->second.expiration = now + te;
v->second.typeExpiration = te;
if (value->seq > v->second.data->seq) {
// edited value (same rule as OpValueCache): emit as add
v->second.data = value;
nvals.emplace_back(value);
}
v->second.expiration = now + types.getType(v->second.data->type).expiration;
}
}
CallbackQueue ret;
@@ -220,10 +201,8 @@ private:
auto v = values.find(vid);
if (v == values.end())
return;
auto te = types.getType(v->second.data->type).expiration;
v->second.created = now;
v->second.expiration = now + te;
v->second.typeExpiration = te;
v->second.expiration = now + types.getType(v->second.data->type).expiration;
}
};
+2 -188
View File
@@ -59,24 +59,6 @@ CryptoTester::testSignatureEncryption()
}
}
void
CryptoTester::testPublicKeyPemIsPlainText()
{
auto key = dht::crypto::PrivateKey::generate();
auto pem = key.getPublicKey().toString();
// The exported key is PEM text: it must not carry the terminating null
// byte GnuTLS counts in the exported size, which would otherwise be
// embedded in anything the string is serialized into.
CPPUNIT_ASSERT(not pem.empty());
CPPUNIT_ASSERT_EQUAL(std::string::npos, pem.find('\0'));
CPPUNIT_ASSERT_EQUAL('\n', pem.back());
// The exported text still reads back as the same key.
dht::crypto::PublicKey reloaded(dht::Blob(pem.begin(), pem.end()));
CPPUNIT_ASSERT_EQUAL(key.getPublicKey().getId(), reloaded.getId());
}
void
CryptoTester::testCertificateRevocation()
{
@@ -113,159 +95,10 @@ CryptoTester::testCertificateRevocation()
CPPUNIT_ASSERT_MESSAGE(v.toString(), v);
}
namespace {
/** Read a CRL Number extension blob (big-endian, variable length) as an integer. */
uint64_t
crlNumberValue(const dht::Blob& number)
{
uint64_t value {0};
for (uint8_t byte : number)
value = (value << 8) | byte;
return value;
}
} // namespace
void
CryptoTester::testRevocationListNumber()
{
auto ca = dht::crypto::generateIdentity("ca");
auto device = dht::crypto::generateIdentity("dev", ca);
dht::crypto::RevocationList crl;
CPPUNIT_ASSERT_EQUAL(uint64_t(0), crlNumberValue(crl.getNumber()));
crl.revoke(*device.second);
crl.sign(ca);
auto first = crl.getNumber();
// An unnumbered list gets a random number on its first signature.
CPPUNIT_ASSERT(not first.empty());
CPPUNIT_ASSERT(crlNumberValue(first) != 0);
// Re-signing keeps the same lineage and moves the number forward.
crl.sign(ca);
auto second = crl.getNumber();
CPPUNIT_ASSERT_EQUAL(crlNumberValue(first) + 1, crlNumberValue(second));
crl.sign(ca);
CPPUNIT_ASSERT_EQUAL(crlNumberValue(first) + 2, crlNumberValue(crl.getNumber()));
// The number is a DER integer, so it is stored with leading zeros stripped.
// Incrementing must still work for numbers shorter than 8 bytes.
for (uint64_t start : {uint64_t(1), uint64_t(0x7f), uint64_t(0x80), uint64_t(0xffff)}) {
dht::crypto::RevocationList small;
small.revoke(*device.second);
small.sign(ca, {}, start);
CPPUNIT_ASSERT_EQUAL(start, crlNumberValue(small.getNumber()));
small.sign(ca);
CPPUNIT_ASSERT_EQUAL(start + 1, crlNumberValue(small.getNumber()));
small.sign(ca);
CPPUNIT_ASSERT_EQUAL(start + 2, crlNumberValue(small.getNumber()));
}
}
void
CryptoTester::testRevocationListExplicitNumber()
{
auto ca = dht::crypto::generateIdentity("ca");
auto device = dht::crypto::generateIdentity("dev", ca);
// Values chosen to cover leading zero bytes, a full-width number and the largest
// value that still encodes as a positive 8-byte DER integer.
const std::vector<uint64_t> numbers {1,
42,
0xff,
0x100,
0x0000'0001'0000'0000ull,
0x0000'02a1'0000'0001ull,
0x7fff'ffff'ffff'ffffull};
for (auto number : numbers) {
dht::crypto::RevocationList crl;
crl.revoke(*device.second);
crl.sign(ca, {}, number);
CPPUNIT_ASSERT_EQUAL(number, crlNumberValue(crl.getNumber()));
CPPUNIT_ASSERT(crl.isSignedBy(*ca.second));
CPPUNIT_ASSERT(crl.isRevoked(*device.second));
// The number must survive a pack/unpack round-trip.
dht::crypto::RevocationList unpacked(crl.getPacked());
CPPUNIT_ASSERT_EQUAL(number, crlNumberValue(unpacked.getNumber()));
}
// An explicit number overrides the increment, in both directions.
dht::crypto::RevocationList crl;
crl.revoke(*device.second);
crl.sign(ca, {}, 1000);
CPPUNIT_ASSERT_EQUAL(uint64_t(1000), crlNumberValue(crl.getNumber()));
crl.sign(ca, {}, 5);
CPPUNIT_ASSERT_EQUAL(uint64_t(5), crlNumberValue(crl.getNumber()));
// Passing 0 falls back to the default behavior: increment the current number.
crl.sign(ca, {}, 0);
CPPUNIT_ASSERT_EQUAL(uint64_t(6), crlNumberValue(crl.getNumber()));
}
void
CryptoTester::testRevocationListPartitionedNumber()
{
// Two devices holding the same authority issue revocation lists independently.
// RFC 5280 5.2.3 requires CRL numbers to increase monotonically for a given CRL
// scope and issuer; partitioning the low bits by device keeps each sequence
// monotonic while making collisions between devices impossible.
constexpr unsigned TAG_BITS = 24;
constexpr uint64_t TAG_A = 0x0a0b0c;
constexpr uint64_t TAG_B = 0x0a0b0d;
auto ca = dht::crypto::generateIdentity("ca");
auto account = dht::crypto::generateIdentity("acc", ca, 4096, true);
auto deviceA = dht::crypto::generateIdentity("devA", account);
auto deviceB = dht::crypto::generateIdentity("devB", account);
uint64_t previous {0};
dht::crypto::RevocationList crlA;
for (unsigned i = 1; i <= 4; i++) {
crlA.revoke(*deviceA.second);
crlA.sign(account, {}, (uint64_t(i) << TAG_BITS) | TAG_A);
auto number = crlNumberValue(crlA.getNumber());
CPPUNIT_ASSERT_EQUAL(TAG_A, number & ((uint64_t(1) << TAG_BITS) - 1));
CPPUNIT_ASSERT(number > previous);
CPPUNIT_ASSERT_EQUAL(uint64_t(0), number >> 63);
previous = number;
}
// Both devices branch from the same counter but never mint the same number.
dht::crypto::RevocationList branchA;
branchA.revoke(*deviceA.second);
branchA.sign(account, {}, (uint64_t(9) << TAG_BITS) | TAG_A);
dht::crypto::RevocationList branchB;
branchB.revoke(*deviceB.second);
branchB.sign(account, {}, (uint64_t(9) << TAG_BITS) | TAG_B);
CPPUNIT_ASSERT(crlNumberValue(branchA.getNumber()) != crlNumberValue(branchB.getNumber()));
// Both are valid and revoke only their own target.
CPPUNIT_ASSERT(branchA.isSignedBy(*account.second));
CPPUNIT_ASSERT(branchB.isSignedBy(*account.second));
CPPUNIT_ASSERT(branchA.isRevoked(*deviceA.second));
CPPUNIT_ASSERT(not branchA.isRevoked(*deviceB.second));
CPPUNIT_ASSERT(branchB.isRevoked(*deviceB.second));
CPPUNIT_ASSERT(not branchB.isRevoked(*deviceA.second));
// A number minted by an older client is fully random, so bit 63 may be set. Masking
// it out and taking the counter part still yields a strictly greater number.
constexpr uint64_t legacy = 0xdead'beef'0123'4567ull;
dht::crypto::RevocationList legacyCrl;
legacyCrl.revoke(*deviceA.second);
legacyCrl.sign(account, {}, legacy);
auto counter = ((crlNumberValue(legacyCrl.getNumber()) & ~(uint64_t(1) << 63)) >> TAG_BITS) + 1;
legacyCrl.sign(account, {}, (counter << TAG_BITS) | TAG_A);
CPPUNIT_ASSERT(crlNumberValue(legacyCrl.getNumber()) > (legacy & ~(uint64_t(1) << 63)));
CPPUNIT_ASSERT_EQUAL(uint64_t(0), crlNumberValue(legacyCrl.getNumber()) >> 63);
}
void
CryptoTester::testCertificateRequest()
{ // Generate CA
{
// Generate CA
auto ca = dht::crypto::generateIdentity("Test CA");
// Generate signed request
@@ -316,25 +149,6 @@ CryptoTester::testCertificateSerialNumber()
};
auto serial = dht::crypto::Certificate(cert_pem).getSerialNumber();
CPPUNIT_ASSERT(std::equal(SERIAL.begin(), SERIAL.end(), serial.begin(), serial.end()));
auto key = dht::crypto::PrivateKey::generateEC();
for (unsigned i = 0; i < 32; i++) {
auto generated = dht::crypto::Certificate::generate(key, "generated", {});
auto generatedSerial = generated.getSerialNumber();
CPPUNIT_ASSERT(not generatedSerial.empty());
CPPUNIT_ASSERT_EQUAL(uint8_t {0}, uint8_t(generatedSerial.front() & 0x80));
CPPUNIT_ASSERT(std::any_of(generatedSerial.begin(), generatedSerial.end(), [](uint8_t byte) { return byte != 0; }));
}
}
void
CryptoTester::testEcKeyCurve()
{
auto key = dht::crypto::PrivateKey::generateEC(GNUTLS_ECC_CURVE_SECP256R1);
unsigned int bits = 0;
CPPUNIT_ASSERT_EQUAL(static_cast<int>(GNUTLS_PK_EC),
gnutls_x509_privkey_get_pk_algorithm2(key.x509_key, &bits));
CPPUNIT_ASSERT_EQUAL(256u, bits);
}
void
-25
View File
@@ -12,14 +12,9 @@ class CryptoTester : public CppUnit::TestFixture
{
CPPUNIT_TEST_SUITE(CryptoTester);
CPPUNIT_TEST(testSignatureEncryption);
CPPUNIT_TEST(testPublicKeyPemIsPlainText);
CPPUNIT_TEST(testCertificateRevocation);
CPPUNIT_TEST(testRevocationListNumber);
CPPUNIT_TEST(testRevocationListExplicitNumber);
CPPUNIT_TEST(testRevocationListPartitionedNumber);
CPPUNIT_TEST(testCertificateRequest);
CPPUNIT_TEST(testCertificateSerialNumber);
CPPUNIT_TEST(testEcKeyCurve);
CPPUNIT_TEST(testOcsp);
CPPUNIT_TEST(testAesEncryption);
CPPUNIT_TEST(testAesEncryptionWithMultipleKeySizes);
@@ -41,26 +36,10 @@ public:
* Test data signature, encryption and decryption
*/
void testSignatureEncryption();
/**
* Test that an exported public key is text only, with no embedded null byte
*/
void testPublicKeyPemIsPlainText();
/**
* Test certificate generation, validation and revocation
*/
void testCertificateRevocation();
/**
* Test the default CRL Number behavior: random on first signature, incremented after
*/
void testRevocationListNumber();
/**
* Test signing a revocation list with a caller-provided CRL Number
*/
void testRevocationListExplicitNumber();
/**
* Test that issuers sharing an authority can partition the CRL Number space
*/
void testRevocationListPartitionedNumber();
/**
* Test certificate requests
*/
@@ -69,10 +48,6 @@ public:
* Test certificate serial number extraction
*/
void testCertificateSerialNumber();
/**
* Test EC private key generation with an explicit curve
*/
void testEcKeyCurve();
/**
* Test OCSP
*/
+24 -1471
View File
File diff suppressed because it is too large. Load diff
+13 -35
View File
@@ -8,6 +8,7 @@
#include <opendht/dhtrunner.h>
#include <opendht/dht_proxy_server.h>
#include <opendht/log.h>
namespace test {
@@ -16,21 +17,13 @@ class DhtProxyTester : public CppUnit::TestFixture
CPPUNIT_TEST_SUITE(DhtProxyTester);
CPPUNIT_TEST(testGetPut);
CPPUNIT_TEST(testListen);
CPPUNIT_TEST(testListenValueEdit);
CPPUNIT_TEST(testPushListenValueEdit);
CPPUNIT_TEST(testResubscribeGetValues);
CPPUNIT_TEST(testPushNotification);
CPPUNIT_TEST(testProxyServerClientFullChain);
CPPUNIT_TEST(testPutGet40KChars);
CPPUNIT_TEST(testFuzzy);
CPPUNIT_TEST(testShutdownStop);
CPPUNIT_TEST(testGetAfterListen);
CPPUNIT_TEST(testListenDuplicatePut);
CPPUNIT_TEST(testPushMultiValueEditExpire);
CPPUNIT_TEST(testPushRefreshNoSpuriousExpire);
CPPUNIT_TEST(testProxyListenEditChain);
CPPUNIT_TEST(testPushRapidEditsNoPhantom);
CPPUNIT_TEST(testPushRefreshAfterEditKeepsLatest);
#ifdef OPENDHT_PUSH_NOTIFICATIONS
CPPUNIT_TEST(testPushNotificationPriority);
#endif
CPPUNIT_TEST_SUITE_END();
public:
@@ -51,28 +44,11 @@ public:
* Test listen
*/
void testListen();
/**
* Test that editing a value only triggers an add callback via proxy, no expire
*/
void testListenValueEdit();
/**
* Test that editing a value only triggers an add callback via push,
* no phantom expire, and that real expiration is received.
*/
void testPushListenValueEdit();
/**
* When a proxy redo a subscribe on the proxy
* it should retrieve existant values
*/
void testResubscribeGetValues();
/**
* Test push notification mechanism and OpValueCache
*/
void testPushNotification();
/**
* Test end-to-end chain through proxy server and proxy client
*/
void testProxyServerClientFullChain();
/**
* Test MTU put/get on dht
*/
@@ -81,13 +57,15 @@ public:
void testFuzzy();
void testShutdownStop();
void testGetAfterListen();
void testListenDuplicatePut();
void testPushMultiValueEditExpire();
void testPushRefreshNoSpuriousExpire();
void testProxyListenEditChain();
void testPushRapidEditsNoPhantom();
void testPushRefreshAfterEditKeepsLatest();
#ifdef OPENDHT_PUSH_NOTIFICATIONS
/**
* The proxy must only request a high-priority push for real-time call
* signaling (audioCall/videoCall at priority 0); everything else stays at
* normal priority.
*/
void testPushNotificationPriority();
#endif
private:
dht::DhtRunner::Config clientConfig {};
+2 -2
View File
@@ -37,7 +37,7 @@ DhtProxyClientTester::testResubscribeUsesKeyRoute()
#ifdef OPENDHT_PUSH_NOTIFICATIONS
DhtProxyClient client({}, {}, [] {}, "http://127.0.0.1:8080", "OpenDHT-Test", "client-id", "push-token");
DhtProxyClient::Listener listener {ValueCallback([](const std::vector<Sp<Value>>&, bool) { return true; })};
DhtProxyClient::Listener listener {OpValueCache([](const std::vector<Sp<Value>>&, bool) { return true; })};
listener.opstate = std::make_shared<DhtProxyClient::OperationState>();
listener.cb = [](const std::vector<Sp<Value>>&, bool, system_clock::time_point) {
return true;
@@ -67,7 +67,7 @@ DhtProxyClientTester::testSetPushNotificationTokenResubscribesWithNewToken()
auto& search = client.searches_[key];
auto [it, inserted] = search.listeners.emplace(std::piecewise_construct,
std::forward_as_tuple(1),
std::forward_as_tuple(ValueCallback(
std::forward_as_tuple(OpValueCache(
[](const std::vector<Sp<Value>>&, bool) { return true; })));
CPPUNIT_ASSERT(inserted);
+50 -691
View File
@@ -9,8 +9,6 @@
#include <future>
#include <mutex>
#include <condition_variable>
#include <fstream>
#include <cstdio>
using namespace std::chrono_literals;
using namespace std::literals;
@@ -91,42 +89,6 @@ DhtRunnerTester::testGetPut()
CPPUNIT_ASSERT(vals.front()->data == val_data);
}
void
DhtRunnerTester::testStoreEmptyValue()
{
dht::DhtRunner::Config config;
config.dht_config.node_config.max_peer_req_per_sec = -1;
config.dht_config.node_config.max_req_per_sec = -1;
config.dht_config.node_config.max_store_size = 100; // Small size to force expireStore()
config.dht_config.node_config.max_store_keys = -1;
dht::DhtRunner testNode;
testNode.run(0, config);
testNode.bootstrap(node1.getBound());
auto key = testNode.getId();
auto val = std::make_shared<dht::Value>();
val->data.clear(); // Ensure size is 0
CPPUNIT_ASSERT_EQUAL((size_t) 0, val->size());
std::promise<bool> p;
testNode.put(key, val, [&](bool ok) { p.set_value(ok); });
p.get_future().get(); // Wait for put to finish
// Put a large value to exceed max_store_size and trigger expireStore()
auto key2 = dht::InfoHash::get("large_value_test");
auto val2 = std::make_shared<dht::Value>();
val2->data = std::vector<uint8_t>(200, 0); // 200 bytes
std::promise<bool> p2;
testNode.put(key2, val2, [&](bool ok) { p2.set_value(ok); });
p2.get_future().get(); // Wait for put to finish
// Wait a bit to ensure the value is processed
std::this_thread::sleep_for(std::chrono::milliseconds(200));
testNode.join();
}
void
DhtRunnerTester::testPutDuplicate()
{
@@ -246,42 +208,6 @@ DhtRunnerTester::testImportValuesPreservesRemoteQuota()
restored.join();
}
void
DhtRunnerTester::testExportValuesUsesSystemTime()
{
auto key = dht::InfoHash::get("export-system-time");
const auto created = dht::clock::now();
msgpack::sbuffer importBuffer;
msgpack::packer<msgpack::sbuffer> pk(&importBuffer);
pk.pack_array(1);
pk.pack_array(2);
pk.pack(created.time_since_epoch().count());
dht::Value {std::string("persisted")}.msgpack_pack(pk);
node1.importValues({
dht::ValuesExport {key, {importBuffer.data(), importBuffer.data() + importBuffer.size()}}
});
auto exported = node1.exportValues();
CPPUNIT_ASSERT_EQUAL(size_t(1), exported.size());
msgpack::unpacked msg;
msgpack::unpack(msg, reinterpret_cast<const char*>(exported.front().second.data()), exported.front().second.size());
const auto values = msg.get();
CPPUNIT_ASSERT(values.type == msgpack::type::ARRAY);
CPPUNIT_ASSERT_EQUAL(uint32_t(1), values.via.array.size);
const auto& persistedValue = values.via.array.ptr[0];
CPPUNIT_ASSERT_EQUAL(uint32_t(4), persistedValue.via.array.size);
CPPUNIT_ASSERT(persistedValue.via.array.ptr[0].type == msgpack::type::EXT);
CPPUNIT_ASSERT(persistedValue.via.array.ptr[3].type == msgpack::type::EXT);
const auto persistedCreated = persistedValue.via.array.ptr[0].as<std::chrono::system_clock::time_point>();
const auto persistedExpiration = persistedValue.via.array.ptr[3].as<std::chrono::system_clock::time_point>();
const auto delta = std::chrono::system_clock::now() - persistedCreated;
CPPUNIT_ASSERT(delta >= std::chrono::system_clock::duration::zero());
CPPUNIT_ASSERT(delta < 5s);
CPPUNIT_ASSERT(persistedExpiration > persistedCreated);
}
void
DhtRunnerTester::testImportValuesPreservesStoredExpiration()
{
@@ -339,25 +265,14 @@ DhtRunnerTester::testImportValuesPreservesStoredExpiration()
void
DhtRunnerTester::testImportPermanentValueExpires()
{
static const dht::ValueType SHORT_TYPE {201, "Short perm", 500ms};
dht::DhtRunner::Config config;
config.dht_config.node_config.max_peer_req_per_sec = -1;
config.dht_config.node_config.max_req_per_sec = -1;
config.dht_config.node_config.max_store_size = -1;
config.dht_config.node_config.max_store_keys = -1;
dht::DhtRunner restored;
restored.run(0, config);
restored.registerInsecureType(SHORT_TYPE);
// Ensure the runner is waiting on a previously computed scheduler deadline.
std::promise<void> loopCompleted;
auto loopCompletedFuture = loopCompleted.get_future();
restored.getNodeInfo([&loopCompleted](auto) { loopCompleted.set_value(); });
getFutureValue(std::move(loopCompletedFuture));
// Simulate importing a value that was originally stored as permanent
// (expiration = time_point::max()). After import the permanent flag is lost,
// so the value must expire within the type's normal expiration window.
static const dht::ValueType SHORT_TYPE {201, "Short perm", 2s};
auto key = dht::InfoHash::get("import-permanent-expiration");
auto now = dht::clock::now();
msgpack::sbuffer buffer;
msgpack::packer<msgpack::sbuffer> pk(&buffer);
pk.pack_array(1);
@@ -365,33 +280,63 @@ DhtRunnerTester::testImportPermanentValueExpires()
pk.pack(now.time_since_epoch().count());
dht::Value {SHORT_TYPE.id, std::string("was-permanent")}.msgpack_pack(pk);
pk.pack_bin(0);
// Permanent values are exported with time_point::max() expiration
pk.pack(dht::clock::time_point::max().time_since_epoch().count());
dht::DhtRunner restored;
dht::DhtRunner::Config config;
config.dht_config.node_config.max_peer_req_per_sec = -1;
config.dht_config.node_config.max_req_per_sec = -1;
config.dht_config.node_config.max_store_size = -1;
config.dht_config.node_config.max_store_keys = -1;
restored.run(0, config);
restored.importValues({
dht::ValuesExport {key, {buffer.data(), buffer.data() + buffer.size()}}
});
auto containsValue = [&] {
for (const auto& [exportedKey, data] : restored.exportValues()) {
if (exportedKey != key)
continue;
// Value should be present right after import
auto exportedBefore = restored.exportValues();
CPPUNIT_ASSERT(!exportedBefore.empty());
// Wait longer than the default USER_DATA expiration (10 min) is not
// practical, but the type is unregistered so the default 10 min expiration
// applies. Instead, register the short type so the cap uses 2 s.
// Re-import into a fresh node that knows the short type.
dht::DhtRunner restored2;
restored2.run(0, config);
restored2.registerInsecureType(SHORT_TYPE);
restored2.importValues({
dht::ValuesExport {key, {buffer.data(), buffer.data() + buffer.size()}}
});
CPPUNIT_ASSERT(!restored2.exportValues().empty());
// Wait for the short expiration to pass
std::this_thread::sleep_for(3s);
auto restoredValues = restored2.exportValues();
bool found = false;
for (const auto& ve : restoredValues) {
if (ve.first == key) {
msgpack::unpacked msg;
msgpack::unpack(msg, reinterpret_cast<const char*>(data.data()), data.size());
msgpack::unpack(msg, (const char*) ve.second.data(), ve.second.size());
auto valarr = msg.get();
CPPUNIT_ASSERT(valarr.type == msgpack::type::ARRAY);
return valarr.via.array.size != 0;
// The value should have expired
CPPUNIT_ASSERT_EQUAL(uint32_t(0), valarr.via.array.size);
found = true;
}
return false;
};
CPPUNIT_ASSERT(containsValue());
auto timeout = dht::clock::now() + 2s;
while (containsValue() and dht::clock::now() < timeout)
std::this_thread::sleep_for(10ms);
CPPUNIT_ASSERT(!containsValue());
}
// Either the key was cleaned up entirely or it was found with 0 values
CPPUNIT_ASSERT(found || restoredValues.empty()
|| std::none_of(restoredValues.begin(), restoredValues.end(),
[&](const auto& ve) { return ve.first == key; }));
restored.shutdown();
restored.join();
restored2.shutdown();
restored2.join();
}
void
@@ -595,143 +540,6 @@ DhtRunnerTester::testShutdownCompletesWithPendingPut()
clientNode.join();
}
void
DhtRunnerTester::testSavedPortFallback()
{
// A saved port that is already taken by another process (e.g. fast
// application restart while the old instance still holds it) must not
// prevent the node from starting: it should fall back to a random port.
dht::DhtRunner::Config config;
config.dht_config.node_config.persist_path = "/tmp/opendht_test_port_fallback";
std::string portFile = config.dht_config.node_config.persist_path + "_port.txt";
// node1 (from setUp) holds a port; save it as our persisted port.
auto takenPort = node1.getBoundPort();
{
std::ofstream out(portFile);
out << takenPort << std::endl << takenPort << std::endl;
}
dht::DhtRunner testNode;
CPPUNIT_ASSERT_NO_THROW(testNode.run(0, config));
CPPUNIT_ASSERT(testNode.getBoundPort());
CPPUNIT_ASSERT(testNode.getBoundPort() != takenPort);
testNode.shutdown();
testNode.join();
std::remove(portFile.c_str());
std::remove((config.dht_config.node_config.persist_path + "_port.txt").c_str());
}
void
DhtRunnerTester::testListenValueEdit()
{
// Register an editable type on both nodes so the DHT allows edits.
static constexpr dht::ValueType::Id EDITABLE_TYPE_ID = 9999;
const dht::ValueType editableType {EDITABLE_TYPE_ID,
"editable-test",
std::chrono::seconds(2),
dht::ValueType::DEFAULT_STORE_POLICY,
[](dht::InfoHash,
const std::shared_ptr<dht::Value>&,
std::shared_ptr<dht::Value>&,
const dht::InfoHash&,
const dht::SockAddr&) { return true; }};
node1.registerType(editableType);
node2.registerType(editableType);
auto key = dht::InfoHash::get("listenValueEdit");
auto identity = dht::crypto::generateIdentity("ListenEditTester");
std::mutex mtx;
std::condition_variable cv;
std::vector<std::pair<dht::Value::Id, bool>> events; // (id, expired?)
auto ftoken = node1.listen(key, [&](const std::vector<std::shared_ptr<dht::Value>>& vals, bool expired) {
std::lock_guard lk(mtx);
for (const auto& v : vals) {
events.emplace_back(v->id, expired);
}
cv.notify_all();
return true;
});
// Put initial value (signed, editable type)
auto val1 = std::make_shared<dht::Value>("version1");
val1->type = EDITABLE_TYPE_ID;
val1->id = 42;
val1->seq = 1;
val1->sign(*identity.first);
{
std::promise<bool> p;
node2.put(key, val1, [&](bool ok) { p.set_value(ok); });
CPPUNIT_ASSERT(getFutureValue(p.get_future()));
}
// Wait for the first value to be received
{
std::unique_lock lk(mtx);
CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] {
return std::any_of(events.begin(), events.end(), [](const auto& e) { return !e.second; });
}));
}
// Edit the value (same id, higher seq)
auto val2 = std::make_shared<dht::Value>("version2");
val2->type = EDITABLE_TYPE_ID;
val2->id = 42;
val2->seq = 2;
val2->sign(*identity.first);
{
std::promise<bool> p;
node2.put(key, val2, [&](bool ok) { p.set_value(ok); });
CPPUNIT_ASSERT(getFutureValue(p.get_future()));
}
// Wait for the edited value to be received
{
std::unique_lock lk(mtx);
CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] {
int adds = 0;
for (const auto& e : events)
if (!e.second)
adds++;
return adds >= 2;
}));
}
// Give some extra time for any stray callbacks
std::this_thread::sleep_for(500ms);
// Verify: we should have exactly 2 add events, no expire events.
// On value edition the listener should see only the new version
// (as an add), not an expiration of the old version.
{
std::lock_guard lk(mtx);
int addCount = 0;
int expireCount = 0;
for (const auto& e : events) {
if (e.second)
expireCount++;
else
addCount++;
}
CPPUNIT_ASSERT_EQUAL_MESSAGE("Should receive 2 add callbacks (original + edit)", 2, addCount);
CPPUNIT_ASSERT_EQUAL_MESSAGE("Should receive no expire callbacks on edit", 0, expireCount);
}
// Now wait for the edited value to actually expire.
// Type expiration is 2s. The cache may add a grace period.
{
std::unique_lock lk(mtx);
CPPUNIT_ASSERT_MESSAGE("Edited value should eventually expire", cv.wait_for(lk, 60s, [&] {
return std::any_of(events.begin(), events.end(), [](const auto& e) { return e.second; });
}));
}
node1.cancelListen(key, ftoken.get());
}
void
DhtRunnerTester::testListen()
{
@@ -944,7 +752,7 @@ DhtRunnerTester::testIdOps()
valueCountEdit++;
cv.notify_all();
});
auto listenToken = node2.listen(key2, [&](const std::vector<std::shared_ptr<dht::Value>>& values, bool /*expired*/) {
node2.listen(key2, [&](const std::vector<std::shared_ptr<dht::Value>>& values, bool /*expired*/) {
for (const auto& v : values) {
if (v->seq == 0)
CPPUNIT_ASSERT_EQUAL("v1"s, dht::unpackMsg<std::string>(v->data));
@@ -988,8 +796,6 @@ DhtRunnerTester::testIdOps()
});
std::unique_lock lk(mutex);
CPPUNIT_ASSERT(cv.wait_for(lk, 20s, [&] { return valueCountEdit == 4u; }));
lk.unlock();
node2.cancelListen(key2, listenToken.get());
}
void
@@ -1080,451 +886,4 @@ DhtRunnerTester::testMultithread()
CPPUNIT_ASSERT_EQUAL(2 * N, putOkCount);
}
void
DhtRunnerTester::testGetAfterListen()
{
// Test that get() after a synced listen delivers values exactly once
// from the cache, and that the GetCallback return value is honored.
auto key = dht::InfoHash::get("getAfterListen");
// Put a value first
auto val = std::make_shared<dht::Value>("cached_val");
{
std::promise<bool> p;
node2.put(key, val, [&](bool ok) { p.set_value(ok); });
CPPUNIT_ASSERT(getFutureValue(p.get_future()));
}
// Start a listen on node1 and wait for it to sync
std::mutex mtx;
std::condition_variable cv;
std::atomic_int listenAddCount {0};
auto ftoken = node1.listen(key, [&](const std::vector<std::shared_ptr<dht::Value>>& vals, bool expired) {
if (!expired)
listenAddCount += vals.size();
cv.notify_all();
return true;
});
// Wait for listen to receive the value
{
std::unique_lock lk(mtx);
CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] { return listenAddCount.load() >= 1; }));
}
// Now do a get — should be served from the synced cache
auto vals = getFutureValue(node1.get(key));
CPPUNIT_ASSERT_MESSAGE("get() should return values", not vals.empty());
CPPUNIT_ASSERT(val->data == vals.front()->data);
// The listen callback should not have been called again by the get
std::this_thread::sleep_for(200ms);
CPPUNIT_ASSERT_EQUAL_MESSAGE("Listen should not fire extra callbacks from get()", 1, listenAddCount.load());
node1.cancelListen(key, ftoken.get());
}
void
DhtRunnerTester::testListenDuplicatePut()
{
// Test that putting the exact same value multiple times only triggers
// one add callback in the listener (no duplicate delivery).
auto key = dht::InfoHash::get("listenDupPut");
std::mutex mtx;
std::condition_variable cv;
std::vector<std::pair<dht::Value::Id, bool>> events;
auto ftoken = node1.listen(key, [&](const std::vector<std::shared_ptr<dht::Value>>& vals, bool expired) {
std::lock_guard lk(mtx);
for (const auto& v : vals)
events.emplace_back(v->id, expired);
cv.notify_all();
return true;
});
// Put the same value 3 times
auto val = std::make_shared<dht::Value>("duplicate_test");
val->id = 123;
for (int i = 0; i < 3; i++) {
std::promise<bool> p;
node2.put(key, val, [&](bool ok) { p.set_value(ok); });
CPPUNIT_ASSERT(getFutureValue(p.get_future()));
}
// Wait for at least 1 add to arrive
{
std::unique_lock lk(mtx);
CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] {
return std::any_of(events.begin(), events.end(), [](const auto& e) { return !e.second; });
}));
}
// Give time for any spurious callbacks
std::this_thread::sleep_for(500ms);
// Should have exactly 1 add, 0 expires
{
std::lock_guard lk(mtx);
int addCount = 0, expireCount = 0;
for (const auto& e : events) {
if (e.second)
expireCount++;
else
addCount++;
}
CPPUNIT_ASSERT_EQUAL_MESSAGE("Duplicate puts should produce only 1 add callback", 1, addCount);
CPPUNIT_ASSERT_EQUAL_MESSAGE("No expire callbacks expected", 0, expireCount);
}
node1.cancelListen(key, ftoken.get());
}
void
DhtRunnerTester::testListenMultiSourceExpire()
{
// Test that when multiple nodes store the same value, the listener sees
// exactly 1 add (no duplicates). When the value expires naturally, the
// listener sees exactly 1 expire.
static constexpr dht::ValueType::Id SHORT_TYPE_ID = 8888;
const dht::ValueType shortType {SHORT_TYPE_ID,
"short-lived",
std::chrono::seconds(2),
dht::ValueType::DEFAULT_STORE_POLICY,
[](dht::InfoHash,
const std::shared_ptr<dht::Value>&,
std::shared_ptr<dht::Value>&,
const dht::InfoHash&,
const dht::SockAddr&) { return true; }};
node1.registerType(shortType);
node2.registerType(shortType);
auto key = dht::InfoHash::get("multiSourceExpire");
auto identity = dht::crypto::generateIdentity("MultiSourceTester");
std::mutex mtx;
std::condition_variable cv;
std::vector<std::pair<dht::Value::Id, bool>> events;
// node1 listens; node1 and node2 put
auto ftoken = node1.listen(key, [&](const std::vector<std::shared_ptr<dht::Value>>& vals, bool expired) {
std::lock_guard lk(mtx);
for (const auto& v : vals)
events.emplace_back(v->id, expired);
cv.notify_all();
return true;
});
// Both node1 and node2 put the same value (same id, same seq)
auto val = std::make_shared<dht::Value>("multi_source");
val->type = SHORT_TYPE_ID;
val->id = 55;
val->seq = 1;
val->sign(*identity.first);
{
std::promise<bool> p1, p2;
node1.put(key, val, [&](bool ok) { p1.set_value(ok); });
node2.put(key, val, [&](bool ok) { p2.set_value(ok); });
CPPUNIT_ASSERT(getFutureValue(p1.get_future()));
CPPUNIT_ASSERT(getFutureValue(p2.get_future()));
}
// Wait for at least one add
{
std::unique_lock lk(mtx);
CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] {
return std::any_of(events.begin(), events.end(), [](const auto& e) { return !e.second; });
}));
}
// Allow time for duplicate callbacks
std::this_thread::sleep_for(1s);
// Should have exactly 1 add (deduplication in OpValueCache)
{
std::lock_guard lk(mtx);
int addCount = 0;
for (const auto& e : events)
if (!e.second)
addCount++;
CPPUNIT_ASSERT_EQUAL_MESSAGE("Multiple sources should produce only 1 add callback", 1, addCount);
}
// Wait for natural expiration (type is 2s + grace period)
{
std::unique_lock lk(mtx);
CPPUNIT_ASSERT_MESSAGE("Value should eventually expire", cv.wait_for(lk, 60s, [&] {
return std::any_of(events.begin(), events.end(), [](const auto& e) { return e.second; });
}));
}
// Should have exactly 1 expire (no duplicate expires from multiple sources)
{
std::lock_guard lk(mtx);
int expireCount = 0;
for (const auto& e : events)
if (e.second)
expireCount++;
CPPUNIT_ASSERT_EQUAL_MESSAGE("Should receive exactly 1 expire callback", 1, expireCount);
}
node1.cancelListen(key, ftoken.get());
}
void
DhtRunnerTester::testListenEditChainThenExpire()
{
// Chain of 5 rapid edits (seq 1→5) on the same value, then natural expiration.
// Must see 5 adds (one per edit), 0 expires during edits, then exactly 1 expire.
static constexpr dht::ValueType::Id CHAIN_TYPE_ID = 7654;
const dht::ValueType chainType {CHAIN_TYPE_ID,
"chain-edit",
std::chrono::seconds(2),
dht::ValueType::DEFAULT_STORE_POLICY,
[](dht::InfoHash,
const std::shared_ptr<dht::Value>&,
std::shared_ptr<dht::Value>&,
const dht::InfoHash&,
const dht::SockAddr&) { return true; }};
node1.registerType(chainType);
node2.registerType(chainType);
auto key = dht::InfoHash::get("editChainExpire");
auto identity = dht::crypto::generateIdentity("ChainEditTester");
std::mutex mtx;
std::condition_variable cv;
struct Event
{
dht::Value::Id id;
uint16_t seq;
bool expired;
};
std::vector<Event> events;
auto ftoken = node1.listen(key, [&](const std::vector<std::shared_ptr<dht::Value>>& vals, bool expired) {
std::lock_guard lk(mtx);
for (const auto& v : vals)
events.push_back({v->id, v->seq, expired});
cv.notify_all();
return true;
});
// Rapid chain of 5 edits — wait for the first to arrive before continuing
constexpr int NUM_EDITS = 5;
{
auto v = std::make_shared<dht::Value>("version1");
v->type = CHAIN_TYPE_ID;
v->id = 99;
v->seq = 1;
v->sign(*identity.first);
std::promise<bool> p;
node2.put(key, v, [&](bool ok) { p.set_value(ok); });
CPPUNIT_ASSERT(getFutureValue(p.get_future()));
}
// Wait for first add to arrive before sending more edits
{
std::unique_lock lk(mtx);
CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] {
return std::any_of(events.begin(), events.end(), [](const Event& e) { return !e.expired; });
}));
}
// Now send remaining edits rapidly
for (int seq = 2; seq <= NUM_EDITS; seq++) {
auto v = std::make_shared<dht::Value>("version" + std::to_string(seq));
v->type = CHAIN_TYPE_ID;
v->id = 99;
v->seq = seq;
v->sign(*identity.first);
std::promise<bool> p;
node2.put(key, v, [&](bool ok) { p.set_value(ok); });
CPPUNIT_ASSERT(getFutureValue(p.get_future()));
}
// Wait for edits to arrive (at least 2: initial + one or more updates)
// Note: rapid edits may coalesce — the DHT stores only the latest seq,
// so the listener may not see every intermediate version.
{
std::unique_lock lk(mtx);
CPPUNIT_ASSERT(cv.wait_for(lk, 15s, [&] {
int adds = 0;
for (const auto& e : events)
if (!e.expired)
adds++;
return adds >= 2;
}));
}
// Wait a bit more for any remaining events
std::this_thread::sleep_for(1s);
// Verify: multiple adds, 0 expires during edits (no phantom expiry)
int totalAdds;
{
std::lock_guard lk(mtx);
totalAdds = 0;
int expireCount = 0;
for (const auto& e : events) {
if (e.expired)
expireCount++;
else
totalAdds++;
}
CPPUNIT_ASSERT_MESSAGE("Should receive at least 2 add callbacks", totalAdds >= 2);
CPPUNIT_ASSERT_EQUAL_MESSAGE("No expires during edit chain", 0, expireCount);
// Verify the latest add has the highest seq
uint16_t maxSeq = 0;
for (const auto& e : events)
if (!e.expired && e.seq > maxSeq)
maxSeq = e.seq;
CPPUNIT_ASSERT_EQUAL_MESSAGE("Latest add should have the final seq", (uint16_t) NUM_EDITS, maxSeq);
}
// Wait for natural expiration
{
std::unique_lock lk(mtx);
CPPUNIT_ASSERT_MESSAGE("Value should eventually expire", cv.wait_for(lk, 60s, [&] {
return std::any_of(events.begin(), events.end(), [](const Event& e) { return e.expired; });
}));
}
// Verify exactly 1 expire with the latest seq
{
std::lock_guard lk(mtx);
int expireCount = 0;
uint16_t expiredSeq = 0;
for (const auto& e : events) {
if (e.expired) {
expireCount++;
expiredSeq = e.seq;
}
}
CPPUNIT_ASSERT_EQUAL_MESSAGE("Should receive exactly 1 expire", 1, expireCount);
CPPUNIT_ASSERT_EQUAL_MESSAGE("Expired value should be the latest version", (uint16_t) NUM_EDITS, expiredSeq);
}
node1.cancelListen(key, ftoken.get());
}
void
DhtRunnerTester::testListenMultiValuePartialExpire()
{
// Two values with different types: one short-lived (2s), one long-lived (60s).
// The short one expires while the long one stays. Verify isolation:
// no spurious expire for the long-lived value.
static constexpr dht::ValueType::Id SHORT_TYPE_ID = 3001;
static constexpr dht::ValueType::Id LONG_TYPE_ID = 3002;
const dht::ValueType shortType {SHORT_TYPE_ID,
"short-partial",
std::chrono::seconds(2),
dht::ValueType::DEFAULT_STORE_POLICY,
[](dht::InfoHash,
const std::shared_ptr<dht::Value>&,
std::shared_ptr<dht::Value>&,
const dht::InfoHash&,
const dht::SockAddr&) { return true; }};
const dht::ValueType longType {LONG_TYPE_ID,
"long-partial",
std::chrono::seconds(600),
dht::ValueType::DEFAULT_STORE_POLICY,
[](dht::InfoHash,
const std::shared_ptr<dht::Value>&,
std::shared_ptr<dht::Value>&,
const dht::InfoHash&,
const dht::SockAddr&) { return true; }};
node1.registerType(shortType);
node1.registerType(longType);
node2.registerType(shortType);
node2.registerType(longType);
auto key = dht::InfoHash::get("multiValuePartialExpire");
auto identity = dht::crypto::generateIdentity("PartialExpireTester");
std::mutex mtx;
std::condition_variable cv;
struct Event
{
dht::Value::Id id;
bool expired;
};
std::vector<Event> events;
auto ftoken = node1.listen(key, [&](const std::vector<std::shared_ptr<dht::Value>>& vals, bool expired) {
std::lock_guard lk(mtx);
for (const auto& v : vals)
events.push_back({v->id, expired});
cv.notify_all();
return true;
});
// Put short-lived value (id=1)
auto shortVal = std::make_shared<dht::Value>("short");
shortVal->type = SHORT_TYPE_ID;
shortVal->id = 1;
shortVal->seq = 1;
shortVal->sign(*identity.first);
{
std::promise<bool> p;
node2.put(key, shortVal, [&](bool ok) { p.set_value(ok); });
CPPUNIT_ASSERT(getFutureValue(p.get_future()));
}
// Put long-lived value (id=2)
auto longVal = std::make_shared<dht::Value>("long");
longVal->type = LONG_TYPE_ID;
longVal->id = 2;
longVal->seq = 1;
longVal->sign(*identity.first);
{
std::promise<bool> p;
node2.put(key, longVal, [&](bool ok) { p.set_value(ok); });
CPPUNIT_ASSERT(getFutureValue(p.get_future()));
}
// Wait for both adds
{
std::unique_lock lk(mtx);
CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] {
int adds = 0;
for (const auto& e : events)
if (!e.expired)
adds++;
return adds >= 2;
}));
}
// Wait for short-lived value to expire
{
std::unique_lock lk(mtx);
CPPUNIT_ASSERT_MESSAGE("Short-lived value should expire", cv.wait_for(lk, 60s, [&] {
return std::any_of(events.begin(), events.end(), [](const Event& e) { return e.id == 1 && e.expired; });
}));
}
// Give extra time for any spurious callbacks
std::this_thread::sleep_for(1s);
// Verify: value 1 expired, value 2 did NOT expire
{
std::lock_guard lk(mtx);
bool longExpired = std::any_of(events.begin(), events.end(), [](const Event& e) {
return e.id == 2 && e.expired;
});
CPPUNIT_ASSERT_MESSAGE("Long-lived value should NOT be expired", !longExpired);
int shortExpires = 0;
for (const auto& e : events)
if (e.id == 1 && e.expired)
shortExpires++;
CPPUNIT_ASSERT_EQUAL_MESSAGE("Short-lived value should expire exactly once", 1, shortExpires);
}
node1.cancelListen(key, ftoken.get());
}
} // namespace test
-24
View File
@@ -18,24 +18,15 @@ class DhtRunnerTester : public CppUnit::TestFixture
CPPUNIT_TEST(testPutDuplicate);
CPPUNIT_TEST(testPutOverride);
CPPUNIT_TEST(testListen);
CPPUNIT_TEST(testListenValueEdit);
CPPUNIT_TEST(testListenLotOfBytes);
CPPUNIT_TEST(testStoreEmptyValue);
CPPUNIT_TEST(testIdOps);
CPPUNIT_TEST(testImportValuesPreservesRemoteQuota);
CPPUNIT_TEST(testExportValuesUsesSystemTime);
CPPUNIT_TEST(testImportValuesPreservesStoredExpiration);
CPPUNIT_TEST(testImportPermanentValueExpires);
CPPUNIT_TEST(testBootstrapSetsConnectingState);
CPPUNIT_TEST(testBootstrapThenPutNoRace);
CPPUNIT_TEST(testBootstrapMissingNodeThenPutFails);
CPPUNIT_TEST(testShutdownCompletesWithPendingPut);
CPPUNIT_TEST(testSavedPortFallback);
CPPUNIT_TEST(testGetAfterListen);
CPPUNIT_TEST(testListenDuplicatePut);
CPPUNIT_TEST(testListenMultiSourceExpire);
CPPUNIT_TEST(testListenEditChainThenExpire);
CPPUNIT_TEST(testListenMultiValuePartialExpire);
CPPUNIT_TEST_SUITE_END();
dht::DhtRunner node1 {};
@@ -75,31 +66,16 @@ public:
*/
void testIdOps();
void testImportValuesPreservesRemoteQuota();
void testExportValuesUsesSystemTime();
void testImportValuesPreservesStoredExpiration();
void testImportPermanentValueExpires();
void testBootstrapSetsConnectingState();
void testBootstrapThenPutNoRace();
void testBootstrapMissingNodeThenPutFails();
void testShutdownCompletesWithPendingPut();
void testSavedPortFallback();
void testGetAfterListen();
void testListenDuplicatePut();
void testListenMultiSourceExpire();
void testListenEditChainThenExpire();
void testListenMultiValuePartialExpire();
/**
* Test that editing a value only triggers an add callback, no expire
*/
void testListenValueEdit();
/**
* Test listen method with lot of datas
*/
void testListenLotOfBytes();
/**
* Test storing an empty value
*/
void testStoreEmptyValue();
/**
* Test multithread
*/
-31
View File
@@ -8,7 +8,6 @@
#include <iostream>
#include <string>
#include <atomic>
#include <chrono>
#include <condition_variable>
@@ -333,36 +332,6 @@ HttpTester::test_parse_url_just_slash()
CPPUNIT_ASSERT_EQUAL(std::string("/"), parsed.target);
}
void
HttpTester::test_completed_resolver_callback_uses_io_thread()
{
asio::io_context ioContext;
const asio::ip::tcp::endpoint endpoint(asio::ip::address_v4::loopback(), 80);
dht::http::Resolver resolver(ioContext, {endpoint});
const auto callerThread = std::this_thread::get_id();
std::thread::id callbackThread;
asio::error_code callbackError;
std::vector<asio::ip::tcp::endpoint> resolvedEndpoints;
std::atomic_bool callbackCalled {false};
resolver.add_callback([&](const asio::error_code& ec, const auto& endpoints) {
callbackError = ec;
callbackThread = std::this_thread::get_id();
resolvedEndpoints = endpoints;
callbackCalled = true;
});
CPPUNIT_ASSERT(!callbackCalled);
std::thread ioThread([&] { ioContext.run(); });
ioThread.join();
CPPUNIT_ASSERT(callbackCalled);
CPPUNIT_ASSERT(!callbackError);
CPPUNIT_ASSERT(callbackThread != callerThread);
CPPUNIT_ASSERT_EQUAL(size_t(1), resolvedEndpoints.size());
CPPUNIT_ASSERT_EQUAL(endpoint, resolvedEndpoints.front());
}
void
HttpTester::test_send_json()
{
-2
View File
@@ -39,7 +39,6 @@ class HttpTester : public CppUnit::TestFixture
CPPUNIT_TEST(test_parse_url_complex_path_query_fragment);
CPPUNIT_TEST(test_parse_url_empty);
CPPUNIT_TEST(test_parse_url_just_slash);
CPPUNIT_TEST(test_completed_resolver_callback_uses_io_thread);
// send
CPPUNIT_TEST(test_send_json);
CPPUNIT_TEST_SUITE_END();
@@ -83,7 +82,6 @@ public:
void test_parse_url_complex_path_query_fragment();
void test_parse_url_empty();
void test_parse_url_just_slash();
void test_completed_resolver_callback_uses_io_thread();
/**
* Test send(json)
*/
+29 -150
View File
@@ -214,12 +214,9 @@ makeEngine(std::unique_ptr<TestDatagramSocket>&& socket,
std::mt19937_64& rd,
int& onNewNodeCalls)
{
net::NetworkConfig config {};
config.max_req_per_sec = -1;
config.max_peer_req_per_sec = -1;
return net::NetworkEngine(
myid,
config,
{},
std::move(socket),
{},
rd,
@@ -237,21 +234,6 @@ makeEngine(std::unique_ptr<TestDatagramSocket>&& socket,
[](Sp<Node>, const InfoHash&, const Blob&, const Value::Id&) { return net::RequestAnswer {}; });
}
static Sp<Node>
addPendingGetRequest(net::NetworkEngine& engine, const InfoHash& id, const SockAddr& addr, Tid tid)
{
auto node = engine.insertNode(id, addr);
auto request = std::make_shared<net::Request>(
MessageType::GetValues,
tid,
node,
Blob {},
[](const net::Request&, ParsedMessage&&) {},
[](const net::Request&, bool) {});
node->requested(request);
return node;
}
void
NetworkEngineTester::setUp()
{}
@@ -261,130 +243,24 @@ NetworkEngineTester::tearDown()
{}
void
NetworkEngineTester::testCompletesPartialSessionWithDataBeforeHeader()
NetworkEngineTester::testIgnoresUnknownPartialData()
{
Scheduler scheduler;
std::mt19937_64 rd(0);
InfoHash myid = InfoHash::getRandom(rd);
InfoHash remoteId = InfoHash::getRandom(rd);
int onNewNodeCalls = 0;
auto engine = makeEngine(std::make_unique<TestDatagramSocket>(), scheduler, myid, rd, onNewNodeCalls);
auto serialized = serializeValue("fragment before header");
Blob fragment = {1, 2, 3};
auto packet = makeValueDataPacketBlob(99, 0, 0, fragment);
auto from = makeIPv4("127.0.0.2", 5000);
addPendingGetRequest(engine, remoteId, from, 99);
auto initialOnNewNodeCalls = onNewNodeCalls;
auto fragment = makeValueDataPacketBlob(99, 0, 0, serialized);
auto header = makeReplyHeaderPacketBlob(remoteId, 99, {serialized.size()});
engine.processMessage(fragment.data(), fragment.size(), from);
CPPUNIT_ASSERT_EQUAL((size_t) 1, engine.getPartialCount());
CPPUNIT_ASSERT_EQUAL(initialOnNewNodeCalls, onNewNodeCalls);
engine.processMessage(header.data(), header.size(), from);
CPPUNIT_ASSERT_EQUAL((size_t) 0, engine.getPartialCount());
CPPUNIT_ASSERT(onNewNodeCalls > initialOnNewNodeCalls);
}
void
NetworkEngineTester::testBuffersLargeValueBeforeHeader()
{
Scheduler scheduler;
std::mt19937_64 rd(8);
InfoHash myid = InfoHash::getRandom(rd);
InfoHash remoteId = InfoHash::getRandom(rd);
int onNewNodeCalls = 0;
auto engine = makeEngine(std::make_unique<TestDatagramSocket>(), scheduler, myid, rd, onNewNodeCalls);
auto serialized = serializeValue(std::string(57 * 1024, 'l'));
auto from = makeIPv4("127.0.0.2", 5008);
addPendingGetRequest(engine, remoteId, from, 101);
auto initialOnNewNodeCalls = onNewNodeCalls;
for (size_t offset = 0; offset < serialized.size(); offset += TEST_MTU) {
auto end = std::min(offset + TEST_MTU, serialized.size());
Blob data(serialized.begin() + offset, serialized.begin() + end);
auto fragment = makeValueDataPacketBlob(101, 0, offset, data);
engine.processMessage(fragment.data(), fragment.size(), from);
}
CPPUNIT_ASSERT_EQUAL((size_t) 1, engine.getPartialCount());
auto header = makeReplyHeaderPacketBlob(remoteId, 101, {serialized.size()});
engine.processMessage(header.data(), header.size(), from);
CPPUNIT_ASSERT_EQUAL((size_t) 0, engine.getPartialCount());
CPPUNIT_ASSERT(onNewNodeCalls > initialOnNewNodeCalls);
}
void
NetworkEngineTester::testIgnoresEmptyPartialData()
{
Scheduler scheduler;
std::mt19937_64 rd(7);
InfoHash myid = InfoHash::getRandom(rd);
int onNewNodeCalls = 0;
auto engine = makeEngine(std::make_unique<TestDatagramSocket>(), scheduler, myid, rd, onNewNodeCalls);
auto packet = makeValueDataPacketBlob(100, 0, 0, {});
auto from = makeIPv4("127.0.0.2", 5007);
engine.processMessage(packet.data(), packet.size(), from);
CPPUNIT_ASSERT_EQUAL((size_t) 0, engine.getPartialCount());
CPPUNIT_ASSERT_EQUAL(0, onNewNodeCalls);
}
void
NetworkEngineTester::testRejectsUnsolicitedPartialReply()
{
Scheduler scheduler;
std::mt19937_64 rd(9);
InfoHash myid = InfoHash::getRandom(rd);
InfoHash remoteId = InfoHash::getRandom(rd);
int onNewNodeCalls = 0;
auto engine = makeEngine(std::make_unique<TestDatagramSocket>(), scheduler, myid, rd, onNewNodeCalls);
auto from = makeIPv4("127.0.0.2", 5009);
auto header = makeReplyHeaderPacketBlob(remoteId, 102, {TEST_MTU});
engine.processMessage(header.data(), header.size(), from);
CPPUNIT_ASSERT_EQUAL((size_t) 0, engine.getPartialCount());
CPPUNIT_ASSERT_EQUAL((size_t) 0, engine.getNodeCacheSize());
}
void
NetworkEngineTester::testDropsMalformedCompletedPartialMessage()
{
Scheduler scheduler;
std::mt19937_64 rd(10);
InfoHash myid = InfoHash::getRandom(rd);
InfoHash remoteId = InfoHash::getRandom(rd);
int onNewNodeCalls = 0;
auto engine = makeEngine(std::make_unique<TestDatagramSocket>(), scheduler, myid, rd, onNewNodeCalls);
auto from = makeIPv4("127.0.0.2", 5010);
addPendingGetRequest(engine, remoteId, from, 103);
Blob malformed {0xc1};
auto header = makeReplyHeaderPacketBlob(remoteId, 103, {malformed.size()});
auto fragment = makeValueDataPacketBlob(103, 0, 0, malformed);
engine.processMessage(header.data(), header.size(), from);
CPPUNIT_ASSERT_EQUAL((size_t) 1, engine.getPartialCount());
engine.processMessage(fragment.data(), fragment.size(), from);
CPPUNIT_ASSERT_EQUAL((size_t) 0, engine.getPartialCount());
auto earlyFrom = makeIPv4("127.0.0.2", 5011);
addPendingGetRequest(engine, remoteId, earlyFrom, 104);
auto earlyHeader = makeReplyHeaderPacketBlob(remoteId, 104, {malformed.size()});
auto earlyFragment = makeValueDataPacketBlob(104, 0, 0, malformed);
engine.processMessage(earlyFragment.data(), earlyFragment.size(), earlyFrom);
CPPUNIT_ASSERT_EQUAL((size_t) 1, engine.getPartialCount());
engine.processMessage(earlyHeader.data(), earlyHeader.size(), earlyFrom);
CPPUNIT_ASSERT_EQUAL((size_t) 0, engine.getPartialCount());
}
void
NetworkEngineTester::testCompletesPartialSessionFromSameSource()
{
@@ -398,8 +274,6 @@ NetworkEngineTester::testCompletesPartialSessionFromSameSource()
std::string data(TEST_MTU * 2 + 64, 'z');
auto serialized = serializeValue(data);
auto from = makeIPv4("127.0.0.2", 5001);
addPendingGetRequest(engine, remoteId, from, 7);
auto initialOnNewNodeCalls = onNewNodeCalls;
auto header = makeReplyHeaderPacketBlob(remoteId, 7, {serialized.size()});
engine.processMessage(header.data(), header.size(), from);
@@ -413,40 +287,45 @@ NetworkEngineTester::testCompletesPartialSessionFromSameSource()
}
CPPUNIT_ASSERT_EQUAL((size_t) 0, engine.getPartialCount());
CPPUNIT_ASSERT(onNewNodeCalls > initialOnNewNodeCalls);
CPPUNIT_ASSERT(onNewNodeCalls > 0);
}
void
NetworkEngineTester::testSeparatesPartialSessionsBySource()
NetworkEngineTester::testKeepsSessionForWrongSourceFragment()
{
Scheduler scheduler;
std::mt19937_64 rd(2);
InfoHash myid = InfoHash::getRandom(rd);
InfoHash firstRemoteId = InfoHash::getRandom(rd);
InfoHash secondRemoteId = InfoHash::getRandom(rd);
InfoHash remoteId = InfoHash::getRandom(rd);
int onNewNodeCalls = 0;
auto engine = makeEngine(std::make_unique<TestDatagramSocket>(), scheduler, myid, rd, onNewNodeCalls);
auto serialized = serializeValue("same transaction, different peers");
auto firstFrom = makeIPv4("127.0.0.2", 5002);
auto secondFrom = makeIPv4("127.0.0.3", 5002);
addPendingGetRequest(engine, firstRemoteId, firstFrom, 11);
addPendingGetRequest(engine, secondRemoteId, secondFrom, 11);
auto initialOnNewNodeCalls = onNewNodeCalls;
auto firstHeader = makeReplyHeaderPacketBlob(firstRemoteId, 11, {serialized.size()});
auto secondHeader = makeReplyHeaderPacketBlob(secondRemoteId, 11, {serialized.size()});
std::string data(TEST_MTU + 32, 'w');
auto serialized = serializeValue(data);
auto goodFrom = makeIPv4("127.0.0.2", 5002);
auto badFrom = makeIPv4("127.0.0.3", 5002);
auto header = makeReplyHeaderPacketBlob(remoteId, 11, {serialized.size()});
engine.processMessage(firstHeader.data(), firstHeader.size(), firstFrom);
engine.processMessage(secondHeader.data(), secondHeader.size(), secondFrom);
CPPUNIT_ASSERT_EQUAL((size_t) 2, engine.getPartialCount());
auto fragment = makeValueDataPacketBlob(11, 0, 0, serialized);
engine.processMessage(fragment.data(), fragment.size(), firstFrom);
engine.processMessage(header.data(), header.size(), goodFrom);
CPPUNIT_ASSERT_EQUAL((size_t) 1, engine.getPartialCount());
engine.processMessage(fragment.data(), fragment.size(), secondFrom);
auto firstEnd = std::min(TEST_MTU, serialized.size());
Blob firstFragment(serialized.begin(), serialized.begin() + firstEnd);
auto firstPacket = makeValueDataPacketBlob(11, 0, 0, firstFragment);
engine.processMessage(firstPacket.data(), firstPacket.size(), badFrom);
CPPUNIT_ASSERT_EQUAL((size_t) 1, engine.getPartialCount());
CPPUNIT_ASSERT_EQUAL(0, onNewNodeCalls);
engine.processMessage(firstPacket.data(), firstPacket.size(), goodFrom);
if (firstEnd < serialized.size()) {
Blob tail(serialized.begin() + firstEnd, serialized.end());
auto tailPacket = makeValueDataPacketBlob(11, 0, firstEnd, tail);
engine.processMessage(tailPacket.data(), tailPacket.size(), goodFrom);
}
CPPUNIT_ASSERT_EQUAL((size_t) 0, engine.getPartialCount());
CPPUNIT_ASSERT(onNewNodeCalls > initialOnNewNodeCalls);
CPPUNIT_ASSERT(onNewNodeCalls > 0);
}
void
+4 -12
View File
@@ -13,13 +13,9 @@ class NetworkEngineTester : public CppUnit::TestFixture
#ifdef _MSC_VER
CPPUNIT_TEST(testDisabledOnMsvc);
#else
CPPUNIT_TEST(testCompletesPartialSessionWithDataBeforeHeader);
CPPUNIT_TEST(testBuffersLargeValueBeforeHeader);
CPPUNIT_TEST(testIgnoresEmptyPartialData);
CPPUNIT_TEST(testRejectsUnsolicitedPartialReply);
CPPUNIT_TEST(testDropsMalformedCompletedPartialMessage);
CPPUNIT_TEST(testIgnoresUnknownPartialData);
CPPUNIT_TEST(testCompletesPartialSessionFromSameSource);
CPPUNIT_TEST(testSeparatesPartialSessionsBySource);
CPPUNIT_TEST(testKeepsSessionForWrongSourceFragment);
CPPUNIT_TEST(testListenConfirmationCarriesToken);
CPPUNIT_TEST(testListenConfirmationUpdatesSearchNodeToken);
CPPUNIT_TEST(testListenReopensSocketAfterNodeExpiration);
@@ -34,13 +30,9 @@ public:
#ifdef _MSC_VER
void testDisabledOnMsvc();
#else
void testCompletesPartialSessionWithDataBeforeHeader();
void testBuffersLargeValueBeforeHeader();
void testIgnoresEmptyPartialData();
void testRejectsUnsolicitedPartialReply();
void testDropsMalformedCompletedPartialMessage();
void testIgnoresUnknownPartialData();
void testCompletesPartialSessionFromSameSource();
void testSeparatesPartialSessionsBySource();
void testKeepsSessionForWrongSourceFragment();
void testListenConfirmationCarriesToken();
void testListenConfirmationUpdatesSearchNodeToken();
void testListenReopensSocketAfterNodeExpiration();
-674
View File
@@ -1,674 +0,0 @@
// Copyright (c) 2014-2026 Savoir-faire Linux Inc.
// SPDX-License-Identifier: MIT
#include "test_opcache.h"
// Hack to test internal class
#include "../src/op_cache.cpp"
namespace test {
CPPUNIT_TEST_SUITE_REGISTRATION(OpCacheTester);
namespace {
using Values = std::vector<std::shared_ptr<dht::Value>>;
struct Event
{
dht::Value::Id id;
uint16_t seq;
bool expired;
};
std::shared_ptr<dht::Value>
makeValue(dht::Value::Id id, uint16_t seq, std::string data)
{
auto v = std::make_shared<dht::Value>();
v->id = id;
v->seq = seq;
v->data = {data.begin(), data.end()};
return v;
}
dht::ValueCallback
recordEvents(std::vector<Event>& events)
{
return [&events](const Values& vals, bool expired) {
for (const auto& v : vals)
events.push_back({v->id, v->seq, expired});
return true;
};
}
unsigned
count(const std::vector<Event>& events, bool expired)
{
unsigned n = 0;
for (const auto& e : events)
if (e.expired == expired)
n++;
return n;
}
} // namespace
void
OpCacheTester::setUp()
{}
void
OpCacheTester::tearDown()
{}
void
OpCacheTester::testBasicAddExpire()
{
dht::OpValueCache cache([](const Values&, bool) { return true; });
auto v1 = makeValue(10, 0, "A");
cache.onValuesAdded({v1});
CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
CPPUNIT_ASSERT(cache.get(10));
std::vector<dht::Value::Id> ids = {v1->id};
cache.onValuesExpired(ids);
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
CPPUNIT_ASSERT(!cache.get(10));
}
void
OpCacheTester::testMultipleSources()
{
// Two sources report the same value: it must survive until both have
// expired it, and the user must see exactly one add and one expire.
std::vector<Event> events;
dht::OpValueCache cache(recordEvents(events));
auto v = makeValue(20, 0, "B");
cache.onValuesAdded({v}); // source A
cache.onValuesAdded({v}); // source B
CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
CPPUNIT_ASSERT_EQUAL(1u, count(events, false));
cache.onValuesExpired({v}); // source A goes away
CPPUNIT_ASSERT_EQUAL_MESSAGE("Value still held by source B", (size_t) 1, cache.size());
CPPUNIT_ASSERT_EQUAL(0u, count(events, true));
cache.onValuesExpired({v}); // source B goes away
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
CPPUNIT_ASSERT_EQUAL(1u, count(events, true));
}
void
OpCacheTester::testEditResetsRefCount()
{
// A and B hold v1. A reports the edit v2: only A is known to hold v2, so
// the refcount restarts at 1. B then confirms v2 (refcount 2).
std::vector<Event> events;
dht::OpValueCache cache(recordEvents(events));
auto v1 = makeValue(1, 1, "data1");
auto v2 = makeValue(1, 2, "data2");
cache.onValuesAdded({v1}); // A
cache.onValuesAdded({v1}); // B
cache.onValuesAdded({v2}); // A edits
CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
CPPUNIT_ASSERT_EQUAL(v2->seq, cache.get(1)->seq);
CPPUNIT_ASSERT_EQUAL(2u, count(events, false));
cache.onValuesExpired({v2}); // A expires v2 -> nobody known to hold it
CPPUNIT_ASSERT_EQUAL_MESSAGE("Edit reset the refcount to the editing source", (size_t) 0, cache.size());
CPPUNIT_ASSERT_EQUAL(1u, count(events, true));
// Same scenario, but B confirms v2 before A expires it.
events.clear();
cache.onValuesAdded({v1}); // A
cache.onValuesAdded({v1}); // B
cache.onValuesAdded({v2}); // A edits
cache.onValuesAdded({v2}); // B confirms
cache.onValuesExpired({v2});
CPPUNIT_ASSERT_EQUAL_MESSAGE("Still held by B", (size_t) 1, cache.size());
cache.onValuesExpired({v2});
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
CPPUNIT_ASSERT_EQUAL(2u, count(events, false));
CPPUNIT_ASSERT_EQUAL(1u, count(events, true));
}
void
OpCacheTester::testEditChainMultiSource()
{
// Rapid edit chain seen from two sources with interleaving: the user
// must see each version once as an add and a single final expire.
std::vector<Event> events;
dht::OpValueCache cache(recordEvents(events));
auto v1 = makeValue(7, 1, "v1");
auto v2 = makeValue(7, 2, "v2");
auto v3 = makeValue(7, 3, "v3");
cache.onValuesAdded({v1}); // A
cache.onValuesAdded({v1}); // B
cache.onValuesAdded({v2}); // A
cache.onValuesAdded({v3}); // A (B skipped v2)
cache.onValuesAdded({v2}); // B, late and stale: ignored
cache.onValuesAdded({v3}); // B
CPPUNIT_ASSERT_EQUAL(3u, count(events, false));
CPPUNIT_ASSERT_EQUAL((uint16_t) 3, cache.get(7)->seq);
cache.onValuesExpired({v3}); // A
CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
cache.onValuesExpired({v3}); // B
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
CPPUNIT_ASSERT_EQUAL(1u, count(events, true));
CPPUNIT_ASSERT_EQUAL((uint16_t) 3, events.back().seq);
}
void
OpCacheTester::testStaleAddIgnored()
{
dht::OpValueCache cache([](const Values&, bool) { return true; });
auto v1 = makeValue(30, 10, "C");
auto v2 = makeValue(30, 20, "D");
auto v3 = makeValue(30, 5, "E");
cache.onValuesAdded({v1});
CPPUNIT_ASSERT_EQUAL((uint16_t) 10, cache.get(30)->seq);
cache.onValuesAdded({v2});
CPPUNIT_ASSERT_EQUAL((uint16_t) 20, cache.get(30)->seq);
// Older version from a lagging source: neither stored nor counted
cache.onValuesAdded({v3});
CPPUNIT_ASSERT_EQUAL((uint16_t) 20, cache.get(30)->seq);
cache.onValuesExpired({v2});
CPPUNIT_ASSERT_MESSAGE("Stale add must not have incremented the refcount", !cache.get(30));
}
void
OpCacheTester::testStaleExpireIgnored()
{
// A source that still holds an older version expiring it must not
// affect the newer version held by others.
std::vector<Event> events;
dht::OpValueCache cache(recordEvents(events));
auto v1 = makeValue(31, 1, "old");
auto v2 = makeValue(31, 2, "new");
cache.onValuesAdded({v1}); // A
cache.onValuesAdded({v1}); // B
cache.onValuesAdded({v2}); // A edits; B never receives v2
cache.onValuesExpired({v1}); // B expires its stale copy
CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
CPPUNIT_ASSERT_EQUAL(0u, count(events, true));
cache.onValuesExpired({v2}); // A
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
CPPUNIT_ASSERT_EQUAL(1u, count(events, true));
CPPUNIT_ASSERT_EQUAL((uint16_t) 2, events.back().seq);
}
void
OpCacheTester::testCallbacks()
{
std::vector<Event> events;
dht::OpValueCache cache(recordEvents(events));
auto v1 = makeValue(40, 0, "");
cache.onValuesAdded({v1});
CPPUNIT_ASSERT_EQUAL(1u, count(events, false));
// Duplicate from another source: counted, but not re-delivered
cache.onValuesAdded({v1});
CPPUNIT_ASSERT_EQUAL(1u, count(events, false));
auto v2 = makeValue(40, 100, "MOD");
cache.onValuesAdded({v2});
CPPUNIT_ASSERT_EQUAL(2u, count(events, false));
// The edit reset the refcount: one expire removes it
cache.onValuesExpired({v2});
CPPUNIT_ASSERT_EQUAL(1u, count(events, true));
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
}
void
OpCacheTester::testFilters()
{
dht::OpValueCache cache([](const Values&, bool) { return true; });
auto v1 = makeValue(50, 0, "");
v1->type = 1;
auto v2 = makeValue(51, 0, "");
v2->type = 2;
cache.onValuesAdded({v1, v2});
auto res1 = cache.get(dht::Value::TypeFilter(1));
CPPUNIT_ASSERT_EQUAL((size_t) 1, res1.size());
CPPUNIT_ASSERT_EQUAL(v1->id, res1[0]->id);
auto res2 = cache.get(dht::Value::TypeFilter(2));
CPPUNIT_ASSERT_EQUAL((size_t) 1, res2.size());
CPPUNIT_ASSERT_EQUAL(v2->id, res2[0]->id);
CPPUNIT_ASSERT_EQUAL((size_t) 2, cache.getValues().size());
}
void
OpCacheTester::testSyncStatus()
{
dht::OpValueCache cache([](const Values&, bool) { return true; });
CPPUNIT_ASSERT(!cache.isSynced());
cache.onNodeChanged(dht::ListenSyncStatus::ADDED);
CPPUNIT_ASSERT(!cache.isSynced());
cache.onNodeChanged(dht::ListenSyncStatus::SYNCED);
CPPUNIT_ASSERT(cache.isSynced());
cache.onNodeChanged(dht::ListenSyncStatus::ADDED);
CPPUNIT_ASSERT(!cache.isSynced());
cache.onNodeChanged(dht::ListenSyncStatus::SYNCED);
CPPUNIT_ASSERT(cache.isSynced());
cache.onNodeChanged(dht::ListenSyncStatus::UNSYNCED);
CPPUNIT_ASSERT(!cache.isSynced());
cache.onNodeChanged(dht::ListenSyncStatus::REMOVED);
CPPUNIT_ASSERT(cache.isSynced());
}
void
OpCacheTester::testGetWhileSynced()
{
dht::SearchCache searchCache;
int getCallCount = 0;
bool doneCalled = false;
bool doneSuccess = false;
auto v1 = makeValue(100, 0, "X");
auto v2 = makeValue(101, 0, "Y");
auto query = std::make_shared<dht::Query>();
auto token = searchCache.listen([](const Values&, bool) { return true; },
query,
{},
[&](const std::shared_ptr<dht::Query>&,
dht::ValueCallback vcb,
dht::SyncCallback scb) -> size_t {
scb(dht::ListenSyncStatus::ADDED);
vcb({v1, v2}, false);
scb(dht::ListenSyncStatus::SYNCED);
return 1;
});
CPPUNIT_ASSERT(token != 0);
bool result = searchCache.get(
{},
query,
[&](const Values& vals) {
getCallCount++;
CPPUNIT_ASSERT_EQUAL((size_t) 2, vals.size());
return true;
},
[&](bool success, const std::vector<std::shared_ptr<dht::Node>>&) {
doneCalled = true;
doneSuccess = success;
});
CPPUNIT_ASSERT_MESSAGE("get() should be served from a synced cache", result);
CPPUNIT_ASSERT_EQUAL(1, getCallCount);
CPPUNIT_ASSERT(doneCalled);
CPPUNIT_ASSERT(doneSuccess);
}
void
OpCacheTester::testGetWhileNotSynced()
{
// An unsynced cache must not answer a get: the caller falls back to the
// network and values are delivered once, from the network only.
dht::SearchCache searchCache;
std::vector<dht::Value::Id> deliveries;
auto v1 = makeValue(200, 0, "A");
auto query = std::make_shared<dht::Query>();
dht::SyncCallback storedScb;
auto token = searchCache.listen([](const Values&, bool) { return true; },
query,
{},
[&](const std::shared_ptr<dht::Query>&,
dht::ValueCallback vcb,
dht::SyncCallback scb) -> size_t {
storedScb = scb;
scb(dht::ListenSyncStatus::ADDED);
vcb({v1}, false);
return 1;
});
CPPUNIT_ASSERT(token != 0);
bool doneCalled = false;
auto gcb = [&](const Values& vals) {
for (const auto& v : vals)
deliveries.emplace_back(v->id);
return true;
};
auto dcb = [&](bool, const std::vector<std::shared_ptr<dht::Node>>&) {
doneCalled = true;
};
CPPUNIT_ASSERT(!searchCache.get({}, query, gcb, dcb));
CPPUNIT_ASSERT(deliveries.empty());
CPPUNIT_ASSERT(!doneCalled);
// Simulated network reply
gcb({v1});
CPPUNIT_ASSERT_EQUAL((size_t) 1, deliveries.size());
storedScb(dht::ListenSyncStatus::SYNCED);
deliveries.clear();
CPPUNIT_ASSERT(searchCache.get({}, query, gcb, dcb));
CPPUNIT_ASSERT(doneCalled);
CPPUNIT_ASSERT_EQUAL((size_t) 1, deliveries.size());
}
void
OpCacheTester::testGetEmptySynced()
{
dht::SearchCache searchCache;
int getCallCount = 0;
bool doneCalled = false;
bool doneSuccess = false;
auto query = std::make_shared<dht::Query>();
auto token = searchCache.listen([](const Values&, bool) { return true; },
query,
{},
[&](const std::shared_ptr<dht::Query>&,
dht::ValueCallback,
dht::SyncCallback scb) -> size_t {
scb(dht::ListenSyncStatus::ADDED);
scb(dht::ListenSyncStatus::SYNCED);
return 1;
});
CPPUNIT_ASSERT(token != 0);
bool result = searchCache.get(
{},
query,
[&](const Values&) {
getCallCount++;
return true;
},
[&](bool success, const std::vector<std::shared_ptr<dht::Node>>&) {
doneCalled = true;
doneSuccess = success;
});
CPPUNIT_ASSERT(result);
CPPUNIT_ASSERT_EQUAL(0, getCallCount);
CPPUNIT_ASSERT(doneCalled);
CPPUNIT_ASSERT(doneSuccess);
}
void
OpCacheTester::testGetWithoutCallback()
{
// Dht::query() passes no GetCallback: the cache cannot answer it and
// must let the network path run instead of calling an empty function.
dht::SearchCache searchCache;
auto v1 = makeValue(300, 0, "Q");
auto query = std::make_shared<dht::Query>();
searchCache.listen([](const Values&, bool) { return true; },
query,
{},
[&](const std::shared_ptr<dht::Query>&, dht::ValueCallback vcb, dht::SyncCallback scb) -> size_t {
scb(dht::ListenSyncStatus::ADDED);
vcb({v1}, false);
scb(dht::ListenSyncStatus::SYNCED);
return 1;
});
bool doneCalled = false;
CPPUNIT_ASSERT(!searchCache.get({}, query, {}, [&](bool, const std::vector<std::shared_ptr<dht::Node>>&) {
doneCalled = true;
}));
CPPUNIT_ASSERT(!doneCalled);
}
void
OpCacheTester::testGetCallbackReturnsFalse()
{
// A consumer returning false means "I have enough", not a failure.
dht::SearchCache searchCache;
auto v1 = makeValue(1000, 0, "X");
auto query = std::make_shared<dht::Query>();
searchCache.listen([](const Values&, bool) { return true; },
query,
{},
[&](const std::shared_ptr<dht::Query>&, dht::ValueCallback vcb, dht::SyncCallback scb) -> size_t {
scb(dht::ListenSyncStatus::ADDED);
vcb({v1}, false);
scb(dht::ListenSyncStatus::SYNCED);
return 1;
});
bool doneCalled = false;
bool doneSuccess = false;
int gcbCallCount = 0;
bool result = searchCache.get(
{},
query,
[&](const Values&) {
gcbCallCount++;
return false;
},
[&](bool success, const std::vector<std::shared_ptr<dht::Node>>&) {
doneCalled = true;
doneSuccess = success;
});
CPPUNIT_ASSERT(result);
CPPUNIT_ASSERT_EQUAL(1, gcbCallCount);
CPPUNIT_ASSERT(doneCalled);
CPPUNIT_ASSERT_MESSAGE("Stopping early is reported as success, like the network path", doneSuccess);
}
void
OpCacheTester::testTimestampOrdering()
{
dht::OpValueCache cache([](const Values&, bool) { return true; });
auto v1 = makeValue(400, 0, "T");
auto t1 = std::chrono::system_clock::now();
auto t2 = t1 + std::chrono::seconds(10);
cache.onValue({v1}, false, t2);
CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
cache.onValue({v1}, true, t1);
CPPUNIT_ASSERT_EQUAL_MESSAGE("Stale expiration should be ignored", (size_t) 1, cache.size());
cache.onValue({v1}, true, t2);
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
}
void
OpCacheTester::testSourceDropKeepsValue()
{
// Two per-node ValueCaches feed one Search-level OpCache. When one node
// leaves the search (its ValueCache is destroyed, expiring everything it
// held) the value must stay visible as long as the other node holds it.
std::vector<Event> events;
dht::SearchCache searchCache;
auto query = std::make_shared<dht::Query>();
dht::ValueCallback toOpCache;
dht::SyncCallback toOpCacheSync;
searchCache.listen(recordEvents(events),
query,
{},
[&](const std::shared_ptr<dht::Query>&, dht::ValueCallback vcb, dht::SyncCallback scb) -> size_t {
toOpCache = std::move(vcb);
toOpCacheSync = std::move(scb);
return 1;
});
dht::TypeStore types;
auto now = dht::clock::now();
auto v = makeValue(42, 0, "hello");
auto makeNodeCache = [&] {
return std::make_unique<dht::ValueCache>([&](const Values& vals, bool expired) { toOpCache(vals, expired); },
[&](dht::ListenSyncStatus s) { toOpCacheSync(s); });
};
auto nodeA = makeNodeCache();
auto nodeB = makeNodeCache();
nodeA->onValues({v}, {}, {}, types, now);
nodeA->onSynced(true);
nodeB->onValues({v}, {}, {}, types, now);
nodeB->onSynced(true);
CPPUNIT_ASSERT_EQUAL(1u, count(events, false));
CPPUNIT_ASSERT_EQUAL((size_t) 1, searchCache.get(dht::Value::Filter {}).size());
nodeA.reset();
CPPUNIT_ASSERT_EQUAL_MESSAGE("No expire while another node holds the value", 0u, count(events, true));
CPPUNIT_ASSERT_EQUAL((size_t) 1, searchCache.get(dht::Value::Filter {}).size());
nodeB->onValues({v}, {}, {}, types, now + std::chrono::seconds(1));
CPPUNIT_ASSERT_EQUAL_MESSAGE("Refresh from the remaining node is not re-delivered", 1u, count(events, false));
nodeB.reset();
CPPUNIT_ASSERT_EQUAL_MESSAGE("Last holder gone: expire once", 1u, count(events, true));
CPPUNIT_ASSERT_EQUAL((size_t) 0, searchCache.get(dht::Value::Filter {}).size());
}
void
OpCacheTester::testHighChurnRefCountConsistency()
{
dht::OpValueCache cache([](const Values&, bool) { return true; });
auto v = makeValue(800, 0, "CHURN");
for (int i = 0; i < 50; i++) {
cache.onNodeChanged(dht::ListenSyncStatus::ADDED);
cache.onValuesAdded({v});
cache.onValuesExpired({v});
cache.onNodeChanged(dht::ListenSyncStatus::REMOVED);
}
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
// Overlapping churn: add A, add B, expire A, expire B
for (int i = 0; i < 20; i++) {
cache.onNodeChanged(dht::ListenSyncStatus::ADDED);
cache.onValuesAdded({v});
cache.onNodeChanged(dht::ListenSyncStatus::ADDED);
cache.onValuesAdded({v});
cache.onValuesExpired({v});
cache.onNodeChanged(dht::ListenSyncStatus::REMOVED);
CPPUNIT_ASSERT_EQUAL_MESSAGE("Value kept while one source remains", (size_t) 1, cache.size());
cache.onValuesExpired({v});
cache.onNodeChanged(dht::ListenSyncStatus::REMOVED);
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
}
// Extra expirations from sources that never added must be no-ops
cache.onValuesExpired({v});
cache.onValuesExpired({v});
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
}
void
OpCacheTester::testValueUpdateSingleSourcePhantom()
{
// Editing a value (same id, higher seq) is delivered as a single add of
// the new version, without a phantom expiration of the old one.
std::vector<Event> events;
dht::OpValueCache cache(recordEvents(events));
auto v1 = makeValue(700, 1, "OLD");
auto v2 = makeValue(700, 2, "NEW");
cache.onValuesAdded({v1});
events.clear();
cache.onValuesAdded({v2});
CPPUNIT_ASSERT_EQUAL((size_t) 1, events.size());
CPPUNIT_ASSERT(!events[0].expired);
CPPUNIT_ASSERT_EQUAL(v2->id, events[0].id);
CPPUNIT_ASSERT_EQUAL(v2->seq, cache.get(700)->seq);
}
void
OpCacheTester::testExpireAfterUpdate()
{
std::vector<Event> events;
dht::OpValueCache cache(recordEvents(events));
auto v1 = makeValue(1200, 1, "V1");
auto v2 = makeValue(1200, 5, "V2");
cache.onValuesAdded({v1});
cache.onValuesAdded({v2});
CPPUNIT_ASSERT_EQUAL(v2->seq, cache.get(1200)->seq);
cache.onValuesExpired({v2});
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
CPPUNIT_ASSERT_EQUAL(1u, count(events, true));
CPPUNIT_ASSERT_EQUAL_MESSAGE("Expired value should be the updated one", v2->seq, events.back().seq);
// Once gone, an older version is accepted again
cache.onValuesAdded({v1});
CPPUNIT_ASSERT_EQUAL(v1->seq, cache.get(1200)->seq);
}
void
OpCacheTester::testSingleSourceMode()
{
// Single-source caches (proxy client) get the same values re-delivered
// on refresh: duplicates must not be counted, one expire removes.
std::vector<Event> events;
dht::OpValueCache cache(recordEvents(events), false);
auto v = makeValue(1100, 0, "DUP");
for (int i = 0; i < 10; i++)
cache.onValuesAdded({v});
CPPUNIT_ASSERT_EQUAL(1u, count(events, false));
CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
cache.onValuesExpired({v});
CPPUNIT_ASSERT_EQUAL(1u, count(events, true));
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
cache.onValuesExpired({v});
CPPUNIT_ASSERT_EQUAL(1u, count(events, true));
}
void
OpCacheTester::testSingleSourceModeSearchCache()
{
std::vector<Event> events;
dht::SearchCache searchCache(false);
auto query = std::make_shared<dht::Query>();
dht::ValueCallback vcbStored;
searchCache.listen(recordEvents(events),
query,
{},
[&](const std::shared_ptr<dht::Query>&, dht::ValueCallback vcb, dht::SyncCallback) -> size_t {
vcbStored = std::move(vcb);
return 1;
});
auto v = makeValue(1300, 0, "S");
vcbStored({v}, false);
vcbStored({v}, false); // re-delivery after listen restart
CPPUNIT_ASSERT_EQUAL(1u, count(events, false));
vcbStored({v}, true);
CPPUNIT_ASSERT_EQUAL(1u, count(events, true));
CPPUNIT_ASSERT_EQUAL((size_t) 0, searchCache.get(dht::Value::Filter {}).size());
}
} // namespace test
-63
View File
@@ -1,63 +0,0 @@
// Copyright (c) 2014-2026 Savoir-faire Linux Inc.
// SPDX-License-Identifier: MIT
#pragma once
#include <cppunit/TestFixture.h>
#include <cppunit/extensions/HelperMacros.h>
namespace test {
class OpCacheTester : public CppUnit::TestFixture
{
CPPUNIT_TEST_SUITE(OpCacheTester);
CPPUNIT_TEST(testBasicAddExpire);
CPPUNIT_TEST(testMultipleSources);
CPPUNIT_TEST(testEditResetsRefCount);
CPPUNIT_TEST(testEditChainMultiSource);
CPPUNIT_TEST(testStaleAddIgnored);
CPPUNIT_TEST(testStaleExpireIgnored);
CPPUNIT_TEST(testCallbacks);
CPPUNIT_TEST(testFilters);
CPPUNIT_TEST(testSyncStatus);
CPPUNIT_TEST(testGetWhileSynced);
CPPUNIT_TEST(testGetWhileNotSynced);
CPPUNIT_TEST(testGetEmptySynced);
CPPUNIT_TEST(testGetWithoutCallback);
CPPUNIT_TEST(testGetCallbackReturnsFalse);
CPPUNIT_TEST(testTimestampOrdering);
CPPUNIT_TEST(testSourceDropKeepsValue);
CPPUNIT_TEST(testHighChurnRefCountConsistency);
CPPUNIT_TEST(testValueUpdateSingleSourcePhantom);
CPPUNIT_TEST(testExpireAfterUpdate);
CPPUNIT_TEST(testSingleSourceMode);
CPPUNIT_TEST(testSingleSourceModeSearchCache);
CPPUNIT_TEST_SUITE_END();
public:
void setUp();
void tearDown();
void testBasicAddExpire();
void testMultipleSources();
void testEditResetsRefCount();
void testEditChainMultiSource();
void testStaleAddIgnored();
void testStaleExpireIgnored();
void testCallbacks();
void testFilters();
void testSyncStatus();
void testGetWhileSynced();
void testGetWhileNotSynced();
void testGetEmptySynced();
void testGetWithoutCallback();
void testGetCallbackReturnsFalse();
void testTimestampOrdering();
void testSourceDropKeepsValue();
void testHighChurnRefCountConsistency();
void testValueUpdateSingleSourcePhantom();
void testExpireAfterUpdate();
void testSingleSourceMode();
void testSingleSourceModeSearchCache();
};
} // namespace test
-98
View File
@@ -328,104 +328,6 @@ ParsedMessageTester::testParseRejectsInvalidValuesField()
CPPUNIT_ASSERT_THROW(parsed.msgpack_unpack(msg.get()), msgpack::type_error);
}
void
ParsedMessageTester::testParseRejectsEmptyFieldSelection()
{
msgpack::sbuffer buffer;
msgpack::packer<msgpack::sbuffer> pk(&buffer);
pk.pack_map(3);
pk.pack(KEY_R);
pk.pack_map(1);
pk.pack(KEY_REQ_FIELDS);
pk.pack_map(2);
pk.pack("f");
pk.pack_array(0);
pk.pack("v");
pk.pack_array(0);
pk.pack(KEY_TID);
pk.pack(1u);
pk.pack(KEY_Y);
pk.pack(KEY_R);
auto msg = msgpack::unpack(buffer.data(), buffer.size());
ParsedMessage parsed;
CPPUNIT_ASSERT_THROW(parsed.msgpack_unpack(msg.get()), msgpack::type_error);
}
void
ParsedMessageTester::testParseRejectsEmptyErrorArray()
{
msgpack::sbuffer buffer;
msgpack::packer<msgpack::sbuffer> pk(&buffer);
pk.pack_map(3);
pk.pack(KEY_E);
pk.pack_array(0);
pk.pack(KEY_TID);
pk.pack(1u);
pk.pack(KEY_Y);
pk.pack(KEY_E);
auto msg = msgpack::unpack(buffer.data(), buffer.size());
ParsedMessage parsed;
CPPUNIT_ASSERT_THROW(parsed.msgpack_unpack(msg.get()), msgpack::type_error);
}
void
ParsedMessageTester::testParseRejectsNonMapRequest()
{
msgpack::sbuffer buffer;
msgpack::packer<msgpack::sbuffer> pk(&buffer);
pk.pack_map(4);
pk.pack(KEY_A);
pk.pack_array(2);
pk.pack(1u);
pk.pack(2u);
pk.pack(KEY_Q);
pk.pack(QUERY_PING);
pk.pack(KEY_TID);
pk.pack(1u);
pk.pack(KEY_Y);
pk.pack("q");
auto msg = msgpack::unpack(buffer.data(), buffer.size());
ParsedMessage parsed;
CPPUNIT_ASSERT_THROW(parsed.msgpack_unpack(msg.get()), msgpack::type_error);
}
void
ParsedMessageTester::testParseErrorOnlyMessageKeepsRequestFieldsEmpty()
{
msgpack::sbuffer buffer;
msgpack::packer<msgpack::sbuffer> pk(&buffer);
// An error message with no request map: the error array must not be walked
// as if it were a sequence of key/value pairs.
pk.pack_map(3);
pk.pack(KEY_E);
pk.pack_array(2);
pk.pack(401u);
pk.pack("unauthorized");
pk.pack(KEY_TID);
pk.pack(9u);
pk.pack(KEY_Y);
pk.pack(KEY_E);
auto msg = msgpack::unpack(buffer.data(), buffer.size());
ParsedMessage parsed;
parsed.msgpack_unpack(msg.get());
CPPUNIT_ASSERT(parsed.type == MessageType::Error);
CPPUNIT_ASSERT_EQUAL((uint16_t) 401, parsed.error_code);
CPPUNIT_ASSERT(not parsed.id);
CPPUNIT_ASSERT(parsed.token.empty());
CPPUNIT_ASSERT(parsed.values.empty());
CPPUNIT_ASSERT(parsed.nodes4_raw.empty());
CPPUNIT_ASSERT(parsed.nodes6_raw.empty());
}
void
ParsedMessageTester::testParseIgnoresIncompleteValueDataEntry()
{
-8
View File
@@ -24,10 +24,6 @@ class ParsedMessageTester : public CppUnit::TestFixture
CPPUNIT_TEST(testParseRejectsNonMapPacket);
CPPUNIT_TEST(testParseRejectsInvalidValueDataPayload);
CPPUNIT_TEST(testParseRejectsInvalidValuesField);
CPPUNIT_TEST(testParseRejectsEmptyFieldSelection);
CPPUNIT_TEST(testParseRejectsEmptyErrorArray);
CPPUNIT_TEST(testParseRejectsNonMapRequest);
CPPUNIT_TEST(testParseErrorOnlyMessageKeepsRequestFieldsEmpty);
CPPUNIT_TEST(testParseIgnoresIncompleteValueDataEntry);
CPPUNIT_TEST(testCompleteEmpty);
CPPUNIT_TEST(testCompleteIncomplete);
@@ -57,10 +53,6 @@ public:
void testParseRejectsNonMapPacket();
void testParseRejectsInvalidValueDataPayload();
void testParseRejectsInvalidValuesField();
void testParseRejectsEmptyFieldSelection();
void testParseRejectsEmptyErrorArray();
void testParseRejectsNonMapRequest();
void testParseErrorOnlyMessageKeepsRequestFieldsEmpty();
void testParseIgnoresIncompleteValueDataEntry();
void testCompleteEmpty();
void testCompleteIncomplete();
-25
View File
@@ -137,29 +137,4 @@ ValueTester::testPushTypePreservedAfterEncrypt()
CPPUNIT_ASSERT(encrypted.isEncrypted());
}
void
ValueTester::testPushTypeJsonRoundTrip()
{
#ifdef OPENDHT_JSONCPP
dht::Value original {(const uint8_t*) "hello", 5};
original.id = 42;
original.priority = 1;
original.pushType = "videoCall";
dht::Value restored(original.toJson());
CPPUNIT_ASSERT_EQUAL(original.id, restored.id);
CPPUNIT_ASSERT_EQUAL(original.priority, restored.priority);
CPPUNIT_ASSERT_EQUAL(original.pushType, restored.pushType);
// No pushType: the field must stay absent from the JSON and empty after parse.
dht::Value plain {(const uint8_t*) "data", 4};
plain.id = 7;
auto json = plain.toJson();
CPPUNIT_ASSERT(!json.isMember("pt"));
dht::Value plainRestored(json);
CPPUNIT_ASSERT_EQUAL(std::string(), plainRestored.pushType);
#endif
}
} // namespace test
-2
View File
@@ -16,7 +16,6 @@ class ValueTester : public CppUnit::TestFixture
CPPUNIT_TEST(testPushTypeMsgpackRoundTrip);
CPPUNIT_TEST(testPushTypeAbsentAfterUnpack);
CPPUNIT_TEST(testPushTypePreservedAfterEncrypt);
CPPUNIT_TEST(testPushTypeJsonRoundTrip);
CPPUNIT_TEST_SUITE_END();
public:
@@ -39,7 +38,6 @@ public:
void testPushTypeMsgpackRoundTrip();
void testPushTypeAbsentAfterUnpack();
void testPushTypePreservedAfterEncrypt();
void testPushTypeJsonRoundTrip();
};
} // namespace test
-560
View File
@@ -1,560 +0,0 @@
// Copyright (c) 2014-2026 Savoir-faire Linux Inc.
// SPDX-License-Identifier: MIT
#include "test_valuecache.h"
#include "../src/value_cache.h"
namespace test {
CPPUNIT_TEST_SUITE_REGISTRATION(ValueCacheTester);
void
ValueCacheTester::setUp()
{}
void
ValueCacheTester::tearDown()
{}
void
ValueCacheTester::testUpdate()
{
int addCount = 0;
dht::ValueCache cache([&](const std::vector<std::shared_ptr<dht::Value>>& vals, bool expired) {
if (!expired)
addCount += vals.size();
});
dht::TypeStore types;
auto v1 = std::make_shared<dht::Value>();
v1->id = 1;
v1->seq = 1;
v1->data = {'A'};
auto v2 = std::make_shared<dht::Value>();
v2->id = 1;
v2->seq = 2;
v2->data = {'B'};
auto now = std::chrono::steady_clock::now();
// Add v1
cache.onValues({v1}, {}, {}, types, now);
CPPUNIT_ASSERT_EQUAL(1, addCount);
// Update v1 -> v2
cache.onValues({v2}, {}, {}, types, now);
CPPUNIT_ASSERT_EQUAL_MESSAGE("Callback should be called for update", 2, addCount);
}
void
ValueCacheTester::testUpdateExpiration()
{
int addCount = 0;
int expireCount = 0;
dht::ValueCache cache([&](const std::vector<std::shared_ptr<dht::Value>>& vals, bool expired) {
if (expired)
expireCount += vals.size();
else
addCount += vals.size();
});
dht::TypeStore types;
auto v1 = std::make_shared<dht::Value>();
v1->id = 1;
v1->seq = 1;
v1->data = {'A'};
auto v2 = std::make_shared<dht::Value>();
v2->id = 1;
v2->seq = 2;
v2->data = {'B'};
auto now = std::chrono::steady_clock::now();
// Add v1
cache.onValues({v1}, {}, {}, types, now);
CPPUNIT_ASSERT_EQUAL(1, addCount);
CPPUNIT_ASSERT_EQUAL(0, expireCount);
// Update v1 -> v2: only the new value is emitted as an add,
// no expire callback for the old value.
cache.onValues({v2}, {}, {}, types, now);
CPPUNIT_ASSERT_EQUAL(2, addCount);
CPPUNIT_ASSERT_EQUAL_MESSAGE("No expire callback on edit", 0, expireCount);
}
void
ValueCacheTester::testExpiration()
{
int expireCount = 0;
dht::ValueCache cache([&](const std::vector<std::shared_ptr<dht::Value>>& vals, bool expired) {
if (expired)
expireCount += vals.size();
});
dht::TypeStore types;
auto v1 = std::make_shared<dht::Value>();
v1->id = 10;
v1->data = {'A'};
// Default expiration is 10 minutes
auto now = std::chrono::steady_clock::now();
cache.onValues({v1}, {}, {}, types, now);
// Advance time past expiration
auto future = now + std::chrono::minutes(11);
// expireValues returns next expiration time
cache.expireValues(future);
CPPUNIT_ASSERT_EQUAL(1, expireCount);
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
}
void
ValueCacheTester::testRefresh()
{
int expireCount = 0;
dht::ValueCache cache([&](const std::vector<std::shared_ptr<dht::Value>>& vals, bool expired) {
if (expired)
expireCount += vals.size();
});
dht::TypeStore types;
auto v1 = std::make_shared<dht::Value>();
v1->id = 20;
v1->data = {'B'};
auto now = std::chrono::steady_clock::now();
cache.onValues({v1}, {}, {}, types, now);
// Refresh at +5 mins
auto later = now + std::chrono::minutes(5);
cache.onValues({}, {v1->id}, {}, types, later);
// Should not expire at +11 mins (original expiration was +10, so +10 from start)
// But refresh should extend it to +10 from 'later' = +15 from start.
auto checkTime = now + std::chrono::minutes(11);
cache.expireValues(checkTime);
CPPUNIT_ASSERT_EQUAL(0, expireCount);
CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
// Should expire at +16 mins
auto future = now + std::chrono::minutes(16);
cache.expireValues(future);
CPPUNIT_ASSERT_EQUAL(1, expireCount);
}
void
ValueCacheTester::testExplicitExpiration()
{
int expireCount = 0;
dht::ValueCache cache([&](const std::vector<std::shared_ptr<dht::Value>>& vals, bool expired) {
if (expired)
expireCount += vals.size();
});
dht::TypeStore types;
auto v1 = std::make_shared<dht::Value>();
v1->id = 30;
v1->data = {'C'};
auto now = std::chrono::steady_clock::now();
cache.onValues({v1}, {}, {}, types, now);
cache.onValues({}, {}, {v1->id}, types, now);
CPPUNIT_ASSERT_EQUAL(1, expireCount);
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
}
void
ValueCacheTester::testClear()
{
dht::ValueCache cache([](const std::vector<std::shared_ptr<dht::Value>>&, bool) {});
dht::TypeStore types;
auto now = std::chrono::steady_clock::now();
auto v1 = std::make_shared<dht::Value>();
v1->id = 1;
auto v2 = std::make_shared<dht::Value>();
v2->id = 2;
cache.onValues({v1, v2}, {}, {}, types, now);
CPPUNIT_ASSERT_EQUAL((size_t) 2, cache.size());
auto cbs = cache.clear();
// Execute callbacks
for (auto& cb : cbs)
cb();
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
}
void
ValueCacheTester::testSyncStatus()
{
std::vector<dht::ListenSyncStatus> statuses;
{
dht::ValueCache cache([](const std::vector<std::shared_ptr<dht::Value>>&, bool) {},
[&](dht::ListenSyncStatus status) { statuses.push_back(status); });
CPPUNIT_ASSERT_EQUAL((size_t) 1, statuses.size());
CPPUNIT_ASSERT(statuses.back() == dht::ListenSyncStatus::ADDED);
cache.onSynced(true);
CPPUNIT_ASSERT_EQUAL((size_t) 2, statuses.size());
CPPUNIT_ASSERT(statuses.back() == dht::ListenSyncStatus::SYNCED);
// Calling onSynced(true) again should not trigger the callback
cache.onSynced(true);
CPPUNIT_ASSERT_EQUAL((size_t) 2, statuses.size());
cache.onSynced(false);
CPPUNIT_ASSERT_EQUAL((size_t) 3, statuses.size());
CPPUNIT_ASSERT(statuses.back() == dht::ListenSyncStatus::UNSYNCED);
// Calling onSynced(false) again should not trigger the callback
cache.onSynced(false);
CPPUNIT_ASSERT_EQUAL((size_t) 3, statuses.size());
// Set to synced before destruction to test UNSYNCED on destruction
cache.onSynced(true);
CPPUNIT_ASSERT_EQUAL((size_t) 4, statuses.size());
CPPUNIT_ASSERT(statuses.back() == dht::ListenSyncStatus::SYNCED);
}
// Destructor should have been called
CPPUNIT_ASSERT_EQUAL((size_t) 6, statuses.size());
CPPUNIT_ASSERT(statuses[4] == dht::ListenSyncStatus::UNSYNCED);
CPPUNIT_ASSERT(statuses[5] == dht::ListenSyncStatus::REMOVED);
}
void
ValueCacheTester::testMaxValues()
{
dht::ValueCache cache([](const std::vector<std::shared_ptr<dht::Value>>&, bool) {});
dht::TypeStore types;
auto now = std::chrono::steady_clock::now();
// MAX_VALUES is 4096
for (int i = 0; i < 4100; ++i) {
auto v = std::make_shared<dht::Value>();
v->id = (dht::Value::Id) i + 1;
cache.onValues({v}, {}, {}, types, now + std::chrono::milliseconds(i));
}
// Should be capped at 4096
CPPUNIT_ASSERT_EQUAL((size_t) 4096, cache.size());
}
void
ValueCacheTester::testUpdateTypeExpiration()
{
// Test that when a value is updated with a different type, the expiration
// uses the new type's expiration period, not the old one.
int addCount = 0;
int expireCount = 0;
dht::ValueCache cache([&](const std::vector<std::shared_ptr<dht::Value>>& vals, bool expired) {
if (expired)
expireCount += vals.size();
else
addCount += vals.size();
});
// Register a custom type with a short expiration (2 minutes)
dht::TypeStore types;
dht::ValueType shortType(42, "short", std::chrono::minutes(2));
types.registerType(shortType);
// Register a custom type with a long expiration (30 minutes)
dht::ValueType longType(99, "long", std::chrono::minutes(30));
types.registerType(longType);
auto v1 = std::make_shared<dht::Value>();
v1->id = 1;
v1->type = 42; // short type: 2 min expiration
v1->data = {'A'};
auto now = std::chrono::steady_clock::now();
// Add v1 with short type
cache.onValues({v1}, {}, {}, types, now);
CPPUNIT_ASSERT_EQUAL(1, addCount);
// Update with new type (long expiration)
auto v2 = std::make_shared<dht::Value>();
v2->id = 1;
v2->seq = 1;
v2->type = 99; // long type: 30 min expiration
v2->data = {'B'};
cache.onValues({v2}, {}, {}, types, now);
CPPUNIT_ASSERT_EQUAL(2, addCount);
// At +3 minutes: should NOT be expired (long type has 30 min expiration)
// Before the fix, the old type (2 min) would be used, causing expiration here
expireCount = 0;
auto at3min = now + std::chrono::minutes(3);
cache.expireValues(at3min);
CPPUNIT_ASSERT_EQUAL_MESSAGE("Value updated to long-expiry type should NOT expire at 3 min", 0, expireCount);
CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
// At +31 minutes: should be expired
auto at31min = now + std::chrono::minutes(31);
cache.expireValues(at31min);
CPPUNIT_ASSERT_EQUAL(1, expireCount);
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
}
void
ValueCacheTester::testSameSeqEditIgnored()
{
// Like OpValueCache, an edit is only recognized by an increasing seq:
// a different payload with the same seq is a refresh, not an edit.
int addCount = 0;
dht::ValueCache cache([&](const std::vector<std::shared_ptr<dht::Value>>& vals, bool expired) {
if (!expired)
addCount += vals.size();
});
dht::TypeStore types;
auto now = std::chrono::steady_clock::now();
auto v1 = std::make_shared<dht::Value>();
v1->id = 1;
v1->seq = 3;
v1->data = {'A'};
cache.onValues({v1}, {}, {}, types, now);
CPPUNIT_ASSERT_EQUAL(1, addCount);
auto sameSeq = std::make_shared<dht::Value>();
sameSeq->id = 1;
sameSeq->seq = 3;
sameSeq->data = {'B'};
cache.onValues({sameSeq}, {}, {}, types, now);
CPPUNIT_ASSERT_EQUAL(1, addCount);
auto older = std::make_shared<dht::Value>();
older->id = 1;
older->seq = 2;
older->data = {'C'};
cache.onValues({older}, {}, {}, types, now);
CPPUNIT_ASSERT_EQUAL(1, addCount);
auto newer = std::make_shared<dht::Value>();
newer->id = 1;
newer->seq = 4;
newer->data = {'D'};
cache.onValues({newer}, {}, {}, types, now);
CPPUNIT_ASSERT_EQUAL(2, addCount);
}
void
ValueCacheTester::testShortExpirationRefreshLost()
{
// Demonstrates that a value with a short expiration type survives
// past its nominal expiration when the listen is synced, thanks to
// a proportional grace period (= typeExpiration).
//
// For a 5s type: effective expiration when synced = expiration + 5s = 10s
//
// Scenario:
// t=0 : Value V added → nominal expiration t+5s, effective t+10s
// t=6 : Past nominal but within grace → alive
// t=8 : Refresh arrives → V refreshed → expiration reset to t8+5s, effective t8+10s=t18s
// t=14: Still alive (within refreshed effective range)
// t=20: No more refreshes → past t8+10s=t18s → expired
int addCount = 0;
int expireCount = 0;
dht::ValueCache cache([&](const std::vector<std::shared_ptr<dht::Value>>& vals, bool expired) {
if (expired)
expireCount += vals.size();
else
addCount += vals.size();
});
// Register a type with a very short expiration (5 seconds)
dht::TypeStore types;
dht::ValueType shortType(100, "short_lived", std::chrono::seconds(5));
types.registerType(shortType);
auto v1 = std::make_shared<dht::Value>();
v1->id = 1;
v1->type = 100;
v1->data = {'V'};
auto t0 = std::chrono::steady_clock::now();
// t=0: Listen response arrives with V
cache.onValues({v1}, {}, {}, types, t0);
CPPUNIT_ASSERT_EQUAL(1, addCount);
CPPUNIT_ASSERT_EQUAL(0, expireCount);
CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
// Mark cache as synced (listen is active)
cache.onSynced(true);
// t=6: Past nominal 5s expiration, but grace (5s) gives effective 10s
auto t6 = t0 + std::chrono::seconds(6);
cache.expireValues(t6);
CPPUNIT_ASSERT_EQUAL_MESSAGE("Value should survive past nominal expiration thanks to proportional grace",
0,
expireCount);
CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
// t=8: Value refresh arrives → expiration reset to t8+5s=t13s, effective t8+10s=t18s
auto t8 = t0 + std::chrono::seconds(8);
cache.onValues({}, {v1->id}, {}, types, t8);
CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
// t=14: Within refreshed effective range (t18s) → alive
auto t14 = t0 + std::chrono::seconds(14);
cache.expireValues(t14);
CPPUNIT_ASSERT_EQUAL_MESSAGE("Value should survive past short expiration after refresh", 0, expireCount);
CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
// t=20: No more refreshes → past effective t18s → must expire
auto t20 = t0 + std::chrono::seconds(20);
cache.expireValues(t20);
CPPUNIT_ASSERT_EQUAL_MESSAGE("Value must eventually expire even while synced if server stops refreshing",
1,
expireCount);
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
}
void
ValueCacheTester::testShortExpirationHighChurn()
{
// Simulates high churn: multiple rapid add/expire/refresh cycles with
// short-lived values, verifying the cache stays consistent.
int addCount = 0;
int expireCount = 0;
dht::ValueCache cache([&](const std::vector<std::shared_ptr<dht::Value>>& vals, bool expired) {
if (expired)
expireCount += vals.size();
else
addCount += vals.size();
});
dht::TypeStore types;
dht::ValueType shortType(101, "fast_churn", std::chrono::seconds(3));
types.registerType(shortType);
auto now = std::chrono::steady_clock::now();
// Simulate 10 churn cycles: add value, advance time, refresh, advance, expire
for (int cycle = 0; cycle < 10; cycle++) {
auto v = std::make_shared<dht::Value>();
v->id = static_cast<dht::Value::Id>(500 + cycle);
v->type = 101;
v->data = {static_cast<uint8_t>('A' + cycle)};
auto t = now + std::chrono::seconds(cycle * 10);
// Add the value
cache.onValues({v}, {}, {}, types, t);
// Refresh halfway through
auto tRefresh = t + std::chrono::seconds(2);
cache.onValues({}, {v->id}, {}, types, tRefresh);
// Explicit expire from server
auto tExpire = t + std::chrono::seconds(8);
cache.onValues({}, {}, {v->id}, types, tExpire);
}
// All 10 values were added and explicitly expired
CPPUNIT_ASSERT_EQUAL(10, addCount);
CPPUNIT_ASSERT_EQUAL(10, expireCount);
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
}
void
ValueCacheTester::testGracePeriodWhileSynced()
{
// Verify that when the listen is synced, the proportional grace period
// (= typeExpiration) delays time-based expiration but doesn't prevent
// it entirely. For a 2s type: effective expiration = 2s + 2s = 4s.
int expireCount = 0;
dht::ValueCache cache([&](const std::vector<std::shared_ptr<dht::Value>>& vals, bool expired) {
if (expired)
expireCount += vals.size();
});
dht::TypeStore types;
dht::ValueType shortType(110, "short", std::chrono::seconds(2));
types.registerType(shortType);
auto v1 = std::make_shared<dht::Value>();
v1->id = 1;
v1->type = 110;
v1->data = {'X'};
auto t0 = std::chrono::steady_clock::now();
cache.onValues({v1}, {}, {}, types, t0);
CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
// Mark as synced
cache.onSynced(true);
// t=3: Past nominal 2s, within effective 4s → grace protects
auto t3 = t0 + std::chrono::seconds(3);
cache.expireValues(t3);
CPPUNIT_ASSERT_EQUAL_MESSAGE("Synced cache: grace period should prevent early expiry", 0, expireCount);
CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
// t=5: Past effective 4s (2s + 2s grace) → must expire
auto t5 = t0 + std::chrono::seconds(5);
cache.expireValues(t5);
CPPUNIT_ASSERT_EQUAL_MESSAGE("Synced cache: value must eventually expire after grace period", 1, expireCount);
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
}
void
ValueCacheTester::testUnsyncExpiresImmediately()
{
// When the cache goes from synced to unsynced, the grace period is
// removed and already-past-due values expire on the next check.
// Type expiration = 5s. Grace when synced = 5s → effective = 10s.
int expireCount = 0;
dht::ValueCache cache([&](const std::vector<std::shared_ptr<dht::Value>>& vals, bool expired) {
if (expired)
expireCount += vals.size();
});
dht::TypeStore types;
dht::ValueType shortType(120, "short", std::chrono::seconds(5));
types.registerType(shortType);
auto v1 = std::make_shared<dht::Value>();
v1->id = 1;
v1->type = 120;
v1->data = {'U'};
auto t0 = std::chrono::steady_clock::now();
cache.onValues({v1}, {}, {}, types, t0);
cache.onSynced(true);
// t=7: past nominal 5s, within effective 10s while synced → alive
auto t7 = t0 + std::chrono::seconds(7);
cache.expireValues(t7);
CPPUNIT_ASSERT_EQUAL(0, expireCount);
// Now unsync (node went away / connectivity loss)
cache.onSynced(false);
// Same time point t=7: past nominal 5s, no grace → expires
cache.expireValues(t7);
CPPUNIT_ASSERT_EQUAL_MESSAGE("Unsynced cache should immediately expire past-due values", 1, expireCount);
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
}
} // namespace test
-49
View File
@@ -1,49 +0,0 @@
// Copyright (c) 2014-2026 Savoir-faire Linux Inc.
// SPDX-License-Identifier: MIT
#pragma once
#include <cppunit/TestFixture.h>
#include <cppunit/extensions/HelperMacros.h>
namespace test {
class ValueCacheTester : public CppUnit::TestFixture
{
CPPUNIT_TEST_SUITE(ValueCacheTester);
CPPUNIT_TEST(testUpdate);
CPPUNIT_TEST(testUpdateExpiration);
CPPUNIT_TEST(testExpiration);
CPPUNIT_TEST(testRefresh);
CPPUNIT_TEST(testExplicitExpiration);
CPPUNIT_TEST(testClear);
CPPUNIT_TEST(testSyncStatus);
CPPUNIT_TEST(testMaxValues);
CPPUNIT_TEST(testUpdateTypeExpiration);
CPPUNIT_TEST(testSameSeqEditIgnored);
CPPUNIT_TEST(testShortExpirationRefreshLost);
CPPUNIT_TEST(testShortExpirationHighChurn);
CPPUNIT_TEST(testGracePeriodWhileSynced);
CPPUNIT_TEST(testUnsyncExpiresImmediately);
CPPUNIT_TEST_SUITE_END();
public:
void setUp();
void tearDown();
void testUpdate();
void testUpdateExpiration();
void testExpiration();
void testRefresh();
void testExplicitExpiration();
void testClear();
void testSyncStatus();
void testMaxValues();
void testUpdateTypeExpiration();
void testSameSeqEditIgnored();
void testShortExpirationRefreshLost();
void testShortExpirationHighChurn();
void testGracePeriodWhileSynced();
void testUnsyncExpiresImmediately();
};
} // namespace test
+1 -1
View File
@@ -1,7 +1,7 @@
{
"default-registry": {
"kind": "git",
"baseline": "f3e10653cc27d62a37a3763cd84b38bca07c6075",
"baseline": "66c0373dc7fca549e5803087b9487edfe3aca0a1",
"repository": "https://github.com/microsoft/vcpkg"
},
"registries": [
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "opendht",
"version-string": "4.3.0",
"version-string": "4.0.0",
"port-version": 1,
"description": "OpenDHT: A lightweight C++17 Distributed Hash Table library.",
"homepage": "https://opendht.net",